Skip to content

feat(runner): DEV-2156 app chrome — top bar, column bars, authed action bar - #92

Merged
demtario merged 8 commits into
feat/DEV-2027-redesignfrom
feat/DEV-2156-app-chrome
Jul 30, 2026
Merged

feat(runner): DEV-2156 app chrome — top bar, column bars, authed action bar#92
demtario merged 8 commits into
feat/DEV-2027-redesignfrom
feat/DEV-2156-app-chrome

Conversation

@demtario

@demtario demtario commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

T2 of the DEV-2027 redesign — DEV-2156. Depends on T0 (DEV-2154, theming) and T1 (DEV-2155, icons), both already on this branch. Targets feat/DEV-2027-redesign, not master.

Rebuilds the shell chrome against Figma 72:15697 (logged out) and 48:6560 (light, authed). Toolbar.tsx is deleted; its job is split across TopBar (row 1), EditorBar and PreviewBar (the two 36px column bars), AuthedActionBar, and a shared MenuButton backing the version and framework dropdowns.

Three things the frames settle that the task's scope line got wrong

Row 2 is two bars, not one. 72:15811 lives inside the editor column frame and 72:15706 inside the preview column frame — there is no full-width secondary row. The "divider" in the scope line is the sidebar/editor boundary, which s.sidebar's existing borderRight already draws, so there is no divider component either.

The preview column is a fixed half. It starts at x=864 of 1728 in both 72:15697 (sidebar collapsed) and 48:6560 (sidebar open at 240) — the editor absorbs the sidebar, the preview does not move. s.body is 240px minmax(0,1fr) 50%; 1fr 1fr would have put the boundary at 984 with the sidebar open. T6 replaces this with the draggable ratio.

Download is not gated on auth. The scope line says Download (authed) / Sign in (anon), but share mode has always offered Download to anonymous visitors and no frame anywhere shows an anonymous share view. ADR-0023 rule 1 wins: Download renders whenever a file set exists, Sign in when anonymous, and an anonymous share page shows both. This is the one deliberate deviation from the task text.

Refresh needed a runtime change

packages/runtime/src/{types,container,sandpack}.ts are outside the task's file list. The row-2 refresh button needs somewhere to point, and the only cheap correct answer lives in the runtime: reload() on DemoRuntime. The container re-points the iframe at its existing previewUrl (container.ts:242 already does that assignment); Sandpack re-runs updateSandbox(setup, true). Neither creates a session — a mountGen remount would mint a fresh container per click against a five-slot pool, which is the exhaustion bug we already fixed once in #87.

DocsCascader is touched further than "restyle the trigger"

Two things the pill forces. Its trigger lost its own border/background, because the pill is the one box the design draws (72:15859) and keeping both nested a second box inside the first. And s.pop gained width: max-content while s.topBar gained a zIndex: the pill is centred with a transform, which makes it a stacking context, so without that the preview pane — position: relative, later in the DOM — painted straight over the open popover. T7 owns the popover's own restyle and should know both.

Also here

PreviewPane's full-width accent status strip is dropped and its boot/error overlays re-anchor to inset: 0 — the design gives the preview one bar, and ● ready belongs in T5's bottom 28px bar. The tab strip renders inline in EditorBar with one open file, for T4 to extract into EditorTabs.tsx. App.tsx captures mount()'s previewUrl for the row-2 address field, container engine only: Tier 1 reports Sandpack's bundler origin, which is meaningless to a user and a CodeSandbox mark ADR-0001 keeps out of the UI. A closed tab reopens when the workspace is replaced, driven by App's existing mountGen — two examples can share an entry path, so a still-valid active is not proof the document is the same one.

Sidebar CRUD gating stays T3's job; the handlers pass through unchanged.

Verification

pnpm typecheck clean across all four projects (the acceptance criterion). No lint script exists in this workspace.

Walked in a browser at 1728×900, in both themes: anonymous play (starter), anonymous docs example, anonymous share, authed play, authed edit. All nine signed-in affordances exercised end to end — fork → /edit/:id, Save (PATCH 200), the Share dialog, My demos, Log out, title/description, the custom-version input, Embed, plus the framework dropdown actually switching examples.

Tier-2 refresh checked against the network log: one click produced a second GET to the preview URL and zero additional POST /api/session. Tier-1 refresh re-renders with no console errors.

Open items logged, not resolved here

