Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 51 additions & 12 deletions web/src/scripts/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>,
): Promise<void> {
const originalLabel = btn.innerHTML;
btn.disabled = true;
btn.classList.add("is-busy");
btn.innerHTML = `<span class="btn-spinner" aria-hidden="true"></span><span>${busyLabel}</span>`;
try {
await handler();
} finally {
btn.classList.remove("is-busy");
btn.disabled = false;
btn.innerHTML = originalLabel;
}
}

interface ActionConfig {
handler: () => void | Promise<void>;
/** 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<string, () => void | Promise<void>> = {
"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<string, ActionConfig> = {
// 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<HTMLButtonElement>(`[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();
}
});
});
}
Expand Down
25 changes: 23 additions & 2 deletions web/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -268,18 +268,39 @@ 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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix currentColor casing to satisfy Stylelint value-keyword-case

CSS is case-insensitive with respect to currentColor, so browsers render it correctly regardless of casing, but the project's Stylelint config (value-keyword-case) requires the all-lowercase form and will error here.

Proposed fix
-  border: 2px solid currentColor;
+  border: 2px solid currentcolor;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
border: 2px solid currentColor;
border: 2px solid currentcolor;
🧰 Tools
🪛 Stylelint (17.9.0)

[error] 280-280: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/styles/global.css` at line 280, The border declaration uses the
non-lowercase keyword "currentColor" which breaks the Stylelint rule
value-keyword-case; update the declaration in global.css where "border: 2px
solid currentColor;" appears to use the lowercase form "currentcolor" (i.e.,
"border: 2px solid currentcolor;") so the rule passes while preserving the same
rendering.

border-top-color: transparent;
opacity: 0.9;
animation: btn-spin 0.7s linear infinite;
}
@keyframes btn-spin {
to { transform: rotate(360deg); }
}
/* The busy state injects a `<span class="btn-spinner">` 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;
cursor: progress;
}
Comment on lines +299 to +303
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

.btn-ghost.is-busy:hover overrides the busy state — rule must come after :hover

.btn-ghost:hover at line 325 has identical specificity (0,2,0) and appears later in the cascade, so it wins. Hovering a busy ghost button will apply the hover color, border-color, and background, making it look interactive and undoing the visual "disabled" feedback. The .btn-primary counterpart avoids this by placing its disabled rule after its hover rule (lines 259–265 vs 255–258).

The fix requires two things: move the block after .btn-ghost:hover, and explicitly reset the properties that hover changes (since just ordering is not enough — the disabled rule must actively reset them):

Proposed fix
-.btn-ghost:disabled,
-.btn-ghost.is-busy {
-  opacity: 0.7;
-  cursor: progress;
-}
 .btn-ghost {
   font-family: var(--mono);
   ...
 }
 .btn-ghost:hover {
   color: var(--paper);
   border-color: var(--paper-soft);
   background: var(--ink-2);
 }
+/* Must follow :hover so these values win in the cascade */
+.btn-ghost:disabled,
+.btn-ghost.is-busy {
+  opacity: 0.7;
+  cursor: progress;
+  color: var(--paper-soft);
+  border-color: var(--rule);
+  background: transparent;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.btn-ghost:disabled,
.btn-ghost.is-busy {
opacity: 0.7;
cursor: progress;
}
.btn-ghost:disabled,
.btn-ghost.is-busy {
opacity: 0.7;
cursor: progress;
color: var(--paper-soft);
border-color: var(--rule);
background: transparent;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/styles/global.css` around lines 299 - 303, The
`.btn-ghost.is-busy`/`.btn-ghost:disabled` rule is being overridden by the later
`.btn-ghost:hover` rule; move the `.btn-ghost:disabled, .btn-ghost.is-busy { ...
}` block so it appears after `.btn-ghost:hover` and explicitly reset the
hover-affected properties (color, border-color, background, cursor, and opacity)
inside that disabled/busy rule to ensure hover cannot override the busy/disabled
styling; reference the selectors `.btn-ghost:disabled`, `.btn-ghost.is-busy`,
and `.btn-ghost:hover` when making the change.

.btn-primary::after {
content: "→";
font-family: var(--serif);
Expand Down
Loading