Skip to content

Commit 38288df

Browse files
ralyodioclaude
andcommitted
feat(store): replace alert/confirm/prompt with <dialog>
Native dialogs render as browser chrome outside the page's styling and block the tab. The worst offender was the signing-key warning: an irreversible, identity-setting decision asked through a grey OS box that looks indistinguishable from a phishing popup. Adds openModal/uiAlert/uiConfirm/uiChoose on <dialog>, and replaces all six call sites. The abuse report was a prompt() asking people to type one of "malware, privacy, broken, spam, other" — it is now a <select> of exactly those, so a typo can't produce an unclassifiable report. The signing-key confirm is styled destructive. Falls back to toggling `open` and firing `close` where the <dialog> methods are unavailable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c688f3f commit 38288df

2 files changed

Lines changed: 105 additions & 7 deletions

File tree

apps/extensions/public/store.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,20 @@ footer { border-top: 1px solid var(--border); color: var(--muted); font-size: 13
174174
/* Footer link list: give it room so it wraps between links, not mid-phrase. */
175175
footer .wrap { line-height: 2; }
176176
}
177+
178+
/* ── <dialog> modals (replacing alert/confirm/prompt) ── */
179+
.modal {
180+
border: 1px solid var(--border);
181+
background: var(--panel);
182+
color: var(--text);
183+
border-radius: var(--radius);
184+
padding: 0;
185+
max-width: 420px;
186+
width: calc(100% - 32px);
187+
}
188+
.modal::backdrop { background: rgba(3, 7, 13, 0.66); }
189+
.modal form { padding: 22px; }
190+
.modal-title { margin: 0 0 8px; font-size: 17px; }
191+
.modal-text { margin: 0 0 14px; font-size: 14px; color: var(--muted); line-height: 1.5; }
192+
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; }
193+
.btn.danger { background: var(--danger); color: #fff; }

apps/extensions/public/store.js

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,74 @@ function wireAutoIngest(form) {
113113
});
114114
}
115115