Both appended to runner/docs/dev-2027-redesign-plan.md per the redesign's convention:

  1. The pill's Handsontable mark has no asset. Both pill forms draw a 20×20 rounded-square mark (48:6582, 72:15861); the repo has only the 145×22 wordmark, which at 20px tall is ~130px wide and swamps the pill. Shipped without it. The same gap blocks the favicon, which T9 lists and which currently 404s in dev — one square asset closes both.
  2. Tier 1 has no preview URL. The row-2 field shows the public demo URL when there is one, else the container preview URL, else a muted Live preview placeholder. So anonymous play on a Tier-1 starter — the exact case 72:15697 draws with a /share/… URL — shows the placeholder. Worth a line from design on whether to drop the field instead.

One known temporary gap: share mode used to print {displayName} · HOT {version} in the top bar. frameworkLabel still flows through the shell but nothing renders it until T5 builds the preview's bottom status bar.

🤖 Generated with Claude Code


Note

Medium Risk
Touches core authoring layout, preview lifecycle (reload vs remount), and widespread E2E selectors; behaviour is mostly restyle plus intentional auth/Download rules, but container session handling is sensitive.

Overview
T2 (DEV-2156) replaces the monolithic toolbar with Figma-aligned chrome: a 72px top bar (logo, centred example pill, theme toggle, Download / Sign in), separate 36px bars on the editor column (sidebar toggle + tabs) and preview column (refresh, URL, version/framework menus, docs/GitHub links, maximize), plus a signed-in-only action bar above the preview for Save/Share, Fork/Embed, and custom version.

App.tsx drops its bespoke header and feeds EditorShell via new slots (examplePill, authedExtras, publicUrl, previewUrl, etc.). The example cascader lives inside the pill with a search affordance; framework picking moves from button groups to MenuButton listboxes. Download stays available when anonymous (share/play), matching prior behaviour vs the task wording.

Preview refresh adds optional DemoRuntime.reload() — container re-navigates the iframe; Sandpack re-runs updateSandbox(..., true) — avoiding remounts that would exhaust container session slots. Row-2 URL shows /share/:id when saved, container previewUrl for Tier 2, or a "Live preview" placeholder for Tier 1 Sandpack.

PreviewPane removes the top "Live" status strip; E2E now asserts data-preview-status="ready". Playwright docs tests use menu helpers for version/framework.

Reviewed by Cursor Bugbot for commit 06d34b2. Bugbot is set up for automated code reviews on this repo. Configure here.

…on bar

Rebuild the shell chrome against Figma 72:15697 / 48:6560. Toolbar.tsx is gone;
its job is split across TopBar (row 1), EditorBar and PreviewBar (the two 36px
column bars), AuthedActionBar, and a shared MenuButton backing the version and
framework dropdowns.

Three things the frames settle that the task's scope line got wrong:

* Row 2 is two bars, not one. 72:15811 lives inside the editor column frame and
  72:15706 inside the preview column frame — there is no full-width secondary
  row, and no divider component either (the sidebar's own borderRight is the
  only rule at that height).
* The preview column is a fixed half. It starts at x=864 of 1728 in both
  72:15697 (sidebar collapsed) and 48:6560 (sidebar open at 240): the editor
  absorbs the sidebar. s.body is `240px minmax(0,1fr) 50%`.
* Download is not gated on auth. Share mode has always offered it to anonymous
  visitors and no frame shows an anonymous share view, so ADR-0023 rule 1 wins:
  Download whenever a file set exists, Sign in when anonymous, both together on
  an anonymous share page.

The refresh button needed a reload() on DemoRuntime. Container re-points the
iframe at its existing previewUrl; Sandpack re-runs updateSandbox(setup, true).
Neither creates a session — a remount would mint a fresh container per click
against a five-slot pool.

DocsCascader is touched further than "restyle the trigger" for two reasons the
pill forces. Its trigger lost its border/background, because the pill is the one
box the design draws and keeping both nested two. And the popover needed
width: max-content plus a zIndex on the top bar: the pill is centred with a
transform, which makes it a stacking context, so the preview pane — position:
relative, later in the DOM — painted over the open popover.

