feat(i18n): Arabic, Hebrew, Persian and Urdu, and a layout that mirrors; release v6.4.0
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s

Four right-to-left languages at /ar/, /he/, /fa/ and /ur/ — thirteen in all.

The layout no longer has a left and a right, it has a start and an end: the
margins, insets and corners are CSS logical properties now, so they follow
dir on <html>. Directional glyphs flip; keys, safety codes and session
descriptors are pinned left-to-right so bidi cannot reorder what two people
compare against each other's screens.

Also fixed: the Service Worker was registered as './sw.js', which 404s from
every locale subdirectory, so twelve of the thirteen pages had no worker at
all. And the bundler ran before the dictionaries were generated, so a newly
added language could reach the page ahead of the app that renders it.
This commit is contained in:
lockbitchat
2026-08-29 18:05:20 -04:00
parent 98d42ac2fb
commit 5e32f547b9
68 changed files with 15147 additions and 500 deletions
+79 -71
View File
@@ -25,7 +25,12 @@ import { GroupSession, GROUP_FRAMES, isGroupFrame, groupFrameType, decodeEnvelop
import { GROUP_LIMITS } from './group/groupCrypto.js';
import { createGroupSender } from './group/groupSender.js';
import { spring, snapTarget, rubberband, velocityTracker, prefersReducedMotion, SPRING } from './ui/motion.js';
import { t } from './i18n/index.js';
import { t, direction, LTR_TEXT } from './i18n/index.js';
// +1 in a left-to-right locale, -1 in a right-to-left one. Only the handful of
// horizontal transforms that CSS logical properties cannot express need it
// everything else mirrors on its own from dir on <html>.
const DIR = direction();
// A programmatic smooth scroll is a full-viewport slide, which is exactly what
// someone who asked for less motion does not want. The jump still happens and
@@ -195,7 +200,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
}, [
React.createElement('i', {
key: 'ic',
className: `${copied ? 'fas fa-check text-green-400' : 'far fa-copy'} mr-1`
className: `${copied ? 'fas fa-check text-green-400' : 'far fa-copy'} me-1`
}),
copied ? t('action.copied') : t('action.copy')
])
@@ -250,7 +255,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
type: 'button',
onClick: () => { onPick(opt.value); setOpenMenu(null); },
// Comfortable tap target + readable size on mobile.
className: `w-full text-left px-4 py-3 sm:py-2.5 text-sm flex items-center gap-3 transition-colors ${current === opt.value
className: `w-full text-start px-4 py-3 sm:py-2.5 text-sm flex items-center gap-3 transition-colors ${current === opt.value
? 'accent-orange bg-orange-500/10'
: 'text-gray-200 hover:bg-gray-700/50 active:bg-gray-700/60'}`
}, [
@@ -262,7 +267,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
// purgeable utility class and never pushes the composer layout down.
const picker = (options, current, onPick) =>
React.createElement('div', {
className: "absolute right-0 z-50 min-w-[180px] max-w-[78vw] rounded-xl shadow-2xl overflow-hidden",
className: "absolute end-0 z-50 min-w-[180px] max-w-[78vw] rounded-xl shadow-2xl overflow-hidden",
style: { backgroundColor: '#1f201f', border: '0 solid #e5e7eb', bottom: '100%', marginBottom: '8px' }
}, options.map(opt => pickerItem(opt, current, onPick)));
@@ -361,7 +366,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
}, [
React.createElement('i', {
key: 'icon',
className: `${copied ? 'fas fa-check accent-green' : 'fas fa-copy text-secondary'} mr-2`
className: `${copied ? 'fas fa-check accent-green' : 'fas fa-copy text-secondary'} me-2`
}),
copied ? t('chat.copied') : children
]);
@@ -403,7 +408,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
}, [
React.createElement('div', {
key: 'icon',
className: "w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3"
className: "w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center me-3"
}, [
React.createElement('i', {
className: 'fas fa-shield-alt accent-purple'
@@ -428,7 +433,9 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
}, [
React.createElement('div', {
key: 'code',
className: "verification-code text-2xl py-4"
...LTR_TEXT,
className: "verification-code text-2xl py-4",
style: { ...LTR_TEXT.style, textAlign: 'center' }
}, verificationCode)
]),
React.createElement('div', {
@@ -442,6 +449,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
React.createElement('input', {
key: 'sas-input',
type: 'text',
dir: 'ltr',
value: sasInput,
onChange: (event) => {
setSasInput(event.target.value.toUpperCase());
@@ -480,7 +488,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
}, [
React.createElement('i', {
key: 'local-icon',
className: `fas ${localConfirmed ? 'fa-check-circle text-green-400' : 'fa-clock text-gray-400'} mr-2`
className: `fas ${localConfirmed ? 'fa-check-circle text-green-400' : 'fa-clock text-gray-400'} me-2`
}),
React.createElement('span', {
key: 'local-text',
@@ -502,7 +510,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
}, [
React.createElement('i', {
key: 'remote-icon',
className: `fas ${remoteConfirmed ? 'fa-check-circle text-green-400' : 'fa-clock text-gray-400'} mr-2`
className: `fas ${remoteConfirmed ? 'fa-check-circle text-green-400' : 'fa-clock text-gray-400'} me-2`
}),
React.createElement('span', {
key: 'remote-text',
@@ -519,7 +527,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
className: "text-yellow-400 text-sm flex items-center"
}, [
React.createElement('i', {
className: 'fas fa-exclamation-triangle mr-2'
className: 'fas fa-exclamation-triangle me-2'
}),
t('sas.makeSure')
])
@@ -535,7 +543,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
className: `flex-1 py-3 px-4 rounded-lg font-medium transition-all duration-200 ${!canConfirm ? 'bg-gray-500/20 text-gray-400 cursor-not-allowed' : 'btn-verify text-white'}`
}, [
React.createElement('i', {
className: `fas ${localConfirmed ? 'fa-check-circle' : 'fa-check'} mr-2`
className: `fas ${localConfirmed ? 'fa-check-circle' : 'fa-check'} me-2`
}),
localConfirmed ? t('verify.confirmed') : t('verify.confirm')
]),
@@ -545,7 +553,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
className: "flex-1 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 py-3 px-4 rounded-lg font-medium transition-all duration-200"
}, [
React.createElement('i', {
className: 'fas fa-times mr-2'
className: 'fas fa-times me-2'
}),
t('sas.noMatch')
])
@@ -1385,7 +1393,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
if (opts.animation) { svgStyle.animation = opts.animation; svgStyle.transformOrigin = 'center'; svgStyle.transformBox = 'fill-box'; }
if (opts.style) Object.assign(svgStyle, opts.style);
return h('svg', {
key: opts.key, width: size, height: size, viewBox: '0 0 24 24',
key: opts.key, className: name, width: size, height: size, viewBox: '0 0 24 24',
fill: 'none', stroke: opts.color || 'currentColor',
strokeWidth: def.sw || 2, strokeLinecap: 'round', strokeLinejoin: 'round',
style: svgStyle
@@ -1403,7 +1411,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
minHeight: '100vh', boxSizing: 'border-box',
padding: '46px', display: 'flex', flexDirection: 'column',
justifyContent: 'space-between', gap: '36px',
borderRight: '1px solid rgba(255,255,255,0.06)',
borderInlineEnd: '1px solid rgba(255,255,255,0.06)',
background: 'radial-gradient(900px 600px at 25% 18%, rgba(240,137,42,0.07), transparent 62%), radial-gradient(800px 700px at 80% 92%, rgba(62,207,142,0.06), transparent 60%), #0c0c0e'
}
}, [
@@ -1485,19 +1493,19 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
// RIGHT PANEL · flow body (varies by step)
const segToggle = atIntro && h('div', { key: 'seg', style: { position: 'relative', display: 'flex', padding: '4px', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.07)', background: '#141416', marginBottom: '26px' } }, [
h('div', { key: 'ind', style: { position: 'absolute', top: '4px', bottom: '4px', left: '4px', width: 'calc(50% - 4px)', borderRadius: '9px', background: 'rgba(255,255,255,0.07)', border: '1px solid rgba(255,255,255,0.08)', transform: isCreate ? 'translateX(0%)' : 'translateX(100%)', transition: 'transform .26s cubic-bezier(.3,.8,.3,1)' } }),
h('div', { key: 'ind', style: { position: 'absolute', top: '4px', bottom: '4px', insetInlineStart: '4px', width: 'calc(50% - 4px)', borderRadius: '9px', background: 'rgba(255,255,255,0.07)', border: '1px solid rgba(255,255,255,0.08)', transform: isCreate ? 'translateX(0%)' : `translateX(${100 * DIR}%)`, transition: 'transform .26s cubic-bezier(.3,.8,.3,1)' } }),
h('button', { key: 'c', className: 'sb-seg-btn', onClick: () => setMode('create'), style: { position: 'relative', zIndex: 1, flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '11px', border: 'none', background: 'transparent', color: isCreate ? '#f4f4f6' : '#7b7b83', fontFamily: 'inherit', fontSize: '14px', fontWeight: 700, cursor: 'pointer' } }, [fa('fa-plus', { key: 'i' }), t('action.create')]),
h('button', { key: 'j', className: 'sb-seg-btn', onClick: () => setMode('join'), style: { position: 'relative', zIndex: 1, flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '11px', border: 'none', background: 'transparent', color: !isCreate ? '#f4f4f6' : '#7b7b83', fontFamily: 'inherit', fontSize: '14px', fontWeight: 700, cursor: 'pointer' } }, [fa('fa-link', { key: 'i' }), t('action.join')])
]);
const backButton = (key) => h('button', { key: key || 'back', className: 'sb-soft-btn', onClick: resetToSelect, style: { display: 'inline-flex', alignItems: 'center', gap: '6px', marginBottom: '14px', padding: '6px 11px 6px 8px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.08)', background: 'transparent', color: '#9a9aa2', fontFamily: 'inherit', fontSize: '12.5px', fontWeight: 600, cursor: 'pointer' } }, [fa('fa-chevron-left', { key: 'i' }), t('action.back')]);
const backButton = (key) => h('button', { key: key || 'back', className: 'sb-soft-btn', onClick: resetToSelect, style: { display: 'inline-flex', alignItems: 'center', gap: '6px', marginBottom: '14px', paddingBlock: '6px', paddingInlineStart: '8px', paddingInlineEnd: '11px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.08)', background: 'transparent', color: '#9a9aa2', fontFamily: 'inherit', fontSize: '12.5px', fontWeight: 600, cursor: 'pointer' } }, [fa('fa-chevron-left', { key: 'i' }), t('action.back')]);
// credential code block (offer/answer text fallback + copy)
const credBlock = h('div', { key: 'codeblock', style: { borderRadius: '13px', border: '1px solid rgba(255,255,255,0.08)', background: '#141416', overflow: 'hidden', marginBottom: '16px' } }, [
h('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '9px 12px', borderBottom: '1px solid rgba(255,255,255,0.06)', background: 'rgba(0,0,0,0.2)' } }, [
h('span', { key: 'dot', style: { width: '7px', height: '7px', borderRadius: '50%', background: accent } }),
h('span', { key: 'tag', style: { fontFamily: MONO, fontSize: '10.5px', fontWeight: 600, color: '#8a8a92' } }, isCreate ? t('cred.offerTag') : t('cred.answerTag')),
h('button', { key: 'copy', onClick: copyCred, style: { marginLeft: 'auto', padding: '4px 9px', borderRadius: '6px', border: `1px solid ${copied ? 'rgba(62,207,142,0.4)' : 'rgba(255,255,255,0.1)'}`, background: copied ? 'rgba(62,207,142,0.1)' : 'rgba(255,255,255,0.04)', color: copied ? C_GREEN : '#b3b3ba', fontFamily: 'inherit', fontSize: '11px', fontWeight: 600, cursor: 'pointer', transition: 'all .14s' } }, copied ? t('action.copied') : t('action.copy'))
h('button', { key: 'copy', onClick: copyCred, style: { marginInlineStart: 'auto', padding: '4px 9px', borderRadius: '6px', border: `1px solid ${copied ? 'rgba(62,207,142,0.4)' : 'rgba(255,255,255,0.1)'}`, background: copied ? 'rgba(62,207,142,0.1)' : 'rgba(255,255,255,0.04)', color: copied ? C_GREEN : '#b3b3ba', fontFamily: 'inherit', fontSize: '11px', fontWeight: 600, cursor: 'pointer', transition: 'all .14s' } }, copied ? t('action.copied') : t('action.copy'))
]),
// The handshake code is sensitive keep it blurred until the
// user deliberately reveals it, underscoring that it must be
@@ -1511,7 +1519,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
])
]);
const showQrButton = qrCodeUrl && h('button', { key: 'showqr', onClick: () => setQrModalOpen(true), style: { width: '100%', display: 'flex', alignItems: 'center', gap: '13px', padding: '15px 16px', borderRadius: '14px', border: `1px solid ${isCreate ? 'rgba(240,137,42,0.3)' : 'rgba(62,207,142,0.3)'}`, background: isCreate ? 'rgba(240,137,42,0.06)' : 'rgba(62,207,142,0.06)', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'left', marginBottom: '14px' } }, [
const showQrButton = qrCodeUrl && h('button', { key: 'showqr', onClick: () => setQrModalOpen(true), style: { width: '100%', display: 'flex', alignItems: 'center', gap: '13px', padding: '15px 16px', borderRadius: '14px', border: `1px solid ${isCreate ? 'rgba(240,137,42,0.3)' : 'rgba(62,207,142,0.3)'}`, background: isCreate ? 'rgba(240,137,42,0.06)' : 'rgba(62,207,142,0.06)', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'start', marginBottom: '14px' } }, [
h('span', { key: 'ic', style: { flex: 'none', width: '42px', height: '42px', borderRadius: '12px', display: 'grid', placeItems: 'center', background: isCreate ? 'rgba(240,137,42,0.12)' : 'rgba(62,207,142,0.12)', border: `1px solid ${isCreate ? 'rgba(240,137,42,0.28)' : 'rgba(62,207,142,0.28)'}` } }, fa('fa-qrcode', { color: accent, fontSize: '18px' })),
h('span', { key: 'tx', style: { flex: 1 } }, [
h('span', { key: 't', style: { display: 'block', fontSize: '14.5px', fontWeight: 700, color: '#f4f4f6' } }, t('qr.showTitle')),
@@ -1532,7 +1540,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
h('h2', { key: 't', style: { margin: 0, fontSize: '21px', fontWeight: 800, letterSpacing: '-0.4px', color: '#f4f4f6' } }, t('verify.title'))
]),
h('p', { key: 'sub', style: { margin: '0 0 18px', fontSize: '13.5px', lineHeight: 1.55, color: '#8a8a92' } }, t('verify.desc')),
h('div', { key: 'cells', style: { display: 'flex', gap: '6px', justifyContent: 'center', marginBottom: '20px', flexWrap: 'wrap' } }, cells),
h('div', { key: 'cells', dir: 'ltr', style: { display: 'flex', gap: '6px', justifyContent: 'center', marginBottom: '20px', flexWrap: 'wrap' } }, cells),
verified
? h('div', { key: 'ok', style: { display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center', padding: '24px 16px', borderRadius: '16px', border: '1px solid rgba(62,207,142,0.25)', background: 'rgba(62,207,142,0.06)', animation: 'sbUp .3s ease' } }, [
h('div', { key: 'i', style: { width: '54px', height: '54px', borderRadius: '16px', display: 'grid', placeItems: 'center', background: 'rgba(62,207,142,0.14)', border: '1px solid rgba(62,207,142,0.35)', marginBottom: '14px' } }, fa('fa-check', { color: C_GREEN, fontSize: '24px' })),
@@ -1541,7 +1549,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
])
: h('div', { key: 'form' }, [
h('div', { key: 'lbl', style: { fontSize: '12.5px', fontWeight: 600, color: '#9a9aa2', marginBottom: '8px' } }, t('verify.enterLabel')),
h('input', { key: 'in', value: sasInput, onChange: (e) => { setSasInput(e.target.value.toUpperCase()); if (sasError) setSasError(''); }, disabled: localVerificationConfirmed, autoFocus: true, autoComplete: 'off', spellCheck: false, placeholder: verificationCode ? t('verify.placeholder') : t('verify.waiting'), style: { width: '100%', textAlign: 'center', letterSpacing: '6px', borderRadius: '12px', border: `1px solid ${sasInput.length ? (canConfirm || localVerificationConfirmed ? 'rgba(62,207,142,0.5)' : 'rgba(255,255,255,0.14)') : 'rgba(255,255,255,0.08)'}`, background: '#141416', color: '#f4f4f6', fontFamily: MONO, fontSize: '20px', fontWeight: 700, padding: '14px', outline: 'none', textTransform: 'uppercase', marginBottom: sasError ? '8px' : '16px' } }),
h('input', { key: 'in', dir: 'ltr', value: sasInput, onChange: (e) => { setSasInput(e.target.value.toUpperCase()); if (sasError) setSasError(''); }, disabled: localVerificationConfirmed, autoFocus: true, autoComplete: 'off', spellCheck: false, placeholder: verificationCode ? t('verify.placeholder') : t('verify.waiting'), style: { width: '100%', textAlign: 'center', letterSpacing: '6px', borderRadius: '12px', border: `1px solid ${sasInput.length ? (canConfirm || localVerificationConfirmed ? 'rgba(62,207,142,0.5)' : 'rgba(255,255,255,0.14)') : 'rgba(255,255,255,0.08)'}`, background: '#141416', color: '#f4f4f6', fontFamily: MONO, fontSize: '20px', fontWeight: 700, padding: '14px', outline: 'none', textTransform: 'uppercase', marginBottom: sasError ? '8px' : '16px' } }),
sasError && h('p', { key: 'err', style: { color: '#e5727a', fontSize: '12.5px', margin: '0 0 16px' } }, sasError),
h('div', { key: 'status', style: { display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' } }, [
h('div', { key: 'you', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '11px 14px', borderRadius: '11px', border: '1px solid rgba(255,255,255,0.06)', background: '#141416' } }, [
@@ -1593,7 +1601,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
isOfferCred && h('div', { key: 'offerextra', style: { marginTop: '4px' } }, [
h('div', { key: 'lbl', style: { fontSize: '12.5px', fontWeight: 600, color: '#9a9aa2', marginBottom: '8px' } }, t('handshake.thenReceive')),
h('div', { key: 'ta', style: { borderRadius: '12px', border: `1px solid ${hasAnswer ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.07)'}`, background: '#141416', padding: '11px 14px', marginBottom: '10px' } },
h('textarea', { value: answerInput, onChange: (e) => { setAnswerInput(e.target.value); if (e.target.value.trim().length > 0 && typeof markAnswerCreated === 'function') markAnswerCreated(); }, rows: 2, placeholder: t('handshake.pasteAnswerPlaceholder'), style: { width: '100%', resize: 'none', border: 'none', outline: 'none', background: 'transparent', color: '#d7d7db', fontFamily: MONO, fontSize: '12px', lineHeight: 1.55, minHeight: '44px' } })),
h('textarea', { dir: 'ltr', value: answerInput, onChange: (e) => { setAnswerInput(e.target.value); if (e.target.value.trim().length > 0 && typeof markAnswerCreated === 'function') markAnswerCreated(); }, rows: 2, placeholder: t('handshake.pasteAnswerPlaceholder'), style: { width: '100%', resize: 'none', border: 'none', outline: 'none', background: 'transparent', color: '#d7d7db', fontFamily: MONO, fontSize: '12px', lineHeight: 1.55, minHeight: '44px' } })),
h('div', { key: 'btns', style: { display: 'flex', gap: '10px' } }, [
h('button', { key: 'scan', className: 'sb-scan-btn', onClick: () => setShowQRScannerModal(true), style: { flex: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '14px 16px', borderRadius: '13px', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.04)', color: '#cfcfd4', fontFamily: 'inherit', fontSize: '14px', fontWeight: 700, cursor: 'pointer' } }, [fa('fa-camera', { key: 'i' }), t('action.scan')]),
h('button', { key: 'est', onClick: onConnect, disabled: !hasAnswer, style: { flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '9px', padding: '14px', borderRadius: '13px', border: 'none', background: hasAnswer ? C_ORANGE : 'rgba(255,255,255,0.05)', color: hasAnswer ? '#1a0f04' : '#56565e', fontFamily: 'inherit', fontSize: '14.5px', fontWeight: 700, cursor: hasAnswer ? 'pointer' : 'not-allowed', boxShadow: hasAnswer ? '0 8px 24px rgba(240,137,42,0.28)' : 'none' } }, t('handshake.establish'))
@@ -1616,7 +1624,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
inner = h('div', { key: 'introJ', style: { animation: 'sbUp .28s ease' } }, [
h('h2', { key: 'h', style: { margin: '0 0 6px', fontSize: '23px', fontWeight: 800, letterSpacing: '-0.5px', color: '#f4f4f6' } }, t('intro.joinTitle')),
h('p', { key: 'p', style: { margin: '0 0 16px', fontSize: '14px', lineHeight: 1.55, color: '#8a8a92' } }, "Scan your peer's QR with your camera, or paste their invitation code."),
h('button', { key: 'scan', className: 'sb-scan-btn', onClick: () => { requestNotificationPermissionOnInteraction(); setShowQRScannerModal(true); }, style: { width: '100%', display: 'flex', alignItems: 'center', gap: '13px', padding: '15px 16px', borderRadius: '14px', border: '1px solid rgba(62,207,142,0.3)', background: 'rgba(62,207,142,0.06)', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'left', marginBottom: '14px' } }, [
h('button', { key: 'scan', className: 'sb-scan-btn', onClick: () => { requestNotificationPermissionOnInteraction(); setShowQRScannerModal(true); }, style: { width: '100%', display: 'flex', alignItems: 'center', gap: '13px', padding: '15px 16px', borderRadius: '14px', border: '1px solid rgba(62,207,142,0.3)', background: 'rgba(62,207,142,0.06)', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'start', marginBottom: '14px' } }, [
h('span', { key: 'ic', style: { flex: 'none', width: '42px', height: '42px', borderRadius: '12px', display: 'grid', placeItems: 'center', background: 'rgba(62,207,142,0.12)', border: '1px solid rgba(62,207,142,0.28)' } }, fa('fa-camera', { color: C_GREEN, fontSize: '18px' })),
h('span', { key: 'tx', style: { flex: 1 } }, [
h('span', { key: 't', style: { display: 'block', fontSize: '14.5px', fontWeight: 700, color: '#f4f4f6' } }, t('intro.scanTitle')),
@@ -1630,7 +1638,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
h('span', { key: 'b', style: { flex: 1, height: '1px', background: 'rgba(255,255,255,0.07)' } })
]),
h('div', { key: 'ta', style: { borderRadius: '13px', border: `1px solid ${hasInvite ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.07)'}`, background: '#141416', padding: '13px 15px', marginBottom: '12px' } },
h('textarea', { value: offerInput, onChange: (e) => { setOfferInput(e.target.value); if (e.target.value.trim().length > 0 && typeof markAnswerCreated === 'function') markAnswerCreated(); }, rows: 3, placeholder: t('intro.pastePlaceholder'), style: { width: '100%', resize: 'none', border: 'none', outline: 'none', background: 'transparent', color: '#d7d7db', fontFamily: MONO, fontSize: '12.5px', lineHeight: 1.6, minHeight: '66px' } })),
h('textarea', { dir: 'ltr', value: offerInput, onChange: (e) => { setOfferInput(e.target.value); if (e.target.value.trim().length > 0 && typeof markAnswerCreated === 'function') markAnswerCreated(); }, rows: 3, placeholder: t('intro.pastePlaceholder'), style: { width: '100%', resize: 'none', border: 'none', outline: 'none', background: 'transparent', color: '#d7d7db', fontFamily: MONO, fontSize: '12.5px', lineHeight: 1.6, minHeight: '66px' } })),
h('button', { key: 'connect', onClick: () => { requestNotificationPermissionOnInteraction(); onCreateAnswer(); }, disabled: !hasInvite || connectionStatus === 'connecting', style: { width: '100%', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '9px', padding: '14px', borderRadius: '13px', border: 'none', background: (hasInvite && connectionStatus !== 'connecting') ? C_ORANGE : 'rgba(255,255,255,0.05)', color: (hasInvite && connectionStatus !== 'connecting') ? '#1a0f04' : '#56565e', fontFamily: 'inherit', fontSize: '15px', fontWeight: 700, cursor: (hasInvite && connectionStatus !== 'connecting') ? 'pointer' : 'not-allowed', boxShadow: (hasInvite && connectionStatus !== 'connecting') ? '0 8px 24px rgba(240,137,42,0.28)' : 'none' } }, connectionStatus === 'connecting' ? t('intro.connecting') : t('intro.connect'))
]);
}
@@ -1665,7 +1673,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
const otherOS = ['mac', 'win', 'linux'].filter((k) => k !== detectedOS);
const dlLink = (url) => { try { window.open(url, '_blank', 'noopener'); } catch (e) {} };
const platformsMenu = platformsOpen && h('div', { key: 'platmenu', className: 'sb-platforms-menu', style: { position: 'absolute', left: 0, bottom: 'calc(100% + 10px)', width: '344px', maxWidth: '100%', borderRadius: '16px', border: '1px solid rgba(255,255,255,0.1)', background: '#161618', boxShadow: '0 24px 60px rgba(0,0,0,0.55)', overflow: 'hidden', zIndex: 25, animation: 'sbUp .2s ease' } }, [
const platformsMenu = platformsOpen && h('div', { key: 'platmenu', className: 'sb-platforms-menu', style: { position: 'absolute', insetInlineStart: 0, bottom: 'calc(100% + 10px)', width: '344px', maxWidth: '100%', borderRadius: '16px', border: '1px solid rgba(255,255,255,0.1)', background: '#161618', boxShadow: '0 24px 60px rgba(0,0,0,0.55)', overflow: 'hidden', zIndex: 25, animation: 'sbUp .2s ease' } }, [
h('div', { key: 'mh', style: { display: 'flex', alignItems: 'center', gap: '10px', padding: '14px 16px', borderBottom: '1px solid rgba(255,255,255,0.06)' } }, [
h('div', { key: 't', style: { flex: 1, lineHeight: 1.2 } }, [
h('div', { key: 'a', style: { fontSize: '14px', fontWeight: 800, color: '#f4f4f6' } }, t('dl.title')),
@@ -1674,7 +1682,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
h('span', { key: 'pill', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 600, color: C_GREEN, padding: '3px 8px', borderRadius: '6px', background: 'rgba(62,207,142,0.1)', border: '1px solid rgba(62,207,142,0.22)' } }, t('chat.onWeb'))
]),
h('div', { key: 'rec', style: { padding: '12px 12px 6px' } },
h('button', { key: 'b', onClick: () => dlLink(DOWNLOADS[detectedOS].url), style: { width: '100%', display: 'flex', alignItems: 'center', gap: '12px', padding: '13px 14px', borderRadius: '12px', border: '1px solid rgba(240,137,42,0.4)', background: 'rgba(240,137,42,0.08)', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'left' } }, [
h('button', { key: 'b', onClick: () => dlLink(DOWNLOADS[detectedOS].url), style: { width: '100%', display: 'flex', alignItems: 'center', gap: '12px', padding: '13px 14px', borderRadius: '12px', border: '1px solid rgba(240,137,42,0.4)', background: 'rgba(240,137,42,0.08)', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'start' } }, [
h('span', { key: 'ic', style: { flex: 'none', display: 'grid', placeItems: 'center', width: '38px', height: '38px', borderRadius: '11px', background: 'rgba(240,137,42,0.14)', border: '1px solid rgba(240,137,42,0.3)', color: C_ORANGE } }, h('i', { className: DOWNLOADS[detectedOS].icon, style: { fontSize: '17px' } })),
h('span', { key: 'tx', style: { flex: 1, minWidth: 0 } }, [
h('span', { key: 'n', style: { display: 'block', fontSize: '13.5px', fontWeight: 700, color: '#f4f4f6' } }, DOWNLOADS[detectedOS].name),
@@ -1683,7 +1691,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
fa('fa-download', { color: C_ORANGE })
])),
h('div', { key: 'others', style: { padding: '0 12px 8px', display: 'flex', flexDirection: 'column', gap: '2px' } },
otherOS.map((k) => h('button', { key: k, onClick: () => dlLink(DOWNLOADS[k].url), style: { width: '100%', display: 'flex', alignItems: 'center', gap: '12px', padding: '11px 14px', borderRadius: '11px', border: 'none', background: 'transparent', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'left' } }, [
otherOS.map((k) => h('button', { key: k, onClick: () => dlLink(DOWNLOADS[k].url), style: { width: '100%', display: 'flex', alignItems: 'center', gap: '12px', padding: '11px 14px', borderRadius: '11px', border: 'none', background: 'transparent', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', textAlign: 'start' } }, [
h('span', { key: 'ic', style: { flex: 'none', display: 'grid', placeItems: 'center', width: '34px', height: '34px', borderRadius: '10px', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', color: '#cfcfd4' } }, h('i', { className: DOWNLOADS[k].icon, style: { fontSize: '15px' } })),
h('span', { key: 'tx', style: { flex: 1, minWidth: 0 } }, [
h('span', { key: 'n', style: { display: 'block', fontSize: '13px', fontWeight: 600, color: '#e8e8eb' } }, DOWNLOADS[k].name),
@@ -1698,7 +1706,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
]);
const footer = h('div', { key: 'footer', className: 'sb-conn-footer', style: { position: 'relative', marginTop: '30px', paddingTop: '18px', borderTop: '1px solid rgba(255,255,255,0.06)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', flexWrap: 'wrap' } }, [
h('button', { key: 'dl', onClick: () => setPlatformsOpen((v) => !v), style: { display: 'inline-flex', alignItems: 'center', gap: '9px', padding: '8px 13px 8px 9px', borderRadius: '10px', border: `1px solid ${platformsOpen ? 'rgba(240,137,42,0.4)' : 'rgba(255,255,255,0.08)'}`, background: platformsOpen ? 'rgba(240,137,42,0.06)' : 'rgba(255,255,255,0.02)', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', transition: 'all .15s' } }, [
h('button', { key: 'dl', onClick: () => setPlatformsOpen((v) => !v), style: { display: 'inline-flex', alignItems: 'center', gap: '9px', paddingBlock: '8px', paddingInlineStart: '9px', paddingInlineEnd: '13px', borderRadius: '10px', border: `1px solid ${platformsOpen ? 'rgba(240,137,42,0.4)' : 'rgba(255,255,255,0.08)'}`, background: platformsOpen ? 'rgba(240,137,42,0.06)' : 'rgba(255,255,255,0.02)', color: 'inherit', fontFamily: 'inherit', cursor: 'pointer', transition: 'all .15s' } }, [
fa('fa-download', { key: 'i', color: C_ORANGE }),
h('span', { key: 't', style: { fontSize: '12.5px', fontWeight: 700, color: '#e8e8eb' } }, t('action.downloadDesktop')),
fa('fa-chevron-down', { key: 'c', color: '#6b6b73', style: { fontSize: '11px', transform: platformsOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform .2s' } })
@@ -1928,7 +1936,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
modal.innerHTML = `
<style>@keyframes svIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes svPulse{0%,100%{opacity:1}50%{opacity:.4}}</style>
<div style="position:relative; max-width:960px; width:100%; border-radius:20px; background:radial-gradient(1100px 640px at 50% -6%, rgba(${accentRGB},0.05), transparent 62%), #0f0f11; border:1px solid rgba(255,255,255,0.08); color:#e8e8eb; padding:30px; box-shadow:0 30px 70px rgba(0,0,0,0.55); animation:svIn .3s cubic-bezier(.2,.7,.3,1); max-height:calc(100vh - 48px); overflow:auto;">
<button class="sv-close" type="button" title="${t('chat.close')}" style="position:absolute; top:16px; right:16px; width:32px; height:32px; 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; z-index:2;">
<button class="sv-close" type="button" title="${t('chat.close')}" style="position:absolute; top:16px; inset-inline-end:16px; width:32px; height:32px; 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; z-index:2;">
<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="position:relative; overflow:hidden; border-radius:18px; background:#141416; border:1px solid rgba(255,255,255,0.07); padding:28px 30px; display:flex; align-items:center; gap:30px; flex-wrap:wrap; margin-bottom:20px;">
@@ -2065,7 +2073,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
// Mobile: leave room for the drawer hamburger and shed non-essential header
// chrome so avatar + name + status + call/Disconnect fit a narrow screen.
'@media (max-width:768px){' +
'.sb-chat-header{padding-left:58px !important;gap:8px !important;}' +
'.sb-chat-header{padding-inline-start:58px !important;gap:8px !important;}' +
// Remove the security/verification pill on mobile (per request) the
// full report is still available from the desktop header.
'.sb-chat-header .sb-secpill{display:none !important;}' +
@@ -2084,7 +2092,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
'.sb-chips .sb-chips-right{flex-wrap:nowrap !important;}' +
'.sb-chips .sb-chip{white-space:nowrap;}' +
'}' +
'@media (max-width:480px){.sb-chat-header{padding-right:12px !important;}}'
'@media (max-width:480px){.sb-chat-header{padding-inline-end:12px !important;}}'
} });
const header = React.createElement('header', {
key: 'hdr', className: 'sb-chat-header', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '24px', padding: '0 20px', height: '64px', borderBottom: '1px solid rgba(255,255,255,0.06)', background: 'rgba(18,18,20,0.72)', backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)' }
@@ -2095,7 +2103,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
React.createElement('div', { key: 'left', style: { display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0 } }, [
React.createElement('div', { key: 'avatar', style: { position: 'relative', flex: 'none', width: '36px', height: '36px', borderRadius: '10px', display: 'grid', placeItems: 'center', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.09)', fontSize: '13px', fontWeight: 700, letterSpacing: '-0.3px', color: '#e8e8eb' } }, [
monoInitials(title || t('chatHdr.chat')),
React.createElement('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: peerDot, border: '2px solid #121214' } })
React.createElement('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: peerDot, border: '2px solid #121214' } })
]),
editingName
? React.createElement('div', { key: 'edit', style: { display: 'flex', flexDirection: 'column', gap: '4px', minWidth: 0 } }, [
@@ -2358,7 +2366,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
}, [
React.createElement('button', {
key: 'hs-btn', onClick: () => setShowHandshake(v => !v),
style: { width: '100%', display: 'flex', alignItems: 'center', gap: '13px', padding: '14px 16px', background: 'transparent', border: 'none', color: 'inherit', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit' }
style: { width: '100%', display: 'flex', alignItems: 'center', gap: '13px', padding: '14px 16px', background: 'transparent', border: 'none', color: 'inherit', cursor: 'pointer', textAlign: 'start', fontFamily: 'inherit' }
}, [
React.createElement('div', { key: 'ic', style: { flex: 'none', width: '30px', height: '30px', display: 'grid', placeItems: 'center' } },
React.createElement('i', { className: 'fas fa-check', style: { color: '#3ecf8e', fontSize: '16px' } })
@@ -2369,8 +2377,8 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
]),
React.createElement('i', { key: 'chev', className: 'fas fa-chevron-down', style: { flex: 'none', color: '#6b6b73', fontSize: '13px', transform: showHandshake ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform .2s' } })
]),
showHandshake && React.createElement('div', { key: 'hs-body', style: { padding: '2px 16px 14px 59px' } }, [
systemMessages.length > 0 && React.createElement('div', { key: 'steps', className: 'sb-scroll', style: { marginBottom: '12px', maxHeight: '220px', overflowY: 'auto', paddingRight: '6px' } },
showHandshake && React.createElement('div', { key: 'hs-body', style: { paddingTop: '2px', paddingBottom: '14px', paddingInlineStart: '59px', paddingInlineEnd: '16px' } }, [
systemMessages.length > 0 && React.createElement('div', { key: 'steps', className: 'sb-scroll', style: { marginBottom: '12px', maxHeight: '220px', overflowY: 'auto', paddingInlineEnd: '6px' } },
systemMessages.map((m, i) => React.createElement('div', { key: 's' + i, style: { display: 'flex', gap: '11px', padding: '6px 0', borderTop: i === 0 ? 'none' : '1px solid rgba(255,255,255,0.04)' } }, [
React.createElement('span', { key: 'd', style: { flex: 'none', width: '5px', height: '5px', borderRadius: '50%', background: '#3ecf8e', marginTop: '7px', opacity: 0.6 } }),
React.createElement('span', { key: 't', style: { flex: 1, fontSize: '12.5px', color: '#9a9aa2', lineHeight: 1.5, wordBreak: 'break-word' } }, String(m.message || '').trim()),
@@ -2425,12 +2433,12 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
// ---- option rows ----
const timerRow = showTimer && React.createElement('div', { key: 'timer-row', style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '8px', padding: '10px 12px', marginBottom: '10px', borderRadius: '11px', border: '1px solid rgba(255,255,255,0.07)', background: '#161618' } },
[React.createElement('span', { key: 'lbl', style: { fontSize: '12px', color: '#8a8a92', fontWeight: 600, marginRight: '4px' } }, t('chat.disappearAfter'))].concat(
[React.createElement('span', { key: 'lbl', style: { fontSize: '12px', color: '#8a8a92', fontWeight: 600, marginInlineEnd: '4px' } }, t('chat.disappearAfter'))].concat(
timerDefs.map((d) => React.createElement('button', { key: 'td' + d.v, onClick: () => pickTimer(d.v), style: optStyle(disappearTtl === d.v) }, d.label))
)
);
const onceRow = showOnce && React.createElement('div', { key: 'once-row', style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '8px', padding: '10px 12px', marginBottom: '10px', borderRadius: '11px', border: '1px solid rgba(255,255,255,0.07)', background: '#161618' } },
[React.createElement('span', { key: 'lbl', style: { fontSize: '12px', color: '#8a8a92', fontWeight: 600, marginRight: '4px' } }, t('chat.visibleFor'))].concat(
[React.createElement('span', { key: 'lbl', style: { fontSize: '12px', color: '#8a8a92', fontWeight: 600, marginInlineEnd: '4px' } }, t('chat.visibleFor'))].concat(
onceDefs.map((d) => React.createElement('button', { key: 'od' + d.v, onClick: () => pickOnce(d.v), style: optStyle(onceSelected === d.v) }, d.label))
)
);
@@ -2474,14 +2482,14 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
const codeStrip = codeMode && React.createElement('div', { key: 'code-strip', style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 14px', border: '1px solid rgba(255,255,255,0.08)', borderBottom: 'none', borderRadius: '14px 14px 0 0', background: '#161618' } }, [
React.createElement('i', { key: 'i', className: 'fas fa-code', style: { color: '#8a8a92', fontSize: '12px' } }),
React.createElement('span', { key: 's', style: { fontSize: '11.5px', fontWeight: 600, color: '#8a8a92' } }, t('chat.codeHint')),
React.createElement('button', { key: 'c', onClick: () => setCodeMode(false), className: 'sb-link', style: { marginLeft: 'auto', background: 'none', border: 'none', color: '#6b6b73', cursor: 'pointer', fontSize: '11.5px', fontFamily: 'inherit', fontWeight: 600 } }, t('chat.close'))
React.createElement('button', { key: 'c', onClick: () => setCodeMode(false), className: 'sb-link', style: { marginInlineStart: 'auto', background: 'none', border: 'none', color: '#6b6b73', cursor: 'pointer', fontSize: '11.5px', fontFamily: 'inherit', fontWeight: 600 } }, t('chat.close'))
]);
// ---- input row ----
const sendBtn = React.createElement('button', {
key: 'send', onClick: onSendMessage, disabled: !hasText, title: t('chat.send'), className: 'sb-send',
style: { flex: 'none', width: '44px', height: '44px', borderRadius: '11px', border: 'none', display: 'grid', placeItems: 'center', cursor: hasText ? 'pointer' : 'default', background: hasText ? '#f0892a' : 'rgba(255,255,255,0.05)', color: hasText ? '#1a0f04' : '#56565e', transition: 'all .15s' }
}, React.createElement('i', { className: 'fas fa-paper-plane', style: { fontSize: '15px' } }));
}, React.createElement('i', { className: 'fas fa-paper-plane sb-mirror-rtl', style: { fontSize: '15px' } }));
const micBtn = React.createElement('button', {
key: 'mic', onClick: () => setIsRecording(true), title: t('chat.recordVoice'), className: 'sb-send',
style: { flex: 'none', width: '44px', height: '44px', borderRadius: '50%', border: 'none', display: 'grid', placeItems: 'center', cursor: 'pointer', background: '#f0892a', color: '#1a0f04', boxShadow: '0 8px 22px rgba(240,137,42,0.3)', transition: 'transform .15s cubic-bezier(.2,.7,.3,1)' }
@@ -2498,7 +2506,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
const inputRow = React.createElement('div', {
key: 'input',
style: { display: 'flex', alignItems: 'flex-end', gap: '11px', padding: '11px 11px 11px 16px', border: '1px solid ' + (hasText ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)'), background: '#161618', borderRadius: codeMode ? '0 0 14px 14px' : '14px', transition: 'border .15s' }
style: { display: 'flex', alignItems: 'flex-end', gap: '11px', paddingBlock: '11px', paddingInlineStart: '16px', paddingInlineEnd: '11px', border: '1px solid ' + (hasText ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)'), background: '#161618', borderRadius: codeMode ? '0 0 14px 14px' : '14px', transition: 'border .15s' }
}, [
React.createElement('div', { key: 'ta-wrap', style: { flex: 1, minWidth: 0 } }, [
React.createElement('textarea', {
@@ -2518,7 +2526,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
React.createElement('i', { key: 'i', className: 'fas fa-lock', style: { color: '#3ecf8e', fontSize: '10px' } }),
t('chat.encryptedOnDevice')
]),
React.createElement('span', { key: 'cnt', style: { fontFamily: MONO, fontSize: '10.5px', color: '#56565e', marginLeft: 'auto' } }, (messageInput ? messageInput.length : 0) + '/2000')
React.createElement('span', { key: 'cnt', style: { fontFamily: MONO, fontSize: '10.5px', color: '#56565e', marginInlineStart: 'auto' } }, (messageInput ? messageInput.length : 0) + '/2000')
])
])
].concat(trailingButtons));
@@ -2542,7 +2550,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
const scrollBtn = showScrollButton && React.createElement('button', {
key: 'scrollbtn', onClick: handleScrollToBottom,
style: { position: 'fixed', right: '24px', bottom: '150px', width: '44px', height: '44px', borderRadius: '50%', border: '1px solid rgba(255,255,255,0.1)', background: '#26262b', color: '#cfcfd4', display: 'grid', placeItems: 'center', cursor: 'pointer', zIndex: 50, boxShadow: '0 6px 20px rgba(0,0,0,0.4)' }
style: { position: 'fixed', insetInlineEnd: '24px', bottom: '150px', width: '44px', height: '44px', borderRadius: '50%', border: '1px solid rgba(255,255,255,0.1)', background: '#26262b', color: '#cfcfd4', display: 'grid', placeItems: 'center', cursor: 'pointer', zIndex: 50, boxShadow: '0 6px 20px rgba(0,0,0,0.4)' }
}, React.createElement('i', { className: 'fas fa-arrow-down', style: { fontSize: '15px' } }));
const chatHeader = React.createElement(SecureBitChatHeader, {
@@ -2585,8 +2593,8 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
// (72px) plus a mobile slide-out drawer. Pure presentational: all data comes
// from decorated session objects, all actions are callbacks.
const SB_SVG = {
chevL: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>',
chevR: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>',
chevL: '<svg class="sb-mirror-rtl" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>',
chevR: '<svg class="sb-mirror-rtl" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>',
plus: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>',
users: '<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 19v-1.5a3.5 3.5 0 0 0-3.5-3.5h-5A3.5 3.5 0 0 0 4 17.5V19"/><circle cx="10" cy="8" r="3.2"/><path d="M20 19v-1.5a3.5 3.5 0 0 0-2.6-3.4"/><path d="M15.5 5.3a3.2 3.2 0 0 1 0 5.4"/></svg>',
burger: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg>'
@@ -2627,7 +2635,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
offsetRef.current = x;
const w = drawerWidth();
const progress = Math.min(1, Math.max(0, 1 + x / w));
if (panelRef.current) panelRef.current.style.transform = 'translate3d(' + x.toFixed(2) + 'px,0,0)';
if (panelRef.current) panelRef.current.style.transform = 'translate3d(' + (x * DIR).toFixed(2) + 'px,0,0)';
if (scrimRef.current) {
scrimRef.current.style.opacity = progress.toFixed(3);
// The blur comes up with the panel rather than being switched
@@ -2699,7 +2707,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
axis: null,
vel: velocityTracker()
};
dragRef.current.vel.add(e.clientX, e.timeStamp || performance.now());
dragRef.current.vel.add(e.clientX * DIR, e.timeStamp || performance.now());
};
const drawerMove = (e) => {
@@ -2724,9 +2732,9 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
}
try { e.currentTarget.setPointerCapture(d.id); } catch (_) {}
}
d.vel.add(e.clientX, e.timeStamp || performance.now());
d.vel.add(e.clientX * DIR, e.timeStamp || performance.now());
const w = drawerWidth();
let x = d.base + (e.clientX - d.x0);
let x = d.base + (e.clientX - d.x0) * DIR;
// Past the open edge there is nothing left to reveal. Resisting
// reads as "responsive, but this is the end"; stopping dead reads
// as the app having frozen.
@@ -2772,7 +2780,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
const icon = (svg, style) => h('span', { style: Object.assign({ display: 'grid', placeItems: 'center' }, style || {}), dangerouslySetInnerHTML: { __html: svg } });
const avatar = (c, size, ring) => h('div', {
style: { position: 'relative', flex: 'none', width: size + 'px', height: size + 'px', borderRadius: (size >= 44 ? 12 : 11) + 'px', display: 'grid', placeItems: 'center', background: c.active ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.035)', border: '1px solid rgba(255,255,255,' + (c.active ? '0.14' : '0.07') + ')', fontSize: '13px', fontWeight: 700, letterSpacing: '-0.3px', color: c.active ? '#f4f4f6' : '#9a9aa2' }
}, [c.mono, h('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: c.dot, border: '2px solid ' + ring } })]);
}, [c.mono, h('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: c.dot, border: '2px solid ' + ring } })]);
// ---- Expanded list row ----
const expandedRow = (c) => h('div', {
@@ -2780,7 +2788,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
onClick: () => onSelect(c.id),
style: { position: 'relative', display: 'flex', alignItems: 'center', gap: '12px', padding: '11px 12px', marginBottom: '4px', borderRadius: '11px', background: c.active ? '#161618' : 'transparent', border: '1px solid ' + (c.active ? 'rgba(255,255,255,0.08)' : 'transparent'), cursor: 'pointer' }
}, [
c.active && h('span', { key: 'bar', style: { position: 'absolute', left: 0, top: '12px', bottom: '12px', width: '3px', borderRadius: '0 3px 3px 0', background: '#f0892a' } }),
c.active && h('span', { key: 'bar', style: { position: 'absolute', insetInlineStart: 0, top: '12px', bottom: '12px', width: '3px', borderStartEndRadius: '3px', borderEndEndRadius: '3px', background: '#f0892a' } }),
avatar(c, 38, c.active ? '#161618' : '#0c0c0e'),
h('div', { key: 'body', style: { flex: 1, minWidth: 0 } }, [
h('div', { key: 'top', style: { display: 'flex', alignItems: 'center', gap: '7px' } }, [
@@ -2800,7 +2808,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
// ---- Collapsed dock item ----
const dockItem = (c) => h('div', { key: c.id, style: { position: 'relative' } }, [
c.active && h('span', { key: 'bar', style: { position: 'absolute', left: '-13px', top: '9px', bottom: '9px', width: '3px', borderRadius: '0 3px 3px 0', background: '#f0892a' } }),
c.active && h('span', { key: 'bar', style: { position: 'absolute', insetInlineStart: '-13px', top: '9px', bottom: '9px', width: '3px', borderStartEndRadius: '3px', borderEndEndRadius: '3px', background: '#f0892a' } }),
h('div', {
key: 'tile',
onClick: () => onSelect(c.id),
@@ -2808,8 +2816,8 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
style: { position: 'relative', width: '44px', height: '44px', borderRadius: '12px', display: 'grid', placeItems: 'center', cursor: 'pointer', background: c.active ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,' + (c.active ? '0.14' : '0.07') + ')', fontSize: '13px', fontWeight: 700, letterSpacing: '-0.3px', color: c.active ? '#f4f4f6' : '#9a9aa2' }
}, [
c.mono,
h('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: c.dot, border: '2.5px solid #0c0c0e' } }),
c.unread && h('span', { key: 'u', style: { position: 'absolute', left: '-5px', top: '-5px', minWidth: '17px', height: '17px', padding: '0 4px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: '#f0892a', color: '#1a0f04', fontFamily: "'JetBrains Mono',monospace", fontSize: '9.5px', fontWeight: 700, border: '2px solid #0c0c0e' } }, c.unread)
h('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: c.dot, border: '2.5px solid #0c0c0e' } }),
c.unread && h('span', { key: 'u', style: { position: 'absolute', insetInlineStart: '-5px', top: '-5px', minWidth: '17px', height: '17px', padding: '0 4px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: '#f0892a', color: '#1a0f04', fontFamily: "'JetBrains Mono',monospace", fontSize: '9.5px', fontWeight: 700, border: '2px solid #0c0c0e' } }, c.unread)
])
]);
@@ -2836,7 +2844,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
...MY_STATUS_OPTIONS.map((o) => h('button', {
key: o.key,
onClick: () => { onSetStatus(o.key); setPresenceOpen(false); },
style: { width: '100%', display: 'flex', alignItems: 'center', gap: '11px', padding: '9px 10px', borderRadius: '9px', border: 'none', background: 'transparent', cursor: 'pointer', textAlign: 'left' }
style: { width: '100%', display: 'flex', alignItems: 'center', gap: '11px', padding: '9px 10px', borderRadius: '9px', border: 'none', background: 'transparent', cursor: 'pointer', textAlign: 'start' }
}, [
h('span', { key: 'd', style: { flex: 'none', width: '10px', height: '10px', borderRadius: '50%', background: o.dot } }),
h('span', { key: 't', style: { flex: 1, minWidth: 0 } }, [
@@ -2855,9 +2863,9 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
h('button', { key: 'btn', onClick: () => setPresenceOpen((v) => !v), style: { width: '100%', display: 'flex', alignItems: 'center', gap: '11px', padding: '7px 8px', borderRadius: '11px', border: '1px solid rgba(255,255,255,0.06)', background: 'rgba(255,255,255,0.02)', cursor: 'pointer' } }, [
h('div', { key: 'av', style: { position: 'relative', flex: 'none', width: '36px', height: '36px', borderRadius: '10px', display: 'grid', placeItems: 'center', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.24)', color: '#f0892a' } }, [
h('span', { key: 'i', style: { display: 'grid' }, dangerouslySetInnerHTML: { __html: PRES_SVG.user } }),
h('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: myMeta.dot, border: '2px solid #0c0c0e' } })
h('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: myMeta.dot, border: '2px solid #0c0c0e' } })
]),
h('div', { key: 'tx', style: { flex: 1, minWidth: 0, textAlign: 'left' } }, [
h('div', { key: 'tx', style: { flex: 1, minWidth: 0, textAlign: 'start' } }, [
h('div', { key: 'y', style: { fontSize: '13.5px', fontWeight: 700, color: '#f4f4f6' } }, t('chatHdr.you')),
h('div', { key: 'w', style: { fontSize: '12px', color: '#8a8a92' } }, myMeta.word)
]),
@@ -2865,15 +2873,15 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
])
]);
const presencePanelCollapsed = h('div', { key: 'you', style: { flex: 'none', position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '0 0 13px' } }, [
presenceMenu({ left: '60px', bottom: '8px', width: '248px' }),
presenceMenu({ insetInlineStart: '60px', bottom: '8px', width: '248px' }),
h('button', { key: 'btn', onClick: () => setPresenceOpen((v) => !v), title: t('chat.yourStatusPrefix') + myMeta.word, style: { position: 'relative', width: '44px', height: '44px', borderRadius: '12px', display: 'grid', placeItems: 'center', cursor: 'pointer', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.24)', color: '#f0892a' } }, [
h('span', { key: 'i', style: { display: 'grid' }, dangerouslySetInnerHTML: { __html: PRES_SVG.user } }),
h('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '12px', height: '12px', borderRadius: '50%', background: myMeta.dot, border: '2.5px solid #0c0c0e' } })
h('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '12px', height: '12px', borderRadius: '50%', background: myMeta.dot, border: '2.5px solid #0c0c0e' } })
])
]);
const expandedInner = [
h('div', { key: 'head', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 12px 0 16px', height: '64px', borderBottom: '1px solid rgba(255,255,255,0.06)' } }, [
h('div', { key: 'head', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingBlock: 0, paddingInlineStart: '16px', paddingInlineEnd: '12px', height: '64px', borderBottom: '1px solid rgba(255,255,255,0.06)' } }, [
h('div', { key: 'brand', style: { display: 'flex', alignItems: 'center', gap: '10px' } }, [brandMark(30), h('span', { key: 't', style: { fontSize: '15px', fontWeight: 800, letterSpacing: '-0.3px', color: '#f4f4f6' } }, 'SecureBit')]),
collapseBtn(SB_SVG.chevL, t('chat.collapse'))
]),
@@ -2903,7 +2911,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
onClick: () => onSelectGroup(g.id),
style: { position: 'relative', display: 'flex', alignItems: 'center', gap: '12px', padding: '11px 12px', marginBottom: '4px', borderRadius: '11px', background: g.active ? '#161618' : 'transparent', border: '1px solid ' + (g.active ? 'rgba(255,255,255,0.08)' : 'transparent'), cursor: 'pointer' }
}, [
g.active && h('span', { key: 'bar', style: { position: 'absolute', left: 0, top: '12px', bottom: '12px', width: '3px', borderRadius: '0 3px 3px 0', background: '#f0892a' } }),
g.active && h('span', { key: 'bar', style: { position: 'absolute', insetInlineStart: 0, top: '12px', bottom: '12px', width: '3px', borderStartEndRadius: '3px', borderEndEndRadius: '3px', background: '#f0892a' } }),
avatar(g, 38, g.active ? '#161618' : '#0c0c0e'),
h('div', { key: 'body', style: { flex: 1, minWidth: 0 } }, [
h('div', { key: 'top', style: { display: 'flex', alignItems: 'center', gap: '7px' } }, [
@@ -2928,14 +2936,14 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
...chats.map(dockItem),
h('div', { key: 'sep', style: { width: '30px', height: '1px', background: 'rgba(255,255,255,0.07)', margin: '2px 0' } }),
...groups.map((g) => h('div', { key: g.id, style: { position: 'relative' } }, [
g.active && h('span', { key: 'bar', style: { position: 'absolute', left: '-13px', top: '9px', bottom: '9px', width: '3px', borderRadius: '0 3px 3px 0', background: '#f0892a' } }),
g.active && h('span', { key: 'bar', style: { position: 'absolute', insetInlineStart: '-13px', top: '9px', bottom: '9px', width: '3px', borderStartEndRadius: '3px', borderEndEndRadius: '3px', background: '#f0892a' } }),
h('div', {
key: 'tile', onClick: () => onSelectGroup(g.id), title: g.name + ' — ' + g.headerSub,
style: { position: 'relative', width: '44px', height: '44px', borderRadius: '12px', display: 'grid', placeItems: 'center', cursor: 'pointer', background: g.active ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,' + (g.active ? '0.14' : '0.07') + ')', fontSize: '13px', fontWeight: 700, letterSpacing: '-0.3px', color: g.active ? '#f4f4f6' : '#9a9aa2' }
}, [
g.mono,
h('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: g.dot, border: '2.5px solid #0c0c0e' } }),
g.unread && h('span', { key: 'u', style: { position: 'absolute', left: '-5px', top: '-5px', minWidth: '17px', height: '17px', padding: '0 4px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: '#f0892a', color: '#1a0f04', fontFamily: "'JetBrains Mono',monospace", fontSize: '9.5px', fontWeight: 700, border: '2px solid #0c0c0e' } }, g.unread)
h('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: g.dot, border: '2.5px solid #0c0c0e' } }),
g.unread && h('span', { key: 'u', style: { position: 'absolute', insetInlineStart: '-5px', top: '-5px', minWidth: '17px', height: '17px', padding: '0 4px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: '#f0892a', color: '#1a0f04', fontFamily: "'JetBrains Mono',monospace", fontSize: '9.5px', fontWeight: 700, border: '2px solid #0c0c0e' } }, g.unread)
])
])),
h('div', { key: 'gph', onClick: onNewGroup, title: t('chat.newGroup'), style: { position: 'relative', width: '44px', height: '44px', borderRadius: '12px', display: 'grid', placeItems: 'center', cursor: 'pointer', background: 'transparent', border: '1px dashed rgba(255,255,255,0.1)', color: '#56565e' }, dangerouslySetInnerHTML: { __html: SB_SVG.users } })
@@ -2948,7 +2956,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
];
const railWidth = collapsed ? '72px' : '292px';
const railStyle = { flex: 'none', width: railWidth, display: 'flex', flexDirection: 'column', alignItems: collapsed ? 'center' : 'stretch', background: '#0c0c0e', borderRight: '1px solid rgba(255,255,255,0.06)' };
const railStyle = { flex: 'none', width: railWidth, display: 'flex', flexDirection: 'column', alignItems: collapsed ? 'center' : 'stretch', background: '#0c0c0e', borderInlineEnd: '1px solid rgba(255,255,255,0.06)' };
const inner = collapsed ? collapsedInner : expandedInner;
return h(React.Fragment, null, [
@@ -3030,11 +3038,11 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
onPointerUp: drawerUp,
onPointerCancel: drawerUp,
style: { position: 'fixed', inset: 0, zIndex: 60, background: 'rgba(6,6,8,0.6)', backdropFilter: 'blur(0px)', WebkitBackdropFilter: 'blur(0px)', opacity: 0, display: drawerMounted ? 'block' : 'none', touchAction: 'pan-y', willChange: 'opacity' }
}, h('aside', { className: 'sb-mobile-drawer', ref: panelRef, onClick: (e) => e.stopPropagation(), style: { position: 'absolute', left: 0, top: 0, bottom: 0, width: 'min(292px, 86vw)', display: 'flex', flexDirection: 'column', background: '#0c0c0e', borderRight: '1px solid rgba(255,255,255,0.06)', boxShadow: '0 0 60px rgba(0,0,0,0.6)', touchAction: 'pan-y', willChange: 'transform' } }, [
}, h('aside', { className: 'sb-mobile-drawer', ref: panelRef, onClick: (e) => e.stopPropagation(), style: { position: 'absolute', insetInlineStart: 0, top: 0, bottom: 0, width: 'min(292px, 86vw)', display: 'flex', flexDirection: 'column', background: '#0c0c0e', borderInlineEnd: '1px solid rgba(255,255,255,0.06)', boxShadow: '0 0 60px rgba(0,0,0,0.6)', touchAction: 'pan-y', willChange: 'transform' } }, [
// Explicit close button the drawer's own header only has a
// "collapse" chevron (a desktop-rail action), so on mobile there was
// no obvious way to dismiss it. This X closes the drawer reliably.
h('button', { key: 'x', onClick: onCloseDrawer, title: t('chat.closeMenu'), 'aria-label': t('chat.closeMenu'), style: { position: 'absolute', top: '15px', right: '13px', zIndex: 2, width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.05)', color: '#cfcfd4', cursor: 'pointer' } }, h('i', { className: 'fas fa-xmark', style: { fontSize: '16px' } })),
h('button', { key: 'x', onClick: onCloseDrawer, title: t('chat.closeMenu'), 'aria-label': t('chat.closeMenu'), style: { position: 'absolute', top: '15px', insetInlineEnd: '13px', zIndex: 2, width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.05)', color: '#cfcfd4', cursor: 'pointer' } }, h('i', { className: 'fas fa-xmark', style: { fontSize: '16px' } })),
expandedInner
]))
]);
@@ -6637,7 +6645,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
React.createElement('button', {
key: 'burger', className: 'sb-burger',
onClick: () => setSidebarDrawerOpen(true),
style: { display: 'none', position: 'fixed', top: '13px', left: '13px', zIndex: 55, width: '38px', height: '38px', borderRadius: '10px', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(18,18,20,0.9)', color: '#cfcfd4', cursor: 'pointer' },
style: { display: 'none', position: 'fixed', top: '13px', insetInlineStart: '13px', zIndex: 55, width: '38px', height: '38px', borderRadius: '10px', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(18,18,20,0.9)', color: '#cfcfd4', cursor: 'pointer' },
dangerouslySetInnerHTML: { __html: SB_SVG.burger }
}),
React.createElement('div', {
@@ -6737,7 +6745,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
key: 'sb-burger',
className: 'sb-burger',
onClick: () => setSidebarDrawerOpen(true),
style: { display: 'none', position: 'fixed', top: '13px', left: '13px', zIndex: 55, width: '38px', height: '38px', borderRadius: '10px', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(18,18,20,0.9)', color: '#cfcfd4', cursor: 'pointer' },
style: { display: 'none', position: 'fixed', top: '13px', insetInlineStart: '13px', zIndex: 55, width: '38px', height: '38px', borderRadius: '10px', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(18,18,20,0.9)', color: '#cfcfd4', cursor: 'pointer' },
dangerouslySetInnerHTML: { __html: SB_SVG.burger }
}),
React.createElement('div', {
@@ -6950,10 +6958,10 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
id: 'qr-reader',
style: { position: 'absolute', inset: 0, zIndex: 1 }
}),
corner('c-tl', { top: '18px', left: '18px', borderTop: '2.5px solid #3ecf8e', borderLeft: '2.5px solid #3ecf8e', borderRadius: '8px 0 0 0' }),
corner('c-tr', { top: '18px', right: '18px', borderTop: '2.5px solid #3ecf8e', borderRight: '2.5px solid #3ecf8e', borderRadius: '0 8px 0 0' }),
corner('c-bl', { bottom: '18px', left: '18px', borderBottom: '2.5px solid #3ecf8e', borderLeft: '2.5px solid #3ecf8e', borderRadius: '0 0 0 8px' }),
corner('c-br', { bottom: '18px', right: '18px', borderBottom: '2.5px solid #3ecf8e', borderRight: '2.5px solid #3ecf8e', borderRadius: '0 0 8px 0' }),
corner('c-tl', { top: '18px', insetInlineStart: '18px', borderTop: '2.5px solid #3ecf8e', borderInlineStart: '2.5px solid #3ecf8e', borderRadius: '8px 0 0 0' }),
corner('c-tr', { top: '18px', insetInlineEnd: '18px', borderTop: '2.5px solid #3ecf8e', borderInlineEnd: '2.5px solid #3ecf8e', borderRadius: '0 8px 0 0' }),
corner('c-bl', { bottom: '18px', insetInlineStart: '18px', borderBottom: '2.5px solid #3ecf8e', borderInlineStart: '2.5px solid #3ecf8e', borderRadius: '0 0 0 8px' }),
corner('c-br', { bottom: '18px', insetInlineEnd: '18px', borderBottom: '2.5px solid #3ecf8e', borderInlineEnd: '2.5px solid #3ecf8e', borderRadius: '0 0 8px 0' }),
React.createElement('span', {
key: 'scan-line',
style: { position: 'absolute', left: '18px', right: '18px', height: '2.5px', zIndex: 2, background: 'linear-gradient(90deg, transparent, #3ecf8e, transparent)', boxShadow: '0 0 16px #3ecf8e', animation: 'sbScan 1.5s ease-in-out infinite alternate' }
+1 -1
View File
@@ -461,7 +461,7 @@ const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
}, [
React.createElement('i', {
key: 'tips-icon',
className: 'fas fa-lightbulb mr-2'
className: 'fas fa-lightbulb me-2'
}),
'Tips for better scanning:'
]),
+1 -1
View File
@@ -183,7 +183,7 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
// 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: 'left' }
style: { borderRadius: '14px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,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')),
+3 -3
View File
@@ -47,9 +47,9 @@ const BecomePartner = () => {
}
];
const svg = (inner, size, stroke, sw) =>
const svg = (inner, size, stroke, sw, className) =>
React.createElement('svg', {
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
className, width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
stroke, strokeWidth: sw, strokeLinecap: 'round', strokeLinejoin: 'round',
dangerouslySetInnerHTML: { __html: inner }
});
@@ -123,7 +123,7 @@ const BecomePartner = () => {
}
}, [
t('partners.inviteCta'),
svg('<path d="M5 12h14M13 6l6 6-6 6"/>', 17, 'currentColor', 2.2)
svg('<path d="M5 12h14M13 6l6 6-6 6"/>', 17, 'currentColor', 2.2, 'sb-mirror-rtl')
])
]);
+3 -3
View File
@@ -177,12 +177,12 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
// ── MINIMIZED widget ─────────────────────────────────────────────────────
if (minimized) {
return h('div', { style: { position: 'absolute', bottom: '18px', right: '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: '#161618', border: '1px solid rgba(255,255,255,0.1)', boxShadow: '0 18px 44px rgba(0,0,0,0.55)', 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)),
h('span', { key: 's', style: { position: 'absolute', top: '8px', left: '9px', fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#fff', padding: '3px 7px', borderRadius: '6px', background: 'rgba(0,0,0,0.5)' } }, callStatus)
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)),
@@ -222,7 +222,7 @@ 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', right: '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(255,255,255,0.16)', boxShadow: '0 12px 30px rgba(0,0,0,0.5)', 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' } }, [
svg(ICON.camOff, 24, 1.8),
+6 -6
View File
@@ -180,7 +180,7 @@
{/* Mobile Alert */}
<div className="md:hidden p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg mb-4">
<p className="text-yellow-400 text-sm text-center">
<i className="fas fa-lightbulb mr-2"></i>
<i className="fas fa-lightbulb me-2"></i>
Rotate your device horizontally for better viewing
</p>
</div>
@@ -194,7 +194,7 @@
{/* Table Header */}
<thead>
<tr className="bg-black-table">
<th className="text-left p-4 border-b border-gray-600 text-white font-bold min-w-[240px]">
<th className="text-start p-4 border-b border-gray-600 text-white font-bold min-w-[240px]">
Security Criterion
</th>
{messengers.map((messenger, index) => (
@@ -282,20 +282,20 @@
{/* Legend */}
<div className="mt-8 grid grid-cols-2 md:grid-cols-4 gap-4 max-w-5xl mx-auto">
<div className="flex items-center justify-center p-4 bg-orange-500/10 rounded-xl hover:bg-orange-500/40 transition-colors">
<i className="fas fa-trophy text-orange-400 mr-2 text-xl"></i>
<i className="fas fa-trophy text-orange-400 me-2 text-xl"></i>
<span className="text-orange-300 text-sm font-bold">Category Leader</span>
</div>
<div className="flex items-center justify-center p-4 bg-green-500/10 rounded-xl hover:bg-green-600/40 transition-colors">
<i className="fas fa-check text-green-300 mr-2 text-xl"></i>
<i className="fas fa-check text-green-300 me-2 text-xl"></i>
<span className="text-green-200 text-sm font-bold">Excellent</span>
</div>
<div className="flex items-center justify-center p-4 bg-yellow-500/10 rounded-xl hover:bg-yellow-600/40 transition-colors">
<i className="fas fa-exclamation-triangle text-yellow-300 mr-2 text-xl"></i>
<i className="fas fa-exclamation-triangle text-yellow-300 me-2 text-xl"></i>
<span className="text-yellow-200 text-sm font-bold">Partial/Limited</span>
</div>
<div className="flex items-center justify-center p-4 bg-red-500/10 rounded-xl hover:bg-red-600/40 transition-colors">
<i className="fas fa-times text-red-300 mr-2 text-xl"></i>
<i className="fas fa-times text-red-300 me-2 text-xl"></i>
<span className="text-red-200 text-sm font-bold">Not Available</span>
</div>
</div>
+6 -6
View File
@@ -203,7 +203,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
}, [
React.createElement('i', {
key: 'icon',
className: 'fas fa-exclamation-triangle mr-2'
className: 'fas fa-exclamation-triangle me-2'
}),
'Соединение устанавливается... Передача файлов будет доступна после завершения установки.'
]);
@@ -333,7 +333,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
React.createElement('i', {
key: 'icon',
className: 'fas fa-arrow-up',
style: { color: '#f0892a', fontSize: '13px', marginRight: '8px' }
style: { color: '#f0892a', fontSize: '13px', marginInlineEnd: '8px' }
}),
React.createElement('span', {
key: 'name',
@@ -342,7 +342,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
}, transfer.fileName),
React.createElement('span', {
key: 'size',
className: "text-xs ml-2",
className: "text-xs ms-2",
style: { color: '#7b7b83' }
}, formatFileSize(transfer.fileSize))
]),
@@ -377,7 +377,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
React.createElement('i', {
key: 'icon',
className: 'fas fa-arrow-down',
style: { color: '#3ecf8e', fontSize: '13px', marginRight: '8px' }
style: { color: '#3ecf8e', fontSize: '13px', marginInlineEnd: '8px' }
}),
React.createElement('span', {
key: 'name',
@@ -386,7 +386,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
}, transfer.fileName),
React.createElement('span', {
key: 'size',
className: "text-xs ml-2",
className: "text-xs ms-2",
style: { color: '#7b7b83' }
}, formatFileSize(transfer.fileSize))
]),
@@ -408,7 +408,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
}
}
}, [
React.createElement('i', { key: 'i', className: 'fas fa-download mr-1' }),
React.createElement('i', { key: 'i', className: 'fas fa-download me-1' }),
t('file.download')
]) : null,
React.createElement('button', {
+10 -5
View File
@@ -167,9 +167,12 @@ export function GroupSasModal({ group, onConfirm, onCancel }) {
background: 'rgba(240,137,42,0.09)', border: '1px solid rgba(240,137,42,0.3)',
},
}, h('span', {
// Everyone in the group compares these digits against each other's
// screens, so their order must not depend on the reader's language.
dir: 'ltr',
style: {
fontFamily: C.mono, fontSize: 'clamp(30px, 9vw, 42px)', fontWeight: 700,
letterSpacing: '9px', color: C.accent,
letterSpacing: '9px', color: C.accent, unicodeBidi: 'isolate',
},
}, group.sasCode)),
@@ -277,7 +280,7 @@ export function CreateGroupModal({ candidates, relayOnly, onCreate, onCancel })
disabled: full,
style: {
display: 'flex', alignItems: 'center', gap: '11px', padding: '10px 12px',
borderRadius: '10px', cursor: full ? 'not-allowed' : 'pointer', textAlign: 'left',
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}`,
opacity: full ? 0.4 : 1, fontFamily: 'inherit',
@@ -405,7 +408,7 @@ export function AddMembersModal({ candidates, remaining, onAdd, onCancel }) {
disabled: full,
style: {
display: 'flex', alignItems: 'center', gap: '11px', padding: '10px 12px',
borderRadius: '10px', cursor: full ? 'not-allowed' : 'pointer', textAlign: 'left',
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}`,
opacity: full ? 0.4 : 1, fontFamily: 'inherit',
@@ -558,7 +561,7 @@ function Bubble({ msg }) {
}, [
!mine && h('span', {
key: 'who',
style: { fontSize: '11.5px', fontWeight: 600, color: C.accent, paddingLeft: '3px' },
style: { fontSize: '11.5px', fontWeight: 600, color: C.accent, paddingInlineStart: '3px' },
}, msg.senderName || t('group.member')),
h('div', {
key: 'b',
@@ -626,7 +629,9 @@ export function GroupChatView({
}, [
svg(ICON.users, { key: 'i', width: '13px', height: '13px' }),
t('group.membersCount', { count: group.members.length }),
ready && group.sasCode ? t('group.codeSuffix', { code: group.sasCode }) : '',
ready && group.sasCode
? h('span', { key: 'code', dir: 'ltr', style: { unicodeBidi: 'isolate' } }, t('group.codeSuffix', { code: group.sasCode }))
: '',
]),
]),
(isAdmin && onAddMembers && group.members.length < GROUP_LIMITS.MAX_MEMBERS) && h('button', {
+8 -7
View File
@@ -1,4 +1,4 @@
import { t } from '../../i18n/index.js';
import { t, direction } from '../../i18n/index.js';
// Advanced network settings: lets a user supply their own STUN/TURN servers
// instead of the bundled public defaults, and toggle relay-only privacy mode.
// Free / power-user feature, hidden behind an explicit "Advanced" entry point.
@@ -115,7 +115,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
const radioCard = (selected, onClick, title, desc, extraStyle) => h('button', {
type: 'button', onClick,
style: Object.assign({
width: '100%', textAlign: 'left', display: 'flex', alignItems: 'flex-start', gap: '12px',
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',
@@ -134,7 +134,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
const toggleRow = (on, onClick, title, desc, accent, badge) => h('button', {
type: 'button', onClick,
style: {
width: '100%', textAlign: 'left', display: 'flex', alignItems: 'flex-start', gap: '12px',
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',
@@ -149,7 +149,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', lineHeight: 1.5, color: '#8a8a92', marginTop: '3px' } }, desc)
]),
h('span', { key: 'tr', style: { flex: 'none', width: '42px', height: '24px', borderRadius: '99px', background: on ? (accent || C_GREEN) : 'rgba(255,255,255,0.08)', border: `1px solid ${on ? (accent || C_GREEN) : 'rgba(255,255,255,0.12)'}`, position: 'relative', transition: 'all .18s', marginTop: '1px' } },
h('span', { style: { position: 'absolute', top: '2px', left: '2px', width: '18px', height: '18px', borderRadius: '50%', background: '#fff', transform: on ? 'translateX(18px)' : 'translateX(0)', transition: 'transform .18s' } }))
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 ──
@@ -163,16 +163,17 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
const custom = [];
custom.push(h('div', { key: 'ta', style: { borderRadius: '13px', border: '1px solid rgba(255,255,255,0.08)', background: '#0c0c0e', overflow: 'hidden', marginBottom: '12px' } },
h('textarea', {
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' }
})));
if (parsed.errors.length > 0) {
custom.push(h('ul', { key: 'err', style: { margin: '0 0 10px', paddingLeft: '18px', color: '#e5727a', fontSize: '12.5px' } },
custom.push(h('ul', { key: 'err', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: '#e5727a', 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', paddingLeft: '18px', color: '#e3c84e', fontSize: '12.5px' } },
custom.push(h('ul', { key: 'warn', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: '#e3c84e', fontSize: '12.5px' } },
parsed.warnings.slice(0, 6).map((w, i) => h('li', { key: i }, w))));
}
if (parsed.servers.length > 0 && parsed.errors.length === 0) {
@@ -219,7 +220,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
const footerBtns = [];
if (hasSaved) {
footerBtns.push(h('button', { key: 'forget', type: 'button', onClick: handleForget,
style: { marginRight: 'auto', padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(229,114,122,0.3)', background: 'transparent', color: '#e5727a', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, t('ice.forget')));
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')));
}
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')));
+12 -2
View File
@@ -68,20 +68,30 @@ const LanguageSwitcher = () => {
const menu = React.createElement('div', {
key: 'menu',
role: 'menu',
// The same scrollbar the chat uses, rather than the browser's default slab —
// a 170px menu is exactly where a 15px stock scrollbar looks like a mistake.
className: 'sb-scroll',
// Kept in the DOM when shut: the links stay followable and the menu costs nothing
// to reopen.
style: {
position: 'absolute', top: 'calc(100% + 6px)', right: 0, zIndex: 60,
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)',
// 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.
maxHeight: 'min(62vh, 420px)', overflowY: 'auto', overscrollBehavior: 'contain',
},
}, links.map((link) => React.createElement('a', {
key: link.code,
href: link.href,
hrefLang: link.hrefLang,
lang: link.hrefLang,
// Each row is a word in its own script, so it gets its own direction — the menu
// around it stays in the direction of the page being read.
dir: link.dir,
role: 'menuitem',
// aria-current names the page you are on for a screen reader; the weight and
// contrast below say the same thing to everyone else.
@@ -101,7 +111,7 @@ const LanguageSwitcher = () => {
}, [
React.createElement('span', {
key: 'a',
style: { fontSize: '11px', fontWeight: 700, letterSpacing: '0.4px', color: '#6b6b73', width: '22px' },
style: { fontSize: '11px', fontWeight: 700, letterSpacing: '0.4px', color: '#6b6b73', width: '22px', flex: 'none', textAlign: 'start' },
}, link.abbr),
React.createElement('span', { key: 'n' }, link.label),
])));
+3 -3
View File
@@ -29,7 +29,7 @@ const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPasswor
}, [
React.createElement('div', {
key: 'icon',
className: 'w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3'
className: 'w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center me-3'
}, [
React.createElement('i', {
className: 'fas fa-key accent-purple'
@@ -68,7 +68,7 @@ const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPasswor
className: 'flex-1 btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
}, [
React.createElement('i', {
className: 'fas fa-unlock-alt mr-2'
className: 'fas fa-unlock-alt me-2'
}),
t('pw.decrypt')
]),
@@ -79,7 +79,7 @@ const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPasswor
className: 'flex-1 btn-secondary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
}, [
React.createElement('i', {
className: 'fas fa-times mr-2'
className: 'fas fa-times me-2'
}),
t('pw.cancel')
])
+6 -6
View File
@@ -75,28 +75,28 @@ function Roadmap() {
const renderNode = (status) => {
if (status === 'released') {
return (
<div style={{ position: 'absolute', left: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(62,207,142,0.16),rgba(62,207,142,0.16)), #0f0f11', border: '1px solid rgba(62,207,142,0.4)', zIndex: 2 }}>
<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>
);
}
if (status === 'current') {
return (
<div style={{ position: 'absolute', left: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(240,137,42,0.2),rgba(240,137,42,0.2)), #0f0f11', border: '1px solid #f0892a', zIndex: 2, animation: 'rmPulse 2.4s ease-out infinite' }}>
<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>
);
}
if (status === 'dev') {
return (
<div style={{ position: 'absolute', left: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'linear-gradient(rgba(227,179,65,0.15),rgba(227,179,65,0.15)), #0f0f11', border: '1px solid rgba(227,179,65,0.4)', zIndex: 2 }}>
<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>
);
}
// planned / research
return (
<div style={{ position: 'absolute', left: '13px', top: '16px', width: '28px', height: '28px', borderRadius: '50%', display: 'grid', placeItems: 'center', background: '#0f0f11', border: `1px ${status === 'research' ? 'dashed' : 'solid'} rgba(255,255,255,0.18)`, zIndex: 2 }}>
<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>
);
@@ -134,7 +134,7 @@ function Roadmap() {
{/* spine */}
<div style={{ position: 'relative' }}>
{notLast && <div style={{ position: 'absolute', left: '26px', top: '30px', height: 'calc(100% + 16px)', width: '2px', background: meta.line }} />}
{notLast && <div style={{ position: 'absolute', insetInlineStart: '26px', top: '30px', height: 'calc(100% + 16px)', width: '2px', background: meta.line }} />}
{renderNode(d.status)}
</div>
@@ -156,7 +156,7 @@ function Roadmap() {
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: meta.color }} />
{!isMobile && meta.word}
</span>
{!isMobile && <span style={{ fontFamily: MONO, fontSize: '12px', fontWeight: 500, color: '#8a8a92', whiteSpace: 'nowrap', minWidth: '74px', textAlign: 'right' }}>{d.date}</span>}
{!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)' }}>
<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>
+3 -3
View File
@@ -84,7 +84,7 @@ const Testimonials = () => {
<div key={`${t.id}-${index}`} className="card bg-neutral-900 rounded-xl p-5 shadow-md w-72 text-sm text-white flex-shrink-0">
<div className="flex items-center mb-2 text-yellow-400">
{"★".repeat(Math.floor(t.rating))}
<span className="ml-2 text-secondary">{t.rating.toFixed(1)}</span>
<span className="ms-2 text-secondary">{t.rating.toFixed(1)}</span>
</div>
<p className="text-secondary mb-3">{t.text}</p>
</div>
@@ -104,8 +104,8 @@ const Testimonials = () => {
</div>
<div className="lg:col-span-3 testimonials-wrapper flex gap-6 overflow-hidden relative h-[420px]">
<div className="pointer-events-none absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-[#1f1f1f]/90 to-transparent z-20"></div>
<div className="pointer-events-none absolute bottom-0 left-0 w-full h-16 bg-gradient-to-t from-[#1f1f1f]/90 to-transparent z-20"></div>
<div className="pointer-events-none absolute top-0 start-0 w-full h-16 bg-gradient-to-b from-[#1f1f1f]/90 to-transparent z-20"></div>
<div className="pointer-events-none absolute bottom-0 start-0 w-full h-16 bg-gradient-to-t from-[#1f1f1f]/90 to-transparent z-20"></div>
<div className="col-up flex flex-col gap-6">
{testimonials.map((t, i) => renderCard(t, i))}
+1 -1
View File
@@ -82,7 +82,7 @@ const UniqueFeatureSlider = () => {
const navBtn = (key, onClick, path) =>
React.createElement('button', {
key, onClick, 'aria-label': key,
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)',
+3025 -1
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -110,6 +110,44 @@ export function currentLocale() {
return resolvedLocale;
}
/**
* Writing direction of a locale. Arabic, Hebrew, Persian and Urdu read right to left,
* and the whole layout — not just the text — has to follow: an avatar that sits before
* a name in English sits after it in Arabic.
*
* The generated page already carries dir on <html>, so nothing here needs to apply it
* at load. This exists for the handful of decisions CSS cannot express on its own —
* which way an arrow points, which edge a drawer slides in from.
*/
export function localeDir(code = currentLocale()) {
return LOCALE_META[code]?.dir === 'rtl' ? 'rtl' : 'ltr';
}
export function isRTL(code = currentLocale()) {
return localeDir(code) === 'rtl';
}
/**
* +1 or -1, for arithmetic on a horizontal offset: a swipe threshold, a translate, the
* side a sheet enters from. `x * direction()` is the whole of what a mirrored gesture
* needs, and it reads better than an `isRTL ? -x : x` at every call site.
*/
export function direction(code = currentLocale()) {
return isRTL(code) ? -1 : 1;
}
/**
* Force a fragment to be laid out left to right inside right-to-left text.
*
* Bidi reordering is done by the browser on the rendered text, and it mangles exactly
* the strings this app is made of: a key fingerprint, a base64 session descriptor, a
* URL, a version number. In an RTL paragraph "a1b2:c3d4" can come out as "c3d4:a1b2" —
* the characters are all there, so nothing looks broken, and the reader compares the
* wrong thing against their peer's screen. Any element showing machine text gets these
* props.
*/
export const LTR_TEXT = { dir: 'ltr', style: { unicodeBidi: 'isolate', textAlign: 'start' } };
/**
* A locale the visitor would probably rather read, when it is not the one they are on.
* Used to offer a link, never to redirect: an automatic redirect sends Googlebot —
@@ -150,6 +188,9 @@ export function languageLinks({ pathname = '/', active = DEFAULT_LOCALE } = {})
label: LOCALE_META[code]?.nativeName || code,
// Short code for the collapsed switcher.
abbr: LOCALE_META[code]?.abbr || code.toUpperCase(),
// The row renders a name in its own script, so it needs its own direction:
// "العربية" laid out left-to-right is the word spelled backwards.
dir: LOCALE_META[code]?.dir || 'ltr',
isCurrent: code === active,
}));
}
+9 -9
View File
@@ -161,14 +161,14 @@ class PWAInstallPrompt {
this.installButton = document.createElement('div');
this.installButton.id = 'pwa-install-button';
this.installButton.className = 'hidden';
this.installButton.style.cssText = "position:fixed; bottom:24px; right:24px; z-index:50; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
this.installButton.style.cssText = "position:fixed; bottom:24px; inset-inline-end:24px; z-index:50; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
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; right:-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(255,255,255,0.1); background:#1a1a1d; color:#9a9aa2; 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:15px 26px 15px 22px; 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:#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);">
<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>
@@ -219,7 +219,7 @@ class PWAInstallPrompt {
</div>
<div class="flex items-center space-x-3">
<button class="install-btn bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded-lg font-medium transition-colors" data-action="install">
<i class="fas fa-download mr-2"></i>
<i class="fas fa-download me-2"></i>
${t('pwa.install')}
</button>
<button class="close-btn text-gray-400 hover:text-white px-3 py-2 rounded-lg transition-colors" data-action="close">
@@ -360,13 +360,13 @@ class PWAInstallPrompt {
</div>
<h3 class="text-xl font-semibold text-white mb-4">${t('pwa.iosTitle')}</h3>
<div class="space-y-4 text-left text-sm text-gray-300 mb-6">
<div class="space-y-4 text-start text-sm text-gray-300 mb-6">
<div class="flex items-start space-x-3">
<div class="w-8 h-8 bg-blue-500 rounded-full text-white flex items-center justify-center text-sm font-bold flex-shrink-0 mt-0.5">1</div>
<div class="flex-1">
<div class="font-medium text-white mb-1">${t('pwa.iosStep1')}</div>
<div class="flex items-center text-blue-400">
<i class="fas fa-share mr-2"></i>
<i class="fas fa-share me-2"></i>
<span>${t('pwa.iosStep1Hint')}</span>
</div>
</div>
@@ -391,7 +391,7 @@ class PWAInstallPrompt {
<div class="bg-orange-500/10 border border-orange-500/20 rounded-lg p-3 mb-4">
<p class="text-orange-300 text-xs">
<i class="fas fa-info-circle mr-1"></i>
<i class="fas fa-info-circle me-1"></i>
After installation, open SecureBit from your home screen for the best experience.
</p>
</div>
@@ -451,7 +451,7 @@ class PWAInstallPrompt {
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; right: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);">
<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);">
<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>
@@ -499,7 +499,7 @@ class PWAInstallPrompt {
showInstallSuccess() {
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform translate-x-full transition-transform duration-300';
notification.className = 'fixed top-4 end-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform translate-x-full transition-transform duration-300';
const successText = this.isIOSSafari() ?
t('pwa.installedIos') :
+15 -15
View File
@@ -258,7 +258,7 @@ class PWAOfflineManager {
`<div style="${PILL} border:1px solid rgba(227,179,65,0.32);">
<span style="width:8px; height:8px; border-radius:50%; background:#e3b341;"></span>
<span>${t('offline.mode')}</span>
<button class="oi-close" type="button" aria-label="${t('offline.dismiss')}" style="margin-left: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:#8a8a92; 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>`;
@@ -366,7 +366,7 @@ class PWAOfflineManager {
</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);">
${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" style="pointer-events:none;"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
<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>
</div>
</div>`;
@@ -375,7 +375,7 @@ class PWAOfflineManager {
<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);">
<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="M15 6l-6 6 6 6"/></svg>
<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>
</div>
@@ -713,7 +713,7 @@ class PWAOfflineManager {
showSyncNotification(successCount, errorCount) {
const notification = document.createElement('div');
notification.className = 'fixed bottom-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform translate-x-full transition-transform duration-300';
notification.className = 'fixed bottom-4 end-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform translate-x-full transition-transform duration-300';
let message = '';
if (successCount > 0 && errorCount === 0) {
@@ -796,7 +796,7 @@ class PWAOfflineManager {
showReconnectionSuccess() {
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm';
notification.className = 'fixed top-4 end-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm';
notification.innerHTML = `
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-lg"></i>
@@ -814,7 +814,7 @@ class PWAOfflineManager {
showReconnectionError(error) {
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-yellow-500 text-black p-4 rounded-lg shadow-lg z-50 max-w-sm';
notification.className = 'fixed top-4 end-4 bg-yellow-500 text-black p-4 rounded-lg shadow-lg z-50 max-w-sm';
notification.innerHTML = `
<div class="flex items-center space-x-3">
<i class="fas fa-exclamation-triangle text-lg"></i>
@@ -1125,7 +1125,7 @@ class PWAOfflineManager {
// Show fallback UI
const fallback = document.createElement('div');
fallback.className = 'fixed bottom-4 right-4 bg-red-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm';
fallback.className = 'fixed bottom-4 end-4 bg-red-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm';
fallback.innerHTML = `
<div class="flex items-start space-x-3">
<i class="fas fa-exclamation-triangle text-lg mt-0.5"></i>
@@ -1150,7 +1150,7 @@ class PWAOfflineManager {
helpModal.innerHTML = `
<div class="bg-gray-800 rounded-xl p-6 max-w-lg w-full max-h-[80vh] overflow-y-auto">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-blue-500/10 rounded-full flex items-center justify-center mr-4">
<div class="w-12 h-12 bg-blue-500/10 rounded-full flex items-center justify-center me-4">
<i class="fas fa-question-circle text-blue-400 text-xl"></i>
</div>
<h3 class="text-xl font-semibold text-white">Offline Mode Guide</h3>
@@ -1159,10 +1159,10 @@ class PWAOfflineManager {
<div class="space-y-6 text-gray-300 text-sm">
<div>
<h4 class="font-medium text-white mb-3 flex items-center">
<i class="fas fa-check-circle text-green-400 mr-2"></i>
<i class="fas fa-check-circle text-green-400 me-2"></i>
What works offline:
</h4>
<ul class="space-y-2 ml-6">
<ul class="space-y-2 ms-6">
<li>• App interface and navigation</li>
<li>• Previously cached resources</li>
<li>• Session data and keys (preserved in memory)</li>
@@ -1173,10 +1173,10 @@ class PWAOfflineManager {
<div>
<h4 class="font-medium text-white mb-3 flex items-center">
<i class="fas fa-times-circle text-red-400 mr-2"></i>
<i class="fas fa-times-circle text-red-400 me-2"></i>
What requires internet:
</h4>
<ul class="space-y-2 ml-6">
<ul class="space-y-2 ms-6">
<li>• P2P connections (WebRTC)</li>
<li>• Real-time messaging</li>
<li>• Session verification</li>
@@ -1186,10 +1186,10 @@ class PWAOfflineManager {
<div>
<h4 class="font-medium text-white mb-3 flex items-center">
<i class="fas fa-sync-alt text-blue-400 mr-2"></i>
<i class="fas fa-sync-alt text-blue-400 me-2"></i>
Automatic sync:
</h4>
<p class="ml-6">
<p class="ms-6">
When you're back online, all queued messages and actions
will be automatically synchronized. No data is lost.
</p>
@@ -1197,7 +1197,7 @@ class PWAOfflineManager {
<div class="bg-orange-500/10 border border-orange-500/20 rounded-lg p-4">
<h4 class="font-medium text-orange-300 mb-2 flex items-center">
<i class="fas fa-shield-alt mr-2"></i>
<i class="fas fa-shield-alt me-2"></i>
Security Notice
</h4>
<p class="text-orange-200 text-xs">
+1 -1
View File
@@ -70,7 +70,7 @@ window.showUpdateNotification = function showUpdateNotification() {
<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>
<div style="border-radius:14px; background:#0c0c0e; border:1px solid rgba(255,255,255,0.06); padding:16px 18px; margin-bottom:24px; text-align:left;">
<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="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>
+1 -1
View File
@@ -145,7 +145,7 @@ class PWAOfflineTester {
modal.innerHTML = `
<div class="bg-gray-800 rounded-xl p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-blue-500/10 rounded-full flex items-center justify-center mr-4">
<div class="w-12 h-12 bg-blue-500/10 rounded-full flex items-center justify-center me-4">
<i class="fas fa-vial text-blue-400 text-xl"></i>
</div>
<h3 class="text-xl font-semibold text-white">PWA Offline Test Results</h3>
+8 -2
View File
@@ -2,8 +2,14 @@
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('./sw.js', {
scope: './',
// Root-absolute, both of them. Every locale is its own page in its own
// subdirectory, so './sw.js' asks /ar/ for /ar/sw.js — a 404, and no Service
// Worker at all on twelve of the thirteen pages. There is one worker for the
// whole site and it needs the whole site as its scope, or an installed app
// that started at /ar/ would stop being offline-capable the moment it
// navigated to the root.
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
});
// Store registration for use in other modules
+15 -8
View File
@@ -31,7 +31,7 @@
/* Кнопка прокрутки на мобильном */
.scroll-to-bottom-mobile {
bottom: 140px !important;
right: 16px !important;
inset-inline-end: 16px !important;
width: 40px !important;
height: 40px !important;
}
@@ -102,7 +102,7 @@
.progress-text {
position: absolute;
top: -20px;
right: 0;
inset-inline-end: 0;
color: #9ca3af;
}
@@ -218,7 +218,7 @@ main {
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
margin-inline-end: 8px;
}
.status-connected { background: #10b981; }
.status-connecting { background: #6b7280; }
@@ -346,7 +346,7 @@ main {
/* Icon alignment in buttons */
button i {
vertical-align: middle;
margin-right: 0.5rem;
margin-inline-end: 0.5rem;
}
/* Lightning button */
@@ -736,7 +736,14 @@ button i {
/* ============================================================
SecureBit Chat redesigned chat surface (v4.8.21)
============================================================ */
.sb-scroll { scrollbar-gutter: auto; }
/* Firefox has no ::-webkit-scrollbar, so without these two it ignored everything below
and drew its default slab the chat and the language menu looked like different
applications depending on the browser. */
.sb-scroll {
scrollbar-gutter: auto;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.13) transparent;
}
.sb-scroll::-webkit-scrollbar { width: 9px; }
.sb-scroll::-webkit-scrollbar-track { background: transparent; }
.sb-scroll::-webkit-scrollbar-thumb {
@@ -790,7 +797,7 @@ button i {
.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-start-card { transition: transform .26s cubic-bezier(.3,.8,.3,1); }
@media (max-width: 900px) { .sb-start-left { border-right: none !important; } }
@media (max-width: 900px) { .sb-start-left { border-inline-end: none !important; } }
@media (max-width: 560px) { .sb-start-left { padding: 30px 22px !important; } }
/* PWA install pill belongs to the landing page only — hide it inside the chat. */
body.sb-in-chat #pwa-install-button { display: none !important; }
@@ -802,5 +809,5 @@ body.sb-in-chat #pwa-connection-status {
top: calc(64px + 12px + env(safe-area-inset-top, 0px)) !important;
}
/* The new design spaces icons with flex gap, not icon margins neutralise the
global `button i { margin-right: .5rem }` so icons stay centered in their tiles. */
.sb-start button i, .sb-ice-overlay button i { margin-right: 0; vertical-align: baseline; }
global `button i { margin-inline-end: .5rem }` so icons stay centered in their tiles. */
.sb-start button i, .sb-ice-overlay button i { margin-inline-end: 0; vertical-align: baseline; }
+3 -5
View File
@@ -111,8 +111,7 @@ tr.bg-black-table {
}
.header-minimal .max-w-7xl {
padding-left: 8px;
padding-right: 8px;
padding-inline: 8px;
}
.header-minimal button {
@@ -123,8 +122,7 @@ tr.bg-black-table {
@media (min-width: 641px) and (max-width: 1024px) {
.header-minimal .max-w-7xl {
padding-left: 16px;
padding-right: 16px;
padding-inline: 16px;
}
}
@@ -410,7 +408,7 @@ body {
writing-mode: horizontal-tb;
transform: none;
font-size: 1.2rem;
margin-right: auto;
margin-inline-end: auto;
}
.project-card__thumb,
+1 -1
View File
@@ -232,7 +232,7 @@
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
margin-inline-end: 16px;
}
.pwa-install-banner .text {
+100
View File
@@ -0,0 +1,100 @@
/* ============================================================================
Right-to-left support.
============================================================================
Almost nothing is here. The layout mirrors because the app was moved onto CSS
logical properties margin-inline-start instead of margin-left, inset-inline-end
instead of right and those follow dir on <html>, which build-i18n.js writes from
the locale file. That is the load-bearing change; this sheet is only for the
handful of things logical properties cannot express:
· glyphs that carry a direction, which have to be flipped rather than moved,
· text that must NOT mirror keys, descriptors, URLs, version numbers,
· Latin fonts that have no Arabic or Hebrew in them.
Loaded last so it wins over the utility classes above it. Every rule is scoped to
[dir="rtl"], so a left-to-right page is byte-for-byte unaffected.
========================================================================= */
/* Typography
Inter is subsetted to Latin only (deliberately it is self-hosted to keep
fonts.gstatic.com out of the request log, and the CSP has no room for a third
party anyway). Its unicode-range simply does not match Arabic or Hebrew, so the
browser falls through to the next family in the stack; naming that family here
is the difference between SF Arabic and whatever the fallback happens to be.
Nothing is downloaded for this. An Arabic webfont worth reading is 100 KB+ per
weight, and every platform that reads these scripts already ships a good one. */
[lang="ar"], [lang="fa"], [lang="ur"] {
font-family: 'Inter', 'SF Arabic', 'Geeza Pro', 'Segoe UI', Tahoma,
'Noto Naskh Arabic', 'Noto Sans Arabic', 'Droid Arabic Naskh', sans-serif;
}
/* Urdu is written in Nastaliq, which cascades diagonally and needs far more line
height than a Naskh face. Where no Nastaliq font is installed the text falls back
to Naskh, which Urdu readers read without trouble the extra leading is harmless
there and essential when Nastaliq does resolve. */
[lang="ur"] {
font-family: 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Urdu Typesetting',
'Inter', 'SF Arabic', 'Segoe UI', Tahoma, 'Noto Naskh Arabic', sans-serif;
line-height: 2;
}
[lang="he"] {
font-family: 'Inter', 'SF Hebrew', 'Arial Hebrew', 'Segoe UI', 'Noto Sans Hebrew', sans-serif;
}
/* Text that must not mirror
Bidi reordering is applied to rendered text, and it does not know that a session
descriptor is one atom. Inside an RTL paragraph the browser will happily print
"a1b2:c3d4" as "c3d4:a1b2": every character is present, nothing looks broken, and
the person comparing it against their peer's screen is comparing the wrong thing.
`direction: ltr` fixes the order; `unicode-bidi: isolate` stops the run from
dragging the surrounding sentence around with it. Both are needed. */
[dir="rtl"] code,
[dir="rtl"] pre,
[dir="rtl"] .sb-sc {
direction: ltr;
unicode-bidi: isolate;
text-align: start;
}
/* Digit shape is left to the font rather than forced here. Everything that has to be
read back character by character the safety code, a key, a byte count is already
set in the monospace stack, which carries no Arabic-Indic substitution, so those
digits stay Western without a rule. Prose is free to render its own numerals. */
/* Glyphs that carry a direction
A chevron pointing at the next item points the other way when "next" is the other
way. These are flipped, not repositioned the element is already in the right
place, it is the drawing inside it that is backwards.
Only genuinely directional icons are listed. fa-share (the iOS share sheet: a box
with an arrow leaving the top) and fa-download are vertical and stay put; mirroring
them would make them wrong rather than localized. */
[dir="rtl"] .fa-chevron-left,
[dir="rtl"] .fa-chevron-right,
[dir="rtl"] .fa-angle-left,
[dir="rtl"] .fa-angle-right,
[dir="rtl"] .fa-arrow-left,
[dir="rtl"] .fa-arrow-right,
[dir="rtl"] .fa-caret-left,
[dir="rtl"] .fa-caret-right,
[dir="rtl"] .fa-reply,
[dir="rtl"] .sb-mirror-rtl {
transform: scaleX(-1);
}
/* Utility classes assembled at runtime
Tailwind's translate utilities set a custom property that the transform reads, so
redefining the property is enough to send a toast in from the other edge. This has
to be done in CSS rather than by swapping classes: the toasts add and remove
`translate-x-full` by name to animate, and a second class would survive the
removal and pin them off-screen. */
[dir="rtl"] .translate-x-full { --tw-translate-x: -100%; }
[dir="rtl"] .-translate-x-full { --tw-translate-x: 100%; }
/* The mesh animation on the landing page moves its packet along an offset-path taken
from the SVG it travels over, so it mirrors with the drawing and needs nothing here.
Scrollbars, gradients and shadows are symmetric in this design; likewise nothing. */