diff --git a/CHANGELOG.md b/CHANGELOG.md index 01d676f..9149244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## v4.9.0 — Full redesign + reworked offline mode + +A ground-up visual redesign of the whole application surface — landing page, "Why unique" / partners / roadmap / community sections, connection setup, in-chat header, real-time security verification report, file transfer, and the PWA install / update / offline / install-guide dialogs. + +Offline experience reworked with store-and-forward over the live P2P channel: + +- Messages sent while offline are queued (single ✓) and transmitted on reconnect, preserving their original send time. +- Messages to an offline peer stay at one check until that peer returns; the offline client holds them back and surfaces them on reconnect with a notice. +- WhatsApp-style per-message delivery status (sending → sent → delivered, plus a "not sent" state) via an authenticated delivery-receipt control message. +- Browser offline state no longer leaks into the P2P connection indicator. + +Resilient file transfer: per-chunk segmented progress, receiver-driven retransmission of missing chunks with auto-resume after a connection blip, corrected receive rate limits, and automatic save on completion. + +## v4.8.21 — Redesigned chat surface + +A full visual refresh of the connected chat experience, ported from the SecureBit Chat design. No protocol, crypto or message-handling changes — only the presentation layer of the chat screen. + +### Changed + +- **Message bubbles** redesigned: tighter dark surface (`#0f0f11` canvas, `#26262b` sent / `#161618` received), asymmetric corner radii, monospace timestamps, and a compact per-message status row showing **Encrypted** / **Decrypted** with a lock glyph. +- **View-once** now uses a Telegram-style blurred cover with an SVG grain overlay and a centered "View once · tap to reveal" prompt; after reveal it shows a "Viewed once" tag and still burns after the sender-chosen window. +- **Disappearing timers** render a live `mm:ss` countdown in the message meta in brand orange. +- **Composer** rebuilt: inline `Send files` / `Code` / `View once` / `Timer` chips with active states, inline time-picker rows (view-once: Off/5s/10s/30s/1m, timer: Off/5s/30s/1m/1h/24h), an auto-growing message field, an "Encrypted on your device" affordance, a live character counter, and an orange send button. +- **Handshake summary** card at the top of a verified chat (collapsible): transport / cipher / key-exchange / integrity facts plus the safety number (key fingerprint). +- Fonts are mapped to the self-hosted **Inter** + system monospace stack rather than loading Google Fonts, preserving the look without an external request from a privacy-focused client. + ## v4.8.20 — Secure chat tools: completed, fixed and polished Completes the messaging controls introduced in v4.8.14 and fixes the bug that made them appear broken for recipients. All per-message options travel inside the encrypted message envelope (never in the sanitized text), so message content cannot spoof or corrupt them. diff --git a/README.md b/README.md index 6612305..1a69b29 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,37 @@ -# SecureBit.chat v4.8.20 +
+ +SecureBit.chat + +# SecureBit.chat + +**End-to-end encrypted, peer-to-peer chat — fully redesigned in v4.9.0** + +ECDH P-384 · AES-256-GCM · DTLS · SAS verification · no accounts, no servers + +
SecureBit.chat is a browser-based peer-to-peer chat application built on WebRTC and Web Crypto APIs. It is designed for direct encrypted communication, explicit peer verification, and a small operational footprint without account registration or server-side message storage. +## Screenshots + +**Open a secure channel** + +![SecureBit.chat — open a channel](assets/screenshots/login.png) + +**Encrypted conversation** + +![SecureBit.chat — encrypted chat](assets/screenshots/chat.png) + +## What's new in v4.9.0 — full redesign + +A ground-up visual redesign of the entire surface — landing page, connection setup, in-chat header, security verification report, file transfer, and the PWA install / update / offline dialogs — together with a reworked offline experience and WhatsApp-style delivery status. + +**Offline mode, reworked.** SecureBit now does proper store-and-forward over the live P2P channel: + +- Sending while offline shows a single ✓ and **queues** the message; it transmits automatically once you reconnect, keeping its original send time. +- A message addressed to an **offline peer** stays at one check until they come back — their client holds it back (not shown, not acknowledged) and surfaces it on reconnect with a "Connection restored" notice. +- **Delivery status on every message:** sending → ✓ sent → ✓✓ delivered (peer-acknowledged), with a clear "not sent" state — and offline status no longer leaks into the connection indicator. + ## Security model SecureBit.chat uses: diff --git a/SECURITY_DISCLAIMER.md b/SECURITY_DISCLAIMER.md index 36c9a95..65d553b 100644 --- a/SECURITY_DISCLAIMER.md +++ b/SECURITY_DISCLAIMER.md @@ -22,6 +22,6 @@ SecureBit.chat is intended for legitimate private communication, journalism, res ## Current release -- Product release: `v4.8.20` +- Product release: `v4.8.21` - Protocol version: `4.1` - Last updated: May 17, 2026 diff --git a/assets/screenshots/chat.png b/assets/screenshots/chat.png new file mode 100644 index 0000000..38d6843 Binary files /dev/null and b/assets/screenshots/chat.png differ diff --git a/assets/screenshots/login.png b/assets/screenshots/login.png new file mode 100644 index 0000000..ca35d88 Binary files /dev/null and b/assets/screenshots/login.png differ diff --git a/assets/tailwind.css b/assets/tailwind.css index 5ae1de4..d69e0fa 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -1 +1 @@ -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.right-3{right:.75rem}.right-4{right:1rem}.right-6{right:1.5rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.z-\[9999\]{z-index:9999}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem}.mb-1,.my-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mr-auto{margin-right:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1{height:.25rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-28{height:7rem}.h-36{height:9rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-80{height:20rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-\[420px\]{height:420px}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.max-h-\[80vh\]{max-height:80vh}.max-h-\[90vh\]{max-height:90vh}.min-h-\[72px\]{min-height:72px}.min-h-\[calc\(100vh-104px\)\]{min-height:calc(100vh - 104px)}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-28{width:7rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-72{width:18rem}.w-8{width:2rem}.w-\[20rem\]{width:20rem}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[160px\]{min-width:160px}.min-w-\[180px\]{min-width:180px}.min-w-\[240px\]{min-width:240px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[78vw\]{max-width:78vw}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.translate-x-full{--tw-translate-x:100%}.translate-x-full,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.rotate-180{--tw-rotate:180deg}.rotate-180,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-500\/20{border-color:rgba(59,130,246,.2)}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-500\/10{border-color:hsla(220,9%,46%,.1)}.border-gray-500\/20{border-color:hsla(220,9%,46%,.2)}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-gray-600\/30{border-color:rgba(75,85,99,.3)}.border-gray-700\/30{border-color:rgba(55,65,81,.3)}.border-green-500\/20{border-color:rgba(34,197,94,.2)}.border-green-500\/30{border-color:rgba(34,197,94,.3)}.border-orange-500\/20{border-color:rgba(249,115,22,.2)}.border-orange-500\/40{border-color:rgba(249,115,22,.4)}.border-purple-500\/20{border-color:rgba(168,85,247,.2)}.border-purple-500\/30{border-color:rgba(168,85,247,.3)}.border-red-500\/20{border-color:rgba(239,68,68,.2)}.border-red-500\/30{border-color:rgba(239,68,68,.3)}.border-white\/10{border-color:hsla(0,0%,100%,.1)}.border-yellow-500\/20{border-color:rgba(234,179,8,.2)}.border-yellow-500\/30{border-color:rgba(234,179,8,.3)}.bg-\[rgb\(20_20_20_\/50\%\)\]{background-color:hsla(0,0%,8%,.5)}.bg-black\/20{background-color:rgba(0,0,0,.2)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-500\/20{background-color:rgba(59,130,246,.2)}.bg-blue-500\/90{background-color:rgba(59,130,246,.9)}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-gray-500\/10{background-color:hsla(220,9%,46%,.1)}.bg-gray-500\/20{background-color:hsla(220,9%,46%,.2)}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.bg-gray-600\/20{background-color:rgba(75,85,99,.2)}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.bg-gray-700\/50{background-color:rgba(55,65,81,.5)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.bg-gray-800\/95{background-color:rgba(31,41,55,.95)}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-gray-900\/30{background-color:rgba(17,24,39,.3)}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(187 247 208/var(--tw-bg-opacity,1))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity,1))}.bg-green-400\/20{background-color:rgba(74,222,128,.2)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:rgba(34,197,94,.1)}.bg-green-500\/20{background-color:rgba(34,197,94,.2)}.bg-green-500\/90{background-color:rgba(34,197,94,.9)}.bg-green-600\/20{background-color:rgba(22,163,74,.2)}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity,1))}.bg-orange-400{--tw-bg-opacity:1;background-color:rgb(251 146 60/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-orange-500\/10{background-color:rgba(249,115,22,.1)}.bg-orange-500\/20{background-color:rgba(249,115,22,.2)}.bg-orange-500\/5{background-color:rgba(249,115,22,.05)}.bg-purple-500\/10{background-color:rgba(168,85,247,.1)}.bg-purple-500\/20{background-color:rgba(168,85,247,.2)}.bg-purple-600{--tw-bg-opacity:1;background-color:rgb(147 51 234/var(--tw-bg-opacity,1))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(254 202 202/var(--tw-bg-opacity,1))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/20{background-color:rgba(239,68,68,.2)}.bg-red-500\/90{background-color:rgba(239,68,68,.9)}.bg-red-900\/50{background-color:rgba(127,29,29,.5)}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/20{background-color:hsla(0,0%,100%,.2)}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity,1))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.bg-yellow-500\/20{background-color:rgba(234,179,8,.2)}.bg-yellow-500\/90{background-color:rgba(234,179,8,.9)}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-\[\#1f1f1f\]\/90{--tw-gradient-from:rgba(31,31,31,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,31,31,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gray-800\/20{--tw-gradient-from:rgba(31,41,55,.2) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,41,55,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-500{--tw-gradient-from:#f97316 var(--tw-gradient-from-position);--tw-gradient-to:rgba(249,115,22,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:transparent var(--tw-gradient-from-position);--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-zinc-700{--tw-gradient-to:rgba(63,63,70,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#3f3f46 var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-gray-900\/20{--tw-gradient-to:rgba(17,24,39,.2) var(--tw-gradient-to-position)}.to-orange-600{--tw-gradient-to:#ea580c var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.fill-blue-500{fill:#3b82f6}.fill-white{fill:#fff}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pl-5{padding-left:1.25rem}.pr-2{padding-right:.5rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[3rem\]{font-size:3rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.italic{font-style:italic}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.3em\]{letter-spacing:.3em}.tracking-wide{letter-spacing:.025em}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity,1))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-green-200{--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}.text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-orange-200{--tw-text-opacity:1;color:rgb(254 215 170/var(--tw-text-opacity,1))}.text-orange-300{--tw-text-opacity:1;color:rgb(253 186 116/var(--tw-text-opacity,1))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity,1))}.text-orange-400\/80{color:rgba(251,146,60,.8)}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity,1))}.text-purple-300{--tw-text-opacity:1;color:rgb(216 180 254/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-purple-500{--tw-text-opacity:1;color:rgb(168 85 247/var(--tw-text-opacity,1))}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-sky-300{--tw-text-opacity:1;color:rgb(125 211 252/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/10{color:hsla(0,0%,100%,.1)}.text-yellow-200{--tw-text-opacity:1;color:rgb(254 240 138/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.opacity-0{opacity:0}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.grayscale{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.grayscale{--tw-grayscale:grayscale(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-transform{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[checksum\:4\]{checksum:4}.\[data\:variable\]{data:variable}.\[name\:4\]{name:4}.\[size\:4\]{size:4}.hover\:bg-\[rgb\(20_20_20_\/30\%\)\]:hover{background-color:hsla(0,0%,8%,.3)}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.hover\:bg-emerald-600:hover{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.hover\:bg-gray-500:hover{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.hover\:bg-gray-600\/30:hover{background-color:rgba(75,85,99,.3)}.hover\:bg-gray-700\/40:hover{background-color:rgba(55,65,81,.4)}.hover\:bg-gray-700\/50:hover{background-color:rgba(55,65,81,.5)}.hover\:bg-green-500\/20:hover{background-color:rgba(34,197,94,.2)}.hover\:bg-green-500\/30:hover{background-color:rgba(34,197,94,.3)}.hover\:bg-green-600\/30:hover{background-color:rgba(22,163,74,.3)}.hover\:bg-green-600\/40:hover{background-color:rgba(22,163,74,.4)}.hover\:bg-orange-500\/20:hover{background-color:rgba(249,115,22,.2)}.hover\:bg-orange-500\/40:hover{background-color:rgba(249,115,22,.4)}.hover\:bg-orange-600:hover{--tw-bg-opacity:1;background-color:rgb(234 88 12/var(--tw-bg-opacity,1))}.hover\:bg-purple-500:hover{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity,1))}.hover\:bg-purple-500\/20:hover{background-color:rgba(168,85,247,.2)}.hover\:bg-red-500\/20:hover{background-color:rgba(239,68,68,.2)}.hover\:bg-red-500\/30:hover{background-color:rgba(239,68,68,.3)}.hover\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.hover\:bg-red-600\/40:hover{background-color:rgba(220,38,38,.4)}.hover\:bg-white\/30:hover{background-color:hsla(0,0%,100%,.3)}.hover\:bg-white\/5:hover{background-color:hsla(0,0%,100%,.05)}.hover\:bg-yellow-600\/40:hover{background-color:rgba(202,138,4,.4)}.hover\:from-orange-600:hover{--tw-gradient-from:#ea580c var(--tw-gradient-from-position);--tw-gradient-to:rgba(234,88,12,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:to-orange-700:hover{--tw-gradient-to:#c2410c var(--tw-gradient-to-position)}.hover\:text-gray-200:hover{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.hover\:text-green-300:hover{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.hover\:text-green-400:hover{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-red-400:hover{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:opacity-100:hover{opacity:1}.hover\:opacity-80:hover{opacity:.8}.focus\:border-green-500\/40:focus{border-color:rgba(34,197,94,.4)}.focus\:border-orange-500\/40:focus{border-color:rgba(249,115,22,.4)}.focus\:border-orange-500\/60:focus{border-color:rgba(249,115,22,.6)}.focus\:border-purple-400:focus{--tw-border-opacity:1;border-color:rgb(192 132 252/var(--tw-border-opacity,1))}.focus\:border-purple-500\/40:focus{border-color:rgba(168,85,247,.4)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-gray-700\/60:active{background-color:rgba(55,65,81,.6)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:scale-105,.group:hover .group-hover\:scale-110{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:mb-0{margin-bottom:0}.sm\:mb-3{margin-bottom:.75rem}.sm\:mr-2{margin-right:.5rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:h-10{height:2.5rem}.sm\:h-12{height:3rem}.sm\:h-16{height:4rem}.sm\:w-10{width:2.5rem}.sm\:w-12{width:3rem}.sm\:w-\[24rem\]{width:24rem}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:1rem}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.sm\:p-4{padding:1rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-3{padding-left:.75rem;padding-right:.75rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.sm\:py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-32{height:8rem}.md\:h-\[32rem\]{height:32rem}.md\:w-\[28rem\]{width:28rem}.md\:w-auto{width:auto}.md\:w-px{width:1px}.md\:flex-none{flex:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:block{display:block}.lg\:w-\[32rem\]{width:32rem}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity,1))}.dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.dark\:text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.dark\:hover\:text-white:hover,.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}} \ No newline at end of file +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem}.mb-1,.my-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-28{height:7rem}.h-8{height:2rem}.h-80{height:20rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-\[420px\]{height:420px}.max-h-\[80vh\]{max-height:80vh}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-28{width:7rem}.w-4{width:1rem}.w-72{width:18rem}.w-8{width:2rem}.w-full{width:100%}.min-w-\[160px\]{min-width:160px}.min-w-\[180px\]{min-width:180px}.min-w-\[240px\]{min-width:240px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[78vw\]{max-width:78vw}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.translate-x-full{--tw-translate-x:100%}.translate-x-full,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-500\/20{border-color:rgba(59,130,246,.2)}.border-gray-500\/20{border-color:hsla(220,9%,46%,.2)}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-gray-600\/30{border-color:rgba(75,85,99,.3)}.border-gray-700\/30{border-color:rgba(55,65,81,.3)}.border-green-500\/20{border-color:rgba(34,197,94,.2)}.border-orange-500\/20{border-color:rgba(249,115,22,.2)}.border-purple-500\/20{border-color:rgba(168,85,247,.2)}.border-purple-500\/30{border-color:rgba(168,85,247,.3)}.border-red-500\/20{border-color:rgba(239,68,68,.2)}.border-yellow-500\/20{border-color:rgba(234,179,8,.2)}.bg-\[rgb\(20_20_20_\/50\%\)\]{background-color:hsla(0,0%,8%,.5)}.bg-black\/20{background-color:rgba(0,0,0,.2)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-500\/90{background-color:rgba(59,130,246,.9)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-gray-500\/10{background-color:hsla(220,9%,46%,.1)}.bg-gray-500\/20{background-color:hsla(220,9%,46%,.2)}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.bg-gray-700\/50{background-color:rgba(55,65,81,.5)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.bg-gray-800\/95{background-color:rgba(31,41,55,.95)}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-gray-900\/30{background-color:rgba(17,24,39,.3)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:rgba(34,197,94,.1)}.bg-green-500\/90{background-color:rgba(34,197,94,.9)}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-orange-500\/10{background-color:rgba(249,115,22,.1)}.bg-purple-500\/10{background-color:rgba(168,85,247,.1)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/20{background-color:rgba(239,68,68,.2)}.bg-red-900\/50{background-color:rgba(127,29,29,.5)}.bg-transparent{background-color:transparent}.bg-white\/20{background-color:hsla(0,0%,100%,.2)}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.bg-yellow-500\/90{background-color:rgba(234,179,8,.9)}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-\[\#1f1f1f\]\/90{--tw-gradient-from:rgba(31,31,31,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,31,31,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gray-800\/20{--tw-gradient-from:rgba(31,41,55,.2) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,41,55,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-gray-900\/20{--tw-gradient-to:rgba(17,24,39,.2) var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.fill-blue-500{fill:#3b82f6}.fill-white{fill:#fff}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[3rem\]{font-size:3rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.italic{font-style:italic}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.3em\]{letter-spacing:.3em}.tracking-wide{letter-spacing:.025em}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity,1))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-green-200{--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}.text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-orange-200{--tw-text-opacity:1;color:rgb(254 215 170/var(--tw-text-opacity,1))}.text-orange-300{--tw-text-opacity:1;color:rgb(253 186 116/var(--tw-text-opacity,1))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity,1))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity,1))}.text-purple-300{--tw-text-opacity:1;color:rgb(216 180 254/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-sky-300{--tw-text-opacity:1;color:rgb(125 211 252/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/10{color:hsla(0,0%,100%,.1)}.text-yellow-200{--tw-text-opacity:1;color:rgb(254 240 138/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.opacity-0{opacity:0}.opacity-60{opacity:.6}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-transform{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[checksum\:4\]{checksum:4}.\[data\:variable\]{data:variable}.\[name\:4\]{name:4}.\[size\:4\]{size:4}.hover\:bg-\[rgb\(20_20_20_\/30\%\)\]:hover{background-color:hsla(0,0%,8%,.3)}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.hover\:bg-emerald-600:hover{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.hover\:bg-gray-500:hover{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.hover\:bg-gray-700\/40:hover{background-color:rgba(55,65,81,.4)}.hover\:bg-gray-700\/50:hover{background-color:rgba(55,65,81,.5)}.hover\:bg-green-600\/40:hover{background-color:rgba(22,163,74,.4)}.hover\:bg-orange-500\/40:hover{background-color:rgba(249,115,22,.4)}.hover\:bg-orange-600:hover{--tw-bg-opacity:1;background-color:rgb(234 88 12/var(--tw-bg-opacity,1))}.hover\:bg-red-500\/20:hover{background-color:rgba(239,68,68,.2)}.hover\:bg-red-500\/30:hover{background-color:rgba(239,68,68,.3)}.hover\:bg-red-600\/40:hover{background-color:rgba(220,38,38,.4)}.hover\:bg-white\/30:hover{background-color:hsla(0,0%,100%,.3)}.hover\:bg-yellow-600\/40:hover{background-color:rgba(202,138,4,.4)}.hover\:text-gray-200:hover{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity,1))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-green-300:hover{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.hover\:text-green-400:hover{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.focus\:border-purple-400:focus{--tw-border-opacity:1;border-color:rgb(192 132 252/var(--tw-border-opacity,1))}.focus\:border-purple-500\/40:focus{border-color:rgba(168,85,247,.4)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-gray-700\/60:active{background-color:rgba(55,65,81,.6)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-60:disabled{opacity:.6}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:mb-3{margin-bottom:.75rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:h-12{height:3rem}.sm\:w-12{width:3rem}.sm\:gap-4{gap:1rem}.sm\:p-4{padding:1rem}.sm\:py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:hidden{display:none}.md\:h-\[32rem\]{height:32rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}} \ No newline at end of file diff --git a/dist/app-boot.js b/dist/app-boot.js index 3ddfece..e996d30 100644 --- a/dist/app-boot.js +++ b/dist/app-boot.js @@ -4496,9 +4496,9 @@ var EnhancedSecureFileTransfer = class { this.transferQueue = []; this.pendingChunks = /* @__PURE__ */ new Map(); this.incomingOfferLimiter = new RateLimiter(5, 6e4); - this.incomingChunkLimiter = new RateLimiter(240, 6e4); + this.incomingChunkLimiter = new RateLimiter(6e4, 6e4); this.incomingTransferChunkLimiters = /* @__PURE__ */ new Map(); - this.MAX_INCOMING_CHUNKS_PER_TRANSFER_PER_MINUTE = 120; + this.MAX_INCOMING_CHUNKS_PER_TRANSFER_PER_MINUTE = 3e4; this.MAX_PENDING_INCOMING_TRANSFERS = 3; this.sessionKeys = /* @__PURE__ */ new Map(); this.processedChunks = /* @__PURE__ */ new Set(); @@ -4739,6 +4739,7 @@ var EnhancedSecureFileTransfer = class { "file_transfer_response", "file_chunk", "chunk_confirmation", + "file_chunk_request", "file_transfer_complete", "file_transfer_error" ]; @@ -4789,6 +4790,9 @@ var EnhancedSecureFileTransfer = class { case "chunk_confirmation": this.handleChunkConfirmation(message); break; + case "file_chunk_request": + await this.handleChunkRequest(message); + break; case "file_transfer_complete": this.handleTransferComplete(message); break; @@ -5011,14 +5015,7 @@ var EnhancedSecureFileTransfer = class { await this.waitForBackpressure(); } transferState.status = "waiting_confirmation"; - setTimeout(() => { - if (this.activeTransfers.has(transferState.fileId)) { - const state = this.activeTransfers.get(transferState.fileId); - if (state.status === "waiting_confirmation") { - this.cleanupTransfer(transferState.fileId); - } - } - }, 3e4); + this._armSenderIdleTimeout(transferState); } catch (error) { const safeError = SecurityErrorHandler.sanitizeError(error); console.error("\u274C Chunk transmission failed:", safeError); @@ -5026,6 +5023,45 @@ var EnhancedSecureFileTransfer = class { throw new Error(safeError); } } + // Resets a long idle timer; the sender stays available to retransmit missing + // chunks until the receiver finishes or this fires after sustained silence. + _armSenderIdleTimeout(transferState) { + const IDLE_MS = 18e4; + if (transferState._idleTimeout) clearTimeout(transferState._idleTimeout); + transferState._idleTimeout = setTimeout(() => { + const state = this.activeTransfers.get(transferState.fileId); + if (state && state.status !== "completed") { + this.cleanupTransfer(transferState.fileId); + } + }, IDLE_MS); + } + // Receiver asked us to re-send specific chunk indices (loss recovery / resume). + async handleChunkRequest(message) { + const transferState = this.activeTransfers.get(message?.fileId); + if (!transferState || !transferState.file) return; + const missing = Array.isArray(message.missing) ? message.missing : []; + if (missing.length === 0) return; + this._armSenderIdleTimeout(transferState); + transferState.status = "transmitting"; + const MAX_PER_REQUEST = 512; + const indices = missing.slice(0, MAX_PER_REQUEST); + for (const idx of indices) { + if (!Number.isInteger(idx) || idx < 0 || idx >= transferState.totalChunks) continue; + try { + const start2 = idx * this.CHUNK_SIZE; + const end = Math.min(start2 + this.CHUNK_SIZE, transferState.file.size); + const chunkData = await this.readFileChunk(transferState.file, start2, end); + await this.sendFileChunk(transferState, idx, chunkData); + await this.waitForBackpressure(); + } catch (error) { + console.warn("\u26A0\uFE0F Failed to retransmit chunk", idx, SecurityErrorHandler.sanitizeError(error)); + } + } + if (transferState.status === "transmitting") { + transferState.status = "waiting_confirmation"; + } + this._armSenderIdleTimeout(transferState); + } async readFileChunk(file, start2, end) { try { const blob = file.slice(start2, end); @@ -5207,6 +5243,9 @@ var EnhancedSecureFileTransfer = class { if (!receivingState) { return; } + if (receivingState._assembled || receivingState.status === "completed") { + return; + } if (!this._isIncomingChunkAllowed(chunkMessage.fileId)) { console.warn("\u26A0\uFE0F Incoming file chunk rate limit exceeded; cleaning up transfer:", chunkMessage.fileId); this.cleanupReceivingTransfer(chunkMessage.fileId); @@ -5253,22 +5292,7 @@ var EnhancedSecureFileTransfer = class { } } catch (error) { const safeError = SecurityErrorHandler.sanitizeError(error); - console.error("\u274C Failed to handle file chunk:", safeError); - const errorMessage = { - type: "file_transfer_error", - fileId: chunkMessage.fileId, - error: safeError, - chunkIndex: chunkMessage.chunkIndex, - timestamp: Date.now() - }; - await this.sendSecureMessage(errorMessage); - const receivingState = this.receivingTransfers.get(chunkMessage.fileId); - if (receivingState) { - receivingState.status = "failed"; - } - if (this.onError) { - this.onError(`Chunk processing failed: ${safeError}`); - } + console.warn("\u26A0\uFE0F Dropping unprocessable file chunk (will be re-requested):", chunkMessage.chunkIndex, safeError); } } ); @@ -5377,11 +5401,12 @@ var EnhancedSecureFileTransfer = class { timestamp: Date.now() }; await this.sendSecureMessage(completionMessage); - if (this.receivingTransfers.has(receivingState.fileId)) { - const rs = this.receivingTransfers.get(receivingState.fileId); - if (rs && rs.receivedChunks) rs.receivedChunks.clear(); + if (receivingState._stallTimer) { + clearInterval(receivingState._stallTimer); + receivingState._stallTimer = null; } - this.receivingTransfers.delete(receivingState.fileId); + if (receivingState.receivedChunks) receivingState.receivedChunks.clear(); + receivingState.sessionKey = null; } catch (error) { console.error("\u274C File assembly failed:", error); receivingState.status = "failed"; @@ -5446,6 +5471,9 @@ var EnhancedSecureFileTransfer = class { } transferState.confirmedChunks++; transferState.lastChunkTime = Date.now(); + if (transferState.status === "waiting_confirmation") { + this._armSenderIdleTimeout(transferState); + } } catch (error) { console.error("\u274C Failed to handle chunk confirmation:", error); } @@ -5507,6 +5535,9 @@ var EnhancedSecureFileTransfer = class { fileName: transfer.file?.name || "Unknown", fileSize: transfer.file?.size || 0, progress: Math.round(transfer.sentChunks / transfer.totalChunks * 100), + // Per-chunk detail for the segmented progress UI. + totalChunks: transfer.totalChunks || 0, + transferredChunks: transfer.sentChunks || 0, status: transfer.status, startTime: transfer.startTime })); @@ -5517,6 +5548,9 @@ var EnhancedSecureFileTransfer = class { fileName: transfer.fileName || "Unknown", fileSize: transfer.fileSize || 0, progress: Math.round(transfer.receivedCount / transfer.totalChunks * 100), + // Per-chunk detail for the segmented progress UI. + totalChunks: transfer.totalChunks || 0, + transferredChunks: transfer.receivedCount || 0, status: transfer.status, startTime: transfer.startTime })); @@ -5552,8 +5586,69 @@ var EnhancedSecureFileTransfer = class { }); this.pendingIncomingTransfers.delete(fileId); await this.sendSecureMessage({ type: "file_transfer_response", fileId, accepted: true, timestamp: Date.now() }); + this._startReceiverStallDetector(fileId); return true; } + // Periodically detects a stalled receive (lost chunks, connection blip, + // reconnect) and asks the sender to retransmit only the chunks we are still + // missing — so a dropped connection never loses the file. + _startReceiverStallDetector(fileId) { + const TICK_MS = 2500; + const STALL_MS = 5e3; + const MAX_IDLE_MS = 18e4; + const rs = this.receivingTransfers.get(fileId); + if (!rs) return; + if (rs._stallTimer) clearInterval(rs._stallTimer); + rs._lastProgressCount = rs.receivedCount || 0; + rs._lastProgressTime = Date.now(); + rs._stallTimer = setInterval(async () => { + const state = this.receivingTransfers.get(fileId); + if (!state || state._stallTimer !== rs._stallTimer) { + clearInterval(rs._stallTimer); + return; + } + if (state.status === "completed" || state._assembled) { + clearInterval(state._stallTimer); + state._stallTimer = null; + return; + } + if (state.receivedCount !== state._lastProgressCount) { + state._lastProgressCount = state.receivedCount; + state._lastProgressTime = Date.now(); + } + if (state.receivedCount >= state.totalChunks) return; + if (Date.now() - (state.lastChunkTime || 0) < STALL_MS) return; + if (Date.now() - state._lastProgressTime > MAX_IDLE_MS) { + clearInterval(state._stallTimer); + state._stallTimer = null; + state.status = "failed"; + if (this.onError) this.onError("File transfer stalled \u2014 no data received. Please try again."); + this.cleanupReceivingTransfer(fileId); + return; + } + await this._requestMissingChunks(fileId); + }, TICK_MS); + } + async _requestMissingChunks(fileId) { + const state = this.receivingTransfers.get(fileId); + if (!state || !state.receivedChunks) return; + const MAX_PER_REQUEST = 256; + const missing = []; + for (let i = 0; i < state.totalChunks && missing.length < MAX_PER_REQUEST; i++) { + if (!state.receivedChunks.has(i)) missing.push(i); + } + if (missing.length === 0) return; + state.status = "receiving"; + try { + await this.sendSecureMessage({ + type: "file_chunk_request", + fileId, + missing, + timestamp: Date.now() + }); + } catch (_) { + } + } async rejectIncomingFile(fileId, error = "Rejected by user") { if (!this.pendingIncomingTransfers.has(fileId)) return false; this.pendingIncomingTransfers.delete(fileId); @@ -5579,6 +5674,10 @@ var EnhancedSecureFileTransfer = class { cleanupTransfer(fileId) { const transferState = this.activeTransfers.get(fileId); if (transferState) { + if (transferState._idleTimeout) { + clearTimeout(transferState._idleTimeout); + transferState._idleTimeout = null; + } if (transferState.consentTimeout) { clearTimeout(transferState.consentTimeout); transferState.consentTimeout = null; @@ -5617,6 +5716,14 @@ var EnhancedSecureFileTransfer = class { } catch (_) { } this.receivedFileBuffers.delete(fileId); + const rs = this.receivingTransfers.get(fileId); + if (rs && (rs.status === "completed" || rs._assembled)) { + if (rs._stallTimer) { + clearInterval(rs._stallTimer); + rs._stallTimer = null; + } + this.receivingTransfers.delete(fileId); + } } // ✅ УЛУЧШЕННАЯ безопасная очистка памяти для предотвращения use-after-free cleanupReceivingTransfer(fileId) { @@ -5624,6 +5731,10 @@ var EnhancedSecureFileTransfer = class { this.pendingChunks.delete(fileId); const receivingState = this.receivingTransfers.get(fileId); if (receivingState) { + if (receivingState._stallTimer) { + clearInterval(receivingState._stallTimer); + receivingState._stallTimer = null; + } if (receivingState.receivedChunks && receivingState.receivedChunks.size > 0) { for (const [index, chunk] of receivingState.receivedChunks) { try { @@ -6065,6 +6176,8 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { ENHANCED_MESSAGE: "enhanced_message", // Per-message control (unsend / disappearing sync) MESSAGE_DELETE: "message_delete", + // Delivery receipt: recipient acks a chat message by id (WhatsApp ✓✓). + MESSAGE_RECEIPT: "message_receipt", // System messages HEARTBEAT: "heartbeat", VERIFICATION: "verification", @@ -8534,9 +8647,14 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { async _performPeriodicMemoryCleanup() { try { this._secureMemoryManager.isCleaning = true; - const shouldPreserveActiveKeys = this.sessionMode === "ratchet" && this.isConnected && this.dataChannel && this.dataChannel.readyState === "open"; + const preserveActiveRatchet = this.sessionMode === "ratchet" && this.isConnected && this.dataChannel && this.dataChannel.readyState === "open"; + const pendingOfferAgeMs = this._pendingOfferContext ? Date.now() - (this._pendingOfferContext.createdAt || 0) : Infinity; + const hasPendingOffer = !!this._pendingOfferContext && Array.isArray(this._pendingOfferContext.sessionSalt) && this._pendingOfferContext.sessionSalt.length === 64 && pendingOfferAgeMs < _EnhancedSecureWebRTCManager.LIMITS.OFFER_MAX_AGE; + const shouldPreserveActiveKeys = preserveActiveRatchet || hasPendingOffer; if (shouldPreserveActiveKeys) { - this._secureLog("debug", "\u{1F9F9} Skipping crypto key wipe during periodic cleanup (ratchet mode, active connection)"); + this._secureLog("debug", "\u{1F9F9} Skipping crypto key wipe during periodic cleanup", { + reason: preserveActiveRatchet ? "active ratchet connection" : "offer awaiting answer" + }); } else { this._secureCleanupCryptographicMaterials(); } @@ -11111,6 +11229,20 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { messageId: messageId.slice(0, 64) }); } + /** + * Delivery receipt: tell the sender we received a chat message (by id), so + * their bubble can flip from "sent" (✓) to "delivered" (✓✓). Best-effort, + * over the same authenticated control channel as unsend. + * @param {string} messageId + * @returns {boolean} + */ + sendDeliveryReceipt(messageId) { + if (typeof messageId !== "string" || !messageId) return false; + return this.sendSystemMessage({ + type: _EnhancedSecureWebRTCManager.MESSAGE_TYPES.MESSAGE_RECEIPT, + messageId: messageId.slice(0, 64) + }); + } async sendMessage(data, meta = null) { const validation = this._validateInputData(data, "sendMessage"); if (!validation.isValid) { @@ -11337,6 +11469,16 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { } return; } + if (parsed.type === _EnhancedSecureWebRTCManager.MESSAGE_TYPES.MESSAGE_RECEIPT) { + const messageId = parsed?.data?.messageId ?? parsed?.messageId; + if (typeof messageId === "string" && messageId) { + try { + this.onMessageDelivered?.(messageId.slice(0, 64)); + } catch (_) { + } + } + return; + } if (parsed.type && ["heartbeat", "verification", "verification_response", "verification_confirmed", "verification_both_confirmed", "peer_disconnect", "security_upgrade"].includes(parsed.type)) { this.handleSystemMessage(parsed); return; @@ -12162,6 +12304,16 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager { } return; } + if (parsed.type === _EnhancedSecureWebRTCManager.MESSAGE_TYPES.MESSAGE_RECEIPT) { + const messageId = parsed?.data?.messageId ?? parsed?.messageId; + if (typeof messageId === "string" && messageId) { + try { + this.onMessageDelivered?.(messageId.slice(0, 64)); + } catch (_) { + } + } + return; + } if (parsed.type && ["heartbeat", "verification", "verification_response", "verification_confirmed", "verification_both_confirmed", "sas_code", "peer_disconnect", "security_upgrade"].includes(parsed.type)) { this.handleSystemMessage(parsed); return; @@ -17467,156 +17619,87 @@ Right-click or Ctrl+click to disconnect`, delete window.debugHeaderSecurity; }; }, [realSecurityLevel, lastSecurityUpdate, isConnected, webrtcManager, displaySecurityLevel, securityDetails]); + const secColor = displaySecurityLevel ? displaySecurityLevel.color === "green" ? "#3ecf8e" : displaySecurityLevel.color === "orange" ? "#f0892a" : displaySecurityLevel.color === "yellow" ? "#e3c84e" : "#e5727a" : "#3ecf8e"; + const dotColor = isConnected ? "#3ecf8e" : ["connecting", "verifying", "retrying", "reconnecting"].includes(status) ? "#e3c84e" : status === "failed" ? "#e5727a" : "#6b6b73"; + const dotGlow = dotColor === "#3ecf8e" ? "rgba(62,207,142,0.16)" : dotColor === "#e3c84e" ? "rgba(227,200,78,0.16)" : dotColor === "#e5727a" ? "rgba(229,114,122,0.16)" : "rgba(107,107,115,0.16)"; + const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace"; + const onLanding = !isConnected; + const [scrolled, setScrolled] = React.useState(false); + React.useEffect(() => { + const onScroll = () => setScrolled((window.scrollY || window.pageYOffset || 0) > 8); + onScroll(); + window.addEventListener("scroll", onScroll, { passive: true }); + return () => window.removeEventListener("scroll", onScroll); + }, []); + const overlay = { position: "fixed", top: 0, left: 0, right: 0 }; + const headerStyle = onLanding ? scrolled ? { ...overlay, background: "rgba(15,15,17,0.72)", backdropFilter: "blur(14px)", WebkitBackdropFilter: "blur(14px)", borderBottom: "1px solid rgba(255,255,255,0.06)", transition: "background .25s ease, backdrop-filter .25s ease, border-color .25s ease" } : { ...overlay, background: "transparent", backdropFilter: "none", WebkitBackdropFilter: "none", borderBottom: "1px solid transparent", transition: "background .25s ease, backdrop-filter .25s ease, border-color .25s ease" } : { background: "rgba(18,18,20,0.72)", backdropFilter: "blur(14px)", WebkitBackdropFilter: "blur(14px)", borderBottom: "1px solid rgba(255,255,255,0.06)" }; return React.createElement("header", { - className: "header-minimal sticky top-0 z-50" + className: onLanding ? "header-minimal z-50" : "header-minimal sticky top-0 z-50", + style: headerStyle }, [ React.createElement("div", { key: "container", - className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" + className: "max-w-7xl mx-auto", + style: { padding: "0 20px" } }, [ React.createElement("div", { key: "content", - className: "flex items-center justify-between h-16" + className: "flex items-center justify-between", + style: { height: "64px", gap: "16px" } }, [ - // Logo and Title - React.createElement("div", { - key: "logo-section", - className: "flex items-center space-x-2 sm:space-x-3" - }, [ - React.createElement("div", { - key: "logo", - className: "icon-container w-8 h-8 sm:w-10 sm:h-10" - }, [ - React.createElement("i", { - className: "fas fa-shield-halved accent-orange text-sm sm:text-base" - }) - ]), - React.createElement("div", { - key: "title-section" - }, [ - React.createElement("h1", { - key: "title", - className: "text-lg sm:text-xl font-semibold text-primary" - }, "SecureBit.chat"), - React.createElement("p", { - key: "subtitle", - className: "text-xs sm:text-sm text-muted hidden sm:block" - }, "End-to-end freedom v4.8.20") + // Left: logo + wordmark + React.createElement("div", { key: "left", style: { display: "flex", alignItems: "center", gap: "12px", minWidth: 0 } }, [ + React.createElement( + "div", + { key: "logo", style: { width: "36px", height: "36px", flex: "none", display: "grid", placeItems: "center" } }, + React.createElement("img", { src: "/logo/securebit-mark.svg", alt: "SecureBit", style: { width: "100%", height: "100%", objectFit: "contain", display: "block" } }) + ), + 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("div", { key: "r2", className: "hidden sm:block", style: { fontSize: "11px", color: "#6b6b73", fontWeight: 500 } }, "End-to-end encrypted") ]) ]), - // Status and Controls - Responsive - React.createElement("div", { - key: "status-section", - className: "flex items-center space-x-2 sm:space-x-3" - }, [ - React.createElement("button", { - key: "network-settings", + // Right: controls + React.createElement("div", { key: "right", style: { display: "flex", alignItems: "center", gap: "9px" } }, [ + !onLanding && React.createElement("button", { + key: "net", type: "button", onClick: () => window.dispatchEvent(new CustomEvent("securebit:open-network-settings")), title: "Advanced network settings (STUN/TURN)", "aria-label": "Advanced network settings", - className: "w-8 h-8 rounded-full flex items-center justify-center text-muted hover:text-primary hover:bg-white/5 transition-colors duration-200" - }, [ - React.createElement("i", { key: "i", className: "fas fa-network-wired text-sm" }) - ]), - displaySecurityLevel && React.createElement("div", { - key: "security-level", - className: "hidden md:flex items-center space-x-2 cursor-pointer hover:opacity-80 transition-opacity duration-200", + className: "sb-disconnect", + style: { display: "grid", placeItems: "center", width: "38px", height: "38px", borderRadius: "9px", border: "1px solid rgba(255,255,255,0.07)", background: "rgba(255,255,255,0.02)", color: "#9a9aa2", cursor: "pointer", transition: "all .15s" } + }, React.createElement("i", { className: "fas fa-network-wired", style: { fontSize: "13px" } })), + !onLanding && displaySecurityLevel && React.createElement("div", { + key: "sec", onClick: handleSecurityClick, onContextMenu: (e) => { e.preventDefault(); - if (onDisconnect && typeof onDisconnect === "function") { - onDisconnect(); - } + if (typeof onDisconnect === "function") onDisconnect(); }, - title: securityDetails.tooltip + title: securityDetails.tooltip, + className: "sb-secpill", + style: { display: "flex", alignItems: "center", gap: "8px", padding: "7px 12px", borderRadius: "9px", border: "1px solid rgba(255,255,255,0.07)", background: "rgba(255,255,255,0.02)", cursor: "pointer" } }, [ - React.createElement("div", { - key: "security-icon", - className: `w-6 h-6 rounded-full flex items-center justify-center relative ${displaySecurityLevel.color === "green" ? "bg-green-500/20" : displaySecurityLevel.color === "orange" ? "bg-orange-500/20" : displaySecurityLevel.color === "yellow" ? "bg-yellow-500/20" : "bg-red-500/20"} ${securityDetails.isVerified ? "" : "animate-pulse"}` - }, [ - React.createElement("i", { - className: `fas fa-shield-alt text-xs ${displaySecurityLevel.color === "green" ? "text-green-400" : displaySecurityLevel.color === "orange" ? "text-orange-400" : displaySecurityLevel.color === "yellow" ? "text-yellow-400" : "text-red-400"}` - }) - ]), - React.createElement("div", { - key: "security-info", - className: "flex flex-col" - }, [ - React.createElement("div", { - key: "security-level-text", - className: "text-xs font-medium text-primary flex items-center space-x-1" - }, [ - React.createElement("span", {}, `${displaySecurityLevel.level} (${displaySecurityLevel.score}%)`) - ]), - React.createElement( - "div", - { - key: "security-details", - className: "text-xs text-muted mt-1 hidden lg:block" - }, - securityDetails.dataSource === "real" ? `${displaySecurityLevel.passedChecks || 0}/${displaySecurityLevel.totalChecks || 0} tests` : displaySecurityLevel.details || `Stage ${displaySecurityLevel.stage || 1}` - ), - React.createElement("div", { - key: "security-progress", - className: "w-16 h-1 bg-gray-600 rounded-full overflow-hidden" - }, [ - React.createElement("div", { - key: "progress-bar", - className: `h-full transition-all duration-500 ${displaySecurityLevel.color === "green" ? "bg-green-400" : displaySecurityLevel.color === "orange" ? "bg-orange-400" : displaySecurityLevel.color === "yellow" ? "bg-yellow-400" : "bg-red-400"}`, - style: { width: `${displaySecurityLevel.score}%` } - }) - ]) - ]) + React.createElement("i", { key: "i", className: "fas fa-shield-halved", style: { fontSize: "13px", color: secColor } }), + React.createElement("span", { key: "l", className: "hidden sm:inline", style: { fontSize: "12.5px", fontWeight: 600, color: "#e8e8eb" } }, String(displaySecurityLevel.level)), + React.createElement("span", { key: "s", style: { fontFamily: MONO, fontSize: "11.5px", color: "#8a8a92" } }, displaySecurityLevel.score + "%") ]), - // Mobile Security Indicator - displaySecurityLevel && React.createElement("div", { - key: "mobile-security", - className: "md:hidden flex items-center" - }, [ - React.createElement("div", { - key: "mobile-security-icon", - className: `w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity duration-200 relative ${displaySecurityLevel.color === "green" ? "bg-green-500/20" : displaySecurityLevel.color === "orange" ? "bg-orange-500/20" : displaySecurityLevel.color === "yellow" ? "bg-yellow-500/20" : "bg-red-500/20"} ${securityDetails.isVerified ? "" : "animate-pulse"}`, - title: securityDetails.tooltip, - onClick: handleSecurityClick, - onContextMenu: (e) => { - e.preventDefault(); - if (onDisconnect && typeof onDisconnect === "function") { - onDisconnect(); - } - } - }, [ - React.createElement("i", { - className: `fas fa-shield-alt text-sm ${displaySecurityLevel.color === "green" ? "text-green-400" : displaySecurityLevel.color === "orange" ? "text-orange-400" : displaySecurityLevel.color === "yellow" ? "text-yellow-400" : "text-red-400"}` - }) - ]) + !onLanding && React.createElement("div", { key: "status", style: { display: "flex", alignItems: "center", gap: "8px", padding: "8px 13px", borderRadius: "9px", border: "1px solid rgba(255,255,255,0.07)", background: "rgba(255,255,255,0.02)" } }, [ + React.createElement("span", { key: "dot", style: { width: "7px", height: "7px", borderRadius: "50%", background: dotColor, boxShadow: "0 0 0 3px " + dotGlow } }), + React.createElement("span", { key: "t", className: "hidden sm:inline", style: { fontSize: "13px", fontWeight: 600, color: "#cfcfd4" } }, config.text) ]), - // Status Badge - React.createElement("div", { - key: "status-badge", - className: `px-2 sm:px-3 py-1.5 rounded-lg border ${config.badgeClass} flex items-center space-x-1 sm:space-x-2` - }, [ - React.createElement("span", { - key: "status-dot", - className: `status-dot ${config.className}` - }), - React.createElement("span", { - key: "status-text", - className: "text-xs sm:text-sm font-medium" - }, config.text) - ]), - // Disconnect Button isConnected && React.createElement("button", { - key: "disconnect-btn", + key: "dc", onClick: onDisconnect, - className: "p-1.5 sm:px-3 sm:py-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 rounded-lg transition-all duration-200 text-sm" + className: "sb-disconnect", + style: { display: "flex", alignItems: "center", gap: "7px", padding: "8px 14px", borderRadius: "9px", border: "1px solid rgba(255,255,255,0.08)", background: "transparent", color: "#9a9aa2", fontFamily: "inherit", fontSize: "13px", fontWeight: 600, cursor: "pointer", transition: "all .15s" } }, [ - React.createElement("i", { - className: "fas fa-power-off sm:mr-2" - }), - React.createElement("span", { - className: "hidden sm:inline" - }, "Disconnect") + React.createElement("i", { key: "i", className: "fas fa-power-off", style: { fontSize: "12px" } }), + React.createElement("span", { key: "t", className: "sb-hide-sm" }, "Disconnect") ]) ]) ]) @@ -17706,1011 +17789,815 @@ window.DownloadApps = DownloadApps; // src/components/ui/BecomePartner.jsx var BecomePartner = () => { - const partners = [ - { id: "aegis", name: "Aegis", logo: "logo/aegis.png", isColor: true, url: "https://aegis-investment.com/" }, - { id: "furi", name: "Furi Labs", logo: "logo/furi.png", isColor: true, url: "https://furilabs.com/" } - ]; + const [isMobile, setIsMobile] = React.useState( + typeof window !== "undefined" && window.matchMedia("(max-width:767px)").matches + ); + React.useEffect(() => { + const mq = window.matchMedia("(max-width:767px)"); + const onChange = () => setIsMobile(mq.matches); + mq.addEventListener ? mq.addEventListener("change", onChange) : mq.addListener(onChange); + return () => { + mq.removeEventListener ? mq.removeEventListener("change", onChange) : mq.removeListener(onChange); + }; + }, []); + const ACCENT = "#f0892a"; + const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace"; + const SANS = "'Manrope', system-ui, -apple-system, sans-serif"; const formUrl = "https://docs.google.com/forms/d/e/1FAIpQLSc9ijV9PCoyXkus6vEx1OWwvwAsLq8fKS6-H5BmX-c-bvia6w/viewform?usp=dialog"; - return React.createElement("div", { className: "mt-20 px-6" }, [ - // Header "Trusted by our partners" - React.createElement("div", { key: "header", className: "text-center max-w-3xl mx-auto mb-8" }, [ - React.createElement("h3", { key: "title", className: "text-3xl font-bold text-primary mb-3" }, "Trusted by our partners") - ]), - // First divider line with fade - React.createElement("div", { - key: "divider-1", - className: "h-px w-full max-w-3xl mx-auto mb-8 bg-gradient-to-r from-transparent via-zinc-700 to-transparent" - }), - // Partner Logos + const partners = [ + { + id: "aegis", + name: "Aegis Investment", + logo: "logo/aegis.png", + logoHeight: "42px", + url: "https://aegis-investment.com/", + desc: "Capital partner securing confidential financial communications across its portfolio.", + role: "Strategic backer", + delay: ".5s" + }, + { + id: "furi", + name: "FuriLabs", + logo: "logo/furi.png", + logoHeight: "54px", + url: "https://furilabs.com/", + desc: "Privacy-first Linux phones that ship SecureBit as a default secure channel.", + role: "Technology partner", + delay: ".56s" + } + ]; + const svg2 = (inner2, size, stroke, sw) => React.createElement("svg", { + width: size, + height: size, + viewBox: "0 0 24 24", + fill: "none", + stroke, + strokeWidth: sw, + strokeLinecap: "round", + strokeLinejoin: "round", + dangerouslySetInnerHTML: { __html: inner2 } + }); + const roleTag = (role) => React.createElement("span", { + key: "role", + style: { fontFamily: MONO, fontSize: "10.5px", fontWeight: 600, color: "#6b6b73", textTransform: "uppercase", letterSpacing: "1.2px", padding: "6px 11px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.07)", background: "rgba(255,255,255,0.025)", whiteSpace: "nowrap" } + }, role); + const partnerCard = (p) => React.createElement("a", { + key: p.id, + href: p.url, + target: "_blank", + rel: "noopener noreferrer", + style: { + flex: "1 1 320px", + minWidth: isMobile ? "auto" : "300px", + borderRadius: "18px", + background: "#141416", + border: "1px solid rgba(255,255,255,0.06)", + padding: "30px 30px 26px", + display: "flex", + flexDirection: "column", + textDecoration: "none", + color: "inherit", + transition: "transform .28s cubic-bezier(.2,.7,.3,1), border-color .28s cubic-bezier(.2,.7,.3,1)", + animation: `ptUp ${p.delay} cubic-bezier(.2,.7,.3,1)` + }, + onMouseEnter: (e) => { + e.currentTarget.style.transform = "translateY(-4px)"; + e.currentTarget.style.borderColor = "rgba(255,255,255,0.13)"; + }, + onMouseLeave: (e) => { + e.currentTarget.style.transform = "none"; + e.currentTarget.style.borderColor = "rgba(255,255,255,0.06)"; + } + }, [ React.createElement( "div", - { - key: "partners-row", - className: "flex justify-center items-center flex-wrap gap-12 mb-8" - }, - partners.map( - (partner) => React.createElement("a", { - key: partner.id, - href: partner.url, - target: "_blank", - rel: "noopener noreferrer", - className: "flex items-center justify-center cursor-pointer hover:opacity-100 transition-opacity duration-300" - }, [ - React.createElement("img", { - key: "logo", - src: partner.logo, - alt: partner.name, - className: "h-12 sm:h-16 opacity-80 hover:opacity-100 transition-opacity duration-300", - style: partner.isColor ? { - filter: "grayscale(100%) brightness(1.2) contrast(1.1)", - WebkitFilter: "grayscale(100%) brightness(1.2) contrast(1.1)" - } : {} - }) - ]) - ) + { key: "logo", style: { display: "flex", alignItems: "center", marginBottom: "30px", height: "54px" } }, + React.createElement("img", { + src: p.logo, + alt: p.name, + style: { height: p.logoHeight, width: "auto", maxWidth: "190px", objectFit: "contain", display: "block" } + }) ), - // Second divider line with fade - React.createElement("div", { - key: "divider-2", - className: "h-px w-full max-w-3xl mx-auto mb-8 bg-gradient-to-r from-transparent via-zinc-700 to-transparent" - }), - // Section with subtitle and text - React.createElement("div", { key: "cta-section", className: "text-center max-w-3xl mx-auto" }, [ - React.createElement("h4", { - key: "subtitle", - className: "text-base font-semibold text-primary mb-4" - }, "Technology & Community Partners"), - React.createElement("p", { - key: "description", - className: "text-secondary text-sm mb-6" - }, "Interested in partnering with us?"), - // CTA Button with 3D glass effect - React.createElement("div", { - key: "button-wrapper", - className: "button-container flex justify-center" - }, [ - React.createElement("a", { - key: "button-link", - href: formUrl, - target: "_blank", - rel: "noopener noreferrer", - className: "button" - }, [ - React.createElement("span", { key: "text" }, "Become a Partner") - ]) - ]) + React.createElement("h3", { key: "name", style: { margin: "0 0 9px", fontSize: "21px", fontWeight: 800, letterSpacing: "-0.4px", color: "#f4f4f6" } }, p.name), + React.createElement("p", { key: "desc", style: { margin: "0 0 22px", fontSize: "14.5px", lineHeight: 1.6, color: "#9a9aa2" } }, p.desc), + React.createElement("div", { key: "foot", style: { marginTop: "auto", paddingTop: "6px", display: "flex", alignItems: "center", gap: "12px" } }, [ + roleTag(p.role) ]) ]); + const inviteCard = React.createElement("a", { + key: "invite", + href: formUrl, + target: "_blank", + rel: "noopener noreferrer", + style: { + flex: "1 1 320px", + minWidth: isMobile ? "auto" : "300px", + borderRadius: "18px", + background: "#111113", + border: "1px dashed rgba(255,255,255,0.12)", + padding: "30px", + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + textDecoration: "none", + color: "inherit", + transition: "border-color .28s cubic-bezier(.2,.7,.3,1)", + animation: "ptUp .62s cubic-bezier(.2,.7,.3,1)" + }, + onMouseEnter: (e) => { + e.currentTarget.style.borderColor = "rgba(240,137,42,0.4)"; + }, + onMouseLeave: (e) => { + e.currentTarget.style.borderColor = "rgba(255,255,255,0.12)"; + } + }, [ + React.createElement("div", { key: "top" }, [ + React.createElement("div", { + key: "icon", + style: { width: "48px", height: "48px", borderRadius: "13px", display: "grid", placeItems: "center", background: "rgba(240,137,42,0.12)", border: "1px solid rgba(240,137,42,0.28)", marginBottom: "24px" } + }, svg2('', 23, ACCENT, 1.9)), + React.createElement("h3", { key: "title", style: { margin: "0 0 8px", fontSize: "21px", fontWeight: 800, letterSpacing: "-0.4px", color: "#f4f4f6" } }, "Become a partner"), + React.createElement("p", { key: "desc", style: { margin: 0, fontSize: "14.5px", lineHeight: 1.6, color: "#8a8a92" } }, "Building privacy hardware or infrastructure? Let's integrate SecureBit.") + ]), + React.createElement("span", { + key: "btn", + style: { + marginTop: "26px", + width: "100%", + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + gap: "10px", + padding: "15px 20px", + borderRadius: "12px", + border: "none", + background: ACCENT, + color: "#1a0f04", + fontFamily: SANS, + fontSize: "15px", + fontWeight: 700, + cursor: "pointer", + boxShadow: "0 8px 24px rgba(240,137,42,0.28)", + boxSizing: "border-box", + transition: "background .2s cubic-bezier(.2,.7,.3,1), transform .2s cubic-bezier(.2,.7,.3,1)" + } + }, [ + "Start a conversation", + svg2('', 17, "currentColor", 2.2) + ]) + ]); + const inner = React.createElement("div", { + key: "inner", + style: { maxWidth: "1240px", margin: "0 auto", padding: isMobile ? "0 18px" : "0 40px" } + }, [ + // Header + React.createElement("div", { key: "head", style: { marginBottom: "44px" } }, [ + React.createElement("div", { + key: "eyebrow", + style: { fontFamily: MONO, fontSize: "11px", fontWeight: 600, color: "#6b6b73", textTransform: "uppercase", letterSpacing: "1.6px", marginBottom: "14px" } + }, "Partners & ecosystem"), + React.createElement("div", { + key: "row", + style: { display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: "32px", flexWrap: "wrap" } + }, [ + React.createElement("h2", { + key: "h2", + style: { margin: 0, fontSize: isMobile ? "30px" : "40px", fontWeight: 800, letterSpacing: "-1.1px", lineHeight: 1.04, color: "#f4f4f6" } + }, "Trusted by our partners"), + React.createElement("p", { + key: "sub", + style: { margin: "0 0 4px", fontSize: "15px", lineHeight: 1.55, color: "#8a8a92", maxWidth: "360px" } + }, "A small, vetted circle \u2014 no pay-to-list logos and no badges we can't stand behind.") + ]) + ]), + // Cards + React.createElement("div", { + key: "cards", + style: { display: "flex", gap: "18px", alignItems: "stretch", flexWrap: "wrap" } + }, [ + ...partners.map(partnerCard), + inviteCard + ]) + ]); + return React.createElement("section", { + style: { + width: "100%", + color: "#e8e8eb", + fontFamily: SANS, + padding: isMobile ? "48px 0" : "72px 0", + background: "radial-gradient(1100px 640px at 50% -6%, rgba(240,137,42,0.055), transparent 62%), #0f0f11" + } + }, [ + React.createElement("style", { key: "kf", dangerouslySetInnerHTML: { __html: "@keyframes ptUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}" } }), + inner + ]); }; window.BecomePartner = BecomePartner; // src/components/ui/UniqueFeatureSlider.jsx var UniqueFeatureSlider = () => { - const trackRef = React.useRef(null); - const wrapRef = React.useRef(null); - const [current, setCurrent] = React.useState(0); - const [isReady, setIsReady] = React.useState(false); + const [active, setActive] = React.useState(0); + const [isMobile, setIsMobile] = React.useState( + typeof window !== "undefined" && window.matchMedia("(max-width:767px)").matches + ); + React.useEffect(() => { + const mq = window.matchMedia("(max-width:767px)"); + const onChange = () => setIsMobile(mq.matches); + mq.addEventListener ? mq.addEventListener("change", onChange) : mq.addListener(onChange); + return () => { + mq.removeEventListener ? mq.removeEventListener("change", onChange) : mq.removeListener(onChange); + }; + }, []); + const ACCENT = "#f0892a"; + const ACTIVE_BG = "radial-gradient(130% 90% at 28% 0%, rgba(240,137,42,0.11), transparent 60%), #141416"; + const ACTIVE_BD = "rgba(240,137,42,0.3)"; + const IDLE_BG = "#111113"; + const IDLE_BD = "rgba(255,255,255,0.06)"; + const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace"; + const SANS = "'Manrope', system-ui, -apple-system, sans-serif"; const slides = [ { - icon: "\u{1F6E1}\uFE0F", - bgImage: "linear-gradient(135deg, rgb(255 107 53 / 6%) 0%, rgb(255 140 66 / 45%) 100%)", - thumbIcon: "\u{1F512}", - title: "18-Layer Military Security", - description: "Revolutionary defense system with ECDH P-384 + AES-GCM 256 + ECDSA + Complete ASN.1 Validation." + num: "01", + title: ["Layered", "encryption core"], + collapsed: "Encryption core", + desc: "ECDH P-384 key exchange, AES-256-GCM payloads, ECDSA signatures and full ASN.1 validation \u2014 composed into one hardened pipeline.", + tags: ["ECDH P-384", "AES-256-GCM", "ECDSA", "ASN.1"], + icon: '' }, { - icon: "\u{1F310}", - bgImage: "linear-gradient(135deg, rgb(147 51 234 / 6%) 0%, rgb(168 85 247 / 45%) 100%)", - thumbIcon: "\u{1F517}", - title: "Pure P2P WebRTC", - description: "Direct peer-to-peer connections without any servers. Complete decentralization with zero infrastructure." + num: "02", + title: ["Pure P2P", "WebRTC"], + collapsed: "Pure P2P WebRTC", + desc: "Messages travel directly between devices over WebRTC. No relay holds your data \u2014 the server only helps two peers find each other.", + tags: ["DTLS 1.3", "No relay"], + icon: '' }, { - icon: "\u{1F504}", - bgImage: "linear-gradient(135deg, rgb(16 185 129 / 6%) 0%, rgb(52 211 153 / 45%) 100%)", - thumbIcon: "\u26A1", - title: "Perfect Forward Secrecy", - description: "Automatic key rotation every 5 minutes. Non-extractable keys with hardware protection." + num: "03", + title: ["Perfect", "forward secrecy"], + collapsed: "Forward secrecy", + desc: "Session keys rotate continuously and are discarded after use, so a single compromised key can never unlock past conversations.", + tags: ["Ephemeral keys", "Auto-rotate"], + icon: '' }, { - icon: "\u{1F3AD}", - bgImage: "linear-gradient(135deg, rgb(6 182 212 / 6%) 0%, rgb(34 211 238 / 45%) 100%)", - thumbIcon: "\u{1F32B}\uFE0F", - title: "Traffic Obfuscation", - description: "Fake traffic generation and pattern masking make communication indistinguishable from noise." + num: "04", + title: ["Traffic", "obfuscation"], + collapsed: "Traffic obfuscation", + desc: "Packet sizes and timing are padded and randomized, hiding metadata patterns from anyone watching the wire.", + tags: ["Packet padding", "Timing jitter"], + icon: '' }, { - icon: "\u{1F441}\uFE0F", - bgImage: "linear-gradient(135deg, rgb(37 99 235 / 6%) 0%, rgb(59 130 246 / 45%) 100%)", - thumbIcon: "\u{1F6AB}", - title: "Zero Data Collection", - description: "No registration, no servers, no logs. Complete anonymity with instant channels." + num: "05", + title: ["Zero data", "collection"], + collapsed: "Zero data collection", + desc: "No accounts, no logs, no message storage. There is nothing on a server to leak, subpoena, or sell.", + tags: ["No accounts", "No logs"], + icon: '' } ]; - React.useEffect(() => { - const timer = setTimeout(() => { - setIsReady(true); - }, 100); - return () => clearTimeout(timer); - }, []); - const isMobile = () => window.matchMedia("(max-width:767px)").matches; - const center = React.useCallback((i) => { - if (!trackRef.current || !wrapRef.current) return; - const card = trackRef.current.children[i]; - if (!card) return; - const axis = isMobile() ? "top" : "left"; - const size = isMobile() ? "clientHeight" : "clientWidth"; - const start2 = isMobile() ? card.offsetTop : card.offsetLeft; - wrapRef.current.scrollTo({ - [axis]: start2 - (wrapRef.current[size] / 2 - card[size] / 2), - behavior: "smooth" - }); - }, []); - const activate = React.useCallback((i, scroll = false) => { - if (i === current) return; - setCurrent(i); - if (scroll) { - setTimeout(() => center(i), 50); + const svg2 = (inner2, size, stroke, sw) => React.createElement("svg", { + width: size, + height: size, + viewBox: "0 0 24 24", + fill: "none", + stroke, + strokeWidth: sw, + strokeLinecap: "round", + strokeLinejoin: "round", + dangerouslySetInnerHTML: { __html: inner2 } + }); + const go = (step) => setActive((a) => (a + step + slides.length) % slides.length); + const navBtn = (key, onClick, path) => React.createElement("button", { + key, + onClick, + "aria-label": key, + style: { + width: "46px", + height: "46px", + display: "grid", + placeItems: "center", + borderRadius: "50%", + border: "1px solid rgba(255,255,255,0.1)", + background: "rgba(255,255,255,0.025)", + color: "#cfcfd4", + cursor: "pointer", + transition: "all .2s cubic-bezier(.2,.7,.3,1)" + }, + onMouseEnter: (e) => { + e.currentTarget.style.borderColor = ACTIVE_BD; + e.currentTarget.style.color = ACCENT; + }, + onMouseLeave: (e) => { + e.currentTarget.style.borderColor = "rgba(255,255,255,0.1)"; + e.currentTarget.style.color = "#cfcfd4"; } - }, [current, center]); - const go = (step) => { - const newIndex = Math.min(Math.max(current + step, 0), slides.length - 1); - activate(newIndex, true); - }; - React.useEffect(() => { - const handleKeydown = (e) => { - if (["ArrowRight", "ArrowDown"].includes(e.key)) go(1); - if (["ArrowLeft", "ArrowUp"].includes(e.key)) go(-1); - }; - window.addEventListener("keydown", handleKeydown, { passive: true }); - return () => window.removeEventListener("keydown", handleKeydown); - }, [current]); - React.useEffect(() => { - if (isReady) { - center(current); + }, svg2(path, 18, "currentColor", 2.1)); + const tag = (label) => React.createElement("span", { + key: label, + style: { + display: "inline-flex", + alignItems: "center", + gap: "7px", + padding: "7px 12px", + borderRadius: "9px", + border: "1px solid rgba(255,255,255,0.07)", + background: "rgba(255,255,255,0.025)", + fontFamily: MONO, + fontSize: "11.5px", + fontWeight: 500, + color: "#9a9aa2" } - }, [current, center, isReady]); - if (!isReady) { - return React.createElement( - "section", - { - style: { - background: "transparent", - minHeight: "400px", - display: "flex", - alignItems: "center", - justifyContent: "center" - } - }, + }, [ + React.createElement("span", { key: "dot", style: { width: "5px", height: "5px", borderRadius: "50%", background: "#3ecf8e" } }), + label + ]); + const expandedContent = (s) => React.createElement("div", { + key: "exp", + style: { + height: "100%", + display: "flex", + flexDirection: "column", + justifyContent: isMobile ? "flex-start" : "space-between", + gap: isMobile ? "18px" : 0, + padding: isMobile ? "24px 22px" : "32px 34px", + minWidth: isMobile ? "auto" : "320px", + animation: "wuUp .42s cubic-bezier(.2,.7,.3,1)" + } + }, [ + React.createElement("div", { key: "top", style: { display: "flex", alignItems: "center", justifyContent: "space-between" } }, [ React.createElement("div", { + key: "ic", style: { - opacity: 0.5, - fontSize: "14px", - color: "#fff" + width: "54px", + height: "54px", + borderRadius: "15px", + display: "grid", + placeItems: "center", + background: "rgba(240,137,42,0.13)", + border: "1px solid rgba(240,137,42,0.3)" } - }, "Loading...") - ); - } - return React.createElement("section", { style: { background: "transparent" } }, [ + }, svg2(s.icon, 26, ACCENT, 1.9)), + React.createElement("span", { key: "n", style: { fontFamily: MONO, fontSize: "13px", fontWeight: 600, color: "#6b6b73" } }, s.num) + ]), + React.createElement("div", { key: "mid" }, [ + React.createElement("h3", { + key: "h", + style: { margin: "0 0 12px", fontSize: isMobile ? "24px" : "30px", fontWeight: 800, letterSpacing: "-0.7px", lineHeight: 1.08, color: "#f4f4f6" } + }, [s.title[0], React.createElement("br", { key: "br" }), s.title[1]]), + React.createElement("p", { + key: "p", + style: { margin: 0, fontSize: "15px", lineHeight: 1.6, color: "#9a9aa2", maxWidth: "380px" } + }, s.desc) + ]), + React.createElement("div", { key: "tags", style: { display: "flex", flexWrap: "wrap", gap: "8px" } }, s.tags.map(tag)) + ]); + const collapsedContent = (s) => isMobile ? React.createElement("div", { + key: "col", + style: { display: "flex", alignItems: "center", gap: "16px", padding: "20px 22px" } + }, [ + React.createElement("span", { key: "n", style: { fontFamily: MONO, fontSize: "12px", fontWeight: 600, color: "#56565e" } }, s.num), + React.createElement("span", { key: "l", style: { fontSize: "16px", fontWeight: 800, letterSpacing: "-0.2px", color: "#cfcfd4" } }, s.collapsed) + ]) : React.createElement("div", { + key: "col", + style: { position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "space-between", padding: "24px 0" } + }, [ + React.createElement("span", { key: "n", style: { fontFamily: MONO, fontSize: "12px", fontWeight: 600, color: "#56565e" } }, s.num), + React.createElement("span", { + key: "l", + style: { writingMode: "vertical-rl", transform: "rotate(180deg)", fontSize: "17px", fontWeight: 800, letterSpacing: "-0.2px", color: "#cfcfd4", whiteSpace: "nowrap" } + }, s.collapsed), + svg2(s.icon, 22, "#56565e", 1.8) + ]); + const panels = slides.map((s, i) => { + const isActive = active === i; + return React.createElement("div", { + key: i, + onClick: () => setActive(i), + // Selection is click-only (like the design); hover just brightens the panel + // a touch so the orange glow never jumps around chasing the cursor. + onMouseEnter: (e) => { + if (!isActive) e.currentTarget.style.filter = "brightness(1.18)"; + }, + onMouseLeave: (e) => { + e.currentTarget.style.filter = "none"; + }, + style: { + flex: isMobile ? "none" : isActive ? 6.2 : 1, + minWidth: isMobile ? "auto" : "72px", + position: "relative", + borderRadius: "18px", + overflow: "hidden", + cursor: "pointer", + background: isActive ? ACTIVE_BG : IDLE_BG, + border: "1px solid " + (isActive ? ACTIVE_BD : IDLE_BD), + color: "#8a8a92", + transition: "flex .46s cubic-bezier(.2,.7,.3,1), background .3s ease, border-color .3s ease, filter .2s ease" + } + }, isActive ? expandedContent(s) : collapsedContent(s)); + }); + const inner = React.createElement("div", { + key: "inner", + style: { + maxWidth: "1180px", + margin: "0 auto", + padding: isMobile ? "0 18px" : "0 40px" + } + }, [ // Header React.createElement("div", { key: "head", - className: "head" + style: { display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: "24px", marginBottom: "28px" } }, [ - React.createElement("h2", { - key: "title", - className: "text-2xl sm:text-3xl font-bold text-white mb-4 leading-snug" - }, "Why SecureBit.chat is unique"), - React.createElement("div", { - key: "controls", - className: "controls" - }, [ - React.createElement("button", { - key: "prev", - id: "prev-slider", - className: "nav-btn", - "aria-label": "Prev", - disabled: current === 0, - onClick: () => go(-1) - }, "\u2039"), - React.createElement("button", { - key: "next", - id: "next-slider", - className: "nav-btn", - "aria-label": "Next", - disabled: current === slides.length - 1, - onClick: () => go(1) - }, "\u203A") + React.createElement("div", { key: "titles" }, [ + React.createElement("div", { + key: "eyebrow", + style: { fontFamily: MONO, fontSize: "11px", fontWeight: 600, color: "#6b6b73", textTransform: "uppercase", letterSpacing: "1.4px", marginBottom: "12px" } + }, "What sets us apart"), + React.createElement("h2", { + key: "h2", + style: { margin: 0, fontSize: isMobile ? "28px" : "38px", fontWeight: 800, letterSpacing: "-1.1px", lineHeight: 1.05, color: "#f4f4f6" } + }, "Why SecureBit is unique") + ]), + React.createElement("div", { key: "nav", style: { display: "flex", alignItems: "center", gap: "10px", flex: "none" } }, [ + navBtn("prev", () => go(-1), ''), + navBtn("next", () => go(1), '') ]) ]), - // Slider - React.createElement( - "div", - { - key: "slider", - className: "slider", - ref: wrapRef - }, - React.createElement("div", { - className: "track", - ref: trackRef - }, slides.map( - (slide, index) => React.createElement("article", { - key: index, - className: "project-card", - ...index === current ? { active: "" } : {}, - onMouseEnter: () => { - if (window.matchMedia("(hover:hover)").matches) { - activate(index, true); - } - }, - onClick: () => activate(index, true) - }, [ - // Background - React.createElement("div", { - key: "bg", - className: "project-card__bg", - style: { - background: slide.bgImage, - backgroundSize: "cover", - backgroundPosition: "center" - } - }), - // Content - React.createElement("div", { - key: "content", - className: "project-card__content" - }, [ - // Text container - React.createElement("div", { key: "text" }, [ - React.createElement("h3", { - key: "title", - className: "project-card__title" - }, slide.title), - React.createElement("p", { - key: "desc", - className: "project-card__desc" - }, slide.description) - ]) - ]) - ]) - )) - ) + // Accordion + React.createElement("div", { + key: "accordion", + style: { + display: "flex", + flexDirection: isMobile ? "column" : "row", + gap: isMobile ? "12px" : "14px", + height: isMobile ? "auto" : "440px" + } + }, panels) + ]); + return React.createElement("section", { + style: { + width: "100%", + color: "#e8e8eb", + fontFamily: SANS, + padding: isMobile ? "44px 0" : "64px 0", + background: "radial-gradient(1100px 700px at 18% 8%, rgba(240,137,42,0.05), transparent 60%), #0f0f11" + } + }, [ + React.createElement("style", { key: "kf", dangerouslySetInnerHTML: { __html: "@keyframes wuUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}" } }), + inner ]); }; window.UniqueFeatureSlider = UniqueFeatureSlider; -// src/components/ui/SecurityFeatures.jsx -var SecurityFeatures = () => { - const features = [ - { id: "feature1", color: "#00ff88", icon: "fas fa-key accent-green", title: "ECDH P-384 Key Exchange", desc: "Military-grade elliptic curve key exchange" }, - { id: "feature2", color: "#a78bfa", icon: "fas fa-user-shield accent-purple", title: "MITM Protection", desc: "Out-of-band verification against attacks" }, - { id: "feature3", color: "#ff8800", icon: "fas fa-lock accent-orange", title: "AES-GCM 256 Encryption", desc: "Authenticated encryption standard" }, - { id: "feature4", color: "#00ffff", icon: "fas fa-sync-alt accent-cyan", title: "Perfect Forward Secrecy", desc: "Automatic key rotation every 5 minutes" }, - { id: "feature5", color: "#0088ff", icon: "fas fa-signature accent-blue", title: "ECDSA P-384 Signatures", desc: "Digital signatures for message integrity" }, - { id: "feature6", color: "#f87171", icon: "fas fa-shield-alt accent-red", title: "SAS Security", desc: "Revolutionary key exchange & MITM protection" } - ]; - React.useEffect(() => { - const cards = document.querySelectorAll(".card"); - const radius = 200; - const handleMove = (e) => { - cards.forEach((card) => { - const rect = card.getBoundingClientRect(); - const cx = rect.left + rect.width / 2; - const cy = rect.top + rect.height / 2; - const dx = e.clientX - cx; - const dy = e.clientY - cy; - const dist = Math.sqrt(dx * dx + dy * dy); - if (dist < radius) { - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - card.style.setProperty("--x", `${x}px`); - card.style.setProperty("--y", `${y}px`); - card.classList.add("active-glow"); - } else { - card.classList.remove("active-glow"); - } - }); - }; - window.addEventListener("mousemove", handleMove); - return () => window.removeEventListener("mousemove", handleMove); - }, []); - const renderFeature = (f) => React.createElement("div", { - key: f.id, - className: "card p-3 sm:p-4 text-center", - style: { "--color": f.color } - }, [ - React.createElement("div", { key: "icon", className: "w-10 h-10 sm:w-12 sm:h-12 flex items-center justify-center mx-auto mb-2 sm:mb-3 relative z-10" }, [ - React.createElement("i", { className: f.icon }) - ]), - React.createElement("h4", { key: "title", className: "text-xs sm:text-sm font-medium text-primary mb-1 relative z-10" }, f.title), - React.createElement("p", { key: "desc", className: "text-xs text-muted leading-tight relative z-10" }, f.desc) - ]); - return React.createElement("div", { - className: "grid grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 max-w-6xl mx-auto mt-8" - }, features.map(renderFeature)); -}; -window.SecurityFeatures = SecurityFeatures; - -// src/components/ui/Testimonials.jsx -var Testimonials = () => { - const testimonials = [ - { id: "t1", rating: 5, text: "The interface feels modern and smooth. It saves me at least 2 hours every day when managing design tasks." }, - { id: "t2", rating: 5, text: "Finally, a solution that blends speed with simplicity. My team adopted it within a week without training." }, - { id: "t3", rating: 5, text: "I can track progress in real time and get a clear overview of our workflow. It feels empowering." }, - { id: "t4", rating: 5, text: "Our pipeline visibility improved dramatically. I no longer need to manually track updates." }, - { id: "t5", rating: 5, text: "The security-first approach gives me peace of mind. We handle sensitive data with confidence now." }, - { id: "t6", rating: 5, text: "User feedback cycles are now twice as fast. It helps us test and ship features quickly." } - ]; - React.useEffect(() => { - const colUp = document.querySelector(".col-up"); - const colDown = document.querySelector(".col-down"); - const wrapper = document.querySelector(".testimonials-wrapper"); - if (!colUp || !colDown || !wrapper) return; - let paused = false; - const speed = 0.5; - let animationId; - const cloneCards = (container) => { - const cards = Array.from(container.children); - cards.forEach((card) => { - const clone2 = card.cloneNode(true); - container.appendChild(clone2); - }); - }; - cloneCards(colUp); - cloneCards(colDown); - const getHalfHeight = (el) => { - const children = Array.from(el.children); - const halfCount = children.length / 2; - let height = 0; - for (let i = 0; i < halfCount; i++) { - height += children[i].offsetHeight; - if (i < halfCount - 1) height += 24; - } - return height; - }; - let y1 = 0; - const maxScroll1 = getHalfHeight(colUp); - const maxScroll2 = getHalfHeight(colDown); - let y2 = -maxScroll2; - function animate() { - if (!paused) { - y1 -= speed; - y2 += speed; - if (Math.abs(y1) >= maxScroll1) { - y1 = 0; - } - if (y2 >= 0) { - y2 = -maxScroll2; - } - colUp.style.transform = `translateY(${y1}px)`; - colDown.style.transform = `translateY(${y2}px)`; - } - animationId = requestAnimationFrame(animate); - } - animate(); - const handleMouseEnter = () => { - paused = true; - }; - const handleMouseLeave = () => { - paused = false; - }; - wrapper.addEventListener("mouseenter", handleMouseEnter); - wrapper.addEventListener("mouseleave", handleMouseLeave); - return () => { - cancelAnimationFrame(animationId); - wrapper.removeEventListener("mouseenter", handleMouseEnter); - wrapper.removeEventListener("mouseleave", handleMouseLeave); - }; - }, []); - const renderCard = (t, index) => /* @__PURE__ */ React.createElement("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" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center mb-2 text-yellow-400" }, "\u2605".repeat(Math.floor(t.rating)), /* @__PURE__ */ React.createElement("span", { className: "ml-2 text-secondary" }, t.rating.toFixed(1))), /* @__PURE__ */ React.createElement("p", { className: "text-secondary mb-3" }, t.text)); - return /* @__PURE__ */ React.createElement("section", { className: "py-14 px-6 bg-transparent" }, /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-1 lg:grid-cols-5 gap-12 max-w-7xl mx-auto items-center" }, /* @__PURE__ */ React.createElement("div", { className: "lg:col-span-2 flex flex-col justify-center" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm text-secondary mb-2" }, "Testimonials"), /* @__PURE__ */ React.createElement("h2", { className: "text-2xl sm:text-3xl font-bold text-white mb-4 leading-snug" }, "What our users are saying"), /* @__PURE__ */ React.createElement("p", { className: "text-secondary text-sm" }, "We continuously listen to our community and improve every day.")), /* @__PURE__ */ React.createElement("div", { className: "lg:col-span-3 testimonials-wrapper flex gap-6 overflow-hidden relative h-[420px]" }, /* @__PURE__ */ React.createElement("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" }), /* @__PURE__ */ React.createElement("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" }), /* @__PURE__ */ React.createElement("div", { className: "col-up flex flex-col gap-6" }, testimonials.map((t, i) => renderCard(t, i))), /* @__PURE__ */ React.createElement("div", { className: "col-down flex flex-col gap-6" }, testimonials.map((t, i) => renderCard(t, i)))))); -}; -window.Testimonials = Testimonials; - -// src/components/ui/ComparisonTable.jsx -var ComparisonTable = () => { - const [selectedFeature, setSelectedFeature] = React.useState(null); - const messengers = [ - { - name: "SecureBit.chat", - logo: /* @__PURE__ */ React.createElement("div", { className: "w-8 h-8 bg-orange-500/10 border border-orange-500/20 rounded-lg flex items-center justify-center" }, /* @__PURE__ */ React.createElement("i", { className: "fas fa-shield-halved text-orange-400" })), - type: "P2P WebRTC", - version: "Latest", - color: "orange" - }, - { - name: "Signal", - logo: /* @__PURE__ */ React.createElement("svg", { className: "w-8 h-8", viewBox: "0 0 122.88 122.31", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ React.createElement("path", { className: "fill-blue-500", d: "M27.75,0H95.13a27.83,27.83,0,0,1,27.75,27.75V94.57a27.83,27.83,0,0,1-27.75,27.74H27.75A27.83,27.83,0,0,1,0,94.57V27.75A27.83,27.83,0,0,1,27.75,0Z" }), /* @__PURE__ */ React.createElement("path", { className: "fill-white", d: "M61.44,25.39A35.76,35.76,0,0,0,31.18,80.18L27.74,94.86l14.67-3.44a35.77,35.77,0,1,0,19-66Z" })), - type: "Centralized", - version: "Latest", - color: "blue" - }, - { - name: "Threema", - logo: /* @__PURE__ */ React.createElement("svg", { className: "w-8 h-8", viewBox: "0 0 122.88 122.88", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ React.createElement("rect", { width: "122.88", height: "122.88", rx: "18.43", fill: "#474747" }), /* @__PURE__ */ React.createElement("path", { fill: "#FFFFFF", d: "M44.26,78.48l-19.44,4.8l4.08-16.56c-4.08-5.28-6.48-12-6.48-18.96c0-18.96,17.52-34.32,39.12-34.32c21.6,0,39.12,15.36,39.12,34.32c0,18.96-17.52,34.32-39.12,34.32c-6,0-12-1.2-17.04-3.36L44.26,78.48z M50.26,44.64h-0.48c-0.96,0-1.68,0.72-1.44,1.68v15.6c0,0.96,0.72,1.68,1.68,1.68l23.04,0c0.96,0,1.68-0.72,1.68-1.68v-15.6c0-0.96-0.72-1.68-1.68-1.68h-0.48v-4.32c0-6-5.04-11.04-11.04-11.04S50.5,34.32,50.5,40.32v4.32H50.26z M68.02,44.64h-13.2v-4.32c0-3.6,2.88-6.72,6.72-6.72c3.6,0,6.72,2.88,6.72,6.72v4.32H68.02z" }), /* @__PURE__ */ React.createElement("circle", { cx: "37.44", cy: "97.44", r: "6.72", fill: "#3fe669" }), /* @__PURE__ */ React.createElement("circle", { cx: "61.44", cy: "97.44", r: "6.72", fill: "#3fe669" }), /* @__PURE__ */ React.createElement("circle", { cx: "85.44", cy: "97.44", r: "6.72", fill: "#3fe669" })), - type: "Centralized", - version: "Latest", - color: "green" - }, - { - name: "Session", - logo: /* @__PURE__ */ React.createElement("svg", { className: "w-8 h-8", viewBox: "0 0 1024 1024", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ React.createElement("rect", { width: "1024", height: "1024", fill: "#333132" }), /* @__PURE__ */ React.createElement("path", { fill: "#00f782", d: "M431 574.8c-.8-7.4-6.7-8.2-10.8-10.6-13.6-7.9-27.5-15.4-41.3-23l-22.5-12.3c-8.5-4.7-17.1-9.2-25.6-14.1-10.5-6-21-11.9-31.1-18.6-18.9-12.5-33.8-29.1-46.3-48.1-8.3-12.6-14.8-26.1-19.2-40.4-6.7-21.7-10.8-44.1-7.8-66.8 1.8-14 4.6-28 9.7-41.6 7.8-20.8 19.3-38.8 34.2-54.8 9.8-10.6 21.2-19.1 33.4-26.8 14.7-9.3 30.7-15.4 47.4-19 13.8-3 28.1-4.3 42.2-4.4 89.9-.4 179.7-.3 269.6 0 12.6 0 25.5 1 37.7 4.1 24.3 6.2 45.7 18.2 63 37 11.2 12.2 20.4 25.8 25.8 41.2 7.3 20.7 12.3 42.1 6.7 64.4-2.1 8.5-2.7 17.5-6.1 25.4-4.7 10.9-10.8 21.2-17.2 31.2-8.7 13.5-20.5 24.3-34.4 32.2-10.1 5.7-21 10.2-32 14.3-18.1 6.7-37.2 5-56.1 5.2-17.2.2-34.5 0-51.7.1-1.7 0-3.4 1.2-5.1 1.9 1.3 1.8 2.1 4.3 3.9 5.3 13.5 7.8 27.2 15.4 40.8 22.9 11 6 22.3 11.7 33.2 17.9 15.2 8.5 30.2 17.4 45.3 26.1 19.3 11.1 34.8 26.4 47.8 44.3 9.7 13.3 17.2 27.9 23 43.5 6.1 16.6 9.2 33.8 10.4 51.3.6 9.1-.7 18.5-1.9 27.6-1.2 9.1-2.7 18.4-5.6 27.1-3.3 10.2-7.4 20.2-12.4 29.6-8.4 15.7-19.6 29.4-32.8 41.4-12.7 11.5-26.8 20.6-42.4 27.6-22.9 10.3-46.9 14.4-71.6 14.5-89.7.3-179.4.2-269.1-.1-12.6 0-25.5-1-37.7-3.9-24.5-5.7-45.8-18-63.3-36.4-11.6-12.3-20.2-26.5-26.6-41.9-2.7-6.4-4.1-13.5-5.4-20.4-1.5-8.1-2.8-16.3-3.1-24.5-.6-15.7 2.8-30.9 8.2-45.4 8.2-22 21.7-40.6 40.2-55.2 10-7.9 21.3-13.7 33.1-18.8 16.6-7.2 34-8.1 51.4-8.5 21.9-.5 43.9-.1 65.9-.1 1.9-.1 3.9-.3 6.2-.4zm96.3-342.4c0 .1 0 .1 0 0-48.3.1-96.6-.6-144.9.5-13.5.3-27.4 3.9-40.1 8.7-14.9 5.6-28.1 14.6-39.9 25.8-20.2 19-32.2 42.2-37.2 68.9-3.6 19-1.4 38.1 4.1 56.5 4.1 13.7 10.5 26.4 18.5 38.4 14.8 22.2 35.7 36.7 58.4 49.2 11 6.1 22.2 11.9 33.2 18 13.5 7.5 26.9 15.1 40.4 22.6 13.1 7.3 26.2 14.5 39.2 21.7 9.7 5.3 19.4 10.7 29.1 16.1 2.9 1.6 4.1.2 4.5-2.4.3-2 .3-4 .3-6.1v-58.8c0-19.9.1-39.9 0-59.8 0-6.6 1.7-12.8 7.6-16.1 3.5-2 8.2-2.8 12.4-2.8 50.3-.2 100.7-.2 151-.1 19.8 0 38.3-4.4 55.1-15.1 23.1-14.8 36.3-36.3 40.6-62.9 3.4-20.8-1-40.9-12.4-58.5-17.8-27.5-43.6-43-76.5-43.6-47.8-.8-95.6-.2-143.4-.2zm-30.6 559.7c45.1 0 90.2-.2 135.3.1 18.9.1 36.6-3.9 53.9-11.1 18.4-7.7 33.6-19.8 46.3-34.9 9.1-10.8 16.2-22.9 20.8-36.5 4.2-12.4 7.4-24.7 7.3-37.9-.1-10.3.2-20.5-3.4-30.5-2.6-7.2-3.4-15.2-6.4-22.1-3.9-8.9-8.9-17.3-14-25.5-12.9-20.8-31.9-34.7-52.8-46.4-10.6-5.9-21.2-11.6-31.8-17.5-10.3-5.7-20.4-11.7-30.7-17.4-11.2-6.1-22.5-11.9-33.7-18-16.6-9.1-33.1-18.4-49.8-27.5-4.9-2.7-6.1-1.9-6.4 3.9-.1 2-.1 4.1-.1 6.1v114.5c0 14.8-5.6 20.4-20.4 20.4-47.6.1-95.3-.1-142.9.2-10.5.1-21.1 1.4-31.6 2.8-16.5 2.2-30.5 9.9-42.8 21-17 15.5-27 34.7-29.4 57.5-1.1 10.9-.4 21.7 2.9 32.5 3.7 12.3 9.2 23.4 17.5 33 19.2 22.1 43.4 33.3 72.7 33.3 46.6.1 93 0 139.5 0z" })), - type: "Onion Network", - version: "Latest", - color: "cyan" - } - ]; - const features = [ - { - name: "Security Architecture", - lockbit: { status: "trophy", detail: "18-layer military-grade defense system with complete ASN.1 validation" }, - signal: { status: "check", detail: "Signal Protocol with double ratchet" }, - threema: { status: "check", detail: "Standard security implementation" }, - session: { status: "check", detail: "Modified Signal Protocol + Onion routing" } - }, - { - name: "Cryptography", - lockbit: { status: "trophy", detail: "ECDH P-384 + AES-GCM 256 + ECDSA P-384" }, - signal: { status: "check", detail: "Signal Protocol + Double Ratchet" }, - threema: { status: "check", detail: "NaCl + XSalsa20 + Poly1305" }, - session: { status: "check", detail: "Modified Signal Protocol" } - }, - { - name: "Perfect Forward Secrecy", - lockbit: { status: "trophy", detail: "Auto rotation every 5 minutes or 100 messages" }, - signal: { status: "check", detail: "Double Ratchet algorithm" }, - threema: { status: "warning", detail: "Partial (group chats)" }, - session: { status: "check", detail: "Session Ratchet algorithm" } - }, - { - name: "Architecture", - lockbit: { status: "trophy", detail: "Pure P2P WebRTC without servers" }, - signal: { status: "times", detail: "Centralized Signal servers" }, - threema: { status: "times", detail: "Threema servers in Switzerland" }, - session: { status: "warning", detail: "Onion routing via network nodes" } - }, - { - name: "Registration Anonymity", - lockbit: { status: "trophy", detail: "No registration required, instant anonymous channels" }, - signal: { status: "times", detail: "Phone number required" }, - threema: { status: "check", detail: "ID generated locally" }, - session: { status: "check", detail: "Random session ID" } - }, - { - name: "Metadata Protection", - lockbit: { status: "trophy", detail: "Full metadata encryption + traffic obfuscation" }, - signal: { status: "warning", detail: "Sealed Sender (partial)" }, - threema: { status: "warning", detail: "Minimal metadata" }, - session: { status: "check", detail: "Onion routing hides metadata" } - }, - { - name: "Traffic Obfuscation", - lockbit: { status: "trophy", detail: "Fake traffic + pattern masking + packet padding" }, - signal: { status: "times", detail: "No traffic obfuscation" }, - threema: { status: "times", detail: "No traffic obfuscation" }, - session: { status: "check", detail: "Onion routing provides obfuscation" } - }, - { - name: "Open Source", - lockbit: { status: "trophy", detail: "100% open + auditable + MIT license" }, - signal: { status: "check", detail: "Fully open" }, - threema: { status: "warning", detail: "Only clients open" }, - session: { status: "check", detail: "Fully open" } - }, - { - name: "MITM Protection", - lockbit: { status: "trophy", detail: "Out-of-band verification + mutual auth + ECDSA" }, - signal: { status: "check", detail: "Safety numbers verification" }, - threema: { status: "check", detail: "QR code scanning" }, - session: { status: "warning", detail: "Basic key verification" } - }, - { - name: "Censorship Resistance", - lockbit: { status: "trophy", detail: "Impossible to block P2P + no servers to target" }, - signal: { status: "warning", detail: "Blocked in authoritarian countries" }, - threema: { status: "warning", detail: "May be blocked" }, - session: { status: "check", detail: "Onion routing bypasses blocks" } - }, - { - name: "Data Storage", - lockbit: { status: "trophy", detail: "Zero data storage - only in browser memory" }, - signal: { status: "warning", detail: "Local database storage" }, - threema: { status: "warning", detail: "Local + optional backup" }, - session: { status: "warning", detail: "Local database storage" } - }, - { - name: "Key Security", - lockbit: { status: "trophy", detail: "Non-extractable keys + hardware protection" }, - signal: { status: "check", detail: "Secure key storage" }, - threema: { status: "check", detail: "Local key storage" }, - session: { status: "check", detail: "Secure key storage" } - }, - { - name: "Post-Quantum Roadmap", - lockbit: { status: "check", detail: "Planned v5.0 - CRYSTALS-Kyber/Dilithium" }, - signal: { status: "warning", detail: "PQXDH in development" }, - threema: { status: "times", detail: "Not announced" }, - session: { status: "times", detail: "Not announced" } - } - ]; - const getStatusIcon = (status) => { - const statusMap = { - "trophy": { icon: "fa-trophy", color: "accent-orange" }, - "check": { icon: "fa-check", color: "text-green-300" }, - "warning": { icon: "fa-exclamation-triangle", color: "text-yellow-300" }, - "times": { icon: "fa-times", color: "text-red-300" } - }; - return statusMap[status] || { icon: "fa-question", color: "text-gray-400" }; - }; - const toggleFeatureDetail = (index) => { - setSelectedFeature(selectedFeature === index ? null : index); - }; - return /* @__PURE__ */ React.createElement("div", { className: "mt-16" }, /* @__PURE__ */ React.createElement("div", { className: "text-center mb-8" }, /* @__PURE__ */ React.createElement("h3", { className: "text-3xl font-bold text-white mb-3" }, "Enhanced Security Edition Comparison"), /* @__PURE__ */ React.createElement("p", { className: "text-gray-400 max-w-2xl mx-auto mb-4" }, "Enhanced Security Edition vs leading secure messengers")), /* @__PURE__ */ React.createElement("div", { className: "max-w-7xl mx-auto" }, /* @__PURE__ */ React.createElement("div", { className: "md:hidden p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg mb-4" }, /* @__PURE__ */ React.createElement("p", { className: "text-yellow-400 text-sm text-center" }, /* @__PURE__ */ React.createElement("i", { className: "fas fa-lightbulb mr-2" }), "Rotate your device horizontally for better viewing")), /* @__PURE__ */ React.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ React.createElement( - "table", - { - className: "w-full border-collapse rounded-xl overflow-hidden shadow-2xl", - style: { backgroundColor: "rgba(42, 43, 42, 0.9)" } - }, - /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", { className: "bg-black-table" }, /* @__PURE__ */ React.createElement("th", { className: "text-left p-4 border-b border-gray-600 text-white font-bold min-w-[240px]" }, "Security Criterion"), messengers.map((messenger, index) => /* @__PURE__ */ React.createElement("th", { key: `messenger-${index}`, className: "text-center p-4 border-b border-gray-600 min-w-[160px]" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col items-center" }, /* @__PURE__ */ React.createElement("div", { className: "mb-2" }, messenger.logo), /* @__PURE__ */ React.createElement("div", { className: `text-sm font-bold ${messenger.color === "orange" ? "text-orange-400" : messenger.color === "blue" ? "text-blue-400" : messenger.color === "green" ? "text-green-400" : "text-cyan-400"}` }, messenger.name), /* @__PURE__ */ React.createElement("div", { className: "text-xs text-gray-400" }, messenger.type), /* @__PURE__ */ React.createElement("div", { className: "text-xs text-gray-500 mt-1" }, messenger.version)))))), - /* @__PURE__ */ React.createElement("tbody", null, features.map((feature, featureIndex) => /* @__PURE__ */ React.createElement(React.Fragment, { key: `feature-${featureIndex}` }, /* @__PURE__ */ React.createElement( - "tr", - { - className: `border-b border-gray-700/30 transition-all duration-200 cursor-pointer hover:bg-[rgb(20_20_20_/30%)] ${selectedFeature === featureIndex ? "bg-[rgb(20_20_20_/50%)]" : ""}`, - onClick: () => toggleFeatureDetail(featureIndex) - }, - /* @__PURE__ */ React.createElement("td", { className: "p-4 text-white font-semibold" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React.createElement("span", null, feature.name), /* @__PURE__ */ React.createElement("i", { className: `fas fa-chevron-${selectedFeature === featureIndex ? "up" : "down"} text-xs text-gray-400 opacity-60 transition-all duration-200` }))), - /* @__PURE__ */ React.createElement("td", { className: "p-4 text-center" }, /* @__PURE__ */ React.createElement("i", { className: `fas ${getStatusIcon(feature.lockbit.status).icon} ${getStatusIcon(feature.lockbit.status).color} text-2xl` })), - /* @__PURE__ */ React.createElement("td", { className: "p-4 text-center" }, /* @__PURE__ */ React.createElement("i", { className: `fas ${getStatusIcon(feature.signal.status).icon} ${getStatusIcon(feature.signal.status).color} text-2xl` })), - /* @__PURE__ */ React.createElement("td", { className: "p-4 text-center" }, /* @__PURE__ */ React.createElement("i", { className: `fas ${getStatusIcon(feature.threema.status).icon} ${getStatusIcon(feature.threema.status).color} text-2xl` })), - /* @__PURE__ */ React.createElement("td", { className: "p-4 text-center" }, /* @__PURE__ */ React.createElement("i", { className: `fas ${getStatusIcon(feature.session.status).icon} ${getStatusIcon(feature.session.status).color} text-2xl` })) - ), selectedFeature === featureIndex && /* @__PURE__ */ React.createElement("tr", { className: "border-b border-gray-700/30 bg-gradient-to-r from-gray-800/20 to-gray-900/20" }, /* @__PURE__ */ React.createElement("td", { className: "p-4 text-xs text-gray-400 font-medium" }, "Technical Details:"), /* @__PURE__ */ React.createElement("td", { className: "p-4 text-center" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs text-orange-300 font-medium leading-relaxed" }, feature.lockbit.detail)), /* @__PURE__ */ React.createElement("td", { className: "p-4 text-center" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs text-blue-300 leading-relaxed" }, feature.signal.detail)), /* @__PURE__ */ React.createElement("td", { className: "p-4 text-center" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs text-green-300 leading-relaxed" }, feature.threema.detail)), /* @__PURE__ */ React.createElement("td", { className: "p-4 text-center" }, /* @__PURE__ */ React.createElement("div", { className: "text-xs text-cyan-300 leading-relaxed" }, feature.session.detail)))))) - )), /* @__PURE__ */ React.createElement("div", { className: "mt-8 grid grid-cols-2 md:grid-cols-4 gap-4 max-w-5xl mx-auto" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-center p-4 bg-orange-500/10 rounded-xl hover:bg-orange-500/40 transition-colors" }, /* @__PURE__ */ React.createElement("i", { className: "fas fa-trophy text-orange-400 mr-2 text-xl" }), /* @__PURE__ */ React.createElement("span", { className: "text-orange-300 text-sm font-bold" }, "Category Leader")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-center p-4 bg-green-500/10 rounded-xl hover:bg-green-600/40 transition-colors" }, /* @__PURE__ */ React.createElement("i", { className: "fas fa-check text-green-300 mr-2 text-xl" }), /* @__PURE__ */ React.createElement("span", { className: "text-green-200 text-sm font-bold" }, "Excellent")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-center p-4 bg-yellow-500/10 rounded-xl hover:bg-yellow-600/40 transition-colors" }, /* @__PURE__ */ React.createElement("i", { className: "fas fa-exclamation-triangle text-yellow-300 mr-2 text-xl" }), /* @__PURE__ */ React.createElement("span", { className: "text-yellow-200 text-sm font-bold" }, "Partial/Limited")), /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-center p-4 bg-red-500/10 rounded-xl hover:bg-red-600/40 transition-colors" }, /* @__PURE__ */ React.createElement("i", { className: "fas fa-times text-red-300 mr-2 text-xl" }), /* @__PURE__ */ React.createElement("span", { className: "text-red-200 text-sm font-bold" }, "Not Available"))))); -}; -window.ComparisonTable = ComparisonTable; - // src/components/ui/Roadmap.jsx function Roadmap() { - const [selectedPhase, setSelectedPhase] = React.useState(null); - const phases = [ + const [isMobile, setIsMobile] = React.useState( + typeof window !== "undefined" && window.matchMedia("(max-width:767px)").matches + ); + React.useEffect(() => { + const mq = window.matchMedia("(max-width:767px)"); + const onChange = () => setIsMobile(mq.matches); + mq.addEventListener ? mq.addEventListener("change", onChange) : mq.addListener(onChange); + return () => { + mq.removeEventListener ? mq.removeEventListener("change", onChange) : mq.removeListener(onChange); + }; + }, []); + const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace"; + const SANS = "'Manrope', system-ui, -apple-system, sans-serif"; + const DATA = [ { - version: "v1.0", + v: "v1.0", title: "Start of Development", - status: "done", + sub: "Idea, prototype, and infrastructure setup", + status: "released", date: "Early 2025", - description: "Idea, prototype, and infrastructure setup", - features: [ - "Concept and requirements formation", - "Stack selection: WebRTC, P2P, cryptography", - "First messaging prototypes", - "Repository creation and CI", - "Basic encryption architecture", - "UX/UI design" - ] + features: ["Concept and requirements formation", "Stack selection: WebRTC, P2P, cryptography", "First messaging prototypes", "Repository creation and CI", "Basic encryption architecture", "UX/UI design"] }, { - version: "v1.5", + v: "v1.5", title: "Alpha Release", - status: "done", + sub: "First public alpha: basic chat and key exchange", + status: "released", date: "Spring 2025", - description: "First public alpha: basic chat and key exchange", - features: [ - "Basic P2P messaging via WebRTC", - "Simple E2E encryption (demo scheme)", - "Stable signaling and reconnection", - "Minimal UX for testing", - "Feedback collection from early testers" - ] + features: ["Basic P2P messaging via WebRTC", "Simple E2E encryption (demo scheme)", "Stable signaling and reconnection", "Minimal UX for testing", "Feedback collection from early testers"] }, { - version: "v2.0", + v: "v2.0", title: "Security Hardened", - status: "done", + sub: "Security strengthening and stable branch release", + status: "released", date: "Summer 2025", - description: "Security strengthening and stable branch release", - features: [ - "ECDH/ECDSA implementation in production", - "Perfect Forward Secrecy and key rotation", - "Improved authentication checks", - "File encryption and large payload transfers", - "Audit of basic cryptoprocesses" - ] + features: ["ECDH/ECDSA implementation in production", "Perfect Forward Secrecy and key rotation", "Improved authentication checks", "File encryption and large payload transfers", "Audit of basic cryptoprocesses"] }, { - version: "v3.0", + v: "v3.0", title: "Scaling & Stability", - status: "done", + sub: "Network scaling and stability improvements", + status: "released", date: "Fall 2025", - description: "Network scaling and stability improvements", - features: [ - "Optimization of P2P connections and NAT traversal", - "Reconnection mechanisms and message queues", - "Reduced battery consumption on mobile", - "Support for multi-device synchronization", - "Monitoring and logging tools for developers" - ] + features: ["Optimization of P2P connections and NAT traversal", "Reconnection mechanisms and message queues", "Reduced battery consumption on mobile", "Multi-device synchronization support", "Monitoring and logging tools for developers"] }, { - version: "v3.5", + v: "v3.5", title: "Privacy-first Release", - status: "done", + sub: "Focus on privacy: minimizing metadata", + status: "released", date: "Winter 2025", - description: "Focus on privacy: minimizing metadata", - features: [ - "Metadata protection and fingerprint reduction", - "Experiments with onion routing and DHT", - "Options for anonymous connections", - "Preparation for open code audit", - "Improved user verification processes" - ] + features: ["Metadata protection and fingerprint reduction", "Experiments with onion routing and DHT", "Options for anonymous connections", "Preparation for open code audit", "Improved user verification processes"] }, - // current and future phases { - version: "v4.5", + v: "v4.5", title: "Enhanced Security Edition", - status: "done", - date: "Now", - description: "Version with ECDH + DTLS + SAS security, 18-layer military-grade cryptography and complete ASN.1 validation", - features: [ - "ECDH + DTLS + SAS triple-layer security", - "ECDH P-384 + AES-GCM 256-bit encryption", - "DTLS fingerprint verification", - "SAS (Short Authentication String) verification", - "Perfect Forward Secrecy with key rotation", - "Enhanced MITM attack prevention", - "Complete ASN.1 DER validation", - "OID and EC point verification", - "SPKI structure validation", - "P2P WebRTC architecture", - "Metadata protection", - "100% open source code" - ] + sub: "18-layer military-grade cryptography with complete ASN.1 validation", + status: "released", + date: "Late 2025", + features: ["ECDH + DTLS + SAS triple-layer security", "ECDH P-384 + AES-GCM 256-bit encryption", "DTLS fingerprint verification", "SAS (Short Authentication String) verification", "Perfect Forward Secrecy with key rotation", "Enhanced MITM attack prevention", "Complete ASN.1 DER validation", "OID and EC point verification", "SPKI structure validation", "P2P WebRTC architecture", "Metadata protection", "100% open source code"] }, { - version: "v4.7", + v: "v4.7", title: "Desktop Edition", + sub: "Native desktop apps for Windows, macOS, and Linux", status: "current", date: "Now", - description: "Native desktop applications for Windows, macOS, and Linux", - features: [ - "Windows desktop app (Tauri v2)", - "macOS desktop app (Tauri v2)", - "Linux AppImage support (Tauri v2)", - "Real-time notifications", - "Automatic reconnection", - "Cross-device synchronization", - "Improved UX/UI", - "Support for files up to 100MB" - ] + features: ["Windows desktop app (Tauri v2)", "macOS desktop app (Tauri v2)", "Linux AppImage support (Tauri v2)", "Real-time notifications", "Automatic reconnection", "Cross-device synchronization", "Improved UX/UI", "Support for files up to 100MB"] }, { - version: "v5.0", + v: "v5.0", title: "Mobile Edition", - status: "development", + sub: "Native mobile apps for iOS and Android", + status: "dev", date: "Q1 2026", - description: "Native mobile applications for iOS and Android", - features: [ - "iOS native app (Swift/SwiftUI)", - "Android native app (Kotlin/Jetpack Compose)", - "PWA support for mobile browsers", - "Real-time push notifications", - "Battery optimization", - "Mobile-optimized UX/UI", - "Offline message queuing", - "Biometric authentication" - ] + features: ["iOS native app (Swift/SwiftUI)", "Android native app (Kotlin/Jetpack Compose)", "PWA support for mobile browsers", "Real-time push notifications", "Battery optimization", "Mobile-optimized UX/UI", "Offline message queuing", "Biometric authentication"] }, { - version: "v5.5", + v: "v5.5", title: "Quantum-Resistant Edition", + sub: "Protection against quantum computers", status: "planned", date: "Q2 2026", - description: "Protection against quantum computers", - features: [ - "Post-quantum cryptography CRYSTALS-Kyber", - "SPHINCS+ digital signatures", - "Hybrid scheme: classic + PQ", - "Quantum-safe key exchange", - "Updated hashing algorithms", - "Migration of existing sessions", - "Compatibility with v4.x", - "Quantum-resistant protocols" - ] + features: ["Post-quantum cryptography CRYSTALS-Kyber", "SPHINCS+ digital signatures", "Hybrid scheme: classic + PQ", "Quantum-safe key exchange", "Updated hashing algorithms", "Migration of existing sessions", "Compatibility with v4.x", "Quantum-resistant protocols"] }, { - version: "v6.0", + v: "v6.0", title: "Group Communications", + sub: "Group chats with preserved privacy", status: "planned", date: "Q4 2026", - description: "Group chats with preserved privacy", - features: [ - "P2P group connections up to 8 participants", - "Mesh networking for groups", - "Signal Double Ratchet for groups", - "Anonymous groups without metadata", - "Ephemeral groups (disappear after session)", - "Cryptographic group administration", - "Group member auditing" - ] + features: ["P2P group connections up to 8 participants", "Mesh networking for groups", "Signal Double Ratchet for groups", "Anonymous groups without metadata", "Ephemeral groups (disappear after session)", "Cryptographic group administration", "Group member auditing"] }, { - version: "v6.5", + v: "v6.5", title: "Decentralized Network", + sub: "Fully decentralized network", status: "research", date: "2027", - description: "Fully decentralized network", - features: [ - "LockBit node mesh network", - "DHT for peer discovery", - "Built-in onion routing", - "Tokenomics and node incentives", - "Governance via DAO", - "Interoperability with other networks", - "Cross-platform compatibility", - "Self-healing network" - ] + features: ["Node mesh network", "DHT for peer discovery", "Built-in onion routing", "Tokenomics and node incentives", "Governance via DAO", "Interoperability with other networks", "Cross-platform compatibility", "Self-healing network"] }, { - version: "v7.0", + v: "v7.0", title: "AI Privacy Assistant", + sub: "AI for privacy and security", status: "research", date: "2028+", - description: "AI for privacy and security", - features: [ - "Local AI threat analysis", - "Automatic MITM detection", - "Adaptive cryptography", - "Personalized security recommendations", - "Zero-knowledge machine learning", - "Private AI assistant", - "Predictive security", - "Autonomous attack protection" - ] + features: ["Local AI threat analysis", "Automatic MITM detection", "Adaptive cryptography", "Personalized security recommendations", "Zero-knowledge machine learning", "Private AI assistant", "Predictive security", "Autonomous attack protection"] } ]; - const getStatusConfig = (status) => { - switch (status) { - case "current": - return { - color: "green", - bgClass: "bg-green-500/10 border-green-500/20", - textClass: "text-green-400", - icon: "fas fa-check-circle", - label: "Current Version" - }; - case "development": - return { - color: "orange", - bgClass: "bg-orange-500/10 border-orange-500/20", - textClass: "text-orange-400", - icon: "fas fa-code", - label: "In Development" - }; - case "planned": - return { - color: "blue", - bgClass: "bg-blue-500/10 border-blue-500/20", - textClass: "text-blue-400", - icon: "fas fa-calendar-alt", - label: "Planned" - }; - case "research": - return { - color: "purple", - bgClass: "bg-purple-500/10 border-purple-500/20", - textClass: "text-purple-400", - icon: "fas fa-flask", - label: "Research" - }; - case "done": - return { - color: "gray", - bgClass: "bg-gray-500/10 border-gray-500/20", - textClass: "text-gray-300", - icon: "fas fa-flag-checkered", - label: "Released" - }; - default: - return { - color: "gray", - bgClass: "bg-gray-500/10 border-gray-500/20", - textClass: "text-gray-400", - icon: "fas fa-question", - label: "Unknown" - }; + const META = { + released: { word: "Released", color: "#3ecf8e", line: "rgba(62,207,142,0.32)" }, + current: { word: "Current", color: "#f0892a", line: "rgba(240,137,42,0.32)" }, + dev: { word: "In development", color: "#e3b341", line: "rgba(255,255,255,0.08)" }, + planned: { word: "Planned", color: "#8a8a92", line: "rgba(255,255,255,0.08)" }, + research: { word: "Research", color: "#6b6b73", line: "rgba(255,255,255,0.08)" } + }; + const [open, setOpen] = React.useState({}); + const isOpen = (i) => open[i] === void 0 ? DATA[i].status === "current" : open[i]; + const toggle = (i) => setOpen((s) => ({ ...s, [i]: !isOpen(i) })); + const hexA = (hex, a) => { + const n = parseInt(hex.slice(1), 16); + return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`; + }; + const total = DATA.length; + const shipped = DATA.filter((d) => d.status === "released" || d.status === "current").length; + const upcoming = total - shipped; + const shippedPct = (shipped / total * 100).toFixed(1) + "%"; + const renderNode = (status) => { + if (status === "released") { + return /* @__PURE__ */ React.createElement("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 } }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "#3ecf8e", strokeWidth: "2.4", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M5 13l4 4 10-11" }))); } + if (status === "current") { + return /* @__PURE__ */ React.createElement("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" } }, /* @__PURE__ */ React.createElement("span", { style: { width: "9px", height: "9px", borderRadius: "50%", background: "#f0892a" } })); + } + if (status === "dev") { + return /* @__PURE__ */ React.createElement("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 } }, /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "#e3b341", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M12 3a9 9 0 1 0 9 9" }))); + } + return /* @__PURE__ */ React.createElement("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 } }, /* @__PURE__ */ React.createElement("span", { style: { width: "7px", height: "7px", borderRadius: "50%", background: META[status].color } })); }; - const togglePhaseDetail = (index) => { - setSelectedPhase(selectedPhase === index ? null : index); - }; - return /* @__PURE__ */ React.createElement("div", { key: "roadmap-section", className: "mt-16 px-4 sm:px-0" }, /* @__PURE__ */ React.createElement("div", { key: "section-header", className: "text-center mb-12" }, /* @__PURE__ */ React.createElement("h3", { key: "title", className: "text-2xl font-semibold text-primary mb-3" }, "Development Roadmap"), /* @__PURE__ */ React.createElement("p", { key: "subtitle", className: "text-secondary max-w-2xl mx-auto mb-6" }, "Evolution of SecureBit.chat : from initial development to quantum-resistant decentralized network with complete ASN.1 validation")), /* @__PURE__ */ React.createElement("div", { key: "roadmap-container", className: "max-w-6xl mx-auto" }, /* @__PURE__ */ React.createElement("div", { key: "timeline", className: "relative" }, /* @__PURE__ */ React.createElement("div", { key: "phases", className: "space-y-8" }, phases.map((phase, index) => { - const statusConfig = getStatusConfig(phase.status); - const isExpanded = selectedPhase === index; - return /* @__PURE__ */ React.createElement("div", { key: `phase-${index}`, className: "relative" }, /* @__PURE__ */ React.createElement( - "button", - { - type: "button", - "aria-expanded": isExpanded, - onClick: () => togglePhaseDetail(index), - key: `phase-button-${index}`, - className: `card-minimal rounded-xl p-4 text-left w-full transition-all duration-300 ${isExpanded ? "ring-2 ring-" + statusConfig.color + "-500/30" : ""}` - }, - /* @__PURE__ */ React.createElement( - "div", - { - key: "phase-header", - className: "flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4 space-y-2 sm:space-y-0" - }, - /* @__PURE__ */ React.createElement( - "div", - { - key: "phase-info", - className: "flex flex-col sm:flex-row sm:items-center sm:space-x-4" - }, - /* @__PURE__ */ React.createElement( - "div", - { - key: "version-badge", - className: `px-3 py-1 ${statusConfig.bgClass} border rounded-lg mb-2 sm:mb-0` - }, - /* @__PURE__ */ React.createElement( - "span", - { - key: "version", - className: `${statusConfig.textClass} font-bold text-sm` - }, - phase.version - ) - ), - /* @__PURE__ */ React.createElement("div", { key: "title-section" }, /* @__PURE__ */ React.createElement( - "h4", - { - key: "title", - className: "text-lg font-semibold text-primary" - }, - phase.title - ), /* @__PURE__ */ React.createElement( - "p", - { - key: "description", - className: "text-secondary text-sm" - }, - phase.description - )) - ), - /* @__PURE__ */ React.createElement( - "div", - { - key: "phase-meta", - className: "flex items-center space-x-3 text-sm text-gray-400 font-medium" - }, - /* @__PURE__ */ React.createElement( - "div", - { - key: "status-badge", - className: `flex items-center px-3 py-1 ${statusConfig.bgClass} border rounded-lg` - }, - /* @__PURE__ */ React.createElement( - "i", - { - key: "status-icon", - className: `${statusConfig.icon} ${statusConfig.textClass} mr-2 text-xs` - } - ), - /* @__PURE__ */ React.createElement( - "span", - { - key: "status-text", - className: `${statusConfig.textClass} text-xs font-medium` - }, - statusConfig.label - ) - ), - /* @__PURE__ */ React.createElement("div", { key: "date" }, phase.date), - /* @__PURE__ */ React.createElement( - "i", - { - key: "expand-icon", - className: `fas fa-chevron-${isExpanded ? "up" : "down"} text-gray-400 text-sm` - } - ) - ) - ), - isExpanded && /* @__PURE__ */ React.createElement( - "div", - { - key: "features-section", - className: "mt-6 pt-6 border-t border-gray-700/30" - }, - /* @__PURE__ */ React.createElement( - "h5", - { - key: "features-title", - className: "text-primary font-medium mb-4 flex items-center" - }, - /* @__PURE__ */ React.createElement( - "i", - { - key: "features-icon", - className: "fas fa-list-ul mr-2 text-sm" - } - ), - "Key features:" - ), - /* @__PURE__ */ React.createElement( - "div", - { - key: "features-grid", - className: "grid md:grid-cols-2 gap-3" - }, - phase.features.map((feature, featureIndex) => /* @__PURE__ */ React.createElement( - "div", - { - key: `feature-${featureIndex}`, - className: "flex items-center space-x-3 p-3 bg-custom-bg rounded-lg" - }, - /* @__PURE__ */ React.createElement( - "div", - { - className: `w-2 h-2 rounded-full ${statusConfig.textClass.replace( - "text-", - "bg-" - )}` - } - ), - /* @__PURE__ */ React.createElement("span", { className: "text-secondary text-sm" }, feature) - )) - ) - ) - )); - })))), /* @__PURE__ */ React.createElement("div", { key: "cta-section", className: "mt-12 text-center" }, /* @__PURE__ */ React.createElement( - "div", - { - key: "cta-card", - className: "card-minimal rounded-xl p-8 max-w-2xl mx-auto" - }, - /* @__PURE__ */ React.createElement( - "h4", - { - key: "cta-title", - className: "text-xl font-semibold text-primary mb-3" - }, - "Join the future of privacy" - ), - /* @__PURE__ */ React.createElement("p", { key: "cta-description", className: "text-secondary mb-6" }, "SecureBit.chat grows thanks to the community. Your ideas and feedback help shape the future of secure communication with complete ASN.1 validation."), - /* @__PURE__ */ React.createElement( + return /* @__PURE__ */ React.createElement("section", { style: { width: "100%", color: "#e8e8eb", fontFamily: SANS, padding: isMobile ? "48px 0" : "64px 0", background: "radial-gradient(1200px 720px at 50% -8%, rgba(240,137,42,0.05), transparent 60%), #0f0f11" } }, /* @__PURE__ */ React.createElement("style", { dangerouslySetInnerHTML: { __html: "@keyframes rmExp{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes rmPulse{0%,100%{box-shadow:0 0 0 0 rgba(240,137,42,0.18)}60%{box-shadow:0 0 0 9px rgba(240,137,42,0)}}" } }), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: "1040px", margin: "0 auto", padding: isMobile ? "0 18px" : "0 40px" } }, /* @__PURE__ */ React.createElement("div", { style: { marginBottom: "30px" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: MONO, fontSize: "11px", fontWeight: 600, color: "#6b6b73", textTransform: "uppercase", letterSpacing: "1.6px", marginBottom: "13px" } }, "Development Roadmap"), /* @__PURE__ */ React.createElement("h2", { style: { margin: "0 0 14px", fontSize: isMobile ? "27px" : "34px", fontWeight: 800, letterSpacing: "-1px", lineHeight: 1.08, color: "#f4f4f6" } }, "The evolution of SecureBit"), /* @__PURE__ */ React.createElement("p", { style: { margin: 0, fontSize: "15.5px", lineHeight: 1.6, color: "#8a8a92", maxWidth: "660px" } }, "From the first prototype to a quantum-resistant, decentralized network \u2014 with complete ASN.1 validation at every layer.")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: "18px", flexWrap: "wrap", padding: "18px 22px", borderRadius: "14px", background: "#141416", border: "1px solid rgba(255,255,255,0.06)", marginBottom: "36px" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: MONO, fontSize: "12px", fontWeight: 600, color: "#e8e8eb", whiteSpace: "nowrap" } }, /* @__PURE__ */ React.createElement("span", { style: { color: "#3ecf8e" } }, shipped), " of ", total, " milestones shipped"), /* @__PURE__ */ React.createElement("div", { style: { flex: "1 1 240px", minWidth: "200px", height: "8px", borderRadius: "99px", background: "#0c0c0e", border: "1px solid rgba(255,255,255,0.06)", overflow: "hidden" } }, /* @__PURE__ */ React.createElement("div", { style: { height: "100%", width: shippedPct, background: "linear-gradient(90deg, #3ecf8e, #f0892a)" } })), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: MONO, fontSize: "11px", fontWeight: 600, color: "#6b6b73", textTransform: "uppercase", letterSpacing: "0.8px", whiteSpace: "nowrap" } }, upcoming, " on the way")), DATA.map((d, i) => { + const meta = META[d.status]; + const opened = isOpen(i); + const notLast = i < total - 1; + return /* @__PURE__ */ React.createElement("div", { key: i, style: { position: "relative", display: "grid", gridTemplateColumns: "54px 1fr", marginBottom: "16px" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, notLast && /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", left: "26px", top: "30px", height: "calc(100% + 16px)", width: "2px", background: meta.line } }), renderNode(d.status)), /* @__PURE__ */ React.createElement("div", { style: { borderRadius: "16px", background: "#141416", border: `1px solid ${d.status === "current" ? "rgba(240,137,42,0.28)" : "rgba(255,255,255,0.06)"}`, overflow: "hidden" } }, /* @__PURE__ */ React.createElement( "div", { - key: "cta-buttons", - className: "flex flex-col sm:flex-row gap-4 justify-center" + onClick: () => toggle(i), + style: { display: "flex", alignItems: "center", gap: isMobile ? "11px" : "16px", padding: isMobile ? "16px 16px" : "18px 22px", cursor: "pointer", transition: "background .18s ease" }, + onMouseEnter: (e) => { + e.currentTarget.style.background = "rgba(255,255,255,0.018)"; + }, + onMouseLeave: (e) => { + e.currentTarget.style.background = "transparent"; + } }, - /* @__PURE__ */ React.createElement( - "a", - { - key: "github-link", - href: "https://github.com/SecureBitChat/securebit-chat/", - className: "btn-primary text-white py-3 px-6 rounded-lg font-medium transition-all duration-200 flex items-center justify-center" - }, - /* @__PURE__ */ React.createElement("i", { key: "github-icon", className: "fab fa-github mr-2" }), - "GitHub Repository" - ), - /* @__PURE__ */ React.createElement( - "a", - { - key: "feedback-link", - href: "mailto:lockbitchat@tutanota.com", - className: "btn-secondary text-white py-3 px-6 rounded-lg font-medium transition-all duration-200 flex items-center justify-center" - }, - /* @__PURE__ */ React.createElement("i", { key: "feedback-icon", className: "fas fa-comments mr-2" }), - "Feedback" - ) - ) - ))); + /* @__PURE__ */ React.createElement("div", { style: { flex: "none", minWidth: "52px", textAlign: "center", padding: "7px 10px", borderRadius: "9px", background: "#0c0c0e", border: "1px solid rgba(255,255,255,0.07)", fontFamily: MONO, fontSize: "13px", fontWeight: 700, color: d.status === "current" ? "#f0892a" : "#cfcfd4" } }, d.v), + /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { fontSize: isMobile ? "15.5px" : "17px", fontWeight: 800, letterSpacing: "-0.4px", color: "#f4f4f6" } }, d.title), !isMobile && /* @__PURE__ */ React.createElement("div", { style: { marginTop: "3px", fontSize: "13.5px", color: "#9a9aa2" } }, d.sub)), + /* @__PURE__ */ React.createElement("div", { style: { flex: "none", display: "flex", alignItems: "center", gap: isMobile ? "8px" : "14px" } }, /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: "7px", padding: "6px 11px", borderRadius: "8px", background: hexA(meta.color, 0.1), border: `1px solid ${hexA(meta.color, 0.22)}`, fontFamily: MONO, fontSize: "10.5px", fontWeight: 600, color: meta.color, textTransform: "uppercase", letterSpacing: "0.8px", whiteSpace: "nowrap" } }, /* @__PURE__ */ React.createElement("span", { style: { width: "6px", height: "6px", borderRadius: "50%", background: meta.color } }), !isMobile && meta.word), !isMobile && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: MONO, fontSize: "12px", fontWeight: 500, color: "#8a8a92", whiteSpace: "nowrap", minWidth: "74px", textAlign: "right" } }, d.date), /* @__PURE__ */ React.createElement("span", { style: { color: "#6b6b73", display: "inline-flex", transition: "transform .22s cubic-bezier(.2,.7,.3,1)", transform: opened ? "rotate(180deg)" : "rotate(0deg)" } }, /* @__PURE__ */ React.createElement("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.1", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M6 9l6 6 6-6" })))) + ), opened && /* @__PURE__ */ React.createElement("div", { style: { padding: "4px 22px 22px 22px", animation: "rmExp .24s cubic-bezier(.2,.7,.3,1)" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: MONO, fontSize: "10px", fontWeight: 600, color: "#56565e", textTransform: "uppercase", letterSpacing: "1.2px", marginBottom: "14px", paddingTop: "14px", borderTop: "1px solid rgba(255,255,255,0.05)" } }, "Key features"), /* @__PURE__ */ React.createElement("div", { style: { display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: "11px 28px" } }, d.features.map((f, fi) => /* @__PURE__ */ React.createElement("div", { key: fi, style: { display: "flex", alignItems: "flex-start", gap: "10px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: "none", marginTop: "7px", width: "5px", height: "5px", borderRadius: "50%", background: meta.color } }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: "13.5px", lineHeight: 1.5, color: "#cfcfd4" } }, f))))))); + }))); } window.Roadmap = Roadmap; +// src/components/ui/CommunityCTA.jsx +var CommunityCTA = () => { + const [isMobile, setIsMobile] = React.useState( + typeof window !== "undefined" && window.matchMedia("(max-width:767px)").matches + ); + React.useEffect(() => { + const mq = window.matchMedia("(max-width:767px)"); + const onChange = () => setIsMobile(mq.matches); + mq.addEventListener ? mq.addEventListener("change", onChange) : mq.addListener(onChange); + return () => { + mq.removeEventListener ? mq.removeEventListener("change", onChange) : mq.removeListener(onChange); + }; + }, []); + const ACCENT = "#f0892a"; + const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace"; + const SANS = "'Manrope', system-ui, -apple-system, sans-serif"; + const githubUrl = "https://github.com/SecureBitChat/securebit-chat/"; + const feedbackUrl = "mailto:lockbitchat@tutanota.com"; + const githubBtn = React.createElement("a", { + key: "gh", + href: githubUrl, + target: "_blank", + rel: "noopener noreferrer", + style: { + display: "inline-flex", + alignItems: "center", + gap: "11px", + padding: "15px 26px", + borderRadius: "13px", + background: ACCENT, + color: "#1a0f04", + textDecoration: "none", + fontSize: "15.5px", + fontWeight: 700, + letterSpacing: "-0.2px", + boxShadow: "0 8px 24px rgba(240,137,42,0.28)", + whiteSpace: "nowrap", + transition: "all .2s cubic-bezier(.2,.7,.3,1)" + }, + onMouseEnter: (e) => { + e.currentTarget.style.background = "#ff9637"; + e.currentTarget.style.transform = "translateY(-2px)"; + }, + onMouseLeave: (e) => { + e.currentTarget.style.background = ACCENT; + e.currentTarget.style.transform = "none"; + } + }, [ + React.createElement("svg", { + key: "i", + width: 20, + height: 20, + viewBox: "0 0 24 24", + fill: "currentColor", + dangerouslySetInnerHTML: { __html: '' } + }), + "GitHub Repository" + ]); + const feedbackBtn = React.createElement("a", { + key: "fb", + href: feedbackUrl, + rel: "noopener noreferrer", + style: { + display: "inline-flex", + alignItems: "center", + gap: "11px", + padding: "15px 26px", + borderRadius: "13px", + background: "rgba(255,255,255,0.03)", + color: "#e8e8eb", + textDecoration: "none", + fontSize: "15.5px", + fontWeight: 700, + letterSpacing: "-0.2px", + border: "1px solid rgba(255,255,255,0.1)", + whiteSpace: "nowrap", + transition: "all .2s cubic-bezier(.2,.7,.3,1)" + }, + onMouseEnter: (e) => { + e.currentTarget.style.borderColor = "rgba(255,255,255,0.24)"; + e.currentTarget.style.background = "rgba(255,255,255,0.06)"; + }, + onMouseLeave: (e) => { + e.currentTarget.style.borderColor = "rgba(255,255,255,0.1)"; + e.currentTarget.style.background = "rgba(255,255,255,0.03)"; + } + }, [ + React.createElement("svg", { + key: "i", + width: 20, + height: 20, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + strokeWidth: 1.9, + strokeLinecap: "round", + strokeLinejoin: "round", + dangerouslySetInnerHTML: { __html: '' } + }), + "Feedback" + ]); + const chip = (label) => React.createElement("span", { + key: label, + style: { display: "inline-flex", alignItems: "center", gap: "7px" } + }, [ + React.createElement("span", { key: "d", style: { width: "5px", height: "5px", borderRadius: "50%", background: "#3ecf8e" } }), + label + ]); + const card = React.createElement("div", { + key: "card", + style: { + position: "relative", + overflow: "hidden", + maxWidth: "860px", + width: "100%", + borderRadius: "24px", + background: "radial-gradient(700px 360px at 50% 0%, rgba(240,137,42,0.1), transparent 65%), #121214", + border: "1px solid rgba(255,255,255,0.07)", + padding: isMobile ? "40px 24px 36px" : "56px 56px 48px", + textAlign: "center", + boxShadow: "0 24px 60px rgba(0,0,0,0.4)" + } + }, [ + // hairline accent + React.createElement("div", { + key: "hairline", + style: { position: "absolute", top: 0, left: "50%", transform: "translateX(-50%)", width: "180px", height: "1px", background: "linear-gradient(90deg, transparent, rgba(240,137,42,0.7), transparent)" } + }), + // brand mark (same SVG as the header — no border or background) + React.createElement("img", { + key: "icon", + src: "/logo/securebit-mark.svg", + alt: "SecureBit", + style: { display: "inline-block", width: "64px", height: "64px", objectFit: "contain", marginBottom: "22px", animation: "ccUp .4s cubic-bezier(.2,.7,.3,1)" } + }), + // eyebrow + React.createElement("div", { + key: "eyebrow", + style: { fontFamily: MONO, fontSize: "11px", fontWeight: 600, color: "#6b6b73", textTransform: "uppercase", letterSpacing: "1.8px", marginBottom: "14px" } + }, "Open source \xB7 community-driven"), + // title + React.createElement("h2", { + key: "title", + style: { margin: "0 0 16px", fontSize: isMobile ? "28px" : "36px", fontWeight: 800, letterSpacing: "-1px", lineHeight: 1.05, color: "#f4f4f6" } + }, "Join the future of privacy"), + // description + React.createElement("p", { + key: "desc", + style: { margin: "0 auto 32px", maxWidth: "560px", fontSize: "16px", lineHeight: 1.65, color: "#9a9aa2" } + }, "SecureBit grows thanks to its community. Your ideas and feedback shape the future of secure communication \u2014 built in the open, with complete ASN.1 validation end\u2011to\u2011end."), + // buttons + React.createElement("div", { + key: "btns", + style: { display: "flex", gap: "14px", justifyContent: "center", flexWrap: "wrap" } + }, [githubBtn, feedbackBtn]), + // trust chips + React.createElement("div", { + key: "chips", + style: { display: "flex", gap: "10px 22px", justifyContent: "center", flexWrap: "wrap", marginTop: "30px", fontFamily: MONO, fontSize: "11px", fontWeight: 500, color: "#56565e", textTransform: "uppercase", letterSpacing: "1px" } + }, [chip("MIT licensed"), chip("No tracking"), chip("Auditable cryptography")]) + ]); + return React.createElement("section", { + style: { + width: "100%", + display: "flex", + alignItems: "center", + justifyContent: "center", + background: "#0f0f11", + fontFamily: SANS, + padding: isMobile ? "48px 18px" : "64px 48px" + } + }, [ + React.createElement("style", { key: "kf", dangerouslySetInnerHTML: { __html: "@keyframes ccUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}" } }), + card + ]); +}; +window.CommunityCTA = CommunityCTA; + // src/components/ui/FileTransfer.jsx -var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles = [], onIncomingDecision }) => { +var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles = [], onIncomingDecision, showDropzone = true }) => { const [dragOver, setDragOver] = React.useState(false); const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] }); const fileInputRef = React.useRef(null); @@ -18822,6 +18709,48 @@ var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles return status; } }; + const renderProgress = (transfer, color) => { + const total = transfer.totalChunks || 0; + const done = transfer.transferredChunks || 0; + const isDone = transfer.status === "completed"; + const squares = total > 0 ? Math.min(total, 32) : 24; + let filled; + if (isDone) filled = squares; + else if (total > 0) filled = Math.floor(done / total * squares); + else filled = Math.floor((transfer.progress || 0) / 100 * squares); + filled = Math.max(0, Math.min(squares, filled)); + return React.createElement("div", { key: "progress" }, [ + React.createElement("div", { + key: "squares", + style: { display: "flex", flexWrap: "wrap", gap: "3px", marginBottom: "7px" } + }, Array.from({ length: squares }, (_, i) => React.createElement("div", { + key: i, + style: { + width: "11px", + height: "11px", + borderRadius: "2px", + background: i < filled ? color : "rgba(255,255,255,0.07)", + border: "1px solid " + (i < filled ? "transparent" : "rgba(255,255,255,0.05)"), + boxShadow: i < filled ? `0 0 5px ${color}55` : "none", + transition: "background .2s ease, box-shadow .2s ease" + } + }))), + React.createElement("div", { + key: "text", + style: { display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "11.5px", color: "#8a8a92" } + }, [ + React.createElement("span", { key: "status", style: { display: "inline-flex", alignItems: "center", gap: "5px" } }, [ + React.createElement("i", { key: "icon", className: getStatusIcon(transfer.status) }), + getStatusText(transfer.status) + ]), + React.createElement("span", { + key: "count", + style: { fontFamily: "'JetBrains Mono', ui-monospace, monospace", color: i_done(transfer) ? color : "#8a8a92" } + }, total > 0 ? `${Math.min(done, total)} / ${total} chunks` : `${(transfer.progress || 0).toFixed(0)}%`) + ]) + ]); + }; + const i_done = (t) => t.status === "completed"; const handleIncomingDecision = async (fileId, accepted) => { if (typeof onIncomingDecision === "function") { await onIncomingDecision(fileId, accepted); @@ -18848,35 +18777,42 @@ var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles return React.createElement("div", { className: "file-transfer-component" }, [ - // File Drop Zone - React.createElement("div", { + // File Drop Zone (SecureBit Chat design) — only when the panel is opened to SEND, + // so a receiver never sees the "send attachments" UI. + showDropzone && React.createElement("div", { key: "drop-zone", - className: `file-drop-zone ${dragOver ? "drag-over" : ""}`, onDrop: handleDrop, onDragOver: handleDragOver, onDragLeave: handleDragLeave, - onClick: () => fileInputRef.current?.click() + style: { + position: "relative", + border: "1.5px dashed " + (dragOver ? "rgba(240,137,42,0.7)" : "rgba(255,255,255,0.14)"), + borderRadius: "14px", + background: dragOver ? "rgba(240,137,42,0.07)" : "#141416", + padding: "24px 22px", + textAlign: "center", + transition: "all .15s" + } }, [ React.createElement("div", { - key: "drop-content", - className: "drop-content" + key: "icon-box", + style: { width: "42px", height: "42px", margin: "0 auto 10px", borderRadius: "12px", display: "grid", placeItems: "center", background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.08)" } + }, React.createElement("i", { className: "fas fa-arrow-up-from-bracket", style: { color: "#9a9aa2", fontSize: "18px" } })), + React.createElement("div", { key: "title", style: { fontSize: "14px", fontWeight: 700, color: "#e8e8eb" } }, "Drag & drop files here"), + React.createElement("div", { key: "sub", style: { fontSize: "12px", color: "#7b7b83", marginTop: "4px" } }, "Encrypted end-to-end before transfer \xB7 up to 100 MB"), + React.createElement("button", { + key: "browse", + type: "button", + onClick: () => fileInputRef.current?.click(), + className: "sb-send", + style: { marginTop: "14px", display: "inline-flex", alignItems: "center", gap: "7px", padding: "9px 16px", borderRadius: "9px", border: "none", background: "#f0892a", color: "#1a0f04", fontFamily: "inherit", fontSize: "13px", fontWeight: 700, cursor: "pointer" } }, [ - React.createElement("i", { - key: "icon", - className: "fas fa-cloud-upload-alt text-2xl mb-2 text-blue-400" - }), - React.createElement("p", { - key: "text", - className: "text-primary font-medium" - }, "Drag files here or click to select"), - React.createElement("p", { - key: "subtext", - className: "text-muted text-sm" - }, "Maximum size: 100 MB per file") + React.createElement("i", { key: "i", className: "fas fa-folder-open", style: { fontSize: "13px" } }), + "Browse device" ]) ]), // Hidden file input - React.createElement("input", { + showDropzone && React.createElement("input", { key: "file-input", ref: fileInputRef, type: "file", @@ -18889,37 +18825,42 @@ var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles className: "mt-4 space-y-2" }, pendingIncomingFiles.map((file) => React.createElement("div", { key: file.fileId, - className: "rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3" + style: { borderRadius: "12px", border: "1px solid rgba(255,255,255,0.08)", background: "#161618", padding: "12px 14px" } }, [ React.createElement("div", { key: "info", - className: "mb-3 flex items-center justify-between gap-3" + style: { marginBottom: "12px", display: "flex", alignItems: "center", gap: "11px" } }, [ - React.createElement("div", { key: "text" }, [ + React.createElement( + "div", + { key: "ic", style: { flex: "none", width: "34px", height: "34px", borderRadius: "9px", display: "grid", placeItems: "center", background: "rgba(240,137,42,0.12)", border: "1px solid rgba(240,137,42,0.22)" } }, + React.createElement("i", { className: "fas fa-file-arrow-down", style: { color: "#f0892a", fontSize: "15px" } }) + ), + React.createElement("div", { key: "text", style: { minWidth: 0 } }, [ React.createElement("div", { key: "title", - className: "text-sm font-medium text-primary" + style: { fontSize: "13px", fontWeight: 600, color: "#e8e8eb" } }, "Incoming file request"), React.createElement("div", { key: "meta", - className: "text-xs text-secondary" + style: { fontSize: "11.5px", color: "#7b7b83", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, `${file.fileName} \xB7 ${formatFileSize(file.fileSize)} \xB7 ${file.mimeType}`) ]) ]), React.createElement("div", { key: "actions", - className: "flex gap-2" + style: { display: "flex", gap: "8px" } }, [ React.createElement("button", { key: "accept", onClick: () => handleIncomingDecision(file.fileId, true), - className: "rounded-md bg-green-500/20 px-3 py-2 text-sm text-green-300 hover:bg-green-500/30" - }, "Accept"), + style: { display: "inline-flex", alignItems: "center", gap: "6px", borderRadius: "8px", border: "none", background: "#f0892a", color: "#1a0f04", padding: "8px 14px", fontSize: "13px", fontWeight: 700, cursor: "pointer" } + }, [React.createElement("i", { key: "i", className: "fas fa-check", style: { fontSize: "12px" } }), "Accept"]), React.createElement("button", { key: "reject", onClick: () => handleIncomingDecision(file.fileId, false), - className: "rounded-md bg-red-500/20 px-3 py-2 text-sm text-red-300 hover:bg-red-500/30" - }, "Reject") + style: { display: "inline-flex", alignItems: "center", gap: "6px", borderRadius: "8px", border: "1px solid rgba(229,114,122,0.3)", background: "rgba(229,114,122,0.08)", color: "#e5727a", padding: "8px 14px", fontSize: "13px", fontWeight: 600, cursor: "pointer" } + }, [React.createElement("i", { key: "i", className: "fas fa-xmark", style: { fontSize: "12px" } }), "Reject"]) ]) ]))), // Active Transfers @@ -18929,19 +18870,20 @@ var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles }, [ React.createElement("h4", { key: "title", - className: "text-primary font-medium mb-3 flex items-center" + style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12.5px", fontWeight: 600, color: "#8a8a92", marginBottom: "10px" } }, [ React.createElement("i", { key: "icon", - className: "fas fa-exchange-alt mr-2" + className: "fas fa-right-left", + style: { fontSize: "12px" } }), - "\u041F\u0435\u0440\u0435\u0434\u0430\u0447\u0430 \u0444\u0430\u0439\u043B\u043E\u0432" + "File transfers" ]), // Sending files ...transfers.sending.map( (transfer) => React.createElement("div", { key: `send-${transfer.fileId}`, - className: "transfer-item bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 mb-2" + style: { borderRadius: "11px", border: "1px solid rgba(255,255,255,0.07)", background: "#161618", padding: "12px", marginBottom: "8px" } }, [ React.createElement("div", { key: "header", @@ -18953,15 +18895,18 @@ var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles }, [ React.createElement("i", { key: "icon", - className: "fas fa-upload text-blue-400 mr-2" + className: "fas fa-arrow-up", + style: { color: "#f0892a", fontSize: "13px", marginRight: "8px" } }), React.createElement("span", { key: "name", - className: "text-primary font-medium text-sm" + className: "font-medium text-sm", + style: { color: "#e8e8eb" } }, transfer.fileName), React.createElement("span", { key: "size", - className: "text-muted text-xs ml-2" + className: "text-xs ml-2", + style: { color: "#7b7b83" } }, formatFileSize(transfer.fileSize)) ]), React.createElement("button", { @@ -18974,41 +18919,14 @@ var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles }) ]) ]), - React.createElement("div", { - key: "progress", - className: "progress-bar" - }, [ - React.createElement("div", { - key: "fill", - className: "progress-fill bg-blue-400", - style: { width: `${transfer.progress}%` } - }), - React.createElement("div", { - key: "text", - className: "progress-text text-xs flex items-center justify-between" - }, [ - React.createElement("span", { - key: "status", - className: "flex items-center" - }, [ - React.createElement("i", { - key: "icon", - className: `${getStatusIcon(transfer.status)} mr-1` - }), - getStatusText(transfer.status) - ]), - React.createElement("span", { - key: "percent" - }, `${transfer.progress.toFixed(1)}%`) - ]) - ]) + renderProgress(transfer, "#f0892a") ]) ), // Receiving files ...transfers.receiving.map( (transfer) => React.createElement("div", { key: `recv-${transfer.fileId}`, - className: "transfer-item bg-green-500/10 border border-green-500/20 rounded-lg p-3 mb-2" + style: { borderRadius: "11px", border: "1px solid rgba(255,255,255,0.07)", background: "#161618", padding: "12px", marginBottom: "8px" } }, [ React.createElement("div", { key: "header", @@ -19020,15 +18938,18 @@ var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles }, [ React.createElement("i", { key: "icon", - className: "fas fa-download text-green-400 mr-2" + className: "fas fa-arrow-down", + style: { color: "#3ecf8e", fontSize: "13px", marginRight: "8px" } }), React.createElement("span", { key: "name", - className: "text-primary font-medium text-sm" + className: "font-medium text-sm", + style: { color: "#e8e8eb" } }, transfer.fileName), React.createElement("span", { key: "size", - className: "text-muted text-xs ml-2" + className: "text-xs ml-2", + style: { color: "#7b7b83" } }, formatFileSize(transfer.fileSize)) ]), React.createElement("div", { key: "actions", className: "flex items-center space-x-2" }, [ @@ -19066,34 +18987,7 @@ var FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFiles ]) ]) ]), - React.createElement("div", { - key: "progress", - className: "progress-bar" - }, [ - React.createElement("div", { - key: "fill", - className: "progress-fill bg-green-400", - style: { width: `${transfer.progress}%` } - }), - React.createElement("div", { - key: "text", - className: "progress-text text-xs flex items-center justify-between" - }, [ - React.createElement("span", { - key: "status", - className: "flex items-center" - }, [ - React.createElement("i", { - key: "icon", - className: `${getStatusIcon(transfer.status)} mr-1` - }), - getStatusText(transfer.status) - ]), - React.createElement("span", { - key: "percent" - }, `${transfer.progress.toFixed(1)}%`) - ]) - ]) + renderProgress(transfer, "#3ecf8e") ]) ) ]) @@ -19278,7 +19172,7 @@ async function testIceServers(servers, timeoutMs = 6e3) { } }); } -var IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForget }) => { +var IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForget, embedded }) => { if (!isOpen) return null; const [useCustom, setUseCustom] = React2.useState(initial?.useCustom || false); const [serversText, setServersText] = React2.useState(initial?.serversText || ""); @@ -19312,172 +19206,217 @@ var IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForget if (onForget) await onForget(); setPersist(false); }; - const labelCls = "block text-sm font-medium text-primary"; - const descCls = "block text-sm text-secondary"; - const children = []; - children.push(React2.createElement("div", { key: "header", className: "flex items-center mb-4" }, [ - React2.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" - }, [React2.createElement("i", { className: "fas fa-network-wired accent-purple" })]), - React2.createElement("h3", { key: "title", className: "text-lg font-medium text-primary" }, "Advanced network settings") - ])); - children.push(React2.createElement( - "p", - { key: "intro", className: "text-sm text-secondary mb-4" }, - "By default SecureBit uses public STUN servers. You can supply your own STUN/TURN servers \u2014 useful if you self-host a TURN relay and do not want to rely on public infrastructure. Servers are configured locally on your side only; you do not need to share them with your peer." - )); - children.push(React2.createElement("div", { key: "mode", className: "space-y-2 mb-4" }, [ - React2.createElement("label", { key: "public", className: "flex items-start gap-3" }, [ - React2.createElement("input", { - key: "r", - type: "radio", - name: "ice-mode", - checked: !useCustom, - onChange: () => setUseCustom(false), - className: "mt-1" - }), - React2.createElement("span", { key: "s" }, [ - React2.createElement("span", { key: "t", className: labelCls }, "Public servers (default)"), - React2.createElement("span", { key: "d", className: descCls }, "Zero-config. Good for most users.") - ]) - ]), - React2.createElement("label", { key: "custom", className: "flex items-start gap-3" }, [ - React2.createElement("input", { - key: "r", - type: "radio", - name: "ice-mode", - checked: useCustom, - onChange: () => setUseCustom(true), - className: "mt-1" - }), - React2.createElement("span", { key: "s" }, [ - React2.createElement("span", { key: "t", className: labelCls }, "My own STUN/TURN servers"), - React2.createElement("span", { key: "d", className: descCls }, `Up to ${ICE_LIMITS.MAX_SERVERS} servers.`) - ]) + const h = React2.createElement; + const C_ORANGE = "#f0892a"; + const C_GREEN = "#3ecf8e"; + const MONO = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace"; + 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", + 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", + color: "inherit", + fontFamily: "inherit", + cursor: "pointer", + transition: "all .15s", + marginBottom: "10px" + }, extraStyle || {}) + }, [ + h( + "span", + { key: "ring", style: { flex: "none", width: "18px", height: "18px", marginTop: "1px", borderRadius: "50%", border: `1.5px solid ${selected ? C_ORANGE : "rgba(255,255,255,0.22)"}`, display: "grid", placeItems: "center" } }, + h("span", { style: { width: "8px", height: "8px", borderRadius: "50%", background: selected ? C_ORANGE : "transparent" } }) + ), + h("span", { key: "tx", style: { flex: 1 } }, [ + h("span", { key: "t", style: { display: "block", fontSize: "14px", fontWeight: 700, color: "#f4f4f6" } }, title), + h("span", { key: "d", style: { display: "block", fontSize: "12.5px", color: "#8a8a92", marginTop: "2px" } }, desc) ]) - ])); + ]); + 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", + 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", + color: "inherit", + fontFamily: "inherit", + cursor: "pointer", + transition: "all .15s", + marginBottom: "10px" + } + }, [ + h("span", { key: "tx", style: { flex: 1 } }, [ + h("span", { key: "r1", style: { display: "flex", alignItems: "center", gap: "8px" } }, [ + h("span", { key: "t", style: { fontSize: "14px", fontWeight: 700, color: "#f4f4f6" } }, title), + badge && h("span", { key: "b", style: { fontSize: "10px", fontWeight: 700, color: C_GREEN, padding: "2px 7px", borderRadius: "5px", background: "rgba(62,207,142,0.1)", border: "1px solid rgba(62,207,142,0.22)" } }, badge) + ]), + h("span", { key: "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" } }) + ) + ]); + const body = []; + body.push(h( + "p", + { key: "intro", style: { margin: "0 0 18px", fontSize: "13.5px", lineHeight: 1.6, color: "#9a9aa2" } }, + "SecureBit uses public STUN servers by default to negotiate the peer-to-peer link. Point it at your own STUN/TURN if you self-host." + )); + body.push(radioCard(!useCustom, () => setUseCustom(false), "Public servers (default)", "Zero-config. Good for most users.")); + body.push(radioCard(useCustom, () => setUseCustom(true), "My own STUN/TURN servers", `Up to ${ICE_LIMITS.MAX_SERVERS} servers.`, useCustom ? { marginBottom: "14px" } : null)); if (useCustom) { - children.push(React2.createElement("textarea", { - key: "textarea", - value: serversText, - onChange: (e) => setServersText(e.target.value), - placeholder: PLACEHOLDER, - spellCheck: false, - autoComplete: "off", - className: "w-full h-36 mb-2 p-3 rounded-lg bg-black/30 border border-purple-500/20 text-sm text-primary font-mono" - })); + 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", { + 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) { - children.push(React2.createElement( + custom.push(h( "ul", - { key: "errors", className: "mb-2 text-sm text-red-400 list-disc pl-5" }, - parsed.errors.slice(0, 6).map((err, i) => React2.createElement("li", { key: i }, err)) + { key: "err", style: { margin: "0 0 10px", paddingLeft: "18px", color: "#e5727a", fontSize: "12.5px" } }, + parsed.errors.slice(0, 6).map((err, i) => h("li", { key: i }, err)) )); } if (parsed.warnings.length > 0) { - children.push(React2.createElement( + custom.push(h( "ul", - { key: "warnings", className: "mb-2 text-sm text-yellow-400 list-disc pl-5" }, - parsed.warnings.slice(0, 6).map((w, i) => React2.createElement("li", { key: i }, w)) + { key: "warn", style: { margin: "0 0 10px", paddingLeft: "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) { - children.push(React2.createElement( + custom.push(h( "p", - { key: "ok", className: "mb-2 text-sm text-green-400" }, + { key: "ok", style: { margin: "0 0 10px", fontSize: "12.5px", color: C_GREEN } }, `${parsed.servers.length} server(s) parsed${hasTurn ? " (TURN present)" : " (STUN only \u2014 does not hide IP)"}.` )); } - children.push(React2.createElement( - "p", - { key: "disclaimer", className: "mb-3 text-xs text-secondary" }, - "Privacy note: a TURN relay sees the IP addresses and traffic timing of both peers (never your message contents, which stay end-to-end encrypted). Only a TURN server you trust or self-host improves privacy \u2014 pointing this at a random public relay does not. Prefer turns: (TLS)." - )); - children.push(React2.createElement("div", { key: "test", className: "mb-3" }, [ - React2.createElement("button", { + custom.push(h("div", { key: "note", style: { display: "flex", alignItems: "flex-start", gap: "9px", padding: "12px 13px", borderRadius: "11px", border: "1px solid rgba(62,207,142,0.18)", background: "rgba(62,207,142,0.05)", marginBottom: "12px" } }, [ + h("i", { key: "i", className: "fas fa-info-circle", style: { color: C_GREEN, fontSize: "13px", marginTop: "2px", flex: "none" } }), + h("span", { key: "t", style: { fontSize: "12px", lineHeight: 1.55, color: "#a8b8ae" } }, [ + "A TURN relay sees both peers\u2019 IP and traffic timing \u2014 but never message contents, which stay end-to-end encrypted. Prefer ", + h("span", { key: "m", style: { fontFamily: MONO, color: C_GREEN } }, "turns:"), + " (TLS)." + ]) + ])); + const testColor = testState === "done" && testResult && !testResult.error ? C_GREEN : "#cfcfd4"; + custom.push(h("div", { key: "test", style: { display: "flex", alignItems: "center", gap: "12px", flexWrap: "wrap", marginBottom: "4px" } }, [ + h("button", { key: "btn", type: "button", disabled: !canApply || testState === "running", onClick: handleTest, - className: "px-3 py-2 text-sm rounded-lg border border-purple-500/30 text-primary disabled:opacity-50" - }, testState === "running" ? "Testing\u2026" : "Test servers"), - testState === "done" && testResult ? React2.createElement( + style: { display: "inline-flex", alignItems: "center", gap: "8px", padding: "10px 15px", borderRadius: "10px", border: `1px solid ${testState === "done" && testResult && !testResult.error ? "rgba(62,207,142,0.4)" : "rgba(255,255,255,0.1)"}`, background: testState === "done" && testResult && !testResult.error ? "rgba(62,207,142,0.08)" : "rgba(255,255,255,0.04)", color: testColor, fontFamily: "inherit", fontSize: "13px", fontWeight: 600, cursor: !canApply || testState === "running" ? "not-allowed" : "pointer", opacity: !canApply || testState === "running" ? 0.6 : 1 } + }, [ + h("i", { key: "i", className: testState === "running" ? "fas fa-circle-notch" : "fas fa-play-circle", style: testState === "running" ? { animation: "sbSpin 1s linear infinite" } : null }), + testState === "running" ? "Testing\u2026" : "Test servers" + ]), + testState === "done" && testResult ? h( "span", - { - key: "res", - className: "ml-3 text-sm " + (testResult.error ? "text-red-400" : "text-secondary") - }, - testResult.error ? `Test failed: ${testResult.error}` : testResult.srflx > 0 || testResult.relay > 0 ? `STUN ${testResult.srflx > 0 ? "OK" : "none"} \xB7 TURN ${testResult.relay > 0 ? "OK" : "none"} \xB7 host ${testResult.host}` : `host ${testResult.host} \xB7 this browser (e.g. Safari) hides STUN/TURN candidates from the test \u2014 your servers are still applied to real connections` + { key: "res", style: { fontSize: "12px", color: testResult.error ? "#e5727a" : "#8a8a92" } }, + testResult.error ? `Test failed: ${testResult.error}` : testResult.srflx > 0 || testResult.relay > 0 ? `STUN ${testResult.srflx > 0 ? "OK" : "none"} \xB7 TURN ${testResult.relay > 0 ? "OK" : "none"} \xB7 host ${testResult.host}` : `host ${testResult.host} \xB7 this browser hides STUN/TURN candidates from the test \u2014 your servers still apply to real connections` ) : null ])); + body.push(h("div", { key: "custom", style: { marginBottom: "16px" } }, custom)); } - children.push(React2.createElement("label", { key: "relay", className: "flex items-start gap-3 mb-3 rounded-lg border border-purple-500/20 bg-purple-500/10 p-3" }, [ - React2.createElement("input", { - key: "i", - type: "checkbox", - checked: relayOnly, - onChange: (e) => setRelayOnly(e.target.checked), - className: "mt-1" - }), - React2.createElement("span", { key: "s" }, [ - React2.createElement("span", { key: "t", className: labelCls }, "Relay-only mode (maximum privacy)"), - React2.createElement("span", { key: "d", className: descCls }, "Routes all traffic through TURN so your IP is not exposed to the peer. Requires a TURN server; connections cannot start without one.") - ]) - ])); + body.push(toggleRow( + relayOnly, + () => setRelayOnly(!relayOnly), + "Relay-only mode", + "Routes all traffic through TURN so your IP is never exposed to the peer. Requires a TURN server.", + C_GREEN, + "MAX PRIVACY" + )); if (relayOnly && useCustom && !hasTurn) { - children.push(React2.createElement( + body.push(h( "p", - { key: "relaywarn", className: "mb-3 text-sm text-yellow-400" }, + { key: "relaywarn", style: { margin: "-4px 0 10px", fontSize: "12.5px", color: "#e3c84e" } }, "Relay-only is enabled but no TURN server is configured. The connection will not be able to start." )); } - children.push(React2.createElement("label", { key: "persist", className: "flex items-start gap-3 mb-4" }, [ - React2.createElement("input", { - key: "i", - type: "checkbox", - checked: persist, - onChange: (e) => setPersist(e.target.checked), - className: "mt-1" - }), - React2.createElement("span", { key: "s" }, [ - React2.createElement("span", { key: "t", className: labelCls }, "Save on this device"), - React2.createElement("span", { key: "d", className: descCls }, "Stored encrypted in this browser. Leave off to use only for this session.") - ]) - ])); - const actions = [ - React2.createElement("button", { - key: "cancel", - type: "button", - onClick: onClose, - className: "px-4 py-2 text-sm rounded-lg border border-white/10 text-secondary" - }, "Cancel"), - React2.createElement("button", { - key: "apply", - type: "button", - onClick: handleApply, - disabled: !canApply, - className: "px-4 py-2 text-sm rounded-lg bg-purple-500/20 border border-purple-500/30 text-primary disabled:opacity-50" - }, "Apply") - ]; + body.push(toggleRow( + persist, + () => setPersist(!persist), + "Save on this device", + "Stored encrypted in this browser. Leave off to use only for this session.", + C_ORANGE + )); + const footerBtns = []; if (hasSaved) { - actions.unshift(React2.createElement("button", { + footerBtns.push(h("button", { key: "forget", type: "button", onClick: handleForget, - className: "px-4 py-2 text-sm rounded-lg border border-red-500/30 text-red-400 mr-auto" + 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" } }, "Forget saved")); } - children.push(React2.createElement("div", { key: "actions", className: "flex items-center gap-2 flex-wrap" }, actions)); - return React2.createElement("div", { - className: "fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4", - onClick: (e) => { - if (e.target === e.currentTarget) onClose(); - } + 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" } + }, "Cancel")); + footerBtns.push(h("button", { + key: "apply", + type: "button", + onClick: handleApply, + disabled: !canApply, + style: { display: "inline-flex", alignItems: "center", gap: "8px", padding: "11px 20px", borderRadius: "11px", border: "none", background: C_ORANGE, color: "#1a0f04", fontFamily: "inherit", fontSize: "13.5px", fontWeight: 700, cursor: canApply ? "pointer" : "not-allowed", opacity: canApply ? 1 : 0.5, boxShadow: "0 6px 18px rgba(240,137,42,0.28)" } }, [ - React2.createElement("div", { - key: "modal", - className: "card-minimal rounded-xl p-6 max-w-lg w-full border-purple-500/20 max-h-[90vh] overflow-y-auto" - }, children) + h("i", { key: "i", className: "fas fa-check" }), + "Apply" + ])); + 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: "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 }, [ + h(React2.Fragment, { key: "panel" }, [ + // header + h("div", { key: "head", style: { display: "flex", alignItems: "center", gap: "12px", padding: "20px 24px", borderBottom: "1px solid rgba(255,255,255,0.06)" } }, [ + h( + "div", + { key: "ic", style: { width: "38px", height: "38px", flex: "none", display: "grid", placeItems: "center", borderRadius: "10px", background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.06)" } }, + h("i", { className: "fas fa-sliders-h", style: { color: "#cfcfd4", fontSize: "15px" } }) + ), + h("div", { key: "tx", style: { flex: 1, lineHeight: 1.25 } }, [ + h("div", { key: "t", style: { fontSize: "16.5px", fontWeight: 800, letterSpacing: "-0.3px", color: "#f4f4f6" } }, "Network settings"), + h("div", { key: "s", style: { fontSize: "12px", color: "#7b7b83" } }, "Configured locally \u2014 never shared with your peer") + ]), + h( + "button", + { key: "x", type: "button", onClick: onClose, style: { width: "32px", height: "32px", flex: "none", display: "grid", placeItems: "center", borderRadius: "9px", border: "none", background: "rgba(255,255,255,0.04)", color: "#8a8a92", cursor: "pointer" } }, + h("i", { className: "fas fa-times" }) + ) + ]), + // scroll body + h("div", { key: "body", className: "custom-scrollbar", style: { flex: 1, overflowY: "auto", padding: "20px 24px" } }, body), + // footer + h("div", { key: "foot", style: { display: "flex", alignItems: "center", justifyContent: "flex-end", gap: "10px", padding: "16px 24px", borderTop: "1px solid rgba(255,255,255,0.06)", background: "#0e0e10", borderRadius: "0" } }, footerBtns) + ]) ]); }; window.IceServerSettings = IceServerSettings; diff --git a/dist/app-boot.js.map b/dist/app-boot.js.map index dedbafc..384b4a3 100644 --- a/dist/app-boot.js.map +++ b/dist/app-boot.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../src/notifications/SecureNotificationManager.js", "../src/notifications/NotificationIntegration.js", "../node_modules/dompurify/src/utils.ts", "../node_modules/dompurify/src/tags.ts", "../node_modules/dompurify/src/attrs.ts", "../node_modules/dompurify/src/regexp.ts", "../node_modules/dompurify/src/purify.ts", "../src/crypto/EnhancedSecureCryptoUtils.js", "../src/transfer/EnhancedSecureFileTransfer.js", "../src/network/EnhancedSecureWebRTCManager.js", "../src/scripts/app-boot.js", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/BecomePartner.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/SecurityFeatures.jsx", "../src/components/ui/Testimonials.jsx", "../src/components/ui/ComparisonTable.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/FileTransfer.jsx", "../src/network/iceServers.js", "../src/components/ui/IceServerSettings.jsx"], - "sourcesContent": ["/**\n * Secure and Reliable Notification Manager for P2P WebRTC Chat\n * Follows best practices: OWASP, MDN, Chrome DevRel\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nclass SecureChatNotificationManager {\n constructor(config = {}) {\n // Safely read Notification permission (iOS Safari may not define Notification)\n this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')\n ? Notification.permission\n : 'denied';\n this.isTabActive = this.checkTabActive(); // Initialize with proper check\n this.unreadCount = 0;\n this.originalTitle = document.title;\n this.notificationQueue = [];\n this.maxQueueSize = config.maxQueueSize || 5;\n this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection\n this.lastNotificationTime = 0;\n this.trustedOrigins = config.trustedOrigins || [];\n \n // Secure context flag\n this.isSecureContext = window.isSecureContext;\n \n // Cross-browser compatibility for Page Visibility API\n this.hidden = this.getHiddenProperty();\n this.visibilityChange = this.getVisibilityChangeEvent();\n \n this.initVisibilityTracking();\n this.initSecurityChecks();\n }\n\n /**\n * Initialize security checks and validation\n * @private\n */\n initSecurityChecks() {\n // Security checks are performed silently\n }\n\n /**\n * Get hidden property name for cross-browser compatibility\n * @returns {string} Hidden property name\n * @private\n */\n getHiddenProperty() {\n if (typeof document.hidden !== \"undefined\") {\n return \"hidden\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msHidden\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitHidden\";\n }\n return \"hidden\"; // fallback\n }\n\n /**\n * Get visibility change event name for cross-browser compatibility\n * @returns {string} Visibility change event name\n * @private\n */\n getVisibilityChangeEvent() {\n if (typeof document.hidden !== \"undefined\") {\n return \"visibilitychange\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msvisibilitychange\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitvisibilitychange\";\n }\n return \"visibilitychange\"; // fallback\n }\n\n /**\n * Check if tab is currently active using multiple methods\n * @returns {boolean} True if tab is active\n * @private\n */\n checkTabActive() {\n // Primary method: Page Visibility API\n if (this.hidden && typeof document[this.hidden] !== \"undefined\") {\n return !document[this.hidden];\n }\n \n // Fallback method: document.hasFocus()\n if (typeof document.hasFocus === \"function\") {\n return document.hasFocus();\n }\n \n // Ultimate fallback: assume active\n return true;\n }\n\n /**\n * Initialize page visibility tracking (Page Visibility API)\n * @private\n */\n initVisibilityTracking() {\n // Primary method: Page Visibility API with cross-browser support\n if (typeof document.addEventListener !== \"undefined\" && typeof document[this.hidden] !== \"undefined\") {\n document.addEventListener(this.visibilityChange, () => {\n this.isTabActive = this.checkTabActive();\n \n if (this.isTabActive) {\n this.resetUnreadCount();\n this.clearNotificationQueue();\n }\n });\n }\n\n // Fallback method: Window focus/blur events\n window.addEventListener('focus', () => {\n this.isTabActive = this.checkTabActive();\n if (this.isTabActive) {\n this.resetUnreadCount();\n }\n });\n\n window.addEventListener('blur', () => {\n this.isTabActive = this.checkTabActive();\n });\n\n // Page unload cleanup\n window.addEventListener('beforeunload', () => {\n this.clearNotificationQueue();\n });\n }\n\n /**\n * Request notification permission (BEST PRACTICE: Only call in response to user action)\n * Never call on page load!\n * @returns {Promise} Permission granted status\n */\n async requestPermission() {\n // Secure context check\n if (!this.isSecureContext || !('Notification' in window)) {\n return false;\n }\n\n if (this.permission === 'granted') {\n return true;\n }\n\n if (this.permission === 'denied') {\n return false;\n }\n\n try {\n this.permission = await Notification.requestPermission();\n return this.permission === 'granted';\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Update page title with unread count\n * @private\n */\n updateTitle() {\n if (this.unreadCount > 0) {\n document.title = `(${this.unreadCount}) ${this.originalTitle}`;\n } else {\n document.title = this.originalTitle;\n }\n }\n\n /**\n * XSS Protection: Sanitize input text\n * @param {string} text - Text to sanitize\n * @returns {string} Sanitized text\n * @private\n */\n sanitizeText(text) {\n if (typeof text !== 'string') {\n return '';\n }\n \n // Remove HTML tags and potentially dangerous characters\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .substring(0, 500); // Length limit\n }\n\n /**\n * Validate icon URL (XSS protection)\n * @param {string} url - URL to validate\n * @returns {string|null} Validated URL or null\n * @private\n */\n validateIconUrl(url) {\n if (!url) return null;\n \n try {\n const parsedUrl = new URL(url, window.location.origin);\n \n // Only allow HTTPS and data URLs\n if (parsedUrl.protocol === 'https:' || parsedUrl.protocol === 'data:') {\n // Check trusted origins if specified\n if (this.trustedOrigins.length > 0) {\n const isTrusted = this.trustedOrigins.some(origin => \n parsedUrl.origin === origin\n );\n return isTrusted ? parsedUrl.href : null;\n }\n return parsedUrl.href;\n }\n \n return null;\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Rate limiting for spam protection\n * @returns {boolean} Rate limit check passed\n * @private\n */\n checkRateLimit() {\n const now = Date.now();\n if (now - this.lastNotificationTime < this.rateLimitMs) {\n return false;\n }\n this.lastNotificationTime = now;\n return true;\n }\n\n /**\n * Send secure notification\n * @param {string} senderName - Name of message sender\n * @param {string} message - Message content\n * @param {Object} options - Notification options\n * @returns {Notification|null} Created notification or null\n */\n notify(senderName, message, options = {}) {\n // Abort if Notifications API is not available (e.g., iOS Safari)\n if (typeof Notification === 'undefined') {\n return null;\n }\n // Update tab active state before checking\n this.isTabActive = this.checkTabActive();\n \n // Only show if tab is NOT active (user is on another tab or minimized)\n if (this.isTabActive) {\n return null;\n }\n\n // Permission check\n if (this.permission !== 'granted') {\n return null;\n }\n\n // Rate limiting\n if (!this.checkRateLimit()) {\n return null;\n }\n\n // Data sanitization (XSS Protection)\n const safeSenderName = this.sanitizeText(senderName || 'Unknown');\n const safeMessage = this.sanitizeText(message || '');\n const safeIcon = this.validateIconUrl(options.icon) || '/logo/icon-192x192.png';\n\n // Queue overflow protection\n if (this.notificationQueue.length >= this.maxQueueSize) {\n this.clearNotificationQueue();\n }\n\n try {\n \n const notification = new Notification(\n `${safeSenderName}`,\n {\n body: safeMessage.substring(0, 200), // Length limit\n icon: safeIcon,\n badge: safeIcon,\n tag: `chat-${options.senderId || 'unknown'}`, // Grouping\n requireInteraction: false, // Don't block user\n silent: options.silent || false,\n // Vibrate only for mobile and if supported\n vibrate: navigator.vibrate ? [200, 100, 200] : undefined,\n // Safe metadata\n data: {\n senderId: this.sanitizeText(options.senderId),\n timestamp: Date.now(),\n // Don't include sensitive data!\n }\n }\n );\n\n // Increment counter\n this.unreadCount++;\n this.updateTitle();\n\n // Add to queue for management\n this.notificationQueue.push(notification);\n\n // Safe click handler\n notification.onclick = (event) => {\n event.preventDefault(); // Prevent default behavior\n window.focus();\n notification.close();\n \n // Safe callback\n if (typeof options.onClick === 'function') {\n try {\n options.onClick(options.senderId);\n } catch (error) {\n console.error('[Notifications] Error in onClick handler:', error);\n }\n }\n };\n\n // Error handler\n notification.onerror = (event) => {\n console.error('[Notifications] Error showing notification:', event);\n };\n\n // Auto-close after reasonable time\n const autoCloseTimeout = Math.min(options.autoClose || 5000, 10000);\n setTimeout(() => {\n notification.close();\n this.removeFromQueue(notification);\n }, autoCloseTimeout);\n\n return notification;\n \n } catch (error) {\n console.error('[Notifications] Failed to create notification:', error);\n return null;\n }\n }\n\n /**\n * Remove notification from queue\n * @param {Notification} notification - Notification to remove\n * @private\n */\n removeFromQueue(notification) {\n const index = this.notificationQueue.indexOf(notification);\n if (index > -1) {\n this.notificationQueue.splice(index, 1);\n }\n }\n\n /**\n * Clear all notifications\n */\n clearNotificationQueue() {\n this.notificationQueue.forEach(notification => {\n try {\n notification.close();\n } catch (error) {\n // Ignore errors when closing\n }\n });\n this.notificationQueue = [];\n }\n\n /**\n * Reset unread counter\n */\n resetUnreadCount() {\n this.unreadCount = 0;\n this.updateTitle();\n }\n\n /**\n * Get current status\n * @returns {Object} Current notification status\n */\n getStatus() {\n return {\n permission: this.permission,\n isTabActive: this.isTabActive,\n unreadCount: this.unreadCount,\n isSecureContext: this.isSecureContext,\n queueSize: this.notificationQueue.length\n };\n }\n}\n\n/**\n * Secure integration with WebRTC\n */\nclass SecureP2PChat {\n constructor() {\n this.notificationManager = new SecureChatNotificationManager({\n maxQueueSize: 5,\n rateLimitMs: 2000,\n trustedOrigins: [\n window.location.origin,\n // Add other trusted origins for CDN icons\n ]\n });\n \n this.dataChannel = null;\n this.peerConnection = null;\n this.remotePeerName = 'Peer';\n this.messageHistory = [];\n this.maxHistorySize = 100;\n }\n\n /**\n * Initialize when user connects\n */\n async init() {\n // Initialize notification manager silently\n }\n\n /**\n * Method for manual permission request (called on click)\n * @returns {Promise} Permission granted status\n */\n async enableNotifications() {\n const granted = await this.notificationManager.requestPermission();\n return granted;\n }\n\n /**\n * Setup DataChannel with security checks\n * @param {RTCDataChannel} dataChannel - WebRTC data channel\n */\n setupDataChannel(dataChannel) {\n if (!dataChannel) {\n console.error('[Chat] Invalid DataChannel');\n return;\n }\n\n this.dataChannel = dataChannel;\n \n // Setup handlers\n this.dataChannel.onmessage = (event) => {\n this.handleIncomingMessage(event.data);\n };\n\n this.dataChannel.onerror = (error) => {\n // Handle error silently\n };\n }\n\n /**\n * XSS Protection: Validate incoming messages\n * @param {string|Object} data - Message data\n * @returns {Object|null} Validated message or null\n * @private\n */\n validateMessage(data) {\n try {\n const message = typeof data === 'string' ? JSON.parse(data) : data;\n \n // Check message structure\n if (!message || typeof message !== 'object') {\n throw new Error('Invalid message structure');\n }\n\n // Check required fields\n if (!message.text || typeof message.text !== 'string') {\n throw new Error('Invalid message text');\n }\n\n // Message length limit (DoS protection)\n if (message.text.length > 10000) {\n throw new Error('Message too long');\n }\n\n return {\n text: message.text,\n senderName: message.senderName || 'Unknown',\n senderId: message.senderId || 'unknown',\n timestamp: message.timestamp || Date.now(),\n senderAvatar: message.senderAvatar || null\n };\n \n } catch (error) {\n console.error('[Chat] Message validation failed:', error);\n return null;\n }\n }\n\n /**\n * Secure handling of incoming messages\n * @param {string|Object} data - Message data\n * @private\n */\n handleIncomingMessage(data) {\n const message = this.validateMessage(data);\n \n if (!message) {\n return;\n }\n\n // Save to history (with limit)\n this.messageHistory.push(message);\n if (this.messageHistory.length > this.maxHistorySize) {\n this.messageHistory.shift();\n }\n\n // Display in UI (with sanitization)\n this.displayMessage(message);\n\n // Send notification only if tab is inactive\n this.notificationManager.notify(\n message.senderName,\n message.text,\n {\n icon: message.senderAvatar,\n senderId: message.senderId,\n onClick: (senderId) => {\n this.scrollToLatestMessage();\n }\n }\n );\n\n // Optional: sound (with check)\n if (!this.notificationManager.isTabActive) {\n this.playNotificationSound();\n }\n }\n\n /**\n * XSS Protection: Safe message display\n * @param {Object} message - Message to display\n * @private\n */\n displayMessage(message) {\n const container = document.getElementById('messages');\n if (!container) {\n return;\n }\n\n const messageEl = document.createElement('div');\n messageEl.className = 'message';\n \n // Use textContent to prevent XSS\n const nameEl = document.createElement('strong');\n nameEl.textContent = message.senderName + ': ';\n \n const textEl = document.createElement('span');\n textEl.textContent = message.text;\n textEl.style.wordWrap = 'break-word';\n textEl.style.overflowWrap = 'break-word';\n textEl.style.whiteSpace = 'normal';\n \n const timeEl = document.createElement('small');\n timeEl.textContent = new Date(message.timestamp).toLocaleTimeString();\n \n messageEl.appendChild(nameEl);\n messageEl.appendChild(textEl);\n messageEl.appendChild(document.createElement('br'));\n messageEl.appendChild(timeEl);\n \n container.appendChild(messageEl);\n this.scrollToLatestMessage();\n }\n\n /**\n * Safe sound playback\n * @private\n */\n playNotificationSound() {\n try {\n // Use only local audio files\n const audio = new Audio('/assets/audio/notification.mp3');\n audio.volume = 0.3; // Moderate volume\n \n // Error handling\n audio.play().catch(error => {\n // Handle audio error silently\n });\n } catch (error) {\n // Handle audio creation error silently\n }\n }\n\n /**\n * Scroll to latest message\n * @private\n */\n scrollToLatestMessage() {\n const container = document.getElementById('messages');\n if (container) {\n container.scrollTop = container.scrollHeight;\n }\n }\n\n /**\n * Get status\n * @returns {Object} Current chat status\n */\n getStatus() {\n return {\n notifications: this.notificationManager.getStatus(),\n messageCount: this.messageHistory.length,\n connected: this.dataChannel?.readyState === 'open'\n };\n }\n}\n\n// Export for use in other modules\nif (typeof module !== 'undefined' && module.exports) {\n module.exports = { SecureChatNotificationManager, SecureP2PChat };\n}\n\n// Global export for browser usage\nif (typeof window !== 'undefined') {\n window.SecureChatNotificationManager = SecureChatNotificationManager;\n window.SecureP2PChat = SecureP2PChat;\n}\n", "/**\n * Notification Integration Module for SecureBit WebRTC Chat\n * Integrates secure notifications with existing WebRTC architecture\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nimport { SecureChatNotificationManager } from './SecureNotificationManager.js';\n\nclass NotificationIntegration {\n constructor(webrtcManager) {\n this.webrtcManager = webrtcManager;\n this.notificationManager = new SecureChatNotificationManager({\n maxQueueSize: 10,\n rateLimitMs: 1000, // Reduced from 2000ms to 1000ms\n trustedOrigins: [\n window.location.origin,\n // Add other trusted origins for CDN icons\n ]\n });\n \n this.isInitialized = false;\n this.originalOnMessage = null;\n this.originalOnStatusChange = null;\n this.processedMessages = new Set(); // Track processed messages to avoid duplicates\n }\n\n /**\n * Initialize notification integration\n * @returns {Promise} Initialization success\n */\n async init() {\n try {\n if (this.isInitialized) {\n return true;\n }\n\n // Store original callbacks\n this.originalOnMessage = this.webrtcManager.onMessage;\n this.originalOnStatusChange = this.webrtcManager.onStatusChange;\n\n\n // Wrap the original onMessage callback.\n // IMPORTANT: forward ALL arguments (incl. per-message `meta`) so the app\n // still receives view-once / disappearing / unsend metadata.\n this.webrtcManager.onMessage = (message, type, ...rest) => {\n this.handleIncomingMessage(message, type);\n\n // Call original callback if it exists\n if (this.originalOnMessage) {\n this.originalOnMessage(message, type, ...rest);\n }\n };\n\n // Wrap the original onStatusChange callback\n this.webrtcManager.onStatusChange = (status) => {\n this.handleStatusChange(status);\n \n // Call original callback if it exists\n if (this.originalOnStatusChange) {\n this.originalOnStatusChange(status);\n }\n };\n\n // Also hook into the deliverMessageToUI method if it exists.\n // IMPORTANT: forward ALL arguments (incl. per-message `meta`) to the\n // original, otherwise view-once / disappearing / unsend metadata is lost.\n if (this.webrtcManager.deliverMessageToUI) {\n this.originalDeliverMessageToUI = this.webrtcManager.deliverMessageToUI.bind(this.webrtcManager);\n this.webrtcManager.deliverMessageToUI = (message, type, ...rest) => {\n this.handleIncomingMessage(message, type);\n this.originalDeliverMessageToUI(message, type, ...rest);\n };\n }\n\n this.isInitialized = true;\n return true;\n\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Handle incoming messages and trigger notifications\n * @param {*} message - Message content\n * @param {string} type - Message type\n * @private\n */\n handleIncomingMessage(message, type) {\n try {\n // Create a unique key for this message to avoid duplicates\n const messageKey = `${type}:${typeof message === 'string' ? message : JSON.stringify(message)}`;\n \n // Skip if we've already processed this message\n if (this.processedMessages.has(messageKey)) {\n return;\n }\n \n // Mark message as processed\n this.processedMessages.add(messageKey);\n \n // Clean up old processed messages (keep only last 100)\n if (this.processedMessages.size > 100) {\n const messagesArray = Array.from(this.processedMessages);\n this.processedMessages.clear();\n messagesArray.slice(-50).forEach(msg => this.processedMessages.add(msg));\n }\n \n \n // Only process chat messages, not system messages\n if (type === 'system' || type === 'file-transfer' || type === 'heartbeat') {\n return;\n }\n\n // Extract message information\n const messageInfo = this.extractMessageInfo(message, type);\n if (!messageInfo) {\n return;\n }\n\n // Send notification\n const notificationResult = this.notificationManager.notify(\n messageInfo.senderName,\n messageInfo.text,\n {\n icon: messageInfo.senderAvatar,\n senderId: messageInfo.senderId,\n onClick: (senderId) => {\n this.focusChatWindow();\n }\n }\n );\n\n } catch (error) {\n // Handle error silently\n }\n }\n\n /**\n * Handle status changes\n * @param {string} status - Connection status\n * @private\n */\n handleStatusChange(status) {\n try {\n // Clear notifications when connection is lost\n if (status === 'disconnected' || status === 'failed') {\n this.notificationManager.clearNotificationQueue();\n this.notificationManager.resetUnreadCount();\n }\n } catch (error) {\n // Handle error silently\n }\n }\n\n /**\n * Extract message information for notifications\n * @param {*} message - Message content\n * @param {string} type - Message type\n * @returns {Object|null} Extracted message info or null\n * @private\n */\n extractMessageInfo(message, type) {\n try {\n let messageData = message;\n\n // Handle different message formats\n if (typeof message === 'string') {\n try {\n messageData = JSON.parse(message);\n } catch (e) {\n // Plain text message\n return {\n senderName: 'Peer',\n text: message,\n senderId: 'peer',\n senderAvatar: null\n };\n }\n }\n\n // Handle structured message data\n if (typeof messageData === 'object' && messageData !== null) {\n return {\n senderName: messageData.senderName || messageData.name || 'Peer',\n text: messageData.text || messageData.message || messageData.content || '',\n senderId: messageData.senderId || messageData.id || 'peer',\n senderAvatar: messageData.senderAvatar || messageData.avatar || null\n };\n }\n\n return null;\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Focus chat window when notification is clicked\n * @private\n */\n focusChatWindow() {\n try {\n window.focus();\n \n // Scroll to bottom of messages if container exists\n const messagesContainer = document.getElementById('messages');\n if (messagesContainer) {\n messagesContainer.scrollTop = messagesContainer.scrollHeight;\n }\n } catch (error) {\n // Handle error silently\n }\n }\n\n /**\n * Request notification permission\n * @returns {Promise} Permission granted status\n */\n async requestPermission() {\n try {\n return await this.notificationManager.requestPermission();\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Get notification status\n * @returns {Object} Notification status\n */\n getStatus() {\n return this.notificationManager.getStatus();\n }\n\n /**\n * Clear all notifications\n */\n clearNotifications() {\n this.notificationManager.clearNotificationQueue();\n this.notificationManager.resetUnreadCount();\n }\n\n /**\n * Cleanup integration\n */\n cleanup() {\n try {\n if (this.isInitialized) {\n // Restore original callbacks\n if (this.originalOnMessage) {\n this.webrtcManager.onMessage = this.originalOnMessage;\n }\n if (this.originalOnStatusChange) {\n this.webrtcManager.onStatusChange = this.originalOnStatusChange;\n }\n if (this.originalDeliverMessageToUI) {\n this.webrtcManager.deliverMessageToUI = this.originalDeliverMessageToUI;\n }\n\n // Clear notifications\n this.clearNotifications();\n\n this.isInitialized = false;\n }\n } catch (error) {\n // Handle error silently\n }\n }\n}\n\n// Export for use in other modules\nif (typeof module !== 'undefined' && module.exports) {\n module.exports = { NotificationIntegration };\n}\n\n// Global export for browser usage\nif (typeof window !== 'undefined') {\n window.NotificationIntegration = NotificationIntegration;\n}\n", "const {\n entries,\n setPrototypeOf,\n isFrozen,\n getPrototypeOf,\n getOwnPropertyDescriptor,\n} = Object;\n\nlet { freeze, seal, create } = Object; // eslint-disable-line import/no-mutable-exports\nlet { apply, construct } = typeof Reflect !== 'undefined' && Reflect;\n\nif (!freeze) {\n freeze = function (x: T): T {\n return x;\n };\n}\n\nif (!seal) {\n seal = function (x: T): T {\n return x;\n };\n}\n\nif (!apply) {\n apply = function (\n func: (thisArg: any, ...args: any[]) => T,\n thisArg: any,\n ...args: any[]\n ): T {\n return func.apply(thisArg, args);\n };\n}\n\nif (!construct) {\n construct = function (Func: new (...args: any[]) => T, ...args: any[]): T {\n return new Func(...args);\n };\n}\n\nconst arrayForEach = unapply(Array.prototype.forEach);\nconst arrayIndexOf = unapply(Array.prototype.indexOf);\nconst arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);\nconst arrayPop = unapply(Array.prototype.pop);\nconst arrayPush = unapply(Array.prototype.push);\nconst arraySlice = unapply(Array.prototype.slice);\nconst arraySplice = unapply(Array.prototype.splice);\nconst arrayIsArray = Array.isArray;\n\nconst stringToLowerCase = unapply(String.prototype.toLowerCase);\nconst stringToString = unapply(String.prototype.toString);\nconst stringMatch = unapply(String.prototype.match);\nconst stringReplace = unapply(String.prototype.replace);\nconst stringIndexOf = unapply(String.prototype.indexOf);\nconst stringTrim = unapply(String.prototype.trim);\n\nconst numberToString = unapply(Number.prototype.toString);\nconst booleanToString = unapply(Boolean.prototype.toString);\nconst bigintToString =\n typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString);\nconst symbolToString =\n typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString);\n\nconst objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);\nconst objectToString = unapply(Object.prototype.toString);\n\nconst regExpTest = unapply(RegExp.prototype.test);\n\nconst typeErrorCreate = unconstruct(TypeError);\n\n/**\n * Creates a new function that calls the given function with a specified thisArg and arguments.\n *\n * @param func - The function to be wrapped and called.\n * @returns A new function that calls the given function with a specified thisArg and arguments.\n */\nfunction unapply(\n func: (thisArg: any, ...args: any[]) => T\n): (thisArg: any, ...args: any[]) => T {\n return (thisArg: any, ...args: any[]): T => {\n if (thisArg instanceof RegExp) {\n thisArg.lastIndex = 0;\n }\n\n return apply(func, thisArg, args);\n };\n}\n\n/**\n * Creates a new function that constructs an instance of the given constructor function with the provided arguments.\n *\n * @param func - The constructor function to be wrapped and called.\n * @returns A new function that constructs an instance of the given constructor function with the provided arguments.\n */\nfunction unconstruct(\n Func: new (...args: any[]) => T\n): (...args: any[]) => T {\n return (...args: any[]): T => construct(Func, args);\n}\n\n/**\n * Add properties to a lookup table\n *\n * @param set - The set to which elements will be added.\n * @param array - The array containing elements to be added to the set.\n * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set.\n * @returns The modified set with added elements.\n */\nfunction addToSet(\n set: Record,\n array: readonly unknown[],\n transformCaseFunc: ReturnType> = stringToLowerCase\n): Record {\n if (setPrototypeOf) {\n // Make 'in' and truthy checks like Boolean(set.constructor)\n // independent of any properties defined on Object.prototype.\n // Prevent prototype setters from intercepting set as a this value.\n setPrototypeOf(set, null);\n }\n\n if (!arrayIsArray(array)) {\n return set;\n }\n\n let l = array.length;\n while (l--) {\n let element = array[l];\n\n if (typeof element === 'string') {\n const lcElement = transformCaseFunc(element);\n\n if (lcElement !== element) {\n // Config presets (e.g. tags.js, attrs.js) are immutable.\n if (!isFrozen(array)) {\n (array as unknown[])[l] = lcElement;\n }\n\n element = lcElement;\n }\n }\n\n set[element as string] = true;\n }\n\n return set;\n}\n\n/**\n * Clean up an array to harden against CSPP\n *\n * @param array - The array to be cleaned.\n * @returns The cleaned version of the array\n */\nfunction cleanArray(array: T[]): Array {\n for (let index = 0; index < array.length; index++) {\n const isPropertyExist = objectHasOwnProperty(array, index);\n\n if (!isPropertyExist) {\n array[index] = null;\n }\n }\n\n return array;\n}\n\n/**\n * Shallow clone an object\n *\n * @param object - The object to be cloned.\n * @returns A new object that copies the original.\n */\nfunction clone>(object: T): T {\n const newObject = create(null);\n\n for (const [property, value] of entries(object)) {\n const isPropertyExist = objectHasOwnProperty(object, property);\n\n if (isPropertyExist) {\n if (arrayIsArray(value)) {\n newObject[property] = cleanArray(value);\n } else if (\n value &&\n typeof value === 'object' &&\n value.constructor === Object\n ) {\n newObject[property] = clone(value);\n } else {\n newObject[property] = value;\n }\n }\n }\n\n return newObject;\n}\n\n/**\n * Convert non-node values into strings without depending on direct property access.\n *\n * @param value - The value to stringify.\n * @returns A string representation of the provided value.\n */\nfunction stringifyValue(value: unknown): string {\n switch (typeof value) {\n case 'string': {\n return value;\n }\n\n case 'number': {\n return numberToString(value);\n }\n\n case 'boolean': {\n return booleanToString(value);\n }\n\n case 'bigint': {\n return bigintToString ? bigintToString(value) : '0';\n }\n\n case 'symbol': {\n return symbolToString ? symbolToString(value) : 'Symbol()';\n }\n\n case 'undefined': {\n return objectToString(value);\n }\n\n case 'function':\n case 'object': {\n if (value === null) {\n return objectToString(value);\n }\n\n const valueAsRecord = value as Record;\n const valueToString = lookupGetter(valueAsRecord, 'toString');\n\n if (typeof valueToString === 'function') {\n const stringified = valueToString(valueAsRecord);\n\n return typeof stringified === 'string'\n ? stringified\n : objectToString(stringified);\n }\n\n return objectToString(value);\n }\n\n default: {\n return objectToString(value);\n }\n }\n}\n\n/**\n * This method automatically checks if the prop is function or getter and behaves accordingly.\n *\n * @param object - The object to look up the getter function in its prototype chain.\n * @param prop - The property name for which to find the getter function.\n * @returns The getter function found in the prototype chain or a fallback function.\n */\nfunction lookupGetter>(\n object: T,\n prop: string\n): ReturnType> | (() => null) {\n while (object !== null) {\n const desc = getOwnPropertyDescriptor(object, prop);\n\n if (desc) {\n if (desc.get) {\n return unapply(desc.get);\n }\n\n if (typeof desc.value === 'function') {\n return unapply(desc.value);\n }\n }\n\n object = getPrototypeOf(object);\n }\n\n function fallbackValue(): null {\n return null;\n }\n\n return fallbackValue;\n}\n\nfunction isRegex(value: unknown): value is RegExp {\n try {\n regExpTest(value as RegExp, '');\n return true;\n } catch {\n return false;\n }\n}\n\nexport {\n // Array\n arrayForEach,\n arrayIndexOf,\n arrayIsArray,\n arrayLastIndexOf,\n arrayPop,\n arrayPush,\n arraySlice,\n arraySplice,\n // Object\n entries,\n freeze,\n getPrototypeOf,\n getOwnPropertyDescriptor,\n isFrozen,\n setPrototypeOf,\n seal,\n clone,\n create,\n objectHasOwnProperty,\n objectToString,\n // RegExp\n regExpTest,\n isRegex,\n // String\n stringIndexOf,\n stringMatch,\n stringReplace,\n stringToLowerCase,\n stringToString,\n stringTrim,\n // Other conversion\n stringifyValue,\n // Errors\n typeErrorCreate,\n // Other\n lookupGetter,\n addToSet,\n // Reflect\n unapply,\n unconstruct,\n};\n", "import { freeze } from './utils.js';\n\nexport const html = freeze([\n 'a',\n 'abbr',\n 'acronym',\n 'address',\n 'area',\n 'article',\n 'aside',\n 'audio',\n 'b',\n 'bdi',\n 'bdo',\n 'big',\n 'blink',\n 'blockquote',\n 'body',\n 'br',\n 'button',\n 'canvas',\n 'caption',\n 'center',\n 'cite',\n 'code',\n 'col',\n 'colgroup',\n 'content',\n 'data',\n 'datalist',\n 'dd',\n 'decorator',\n 'del',\n 'details',\n 'dfn',\n 'dialog',\n 'dir',\n 'div',\n 'dl',\n 'dt',\n 'element',\n 'em',\n 'fieldset',\n 'figcaption',\n 'figure',\n 'font',\n 'footer',\n 'form',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'head',\n 'header',\n 'hgroup',\n 'hr',\n 'html',\n 'i',\n 'img',\n 'input',\n 'ins',\n 'kbd',\n 'label',\n 'legend',\n 'li',\n 'main',\n 'map',\n 'mark',\n 'marquee',\n 'menu',\n 'menuitem',\n 'meter',\n 'nav',\n 'nobr',\n 'ol',\n 'optgroup',\n 'option',\n 'output',\n 'p',\n 'picture',\n 'pre',\n 'progress',\n 'q',\n 'rp',\n 'rt',\n 'ruby',\n 's',\n 'samp',\n 'search',\n 'section',\n 'select',\n 'shadow',\n 'slot',\n 'small',\n 'source',\n 'spacer',\n 'span',\n 'strike',\n 'strong',\n 'style',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'template',\n 'textarea',\n 'tfoot',\n 'th',\n 'thead',\n 'time',\n 'tr',\n 'track',\n 'tt',\n 'u',\n 'ul',\n 'var',\n 'video',\n 'wbr',\n] as const);\n\nexport const svg = freeze([\n 'svg',\n 'a',\n 'altglyph',\n 'altglyphdef',\n 'altglyphitem',\n 'animatecolor',\n 'animatemotion',\n 'animatetransform',\n 'circle',\n 'clippath',\n 'defs',\n 'desc',\n 'ellipse',\n 'enterkeyhint',\n 'exportparts',\n 'filter',\n 'font',\n 'g',\n 'glyph',\n 'glyphref',\n 'hkern',\n 'image',\n 'inputmode',\n 'line',\n 'lineargradient',\n 'marker',\n 'mask',\n 'metadata',\n 'mpath',\n 'part',\n 'path',\n 'pattern',\n 'polygon',\n 'polyline',\n 'radialgradient',\n 'rect',\n 'stop',\n 'style',\n 'switch',\n 'symbol',\n 'text',\n 'textpath',\n 'title',\n 'tref',\n 'tspan',\n 'view',\n 'vkern',\n] as const);\n\nexport const svgFilters = freeze([\n 'feBlend',\n 'feColorMatrix',\n 'feComponentTransfer',\n 'feComposite',\n 'feConvolveMatrix',\n 'feDiffuseLighting',\n 'feDisplacementMap',\n 'feDistantLight',\n 'feDropShadow',\n 'feFlood',\n 'feFuncA',\n 'feFuncB',\n 'feFuncG',\n 'feFuncR',\n 'feGaussianBlur',\n 'feImage',\n 'feMerge',\n 'feMergeNode',\n 'feMorphology',\n 'feOffset',\n 'fePointLight',\n 'feSpecularLighting',\n 'feSpotLight',\n 'feTile',\n 'feTurbulence',\n] as const);\n\n// List of SVG elements that are disallowed by default.\n// We still need to know them so that we can do namespace\n// checks properly in case one wants to add them to\n// allow-list.\nexport const svgDisallowed = freeze([\n 'animate',\n 'color-profile',\n 'cursor',\n 'discard',\n 'font-face',\n 'font-face-format',\n 'font-face-name',\n 'font-face-src',\n 'font-face-uri',\n 'foreignobject',\n 'hatch',\n 'hatchpath',\n 'mesh',\n 'meshgradient',\n 'meshpatch',\n 'meshrow',\n 'missing-glyph',\n 'script',\n 'set',\n 'solidcolor',\n 'unknown',\n 'use',\n] as const);\n\nexport const mathMl = freeze([\n 'math',\n 'menclose',\n 'merror',\n 'mfenced',\n 'mfrac',\n 'mglyph',\n 'mi',\n 'mlabeledtr',\n 'mmultiscripts',\n 'mn',\n 'mo',\n 'mover',\n 'mpadded',\n 'mphantom',\n 'mroot',\n 'mrow',\n 'ms',\n 'mspace',\n 'msqrt',\n 'mstyle',\n 'msub',\n 'msup',\n 'msubsup',\n 'mtable',\n 'mtd',\n 'mtext',\n 'mtr',\n 'munder',\n 'munderover',\n 'mprescripts',\n] as const);\n\n// Similarly to SVG, we want to know all MathML elements,\n// even those that we disallow by default.\nexport const mathMlDisallowed = freeze([\n 'maction',\n 'maligngroup',\n 'malignmark',\n 'mlongdiv',\n 'mscarries',\n 'mscarry',\n 'msgroup',\n 'mstack',\n 'msline',\n 'msrow',\n 'semantics',\n 'annotation',\n 'annotation-xml',\n 'mprescripts',\n 'none',\n] as const);\n\nexport const text = freeze(['#text'] as const);\n", "import { freeze } from './utils.js';\n\nexport const html = freeze([\n 'accept',\n 'action',\n 'align',\n 'alt',\n 'autocapitalize',\n 'autocomplete',\n 'autopictureinpicture',\n 'autoplay',\n 'background',\n 'bgcolor',\n 'border',\n 'capture',\n 'cellpadding',\n 'cellspacing',\n 'checked',\n 'cite',\n 'class',\n 'clear',\n 'color',\n 'cols',\n 'colspan',\n 'command',\n 'commandfor',\n 'controls',\n 'controlslist',\n 'coords',\n 'crossorigin',\n 'datetime',\n 'decoding',\n 'default',\n 'dir',\n 'disabled',\n 'disablepictureinpicture',\n 'disableremoteplayback',\n 'download',\n 'draggable',\n 'enctype',\n 'enterkeyhint',\n 'exportparts',\n 'face',\n 'for',\n 'headers',\n 'height',\n 'hidden',\n 'high',\n 'href',\n 'hreflang',\n 'id',\n 'inert',\n 'inputmode',\n 'integrity',\n 'ismap',\n 'kind',\n 'label',\n 'lang',\n 'list',\n 'loading',\n 'loop',\n 'low',\n 'max',\n 'maxlength',\n 'media',\n 'method',\n 'min',\n 'minlength',\n 'multiple',\n 'muted',\n 'name',\n 'nonce',\n 'noshade',\n 'novalidate',\n 'nowrap',\n 'open',\n 'optimum',\n 'part',\n 'pattern',\n 'placeholder',\n 'playsinline',\n 'popover',\n 'popovertarget',\n 'popovertargetaction',\n 'poster',\n 'preload',\n 'pubdate',\n 'radiogroup',\n 'readonly',\n 'rel',\n 'required',\n 'rev',\n 'reversed',\n 'role',\n 'rows',\n 'rowspan',\n 'spellcheck',\n 'scope',\n 'selected',\n 'shape',\n 'size',\n 'sizes',\n 'slot',\n 'span',\n 'srclang',\n 'start',\n 'src',\n 'srcset',\n 'step',\n 'style',\n 'summary',\n 'tabindex',\n 'title',\n 'translate',\n 'type',\n 'usemap',\n 'valign',\n 'value',\n 'width',\n 'wrap',\n 'xmlns',\n] as const);\n\nexport const svg = freeze([\n 'accent-height',\n 'accumulate',\n 'additive',\n 'alignment-baseline',\n 'amplitude',\n 'ascent',\n 'attributename',\n 'attributetype',\n 'azimuth',\n 'basefrequency',\n 'baseline-shift',\n 'begin',\n 'bias',\n 'by',\n 'class',\n 'clip',\n 'clippathunits',\n 'clip-path',\n 'clip-rule',\n 'color',\n 'color-interpolation',\n 'color-interpolation-filters',\n 'color-profile',\n 'color-rendering',\n 'cx',\n 'cy',\n 'd',\n 'dx',\n 'dy',\n 'diffuseconstant',\n 'direction',\n 'display',\n 'divisor',\n 'dur',\n 'edgemode',\n 'elevation',\n 'end',\n 'exponent',\n 'fill',\n 'fill-opacity',\n 'fill-rule',\n 'filter',\n 'filterunits',\n 'flood-color',\n 'flood-opacity',\n 'font-family',\n 'font-size',\n 'font-size-adjust',\n 'font-stretch',\n 'font-style',\n 'font-variant',\n 'font-weight',\n 'fx',\n 'fy',\n 'g1',\n 'g2',\n 'glyph-name',\n 'glyphref',\n 'gradientunits',\n 'gradienttransform',\n 'height',\n 'href',\n 'id',\n 'image-rendering',\n 'in',\n 'in2',\n 'intercept',\n 'k',\n 'k1',\n 'k2',\n 'k3',\n 'k4',\n 'kerning',\n 'keypoints',\n 'keysplines',\n 'keytimes',\n 'lang',\n 'lengthadjust',\n 'letter-spacing',\n 'kernelmatrix',\n 'kernelunitlength',\n 'lighting-color',\n 'local',\n 'marker-end',\n 'marker-mid',\n 'marker-start',\n 'markerheight',\n 'markerunits',\n 'markerwidth',\n 'maskcontentunits',\n 'maskunits',\n 'max',\n 'mask',\n 'mask-type',\n 'media',\n 'method',\n 'mode',\n 'min',\n 'name',\n 'numoctaves',\n 'offset',\n 'operator',\n 'opacity',\n 'order',\n 'orient',\n 'orientation',\n 'origin',\n 'overflow',\n 'paint-order',\n 'path',\n 'pathlength',\n 'patterncontentunits',\n 'patterntransform',\n 'patternunits',\n 'points',\n 'preservealpha',\n 'preserveaspectratio',\n 'primitiveunits',\n 'r',\n 'rx',\n 'ry',\n 'radius',\n 'refx',\n 'refy',\n 'repeatcount',\n 'repeatdur',\n 'restart',\n 'result',\n 'rotate',\n 'scale',\n 'seed',\n 'shape-rendering',\n 'slope',\n 'specularconstant',\n 'specularexponent',\n 'spreadmethod',\n 'startoffset',\n 'stddeviation',\n 'stitchtiles',\n 'stop-color',\n 'stop-opacity',\n 'stroke-dasharray',\n 'stroke-dashoffset',\n 'stroke-linecap',\n 'stroke-linejoin',\n 'stroke-miterlimit',\n 'stroke-opacity',\n 'stroke',\n 'stroke-width',\n 'style',\n 'surfacescale',\n 'systemlanguage',\n 'tabindex',\n 'tablevalues',\n 'targetx',\n 'targety',\n 'transform',\n 'transform-origin',\n 'text-anchor',\n 'text-decoration',\n 'text-rendering',\n 'textlength',\n 'type',\n 'u1',\n 'u2',\n 'unicode',\n 'values',\n 'viewbox',\n 'visibility',\n 'version',\n 'vert-adv-y',\n 'vert-origin-x',\n 'vert-origin-y',\n 'width',\n 'word-spacing',\n 'wrap',\n 'writing-mode',\n 'xchannelselector',\n 'ychannelselector',\n 'x',\n 'x1',\n 'x2',\n 'xmlns',\n 'y',\n 'y1',\n 'y2',\n 'z',\n 'zoomandpan',\n] as const);\n\nexport const mathMl = freeze([\n 'accent',\n 'accentunder',\n 'align',\n 'bevelled',\n 'close',\n 'columnalign',\n 'columnlines',\n 'columnspacing',\n 'columnspan',\n 'denomalign',\n 'depth',\n 'dir',\n 'display',\n 'displaystyle',\n 'encoding',\n 'fence',\n 'frame',\n 'height',\n 'href',\n 'id',\n 'largeop',\n 'length',\n 'linethickness',\n 'lquote',\n 'lspace',\n 'mathbackground',\n 'mathcolor',\n 'mathsize',\n 'mathvariant',\n 'maxsize',\n 'minsize',\n 'movablelimits',\n 'notation',\n 'numalign',\n 'open',\n 'rowalign',\n 'rowlines',\n 'rowspacing',\n 'rowspan',\n 'rspace',\n 'rquote',\n 'scriptlevel',\n 'scriptminsize',\n 'scriptsizemultiplier',\n 'selection',\n 'separator',\n 'separators',\n 'stretchy',\n 'subscriptshift',\n 'supscriptshift',\n 'symmetric',\n 'voffset',\n 'width',\n 'xmlns',\n]);\n\nexport const xml = freeze([\n 'xlink:href',\n 'xml:id',\n 'xlink:title',\n 'xml:space',\n 'xmlns:xlink',\n] as const);\n", "import { seal } from './utils.js';\n\nexport const MUSTACHE_EXPR = seal(/{{[\\w\\W]*|^[\\w\\W]*}}/g);\nexport const ERB_EXPR = seal(/<%[\\w\\W]*|^[\\w\\W]*%>/g);\nexport const TMPLIT_EXPR = seal(/\\${[\\w\\W]*/g);\nexport const DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]+$/); // eslint-disable-line no-useless-escape\nexport const ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\nexport const IS_ALLOWED_URI = seal(\n /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n);\nexport const IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\nexport const ATTR_WHITESPACE = seal(\n /[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n);\nexport const DOCTYPE_NAME = seal(/^html$/i);\nexport const CUSTOM_ELEMENT = seal(/^[a-z][.\\w]*(-[.\\w]+)+$/i);\n\n// Markup-significant character probes used by _sanitizeElements.\n// Shared module-level instances are safe despite the sticky /g flags:\n// unapply() resets lastIndex for RegExp receivers before every call.\nexport const ELEMENT_MARKUP_PROBE = seal(/<[/\\w!]/g);\nexport const COMMENT_MARKUP_PROBE = seal(/<[/\\w]/g);\nexport const FALLBACK_TAG_CLOSE = seal(/<\\/no(script|embed|frames)/i);\nexport const SELF_CLOSING_TAG = seal(/\\/>/i);\n", "/* eslint-disable @typescript-eslint/indent */\n\nimport type { Config, UseProfilesConfig } from './config';\nimport type { DOMPurify, HooksMap, HookFunction, WindowLike } from './types';\nimport * as TAGS from './tags.js';\nimport * as ATTRS from './attrs.js';\nimport * as EXPRESSIONS from './regexp.js';\nimport {\n addToSet,\n clone,\n entries,\n freeze,\n seal,\n arrayForEach,\n arrayIsArray,\n arrayLastIndexOf,\n arrayPop,\n arrayPush,\n arraySplice,\n stringMatch,\n stringReplace,\n stringToLowerCase,\n stringToString,\n stringIndexOf,\n stringTrim,\n regExpTest,\n isRegex,\n typeErrorCreate,\n lookupGetter,\n create,\n objectHasOwnProperty,\n stringifyValue,\n} from './utils.js';\n\nexport type { Config } from './config';\n\nexport type {\n DOMPurify,\n RemovedElement,\n RemovedAttribute,\n HookName,\n NodeHook,\n ElementHook,\n DocumentFragmentHook,\n UponSanitizeElementHook,\n UponSanitizeAttributeHook,\n UponSanitizeElementHookEvent,\n UponSanitizeAttributeHookEvent,\n WindowLike,\n} from './types';\n\ndeclare const VERSION: string;\n\n// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType\nconst NODE_TYPE = {\n element: 1,\n attribute: 2,\n text: 3,\n cdataSection: 4,\n entityReference: 5, // Deprecated\n entityNode: 6, // Deprecated\n processingInstruction: 7,\n comment: 8,\n document: 9,\n documentType: 10,\n documentFragment: 11,\n notation: 12, // Deprecated\n};\n\nconst getGlobal = function (): WindowLike {\n return typeof window === 'undefined' ? null : window;\n};\n\n/**\n * Creates a no-op policy for internal use only.\n * Don't export this function outside this module!\n * @param trustedTypes The policy factory.\n * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).\n * @return The policy created (or null, if Trusted Types\n * are not supported or creating the policy failed).\n */\nconst _createTrustedTypesPolicy = function (\n trustedTypes: TrustedTypePolicyFactory,\n purifyHostElement: HTMLScriptElement\n) {\n if (\n typeof trustedTypes !== 'object' ||\n typeof trustedTypes.createPolicy !== 'function'\n ) {\n return null;\n }\n\n // Allow the callers to control the unique policy name\n // by adding a data-tt-policy-suffix to the script element with the DOMPurify.\n // Policy creation with duplicate names throws in Trusted Types.\n let suffix = null;\n const ATTR_NAME = 'data-tt-policy-suffix';\n if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {\n suffix = purifyHostElement.getAttribute(ATTR_NAME);\n }\n\n const policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n\n try {\n return trustedTypes.createPolicy(policyName, {\n createHTML(html) {\n return html;\n },\n createScriptURL(scriptUrl) {\n return scriptUrl;\n },\n });\n } catch (_) {\n // Policy creation failed (most likely another DOMPurify script has\n // already run). Skip creating the policy, as this will only cause errors\n // if TT are enforced.\n console.warn(\n 'TrustedTypes policy ' + policyName + ' could not be created.'\n );\n return null;\n }\n};\n\nconst _createHooksMap = function (): HooksMap {\n return {\n afterSanitizeAttributes: [],\n afterSanitizeElements: [],\n afterSanitizeShadowDOM: [],\n beforeSanitizeAttributes: [],\n beforeSanitizeElements: [],\n beforeSanitizeShadowDOM: [],\n uponSanitizeAttribute: [],\n uponSanitizeElement: [],\n uponSanitizeShadowNode: [],\n };\n};\n\n/**\n * Resolve a set-valued configuration option: a fresh set built from\n * cfg[key] when it is an own array property (seeded with a clone of\n * options.base when given, case-normalized via options.transform),\n * the fallback set otherwise.\n *\n * @param cfg the cloned, prototype-free configuration object\n * @param key the configuration property to read\n * @param fallback the set to use when the option is absent or not an array\n * @param options transform and optional base set to merge into\n * @returns the resolved set\n */\nconst _resolveSetOption = function (\n cfg: Config,\n key: keyof Config,\n fallback: Record,\n options: {\n transform: Parameters[2];\n base?: Record;\n }\n): Record {\n return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key])\n ? addToSet(\n options.base ? clone(options.base) : {},\n cfg[key] as readonly unknown[],\n options.transform\n )\n : fallback;\n};\n\nfunction createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {\n const DOMPurify: DOMPurify = (root: WindowLike) => createDOMPurify(root);\n\n DOMPurify.version = VERSION;\n\n DOMPurify.removed = [];\n\n if (\n !window ||\n !window.document ||\n window.document.nodeType !== NODE_TYPE.document ||\n !window.Element\n ) {\n // Not running in a browser, provide a factory function\n // so that you can pass your own Window\n DOMPurify.isSupported = false;\n\n return DOMPurify;\n }\n\n let { document } = window;\n\n const originalDocument = document;\n const currentScript: HTMLScriptElement =\n originalDocument.currentScript as HTMLScriptElement;\n const {\n DocumentFragment,\n HTMLTemplateElement,\n Node,\n Element,\n NodeFilter,\n NamedNodeMap = window.NamedNodeMap || (window as any).MozNamedAttrMap,\n HTMLFormElement,\n DOMParser,\n trustedTypes,\n } = window;\n\n const ElementPrototype = Element.prototype;\n\n const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n const remove = lookupGetter(ElementPrototype, 'remove');\n const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n const getParentNode = lookupGetter(ElementPrototype, 'parentNode');\n const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');\n const getAttributes = lookupGetter(ElementPrototype, 'attributes');\n const getNodeType =\n Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;\n const getNodeName =\n Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;\n\n // As per issue #47, the web-components registry is inherited by a\n // new document created via createHTMLDocument. As per the spec\n // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)\n // a new empty registry is used when creating a template contents owner\n // document, so we use that as our parent document to ensure nothing\n // is inherited.\n if (typeof HTMLTemplateElement === 'function') {\n const template = document.createElement('template');\n if (template.content && template.content.ownerDocument) {\n document = template.content.ownerDocument;\n }\n }\n\n let trustedTypesPolicy;\n let emptyHTML = '';\n\n // The instance's own internal Trusted Types policy. Unlike a caller-supplied\n // `TRUSTED_TYPES_POLICY`, this is created at most once \u2014 Trusted Types throws\n // on duplicate policy names \u2014 and is the only policy allowed to persist\n // across configurations and survive `clearConfig()`.\n let defaultTrustedTypesPolicy;\n let defaultTrustedTypesPolicyResolved = false;\n\n // Tracks whether we are already inside a call to the configured Trusted Types\n // policy (`createHTML` or `createScriptURL`). If a supplied policy callback\n // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would\n // re-enter the policy and recurse until the stack overflows. We detect that\n // re-entry and throw a clear, actionable error instead. The guard is shared\n // across both callbacks, because either one re-entering `sanitize` triggers\n // the same unbounded recursion.\n let IN_TRUSTED_TYPES_POLICY = 0;\n const _assertNotInTrustedTypesPolicy = function (): void {\n if (IN_TRUSTED_TYPES_POLICY > 0) {\n throw typeErrorCreate(\n 'A configured TRUSTED_TYPES_POLICY callback (createHTML or ' +\n 'createScriptURL) must not call DOMPurify.sanitize, as that causes ' +\n 'infinite recursion. Do not pass a policy whose callbacks wrap ' +\n 'DOMPurify as TRUSTED_TYPES_POLICY; see the \"DOMPurify and Trusted ' +\n 'Types\" section of the README.'\n );\n }\n };\n\n const _createTrustedHTML = function (html: string): string {\n _assertNotInTrustedTypesPolicy();\n\n IN_TRUSTED_TYPES_POLICY++;\n try {\n return trustedTypesPolicy.createHTML(html);\n } finally {\n IN_TRUSTED_TYPES_POLICY--;\n }\n };\n\n const _createTrustedScriptURL = function (scriptUrl: string): string {\n _assertNotInTrustedTypesPolicy();\n\n IN_TRUSTED_TYPES_POLICY++;\n try {\n return trustedTypesPolicy.createScriptURL(scriptUrl);\n } finally {\n IN_TRUSTED_TYPES_POLICY--;\n }\n };\n\n // Lazily resolve (and cache) the instance's internal default policy.\n // Resolution is attempted at most once: a successful `createPolicy` cannot be\n // repeated (Trusted Types throws on duplicate names), and a failed or\n // unsupported attempt must not be retried on every parse.\n const _getDefaultTrustedTypesPolicy = function () {\n if (!defaultTrustedTypesPolicyResolved) {\n defaultTrustedTypesPolicy = _createTrustedTypesPolicy(\n trustedTypes,\n currentScript\n );\n defaultTrustedTypesPolicyResolved = true;\n }\n\n return defaultTrustedTypesPolicy;\n };\n\n const {\n implementation,\n createNodeIterator,\n createDocumentFragment,\n getElementsByTagName,\n } = document;\n const { importNode } = originalDocument;\n\n let hooks = _createHooksMap();\n\n /**\n * Expose whether this browser supports running the full DOMPurify.\n */\n DOMPurify.isSupported =\n typeof entries === 'function' &&\n typeof getParentNode === 'function' &&\n implementation &&\n implementation.createHTMLDocument !== undefined;\n\n const {\n MUSTACHE_EXPR,\n ERB_EXPR,\n TMPLIT_EXPR,\n DATA_ATTR,\n ARIA_ATTR,\n IS_SCRIPT_OR_DATA,\n ATTR_WHITESPACE,\n CUSTOM_ELEMENT,\n } = EXPRESSIONS;\n\n let { IS_ALLOWED_URI } = EXPRESSIONS;\n\n /**\n * We consider the elements and attributes below to be safe. Ideally\n * don't add any new ones but feel free to remove unwanted ones.\n */\n\n /* allowed element names */\n let ALLOWED_TAGS = null;\n const DEFAULT_ALLOWED_TAGS = addToSet({}, [\n ...TAGS.html,\n ...TAGS.svg,\n ...TAGS.svgFilters,\n ...TAGS.mathMl,\n ...TAGS.text,\n ]);\n\n /* Allowed attribute names */\n let ALLOWED_ATTR = null;\n const DEFAULT_ALLOWED_ATTR = addToSet({}, [\n ...ATTRS.html,\n ...ATTRS.svg,\n ...ATTRS.mathMl,\n ...ATTRS.xml,\n ]);\n\n /*\n * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements.\n * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)\n * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)\n * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.\n */\n let CUSTOM_ELEMENT_HANDLING = Object.seal(\n create(null, {\n tagNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n attributeNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n allowCustomizedBuiltInElements: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: false,\n },\n })\n );\n\n /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n let FORBID_TAGS = null;\n\n /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n let FORBID_ATTR = null;\n\n /* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */\n const EXTRA_ELEMENT_HANDLING = Object.seal(\n create(null, {\n tagCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n attributeCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n })\n );\n\n /* Decide if ARIA attributes are okay */\n let ALLOW_ARIA_ATTR = true;\n\n /* Decide if custom data attributes are okay */\n let ALLOW_DATA_ATTR = true;\n\n /* Decide if unknown protocols are okay */\n let ALLOW_UNKNOWN_PROTOCOLS = false;\n\n /* Decide if self-closing tags in attributes are allowed.\n * Usually removed due to a mXSS issue in jQuery 3.0 */\n let ALLOW_SELF_CLOSE_IN_ATTR = true;\n\n /* Output should be safe for common template engines.\n * This means, DOMPurify removes data attributes, mustaches and ERB\n */\n let SAFE_FOR_TEMPLATES = false;\n\n /* Output should be safe even for XML used within HTML and alike.\n * This means, DOMPurify removes comments when containing risky content.\n */\n let SAFE_FOR_XML = true;\n\n /* Decide if document with ... should be returned */\n let WHOLE_DOCUMENT = false;\n\n /* Track whether config is already set on this instance of DOMPurify. */\n let SET_CONFIG = false;\n\n /* Decide if all elements (e.g. style, script) must be children of\n * document.body. By default, browsers might move them to document.head */\n let FORCE_BODY = false;\n\n /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported).\n * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead\n */\n let RETURN_DOM = false;\n\n /* Decide if a DOM `DocumentFragment` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported) */\n let RETURN_DOM_FRAGMENT = false;\n\n /* Try to return a Trusted Type object instead of a string, return a string in\n * case Trusted Types are not supported */\n let RETURN_TRUSTED_TYPE = false;\n\n /* Output should be free from DOM clobbering attacks?\n * This sanitizes markups named with colliding, clobberable built-in DOM APIs.\n */\n let SANITIZE_DOM = true;\n\n /* Achieve full DOM Clobbering protection by isolating the namespace of named\n * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.\n *\n * HTML/DOM spec rules that enable DOM Clobbering:\n * - Named Access on Window (\u00A77.3.3)\n * - DOM Tree Accessors (\u00A73.1.5)\n * - Form Element Parent-Child Relations (\u00A74.10.3)\n * - Iframe srcdoc / Nested WindowProxies (\u00A74.8.5)\n * - HTMLCollection (\u00A74.2.10.2)\n *\n * Namespace isolation is implemented by prefixing `id` and `name` attributes\n * with a constant string, i.e., `user-content-`\n */\n let SANITIZE_NAMED_PROPS = false;\n const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';\n\n /* Keep element content when removing element? */\n let KEEP_CONTENT = true;\n\n /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead\n * of importing it into a new Document and returning a sanitized copy */\n let IN_PLACE = false;\n\n /* Allow usage of profiles like html, svg and mathMl */\n let USE_PROFILES: UseProfilesConfig | false = {};\n\n /* Tags to ignore content of when KEEP_CONTENT is true */\n let FORBID_CONTENTS = null;\n const DEFAULT_FORBID_CONTENTS = addToSet({}, [\n 'annotation-xml',\n 'audio',\n 'colgroup',\n 'desc',\n 'foreignobject',\n 'head',\n 'iframe',\n 'math',\n 'mi',\n 'mn',\n 'mo',\n 'ms',\n 'mtext',\n 'noembed',\n 'noframes',\n 'noscript',\n 'plaintext',\n 'script',\n // mirrors the selected