116+
117+
/* ── <dialog> modals ──────────────────────────────────────────────────────────
118+
alert/confirm/prompt render as browser chrome outside the page's styling and
119+
block the tab; a signing-key warning that looks like a phishing popup is not
120+
the place to ask for an irreversible decision. These resolve like the native
121+
calls they replace. */
122+
function openModal(build) {
123+
return new Promise((resolve) => {
124+
const dialog = document.createElement('dialog');
125+
dialog.className = 'modal';
126+
let settled = false;
127+
const done = (value) => {
128+
if (settled) return;
129+
settled = true;
130+
resolve(value);
131+
if (typeof dialog.close === 'function') dialog.close();
132+
else { dialog.removeAttribute('open'); dialog.dispatchEvent(new Event('close')); }
133+
};
134+
build(dialog, done);
135+
document.body.append(dialog);
136+
dialog.addEventListener('close', () => {
137+
dialog.remove();
138+
if (!settled) { settled = true; resolve(undefined); }
139+
});
140+
if (typeof dialog.showModal === 'function') dialog.showModal();
141+
else dialog.setAttribute('open', '');
142+
});
143+
}
144+
145+
function modalShell(title, bodyHtml, actionsHtml) {
146+
return `<form method="dialog">
147+
<h2 class="modal-title">${esc(title)}</h2>
148+
${bodyHtml}
149+
<div class="modal-actions">${actionsHtml}</div>
150+
</form>`;
151+
}
152+
153+
function uiAlert(title, message) {
154+
return openModal((dialog, done) => {
155+
dialog.innerHTML = modalShell(title, message ? `<p class="modal-text">${esc(message)}</p>` : '',
156+
'<button type="button" class="btn" data-ok>OK</button>');
157+
dialog.querySelector('[data-ok]').addEventListener('click', () => done());
158+
});
159+
}
160+
161+
function uiConfirm(title, message, { confirmLabel = 'Confirm', danger = false } = {}) {
162+
return openModal((dialog, done) => {
163+
dialog.innerHTML = modalShell(title, message ? `<p class="modal-text">${esc(message)}</p>` : '',
164+
`<button type="button" class="btn secondary" data-cancel>Cancel</button>
165+
<button type="button" class="btn${danger ? ' danger' : ''}" data-ok>${esc(confirmLabel)}</button>`);
166+
dialog.querySelector('[data-cancel]').addEventListener('click', () => done(false));
167+
dialog.querySelector('[data-ok]').addEventListener('click', () => done(true));
168+
}).then((v) => v === true);
169+
}
170+
171+
/** Choose from a fixed set — a <select> beats asking someone to type a keyword. */
172+
function uiChoose(title, message, options, { confirmLabel = 'Submit' } = {}) {
173+
return openModal((dialog, done) => {
174+
const opts = options.map((o) => `<option value="${esc(o.value)}">${esc(o.label)}</option>`).join('');
175+
dialog.innerHTML = modalShell(title,
176+
`${message ? `<p class="modal-text">${esc(message)}</p>` : ''}<select class="acct-select" data-choice>${opts}</select>`,
177+
`<button type="button" class="btn secondary" data-cancel>Cancel</button>
178+
<button type="button" class="btn" data-ok>${esc(confirmLabel)}</button>`);
179+
dialog.querySelector('[data-cancel]').addEventListener('click', () => done(null));
180+
dialog.querySelector('[data-ok]').addEventListener('click', () => done(dialog.querySelector('[data-choice]').value));
181+
});
182+
}
183+
116184
async function api(path, opts = {}) {
117185
const res = await fetch(API + path, { credentials: 'include', ...opts });
118186
const data = await res.json().catch(() => ({}));
@@ -284,25 +352,38 @@ async function renderDetail(slug, root) {
284352
hydrateIcons(root);
285353

286354
document.getElementById('flagBtn').addEventListener('click', async () => {
287-
const reason = prompt('Reason? (malware, privacy, broken, spam, other)', 'other');
355+
const reason = await uiChoose('Report this extension', 'What is wrong with it?', [
356+
{ value: 'malware', label: 'Malware or malicious code' },
357+
{ value: 'privacy', label: 'Privacy violation' },
358+
{ value: 'broken', label: "Broken — doesn't work" },
359+
{ value: 'spam', label: 'Spam or misleading listing' },
360+
{ value: 'other', label: 'Something else' },
361+
], { confirmLabel: 'Report' });
288362
if (!reason) return;
289-
try { await api(`/extensions/${encodeURIComponent(ext.slug)}/flag`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason }) }); alert('Thanks — flagged for review.'); }
290-
catch (e) { alert('Could not flag: ' + e.message); }
363+
try {
364+
await api(`/extensions/${encodeURIComponent(ext.slug)}/flag`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason }) });
365+
await uiAlert('Thanks', 'Flagged for review.');
366+
} catch (e) { await uiAlert('Could not flag', e.message); }
291367
});
292368

293369
wireEditForm(ext, () => renderDetail(slug, root));
294370

295371
document.getElementById('keyBtn')?.addEventListener('click', async (e) => {
296-
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;
372+
const go = await uiConfirm(
373+
'Generate a signing key?',
374+
'The key permanently sets this extension\u2019s ID. It cannot be rotated later without every existing install having to be redone.',
375+
{ confirmLabel: 'Generate key', danger: true },
376+
);
377+
if (!go) return;
297378
const btn = e.currentTarget;
298379
btn.disabled = true;
299380
btn.textContent = '🔑 Generating…';
300381
try {
301382
const { crxId } = await api(`/extensions/${encodeURIComponent(ext.id)}/signing-key`, { method: 'POST' });
302-
alert(`Signing key created.\n\nExtension ID: ${crxId}\n\nInstall now serves a signed .crx.`);
383+
await uiAlert('Signing key created', `Extension ID: ${crxId} — Install now serves a signed .crx.`);
303384
await renderDetail(slug, root);
304385
} catch (err) {
305-
alert('Could not generate key: ' + err.message);
386+
await uiAlert('Could not generate key', err.message);
306387
btn.disabled = false;
307388
btn.textContent = '🔑 Generate signing key';
308389
}
@@ -318,7 +399,7 @@ async function renderDetail(slug, root) {
318399
await api(`/extensions/${encodeURIComponent(ext.id)}/rescan`, { method: 'POST' });
319400
await renderDetail(slug, root);
320401
} catch (err) {
321-
alert('Could not scan: ' + err.message);
402+
await uiAlert('Could not scan', err.message);
322403
btn.disabled = false;
323404
btn.textContent = '🛡 Re-scan';
324405
}

0 commit comments

Comments
 (0)