@shhhum/xftp-web 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- #app{font-family:system-ui,-apple-system,sans-serif;color:#333;width:100%;max-width:480px;padding:16px;box-sizing:border-box}#app .card{background:#fff;border-radius:12px;padding:32px 24px;box-shadow:0 1px 3px #0000001a;text-align:center}#app h1{font-size:1.25rem;font-weight:600;margin-bottom:24px}#app .stage{margin-top:16px}#app .drop-zone{border:2px dashed #ccc;border-radius:8px;padding:32px 16px;transition:border-color .15s,background .15s}#app .drop-zone.drag-over{border-color:#3b82f6;background:#eff6ff}#app .btn{display:inline-block;padding:10px 24px;border:none;border-radius:6px;background:#3b82f6;color:#fff;font-size:.9rem;font-weight:500;cursor:pointer;transition:background .15s}#app .btn:hover{background:#2563eb}#app .btn-secondary{background:#6b7280}#app .btn-secondary:hover{background:#4b5563}#app .hint{color:#999;font-size:.85rem;margin-top:8px}#app .expiry{margin-top:12px}#app .progress-ring{display:block;margin:0 auto 12px}#app #upload-status,#app #dl-status{font-size:.9rem;color:#666;margin-bottom:12px}#app .link-row{display:flex;gap:8px;margin-top:12px}#app .link-row input{flex:1;padding:8px 10px;border:1px solid #ccc;border-radius:6px;font-size:.85rem;background:#f9fafb}#app .success{color:#16a34a;font-weight:600}#app .error{color:#dc2626;font-weight:500;margin-bottom:12px}#app .security-note{margin-top:20px;padding:12px;background:#f0fdf4;border-radius:6px;font-size:.8rem;color:#555;text-align:left}#app .security-note p+p{margin-top:6px}#app .security-note a{color:#3b82f6;text-decoration:none}#app .security-note a:hover{text-decoration:underline}.dark #app{color:#e5e7eb;--xftp-ring-bg: #374151;--xftp-ring-fg: #60a5fa;--xftp-ring-text: #e5e7eb}.dark #app .card{background:#1f2937;box-shadow:0 1px 3px #0006}.dark #app .drop-zone{border-color:#4b5563}.dark #app .drop-zone.drag-over{border-color:#60a5fa;background:#3b82f626}.dark #app .btn-secondary{background:#4b5563}.dark #app .btn-secondary:hover{background:#374151}.dark #app .hint,.dark #app #upload-status,.dark #app #dl-status{color:#9ca3af}.dark #app .link-row input{background:#374151;border-color:#4b5563;color:#e5e7eb}.dark #app .success{color:#4ade80}.dark #app .error{color:#f87171}.dark #app .security-note{background:#22c55e1a;color:#d1d5db}.dark #app .security-note a{color:#60a5fa}
1
+ #app,[data-xftp-app]{font-family:system-ui,-apple-system,sans-serif;color:#333;width:100%;max-width:480px;padding:16px;box-sizing:border-box}#app:empty,[data-xftp-app]:empty{min-height:var(--xftp-reserve-height, 22rem)}#app .card{background:#fff;border-radius:12px;padding:32px 24px;box-shadow:0 1px 3px #0000001a;text-align:center}#app h1{font-size:1.25rem;font-weight:600;margin-bottom:24px}#app .stage{margin-top:16px}#app .drop-zone{border:2px dashed #ccc;border-radius:8px;padding:32px 16px;transition:border-color .15s,background .15s}#app .drop-zone.drag-over{border-color:#3b82f6;background:#eff6ff}#app .btn{display:inline-block;padding:10px 24px;border:none;border-radius:6px;background:#3b82f6;color:#fff;font-size:.9rem;font-weight:500;cursor:pointer;transition:background .15s}#app .btn:hover{background:#2563eb}#app .btn-secondary{background:#6b7280}#app .btn-secondary:hover{background:#4b5563}#app .hint{color:#999;font-size:.85rem;margin-top:8px}#app .expiry{margin-top:12px}#app .progress-ring{display:block;margin:0 auto 12px}#app #upload-status,#app #dl-status{font-size:.9rem;color:#666;margin-bottom:12px}#app .link-row{display:flex;gap:8px;margin-top:12px}#app .link-row input{flex:1;padding:8px 10px;border:1px solid #ccc;border-radius:6px;font-size:.85rem;background:#f9fafb}#app .success{color:#16a34a;font-weight:600}#app .error{color:#dc2626;font-weight:500;margin-bottom:12px}#app .security-note{margin-top:20px;padding:12px;background:#f0fdf4;border-radius:6px;font-size:.8rem;color:#555;text-align:left}#app .security-note p+p{margin-top:6px}#app .security-note a{color:#3b82f6;text-decoration:none}#app .security-note a:hover{text-decoration:underline}.dark #app{color:#e5e7eb;--xftp-ring-bg: #374151;--xftp-ring-fg: #60a5fa;--xftp-ring-text: #e5e7eb}.dark #app .card{background:#1f2937;box-shadow:0 1px 3px #0006}.dark #app .drop-zone{border-color:#4b5563}.dark #app .drop-zone.drag-over{border-color:#60a5fa;background:#3b82f626}.dark #app .btn-secondary{background:#4b5563}.dark #app .btn-secondary:hover{background:#374151}.dark #app .hint,.dark #app #upload-status,.dark #app #dl-status{color:#9ca3af}.dark #app .link-row input{background:#374151;border-color:#4b5563;color:#e5e7eb}.dark #app .success{color:#4ade80}.dark #app .error{color:#f87171}.dark #app .security-note{background:#22c55e1a;color:#d1d5db}.dark #app .security-note a{color:#60a5fa}
@@ -1465,4 +1465,4 @@ jv.Ɂ…,r’¡è¿¢Kf¨p‹K£QlÇè’Ñ$™Ö…5ôp jÁ¤\bl7LwH'
1465
1465
  <p class="error" id="dl-error-msg"></p>
