diff --git a/apps/extensions/public/store.css b/apps/extensions/public/store.css index 9c028ac..65b84f2 100644 --- a/apps/extensions/public/store.css +++ b/apps/extensions/public/store.css @@ -174,3 +174,20 @@ footer { border-top: 1px solid var(--border); color: var(--muted); font-size: 13 /* Footer link list: give it room so it wraps between links, not mid-phrase. */ footer .wrap { line-height: 2; } } + +/* ── modals (replacing alert/confirm/prompt) ── */ +.modal { + border: 1px solid var(--border); + background: var(--panel); + color: var(--text); + border-radius: var(--radius); + padding: 0; + max-width: 420px; + width: calc(100% - 32px); +} +.modal::backdrop { background: rgba(3, 7, 13, 0.66); } +.modal form { padding: 22px; } +.modal-title { margin: 0 0 8px; font-size: 17px; } +.modal-text { margin: 0 0 14px; font-size: 14px; color: var(--muted); line-height: 1.5; } +.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; } +.btn.danger { background: var(--danger); color: #fff; } diff --git a/apps/extensions/public/store.js b/apps/extensions/public/store.js index 668db89..89d0a9e 100644 --- a/apps/extensions/public/store.js +++ b/apps/extensions/public/store.js @@ -113,6 +113,74 @@ function wireAutoIngest(form) { }); } + +/* ── modals ────────────────────────────────────────────────────────── + alert/confirm/prompt render as browser chrome outside the page's styling and + block the tab; a signing-key warning that looks like a phishing popup is not + the place to ask for an irreversible decision. These resolve like the native + calls they replace. */ +function openModal(build) { + return new Promise((resolve) => { + const dialog = document.createElement('dialog'); + dialog.className = 'modal'; + let settled = false; + const done = (value) => { + if (settled) return; + settled = true; + resolve(value); + if (typeof dialog.close === 'function') dialog.close(); + else { dialog.removeAttribute('open'); dialog.dispatchEvent(new Event('close')); } + }; + build(dialog, done); + document.body.append(dialog); + dialog.addEventListener('close', () => { + dialog.remove(); + if (!settled) { settled = true; resolve(undefined); } + }); + if (typeof dialog.showModal === 'function') dialog.showModal(); + else dialog.setAttribute('open', ''); + }); +} + +function modalShell(title, bodyHtml, actionsHtml) { + return `
+ + ${bodyHtml} + +
`; +} + +function uiAlert(title, message) { + return openModal((dialog, done) => { + dialog.innerHTML = modalShell(title, message ? `` : '', + ''); + dialog.querySelector('[data-ok]').addEventListener('click', () => done()); + }); +} + +function uiConfirm(title, message, { confirmLabel = 'Confirm', danger = false } = {}) { + return openModal((dialog, done) => { + dialog.innerHTML = modalShell(title, message ? `` : '', + ` + `); + dialog.querySelector('[data-cancel]').addEventListener('click', () => done(false)); + dialog.querySelector('[data-ok]').addEventListener('click', () => done(true)); + }).then((v) => v === true); +} + +/** Choose from a fixed set — a ${opts}`, + ` + `); + dialog.querySelector('[data-cancel]').addEventListener('click', () => done(null)); + dialog.querySelector('[data-ok]').addEventListener('click', () => done(dialog.querySelector('[data-choice]').value)); + }); +} + async function api(path, opts = {}) { const res = await fetch(API + path, { credentials: 'include', ...opts }); const data = await res.json().catch(() => ({})); @@ -284,25 +352,38 @@ async function renderDetail(slug, root) { hydrateIcons(root); document.getElementById('flagBtn').addEventListener('click', async () => { - const reason = prompt('Reason? (malware, privacy, broken, spam, other)', 'other'); + const reason = await uiChoose('Report this extension', 'What is wrong with it?', [ + { value: 'malware', label: 'Malware or malicious code' }, + { value: 'privacy', label: 'Privacy violation' }, + { value: 'broken', label: "Broken — doesn't work" }, + { value: 'spam', label: 'Spam or misleading listing' }, + { value: 'other', label: 'Something else' }, + ], { confirmLabel: 'Report' }); if (!reason) return; - try { await api(`/extensions/${encodeURIComponent(ext.slug)}/flag`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason }) }); alert('Thanks — flagged for review.'); } - catch (e) { alert('Could not flag: ' + e.message); } + try { + await api(`/extensions/${encodeURIComponent(ext.slug)}/flag`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason }) }); + await uiAlert('Thanks', 'Flagged for review.'); + } catch (e) { await uiAlert('Could not flag', e.message); } }); wireEditForm(ext, () => renderDetail(slug, root)); document.getElementById('keyBtn')?.addEventListener('click', async (e) => { - if (!confirm('Generate a signing key for this extension?\n\nThe key permanently sets the extension ID — it cannot be rotated later without every install having to be redone.')) return; + const go = await uiConfirm( + 'Generate a signing key?', + 'The key permanently sets this extension\u2019s ID. It cannot be rotated later without every existing install having to be redone.', + { confirmLabel: 'Generate key', danger: true }, + ); + if (!go) return; const btn = e.currentTarget; btn.disabled = true; btn.textContent = '🔑 Generating…'; try { const { crxId } = await api(`/extensions/${encodeURIComponent(ext.id)}/signing-key`, { method: 'POST' }); - alert(`Signing key created.\n\nExtension ID: ${crxId}\n\nInstall now serves a signed .crx.`); + await uiAlert('Signing key created', `Extension ID: ${crxId} — Install now serves a signed .crx.`); await renderDetail(slug, root); } catch (err) { - alert('Could not generate key: ' + err.message); + await uiAlert('Could not generate key', err.message); btn.disabled = false; btn.textContent = '🔑 Generate signing key'; } @@ -318,7 +399,7 @@ async function renderDetail(slug, root) { await api(`/extensions/${encodeURIComponent(ext.id)}/rescan`, { method: 'POST' }); await renderDetail(slug, root); } catch (err) { - alert('Could not scan: ' + err.message); + await uiAlert('Could not scan', err.message); btn.disabled = false; btn.textContent = '🛡 Re-scan'; }