Skip to content

fix(ui): align the window controls with the title bar - #84

Merged
mrsibe merged 1 commit into
mainfrom
style/titlebar-and-theme-polish
Sep 24, 2026
Merged

mrsibe merged 1 commit into
mainfrom
style/titlebar-and-theme-polish

Conversation

@mrsibe

@mrsibe mrsibe commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

Fixes the title-bar / window-control geometry mismatch on Windows and Linux, makes a small pass over the theme and layout details around it, and corrects DESIGN.md where it describes something the code no longer does.

Why?

The OS draws minimize / maximize / close into a region whose height is
titleBarOverlay.height. It was 35px while the renderer's title bar was
h-11 (44px), so on Windows the controls sat above the centre of the bar they
appear to belong to.

Fixing that exposed three neighbours in the same surface, so they are fixed here
rather than left as follow-ups:

  • The renderer reserved w-32 (128px) for the controls, but three caption
    buttons occupy 138px — 10px short, which let the settings button slide under
    the minimize button.
  • Linux reserved nothing at all, so its tab strip ran underneath the controls.
  • surface-hover is translucent, so it cannot give an opaque surface-raised
    card a visible hover in dark mode: surface-hover composited over
    surface-base lands on almost the same lightness as surface-raised. That is
    why the card hover is composited as an overlay rather than swapped in.

Related issue

Related to #26 (same titleBarOverlay module; that PR fixed the symbol colour,
this one fixes the geometry).

What changed?

Window chrome

  • New src/shared/utils/windowChrome.ts holds TITLE_BAR_HEIGHT (44px) and
    WINDOW_CONTROLS_WIDTH (138px). The main process and the renderer both read
    from it, so the two halves of the title bar cannot drift apart again.
  • titleBarOverlayOptions() now returns height: TITLE_BAR_HEIGHT (was a
    literal 35).
  • TopNavigationBar reserves the control strip on every non-macOS platform, at
    the real caption-button width.
  • test/titleBarOverlay.test.ts asserts the overlay height equals the renderer
    bar height and that the strip is 3 × 46px, so a regression fails a test rather
    than showing up on someone's Windows machine.

Theme

  • theme.css declares color-scheme per theme. Native surfaces the CSS does not
    paint — scrollbars, caret, text selection, form controls, <select> popups,
    autofill — were rendering in light mode inside the dark theme.
  • Document title was still Electron.
  • The boot screen's hardcoded Loading... now uses the existing
    common.loading key and a spinner, so it is translated and not a bare string.

Layout

  • Home page gutters: the hero used px-6 and the notebook grid px-12, so the
    page title and the first card did not share a left edge. Both now use px-6
    (the page gutter in DESIGN.md), as does the empty state.
  • NotebookCard no longer renders a --chart-* colour rail. DESIGN.md forbids
    chart colours outside chart/graph surfaces, and the rail was the only place
    they appeared as decoration. The card picks up the hover state it was missing.
  • Blockquotes are 2px in both reading surfaces; they were 4px in chat and 3px in
    the editor.

Documentation (docs(design) commit)

impeccable doctor reports one finding (design-md-coverage), but reading
DESIGN.md against the code found several places where the document describes
something the code does not do. No rendered pixel changes.

  • Form field recipe was wrong twice. It said the label is text-xs and the
    control h-8; Label/FieldLabel are text-sm font-medium, Input and the
    Select trigger are h-9, and the density table already said h-9 — the
    recipe contradicted both the components and the table. Error text was
    documented as text-xs; FieldError is text-sm.
  • components/ui/field.tsx was never documented, yet it is what the settings
    surfaces use. The recipe described a hand-rolled stack instead.
  • The title-bar density tier was undocumented (28px tabs and icon buttons in
    a 44px bar), which made TopNavigationBar read as a violation of the size-8
    toolbar-icon-button rule rather than a deliberate tier.
  • Tabs was documented as having exactly one form. The open-notebook strip is
    a second form, and it expresses selection with the translucent fill rather than
    a segmented control's surface step.
  • The border-none opt-out that fix(ui): remove the dashed border from the knowledge base empty state #58 added to panel-level empty states was not
    recorded.
  • The legacy-alias list was incomplete, and implied --secondary was one of those
    aliases. It is not: it holds its own literals, nothing renders it (Button and
    Badge variant="secondary" use bg-muted), and its light value is the only
    blue-tinted neutral in the palette. DESIGN.md now marks it for deletion rather
    than adoption.

How was this tested?

  • npm run typecheck — passes.
  • npm run lint — 0 errors (113 pre-existing warnings, unchanged).
  • npm test — 50/50 pass, including the new geometry assertions.
  • npm run check:design — no violations.
  • npx electron-vite build — main, preload and renderer all build; verified in
    the output that main carries TITLE_BAR_HEIGHT = 44 and the renderer carries
    WINDOW_CONTROLS_WIDTH = 138 used by the title-bar spacer.
  • npx prettier --check — clean.

Not verified: the actual on-screen result of the controls against the bar. That
needs a Windows (and Linux) run — I could not capture it from this environment,
and I am not going to publish screenshots of the local notebook data that a
running instance would show. The geometry is asserted by test instead; a
screenshot from a Windows build would still be worth having before merge.

