Skip to content

Simplify the app chrome: one job per corner, and glass that follows the theme - #152

Merged
thalida merged 19 commits into
mainfrom
feat/header-footer-simplify
Aug 9, 2026
Merged

Simplify the app chrome: one job per corner, and glass that follows the theme#152
thalida merged 19 commits into
mainfrom
feat/header-footer-simplify

Conversation

@thalida

@thalida thalida commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Started as "simplify the header, move the project switcher left, make the gem open the switcher, put version/about/credit in the footer" and grew from there as each change surfaced the next thing.

Header and footer

The gem and the project chip would both have opened the switcher, so they merged into one control: the gem is now the chip's leading glyph. ResetViewButton is deleted; R / Home still reset the camera, as does the gem in the city.

Per-node file stats moved out of the footer and into the selection pane, where they sit beside the node they describe. That freed both bars to split by meaning:

header  project switcher, copy, origin ......... about · shortcuts
footer  status · version · debug ....... made by 🦄 thalida.

about and the shortcuts button are meta any user might want, so they took the header's empty right side. Version answers the same question as the status dot, so it sits beside it, along with the debug button (developer-only, and conditional, which would otherwise resize the header cluster depending on how the app was loaded).

The version is real: ConfigResponse gained a version field off api.__version__, so a released image reports its own tag instead of a bundled constant.

Selection panes

FilePreviewPane and StreetPane gained a fixed stats row under their scrolling body, via Pane's existing footerSlot. The stat builders are shared with the hover tooltip, so hover and pane cannot report different numbers.

Hover tooltip

Was one flat line of path + a stat with no width cap, and its only clamp was a flip to the other side of the cursor, which cannot help a card too wide to fit either way. Now three lines (name, folder, stats), a path that drops middle segments rather than its tail, a max-width, and a clamp that actually clamps.

A real bug fell out: fileStatItems reported "0 lines" for images. It now reports pixel dimensions, which fixes the pane too.

Glass

TimeTravelBar and SceneModeToggle each hand-rolled a frosted fill and disagreed (78% vs 82% of the same colour). One --cc-glass-fill / --cc-glass-blur pair and a .surface-glass role now serve the scrubber, the mode toggle, the Shortcuts / Debug / Loading cards, the landing cards, and the tooltip. Chrome laid out in the app body stays opaque, since a blur there has nothing to sample.

Borders follow the theme

themes.css swaps the three surface tokens across four presets but overrode no border token, so picking warm or green gave you a warm panel ringed by a cool blue border, app-wide. Borders and tracks now derive from the surface with relative colour syntax: lightness as an offset, chroma as a multiplier, hue straight through. Both constants are measured off the default surface, so cool is unchanged to the eye. Behind @supports, because a custom property holding an unparseable value fails at substitution rather than parse time, which would drop every border to currentColor.

Landing page on small screens

Below 900px the hero stacks above the cards, and the styles written for the side-by-side composition misled there: a viewport-height cap and centering stranded the cards a screen down, grid's default align-content: stretch opened a screen-tall gap, hero text and card titles disagreed on their left edge, and the close button shared a band with the brand row.

Behaviour changes worth a look

  • Hover no longer previews stats in the footer. It followed hov ?? sel, and the sidebar opens only on selection. The rebuilt tooltip covers most of what it did.
  • Modal headers lost their separate chrome fill, so each card reads as one glass panel rather than an opaque band over glass.
  • Modal card glass is currently a no-op: .modal-backdrop has its own blur(4px), making it a backdrop root, so the card's blur samples the flat dim rather than the city. Landing cards and the tooltip are genuinely glass. Fixing it means dropping the backdrop's blur.

Verification

just test (370 pytest, 2892 vitest) and just lint green; the full pre-push gate passed. Two rounds of code review were applied, one of which caught a Critical: the small-screen overrides sat above the rules they override, so half that fix silently never applied.

jsdom computes neither grid layout nor backdrop-filter, so the layout and glass work is not unit-testable and was checked in a real browser.

🤖 Generated with Claude Code

thalida and others added 19 commits August 8, 2026 18:03
app/.prettierignore excludes this file (auto-generated by openapi-typescript,
prettier --write from the repo root skipped that ignore file since it isn't
at the repo root). Regenerate to restore raw openapi-typescript output.
Fixes the footer's narrow-viewport grid (right cluster was drifting
into a dead third track once the center credit line hid), corrects
stale comments left behind by the gem/reset-key merge and an earlier
chip max-width fix, scopes .btn-chip-gem's sizing so it actually wins
over .gem-icon's specificity-tied 1em default, and consolidates the
three hardcoded copies of the repo URL into one REPO_URL constant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
At the prose color they were indistinguishable from the text around
them. --cc-text-primary puts them two steps above that prose; hover
keeps --cc-text-strong plus the underline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.btn-chip .gem-icon is (0,2,0) against .gem-icon's (0,1,0), so it wins
on specificity outright, not on import order at a tie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The chip's flex gap spaces every child equally, so the gem read as a
glyph inside the name. Only this gap widens; the name, branch pill, and
affordance stay a tight cluster.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Below 900px the hero stacks above the cards, and the styles written for
the side-by-side composition mislead in that arrangement:

- The action column kept a viewport-height cap and align-self: center,
  so the cards sat a screen below the hero with the recents card
  stretching to absorb the slack.
- Grid's default align-content: stretch inflated the auto rows to fill
  min-height, opening a screen-tall gap between hero and cards. It now
  centers only while the content fits: `safe` keeps a full recents list
  from clipping the hero above the scroll origin.
- Hero text sat at the page gutter while card titles sat a further
  16px in, so two vertically adjacent blocks disagreed on their left
  edge. The hero now carries the card's own padding.
