Skip to content

refactor(header): collapse the panel header by container width, not viewport - #5241

Merged
tlgimenes merged 2 commits into
feat/header-branch-uifrom
claude/header-container-queries
Jul 25, 2026
Merged

refactor(header): collapse the panel header by container width, not viewport#5241
tlgimenes merged 2 commits into
feat/header-branch-uifrom
claude/header-container-queries

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Stacked on #5230 — review that one first; this PR is the two commits on top.

1-wide-1440 2-mid-1000 3-chat-open-1180

The problem, for anyone not on a big monitor

The panel header collapsed its controls on viewport media queries (lg, 1024px). But the header doesn't span the viewport — it lives inside one panel. So screen width tells you almost nothing about the room it actually has:

Situation Screen Main panel header
Chat closed 1074px 826px
Chat open 1180px 605px

That second row is the bug. At a 1180px screen the viewport rule says "plenty of room, show every label" — while the strip is actually 605px and out of space. The labels stayed full-width, the tab budget got squeezed, and view tabs fell into the ⋯ overflow menu earlier than they needed to.

It also meant the same screen behaved differently depending on where you dragged the splitter, which is impossible to reason about — and it's why this looked fine on a wide monitor and bad on a laptop.

Reported as: "the screen is 882px wide and I'm still seeing the chat and preview labels."

What changed

PanelHeader now declares @container/panel-header and every control inside queries its own panel's width:

Control Collapses below Was
Chat, system tabs (Preview/Code), Library, Tasks, CMS 768px of header viewport 768px / none
Dynamic tabs (files, pinned views, expanded tools) 576px of header viewport 768px
Branch pill, branch picker, Develop/Live, Publish 768px of header viewport 1024px
Preview's URL group (CMS · refresh · page select · open-in-new-tab) 448px of header JS measurement, ~668px

Two tiers rather than one, because the icon has to carry the button once the label goes. Chat/Preview/Code/Library/Tasks have fixed, distinctive icons and collapse first. Dynamic tabs can resolve to a generic fallback glyph (resolve-tab-icon.ts), so several open files would become mutually indistinguishable — they hold their text longest.

Small screens keep the Preview controls much longer: the old JS rule hid the whole URL group below ~668px of header; it now survives to 448px.

Tabs finally have tooltips

HeaderTabButton had no Tooltip at all, while its own docstring claimed "every button is wrapped in a Tooltip." Below md the tabs were already collapsing to bare icons — so on a small screen you got an unlabelled icon with nothing identifying it but aria-label. That's the part that actually hurt. Tooltips are now real, which is what makes collapsing labels safe in the first place.

preview.tsx wrapped one of these in its own Tooltip; that's replaced by a tooltip prop so the two don't nest.

Screenshots

  1. 1440px, chat closed — header 1192px, everything labelled.
  2. 1000px, chat closed — header ~752px, labels collapsed to icon + tooltip, page selector still there.
  3. 1180px, chat open — header 605px. The point of the PR: a large screen where the panel is narrow, so the strip collapses. Under the old rule this showed full labels and ran out of room.

Trade-off worth a reviewer's opinion

The URL group's 448px cutoff is deliberately later than the old ~668px. Between roughly 448 and 530px the page selector has less than HEADER_W.middle (232px) and degrades toward a bare chevron instead of showing the page name — measured 220px at a 530px header, 150px at 460px.

That squish is the accepted cost of keeping the controls reachable on narrow panels; the old code hid them instead. Documented at the call site. Easy to move to 576px (@xl) if it reads badly in practice.

Dead code removed

headerLayout no longer returns showPageSelector — CSS owns that now, so keeping it would be dead. maxTabs stays in JS, because it changes which items render in the bar vs the overflow popover and CSS can't express that. Its tests were inverted rather than appended to, plus one asserting the field doesn't come back. HEADER_W.middle's docstring no longer claims to gate the centre's visibility.

Testing

  • bun run check clean across all workspaces; bun run lint 0 errors; bun run fmt applied.
  • 1029 unit tests pass (apps/web layouts, sandbox, chat).
  • Verified in the running app by driving the container's inline size directly and sweeping the thresholds:
Forced header Chat label Centre group
820, 780 shown visible (397, 379px)
767 collapsed visible (361px)
700 collapsed visible (361px)
530 collapsed visible (220px — into the squish band)
460 collapsed visible (150px)
447 collapsed hidden
  • Confirmed all four right-cluster components (BranchPill, BranchPicker, DevAgentControl, HeaderActions) render only inside a PanelHeader, so the container always resolves. Outside one the query finds no container and the label simply stays — the safe direction.
  • Mobile (MobileTaskWorkspace) doesn't render the branch selector or publish actions at all; Preview's inline fallback bar declares the same container so its queries still work on that path.

