release: v4.9.1 — redesigned QR scanner, UI fixes, README overhaul
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled

UI / design
- Rework the camera scan modal to the new "Start Secure" design: green
  viewfinder with corner brackets, animated scan line, spinner + live
  frame counter, and a blurred dark backdrop. Keep the Html5Qrcode
  #qr-reader video feed, styled to fill the square viewfinder.
- Fix Advanced (network) settings: the fixed landing header (z-50) was
  covering the panel's close button — raise the embedded overlay to z-60.
- Stack the connection-screen footer buttons ("Download desktop app" /
  "Advanced settings") full-width on mobile and tablet instead of in a row.

Docs
- Rewrite README to follow GitHub best practices: capability-oriented
  Features, How it works, and Security model sections; move release notes
  out of the README and point to CHANGELOG.md. Keep logo and screenshots.

Chore
- Bump version to 4.9.1 (header, package.json, manifest) and rebuild bundles.
This commit is contained in:
lockbitchat
2026-06-24 15:41:15 -04:00
parent cbf5c5f834
commit 96fffab765
13 changed files with 350 additions and 278 deletions
+95 -64
View File
@@ -1180,7 +1180,7 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
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', 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', 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' } }, [
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' } }, 'Download SecureBit'),
@@ -1212,7 +1212,7 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
])
]);
const footer = h('div', { key: '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' } }, [
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' } }, [
fa('fa-download', { key: 'i', color: C_ORANGE }),
h('span', { key: 't', style: { fontSize: '12.5px', fontWeight: 700, color: '#e8e8eb' } }, 'Download desktop app'),
@@ -1300,6 +1300,7 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
'@keyframes sbSpin{to{transform:rotate(360deg)}}' +
'@keyframes sbUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}' +
'@keyframes sbNode{0%,100%{box-shadow:0 0 0 0 rgba(62,207,142,0)}50%{box-shadow:0 0 0 6px rgba(62,207,142,.06)}}' +
'@keyframes sbScan{0%{top:8%}100%{top:88%}}' +
'@keyframes sbBlink{0%,100%{opacity:1}50%{opacity:.35}}'
} });
@@ -4413,81 +4414,111 @@ import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/ic
})
),
// QR Scanner Modal
showQRScannerModal && React.createElement('div', {
key: 'qr-scanner-modal',
className: "fixed inset-0 bg-black/80 flex items-center justify-center z-50"
}, [
React.createElement('div', {
key: 'scanner-container',
className: "bg-gray-900 rounded-lg p-4 max-w-2xl w-full mx-4"
// QR Scanner Modal camera scan (design import: "Start Secure" / Camera scan modal)
showQRScannerModal && (() => {
const closeScanner = () => {
setShowQRScannerModal(false);
// Clear QR scanner buffer
qrChunksBufferRef.current = { id: null, total: 0, seen: new Set(), items: [] };
};
const buf = qrChunksBufferRef.current;
const hasParts = !!(buf && buf.id && buf.total > 1);
const framesText = hasParts
? `Scanning frames… ${buf.seen.size} / ${buf.total}`
: 'Scanning…';
const corner = (k, st) => React.createElement('span', {
key: k,
style: Object.assign({ position: 'absolute', width: '34px', height: '34px', zIndex: 3 }, st)
});
return React.createElement('div', {
key: 'qr-scanner-modal',
onClick: closeScanner,
style: { position: 'fixed', inset: 0, zIndex: 50, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px', background: 'rgba(6,6,8,0.82)', backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)', animation: 'sbUp .2s ease' }
}, [
React.createElement('div', {
key: 'scanner-header',
className: "flex items-center justify-between mb-4"
key: 'scanner-container',
onClick: (e) => e.stopPropagation(),
style: { width: '100%', maxWidth: '420px', borderRadius: '22px', border: '1px solid rgba(255,255,255,0.1)', background: '#111113', boxShadow: '0 30px 90px rgba(0,0,0,0.6)', overflow: 'hidden' }
}, [
React.createElement('h3', {
key: 'scanner-title',
className: "text-lg font-medium text-white"
}, 'QR Code Scanner'),
React.createElement('button', {
key: 'close-btn',
onClick: () => {
setShowQRScannerModal(false);
// Clear QR scanner buffer
qrChunksBufferRef.current = { id: null, total: 0, seen: new Set(), items: [] };
},
className: "text-gray-400 hover:text-white"
// Header
React.createElement('div', {
key: 'scanner-header',
style: { display: 'flex', alignItems: 'center', gap: '11px', padding: '18px 20px', borderBottom: '1px solid rgba(255,255,255,0.06)' }
}, [
React.createElement('i', {
key: 'close-icon',
className: 'fas fa-times'
})
])
]),
React.createElement('div', {
key: 'scanner-content',
className: "text-center"
}, [
React.createElement('p', {
key: 'scanner-description',
className: "text-gray-400 mb-4"
}, 'Point your camera at the QR code to scan'),
qrChunksBufferRef.current && qrChunksBufferRef.current.id && React.createElement('div', {
key: 'progress-indicator',
className: "mb-4 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg"
}, [
React.createElement('p', {
key: 'progress-text',
className: "text-blue-400 text-sm"
}, `Scanned: ${qrChunksBufferRef.current.seen.size}/${qrChunksBufferRef.current.total} parts`),
React.createElement('span', {
key: 'scanner-icon',
style: { display: 'flex' },
dangerouslySetInnerHTML: { __html: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#3ecf8e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8.5V6.5A2.5 2.5 0 0 1 4.5 4h2M17.5 4h2A2.5 2.5 0 0 1 22 6.5v2M22 15.5v2a2.5 2.5 0 0 1-2.5 2.5h-2M6.5 20h-2A2.5 2.5 0 0 1 2 17.5v-2"/><circle cx="12" cy="12" r="3.2"/></svg>' }
}),
React.createElement('div', {
key: 'progress-bar',
className: "w-full bg-gray-700 rounded-full h-2 mt-2"
key: 'scanner-titles',
style: { flex: 1, lineHeight: 1.2 }
}, [
React.createElement('div', {
key: 'progress-fill',
className: "bg-blue-500 h-2 rounded-full transition-all duration-300",
style: {
width: `${(qrChunksBufferRef.current.seen.size / qrChunksBufferRef.current.total) * 100}%`
}
})
])
key: 'scanner-title',
style: { fontSize: '15.5px', fontWeight: 800, color: '#f4f4f6' }
}, 'Scan QR code'),
React.createElement('div', {
key: 'scanner-hint',
style: { fontSize: '12px', color: '#7b7b83' }
}, "Point your camera at their QR")
]),
React.createElement('button', {
key: 'close-btn',
onClick: closeScanner,
style: { width: '32px', height: '32px', display: 'grid', placeItems: 'center', borderRadius: '9px', border: 'none', background: 'rgba(255,255,255,0.05)', color: '#9a9aa2', cursor: 'pointer' }
}, React.createElement('i', { className: 'fas fa-times' }))
]),
// Body
React.createElement('div', {
key: 'scanner-placeholder',
id: "qr-reader",
className: "w-full h-96 bg-gray-800 rounded-lg flex items-center justify-center",
style: { minHeight: '400px' }
key: 'scanner-body',
style: { padding: '22px 24px 24px' }
}, [
React.createElement('div', {
key: 'viewfinder',
style: { position: 'relative', aspectRatio: '1', borderRadius: '18px', overflow: 'hidden', background: '#000', border: '1px solid rgba(255,255,255,0.1)' }
}, [
React.createElement('div', {
key: 'vf-bg',
style: { position: 'absolute', inset: 0, background: 'radial-gradient(circle at 50% 45%, #1a1a1f, #000)' }
}),
// Camera video is injected here by Html5Qrcode
React.createElement('div', {
key: 'qr-reader',
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' }),
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' }
}),
React.createElement('div', {
key: 'scan-status',
style: { position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 3, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: '14px', background: 'linear-gradient(transparent, rgba(0,0,0,0.6))' }
}, [
React.createElement('span', {
key: 'spinner',
style: { display: 'flex', animation: 'sbSpin 1.4s linear infinite' },
dangerouslySetInnerHTML: { __html: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#3ecf8e" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-6.2-8.6"/></svg>' }
}),
React.createElement('span', {
key: 'scan-frames',
style: { fontSize: '12.5px', fontWeight: 600, color: '#cfcfd4' }
}, framesText)
])
]),
React.createElement('p', {
key: 'scanner-placeholder-text',
className: "text-gray-500"
}, 'Camera will start here...')
key: 'scanner-note',
style: { margin: '16px 0 0', textAlign: 'center', fontSize: '12px', lineHeight: 1.5, color: '#6b6b73' }
}, 'Hold steady until all parts are captured. Camera access is local — nothing is uploaded.')
])
])
])
]),
]);
})(),
]);
+1 -1
View File
@@ -559,7 +559,7 @@ const EnhancedMinimalHeader = ({
React.createElement('div', { key: 'txt', style: { lineHeight: 1.2, minWidth: 0 } }, [
React.createElement('div', { key: 'r1', style: { display: 'flex', alignItems: 'baseline', gap: '7px' } }, [
React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: '#e8e8eb' } }, 'SecureBit'),
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, 'v4.9.0')
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, 'v4.9.1')
]),
React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: '#6b6b73', fontWeight: 500 } }, 'End-to-end encrypted')
])
+1 -1
View File
@@ -230,7 +230,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
// Embedded mode (default for the new design): fill the connection screen's
// right column and slide up over it. Fallback: a fixed right-side drawer.
const wrapperStyle = embedded
? { position: 'absolute', inset: 0, zIndex: 30, display: 'flex', flexDirection: 'column', background: '#0f0f11', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' }
? { position: 'absolute', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', background: '#0f0f11', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' }
: { position: 'fixed', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', alignItems: 'stretch', background: '#0f0f11', animation: 'sbSlideUp .32s cubic-bezier(.2,.7,.3,1)' };
return h('div', { className: 'sb-ice-overlay', style: wrapperStyle }, [
+15
View File
@@ -765,7 +765,22 @@ button i {
@keyframes sbSpin { to { transform: rotate(360deg); } }
@keyframes sbUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes sbNode { 0%,100% { box-shadow: 0 0 0 0 rgba(62,207,142,0.0); } 50% { box-shadow: 0 0 0 6px rgba(62,207,142,0.06); } }
@keyframes sbScan { 0% { top: 8%; } 100% { top: 88%; } }
@keyframes sbSlideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
/* Camera scan modal — make the Html5Qrcode-injected video fill the square viewfinder */
#qr-reader { width: 100%; height: 100%; border: none !important; }
#qr-reader video { width: 100% !important; height: 100% !important; object-fit: cover !important; display: block; }
#qr-reader img, #qr-reader__dashboard, #qr-reader__status_span { display: none !important; }
/* Connection screen footer — stack "Download desktop app" / "Advanced settings"
full-width on mobile and tablet instead of side-by-side. Inline styles win on
specificity, so the responsive overrides are marked !important. */
@media (max-width: 1024px) {
.sb-conn-footer { flex-direction: column !important; align-items: stretch !important; }
.sb-conn-footer > button { width: 100% !important; justify-content: center !important; }
.sb-conn-footer > .sb-platforms-menu { width: 100% !important; }
}
.sb-start textarea::placeholder, .sb-start input::placeholder { color: #56565e; }
.sb-start .sb-sc::-webkit-scrollbar { width: 8px; }
.sb-start .sb-sc::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 99px; }