v6.8.0: light theme
The palette lived as ~620 hex literals in inline styles plus a few hundred more in
the stylesheets, so there was no single thing to change. It is now 113 custom
properties in src/styles/theme.css, in two blocks.
src/scripts/theme-boot.js decides the theme before first paint — blocking, in <head>,
above the stylesheet, because a deferred script paints dark first and corrects itself.
It stores the mode ('system' | 'light' | 'dark'), never the colour it resolved to, and
stamps data-theme so an explicit choice can beat the media query. The switcher in the
header is a view onto it.
A filled accent stays the brand colour in both themes — the ink on it is near-black
either way — while an accent used as text darkens to clear 4.5:1 on white. A colour
reaches a fill by four routes (a style property, a constant, a helper argument, an SVG
source string), and tests/theme-switching.test.mjs covers all four.
The dark theme is unchanged: every colour declaration the previous build produced comes
out of this one identically once the properties are resolved.
Also: the roadmap drops its status chips on mobile, and Roadmap.jsx no longer splits a
colour with parseInt at runtime, which a var() reference cannot survive.
This commit is contained in:
@@ -149,7 +149,7 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
|
||||
key: 'update-modal',
|
||||
style: {
|
||||
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)',
|
||||
padding: '24px', background: 'rgba(var(--sb-scrim-rgb), 0.55)', backdropFilter: 'blur(3px)', WebkitBackdropFilter: 'blur(3px)',
|
||||
animation: 'unFade .3s ease', fontFamily: SANS
|
||||
}
|
||||
}, [
|
||||
@@ -163,39 +163,39 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
|
||||
key: 'card',
|
||||
style: {
|
||||
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)'
|
||||
background: 'var(--sb-bg)', border: '1px solid rgba(var(--sb-ink), 0.08)', padding: '36px 32px 28px',
|
||||
textAlign: 'center', boxShadow: '0 30px 70px rgba(var(--sb-shadow-rgb), calc(0.6 * var(--sb-shadow-k)))', animation: 'unPop .32s cubic-bezier(.2,.7,.3,1)'
|
||||
}
|
||||
}, [
|
||||
// spinning update icon
|
||||
React.createElement('div', {
|
||||
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' }
|
||||
style: { display: 'inline-flex', width: '64px', height: '64px', borderRadius: '50%', alignItems: 'center', justifyContent: 'center', background: 'rgba(var(--sb-orange-rgb), 0.12)', border: '1px solid rgba(var(--sb-orange-rgb), 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',
|
||||
width: 28, height: 28, viewBox: '0 0 24 24', fill: 'none', stroke: 'var(--sb-orange-solid)', 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' } }, t('update.title')),
|
||||
React.createElement('p', { key: 'sub', style: { margin: '0 0 24px', fontSize: '14.5px', lineHeight: 1.55, color: '#9a9aa2' } }, t('update.desc')),
|
||||
React.createElement('h2', { key: 'title', style: { margin: '0 0 9px', fontSize: '26px', fontWeight: 800, letterSpacing: '-0.7px', color: 'var(--sb-text-1)' } }, t('update.title')),
|
||||
React.createElement('p', { key: 'sub', style: { margin: '0 0 24px', fontSize: '14.5px', lineHeight: 1.55, color: 'var(--sb-text-6)' } }, t('update.desc')),
|
||||
|
||||
// version comparison
|
||||
React.createElement('div', {
|
||||
key: 'vbox',
|
||||
style: { borderRadius: '14px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', padding: '16px 18px', marginBottom: '24px', textAlign: 'start' }
|
||||
style: { borderRadius: '14px', background: 'var(--sb-bg-deep)', border: '1px solid rgba(var(--sb-ink), 0.06)', padding: '16px 18px', marginBottom: '24px', textAlign: 'start' }
|
||||
}, [
|
||||
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' } }, t('update.currentVersion')),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 500, color: '#9a9aa2', whiteSpace: 'nowrap' } }, formatVersion(updateState.currentVersion))
|
||||
React.createElement('span', { key: 'l', style: { fontSize: '13.5px', fontWeight: 500, color: 'var(--sb-text-7)' } }, t('update.currentVersion')),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 500, color: 'var(--sb-text-6)', whiteSpace: 'nowrap' } }, formatVersion(updateState.currentVersion))
|
||||
]),
|
||||
React.createElement('div', { key: 'sep', style: { height: '1px', background: 'rgba(255,255,255,0.05)', margin: '4px 0' } }),
|
||||
React.createElement('div', { key: 'sep', style: { height: '1px', background: 'rgba(var(--sb-ink), 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' } }),
|
||||
React.createElement('span', { key: 'l', style: { display: 'inline-flex', alignItems: 'center', gap: '8px', fontSize: '13.5px', fontWeight: 600, color: 'var(--sb-text-2)' } }, [
|
||||
React.createElement('span', { key: 'd', style: { width: '6px', height: '6px', borderRadius: '50%', background: 'var(--sb-orange-solid)' } }),
|
||||
t('update.newVersion')
|
||||
]),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 700, color: '#f0892a', whiteSpace: 'nowrap' } }, formatVersion(updateState.newVersion))
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 700, color: 'var(--sb-orange)', whiteSpace: 'nowrap' } }, formatVersion(updateState.newVersion))
|
||||
])
|
||||
]),
|
||||
|
||||
@@ -204,17 +204,17 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
|
||||
? React.createElement('div', { key: 'progress' }, [
|
||||
React.createElement('div', {
|
||||
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}%`)
|
||||
style: { width: '100%', height: '8px', borderRadius: '99px', background: 'var(--sb-bg-deep)', border: '1px solid rgba(var(--sb-ink), 0.06)', overflow: 'hidden', marginBottom: '10px' }
|
||||
}, React.createElement('div', { key: 'fill', style: { height: '100%', width: `${updateState.progress}%`, background: 'linear-gradient(90deg,var(--sb-green),var(--sb-orange))', transition: 'width .3s ease' } })),
|
||||
React.createElement('p', { key: 't', style: { margin: 0, fontFamily: MONO, fontSize: '12px', color: 'var(--sb-text-7)' } }, `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'; }
|
||||
style: { flex: 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '10px', padding: '15px 20px', borderRadius: '13px', border: 'none', background: 'var(--sb-orange-solid)', color: 'var(--sb-on-accent)', fontFamily: 'inherit', fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px', cursor: 'pointer', boxShadow: '0 8px 24px rgba(var(--sb-orange-rgb), 0.28)', transition: 'all .2s cubic-bezier(.2,.7,.3,1)' },
|
||||
onMouseEnter: (e) => { e.currentTarget.style.background = 'var(--sb-orange-hi)'; e.currentTarget.style.transform = 'translateY(-2px)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.background = 'var(--sb-orange)'; 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"/>' } }),
|
||||
t('update.now')
|
||||
@@ -223,9 +223,9 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
|
||||
key: 'later',
|
||||
onClick: handleCloseModal,
|
||||
title: t('update.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)'; }
|
||||
style: { flex: 'none', width: '50px', height: '50px', borderRadius: '13px', display: 'grid', placeItems: 'center', border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'rgba(var(--sb-ink), 0.025)', color: 'var(--sb-text-6)', cursor: 'pointer', transition: 'all .18s cubic-bezier(.2,.7,.3,1)' },
|
||||
onMouseEnter: (e) => { e.currentTarget.style.color = 'var(--sb-red)'; e.currentTarget.style.borderColor = 'rgba(var(--sb-red-rgb), 0.4)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.color = 'var(--sb-text-6)'; e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 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"/>' } }))
|
||||
])
|
||||
])
|
||||
|
||||
@@ -18,7 +18,9 @@ const BecomePartner = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ACCENT = '#f0892a';
|
||||
const ACCENT = 'var(--sb-orange)';
|
||||
// Fill form: brand colour in both themes. See app.jsx.
|
||||
const ACCENT_SOLID = 'var(--sb-orange-solid)';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
@@ -60,7 +62,7 @@ const BecomePartner = () => {
|
||||
|
||||
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' }
|
||||
style: { fontFamily: MONO, fontSize: '10.5px', fontWeight: 600, color: 'var(--sb-text-9)', textTransform: 'uppercase', letterSpacing: '1.2px', padding: '6px 11px', borderRadius: '8px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'rgba(var(--sb-ink), 0.025)', whiteSpace: 'nowrap' }
|
||||
}, role);
|
||||
|
||||
const partnerCard = (p) => React.createElement('a', {
|
||||
@@ -70,14 +72,14 @@ const BecomePartner = () => {
|
||||
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)',
|
||||
borderRadius: '18px', background: 'var(--sb-surface)', border: '1px solid rgba(var(--sb-ink), 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)'; }
|
||||
onMouseEnter: (e) => { e.currentTarget.style.transform = 'translateY(-4px)'; e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.13)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.06)'; }
|
||||
}, [
|
||||
React.createElement('div', { key: 'logo', style: { display: 'flex', alignItems: 'center', marginBottom: '30px', height: '54px' } },
|
||||
React.createElement('img', {
|
||||
@@ -90,8 +92,8 @@ const BecomePartner = () => {
|
||||
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('h3', { key: 'name', style: { margin: '0 0 9px', fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: 'var(--sb-text-1)' } }, p.name),
|
||||
React.createElement('p', { key: 'desc', style: { margin: '0 0 22px', fontSize: '14.5px', lineHeight: 1.6, color: 'var(--sb-text-6)' } }, p.desc),
|
||||
React.createElement('div', { key: 'foot', style: { marginTop: 'auto', paddingTop: '6px', display: 'flex', alignItems: 'center', gap: '12px' } }, [
|
||||
roleTag(p.role)
|
||||
])
|
||||
@@ -104,30 +106,30 @@ const BecomePartner = () => {
|
||||
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)',
|
||||
borderRadius: '18px', background: 'var(--sb-bg)', border: '1px dashed rgba(var(--sb-ink), 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)'; }
|
||||
onMouseEnter: (e) => { e.currentTarget.style.borderColor = 'rgba(var(--sb-orange-rgb), 0.4)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 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' } }, t('partners.inviteTitle')),
|
||||
React.createElement('p', { key: 'desc', style: { margin: 0, fontSize: '14.5px', lineHeight: 1.6, color: '#8a8a92' } }, t('partners.inviteDesc'))
|
||||
style: { width: '48px', height: '48px', borderRadius: '13px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-orange-rgb), 0.12)', border: '1px solid rgba(var(--sb-orange-rgb), 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_SOLID, 1.9)),
|
||||
React.createElement('h3', { key: 'title', style: { margin: '0 0 8px', fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: 'var(--sb-text-1)' } }, t('partners.inviteTitle')),
|
||||
React.createElement('p', { key: 'desc', style: { margin: 0, fontSize: '14.5px', lineHeight: 1.6, color: 'var(--sb-text-7)' } }, t('partners.inviteDesc'))
|
||||
]),
|
||||
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',
|
||||
padding: '15px 20px', borderRadius: '12px', border: 'none', background: ACCENT_SOLID, color: 'var(--sb-on-accent)',
|
||||
fontFamily: SANS, fontSize: '15px', fontWeight: 700, cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(240,137,42,0.28)', boxSizing: 'border-box',
|
||||
boxShadow: '0 8px 24px rgba(var(--sb-orange-rgb), 0.28)', boxSizing: 'border-box',
|
||||
transition: 'background .2s cubic-bezier(.2,.7,.3,1), transform .2s cubic-bezier(.2,.7,.3,1)'
|
||||
}
|
||||
}, [
|
||||
@@ -144,13 +146,13 @@ const BecomePartner = () => {
|
||||
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' }
|
||||
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: 'var(--sb-text-9)', textTransform: 'uppercase', letterSpacing: '1.6px', marginBottom: '14px' }
|
||||
}, t('partners.eyebrow')),
|
||||
// The heading used to share a flex row with a strapline; with that gone the
|
||||
// row had one child and nothing left to arrange.
|
||||
React.createElement('h2', {
|
||||
key: 'h2',
|
||||
style: { margin: 0, fontSize: isMobile ? '30px' : '40px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.04, color: '#f4f4f6' }
|
||||
style: { margin: 0, fontSize: isMobile ? '30px' : '40px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.04, color: 'var(--sb-text-1)' }
|
||||
}, t('partners.heading'))
|
||||
]),
|
||||
|
||||
@@ -166,9 +168,9 @@ const BecomePartner = () => {
|
||||
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
width: '100%', color: '#e8e8eb', fontFamily: SANS,
|
||||
width: '100%', color: 'var(--sb-text-2)', 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'
|
||||
background: 'radial-gradient(1100px 640px at 50% -6%, rgba(var(--sb-orange-rgb), 0.055), transparent 62%), var(--sb-bg)'
|
||||
}
|
||||
}, [
|
||||
React.createElement('style', { key: 'kf', dangerouslySetInnerHTML: { __html: '@keyframes ptUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}' } }),
|
||||
|
||||
@@ -103,39 +103,39 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
|
||||
// ── shared styles (from the design) ──────────────────────────────────────
|
||||
const ctrlBase = {
|
||||
width: '56px', height: '56px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.05)',
|
||||
color: '#cfcfd4', cursor: 'pointer', transition: 'all .15s'
|
||||
border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'rgba(var(--sb-ink), 0.05)',
|
||||
color: 'var(--sb-text-4)', cursor: 'pointer', transition: 'all .15s'
|
||||
};
|
||||
const dangerCtrl = { ...ctrlBase, background: '#e5484d', color: '#fff', border: '1px solid transparent' };
|
||||
const dangerCtrl = { ...ctrlBase, background: 'var(--sb-red-strong-solid)', color: '#fff', border: '1px solid transparent' };
|
||||
const endBtn = {
|
||||
width: '56px', height: '56px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
border: 'none', background: '#e5484d', color: '#fff', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(229,72,77,0.35)', transition: 'transform .15s'
|
||||
border: 'none', background: 'var(--sb-red-strong-solid)', color: '#fff', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(var(--sb-red-strong-rgb), 0.35)', transition: 'transform .15s'
|
||||
};
|
||||
const minimizeBtn = (light) => ({
|
||||
width: '36px', height: '36px', borderRadius: '9px', display: 'grid', placeItems: 'center',
|
||||
border: '1px solid rgba(255,255,255,' + (light ? '0.15' : '0.1') + ')',
|
||||
background: light ? 'rgba(0,0,0,0.35)' : 'rgba(255,255,255,0.04)',
|
||||
color: light ? '#fff' : '#cfcfd4', cursor: 'pointer', transition: 'all .15s'
|
||||
border: '1px solid rgba(var(--sb-ink), ' + (light ? '0.15' : '0.1') + ')',
|
||||
background: light ? 'rgba(0,0,0,0.35)' : 'rgba(var(--sb-ink), 0.04)',
|
||||
color: light ? '#fff' : 'var(--sb-text-4)', cursor: 'pointer', transition: 'all .15s'
|
||||
});
|
||||
|
||||
const encBadge = h('span', { key: 'enc', style: { display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '11px', fontWeight: 600, color: '#3ecf8e' } },
|
||||
const encBadge = h('span', { key: 'enc', style: { display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '11px', fontWeight: 600, color: 'var(--sb-green)' } },
|
||||
[svg(ICON.lock, 11, 2), t('call.encryptedShort')]);
|
||||
|
||||
// Connection-quality indicator — driven by the adaptation controller's
|
||||
// getStats (loss + RTT). Signal bars + label; hidden until there's data.
|
||||
const QUALITY = {
|
||||
excellent: { bars: 4, color: '#3ecf8e', label: t('call.qualityExcellent') },
|
||||
good: { bars: 3, color: '#3ecf8e', label: t('call.qualityGood') },
|
||||
fair: { bars: 2, color: '#e3c84e', label: t('call.qualityFair') },
|
||||
poor: { bars: 1, color: '#e5727a', label: t('call.qualityWeak') },
|
||||
excellent: { bars: 4, color: 'var(--sb-green)', label: t('call.qualityExcellent') },
|
||||
good: { bars: 3, color: 'var(--sb-green)', label: t('call.qualityGood') },
|
||||
fair: { bars: 2, color: 'var(--sb-yellow)', label: t('call.qualityFair') },
|
||||
poor: { bars: 1, color: 'var(--sb-red)', label: t('call.qualityWeak') },
|
||||
};
|
||||
const qualityIndicator = (compact) => {
|
||||
const q = QUALITY[call.quality];
|
||||
if (!q) return null;
|
||||
const bars = h('span', { key: 'bars', style: { display: 'inline-flex', alignItems: 'flex-end', gap: '2px', height: '14px' } },
|
||||
[0, 1, 2, 3].map(i => h('span', {
|
||||
key: i, style: { width: '3px', height: (5 + i * 3) + 'px', borderRadius: '1px', background: i < q.bars ? q.color : 'rgba(255,255,255,0.18)' }
|
||||
key: i, style: { width: '3px', height: (5 + i * 3) + 'px', borderRadius: '1px', background: i < q.bars ? q.color : 'rgba(var(--sb-ink), 0.18)' }
|
||||
})));
|
||||
if (compact) return bars;
|
||||
return h('span', { key: 'q', title: t('call.quality'), style: { display: 'inline-flex', alignItems: 'center', gap: '6px', fontSize: '11.5px', fontWeight: 600, color: q.color } }, [bars, q.label]);
|
||||
@@ -153,73 +153,73 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
|
||||
const hiddenAudio = h('audio', { key: 'ra', ref: remoteAudioRef, autoPlay: true, playsInline: true, style: { display: 'none' } });
|
||||
|
||||
const labeled = (key, btn, label) => h('div', { key, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' } },
|
||||
[btn, h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: '#8a8a92' } }, label)]);
|
||||
[btn, h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: 'var(--sb-text-7)' } }, label)]);
|
||||
|
||||
// Avatar disc with optional ringing pulse (voice + incoming).
|
||||
const avatarDisc = (size, ring) => h('div', { key: 'av', style: { position: 'relative', width: '120px', height: '120px', marginBottom: '28px', display: 'grid', placeItems: 'center' } }, [
|
||||
ring && h('span', { key: 'p1', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(240,137,42,0.5)', animation: 'sbCallPulse 2s ease-out infinite' } }),
|
||||
ring && h('span', { key: 'p2', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(240,137,42,0.4)', animation: 'sbCallPulse 2s ease-out infinite', animationDelay: '1s' } }),
|
||||
h('div', { key: 'c', style: { width: '104px', height: '104px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'radial-gradient(circle at 35% 30%, #2a2a30, #161618)', border: '1px solid rgba(255,255,255,0.1)', boxShadow: '0 12px 30px rgba(0,0,0,0.4)', color: '#8a8a92' } }, svg(ICON.user, size, 1.6))
|
||||
ring && h('span', { key: 'p1', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(var(--sb-orange-rgb), 0.5)', animation: 'sbCallPulse 2s ease-out infinite' } }),
|
||||
ring && h('span', { key: 'p2', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(var(--sb-orange-rgb), 0.4)', animation: 'sbCallPulse 2s ease-out infinite', animationDelay: '1s' } }),
|
||||
h('div', { key: 'c', style: { width: '104px', height: '104px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'radial-gradient(circle at 35% 30%, var(--sb-surface-4), var(--sb-surface))', border: '1px solid rgba(var(--sb-ink), 0.1)', boxShadow: '0 12px 30px rgba(var(--sb-shadow-rgb), calc(0.4 * var(--sb-shadow-k)))', color: 'var(--sb-text-7)' } }, svg(ICON.user, size, 1.6))
|
||||
]);
|
||||
|
||||
// ── INCOMING (ringing) prompt ────────────────────────────────────────────
|
||||
if (phase === 'incoming') {
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column', background: 'radial-gradient(680px 460px at 50% 36%, rgba(240,137,42,0.08), transparent 70%), #0d0d0f', animation: 'sbExpand .2s ease' } }, [
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column', background: 'radial-gradient(680px 460px at 50% 36%, rgba(var(--sb-orange-rgb), 0.08), transparent 70%), var(--sb-bg-deep)', animation: 'sbExpand .2s ease' } }, [
|
||||
hiddenAudio,
|
||||
h('div', { key: 'top', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', padding: '16px 18px' } },
|
||||
h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: '#3ecf8e' } }, [svg(ICON.lock, 13, 2), t('call.encrypted')])),
|
||||
h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: 'var(--sb-green)' } }, [svg(ICON.lock, 13, 2), t('call.encrypted')])),
|
||||
h('div', { key: 'mid', style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' } }, [
|
||||
avatarDisc(46, true),
|
||||
h('div', { key: 'nm', style: { fontSize: '24px', fontWeight: 800, letterSpacing: '-0.5px', color: '#f4f4f6' } }, name),
|
||||
h('div', { key: 'st', style: { fontFamily: MONO, fontSize: '14px', fontWeight: 500, color: '#9a9aa2', marginTop: '8px' } }, call.withVideo ? t('call.incomingVideo') : t('call.incoming'))
|
||||
h('div', { key: 'nm', style: { fontSize: '24px', fontWeight: 800, letterSpacing: '-0.5px', color: 'var(--sb-text-1)' } }, name),
|
||||
h('div', { key: 'st', style: { fontFamily: MONO, fontSize: '14px', fontWeight: 500, color: 'var(--sb-text-6)', marginTop: '8px' } }, call.withVideo ? t('call.incomingVideo') : t('call.incoming'))
|
||||
]),
|
||||
h('div', { key: 'ctrls', style: { flex: 'none', display: 'flex', alignItems: 'flex-start', justifyContent: 'center', gap: '48px', padding: '28px 24px 40px' } }, [
|
||||
labeled('dec', h('button', { onClick: doDecline, title: t('call.decline'), style: { ...endBtn, width: '62px', height: '62px' } }, svg(ICON.phoneHangup, 24, 1.9)), t('call.decline')),
|
||||
labeled('acc', h('button', { onClick: doAccept, title: t('call.accept'), style: { width: '62px', height: '62px', borderRadius: '50%', display: 'grid', placeItems: 'center', border: 'none', background: '#3ecf8e', color: '#06231a', cursor: 'pointer', boxShadow: '0 8px 24px rgba(62,207,142,0.35)' } }, svg(ICON.phone, 24, 1.9)), t('call.accept'))
|
||||
labeled('acc', h('button', { onClick: doAccept, title: t('call.accept'), style: { width: '62px', height: '62px', borderRadius: '50%', display: 'grid', placeItems: 'center', border: 'none', background: 'var(--sb-green-solid)', color: 'var(--sb-on-green)', cursor: 'pointer', boxShadow: '0 8px 24px rgba(var(--sb-green-rgb), 0.35)' } }, svg(ICON.phone, 24, 1.9)), t('call.accept'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// ── MINIMIZED widget ─────────────────────────────────────────────────────
|
||||
if (minimized) {
|
||||
return h('div', { style: { position: 'absolute', bottom: '18px', insetInlineEnd: '18px', zIndex: 40, width: '236px', borderRadius: '14px', overflow: 'hidden', background: '#161618', border: '1px solid rgba(255,255,255,0.1)', boxShadow: '0 18px 44px rgba(0,0,0,0.55)', animation: 'sbExpand .18s ease' } }, [
|
||||
return h('div', { style: { position: 'absolute', bottom: '18px', insetInlineEnd: '18px', zIndex: 40, width: '236px', borderRadius: '14px', overflow: 'hidden', background: 'var(--sb-surface)', border: '1px solid rgba(var(--sb-ink), 0.1)', boxShadow: '0 18px 44px rgba(var(--sb-shadow-rgb), calc(0.55 * var(--sb-shadow-k)))', animation: 'sbExpand .18s ease' } }, [
|
||||
hiddenAudio,
|
||||
isVideo && h('div', { key: 'v', style: { position: 'relative', height: '132px', background: '#111' } }, [
|
||||
h('video', { key: 'rv', ref: remoteVideoRef, autoPlay: true, muted: true, playsInline: true, style: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' } }),
|
||||
!call.remoteHasVideo && h('div', { key: 'off', style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', background: 'linear-gradient(120deg,#15151b,#1d1a24)', color: '#6b6b73' } }, svg(ICON.camOff, 22, 1.8)),
|
||||
!call.remoteHasVideo && h('div', { key: 'off', style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', background: 'linear-gradient(120deg,var(--sb-grad-1),var(--sb-grad-2))', color: 'var(--sb-text-9)' } }, svg(ICON.camOff, 22, 1.8)),
|
||||
h('span', { key: 's', style: { position: 'absolute', top: '8px', insetInlineStart: '9px', fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#fff', padding: '3px 7px', borderRadius: '6px', background: 'rgba(0,0,0,0.5)' } }, callStatus)
|
||||
]),
|
||||
h('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: '11px', padding: '11px 12px' } }, [
|
||||
h('span', { key: 'ic', style: { position: 'relative', flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: 'rgba(62,207,142,0.1)', border: '1px solid rgba(62,207,142,0.25)', color: '#3ecf8e' } }, svg(ICON.user, 16, 1.9)),
|
||||
h('span', { key: 'ic', style: { position: 'relative', flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-green-rgb), 0.1)', border: '1px solid rgba(var(--sb-green-rgb), 0.25)', color: 'var(--sb-green)' } }, svg(ICON.user, 16, 1.9)),
|
||||
h('div', { key: 'tx', style: { flex: 1, minWidth: 0 } }, [
|
||||
h('div', { key: 'n', style: { fontSize: '13px', fontWeight: 700, color: '#f4f4f6', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, name),
|
||||
h('div', { key: 's', style: { display: 'flex', alignItems: 'center', gap: '7px', fontFamily: MONO, fontSize: '11px', color: '#9a9aa2' } }, [
|
||||
h('div', { key: 'n', style: { fontSize: '13px', fontWeight: 700, color: 'var(--sb-text-1)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, name),
|
||||
h('div', { key: 's', style: { display: 'flex', alignItems: 'center', gap: '7px', fontFamily: MONO, fontSize: '11px', color: 'var(--sb-text-6)' } }, [
|
||||
(isVideo ? t('call.videoPrefix') : t('call.voicePrefix')) + callStatus,
|
||||
phase === 'active' && qualityIndicator(true)
|
||||
])
|
||||
]),
|
||||
h('button', { key: 'exp', onClick: () => setMinimized(false), title: t('call.expand'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'rgba(255,255,255,0.05)', color: '#cfcfd4', cursor: 'pointer', transition: 'all .15s' } }, svg(ICON.expand, 15, 2)),
|
||||
h('button', { key: 'end', onClick: doEnd, title: t('call.end'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: '#e5484d', color: '#fff', cursor: 'pointer', transition: 'transform .15s' } }, svg(ICON.phoneHangup, 15, 2))
|
||||
h('button', { key: 'exp', onClick: () => setMinimized(false), title: t('call.expand'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'rgba(var(--sb-ink), 0.05)', color: 'var(--sb-text-4)', cursor: 'pointer', transition: 'all .15s' } }, svg(ICON.expand, 15, 2)),
|
||||
h('button', { key: 'end', onClick: doEnd, title: t('call.end'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'var(--sb-red-strong-solid)', color: '#fff', cursor: 'pointer', transition: 'transform .15s' } }, svg(ICON.phoneHangup, 15, 2))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// ── VIDEO call (expanded) ────────────────────────────────────────────────
|
||||
if (isVideo) {
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, overflow: 'hidden', background: '#0a0a0c', animation: 'sbExpand .2s ease' } }, [
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, overflow: 'hidden', background: 'var(--sb-bg-deepest)', animation: 'sbExpand .2s ease' } }, [
|
||||
hiddenAudio,
|
||||
call.remoteHasVideo
|
||||
? h('video', { key: 'rv', ref: remoteVideoRef, autoPlay: true, muted: true, playsInline: true, style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', background: '#0a0a0c' } })
|
||||
: h('div', { key: 'ph', style: { position: 'absolute', inset: 0, background: 'linear-gradient(120deg, #15151b, #1d1a24, #161620)', backgroundSize: '200% 200%', animation: 'sbLiveBg 9s ease-in-out infinite', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '18px' } }, [
|
||||
h('div', { key: 'a', style: { width: '120px', height: '120px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'radial-gradient(circle at 35% 30%, #2a2a30, #161618)', border: '1px solid rgba(255,255,255,0.1)', color: '#9a9aa2' } }, svg(ICON.user, 54, 1.5)),
|
||||
h('div', { key: 't', style: { fontSize: '15px', fontWeight: 600, color: '#8a8a92' } }, t('call.peerCameraOff'))
|
||||
? h('video', { key: 'rv', ref: remoteVideoRef, autoPlay: true, muted: true, playsInline: true, style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', background: 'var(--sb-bg-deepest)' } })
|
||||
: h('div', { key: 'ph', style: { position: 'absolute', inset: 0, background: 'linear-gradient(120deg, var(--sb-grad-1), var(--sb-grad-2), var(--sb-grad-3))', backgroundSize: '200% 200%', animation: 'sbLiveBg 9s ease-in-out infinite', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '18px' } }, [
|
||||
h('div', { key: 'a', style: { width: '120px', height: '120px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'radial-gradient(circle at 35% 30%, var(--sb-surface-4), var(--sb-surface))', border: '1px solid rgba(var(--sb-ink), 0.1)', color: 'var(--sb-text-6)' } }, svg(ICON.user, 54, 1.5)),
|
||||
h('div', { key: 't', style: { fontSize: '15px', fontWeight: 600, color: 'var(--sb-text-7)' } }, t('call.peerCameraOff'))
|
||||
]),
|
||||
// Top bar
|
||||
h('div', { key: 'top', style: { position: 'absolute', top: 0, left: 0, right: 0, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '14px', padding: '18px 20px', background: 'linear-gradient(180deg, rgba(0,0,0,0.55), transparent)' } }, [
|
||||
h('div', { key: 'l' }, [
|
||||
h('div', { key: 'n', style: { fontSize: '18px', fontWeight: 800, letterSpacing: '-0.3px', color: '#fff' } }, name),
|
||||
h('div', { key: 's', style: { display: 'inline-flex', alignItems: 'center', gap: '9px', marginTop: '4px' } }, [
|
||||
h('span', { key: 'st', style: { fontFamily: MONO, fontSize: '12.5px', fontWeight: 500, color: '#e8e8eb' } }, callStatus),
|
||||
h('span', { key: 'st', style: { fontFamily: MONO, fontSize: '12.5px', fontWeight: 500, color: 'var(--sb-text-2)' } }, callStatus),
|
||||
encBadge,
|
||||
phase === 'active' && qualityIndicator(false)
|
||||
])
|
||||
@@ -227,11 +227,11 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
|
||||
h('button', { key: 'min', onClick: () => setMinimized(true), title: t('call.minimize'), style: { flex: 'none', ...minimizeBtn(true) } }, svg(ICON.minimize, 16, 2))
|
||||
]),
|
||||
// Self-cam PiP
|
||||
h('div', { key: 'self', style: { position: 'absolute', bottom: '108px', insetInlineEnd: '18px', width: '132px', height: '176px', borderRadius: '14px', overflow: 'hidden', border: '1px solid rgba(255,255,255,0.16)', boxShadow: '0 12px 30px rgba(0,0,0,0.5)', background: '#111' } }, [
|
||||
h('div', { key: 'self', style: { position: 'absolute', bottom: '108px', insetInlineEnd: '18px', width: '132px', height: '176px', borderRadius: '14px', overflow: 'hidden', border: '1px solid rgba(var(--sb-ink), 0.16)', boxShadow: '0 12px 30px rgba(var(--sb-shadow-rgb), calc(0.5 * var(--sb-shadow-k)))', background: '#111' } }, [
|
||||
h('video', { key: 'sv', ref: selfVideoRef, autoPlay: true, muted: true, playsInline: true, style: { width: '100%', height: '100%', objectFit: 'cover', transform: 'scaleX(-1)', display: 'block' } }),
|
||||
!call.cameraEnabled && h('div', { key: 'off', style: { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', background: '#161618', color: '#6b6b73' } }, [
|
||||
!call.cameraEnabled && h('div', { key: 'off', style: { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', background: 'var(--sb-surface)', color: 'var(--sb-text-9)' } }, [
|
||||
svg(ICON.camOff, 24, 1.8),
|
||||
h('span', { key: 't', style: { fontSize: '10.5px', color: '#6b6b73', fontFamily: MONO } }, t('call.cameraOff'))
|
||||
h('span', { key: 't', style: { fontSize: '10.5px', color: 'var(--sb-text-9)', fontFamily: MONO } }, t('call.cameraOff'))
|
||||
])
|
||||
]),
|
||||
// Control bar
|
||||
@@ -245,16 +245,16 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
|
||||
}
|
||||
|
||||
// ── VOICE call (expanded) ────────────────────────────────────────────────
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column', background: 'radial-gradient(680px 460px at 50% 36%, rgba(240,137,42,0.08), transparent 70%), #0d0d0f', animation: 'sbExpand .2s ease' } }, [
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column', background: 'radial-gradient(680px 460px at 50% 36%, rgba(var(--sb-orange-rgb), 0.08), transparent 70%), var(--sb-bg-deep)', animation: 'sbExpand .2s ease' } }, [
|
||||
hiddenAudio,
|
||||
h('div', { key: 'top', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 18px' } }, [
|
||||
h('span', { key: 'enc', style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: '#3ecf8e' } }, [svg(ICON.lock, 13, 2), t('call.encrypted')]),
|
||||
h('span', { key: 'enc', style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: 'var(--sb-green)' } }, [svg(ICON.lock, 13, 2), t('call.encrypted')]),
|
||||
h('button', { key: 'min', onClick: () => setMinimized(true), title: t('call.minimize'), style: minimizeBtn(false) }, svg(ICON.minimize, 16, 2))
|
||||
]),
|
||||
h('div', { key: 'mid', style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' } }, [
|
||||
avatarDisc(46, ringing),
|
||||
h('div', { key: 'nm', style: { fontSize: '24px', fontWeight: 800, letterSpacing: '-0.5px', color: '#f4f4f6' } }, name),
|
||||
h('div', { key: 'st', style: { fontFamily: MONO, fontSize: '14px', fontWeight: 500, color: '#9a9aa2', marginTop: '8px' } }, callStatus),
|
||||
h('div', { key: 'nm', style: { fontSize: '24px', fontWeight: 800, letterSpacing: '-0.5px', color: 'var(--sb-text-1)' } }, name),
|
||||
h('div', { key: 'st', style: { fontFamily: MONO, fontSize: '14px', fontWeight: 500, color: 'var(--sb-text-6)', marginTop: '8px' } }, callStatus),
|
||||
phase === 'active' && h('div', { key: 'q', style: { marginTop: '12px' } }, qualityIndicator(false))
|
||||
]),
|
||||
h('div', { key: 'ctrls', style: { flex: 'none', display: 'flex', alignItems: 'flex-start', justifyContent: 'center', gap: '26px', padding: '28px 24px 34px' } }, [
|
||||
|
||||
@@ -17,7 +17,9 @@ const CommunityCTA = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ACCENT = '#f0892a';
|
||||
const ACCENT = 'var(--sb-orange)';
|
||||
// Fill form: brand colour in both themes. See app.jsx.
|
||||
const ACCENT_SOLID = 'var(--sb-orange-solid)';
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
const githubUrl = 'https://github.com/SecureBitChat/securebit-chat/';
|
||||
@@ -30,13 +32,13 @@ const CommunityCTA = () => {
|
||||
rel: 'noopener noreferrer',
|
||||
style: {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '11px', padding: '15px 26px',
|
||||
borderRadius: '13px', background: ACCENT, color: '#1a0f04', textDecoration: 'none',
|
||||
borderRadius: '13px', background: ACCENT_SOLID, color: 'var(--sb-on-accent)', textDecoration: 'none',
|
||||
fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px',
|
||||
boxShadow: '0 8px 24px rgba(240,137,42,0.28)', whiteSpace: 'nowrap',
|
||||
boxShadow: '0 8px 24px rgba(var(--sb-orange-rgb), 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'; }
|
||||
onMouseEnter: (e) => { e.currentTarget.style.background = 'var(--sb-orange-hi)'; e.currentTarget.style.transform = 'translateY(-2px)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.background = ACCENT_SOLID; e.currentTarget.style.transform = 'none'; }
|
||||
}, [
|
||||
React.createElement('svg', {
|
||||
key: 'i', width: 20, height: 20, viewBox: '0 0 24 24', fill: 'currentColor',
|
||||
@@ -51,13 +53,13 @@ const CommunityCTA = () => {
|
||||
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',
|
||||
borderRadius: '13px', background: 'rgba(var(--sb-ink), 0.03)', color: 'var(--sb-text-2)', textDecoration: 'none',
|
||||
fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px',
|
||||
border: '1px solid rgba(255,255,255,0.1)', whiteSpace: 'nowrap',
|
||||
border: '1px solid rgba(var(--sb-ink), 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)'; }
|
||||
onMouseEnter: (e) => { e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.24)'; e.currentTarget.style.background = 'rgba(var(--sb-ink), 0.06)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.1)'; e.currentTarget.style.background = 'rgba(var(--sb-ink), 0.03)'; }
|
||||
}, [
|
||||
React.createElement('svg', {
|
||||
key: 'i', width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
||||
@@ -72,16 +74,16 @@ const CommunityCTA = () => {
|
||||
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)',
|
||||
background: 'radial-gradient(700px 360px at 50% 0%, rgba(var(--sb-orange-rgb), 0.1), transparent 65%), var(--sb-bg)',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.07)',
|
||||
padding: isMobile ? '40px 24px 36px' : '56px 56px 48px',
|
||||
textAlign: 'center', boxShadow: '0 24px 60px rgba(0,0,0,0.4)'
|
||||
textAlign: 'center', boxShadow: '0 24px 60px rgba(var(--sb-shadow-rgb), calc(0.4 * var(--sb-shadow-k)))'
|
||||
}
|
||||
}, [
|
||||
// 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)' }
|
||||
style: { position: 'absolute', top: 0, left: '50%', transform: 'translateX(-50%)', width: '180px', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(var(--sb-orange-rgb), 0.7), transparent)' }
|
||||
}),
|
||||
// brand mark (same SVG as the header — no border or background)
|
||||
React.createElement('img', {
|
||||
@@ -93,12 +95,12 @@ const CommunityCTA = () => {
|
||||
// title
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
style: { margin: '0 0 16px', fontSize: isMobile ? '28px' : '36px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.05, color: '#f4f4f6' }
|
||||
style: { margin: '0 0 16px', fontSize: isMobile ? '28px' : '36px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.05, color: 'var(--sb-text-1)' }
|
||||
}, t('community.title')),
|
||||
// description
|
||||
React.createElement('p', {
|
||||
key: 'desc',
|
||||
style: { margin: '0 auto 32px', maxWidth: '560px', fontSize: '16px', lineHeight: 1.65, color: '#9a9aa2' }
|
||||
style: { margin: '0 auto 32px', maxWidth: '560px', fontSize: '16px', lineHeight: 1.65, color: 'var(--sb-text-6)' }
|
||||
}, t('community.description')),
|
||||
// buttons
|
||||
React.createElement('div', {
|
||||
@@ -140,7 +142,7 @@ const CommunityCTA = () => {
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: '#0f0f11', fontFamily: SANS,
|
||||
background: 'var(--sb-bg)', fontFamily: SANS,
|
||||
padding: isMobile ? '48px 18px' : '64px 48px'
|
||||
}
|
||||
}, [
|
||||
|
||||
@@ -158,15 +158,15 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
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)'),
|
||||
background: i < filled ? color : 'rgba(var(--sb-ink), 0.07)',
|
||||
border: '1px solid ' + (i < filled ? 'transparent' : 'rgba(var(--sb-ink), 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' }
|
||||
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: '11.5px', color: 'var(--sb-text-7)' }
|
||||
}, [
|
||||
React.createElement('span', { key: 'status', style: { display: 'inline-flex', alignItems: 'center', gap: '5px' } }, [
|
||||
React.createElement('i', { key: 'icon', className: getStatusIcon(transfer.status) }),
|
||||
@@ -174,7 +174,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'count',
|
||||
style: { fontFamily: "'JetBrains Mono', ui-monospace, monospace", color: i_done(transfer) ? color : '#8a8a92' }
|
||||
style: { fontFamily: "'JetBrains Mono', ui-monospace, monospace", color: i_done(transfer) ? color : 'var(--sb-text-7)' }
|
||||
}, total > 0 ? `${Math.min(done, total)} / ${total} chunks` : `${(transfer.progress || 0).toFixed(0)}%`)
|
||||
])
|
||||
]);
|
||||
@@ -221,9 +221,9 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
onDragLeave: handleDragLeave,
|
||||
style: {
|
||||
position: 'relative',
|
||||
border: '1.5px dashed ' + (dragOver ? 'rgba(240,137,42,0.7)' : 'rgba(255,255,255,0.14)'),
|
||||
border: '1.5px dashed ' + (dragOver ? 'rgba(var(--sb-orange-rgb), 0.7)' : 'rgba(var(--sb-ink), 0.14)'),
|
||||
borderRadius: '14px',
|
||||
background: dragOver ? 'rgba(240,137,42,0.07)' : '#141416',
|
||||
background: dragOver ? 'rgba(var(--sb-orange-rgb), 0.07)' : 'var(--sb-surface)',
|
||||
padding: '24px 22px',
|
||||
textAlign: 'center',
|
||||
transition: 'all .15s'
|
||||
@@ -231,16 +231,16 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
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' } }, t('file.drop')),
|
||||
React.createElement('div', { key: 'sub', style: { fontSize: '12px', color: '#7b7b83', marginTop: '4px' } }, t('file.dropHint')),
|
||||
style: { width: '42px', height: '42px', margin: '0 auto 10px', borderRadius: '12px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-ink), 0.04)', border: '1px solid rgba(var(--sb-ink), 0.08)' }
|
||||
}, React.createElement('i', { className: 'fas fa-arrow-up-from-bracket', style: { color: 'var(--sb-text-6)', fontSize: '18px' } })),
|
||||
React.createElement('div', { key: 'title', style: { fontSize: '14px', fontWeight: 700, color: 'var(--sb-text-2)' } }, t('file.drop')),
|
||||
React.createElement('div', { key: 'sub', style: { fontSize: '12px', color: 'var(--sb-text-8)', marginTop: '4px' } }, t('file.dropHint')),
|
||||
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' }
|
||||
style: { marginTop: '14px', display: 'inline-flex', alignItems: 'center', gap: '7px', padding: '9px 16px', borderRadius: '9px', border: 'none', background: 'var(--sb-orange-solid)', color: 'var(--sb-on-accent)', fontFamily: 'inherit', fontSize: '13px', fontWeight: 700, cursor: 'pointer' }
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-folder-open', style: { fontSize: '13px' } }),
|
||||
t('file.browse')
|
||||
@@ -262,23 +262,23 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
className: "mt-4 space-y-2"
|
||||
}, pendingIncomingFiles.map(file => React.createElement('div', {
|
||||
key: file.fileId,
|
||||
style: { borderRadius: '12px', border: '1px solid rgba(255,255,255,0.08)', background: '#161618', padding: '12px 14px' }
|
||||
style: { borderRadius: '12px', border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-surface)', padding: '12px 14px' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'info',
|
||||
style: { marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '11px' }
|
||||
}, [
|
||||
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: 'ic', style: { flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-orange-rgb), 0.12)', border: '1px solid rgba(var(--sb-orange-rgb), 0.22)' } },
|
||||
React.createElement('i', { className: 'fas fa-file-arrow-down', style: { color: 'var(--sb-orange)', fontSize: '15px' } })
|
||||
),
|
||||
React.createElement('div', { key: 'text', style: { minWidth: 0 } }, [
|
||||
React.createElement('div', {
|
||||
key: 'title',
|
||||
style: { fontSize: '13px', fontWeight: 600, color: '#e8e8eb' }
|
||||
style: { fontSize: '13px', fontWeight: 600, color: 'var(--sb-text-2)' }
|
||||
}, t('file.incoming')),
|
||||
React.createElement('div', {
|
||||
key: 'meta',
|
||||
style: { fontSize: '11.5px', color: '#7b7b83', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }
|
||||
style: { fontSize: '11.5px', color: 'var(--sb-text-8)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }
|
||||
}, `${file.fileName} · ${formatFileSize(file.fileSize)} · ${file.mimeType}`)
|
||||
])
|
||||
]),
|
||||
@@ -289,12 +289,12 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
React.createElement('button', {
|
||||
key: 'accept',
|
||||
onClick: () => handleIncomingDecision(file.fileId, true),
|
||||
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' }
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: '8px', border: 'none', background: 'var(--sb-orange-solid)', color: 'var(--sb-on-accent)', padding: '8px 14px', fontSize: '13px', fontWeight: 700, cursor: 'pointer' }
|
||||
}, [React.createElement('i', { key: 'i', className: 'fas fa-check', style: { fontSize: '12px' } }), t('file.accept')]),
|
||||
React.createElement('button', {
|
||||
key: 'reject',
|
||||
onClick: () => handleIncomingDecision(file.fileId, false),
|
||||
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' }
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: '8px', border: '1px solid rgba(var(--sb-red-rgb), 0.3)', background: 'rgba(var(--sb-red-rgb), 0.08)', color: 'var(--sb-red)', padding: '8px 14px', fontSize: '13px', fontWeight: 600, cursor: 'pointer' }
|
||||
}, [React.createElement('i', { key: 'i', className: 'fas fa-xmark', style: { fontSize: '12px' } }), t('file.reject')])
|
||||
])
|
||||
]))),
|
||||
@@ -306,7 +306,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'title',
|
||||
style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12.5px', fontWeight: 600, color: '#8a8a92', marginBottom: '10px' }
|
||||
style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12.5px', fontWeight: 600, color: 'var(--sb-text-7)', marginBottom: '10px' }
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
@@ -320,7 +320,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
...transfers.sending.map(transfer =>
|
||||
React.createElement('div', {
|
||||
key: `send-${transfer.fileId}`,
|
||||
style: { borderRadius: '11px', border: '1px solid rgba(255,255,255,0.07)', background: '#161618', padding: '12px', marginBottom: '8px' }
|
||||
style: { borderRadius: '11px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'var(--sb-surface)', padding: '12px', marginBottom: '8px' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
@@ -333,17 +333,17 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-arrow-up',
|
||||
style: { color: '#f0892a', fontSize: '13px', marginInlineEnd: '8px' }
|
||||
style: { color: 'var(--sb-orange)', fontSize: '13px', marginInlineEnd: '8px' }
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'name',
|
||||
className: "font-medium text-sm",
|
||||
style: { color: '#e8e8eb' }
|
||||
style: { color: 'var(--sb-text-2)' }
|
||||
}, transfer.fileName),
|
||||
React.createElement('span', {
|
||||
key: 'size',
|
||||
className: "text-xs ms-2",
|
||||
style: { color: '#7b7b83' }
|
||||
style: { color: 'var(--sb-text-8)' }
|
||||
}, formatFileSize(transfer.fileSize))
|
||||
]),
|
||||
React.createElement('button', {
|
||||
@@ -356,7 +356,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
})
|
||||
])
|
||||
]),
|
||||
renderProgress(transfer, '#f0892a')
|
||||
renderProgress(transfer, 'var(--sb-orange)')
|
||||
])
|
||||
),
|
||||
|
||||
@@ -364,7 +364,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
...transfers.receiving.map(transfer =>
|
||||
React.createElement('div', {
|
||||
key: `recv-${transfer.fileId}`,
|
||||
style: { borderRadius: '11px', border: '1px solid rgba(255,255,255,0.07)', background: '#161618', padding: '12px', marginBottom: '8px' }
|
||||
style: { borderRadius: '11px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'var(--sb-surface)', padding: '12px', marginBottom: '8px' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
@@ -377,17 +377,17 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-arrow-down',
|
||||
style: { color: '#3ecf8e', fontSize: '13px', marginInlineEnd: '8px' }
|
||||
style: { color: 'var(--sb-green)', fontSize: '13px', marginInlineEnd: '8px' }
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'name',
|
||||
className: "font-medium text-sm",
|
||||
style: { color: '#e8e8eb' }
|
||||
style: { color: 'var(--sb-text-2)' }
|
||||
}, transfer.fileName),
|
||||
React.createElement('span', {
|
||||
key: 'size',
|
||||
className: "text-xs ms-2",
|
||||
style: { color: '#7b7b83' }
|
||||
style: { color: 'var(--sb-text-8)' }
|
||||
}, formatFileSize(transfer.fileSize))
|
||||
]),
|
||||
React.createElement('div', { key: 'actions', className: 'flex items-center space-x-2' }, [
|
||||
@@ -422,7 +422,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
])
|
||||
])
|
||||
]),
|
||||
renderProgress(transfer, '#3ecf8e')
|
||||
renderProgress(transfer, 'var(--sb-green)')
|
||||
])
|
||||
)
|
||||
])
|
||||
|
||||
@@ -40,26 +40,26 @@ const svg = (inner, size, sw) => h('span', {
|
||||
|
||||
const ctrlBase = {
|
||||
width: '56px', height: '56px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.05)',
|
||||
color: '#cfcfd4', cursor: 'pointer', transition: 'all .15s',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'rgba(var(--sb-ink), 0.05)',
|
||||
color: 'var(--sb-text-4)', cursor: 'pointer', transition: 'all .15s',
|
||||
};
|
||||
const dangerCtrl = { ...ctrlBase, background: '#e5484d', color: '#fff', border: '1px solid transparent' };
|
||||
const dangerCtrl = { ...ctrlBase, background: 'var(--sb-red-strong-solid)', color: '#fff', border: '1px solid transparent' };
|
||||
const endBtn = {
|
||||
width: '56px', height: '56px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
border: 'none', background: '#e5484d', color: '#fff', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(229,72,77,0.35)', transition: 'transform .15s',
|
||||
border: 'none', background: 'var(--sb-red-strong-solid)', color: '#fff', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(var(--sb-red-strong-rgb), 0.35)', transition: 'transform .15s',
|
||||
};
|
||||
const minimizeBtn = {
|
||||
width: '36px', height: '36px', borderRadius: '9px', display: 'grid', placeItems: 'center',
|
||||
border: '1px solid rgba(255,255,255,0.15)', background: 'rgba(0,0,0,0.35)',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.15)', background: 'rgba(0,0,0,0.35)',
|
||||
color: '#fff', cursor: 'pointer', transition: 'all .15s',
|
||||
};
|
||||
|
||||
const QUALITY = {
|
||||
excellent: { bars: 4, color: '#3ecf8e' },
|
||||
good: { bars: 3, color: '#3ecf8e' },
|
||||
fair: { bars: 2, color: '#e3c84e' },
|
||||
poor: { bars: 1, color: '#e5727a' },
|
||||
excellent: { bars: 4, color: 'var(--sb-green)' },
|
||||
good: { bars: 3, color: 'var(--sb-green)' },
|
||||
fair: { bars: 2, color: 'var(--sb-yellow)' },
|
||||
poor: { bars: 1, color: 'var(--sb-red)' },
|
||||
};
|
||||
|
||||
function qualityBars(quality) {
|
||||
@@ -70,7 +70,7 @@ function qualityBars(quality) {
|
||||
key: i,
|
||||
style: {
|
||||
width: '2.5px', height: (4 + i * 2.6) + 'px', borderRadius: '1px',
|
||||
background: i < q.bars ? q.color : 'rgba(255,255,255,0.18)',
|
||||
background: i < q.bars ? q.color : 'rgba(var(--sb-ink), 0.18)',
|
||||
},
|
||||
})));
|
||||
}
|
||||
@@ -180,9 +180,9 @@ function Tile({ peer, stream, self, localStream, cameraEnabled, speaking, tileW,
|
||||
: (speaking ? t('groupCall.speaking', { name: peer.name }) : peer.name),
|
||||
style: {
|
||||
cursor: onSelect ? 'pointer' : 'default',
|
||||
position: 'relative', borderRadius: '14px', overflow: 'hidden', background: '#141417',
|
||||
border: speaking ? '2px solid #3ecf8e' : '2px solid rgba(255,255,255,0.08)',
|
||||
boxShadow: speaking ? '0 0 0 3px rgba(62,207,142,0.16)' : 'none',
|
||||
position: 'relative', borderRadius: '14px', overflow: 'hidden', background: 'var(--sb-surface)',
|
||||
border: speaking ? '2px solid var(--sb-green-solid)' : '2px solid rgba(var(--sb-ink), 0.08)',
|
||||
boxShadow: speaking ? '0 0 0 3px rgba(var(--sb-green-rgb), 0.16)' : 'none',
|
||||
transition: 'border-color .12s ease, box-shadow .12s ease',
|
||||
// Sized by the grid, which is the only thing that knows how much
|
||||
// room there is. aspectRatio stays as the fallback for the first
|
||||
@@ -205,7 +205,7 @@ function Tile({ peer, stream, self, localStream, cameraEnabled, speaking, tileW,
|
||||
key: 'v', ref: videoRef, autoPlay: true, muted: true, playsInline: true,
|
||||
style: {
|
||||
position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover',
|
||||
transform: self ? 'scaleX(-1)' : 'none', background: '#0f0f11',
|
||||
transform: self ? 'scaleX(-1)' : 'none', background: 'var(--sb-bg)',
|
||||
},
|
||||
})
|
||||
: h('div', {
|
||||
@@ -215,9 +215,9 @@ function Tile({ peer, stream, self, localStream, cameraEnabled, speaking, tileW,
|
||||
// large tile and crowds a small one.
|
||||
width: avatarPx + 'px', height: avatarPx + 'px',
|
||||
borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
background: 'radial-gradient(circle at 35% 30%, #2a2a30, #161618)',
|
||||
border: speaking ? '1.5px solid rgba(62,207,142,0.65)' : '1px solid rgba(255,255,255,0.1)',
|
||||
color: speaking ? '#3ecf8e' : '#cfcfd4',
|
||||
background: 'radial-gradient(circle at 35% 30%, var(--sb-surface-4), var(--sb-surface))',
|
||||
border: speaking ? '1.5px solid rgba(var(--sb-green-rgb), 0.65)' : '1px solid rgba(var(--sb-ink), 0.1)',
|
||||
color: speaking ? 'var(--sb-green)' : 'var(--sb-text-4)',
|
||||
fontFamily: MONO, fontSize: Math.round(avatarPx * 0.3) + 'px', fontWeight: 700,
|
||||
transition: 'color .12s ease, border-color .12s ease',
|
||||
},
|
||||
@@ -233,18 +233,18 @@ function Tile({ peer, stream, self, localStream, cameraEnabled, speaking, tileW,
|
||||
h('span', {
|
||||
key: 'n',
|
||||
style: {
|
||||
flex: 1, minWidth: 0, fontSize: compact ? '10.5px' : '12.5px', fontWeight: 700, color: '#f4f4f6',
|
||||
flex: 1, minWidth: 0, fontSize: compact ? '10.5px' : '12.5px', fontWeight: 700, color: 'var(--sb-text-1)',
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
},
|
||||
}, peer.name),
|
||||
speaking && h('span', {
|
||||
key: 'sp', style: { flex: 'none', color: '#3ecf8e', display: 'grid', placeItems: 'center' },
|
||||
key: 'sp', style: { flex: 'none', color: 'var(--sb-green)', display: 'grid', placeItems: 'center' },
|
||||
}, svg(ICON.micOn, compact ? 11 : 13, 2)),
|
||||
(statusWord && !compact) && h('span', {
|
||||
key: 's',
|
||||
style: {
|
||||
flex: 'none', fontFamily: MONO, fontSize: '10.5px',
|
||||
color: peer.state === LEG_STATE.FAILED ? '#e5727a' : '#9a9aa2',
|
||||
color: peer.state === LEG_STATE.FAILED ? 'var(--sb-red)' : 'var(--sb-text-6)',
|
||||
},
|
||||
}, statusWord),
|
||||
(!self && !compact) && qualityBars(peer.quality),
|
||||
@@ -300,7 +300,7 @@ export function GroupCallUI({
|
||||
|
||||
const encBadge = h('span', {
|
||||
key: 'enc',
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '5px', fontSize: '11.5px', fontWeight: 600, color: '#3ecf8e' },
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '5px', fontSize: '11.5px', fontWeight: 600, color: 'var(--sb-green)' },
|
||||
}, [svg(ICON.lock, 12, 2), t('call.encryptedShort')]);
|
||||
|
||||
// ── somebody is calling the group and we have not joined ─────────────────
|
||||
@@ -308,12 +308,12 @@ export function GroupCallUI({
|
||||
return h('div', {
|
||||
style: {
|
||||
position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column',
|
||||
background: 'radial-gradient(680px 460px at 50% 36%, rgba(240,137,42,0.08), transparent 70%), #0d0d0f',
|
||||
background: 'radial-gradient(680px 460px at 50% 36%, rgba(var(--sb-orange-rgb), 0.08), transparent 70%), var(--sb-bg-deep)',
|
||||
animation: 'sbExpand .2s ease',
|
||||
},
|
||||
}, [
|
||||
h('div', { key: 'top', style: { flex: 'none', padding: '16px 18px' } },
|
||||
h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: '#3ecf8e' } },
|
||||
h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: 'var(--sb-green)' } },
|
||||
[svg(ICON.lock, 13, 2), t('call.encrypted')])),
|
||||
h('div', { key: 'mid', style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '0 24px', textAlign: 'center' } }, [
|
||||
h('div', {
|
||||
@@ -323,40 +323,40 @@ export function GroupCallUI({
|
||||
display: 'grid', placeItems: 'center',
|
||||
},
|
||||
}, [
|
||||
h('span', { key: 'p1', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(240,137,42,0.5)', animation: 'sbCallPulse 2s ease-out infinite' } }),
|
||||
h('span', { key: 'p2', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(240,137,42,0.4)', animation: 'sbCallPulse 2s ease-out infinite', animationDelay: '1s' } }),
|
||||
h('span', { key: 'p1', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(var(--sb-orange-rgb), 0.5)', animation: 'sbCallPulse 2s ease-out infinite' } }),
|
||||
h('span', { key: 'p2', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(var(--sb-orange-rgb), 0.4)', animation: 'sbCallPulse 2s ease-out infinite', animationDelay: '1s' } }),
|
||||
h('div', {
|
||||
key: 'c',
|
||||
style: {
|
||||
width: '96px', height: '96px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
background: 'radial-gradient(circle at 35% 30%, #2a2a30, #161618)',
|
||||
border: '1px solid rgba(255,255,255,0.1)', color: '#8a8a92',
|
||||
background: 'radial-gradient(circle at 35% 30%, var(--sb-surface-4), var(--sb-surface))',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.1)', color: 'var(--sb-text-7)',
|
||||
},
|
||||
}, svg(ICON.users, 42, 1.6)),
|
||||
]),
|
||||
h('div', { key: 'n', style: { fontSize: '22px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' } }, groupName),
|
||||
h('div', { key: 's', style: { fontFamily: MONO, fontSize: '13.5px', color: '#9a9aa2', marginTop: '8px' } },
|
||||
h('div', { key: 'n', style: { fontSize: '22px', fontWeight: 800, letterSpacing: '-0.4px', color: 'var(--sb-text-1)' } }, groupName),
|
||||
h('div', { key: 's', style: { fontFamily: MONO, fontSize: '13.5px', color: 'var(--sb-text-6)', marginTop: '8px' } },
|
||||
call.withVideo
|
||||
? t('groupCall.startedVideo', { name: call.startedByName })
|
||||
: t('groupCall.startedVoice', { name: call.startedByName })),
|
||||
h('div', { key: 'p', style: { fontSize: '12.5px', color: '#6b6b73', marginTop: '6px' } },
|
||||
h('div', { key: 'p', style: { fontSize: '12.5px', color: 'var(--sb-text-9)', marginTop: '6px' } },
|
||||
t('groupCall.inCall', { count: call.participants.length })),
|
||||
]),
|
||||
h('div', { key: 'ctrls', style: { flex: 'none', display: 'flex', justifyContent: 'center', gap: '48px', padding: '24px 24px 40px' } }, [
|
||||
h('div', { key: 'dec', style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' } }, [
|
||||
h('button', { key: 'b', onClick: onDismiss, title: t('groupCall.dismiss'), style: { ...endBtn, width: '62px', height: '62px' } }, svg(ICON.phoneHangup, 24, 1.9)),
|
||||
h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: '#8a8a92' } }, t('groupCall.dismiss')),
|
||||
h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: 'var(--sb-text-7)' } }, t('groupCall.dismiss')),
|
||||
]),
|
||||
h('div', { key: 'acc', style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' } }, [
|
||||
h('button', {
|
||||
key: 'b', onClick: onJoin, title: t('groupCall.join'),
|
||||
style: {
|
||||
width: '62px', height: '62px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
border: 'none', background: '#3ecf8e', color: '#06231a', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(62,207,142,0.35)',
|
||||
border: 'none', background: 'var(--sb-green-solid)', color: 'var(--sb-on-green)', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(var(--sb-green-rgb), 0.35)',
|
||||
},
|
||||
}, svg(ICON.phone, 24, 1.9)),
|
||||
h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: '#8a8a92' } }, t('groupCall.join')),
|
||||
h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: 'var(--sb-text-7)' } }, t('groupCall.join')),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
@@ -379,8 +379,8 @@ export function GroupCallUI({
|
||||
return h('div', {
|
||||
style: {
|
||||
position: 'absolute', bottom: '18px', insetInlineEnd: '18px', zIndex: 40, width: '244px',
|
||||
borderRadius: '14px', overflow: 'hidden', background: '#161618',
|
||||
border: '1px solid rgba(255,255,255,0.1)', boxShadow: '0 18px 44px rgba(0,0,0,0.55)',
|
||||
borderRadius: '14px', overflow: 'hidden', background: 'var(--sb-surface)',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.1)', boxShadow: '0 18px 44px rgba(var(--sb-shadow-rgb), calc(0.55 * var(--sb-shadow-k)))',
|
||||
animation: 'sbExpand .18s ease',
|
||||
},
|
||||
}, [
|
||||
@@ -389,24 +389,24 @@ export function GroupCallUI({
|
||||
key: 'ic',
|
||||
style: {
|
||||
flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid',
|
||||
placeItems: 'center', background: 'rgba(62,207,142,0.1)',
|
||||
border: '1px solid rgba(62,207,142,0.25)', color: '#3ecf8e',
|
||||
placeItems: 'center', background: 'rgba(var(--sb-green-rgb), 0.1)',
|
||||
border: '1px solid rgba(var(--sb-green-rgb), 0.25)', color: 'var(--sb-green)',
|
||||
},
|
||||
}, svg(ICON.users, 16, 1.9)),
|
||||
h('div', { key: 'tx', style: { flex: 1, minWidth: 0 } }, [
|
||||
h('div', { key: 'n', style: { fontSize: '13px', fontWeight: 700, color: '#f4f4f6', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, groupName),
|
||||
h('div', { key: 'n', style: { fontSize: '13px', fontWeight: 700, color: 'var(--sb-text-1)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, groupName),
|
||||
h('div', {
|
||||
key: 's',
|
||||
style: {
|
||||
fontFamily: MONO, fontSize: '11px', color: talking ? '#3ecf8e' : '#9a9aa2',
|
||||
fontFamily: MONO, fontSize: '11px', color: talking ? 'var(--sb-green)' : 'var(--sb-text-6)',
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
},
|
||||
}, talking
|
||||
? t('groupCall.speaking', { name: talking })
|
||||
: `${t('groupCall.inCall', { count: call.participants.length })} · ${fmt(seconds)}`),
|
||||
]),
|
||||
h('button', { key: 'exp', onClick: () => setMinimized(false), title: t('call.expand'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'rgba(255,255,255,0.05)', color: '#cfcfd4', cursor: 'pointer' } }, svg(ICON.expand, 15, 2)),
|
||||
h('button', { key: 'end', onClick: onLeave, title: t('groupCall.leave'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: '#e5484d', color: '#fff', cursor: 'pointer' } }, svg(ICON.phoneHangup, 15, 2)),
|
||||
h('button', { key: 'exp', onClick: () => setMinimized(false), title: t('call.expand'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'rgba(var(--sb-ink), 0.05)', color: 'var(--sb-text-4)', cursor: 'pointer' } }, svg(ICON.expand, 15, 2)),
|
||||
h('button', { key: 'end', onClick: onLeave, title: t('groupCall.leave'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'var(--sb-red-strong-solid)', color: '#fff', cursor: 'pointer' } }, svg(ICON.phoneHangup, 15, 2)),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -436,7 +436,7 @@ export function GroupCallUI({
|
||||
return h('div', {
|
||||
style: {
|
||||
position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column',
|
||||
background: '#0a0a0c', animation: 'sbExpand .2s ease',
|
||||
background: 'var(--sb-bg-deepest)', animation: 'sbExpand .2s ease',
|
||||
},
|
||||
}, [
|
||||
h('div', {
|
||||
@@ -449,9 +449,9 @@ export function GroupCallUI({
|
||||
h('div', { key: 'l', style: { minWidth: 0 } }, [
|
||||
h('div', { key: 'n', style: { fontSize: '17px', fontWeight: 800, letterSpacing: '-0.3px', color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, groupName),
|
||||
h('div', { key: 's', style: { display: 'inline-flex', alignItems: 'center', gap: '9px', marginTop: '4px' } }, [
|
||||
h('span', { key: 'd', style: { fontFamily: MONO, fontSize: '12.5px', color: '#e8e8eb' } }, fmt(seconds)),
|
||||
h('span', { key: 'd', style: { fontFamily: MONO, fontSize: '12.5px', color: 'var(--sb-text-2)' } }, fmt(seconds)),
|
||||
encBadge,
|
||||
h('span', { key: 'c', style: { fontSize: '11.5px', color: '#8a8a92' } },
|
||||
h('span', { key: 'c', style: { fontSize: '11.5px', color: 'var(--sb-text-7)' } },
|
||||
t('groupCall.inCall', { count: call.participants.length })),
|
||||
]),
|
||||
]),
|
||||
@@ -460,7 +460,7 @@ export function GroupCallUI({
|
||||
key: 'grid', onClick: () => setPinned(null), title: t('groupCall.showEveryone'),
|
||||
style: {
|
||||
height: '36px', padding: '0 12px', borderRadius: '9px', display: 'inline-flex',
|
||||
alignItems: 'center', gap: '7px', border: '1px solid rgba(255,255,255,0.15)',
|
||||
alignItems: 'center', gap: '7px', border: '1px solid rgba(var(--sb-ink), 0.15)',
|
||||
background: 'rgba(0,0,0,0.35)', color: '#fff', cursor: 'pointer',
|
||||
fontFamily: 'inherit', fontSize: '12.5px', fontWeight: 600,
|
||||
},
|
||||
@@ -473,8 +473,8 @@ export function GroupCallUI({
|
||||
key: 'err',
|
||||
style: {
|
||||
flex: 'none', margin: '0 18px 10px', padding: '9px 12px', borderRadius: '10px',
|
||||
background: 'rgba(229,114,122,0.1)', border: '1px solid rgba(229,114,122,0.28)',
|
||||
color: '#e5727a', fontSize: '12.5px', lineHeight: 1.5,
|
||||
background: 'rgba(var(--sb-red-rgb), 0.1)', border: '1px solid rgba(var(--sb-red-rgb), 0.28)',
|
||||
color: 'var(--sb-red)', fontSize: '12.5px', lineHeight: 1.5,
|
||||
},
|
||||
}, t(`groupCall.err.${media.error}`)),
|
||||
|
||||
|
||||
@@ -15,18 +15,18 @@ import { GROUP_LIMITS } from '../../group/groupCrypto.js';
|
||||
const h = (...args) => React.createElement(...args);
|
||||
|
||||
const C = {
|
||||
bg: '#0c0c0e',
|
||||
panel: '#141417',
|
||||
panel2: '#1b1b1f',
|
||||
line: 'rgba(255,255,255,0.07)',
|
||||
line2: 'rgba(255,255,255,0.13)',
|
||||
ink: '#f4f4f6',
|
||||
ink2: '#a7a7b0',
|
||||
ink3: '#6b6b73',
|
||||
accent: '#f0892a',
|
||||
good: '#3ecf8e',
|
||||
warn: '#e3b341',
|
||||
bad: '#e5727a',
|
||||
bg: 'var(--sb-bg-deep)',
|
||||
panel: 'var(--sb-surface)',
|
||||
panel2: 'var(--sb-surface-2)',
|
||||
line: 'rgba(var(--sb-ink), 0.07)',
|
||||
line2: 'rgba(var(--sb-ink), 0.13)',
|
||||
ink: 'var(--sb-text-1)',
|
||||
ink2: 'var(--sb-text-5b)',
|
||||
ink3: 'var(--sb-text-9)',
|
||||
accent: 'var(--sb-orange)',
|
||||
good: 'var(--sb-green)',
|
||||
warn: 'var(--sb-yellow-2)',
|
||||
bad: 'var(--sb-red)',
|
||||
mono: "'JetBrains Mono', ui-monospace, monospace",
|
||||
};
|
||||
|
||||
@@ -53,18 +53,18 @@ const btn = (accent = false) => ({
|
||||
fontFamily: 'inherit', fontSize: '14px', fontWeight: 700,
|
||||
border: accent ? 'none' : `1px solid ${C.line2}`,
|
||||
background: accent ? C.accent : 'transparent',
|
||||
color: accent ? '#1a0f04' : C.ink2,
|
||||
color: accent ? 'var(--sb-on-accent)' : C.ink2,
|
||||
});
|
||||
|
||||
const overlay = {
|
||||
position: 'fixed', inset: 0, zIndex: 90, display: 'grid', placeItems: 'center',
|
||||
background: 'rgba(5,5,7,0.72)', backdropFilter: 'blur(6px)', padding: '20px',
|
||||
background: 'rgba(var(--sb-scrim-rgb), 0.72)', backdropFilter: 'blur(6px)', padding: '20px',
|
||||
};
|
||||
|
||||
const card = {
|
||||
width: '100%', maxWidth: '440px', background: C.panel, border: `1px solid ${C.line}`,
|
||||
borderRadius: '16px', padding: '24px', display: 'flex', flexDirection: 'column', gap: '18px',
|
||||
boxShadow: '0 24px 60px rgba(0,0,0,0.5)',
|
||||
boxShadow: '0 24px 60px rgba(var(--sb-shadow-rgb), calc(0.5 * var(--sb-shadow-k)))',
|
||||
};
|
||||
|
||||
/** Trim a string so its UTF-8 encoding fits `max` bytes, never mid-character. */
|
||||
@@ -167,7 +167,7 @@ export function GroupSasModal({ group, onConfirm, onCancel }) {
|
||||
key: 'code',
|
||||
style: {
|
||||
padding: '22px 16px', textAlign: 'center', borderRadius: '12px',
|
||||
background: 'rgba(240,137,42,0.09)', border: '1px solid rgba(240,137,42,0.3)',
|
||||
background: 'rgba(var(--sb-orange-rgb), 0.09)', border: '1px solid rgba(var(--sb-orange-rgb), 0.3)',
|
||||
},
|
||||
}, h('span', {
|
||||
// Everyone in the group compares these digits against each other's
|
||||
@@ -191,7 +191,7 @@ export function GroupSasModal({ group, onConfirm, onCancel }) {
|
||||
key: 'warn',
|
||||
style: {
|
||||
margin: 0, padding: '11px 13px', borderRadius: '9px', fontSize: '12.5px', lineHeight: 1.55,
|
||||
background: 'rgba(229,114,122,0.09)', border: '1px solid rgba(229,114,122,0.26)', color: '#f0a6ab',
|
||||
background: 'rgba(var(--sb-red-rgb), 0.09)', border: '1px solid rgba(var(--sb-red-rgb), 0.26)', color: 'var(--sb-red-soft)',
|
||||
},
|
||||
}, failed
|
||||
? t('group.errNothingSent')
|
||||
@@ -284,8 +284,8 @@ export function CreateGroupModal({ candidates, relayOnly, onCreate, onCancel })
|
||||
style: {
|
||||
display: 'flex', alignItems: 'center', gap: '11px', padding: '10px 12px',
|
||||
borderRadius: '10px', cursor: full ? 'not-allowed' : 'pointer', textAlign: 'start',
|
||||
background: on ? 'rgba(240,137,42,0.1)' : 'transparent',
|
||||
border: `1px solid ${on ? 'rgba(240,137,42,0.32)' : C.line}`,
|
||||
background: on ? 'rgba(var(--sb-orange-rgb), 0.1)' : 'transparent',
|
||||
border: `1px solid ${on ? 'rgba(var(--sb-orange-rgb), 0.32)' : C.line}`,
|
||||
opacity: full ? 0.4 : 1, fontFamily: 'inherit',
|
||||
},
|
||||
}, [
|
||||
@@ -318,7 +318,7 @@ export function CreateGroupModal({ candidates, relayOnly, onCreate, onCancel })
|
||||
key: 'ip',
|
||||
style: {
|
||||
margin: 0, padding: '11px 13px', borderRadius: '9px', fontSize: '12.5px', lineHeight: 1.55,
|
||||
background: 'rgba(227,179,65,0.08)', border: '1px solid rgba(227,179,65,0.26)', color: '#e3b341',
|
||||
background: 'rgba(var(--sb-yellow-2-rgb), 0.08)', border: '1px solid rgba(var(--sb-yellow-2-rgb), 0.26)', color: 'var(--sb-yellow-2)',
|
||||
},
|
||||
}, t('group.relayOnlyOff')),
|
||||
|
||||
@@ -412,8 +412,8 @@ export function AddMembersModal({ candidates, remaining, onAdd, onCancel }) {
|
||||
style: {
|
||||
display: 'flex', alignItems: 'center', gap: '11px', padding: '10px 12px',
|
||||
borderRadius: '10px', cursor: full ? 'not-allowed' : 'pointer', textAlign: 'start',
|
||||
background: on ? 'rgba(240,137,42,0.1)' : 'transparent',
|
||||
border: `1px solid ${on ? 'rgba(240,137,42,0.32)' : C.line}`,
|
||||
background: on ? 'rgba(var(--sb-orange-rgb), 0.1)' : 'transparent',
|
||||
border: `1px solid ${on ? 'rgba(var(--sb-orange-rgb), 0.32)' : C.line}`,
|
||||
opacity: full ? 0.4 : 1, fontFamily: 'inherit',
|
||||
},
|
||||
}, [
|
||||
@@ -521,7 +521,7 @@ function MemberStrip({ group, onRemove, isAdmin }) {
|
||||
// fact that a dropped connection does not change — but the chip
|
||||
// says plainly that nothing sent now reaches them.
|
||||
background: lost ? 'transparent' : C.panel2,
|
||||
border: `1px solid ${lost ? 'rgba(229,114,122,0.3)' : C.line}`,
|
||||
border: `1px solid ${lost ? 'rgba(var(--sb-red-rgb), 0.3)' : C.line}`,
|
||||
fontSize: '12.5px', color: self ? C.ink : (lost ? C.ink3 : C.ink2),
|
||||
opacity: lost ? 0.75 : 1,
|
||||
},
|
||||
@@ -570,8 +570,8 @@ function Bubble({ msg }) {
|
||||
key: 'b',
|
||||
style: {
|
||||
padding: '9px 13px', borderRadius: mine ? '13px 13px 4px 13px' : '13px 13px 13px 4px',
|
||||
background: mine ? 'rgba(240,137,42,0.14)' : C.panel2,
|
||||
border: `1px solid ${mine ? 'rgba(240,137,42,0.26)' : C.line}`,
|
||||
background: mine ? 'rgba(var(--sb-orange-rgb), 0.14)' : C.panel2,
|
||||
border: `1px solid ${mine ? 'rgba(var(--sb-orange-rgb), 0.26)' : C.line}`,
|
||||
color: C.ink, fontSize: '14.5px', lineHeight: 1.5, wordBreak: 'break-word', whiteSpace: 'pre-wrap',
|
||||
},
|
||||
}, msg.message),
|
||||
@@ -643,8 +643,8 @@ export function GroupChatView({
|
||||
key: 'av',
|
||||
style: {
|
||||
flex: 'none', width: '38px', height: '38px', borderRadius: '11px', display: 'grid',
|
||||
placeItems: 'center', background: 'rgba(240,137,42,0.12)',
|
||||
border: '1px solid rgba(240,137,42,0.24)', color: C.accent,
|
||||
placeItems: 'center', background: 'rgba(var(--sb-orange-rgb), 0.12)',
|
||||
border: '1px solid rgba(var(--sb-orange-rgb), 0.24)', color: C.accent,
|
||||
fontFamily: C.mono, fontSize: '12px', fontWeight: 700,
|
||||
},
|
||||
}, groupInitials(group.name)),
|
||||
@@ -693,7 +693,7 @@ export function GroupChatView({
|
||||
h('button', {
|
||||
key: 'leave', className: 'sb-gh-btn', onClick: onLeave, title: t('group.leaveThis'),
|
||||
'aria-label': t('group.leaveThis'),
|
||||
style: { ...btn(false), flex: 'none', padding: '8px 12px', fontSize: '12.5px', color: C.bad, borderColor: 'rgba(229,114,122,0.3)' },
|
||||
style: { ...btn(false), flex: 'none', padding: '8px 12px', fontSize: '12.5px', color: C.bad, borderColor: 'rgba(var(--sb-red-rgb), 0.3)' },
|
||||
}, [
|
||||
svg(ICON.exit, { key: 'i' }),
|
||||
h('span', { key: 'l', className: 'sb-gh-label' }, t('group.leave')),
|
||||
@@ -706,7 +706,7 @@ export function GroupChatView({
|
||||
key: 'relay-note',
|
||||
style: {
|
||||
flex: 'none', padding: '8px 16px', fontSize: '12px', lineHeight: 1.5, color: C.warn,
|
||||
background: 'rgba(227,179,65,0.08)', borderBottom: `1px solid ${C.line}`,
|
||||
background: 'rgba(var(--sb-yellow-2-rgb), 0.08)', borderBottom: `1px solid ${C.line}`,
|
||||
},
|
||||
}, t('group.relayNote')),
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { t } from '../../i18n/index.js';
|
||||
// instead of embedding the whole manifest.
|
||||
import { version as packageVersion } from '../../../package.json';
|
||||
import { LanguageSwitcher } from './LanguageSwitcher.jsx';
|
||||
import { ThemeSwitcher } from './ThemeSwitcher.jsx';
|
||||
|
||||
const APP_VERSION = `v${packageVersion}`;
|
||||
|
||||
@@ -362,7 +363,7 @@ const EnhancedMinimalHeader = ({
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
background: rgba(var(--sb-scrim-rgb), 0.8);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -372,15 +373,15 @@ const EnhancedMinimalHeader = ({
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
background: var(--sb-legacy-panel);
|
||||
color: var(--sb-text-1);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
white-space: pre-line;
|
||||
border: 1px solid #333;
|
||||
border: 1px solid var(--sb-surface-5);
|
||||
`;
|
||||
|
||||
content.textContent = message;
|
||||
@@ -516,12 +517,15 @@ const EnhancedMinimalHeader = ({
|
||||
// ============================================
|
||||
|
||||
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)';
|
||||
? (displaySecurityLevel.color === 'green' ? 'var(--sb-green)' : displaySecurityLevel.color === 'orange' ? 'var(--sb-orange)' : displaySecurityLevel.color === 'yellow' ? 'var(--sb-yellow)' : 'var(--sb-red)')
|
||||
: 'var(--sb-green)';
|
||||
// The dot is a mark, not a label, so it takes the -solid accents: the brand colour
|
||||
// in both themes. The text-safe accents darken on a light ground, which is right for
|
||||
// a word and wrong for a 7px circle.
|
||||
const dotColor = isConnected ? 'var(--sb-green-solid)'
|
||||
: (['connecting', 'verifying', 'retrying', 'reconnecting'].includes(status) ? 'var(--sb-yellow-solid)'
|
||||
: (status === 'failed' ? 'var(--sb-red-solid)' : 'var(--sb-text-9)'));
|
||||
const dotGlow = dotColor === 'var(--sb-green-solid)' ? 'rgba(var(--sb-green-rgb), 0.16)' : dotColor === 'var(--sb-yellow-solid)' ? 'rgba(var(--sb-yellow-rgb), 0.16)' : dotColor === 'var(--sb-red-solid)' ? 'rgba(var(--sb-red-rgb), 0.16)' : 'rgba(var(--sb-text-9-rgb), 0.16)';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
|
||||
// On the landing / setup view (no verified connection) the new Start Secure
|
||||
@@ -555,9 +559,9 @@ const EnhancedMinimalHeader = ({
|
||||
const overlay = { position: 'fixed', top: 0, left: 0, right: 0, ...safeTop };
|
||||
const headerStyle = onLanding
|
||||
? (scrolled
|
||||
? { ...overlay, background: 'rgba(15,15,17,0.72)', backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: '1px solid rgba(255,255,255,0.06)', transition: MATERIALISE }
|
||||
? { ...overlay, background: 'rgba(var(--sb-bg-rgb), 0.72)', backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: '1px solid rgba(var(--sb-ink), 0.06)', transition: MATERIALISE }
|
||||
: { ...overlay, background: 'transparent', backdropFilter: 'blur(0px) saturate(100%)', WebkitBackdropFilter: 'blur(0px) saturate(100%)', borderBottom: '1px solid transparent', transition: MATERIALISE })
|
||||
: { ...safeTop, background: 'rgba(18,18,20,0.72)', backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: '1px solid rgba(255,255,255,0.06)' };
|
||||
: { ...safeTop, background: 'rgba(var(--sb-bg-rgb), 0.72)', backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: '1px solid rgba(var(--sb-ink), 0.06)' };
|
||||
|
||||
return React.createElement('header', {
|
||||
className: onLanding ? 'header-minimal z-50' : 'header-minimal sticky top-0 z-50',
|
||||
@@ -580,10 +584,10 @@ const EnhancedMinimalHeader = ({
|
||||
),
|
||||
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' } }, APP_VERSION)
|
||||
React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: 'var(--sb-text-2)' } }, 'SecureBit'),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: 'var(--sb-text-faint)' } }, APP_VERSION)
|
||||
]),
|
||||
React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: '#6b6b73', fontWeight: 500 } }, t('hdr.tagline'))
|
||||
React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: 'var(--sb-text-9)', fontWeight: 500 } }, t('hdr.tagline'))
|
||||
])
|
||||
]),
|
||||
// Right: controls
|
||||
@@ -592,33 +596,38 @@ const EnhancedMinimalHeader = ({
|
||||
// tear down an live peer connection if offered inside the chat.
|
||||
onLanding && React.createElement(LanguageSwitcher, { key: 'lang' }),
|
||||
|
||||
// Landing only, alongside the language switcher: the connected
|
||||
// header has no room left, and the preference survives the move
|
||||
// into the chat regardless of where it was set.
|
||||
onLanding && React.createElement(ThemeSwitcher, { key: 'theme' }),
|
||||
|
||||
!onLanding && React.createElement('button', {
|
||||
key: 'net', type: 'button',
|
||||
onClick: () => window.dispatchEvent(new CustomEvent('securebit:open-network-settings')),
|
||||
title: t('hdr.netSettingsTitle'), 'aria-label': t('hdr.netSettings'),
|
||||
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' }
|
||||
style: { display: 'grid', placeItems: 'center', width: '38px', height: '38px', borderRadius: '9px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'rgba(var(--sb-ink), 0.02)', color: 'var(--sb-text-6)', 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' }
|
||||
style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '7px 12px', borderRadius: '9px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'rgba(var(--sb-ink), 0.02)', cursor: 'pointer' }
|
||||
}, [
|
||||
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' } }, (t(`secLevel.${displaySecurityLevel.level}`) === `secLevel.${displaySecurityLevel.level}` ? String(displaySecurityLevel.level) : t(`secLevel.${displaySecurityLevel.level}`))),
|
||||
React.createElement('span', { key: 's', style: { fontFamily: MONO, fontSize: '11.5px', color: '#8a8a92' } }, displaySecurityLevel.score + '%')
|
||||
React.createElement('span', { key: 'l', className: 'hidden sm:inline', style: { fontSize: '12.5px', fontWeight: 600, color: 'var(--sb-text-2)' } }, (t(`secLevel.${displaySecurityLevel.level}`) === `secLevel.${displaySecurityLevel.level}` ? String(displaySecurityLevel.level) : t(`secLevel.${displaySecurityLevel.level}`))),
|
||||
React.createElement('span', { key: 's', style: { fontFamily: MONO, fontSize: '11.5px', color: 'var(--sb-text-7)' } }, 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)' } }, [
|
||||
!onLanding && React.createElement('div', { key: 'status', style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 13px', borderRadius: '9px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'rgba(var(--sb-ink), 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)
|
||||
React.createElement('span', { key: 't', className: 'hidden sm:inline', style: { fontSize: '13px', fontWeight: 600, color: 'var(--sb-text-4)' } }, config.text)
|
||||
]),
|
||||
|
||||
isConnected && React.createElement('button', {
|
||||
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' }
|
||||
style: { display: 'flex', alignItems: 'center', gap: '7px', padding: '8px 14px', borderRadius: '9px', border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'transparent', color: 'var(--sb-text-6)', fontFamily: 'inherit', fontSize: '13px', fontWeight: 600, cursor: 'pointer', transition: 'all .15s' }
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-power-off', style: { fontSize: '12px' } }),
|
||||
React.createElement('span', { key: 't', className: 'sb-hide-sm' }, t('hdr.disconnect'))
|
||||
|
||||
@@ -107,8 +107,11 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
|
||||
// ── Design import: dark slide-up overlay (orange/green accents) ──────────
|
||||
const h = React.createElement;
|
||||
const C_ORANGE = '#f0892a';
|
||||
const C_GREEN = '#3ecf8e';
|
||||
const C_ORANGE = 'var(--sb-orange)';
|
||||
const C_GREEN = 'var(--sb-green)';
|
||||
// Fill forms of the two: brand colour in both themes. See app.jsx.
|
||||
const C_ORANGE_SOLID = 'var(--sb-orange-solid)';
|
||||
const C_GREEN_SOLID = 'var(--sb-green-solid)';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
|
||||
// radio card (public / own servers)
|
||||
@@ -117,16 +120,16 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
style: Object.assign({
|
||||
width: '100%', textAlign: 'start', 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',
|
||||
border: `1px solid ${selected ? 'rgba(var(--sb-orange-rgb), 0.45)' : 'rgba(var(--sb-ink), 0.07)'}`,
|
||||
background: selected ? 'rgba(var(--sb-orange-rgb), 0.06)' : 'var(--sb-surface)',
|
||||
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: 'ring', style: { flex: 'none', width: '18px', height: '18px', marginTop: '1px', borderRadius: '50%', border: `1.5px solid ${selected ? C_ORANGE : 'rgba(var(--sb-ink), 0.22)'}`, display: 'grid', placeItems: 'center' } },
|
||||
h('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: selected ? C_ORANGE_SOLID : '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)
|
||||
h('span', { key: 't', style: { display: 'block', fontSize: '14px', fontWeight: 700, color: 'var(--sb-text-1)' } }, title),
|
||||
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', color: 'var(--sb-text-7)', marginTop: '2px' } }, desc)
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -136,67 +139,67 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
style: {
|
||||
width: '100%', textAlign: 'start', 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',
|
||||
border: `1px solid ${on ? 'rgba(var(--sb-green-rgb), 0.3)' : 'rgba(var(--sb-ink), 0.07)'}`,
|
||||
background: on ? 'rgba(var(--sb-green-rgb), 0.05)' : 'var(--sb-surface)',
|
||||
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: 't', style: { fontSize: '14px', fontWeight: 700, color: 'var(--sb-text-1)' } }, title),
|
||||
badge && h('span', { key: 'b', style: { fontSize: '10px', fontWeight: 700, color: C_GREEN, padding: '2px 7px', borderRadius: '5px', background: 'rgba(var(--sb-green-rgb), 0.1)', border: '1px solid rgba(var(--sb-green-rgb), 0.22)' } }, badge)
|
||||
]),
|
||||
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', lineHeight: 1.5, color: '#8a8a92', marginTop: '3px' } }, desc)
|
||||
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', lineHeight: 1.5, color: 'var(--sb-text-7)', 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', { key: 'tr', style: { flex: 'none', width: '42px', height: '24px', borderRadius: '99px', background: on ? (accent || C_GREEN_SOLID) : 'rgba(var(--sb-ink), 0.08)', border: `1px solid ${on ? (accent || C_GREEN) : 'rgba(var(--sb-ink), 0.12)'}`, position: 'relative', transition: 'all .18s', marginTop: '1px' } },
|
||||
h('span', { style: { position: 'absolute', top: '2px', insetInlineStart: '2px', width: '18px', height: '18px', borderRadius: '50%', background: '#fff', transform: on ? `translateX(${18 * direction()}px)` : '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' } },
|
||||
body.push(h('p', { key: 'intro', style: { margin: '0 0 18px', fontSize: '13.5px', lineHeight: 1.6, color: 'var(--sb-text-6)' } },
|
||||
t('ice.intro')));
|
||||
body.push(radioCard(!useCustom, () => setUseCustom(false), t('ice.publicTitle'), t('ice.publicDesc')));
|
||||
body.push(radioCard(useCustom, () => setUseCustom(true), t('ice.customTitle'), t('ice.customDesc', { max: ICE_LIMITS.MAX_SERVERS }), useCustom ? { marginBottom: '14px' } : null));
|
||||
|
||||
if (useCustom) {
|
||||
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' } },
|
||||
custom.push(h('div', { key: 'ta', style: { borderRadius: '13px', border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-bg-deep)', overflow: 'hidden', marginBottom: '12px' } },
|
||||
h('textarea', {
|
||||
dir: 'ltr',
|
||||
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' }
|
||||
style: { width: '100%', resize: 'vertical', border: 'none', outline: 'none', background: 'transparent', color: 'var(--sb-text-code)', fontFamily: MONO, fontSize: '12px', lineHeight: 1.65, padding: '13px 14px', minHeight: '104px' }
|
||||
})));
|
||||
if (parsed.errors.length > 0) {
|
||||
custom.push(h('ul', { key: 'err', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: '#e5727a', fontSize: '12.5px' } },
|
||||
custom.push(h('ul', { key: 'err', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: 'var(--sb-red)', fontSize: '12.5px' } },
|
||||
parsed.errors.slice(0, 6).map((err, i) => h('li', { key: i }, err))));
|
||||
}
|
||||
if (parsed.warnings.length > 0) {
|
||||
custom.push(h('ul', { key: 'warn', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: '#e3c84e', fontSize: '12.5px' } },
|
||||
custom.push(h('ul', { key: 'warn', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: 'var(--sb-yellow)', 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) {
|
||||
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)'}.`));
|
||||
}
|
||||
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' } }, [
|
||||
custom.push(h('div', { key: 'note', style: { display: 'flex', alignItems: 'flex-start', gap: '9px', padding: '12px 13px', borderRadius: '11px', border: '1px solid rgba(var(--sb-green-rgb), 0.18)', background: 'rgba(var(--sb-green-rgb), 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' } }, [
|
||||
h('span', { key: 't', style: { fontSize: '12px', lineHeight: 1.55, color: 'var(--sb-green-muted)' } }, [
|
||||
t('ice.turnNote'),
|
||||
h('span', { key: 'm', style: { fontFamily: MONO, color: C_GREEN } }, 'turns:'), t('ice.turnNoteTls')
|
||||
])
|
||||
]));
|
||||
const testColor = testState === 'done' && testResult && !testResult.error ? C_GREEN : '#cfcfd4';
|
||||
const testColor = testState === 'done' && testResult && !testResult.error ? C_GREEN : 'var(--sb-text-4)';
|
||||
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 }
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '10px 15px', borderRadius: '10px', border: `1px solid ${testState === 'done' && testResult && !testResult.error ? 'rgba(var(--sb-green-rgb), 0.4)' : 'rgba(var(--sb-ink), 0.1)'}`, background: testState === 'done' && testResult && !testResult.error ? 'rgba(var(--sb-green-rgb), 0.08)' : 'rgba(var(--sb-ink), 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' ? t('ice.testing') : t('ice.test')
|
||||
]),
|
||||
(testState === 'done' && testResult) ? h('span', { key: 'res', style: { fontSize: '12px', color: testResult.error ? '#e5727a' : '#8a8a92' } },
|
||||
(testState === 'done' && testResult) ? h('span', { key: 'res', style: { fontSize: '12px', color: testResult.error ? 'var(--sb-red)' : 'var(--sb-text-7)' } },
|
||||
testResult.error
|
||||
? `Test failed: ${testResult.error}`
|
||||
: (testResult.srflx > 0 || testResult.relay > 0)
|
||||
@@ -210,7 +213,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
body.push(toggleRow(relayOnly, () => setRelayOnly(!relayOnly), t('ice.relayTitle'),
|
||||
t('ice.relayDesc'), C_GREEN, t('ice.relayBadge')));
|
||||
if (relayOnly && useCustom && !hasTurn) {
|
||||
body.push(h('p', { key: 'relaywarn', style: { margin: '-4px 0 10px', fontSize: '12.5px', color: '#e3c84e' } },
|
||||
body.push(h('p', { key: 'relaywarn', style: { margin: '-4px 0 10px', fontSize: '12.5px', color: 'var(--sb-yellow)' } },
|
||||
t('ice.relayWarning')));
|
||||
}
|
||||
body.push(toggleRow(persist, () => setPersist(!persist), t('ice.persist'),
|
||||
@@ -220,38 +223,38 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
const footerBtns = [];
|
||||
if (hasSaved) {
|
||||
footerBtns.push(h('button', { key: 'forget', type: 'button', onClick: handleForget,
|
||||
style: { marginInlineEnd: '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' } }, t('ice.forget')));
|
||||
style: { marginInlineEnd: 'auto', padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(var(--sb-red-rgb), 0.3)', background: 'transparent', color: 'var(--sb-red)', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, t('ice.forget')));
|
||||
}
|
||||
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' } }, t('ice.cancel')));
|
||||
style: { padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'transparent', color: 'var(--sb-text-5)', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, t('ice.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)' } }, [
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '11px 20px', borderRadius: '11px', border: 'none', background: C_ORANGE_SOLID, color: 'var(--sb-on-accent)', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 700, cursor: canApply ? 'pointer' : 'not-allowed', opacity: canApply ? 1 : 0.5, boxShadow: '0 6px 18px rgba(var(--sb-orange-rgb), 0.28)' } }, [
|
||||
h('i', { key: 'i', className: 'fas fa-check' }), t('ice.apply')
|
||||
]));
|
||||
|
||||
// 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: 60, 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)' };
|
||||
? { position: 'absolute', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', background: 'var(--sb-bg)', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' }
|
||||
: { position: 'fixed', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', alignItems: 'stretch', background: 'var(--sb-bg)', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' };
|
||||
|
||||
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: 'head', style: { display: 'flex', alignItems: 'center', gap: '12px', padding: '20px 24px', borderBottom: '1px solid rgba(var(--sb-ink), 0.06)' } }, [
|
||||
h('div', { key: 'ic', style: { width: '38px', height: '38px', flex: 'none', display: 'grid', placeItems: 'center', borderRadius: '10px', background: 'rgba(var(--sb-ink), 0.03)', border: '1px solid rgba(var(--sb-ink), 0.06)' } },
|
||||
h('i', { className: 'fas fa-sliders-h', style: { color: 'var(--sb-text-4)', 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' } }, t('ice.title')),
|
||||
h('div', { key: 's', style: { fontSize: '12px', color: '#7b7b83' } }, t('ice.subtitle'))
|
||||
h('div', { key: 't', style: { fontSize: '16.5px', fontWeight: 800, letterSpacing: '-0.3px', color: 'var(--sb-text-1)' } }, t('ice.title')),
|
||||
h('div', { key: 's', style: { fontSize: '12px', color: 'var(--sb-text-8)' } }, t('ice.subtitle'))
|
||||
]),
|
||||
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('button', { key: 'x', type: 'button', onClick: onClose, style: { width: '32px', height: '32px', flex: 'none', display: 'grid', placeItems: 'center', borderRadius: '9px', border: 'none', background: 'rgba(var(--sb-ink), 0.04)', color: 'var(--sb-text-7)', 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)
|
||||
h('div', { key: 'foot', style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '10px', padding: '16px 24px', borderTop: '1px solid rgba(var(--sb-ink), 0.06)', background: 'var(--sb-bg-deep)', borderRadius: '0' } }, footerBtns)
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ const LanguageSuggestion = () => {
|
||||
key: 'line',
|
||||
lang: meta.htmlLang || target,
|
||||
dir,
|
||||
style: { fontSize: '13px', lineHeight: 1.45, color: '#cfcfd4', textAlign: 'start' },
|
||||
style: { fontSize: '13px', lineHeight: 1.45, color: 'var(--sb-text-4)', textAlign: 'start' },
|
||||
}, t('language.suggest.text', null, target));
|
||||
|
||||
const link = React.createElement('a', {
|
||||
@@ -87,8 +87,8 @@ const LanguageSuggestion = () => {
|
||||
style: {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
||||
padding: '7px 12px', borderRadius: '9px',
|
||||
border: '1px solid rgba(240,137,42,0.30)', background: 'rgba(240,137,42,0.12)',
|
||||
color: '#f0892a', fontSize: '12.5px', fontWeight: 600,
|
||||
border: '1px solid rgba(var(--sb-orange-rgb), 0.30)', background: 'rgba(var(--sb-orange-rgb), 0.12)',
|
||||
color: 'var(--sb-orange)', fontSize: '12.5px', fontWeight: 600,
|
||||
textDecoration: 'none', whiteSpace: 'nowrap',
|
||||
},
|
||||
}, t('language.suggest.cta', null, target));
|
||||
@@ -101,8 +101,8 @@ const LanguageSuggestion = () => {
|
||||
'aria-label': t('language.suggest.dismiss', null, target),
|
||||
style: {
|
||||
padding: '7px 10px', borderRadius: '9px',
|
||||
border: '1px solid rgba(255,255,255,0.07)', background: 'rgba(255,255,255,0.02)',
|
||||
color: '#8a8a92', font: 'inherit', fontSize: '12.5px', fontWeight: 500,
|
||||
border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'rgba(var(--sb-ink), 0.02)',
|
||||
color: 'var(--sb-text-7)', font: 'inherit', fontSize: '12.5px', fontWeight: 500,
|
||||
cursor: 'pointer', whiteSpace: 'nowrap',
|
||||
},
|
||||
}, t('language.suggest.dismiss', null, target));
|
||||
@@ -117,8 +117,8 @@ const LanguageSuggestion = () => {
|
||||
maxWidth: 'min(340px, calc(100vw - 48px))',
|
||||
display: 'flex', flexDirection: 'column', gap: '11px',
|
||||
padding: '14px 16px', borderRadius: '14px',
|
||||
border: '1px solid rgba(255,255,255,0.08)', background: '#161618',
|
||||
boxShadow: '0 16px 40px rgba(0,0,0,0.5)',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-surface)',
|
||||
boxShadow: '0 16px 40px rgba(var(--sb-shadow-rgb), calc(0.5 * var(--sb-shadow-k)))',
|
||||
opacity: shown ? 1 : 0,
|
||||
transform: shown || still ? 'none' : 'translateY(10px)',
|
||||
transition: still ? 'opacity .2s linear' : 'opacity .3s ease, transform .3s cubic-bezier(.2,.7,.3,1)',
|
||||
|
||||
@@ -50,9 +50,9 @@ const LanguageSwitcher = () => {
|
||||
style: {
|
||||
display: 'flex', alignItems: 'center', gap: '6px',
|
||||
padding: '7px 10px', borderRadius: '9px',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
background: open ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.02)',
|
||||
color: '#cfcfd4', font: 'inherit', fontSize: '12.5px', fontWeight: 600,
|
||||
border: '1px solid rgba(var(--sb-ink), 0.07)',
|
||||
background: open ? 'rgba(var(--sb-ink), 0.06)' : 'rgba(var(--sb-ink), 0.02)',
|
||||
color: 'var(--sb-text-4)', font: 'inherit', fontSize: '12.5px', fontWeight: 600,
|
||||
cursor: 'pointer', transition: 'background .15s, color .15s',
|
||||
},
|
||||
}, [
|
||||
@@ -77,8 +77,8 @@ const LanguageSwitcher = () => {
|
||||
position: 'absolute', top: 'calc(100% + 6px)', insetInlineEnd: 0, zIndex: 60,
|
||||
display: open ? 'block' : 'none',
|
||||
minWidth: '170px', padding: '5px', borderRadius: '11px',
|
||||
border: '1px solid rgba(255,255,255,0.08)', background: '#161618',
|
||||
boxShadow: '0 14px 34px rgba(0,0,0,0.45)',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-surface)',
|
||||
boxShadow: '0 14px 34px rgba(var(--sb-shadow-rgb), calc(0.45 * var(--sb-shadow-k)))',
|
||||
// Thirteen languages is taller than a phone in landscape. Cap it and scroll
|
||||
// rather than let the list run off the bottom, where the last few entries
|
||||
// would be unreachable.
|
||||
@@ -104,14 +104,14 @@ const LanguageSwitcher = () => {
|
||||
padding: '8px 10px', borderRadius: '8px',
|
||||
fontSize: '13px',
|
||||
fontWeight: link.isCurrent ? 600 : 500,
|
||||
color: link.isCurrent ? '#e8e8eb' : '#9a9aa2',
|
||||
background: link.isCurrent ? 'rgba(255,255,255,0.06)' : 'transparent',
|
||||
color: link.isCurrent ? 'var(--sb-text-2)' : 'var(--sb-text-6)',
|
||||
background: link.isCurrent ? 'rgba(var(--sb-ink), 0.06)' : 'transparent',
|
||||
textDecoration: 'none', whiteSpace: 'nowrap',
|
||||
},
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'a',
|
||||
style: { fontSize: '11px', fontWeight: 700, letterSpacing: '0.4px', color: '#6b6b73', width: '22px', flex: 'none', textAlign: 'start' },
|
||||
style: { fontSize: '11px', fontWeight: 700, letterSpacing: '0.4px', color: 'var(--sb-text-9)', width: '22px', flex: 'none', textAlign: 'start' },
|
||||
}, link.abbr),
|
||||
React.createElement('span', { key: 'n' }, link.label),
|
||||
])));
|
||||
|
||||
@@ -44,22 +44,32 @@ function Roadmap() {
|
||||
features: tList(`roadmap.${d.k}.features`)
|
||||
}));
|
||||
|
||||
// `rgb` is the same colour as `color`, published as bare channels. The pill draws
|
||||
// its fill and border from it at low alpha, and there is no way to add an alpha to
|
||||
// a colour that a custom property has already resolved — hence the pair, which has
|
||||
// to be kept in step.
|
||||
// Three forms of the same status colour, because they are used three ways and only
|
||||
// one of them is text. `color` is the word — it darkens in the light theme so it can
|
||||
// be read on white. `solid` is the dot, which is a mark rather than a label and
|
||||
// stays the brand colour in both themes. `rgb` is the channels, for the pill's fill
|
||||
// and border at low alpha. The three have to be kept in step.
|
||||
const META = {
|
||||
released: { word: t('roadmap.status.released'), color: "#3ecf8e", line: "rgba(62,207,142,0.32)" },
|
||||
current: { word: t('roadmap.status.current'), color: "#f0892a", line: "rgba(240,137,42,0.32)" },
|
||||
dev: { word: t('roadmap.status.dev'), color: "#e3b341", line: "rgba(255,255,255,0.08)" },
|
||||
planned: { word: t('roadmap.status.planned'), color: "#8a8a92", line: "rgba(255,255,255,0.08)" },
|
||||
research: { word: t('roadmap.status.research'), color: "#6b6b73", line: "rgba(255,255,255,0.08)" }
|
||||
released: { word: t('roadmap.status.released'), color: "var(--sb-green)", solid: "var(--sb-green-solid)", rgb: "var(--sb-green-rgb)", line: "rgba(var(--sb-green-rgb), 0.32)" },
|
||||
current: { word: t('roadmap.status.current'), color: "var(--sb-orange)", solid: "var(--sb-orange-solid)", rgb: "var(--sb-orange-rgb)", line: "rgba(var(--sb-orange-rgb), 0.32)" },
|
||||
dev: { word: t('roadmap.status.dev'), color: "var(--sb-yellow-2)", solid: "var(--sb-yellow-2-solid)", rgb: "var(--sb-yellow-2-rgb)", line: "rgba(var(--sb-ink), 0.08)" },
|
||||
planned: { word: t('roadmap.status.planned'), color: "var(--sb-text-7)", solid: "var(--sb-text-7)", rgb: "var(--sb-text-7-rgb)", line: "rgba(var(--sb-ink), 0.08)" },
|
||||
research: { word: t('roadmap.status.research'), color: "var(--sb-text-9)", solid: "var(--sb-text-9)", rgb: "var(--sb-text-9-rgb)", line: "rgba(var(--sb-ink), 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})`;
|
||||
};
|
||||
// Was hexA(), which took the literal hex out of META and split it into channels by
|
||||
// hand. Those literals are custom properties now, so there is nothing to parse at
|
||||
// runtime — the browser does the substitution instead, and it does it again when
|
||||
// the theme changes, which the old version could not.
|
||||
const tint = (channels, a) => `rgba(${channels}, ${a})`;
|
||||
|
||||
const total = DATA.length;
|
||||
const shipped = DATA.filter((d) => d.status === 'released' || d.status === 'current').length;
|
||||
@@ -75,53 +85,53 @@ function Roadmap() {
|
||||
const renderNode = (status) => {
|
||||
if (status === 'released') {
|
||||
return (
|
||||
<div style={{ position: 'absolute', insetInlineStart: '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 style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(var(--sb-green-rgb), 0.16),rgba(var(--sb-green-rgb), 0.16)), var(--sb-bg)', border: '1px solid rgba(var(--sb-green-rgb), 0.4)', zIndex: 2 }}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--sb-green-solid)" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M5 13l4 4 10-11" /></svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (status === 'current') {
|
||||
return (
|
||||
<div style={{ position: 'absolute', insetInlineStart: '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 style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(var(--sb-orange-rgb), 0.2),rgba(var(--sb-orange-rgb), 0.2)), var(--sb-bg)', border: '1px solid var(--sb-orange-solid)', zIndex: 2, animation: 'rmPulse 2.4s ease-out infinite' }}>
|
||||
<span style={{ width: '9px', height: '9px', borderRadius: '50%', background: 'var(--sb-orange-solid)' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (status === 'dev') {
|
||||
return (
|
||||
<div style={{ position: 'absolute', insetInlineStart: '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 style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(var(--sb-yellow-2-rgb), 0.15),rgba(var(--sb-yellow-2-rgb), 0.15)), var(--sb-bg)', border: '1px solid rgba(var(--sb-yellow-2-rgb), 0.4)', zIndex: 2 }}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--sb-yellow-2-solid)" 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', insetInlineStart: '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 style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'var(--sb-bg)', border: `1px ${status === 'research' ? 'dashed' : 'solid'} rgba(var(--sb-ink), 0.18)`, zIndex: 2 }}>
|
||||
<span style={{ width: '7px', height: '7px', borderRadius: '50%', background: META[status].solid }} />
|
||||
</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)}}' }} />
|
||||
<section style={{ width: '100%', color: 'var(--sb-text-2)', fontFamily: SANS, padding: isMobile ? '48px 0' : '64px 0', background: 'radial-gradient(1200px 720px at 50% -8%, rgba(var(--sb-orange-rgb), 0.05), transparent 60%), var(--sb-bg)' }}>
|
||||
<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(var(--sb-orange-rgb), 0.18)}60%{box-shadow:0 0 0 9px rgba(var(--sb-orange-rgb), 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' }}>{t('roadmap.eyebrow')}</div>
|
||||
<h2 style={{ margin: '0 0 14px', fontSize: isMobile ? '27px' : '34px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.08, color: '#f4f4f6' }}>{t('roadmap.heading')}</h2>
|
||||
<p style={{ margin: 0, fontSize: '15.5px', lineHeight: 1.6, color: '#8a8a92', maxWidth: '660px' }}>{t('roadmap.subheading')}</p>
|
||||
<div style={{ fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: 'var(--sb-text-9)', textTransform: 'uppercase', letterSpacing: '1.6px', marginBottom: '13px' }}>{t('roadmap.eyebrow')}</div>
|
||||
<h2 style={{ margin: '0 0 14px', fontSize: isMobile ? '27px' : '34px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.08, color: 'var(--sb-text-1)' }}>{t('roadmap.heading')}</h2>
|
||||
<p style={{ margin: 0, fontSize: '15.5px', lineHeight: 1.6, color: 'var(--sb-text-7)', maxWidth: '660px' }}>{t('roadmap.subheading')}</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' }}>{progressBefore}<span style={{ color: '#3ecf8e' }}>{shipped}</span>{progressAfter}</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 style={{ display: 'flex', alignItems: 'center', gap: '18px', flexWrap: 'wrap', padding: '18px 22px', borderRadius: '14px', background: 'var(--sb-surface)', border: '1px solid rgba(var(--sb-ink), 0.06)', marginBottom: '36px' }}>
|
||||
<div style={{ fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: 'var(--sb-text-2)', whiteSpace: 'nowrap' }}>{progressBefore}<span style={{ color: 'var(--sb-green)' }}>{shipped}</span>{progressAfter}</div>
|
||||
<div style={{ flex: '1 1 240px', minWidth: '200px', height: '8px', borderRadius: '99px', background: 'var(--sb-bg-deep)', border: '1px solid rgba(var(--sb-ink), 0.06)', overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: shippedPct, background: 'linear-gradient(90deg, var(--sb-green), var(--sb-orange))' }} />
|
||||
</div>
|
||||
<div style={{ fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '0.8px', whiteSpace: 'nowrap' }}>{t('roadmap.upcoming', { upcoming })}</div>
|
||||
<div style={{ fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: 'var(--sb-text-9)', textTransform: 'uppercase', letterSpacing: '0.8px', whiteSpace: 'nowrap' }}>{t('roadmap.upcoming', { upcoming })}</div>
|
||||
</div>
|
||||
|
||||
{/* timeline */}
|
||||
@@ -139,37 +149,45 @@ function Roadmap() {
|
||||
</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 style={{ borderRadius: '16px', background: 'var(--sb-surface)', border: `1px solid ${d.status === 'current' ? 'rgba(var(--sb-orange-rgb), 0.28)' : 'rgba(var(--sb-ink), 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)'; }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(var(--sb-ink), 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: 'none', minWidth: '52px', textAlign: 'center', padding: '7px 10px', borderRadius: '9px', background: 'var(--sb-bg-deep)', border: '1px solid rgba(var(--sb-ink), 0.07)', fontFamily: MONO, fontSize: '13px', fontWeight: 700, color: d.status === 'current' ? 'var(--sb-orange)' : 'var(--sb-text-4)' }}>{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 style={{ fontSize: isMobile ? '15.5px' : '17px', fontWeight: 800, letterSpacing: '-0.4px', color: 'var(--sb-text-1)' }}>{d.title}</div>
|
||||
{!isMobile && <div style={{ marginTop: '3px', fontSize: '13.5px', color: 'var(--sb-text-6)' }}>{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: 'end' }}>{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)' }}>
|
||||
{/* The status pill is desktop-only. On a phone its
|
||||
word does not fit, and what was left — a bordered
|
||||
box around a single coloured dot — said nothing the
|
||||
marker on the timeline was not already saying in
|
||||
the same colour, while taking room from the title.
|
||||
So the whole pill goes, not just its label. */}
|
||||
{!isMobile && (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '7px', padding: '6px 11px', borderRadius: '8px', background: tint(meta.rgb, 0.1), border: `1px solid ${tint(meta.rgb, 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.solid }} />
|
||||
{meta.word}
|
||||
</span>
|
||||
)}
|
||||
{!isMobile && <span style={{ fontFamily: MONO, fontSize: '12px', fontWeight: 500, color: 'var(--sb-text-7)', whiteSpace: 'nowrap', minWidth: '74px', textAlign: 'end' }}>{d.date}</span>}
|
||||
<span style={{ color: 'var(--sb-text-9)', 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)' }}>{t('roadmap.keyFeatures')}</div>
|
||||
<div style={{ fontFamily: MONO, fontSize: '10px', fontWeight: 600, color: 'var(--sb-text-faint)', textTransform: 'uppercase', letterSpacing: '1.2px', marginBottom: '14px', paddingTop: '14px', borderTop: '1px solid rgba(var(--sb-ink), 0.05)' }}>{t('roadmap.keyFeatures')}</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>
|
||||
<span style={{ flex: 'none', marginTop: '7px', width: '5px', height: '5px', borderRadius: '50%', background: meta.solid }} />
|
||||
<span style={{ fontSize: '13.5px', lineHeight: 1.5, color: 'var(--sb-text-4)' }}>{f}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// The three-state theme control, modelled on the language switcher next to it.
|
||||
//
|
||||
// Three states rather than a two-way toggle, because "follow the system" is a real
|
||||
// preference and not the same as "dark": someone whose laptop turns light at sunrise
|
||||
// wants the page to follow, and a two-way switch has nowhere to say so. The stored
|
||||
// value is the *mode* ('system' | 'light' | 'dark'), never the colour it resolved to,
|
||||
// so a page opened tomorrow in system mode answers the question again rather than
|
||||
// replaying yesterday's answer.
|
||||
//
|
||||
// Nothing here owns the theme. src/scripts/theme-boot.js does — it runs blocking in
|
||||
// <head>, long before React exists, because the theme has to be right on the first
|
||||
// paint. This component is a view onto it: it reads window.SecureBitTheme, calls set(),
|
||||
// and re-renders from the subscription rather than from its own state, so the header
|
||||
// stays correct when the OS theme changes under it or another tab switches.
|
||||
//
|
||||
// Landing page only, like the language switcher, though for a different reason: there
|
||||
// is no room for it in the connected header, which already carries the security pill,
|
||||
// the connection status and the network settings, and the preference persists across
|
||||
// the transition into the chat anyway.
|
||||
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
// 24×24 line icons, drawn to match the chevron the language switcher uses.
|
||||
const ICON = {
|
||||
system: '<rect x="2.5" y="4" width="19" height="13" rx="2"/><path d="M8 20.5h8M12 17.5v3"/>',
|
||||
light: '<circle cx="12" cy="12" r="4.2"/><path d="M12 2.6v2.4M12 19v2.4M2.6 12h2.4M19 12h2.4M5.3 5.3l1.7 1.7M17 17l1.7 1.7M18.7 5.3L17 7M7 17l-1.7 1.7"/>',
|
||||
dark: '<path d="M20.5 14.3A8.6 8.6 0 0 1 9.7 3.5a8.6 8.6 0 1 0 10.8 10.8z"/>',
|
||||
};
|
||||
|
||||
const svg = (path, size, width) => React.createElement('svg', {
|
||||
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
|
||||
stroke: 'currentColor', strokeWidth: width, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
'aria-hidden': 'true',
|
||||
dangerouslySetInnerHTML: { __html: path },
|
||||
});
|
||||
|
||||
const ThemeSwitcher = () => {
|
||||
const api = typeof window !== 'undefined' ? window.SecureBitTheme : null;
|
||||
// Without the boot script there is nothing to drive, and a control that cannot
|
||||
// change anything is worse than no control.
|
||||
if (!api) return null;
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [mode, setMode] = React.useState(() => api.get());
|
||||
const [resolved, setResolved] = React.useState(() => api.resolved());
|
||||
const rootRef = React.useRef(null);
|
||||
|
||||
// The mode can change without this component doing it — the OS flipping while in
|
||||
// system mode, or another tab writing the preference.
|
||||
React.useEffect(() => api.subscribe((nextMode, nextResolved) => {
|
||||
setMode(nextMode);
|
||||
setResolved(nextResolved);
|
||||
}), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
const onDown = (e) => {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
||||
};
|
||||
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
|
||||
document.addEventListener('pointerdown', onDown);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', onDown);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const OPTIONS = [
|
||||
{ mode: 'system', icon: ICON.system, label: t('theme.system') },
|
||||
{ mode: 'light', icon: ICON.light, label: t('theme.light') },
|
||||
{ mode: 'dark', icon: ICON.dark, label: t('theme.dark') },
|
||||
];
|
||||
|
||||
// The trigger shows what you are *getting*, not what you picked: in system mode the
|
||||
// useful thing to see is whether the page is currently light or dark. Which mode is
|
||||
// selected is the menu's job, and the check mark there says it.
|
||||
const triggerIcon = mode === 'system' ? ICON.system : (resolved === 'light' ? ICON.light : ICON.dark);
|
||||
const current = OPTIONS.find((o) => o.mode === mode) || OPTIONS[0];
|
||||
|
||||
const trigger = React.createElement('button', {
|
||||
key: 'trigger',
|
||||
type: 'button',
|
||||
onClick: () => setOpen((v) => !v),
|
||||
'aria-haspopup': 'menu',
|
||||
'aria-expanded': open ? 'true' : 'false',
|
||||
'aria-label': t('theme.label') + ': ' + current.label,
|
||||
title: t('theme.label'),
|
||||
style: {
|
||||
display: 'flex', alignItems: 'center', gap: '6px',
|
||||
padding: '8px 9px', borderRadius: '9px',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.07)',
|
||||
background: open ? 'rgba(var(--sb-ink), 0.06)' : 'rgba(var(--sb-ink), 0.02)',
|
||||
color: 'var(--sb-text-4)', font: 'inherit',
|
||||
cursor: 'pointer', transition: 'background .15s, color .15s',
|
||||
},
|
||||
}, [
|
||||
React.createElement('span', { key: 'i', style: { display: 'grid', placeItems: 'center' } }, svg(triggerIcon, 15, 1.9)),
|
||||
React.createElement('svg', {
|
||||
key: 'v', width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none',
|
||||
stroke: 'currentColor', strokeWidth: 2.4, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .18s' },
|
||||
dangerouslySetInnerHTML: { __html: '<path d="M6 9l6 6 6-6"/>' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const menu = React.createElement('div', {
|
||||
key: 'menu',
|
||||
role: 'menu',
|
||||
style: {
|
||||
position: 'absolute', top: 'calc(100% + 6px)', insetInlineEnd: 0, zIndex: 60,
|
||||
display: open ? 'block' : 'none',
|
||||
minWidth: '158px', padding: '5px', borderRadius: '11px',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-surface)',
|
||||
boxShadow: '0 14px 34px rgba(var(--sb-shadow-rgb), calc(0.45 * var(--sb-shadow-k)))',
|
||||
},
|
||||
}, OPTIONS.map((option) => {
|
||||
const active = option.mode === mode;
|
||||
return React.createElement('button', {
|
||||
key: option.mode,
|
||||
type: 'button',
|
||||
role: 'menuitemradio',
|
||||
'aria-checked': active ? 'true' : 'false',
|
||||
onClick: () => { api.set(option.mode); setOpen(false); },
|
||||
style: {
|
||||
width: '100%', display: 'flex', alignItems: 'center', gap: '10px',
|
||||
padding: '8px 10px', borderRadius: '8px', border: 'none',
|
||||
fontSize: '13px', fontFamily: 'inherit',
|
||||
fontWeight: active ? 600 : 500,
|
||||
color: active ? 'var(--sb-text-2)' : 'var(--sb-text-6)',
|
||||
background: active ? 'rgba(var(--sb-ink), 0.06)' : 'transparent',
|
||||
cursor: 'pointer', textAlign: 'start', whiteSpace: 'nowrap',
|
||||
},
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'i',
|
||||
style: { flex: 'none', display: 'grid', placeItems: 'center', width: '16px', color: active ? 'var(--sb-orange)' : 'var(--sb-text-9)' },
|
||||
}, svg(option.icon, 14, 1.9)),
|
||||
React.createElement('span', { key: 'l', style: { flex: 1 } }, option.label),
|
||||
// The check is the only thing that distinguishes "system, currently dark"
|
||||
// from "dark" — the trigger icon cannot, because both look dark.
|
||||
active && React.createElement('span', {
|
||||
key: 'c',
|
||||
style: { flex: 'none', display: 'grid', placeItems: 'center', color: 'var(--sb-orange)' },
|
||||
}, svg('<path d="M4.5 12.5l5 5 10-11"/>', 12, 2.6)),
|
||||
]);
|
||||
}));
|
||||
|
||||
return React.createElement('div', {
|
||||
ref: rootRef,
|
||||
style: { position: 'relative', display: 'inline-flex' },
|
||||
}, [trigger, menu]);
|
||||
};
|
||||
|
||||
window.ThemeSwitcher = ThemeSwitcher;
|
||||
|
||||
export { ThemeSwitcher };
|
||||
@@ -19,11 +19,14 @@ const UniqueFeatureSlider = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
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 ACCENT = 'var(--sb-orange)';
|
||||
// The icons are stroked with this, and a stroke is a mark, not a label — brand
|
||||
// colour in both themes. ACCENT above is for text and darkens on a light ground.
|
||||
const ACCENT_SOLID = 'var(--sb-orange-solid)';
|
||||
const ACTIVE_BG = 'radial-gradient(130% 90% at 28% 0%, rgba(var(--sb-orange-rgb), 0.11), transparent 60%), var(--sb-surface)';
|
||||
const ACTIVE_BD = 'rgba(var(--sb-orange-rgb), 0.3)';
|
||||
const IDLE_BG = 'var(--sb-bg)';
|
||||
const IDLE_BD = 'rgba(var(--sb-ink), 0.06)';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
@@ -85,12 +88,12 @@ const UniqueFeatureSlider = () => {
|
||||
key, onClick, 'aria-label': key, className: 'sb-mirror-rtl',
|
||||
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',
|
||||
borderRadius: '50%', border: '1px solid rgba(var(--sb-ink), 0.1)',
|
||||
background: 'rgba(var(--sb-ink), 0.025)', color: 'var(--sb-text-4)', 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'; }
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.1)'; e.currentTarget.style.color = 'var(--sb-text-4)'; }
|
||||
}, svg(path, 18, 'currentColor', 2.1));
|
||||
|
||||
const tag = (label) =>
|
||||
@@ -98,12 +101,12 @@ const UniqueFeatureSlider = () => {
|
||||
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'
|
||||
borderRadius: '9px', border: '1px solid rgba(var(--sb-ink), 0.07)',
|
||||
background: 'rgba(var(--sb-ink), 0.025)', fontFamily: MONO,
|
||||
fontSize: '11.5px', fontWeight: 500, color: 'var(--sb-text-6)'
|
||||
}
|
||||
}, [
|
||||
React.createElement('span', { key: 'dot', style: { width: '5px', height: '5px', borderRadius: '50%', background: '#3ecf8e' } }),
|
||||
React.createElement('span', { key: 'dot', style: { width: '5px', height: '5px', borderRadius: '50%', background: 'var(--sb-green-solid)' } }),
|
||||
label
|
||||
]);
|
||||
|
||||
@@ -124,17 +127,17 @@ const UniqueFeatureSlider = () => {
|
||||
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)'
|
||||
background: 'rgba(var(--sb-orange-rgb), 0.13)', border: '1px solid rgba(var(--sb-orange-rgb), 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)
|
||||
}, svg(s.icon, 26, ACCENT_SOLID, 1.9)),
|
||||
React.createElement('span', { key: 'n', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 600, color: 'var(--sb-text-9)' } }, 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' }
|
||||
key: 'h', style: { margin: '0 0 12px', fontSize: isMobile ? '24px' : '30px', fontWeight: 800, letterSpacing: '-0.7px', lineHeight: 1.08, color: 'var(--sb-text-1)' }
|
||||
}, [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' }
|
||||
key: 'p', style: { margin: 0, fontSize: '15px', lineHeight: 1.6, color: 'var(--sb-text-6)', maxWidth: '380px' }
|
||||
}, s.desc)
|
||||
]),
|
||||
React.createElement('div', { key: 'tags', style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } }, s.tags.map(tag))
|
||||
@@ -145,19 +148,19 @@ const UniqueFeatureSlider = () => {
|
||||
key: 'col',
|
||||
style: { display: 'flex', alignItems: 'center', gap: '16px', padding: '20px 22px' }
|
||||
}, [
|
||||
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('span', { key: 'n', style: { fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: 'var(--sb-text-faint)' } }, s.num),
|
||||
React.createElement('span', { key: 'l', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.2px', color: 'var(--sb-text-4)' } }, 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: 'n', style: { fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: 'var(--sb-text-faint)' } }, 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' }
|
||||
style: { writingMode: 'vertical-rl', transform: 'rotate(180deg)', fontSize: '17px', fontWeight: 800, letterSpacing: '-0.2px', color: 'var(--sb-text-4)', whiteSpace: 'nowrap' }
|
||||
}, s.collapsed),
|
||||
svg(s.icon, 22, '#56565e', 1.8)
|
||||
svg(s.icon, 22, 'var(--sb-text-faint)', 1.8)
|
||||
]);
|
||||
|
||||
const panels = slides.map((s, i) => {
|
||||
@@ -178,7 +181,7 @@ const UniqueFeatureSlider = () => {
|
||||
cursor: 'pointer',
|
||||
background: isActive ? ACTIVE_BG : IDLE_BG,
|
||||
border: '1px solid ' + (isActive ? ACTIVE_BD : IDLE_BD),
|
||||
color: '#8a8a92',
|
||||
color: 'var(--sb-text-7)',
|
||||
transition: 'flex .46s cubic-bezier(.2,.7,.3,1), background .3s ease, border-color .3s ease, filter .2s ease'
|
||||
}
|
||||
}, isActive ? expandedContent(s) : collapsedContent(s));
|
||||
@@ -199,11 +202,11 @@ const UniqueFeatureSlider = () => {
|
||||
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' }
|
||||
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: 'var(--sb-text-9)', textTransform: 'uppercase', letterSpacing: '1.4px', marginBottom: '12px' }
|
||||
}, t('unique.eyebrow')),
|
||||
React.createElement('h2', {
|
||||
key: 'h2',
|
||||
style: { margin: 0, fontSize: isMobile ? '28px' : '38px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.05, color: '#f4f4f6' }
|
||||
style: { margin: 0, fontSize: isMobile ? '28px' : '38px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.05, color: 'var(--sb-text-1)' }
|
||||
}, t('unique.heading'))
|
||||
]),
|
||||
React.createElement('div', { key: 'nav', style: { display: 'flex', alignItems: 'center', gap: '10px', flex: 'none' } }, [
|
||||
@@ -227,9 +230,9 @@ const UniqueFeatureSlider = () => {
|
||||
// Full-bleed dark band with the radial accent glow — matches the design mockup.
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
width: '100%', color: '#e8e8eb', fontFamily: SANS,
|
||||
width: '100%', color: 'var(--sb-text-2)', 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'
|
||||
background: 'radial-gradient(1100px 700px at 18% 8%, rgba(var(--sb-orange-rgb), 0.05), transparent 60%), var(--sb-bg)'
|
||||
}
|
||||
}, [
|
||||
React.createElement('style', { key: 'kf', dangerouslySetInnerHTML: { __html: '@keyframes wuUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}' } }),
|
||||
|
||||
Reference in New Issue
Block a user