@@ -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+
116184async 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