From 8ec5d4820d2cab6b3cfacd43a7d58661c520ae38 Mon Sep 17 00:00:00 2001 From: AnkanMisra Date: Sun, 3 May 2026 16:52:27 +0530 Subject: [PATCH 1/2] show busy state on quick demo + refresh buttons so the ui doesn't look stuck during the 5-30s 0g anchor: extracted a runWithBusyState(btn, label, handler) helper in main.ts that disables the button, swaps its label for a spinner + the busy text, awaits the handler, and restores the original innerHTML in finally; rewired bindActionButtons to take an ActionConfig (handler + optional busy + busyLabel) so async actions (load-demo, refresh-policies, refresh-timeline) get the loader and sync helpers (presets, modal copy/close) stay zero-overhead; .btn-spinner is now color-adaptive via currentColor + transparent border-top so it reads correctly inside both .btn-primary (dark on lime) and .btn-ghost (light on dark); .btn-ghost gets the same disabled/is-busy opacity + cursor: progress treatment as .btn-primary did from the evaluate panel work --- web/src/scripts/main.ts | 63 +++++++++++++++++++++++++++++++-------- web/src/styles/global.css | 14 +++++++-- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/web/src/scripts/main.ts b/web/src/scripts/main.ts index df2dbcb..a14a094 100644 --- a/web/src/scripts/main.ts +++ b/web/src/scripts/main.ts @@ -33,25 +33,64 @@ function bindForms(): void { }); } +/** + * Disable a button + swap its label for a spinner while an async handler + * runs, then restore both. Mirrors the pattern used inside the evaluate + * form (lib/evaluate.ts) so the UX is consistent across the page. + */ +async function runWithBusyState( + btn: HTMLButtonElement, + busyLabel: string, + handler: () => void | Promise, +): Promise { + const originalLabel = btn.innerHTML; + btn.disabled = true; + btn.classList.add("is-busy"); + btn.innerHTML = `${busyLabel}`; + try { + await handler(); + } finally { + btn.classList.remove("is-busy"); + btn.disabled = false; + btn.innerHTML = originalLabel; + } +} + +interface ActionConfig { + handler: () => void | Promise; + /** Async actions render a busy state. Sync ones fire and forget. */ + busy?: boolean; + /** Label to show inside the button while it's running. */ + busyLabel?: string; +} + function bindActionButtons(): void { - const map: Record void | Promise> = { - "load-demo": loadDemo, - "refresh-policies": loadPolicies, - "refresh-timeline": loadTimeline, - "preset-safe-transfer": presetSafeTransfer, - "preset-over-cap": presetOverCap, - "preset-forbidden-approve": presetForbiddenApprove, - "preset-unknown-destination": presetUnknownDest, - "modal-copy": copyJsonModal, - "modal-close": () => closeJsonModal(), + const map: Record = { + // Async actions hit the API and should show a busy state. The Quick + // Demo path posts a policy that the server then anchors on 0G storage, + // which can take 5-30s on Galileo — the loader keeps the UI honest. + "load-demo": { handler: loadDemo, busy: true, busyLabel: "Loading demo" }, + "refresh-policies": { handler: loadPolicies, busy: true, busyLabel: "Refreshing" }, + "refresh-timeline": { handler: loadTimeline, busy: true, busyLabel: "Refreshing" }, + // Synchronous form-fill helpers. No await, no busy state. + "preset-safe-transfer": { handler: presetSafeTransfer }, + "preset-over-cap": { handler: presetOverCap }, + "preset-forbidden-approve": { handler: presetForbiddenApprove }, + "preset-unknown-destination": { handler: presetUnknownDest }, + "modal-copy": { handler: copyJsonModal }, + "modal-close": { handler: () => closeJsonModal() }, }; - for (const [action, handler] of Object.entries(map)) { + for (const [action, cfg] of Object.entries(map)) { document .querySelectorAll(`[data-action="${action}"]`) .forEach((el) => { el.addEventListener("click", (e) => { e.preventDefault(); - void handler(); + if (cfg.busy) { + void runWithBusyState(el, cfg.busyLabel ?? "Working", cfg.handler); + } else { + void cfg.handler(); + } }); }); } diff --git a/web/src/styles/global.css b/web/src/styles/global.css index fcddeea..9e59c70 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -268,18 +268,28 @@ body::after { content: none; } +/* Color-adaptive spinner: inherits the button's text color via currentColor + * so it looks right inside both .btn-primary (dark on lime) and .btn-ghost + * (light on dark). Three quarters of the ring is currentColor; the fourth + * is transparent — the rotation reads as a spinning gap. */ .btn-spinner { display: inline-block; width: 0.85rem; height: 0.85rem; border-radius: 50%; - border: 2px solid rgba(12, 13, 12, 0.25); - border-top-color: var(--ink-bg); + border: 2px solid currentColor; + border-top-color: transparent; + opacity: 0.9; animation: btn-spin 0.7s linear infinite; } @keyframes btn-spin { to { transform: rotate(360deg); } } +.btn-ghost:disabled, +.btn-ghost.is-busy { + opacity: 0.7; + cursor: progress; +} .btn-primary::after { content: "→"; font-family: var(--serif); From 84b5d3e82387e25e14855d4b4be40f362139eb6f Mon Sep 17 00:00:00 2001 From: AnkanMisra Date: Sun, 3 May 2026 17:40:20 +0530 Subject: [PATCH 2/2] fix the spinner-label collision on .btn-ghost busy state by giving it the same inline-flex + gap shape as .btn-primary; the screenshot shared by user showed the spinner glyph touching the leading L of LOADING DEMO because .btn-ghost was display: inline-block (browser default for buttons), so the two spans the busy helper injects (Loading demo) sat flush against each other; matching the .btn-primary 0.5rem gap fixes it for both Quick Demo and the two Refresh buttons --- web/src/styles/global.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 9e59c70..ed219ae 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -285,6 +285,17 @@ body::after { @keyframes btn-spin { to { transform: rotate(360deg); } } +/* The busy state injects a `` next to the label + * span. .btn-ghost was previously `display: inline-block` which leaves both + * spans flush against each other and visibly collides the spinner with the + * leading "L" of "LOADING DEMO". Match the .btn-primary inline-flex + gap + * shape so the spinner gets proper breathing room from the text. */ +.btn-ghost { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} .btn-ghost:disabled, .btn-ghost.is-busy { opacity: 0.7;