- The close button floated in a band the brand row also occupied. The
  top inset now clears it.
- The flat 90% veil rendered the city as dead black. The scrim runs
  down the page instead, thinning toward a horizon at the bottom.

The page is fixed to the viewport, so the insets clear safe areas.
Recents is capped at MAX_RECENT_SOURCES, which bounds the page length
without a scroller nested inside the card.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scrubber and the mode toggle each hand-rolled a frosted fill, and
they disagreed: 78% against 82% of the same colour. The timeline's is
the reference, so 78% and blur(12px) become tokens, and .surface-glass
joins the surface roles as the fill for a panel with the canvas behind
it.

Now shared by the scrubber, the mode toggle, the Shortcuts, Debug and
Loading cards, and the landing cards. The timeline's stale-scrub toast
keeps its semantic error fill and takes only the blur radius.

Chrome laid out in the app body (header, footer, activity bar, sidebars,
editor pane) stays opaque: a blur there has nothing to sample. The modal
header drops its separate chrome fill so each card reads as one panel
rather than an opaque band over glass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A media query carries no extra specificity, so the breakpoint block sat
above the base rules and lost to them: the action column kept its
viewport cap and centering, and the recents card kept stretching, which
left the very nested-scroll trap the breakpoint exists to remove. It
moves to the end of the file.

Also drops the safe-area terms: index.html sets no viewport-fit=cover,
so every inset resolved to 0 and the page does not span the status bar.
Adds an unprefixed align-content ahead of the `safe` variant for engines
that drop the whole declaration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per-node stats sat in the footer, across the screen from the pane
describing the same node, and their content-sized track was what the
centered credit line had to compete with. They move into the selection
pane as <PaneStats>, passed through Pane's existing footerSlot, so the
file and road panes each gain a fixed stats row under their scrolling
body.

That frees the footer to split by meaning rather than by space:

  header  project switcher, copy, origin ......... about · shortcuts
  footer  status · version · debug ....... made by 🦄 thalida.

About and the shortcuts button are meta any user might want, so they
join the header opposite the project controls. Version answers the same
question as the status dot, so it sits beside it, with the debug button
(developer-only, and conditional on debug mode, which would otherwise
resize the header cluster depending on how the app was loaded). The
credit keeps the quiet corner, without the // that made it a comment.

The footer returns to two tracks, so the 581-900px band no longer
ellipsizes anything early.

One behaviour change: the footer stats followed hover, and the sidebar
opens only on selection, so hovering a building no longer previews its
stats.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It was one flat line of path + a stat, joined by dots, with no width cap.
A deep path made an arbitrarily wide card, and the only clamp was a flip
to the other side of the cursor, which cannot help a card too wide to fit
on either side: it pushed it off the opposite edge instead.

Now: the name in strong text, the containing folder beneath it, and the
stats last. The path drops whole middle segments to fit a budget, since
truncating the tail would hide the end nearest the file. A max-width and
per-line ellipsis back that up, because the budget counts characters and
only approximates rendered width, and the placement clamps to the
viewport after flipping.

Stats come from the same builders the selection pane uses, so hover and
pane cannot report different numbers, and hovering recovers some of the
preview lost when the stats moved out of the app footer. Files pass
dates: false to stay to one line. Commits lead with their subject, which
is the most identifying thing about them and was previously unused.

fileStatItems learned media dimensions, which the old tooltip special
cased and the pane got wrong: a PNG reported 0 lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tooltip rode on --cc-bg-tooltip: translucent at 94% with no blur, so
the city showed through raw and muddied the smallest text in the app. It
floats on the canvas like the panels do, so it takes .surface-glass, and
the token it was the only consumer of goes away.

A directory stat row now leads with its kind. A folder named `app` and a
file named `app` look alike, and the counts that follow only mean
something once you know which one you are reading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tooltip's border was oklch(0.32 0.058 274.6): three times the
surface's chroma and hue-shifted bluer. Absolute colours were fine when
the panels were opaque, but a glass fill picks up whatever is behind it
while the border holds one fixed hue, so the edge read cool against a
panel that no longer was.

--cc-glass-border is alpha, like the fill, so the edge shifts with the
surface. The modal and landing cards had the same mismatch through
--cc-border-subtle, less visibly because its chroma is lower.

Declared per consumer rather than on .surface-glass, so the borderless
glass (scrubber, mode toggle) stays borderless and no cascade order
decides the colour. --cc-border-tooltip stays for the InfoPane
blockquote that still uses it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
themes.css swaps --cc-bg-app/-chrome/-sidebar across four presets but
overrode no border token, so picking warm or green gave you a warm panel
ringed by a cool blue border, and every divider, input outline and slider
track in the app stayed at hue ~276 regardless.

Borders and tracks now derive from --cc-bg-sidebar with relative color
syntax: lightness as an offset, chroma as a multiplier, hue straight
through. Both constants are measured off the default surface, so the cool
theme is unchanged to the eye while the others carry their own cast. A
preset only has to swap the surface; no border restates anything.

The multiplier matters: today's borders are more saturated than the
surface they sit on, so passing chroma through unchanged would have
washed out the default theme.

Behind @supports, since relative color syntax has no per-declaration
fallback. A custom property holding a value the engine cannot parse fails
at substitution rather than at parse time, which would drop every border
to currentColor instead of to the absolute value above it.

This supersedes the alpha glass border: it was neutral, and neutral was
the one thing that would not follow the surface. The existing hierarchy,
a brighter edge on the tooltip than on the panels, now tints along with
everything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thalida
thalida merged commit 973bc5c into main Aug 9, 2026
1 check passed
@thalida
thalida deleted the feat/header-footer-simplify branch August 9, 2026 01:37
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