Uh oh!
There was an error while loading. Please reload this page.
feat(ui): style-hook convention + small Base UI migrations (Collapsible/Tooltip/NumberField) (#520 PR5) - #543
Conversation
…review P2)
The disclosure migration made tool-activity's tool cards `defaultOpen={isOpenByDefault(item.status)}` (uncontrolled — open is decided once on first render). That regressed the pre-Collapsible `<details open={isOpenByDefault(status)}>` behavior, which re-evaluated open every render: a card that defaults open while pending/running no longer auto-collapsed when it settled to completed/interrupted.
Restore the status-following behavior with a controlled state machine:
- Extract the per-item card into a `ToolActivityCard` component (hooks can't
live in the `.map` callback).
- `useState(isOpenByDefault(item.status))` seeds open; `useEffect` re-syncs it
to `isOpenByDefault(item.status)` on every `[item.status]` change, so a
pending/running card collapses when it settles.
- `onOpenChange={setOpen}` lets the user still toggle the card in between;
the next status change re-syncs. (The pre-Collapsible `<details>` let the
user toggle too, but only as a side effect of React not re-rendering — the
state machine makes that intentional and predictable.)
TDD: disclosure-collapsible-contract gains an assertion that tool-activity
must NOT use `defaultOpen`, must use `onOpenChange`, and must re-sync via
`useEffect` on `[item.status]`. RED first (defaultOpen was present, no
onOpenChange, no re-sync), then GREEN. (maka has no React testing-library /
react-test-renderer, so the behavior is locked structurally + verified by
hand; the rerender-act path is out of scope.)
Verification: 2009/2009 desktop tests pass (+1 contract assertion). typecheck
clean. turn-narrative screenshot unchanged (0.09% — the disclosure commit's
summary→button residual; the card state is fixed by the scenario seed, and
controlled-open matches the pre-Collapsible behavior at that fixed state).review P3) Two unrelated P3 nits from review, both one-line-ish: 1. Collapsible barrel export was aliased `as PrimitiveCollapsible` (the old Tabs/Accordion convention), while the other new-in-PR5 primitives (Tooltip, NumberField) export directly. The disclosure-collapsible-contract regex allowed `@maka/ui` as an import path, but the barrel didn't actually export `Collapsible` under that name — a phantom-pass path. Export Collapsible/CollapsibleTrigger/CollapsiblePanel directly (drop the `Primitive*` aliases and the `CollapsiblePrimitive` re-export — nothing consumes them, and Tooltip/NumberField don't export their Primitive either). The internal sites keep importing via `./primitives/collapsible.js` (the in-package convention — alert/chat/etc. all use relative primitive paths), so the `@maka/ui` path is now genuinely available rather than phantom. 2. tooltip.tsx usage doc showed `<TooltipTrigger asChild>` (Radix), but this PR's convention is Base UI v1's `render` prop. Fix the example to `render={<Button />}` and note that the Trigger merges its props + children into the rendered element, so the next person doesn't copy the comment and hit the same typecheck failure the migration hit. Verification: 2009/2009 desktop tests pass. typecheck clean. No visual change (barrel export names + a doc comment).
The tooltip migration dropped the delete button's native `title="删除"` (the button is icon-only at rest — the visible 删除/删除中… label is visually hidden via .maka-artifact-toolbar-destructive-label, kept only for screen readers). The screen-reader path was preserved, but mouse users lost the hover hint. Wrap the delete button in a Base UI Tooltip (same shape as the artifact-pane collapse button already migrated in this PR): TooltipTrigger renders the destructive Button, TooltipContent mirrors the 删除/删除中… label for hover. The visually-hidden span still carries the SR label; the two are complementary, not redundant. Verification: 2009/2009 desktop tests pass. typecheck clean. turn-narrative screenshot unchanged (0.09% — the disclosure commit's residual; the delete button's tooltip is hover-only, and the button's rest appearance is unchanged).
Astro-Han
commented
Jul 5, 2026
Thanks for the review. Verified every claim against the code before acting; results + fixes below. P2 — tool-activity |
…ew P2) The disclosure migration left the tool-card open-state divider pinned to the old native selector `[&[open]>summary]`. After migrating the card to Base UI Collapsible, the real DOM is a root with `[data-open]` and a button trigger with `data-slot="collapsible-trigger"`, so the old selector no longer matched and open tool-card headers could lose the body divider. Use the Base UI / ShadCN-style ownership instead of a root-owned child selector: - the card root exposes a named group (`group/tool`) alongside the Base UI `[data-open]` state, - the styled trigger/header part declares the divider with `group-data-[open]/tool:[border-bottom:1px_solid_var(--border)]`, - stale native-disclosure comments are removed. TDD: chat-tool-card-cascade-contract first failed because chat.tsx still kept `[open]>summary`; it now requires the named group + trigger-owned divider and bans the old selector from source/comments. Verification: related contracts pass (12/12), desktop tests pass (2009/2009), typecheck clean, renderer build succeeds and generated CSS contains the named group data selector.
… review P3) Base UI Collapsible.Trigger already exposes `data-panel-open` when its panel is open, so the previous named root group (`group/tool` + `group-data-[open]/tool`) was more machinery than this divider needs. Keep the Base UI / ShadCN ownership principle but make it smaller: the styled trigger/header part now owns the divider directly via `data-[panel-open]:[border-bottom:1px_solid_var(--border)]`, and the root drops `group/tool`. TDD: chat-tool-card-cascade-contract first failed because the source still used the root group path; it now bans both the old native `[open]>summary` selector and the root group open-state path, and requires the trigger-level `data-[panel-open]` divider. Verification: related contracts pass (12/12), desktop tests pass (2009/2009), typecheck clean, renderer build succeeds, generated CSS contains the `data-[panel-open]` utility and no `group-data-[open]/tool` / `group\/tool` selector. turn-narrative screenshot remains at the existing 0.09% disclosure residual.
… state attrs) (#520 PR5 item 23) Foundation commit for PR5 — the convention the disclosure / tooltip / number-field / input migrations in the following commits follow. data-slot: add data-slot="<name>" to all 20 Base UI wrappers in ui.tsx (Button / Separator / Checkbox / DialogBackdrop / DialogPopup / Select Trigger/List/Popup/Group/GroupLabel/Separator/Item / FieldDescription / Label / Switch / Toggle / ToggleGroup / RadioGroup / Radio / Progress). The ./primitives/ wrappers (accordion / alert / badge / …) already do this; ui.tsx wrappers did not. data-slot is a stable CSS targeting hook that survives className drift. Base UI does not emit data-slot itself, so the wrapper owns it. No CSS targets these yet — this is forward-looking convention, a visual no-op. State-attribute form: adopt Base UI's native attribute-presence form ([data-active] / [data-open] / [data-checked] / [data-selected] / [data-pressed] / [data-highlighted] / [data-disabled]), NOT the attribute-value form ([data-active="true"]). Maka's renderer CSS has zero state-attribute selectors today, so there is nothing to break and no override layer to maintain. A per-component hook map (Tabs data-active / Select data-[highlighted]/data-[selected] / Checkbox/Switch/Radio data-[checked] / Toggle data-[pressed] / Dialog/Tooltip/Popover data-[open]) is documented in a doc comment at the top of ui.tsx, along with the whitelisted CSS var hooks (--anchor-* / --available-* / --active-tab-*). className(state) function form is deferred — add only when a migration in this PR actually needs state-based classes. style-hook-convention-contract.test.ts locks the data-slot rule: every ui.tsx wrapper that forwards to a <Base*> component must carry data-slot, and the convention doc comment must stay present. Hand-written native elements (legacy Input / Textarea / Badge) are out of scope until they retire onto a Base UI primitive (input canonical, commit 5). Verification: 1972/1972 desktop tests pass (+4 contract). typecheck clean. renderer build clean. Visual no-op (data-slot is an attribute; no CSS targets it yet).
…tem 17) Migrate the four native <details>/<summary> disclosure sites onto Base UI Collapsible, following the style-hook convention (item 23). The code comments at the sites already said "future Base UI Accordion path"; all four are independent single sections (not grouped), so Collapsible (not Accordion) is the right primitive. Sites: - chat-view.tsx turn-thinking (maka-turn-thinking): uncontrolled, default closed. - chat-view.tsx reasoning-panel (maka-reasoning-panel): controlled open state moves from reading e.currentTarget.open off the native toggle event to Collapsible's open + onOpenChange props (default-open behavior preserved). - permission-dialog.tsx permission-raw (maka-permission-raw): uncontrolled. - tool-activity.tsx tool card ([data-slot="tool"]): uncontrolled with defaultOpen = isOpenByDefault(status); the data-slot="tool" hook overrides the Collapsible wrapper's default data-slot="collapsible" via prop spread, so the existing [data-slot="tool"] CSS keeps working. New primitive: packages/ui/src/primitives/collapsible.tsx wraps Base UI Collapsible Root/Trigger/Panel with data-slot="collapsible" / "collapsible-trigger" / "collapsible-panel". The Trigger carries a button chrome reset (appearance-none / bg-transparent / border-0 / p-0 / font inherit) so the summary row's clean look is preserved — native <summary> had no background/border/padding, but <button> does. Exported from the barrel as PrimitiveCollapsible / PrimitiveCollapsibleTrigger / PrimitiveCollapsiblePanel. CSS retargeting (the DOM changed: <summary> -> <button> trigger, <details open> -> Collapsible with data-panel-open on the trigger): - .maka-turn-thinking summary / [open] > summary::before -> [data-slot="collapsible-trigger"] / [data-slot="collapsible-trigger"][data-panel-open]::before - .maka-permission-raw > summary / [open] > summary::before -> [data-slot="collapsible-trigger"] / [data-panel-open] variants - .maka-reasoning-panel-header marker resets dropped (button has no native marker); [open] .maka-reasoning-panel-chevron -> [data-slot="collapsible-trigger"][data-panel-open] .maka-reasoning-panel-chevron - [data-slot="tool"] > summary ::-webkit-details-marker / ::marker resets dropped (button trigger has no native marker). - list-style: none rules dropped (no list marker on a button). TDD: disclosure-collapsible-contract.test.ts written first (RED: sites still used <details>, no primitive). GREEN: primitive + 4 migrations + CSS retarget. The contract bans <details>/<summary> (incl. in comments) in the three migrated files and requires a Collapsible import; it also pins the primitive's data-slot on Root/Trigger/Panel. Existing tests updated: border-width-converge-contract TRIANGLE_CARET_SELECTORS (the chevron ::before border-width: 4px 0 4px 5px moved from summary::before to [data-slot="collapsible-trigger"]::before); chat-marker-cascade-contract (.maka-turn-thinking summary -> [data-slot="collapsible-trigger"]); chat-tool-card-cascade-contract (the marker-reset residue on [data-slot="tool"] > summary is gone — the trigger is a button now). Verification: 1975/1975 desktop tests pass (+4 disclosure contract; the 3 existing contracts updated). typecheck clean. Screenshots vs clean main (PR4): turn-narrative 0.09% (the summary->button swap's tiny pixel difference — button baseline, the CollapsiblePanel wrapper), first-run 0%. No regression.
…tem 20)
Migrate the clearest icon-only-action button tooltips off the native title=
attribute (an unstyled, delayed browser tooltip) onto Base UI Tooltip,
which gives a themed, positioned, hover+focus tooltip matching the app.
Sites (13 buttons across 3 files):
- app-shell-chrome-actions.tsx: search / sidebar / new-task / feedback /
command-palette / help / health (7).
- browser-panel.tsx: back / forward / refresh / close (4).
- artifact-pane.tsx: the collapse toggle (1); the delete button's redundant
title= is dropped instead (it already shows a visible 删除 label, so a
tooltip would be a duplicate).
New primitive: packages/ui/src/primitives/tooltip.tsx wraps Base UI Tooltip
Root / Trigger / Content with data-slot="tooltip" / "tooltip-trigger" /
"tooltip-content". TooltipContent collapses Portal + Positioner + Popup
(the DialogContent shape) with a themed look (popover bg/corner/shadow +
z-overlay). The Trigger forwards via Base UI's render prop
(<TooltipTrigger render={<Button …/>} …props>…children…</TooltipTrigger>),
which merges the Trigger's props + children into the rendered Button — Base
UI v1 uses render, not the Radix asChild prop. Exported from the barrel as
Tooltip / TooltipTrigger / TooltipContent (no ui.tsx wrapper, so direct
names). aria-label stays on the button (accessible name); the TooltipContent
is the visual hover hint.
TDD: tooltip-converge-contract.test.ts written first (RED: the 3 files still
carried native title=, no primitive). GREEN: primitive + 13 migrations. The
contract bans a title= JSX attribute in the 3 migrated files and requires
a Tooltip import; it pins the primitive's data-slot on Root/Trigger/Content.
The regex \btitle= matches JSX attributes but not JS `const title =` (the
space before =).
Scope note: the longer tail of title= usages (truncation spans like
memory-settings path + chat-model-switcher chip, SelectTrigger titles,
status-badge icons, OnboardingHero's submit button) are tooltip-eligible but
each needs a label-vs-tooltip judgment and are not swept in this commit —
deferred to a follow-up. label-prop components (SettingRow / MetricCard /
SetupHero / Section) render title as visible text and are NOT tooltips;
they stay.
Existing tests updated: app-region-hygiene-contract's
extractStaticUiButtonClassNames now also matches the
<TooltipTrigger render={<UiButton/>}> shape (the className moved from the
UiButton element to the TooltipTrigger that renders it); search-modal-
lifecycle's shell-search-button regex now matches </TooltipTrigger> (the
UiButton is the render target, self-closing).
Verification: 1979/1979 desktop tests pass (+4 tooltip contract). typecheck
clean (render prop, not asChild). renderer build clean. Screenshots vs clean
main (PR4): turn-narrative 0.09% (the disclosure commit's residual; tooltips
are hover-only so rest state is unchanged), first-run 0%.Migrate the two gateway/proxy port inputs off the native Input +
Number(event.currentTarget.value) hand-conversion onto Base UI NumberField,
which binds value: number | null directly and parses numeric input itself.
Sites:
- general-settings-page proxy port: value={proxyDraft.port || null}
(0 renders empty, matching the old String(port || '')), onValueChange
writes v ?? 0 (empty -> 0, matching the old Number(value) || 0).
- open-gateway-settings-page gateway port: value={gatewayDraft.port}
(0 renders "0", matching String(port)), onValueChange writes v ?? 3939
(empty -> 3939, matching Number(value) || 3939). inputMode="numeric" stays
on the input.
New primitive: packages/ui/src/primitives/number-field.tsx wraps Base UI
NumberField Root / Input with data-slot="number-field" / "number-field-input".
NumberFieldInput carries the maka standalone-input look via the shared
inputClasses export (the same classes the native ui.tsx Input used), so the
port inputs keep their look; unstyled gives the bare form for any future
Field/InputGroup embedding. Exported from the barrel as NumberField /
NumberFieldInput (no ui.tsx wrapper, so direct names, like Tooltip).
To share the maka input styling without duplicating it, ui.tsx exports
inputClasses + bareFieldClasses (const -> export const); the number-field
primitive imports them. (The input-canonical commit, item 22, will move
these into primitives/input.tsx and update this import.)
TDD: number-field-converge-contract.test.ts written first (RED: the two
files still carried Number(event.currentTarget.value), no primitive). GREEN:
primitive + 2 migrations. The contract bans the hand-conversion
(Number(event.currentTarget.value)) and requires a NumberField import; it
pins the primitive's data-slot on Root/Input. The hand-convert regex is
specific (Number(event.currentTarget.value)) so a different Number() call is
not flagged.
Existing test updated: settings-network-gateway-contract's two port-input
assertions now pin the NumberField form (value={...|| null} / value={...} +
onValueChange={(v) => update...({ port: v ?? default })}) instead of the old
String(value) + onChange Number(event.currentTarget.value). The intent —
"port input renders from the local draft while persisting in the background"
— is preserved; only the binding form changed. The "port input must not
lock after each digit" doesNotMatch still passes (the NumberFieldInput is not
disabled while saving).
Verification: 1982/1982 desktop tests pass (+3 number-field contract).
typecheck clean. renderer build clean. Screenshots: the port inputs are in
general/open-gateway settings (no capture scenario), so the captured
scenarios show no change — settings-appearance 0.0136 (PR4's settings-nav
snap residual) and turn-narrative 0.0009 (the disclosure commit's residual);
commit 4 adds 0. The port inputs keep their look via the shared inputClasses.…review P2)
The disclosure migration made tool-activity's tool cards `defaultOpen={isOpenByDefault(item.status)}` (uncontrolled — open is decided once on first render). That regressed the pre-Collapsible `<details open={isOpenByDefault(status)}>` behavior, which re-evaluated open every render: a card that defaults open while pending/running no longer auto-collapsed when it settled to completed/interrupted.
Restore the status-following behavior with a controlled state machine:
- Extract the per-item card into a `ToolActivityCard` component (hooks can't
live in the `.map` callback).
- `useState(isOpenByDefault(item.status))` seeds open; `useEffect` re-syncs it
to `isOpenByDefault(item.status)` on every `[item.status]` change, so a
pending/running card collapses when it settles.
- `onOpenChange={setOpen}` lets the user still toggle the card in between;
the next status change re-syncs. (The pre-Collapsible `<details>` let the
user toggle too, but only as a side effect of React not re-rendering — the
state machine makes that intentional and predictable.)
TDD: disclosure-collapsible-contract gains an assertion that tool-activity
must NOT use `defaultOpen`, must use `onOpenChange`, and must re-sync via
`useEffect` on `[item.status]`. RED first (defaultOpen was present, no
onOpenChange, no re-sync), then GREEN. (maka has no React testing-library /
react-test-renderer, so the behavior is locked structurally + verified by
hand; the rerender-act path is out of scope.)
Verification: 2009/2009 desktop tests pass (+1 contract assertion). typecheck
clean. turn-narrative screenshot unchanged (0.09% — the disclosure commit's
summary→button residual; the card state is fixed by the scenario seed, and
controlled-open matches the pre-Collapsible behavior at that fixed state).review P3) Two unrelated P3 nits from review, both one-line-ish: 1. Collapsible barrel export was aliased `as PrimitiveCollapsible` (the old Tabs/Accordion convention), while the other new-in-PR5 primitives (Tooltip, NumberField) export directly. The disclosure-collapsible-contract regex allowed `@maka/ui` as an import path, but the barrel didn't actually export `Collapsible` under that name — a phantom-pass path. Export Collapsible/CollapsibleTrigger/CollapsiblePanel directly (drop the `Primitive*` aliases and the `CollapsiblePrimitive` re-export — nothing consumes them, and Tooltip/NumberField don't export their Primitive either). The internal sites keep importing via `./primitives/collapsible.js` (the in-package convention — alert/chat/etc. all use relative primitive paths), so the `@maka/ui` path is now genuinely available rather than phantom. 2. tooltip.tsx usage doc showed `<TooltipTrigger asChild>` (Radix), but this PR's convention is Base UI v1's `render` prop. Fix the example to `render={<Button />}` and note that the Trigger merges its props + children into the rendered element, so the next person doesn't copy the comment and hit the same typecheck failure the migration hit. Verification: 2009/2009 desktop tests pass. typecheck clean. No visual change (barrel export names + a doc comment).
The tooltip migration dropped the delete button's native `title="删除"` (the button is icon-only at rest — the visible 删除/删除中… label is visually hidden via .maka-artifact-toolbar-destructive-label, kept only for screen readers). The screen-reader path was preserved, but mouse users lost the hover hint. Wrap the delete button in a Base UI Tooltip (same shape as the artifact-pane collapse button already migrated in this PR): TooltipTrigger renders the destructive Button, TooltipContent mirrors the 删除/删除中… label for hover. The visually-hidden span still carries the SR label; the two are complementary, not redundant. Verification: 2009/2009 desktop tests pass. typecheck clean. turn-narrative screenshot unchanged (0.09% — the disclosure commit's residual; the delete button's tooltip is hover-only, and the button's rest appearance is unchanged).
…ew P2) The disclosure migration left the tool-card open-state divider pinned to the old native selector `[&[open]>summary]`. After migrating the card to Base UI Collapsible, the real DOM is a root with `[data-open]` and a button trigger with `data-slot="collapsible-trigger"`, so the old selector no longer matched and open tool-card headers could lose the body divider. Use the Base UI / ShadCN-style ownership instead of a root-owned child selector: - the card root exposes a named group (`group/tool`) alongside the Base UI `[data-open]` state, - the styled trigger/header part declares the divider with `group-data-[open]/tool:[border-bottom:1px_solid_var(--border)]`, - stale native-disclosure comments are removed. TDD: chat-tool-card-cascade-contract first failed because chat.tsx still kept `[open]>summary`; it now requires the named group + trigger-owned divider and bans the old selector from source/comments. Verification: related contracts pass (12/12), desktop tests pass (2009/2009), typecheck clean, renderer build succeeds and generated CSS contains the named group data selector.
… review P3) Base UI Collapsible.Trigger already exposes `data-panel-open` when its panel is open, so the previous named root group (`group/tool` + `group-data-[open]/tool`) was more machinery than this divider needs. Keep the Base UI / ShadCN ownership principle but make it smaller: the styled trigger/header part now owns the divider directly via `data-[panel-open]:[border-bottom:1px_solid_var(--border)]`, and the root drops `group/tool`. TDD: chat-tool-card-cascade-contract first failed because the source still used the root group path; it now bans both the old native `[open]>summary` selector and the root group open-state path, and requires the trigger-level `data-[panel-open]` divider. Verification: related contracts pass (12/12), desktop tests pass (2009/2009), typecheck clean, renderer build succeeds, generated CSS contains the `data-[panel-open]` utility and no `group-data-[open]/tool` / `group\/tool` selector. turn-narrative screenshot remains at the existing 0.09% disclosure residual.
After rebasing PR5 onto main, the old PR5 style-hook commit still introduced a SelectList wrapper, but main now has the ui.tsx dead-export contract and there is no consumer for SelectList. Keep the style-hook convention on used wrappers only and remove the dead export instead of adding a pending allowlist entry. Verification: style-hook/divider/disclosure contracts pass (13/13), desktop tests pass (2019/2019), typecheck clean.
4a3c541 to
92fa1e1Compare
Summary
PR5 of #520 — the style-hook convention foundation + the smaller Base UI migrations that follow it. Four items, one commit each (each independently revertible), TDD red → green → refactor.
#22 (input canonical) was scoped into PR5 but moved to PR8 during this work —
primitives/input.tsxcarries a different structure (data-slot="input-control"span + inner Base UI input,ring-[3px]) than the nativeui.tsxInput(single<input>+inputClasses,ring-2), andbare-field-chrome-contract.test.tspins the native'sdata-maka-field-chrome+border-input+ring-2, so retiring the native is not a clean look-preserving swap. The issue plan was re-arranged (#22 → PR8); this PR opens with 1-4 only.New primitives (
packages/ui/src/primitives/)collapsible.tsx@base-ui/react/collapsibleRoot/Trigger/Paneltooltip.tsx@base-ui/react/tooltipRoot/Trigger/Content (Portal+Positioner+Popup)number-field.tsx@base-ui/react/number-fieldRoot/InputAll follow the style-hook convention (item 23):
data-sloton each part, Base UI's native boolean state attributes (data-panel-open,data-open, etc.) adopted as-is. The Collapsible Trigger carries a button-chrome reset (the native<summary>had no bg/border/padding, but<button>does). The Tooltip Trigger uses Base UI v1'srenderprop (not RadixasChild). The NumberField Input reuses maka'sinputClasses(now exported fromui.tsx) so the port inputs keep their look.Convergence
data-slotadded to all 20 Base UI wrappers inui.tsx(Button/Separator/Checkbox/Dialog backdrop+popup/Select Trigger+List+Popup+Group+GroupLabel+Separator+Item/FieldDescription/Label/Switch/Toggle/ToggleGroup/RadioGroup/Radio/Progress). A doc comment at the top ofui.tsxrecords the state-attribute decision (adopt Base UI native attribute-presence form — maka's renderer CSS has zero state-attribute selectors today, so nothing breaks), the per-component hook map, the whitelisted CSS var hooks, and theclassName(state)deferral.<details>/<summary>sites (chat-view turn-thinking + reasoning-panel, permission-dialog permission-raw, tool-activity) migrate to Base UI Collapsible (all independent single sections, so Collapsible not Accordion). CSS retargeted:summary→[data-slot="collapsible-trigger"],[open]→[data-panel-open],::-webkit-details-marker/::markerresets dropped (button trigger has no native marker). The reasoning-panel's controlled-open moves frome.currentTarget.openoff the toggle event to Collapsible'sopen+onOpenChange.title=usages (app-shell-chrome-actions 7, browser-panel 4, artifact-pane collapse) migrate to Base UI Tooltip;aria-labelstays on the button,TooltipContentis the visual hover hint. The artifact-pane delete button's redundanttitle=is dropped (it shows a visible 删除 label). The longer tail (truncation spans, SelectTrigger titles, status badges, OnboardingHero submit) are tooltip-eligible but need per-site label-vs-tooltip judgment — deferred to a follow-up.value: number | null,onValueChange), dropping theNumber(event.currentTarget.value)hand-conversion. Proxy: 0 ↔ empty; gateway: empty → 3939 default, both preserved.New contracts
style-hook-convention-contract.test.ts<Base*>withoutdata-slotdisclosure-collapsible-contract.test.ts<details>/<summary>(incl. in comments) in the 3 migrated files; missing Collapsible importprimitives/collapsible.tsxdata-slot on Root/Trigger/Paneltooltip-converge-contract.test.tstitle=JSX attribute in the 3 migrated files; missing Tooltip importprimitives/tooltip.tsxdata-slot on Root/Trigger/Contentnumber-field-converge-contract.test.tsNumber(event.currentTarget.value)hand-conversion in the 2 port files; missing NumberField importprimitives/number-field.tsxdata-slot on Root/InputExisting tests updated
border-width-converge-contract: the triangle-caret allowlist selectors moved fromsummary::beforeto[data-slot="collapsible-trigger"]::before(the chevronborder-width: 4px 0 4px 5pxmoved with the disclosure migration).chat-marker-cascade-contract/chat-tool-card-cascade-contract: the turn-thinking / tool-card residue selectors + the dropped marker-reset residue.app-region-hygiene-contract/search-modal-lifecycle-contract: the topbar-button className extraction + the shell-search-button regex now match the<TooltipTrigger render={<UiButton/>}>shape (the className moved from the UiButton element to the TooltipTrigger that renders it).settings-network-gateway-contract: the two port-input assertions now pin the NumberField form (value={...|| null}/value={...}+onValueChange={(v) => update…({ port: v ?? default })).Commits
feat(ui): establish Base UI style-hook convention (data-slot + native state attrs)— item 23 (foundation).feat(ui): migrate disclosure sites to Base UI Collapsible— item 17.feat(ui): migrate icon-action tooltips to Base UI Tooltip— item 20.feat(ui): migrate port inputs to Base UI NumberField— item 21.Verification
npm run -w @maka/desktop test— 2008/2008 pass (rebased onto main with PR2; +26 from PR2, +10 new contract assertions across 4 files). typecheck clean.inputClasses.Out of scope / deferred