Uh oh!
There was an error while loading. Please reload this page.
feat(ui): migrate chat turn markers onto @maka/ui Marker primitive (#332) - #337
Merged
Conversation
) 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.
This was referenced Jun 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR2 of #332. Moves the per-turn status / lineage / footer chrome onto a new
@maka/uiMarkerchat 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— addMarker+ amarkerVariantscva (kept internal, see below). Literalized pixels / oklch relative-color tints /var()tokens;data-[kind|state|switched|direction|pending|copy-feedback]conditionals;::beforemiddot separators;[&:hover:not(:disabled)]/focus-visible/disabled/aria-disabledstates. The measure-column geometry the oldtool-output.cssre-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.mjs— new, 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 frommainand this branch and diffsgetComputedStylefor every migrated element, in a DOM that mirrorsTurnViewnesting. Not production code — a verification script alongsidediff-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 +::beforemiddots yes; the interactive:hover/:focus-visiblepseudo-states no —getComputedStylecan't see a forced pseudo).packages/ui/src/components.tsx— wireTurnSummary/TurnView/TurnFooterActions+ the reused footer-style copy ontoMarker/markerVariants. No new responsibilities added to the sink. Thefooter-action/lineage-badgecall sites render asUiButton variant="quiet" size="nav"(the bare size), with the geometrysize="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 nowquietin every state: the pending branch that used to switch the Button tovariant="secondary"is dropped — the marker shell fully overrides the variant either way, so the switch was visually inert (proven below), andquietis now the single, already-pinned merge path (no untestedsecondarymerge, and no reliance on stylesheet source-order to suppresssecondary'sborder/border-border). Pending still surfaces viadata-pending/aria-busyand the full label+icon stay visible, exactly as before. TheSTATUS_FOOTER_PRIORITYmap (all four actions were staticallysecondary) and its stale PR-UI-17 hover-expansion comment are removed; thepriorityvariable and itsdata-priorityattribute go with them — a grep shows no CSS / test / runtime consumer ofdata-priority, so it was a dead presentation hook (YAGNI).maka-tokens.css(summary),styles/settings/models.css(aborted / failed / lineage / footer), and thestyles/tool-output.cssmeasure-column re-anchor; each removal replaced with a one-line pointer comment.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; addMarkerhook tests + the non-leaf merge-resolution tests in@maka/ui.Not included:
.maka-turn-thinkingis deferred (decision surfaced per the issue): the committed-turn reasoning<details>styles itself viasummary::before/::-webkit-details-markerpseudo-elements and an@starting-stylebody fade that don't reduce to leaf utilities (the source-string == computed-style proof wouldn't hold), andmaka-tokens.cssalready documents an intended Base UI Accordion path for it. It stays hand-written for that later effort..maka-turncontainer,.maka-turn-tools,.maka-turn-streaming, and.maka-turn[data-search-highlight]are turn-container concerns, not markers — kept.footer-action/UiButtonconsolidation is deferred (reviewer P3, partial). Done here:size="sm"→"nav", and the footer action is nowvariant="quiet"in every state (the inertsecondarypending branch is gone). What remains deferred is making thefooter-actionshell stop declaring its ownfocus-visible/hoverpixels and instead inheritUiButton variant="quiet"'s. That would change the rendered result — the footer'sfocus-visibleoutline vsUiButton'sring, and itsoklch(… / 0.05)hover tint vs quiet'sbg-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 (mainalready wrapped these inUiButton).Verification
@maka/uitest: 10/10 (incl. 2Markerhook tests + 2 non-leaf merge-resolution tests).node --test: 1595/1595 (incl. the new marker cascade contract, updated PR1 contract, updated copy-hygiene contract, and dead-CSS pruning contract).npm run typecheckclean across workspaces.mainvs 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 bothmainand this branch and readsgetComputedStylefor the migrated chrome on each — footer action (resting + pending + copy-pending + copied + failed), forward/reverse lineage badge, summary strip, summary chips for everydata-kind(model / tools / duration / tokens) + the in-progressdata-state+ thesummary-switched"切换" pill in adata-switchedchip, failed banner + recovery, footer toolbar, both the forwardlineage-rowand thelineage-row-reversecontainer, aborted marker (22 resting element/state rows), plus the::beforemiddot separators on a non-first summary chip and the failed-recovery (2 pseudo rows), in a DOM that mirrorsTurnViewnesting (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-turnancestor somain'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 oldsize="sm"form had masked (atext-xsline-height andborder:0's style/color), now folded into the variants and pinned by the merge-resolution unit tests; (b) the pending footer action'ssecondary-vs-quietButton variant is visually inert (the marker shell overrides it), so that branch is dropped — thefooter-pending/footer-copy-pendingrows verifymain's oldsecondarypath is pixel-equal to the newquietone.<link>ed the CSS bundle from adata:/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 afile://temp document; with the CSS actually applied, all 22 element rows read their real literalized values (e.g.footer-rest→border-radius: 8px,summary-switched→oklch(0.17 0.005 75 / 0.06)bg /11px/600,lineage-row-reverse→margin-top: 4px, the in-progress chip → accentoklch(0.7 0.135 152)+font-weight: 600), and the::beforemiddots resolve to a realcontent: "·"— both previously misattributed to harness limits that were really just the unloaded stylesheet.::beforemiddots for real. The only thing still not observable is the interactive pseudo-states —:hover/:focus-visible/:focus-within— becausegetComputedStyledoes not reflect a DevToolsCSS.forcePseudoStateforce (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) inchat-marker-cascade-contract.test.ts. The one genuinely non-leaf interactive case — does the footer action's hover/focus win thecn(buttonVariants, markerVariants)merge? — is a deterministic specificity fact, not a rendering question: the marker's[&:hover:not(:disabled)](0,3,0) outranks UiButton quiet'shover:bg-muted(0,2,0), exactly as the retired.maka-turn-footer-action:hover:not(:disabled)did onmain. So this script is a rendered proof of the resting surface + middots, with only the interactive states pinned by source string.footer-action/lineage-badge(the only markers that render asUiButton) have theircn(buttonVariants({quiet,nav}), markerVariants(...))merge pinned: the marker shell's pixels win, and thebuttonVariantsbase/quiet conflicts (gap-2,rounded-md,text-muted-foreground) drop out..maka-turn-*is shared-by-file-location withstyles/settings/models.css, but static analysis confirms no.maka-turn-summary/-aborted/-failed/-lineage/-footerclassName exists outsidecomponents.tsx(only doc comments), andTurnViewrenders only in the main chat — so deleting the CSS orphans nothing. (maka-tokens.cssisn't scanned bycheck-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::beforemiddot rows, with the CSS actually applied — see the inlining fix in Verification), and the interactive:hover/:focus-visiblestates pinned by the cascade contract source strings (which now also pin every chipdata-kindconditional and the combineddisabled/aria-disabled+data-pendingopacity guards).Reviewer notes
chat.tsxagainst the deleted CSS rather than trusting the diff wholesale.markerVariantsis intentionally not re-exported from the@maka/uibarrel — onlyMarker+ 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). ContrastbuttonVariants, which is public because it has external consumers.disabledanddata-pending(transient, during a copy click), the pending0.78dim now outranks the disabled0.45dim by specificity — combined-modifier guards (disabled:data-[pending=true]:opacity-[0.78]) raise it to(0,3,0), so the value stays0.78(matching the retired CSS) regardless of Tailwind emit order, not by source order.