Also here: PreviewPane's full-width accent status strip is dropped (the design
gives the preview one bar, and `● ready` belongs in T5's bottom bar) and its
overlays re-anchor to inset 0; the tab strip renders inline in EditorBar with
one open file, for T4 to extract; App.tsx captures mount()'s previewUrl for the
row-2 address field, container engine only — Tier 1 reports Sandpack's bundler
origin, which is meaningless here and a CodeSandbox mark ADR-0001 keeps out of
the UI.

A closed tab reopens when the workspace is replaced, driven by App's existing
mountGen: two examples can share an entry path, so a still-valid active file is
not proof the document is the same one.

Two open items logged: the pill's 20x20 Handsontable mark has no asset in the
repo (same gap blocks the favicon), and Tier 1 has no preview URL to show.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread runner/apps/authoring/src/App.tsx
Comment thread runner/packages/runtime/src/sandpack.ts
demtario and others added 2 commits July 30, 2026 15:03
Both findings hold.

The GitHub control rendered in edit and share too, because repoUrl was always a
non-empty string. Neither mode has a docsPath, so it pointed at the framework
starter and called it "Fork this starter" — but a saved demo's source is the
demo, not the template it came from, and 48:6560 (the one edit-mode frame) ends
its bar at window-maximize. Play only, as the old top bar had it. docsUrl and
frameworks were already play-only in practice: docsPath is seeded from the URL
only when route.mode is "play".

