@@ -426,20 +426,46 @@ speed.addEventListener("change", () => viewer?.setSpeed(Number(speed.value)));
426426// --- Button label feedback ---
427427// Swap a button's label (the inner `.lbl` span when present, else the button
428428// text) to a transient message, then restore it.
429- function flash ( btn : HTMLElement , message : string ) : void {
429+ const flashTimers = new WeakMap < HTMLElement , number > ( ) ;
430+
431+ function flash (
432+ btn : HTMLElement ,
433+ message : string ,
434+ status : "pending" | "success" | "error" ,
435+ duration = 4000 ,
436+ ) : void {
430437 const el = btn . querySelector < HTMLElement > ( ".lbl" ) ?? btn ;
431- const prev = el . textContent ;
438+ const previousTimer = flashTimers . get ( btn ) ;
439+ if ( previousTimer !== undefined ) window . clearTimeout ( previousTimer ) ;
440+
441+ const defaultLabel = el . dataset . defaultLabel ?? el . textContent ?? "" ;
442+ const defaultAriaLabel =
443+ btn . dataset . defaultAriaLabel ?? btn . getAttribute ( "aria-label" ) ?? "" ;
444+ el . dataset . defaultLabel = defaultLabel ;
445+ btn . dataset . defaultAriaLabel = defaultAriaLabel ;
432446 el . textContent = message ;
433- window . setTimeout ( ( ) => ( el . textContent = prev ) , 1500 ) ;
447+ btn . dataset . status = status ;
448+ btn . setAttribute ( "aria-label" , message ) ;
449+ if ( duration === 0 ) return ;
450+
451+ const timer = window . setTimeout ( ( ) => {
452+ el . textContent = defaultLabel ;
453+ delete btn . dataset . status ;
454+ if ( defaultAriaLabel === "" ) btn . removeAttribute ( "aria-label" ) ;
455+ else btn . setAttribute ( "aria-label" , defaultAriaLabel ) ;
456+ flashTimers . delete ( btn ) ;
457+ } , duration ) ;
458+ flashTimers . set ( btn , timer ) ;
434459}
435460
436461// --- Copy LLM prompt (topbar) ---
437462async function copyPrompt ( btn : HTMLButtonElement ) : Promise < void > {
463+ flash ( btn , "Copying…" , "pending" , 0 ) ;
438464 try {
439465 await navigator . clipboard . writeText ( llmPrompt ) ;
440- flash ( btn , "Copied ✓" ) ;
466+ flash ( btn , "Copied ✓" , "success" ) ;
441467 } catch {
442- flash ( btn , "Copy failed" ) ;
468+ flash ( btn , "Copy failed" , "error" ) ;
443469 }
444470}
445471copyBtn . addEventListener ( "click" , ( ) => copyPrompt ( copyBtn ) ) ;
@@ -449,22 +475,23 @@ copyBtn.addEventListener("click", () => copyPrompt(copyBtn));
449475// (so it's bookmarkable), and copy the full link to the clipboard.
450476async function shareLink ( ) : Promise < void > {
451477 if ( ! editorApi ) return ; // editor still loading; nothing to snapshot yet
478+ flash ( shareBtn , "Copying…" , "pending" , 0 ) ;
452479 try {
453480 const source = editorApi . getValue ( ) ;
454481 const path = buildNicePlayPath ( source ) ;
455482 const hash = path === "/play" ? buildNiceShareHash ( source ) : "" ;
456483 const url = `${ location . origin } ${ path } ${ hash } ` ;
457484 history . replaceState ( null , "" , `${ path } ${ hash } ` ) ;
458485 await navigator . clipboard . writeText ( url ) ;
459- flash ( shareBtn , "Link copied ✓" ) ;
486+ flash ( shareBtn , "Link copied ✓" , "success" ) ;
460487 } catch ( err ) {
461488 const message =
462489 err instanceof TypeError
463490 ? "Nothing to share"
464491 : err instanceof RangeError
465492 ? "Too long to link"
466493 : "Copy failed" ;
467- flash ( shareBtn , message ) ;
494+ flash ( shareBtn , message , "error" ) ;
468495 }
469496}
470497shareBtn . addEventListener ( "click" , shareLink ) ;
0 commit comments