release: v4.9.0 — full redesign + reworked offline mode
Ground-up visual redesign across the entire surface (landing, connection setup, chat header, security verification report, file transfer, PWA install/update/offline dialogs). Offline reworked: store-and-forward queue (send while offline → queued, delivered on reconnect), WhatsApp-style per-message delivery status (sending/sent/delivered/not-sent) via delivery receipts, offline buffering for messages to an offline peer, and offline state no longer leaking into the connection indicator. Resilient chunked file transfer with retransmission and auto-save. README + screenshots added.
This commit is contained in:
@@ -135,147 +135,98 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
|
||||
return version;
|
||||
};
|
||||
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
// Update modal — translated from the Claude Design component
|
||||
// (Update Notification.dc.html). Styling is inline so it tracks the design.
|
||||
return React.createElement(React.Fragment, null, [
|
||||
// Main application content
|
||||
children,
|
||||
|
||||
// Update modal window
|
||||
|
||||
updateState.showModal && React.createElement('div', {
|
||||
key: 'update-modal',
|
||||
className: 'fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm',
|
||||
style: {
|
||||
animation: 'fadeIn 0.3s ease-in-out'
|
||||
position: 'fixed', inset: 0, zIndex: 9999, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: '24px', background: 'rgba(8,8,10,0.55)', backdropFilter: 'blur(3px)', WebkitBackdropFilter: 'blur(3px)',
|
||||
animation: 'unFade .3s ease', fontFamily: SANS
|
||||
}
|
||||
}, [
|
||||
React.createElement('style', { key: 'kf', dangerouslySetInnerHTML: { __html:
|
||||
'@keyframes unPop{from{opacity:0;transform:scale(.96) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}' +
|
||||
'@keyframes unFade{from{opacity:0}to{opacity:1}}' +
|
||||
'@keyframes unSpin{to{transform:rotate(360deg)}}'
|
||||
} }),
|
||||
|
||||
React.createElement('div', {
|
||||
key: 'modal-content',
|
||||
className: 'bg-white dark:bg-gray-800 rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 border border-gray-200 dark:border-gray-700',
|
||||
key: 'card',
|
||||
style: {
|
||||
animation: 'slideUp 0.3s ease-out'
|
||||
position: 'relative', width: '440px', maxWidth: 'calc(100vw - 48px)', borderRadius: '22px',
|
||||
background: '#121214', border: '1px solid rgba(255,255,255,0.08)', padding: '36px 32px 28px',
|
||||
textAlign: 'center', boxShadow: '0 30px 70px rgba(0,0,0,0.6)', animation: 'unPop .32s cubic-bezier(.2,.7,.3,1)'
|
||||
}
|
||||
}, [
|
||||
// Header
|
||||
// spinning update icon
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: 'text-center mb-6'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'icon',
|
||||
className: 'w-16 h-16 mx-auto mb-4 bg-blue-500/10 rounded-full flex items-center justify-center'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon-fa',
|
||||
className: 'fas fa-sync-alt text-blue-500 text-2xl animate-spin'
|
||||
})
|
||||
]),
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
className: 'text-2xl font-bold text-gray-900 dark:text-white mb-2'
|
||||
}, 'Update Available'),
|
||||
React.createElement('p', {
|
||||
key: 'subtitle',
|
||||
className: 'text-gray-600 dark:text-gray-300 text-sm'
|
||||
}, 'A new version of the application has been detected')
|
||||
]),
|
||||
|
||||
// Version information
|
||||
key: 'icon',
|
||||
style: { display: 'inline-flex', width: '64px', height: '64px', borderRadius: '50%', alignItems: 'center', justifyContent: 'center', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.3)', marginBottom: '20px' }
|
||||
}, React.createElement('svg', {
|
||||
width: 28, height: 28, viewBox: '0 0 24 24', fill: 'none', stroke: '#f0892a', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
style: { animation: 'unSpin 6s linear infinite' },
|
||||
dangerouslySetInnerHTML: { __html: '<path d="M21 8a8.5 8.5 0 0 0-15.6-2.5M3 4v4h4"/><path d="M3 16a8.5 8.5 0 0 0 15.6 2.5M21 20v-4h-4"/>' }
|
||||
})),
|
||||
|
||||
React.createElement('h2', { key: 'title', style: { margin: '0 0 9px', fontSize: '26px', fontWeight: 800, letterSpacing: '-0.7px', color: '#f4f4f6' } }, 'Update available'),
|
||||
React.createElement('p', { key: 'sub', style: { margin: '0 0 24px', fontSize: '14.5px', lineHeight: 1.55, color: '#9a9aa2' } }, 'A newer version of SecureBit has been detected.'),
|
||||
|
||||
// version comparison
|
||||
React.createElement('div', {
|
||||
key: 'version-info',
|
||||
className: 'bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-6 space-y-2'
|
||||
key: 'vbox',
|
||||
style: { borderRadius: '14px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', padding: '16px 18px', marginBottom: '24px', textAlign: 'left' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'current',
|
||||
className: 'flex justify-between items-center'
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'current-label',
|
||||
className: 'text-sm text-gray-600 dark:text-gray-400'
|
||||
}, 'Current version:'),
|
||||
React.createElement('span', {
|
||||
key: 'current-value',
|
||||
className: 'text-sm font-mono text-gray-900 dark:text-white'
|
||||
}, formatVersion(updateState.currentVersion))
|
||||
React.createElement('div', { key: 'cur', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '14px', padding: '5px 0' } }, [
|
||||
React.createElement('span', { key: 'l', style: { fontSize: '13.5px', fontWeight: 500, color: '#8a8a92' } }, 'Current version'),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 500, color: '#9a9aa2', whiteSpace: 'nowrap' } }, formatVersion(updateState.currentVersion))
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'new',
|
||||
className: 'flex justify-between items-center'
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'new-label',
|
||||
className: 'text-sm text-gray-600 dark:text-gray-400'
|
||||
}, 'New version:'),
|
||||
React.createElement('span', {
|
||||
key: 'new-value',
|
||||
className: 'text-sm font-mono text-blue-600 dark:text-blue-400 font-semibold'
|
||||
}, formatVersion(updateState.newVersion))
|
||||
React.createElement('div', { key: 'sep', style: { height: '1px', background: 'rgba(255,255,255,0.05)', margin: '4px 0' } }),
|
||||
React.createElement('div', { key: 'new', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '14px', padding: '5px 0' } }, [
|
||||
React.createElement('span', { key: 'l', style: { display: 'inline-flex', alignItems: 'center', gap: '8px', fontSize: '13.5px', fontWeight: 600, color: '#e8e8eb' } }, [
|
||||
React.createElement('span', { key: 'd', style: { width: '6px', height: '6px', borderRadius: '50%', background: '#f0892a' } }),
|
||||
'New version'
|
||||
]),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 700, color: '#f0892a', whiteSpace: 'nowrap' } }, formatVersion(updateState.newVersion))
|
||||
])
|
||||
]),
|
||||
|
||||
// Update progress
|
||||
updateState.isUpdating && React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: 'mb-6'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-bar',
|
||||
className: 'w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 mb-2'
|
||||
}, [
|
||||
|
||||
// progress while updating, otherwise the action buttons
|
||||
updateState.isUpdating
|
||||
? React.createElement('div', { key: 'progress' }, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-fill',
|
||||
className: 'bg-blue-500 h-2.5 rounded-full transition-all duration-300',
|
||||
style: {
|
||||
width: `${updateState.progress}%`
|
||||
}
|
||||
})
|
||||
]),
|
||||
React.createElement('p', {
|
||||
key: 'progress-text',
|
||||
className: 'text-center text-sm text-gray-600 dark:text-gray-400'
|
||||
}, `${updateState.progress}%`)
|
||||
]),
|
||||
|
||||
// Action buttons
|
||||
!updateState.isUpdating && React.createElement('div', {
|
||||
key: 'actions',
|
||||
className: 'flex gap-3'
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'update-btn',
|
||||
onClick: handleForceUpdate,
|
||||
className: 'flex-1 bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2',
|
||||
disabled: updateState.isUpdating
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'update-icon',
|
||||
className: 'fas fa-download'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'update-text'
|
||||
}, 'Update Now')
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'close-btn',
|
||||
onClick: handleCloseModal,
|
||||
className: 'px-4 py-3 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors duration-200',
|
||||
disabled: updateState.isUpdating
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'close-icon',
|
||||
className: 'fas fa-times'
|
||||
})
|
||||
key: 'bar',
|
||||
style: { width: '100%', height: '8px', borderRadius: '99px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden', marginBottom: '10px' }
|
||||
}, React.createElement('div', { key: 'fill', style: { height: '100%', width: `${updateState.progress}%`, background: 'linear-gradient(90deg,#3ecf8e,#f0892a)', transition: 'width .3s ease' } })),
|
||||
React.createElement('p', { key: 't', style: { margin: 0, fontFamily: MONO, fontSize: '12px', color: '#8a8a92' } }, `Updating… ${updateState.progress}%`)
|
||||
])
|
||||
: React.createElement('div', { key: 'actions', style: { display: 'flex', alignItems: 'center', gap: '12px' } }, [
|
||||
React.createElement('button', {
|
||||
key: 'update',
|
||||
onClick: handleForceUpdate,
|
||||
style: { flex: 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '10px', padding: '15px 20px', borderRadius: '13px', border: 'none', background: '#f0892a', color: '#1a0f04', fontFamily: 'inherit', fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px', cursor: 'pointer', boxShadow: '0 8px 24px rgba(240,137,42,0.28)', transition: 'all .2s cubic-bezier(.2,.7,.3,1)' },
|
||||
onMouseEnter: (e) => { e.currentTarget.style.background = '#ff9637'; e.currentTarget.style.transform = 'translateY(-2px)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.background = '#f0892a'; e.currentTarget.style.transform = 'none'; }
|
||||
}, [
|
||||
React.createElement('svg', { key: 'i', width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.1, strokeLinecap: 'round', strokeLinejoin: 'round', dangerouslySetInnerHTML: { __html: '<path d="M12 3v11"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 20h14"/>' } }),
|
||||
'Update now'
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'later',
|
||||
onClick: handleCloseModal,
|
||||
title: 'Later',
|
||||
style: { flex: 'none', width: '50px', height: '50px', borderRadius: '13px', display: 'grid', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.025)', color: '#9a9aa2', cursor: 'pointer', transition: 'all .18s cubic-bezier(.2,.7,.3,1)' },
|
||||
onMouseEnter: (e) => { e.currentTarget.style.color = '#e5727a'; e.currentTarget.style.borderColor = 'rgba(229,114,122,0.4)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.color = '#9a9aa2'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'; }
|
||||
}, React.createElement('svg', { width: 17, height: 17, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.1, strokeLinecap: 'round', strokeLinejoin: 'round', dangerouslySetInnerHTML: { __html: '<path d="M6 6l12 12M18 6L6 18"/>' } }))
|
||||
])
|
||||
]),
|
||||
|
||||
// Update indicator
|
||||
updateState.isUpdating && React.createElement('div', {
|
||||
key: 'updating',
|
||||
className: 'text-center'
|
||||
}, [
|
||||
React.createElement('p', {
|
||||
key: 'updating-text',
|
||||
className: 'text-sm text-gray-600 dark:text-gray-400'
|
||||
}, 'Update in progress...')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -1,86 +1,175 @@
|
||||
// "Trusted by our partners" — partner ecosystem section.
|
||||
// Translated from the Claude Design component (Partners.dc.html) into the
|
||||
// project's React.createElement style: a full-bleed dark band with partner
|
||||
// cards plus a dashed "Become a partner" invite card.
|
||||
const BecomePartner = () => {
|
||||
const partners = [
|
||||
{ id: 'aegis', name: 'Aegis', logo: 'logo/aegis.png', isColor: true, url: 'https://aegis-investment.com/' },
|
||||
{ id: 'furi', name: 'Furi Labs', logo: 'logo/furi.png', isColor: true, url: 'https://furilabs.com/' }
|
||||
];
|
||||
const [isMobile, setIsMobile] = React.useState(
|
||||
typeof window !== 'undefined' && window.matchMedia('(max-width:767px)').matches
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mq = window.matchMedia('(max-width:767px)');
|
||||
const onChange = () => setIsMobile(mq.matches);
|
||||
mq.addEventListener ? mq.addEventListener('change', onChange) : mq.addListener(onChange);
|
||||
return () => {
|
||||
mq.removeEventListener ? mq.removeEventListener('change', onChange) : mq.removeListener(onChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ACCENT = '#f0892a';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
const formUrl = 'https://docs.google.com/forms/d/e/1FAIpQLSc9ijV9PCoyXkus6vEx1OWwvwAsLq8fKS6-H5BmX-c-bvia6w/viewform?usp=dialog';
|
||||
|
||||
return React.createElement('div', { className: "mt-20 px-6" }, [
|
||||
// Header "Trusted by our partners"
|
||||
React.createElement('div', { key: 'header', className: "text-center max-w-3xl mx-auto mb-8" }, [
|
||||
React.createElement('h3', { key: 'title', className: "text-3xl font-bold text-primary mb-3" }, 'Trusted by our partners')
|
||||
const partners = [
|
||||
{
|
||||
id: 'aegis',
|
||||
name: 'Aegis Investment',
|
||||
logo: 'logo/aegis.png',
|
||||
logoHeight: '42px',
|
||||
url: 'https://aegis-investment.com/',
|
||||
desc: 'Capital partner securing confidential financial communications across its portfolio.',
|
||||
role: 'Strategic backer',
|
||||
delay: '.5s'
|
||||
},
|
||||
{
|
||||
id: 'furi',
|
||||
name: 'FuriLabs',
|
||||
logo: 'logo/furi.png',
|
||||
logoHeight: '54px',
|
||||
url: 'https://furilabs.com/',
|
||||
desc: 'Privacy-first Linux phones that ship SecureBit as a default secure channel.',
|
||||
role: 'Technology partner',
|
||||
delay: '.56s'
|
||||
}
|
||||
];
|
||||
|
||||
const svg = (inner, size, stroke, sw) =>
|
||||
React.createElement('svg', {
|
||||
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
|
||||
stroke, strokeWidth: sw, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
dangerouslySetInnerHTML: { __html: inner }
|
||||
});
|
||||
|
||||
const roleTag = (role) => React.createElement('span', {
|
||||
key: 'role',
|
||||
style: { fontFamily: MONO, fontSize: '10.5px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.2px', padding: '6px 11px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.07)', background: 'rgba(255,255,255,0.025)', whiteSpace: 'nowrap' }
|
||||
}, role);
|
||||
|
||||
const partnerCard = (p) => React.createElement('a', {
|
||||
key: p.id,
|
||||
href: p.url,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
style: {
|
||||
flex: '1 1 320px', minWidth: isMobile ? 'auto' : '300px',
|
||||
borderRadius: '18px', background: '#141416', border: '1px solid rgba(255,255,255,0.06)',
|
||||
padding: '30px 30px 26px', display: 'flex', flexDirection: 'column',
|
||||
textDecoration: 'none', color: 'inherit',
|
||||
transition: 'transform .28s cubic-bezier(.2,.7,.3,1), border-color .28s cubic-bezier(.2,.7,.3,1)',
|
||||
animation: `ptUp ${p.delay} cubic-bezier(.2,.7,.3,1)`
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.transform = 'translateY(-4px)'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.13)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.06)'; }
|
||||
}, [
|
||||
React.createElement('div', { key: 'logo', style: { display: 'flex', alignItems: 'center', marginBottom: '30px', height: '54px' } },
|
||||
React.createElement('img', {
|
||||
src: p.logo, alt: p.name,
|
||||
style: { height: p.logoHeight, width: 'auto', maxWidth: '190px', objectFit: 'contain', display: 'block' }
|
||||
})
|
||||
),
|
||||
React.createElement('h3', { key: 'name', style: { margin: '0 0 9px', fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' } }, p.name),
|
||||
React.createElement('p', { key: 'desc', style: { margin: '0 0 22px', fontSize: '14.5px', lineHeight: 1.6, color: '#9a9aa2' } }, p.desc),
|
||||
React.createElement('div', { key: 'foot', style: { marginTop: 'auto', paddingTop: '6px', display: 'flex', alignItems: 'center', gap: '12px' } }, [
|
||||
roleTag(p.role)
|
||||
])
|
||||
]);
|
||||
|
||||
const inviteCard = React.createElement('a', {
|
||||
key: 'invite',
|
||||
href: formUrl,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
style: {
|
||||
flex: '1 1 320px', minWidth: isMobile ? 'auto' : '300px',
|
||||
borderRadius: '18px', background: '#111113', border: '1px dashed rgba(255,255,255,0.12)',
|
||||
padding: '30px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
|
||||
textDecoration: 'none', color: 'inherit',
|
||||
transition: 'border-color .28s cubic-bezier(.2,.7,.3,1)',
|
||||
animation: 'ptUp .62s cubic-bezier(.2,.7,.3,1)'
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.borderColor = 'rgba(240,137,42,0.4)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.12)'; }
|
||||
}, [
|
||||
React.createElement('div', { key: 'top' }, [
|
||||
React.createElement('div', {
|
||||
key: 'icon',
|
||||
style: { width: '48px', height: '48px', borderRadius: '13px', display: 'grid', placeItems: 'center', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.28)', marginBottom: '24px' }
|
||||
}, svg('<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/>', 23, ACCENT, 1.9)),
|
||||
React.createElement('h3', { key: 'title', style: { margin: '0 0 8px', fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' } }, 'Become a partner'),
|
||||
React.createElement('p', { key: 'desc', style: { margin: 0, fontSize: '14.5px', lineHeight: 1.6, color: '#8a8a92' } }, "Building privacy hardware or infrastructure? Let's integrate SecureBit.")
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'btn',
|
||||
style: {
|
||||
marginTop: '26px', width: '100%', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '10px',
|
||||
padding: '15px 20px', borderRadius: '12px', border: 'none', background: ACCENT, color: '#1a0f04',
|
||||
fontFamily: SANS, fontSize: '15px', fontWeight: 700, cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(240,137,42,0.28)', boxSizing: 'border-box',
|
||||
transition: 'background .2s cubic-bezier(.2,.7,.3,1), transform .2s cubic-bezier(.2,.7,.3,1)'
|
||||
}
|
||||
}, [
|
||||
'Start a conversation',
|
||||
svg('<path d="M5 12h14M13 6l6 6-6 6"/>', 17, 'currentColor', 2.2)
|
||||
])
|
||||
]);
|
||||
|
||||
const inner = React.createElement('div', {
|
||||
key: 'inner',
|
||||
style: { maxWidth: '1240px', margin: '0 auto', padding: isMobile ? '0 18px' : '0 40px' }
|
||||
}, [
|
||||
// Header
|
||||
React.createElement('div', { key: 'head', style: { marginBottom: '44px' } }, [
|
||||
React.createElement('div', {
|
||||
key: 'eyebrow',
|
||||
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.6px', marginBottom: '14px' }
|
||||
}, 'Partners & ecosystem'),
|
||||
React.createElement('div', {
|
||||
key: 'row',
|
||||
style: { display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: '32px', flexWrap: 'wrap' }
|
||||
}, [
|
||||
React.createElement('h2', {
|
||||
key: 'h2',
|
||||
style: { margin: 0, fontSize: isMobile ? '30px' : '40px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.04, color: '#f4f4f6' }
|
||||
}, 'Trusted by our partners'),
|
||||
React.createElement('p', {
|
||||
key: 'sub',
|
||||
style: { margin: '0 0 4px', fontSize: '15px', lineHeight: 1.55, color: '#8a8a92', maxWidth: '360px' }
|
||||
}, "A small, vetted circle — no pay-to-list logos and no badges we can't stand behind.")
|
||||
])
|
||||
]),
|
||||
|
||||
// First divider line with fade
|
||||
// Cards
|
||||
React.createElement('div', {
|
||||
key: 'divider-1',
|
||||
className: "h-px w-full max-w-3xl mx-auto mb-8 bg-gradient-to-r from-transparent via-zinc-700 to-transparent"
|
||||
}),
|
||||
|
||||
// Partner Logos
|
||||
React.createElement('div', {
|
||||
key: 'partners-row',
|
||||
className: "flex justify-center items-center flex-wrap gap-12 mb-8"
|
||||
},
|
||||
partners.map(partner =>
|
||||
React.createElement('a', {
|
||||
key: partner.id,
|
||||
href: partner.url,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
className: "flex items-center justify-center cursor-pointer hover:opacity-100 transition-opacity duration-300"
|
||||
}, [
|
||||
React.createElement('img', {
|
||||
key: 'logo',
|
||||
src: partner.logo,
|
||||
alt: partner.name,
|
||||
className: "h-12 sm:h-16 opacity-80 hover:opacity-100 transition-opacity duration-300",
|
||||
style: partner.isColor ? {
|
||||
filter: 'grayscale(100%) brightness(1.2) contrast(1.1)',
|
||||
WebkitFilter: 'grayscale(100%) brightness(1.2) contrast(1.1)'
|
||||
} : {}
|
||||
})
|
||||
])
|
||||
)
|
||||
),
|
||||
|
||||
// Second divider line with fade
|
||||
React.createElement('div', {
|
||||
key: 'divider-2',
|
||||
className: "h-px w-full max-w-3xl mx-auto mb-8 bg-gradient-to-r from-transparent via-zinc-700 to-transparent"
|
||||
}),
|
||||
|
||||
// Section with subtitle and text
|
||||
React.createElement('div', { key: 'cta-section', className: "text-center max-w-3xl mx-auto" }, [
|
||||
React.createElement('h4', {
|
||||
key: 'subtitle',
|
||||
className: "text-base font-semibold text-primary mb-4"
|
||||
}, 'Technology & Community Partners'),
|
||||
|
||||
React.createElement('p', {
|
||||
key: 'description',
|
||||
className: "text-secondary text-sm mb-6"
|
||||
}, 'Interested in partnering with us?'),
|
||||
|
||||
// CTA Button with 3D glass effect
|
||||
React.createElement('div', {
|
||||
key: 'button-wrapper',
|
||||
className: "button-container flex justify-center"
|
||||
}, [
|
||||
React.createElement('a', {
|
||||
key: 'button-link',
|
||||
href: formUrl,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
className: "button"
|
||||
}, [
|
||||
React.createElement('span', { key: 'text' }, 'Become a Partner')
|
||||
])
|
||||
])
|
||||
key: 'cards',
|
||||
style: { display: 'flex', gap: '18px', alignItems: 'stretch', flexWrap: 'wrap' }
|
||||
}, [
|
||||
...partners.map(partnerCard),
|
||||
inviteCard
|
||||
])
|
||||
]);
|
||||
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
width: '100%', color: '#e8e8eb', fontFamily: SANS,
|
||||
padding: isMobile ? '48px 0' : '72px 0',
|
||||
background: 'radial-gradient(1100px 640px at 50% -6%, rgba(240,137,42,0.055), transparent 62%), #0f0f11'
|
||||
}
|
||||
}, [
|
||||
React.createElement('style', { key: 'kf', dangerouslySetInnerHTML: { __html: '@keyframes ptUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}' } }),
|
||||
inner
|
||||
]);
|
||||
};
|
||||
|
||||
window.BecomePartner = BecomePartner;
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// "Join the future of privacy" — community / open-source call-to-action.
|
||||
// Translated from the Claude Design component (Community CTA.dc.html): a centered
|
||||
// glowing card with GitHub + Feedback actions on a full-bleed dark band.
|
||||
const CommunityCTA = () => {
|
||||
const [isMobile, setIsMobile] = React.useState(
|
||||
typeof window !== 'undefined' && window.matchMedia('(max-width:767px)').matches
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mq = window.matchMedia('(max-width:767px)');
|
||||
const onChange = () => setIsMobile(mq.matches);
|
||||
mq.addEventListener ? mq.addEventListener('change', onChange) : mq.addListener(onChange);
|
||||
return () => {
|
||||
mq.removeEventListener ? mq.removeEventListener('change', onChange) : mq.removeListener(onChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ACCENT = '#f0892a';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
const githubUrl = 'https://github.com/SecureBitChat/securebit-chat/';
|
||||
const feedbackUrl = 'mailto:lockbitchat@tutanota.com';
|
||||
|
||||
const githubBtn = React.createElement('a', {
|
||||
key: 'gh',
|
||||
href: githubUrl,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
style: {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '11px', padding: '15px 26px',
|
||||
borderRadius: '13px', background: ACCENT, color: '#1a0f04', textDecoration: 'none',
|
||||
fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px',
|
||||
boxShadow: '0 8px 24px rgba(240,137,42,0.28)', whiteSpace: 'nowrap',
|
||||
transition: 'all .2s cubic-bezier(.2,.7,.3,1)'
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.background = '#ff9637'; e.currentTarget.style.transform = 'translateY(-2px)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.background = ACCENT; e.currentTarget.style.transform = 'none'; }
|
||||
}, [
|
||||
React.createElement('svg', {
|
||||
key: 'i', width: 20, height: 20, viewBox: '0 0 24 24', fill: 'currentColor',
|
||||
dangerouslySetInnerHTML: { __html: '<path d="M12 2C6.48 2 2 6.58 2 12.26c0 4.5 2.87 8.32 6.84 9.67.5.09.68-.22.68-.49 0-.24-.01-.87-.01-1.71-2.78.62-3.37-1.36-3.37-1.36-.46-1.18-1.11-1.5-1.11-1.5-.91-.63.07-.62.07-.62 1 .07 1.53 1.05 1.53 1.05.89 1.56 2.34 1.11 2.91.85.09-.66.35-1.11.63-1.36-2.22-.26-4.55-1.14-4.55-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.27 2.75 1.05a9.3 9.3 0 0 1 5 0c1.91-1.32 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.81-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.47-.01 2.81 0 .27.18.59.69.49A10.02 10.02 0 0 0 22 12.26C22 6.58 17.52 2 12 2z"/>' }
|
||||
}),
|
||||
'GitHub Repository'
|
||||
]);
|
||||
|
||||
const feedbackBtn = React.createElement('a', {
|
||||
key: 'fb',
|
||||
href: feedbackUrl,
|
||||
rel: 'noopener noreferrer',
|
||||
style: {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '11px', padding: '15px 26px',
|
||||
borderRadius: '13px', background: 'rgba(255,255,255,0.03)', color: '#e8e8eb', textDecoration: 'none',
|
||||
fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px',
|
||||
border: '1px solid rgba(255,255,255,0.1)', whiteSpace: 'nowrap',
|
||||
transition: 'all .2s cubic-bezier(.2,.7,.3,1)'
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.24)'; e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'; e.currentTarget.style.background = 'rgba(255,255,255,0.03)'; }
|
||||
}, [
|
||||
React.createElement('svg', {
|
||||
key: 'i', width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
||||
strokeWidth: 1.9, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
dangerouslySetInnerHTML: { __html: '<path d="M21 11.5a8 8 0 0 1-11.6 7.1L4 20l1.4-5.3A8 8 0 1 1 21 11.5z"/><path d="M8.5 11h7M8.5 14h4.5"/>' }
|
||||
}),
|
||||
'Feedback'
|
||||
]);
|
||||
|
||||
const chip = (label) => React.createElement('span', {
|
||||
key: label,
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '7px' }
|
||||
}, [
|
||||
React.createElement('span', { key: 'd', style: { width: '5px', height: '5px', borderRadius: '50%', background: '#3ecf8e' } }),
|
||||
label
|
||||
]);
|
||||
|
||||
const card = React.createElement('div', {
|
||||
key: 'card',
|
||||
style: {
|
||||
position: 'relative', overflow: 'hidden', maxWidth: '860px', width: '100%',
|
||||
borderRadius: '24px',
|
||||
background: 'radial-gradient(700px 360px at 50% 0%, rgba(240,137,42,0.1), transparent 65%), #121214',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
padding: isMobile ? '40px 24px 36px' : '56px 56px 48px',
|
||||
textAlign: 'center', boxShadow: '0 24px 60px rgba(0,0,0,0.4)'
|
||||
}
|
||||
}, [
|
||||
// hairline accent
|
||||
React.createElement('div', {
|
||||
key: 'hairline',
|
||||
style: { position: 'absolute', top: 0, left: '50%', transform: 'translateX(-50%)', width: '180px', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(240,137,42,0.7), transparent)' }
|
||||
}),
|
||||
// brand mark (same SVG as the header — no border or background)
|
||||
React.createElement('img', {
|
||||
key: 'icon',
|
||||
src: '/logo/securebit-mark.svg',
|
||||
alt: 'SecureBit',
|
||||
style: { display: 'inline-block', width: '64px', height: '64px', objectFit: 'contain', marginBottom: '22px', animation: 'ccUp .4s cubic-bezier(.2,.7,.3,1)' }
|
||||
}),
|
||||
// eyebrow
|
||||
React.createElement('div', {
|
||||
key: 'eyebrow',
|
||||
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.8px', marginBottom: '14px' }
|
||||
}, 'Open source · community-driven'),
|
||||
// title
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
style: { margin: '0 0 16px', fontSize: isMobile ? '28px' : '36px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.05, color: '#f4f4f6' }
|
||||
}, 'Join the future of privacy'),
|
||||
// description
|
||||
React.createElement('p', {
|
||||
key: 'desc',
|
||||
style: { margin: '0 auto 32px', maxWidth: '560px', fontSize: '16px', lineHeight: 1.65, color: '#9a9aa2' }
|
||||
}, 'SecureBit grows thanks to its community. Your ideas and feedback shape the future of secure communication — built in the open, with complete ASN.1 validation end‑to‑end.'),
|
||||
// buttons
|
||||
React.createElement('div', {
|
||||
key: 'btns',
|
||||
style: { display: 'flex', gap: '14px', justifyContent: 'center', flexWrap: 'wrap' }
|
||||
}, [githubBtn, feedbackBtn]),
|
||||
// trust chips
|
||||
React.createElement('div', {
|
||||
key: 'chips',
|
||||
style: { display: 'flex', gap: '10px 22px', justifyContent: 'center', flexWrap: 'wrap', marginTop: '30px', fontFamily: MONO, fontSize: '11px', fontWeight: 500, color: '#56565e', textTransform: 'uppercase', letterSpacing: '1px' }
|
||||
}, [chip('MIT licensed'), chip('No tracking'), chip('Auditable cryptography')])
|
||||
]);
|
||||
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: '#0f0f11', fontFamily: SANS,
|
||||
padding: isMobile ? '48px 18px' : '64px 48px'
|
||||
}
|
||||
}, [
|
||||
React.createElement('style', { key: 'kf', dangerouslySetInnerHTML: { __html: '@keyframes ccUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}' } }),
|
||||
card
|
||||
]);
|
||||
};
|
||||
|
||||
window.CommunityCTA = CommunityCTA;
|
||||
@@ -1,5 +1,5 @@
|
||||
// File Transfer Component for Chat Interface - Fixed Version
|
||||
const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles = [], onIncomingDecision }) => {
|
||||
const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles = [], onIncomingDecision, showDropzone = true }) => {
|
||||
const [dragOver, setDragOver] = React.useState(false);
|
||||
const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] });
|
||||
const fileInputRef = React.useRef(null);
|
||||
@@ -135,6 +135,51 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
}
|
||||
};
|
||||
|
||||
// Segmented (per-chunk) progress — squares fill as chunks transfer, like a
|
||||
// download manager. For large files chunks are grouped into a fixed grid;
|
||||
// for small files it's literally one square per chunk.
|
||||
const renderProgress = (transfer, color) => {
|
||||
const total = transfer.totalChunks || 0;
|
||||
const done = transfer.transferredChunks || 0;
|
||||
const isDone = transfer.status === 'completed';
|
||||
const squares = total > 0 ? Math.min(total, 32) : 24;
|
||||
let filled;
|
||||
if (isDone) filled = squares;
|
||||
else if (total > 0) filled = Math.floor((done / total) * squares);
|
||||
else filled = Math.floor(((transfer.progress || 0) / 100) * squares);
|
||||
filled = Math.max(0, Math.min(squares, filled));
|
||||
|
||||
return React.createElement('div', { key: 'progress' }, [
|
||||
React.createElement('div', {
|
||||
key: 'squares',
|
||||
style: { display: 'flex', flexWrap: 'wrap', gap: '3px', marginBottom: '7px' }
|
||||
}, Array.from({ length: squares }, (_, i) => React.createElement('div', {
|
||||
key: i,
|
||||
style: {
|
||||
width: '11px', height: '11px', borderRadius: '2px',
|
||||
background: i < filled ? color : 'rgba(255,255,255,0.07)',
|
||||
border: '1px solid ' + (i < filled ? 'transparent' : 'rgba(255,255,255,0.05)'),
|
||||
boxShadow: i < filled ? `0 0 5px ${color}55` : 'none',
|
||||
transition: 'background .2s ease, box-shadow .2s ease'
|
||||
}
|
||||
}))),
|
||||
React.createElement('div', {
|
||||
key: 'text',
|
||||
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: '11.5px', color: '#8a8a92' }
|
||||
}, [
|
||||
React.createElement('span', { key: 'status', style: { display: 'inline-flex', alignItems: 'center', gap: '5px' } }, [
|
||||
React.createElement('i', { key: 'icon', className: getStatusIcon(transfer.status) }),
|
||||
getStatusText(transfer.status)
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'count',
|
||||
style: { fontFamily: "'JetBrains Mono', ui-monospace, monospace", color: i_done(transfer) ? color : '#8a8a92' }
|
||||
}, total > 0 ? `${Math.min(done, total)} / ${total} chunks` : `${(transfer.progress || 0).toFixed(0)}%`)
|
||||
])
|
||||
]);
|
||||
};
|
||||
const i_done = (t) => t.status === 'completed';
|
||||
|
||||
const handleIncomingDecision = async (fileId, accepted) => {
|
||||
if (typeof onIncomingDecision === 'function') {
|
||||
await onIncomingDecision(fileId, accepted);
|
||||
@@ -166,36 +211,43 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
return React.createElement('div', {
|
||||
className: "file-transfer-component"
|
||||
}, [
|
||||
// File Drop Zone
|
||||
React.createElement('div', {
|
||||
// File Drop Zone (SecureBit Chat design) — only when the panel is opened to SEND,
|
||||
// so a receiver never sees the "send attachments" UI.
|
||||
showDropzone && React.createElement('div', {
|
||||
key: 'drop-zone',
|
||||
className: `file-drop-zone ${dragOver ? 'drag-over' : ''}`,
|
||||
onDrop: handleDrop,
|
||||
onDragOver: handleDragOver,
|
||||
onDragLeave: handleDragLeave,
|
||||
onClick: () => fileInputRef.current?.click()
|
||||
style: {
|
||||
position: 'relative',
|
||||
border: '1.5px dashed ' + (dragOver ? 'rgba(240,137,42,0.7)' : 'rgba(255,255,255,0.14)'),
|
||||
borderRadius: '14px',
|
||||
background: dragOver ? 'rgba(240,137,42,0.07)' : '#141416',
|
||||
padding: '24px 22px',
|
||||
textAlign: 'center',
|
||||
transition: 'all .15s'
|
||||
}
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'drop-content',
|
||||
className: "drop-content"
|
||||
key: 'icon-box',
|
||||
style: { width: '42px', height: '42px', margin: '0 auto 10px', borderRadius: '12px', display: 'grid', placeItems: 'center', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)' }
|
||||
}, React.createElement('i', { className: 'fas fa-arrow-up-from-bracket', style: { color: '#9a9aa2', fontSize: '18px' } })),
|
||||
React.createElement('div', { key: 'title', style: { fontSize: '14px', fontWeight: 700, color: '#e8e8eb' } }, 'Drag & drop files here'),
|
||||
React.createElement('div', { key: 'sub', style: { fontSize: '12px', color: '#7b7b83', marginTop: '4px' } }, 'Encrypted end-to-end before transfer · up to 100 MB'),
|
||||
React.createElement('button', {
|
||||
key: 'browse',
|
||||
type: 'button',
|
||||
onClick: () => fileInputRef.current?.click(),
|
||||
className: 'sb-send',
|
||||
style: { marginTop: '14px', display: 'inline-flex', alignItems: 'center', gap: '7px', padding: '9px 16px', borderRadius: '9px', border: 'none', background: '#f0892a', color: '#1a0f04', fontFamily: 'inherit', fontSize: '13px', fontWeight: 700, cursor: 'pointer' }
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-cloud-upload-alt text-2xl mb-2 text-blue-400'
|
||||
}),
|
||||
React.createElement('p', {
|
||||
key: 'text',
|
||||
className: "text-primary font-medium"
|
||||
}, 'Drag files here or click to select'),
|
||||
React.createElement('p', {
|
||||
key: 'subtext',
|
||||
className: "text-muted text-sm"
|
||||
}, 'Maximum size: 100 MB per file')
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-folder-open', style: { fontSize: '13px' } }),
|
||||
'Browse device'
|
||||
])
|
||||
]),
|
||||
|
||||
// Hidden file input
|
||||
React.createElement('input', {
|
||||
showDropzone && React.createElement('input', {
|
||||
key: 'file-input',
|
||||
ref: fileInputRef,
|
||||
type: 'file',
|
||||
@@ -209,37 +261,40 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
className: "mt-4 space-y-2"
|
||||
}, pendingIncomingFiles.map(file => React.createElement('div', {
|
||||
key: file.fileId,
|
||||
className: "rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3"
|
||||
style: { borderRadius: '12px', border: '1px solid rgba(255,255,255,0.08)', background: '#161618', padding: '12px 14px' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'info',
|
||||
className: "mb-3 flex items-center justify-between gap-3"
|
||||
style: { marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '11px' }
|
||||
}, [
|
||||
React.createElement('div', { key: 'text' }, [
|
||||
React.createElement('div', { key: 'ic', style: { flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.22)' } },
|
||||
React.createElement('i', { className: 'fas fa-file-arrow-down', style: { color: '#f0892a', fontSize: '15px' } })
|
||||
),
|
||||
React.createElement('div', { key: 'text', style: { minWidth: 0 } }, [
|
||||
React.createElement('div', {
|
||||
key: 'title',
|
||||
className: "text-sm font-medium text-primary"
|
||||
style: { fontSize: '13px', fontWeight: 600, color: '#e8e8eb' }
|
||||
}, 'Incoming file request'),
|
||||
React.createElement('div', {
|
||||
key: 'meta',
|
||||
className: "text-xs text-secondary"
|
||||
style: { fontSize: '11.5px', color: '#7b7b83', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }
|
||||
}, `${file.fileName} · ${formatFileSize(file.fileSize)} · ${file.mimeType}`)
|
||||
])
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'actions',
|
||||
className: "flex gap-2"
|
||||
style: { display: 'flex', gap: '8px' }
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'accept',
|
||||
onClick: () => handleIncomingDecision(file.fileId, true),
|
||||
className: "rounded-md bg-green-500/20 px-3 py-2 text-sm text-green-300 hover:bg-green-500/30"
|
||||
}, 'Accept'),
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: '8px', border: 'none', background: '#f0892a', color: '#1a0f04', padding: '8px 14px', fontSize: '13px', fontWeight: 700, cursor: 'pointer' }
|
||||
}, [React.createElement('i', { key: 'i', className: 'fas fa-check', style: { fontSize: '12px' } }), 'Accept']),
|
||||
React.createElement('button', {
|
||||
key: 'reject',
|
||||
onClick: () => handleIncomingDecision(file.fileId, false),
|
||||
className: "rounded-md bg-red-500/20 px-3 py-2 text-sm text-red-300 hover:bg-red-500/30"
|
||||
}, 'Reject')
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: '8px', border: '1px solid rgba(229,114,122,0.3)', background: 'rgba(229,114,122,0.08)', color: '#e5727a', padding: '8px 14px', fontSize: '13px', fontWeight: 600, cursor: 'pointer' }
|
||||
}, [React.createElement('i', { key: 'i', className: 'fas fa-xmark', style: { fontSize: '12px' } }), 'Reject'])
|
||||
])
|
||||
]))),
|
||||
|
||||
@@ -250,20 +305,21 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'title',
|
||||
className: "text-primary font-medium mb-3 flex items-center"
|
||||
style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12.5px', fontWeight: 600, color: '#8a8a92', marginBottom: '10px' }
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-exchange-alt mr-2'
|
||||
className: 'fas fa-right-left',
|
||||
style: { fontSize: '12px' }
|
||||
}),
|
||||
'Передача файлов'
|
||||
'File transfers'
|
||||
]),
|
||||
|
||||
// Sending files
|
||||
...transfers.sending.map(transfer =>
|
||||
...transfers.sending.map(transfer =>
|
||||
React.createElement('div', {
|
||||
key: `send-${transfer.fileId}`,
|
||||
className: "transfer-item bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 mb-2"
|
||||
style: { borderRadius: '11px', border: '1px solid rgba(255,255,255,0.07)', background: '#161618', padding: '12px', marginBottom: '8px' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
@@ -275,15 +331,18 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-upload text-blue-400 mr-2'
|
||||
className: 'fas fa-arrow-up',
|
||||
style: { color: '#f0892a', fontSize: '13px', marginRight: '8px' }
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'name',
|
||||
className: "text-primary font-medium text-sm"
|
||||
className: "font-medium text-sm",
|
||||
style: { color: '#e8e8eb' }
|
||||
}, transfer.fileName),
|
||||
React.createElement('span', {
|
||||
key: 'size',
|
||||
className: "text-muted text-xs ml-2"
|
||||
className: "text-xs ml-2",
|
||||
style: { color: '#7b7b83' }
|
||||
}, formatFileSize(transfer.fileSize))
|
||||
]),
|
||||
React.createElement('button', {
|
||||
@@ -296,34 +355,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
})
|
||||
])
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: "progress-bar"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'fill',
|
||||
className: "progress-fill bg-blue-400",
|
||||
style: { width: `${transfer.progress}%` }
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'text',
|
||||
className: "progress-text text-xs flex items-center justify-between"
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'status',
|
||||
className: "flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `${getStatusIcon(transfer.status)} mr-1`
|
||||
}),
|
||||
getStatusText(transfer.status)
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'percent'
|
||||
}, `${transfer.progress.toFixed(1)}%`)
|
||||
])
|
||||
])
|
||||
renderProgress(transfer, '#f0892a')
|
||||
])
|
||||
),
|
||||
|
||||
@@ -331,7 +363,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
...transfers.receiving.map(transfer =>
|
||||
React.createElement('div', {
|
||||
key: `recv-${transfer.fileId}`,
|
||||
className: "transfer-item bg-green-500/10 border border-green-500/20 rounded-lg p-3 mb-2"
|
||||
style: { borderRadius: '11px', border: '1px solid rgba(255,255,255,0.07)', background: '#161618', padding: '12px', marginBottom: '8px' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
@@ -343,15 +375,18 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-download text-green-400 mr-2'
|
||||
className: 'fas fa-arrow-down',
|
||||
style: { color: '#3ecf8e', fontSize: '13px', marginRight: '8px' }
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'name',
|
||||
className: "text-primary font-medium text-sm"
|
||||
className: "font-medium text-sm",
|
||||
style: { color: '#e8e8eb' }
|
||||
}, transfer.fileName),
|
||||
React.createElement('span', {
|
||||
key: 'size',
|
||||
className: "text-muted text-xs ml-2"
|
||||
className: "text-xs ml-2",
|
||||
style: { color: '#7b7b83' }
|
||||
}, formatFileSize(transfer.fileSize))
|
||||
]),
|
||||
React.createElement('div', { key: 'actions', className: 'flex items-center space-x-2' }, [
|
||||
@@ -386,34 +421,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
])
|
||||
])
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: "progress-bar"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'fill',
|
||||
className: "progress-fill bg-green-400",
|
||||
style: { width: `${transfer.progress}%` }
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'text',
|
||||
className: "progress-text text-xs flex items-center justify-between"
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'status',
|
||||
className: "flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `${getStatusIcon(transfer.status)} mr-1`
|
||||
}),
|
||||
getStatusText(transfer.status)
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'percent'
|
||||
}, `${transfer.progress.toFixed(1)}%`)
|
||||
])
|
||||
])
|
||||
renderProgress(transfer, '#3ecf8e')
|
||||
])
|
||||
)
|
||||
])
|
||||
|
||||
+73
-157
@@ -505,181 +505,97 @@ const EnhancedMinimalHeader = ({
|
||||
// RENDER
|
||||
// ============================================
|
||||
|
||||
const secColor = displaySecurityLevel
|
||||
? (displaySecurityLevel.color === 'green' ? '#3ecf8e' : displaySecurityLevel.color === 'orange' ? '#f0892a' : displaySecurityLevel.color === 'yellow' ? '#e3c84e' : '#e5727a')
|
||||
: '#3ecf8e';
|
||||
const dotColor = isConnected ? '#3ecf8e'
|
||||
: (['connecting', 'verifying', 'retrying', 'reconnecting'].includes(status) ? '#e3c84e'
|
||||
: (status === 'failed' ? '#e5727a' : '#6b6b73'));
|
||||
const dotGlow = dotColor === '#3ecf8e' ? 'rgba(62,207,142,0.16)' : dotColor === '#e3c84e' ? 'rgba(227,200,78,0.16)' : dotColor === '#e5727a' ? 'rgba(229,114,122,0.16)' : 'rgba(107,107,115,0.16)';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
|
||||
// On the landing / setup view (no verified connection) the new Start Secure
|
||||
// screen owns the network settings and step status, so keep the header clean:
|
||||
// brand only — no status pill, no settings. The bar is transparent at the top
|
||||
// of the page and gains a blurred background once the user scrolls.
|
||||
const onLanding = !isConnected;
|
||||
const [scrolled, setScrolled] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
const onScroll = () => setScrolled((window.scrollY || window.pageYOffset || 0) > 8);
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
// On the landing the header floats *over* the full-height hero (position
|
||||
// fixed), transparent at the top and blurred once scrolled. When connected it
|
||||
// falls back to the in-flow sticky bar.
|
||||
const overlay = { position: 'fixed', top: 0, left: 0, right: 0 };
|
||||
const headerStyle = onLanding
|
||||
? (scrolled
|
||||
? { ...overlay, background: 'rgba(15,15,17,0.72)', backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)', borderBottom: '1px solid rgba(255,255,255,0.06)', transition: 'background .25s ease, backdrop-filter .25s ease, border-color .25s ease' }
|
||||
: { ...overlay, background: 'transparent', backdropFilter: 'none', WebkitBackdropFilter: 'none', borderBottom: '1px solid transparent', transition: 'background .25s ease, backdrop-filter .25s ease, border-color .25s ease' })
|
||||
: { background: 'rgba(18,18,20,0.72)', backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)', borderBottom: '1px solid rgba(255,255,255,0.06)' };
|
||||
|
||||
return React.createElement('header', {
|
||||
className: 'header-minimal sticky top-0 z-50'
|
||||
className: onLanding ? 'header-minimal z-50' : 'header-minimal sticky top-0 z-50',
|
||||
style: headerStyle
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'container',
|
||||
className: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
|
||||
className: 'max-w-7xl mx-auto',
|
||||
style: { padding: '0 20px' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'content',
|
||||
className: 'flex items-center justify-between h-16'
|
||||
className: 'flex items-center justify-between',
|
||||
style: { height: '64px', gap: '16px' }
|
||||
}, [
|
||||
// Logo and Title
|
||||
React.createElement('div', {
|
||||
key: 'logo-section',
|
||||
className: 'flex items-center space-x-2 sm:space-x-3'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'logo',
|
||||
className: 'icon-container w-8 h-8 sm:w-10 sm:h-10'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-shield-halved accent-orange text-sm sm:text-base'
|
||||
})
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'title-section'
|
||||
}, [
|
||||
React.createElement('h1', {
|
||||
key: 'title',
|
||||
className: 'text-lg sm:text-xl font-semibold text-primary'
|
||||
}, 'SecureBit.chat'),
|
||||
React.createElement('p', {
|
||||
key: 'subtitle',
|
||||
className: 'text-xs sm:text-sm text-muted hidden sm:block'
|
||||
}, 'End-to-end freedom v4.8.20')
|
||||
// Left: logo + wordmark
|
||||
React.createElement('div', { key: 'left', style: { display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0 } }, [
|
||||
React.createElement('div', { key: 'logo', style: { width: '36px', height: '36px', flex: 'none', display: 'grid', placeItems: 'center' } },
|
||||
React.createElement('img', { src: '/logo/securebit-mark.svg', alt: 'SecureBit', style: { width: '100%', height: '100%', objectFit: 'contain', display: 'block' } })
|
||||
),
|
||||
React.createElement('div', { key: 'txt', style: { lineHeight: 1.2, minWidth: 0 } }, [
|
||||
React.createElement('div', { key: 'r1', style: { display: 'flex', alignItems: 'baseline', gap: '7px' } }, [
|
||||
React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: '#e8e8eb' } }, 'SecureBit'),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, 'v4.9.0')
|
||||
]),
|
||||
React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: '#6b6b73', fontWeight: 500 } }, 'End-to-end encrypted')
|
||||
])
|
||||
]),
|
||||
|
||||
// Status and Controls - Responsive
|
||||
React.createElement('div', {
|
||||
key: 'status-section',
|
||||
className: 'flex items-center space-x-2 sm:space-x-3'
|
||||
}, [
|
||||
|
||||
React.createElement('button', {
|
||||
key: 'network-settings',
|
||||
type: 'button',
|
||||
// Right: controls
|
||||
React.createElement('div', { key: 'right', style: { display: 'flex', alignItems: 'center', gap: '9px' } }, [
|
||||
!onLanding && React.createElement('button', {
|
||||
key: 'net', type: 'button',
|
||||
onClick: () => window.dispatchEvent(new CustomEvent('securebit:open-network-settings')),
|
||||
title: 'Advanced network settings (STUN/TURN)',
|
||||
'aria-label': 'Advanced network settings',
|
||||
className: 'w-8 h-8 rounded-full flex items-center justify-center text-muted hover:text-primary hover:bg-white/5 transition-colors duration-200'
|
||||
title: 'Advanced network settings (STUN/TURN)', 'aria-label': 'Advanced network settings',
|
||||
className: 'sb-disconnect',
|
||||
style: { display: 'grid', placeItems: 'center', width: '38px', height: '38px', borderRadius: '9px', border: '1px solid rgba(255,255,255,0.07)', background: 'rgba(255,255,255,0.02)', color: '#9a9aa2', cursor: 'pointer', transition: 'all .15s' }
|
||||
}, React.createElement('i', { className: 'fas fa-network-wired', style: { fontSize: '13px' } })),
|
||||
|
||||
(!onLanding && displaySecurityLevel) && React.createElement('div', {
|
||||
key: 'sec', onClick: handleSecurityClick,
|
||||
onContextMenu: (e) => { e.preventDefault(); if (typeof onDisconnect === 'function') onDisconnect(); },
|
||||
title: securityDetails.tooltip, className: 'sb-secpill',
|
||||
style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '7px 12px', borderRadius: '9px', border: '1px solid rgba(255,255,255,0.07)', background: 'rgba(255,255,255,0.02)', cursor: 'pointer' }
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-network-wired text-sm' })
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-shield-halved', style: { fontSize: '13px', color: secColor } }),
|
||||
React.createElement('span', { key: 'l', className: 'hidden sm:inline', style: { fontSize: '12.5px', fontWeight: 600, color: '#e8e8eb' } }, String(displaySecurityLevel.level)),
|
||||
React.createElement('span', { key: 's', style: { fontFamily: MONO, fontSize: '11.5px', color: '#8a8a92' } }, displaySecurityLevel.score + '%')
|
||||
]),
|
||||
|
||||
displaySecurityLevel && React.createElement('div', {
|
||||
key: 'security-level',
|
||||
className: 'hidden md:flex items-center space-x-2 cursor-pointer hover:opacity-80 transition-opacity duration-200',
|
||||
onClick: handleSecurityClick,
|
||||
onContextMenu: (e) => {
|
||||
e.preventDefault();
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
}
|
||||
},
|
||||
title: securityDetails.tooltip
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'security-icon',
|
||||
className: `w-6 h-6 rounded-full flex items-center justify-center relative ${
|
||||
displaySecurityLevel.color === 'green' ? 'bg-green-500/20' :
|
||||
displaySecurityLevel.color === 'orange' ? 'bg-orange-500/20' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
|
||||
} ${securityDetails.isVerified ? '' : 'animate-pulse'}`
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: `fas fa-shield-alt text-xs ${
|
||||
displaySecurityLevel.color === 'green' ? 'text-green-400' :
|
||||
displaySecurityLevel.color === 'orange' ? 'text-orange-400' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400'
|
||||
}`
|
||||
})
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'security-info',
|
||||
className: 'flex flex-col'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'security-level-text',
|
||||
className: 'text-xs font-medium text-primary flex items-center space-x-1'
|
||||
}, [
|
||||
React.createElement('span', {}, `${displaySecurityLevel.level} (${displaySecurityLevel.score}%)`)
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'security-details',
|
||||
className: 'text-xs text-muted mt-1 hidden lg:block'
|
||||
}, securityDetails.dataSource === 'real' ?
|
||||
`${displaySecurityLevel.passedChecks || 0}/${displaySecurityLevel.totalChecks || 0} tests` :
|
||||
(displaySecurityLevel.details || `Stage ${displaySecurityLevel.stage || 1}`)
|
||||
),
|
||||
React.createElement('div', {
|
||||
key: 'security-progress',
|
||||
className: 'w-16 h-1 bg-gray-600 rounded-full overflow-hidden'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-bar',
|
||||
className: `h-full transition-all duration-500 ${
|
||||
displaySecurityLevel.color === 'green' ? 'bg-green-400' :
|
||||
displaySecurityLevel.color === 'orange' ? 'bg-orange-400' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'bg-yellow-400' : 'bg-red-400'
|
||||
}`,
|
||||
style: { width: `${displaySecurityLevel.score}%` }
|
||||
})
|
||||
])
|
||||
])
|
||||
!onLanding && React.createElement('div', { key: 'status', style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 13px', borderRadius: '9px', border: '1px solid rgba(255,255,255,0.07)', background: 'rgba(255,255,255,0.02)' } }, [
|
||||
React.createElement('span', { key: 'dot', style: { width: '7px', height: '7px', borderRadius: '50%', background: dotColor, boxShadow: '0 0 0 3px ' + dotGlow } }),
|
||||
React.createElement('span', { key: 't', className: 'hidden sm:inline', style: { fontSize: '13px', fontWeight: 600, color: '#cfcfd4' } }, config.text)
|
||||
]),
|
||||
|
||||
// Mobile Security Indicator
|
||||
displaySecurityLevel && React.createElement('div', {
|
||||
key: 'mobile-security',
|
||||
className: 'md:hidden flex items-center'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'mobile-security-icon',
|
||||
className: `w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity duration-200 relative ${
|
||||
displaySecurityLevel.color === 'green' ? 'bg-green-500/20' :
|
||||
displaySecurityLevel.color === 'orange' ? 'bg-orange-500/20' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
|
||||
} ${securityDetails.isVerified ? '' : 'animate-pulse'}`,
|
||||
title: securityDetails.tooltip,
|
||||
onClick: handleSecurityClick,
|
||||
onContextMenu: (e) => {
|
||||
e.preventDefault();
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: `fas fa-shield-alt text-sm ${
|
||||
displaySecurityLevel.color === 'green' ? 'text-green-400' :
|
||||
displaySecurityLevel.color === 'orange' ? 'text-orange-400' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400'
|
||||
}`
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Status Badge
|
||||
React.createElement('div', {
|
||||
key: 'status-badge',
|
||||
className: `px-2 sm:px-3 py-1.5 rounded-lg border ${config.badgeClass} flex items-center space-x-1 sm:space-x-2`
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'status-dot',
|
||||
className: `status-dot ${config.className}`
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'status-text',
|
||||
className: 'text-xs sm:text-sm font-medium'
|
||||
}, config.text),
|
||||
]),
|
||||
|
||||
// Disconnect Button
|
||||
isConnected && React.createElement('button', {
|
||||
key: 'disconnect-btn',
|
||||
onClick: onDisconnect,
|
||||
className: 'p-1.5 sm:px-3 sm:py-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 rounded-lg transition-all duration-200 text-sm'
|
||||
key: 'dc', onClick: onDisconnect, className: 'sb-disconnect',
|
||||
style: { display: 'flex', alignItems: 'center', gap: '7px', padding: '8px 14px', borderRadius: '9px', border: '1px solid rgba(255,255,255,0.08)', background: 'transparent', color: '#9a9aa2', fontFamily: 'inherit', fontSize: '13px', fontWeight: 600, cursor: 'pointer', transition: 'all .15s' }
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-power-off sm:mr-2'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
className: 'hidden sm:inline'
|
||||
}, 'Disconnect')
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-power-off', style: { fontSize: '12px' } }),
|
||||
React.createElement('span', { key: 't', className: 'sb-hide-sm' }, 'Disconnect')
|
||||
])
|
||||
])
|
||||
])
|
||||
|
||||
@@ -64,7 +64,7 @@ async function testIceServers(servers, timeoutMs = 6000) {
|
||||
});
|
||||
}
|
||||
|
||||
const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForget }) => {
|
||||
const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForget, embedded }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const [useCustom, setUseCustom] = React.useState(initial?.useCustom || false);
|
||||
@@ -104,159 +104,153 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
setPersist(false);
|
||||
};
|
||||
|
||||
const labelCls = 'block text-sm font-medium text-primary';
|
||||
const descCls = 'block text-sm text-secondary';
|
||||
// ── Design import: dark slide-up overlay (orange/green accents) ──────────
|
||||
const h = React.createElement;
|
||||
const C_ORANGE = '#f0892a';
|
||||
const C_GREEN = '#3ecf8e';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
|
||||
const children = [];
|
||||
|
||||
// Header
|
||||
children.push(React.createElement('div', { key: 'header', className: 'flex items-center mb-4' }, [
|
||||
React.createElement('div', {
|
||||
key: 'icon',
|
||||
className: 'w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3'
|
||||
}, [React.createElement('i', { className: 'fas fa-network-wired accent-purple' })]),
|
||||
React.createElement('h3', { key: 'title', className: 'text-lg font-medium text-primary' }, 'Advanced network settings')
|
||||
]));
|
||||
|
||||
// Explainer
|
||||
children.push(React.createElement('p', { key: 'intro', className: 'text-sm text-secondary mb-4' },
|
||||
'By default SecureBit uses public STUN servers. You can supply your own STUN/TURN servers — useful if you self-host a TURN relay and do not want to rely on public infrastructure. Servers are configured locally on your side only; you do not need to share them with your peer.'
|
||||
));
|
||||
|
||||
// Mode radios
|
||||
children.push(React.createElement('div', { key: 'mode', className: 'space-y-2 mb-4' }, [
|
||||
React.createElement('label', { key: 'public', className: 'flex items-start gap-3' }, [
|
||||
React.createElement('input', {
|
||||
key: 'r', type: 'radio', name: 'ice-mode', checked: !useCustom,
|
||||
onChange: () => setUseCustom(false), className: 'mt-1'
|
||||
}),
|
||||
React.createElement('span', { key: 's' }, [
|
||||
React.createElement('span', { key: 't', className: labelCls }, 'Public servers (default)'),
|
||||
React.createElement('span', { key: 'd', className: descCls }, 'Zero-config. Good for most users.')
|
||||
])
|
||||
]),
|
||||
React.createElement('label', { key: 'custom', className: 'flex items-start gap-3' }, [
|
||||
React.createElement('input', {
|
||||
key: 'r', type: 'radio', name: 'ice-mode', checked: useCustom,
|
||||
onChange: () => setUseCustom(true), className: 'mt-1'
|
||||
}),
|
||||
React.createElement('span', { key: 's' }, [
|
||||
React.createElement('span', { key: 't', className: labelCls }, 'My own STUN/TURN servers'),
|
||||
React.createElement('span', { key: 'd', className: descCls }, `Up to ${ICE_LIMITS.MAX_SERVERS} servers.`)
|
||||
])
|
||||
// radio card (public / own servers)
|
||||
const radioCard = (selected, onClick, title, desc, extraStyle) => h('button', {
|
||||
type: 'button', onClick,
|
||||
style: Object.assign({
|
||||
width: '100%', textAlign: 'left', display: 'flex', alignItems: 'flex-start', gap: '12px',
|
||||
padding: '14px 15px', borderRadius: '13px',
|
||||
border: `1px solid ${selected ? 'rgba(240,137,42,0.45)' : 'rgba(255,255,255,0.07)'}`,
|
||||
background: selected ? 'rgba(240,137,42,0.06)' : '#141416',
|
||||
color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', transition: 'all .15s', marginBottom: '10px'
|
||||
}, extraStyle || {})
|
||||
}, [
|
||||
h('span', { key: 'ring', style: { flex: 'none', width: '18px', height: '18px', marginTop: '1px', borderRadius: '50%', border: `1.5px solid ${selected ? C_ORANGE : 'rgba(255,255,255,0.22)'}`, display: 'grid', placeItems: 'center' } },
|
||||
h('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: selected ? C_ORANGE : 'transparent' } })),
|
||||
h('span', { key: 'tx', style: { flex: 1 } }, [
|
||||
h('span', { key: 't', style: { display: 'block', fontSize: '14px', fontWeight: 700, color: '#f4f4f6' } }, title),
|
||||
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', color: '#8a8a92', marginTop: '2px' } }, desc)
|
||||
])
|
||||
]));
|
||||
]);
|
||||
|
||||
// pill toggle row (relay / save)
|
||||
const toggleRow = (on, onClick, title, desc, accent, badge) => h('button', {
|
||||
type: 'button', onClick,
|
||||
style: {
|
||||
width: '100%', textAlign: 'left', display: 'flex', alignItems: 'flex-start', gap: '12px',
|
||||
padding: '14px 15px', borderRadius: '13px',
|
||||
border: `1px solid ${on ? 'rgba(62,207,142,0.3)' : 'rgba(255,255,255,0.07)'}`,
|
||||
background: on ? 'rgba(62,207,142,0.05)' : '#141416',
|
||||
color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', transition: 'all .15s', marginBottom: '10px'
|
||||
}
|
||||
}, [
|
||||
h('span', { key: 'tx', style: { flex: 1 } }, [
|
||||
h('span', { key: 'r1', style: { display: 'flex', alignItems: 'center', gap: '8px' } }, [
|
||||
h('span', { key: 't', style: { fontSize: '14px', fontWeight: 700, color: '#f4f4f6' } }, title),
|
||||
badge && h('span', { key: 'b', style: { fontSize: '10px', fontWeight: 700, color: C_GREEN, padding: '2px 7px', borderRadius: '5px', background: 'rgba(62,207,142,0.1)', border: '1px solid rgba(62,207,142,0.22)' } }, badge)
|
||||
]),
|
||||
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', lineHeight: 1.5, color: '#8a8a92', marginTop: '3px' } }, desc)
|
||||
]),
|
||||
h('span', { key: 'tr', style: { flex: 'none', width: '42px', height: '24px', borderRadius: '99px', background: on ? (accent || C_GREEN) : 'rgba(255,255,255,0.08)', border: `1px solid ${on ? (accent || C_GREEN) : 'rgba(255,255,255,0.12)'}`, position: 'relative', transition: 'all .18s', marginTop: '1px' } },
|
||||
h('span', { style: { position: 'absolute', top: '2px', left: '2px', width: '18px', height: '18px', borderRadius: '50%', background: '#fff', transform: on ? 'translateX(18px)' : 'translateX(0)', transition: 'transform .18s' } }))
|
||||
]);
|
||||
|
||||
// ── scrollable body ──
|
||||
const body = [];
|
||||
body.push(h('p', { key: 'intro', style: { margin: '0 0 18px', fontSize: '13.5px', lineHeight: 1.6, color: '#9a9aa2' } },
|
||||
'SecureBit uses public STUN servers by default to negotiate the peer-to-peer link. Point it at your own STUN/TURN if you self-host.'));
|
||||
body.push(radioCard(!useCustom, () => setUseCustom(false), 'Public servers (default)', 'Zero-config. Good for most users.'));
|
||||
body.push(radioCard(useCustom, () => setUseCustom(true), 'My own STUN/TURN servers', `Up to ${ICE_LIMITS.MAX_SERVERS} servers.`, useCustom ? { marginBottom: '14px' } : null));
|
||||
|
||||
// Textarea + validation (only in custom mode)
|
||||
if (useCustom) {
|
||||
children.push(React.createElement('textarea', {
|
||||
key: 'textarea',
|
||||
value: serversText,
|
||||
onChange: (e) => setServersText(e.target.value),
|
||||
placeholder: PLACEHOLDER,
|
||||
spellCheck: false,
|
||||
autoComplete: 'off',
|
||||
className: 'w-full h-36 mb-2 p-3 rounded-lg bg-black/30 border border-purple-500/20 text-sm text-primary font-mono'
|
||||
}));
|
||||
|
||||
const custom = [];
|
||||
custom.push(h('div', { key: 'ta', style: { borderRadius: '13px', border: '1px solid rgba(255,255,255,0.08)', background: '#0c0c0e', overflow: 'hidden', marginBottom: '12px' } },
|
||||
h('textarea', {
|
||||
value: serversText, onChange: (e) => setServersText(e.target.value), rows: 5, spellCheck: false, autoComplete: 'off',
|
||||
placeholder: PLACEHOLDER,
|
||||
style: { width: '100%', resize: 'vertical', border: 'none', outline: 'none', background: 'transparent', color: '#c9ccd8', fontFamily: MONO, fontSize: '12px', lineHeight: 1.65, padding: '13px 14px', minHeight: '104px' }
|
||||
})));
|
||||
if (parsed.errors.length > 0) {
|
||||
children.push(React.createElement('ul', { key: 'errors', className: 'mb-2 text-sm text-red-400 list-disc pl-5' },
|
||||
parsed.errors.slice(0, 6).map((err, i) => React.createElement('li', { key: i }, err))
|
||||
));
|
||||
custom.push(h('ul', { key: 'err', style: { margin: '0 0 10px', paddingLeft: '18px', color: '#e5727a', fontSize: '12.5px' } },
|
||||
parsed.errors.slice(0, 6).map((err, i) => h('li', { key: i }, err))));
|
||||
}
|
||||
if (parsed.warnings.length > 0) {
|
||||
children.push(React.createElement('ul', { key: 'warnings', className: 'mb-2 text-sm text-yellow-400 list-disc pl-5' },
|
||||
parsed.warnings.slice(0, 6).map((w, i) => React.createElement('li', { key: i }, w))
|
||||
));
|
||||
custom.push(h('ul', { key: 'warn', style: { margin: '0 0 10px', paddingLeft: '18px', color: '#e3c84e', fontSize: '12.5px' } },
|
||||
parsed.warnings.slice(0, 6).map((w, i) => h('li', { key: i }, w))));
|
||||
}
|
||||
if (parsed.servers.length > 0 && parsed.errors.length === 0) {
|
||||
children.push(React.createElement('p', { key: 'ok', className: 'mb-2 text-sm text-green-400' },
|
||||
`${parsed.servers.length} server(s) parsed${hasTurn ? ' (TURN present)' : ' (STUN only — does not hide IP)'}.`
|
||||
));
|
||||
custom.push(h('p', { key: 'ok', style: { margin: '0 0 10px', fontSize: '12.5px', color: C_GREEN } },
|
||||
`${parsed.servers.length} server(s) parsed${hasTurn ? ' (TURN present)' : ' (STUN only — does not hide IP)'}.`));
|
||||
}
|
||||
|
||||
// Privacy disclaimer about third-party relays
|
||||
children.push(React.createElement('p', { key: 'disclaimer', className: 'mb-3 text-xs text-secondary' },
|
||||
'Privacy note: a TURN relay sees the IP addresses and traffic timing of both peers (never your message contents, which stay end-to-end encrypted). Only a TURN server you trust or self-host improves privacy — pointing this at a random public relay does not. Prefer turns: (TLS).'
|
||||
));
|
||||
|
||||
// Test button + result
|
||||
children.push(React.createElement('div', { key: 'test', className: 'mb-3' }, [
|
||||
React.createElement('button', {
|
||||
key: 'btn',
|
||||
type: 'button',
|
||||
disabled: !canApply || testState === 'running',
|
||||
onClick: handleTest,
|
||||
className: 'px-3 py-2 text-sm rounded-lg border border-purple-500/30 text-primary disabled:opacity-50'
|
||||
}, testState === 'running' ? 'Testing…' : 'Test servers'),
|
||||
testState === 'done' && testResult ? React.createElement('span', {
|
||||
key: 'res',
|
||||
className: 'ml-3 text-sm ' + (testResult.error ? 'text-red-400' : 'text-secondary')
|
||||
}, testResult.error
|
||||
? `Test failed: ${testResult.error}`
|
||||
: (testResult.srflx > 0 || testResult.relay > 0)
|
||||
? `STUN ${testResult.srflx > 0 ? 'OK' : 'none'} · TURN ${testResult.relay > 0 ? 'OK' : 'none'} · host ${testResult.host}`
|
||||
: `host ${testResult.host} · this browser (e.g. Safari) hides STUN/TURN candidates from the test — your servers are still applied to real connections`
|
||||
custom.push(h('div', { key: 'note', style: { display: 'flex', alignItems: 'flex-start', gap: '9px', padding: '12px 13px', borderRadius: '11px', border: '1px solid rgba(62,207,142,0.18)', background: 'rgba(62,207,142,0.05)', marginBottom: '12px' } }, [
|
||||
h('i', { key: 'i', className: 'fas fa-info-circle', style: { color: C_GREEN, fontSize: '13px', marginTop: '2px', flex: 'none' } }),
|
||||
h('span', { key: 't', style: { fontSize: '12px', lineHeight: 1.55, color: '#a8b8ae' } }, [
|
||||
'A TURN relay sees both peers’ IP and traffic timing — but never message contents, which stay end-to-end encrypted. Prefer ',
|
||||
h('span', { key: 'm', style: { fontFamily: MONO, color: C_GREEN } }, 'turns:'), ' (TLS).'
|
||||
])
|
||||
]));
|
||||
const testColor = testState === 'done' && testResult && !testResult.error ? C_GREEN : '#cfcfd4';
|
||||
custom.push(h('div', { key: 'test', style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', marginBottom: '4px' } }, [
|
||||
h('button', {
|
||||
key: 'btn', type: 'button', disabled: !canApply || testState === 'running', onClick: handleTest,
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '10px 15px', borderRadius: '10px', border: `1px solid ${testState === 'done' && testResult && !testResult.error ? 'rgba(62,207,142,0.4)' : 'rgba(255,255,255,0.1)'}`, background: testState === 'done' && testResult && !testResult.error ? 'rgba(62,207,142,0.08)' : 'rgba(255,255,255,0.04)', color: testColor, fontFamily: 'inherit', fontSize: '13px', fontWeight: 600, cursor: (!canApply || testState === 'running') ? 'not-allowed' : 'pointer', opacity: (!canApply || testState === 'running') ? 0.6 : 1 }
|
||||
}, [
|
||||
h('i', { key: 'i', className: testState === 'running' ? 'fas fa-circle-notch' : 'fas fa-play-circle', style: testState === 'running' ? { animation: 'sbSpin 1s linear infinite' } : null }),
|
||||
testState === 'running' ? 'Testing…' : 'Test servers'
|
||||
]),
|
||||
(testState === 'done' && testResult) ? h('span', { key: 'res', style: { fontSize: '12px', color: testResult.error ? '#e5727a' : '#8a8a92' } },
|
||||
testResult.error
|
||||
? `Test failed: ${testResult.error}`
|
||||
: (testResult.srflx > 0 || testResult.relay > 0)
|
||||
? `STUN ${testResult.srflx > 0 ? 'OK' : 'none'} · TURN ${testResult.relay > 0 ? 'OK' : 'none'} · host ${testResult.host}`
|
||||
: `host ${testResult.host} · this browser hides STUN/TURN candidates from the test — your servers still apply to real connections`
|
||||
) : null
|
||||
]));
|
||||
body.push(h('div', { key: 'custom', style: { marginBottom: '16px' } }, custom));
|
||||
}
|
||||
|
||||
// Relay-only privacy toggle
|
||||
children.push(React.createElement('label', { key: 'relay', className: 'flex items-start gap-3 mb-3 rounded-lg border border-purple-500/20 bg-purple-500/10 p-3' }, [
|
||||
React.createElement('input', {
|
||||
key: 'i', type: 'checkbox', checked: relayOnly,
|
||||
onChange: (e) => setRelayOnly(e.target.checked), className: 'mt-1'
|
||||
}),
|
||||
React.createElement('span', { key: 's' }, [
|
||||
React.createElement('span', { key: 't', className: labelCls }, 'Relay-only mode (maximum privacy)'),
|
||||
React.createElement('span', { key: 'd', className: descCls }, 'Routes all traffic through TURN so your IP is not exposed to the peer. Requires a TURN server; connections cannot start without one.')
|
||||
])
|
||||
]));
|
||||
body.push(toggleRow(relayOnly, () => setRelayOnly(!relayOnly), 'Relay-only mode',
|
||||
'Routes all traffic through TURN so your IP is never exposed to the peer. Requires a TURN server.', C_GREEN, 'MAX PRIVACY'));
|
||||
if (relayOnly && useCustom && !hasTurn) {
|
||||
children.push(React.createElement('p', { key: 'relaywarn', className: 'mb-3 text-sm text-yellow-400' },
|
||||
'Relay-only is enabled but no TURN server is configured. The connection will not be able to start.'
|
||||
));
|
||||
body.push(h('p', { key: 'relaywarn', style: { margin: '-4px 0 10px', fontSize: '12.5px', color: '#e3c84e' } },
|
||||
'Relay-only is enabled but no TURN server is configured. The connection will not be able to start.'));
|
||||
}
|
||||
body.push(toggleRow(persist, () => setPersist(!persist), 'Save on this device',
|
||||
'Stored encrypted in this browser. Leave off to use only for this session.', C_ORANGE));
|
||||
|
||||
// Save on device
|
||||
children.push(React.createElement('label', { key: 'persist', className: 'flex items-start gap-3 mb-4' }, [
|
||||
React.createElement('input', {
|
||||
key: 'i', type: 'checkbox', checked: persist,
|
||||
onChange: (e) => setPersist(e.target.checked), className: 'mt-1'
|
||||
}),
|
||||
React.createElement('span', { key: 's' }, [
|
||||
React.createElement('span', { key: 't', className: labelCls }, 'Save on this device'),
|
||||
React.createElement('span', { key: 'd', className: descCls }, 'Stored encrypted in this browser. Leave off to use only for this session.')
|
||||
])
|
||||
// ── footer actions ──
|
||||
const footerBtns = [];
|
||||
if (hasSaved) {
|
||||
footerBtns.push(h('button', { key: 'forget', type: 'button', onClick: handleForget,
|
||||
style: { marginRight: 'auto', padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(229,114,122,0.3)', background: 'transparent', color: '#e5727a', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, 'Forget saved'));
|
||||
}
|
||||
footerBtns.push(h('button', { key: 'cancel', type: 'button', onClick: onClose,
|
||||
style: { padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(255,255,255,0.1)', background: 'transparent', color: '#b3b3ba', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, 'Cancel'));
|
||||
footerBtns.push(h('button', { key: 'apply', type: 'button', onClick: handleApply, disabled: !canApply,
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '11px 20px', borderRadius: '11px', border: 'none', background: C_ORANGE, color: '#1a0f04', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 700, cursor: canApply ? 'pointer' : 'not-allowed', opacity: canApply ? 1 : 0.5, boxShadow: '0 6px 18px rgba(240,137,42,0.28)' } }, [
|
||||
h('i', { key: 'i', className: 'fas fa-check' }), 'Apply'
|
||||
]));
|
||||
|
||||
// Action buttons
|
||||
const actions = [
|
||||
React.createElement('button', {
|
||||
key: 'cancel', type: 'button', onClick: onClose,
|
||||
className: 'px-4 py-2 text-sm rounded-lg border border-white/10 text-secondary'
|
||||
}, 'Cancel'),
|
||||
React.createElement('button', {
|
||||
key: 'apply', type: 'button', onClick: handleApply, disabled: !canApply,
|
||||
className: 'px-4 py-2 text-sm rounded-lg bg-purple-500/20 border border-purple-500/30 text-primary disabled:opacity-50'
|
||||
}, 'Apply')
|
||||
];
|
||||
if (hasSaved) {
|
||||
actions.unshift(React.createElement('button', {
|
||||
key: 'forget', type: 'button', onClick: handleForget,
|
||||
className: 'px-4 py-2 text-sm rounded-lg border border-red-500/30 text-red-400 mr-auto'
|
||||
}, 'Forget saved'));
|
||||
}
|
||||
children.push(React.createElement('div', { key: 'actions', className: 'flex items-center gap-2 flex-wrap' }, actions));
|
||||
// Embedded mode (default for the new design): fill the connection screen's
|
||||
// right column and slide up over it. Fallback: a fixed right-side drawer.
|
||||
const wrapperStyle = embedded
|
||||
? { position: 'absolute', inset: 0, zIndex: 30, display: 'flex', flexDirection: 'column', background: '#0f0f11', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' }
|
||||
: { position: 'fixed', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', alignItems: 'stretch', background: '#0f0f11', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' };
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4',
|
||||
onClick: (e) => { if (e.target === e.currentTarget) onClose(); }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'modal',
|
||||
className: 'card-minimal rounded-xl p-6 max-w-lg w-full border-purple-500/20 max-h-[90vh] overflow-y-auto'
|
||||
}, children)
|
||||
return h('div', { className: 'sb-ice-overlay', style: wrapperStyle }, [
|
||||
h(React.Fragment, { key: 'panel' }, [
|
||||
// header
|
||||
h('div', { key: 'head', style: { display: 'flex', alignItems: 'center', gap: '12px', padding: '20px 24px', borderBottom: '1px solid rgba(255,255,255,0.06)' } }, [
|
||||
h('div', { key: 'ic', style: { width: '38px', height: '38px', flex: 'none', display: 'grid', placeItems: 'center', borderRadius: '10px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)' } },
|
||||
h('i', { className: 'fas fa-sliders-h', style: { color: '#cfcfd4', fontSize: '15px' } })),
|
||||
h('div', { key: 'tx', style: { flex: 1, lineHeight: 1.25 } }, [
|
||||
h('div', { key: 't', style: { fontSize: '16.5px', fontWeight: 800, letterSpacing: '-0.3px', color: '#f4f4f6' } }, 'Network settings'),
|
||||
h('div', { key: 's', style: { fontSize: '12px', color: '#7b7b83' } }, 'Configured locally — never shared with your peer')
|
||||
]),
|
||||
h('button', { key: 'x', type: 'button', onClick: onClose, style: { width: '32px', height: '32px', flex: 'none', display: 'grid', placeItems: 'center', borderRadius: '9px', border: 'none', background: 'rgba(255,255,255,0.04)', color: '#8a8a92', cursor: 'pointer' } },
|
||||
h('i', { className: 'fas fa-times' }))
|
||||
]),
|
||||
// scroll body
|
||||
h('div', { key: 'body', className: 'custom-scrollbar', style: { flex: 1, overflowY: 'auto', padding: '20px 24px' } }, body),
|
||||
// footer
|
||||
h('div', { key: 'foot', style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '10px', padding: '16px 24px', borderTop: '1px solid rgba(255,255,255,0.06)', background: '#0e0e10', borderRadius: '0' } }, footerBtns)
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
+181
-447
@@ -1,449 +1,183 @@
|
||||
// "Development Roadmap" — milestone timeline section.
|
||||
// Translated from the Claude Design component (Roadmap.dc.html): a full-bleed
|
||||
// dark band with a shipped-progress bar and an expandable, status-coded timeline.
|
||||
function Roadmap() {
|
||||
const [selectedPhase, setSelectedPhase] = React.useState(null);
|
||||
const phases = [
|
||||
{
|
||||
version: "v1.0",
|
||||
title: "Start of Development",
|
||||
status: "done",
|
||||
date: "Early 2025",
|
||||
description: "Idea, prototype, and infrastructure setup",
|
||||
features: [
|
||||
"Concept and requirements formation",
|
||||
"Stack selection: WebRTC, P2P, cryptography",
|
||||
"First messaging prototypes",
|
||||
"Repository creation and CI",
|
||||
"Basic encryption architecture",
|
||||
"UX/UI design"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v1.5",
|
||||
title: "Alpha Release",
|
||||
status: "done",
|
||||
date: "Spring 2025",
|
||||
description: "First public alpha: basic chat and key exchange",
|
||||
features: [
|
||||
"Basic P2P messaging via WebRTC",
|
||||
"Simple E2E encryption (demo scheme)",
|
||||
"Stable signaling and reconnection",
|
||||
"Minimal UX for testing",
|
||||
"Feedback collection from early testers"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v2.0",
|
||||
title: "Security Hardened",
|
||||
status: "done",
|
||||
date: "Summer 2025",
|
||||
description: "Security strengthening and stable branch release",
|
||||
features: [
|
||||
"ECDH/ECDSA implementation in production",
|
||||
"Perfect Forward Secrecy and key rotation",
|
||||
"Improved authentication checks",
|
||||
"File encryption and large payload transfers",
|
||||
"Audit of basic cryptoprocesses"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v3.0",
|
||||
title: "Scaling & Stability",
|
||||
status: "done",
|
||||
date: "Fall 2025",
|
||||
description: "Network scaling and stability improvements",
|
||||
features: [
|
||||
"Optimization of P2P connections and NAT traversal",
|
||||
"Reconnection mechanisms and message queues",
|
||||
"Reduced battery consumption on mobile",
|
||||
"Support for multi-device synchronization",
|
||||
"Monitoring and logging tools for developers"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v3.5",
|
||||
title: "Privacy-first Release",
|
||||
status: "done",
|
||||
date: "Winter 2025",
|
||||
description: "Focus on privacy: minimizing metadata",
|
||||
features: [
|
||||
"Metadata protection and fingerprint reduction",
|
||||
"Experiments with onion routing and DHT",
|
||||
"Options for anonymous connections",
|
||||
"Preparation for open code audit",
|
||||
"Improved user verification processes"
|
||||
]
|
||||
},
|
||||
|
||||
// current and future phases
|
||||
{
|
||||
version: "v4.5",
|
||||
title: "Enhanced Security Edition",
|
||||
status: "done",
|
||||
date: "Now",
|
||||
description: "Version with ECDH + DTLS + SAS security, 18-layer military-grade cryptography and complete ASN.1 validation",
|
||||
features: [
|
||||
"ECDH + DTLS + SAS triple-layer security",
|
||||
"ECDH P-384 + AES-GCM 256-bit encryption",
|
||||
"DTLS fingerprint verification",
|
||||
"SAS (Short Authentication String) verification",
|
||||
"Perfect Forward Secrecy with key rotation",
|
||||
"Enhanced MITM attack prevention",
|
||||
"Complete ASN.1 DER validation",
|
||||
"OID and EC point verification",
|
||||
"SPKI structure validation",
|
||||
"P2P WebRTC architecture",
|
||||
"Metadata protection",
|
||||
"100% open source code"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v4.7",
|
||||
title: "Desktop Edition",
|
||||
status: "current",
|
||||
date: "Now",
|
||||
description: "Native desktop applications for Windows, macOS, and Linux",
|
||||
features: [
|
||||
"Windows desktop app (Tauri v2)",
|
||||
"macOS desktop app (Tauri v2)",
|
||||
"Linux AppImage support (Tauri v2)",
|
||||
"Real-time notifications",
|
||||
"Automatic reconnection",
|
||||
"Cross-device synchronization",
|
||||
"Improved UX/UI",
|
||||
"Support for files up to 100MB"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v5.0",
|
||||
title: "Mobile Edition",
|
||||
status: "development",
|
||||
date: "Q1 2026",
|
||||
description: "Native mobile applications for iOS and Android",
|
||||
features: [
|
||||
"iOS native app (Swift/SwiftUI)",
|
||||
"Android native app (Kotlin/Jetpack Compose)",
|
||||
"PWA support for mobile browsers",
|
||||
"Real-time push notifications",
|
||||
"Battery optimization",
|
||||
"Mobile-optimized UX/UI",
|
||||
"Offline message queuing",
|
||||
"Biometric authentication"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v5.5",
|
||||
title: "Quantum-Resistant Edition",
|
||||
status: "planned",
|
||||
date: "Q2 2026",
|
||||
description: "Protection against quantum computers",
|
||||
features: [
|
||||
"Post-quantum cryptography CRYSTALS-Kyber",
|
||||
"SPHINCS+ digital signatures",
|
||||
"Hybrid scheme: classic + PQ",
|
||||
"Quantum-safe key exchange",
|
||||
"Updated hashing algorithms",
|
||||
"Migration of existing sessions",
|
||||
"Compatibility with v4.x",
|
||||
"Quantum-resistant protocols"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v6.0",
|
||||
title: "Group Communications",
|
||||
status: "planned",
|
||||
date: "Q4 2026",
|
||||
description: "Group chats with preserved privacy",
|
||||
features: [
|
||||
"P2P group connections up to 8 participants",
|
||||
"Mesh networking for groups",
|
||||
"Signal Double Ratchet for groups",
|
||||
"Anonymous groups without metadata",
|
||||
"Ephemeral groups (disappear after session)",
|
||||
"Cryptographic group administration",
|
||||
"Group member auditing"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v6.5",
|
||||
title: "Decentralized Network",
|
||||
status: "research",
|
||||
date: "2027",
|
||||
description: "Fully decentralized network",
|
||||
features: [
|
||||
"LockBit node mesh network",
|
||||
"DHT for peer discovery",
|
||||
"Built-in onion routing",
|
||||
"Tokenomics and node incentives",
|
||||
"Governance via DAO",
|
||||
"Interoperability with other networks",
|
||||
"Cross-platform compatibility",
|
||||
"Self-healing network"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v7.0",
|
||||
title: "AI Privacy Assistant",
|
||||
status: "research",
|
||||
date: "2028+",
|
||||
description: "AI for privacy and security",
|
||||
features: [
|
||||
"Local AI threat analysis",
|
||||
"Automatic MITM detection",
|
||||
"Adaptive cryptography",
|
||||
"Personalized security recommendations",
|
||||
"Zero-knowledge machine learning",
|
||||
"Private AI assistant",
|
||||
"Predictive security",
|
||||
"Autonomous attack protection"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const getStatusConfig = (status) => {
|
||||
switch (status) {
|
||||
case 'current':
|
||||
return {
|
||||
color: 'green',
|
||||
bgClass: 'bg-green-500/10 border-green-500/20',
|
||||
textClass: 'text-green-400',
|
||||
icon: 'fas fa-check-circle',
|
||||
label: 'Current Version'
|
||||
};
|
||||
case 'development':
|
||||
return {
|
||||
color: 'orange',
|
||||
bgClass: 'bg-orange-500/10 border-orange-500/20',
|
||||
textClass: 'text-orange-400',
|
||||
icon: 'fas fa-code',
|
||||
label: 'In Development'
|
||||
};
|
||||
case 'planned':
|
||||
return {
|
||||
color: 'blue',
|
||||
bgClass: 'bg-blue-500/10 border-blue-500/20',
|
||||
textClass: 'text-blue-400',
|
||||
icon: 'fas fa-calendar-alt',
|
||||
label: 'Planned'
|
||||
};
|
||||
case 'research':
|
||||
return {
|
||||
color: 'purple',
|
||||
bgClass: 'bg-purple-500/10 border-purple-500/20',
|
||||
textClass: 'text-purple-400',
|
||||
icon: 'fas fa-flask',
|
||||
label: 'Research'
|
||||
};
|
||||
case 'done':
|
||||
return {
|
||||
color: 'gray',
|
||||
bgClass: 'bg-gray-500/10 border-gray-500/20',
|
||||
textClass: 'text-gray-300',
|
||||
icon: 'fas fa-flag-checkered',
|
||||
label: 'Released'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: 'gray',
|
||||
bgClass: 'bg-gray-500/10 border-gray-500/20',
|
||||
textClass: 'text-gray-400',
|
||||
icon: 'fas fa-question',
|
||||
label: 'Unknown'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const togglePhaseDetail = (index) => {
|
||||
setSelectedPhase(selectedPhase === index ? null : index);
|
||||
};
|
||||
return (
|
||||
<div key="roadmap-section" className="mt-16 px-4 sm:px-0">
|
||||
<div key="section-header" className="text-center mb-12">
|
||||
<h3 key="title" className="text-2xl font-semibold text-primary mb-3">
|
||||
Development Roadmap
|
||||
</h3>
|
||||
<p key="subtitle" className="text-secondary max-w-2xl mx-auto mb-6">
|
||||
Evolution of SecureBit.chat : from initial development to quantum-resistant decentralized network with complete ASN.1 validation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div key="roadmap-container" className="max-w-6xl mx-auto">
|
||||
<div key="timeline" className="relative">
|
||||
{/* The line has been removed */}
|
||||
|
||||
<div key="phases" className="space-y-8">
|
||||
{phases.map((phase, index) => {
|
||||
const statusConfig = getStatusConfig(phase.status);
|
||||
const isExpanded = selectedPhase === index;
|
||||
|
||||
return (
|
||||
<div key={`phase-${index}`} className="relative">
|
||||
{/* The dots are visible only on sm and larger screens */}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isExpanded}
|
||||
onClick={() => togglePhaseDetail(index)}
|
||||
key={`phase-button-${index}`}
|
||||
className={`card-minimal rounded-xl p-4 text-left w-full transition-all duration-300 ${
|
||||
isExpanded
|
||||
? "ring-2 ring-" + statusConfig.color + "-500/30"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
key="phase-header"
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4 space-y-2 sm:space-y-0"
|
||||
>
|
||||
<div
|
||||
key="phase-info"
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:space-x-4"
|
||||
>
|
||||
<div
|
||||
key="version-badge"
|
||||
className={`px-3 py-1 ${statusConfig.bgClass} border rounded-lg mb-2 sm:mb-0`}
|
||||
>
|
||||
<span
|
||||
key="version"
|
||||
className={`${statusConfig.textClass} font-bold text-sm`}
|
||||
>
|
||||
{phase.version}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div key="title-section">
|
||||
<h4
|
||||
key="title"
|
||||
className="text-lg font-semibold text-primary"
|
||||
>
|
||||
{phase.title}
|
||||
</h4>
|
||||
<p
|
||||
key="description"
|
||||
className="text-secondary text-sm"
|
||||
>
|
||||
{phase.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
key="phase-meta"
|
||||
className="flex items-center space-x-3 text-sm text-gray-400 font-medium"
|
||||
>
|
||||
<div
|
||||
key="status-badge"
|
||||
className={`flex items-center px-3 py-1 ${statusConfig.bgClass} border rounded-lg`}
|
||||
>
|
||||
<i
|
||||
key="status-icon"
|
||||
className={`${statusConfig.icon} ${statusConfig.textClass} mr-2 text-xs`}
|
||||
/>
|
||||
<span
|
||||
key="status-text"
|
||||
className={`${statusConfig.textClass} text-xs font-medium`}
|
||||
>
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div key="date">{phase.date}</div>
|
||||
<i
|
||||
key="expand-icon"
|
||||
className={`fas fa-chevron-${
|
||||
isExpanded ? "up" : "down"
|
||||
} text-gray-400 text-sm`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div
|
||||
key="features-section"
|
||||
className="mt-6 pt-6 border-t border-gray-700/30"
|
||||
>
|
||||
<h5
|
||||
key="features-title"
|
||||
className="text-primary font-medium mb-4 flex items-center"
|
||||
>
|
||||
<i
|
||||
key="features-icon"
|
||||
className="fas fa-list-ul mr-2 text-sm"
|
||||
/>
|
||||
Key features:
|
||||
</h5>
|
||||
|
||||
<div
|
||||
key="features-grid"
|
||||
className="grid md:grid-cols-2 gap-3"
|
||||
>
|
||||
{phase.features.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={`feature-${featureIndex}`}
|
||||
className="flex items-center space-x-3 p-3 bg-custom-bg rounded-lg"
|
||||
>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${statusConfig.textClass.replace(
|
||||
"text-",
|
||||
"bg-"
|
||||
)}`}
|
||||
/>
|
||||
<span className="text-secondary text-sm">
|
||||
{feature}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div key="cta-section" className="mt-12 text-center">
|
||||
<div
|
||||
key="cta-card"
|
||||
className="card-minimal rounded-xl p-8 max-w-2xl mx-auto"
|
||||
>
|
||||
<h4
|
||||
key="cta-title"
|
||||
className="text-xl font-semibold text-primary mb-3"
|
||||
>
|
||||
Join the future of privacy
|
||||
</h4>
|
||||
<p key="cta-description" className="text-secondary mb-6">
|
||||
SecureBit.chat grows thanks to the community. Your ideas and feedback help shape the future of secure communication with complete ASN.1 validation.
|
||||
</p>
|
||||
|
||||
<div
|
||||
key="cta-buttons"
|
||||
className="flex flex-col sm:flex-row gap-4 justify-center"
|
||||
>
|
||||
<a
|
||||
key="github-link"
|
||||
href="https://github.com/SecureBitChat/securebit-chat/"
|
||||
className="btn-primary text-white py-3 px-6 rounded-lg font-medium transition-all duration-200 flex items-center justify-center"
|
||||
>
|
||||
<i key="github-icon" className="fab fa-github mr-2" />
|
||||
GitHub Repository
|
||||
</a>
|
||||
|
||||
<a
|
||||
key="feedback-link"
|
||||
href="mailto:lockbitchat@tutanota.com"
|
||||
className="btn-secondary text-white py-3 px-6 rounded-lg font-medium transition-all duration-200 flex items-center justify-center"
|
||||
>
|
||||
<i key="feedback-icon" className="fas fa-comments mr-2" />
|
||||
Feedback
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
const [isMobile, setIsMobile] = React.useState(
|
||||
typeof window !== 'undefined' && window.matchMedia('(max-width:767px)').matches
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mq = window.matchMedia('(max-width:767px)');
|
||||
const onChange = () => setIsMobile(mq.matches);
|
||||
mq.addEventListener ? mq.addEventListener('change', onChange) : mq.addListener(onChange);
|
||||
return () => {
|
||||
mq.removeEventListener ? mq.removeEventListener('change', onChange) : mq.removeListener(onChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
const DATA = [
|
||||
{ v: "v1.0", title: "Start of Development", sub: "Idea, prototype, and infrastructure setup", status: "released", date: "Early 2025",
|
||||
features: ["Concept and requirements formation", "Stack selection: WebRTC, P2P, cryptography", "First messaging prototypes", "Repository creation and CI", "Basic encryption architecture", "UX/UI design"] },
|
||||
{ v: "v1.5", title: "Alpha Release", sub: "First public alpha: basic chat and key exchange", status: "released", date: "Spring 2025",
|
||||
features: ["Basic P2P messaging via WebRTC", "Simple E2E encryption (demo scheme)", "Stable signaling and reconnection", "Minimal UX for testing", "Feedback collection from early testers"] },
|
||||
{ v: "v2.0", title: "Security Hardened", sub: "Security strengthening and stable branch release", status: "released", date: "Summer 2025",
|
||||
features: ["ECDH/ECDSA implementation in production", "Perfect Forward Secrecy and key rotation", "Improved authentication checks", "File encryption and large payload transfers", "Audit of basic cryptoprocesses"] },
|
||||
{ v: "v3.0", title: "Scaling & Stability", sub: "Network scaling and stability improvements", status: "released", date: "Fall 2025",
|
||||
features: ["Optimization of P2P connections and NAT traversal", "Reconnection mechanisms and message queues", "Reduced battery consumption on mobile", "Multi-device synchronization support", "Monitoring and logging tools for developers"] },
|
||||
{ v: "v3.5", title: "Privacy-first Release", sub: "Focus on privacy: minimizing metadata", status: "released", date: "Winter 2025",
|
||||
features: ["Metadata protection and fingerprint reduction", "Experiments with onion routing and DHT", "Options for anonymous connections", "Preparation for open code audit", "Improved user verification processes"] },
|
||||
{ v: "v4.5", title: "Enhanced Security Edition", sub: "18-layer military-grade cryptography with complete ASN.1 validation", status: "released", date: "Late 2025",
|
||||
features: ["ECDH + DTLS + SAS triple-layer security", "ECDH P-384 + AES-GCM 256-bit encryption", "DTLS fingerprint verification", "SAS (Short Authentication String) verification", "Perfect Forward Secrecy with key rotation", "Enhanced MITM attack prevention", "Complete ASN.1 DER validation", "OID and EC point verification", "SPKI structure validation", "P2P WebRTC architecture", "Metadata protection", "100% open source code"] },
|
||||
{ v: "v4.7", title: "Desktop Edition", sub: "Native desktop apps for Windows, macOS, and Linux", status: "current", date: "Now",
|
||||
features: ["Windows desktop app (Tauri v2)", "macOS desktop app (Tauri v2)", "Linux AppImage support (Tauri v2)", "Real-time notifications", "Automatic reconnection", "Cross-device synchronization", "Improved UX/UI", "Support for files up to 100MB"] },
|
||||
{ v: "v5.0", title: "Mobile Edition", sub: "Native mobile apps for iOS and Android", status: "dev", date: "Q1 2026",
|
||||
features: ["iOS native app (Swift/SwiftUI)", "Android native app (Kotlin/Jetpack Compose)", "PWA support for mobile browsers", "Real-time push notifications", "Battery optimization", "Mobile-optimized UX/UI", "Offline message queuing", "Biometric authentication"] },
|
||||
{ v: "v5.5", title: "Quantum-Resistant Edition", sub: "Protection against quantum computers", status: "planned", date: "Q2 2026",
|
||||
features: ["Post-quantum cryptography CRYSTALS-Kyber", "SPHINCS+ digital signatures", "Hybrid scheme: classic + PQ", "Quantum-safe key exchange", "Updated hashing algorithms", "Migration of existing sessions", "Compatibility with v4.x", "Quantum-resistant protocols"] },
|
||||
{ v: "v6.0", title: "Group Communications", sub: "Group chats with preserved privacy", status: "planned", date: "Q4 2026",
|
||||
features: ["P2P group connections up to 8 participants", "Mesh networking for groups", "Signal Double Ratchet for groups", "Anonymous groups without metadata", "Ephemeral groups (disappear after session)", "Cryptographic group administration", "Group member auditing"] },
|
||||
{ v: "v6.5", title: "Decentralized Network", sub: "Fully decentralized network", status: "research", date: "2027",
|
||||
features: ["Node mesh network", "DHT for peer discovery", "Built-in onion routing", "Tokenomics and node incentives", "Governance via DAO", "Interoperability with other networks", "Cross-platform compatibility", "Self-healing network"] },
|
||||
{ v: "v7.0", title: "AI Privacy Assistant", sub: "AI for privacy and security", status: "research", date: "2028+",
|
||||
features: ["Local AI threat analysis", "Automatic MITM detection", "Adaptive cryptography", "Personalized security recommendations", "Zero-knowledge machine learning", "Private AI assistant", "Predictive security", "Autonomous attack protection"] }
|
||||
];
|
||||
|
||||
const META = {
|
||||
released: { word: "Released", color: "#3ecf8e", line: "rgba(62,207,142,0.32)" },
|
||||
current: { word: "Current", color: "#f0892a", line: "rgba(240,137,42,0.32)" },
|
||||
dev: { word: "In development", color: "#e3b341", line: "rgba(255,255,255,0.08)" },
|
||||
planned: { word: "Planned", color: "#8a8a92", line: "rgba(255,255,255,0.08)" },
|
||||
research: { word: "Research", color: "#6b6b73", line: "rgba(255,255,255,0.08)" }
|
||||
};
|
||||
|
||||
const [open, setOpen] = React.useState({});
|
||||
const isOpen = (i) => (open[i] === undefined ? DATA[i].status === 'current' : open[i]);
|
||||
const toggle = (i) => setOpen((s) => ({ ...s, [i]: !isOpen(i) }));
|
||||
|
||||
const hexA = (hex, a) => {
|
||||
const n = parseInt(hex.slice(1), 16);
|
||||
return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`;
|
||||
};
|
||||
|
||||
const total = DATA.length;
|
||||
const shipped = DATA.filter((d) => d.status === 'released' || d.status === 'current').length;
|
||||
const upcoming = total - shipped;
|
||||
const shippedPct = (shipped / total * 100).toFixed(1) + '%';
|
||||
|
||||
const renderNode = (status) => {
|
||||
if (status === 'released') {
|
||||
return (
|
||||
<div style={{ position: 'absolute', left: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(62,207,142,0.16),rgba(62,207,142,0.16)), #0f0f11', border: '1px solid rgba(62,207,142,0.4)', zIndex: 2 }}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#3ecf8e" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M5 13l4 4 10-11" /></svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (status === 'current') {
|
||||
return (
|
||||
<div style={{ position: 'absolute', left: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(240,137,42,0.2),rgba(240,137,42,0.2)), #0f0f11', border: '1px solid #f0892a', zIndex: 2, animation: 'rmPulse 2.4s ease-out infinite' }}>
|
||||
<span style={{ width: '9px', height: '9px', borderRadius: '50%', background: '#f0892a' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (status === 'dev') {
|
||||
return (
|
||||
<div style={{ position: 'absolute', left: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(227,179,65,0.15),rgba(227,179,65,0.15)), #0f0f11', border: '1px solid rgba(227,179,65,0.4)', zIndex: 2 }}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#e3b341" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3a9 9 0 1 0 9 9" /></svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// planned / research
|
||||
return (
|
||||
<div style={{ position: 'absolute', left: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: '#0f0f11', border: `1px ${status === 'research' ? 'dashed' : 'solid'} rgba(255,255,255,0.18)`, zIndex: 2 }}>
|
||||
<span style={{ width: '7px', height: '7px', borderRadius: '50%', background: META[status].color }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section style={{ width: '100%', color: '#e8e8eb', fontFamily: SANS, padding: isMobile ? '48px 0' : '64px 0', background: 'radial-gradient(1200px 720px at 50% -8%, rgba(240,137,42,0.05), transparent 60%), #0f0f11' }}>
|
||||
<style dangerouslySetInnerHTML={{ __html: '@keyframes rmExp{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes rmPulse{0%,100%{box-shadow:0 0 0 0 rgba(240,137,42,0.18)}60%{box-shadow:0 0 0 9px rgba(240,137,42,0)}}' }} />
|
||||
|
||||
<div style={{ maxWidth: '1040px', margin: '0 auto', padding: isMobile ? '0 18px' : '0 40px' }}>
|
||||
|
||||
{/* header */}
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<div style={{ fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.6px', marginBottom: '13px' }}>Development Roadmap</div>
|
||||
<h2 style={{ margin: '0 0 14px', fontSize: isMobile ? '27px' : '34px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.08, color: '#f4f4f6' }}>The evolution of SecureBit</h2>
|
||||
<p style={{ margin: 0, fontSize: '15.5px', lineHeight: 1.6, color: '#8a8a92', maxWidth: '660px' }}>From the first prototype to a quantum-resistant, decentralized network — with complete ASN.1 validation at every layer.</p>
|
||||
</div>
|
||||
|
||||
{/* progress */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '18px', flexWrap: 'wrap', padding: '18px 22px', borderRadius: '14px', background: '#141416', border: '1px solid rgba(255,255,255,0.06)', marginBottom: '36px' }}>
|
||||
<div style={{ fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: '#e8e8eb', whiteSpace: 'nowrap' }}><span style={{ color: '#3ecf8e' }}>{shipped}</span> of {total} milestones shipped</div>
|
||||
<div style={{ flex: '1 1 240px', minWidth: '200px', height: '8px', borderRadius: '99px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: shippedPct, background: 'linear-gradient(90deg, #3ecf8e, #f0892a)' }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
window.Roadmap = Roadmap;
|
||||
<div style={{ fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '0.8px', whiteSpace: 'nowrap' }}>{upcoming} on the way</div>
|
||||
</div>
|
||||
|
||||
{/* timeline */}
|
||||
{DATA.map((d, i) => {
|
||||
const meta = META[d.status];
|
||||
const opened = isOpen(i);
|
||||
const notLast = i < total - 1;
|
||||
return (
|
||||
<div key={i} style={{ position: 'relative', display: 'grid', gridTemplateColumns: '54px 1fr', marginBottom: '16px' }}>
|
||||
|
||||
{/* spine */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
{notLast && <div style={{ position: 'absolute', left: '26px', top: '30px', height: 'calc(100% + 16px)', width: '2px', background: meta.line }} />}
|
||||
{renderNode(d.status)}
|
||||
</div>
|
||||
|
||||
{/* card */}
|
||||
<div style={{ borderRadius: '16px', background: '#141416', border: `1px solid ${d.status === 'current' ? 'rgba(240,137,42,0.28)' : 'rgba(255,255,255,0.06)'}`, overflow: 'hidden' }}>
|
||||
<div
|
||||
onClick={() => toggle(i)}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: isMobile ? '11px' : '16px', padding: isMobile ? '16px 16px' : '18px 22px', cursor: 'pointer', transition: 'background .18s ease' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.018)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
|
||||
>
|
||||
<div style={{ flex: 'none', minWidth: '52px', textAlign: 'center', padding: '7px 10px', borderRadius: '9px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.07)', fontFamily: MONO, fontSize: '13px', fontWeight: 700, color: d.status === 'current' ? '#f0892a' : '#cfcfd4' }}>{d.v}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: isMobile ? '15.5px' : '17px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' }}>{d.title}</div>
|
||||
{!isMobile && <div style={{ marginTop: '3px', fontSize: '13.5px', color: '#9a9aa2' }}>{d.sub}</div>}
|
||||
</div>
|
||||
<div style={{ flex: 'none', display: 'flex', alignItems: 'center', gap: isMobile ? '8px' : '14px' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '7px', padding: '6px 11px', borderRadius: '8px', background: hexA(meta.color, 0.1), border: `1px solid ${hexA(meta.color, 0.22)}`, fontFamily: MONO, fontSize: '10.5px', fontWeight: 600, color: meta.color, textTransform: 'uppercase', letterSpacing: '0.8px', whiteSpace: 'nowrap' }}>
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: meta.color }} />
|
||||
{!isMobile && meta.word}
|
||||
</span>
|
||||
{!isMobile && <span style={{ fontFamily: MONO, fontSize: '12px', fontWeight: 500, color: '#8a8a92', whiteSpace: 'nowrap', minWidth: '74px', textAlign: 'right' }}>{d.date}</span>}
|
||||
<span style={{ color: '#6b6b73', display: 'inline-flex', transition: 'transform .22s cubic-bezier(.2,.7,.3,1)', transform: opened ? 'rotate(180deg)' : 'rotate(0deg)' }}>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round"><path d="M6 9l6 6 6-6" /></svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{opened && (
|
||||
<div style={{ padding: '4px 22px 22px 22px', animation: 'rmExp .24s cubic-bezier(.2,.7,.3,1)' }}>
|
||||
<div style={{ fontFamily: MONO, fontSize: '10px', fontWeight: 600, color: '#56565e', textTransform: 'uppercase', letterSpacing: '1.2px', marginBottom: '14px', paddingTop: '14px', borderTop: '1px solid rgba(255,255,255,0.05)' }}>Key features</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: '11px 28px' }}>
|
||||
{d.features.map((f, fi) => (
|
||||
<div key={fi} style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}>
|
||||
<span style={{ flex: 'none', marginTop: '7px', width: '5px', height: '5px', borderRadius: '50%', background: meta.color }} />
|
||||
<span style={{ fontSize: '13.5px', lineHeight: 1.5, color: '#cfcfd4' }}>{f}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
window.Roadmap = Roadmap;
|
||||
|
||||
@@ -1,209 +1,239 @@
|
||||
// Enhanced Modern Slider Component with Loading Protection
|
||||
// "Why SecureBit is unique" — interactive accordion section.
|
||||
// Translated from the Claude Design component (Why Unique.dc.html) into the
|
||||
// project's React.createElement style. Five horizontal panels; the active one
|
||||
// expands to reveal full content, the rest collapse to a vertical spine label.
|
||||
const UniqueFeatureSlider = () => {
|
||||
const trackRef = React.useRef(null);
|
||||
const wrapRef = React.useRef(null);
|
||||
const [current, setCurrent] = React.useState(0);
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
const [active, setActive] = React.useState(0);
|
||||
const [isMobile, setIsMobile] = React.useState(
|
||||
typeof window !== 'undefined' && window.matchMedia('(max-width:767px)').matches
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mq = window.matchMedia('(max-width:767px)');
|
||||
const onChange = () => setIsMobile(mq.matches);
|
||||
mq.addEventListener ? mq.addEventListener('change', onChange) : mq.addListener(onChange);
|
||||
return () => {
|
||||
mq.removeEventListener ? mq.removeEventListener('change', onChange) : mq.removeListener(onChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ACCENT = '#f0892a';
|
||||
const ACTIVE_BG = 'radial-gradient(130% 90% at 28% 0%, rgba(240,137,42,0.11), transparent 60%), #141416';
|
||||
const ACTIVE_BD = 'rgba(240,137,42,0.3)';
|
||||
const IDLE_BG = '#111113';
|
||||
const IDLE_BD = 'rgba(255,255,255,0.06)';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
const slides = [
|
||||
{
|
||||
icon: "🛡️",
|
||||
bgImage: "linear-gradient(135deg, rgb(255 107 53 / 6%) 0%, rgb(255 140 66 / 45%) 100%)",
|
||||
thumbIcon: "🔒",
|
||||
title: "18-Layer Military Security",
|
||||
description: "Revolutionary defense system with ECDH P-384 + AES-GCM 256 + ECDSA + Complete ASN.1 Validation."
|
||||
num: '01',
|
||||
title: ['Layered', 'encryption core'],
|
||||
collapsed: 'Encryption core',
|
||||
desc: 'ECDH P-384 key exchange, AES-256-GCM payloads, ECDSA signatures and full ASN.1 validation — composed into one hardened pipeline.',
|
||||
tags: ['ECDH P-384', 'AES-256-GCM', 'ECDSA', 'ASN.1'],
|
||||
icon: '<path d="M12 3l8 4v5c0 4.5-3.2 7.8-8 9-4.8-1.2-8-4.5-8-9V7l8-4z"/><path d="M9.2 12.2l2 2 3.6-3.8"/>'
|
||||
},
|
||||
{
|
||||
icon: "🌐",
|
||||
bgImage: "linear-gradient(135deg, rgb(147 51 234 / 6%) 0%, rgb(168 85 247 / 45%) 100%)",
|
||||
thumbIcon: "🔗",
|
||||
title: "Pure P2P WebRTC",
|
||||
description: "Direct peer-to-peer connections without any servers. Complete decentralization with zero infrastructure."
|
||||
num: '02',
|
||||
title: ['Pure P2P', 'WebRTC'],
|
||||
collapsed: 'Pure P2P WebRTC',
|
||||
desc: 'Messages travel directly between devices over WebRTC. No relay holds your data — the server only helps two peers find each other.',
|
||||
tags: ['DTLS 1.3', 'No relay'],
|
||||
icon: '<circle cx="5.5" cy="12" r="2.5"/><circle cx="18.5" cy="6" r="2.5"/><circle cx="18.5" cy="18" r="2.5"/><path d="M7.8 10.8l8.4-3.6M7.8 13.2l8.4 3.6"/>'
|
||||
},
|
||||
{
|
||||
icon: "🔄",
|
||||
bgImage: "linear-gradient(135deg, rgb(16 185 129 / 6%) 0%, rgb(52 211 153 / 45%) 100%)",
|
||||
thumbIcon: "⚡",
|
||||
title: "Perfect Forward Secrecy",
|
||||
description: "Automatic key rotation every 5 minutes. Non-extractable keys with hardware protection."
|
||||
num: '03',
|
||||
title: ['Perfect', 'forward secrecy'],
|
||||
collapsed: 'Forward secrecy',
|
||||
desc: 'Session keys rotate continuously and are discarded after use, so a single compromised key can never unlock past conversations.',
|
||||
tags: ['Ephemeral keys', 'Auto-rotate'],
|
||||
icon: '<path d="M21 8a8.5 8.5 0 0 0-15.6-2.5M3 4v4h4"/><path d="M3 16a8.5 8.5 0 0 0 15.6 2.5M21 20v-4h-4"/>'
|
||||
},
|
||||
{
|
||||
icon: "🎭",
|
||||
bgImage: "linear-gradient(135deg, rgb(6 182 212 / 6%) 0%, rgb(34 211 238 / 45%) 100%)",
|
||||
thumbIcon: "🌫️",
|
||||
title: "Traffic Obfuscation",
|
||||
description: "Fake traffic generation and pattern masking make communication indistinguishable from noise."
|
||||
num: '04',
|
||||
title: ['Traffic', 'obfuscation'],
|
||||
collapsed: 'Traffic obfuscation',
|
||||
desc: 'Packet sizes and timing are padded and randomized, hiding metadata patterns from anyone watching the wire.',
|
||||
tags: ['Packet padding', 'Timing jitter'],
|
||||
icon: '<path d="M3 7h4l3 10h4M14 7h3l3 0"/><path d="M17 4l3 3-3 3"/><path d="M3 17h4l2-6"/>'
|
||||
},
|
||||
{
|
||||
icon: "👁️",
|
||||
bgImage: "linear-gradient(135deg, rgb(37 99 235 / 6%) 0%, rgb(59 130 246 / 45%) 100%)",
|
||||
thumbIcon: "🚫",
|
||||
title: "Zero Data Collection",
|
||||
description: "No registration, no servers, no logs. Complete anonymity with instant channels."
|
||||
num: '05',
|
||||
title: ['Zero data', 'collection'],
|
||||
collapsed: 'Zero data collection',
|
||||
desc: 'No accounts, no logs, no message storage. There is nothing on a server to leak, subpoena, or sell.',
|
||||
tags: ['No accounts', 'No logs'],
|
||||
icon: '<path d="M9.9 5.1A9.6 9.6 0 0 1 12 5c5.5 0 9 5 9 7a11 11 0 0 1-2.2 3M6.3 7.3C3.6 8.9 2 11.2 2 12c0 1.4 3.5 7 10 7 1.6 0 3-.3 4.2-.8"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2M3 3l18 18"/>'
|
||||
}
|
||||
];
|
||||
|
||||
// Проверка готовности компонента
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsReady(true);
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const isMobile = () => window.matchMedia("(max-width:767px)").matches;
|
||||
|
||||
const center = React.useCallback((i) => {
|
||||
if (!trackRef.current || !wrapRef.current) return;
|
||||
const card = trackRef.current.children[i];
|
||||
if (!card) return;
|
||||
|
||||
const axis = isMobile() ? "top" : "left";
|
||||
const size = isMobile() ? "clientHeight" : "clientWidth";
|
||||
const start = isMobile() ? card.offsetTop : card.offsetLeft;
|
||||
|
||||
wrapRef.current.scrollTo({
|
||||
[axis]: start - (wrapRef.current[size] / 2 - card[size] / 2),
|
||||
behavior: "smooth"
|
||||
const svg = (inner, size, stroke, sw) =>
|
||||
React.createElement('svg', {
|
||||
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
|
||||
stroke, strokeWidth: sw, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
dangerouslySetInnerHTML: { __html: inner }
|
||||
});
|
||||
}, []);
|
||||
|
||||
const activate = React.useCallback((i, scroll = false) => {
|
||||
if (i === current) return;
|
||||
setCurrent(i);
|
||||
if (scroll) {
|
||||
setTimeout(() => center(i), 50);
|
||||
}
|
||||
}, [current, center]);
|
||||
const go = (step) =>
|
||||
setActive((a) => (a + step + slides.length) % slides.length);
|
||||
|
||||
const go = (step) => {
|
||||
const newIndex = Math.min(Math.max(current + step, 0), slides.length - 1);
|
||||
activate(newIndex, true);
|
||||
};
|
||||
const navBtn = (key, onClick, path) =>
|
||||
React.createElement('button', {
|
||||
key, onClick, 'aria-label': key,
|
||||
style: {
|
||||
width: '46px', height: '46px', display: 'grid', placeItems: 'center',
|
||||
borderRadius: '50%', border: '1px solid rgba(255,255,255,0.1)',
|
||||
background: 'rgba(255,255,255,0.025)', color: '#cfcfd4', cursor: 'pointer',
|
||||
transition: 'all .2s cubic-bezier(.2,.7,.3,1)'
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.borderColor = ACTIVE_BD; e.currentTarget.style.color = ACCENT; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'; e.currentTarget.style.color = '#cfcfd4'; }
|
||||
}, svg(path, 18, 'currentColor', 2.1));
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeydown = (e) => {
|
||||
if (["ArrowRight", "ArrowDown"].includes(e.key)) go(1);
|
||||
if (["ArrowLeft", "ArrowUp"].includes(e.key)) go(-1);
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeydown, { passive: true });
|
||||
return () => window.removeEventListener("keydown", handleKeydown);
|
||||
}, [current]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isReady) {
|
||||
center(current);
|
||||
}
|
||||
}, [current, center, isReady]);
|
||||
// Render loading state if not ready
|
||||
if (!isReady) {
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
background: 'transparent',
|
||||
minHeight: '400px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
},
|
||||
React.createElement('div', {
|
||||
style: {
|
||||
opacity: 0.5,
|
||||
fontSize: '14px',
|
||||
color: '#fff'
|
||||
}
|
||||
}, 'Loading...')
|
||||
);
|
||||
}
|
||||
|
||||
return React.createElement('section', { style: { background: 'transparent' } }, [
|
||||
// Header
|
||||
React.createElement('div', {
|
||||
key: 'head',
|
||||
className: 'head'
|
||||
const tag = (label) =>
|
||||
React.createElement('span', {
|
||||
key: label,
|
||||
style: {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '7px', padding: '7px 12px',
|
||||
borderRadius: '9px', border: '1px solid rgba(255,255,255,0.07)',
|
||||
background: 'rgba(255,255,255,0.025)', fontFamily: MONO,
|
||||
fontSize: '11.5px', fontWeight: 500, color: '#9a9aa2'
|
||||
}
|
||||
}, [
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
className: 'text-2xl sm:text-3xl font-bold text-white mb-4 leading-snug'
|
||||
}, 'Why SecureBit.chat is unique'),
|
||||
React.createElement('div', {
|
||||
key: 'controls',
|
||||
className: 'controls'
|
||||
React.createElement('span', { key: 'dot', style: { width: '5px', height: '5px', borderRadius: '50%', background: '#3ecf8e' } }),
|
||||
label
|
||||
]);
|
||||
|
||||
const expandedContent = (s) =>
|
||||
React.createElement('div', {
|
||||
key: 'exp',
|
||||
style: {
|
||||
height: '100%', display: 'flex', flexDirection: 'column',
|
||||
justifyContent: isMobile ? 'flex-start' : 'space-between',
|
||||
gap: isMobile ? '18px' : 0,
|
||||
padding: isMobile ? '24px 22px' : '32px 34px',
|
||||
minWidth: isMobile ? 'auto' : '320px',
|
||||
animation: 'wuUp .42s cubic-bezier(.2,.7,.3,1)'
|
||||
}
|
||||
}, [
|
||||
React.createElement('div', { key: 'top', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }, [
|
||||
React.createElement('div', {
|
||||
key: 'ic',
|
||||
style: {
|
||||
width: '54px', height: '54px', borderRadius: '15px', display: 'grid', placeItems: 'center',
|
||||
background: 'rgba(240,137,42,0.13)', border: '1px solid rgba(240,137,42,0.3)'
|
||||
}
|
||||
}, svg(s.icon, 26, ACCENT, 1.9)),
|
||||
React.createElement('span', { key: 'n', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 600, color: '#6b6b73' } }, s.num)
|
||||
]),
|
||||
React.createElement('div', { key: 'mid' }, [
|
||||
React.createElement('h3', {
|
||||
key: 'h', style: { margin: '0 0 12px', fontSize: isMobile ? '24px' : '30px', fontWeight: 800, letterSpacing: '-0.7px', lineHeight: 1.08, color: '#f4f4f6' }
|
||||
}, [s.title[0], React.createElement('br', { key: 'br' }), s.title[1]]),
|
||||
React.createElement('p', {
|
||||
key: 'p', style: { margin: 0, fontSize: '15px', lineHeight: 1.6, color: '#9a9aa2', maxWidth: '380px' }
|
||||
}, s.desc)
|
||||
]),
|
||||
React.createElement('div', { key: 'tags', style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } }, s.tags.map(tag))
|
||||
]);
|
||||
|
||||
const collapsedContent = (s) => isMobile
|
||||
? React.createElement('div', {
|
||||
key: 'col',
|
||||
style: { display: 'flex', alignItems: 'center', gap: '16px', padding: '20px 22px' }
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'prev',
|
||||
id: 'prev-slider',
|
||||
className: 'nav-btn',
|
||||
'aria-label': 'Prev',
|
||||
disabled: current === 0,
|
||||
onClick: () => go(-1)
|
||||
}, '‹'),
|
||||
React.createElement('button', {
|
||||
key: 'next',
|
||||
id: 'next-slider',
|
||||
className: 'nav-btn',
|
||||
'aria-label': 'Next',
|
||||
disabled: current === slides.length - 1,
|
||||
onClick: () => go(1)
|
||||
}, '›')
|
||||
React.createElement('span', { key: 'n', style: { fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: '#56565e' } }, s.num),
|
||||
React.createElement('span', { key: 'l', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.2px', color: '#cfcfd4' } }, s.collapsed)
|
||||
])
|
||||
: React.createElement('div', {
|
||||
key: 'col',
|
||||
style: { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'space-between', padding: '24px 0' }
|
||||
}, [
|
||||
React.createElement('span', { key: 'n', style: { fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: '#56565e' } }, s.num),
|
||||
React.createElement('span', {
|
||||
key: 'l',
|
||||
style: { writingMode: 'vertical-rl', transform: 'rotate(180deg)', fontSize: '17px', fontWeight: 800, letterSpacing: '-0.2px', color: '#cfcfd4', whiteSpace: 'nowrap' }
|
||||
}, s.collapsed),
|
||||
svg(s.icon, 22, '#56565e', 1.8)
|
||||
]);
|
||||
|
||||
const panels = slides.map((s, i) => {
|
||||
const isActive = active === i;
|
||||
return React.createElement('div', {
|
||||
key: i,
|
||||
onClick: () => setActive(i),
|
||||
// Selection is click-only (like the design); hover just brightens the panel
|
||||
// a touch so the orange glow never jumps around chasing the cursor.
|
||||
onMouseEnter: (e) => { if (!isActive) e.currentTarget.style.filter = 'brightness(1.18)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.filter = 'none'; },
|
||||
style: {
|
||||
flex: isMobile ? 'none' : (isActive ? 6.2 : 1),
|
||||
minWidth: isMobile ? 'auto' : '72px',
|
||||
position: 'relative',
|
||||
borderRadius: '18px',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
background: isActive ? ACTIVE_BG : IDLE_BG,
|
||||
border: '1px solid ' + (isActive ? ACTIVE_BD : IDLE_BD),
|
||||
color: '#8a8a92',
|
||||
transition: 'flex .46s cubic-bezier(.2,.7,.3,1), background .3s ease, border-color .3s ease, filter .2s ease'
|
||||
}
|
||||
}, isActive ? expandedContent(s) : collapsedContent(s));
|
||||
});
|
||||
|
||||
const inner = React.createElement('div', {
|
||||
key: 'inner',
|
||||
style: {
|
||||
maxWidth: '1180px', margin: '0 auto',
|
||||
padding: isMobile ? '0 18px' : '0 40px'
|
||||
}
|
||||
}, [
|
||||
// Header
|
||||
React.createElement('div', {
|
||||
key: 'head',
|
||||
style: { display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: '24px', marginBottom: '28px' }
|
||||
}, [
|
||||
React.createElement('div', { key: 'titles' }, [
|
||||
React.createElement('div', {
|
||||
key: 'eyebrow',
|
||||
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.4px', marginBottom: '12px' }
|
||||
}, 'What sets us apart'),
|
||||
React.createElement('h2', {
|
||||
key: 'h2',
|
||||
style: { margin: 0, fontSize: isMobile ? '28px' : '38px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.05, color: '#f4f4f6' }
|
||||
}, 'Why SecureBit is unique')
|
||||
]),
|
||||
React.createElement('div', { key: 'nav', style: { display: 'flex', alignItems: 'center', gap: '10px', flex: 'none' } }, [
|
||||
navBtn('prev', () => go(-1), '<path d="M15 6l-6 6 6 6"/>'),
|
||||
navBtn('next', () => go(1), '<path d="M9 6l6 6-6 6"/>')
|
||||
])
|
||||
]),
|
||||
|
||||
// Slider
|
||||
// Accordion
|
||||
React.createElement('div', {
|
||||
key: 'slider',
|
||||
className: 'slider',
|
||||
ref: wrapRef
|
||||
},
|
||||
React.createElement('div', {
|
||||
className: 'track',
|
||||
ref: trackRef
|
||||
}, slides.map((slide, index) =>
|
||||
React.createElement('article', {
|
||||
key: index,
|
||||
className: 'project-card',
|
||||
...(index === current ? { active: '' } : {}),
|
||||
onMouseEnter: () => {
|
||||
if (window.matchMedia("(hover:hover)").matches) {
|
||||
activate(index, true);
|
||||
}
|
||||
},
|
||||
onClick: () => activate(index, true)
|
||||
}, [
|
||||
// Background
|
||||
React.createElement('div', {
|
||||
key: 'bg',
|
||||
className: 'project-card__bg',
|
||||
style: {
|
||||
background: slide.bgImage,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
}
|
||||
}),
|
||||
key: 'accordion',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
gap: isMobile ? '12px' : '14px',
|
||||
height: isMobile ? 'auto' : '440px'
|
||||
}
|
||||
}, panels)
|
||||
]);
|
||||
|
||||
// Content
|
||||
React.createElement('div', {
|
||||
key: 'content',
|
||||
className: 'project-card__content'
|
||||
}, [
|
||||
// Text container
|
||||
React.createElement('div', { key: 'text' }, [
|
||||
React.createElement('h3', {
|
||||
key: 'title',
|
||||
className: 'project-card__title'
|
||||
}, slide.title),
|
||||
React.createElement('p', {
|
||||
key: 'desc',
|
||||
className: 'project-card__desc'
|
||||
}, slide.description)
|
||||
])
|
||||
])
|
||||
])
|
||||
))
|
||||
),
|
||||
// Full-bleed dark band with the radial accent glow — matches the design mockup.
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
width: '100%', color: '#e8e8eb', fontFamily: SANS,
|
||||
padding: isMobile ? '44px 0' : '64px 0',
|
||||
background: 'radial-gradient(1100px 700px at 18% 8%, rgba(240,137,42,0.05), transparent 60%), #0f0f11'
|
||||
}
|
||||
}, [
|
||||
React.createElement('style', { key: 'kf', dangerouslySetInnerHTML: { __html: '@keyframes wuUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}' } }),
|
||||
inner
|
||||
]);
|
||||
};
|
||||
|
||||
// Export for use in your app
|
||||
window.UniqueFeatureSlider = UniqueFeatureSlider;
|
||||
window.UniqueFeatureSlider = UniqueFeatureSlider;
|
||||
|
||||
Reference in New Issue
Block a user