Skip to content

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) - #517

Merged
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec
Jul 4, 2026
Merged

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3)#517
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Tab component governance for #499 P0-3: one tab spec primitive (maka-tab class + underline/pill variants on Base UI Tabs), migrate the 4 hand-written tab surfaces, retire per-surface hand-written tab CSS, and collapse the duplicate tab sets (#477 debt). Tab active/hover repoint to the neutral --state-* tokens from #503 — no dedicated tab color token.

Why

#499 P0-3 — tabs had no spec; 4 hand-written implementations drifted: plan/skill underline with dead [data-state] selectors (Base UI sets data-active), catalog brand pill hiding the Base UI indicator, daily-review range hand-rolled <button aria-pressed>. Plus packages/ui/src/ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no variant, dead data-[selected]) that plan-reminder-panel consumed, bypassing the existing primitives/tabs.tsx. Direction: structural chrome stays neutral (brand is garnish), one primitive, two intentional variants — and a segmented control for parameter switches (not Tabs).

Refs #499

Scope

Changed:

  • primitives/tabs.tsx: TabsVariant adds "pill"; TabsList sets data-variant + maka-tabs-list; pill hides the Base UI indicator; underline indicator recolored from brand bg-control to neutral bg-foreground. TabsTab emits the shared maka-tab class.
  • ui.tsx: TabsRoot/TabsList/TabsTrigger/TabsPanel re-export from primitives/tabs.tsx (drops the second hand-rolled set + dead data-[selected] active selectors). One tab primitive, not two (refactor(ui): React architecture debt cleanup (post #430 converge) #477 debt).
  • maka-tokens.css: .maka-tab spec — pill base shape + --state-selected-bg active + --state-hover-bg hover + neutral border; underline active = --foreground text. Reuses refactor(ui): retire --hover/--active for state tokens, unify selected neutral, drop press scale #503 state tokens; no dedicated tab color token.
  • plan-reminder-panel.tsx: variant="underline" + maka-tab; drops the dead .maka-plan-tab[data-state="active"]::after under-bar (Base UI sets data-active, not data-state — plan's active visual was actually the Base UI default sliding plate, not the hand-written under-bar).
  • ProvidersPanel.tsx: catalog → variant="pill" + maka-tab + TabsPanel (oauth → ModelOAuthSection; domestic/overseas/local → filtered provider grid). Drops data-active={catalogTab === tab.id} (Base UI sets it) and the catalogTab hand-written class; keeps data-catalog-tab. Content moves from bare conditional render into panels.
  • skills-panel.tsx: hand-rolled segmented switcher (UiButton + aria-pressed + data-state) → Tabs/TabsList/TabsTrigger/TabsPanel (market/builtin/installed each a panel). a11y upgrade: roving tabindex + arrow-key contract + linked tabpanels. skillList gains a label param (no longer depends on activeSkillTab — safe under default keepMounted=false).
  • daily-review-panel.tsx: range (今日/本周/本月) → SettingsSegmented (Base UI ToggleGroup). It switches a time-window parameter, not views — segmented control is the a11y-correct primitive, not Tabs/TabsPanel. Active inherits the neutral .settingsSegmented button[data-pressed] chrome.
  • CSS cleanup: drop .catalogPillTabs button base/hover/active + [data-slot=tab-indicator] display:none (models.css), .catalogTab:hover/[data-active] (provider-editor.css), .maka-skill-tab[data-state=active] + ::after (skills.css), .maka-daily-review-range-tab[data-active] brand (daily-review.css), .maka-plan-tab[data-state=active] + ::after (plan-reminders.css).
  • state-token-governance-499-contract.test.ts: drop the 5 tab selectors from the selected/active brand-token allowlist (tabs no longer need the carve-out). Onboarding brand-emphasis allowlist stays.
  • design-system.md §3.13 Tabs: documents the spec (primitive, variants, no dedicated color token, segmented-control rule).
  • Contract tests updated: tab-spec-499-contract.test.ts (new, per-surface), model-oauth-section-contract + claude-subscription-experimental-gate (catalog conditional render → TabsPanel), skills.test (skillList aria-label dynamic), daily-review-copy-feedback-contract (range → SettingsSegmented), radius-converge-contract (drop stale TabsTrigger/TabsList ui.tsx entries), design-system-governance-406-contract (under-bar bg-controlbg-foreground).

Not included:

Verification

  • npm run -w @maka/desktop test: 1888 pass / 0 fail (rebased onto main 85c81d9d).
  • npm run typecheck: clean (main + renderer + storybook).
  • Visual: captured the 4 tab/segmented surfaces × {light, dark} = 8 default-state screenshots with the in-repo fixture harness and human-verified them. Command (from repo root, after npm run build):
    node scripts/capture-screenshots.mjs --scenario <scenario> --variant <light-1280-motion|dark-1280-motion>
    
    Scenarios: provider-workspace (catalog pill), module-skills (skill tabs), module-daily-review (daily-review range), plan-reminders (plan underline). Output: apps/desktop/tests/screenshots/<scenario>/<variant>.png. Conclusion: catalog pill active = neutral wash, skill/plan underline = neutral bar, daily-review range = neutral SettingsSegmented chrome — correct in both light and dark.
  • Token / a11y (objective, not pixel-based): tab-spec-499-contract verifies .maka-tab active/hover use --state-selected-bg / --state-hover-bg (neutral) with no brand token; design-system-governance-406 verifies the under-bar is bg-foreground; keyboard a11y (roving tabindex + arrow keys + tabpanel pairing) is guaranteed by Base UI Tabs (plan/catalog/skill) and Base UI ToggleGroup (daily-review range).
  • Harness limit: capture-screenshots.mjs variants are theme × viewport × motion; it does not capture hover/active/keyboard state variants. Hover/active state correctness is covered by the token/static contract tests above (not by screenshots); keyboard behavior is covered by the Base UI primitive contract. A CDP-driven state-variant harness is a separate piece of work.
  • Pre-existing note (not introduced here): 23 tests import dist/renderer/settings/*.js by path, but build:renderer is vite (chunks, not per-path). A fresh worktree lacks these by-path files; this PR was verified with tsc -p tsconfig.renderer.json --outDir dist/renderer to emit per-path dist. This is a test/build setup mismatch — CI may need the same workaround, or the test script should build renderer per-path.

User-facing impact

  • Catalog tabs (供应商分类) active: brand blue → neutral wash + bold.
  • Skill tabs: same neutral underline visual, but now real tabs (arrow-key nav + tabpanel) instead of a segmented switcher.
  • Daily-review range: brand blue active → neutral white-fill + shadow (SettingsSegmented chrome).
  • Plan tabs: Base UI default sliding plate → neutral underline (the hand-written under-bar was dead CSS).

Reviewer notes

  • 8 commits: slice 1 primitive+plan, unify ui.tsx, slice 2 catalog, slice 3 skill, slice 4 daily-review, slice 5 close-out, review P3-2/4 (drop redundant maka-tab + merge skill double return), review P3 (tighten TabsPanel locks to per-value). Each independently builds/typechecks/tests; squash-merge recommended.
  • The ui.tsx two-sets-of-tabs unification is the structural fix that makes slice 1's variant="underline" actually typecheck (plan imported TabsList from ui.js, which was the hand-rolled set without variant). It's the refactor(ui): React architecture debt cleanup (post #430 converge) #477 duplicate-component debt P0-3 had to collapse.
  • Biggest a11y change: skill (segmented switcher → real tabs). Biggest visual change: catalog + daily-review range (brand → neutral).
  • Review round 2: dropped the redundant maka-tab class on tab triggers (primitive already emits it); merged the skills-panel empty/non-empty duplicate return; tightened the TabsPanel contract locks from /TabsPanel/ (matched imports) to per-value (TabsPanel value="tasks", value="oauth" + value={cat} + ['domestic','overseas','local'], value="market|builtin|installed"). See Verification for the visual verification record.

…slice 1)
Introduce the shared tab spec: maka-tab class + underline/pill variants
on the Base UI Tabs primitive, active/hover repointed to
--state-selected-bg / --state-hover-bg (no brand token, no per-surface
hand-written tab CSS). underline keeps the Base UI indicator recolored
to neutral --foreground (was brand --control); pill hides the indicator
and fills the active tab with --state-selected-bg + bold.
Migrate plan tabs as the first consumer: TabsList passes variant=
"underline", TabsTrigger carries maka-tab (layout stays on maka-plan-tab).
Removes the dead .maka-plan-tab[data-state="active"] under-bar (Base UI
sets data-active, not data-state, so it never matched — plan's active
visual was actually the Base UI default sliding plate, not the
hand-written under-bar).
Tab spec contract test (tab-spec-499-contract.test.ts) locks: primitive
exposes maka-tab + underline|pill; .maka-tab uses neutral state tokens;
plan consumes maka-tab + underline with no hand-written active CSS.
ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no
variant, dead data-[selected] active selectors — Base UI sets data-active)
that plan-reminder-panel consumed, bypassing the tab spec primitive. Re-export
TabsRoot/TabsList/TabsTrigger/TabsPanel from primitives/tabs so every tab
surface gets the maka-tab class + underline/pill variants + the correct
data-active attribute from one primitive. Removes the BaseTabs import.
This is the #477 duplicate-component debt that #499 P0-3 tab governance has
to collapse: one tab primitive, not two.
…3 slice 2)
ProvidersPanel catalog tabs move to the shared spec: TabsList passes
variant="pill", TabsTrigger carries maka-tab (catalogTab hand-written class
dropped), content renders through PrimitiveTabsPanel (one per category:
oauth -> ModelOAuthSection, domestic/overseas/local -> provider grid filtered
by category) instead of a bare conditional render. The hand-written
data-active={catalogTab === tab.id} boolean is removed (Base UI sets
data-active); data-catalog-tab={tab.id} stays (locked by model-oauth contract
as the tab identifier).
CSS cleanup: drop .catalogPillTabs button base/hover/active and the
[data-slot=tab-indicator] display:none (models.css), and .catalogTab:hover /
[data-active] (provider-editor.css). The pill base shape (radius/border/
bg/padding) moves to .maka-tab[data-variant=pill] in maka-tokens.css; active
repoints from brand --nav-active to neutral --state-selected-bg + a neutral
border. .catalogTabs/.catalogPillTabs strong stay as the label font hook.
model-oauth-section-contract.test.ts: update the catalog tab assertions from
the old catalogTab/catalogTabs catalogPillTabs class lock to variant=pill +
maka-tab + TabsPanel (intent unchanged — use the primitive, keep
data-catalog-tab, no manual keyboard handler). tab-spec-499-contract.test.ts
adds the slice 2 contract: catalog consumes maka-tab + pill + TabsPanel, no
hand-written catalogTab/catalogPillTabs active/hover/indicator CSS.
…nderline + TabsPanel (#499 P0-3 slice 3)
skills-panel tabs move from a hand-rolled segmented switcher (UiButton +
aria-pressed + data-state, with a comment explicitly disowning the ARIA tabs
contract) to the shared Base UI Tabs primitive: TabsList passes
variant="underline", TabsTrigger carries maka-tab, and content renders through
TabsPanel (one per market/builtin/installed). This upgrades the surface from
a segmented switcher to real tabs with the roving-tabindex + arrow-key
contract + linked tabpanels.
The dispersed conditional render ({activeSkillTab === 'market' ? market :
skillList(...)} + {activeSkillTab !== 'market' && templates}) concentrates into
three TabsPanels. skillList gains a label param (replacing the internal
activeSkillTab === 'installed' ? ... : ... label switch) so each panel renders
its own label without depending on the active tab state — safe under the
default keepMounted=false (only the active panel mounts) and robust if
keepMounted is ever flipped. The empty-title/empty-body strings hoist to
component scope (skillListEmptyTitle/skillListEmptyBody) so both the empty-skills
and populated returns share them.
The tabs-bar keeps its right-side filter actions (static labels, market-only)
as a sibling of TabsList inside TabsRoot — they are not a tab/panel.
CSS: drop .maka-skill-tab[data-state="active"] and its ::after under-bar
(skills.css). The active state + under-bar move to .maka-tab[data-variant=
underline] via the Base UI indicator (neutral --foreground).
skills.test.ts: update the skillList section assertion from the literal
aria-label="已安装技能" to the dynamic aria-label={label} (intent unchanged —
the section still carries an aria-label, now driven by the label param).
tab-spec-499-contract.test.ts adds the slice 3 contract: skill uses maka-tab +
underline + TabsPanel, no hand-written aria-pressed/data-state switcher, no
hand-written .maka-skill-tab[data-state="active"] / ::after CSS.
…tive (#499 P0-3 slice 4)
daily-review range (今日/本周/本月) switches a time-window parameter — the
report re-fetches for the chosen range, it is not three distinct views — so it
is a segmented control, not tabs. Move it from a hand-rolled segmented switcher
(UiButton + aria-pressed + data-active) to the project's SettingsSegmented
primitive (Base UI ToggleGroup, single-select, roving tabindex + arrow keys +
data-pressed). This is the a11y-correct primitive for a parameter switcher
(radiogroup/pressed semantics), unlike Tabs/TabsPanel which expect linked
tabpanel views — the other three surfaces (plan/catalog/skill) are view
switches and stay on Tabs.
Active repoints from brand --nav-active (0.12 bg + color) to the neutral
.settingsSegmented button[data-pressed] chrome (white fill + shadow), inherited
via the primitive's global CSS. The hand-written .maka-daily-review-range-tab
base + [data-active="true"] brand block (daily-review.css) and the
PR-DAILY-REVIEW-RANGE-0 brand-accent comment are removed; .maka-daily-review-
range-tabs keeps its flex/gap layout (now the SettingsSegmented className).
daily-review-copy-feedback-contract.test.ts: update the range-tab assertion
from the hand-rolled UiButton maka-daily-review-range-tab to SettingsSegmented +
maka-daily-review-range-tabs, and flip the brand-active CSS assertion from
assert.match to assert.doesNotMatch (the brand active CSS is gone). Intent
unchanged — Daily Review controls use shared primitives, no legacy classes.
tab-spec-499-contract.test.ts adds the slice 4 contract: range uses
SettingsSegmented, no hand-written aria-pressed/data-active switcher, no
.maka-daily-review-range-tab[data-active] brand CSS.
…13, contract updates (#499 P0-3 slice 5)
Tab spec close-out:
- state-token-governance-499-contract.test.ts: drop the 5 tab selectors
(.maka-daily-review-range-tab / .catalogTab / .catalogPillTabs / .maka-skill-tab
/ .maka-plan-tab) from the selected/active brand-token allowlist. All four
tab surfaces migrated in slices 1-4 now use neutral state tokens, so the
"pending tab-spec" carve-out is gone; only the onboarding brand-emphasis
allowlist (.maka-firstrun-step / .maka-onboarding-setup-steps) remains.
- design-system.md §3.13 Tabs: document the spec — one primitive
(primitives/tabs.tsx, re-exported from ui.tsx), two variants (underline =
neutral Base UI indicator; pill = --state-selected-bg fill + bold), no
dedicated tab color token (reuse --state-hover-bg / --state-selected-bg),
and the segmented-control rule (parameter switches like daily-review range
use SettingsSegmented / Base UI ToggleGroup, not Tabs).
- radius-converge-contract.test.ts: remove the stale TabsTrigger/TabsList
entries (they used to be declared in ui.tsx with --radius-* tokens; #499 P0-3
re-exports them from primitives/tabs.tsx, which uses Tailwind rounded-md/
rounded-sm governed by primitives-design-contract escape hatches, not the
radius-token convergence contract).
- design-system-governance-406-contract.test.ts: the tab underline indicator
moves from brand --control to neutral --foreground (structural chrome stays
calm, brand is garnish — the principle already documented in
plan-reminders.css). Update the assertion from bg-control to bg-foreground
and add a ban on bg-control for the under-bar (checkbox/progress/menu keep
bg-control — those are checked controls, not structural chrome).
- claude-subscription-experimental-gate.test.ts: the OAuth catalog content
moved from a bare conditional render (catalogTab === 'oauth' ?
<ModelOAuthSection/>) into a TabsPanel, matching the model-oauth-section
contract update in slice 2. Intent unchanged — the OAuth tab renders the
real login cards.
Verified: npm run -w @maka/desktop test 1878 pass / 0 fail; typecheck clean.
@Astro-Han
Astro-Hanforce-pushed the feat/499-tab-component-spec branch from 3bfaf67 to d08300aCompareJuly 4, 2026 16:01
…l return (#499 P0-3 review P3-2/4)
Address review P3-2 + P3-4 on PR #517 (tab component spec).
P3-2 — redundant maka-tab class on tab triggers. The TabsTab primitive
already emits `maka-tab` (tabs.tsx), so callers passing
`className="maka-tab ..."` duplicate it. Drop `maka-tab` from the three
call sites, keeping local layout classes:
- skills-panel.tsx: "maka-tab maka-skill-tab" → "maka-skill-tab"
- plan-reminder-panel.tsx: "maka-tab maka-plan-tab" → "maka-plan-tab" (×2)
- ProvidersPanel.tsx: drop the bare "maka-tab" (catalog has no local class)
Sync the contract tests that locked "caller must pass maka-tab" — that
constraint is redundant now the primitive guarantees the class:
- tab-spec-499-contract.test.ts: drop the three per-surface
assert.match(TabsTrigger className maka-tab). Keep variant, TabsPanel,
and the doesNotMatch anti-regression locks.
- model-oauth-section-contract.test.ts: catalog tablist contract keeps
PrimitiveTabsTrigger + value={tab.id}, drops className="maka-tab"
(maka-tab comes from the primitive).
P3-4 — skills-panel double return. The empty-skills and non-empty
returns duplicated the entire TabsRoot/tabs/TabsPanel tree, differing
only by `skillList([] …)` vs `skillList(filteredSkills …)` and a
trailing tool-summary span. Merge to one return: filteredSkills is `[]`
when props.skills is empty (it's `(props.skills ?? []).filter(…)`), so
`skillList(filteredSkills, …)` is equivalent in both states; the summary
span becomes conditional. Cuts ~15 lines and removes a future drift
hazard (changing the tabs tree used to require editing two returns).
Not done (P3-3 / P3-5): the rest of tab-spec-499-contract is not
redundant lock-on-implementation. `variant="underline|pill"` locks each
surface's form spec (a result). `TabsPanel` is the only a11y guard
(tabpanel role + roving tabindex) until the project has render-test
infrastructure. The doesNotMatch locks prevent hand-written tab
CSS/switcher regression. Only the className maka-tab lock was true
redundant lock-on-implementation; the rest stays.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
…view P3)
Address review round 2 P3 (PR #517): the `assert.match(panel, /TabsPanel/)`
locks were too wide — they matched the import statement
(`TabsPanel } from './ui.js'`) and any TabsPanel, not a panel for each tab
value.
Tighten to per-value locks so each tab value has a corresponding panel
(tabpanel a11y pairing), and deleting any panel fails the test:
- plan: TabsPanel value="tasks" + value="runs" (new — plan had no TabsPanel
lock before)
- catalog: PrimitiveTabsPanel value="oauth" (fixed OAuth panel) +
value={cat} (mapped category panel template) + ['domestic','overseas','local']
(the mapped array — proves every category has a panel without a runtime
render test)
- skill: TabsPanel value="market" + value="builtin" + value="installed"
Unrelated TabsPanel occurrences (e.g. import lines) no longer satisfy the
lock: deleting any target surface panel fails the test, the import line
alone does not pass it.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) by Astro-Han · Pull Request #517 · apache/maka · GitHub
Skip to content

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) - #517

Merged
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec
Jul 4, 2026
Merged

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3)#517
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Tab component governance for #499 P0-3: one tab spec primitive (maka-tab class + underline/pill variants on Base UI Tabs), migrate the 4 hand-written tab surfaces, retire per-surface hand-written tab CSS, and collapse the duplicate tab sets (#477 debt). Tab active/hover repoint to the neutral --state-* tokens from #503 — no dedicated tab color token.

Why

#499 P0-3 — tabs had no spec; 4 hand-written implementations drifted: plan/skill underline with dead [data-state] selectors (Base UI sets data-active), catalog brand pill hiding the Base UI indicator, daily-review range hand-rolled <button aria-pressed>. Plus packages/ui/src/ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no variant, dead data-[selected]) that plan-reminder-panel consumed, bypassing the existing primitives/tabs.tsx. Direction: structural chrome stays neutral (brand is garnish), one primitive, two intentional variants — and a segmented control for parameter switches (not Tabs).

Refs #499

Scope

Changed:

  • primitives/tabs.tsx: TabsVariant adds "pill"; TabsList sets data-variant + maka-tabs-list; pill hides the Base UI indicator; underline indicator recolored from brand bg-control to neutral bg-foreground. TabsTab emits the shared maka-tab class.
  • ui.tsx: TabsRoot/TabsList/TabsTrigger/TabsPanel re-export from primitives/tabs.tsx (drops the second hand-rolled set + dead data-[selected] active selectors). One tab primitive, not two (refactor(ui): React architecture debt cleanup (post #430 converge) #477 debt).
  • maka-tokens.css: .maka-tab spec — pill base shape + --state-selected-bg active + --state-hover-bg hover + neutral border; underline active = --foreground text. Reuses refactor(ui): retire --hover/--active for state tokens, unify selected neutral, drop press scale #503 state tokens; no dedicated tab color token.
  • plan-reminder-panel.tsx: variant="underline" + maka-tab; drops the dead .maka-plan-tab[data-state="active"]::after under-bar (Base UI sets data-active, not data-state — plan's active visual was actually the Base UI default sliding plate, not the hand-written under-bar).
  • ProvidersPanel.tsx: catalog → variant="pill" + maka-tab + TabsPanel (oauth → ModelOAuthSection; domestic/overseas/local → filtered provider grid). Drops data-active={catalogTab === tab.id} (Base UI sets it) and the catalogTab hand-written class; keeps data-catalog-tab. Content moves from bare conditional render into panels.
  • skills-panel.tsx: hand-rolled segmented switcher (UiButton + aria-pressed + data-state) → Tabs/TabsList/TabsTrigger/TabsPanel (market/builtin/installed each a panel). a11y upgrade: roving tabindex + arrow-key contract + linked tabpanels. skillList gains a label param (no longer depends on activeSkillTab — safe under default keepMounted=false).
  • daily-review-panel.tsx: range (今日/本周/本月) → SettingsSegmented (Base UI ToggleGroup). It switches a time-window parameter, not views — segmented control is the a11y-correct primitive, not Tabs/TabsPanel. Active inherits the neutral .settingsSegmented button[data-pressed] chrome.
  • CSS cleanup: drop .catalogPillTabs button base/hover/active + [data-slot=tab-indicator] display:none (models.css), .catalogTab:hover/[data-active] (provider-editor.css), .maka-skill-tab[data-state=active] + ::after (skills.css), .maka-daily-review-range-tab[data-active] brand (daily-review.css), .maka-plan-tab[data-state=active] + ::after (plan-reminders.css).
  • state-token-governance-499-contract.test.ts: drop the 5 tab selectors from the selected/active brand-token allowlist (tabs no longer need the carve-out). Onboarding brand-emphasis allowlist stays.
  • design-system.md §3.13 Tabs: documents the spec (primitive, variants, no dedicated color token, segmented-control rule).
  • Contract tests updated: tab-spec-499-contract.test.ts (new, per-surface), model-oauth-section-contract + claude-subscription-experimental-gate (catalog conditional render → TabsPanel), skills.test (skillList aria-label dynamic), daily-review-copy-feedback-contract (range → SettingsSegmented), radius-converge-contract (drop stale TabsTrigger/TabsList ui.tsx entries), design-system-governance-406-contract (under-bar bg-controlbg-foreground).

Not included:

Verification

  • npm run -w @maka/desktop test: 1888 pass / 0 fail (rebased onto main 85c81d9d).
  • npm run typecheck: clean (main + renderer + storybook).
  • Visual: captured the 4 tab/segmented surfaces × {light, dark} = 8 default-state screenshots with the in-repo fixture harness and human-verified them. Command (from repo root, after npm run build):
    node scripts/capture-screenshots.mjs --scenario <scenario> --variant <light-1280-motion|dark-1280-motion>
    
    Scenarios: provider-workspace (catalog pill), module-skills (skill tabs), module-daily-review (daily-review range), plan-reminders (plan underline). Output: apps/desktop/tests/screenshots/<scenario>/<variant>.png. Conclusion: catalog pill active = neutral wash, skill/plan underline = neutral bar, daily-review range = neutral SettingsSegmented chrome — correct in both light and dark.
  • Token / a11y (objective, not pixel-based): tab-spec-499-contract verifies .maka-tab active/hover use --state-selected-bg / --state-hover-bg (neutral) with no brand token; design-system-governance-406 verifies the under-bar is bg-foreground; keyboard a11y (roving tabindex + arrow keys + tabpanel pairing) is guaranteed by Base UI Tabs (plan/catalog/skill) and Base UI ToggleGroup (daily-review range).
  • Harness limit: capture-screenshots.mjs variants are theme × viewport × motion; it does not capture hover/active/keyboard state variants. Hover/active state correctness is covered by the token/static contract tests above (not by screenshots); keyboard behavior is covered by the Base UI primitive contract. A CDP-driven state-variant harness is a separate piece of work.
  • Pre-existing note (not introduced here): 23 tests import dist/renderer/settings/*.js by path, but build:renderer is vite (chunks, not per-path). A fresh worktree lacks these by-path files; this PR was verified with tsc -p tsconfig.renderer.json --outDir dist/renderer to emit per-path dist. This is a test/build setup mismatch — CI may need the same workaround, or the test script should build renderer per-path.

User-facing impact

  • Catalog tabs (供应商分类) active: brand blue → neutral wash + bold.
  • Skill tabs: same neutral underline visual, but now real tabs (arrow-key nav + tabpanel) instead of a segmented switcher.
  • Daily-review range: brand blue active → neutral white-fill + shadow (SettingsSegmented chrome).
  • Plan tabs: Base UI default sliding plate → neutral underline (the hand-written under-bar was dead CSS).

Reviewer notes

  • 8 commits: slice 1 primitive+plan, unify ui.tsx, slice 2 catalog, slice 3 skill, slice 4 daily-review, slice 5 close-out, review P3-2/4 (drop redundant maka-tab + merge skill double return), review P3 (tighten TabsPanel locks to per-value). Each independently builds/typechecks/tests; squash-merge recommended.
  • The ui.tsx two-sets-of-tabs unification is the structural fix that makes slice 1's variant="underline" actually typecheck (plan imported TabsList from ui.js, which was the hand-rolled set without variant). It's the refactor(ui): React architecture debt cleanup (post #430 converge) #477 duplicate-component debt P0-3 had to collapse.
  • Biggest a11y change: skill (segmented switcher → real tabs). Biggest visual change: catalog + daily-review range (brand → neutral).
  • Review round 2: dropped the redundant maka-tab class on tab triggers (primitive already emits it); merged the skills-panel empty/non-empty duplicate return; tightened the TabsPanel contract locks from /TabsPanel/ (matched imports) to per-value (TabsPanel value="tasks", value="oauth" + value={cat} + ['domestic','overseas','local'], value="market|builtin|installed"). See Verification for the visual verification record.

…slice 1)
Introduce the shared tab spec: maka-tab class + underline/pill variants
on the Base UI Tabs primitive, active/hover repointed to
--state-selected-bg / --state-hover-bg (no brand token, no per-surface
hand-written tab CSS). underline keeps the Base UI indicator recolored
to neutral --foreground (was brand --control); pill hides the indicator
and fills the active tab with --state-selected-bg + bold.
Migrate plan tabs as the first consumer: TabsList passes variant=
"underline", TabsTrigger carries maka-tab (layout stays on maka-plan-tab).
Removes the dead .maka-plan-tab[data-state="active"] under-bar (Base UI
sets data-active, not data-state, so it never matched — plan's active
visual was actually the Base UI default sliding plate, not the
hand-written under-bar).
Tab spec contract test (tab-spec-499-contract.test.ts) locks: primitive
exposes maka-tab + underline|pill; .maka-tab uses neutral state tokens;
plan consumes maka-tab + underline with no hand-written active CSS.
ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no
variant, dead data-[selected] active selectors — Base UI sets data-active)
that plan-reminder-panel consumed, bypassing the tab spec primitive. Re-export
TabsRoot/TabsList/TabsTrigger/TabsPanel from primitives/tabs so every tab
surface gets the maka-tab class + underline/pill variants + the correct
data-active attribute from one primitive. Removes the BaseTabs import.
This is the #477 duplicate-component debt that #499 P0-3 tab governance has
to collapse: one tab primitive, not two.
…3 slice 2)
ProvidersPanel catalog tabs move to the shared spec: TabsList passes
variant="pill", TabsTrigger carries maka-tab (catalogTab hand-written class
dropped), content renders through PrimitiveTabsPanel (one per category:
oauth -> ModelOAuthSection, domestic/overseas/local -> provider grid filtered
by category) instead of a bare conditional render. The hand-written
data-active={catalogTab === tab.id} boolean is removed (Base UI sets
data-active); data-catalog-tab={tab.id} stays (locked by model-oauth contract
as the tab identifier).
CSS cleanup: drop .catalogPillTabs button base/hover/active and the
[data-slot=tab-indicator] display:none (models.css), and .catalogTab:hover /
[data-active] (provider-editor.css). The pill base shape (radius/border/
bg/padding) moves to .maka-tab[data-variant=pill] in maka-tokens.css; active
repoints from brand --nav-active to neutral --state-selected-bg + a neutral
border. .catalogTabs/.catalogPillTabs strong stay as the label font hook.
model-oauth-section-contract.test.ts: update the catalog tab assertions from
the old catalogTab/catalogTabs catalogPillTabs class lock to variant=pill +
maka-tab + TabsPanel (intent unchanged — use the primitive, keep
data-catalog-tab, no manual keyboard handler). tab-spec-499-contract.test.ts
adds the slice 2 contract: catalog consumes maka-tab + pill + TabsPanel, no
hand-written catalogTab/catalogPillTabs active/hover/indicator CSS.
…nderline + TabsPanel (#499 P0-3 slice 3)
skills-panel tabs move from a hand-rolled segmented switcher (UiButton +
aria-pressed + data-state, with a comment explicitly disowning the ARIA tabs
contract) to the shared Base UI Tabs primitive: TabsList passes
variant="underline", TabsTrigger carries maka-tab, and content renders through
TabsPanel (one per market/builtin/installed). This upgrades the surface from
a segmented switcher to real tabs with the roving-tabindex + arrow-key
contract + linked tabpanels.
The dispersed conditional render ({activeSkillTab === 'market' ? market :
skillList(...)} + {activeSkillTab !== 'market' && templates}) concentrates into
three TabsPanels. skillList gains a label param (replacing the internal
activeSkillTab === 'installed' ? ... : ... label switch) so each panel renders
its own label without depending on the active tab state — safe under the
default keepMounted=false (only the active panel mounts) and robust if
keepMounted is ever flipped. The empty-title/empty-body strings hoist to
component scope (skillListEmptyTitle/skillListEmptyBody) so both the empty-skills
and populated returns share them.
The tabs-bar keeps its right-side filter actions (static labels, market-only)
as a sibling of TabsList inside TabsRoot — they are not a tab/panel.
CSS: drop .maka-skill-tab[data-state="active"] and its ::after under-bar
(skills.css). The active state + under-bar move to .maka-tab[data-variant=
underline] via the Base UI indicator (neutral --foreground).
skills.test.ts: update the skillList section assertion from the literal
aria-label="已安装技能" to the dynamic aria-label={label} (intent unchanged —
the section still carries an aria-label, now driven by the label param).
tab-spec-499-contract.test.ts adds the slice 3 contract: skill uses maka-tab +
underline + TabsPanel, no hand-written aria-pressed/data-state switcher, no
hand-written .maka-skill-tab[data-state="active"] / ::after CSS.
…tive (#499 P0-3 slice 4)
daily-review range (今日/本周/本月) switches a time-window parameter — the
report re-fetches for the chosen range, it is not three distinct views — so it
is a segmented control, not tabs. Move it from a hand-rolled segmented switcher
(UiButton + aria-pressed + data-active) to the project's SettingsSegmented
primitive (Base UI ToggleGroup, single-select, roving tabindex + arrow keys +
data-pressed). This is the a11y-correct primitive for a parameter switcher
(radiogroup/pressed semantics), unlike Tabs/TabsPanel which expect linked
tabpanel views — the other three surfaces (plan/catalog/skill) are view
switches and stay on Tabs.
Active repoints from brand --nav-active (0.12 bg + color) to the neutral
.settingsSegmented button[data-pressed] chrome (white fill + shadow), inherited
via the primitive's global CSS. The hand-written .maka-daily-review-range-tab
base + [data-active="true"] brand block (daily-review.css) and the
PR-DAILY-REVIEW-RANGE-0 brand-accent comment are removed; .maka-daily-review-
range-tabs keeps its flex/gap layout (now the SettingsSegmented className).
daily-review-copy-feedback-contract.test.ts: update the range-tab assertion
from the hand-rolled UiButton maka-daily-review-range-tab to SettingsSegmented +
maka-daily-review-range-tabs, and flip the brand-active CSS assertion from
assert.match to assert.doesNotMatch (the brand active CSS is gone). Intent
unchanged — Daily Review controls use shared primitives, no legacy classes.
tab-spec-499-contract.test.ts adds the slice 4 contract: range uses
SettingsSegmented, no hand-written aria-pressed/data-active switcher, no
.maka-daily-review-range-tab[data-active] brand CSS.
…13, contract updates (#499 P0-3 slice 5)
Tab spec close-out:
- state-token-governance-499-contract.test.ts: drop the 5 tab selectors
(.maka-daily-review-range-tab / .catalogTab / .catalogPillTabs / .maka-skill-tab
/ .maka-plan-tab) from the selected/active brand-token allowlist. All four
tab surfaces migrated in slices 1-4 now use neutral state tokens, so the
"pending tab-spec" carve-out is gone; only the onboarding brand-emphasis
allowlist (.maka-firstrun-step / .maka-onboarding-setup-steps) remains.
- design-system.md §3.13 Tabs: document the spec — one primitive
(primitives/tabs.tsx, re-exported from ui.tsx), two variants (underline =
neutral Base UI indicator; pill = --state-selected-bg fill + bold), no
dedicated tab color token (reuse --state-hover-bg / --state-selected-bg),
and the segmented-control rule (parameter switches like daily-review range
use SettingsSegmented / Base UI ToggleGroup, not Tabs).
- radius-converge-contract.test.ts: remove the stale TabsTrigger/TabsList
entries (they used to be declared in ui.tsx with --radius-* tokens; #499 P0-3
re-exports them from primitives/tabs.tsx, which uses Tailwind rounded-md/
rounded-sm governed by primitives-design-contract escape hatches, not the
radius-token convergence contract).
- design-system-governance-406-contract.test.ts: the tab underline indicator
moves from brand --control to neutral --foreground (structural chrome stays
calm, brand is garnish — the principle already documented in
plan-reminders.css). Update the assertion from bg-control to bg-foreground
and add a ban on bg-control for the under-bar (checkbox/progress/menu keep
bg-control — those are checked controls, not structural chrome).
- claude-subscription-experimental-gate.test.ts: the OAuth catalog content
moved from a bare conditional render (catalogTab === 'oauth' ?
<ModelOAuthSection/>) into a TabsPanel, matching the model-oauth-section
contract update in slice 2. Intent unchanged — the OAuth tab renders the
real login cards.
Verified: npm run -w @maka/desktop test 1878 pass / 0 fail; typecheck clean.
@Astro-Han
Astro-Hanforce-pushed the feat/499-tab-component-spec branch from 3bfaf67 to d08300aCompareJuly 4, 2026 16:01
…l return (#499 P0-3 review P3-2/4)
Address review P3-2 + P3-4 on PR #517 (tab component spec).
P3-2 — redundant maka-tab class on tab triggers. The TabsTab primitive
already emits `maka-tab` (tabs.tsx), so callers passing
`className="maka-tab ..."` duplicate it. Drop `maka-tab` from the three
call sites, keeping local layout classes:
- skills-panel.tsx: "maka-tab maka-skill-tab" → "maka-skill-tab"
- plan-reminder-panel.tsx: "maka-tab maka-plan-tab" → "maka-plan-tab" (×2)
- ProvidersPanel.tsx: drop the bare "maka-tab" (catalog has no local class)
Sync the contract tests that locked "caller must pass maka-tab" — that
constraint is redundant now the primitive guarantees the class:
- tab-spec-499-contract.test.ts: drop the three per-surface
assert.match(TabsTrigger className maka-tab). Keep variant, TabsPanel,
and the doesNotMatch anti-regression locks.
- model-oauth-section-contract.test.ts: catalog tablist contract keeps
PrimitiveTabsTrigger + value={tab.id}, drops className="maka-tab"
(maka-tab comes from the primitive).
P3-4 — skills-panel double return. The empty-skills and non-empty
returns duplicated the entire TabsRoot/tabs/TabsPanel tree, differing
only by `skillList([] …)` vs `skillList(filteredSkills …)` and a
trailing tool-summary span. Merge to one return: filteredSkills is `[]`
when props.skills is empty (it's `(props.skills ?? []).filter(…)`), so
`skillList(filteredSkills, …)` is equivalent in both states; the summary
span becomes conditional. Cuts ~15 lines and removes a future drift
hazard (changing the tabs tree used to require editing two returns).
Not done (P3-3 / P3-5): the rest of tab-spec-499-contract is not
redundant lock-on-implementation. `variant="underline|pill"` locks each
surface's form spec (a result). `TabsPanel` is the only a11y guard
(tabpanel role + roving tabindex) until the project has render-test
infrastructure. The doesNotMatch locks prevent hand-written tab
CSS/switcher regression. Only the className maka-tab lock was true
redundant lock-on-implementation; the rest stays.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
…view P3)
Address review round 2 P3 (PR #517): the `assert.match(panel, /TabsPanel/)`
locks were too wide — they matched the import statement
(`TabsPanel } from './ui.js'`) and any TabsPanel, not a panel for each tab
value.
Tighten to per-value locks so each tab value has a corresponding panel
(tabpanel a11y pairing), and deleting any panel fails the test:
- plan: TabsPanel value="tasks" + value="runs" (new — plan had no TabsPanel
lock before)
- catalog: PrimitiveTabsPanel value="oauth" (fixed OAuth panel) +
value={cat} (mapped category panel template) + ['domestic','overseas','local']
(the mapped array — proves every category has a panel without a runtime
render test)
- skill: TabsPanel value="market" + value="builtin" + value="installed"
Unrelated TabsPanel occurrences (e.g. import lines) no longer satisfy the
lock: deleting any target surface panel fails the test, the import line
alone does not pass it.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) by Astro-Han · Pull Request #517 · apache/maka · GitHub
Skip to content

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) - #517

Merged
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec
Jul 4, 2026
Merged

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3)#517
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Tab component governance for #499 P0-3: one tab spec primitive (maka-tab class + underline/pill variants on Base UI Tabs), migrate the 4 hand-written tab surfaces, retire per-surface hand-written tab CSS, and collapse the duplicate tab sets (#477 debt). Tab active/hover repoint to the neutral --state-* tokens from #503 — no dedicated tab color token.

Why

#499 P0-3 — tabs had no spec; 4 hand-written implementations drifted: plan/skill underline with dead [data-state] selectors (Base UI sets data-active), catalog brand pill hiding the Base UI indicator, daily-review range hand-rolled <button aria-pressed>. Plus packages/ui/src/ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no variant, dead data-[selected]) that plan-reminder-panel consumed, bypassing the existing primitives/tabs.tsx. Direction: structural chrome stays neutral (brand is garnish), one primitive, two intentional variants — and a segmented control for parameter switches (not Tabs).

Refs #499

Scope

Changed:

  • primitives/tabs.tsx: TabsVariant adds "pill"; TabsList sets data-variant + maka-tabs-list; pill hides the Base UI indicator; underline indicator recolored from brand bg-control to neutral bg-foreground. TabsTab emits the shared maka-tab class.
  • ui.tsx: TabsRoot/TabsList/TabsTrigger/TabsPanel re-export from primitives/tabs.tsx (drops the second hand-rolled set + dead data-[selected] active selectors). One tab primitive, not two (refactor(ui): React architecture debt cleanup (post #430 converge) #477 debt).
  • maka-tokens.css: .maka-tab spec — pill base shape + --state-selected-bg active + --state-hover-bg hover + neutral border; underline active = --foreground text. Reuses refactor(ui): retire --hover/--active for state tokens, unify selected neutral, drop press scale #503 state tokens; no dedicated tab color token.
  • plan-reminder-panel.tsx: variant="underline" + maka-tab; drops the dead .maka-plan-tab[data-state="active"]::after under-bar (Base UI sets data-active, not data-state — plan's active visual was actually the Base UI default sliding plate, not the hand-written under-bar).
  • ProvidersPanel.tsx: catalog → variant="pill" + maka-tab + TabsPanel (oauth → ModelOAuthSection; domestic/overseas/local → filtered provider grid). Drops data-active={catalogTab === tab.id} (Base UI sets it) and the catalogTab hand-written class; keeps data-catalog-tab. Content moves from bare conditional render into panels.
  • skills-panel.tsx: hand-rolled segmented switcher (UiButton + aria-pressed + data-state) → Tabs/TabsList/TabsTrigger/TabsPanel (market/builtin/installed each a panel). a11y upgrade: roving tabindex + arrow-key contract + linked tabpanels. skillList gains a label param (no longer depends on activeSkillTab — safe under default keepMounted=false).
  • daily-review-panel.tsx: range (今日/本周/本月) → SettingsSegmented (Base UI ToggleGroup). It switches a time-window parameter, not views — segmented control is the a11y-correct primitive, not Tabs/TabsPanel. Active inherits the neutral .settingsSegmented button[data-pressed] chrome.
  • CSS cleanup: drop .catalogPillTabs button base/hover/active + [data-slot=tab-indicator] display:none (models.css), .catalogTab:hover/[data-active] (provider-editor.css), .maka-skill-tab[data-state=active] + ::after (skills.css), .maka-daily-review-range-tab[data-active] brand (daily-review.css), .maka-plan-tab[data-state=active] + ::after (plan-reminders.css).
  • state-token-governance-499-contract.test.ts: drop the 5 tab selectors from the selected/active brand-token allowlist (tabs no longer need the carve-out). Onboarding brand-emphasis allowlist stays.
  • design-system.md §3.13 Tabs: documents the spec (primitive, variants, no dedicated color token, segmented-control rule).
  • Contract tests updated: tab-spec-499-contract.test.ts (new, per-surface), model-oauth-section-contract + claude-subscription-experimental-gate (catalog conditional render → TabsPanel), skills.test (skillList aria-label dynamic), daily-review-copy-feedback-contract (range → SettingsSegmented), radius-converge-contract (drop stale TabsTrigger/TabsList ui.tsx entries), design-system-governance-406-contract (under-bar bg-controlbg-foreground).

Not included:

Verification

  • npm run -w @maka/desktop test: 1888 pass / 0 fail (rebased onto main 85c81d9d).
  • npm run typecheck: clean (main + renderer + storybook).
  • Visual: captured the 4 tab/segmented surfaces × {light, dark} = 8 default-state screenshots with the in-repo fixture harness and human-verified them. Command (from repo root, after npm run build):
    node scripts/capture-screenshots.mjs --scenario <scenario> --variant <light-1280-motion|dark-1280-motion>
    
    Scenarios: provider-workspace (catalog pill), module-skills (skill tabs), module-daily-review (daily-review range), plan-reminders (plan underline). Output: apps/desktop/tests/screenshots/<scenario>/<variant>.png. Conclusion: catalog pill active = neutral wash, skill/plan underline = neutral bar, daily-review range = neutral SettingsSegmented chrome — correct in both light and dark.
  • Token / a11y (objective, not pixel-based): tab-spec-499-contract verifies .maka-tab active/hover use --state-selected-bg / --state-hover-bg (neutral) with no brand token; design-system-governance-406 verifies the under-bar is bg-foreground; keyboard a11y (roving tabindex + arrow keys + tabpanel pairing) is guaranteed by Base UI Tabs (plan/catalog/skill) and Base UI ToggleGroup (daily-review range).
  • Harness limit: capture-screenshots.mjs variants are theme × viewport × motion; it does not capture hover/active/keyboard state variants. Hover/active state correctness is covered by the token/static contract tests above (not by screenshots); keyboard behavior is covered by the Base UI primitive contract. A CDP-driven state-variant harness is a separate piece of work.
  • Pre-existing note (not introduced here): 23 tests import dist/renderer/settings/*.js by path, but build:renderer is vite (chunks, not per-path). A fresh worktree lacks these by-path files; this PR was verified with tsc -p tsconfig.renderer.json --outDir dist/renderer to emit per-path dist. This is a test/build setup mismatch — CI may need the same workaround, or the test script should build renderer per-path.

User-facing impact

  • Catalog tabs (供应商分类) active: brand blue → neutral wash + bold.
  • Skill tabs: same neutral underline visual, but now real tabs (arrow-key nav + tabpanel) instead of a segmented switcher.
  • Daily-review range: brand blue active → neutral white-fill + shadow (SettingsSegmented chrome).
  • Plan tabs: Base UI default sliding plate → neutral underline (the hand-written under-bar was dead CSS).

Reviewer notes

  • 8 commits: slice 1 primitive+plan, unify ui.tsx, slice 2 catalog, slice 3 skill, slice 4 daily-review, slice 5 close-out, review P3-2/4 (drop redundant maka-tab + merge skill double return), review P3 (tighten TabsPanel locks to per-value). Each independently builds/typechecks/tests; squash-merge recommended.
  • The ui.tsx two-sets-of-tabs unification is the structural fix that makes slice 1's variant="underline" actually typecheck (plan imported TabsList from ui.js, which was the hand-rolled set without variant). It's the refactor(ui): React architecture debt cleanup (post #430 converge) #477 duplicate-component debt P0-3 had to collapse.
  • Biggest a11y change: skill (segmented switcher → real tabs). Biggest visual change: catalog + daily-review range (brand → neutral).
  • Review round 2: dropped the redundant maka-tab class on tab triggers (primitive already emits it); merged the skills-panel empty/non-empty duplicate return; tightened the TabsPanel contract locks from /TabsPanel/ (matched imports) to per-value (TabsPanel value="tasks", value="oauth" + value={cat} + ['domestic','overseas','local'], value="market|builtin|installed"). See Verification for the visual verification record.

…slice 1)
Introduce the shared tab spec: maka-tab class + underline/pill variants
on the Base UI Tabs primitive, active/hover repointed to
--state-selected-bg / --state-hover-bg (no brand token, no per-surface
hand-written tab CSS). underline keeps the Base UI indicator recolored
to neutral --foreground (was brand --control); pill hides the indicator
and fills the active tab with --state-selected-bg + bold.
Migrate plan tabs as the first consumer: TabsList passes variant=
"underline", TabsTrigger carries maka-tab (layout stays on maka-plan-tab).
Removes the dead .maka-plan-tab[data-state="active"] under-bar (Base UI
sets data-active, not data-state, so it never matched — plan's active
visual was actually the Base UI default sliding plate, not the
hand-written under-bar).
Tab spec contract test (tab-spec-499-contract.test.ts) locks: primitive
exposes maka-tab + underline|pill; .maka-tab uses neutral state tokens;
plan consumes maka-tab + underline with no hand-written active CSS.
ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no
variant, dead data-[selected] active selectors — Base UI sets data-active)
that plan-reminder-panel consumed, bypassing the tab spec primitive. Re-export
TabsRoot/TabsList/TabsTrigger/TabsPanel from primitives/tabs so every tab
surface gets the maka-tab class + underline/pill variants + the correct
data-active attribute from one primitive. Removes the BaseTabs import.
This is the #477 duplicate-component debt that #499 P0-3 tab governance has
to collapse: one tab primitive, not two.
…3 slice 2)
ProvidersPanel catalog tabs move to the shared spec: TabsList passes
variant="pill", TabsTrigger carries maka-tab (catalogTab hand-written class
dropped), content renders through PrimitiveTabsPanel (one per category:
oauth -> ModelOAuthSection, domestic/overseas/local -> provider grid filtered
by category) instead of a bare conditional render. The hand-written
data-active={catalogTab === tab.id} boolean is removed (Base UI sets
data-active); data-catalog-tab={tab.id} stays (locked by model-oauth contract
as the tab identifier).
CSS cleanup: drop .catalogPillTabs button base/hover/active and the
[data-slot=tab-indicator] display:none (models.css), and .catalogTab:hover /
[data-active] (provider-editor.css). The pill base shape (radius/border/
bg/padding) moves to .maka-tab[data-variant=pill] in maka-tokens.css; active
repoints from brand --nav-active to neutral --state-selected-bg + a neutral
border. .catalogTabs/.catalogPillTabs strong stay as the label font hook.
model-oauth-section-contract.test.ts: update the catalog tab assertions from
the old catalogTab/catalogTabs catalogPillTabs class lock to variant=pill +
maka-tab + TabsPanel (intent unchanged — use the primitive, keep
data-catalog-tab, no manual keyboard handler). tab-spec-499-contract.test.ts
adds the slice 2 contract: catalog consumes maka-tab + pill + TabsPanel, no
hand-written catalogTab/catalogPillTabs active/hover/indicator CSS.
…nderline + TabsPanel (#499 P0-3 slice 3)
skills-panel tabs move from a hand-rolled segmented switcher (UiButton +
aria-pressed + data-state, with a comment explicitly disowning the ARIA tabs
contract) to the shared Base UI Tabs primitive: TabsList passes
variant="underline", TabsTrigger carries maka-tab, and content renders through
TabsPanel (one per market/builtin/installed). This upgrades the surface from
a segmented switcher to real tabs with the roving-tabindex + arrow-key
contract + linked tabpanels.
The dispersed conditional render ({activeSkillTab === 'market' ? market :
skillList(...)} + {activeSkillTab !== 'market' && templates}) concentrates into
three TabsPanels. skillList gains a label param (replacing the internal
activeSkillTab === 'installed' ? ... : ... label switch) so each panel renders
its own label without depending on the active tab state — safe under the
default keepMounted=false (only the active panel mounts) and robust if
keepMounted is ever flipped. The empty-title/empty-body strings hoist to
component scope (skillListEmptyTitle/skillListEmptyBody) so both the empty-skills
and populated returns share them.
The tabs-bar keeps its right-side filter actions (static labels, market-only)
as a sibling of TabsList inside TabsRoot — they are not a tab/panel.
CSS: drop .maka-skill-tab[data-state="active"] and its ::after under-bar
(skills.css). The active state + under-bar move to .maka-tab[data-variant=
underline] via the Base UI indicator (neutral --foreground).
skills.test.ts: update the skillList section assertion from the literal
aria-label="已安装技能" to the dynamic aria-label={label} (intent unchanged —
the section still carries an aria-label, now driven by the label param).
tab-spec-499-contract.test.ts adds the slice 3 contract: skill uses maka-tab +
underline + TabsPanel, no hand-written aria-pressed/data-state switcher, no
hand-written .maka-skill-tab[data-state="active"] / ::after CSS.
…tive (#499 P0-3 slice 4)
daily-review range (今日/本周/本月) switches a time-window parameter — the
report re-fetches for the chosen range, it is not three distinct views — so it
is a segmented control, not tabs. Move it from a hand-rolled segmented switcher
(UiButton + aria-pressed + data-active) to the project's SettingsSegmented
primitive (Base UI ToggleGroup, single-select, roving tabindex + arrow keys +
data-pressed). This is the a11y-correct primitive for a parameter switcher
(radiogroup/pressed semantics), unlike Tabs/TabsPanel which expect linked
tabpanel views — the other three surfaces (plan/catalog/skill) are view
switches and stay on Tabs.
Active repoints from brand --nav-active (0.12 bg + color) to the neutral
.settingsSegmented button[data-pressed] chrome (white fill + shadow), inherited
via the primitive's global CSS. The hand-written .maka-daily-review-range-tab
base + [data-active="true"] brand block (daily-review.css) and the
PR-DAILY-REVIEW-RANGE-0 brand-accent comment are removed; .maka-daily-review-
range-tabs keeps its flex/gap layout (now the SettingsSegmented className).
daily-review-copy-feedback-contract.test.ts: update the range-tab assertion
from the hand-rolled UiButton maka-daily-review-range-tab to SettingsSegmented +
maka-daily-review-range-tabs, and flip the brand-active CSS assertion from
assert.match to assert.doesNotMatch (the brand active CSS is gone). Intent
unchanged — Daily Review controls use shared primitives, no legacy classes.
tab-spec-499-contract.test.ts adds the slice 4 contract: range uses
SettingsSegmented, no hand-written aria-pressed/data-active switcher, no
.maka-daily-review-range-tab[data-active] brand CSS.
…13, contract updates (#499 P0-3 slice 5)
Tab spec close-out:
- state-token-governance-499-contract.test.ts: drop the 5 tab selectors
(.maka-daily-review-range-tab / .catalogTab / .catalogPillTabs / .maka-skill-tab
/ .maka-plan-tab) from the selected/active brand-token allowlist. All four
tab surfaces migrated in slices 1-4 now use neutral state tokens, so the
"pending tab-spec" carve-out is gone; only the onboarding brand-emphasis
allowlist (.maka-firstrun-step / .maka-onboarding-setup-steps) remains.
- design-system.md §3.13 Tabs: document the spec — one primitive
(primitives/tabs.tsx, re-exported from ui.tsx), two variants (underline =
neutral Base UI indicator; pill = --state-selected-bg fill + bold), no
dedicated tab color token (reuse --state-hover-bg / --state-selected-bg),
and the segmented-control rule (parameter switches like daily-review range
use SettingsSegmented / Base UI ToggleGroup, not Tabs).
- radius-converge-contract.test.ts: remove the stale TabsTrigger/TabsList
entries (they used to be declared in ui.tsx with --radius-* tokens; #499 P0-3
re-exports them from primitives/tabs.tsx, which uses Tailwind rounded-md/
rounded-sm governed by primitives-design-contract escape hatches, not the
radius-token convergence contract).
- design-system-governance-406-contract.test.ts: the tab underline indicator
moves from brand --control to neutral --foreground (structural chrome stays
calm, brand is garnish — the principle already documented in
plan-reminders.css). Update the assertion from bg-control to bg-foreground
and add a ban on bg-control for the under-bar (checkbox/progress/menu keep
bg-control — those are checked controls, not structural chrome).
- claude-subscription-experimental-gate.test.ts: the OAuth catalog content
moved from a bare conditional render (catalogTab === 'oauth' ?
<ModelOAuthSection/>) into a TabsPanel, matching the model-oauth-section
contract update in slice 2. Intent unchanged — the OAuth tab renders the
real login cards.
Verified: npm run -w @maka/desktop test 1878 pass / 0 fail; typecheck clean.
@Astro-Han
Astro-Hanforce-pushed the feat/499-tab-component-spec branch from 3bfaf67 to d08300aCompareJuly 4, 2026 16:01
…l return (#499 P0-3 review P3-2/4)
Address review P3-2 + P3-4 on PR #517 (tab component spec).
P3-2 — redundant maka-tab class on tab triggers. The TabsTab primitive
already emits `maka-tab` (tabs.tsx), so callers passing
`className="maka-tab ..."` duplicate it. Drop `maka-tab` from the three
call sites, keeping local layout classes:
- skills-panel.tsx: "maka-tab maka-skill-tab" → "maka-skill-tab"
- plan-reminder-panel.tsx: "maka-tab maka-plan-tab" → "maka-plan-tab" (×2)
- ProvidersPanel.tsx: drop the bare "maka-tab" (catalog has no local class)
Sync the contract tests that locked "caller must pass maka-tab" — that
constraint is redundant now the primitive guarantees the class:
- tab-spec-499-contract.test.ts: drop the three per-surface
assert.match(TabsTrigger className maka-tab). Keep variant, TabsPanel,
and the doesNotMatch anti-regression locks.
- model-oauth-section-contract.test.ts: catalog tablist contract keeps
PrimitiveTabsTrigger + value={tab.id}, drops className="maka-tab"
(maka-tab comes from the primitive).
P3-4 — skills-panel double return. The empty-skills and non-empty
returns duplicated the entire TabsRoot/tabs/TabsPanel tree, differing
only by `skillList([] …)` vs `skillList(filteredSkills …)` and a
trailing tool-summary span. Merge to one return: filteredSkills is `[]`
when props.skills is empty (it's `(props.skills ?? []).filter(…)`), so
`skillList(filteredSkills, …)` is equivalent in both states; the summary
span becomes conditional. Cuts ~15 lines and removes a future drift
hazard (changing the tabs tree used to require editing two returns).
Not done (P3-3 / P3-5): the rest of tab-spec-499-contract is not
redundant lock-on-implementation. `variant="underline|pill"` locks each
surface's form spec (a result). `TabsPanel` is the only a11y guard
(tabpanel role + roving tabindex) until the project has render-test
infrastructure. The doesNotMatch locks prevent hand-written tab
CSS/switcher regression. Only the className maka-tab lock was true
redundant lock-on-implementation; the rest stays.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
…view P3)
Address review round 2 P3 (PR #517): the `assert.match(panel, /TabsPanel/)`
locks were too wide — they matched the import statement
(`TabsPanel } from './ui.js'`) and any TabsPanel, not a panel for each tab
value.
Tighten to per-value locks so each tab value has a corresponding panel
(tabpanel a11y pairing), and deleting any panel fails the test:
- plan: TabsPanel value="tasks" + value="runs" (new — plan had no TabsPanel
lock before)
- catalog: PrimitiveTabsPanel value="oauth" (fixed OAuth panel) +
value={cat} (mapped category panel template) + ['domestic','overseas','local']
(the mapped array — proves every category has a panel without a runtime
render test)
- skill: TabsPanel value="market" + value="builtin" + value="installed"
Unrelated TabsPanel occurrences (e.g. import lines) no longer satisfy the
lock: deleting any target surface panel fails the test, the import line
alone does not pass it.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) by Astro-Han · Pull Request #517 · apache/maka · GitHub
Skip to content

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) - #517

Merged
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec
Jul 4, 2026
Merged

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3)#517
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Tab component governance for #499 P0-3: one tab spec primitive (maka-tab class + underline/pill variants on Base UI Tabs), migrate the 4 hand-written tab surfaces, retire per-surface hand-written tab CSS, and collapse the duplicate tab sets (#477 debt). Tab active/hover repoint to the neutral --state-* tokens from #503 — no dedicated tab color token.

Why

#499 P0-3 — tabs had no spec; 4 hand-written implementations drifted: plan/skill underline with dead [data-state] selectors (Base UI sets data-active), catalog brand pill hiding the Base UI indicator, daily-review range hand-rolled <button aria-pressed>. Plus packages/ui/src/ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no variant, dead data-[selected]) that plan-reminder-panel consumed, bypassing the existing primitives/tabs.tsx. Direction: structural chrome stays neutral (brand is garnish), one primitive, two intentional variants — and a segmented control for parameter switches (not Tabs).

Refs #499

Scope

Changed:

  • primitives/tabs.tsx: TabsVariant adds "pill"; TabsList sets data-variant + maka-tabs-list; pill hides the Base UI indicator; underline indicator recolored from brand bg-control to neutral bg-foreground. TabsTab emits the shared maka-tab class.
  • ui.tsx: TabsRoot/TabsList/TabsTrigger/TabsPanel re-export from primitives/tabs.tsx (drops the second hand-rolled set + dead data-[selected] active selectors). One tab primitive, not two (refactor(ui): React architecture debt cleanup (post #430 converge) #477 debt).
  • maka-tokens.css: .maka-tab spec — pill base shape + --state-selected-bg active + --state-hover-bg hover + neutral border; underline active = --foreground text. Reuses refactor(ui): retire --hover/--active for state tokens, unify selected neutral, drop press scale #503 state tokens; no dedicated tab color token.
  • plan-reminder-panel.tsx: variant="underline" + maka-tab; drops the dead .maka-plan-tab[data-state="active"]::after under-bar (Base UI sets data-active, not data-state — plan's active visual was actually the Base UI default sliding plate, not the hand-written under-bar).
  • ProvidersPanel.tsx: catalog → variant="pill" + maka-tab + TabsPanel (oauth → ModelOAuthSection; domestic/overseas/local → filtered provider grid). Drops data-active={catalogTab === tab.id} (Base UI sets it) and the catalogTab hand-written class; keeps data-catalog-tab. Content moves from bare conditional render into panels.
  • skills-panel.tsx: hand-rolled segmented switcher (UiButton + aria-pressed + data-state) → Tabs/TabsList/TabsTrigger/TabsPanel (market/builtin/installed each a panel). a11y upgrade: roving tabindex + arrow-key contract + linked tabpanels. skillList gains a label param (no longer depends on activeSkillTab — safe under default keepMounted=false).
  • daily-review-panel.tsx: range (今日/本周/本月) → SettingsSegmented (Base UI ToggleGroup). It switches a time-window parameter, not views — segmented control is the a11y-correct primitive, not Tabs/TabsPanel. Active inherits the neutral .settingsSegmented button[data-pressed] chrome.
  • CSS cleanup: drop .catalogPillTabs button base/hover/active + [data-slot=tab-indicator] display:none (models.css), .catalogTab:hover/[data-active] (provider-editor.css), .maka-skill-tab[data-state=active] + ::after (skills.css), .maka-daily-review-range-tab[data-active] brand (daily-review.css), .maka-plan-tab[data-state=active] + ::after (plan-reminders.css).
  • state-token-governance-499-contract.test.ts: drop the 5 tab selectors from the selected/active brand-token allowlist (tabs no longer need the carve-out). Onboarding brand-emphasis allowlist stays.
  • design-system.md §3.13 Tabs: documents the spec (primitive, variants, no dedicated color token, segmented-control rule).
  • Contract tests updated: tab-spec-499-contract.test.ts (new, per-surface), model-oauth-section-contract + claude-subscription-experimental-gate (catalog conditional render → TabsPanel), skills.test (skillList aria-label dynamic), daily-review-copy-feedback-contract (range → SettingsSegmented), radius-converge-contract (drop stale TabsTrigger/TabsList ui.tsx entries), design-system-governance-406-contract (under-bar bg-controlbg-foreground).

Not included:

Verification

  • npm run -w @maka/desktop test: 1888 pass / 0 fail (rebased onto main 85c81d9d).
  • npm run typecheck: clean (main + renderer + storybook).
  • Visual: captured the 4 tab/segmented surfaces × {light, dark} = 8 default-state screenshots with the in-repo fixture harness and human-verified them. Command (from repo root, after npm run build):
    node scripts/capture-screenshots.mjs --scenario <scenario> --variant <light-1280-motion|dark-1280-motion>
    
    Scenarios: provider-workspace (catalog pill), module-skills (skill tabs), module-daily-review (daily-review range), plan-reminders (plan underline). Output: apps/desktop/tests/screenshots/<scenario>/<variant>.png. Conclusion: catalog pill active = neutral wash, skill/plan underline = neutral bar, daily-review range = neutral SettingsSegmented chrome — correct in both light and dark.
  • Token / a11y (objective, not pixel-based): tab-spec-499-contract verifies .maka-tab active/hover use --state-selected-bg / --state-hover-bg (neutral) with no brand token; design-system-governance-406 verifies the under-bar is bg-foreground; keyboard a11y (roving tabindex + arrow keys + tabpanel pairing) is guaranteed by Base UI Tabs (plan/catalog/skill) and Base UI ToggleGroup (daily-review range).
  • Harness limit: capture-screenshots.mjs variants are theme × viewport × motion; it does not capture hover/active/keyboard state variants. Hover/active state correctness is covered by the token/static contract tests above (not by screenshots); keyboard behavior is covered by the Base UI primitive contract. A CDP-driven state-variant harness is a separate piece of work.
  • Pre-existing note (not introduced here): 23 tests import dist/renderer/settings/*.js by path, but build:renderer is vite (chunks, not per-path). A fresh worktree lacks these by-path files; this PR was verified with tsc -p tsconfig.renderer.json --outDir dist/renderer to emit per-path dist. This is a test/build setup mismatch — CI may need the same workaround, or the test script should build renderer per-path.

User-facing impact

  • Catalog tabs (供应商分类) active: brand blue → neutral wash + bold.
  • Skill tabs: same neutral underline visual, but now real tabs (arrow-key nav + tabpanel) instead of a segmented switcher.
  • Daily-review range: brand blue active → neutral white-fill + shadow (SettingsSegmented chrome).
  • Plan tabs: Base UI default sliding plate → neutral underline (the hand-written under-bar was dead CSS).

Reviewer notes

  • 8 commits: slice 1 primitive+plan, unify ui.tsx, slice 2 catalog, slice 3 skill, slice 4 daily-review, slice 5 close-out, review P3-2/4 (drop redundant maka-tab + merge skill double return), review P3 (tighten TabsPanel locks to per-value). Each independently builds/typechecks/tests; squash-merge recommended.
  • The ui.tsx two-sets-of-tabs unification is the structural fix that makes slice 1's variant="underline" actually typecheck (plan imported TabsList from ui.js, which was the hand-rolled set without variant). It's the refactor(ui): React architecture debt cleanup (post #430 converge) #477 duplicate-component debt P0-3 had to collapse.
  • Biggest a11y change: skill (segmented switcher → real tabs). Biggest visual change: catalog + daily-review range (brand → neutral).
  • Review round 2: dropped the redundant maka-tab class on tab triggers (primitive already emits it); merged the skills-panel empty/non-empty duplicate return; tightened the TabsPanel contract locks from /TabsPanel/ (matched imports) to per-value (TabsPanel value="tasks", value="oauth" + value={cat} + ['domestic','overseas','local'], value="market|builtin|installed"). See Verification for the visual verification record.

…slice 1)
Introduce the shared tab spec: maka-tab class + underline/pill variants
on the Base UI Tabs primitive, active/hover repointed to
--state-selected-bg / --state-hover-bg (no brand token, no per-surface
hand-written tab CSS). underline keeps the Base UI indicator recolored
to neutral --foreground (was brand --control); pill hides the indicator
and fills the active tab with --state-selected-bg + bold.
Migrate plan tabs as the first consumer: TabsList passes variant=
"underline", TabsTrigger carries maka-tab (layout stays on maka-plan-tab).
Removes the dead .maka-plan-tab[data-state="active"] under-bar (Base UI
sets data-active, not data-state, so it never matched — plan's active
visual was actually the Base UI default sliding plate, not the
hand-written under-bar).
Tab spec contract test (tab-spec-499-contract.test.ts) locks: primitive
exposes maka-tab + underline|pill; .maka-tab uses neutral state tokens;
plan consumes maka-tab + underline with no hand-written active CSS.
ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no
variant, dead data-[selected] active selectors — Base UI sets data-active)
that plan-reminder-panel consumed, bypassing the tab spec primitive. Re-export
TabsRoot/TabsList/TabsTrigger/TabsPanel from primitives/tabs so every tab
surface gets the maka-tab class + underline/pill variants + the correct
data-active attribute from one primitive. Removes the BaseTabs import.
This is the #477 duplicate-component debt that #499 P0-3 tab governance has
to collapse: one tab primitive, not two.
…3 slice 2)
ProvidersPanel catalog tabs move to the shared spec: TabsList passes
variant="pill", TabsTrigger carries maka-tab (catalogTab hand-written class
dropped), content renders through PrimitiveTabsPanel (one per category:
oauth -> ModelOAuthSection, domestic/overseas/local -> provider grid filtered
by category) instead of a bare conditional render. The hand-written
data-active={catalogTab === tab.id} boolean is removed (Base UI sets
data-active); data-catalog-tab={tab.id} stays (locked by model-oauth contract
as the tab identifier).
CSS cleanup: drop .catalogPillTabs button base/hover/active and the
[data-slot=tab-indicator] display:none (models.css), and .catalogTab:hover /
[data-active] (provider-editor.css). The pill base shape (radius/border/
bg/padding) moves to .maka-tab[data-variant=pill] in maka-tokens.css; active
repoints from brand --nav-active to neutral --state-selected-bg + a neutral
border. .catalogTabs/.catalogPillTabs strong stay as the label font hook.
model-oauth-section-contract.test.ts: update the catalog tab assertions from
the old catalogTab/catalogTabs catalogPillTabs class lock to variant=pill +
maka-tab + TabsPanel (intent unchanged — use the primitive, keep
data-catalog-tab, no manual keyboard handler). tab-spec-499-contract.test.ts
adds the slice 2 contract: catalog consumes maka-tab + pill + TabsPanel, no
hand-written catalogTab/catalogPillTabs active/hover/indicator CSS.
…nderline + TabsPanel (#499 P0-3 slice 3)
skills-panel tabs move from a hand-rolled segmented switcher (UiButton +
aria-pressed + data-state, with a comment explicitly disowning the ARIA tabs
contract) to the shared Base UI Tabs primitive: TabsList passes
variant="underline", TabsTrigger carries maka-tab, and content renders through
TabsPanel (one per market/builtin/installed). This upgrades the surface from
a segmented switcher to real tabs with the roving-tabindex + arrow-key
contract + linked tabpanels.
The dispersed conditional render ({activeSkillTab === 'market' ? market :
skillList(...)} + {activeSkillTab !== 'market' && templates}) concentrates into
three TabsPanels. skillList gains a label param (replacing the internal
activeSkillTab === 'installed' ? ... : ... label switch) so each panel renders
its own label without depending on the active tab state — safe under the
default keepMounted=false (only the active panel mounts) and robust if
keepMounted is ever flipped. The empty-title/empty-body strings hoist to
component scope (skillListEmptyTitle/skillListEmptyBody) so both the empty-skills
and populated returns share them.
The tabs-bar keeps its right-side filter actions (static labels, market-only)
as a sibling of TabsList inside TabsRoot — they are not a tab/panel.
CSS: drop .maka-skill-tab[data-state="active"] and its ::after under-bar
(skills.css). The active state + under-bar move to .maka-tab[data-variant=
underline] via the Base UI indicator (neutral --foreground).
skills.test.ts: update the skillList section assertion from the literal
aria-label="已安装技能" to the dynamic aria-label={label} (intent unchanged —
the section still carries an aria-label, now driven by the label param).
tab-spec-499-contract.test.ts adds the slice 3 contract: skill uses maka-tab +
underline + TabsPanel, no hand-written aria-pressed/data-state switcher, no
hand-written .maka-skill-tab[data-state="active"] / ::after CSS.
…tive (#499 P0-3 slice 4)
daily-review range (今日/本周/本月) switches a time-window parameter — the
report re-fetches for the chosen range, it is not three distinct views — so it
is a segmented control, not tabs. Move it from a hand-rolled segmented switcher
(UiButton + aria-pressed + data-active) to the project's SettingsSegmented
primitive (Base UI ToggleGroup, single-select, roving tabindex + arrow keys +
data-pressed). This is the a11y-correct primitive for a parameter switcher
(radiogroup/pressed semantics), unlike Tabs/TabsPanel which expect linked
tabpanel views — the other three surfaces (plan/catalog/skill) are view
switches and stay on Tabs.
Active repoints from brand --nav-active (0.12 bg + color) to the neutral
.settingsSegmented button[data-pressed] chrome (white fill + shadow), inherited
via the primitive's global CSS. The hand-written .maka-daily-review-range-tab
base + [data-active="true"] brand block (daily-review.css) and the
PR-DAILY-REVIEW-RANGE-0 brand-accent comment are removed; .maka-daily-review-
range-tabs keeps its flex/gap layout (now the SettingsSegmented className).
daily-review-copy-feedback-contract.test.ts: update the range-tab assertion
from the hand-rolled UiButton maka-daily-review-range-tab to SettingsSegmented +
maka-daily-review-range-tabs, and flip the brand-active CSS assertion from
assert.match to assert.doesNotMatch (the brand active CSS is gone). Intent
unchanged — Daily Review controls use shared primitives, no legacy classes.
tab-spec-499-contract.test.ts adds the slice 4 contract: range uses
SettingsSegmented, no hand-written aria-pressed/data-active switcher, no
.maka-daily-review-range-tab[data-active] brand CSS.
…13, contract updates (#499 P0-3 slice 5)
Tab spec close-out:
- state-token-governance-499-contract.test.ts: drop the 5 tab selectors
(.maka-daily-review-range-tab / .catalogTab / .catalogPillTabs / .maka-skill-tab
/ .maka-plan-tab) from the selected/active brand-token allowlist. All four
tab surfaces migrated in slices 1-4 now use neutral state tokens, so the
"pending tab-spec" carve-out is gone; only the onboarding brand-emphasis
allowlist (.maka-firstrun-step / .maka-onboarding-setup-steps) remains.
- design-system.md §3.13 Tabs: document the spec — one primitive
(primitives/tabs.tsx, re-exported from ui.tsx), two variants (underline =
neutral Base UI indicator; pill = --state-selected-bg fill + bold), no
dedicated tab color token (reuse --state-hover-bg / --state-selected-bg),
and the segmented-control rule (parameter switches like daily-review range
use SettingsSegmented / Base UI ToggleGroup, not Tabs).
- radius-converge-contract.test.ts: remove the stale TabsTrigger/TabsList
entries (they used to be declared in ui.tsx with --radius-* tokens; #499 P0-3
re-exports them from primitives/tabs.tsx, which uses Tailwind rounded-md/
rounded-sm governed by primitives-design-contract escape hatches, not the
radius-token convergence contract).
- design-system-governance-406-contract.test.ts: the tab underline indicator
moves from brand --control to neutral --foreground (structural chrome stays
calm, brand is garnish — the principle already documented in
plan-reminders.css). Update the assertion from bg-control to bg-foreground
and add a ban on bg-control for the under-bar (checkbox/progress/menu keep
bg-control — those are checked controls, not structural chrome).
- claude-subscription-experimental-gate.test.ts: the OAuth catalog content
moved from a bare conditional render (catalogTab === 'oauth' ?
<ModelOAuthSection/>) into a TabsPanel, matching the model-oauth-section
contract update in slice 2. Intent unchanged — the OAuth tab renders the
real login cards.
Verified: npm run -w @maka/desktop test 1878 pass / 0 fail; typecheck clean.
@Astro-Han
Astro-Hanforce-pushed the feat/499-tab-component-spec branch from 3bfaf67 to d08300aCompareJuly 4, 2026 16:01
…l return (#499 P0-3 review P3-2/4)
Address review P3-2 + P3-4 on PR #517 (tab component spec).
P3-2 — redundant maka-tab class on tab triggers. The TabsTab primitive
already emits `maka-tab` (tabs.tsx), so callers passing
`className="maka-tab ..."` duplicate it. Drop `maka-tab` from the three
call sites, keeping local layout classes:
- skills-panel.tsx: "maka-tab maka-skill-tab" → "maka-skill-tab"
- plan-reminder-panel.tsx: "maka-tab maka-plan-tab" → "maka-plan-tab" (×2)
- ProvidersPanel.tsx: drop the bare "maka-tab" (catalog has no local class)
Sync the contract tests that locked "caller must pass maka-tab" — that
constraint is redundant now the primitive guarantees the class:
- tab-spec-499-contract.test.ts: drop the three per-surface
assert.match(TabsTrigger className maka-tab). Keep variant, TabsPanel,
and the doesNotMatch anti-regression locks.
- model-oauth-section-contract.test.ts: catalog tablist contract keeps
PrimitiveTabsTrigger + value={tab.id}, drops className="maka-tab"
(maka-tab comes from the primitive).
P3-4 — skills-panel double return. The empty-skills and non-empty
returns duplicated the entire TabsRoot/tabs/TabsPanel tree, differing
only by `skillList([] …)` vs `skillList(filteredSkills …)` and a
trailing tool-summary span. Merge to one return: filteredSkills is `[]`
when props.skills is empty (it's `(props.skills ?? []).filter(…)`), so
`skillList(filteredSkills, …)` is equivalent in both states; the summary
span becomes conditional. Cuts ~15 lines and removes a future drift
hazard (changing the tabs tree used to require editing two returns).
Not done (P3-3 / P3-5): the rest of tab-spec-499-contract is not
redundant lock-on-implementation. `variant="underline|pill"` locks each
surface's form spec (a result). `TabsPanel` is the only a11y guard
(tabpanel role + roving tabindex) until the project has render-test
infrastructure. The doesNotMatch locks prevent hand-written tab
CSS/switcher regression. Only the className maka-tab lock was true
redundant lock-on-implementation; the rest stays.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
…view P3)
Address review round 2 P3 (PR #517): the `assert.match(panel, /TabsPanel/)`
locks were too wide — they matched the import statement
(`TabsPanel } from './ui.js'`) and any TabsPanel, not a panel for each tab
value.
Tighten to per-value locks so each tab value has a corresponding panel
(tabpanel a11y pairing), and deleting any panel fails the test:
- plan: TabsPanel value="tasks" + value="runs" (new — plan had no TabsPanel
lock before)
- catalog: PrimitiveTabsPanel value="oauth" (fixed OAuth panel) +
value={cat} (mapped category panel template) + ['domestic','overseas','local']
(the mapped array — proves every category has a panel without a runtime
render test)
- skill: TabsPanel value="market" + value="builtin" + value="installed"
Unrelated TabsPanel occurrences (e.g. import lines) no longer satisfy the
lock: deleting any target surface panel fails the test, the import line
alone does not pass it.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) by Astro-Han · Pull Request #517 · apache/maka · GitHub
Skip to content

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) - #517

Merged
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec
Jul 4, 2026
Merged

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3)#517
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Tab component governance for #499 P0-3: one tab spec primitive (maka-tab class + underline/pill variants on Base UI Tabs), migrate the 4 hand-written tab surfaces, retire per-surface hand-written tab CSS, and collapse the duplicate tab sets (#477 debt). Tab active/hover repoint to the neutral --state-* tokens from #503 — no dedicated tab color token.

Why

#499 P0-3 — tabs had no spec; 4 hand-written implementations drifted: plan/skill underline with dead [data-state] selectors (Base UI sets data-active), catalog brand pill hiding the Base UI indicator, daily-review range hand-rolled <button aria-pressed>. Plus packages/ui/src/ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no variant, dead data-[selected]) that plan-reminder-panel consumed, bypassing the existing primitives/tabs.tsx. Direction: structural chrome stays neutral (brand is garnish), one primitive, two intentional variants — and a segmented control for parameter switches (not Tabs).

Refs #499

Scope

Changed:

  • primitives/tabs.tsx: TabsVariant adds "pill"; TabsList sets data-variant + maka-tabs-list; pill hides the Base UI indicator; underline indicator recolored from brand bg-control to neutral bg-foreground. TabsTab emits the shared maka-tab class.
  • ui.tsx: TabsRoot/TabsList/TabsTrigger/TabsPanel re-export from primitives/tabs.tsx (drops the second hand-rolled set + dead data-[selected] active selectors). One tab primitive, not two (refactor(ui): React architecture debt cleanup (post #430 converge) #477 debt).
  • maka-tokens.css: .maka-tab spec — pill base shape + --state-selected-bg active + --state-hover-bg hover + neutral border; underline active = --foreground text. Reuses refactor(ui): retire --hover/--active for state tokens, unify selected neutral, drop press scale #503 state tokens; no dedicated tab color token.
  • plan-reminder-panel.tsx: variant="underline" + maka-tab; drops the dead .maka-plan-tab[data-state="active"]::after under-bar (Base UI sets data-active, not data-state — plan's active visual was actually the Base UI default sliding plate, not the hand-written under-bar).
  • ProvidersPanel.tsx: catalog → variant="pill" + maka-tab + TabsPanel (oauth → ModelOAuthSection; domestic/overseas/local → filtered provider grid). Drops data-active={catalogTab === tab.id} (Base UI sets it) and the catalogTab hand-written class; keeps data-catalog-tab. Content moves from bare conditional render into panels.
  • skills-panel.tsx: hand-rolled segmented switcher (UiButton + aria-pressed + data-state) → Tabs/TabsList/TabsTrigger/TabsPanel (market/builtin/installed each a panel). a11y upgrade: roving tabindex + arrow-key contract + linked tabpanels. skillList gains a label param (no longer depends on activeSkillTab — safe under default keepMounted=false).
  • daily-review-panel.tsx: range (今日/本周/本月) → SettingsSegmented (Base UI ToggleGroup). It switches a time-window parameter, not views — segmented control is the a11y-correct primitive, not Tabs/TabsPanel. Active inherits the neutral .settingsSegmented button[data-pressed] chrome.
  • CSS cleanup: drop .catalogPillTabs button base/hover/active + [data-slot=tab-indicator] display:none (models.css), .catalogTab:hover/[data-active] (provider-editor.css), .maka-skill-tab[data-state=active] + ::after (skills.css), .maka-daily-review-range-tab[data-active] brand (daily-review.css), .maka-plan-tab[data-state=active] + ::after (plan-reminders.css).
  • state-token-governance-499-contract.test.ts: drop the 5 tab selectors from the selected/active brand-token allowlist (tabs no longer need the carve-out). Onboarding brand-emphasis allowlist stays.
  • design-system.md §3.13 Tabs: documents the spec (primitive, variants, no dedicated color token, segmented-control rule).
  • Contract tests updated: tab-spec-499-contract.test.ts (new, per-surface), model-oauth-section-contract + claude-subscription-experimental-gate (catalog conditional render → TabsPanel), skills.test (skillList aria-label dynamic), daily-review-copy-feedback-contract (range → SettingsSegmented), radius-converge-contract (drop stale TabsTrigger/TabsList ui.tsx entries), design-system-governance-406-contract (under-bar bg-controlbg-foreground).

Not included:

Verification

  • npm run -w @maka/desktop test: 1888 pass / 0 fail (rebased onto main 85c81d9d).
  • npm run typecheck: clean (main + renderer + storybook).
  • Visual: captured the 4 tab/segmented surfaces × {light, dark} = 8 default-state screenshots with the in-repo fixture harness and human-verified them. Command (from repo root, after npm run build):
    node scripts/capture-screenshots.mjs --scenario <scenario> --variant <light-1280-motion|dark-1280-motion>
    
    Scenarios: provider-workspace (catalog pill), module-skills (skill tabs), module-daily-review (daily-review range), plan-reminders (plan underline). Output: apps/desktop/tests/screenshots/<scenario>/<variant>.png. Conclusion: catalog pill active = neutral wash, skill/plan underline = neutral bar, daily-review range = neutral SettingsSegmented chrome — correct in both light and dark.
  • Token / a11y (objective, not pixel-based): tab-spec-499-contract verifies .maka-tab active/hover use --state-selected-bg / --state-hover-bg (neutral) with no brand token; design-system-governance-406 verifies the under-bar is bg-foreground; keyboard a11y (roving tabindex + arrow keys + tabpanel pairing) is guaranteed by Base UI Tabs (plan/catalog/skill) and Base UI ToggleGroup (daily-review range).
  • Harness limit: capture-screenshots.mjs variants are theme × viewport × motion; it does not capture hover/active/keyboard state variants. Hover/active state correctness is covered by the token/static contract tests above (not by screenshots); keyboard behavior is covered by the Base UI primitive contract. A CDP-driven state-variant harness is a separate piece of work.
  • Pre-existing note (not introduced here): 23 tests import dist/renderer/settings/*.js by path, but build:renderer is vite (chunks, not per-path). A fresh worktree lacks these by-path files; this PR was verified with tsc -p tsconfig.renderer.json --outDir dist/renderer to emit per-path dist. This is a test/build setup mismatch — CI may need the same workaround, or the test script should build renderer per-path.

User-facing impact

  • Catalog tabs (供应商分类) active: brand blue → neutral wash + bold.
  • Skill tabs: same neutral underline visual, but now real tabs (arrow-key nav + tabpanel) instead of a segmented switcher.
  • Daily-review range: brand blue active → neutral white-fill + shadow (SettingsSegmented chrome).
  • Plan tabs: Base UI default sliding plate → neutral underline (the hand-written under-bar was dead CSS).

Reviewer notes

  • 8 commits: slice 1 primitive+plan, unify ui.tsx, slice 2 catalog, slice 3 skill, slice 4 daily-review, slice 5 close-out, review P3-2/4 (drop redundant maka-tab + merge skill double return), review P3 (tighten TabsPanel locks to per-value). Each independently builds/typechecks/tests; squash-merge recommended.
  • The ui.tsx two-sets-of-tabs unification is the structural fix that makes slice 1's variant="underline" actually typecheck (plan imported TabsList from ui.js, which was the hand-rolled set without variant). It's the refactor(ui): React architecture debt cleanup (post #430 converge) #477 duplicate-component debt P0-3 had to collapse.
  • Biggest a11y change: skill (segmented switcher → real tabs). Biggest visual change: catalog + daily-review range (brand → neutral).
  • Review round 2: dropped the redundant maka-tab class on tab triggers (primitive already emits it); merged the skills-panel empty/non-empty duplicate return; tightened the TabsPanel contract locks from /TabsPanel/ (matched imports) to per-value (TabsPanel value="tasks", value="oauth" + value={cat} + ['domestic','overseas','local'], value="market|builtin|installed"). See Verification for the visual verification record.

…slice 1)
Introduce the shared tab spec: maka-tab class + underline/pill variants
on the Base UI Tabs primitive, active/hover repointed to
--state-selected-bg / --state-hover-bg (no brand token, no per-surface
hand-written tab CSS). underline keeps the Base UI indicator recolored
to neutral --foreground (was brand --control); pill hides the indicator
and fills the active tab with --state-selected-bg + bold.
Migrate plan tabs as the first consumer: TabsList passes variant=
"underline", TabsTrigger carries maka-tab (layout stays on maka-plan-tab).
Removes the dead .maka-plan-tab[data-state="active"] under-bar (Base UI
sets data-active, not data-state, so it never matched — plan's active
visual was actually the Base UI default sliding plate, not the
hand-written under-bar).
Tab spec contract test (tab-spec-499-contract.test.ts) locks: primitive
exposes maka-tab + underline|pill; .maka-tab uses neutral state tokens;
plan consumes maka-tab + underline with no hand-written active CSS.
ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no
variant, dead data-[selected] active selectors — Base UI sets data-active)
that plan-reminder-panel consumed, bypassing the tab spec primitive. Re-export
TabsRoot/TabsList/TabsTrigger/TabsPanel from primitives/tabs so every tab
surface gets the maka-tab class + underline/pill variants + the correct
data-active attribute from one primitive. Removes the BaseTabs import.
This is the #477 duplicate-component debt that #499 P0-3 tab governance has
to collapse: one tab primitive, not two.
…3 slice 2)
ProvidersPanel catalog tabs move to the shared spec: TabsList passes
variant="pill", TabsTrigger carries maka-tab (catalogTab hand-written class
dropped), content renders through PrimitiveTabsPanel (one per category:
oauth -> ModelOAuthSection, domestic/overseas/local -> provider grid filtered
by category) instead of a bare conditional render. The hand-written
data-active={catalogTab === tab.id} boolean is removed (Base UI sets
data-active); data-catalog-tab={tab.id} stays (locked by model-oauth contract
as the tab identifier).
CSS cleanup: drop .catalogPillTabs button base/hover/active and the
[data-slot=tab-indicator] display:none (models.css), and .catalogTab:hover /
[data-active] (provider-editor.css). The pill base shape (radius/border/
bg/padding) moves to .maka-tab[data-variant=pill] in maka-tokens.css; active
repoints from brand --nav-active to neutral --state-selected-bg + a neutral
border. .catalogTabs/.catalogPillTabs strong stay as the label font hook.
model-oauth-section-contract.test.ts: update the catalog tab assertions from
the old catalogTab/catalogTabs catalogPillTabs class lock to variant=pill +
maka-tab + TabsPanel (intent unchanged — use the primitive, keep
data-catalog-tab, no manual keyboard handler). tab-spec-499-contract.test.ts
adds the slice 2 contract: catalog consumes maka-tab + pill + TabsPanel, no
hand-written catalogTab/catalogPillTabs active/hover/indicator CSS.
…nderline + TabsPanel (#499 P0-3 slice 3)
skills-panel tabs move from a hand-rolled segmented switcher (UiButton +
aria-pressed + data-state, with a comment explicitly disowning the ARIA tabs
contract) to the shared Base UI Tabs primitive: TabsList passes
variant="underline", TabsTrigger carries maka-tab, and content renders through
TabsPanel (one per market/builtin/installed). This upgrades the surface from
a segmented switcher to real tabs with the roving-tabindex + arrow-key
contract + linked tabpanels.
The dispersed conditional render ({activeSkillTab === 'market' ? market :
skillList(...)} + {activeSkillTab !== 'market' && templates}) concentrates into
three TabsPanels. skillList gains a label param (replacing the internal
activeSkillTab === 'installed' ? ... : ... label switch) so each panel renders
its own label without depending on the active tab state — safe under the
default keepMounted=false (only the active panel mounts) and robust if
keepMounted is ever flipped. The empty-title/empty-body strings hoist to
component scope (skillListEmptyTitle/skillListEmptyBody) so both the empty-skills
and populated returns share them.
The tabs-bar keeps its right-side filter actions (static labels, market-only)
as a sibling of TabsList inside TabsRoot — they are not a tab/panel.
CSS: drop .maka-skill-tab[data-state="active"] and its ::after under-bar
(skills.css). The active state + under-bar move to .maka-tab[data-variant=
underline] via the Base UI indicator (neutral --foreground).
skills.test.ts: update the skillList section assertion from the literal
aria-label="已安装技能" to the dynamic aria-label={label} (intent unchanged —
the section still carries an aria-label, now driven by the label param).
tab-spec-499-contract.test.ts adds the slice 3 contract: skill uses maka-tab +
underline + TabsPanel, no hand-written aria-pressed/data-state switcher, no
hand-written .maka-skill-tab[data-state="active"] / ::after CSS.
…tive (#499 P0-3 slice 4)
daily-review range (今日/本周/本月) switches a time-window parameter — the
report re-fetches for the chosen range, it is not three distinct views — so it
is a segmented control, not tabs. Move it from a hand-rolled segmented switcher
(UiButton + aria-pressed + data-active) to the project's SettingsSegmented
primitive (Base UI ToggleGroup, single-select, roving tabindex + arrow keys +
data-pressed). This is the a11y-correct primitive for a parameter switcher
(radiogroup/pressed semantics), unlike Tabs/TabsPanel which expect linked
tabpanel views — the other three surfaces (plan/catalog/skill) are view
switches and stay on Tabs.
Active repoints from brand --nav-active (0.12 bg + color) to the neutral
.settingsSegmented button[data-pressed] chrome (white fill + shadow), inherited
via the primitive's global CSS. The hand-written .maka-daily-review-range-tab
base + [data-active="true"] brand block (daily-review.css) and the
PR-DAILY-REVIEW-RANGE-0 brand-accent comment are removed; .maka-daily-review-
range-tabs keeps its flex/gap layout (now the SettingsSegmented className).
daily-review-copy-feedback-contract.test.ts: update the range-tab assertion
from the hand-rolled UiButton maka-daily-review-range-tab to SettingsSegmented +
maka-daily-review-range-tabs, and flip the brand-active CSS assertion from
assert.match to assert.doesNotMatch (the brand active CSS is gone). Intent
unchanged — Daily Review controls use shared primitives, no legacy classes.
tab-spec-499-contract.test.ts adds the slice 4 contract: range uses
SettingsSegmented, no hand-written aria-pressed/data-active switcher, no
.maka-daily-review-range-tab[data-active] brand CSS.
…13, contract updates (#499 P0-3 slice 5)
Tab spec close-out:
- state-token-governance-499-contract.test.ts: drop the 5 tab selectors
(.maka-daily-review-range-tab / .catalogTab / .catalogPillTabs / .maka-skill-tab
/ .maka-plan-tab) from the selected/active brand-token allowlist. All four
tab surfaces migrated in slices 1-4 now use neutral state tokens, so the
"pending tab-spec" carve-out is gone; only the onboarding brand-emphasis
allowlist (.maka-firstrun-step / .maka-onboarding-setup-steps) remains.
- design-system.md §3.13 Tabs: document the spec — one primitive
(primitives/tabs.tsx, re-exported from ui.tsx), two variants (underline =
neutral Base UI indicator; pill = --state-selected-bg fill + bold), no
dedicated tab color token (reuse --state-hover-bg / --state-selected-bg),
and the segmented-control rule (parameter switches like daily-review range
use SettingsSegmented / Base UI ToggleGroup, not Tabs).
- radius-converge-contract.test.ts: remove the stale TabsTrigger/TabsList
entries (they used to be declared in ui.tsx with --radius-* tokens; #499 P0-3
re-exports them from primitives/tabs.tsx, which uses Tailwind rounded-md/
rounded-sm governed by primitives-design-contract escape hatches, not the
radius-token convergence contract).
- design-system-governance-406-contract.test.ts: the tab underline indicator
moves from brand --control to neutral --foreground (structural chrome stays
calm, brand is garnish — the principle already documented in
plan-reminders.css). Update the assertion from bg-control to bg-foreground
and add a ban on bg-control for the under-bar (checkbox/progress/menu keep
bg-control — those are checked controls, not structural chrome).
- claude-subscription-experimental-gate.test.ts: the OAuth catalog content
moved from a bare conditional render (catalogTab === 'oauth' ?
<ModelOAuthSection/>) into a TabsPanel, matching the model-oauth-section
contract update in slice 2. Intent unchanged — the OAuth tab renders the
real login cards.
Verified: npm run -w @maka/desktop test 1878 pass / 0 fail; typecheck clean.
@Astro-Han
Astro-Hanforce-pushed the feat/499-tab-component-spec branch from 3bfaf67 to d08300aCompareJuly 4, 2026 16:01
…l return (#499 P0-3 review P3-2/4)
Address review P3-2 + P3-4 on PR #517 (tab component spec).
P3-2 — redundant maka-tab class on tab triggers. The TabsTab primitive
already emits `maka-tab` (tabs.tsx), so callers passing
`className="maka-tab ..."` duplicate it. Drop `maka-tab` from the three
call sites, keeping local layout classes:
- skills-panel.tsx: "maka-tab maka-skill-tab" → "maka-skill-tab"
- plan-reminder-panel.tsx: "maka-tab maka-plan-tab" → "maka-plan-tab" (×2)
- ProvidersPanel.tsx: drop the bare "maka-tab" (catalog has no local class)
Sync the contract tests that locked "caller must pass maka-tab" — that
constraint is redundant now the primitive guarantees the class:
- tab-spec-499-contract.test.ts: drop the three per-surface
assert.match(TabsTrigger className maka-tab). Keep variant, TabsPanel,
and the doesNotMatch anti-regression locks.
- model-oauth-section-contract.test.ts: catalog tablist contract keeps
PrimitiveTabsTrigger + value={tab.id}, drops className="maka-tab"
(maka-tab comes from the primitive).
P3-4 — skills-panel double return. The empty-skills and non-empty
returns duplicated the entire TabsRoot/tabs/TabsPanel tree, differing
only by `skillList([] …)` vs `skillList(filteredSkills …)` and a
trailing tool-summary span. Merge to one return: filteredSkills is `[]`
when props.skills is empty (it's `(props.skills ?? []).filter(…)`), so
`skillList(filteredSkills, …)` is equivalent in both states; the summary
span becomes conditional. Cuts ~15 lines and removes a future drift
hazard (changing the tabs tree used to require editing two returns).
Not done (P3-3 / P3-5): the rest of tab-spec-499-contract is not
redundant lock-on-implementation. `variant="underline|pill"` locks each
surface's form spec (a result). `TabsPanel` is the only a11y guard
(tabpanel role + roving tabindex) until the project has render-test
infrastructure. The doesNotMatch locks prevent hand-written tab
CSS/switcher regression. Only the className maka-tab lock was true
redundant lock-on-implementation; the rest stays.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
…view P3)
Address review round 2 P3 (PR #517): the `assert.match(panel, /TabsPanel/)`
locks were too wide — they matched the import statement
(`TabsPanel } from './ui.js'`) and any TabsPanel, not a panel for each tab
value.
Tighten to per-value locks so each tab value has a corresponding panel
(tabpanel a11y pairing), and deleting any panel fails the test:
- plan: TabsPanel value="tasks" + value="runs" (new — plan had no TabsPanel
lock before)
- catalog: PrimitiveTabsPanel value="oauth" (fixed OAuth panel) +
value={cat} (mapped category panel template) + ['domestic','overseas','local']
(the mapped array — proves every category has a panel without a runtime
render test)
- skill: TabsPanel value="market" + value="builtin" + value="installed"
Unrelated TabsPanel occurrences (e.g. import lines) no longer satisfy the
lock: deleting any target surface panel fails the test, the import line
alone does not pass it.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) by Astro-Han · Pull Request #517 · apache/maka · GitHub
Skip to content

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) - #517

Merged
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec
Jul 4, 2026
Merged

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3)#517
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Tab component governance for #499 P0-3: one tab spec primitive (maka-tab class + underline/pill variants on Base UI Tabs), migrate the 4 hand-written tab surfaces, retire per-surface hand-written tab CSS, and collapse the duplicate tab sets (#477 debt). Tab active/hover repoint to the neutral --state-* tokens from #503 — no dedicated tab color token.

Why

#499 P0-3 — tabs had no spec; 4 hand-written implementations drifted: plan/skill underline with dead [data-state] selectors (Base UI sets data-active), catalog brand pill hiding the Base UI indicator, daily-review range hand-rolled <button aria-pressed>. Plus packages/ui/src/ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no variant, dead data-[selected]) that plan-reminder-panel consumed, bypassing the existing primitives/tabs.tsx. Direction: structural chrome stays neutral (brand is garnish), one primitive, two intentional variants — and a segmented control for parameter switches (not Tabs).

Refs #499

Scope

Changed:

  • primitives/tabs.tsx: TabsVariant adds "pill"; TabsList sets data-variant + maka-tabs-list; pill hides the Base UI indicator; underline indicator recolored from brand bg-control to neutral bg-foreground. TabsTab emits the shared maka-tab class.
  • ui.tsx: TabsRoot/TabsList/TabsTrigger/TabsPanel re-export from primitives/tabs.tsx (drops the second hand-rolled set + dead data-[selected] active selectors). One tab primitive, not two (refactor(ui): React architecture debt cleanup (post #430 converge) #477 debt).
  • maka-tokens.css: .maka-tab spec — pill base shape + --state-selected-bg active + --state-hover-bg hover + neutral border; underline active = --foreground text. Reuses refactor(ui): retire --hover/--active for state tokens, unify selected neutral, drop press scale #503 state tokens; no dedicated tab color token.
  • plan-reminder-panel.tsx: variant="underline" + maka-tab; drops the dead .maka-plan-tab[data-state="active"]::after under-bar (Base UI sets data-active, not data-state — plan's active visual was actually the Base UI default sliding plate, not the hand-written under-bar).
  • ProvidersPanel.tsx: catalog → variant="pill" + maka-tab + TabsPanel (oauth → ModelOAuthSection; domestic/overseas/local → filtered provider grid). Drops data-active={catalogTab === tab.id} (Base UI sets it) and the catalogTab hand-written class; keeps data-catalog-tab. Content moves from bare conditional render into panels.
  • skills-panel.tsx: hand-rolled segmented switcher (UiButton + aria-pressed + data-state) → Tabs/TabsList/TabsTrigger/TabsPanel (market/builtin/installed each a panel). a11y upgrade: roving tabindex + arrow-key contract + linked tabpanels. skillList gains a label param (no longer depends on activeSkillTab — safe under default keepMounted=false).
  • daily-review-panel.tsx: range (今日/本周/本月) → SettingsSegmented (Base UI ToggleGroup). It switches a time-window parameter, not views — segmented control is the a11y-correct primitive, not Tabs/TabsPanel. Active inherits the neutral .settingsSegmented button[data-pressed] chrome.
  • CSS cleanup: drop .catalogPillTabs button base/hover/active + [data-slot=tab-indicator] display:none (models.css), .catalogTab:hover/[data-active] (provider-editor.css), .maka-skill-tab[data-state=active] + ::after (skills.css), .maka-daily-review-range-tab[data-active] brand (daily-review.css), .maka-plan-tab[data-state=active] + ::after (plan-reminders.css).
  • state-token-governance-499-contract.test.ts: drop the 5 tab selectors from the selected/active brand-token allowlist (tabs no longer need the carve-out). Onboarding brand-emphasis allowlist stays.
  • design-system.md §3.13 Tabs: documents the spec (primitive, variants, no dedicated color token, segmented-control rule).
  • Contract tests updated: tab-spec-499-contract.test.ts (new, per-surface), model-oauth-section-contract + claude-subscription-experimental-gate (catalog conditional render → TabsPanel), skills.test (skillList aria-label dynamic), daily-review-copy-feedback-contract (range → SettingsSegmented), radius-converge-contract (drop stale TabsTrigger/TabsList ui.tsx entries), design-system-governance-406-contract (under-bar bg-controlbg-foreground).

Not included:

Verification

  • npm run -w @maka/desktop test: 1888 pass / 0 fail (rebased onto main 85c81d9d).
  • npm run typecheck: clean (main + renderer + storybook).
  • Visual: captured the 4 tab/segmented surfaces × {light, dark} = 8 default-state screenshots with the in-repo fixture harness and human-verified them. Command (from repo root, after npm run build):
    node scripts/capture-screenshots.mjs --scenario <scenario> --variant <light-1280-motion|dark-1280-motion>
    
    Scenarios: provider-workspace (catalog pill), module-skills (skill tabs), module-daily-review (daily-review range), plan-reminders (plan underline). Output: apps/desktop/tests/screenshots/<scenario>/<variant>.png. Conclusion: catalog pill active = neutral wash, skill/plan underline = neutral bar, daily-review range = neutral SettingsSegmented chrome — correct in both light and dark.
  • Token / a11y (objective, not pixel-based): tab-spec-499-contract verifies .maka-tab active/hover use --state-selected-bg / --state-hover-bg (neutral) with no brand token; design-system-governance-406 verifies the under-bar is bg-foreground; keyboard a11y (roving tabindex + arrow keys + tabpanel pairing) is guaranteed by Base UI Tabs (plan/catalog/skill) and Base UI ToggleGroup (daily-review range).
  • Harness limit: capture-screenshots.mjs variants are theme × viewport × motion; it does not capture hover/active/keyboard state variants. Hover/active state correctness is covered by the token/static contract tests above (not by screenshots); keyboard behavior is covered by the Base UI primitive contract. A CDP-driven state-variant harness is a separate piece of work.
  • Pre-existing note (not introduced here): 23 tests import dist/renderer/settings/*.js by path, but build:renderer is vite (chunks, not per-path). A fresh worktree lacks these by-path files; this PR was verified with tsc -p tsconfig.renderer.json --outDir dist/renderer to emit per-path dist. This is a test/build setup mismatch — CI may need the same workaround, or the test script should build renderer per-path.

User-facing impact

  • Catalog tabs (供应商分类) active: brand blue → neutral wash + bold.
  • Skill tabs: same neutral underline visual, but now real tabs (arrow-key nav + tabpanel) instead of a segmented switcher.
  • Daily-review range: brand blue active → neutral white-fill + shadow (SettingsSegmented chrome).
  • Plan tabs: Base UI default sliding plate → neutral underline (the hand-written under-bar was dead CSS).

Reviewer notes

  • 8 commits: slice 1 primitive+plan, unify ui.tsx, slice 2 catalog, slice 3 skill, slice 4 daily-review, slice 5 close-out, review P3-2/4 (drop redundant maka-tab + merge skill double return), review P3 (tighten TabsPanel locks to per-value). Each independently builds/typechecks/tests; squash-merge recommended.
  • The ui.tsx two-sets-of-tabs unification is the structural fix that makes slice 1's variant="underline" actually typecheck (plan imported TabsList from ui.js, which was the hand-rolled set without variant). It's the refactor(ui): React architecture debt cleanup (post #430 converge) #477 duplicate-component debt P0-3 had to collapse.
  • Biggest a11y change: skill (segmented switcher → real tabs). Biggest visual change: catalog + daily-review range (brand → neutral).
  • Review round 2: dropped the redundant maka-tab class on tab triggers (primitive already emits it); merged the skills-panel empty/non-empty duplicate return; tightened the TabsPanel contract locks from /TabsPanel/ (matched imports) to per-value (TabsPanel value="tasks", value="oauth" + value={cat} + ['domestic','overseas','local'], value="market|builtin|installed"). See Verification for the visual verification record.

…slice 1)
Introduce the shared tab spec: maka-tab class + underline/pill variants
on the Base UI Tabs primitive, active/hover repointed to
--state-selected-bg / --state-hover-bg (no brand token, no per-surface
hand-written tab CSS). underline keeps the Base UI indicator recolored
to neutral --foreground (was brand --control); pill hides the indicator
and fills the active tab with --state-selected-bg + bold.
Migrate plan tabs as the first consumer: TabsList passes variant=
"underline", TabsTrigger carries maka-tab (layout stays on maka-plan-tab).
Removes the dead .maka-plan-tab[data-state="active"] under-bar (Base UI
sets data-active, not data-state, so it never matched — plan's active
visual was actually the Base UI default sliding plate, not the
hand-written under-bar).
Tab spec contract test (tab-spec-499-contract.test.ts) locks: primitive
exposes maka-tab + underline|pill; .maka-tab uses neutral state tokens;
plan consumes maka-tab + underline with no hand-written active CSS.
ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no
variant, dead data-[selected] active selectors — Base UI sets data-active)
that plan-reminder-panel consumed, bypassing the tab spec primitive. Re-export
TabsRoot/TabsList/TabsTrigger/TabsPanel from primitives/tabs so every tab
surface gets the maka-tab class + underline/pill variants + the correct
data-active attribute from one primitive. Removes the BaseTabs import.
This is the #477 duplicate-component debt that #499 P0-3 tab governance has
to collapse: one tab primitive, not two.
…3 slice 2)
ProvidersPanel catalog tabs move to the shared spec: TabsList passes
variant="pill", TabsTrigger carries maka-tab (catalogTab hand-written class
dropped), content renders through PrimitiveTabsPanel (one per category:
oauth -> ModelOAuthSection, domestic/overseas/local -> provider grid filtered
by category) instead of a bare conditional render. The hand-written
data-active={catalogTab === tab.id} boolean is removed (Base UI sets
data-active); data-catalog-tab={tab.id} stays (locked by model-oauth contract
as the tab identifier).
CSS cleanup: drop .catalogPillTabs button base/hover/active and the
[data-slot=tab-indicator] display:none (models.css), and .catalogTab:hover /
[data-active] (provider-editor.css). The pill base shape (radius/border/
bg/padding) moves to .maka-tab[data-variant=pill] in maka-tokens.css; active
repoints from brand --nav-active to neutral --state-selected-bg + a neutral
border. .catalogTabs/.catalogPillTabs strong stay as the label font hook.
model-oauth-section-contract.test.ts: update the catalog tab assertions from
the old catalogTab/catalogTabs catalogPillTabs class lock to variant=pill +
maka-tab + TabsPanel (intent unchanged — use the primitive, keep
data-catalog-tab, no manual keyboard handler). tab-spec-499-contract.test.ts
adds the slice 2 contract: catalog consumes maka-tab + pill + TabsPanel, no
hand-written catalogTab/catalogPillTabs active/hover/indicator CSS.
…nderline + TabsPanel (#499 P0-3 slice 3)
skills-panel tabs move from a hand-rolled segmented switcher (UiButton +
aria-pressed + data-state, with a comment explicitly disowning the ARIA tabs
contract) to the shared Base UI Tabs primitive: TabsList passes
variant="underline", TabsTrigger carries maka-tab, and content renders through
TabsPanel (one per market/builtin/installed). This upgrades the surface from
a segmented switcher to real tabs with the roving-tabindex + arrow-key
contract + linked tabpanels.
The dispersed conditional render ({activeSkillTab === 'market' ? market :
skillList(...)} + {activeSkillTab !== 'market' && templates}) concentrates into
three TabsPanels. skillList gains a label param (replacing the internal
activeSkillTab === 'installed' ? ... : ... label switch) so each panel renders
its own label without depending on the active tab state — safe under the
default keepMounted=false (only the active panel mounts) and robust if
keepMounted is ever flipped. The empty-title/empty-body strings hoist to
component scope (skillListEmptyTitle/skillListEmptyBody) so both the empty-skills
and populated returns share them.
The tabs-bar keeps its right-side filter actions (static labels, market-only)
as a sibling of TabsList inside TabsRoot — they are not a tab/panel.
CSS: drop .maka-skill-tab[data-state="active"] and its ::after under-bar
(skills.css). The active state + under-bar move to .maka-tab[data-variant=
underline] via the Base UI indicator (neutral --foreground).
skills.test.ts: update the skillList section assertion from the literal
aria-label="已安装技能" to the dynamic aria-label={label} (intent unchanged —
the section still carries an aria-label, now driven by the label param).
tab-spec-499-contract.test.ts adds the slice 3 contract: skill uses maka-tab +
underline + TabsPanel, no hand-written aria-pressed/data-state switcher, no
hand-written .maka-skill-tab[data-state="active"] / ::after CSS.
…tive (#499 P0-3 slice 4)
daily-review range (今日/本周/本月) switches a time-window parameter — the
report re-fetches for the chosen range, it is not three distinct views — so it
is a segmented control, not tabs. Move it from a hand-rolled segmented switcher
(UiButton + aria-pressed + data-active) to the project's SettingsSegmented
primitive (Base UI ToggleGroup, single-select, roving tabindex + arrow keys +
data-pressed). This is the a11y-correct primitive for a parameter switcher
(radiogroup/pressed semantics), unlike Tabs/TabsPanel which expect linked
tabpanel views — the other three surfaces (plan/catalog/skill) are view
switches and stay on Tabs.
Active repoints from brand --nav-active (0.12 bg + color) to the neutral
.settingsSegmented button[data-pressed] chrome (white fill + shadow), inherited
via the primitive's global CSS. The hand-written .maka-daily-review-range-tab
base + [data-active="true"] brand block (daily-review.css) and the
PR-DAILY-REVIEW-RANGE-0 brand-accent comment are removed; .maka-daily-review-
range-tabs keeps its flex/gap layout (now the SettingsSegmented className).
daily-review-copy-feedback-contract.test.ts: update the range-tab assertion
from the hand-rolled UiButton maka-daily-review-range-tab to SettingsSegmented +
maka-daily-review-range-tabs, and flip the brand-active CSS assertion from
assert.match to assert.doesNotMatch (the brand active CSS is gone). Intent
unchanged — Daily Review controls use shared primitives, no legacy classes.
tab-spec-499-contract.test.ts adds the slice 4 contract: range uses
SettingsSegmented, no hand-written aria-pressed/data-active switcher, no
.maka-daily-review-range-tab[data-active] brand CSS.
…13, contract updates (#499 P0-3 slice 5)
Tab spec close-out:
- state-token-governance-499-contract.test.ts: drop the 5 tab selectors
(.maka-daily-review-range-tab / .catalogTab / .catalogPillTabs / .maka-skill-tab
/ .maka-plan-tab) from the selected/active brand-token allowlist. All four
tab surfaces migrated in slices 1-4 now use neutral state tokens, so the
"pending tab-spec" carve-out is gone; only the onboarding brand-emphasis
allowlist (.maka-firstrun-step / .maka-onboarding-setup-steps) remains.
- design-system.md §3.13 Tabs: document the spec — one primitive
(primitives/tabs.tsx, re-exported from ui.tsx), two variants (underline =
neutral Base UI indicator; pill = --state-selected-bg fill + bold), no
dedicated tab color token (reuse --state-hover-bg / --state-selected-bg),
and the segmented-control rule (parameter switches like daily-review range
use SettingsSegmented / Base UI ToggleGroup, not Tabs).
- radius-converge-contract.test.ts: remove the stale TabsTrigger/TabsList
entries (they used to be declared in ui.tsx with --radius-* tokens; #499 P0-3
re-exports them from primitives/tabs.tsx, which uses Tailwind rounded-md/
rounded-sm governed by primitives-design-contract escape hatches, not the
radius-token convergence contract).
- design-system-governance-406-contract.test.ts: the tab underline indicator
moves from brand --control to neutral --foreground (structural chrome stays
calm, brand is garnish — the principle already documented in
plan-reminders.css). Update the assertion from bg-control to bg-foreground
and add a ban on bg-control for the under-bar (checkbox/progress/menu keep
bg-control — those are checked controls, not structural chrome).
- claude-subscription-experimental-gate.test.ts: the OAuth catalog content
moved from a bare conditional render (catalogTab === 'oauth' ?
<ModelOAuthSection/>) into a TabsPanel, matching the model-oauth-section
contract update in slice 2. Intent unchanged — the OAuth tab renders the
real login cards.
Verified: npm run -w @maka/desktop test 1878 pass / 0 fail; typecheck clean.
@Astro-Han
Astro-Hanforce-pushed the feat/499-tab-component-spec branch from 3bfaf67 to d08300aCompareJuly 4, 2026 16:01
…l return (#499 P0-3 review P3-2/4)
Address review P3-2 + P3-4 on PR #517 (tab component spec).
P3-2 — redundant maka-tab class on tab triggers. The TabsTab primitive
already emits `maka-tab` (tabs.tsx), so callers passing
`className="maka-tab ..."` duplicate it. Drop `maka-tab` from the three
call sites, keeping local layout classes:
- skills-panel.tsx: "maka-tab maka-skill-tab" → "maka-skill-tab"
- plan-reminder-panel.tsx: "maka-tab maka-plan-tab" → "maka-plan-tab" (×2)
- ProvidersPanel.tsx: drop the bare "maka-tab" (catalog has no local class)
Sync the contract tests that locked "caller must pass maka-tab" — that
constraint is redundant now the primitive guarantees the class:
- tab-spec-499-contract.test.ts: drop the three per-surface
assert.match(TabsTrigger className maka-tab). Keep variant, TabsPanel,
and the doesNotMatch anti-regression locks.
- model-oauth-section-contract.test.ts: catalog tablist contract keeps
PrimitiveTabsTrigger + value={tab.id}, drops className="maka-tab"
(maka-tab comes from the primitive).
P3-4 — skills-panel double return. The empty-skills and non-empty
returns duplicated the entire TabsRoot/tabs/TabsPanel tree, differing
only by `skillList([] …)` vs `skillList(filteredSkills …)` and a
trailing tool-summary span. Merge to one return: filteredSkills is `[]`
when props.skills is empty (it's `(props.skills ?? []).filter(…)`), so
`skillList(filteredSkills, …)` is equivalent in both states; the summary
span becomes conditional. Cuts ~15 lines and removes a future drift
hazard (changing the tabs tree used to require editing two returns).
Not done (P3-3 / P3-5): the rest of tab-spec-499-contract is not
redundant lock-on-implementation. `variant="underline|pill"` locks each
surface's form spec (a result). `TabsPanel` is the only a11y guard
(tabpanel role + roving tabindex) until the project has render-test
infrastructure. The doesNotMatch locks prevent hand-written tab
CSS/switcher regression. Only the className maka-tab lock was true
redundant lock-on-implementation; the rest stays.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
…view P3)
Address review round 2 P3 (PR #517): the `assert.match(panel, /TabsPanel/)`
locks were too wide — they matched the import statement
(`TabsPanel } from './ui.js'`) and any TabsPanel, not a panel for each tab
value.
Tighten to per-value locks so each tab value has a corresponding panel
(tabpanel a11y pairing), and deleting any panel fails the test:
- plan: TabsPanel value="tasks" + value="runs" (new — plan had no TabsPanel
lock before)
- catalog: PrimitiveTabsPanel value="oauth" (fixed OAuth panel) +
value={cat} (mapped category panel template) + ['domestic','overseas','local']
(the mapped array — proves every category has a panel without a runtime
render test)
- skill: TabsPanel value="market" + value="builtin" + value="installed"
Unrelated TabsPanel occurrences (e.g. import lines) no longer satisfy the
lock: deleting any target surface panel fails the test, the import line
alone does not pass it.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
Sign up for freeto 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

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) by Astro-Han · Pull Request #517 · apache/maka · GitHub
Skip to content

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3) - #517

Merged
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec
Jul 4, 2026
Merged

feat(ui): tab component spec — maka-tab + variants, migrate 4 tab surfaces (#499 P0-3)#517
Astro-Han merged 8 commits into
mainfrom
feat/499-tab-component-spec

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Tab component governance for #499 P0-3: one tab spec primitive (maka-tab class + underline/pill variants on Base UI Tabs), migrate the 4 hand-written tab surfaces, retire per-surface hand-written tab CSS, and collapse the duplicate tab sets (#477 debt). Tab active/hover repoint to the neutral --state-* tokens from #503 — no dedicated tab color token.

Why

#499 P0-3 — tabs had no spec; 4 hand-written implementations drifted: plan/skill underline with dead [data-state] selectors (Base UI sets data-active), catalog brand pill hiding the Base UI indicator, daily-review range hand-rolled <button aria-pressed>. Plus packages/ui/src/ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no variant, dead data-[selected]) that plan-reminder-panel consumed, bypassing the existing primitives/tabs.tsx. Direction: structural chrome stays neutral (brand is garnish), one primitive, two intentional variants — and a segmented control for parameter switches (not Tabs).

Refs #499

Scope

Changed:

  • primitives/tabs.tsx: TabsVariant adds "pill"; TabsList sets data-variant + maka-tabs-list; pill hides the Base UI indicator; underline indicator recolored from brand bg-control to neutral bg-foreground. TabsTab emits the shared maka-tab class.
  • ui.tsx: TabsRoot/TabsList/TabsTrigger/TabsPanel re-export from primitives/tabs.tsx (drops the second hand-rolled set + dead data-[selected] active selectors). One tab primitive, not two (refactor(ui): React architecture debt cleanup (post #430 converge) #477 debt).
  • maka-tokens.css: .maka-tab spec — pill base shape + --state-selected-bg active + --state-hover-bg hover + neutral border; underline active = --foreground text. Reuses refactor(ui): retire --hover/--active for state tokens, unify selected neutral, drop press scale #503 state tokens; no dedicated tab color token.
  • plan-reminder-panel.tsx: variant="underline" + maka-tab; drops the dead .maka-plan-tab[data-state="active"]::after under-bar (Base UI sets data-active, not data-state — plan's active visual was actually the Base UI default sliding plate, not the hand-written under-bar).
  • ProvidersPanel.tsx: catalog → variant="pill" + maka-tab + TabsPanel (oauth → ModelOAuthSection; domestic/overseas/local → filtered provider grid). Drops data-active={catalogTab === tab.id} (Base UI sets it) and the catalogTab hand-written class; keeps data-catalog-tab. Content moves from bare conditional render into panels.
  • skills-panel.tsx: hand-rolled segmented switcher (UiButton + aria-pressed + data-state) → Tabs/TabsList/TabsTrigger/TabsPanel (market/builtin/installed each a panel). a11y upgrade: roving tabindex + arrow-key contract + linked tabpanels. skillList gains a label param (no longer depends on activeSkillTab — safe under default keepMounted=false).
  • daily-review-panel.tsx: range (今日/本周/本月) → SettingsSegmented (Base UI ToggleGroup). It switches a time-window parameter, not views — segmented control is the a11y-correct primitive, not Tabs/TabsPanel. Active inherits the neutral .settingsSegmented button[data-pressed] chrome.
  • CSS cleanup: drop .catalogPillTabs button base/hover/active + [data-slot=tab-indicator] display:none (models.css), .catalogTab:hover/[data-active] (provider-editor.css), .maka-skill-tab[data-state=active] + ::after (skills.css), .maka-daily-review-range-tab[data-active] brand (daily-review.css), .maka-plan-tab[data-state=active] + ::after (plan-reminders.css).
  • state-token-governance-499-contract.test.ts: drop the 5 tab selectors from the selected/active brand-token allowlist (tabs no longer need the carve-out). Onboarding brand-emphasis allowlist stays.
  • design-system.md §3.13 Tabs: documents the spec (primitive, variants, no dedicated color token, segmented-control rule).
  • Contract tests updated: tab-spec-499-contract.test.ts (new, per-surface), model-oauth-section-contract + claude-subscription-experimental-gate (catalog conditional render → TabsPanel), skills.test (skillList aria-label dynamic), daily-review-copy-feedback-contract (range → SettingsSegmented), radius-converge-contract (drop stale TabsTrigger/TabsList ui.tsx entries), design-system-governance-406-contract (under-bar bg-controlbg-foreground).

Not included:

Verification

  • npm run -w @maka/desktop test: 1888 pass / 0 fail (rebased onto main 85c81d9d).
  • npm run typecheck: clean (main + renderer + storybook).
  • Visual: captured the 4 tab/segmented surfaces × {light, dark} = 8 default-state screenshots with the in-repo fixture harness and human-verified them. Command (from repo root, after npm run build):
    node scripts/capture-screenshots.mjs --scenario <scenario> --variant <light-1280-motion|dark-1280-motion>
    
    Scenarios: provider-workspace (catalog pill), module-skills (skill tabs), module-daily-review (daily-review range), plan-reminders (plan underline). Output: apps/desktop/tests/screenshots/<scenario>/<variant>.png. Conclusion: catalog pill active = neutral wash, skill/plan underline = neutral bar, daily-review range = neutral SettingsSegmented chrome — correct in both light and dark.
  • Token / a11y (objective, not pixel-based): tab-spec-499-contract verifies .maka-tab active/hover use --state-selected-bg / --state-hover-bg (neutral) with no brand token; design-system-governance-406 verifies the under-bar is bg-foreground; keyboard a11y (roving tabindex + arrow keys + tabpanel pairing) is guaranteed by Base UI Tabs (plan/catalog/skill) and Base UI ToggleGroup (daily-review range).
  • Harness limit: capture-screenshots.mjs variants are theme × viewport × motion; it does not capture hover/active/keyboard state variants. Hover/active state correctness is covered by the token/static contract tests above (not by screenshots); keyboard behavior is covered by the Base UI primitive contract. A CDP-driven state-variant harness is a separate piece of work.
  • Pre-existing note (not introduced here): 23 tests import dist/renderer/settings/*.js by path, but build:renderer is vite (chunks, not per-path). A fresh worktree lacks these by-path files; this PR was verified with tsc -p tsconfig.renderer.json --outDir dist/renderer to emit per-path dist. This is a test/build setup mismatch — CI may need the same workaround, or the test script should build renderer per-path.

User-facing impact

  • Catalog tabs (供应商分类) active: brand blue → neutral wash + bold.
  • Skill tabs: same neutral underline visual, but now real tabs (arrow-key nav + tabpanel) instead of a segmented switcher.
  • Daily-review range: brand blue active → neutral white-fill + shadow (SettingsSegmented chrome).
  • Plan tabs: Base UI default sliding plate → neutral underline (the hand-written under-bar was dead CSS).

Reviewer notes

  • 8 commits: slice 1 primitive+plan, unify ui.tsx, slice 2 catalog, slice 3 skill, slice 4 daily-review, slice 5 close-out, review P3-2/4 (drop redundant maka-tab + merge skill double return), review P3 (tighten TabsPanel locks to per-value). Each independently builds/typechecks/tests; squash-merge recommended.
  • The ui.tsx two-sets-of-tabs unification is the structural fix that makes slice 1's variant="underline" actually typecheck (plan imported TabsList from ui.js, which was the hand-rolled set without variant). It's the refactor(ui): React architecture debt cleanup (post #430 converge) #477 duplicate-component debt P0-3 had to collapse.
  • Biggest a11y change: skill (segmented switcher → real tabs). Biggest visual change: catalog + daily-review range (brand → neutral).
  • Review round 2: dropped the redundant maka-tab class on tab triggers (primitive already emits it); merged the skills-panel empty/non-empty duplicate return; tightened the TabsPanel contract locks from /TabsPanel/ (matched imports) to per-value (TabsPanel value="tasks", value="oauth" + value={cat} + ['domestic','overseas','local'], value="market|builtin|installed"). See Verification for the visual verification record.

…slice 1)
Introduce the shared tab spec: maka-tab class + underline/pill variants
on the Base UI Tabs primitive, active/hover repointed to
--state-selected-bg / --state-hover-bg (no brand token, no per-surface
hand-written tab CSS). underline keeps the Base UI indicator recolored
to neutral --foreground (was brand --control); pill hides the indicator
and fills the active tab with --state-selected-bg + bold.
Migrate plan tabs as the first consumer: TabsList passes variant=
"underline", TabsTrigger carries maka-tab (layout stays on maka-plan-tab).
Removes the dead .maka-plan-tab[data-state="active"] under-bar (Base UI
sets data-active, not data-state, so it never matched — plan's active
visual was actually the Base UI default sliding plate, not the
hand-written under-bar).
Tab spec contract test (tab-spec-499-contract.test.ts) locks: primitive
exposes maka-tab + underline|pill; .maka-tab uses neutral state tokens;
plan consumes maka-tab + underline with no hand-written active CSS.
ui.tsx carried a second hand-rolled tab set (Base UI + bg-muted plate, no
variant, dead data-[selected] active selectors — Base UI sets data-active)
that plan-reminder-panel consumed, bypassing the tab spec primitive. Re-export
TabsRoot/TabsList/TabsTrigger/TabsPanel from primitives/tabs so every tab
surface gets the maka-tab class + underline/pill variants + the correct
data-active attribute from one primitive. Removes the BaseTabs import.
This is the #477 duplicate-component debt that #499 P0-3 tab governance has
to collapse: one tab primitive, not two.
…3 slice 2)
ProvidersPanel catalog tabs move to the shared spec: TabsList passes
variant="pill", TabsTrigger carries maka-tab (catalogTab hand-written class
dropped), content renders through PrimitiveTabsPanel (one per category:
oauth -> ModelOAuthSection, domestic/overseas/local -> provider grid filtered
by category) instead of a bare conditional render. The hand-written
data-active={catalogTab === tab.id} boolean is removed (Base UI sets
data-active); data-catalog-tab={tab.id} stays (locked by model-oauth contract
as the tab identifier).
CSS cleanup: drop .catalogPillTabs button base/hover/active and the
[data-slot=tab-indicator] display:none (models.css), and .catalogTab:hover /
[data-active] (provider-editor.css). The pill base shape (radius/border/
bg/padding) moves to .maka-tab[data-variant=pill] in maka-tokens.css; active
repoints from brand --nav-active to neutral --state-selected-bg + a neutral
border. .catalogTabs/.catalogPillTabs strong stay as the label font hook.
model-oauth-section-contract.test.ts: update the catalog tab assertions from
the old catalogTab/catalogTabs catalogPillTabs class lock to variant=pill +
maka-tab + TabsPanel (intent unchanged — use the primitive, keep
data-catalog-tab, no manual keyboard handler). tab-spec-499-contract.test.ts
adds the slice 2 contract: catalog consumes maka-tab + pill + TabsPanel, no
hand-written catalogTab/catalogPillTabs active/hover/indicator CSS.
…nderline + TabsPanel (#499 P0-3 slice 3)
skills-panel tabs move from a hand-rolled segmented switcher (UiButton +
aria-pressed + data-state, with a comment explicitly disowning the ARIA tabs
contract) to the shared Base UI Tabs primitive: TabsList passes
variant="underline", TabsTrigger carries maka-tab, and content renders through
TabsPanel (one per market/builtin/installed). This upgrades the surface from
a segmented switcher to real tabs with the roving-tabindex + arrow-key
contract + linked tabpanels.
The dispersed conditional render ({activeSkillTab === 'market' ? market :
skillList(...)} + {activeSkillTab !== 'market' && templates}) concentrates into
three TabsPanels. skillList gains a label param (replacing the internal
activeSkillTab === 'installed' ? ... : ... label switch) so each panel renders
its own label without depending on the active tab state — safe under the
default keepMounted=false (only the active panel mounts) and robust if
keepMounted is ever flipped. The empty-title/empty-body strings hoist to
component scope (skillListEmptyTitle/skillListEmptyBody) so both the empty-skills
and populated returns share them.
The tabs-bar keeps its right-side filter actions (static labels, market-only)
as a sibling of TabsList inside TabsRoot — they are not a tab/panel.
CSS: drop .maka-skill-tab[data-state="active"] and its ::after under-bar
(skills.css). The active state + under-bar move to .maka-tab[data-variant=
underline] via the Base UI indicator (neutral --foreground).
skills.test.ts: update the skillList section assertion from the literal
aria-label="已安装技能" to the dynamic aria-label={label} (intent unchanged —
the section still carries an aria-label, now driven by the label param).
tab-spec-499-contract.test.ts adds the slice 3 contract: skill uses maka-tab +
underline + TabsPanel, no hand-written aria-pressed/data-state switcher, no
hand-written .maka-skill-tab[data-state="active"] / ::after CSS.
…tive (#499 P0-3 slice 4)
daily-review range (今日/本周/本月) switches a time-window parameter — the
report re-fetches for the chosen range, it is not three distinct views — so it
is a segmented control, not tabs. Move it from a hand-rolled segmented switcher
(UiButton + aria-pressed + data-active) to the project's SettingsSegmented
primitive (Base UI ToggleGroup, single-select, roving tabindex + arrow keys +
data-pressed). This is the a11y-correct primitive for a parameter switcher
(radiogroup/pressed semantics), unlike Tabs/TabsPanel which expect linked
tabpanel views — the other three surfaces (plan/catalog/skill) are view
switches and stay on Tabs.
Active repoints from brand --nav-active (0.12 bg + color) to the neutral
.settingsSegmented button[data-pressed] chrome (white fill + shadow), inherited
via the primitive's global CSS. The hand-written .maka-daily-review-range-tab
base + [data-active="true"] brand block (daily-review.css) and the
PR-DAILY-REVIEW-RANGE-0 brand-accent comment are removed; .maka-daily-review-
range-tabs keeps its flex/gap layout (now the SettingsSegmented className).
daily-review-copy-feedback-contract.test.ts: update the range-tab assertion
from the hand-rolled UiButton maka-daily-review-range-tab to SettingsSegmented +
maka-daily-review-range-tabs, and flip the brand-active CSS assertion from
assert.match to assert.doesNotMatch (the brand active CSS is gone). Intent
unchanged — Daily Review controls use shared primitives, no legacy classes.
tab-spec-499-contract.test.ts adds the slice 4 contract: range uses
SettingsSegmented, no hand-written aria-pressed/data-active switcher, no
.maka-daily-review-range-tab[data-active] brand CSS.
…13, contract updates (#499 P0-3 slice 5)
Tab spec close-out:
- state-token-governance-499-contract.test.ts: drop the 5 tab selectors
(.maka-daily-review-range-tab / .catalogTab / .catalogPillTabs / .maka-skill-tab
/ .maka-plan-tab) from the selected/active brand-token allowlist. All four
tab surfaces migrated in slices 1-4 now use neutral state tokens, so the
"pending tab-spec" carve-out is gone; only the onboarding brand-emphasis
allowlist (.maka-firstrun-step / .maka-onboarding-setup-steps) remains.
- design-system.md §3.13 Tabs: document the spec — one primitive
(primitives/tabs.tsx, re-exported from ui.tsx), two variants (underline =
neutral Base UI indicator; pill = --state-selected-bg fill + bold), no
dedicated tab color token (reuse --state-hover-bg / --state-selected-bg),
and the segmented-control rule (parameter switches like daily-review range
use SettingsSegmented / Base UI ToggleGroup, not Tabs).
- radius-converge-contract.test.ts: remove the stale TabsTrigger/TabsList
entries (they used to be declared in ui.tsx with --radius-* tokens; #499 P0-3
re-exports them from primitives/tabs.tsx, which uses Tailwind rounded-md/
rounded-sm governed by primitives-design-contract escape hatches, not the
radius-token convergence contract).
- design-system-governance-406-contract.test.ts: the tab underline indicator
moves from brand --control to neutral --foreground (structural chrome stays
calm, brand is garnish — the principle already documented in
plan-reminders.css). Update the assertion from bg-control to bg-foreground
and add a ban on bg-control for the under-bar (checkbox/progress/menu keep
bg-control — those are checked controls, not structural chrome).
- claude-subscription-experimental-gate.test.ts: the OAuth catalog content
moved from a bare conditional render (catalogTab === 'oauth' ?
<ModelOAuthSection/>) into a TabsPanel, matching the model-oauth-section
contract update in slice 2. Intent unchanged — the OAuth tab renders the
real login cards.
Verified: npm run -w @maka/desktop test 1878 pass / 0 fail; typecheck clean.
@Astro-Han
Astro-Hanforce-pushed the feat/499-tab-component-spec branch from 3bfaf67 to d08300aCompareJuly 4, 2026 16:01
…l return (#499 P0-3 review P3-2/4)
Address review P3-2 + P3-4 on PR #517 (tab component spec).
P3-2 — redundant maka-tab class on tab triggers. The TabsTab primitive
already emits `maka-tab` (tabs.tsx), so callers passing
`className="maka-tab ..."` duplicate it. Drop `maka-tab` from the three
call sites, keeping local layout classes:
- skills-panel.tsx: "maka-tab maka-skill-tab" → "maka-skill-tab"
- plan-reminder-panel.tsx: "maka-tab maka-plan-tab" → "maka-plan-tab" (×2)
- ProvidersPanel.tsx: drop the bare "maka-tab" (catalog has no local class)
Sync the contract tests that locked "caller must pass maka-tab" — that
constraint is redundant now the primitive guarantees the class:
- tab-spec-499-contract.test.ts: drop the three per-surface
assert.match(TabsTrigger className maka-tab). Keep variant, TabsPanel,
and the doesNotMatch anti-regression locks.
- model-oauth-section-contract.test.ts: catalog tablist contract keeps
PrimitiveTabsTrigger + value={tab.id}, drops className="maka-tab"
(maka-tab comes from the primitive).
P3-4 — skills-panel double return. The empty-skills and non-empty
returns duplicated the entire TabsRoot/tabs/TabsPanel tree, differing
only by `skillList([] …)` vs `skillList(filteredSkills …)` and a
trailing tool-summary span. Merge to one return: filteredSkills is `[]`
when props.skills is empty (it's `(props.skills ?? []).filter(…)`), so
`skillList(filteredSkills, …)` is equivalent in both states; the summary
span becomes conditional. Cuts ~15 lines and removes a future drift
hazard (changing the tabs tree used to require editing two returns).
Not done (P3-3 / P3-5): the rest of tab-spec-499-contract is not
redundant lock-on-implementation. `variant="underline|pill"` locks each
surface's form spec (a result). `TabsPanel` is the only a11y guard
(tabpanel role + roving tabindex) until the project has render-test
infrastructure. The doesNotMatch locks prevent hand-written tab
CSS/switcher regression. Only the className maka-tab lock was true
redundant lock-on-implementation; the rest stays.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
…view P3)
Address review round 2 P3 (PR #517): the `assert.match(panel, /TabsPanel/)`
locks were too wide — they matched the import statement
(`TabsPanel } from './ui.js'`) and any TabsPanel, not a panel for each tab
value.
Tighten to per-value locks so each tab value has a corresponding panel
(tabpanel a11y pairing), and deleting any panel fails the test:
- plan: TabsPanel value="tasks" + value="runs" (new — plan had no TabsPanel
lock before)
- catalog: PrimitiveTabsPanel value="oauth" (fixed OAuth panel) +
value={cat} (mapped category panel template) + ['domestic','overseas','local']
(the mapped array — proves every category has a panel without a runtime
render test)
- skill: TabsPanel value="market" + value="builtin" + value="installed"
Unrelated TabsPanel occurrences (e.g. import lines) no longer satisfy the
lock: deleting any target surface panel fails the test, the import line
alone does not pass it.
Verified: npm run -w @maka/desktop test 1888 pass / 0 fail; typecheck clean.
Sign up for freeto 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

@Astro-Han