Skip to content

feat(portal): light / system / dark theme switch; language selector on its own row (#17) - #21

Merged
rbferreira merged 3 commits into
mainfrom
feat/17-theme-switch
Oct 14, 2026
Merged

rbferreira merged 3 commits into
mainfrom
feat/17-theme-switch

Conversation

@rbferreira

@rbferreira rbferreira commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

What was wrong / what was missing

The sidebar footer put the theme button and the language selector side by side in a 192px sidebar. They did not fit: the selector spilled past the sidebar edge — visible in every README screenshot — and Light theme wrapped onto two lines. The theme was also only light or dark, and a first visit always got dark, whatever the operating system preferred.

What this changes

  • Theme switch with three positions — light / system / dark — as a small rounded pill with a knob in the brand emerald that slides to the chosen option; the selected icon is white, the others muted.
  • System follows prefers-color-scheme and changes live when the operating system does. It is the default for anyone who never chose; a saved light or dark is kept.
  • Accessible: radiogroup / radio with aria-checked, arrow keys move the selection (roving tabindex), focus ring on the whole pill, translated labels and tooltips.
  • Sidebar footer: two rows, label on the left (small caps, like the Dashboard Admission label), control on the right.
  • The language selector stays a list — more languages are expected — styled as a pill; it truncates instead of pushing past the sidebar edge.
  • The mobile header gets the same pill in place of the ☀/🌙 button.
  • The Dashboard admission switch (Full auto / Flex) and the per-Node SG / Manual switch use the same pill — a shared PillSwitch with equal-width options; the knob turns amber while a Node is in Manual. The read-only badges are pills too.
  • <html data-theme> still holds the resolved light/dark, so the CSS and the chart colours (useTheme) are untouched. The pre-paint script in index.html mirrors the new src/lib/theme.ts.
  • Docs: Portal §40 (both languages) describes the three options and the sg-theme values.

README screenshots are retaken in a follow-up, once this is merged.

How it was verified

  • New src/lib/theme.test.ts (saved choice, default, blocked storage, resolution), src/lib/pill-switch.test.ts (keyboard wrap-around), src/components/PillSwitch.test.tsx (radiogroup, knob slot and tone, only the selected option checked and tabbable, busy) and src/components/ThemeSwitch.test.tsx (order, labels, language).
  • Clean node:22 container: tsc --noEmit, vitest (733 passing), npm run build; the no-literals guard covers the new component.
  • Deployed to a test cluster and checked in a browser: fits the 192px sidebar, dark and light; under System the Portal switched live when the operating system did; mobile header checked at 390px; Admission and per-Node mode switched on the test cluster, in both themes.

Checklist

  • Documentation updated in this pull request (or nothing documented changed)
  • CHANGELOG.md updated under [Unreleased], if users will notice the change
  • ADR written first, if the change is architectural — not architectural
  • Database changes only through a new migration — none
  • No secrets, private addresses or machine names in code, tests or docs

Closes #17

🤖 Generated with Claude Code

https://claude.ai/code/session_01Tk9HuwXZP8CdJu5vRLaoBq

@rbferreira
rbferreira merged commit ee1b649 into main Oct 14, 2026
10 checks passed
@rbferreira
rbferreira deleted the feat/17-theme-switch branch October 14, 2026 21:08
…n its own row

The sidebar footer put the theme button and the language selector side by
side in a 192px sidebar. They did not fit: the selector spilled past the
edge (visible in every README screenshot) and "Light theme" wrapped. The
theme was also only light or dark, and a first visit always got dark.

The theme is now a three-position switch with icons, in the same visual
language as the Dashboard admission switch: light, system, dark. System
follows the operating system and changes live when it does, and it is the
default for anyone who never chose; a saved light or dark is kept. The
switch is a radiogroup: arrow keys move the selection and only the
selected option is in the Tab order. The mobile header gets the same
switch, compact.

The language selector stays a list, since more languages are expected, on
its own row as wide as the sidebar.

<html data-theme> still holds the resolved light/dark, so the CSS and the
chart colours did not change. The pre-paint script in index.html mirrors
the new lib/theme.ts.

Closes #17
The first version was a rectangular segmented control with a solid blue
fill on the selected option. It worked but looked heavy next to the rest
of the sidebar.

The switch is now a small rounded pill with a knob in the brand emerald
that slides to the chosen option, the selected icon white and the others
muted. Theme and language sit on two rows, label on the left in the same
small caps as the Dashboard "Admission" label, control on the right; the
language list is a pill too and truncates instead of pushing past the
sidebar edge. The mobile header uses the same pill.

The selected icon uses a literal white: the light theme remaps
text-white to near-black, which would have put a dark icon on the knob.
…heme

The Dashboard admission switch (Full auto / Flex) and the per-Node
SG / Manual switch were the rectangular segmented controls the theme
switch started from. They now use the same pill, through a shared
PillSwitch: equal-width options, a knob that slides one slot per
position, emerald, or amber while a Node is in Manual. The read-only
badges shown without nodes:operation-mode are pills too.

The theme switch is now PillSwitch with icons, and the arrow-key logic
moved to lib/pill-switch.ts so every switch shares it.
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.

Theme control overflows the sidebar; make it a light/system/dark switch

1 participant