1466
1466
  <button id="dl-retry-btn" class="btn">${F2("retry","Retry")}</button>
1467
1467
  </div>
1468
- </div>`;const j=document.getElementById("dl-ready"),w=document.getElementById("dl-progress"),q=document.getElementById("dl-error"),P=document.getElementById("dl-progress-container"),R=document.getElementById("dl-status"),Y=document.getElementById("dl-btn"),X=document.getElementById("dl-error-msg"),n0=document.getElementById("dl-retry-btn");function Z(k0){for(const T0 of[j,w,q])T0.hidden=!0;k0.hidden=!1}function r0(k0){X.innerHTML=k0,Z(q)}Y.addEventListener("click",o0),n0.addEventListener("click",()=>Z(j));async function o0(){Z(w);const k0=ws();P.innerHTML="",P.appendChild(k0.canvas),R.textContent=F2("downloading","Downloading…");const T0=ks(),B0=Q8();try{const q0=await Fk(B0,y,async U0=>{await T0.decryptAndStoreChunk(U0.dhSecret,U0.nonce,U0.body,U0.digest,U0.chunkNo)},{onProgress:(U0,G0)=>{k0.update(U0/G0*.8)}});R.textContent=F2("decrypting","Decrypting…"),k0.update(.85);const{header:D0,content:R0}=await T0.verifyAndDecrypt({size:q0.size,digest:q0.digest,key:q0.key,nonce:q0.nonce});k0.update(.95);const i2=Kk(D0.fileName),c2=new Blob([R0.buffer]),b0=URL.createObjectURL(c2),f2=document.createElement("a");f2.href=b0,f2.download=encodeURIComponent(i2),f2.style.display="none",document.body.appendChild(f2),f2.click(),document.body.removeChild(f2),setTimeout(()=>URL.revokeObjectURL(b0),1e3),k0.update(1),R.textContent=F2("downloadComplete","Download complete"),l.dispatchEvent(new CustomEvent("xftp:download-complete",{detail:{fileName:i2},bubbles:!0}))}catch(q0){const D0=q0?.message??String(q0);r0(D0),q0 instanceof tn?n0.hidden=!0:n0.hidden=!1}finally{await T0.cleanup().catch(()=>{}),GA(B0)}}}function Kk(l){let p=l;return p=p.replace(/[/\\]/g,""),p=p.replace(/[\x00-\x1f\x7f]/g,"_"),p=p.replace(/[\u202a-\u202e\u2066-\u2069]/g,""),p.length>255&&(p=p.slice(0,255)),p||"download"}function zk(l){return l<1024?l+" B":l<1024*1024?(l/1024).toFixed(1)+" KB":(l/(1024*1024)).toFixed(1)+" MB"}function ci(){return document.querySelector("[data-xftp-app]")??document.getElementById("app")}const z8=T2.ready;async function Zk(){await z8;const l=ci();l?.hasAttribute("data-defer-init")||KA(),l?.hasAttribute("data-no-hashchange")||window.addEventListener("hashchange",()=>{const p=window.location.hash.slice(1);(!p||Z8(p))&&KA()}),l?.dispatchEvent(new CustomEvent("xftp:ready",{bubbles:!0}))}function Z8(l){try{return G8(l),!0}catch{return!1}}function KA(){const l=ci(),p=window.location.hash.slice(1);p&&Z8(p)?Gk(l,p):Xk(l)}window.__xftp_initApp=async()=>{await z8,KA()};Zk().catch(l=>{const p=ci();p&&(p.innerHTML=`<div class="error"><p>${F2("initError","Failed to initialize: %error%").replace("%error%",l.message)}</p></div>`),console.error(l)});
1468
+ </div>`;const j=document.getElementById("dl-ready"),w=document.getElementById("dl-progress"),q=document.getElementById("dl-error"),P=document.getElementById("dl-progress-container"),R=document.getElementById("dl-status"),Y=document.getElementById("dl-btn"),X=document.getElementById("dl-error-msg"),n0=document.getElementById("dl-retry-btn");function Z(k0){for(const T0 of[j,w,q])T0.hidden=!0;k0.hidden=!1}function r0(k0){X.innerHTML=k0,Z(q)}Y.addEventListener("click",o0),n0.addEventListener("click",()=>Z(j));async function o0(){Z(w);const k0=ws();P.innerHTML="",P.appendChild(k0.canvas),R.textContent=F2("downloading","Downloading…");const T0=ks(),B0=Q8();try{const q0=await Fk(B0,y,async U0=>{await T0.decryptAndStoreChunk(U0.dhSecret,U0.nonce,U0.body,U0.digest,U0.chunkNo)},{onProgress:(U0,G0)=>{k0.update(U0/G0*.8)}});R.textContent=F2("decrypting","Decrypting…"),k0.update(.85);const{header:D0,content:R0}=await T0.verifyAndDecrypt({size:q0.size,digest:q0.digest,key:q0.key,nonce:q0.nonce});k0.update(.95);const i2=Kk(D0.fileName),c2=new Blob([R0.buffer]),b0=URL.createObjectURL(c2),f2=document.createElement("a");f2.href=b0,f2.download=encodeURIComponent(i2),f2.style.display="none",document.body.appendChild(f2),f2.click(),document.body.removeChild(f2),setTimeout(()=>URL.revokeObjectURL(b0),1e3),k0.update(1),R.textContent=F2("downloadComplete","Download complete"),l.dispatchEvent(new CustomEvent("xftp:download-complete",{detail:{fileName:i2},bubbles:!0}))}catch(q0){const D0=q0?.message??String(q0);r0(D0),q0 instanceof tn?n0.hidden=!0:n0.hidden=!1}finally{await T0.cleanup().catch(()=>{}),GA(B0)}}}function Kk(l){let p=l;return p=p.replace(/[/\\]/g,""),p=p.replace(/[\x00-\x1f\x7f]/g,"_"),p=p.replace(/[\u202a-\u202e\u2066-\u2069]/g,""),p.length>255&&(p=p.slice(0,255)),p||"download"}function zk(l){return l<1024?l+" B":l<1024*1024?(l/1024).toFixed(1)+" KB":(l/(1024*1024)).toFixed(1)+" MB"}function ci(){return document.querySelector("[data-xftp-app]")??document.getElementById("app")}const z8=T2.ready;async function Zk(){const l=ci();l?.hasAttribute("data-defer-init")||KA(),l?.hasAttribute("data-no-hashchange")||window.addEventListener("hashchange",()=>{const p=window.location.hash.slice(1);(!p||Z8(p))&&KA()}),await z8,l?.dispatchEvent(new CustomEvent("xftp:ready",{bubbles:!0}))}function Z8(l){try{return G8(l),!0}catch{return!1}}function KA(){const l=ci(),p=window.location.hash.slice(1);p&&Z8(p)?Gk(l,p):Xk(l)}window.__xftp_initApp=async()=>{await z8,KA()};Zk().catch(l=>{const p=ci();p&&(p.innerHTML=`<div class="error"><p>${F2("initError","Failed to initialize: %error%").replace("%error%",l.message)}</p></div>`),console.error(l)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shhhum/xftp-web",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "XFTP file transfer protocol client for web/browser environments",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
package/web/main.ts CHANGED
@@ -11,8 +11,8 @@ function getAppElement(): HTMLElement | null {
11
11
  const wasmReady = sodium.ready
12
12
 
13
13
  async function main() {
14
- await wasmReady
15
-
14
+ // Render UI immediately — no WASM needed for HTML + event listeners.
15
+ // WASM is only used later when user triggers upload/download.
16
16
  const app = getAppElement()
17
17
  if (!app?.hasAttribute('data-defer-init')) {
18
18
  initApp()
@@ -23,6 +23,7 @@ async function main() {
23
23
  if (!hash || isXFTPHash(hash)) initApp()
24
24
  })
25
25
  }
26
+ await wasmReady
26
27
  app?.dispatchEvent(new CustomEvent('xftp:ready', {bubbles: true}))
27
28
  }
28
29
 
package/web/style.css CHANGED
@@ -1,4 +1,4 @@
1
- #app {
1
+ #app, [data-xftp-app] {
2
2
  font-family: system-ui, -apple-system, sans-serif;
3
3
  color: #333;
4
4
  width: 100%;
@@ -7,6 +7,13 @@
7
7
  box-sizing: border-box;
8
8
  }
9
9
 
10
+ /* Reserve space before JS renders — prevents layout shift.
11
+ :empty stops matching once initApp() sets innerHTML.
12
+ Override with --xftp-reserve-height on the container. */
13
+ #app:empty, [data-xftp-app]:empty {
14
+ min-height: var(--xftp-reserve-height, 22rem);
15
+ }
16
+
10
17
  #app .card {
11
18
  background: #fff;
12
19
  border-radius: 12px;