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:
+374
-359
File diff suppressed because it is too large
Load Diff
@@ -149,7 +149,7 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
|
||||
key: 'update-modal',
|
||||
style: {
|
||||
position: 'fixed', inset: 0, zIndex: 9999, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: '24px', background: 'rgba(8,8,10,0.55)', backdropFilter: 'blur(3px)', WebkitBackdropFilter: 'blur(3px)',
|
||||
padding: '24px', background: 'rgba(var(--sb-scrim-rgb), 0.55)', backdropFilter: 'blur(3px)', WebkitBackdropFilter: 'blur(3px)',
|
||||
animation: 'unFade .3s ease', fontFamily: SANS
|
||||
}
|
||||
}, [
|
||||
@@ -163,39 +163,39 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
|
||||
key: 'card',
|
||||
style: {
|
||||
position: 'relative', width: '440px', maxWidth: 'calc(100vw - 48px)', borderRadius: '22px',
|
||||
background: '#121214', border: '1px solid rgba(255,255,255,0.08)', padding: '36px 32px 28px',
|
||||
textAlign: 'center', boxShadow: '0 30px 70px rgba(0,0,0,0.6)', animation: 'unPop .32s cubic-bezier(.2,.7,.3,1)'
|
||||
background: 'var(--sb-bg)', border: '1px solid rgba(var(--sb-ink), 0.08)', padding: '36px 32px 28px',
|
||||
textAlign: 'center', boxShadow: '0 30px 70px rgba(var(--sb-shadow-rgb), calc(0.6 * var(--sb-shadow-k)))', animation: 'unPop .32s cubic-bezier(.2,.7,.3,1)'
|
||||
}
|
||||
}, [
|
||||
// spinning update icon
|
||||
React.createElement('div', {
|
||||
key: 'icon',
|
||||
style: { display: 'inline-flex', width: '64px', height: '64px', borderRadius: '50%', alignItems: 'center', justifyContent: 'center', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.3)', marginBottom: '20px' }
|
||||
style: { display: 'inline-flex', width: '64px', height: '64px', borderRadius: '50%', alignItems: 'center', justifyContent: 'center', background: 'rgba(var(--sb-orange-rgb), 0.12)', border: '1px solid rgba(var(--sb-orange-rgb), 0.3)', marginBottom: '20px' }
|
||||
}, React.createElement('svg', {
|
||||
width: 28, height: 28, viewBox: '0 0 24 24', fill: 'none', stroke: '#f0892a', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
width: 28, height: 28, viewBox: '0 0 24 24', fill: 'none', stroke: 'var(--sb-orange-solid)', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
style: { animation: 'unSpin 6s linear infinite' },
|
||||
dangerouslySetInnerHTML: { __html: '<path d="M21 8a8.5 8.5 0 0 0-15.6-2.5M3 4v4h4"/><path d="M3 16a8.5 8.5 0 0 0 15.6 2.5M21 20v-4h-4"/>' }
|
||||
})),
|
||||
|
||||
React.createElement('h2', { key: 'title', style: { margin: '0 0 9px', fontSize: '26px', fontWeight: 800, letterSpacing: '-0.7px', color: '#f4f4f6' } }, t('update.title')),
|
||||
React.createElement('p', { key: 'sub', style: { margin: '0 0 24px', fontSize: '14.5px', lineHeight: 1.55, color: '#9a9aa2' } }, t('update.desc')),
|
||||
React.createElement('h2', { key: 'title', style: { margin: '0 0 9px', fontSize: '26px', fontWeight: 800, letterSpacing: '-0.7px', color: 'var(--sb-text-1)' } }, t('update.title')),
|
||||
React.createElement('p', { key: 'sub', style: { margin: '0 0 24px', fontSize: '14.5px', lineHeight: 1.55, color: 'var(--sb-text-6)' } }, t('update.desc')),
|
||||
|
||||
// version comparison
|
||||
React.createElement('div', {
|
||||
key: 'vbox',
|
||||
style: { borderRadius: '14px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', padding: '16px 18px', marginBottom: '24px', textAlign: 'start' }
|
||||
style: { borderRadius: '14px', background: 'var(--sb-bg-deep)', border: '1px solid rgba(var(--sb-ink), 0.06)', padding: '16px 18px', marginBottom: '24px', textAlign: 'start' }
|
||||
}, [
|
||||
React.createElement('div', { key: 'cur', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '14px', padding: '5px 0' } }, [
|
||||
React.createElement('span', { key: 'l', style: { fontSize: '13.5px', fontWeight: 500, color: '#8a8a92' } }, t('update.currentVersion')),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 500, color: '#9a9aa2', whiteSpace: 'nowrap' } }, formatVersion(updateState.currentVersion))
|
||||
React.createElement('span', { key: 'l', style: { fontSize: '13.5px', fontWeight: 500, color: 'var(--sb-text-7)' } }, t('update.currentVersion')),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 500, color: 'var(--sb-text-6)', whiteSpace: 'nowrap' } }, formatVersion(updateState.currentVersion))
|
||||
]),
|
||||
React.createElement('div', { key: 'sep', style: { height: '1px', background: 'rgba(255,255,255,0.05)', margin: '4px 0' } }),
|
||||
React.createElement('div', { key: 'sep', style: { height: '1px', background: 'rgba(var(--sb-ink), 0.05)', margin: '4px 0' } }),
|
||||
React.createElement('div', { key: 'new', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '14px', padding: '5px 0' } }, [
|
||||
React.createElement('span', { key: 'l', style: { display: 'inline-flex', alignItems: 'center', gap: '8px', fontSize: '13.5px', fontWeight: 600, color: '#e8e8eb' } }, [
|
||||
React.createElement('span', { key: 'd', style: { width: '6px', height: '6px', borderRadius: '50%', background: '#f0892a' } }),
|
||||
React.createElement('span', { key: 'l', style: { display: 'inline-flex', alignItems: 'center', gap: '8px', fontSize: '13.5px', fontWeight: 600, color: 'var(--sb-text-2)' } }, [
|
||||
React.createElement('span', { key: 'd', style: { width: '6px', height: '6px', borderRadius: '50%', background: 'var(--sb-orange-solid)' } }),
|
||||
t('update.newVersion')
|
||||
]),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 700, color: '#f0892a', whiteSpace: 'nowrap' } }, formatVersion(updateState.newVersion))
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 700, color: 'var(--sb-orange)', whiteSpace: 'nowrap' } }, formatVersion(updateState.newVersion))
|
||||
])
|
||||
]),
|
||||
|
||||
@@ -204,17 +204,17 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
|
||||
? React.createElement('div', { key: 'progress' }, [
|
||||
React.createElement('div', {
|
||||
key: 'bar',
|
||||
style: { width: '100%', height: '8px', borderRadius: '99px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden', marginBottom: '10px' }
|
||||
}, React.createElement('div', { key: 'fill', style: { height: '100%', width: `${updateState.progress}%`, background: 'linear-gradient(90deg,#3ecf8e,#f0892a)', transition: 'width .3s ease' } })),
|
||||
React.createElement('p', { key: 't', style: { margin: 0, fontFamily: MONO, fontSize: '12px', color: '#8a8a92' } }, `Updating… ${updateState.progress}%`)
|
||||
style: { width: '100%', height: '8px', borderRadius: '99px', background: 'var(--sb-bg-deep)', border: '1px solid rgba(var(--sb-ink), 0.06)', overflow: 'hidden', marginBottom: '10px' }
|
||||
}, React.createElement('div', { key: 'fill', style: { height: '100%', width: `${updateState.progress}%`, background: 'linear-gradient(90deg,var(--sb-green),var(--sb-orange))', transition: 'width .3s ease' } })),
|
||||
React.createElement('p', { key: 't', style: { margin: 0, fontFamily: MONO, fontSize: '12px', color: 'var(--sb-text-7)' } }, `Updating… ${updateState.progress}%`)
|
||||
])
|
||||
: React.createElement('div', { key: 'actions', style: { display: 'flex', alignItems: 'center', gap: '12px' } }, [
|
||||
React.createElement('button', {
|
||||
key: 'update',
|
||||
onClick: handleForceUpdate,
|
||||
style: { flex: 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '10px', padding: '15px 20px', borderRadius: '13px', border: 'none', background: '#f0892a', color: '#1a0f04', fontFamily: 'inherit', fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px', cursor: 'pointer', boxShadow: '0 8px 24px rgba(240,137,42,0.28)', transition: 'all .2s cubic-bezier(.2,.7,.3,1)' },
|
||||
onMouseEnter: (e) => { e.currentTarget.style.background = '#ff9637'; e.currentTarget.style.transform = 'translateY(-2px)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.background = '#f0892a'; e.currentTarget.style.transform = 'none'; }
|
||||
style: { flex: 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '10px', padding: '15px 20px', borderRadius: '13px', border: 'none', background: 'var(--sb-orange-solid)', color: 'var(--sb-on-accent)', fontFamily: 'inherit', fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px', cursor: 'pointer', boxShadow: '0 8px 24px rgba(var(--sb-orange-rgb), 0.28)', transition: 'all .2s cubic-bezier(.2,.7,.3,1)' },
|
||||
onMouseEnter: (e) => { e.currentTarget.style.background = 'var(--sb-orange-hi)'; e.currentTarget.style.transform = 'translateY(-2px)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.background = 'var(--sb-orange)'; e.currentTarget.style.transform = 'none'; }
|
||||
}, [
|
||||
React.createElement('svg', { key: 'i', width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.1, strokeLinecap: 'round', strokeLinejoin: 'round', dangerouslySetInnerHTML: { __html: '<path d="M12 3v11"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 20h14"/>' } }),
|
||||
t('update.now')
|
||||
@@ -223,9 +223,9 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
|
||||
key: 'later',
|
||||
onClick: handleCloseModal,
|
||||
title: t('update.later'),
|
||||
style: { flex: 'none', width: '50px', height: '50px', borderRadius: '13px', display: 'grid', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.025)', color: '#9a9aa2', cursor: 'pointer', transition: 'all .18s cubic-bezier(.2,.7,.3,1)' },
|
||||
onMouseEnter: (e) => { e.currentTarget.style.color = '#e5727a'; e.currentTarget.style.borderColor = 'rgba(229,114,122,0.4)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.color = '#9a9aa2'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'; }
|
||||
style: { flex: 'none', width: '50px', height: '50px', borderRadius: '13px', display: 'grid', placeItems: 'center', border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'rgba(var(--sb-ink), 0.025)', color: 'var(--sb-text-6)', cursor: 'pointer', transition: 'all .18s cubic-bezier(.2,.7,.3,1)' },
|
||||
onMouseEnter: (e) => { e.currentTarget.style.color = 'var(--sb-red)'; e.currentTarget.style.borderColor = 'rgba(var(--sb-red-rgb), 0.4)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.color = 'var(--sb-text-6)'; e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.1)'; }
|
||||
}, React.createElement('svg', { width: 17, height: 17, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.1, strokeLinecap: 'round', strokeLinejoin: 'round', dangerouslySetInnerHTML: { __html: '<path d="M6 6l12 12M18 6L6 18"/>' } }))
|
||||
])
|
||||
])
|
||||
|
||||
@@ -18,7 +18,9 @@ const BecomePartner = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ACCENT = '#f0892a';
|
||||
const ACCENT = 'var(--sb-orange)';
|
||||
// Fill form: brand colour in both themes. See app.jsx.
|
||||
const ACCENT_SOLID = 'var(--sb-orange-solid)';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
@@ -60,7 +62,7 @@ const BecomePartner = () => {
|
||||
|
||||
const roleTag = (role) => React.createElement('span', {
|
||||
key: 'role',
|
||||
style: { fontFamily: MONO, fontSize: '10.5px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.2px', padding: '6px 11px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.07)', background: 'rgba(255,255,255,0.025)', whiteSpace: 'nowrap' }
|
||||
style: { fontFamily: MONO, fontSize: '10.5px', fontWeight: 600, color: 'var(--sb-text-9)', textTransform: 'uppercase', letterSpacing: '1.2px', padding: '6px 11px', borderRadius: '8px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'rgba(var(--sb-ink), 0.025)', whiteSpace: 'nowrap' }
|
||||
}, role);
|
||||
|
||||
const partnerCard = (p) => React.createElement('a', {
|
||||
@@ -70,14 +72,14 @@ const BecomePartner = () => {
|
||||
rel: 'noopener noreferrer',
|
||||
style: {
|
||||
flex: '1 1 320px', minWidth: isMobile ? 'auto' : '300px',
|
||||
borderRadius: '18px', background: '#141416', border: '1px solid rgba(255,255,255,0.06)',
|
||||
borderRadius: '18px', background: 'var(--sb-surface)', border: '1px solid rgba(var(--sb-ink), 0.06)',
|
||||
padding: '30px 30px 26px', display: 'flex', flexDirection: 'column',
|
||||
textDecoration: 'none', color: 'inherit',
|
||||
transition: 'transform .28s cubic-bezier(.2,.7,.3,1), border-color .28s cubic-bezier(.2,.7,.3,1)',
|
||||
animation: `ptUp ${p.delay} cubic-bezier(.2,.7,.3,1)`
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.transform = 'translateY(-4px)'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.13)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.06)'; }
|
||||
onMouseEnter: (e) => { e.currentTarget.style.transform = 'translateY(-4px)'; e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.13)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.06)'; }
|
||||
}, [
|
||||
React.createElement('div', { key: 'logo', style: { display: 'flex', alignItems: 'center', marginBottom: '30px', height: '54px' } },
|
||||
React.createElement('img', {
|
||||
@@ -90,8 +92,8 @@ const BecomePartner = () => {
|
||||
style: { height: p.logoHeight, width: 'auto', maxWidth: '190px', objectFit: 'contain', display: 'block' }
|
||||
})
|
||||
),
|
||||
React.createElement('h3', { key: 'name', style: { margin: '0 0 9px', fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' } }, p.name),
|
||||
React.createElement('p', { key: 'desc', style: { margin: '0 0 22px', fontSize: '14.5px', lineHeight: 1.6, color: '#9a9aa2' } }, p.desc),
|
||||
React.createElement('h3', { key: 'name', style: { margin: '0 0 9px', fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: 'var(--sb-text-1)' } }, p.name),
|
||||
React.createElement('p', { key: 'desc', style: { margin: '0 0 22px', fontSize: '14.5px', lineHeight: 1.6, color: 'var(--sb-text-6)' } }, p.desc),
|
||||
React.createElement('div', { key: 'foot', style: { marginTop: 'auto', paddingTop: '6px', display: 'flex', alignItems: 'center', gap: '12px' } }, [
|
||||
roleTag(p.role)
|
||||
])
|
||||
@@ -104,30 +106,30 @@ const BecomePartner = () => {
|
||||
rel: 'noopener noreferrer',
|
||||
style: {
|
||||
flex: '1 1 320px', minWidth: isMobile ? 'auto' : '300px',
|
||||
borderRadius: '18px', background: '#111113', border: '1px dashed rgba(255,255,255,0.12)',
|
||||
borderRadius: '18px', background: 'var(--sb-bg)', border: '1px dashed rgba(var(--sb-ink), 0.12)',
|
||||
padding: '30px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
|
||||
textDecoration: 'none', color: 'inherit',
|
||||
transition: 'border-color .28s cubic-bezier(.2,.7,.3,1)',
|
||||
animation: 'ptUp .62s cubic-bezier(.2,.7,.3,1)'
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.borderColor = 'rgba(240,137,42,0.4)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.12)'; }
|
||||
onMouseEnter: (e) => { e.currentTarget.style.borderColor = 'rgba(var(--sb-orange-rgb), 0.4)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.12)'; }
|
||||
}, [
|
||||
React.createElement('div', { key: 'top' }, [
|
||||
React.createElement('div', {
|
||||
key: 'icon',
|
||||
style: { width: '48px', height: '48px', borderRadius: '13px', display: 'grid', placeItems: 'center', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.28)', marginBottom: '24px' }
|
||||
}, svg('<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/>', 23, ACCENT, 1.9)),
|
||||
React.createElement('h3', { key: 'title', style: { margin: '0 0 8px', fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' } }, t('partners.inviteTitle')),
|
||||
React.createElement('p', { key: 'desc', style: { margin: 0, fontSize: '14.5px', lineHeight: 1.6, color: '#8a8a92' } }, t('partners.inviteDesc'))
|
||||
style: { width: '48px', height: '48px', borderRadius: '13px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-orange-rgb), 0.12)', border: '1px solid rgba(var(--sb-orange-rgb), 0.28)', marginBottom: '24px' }
|
||||
}, svg('<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M19 8v6M22 11h-6"/>', 23, ACCENT_SOLID, 1.9)),
|
||||
React.createElement('h3', { key: 'title', style: { margin: '0 0 8px', fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: 'var(--sb-text-1)' } }, t('partners.inviteTitle')),
|
||||
React.createElement('p', { key: 'desc', style: { margin: 0, fontSize: '14.5px', lineHeight: 1.6, color: 'var(--sb-text-7)' } }, t('partners.inviteDesc'))
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'btn',
|
||||
style: {
|
||||
marginTop: '26px', width: '100%', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '10px',
|
||||
padding: '15px 20px', borderRadius: '12px', border: 'none', background: ACCENT, color: '#1a0f04',
|
||||
padding: '15px 20px', borderRadius: '12px', border: 'none', background: ACCENT_SOLID, color: 'var(--sb-on-accent)',
|
||||
fontFamily: SANS, fontSize: '15px', fontWeight: 700, cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(240,137,42,0.28)', boxSizing: 'border-box',
|
||||
boxShadow: '0 8px 24px rgba(var(--sb-orange-rgb), 0.28)', boxSizing: 'border-box',
|
||||
transition: 'background .2s cubic-bezier(.2,.7,.3,1), transform .2s cubic-bezier(.2,.7,.3,1)'
|
||||
}
|
||||
}, [
|
||||
@@ -144,13 +146,13 @@ const BecomePartner = () => {
|
||||
React.createElement('div', { key: 'head', style: { marginBottom: '44px' } }, [
|
||||
React.createElement('div', {
|
||||
key: 'eyebrow',
|
||||
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.6px', marginBottom: '14px' }
|
||||
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: 'var(--sb-text-9)', textTransform: 'uppercase', letterSpacing: '1.6px', marginBottom: '14px' }
|
||||
}, t('partners.eyebrow')),
|
||||
// The heading used to share a flex row with a strapline; with that gone the
|
||||
// row had one child and nothing left to arrange.
|
||||
React.createElement('h2', {
|
||||
key: 'h2',
|
||||
style: { margin: 0, fontSize: isMobile ? '30px' : '40px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.04, color: '#f4f4f6' }
|
||||
style: { margin: 0, fontSize: isMobile ? '30px' : '40px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.04, color: 'var(--sb-text-1)' }
|
||||
}, t('partners.heading'))
|
||||
]),
|
||||
|
||||
@@ -166,9 +168,9 @@ const BecomePartner = () => {
|
||||
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
width: '100%', color: '#e8e8eb', fontFamily: SANS,
|
||||
width: '100%', color: 'var(--sb-text-2)', fontFamily: SANS,
|
||||
padding: isMobile ? '48px 0' : '72px 0',
|
||||
background: 'radial-gradient(1100px 640px at 50% -6%, rgba(240,137,42,0.055), transparent 62%), #0f0f11'
|
||||
background: 'radial-gradient(1100px 640px at 50% -6%, rgba(var(--sb-orange-rgb), 0.055), transparent 62%), var(--sb-bg)'
|
||||
}
|
||||
}, [
|
||||
React.createElement('style', { key: 'kf', dangerouslySetInnerHTML: { __html: '@keyframes ptUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}' } }),
|
||||
|
||||
@@ -103,39 +103,39 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
|
||||
// ── shared styles (from the design) ──────────────────────────────────────
|
||||
const ctrlBase = {
|
||||
width: '56px', height: '56px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.05)',
|
||||
color: '#cfcfd4', cursor: 'pointer', transition: 'all .15s'
|
||||
border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'rgba(var(--sb-ink), 0.05)',
|
||||
color: 'var(--sb-text-4)', cursor: 'pointer', transition: 'all .15s'
|
||||
};
|
||||
const dangerCtrl = { ...ctrlBase, background: '#e5484d', color: '#fff', border: '1px solid transparent' };
|
||||
const dangerCtrl = { ...ctrlBase, background: 'var(--sb-red-strong-solid)', color: '#fff', border: '1px solid transparent' };
|
||||
const endBtn = {
|
||||
width: '56px', height: '56px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
border: 'none', background: '#e5484d', color: '#fff', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(229,72,77,0.35)', transition: 'transform .15s'
|
||||
border: 'none', background: 'var(--sb-red-strong-solid)', color: '#fff', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(var(--sb-red-strong-rgb), 0.35)', transition: 'transform .15s'
|
||||
};
|
||||
const minimizeBtn = (light) => ({
|
||||
width: '36px', height: '36px', borderRadius: '9px', display: 'grid', placeItems: 'center',
|
||||
border: '1px solid rgba(255,255,255,' + (light ? '0.15' : '0.1') + ')',
|
||||
background: light ? 'rgba(0,0,0,0.35)' : 'rgba(255,255,255,0.04)',
|
||||
color: light ? '#fff' : '#cfcfd4', cursor: 'pointer', transition: 'all .15s'
|
||||
border: '1px solid rgba(var(--sb-ink), ' + (light ? '0.15' : '0.1') + ')',
|
||||
background: light ? 'rgba(0,0,0,0.35)' : 'rgba(var(--sb-ink), 0.04)',
|
||||
color: light ? '#fff' : 'var(--sb-text-4)', cursor: 'pointer', transition: 'all .15s'
|
||||
});
|
||||
|
||||
const encBadge = h('span', { key: 'enc', style: { display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '11px', fontWeight: 600, color: '#3ecf8e' } },
|
||||
const encBadge = h('span', { key: 'enc', style: { display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '11px', fontWeight: 600, color: 'var(--sb-green)' } },
|
||||
[svg(ICON.lock, 11, 2), t('call.encryptedShort')]);
|
||||
|
||||
// Connection-quality indicator — driven by the adaptation controller's
|
||||
// getStats (loss + RTT). Signal bars + label; hidden until there's data.
|
||||
const QUALITY = {
|
||||
excellent: { bars: 4, color: '#3ecf8e', label: t('call.qualityExcellent') },
|
||||
good: { bars: 3, color: '#3ecf8e', label: t('call.qualityGood') },
|
||||
fair: { bars: 2, color: '#e3c84e', label: t('call.qualityFair') },
|
||||
poor: { bars: 1, color: '#e5727a', label: t('call.qualityWeak') },
|
||||
excellent: { bars: 4, color: 'var(--sb-green)', label: t('call.qualityExcellent') },
|
||||
good: { bars: 3, color: 'var(--sb-green)', label: t('call.qualityGood') },
|
||||
fair: { bars: 2, color: 'var(--sb-yellow)', label: t('call.qualityFair') },
|
||||
poor: { bars: 1, color: 'var(--sb-red)', label: t('call.qualityWeak') },
|
||||
};
|
||||
const qualityIndicator = (compact) => {
|
||||
const q = QUALITY[call.quality];
|
||||
if (!q) return null;
|
||||
const bars = h('span', { key: 'bars', style: { display: 'inline-flex', alignItems: 'flex-end', gap: '2px', height: '14px' } },
|
||||
[0, 1, 2, 3].map(i => h('span', {
|
||||
key: i, style: { width: '3px', height: (5 + i * 3) + 'px', borderRadius: '1px', background: i < q.bars ? q.color : 'rgba(255,255,255,0.18)' }
|
||||
key: i, style: { width: '3px', height: (5 + i * 3) + 'px', borderRadius: '1px', background: i < q.bars ? q.color : 'rgba(var(--sb-ink), 0.18)' }
|
||||
})));
|
||||
if (compact) return bars;
|
||||
return h('span', { key: 'q', title: t('call.quality'), style: { display: 'inline-flex', alignItems: 'center', gap: '6px', fontSize: '11.5px', fontWeight: 600, color: q.color } }, [bars, q.label]);
|
||||
@@ -153,73 +153,73 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
|
||||
const hiddenAudio = h('audio', { key: 'ra', ref: remoteAudioRef, autoPlay: true, playsInline: true, style: { display: 'none' } });
|
||||
|
||||
const labeled = (key, btn, label) => h('div', { key, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' } },
|
||||
[btn, h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: '#8a8a92' } }, label)]);
|
||||
[btn, h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: 'var(--sb-text-7)' } }, label)]);
|
||||
|
||||
// Avatar disc with optional ringing pulse (voice + incoming).
|
||||
const avatarDisc = (size, ring) => h('div', { key: 'av', style: { position: 'relative', width: '120px', height: '120px', marginBottom: '28px', display: 'grid', placeItems: 'center' } }, [
|
||||
ring && h('span', { key: 'p1', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(240,137,42,0.5)', animation: 'sbCallPulse 2s ease-out infinite' } }),
|
||||
ring && h('span', { key: 'p2', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(240,137,42,0.4)', animation: 'sbCallPulse 2s ease-out infinite', animationDelay: '1s' } }),
|
||||
h('div', { key: 'c', style: { width: '104px', height: '104px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'radial-gradient(circle at 35% 30%, #2a2a30, #161618)', border: '1px solid rgba(255,255,255,0.1)', boxShadow: '0 12px 30px rgba(0,0,0,0.4)', color: '#8a8a92' } }, svg(ICON.user, size, 1.6))
|
||||
ring && h('span', { key: 'p1', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(var(--sb-orange-rgb), 0.5)', animation: 'sbCallPulse 2s ease-out infinite' } }),
|
||||
ring && h('span', { key: 'p2', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(var(--sb-orange-rgb), 0.4)', animation: 'sbCallPulse 2s ease-out infinite', animationDelay: '1s' } }),
|
||||
h('div', { key: 'c', style: { width: '104px', height: '104px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'radial-gradient(circle at 35% 30%, var(--sb-surface-4), var(--sb-surface))', border: '1px solid rgba(var(--sb-ink), 0.1)', boxShadow: '0 12px 30px rgba(var(--sb-shadow-rgb), calc(0.4 * var(--sb-shadow-k)))', color: 'var(--sb-text-7)' } }, svg(ICON.user, size, 1.6))
|
||||
]);
|
||||
|
||||
// ── INCOMING (ringing) prompt ────────────────────────────────────────────
|
||||
if (phase === 'incoming') {
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column', background: 'radial-gradient(680px 460px at 50% 36%, rgba(240,137,42,0.08), transparent 70%), #0d0d0f', animation: 'sbExpand .2s ease' } }, [
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column', background: 'radial-gradient(680px 460px at 50% 36%, rgba(var(--sb-orange-rgb), 0.08), transparent 70%), var(--sb-bg-deep)', animation: 'sbExpand .2s ease' } }, [
|
||||
hiddenAudio,
|
||||
h('div', { key: 'top', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', padding: '16px 18px' } },
|
||||
h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: '#3ecf8e' } }, [svg(ICON.lock, 13, 2), t('call.encrypted')])),
|
||||
h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: 'var(--sb-green)' } }, [svg(ICON.lock, 13, 2), t('call.encrypted')])),
|
||||
h('div', { key: 'mid', style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' } }, [
|
||||
avatarDisc(46, true),
|
||||
h('div', { key: 'nm', style: { fontSize: '24px', fontWeight: 800, letterSpacing: '-0.5px', color: '#f4f4f6' } }, name),
|
||||
h('div', { key: 'st', style: { fontFamily: MONO, fontSize: '14px', fontWeight: 500, color: '#9a9aa2', marginTop: '8px' } }, call.withVideo ? t('call.incomingVideo') : t('call.incoming'))
|
||||
h('div', { key: 'nm', style: { fontSize: '24px', fontWeight: 800, letterSpacing: '-0.5px', color: 'var(--sb-text-1)' } }, name),
|
||||
h('div', { key: 'st', style: { fontFamily: MONO, fontSize: '14px', fontWeight: 500, color: 'var(--sb-text-6)', marginTop: '8px' } }, call.withVideo ? t('call.incomingVideo') : t('call.incoming'))
|
||||
]),
|
||||
h('div', { key: 'ctrls', style: { flex: 'none', display: 'flex', alignItems: 'flex-start', justifyContent: 'center', gap: '48px', padding: '28px 24px 40px' } }, [
|
||||
labeled('dec', h('button', { onClick: doDecline, title: t('call.decline'), style: { ...endBtn, width: '62px', height: '62px' } }, svg(ICON.phoneHangup, 24, 1.9)), t('call.decline')),
|
||||
labeled('acc', h('button', { onClick: doAccept, title: t('call.accept'), style: { width: '62px', height: '62px', borderRadius: '50%', display: 'grid', placeItems: 'center', border: 'none', background: '#3ecf8e', color: '#06231a', cursor: 'pointer', boxShadow: '0 8px 24px rgba(62,207,142,0.35)' } }, svg(ICON.phone, 24, 1.9)), t('call.accept'))
|
||||
labeled('acc', h('button', { onClick: doAccept, title: t('call.accept'), style: { width: '62px', height: '62px', borderRadius: '50%', display: 'grid', placeItems: 'center', border: 'none', background: 'var(--sb-green-solid)', color: 'var(--sb-on-green)', cursor: 'pointer', boxShadow: '0 8px 24px rgba(var(--sb-green-rgb), 0.35)' } }, svg(ICON.phone, 24, 1.9)), t('call.accept'))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// ── MINIMIZED widget ─────────────────────────────────────────────────────
|
||||
if (minimized) {
|
||||
return h('div', { style: { position: 'absolute', bottom: '18px', insetInlineEnd: '18px', zIndex: 40, width: '236px', borderRadius: '14px', overflow: 'hidden', background: '#161618', border: '1px solid rgba(255,255,255,0.1)', boxShadow: '0 18px 44px rgba(0,0,0,0.55)', animation: 'sbExpand .18s ease' } }, [
|
||||
return h('div', { style: { position: 'absolute', bottom: '18px', insetInlineEnd: '18px', zIndex: 40, width: '236px', borderRadius: '14px', overflow: 'hidden', background: 'var(--sb-surface)', border: '1px solid rgba(var(--sb-ink), 0.1)', boxShadow: '0 18px 44px rgba(var(--sb-shadow-rgb), calc(0.55 * var(--sb-shadow-k)))', animation: 'sbExpand .18s ease' } }, [
|
||||
hiddenAudio,
|
||||
isVideo && h('div', { key: 'v', style: { position: 'relative', height: '132px', background: '#111' } }, [
|
||||
h('video', { key: 'rv', ref: remoteVideoRef, autoPlay: true, muted: true, playsInline: true, style: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' } }),
|
||||
!call.remoteHasVideo && h('div', { key: 'off', style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', background: 'linear-gradient(120deg,#15151b,#1d1a24)', color: '#6b6b73' } }, svg(ICON.camOff, 22, 1.8)),
|
||||
!call.remoteHasVideo && h('div', { key: 'off', style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', background: 'linear-gradient(120deg,var(--sb-grad-1),var(--sb-grad-2))', color: 'var(--sb-text-9)' } }, svg(ICON.camOff, 22, 1.8)),
|
||||
h('span', { key: 's', style: { position: 'absolute', top: '8px', insetInlineStart: '9px', fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#fff', padding: '3px 7px', borderRadius: '6px', background: 'rgba(0,0,0,0.5)' } }, callStatus)
|
||||
]),
|
||||
h('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: '11px', padding: '11px 12px' } }, [
|
||||
h('span', { key: 'ic', style: { position: 'relative', flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: 'rgba(62,207,142,0.1)', border: '1px solid rgba(62,207,142,0.25)', color: '#3ecf8e' } }, svg(ICON.user, 16, 1.9)),
|
||||
h('span', { key: 'ic', style: { position: 'relative', flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-green-rgb), 0.1)', border: '1px solid rgba(var(--sb-green-rgb), 0.25)', color: 'var(--sb-green)' } }, svg(ICON.user, 16, 1.9)),
|
||||
h('div', { key: 'tx', style: { flex: 1, minWidth: 0 } }, [
|
||||
h('div', { key: 'n', style: { fontSize: '13px', fontWeight: 700, color: '#f4f4f6', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, name),
|
||||
h('div', { key: 's', style: { display: 'flex', alignItems: 'center', gap: '7px', fontFamily: MONO, fontSize: '11px', color: '#9a9aa2' } }, [
|
||||
h('div', { key: 'n', style: { fontSize: '13px', fontWeight: 700, color: 'var(--sb-text-1)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, name),
|
||||
h('div', { key: 's', style: { display: 'flex', alignItems: 'center', gap: '7px', fontFamily: MONO, fontSize: '11px', color: 'var(--sb-text-6)' } }, [
|
||||
(isVideo ? t('call.videoPrefix') : t('call.voicePrefix')) + callStatus,
|
||||
phase === 'active' && qualityIndicator(true)
|
||||
])
|
||||
]),
|
||||
h('button', { key: 'exp', onClick: () => setMinimized(false), title: t('call.expand'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'rgba(255,255,255,0.05)', color: '#cfcfd4', cursor: 'pointer', transition: 'all .15s' } }, svg(ICON.expand, 15, 2)),
|
||||
h('button', { key: 'end', onClick: doEnd, title: t('call.end'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: '#e5484d', color: '#fff', cursor: 'pointer', transition: 'transform .15s' } }, svg(ICON.phoneHangup, 15, 2))
|
||||
h('button', { key: 'exp', onClick: () => setMinimized(false), title: t('call.expand'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'rgba(var(--sb-ink), 0.05)', color: 'var(--sb-text-4)', cursor: 'pointer', transition: 'all .15s' } }, svg(ICON.expand, 15, 2)),
|
||||
h('button', { key: 'end', onClick: doEnd, title: t('call.end'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'var(--sb-red-strong-solid)', color: '#fff', cursor: 'pointer', transition: 'transform .15s' } }, svg(ICON.phoneHangup, 15, 2))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// ── VIDEO call (expanded) ────────────────────────────────────────────────
|
||||
if (isVideo) {
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, overflow: 'hidden', background: '#0a0a0c', animation: 'sbExpand .2s ease' } }, [
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, overflow: 'hidden', background: 'var(--sb-bg-deepest)', animation: 'sbExpand .2s ease' } }, [
|
||||
hiddenAudio,
|
||||
call.remoteHasVideo
|
||||
? h('video', { key: 'rv', ref: remoteVideoRef, autoPlay: true, muted: true, playsInline: true, style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', background: '#0a0a0c' } })
|
||||
: h('div', { key: 'ph', style: { position: 'absolute', inset: 0, background: 'linear-gradient(120deg, #15151b, #1d1a24, #161620)', backgroundSize: '200% 200%', animation: 'sbLiveBg 9s ease-in-out infinite', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '18px' } }, [
|
||||
h('div', { key: 'a', style: { width: '120px', height: '120px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'radial-gradient(circle at 35% 30%, #2a2a30, #161618)', border: '1px solid rgba(255,255,255,0.1)', color: '#9a9aa2' } }, svg(ICON.user, 54, 1.5)),
|
||||
h('div', { key: 't', style: { fontSize: '15px', fontWeight: 600, color: '#8a8a92' } }, t('call.peerCameraOff'))
|
||||
? h('video', { key: 'rv', ref: remoteVideoRef, autoPlay: true, muted: true, playsInline: true, style: { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', background: 'var(--sb-bg-deepest)' } })
|
||||
: h('div', { key: 'ph', style: { position: 'absolute', inset: 0, background: 'linear-gradient(120deg, var(--sb-grad-1), var(--sb-grad-2), var(--sb-grad-3))', backgroundSize: '200% 200%', animation: 'sbLiveBg 9s ease-in-out infinite', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '18px' } }, [
|
||||
h('div', { key: 'a', style: { width: '120px', height: '120px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'radial-gradient(circle at 35% 30%, var(--sb-surface-4), var(--sb-surface))', border: '1px solid rgba(var(--sb-ink), 0.1)', color: 'var(--sb-text-6)' } }, svg(ICON.user, 54, 1.5)),
|
||||
h('div', { key: 't', style: { fontSize: '15px', fontWeight: 600, color: 'var(--sb-text-7)' } }, t('call.peerCameraOff'))
|
||||
]),
|
||||
// Top bar
|
||||
h('div', { key: 'top', style: { position: 'absolute', top: 0, left: 0, right: 0, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '14px', padding: '18px 20px', background: 'linear-gradient(180deg, rgba(0,0,0,0.55), transparent)' } }, [
|
||||
h('div', { key: 'l' }, [
|
||||
h('div', { key: 'n', style: { fontSize: '18px', fontWeight: 800, letterSpacing: '-0.3px', color: '#fff' } }, name),
|
||||
h('div', { key: 's', style: { display: 'inline-flex', alignItems: 'center', gap: '9px', marginTop: '4px' } }, [
|
||||
h('span', { key: 'st', style: { fontFamily: MONO, fontSize: '12.5px', fontWeight: 500, color: '#e8e8eb' } }, callStatus),
|
||||
h('span', { key: 'st', style: { fontFamily: MONO, fontSize: '12.5px', fontWeight: 500, color: 'var(--sb-text-2)' } }, callStatus),
|
||||
encBadge,
|
||||
phase === 'active' && qualityIndicator(false)
|
||||
])
|
||||
@@ -227,11 +227,11 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
|
||||
h('button', { key: 'min', onClick: () => setMinimized(true), title: t('call.minimize'), style: { flex: 'none', ...minimizeBtn(true) } }, svg(ICON.minimize, 16, 2))
|
||||
]),
|
||||
// Self-cam PiP
|
||||
h('div', { key: 'self', style: { position: 'absolute', bottom: '108px', insetInlineEnd: '18px', width: '132px', height: '176px', borderRadius: '14px', overflow: 'hidden', border: '1px solid rgba(255,255,255,0.16)', boxShadow: '0 12px 30px rgba(0,0,0,0.5)', background: '#111' } }, [
|
||||
h('div', { key: 'self', style: { position: 'absolute', bottom: '108px', insetInlineEnd: '18px', width: '132px', height: '176px', borderRadius: '14px', overflow: 'hidden', border: '1px solid rgba(var(--sb-ink), 0.16)', boxShadow: '0 12px 30px rgba(var(--sb-shadow-rgb), calc(0.5 * var(--sb-shadow-k)))', background: '#111' } }, [
|
||||
h('video', { key: 'sv', ref: selfVideoRef, autoPlay: true, muted: true, playsInline: true, style: { width: '100%', height: '100%', objectFit: 'cover', transform: 'scaleX(-1)', display: 'block' } }),
|
||||
!call.cameraEnabled && h('div', { key: 'off', style: { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', background: '#161618', color: '#6b6b73' } }, [
|
||||
!call.cameraEnabled && h('div', { key: 'off', style: { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', background: 'var(--sb-surface)', color: 'var(--sb-text-9)' } }, [
|
||||
svg(ICON.camOff, 24, 1.8),
|
||||
h('span', { key: 't', style: { fontSize: '10.5px', color: '#6b6b73', fontFamily: MONO } }, t('call.cameraOff'))
|
||||
h('span', { key: 't', style: { fontSize: '10.5px', color: 'var(--sb-text-9)', fontFamily: MONO } }, t('call.cameraOff'))
|
||||
])
|
||||
]),
|
||||
// Control bar
|
||||
@@ -245,16 +245,16 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
|
||||
}
|
||||
|
||||
// ── VOICE call (expanded) ────────────────────────────────────────────────
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column', background: 'radial-gradient(680px 460px at 50% 36%, rgba(240,137,42,0.08), transparent 70%), #0d0d0f', animation: 'sbExpand .2s ease' } }, [
|
||||
return h('div', { style: { position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column', background: 'radial-gradient(680px 460px at 50% 36%, rgba(var(--sb-orange-rgb), 0.08), transparent 70%), var(--sb-bg-deep)', animation: 'sbExpand .2s ease' } }, [
|
||||
hiddenAudio,
|
||||
h('div', { key: 'top', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 18px' } }, [
|
||||
h('span', { key: 'enc', style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: '#3ecf8e' } }, [svg(ICON.lock, 13, 2), t('call.encrypted')]),
|
||||
h('span', { key: 'enc', style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: 'var(--sb-green)' } }, [svg(ICON.lock, 13, 2), t('call.encrypted')]),
|
||||
h('button', { key: 'min', onClick: () => setMinimized(true), title: t('call.minimize'), style: minimizeBtn(false) }, svg(ICON.minimize, 16, 2))
|
||||
]),
|
||||
h('div', { key: 'mid', style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' } }, [
|
||||
avatarDisc(46, ringing),
|
||||
h('div', { key: 'nm', style: { fontSize: '24px', fontWeight: 800, letterSpacing: '-0.5px', color: '#f4f4f6' } }, name),
|
||||
h('div', { key: 'st', style: { fontFamily: MONO, fontSize: '14px', fontWeight: 500, color: '#9a9aa2', marginTop: '8px' } }, callStatus),
|
||||
h('div', { key: 'nm', style: { fontSize: '24px', fontWeight: 800, letterSpacing: '-0.5px', color: 'var(--sb-text-1)' } }, name),
|
||||
h('div', { key: 'st', style: { fontFamily: MONO, fontSize: '14px', fontWeight: 500, color: 'var(--sb-text-6)', marginTop: '8px' } }, callStatus),
|
||||
phase === 'active' && h('div', { key: 'q', style: { marginTop: '12px' } }, qualityIndicator(false))
|
||||
]),
|
||||
h('div', { key: 'ctrls', style: { flex: 'none', display: 'flex', alignItems: 'flex-start', justifyContent: 'center', gap: '26px', padding: '28px 24px 34px' } }, [
|
||||
|
||||
@@ -17,7 +17,9 @@ const CommunityCTA = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ACCENT = '#f0892a';
|
||||
const ACCENT = 'var(--sb-orange)';
|
||||
// Fill form: brand colour in both themes. See app.jsx.
|
||||
const ACCENT_SOLID = 'var(--sb-orange-solid)';
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
const githubUrl = 'https://github.com/SecureBitChat/securebit-chat/';
|
||||
@@ -30,13 +32,13 @@ const CommunityCTA = () => {
|
||||
rel: 'noopener noreferrer',
|
||||
style: {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '11px', padding: '15px 26px',
|
||||
borderRadius: '13px', background: ACCENT, color: '#1a0f04', textDecoration: 'none',
|
||||
borderRadius: '13px', background: ACCENT_SOLID, color: 'var(--sb-on-accent)', textDecoration: 'none',
|
||||
fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px',
|
||||
boxShadow: '0 8px 24px rgba(240,137,42,0.28)', whiteSpace: 'nowrap',
|
||||
boxShadow: '0 8px 24px rgba(var(--sb-orange-rgb), 0.28)', whiteSpace: 'nowrap',
|
||||
transition: 'all .2s cubic-bezier(.2,.7,.3,1)'
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.background = '#ff9637'; e.currentTarget.style.transform = 'translateY(-2px)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.background = ACCENT; e.currentTarget.style.transform = 'none'; }
|
||||
onMouseEnter: (e) => { e.currentTarget.style.background = 'var(--sb-orange-hi)'; e.currentTarget.style.transform = 'translateY(-2px)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.background = ACCENT_SOLID; e.currentTarget.style.transform = 'none'; }
|
||||
}, [
|
||||
React.createElement('svg', {
|
||||
key: 'i', width: 20, height: 20, viewBox: '0 0 24 24', fill: 'currentColor',
|
||||
@@ -51,13 +53,13 @@ const CommunityCTA = () => {
|
||||
rel: 'noopener noreferrer',
|
||||
style: {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '11px', padding: '15px 26px',
|
||||
borderRadius: '13px', background: 'rgba(255,255,255,0.03)', color: '#e8e8eb', textDecoration: 'none',
|
||||
borderRadius: '13px', background: 'rgba(var(--sb-ink), 0.03)', color: 'var(--sb-text-2)', textDecoration: 'none',
|
||||
fontSize: '15.5px', fontWeight: 700, letterSpacing: '-0.2px',
|
||||
border: '1px solid rgba(255,255,255,0.1)', whiteSpace: 'nowrap',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.1)', whiteSpace: 'nowrap',
|
||||
transition: 'all .2s cubic-bezier(.2,.7,.3,1)'
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.24)'; e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'; e.currentTarget.style.background = 'rgba(255,255,255,0.03)'; }
|
||||
onMouseEnter: (e) => { e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.24)'; e.currentTarget.style.background = 'rgba(var(--sb-ink), 0.06)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.1)'; e.currentTarget.style.background = 'rgba(var(--sb-ink), 0.03)'; }
|
||||
}, [
|
||||
React.createElement('svg', {
|
||||
key: 'i', width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
||||
@@ -72,16 +74,16 @@ const CommunityCTA = () => {
|
||||
style: {
|
||||
position: 'relative', overflow: 'hidden', maxWidth: '860px', width: '100%',
|
||||
borderRadius: '24px',
|
||||
background: 'radial-gradient(700px 360px at 50% 0%, rgba(240,137,42,0.1), transparent 65%), #121214',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
background: 'radial-gradient(700px 360px at 50% 0%, rgba(var(--sb-orange-rgb), 0.1), transparent 65%), var(--sb-bg)',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.07)',
|
||||
padding: isMobile ? '40px 24px 36px' : '56px 56px 48px',
|
||||
textAlign: 'center', boxShadow: '0 24px 60px rgba(0,0,0,0.4)'
|
||||
textAlign: 'center', boxShadow: '0 24px 60px rgba(var(--sb-shadow-rgb), calc(0.4 * var(--sb-shadow-k)))'
|
||||
}
|
||||
}, [
|
||||
// hairline accent
|
||||
React.createElement('div', {
|
||||
key: 'hairline',
|
||||
style: { position: 'absolute', top: 0, left: '50%', transform: 'translateX(-50%)', width: '180px', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(240,137,42,0.7), transparent)' }
|
||||
style: { position: 'absolute', top: 0, left: '50%', transform: 'translateX(-50%)', width: '180px', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(var(--sb-orange-rgb), 0.7), transparent)' }
|
||||
}),
|
||||
// brand mark (same SVG as the header — no border or background)
|
||||
React.createElement('img', {
|
||||
@@ -93,12 +95,12 @@ const CommunityCTA = () => {
|
||||
// title
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
style: { margin: '0 0 16px', fontSize: isMobile ? '28px' : '36px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.05, color: '#f4f4f6' }
|
||||
style: { margin: '0 0 16px', fontSize: isMobile ? '28px' : '36px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.05, color: 'var(--sb-text-1)' }
|
||||
}, t('community.title')),
|
||||
// description
|
||||
React.createElement('p', {
|
||||
key: 'desc',
|
||||
style: { margin: '0 auto 32px', maxWidth: '560px', fontSize: '16px', lineHeight: 1.65, color: '#9a9aa2' }
|
||||
style: { margin: '0 auto 32px', maxWidth: '560px', fontSize: '16px', lineHeight: 1.65, color: 'var(--sb-text-6)' }
|
||||
}, t('community.description')),
|
||||
// buttons
|
||||
React.createElement('div', {
|
||||
@@ -140,7 +142,7 @@ const CommunityCTA = () => {
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: '#0f0f11', fontFamily: SANS,
|
||||
background: 'var(--sb-bg)', fontFamily: SANS,
|
||||
padding: isMobile ? '48px 18px' : '64px 48px'
|
||||
}
|
||||
}, [
|
||||
|
||||
@@ -158,15 +158,15 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
key: i,
|
||||
style: {
|
||||
width: '11px', height: '11px', borderRadius: '2px',
|
||||
background: i < filled ? color : 'rgba(255,255,255,0.07)',
|
||||
border: '1px solid ' + (i < filled ? 'transparent' : 'rgba(255,255,255,0.05)'),
|
||||
background: i < filled ? color : 'rgba(var(--sb-ink), 0.07)',
|
||||
border: '1px solid ' + (i < filled ? 'transparent' : 'rgba(var(--sb-ink), 0.05)'),
|
||||
boxShadow: i < filled ? `0 0 5px ${color}55` : 'none',
|
||||
transition: 'background .2s ease, box-shadow .2s ease'
|
||||
}
|
||||
}))),
|
||||
React.createElement('div', {
|
||||
key: 'text',
|
||||
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: '11.5px', color: '#8a8a92' }
|
||||
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: '11.5px', color: 'var(--sb-text-7)' }
|
||||
}, [
|
||||
React.createElement('span', { key: 'status', style: { display: 'inline-flex', alignItems: 'center', gap: '5px' } }, [
|
||||
React.createElement('i', { key: 'icon', className: getStatusIcon(transfer.status) }),
|
||||
@@ -174,7 +174,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'count',
|
||||
style: { fontFamily: "'JetBrains Mono', ui-monospace, monospace", color: i_done(transfer) ? color : '#8a8a92' }
|
||||
style: { fontFamily: "'JetBrains Mono', ui-monospace, monospace", color: i_done(transfer) ? color : 'var(--sb-text-7)' }
|
||||
}, total > 0 ? `${Math.min(done, total)} / ${total} chunks` : `${(transfer.progress || 0).toFixed(0)}%`)
|
||||
])
|
||||
]);
|
||||
@@ -221,9 +221,9 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
onDragLeave: handleDragLeave,
|
||||
style: {
|
||||
position: 'relative',
|
||||
border: '1.5px dashed ' + (dragOver ? 'rgba(240,137,42,0.7)' : 'rgba(255,255,255,0.14)'),
|
||||
border: '1.5px dashed ' + (dragOver ? 'rgba(var(--sb-orange-rgb), 0.7)' : 'rgba(var(--sb-ink), 0.14)'),
|
||||
borderRadius: '14px',
|
||||
background: dragOver ? 'rgba(240,137,42,0.07)' : '#141416',
|
||||
background: dragOver ? 'rgba(var(--sb-orange-rgb), 0.07)' : 'var(--sb-surface)',
|
||||
padding: '24px 22px',
|
||||
textAlign: 'center',
|
||||
transition: 'all .15s'
|
||||
@@ -231,16 +231,16 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'icon-box',
|
||||
style: { width: '42px', height: '42px', margin: '0 auto 10px', borderRadius: '12px', display: 'grid', placeItems: 'center', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)' }
|
||||
}, React.createElement('i', { className: 'fas fa-arrow-up-from-bracket', style: { color: '#9a9aa2', fontSize: '18px' } })),
|
||||
React.createElement('div', { key: 'title', style: { fontSize: '14px', fontWeight: 700, color: '#e8e8eb' } }, t('file.drop')),
|
||||
React.createElement('div', { key: 'sub', style: { fontSize: '12px', color: '#7b7b83', marginTop: '4px' } }, t('file.dropHint')),
|
||||
style: { width: '42px', height: '42px', margin: '0 auto 10px', borderRadius: '12px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-ink), 0.04)', border: '1px solid rgba(var(--sb-ink), 0.08)' }
|
||||
}, React.createElement('i', { className: 'fas fa-arrow-up-from-bracket', style: { color: 'var(--sb-text-6)', fontSize: '18px' } })),
|
||||
React.createElement('div', { key: 'title', style: { fontSize: '14px', fontWeight: 700, color: 'var(--sb-text-2)' } }, t('file.drop')),
|
||||
React.createElement('div', { key: 'sub', style: { fontSize: '12px', color: 'var(--sb-text-8)', marginTop: '4px' } }, t('file.dropHint')),
|
||||
React.createElement('button', {
|
||||
key: 'browse',
|
||||
type: 'button',
|
||||
onClick: () => fileInputRef.current?.click(),
|
||||
className: 'sb-send',
|
||||
style: { marginTop: '14px', display: 'inline-flex', alignItems: 'center', gap: '7px', padding: '9px 16px', borderRadius: '9px', border: 'none', background: '#f0892a', color: '#1a0f04', fontFamily: 'inherit', fontSize: '13px', fontWeight: 700, cursor: 'pointer' }
|
||||
style: { marginTop: '14px', display: 'inline-flex', alignItems: 'center', gap: '7px', padding: '9px 16px', borderRadius: '9px', border: 'none', background: 'var(--sb-orange-solid)', color: 'var(--sb-on-accent)', fontFamily: 'inherit', fontSize: '13px', fontWeight: 700, cursor: 'pointer' }
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-folder-open', style: { fontSize: '13px' } }),
|
||||
t('file.browse')
|
||||
@@ -262,23 +262,23 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
className: "mt-4 space-y-2"
|
||||
}, pendingIncomingFiles.map(file => React.createElement('div', {
|
||||
key: file.fileId,
|
||||
style: { borderRadius: '12px', border: '1px solid rgba(255,255,255,0.08)', background: '#161618', padding: '12px 14px' }
|
||||
style: { borderRadius: '12px', border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-surface)', padding: '12px 14px' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'info',
|
||||
style: { marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '11px' }
|
||||
}, [
|
||||
React.createElement('div', { key: 'ic', style: { flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.22)' } },
|
||||
React.createElement('i', { className: 'fas fa-file-arrow-down', style: { color: '#f0892a', fontSize: '15px' } })
|
||||
React.createElement('div', { key: 'ic', style: { flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: 'rgba(var(--sb-orange-rgb), 0.12)', border: '1px solid rgba(var(--sb-orange-rgb), 0.22)' } },
|
||||
React.createElement('i', { className: 'fas fa-file-arrow-down', style: { color: 'var(--sb-orange)', fontSize: '15px' } })
|
||||
),
|
||||
React.createElement('div', { key: 'text', style: { minWidth: 0 } }, [
|
||||
React.createElement('div', {
|
||||
key: 'title',
|
||||
style: { fontSize: '13px', fontWeight: 600, color: '#e8e8eb' }
|
||||
style: { fontSize: '13px', fontWeight: 600, color: 'var(--sb-text-2)' }
|
||||
}, t('file.incoming')),
|
||||
React.createElement('div', {
|
||||
key: 'meta',
|
||||
style: { fontSize: '11.5px', color: '#7b7b83', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }
|
||||
style: { fontSize: '11.5px', color: 'var(--sb-text-8)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }
|
||||
}, `${file.fileName} · ${formatFileSize(file.fileSize)} · ${file.mimeType}`)
|
||||
])
|
||||
]),
|
||||
@@ -289,12 +289,12 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
React.createElement('button', {
|
||||
key: 'accept',
|
||||
onClick: () => handleIncomingDecision(file.fileId, true),
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: '8px', border: 'none', background: '#f0892a', color: '#1a0f04', padding: '8px 14px', fontSize: '13px', fontWeight: 700, cursor: 'pointer' }
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: '8px', border: 'none', background: 'var(--sb-orange-solid)', color: 'var(--sb-on-accent)', padding: '8px 14px', fontSize: '13px', fontWeight: 700, cursor: 'pointer' }
|
||||
}, [React.createElement('i', { key: 'i', className: 'fas fa-check', style: { fontSize: '12px' } }), t('file.accept')]),
|
||||
React.createElement('button', {
|
||||
key: 'reject',
|
||||
onClick: () => handleIncomingDecision(file.fileId, false),
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: '8px', border: '1px solid rgba(229,114,122,0.3)', background: 'rgba(229,114,122,0.08)', color: '#e5727a', padding: '8px 14px', fontSize: '13px', fontWeight: 600, cursor: 'pointer' }
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: '8px', border: '1px solid rgba(var(--sb-red-rgb), 0.3)', background: 'rgba(var(--sb-red-rgb), 0.08)', color: 'var(--sb-red)', padding: '8px 14px', fontSize: '13px', fontWeight: 600, cursor: 'pointer' }
|
||||
}, [React.createElement('i', { key: 'i', className: 'fas fa-xmark', style: { fontSize: '12px' } }), t('file.reject')])
|
||||
])
|
||||
]))),
|
||||
@@ -306,7 +306,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'title',
|
||||
style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12.5px', fontWeight: 600, color: '#8a8a92', marginBottom: '10px' }
|
||||
style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12.5px', fontWeight: 600, color: 'var(--sb-text-7)', marginBottom: '10px' }
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
@@ -320,7 +320,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
...transfers.sending.map(transfer =>
|
||||
React.createElement('div', {
|
||||
key: `send-${transfer.fileId}`,
|
||||
style: { borderRadius: '11px', border: '1px solid rgba(255,255,255,0.07)', background: '#161618', padding: '12px', marginBottom: '8px' }
|
||||
style: { borderRadius: '11px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'var(--sb-surface)', padding: '12px', marginBottom: '8px' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
@@ -333,17 +333,17 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-arrow-up',
|
||||
style: { color: '#f0892a', fontSize: '13px', marginInlineEnd: '8px' }
|
||||
style: { color: 'var(--sb-orange)', fontSize: '13px', marginInlineEnd: '8px' }
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'name',
|
||||
className: "font-medium text-sm",
|
||||
style: { color: '#e8e8eb' }
|
||||
style: { color: 'var(--sb-text-2)' }
|
||||
}, transfer.fileName),
|
||||
React.createElement('span', {
|
||||
key: 'size',
|
||||
className: "text-xs ms-2",
|
||||
style: { color: '#7b7b83' }
|
||||
style: { color: 'var(--sb-text-8)' }
|
||||
}, formatFileSize(transfer.fileSize))
|
||||
]),
|
||||
React.createElement('button', {
|
||||
@@ -356,7 +356,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
})
|
||||
])
|
||||
]),
|
||||
renderProgress(transfer, '#f0892a')
|
||||
renderProgress(transfer, 'var(--sb-orange)')
|
||||
])
|
||||
),
|
||||
|
||||
@@ -364,7 +364,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
...transfers.receiving.map(transfer =>
|
||||
React.createElement('div', {
|
||||
key: `recv-${transfer.fileId}`,
|
||||
style: { borderRadius: '11px', border: '1px solid rgba(255,255,255,0.07)', background: '#161618', padding: '12px', marginBottom: '8px' }
|
||||
style: { borderRadius: '11px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'var(--sb-surface)', padding: '12px', marginBottom: '8px' }
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
@@ -377,17 +377,17 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-arrow-down',
|
||||
style: { color: '#3ecf8e', fontSize: '13px', marginInlineEnd: '8px' }
|
||||
style: { color: 'var(--sb-green)', fontSize: '13px', marginInlineEnd: '8px' }
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'name',
|
||||
className: "font-medium text-sm",
|
||||
style: { color: '#e8e8eb' }
|
||||
style: { color: 'var(--sb-text-2)' }
|
||||
}, transfer.fileName),
|
||||
React.createElement('span', {
|
||||
key: 'size',
|
||||
className: "text-xs ms-2",
|
||||
style: { color: '#7b7b83' }
|
||||
style: { color: 'var(--sb-text-8)' }
|
||||
}, formatFileSize(transfer.fileSize))
|
||||
]),
|
||||
React.createElement('div', { key: 'actions', className: 'flex items-center space-x-2' }, [
|
||||
@@ -422,7 +422,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
|
||||
])
|
||||
])
|
||||
]),
|
||||
renderProgress(transfer, '#3ecf8e')
|
||||
renderProgress(transfer, 'var(--sb-green)')
|
||||
])
|
||||
)
|
||||
])
|
||||
|
||||
@@ -40,26 +40,26 @@ const svg = (inner, size, sw) => h('span', {
|
||||
|
||||
const ctrlBase = {
|
||||
width: '56px', height: '56px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.05)',
|
||||
color: '#cfcfd4', cursor: 'pointer', transition: 'all .15s',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'rgba(var(--sb-ink), 0.05)',
|
||||
color: 'var(--sb-text-4)', cursor: 'pointer', transition: 'all .15s',
|
||||
};
|
||||
const dangerCtrl = { ...ctrlBase, background: '#e5484d', color: '#fff', border: '1px solid transparent' };
|
||||
const dangerCtrl = { ...ctrlBase, background: 'var(--sb-red-strong-solid)', color: '#fff', border: '1px solid transparent' };
|
||||
const endBtn = {
|
||||
width: '56px', height: '56px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
border: 'none', background: '#e5484d', color: '#fff', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(229,72,77,0.35)', transition: 'transform .15s',
|
||||
border: 'none', background: 'var(--sb-red-strong-solid)', color: '#fff', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(var(--sb-red-strong-rgb), 0.35)', transition: 'transform .15s',
|
||||
};
|
||||
const minimizeBtn = {
|
||||
width: '36px', height: '36px', borderRadius: '9px', display: 'grid', placeItems: 'center',
|
||||
border: '1px solid rgba(255,255,255,0.15)', background: 'rgba(0,0,0,0.35)',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.15)', background: 'rgba(0,0,0,0.35)',
|
||||
color: '#fff', cursor: 'pointer', transition: 'all .15s',
|
||||
};
|
||||
|
||||
const QUALITY = {
|
||||
excellent: { bars: 4, color: '#3ecf8e' },
|
||||
good: { bars: 3, color: '#3ecf8e' },
|
||||
fair: { bars: 2, color: '#e3c84e' },
|
||||
poor: { bars: 1, color: '#e5727a' },
|
||||
excellent: { bars: 4, color: 'var(--sb-green)' },
|
||||
good: { bars: 3, color: 'var(--sb-green)' },
|
||||
fair: { bars: 2, color: 'var(--sb-yellow)' },
|
||||
poor: { bars: 1, color: 'var(--sb-red)' },
|
||||
};
|
||||
|
||||
function qualityBars(quality) {
|
||||
@@ -70,7 +70,7 @@ function qualityBars(quality) {
|
||||
key: i,
|
||||
style: {
|
||||
width: '2.5px', height: (4 + i * 2.6) + 'px', borderRadius: '1px',
|
||||
background: i < q.bars ? q.color : 'rgba(255,255,255,0.18)',
|
||||
background: i < q.bars ? q.color : 'rgba(var(--sb-ink), 0.18)',
|
||||
},
|
||||
})));
|
||||
}
|
||||
@@ -180,9 +180,9 @@ function Tile({ peer, stream, self, localStream, cameraEnabled, speaking, tileW,
|
||||
: (speaking ? t('groupCall.speaking', { name: peer.name }) : peer.name),
|
||||
style: {
|
||||
cursor: onSelect ? 'pointer' : 'default',
|
||||
position: 'relative', borderRadius: '14px', overflow: 'hidden', background: '#141417',
|
||||
border: speaking ? '2px solid #3ecf8e' : '2px solid rgba(255,255,255,0.08)',
|
||||
boxShadow: speaking ? '0 0 0 3px rgba(62,207,142,0.16)' : 'none',
|
||||
position: 'relative', borderRadius: '14px', overflow: 'hidden', background: 'var(--sb-surface)',
|
||||
border: speaking ? '2px solid var(--sb-green-solid)' : '2px solid rgba(var(--sb-ink), 0.08)',
|
||||
boxShadow: speaking ? '0 0 0 3px rgba(var(--sb-green-rgb), 0.16)' : 'none',
|
||||
transition: 'border-color .12s ease, box-shadow .12s ease',
|
||||
// Sized by the grid, which is the only thing that knows how much
|
||||
// room there is. aspectRatio stays as the fallback for the first
|
||||
@@ -205,7 +205,7 @@ function Tile({ peer, stream, self, localStream, cameraEnabled, speaking, tileW,
|
||||
key: 'v', ref: videoRef, autoPlay: true, muted: true, playsInline: true,
|
||||
style: {
|
||||
position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover',
|
||||
transform: self ? 'scaleX(-1)' : 'none', background: '#0f0f11',
|
||||
transform: self ? 'scaleX(-1)' : 'none', background: 'var(--sb-bg)',
|
||||
},
|
||||
})
|
||||
: h('div', {
|
||||
@@ -215,9 +215,9 @@ function Tile({ peer, stream, self, localStream, cameraEnabled, speaking, tileW,
|
||||
// large tile and crowds a small one.
|
||||
width: avatarPx + 'px', height: avatarPx + 'px',
|
||||
borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
background: 'radial-gradient(circle at 35% 30%, #2a2a30, #161618)',
|
||||
border: speaking ? '1.5px solid rgba(62,207,142,0.65)' : '1px solid rgba(255,255,255,0.1)',
|
||||
color: speaking ? '#3ecf8e' : '#cfcfd4',
|
||||
background: 'radial-gradient(circle at 35% 30%, var(--sb-surface-4), var(--sb-surface))',
|
||||
border: speaking ? '1.5px solid rgba(var(--sb-green-rgb), 0.65)' : '1px solid rgba(var(--sb-ink), 0.1)',
|
||||
color: speaking ? 'var(--sb-green)' : 'var(--sb-text-4)',
|
||||
fontFamily: MONO, fontSize: Math.round(avatarPx * 0.3) + 'px', fontWeight: 700,
|
||||
transition: 'color .12s ease, border-color .12s ease',
|
||||
},
|
||||
@@ -233,18 +233,18 @@ function Tile({ peer, stream, self, localStream, cameraEnabled, speaking, tileW,
|
||||
h('span', {
|
||||
key: 'n',
|
||||
style: {
|
||||
flex: 1, minWidth: 0, fontSize: compact ? '10.5px' : '12.5px', fontWeight: 700, color: '#f4f4f6',
|
||||
flex: 1, minWidth: 0, fontSize: compact ? '10.5px' : '12.5px', fontWeight: 700, color: 'var(--sb-text-1)',
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
},
|
||||
}, peer.name),
|
||||
speaking && h('span', {
|
||||
key: 'sp', style: { flex: 'none', color: '#3ecf8e', display: 'grid', placeItems: 'center' },
|
||||
key: 'sp', style: { flex: 'none', color: 'var(--sb-green)', display: 'grid', placeItems: 'center' },
|
||||
}, svg(ICON.micOn, compact ? 11 : 13, 2)),
|
||||
(statusWord && !compact) && h('span', {
|
||||
key: 's',
|
||||
style: {
|
||||
flex: 'none', fontFamily: MONO, fontSize: '10.5px',
|
||||
color: peer.state === LEG_STATE.FAILED ? '#e5727a' : '#9a9aa2',
|
||||
color: peer.state === LEG_STATE.FAILED ? 'var(--sb-red)' : 'var(--sb-text-6)',
|
||||
},
|
||||
}, statusWord),
|
||||
(!self && !compact) && qualityBars(peer.quality),
|
||||
@@ -300,7 +300,7 @@ export function GroupCallUI({
|
||||
|
||||
const encBadge = h('span', {
|
||||
key: 'enc',
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '5px', fontSize: '11.5px', fontWeight: 600, color: '#3ecf8e' },
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '5px', fontSize: '11.5px', fontWeight: 600, color: 'var(--sb-green)' },
|
||||
}, [svg(ICON.lock, 12, 2), t('call.encryptedShort')]);
|
||||
|
||||
// ── somebody is calling the group and we have not joined ─────────────────
|
||||
@@ -308,12 +308,12 @@ export function GroupCallUI({
|
||||
return h('div', {
|
||||
style: {
|
||||
position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column',
|
||||
background: 'radial-gradient(680px 460px at 50% 36%, rgba(240,137,42,0.08), transparent 70%), #0d0d0f',
|
||||
background: 'radial-gradient(680px 460px at 50% 36%, rgba(var(--sb-orange-rgb), 0.08), transparent 70%), var(--sb-bg-deep)',
|
||||
animation: 'sbExpand .2s ease',
|
||||
},
|
||||
}, [
|
||||
h('div', { key: 'top', style: { flex: 'none', padding: '16px 18px' } },
|
||||
h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: '#3ecf8e' } },
|
||||
h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '7px', fontSize: '12px', fontWeight: 600, color: 'var(--sb-green)' } },
|
||||
[svg(ICON.lock, 13, 2), t('call.encrypted')])),
|
||||
h('div', { key: 'mid', style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '0 24px', textAlign: 'center' } }, [
|
||||
h('div', {
|
||||
@@ -323,40 +323,40 @@ export function GroupCallUI({
|
||||
display: 'grid', placeItems: 'center',
|
||||
},
|
||||
}, [
|
||||
h('span', { key: 'p1', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(240,137,42,0.5)', animation: 'sbCallPulse 2s ease-out infinite' } }),
|
||||
h('span', { key: 'p2', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(240,137,42,0.4)', animation: 'sbCallPulse 2s ease-out infinite', animationDelay: '1s' } }),
|
||||
h('span', { key: 'p1', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(var(--sb-orange-rgb), 0.5)', animation: 'sbCallPulse 2s ease-out infinite' } }),
|
||||
h('span', { key: 'p2', style: { position: 'absolute', inset: 0, borderRadius: '50%', border: '1.5px solid rgba(var(--sb-orange-rgb), 0.4)', animation: 'sbCallPulse 2s ease-out infinite', animationDelay: '1s' } }),
|
||||
h('div', {
|
||||
key: 'c',
|
||||
style: {
|
||||
width: '96px', height: '96px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
background: 'radial-gradient(circle at 35% 30%, #2a2a30, #161618)',
|
||||
border: '1px solid rgba(255,255,255,0.1)', color: '#8a8a92',
|
||||
background: 'radial-gradient(circle at 35% 30%, var(--sb-surface-4), var(--sb-surface))',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.1)', color: 'var(--sb-text-7)',
|
||||
},
|
||||
}, svg(ICON.users, 42, 1.6)),
|
||||
]),
|
||||
h('div', { key: 'n', style: { fontSize: '22px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' } }, groupName),
|
||||
h('div', { key: 's', style: { fontFamily: MONO, fontSize: '13.5px', color: '#9a9aa2', marginTop: '8px' } },
|
||||
h('div', { key: 'n', style: { fontSize: '22px', fontWeight: 800, letterSpacing: '-0.4px', color: 'var(--sb-text-1)' } }, groupName),
|
||||
h('div', { key: 's', style: { fontFamily: MONO, fontSize: '13.5px', color: 'var(--sb-text-6)', marginTop: '8px' } },
|
||||
call.withVideo
|
||||
? t('groupCall.startedVideo', { name: call.startedByName })
|
||||
: t('groupCall.startedVoice', { name: call.startedByName })),
|
||||
h('div', { key: 'p', style: { fontSize: '12.5px', color: '#6b6b73', marginTop: '6px' } },
|
||||
h('div', { key: 'p', style: { fontSize: '12.5px', color: 'var(--sb-text-9)', marginTop: '6px' } },
|
||||
t('groupCall.inCall', { count: call.participants.length })),
|
||||
]),
|
||||
h('div', { key: 'ctrls', style: { flex: 'none', display: 'flex', justifyContent: 'center', gap: '48px', padding: '24px 24px 40px' } }, [
|
||||
h('div', { key: 'dec', style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' } }, [
|
||||
h('button', { key: 'b', onClick: onDismiss, title: t('groupCall.dismiss'), style: { ...endBtn, width: '62px', height: '62px' } }, svg(ICON.phoneHangup, 24, 1.9)),
|
||||
h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: '#8a8a92' } }, t('groupCall.dismiss')),
|
||||
h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: 'var(--sb-text-7)' } }, t('groupCall.dismiss')),
|
||||
]),
|
||||
h('div', { key: 'acc', style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' } }, [
|
||||
h('button', {
|
||||
key: 'b', onClick: onJoin, title: t('groupCall.join'),
|
||||
style: {
|
||||
width: '62px', height: '62px', borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
border: 'none', background: '#3ecf8e', color: '#06231a', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(62,207,142,0.35)',
|
||||
border: 'none', background: 'var(--sb-green-solid)', color: 'var(--sb-on-green)', cursor: 'pointer',
|
||||
boxShadow: '0 8px 24px rgba(var(--sb-green-rgb), 0.35)',
|
||||
},
|
||||
}, svg(ICON.phone, 24, 1.9)),
|
||||
h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: '#8a8a92' } }, t('groupCall.join')),
|
||||
h('span', { key: 'l', style: { fontFamily: MONO, fontSize: '10.5px', color: 'var(--sb-text-7)' } }, t('groupCall.join')),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
@@ -379,8 +379,8 @@ export function GroupCallUI({
|
||||
return h('div', {
|
||||
style: {
|
||||
position: 'absolute', bottom: '18px', insetInlineEnd: '18px', zIndex: 40, width: '244px',
|
||||
borderRadius: '14px', overflow: 'hidden', background: '#161618',
|
||||
border: '1px solid rgba(255,255,255,0.1)', boxShadow: '0 18px 44px rgba(0,0,0,0.55)',
|
||||
borderRadius: '14px', overflow: 'hidden', background: 'var(--sb-surface)',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.1)', boxShadow: '0 18px 44px rgba(var(--sb-shadow-rgb), calc(0.55 * var(--sb-shadow-k)))',
|
||||
animation: 'sbExpand .18s ease',
|
||||
},
|
||||
}, [
|
||||
@@ -389,24 +389,24 @@ export function GroupCallUI({
|
||||
key: 'ic',
|
||||
style: {
|
||||
flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid',
|
||||
placeItems: 'center', background: 'rgba(62,207,142,0.1)',
|
||||
border: '1px solid rgba(62,207,142,0.25)', color: '#3ecf8e',
|
||||
placeItems: 'center', background: 'rgba(var(--sb-green-rgb), 0.1)',
|
||||
border: '1px solid rgba(var(--sb-green-rgb), 0.25)', color: 'var(--sb-green)',
|
||||
},
|
||||
}, svg(ICON.users, 16, 1.9)),
|
||||
h('div', { key: 'tx', style: { flex: 1, minWidth: 0 } }, [
|
||||
h('div', { key: 'n', style: { fontSize: '13px', fontWeight: 700, color: '#f4f4f6', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, groupName),
|
||||
h('div', { key: 'n', style: { fontSize: '13px', fontWeight: 700, color: 'var(--sb-text-1)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, groupName),
|
||||
h('div', {
|
||||
key: 's',
|
||||
style: {
|
||||
fontFamily: MONO, fontSize: '11px', color: talking ? '#3ecf8e' : '#9a9aa2',
|
||||
fontFamily: MONO, fontSize: '11px', color: talking ? 'var(--sb-green)' : 'var(--sb-text-6)',
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
},
|
||||
}, talking
|
||||
? t('groupCall.speaking', { name: talking })
|
||||
: `${t('groupCall.inCall', { count: call.participants.length })} · ${fmt(seconds)}`),
|
||||
]),
|
||||
h('button', { key: 'exp', onClick: () => setMinimized(false), title: t('call.expand'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'rgba(255,255,255,0.05)', color: '#cfcfd4', cursor: 'pointer' } }, svg(ICON.expand, 15, 2)),
|
||||
h('button', { key: 'end', onClick: onLeave, title: t('groupCall.leave'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: '#e5484d', color: '#fff', cursor: 'pointer' } }, svg(ICON.phoneHangup, 15, 2)),
|
||||
h('button', { key: 'exp', onClick: () => setMinimized(false), title: t('call.expand'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'rgba(var(--sb-ink), 0.05)', color: 'var(--sb-text-4)', cursor: 'pointer' } }, svg(ICON.expand, 15, 2)),
|
||||
h('button', { key: 'end', onClick: onLeave, title: t('groupCall.leave'), style: { flex: 'none', width: '32px', height: '32px', borderRadius: '8px', display: 'grid', placeItems: 'center', border: 'none', background: 'var(--sb-red-strong-solid)', color: '#fff', cursor: 'pointer' } }, svg(ICON.phoneHangup, 15, 2)),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -436,7 +436,7 @@ export function GroupCallUI({
|
||||
return h('div', {
|
||||
style: {
|
||||
position: 'absolute', inset: 0, zIndex: 40, display: 'flex', flexDirection: 'column',
|
||||
background: '#0a0a0c', animation: 'sbExpand .2s ease',
|
||||
background: 'var(--sb-bg-deepest)', animation: 'sbExpand .2s ease',
|
||||
},
|
||||
}, [
|
||||
h('div', {
|
||||
@@ -449,9 +449,9 @@ export function GroupCallUI({
|
||||
h('div', { key: 'l', style: { minWidth: 0 } }, [
|
||||
h('div', { key: 'n', style: { fontSize: '17px', fontWeight: 800, letterSpacing: '-0.3px', color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, groupName),
|
||||
h('div', { key: 's', style: { display: 'inline-flex', alignItems: 'center', gap: '9px', marginTop: '4px' } }, [
|
||||
h('span', { key: 'd', style: { fontFamily: MONO, fontSize: '12.5px', color: '#e8e8eb' } }, fmt(seconds)),
|
||||
h('span', { key: 'd', style: { fontFamily: MONO, fontSize: '12.5px', color: 'var(--sb-text-2)' } }, fmt(seconds)),
|
||||
encBadge,
|
||||
h('span', { key: 'c', style: { fontSize: '11.5px', color: '#8a8a92' } },
|
||||
h('span', { key: 'c', style: { fontSize: '11.5px', color: 'var(--sb-text-7)' } },
|
||||
t('groupCall.inCall', { count: call.participants.length })),
|
||||
]),
|
||||
]),
|
||||
@@ -460,7 +460,7 @@ export function GroupCallUI({
|
||||
key: 'grid', onClick: () => setPinned(null), title: t('groupCall.showEveryone'),
|
||||
style: {
|
||||
height: '36px', padding: '0 12px', borderRadius: '9px', display: 'inline-flex',
|
||||
alignItems: 'center', gap: '7px', border: '1px solid rgba(255,255,255,0.15)',
|
||||
alignItems: 'center', gap: '7px', border: '1px solid rgba(var(--sb-ink), 0.15)',
|
||||
background: 'rgba(0,0,0,0.35)', color: '#fff', cursor: 'pointer',
|
||||
fontFamily: 'inherit', fontSize: '12.5px', fontWeight: 600,
|
||||
},
|
||||
@@ -473,8 +473,8 @@ export function GroupCallUI({
|
||||
key: 'err',
|
||||
style: {
|
||||
flex: 'none', margin: '0 18px 10px', padding: '9px 12px', borderRadius: '10px',
|
||||
background: 'rgba(229,114,122,0.1)', border: '1px solid rgba(229,114,122,0.28)',
|
||||
color: '#e5727a', fontSize: '12.5px', lineHeight: 1.5,
|
||||
background: 'rgba(var(--sb-red-rgb), 0.1)', border: '1px solid rgba(var(--sb-red-rgb), 0.28)',
|
||||
color: 'var(--sb-red)', fontSize: '12.5px', lineHeight: 1.5,
|
||||
},
|
||||
}, t(`groupCall.err.${media.error}`)),
|
||||
|
||||
|
||||
@@ -15,18 +15,18 @@ import { GROUP_LIMITS } from '../../group/groupCrypto.js';
|
||||
const h = (...args) => React.createElement(...args);
|
||||
|
||||
const C = {
|
||||
bg: '#0c0c0e',
|
||||
panel: '#141417',
|
||||
panel2: '#1b1b1f',
|
||||
line: 'rgba(255,255,255,0.07)',
|
||||
line2: 'rgba(255,255,255,0.13)',
|
||||
ink: '#f4f4f6',
|
||||
ink2: '#a7a7b0',
|
||||
ink3: '#6b6b73',
|
||||
accent: '#f0892a',
|
||||
good: '#3ecf8e',
|
||||
warn: '#e3b341',
|
||||
bad: '#e5727a',
|
||||
bg: 'var(--sb-bg-deep)',
|
||||
panel: 'var(--sb-surface)',
|
||||
panel2: 'var(--sb-surface-2)',
|
||||
line: 'rgba(var(--sb-ink), 0.07)',
|
||||
line2: 'rgba(var(--sb-ink), 0.13)',
|
||||
ink: 'var(--sb-text-1)',
|
||||
ink2: 'var(--sb-text-5b)',
|
||||
ink3: 'var(--sb-text-9)',
|
||||
accent: 'var(--sb-orange)',
|
||||
good: 'var(--sb-green)',
|
||||
warn: 'var(--sb-yellow-2)',
|
||||
bad: 'var(--sb-red)',
|
||||
mono: "'JetBrains Mono', ui-monospace, monospace",
|
||||
};
|
||||
|
||||
@@ -53,18 +53,18 @@ const btn = (accent = false) => ({
|
||||
fontFamily: 'inherit', fontSize: '14px', fontWeight: 700,
|
||||
border: accent ? 'none' : `1px solid ${C.line2}`,
|
||||
background: accent ? C.accent : 'transparent',
|
||||
color: accent ? '#1a0f04' : C.ink2,
|
||||
color: accent ? 'var(--sb-on-accent)' : C.ink2,
|
||||
});
|
||||
|
||||
const overlay = {
|
||||
position: 'fixed', inset: 0, zIndex: 90, display: 'grid', placeItems: 'center',
|
||||
background: 'rgba(5,5,7,0.72)', backdropFilter: 'blur(6px)', padding: '20px',
|
||||
background: 'rgba(var(--sb-scrim-rgb), 0.72)', backdropFilter: 'blur(6px)', padding: '20px',
|
||||
};
|
||||
|
||||
const card = {
|
||||
width: '100%', maxWidth: '440px', background: C.panel, border: `1px solid ${C.line}`,
|
||||
borderRadius: '16px', padding: '24px', display: 'flex', flexDirection: 'column', gap: '18px',
|
||||
boxShadow: '0 24px 60px rgba(0,0,0,0.5)',
|
||||
boxShadow: '0 24px 60px rgba(var(--sb-shadow-rgb), calc(0.5 * var(--sb-shadow-k)))',
|
||||
};
|
||||
|
||||
/** Trim a string so its UTF-8 encoding fits `max` bytes, never mid-character. */
|
||||
@@ -167,7 +167,7 @@ export function GroupSasModal({ group, onConfirm, onCancel }) {
|
||||
key: 'code',
|
||||
style: {
|
||||
padding: '22px 16px', textAlign: 'center', borderRadius: '12px',
|
||||
background: 'rgba(240,137,42,0.09)', border: '1px solid rgba(240,137,42,0.3)',
|
||||
background: 'rgba(var(--sb-orange-rgb), 0.09)', border: '1px solid rgba(var(--sb-orange-rgb), 0.3)',
|
||||
},
|
||||
}, h('span', {
|
||||
// Everyone in the group compares these digits against each other's
|
||||
@@ -191,7 +191,7 @@ export function GroupSasModal({ group, onConfirm, onCancel }) {
|
||||
key: 'warn',
|
||||
style: {
|
||||
margin: 0, padding: '11px 13px', borderRadius: '9px', fontSize: '12.5px', lineHeight: 1.55,
|
||||
background: 'rgba(229,114,122,0.09)', border: '1px solid rgba(229,114,122,0.26)', color: '#f0a6ab',
|
||||
background: 'rgba(var(--sb-red-rgb), 0.09)', border: '1px solid rgba(var(--sb-red-rgb), 0.26)', color: 'var(--sb-red-soft)',
|
||||
},
|
||||
}, failed
|
||||
? t('group.errNothingSent')
|
||||
@@ -284,8 +284,8 @@ export function CreateGroupModal({ candidates, relayOnly, onCreate, onCancel })
|
||||
style: {
|
||||
display: 'flex', alignItems: 'center', gap: '11px', padding: '10px 12px',
|
||||
borderRadius: '10px', cursor: full ? 'not-allowed' : 'pointer', textAlign: 'start',
|
||||
background: on ? 'rgba(240,137,42,0.1)' : 'transparent',
|
||||
border: `1px solid ${on ? 'rgba(240,137,42,0.32)' : C.line}`,
|
||||
background: on ? 'rgba(var(--sb-orange-rgb), 0.1)' : 'transparent',
|
||||
border: `1px solid ${on ? 'rgba(var(--sb-orange-rgb), 0.32)' : C.line}`,
|
||||
opacity: full ? 0.4 : 1, fontFamily: 'inherit',
|
||||
},
|
||||
}, [
|
||||
@@ -318,7 +318,7 @@ export function CreateGroupModal({ candidates, relayOnly, onCreate, onCancel })
|
||||
key: 'ip',
|
||||
style: {
|
||||
margin: 0, padding: '11px 13px', borderRadius: '9px', fontSize: '12.5px', lineHeight: 1.55,
|
||||
background: 'rgba(227,179,65,0.08)', border: '1px solid rgba(227,179,65,0.26)', color: '#e3b341',
|
||||
background: 'rgba(var(--sb-yellow-2-rgb), 0.08)', border: '1px solid rgba(var(--sb-yellow-2-rgb), 0.26)', color: 'var(--sb-yellow-2)',
|
||||
},
|
||||
}, t('group.relayOnlyOff')),
|
||||
|
||||
@@ -412,8 +412,8 @@ export function AddMembersModal({ candidates, remaining, onAdd, onCancel }) {
|
||||
style: {
|
||||
display: 'flex', alignItems: 'center', gap: '11px', padding: '10px 12px',
|
||||
borderRadius: '10px', cursor: full ? 'not-allowed' : 'pointer', textAlign: 'start',
|
||||
background: on ? 'rgba(240,137,42,0.1)' : 'transparent',
|
||||
border: `1px solid ${on ? 'rgba(240,137,42,0.32)' : C.line}`,
|
||||
background: on ? 'rgba(var(--sb-orange-rgb), 0.1)' : 'transparent',
|
||||
border: `1px solid ${on ? 'rgba(var(--sb-orange-rgb), 0.32)' : C.line}`,
|
||||
opacity: full ? 0.4 : 1, fontFamily: 'inherit',
|
||||
},
|
||||
}, [
|
||||
@@ -521,7 +521,7 @@ function MemberStrip({ group, onRemove, isAdmin }) {
|
||||
// fact that a dropped connection does not change — but the chip
|
||||
// says plainly that nothing sent now reaches them.
|
||||
background: lost ? 'transparent' : C.panel2,
|
||||
border: `1px solid ${lost ? 'rgba(229,114,122,0.3)' : C.line}`,
|
||||
border: `1px solid ${lost ? 'rgba(var(--sb-red-rgb), 0.3)' : C.line}`,
|
||||
fontSize: '12.5px', color: self ? C.ink : (lost ? C.ink3 : C.ink2),
|
||||
opacity: lost ? 0.75 : 1,
|
||||
},
|
||||
@@ -570,8 +570,8 @@ function Bubble({ msg }) {
|
||||
key: 'b',
|
||||
style: {
|
||||
padding: '9px 13px', borderRadius: mine ? '13px 13px 4px 13px' : '13px 13px 13px 4px',
|
||||
background: mine ? 'rgba(240,137,42,0.14)' : C.panel2,
|
||||
border: `1px solid ${mine ? 'rgba(240,137,42,0.26)' : C.line}`,
|
||||
background: mine ? 'rgba(var(--sb-orange-rgb), 0.14)' : C.panel2,
|
||||
border: `1px solid ${mine ? 'rgba(var(--sb-orange-rgb), 0.26)' : C.line}`,
|
||||
color: C.ink, fontSize: '14.5px', lineHeight: 1.5, wordBreak: 'break-word', whiteSpace: 'pre-wrap',
|
||||
},
|
||||
}, msg.message),
|
||||
@@ -643,8 +643,8 @@ export function GroupChatView({
|
||||
key: 'av',
|
||||
style: {
|
||||
flex: 'none', width: '38px', height: '38px', borderRadius: '11px', display: 'grid',
|
||||
placeItems: 'center', background: 'rgba(240,137,42,0.12)',
|
||||
border: '1px solid rgba(240,137,42,0.24)', color: C.accent,
|
||||
placeItems: 'center', background: 'rgba(var(--sb-orange-rgb), 0.12)',
|
||||
border: '1px solid rgba(var(--sb-orange-rgb), 0.24)', color: C.accent,
|
||||
fontFamily: C.mono, fontSize: '12px', fontWeight: 700,
|
||||
},
|
||||
}, groupInitials(group.name)),
|
||||
@@ -693,7 +693,7 @@ export function GroupChatView({
|
||||
h('button', {
|
||||
key: 'leave', className: 'sb-gh-btn', onClick: onLeave, title: t('group.leaveThis'),
|
||||
'aria-label': t('group.leaveThis'),
|
||||
style: { ...btn(false), flex: 'none', padding: '8px 12px', fontSize: '12.5px', color: C.bad, borderColor: 'rgba(229,114,122,0.3)' },
|
||||
style: { ...btn(false), flex: 'none', padding: '8px 12px', fontSize: '12.5px', color: C.bad, borderColor: 'rgba(var(--sb-red-rgb), 0.3)' },
|
||||
}, [
|
||||
svg(ICON.exit, { key: 'i' }),
|
||||
h('span', { key: 'l', className: 'sb-gh-label' }, t('group.leave')),
|
||||
@@ -706,7 +706,7 @@ export function GroupChatView({
|
||||
key: 'relay-note',
|
||||
style: {
|
||||
flex: 'none', padding: '8px 16px', fontSize: '12px', lineHeight: 1.5, color: C.warn,
|
||||
background: 'rgba(227,179,65,0.08)', borderBottom: `1px solid ${C.line}`,
|
||||
background: 'rgba(var(--sb-yellow-2-rgb), 0.08)', borderBottom: `1px solid ${C.line}`,
|
||||
},
|
||||
}, t('group.relayNote')),
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { t } from '../../i18n/index.js';
|
||||
// instead of embedding the whole manifest.
|
||||
import { version as packageVersion } from '../../../package.json';
|
||||
import { LanguageSwitcher } from './LanguageSwitcher.jsx';
|
||||
import { ThemeSwitcher } from './ThemeSwitcher.jsx';
|
||||
|
||||
const APP_VERSION = `v${packageVersion}`;
|
||||
|
||||
@@ -362,7 +363,7 @@ const EnhancedMinimalHeader = ({
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
background: rgba(var(--sb-scrim-rgb), 0.8);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -372,15 +373,15 @@ const EnhancedMinimalHeader = ({
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
background: var(--sb-legacy-panel);
|
||||
color: var(--sb-text-1);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
white-space: pre-line;
|
||||
border: 1px solid #333;
|
||||
border: 1px solid var(--sb-surface-5);
|
||||
`;
|
||||
|
||||
content.textContent = message;
|
||||
@@ -516,12 +517,15 @@ const EnhancedMinimalHeader = ({
|
||||
// ============================================
|
||||
|
||||
const secColor = displaySecurityLevel
|
||||
? (displaySecurityLevel.color === 'green' ? '#3ecf8e' : displaySecurityLevel.color === 'orange' ? '#f0892a' : displaySecurityLevel.color === 'yellow' ? '#e3c84e' : '#e5727a')
|
||||
: '#3ecf8e';
|
||||
const dotColor = isConnected ? '#3ecf8e'
|
||||
: (['connecting', 'verifying', 'retrying', 'reconnecting'].includes(status) ? '#e3c84e'
|
||||
: (status === 'failed' ? '#e5727a' : '#6b6b73'));
|
||||
const dotGlow = dotColor === '#3ecf8e' ? 'rgba(62,207,142,0.16)' : dotColor === '#e3c84e' ? 'rgba(227,200,78,0.16)' : dotColor === '#e5727a' ? 'rgba(229,114,122,0.16)' : 'rgba(107,107,115,0.16)';
|
||||
? (displaySecurityLevel.color === 'green' ? 'var(--sb-green)' : displaySecurityLevel.color === 'orange' ? 'var(--sb-orange)' : displaySecurityLevel.color === 'yellow' ? 'var(--sb-yellow)' : 'var(--sb-red)')
|
||||
: 'var(--sb-green)';
|
||||
// The dot is a mark, not a label, so it takes the -solid accents: the brand colour
|
||||
// in both themes. The text-safe accents darken on a light ground, which is right for
|
||||
// a word and wrong for a 7px circle.
|
||||
const dotColor = isConnected ? 'var(--sb-green-solid)'
|
||||
: (['connecting', 'verifying', 'retrying', 'reconnecting'].includes(status) ? 'var(--sb-yellow-solid)'
|
||||
: (status === 'failed' ? 'var(--sb-red-solid)' : 'var(--sb-text-9)'));
|
||||
const dotGlow = dotColor === 'var(--sb-green-solid)' ? 'rgba(var(--sb-green-rgb), 0.16)' : dotColor === 'var(--sb-yellow-solid)' ? 'rgba(var(--sb-yellow-rgb), 0.16)' : dotColor === 'var(--sb-red-solid)' ? 'rgba(var(--sb-red-rgb), 0.16)' : 'rgba(var(--sb-text-9-rgb), 0.16)';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
|
||||
// On the landing / setup view (no verified connection) the new Start Secure
|
||||
@@ -555,9 +559,9 @@ const EnhancedMinimalHeader = ({
|
||||
const overlay = { position: 'fixed', top: 0, left: 0, right: 0, ...safeTop };
|
||||
const headerStyle = onLanding
|
||||
? (scrolled
|
||||
? { ...overlay, background: 'rgba(15,15,17,0.72)', backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: '1px solid rgba(255,255,255,0.06)', transition: MATERIALISE }
|
||||
? { ...overlay, background: 'rgba(var(--sb-bg-rgb), 0.72)', backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: '1px solid rgba(var(--sb-ink), 0.06)', transition: MATERIALISE }
|
||||
: { ...overlay, background: 'transparent', backdropFilter: 'blur(0px) saturate(100%)', WebkitBackdropFilter: 'blur(0px) saturate(100%)', borderBottom: '1px solid transparent', transition: MATERIALISE })
|
||||
: { ...safeTop, background: 'rgba(18,18,20,0.72)', backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: '1px solid rgba(255,255,255,0.06)' };
|
||||
: { ...safeTop, background: 'rgba(var(--sb-bg-rgb), 0.72)', backdropFilter: GLASS, WebkitBackdropFilter: GLASS, borderBottom: '1px solid rgba(var(--sb-ink), 0.06)' };
|
||||
|
||||
return React.createElement('header', {
|
||||
className: onLanding ? 'header-minimal z-50' : 'header-minimal sticky top-0 z-50',
|
||||
@@ -580,10 +584,10 @@ const EnhancedMinimalHeader = ({
|
||||
),
|
||||
React.createElement('div', { key: 'txt', style: { lineHeight: 1.2, minWidth: 0 } }, [
|
||||
React.createElement('div', { key: 'r1', style: { display: 'flex', alignItems: 'baseline', gap: '7px' } }, [
|
||||
React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: '#e8e8eb' } }, 'SecureBit'),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, APP_VERSION)
|
||||
React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: 'var(--sb-text-2)' } }, 'SecureBit'),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: 'var(--sb-text-faint)' } }, APP_VERSION)
|
||||
]),
|
||||
React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: '#6b6b73', fontWeight: 500 } }, t('hdr.tagline'))
|
||||
React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: 'var(--sb-text-9)', fontWeight: 500 } }, t('hdr.tagline'))
|
||||
])
|
||||
]),
|
||||
// Right: controls
|
||||
@@ -592,33 +596,38 @@ const EnhancedMinimalHeader = ({
|
||||
// tear down an live peer connection if offered inside the chat.
|
||||
onLanding && React.createElement(LanguageSwitcher, { key: 'lang' }),
|
||||
|
||||
// Landing only, alongside the language switcher: the connected
|
||||
// header has no room left, and the preference survives the move
|
||||
// into the chat regardless of where it was set.
|
||||
onLanding && React.createElement(ThemeSwitcher, { key: 'theme' }),
|
||||
|
||||
!onLanding && React.createElement('button', {
|
||||
key: 'net', type: 'button',
|
||||
onClick: () => window.dispatchEvent(new CustomEvent('securebit:open-network-settings')),
|
||||
title: t('hdr.netSettingsTitle'), 'aria-label': t('hdr.netSettings'),
|
||||
className: 'sb-disconnect',
|
||||
style: { display: 'grid', placeItems: 'center', width: '38px', height: '38px', borderRadius: '9px', border: '1px solid rgba(255,255,255,0.07)', background: 'rgba(255,255,255,0.02)', color: '#9a9aa2', cursor: 'pointer', transition: 'all .15s' }
|
||||
style: { display: 'grid', placeItems: 'center', width: '38px', height: '38px', borderRadius: '9px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'rgba(var(--sb-ink), 0.02)', color: 'var(--sb-text-6)', cursor: 'pointer', transition: 'all .15s' }
|
||||
}, React.createElement('i', { className: 'fas fa-network-wired', style: { fontSize: '13px' } })),
|
||||
|
||||
(!onLanding && displaySecurityLevel) && React.createElement('div', {
|
||||
key: 'sec', onClick: handleSecurityClick,
|
||||
onContextMenu: (e) => { e.preventDefault(); if (typeof onDisconnect === 'function') onDisconnect(); },
|
||||
title: securityDetails.tooltip, className: 'sb-secpill',
|
||||
style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '7px 12px', borderRadius: '9px', border: '1px solid rgba(255,255,255,0.07)', background: 'rgba(255,255,255,0.02)', cursor: 'pointer' }
|
||||
style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '7px 12px', borderRadius: '9px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'rgba(var(--sb-ink), 0.02)', cursor: 'pointer' }
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-shield-halved', style: { fontSize: '13px', color: secColor } }),
|
||||
React.createElement('span', { key: 'l', className: 'hidden sm:inline', style: { fontSize: '12.5px', fontWeight: 600, color: '#e8e8eb' } }, (t(`secLevel.${displaySecurityLevel.level}`) === `secLevel.${displaySecurityLevel.level}` ? String(displaySecurityLevel.level) : t(`secLevel.${displaySecurityLevel.level}`))),
|
||||
React.createElement('span', { key: 's', style: { fontFamily: MONO, fontSize: '11.5px', color: '#8a8a92' } }, displaySecurityLevel.score + '%')
|
||||
React.createElement('span', { key: 'l', className: 'hidden sm:inline', style: { fontSize: '12.5px', fontWeight: 600, color: 'var(--sb-text-2)' } }, (t(`secLevel.${displaySecurityLevel.level}`) === `secLevel.${displaySecurityLevel.level}` ? String(displaySecurityLevel.level) : t(`secLevel.${displaySecurityLevel.level}`))),
|
||||
React.createElement('span', { key: 's', style: { fontFamily: MONO, fontSize: '11.5px', color: 'var(--sb-text-7)' } }, displaySecurityLevel.score + '%')
|
||||
]),
|
||||
|
||||
!onLanding && React.createElement('div', { key: 'status', style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 13px', borderRadius: '9px', border: '1px solid rgba(255,255,255,0.07)', background: 'rgba(255,255,255,0.02)' } }, [
|
||||
!onLanding && React.createElement('div', { key: 'status', style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 13px', borderRadius: '9px', border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'rgba(var(--sb-ink), 0.02)' } }, [
|
||||
React.createElement('span', { key: 'dot', style: { width: '7px', height: '7px', borderRadius: '50%', background: dotColor, boxShadow: '0 0 0 3px ' + dotGlow } }),
|
||||
React.createElement('span', { key: 't', className: 'hidden sm:inline', style: { fontSize: '13px', fontWeight: 600, color: '#cfcfd4' } }, config.text)
|
||||
React.createElement('span', { key: 't', className: 'hidden sm:inline', style: { fontSize: '13px', fontWeight: 600, color: 'var(--sb-text-4)' } }, config.text)
|
||||
]),
|
||||
|
||||
isConnected && React.createElement('button', {
|
||||
key: 'dc', onClick: onDisconnect, className: 'sb-disconnect',
|
||||
style: { display: 'flex', alignItems: 'center', gap: '7px', padding: '8px 14px', borderRadius: '9px', border: '1px solid rgba(255,255,255,0.08)', background: 'transparent', color: '#9a9aa2', fontFamily: 'inherit', fontSize: '13px', fontWeight: 600, cursor: 'pointer', transition: 'all .15s' }
|
||||
style: { display: 'flex', alignItems: 'center', gap: '7px', padding: '8px 14px', borderRadius: '9px', border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'transparent', color: 'var(--sb-text-6)', fontFamily: 'inherit', fontSize: '13px', fontWeight: 600, cursor: 'pointer', transition: 'all .15s' }
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-power-off', style: { fontSize: '12px' } }),
|
||||
React.createElement('span', { key: 't', className: 'sb-hide-sm' }, t('hdr.disconnect'))
|
||||
|
||||
@@ -107,8 +107,11 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
|
||||
// ── Design import: dark slide-up overlay (orange/green accents) ──────────
|
||||
const h = React.createElement;
|
||||
const C_ORANGE = '#f0892a';
|
||||
const C_GREEN = '#3ecf8e';
|
||||
const C_ORANGE = 'var(--sb-orange)';
|
||||
const C_GREEN = 'var(--sb-green)';
|
||||
// Fill forms of the two: brand colour in both themes. See app.jsx.
|
||||
const C_ORANGE_SOLID = 'var(--sb-orange-solid)';
|
||||
const C_GREEN_SOLID = 'var(--sb-green-solid)';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
|
||||
// radio card (public / own servers)
|
||||
@@ -117,16 +120,16 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
style: Object.assign({
|
||||
width: '100%', textAlign: 'start', display: 'flex', alignItems: 'flex-start', gap: '12px',
|
||||
padding: '14px 15px', borderRadius: '13px',
|
||||
border: `1px solid ${selected ? 'rgba(240,137,42,0.45)' : 'rgba(255,255,255,0.07)'}`,
|
||||
background: selected ? 'rgba(240,137,42,0.06)' : '#141416',
|
||||
border: `1px solid ${selected ? 'rgba(var(--sb-orange-rgb), 0.45)' : 'rgba(var(--sb-ink), 0.07)'}`,
|
||||
background: selected ? 'rgba(var(--sb-orange-rgb), 0.06)' : 'var(--sb-surface)',
|
||||
color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', transition: 'all .15s', marginBottom: '10px'
|
||||
}, extraStyle || {})
|
||||
}, [
|
||||
h('span', { key: 'ring', style: { flex: 'none', width: '18px', height: '18px', marginTop: '1px', borderRadius: '50%', border: `1.5px solid ${selected ? C_ORANGE : 'rgba(255,255,255,0.22)'}`, display: 'grid', placeItems: 'center' } },
|
||||
h('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: selected ? C_ORANGE : 'transparent' } })),
|
||||
h('span', { key: 'ring', style: { flex: 'none', width: '18px', height: '18px', marginTop: '1px', borderRadius: '50%', border: `1.5px solid ${selected ? C_ORANGE : 'rgba(var(--sb-ink), 0.22)'}`, display: 'grid', placeItems: 'center' } },
|
||||
h('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: selected ? C_ORANGE_SOLID : 'transparent' } })),
|
||||
h('span', { key: 'tx', style: { flex: 1 } }, [
|
||||
h('span', { key: 't', style: { display: 'block', fontSize: '14px', fontWeight: 700, color: '#f4f4f6' } }, title),
|
||||
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', color: '#8a8a92', marginTop: '2px' } }, desc)
|
||||
h('span', { key: 't', style: { display: 'block', fontSize: '14px', fontWeight: 700, color: 'var(--sb-text-1)' } }, title),
|
||||
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', color: 'var(--sb-text-7)', marginTop: '2px' } }, desc)
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -136,67 +139,67 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
style: {
|
||||
width: '100%', textAlign: 'start', display: 'flex', alignItems: 'flex-start', gap: '12px',
|
||||
padding: '14px 15px', borderRadius: '13px',
|
||||
border: `1px solid ${on ? 'rgba(62,207,142,0.3)' : 'rgba(255,255,255,0.07)'}`,
|
||||
background: on ? 'rgba(62,207,142,0.05)' : '#141416',
|
||||
border: `1px solid ${on ? 'rgba(var(--sb-green-rgb), 0.3)' : 'rgba(var(--sb-ink), 0.07)'}`,
|
||||
background: on ? 'rgba(var(--sb-green-rgb), 0.05)' : 'var(--sb-surface)',
|
||||
color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', transition: 'all .15s', marginBottom: '10px'
|
||||
}
|
||||
}, [
|
||||
h('span', { key: 'tx', style: { flex: 1 } }, [
|
||||
h('span', { key: 'r1', style: { display: 'flex', alignItems: 'center', gap: '8px' } }, [
|
||||
h('span', { key: 't', style: { fontSize: '14px', fontWeight: 700, color: '#f4f4f6' } }, title),
|
||||
badge && h('span', { key: 'b', style: { fontSize: '10px', fontWeight: 700, color: C_GREEN, padding: '2px 7px', borderRadius: '5px', background: 'rgba(62,207,142,0.1)', border: '1px solid rgba(62,207,142,0.22)' } }, badge)
|
||||
h('span', { key: 't', style: { fontSize: '14px', fontWeight: 700, color: 'var(--sb-text-1)' } }, title),
|
||||
badge && h('span', { key: 'b', style: { fontSize: '10px', fontWeight: 700, color: C_GREEN, padding: '2px 7px', borderRadius: '5px', background: 'rgba(var(--sb-green-rgb), 0.1)', border: '1px solid rgba(var(--sb-green-rgb), 0.22)' } }, badge)
|
||||
]),
|
||||
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', lineHeight: 1.5, color: '#8a8a92', marginTop: '3px' } }, desc)
|
||||
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', lineHeight: 1.5, color: 'var(--sb-text-7)', marginTop: '3px' } }, desc)
|
||||
]),
|
||||
h('span', { key: 'tr', style: { flex: 'none', width: '42px', height: '24px', borderRadius: '99px', background: on ? (accent || C_GREEN) : 'rgba(255,255,255,0.08)', border: `1px solid ${on ? (accent || C_GREEN) : 'rgba(255,255,255,0.12)'}`, position: 'relative', transition: 'all .18s', marginTop: '1px' } },
|
||||
h('span', { key: 'tr', style: { flex: 'none', width: '42px', height: '24px', borderRadius: '99px', background: on ? (accent || C_GREEN_SOLID) : 'rgba(var(--sb-ink), 0.08)', border: `1px solid ${on ? (accent || C_GREEN) : 'rgba(var(--sb-ink), 0.12)'}`, position: 'relative', transition: 'all .18s', marginTop: '1px' } },
|
||||
h('span', { style: { position: 'absolute', top: '2px', insetInlineStart: '2px', width: '18px', height: '18px', borderRadius: '50%', background: '#fff', transform: on ? `translateX(${18 * direction()}px)` : 'translateX(0)', transition: 'transform .18s' } }))
|
||||
]);
|
||||
|
||||
// ── scrollable body ──
|
||||
const body = [];
|
||||
body.push(h('p', { key: 'intro', style: { margin: '0 0 18px', fontSize: '13.5px', lineHeight: 1.6, color: '#9a9aa2' } },
|
||||
body.push(h('p', { key: 'intro', style: { margin: '0 0 18px', fontSize: '13.5px', lineHeight: 1.6, color: 'var(--sb-text-6)' } },
|
||||
t('ice.intro')));
|
||||
body.push(radioCard(!useCustom, () => setUseCustom(false), t('ice.publicTitle'), t('ice.publicDesc')));
|
||||
body.push(radioCard(useCustom, () => setUseCustom(true), t('ice.customTitle'), t('ice.customDesc', { max: ICE_LIMITS.MAX_SERVERS }), useCustom ? { marginBottom: '14px' } : null));
|
||||
|
||||
if (useCustom) {
|
||||
const custom = [];
|
||||
custom.push(h('div', { key: 'ta', style: { borderRadius: '13px', border: '1px solid rgba(255,255,255,0.08)', background: '#0c0c0e', overflow: 'hidden', marginBottom: '12px' } },
|
||||
custom.push(h('div', { key: 'ta', style: { borderRadius: '13px', border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-bg-deep)', overflow: 'hidden', marginBottom: '12px' } },
|
||||
h('textarea', {
|
||||
dir: 'ltr',
|
||||
value: serversText, onChange: (e) => setServersText(e.target.value), rows: 5, spellCheck: false, autoComplete: 'off',
|
||||
placeholder: PLACEHOLDER,
|
||||
style: { width: '100%', resize: 'vertical', border: 'none', outline: 'none', background: 'transparent', color: '#c9ccd8', fontFamily: MONO, fontSize: '12px', lineHeight: 1.65, padding: '13px 14px', minHeight: '104px' }
|
||||
style: { width: '100%', resize: 'vertical', border: 'none', outline: 'none', background: 'transparent', color: 'var(--sb-text-code)', fontFamily: MONO, fontSize: '12px', lineHeight: 1.65, padding: '13px 14px', minHeight: '104px' }
|
||||
})));
|
||||
if (parsed.errors.length > 0) {
|
||||
custom.push(h('ul', { key: 'err', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: '#e5727a', fontSize: '12.5px' } },
|
||||
custom.push(h('ul', { key: 'err', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: 'var(--sb-red)', fontSize: '12.5px' } },
|
||||
parsed.errors.slice(0, 6).map((err, i) => h('li', { key: i }, err))));
|
||||
}
|
||||
if (parsed.warnings.length > 0) {
|
||||
custom.push(h('ul', { key: 'warn', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: '#e3c84e', fontSize: '12.5px' } },
|
||||
custom.push(h('ul', { key: 'warn', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: 'var(--sb-yellow)', fontSize: '12.5px' } },
|
||||
parsed.warnings.slice(0, 6).map((w, i) => h('li', { key: i }, w))));
|
||||
}
|
||||
if (parsed.servers.length > 0 && parsed.errors.length === 0) {
|
||||
custom.push(h('p', { key: 'ok', style: { margin: '0 0 10px', fontSize: '12.5px', color: C_GREEN } },
|
||||
`${parsed.servers.length} server(s) parsed${hasTurn ? ' (TURN present)' : ' (STUN only — does not hide IP)'}.`));
|
||||
}
|
||||
custom.push(h('div', { key: 'note', style: { display: 'flex', alignItems: 'flex-start', gap: '9px', padding: '12px 13px', borderRadius: '11px', border: '1px solid rgba(62,207,142,0.18)', background: 'rgba(62,207,142,0.05)', marginBottom: '12px' } }, [
|
||||
custom.push(h('div', { key: 'note', style: { display: 'flex', alignItems: 'flex-start', gap: '9px', padding: '12px 13px', borderRadius: '11px', border: '1px solid rgba(var(--sb-green-rgb), 0.18)', background: 'rgba(var(--sb-green-rgb), 0.05)', marginBottom: '12px' } }, [
|
||||
h('i', { key: 'i', className: 'fas fa-info-circle', style: { color: C_GREEN, fontSize: '13px', marginTop: '2px', flex: 'none' } }),
|
||||
h('span', { key: 't', style: { fontSize: '12px', lineHeight: 1.55, color: '#a8b8ae' } }, [
|
||||
h('span', { key: 't', style: { fontSize: '12px', lineHeight: 1.55, color: 'var(--sb-green-muted)' } }, [
|
||||
t('ice.turnNote'),
|
||||
h('span', { key: 'm', style: { fontFamily: MONO, color: C_GREEN } }, 'turns:'), t('ice.turnNoteTls')
|
||||
])
|
||||
]));
|
||||
const testColor = testState === 'done' && testResult && !testResult.error ? C_GREEN : '#cfcfd4';
|
||||
const testColor = testState === 'done' && testResult && !testResult.error ? C_GREEN : 'var(--sb-text-4)';
|
||||
custom.push(h('div', { key: 'test', style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', marginBottom: '4px' } }, [
|
||||
h('button', {
|
||||
key: 'btn', type: 'button', disabled: !canApply || testState === 'running', onClick: handleTest,
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '10px 15px', borderRadius: '10px', border: `1px solid ${testState === 'done' && testResult && !testResult.error ? 'rgba(62,207,142,0.4)' : 'rgba(255,255,255,0.1)'}`, background: testState === 'done' && testResult && !testResult.error ? 'rgba(62,207,142,0.08)' : 'rgba(255,255,255,0.04)', color: testColor, fontFamily: 'inherit', fontSize: '13px', fontWeight: 600, cursor: (!canApply || testState === 'running') ? 'not-allowed' : 'pointer', opacity: (!canApply || testState === 'running') ? 0.6 : 1 }
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '10px 15px', borderRadius: '10px', border: `1px solid ${testState === 'done' && testResult && !testResult.error ? 'rgba(var(--sb-green-rgb), 0.4)' : 'rgba(var(--sb-ink), 0.1)'}`, background: testState === 'done' && testResult && !testResult.error ? 'rgba(var(--sb-green-rgb), 0.08)' : 'rgba(var(--sb-ink), 0.04)', color: testColor, fontFamily: 'inherit', fontSize: '13px', fontWeight: 600, cursor: (!canApply || testState === 'running') ? 'not-allowed' : 'pointer', opacity: (!canApply || testState === 'running') ? 0.6 : 1 }
|
||||
}, [
|
||||
h('i', { key: 'i', className: testState === 'running' ? 'fas fa-circle-notch' : 'fas fa-play-circle', style: testState === 'running' ? { animation: 'sbSpin 1s linear infinite' } : null }),
|
||||
testState === 'running' ? t('ice.testing') : t('ice.test')
|
||||
]),
|
||||
(testState === 'done' && testResult) ? h('span', { key: 'res', style: { fontSize: '12px', color: testResult.error ? '#e5727a' : '#8a8a92' } },
|
||||
(testState === 'done' && testResult) ? h('span', { key: 'res', style: { fontSize: '12px', color: testResult.error ? 'var(--sb-red)' : 'var(--sb-text-7)' } },
|
||||
testResult.error
|
||||
? `Test failed: ${testResult.error}`
|
||||
: (testResult.srflx > 0 || testResult.relay > 0)
|
||||
@@ -210,7 +213,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
body.push(toggleRow(relayOnly, () => setRelayOnly(!relayOnly), t('ice.relayTitle'),
|
||||
t('ice.relayDesc'), C_GREEN, t('ice.relayBadge')));
|
||||
if (relayOnly && useCustom && !hasTurn) {
|
||||
body.push(h('p', { key: 'relaywarn', style: { margin: '-4px 0 10px', fontSize: '12.5px', color: '#e3c84e' } },
|
||||
body.push(h('p', { key: 'relaywarn', style: { margin: '-4px 0 10px', fontSize: '12.5px', color: 'var(--sb-yellow)' } },
|
||||
t('ice.relayWarning')));
|
||||
}
|
||||
body.push(toggleRow(persist, () => setPersist(!persist), t('ice.persist'),
|
||||
@@ -220,38 +223,38 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
|
||||
const footerBtns = [];
|
||||
if (hasSaved) {
|
||||
footerBtns.push(h('button', { key: 'forget', type: 'button', onClick: handleForget,
|
||||
style: { marginInlineEnd: 'auto', padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(229,114,122,0.3)', background: 'transparent', color: '#e5727a', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, t('ice.forget')));
|
||||
style: { marginInlineEnd: 'auto', padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(var(--sb-red-rgb), 0.3)', background: 'transparent', color: 'var(--sb-red)', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, t('ice.forget')));
|
||||
}
|
||||
footerBtns.push(h('button', { key: 'cancel', type: 'button', onClick: onClose,
|
||||
style: { padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(255,255,255,0.1)', background: 'transparent', color: '#b3b3ba', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, t('ice.cancel')));
|
||||
style: { padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(var(--sb-ink), 0.1)', background: 'transparent', color: 'var(--sb-text-5)', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, t('ice.cancel')));
|
||||
footerBtns.push(h('button', { key: 'apply', type: 'button', onClick: handleApply, disabled: !canApply,
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '11px 20px', borderRadius: '11px', border: 'none', background: C_ORANGE, color: '#1a0f04', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 700, cursor: canApply ? 'pointer' : 'not-allowed', opacity: canApply ? 1 : 0.5, boxShadow: '0 6px 18px rgba(240,137,42,0.28)' } }, [
|
||||
style: { display: 'inline-flex', alignItems: 'center', gap: '8px', padding: '11px 20px', borderRadius: '11px', border: 'none', background: C_ORANGE_SOLID, color: 'var(--sb-on-accent)', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 700, cursor: canApply ? 'pointer' : 'not-allowed', opacity: canApply ? 1 : 0.5, boxShadow: '0 6px 18px rgba(var(--sb-orange-rgb), 0.28)' } }, [
|
||||
h('i', { key: 'i', className: 'fas fa-check' }), t('ice.apply')
|
||||
]));
|
||||
|
||||
// Embedded mode (default for the new design): fill the connection screen's
|
||||
// right column and slide up over it. Fallback: a fixed right-side drawer.
|
||||
const wrapperStyle = embedded
|
||||
? { position: 'absolute', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', background: '#0f0f11', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' }
|
||||
: { position: 'fixed', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', alignItems: 'stretch', background: '#0f0f11', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' };
|
||||
? { position: 'absolute', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', background: 'var(--sb-bg)', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' }
|
||||
: { position: 'fixed', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', alignItems: 'stretch', background: 'var(--sb-bg)', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' };
|
||||
|
||||
return h('div', { className: 'sb-ice-overlay', style: wrapperStyle }, [
|
||||
h(React.Fragment, { key: 'panel' }, [
|
||||
// header
|
||||
h('div', { key: 'head', style: { display: 'flex', alignItems: 'center', gap: '12px', padding: '20px 24px', borderBottom: '1px solid rgba(255,255,255,0.06)' } }, [
|
||||
h('div', { key: 'ic', style: { width: '38px', height: '38px', flex: 'none', display: 'grid', placeItems: 'center', borderRadius: '10px', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)' } },
|
||||
h('i', { className: 'fas fa-sliders-h', style: { color: '#cfcfd4', fontSize: '15px' } })),
|
||||
h('div', { key: 'head', style: { display: 'flex', alignItems: 'center', gap: '12px', padding: '20px 24px', borderBottom: '1px solid rgba(var(--sb-ink), 0.06)' } }, [
|
||||
h('div', { key: 'ic', style: { width: '38px', height: '38px', flex: 'none', display: 'grid', placeItems: 'center', borderRadius: '10px', background: 'rgba(var(--sb-ink), 0.03)', border: '1px solid rgba(var(--sb-ink), 0.06)' } },
|
||||
h('i', { className: 'fas fa-sliders-h', style: { color: 'var(--sb-text-4)', fontSize: '15px' } })),
|
||||
h('div', { key: 'tx', style: { flex: 1, lineHeight: 1.25 } }, [
|
||||
h('div', { key: 't', style: { fontSize: '16.5px', fontWeight: 800, letterSpacing: '-0.3px', color: '#f4f4f6' } }, t('ice.title')),
|
||||
h('div', { key: 's', style: { fontSize: '12px', color: '#7b7b83' } }, t('ice.subtitle'))
|
||||
h('div', { key: 't', style: { fontSize: '16.5px', fontWeight: 800, letterSpacing: '-0.3px', color: 'var(--sb-text-1)' } }, t('ice.title')),
|
||||
h('div', { key: 's', style: { fontSize: '12px', color: 'var(--sb-text-8)' } }, t('ice.subtitle'))
|
||||
]),
|
||||
h('button', { key: 'x', type: 'button', onClick: onClose, style: { width: '32px', height: '32px', flex: 'none', display: 'grid', placeItems: 'center', borderRadius: '9px', border: 'none', background: 'rgba(255,255,255,0.04)', color: '#8a8a92', cursor: 'pointer' } },
|
||||
h('button', { key: 'x', type: 'button', onClick: onClose, style: { width: '32px', height: '32px', flex: 'none', display: 'grid', placeItems: 'center', borderRadius: '9px', border: 'none', background: 'rgba(var(--sb-ink), 0.04)', color: 'var(--sb-text-7)', cursor: 'pointer' } },
|
||||
h('i', { className: 'fas fa-times' }))
|
||||
]),
|
||||
// scroll body
|
||||
h('div', { key: 'body', className: 'custom-scrollbar', style: { flex: 1, overflowY: 'auto', padding: '20px 24px' } }, body),
|
||||
// footer
|
||||
h('div', { key: 'foot', style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '10px', padding: '16px 24px', borderTop: '1px solid rgba(255,255,255,0.06)', background: '#0e0e10', borderRadius: '0' } }, footerBtns)
|
||||
h('div', { key: 'foot', style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '10px', padding: '16px 24px', borderTop: '1px solid rgba(var(--sb-ink), 0.06)', background: 'var(--sb-bg-deep)', borderRadius: '0' } }, footerBtns)
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ const LanguageSuggestion = () => {
|
||||
key: 'line',
|
||||
lang: meta.htmlLang || target,
|
||||
dir,
|
||||
style: { fontSize: '13px', lineHeight: 1.45, color: '#cfcfd4', textAlign: 'start' },
|
||||
style: { fontSize: '13px', lineHeight: 1.45, color: 'var(--sb-text-4)', textAlign: 'start' },
|
||||
}, t('language.suggest.text', null, target));
|
||||
|
||||
const link = React.createElement('a', {
|
||||
@@ -87,8 +87,8 @@ const LanguageSuggestion = () => {
|
||||
style: {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '6px',
|
||||
padding: '7px 12px', borderRadius: '9px',
|
||||
border: '1px solid rgba(240,137,42,0.30)', background: 'rgba(240,137,42,0.12)',
|
||||
color: '#f0892a', fontSize: '12.5px', fontWeight: 600,
|
||||
border: '1px solid rgba(var(--sb-orange-rgb), 0.30)', background: 'rgba(var(--sb-orange-rgb), 0.12)',
|
||||
color: 'var(--sb-orange)', fontSize: '12.5px', fontWeight: 600,
|
||||
textDecoration: 'none', whiteSpace: 'nowrap',
|
||||
},
|
||||
}, t('language.suggest.cta', null, target));
|
||||
@@ -101,8 +101,8 @@ const LanguageSuggestion = () => {
|
||||
'aria-label': t('language.suggest.dismiss', null, target),
|
||||
style: {
|
||||
padding: '7px 10px', borderRadius: '9px',
|
||||
border: '1px solid rgba(255,255,255,0.07)', background: 'rgba(255,255,255,0.02)',
|
||||
color: '#8a8a92', font: 'inherit', fontSize: '12.5px', fontWeight: 500,
|
||||
border: '1px solid rgba(var(--sb-ink), 0.07)', background: 'rgba(var(--sb-ink), 0.02)',
|
||||
color: 'var(--sb-text-7)', font: 'inherit', fontSize: '12.5px', fontWeight: 500,
|
||||
cursor: 'pointer', whiteSpace: 'nowrap',
|
||||
},
|
||||
}, t('language.suggest.dismiss', null, target));
|
||||
@@ -117,8 +117,8 @@ const LanguageSuggestion = () => {
|
||||
maxWidth: 'min(340px, calc(100vw - 48px))',
|
||||
display: 'flex', flexDirection: 'column', gap: '11px',
|
||||
padding: '14px 16px', borderRadius: '14px',
|
||||
border: '1px solid rgba(255,255,255,0.08)', background: '#161618',
|
||||
boxShadow: '0 16px 40px rgba(0,0,0,0.5)',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-surface)',
|
||||
boxShadow: '0 16px 40px rgba(var(--sb-shadow-rgb), calc(0.5 * var(--sb-shadow-k)))',
|
||||
opacity: shown ? 1 : 0,
|
||||
transform: shown || still ? 'none' : 'translateY(10px)',
|
||||
transition: still ? 'opacity .2s linear' : 'opacity .3s ease, transform .3s cubic-bezier(.2,.7,.3,1)',
|
||||
|
||||
@@ -50,9 +50,9 @@ const LanguageSwitcher = () => {
|
||||
style: {
|
||||
display: 'flex', alignItems: 'center', gap: '6px',
|
||||
padding: '7px 10px', borderRadius: '9px',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
background: open ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.02)',
|
||||
color: '#cfcfd4', font: 'inherit', fontSize: '12.5px', fontWeight: 600,
|
||||
border: '1px solid rgba(var(--sb-ink), 0.07)',
|
||||
background: open ? 'rgba(var(--sb-ink), 0.06)' : 'rgba(var(--sb-ink), 0.02)',
|
||||
color: 'var(--sb-text-4)', font: 'inherit', fontSize: '12.5px', fontWeight: 600,
|
||||
cursor: 'pointer', transition: 'background .15s, color .15s',
|
||||
},
|
||||
}, [
|
||||
@@ -77,8 +77,8 @@ const LanguageSwitcher = () => {
|
||||
position: 'absolute', top: 'calc(100% + 6px)', insetInlineEnd: 0, zIndex: 60,
|
||||
display: open ? 'block' : 'none',
|
||||
minWidth: '170px', padding: '5px', borderRadius: '11px',
|
||||
border: '1px solid rgba(255,255,255,0.08)', background: '#161618',
|
||||
boxShadow: '0 14px 34px rgba(0,0,0,0.45)',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-surface)',
|
||||
boxShadow: '0 14px 34px rgba(var(--sb-shadow-rgb), calc(0.45 * var(--sb-shadow-k)))',
|
||||
// Thirteen languages is taller than a phone in landscape. Cap it and scroll
|
||||
// rather than let the list run off the bottom, where the last few entries
|
||||
// would be unreachable.
|
||||
@@ -104,14 +104,14 @@ const LanguageSwitcher = () => {
|
||||
padding: '8px 10px', borderRadius: '8px',
|
||||
fontSize: '13px',
|
||||
fontWeight: link.isCurrent ? 600 : 500,
|
||||
color: link.isCurrent ? '#e8e8eb' : '#9a9aa2',
|
||||
background: link.isCurrent ? 'rgba(255,255,255,0.06)' : 'transparent',
|
||||
color: link.isCurrent ? 'var(--sb-text-2)' : 'var(--sb-text-6)',
|
||||
background: link.isCurrent ? 'rgba(var(--sb-ink), 0.06)' : 'transparent',
|
||||
textDecoration: 'none', whiteSpace: 'nowrap',
|
||||
},
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'a',
|
||||
style: { fontSize: '11px', fontWeight: 700, letterSpacing: '0.4px', color: '#6b6b73', width: '22px', flex: 'none', textAlign: 'start' },
|
||||
style: { fontSize: '11px', fontWeight: 700, letterSpacing: '0.4px', color: 'var(--sb-text-9)', width: '22px', flex: 'none', textAlign: 'start' },
|
||||
}, link.abbr),
|
||||
React.createElement('span', { key: 'n' }, link.label),
|
||||
])));
|
||||
|
||||
@@ -44,22 +44,32 @@ function Roadmap() {
|
||||
features: tList(`roadmap.${d.k}.features`)
|
||||
}));
|
||||
|
||||
// `rgb` is the same colour as `color`, published as bare channels. The pill draws
|
||||
// its fill and border from it at low alpha, and there is no way to add an alpha to
|
||||
// a colour that a custom property has already resolved — hence the pair, which has
|
||||
// to be kept in step.
|
||||
// Three forms of the same status colour, because they are used three ways and only
|
||||
// one of them is text. `color` is the word — it darkens in the light theme so it can
|
||||
// be read on white. `solid` is the dot, which is a mark rather than a label and
|
||||
// stays the brand colour in both themes. `rgb` is the channels, for the pill's fill
|
||||
// and border at low alpha. The three have to be kept in step.
|
||||
const META = {
|
||||
released: { word: t('roadmap.status.released'), color: "#3ecf8e", line: "rgba(62,207,142,0.32)" },
|
||||
current: { word: t('roadmap.status.current'), color: "#f0892a", line: "rgba(240,137,42,0.32)" },
|
||||
dev: { word: t('roadmap.status.dev'), color: "#e3b341", line: "rgba(255,255,255,0.08)" },
|
||||
planned: { word: t('roadmap.status.planned'), color: "#8a8a92", line: "rgba(255,255,255,0.08)" },
|
||||
research: { word: t('roadmap.status.research'), color: "#6b6b73", line: "rgba(255,255,255,0.08)" }
|
||||
released: { word: t('roadmap.status.released'), color: "var(--sb-green)", solid: "var(--sb-green-solid)", rgb: "var(--sb-green-rgb)", line: "rgba(var(--sb-green-rgb), 0.32)" },
|
||||
current: { word: t('roadmap.status.current'), color: "var(--sb-orange)", solid: "var(--sb-orange-solid)", rgb: "var(--sb-orange-rgb)", line: "rgba(var(--sb-orange-rgb), 0.32)" },
|
||||
dev: { word: t('roadmap.status.dev'), color: "var(--sb-yellow-2)", solid: "var(--sb-yellow-2-solid)", rgb: "var(--sb-yellow-2-rgb)", line: "rgba(var(--sb-ink), 0.08)" },
|
||||
planned: { word: t('roadmap.status.planned'), color: "var(--sb-text-7)", solid: "var(--sb-text-7)", rgb: "var(--sb-text-7-rgb)", line: "rgba(var(--sb-ink), 0.08)" },
|
||||
research: { word: t('roadmap.status.research'), color: "var(--sb-text-9)", solid: "var(--sb-text-9)", rgb: "var(--sb-text-9-rgb)", line: "rgba(var(--sb-ink), 0.08)" }
|
||||
};
|
||||
|
||||
const [open, setOpen] = React.useState({});
|
||||
const isOpen = (i) => (open[i] === undefined ? DATA[i].status === 'current' : open[i]);
|
||||
const toggle = (i) => setOpen((s) => ({ ...s, [i]: !isOpen(i) }));
|
||||
|
||||
const hexA = (hex, a) => {
|
||||
const n = parseInt(hex.slice(1), 16);
|
||||
return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`;
|
||||
};
|
||||
// Was hexA(), which took the literal hex out of META and split it into channels by
|
||||
// hand. Those literals are custom properties now, so there is nothing to parse at
|
||||
// runtime — the browser does the substitution instead, and it does it again when
|
||||
// the theme changes, which the old version could not.
|
||||
const tint = (channels, a) => `rgba(${channels}, ${a})`;
|
||||
|
||||
const total = DATA.length;
|
||||
const shipped = DATA.filter((d) => d.status === 'released' || d.status === 'current').length;
|
||||
@@ -75,53 +85,53 @@ function Roadmap() {
|
||||
const renderNode = (status) => {
|
||||
if (status === 'released') {
|
||||
return (
|
||||
<div style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(62,207,142,0.16),rgba(62,207,142,0.16)), #0f0f11', border: '1px solid rgba(62,207,142,0.4)', zIndex: 2 }}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#3ecf8e" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M5 13l4 4 10-11" /></svg>
|
||||
<div style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(var(--sb-green-rgb), 0.16),rgba(var(--sb-green-rgb), 0.16)), var(--sb-bg)', border: '1px solid rgba(var(--sb-green-rgb), 0.4)', zIndex: 2 }}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--sb-green-solid)" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M5 13l4 4 10-11" /></svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (status === 'current') {
|
||||
return (
|
||||
<div style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(240,137,42,0.2),rgba(240,137,42,0.2)), #0f0f11', border: '1px solid #f0892a', zIndex: 2, animation: 'rmPulse 2.4s ease-out infinite' }}>
|
||||
<span style={{ width: '9px', height: '9px', borderRadius: '50%', background: '#f0892a' }} />
|
||||
<div style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(var(--sb-orange-rgb), 0.2),rgba(var(--sb-orange-rgb), 0.2)), var(--sb-bg)', border: '1px solid var(--sb-orange-solid)', zIndex: 2, animation: 'rmPulse 2.4s ease-out infinite' }}>
|
||||
<span style={{ width: '9px', height: '9px', borderRadius: '50%', background: 'var(--sb-orange-solid)' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (status === 'dev') {
|
||||
return (
|
||||
<div style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(227,179,65,0.15),rgba(227,179,65,0.15)), #0f0f11', border: '1px solid rgba(227,179,65,0.4)', zIndex: 2 }}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#e3b341" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3a9 9 0 1 0 9 9" /></svg>
|
||||
<div style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(var(--sb-yellow-2-rgb), 0.15),rgba(var(--sb-yellow-2-rgb), 0.15)), var(--sb-bg)', border: '1px solid rgba(var(--sb-yellow-2-rgb), 0.4)', zIndex: 2 }}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--sb-yellow-2-solid)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3a9 9 0 1 0 9 9" /></svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// planned / research
|
||||
return (
|
||||
<div style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: '#0f0f11', border: `1px ${status === 'research' ? 'dashed' : 'solid'} rgba(255,255,255,0.18)`, zIndex: 2 }}>
|
||||
<span style={{ width: '7px', height: '7px', borderRadius: '50%', background: META[status].color }} />
|
||||
<div style={{ position: 'absolute', insetInlineStart: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'var(--sb-bg)', border: `1px ${status === 'research' ? 'dashed' : 'solid'} rgba(var(--sb-ink), 0.18)`, zIndex: 2 }}>
|
||||
<span style={{ width: '7px', height: '7px', borderRadius: '50%', background: META[status].solid }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section style={{ width: '100%', color: '#e8e8eb', fontFamily: SANS, padding: isMobile ? '48px 0' : '64px 0', background: 'radial-gradient(1200px 720px at 50% -8%, rgba(240,137,42,0.05), transparent 60%), #0f0f11' }}>
|
||||
<style dangerouslySetInnerHTML={{ __html: '@keyframes rmExp{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes rmPulse{0%,100%{box-shadow:0 0 0 0 rgba(240,137,42,0.18)}60%{box-shadow:0 0 0 9px rgba(240,137,42,0)}}' }} />
|
||||
<section style={{ width: '100%', color: 'var(--sb-text-2)', fontFamily: SANS, padding: isMobile ? '48px 0' : '64px 0', background: 'radial-gradient(1200px 720px at 50% -8%, rgba(var(--sb-orange-rgb), 0.05), transparent 60%), var(--sb-bg)' }}>
|
||||
<style dangerouslySetInnerHTML={{ __html: '@keyframes rmExp{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes rmPulse{0%,100%{box-shadow:0 0 0 0 rgba(var(--sb-orange-rgb), 0.18)}60%{box-shadow:0 0 0 9px rgba(var(--sb-orange-rgb), 0)}}' }} />
|
||||
|
||||
<div style={{ maxWidth: '1040px', margin: '0 auto', padding: isMobile ? '0 18px' : '0 40px' }}>
|
||||
|
||||
{/* header */}
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<div style={{ fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.6px', marginBottom: '13px' }}>{t('roadmap.eyebrow')}</div>
|
||||
<h2 style={{ margin: '0 0 14px', fontSize: isMobile ? '27px' : '34px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.08, color: '#f4f4f6' }}>{t('roadmap.heading')}</h2>
|
||||
<p style={{ margin: 0, fontSize: '15.5px', lineHeight: 1.6, color: '#8a8a92', maxWidth: '660px' }}>{t('roadmap.subheading')}</p>
|
||||
<div style={{ fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: 'var(--sb-text-9)', textTransform: 'uppercase', letterSpacing: '1.6px', marginBottom: '13px' }}>{t('roadmap.eyebrow')}</div>
|
||||
<h2 style={{ margin: '0 0 14px', fontSize: isMobile ? '27px' : '34px', fontWeight: 800, letterSpacing: '-1px', lineHeight: 1.08, color: 'var(--sb-text-1)' }}>{t('roadmap.heading')}</h2>
|
||||
<p style={{ margin: 0, fontSize: '15.5px', lineHeight: 1.6, color: 'var(--sb-text-7)', maxWidth: '660px' }}>{t('roadmap.subheading')}</p>
|
||||
</div>
|
||||
|
||||
{/* progress */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '18px', flexWrap: 'wrap', padding: '18px 22px', borderRadius: '14px', background: '#141416', border: '1px solid rgba(255,255,255,0.06)', marginBottom: '36px' }}>
|
||||
<div style={{ fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: '#e8e8eb', whiteSpace: 'nowrap' }}>{progressBefore}<span style={{ color: '#3ecf8e' }}>{shipped}</span>{progressAfter}</div>
|
||||
<div style={{ flex: '1 1 240px', minWidth: '200px', height: '8px', borderRadius: '99px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: shippedPct, background: 'linear-gradient(90deg, #3ecf8e, #f0892a)' }} />
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '18px', flexWrap: 'wrap', padding: '18px 22px', borderRadius: '14px', background: 'var(--sb-surface)', border: '1px solid rgba(var(--sb-ink), 0.06)', marginBottom: '36px' }}>
|
||||
<div style={{ fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: 'var(--sb-text-2)', whiteSpace: 'nowrap' }}>{progressBefore}<span style={{ color: 'var(--sb-green)' }}>{shipped}</span>{progressAfter}</div>
|
||||
<div style={{ flex: '1 1 240px', minWidth: '200px', height: '8px', borderRadius: '99px', background: 'var(--sb-bg-deep)', border: '1px solid rgba(var(--sb-ink), 0.06)', overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: shippedPct, background: 'linear-gradient(90deg, var(--sb-green), var(--sb-orange))' }} />
|
||||
</div>
|
||||
<div style={{ fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '0.8px', whiteSpace: 'nowrap' }}>{t('roadmap.upcoming', { upcoming })}</div>
|
||||
<div style={{ fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: 'var(--sb-text-9)', textTransform: 'uppercase', letterSpacing: '0.8px', whiteSpace: 'nowrap' }}>{t('roadmap.upcoming', { upcoming })}</div>
|
||||
</div>
|
||||
|
||||
{/* timeline */}
|
||||
@@ -139,37 +149,45 @@ function Roadmap() {
|
||||
</div>
|
||||
|
||||
{/* card */}
|
||||
<div style={{ borderRadius: '16px', background: '#141416', border: `1px solid ${d.status === 'current' ? 'rgba(240,137,42,0.28)' : 'rgba(255,255,255,0.06)'}`, overflow: 'hidden' }}>
|
||||
<div style={{ borderRadius: '16px', background: 'var(--sb-surface)', border: `1px solid ${d.status === 'current' ? 'rgba(var(--sb-orange-rgb), 0.28)' : 'rgba(var(--sb-ink), 0.06)'}`, overflow: 'hidden' }}>
|
||||
<div
|
||||
onClick={() => toggle(i)}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: isMobile ? '11px' : '16px', padding: isMobile ? '16px 16px' : '18px 22px', cursor: 'pointer', transition: 'background .18s ease' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.018)'; }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(var(--sb-ink), 0.018)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
|
||||
>
|
||||
<div style={{ flex: 'none', minWidth: '52px', textAlign: 'center', padding: '7px 10px', borderRadius: '9px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.07)', fontFamily: MONO, fontSize: '13px', fontWeight: 700, color: d.status === 'current' ? '#f0892a' : '#cfcfd4' }}>{d.v}</div>
|
||||
<div style={{ flex: 'none', minWidth: '52px', textAlign: 'center', padding: '7px 10px', borderRadius: '9px', background: 'var(--sb-bg-deep)', border: '1px solid rgba(var(--sb-ink), 0.07)', fontFamily: MONO, fontSize: '13px', fontWeight: 700, color: d.status === 'current' ? 'var(--sb-orange)' : 'var(--sb-text-4)' }}>{d.v}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: isMobile ? '15.5px' : '17px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' }}>{d.title}</div>
|
||||
{!isMobile && <div style={{ marginTop: '3px', fontSize: '13.5px', color: '#9a9aa2' }}>{d.sub}</div>}
|
||||
<div style={{ fontSize: isMobile ? '15.5px' : '17px', fontWeight: 800, letterSpacing: '-0.4px', color: 'var(--sb-text-1)' }}>{d.title}</div>
|
||||
{!isMobile && <div style={{ marginTop: '3px', fontSize: '13.5px', color: 'var(--sb-text-6)' }}>{d.sub}</div>}
|
||||
</div>
|
||||
<div style={{ flex: 'none', display: 'flex', alignItems: 'center', gap: isMobile ? '8px' : '14px' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '7px', padding: '6px 11px', borderRadius: '8px', background: hexA(meta.color, 0.1), border: `1px solid ${hexA(meta.color, 0.22)}`, fontFamily: MONO, fontSize: '10.5px', fontWeight: 600, color: meta.color, textTransform: 'uppercase', letterSpacing: '0.8px', whiteSpace: 'nowrap' }}>
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: meta.color }} />
|
||||
{!isMobile && meta.word}
|
||||
</span>
|
||||
{!isMobile && <span style={{ fontFamily: MONO, fontSize: '12px', fontWeight: 500, color: '#8a8a92', whiteSpace: 'nowrap', minWidth: '74px', textAlign: 'end' }}>{d.date}</span>}
|
||||
<span style={{ color: '#6b6b73', display: 'inline-flex', transition: 'transform .22s cubic-bezier(.2,.7,.3,1)', transform: opened ? 'rotate(180deg)' : 'rotate(0deg)' }}>
|
||||
{/* The status pill is desktop-only. On a phone its
|
||||
word does not fit, and what was left — a bordered
|
||||
box around a single coloured dot — said nothing the
|
||||
marker on the timeline was not already saying in
|
||||
the same colour, while taking room from the title.
|
||||
So the whole pill goes, not just its label. */}
|
||||
{!isMobile && (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '7px', padding: '6px 11px', borderRadius: '8px', background: tint(meta.rgb, 0.1), border: `1px solid ${tint(meta.rgb, 0.22)}`, fontFamily: MONO, fontSize: '10.5px', fontWeight: 600, color: meta.color, textTransform: 'uppercase', letterSpacing: '0.8px', whiteSpace: 'nowrap' }}>
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: meta.solid }} />
|
||||
{meta.word}
|
||||
</span>
|
||||
)}
|
||||
{!isMobile && <span style={{ fontFamily: MONO, fontSize: '12px', fontWeight: 500, color: 'var(--sb-text-7)', whiteSpace: 'nowrap', minWidth: '74px', textAlign: 'end' }}>{d.date}</span>}
|
||||
<span style={{ color: 'var(--sb-text-9)', display: 'inline-flex', transition: 'transform .22s cubic-bezier(.2,.7,.3,1)', transform: opened ? 'rotate(180deg)' : 'rotate(0deg)' }}>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round"><path d="M6 9l6 6 6-6" /></svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{opened && (
|
||||
<div style={{ padding: '4px 22px 22px 22px', animation: 'rmExp .24s cubic-bezier(.2,.7,.3,1)' }}>
|
||||
<div style={{ fontFamily: MONO, fontSize: '10px', fontWeight: 600, color: '#56565e', textTransform: 'uppercase', letterSpacing: '1.2px', marginBottom: '14px', paddingTop: '14px', borderTop: '1px solid rgba(255,255,255,0.05)' }}>{t('roadmap.keyFeatures')}</div>
|
||||
<div style={{ fontFamily: MONO, fontSize: '10px', fontWeight: 600, color: 'var(--sb-text-faint)', textTransform: 'uppercase', letterSpacing: '1.2px', marginBottom: '14px', paddingTop: '14px', borderTop: '1px solid rgba(var(--sb-ink), 0.05)' }}>{t('roadmap.keyFeatures')}</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: '11px 28px' }}>
|
||||
{d.features.map((f, fi) => (
|
||||
<div key={fi} style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}>
|
||||
<span style={{ flex: 'none', marginTop: '7px', width: '5px', height: '5px', borderRadius: '50%', background: meta.color }} />
|
||||
<span style={{ fontSize: '13.5px', lineHeight: 1.5, color: '#cfcfd4' }}>{f}</span>
|
||||
<span style={{ flex: 'none', marginTop: '7px', width: '5px', height: '5px', borderRadius: '50%', background: meta.solid }} />
|
||||
<span style={{ fontSize: '13.5px', lineHeight: 1.5, color: 'var(--sb-text-4)' }}>{f}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// The three-state theme control, modelled on the language switcher next to it.
|
||||
//
|
||||
// Three states rather than a two-way toggle, because "follow the system" is a real
|
||||
// preference and not the same as "dark": someone whose laptop turns light at sunrise
|
||||
// wants the page to follow, and a two-way switch has nowhere to say so. The stored
|
||||
// value is the *mode* ('system' | 'light' | 'dark'), never the colour it resolved to,
|
||||
// so a page opened tomorrow in system mode answers the question again rather than
|
||||
// replaying yesterday's answer.
|
||||
//
|
||||
// Nothing here owns the theme. src/scripts/theme-boot.js does — it runs blocking in
|
||||
// <head>, long before React exists, because the theme has to be right on the first
|
||||
// paint. This component is a view onto it: it reads window.SecureBitTheme, calls set(),
|
||||
// and re-renders from the subscription rather than from its own state, so the header
|
||||
// stays correct when the OS theme changes under it or another tab switches.
|
||||
//
|
||||
// Landing page only, like the language switcher, though for a different reason: there
|
||||
// is no room for it in the connected header, which already carries the security pill,
|
||||
// the connection status and the network settings, and the preference persists across
|
||||
// the transition into the chat anyway.
|
||||
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
// 24×24 line icons, drawn to match the chevron the language switcher uses.
|
||||
const ICON = {
|
||||
system: '<rect x="2.5" y="4" width="19" height="13" rx="2"/><path d="M8 20.5h8M12 17.5v3"/>',
|
||||
light: '<circle cx="12" cy="12" r="4.2"/><path d="M12 2.6v2.4M12 19v2.4M2.6 12h2.4M19 12h2.4M5.3 5.3l1.7 1.7M17 17l1.7 1.7M18.7 5.3L17 7M7 17l-1.7 1.7"/>',
|
||||
dark: '<path d="M20.5 14.3A8.6 8.6 0 0 1 9.7 3.5a8.6 8.6 0 1 0 10.8 10.8z"/>',
|
||||
};
|
||||
|
||||
const svg = (path, size, width) => React.createElement('svg', {
|
||||
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
|
||||
stroke: 'currentColor', strokeWidth: width, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
'aria-hidden': 'true',
|
||||
dangerouslySetInnerHTML: { __html: path },
|
||||
});
|
||||
|
||||
const ThemeSwitcher = () => {
|
||||
const api = typeof window !== 'undefined' ? window.SecureBitTheme : null;
|
||||
// Without the boot script there is nothing to drive, and a control that cannot
|
||||
// change anything is worse than no control.
|
||||
if (!api) return null;
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [mode, setMode] = React.useState(() => api.get());
|
||||
const [resolved, setResolved] = React.useState(() => api.resolved());
|
||||
const rootRef = React.useRef(null);
|
||||
|
||||
// The mode can change without this component doing it — the OS flipping while in
|
||||
// system mode, or another tab writing the preference.
|
||||
React.useEffect(() => api.subscribe((nextMode, nextResolved) => {
|
||||
setMode(nextMode);
|
||||
setResolved(nextResolved);
|
||||
}), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
const onDown = (e) => {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
||||
};
|
||||
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
|
||||
document.addEventListener('pointerdown', onDown);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', onDown);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const OPTIONS = [
|
||||
{ mode: 'system', icon: ICON.system, label: t('theme.system') },
|
||||
{ mode: 'light', icon: ICON.light, label: t('theme.light') },
|
||||
{ mode: 'dark', icon: ICON.dark, label: t('theme.dark') },
|
||||
];
|
||||
|
||||
// The trigger shows what you are *getting*, not what you picked: in system mode the
|
||||
// useful thing to see is whether the page is currently light or dark. Which mode is
|
||||
// selected is the menu's job, and the check mark there says it.
|
||||
const triggerIcon = mode === 'system' ? ICON.system : (resolved === 'light' ? ICON.light : ICON.dark);
|
||||
const current = OPTIONS.find((o) => o.mode === mode) || OPTIONS[0];
|
||||
|
||||
const trigger = React.createElement('button', {
|
||||
key: 'trigger',
|
||||
type: 'button',
|
||||
onClick: () => setOpen((v) => !v),
|
||||
'aria-haspopup': 'menu',
|
||||
'aria-expanded': open ? 'true' : 'false',
|
||||
'aria-label': t('theme.label') + ': ' + current.label,
|
||||
title: t('theme.label'),
|
||||
style: {
|
||||
display: 'flex', alignItems: 'center', gap: '6px',
|
||||
padding: '8px 9px', borderRadius: '9px',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.07)',
|
||||
background: open ? 'rgba(var(--sb-ink), 0.06)' : 'rgba(var(--sb-ink), 0.02)',
|
||||
color: 'var(--sb-text-4)', font: 'inherit',
|
||||
cursor: 'pointer', transition: 'background .15s, color .15s',
|
||||
},
|
||||
}, [
|
||||
React.createElement('span', { key: 'i', style: { display: 'grid', placeItems: 'center' } }, svg(triggerIcon, 15, 1.9)),
|
||||
React.createElement('svg', {
|
||||
key: 'v', width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none',
|
||||
stroke: 'currentColor', strokeWidth: 2.4, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .18s' },
|
||||
dangerouslySetInnerHTML: { __html: '<path d="M6 9l6 6 6-6"/>' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const menu = React.createElement('div', {
|
||||
key: 'menu',
|
||||
role: 'menu',
|
||||
style: {
|
||||
position: 'absolute', top: 'calc(100% + 6px)', insetInlineEnd: 0, zIndex: 60,
|
||||
display: open ? 'block' : 'none',
|
||||
minWidth: '158px', padding: '5px', borderRadius: '11px',
|
||||
border: '1px solid rgba(var(--sb-ink), 0.08)', background: 'var(--sb-surface)',
|
||||
boxShadow: '0 14px 34px rgba(var(--sb-shadow-rgb), calc(0.45 * var(--sb-shadow-k)))',
|
||||
},
|
||||
}, OPTIONS.map((option) => {
|
||||
const active = option.mode === mode;
|
||||
return React.createElement('button', {
|
||||
key: option.mode,
|
||||
type: 'button',
|
||||
role: 'menuitemradio',
|
||||
'aria-checked': active ? 'true' : 'false',
|
||||
onClick: () => { api.set(option.mode); setOpen(false); },
|
||||
style: {
|
||||
width: '100%', display: 'flex', alignItems: 'center', gap: '10px',
|
||||
padding: '8px 10px', borderRadius: '8px', border: 'none',
|
||||
fontSize: '13px', fontFamily: 'inherit',
|
||||
fontWeight: active ? 600 : 500,
|
||||
color: active ? 'var(--sb-text-2)' : 'var(--sb-text-6)',
|
||||
background: active ? 'rgba(var(--sb-ink), 0.06)' : 'transparent',
|
||||
cursor: 'pointer', textAlign: 'start', whiteSpace: 'nowrap',
|
||||
},
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'i',
|
||||
style: { flex: 'none', display: 'grid', placeItems: 'center', width: '16px', color: active ? 'var(--sb-orange)' : 'var(--sb-text-9)' },
|
||||
}, svg(option.icon, 14, 1.9)),
|
||||
React.createElement('span', { key: 'l', style: { flex: 1 } }, option.label),
|
||||
// The check is the only thing that distinguishes "system, currently dark"
|
||||
// from "dark" — the trigger icon cannot, because both look dark.
|
||||
active && React.createElement('span', {
|
||||
key: 'c',
|
||||
style: { flex: 'none', display: 'grid', placeItems: 'center', color: 'var(--sb-orange)' },
|
||||
}, svg('<path d="M4.5 12.5l5 5 10-11"/>', 12, 2.6)),
|
||||
]);
|
||||
}));
|
||||
|
||||
return React.createElement('div', {
|
||||
ref: rootRef,
|
||||
style: { position: 'relative', display: 'inline-flex' },
|
||||
}, [trigger, menu]);
|
||||
};
|
||||
|
||||
window.ThemeSwitcher = ThemeSwitcher;
|
||||
|
||||
export { ThemeSwitcher };
|
||||
@@ -19,11 +19,14 @@ const UniqueFeatureSlider = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ACCENT = '#f0892a';
|
||||
const ACTIVE_BG = 'radial-gradient(130% 90% at 28% 0%, rgba(240,137,42,0.11), transparent 60%), #141416';
|
||||
const ACTIVE_BD = 'rgba(240,137,42,0.3)';
|
||||
const IDLE_BG = '#111113';
|
||||
const IDLE_BD = 'rgba(255,255,255,0.06)';
|
||||
const ACCENT = 'var(--sb-orange)';
|
||||
// The icons are stroked with this, and a stroke is a mark, not a label — brand
|
||||
// colour in both themes. ACCENT above is for text and darkens on a light ground.
|
||||
const ACCENT_SOLID = 'var(--sb-orange-solid)';
|
||||
const ACTIVE_BG = 'radial-gradient(130% 90% at 28% 0%, rgba(var(--sb-orange-rgb), 0.11), transparent 60%), var(--sb-surface)';
|
||||
const ACTIVE_BD = 'rgba(var(--sb-orange-rgb), 0.3)';
|
||||
const IDLE_BG = 'var(--sb-bg)';
|
||||
const IDLE_BD = 'rgba(var(--sb-ink), 0.06)';
|
||||
const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace";
|
||||
const SANS = "'Manrope', system-ui, -apple-system, sans-serif";
|
||||
|
||||
@@ -85,12 +88,12 @@ const UniqueFeatureSlider = () => {
|
||||
key, onClick, 'aria-label': key, className: 'sb-mirror-rtl',
|
||||
style: {
|
||||
width: '46px', height: '46px', display: 'grid', placeItems: 'center',
|
||||
borderRadius: '50%', border: '1px solid rgba(255,255,255,0.1)',
|
||||
background: 'rgba(255,255,255,0.025)', color: '#cfcfd4', cursor: 'pointer',
|
||||
borderRadius: '50%', border: '1px solid rgba(var(--sb-ink), 0.1)',
|
||||
background: 'rgba(var(--sb-ink), 0.025)', color: 'var(--sb-text-4)', cursor: 'pointer',
|
||||
transition: 'all .2s cubic-bezier(.2,.7,.3,1)'
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.borderColor = ACTIVE_BD; e.currentTarget.style.color = ACCENT; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'; e.currentTarget.style.color = '#cfcfd4'; }
|
||||
onMouseLeave: (e) => { e.currentTarget.style.borderColor = 'rgba(var(--sb-ink), 0.1)'; e.currentTarget.style.color = 'var(--sb-text-4)'; }
|
||||
}, svg(path, 18, 'currentColor', 2.1));
|
||||
|
||||
const tag = (label) =>
|
||||
@@ -98,12 +101,12 @@ const UniqueFeatureSlider = () => {
|
||||
key: label,
|
||||
style: {
|
||||
display: 'inline-flex', alignItems: 'center', gap: '7px', padding: '7px 12px',
|
||||
borderRadius: '9px', border: '1px solid rgba(255,255,255,0.07)',
|
||||
background: 'rgba(255,255,255,0.025)', fontFamily: MONO,
|
||||
fontSize: '11.5px', fontWeight: 500, color: '#9a9aa2'
|
||||
borderRadius: '9px', border: '1px solid rgba(var(--sb-ink), 0.07)',
|
||||
background: 'rgba(var(--sb-ink), 0.025)', fontFamily: MONO,
|
||||
fontSize: '11.5px', fontWeight: 500, color: 'var(--sb-text-6)'
|
||||
}
|
||||
}, [
|
||||
React.createElement('span', { key: 'dot', style: { width: '5px', height: '5px', borderRadius: '50%', background: '#3ecf8e' } }),
|
||||
React.createElement('span', { key: 'dot', style: { width: '5px', height: '5px', borderRadius: '50%', background: 'var(--sb-green-solid)' } }),
|
||||
label
|
||||
]);
|
||||
|
||||
@@ -124,17 +127,17 @@ const UniqueFeatureSlider = () => {
|
||||
key: 'ic',
|
||||
style: {
|
||||
width: '54px', height: '54px', borderRadius: '15px', display: 'grid', placeItems: 'center',
|
||||
background: 'rgba(240,137,42,0.13)', border: '1px solid rgba(240,137,42,0.3)'
|
||||
background: 'rgba(var(--sb-orange-rgb), 0.13)', border: '1px solid rgba(var(--sb-orange-rgb), 0.3)'
|
||||
}
|
||||
}, svg(s.icon, 26, ACCENT, 1.9)),
|
||||
React.createElement('span', { key: 'n', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 600, color: '#6b6b73' } }, s.num)
|
||||
}, svg(s.icon, 26, ACCENT_SOLID, 1.9)),
|
||||
React.createElement('span', { key: 'n', style: { fontFamily: MONO, fontSize: '13px', fontWeight: 600, color: 'var(--sb-text-9)' } }, s.num)
|
||||
]),
|
||||
React.createElement('div', { key: 'mid' }, [
|
||||
React.createElement('h3', {
|
||||
key: 'h', style: { margin: '0 0 12px', fontSize: isMobile ? '24px' : '30px', fontWeight: 800, letterSpacing: '-0.7px', lineHeight: 1.08, color: '#f4f4f6' }
|
||||
key: 'h', style: { margin: '0 0 12px', fontSize: isMobile ? '24px' : '30px', fontWeight: 800, letterSpacing: '-0.7px', lineHeight: 1.08, color: 'var(--sb-text-1)' }
|
||||
}, [s.title[0], React.createElement('br', { key: 'br' }), s.title[1]]),
|
||||
React.createElement('p', {
|
||||
key: 'p', style: { margin: 0, fontSize: '15px', lineHeight: 1.6, color: '#9a9aa2', maxWidth: '380px' }
|
||||
key: 'p', style: { margin: 0, fontSize: '15px', lineHeight: 1.6, color: 'var(--sb-text-6)', maxWidth: '380px' }
|
||||
}, s.desc)
|
||||
]),
|
||||
React.createElement('div', { key: 'tags', style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } }, s.tags.map(tag))
|
||||
@@ -145,19 +148,19 @@ const UniqueFeatureSlider = () => {
|
||||
key: 'col',
|
||||
style: { display: 'flex', alignItems: 'center', gap: '16px', padding: '20px 22px' }
|
||||
}, [
|
||||
React.createElement('span', { key: 'n', style: { fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: '#56565e' } }, s.num),
|
||||
React.createElement('span', { key: 'l', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.2px', color: '#cfcfd4' } }, s.collapsed)
|
||||
React.createElement('span', { key: 'n', style: { fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: 'var(--sb-text-faint)' } }, s.num),
|
||||
React.createElement('span', { key: 'l', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.2px', color: 'var(--sb-text-4)' } }, s.collapsed)
|
||||
])
|
||||
: React.createElement('div', {
|
||||
key: 'col',
|
||||
style: { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'space-between', padding: '24px 0' }
|
||||
}, [
|
||||
React.createElement('span', { key: 'n', style: { fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: '#56565e' } }, s.num),
|
||||
React.createElement('span', { key: 'n', style: { fontFamily: MONO, fontSize: '12px', fontWeight: 600, color: 'var(--sb-text-faint)' } }, s.num),
|
||||
React.createElement('span', {
|
||||
key: 'l',
|
||||
style: { writingMode: 'vertical-rl', transform: 'rotate(180deg)', fontSize: '17px', fontWeight: 800, letterSpacing: '-0.2px', color: '#cfcfd4', whiteSpace: 'nowrap' }
|
||||
style: { writingMode: 'vertical-rl', transform: 'rotate(180deg)', fontSize: '17px', fontWeight: 800, letterSpacing: '-0.2px', color: 'var(--sb-text-4)', whiteSpace: 'nowrap' }
|
||||
}, s.collapsed),
|
||||
svg(s.icon, 22, '#56565e', 1.8)
|
||||
svg(s.icon, 22, 'var(--sb-text-faint)', 1.8)
|
||||
]);
|
||||
|
||||
const panels = slides.map((s, i) => {
|
||||
@@ -178,7 +181,7 @@ const UniqueFeatureSlider = () => {
|
||||
cursor: 'pointer',
|
||||
background: isActive ? ACTIVE_BG : IDLE_BG,
|
||||
border: '1px solid ' + (isActive ? ACTIVE_BD : IDLE_BD),
|
||||
color: '#8a8a92',
|
||||
color: 'var(--sb-text-7)',
|
||||
transition: 'flex .46s cubic-bezier(.2,.7,.3,1), background .3s ease, border-color .3s ease, filter .2s ease'
|
||||
}
|
||||
}, isActive ? expandedContent(s) : collapsedContent(s));
|
||||
@@ -199,11 +202,11 @@ const UniqueFeatureSlider = () => {
|
||||
React.createElement('div', { key: 'titles' }, [
|
||||
React.createElement('div', {
|
||||
key: 'eyebrow',
|
||||
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#6b6b73', textTransform: 'uppercase', letterSpacing: '1.4px', marginBottom: '12px' }
|
||||
style: { fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: 'var(--sb-text-9)', textTransform: 'uppercase', letterSpacing: '1.4px', marginBottom: '12px' }
|
||||
}, t('unique.eyebrow')),
|
||||
React.createElement('h2', {
|
||||
key: 'h2',
|
||||
style: { margin: 0, fontSize: isMobile ? '28px' : '38px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.05, color: '#f4f4f6' }
|
||||
style: { margin: 0, fontSize: isMobile ? '28px' : '38px', fontWeight: 800, letterSpacing: '-1.1px', lineHeight: 1.05, color: 'var(--sb-text-1)' }
|
||||
}, t('unique.heading'))
|
||||
]),
|
||||
React.createElement('div', { key: 'nav', style: { display: 'flex', alignItems: 'center', gap: '10px', flex: 'none' } }, [
|
||||
@@ -227,9 +230,9 @@ const UniqueFeatureSlider = () => {
|
||||
// Full-bleed dark band with the radial accent glow — matches the design mockup.
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
width: '100%', color: '#e8e8eb', fontFamily: SANS,
|
||||
width: '100%', color: 'var(--sb-text-2)', fontFamily: SANS,
|
||||
padding: isMobile ? '44px 0' : '64px 0',
|
||||
background: 'radial-gradient(1100px 700px at 18% 8%, rgba(240,137,42,0.05), transparent 60%), #0f0f11'
|
||||
background: 'radial-gradient(1100px 700px at 18% 8%, rgba(var(--sb-orange-rgb), 0.05), transparent 60%), var(--sb-bg)'
|
||||
}
|
||||
}, [
|
||||
React.createElement('style', { key: 'kf', dangerouslySetInnerHTML: { __html: '@keyframes wuUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}' } }),
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "هذه الصفحة متوفرة أيضًا بالعربية.",
|
||||
"language.suggest.cta": "اقرأ بالعربية",
|
||||
"language.suggest.dismiss": "إغلاق",
|
||||
"theme.label": "المظهر",
|
||||
"theme.system": "النظام",
|
||||
"theme.light": "فاتح",
|
||||
"theme.dark": "داكن",
|
||||
"community.title": "انضمّ إلى مستقبل الخصوصية",
|
||||
"community.description": "ينمو SecureBit بفضل مجتمعه. أفكاركم وملاحظاتكم هي ما يصوغ مستقبل التواصل الآمن - مبنيّ في العلن، مع تحقّق ASN.1 كامل من طرف إلى طرف.",
|
||||
"community.github": "مستودع GitHub",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "أكّد لتثبيت SecureBit.chat",
|
||||
"pwa.genericTitle": "تثبيت SecureBit",
|
||||
"pwa.genericDesc": "لكلّ متصفّح طريقته في التثبيت. اختر الخطوات التي تناسب متصفّحك.",
|
||||
"pwa.androidChromeHint": "اضغط على ⋮ في الزاوية العلوية، ثم «تثبيت التطبيق»",
|
||||
"pwa.androidOtherHint": "افتح القائمة، ثم «إضافة إلى الشاشة الرئيسية»",
|
||||
"pwa.gotIt": "فهمت",
|
||||
"pwa.installedTitle": "تمّ تثبيت التطبيق!",
|
||||
"pwa.installedIos": "تمّ تثبيت تطبيق iOS! افتحه من الشاشة الرئيسية.",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "Diese Seite gibt es auch auf Deutsch.",
|
||||
"language.suggest.cta": "Auf Deutsch lesen",
|
||||
"language.suggest.dismiss": "Schließen",
|
||||
"theme.label": "Design",
|
||||
"theme.system": "System",
|
||||
"theme.light": "Hell",
|
||||
"theme.dark": "Dunkel",
|
||||
"community.title": "Gestalten Sie die Zukunft der Privatsphäre mit",
|
||||
"community.description": "SecureBit wächst durch seine Community. Ihre Ideen und Ihr Feedback prägen die Zukunft sicherer Kommunikation - offen entwickelt, mit vollständiger ASN.1-Validierung von Ende zu Ende.",
|
||||
"community.github": "GitHub-Repository",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "Bestätigen, um SecureBit.chat zu installieren",
|
||||
"pwa.genericTitle": "SecureBit installieren",
|
||||
"pwa.genericDesc": "Jeder Browser installiert auf seine eigene Weise. Wählen Sie die Schritte, die zu Ihrem passen.",
|
||||
"pwa.androidChromeHint": "Tippe oben auf ⋮ und dann auf „App installieren“",
|
||||
"pwa.androidOtherHint": "Öffne das Menü und wähle „Zum Startbildschirm hinzufügen“",
|
||||
"pwa.gotIt": "Verstanden",
|
||||
"pwa.installedTitle": "App installiert!",
|
||||
"pwa.installedIos": "iOS-App installiert! Vom Home-Bildschirm öffnen.",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "This page is also available in English.",
|
||||
"language.suggest.cta": "Read in English",
|
||||
"language.suggest.dismiss": "Dismiss",
|
||||
"theme.label": "Theme",
|
||||
"theme.system": "System",
|
||||
"theme.light": "Light",
|
||||
"theme.dark": "Dark",
|
||||
"community.title": "Join the future of privacy",
|
||||
"community.description": "SecureBit grows thanks to its community. Your ideas and feedback shape the future of secure communication - built in the open, with complete ASN.1 validation end-to-end.",
|
||||
"community.github": "GitHub Repository",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "Confirm to install SecureBit.chat",
|
||||
"pwa.genericTitle": "Install SecureBit",
|
||||
"pwa.genericDesc": "Your browser handles installs its own way. Pick the steps that match yours.",
|
||||
"pwa.androidChromeHint": "Tap ⋮ in the top corner, then “Install app”",
|
||||
"pwa.androidOtherHint": "Tap the menu, then “Add to Home screen”",
|
||||
"pwa.gotIt": "Got it",
|
||||
"pwa.installedTitle": "App Installed!",
|
||||
"pwa.installedIos": "iOS App installed! Open from home screen.",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "Esta página también está disponible en español.",
|
||||
"language.suggest.cta": "Leer en español",
|
||||
"language.suggest.dismiss": "Cerrar",
|
||||
"theme.label": "Tema",
|
||||
"theme.system": "Sistema",
|
||||
"theme.light": "Claro",
|
||||
"theme.dark": "Oscuro",
|
||||
"community.title": "Construye el futuro de la privacidad",
|
||||
"community.description": "SecureBit crece gracias a su comunidad. Tus ideas y comentarios dan forma al futuro de la comunicación segura, desarrollada de forma abierta y con validación ASN.1 completa de extremo a extremo.",
|
||||
"community.github": "Repositorio en GitHub",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "Confirma para instalar SecureBit.chat",
|
||||
"pwa.genericTitle": "Instalar SecureBit",
|
||||
"pwa.genericDesc": "Cada navegador instala a su manera. Elige los pasos que coincidan con el tuyo.",
|
||||
"pwa.androidChromeHint": "Toca ⋮ en la esquina superior y elige «Instalar aplicación»",
|
||||
"pwa.androidOtherHint": "Abre el menú y elige «Añadir a la pantalla de inicio»",
|
||||
"pwa.gotIt": "Entendido",
|
||||
"pwa.installedTitle": "¡App instalada!",
|
||||
"pwa.installedIos": "¡App de iOS instalada! Ábrela desde la pantalla de inicio.",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "این صفحه به فارسی هم در دسترس است.",
|
||||
"language.suggest.cta": "خواندن به فارسی",
|
||||
"language.suggest.dismiss": "بستن",
|
||||
"theme.label": "پوسته",
|
||||
"theme.system": "سیستم",
|
||||
"theme.light": "روشن",
|
||||
"theme.dark": "تیره",
|
||||
"community.title": "به آیندهٔ حریم خصوصی بپیوندید",
|
||||
"community.description": "SecureBit بهلطف جامعهاش رشد میکند. ایدهها و بازخوردهای شما آیندهٔ ارتباط امن را شکل میدهد - ساختهشده در فضای باز، با اعتبارسنجی کامل ASN.1 از یک سر تا سر دیگر.",
|
||||
"community.github": "مخزن GitHub",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "برای نصب SecureBit.chat تأیید کنید",
|
||||
"pwa.genericTitle": "نصب SecureBit",
|
||||
"pwa.genericDesc": "هر مرورگر روش نصب خودش را دارد. گامهایی را برگزینید که با مرورگر شما میخواند.",
|
||||
"pwa.androidChromeHint": "روی ⋮ در گوشهٔ بالا بزنید و «نصب برنامه» را بزنید",
|
||||
"pwa.androidOtherHint": "منو را باز کنید و «افزودن به صفحهٔ اصلی» را بزنید",
|
||||
"pwa.gotIt": "متوجه شدم",
|
||||
"pwa.installedTitle": "برنامه نصب شد!",
|
||||
"pwa.installedIos": "برنامهٔ iOS نصب شد! از صفحهٔ اصلی بازش کنید.",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "Cette page est aussi disponible en français.",
|
||||
"language.suggest.cta": "Lire en français",
|
||||
"language.suggest.dismiss": "Fermer",
|
||||
"theme.label": "Thème",
|
||||
"theme.system": "Système",
|
||||
"theme.light": "Clair",
|
||||
"theme.dark": "Sombre",
|
||||
"community.title": "Construisez l'avenir de la vie privée",
|
||||
"community.description": "SecureBit grandit grâce à sa communauté. Vos idées et vos retours façonnent l'avenir des communications sécurisées, développées à ciel ouvert, avec une validation ASN.1 complète de bout en bout.",
|
||||
"community.github": "Dépôt GitHub",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "Confirmez pour installer SecureBit.chat",
|
||||
"pwa.genericTitle": "Installer SecureBit",
|
||||
"pwa.genericDesc": "Chaque navigateur installe à sa manière. Choisissez les étapes qui correspondent au vôtre.",
|
||||
"pwa.androidChromeHint": "Appuyez sur ⋮ en haut, puis sur « Installer l’application »",
|
||||
"pwa.androidOtherHint": "Ouvrez le menu, puis « Ajouter à l’écran d’accueil »",
|
||||
"pwa.gotIt": "Compris",
|
||||
"pwa.installedTitle": "Application installée !",
|
||||
"pwa.installedIos": "Application iOS installée ! Ouvrez-la depuis l'écran d'accueil.",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "הדף הזה זמין גם בעברית.",
|
||||
"language.suggest.cta": "לקריאה בעברית",
|
||||
"language.suggest.dismiss": "סגירה",
|
||||
"theme.label": "ערכת נושא",
|
||||
"theme.system": "מערכת",
|
||||
"theme.light": "בהיר",
|
||||
"theme.dark": "כהה",
|
||||
"community.title": "הצטרפו לעתיד של הפרטיות",
|
||||
"community.description": "SecureBit גדל בזכות הקהילה שלו. הרעיונות והמשוב שלכם מעצבים את עתיד התקשורת המאובטחת - נבנה בגלוי, עם אימות ASN.1 מלא מקצה לקצה.",
|
||||
"community.github": "מאגר GitHub",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "אשרו כדי להתקין את SecureBit.chat",
|
||||
"pwa.genericTitle": "התקנת SecureBit",
|
||||
"pwa.genericDesc": "לכל דפדפן דרך משלו להתקין. בחרו את השלבים שמתאימים לשלכם.",
|
||||
"pwa.androidChromeHint": "הקישו על ⋮ בפינה העליונה ובחרו ב־”התקנת אפליקציה”",
|
||||
"pwa.androidOtherHint": "פתחו את התפריט ובחרו ב־”הוספה למסך הבית”",
|
||||
"pwa.gotIt": "הבנתי",
|
||||
"pwa.installedTitle": "האפליקציה הותקנה!",
|
||||
"pwa.installedIos": "אפליקציית iOS הותקנה! פתחו ממסך הבית.",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "यह पृष्ठ हिन्दी में भी उपलब्ध है।",
|
||||
"language.suggest.cta": "हिन्दी में पढ़ें",
|
||||
"language.suggest.dismiss": "बंद करें",
|
||||
"theme.label": "थीम",
|
||||
"theme.system": "सिस्टम",
|
||||
"theme.light": "हल्की",
|
||||
"theme.dark": "गहरी",
|
||||
"community.title": "निजता के भविष्य में शामिल हों",
|
||||
"community.description": "SecureBit अपने समुदाय से बढ़ता है। आपके विचार और सुझाव सुरक्षित संवाद का भविष्य गढ़ते हैं - खुले में बना, और पूरे रास्ते पूरी ASN.1 जाँच के साथ।",
|
||||
"community.github": "GitHub रिपॉज़िटरी",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "पुष्टि कीजिए, SecureBit.chat इंस्टॉल हो जाएगा",
|
||||
"pwa.genericTitle": "SecureBit इंस्टॉल करें",
|
||||
"pwa.genericDesc": "हर ब्राउज़र अपने तरीक़े से इंस्टॉल करता है। अपने ब्राउज़र से मेल खाते चरण चुनिए।",
|
||||
"pwa.androidChromeHint": "ऊपर के कोने में ⋮ दबाएँ, फिर “ऐप इंस्टॉल करें” चुनें",
|
||||
"pwa.androidOtherHint": "मेनू खोलें और “होम स्क्रीन पर जोड़ें” चुनें",
|
||||
"pwa.gotIt": "समझ गया",
|
||||
"pwa.installedTitle": "ऐप इंस्टॉल हो गया!",
|
||||
"pwa.installedIos": "iOS ऐप इंस्टॉल हो गया! होम स्क्रीन से खोलिए।",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "이 페이지는 한국어로도 볼 수 있습니다.",
|
||||
"language.suggest.cta": "한국어로 보기",
|
||||
"language.suggest.dismiss": "닫기",
|
||||
"theme.label": "테마",
|
||||
"theme.system": "시스템",
|
||||
"theme.light": "밝게",
|
||||
"theme.dark": "어둡게",
|
||||
"community.title": "프라이버시의 미래를 함께 만듭니다",
|
||||
"community.description": "SecureBit은 커뮤니티 덕분에 자랍니다. 여러분의 아이디어와 의견이 안전한 소통의 미래를 만듭니다 - 모든 과정이 공개되어 있고, 전 구간에서 ASN.1 검증을 수행합니다.",
|
||||
"community.github": "GitHub 저장소",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "확인하면 SecureBit.chat이 설치됩니다",
|
||||
"pwa.genericTitle": "SecureBit 설치",
|
||||
"pwa.genericDesc": "브라우저마다 설치 방법이 다릅니다. 쓰시는 브라우저에 맞는 순서를 고르세요.",
|
||||
"pwa.androidChromeHint": "오른쪽 위 ⋮ 를 누른 뒤 “앱 설치”를 선택하세요",
|
||||
"pwa.androidOtherHint": "메뉴를 열고 “홈 화면에 추가”를 선택하세요",
|
||||
"pwa.gotIt": "알겠습니다",
|
||||
"pwa.installedTitle": "앱이 설치되었습니다",
|
||||
"pwa.installedIos": "iOS 앱이 설치되었습니다. 홈 화면에서 열어보세요.",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "Эта страница также доступна на русском.",
|
||||
"language.suggest.cta": "Читать по-русски",
|
||||
"language.suggest.dismiss": "Закрыть",
|
||||
"theme.label": "Тема",
|
||||
"theme.system": "Системная",
|
||||
"theme.light": "Светлая",
|
||||
"theme.dark": "Тёмная",
|
||||
"community.title": "Присоединяйтесь к будущему приватности",
|
||||
"community.description": "SecureBit растёт благодаря сообществу. Ваши идеи и отзывы формируют будущее защищённого общения - разработка ведётся открыто, с полной проверкой ASN.1 на всём пути.",
|
||||
"community.github": "Репозиторий на GitHub",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "Подтвердите, чтобы установить SecureBit.chat",
|
||||
"pwa.genericTitle": "Установить SecureBit",
|
||||
"pwa.genericDesc": "Каждый браузер устанавливает по-своему. Выберите шаги, подходящие вашему.",
|
||||
"pwa.androidChromeHint": "Нажмите ⋮ в верхнем углу и выберите «Установить приложение»",
|
||||
"pwa.androidOtherHint": "Откройте меню и выберите «Добавить на главный экран»",
|
||||
"pwa.gotIt": "Понятно",
|
||||
"pwa.installedTitle": "Приложение установлено!",
|
||||
"pwa.installedIos": "Приложение для iOS установлено! Откройте его с экрана «Домой».",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "Ця сторінка також доступна українською.",
|
||||
"language.suggest.cta": "Читати українською",
|
||||
"language.suggest.dismiss": "Закрити",
|
||||
"theme.label": "Тема",
|
||||
"theme.system": "Системна",
|
||||
"theme.light": "Світла",
|
||||
"theme.dark": "Темна",
|
||||
"community.title": "Долучайтеся до майбутнього приватності",
|
||||
"community.description": "SecureBit росте завдяки своїй спільноті. Ваші ідеї та відгуки формують майбутнє захищеного спілкування - розробка ведеться відкрито, з повною валідацією ASN.1 на всьому шляху.",
|
||||
"community.github": "Репозиторій на GitHub",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "Підтвердіть, щоб встановити SecureBit.chat",
|
||||
"pwa.genericTitle": "Встановити SecureBit",
|
||||
"pwa.genericDesc": "Кожен браузер встановлює по-своєму. Оберіть кроки, які відповідають вашому.",
|
||||
"pwa.androidChromeHint": "Натисніть ⋮ у верхньому кутку та оберіть «Встановити застосунок»",
|
||||
"pwa.androidOtherHint": "Відкрийте меню та оберіть «Додати на головний екран»",
|
||||
"pwa.gotIt": "Зрозуміло",
|
||||
"pwa.installedTitle": "Застосунок встановлено!",
|
||||
"pwa.installedIos": "Застосунок для iOS встановлено! Відкрийте його з екрана «Домів».",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "یہ صفحہ اردو میں بھی دستیاب ہے۔",
|
||||
"language.suggest.cta": "اردو میں پڑھیں",
|
||||
"language.suggest.dismiss": "بند کریں",
|
||||
"theme.label": "تھیم",
|
||||
"theme.system": "سسٹم",
|
||||
"theme.light": "روشن",
|
||||
"theme.dark": "گہرا",
|
||||
"community.title": "نجیت کے مستقبل میں شامل ہوں",
|
||||
"community.description": "SecureBit اپنی برادری کی بدولت بڑھتا ہے۔ آپ کے خیالات اور آراء محفوظ رابطے کا مستقبل تشکیل دیتے ہیں - کھلے عام بنایا گیا، سرے سے سرے تک مکمل ASN.1 توثیق کے ساتھ۔",
|
||||
"community.github": "GitHub مخزن",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "SecureBit.chat نصب کرنے کی تصدیق کریں",
|
||||
"pwa.genericTitle": "SecureBit نصب کریں",
|
||||
"pwa.genericDesc": "ہر براؤزر اپنے انداز میں نصب کرتا ہے۔ وہ مراحل چنیں جو آپ کے براؤزر سے میل کھاتے ہوں۔",
|
||||
"pwa.androidChromeHint": "اوپر کے کونے میں ⋮ دبائیں، پھر «ایپ انسٹال کریں»",
|
||||
"pwa.androidOtherHint": "مینیو کھولیں اور «ہوم اسکرین پر شامل کریں» منتخب کریں",
|
||||
"pwa.gotIt": "سمجھ گیا",
|
||||
"pwa.installedTitle": "ایپ نصب ہو گئی!",
|
||||
"pwa.installedIos": "iOS ایپ نصب ہو گئی! ہوم سکرین سے کھولیں۔",
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DICTIONARY = {
|
||||
"language.suggest.text": "本页面也有简体中文版本。",
|
||||
"language.suggest.cta": "阅读简体中文",
|
||||
"language.suggest.dismiss": "关闭",
|
||||
"theme.label": "主题",
|
||||
"theme.system": "跟随系统",
|
||||
"theme.light": "浅色",
|
||||
"theme.dark": "深色",
|
||||
"community.title": "一起构筑隐私的未来",
|
||||
"community.description": "SecureBit 因社区而成长。你的想法与反馈塑造着安全通讯的未来 —— 开放开发,全程完整的 ASN.1 校验。",
|
||||
"community.github": "GitHub 仓库",
|
||||
@@ -290,6 +294,8 @@ export const DICTIONARY = {
|
||||
"pwa.iosStep3Hint": "确认以安装 SecureBit.chat",
|
||||
"pwa.genericTitle": "安装 SecureBit",
|
||||
"pwa.genericDesc": "每种浏览器的安装方式都不一样。请选择与你的浏览器相符的步骤。",
|
||||
"pwa.androidChromeHint": "点击右上角的 ⋮ ,选择“安装应用”",
|
||||
"pwa.androidOtherHint": "打开菜单,选择“添加到主屏幕”",
|
||||
"pwa.gotIt": "知道了",
|
||||
"pwa.installedTitle": "应用已安装!",
|
||||
"pwa.installedIos": "iOS 应用已安装!请从主屏幕打开。",
|
||||
|
||||
+103
-69
@@ -100,18 +100,33 @@ class PWAInstallPrompt {
|
||||
});
|
||||
}
|
||||
|
||||
// Called both by our own listener and by pwa-install-capture.js, which owns
|
||||
// the event whenever it fired before this module was evaluated.
|
||||
adoptInstallEvent(event) {
|
||||
this.deferredPrompt = event;
|
||||
|
||||
if (this.checkInstallationStatus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isInstalled && this.shouldShowPrompt()) {
|
||||
setTimeout(() => this.showInstallOptions(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// If the capture script already caught it, take it now — the event does
|
||||
// not fire a second time for this page load.
|
||||
if (window.__pwaInstallEvent) {
|
||||
this.adoptInstallEvent(window.__pwaInstallEvent);
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
// Don't prevent default - let browser show its own banner
|
||||
this.deferredPrompt = event;
|
||||
|
||||
if (this.checkInstallationStatus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isInstalled && this.shouldShowPrompt()) {
|
||||
setTimeout(() => this.showInstallOptions(), 1000);
|
||||
}
|
||||
// preventDefault() is what keeps the event usable. Let Chrome run its
|
||||
// default action and the stashed event is spent: prompt() rejects and
|
||||
// the install button silently degrades to a manual how-to.
|
||||
event.preventDefault();
|
||||
this.adoptInstallEvent(event);
|
||||
});
|
||||
|
||||
window.addEventListener('appinstalled', () => {
|
||||
@@ -166,10 +181,10 @@ class PWAInstallPrompt {
|
||||
|
||||
this.installButton.innerHTML = `
|
||||
<div style="position:relative; display:inline-flex;">
|
||||
<button class="close-btn" type="button" title="${t('pwa.dismiss')}" aria-label="${t('pwa.dismiss')}" style="position:absolute; top:-11px; inset-inline-end:-11px; z-index:3; width:28px; height:28px; padding:0; border-radius:50%; display:grid; place-items:center; border:1px solid rgba(255,255,255,0.1); background:#1a1a1d; color:#9a9aa2; cursor:pointer; transition:all .18s cubic-bezier(.2,.7,.3,1);">
|
||||
<button class="close-btn" type="button" title="${t('pwa.dismiss')}" aria-label="${t('pwa.dismiss')}" style="position:absolute; top:-11px; inset-inline-end:-11px; z-index:3; width:28px; height:28px; padding:0; border-radius:50%; display:grid; place-items:center; border:1px solid rgba(var(--sb-ink), 0.1); background:var(--sb-surface-2); color:var(--sb-text-6); cursor:pointer; transition:all .18s cubic-bezier(.2,.7,.3,1);">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</button>
|
||||
<button class="install-pill" type="button" style="display:inline-flex; align-items:center; gap:11px; padding-block:15px; padding-inline-start:22px; padding-inline-end:26px; border-radius:15px; border:none; background:#f0892a; color:#1a0f04; font-family:inherit; font-size:16px; font-weight:700; letter-spacing:-0.2px; cursor:pointer; box-shadow:0 10px 30px rgba(240,137,42,0.32); transition:all .2s cubic-bezier(.2,.7,.3,1);">
|
||||
<button class="install-pill" type="button" style="display:inline-flex; align-items:center; gap:11px; padding-block:15px; padding-inline-start:22px; padding-inline-end:26px; border-radius:15px; border:none; background:var(--sb-orange-solid); color:var(--sb-on-accent); font-family:inherit; font-size:16px; font-weight:700; letter-spacing:-0.2px; cursor:pointer; box-shadow:0 10px 30px rgba(var(--sb-orange-rgb), 0.32); transition:all .2s cubic-bezier(.2,.7,.3,1);">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><path d="M12 3v11"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 20h14"/></svg>
|
||||
${t('pwa.installApp')}
|
||||
</button>
|
||||
@@ -177,12 +192,12 @@ class PWAInstallPrompt {
|
||||
`;
|
||||
|
||||
const pill = this.installButton.querySelector('.install-pill');
|
||||
pill.addEventListener('mouseenter', () => { pill.style.background = '#ff9637'; pill.style.transform = 'translateY(-2px)'; });
|
||||
pill.addEventListener('mouseleave', () => { pill.style.background = '#f0892a'; pill.style.transform = 'none'; });
|
||||
pill.addEventListener('mouseenter', () => { pill.style.background = 'var(--sb-orange-hi)'; pill.style.transform = 'translateY(-2px)'; });
|
||||
pill.addEventListener('mouseleave', () => { pill.style.background = 'var(--sb-orange)'; pill.style.transform = 'none'; });
|
||||
|
||||
const closeBtn = this.installButton.querySelector('.close-btn');
|
||||
closeBtn.addEventListener('mouseenter', () => { closeBtn.style.color = '#e5727a'; closeBtn.style.borderColor = 'rgba(229,114,122,0.4)'; closeBtn.style.background = '#201416'; });
|
||||
closeBtn.addEventListener('mouseleave', () => { closeBtn.style.color = '#9a9aa2'; closeBtn.style.borderColor = 'rgba(255,255,255,0.1)'; closeBtn.style.background = '#1a1a1d'; });
|
||||
closeBtn.addEventListener('mouseenter', () => { closeBtn.style.color = 'var(--sb-red)'; closeBtn.style.borderColor = 'rgba(var(--sb-red-rgb), 0.4)'; closeBtn.style.background = 'var(--sb-surface-warm)'; });
|
||||
closeBtn.addEventListener('mouseleave', () => { closeBtn.style.color = 'var(--sb-text-6)'; closeBtn.style.borderColor = 'rgba(var(--sb-ink), 0.1)'; closeBtn.style.background = 'var(--sb-surface-2)'; });
|
||||
|
||||
this.installButton.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.close-btn')) {
|
||||
@@ -221,19 +236,19 @@ class PWAInstallPrompt {
|
||||
: "transition:transform .44s cubic-bezier(.2,.7,.3,1), opacity .3s ease;");
|
||||
|
||||
this.installBanner.innerHTML = `
|
||||
<div style="max-width:520px; margin-inline:auto; border-radius:20px; background:#121214; border:1px solid rgba(255,255,255,0.08); padding:18px; box-shadow:0 20px 50px rgba(0,0,0,0.55);">
|
||||
<div style="max-width:520px; margin-inline:auto; border-radius:20px; background:var(--sb-bg); border:1px solid rgba(var(--sb-ink), 0.08); padding:18px; box-shadow:0 20px 50px rgba(var(--sb-shadow-rgb), calc(0.55 * var(--sb-shadow-k)));">
|
||||
<div style="display:flex; align-items:center; gap:14px; margin-bottom:16px;">
|
||||
<div style="flex:none; width:44px; height:44px; border-radius:12px; display:grid; place-items:center; background:rgba(240,137,42,0.12); border:1px solid rgba(240,137,42,0.28);">
|
||||
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="#f0892a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v11"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 20h14"/></svg>
|
||||
<div style="flex:none; width:44px; height:44px; border-radius:12px; display:grid; place-items:center; background:rgba(var(--sb-orange-rgb), 0.12); border:1px solid rgba(var(--sb-orange-rgb), 0.28);">
|
||||
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="var(--sb-orange-solid)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v11"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 20h14"/></svg>
|
||||
</div>
|
||||
<div style="flex:1; min-width:0;">
|
||||
<div style="font-size:15.5px; font-weight:800; letter-spacing:-0.3px; color:#f4f4f6; margin-bottom:3px;">${t('pwa.bannerTitle')}</div>
|
||||
<div style="font-size:13px; line-height:1.45; color:#8a8a92;">${t('pwa.bannerDesc')}</div>
|
||||
<div style="font-size:15.5px; font-weight:800; letter-spacing:-0.3px; color:var(--sb-text-1); margin-bottom:3px;">${t('pwa.bannerTitle')}</div>
|
||||
<div style="font-size:13px; line-height:1.45; color:var(--sb-text-7);">${t('pwa.bannerDesc')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px;">
|
||||
<button class="install-btn" type="button" data-action="install" style="flex:1; padding:13px 20px; border-radius:13px; border:none; background:#f0892a; color:#1a0f04; font-family:inherit; font-size:15px; font-weight:700; letter-spacing:-0.2px; cursor:pointer; transition:background .2s cubic-bezier(.2,.7,.3,1);">${t('pwa.install')}</button>
|
||||
<button class="close-btn" type="button" data-action="close" style="flex:none; padding:13px 18px; border-radius:13px; border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.03); color:#9a9aa2; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:all .2s cubic-bezier(.2,.7,.3,1);">${t('pwa.dismiss')}</button>
|
||||
<button class="install-btn" type="button" data-action="install" style="flex:1; padding:13px 20px; border-radius:13px; border:none; background:var(--sb-orange-solid); color:var(--sb-on-accent); font-family:inherit; font-size:15px; font-weight:700; letter-spacing:-0.2px; cursor:pointer; transition:background .2s cubic-bezier(.2,.7,.3,1);">${t('pwa.install')}</button>
|
||||
<button class="close-btn" type="button" data-action="close" style="flex:none; padding:13px 18px; border-radius:13px; border:1px solid rgba(var(--sb-ink), 0.1); background:rgba(var(--sb-ink), 0.03); color:var(--sb-text-6); font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:all .2s cubic-bezier(.2,.7,.3,1);">${t('pwa.dismiss')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -339,23 +354,34 @@ class PWAInstallPrompt {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const result = await this.deferredPrompt.prompt();
|
||||
const prompt = this.deferredPrompt;
|
||||
// Clear first: prompt() may only be called once per event, and a second
|
||||
// click while the sheet is open would otherwise throw.
|
||||
this.deferredPrompt = null;
|
||||
window.__pwaInstallEvent = null;
|
||||
|
||||
if (result.outcome === 'accepted') {
|
||||
this.isInstalled = true;
|
||||
try {
|
||||
// Chrome resolves prompt() with the choice; older implementations
|
||||
// resolve it with nothing and expose userChoice instead.
|
||||
const result = (await prompt.prompt()) || (await prompt.userChoice);
|
||||
const outcome = result && result.outcome;
|
||||
|
||||
if (outcome === 'accepted') {
|
||||
this.isInstalled = true;
|
||||
this.hideInstallPrompts();
|
||||
this.saveInstallPreference('accepted', true);
|
||||
this.saveInstallPreference('installed', true);
|
||||
} else {
|
||||
} else if (outcome === 'dismissed') {
|
||||
this.handleInstallDismissal();
|
||||
}
|
||||
|
||||
this.deferredPrompt = null;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Install prompt failed:', error);
|
||||
// A rejected prompt() has not been consumed — Chrome rejects it for a
|
||||
// missing user gesture too — so put the event back rather than
|
||||
// stranding the button on the manual guide for the rest of the visit.
|
||||
this.deferredPrompt = prompt;
|
||||
window.__pwaInstallEvent = prompt;
|
||||
this.showFallbackInstructions();
|
||||
}
|
||||
}
|
||||
@@ -366,28 +392,28 @@ class PWAInstallPrompt {
|
||||
// strings baked in English, which a thirteen-locale site cannot ship.
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'pwa-ios-install';
|
||||
modal.style.cssText = "position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center; padding:24px; background:rgba(8,8,10,0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); animation:igFade .3s ease; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
|
||||
modal.style.cssText = "position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center; padding:24px; background:rgba(var(--sb-scrim-rgb), 0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); animation:igFade .3s ease; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
|
||||
|
||||
const step = (n, title, hint, delay) => `
|
||||
<div style="display:flex; align-items:flex-start; gap:14px; padding:14px 16px; border-radius:13px; background:#161618; border:1px solid rgba(255,255,255,0.06); animation:igRow ${delay} cubic-bezier(.2,.7,.3,1);">
|
||||
<div style="flex:none; width:26px; height:26px; border-radius:9px; display:grid; place-items:center; background:rgba(240,137,42,0.12); border:1px solid rgba(240,137,42,0.28); font-size:13px; font-weight:800; color:#f0892a;">${n}</div>
|
||||
<div style="display:flex; align-items:flex-start; gap:14px; padding:14px 16px; border-radius:13px; background:var(--sb-surface); border:1px solid rgba(var(--sb-ink), 0.06); animation:igRow ${delay} cubic-bezier(.2,.7,.3,1);">
|
||||
<div style="flex:none; width:26px; height:26px; border-radius:9px; display:grid; place-items:center; background:rgba(var(--sb-orange-rgb), 0.12); border:1px solid rgba(var(--sb-orange-rgb), 0.28); font-size:13px; font-weight:800; color:var(--sb-orange);">${n}</div>
|
||||
<div style="flex:1; min-width:0;">
|
||||
<div style="font-size:14.5px; font-weight:700; color:#f4f4f6; margin-bottom:2px;">${title}</div>
|
||||
<div style="font-size:13px; line-height:1.45; color:#8a8a92;">${hint}</div>
|
||||
<div style="font-size:14.5px; font-weight:700; color:var(--sb-text-1); margin-bottom:2px;">${title}</div>
|
||||
<div style="font-size:13px; line-height:1.45; color:var(--sb-text-7);">${hint}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
modal.innerHTML = `
|
||||
<div style="position:relative; z-index:2; width:440px; max-width:calc(100vw - 48px); border-radius:22px; background:#121214; border:1px solid rgba(255,255,255,0.08); padding:34px 30px 26px; box-shadow:0 30px 70px rgba(0,0,0,0.6); animation:igPop .32s cubic-bezier(.2,.7,.3,1);">
|
||||
<button class="close-x" type="button" title="${t('pwa.close')}" aria-label="${t('pwa.close')}" style="position:absolute; top:18px; inset-inline-end:18px; width:30px; height:30px; padding:0; border-radius:9px; display:grid; place-items:center; border:1px solid rgba(255,255,255,0.08); background:rgba(255,255,255,0.02); color:#8a8a92; cursor:pointer; transition:all .18s cubic-bezier(.2,.7,.3,1);">
|
||||
<div style="position:relative; z-index:2; width:440px; max-width:calc(100vw - 48px); border-radius:22px; background:var(--sb-bg); border:1px solid rgba(var(--sb-ink), 0.08); padding:34px 30px 26px; box-shadow:0 30px 70px rgba(var(--sb-shadow-rgb), calc(0.6 * var(--sb-shadow-k))); animation:igPop .32s cubic-bezier(.2,.7,.3,1);">
|
||||
<button class="close-x" type="button" title="${t('pwa.close')}" aria-label="${t('pwa.close')}" style="position:absolute; top:18px; inset-inline-end:18px; width:30px; height:30px; padding:0; border-radius:9px; display:grid; place-items:center; border:1px solid rgba(var(--sb-ink), 0.08); background:rgba(var(--sb-ink), 0.02); color:var(--sb-text-7); cursor:pointer; transition:all .18s cubic-bezier(.2,.7,.3,1);">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</button>
|
||||
|
||||
<div style="text-align:center; margin-bottom:22px;">
|
||||
<div style="display:inline-flex; width:60px; height:60px; border-radius:16px; align-items:center; justify-content:center; background:rgba(240,137,42,0.12); border:1px solid rgba(240,137,42,0.3); margin-bottom:18px;">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#f0892a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V4M8.5 7.5L12 4l3.5 3.5"/><path d="M6 11H5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1h-1"/></svg>
|
||||
<div style="display:inline-flex; width:60px; height:60px; border-radius:16px; align-items:center; justify-content:center; background:rgba(var(--sb-orange-rgb), 0.12); border:1px solid rgba(var(--sb-orange-rgb), 0.3); margin-bottom:18px;">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="var(--sb-orange-solid)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V4M8.5 7.5L12 4l3.5 3.5"/><path d="M6 11H5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1h-1"/></svg>
|
||||
</div>
|
||||
<h3 style="margin:0; font-size:23px; font-weight:800; letter-spacing:-0.6px; color:#f4f4f6;">${t('pwa.iosTitle')}</h3>
|
||||
<h3 style="margin:0; font-size:23px; font-weight:800; letter-spacing:-0.6px; color:var(--sb-text-1);">${t('pwa.iosTitle')}</h3>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; flex-direction:column; gap:10px; margin-bottom:22px;">
|
||||
@@ -396,13 +422,13 @@ class PWAInstallPrompt {
|
||||
${step(3, t('pwa.iosStep3'), t('pwa.iosStep3Hint'), '.5s')}
|
||||
</div>
|
||||
|
||||
<button class="got-it" type="button" style="width:100%; padding:14px 20px; border-radius:13px; border:none; background:#f0892a; color:#1a0f04; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:background .2s cubic-bezier(.2,.7,.3,1);">${t('pwa.gotIt')}</button>
|
||||
<button class="got-it" type="button" style="width:100%; padding:14px 20px; border-radius:13px; border:none; background:var(--sb-orange-solid); color:var(--sb-on-accent); font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:background .2s cubic-bezier(.2,.7,.3,1);">${t('pwa.gotIt')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const closeX = modal.querySelector('.close-x');
|
||||
closeX.addEventListener('mouseenter', () => { closeX.style.color = '#e5727a'; closeX.style.borderColor = 'rgba(229,114,122,0.4)'; });
|
||||
closeX.addEventListener('mouseleave', () => { closeX.style.color = '#8a8a92'; closeX.style.borderColor = 'rgba(255,255,255,0.08)'; });
|
||||
closeX.addEventListener('mouseenter', () => { closeX.style.color = 'var(--sb-red)'; closeX.style.borderColor = 'rgba(var(--sb-red-rgb), 0.4)'; });
|
||||
closeX.addEventListener('mouseleave', () => { closeX.style.color = 'var(--sb-text-7)'; closeX.style.borderColor = 'rgba(var(--sb-ink), 0.08)'; });
|
||||
|
||||
const gotIt = modal.querySelector('.got-it');
|
||||
|
||||
@@ -441,7 +467,7 @@ class PWAInstallPrompt {
|
||||
// (Install Guide.dc.html). Styling is inline so it tracks the design.
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'pwa-install-guide';
|
||||
modal.style.cssText = "position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center; padding:24px; background:rgba(8,8,10,0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); animation:igFade .3s ease; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
|
||||
modal.style.cssText = "position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center; padding:24px; background:rgba(var(--sb-scrim-rgb), 0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); animation:igFade .3s ease; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
|
||||
|
||||
const rowIcon = {
|
||||
chromeEdge: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 9h18"/><path d="M12 12v4M10 14l2 2 2-2"/>',
|
||||
@@ -450,47 +476,55 @@ class PWAInstallPrompt {
|
||||
};
|
||||
|
||||
const row = (icon, title, desc, delay, nowrap) => `
|
||||
<div style="display:flex; align-items:center; gap:14px; padding:14px 16px; border-radius:13px; background:#161618; border:1px solid rgba(255,255,255,0.06); animation:igRow ${delay} cubic-bezier(.2,.7,.3,1);">
|
||||
<div style="flex:none; width:40px; height:40px; border-radius:11px; display:grid; place-items:center; background:rgba(240,137,42,0.1); border:1px solid rgba(240,137,42,0.22);">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f0892a" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">${icon}</svg>
|
||||
<div style="display:flex; align-items:center; gap:14px; padding:14px 16px; border-radius:13px; background:var(--sb-surface); border:1px solid rgba(var(--sb-ink), 0.06); animation:igRow ${delay} cubic-bezier(.2,.7,.3,1);">
|
||||
<div style="flex:none; width:40px; height:40px; border-radius:11px; display:grid; place-items:center; background:rgba(var(--sb-orange-rgb), 0.1); border:1px solid rgba(var(--sb-orange-rgb), 0.22);">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--sb-orange-solid)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">${icon}</svg>
|
||||
</div>
|
||||
<div style="flex:1; min-width:0;">
|
||||
<div style="font-size:14.5px; font-weight:700; color:#f4f4f6; margin-bottom:2px;">${title}</div>
|
||||
<div style="font-size:13px; color:#8a8a92;${nowrap ? ' white-space:nowrap;' : ''}">${desc}</div>
|
||||
<div style="font-size:14.5px; font-weight:700; color:var(--sb-text-1); margin-bottom:2px;">${title}</div>
|
||||
<div style="font-size:13px; color:var(--sb-text-7);${nowrap ? ' white-space:nowrap;' : ''}">${desc}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const isAndroid = /Android/i.test(navigator.userAgent);
|
||||
|
||||
modal.innerHTML = `
|
||||
<div style="position:relative; z-index:2; width:480px; max-width:calc(100vw - 48px); border-radius:22px; background:#121214; border:1px solid rgba(255,255,255,0.08); padding:34px 30px 26px; box-shadow:0 30px 70px rgba(0,0,0,0.6); animation:igPop .32s cubic-bezier(.2,.7,.3,1);">
|
||||
<button class="close-x" type="button" title="${t('pwa.close')}" aria-label="${t('pwa.close')}" style="position:absolute; top:18px; inset-inline-end:18px; width:30px; height:30px; padding:0; border-radius:9px; display:grid; place-items:center; border:1px solid rgba(255,255,255,0.08); background:rgba(255,255,255,0.02); color:#8a8a92; cursor:pointer; transition:all .18s cubic-bezier(.2,.7,.3,1);">
|
||||
<div style="position:relative; z-index:2; width:480px; max-width:calc(100vw - 48px); border-radius:22px; background:var(--sb-bg); border:1px solid rgba(var(--sb-ink), 0.08); padding:34px 30px 26px; box-shadow:0 30px 70px rgba(var(--sb-shadow-rgb), calc(0.6 * var(--sb-shadow-k))); animation:igPop .32s cubic-bezier(.2,.7,.3,1);">
|
||||
<button class="close-x" type="button" title="${t('pwa.close')}" aria-label="${t('pwa.close')}" style="position:absolute; top:18px; inset-inline-end:18px; width:30px; height:30px; padding:0; border-radius:9px; display:grid; place-items:center; border:1px solid rgba(var(--sb-ink), 0.08); background:rgba(var(--sb-ink), 0.02); color:var(--sb-text-7); cursor:pointer; transition:all .18s cubic-bezier(.2,.7,.3,1);">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</button>
|
||||
|
||||
<div style="text-align:center; margin-bottom:24px;">
|
||||
<div style="display:inline-flex; width:60px; height:60px; border-radius:16px; align-items:center; justify-content:center; background:rgba(240,137,42,0.12); border:1px solid rgba(240,137,42,0.3); margin-bottom:18px;">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#f0892a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v11"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 20h14"/></svg>
|
||||
<div style="display:inline-flex; width:60px; height:60px; border-radius:16px; align-items:center; justify-content:center; background:rgba(var(--sb-orange-rgb), 0.12); border:1px solid rgba(var(--sb-orange-rgb), 0.3); margin-bottom:18px;">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="var(--sb-orange-solid)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v11"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 20h14"/></svg>
|
||||
</div>
|
||||
<h3 style="margin:0 0 10px; font-size:24px; font-weight:800; letter-spacing:-0.6px; color:#f4f4f6;">${t('pwa.genericTitle')}</h3>
|
||||
<p style="margin:0 auto; max-width:380px; font-size:14px; line-height:1.55; color:#9a9aa2;">${t('pwa.genericDesc')}</p>
|
||||
<h3 style="margin:0 0 10px; font-size:24px; font-weight:800; letter-spacing:-0.6px; color:var(--sb-text-1);">${t('pwa.genericTitle')}</h3>
|
||||
<p style="margin:0 auto; max-width:380px; font-size:14px; line-height:1.55; color:var(--sb-text-6);">${t('pwa.genericDesc')}</p>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; flex-direction:column; gap:10px; margin-bottom:22px;">
|
||||
${row(rowIcon.chromeEdge, 'Chrome / Edge', 'Click the install icon in the address bar', '.34s', false)}
|
||||
${row(rowIcon.firefox, 'Firefox', 'Add a bookmark to your home screen', '.42s', false)}
|
||||
${row(rowIcon.safari, 'Safari', 'Share → Add to Home Screen', '.5s', true)}
|
||||
${isAndroid
|
||||
// A phone has no address bar to click an install icon in, and
|
||||
// telling an Android user to "add a bookmark" is what got them
|
||||
// a browser shortcut instead of an app in the first place.
|
||||
? row(rowIcon.chromeEdge, 'Chrome', t('pwa.androidChromeHint'), '.34s', false) +
|
||||
row(rowIcon.firefox, 'Firefox / Samsung Internet', t('pwa.androidOtherHint'), '.42s', false)
|
||||
: row(rowIcon.chromeEdge, 'Chrome / Edge', 'Click the install icon in the address bar', '.34s', false) +
|
||||
row(rowIcon.firefox, 'Firefox', 'Add a bookmark to your home screen', '.42s', false) +
|
||||
row(rowIcon.safari, 'Safari', 'Share → Add to Home Screen', '.5s', true)}
|
||||
</div>
|
||||
|
||||
<button class="got-it" type="button" style="width:100%; padding:14px 20px; border-radius:13px; border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.03); color:#e8e8eb; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:all .2s cubic-bezier(.2,.7,.3,1);">${t('pwa.gotIt')}</button>
|
||||
<button class="got-it" type="button" style="width:100%; padding:14px 20px; border-radius:13px; border:1px solid rgba(var(--sb-ink), 0.1); background:rgba(var(--sb-ink), 0.03); color:var(--sb-text-2); font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:all .2s cubic-bezier(.2,.7,.3,1);">${t('pwa.gotIt')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const closeX = modal.querySelector('.close-x');
|
||||
closeX.addEventListener('mouseenter', () => { closeX.style.color = '#e5727a'; closeX.style.borderColor = 'rgba(229,114,122,0.4)'; });
|
||||
closeX.addEventListener('mouseleave', () => { closeX.style.color = '#8a8a92'; closeX.style.borderColor = 'rgba(255,255,255,0.08)'; });
|
||||
closeX.addEventListener('mouseenter', () => { closeX.style.color = 'var(--sb-red)'; closeX.style.borderColor = 'rgba(var(--sb-red-rgb), 0.4)'; });
|
||||
closeX.addEventListener('mouseleave', () => { closeX.style.color = 'var(--sb-text-7)'; closeX.style.borderColor = 'rgba(var(--sb-ink), 0.08)'; });
|
||||
|
||||
const gotIt = modal.querySelector('.got-it');
|
||||
gotIt.addEventListener('mouseenter', () => { gotIt.style.borderColor = 'rgba(255,255,255,0.22)'; gotIt.style.background = 'rgba(255,255,255,0.06)'; });
|
||||
gotIt.addEventListener('mouseleave', () => { gotIt.style.borderColor = 'rgba(255,255,255,0.1)'; gotIt.style.background = 'rgba(255,255,255,0.03)'; });
|
||||
gotIt.addEventListener('mouseenter', () => { gotIt.style.borderColor = 'rgba(var(--sb-ink), 0.22)'; gotIt.style.background = 'rgba(var(--sb-ink), 0.06)'; });
|
||||
gotIt.addEventListener('mouseleave', () => { gotIt.style.borderColor = 'rgba(var(--sb-ink), 0.1)'; gotIt.style.background = 'rgba(var(--sb-ink), 0.03)'; });
|
||||
|
||||
const close = () => modal.remove();
|
||||
closeX.addEventListener('click', close);
|
||||
@@ -514,7 +548,7 @@ class PWAInstallPrompt {
|
||||
notification.style.cssText =
|
||||
"position:fixed; top:calc(16px + var(--sb-safe-top, 0px)); inset-inline-end:16px; inset-inline-start:auto; max-width:min(360px, calc(100vw - 32px)); z-index:9999; " +
|
||||
"font-family:'Manrope',system-ui,-apple-system,sans-serif; " +
|
||||
"border-radius:15px; background:#121214; border:1px solid rgba(62,207,142,0.28); padding:15px 17px; box-shadow:0 20px 50px rgba(0,0,0,0.55); " +
|
||||
"border-radius:15px; background:var(--sb-bg); border:1px solid rgba(var(--sb-green-rgb), 0.28); padding:15px 17px; box-shadow:0 20px 50px rgba(var(--sb-shadow-rgb), calc(0.55 * var(--sb-shadow-k))); " +
|
||||
"transform:translateX(calc(100% + 24px)); opacity:0; " +
|
||||
(prefersReducedMotion()
|
||||
? "transition:opacity .2s linear;"
|
||||
@@ -522,12 +556,12 @@ class PWAInstallPrompt {
|
||||
|
||||
notification.innerHTML = `
|
||||
<div style="display:flex; align-items:center; gap:13px;">
|
||||
<div style="flex:none; width:36px; height:36px; border-radius:11px; display:grid; place-items:center; background:rgba(62,207,142,0.12); border:1px solid rgba(62,207,142,0.28);">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#3ecf8e" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
<div style="flex:none; width:36px; height:36px; border-radius:11px; display:grid; place-items:center; background:rgba(var(--sb-green-rgb), 0.12); border:1px solid rgba(var(--sb-green-rgb), 0.28);">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--sb-green-solid)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
</div>
|
||||
<div style="flex:1; min-width:0;">
|
||||
<div style="font-size:14.5px; font-weight:800; letter-spacing:-0.2px; color:#f4f4f6; margin-bottom:2px;">${t('pwa.installedTitle')}</div>
|
||||
<div style="font-size:12.5px; line-height:1.4; color:#8a8a92;">${successText}</div>
|
||||
<div style="font-size:14.5px; font-weight:800; letter-spacing:-0.2px; color:var(--sb-text-1); margin-bottom:2px;">${t('pwa.installedTitle')}</div>
|
||||
<div style="font-size:12.5px; line-height:1.4; color:var(--sb-text-7);">${successText}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
+41
-41
@@ -248,12 +248,12 @@ class PWAOfflineManager {
|
||||
// Clean pill matching the app's design language (no emoji, no FontAwesome,
|
||||
// proper SVG close wired via a real listener — the old inline onclick was
|
||||
// blocked by the CSP anyway).
|
||||
const PILL = "display:inline-flex; align-items:center; gap:10px; padding:9px 14px; border-radius:11px; background:#161618; box-shadow:0 12px 30px rgba(0,0,0,0.45); font-family:'Manrope',system-ui,-apple-system,sans-serif; font-size:13px; font-weight:600; color:#e8e8eb;";
|
||||
const PILL = "display:inline-flex; align-items:center; gap:10px; padding:9px 14px; border-radius:11px; background:var(--sb-surface); box-shadow:0 12px 30px rgba(var(--sb-shadow-rgb), calc(0.45 * var(--sb-shadow-k))); font-family:'Manrope',system-ui,-apple-system,sans-serif; font-size:13px; font-weight:600; color:var(--sb-text-2);";
|
||||
|
||||
if (isOnline) {
|
||||
this.offlineIndicator.innerHTML =
|
||||
`<div style="${PILL} border:1px solid rgba(62,207,142,0.3);">
|
||||
<span style="width:8px; height:8px; border-radius:50%; background:#3ecf8e; box-shadow:0 0 8px rgba(62,207,142,0.6);"></span>
|
||||
`<div style="${PILL} border:1px solid rgba(var(--sb-green-rgb), 0.3);">
|
||||
<span style="width:8px; height:8px; border-radius:50%; background:var(--sb-green-solid); box-shadow:0 0 8px rgba(var(--sb-green-rgb), 0.6);"></span>
|
||||
<span>${t('offline.backOnline')}</span>
|
||||
</div>`;
|
||||
this.offlineIndicator.classList.remove('hidden');
|
||||
@@ -264,18 +264,18 @@ class PWAOfflineManager {
|
||||
}, 3000);
|
||||
} else {
|
||||
this.offlineIndicator.innerHTML =
|
||||
`<div style="${PILL} border:1px solid rgba(227,179,65,0.32);">
|
||||
<span style="width:8px; height:8px; border-radius:50%; background:#e3b341;"></span>
|
||||
`<div style="${PILL} border:1px solid rgba(var(--sb-yellow-2-rgb), 0.32);">
|
||||
<span style="width:8px; height:8px; border-radius:50%; background:var(--sb-yellow-2-solid);"></span>
|
||||
<span>${t('offline.mode')}</span>
|
||||
<button class="oi-close" type="button" aria-label="${t('offline.dismiss')}" style="margin-inline-start:4px; width:22px; height:22px; padding:0; display:grid; place-items:center; border:none; background:transparent; color:#8a8a92; cursor:pointer; border-radius:6px; transition:color .15s ease;">
|
||||
<button class="oi-close" type="button" aria-label="${t('offline.dismiss')}" style="margin-inline-start:4px; width:22px; height:22px; padding:0; display:grid; place-items:center; border:none; background:transparent; color:var(--sb-text-7); cursor:pointer; border-radius:6px; transition:color .15s ease;">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
this.offlineIndicator.classList.remove('hidden');
|
||||
const closeBtn = this.offlineIndicator.querySelector('.oi-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('mouseenter', () => { closeBtn.style.color = '#e8e8eb'; });
|
||||
closeBtn.addEventListener('mouseleave', () => { closeBtn.style.color = '#8a8a92'; });
|
||||
closeBtn.addEventListener('mouseenter', () => { closeBtn.style.color = 'var(--sb-text-2)'; });
|
||||
closeBtn.addEventListener('mouseleave', () => { closeBtn.style.color = 'var(--sb-text-7)'; });
|
||||
closeBtn.addEventListener('click', () => this.offlineIndicator.classList.add('hidden'));
|
||||
}
|
||||
}
|
||||
@@ -361,46 +361,46 @@ class PWAOfflineManager {
|
||||
|
||||
const guidance = document.createElement('div');
|
||||
guidance.id = 'pwa-offline-modal';
|
||||
guidance.style.cssText = "position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center; padding:24px; background:rgba(8,8,10,0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); animation:omFade .3s ease; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
|
||||
guidance.style.cssText = "position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center; padding:24px; background:rgba(var(--sb-scrim-rgb), 0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); animation:omFade .3s ease; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
|
||||
|
||||
const feature = (bg, bd, stroke, sw, icon, text) => `
|
||||
<div style="display:flex; align-items:center; gap:13px;">
|
||||
<span style="flex:none; width:34px; height:34px; border-radius:9px; display:grid; place-items:center; background:${bg}; border:1px solid ${bd};"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="${stroke}" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round">${icon}</svg></span>
|
||||
<span style="font-size:14.5px; color:#e8e8eb;">${text}</span>
|
||||
<span style="font-size:14.5px; color:var(--sb-text-2);">${text}</span>
|
||||
</div>`;
|
||||
|
||||
const card = (bg, bd, stroke, icon, title, desc) => `
|
||||
<div style="display:flex; align-items:flex-start; gap:13px; padding:14px 16px; border-radius:13px; background:#161618; border:1px solid rgba(255,255,255,0.06);">
|
||||
<div style="display:flex; align-items:flex-start; gap:13px; padding:14px 16px; border-radius:13px; background:var(--sb-surface); border:1px solid rgba(var(--sb-ink), 0.06);">
|
||||
<span style="flex:none; width:36px; height:36px; border-radius:10px; display:grid; place-items:center; background:${bg}; border:1px solid ${bd};"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="${stroke}" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">${icon}</svg></span>
|
||||
<div><div style="font-size:14.5px; font-weight:700; color:#f4f4f6; margin-bottom:2px;">${title}</div><div style="font-size:13px; line-height:1.5; color:#8a8a92;">${desc}</div></div>
|
||||
<div><div style="font-size:14.5px; font-weight:700; color:var(--sb-text-1); margin-bottom:2px;">${title}</div><div style="font-size:13px; line-height:1.5; color:var(--sb-text-7);">${desc}</div></div>
|
||||
</div>`;
|
||||
|
||||
const GREEN_BG = 'rgba(62,207,142,0.12)', GREEN_BD = 'rgba(62,207,142,0.24)';
|
||||
const ORANGE_BG = 'rgba(240,137,42,0.12)', ORANGE_BD = 'rgba(240,137,42,0.24)';
|
||||
const GREEN_BG = 'rgba(var(--sb-green-rgb), 0.12)', GREEN_BD = 'rgba(var(--sb-green-rgb), 0.24)';
|
||||
const ORANGE_BG = 'rgba(var(--sb-orange-rgb), 0.12)', ORANGE_BD = 'rgba(var(--sb-orange-rgb), 0.24)';
|
||||
|
||||
const mainHTML = `
|
||||
<div style="animation:omSwap .26s cubic-bezier(.2,.7,.3,1);">
|
||||
<div style="text-align:center; margin-bottom:22px;">
|
||||
<div style="display:inline-flex; width:64px; height:64px; border-radius:50%; align-items:center; justify-content:center; background:rgba(227,179,65,0.12); border:1px solid rgba(227,179,65,0.3); margin-bottom:18px;">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#e3b341" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8.8a15 15 0 0 1 20 0"/><path d="M5 12.5a11 11 0 0 1 14 0"/><path d="M8.5 16.3a6 6 0 0 1 7 0"/><path d="M12 20h.01"/><path d="M2 2l20 20"/></svg>
|
||||
<div style="display:inline-flex; width:64px; height:64px; border-radius:50%; align-items:center; justify-content:center; background:rgba(var(--sb-yellow-2-rgb), 0.12); border:1px solid rgba(var(--sb-yellow-2-rgb), 0.3); margin-bottom:18px;">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--sb-yellow-2-solid)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8.8a15 15 0 0 1 20 0"/><path d="M5 12.5a11 11 0 0 1 14 0"/><path d="M8.5 16.3a6 6 0 0 1 7 0"/><path d="M12 20h.01"/><path d="M2 2l20 20"/></svg>
|
||||
</div>
|
||||
<h3 style="margin:0 0 10px; font-size:24px; font-weight:800; letter-spacing:-0.6px; color:#f4f4f6;">${t('offline.lostTitle')}</h3>
|
||||
<p style="margin:0 auto; max-width:380px; font-size:14px; line-height:1.55; color:#9a9aa2;">${t('offline.lostDesc')}</p>
|
||||
<h3 style="margin:0 0 10px; font-size:24px; font-weight:800; letter-spacing:-0.6px; color:var(--sb-text-1);">${t('offline.lostTitle')}</h3>
|
||||
<p style="margin:0 auto; max-width:380px; font-size:14px; line-height:1.55; color:var(--sb-text-6);">${t('offline.lostDesc')}</p>
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:14px; margin-bottom:24px; padding:0 6px;">
|
||||
${feature(GREEN_BG, GREEN_BD, '#3ecf8e', '2.3', '<path d="M5 13l4 4 10-11"/>', t('offline.point1'))}
|
||||
${feature(GREEN_BG, GREEN_BD, '#3ecf8e', '1.9', '<path d="M12 3l8 4v5c0 4.5-3.2 7.8-8 9-4.8-1.2-8-4.5-8-9V7l8-4z"/>', t('offline.point2'))}
|
||||
${feature(ORANGE_BG, ORANGE_BD, '#f0892a', '1.9', '<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"/>', t('offline.point3'))}
|
||||
${feature(GREEN_BG, GREEN_BD, 'var(--sb-green)', '2.3', '<path d="M5 13l4 4 10-11"/>', t('offline.point1'))}
|
||||
${feature(GREEN_BG, GREEN_BD, 'var(--sb-green)', '1.9', '<path d="M12 3l8 4v5c0 4.5-3.2 7.8-8 9-4.8-1.2-8-4.5-8-9V7l8-4z"/>', t('offline.point2'))}
|
||||
${feature(ORANGE_BG, ORANGE_BD, 'var(--sb-orange)', '1.9', '<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"/>', t('offline.point3'))}
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:11px;">
|
||||
<div style="display:flex; gap:12px;">
|
||||
<button class="om-continue" type="button" style="flex:1; padding:14px 18px; border-radius:13px; border:none; background:#f0892a; color:#1a0f04; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; box-shadow:0 8px 24px rgba(240,137,42,0.28); transition:all .2s cubic-bezier(.2,.7,.3,1);">${t('offline.continue')}</button>
|
||||
<button class="om-disconnect" type="button" style="flex:1; display:inline-flex; align-items:center; justify-content:center; gap:9px; padding:14px 18px; border-radius:13px; border:1px solid rgba(229,114,122,0.3); background:rgba(229,114,122,0.08); color:#e5727a; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:all .2s cubic-bezier(.2,.7,.3,1);">
|
||||
<button class="om-continue" type="button" style="flex:1; padding:14px 18px; border-radius:13px; border:none; background:var(--sb-orange-solid); color:var(--sb-on-accent); font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; box-shadow:0 8px 24px rgba(var(--sb-orange-rgb), 0.28); transition:all .2s cubic-bezier(.2,.7,.3,1);">${t('offline.continue')}</button>
|
||||
<button class="om-disconnect" type="button" style="flex:1; display:inline-flex; align-items:center; justify-content:center; gap:9px; padding:14px 18px; border-radius:13px; border:1px solid rgba(var(--sb-red-rgb), 0.3); background:rgba(var(--sb-red-rgb), 0.08); color:var(--sb-red); font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:all .2s cubic-bezier(.2,.7,.3,1);">
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><path d="M12 4v8"/><path d="M7 7a8 8 0 1 0 10 0"/></svg>
|
||||
${t('offline.disconnect')}
|
||||
</button>
|
||||
</div>
|
||||
<button class="om-learn" type="button" style="width:100%; display:inline-flex; align-items:center; justify-content:center; gap:8px; padding:12px 18px; border-radius:13px; border:none; background:transparent; color:#9a9aa2; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:color .18s cubic-bezier(.2,.7,.3,1);">
|
||||
<button class="om-learn" type="button" style="width:100%; display:inline-flex; align-items:center; justify-content:center; gap:8px; padding:12px 18px; border-radius:13px; border:none; background:transparent; color:var(--sb-text-6); font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:color .18s cubic-bezier(.2,.7,.3,1);">
|
||||
${t('offline.learnMore')}
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" class="sb-mirror-rtl" style="pointer-events:none;"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
|
||||
</button>
|
||||
@@ -410,28 +410,28 @@ class PWAOfflineManager {
|
||||
const detailsHTML = `
|
||||
<div style="animation:omSwap .26s cubic-bezier(.2,.7,.3,1);">
|
||||
<div style="display:flex; align-items:center; gap:12px; margin-bottom:18px;">
|
||||
<button class="om-back" type="button" title="Back" style="flex:none; width:34px; height:34px; border-radius:10px; display:grid; place-items:center; border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.025); color:#cfcfd4; cursor:pointer; transition:all .18s cubic-bezier(.2,.7,.3,1);">
|
||||
<button class="om-back" type="button" title="Back" style="flex:none; width:34px; height:34px; border-radius:10px; display:grid; place-items:center; 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 .18s cubic-bezier(.2,.7,.3,1);">
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" class="sb-mirror-rtl" style="pointer-events:none;"><path d="M15 6l-6 6 6 6"/></svg>
|
||||
</button>
|
||||
<h3 style="margin:0; font-size:20px; font-weight:800; letter-spacing:-0.5px; color:#f4f4f6;">${t('offline.restoredTitle')}</h3>
|
||||
<h3 style="margin:0; font-size:20px; font-weight:800; letter-spacing:-0.5px; color:var(--sb-text-1);">${t('offline.restoredTitle')}</h3>
|
||||
</div>
|
||||
<p style="margin:0 0 20px; font-size:14px; line-height:1.6; color:#9a9aa2;">${t('offline.restoredDesc')}</p>
|
||||
<p style="margin:0 0 20px; font-size:14px; line-height:1.6; color:var(--sb-text-6);">${t('offline.restoredDesc')}</p>
|
||||
<div style="display:flex; flex-direction:column; gap:11px; margin-bottom:22px;">
|
||||
${card(GREEN_BG, GREEN_BD, '#3ecf8e', '<path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4z"/>', t('offline.r1Title'), t('offline.r1Desc'))}
|
||||
${card(GREEN_BG, GREEN_BD, '#3ecf8e', '<path d="M14 3v5h5"/><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M12 18v-6M9.5 14.5L12 12l2.5 2.5"/>', t('offline.r2Title'), t('offline.r2Desc'))}
|
||||
${card(GREEN_BG, GREEN_BD, '#3ecf8e', '<path d="M12 3v12"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 20h14"/>', t('offline.r3Title'), t('offline.r3Desc'))}
|
||||
${card(ORANGE_BG, ORANGE_BD, '#f0892a', '<path d="M12 3l8 4v5c0 4.5-3.2 7.8-8 9-4.8-1.2-8-4.5-8-9V7l8-4z"/><path d="M9.2 12.2l2 2 3.6-3.8"/>', t('offline.r4Title'), t('offline.r4Desc'))}
|
||||
${card(GREEN_BG, GREEN_BD, 'var(--sb-green)', '<path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4z"/>', t('offline.r1Title'), t('offline.r1Desc'))}
|
||||
${card(GREEN_BG, GREEN_BD, 'var(--sb-green)', '<path d="M14 3v5h5"/><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M12 18v-6M9.5 14.5L12 12l2.5 2.5"/>', t('offline.r2Title'), t('offline.r2Desc'))}
|
||||
${card(GREEN_BG, GREEN_BD, 'var(--sb-green)', '<path d="M12 3v12"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 20h14"/>', t('offline.r3Title'), t('offline.r3Desc'))}
|
||||
${card(ORANGE_BG, ORANGE_BD, 'var(--sb-orange)', '<path d="M12 3l8 4v5c0 4.5-3.2 7.8-8 9-4.8-1.2-8-4.5-8-9V7l8-4z"/><path d="M9.2 12.2l2 2 3.6-3.8"/>', t('offline.r4Title'), t('offline.r4Desc'))}
|
||||
</div>
|
||||
<button class="om-gotit" type="button" style="width:100%; padding:14px 18px; border-radius:13px; border:none; background:#f0892a; color:#1a0f04; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; box-shadow:0 8px 24px rgba(240,137,42,0.28); transition:all .2s cubic-bezier(.2,.7,.3,1);">${t('offline.gotIt')}</button>
|
||||
<button class="om-gotit" type="button" style="width:100%; padding:14px 18px; border-radius:13px; border:none; background:var(--sb-orange-solid); color:var(--sb-on-accent); font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; box-shadow:0 8px 24px rgba(var(--sb-orange-rgb), 0.28); transition:all .2s cubic-bezier(.2,.7,.3,1);">${t('offline.gotIt')}</button>
|
||||
</div>`;
|
||||
|
||||
const cardWrap = document.createElement('div');
|
||||
cardWrap.style.cssText = "position:relative; z-index:2; width:470px; max-width:calc(100vw - 48px); border-radius:22px; background:#121214; border:1px solid rgba(255,255,255,0.08); padding:34px 30px 26px; box-shadow:0 30px 70px rgba(0,0,0,0.6); animation:omPop .32s cubic-bezier(.2,.7,.3,1);";
|
||||
cardWrap.style.cssText = "position:relative; z-index:2; width:470px; max-width:calc(100vw - 48px); border-radius:22px; background:var(--sb-bg); border:1px solid rgba(var(--sb-ink), 0.08); padding:34px 30px 26px; box-shadow:0 30px 70px rgba(var(--sb-shadow-rgb), calc(0.6 * var(--sb-shadow-k))); animation:omPop .32s cubic-bezier(.2,.7,.3,1);";
|
||||
guidance.appendChild(cardWrap);
|
||||
|
||||
const hoverLift = (btn) => {
|
||||
btn.addEventListener('mouseenter', () => { btn.style.background = '#ff9637'; btn.style.transform = 'translateY(-2px)'; });
|
||||
btn.addEventListener('mouseleave', () => { btn.style.background = '#f0892a'; btn.style.transform = 'none'; });
|
||||
btn.addEventListener('mouseenter', () => { btn.style.background = 'var(--sb-orange-hi)'; btn.style.transform = 'translateY(-2px)'; });
|
||||
btn.addEventListener('mouseleave', () => { btn.style.background = 'var(--sb-orange)'; btn.style.transform = 'none'; });
|
||||
};
|
||||
const close = () => guidance.remove();
|
||||
|
||||
@@ -442,8 +442,8 @@ class PWAOfflineManager {
|
||||
cont.addEventListener('click', close);
|
||||
|
||||
const disc = cardWrap.querySelector('.om-disconnect');
|
||||
disc.addEventListener('mouseenter', () => { disc.style.background = 'rgba(229,114,122,0.14)'; disc.style.borderColor = 'rgba(229,114,122,0.5)'; });
|
||||
disc.addEventListener('mouseleave', () => { disc.style.background = 'rgba(229,114,122,0.08)'; disc.style.borderColor = 'rgba(229,114,122,0.3)'; });
|
||||
disc.addEventListener('mouseenter', () => { disc.style.background = 'rgba(var(--sb-red-rgb), 0.14)'; disc.style.borderColor = 'rgba(var(--sb-red-rgb), 0.5)'; });
|
||||
disc.addEventListener('mouseleave', () => { disc.style.background = 'rgba(var(--sb-red-rgb), 0.08)'; disc.style.borderColor = 'rgba(var(--sb-red-rgb), 0.3)'; });
|
||||
disc.addEventListener('click', () => {
|
||||
try {
|
||||
if (window.webrtcManager && typeof window.webrtcManager.disconnect === 'function') {
|
||||
@@ -454,16 +454,16 @@ class PWAOfflineManager {
|
||||
});
|
||||
|
||||
const learn = cardWrap.querySelector('.om-learn');
|
||||
learn.addEventListener('mouseenter', () => { learn.style.color = '#f0892a'; });
|
||||
learn.addEventListener('mouseleave', () => { learn.style.color = '#9a9aa2'; });
|
||||
learn.addEventListener('mouseenter', () => { learn.style.color = 'var(--sb-orange)'; });
|
||||
learn.addEventListener('mouseleave', () => { learn.style.color = 'var(--sb-text-6)'; });
|
||||
learn.addEventListener('click', renderDetails);
|
||||
};
|
||||
|
||||
const renderDetails = () => {
|
||||
cardWrap.innerHTML = detailsHTML;
|
||||
const back = cardWrap.querySelector('.om-back');
|
||||
back.addEventListener('mouseenter', () => { back.style.color = '#f0892a'; back.style.borderColor = 'rgba(240,137,42,0.45)'; });
|
||||
back.addEventListener('mouseleave', () => { back.style.color = '#cfcfd4'; back.style.borderColor = 'rgba(255,255,255,0.1)'; });
|
||||
back.addEventListener('mouseenter', () => { back.style.color = 'var(--sb-orange)'; back.style.borderColor = 'rgba(var(--sb-orange-rgb), 0.45)'; });
|
||||
back.addEventListener('mouseleave', () => { back.style.color = 'var(--sb-text-4)'; back.style.borderColor = 'rgba(var(--sb-ink), 0.1)'; });
|
||||
back.addEventListener('click', renderMain);
|
||||
|
||||
const gotit = cardWrap.querySelector('.om-gotit');
|
||||
|
||||
+18
-18
@@ -60,47 +60,47 @@ window.showUpdateNotification = function showUpdateNotification() {
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'pwa-update-modal';
|
||||
modal.style.cssText = "position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center; padding:24px; background:rgba(8,8,10,0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); animation:unFade .3s ease; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
|
||||
modal.style.cssText = "position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center; padding:24px; background:rgba(var(--sb-scrim-rgb), 0.55); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); animation:unFade .3s ease; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
|
||||
|
||||
modal.innerHTML = `
|
||||
<div style="position:relative; width:440px; max-width:calc(100vw - 48px); border-radius:22px; background:#121214; border:1px solid rgba(255,255,255,0.08); padding:36px 32px 28px; text-align:center; box-shadow:0 30px 70px rgba(0,0,0,0.6); animation:unPop .32s cubic-bezier(.2,.7,.3,1);">
|
||||
<div style="display:inline-flex; width:64px; height:64px; border-radius:50%; align-items:center; justify-content:center; background:rgba(240,137,42,0.12); border:1px solid rgba(240,137,42,0.3); margin-bottom:20px;">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#f0892a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="animation:unSpin 6s linear infinite;"><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"/></svg>
|
||||
<div style="position:relative; width:440px; max-width:calc(100vw - 48px); border-radius:22px; background:var(--sb-bg); border:1px solid rgba(var(--sb-ink), 0.08); padding:36px 32px 28px; text-align:center; box-shadow:0 30px 70px rgba(var(--sb-shadow-rgb), calc(0.6 * var(--sb-shadow-k))); animation:unPop .32s cubic-bezier(.2,.7,.3,1);">
|
||||
<div style="display:inline-flex; width:64px; height:64px; border-radius:50%; align-items:center; justify-content:center; background:rgba(var(--sb-orange-rgb), 0.12); border:1px solid rgba(var(--sb-orange-rgb), 0.3); margin-bottom:20px;">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--sb-orange-solid)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="animation:unSpin 6s linear infinite;"><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"/></svg>
|
||||
</div>
|
||||
<h2 style="margin:0 0 9px; font-size:26px; font-weight:800; letter-spacing:-0.7px; color:#f4f4f6;">Update available</h2>
|
||||
<p style="margin:0 0 24px; font-size:14.5px; line-height:1.55; color:#9a9aa2;">A newer version of SecureBit has been detected.</p>
|
||||
<h2 style="margin:0 0 9px; font-size:26px; font-weight:800; letter-spacing:-0.7px; color:var(--sb-text-1);">Update available</h2>
|
||||
<p style="margin:0 0 24px; font-size:14.5px; line-height:1.55; color:var(--sb-text-6);">A newer version of SecureBit has been detected.</p>
|
||||
|
||||
<div style="border-radius:14px; background:#0c0c0e; border:1px solid rgba(255,255,255,0.06); padding:16px 18px; margin-bottom:24px; text-align:start;">
|
||||
<div style="border-radius:14px; background:var(--sb-bg-deep); border:1px solid rgba(var(--sb-ink), 0.06); padding:16px 18px; margin-bottom:24px; text-align:start;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:14px; padding:5px 0;">
|
||||
<span style="font-size:13.5px; font-weight:500; color:#8a8a92;">Current version</span>
|
||||
<span class="cur-ver" style="font-family:'JetBrains Mono',ui-monospace,Menlo,monospace; font-size:13px; font-weight:500; color:#9a9aa2; white-space:nowrap;">${currentStr}</span>
|
||||
<span style="font-size:13.5px; font-weight:500; color:var(--sb-text-7);">Current version</span>
|
||||
<span class="cur-ver" style="font-family:'JetBrains Mono',ui-monospace,Menlo,monospace; font-size:13px; font-weight:500; color:var(--sb-text-6); white-space:nowrap;">${currentStr}</span>
|
||||
</div>
|
||||
<div style="height:1px; background:rgba(255,255,255,0.05); margin:4px 0;"></div>
|
||||
<div style="height:1px; background:rgba(var(--sb-ink), 0.05); margin:4px 0;"></div>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:14px; padding:5px 0;">
|
||||
<span style="display:inline-flex; align-items:center; gap:8px; font-size:13.5px; font-weight:600; color:#e8e8eb;"><span style="width:6px; height:6px; border-radius:50%; background:#f0892a;"></span>New version</span>
|
||||
<span class="new-ver" style="font-family:'JetBrains Mono',ui-monospace,Menlo,monospace; font-size:13px; font-weight:700; color:#f0892a; white-space:nowrap;">Latest</span>
|
||||
<span style="display:inline-flex; align-items:center; gap:8px; font-size:13.5px; font-weight:600; color:var(--sb-text-2);"><span style="width:6px; height:6px; border-radius:50%; background:var(--sb-orange-solid);"></span>New version</span>
|
||||
<span class="new-ver" style="font-family:'JetBrains Mono',ui-monospace,Menlo,monospace; font-size:13px; font-weight:700; color:var(--sb-orange); white-space:nowrap;">Latest</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:12px;">
|
||||
<button class="upd-now" type="button" style="flex:1; display:inline-flex; align-items:center; justify-content:center; gap:10px; padding:15px 20px; border-radius:13px; border:none; background:#f0892a; color:#1a0f04; font-family:inherit; font-size:15.5px; font-weight:700; letter-spacing:-0.2px; cursor:pointer; box-shadow:0 8px 24px rgba(240,137,42,0.28); transition:all .2s cubic-bezier(.2,.7,.3,1);">
|
||||
<button class="upd-now" type="button" style="flex:1; display:inline-flex; align-items:center; justify-content:center; gap:10px; padding:15px 20px; border-radius:13px; border:none; background:var(--sb-orange-solid); color:var(--sb-on-accent); font-family:inherit; font-size:15.5px; font-weight:700; letter-spacing:-0.2px; cursor:pointer; box-shadow:0 8px 24px rgba(var(--sb-orange-rgb), 0.28); transition:all .2s cubic-bezier(.2,.7,.3,1);">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><path d="M12 3v11"/><path d="M7.5 10.5L12 15l4.5-4.5"/><path d="M5 20h14"/></svg>
|
||||
Update now
|
||||
</button>
|
||||
<button class="upd-later" type="button" title="Later" style="flex:none; width:50px; height:50px; border-radius:13px; display:grid; place-items: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);">
|
||||
<button class="upd-later" type="button" title="Later" style="flex:none; width:50px; height:50px; border-radius:13px; display:grid; place-items: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);">
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const updNow = modal.querySelector('.upd-now');
|
||||
updNow.addEventListener('mouseenter', () => { updNow.style.background = '#ff9637'; updNow.style.transform = 'translateY(-2px)'; });
|
||||
updNow.addEventListener('mouseleave', () => { updNow.style.background = '#f0892a'; updNow.style.transform = 'none'; });
|
||||
updNow.addEventListener('mouseenter', () => { updNow.style.background = 'var(--sb-orange-hi)'; updNow.style.transform = 'translateY(-2px)'; });
|
||||
updNow.addEventListener('mouseleave', () => { updNow.style.background = 'var(--sb-orange)'; updNow.style.transform = 'none'; });
|
||||
updNow.addEventListener('click', () => window.location.reload());
|
||||
|
||||
const updLater = modal.querySelector('.upd-later');
|
||||
updLater.addEventListener('mouseenter', () => { updLater.style.color = '#e5727a'; updLater.style.borderColor = 'rgba(229,114,122,0.4)'; });
|
||||
updLater.addEventListener('mouseleave', () => { updLater.style.color = '#9a9aa2'; updLater.style.borderColor = 'rgba(255,255,255,0.1)'; });
|
||||
updLater.addEventListener('mouseenter', () => { updLater.style.color = 'var(--sb-red)'; updLater.style.borderColor = 'rgba(var(--sb-red-rgb), 0.4)'; });
|
||||
updLater.addEventListener('mouseleave', () => { updLater.style.color = 'var(--sb-text-6)'; updLater.style.borderColor = 'rgba(var(--sb-ink), 0.1)'; });
|
||||
updLater.addEventListener('click', () => modal.remove());
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// beforeinstallprompt lands once, early, and never comes back.
|
||||
//
|
||||
// Chrome fires it as soon as it has decided the page is installable — which on
|
||||
// Android is usually before install-prompt.js (a module, so deferred) has even
|
||||
// been evaluated. A listener registered later never sees it, and the event is
|
||||
// not replayed. Worse, without preventDefault() Chrome runs its own default
|
||||
// action for the event, and the object we stashed can no longer be prompted
|
||||
// with: prompt() rejects, handleInstallClick() falls through to the manual
|
||||
// guide, and the user ends up using "Add to Home screen" — which mints a
|
||||
// browser shortcut, not a WebAPK. That is the whole bug: the app looked
|
||||
// installable, the button did nothing real, and the icon that landed on the
|
||||
// home screen opened a Chrome tab.
|
||||
//
|
||||
// So this file is a classic, blocking <script> in <head>: it is the first
|
||||
// listener on the page, it takes the event out of Chrome's hands, and it holds
|
||||
// it until PWAInstallPrompt is ready to adopt it.
|
||||
(function () {
|
||||
if (window.__pwaInstallCapture) return;
|
||||
window.__pwaInstallCapture = true;
|
||||
|
||||
window.__pwaInstallEvent = null;
|
||||
window.__pwaAppInstalled = false;
|
||||
|
||||
window.addEventListener('beforeinstallprompt', function (event) {
|
||||
event.preventDefault();
|
||||
window.__pwaInstallEvent = event;
|
||||
|
||||
// If the prompt UI is already up, hand it over now; otherwise its
|
||||
// constructor picks the event up from window.__pwaInstallEvent.
|
||||
if (window.pwaInstallPrompt && typeof window.pwaInstallPrompt.adoptInstallEvent === 'function') {
|
||||
window.pwaInstallPrompt.adoptInstallEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('appinstalled', function () {
|
||||
window.__pwaInstallEvent = null;
|
||||
window.__pwaAppInstalled = true;
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,162 @@
|
||||
// The theme has to be decided before the first pixel, not after React mounts.
|
||||
//
|
||||
// A stylesheet cannot read localStorage, so the stored preference can only be applied
|
||||
// by script — and if that script runs late, the page paints in the default (dark) and
|
||||
// then repaints, which is the white/black flash every themed site is judged on. So this
|
||||
// is a classic, blocking <script> in <head>, ahead of the stylesheet: it stamps
|
||||
// data-theme on <html> while the parser is still inside <head>, and the CSS that
|
||||
// arrives afterwards is matched against the right selector on the very first paint.
|
||||
//
|
||||
// It cannot be inline — the page's CSP is `script-src 'self'` with no hash or nonce —
|
||||
// and it deliberately does not import anything, because a module is deferred and
|
||||
// deferred is too late. Everything it needs is in this file.
|
||||
//
|
||||
// Three settings, not two. "system" is the default and means: follow the OS, and keep
|
||||
// following it if the OS changes while the page is open. "light" and "dark" are the
|
||||
// user overriding that, which is why the resolved value is stamped as data-theme rather
|
||||
// than leaving CSS to answer prefers-color-scheme itself — a media query cannot be
|
||||
// overridden by a preference, and someone who wants a light page on a dark laptop is
|
||||
// exactly who a theme switch is for.
|
||||
(function () {
|
||||
if (window.SecureBitTheme) return;
|
||||
|
||||
var STORAGE_KEY = 'securebit-theme';
|
||||
var MODES = ['system', 'light', 'dark'];
|
||||
// Kept in step with --sb-bg in src/styles/theme.css. This paints the browser UI
|
||||
// around the page (Android's address bar, the iOS status bar area in a PWA), and a
|
||||
// dark strip above a light page is more obviously wrong than a slightly-off shade.
|
||||
var BAR_COLOR = { dark: '#0f0f11', light: '#f6f7f8' };
|
||||
|
||||
var root = document.documentElement;
|
||||
var listeners = [];
|
||||
var query = null;
|
||||
|
||||
try {
|
||||
query = window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;
|
||||
} catch (e) {
|
||||
query = null;
|
||||
}
|
||||
|
||||
// Storage throws rather than returning null in a few real situations — Safari in
|
||||
// private browsing, and any browser set to block site data — and a theme is not
|
||||
// worth breaking the page's first paint over.
|
||||
function readStored() {
|
||||
try {
|
||||
var value = window.localStorage.getItem(STORAGE_KEY);
|
||||
return MODES.indexOf(value) === -1 ? 'system' : value;
|
||||
} catch (e) {
|
||||
return 'system';
|
||||
}
|
||||
}
|
||||
|
||||
function writeStored(mode) {
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, mode);
|
||||
} catch (e) {
|
||||
/* preference is not persisted; the session still honours it */
|
||||
}
|
||||
}
|
||||
|
||||
var mode = readStored();
|
||||
|
||||
function resolve(m) {
|
||||
if (m === 'light' || m === 'dark') return m;
|
||||
return query && query.matches ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
function paint(resolved) {
|
||||
root.setAttribute('data-theme', resolved);
|
||||
// The document ships two theme-color tags carrying prefers-color-scheme media
|
||||
// queries, so the browser has an answer before this script exists and a client
|
||||
// with no script keeps one. From here on the answer is ours: the media
|
||||
// attributes come off — a media query cannot express "light on a dark system",
|
||||
// which is the whole point of an explicit choice — and both tags are set to the
|
||||
// resolved colour. Setting content while leaving media in place would do
|
||||
// nothing on the tag whose query does not match.
|
||||
//
|
||||
// The tags may not exist yet on the very first run: this script sits above them
|
||||
// in <head> so that data-theme is stamped before the stylesheet lands. The
|
||||
// DOMContentLoaded pass below catches them, and every later switch finds them.
|
||||
var metas = document.querySelectorAll('meta[name="theme-color"]');
|
||||
for (var i = 0; i < metas.length; i++) {
|
||||
metas[i].removeAttribute('media');
|
||||
metas[i].setAttribute('content', BAR_COLOR[resolved] || BAR_COLOR.dark);
|
||||
}
|
||||
}
|
||||
|
||||
function apply(animated) {
|
||||
var resolved = resolve(mode);
|
||||
if (animated && root.getAttribute('data-theme') !== resolved) {
|
||||
// The cross-fade is opt-in per switch rather than a standing rule, so a
|
||||
// page load never animates and a click always does. See theme.css.
|
||||
root.classList.add('sb-theme-switching');
|
||||
window.setTimeout(function () {
|
||||
root.classList.remove('sb-theme-switching');
|
||||
}, 260);
|
||||
}
|
||||
paint(resolved);
|
||||
for (var i = 0; i < listeners.length; i++) {
|
||||
try {
|
||||
listeners[i](mode, resolved);
|
||||
} catch (e) {
|
||||
/* one bad subscriber must not stop the rest */
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// First paint. Runs while <head> is still parsing, before the stylesheet lands.
|
||||
apply(false);
|
||||
|
||||
// The OS theme changing mid-session only matters in "system" mode; in the other two
|
||||
// the user has already said what they want. The listener stays attached either way
|
||||
// so switching back to "system" picks up the current OS value with no extra wiring.
|
||||
if (query) {
|
||||
var onSystemChange = function () {
|
||||
if (mode === 'system') apply(true);
|
||||
};
|
||||
if (typeof query.addEventListener === 'function') {
|
||||
query.addEventListener('change', onSystemChange);
|
||||
} else if (typeof query.addListener === 'function') {
|
||||
query.addListener(onSystemChange); // Safari < 14
|
||||
}
|
||||
}
|
||||
|
||||
// A second document in the same browser — another tab, or the PWA alongside a tab —
|
||||
// should not disagree with this one about the theme.
|
||||
window.addEventListener('storage', function (event) {
|
||||
if (event.key !== STORAGE_KEY) return;
|
||||
var next = MODES.indexOf(event.newValue) === -1 ? 'system' : event.newValue;
|
||||
if (next === mode) return;
|
||||
mode = next;
|
||||
apply(true);
|
||||
});
|
||||
|
||||
// theme-color is written again once the parser has passed the <meta> tags.
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () { paint(resolve(mode)); });
|
||||
}
|
||||
|
||||
window.SecureBitTheme = {
|
||||
MODES: MODES.slice(),
|
||||
/** 'system' | 'light' | 'dark' — what the user chose. */
|
||||
get: function () { return mode; },
|
||||
/** 'light' | 'dark' — what that currently resolves to. */
|
||||
resolved: function () { return resolve(mode); },
|
||||
set: function (next) {
|
||||
if (MODES.indexOf(next) === -1) return resolve(mode);
|
||||
mode = next;
|
||||
writeStored(next);
|
||||
return apply(true);
|
||||
},
|
||||
/** Returns an unsubscribe function. Called with (mode, resolved). */
|
||||
subscribe: function (fn) {
|
||||
if (typeof fn !== 'function') return function () {};
|
||||
listeners.push(fn);
|
||||
return function () {
|
||||
var i = listeners.indexOf(fn);
|
||||
if (i !== -1) listeners.splice(i, 1);
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -321,11 +321,11 @@ export function groupSub(group) {
|
||||
export function groupDot(group) {
|
||||
switch (group.phase) {
|
||||
case GROUP_PHASE.READY:
|
||||
return linkedCount(group) < group.members.length ? '#e3b341' : '#3ecf8e';
|
||||
return linkedCount(group) < group.members.length ? 'var(--sb-yellow-2-solid)' : 'var(--sb-green-solid)';
|
||||
case GROUP_PHASE.FAILED:
|
||||
return '#e5727a';
|
||||
return 'var(--sb-red-solid)';
|
||||
default:
|
||||
return '#e3b341';
|
||||
return 'var(--sb-yellow-2-solid)';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-11
@@ -34,14 +34,14 @@ export const SESSION_ACTIONS = Object.freeze({
|
||||
|
||||
// Availability presence the PEER advertises to us (sent E2E over the data channel, never
|
||||
// stored on a server). 'invisible' is sent on the wire as 'offline' so peers can't tell.
|
||||
export const PRESENCE_DOT = { available: '#3ecf8e', away: '#e3b341', busy: '#e5727a', offline: '#6b6b73' };
|
||||
export const PRESENCE_DOT = { available: 'var(--sb-green-solid)', away: 'var(--sb-yellow-2-solid)', busy: 'var(--sb-red-solid)', offline: 'var(--sb-text-9)' };
|
||||
export const PRESENCE_WORD = { available: t('presence.available'), away: t('presence.away'), busy: t('presence.busy'), offline: t('presence.offline') };
|
||||
// The statuses the local user can pick for themselves (design: Set your status).
|
||||
export const MY_STATUS_OPTIONS = [
|
||||
{ key: 'available', word: t('presence.available'), desc: t('presence.availableDesc'), dot: '#3ecf8e' },
|
||||
{ key: 'away', word: t('presence.away'), desc: t('presence.awayDesc'), dot: '#e3b341' },
|
||||
{ key: 'busy', word: t('presence.busy'), desc: t('presence.busyDesc'), dot: '#e5727a' },
|
||||
{ key: 'invisible', word: t('presence.invisible'), desc: t('presence.invisibleDesc'), dot: '#6b6b73' }
|
||||
{ key: 'available', word: t('presence.available'), desc: t('presence.availableDesc'), dot: 'var(--sb-green-solid)' },
|
||||
{ key: 'away', word: t('presence.away'), desc: t('presence.awayDesc'), dot: 'var(--sb-yellow-2-solid)' },
|
||||
{ key: 'busy', word: t('presence.busy'), desc: t('presence.busyDesc'), dot: 'var(--sb-red-solid)' },
|
||||
{ key: 'invisible', word: t('presence.invisible'), desc: t('presence.invisibleDesc'), dot: 'var(--sb-text-9)' }
|
||||
];
|
||||
|
||||
// Short, human-friendly default label derived from the local sessionId. Never the peer's
|
||||
@@ -64,13 +64,13 @@ export function statusDot(status) {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
case 'verified':
|
||||
return '#3ecf8e';
|
||||
return 'var(--sb-green-solid)';
|
||||
case 'connecting':
|
||||
case 'verifying':
|
||||
case 'new':
|
||||
return '#e3b341';
|
||||
return 'var(--sb-yellow-2-solid)';
|
||||
default:
|
||||
return '#e5727a'; // disconnected / peer_disconnected / lost
|
||||
return 'var(--sb-red-solid)'; // disconnected / peer_disconnected / lost
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,13 +349,13 @@ export function decorateSession(session, activeSessionId) {
|
||||
// otherwise reflect the connection state (amber = connecting, red = dropped).
|
||||
let dot, headerSub;
|
||||
if (isPending) {
|
||||
dot = '#e3b341';
|
||||
dot = 'var(--sb-yellow-2-solid)';
|
||||
headerSub = statusSub(s);
|
||||
} else if (isUp) {
|
||||
dot = session.peerPresence ? (PRESENCE_DOT[session.peerPresence] || '#6b6b73') : '#3ecf8e';
|
||||
dot = session.peerPresence ? (PRESENCE_DOT[session.peerPresence] || 'var(--sb-text-9)') : 'var(--sb-green-solid)';
|
||||
headerSub = session.peerPresence ? (PRESENCE_WORD[session.peerPresence] || t('presence.online')) : t('conn.p2p');
|
||||
} else {
|
||||
dot = '#e5727a';
|
||||
dot = 'var(--sb-red-solid)';
|
||||
headerSub = statusSub(s);
|
||||
}
|
||||
const preview = lastMessage ? (lastMessage.voice ? '🎙 Voice message' : lastMessage.message) : headerSub;
|
||||
|
||||
@@ -90,7 +90,7 @@ button:disabled,
|
||||
|
||||
/* Keyboard users get the same clarity as pointer users. */
|
||||
:focus-visible {
|
||||
outline: 2px solid rgba(249, 115, 22, 0.75);
|
||||
outline: 2px solid rgba(var(--sb-legacy-orange-500-rgb), 0.75);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ button:disabled,
|
||||
bright top edge — light catching the near face of the glass, which is what
|
||||
separates a translucent bar from a washed-out rectangle. */
|
||||
.header-minimal {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
box-shadow: inset 0 1px 0 rgba(var(--sb-ink), 0.05);
|
||||
}
|
||||
|
||||
/* Cards are a smaller surface than chrome, so they read thinner: less blur,
|
||||
@@ -139,7 +139,7 @@ h3, .text-3xl { letter-spacing: -0.018em; line-height: 1.15; }
|
||||
.header-minimal .text-secondary,
|
||||
.chat-input-area .text-secondary,
|
||||
.card-minimal .text-muted {
|
||||
color: #c3c3c9;
|
||||
color: var(--sb-motion-text-3);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
@@ -244,8 +244,8 @@ h3, .text-3xl { letter-spacing: -0.018em; line-height: 1.15; }
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
.header-minimal { background: #18181a !important; }
|
||||
.card-minimal { background: #202022 !important; }
|
||||
.header-minimal { background: var(--sb-motion-surface) !important; }
|
||||
.card-minimal { background: var(--sb-motion-raise) !important; }
|
||||
}
|
||||
|
||||
/* ── 6. Increased contrast ───────────────────────────────────────────────── */
|
||||
@@ -255,10 +255,10 @@ h3, .text-3xl { letter-spacing: -0.018em; line-height: 1.15; }
|
||||
.icon-container,
|
||||
.verification-code,
|
||||
.security-shield {
|
||||
background: #141416 !important;
|
||||
border-color: rgba(255, 255, 255, 0.42) !important;
|
||||
background: var(--sb-surface) !important;
|
||||
border-color: rgba(var(--sb-ink), 0.42) !important;
|
||||
}
|
||||
.text-secondary { color: #e4e4e8 !important; }
|
||||
.text-muted { color: #c8c8cf !important; }
|
||||
.text-secondary { color: var(--sb-motion-text) !important; }
|
||||
.text-muted { color: var(--sb-motion-text-2) !important; }
|
||||
:focus-visible { outline-width: 3px; }
|
||||
}
|
||||
|
||||
+94
-94
@@ -53,23 +53,23 @@
|
||||
}
|
||||
|
||||
.file-drop-zone {
|
||||
border: 2px dashed #4b5563;
|
||||
border: 2px dashed rgba(var(--sb-legacy-border-rgb), 1);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(55, 65, 81, 0.1);
|
||||
background: rgba(var(--sb-legacy-slate-rgb), 0.1);
|
||||
}
|
||||
|
||||
.file-drop-zone:hover {
|
||||
border-color: #3b82f6;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-color: var(--sb-legacy-blue-500);
|
||||
background: rgba(var(--sb-legacy-blue-rgb), 0.1);
|
||||
}
|
||||
|
||||
.file-drop-zone.drag-over {
|
||||
border-color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: var(--sb-legacy-emerald-500);
|
||||
background: rgba(var(--sb-legacy-emerald-rgb), 0.1);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
height: 6px;
|
||||
background: rgba(75, 85, 99, 0.3);
|
||||
background: rgba(var(--sb-legacy-border-rgb), 0.3);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -108,11 +108,11 @@
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
inset-inline-end: 0;
|
||||
color: #9ca3af;
|
||||
color: var(--sb-legacy-gray);
|
||||
}
|
||||
|
||||
.file-transfer-section {
|
||||
border-top: 1px solid rgba(75, 85, 99, 0.1);
|
||||
border-top: 1px solid rgba(var(--sb-legacy-border-rgb), 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@@ -130,7 +130,7 @@
|
||||
}
|
||||
|
||||
.header-minimal {
|
||||
background: rgb(35 36 35 / 13%);
|
||||
background: rgba(var(--sb-legacy-bar-rgb), 0.13);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
flex-shrink: 0;
|
||||
@@ -169,9 +169,9 @@
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: rgba(42, 43, 42, 0.95);
|
||||
background: rgba(var(--sb-legacy-fill-rgb), 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(75, 85, 99, 0.2);
|
||||
border-top: 1px solid rgba(var(--sb-legacy-border-rgb), 0.2);
|
||||
}
|
||||
|
||||
/* The message container must occupy the entire available height. */
|
||||
@@ -201,19 +201,19 @@ main {
|
||||
}
|
||||
|
||||
.card-minimal {
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
background: rgba(var(--sb-legacy-fill-rgb), 0.8);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(75, 85, 99, 0.2);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(var(--sb-legacy-border-rgb), 0.2);
|
||||
box-shadow: 0 4px 16px rgba(var(--sb-shadow-rgb), calc(0.1 * var(--sb-shadow-k)));
|
||||
}
|
||||
/* Selected state override (higher precedence than Tailwind bg-*) */
|
||||
.card-minimal--selected {
|
||||
background: rgba(249, 115, 22, 0.15) !important; /* orange-500 @ 0.15 */
|
||||
border-color: rgba(249, 115, 22, 1) !important; /* border-orange-500 */
|
||||
box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.3) !important; /* soft ring */
|
||||
background: rgba(var(--sb-legacy-orange-500-rgb), 0.15) !important; /* orange-500 @ 0.15 */
|
||||
border-color: rgba(var(--sb-legacy-orange-500-rgb), 1) !important; /* border-orange-500 */
|
||||
box-shadow: 0 0 0 2px rgba(var(--sb-legacy-orange-500-rgb), 0.3) !important; /* soft ring */
|
||||
}
|
||||
/* .card-minimal:hover {
|
||||
border-color: rgba(249, 115, 22, 0.3);
|
||||
border-color: rgba(var(--sb-legacy-orange-500-rgb), 0.3);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease;
|
||||
} */
|
||||
@@ -225,21 +225,21 @@ main {
|
||||
display: inline-block;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.status-connected { background: #10b981; }
|
||||
.status-connecting { background: #6b7280; }
|
||||
.status-failed { background: #ef4444; }
|
||||
.status-disconnected { background: #6b7280; }
|
||||
.status-verifying { background: #9ca3af; }
|
||||
.status-connected { background: var(--sb-legacy-emerald-500); }
|
||||
.status-connecting { background: var(--sb-legacy-gray-500); }
|
||||
.status-failed { background: var(--sb-legacy-red-500); }
|
||||
.status-disconnected { background: var(--sb-legacy-gray-500); }
|
||||
.status-verifying { background: var(--sb-legacy-gray); }
|
||||
|
||||
.security-shield {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-body) 0%, var(--sb-legacy-raise-1) 100%);
|
||||
border: 1px solid rgba(var(--sb-legacy-border-rgb), 0.3);
|
||||
}
|
||||
|
||||
.verification-code {
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
color: #f1f5f9;
|
||||
background: rgba(var(--sb-legacy-fill-rgb), 0.8);
|
||||
border: 1px solid rgba(var(--sb-legacy-border-rgb), 0.3);
|
||||
color: var(--sb-legacy-text);
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 1.2em;
|
||||
@@ -251,8 +251,8 @@ main {
|
||||
.icon-container {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
background: rgba(var(--sb-legacy-fill-rgb), 0.8);
|
||||
border: 1px solid rgba(var(--sb-legacy-border-rgb), 0.3);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -272,7 +272,7 @@ main {
|
||||
.step-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-orange-500), var(--sb-legacy-orange-600));
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
@@ -356,39 +356,39 @@ button i {
|
||||
|
||||
/* Lightning button */
|
||||
.lightning-button {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-amber-500) 0%, var(--sb-legacy-amber-600) 100%);
|
||||
border: 1px solid rgba(var(--sb-legacy-amber-rgb), 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.lightning-button:hover {
|
||||
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-amber-600) 0%, var(--sb-legacy-amber-700) 100%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
border: 1px solid rgba(249, 115, 22, 0.3);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-orange-500) 0%, var(--sb-legacy-orange-600) 100%);
|
||||
border: 1px solid rgba(var(--sb-legacy-orange-500-rgb), 0.3);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-orange-600) 0%, var(--sb-legacy-red-600) 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-body) 0%, var(--sb-legacy-raise-1) 100%);
|
||||
border: 1px solid rgba(var(--sb-legacy-border-rgb), 0.3);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #3a3b3a 0%, #4a4b4a 100%);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-raise-1) 0%, var(--sb-legacy-raise-2) 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-verify {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-body) 0%, var(--sb-legacy-raise-1) 100%);
|
||||
border: 1px solid rgba(var(--sb-legacy-border-rgb), 0.3);
|
||||
}
|
||||
.btn-verify:hover {
|
||||
background: linear-gradient(135deg, #3a3b3a 0%, #4a4b4a 100%);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-raise-1) 0%, var(--sb-legacy-raise-2) 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -417,20 +417,20 @@ button i {
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 48px;
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
background: rgba(var(--sb-legacy-fill-rgb), 0.8);
|
||||
border: 1px solid rgba(var(--sb-legacy-border-rgb), 0.3);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
color: var(--sb-legacy-text);
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.wallet-logo:hover {
|
||||
border-color: rgba(249, 115, 22, 0.3);
|
||||
background: rgba(249, 115, 22, 0.1);
|
||||
border-color: rgba(var(--sb-legacy-orange-500-rgb), 0.3);
|
||||
background: rgba(var(--sb-legacy-orange-500-rgb), 0.1);
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 4px 10px rgba(var(--sb-shadow-rgb), calc(0.2 * var(--sb-shadow-k)));
|
||||
}
|
||||
|
||||
.wallet-logo.bitcoin-lightning { background: transparent; padding: 4px; }
|
||||
@@ -536,10 +536,10 @@ button i {
|
||||
/* Modern animated button */
|
||||
.btn-partner-modern {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 50%, #dc2626 100%);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-orange-500) 0%, var(--sb-legacy-orange-600) 50%, var(--sb-legacy-red-600) 100%);
|
||||
background-size: 200% 200%;
|
||||
background-position: 0% 0;
|
||||
box-shadow: 0 4px 15px rgba(249, 115, 22, 0.3);
|
||||
box-shadow: 0 4px 15px rgba(var(--sb-legacy-orange-500-rgb), 0.3);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -558,7 +558,7 @@ button i {
|
||||
.btn-partner-modern:hover {
|
||||
background-position: 100% 0;
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
box-shadow: 0 8px 25px rgba(249, 115, 22, 0.5), 0 0 30px rgba(249, 115, 22, 0.3);
|
||||
box-shadow: 0 8px 25px rgba(var(--sb-legacy-orange-500-rgb), 0.5), 0 0 30px rgba(var(--sb-legacy-orange-500-rgb), 0.3);
|
||||
}
|
||||
|
||||
.btn-partner-modern:hover::before {
|
||||
@@ -568,14 +568,14 @@ button i {
|
||||
|
||||
.btn-partner-modern:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
box-shadow: 0 2px 10px rgba(249, 115, 22, 0.4);
|
||||
box-shadow: 0 2px 10px rgba(var(--sb-legacy-orange-500-rgb), 0.4);
|
||||
}
|
||||
|
||||
|
||||
/* Custom scrollbar */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
scrollbar-color: rgba(var(--sb-legacy-scroll-rgb), 0.5) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
@@ -587,22 +587,22 @@ button i {
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
background-color: rgba(var(--sb-legacy-scroll-rgb), 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.7);
|
||||
background-color: rgba(var(--sb-legacy-scroll-rgb), 0.7);
|
||||
}
|
||||
|
||||
.accent-orange { color: #fb923c !important; }
|
||||
.accent-green { color: #34d399 !important; }
|
||||
.accent-red { color: #f87171 !important; }
|
||||
.accent-yellow { color: #fbbf24 !important; }
|
||||
.accent-purple { color: #a78bfa !important; }
|
||||
.accent-gray { color: #9ca3af !important; }
|
||||
.accent-cyan { color: #22d3ee !important; }
|
||||
.accent-blue { color: #60a5fa !important; }
|
||||
.accent-orange { color: var(--sb-legacy-orange) !important; }
|
||||
.accent-green { color: var(--sb-legacy-green) !important; }
|
||||
.accent-red { color: var(--sb-legacy-red) !important; }
|
||||
.accent-yellow { color: var(--sb-legacy-yellow) !important; }
|
||||
.accent-purple { color: var(--sb-legacy-purple) !important; }
|
||||
.accent-gray { color: var(--sb-legacy-gray) !important; }
|
||||
.accent-cyan { color: var(--sb-legacy-cyan) !important; }
|
||||
.accent-blue { color: var(--sb-legacy-blue) !important; }
|
||||
|
||||
/* Ensure icons visible in dark backgrounds */
|
||||
.text-secondary i {
|
||||
@@ -626,8 +626,8 @@ button i {
|
||||
); /* Transparent glass gradient */
|
||||
box-shadow:
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.4),
|
||||
/* Top highlight */ inset 0 -1px 2px rgba(0, 0, 0, 0.2),
|
||||
/* Bottom shadow */ 0 4px 8px rgba(0, 0, 0, 0.2),
|
||||
/* Top highlight */ inset 0 -1px 2px rgba(var(--sb-shadow-rgb), calc(0.2 * var(--sb-shadow-k))),
|
||||
/* Bottom shadow */ 0 4px 8px rgba(var(--sb-shadow-rgb), calc(0.2 * var(--sb-shadow-k))),
|
||||
/* Drop shadow */ 0 0 20px rgba(255, 255, 255, 0.1); /* Subtle glow */
|
||||
transform: rotateX(15deg) translateZ(0); /* Initial 3D tilt */
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55); /* Springy transition */
|
||||
@@ -671,7 +671,7 @@ button i {
|
||||
height: 10px;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(0, 0, 0, 0.3) 0%,
|
||||
rgba(var(--sb-shadow-rgb), calc(0.3 * var(--sb-shadow-k))) 0%,
|
||||
transparent 70%
|
||||
); /* Soft shadow */
|
||||
z-index: -1;
|
||||
@@ -684,7 +684,7 @@ button i {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); /* Depth for text */
|
||||
text-shadow: 0 1px 2px rgba(var(--sb-shadow-rgb), calc(0.5 * var(--sb-shadow-k))); /* Depth for text */
|
||||
display: block;
|
||||
line-height: 60px; /* Center vertically */
|
||||
text-align: center;
|
||||
@@ -695,8 +695,8 @@ button i {
|
||||
transform: rotateX(0deg) translateZ(15px) scale(1.05); /* Lift and scale */
|
||||
box-shadow:
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.4),
|
||||
inset 0 -1px 2px rgba(0, 0, 0, 0.2),
|
||||
0 8px 16px rgba(0, 0, 0, 0.3),
|
||||
inset 0 -1px 2px rgba(var(--sb-shadow-rgb), calc(0.2 * var(--sb-shadow-k))),
|
||||
0 8px 16px rgba(var(--sb-shadow-rgb), calc(0.3 * var(--sb-shadow-k))),
|
||||
/* Stronger shadow */ 0 0 40px rgba(255, 255, 255, 0.25); /* Brighter glow */
|
||||
}
|
||||
|
||||
@@ -705,8 +705,8 @@ button i {
|
||||
transform: rotateX(0deg) translateZ(-5px) scale(0.95); /* Push down */
|
||||
box-shadow:
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.4),
|
||||
inset 0 -1px 2px rgba(0, 0, 0, 0.2),
|
||||
0 2px 4px rgba(0, 0, 0, 0.2),
|
||||
inset 0 -1px 2px rgba(var(--sb-shadow-rgb), calc(0.2 * var(--sb-shadow-k))),
|
||||
0 2px 4px rgba(var(--sb-shadow-rgb), calc(0.2 * var(--sb-shadow-k))),
|
||||
/* Reduced shadow */ 0 0 10px rgba(255, 255, 255, 0.1); /* Dimmed glow */
|
||||
}
|
||||
|
||||
@@ -716,16 +716,16 @@ button i {
|
||||
100% {
|
||||
box-shadow:
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.4),
|
||||
inset 0 -1px 2px rgba(0, 0, 0, 0.2),
|
||||
0 4px 8px rgba(0, 0, 0, 0.2),
|
||||
inset 0 -1px 2px rgba(var(--sb-shadow-rgb), calc(0.2 * var(--sb-shadow-k))),
|
||||
0 4px 8px rgba(var(--sb-shadow-rgb), calc(0.2 * var(--sb-shadow-k))),
|
||||
0 0 20px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.4),
|
||||
inset 0 -1px 2px rgba(0, 0, 0, 0.2),
|
||||
0 4px 8px rgba(0, 0, 0, 0.2),
|
||||
0 0 30px rgba(255, 255, 255, 0.2); /* Peak glow */
|
||||
inset 0 1px 2px rgba(var(--sb-ink), 0.4),
|
||||
inset 0 -1px 2px rgba(var(--sb-shadow-rgb), calc(0.2 * var(--sb-shadow-k))),
|
||||
0 4px 8px rgba(var(--sb-shadow-rgb), calc(0.2 * var(--sb-shadow-k))),
|
||||
0 0 30px rgba(var(--sb-ink), 0.2); /* Peak glow */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,28 +747,28 @@ button i {
|
||||
.sb-scroll {
|
||||
scrollbar-gutter: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255,255,255,0.13) transparent;
|
||||
scrollbar-color: rgba(var(--sb-ink), 0.13) transparent;
|
||||
}
|
||||
.sb-scroll::-webkit-scrollbar { width: 9px; }
|
||||
.sb-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||
.sb-scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(255,255,255,0.07);
|
||||
background: rgba(var(--sb-ink), 0.07);
|
||||
border-radius: 99px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.sb-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255,255,255,0.13);
|
||||
background: rgba(var(--sb-ink), 0.13);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.sb-textarea::placeholder { color: #56565e; }
|
||||
.sb-chip:hover { border-color: rgba(255,255,255,0.16) !important; color: #e8e8eb !important; }
|
||||
.sb-textarea::placeholder { color: var(--sb-text-faint); }
|
||||
.sb-chip:hover { border-color: rgba(var(--sb-ink), 0.16) !important; color: var(--sb-text-2) !important; }
|
||||
.sb-send:hover:not(:disabled) { filter: brightness(1.06); }
|
||||
.sb-unsend:hover { color: #e5727a !important; }
|
||||
.sb-link:hover { color: #e8e8eb !important; }
|
||||
.sb-disconnect:hover { border-color: rgba(229,114,122,0.4) !important; color: #e5727a !important; background: rgba(229,114,122,0.06) !important; }
|
||||
.sb-unsend:hover { color: var(--sb-red) !important; }
|
||||
.sb-link:hover { color: var(--sb-text-2) !important; }
|
||||
.sb-disconnect:hover { border-color: rgba(var(--sb-red-rgb), 0.4) !important; color: var(--sb-red) !important; background: rgba(var(--sb-red-rgb), 0.06) !important; }
|
||||
@media (max-width: 560px) { .sb-hide-sm { display: none; } }
|
||||
.sb-secpill:hover { border-color: rgba(255,255,255,0.16) !important; background: rgba(255,255,255,0.05) !important; }
|
||||
.sb-secpill:hover { border-color: rgba(var(--sb-ink), 0.16) !important; background: rgba(var(--sb-ink), 0.05) !important; }
|
||||
|
||||
/* ── Start Secure — new connection screen (design import) ───────────────── */
|
||||
@keyframes sbFlowR { 0% { left: 4%; opacity: 0; } 12% { opacity: 1; } 88% { opacity: 1; } 100% { left: 96%; opacity: 0; } }
|
||||
@@ -776,7 +776,7 @@ button i {
|
||||
@keyframes sbPulse { 0%,100% { transform: translate(-50%,-50%) scale(1); opacity: 0.5; } 50% { transform: translate(-50%,-50%) scale(1.5); opacity: 0; } }
|
||||
@keyframes sbSpin { to { transform: rotate(360deg); } }
|
||||
@keyframes sbUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes sbNode { 0%,100% { box-shadow: 0 0 0 0 rgba(62,207,142,0.0); } 50% { box-shadow: 0 0 0 6px rgba(62,207,142,0.06); } }
|
||||
@keyframes sbNode { 0%,100% { box-shadow: 0 0 0 0 rgba(var(--sb-green-rgb), 0.0); } 50% { box-shadow: 0 0 0 6px rgba(var(--sb-green-rgb), 0.06); } }
|
||||
@keyframes sbScan { 0% { top: 8%; } 100% { top: 88%; } }
|
||||
@keyframes sbSlideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
||||
|
||||
@@ -793,14 +793,14 @@ button i {
|
||||
.sb-conn-footer > button { width: 100% !important; justify-content: center !important; }
|
||||
.sb-conn-footer > .sb-platforms-menu { width: 100% !important; }
|
||||
}
|
||||
.sb-start textarea::placeholder, .sb-start input::placeholder { color: #56565e; }
|
||||
.sb-start textarea::placeholder, .sb-start input::placeholder { color: var(--sb-text-faint); }
|
||||
.sb-start .sb-sc::-webkit-scrollbar { width: 8px; }
|
||||
.sb-start .sb-sc::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 99px; }
|
||||
.sb-start .sb-sc::-webkit-scrollbar-thumb { background: rgba(var(--sb-ink), 0.1); border-radius: 99px; }
|
||||
.sb-seg-btn { transition: color .2s; }
|
||||
.sb-soft-btn { transition: all .15s; }
|
||||
.sb-soft-btn:hover { border-color: rgba(255,255,255,0.18) !important; color: #e8e8eb !important; }
|
||||
.sb-scan-btn:hover { border-color: rgba(62,207,142,0.5) !important; background: rgba(62,207,142,0.1) !important; }
|
||||
.sb-gen-btn:hover { background: #ff9637 !important; }
|
||||
.sb-soft-btn:hover { border-color: rgba(var(--sb-ink), 0.18) !important; color: var(--sb-text-2) !important; }
|
||||
.sb-scan-btn:hover { border-color: rgba(var(--sb-green-rgb), 0.5) !important; background: rgba(var(--sb-green-rgb), 0.1) !important; }
|
||||
.sb-gen-btn:hover { background: var(--sb-orange-hi) !important; }
|
||||
.sb-start-card { transition: transform .26s cubic-bezier(.3,.8,.3,1); }
|
||||
@media (max-width: 900px) { .sb-start-left { border-inline-end: none !important; } }
|
||||
@media (max-width: 560px) { .sb-start-left { padding: 30px 22px !important; } }
|
||||
|
||||
+31
-31
@@ -4,69 +4,69 @@
|
||||
}
|
||||
|
||||
body {
|
||||
background: #2A2B2A;
|
||||
background: var(--sb-legacy-body);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Basic backgrounds */
|
||||
.bg-custom-bg {
|
||||
background-color: rgb(37 38 37) !important;
|
||||
background-color: var(--sb-legacy-body-2) !important;
|
||||
}
|
||||
|
||||
.bg-my{
|
||||
background-color: rgb(26 26 26);
|
||||
background-color: var(--sb-legacy-panel);
|
||||
}
|
||||
|
||||
.bg-header {
|
||||
background-color: rgb(35 35 35) !important;
|
||||
background-color: var(--sb-legacy-header) !important;
|
||||
}
|
||||
|
||||
.minimal-bg {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2A2B2A 100%);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-panel) 0%, var(--sb-legacy-body) 100%);
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card-minimal.rounded-xl.p-6.cursor-pointer.group.flex-1.create {
|
||||
background-color: #60a5fa08;
|
||||
background-color: rgba(var(--sb-blue-rgb), 0.03);
|
||||
}
|
||||
|
||||
.card-minimal.rounded-xl.p-6.cursor-pointer.group.flex-1.join {
|
||||
background-color: #34d39908;
|
||||
background-color: rgba(var(--sb-legacy-green-rgb), 0.03);
|
||||
}
|
||||
|
||||
tr.bg-black-table {
|
||||
background-color: #1c1c1c;
|
||||
background-color: var(--sb-legacy-table);
|
||||
}
|
||||
|
||||
|
||||
/* Text styles */
|
||||
.text-primary { color: #f1f5f9; }
|
||||
.text-secondary { color: #b6b6b6; }
|
||||
.text-muted { color: #818080; }
|
||||
.text-primary { color: var(--sb-legacy-text); }
|
||||
.text-secondary { color: var(--sb-legacy-text-2); }
|
||||
.text-muted { color: var(--sb-legacy-text-3); }
|
||||
|
||||
/* Accent colors */
|
||||
.accent-orange { color: #fb923c; }
|
||||
.accent-green { color: #34d399; }
|
||||
.accent-red { color: #f87171; }
|
||||
.accent-yellow { color: #fbbf24; }
|
||||
.accent-purple { color: #a78bfa; }
|
||||
.accent-blue { color: #60a5fa; }
|
||||
.accent-gray { color: #9ca3af; }
|
||||
.accent-cyan { color: #22d3ee; }
|
||||
.accent-orange { color: var(--sb-legacy-orange); }
|
||||
.accent-green { color: var(--sb-legacy-green); }
|
||||
.accent-red { color: var(--sb-legacy-red); }
|
||||
.accent-yellow { color: var(--sb-legacy-yellow); }
|
||||
.accent-purple { color: var(--sb-legacy-purple); }
|
||||
.accent-blue { color: var(--sb-legacy-blue); }
|
||||
.accent-gray { color: var(--sb-legacy-gray); }
|
||||
.accent-cyan { color: var(--sb-legacy-cyan); }
|
||||
|
||||
/* Custom scroll */
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(42, 43, 42, 0.3);
|
||||
background: rgba(var(--sb-legacy-fill-rgb), 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(75, 85, 99, 0.5);
|
||||
background: rgba(var(--sb-legacy-border-rgb), 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(75, 85, 99, 0.7);
|
||||
background: rgba(var(--sb-legacy-border-rgb), 0.7);
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
@@ -171,7 +171,7 @@ tr.bg-black-table {
|
||||
--speed: 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
--closed: 5rem;
|
||||
--open: 30rem;
|
||||
--accent: #fb923c;
|
||||
--accent: var(--sb-legacy-orange);
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -180,8 +180,8 @@ tr.bg-black-table {
|
||||
}
|
||||
body {
|
||||
font-family: Inter, sans-serif;
|
||||
background: #07090d;
|
||||
color: #c5c7ce;
|
||||
background: var(--sb-legacy-ground);
|
||||
color: var(--sb-legacy-body-text);
|
||||
}
|
||||
|
||||
.head {
|
||||
@@ -200,7 +200,7 @@ body {
|
||||
height: 2.5rem;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
background: rgba(var(--sb-ink), 0.12);
|
||||
color: #fff;
|
||||
font-size: 1.5rem;
|
||||
display: flex;
|
||||
@@ -253,7 +253,7 @@ body {
|
||||
.project-card[active] {
|
||||
flex-basis: var(--open);
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 15px 12px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 15px 12px rgba(var(--sb-shadow-rgb), calc(0.25 * var(--sb-shadow-k)));
|
||||
}
|
||||
.project-card__bg {
|
||||
position: absolute;
|
||||
@@ -278,7 +278,7 @@ body {
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
padding: 0;
|
||||
background: linear-gradient(transparent 40%, rgba(0, 0, 0, 0.35) 100%);
|
||||
background: linear-gradient(transparent 40%, rgba(var(--sb-shadow-rgb), calc(0.35 * var(--sb-shadow-k))) 100%);
|
||||
z-index: 2;
|
||||
}
|
||||
.project-card__title {
|
||||
@@ -316,10 +316,10 @@ body {
|
||||
height: 269px;
|
||||
border-radius: 0.45rem;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0 4px 10px rgba(var(--sb-shadow-rgb), calc(0.4 * var(--sb-shadow-k)));
|
||||
}
|
||||
.project-card__desc {
|
||||
color: #b6b6b6;
|
||||
color: var(--sb-legacy-text-2);
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
max-width: 16rem;
|
||||
@@ -393,7 +393,7 @@ body {
|
||||
.project-card[active] {
|
||||
min-height: 300px;
|
||||
transform: none;
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 8px 25px rgba(var(--sb-shadow-rgb), calc(0.3 * var(--sb-shadow-k)));
|
||||
}
|
||||
|
||||
.project-card__content {
|
||||
|
||||
+19
-12
@@ -7,7 +7,7 @@
|
||||
/* PWA Update Banner */
|
||||
#pwa-update-banner {
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 1px solid rgba(var(--sb-ink), 0.1);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@@ -25,18 +25,25 @@
|
||||
/* PWA Notifications */
|
||||
.pwa-notification {
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(var(--sb-ink), 0.1);
|
||||
}
|
||||
|
||||
/* Status-bar inset for the installed app.
|
||||
*
|
||||
* Installed on iOS, the web view runs edge-to-edge: `viewport-fit=cover` plus
|
||||
* `apple-mobile-web-app-status-bar-style: black-translucent` put our own pixels
|
||||
* under the clock and the notch. Every bar pinned to the top of the screen has
|
||||
* to gutter itself out of that strip, or it reads as if it is sliding under the
|
||||
* status bar. In a browser tab the inset is 0 — the browser chrome already
|
||||
* reserves the space — so one variable covers both cases and the bars can use
|
||||
* it unconditionally.
|
||||
* This was written for `apple-mobile-web-app-status-bar-style: black-translucent`,
|
||||
* which ran the web view edge-to-edge and put our own pixels under the clock and the
|
||||
* notch, so every bar pinned to the top had to gutter itself out of that strip or it
|
||||
* read as sliding under the status bar. The status bar style is now `default` — under
|
||||
* black-translucent iOS always drew the clock in white, which is unreadable once the
|
||||
* page can be light, and the meta is read once at launch so nothing could fix it after
|
||||
* the fact. See the comment on that tag in templates/index.template.html.
|
||||
*
|
||||
* None of the machinery below changes as a result, and that is the point of deriving it
|
||||
* from env() rather than from a constant: with `default` the system reserves the strip
|
||||
* itself, so env(safe-area-inset-top) reports 0 in standalone and --sb-safe-top
|
||||
* collapses to 0 — the same value a browser tab already gave it. If the style is ever
|
||||
* put back, the insets come back with it and the bars gutter themselves again.
|
||||
* The bottom inset (the home indicator) is unaffected and still needs viewport-fit=cover.
|
||||
*
|
||||
* The previous version of this file padded `body` instead. That is the wrong
|
||||
* element twice over: the landing header is `position: fixed`, which ignores an
|
||||
@@ -124,7 +131,7 @@ body.pwa-installed .header-minimal { height: auto; }
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
background: linear-gradient(135deg, var(--sb-legacy-panel) 0%, var(--sb-legacy-splash) 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -174,7 +181,7 @@ body.pwa-installed .header-minimal { height: auto; }
|
||||
}
|
||||
|
||||
.pwa-splash .subtitle {
|
||||
color: #9ca3af;
|
||||
color: var(--sb-legacy-gray);
|
||||
font-size: 14px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
@@ -264,7 +271,7 @@ body.pwa-installed .header-minimal { height: auto; }
|
||||
/* PWA Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.pwa-notification {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(var(--sb-ink), 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
/**
|
||||
* theme.css — the one place every colour in the interface is decided.
|
||||
*
|
||||
* Until this file existed the palette lived as ~620 literal hex values inside inline
|
||||
* styles in the JSX plus another few hundred in the hand-written sheets, which is why
|
||||
* the app could only ever be dark: there was no single thing to change. Every one of
|
||||
* those literals now reads a custom property declared here, so a theme is a matter of
|
||||
* redeclaring the block rather than touching a component.
|
||||
*
|
||||
* The dark theme is unchanged by this. Every colour declaration the previous build
|
||||
* produced comes out of this one identically once the properties are resolved — that
|
||||
* was checked declaration by declaration against the last release's assets/app.css, and
|
||||
* it is the reason there are tokens here that look like duplicates of each other
|
||||
* (--sb-orange next to --sb-legacy-orange-500, --sb-surface next to --sb-motion-surface).
|
||||
* They were duplicates in the old stylesheets too, a step apart on Tailwind's ramp
|
||||
* versus the redesign's; collapsing them would have been tidier and would also have
|
||||
* quietly restyled the dark theme, which is not what adding a light one is for.
|
||||
* The only values folded together are greys that differed by one or two counts out of
|
||||
* 255 (#0d0d0f into #0c0c0e, #121214 into #0f0f11, #141417 into #141416) — below what
|
||||
* a display resolves, and not worth two light values that must never drift apart.
|
||||
*
|
||||
* Two palettes, plus two fallbacks:
|
||||
* :root — dark, and the default.
|
||||
* :root[data-theme="light"] — light, selected by src/scripts/theme-boot.js from
|
||||
* localStorage before the first paint.
|
||||
* "Follow the system" is not a third palette. theme-boot.js resolves the media query
|
||||
* itself and stamps data-theme with the answer, so the cascade only ever sees an
|
||||
* explicit value. Answering prefers-color-scheme in CSS instead would get the first
|
||||
* paint right but could never be overridden — and someone who wants a light page on a
|
||||
* dark laptop is precisely who a theme switch is for. The one place the media query
|
||||
* *is* used is further down, for a client running no JavaScript at all, where there is
|
||||
* no stored preference to honour and nothing to stamp the attribute.
|
||||
*
|
||||
* The last block re-points a handful of Tailwind utility classes that only work on a
|
||||
* dark ground; see the comment above it for why they are overridden rather than
|
||||
* rewritten at the call sites.
|
||||
*
|
||||
* Naming: --sb-bg-* are page and container grounds, --sb-surface-* are things that sit
|
||||
* on them, --sb-text-* is a ramp from loudest (1) to faintest, and the accents keep
|
||||
* their colour name. --sb-ink is the pair of RGB channels used for translucent overlay
|
||||
* tints: white-on-dark inverts to black-on-light, so `rgba(var(--sb-ink), 0.06)`
|
||||
* is a hairline border in either theme without the call site knowing which.
|
||||
*/
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
/* Overlay tint base. Everything that used to be rgba(255,255,255,a) reads this. */
|
||||
--sb-ink: 255, 255, 255;
|
||||
|
||||
/* Grounds */
|
||||
--sb-bg-deepest: #0a0a0c;
|
||||
--sb-bg-deep: #0c0c0e;
|
||||
--sb-bg: #0f0f11;
|
||||
|
||||
/* Surfaces, in rising order of elevation */
|
||||
--sb-surface: #141416;
|
||||
--sb-surface-2: #1a1a1f;
|
||||
--sb-surface-warm: #201416;
|
||||
--sb-surface-3: #26262b;
|
||||
--sb-surface-4: #2a2a30;
|
||||
--sb-surface-5: #3f3f47;
|
||||
|
||||
/* Three decorative gradients read as one warm sweep; kept apart so it survives. */
|
||||
--sb-grad-1: #15151b;
|
||||
--sb-grad-2: #1d1a24;
|
||||
--sb-grad-3: #161620;
|
||||
|
||||
/* Text, loudest to faintest */
|
||||
--sb-text-1: #f4f4f6;
|
||||
--sb-text-2: #e8e8eb;
|
||||
--sb-text-3: #d7d7db;
|
||||
--sb-text-4: #cfcfd4;
|
||||
--sb-text-5: #b3b3ba;
|
||||
--sb-text-5b: #a7a7b0;
|
||||
--sb-text-6: #9a9aa2;
|
||||
--sb-text-7: #8a8a92;
|
||||
--sb-text-8: #7b7b83;
|
||||
--sb-text-9: #6b6b73;
|
||||
--sb-text-faint: #56565e;
|
||||
--sb-text-code: #c9ccd8;
|
||||
|
||||
/* Accents */
|
||||
--sb-green: #3ecf8e;
|
||||
--sb-green-soft: #8fe0bb;
|
||||
--sb-green-muted: #a8b8ae;
|
||||
--sb-orange: #f0892a;
|
||||
--sb-orange-hi: #ff9637;
|
||||
--sb-orange-2: #f0a455;
|
||||
--sb-orange-3: #f0b072;
|
||||
--sb-orange-4: #e8b27a;
|
||||
--sb-red: #e5727a;
|
||||
--sb-red-soft: #f0a6ab;
|
||||
--sb-yellow: #e3c84e;
|
||||
--sb-yellow-2: #e3b341;
|
||||
|
||||
/* Text that sits *on* a filled accent. Dark ink on a bright fill reads better than
|
||||
white does at these saturations — but only while the fill stays bright, which is
|
||||
why the light theme darkens the fills and flips these to white. */
|
||||
--sb-on-accent: #1a0f04;
|
||||
--sb-on-green: #06231a;
|
||||
--sb-on-green-2: #08160e;
|
||||
|
||||
/* Warm neutrals used by the hand-drawn SVG diagrams on the landing page. */
|
||||
--sb-diag-text: #d8cfc1;
|
||||
--sb-diag-label: #8f8b84;
|
||||
--sb-diag-stroke: #6b6760;
|
||||
--sb-diag-dim: #85817b;
|
||||
|
||||
/* Legacy surfaces from the pre-redesign screens that are still on the page. */
|
||||
--sb-legacy-1: #1b1c1b;
|
||||
--sb-legacy-2: #222322;
|
||||
--sb-legacy-3: #1f201f;
|
||||
--sb-legacy-panel: #1a1a1a;
|
||||
--sb-legacy-body: #2a2b2a;
|
||||
--sb-legacy-body-2: #252625;
|
||||
/* .header-minimal's own translucent ground, and the drop zone's fill. */
|
||||
--sb-legacy-bar-rgb: 35, 36, 35;
|
||||
--sb-legacy-slate-rgb: 55, 65, 81;
|
||||
--sb-legacy-green-rgb: 52, 211, 153;
|
||||
--sb-legacy-header: #232323;
|
||||
--sb-legacy-table: #1c1c1c;
|
||||
|
||||
|
||||
/* An accent has two jobs and they pull in opposite directions once the page can be
|
||||
light. As a FILL — a button, a chip, a status dot — it should be the brand colour
|
||||
and nothing else, and it stays readable because the text on it is near-black
|
||||
(--sb-on-accent, 7.5:1 on this orange). As TEXT on the page itself it has to
|
||||
clear 4.5:1 against white, and the brand orange manages 2.5:1 there.
|
||||
So: --sb-*-solid is the brand, identical in both themes, and --sb-orange /
|
||||
--sb-green above are the text-safe versions that darken in the light theme.
|
||||
Getting this wrong in the other direction is what made the light theme's buttons
|
||||
brown. */
|
||||
--sb-orange-solid: #f0892a;
|
||||
--sb-green-solid: #3ecf8e;
|
||||
--sb-red-solid: #e5727a;
|
||||
--sb-red-strong-solid: #e5484d;
|
||||
--sb-yellow-solid: #e3c84e;
|
||||
--sb-yellow-2-solid: #e3b341;
|
||||
|
||||
/* Channel triples for the same accents. A translucent tint of an accent
|
||||
(`rgba(var(--sb-orange-rgb), 0.14)`) cannot be built from the hex token, because
|
||||
CSS has no way to add an alpha to a colour a custom property already resolved —
|
||||
so the channels are published separately and the two must be kept in step. */
|
||||
--sb-green-rgb: 62, 207, 142;
|
||||
--sb-orange-rgb: 240, 137, 42;
|
||||
--sb-red-rgb: 229, 114, 122;
|
||||
--sb-red-strong-rgb: 229, 72, 77;
|
||||
--sb-yellow-rgb: 227, 200, 78;
|
||||
--sb-yellow-2-rgb: 227, 179, 65;
|
||||
--sb-blue-rgb: 96, 165, 250;
|
||||
--sb-bg-rgb: 18, 18, 20;
|
||||
--sb-surface-rgb: 20, 20, 22;
|
||||
--sb-text-7-rgb: 138, 138, 146;
|
||||
--sb-text-9-rgb: 107, 107, 115;
|
||||
|
||||
/* What a modal dims the page behind it with. Stays dark in both themes — a light
|
||||
scrim over a light page dims nothing — but lighter and bluer on the light one so
|
||||
it reads as depth rather than as a hole cut in the page. */
|
||||
--sb-scrim-rgb: 6, 6, 8;
|
||||
|
||||
/* The .accent-* and .text-* utility classes predate the redesign and use a slightly
|
||||
different, brighter set than the sb- palette above. They are still on the page, so
|
||||
they keep their own values rather than being snapped to the newer accents — the
|
||||
point here is to give them a light counterpart, not to restyle the dark theme. */
|
||||
--sb-legacy-orange: #fb923c;
|
||||
--sb-legacy-green: #34d399;
|
||||
--sb-legacy-red: #f87171;
|
||||
--sb-legacy-yellow: #fbbf24;
|
||||
--sb-legacy-purple: #a78bfa;
|
||||
--sb-legacy-blue: #60a5fa;
|
||||
--sb-legacy-gray: #9ca3af;
|
||||
--sb-legacy-cyan: #22d3ee;
|
||||
--sb-legacy-text: #f1f5f9;
|
||||
--sb-legacy-text-2: #b6b6b6;
|
||||
--sb-legacy-text-3: #818080;
|
||||
--sb-legacy-body-text: #c5c7ce;
|
||||
/* Borders and fills the older components draw with (Tailwind's gray-600 at low
|
||||
alpha, mostly). Published as channels so their many alphas still work. */
|
||||
--sb-legacy-border-rgb: 75, 85, 99;
|
||||
--sb-legacy-fill-rgb: 42, 43, 42;
|
||||
--sb-legacy-scroll-rgb: 156, 163, 175;
|
||||
|
||||
|
||||
/* The pre-redesign component set (btn-primary, the status dots, the drop zone, the
|
||||
splash) was built on Tailwind's own palette, one step off the sb- accents above.
|
||||
Snapping them together would have been tidier and would also have restyled the
|
||||
dark theme, which is not what adding a light theme is for — so each keeps its
|
||||
exact value here and gets a light counterpart of its own. */
|
||||
--sb-legacy-orange-500: #f97316;
|
||||
--sb-legacy-orange-500-rgb: 249, 115, 22;
|
||||
--sb-legacy-orange-600: #ea580c;
|
||||
--sb-legacy-amber-500: #f59e0b;
|
||||
--sb-legacy-amber-600: #d97706;
|
||||
--sb-legacy-amber-700: #b45309;
|
||||
--sb-legacy-red-500: #ef4444;
|
||||
--sb-legacy-red-600: #dc2626;
|
||||
--sb-legacy-blue-500: #3b82f6;
|
||||
--sb-legacy-emerald-500: #10b981;
|
||||
--sb-legacy-gray-500: #6b7280;
|
||||
--sb-legacy-blue-rgb: 59, 130, 246;
|
||||
--sb-legacy-emerald-rgb: 16, 185, 129;
|
||||
--sb-legacy-amber-rgb: 245, 158, 11;
|
||||
/* The two greys the older gradients step up to. */
|
||||
--sb-legacy-raise-1: #3a3b3a;
|
||||
--sb-legacy-raise-2: #4a4b4a;
|
||||
--sb-legacy-splash: #2d2d2d;
|
||||
/* apple-motion.css draws its own pressed/hover surfaces. */
|
||||
--sb-motion-surface: #18181a;
|
||||
--sb-motion-raise: #202022;
|
||||
--sb-motion-text: #e4e4e8;
|
||||
--sb-motion-text-2: #c8c8cf;
|
||||
--sb-motion-text-3: #c3c3c9;
|
||||
/* The page ground the legacy body rule paints, a shade below --sb-bg-deepest. */
|
||||
--sb-legacy-ground: #07090d;
|
||||
|
||||
/* Shadows. On a dark ground a shadow is nearly invisible and the alphas can be
|
||||
generous; the same alphas on a light page produce grey smears under everything.
|
||||
Rather than restating every shadow in the light block, the call sites keep their
|
||||
own alpha and multiply it by --sb-shadow-k, which the light theme turns down —
|
||||
so one number retunes every shadow in the app at once. */
|
||||
--sb-shadow-rgb: 0, 0, 0;
|
||||
--sb-shadow-k: 1;
|
||||
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
|
||||
--sb-ink: 0, 0, 0;
|
||||
|
||||
/* The grounds invert but do not go to pure white: an all-#fff page under a full-
|
||||
bleed layout is glare, and the cards need something to sit on top of. */
|
||||
--sb-bg-deepest: #e7e7ea;
|
||||
--sb-bg-deep: #eff0f2;
|
||||
--sb-bg: #f6f7f8;
|
||||
|
||||
--sb-surface: #ffffff;
|
||||
--sb-surface-2: #f2f3f5;
|
||||
--sb-surface-warm: #fdeced;
|
||||
--sb-surface-3: #e7e8ec;
|
||||
--sb-surface-4: #e0e1e6;
|
||||
--sb-surface-5: #cfd0d7;
|
||||
|
||||
--sb-grad-1: #f1f1f5;
|
||||
--sb-grad-2: #eae7f0;
|
||||
--sb-grad-3: #eeeef4;
|
||||
|
||||
/* The ramp mirrors rather than inverts: the darkest text stops short of black,
|
||||
and the faint end stops at roughly 4:1 on white so hints stay legible. */
|
||||
--sb-text-1: #0e0e12;
|
||||
--sb-text-2: #1b1b20;
|
||||
--sb-text-3: #26262c;
|
||||
--sb-text-4: #333339;
|
||||
--sb-text-5: #4a4a52;
|
||||
--sb-text-5b: #52525b;
|
||||
--sb-text-6: #56565f;
|
||||
--sb-text-7: #63636c;
|
||||
--sb-text-8: #6f6f78;
|
||||
--sb-text-9: #7c7c85;
|
||||
--sb-text-faint: #85858f;
|
||||
--sb-text-code: #2f3340;
|
||||
|
||||
/* Every accent is darkened until it clears 4.5:1 against white — the dark theme's
|
||||
versions are tuned for a black ground and turn to pastel on a white one. */
|
||||
--sb-green: #0a8657;
|
||||
--sb-green-soft: #0e7a52;
|
||||
--sb-green-muted: #4f6259;
|
||||
--sb-orange: #b05c08;
|
||||
--sb-orange-hi: #ff9637;
|
||||
--sb-orange-2: #f0a455;
|
||||
--sb-orange-3: #96550f;
|
||||
--sb-orange-4: #8d5417;
|
||||
--sb-red: #c62f38;
|
||||
--sb-red-soft: #b8434c;
|
||||
--sb-yellow: #7d6400;
|
||||
--sb-yellow-2: #8a6c00;
|
||||
|
||||
/* The fills above are now dark, so their text flips to white. */
|
||||
--sb-on-accent: #1a0f04;
|
||||
--sb-on-green: #06231a;
|
||||
--sb-on-green-2: #08160e;
|
||||
|
||||
--sb-diag-text: #35302a;
|
||||
--sb-diag-label: #6a665f;
|
||||
--sb-diag-stroke: #a9a49b;
|
||||
--sb-diag-dim: #75716b;
|
||||
|
||||
--sb-legacy-1: #ffffff;
|
||||
--sb-legacy-2: #f4f5f7;
|
||||
--sb-legacy-3: #ffffff;
|
||||
--sb-legacy-panel: #ffffff;
|
||||
--sb-legacy-body: #f6f7f8;
|
||||
--sb-legacy-body-2: #f1f2f4;
|
||||
--sb-legacy-bar-rgb: 250, 250, 251;
|
||||
--sb-legacy-slate-rgb: 226, 228, 233;
|
||||
--sb-legacy-green-rgb: 52, 211, 153;
|
||||
--sb-legacy-header: #ffffff;
|
||||
--sb-legacy-table: #f4f5f7;
|
||||
|
||||
|
||||
/* Identical to the dark theme on purpose — see the note there. A filled accent is
|
||||
the brand mark; only accent *text* moves. */
|
||||
--sb-orange-solid: #f0892a;
|
||||
--sb-green-solid: #3ecf8e;
|
||||
--sb-red-solid: #e5727a;
|
||||
--sb-red-strong-solid: #e5484d;
|
||||
--sb-yellow-solid: #e3c84e;
|
||||
--sb-yellow-2-solid: #e3b341;
|
||||
|
||||
--sb-green-rgb: 62, 207, 142;
|
||||
--sb-orange-rgb: 240, 137, 42;
|
||||
--sb-red-rgb: 229, 114, 122;
|
||||
--sb-red-strong-rgb: 229, 72, 77;
|
||||
--sb-yellow-rgb: 227, 200, 78;
|
||||
--sb-yellow-2-rgb: 227, 179, 65;
|
||||
--sb-blue-rgb: 29, 100, 200;
|
||||
--sb-bg-rgb: 246, 247, 248;
|
||||
--sb-surface-rgb: 255, 255, 255;
|
||||
--sb-text-7-rgb: 99, 99, 108;
|
||||
--sb-text-9-rgb: 124, 124, 133;
|
||||
|
||||
--sb-scrim-rgb: 24, 26, 34;
|
||||
|
||||
--sb-legacy-orange: #b05c08;
|
||||
--sb-legacy-green: #0a8657;
|
||||
--sb-legacy-red: #c62f38;
|
||||
--sb-legacy-yellow: #8a6c00;
|
||||
--sb-legacy-purple: #6d45d9;
|
||||
--sb-legacy-blue: #1d64c8;
|
||||
--sb-legacy-gray: #63636c;
|
||||
--sb-legacy-cyan: #0e7490;
|
||||
--sb-legacy-text: #0e0e12;
|
||||
--sb-legacy-text-2: #4a4a52;
|
||||
--sb-legacy-text-3: #6f6f78;
|
||||
--sb-legacy-body-text: #333339;
|
||||
--sb-legacy-border-rgb: 148, 155, 168;
|
||||
--sb-legacy-fill-rgb: 244, 245, 247;
|
||||
--sb-legacy-scroll-rgb: 120, 126, 138;
|
||||
|
||||
|
||||
--sb-legacy-orange-500: #b05c08;
|
||||
--sb-legacy-orange-500-rgb: 249, 115, 22;
|
||||
--sb-legacy-orange-600: #9c5007;
|
||||
--sb-legacy-amber-500: #8a6c00;
|
||||
--sb-legacy-amber-600: #7a5f00;
|
||||
--sb-legacy-amber-700: #6a5200;
|
||||
--sb-legacy-red-500: #c62f38;
|
||||
--sb-legacy-red-600: #b0212a;
|
||||
--sb-legacy-blue-500: #1d64c8;
|
||||
--sb-legacy-emerald-500: #0a8657;
|
||||
--sb-legacy-gray-500: #63636c;
|
||||
--sb-legacy-blue-rgb: 59, 130, 246;
|
||||
--sb-legacy-emerald-rgb: 16, 185, 129;
|
||||
--sb-legacy-amber-rgb: 245, 158, 11;
|
||||
/* Gradients that stepped *up* from the ground in the dark theme have to step
|
||||
*down* from white here, or the raised end reads as a hole. */
|
||||
--sb-legacy-raise-1: #eceef1;
|
||||
--sb-legacy-raise-2: #e0e2e7;
|
||||
--sb-legacy-splash: #e7e8ec;
|
||||
--sb-motion-surface: #ffffff;
|
||||
--sb-motion-raise: #f2f3f5;
|
||||
--sb-motion-text: #1b1b20;
|
||||
--sb-motion-text-2: #333339;
|
||||
--sb-motion-text-3: #3a3a41;
|
||||
--sb-legacy-ground: #eef0f2;
|
||||
|
||||
--sb-shadow-rgb: 16, 18, 27;
|
||||
--sb-shadow-k: 0.26;
|
||||
|
||||
}
|
||||
|
||||
/* The one case theme-boot.js cannot answer: a client that does not run scripts, where
|
||||
no data-theme is ever stamped. It gets the system preference, which is the closest
|
||||
thing to a right answer available without storage. Guarded on the attribute being
|
||||
absent so it can never fight the explicit choice — the moment JS runs, data-theme
|
||||
exists and this block stops matching. */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root:not([data-theme]) {
|
||||
color-scheme: light;
|
||||
|
||||
--sb-ink: 0, 0, 0;
|
||||
|
||||
--sb-bg-deepest: #e7e7ea;
|
||||
--sb-bg-deep: #eff0f2;
|
||||
--sb-bg: #f6f7f8;
|
||||
|
||||
--sb-surface: #ffffff;
|
||||
--sb-surface-2: #f2f3f5;
|
||||
--sb-surface-3: #e7e8ec;
|
||||
--sb-surface-4: #e0e1e6;
|
||||
--sb-surface-5: #cfd0d7;
|
||||
|
||||
--sb-text-1: #0e0e12;
|
||||
--sb-text-2: #1b1b20;
|
||||
--sb-text-3: #26262c;
|
||||
--sb-text-4: #333339;
|
||||
--sb-text-5: #4a4a52;
|
||||
--sb-text-6: #56565f;
|
||||
--sb-text-7: #63636c;
|
||||
--sb-text-8: #6f6f78;
|
||||
--sb-text-9: #7c7c85;
|
||||
--sb-text-faint: #85858f;
|
||||
|
||||
--sb-green: #0a8657;
|
||||
--sb-green-rgb: 10, 134, 87;
|
||||
--sb-orange: #b05c08;
|
||||
--sb-orange-rgb: 176, 92, 8;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Tailwind utilities that assume a dark ground ────────────────────────────────────
|
||||
*
|
||||
* A handful of screens that predate the redesign — the SAS verification panel, the
|
||||
* password modal, the download cards — are styled with Tailwind classes rather than
|
||||
* inline styles, and Tailwind's palette has no idea a second theme exists. The classes
|
||||
* below are the ones that only work on a dark background: light text (white, gray-200,
|
||||
* gray-400), dark fills (gray-700, gray-900, black at low alpha), and the -300/-400
|
||||
* accent shades, which are picked to glow on black and turn to pastel on white.
|
||||
*
|
||||
* Overriding here rather than rewriting the call sites is deliberate: the alternative is
|
||||
* ~50 edits across five files to swap one palette for another, in screens that are
|
||||
* already scheduled to be replaced. The selector carries the data-theme attribute, so it
|
||||
* outranks Tailwind's single class without needing !important, and it costs nothing in
|
||||
* the dark theme, where none of it matches.
|
||||
*
|
||||
* Not listed, on purpose: bg-black\/50, which is the password modal's backdrop. A scrim
|
||||
* is supposed to be dark in both themes — a light scrim over a light page dims nothing.
|
||||
* The -500/10 fills and -500/20 borders are also left alone; a mid-tone at one tenth
|
||||
* alpha reads correctly against either ground.
|
||||
*/
|
||||
:root[data-theme="light"] .text-white { color: #1b1b20; }
|
||||
:root[data-theme="light"] .text-gray-200 { color: #333339; }
|
||||
:root[data-theme="light"] .text-gray-400 { color: #63636c; }
|
||||
:root[data-theme="light"] .text-gray-500 { color: #6f6f78; }
|
||||
:root[data-theme="light"] .placeholder-gray-500::placeholder { color: #85858f; }
|
||||
|
||||
/* A watermark, not text: the huge icon behind each download card. */
|
||||
:root[data-theme="light"] .text-white\/10 { color: rgba(0, 0, 0, 0.08); }
|
||||
|
||||
:root[data-theme="light"] .bg-gray-700\/40 { background-color: rgba(0, 0, 0, 0.04); }
|
||||
:root[data-theme="light"] .bg-gray-700\/50 { background-color: rgba(0, 0, 0, 0.05); }
|
||||
:root[data-theme="light"] .bg-gray-700\/60 { background-color: rgba(0, 0, 0, 0.06); }
|
||||
:root[data-theme="light"] .bg-gray-900\/30 { background-color: rgba(0, 0, 0, 0.04); }
|
||||
:root[data-theme="light"] .bg-black\/20 { background-color: rgba(0, 0, 0, 0.04); }
|
||||
/* The hover overlay on a download card. Its text is --sb-text-*, which is dark in this
|
||||
theme, so the overlay has to be light or the label disappears into it. */
|
||||
:root[data-theme="light"] .bg-black\/30 { background-color: rgba(255, 255, 255, 0.82); }
|
||||
|
||||
/* The -300 and -400 accent shades, darkened to the same targets as the sb- accents. */
|
||||
:root[data-theme="light"] .text-green-300,
|
||||
:root[data-theme="light"] .text-green-400,
|
||||
:root[data-theme="light"] .text-green-500 { color: #0a8657; }
|
||||
:root[data-theme="light"] .text-red-300,
|
||||
:root[data-theme="light"] .text-red-400,
|
||||
:root[data-theme="light"] .text-red-500 { color: #c62f38; }
|
||||
:root[data-theme="light"] .text-yellow-400 { color: #7d6400; }
|
||||
:root[data-theme="light"] .text-orange-400,
|
||||
:root[data-theme="light"] .text-orange-500 { color: #b05c08; }
|
||||
:root[data-theme="light"] .text-purple-300,
|
||||
:root[data-theme="light"] .text-purple-400 { color: #6d45d9; }
|
||||
:root[data-theme="light"] .text-blue-400,
|
||||
:root[data-theme="light"] .text-blue-500 { color: #1d64c8; }
|
||||
:root[data-theme="light"] .text-sky-300 { color: #0b6fa8; }
|
||||
:root[data-theme="light"] .text-amber-300 { color: #8a6c00; }
|
||||
:root[data-theme="light"] .border-purple-400 { border-color: #6d45d9; }
|
||||
|
||||
/* Switching themes repaints the whole page at once. Without this it happens in one
|
||||
frame and reads as a flash; with it the two palettes cross-fade. Only the two
|
||||
properties that actually carry the theme are transitioned — putting `all` here made
|
||||
every hover and layout animation on the page inherit a 200ms delay.
|
||||
The class is added by theme-boot.js for the duration of a switch only, so a page
|
||||
*load* in either theme still paints instantly with no fade. */
|
||||
:root.sb-theme-switching,
|
||||
:root.sb-theme-switching * {
|
||||
transition: background-color .2s ease, border-color .2s ease, color .2s ease !important;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root.sb-theme-switching,
|
||||
:root.sb-theme-switching * {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user