Repository navigation
refactor(ui): hover/selected state + tab component governance (post #496 color migrate) #499
Description
Background
The #496 color migration (sage green → logo blue) surfaced that hover and selected/active states are not governed by any enforced contract: docs/design-system.md defines a 5-state contract on paper but no test enforces it, so ~20 surfaces drifted to inline oklch / static --foreground-N mixes, and a hardcoded --color-state-selected mint block survived the migration untouched. The same scan also surfaced that tabs have no spec at all — 4 hand-written implementations, one of which bypasses the Base UI Tabs primitive entirely. This issue tracks both governance gaps. The color values themselves are in #496.
Verified against main at 226f31a5 on 2026-07-04 (hover/selected/tab drift is pre-existing; the blue token values land in #496).
Scope
Color values are handled in #496. This issue is about interaction-state governance (hover / selected / pressed) and tab component governance — enforcing the existing 5-state contract, retiring drift, and establishing a tab spec that does not exist yet. Not a visual redesign; the target direction was aligned while doing #496 — list/nav/panel selection = neutral wash + bold (no color, no checkmark), menus/dropdowns keep a checkmark. design-system.md + the settings nav and plan/skill tabs already follow this; the rest of the app does not.
CSS cascade/layer debt is tracked in #476; React architecture debt in #477.
P0 — hover/active state tokens are documented but unenforced; ~20 surfaces drifted
docs/design-system.md:56 defines a 5-state contract (default / hover · focus / active · pressed); :115-116,121 assign --hover (sidebar row / ghost button hover fill, "don't use as active") and --active (sidebar row active / button :active, "don't use as hover"). No contract test enforces this — design-system-governance-406-contract.test.ts only gates var(--accent) usage, not --hover / --active.
Only ~8 sites use --hover/--active. The rest drift across three notations:
| Notation | Distinct values | Representative sites |
|---|---|---|
inline oklch(from var(--foreground) l c h / X) |
8 opacities (0.025/0.04/0.045/0.05/0.06/0.065/0.07/0.08) | sidebar.css:738 (0.06), nav-sidebar.css:203 (0.065), sidebar.css:762 (0.07), daily-review.css:463 (0.045) |
static --foreground-N mix tokens (2/3/5/8/10) |
5 tokens at ~20 hover sites | nav-sidebar.css:193 (--foreground-5), tool-output.css:213 (--foreground-3), models.css:160 (--foreground-10), model-switcher.css:44 (--foreground-8) |
--hover / --active tokens |
4% / 8% | maka-tokens.css:932,1721 (--hover), :934,1722 (--active) — the canonical pattern, underused |
--foreground-N is color-mix static real color, not alpha overlay. composer.css:82-86 already documents the bug: --foreground-3 (≈#f7f7f7) is "indistinguishable from the gray plate (≈#ebebeb)" on glass — yet 16 sites still use --foreground-5 and 4 use --foreground-3 as hover backgrounds. frontend-css-governance.md §5 bans hardcoded colors, so the inline oklch hover values technically violate governance, but nothing enforces it.
Impact: hover visibility is inconsistent (some hovers invisible on glass), selection has 5 mechanisms, and any new surface will hand-roll again.
Direction: Add a contract test that enforces the 5-state contract — hover/active backgrounds for list/nav/button-ghost surfaces must resolve to --hover/--active (or a new --state-hover-bg/--state-selected-bg pair), with an allowlist for genuine exceptions (brand CTAs, destructive). Repoint the ~20 drift sites. Retire --foreground-N as hover/selected backgrounds.
P0 — selected/active state has 5 mechanisms and a platform split
- Sidebar:
theme-glass.css:117(darwin) uses--color-state-selectedsolid color block; non-darwinsidebar.css:762uses 7% neutral wash. Platform split. --color-state-selected(theme-glass.css:92,107) is an orphan token — only the sidebar darwin override uses it; the comment claims it will be "reused for chip/tab/nav selected states later" but it never was.- Settings nav:
nav-sidebar.css:197uses 6.5% neutral wash + weight 500 (comment: "keep selected row neutral. A leading accent rail reads like a stray status marker in the Settings nav"). - But the same-class surfaces use brand color instead: search results
sidebar.css:464, command palettechat-header.css:850, artifact rowsmodels.css:609, daily-review archivedaily-review.css:151, memory previewtool-stream.css:158all use--nav-activebrand color for selected — inconsistent with the settings-nav "neutral" decision. - Tabs: see next P0.
Impact: "selected" reads 5 different ways; the colored-vs-neutral boundary is arbitrary per surface.
Direction: Adopt the aligned direction — list/nav/panel selection = neutral wash + bold (no color, no checkmark); menus/dropdowns = checkmark (model switcher, permission mode already do this). Remove --color-state-selected + the darwin override (collapse the platform split). Retire the colored-selected uses on list/panel items (search/palette/artifact/daily-review/memory) to neutral, or document an allowlist if any must stay colored.
P0 — tabs have no spec; 4 hand-written implementations
There is a shared primitive packages/ui/src/primitives/tabs.tsx (Base UI Tabs, default/underline variants + tailwind), but no surface follows it cleanly:
- plan/skill tabs (
module-pages/plan-reminders.css:424,module-pages/skills.css:225): useTabsList/TabsTriggerbut hand-write neutral-underline CSS over it. - catalog tabs (
settings/models.css:164,settings/provider-editor.css:37): usePrimitiveTabsListbutdisplay:nonethe Base UI sliding indicator (models.css:170-176comment: "the pill background is the single source of the active state") and hand-write brand-color pill. - daily-review range tab (
daily-review-panel.tsx:398): fully hand-rolled<nav><button className="maka-daily-review-range-tab">, does not use the Tabs primitive at all.
No shared maka-tab base class, no --tab-* token. design-system.md has no tab/segmented-control component section; frontend-css-governance.md doesn't mention tabs; the roadmap doesn't list a tab spec. Only notes/reference-atlas.md:300 mentions a workbench-tab | Tab control variant entry (upstream reference, not Maka's spec).
Impact: 4 visual languages for tabs; new tab surfaces hand-roll again; the primitive exists but is bypassed/overridden.
Direction: Decide one tab spec (variant system — e.g., underline vs pill as intentional documented variants, not per-surface hand-writing), expose it through the primitive + shared class/token, migrate the 4 surfaces, add a contract test that bans hand-written tab active/hover CSS outside the shared spec.
P1 — design-system.md color section is stale post-migration
docs/design-system.md:118 still references "accent 绿…2.46:1" and the old --action/--control green semantics. The #496 migration moved accent/action/control to logo blue (hue 250), but the doc wasn't updated.
Impact: the spec doc contradicts the shipped tokens; future work referencing the doc gets wrong color guidance.
Direction: update the color section to the blue tokens (accent/action/control/foreground values + WCAG ratios). Can ride #496 or this issue's first PR.
P2 — press (:active) scale is not converged
:active press feedback uses 5 scale values: 0.92 (chat-header.css:592 toast-close), 0.97 (global base.css:94 + most), 0.98 (sidebar.css:749 list-row, models.css:605 artifact-row), 0.985 (nav-sidebar.css:630 settings nav/back), 0.99 (daily-review.css:466, sidebar.css:945 prompt-chip) plus translate variants. docs/design-refinement-roadmap-2026-07.md:61 says ":active scale(0.97) 已 shipped" as the standard.
Impact: press feel is subtly inconsistent; no contract guards it.
Direction: converge to 0.97 with a documented allowlist for genuine exceptions; optional contract test.
Suggested PR order
- Color doc sync (P1) — update
design-system.mdcolor section to blue; can ride feat(design-system): migrate default color system from sage green to logo blue #496. - Hover/active state governance (P0) — consolidate on
--hover/--active(or--state-hover-bg/--state-selected-bg), repoint ~20 drift sites, add the 5-state contract test, retire--color-state-selected+ the darwin override, unify list/nav/panel selection to neutral + menus to checkmark. - Tab spec (P0) — design the variant system, migrate the 4 surfaces, add a tab contract test. Largest; do after state governance lands.
Out of scope
- Color token values / blue migration (in feat(design-system): migrate default color system from sage green to logo blue #496).
- Visual redesign beyond the documented neutral-wash + checkmark target.
- CSS cascade / layer debt (tracked in refactor(ui): CSS architecture debt cleanup (post #430 converge) #476).
- React architecture debt (tracked in refactor(ui): React architecture debt cleanup (post #430 converge) #477).
Activity
Astro-Han commentedon Jul 4, 2026
Implementation plan (shape alignment 2026-07-04)
4 decision points settled via shape; #499 splits into 2 PRs. Color doc rides #496 (already in progress).
Token model (decision 1)
--state-hover-bg(oklch 4% foreground alpha) +--state-selected-bg(oklch 6.5% foreground alpha), defined in maka-tokens.css:rootand.dark.--hover/--active: delete maka-tokens.css:159-160 definitions, sync the Tailwind bridge--color-hover/--color-active, repoint ~11 references to the new names. Delete the old names outright, no alias.--state-selected-bgas background, with no scale (see decision 4).Selected governance (decision 2, fully neutral, no exceptions)
--state-selected-bg+ font-weight 500; brand border/rail removed:models.css), daily-review archive (daily-review.css), memory preview (tool-stream.css)chat-header.css), search results (sidebar.css)--control 0.07→--state-selected-bg(--controlstays for checkbox/switch/progress)--active8% →--state-selected-bg6.5% (align with the settings nav sample)--color-state-selected(theme-glass.css:92/107) + the darwin override (:117); the platform split collapses.--nav-activeis NOT retired (onboarding brand emphasis still uses it); it just stops being used in selected contexts.Tab spec (decision 3, single underline variant)
var(--foreground)text + font-weight 600 + 2pxvar(--foreground)under-bar.tabs.tsx): drop thedefault(pill sliding indicator) variant, keep only underline; indicator becomes a foreground neutral under-bar.::after, catalog drops the brand pill +display:none, daily-review-range migrates onto the primitive (needs a tsx change).Press governance (decision 4, delete scale)
base.cssbutton:active scale(0.97),buttonVariantsactive:scale-[0.98],.maka-buttontranslateY(0.5px), the non-button press scales, toast-close 0.92.:active/data-pressedbackground--state-selected-bg, no scale. Also drop the reduced-motion transform-none fallback.data-pressedbackground (item.tsxalready does); scale is hand-written redundancy, and the three copies disagree with each other.Doc + storybook
--hover/--activeregistration → new tokens; §3 component 5-state tables repoint to new tokens; Button "active pressed" → "pressed =--state-selected-bgbackground, no scale"; drop SessionRow/ModelTable accent bar descriptions; add a tab component contract section.packages/ui/stories/interaction-states.stories.tsx) +storybook-baseline-contract.test.tsextended (hover/selected bg must use the state tokens).PR split + build order
pi/state-governance): token rename + selected neutral + darwin retire + press delete scale + doc state section + storybook + contract extension. First.pi/tab-spec): primitive underline + migrate 4 surfaces + contract. Depends on the former; second.Out of scope
Merge origin/main into pi/state-governance
refactor(ui): drop dead press-scale CSS residue (palette/toast/artifa…
refactor(ui): drop skill-library-row press transform + composer activ…
refactor(ui): route sidebar action-overlay bg through state tokens
docs(design-system): drop stale "微小 translate (PR40)" from SessionRow…
test(ui): ban --accent too in selected/active + cover [data-checked];…
refactor(ui): neutralize prompt-chip hover (state-hover-bg + border-s…
refactor(ui): neutralize 3 base-neutral hovers; ban --toast-accent al…
refactor(ui): neutralize bot-list active row; ban --bot-brand-* in se…
refactor(ui): retire --hover/--active for state tokens, unify selecte…
feat(ui): add maka-tab spec primitive + migrate plan tabs (#499 P0-3 …
refactor(ui): unify tabs on primitives/tabs, drop ui.tsx hand-rolled set
feat(ui): migrate catalog tabs to maka-tab pill + TabsPanel (#499 P0-…
feat(ui): migrate skill tabs from hand-rolled buttons to maka-tab + u…
feat(ui): migrate daily-review range to SettingsSegmented, neutral ac…
docs(ui): close out tab spec — allowlist retire, design-system.md §3.…
refactor(ui): drop redundant maka-tab class + merge skill empty/norma…
test(ui): tighten tab-spec TabsPanel locks to per-value (#499 P0-3 re…
feat(ui): tab component spec — maka-tab + variants, migrate 4 tab sur…
Astro-Han commentedon Jul 4, 2026
全部 5 部分已落地,关闭本 issue。
P0-3 详情见 #517:一个 tab spec primitive(
maka-tabclass +underline/pillvariant 在 Base UI Tabs 上),active/hover 复用 #503 的 neutral--state-*token(不引专属 tab 颜色 token);4 个手写 tab surface 全迁——plan/skill → 下划线 Tabs+TabsPanel(skill 从手搓 segmented switcher 升级真 tabs,a11y 提升),catalog → pill TabsPanel(active 从 brand 变 neutral),daily-review range → SettingsSegmented(切参数用 ToggleGroup,不是 tabs);顺带清掉ui.tsx两套手写 tab 之债(#477)。契约在tab-spec-499-contract.test.ts(per-value TabsPanel 锁 + 禁手写 tab CSS 防回退)。fix(desktop): use state-hover-bg for session health notice action
refactor(desktop): move session health feedback into the conversation…
fix(design-system): make the radius contract describe the app it gove…