feat(runner): DEV-2163 T9 undesigned surfaces + the After Login frames - #99
Merged
demtario merged 9 commits intoJul 31, 2026
Merged
Conversation
T9 was written when no Figma frames existed for any of its surfaces, so its instruction was ADR-0023 rule 1: keep the feature, restyle it to the token set, to dev judgment. An "After Login" section (114:23287) has since been added to the file and supplies real frames for several of them, specifying new structure rather than a restyle. The subtask splits three ways. Designed after all, built to frame: * My Demos becomes a route (114:25521) — left nav, card grid, kebab (Open/Copy link/Fork/Rename/Delete), Create tile — not a 340px drawer. * Account menu in the top bar (114:21480), avatar + My demos/Settings/Log out. * Share links dialog (114:23289) and a new Edit info dialog (114:24410), the latter behind the BOX INFO pencil, replacing two bare inputs that had been parked in the unframed authed action bar for want of a frame. * A shared Dialog primitive, because there was none: both hand-rolled overlays lacked role="dialog", a focus trap and Escape. * Five icons per ADR-0024, four read off layer names. Still undesigned, dev judgment: * Error states keep their handling verbatim — same statuses, same triggers. Only the body changes: the worker's 404/410 were bare text/plain, so a revoked share link rendered unbranded. Document requests now get a self-contained page; /api/* keeps JSON, missing assets keep plain text. * Favicon/titles/meta resolved by reusing the docs site's shipped assets, wired with the same prefers-color-scheme pair. The title is now route-aware; it said "Authoring" on public /share/:id. * Responsive is deliberately not a breakpoint system — no frame to build to. Two fixes only: the splitter re-clamp (open item 19) and auto-fill wrapping. * Delete keeps a confirmation (there was none), the empty state stays simple, and revoked demos stay visible and muted. Split out: the Settings page (114:26833) needs a profile table, a migration, endpoints and avatar storage. Per ADR-0023 rule 2 it becomes its own task; the menu row ships disabled, which is how the frame draws it. Also fixes a bug the Edit info dialog would have exposed: PATCH coalesced description with `??`, so sending null restored the old text and a description could never be cleared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…olves Bugbot, PR #99. `ShareRoute` seeded `accountUser` as `null`, which reads as "anonymous, confirmed" rather than "not known yet". `currentUser()` round-trips the external login broker, so for a few hundred milliseconds the top bar offered a signed-in visitor a working Sign in button — reintroducing exactly what the T9 identity split exists to remove. The resolve now has three states. `undefined` is pending, and `accountPending` withholds `onSignIn` while it holds, so the window renders neither control instead of the wrong one. `Gate` resolves the user before `Authoring` mounts, so `play` and `edit` never enter that state and default to `false`. Verified against the real broker code path with the request throttled to 1.5s: signed-in and anonymous both show neither control while pending, then the account menu and Sign in respectively. Note `apps/authoring/.env.local` sets VITE_DEV_USER, which short-circuits `currentUser()` before the fetch — testing this needs `VITE_DEV_USER=` or it exercises the bypass instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t accent Bugbot, PR #99. `error-page.ts` restates a handful of tokens as literals because the worker cannot import the shell, and its header promises they mirror `theme.ts`. Dark `accent` didn't: it held `#4669F6`, which is dark `splitterActive` — a different token that happens to also be a lifted blue. So the "Back to the playground" link on the branded 404/410 pages was a different blue from every other accent in the app. `accent` is `#1A42E8` in both modes in `theme.ts` — it is the brand colour and deliberately mode-invariant. Corrected, with a comment so the duplication isn't "tidied" away later. Mirroring it faithfully does surface a pre-existing contrast problem: `#1A42E8` on the dark card `#222222` is ~2.3:1, under AA. That is not this page's to fix unilaterally — the shell renders accent-coloured text on dark elsewhere too, so it belongs in `theme.ts` applied everywhere at once. Logged as open item 38. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bugbot, PR #99. `busy` was a single `{id, what}` slot. Each card disables its own kebab, but only its own — nothing stopped a fork starting on a second card while the first was still running. The second `setBusy` overwrote the slot, so the first card silently dropped its spinner and re-enabled, and whichever request settled first cleared the other one's in-flight state with it. Now a `Record<id, "fork" | "delete">`, added and removed per demo, so two cards can be busy independently. Verified with two overlapping forks where the first fails while the second is still in flight: A releases, B stays busy. The probe discriminates — under the old single slot A would already have read enabled the moment B started. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two Bugbot findings on PR #99. Dialog focused the close button, not the first field. It took `focusables()[0]`, and the X sits in the title row above `children`, so it is always first in the DOM — the comment claimed "the first field" and the code never did that. Edit info and Rename therefore opened with focus on the X: typing did nothing and Space dismissed the dialog. Initial focus now prefers the first focusable inside the content. That alone would have made the delete confirmation focus its *Delete* button, where Space or Enter carries out the thing being confirmed. Dialog honours a `data-autofocus` marker in the content ahead of DOM order, and the confirmation points it at Cancel. The share dialog's middle row was labelled from the frame as "example only — embed in any iframe". Both halves are false: `?mode=full` has carried the design's chrome since T8, and the page iframes `/d/:id/`, which sends `frame-ancestors 'self'` and `X-Frame-Options: SAMEORIGIN` — a third-party ancestor blocks the inner demo. Copying it would have pointed anyone wanting a bare embed at the one URL that cannot do it. Now "Full-window (the demo without the editor)"; the discrepancy is open item 39, to confirm with design in case the copy was a feature request rather than a description. Verified: Edit info focuses the title input and typed characters land in it; the confirmation focuses Cancel and Space closes it without revoking anything; the share rows read as intended. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s login Bugbot, PR #99. `logout()` cleared the token and reloaded the current URL. On `/my-demos` and `/edit/:id` a null user is the cue to call `login()`, so the reload sent whoever had just logged out straight back to the broker with `return_to` pointing at the page they were trying to leave. `logout()` takes an optional `returnTo`. Reloading in place stays right for `/` and `/share/:id`, which render fine anonymously and keep the example on screen; the two gated routes pass `/` instead. `/edit/:id` had this before T9 — Log out lived in the authed action bar there too — but `/my-demos` and its auto-login are new here, so both are fixed together. Verified with the broker's /login endpoint intercepted and counted: 0 hits from either Log out control on /my-demos, landing on the playground. The counter is not blind — visiting /my-demos anonymously registers 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s in flight Bugbot, PR #99. Cancel, the X, Escape and the scrim all cleared `confirming` without touching the request, so dismissing the dialog after pressing Delete still revoked the demo — the dialog said no and the demo went anyway. Aborting is not the fix. `AbortController` stops the client listening; the worker already has the request and the row is revoked regardless. A Cancel that left the demo deleted would be a worse lie than a control that briefly refuses. So every dismissal is withheld until the request settles, which is one round trip, and the button reads "Deleting…" while it does. Also guards `remove`'s `finally`, which cleared `confirming` unconditionally and could close a confirmation opened for a different card. Holding the dialog makes that unreachable — a second one can no longer be opened mid-flight — but the guard is correct on its own terms and costs nothing. Verified with the DELETE held open for 3s: Cancel disabled, Escape does not dismiss, dialog closes on settle, exactly one DELETE sent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… close Two Bugbot findings on PR #99. The My Demos grid scrolls, and a card near the bottom opened its menu past the fold — measured with the last card's Delete row at y=631 against a pane bottom of 620. The pane can be scrolled to reach it, so nothing was truly unreachable, but a menu that appears off-screen reads as broken. It now measures the rendered popover and flips to `bottom: 100%` when there is no room below and there is room above. Not an unconditional flip: on a viewport too short for either direction, dropping up would clip against the top instead. `?edit=info` — the one-shot instruction My Demos' Rename navigates with — was never cleared. It outlived the dialog it opened, so a reload reopened one the user had already dismissed or saved, and the URL was wrong if copied. Closing and saving now strip it via `replaceState`. Verified: the last card's menu lands inside the pane (452 vs 620) while the first card still opens downwards; Rename opens the dialog, Cancel leaves `/edit/:id` with no query, and a reload does not reopen it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fcd2928. Configure here.
Bugbot, PR #99. The trap only recovered focus from outside the card on Shift+Tab. Forward Tab from outside matched neither branch, so the browser's default took over and walked into the page behind the scrim. Focus gets outside without the user ever tabbing out: disabling the control that has it drops focus to <body>. The delete confirmation does precisely that — pressing Delete disables both Delete and Cancel for the duration of the request, and dismissal is blocked meanwhile, so the modal was both inescapable and leaking focus at the same time. Now recovers on either direction, and parks on the card if every control is disabled rather than letting Tab through. Verified with the DELETE held open: focus is confirmed on <body> during flight (the precondition), and five forward Tabs plus a Shift+Tab all land on the close button inside the dialog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Depends on T0 + T1. Closes DEV-2163. PRs into
feat/DEV-2027-redesign, per ADR-0023's one-deploy rule.The premise changed
T9 was written when no Figma frames existed for any of its surfaces, so its instruction was ADR-0023 rule 1: keep the feature, restyle it to the token set, to dev judgment. An After Login section (
114:23287) has since been added to the file and supplies real frames for several of them — specifying new structure, not a restyle. So the subtask splits three ways.A — designed after all, built to frame
114:25521MyDemos.tsx, rewritten from a 340px drawer into a route114:21480AccountMenu.tsx, inTopBar114:23289ShareLinks.tsx, rebuilt114:24410EditInfoDialog.tsx(new)114:21684BoxInfo'sonEdit, opening the aboveThe card grid, the kebab (Open / Copy link / Fork / Rename / Delete) and the
+ Createtile come from114:25521/114:27012. Rename routes to/edit/:id?edit=inforather than editing in place — a demo's title and its code are one PATCH and the dialog already lives there; the param is what keeps Rename from being a second Open.Two supporting pieces the frames imply but don't draw: a shared
Dialogprimitive (there was none — both hand-rolled overlays lackedrole="dialog", a focus trap and Escape), and five icons per ADR-0024, four of them read off layer names.B — still undesigned, dev judgment
Error states keep their handling verbatim — same statuses, same triggers, same conditions. Only the body changed:
share.ts's 404/410 andindex.ts's catch-all were baretext/plain, so a revoked share link rendered browser-default with zero branding. Newworkers/api/src/error-page.tsgives document requests a self-contained branded page;/api/*keeps JSON and a missing hashed asset keeps plain text (wantsHtmlError).Favicon / titles / meta resolved by reuse rather than a new asset:
favicon.png,favicon-dark.pngandhandsontable-banner-og.pngcopied fromhandsontable/handsontable@develop'sdocs/public/, wired with the sameprefers-color-schemelink pair the docs site uses. The title is now route-aware — it said "Authoring" on public/share/:id.Responsive is deliberately not a breakpoint system; there is no frame to build to. Two concrete fixes only: the splitter re-clamp (open item 19) and
auto-fillcard wrapping. A breakpoint study stays deferred pending design.Delete keeps a confirmation (there was none —
remove()firedDELETEon the first click and swallowed every failure), the empty state stays simple, and revoked demos stay visible and muted.C — split out
The Settings page (
114:26833) needs a profile table, a migration, profile endpoints and avatar storage — none of which exist. Per ADR-0023 rule 2 it becomes its own task, recorded as plan open item 33. The menu row ships disabled, which is exactly how114:21480draws it. No sibling ticket has been created yet.Behaviour changes worth a look
authedExtrasis now empty. The title/description inputs and the My demos / identity / Log out buttons lived in the unframed authed action bar only because ADR-0023 had nowhere else to put them. All of it now has a frame.accountEmail, notauthed./share/:idrenders the workspace anonymous on purpose, and it used to offer a signed-in visitor "Sign in".ShareRouteresolves the identity for the menu alone, never as a render gate.descriptioncan now be cleared. Both PATCH branches coalesced with??, so thenullthe client sends for an emptied field restored the old text. Invisible while it was a bare input; obviously broken behind a Save button.Verification
pnpm build→pnpm -r typecheck→ 67/67 tests →wrangler deploy --dry-run→ e2e 20 pass / 0 fail. Hex-literal grep clean outsidetheme.ts;error-page.tsis added to ADR-0022's exception list as the fourth sanctioned one (the worker can't import the shell).Driven live in both themes: the My Demos grid, its empty / revoked / confirm states, the account menu, both dialogs,
?edit=info, and route-aware titles. Against a local worker:/d/x/→ branded 404, a real revoked row → branded 410, missing asset → plain text,/api/*→ JSON.Hovers checked with a real pointer and
getComputedStyle, which caught three things a screenshot can't. Open item 16 is not only aboutbackground: an inlinecolorleft the Delete row red-on-red, and an inlinebordershorthand killed the card hover via the longhand. Third, a revoked card's kebab was five dead ends —getDemoSourcereturns null oncerevokedis set, so Open and Rename both land on "This demo is unavailable." Revoked cards now carry no kebab. All three fixed and re-verified; item 36 documents the wider trap.One caveat for reviewers: the 20 passing e2e specs prove nothing about T9 — nothing in
e2e/touches/my-demos, the dialogs, the account menu or the error pages. I grepped the gated suites for every control removed fromauthedExtrasand found no references, so nothing silently broke. But there is no visual-regression harness in this repo, so light/dark correctness rests on the manual pass above.🤖 Generated with Claude Code
Note
Medium Risk
Large authoring-shell and routing changes affect auth UX on share/edit/my-demos and demo PATCH semantics; worker error HTML is additive but touches public demo URLs.
Overview
T9 (DEV-2163) lands the After Login Figma surfaces and related polish: My Demos is no longer a side drawer—it is an auth-gated
/my-demosroute with a card grid, kebab actions (open, copy link, fork, rename via?edit=info, delete with confirmation), revoked-state handling, and per-demo busy state. Title/description editing moves intoEditInfoDialogbehind the BOX INFO pencil; Share links and delete confirm use a new sharedDialog(focus trap, Escape,role="dialog"). The top bar now shows anAccountMenukeyed offaccountEmail, not workspaceauthed, so signed-in visitors on/share/:idget their menu instead of Sign in while the editor stays read-only (ShareRouteresolves identity without gating render).Routing and chrome:
useDocumentTitlesets tab titles per route/demo;index.htmladds favicons, theme-color, and Open Graph/Twitter meta (docs assets). Global CSS classes (.hot-menu-row,.hot-demo-card, etc.) fix buttonface/hover specificity issues.SplitPanere-clamps the editor/preview split on resize and sidebar toggle (open item 19).authedExtrasis cleared—those controls moved into framed UI.API/worker: PATCH
descriptionnow treatsnullas clear (not??restore). Branded HTML 404/410 for document requests viaerror-page.ts;/api/*stays JSON. UnusedShareDialog.tsxremoved; docs/ADR point atShareLinks.logout(returnTo?)avoids re-login loops on auth-gated routes.Reviewed by Cursor Bugbot for commit 0ac52ae. Bugbot is set up for automated code reviews on this repo. Configure here.