Open decisions this PR does not make

Both were found while checking DESIGN.md and need a call rather than a doc edit:

  1. The surface table claims the title bar is surface-sunken; it renders
    surface-base.
    TopNavigationBar carries no background class and inherits
    the page root, so the bar is the same tone as the canvas, not a distinct chrome
    band. Either the table row is wrong, or the bar should get bg-surface-sunken
    — that second option is a visible change, so I left it alone.
  2. --secondary / --secondary-foreground are dead. Now documented as such
    in DESIGN.md; deleting them from theme.css is a separate, mechanical change.

Screenshots / recordings

No before/after captures attached (see above). Happy to add them if someone can
run a Windows build.

Checklist

  • I have reviewed my own changes.
  • npm run typecheck passes.
  • npm run build passes.
  • I have tested the affected user workflow. (Windows/Linux title bar not run)
  • I have not included unrelated changes.
  • I have updated documentation when necessary.

Desktop / build changes

  • Not applicable

The OS draws minimize/maximize/close into a region whose height is
`titleBarOverlay.height`. It was 35px while the renderer's title bar was
`h-11` (44px), so on Windows the controls sat above the centre of the bar
they appear to belong to. Both halves now read their geometry from
`src/shared/utils/windowChrome.ts`, which also carries the width of the
control strip: the renderer reserved `w-32` (128px) while the three
caption buttons occupy 138px, which put the settings button under the
minimize button.

Same pass, same surfaces:

- Windows and Linux both reserve the control strip. Linux reserved
  nothing, so its tab strip ran under the controls.
- Home page gutters: the hero used `px-6` and the grid `px-12`, so the
  page title and the first card did not share a left edge.
- Notebook cards no longer carry a `--chart-*` colour rail. DESIGN.md
  forbids chart colours outside chart/graph surfaces; the card now has
  the hover state it was missing, composited as an overlay because
  swapping the opaque `surface-raised` fill for translucent
  `surface-hover` is invisible in dark mode.
- `color-scheme` is declared per theme, so native surfaces (scrollbars,
  caret, selection, form controls) stop rendering in light mode inside
  the dark theme.
- The document title was still `Electron`.
- The boot screen's hardcoded "Loading..." moved to the existing
  `common.loading` key.
- Blockquotes are 2px in both reading surfaces; they were 4px in chat
  and 3px in the editor.

DESIGN.md records the window chrome geometry, the raised-card hover
recipe, and the rule that the two halves of the title bar share one
source of truth.
@github-actions github-actions Bot added the bug Something isn't working label Sep 24, 2026
@mrsibe
mrsibe merged commit 9eb200a into main Sep 24, 2026
4 checks passed
@mrsibe
mrsibe deleted the style/titlebar-and-theme-polish branch September 24, 2026 16:51
mrsibe added a commit that referenced this pull request Sep 24, 2026
The app has four windows and each one hand-rolled its own title bar, so the
geometry under the OS window controls drifted apart:

                      macOS left   non-macOS left   right reserve
  TopNavigationBar       80px           0            138px
  MindMap / Quiz         64px         100px          128px, win32 only
  Anki                   64px         128px          128px, win32 + linux

`titleBarOverlay` is transparent, so whatever the renderer draws is what the
user sees under minimize/maximize/close. On Windows the controls sat over the
content in every window; on Linux two windows reserved nothing at all.

`windowChrome.ts` now owns all three measurements, and
`components/common/WindowTitleBar.tsx` is the only place that applies them.
`MAC_TRAFFIC_LIGHTS_WIDTH` (80px) is new: the three secondary windows reserved
64px, which is narrower than the traffic lights they sit under.

Consequences:

- Windows and Linux reserve 138px on the right in every window.
- macOS reserves 80px on the left in every window.
- The 100px / 128px "keep the title centred" left spacers are gone. The centre
  slot is centred by `flex-1` instead of by a magic number that mirrored a
  reserve it did not know the width of.
- The three secondary windows drop their per-page `platform` state and the IPC
  call that fed it. `isMac()` reads the synchronous `userAgentData` value, so
  the reserves are right on the first render instead of after an await.
- `paddingTop: '44px'` becomes `paddingTop: TITLE_BAR_HEIGHT`.

One bug introduced and fixed inside this refactor: the Anki export button lost
its `no-drag` region, which would have made it unclickable. It sets it now.

Two cleanups in the same surfaces:

- Deleted `components/I18nProvider.tsx`. Nothing imported it, it held the last
  hardcoded `Loading...` left after #84, and it was the impeccable detector's
  only remaining finding (a CSS-border spinner, which the rule reads as a card
  with an accent border). The wider dead i18n cluster — `LanguageSelector`,
  `Versions`, `lib/i18n.ts` — is reported separately rather than deleted here.
- Deleted `--secondary` / `--secondary-foreground`. DESIGN.md already recorded
  them as dead, nothing rendered them, and their light value was the only
  blue-tinted neutral in the palette.

DESIGN.md gains the shared-component recipe, the third constant, and a
correction: the surface table listed the title bar under `--surface-sunken`,
but all four windows paint `surface-base`. Three independent implementations
agreeing is the evidence, so the table was wrong, not the code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant