Skip to content

feat(runner): DEV-2163 T9 undesigned surfaces + the After Login frames - #99

Merged
demtario merged 9 commits into
feat/DEV-2027-redesignfrom
feat/DEV-2163-undesigned-surfaces
Jul 31, 2026
Merged

feat(runner): DEV-2163 T9 undesigned surfaces + the After Login frames#99
demtario merged 9 commits into
feat/DEV-2027-redesignfrom
feat/DEV-2163-undesigned-surfaces

Conversation

@demtario

@demtario demtario commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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

Surface Frame Landed as
My Demos page 114:25521 MyDemos.tsx, rewritten from a 340px drawer into a route
Account menu 114:21480 AccountMenu.tsx, in TopBar
Share links dialog 114:23289 ShareLinks.tsx, rebuilt
Edit info dialog 114:24410 EditInfoDialog.tsx (new)
BOX INFO pencil 114:21684 BoxInfo's onEdit, opening the above

The card grid, the kebab (Open / Copy link / Fork / Rename / Delete) and the + Create tile come from 114:25521 / 114:27012. Rename routes to /edit/:id?edit=info rather 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 Dialog primitive (there was none — both hand-rolled overlays lacked role="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 and index.ts's catch-all were bare text/plain, so a revoked share link rendered browser-default with zero branding. New workers/api/src/error-page.ts gives 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.png and handsontable-banner-og.png copied from handsontable/handsontable@develop's docs/public/, wired with the same prefers-color-scheme link 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-fill card wrapping. A breakpoint study stays deferred pending design.

Delete keeps a confirmation (there was none — remove() fired DELETE on 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 how 114:21480 draws it. No sibling ticket has been created yet.

Behaviour changes worth a look

  • authedExtras is 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.
  • The top bar keys off accountEmail, not authed. /share/:id renders the workspace anonymous on purpose, and it used to offer a signed-in visitor "Sign in". ShareRoute resolves the identity for the menu alone, never as a render gate.
  • description can now be cleared. Both PATCH branches coalesced with ??, so the null the 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 buildpnpm -r typecheck → 67/67 tests → wrangler deploy --dry-run → e2e 20 pass / 0 fail. Hex-literal grep clean outside theme.ts; error-page.ts is 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 about background: an inline color left the Delete row red-on-red, and an inline border shorthand killed the card hover via the longhand. Third, a revoked card's kebab was five dead ends — getDemoSource returns null once revoked is 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 from authedExtras and 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-demos route 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 into EditInfoDialog behind the BOX INFO pencil; Share links and delete confirm use a new shared Dialog (focus trap, Escape, role="dialog"). The top bar now shows an AccountMenu keyed off accountEmail, not workspace authed, so signed-in visitors on /share/:id get their menu instead of Sign in while the editor stays read-only (ShareRoute resolves identity without gating render).

Routing and chrome: useDocumentTitle sets tab titles per route/demo; index.html adds 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. SplitPane re-clamps the editor/preview split on resize and sidebar toggle (open item 19). authedExtras is cleared—those controls moved into framed UI.

API/worker: PATCH description now treats null as clear (not ?? restore). Branded HTML 404/410 for document requests via error-page.ts; /api/* stays JSON. Unused ShareDialog.tsx removed; docs/ADR point at ShareLinks. 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.

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>
Comment thread runner/apps/authoring/src/App.tsx Outdated
…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>
Comment thread runner/workers/api/src/error-page.ts
…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>
Comment thread runner/apps/authoring/src/MyDemos.tsx
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>
Comment thread runner/packages/editor-shell/src/Dialog.tsx
Comment thread runner/apps/authoring/src/ShareLinks.tsx
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>
Comment thread runner/apps/authoring/src/App.tsx
…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>
Comment thread runner/apps/authoring/src/MyDemos.tsx
…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>
Comment thread runner/apps/authoring/src/App.tsx
Comment thread runner/apps/authoring/src/MyDemos.tsx
… 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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread runner/packages/editor-shell/src/Dialog.tsx
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>
@demtario
demtario merged commit 254f675 into feat/DEV-2027-redesign Jul 31, 2026
2 checks passed
@demtario
demtario deleted the feat/DEV-2163-undesigned-surfaces branch July 31, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant