From 44288a9d14168766eb1c63b61eafaf3c8e1f4821 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 21 Aug 2026 23:22:11 +0800 Subject: [PATCH 1/3] fix(ui): give transcript paragraphs a full block step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two paragraphs in a chat turn sat 8px apart against 20px body leading, so a paragraph break was 0.4 of a line — narrower than the line break inside a paragraph, which is the one vertical distance a reader already knows. A long answer read as one slab. Take the block rung to 12px, which is Astryx's own paragraph rhythm at document density (spacingParagraphDefault, --spacing-3). The transcript keeps its own heading scale and its own 4px list rung and only stops being denser than the design system between two paragraphs. Where the fix does NOT belong, because the obvious reading is wrong: `density="compact"` in chat-turn.tsx has not owned transcript block spacing since #2348. That table zeroes every Astryx margin on the document's children and declares the four gaps itself, in @layer components — a Markdown density token, or a makaTheme.ts override of `astryx-markdown-paragraph`, lands in an earlier layer and is inert. `--md-gap-block` is the only authority, so the call site now says so rather than leaving the next reader to re-derive it from commit history. One rung moves, so code blocks and blockquotes move with it: they are already carried by the same `> * + *` step (Astryx skips its codeblock wrapper whenever `components.code` is set, which Maka always does), and splitting a per-element rung out would re-introduce the multi-value spacing this table exists to collapse. What that costs is contrast at the top of the ladder — the section step falls from 2x the block gap to 1.33x — and the heading rungs stay put anyway, because heading spacing is #1857's decision and not this change's business. Measured in Storybook against live computed styles, before and after, on Product/Markdown → TranscriptTurn and Product/Shell Official AppShell → Native Conversation: list rows 4px 4px unchanged blocks 8px 12px paragraph, list, quote, code block section 16px 16px unchanged (h3-h6) chapter 24px 24px unchanged (h1, h2) Daily Review renders at density="default" and is out of the selector, so its ladder is byte-for-byte what it was: 12/12 paragraphs, 16/16 quote and table, straight from Astryx. Generated-by: Claude Code --- packages/ui/src/chat-turn.tsx | 7 +++++++ packages/ui/src/styles.css | 31 ++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx index 33faff1601..53dd1574e3 100644 --- a/packages/ui/src/chat-turn.tsx +++ b/packages/ui/src/chat-turn.tsx @@ -1104,6 +1104,13 @@ const AssistantAnswerBubble = memo(function AssistantAnswerBubble(props: Assista text={props.text} streaming={props.phase === 'streaming'} settledText={settledText} + // Names the surface; it does NOT set this turn's block spacing. Every + // top-level gap in a transcript turn comes from the rhythm table in + // styles.css, which keys on the `data-density="compact"` this prop + // reflects and overrides Astryx's own margins outright. So `compact` + // still buys the transcript heading scale and the tighter rhythm + // inside a list item or a quote, and reading it as "paragraphs are + // squeezed here" is the wrong file — retune `--md-gap-block` instead. density="compact" /> {truncated && ( diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index 63bb5dcca7..4b7e5dfc1a 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -193,10 +193,27 @@ by owning every compact prose gap in one place: 4px list items (same level) - 8px blocks (paragraph, list, quote, table, code) + 12px blocks (paragraph, list, quote, table, code) 16px section (h3-h6, and either side of an `hr`) 24px chapter (h1, h2) + Why the block rung is 12px and not the 8px this table first shipped: 8px + restored the ORDER the table exists to fix, but never argued its value. + Body leading here is 20px, so 8px put two paragraphs 0.4 of a line apart — + a paragraph break narrower than the line break inside a paragraph, which is + the one distance a reader already knows. 12px is Astryx's own paragraph + rhythm at document density (spacingParagraphDefault, `--spacing-3`), so this + is the block step returning to the design system rather than a Maka number: + the transcript keeps its own HEADING scale and its own list rung, and only + stops being denser than the design system between two paragraphs. Measured + in Storybook (Product/Markdown → TranscriptTurn) rather than estimated. + + It costs contrast at the top of the ladder — the section step falls from 2x + the block gap to 1.33x, so a heading now separates mainly on weight, size + and colour rather than on space. That is the price of leaving the heading + rungs alone, which is deliberate: heading spacing is what #1857 and this + table already decided, and this change is about paragraphs only. + Three deliberate choices: - Only `[data-density="compact"]`. The document mode is not broken (its @@ -211,11 +228,11 @@ old ancestor-scoped rules could not. - Top level only. `>` keeps every gap on the document's own children, so blocks nested inside a list item or a blockquote keep Astryx's own compact - 4px instead of the 8px block gap. That tier difference is the point, not - an oversight: a list item should read as one unit, so two paragraphs - inside it belong closer together than two paragraphs in the transcript. - The ladder extends downward (4px nested < 8px block) rather than - inverting, which is the invariant that matters. + 4px instead of the block gap. That tier difference is the point, not an + oversight: a list item should read as one unit, so two paragraphs inside + it belong closer together than two paragraphs in the transcript. The + ladder extends downward (4px nested < 12px block) rather than inverting, + which is the invariant that matters. A gap is a relation BETWEEN two blocks, so every gap rule is an adjacent sibling rule. Two consequences, both load-bearing: @@ -232,7 +249,7 @@ two blocks is the value declared here. */ [data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] { --md-gap-list: var(--space-1); - --md-gap-block: var(--space-2); + --md-gap-block: var(--space-3); --md-gap-section: var(--space-4); --md-gap-chapter: var(--space-6); } From c43fbd0a5b4c3ea1ca7fe69342b0bce9fd972728 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 21 Aug 2026 23:34:28 +0800 Subject: [PATCH 2/3] test(ui): guard the two silent ways the rhythm table breaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit styles.css told readers the "compact == transcript" assumption was held by `__tests__/markdown-rhythm-dom-contract.test.tsx`. That file was deleted in #2462 and its stylesheet-side sibling in #2425, so the comment has been citing a guard that does not exist — the worst state to leave an assumption in, since the next person reads the citation and stops looking. Restore only what cannot be noticed without a test. Both failure modes here are silent: no error, no failing check, just spacing that quietly stops being what the table declares, because a CSS selector that matches nothing never complains. - The table selects entirely on DOM Astryx generates at runtime — data-density, astryx-markdown-heading + data-level, astryx-list-item. Those names have one upstream owner and appear in Maka only inside selectors, so a rename kills every rule at once. Astryx is bumped regularly (0.4.0 in #2983, 0.4.3 in flight, plus the Dependabot minor group), so this is a recurring event, not a hypothetical. - Astryx's ListItem carries CONTROL row padding that `density` cannot reach from outside. That padding is what inverted the ladder in #2348 — list items ~10px apart against 4px paragraphs — and one rule neutralizes it. Lose the rule and the original defect returns, silently. - Plus the assumption the comment actually names: compact markdown still has exactly one caller. The table carries heading TYPOGRAPHY on a density key, which Astryx's own RFC says density must not do, so the key is honest only while `compact` and "transcript" are the same set. Deliberately NOT pinned: the ladder's declared values and their order. A first draft asserted them, and it was the wrong instinct — a reversed ladder has to be typed on purpose into four adjacent lines under a comment explaining the order, and it is visible the moment anyone looks at a transcript. It also would not have caught the defect it claimed to guard: #2348's inversion came from the ListItem padding above, which a test reading only the declared variables cannot see. Same for the adjacent-sibling gap form, the `hr` rung and the two heading size tiers — those are how the table is written, not what it promises. Why this is not what #2425/#2462 retired. Those PRs removed markup/copy pins, duplicate presentation assertions, and CSS-structure suites that asserted how a rule was written. Two of the three assertions here render the real component and check the join between Astryx's output and Maka's selectors; the third asserts an outcome the table promises, not a syntax. The stylesheet-reading half does not revive the deleted desktop css-test-helpers module or the renderer-wide CSS aggregate it read — it reads packages/ui/src/styles.css directly, in the package that owns it. Six mutations, each verified to fail the assertion that describes it: deleting the ListItem padding reset; un-zeroing it; un-zeroing it while naming the reset in a comment (this is why comments are stripped first); renaming the contract wrapper; cutting density off before it reaches Astryx; and adding a second compact caller. Generated-by: Claude Code --- .../markdown-rhythm-contract.test.tsx | 165 ++++++++++++++++++ packages/ui/src/styles.css | 16 +- 2 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 packages/ui/src/__tests__/markdown-rhythm-contract.test.tsx diff --git a/packages/ui/src/__tests__/markdown-rhythm-contract.test.tsx b/packages/ui/src/__tests__/markdown-rhythm-contract.test.tsx new file mode 100644 index 0000000000..356ba1b082 --- /dev/null +++ b/packages/ui/src/__tests__/markdown-rhythm-contract.test.tsx @@ -0,0 +1,165 @@ +/** + * Transcript markdown rhythm contract. + * + * Two failure modes, both SILENT — no error, no failing check, just spacing + * that quietly stops being what the table declares. That is the whole reason + * this file exists; a CSS selector that matches nothing never complains. + * + * 1. The table selects entirely on DOM Astryx generates at runtime — + * `data-density`, `astryx-markdown-heading` + `data-level`, + * `astryx-list-item`. Those names have a single upstream owner and appear + * in Maka only inside selectors, so an Astryx rename kills every rule at + * once. Astryx is bumped regularly (0.4.0 in #2983, 0.4.3 in flight, plus + * the Dependabot minor group), so this is a recurring event rather than a + * hypothetical. + * + * 2. Astryx's ListItem carries CONTROL row padding that `density` cannot reach + * from outside. That padding is what inverted the ladder in the first place + * (#2348: list items ~10px apart against 4px paragraphs, so same-level + * items read as further apart than separate paragraphs), and one rule + * neutralizes it. Lose that rule and the original defect returns. + * + * Deliberately NOT pinned: the ladder's declared VALUES and their order. A + * reversed ladder has to be typed on purpose into four adjacent lines under a + * comment that explains the order, and it is visible the moment anyone looks + * at a transcript — unlike the two above, which are invisible until someone + * measures. Same for the adjacent-sibling gap form, the `hr` rung, and the two + * heading size tiers: those are how the table is written, not what it + * promises. + */ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { readdir, readFile } from 'node:fs/promises'; +import { join, relative, resolve } from 'node:path'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { MarkdownBody } from '../markdown-body.js'; + +const UI_SRC = resolve(import.meta.dirname, '..', '..', 'src'); + +async function tsxFiles(dir: string): Promise { + const out: string[] = []; + for (const entry of await readdir(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) out.push(...(await tsxFiles(path))); + else if (entry.name.endsWith('.tsx') && !entry.name.includes('.test.')) out.push(path); + } + return out; +} + +const SAMPLE = ['## Heading two', '', 'A paragraph.', '', '1. First item', '2. Second item'].join('\n'); + +function compactMarkup(): string { + return renderToStaticMarkup(); +} + +describe('transcript markdown rhythm', () => { + it('emits the DOM hooks the rhythm table selects on', () => { + const markup = compactMarkup(); + // Every rule in the table is prefixed with this, so it is the one hook + // whose two halves — selector prefix and runtime attribute — nothing else + // joins. Rename it and all compact spacing dies silently. + assert.match( + markup, + /data-maka-contract="markdown"/, + 'the `data-maka-contract="markdown"` wrapper is gone. Every rule in the rhythm table ' + + 'is scoped on it, so all compact prose spacing and the heading scale are now dead.', + ); + assert.match( + markup, + /]*role="document"[^>]*data-density="compact"|]*data-density="compact"[^>]*role="document"/, + 'the document root no longer carries `data-density="compact"`. Every rule in the ' + + 'rhythm table is scoped on it, so all compact prose spacing is now dead.', + ); + assert.match( + markup, + /class="[^"]*\bastryx-markdown\b/, + 'the document root no longer carries the `astryx-markdown` class the table selects on', + ); + assert.match( + markup, + /]*class="[^"]*\bastryx-markdown-heading\b/, + 'headings no longer carry `astryx-markdown-heading`; the transcript heading scale is dead', + ); + assert.match( + markup, + /]*data-level="2"/, + 'headings no longer carry `data-level`. The table splits h1/h2 from h3-h6 on it, so ' + + 'without it every heading collapses to one tier — the exact defect #2348 replaced.', + ); + assert.match( + markup, + /class="[^"]*\bastryx-list-item\b/, + 'list rows no longer carry `astryx-list-item`, so the rule below cannot reach them. ' + + 'If markdown lists stopped rendering through Astryx `List` that is good news, but ' + + 'the list rhythm needs re-measuring rather than silently inheriting whatever ' + + 'replaced it.', + ); + }); + + /** + * The other half of the join above: the class is emitted AND something + * spends its control padding. Markdown hands its list a hardcoded + * `density="compact"` in both modes, so the row keeps a control's block + * padding no matter what the caller asks for — the real list-item gap is + * `padding + gap`, and only zeroing the padding makes `--md-gap-list` the + * whole distance between two items. + */ + it('neutralizes the ListItem control padding the ladder inverted on', async () => { + const css = (await readFile(join(UI_SRC, 'styles.css'), 'utf8')).replace(/\/\*[\s\S]*?\*\//g, ''); + const rule = new RegExp( + String.raw`\.astryx-markdown\[data-density="compact"\][^{]*\.astryx-list-item\s*\{([^}]*)\}`, + ).exec(css); + assert.ok( + rule, + 'the compact surface no longer neutralizes `.astryx-list-item` padding. Astryx spaces ' + + 'a markdown list as a clickable row, which is what made same-level items read as ' + + 'further apart than separate paragraphs (#2348).', + ); + assert.match( + rule[1], + /padding-block\s*:\s*0/, + 'ListItem block padding is no longer zeroed on the compact surface, so the real ' + + 'list-item gap is padding + gap and the declared ladder is not the spacing you get. ' + + `Found: { ${rule[1].trim()} }`, + ); + }); + + /** + * The rhythm table carries the transcript's heading TYPOGRAPHY, not just its + * spacing, and keys both on `density`. That is a deliberate bet, and it is a + * bet: Astryx's density RFC (facebook/astryx#839) draws the line at "density + * shifts heights and spacing, not typography", so the key is honest only + * while `compact` and "transcript" name the same set. They do today — the + * transcript is the only caller asking for compact, and the Daily Review + * renders a document at the default. + * + * A separate surface attribute would decouple them, but nothing needs it yet + * and it would add a prop to a public component for a caller that does not + * exist. Guard the assumption instead: the moment a second surface asks for + * compact markdown it silently inherits transcript heading sizes and dimmed + * deep headings, and this is what tells whoever adds it that they have to + * choose — inherit deliberately, or split the key then. + */ + it('keeps compact markdown a transcript-only surface', async () => { + const callers: string[] = []; + for (const file of await tsxFiles(UI_SRC)) { + const source = await readFile(file, 'utf8'); + // `` opening tags only — not MarkdownBody's internal plumbing + // and not the density Maka hands its own code-block renderers. + for (const tag of source.matchAll(/]*?\/?>/gs)) { + if (/density=(["'])compact\1/.test(tag[0])) callers.push(relative(UI_SRC, file)); + } + } + + assert.deepEqual( + [...new Set(callers)].sort(), + ['chat-turn.tsx'], + 'a new caller renders markdown at compact density. The rhythm table treats ' + + '`density="compact"` as "this is a transcript" and gives it flattened heading sizes ' + + 'plus secondary-colour h4-h6 — typography, which Astryx\'s density is explicitly not ' + + 'supposed to carry (facebook/astryx#839). Either that is what the new surface wants, ' + + 'and this list grows, or the typography rules need their own key. ' + + `Found: ${JSON.stringify([...new Set(callers)].sort())}`, + ); + }); +}); diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index 4b7e5dfc1a..df96fdab6e 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -214,6 +214,16 @@ rungs alone, which is deliberate: heading spacing is what #1857 and this table already decided, and this change is about paragraphs only. + What holds this: __tests__/markdown-rhythm-contract.test.tsx, and it pins + only the two ways this table can break SILENTLY — the runtime hooks these + selectors need still being emitted (an Astryx rename kills every rule at + once, and a selector that matches nothing never complains), and the + ListItem control padding still being neutralized (the mechanism that + inverted the ladder in the first place). The values below are NOT pinned, + deliberately: a wrong number is visible the moment anyone looks at a + transcript, so it does not need a test to be noticed — which is not true of + either of the other two. + Three deliberate choices: - Only `[data-density="compact"]`. The document mode is not broken (its @@ -312,9 +322,9 @@ second compact surface would silently inherit transcript heading sizes and dimmed deep headings. Giving typography its own surface attribute would decouple them, but nothing needs it yet; the assumption is held by a test - instead (packages/ui/src/__tests__/markdown-rhythm-dom-contract.test.tsx, - "keeps compact markdown a transcript-only surface"), which fails the moment - the sets diverge and says what the choice is. */ + instead (__tests__/markdown-rhythm-contract.test.tsx, "keeps compact + markdown a transcript-only surface"), which fails the moment the sets + diverge and says what the choice is. */ [data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading:is([data-level="1"], [data-level="2"]) { font: var(--maka-text-heading-3); } From d8b2189e376397562ea6641a8bf27923f99250c6 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 22 Aug 2026 00:55:49 +0800 Subject: [PATCH 3/3] fix(ui): stop claiming compact markdown is transcript-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review on #3436 caught the caller-scan test asserting something already false. Two surfaces render MarkdownBody at compact density — the transcript and the desktop Artifact Preview (apps/desktop, since #2506) — and the test passed anyway, for two independent reasons: it walked only packages/ui/src, and its `` form the second caller uses. Either bug alone would have surfaced the other caller. Delete it rather than widen it. The set it wants to pin spans workspaces, so holding it means a library test grepping application source — the dependency backwards — and the mechanism already proved it can be wrong and silent on its first commit. A guard that lies is worse than a documented gap. What the deletion gives up is recorded where the sharing matters, next to the heading rules: compact carries transcript TYPOGRAPHY, which Astryx's density RFC says density should not, so a third compact surface inherits it with nothing to catch that. Left shared deliberately for the preview — it is a narrow pane showing a document a few lines at a time, the shape the flattening was built for. Splitting the key is a visible change to that surface and belongs in its own PR, argued on its own screenshots. The 12px block rung reaches the preview too, and there it is a move toward the design system rather than away: 12px is Astryx's own document paragraph rhythm. The two remaining assertions are untouched — both read rendered DOM and the stylesheet itself, so neither can be wrong about scope. Generated-by: Claude Code --- .../markdown-rhythm-contract.test.tsx | 60 +++---------------- packages/ui/src/chat-turn.tsx | 17 +++--- packages/ui/src/styles.css | 37 +++++++++--- 3 files changed, 47 insertions(+), 67 deletions(-) diff --git a/packages/ui/src/__tests__/markdown-rhythm-contract.test.tsx b/packages/ui/src/__tests__/markdown-rhythm-contract.test.tsx index 356ba1b082..7e6c19a972 100644 --- a/packages/ui/src/__tests__/markdown-rhythm-contract.test.tsx +++ b/packages/ui/src/__tests__/markdown-rhythm-contract.test.tsx @@ -26,26 +26,23 @@ * measures. Same for the adjacent-sibling gap form, the `hr` rung, and the two * heading size tiers: those are how the table is written, not what it * promises. + * + * Also not pinned, and deliberately not testable from here: WHICH surfaces + * ask for compact. That set spans workspaces — `chat-turn.tsx` here and + * Artifact Preview in `apps/desktop` — so asserting it would mean a library + * test reading application source, which is the dependency backwards. It is + * documented where it is decided instead, next to the heading rules in + * styles.css that the sharing actually matters for. */ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { readdir, readFile } from 'node:fs/promises'; -import { join, relative, resolve } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; import { renderToStaticMarkup } from 'react-dom/server'; import { MarkdownBody } from '../markdown-body.js'; const UI_SRC = resolve(import.meta.dirname, '..', '..', 'src'); -async function tsxFiles(dir: string): Promise { - const out: string[] = []; - for (const entry of await readdir(dir, { withFileTypes: true })) { - const path = join(dir, entry.name); - if (entry.isDirectory()) out.push(...(await tsxFiles(path))); - else if (entry.name.endsWith('.tsx') && !entry.name.includes('.test.')) out.push(path); - } - return out; -} - const SAMPLE = ['## Heading two', '', 'A paragraph.', '', '1. First item', '2. Second item'].join('\n'); function compactMarkup(): string { @@ -123,43 +120,4 @@ describe('transcript markdown rhythm', () => { `Found: { ${rule[1].trim()} }`, ); }); - - /** - * The rhythm table carries the transcript's heading TYPOGRAPHY, not just its - * spacing, and keys both on `density`. That is a deliberate bet, and it is a - * bet: Astryx's density RFC (facebook/astryx#839) draws the line at "density - * shifts heights and spacing, not typography", so the key is honest only - * while `compact` and "transcript" name the same set. They do today — the - * transcript is the only caller asking for compact, and the Daily Review - * renders a document at the default. - * - * A separate surface attribute would decouple them, but nothing needs it yet - * and it would add a prop to a public component for a caller that does not - * exist. Guard the assumption instead: the moment a second surface asks for - * compact markdown it silently inherits transcript heading sizes and dimmed - * deep headings, and this is what tells whoever adds it that they have to - * choose — inherit deliberately, or split the key then. - */ - it('keeps compact markdown a transcript-only surface', async () => { - const callers: string[] = []; - for (const file of await tsxFiles(UI_SRC)) { - const source = await readFile(file, 'utf8'); - // `` opening tags only — not MarkdownBody's internal plumbing - // and not the density Maka hands its own code-block renderers. - for (const tag of source.matchAll(/]*?\/?>/gs)) { - if (/density=(["'])compact\1/.test(tag[0])) callers.push(relative(UI_SRC, file)); - } - } - - assert.deepEqual( - [...new Set(callers)].sort(), - ['chat-turn.tsx'], - 'a new caller renders markdown at compact density. The rhythm table treats ' + - '`density="compact"` as "this is a transcript" and gives it flattened heading sizes ' + - 'plus secondary-colour h4-h6 — typography, which Astryx\'s density is explicitly not ' + - 'supposed to carry (facebook/astryx#839). Either that is what the new surface wants, ' + - 'and this list grows, or the typography rules need their own key. ' + - `Found: ${JSON.stringify([...new Set(callers)].sort())}`, - ); - }); }); diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx index 53dd1574e3..c768ab3876 100644 --- a/packages/ui/src/chat-turn.tsx +++ b/packages/ui/src/chat-turn.tsx @@ -1104,13 +1104,16 @@ const AssistantAnswerBubble = memo(function AssistantAnswerBubble(props: Assista text={props.text} streaming={props.phase === 'streaming'} settledText={settledText} - // Names the surface; it does NOT set this turn's block spacing. Every - // top-level gap in a transcript turn comes from the rhythm table in - // styles.css, which keys on the `data-density="compact"` this prop - // reflects and overrides Astryx's own margins outright. So `compact` - // still buys the transcript heading scale and the tighter rhythm - // inside a list item or a quote, and reading it as "paragraphs are - // squeezed here" is the wrong file — retune `--md-gap-block` instead. + // Names the surface, and not exclusively: the desktop Artifact + // Preview asks for compact too and takes the same rules, so retuning + // them here is retuning them there. What this prop does NOT do is set + // this turn's block spacing. Every top-level gap in a transcript turn + // comes from the rhythm table in styles.css, which keys on the + // `data-density="compact"` this prop reflects and overrides Astryx's + // own margins outright. So `compact` still buys the transcript + // heading scale and the tighter rhythm inside a list item or a quote, + // and reading it as "paragraphs are squeezed here" is the wrong + // file — retune `--md-gap-block` instead. density="compact" /> {truncated && ( diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index df96fdab6e..a8de977dd9 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -229,7 +229,12 @@ - Only `[data-density="compact"]`. The document mode is not broken (its lists sit 10px apart against 12px paragraphs, which is already in order), and the Daily Review renders through it. Fixing only the broken half keeps - that surface out of the blast radius. + that surface out of the blast radius. Compact is not the transcript + alone, though — the desktop Artifact Preview asks for it too, so it takes + these gaps as well. For the block rung that is the point rather than a + cost: 12px IS the document rhythm, so the preview moves toward Astryx's + default rather than further from it. The heading rules below are the ones + where sharing is arguable, and they say so. - Scoped to Astryx's own `data-density`, not to a `.maka-turn` ancestor. `themeProps()` reflects every visual prop as a data attribute for exactly this ("consumers target stable data-attribute selectors"), so the rhythm @@ -317,14 +322,28 @@ rather than spacing, and Astryx's density RFC (facebook/astryx#839) draws the line at "density shifts heights and spacing, not typography". Keying them on `data-density` is honest only while `compact` and "transcript" name - the same set — true today, since the transcript is the only caller asking - for compact and the Daily Review renders a document at the default. A - second compact surface would silently inherit transcript heading sizes and - dimmed deep headings. Giving typography its own surface attribute would - decouple them, but nothing needs it yet; the assumption is held by a test - instead (__tests__/markdown-rhythm-contract.test.tsx, "keeps compact - markdown a transcript-only surface"), which fails the moment the sets - diverge and says what the choice is. */ + the same set, and they DO NOT. Two surfaces ask for compact: the transcript + (chat-turn.tsx) and the desktop Artifact Preview + (apps/desktop/src/renderer/artifact-preview.tsx, since #2506), so the + preview pane has been rendering `.md` files at transcript heading sizes + with dimmed h4-h6 ever since. Nothing scopes it out: the + `[data-maka-contract="markdown"]` prefix is on every MarkdownBody, so + `density` is the whole key. + + Left shared on purpose, not by omission. The preview is a narrow side pane + showing a document a few lines at a time, which is the shape the flattening + was designed for, and no report has called it wrong. Splitting the key — + moving these three rules onto a surface attribute the transcript sets and + the preview does not — is a visible change to the preview's typography and + belongs in its own change, argued and looked at on that surface rather + than smuggled in under a spacing token. + + So: a THIRD compact surface inherits transcript typography silently, and + nothing catches it. That is unguarded, and it stays unguarded knowingly — + the caller set spans workspaces, so the only test that could hold it is a + library test grepping application source. One was written and it was wrong + on its first commit, passing while already false. A comment that is checked + when these rules are read beats an assertion that lies. */ [data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading:is([data-level="1"], [data-level="2"]) { font: var(--maka-text-heading-3); }