Sandpack reload() claimed the update sequence *after* awaiting its transpile, so
a keystroke landing inside that window could have its newer result dropped while
the refresh published the older one — preview stuck behind the editor until the
next edit. It now shares pushUpdate(), which claims the sequence up front;
`initial` carries the isInitializationCompile flag that made refresh a separate
path in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T4 (#93) landed the editor tab strip and status bar against the pre-T2 shell, so
both branches rewrote EditorShell around the same region. Resolved in T4's
favour wherever the two overlap — it owns the tabs by the plan's own boundary.

* The inline tab strip T2 rendered in EditorBar is gone; EditorBar is now the
  sidebar toggle plus `<EditorTabs>`. The bar takes the strip's recessed
  background and inset hairline (`s.editorBar`) rather than `s.bar`'s surface +
  bottom border, so toggle and tabs read as the one row the frame draws with no
  seam between them. `s.bar` stays as it was for PreviewBar.
* T2's close-the-tab behaviour is dropped, with the `closed` state and the
  `workspaceGen` prop that existed to reset it. T4 documents the ✕ as decorative
  until multi-tab lands, which is the more coherent call for a single-file shell.
* `s.editorPane` is replaced by T4's `s.editorBody` — the editor column is
  `s.column` now, and that is just the CodeMirror slot between the tabs and the
  status bar. It takes over the editor background `editorPane` carried.
* EditorShell keeps T2's three-column body and T4's cursor state, so the status
  bar sits at the foot of the editor column only.

Open items renumbered: T2's two become 13 and 14, after T4's 6-12.

Verified after the merge in both themes: top bar, both column bars, the tab
strip, the authed action bar and the status bar all render together; typecheck
clean across all four projects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@demtario

Copy link
Copy Markdown
Contributor Author

Merged feat/DEV-2027-redesign in after #93 (T4) landed — 36126145. The two branches rewrote the same region of EditorShell, so a note on how the overlaps were resolved, since most of them went T4's way.

Tabs. T2 rendered a tab strip inline in EditorBar, on the plan's own note that T4 would extract and refine it. T4 got there first, so the inline strip is gone and EditorBar is now the sidebar toggle plus <EditorTabs>. The bar takes the strip's recessed background and inset hairline (a new s.editorBar) instead of s.bar's surface + bottom border, so the toggle and the tabs read as the one row 72:15811 draws rather than showing a seam where the two backgrounds meet. s.bar is unchanged for PreviewBar.

Tab close. T2 made the ✕ functional — it cleared the active file — with a closed flag and a workspaceGen prop to reset it on an example switch. All of that is dropped in favour of T4's reading that the ✕ is decorative until multi-tab lands. For a shell with exactly one open file that is the more coherent call, and it removes the state it needed.

Editor slot. s.editorPane is replaced by T4's s.editorBody. The editor column is s.column now, so what is left is the CodeMirror slot between the tabs and the status bar; it inherits the editor background editorPane carried, and keeps T4's load-bearing overflow: hidden.

EditorShell keeps T2's three-column body and T4's cursor state, so the status bar sits at the foot of the editor column only, as 48:6740 shows. T2's two open items renumbered to 13 and 14 behind T4's 6–12.

Re-verified after the merge at 1728×900 in both themes: top bar, both column bars, the tab strip, the authed action bar and the status bar all render together, and pnpm typecheck is clean across all four projects.

Comment thread runner/apps/authoring/src/App.tsx Outdated
Bugbot, correctly. In edit mode `publicUrl` was `/edit/:id`, and the field is
click-to-copy — but `/edit` is auth-gated (`Gate` sends a signed-out visitor to
the login broker, which only accepts @handsontable.com), so anyone the link was
copied to hit a wall. `/share/:id` is the same demo served without auth, and is
what `ShareLinks` already hands out.

Now `/share/:id` in both edit and share. The field reads a shareable address
rather than mirroring the browser's, which is what "the demo's public URL" meant
in the first place — noted in the plan's open item 14.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread runner/apps/authoring/src/App.tsx
demtario and others added 2 commits July 30, 2026 15:28
…ascader

Bugbot again, and it caught the consequence of my own comment. Dropping the ▾
chevron (`72:15863` has it hidden) left the search icon as the trigger's only
affordance — but the icon was a sibling of `DocsCascader` inside the pill, so it
looked like the opener without being one: only the label area toggled the menu.

The icon moves inside the trigger button. `s.trigger` becomes `width: 100%`
rather than `flex: 1`, because the wrapper it sits in is a positioned *block*
(it is the popover's containing block), so a flex value there did nothing and
the icon tucked itself against the label instead of sitting at the pill's right
edge.

Verified: clicking the icon opens the cascader, and it renders at x≈1086 of the
1104-wide pill, matching `72:15865`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught what I should have: six docs-example specs drive the version and
framework pickers, and T2 turned both from native controls into custom listboxes
in the preview bar. `selectOption` fails outright on a <button>, and the
framework picker no longer has `aria-pressed` because it is no longer a button
group.

A `pickFromMenu` helper opens the trigger by its `aria-label` and clicks the
`role="option"`; the framework assertions move from "all five are buttons, React
is pressed" to "React labels the trigger, all five are options, React is
selected". The docs link is matched by its label rather than its old visible
text, since it is an icon button now.

No product change — the specs were asserting the old chrome.

Full suite locally: 10 passed, 85 skipped, 0 failed (the same subset CI runs;
the rest are gated behind E2E_LIVE / the starter matrix).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread runner/packages/editor-shell/src/PreviewBar.tsx
Bugbot, correctly again. `versionWarning` moved from the old top bar — which
grew with its content, so wrapping was free — into row 2, which is a fixed 36px.
Both strings run ~90 characters, well past the 260px cap, so the span wrapped to
three lines and pushed itself out of the bar and over whatever sat below.

One line with an ellipsis now, full text still in `title`. Measured against the
longer of the two strings: 14px tall inside the 36px bar.

Logged as open item 15 rather than called done. A truncated warning is one you
can miss and `title` is not reachable by touch, but no frame in `18.1` shows a
warning anywhere, so there is no designed slot to move it to. T5's preview status
bar is the obvious candidate — the frames do draw it, and it has the width.

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 ff636ae. Configure here.

Comment thread runner/packages/editor-shell/src/PreviewPane.tsx
… chrome

Bugbot caught what neither CI nor I would have: the starter matrix waits on the
preview's status strip reading "Live", and T2 deleted that strip. The suite is
opt-in (E2E_STARTER_MATRIX=1), so PR CI never ran it — the next matrix run would
have timed out on every entry with a preview that was actually fine.

`PreviewPane` now sets `data-preview-status` on its section and the matrix polls
that instead. The point is that readiness stops being a property of the chrome:
T5 moves the visible readout to a bottom bar, and this survives it.

Worth noting the selector already had one scar — scoping to the strip was itself
a fix for `getByText("Live")` matching a container's boot-log <pre> when the
error text began with "Error:" (mui, strict-mode violation). An attribute has
neither failure mode.

Verified by actually running a matrix case rather than reasoning about it:
`matrix: javascript @ 18 [sandpack]` passes. Default suite still 10 passed,
85 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@demtario
demtario merged commit 1c8ea3e into feat/DEV-2027-redesign Jul 30, 2026
2 checks passed
@demtario
demtario deleted the feat/DEV-2156-app-chrome branch July 30, 2026 13:55
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