Skip to content

feat(ui): migrate chat turn markers onto @maka/ui Marker primitive (#332) - #337

Merged
Astro-Han merged 8 commits into
mainfrom
claude/chat-primitives-pr2
Jun 28, 2026
Merged

feat(ui): migrate chat turn markers onto @maka/ui Marker primitive (#332)#337
Astro-Han merged 8 commits into
mainfrom
claude/chat-primitives-pr2

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

PR2 of #332. Moves the per-turn status / lineage / footer chrome onto a new @maka/uiMarker chat primitive and retires the bespoke .maka-turn-* shell CSS. Zero visual change — same governance pass as PR1, not a redesign.

Migrated families: turn summary strip + chips (incl. the "切换" pill), aborted marker, failed banner (+ icon + recovery), lineage rows + badges (forward/reverse), and the footer + footer actions (also reused by the user-message copy via MessageCopyButton footerStyle).

Why

Closes one more slice of #332: collapse the last bespoke chat-flow island onto the shadcn Base UI + Tailwind substrate (one substrate + a contract-test net). Each retired declaration becomes a literal arbitrary utility that compiles 1:1 to what it replaced, so the cva source string is the computed-style proof — the cascade contract asserts the exact strings, no browser needed.

Scope

Changed:

  • packages/ui/src/primitives/chat.tsx — add Marker + a markerVariants cva (kept internal, see below). Literalized pixels / oklch relative-color tints / var() tokens; data-[kind|state|switched|direction|pending|copy-feedback] conditionals; ::before middot separators; [&:hover:not(:disabled)] / focus-visible / disabled / aria-disabled states. The measure-column geometry the old tool-output.css re-anchor applied to summary / lineage / footer is folded into those container variants (location-independent, no longer keyed on a [data-role=assistant] descendant).
  • scripts/check-chat-marker-computed-style.mjsnew, re-runnable headless-Electron before/after check (the rendered half of feat(ui): govern chat conversation-flow display onto @maka/ui Base UI primitives #332's lock): loads the real built renderer CSS from main and this branch and diffs getComputedStyle for every migrated element, in a DOM that mirrors TurnView nesting. Not production code — a verification script alongside diff-screenshots.mjs. It inlines the bundle into a <style> block (an earlier <link> form applied no CSS and was a vacuous 0-diff; see Verification). Its header documents what it can and can't observe (resting surface + ::before middots yes; the interactive :hover/:focus-visible pseudo-states no — getComputedStyle can't see a forced pseudo).
  • packages/ui/src/components.tsx — wire TurnSummary / TurnView / TurnFooterActions + the reused footer-style copy onto Marker / markerVariants. No new responsibilities added to the sink. The footer-action / lineage-badge call sites render as UiButton variant="quiet" size="nav" (the bare size), with the geometry size="sm" used to supply implicitly — h-8 (→30px), text-xs's 4/3 line-height (leading-[16px]/[12px]), and [border:0] — folded explicitly into the marker variants so the shell fully owns its own pixels instead of inheriting-then-overriding a size token. The footer action is now quiet in every state: the pending branch that used to switch the Button to variant="secondary" is dropped — the marker shell fully overrides the variant either way, so the switch was visually inert (proven below), and quiet is now the single, already-pinned merge path (no untested secondary merge, and no reliance on stylesheet source-order to suppress secondary's border / border-border). Pending still surfaces via data-pending / aria-busy and the full label+icon stay visible, exactly as before. The STATUS_FOOTER_PRIORITY map (all four actions were statically secondary) and its stale PR-UI-17 hover-expansion comment are removed; the priority variable and its data-priority attribute go with them — a grep shows no CSS / test / runtime consumer of data-priority, so it was a dead presentation hook (YAGNI).
  • Delete retired marker CSS in maka-tokens.css (summary), styles/settings/models.css (aborted / failed / lineage / footer), and the styles/tool-output.css measure-column re-anchor; each removal replaced with a one-line pointer comment.
  • Tests: new chat-marker-cascade-contract.test.ts; update the PR1 contract's footer re-anchor assertion and the copy-hygiene footer-state assertions to the primitive; add Marker hook tests + the non-leaf merge-resolution tests in @maka/ui.

Not included:

  • .maka-turn-thinking is deferred (decision surfaced per the issue): the committed-turn reasoning <details> styles itself via summary::before / ::-webkit-details-marker pseudo-elements and an @starting-style body fade that don't reduce to leaf utilities (the source-string == computed-style proof wouldn't hold), and maka-tokens.css already documents an intended Base UI Accordion path for it. It stays hand-written for that later effort.
  • The .maka-turn container, .maka-turn-tools, .maka-turn-streaming, and .maka-turn[data-search-highlight] are turn-container concerns, not markers — kept.
  • The broaderfooter-action / UiButton consolidation is deferred (reviewer P3, partial). Done here: size="sm""nav", and the footer action is now variant="quiet" in every state (the inert secondary pending branch is gone). What remains deferred is making the footer-action shell stop declaring its ownfocus-visible / hover pixels and instead inherit UiButton variant="quiet"'s. That would change the rendered result — the footer's focus-visible outline vs UiButton's ring, and its oklch(… / 0.05) hover tint vs quiet's bg-muted — so it's a visual redesign, not a zero-visual refactor, and belongs in the follow-up "consolidate the chat button story" PR that owns that decision. The remaining two-cva overlap is pre-existing (main already wrapped these in UiButton).

Verification

  • @maka/ui test: 10/10 (incl. 2 Marker hook tests + 2 non-leaf merge-resolution tests).
  • desktop node --test: 1595/1595 (incl. the new marker cascade contract, updated PR1 contract, updated copy-hygiene contract, and dead-CSS pruning contract).
  • npm run typecheck clean across workspaces.
  • Zero-visual proof — main vs head computed style (reviewer P2), now a committed re-runnable artifact:scripts/check-chat-marker-computed-style.mjs (checked in this PR) loads the real built renderer CSS from both main and this branch and reads getComputedStyle for the migrated chrome on each — footer action (resting + pending + copy-pending + copied + failed), forward/reverse lineage badge, summary strip, summary chips for every data-kind (model / tools / duration / tokens) + the in-progress data-state + the summary-switched "切换" pill in a data-switched chip, failed banner + recovery, footer toolbar, both the forward lineage-row and the lineage-row-reverse container, aborted marker (22 resting element/state rows), plus the ::before middot separators on a non-first summary chip and the failed-recovery (2 pseudo rows), in a DOM that mirrors TurnView nesting (chips in a summary, actions in a footer, badges in a lineage row) so positional pseudo-classes and inheritance resolve as in production, all under the same [data-slot=message][data-role=assistant] > .maka-turn ancestor so main's descendant re-anchors apply too. Result across 36 box/typography/color/transition properties per element row (4 per pseudo row): identical on every row — TOTAL DIFFS: 0, exits non-zero on any drift. Two findings the rendered diff caught that the source strings hid: (a) two latent deltas the old size="sm" form had masked (a text-xs line-height and border:0's style/color), now folded into the variants and pinned by the merge-resolution unit tests; (b) the pending footer action's secondary-vs-quiet Button variant is visually inert (the marker shell overrides it), so that branch is dropped — the footer-pending / footer-copy-pending rows verify main's old secondary path is pixel-equal to the new quiet one.
    • The script had to be fixed to mean anything (surfaced while extending coverage for reviewer P2): an earlier version <link>ed the CSS bundle from a data:/file: page, which a browser silently refuses to apply as a cross-origin subresource — so every element read its UA default identically on both sides and the 0-diff was vacuous. It now inlines the bundle into a <style> block of a file:// temp document; with the CSS actually applied, all 22 element rows read their real literalized values (e.g. footer-restborder-radius: 8px, summary-switchedoklch(0.17 0.005 75 / 0.06) bg / 11px / 600, lineage-row-reversemargin-top: 4px, the in-progress chip → accent oklch(0.7 0.135 152) + font-weight: 600), and the ::before middots resolve to a real content: "·" — both previously misattributed to harness limits that were really just the unloaded stylesheet.
    • Scope of the rendered proof (honest): it now covers the resting surface and the ::before middots for real. The only thing still not observable is the interactive pseudo-states — :hover / :focus-visible / :focus-within — because getComputedStyle does not reflect a DevTools CSS.forcePseudoState force (a known Chromium behavior: the force drives the inspector, not in-page computed style; verified resting == forced-"hover" even with the CSS applied), and a headless window has no live pointer. Those rules do compile into the bundle (greppable: …:hover:not(:disabled){background-color:oklch(… / .05)}) and are pinned by the cascade contract's exact source-string literals (focus-visible:[outline:2px_solid_var(--accent)], the hover tint) in chat-marker-cascade-contract.test.ts. The one genuinely non-leaf interactive case — does the footer action's hover/focus win the cn(buttonVariants, markerVariants) merge? — is a deterministic specificity fact, not a rendering question: the marker's [&:hover:not(:disabled)] (0,3,0) outranks UiButton quiet's hover:bg-muted (0,2,0), exactly as the retired .maka-turn-footer-action:hover:not(:disabled) did on main. So this script is a rendered proof of the resting surface + middots, with only the interactive states pinned by source string.
  • The non-leaf footer-action / lineage-badge (the only markers that render as UiButton) have their cn(buttonVariants({quiet,nav}), markerVariants(...)) merge pinned: the marker shell's pixels win, and the buttonVariants base/quiet conflicts (gap-2, rounded-md, text-muted-foreground) drop out.
  • .maka-turn-* is shared-by-file-location with styles/settings/models.css, but static analysis confirms no.maka-turn-summary/-aborted/-failed/-lineage/-footer className exists outside components.tsx (only doc comments), and TurnView renders only in the main chat — so deleting the CSS orphans nothing. (maka-tokens.css isn't scanned by check-dead-css.mjs; verified by grep + the contract test instead.)

User-facing impact

None — zero visual change, now proven by a re-runnable main-vs-head computed-style diff (0 deltas across 22 resting element/state rows × 36 properties + 2 ::before middot rows, with the CSS actually applied — see the inlining fix in Verification), and the interactive :hover/:focus-visible states pinned by the cascade contract source strings (which now also pin every chip data-kind conditional and the combined disabled/aria-disabled + data-pending opacity guards).

Reviewer notes

  • The faithful-translation claim rests on literalization: please sanity-check a couple of variant strings in chat.tsx against the deleted CSS rather than trusting the diff wholesale.
  • markerVariants is intentionally not re-exported from the @maka/ui barrel — only Marker + its types are public. The lineage badge / footer action import it by relative path, so the variant table stays an internal, freely-removable styling detail (the whole point of a governance pass). Contrast buttonVariants, which is public because it has external consumers.
  • When a footer copy button is simultaneously disabled and data-pending (transient, during a copy click), the pending 0.78 dim now outranks the disabled 0.45 dim by specificity — combined-modifier guards (disabled:data-[pending=true]:opacity-[0.78]) raise it to (0,3,0), so the value stays 0.78 (matching the retired CSS) regardless of Tailwind emit order, not by source order.

)
PR2 of issue #332. Move the per-turn status / lineage / footer chrome
(summary strip + chips, aborted marker, failed banner, lineage rows +
badges, footer + footer actions) onto a new @maka/ui `Marker` chat
primitive, retiring the bespoke `.maka-turn-*` shell CSS. Zero visual
change — each value is a literal arbitrary utility that compiles 1:1 to
the declaration it replaces, so the cva source string is the
computed-style proof.
- Add `Marker` + exported `markerVariants` cva to primitives/chat.tsx
(data-slot/data-* hooks; literalized pixels, oklch relative-color
tints, var() tokens; `data-[kind]`/`[direction]`/`[pending]`/
`[copy-feedback]` conditionals and `::before` middots).
- Wire TurnSummary / TurnView / TurnFooterActions + the reused
MessageCopyButton footer style onto Marker / markerVariants.
- Delete the retired marker shell CSS in maka-tokens.css, models.css,
and the tool-output.css measure-column re-anchor (folded into the
Marker container variants); comment each removal.
- Defer `.maka-turn-thinking`: its pseudo-element chevron + @starting-
style fade don't reduce to leaf utilities, and tokens already document
a Base UI Accordion path. Stays hand-written for that later effort.
- Add chat-marker-cascade-contract test; update the PR1 footer re-anchor
assertion and the copy-hygiene footer-state assertions to the
primitive; add Marker hook tests.
…riants internal
Review follow-ups on the Marker chat-primitive migration (#332 PR2):
- P3c: copy-in-progress sets both `disabled` and `data-pending`, where the
pending 0.78 dim and the disabled 0.45 dim are equal specificity (0,2,0) and
only resolved by Tailwind emit order. Add combined-modifier guards
(`disabled:`/`aria-disabled:data-[pending=true]:opacity-[0.78]`) so pending
wins at (0,3,0) by specificity, not source order. Computed value is unchanged
(still 0.78, matching the retired CSS) — zero visual change preserved.
- P3b: stop re-exporting `markerVariants` from the `@maka/ui` barrel. The only
consumers import it by relative path, so the variant table stays an internal,
freely-removable styling detail rather than public API. `Marker` + its types
stay public.
P3a (footer-action vs UiButton shell consolidation) is intentionally deferred —
it would change rendered focus/hover and break the PR's zero-visual contract;
tracked as a follow-up. P2 rendered verification (chat-chrome screenshot diff)
pending separately.
…+ lineage-badge
P2 follow-up on the Marker migration (#332 PR2). The summary/aborted/failed/
lineage-row/footer containers are pure leaves whose literal utilities compile
1:1, so the existing source-string contracts already prove their pixels. The
footer-action and lineage-badge are the ONLY non-leaf markers — they render as
`UiButton`, so the real on-element class is
`cn(buttonVariants({quiet,sm}), markerVariants(...))` and "source string ==
computed style" doesn't hold for free: tailwind-merge must drop the button's
conflicting shell utilities (px-2.5, rounded-md, text-xs, gap-2,
text-muted-foreground) so the retired `.maka-turn-*` pixels win.
This pins that merge resolution deterministically (no browser, no screenshot
rasterization noise — which the repo's own diff-screenshots.mjs documents as too
noisy to gate on). Verified out-of-band that the two cascade-order-decided
properties also match main: the footer/lineage `[transition:...]` arbitrary
shorthand emits after buttonVariants' longhand (so it wins, as it did unlayered
on main), and the pending-copy opacity is specificity-guarded (prior commit).
A captured turn-control-history screenshot confirms summary, forward (info) /
reverse (brand) lineage, the failed banner, and the footer toolbar all render
as before.
…="nav"
Reviewer P3. The footer-action / lineage-badge call sites rendered as
`UiButton variant="quiet" size="sm"`, so `sm` injected `h-8`/`px-2.5`/`text-xs`
that the marker shell then had to merge back out. `nav` is the bare size whose
docstring is exactly this case ("the consumer's className must set height /
padding / font-size … bespoke CSS that fights the standard size variants"), so
switch all four call sites to it and fold the geometry `sm` was implicitly
supplying explicitly into the footer-action / lineage-badge variants:
- `h-8` (→30px height),
- `leading-[16px]` / `leading-[12px]` — `text-xs`'s 4/3 line-height ratio over
the 12px / 9px fonts (12×4/3=16, 9×4/3=12, both exact), which `sm` leaked and
the bespoke CSS never set itself, and
- `[border:0]` replacing `border-0` — the faithful one-utility literalization of
the retired `border: 0` (width 0 AND style none AND color reset), not just the
width.
The marker shell now fully owns its geometry instead of inheriting-then-
overriding a size token. Verified zero visual change by computed style against
`main` (headless electron, real built renderer CSS, 9 elements covering
footer-action, forward/reverse lineage badge, summary, summary-chip, failed
banner, footer toolbar, lineage row, aborted marker): 33/33 properties identical
on every element, TOTAL DIFFS: 0. The merge contract shrinks accordingly — `nav`
no longer emits `px-2.5`/`text-xs`, so only the `buttonVariants` base/quiet
conflicts (`gap-2`, `rounded-md`, `text-muted-foreground`) remain to drop.
@maka/ui 10/10, desktop 1595/1595, typecheck clean.
…ted-style proof
The pending footer action switched UiButton to variant=secondary, but the
markerVariants('footer-action') shell fully overrides the variant, so the
switch was visually inert. Render it as variant=quiet in every state: this
removes the only untested non-leaf merge path and the only conflict that
relied on stylesheet source-order (secondary's border/border-border) rather
than tailwind-merge/specificity. Pending semantics (data-priority/-pending,
aria-busy, full label+icon) are unchanged.
Add scripts/check-chat-marker-computed-style.mjs: a re-runnable headless
before/after computed-style diff over the migrated chrome incl. footer
pending/copy states (13 rows x 36 props, TOTAL DIFFS: 0 vs main) — the
rendered half of #332's lock, committed so reviewers can re-check.
Refs #332
…ed proof honestly
P3: remove the STATUS_FOOTER_PRIORITY map (all four actions were statically
'secondary') and its stale PR-UI-17 hover-expansion comment; priority is now
the inline isActionPending ? 'primary' : 'secondary' that still feeds
data-priority. The MessageCopyButton comment already said size=nav (no change).
P2: the computed-style script now mirrors TurnView nesting (chips in a summary,
actions in a footer, badges in a lineage row) so positional pseudo-classes
resolve as in production. Its header is honest about scope: a headless
show:false harness cannot render :hover/:focus-visible (neither forcePseudoState
nor synthetic input changes getComputedStyle) nor Tailwind --tw-content ::before
(reads content:none), so those are locked by the cascade contract's exact
source-string literals — to which focus-visible:[outline:2px_solid_var(--accent)]
is now added. The non-leaf hover/focus merge winner is a deterministic
specificity fact ((0,3,0) > (0,2,0)), same as on main. Resting diff: 15 rows x
36 props, TOTAL DIFFS: 0 vs main.
Refs #332
… cover switched/reverse variants
Reviewer follow-up (1×P2 + 2×P3) on the #332 PR2 Marker migration.
P2 — extend the rendered proof to the two migrated variants it missed
(`summary-switched` "切换" pill, `lineage-row-reverse` container). While
adding them, sanity-checking the new rows surfaced that the ENTIRE proof
was a false green: the script `<link>`ed the CSS bundle from a data:/file:
page, which the browser silently refuses to apply as a cross-origin
subresource — so every element read its UA default identically on both
sides and TOTAL DIFFS: 0 meant nothing. Fix: inline the bundle into a
<style> block of a file:// temp document. With the CSS actually applied,
all 18 element rows now read their real literalized values and the
`::before` middots resolve to a real content:"·" — both previously
misattributed to harness limits that were really the unloaded stylesheet.
Folds the middots into the diff (2 pseudo rows). The only thing still not
observable is the interactive :hover/:focus-visible states (getComputedStyle
doesn't reflect a forced pseudo — a real Chromium behavior); those stay
pinned by the cascade contract's source-string literals + the specificity
fact. Header rewritten honestly.
P3 — delete `priority` / `data-priority` on the footer action: grep shows
no CSS/test/runtime consumer, so it was a dead presentation hook (YAGNI).
Pending still surfaces via data-pending / aria-busy + the visible label.
P3 — fix the stale `MessageCopyButton footerStyle` comment that still said
`size="sm"`; the call site has used `size="nav"` (bare) since this PR.
@maka/ui 10/10, desktop 1595/1595, typecheck clean; script 18 element rows
+ 2 ::before middots vs main = 0 diffs with the CSS genuinely applied.
…erals; render the kind chips
Reviewer follow-up (P3): the cascade/source-string net pinned only the
model / in-progress / base pending literals, leaving the other migrated
summary-chip conditionals and the combined disabled+pending opacity guards
unasserted. Test-only — no production change.
- chat-marker-cascade-contract.test.ts: pin data-[kind=tools] tint,
data-[kind=duration] / data-[kind=tokens] tabular-nums + tokens mono,
data-[state=in-progress]:font-semibold, and both
disabled:data-[pending=true]:opacity-[0.78] /
aria-disabled:data-[pending=true]:opacity-[0.78] guards. Deleting any now
fails the contract.
- check-chat-marker-computed-style.mjs: add rendered rows for the tools /
duration / tokens / in-progress chips (22 element rows now). With the CSS
inlined these read real values (in-progress → accent oklch(0.7 0.135 152)
+ weight 600, duration/tokens → tabular-nums), main vs head = 0 diffs.
desktop 1595/1595, typecheck clean, script 22 element + 2 ::before rows = 0.
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