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:
lockbitchat
2026-09-04 17:38:41 -04:00
parent 1c153b290a
commit 9e63cf65a4
109 changed files with 4320 additions and 1910 deletions
+23 -23
View File
@@ -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"/>' } }))
])
])
+22 -20
View File
@@ -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)}}' } }),
+43 -43
View File
@@ -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' } }, [
+18 -16
View File
@@ -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'
}
}, [
+29 -29
View File
@@ -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)')
])
)
])
+49 -49
View File
@@ -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}`)),
+29 -29
View File
@@ -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')),
+31 -22
View File
@@ -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'))
+40 -37
View File
@@ -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)
])
]);
};
+7 -7
View File
@@ -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)',
+8 -8
View File
@@ -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),
])));
+59 -41
View File
@@ -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>
+157
View File
@@ -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 };
+30 -27
View File
@@ -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)}}' } }),