feat(i18n): Arabic, Hebrew, Persian and Urdu, and a layout that mirrors; release v6.4.0
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:
+79
-71
@@ -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' }
|
||||
|
||||
@@ -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:'
|
||||
]),
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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')
|
||||
])
|
||||
]);
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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')));
|
||||
|
||||
@@ -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),
|
||||
])));
|
||||
|
||||
@@ -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')
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -232,7 +232,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
.pwa-install-banner .text {
|
||||
|
||||
@@ -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. */
|
||||
Reference in New Issue
Block a user