blob: af97a065068e63d3cb013142e92cba671dd9365d [file]
/* ============================================================================
Chat message body
----------------------------------------------------------------------------
The MessageBody surface: message row shell, assistant bubble shell, the
hover copy button, the streaming caret, and the user-turn meta row.
Astryx owns Markdown prose, code-block chrome, and syntax colors.
Relocated from maka-tokens.css. Authored unlayered (the renderer styles/
contract requires it) but pulled into @layer components by
`@import ... layer(components)` in styles.css, so the rules keep the exact
cascade precedence they had in tokens. Astryx now owns the sender-aware row
and bubble chrome; the remaining selectors are product content and actions.
============================================================================ */
/* Astryx owns sender-aware row alignment; Maka only declares the product
reading geometry and the deliberately dense conversation type scale. */
.maka-chat-message {
width: 100%;
opacity: 1;
}
.maka-chat-message-list {
width: 100%;
}
/* Astryx's ChatMessageList renders a `flex: 1` spacer before its rows so a
short conversation sits on the composer instead of hanging from the top.
That spacer is unconditional, so on the empty surface it splits the column
with the empty-state holder — which is itself `flex: 1` and centres its
content — and the hero lands centred in the BOTTOM half: measured 172px
below the reading column's centre, plus the list's 16px gap.
Collapsing the spacer when the last row is an empty state hands the whole
column back to the holder, and its own centring is then true to the
surface. Keyed on the empty-state content rather than on "no messages"
because a single-message conversation must keep the push-to-bottom. */
.maka-chat-message-list
> *:has(> :last-child > :is(.maka-hero, .astryx-empty-state))
> [aria-hidden]:empty {
display: none;
}
/* The conversation reading tier is Astryx's body role: 14px with the 20px
4px-grid leading the type scale computes for it. The leading used to be a
hand-rolled 1.6154em (21px) that matched neither Maka's --leading-* tiers
nor Astryx's. */
.maka-chat-message-bubble {
font: var(--maka-text-body);
}
/* Padding and max-width belong to ChatMessageBubble; re-declaring those here
only forks the primitive. What stays is the product tint, the verbatim-text
rule (Maka renders user input as typed, which the primitive has no opinion
about) — and, since Visual System 2.0 (T3), the radius TIER.
The primitive resolves `--radius-container` (12px). DESIGN.md §6 assigns
radius by box height, and container is scoped to "modals, panels, portal
surfaces" — a message bubble is none of those. Measured in the chat fixture
the filled user bubble is 44px tall, which lands it in the card tier (10px,
"cards, rows-as-cards, list containers"). Astryx publishes no radius prop on
this component (only `variant`), so the tier is expressed on Maka's own
class rather than by reaching into the primitive's internals.
Scoped to the filled bubble on purpose: the assistant bubble is `ghost` (no
fill, measured transparent), so it has no corner to round and inherits
nothing it needs to override. */
.maka-chat-message-bubble-user {
background: var(--chat-user-bg);
color: var(--chat-user-foreground, var(--foreground));
border-radius: var(--radius-surface);
white-space: pre-wrap;
}
.maka-chat-message-bubble-assistant {
width: 100%;
max-width: none;
padding: var(--space-0-5) 0 0;
}
.maka-processing-sequence {
display: flex;
min-width: 0;
flex-direction: column;
gap: var(--space-1);
}
/* Expanded activity headers stay reachable while their own detail is being
read. Native sticky positioning keeps the header in the transcript flow,
so it leaves naturally at the card boundary and does not disturb ChatLayout
scroll anchoring while reasoning or tool output continues to stream. */
.maka-turn :is(.maka-deep-thinking, .maka-tool-activity-card) {
--maka-activity-sticky-top: var(--space-2);
--maka-activity-header-height: var(--h-control-sm);
}
.maka-turn .maka-activity-card-header[aria-expanded="true"],
.maka-turn .maka-tool-activity-card [role="button"][aria-expanded="true"] {
position: sticky;
top: var(--maka-activity-sticky-top);
z-index: var(--z-sticky);
background: var(--surface-raised);
border-radius: var(--radius-element);
box-shadow: 0 var(--border-width-hairline) 0 var(--border-soft);
}
/* A call detail inside an expanded tool group forms a second, bounded sticky
tier. The group remains available to collapse the whole run; the lower row
collapses only the detail currently being read.
Stock ChatToolCalls uses role=button on call rows (no data-slot patch). */
.maka-turn
.maka-tool-activity-card
> [role="button"][aria-expanded="true"]
~ div
[role="button"][aria-expanded="true"] {
top: calc(var(--maka-activity-sticky-top) + var(--maka-activity-header-height));
}
/* Astryx clips the animated group body with overflow:hidden. `clip` preserves
that visual boundary without becoming a scroll ancestor that captures the
nested sticky row. */
.maka-turn .maka-tool-activity-card > [role="button"] + div > div {
overflow: clip;
}
/* Cursor for tool/reasoning disclosure triggers is owned by
styles/native-cursor.css (role=button + default !important). */
.maka-turn .astryx-chat-tool-calls [role="button"] > span:nth-child(2) {
font: var(--maka-text-label);
}
/* Reasoning and tool-call rows read at BODY size, not supporting size.
Astryx puts the label and preview on inner spans styled as `supporting`
(12px on this scale). Measured on main they rendered at 9.75px — Astryx's
sm tier at 0.75rem against the old 13px root — which made them the least
legible text in the product, and they carry real content — what the agent is
thinking and which tool is running — not metadata.
Cursor makes the same call: its tool rows sit at body size and lean
entirely on --text-secondary/tertiary for de-emphasis. Weight and colour
already do that here, so size does not need to.
Rebinding the role token rather than restyling the spans. Astryx's
supporting atoms are `font-size: var(--text-supporting-size)` and
`line-height: var(--text-supporting-leading)`, so redefining those two on
the trigger reaches every span that opts into the role, at any depth,
through inheritance — no !important, since nothing else declares them
here, and no dependency on child order.
The earlier form of this rule selected `> span:not(:last-child)`, which
read as "every span except the chevron". It was wrong for exactly the
reason positional selectors are: ChatReasoning wraps its label, duration
and preview in a `<div>` (packages/ui/src/astryx-chat-reasoning.tsx), so
the rule enlarged the leading icon wrapper and left the reasoning text at
supporting size — the one row the retune was for. */
.maka-turn .astryx-chat-reasoning [role="button"],
.maka-turn .astryx-chat-tool-calls [role="button"] {
--text-supporting-size: var(--text-body-size);
--text-supporting-leading: var(--maka-line-body);
}
/* These boxes need no !important to beat Astryx: its StyleX atoms live in the
earlier astryx-components layer, so product CSS in `components` wins on
cascade order alone, whatever the specificity — measured, not assumed. */
.maka-turn .astryx-chat-reasoning [role="button"] > span:last-child,
.maka-turn .astryx-chat-tool-calls [role="button"] > span:last-child {
width: 14px;
height: 14px;
margin-inline-start: auto;
}
/* Both chevrons are the same registry icon, which wraps its svg in a span —
so the wrapper (`> *`) needs sizing too, not just the svg. */
.maka-turn :is(.astryx-chat-reasoning, .astryx-chat-tool-calls) [role="button"] > span:last-child svg,
.maka-turn :is(.astryx-chat-reasoning, .astryx-chat-tool-calls) [role="button"] > span:last-child > * {
width: 10px;
height: 10px;
}
@media (hover: hover) {
.maka-turn .astryx-chat-tool-calls [role="button"]:hover,
.maka-turn .astryx-chat-reasoning [role="button"]:hover {
background-color: var(--foreground-5);
border-radius: var(--radius-element);
}
}
/* Row detail open: same 0fr→1fr medium motion as group/reasoning.
The Astryx duration/ease below are deliberate, not a leak of the product
scale. Astryx ChatToolCalls animates its own group open with
`grid-template-rows --duration-medium` and its own easing; a detail row that
expands beside one has to share that clock or the two disclosures visibly
desync. Product motion belongs on the maka-tokens scale — this is the
exception that follows a neighbouring Astryx component, so retune it only
when ChatToolCalls moves. Astryx exports --duration-medium as a CSS var but
not its easing, so the curve is copied by value. */
@keyframes maka-chat-tool-detail-open {
from { grid-template-rows: 0fr; }
to { grid-template-rows: 1fr; }
}
.maka-chat-tool-detail {
display: grid;
grid-template-rows: 1fr;
animation: maka-chat-tool-detail-open var(--duration-medium, 300ms) cubic-bezier(0.24, 1, 0.4, 1) both;
}
.maka-chat-tool-detail-inner {
overflow: hidden;
min-height: 0;
}
@media (prefers-reduced-motion: reduce) {
.maka-chat-tool-detail {
animation: none;
}
}
/* --- Assistant bubble shell ----------------------------------------------- */
/* User message: a tinted Astryx bubble anchored to the right, sized by the
primitive's own compact-density geometry. */
/* Assistant: no bubble — just text on background, like an editor. The
Markdown typography and block rhythm belong to Astryx, so the shell keeps
only container geometry.
The reading measure is inherited from the conversation surface shared with
the composer, so the assistant block matches the composer width. */
/* The streaming ▎ caret + its trailing-<p> inline hack were retired by the
streaming UI rework. Chat smooths the visible text before it reaches
Markdown; PR 8 owns the remaining streaming and scroll-anchoring boundary. */
/* --- User-turn meta row (relocated from tool-output.css) ----------------- */
/* The time is an Astryx `Timestamp`, so the quiet register (supporting tier,
secondary colour) is the component's own default and is not restated here.
What stays is what the component does not decide:
- #1879: height off the control ruler, not off the line box. Measured then,
bumping this element's leading to 40px took the box from 20px to 40px and
carried the meta row with it. `--h-control-xs` is 20px, what the supporting
leading already resolves to, so the pin is pixel-neutral today and
load-bearing the next time a leading moves. Safe to pin because `nowrap`
makes single-line a property of this box rather than an assumption.
- `tabular-nums`, because `Timestamp`'s `time` format renders the hour with
`hour: 'numeric'` — no leading zero, so `9:05` and `14:32` differ by a
digit. The row is right-aligned, so that difference shows up on the left
edge; equal-width digits keep it to the one character it really is. */
.maka-message-time-inline {
height: var(--h-control-xs);
display: inline-flex;
align-items: center;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* User turn meta row: quiet time + a copy affordance, right-aligned
beneath the message block (the parent `Message role="user"` aligns its
items to flex-end, so this row shrink-wraps and hugs the right edge).
The row's own box is 28px — the height of the action buttons, not of the
12px time beside them — so it reads tighter against the bubble than its
4px top margin suggests. Astryx's default is nudged one step to 6px to give
the row back some air without opening a visible gap. */
.maka-message-meta {
display: inline-flex;
align-items: center;
gap: var(--space-1-5);
margin-top: var(--space-1-5);
}
/* Turn summary / lineage rows / footer measure column folded into the
`@maka/ui` Marker chat primitive's own variants (issue #332 PR2 —
packages/ui/src/primitives/chat.tsx); the layout is now location-
independent instead of keyed on a `[data-role="assistant"]` descendant. */
/* --- Conversation heading scale ------------------------------------------- */
/* Moved to the transcript rhythm table in packages/ui/src/styles.css. The
argument for flattening the document ladder is unchanged; what changed is
where it is expressed. These rules keyed on a `.maka-turn` ancestor, which
made the heading scale a function of DOM position: the same MarkdownBody
rendered document-sized headings in the Daily Review and flat ones in a
turn, and no Storybook story could reproduce the transcript without
remembering to wrap one. The rhythm table keys on Astryx's own
`data-density` instead, so the scale follows the `density` prop that already
distinguishes the two surfaces. It also keeps two size steps rather than
one — h2 and h3 used to be identical in size, weight and colour. */
.maka-chat-message pre:not([data-maka-contract="markdown"] pre) {
margin: 0;
font: inherit;
}
/* System-note pill. Markdown descendants are excluded through Maka's product
boundary so raw-pre treatment cannot become a second code-block authority. */
.maka-chat-message[data-sender="system"]
pre:not([data-maka-contract="markdown"] pre) {
font: var(--maka-text-supporting);
display: inline-flex;
border-radius: var(--radius-pill);
background: oklch(from var(--info) l c h / 0.08);
color: var(--info-text);
padding: var(--space-0-5) var(--space-1-5);
}