Also in here: the CMS toggle gets its own icon

Edit05 was doing double duty as the new-chat affordance (task-groups-list.tsx, shell-breadcrumb.tsx) and the header's CMS toggle — two unrelated actions, one pencil-square. That collision matters more once the label collapses and the icon is the only thing identifying the button. Now PuzzlePiece01 (the page-builder "blocks" metaphor, unused elsewhere), applied to both entry points: the toolbar button and the ⋯ menu fallback.

🤖 Generated with Claude Code


Summary by cubic

Collapse the panel header by its own container width instead of the viewport to fix cramped layouts and inconsistent behavior when chat narrows the header. Adds tooltip-backed icon-only states and simplifies headerLayout to only manage tab count.

  • Refactors

    • Switched all header controls to @container/panel-header; labels collapse by panel width with two tiers (Chat/system tabs/CMS and right actions at 768px; dynamic tabs at 576px). Preview’s URL group hides at 448px; the mobile inline toolbar declares the same container.
    • Simplified headerLayout to return { maxTabs } only; center visibility now uses a container query. Updated tests and docs.
    • Gave the CMS toggle a distinct icon (PuzzlePiece01) to avoid the pencil icon collision.
  • Bug Fixes

    • Header now collapses based on actual panel width, preventing early tab overflow and inconsistent behavior across splitter positions.
    • Tabs finally have tooltips, so icon-only buttons are identifiable; preview uses the new tooltip prop to avoid nested tooltips.

Written for commit 41158c5. Summary will update on new commits.

Review in cubic

tlgimenes and others added 2 commits July 25, 2026 02:54
…ewport

The header strip lives inside one panel, so viewport width says nothing about
the room it actually has: with chat open, a 1180px screen leaves the main
header at 605px. Every control in it collapsed on viewport media queries, so
on a laptop the labels stayed at full width while the strip ran out of space,
and the same screen behaved differently depending on the splitter.

PanelHeader now declares `@container/panel-header` and every control queries
that instead:

- Tab / Chat labels collapse to icon + tooltip below 768px of header. Buttons
  with a fixed, distinctive icon (Chat, system tabs, Library, Tasks, CMS) go
  at 768px; dynamic tabs, whose icon can resolve to the generic fallback and
  would be mutually indistinguishable, hold their text to 576px.
- The branch pill, branch picker, Develop/Live toggle and the publish actions
  move off `max-lg:`/`lg:hidden` onto the same 768px cut, so the whole strip
  collapses at one width instead of in two unrelated stages.
- Preview's URL group (CMS / refresh / page select / open-in-new-tab) hides
  below 448px of header. Deliberately later than the old JS rule (~668px):
  between ~448 and ~530px the page selector shows with less than
  HEADER_W.middle and degrades toward a bare chevron, judged better than
  losing the controls outright.

HeaderTabButton also gains the Tooltip its docstring already claimed it had —
without it an icon-only tab had nothing identifying it at all. preview.tsx's
external Tooltip is replaced by the new `tooltip` prop so the two don't nest.

headerLayout keeps only maxTabs; showPageSelector is gone, since CSS owns that
now and leaving it would be dead code. Its tests are inverted rather than
appended to, and HEADER_W.middle's docstring no longer claims to gate the
centre's visibility.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Edit05 is the new-chat affordance in the sidebar (task-groups-list) and the
shell breadcrumb, so the header's CMS toggle rendered the identical
pencil-square — two unrelated actions with one glyph, and the collision got
worse once the label collapses and the icon is all that identifies the button.

PuzzlePiece01 is the page-builder "blocks" metaphor, which is what the toggle
opens, and it is unused elsewhere. Avoided LayoutAlt01 (already the page icon
in this same toolbar's page-selector), Grid01 (means card/grid view in
collections) and Rows01 (a rows-view toggle in the sidebar).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tlgimenes
tlgimenes merged commit 6733acd into feat/header-branch-ui Jul 25, 2026
3 checks passed
@tlgimenes
tlgimenes deleted the claude/header-container-queries branch July 25, 2026 06:29
tlgimenes added a commit that referenced this pull request Jul 25, 2026
…iewport (#5241)

* refactor(header): degrade the panel header by container width, not viewport

The header strip lives inside one panel, so viewport width says nothing about
the room it actually has: with chat open, a 1180px screen leaves the main
header at 605px. Every control in it collapsed on viewport media queries, so
on a laptop the labels stayed at full width while the strip ran out of space,
and the same screen behaved differently depending on the splitter.

PanelHeader now declares `@container/panel-header` and every control queries
that instead:

- Tab / Chat labels collapse to icon + tooltip below 768px of header. Buttons
  with a fixed, distinctive icon (Chat, system tabs, Library, Tasks, CMS) go
  at 768px; dynamic tabs, whose icon can resolve to the generic fallback and
  would be mutually indistinguishable, hold their text to 576px.
- The branch pill, branch picker, Develop/Live toggle and the publish actions
  move off `max-lg:`/`lg:hidden` onto the same 768px cut, so the whole strip
  collapses at one width instead of in two unrelated stages.
- Preview's URL group (CMS / refresh / page select / open-in-new-tab) hides
  below 448px of header. Deliberately later than the old JS rule (~668px):
  between ~448 and ~530px the page selector shows with less than
  HEADER_W.middle and degrades toward a bare chevron, judged better than
  losing the controls outright.

HeaderTabButton also gains the Tooltip its docstring already claimed it had —
without it an icon-only tab had nothing identifying it at all. preview.tsx's
external Tooltip is replaced by the new `tooltip` prop so the two don't nest.

headerLayout keeps only maxTabs; showPageSelector is gone, since CSS owns that
now and leaving it would be dead code. Its tests are inverted rather than
appended to, and HEADER_W.middle's docstring no longer claims to gate the
centre's visibility.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(preview): give the CMS toggle its own icon

Edit05 is the new-chat affordance in the sidebar (task-groups-list) and the
shell breadcrumb, so the header's CMS toggle rendered the identical
pencil-square — two unrelated actions with one glyph, and the collision got
worse once the label collapses and the icon is all that identifies the button.

PuzzlePiece01 is the page-builder "blocks" metaphor, which is what the toggle
opens, and it is unused elsewhere. Avoided LayoutAlt01 (already the page icon
in this same toolbar's page-selector), Grid01 (means card/grid view in
collections) and Rows01 (a rows-view toggle in the sidebar).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
guitavano pushed a commit that referenced this pull request Jul 25, 2026
…iewport (#5241)

* refactor(header): degrade the panel header by container width, not viewport

The header strip lives inside one panel, so viewport width says nothing about
the room it actually has: with chat open, a 1180px screen leaves the main
header at 605px. Every control in it collapsed on viewport media queries, so
on a laptop the labels stayed at full width while the strip ran out of space,
and the same screen behaved differently depending on the splitter.

PanelHeader now declares `@container/panel-header` and every control queries
that instead:

- Tab / Chat labels collapse to icon + tooltip below 768px of header. Buttons
  with a fixed, distinctive icon (Chat, system tabs, Library, Tasks, CMS) go
  at 768px; dynamic tabs, whose icon can resolve to the generic fallback and
  would be mutually indistinguishable, hold their text to 576px.
- The branch pill, branch picker, Develop/Live toggle and the publish actions
  move off `max-lg:`/`lg:hidden` onto the same 768px cut, so the whole strip
  collapses at one width instead of in two unrelated stages.
- Preview's URL group (CMS / refresh / page select / open-in-new-tab) hides
  below 448px of header. Deliberately later than the old JS rule (~668px):
  between ~448 and ~530px the page selector shows with less than
  HEADER_W.middle and degrades toward a bare chevron, judged better than
  losing the controls outright.

HeaderTabButton also gains the Tooltip its docstring already claimed it had —
without it an icon-only tab had nothing identifying it at all. preview.tsx's
external Tooltip is replaced by the new `tooltip` prop so the two don't nest.

headerLayout keeps only maxTabs; showPageSelector is gone, since CSS owns that
now and leaving it would be dead code. Its tests are inverted rather than
appended to, and HEADER_W.middle's docstring no longer claims to gate the
centre's visibility.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(preview): give the CMS toggle its own icon

Edit05 is the new-chat affordance in the sidebar (task-groups-list) and the
shell breadcrumb, so the header's CMS toggle rendered the identical
pencil-square — two unrelated actions with one glyph, and the collision got
worse once the label collapses and the icon is all that identifies the button.

PuzzlePiece01 is the page-builder "blocks" metaphor, which is what the toggle
opens, and it is unused elsewhere. Avoided LayoutAlt01 (already the page icon
in this same toolbar's page-selector), Grid01 (means card/grid view in
collections) and Rows01 (a rows-view toggle in the sidebar).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
guitavano added a commit that referenced this pull request Jul 25, 2026
…s below lg (#5230)

* feat(header): branch label, truncation priority, and icon-only actions below lg

- Branch button always shows the current branch (truncated), and yields its
  width BEFORE the centered address bar (its own `min-w-0` shrink slot; the
  fixed publish/⋯ actions never clip and remain what `rightRef` measures).
- Below `lg` (< 1024px) the header collapses to icon-only + tooltip: the branch
  button, the primary action button (icon keyed off `button.action`), the side
  Publish button (Upload01), and the Develop/Live toggle (Code01 / Globe01).
  Loading pills → spinner-only; plain status pills keep their text. Desktop
  unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(header): collapse the panel header by container width, not viewport (#5241)

* refactor(header): degrade the panel header by container width, not viewport

The header strip lives inside one panel, so viewport width says nothing about
the room it actually has: with chat open, a 1180px screen leaves the main
header at 605px. Every control in it collapsed on viewport media queries, so
on a laptop the labels stayed at full width while the strip ran out of space,
and the same screen behaved differently depending on the splitter.

PanelHeader now declares `@container/panel-header` and every control queries
that instead:

- Tab / Chat labels collapse to icon + tooltip below 768px of header. Buttons
  with a fixed, distinctive icon (Chat, system tabs, Library, Tasks, CMS) go
  at 768px; dynamic tabs, whose icon can resolve to the generic fallback and
  would be mutually indistinguishable, hold their text to 576px.
- The branch pill, branch picker, Develop/Live toggle and the publish actions
  move off `max-lg:`/`lg:hidden` onto the same 768px cut, so the whole strip
  collapses at one width instead of in two unrelated stages.
- Preview's URL group (CMS / refresh / page select / open-in-new-tab) hides
  below 448px of header. Deliberately later than the old JS rule (~668px):
  between ~448 and ~530px the page selector shows with less than
  HEADER_W.middle and degrades toward a bare chevron, judged better than
  losing the controls outright.

HeaderTabButton also gains the Tooltip its docstring already claimed it had —
without it an icon-only tab had nothing identifying it at all. preview.tsx's
external Tooltip is replaced by the new `tooltip` prop so the two don't nest.

headerLayout keeps only maxTabs; showPageSelector is gone, since CSS owns that
now and leaving it would be dead code. Its tests are inverted rather than
appended to, and HEADER_W.middle's docstring no longer claims to gate the
centre's visibility.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(preview): give the CMS toggle its own icon

Edit05 is the new-chat affordance in the sidebar (task-groups-list) and the
shell breadcrumb, so the header's CMS toggle rendered the identical
pencil-square — two unrelated actions with one glyph, and the collision got
worse once the label collapses and the icon is all that identifies the button.

PuzzlePiece01 is the page-builder "blocks" metaphor, which is what the toggle
opens, and it is unused elsewhere. Avoided LayoutAlt01 (already the page icon
in this same toolbar's page-selector), Grid01 (means card/grid view in
collections) and Rows01 (a rows-view toggle in the sidebar).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(header): drop the Preview URL group's cutoff to 384px

The group's min-content is 112px — 36 (CMS) + 28 (refresh) + 24 (page chevron)
+ 28 (open-in-new-tab). Measured against the live header, a 384px container
still leaves it 112-128px once the left group (66px; the tab budget is one tab
at that width) and the actions (178px) take their share, so the icons render
without clipping. One breakpoint lower and they clip.

The page name is already gone by ~448px — the selector collapses to a bare
chevron there — so this buys the 384-448px band four usable icons rather than
nothing. Losing the controls outright is worse than losing the label.

Because container queries measure the content box, `px-1.5` makes this fire
around 396px of rendered width, leaving a ~16px margin above the floor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(preview): centre the page selector in the mobile toolbar

On the inline (mobile) path every control lived in one left-packed group, so
the page selector sat wherever Edit and refresh happened to push it — off
centre on a phone, where that bar is the full width of the screen.

Split the group into `cmsToggle` and `urlGroup` so each layout can place them
independently. Desktop composition is unchanged: Edit leads, divider, then the
view controls, portaled into the header's centre slot. Mobile now runs three
zones — Edit left, the view controls centred, ⋯ right.

Both side zones are `flex-1` so the selector centres on the bar rather than
between two unequal neighbours (Edit is ~9px wider than ⋯, which was the
original offset). On the narrowest phones the left zone cannot shrink below
the Edit button, so ~5px returns; closing that fully would mean pinning the
sides to a fixed width and taking the space from the selector instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(branch-picker): mark the current branch and open focused on it

The picker relied on cmdk's default first-item highlight, which made an
unrelated branch look "selected" while the branch actually in use had no
indication. Seed the active item with the current branch so it opens
focused on it, and render a check on the current branch in each group.

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

---------

Co-authored-by: Deco Studio <studio@deco.cx>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: guitavano <tavano62@gmail.com>
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