@nanisoft/prism-llms 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data.json +151 -21
- package/dist/llms-full.txt +607 -35
- package/dist/llms.txt +9 -5
- package/dist/md/blocks/backdrop-01.md +8 -4
- package/dist/md/blocks/gantt-01.md +4 -4
- package/dist/md/blocks/hero-01.md +9 -1
- package/dist/md/blocks/media-band-01.md +123 -0
- package/dist/md/blocks/site-navbar.md +25 -3
- package/dist/md/changelogs/prism-llms.md +11 -0
- package/dist/md/changelogs/prism-mcp-server.md +7 -0
- package/dist/md/changelogs/prism-tokens.md +10 -0
- package/dist/md/changelogs/prism-ui.md +52 -0
- package/dist/md/components/aurora-wash.md +89 -0
- package/dist/md/components/particle-field.md +100 -0
- package/dist/md/components/reveal.md +113 -0
- package/dist/md/foundation/motion.md +37 -20
- package/dist/prism-skill.md +2 -2
- package/package.json +3 -3
package/dist/data.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.5.0",
|
|
3
3
|
"items": [
|
|
4
4
|
{
|
|
5
5
|
"id": "accordion",
|
|
@@ -144,6 +144,32 @@
|
|
|
144
144
|
"pages": []
|
|
145
145
|
}
|
|
146
146
|
},
|
|
147
|
+
{
|
|
148
|
+
"id": "aurora-wash",
|
|
149
|
+
"slug": "aurora-wash",
|
|
150
|
+
"kind": "component",
|
|
151
|
+
"name": "AuroraWash",
|
|
152
|
+
"category": "Data display",
|
|
153
|
+
"status": "stable",
|
|
154
|
+
"description": "Light moving through a ground: translucent planes in the pack’s own colours, each on its own long orbit of the aurora cycle.",
|
|
155
|
+
"url": "/components/aurora-wash",
|
|
156
|
+
"mirror": "/components/aurora-wash.md",
|
|
157
|
+
"source": "src/components/ui/aurora-wash.tsx",
|
|
158
|
+
"exports": [
|
|
159
|
+
"AuroraWash"
|
|
160
|
+
],
|
|
161
|
+
"importLine": "import { AuroraWash } from '@nanisoft/prism-ui/components/aurora-wash'",
|
|
162
|
+
"doc": "# AuroraWash\n\nLight moving through a ground: translucent planes in the pack’s own colours, each on its own long orbit of the aurora cycle.\n\n```tsx\nimport { AuroraWash } from '@nanisoft/prism-ui/components/aurora-wash'\n```\n\n## Overview\n\nAuroraWash is light moving through a ground: two or three large translucent\nplanes in the pack's own colours, each on its own long orbit of the aurora\ncycle. It follows the wash law, so the stops are semantic, the element is\n`aria-hidden`, and it never touches text.\n\n## Guidelines\n\n### When to use\n\nUse it where a hero's ground would otherwise be flat: a band whose claim is\nthat a surface is alive reads differently with light crossing it, and the wash\nsays that without drawing a single invented mark. It repaints with the pack,\nbecause the planes carry the pack's own primary and series colours, so a wash\nin one pack is a different wash in another.\n\nGive it a clipped container. The wrapper clips, but the caller gives it the\npositioned ancestor to fill, and a wash that escapes its band is a page-wide\nstain.\n\n### When not to use\n\nDo not put type on it without its own contrast. A wash is a ground, and the\ncontent over it owns the contrast the way content over `SignalField` does.\n\nDo not use two on one page. Light moving through two grounds is two weathers,\nand the reader cannot be in both.\n\nDo not reach for it when the claim is a mechanism rather than an atmosphere. A\nmarker crossing a rail is `Backdrop01` with the travel cycle; the wash says a\nsurface is lit, not that a system runs.\n\n### Best practices\n\n- Let it be quiet. The intensity of a wash is the caller's, through opacity on\n the caller's own wrapper, and the restrained version is the one that reads\n as a place rather than as a poster.\n- Pair it with `ParticleField` only when the two claims belong together, and\n let the tempos differ, which the aurora and drift cycles already do.\n- Trust the still frame. The reduced-motion rule holds the planes at rest and\n the wash survives it, which is the test every ambient figure in this system\n passes.\n\n**The wash as a ground, with type over it, and a pack boundary around the**\n\n```tsx\nimport { AuroraWash } from '@nanisoft/prism-ui/components/aurora-wash'\n\n/**\n * The wash as a ground, with type over it, and a pack boundary around the\n * second card so the demo shows the claim the component makes: the planes\n * carry the ground's own colours, and a wash in one pack is a different wash\n * in another.\n */\nexport default function AuroraWashDemo() {\n return (\n <div className=\"flex flex-col gap-6\">\n <div className=\"bg-card relative overflow-hidden rounded-xl border p-10\">\n <AuroraWash className=\"absolute inset-0\" />\n <div className=\"relative\">\n <p className=\"text-muted-foreground text-sm font-medium tracking-wide uppercase\">\n Preview\n </p>\n <h3 className=\"mt-3 text-3xl font-semibold tracking-tight text-balance\">\n Light through the ground the page is wearing\n </h3>\n <p className=\"text-muted-foreground mt-4 max-w-168 text-lg text-pretty\">\n Three planes on one long orbit, out of step with each other, each\n carrying a colour the page already resolved.\n </p>\n </div>\n </div>\n </div>\n )\n}\n```\n\n## Props\n\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. The wash fills the nearest positioned ancestor, so the caller's job is to give it one and to keep the content above it; the wash never carries the content and never takes the pointer.\n",
|
|
163
|
+
"props": "## Props\n\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. The wash fills the nearest positioned ancestor, so the caller's job is to give it one and to keep the content above it; the wash never carries the content and never takes the pointer.",
|
|
164
|
+
"example": {
|
|
165
|
+
"title": "The wash as a ground, with type over it, and a pack boundary around the",
|
|
166
|
+
"code": "import { AuroraWash } from '@nanisoft/prism-ui/components/aurora-wash'\n\n/**\n * The wash as a ground, with type over it, and a pack boundary around the\n * second card so the demo shows the claim the component makes: the planes\n * carry the ground's own colours, and a wash in one pack is a different wash\n * in another.\n */\nexport default function AuroraWashDemo() {\n return (\n <div className=\"flex flex-col gap-6\">\n <div className=\"bg-card relative overflow-hidden rounded-xl border p-10\">\n <AuroraWash className=\"absolute inset-0\" />\n <div className=\"relative\">\n <p className=\"text-muted-foreground text-sm font-medium tracking-wide uppercase\">\n Preview\n </p>\n <h3 className=\"mt-3 text-3xl font-semibold tracking-tight text-balance\">\n Light through the ground the page is wearing\n </h3>\n <p className=\"text-muted-foreground mt-4 max-w-168 text-lg text-pretty\">\n Three planes on one long orbit, out of step with each other, each\n carrying a colour the page already resolved.\n </p>\n </div>\n </div>\n </div>\n )\n}\n"
|
|
167
|
+
},
|
|
168
|
+
"references": {
|
|
169
|
+
"blocks": [],
|
|
170
|
+
"pages": []
|
|
171
|
+
}
|
|
172
|
+
},
|
|
147
173
|
{
|
|
148
174
|
"id": "avatar",
|
|
149
175
|
"slug": "avatar",
|
|
@@ -2120,6 +2146,32 @@
|
|
|
2120
2146
|
"pages": []
|
|
2121
2147
|
}
|
|
2122
2148
|
},
|
|
2149
|
+
{
|
|
2150
|
+
"id": "particle-field",
|
|
2151
|
+
"slug": "particle-field",
|
|
2152
|
+
"kind": "component",
|
|
2153
|
+
"name": "ParticleField",
|
|
2154
|
+
"category": "Data display",
|
|
2155
|
+
"status": "stable",
|
|
2156
|
+
"description": "A living field: points drifting on linked paths, answering the pointer, and standing still as a complete figure when the loop is stopped or declined.",
|
|
2157
|
+
"url": "/components/particle-field",
|
|
2158
|
+
"mirror": "/components/particle-field.md",
|
|
2159
|
+
"source": "src/components/ui/particle-field.tsx",
|
|
2160
|
+
"exports": [
|
|
2161
|
+
"ParticleField"
|
|
2162
|
+
],
|
|
2163
|
+
"importLine": "import { ParticleField } from '@nanisoft/prism-ui/components/particle-field'",
|
|
2164
|
+
"doc": "# ParticleField\n\nA living field: points drifting on linked paths, answering the pointer, and standing still as a complete figure when the loop is stopped or declined.\n\n```tsx\nimport { ParticleField } from '@nanisoft/prism-ui/components/particle-field'\n```\n\n## Overview\n\nParticleField is a living field: points drifting on linked paths, the medium a\npage's largest statements stand in front of. It is the one script-driven figure\nin the system, and the static field it renders on the server is the contract\nthat makes that safe: stop the loop, disarm the canvas, print the page, and the\nfigure is still a field, still true, still legible.\n\n## Guidelines\n\n### When to use\n\nUse it where a page claims there is a medium to move through: a hero that says\ntraffic flows, a band that says an estate is observed, a ground that says the\nsurface is live. The links are what make it a medium rather than dust, so size\n`linkDistance` to the band: a wide band carries longer lines than a panel.\n\nGive it room behind content that has its own position and its own contrast. It\nis a ground, exactly as `SignalField` is, and it composes behind type rather\nthan beside it.\n\n### When not to use\n\nDo not use it behind a reading passage. A medium that moves while a reader\nreads is the first thing a reader with vestibular sensitivity will ask to have\ngone, and `SignalField` still, or nothing, is the ground a passage wants.\n\nDo not use it to say something. It is `aria-hidden` by default because a medium\nis not information; a figure that carries a claim is `PulseGraph` or\n`PulseSeries`, both of which require a name.\n\nDo not stack two of them. Two media on one page is a collision, and the reader\ncannot tell which one they are looking through.\n\n### Best practices\n\n- Leave `interactive` on wherever a reader's hand can reach the ground, and\n turn it off under text a reader will select, because a surface that moves\n during a selection is a surface fighting the selection.\n- Keep the count in the dozens. A field with hundreds of points is a haze, and\n the clamp will not save a composition that wanted fog.\n- Pair it with `AuroraWash` when the claim is light moving through the medium\n as well as across it, and let the two differ in tempo, which they already do\n by an order of magnitude.\n- Trust the floor. The component that renders nothing until a script runs is\n the retired line's defect, and this one renders the whole figure before any\n script decides to help.\n\n**The field as a ground, with type over it, which is the only honest way to**\n\n```tsx\nimport { ParticleField } from '@nanisoft/prism-ui/components/particle-field'\n\n/**\n * The field as a ground, with type over it, which is the only honest way to\n * preview a medium: a field on its own is a scatter and there is nothing to\n * judge.\n *\n * The demo is a server component. The field is a client component under it, so\n * the floor renders here and the loop arms in the reader's browser, which is\n * the arrangement a product composes.\n */\nexport default function ParticleFieldDemo() {\n return (\n <div className=\"bg-card relative overflow-hidden rounded-xl border p-10\">\n <ParticleField count={56} className=\"absolute inset-0\" />\n <div className=\"relative\">\n <p className=\"text-muted-foreground text-sm font-medium tracking-wide uppercase\">\n Preview\n </p>\n <h3 className=\"mt-3 text-3xl font-semibold tracking-tight text-balance\">\n A medium as a ground, with type over it\n </h3>\n <p className=\"text-muted-foreground mt-4 max-w-168 text-lg text-pretty\">\n The points drift on linked paths and answer the pointer. Move the hand\n across this panel and the medium parts around it, and the field it\n falls back to when the loop is stopped is the same field at rest.\n </p>\n </div>\n </div>\n )\n}\n```\n\n## Props\n\n**`count`** `number` · optional · default: — — How many points the field drifts. Read as a suggestion and clamped, because a field behind a hero wants dozens and a field behind a panel wants a handful, and a count outside that range is either a haze or a chart.\n**`linkDistance`** `number` · optional · default: — — How far apart two points may sit and still be linked, in CSS pixels. The links are what make a field read as a medium rather than as dust, and the distance is a prop because a wide band carries longer lines than a panel.\n**`interactive`** `boolean` · optional · default: — — Whether the reader's pointer pushes the field. On by default, because a medium that answers the hand is the claim this figure makes; a caller placing the field under text a reader is selecting turns it off, because a surface that moves while a reader drags across it is a surface fighting the selection.\n**`label`** `string` · optional · default: — — The name of the figure, or `decorative`. See `SignalFieldProps`.\n**`decorative`** `boolean` · optional · default: —\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component.\n",
|
|
2165
|
+
"props": "## Props\n\n**`count`** `number` · optional · default: — — How many points the field drifts. Read as a suggestion and clamped, because a field behind a hero wants dozens and a field behind a panel wants a handful, and a count outside that range is either a haze or a chart.\n**`linkDistance`** `number` · optional · default: — — How far apart two points may sit and still be linked, in CSS pixels. The links are what make a field read as a medium rather than as dust, and the distance is a prop because a wide band carries longer lines than a panel.\n**`interactive`** `boolean` · optional · default: — — Whether the reader's pointer pushes the field. On by default, because a medium that answers the hand is the claim this figure makes; a caller placing the field under text a reader is selecting turns it off, because a surface that moves while a reader drags across it is a surface fighting the selection.\n**`label`** `string` · optional · default: — — The name of the figure, or `decorative`. See `SignalFieldProps`.\n**`decorative`** `boolean` · optional · default: —\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component.",
|
|
2166
|
+
"example": {
|
|
2167
|
+
"title": "The field as a ground, with type over it, which is the only honest way to",
|
|
2168
|
+
"code": "import { ParticleField } from '@nanisoft/prism-ui/components/particle-field'\n\n/**\n * The field as a ground, with type over it, which is the only honest way to\n * preview a medium: a field on its own is a scatter and there is nothing to\n * judge.\n *\n * The demo is a server component. The field is a client component under it, so\n * the floor renders here and the loop arms in the reader's browser, which is\n * the arrangement a product composes.\n */\nexport default function ParticleFieldDemo() {\n return (\n <div className=\"bg-card relative overflow-hidden rounded-xl border p-10\">\n <ParticleField count={56} className=\"absolute inset-0\" />\n <div className=\"relative\">\n <p className=\"text-muted-foreground text-sm font-medium tracking-wide uppercase\">\n Preview\n </p>\n <h3 className=\"mt-3 text-3xl font-semibold tracking-tight text-balance\">\n A medium as a ground, with type over it\n </h3>\n <p className=\"text-muted-foreground mt-4 max-w-168 text-lg text-pretty\">\n The points drift on linked paths and answer the pointer. Move the hand\n across this panel and the medium parts around it, and the field it\n falls back to when the loop is stopped is the same field at rest.\n </p>\n </div>\n </div>\n )\n}\n"
|
|
2169
|
+
},
|
|
2170
|
+
"references": {
|
|
2171
|
+
"blocks": [],
|
|
2172
|
+
"pages": []
|
|
2173
|
+
}
|
|
2174
|
+
},
|
|
2123
2175
|
{
|
|
2124
2176
|
"id": "password-field",
|
|
2125
2177
|
"slug": "password-field",
|
|
@@ -2649,6 +2701,32 @@
|
|
|
2649
2701
|
"pages": []
|
|
2650
2702
|
}
|
|
2651
2703
|
},
|
|
2704
|
+
{
|
|
2705
|
+
"id": "reveal",
|
|
2706
|
+
"slug": "reveal",
|
|
2707
|
+
"kind": "component",
|
|
2708
|
+
"name": "Reveal",
|
|
2709
|
+
"category": "Layout",
|
|
2710
|
+
"status": "stable",
|
|
2711
|
+
"description": "A section that meets the reader: the wrapper that binds an arrival to the reader’s own scroll position, with no script and no second introduction for the hero.",
|
|
2712
|
+
"url": "/components/reveal",
|
|
2713
|
+
"mirror": "/components/reveal.md",
|
|
2714
|
+
"source": "src/components/ui/reveal.tsx",
|
|
2715
|
+
"exports": [
|
|
2716
|
+
"Reveal"
|
|
2717
|
+
],
|
|
2718
|
+
"importLine": "import { Reveal } from '@nanisoft/prism-ui/components/reveal'",
|
|
2719
|
+
"doc": "# Reveal\n\nA section that meets the reader: the wrapper that binds an arrival to the reader’s own scroll position, with no script and no second introduction for the hero.\n\n```tsx\nimport { Reveal } from '@nanisoft/prism-ui/components/reveal'\n```\n\n## Overview\n\nReveal is a section that meets the reader: the wrapper that binds an arrival to\nthe reader's own scroll position and to nothing else. It is a style the browser\nresolves rather than a state a script flips, so there is no observer, no\nlistener and no client boundary, and the content exists from the first paint.\n\n## Guidelines\n\n### When to use\n\nUse it on the bands a reader scrolls to. An arrival acknowledges the reader's\nown movement, which is the only input it answers to, and a band that enters\nthe viewport as the reader arrives reads as a page composed for reading rather\nthan as one long poster.\n\nStagger with `delay` when bands arrive in a rhythm, and keep the stagger to a\npage's own measure: three steps exist, and a cascade longer than the rhythm it\nsits in is a wait the reader did not cause.\n\n### When not to use\n\nDo not wrap the hero. A section above the fold when the page loads has already\nfinished entering, and the page does not introduce itself twice; the one\nauthored moment a page carries belongs to its hero, not to every band under\nit.\n\nDo not wrap everything. A reveal on every section is one identical entrance\nwearing four different clothes, and the second arrival a reader notices is the\none where the device stops being about the content. Let the still bands\nbetween the arriving ones carry their own weight.\n\nDo not use it for feedback or for state. A control answering a reader is the\nfirst law of motion and already has its scale; an arrival is the third law and\nholds exactly one job.\n\n### Best practices\n\n- Compose it around a Block, not inside one. The wrapper is the whole of the\n component, every Block keeps its own surface, and no Block widens for a\n behaviour the whole page shares.\n- Remember the fallbacks are the design: a browser without scroll-driven\n animations shows the section simply there, a reader who asked for less\n motion gets the same still page, and neither of those outcomes is a failure\n to be worked around.\n- The `delay` steps are fractions of the entry rather than waits, so a reader\n who scrolls fast arrives fast. That is the property that separates this law\n from a timer, and it is worth keeping.\n\n**Two cards arriving, the second a step behind the first. The demo is the**\n\n```tsx\nimport { Reveal } from '@nanisoft/prism-ui/components/reveal'\n\n/**\n * Two cards arriving, the second a step behind the first. The demo is the\n * device at its smallest: a staggered pair inside one panel, so the rhythm is\n * visible without a page to scroll.\n *\n * In this documentation frame the cards below the fold are the honest preview:\n * scroll to them and they arrive, arrive again on the way back up at no delay,\n * and a reader who has asked the platform for less motion finds them simply\n * there.\n */\nexport default function RevealDemo() {\n return (\n <div className=\"flex flex-col gap-4\">\n <Reveal>\n <div className=\"bg-card text-card-foreground rounded-xl border p-6 shadow-sm\">\n <p className=\"text-muted-foreground text-sm font-medium tracking-wide uppercase\">\n First band\n </p>\n <h3 className=\"mt-2 text-xl font-semibold tracking-tight\">\n An arrival, bound to the scroll the reader owns\n </h3>\n <p className=\"text-muted-foreground mt-2 text-sm text-pretty\">\n No script decides when this plays. The hand on the scroll does, and\n a browser that cannot run it shows the band simply there.\n </p>\n </div>\n </Reveal>\n <Reveal delay={1}>\n <div className=\"bg-card text-card-foreground rounded-xl border p-6 shadow-sm\">\n <p className=\"text-muted-foreground text-sm font-medium tracking-wide uppercase\">\n Second band\n </p>\n <h3 className=\"mt-2 text-xl font-semibold tracking-tight\">\n The same arrival, a step behind\n </h3>\n <p className=\"text-muted-foreground mt-2 text-sm text-pretty\">\n The step is a fraction of the entry rather than a wait, so a reader\n who scrolls fast arrives fast, and nothing here could delay them.\n </p>\n </div>\n </Reveal>\n </div>\n )\n}\n```\n\n## Props\n\n**`children`** `ReactNode` · required · default: — — The band, grid or figure that arrives.\n**`delay`** `1 | 2 | 3` · optional · default: — — How far into the page's rhythm this arrival sits behind the one before it, as a fraction of the entry rather than as a wait: the reader's scroll is the only clock, and a delay a reader could not hurry would be a delay the reader did not cause. Three steps, as on the ambient delays.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component.\n",
|
|
2720
|
+
"props": "## Props\n\n**`children`** `ReactNode` · required · default: — — The band, grid or figure that arrives.\n**`delay`** `1 | 2 | 3` · optional · default: — — How far into the page's rhythm this arrival sits behind the one before it, as a fraction of the entry rather than as a wait: the reader's scroll is the only clock, and a delay a reader could not hurry would be a delay the reader did not cause. Three steps, as on the ambient delays.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component.",
|
|
2721
|
+
"example": {
|
|
2722
|
+
"title": "Two cards arriving, the second a step behind the first. The demo is the",
|
|
2723
|
+
"code": "import { Reveal } from '@nanisoft/prism-ui/components/reveal'\n\n/**\n * Two cards arriving, the second a step behind the first. The demo is the\n * device at its smallest: a staggered pair inside one panel, so the rhythm is\n * visible without a page to scroll.\n *\n * In this documentation frame the cards below the fold are the honest preview:\n * scroll to them and they arrive, arrive again on the way back up at no delay,\n * and a reader who has asked the platform for less motion finds them simply\n * there.\n */\nexport default function RevealDemo() {\n return (\n <div className=\"flex flex-col gap-4\">\n <Reveal>\n <div className=\"bg-card text-card-foreground rounded-xl border p-6 shadow-sm\">\n <p className=\"text-muted-foreground text-sm font-medium tracking-wide uppercase\">\n First band\n </p>\n <h3 className=\"mt-2 text-xl font-semibold tracking-tight\">\n An arrival, bound to the scroll the reader owns\n </h3>\n <p className=\"text-muted-foreground mt-2 text-sm text-pretty\">\n No script decides when this plays. The hand on the scroll does, and\n a browser that cannot run it shows the band simply there.\n </p>\n </div>\n </Reveal>\n <Reveal delay={1}>\n <div className=\"bg-card text-card-foreground rounded-xl border p-6 shadow-sm\">\n <p className=\"text-muted-foreground text-sm font-medium tracking-wide uppercase\">\n Second band\n </p>\n <h3 className=\"mt-2 text-xl font-semibold tracking-tight\">\n The same arrival, a step behind\n </h3>\n <p className=\"text-muted-foreground mt-2 text-sm text-pretty\">\n The step is a fraction of the entry rather than a wait, so a reader\n who scrolls fast arrives fast, and nothing here could delay them.\n </p>\n </div>\n </Reveal>\n </div>\n )\n}\n"
|
|
2724
|
+
},
|
|
2725
|
+
"references": {
|
|
2726
|
+
"blocks": [],
|
|
2727
|
+
"pages": []
|
|
2728
|
+
}
|
|
2729
|
+
},
|
|
2652
2730
|
{
|
|
2653
2731
|
"id": "scroll-area",
|
|
2654
2732
|
"slug": "scroll-area",
|
|
@@ -3852,7 +3930,7 @@
|
|
|
3852
3930
|
"Backdrop01"
|
|
3853
3931
|
],
|
|
3854
3932
|
"importLine": "import { Backdrop01 } from '@nanisoft/prism-ui/blocks/backdrop-01'",
|
|
3855
|
-
"doc": "# Backdrop01\n\nA band whose content sits over a running figure, with the cycle chosen from the seven the stylesheet publishes, an intensity that is opacity rather than speed, and a real pause control whose two labels come from the caller.\n\n```tsx\nimport { Backdrop01 } from '@nanisoft/prism-ui/blocks/backdrop-01'\n```\n\n## Overview\n\nBackdrop01 is a band whose content sits over a running figure. The figure is the\ncaller's, usually a `SignalField` or a `PulseGraph`; the Block draws the layer it\nrides on, the cycle, an intensity, and a real control that stops the field. The\nband is a layout rather than a template, so the content itself is a slot.\n\nThe motion is the second law of this system's motion scale. A cycle is a\ndemonstration: something runs because the system does, and the movement is how\nthe figure makes its claim visible rather than merely asserting it. The test for\nwhether motion belongs here at all is falsifiable, and it is worth stating before\nanything else. Stop the animation and look at the band. If the figure is still\ntrue and every mark of it is still readable, the movement was emphasis and it\nbelongs on a band. If stopping it leaves an empty rectangle, then the thing it was\nbuilt from was never a figure: it was a video with extra steps, and the Component\nit was built with is the wrong one. So the question is asked of the figure rather\nthan of the band, which is why `figure` is a required slot.\n\nThe three decisions worth reading are the three below. Intensity is opacity and\nnever a length, the pause is this Block's own button rather than a slot, and the\nfigure is the claim half of the two the motion law sorts figures into, with the\natmosphere half already owned by `SignalField`.\n\n## Guidelines\n\n### When to use\n\nUse it for a band that has to say a system is doing something, where the copy\nalone would read as a brochure. The four NaniSoft framings all want one: a\npipeline that runs, a market that is captured, an estate that is observed, and\nwork that agents do.\n\nUse `drift` and `pulse` for any figure. They are the two whose resting form is the\nfigure's own full form, so nothing is lost when the cycle is at either end of its\nturn. Use `travel`, `sweep`, `scan` and `shimmer` when the figure is drawn wider\nthan the band, because those four carry something across a field and the band\nclips what leaves.\n\nUse `pauseControl` on a page that gives a reader reason to want the page still.\n\n### When not to use\n\nDo not use it as a wrapper for a video, a canvas or anything whose meaning is in\nits movement. The test above answers that case before you reach this Block.\n\nDo not use it for a figure that belongs in the layout rather than behind the\ncopy. That is `InstrumentPanel01`, and a panel beside the copy says the same thing\nwith a border and a title bar instead of an opacity.\n\nDo not reach for it to add movement to a page. There is no entrance animation,\nno scroll effect and no motion on text in this system, and this Block is the one\nplace a cycle is allowed, which is exactly why it is not a decoration.\n\n### Best practices\n\n- Test the figure with the field stopped before you ship the band. If the figure\n is still true, the motion is emphasis. If it is not, change the figure.\n- Leave `pauseControl` off and pass `paused` when the state belongs to something\n larger than the band, and put the button in `children`. One state and several\n bands is one state.\n- Prefer `intensity=\"subtle\"` behind body copy. `present` is for a band with a\n short line over it.\n- Give the figure a name that says what it shows. The Block publishes it, and a\n field of dots is not something a screen reader should be asked to read.\n- Keep the copy short. It is sitting over a moving figure, which is the one\n arrangement in this system where legibility is a real cost.\n\n### Content guidelines\n\n- `label` names the figure, not the band. \"The collection line, as a wavefront\n crossing the field\" rather than \"Our platform\".\n- `pauseLabel` and `resumeLabel` are your own words, in your own language. Two\n call sites that pass different words for the same control are the Block working.\n- The content in `children` is yours. There is no prop per arrangement, because\n the arrangement is the thing the Block cannot know.\n\n**Two bands, and the pair is the point rather than the decoration.**\n\n```tsx\n'use client'\n\nimport { Activity, Workflow } from 'lucide-react'\n\nimport { Backdrop01 } from '@nanisoft/prism-ui/blocks/backdrop-01'\nimport { Button } from '@nanisoft/prism-ui/components/button'\nimport { PulseGraph } from '@nanisoft/prism-ui/components/pulse-graph'\nimport { SignalField } from '@nanisoft/prism-ui/components/signal-field'\n\n/**\n * Two bands, and the pair is the point rather than the decoration.\n *\n * The first is the atmosphere half: a `SignalField`, which is `aria-hidden` by\n * default and names nothing, under a control the Block draws. The second is the\n * claim half: a `PulseGraph` with three stages in a lane, which names itself and\n * is stopped by the caller rather than by the Block, so the controlled arm of\n * `paused` and the `children` slot for a caller's own control are both visible\n * here.\n */\nexport default function Backdrop01Demo() {\n return (\n <>\n <Backdrop01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"A line that finishes before anyone asks\"\n description=\"A field behind the copy, a cycle the stylesheet resolves, and a control that stops it.\"\n field=\"drift\"\n intensity=\"present\"\n pauseControl\n pauseLabel=\"Pause the field\"\n resumeLabel=\"Resume the field\"\n label=\"The collection line, as a field of marks drifting on its own cycle\"\n figure={<SignalField count={40} emphasisAt={0.4} />}\n >\n <div className=\"flex max-w-measure flex-col gap-3\">\n <h4 className=\"flex items-center gap-2 text-lg font-semibold tracking-tight\">\n <Workflow className=\"text-muted-foreground size-4\" aria-hidden />\n Nothing here polls\n </h4>\n <p className=\"text-muted-foreground text-pretty text-sm\">\n One site, one line, and a run that finished before anybody had asked\n whether it was useful. The claim being tested was narrow on purpose.\n </p>\n </div>\n </Backdrop01>\n\n <Backdrop01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"The same band, with the field stopped and the control yours\"\n description=\"A claim-carrying figure, `paused` passed as a prop, and the control composed into the content.\"\n field=\"pulse\"\n intensity=\"subtle\"\n paused\n label=\"The collection line, as three stages with a marker travelling the rail\"\n figure={\n <PulseGraph\n label=\"The collection line, as three stages with a marker travelling the rail\"\n nodes={[\n { id: 'collect', name: 'collect', x: 0, y: 0, lane: 0, note: 'the edge' },\n { id: 'load', name: 'load', x: 1, y: 0, lane: 0, note: 'the lake', emphasis: true },\n { id: 'serve', name: 'serve', x: 2, y: 0, lane: 0, note: 'the reader' },\n ]}\n relations={[\n { from: 'collect', to: 'load', carries: true },\n { from: 'load', to: 'serve', carries: true },\n ]}\n />\n }\n >\n <div className=\"flex max-w-measure flex-col gap-3\">\n <h4 className=\"flex items-center gap-2 text-lg font-semibold tracking-tight\">\n <Activity className=\"text-muted-foreground size-4\" aria-hidden />\n Stopped, and still true\n </h4>\n <p className=\"text-muted-foreground text-pretty text-sm\">\n The graph draws every node, every edge and the rail in its resting form,\n so a reader who has asked for no movement reads the same claim. The\n control below is the caller’s own and so are the words on it, and\n the state it moves is the caller’s too, which is why this preview\n holds none of it.\n </p>\n <div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\">\n Resume the field\n </Button>\n </div>\n </div>\n </Backdrop01>\n </>\n )\n}\n```\n\n## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the section title. See the No-Default-Eyebrow Rule.\n**`title`** `ReactNode` · optional · default: — — The section title. Omit it for a band composed under its own heading, which is the usual case for something placed directly under a hero.\n**`description`** `ReactNode` · optional · default: — — One or two sentences under the title.\n**`children`** `ReactNode` · required · default: — — The band's own content, composed by the caller. A slot rather than a set of copy props, and the reason is that the content of a band is the only thing about it that Prism cannot know: it is a headline, a paragraph, two actions and a figure in some arrangement, and a Block that took a prop for each would be four arrangements wearing one name. The content sits over the field, which is why the band is a layout and not a template.\n**`figure`** `ReactNode` · required · default: — — The figure the field runs on, usually a `SignalField` or a `PulseGraph`. Required rather than optional, and the Block draws no figure of its own, because a field of invented marks is the retired line's defect rebuilt: the numbers were never collected and the motion says a system is running. A node is a node the caller named, a reading is a reading the caller holds, and an empty figure is an empty figure. What the Block owns is the layer the figure rides on, which is where a cycle can be applied without the figure having to know it is being animated.\n**`label`** `string` · required · default: — — The figure's accessible name, and the only thing a screen reader reads from the field. Required, and the reason this Block is the claim half of the two is on the Component's own JSDoc. See there for why the atmosphere half is not an arm here.\n**`field`** `Backdrop01Field` · required · default: — — Which cycle the field runs. See `Backdrop01Field`. Required rather than defaulted, because the cycle is a claim about the figure and only the caller knows what the figure claims. A default would be this Block choosing the mechanism a consumer's own claim gets made by, and a backdrop whose wavefront does not match its subject is a decoration wearing a caption.\n**`intensity`** `Backdrop01Intensity` · optional · default: `'subtle'` — How loud the field is behind the copy. Opacity on the layer, and the reasoning for it not being a length is on `INTENSITY` above, where the one-second floor is the reason.\n**`paused`** `boolean` · optional · default: `false` — Whether the field is stopped. The controlled arm. Pass it with `pauseControl={false}` and put the button in `children` when the state belongs to something larger than this band: a dashboard that stops every figure on the page together, or a settings toggle, is one state and several bands, and a state this Block owns would be a second copy of it that disagrees with the first. With `pauseControl` set, the same prop is the value the Block starts from and owns from then on, which is the usual defaultValue arrangement and is documented on the prop because it is the one place a reader of a call site could otherwise be surprised.\n**`pauseControl`** `boolean` · optional · default: `false` — Whether the Block draws the pause control. Off by default because a control on every backdrop is a control on every page a consumer puts one on, and most of those pages have a figure that is worth stopping and no reason to offer it. See the Component JSDoc for why the control is this Block's own button rather than a slot, which is the decision this prop and the two labels after it exist to record.\n**`pauseLabel`** `string` · optional · default: — — The words on the control while the field runs. Required with `pauseControl`. A prop, not a default, for the reason every accessible name in this package is a prop: the control is a sentence a reader hears, every language words stopping differently, and a default here would be an English string in every consumer's product that they cannot translate without forking the library.\n**`resumeLabel`** `string` · optional · default: — — The words on the control while the field is stopped. Required with `pauseControl`. A second prop rather than a second half of the first, because \"Pause\" and \"Play\" are not a pair in every language, and because a caller whose control says \"Stop the sweep\" and \"Sweep stopped\" is describing a figure rather than naming a transport control.\n**`headingLevel`** `HeadingLevel` · optional · default: `'h2'` — Heading level for the section title. See `HeadingLevel`.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Block. Changing a Prism-owned visual property from here is prohibited.\n\n## Composition\n\n**`exports`** `Backdrop01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/backdrop-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `Button, PulseGraph, Section, SignalField` — the public Prism exports this item composes\n",
|
|
3933
|
+
"doc": "# Backdrop01\n\nA band whose content sits over a running figure, with the cycle chosen from the seven the stylesheet publishes, an intensity that is opacity rather than speed, and a real pause control whose two labels come from the caller.\n\n```tsx\nimport { Backdrop01 } from '@nanisoft/prism-ui/blocks/backdrop-01'\n```\n\n## Overview\n\nBackdrop01 is a band whose content sits over a running figure. The figure is the\ncaller's, usually a `SignalField` or a `PulseGraph`; the Block draws the layer it\nrides on, the cycle, an intensity, and a real control that stops the field. The\nband is a layout rather than a template, so the content itself is a slot.\n\nThe motion is the second law of this system's motion scale. A cycle is a\ndemonstration: something runs because the system does, and the movement is how\nthe figure makes its claim visible rather than merely asserting it. The test for\nwhether motion belongs here at all is falsifiable, and it is worth stating before\nanything else. Stop the animation and look at the band. If the figure is still\ntrue and every mark of it is still readable, the movement was emphasis and it\nbelongs on a band. If stopping it leaves an empty rectangle, then the thing it was\nbuilt from was never a figure: it was a video with extra steps, and the Component\nit was built with is the wrong one. So the question is asked of the figure rather\nthan of the band, which is why `figure` is a required slot.\n\nThe three decisions worth reading are the three below. Intensity is opacity and\nnever a length, the pause is this Block's own button rather than a slot, and the\nfigure is the claim half of the two the motion law sorts figures into, with the\natmosphere half already owned by `SignalField`.\n\n## Guidelines\n\n### When to use\n\nUse it for a band that has to say a system is doing something, where the copy\nalone would read as a brochure. The four NaniSoft framings all want one: a\npipeline that runs, a market that is captured, an estate that is observed, and\nwork that agents do.\n\nUse `drift` and `pulse` for any figure. They are the two whose resting form is the\nfigure's own full form, so nothing is lost when the cycle is at either end of its\nturn. Use `travel`, `sweep`, `scan` and `shimmer` when the figure is drawn wider\nthan the band, because those four carry something across a field and the band\nclips what leaves. Use `aurora` when the claim is the ground rather than anything\ncrossing it: the whole layer rides one slow orbit, so it suits a wash or a wide\nfield and mismatches a drawing with a baseline, which would visibly leave the\nground it was measured on.\n\nUse `pauseControl` on a page that gives a reader reason to want the page still.\n\n### When not to use\n\nDo not use it as a wrapper for a video, a canvas or anything whose meaning is in\nits movement. The test above answers that case before you reach this Block.\n\nDo not use it for a figure that belongs in the layout rather than behind the\ncopy. That is `InstrumentPanel01`, and a panel beside the copy says the same thing\nwith a border and a title bar instead of an opacity.\n\nDo not reach for it to add movement to a page. A cycle demonstrates a system\nthat runs, and this Block is the one place in a band a cycle is allowed; an\narrival is `Reveal`'s job and not a faster cycle's, and neither of them is a\ndecoration, which is exactly why each has one place.\n\n### Best practices\n\n- Test the figure with the field stopped before you ship the band. If the figure\n is still true, the motion is emphasis. If it is not, change the figure.\n- Leave `pauseControl` off and pass `paused` when the state belongs to something\n larger than the band, and put the button in `children`. One state and several\n bands is one state.\n- Prefer `intensity=\"subtle\"` behind body copy. `present` is for a band with a\n short line over it.\n- Give the figure a name that says what it shows. The Block publishes it, and a\n field of dots is not something a screen reader should be asked to read.\n- Keep the copy short. It is sitting over a moving figure, which is the one\n arrangement in this system where legibility is a real cost.\n\n### Content guidelines\n\n- `label` names the figure, not the band. \"The collection line, as a wavefront\n crossing the field\" rather than \"Our platform\".\n- `pauseLabel` and `resumeLabel` are your own words, in your own language. Two\n call sites that pass different words for the same control are the Block working.\n- The content in `children` is yours. There is no prop per arrangement, because\n the arrangement is the thing the Block cannot know.\n\n**Two bands, and the pair is the point rather than the decoration.**\n\n```tsx\n'use client'\n\nimport { Activity, Workflow } from 'lucide-react'\n\nimport { Backdrop01 } from '@nanisoft/prism-ui/blocks/backdrop-01'\nimport { Button } from '@nanisoft/prism-ui/components/button'\nimport { PulseGraph } from '@nanisoft/prism-ui/components/pulse-graph'\nimport { SignalField } from '@nanisoft/prism-ui/components/signal-field'\n\n/**\n * Two bands, and the pair is the point rather than the decoration.\n *\n * The first is the atmosphere half: a `SignalField`, which is `aria-hidden` by\n * default and names nothing, under a control the Block draws. The second is the\n * claim half: a `PulseGraph` with three stages in a lane, which names itself and\n * is stopped by the caller rather than by the Block, so the controlled arm of\n * `paused` and the `children` slot for a caller's own control are both visible\n * here.\n */\nexport default function Backdrop01Demo() {\n return (\n <>\n <Backdrop01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"A line that finishes before anyone asks\"\n description=\"A field behind the copy, a cycle the stylesheet resolves, and a control that stops it.\"\n field=\"drift\"\n intensity=\"present\"\n pauseControl\n pauseLabel=\"Pause the field\"\n resumeLabel=\"Resume the field\"\n label=\"The collection line, as a field of marks drifting on its own cycle\"\n figure={<SignalField count={40} emphasisAt={0.4} />}\n >\n <div className=\"flex max-w-measure flex-col gap-3\">\n <h4 className=\"flex items-center gap-2 text-lg font-semibold tracking-tight\">\n <Workflow className=\"text-muted-foreground size-4\" aria-hidden />\n Nothing here polls\n </h4>\n <p className=\"text-muted-foreground text-pretty text-sm\">\n One site, one line, and a run that finished before anybody had asked\n whether it was useful. The claim being tested was narrow on purpose.\n </p>\n </div>\n </Backdrop01>\n\n <Backdrop01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"The same band, with the field stopped and the control yours\"\n description=\"A claim-carrying figure, `paused` passed as a prop, and the control composed into the content.\"\n field=\"pulse\"\n intensity=\"subtle\"\n paused\n label=\"The collection line, as three stages with a marker travelling the rail\"\n figure={\n <PulseGraph\n label=\"The collection line, as three stages with a marker travelling the rail\"\n nodes={[\n { id: 'collect', name: 'collect', x: 0, y: 0, lane: 0, note: 'the edge' },\n { id: 'load', name: 'load', x: 1, y: 0, lane: 0, note: 'the lake', emphasis: true },\n { id: 'serve', name: 'serve', x: 2, y: 0, lane: 0, note: 'the reader' },\n ]}\n relations={[\n { from: 'collect', to: 'load', carries: true },\n { from: 'load', to: 'serve', carries: true },\n ]}\n />\n }\n >\n <div className=\"flex max-w-measure flex-col gap-3\">\n <h4 className=\"flex items-center gap-2 text-lg font-semibold tracking-tight\">\n <Activity className=\"text-muted-foreground size-4\" aria-hidden />\n Stopped, and still true\n </h4>\n <p className=\"text-muted-foreground text-pretty text-sm\">\n The graph draws every node, every edge and the rail in its resting form,\n so a reader who has asked for no movement reads the same claim. The\n control below is the caller’s own and so are the words on it, and\n the state it moves is the caller’s too, which is why this preview\n holds none of it.\n </p>\n <div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\">\n Resume the field\n </Button>\n </div>\n </div>\n </Backdrop01>\n </>\n )\n}\n```\n\n## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the section title. See the No-Default-Eyebrow Rule.\n**`title`** `ReactNode` · optional · default: — — The section title. Omit it for a band composed under its own heading, which is the usual case for something placed directly under a hero.\n**`description`** `ReactNode` · optional · default: — — One or two sentences under the title.\n**`children`** `ReactNode` · required · default: — — The band's own content, composed by the caller. A slot rather than a set of copy props, and the reason is that the content of a band is the only thing about it that Prism cannot know: it is a headline, a paragraph, two actions and a figure in some arrangement, and a Block that took a prop for each would be four arrangements wearing one name. The content sits over the field, which is why the band is a layout and not a template.\n**`figure`** `ReactNode` · required · default: — — The figure the field runs on, usually a `SignalField` or a `PulseGraph`. Required rather than optional, and the Block draws no figure of its own, because a field of invented marks is the retired line's defect rebuilt: the numbers were never collected and the motion says a system is running. A node is a node the caller named, a reading is a reading the caller holds, and an empty figure is an empty figure. What the Block owns is the layer the figure rides on, which is where a cycle can be applied without the figure having to know it is being animated.\n**`label`** `string` · required · default: — — The figure's accessible name, and the only thing a screen reader reads from the field. Required, and the reason this Block is the claim half of the two is on the Component's own JSDoc. See there for why the atmosphere half is not an arm here.\n**`field`** `Backdrop01Field` · required · default: — — Which cycle the field runs. See `Backdrop01Field`. Required rather than defaulted, because the cycle is a claim about the figure and only the caller knows what the figure claims. A default would be this Block choosing the mechanism a consumer's own claim gets made by, and a backdrop whose wavefront does not match its subject is a decoration wearing a caption.\n**`intensity`** `Backdrop01Intensity` · optional · default: `'subtle'` — How loud the field is behind the copy. Opacity on the layer, and the reasoning for it not being a length is on `INTENSITY` above, where the one-second floor is the reason.\n**`paused`** `boolean` · optional · default: `false` — Whether the field is stopped. The controlled arm. Pass it with `pauseControl={false}` and put the button in `children` when the state belongs to something larger than this band: a dashboard that stops every figure on the page together, or a settings toggle, is one state and several bands, and a state this Block owns would be a second copy of it that disagrees with the first. With `pauseControl` set, the same prop is the value the Block starts from and owns from then on, which is the usual defaultValue arrangement and is documented on the prop because it is the one place a reader of a call site could otherwise be surprised.\n**`pauseControl`** `boolean` · optional · default: `false` — Whether the Block draws the pause control. Off by default because a control on every backdrop is a control on every page a consumer puts one on, and most of those pages have a figure that is worth stopping and no reason to offer it. See the Component JSDoc for why the control is this Block's own button rather than a slot, which is the decision this prop and the two labels after it exist to record.\n**`pauseLabel`** `string` · optional · default: — — The words on the control while the field runs. Required with `pauseControl`. A prop, not a default, for the reason every accessible name in this package is a prop: the control is a sentence a reader hears, every language words stopping differently, and a default here would be an English string in every consumer's product that they cannot translate without forking the library.\n**`resumeLabel`** `string` · optional · default: — — The words on the control while the field is stopped. Required with `pauseControl`. A second prop rather than a second half of the first, because \"Pause\" and \"Play\" are not a pair in every language, and because a caller whose control says \"Stop the sweep\" and \"Sweep stopped\" is describing a figure rather than naming a transport control.\n**`headingLevel`** `HeadingLevel` · optional · default: `'h2'` — Heading level for the section title. See `HeadingLevel`.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Block. Changing a Prism-owned visual property from here is prohibited.\n\n## Composition\n\n**`exports`** `Backdrop01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/backdrop-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `Button, PulseGraph, Section, SignalField` — the public Prism exports this item composes\n",
|
|
3856
3934
|
"props": "## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the section title. See the No-Default-Eyebrow Rule.\n**`title`** `ReactNode` · optional · default: — — The section title. Omit it for a band composed under its own heading, which is the usual case for something placed directly under a hero.\n**`description`** `ReactNode` · optional · default: — — One or two sentences under the title.\n**`children`** `ReactNode` · required · default: — — The band's own content, composed by the caller. A slot rather than a set of copy props, and the reason is that the content of a band is the only thing about it that Prism cannot know: it is a headline, a paragraph, two actions and a figure in some arrangement, and a Block that took a prop for each would be four arrangements wearing one name. The content sits over the field, which is why the band is a layout and not a template.\n**`figure`** `ReactNode` · required · default: — — The figure the field runs on, usually a `SignalField` or a `PulseGraph`. Required rather than optional, and the Block draws no figure of its own, because a field of invented marks is the retired line's defect rebuilt: the numbers were never collected and the motion says a system is running. A node is a node the caller named, a reading is a reading the caller holds, and an empty figure is an empty figure. What the Block owns is the layer the figure rides on, which is where a cycle can be applied without the figure having to know it is being animated.\n**`label`** `string` · required · default: — — The figure's accessible name, and the only thing a screen reader reads from the field. Required, and the reason this Block is the claim half of the two is on the Component's own JSDoc. See there for why the atmosphere half is not an arm here.\n**`field`** `Backdrop01Field` · required · default: — — Which cycle the field runs. See `Backdrop01Field`. Required rather than defaulted, because the cycle is a claim about the figure and only the caller knows what the figure claims. A default would be this Block choosing the mechanism a consumer's own claim gets made by, and a backdrop whose wavefront does not match its subject is a decoration wearing a caption.\n**`intensity`** `Backdrop01Intensity` · optional · default: `'subtle'` — How loud the field is behind the copy. Opacity on the layer, and the reasoning for it not being a length is on `INTENSITY` above, where the one-second floor is the reason.\n**`paused`** `boolean` · optional · default: `false` — Whether the field is stopped. The controlled arm. Pass it with `pauseControl={false}` and put the button in `children` when the state belongs to something larger than this band: a dashboard that stops every figure on the page together, or a settings toggle, is one state and several bands, and a state this Block owns would be a second copy of it that disagrees with the first. With `pauseControl` set, the same prop is the value the Block starts from and owns from then on, which is the usual defaultValue arrangement and is documented on the prop because it is the one place a reader of a call site could otherwise be surprised.\n**`pauseControl`** `boolean` · optional · default: `false` — Whether the Block draws the pause control. Off by default because a control on every backdrop is a control on every page a consumer puts one on, and most of those pages have a figure that is worth stopping and no reason to offer it. See the Component JSDoc for why the control is this Block's own button rather than a slot, which is the decision this prop and the two labels after it exist to record.\n**`pauseLabel`** `string` · optional · default: — — The words on the control while the field runs. Required with `pauseControl`. A prop, not a default, for the reason every accessible name in this package is a prop: the control is a sentence a reader hears, every language words stopping differently, and a default here would be an English string in every consumer's product that they cannot translate without forking the library.\n**`resumeLabel`** `string` · optional · default: — — The words on the control while the field is stopped. Required with `pauseControl`. A second prop rather than a second half of the first, because \"Pause\" and \"Play\" are not a pair in every language, and because a caller whose control says \"Stop the sweep\" and \"Sweep stopped\" is describing a figure rather than naming a transport control.\n**`headingLevel`** `HeadingLevel` · optional · default: `'h2'` — Heading level for the section title. See `HeadingLevel`.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Block. Changing a Prism-owned visual property from here is prohibited.",
|
|
3857
3935
|
"composition": "## Composition\n\n**`exports`** `Backdrop01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/backdrop-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `Button, PulseGraph, Section, SignalField` — the public Prism exports this item composes",
|
|
3858
3936
|
"example": {
|
|
@@ -4774,7 +4852,7 @@
|
|
|
4774
4852
|
"Gantt01"
|
|
4775
4853
|
],
|
|
4776
4854
|
"importLine": "import { Gantt01 } from '@nanisoft/prism-ui/blocks/gantt-01'",
|
|
4777
|
-
"doc": "# Gantt01\n\nA schedule of named tasks and bars placed from a caller range, in final positions at first paint, with the same rows as a hidden table.\n\n```tsx\nimport { Gantt01 } from '@nanisoft/prism-ui/blocks/gantt-01'\n```\n\n## Overview\n\nGantt01 is a schedule: a left column of task names, a field of bars positioned\nagainst a date range, a scale above, and a rule for today. Every bar is in its\nfinal position at first paint, and the same rows are rendered as a hidden table so\nthe numbers are readable without sight of the geometry.\n\nThe two things that make it correct are worth stating separately, because they are\nthe two things a hand-rolled version gets wrong.\n\n**The geometry comes from the range rather than from the widest bar.** A field\nsized to its own content would put the left edge at the earliest bar that happened\nto be there, so a task that starts before every other task would be drawn hanging\noff the left of its own chart, clipped by the field, and a reader would see a short\nbar where the truth is a long one. So the field is a range: yours, or the earliest\nstart and the latest end of the tasks. A bar outside a range you passed is clipped\nat the edge rather than pushing the field wider, and the table behind the figure\ncarries its true dates.\n\n**There is no motion, so reduced motion costs the reader nothing.** No draw-in, no\nbar that grows from nothing, no scroll effect, no entrance. DESIGN.md's Motion\nsection refuses entrance animation outright, and a schedule is the case where that\nis most obviously right: a bar that arrives is a bar whose arrival a reader has to\nwait for before they can read the relationship it has with the bar above it, and\nthe whole claim of a Gantt is that relationship.\n\n## The hidden table, and its gaps\n\nThe bars are `aria-hidden` and the numbers are in a table, which is the rule\n`sparkline` and `chart` follow. Everything a schedule draws is a position, and a\nposition is the one thing a screen reader, a crawler and a print stylesheet cannot\nread. The cost is real and worth naming: a table of every task in the document for\na figure that is a few hundred pixels tall.\n\nThe table has no column headings, and that is a gap with a reason. This Block has\nno words for what a start is called, and inventing \"Start\" and \"End\" would be a\nclaim about your plan. The row head is the task's own name, the caption is your\n`label`, and the cells carry your words wherever there are any. The date cells\ncarry the ISO 8601 reading in the mono face, because this Block was not given a\n`locale` and the ones that format dates were.\n\n## Guidelines\n\n### When to use\n\nUse it for a plan: a handful of tasks, a range of weeks or months, and a reader\nasking \"when does this land and what is on top of it\".\n\nUse it when the overlap is the point. Two bars on top of each other is a fact a\nlist cannot show at all, and a schedule whose tasks never overlap is a list.\n\n### When not to use\n\nDo not use it for a queue. A schedule says when, and a queue says what is waiting\nnow. A backlog that cannot be dated is an `Inbox01`.\n\nDo not use it for more than about forty rows. Past that a schedule is a picture\nnobody can scan and the answer is a table, which is `DataTable01`.\n\nDo not use it for a release history. A dated record of what happened is a\n`Changelog01`, and it reads bottom to top rather than left to right.\n\n### Best practices\n\n- Pass `range` whenever the reader needs to see empty time, and omit it when the\n work is the whole story. The derived range is the earliest start to the latest\n end, and a caller who wants a quarter that has not started yet has to say so.\n- Write `scaleLabel` as a function of `unit`, not as a closure over `scale`. That\n way a scale changed in one place cannot leave a formatter labelling weeks as\n days.\n- Keep the labels short. The Block draws one tick per unit with no cap, and the\n labels are your string, so a range that needs shorter labels is a range whose\n formatter returns shorter ones.\n- Pass `todayLabel` whenever you pass `today`. An unlabelled rule is a mark a reader\n has to be told the meaning of.\n- Pass `stateLabel` with every `state`. The run fails without it, and the fill\n colour is a support for the words rather than a substitute for them.\n- Read a bar's exact length from the table when the comparison is between two very\n short tasks. The narrowest a bar is drawn is half a percent of the field, so two\n very short bars are both drawn at about the same width.\n\n### Content guidelines\n\n- `name` is one line. It is a truncated column, not a wrapped one, and a name that\n gets cut is a name a reader cannot match to a ticket.\n- `progress` is a fraction between zero and one. Omit it for planned work, and the\n bar is drawn whole in its state colour.\n- `progressLabel` is the sentence for that fraction, and a fraction in a cell is a\n figure no reader can act on.\n- `todayLabel` is a word a reader would use for the day. \"Today\" in your language\n is the honest answer.\n- `label` is the figure's accessible name and the table's caption, so it should say\n what is plotted and over what period. \"Schedule\" answers no question.\n\n**The plan, with a bar that starts first and a bar that finishes last.**\n\n```tsx\nimport { Gantt01, type GanttScale } from '@nanisoft/prism-ui/blocks/gantt-01'\n\n/**\n * A six-week plan with a task that starts before every other one, a task that\n * finishes after all of them, today marked, and a second copy with no range at\n * all so the derived range can be seen working.\n *\n * The `scaleLabel` is the interesting part of this Demo. It is handed the unit and\n * the tick's index, which means the caller formats the tick rather than the Block\n * printing a date: this package has no locale to format a date in, and the\n * Components that do format dates are the ones that were given a `locale` prop to\n * do it with.\n *\n * The two copies differ in one way each. The first passes a `range`, so the field\n * shows empty time at either end and today sits inside it. The second passes none,\n * so the field is drawn from the earliest start and the latest end of the tasks,\n * and the task that starts first is inside the field rather than hanging off the\n * left of its own chart.\n */\nconst FROM = '2026-09-21T00:00:00Z'\nconst DAY = 86_400_000\n\n/**\n * The words for one tick of the scale, given the unit and the tick's index.\n *\n * The caller already holds the range and the scale, so it can work out which\n * instant a tick sits on without asking the Block for it. That is the arrangement\n * the prop's JSDoc argues: `unit` is a parameter rather than a closure over\n * `scale`, so a caller who changes the scale in one place and forgets the\n * formatter in the other gets caught rather than a fortnight of dates labelled as\n * weeks.\n */\nfunction tickLabel(unit: GanttScale, index: number): string {\n const step = unit === 'day' ? DAY : unit === 'week' ? 7 * DAY : 30 * DAY\n const at = new Date(new Date(FROM).getTime() + index * step)\n if (unit === 'month') {\n return at.toLocaleDateString('en-GB', { month: 'short', year: '2-digit' })\n }\n return at.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })\n}\n\n/** The plan, with a bar that starts first and a bar that finishes last. */\nconst TASKS = [\n {\n id: 'mapping',\n name: 'Fix the Okta group mapping',\n start: '2026-09-14T00:00:00Z',\n end: '2026-09-25T00:00:00Z',\n progress: 0.8,\n progressLabel: (value: number) => `${Math.round(value * 100)} percent done`,\n state: 'active' as const,\n stateLabel: 'Being worked',\n href: '/issues/NEX-412',\n hrefLabel: 'Open NEX-412',\n },\n {\n id: 'run',\n name: 'Tell somebody when a run rejects rows',\n start: '2026-09-21T00:00:00Z',\n end: '2026-09-28T00:00:00Z',\n progress: 0.35,\n progressLabel: (value: number) => `${Math.round(value * 100)} percent done`,\n state: 'active' as const,\n stateLabel: 'Being worked',\n },\n {\n id: 'export',\n name: 'Warehouse export with stable identifiers',\n start: '2026-09-28T00:00:00Z',\n end: '2026-10-16T00:00:00Z',\n },\n {\n id: 'access',\n name: 'Access review for the warehouse',\n start: '2026-10-05T00:00:00Z',\n end: '2026-10-09T00:00:00Z',\n state: 'blocked' as const,\n stateLabel: 'Waiting on the data owner',\n },\n {\n id: 'invoice',\n name: 'Reconcile the September invoice run',\n start: '2026-09-14T00:00:00Z',\n end: '2026-09-18T00:00:00Z',\n state: 'done' as const,\n stateLabel: 'Done last week',\n },\n]\n\nexport default function Gantt01Demo() {\n return (\n <>\n <Gantt01\n headingLevel=\"h3\"\n eyebrow=\"Nexus\"\n title=\"Six weeks of work, with the empty time at either end\"\n description=\"The field is the range you pass. The bar positions are arithmetic over that range and nothing else, and the numbers behind the picture are in the document whether or not anybody can see them.\"\n label=\"Six weeks of work from 21 September to 2 November 2026\"\n tasks={TASKS}\n range={{ from: FROM, to: '2026-11-02T00:00:00Z' }}\n scale=\"week\"\n today=\"2026-09-29T12:00:00Z\"\n todayLabel=\"Today\"\n scaleLabel={tickLabel}\n empty=\"Nothing is planned. That is a claim about your plan, so the sentence is yours.\"\n />\n\n <Gantt01\n headingLevel=\"h3\"\n eyebrow=\"Nexus\"\n title=\"And the same plan with the range derived from the tasks\"\n description=\"No range, so the field runs from the earliest start to the latest end. The task that starts on the 14th is what sets the left edge, which is why it is inside the chart rather than clipped by it.\"\n label=\"The same plan, drawn from the earliest start to the latest end\"\n tasks={TASKS}\n scale=\"week\"\n scaleLabel={tickLabel}\n empty=\"Nothing is planned.\"\n />\n </>\n )\n}\n```\n\n## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the section title. See the No-Default-Eyebrow Rule.\n**`title`** `ReactNode` · required · default: — — The section title, and required. A figure with no heading is a picture on a page, and this is the one Block in this wave whose whole content is a picture.\n**`description`** `ReactNode` · optional · default: — — One or two sentences under the title.\n**`tasks`** `readonly GanttTask[]` · required · default: — — The tasks, in the order a reader should meet them. Order is the caller's and the Block does not sort. A schedule is very often already ordered by the thing that produced it, by start date or by a dependency order the Block cannot see, and a Block that sorted by start would quietly discard a sequence that meant something.\n**`range`** `GanttRange` · optional · default: — — The dates the field is drawn over. Omit it and the Block derives the range from the tasks themselves: the earliest start and the latest end. That is the right default and it is the reason a task which starts before every other task is not clipped, because that task is what sets the left edge of the field. Pass it when the reader needs to see empty time at either end, a quarter that has not started yet, or a run that ends after the last task does, and in that case the range is the claim the figure is making about the period on show.\n**`scale`** `GanttScale` · optional · default: `'week'` — The unit the scale above the field is drawn in. A week is the default because it is the unit a plan is written in, and a plan is what a schedule most often shows. Day and month are for a schedule being read as a delivery window or as a programme of releases.\n**`today`** `string | number` · optional · default: — — The moment to mark on the field, in the same three forms as a task's dates. Optional because a schedule is often historical or projected, in which case there is no today on it to mark. When it is set the field carries a rule at that position and the caller's `todayLabel` beside it, because an unlabelled rule is a mark a reader has to be told the meaning of.\n**`todayLabel`** `string` · optional · default: — — The words for the rule that marks today, and required whenever `today` is set. The run fails without it rather than drawing an unlabelled line, for the reason it fails without a `stateLabel`: a mark with no words is a mark a reader has to guess at.\n**`scaleLabel`** `(unit: GanttScale, count: number) => string` · required · default: — — The words for one tick of the scale, given the unit and the tick's index from zero. Required, and the reason is that a tick label is a sentence about a position on a range: \"1 September\", \"Week 36\", \"Q3\", \"semaine 36\". A Block that formatted those itself would have chosen a locale, and this package has no locale to choose: it is a token pipeline and a component library, and the components that do format dates are the ones that were given a `locale` prop to do it with. So the caller writes the formatter and this Block asks it once per tick. The `unit` is a parameter rather than a closure over `scale` for a reason that is only visible from the call site: a caller who changes `scale` in one place and forgets the formatter in another gets a fortnight of dates labelled as weeks. Taking the unit as an argument means the formatter is told what it is formatting on every call.\n**`label`** `string` · required · default: — — The figure's accessible name, and the only name the geometry carries. Required rather than defaulted, for the reason `Chart` states: two schedules on one page are two figures a reader cannot tell apart, and a figure named \"Chart\" is a Block shipping a sentence about a product it knows nothing about. It is also the caption of the table behind the figure, so the numbers are announced with the same name the picture is.\n**`empty`** `ReactNode` · required · default: — — What the Block renders in place of the figure when there are no tasks. Required and a node, for the reason it is required on every Block here: a schedule with nothing on it is a claim, and \"nothing is planned\" is a different claim in a product that is between releases and in a product whose plan failed to load.\n**`headingLevel`** `HeadingLevel` · optional · default: `'h2'` — Heading level for the section title.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Block. Changing a Prism-owned visual property from here is prohibited.\n\n## Composition\n\n**`exports`** `Gantt01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/gantt-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `CtaLink, Section, Status, Table` — the public Prism exports this item composes\n",
|
|
4855
|
+
"doc": "# Gantt01\n\nA schedule of named tasks and bars placed from a caller range, in final positions at first paint, with the same rows as a hidden table.\n\n```tsx\nimport { Gantt01 } from '@nanisoft/prism-ui/blocks/gantt-01'\n```\n\n## Overview\n\nGantt01 is a schedule: a left column of task names, a field of bars positioned\nagainst a date range, a scale above, and a rule for today. Every bar is in its\nfinal position at first paint, and the same rows are rendered as a hidden table so\nthe numbers are readable without sight of the geometry.\n\nThe two things that make it correct are worth stating separately, because they are\nthe two things a hand-rolled version gets wrong.\n\n**The geometry comes from the range rather than from the widest bar.** A field\nsized to its own content would put the left edge at the earliest bar that happened\nto be there, so a task that starts before every other task would be drawn hanging\noff the left of its own chart, clipped by the field, and a reader would see a short\nbar where the truth is a long one. So the field is a range: yours, or the earliest\nstart and the latest end of the tasks. A bar outside a range you passed is clipped\nat the edge rather than pushing the field wider, and the table behind the figure\ncarries its true dates.\n\n**There is no motion, so reduced motion costs the reader nothing.** No draw-in, no\nbar that grows from nothing, no entrance. DESIGN.md's Motion section lets a band\narrive with the reader's own scroll, and a schedule is the case where arriving\nwould misread: a bar that appears is a bar whose appearance a reader has to wait\nfor before they can read the relationship it has with the bar above it, and\nthe whole claim of a Gantt is that relationship.\n\n## The hidden table, and its gaps\n\nThe bars are `aria-hidden` and the numbers are in a table, which is the rule\n`sparkline` and `chart` follow. Everything a schedule draws is a position, and a\nposition is the one thing a screen reader, a crawler and a print stylesheet cannot\nread. The cost is real and worth naming: a table of every task in the document for\na figure that is a few hundred pixels tall.\n\nThe table has no column headings, and that is a gap with a reason. This Block has\nno words for what a start is called, and inventing \"Start\" and \"End\" would be a\nclaim about your plan. The row head is the task's own name, the caption is your\n`label`, and the cells carry your words wherever there are any. The date cells\ncarry the ISO 8601 reading in the mono face, because this Block was not given a\n`locale` and the ones that format dates were.\n\n## Guidelines\n\n### When to use\n\nUse it for a plan: a handful of tasks, a range of weeks or months, and a reader\nasking \"when does this land and what is on top of it\".\n\nUse it when the overlap is the point. Two bars on top of each other is a fact a\nlist cannot show at all, and a schedule whose tasks never overlap is a list.\n\n### When not to use\n\nDo not use it for a queue. A schedule says when, and a queue says what is waiting\nnow. A backlog that cannot be dated is an `Inbox01`.\n\nDo not use it for more than about forty rows. Past that a schedule is a picture\nnobody can scan and the answer is a table, which is `DataTable01`.\n\nDo not use it for a release history. A dated record of what happened is a\n`Changelog01`, and it reads bottom to top rather than left to right.\n\n### Best practices\n\n- Pass `range` whenever the reader needs to see empty time, and omit it when the\n work is the whole story. The derived range is the earliest start to the latest\n end, and a caller who wants a quarter that has not started yet has to say so.\n- Write `scaleLabel` as a function of `unit`, not as a closure over `scale`. That\n way a scale changed in one place cannot leave a formatter labelling weeks as\n days.\n- Keep the labels short. The Block draws one tick per unit with no cap, and the\n labels are your string, so a range that needs shorter labels is a range whose\n formatter returns shorter ones.\n- Pass `todayLabel` whenever you pass `today`. An unlabelled rule is a mark a reader\n has to be told the meaning of.\n- Pass `stateLabel` with every `state`. The run fails without it, and the fill\n colour is a support for the words rather than a substitute for them.\n- Read a bar's exact length from the table when the comparison is between two very\n short tasks. The narrowest a bar is drawn is half a percent of the field, so two\n very short bars are both drawn at about the same width.\n\n### Content guidelines\n\n- `name` is one line. It is a truncated column, not a wrapped one, and a name that\n gets cut is a name a reader cannot match to a ticket.\n- `progress` is a fraction between zero and one. Omit it for planned work, and the\n bar is drawn whole in its state colour.\n- `progressLabel` is the sentence for that fraction, and a fraction in a cell is a\n figure no reader can act on.\n- `todayLabel` is a word a reader would use for the day. \"Today\" in your language\n is the honest answer.\n- `label` is the figure's accessible name and the table's caption, so it should say\n what is plotted and over what period. \"Schedule\" answers no question.\n\n**The plan, with a bar that starts first and a bar that finishes last.**\n\n```tsx\nimport { Gantt01, type GanttScale } from '@nanisoft/prism-ui/blocks/gantt-01'\n\n/**\n * A six-week plan with a task that starts before every other one, a task that\n * finishes after all of them, today marked, and a second copy with no range at\n * all so the derived range can be seen working.\n *\n * The `scaleLabel` is the interesting part of this Demo. It is handed the unit and\n * the tick's index, which means the caller formats the tick rather than the Block\n * printing a date: this package has no locale to format a date in, and the\n * Components that do format dates are the ones that were given a `locale` prop to\n * do it with.\n *\n * The two copies differ in one way each. The first passes a `range`, so the field\n * shows empty time at either end and today sits inside it. The second passes none,\n * so the field is drawn from the earliest start and the latest end of the tasks,\n * and the task that starts first is inside the field rather than hanging off the\n * left of its own chart.\n */\nconst FROM = '2026-09-21T00:00:00Z'\nconst DAY = 86_400_000\n\n/**\n * The words for one tick of the scale, given the unit and the tick's index.\n *\n * The caller already holds the range and the scale, so it can work out which\n * instant a tick sits on without asking the Block for it. That is the arrangement\n * the prop's JSDoc argues: `unit` is a parameter rather than a closure over\n * `scale`, so a caller who changes the scale in one place and forgets the\n * formatter in the other gets caught rather than a fortnight of dates labelled as\n * weeks.\n */\nfunction tickLabel(unit: GanttScale, index: number): string {\n const step = unit === 'day' ? DAY : unit === 'week' ? 7 * DAY : 30 * DAY\n const at = new Date(new Date(FROM).getTime() + index * step)\n if (unit === 'month') {\n return at.toLocaleDateString('en-GB', { month: 'short', year: '2-digit' })\n }\n return at.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })\n}\n\n/** The plan, with a bar that starts first and a bar that finishes last. */\nconst TASKS = [\n {\n id: 'mapping',\n name: 'Fix the Okta group mapping',\n start: '2026-09-14T00:00:00Z',\n end: '2026-09-25T00:00:00Z',\n progress: 0.8,\n progressLabel: (value: number) => `${Math.round(value * 100)} percent done`,\n state: 'active' as const,\n stateLabel: 'Being worked',\n href: '/issues/NEX-412',\n hrefLabel: 'Open NEX-412',\n },\n {\n id: 'run',\n name: 'Tell somebody when a run rejects rows',\n start: '2026-09-21T00:00:00Z',\n end: '2026-09-28T00:00:00Z',\n progress: 0.35,\n progressLabel: (value: number) => `${Math.round(value * 100)} percent done`,\n state: 'active' as const,\n stateLabel: 'Being worked',\n },\n {\n id: 'export',\n name: 'Warehouse export with stable identifiers',\n start: '2026-09-28T00:00:00Z',\n end: '2026-10-16T00:00:00Z',\n },\n {\n id: 'access',\n name: 'Access review for the warehouse',\n start: '2026-10-05T00:00:00Z',\n end: '2026-10-09T00:00:00Z',\n state: 'blocked' as const,\n stateLabel: 'Waiting on the data owner',\n },\n {\n id: 'invoice',\n name: 'Reconcile the September invoice run',\n start: '2026-09-14T00:00:00Z',\n end: '2026-09-18T00:00:00Z',\n state: 'done' as const,\n stateLabel: 'Done last week',\n },\n]\n\nexport default function Gantt01Demo() {\n return (\n <>\n <Gantt01\n headingLevel=\"h3\"\n eyebrow=\"Nexus\"\n title=\"Six weeks of work, with the empty time at either end\"\n description=\"The field is the range you pass. The bar positions are arithmetic over that range and nothing else, and the numbers behind the picture are in the document whether or not anybody can see them.\"\n label=\"Six weeks of work from 21 September to 2 November 2026\"\n tasks={TASKS}\n range={{ from: FROM, to: '2026-11-02T00:00:00Z' }}\n scale=\"week\"\n today=\"2026-09-29T12:00:00Z\"\n todayLabel=\"Today\"\n scaleLabel={tickLabel}\n empty=\"Nothing is planned. That is a claim about your plan, so the sentence is yours.\"\n />\n\n <Gantt01\n headingLevel=\"h3\"\n eyebrow=\"Nexus\"\n title=\"And the same plan with the range derived from the tasks\"\n description=\"No range, so the field runs from the earliest start to the latest end. The task that starts on the 14th is what sets the left edge, which is why it is inside the chart rather than clipped by it.\"\n label=\"The same plan, drawn from the earliest start to the latest end\"\n tasks={TASKS}\n scale=\"week\"\n scaleLabel={tickLabel}\n empty=\"Nothing is planned.\"\n />\n </>\n )\n}\n```\n\n## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the section title. See the No-Default-Eyebrow Rule.\n**`title`** `ReactNode` · required · default: — — The section title, and required. A figure with no heading is a picture on a page, and this is the one Block in this wave whose whole content is a picture.\n**`description`** `ReactNode` · optional · default: — — One or two sentences under the title.\n**`tasks`** `readonly GanttTask[]` · required · default: — — The tasks, in the order a reader should meet them. Order is the caller's and the Block does not sort. A schedule is very often already ordered by the thing that produced it, by start date or by a dependency order the Block cannot see, and a Block that sorted by start would quietly discard a sequence that meant something.\n**`range`** `GanttRange` · optional · default: — — The dates the field is drawn over. Omit it and the Block derives the range from the tasks themselves: the earliest start and the latest end. That is the right default and it is the reason a task which starts before every other task is not clipped, because that task is what sets the left edge of the field. Pass it when the reader needs to see empty time at either end, a quarter that has not started yet, or a run that ends after the last task does, and in that case the range is the claim the figure is making about the period on show.\n**`scale`** `GanttScale` · optional · default: `'week'` — The unit the scale above the field is drawn in. A week is the default because it is the unit a plan is written in, and a plan is what a schedule most often shows. Day and month are for a schedule being read as a delivery window or as a programme of releases.\n**`today`** `string | number` · optional · default: — — The moment to mark on the field, in the same three forms as a task's dates. Optional because a schedule is often historical or projected, in which case there is no today on it to mark. When it is set the field carries a rule at that position and the caller's `todayLabel` beside it, because an unlabelled rule is a mark a reader has to be told the meaning of.\n**`todayLabel`** `string` · optional · default: — — The words for the rule that marks today, and required whenever `today` is set. The run fails without it rather than drawing an unlabelled line, for the reason it fails without a `stateLabel`: a mark with no words is a mark a reader has to guess at.\n**`scaleLabel`** `(unit: GanttScale, count: number) => string` · required · default: — — The words for one tick of the scale, given the unit and the tick's index from zero. Required, and the reason is that a tick label is a sentence about a position on a range: \"1 September\", \"Week 36\", \"Q3\", \"semaine 36\". A Block that formatted those itself would have chosen a locale, and this package has no locale to choose: it is a token pipeline and a component library, and the components that do format dates are the ones that were given a `locale` prop to do it with. So the caller writes the formatter and this Block asks it once per tick. The `unit` is a parameter rather than a closure over `scale` for a reason that is only visible from the call site: a caller who changes `scale` in one place and forgets the formatter in another gets a fortnight of dates labelled as weeks. Taking the unit as an argument means the formatter is told what it is formatting on every call.\n**`label`** `string` · required · default: — — The figure's accessible name, and the only name the geometry carries. Required rather than defaulted, for the reason `Chart` states: two schedules on one page are two figures a reader cannot tell apart, and a figure named \"Chart\" is a Block shipping a sentence about a product it knows nothing about. It is also the caption of the table behind the figure, so the numbers are announced with the same name the picture is.\n**`empty`** `ReactNode` · required · default: — — What the Block renders in place of the figure when there are no tasks. Required and a node, for the reason it is required on every Block here: a schedule with nothing on it is a claim, and \"nothing is planned\" is a different claim in a product that is between releases and in a product whose plan failed to load.\n**`headingLevel`** `HeadingLevel` · optional · default: `'h2'` — Heading level for the section title.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Block. Changing a Prism-owned visual property from here is prohibited.\n\n## Composition\n\n**`exports`** `Gantt01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/gantt-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `CtaLink, Section, Status, Table` — the public Prism exports this item composes\n",
|
|
4778
4856
|
"props": "## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the section title. See the No-Default-Eyebrow Rule.\n**`title`** `ReactNode` · required · default: — — The section title, and required. A figure with no heading is a picture on a page, and this is the one Block in this wave whose whole content is a picture.\n**`description`** `ReactNode` · optional · default: — — One or two sentences under the title.\n**`tasks`** `readonly GanttTask[]` · required · default: — — The tasks, in the order a reader should meet them. Order is the caller's and the Block does not sort. A schedule is very often already ordered by the thing that produced it, by start date or by a dependency order the Block cannot see, and a Block that sorted by start would quietly discard a sequence that meant something.\n**`range`** `GanttRange` · optional · default: — — The dates the field is drawn over. Omit it and the Block derives the range from the tasks themselves: the earliest start and the latest end. That is the right default and it is the reason a task which starts before every other task is not clipped, because that task is what sets the left edge of the field. Pass it when the reader needs to see empty time at either end, a quarter that has not started yet, or a run that ends after the last task does, and in that case the range is the claim the figure is making about the period on show.\n**`scale`** `GanttScale` · optional · default: `'week'` — The unit the scale above the field is drawn in. A week is the default because it is the unit a plan is written in, and a plan is what a schedule most often shows. Day and month are for a schedule being read as a delivery window or as a programme of releases.\n**`today`** `string | number` · optional · default: — — The moment to mark on the field, in the same three forms as a task's dates. Optional because a schedule is often historical or projected, in which case there is no today on it to mark. When it is set the field carries a rule at that position and the caller's `todayLabel` beside it, because an unlabelled rule is a mark a reader has to be told the meaning of.\n**`todayLabel`** `string` · optional · default: — — The words for the rule that marks today, and required whenever `today` is set. The run fails without it rather than drawing an unlabelled line, for the reason it fails without a `stateLabel`: a mark with no words is a mark a reader has to guess at.\n**`scaleLabel`** `(unit: GanttScale, count: number) => string` · required · default: — — The words for one tick of the scale, given the unit and the tick's index from zero. Required, and the reason is that a tick label is a sentence about a position on a range: \"1 September\", \"Week 36\", \"Q3\", \"semaine 36\". A Block that formatted those itself would have chosen a locale, and this package has no locale to choose: it is a token pipeline and a component library, and the components that do format dates are the ones that were given a `locale` prop to do it with. So the caller writes the formatter and this Block asks it once per tick. The `unit` is a parameter rather than a closure over `scale` for a reason that is only visible from the call site: a caller who changes `scale` in one place and forgets the formatter in another gets a fortnight of dates labelled as weeks. Taking the unit as an argument means the formatter is told what it is formatting on every call.\n**`label`** `string` · required · default: — — The figure's accessible name, and the only name the geometry carries. Required rather than defaulted, for the reason `Chart` states: two schedules on one page are two figures a reader cannot tell apart, and a figure named \"Chart\" is a Block shipping a sentence about a product it knows nothing about. It is also the caption of the table behind the figure, so the numbers are announced with the same name the picture is.\n**`empty`** `ReactNode` · required · default: — — What the Block renders in place of the figure when there are no tasks. Required and a node, for the reason it is required on every Block here: a schedule with nothing on it is a claim, and \"nothing is planned\" is a different claim in a product that is between releases and in a product whose plan failed to load.\n**`headingLevel`** `HeadingLevel` · optional · default: `'h2'` — Heading level for the section title.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Block. Changing a Prism-owned visual property from here is prohibited.",
|
|
4779
4857
|
"composition": "## Composition\n\n**`exports`** `Gantt01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/gantt-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `CtaLink, Section, Status, Table` — the public Prism exports this item composes",
|
|
4780
4858
|
"example": {
|
|
@@ -4855,8 +4933,8 @@
|
|
|
4855
4933
|
"Hero01"
|
|
4856
4934
|
],
|
|
4857
4935
|
"importLine": "import { Hero01 } from '@nanisoft/prism-ui/blocks/hero-01'",
|
|
4858
|
-
"doc": "# Hero01\n\nA centered marketing hero with an optional eyebrow and one or two actions.\n\n```tsx\nimport { Hero01 } from '@nanisoft/prism-ui/blocks/hero-01'\n```\n\n## Overview\n\nHero01 is the opening band of a marketing page: an optional eyebrow, a headline,\nan optional supporting line, and up to two actions. The `01` suffix is the\nvariant ordinal of the hero family.\n\nIt has two forms and they are one Block. The copy can sit centered over the\ncontainer, which suits a page whose claim is a sentence. Or it can sit in the\nleft column of a two-column band with a live figure in the right, which suits a\nproduct whose claim is a mechanism. Passing `instrument` picks the second form.\n\nEvery string is a prop. The block ships no eyebrow and no headline, because a\nhero with hardcoded copy makes every consumer inherit a claim about their own\nproduct.\n\n## Guidelines\n\n### When to use\n\nUse Hero01 as the first band of a page whose purpose is to introduce a product\nor a release. It works when there is one clear headline and one clear next\naction.\n\nUse the split form when the product does something rather than being something.\nA pipeline that runs, a surface that is captured, a fleet that is observed: each\nof those is a mechanism, and a mechanism reads better shown than described. The\nfigure should be one that would still be true with every animation stopped, so\nwhat the reader gets from it is the claim and the motion is only the emphasis.\n\n### When not to use\n\nDo not use Hero01 inside an application screen. It is a marketing composition,\nand its generous layout competes with the task the screen exists to support.\n\nDo not use it twice on one page. A second hero splits the reader's attention and\nleaves two claims competing for the same position.\n\nDo not use it when the headline is really a section heading. Use `SectionHeading`\ninside `Section` instead.\n\nDo not pass a figure that is only interesting while it moves. A slot that reads\nas empty in a still frame is a decoration with a slot, and this system does not\nauthor those.\n\n### Best practices\n\n- Pass one primary action and at most one supporting action. More than two\n actions turn a decision into a menu.\n- Give the first action `default` and the second `outline`. The block already\n defaults to that order.\n- Give every action a destination, or hand the block the control you want instead.\n `href` is **required** on the action arm, and there is no third arm, so a\n `{ label }` on its own does not compile:\n\n ```tsx\n // An anchor. `href` is required on this arm, and `slot` is forbidden on it.\n { label: 'Get started', href: '/docs' }\n // Your own control, for anything Prism cannot make work: a router link, a\n // submit button, a menu trigger. The block places it and adds no class to it.\n { slot: <NextLink href=\"/signup\">Create an account</NextLink> }\n ```\n\n The earlier shape accepted a third thing, `{ label: 'Coming soon' }`, and\n rendered it as a `Button`. That control was focusable, announced as a button,\n and activated to nothing, in the position a reader looks first. A block ships\n no behaviour, so it cannot make a control work, and the two errors a caller can\n make here are both compile errors rather than a runtime branch.\n- The forward arrow on the first action follows the **element**, not the position.\n Every action the block renders is a link, so the position is the whole of the\n test, and a `slot` at the front of the row wears no arrow because the control\n you drew owns its own marks. A second action gets no arrow even when it is a\n link, since the arrow marks the row's one primary destination. The default\n *variant* is still positional: what looks primary is about the row's shape,\n while who drew the control is about the element.\n- Set `headingLevel=\"h1\"` when the hero is the page's top-level heading. Use the\n default `h2` when the page already owns an `h1`.\n- Pass an eyebrow only when it is true and current. It renders nothing when\n absent, so the honest state is no eyebrow.\n- Wrap the figure in `InstrumentPanel01`. A bare figure on the page ground reads\n as an illustration; the same figure in a panel reads as an instrument, which\n is what it is.\n\n### Content guidelines\n\n- Keep the headline under about eight words and let it wrap to two lines at\n most.\n- Write the headline as an outcome, not a feature name.\n- The description is one sentence and states who the product is for or what\n changes if the reader continues.\n\n**The split form, with a live figure beside the copy.**\n\n```tsx\nimport { Hero01 } from '@nanisoft/prism-ui/blocks/hero-01'\nimport { InstrumentPanel01 } from '@nanisoft/prism-ui/blocks/instrument-panel-01'\nimport { PulseGraph } from '@nanisoft/prism-ui/components/pulse-graph'\n\n/** The centered form, with a heading level that nests under the page. */\nfunction Centered() {\n return (\n <Hero01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"A headline that fits on two lines\"\n description=\"One or two sentences of supporting copy, so you can judge the measure and the spacing around it.\"\n actions={[\n { label: 'Primary action', href: '#centered' },\n { label: 'Secondary', href: '#split', variant: 'outline' },\n ]}\n />\n )\n}\n\n/**\n * The split form, with a live figure beside the copy.\n *\n * The figure is a PulseGraph of three stages on one rail, which is the shape a\n * pipeline takes: the stages are lanes, the edges carry, and the marker travels\n * between them. The names are the preview's own and are passed in like any other\n * copy, because a Block ships none.\n */\nconst STAGES = [\n { id: 'issue', name: 'issue', note: 'one', x: 0, y: 0.5, lane: 0 },\n { id: 'build', name: 'build', note: 'fresh', x: 0.5, y: 0.5, lane: 1, emphasis: true },\n { id: 'review', name: 'review', note: 'a person', x: 1, y: 0.5, lane: 2 },\n]\n\nconst EDGES = [\n { from: 'issue', to: 'build', carries: true },\n { from: 'build', to: 'review', carries: true },\n]\n\nfunction Split() {\n return (\n <Hero01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"The same block, beside a running system\"\n description=\"A figure that shows the mechanism. It is still correct with every animation stopped.\"\n actions={[\n { label: 'Primary action', href: '#split' },\n { label: 'Secondary', href: '#centered', variant: 'outline' },\n ]}\n instrument={\n <InstrumentPanel01\n label=\"the loop, running\"\n state=\"live\"\n stateLabel=\"live\"\n caption=\"Three stages on one rail, with a marker travelling between them.\"\n footnote=\"A figure in this block is the consumer's. This one is a PulseGraph of three stages.\"\n >\n <PulseGraph nodes={STAGES} relations={EDGES} label=\"Three stages on one rail\" />\n </InstrumentPanel01>\n }\n />\n )\n}\n\nexport default function Hero01Demo() {\n return (\n <div id=\"centered\">\n <Centered />\n <div id=\"split\">\n <Split />\n </div>\n </div>\n )\n}\n```\n\n## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the headline. Intentionally has no default: an earlier version shipped a hardcoded \"Now in open beta\" pill, which meant every consumer who installed this block inherited a status badge making a claim about their product. Pass one if you mean it.\n**`title`** `ReactNode` · required · default: — — The headline. A `ReactNode` rather than a `string`, because `SectionHeading` already takes one and a Block that narrowed it would have been the odd one out. Three of the five product sites set the emphasised word in their own mark rather than in a wrapper, so a `string` here would have forced each of them to drop the emphasis or reach for a second component. Every string a site renders is still the site's, and `check-block-copy.mjs` still holds this Block to shipping none.\n**`description`** `ReactNode` · optional · default: — — The supporting line. A node for the same reason the title is one.\n**`actions`** `HeroAction[]` · optional · default: — — Up to two actions. See `HeroAction` and its two arms.\n**`align`** `'center' | 'left'` · optional · default: `'center'` — Whether the copy is centered in the band or set flush to the left edge of the container. This is the hero's own alignment and not `SectionHeading`'s, because the two are not the same decision: a left-aligned hero is nearly always the left half of a two-column band with a panel or an instrument beside it, and that column is narrower than the container, so the alignment has to be the hero's before the container can be anything. It has no visual effect on its own, which is the point: it aligns the copy inside whatever width it is given.\n**`instrument`** `ReactNode` · optional · default: — — The figure beside the copy: a live view of the thing the copy is about. A slot, and the reason is the whole shape of this Block's second form. The hero that names what a product does and the hero that shows it running are the same section, and every one of the four NaniSoft product sites hand-wrote the two-column band that joins them, each with its own grid, its own gap and its own breakpoint. This is that band. What goes in it is the consumer's, and specifically it should be something that would still be true with every animation stopped: a `PulseGraph` of the pipeline the product runs, a `PulseSeries` of what it captures. A slot that only reads as motion is a decoration with a slot, and this system does not author those. The `instrumented` slot is marked as required when the hero is given one, so a figure cannot be half-wired.\n**`split`** `boolean` · optional · default: — — Puts the copy and the instrument side by side rather than stacked, and narrows the copy to the column it shares the band with. Implied by `instrument` and overridable: a consumer with a narrow instrument and a wide hero may want them stacked anyway.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the title. Defaults to `h2` because a block is composed, not a page: it installs into a consumer's layout and renders inside catalog previews, so the surrounding document already owns the `h1`. A page that uses this block as its top-level heading opts in with `headingLevel=\"h1\"`; a document that already names this section in its own heading passes one level deeper. See `HeadingLevel`.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited.\n\n## Composition\n\n**`exports`** `Hero01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/hero-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `CtaLink, InstrumentPanel01, PulseGraph, Section` — the public Prism exports this item composes\n\n## Blocks\n\n- [InstrumentPanel01](https://prism.nanisoft.com/blocks/instrument-panel-01.md)\n",
|
|
4859
|
-
"props": "## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the headline. Intentionally has no default: an earlier version shipped a hardcoded \"Now in open beta\" pill, which meant every consumer who installed this block inherited a status badge making a claim about their product. Pass one if you mean it.\n**`title`** `ReactNode` · required · default: — — The headline. A `ReactNode` rather than a `string`, because `SectionHeading` already takes one and a Block that narrowed it would have been the odd one out. Three of the five product sites set the emphasised word in their own mark rather than in a wrapper, so a `string` here would have forced each of them to drop the emphasis or reach for a second component. Every string a site renders is still the site's, and `check-block-copy.mjs` still holds this Block to shipping none.\n**`description`** `ReactNode` · optional · default: — — The supporting line. A node for the same reason the title is one.\n**`actions`** `HeroAction[]` · optional · default: — — Up to two actions. See `HeroAction` and its two arms.\n**`align`** `'center' | 'left'` · optional · default: `'center'` — Whether the copy is centered in the band or set flush to the left edge of the container. This is the hero's own alignment and not `SectionHeading`'s, because the two are not the same decision: a left-aligned hero is nearly always the left half of a two-column band with a panel or an instrument beside it, and that column is narrower than the container, so the alignment has to be the hero's before the container can be anything. It has no visual effect on its own, which is the point: it aligns the copy inside whatever width it is given.\n**`instrument`** `ReactNode` · optional · default: — — The figure beside the copy: a live view of the thing the copy is about. A slot, and the reason is the whole shape of this Block's second form. The hero that names what a product does and the hero that shows it running are the same section, and every one of the four NaniSoft product sites hand-wrote the two-column band that joins them, each with its own grid, its own gap and its own breakpoint. This is that band. What goes in it is the consumer's, and specifically it should be something that would still be true with every animation stopped: a `PulseGraph` of the pipeline the product runs, a `PulseSeries` of what it captures. A slot that only reads as motion is a decoration with a slot, and this system does not author those. The `instrumented` slot is marked as required when the hero is given one, so a figure cannot be half-wired.\n**`split`** `boolean` · optional · default: — — Puts the copy and the instrument side by side rather than stacked, and narrows the copy to the column it shares the band with. Implied by `instrument` and overridable: a consumer with a narrow instrument and a wide hero may want them stacked anyway.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the title. Defaults to `h2` because a block is composed, not a page: it installs into a consumer's layout and renders inside catalog previews, so the surrounding document already owns the `h1`. A page that uses this block as its top-level heading opts in with `headingLevel=\"h1\"`; a document that already names this section in its own heading passes one level deeper. See `HeadingLevel`.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited.",
|
|
4936
|
+
"doc": "# Hero01\n\nA centered marketing hero with an optional eyebrow and one or two actions.\n\n```tsx\nimport { Hero01 } from '@nanisoft/prism-ui/blocks/hero-01'\n```\n\n## Overview\n\nHero01 is the opening band of a marketing page: an optional eyebrow, a headline,\nan optional supporting line, and up to two actions. The `01` suffix is the\nvariant ordinal of the hero family.\n\nIt has two forms and they are one Block. The copy can sit centered over the\ncontainer, which suits a page whose claim is a sentence. Or it can sit in the\nleft column of a two-column band with a live figure in the right, which suits a\nproduct whose claim is a mechanism. Passing `instrument` picks the second form.\n\nEvery string is a prop. The block ships no eyebrow and no headline, because a\nhero with hardcoded copy makes every consumer inherit a claim about their own\nproduct.\n\n## Guidelines\n\n### When to use\n\nUse Hero01 as the first band of a page whose purpose is to introduce a product\nor a release. It works when there is one clear headline and one clear next\naction.\n\nUse the split form when the product does something rather than being something.\nA pipeline that runs, a surface that is captured, a fleet that is observed: each\nof those is a mechanism, and a mechanism reads better shown than described. The\nfigure should be one that would still be true with every animation stopped, so\nwhat the reader gets from it is the claim and the motion is only the emphasis.\n\nCompose a ground with `backdrop` when the claim is the surface the hero stands\non. An `AuroraWash` says the surface is lit, a `ParticleField` says there is a\nmedium to move through, and the band clips and positions whatever it is given,\nso the caller composes a claim rather than a layout.\n\n### When not to use\n\nDo not use Hero01 inside an application screen. It is a marketing composition,\nand its generous layout competes with the task the screen exists to support.\n\nDo not use it twice on one page. A second hero splits the reader's attention and\nleaves two claims competing for the same position.\n\nDo not use it when the headline is really a section heading. Use `SectionHeading`\ninside `Section` instead.\n\nDo not pass a figure that is only interesting while it moves. A slot that reads\nas empty in a still frame is a decoration with a slot, and this system does not\nauthor those. The same test holds for the ground: a `backdrop` that collapses\ninto nothing when it is stopped was a video with extra steps, and the hero is a\nband making a claim, not a frame around one.\n\n### Best practices\n\n- Pass one primary action and at most one supporting action. More than two\n actions turn a decision into a menu.\n- Give the first action `default` and the second `outline`. The block already\n defaults to that order.\n- Give every action a destination, or hand the block the control you want instead.\n `href` is **required** on the action arm, and there is no third arm, so a\n `{ label }` on its own does not compile:\n\n ```tsx\n // An anchor. `href` is required on this arm, and `slot` is forbidden on it.\n { label: 'Get started', href: '/docs' }\n // Your own control, for anything Prism cannot make work: a router link, a\n // submit button, a menu trigger. The block places it and adds no class to it.\n { slot: <NextLink href=\"/signup\">Create an account</NextLink> }\n ```\n\n The earlier shape accepted a third thing, `{ label: 'Coming soon' }`, and\n rendered it as a `Button`. That control was focusable, announced as a button,\n and activated to nothing, in the position a reader looks first. A block ships\n no behaviour, so it cannot make a control work, and the two errors a caller can\n make here are both compile errors rather than a runtime branch.\n- The forward arrow on the first action follows the **element**, not the position.\n Every action the block renders is a link, so the position is the whole of the\n test, and a `slot` at the front of the row wears no arrow because the control\n you drew owns its own marks. A second action gets no arrow even when it is a\n link, since the arrow marks the row's one primary destination. The default\n *variant* is still positional: what looks primary is about the row's shape,\n while who drew the control is about the element.\n- Set `headingLevel=\"h1\"` when the hero is the page's top-level heading. Use the\n default `h2` when the page already owns an `h1`.\n- Pass an eyebrow only when it is true and current. It renders nothing when\n absent, so the honest state is no eyebrow.\n- Wrap the figure in `InstrumentPanel01`. A bare figure on the page ground reads\n as an illustration; the same figure in a panel reads as an instrument, which\n is what it is.\n\n### Content guidelines\n\n- Keep the headline under about eight words and let it wrap to two lines at\n most.\n- Write the headline as an outcome, not a feature name.\n- The description is one sentence and states who the product is for or what\n changes if the reader continues.\n\n**The split form, with a live figure beside the copy.**\n\n```tsx\nimport { Hero01 } from '@nanisoft/prism-ui/blocks/hero-01'\nimport { InstrumentPanel01 } from '@nanisoft/prism-ui/blocks/instrument-panel-01'\nimport { PulseGraph } from '@nanisoft/prism-ui/components/pulse-graph'\n\n/** The centered form, with a heading level that nests under the page. */\nfunction Centered() {\n return (\n <Hero01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"A headline that fits on two lines\"\n description=\"One or two sentences of supporting copy, so you can judge the measure and the spacing around it.\"\n actions={[\n { label: 'Primary action', href: '#centered' },\n { label: 'Secondary', href: '#split', variant: 'outline' },\n ]}\n />\n )\n}\n\n/**\n * The split form, with a live figure beside the copy.\n *\n * The figure is a PulseGraph of three stages on one rail, which is the shape a\n * pipeline takes: the stages are lanes, the edges carry, and the marker travels\n * between them. The names are the preview's own and are passed in like any other\n * copy, because a Block ships none.\n */\nconst STAGES = [\n { id: 'issue', name: 'issue', note: 'one', x: 0, y: 0.5, lane: 0 },\n { id: 'build', name: 'build', note: 'fresh', x: 0.5, y: 0.5, lane: 1, emphasis: true },\n { id: 'review', name: 'review', note: 'a person', x: 1, y: 0.5, lane: 2 },\n]\n\nconst EDGES = [\n { from: 'issue', to: 'build', carries: true },\n { from: 'build', to: 'review', carries: true },\n]\n\nfunction Split() {\n return (\n <Hero01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"The same block, beside a running system\"\n description=\"A figure that shows the mechanism. It is still correct with every animation stopped.\"\n actions={[\n { label: 'Primary action', href: '#split' },\n { label: 'Secondary', href: '#centered', variant: 'outline' },\n ]}\n instrument={\n <InstrumentPanel01\n label=\"the loop, running\"\n state=\"live\"\n stateLabel=\"live\"\n caption=\"Three stages on one rail, with a marker travelling between them.\"\n footnote=\"A figure in this block is the consumer's. This one is a PulseGraph of three stages.\"\n >\n <PulseGraph nodes={STAGES} relations={EDGES} label=\"Three stages on one rail\" />\n </InstrumentPanel01>\n }\n />\n )\n}\n\nexport default function Hero01Demo() {\n return (\n <div id=\"centered\">\n <Centered />\n <div id=\"split\">\n <Split />\n </div>\n </div>\n )\n}\n```\n\n## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the headline. Intentionally has no default: an earlier version shipped a hardcoded \"Now in open beta\" pill, which meant every consumer who installed this block inherited a status badge making a claim about their product. Pass one if you mean it.\n**`title`** `ReactNode` · required · default: — — The headline. A `ReactNode` rather than a `string`, because `SectionHeading` already takes one and a Block that narrowed it would have been the odd one out. Three of the five product sites set the emphasised word in their own mark rather than in a wrapper, so a `string` here would have forced each of them to drop the emphasis or reach for a second component. Every string a site renders is still the site's, and `check-block-copy.mjs` still holds this Block to shipping none.\n**`description`** `ReactNode` · optional · default: — — The supporting line. A node for the same reason the title is one.\n**`actions`** `HeroAction[]` · optional · default: — — Up to two actions. See `HeroAction` and its two arms.\n**`align`** `'center' | 'left'` · optional · default: `'center'` — Whether the copy is centered in the band or set flush to the left edge of the container. This is the hero's own alignment and not `SectionHeading`'s, because the two are not the same decision: a left-aligned hero is nearly always the left half of a two-column band with a panel or an instrument beside it, and that column is narrower than the container, so the alignment has to be the hero's before the container can be anything. It has no visual effect on its own, which is the point: it aligns the copy inside whatever width it is given.\n**`instrument`** `ReactNode` · optional · default: — — The figure beside the copy: a live view of the thing the copy is about. A slot, and the reason is the whole shape of this Block's second form. The hero that names what a product does and the hero that shows it running are the same section, and every one of the four NaniSoft product sites hand-wrote the two-column band that joins them, each with its own grid, its own gap and its own breakpoint. This is that band. What goes in it is the consumer's, and specifically it should be something that would still be true with every animation stopped: a `PulseGraph` of the pipeline the product runs, a `PulseSeries` of what it captures. A slot that only reads as motion is a decoration with a slot, and this system does not author those. The `instrumented` slot is marked as required when the hero is given one, so a figure cannot be half-wired.\n**`split`** `boolean` · optional · default: — — Puts the copy and the instrument side by side rather than stacked, and narrows the copy to the column it shares the band with. Implied by `instrument` and overridable: a consumer with a narrow instrument and a wide hero may want them stacked anyway.\n**`backdrop`** `ReactNode` · optional · default: — — The ground the hero stands on: a figure the whole band is drawn over. A slot, and for the same reason the instrument is one: the ground a hero wants is the product's own claim, and this Block cannot make it. An `AuroraWash` says the surface is lit, a `ParticleField` says there is a medium to move through, and a caller who wants both composes both, because the one decision a hero should not delegate is what its ground claims. The Block clips the ground to the band, takes the pointer away from it, and holds it behind the copy beside the hero's own faint wash, so a slot here never has to arrive pre-positioned. A ground that would not still be true with every animation stopped does not belong here, for the same reason an instrument that only reads as motion does not: this is a band making a claim, not a frame around a video.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the title. Defaults to `h2` because a block is composed, not a page: it installs into a consumer's layout and renders inside catalog previews, so the surrounding document already owns the `h1`. A page that uses this block as its top-level heading opts in with `headingLevel=\"h1\"`; a document that already names this section in its own heading passes one level deeper. See `HeadingLevel`.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited.\n\n## Composition\n\n**`exports`** `Hero01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/hero-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `CtaLink, InstrumentPanel01, PulseGraph, Section` — the public Prism exports this item composes\n\n## Blocks\n\n- [InstrumentPanel01](https://prism.nanisoft.com/blocks/instrument-panel-01.md)\n",
|
|
4937
|
+
"props": "## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the headline. Intentionally has no default: an earlier version shipped a hardcoded \"Now in open beta\" pill, which meant every consumer who installed this block inherited a status badge making a claim about their product. Pass one if you mean it.\n**`title`** `ReactNode` · required · default: — — The headline. A `ReactNode` rather than a `string`, because `SectionHeading` already takes one and a Block that narrowed it would have been the odd one out. Three of the five product sites set the emphasised word in their own mark rather than in a wrapper, so a `string` here would have forced each of them to drop the emphasis or reach for a second component. Every string a site renders is still the site's, and `check-block-copy.mjs` still holds this Block to shipping none.\n**`description`** `ReactNode` · optional · default: — — The supporting line. A node for the same reason the title is one.\n**`actions`** `HeroAction[]` · optional · default: — — Up to two actions. See `HeroAction` and its two arms.\n**`align`** `'center' | 'left'` · optional · default: `'center'` — Whether the copy is centered in the band or set flush to the left edge of the container. This is the hero's own alignment and not `SectionHeading`'s, because the two are not the same decision: a left-aligned hero is nearly always the left half of a two-column band with a panel or an instrument beside it, and that column is narrower than the container, so the alignment has to be the hero's before the container can be anything. It has no visual effect on its own, which is the point: it aligns the copy inside whatever width it is given.\n**`instrument`** `ReactNode` · optional · default: — — The figure beside the copy: a live view of the thing the copy is about. A slot, and the reason is the whole shape of this Block's second form. The hero that names what a product does and the hero that shows it running are the same section, and every one of the four NaniSoft product sites hand-wrote the two-column band that joins them, each with its own grid, its own gap and its own breakpoint. This is that band. What goes in it is the consumer's, and specifically it should be something that would still be true with every animation stopped: a `PulseGraph` of the pipeline the product runs, a `PulseSeries` of what it captures. A slot that only reads as motion is a decoration with a slot, and this system does not author those. The `instrumented` slot is marked as required when the hero is given one, so a figure cannot be half-wired.\n**`split`** `boolean` · optional · default: — — Puts the copy and the instrument side by side rather than stacked, and narrows the copy to the column it shares the band with. Implied by `instrument` and overridable: a consumer with a narrow instrument and a wide hero may want them stacked anyway.\n**`backdrop`** `ReactNode` · optional · default: — — The ground the hero stands on: a figure the whole band is drawn over. A slot, and for the same reason the instrument is one: the ground a hero wants is the product's own claim, and this Block cannot make it. An `AuroraWash` says the surface is lit, a `ParticleField` says there is a medium to move through, and a caller who wants both composes both, because the one decision a hero should not delegate is what its ground claims. The Block clips the ground to the band, takes the pointer away from it, and holds it behind the copy beside the hero's own faint wash, so a slot here never has to arrive pre-positioned. A ground that would not still be true with every animation stopped does not belong here, for the same reason an instrument that only reads as motion does not: this is a band making a claim, not a frame around a video.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the title. Defaults to `h2` because a block is composed, not a page: it installs into a consumer's layout and renders inside catalog previews, so the surrounding document already owns the `h1`. A page that uses this block as its top-level heading opts in with `headingLevel=\"h1\"`; a document that already names this section in its own heading passes one level deeper. See `HeadingLevel`.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited.",
|
|
4860
4938
|
"composition": "## Composition\n\n**`exports`** `Hero01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/hero-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `CtaLink, InstrumentPanel01, PulseGraph, Section` — the public Prism exports this item composes",
|
|
4861
4939
|
"example": {
|
|
4862
4940
|
"title": "The split form, with a live figure beside the copy.",
|
|
@@ -5359,6 +5437,33 @@
|
|
|
5359
5437
|
"pages": []
|
|
5360
5438
|
}
|
|
5361
5439
|
},
|
|
5440
|
+
{
|
|
5441
|
+
"id": "media-band-01",
|
|
5442
|
+
"slug": "media-band-01",
|
|
5443
|
+
"kind": "block",
|
|
5444
|
+
"name": "MediaBand01",
|
|
5445
|
+
"category": null,
|
|
5446
|
+
"status": "stable",
|
|
5447
|
+
"description": "An editorial band: one media slot held at a caller ratio beside one title, description and optional destination, with the copy always on a solid surface.",
|
|
5448
|
+
"url": "/blocks/media-band-01",
|
|
5449
|
+
"mirror": "/blocks/media-band-01.md",
|
|
5450
|
+
"source": "src/blocks/media-band-01/index.tsx",
|
|
5451
|
+
"exports": [
|
|
5452
|
+
"MediaBand01"
|
|
5453
|
+
],
|
|
5454
|
+
"importLine": "import { MediaBand01 } from '@nanisoft/prism-ui/blocks/media-band-01'",
|
|
5455
|
+
"doc": "# MediaBand01\n\nAn editorial band: one media slot held at a caller ratio beside one title, description and optional destination, with the copy always on a solid surface.\n\n```tsx\nimport { MediaBand01 } from '@nanisoft/prism-ui/blocks/media-band-01'\n```\n\n## Overview\n\nMediaBand01 is an editorial band: one media frame beside one statement, with one\naction. It is the marketing surface the catalogue did not have. `Bento01` holds a\nmedia node in a cell, but a cell is one item of a set, and a set is the wrong shape\nfor the one photograph a page stops on. `Gallery01` holds a set of images and opens\none at full size, which is right for a portfolio and wrong for a statement.\n\nThe media is a slot, not a `src`. A caller can pass an image, a video poster, a\nrendered figure, or a composition of its own components. The Block owns the frame,\nthe ratio, the surface and the order. The caller owns the node and its accessible\nname.\n\nThe copy is first in the document, and the media is first only in the visual order\nthe caller asks for. A reader on a phone meets the title before the picture, and a\nreader at `lg` meets the arrangement the caller chose. That is one tree, so the\nheading is in the outline once.\n\nThe copy never sits on the picture. A title over an arbitrary photograph is a\ncontrast failure nobody measured, and the solid column beside the image is both\nmore legible and more editorial.\n\nThere is no animation. An editorial photograph is not feedback and it is not a\nfigure showing a system running, so the band does not zoom, slide or reveal.\n\n## Guidelines\n\n### When to use\n\nUse MediaBand01 when a page has one photograph or one composed figure that is a\nmoment of its own, and one sentence that the picture makes. It is the band a page\nstops on between two sections of argument.\n\n### When not to use\n\nDo not use it for a set of images. A set is `Gallery01` or a `Bento01` grid. Do not\nuse it for a figure that carries a claim a screen reader must hear in full; that is\n`InstrumentPanel01` or one of the figure Components. Do not put the copy on top of\nthe image; the solid column is the arrangement this Block exists to make easy.\n\n### Ratios and sides\n\n`ratio` is `wide`, `square` or `portrait`. `align` is `media-left` or `media-right`,\nand it changes the visual order only. Below the breakpoint the copy always comes\nfirst.\n\n### Tone\n\n`default` leaves the band on the page ground. `muted` puts it on the muted surface,\nwhich is for a band that is a chapter rather than a transition. The muted arm states\nits own ink as well as its own fill.\n\n### The media slot\n\nThe caller's node fills the frame. The Block does not style a node it did not\nrender, so the image should set its own width, height and `object-fit`, and it\nshould carry the alt text that names it. A composed figure should carry its own\naccessible name.\n\n**The band with a placeholder in the media slot.**\n\n```tsx\nimport { ImageIcon } from 'lucide-react'\n\nimport { MediaBand01 } from '@nanisoft/prism-ui/blocks/media-band-01'\n\n/**\n * The band with a placeholder in the media slot.\n *\n * The slot is the point: a marketing band's picture is the consumer's asset, and\n * the demo shows the frame the Block owns rather than a photograph this package\n * would have to license. A real caller passes an image, a video poster or a\n * composed figure, and the frame holds it at the ratio the caller chose.\n */\nexport default function MediaBand01Demo() {\n return (\n <MediaBand01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n size=\"default\"\n title=\"A photograph, a statement and one way forward\"\n description=\"The copy is first in the document and the media is first only in the visual order the caller asks for. The frame holds a wide, square or portrait crop, and the copy never sits on the picture.\"\n media={\n <div className=\"bg-accent text-accent-foreground flex h-full w-full flex-col items-center justify-center gap-3\">\n <ImageIcon className=\"size-8\" aria-hidden />\n <span className=\"font-mono text-xs\">media slot</span>\n </div>\n }\n caption=\"A placeholder in the slot. The caller supplies the image and its accessible name.\"\n action={{ label: 'Read the story', href: '#' }}\n />\n )\n}\n```\n\n## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the title. See the No-Default-Eyebrow Rule.\n**`title`** `ReactNode` · required · default: — — The band's one claim. Required: a band with no title is a picture.\n**`description`** `ReactNode` · optional · default: — — One or two sentences under the title.\n**`media`** `ReactNode` · required · default: — — The media, composed by the caller. A slot rather than a `src`, because the honest media here is not always an image: it is a photograph, a video poster, a rendered figure, or a composition of the consumer's own components. The Block owns the frame, the ratio and the surface, and the caller owns the node and its accessible name. **The node fills the frame.** The caller's image or figure should be sized to its container and should set its own `object-fit`; the Block does not style a node it did not render, which is the same rule every slot in this package follows.\n**`align`** `'media-left' | 'media-right'` · optional · default: `'media-right'` — Which side the media sits on at the wide breakpoint. Below the breakpoint the copy always comes first, because the title is the way into the picture and a reader on a phone should not have to scroll past a photograph to learn what it is for.\n**`ratio`** `MediaBand01Ratio` · optional · default: `'wide'` — The shape the media frame holds.\n**`tone`** `'default' | 'muted'` · optional · default: `'default'` — The surface the band is drawn on. `default` leaves it on the page ground, which is the quieter answer and the right one for a band that is one moment among several. `muted` puts it on the muted surface, for a band that is a chapter of its own. The muted arm states its own ink as well as its own fill, which is the `Stated Ink Rule` `Cta01` and `Bento01` both state: a surface that sets a fill and inherits its ink is correct wherever the design system happened to put it and wrong everywhere else.\n**`caption`** `ReactNode` · optional · default: — — One line under the media. Functional, not decorative: it says what a reader is looking at when the photograph is a placeholder or when the image is not the product itself. It is never a photo credit for a placeholder, and it is never a caption for a picture whose subject the title already names.\n**`action`** `MediaBand01Action` · optional · default: — — The band's one action. Omit it for a band that is a statement and not an ask.\n**`size`** `MediaBand01Size` · optional · default: `'tall'` — How tall the band is. See `MediaBand01Size`.\n**`headingLevel`** `HeadingLevel` · optional · default: `'h2'` — Heading level for the title. See `HeadingLevel`.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Block. Changing a Prism-owned visual property from here is prohibited.\n\n## Composition\n\n**`exports`** `MediaBand01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/media-band-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `AspectRatio, CtaLink, Section` — the public Prism exports this item composes\n",
|
|
5456
|
+
"props": "## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the title. See the No-Default-Eyebrow Rule.\n**`title`** `ReactNode` · required · default: — — The band's one claim. Required: a band with no title is a picture.\n**`description`** `ReactNode` · optional · default: — — One or two sentences under the title.\n**`media`** `ReactNode` · required · default: — — The media, composed by the caller. A slot rather than a `src`, because the honest media here is not always an image: it is a photograph, a video poster, a rendered figure, or a composition of the consumer's own components. The Block owns the frame, the ratio and the surface, and the caller owns the node and its accessible name. **The node fills the frame.** The caller's image or figure should be sized to its container and should set its own `object-fit`; the Block does not style a node it did not render, which is the same rule every slot in this package follows.\n**`align`** `'media-left' | 'media-right'` · optional · default: `'media-right'` — Which side the media sits on at the wide breakpoint. Below the breakpoint the copy always comes first, because the title is the way into the picture and a reader on a phone should not have to scroll past a photograph to learn what it is for.\n**`ratio`** `MediaBand01Ratio` · optional · default: `'wide'` — The shape the media frame holds.\n**`tone`** `'default' | 'muted'` · optional · default: `'default'` — The surface the band is drawn on. `default` leaves it on the page ground, which is the quieter answer and the right one for a band that is one moment among several. `muted` puts it on the muted surface, for a band that is a chapter of its own. The muted arm states its own ink as well as its own fill, which is the `Stated Ink Rule` `Cta01` and `Bento01` both state: a surface that sets a fill and inherits its ink is correct wherever the design system happened to put it and wrong everywhere else.\n**`caption`** `ReactNode` · optional · default: — — One line under the media. Functional, not decorative: it says what a reader is looking at when the photograph is a placeholder or when the image is not the product itself. It is never a photo credit for a placeholder, and it is never a caption for a picture whose subject the title already names.\n**`action`** `MediaBand01Action` · optional · default: — — The band's one action. Omit it for a band that is a statement and not an ask.\n**`size`** `MediaBand01Size` · optional · default: `'tall'` — How tall the band is. See `MediaBand01Size`.\n**`headingLevel`** `HeadingLevel` · optional · default: `'h2'` — Heading level for the title. See `HeadingLevel`.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Block. Changing a Prism-owned visual property from here is prohibited.",
|
|
5457
|
+
"composition": "## Composition\n\n**`exports`** `MediaBand01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/media-band-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `AspectRatio, CtaLink, Section` — the public Prism exports this item composes",
|
|
5458
|
+
"example": {
|
|
5459
|
+
"title": "The band with a placeholder in the media slot.",
|
|
5460
|
+
"code": "import { ImageIcon } from 'lucide-react'\n\nimport { MediaBand01 } from '@nanisoft/prism-ui/blocks/media-band-01'\n\n/**\n * The band with a placeholder in the media slot.\n *\n * The slot is the point: a marketing band's picture is the consumer's asset, and\n * the demo shows the frame the Block owns rather than a photograph this package\n * would have to license. A real caller passes an image, a video poster or a\n * composed figure, and the frame holds it at the ratio the caller chose.\n */\nexport default function MediaBand01Demo() {\n return (\n <MediaBand01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n size=\"default\"\n title=\"A photograph, a statement and one way forward\"\n description=\"The copy is first in the document and the media is first only in the visual order the caller asks for. The frame holds a wide, square or portrait crop, and the copy never sits on the picture.\"\n media={\n <div className=\"bg-accent text-accent-foreground flex h-full w-full flex-col items-center justify-center gap-3\">\n <ImageIcon className=\"size-8\" aria-hidden />\n <span className=\"font-mono text-xs\">media slot</span>\n </div>\n }\n caption=\"A placeholder in the slot. The caller supplies the image and its accessible name.\"\n action={{ label: 'Read the story', href: '#' }}\n />\n )\n}\n"
|
|
5461
|
+
},
|
|
5462
|
+
"references": {
|
|
5463
|
+
"blocks": [],
|
|
5464
|
+
"pages": []
|
|
5465
|
+
}
|
|
5466
|
+
},
|
|
5362
5467
|
{
|
|
5363
5468
|
"id": "member-list-01",
|
|
5364
5469
|
"slug": "member-list-01",
|
|
@@ -6592,8 +6697,8 @@
|
|
|
6592
6697
|
"SiteNavbar"
|
|
6593
6698
|
],
|
|
6594
6699
|
"importLine": "import { SiteNavbar } from '@nanisoft/prism-ui/blocks/site-navbar'",
|
|
6595
|
-
"doc": "# SiteNavbar\n\nThe bar at the top of a product site, with the controls a reader needs on every page: a brand lockup, the site navigation, a menu of the family's sites, search over a static index, a colour chooser, a light and dark control and a panel for narrow viewports.\n\n```tsx\nimport { SiteNavbar } from '@nanisoft/prism-ui/blocks/site-navbar'\n```\n\n## Overview\n\nSiteNavbar is the bar at the top of a product site, with the controls a reader needs\non every page of it: a brand lockup, the site's own navigation, a menu of the\nfamily's sites, search over a static index, a colour chooser, a light and dark\ncontrol, and a panel that carries the navigation below the width the row stops\nfitting.\n\nIt is `SiteHeader` plus the four controls that were missing from it, and it exists\nbecause all five NaniSoft sites had the same bar and every one of them had written\nthe missing four separately. Prism's own site had search and a colour chooser and no\nway to reach its four siblings. Each of the four had a row of five product marks\nwhere a menu belongs, and no search, no mode control, and no navigation at all below\nthe width its own row stopped fitting. Five bars, one design language, and no two of\nthem the same bar.\n\nIt is a Block rather than a Component because it is a region of a page: it composes a\n`ProductMark`, a navigation region and a set of controls, and it owns the bar they\nsit in.\n\n**Every string is a prop and the Block ships none.** There is no site name, no\ndefault navigation, no \"Search\", no list of packs and no list of sites. A bar that\nhardcoded any of them would hand every consumer a claim about a product that is not\ntheirs, which is the defect this Block exists to remove. The one piece of copy it\nderives is a pack's own name from its own identifier, and only when the caller names\nnone.\n\n## Guidelines\n\n### When to use\n\nUse SiteNavbar as the chrome around a whole public site. One bar per site, above the\nmain region, on the landing page and on every documentation and blog page alike. A\nbar that changes between sections of one site is two sites' worth of chrome.\n\nUse it wherever a reader can leave the page they are on. A site that publishes a\nlanding, documentation and a blog and has no persistent way between them is three\nsites wearing one domain.\n\n### When not to use\n\nDo not use it inside an application screen. `AppShell01` is that: a rail, a top bar\nand a mobile sheet, for signed-in state rather than for a public site.\n\nDo not use it for in-page navigation. That is a `Tabs` or a `Breadcrumb`.\n\nDo not reach for `SiteHeader` expecting the controls. `SiteHeader` is the bar with a\nlockup, a switcher and a slot, and it is still the right answer for a site that owns\nnone of the four and would rather compose its own.\n\n### Best practices\n\n- Pass `defaultPack` and `defaultMode` from the same constants the document element\n is rendered from. The control and the page then cannot disagree about what a\n reader who has never chosen sees.\n- Mark the current page with `current` on the one link whose destination is the\n current route, which is what a site composing the bar per page can do. A site that\n renders the bar once in a root layout is handed no pathname and passes\n `currentPath` instead. They are alternatives, not layers.\n- Name every landmark. `navLabel`, `sitesLabel`, `mode.lightLabel`, `mode.darkLabel`\n and `mobileLabels` are all words a reader hears, and a bar with two navigation\n regions of one name is a bar a reader navigating by landmark cannot tell apart.\n- Pass `search` as a URL, never as an index. The index is the largest asset on a\n documentation site, and a value passed as a prop is serialized into the document\n of every route that renders the bar.\n- Omit `theme` on a product site. A site's ground is a property of the site, and a\n reader who repaints it is on a page that no longer describes the site. The chooser\n belongs on a site whose subject is its own palettes.\n- Pass `actions` for what the product owns: an account menu, a sign-in link, a\n playground. They render after the controls this bar composes, at the right-hand\n end.\n- Leave `sticky` on unless the page does not scroll. The bar is how a reader leaves\n the page they are on, and it takes that with them on exactly the long pages where\n it is needed. `sticky={false}` is honoured and puts the bar back in the flow.\n\n### Content guidelines\n\n- Keep a nav label to one word. It is a bar.\n- Use each site's own name in the lockup, in the case it uses everywhere else.\n- Order the nav by how often a reader goes there, not alphabetically.\n- Order `sites` so the site the reader is on is where the family's reading order puts\n it, not always first. A set that is always ordered around the current site reads as\n a different set on every page.\n\n## Notes\n\n### The bar is opaque, and the hairline is the separator\n\nThe bar is `bg-background` with a `border-border` hairline, and it is what\n`SiteHeader` has always shipped. It used to be `bg-background/80` with a\n`backdrop-blur`, so the page showed through it blurred.\n\nA backdrop filter is evaluated against everything painted behind the element. This\nelement is a full-viewport-width sticky bar, so on a scrolling page the browser\nre-sampled and re-blurred the backdrop on every frame, on every page of every site\nthat composes the bar. Keeping the blur behind an `@supports` check would not have\npaid for it either: the check is false only in a browser that was not compositing\nanything, and true in every browser that does the expensive thing.\n\nOpaque is also the more legible of the two, and this is the part a token can be\nheld to. An opaque bar makes the bar's own contrast the pair `muted-foreground` on\n`background`, in every pack and in both modes, which is the pair the contrast gate\nalready checks. An eighty percent background has no token pair at all, because the\ncolour underneath it is whatever the reader's scroll position has brought there.\n\nTwo things to know if you were styling against the old bar:\n\n- A rule that set the bar's background translucency has nothing to sit on now. Set\n the colour on the page behind the bar, or leave the bar to `SiteHeader`'s.\n- A rule that relied on the bar being translucent to hide a scrolling heading will\n no longer hide it, which is the point.\n\n`will-change` is not on the bar and is not proposed for it. Nothing on the bar\nanimates, so a compositing hint applied at rest has no frame to be ready for and\nnothing that would release it.\n\n### The one client boundary\n\nThe lockup and the navigation are server Components and ship no JavaScript. The\ncontrols are a single client island, because a control's state is a reader's state,\nand the theme provider is mounted around those controls rather than in the site's\nroot layout so a page that renders none of them does not ship it.\n\nThat is what makes the whole set of `search`, `sites`, `theme`, `mode` and `nav`\noptional: a site that passes none of them renders the entire bar on the server and\nships none, which is the arrangement a static site wants.\n\n### The search index is a URL\n\n`search.indexUrl` is fetched on the first search and not before, and the dialog\nitself is loaded on the first activation. A site serves a JSON array of\n`{ id, title, url, description?, content?, breadcrumbs? }` from that route, and a\nstatic export can: a route handler with `revalidate = false` is emitted as a file.\n\nThe ranking is a scorer rather than an inverted index, and that is a deliberate\ntrade rather than an oversight. There is no stemming and no typo tolerance; every\nquery token has to be a substring of one of an entry's fields, which is why a\ntwo-word query narrows rather than widens. The alternative is a large search\ndependency in a package whose entire value is that it depends on a design system and\na token pipeline and nothing else, and whose ranking no consumer could then restyle\nor audit against this package's gates.\n\n### The colour chooser reads no token at runtime\n\nEach swatch is a `data-pack` boundary carrying `bg-primary`, not a literal colour read\nfrom the token build. A token resolved at runtime does not follow the cascade, so the\ndot holding a pack's colour would be right in one mode and stale in the other. The\nboundary is safe on this shape and only this shape: a pack repoints its own corner\nradius as well as its colour, so a boundary on anything not fully rounded would\nquietly restyle that element's corners. A disc is fully rounded, so the radius axis\nis a no-op.\n\n**The site bar with every control on it: search, the family's sites, the colour**\n\n```tsx\n'use client'\n\nimport { SiteNavbar } from '@nanisoft/prism-ui/blocks/site-navbar'\n\n/**\n * The site bar with every control on it: search, the family's sites, the colour\n * chooser, the light and dark control, and the panel that carries the navigation\n * below the row's own threshold.\n *\n * The search index URL is the documentation site's own, so the control in this\n * demo is the real one rather than a picture of one.\n */\nexport default function SiteNavbarDemo() {\n return (\n <SiteNavbar\n product={{ id: 'prism', name: 'Prism' }}\n homeHref=\"#navbar\"\n defaultPack=\"default\"\n defaultMode=\"light\"\n navLabel=\"Sections\"\n mobileLabels={{ open: 'Open menu', close: 'Close menu' }}\n currentSiteId=\"prism\"\n sitesLabel=\"The family\"\n sites={[\n { id: 'www', name: 'NaniSoft', pack: 'sky', href: 'https://www.nanisoft.com' },\n { id: 'nexus', name: 'Nexus', pack: 'lavender', href: 'https://nexus.nanisoft.com' },\n { id: 'atlas', name: 'Atlas', pack: 'mint', href: 'https://atlas.nanisoft.com' },\n { id: 'alphalens', name: 'AlphaLens', pack: 'blush', href: 'https://alphalens.nanisoft.com' },\n { id: 'prism', name: 'Prism', pack: null, href: '#navbar' },\n ]}\n nav={[\n { label: 'Overview', href: '#overview' },\n { label: 'Foundation', href: '#foundation' },\n { label: 'Content', href: '#content' },\n { label: 'Components', href: '#components' },\n { label: 'Blocks', href: '#blocks', current: true },\n { label: 'Pages', href: '#pages' },\n ]}\n search={{\n indexUrl: '/api/search',\n label: 'Search documentation',\n hint: 'Type to search every Section and every Item.',\n messages: {\n close: 'Close search',\n loading: 'Loading the search index.',\n failed: 'The search index could not be loaded.',\n empty: 'No matches.',\n one: 'result.',\n other: 'results.',\n },\n }}\n theme={{ label: 'Colour theme' }}\n mode={{ lightLabel: 'Switch to dark mode', darkLabel: 'Switch to light mode' }}\n />\n )\n}\n```\n\n## Props\n\n**`product`** `Pick<ProductMarkProps, 'id' | 'name' | 'pack' | 'mark'>` · required · default: — — The product this bar belongs to: the id, name, pack and drawn mark its `ProductMark` is drawn from, and the mark that heads the mobile panel. The bar renders the mark itself rather than a slot, because a bar whose brand lockup is a slot is a bar every consumer has to reimplement.\n**`homeHref`** `string` · optional · default: `'/'` — Where the brand lockup goes.\n**`defaultPack`** `PackId` · required · default: — — The pack a reader who has never chosen a pack gets. It is passed to the theme runtime rather than read from the document, so a site states its own default in one place and the control and the page cannot disagree about it.\n**`defaultMode`** `Mode` · required · default: — — The mode a reader who has never chosen gets. Required for the same reason `defaultPack` is, and because a toggle with no stated starting mode is a control whose icon is a guess until it is pressed once.\n**`nav`** `readonly SiteNavEntry[]` · optional · default: — — This site's own destinations: links, groups of links, or both in the order a reader should meet them. A link renders as itself; a group renders as one trigger opening its children as a menu. Omit for a bar that carries only the lockup.\n**`navLabel`** `string` · required · default: — — The accessible name of the site's navigation. Required, and required rather than defaulted because it is a word a reader hears: a Block that ships no copy ships no reader-facing copy either.\n**`currentPath`** `string` · optional · default: — — The route the reader is on, for a site that renders the bar once in a root layout and therefore cannot mark the current link itself. A site that composes the bar per page, which is the arrangement that keeps the current link server rendered, sets `current` on the one link instead and leaves this off. The two are alternatives rather than layers, and a site that set both would have two answers to which link is current.\n**`sites`** `readonly SwitcherProduct[]` · optional · default: — — The set of sites to move between, rendered as a menu. Omit it on a site that is the only one; a set of one is a menu with a single item and no reason to exist.\n**`currentSiteId`** `string` · optional · default: — — The `id` of the site the reader is on, marked in the menu.\n**`sitesLabel`** `string` · optional · default: — — The accessible name of the sites menu. Required whenever `sites` is passed, for the same reason `navLabel` is.\n**`search`** `SiteNavSearch` · optional · default: — — The search the bar offers, or nothing for no search.\n**`theme`** `SiteNavTheme` · optional · default: — — The colour chooser the bar offers, or nothing for no chooser.\n**`mode`** `SiteNavMode` · optional · default: — — The light and dark control the bar offers, or nothing for no toggle.\n**`mobileLabels`** `SiteNavMobileLabels` · required · default: — — The two names of the mobile trigger, which is a round icon with no visible word. Required rather than defaulted, because a default would be a sentence this Block ships about a product that is not the caller's, and because a bar that renders an unlabelled round control below its own threshold has a keyboard reader reaching for something it cannot name.\n**`actions`** `ReactNode` · optional · default: — — A slot for controls the product owns, after the ones this bar composes. A slot rather than a prop, because an account menu and a sign-in link are application state and neither is a fact a bar can be told.\n**`sticky`** `boolean` · optional · default: — — Keeps the bar at the top of the viewport as the page scrolls. On by default, because the bar is how a reader leaves the page they are on, and a bar that scrolls away takes that with it on exactly the long pages a reader needs it. `false` is honoured: the bar is then part of the flow and scrolls away with the page. It was accepted and ignored until this release, so a site that passed it was given the sticky bar it had asked not to have, and `SiteHeader` has always applied it the other way round.\n**`className`** `string` · optional · default: — — Layout only. Changing a Prism-owned visual property from here is prohibited.\n\n## Composition\n\n**`exports`** `SiteNavbar` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/site-navbar/index.tsx` — the file this Block is defined in\n**`dependencies`** `ProductMark` — the public Prism exports this item composes\n",
|
|
6596
|
-
"props": "## Props\n\n**`product`** `Pick<ProductMarkProps, 'id' | 'name' | 'pack' | 'mark'>` · required · default: — — The product this bar belongs to: the id, name, pack and drawn mark its `ProductMark` is drawn from, and the mark that heads the mobile panel. The bar renders the mark itself rather than a slot, because a bar whose brand lockup is a slot is a bar every consumer has to reimplement.\n**`homeHref`** `string` · optional · default: `'/'` — Where the brand lockup goes.\n**`defaultPack`** `PackId` · required · default: — — The pack a reader who has never chosen a pack gets. It is passed to the theme runtime rather than read from the document, so a site states its own default in one place and the control and the page cannot disagree about it.\n**`defaultMode`** `Mode` · required · default: — — The mode a reader who has never chosen gets. Required for the same reason `defaultPack` is, and because a toggle with no stated starting mode is a control whose icon is a guess until it is pressed once.\n**`nav`** `readonly SiteNavEntry[]` · optional · default: — — This site's own destinations: links, groups of links, or both in the order a reader should meet them. A link renders as itself; a group renders as one trigger opening its children as a menu. Omit for a bar that carries only the lockup.\n**`navLabel`** `string` · required · default: — — The accessible name of the site's navigation. Required, and required rather than defaulted because it is a word a reader hears: a Block that ships no copy ships no reader-facing copy either.\n**`currentPath`** `string` · optional · default: — — The route the reader is on, for a site that renders the bar once in a root layout and therefore cannot mark the current link itself. A site that composes the bar per page, which is the arrangement that keeps the current link server rendered, sets `current` on the one link instead and leaves this off. The two are alternatives rather than layers, and a site that set both would have two answers to which link is current.\n**`sites`** `readonly SwitcherProduct[]` · optional · default: — — The set of sites to move between, rendered as a menu. Omit it on a site that is the only one; a set of one is a menu with a single item and no reason to exist.\n**`currentSiteId`** `string` · optional · default: — — The `id` of the site the reader is on, marked in the menu.\n**`sitesLabel`** `string` · optional · default: — — The accessible name of the sites menu. Required whenever `sites` is passed, for the same reason `navLabel` is.\n**`search`** `SiteNavSearch` · optional · default: — — The search the bar offers, or nothing for no search.\n**`theme`** `SiteNavTheme` · optional · default: — — The colour chooser the bar offers, or nothing for no chooser.\n**`mode`** `SiteNavMode` · optional · default: — — The light and dark control the bar offers, or nothing for no toggle.\n**`mobileLabels`** `SiteNavMobileLabels` · required · default: — — The two names of the mobile trigger, which is a round icon with no visible word. Required rather than defaulted, because a default would be a sentence this Block ships about a product that is not the caller's, and because a bar that renders an unlabelled round control below its own threshold has a keyboard reader reaching for something it cannot name.\n**`actions`** `ReactNode` · optional · default: — — A slot for controls the product owns, after the ones this bar composes. A slot rather than a prop, because an account menu and a sign-in link are application state and neither is a fact a bar can be told.\n**`sticky`** `boolean` · optional · default: — — Keeps the bar at the top of the viewport as the page scrolls. On by default, because the bar is how a reader leaves the page they are on, and a bar that scrolls away takes that with it on exactly the long pages a reader needs it. `false` is honoured: the bar is then part of the flow and scrolls away with the page. It was accepted and ignored until this release, so a site that passed it was given the sticky bar it had asked not to have, and `SiteHeader` has always applied it the other way round.\n**`className`** `string` · optional · default: — — Layout only. Changing a Prism-owned visual property from here is prohibited.",
|
|
6700
|
+
"doc": "# SiteNavbar\n\nThe bar at the top of a product site, with the controls a reader needs on every page: a brand lockup, the site navigation, a menu of the family's sites, search over a static index, a colour chooser, a light and dark control and a panel for narrow viewports.\n\n```tsx\nimport { SiteNavbar } from '@nanisoft/prism-ui/blocks/site-navbar'\n```\n\n## Overview\n\nSiteNavbar is the bar at the top of a product site, with the controls a reader needs\non every page of it: a brand lockup, the site's own navigation, a menu of the\nfamily's sites, search over a static index, a colour chooser, a light and dark\ncontrol, and a panel that carries the navigation below the width the row stops\nfitting.\n\nIt is `SiteHeader` plus the four controls that were missing from it, and it exists\nbecause all five NaniSoft sites had the same bar and every one of them had written\nthe missing four separately. Prism's own site had search and a colour chooser and no\nway to reach its four siblings. Each of the four had a row of five product marks\nwhere a menu belongs, and no search, no mode control, and no navigation at all below\nthe width its own row stopped fitting. Five bars, one design language, and no two of\nthem the same bar.\n\nIt is a Block rather than a Component because it is a region of a page: it composes a\n`ProductMark`, a navigation region and a set of controls, and it owns the bar they\nsit in.\n\n**Every string is a prop and the Block ships none.** There is no site name, no\ndefault navigation, no \"Search\", no list of packs and no list of sites. A bar that\nhardcoded any of them would hand every consumer a claim about a product that is not\ntheirs, which is the defect this Block exists to remove. The one piece of copy it\nderives is a pack's own name from its own identifier, and only when the caller names\nnone.\n\n## Guidelines\n\n### When to use\n\nUse SiteNavbar as the chrome around a whole public site. One bar per site, above the\nmain region, on the landing page and on every documentation and blog page alike. A\nbar that changes between sections of one site is two sites' worth of chrome.\n\nUse it wherever a reader can leave the page they are on. A site that publishes a\nlanding, documentation and a blog and has no persistent way between them is three\nsites wearing one domain.\n\n### When not to use\n\nDo not use it inside an application screen. `AppShell01` is that: a rail, a top bar\nand a mobile sheet, for signed-in state rather than for a public site.\n\nDo not use it for in-page navigation. That is a `Tabs` or a `Breadcrumb`.\n\nDo not reach for `SiteHeader` expecting the controls. `SiteHeader` is the bar with a\nlockup, a switcher and a slot, and it is still the right answer for a site that owns\nnone of the four and would rather compose its own.\n\n### Best practices\n\n- Pass `defaultPack` and `defaultMode` from the same constants the document element\n is rendered from. The control and the page then cannot disagree about what a\n reader who has never chosen sees.\n- Mark the current page with `current` on the one link whose destination is the\n current route, which is what a site composing the bar per page can do. A site that\n renders the bar once in a root layout is handed no pathname and passes\n `currentPath` instead. They are alternatives, not layers.\n- Name every landmark. `navLabel`, `sitesLabel`, `mode.lightLabel`, `mode.darkLabel`\n and `mobileLabels` are all words a reader hears, and a bar with two navigation\n regions of one name is a bar a reader navigating by landmark cannot tell apart.\n- Pass `search` as a URL, never as an index. The index is the largest asset on a\n documentation site, and a value passed as a prop is serialized into the document\n of every route that renders the bar.\n- Omit `theme` on a product site. A site's ground is a property of the site, and a\n reader who repaints it is on a page that no longer describes the site. The chooser\n belongs on a site whose subject is its own palettes.\n- Pass `actions` for what the product owns: an account menu, a sign-in link, a\n playground. They render after the controls this bar composes, at the right-hand\n end.\n- Leave `sticky` on unless the page does not scroll. The bar is how a reader leaves\n the page they are on, and it takes that with them on exactly the long pages where\n it is needed. `sticky={false}` is honoured and puts the bar back in the flow.\n- Leave `scrollRail` on for pages a reader reads end to end. The rail fills with\n the reader's own progress, a browser that cannot run it draws no rail at all,\n and a reader who asked for less motion gets a still spectrum hairline. Turn\n it off on a short page, where a rail that is born nearly full misreads the\n page it is on.\n\n### Content guidelines\n\n- Keep a nav label to one word. It is a bar.\n- Use each site's own name in the lockup, in the case it uses everywhere else.\n- Order the nav by how often a reader goes there, not alphabetically.\n- Order `sites` so the site the reader is on is where the family's reading order puts\n it, not always first. A set that is always ordered around the current site reads as\n a different set on every page.\n\n## Notes\n\n### The bar is opaque, and the hairline is the separator\n\nThe bar is `bg-background` with a `border-border` hairline, and it is what\n`SiteHeader` has always shipped. It used to be `bg-background/80` with a\n`backdrop-blur`, so the page showed through it blurred.\n\nA backdrop filter is evaluated against everything painted behind the element. This\nelement is a full-viewport-width sticky bar, so on a scrolling page the browser\nre-sampled and re-blurred the backdrop on every frame, on every page of every site\nthat composes the bar. Keeping the blur behind an `@supports` check would not have\npaid for it either: the check is false only in a browser that was not compositing\nanything, and true in every browser that does the expensive thing.\n\nOpaque is also the more legible of the two, and this is the part a token can be\nheld to. An opaque bar makes the bar's own contrast the pair `muted-foreground` on\n`background`, in every pack and in both modes, which is the pair the contrast gate\nalready checks. An eighty percent background has no token pair at all, because the\ncolour underneath it is whatever the reader's scroll position has brought there.\n\nTwo things to know if you were styling against the old bar:\n\n- A rule that set the bar's background translucency has nothing to sit on now. Set\n the colour on the page behind the bar, or leave the bar to `SiteHeader`'s.\n- A rule that relied on the bar being translucent to hide a scrolling heading will\n no longer hide it, which is the point.\n\n`will-change` is not on the bar and is not proposed for it. Two things on the bar\nmove, the current link's underline and the reading rail, and both are compositor\ntransforms the browser promotes while they run, so a hint applied at rest has no\nframe to be ready for and nothing that would release it.\n\n### The reading rail, and the current link's underline\n\nThe sticky bar carries a two-pixel spectrum line on its bottom hairline, filled\nleft to right by the page's own scroll. It is the travel figure at the scale of a\npage: one marker, one rail, the reader's hand providing the motion. The whole\nfigure lives behind a capability check in the stylesheet, so a browser without\nscroll-driven animations draws no rail rather than an empty one, and the rail is\n`aria-hidden` because a reading position the scrollbar already shows is not\ninformation the bar needs to announce.\n\nThe current link is marked by a persistent underline in the same spectrum rather\nthan by a filled pill, so the bar carries one indicator language on one edge. The\nunderline is shape rather than colour, and `aria-current` still says the page to\na reader who sees nothing at all.\n\n### The one client boundary\n\nThe lockup and the navigation are server Components and ship no JavaScript. The\ncontrols are a single client island, because a control's state is a reader's state,\nand the theme provider is mounted around those controls rather than in the site's\nroot layout so a page that renders none of them does not ship it.\n\nThat is what makes the whole set of `search`, `sites`, `theme`, `mode` and `nav`\noptional: a site that passes none of them renders the entire bar on the server and\nships none, which is the arrangement a static site wants.\n\n### The search index is a URL\n\n`search.indexUrl` is fetched on the first search and not before, and the dialog\nitself is loaded on the first activation. A site serves a JSON array of\n`{ id, title, url, description?, content?, breadcrumbs? }` from that route, and a\nstatic export can: a route handler with `revalidate = false` is emitted as a file.\n\nThe ranking is a scorer rather than an inverted index, and that is a deliberate\ntrade rather than an oversight. There is no stemming and no typo tolerance; every\nquery token has to be a substring of one of an entry's fields, which is why a\ntwo-word query narrows rather than widens. The alternative is a large search\ndependency in a package whose entire value is that it depends on a design system and\na token pipeline and nothing else, and whose ranking no consumer could then restyle\nor audit against this package's gates.\n\n### The colour chooser reads no token at runtime\n\nEach swatch is a `data-pack` boundary carrying `bg-primary`, not a literal colour read\nfrom the token build. A token resolved at runtime does not follow the cascade, so the\ndot holding a pack's colour would be right in one mode and stale in the other. The\nboundary is safe on this shape and only this shape: a pack repoints its own corner\nradius as well as its colour, so a boundary on anything not fully rounded would\nquietly restyle that element's corners. A disc is fully rounded, so the radius axis\nis a no-op.\n\n**The site bar with every control on it: search, the family's sites, the colour**\n\n```tsx\n'use client'\n\nimport { SiteNavbar } from '@nanisoft/prism-ui/blocks/site-navbar'\n\n/**\n * The site bar with every control on it: search, the family's sites, the colour\n * chooser, the light and dark control, and the panel that carries the navigation\n * below the row's own threshold.\n *\n * The search index URL is the documentation site's own, so the control in this\n * demo is the real one rather than a picture of one.\n */\nexport default function SiteNavbarDemo() {\n return (\n <SiteNavbar\n product={{ id: 'prism', name: 'Prism' }}\n homeHref=\"#navbar\"\n defaultPack=\"default\"\n defaultMode=\"light\"\n navLabel=\"Sections\"\n mobileLabels={{ open: 'Open menu', close: 'Close menu' }}\n currentSiteId=\"prism\"\n sitesLabel=\"The family\"\n sites={[\n { id: 'www', name: 'NaniSoft', pack: 'sky', href: 'https://www.nanisoft.com' },\n { id: 'nexus', name: 'Nexus', pack: 'lavender', href: 'https://nexus.nanisoft.com' },\n { id: 'atlas', name: 'Atlas', pack: 'mint', href: 'https://atlas.nanisoft.com' },\n { id: 'alphalens', name: 'AlphaLens', pack: 'blush', href: 'https://alphalens.nanisoft.com' },\n { id: 'prism', name: 'Prism', pack: null, href: '#navbar' },\n ]}\n nav={[\n { label: 'Overview', href: '#overview' },\n { label: 'Foundation', href: '#foundation' },\n { label: 'Content', href: '#content' },\n { label: 'Components', href: '#components' },\n { label: 'Blocks', href: '#blocks', current: true },\n { label: 'Pages', href: '#pages' },\n ]}\n search={{\n indexUrl: '/api/search',\n label: 'Search documentation',\n hint: 'Type to search every Section and every Item.',\n messages: {\n close: 'Close search',\n loading: 'Loading the search index.',\n failed: 'The search index could not be loaded.',\n empty: 'No matches.',\n one: 'result.',\n other: 'results.',\n },\n }}\n theme={{ label: 'Colour theme' }}\n mode={{ lightLabel: 'Switch to dark mode', darkLabel: 'Switch to light mode' }}\n />\n )\n}\n```\n\n## Props\n\n**`product`** `Pick<ProductMarkProps, 'id' | 'name' | 'pack' | 'mark'>` · required · default: — — The product this bar belongs to: the id, name, pack and drawn mark its `ProductMark` is drawn from, and the mark that heads the mobile panel. The bar renders the mark itself rather than a slot, because a bar whose brand lockup is a slot is a bar every consumer has to reimplement.\n**`homeHref`** `string` · optional · default: `'/'` — Where the brand lockup goes.\n**`defaultPack`** `PackId` · required · default: — — The pack a reader who has never chosen a pack gets. It is passed to the theme runtime rather than read from the document, so a site states its own default in one place and the control and the page cannot disagree about it.\n**`defaultMode`** `Mode` · required · default: — — The mode a reader who has never chosen gets. Required for the same reason `defaultPack` is, and because a toggle with no stated starting mode is a control whose icon is a guess until it is pressed once.\n**`nav`** `readonly SiteNavEntry[]` · optional · default: — — This site's own destinations: links, groups of links, or both in the order a reader should meet them. A link renders as itself; a group renders as one trigger opening its children as a menu. Omit for a bar that carries only the lockup.\n**`navLabel`** `string` · required · default: — — The accessible name of the site's navigation. Required, and required rather than defaulted because it is a word a reader hears: a Block that ships no copy ships no reader-facing copy either.\n**`currentPath`** `string` · optional · default: — — The route the reader is on, for a site that renders the bar once in a root layout and therefore cannot mark the current link itself. A site that composes the bar per page, which is the arrangement that keeps the current link server rendered, sets `current` on the one link instead and leaves this off. The two are alternatives rather than layers, and a site that set both would have two answers to which link is current.\n**`sites`** `readonly SwitcherProduct[]` · optional · default: — — The set of sites to move between, rendered as a menu. Omit it on a site that is the only one; a set of one is a menu with a single item and no reason to exist.\n**`currentSiteId`** `string` · optional · default: — — The `id` of the site the reader is on, marked in the menu.\n**`sitesLabel`** `string` · optional · default: — — The accessible name of the sites menu. Required whenever `sites` is passed, for the same reason `navLabel` is.\n**`search`** `SiteNavSearch` · optional · default: — — The search the bar offers, or nothing for no search.\n**`theme`** `SiteNavTheme` · optional · default: — — The colour chooser the bar offers, or nothing for no chooser.\n**`mode`** `SiteNavMode` · optional · default: — — The light and dark control the bar offers, or nothing for no toggle.\n**`mobileLabels`** `SiteNavMobileLabels` · required · default: — — The two names of the mobile trigger, which is a round icon with no visible word. Required rather than defaulted, because a default would be a sentence this Block ships about a product that is not the caller's, and because a bar that renders an unlabelled round control below its own threshold has a keyboard reader reaching for something it cannot name.\n**`actions`** `ReactNode` · optional · default: — — A slot for controls the product owns, after the ones this bar composes. A slot rather than a prop, because an account menu and a sign-in link are application state and neither is a fact a bar can be told.\n**`sticky`** `boolean` · optional · default: — — Keeps the bar at the top of the viewport as the page scrolls. On by default, because the bar is how a reader leaves the page they are on, and a bar that scrolls away takes that with it on exactly the long pages a reader needs it. `false` is honoured: the bar is then part of the flow and scrolls away with the page. It was accepted and ignored until this release, so a site that passed it was given the sticky bar it had asked not to have, and `SiteHeader` has always applied it the other way round.\n**`scrollRail`** `boolean` · optional · default: `true` — Draws the reading rail: a two-pixel spectrum line on the bar's bottom hairline that fills with the reader's own progress through the page. On by default, because the rail is the one motion that costs a reader nothing: it is bound to the scroll position the reader already owns, it runs on a transform, and a browser that cannot run it draws no rail rather than an empty promise of one. A reader who asked the platform for less motion gets a still spectrum hairline, the same figure without the claim. `false` is for the pages the rail would misread: a short page whose scroll never gets going, or a composition where a second line under the bar is a line too many. The rail rides only a sticky bar, so `sticky={false}` takes it regardless.\n**`className`** `string` · optional · default: — — Layout only. Changing a Prism-owned visual property from here is prohibited.\n\n## Composition\n\n**`exports`** `SiteNavbar` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/site-navbar/index.tsx` — the file this Block is defined in\n**`dependencies`** `ProductMark` — the public Prism exports this item composes\n",
|
|
6701
|
+
"props": "## Props\n\n**`product`** `Pick<ProductMarkProps, 'id' | 'name' | 'pack' | 'mark'>` · required · default: — — The product this bar belongs to: the id, name, pack and drawn mark its `ProductMark` is drawn from, and the mark that heads the mobile panel. The bar renders the mark itself rather than a slot, because a bar whose brand lockup is a slot is a bar every consumer has to reimplement.\n**`homeHref`** `string` · optional · default: `'/'` — Where the brand lockup goes.\n**`defaultPack`** `PackId` · required · default: — — The pack a reader who has never chosen a pack gets. It is passed to the theme runtime rather than read from the document, so a site states its own default in one place and the control and the page cannot disagree about it.\n**`defaultMode`** `Mode` · required · default: — — The mode a reader who has never chosen gets. Required for the same reason `defaultPack` is, and because a toggle with no stated starting mode is a control whose icon is a guess until it is pressed once.\n**`nav`** `readonly SiteNavEntry[]` · optional · default: — — This site's own destinations: links, groups of links, or both in the order a reader should meet them. A link renders as itself; a group renders as one trigger opening its children as a menu. Omit for a bar that carries only the lockup.\n**`navLabel`** `string` · required · default: — — The accessible name of the site's navigation. Required, and required rather than defaulted because it is a word a reader hears: a Block that ships no copy ships no reader-facing copy either.\n**`currentPath`** `string` · optional · default: — — The route the reader is on, for a site that renders the bar once in a root layout and therefore cannot mark the current link itself. A site that composes the bar per page, which is the arrangement that keeps the current link server rendered, sets `current` on the one link instead and leaves this off. The two are alternatives rather than layers, and a site that set both would have two answers to which link is current.\n**`sites`** `readonly SwitcherProduct[]` · optional · default: — — The set of sites to move between, rendered as a menu. Omit it on a site that is the only one; a set of one is a menu with a single item and no reason to exist.\n**`currentSiteId`** `string` · optional · default: — — The `id` of the site the reader is on, marked in the menu.\n**`sitesLabel`** `string` · optional · default: — — The accessible name of the sites menu. Required whenever `sites` is passed, for the same reason `navLabel` is.\n**`search`** `SiteNavSearch` · optional · default: — — The search the bar offers, or nothing for no search.\n**`theme`** `SiteNavTheme` · optional · default: — — The colour chooser the bar offers, or nothing for no chooser.\n**`mode`** `SiteNavMode` · optional · default: — — The light and dark control the bar offers, or nothing for no toggle.\n**`mobileLabels`** `SiteNavMobileLabels` · required · default: — — The two names of the mobile trigger, which is a round icon with no visible word. Required rather than defaulted, because a default would be a sentence this Block ships about a product that is not the caller's, and because a bar that renders an unlabelled round control below its own threshold has a keyboard reader reaching for something it cannot name.\n**`actions`** `ReactNode` · optional · default: — — A slot for controls the product owns, after the ones this bar composes. A slot rather than a prop, because an account menu and a sign-in link are application state and neither is a fact a bar can be told.\n**`sticky`** `boolean` · optional · default: — — Keeps the bar at the top of the viewport as the page scrolls. On by default, because the bar is how a reader leaves the page they are on, and a bar that scrolls away takes that with it on exactly the long pages a reader needs it. `false` is honoured: the bar is then part of the flow and scrolls away with the page. It was accepted and ignored until this release, so a site that passed it was given the sticky bar it had asked not to have, and `SiteHeader` has always applied it the other way round.\n**`scrollRail`** `boolean` · optional · default: `true` — Draws the reading rail: a two-pixel spectrum line on the bar's bottom hairline that fills with the reader's own progress through the page. On by default, because the rail is the one motion that costs a reader nothing: it is bound to the scroll position the reader already owns, it runs on a transform, and a browser that cannot run it draws no rail rather than an empty promise of one. A reader who asked the platform for less motion gets a still spectrum hairline, the same figure without the claim. `false` is for the pages the rail would misread: a short page whose scroll never gets going, or a composition where a second line under the bar is a line too many. The rail rides only a sticky bar, so `sticky={false}` takes it regardless.\n**`className`** `string` · optional · default: — — Layout only. Changing a Prism-owned visual property from here is prohibited.",
|
|
6597
6702
|
"composition": "## Composition\n\n**`exports`** `SiteNavbar` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/site-navbar/index.tsx` — the file this Block is defined in\n**`dependencies`** `ProductMark` — the public Prism exports this item composes",
|
|
6598
6703
|
"example": {
|
|
6599
6704
|
"title": "The site bar with every control on it: search, the family's sites, the colour",
|
|
@@ -7707,9 +7812,9 @@
|
|
|
7707
7812
|
"slug": "motion",
|
|
7708
7813
|
"section": "foundation",
|
|
7709
7814
|
"title": "Motion",
|
|
7710
|
-
"description": "
|
|
7815
|
+
"description": "Three laws: feedback on a short scale, a running figure on a cycle scale, and an arrival bound to the reader's own scroll, all by token.",
|
|
7711
7816
|
"url": "/foundation/motion",
|
|
7712
|
-
"markdown": "# Motion\n\
|
|
7817
|
+
"markdown": "# Motion\n\nThree laws: feedback on a short scale, a running figure on a cycle scale, and an arrival bound to the reader's own scroll, all by token.\n\nMotion has three laws, and they are different laws rather than one law with\nexceptions. The distinction is falsifiable: stop the animation and ask whether\nthe figure is still true and still legible.\n\n**Feedback is a response.** Something happened because the reader did it. They\nare waiting for an answer, so the answer is fast, decelerating and never\novershoots. The scale is `fast` 80ms, `base` 160ms and `slow` 280ms, with\n`ease-out` and `ease-in-out`. A component names a duration token and an easing\ntoken rather than a value, and it does not decide reduced motion for itself.\n\n**A cycle is a demonstration.** A marker crosses a rail, a reticle crosses a\nseries, a wavefront crosses a field, light crosses a ground. The reader did not\ncause it and is not waiting for it: the figure is making a claim about a\nmechanism and the motion is how the claim is made visible. If stopping the\nanimation leaves nothing true and nothing legible, the motion was the figure,\nand the right component is one that says it is atmosphere.\n\n**An arrival is bound to the reader's own scroll.** A section a reader scrolls\nto may meet them, and the meeting is the third law rather than a loosened\nsecond: it runs on CSS scroll-driven animations, so no script decides when it\nplays and no timer can play it early; the content exists in the document from\nthe first paint; a browser that does not ship the timeline declines the whole\nrule and shows the section simply there; and the universal reduced-motion rule\nhands that reader the same still page. The one script-driven figure in the\nsystem, `ParticleField`, follows the same test from the other side: it renders\nits complete still figure on the server and the script only adds the movement.\n\nThe cycle scale is separate because it measures a different thing by a factor\nof twenty-five, and one name cannot mean both. `ambient` is in seconds, and the\nemitted contract measures it with a floor of one second: a cycle faster than\nthat is unreadable before it is understood.\n\nThree rules bound the ambient layer, and none of them is a matter of taste.\n\n- **Nothing in the ambient layer is hidden.** No ambient rule sets `opacity: 0`\n and no ambient figure waits for a script or a timer. Every figure is complete\n at first paint, so a reader with scripting off, a print stylesheet or a\n crawler sees the whole drawing. The reveal layer is the one exception ever\n granted, and it is granted to the reader's own scroll position rather than to\n a script: it lives behind a capability check, and collapses to the fully\n visible form the moment anything, including reduced motion, declines to run\n it.\n- **Reduced motion removes the movement, not the figure.** The\n `prefers-reduced-motion` block is one unlayered rule setting `animation: none`\n and `transition: none` on the universal selector, and because every resting\n state is the full form, that reader gets the same figure, still.\n- **Compositor properties only.** Every ambient keyframe animates `transform`\n or `opacity`. Nothing in the layer animates a layout or paint property. The\n one named exception is the spectrum ink on display type, which animates a\n gradient position because text cannot be moved behind its own gradient, and\n the exception is a class in the stylesheet with a scope, not a licence.\n\nOne rule covers the whole package rather than each component guarding itself, and\nthat is a correction rather than a preference. A `motion-safe:` variant adds a\nrule inside `prefers-reduced-motion: no-preference` and never removes one, so a\nguarded transition beside an unguarded one ran at every setting: three components\nwere guarding, all three were different shapes, and one was guarding nothing. A\nduration cannot express the policy either, because it cannot tell a colour that\nchanged from something that moved. What a reader with the setting on keeps is\neverything that is not movement, which is to say every state and every\nannouncement, without the time.\n\nA keyframe is a mechanism and lives in the component package's stylesheet; a\nduration is a value and lives in the token source. They meet at exactly one\nplace, an `animation` shorthand whose length is `var(--ambient-travel)`, so\nneither can invent the other. The eight `prism-ambient-*` classes exist so that\na component picks a cycle by naming a mechanism and never writes a timing.\n\n<TokenTable group=\"motion\" />\n\n<TokenTable group=\"ambient\" />\n\n<TokenTable group=\"ambient-ease\" />\n",
|
|
7713
7818
|
"mirror": "/foundation/motion.md"
|
|
7714
7819
|
},
|
|
7715
7820
|
{
|
|
@@ -7887,9 +7992,9 @@
|
|
|
7887
7992
|
"slug": "prism-llms",
|
|
7888
7993
|
"section": "changelogs",
|
|
7889
7994
|
"title": "@nanisoft/prism-llms",
|
|
7890
|
-
"description": "0.5.2, 0.5.1, 0.5.0, 0.2.0, 0.1.0",
|
|
7995
|
+
"description": "0.6.0, 0.5.2, 0.5.1, 0.5.0, 0.2.0, 0.1.0",
|
|
7891
7996
|
"url": "/changelogs/prism-llms",
|
|
7892
|
-
"markdown": "# @nanisoft/prism-llms\n\n## 0.5.2\n\n### Patch Changes\n\n- 250b34e: `llms.txt` lists the Live Section: the three live surfaces were in the Store\n and in the Markdown mirror all along, but the index grouped only three of the\n four kinds, so an agent reading the index could not find them by listing. The\n agent skill's own count line now names the fourth kind and the Patterns\n Section, both of which it already shipped. The pack list the token projection\n reads is the Store's own `STORE_PACKS` rather than a second list in the build\n script, which still named six packs past the release that added Cyan and Gold,\n so every token answer an agent read described a family two members short.\n\n## 0.5.1\n\n### Patch Changes\n\n- 5c43520: Add `DocsShell`, a documentation page that takes a site's navigation as data\n \n `DocsShell` is a documentation screen: a navigation rail, the document, a\n contents rail, and a pager to the neighbouring pages. It replaces the unstyled\n `docs-shell` the retired line published, and it is the Page the three NaniSoft\n product sites already import by that name.\n \n The navigation is data. A tree of one page and a tree of twenty-seven render the\n same way, at any depth, and the frame is sized by what the caller passed rather\n than by a fixed arrangement, so three sites whose documentation sets differ in\n shape share one Page with no per-site fork.\n \n - `nav` and `toc` are closed unions of `page`, `group` and `divider`. Both are\n read in the order given: nothing is sorted or alphabetised, so a site that\n files its rail as a pipeline and its contents as a reference has both in one\n render.\n - The pager is derived from `nav` and `currentHref`. There is no `neighbours`\n prop, so a neighbour that is not in the tree cannot be expressed, and each\n consumer loses one derivation of its own.\n - A group with no `href` renders a `span` rather than an anchor with no\n destination. The three sites spell that case today as an empty string, and two\n of them carry a stylesheet rule that styles the result back into a label; those\n rules are now deletable.\n - A section carries no status, no badge, no count, no collapse and no sort. A\n status written into a title stays in the title as words.\n - Both rails are bounded against the viewport, because one site's tree is half\n again as tall as another's.\n \n The corpus fix that shipped with it: the props extractor read each declaration\n body twice with two depth counters that disagreed, so a Page whose `index.tsx`\n re-exported a value and a type from one sibling published every prop twice and\n swept the members of its navigation union into its own prop list. It now reads\n each body once.\n\n## 0.5.0\n\n### Minor Changes\n\n- 76c8d96: Fourteen items, four catalogue gates, and a Block that publishes its interface\n \n **Fourteen items ship.** Three Components (`Prose`, `FactList`, `ProductSwitcher`),\n nine Blocks (`ProcessRail01`, `StatusLedger01`, `ProductGrid01`, `StackGrid01`,\n `LogoStrip01`, `NoteGrid01`, `InstrumentPanel01`, `SiteHeader`, `SiteFooter`) and two\n Pages (`NotFoundPage`, `BlogPostPage`). Every one of them earns its place by the\n growth rule rather than by taste: a marketing Block when at least three of the four\n sites compose that section today, a Component when it has one job no existing\n Component can express. The evidence is in the ticket and in each item's own\n JSDoc, which names the sites and the line numbers.\n \n **The three contested shapes are settled as specified, and the specifications\n held.** A process rail admits four steps and refuses five, in the type: `steps` is\n a tuple union, so a five-element array is a compile error and not a fifth column. A\n status ledger has four tiers rather than five, and the words for a tier are the\n caller's: the four sites between them use eight words for those four states\n (`live`, `available` and `complete` are one; `approved`, `specified` and `designed`\n are one; then `planned`; then `direction`), and the vocabulary is what `statusLabel`\n is for. A product row's mark is two parts because a filled dot fails contrast on a\n light card, so the mark is `ProductMark`: a hairline ring in `brand-ink` around a\n core in the product's own `primary` fill, with the name in the brand ink beside it.\n \n **Four gates the catalogue's own rules depend on now exist and are wired into\n `check` and, because they read only `src/`, into `build`.** `check-item-docs`\n holds AGENTS.md's claim that a documentation comment is the only path from an Item\n to its published interface. `check-block-copy` holds DESIGN.md's claim that a\n Block ships no copy and no sample data. `check-item-category` holds CONTEXT.md's\n closed set of seven Categories and the sentence that a Block and a Page have none.\n `check-block-imports` holds the claim that a Block fetches nothing and a Page\n imports no router and no data client. Each resolves its roots from its own\n location, fails a root that resolves to nothing, prints its coverage and every\n exclusion on every run, has no report-only mode, and is proved red by a fixture\n before it is proved green.\n \n **Two of those four gates were red on the tree they were written against, and both\n findings were real.** `check-item-docs` found seven exported functions whose\n documentation comment was not attached to the declaration: `CardHeader`,\n `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `SectionHeading` and\n `Hero01`. A comment three functions above a declaration does not reach the emitted\n `.d.ts`, so those seven were absent from the corpus. `check-block-copy` found\n `pricing-01` rendering the word \"Popular\" on a featured plan that passed no badge:\n a hardcoded claim every consumer of the Block inherited, published by the corpus as\n part of the Block's own documentation. The fallback is gone; a featured plan with\n no badge now renders no badge.\n \n **Five additive fields on four shipped items**, each with three of the four\n consumers passing a value the item could not take, counted per consumer with a\n file and a line and never per string. `SectionHeading.index`, for the mono ordinal\n all four sites render above every section head (www `Landing.tsx:49`, atlas\n `landing.tsx:48`, nexus `Landing.tsx:38`, alphalens `Landing.tsx:66`).\n `Cta01.secondaryAction`, for the pair of actions all four closing bands render\n (www `Landing.tsx:186`, atlas `landing.tsx:243`, nexus `Landing.tsx:221`,\n alphalens `Landing.tsx:285`). `Cta01.note`, for the footnote three of the four\n render under it (atlas `landing.tsx:250`, nexus `Landing.tsx:228`, alphalens\n `Landing.tsx:292`). `FeatureGrid01.numbered`, for the ordinal all four render on\n every card (www `Landing.tsx:166`, atlas `landing.tsx:116`, nexus `Landing.tsx:132`,\n alphalens `Landing.tsx:142`). `Hero01.align`, for the left-aligned hero three of\n the four compose (atlas `landing.tsx:69`, nexus `Landing.tsx:63`, alphalens\n `Landing.tsx:87`). All five are additive: nothing changed shape and nothing was\n removed.\n \n **A Block and a Page publish both an interface section and a composition section.**\n `packages/llms` read the extractor's output for a Block and threw the interface half\n away, so the tool told an agent a Block had no own props. The extractor reads a\n named props type off an emitted declaration whether that declaration belongs to a\n Component or to a Block, so both sections are now built and both are assembled into\n the mirror file, `llms-full.txt` and the Store's `doc`.\n \n The Store now carries both fields for every kind, and `get_item_props` prints\n both when it has both. The sentence \"a Block has no own props\" is gone from the\n tool as well as from the published document. It was the one place the two\n sections were still exclusive, because that tool branched on `item.props` and\n fell back to the composition section, so a Block was answered with its\n composition and a denial of the interface it publishes. The test asserts the\n sentence's absence, so a refactor that restores it fails even if the composition\n is still printed, and a second test covers the one-present case so the\n both-present branch cannot swallow it.\n \n **Four accessible names became required props** on `SiteHeader`, `LogoStrip01`,\n `NotFoundPage` and `BlogPostPage`, and a Block that ships no copy ships no\n reader-facing copy either. `navLabel`, `productsLabel`, `label`, `linksLabel` and\n `trailLabel` are words a screen reader reads out, and no Block should be choosing\n them for a consumer whose site calls its own landmarks something else. This is the\n one breaking change in the release, and it is on four new Blocks and two new Pages\n plus one new required prop on `SiteHeader`, all of which are new in the same\n release, so no existing call site changes.\n \n **`ProductSwitcher.label` has no default** for the same reason: a consumer whose set\n is not a set of products should pass its own word rather than accept a claim about\n what the set is.\n\n### Patch Changes\n\n- 4832359: Build the corpus after the changelog routes are copied, and declare what it reads\n \n The corpus asserts that every published package shipping a changelog has a route\n in the site's content tree, and those routes are written by a copy step that\n belongs to the site package. The copy ran after this package was built, so on a\n cold machine the build failed with a message that read like a changelog file had\n been deleted, and a warm machine never saw the failure at all.\n \n The copy is now the task `@nanisoft/site#copy-changelogs`, which this build\n depends on in the task graph, and this build declares the content tree, the Item\n documentation tree, the two site script modules the builder imports, the\n workspace globs, the published manifests and the changelogs as its inputs, so a\n change to any of them rebuilds the corpus instead of replaying a cached one.\n \n The assertion is unchanged and still fails a published package the site does not\n publish, which is how a changelog path the corpus advertised while the site\n returned 404 for it was found. Its message now says which of the two causes it\n is. No corpus byte changes: the emitted corpus is the same.\n\n## 0.2.0\n\n### Minor Changes\n\n- c0cc0a9: Publish seven Sections, and keep every published URL resolving\n \n `STORE_SECTIONS` is `overview`, `foundation`, `content`, `changelogs` where it\n was `docs`, `foundations`, `content`, `changelogs`, and `STORE_SECTION_TITLES`\n carries the Section's own name beside its directory. The two are a pair for a\n reason: the directory is the route a page is addressed by and `get_page` matches\n on it, and the name is what `llms.txt` groups pages under and what `list_pages`\n counts. A Section renamed without its label renamed would be advertised under two\n names in two artifacts, and a page whose `section` no longer names a live\n directory is a page no tool can reach.\n \n The reading order of the prose Sections is the site's: Overview, then Foundation,\n then Content, then the catalogue, then the Changelogs. The corpus is regenerated\n at build, so `llms.txt`, `llms-full.txt`, `data.json` and every mirror under `md/`\n advertise the new routes, and the site Worker answers each old route with a\n permanent redirect generated from the same manifest. An agent holding a cached\n `llms.txt` from before this release resolves every URL it names and is told\n nothing about the move.\n \n Nothing in the item surface moves. The forty-two catalogue routes are the same\n forty-two addresses, their mirrors are byte-identical, and the nine tools keep\n their names and their positions; `list_pages` and `get_page` are unchanged apart\n from the Section names they group and describe, which is a parameter on the\n existing tools and not a rename of any of them.\n- b1125a0: Publish the Changelogs Section, and put the changelogs in the agent surface\n \n There was no Changelogs Section, so the only record of a change in a published\n package was a file inside its tarball. A reader now has one route per published\n package and an agent can ask for it. Both halves land together, so a Section never\n exists without being in the Corpus.\n \n The per-package pages are byte-for-byte copies of each published package's own\n `CHANGELOG.md`, written by a copy step inside the build the site already runs.\n That is the whole reason for the shape: a hand-maintained copy of a changelog\n drifts from what was published while continuing to look correct, and it is a third\n place to remember alongside the changeset and the JSDoc. The published packages\n are discovered from the workspace, so publishing a package is what adds a route,\n and a package that ships a changelog and no route fails the build in three\n places. The reference design system this is modelled on has reader-facing\n changelog pages and no such gate, which is how every one of them can be deleted\n and leave its continuous integration green.\n \n `STORE_SECTIONS` gains `changelogs`, so `llms.txt`, `llms-full.txt` and the\n Markdown mirror carry a Changelogs group, and the content walk reads the second\n extension the Section needs: a copied changelog is plain Markdown, and a walk\n filtered to `.mdx` would have left the whole Section out of the agent surface\n with a green build. The walk already recursed, so a page nested inside a Section\n reaches the Corpus the same way it did before.\n \n `PrismDocsStore` gains a required `changelogs` field carrying each package's\n bytes, its route and its version entries, and the MCP server gains one read-only\n tool, `get_changelog(package, version?)`, registered last. The eight existing\n tools keep their names and their positions: a Section joining the Corpus is a\n parameter on `list_pages` and `get_page`, which now read the Sections from the\n Store rather than from a list of the three prose Sections, and not a rename of\n anything. `get_changelog` returns the package's own file, because a generated\n summary would drift the moment a release was published, and a breaking change in\n a published package is exactly what a consuming agent has to be able to find.\n\n### Patch Changes\n\n- cbf442f: Make the corpus walk the content tree, and make a missing Section fail\n \n The generator read one directory per Section and filtered it to `.mdx`, so a page\n one folder deeper was absent from `llms.txt`, from `llms-full.txt`, from the\n `PrismDocsStore` and from every read-only MCP tool, with a green build. The walk\n now recurses, and a page's route, its `slug` and its Markdown mirror all come\n from where the page sits in the tree, so nesting content publishes a page\n instead of losing it.\n \n The same file guarded the Section loop with `if (!existsSync(dir)) continue`, so a\n Section that was declared and not on disk, which is what renaming one does, was\n skipped silently and deleted from every agent surface. It now throws.\n \n `llms.txt`, `llms-full.txt`, `data.json` and `md/**` are byte-for-byte unchanged\n for the current flat tree.\n\n## 0.1.0\n\nThe first versioned line of the rebuilt Prism agent corpus. One build-fresh\n`dist/` carries `llms.txt`, `llms-full.txt`, `prism-skill.md`, the per-item\nMarkdown mirror and `data.json`, the `PrismDocsStore` projection.\n\nChangesets are appended above this entry for every published change.\n",
|
|
7997
|
+
"markdown": "# @nanisoft/prism-llms\n\n## 0.6.0\n\n### Minor Changes\n\n- Regenerate the corpus for the third law of motion\n \n The corpus picks up the `ParticleField`, `AuroraWash` and `Reveal` entries, the\n `backdrop` slot on `Hero01`, the `aurora` field on `Backdrop01`, the\n `scrollRail` prop on `SiteNavbar`, and the rewritten Motion documentation,\n whose third law the new items document in their own JSDoc.\n\n## 0.5.2\n\n### Patch Changes\n\n- 250b34e: `llms.txt` lists the Live Section: the three live surfaces were in the Store\n and in the Markdown mirror all along, but the index grouped only three of the\n four kinds, so an agent reading the index could not find them by listing. The\n agent skill's own count line now names the fourth kind and the Patterns\n Section, both of which it already shipped. The pack list the token projection\n reads is the Store's own `STORE_PACKS` rather than a second list in the build\n script, which still named six packs past the release that added Cyan and Gold,\n so every token answer an agent read described a family two members short.\n\n## 0.5.1\n\n### Patch Changes\n\n- 5c43520: Add `DocsShell`, a documentation page that takes a site's navigation as data\n \n `DocsShell` is a documentation screen: a navigation rail, the document, a\n contents rail, and a pager to the neighbouring pages. It replaces the unstyled\n `docs-shell` the retired line published, and it is the Page the three NaniSoft\n product sites already import by that name.\n \n The navigation is data. A tree of one page and a tree of twenty-seven render the\n same way, at any depth, and the frame is sized by what the caller passed rather\n than by a fixed arrangement, so three sites whose documentation sets differ in\n shape share one Page with no per-site fork.\n \n - `nav` and `toc` are closed unions of `page`, `group` and `divider`. Both are\n read in the order given: nothing is sorted or alphabetised, so a site that\n files its rail as a pipeline and its contents as a reference has both in one\n render.\n - The pager is derived from `nav` and `currentHref`. There is no `neighbours`\n prop, so a neighbour that is not in the tree cannot be expressed, and each\n consumer loses one derivation of its own.\n - A group with no `href` renders a `span` rather than an anchor with no\n destination. The three sites spell that case today as an empty string, and two\n of them carry a stylesheet rule that styles the result back into a label; those\n rules are now deletable.\n - A section carries no status, no badge, no count, no collapse and no sort. A\n status written into a title stays in the title as words.\n - Both rails are bounded against the viewport, because one site's tree is half\n again as tall as another's.\n \n The corpus fix that shipped with it: the props extractor read each declaration\n body twice with two depth counters that disagreed, so a Page whose `index.tsx`\n re-exported a value and a type from one sibling published every prop twice and\n swept the members of its navigation union into its own prop list. It now reads\n each body once.\n\n## 0.5.0\n\n### Minor Changes\n\n- 76c8d96: Fourteen items, four catalogue gates, and a Block that publishes its interface\n \n **Fourteen items ship.** Three Components (`Prose`, `FactList`, `ProductSwitcher`),\n nine Blocks (`ProcessRail01`, `StatusLedger01`, `ProductGrid01`, `StackGrid01`,\n `LogoStrip01`, `NoteGrid01`, `InstrumentPanel01`, `SiteHeader`, `SiteFooter`) and two\n Pages (`NotFoundPage`, `BlogPostPage`). Every one of them earns its place by the\n growth rule rather than by taste: a marketing Block when at least three of the four\n sites compose that section today, a Component when it has one job no existing\n Component can express. The evidence is in the ticket and in each item's own\n JSDoc, which names the sites and the line numbers.\n \n **The three contested shapes are settled as specified, and the specifications\n held.** A process rail admits four steps and refuses five, in the type: `steps` is\n a tuple union, so a five-element array is a compile error and not a fifth column. A\n status ledger has four tiers rather than five, and the words for a tier are the\n caller's: the four sites between them use eight words for those four states\n (`live`, `available` and `complete` are one; `approved`, `specified` and `designed`\n are one; then `planned`; then `direction`), and the vocabulary is what `statusLabel`\n is for. A product row's mark is two parts because a filled dot fails contrast on a\n light card, so the mark is `ProductMark`: a hairline ring in `brand-ink` around a\n core in the product's own `primary` fill, with the name in the brand ink beside it.\n \n **Four gates the catalogue's own rules depend on now exist and are wired into\n `check` and, because they read only `src/`, into `build`.** `check-item-docs`\n holds AGENTS.md's claim that a documentation comment is the only path from an Item\n to its published interface. `check-block-copy` holds DESIGN.md's claim that a\n Block ships no copy and no sample data. `check-item-category` holds CONTEXT.md's\n closed set of seven Categories and the sentence that a Block and a Page have none.\n `check-block-imports` holds the claim that a Block fetches nothing and a Page\n imports no router and no data client. Each resolves its roots from its own\n location, fails a root that resolves to nothing, prints its coverage and every\n exclusion on every run, has no report-only mode, and is proved red by a fixture\n before it is proved green.\n \n **Two of those four gates were red on the tree they were written against, and both\n findings were real.** `check-item-docs` found seven exported functions whose\n documentation comment was not attached to the declaration: `CardHeader`,\n `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `SectionHeading` and\n `Hero01`. A comment three functions above a declaration does not reach the emitted\n `.d.ts`, so those seven were absent from the corpus. `check-block-copy` found\n `pricing-01` rendering the word \"Popular\" on a featured plan that passed no badge:\n a hardcoded claim every consumer of the Block inherited, published by the corpus as\n part of the Block's own documentation. The fallback is gone; a featured plan with\n no badge now renders no badge.\n \n **Five additive fields on four shipped items**, each with three of the four\n consumers passing a value the item could not take, counted per consumer with a\n file and a line and never per string. `SectionHeading.index`, for the mono ordinal\n all four sites render above every section head (www `Landing.tsx:49`, atlas\n `landing.tsx:48`, nexus `Landing.tsx:38`, alphalens `Landing.tsx:66`).\n `Cta01.secondaryAction`, for the pair of actions all four closing bands render\n (www `Landing.tsx:186`, atlas `landing.tsx:243`, nexus `Landing.tsx:221`,\n alphalens `Landing.tsx:285`). `Cta01.note`, for the footnote three of the four\n render under it (atlas `landing.tsx:250`, nexus `Landing.tsx:228`, alphalens\n `Landing.tsx:292`). `FeatureGrid01.numbered`, for the ordinal all four render on\n every card (www `Landing.tsx:166`, atlas `landing.tsx:116`, nexus `Landing.tsx:132`,\n alphalens `Landing.tsx:142`). `Hero01.align`, for the left-aligned hero three of\n the four compose (atlas `landing.tsx:69`, nexus `Landing.tsx:63`, alphalens\n `Landing.tsx:87`). All five are additive: nothing changed shape and nothing was\n removed.\n \n **A Block and a Page publish both an interface section and a composition section.**\n `packages/llms` read the extractor's output for a Block and threw the interface half\n away, so the tool told an agent a Block had no own props. The extractor reads a\n named props type off an emitted declaration whether that declaration belongs to a\n Component or to a Block, so both sections are now built and both are assembled into\n the mirror file, `llms-full.txt` and the Store's `doc`.\n \n The Store now carries both fields for every kind, and `get_item_props` prints\n both when it has both. The sentence \"a Block has no own props\" is gone from the\n tool as well as from the published document. It was the one place the two\n sections were still exclusive, because that tool branched on `item.props` and\n fell back to the composition section, so a Block was answered with its\n composition and a denial of the interface it publishes. The test asserts the\n sentence's absence, so a refactor that restores it fails even if the composition\n is still printed, and a second test covers the one-present case so the\n both-present branch cannot swallow it.\n \n **Four accessible names became required props** on `SiteHeader`, `LogoStrip01`,\n `NotFoundPage` and `BlogPostPage`, and a Block that ships no copy ships no\n reader-facing copy either. `navLabel`, `productsLabel`, `label`, `linksLabel` and\n `trailLabel` are words a screen reader reads out, and no Block should be choosing\n them for a consumer whose site calls its own landmarks something else. This is the\n one breaking change in the release, and it is on four new Blocks and two new Pages\n plus one new required prop on `SiteHeader`, all of which are new in the same\n release, so no existing call site changes.\n \n **`ProductSwitcher.label` has no default** for the same reason: a consumer whose set\n is not a set of products should pass its own word rather than accept a claim about\n what the set is.\n\n### Patch Changes\n\n- 4832359: Build the corpus after the changelog routes are copied, and declare what it reads\n \n The corpus asserts that every published package shipping a changelog has a route\n in the site's content tree, and those routes are written by a copy step that\n belongs to the site package. The copy ran after this package was built, so on a\n cold machine the build failed with a message that read like a changelog file had\n been deleted, and a warm machine never saw the failure at all.\n \n The copy is now the task `@nanisoft/site#copy-changelogs`, which this build\n depends on in the task graph, and this build declares the content tree, the Item\n documentation tree, the two site script modules the builder imports, the\n workspace globs, the published manifests and the changelogs as its inputs, so a\n change to any of them rebuilds the corpus instead of replaying a cached one.\n \n The assertion is unchanged and still fails a published package the site does not\n publish, which is how a changelog path the corpus advertised while the site\n returned 404 for it was found. Its message now says which of the two causes it\n is. No corpus byte changes: the emitted corpus is the same.\n\n## 0.2.0\n\n### Minor Changes\n\n- c0cc0a9: Publish seven Sections, and keep every published URL resolving\n \n `STORE_SECTIONS` is `overview`, `foundation`, `content`, `changelogs` where it\n was `docs`, `foundations`, `content`, `changelogs`, and `STORE_SECTION_TITLES`\n carries the Section's own name beside its directory. The two are a pair for a\n reason: the directory is the route a page is addressed by and `get_page` matches\n on it, and the name is what `llms.txt` groups pages under and what `list_pages`\n counts. A Section renamed without its label renamed would be advertised under two\n names in two artifacts, and a page whose `section` no longer names a live\n directory is a page no tool can reach.\n \n The reading order of the prose Sections is the site's: Overview, then Foundation,\n then Content, then the catalogue, then the Changelogs. The corpus is regenerated\n at build, so `llms.txt`, `llms-full.txt`, `data.json` and every mirror under `md/`\n advertise the new routes, and the site Worker answers each old route with a\n permanent redirect generated from the same manifest. An agent holding a cached\n `llms.txt` from before this release resolves every URL it names and is told\n nothing about the move.\n \n Nothing in the item surface moves. The forty-two catalogue routes are the same\n forty-two addresses, their mirrors are byte-identical, and the nine tools keep\n their names and their positions; `list_pages` and `get_page` are unchanged apart\n from the Section names they group and describe, which is a parameter on the\n existing tools and not a rename of any of them.\n- b1125a0: Publish the Changelogs Section, and put the changelogs in the agent surface\n \n There was no Changelogs Section, so the only record of a change in a published\n package was a file inside its tarball. A reader now has one route per published\n package and an agent can ask for it. Both halves land together, so a Section never\n exists without being in the Corpus.\n \n The per-package pages are byte-for-byte copies of each published package's own\n `CHANGELOG.md`, written by a copy step inside the build the site already runs.\n That is the whole reason for the shape: a hand-maintained copy of a changelog\n drifts from what was published while continuing to look correct, and it is a third\n place to remember alongside the changeset and the JSDoc. The published packages\n are discovered from the workspace, so publishing a package is what adds a route,\n and a package that ships a changelog and no route fails the build in three\n places. The reference design system this is modelled on has reader-facing\n changelog pages and no such gate, which is how every one of them can be deleted\n and leave its continuous integration green.\n \n `STORE_SECTIONS` gains `changelogs`, so `llms.txt`, `llms-full.txt` and the\n Markdown mirror carry a Changelogs group, and the content walk reads the second\n extension the Section needs: a copied changelog is plain Markdown, and a walk\n filtered to `.mdx` would have left the whole Section out of the agent surface\n with a green build. The walk already recursed, so a page nested inside a Section\n reaches the Corpus the same way it did before.\n \n `PrismDocsStore` gains a required `changelogs` field carrying each package's\n bytes, its route and its version entries, and the MCP server gains one read-only\n tool, `get_changelog(package, version?)`, registered last. The eight existing\n tools keep their names and their positions: a Section joining the Corpus is a\n parameter on `list_pages` and `get_page`, which now read the Sections from the\n Store rather than from a list of the three prose Sections, and not a rename of\n anything. `get_changelog` returns the package's own file, because a generated\n summary would drift the moment a release was published, and a breaking change in\n a published package is exactly what a consuming agent has to be able to find.\n\n### Patch Changes\n\n- cbf442f: Make the corpus walk the content tree, and make a missing Section fail\n \n The generator read one directory per Section and filtered it to `.mdx`, so a page\n one folder deeper was absent from `llms.txt`, from `llms-full.txt`, from the\n `PrismDocsStore` and from every read-only MCP tool, with a green build. The walk\n now recurses, and a page's route, its `slug` and its Markdown mirror all come\n from where the page sits in the tree, so nesting content publishes a page\n instead of losing it.\n \n The same file guarded the Section loop with `if (!existsSync(dir)) continue`, so a\n Section that was declared and not on disk, which is what renaming one does, was\n skipped silently and deleted from every agent surface. It now throws.\n \n `llms.txt`, `llms-full.txt`, `data.json` and `md/**` are byte-for-byte unchanged\n for the current flat tree.\n\n## 0.1.0\n\nThe first versioned line of the rebuilt Prism agent corpus. One build-fresh\n`dist/` carries `llms.txt`, `llms-full.txt`, `prism-skill.md`, the per-item\nMarkdown mirror and `data.json`, the `PrismDocsStore` projection.\n\nChangesets are appended above this entry for every published change.\n",
|
|
7893
7998
|
"mirror": "/changelogs/prism-llms.md"
|
|
7894
7999
|
},
|
|
7895
8000
|
{
|
|
@@ -7897,9 +8002,9 @@
|
|
|
7897
8002
|
"slug": "prism-mcp-server",
|
|
7898
8003
|
"section": "changelogs",
|
|
7899
8004
|
"title": "@nanisoft/prism-mcp-server",
|
|
7900
|
-
"description": "0.4.2, 0.4.1, 0.4.0, 0.2.0, 0.1.0",
|
|
8005
|
+
"description": "0.4.3, 0.4.2, 0.4.1, 0.4.0, 0.2.0, 0.1.0",
|
|
7901
8006
|
"url": "/changelogs/prism-mcp-server",
|
|
7902
|
-
"markdown": "# @nanisoft/prism-mcp-server\n\n## 0.4.2\n\n### Patch Changes\n\n- Updated dependencies [250b34e]\n - @nanisoft/prism-llms@0.5.2\n\n## 0.4.1\n\n### Patch Changes\n\n- Updated dependencies [5c43520]\n - @nanisoft/prism-llms@0.5.1\n\n## 0.4.0\n\n### Patch Changes\n\n- Updated dependencies [4832359]\n- Updated dependencies [76c8d96]\n - @nanisoft/prism-llms@0.3.0\n\n## 0.2.0\n\n### Minor Changes\n\n- c0cc0a9: Publish seven Sections, and keep every published URL resolving\n \n `STORE_SECTIONS` is `overview`, `foundation`, `content`, `changelogs` where it\n was `docs`, `foundations`, `content`, `changelogs`, and `STORE_SECTION_TITLES`\n carries the Section's own name beside its directory. The two are a pair for a\n reason: the directory is the route a page is addressed by and `get_page` matches\n on it, and the name is what `llms.txt` groups pages under and what `list_pages`\n counts. A Section renamed without its label renamed would be advertised under two\n names in two artifacts, and a page whose `section` no longer names a live\n directory is a page no tool can reach.\n \n The reading order of the prose Sections is the site's: Overview, then Foundation,\n then Content, then the catalogue, then the Changelogs. The corpus is regenerated\n at build, so `llms.txt`, `llms-full.txt`, `data.json` and every mirror under `md/`\n advertise the new routes, and the site Worker answers each old route with a\n permanent redirect generated from the same manifest. An agent holding a cached\n `llms.txt` from before this release resolves every URL it names and is told\n nothing about the move.\n \n Nothing in the item surface moves. The forty-two catalogue routes are the same\n forty-two addresses, their mirrors are byte-identical, and the nine tools keep\n their names and their positions; `list_pages` and `get_page` are unchanged apart\n from the Section names they group and describe, which is a parameter on the\n existing tools and not a rename of any of them.\n- b1125a0: Publish the Changelogs Section, and put the changelogs in the agent surface\n \n There was no Changelogs Section, so the only record of a change in a published\n package was a file inside its tarball. A reader now has one route per published\n package and an agent can ask for it. Both halves land together, so a Section never\n exists without being in the Corpus.\n \n The per-package pages are byte-for-byte copies of each published package's own\n `CHANGELOG.md`, written by a copy step inside the build the site already runs.\n That is the whole reason for the shape: a hand-maintained copy of a changelog\n drifts from what was published while continuing to look correct, and it is a third\n place to remember alongside the changeset and the JSDoc. The published packages\n are discovered from the workspace, so publishing a package is what adds a route,\n and a package that ships a changelog and no route fails the build in three\n places. The reference design system this is modelled on has reader-facing\n changelog pages and no such gate, which is how every one of them can be deleted\n and leave its continuous integration green.\n \n `STORE_SECTIONS` gains `changelogs`, so `llms.txt`, `llms-full.txt` and the\n Markdown mirror carry a Changelogs group, and the content walk reads the second\n extension the Section needs: a copied changelog is plain Markdown, and a walk\n filtered to `.mdx` would have left the whole Section out of the agent surface\n with a green build. The walk already recursed, so a page nested inside a Section\n reaches the Corpus the same way it did before.\n \n `PrismDocsStore` gains a required `changelogs` field carrying each package's\n bytes, its route and its version entries, and the MCP server gains one read-only\n tool, `get_changelog(package, version?)`, registered last. The eight existing\n tools keep their names and their positions: a Section joining the Corpus is a\n parameter on `list_pages` and `get_page`, which now read the Sections from the\n Store rather than from a list of the three prose Sections, and not a rename of\n anything. `get_changelog` returns the package's own file, because a generated\n summary would drift the moment a release was published, and a breaking change in\n a published package is exactly what a consuming agent has to be able to find.\n\n### Patch Changes\n\n- Updated dependencies [cbf442f]\n- Updated dependencies [c0cc0a9]\n- Updated dependencies [b1125a0]\n - @nanisoft/prism-llms@0.2.0\n\n## 0.1.0\n\nThe first versioned line of the rebuilt Prism MCP surface. The package is\ntransport-free: `createPrismMcpServer(store, options)` returns an `McpServer`\nthat the site Worker connects to the Agents SDK at `/mcp`. Eight read-only\nretrieval tools answer over the bundled `PrismDocsStore`.\n\nChangesets are appended above this entry for every published change.\n",
|
|
8007
|
+
"markdown": "# @nanisoft/prism-mcp-server\n\n## 0.4.3\n\n### Patch Changes\n\n- Updated dependencies\n - @nanisoft/prism-llms@0.6.0\n\n## 0.4.2\n\n### Patch Changes\n\n- Updated dependencies [250b34e]\n - @nanisoft/prism-llms@0.5.2\n\n## 0.4.1\n\n### Patch Changes\n\n- Updated dependencies [5c43520]\n - @nanisoft/prism-llms@0.5.1\n\n## 0.4.0\n\n### Patch Changes\n\n- Updated dependencies [4832359]\n- Updated dependencies [76c8d96]\n - @nanisoft/prism-llms@0.3.0\n\n## 0.2.0\n\n### Minor Changes\n\n- c0cc0a9: Publish seven Sections, and keep every published URL resolving\n \n `STORE_SECTIONS` is `overview`, `foundation`, `content`, `changelogs` where it\n was `docs`, `foundations`, `content`, `changelogs`, and `STORE_SECTION_TITLES`\n carries the Section's own name beside its directory. The two are a pair for a\n reason: the directory is the route a page is addressed by and `get_page` matches\n on it, and the name is what `llms.txt` groups pages under and what `list_pages`\n counts. A Section renamed without its label renamed would be advertised under two\n names in two artifacts, and a page whose `section` no longer names a live\n directory is a page no tool can reach.\n \n The reading order of the prose Sections is the site's: Overview, then Foundation,\n then Content, then the catalogue, then the Changelogs. The corpus is regenerated\n at build, so `llms.txt`, `llms-full.txt`, `data.json` and every mirror under `md/`\n advertise the new routes, and the site Worker answers each old route with a\n permanent redirect generated from the same manifest. An agent holding a cached\n `llms.txt` from before this release resolves every URL it names and is told\n nothing about the move.\n \n Nothing in the item surface moves. The forty-two catalogue routes are the same\n forty-two addresses, their mirrors are byte-identical, and the nine tools keep\n their names and their positions; `list_pages` and `get_page` are unchanged apart\n from the Section names they group and describe, which is a parameter on the\n existing tools and not a rename of any of them.\n- b1125a0: Publish the Changelogs Section, and put the changelogs in the agent surface\n \n There was no Changelogs Section, so the only record of a change in a published\n package was a file inside its tarball. A reader now has one route per published\n package and an agent can ask for it. Both halves land together, so a Section never\n exists without being in the Corpus.\n \n The per-package pages are byte-for-byte copies of each published package's own\n `CHANGELOG.md`, written by a copy step inside the build the site already runs.\n That is the whole reason for the shape: a hand-maintained copy of a changelog\n drifts from what was published while continuing to look correct, and it is a third\n place to remember alongside the changeset and the JSDoc. The published packages\n are discovered from the workspace, so publishing a package is what adds a route,\n and a package that ships a changelog and no route fails the build in three\n places. The reference design system this is modelled on has reader-facing\n changelog pages and no such gate, which is how every one of them can be deleted\n and leave its continuous integration green.\n \n `STORE_SECTIONS` gains `changelogs`, so `llms.txt`, `llms-full.txt` and the\n Markdown mirror carry a Changelogs group, and the content walk reads the second\n extension the Section needs: a copied changelog is plain Markdown, and a walk\n filtered to `.mdx` would have left the whole Section out of the agent surface\n with a green build. The walk already recursed, so a page nested inside a Section\n reaches the Corpus the same way it did before.\n \n `PrismDocsStore` gains a required `changelogs` field carrying each package's\n bytes, its route and its version entries, and the MCP server gains one read-only\n tool, `get_changelog(package, version?)`, registered last. The eight existing\n tools keep their names and their positions: a Section joining the Corpus is a\n parameter on `list_pages` and `get_page`, which now read the Sections from the\n Store rather than from a list of the three prose Sections, and not a rename of\n anything. `get_changelog` returns the package's own file, because a generated\n summary would drift the moment a release was published, and a breaking change in\n a published package is exactly what a consuming agent has to be able to find.\n\n### Patch Changes\n\n- Updated dependencies [cbf442f]\n- Updated dependencies [c0cc0a9]\n- Updated dependencies [b1125a0]\n - @nanisoft/prism-llms@0.2.0\n\n## 0.1.0\n\nThe first versioned line of the rebuilt Prism MCP surface. The package is\ntransport-free: `createPrismMcpServer(store, options)` returns an `McpServer`\nthat the site Worker connects to the Agents SDK at `/mcp`. Eight read-only\nretrieval tools answer over the bundled `PrismDocsStore`.\n\nChangesets are appended above this entry for every published change.\n",
|
|
7903
8008
|
"mirror": "/changelogs/prism-mcp-server.md"
|
|
7904
8009
|
},
|
|
7905
8010
|
{
|
|
@@ -7907,9 +8012,9 @@
|
|
|
7907
8012
|
"slug": "prism-tokens",
|
|
7908
8013
|
"section": "changelogs",
|
|
7909
8014
|
"title": "@nanisoft/prism-tokens",
|
|
7910
|
-
"description": "1.2.0, 0.16.0, 0.15.0, 0.10.0, 0.6.0, 0.5.1, 0.5.0",
|
|
8015
|
+
"description": "1.5.0, 1.2.0, 0.16.0, 0.15.0, 0.10.0, 0.6.0, 0.5.1, 0.5.0",
|
|
7911
8016
|
"url": "/changelogs/prism-tokens",
|
|
7912
|
-
"markdown": "# @nanisoft/prism-tokens\n\n## 1.2.0\n\n### Minor Changes\n\n- Two new packs: Cyan and Gold\n \n The family's product map asked for two hues the palette did not carry, and a\n product's ground is a fact about the system rather than about the site that\n wears it, so the hues arrive here instead of as consumer overrides.\n \n `cyan` is the Nexus pack: pastel cyan-blue, generated at hue 215 in OKLCH with\n the same perceptually even lightness ramp as the other five, at the crispest\n radius in the set (0.5rem) beside Sky. `gold` is the AlphaLens pack: pastel\n amber-gold at hue 80, at the roomiest radius (1rem) beside Peach. Both carry\n the full shadcn-compatible contract, tinted neutrals at the family's chroma,\n and pass the contrast gate at 38/38 required pairs in both modes.\n \n `PACKS` in the theming module and `STORE_PACKS` in the llms corpus carry the\n two new ids, so a consumer's ground-pack validation and every agent reading\n the corpus see the same vocabulary. Peach returns to reserved. The published\n pack count is now seven pastels plus the neutral default.\n\n## 0.16.0\n\n### Minor Changes\n\n- 1a0ba35: A page `h1` is now visibly larger than the `h2` sections under it\n \n A page `h1` rendered at 1.875rem and every section heading under it at 1.5rem, so\n the two were one fifth apart and the page had no top to its hierarchy. The fix is\n in the type scale rather than in the Component, because the scale stopped at `4xl`\n and there was no step for a page's own claim to take.\n \n **Two steps are authored, and they are the two widest gaps in the group.**\n `5xl` is 3rem and `6xl` is 3.75rem, both at a line-height of 1. Every other step\n in the `text` group is a whole multiple of 0.125rem and these two are too;\n `4xl` to `5xl` is four thirds, the first gap in the group wider than any gap it\n already had, and `5xl` to `6xl` is five fourths. They are authored as a pair\n rather than as one step because every rung of the heading ladder is a pair, a base\n step and one step up at `sm`, so a single step above `4xl` would have made the\n page heading the one heading in the system that does not grow at a width.\n \n Both steps reach CSS and the published pack contract the way every other authored\n step does, so `text-5xl` and `text-6xl` are utilities a consumer can compose, and\n neither is a value a consumer has to write.\n \n **The heading ladder now descends from the new ceiling, one authored step per\n level, and every level holds a step of its own.** There are exactly six authored\n steps at or above Body, so the table lands on the floor with a distinct step at\n every rung, where before `h4` through `h6` shared one. `h1` is at `5xl`, rising to\n `6xl` at `sm`; `h2` is at `4xl`, rising to `5xl`; and the table descends to `lg` at\n `h6`, which is the step Body is set at. **What a consumer sees is every section\n title moving up one step and every page heading moving up two**: a section title\n goes from 1.5rem to 2.25rem and from 1.875rem to 3rem at `sm`, and a page `h1` goes\n from 1.875rem to 3rem and from 2.25rem to 3.75rem at `sm`. No call site changes,\n because the level a Block was already passing is still what decides its size.\n \n **Five surfaces that render a page's own `h1` now ask the same table for its size\n rather than naming a step.** `page-header-01`, `docs-shell`, `blog-post-page`,\n `not-found-page` and `error-page` each wrote a fixed step into a heading that was\n meant to be a page's top-level claim, so with the ceiling lifted they would have\n rendered a page heading a rung or two below the section titles under it. They ask\n `headingSizeClass`, which is what `SectionHeading` and `Cta01` already asked, and\n `Cta01` still carries no literal.\n \n **Four more surfaces that resolved their own heading size, found the same way.**\n A Block that takes a `headingLevel` and writes the size beside it forwards the level\n and changes nothing a reader can see, which is the defect above at the scale of one\n Block. `data-table-01` said `text-lg`, `run-console-01` said `text-base`, and both\n moved the outline without moving the size. `run-stream-01` renders a literal `h2`\n at `text-lg`, which was the floor of the old scale, so a live region's `h2` came\n out below the `h3` introducing the section holding it. `/foundation/themes` said\n `text-3xl`, which had been Display and is now the `h3` step, so that page's `h1`\n rendered a rung below the section titles under it. All four ask `headingSizeClass`\n now, and the three Blocks carry `text-balance` with the size they gained: the ladder\n states weight, tracking and balance as one package and `SectionHeading` carries all\n three at every rung, so a title lifted from 16 or 18 pixels to 24 or 36 is worse\n unwrapped than the one it replaced.\n \n **The Item page's appendix heading is on the article's ladder, and asking the Block\n ladder for it was the wrong correction.** It said `text-xl`, which put an `h2` at 20\n pixels under the prose `h2` at 30 and below the prose `h3` at 24 above it. Asking\n `headingSizeClass` removed the third tier and created a worse one: \"API reference\"\n is a section of an article exactly as \"Overview\" is, so it takes the article's\n ladder, and the Block ladder is a step and a half above it at `sm`. The Item page\n then carried three sections at 30 pixels and an appendix at 48, and the appendix was\n the second largest thing on the page. It is now a bare `<h2>` inside the same\n `.prose` wrapper as the Item's own MDX body, so the article's rules size it at 30\n pixels and it carries no class of its own: every section heading in that article is\n a bare tag the article styles, and a heading naming its own step would be the second\n answer to a question the article has already answered. The table stays outside\n `.prose`, whose descendant rules would put its cells at the body step in the muted\n colour, and the gap above it is the article's own 16 pixels rather than a second\n region's 32.\n \n **The five theme names are card titles, and they compose `CardTitle` to say so.**\n They carried `font-medium` and no size, so they rendered at 16 pixels and 500 under\n a 60 pixel `h1`. Asking the ladder for the size would have been wrong: five themes at\n the `h2` step is five 48 pixel headings over five cards on a page whose own claim is\n the `h1` above them. `CardTitle` already holds the answer the library needs: a card\n title sits at body size, and the element and the visual step are separate questions.\n So the size stands, the tag stands at `h2` because the grid draws no section heading\n of its own, and the weight now comes from the one Component that owns it rather than\n from a literal. What a consumer sees is five theme names one step heavier.\n \n **The two Foundation readers took opposite answers, and the reason is worth more\n than either change.** Both render inside an MDX body, which the site wraps in\n `.prose`, and both used to carry literals, which a utility layer lets win over the\n article's own component-layer rule: `/foundation/colors` published an `h2` at 18\n pixels and an `h3` at 14, below the `h3` above them.\n \n `ColorTokens`' four headings are sections. Two divisions of an article and a\n partition under one of them is a structure, not a list of labels, so they name no\n step and the article decides, which is the one copy of the ladder there is.\n \n `TokenBrowser`'s are not sections, and dropping its literals let the wrong authority\n decide them: every group name came out at the prose `h2`, 30 pixels, which is more\n than twice the 14 pixels the navigation rail's own section titles are set at, on the\n page whose whole job is being navigated. Each of those 184 names is a table's name\n and nothing else, so they are captions: `h3` at `text-sm`, in the mono face because a\n token key is machine notation, at the same 14 pixels and 500 as the column heads of\n the table underneath. That is the caption `ApiTable` already renders for a compound\n export's part names on every Item page, so this is the repository's own treatment\n rather than a third one. One heading on the page, \"Semantic contract\", is a division\n of the article rather than a label, and it keeps naming no step.\n \n **`Prose` walks the same ladder from one rung down, and a prose heading therefore\n sits below a Block heading at the same level.** Its child treatments now set `h1`\n at `4xl` and descend one authored step per level to `lg` at `h5` and `h6`,\n which is the same scale and the same floor entered at the rung that fits the\n reading measure: Display is 3rem, and a 42rem column is fourteen of those. The new\n `h5` and `h6` treatments are new, so a document that reached past `h4` before now\n has an answer rather than inheriting nothing. **The consequence is stated rather\n than left to be found.** A Block-composed `h2` is 2.25rem and 3rem above `sm`; a\n prose `h2` is 1.875rem and does not grow. They are 1.2 apart at the base width and\n 1.6 apart above `sm`, where they were 1.25 apart before this change, so a consumer\n who sets a `Prose` section beside a Block section sees two tiers rather than one.\n `DESIGN.md` under Typography, Hierarchy now says so, and says which to reach for.\n `DocsShell` runs `Prose` at `fullWidth`, so the frame decides the width: 35rem of\n article track at a viewport of 1216px or wider, and less below that, which makes\n the case for entering one rung down stronger there rather than weaker.\n \n **`Heading`'s free `size` prop is unchanged, and this is stated rather than\n implied.** `size` is a choice made beside the tag, for a heading the surrounding\n document has already placed and wants attuned, and its largest arm is a step of\n the scale rather than a rung of the ladder. Its two largest arms were not removed\n and no new ones were added, so no consumer's call site changes and there is still\n no second route to a page `h1` from inside the library. `search-page` and\n `status-page` are the two surfaces that take a heading's *tag* from\n `childLevel(headingLevel)` and its size from this prop, and they are left as they\n are: those are labels over a list of results and over a list of incidents, and a\n caption is not a rung.\n \n **`scripts/check-heading-scale.mjs` now holds three things it described but did not\n check.** Its rule 4 claimed a table that jumps is a finding and only tested for a\n table that rises, so a table that skipped a rung passed it: descending, reaching\n the ceiling and flooring at or above Body, and rendering the page `h1` one gap\n above its own `h2`, which is the defect these steps exist to remove. And the\n literal Display step it refused on `Cta01` was written out as `text-3xl` and\n `sm:text-4xl`, which stopped being Display the moment the ceiling moved. Both are\n now read from the authored scale and the shipped table rather than restated, and\n both have a staged fixture in `scripts/__tests__/heading-scale.test.mjs`. The third\n is the Item rule below, which is new rather than a restatement.\n \n **The Item document's own table was the retired ladder, and it now states this\n one.** `apps/site/items/component/layout/section/section.mdx` is the page a reader\n is on when they ask what `as` resolves to, and it listed `h1` at `text-3xl` and\n `h4` through `h6` all at `text-lg`: the table as it was before this change. It now\n states the ladder the Component renders, and the sentence under it about where the\n scale stops has been corrected with it, because a corrected table beside a stale\n paragraph is the same disagreement one row down.\n \n **The gate also reads the Item document now, and that is the third copy of the\n table.** `apps/site/items/component/layout/section/section.mdx` states the ladder\n in its prose, and neither the declaration build nor the corpus reaches it, so when\n the ceiling moved it kept publishing the retired table. The new rule compares it to\n the same `HEADING_SIZE` the JSDoc is compared to, by the same cell-for-cell shape,\n so it names no step of its own; it reads the whole Item rather than a slice, so a\n second table in that prose is a finding; and it fails an Item that states no table\n at all, because a comparison with no rows finds nothing to disagree with. Three\n staged fixtures cover the three cases.\n \n **What the gate still does not hold is written down rather than left to be\n discovered.** `Cta01` is named as a root because it is the one Block whose heading\n cannot be composed from `SectionHeading` at all, so a literal step on it has no\n table to fall back to. Beyond that file the gate cannot go, and the reason is\n stated rather than left to be found: whether a heading a Block renders is a rung of\n the ladder or a caption inside that Block is not decidable from source.\n Forty-eight headings across `packages/ui/src` and `apps/site/src` name a step of\n their own, the eleven rungs among them now ask the table, and the thirty-seven that\n remain are captions. Two surfaces bind a caption to `headingLevel` rather than to\n `childLevel`, so a rule reading the tag cannot sort them, and a root list holding\n the rungs would be a list somebody maintains. That is how the `Cta01`-only rule\n came to hold the one Block already fixed and pass over the four that were not, and\n the count is written into `docs/quality-gates.md` so the next reader starts from it.\n `TokenBrowser` adds one more to that population, which is the point of naming a\n caption's step rather than inheriting it: the scan can see it, and it is below the\n floor by arithmetic.\n \n **The limit of the scan that took that count is now written down too, and it is the\n more useful half of it.** The count came from looking for a literal size utility\n beside a heading tag, and a heading whose size it inherits names nothing: the five\n theme names above carried no size at all, and two scans of exactly that shape passed\n them at 16 pixels and 500. A size that is inherited is a size nobody chose, and \"no\n literal\" is invisible to a scan that looks for literals, so that count has never\n been a count of every heading that names a step, and the number nobody chose is the\n half of it that reached a reader. What the count is good for is the arithmetic: a\n heading it reports is a rung if the step it names is at or above the step Body is set\n at, and a caption if it is below, so the two sort without anybody deciding which is\n which. The remedy for what the scan cannot see is on the surfaces rather than in the\n gate, and it is the one the token inventory now uses: a caption names its own step,\n so it is visible to the scan and it is below the floor by definition.\n \n The bump is a minor in both packages rather than a patch because a new authored\n step is a new public contract: `--text-5xl` and `--text-6xl` with their\n `--line-height` companions enter the emitted CSS and the published pack contract,\n and every heading on every page composed from a Block moves.\n\n## 0.15.0\n\n### Minor Changes\n\n- 6dc7bb8: There is one container namespace, and `max-w-6xl` no longer resolves\n \n **If you wrote `max-w-6xl`, `max-w-2xl`, `max-w-xl`, `max-w-lg`, `max-w-md`,\n `max-w-sm` or `max-w-5xl` against this package, that class now produces no rule at\n all.** Nothing errors and nothing warns: the element simply loses its cap and lays\n out at whatever its parent gives it. Grep your own repository for `max-w-` before\n you upgrade. The replacements are in the list below and every one of them resolves.\n \n The reason is the defect this release ends. `@nanisoft/prism-tokens` authors three\n container widths and Tailwind ships thirteen of its own, and until now both shipped\n in the same stylesheet. Three of Tailwind's were numerically identical to three of\n ours: its largest at 72rem beside `--container-page`, its fifth at 42rem beside\n `--container-measure`, its fourth at 36rem beside `--container-measure-narrow`.\n Nothing rendered differently, so nothing failed and every gate was green. What it\n cost was that one width had two names, and the first retune of an authored token\n would have moved every surface reaching it by one spelling and left every surface\n reaching it by the other exactly where it was. `Section`, `SiteHeader`,\n `SiteFooter`, `ChartCard01`, `DocsShell` and `QuickView01` all used the Tailwind\n spelling of the page column; `Cta01` and `SectionHeading` used the Tailwind\n spellings of the two measures.\n \n The token build now closes the whole namespace, with\n `--container-*: initial` ahead of the authored entries rather than a list of the\n seven steps that happened to be emitted. A wildcard is the load-bearing word: the\n thirteen names it retires are `3xs 2xs xs sm md lg xl 2xl 3xl 4xl 5xl 6xl 7xl`,\n and a list would have been a second list to keep in step with a dependency's\n default theme, which is the event the line exists to survive. Order matters and is\n asserted: Tailwind resolves a theme in source order, and the same declaration\n written after the authored entries clears all eight of ours and ships no container\n at all.\n \n **What to write instead**\n \n | You wrote | Write | Value |\n | --- | --- | --- |\n | `max-w-6xl` | `max-w-page` | 72rem |\n | `max-w-2xl` | `max-w-measure` | 42rem |\n | `max-w-xl` | `max-w-measure-narrow` | 36rem |\n | `max-w-md` on a dialog or an alert | `max-w-overlay-dialog` | 28rem |\n | `max-w-lg` on a form dialog | `max-w-overlay-form` | 32rem |\n | `max-w-xl` on a command palette or a search | `max-w-overlay-palette` | 36rem |\n | `max-w-sm` on a side sheet or a drawer | `max-w-overlay-panel` | 24rem |\n | `max-w-5xl` on a lightbox | `max-w-overlay-media` | 64rem |\n \n **An overlay's width is a property of the kind of surface it is, not of the page\n grid underneath it**, and that is why the five overlay widths are authored rather\n than carried on a Tailwind name. A retune of the reading measure must not move a\n dialog and a retune of the page column must not move either, so the two families\n are named apart inside the one container group. They share a group because\n Tailwind 4's `max-w-*` resolves `--spacing-*` and `--container-*` and nothing\n else, so a maximum width that is to be a token has to live in one of those two\n namespaces or it is a value written in a Component. `overlay-palette` and\n `measure-narrow` are both 36rem; that is two measurements that happen to agree and\n not one measurement with two names, and the token source says so and the emitted\n contract holds it to saying so.\n \n **Nothing is supposed to change where you can see it.** Every migration above\n preserves the exact rendered width, so a page that only consumed this stylesheet\n looks the same before and after. What changes is the next retune: it now moves one\n surface rather than half of them.\n \n **A width that was arithmetic on the spacing base is left alone, deliberately.**\n `max-w-96`, `max-w-72`, `max-w-64`, `max-w-32` and `max-w-20` resolve to\n `calc(var(--spacing) * N)` and never touched the container namespace at all. They\n are how a chart's axis band, a token table's column and a documentation frame are\n sized, where nobody took a decision and naming one would invent it. They stay.\n \n **Two gates now hold this down, because the question has two sides.**\n `scripts/check-elevation-layout.mjs` reads every `w-*` and `max-w-*` name out of\n the class strings and holds it to the container group read out of the token source,\n so a width nobody authored is a finding with a file and a line; it reads\n `apps/site/items` as well as `apps/site/src` and `packages/ui/src`, because that is\n where the documentation Demos live and where the site's own build scans.\n `packages/ui/scripts/check-container-namespace.mjs` reads the built stylesheet and\n holds the artefact: that the theme block declares exactly the authored containers,\n that none of Tailwind's thirteen steps is declared or read, that the close is\n written ahead of the entries rather than after them, and that every container width\n this package writes is emitted as a utility reading its own variable. It reads the\n step list out of the installed `tailwindcss/theme.css` rather than restating it, so\n a dependency that adds a step is covered without an edit. Tailwind's extractor\n reads this package's comments as well as its code, so a retired class name written\n down in a JSDoc block used to emit a utility; there is now a rule about that too.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** A class a consumer wrote stops producing a rule with no\n error, which is breaking by any ordinary reading, and this entry says so plainly.\n `major` in this repository publishes `1.0.0`, and every release to date has been\n `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent slot.\n Publishing 1.0.0 with this change in it would assert an API stability guarantee\n this library has not earned, and it would be the first release in the project's\n history to use the bump at all. The break is real and is the subject of this\n entry; the number it lands on is the line's decision and the line is pre-1.0.\n \n **One collateral repair on the documentation site, recorded here because it is\n why `globals.css` moved.** The site's own build and this package's meet in one\n `utilities` layer, and this package emits a bare `.grid-cols-6` as the base of the\n `lg:` variant four of its Blocks use. That bare rule landed after the site's\n `sm:grid-cols-11`, so every colour ramp on the Foundations page was drawing six\n columns at every width above 640 against a comment beside the class saying\n eleven. Whether `check-utility-cascade.mjs` saw it depended on which order two\n content-hashed CSS chunks sorted in, which this change perturbed and which nothing\n about the tree controls. Neither side is wrong, so the site's variant is restated\n in the `site-variants` layer that already exists for six other collisions. The\n repaired behaviour is on the documentation site only; nothing in this package\n changes.\n- 6dc7bb8: Ship the interface face and its metric-adjusted fallback, and stop the site loading its own\n \n `@nanisoft/prism-ui` shipped three Inter weights and no fallback face, so a reader\n waiting on the web font saw the platform's UI face jump into place when Inter\n arrived. The stylesheet now also ships a local Arial carrying Inter's metrics\n through `size-adjust`, `ascent-override`, `descent-override` and\n `line-gap-override`, and `--font-sans` names it immediately after Inter, so the\n swap window occupies Inter's own line box. The four numbers are measured off the\n shipped face rather than chosen, and they are the ones `next/font` measured for the\n same typeface, so the line box is unchanged from what the documentation site had.\n \n A fourth face ships beside them: Inter italic at weight 400, the Latin subset of\n the same release, covering the same 230 codepoints as its upright siblings.\n `Prose` sets `blockquote` in italic and the surface had been asking for a style no\n shipped file provided, so every consumer has been reading a synthesized oblique.\n One real face covers every weight a browser asks for, because the font matcher\n takes the closest available weight rather than synthesizing once a face exists.\n The whole set is 93.5 KB, up 22.8 KB, and no consumer has to do anything.\n \n **The documentation site no longer loads a face of its own.** It shipped a 723 KiB\n unsubsetted variable Inter and its italic through `next/font/local`, preloaded both\n in all 581 exported documents, and applied the result to `<body>` as a directly set\n `--font-sans`. A directly applied custom property outranks an inherited one, so the\n library's faces were never fetched on the site that documents them: a missing or\n corrupt shipped font rendered perfectly there with every gate green. The site now\n resolves the same stack you do, which is the only way it can be evidence for the\n package. If you were relying on the site to look right while your own copy of Inter\n was broken, that is now a failure you can see.\n\n## 0.10.0\n\n### Minor Changes\n\n- 75434f8: Add a running figure to the system, and the ambient cycle scale that prices it\n \n The system had one law of motion: motion is state feedback, on an 80/160/280\n millisecond scale, and there are no keyframes anywhere. That law was right and\n it held. It also meant a product page could say what its product does and show\n nothing about it, which is what happened to four sites at once.\n \n This adds the second law, and it is a separate law rather than an exception to\n the first. Feedback is a response to something the reader did. A cycle is a\n demonstration of something the system does, and no reader is waiting for it.\n The test for which one a motion is does not require taste: stop the animation\n and ask whether the figure is still true and still legible.\n \n **New Components**\n \n - `PulseGraph` draws a running system: named nodes, the relations between them, a\n marker travelling a rail, and a breathing focus. A node's `lane` says it is a\n stage in a sequence, which draws the rail; a node with no lane is a field.\n `carries` on a relation says the line is carrying something, and draws a head\n at its end so the claim survives motion being off.\n - `PulseSeries` draws a live instrument: columns rising out of a caller-named\n baseline, with a reticle crossing them once per cycle. The values are the\n caller's own and the tallest sets the scale.\n - `SignalField` is a field of marks: the atmosphere a figure is drawn over.\n Decorative by default, still unless asked to drift, and honestly named as the\n one component in the system that is texture.\n \n **`Hero01` gains the band it was missing**\n \n All four sites hand-wrote the same two-column grid, each with its own gap and its\n own breakpoint. It is now the `instrument` slot, and an action naming an `href`\n renders as a real link rather than a button that goes nowhere. The centred form\n is unchanged, so this is additive.\n \n **New tokens**\n \n `ambient` and `ambient-ease` are two closed groups measured in seconds, separate\n from `duration` because folding them in would have made one name mean both 280ms\n and 7.2s. The emitted contract asserts the closed set and fails any cycle under\n one second, because a sub-second cycle is a flicker and no value in the 80 to\n 280ms band could ever have caught that.\n \n **What this does not change**\n \n Nothing is hidden. No ambient rule sets `opacity: 0` and nothing waits for a\n script, an intersection or a timer, so every figure is complete at first paint\n and a consumer needs no exception to enable the `hidden-state` gate. Reduced\n motion is one `animation: none`, and because every resting state is the full\n form, that reader gets the same figure, still. All three are server Components,\n so this adds zero bytes of client JavaScript.\n\n## 0.6.0\n\n### Minor Changes\n\n- 4f9ce77: A server-rendered pack boundary can now be mode-correct\n \n The only dark selector for a pack was `[data-pack=\"<id>\"].dark`, which requires\n the mode class on the same element. A server cannot know the reader's mode, so a\n subtree carrying a second pack rendered in that pack's light values on a dark\n page. The token build now emits the two-member list\n \n ```\n [data-pack=\"<id>\"].dark, .dark [data-pack=\"<id>\"]\n ```\n \n as one rule with one declaration block, for all five packs. The compound half is\n retained, because it is what a dark surface on a light page is and it is a\n capability the package already published.\n \n Consumers get a second change alongside it. `themeAttributes` now takes `mode` as\n optional, because a required one is that same impossibility written into the type:\n the only way to ask for a boundary with no mode class of its own was to pass\n `'light'` as a guess, and the guess is wrong for half of readers. Omitting `mode`\n is the honest spelling of \"wear the mode of the element carrying `.dark`\". The\n change is a widening, so every existing caller still compiles.\n \n The theme-boundary law in `DESIGN.md` is rewritten to match. It previously\n instructed an implementer to put `data-pack` and `class=\"dark\"` on a boundary\n together, which works for a client-applied boundary and cannot work for a\n server-rendered one.\n- 5dcb347: Every colour role now has a contrast row or a stated reason, and there is a brand ink\n \n The contrast gate decided what to measure by a naming convention: any root token ending in\n `-foreground` had to be the first element of a pair. Six of the seven roles that were in no row at\n all could not be reported by that rule, because `chart-1` through `chart-5` and `sidebar-border`\n do not end in `-foreground`, and `sidebar-ring` does not either. A new role therefore defaulted to\n silence. The gate now walks the token source instead of the table, so a role nobody has written\n down is a failure rather than an absence, and the role set is read from `src/semantic/` rather than\n from the pair table, or the walk would be checking the table against itself.\n \n `sidebar-ring` is what the walk immediately found. It had no row at all, and its shipped values\n measured 2.42:1 in the base pack's light mode, 1.73:1 in its dark mode, 2.86:1 in Mint and 2.97:1\n in Sky against the sidebar surface: a focus indicator four of twelve pack and mode combinations\n could not be seen against, reported as compliant because it was not measured. The values moved, not\n the row. The base pack goes from neutral 400 and neutral 700 to neutral 500 in both modes, because\n the base pack's ring is one ramp rather than two; the pastels go from brand 500 to brand 600 in\n light, because the sidebar is neutral 50 rather than neutral 0 and brand 500 is one step short of\n 3:1 in two packs. The dark pastels keep brand 300 at 8.06:1 or better. `sidebar-border` gains an\n advisory row on the sidebar surface with the same judgement `border` and `input` already carry,\n which is how its 1.18:1 to 1.37:1 became a number on the record rather than an unmeasured value.\n \n `chart-1` through `chart-5` are exempted with a reason rather than measured against a ground. A\n series is a graphic object: shadcn consumes the five as the stroke and fill of a series and reads\n its tooltip and legend text from `foreground` and `muted-foreground`, which are each gated against\n their own ground. What binds a five-way set is that no two series resolve to the same value, so\n that is what the gate asserts instead, in every pack and both modes. The measured tightest pair is\n 37/255 in a channel, Peach in dark mode between `chart-1` and `chart-5`. No minimum separation is\n asserted, because a threshold with no standard behind it is a number that gets bent. `radius` is\n exempted as a length rather than a colour, and the exclusion is a declared entry with a reason\n instead of a name filter buried in a loop. Every exemption is printed on every run with the roles\n that share it.\n \n **`brand-ink` is a new semantic role: a brand hue read as text.** Three sites want a brand colour\n they can read on a surface and `primary` cannot serve it, because `primary` is a fill: a pastel\n brand 400 measures 2.20:1 to 2.44:1 on its own pack's page and 1.90:1 to 2.05:1 on the accent\n surface. It is brand 700 in light and brand 200 in dark, in all five packs and the base, and it is\n gated at 4.5:1 against three grounds rather than one: the page, a card, and the accent surface.\n The accent is the ground that decides the step in both directions, because a light ink has to\n survive the pack's own brand 100 and a dark ink has to survive brand 800, and brand 600 and brand\n 400 respectively are the steps that fail each. The measured worst case across all six sources, both\n modes and all three grounds is 5.57:1 in Mint's light mode. The dark step is brand 200 rather than\n the brand 300 the accent ground would have permitted, because brand 300 is `primary` in every\n pack's dark mode and a brand ink that is the brand fill is the confusion the role exists to end. A\n test asserts that `brand-ink` never resolves to the same value as `primary`, `primary-foreground`\n or `foreground` in any source or mode.\n \n **The two values that were already in the contract are the two answers this gate rules out, and\n both are written into `DESIGN.md` as prohibitions with a number attached.** `primary-foreground` is\n the pack's brand 950 step, in both modes and all five packs, published under three further names in\n light mode, and it is the value a reader of the contract reaches for when they want the brand colour\n as text. It clears its own row by 7.39:1 to 11.06:1 and it measures 1.00:1 to 1.82:1 on the dark\n page ground, the dark card and the dark accent surface. In light mode it reads 15.05:1 to 18.05:1\n on the page, so half the modes it looks right and half it disappears, and no row in the contract\n could see the difference. `ring` is the other: the pack's brand hue as a non-text boundary, gated\n at 3:1 against the page ground alone, and it measures 2.66:1 to 4.35:1 on the accent surface in all\n six light-mode sources. Neither was wrong arithmetically. Both are invisible on the ground a brand\n ink lands on, and both now have a role that is measured there.\n \n **A role is measured in both modes or the run fails.** Every colour role, exempt ones included, has\n to resolve to an sRGB value in light and in dark in every pack, so an exemption buys a role freedom\n from a ground and never from a mode. This is what a role present in one mode and absent from the\n other looks like, and the build's own key-set guard does not reach it because it compares light\n against light.\n \n The old `-foreground` name test is gone rather than kept beside the walk. It was never a second\n opinion on coverage, which the walk settles, and its only additional content is that a role named\n `*-foreground` must be the foreground of a row rather than the background of one, which is a claim\n about the shadcn naming convention and belongs to the Contract Rule.\n \n No consumer action is required. `brand-ink` is additive: it is a new custom property, the stylesheet\n gains a `text-brand-ink` utility, and no existing token changed except `sidebar-ring`, whose value\n moved in the base pack and in the light mode of every pastel. A consumer that styled its own sidebar\n focus ring from `--sidebar-ring` will see a more visible ring, which is the point.\n\n## 0.5.1\n\n### Patch Changes\n\n- 2fc6a0a: Retune `muted-foreground` so muted text clears 4.5:1 on the muted surface\n \n The base pack's `muted-foreground` moves from neutral 500 to neutral 600. It\n cleared 4.5:1 on the page background but fell to 4.34:1 on `muted`, where the\n pill, avatar fallback, kbd and tab-list pattern place it. The contrast gate now\n also checks `muted-foreground` on `muted`. The pastel packs were already pinned\n to their neutral 700 and are unchanged.\n\n## 0.5.0\n\nPrism was rebuilt from a fresh repository on a clean break. The token pipeline is\nDTCG 2025.10 compiled with Style Dictionary, the semantic names keep the shadcn\ncontract verbatim, and motion, typography, spacing, shadows, breakpoints and\ncontainers all reach CSS through a single `@theme static` block.\n\nChangesets are appended above this entry for every published change.\n",
|
|
8017
|
+
"markdown": "# @nanisoft/prism-tokens\n\n## 1.5.0\n\n### Minor Changes\n\n- Add the `aurora` ambient cycle at 24000ms\n \n The new member measures light crossing a ground: the washes a hero stands on.\n It rides `ambient-ease-drift`, holds to the one-second floor with the rest of\n the group, and is admitted into the closed set the emitted-contract gate holds.\n\n## 1.2.0\n\n### Minor Changes\n\n- Two new packs: Cyan and Gold\n \n The family's product map asked for two hues the palette did not carry, and a\n product's ground is a fact about the system rather than about the site that\n wears it, so the hues arrive here instead of as consumer overrides.\n \n `cyan` is the Nexus pack: pastel cyan-blue, generated at hue 215 in OKLCH with\n the same perceptually even lightness ramp as the other five, at the crispest\n radius in the set (0.5rem) beside Sky. `gold` is the AlphaLens pack: pastel\n amber-gold at hue 80, at the roomiest radius (1rem) beside Peach. Both carry\n the full shadcn-compatible contract, tinted neutrals at the family's chroma,\n and pass the contrast gate at 38/38 required pairs in both modes.\n \n `PACKS` in the theming module and `STORE_PACKS` in the llms corpus carry the\n two new ids, so a consumer's ground-pack validation and every agent reading\n the corpus see the same vocabulary. Peach returns to reserved. The published\n pack count is now seven pastels plus the neutral default.\n\n## 0.16.0\n\n### Minor Changes\n\n- 1a0ba35: A page `h1` is now visibly larger than the `h2` sections under it\n \n A page `h1` rendered at 1.875rem and every section heading under it at 1.5rem, so\n the two were one fifth apart and the page had no top to its hierarchy. The fix is\n in the type scale rather than in the Component, because the scale stopped at `4xl`\n and there was no step for a page's own claim to take.\n \n **Two steps are authored, and they are the two widest gaps in the group.**\n `5xl` is 3rem and `6xl` is 3.75rem, both at a line-height of 1. Every other step\n in the `text` group is a whole multiple of 0.125rem and these two are too;\n `4xl` to `5xl` is four thirds, the first gap in the group wider than any gap it\n already had, and `5xl` to `6xl` is five fourths. They are authored as a pair\n rather than as one step because every rung of the heading ladder is a pair, a base\n step and one step up at `sm`, so a single step above `4xl` would have made the\n page heading the one heading in the system that does not grow at a width.\n \n Both steps reach CSS and the published pack contract the way every other authored\n step does, so `text-5xl` and `text-6xl` are utilities a consumer can compose, and\n neither is a value a consumer has to write.\n \n **The heading ladder now descends from the new ceiling, one authored step per\n level, and every level holds a step of its own.** There are exactly six authored\n steps at or above Body, so the table lands on the floor with a distinct step at\n every rung, where before `h4` through `h6` shared one. `h1` is at `5xl`, rising to\n `6xl` at `sm`; `h2` is at `4xl`, rising to `5xl`; and the table descends to `lg` at\n `h6`, which is the step Body is set at. **What a consumer sees is every section\n title moving up one step and every page heading moving up two**: a section title\n goes from 1.5rem to 2.25rem and from 1.875rem to 3rem at `sm`, and a page `h1` goes\n from 1.875rem to 3rem and from 2.25rem to 3.75rem at `sm`. No call site changes,\n because the level a Block was already passing is still what decides its size.\n \n **Five surfaces that render a page's own `h1` now ask the same table for its size\n rather than naming a step.** `page-header-01`, `docs-shell`, `blog-post-page`,\n `not-found-page` and `error-page` each wrote a fixed step into a heading that was\n meant to be a page's top-level claim, so with the ceiling lifted they would have\n rendered a page heading a rung or two below the section titles under it. They ask\n `headingSizeClass`, which is what `SectionHeading` and `Cta01` already asked, and\n `Cta01` still carries no literal.\n \n **Four more surfaces that resolved their own heading size, found the same way.**\n A Block that takes a `headingLevel` and writes the size beside it forwards the level\n and changes nothing a reader can see, which is the defect above at the scale of one\n Block. `data-table-01` said `text-lg`, `run-console-01` said `text-base`, and both\n moved the outline without moving the size. `run-stream-01` renders a literal `h2`\n at `text-lg`, which was the floor of the old scale, so a live region's `h2` came\n out below the `h3` introducing the section holding it. `/foundation/themes` said\n `text-3xl`, which had been Display and is now the `h3` step, so that page's `h1`\n rendered a rung below the section titles under it. All four ask `headingSizeClass`\n now, and the three Blocks carry `text-balance` with the size they gained: the ladder\n states weight, tracking and balance as one package and `SectionHeading` carries all\n three at every rung, so a title lifted from 16 or 18 pixels to 24 or 36 is worse\n unwrapped than the one it replaced.\n \n **The Item page's appendix heading is on the article's ladder, and asking the Block\n ladder for it was the wrong correction.** It said `text-xl`, which put an `h2` at 20\n pixels under the prose `h2` at 30 and below the prose `h3` at 24 above it. Asking\n `headingSizeClass` removed the third tier and created a worse one: \"API reference\"\n is a section of an article exactly as \"Overview\" is, so it takes the article's\n ladder, and the Block ladder is a step and a half above it at `sm`. The Item page\n then carried three sections at 30 pixels and an appendix at 48, and the appendix was\n the second largest thing on the page. It is now a bare `<h2>` inside the same\n `.prose` wrapper as the Item's own MDX body, so the article's rules size it at 30\n pixels and it carries no class of its own: every section heading in that article is\n a bare tag the article styles, and a heading naming its own step would be the second\n answer to a question the article has already answered. The table stays outside\n `.prose`, whose descendant rules would put its cells at the body step in the muted\n colour, and the gap above it is the article's own 16 pixels rather than a second\n region's 32.\n \n **The five theme names are card titles, and they compose `CardTitle` to say so.**\n They carried `font-medium` and no size, so they rendered at 16 pixels and 500 under\n a 60 pixel `h1`. Asking the ladder for the size would have been wrong: five themes at\n the `h2` step is five 48 pixel headings over five cards on a page whose own claim is\n the `h1` above them. `CardTitle` already holds the answer the library needs: a card\n title sits at body size, and the element and the visual step are separate questions.\n So the size stands, the tag stands at `h2` because the grid draws no section heading\n of its own, and the weight now comes from the one Component that owns it rather than\n from a literal. What a consumer sees is five theme names one step heavier.\n \n **The two Foundation readers took opposite answers, and the reason is worth more\n than either change.** Both render inside an MDX body, which the site wraps in\n `.prose`, and both used to carry literals, which a utility layer lets win over the\n article's own component-layer rule: `/foundation/colors` published an `h2` at 18\n pixels and an `h3` at 14, below the `h3` above them.\n \n `ColorTokens`' four headings are sections. Two divisions of an article and a\n partition under one of them is a structure, not a list of labels, so they name no\n step and the article decides, which is the one copy of the ladder there is.\n \n `TokenBrowser`'s are not sections, and dropping its literals let the wrong authority\n decide them: every group name came out at the prose `h2`, 30 pixels, which is more\n than twice the 14 pixels the navigation rail's own section titles are set at, on the\n page whose whole job is being navigated. Each of those 184 names is a table's name\n and nothing else, so they are captions: `h3` at `text-sm`, in the mono face because a\n token key is machine notation, at the same 14 pixels and 500 as the column heads of\n the table underneath. That is the caption `ApiTable` already renders for a compound\n export's part names on every Item page, so this is the repository's own treatment\n rather than a third one. One heading on the page, \"Semantic contract\", is a division\n of the article rather than a label, and it keeps naming no step.\n \n **`Prose` walks the same ladder from one rung down, and a prose heading therefore\n sits below a Block heading at the same level.** Its child treatments now set `h1`\n at `4xl` and descend one authored step per level to `lg` at `h5` and `h6`,\n which is the same scale and the same floor entered at the rung that fits the\n reading measure: Display is 3rem, and a 42rem column is fourteen of those. The new\n `h5` and `h6` treatments are new, so a document that reached past `h4` before now\n has an answer rather than inheriting nothing. **The consequence is stated rather\n than left to be found.** A Block-composed `h2` is 2.25rem and 3rem above `sm`; a\n prose `h2` is 1.875rem and does not grow. They are 1.2 apart at the base width and\n 1.6 apart above `sm`, where they were 1.25 apart before this change, so a consumer\n who sets a `Prose` section beside a Block section sees two tiers rather than one.\n `DESIGN.md` under Typography, Hierarchy now says so, and says which to reach for.\n `DocsShell` runs `Prose` at `fullWidth`, so the frame decides the width: 35rem of\n article track at a viewport of 1216px or wider, and less below that, which makes\n the case for entering one rung down stronger there rather than weaker.\n \n **`Heading`'s free `size` prop is unchanged, and this is stated rather than\n implied.** `size` is a choice made beside the tag, for a heading the surrounding\n document has already placed and wants attuned, and its largest arm is a step of\n the scale rather than a rung of the ladder. Its two largest arms were not removed\n and no new ones were added, so no consumer's call site changes and there is still\n no second route to a page `h1` from inside the library. `search-page` and\n `status-page` are the two surfaces that take a heading's *tag* from\n `childLevel(headingLevel)` and its size from this prop, and they are left as they\n are: those are labels over a list of results and over a list of incidents, and a\n caption is not a rung.\n \n **`scripts/check-heading-scale.mjs` now holds three things it described but did not\n check.** Its rule 4 claimed a table that jumps is a finding and only tested for a\n table that rises, so a table that skipped a rung passed it: descending, reaching\n the ceiling and flooring at or above Body, and rendering the page `h1` one gap\n above its own `h2`, which is the defect these steps exist to remove. And the\n literal Display step it refused on `Cta01` was written out as `text-3xl` and\n `sm:text-4xl`, which stopped being Display the moment the ceiling moved. Both are\n now read from the authored scale and the shipped table rather than restated, and\n both have a staged fixture in `scripts/__tests__/heading-scale.test.mjs`. The third\n is the Item rule below, which is new rather than a restatement.\n \n **The Item document's own table was the retired ladder, and it now states this\n one.** `apps/site/items/component/layout/section/section.mdx` is the page a reader\n is on when they ask what `as` resolves to, and it listed `h1` at `text-3xl` and\n `h4` through `h6` all at `text-lg`: the table as it was before this change. It now\n states the ladder the Component renders, and the sentence under it about where the\n scale stops has been corrected with it, because a corrected table beside a stale\n paragraph is the same disagreement one row down.\n \n **The gate also reads the Item document now, and that is the third copy of the\n table.** `apps/site/items/component/layout/section/section.mdx` states the ladder\n in its prose, and neither the declaration build nor the corpus reaches it, so when\n the ceiling moved it kept publishing the retired table. The new rule compares it to\n the same `HEADING_SIZE` the JSDoc is compared to, by the same cell-for-cell shape,\n so it names no step of its own; it reads the whole Item rather than a slice, so a\n second table in that prose is a finding; and it fails an Item that states no table\n at all, because a comparison with no rows finds nothing to disagree with. Three\n staged fixtures cover the three cases.\n \n **What the gate still does not hold is written down rather than left to be\n discovered.** `Cta01` is named as a root because it is the one Block whose heading\n cannot be composed from `SectionHeading` at all, so a literal step on it has no\n table to fall back to. Beyond that file the gate cannot go, and the reason is\n stated rather than left to be found: whether a heading a Block renders is a rung of\n the ladder or a caption inside that Block is not decidable from source.\n Forty-eight headings across `packages/ui/src` and `apps/site/src` name a step of\n their own, the eleven rungs among them now ask the table, and the thirty-seven that\n remain are captions. Two surfaces bind a caption to `headingLevel` rather than to\n `childLevel`, so a rule reading the tag cannot sort them, and a root list holding\n the rungs would be a list somebody maintains. That is how the `Cta01`-only rule\n came to hold the one Block already fixed and pass over the four that were not, and\n the count is written into `docs/quality-gates.md` so the next reader starts from it.\n `TokenBrowser` adds one more to that population, which is the point of naming a\n caption's step rather than inheriting it: the scan can see it, and it is below the\n floor by arithmetic.\n \n **The limit of the scan that took that count is now written down too, and it is the\n more useful half of it.** The count came from looking for a literal size utility\n beside a heading tag, and a heading whose size it inherits names nothing: the five\n theme names above carried no size at all, and two scans of exactly that shape passed\n them at 16 pixels and 500. A size that is inherited is a size nobody chose, and \"no\n literal\" is invisible to a scan that looks for literals, so that count has never\n been a count of every heading that names a step, and the number nobody chose is the\n half of it that reached a reader. What the count is good for is the arithmetic: a\n heading it reports is a rung if the step it names is at or above the step Body is set\n at, and a caption if it is below, so the two sort without anybody deciding which is\n which. The remedy for what the scan cannot see is on the surfaces rather than in the\n gate, and it is the one the token inventory now uses: a caption names its own step,\n so it is visible to the scan and it is below the floor by definition.\n \n The bump is a minor in both packages rather than a patch because a new authored\n step is a new public contract: `--text-5xl` and `--text-6xl` with their\n `--line-height` companions enter the emitted CSS and the published pack contract,\n and every heading on every page composed from a Block moves.\n\n## 0.15.0\n\n### Minor Changes\n\n- 6dc7bb8: There is one container namespace, and `max-w-6xl` no longer resolves\n \n **If you wrote `max-w-6xl`, `max-w-2xl`, `max-w-xl`, `max-w-lg`, `max-w-md`,\n `max-w-sm` or `max-w-5xl` against this package, that class now produces no rule at\n all.** Nothing errors and nothing warns: the element simply loses its cap and lays\n out at whatever its parent gives it. Grep your own repository for `max-w-` before\n you upgrade. The replacements are in the list below and every one of them resolves.\n \n The reason is the defect this release ends. `@nanisoft/prism-tokens` authors three\n container widths and Tailwind ships thirteen of its own, and until now both shipped\n in the same stylesheet. Three of Tailwind's were numerically identical to three of\n ours: its largest at 72rem beside `--container-page`, its fifth at 42rem beside\n `--container-measure`, its fourth at 36rem beside `--container-measure-narrow`.\n Nothing rendered differently, so nothing failed and every gate was green. What it\n cost was that one width had two names, and the first retune of an authored token\n would have moved every surface reaching it by one spelling and left every surface\n reaching it by the other exactly where it was. `Section`, `SiteHeader`,\n `SiteFooter`, `ChartCard01`, `DocsShell` and `QuickView01` all used the Tailwind\n spelling of the page column; `Cta01` and `SectionHeading` used the Tailwind\n spellings of the two measures.\n \n The token build now closes the whole namespace, with\n `--container-*: initial` ahead of the authored entries rather than a list of the\n seven steps that happened to be emitted. A wildcard is the load-bearing word: the\n thirteen names it retires are `3xs 2xs xs sm md lg xl 2xl 3xl 4xl 5xl 6xl 7xl`,\n and a list would have been a second list to keep in step with a dependency's\n default theme, which is the event the line exists to survive. Order matters and is\n asserted: Tailwind resolves a theme in source order, and the same declaration\n written after the authored entries clears all eight of ours and ships no container\n at all.\n \n **What to write instead**\n \n | You wrote | Write | Value |\n | --- | --- | --- |\n | `max-w-6xl` | `max-w-page` | 72rem |\n | `max-w-2xl` | `max-w-measure` | 42rem |\n | `max-w-xl` | `max-w-measure-narrow` | 36rem |\n | `max-w-md` on a dialog or an alert | `max-w-overlay-dialog` | 28rem |\n | `max-w-lg` on a form dialog | `max-w-overlay-form` | 32rem |\n | `max-w-xl` on a command palette or a search | `max-w-overlay-palette` | 36rem |\n | `max-w-sm` on a side sheet or a drawer | `max-w-overlay-panel` | 24rem |\n | `max-w-5xl` on a lightbox | `max-w-overlay-media` | 64rem |\n \n **An overlay's width is a property of the kind of surface it is, not of the page\n grid underneath it**, and that is why the five overlay widths are authored rather\n than carried on a Tailwind name. A retune of the reading measure must not move a\n dialog and a retune of the page column must not move either, so the two families\n are named apart inside the one container group. They share a group because\n Tailwind 4's `max-w-*` resolves `--spacing-*` and `--container-*` and nothing\n else, so a maximum width that is to be a token has to live in one of those two\n namespaces or it is a value written in a Component. `overlay-palette` and\n `measure-narrow` are both 36rem; that is two measurements that happen to agree and\n not one measurement with two names, and the token source says so and the emitted\n contract holds it to saying so.\n \n **Nothing is supposed to change where you can see it.** Every migration above\n preserves the exact rendered width, so a page that only consumed this stylesheet\n looks the same before and after. What changes is the next retune: it now moves one\n surface rather than half of them.\n \n **A width that was arithmetic on the spacing base is left alone, deliberately.**\n `max-w-96`, `max-w-72`, `max-w-64`, `max-w-32` and `max-w-20` resolve to\n `calc(var(--spacing) * N)` and never touched the container namespace at all. They\n are how a chart's axis band, a token table's column and a documentation frame are\n sized, where nobody took a decision and naming one would invent it. They stay.\n \n **Two gates now hold this down, because the question has two sides.**\n `scripts/check-elevation-layout.mjs` reads every `w-*` and `max-w-*` name out of\n the class strings and holds it to the container group read out of the token source,\n so a width nobody authored is a finding with a file and a line; it reads\n `apps/site/items` as well as `apps/site/src` and `packages/ui/src`, because that is\n where the documentation Demos live and where the site's own build scans.\n `packages/ui/scripts/check-container-namespace.mjs` reads the built stylesheet and\n holds the artefact: that the theme block declares exactly the authored containers,\n that none of Tailwind's thirteen steps is declared or read, that the close is\n written ahead of the entries rather than after them, and that every container width\n this package writes is emitted as a utility reading its own variable. It reads the\n step list out of the installed `tailwindcss/theme.css` rather than restating it, so\n a dependency that adds a step is covered without an edit. Tailwind's extractor\n reads this package's comments as well as its code, so a retired class name written\n down in a JSDoc block used to emit a utility; there is now a rule about that too.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** A class a consumer wrote stops producing a rule with no\n error, which is breaking by any ordinary reading, and this entry says so plainly.\n `major` in this repository publishes `1.0.0`, and every release to date has been\n `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent slot.\n Publishing 1.0.0 with this change in it would assert an API stability guarantee\n this library has not earned, and it would be the first release in the project's\n history to use the bump at all. The break is real and is the subject of this\n entry; the number it lands on is the line's decision and the line is pre-1.0.\n \n **One collateral repair on the documentation site, recorded here because it is\n why `globals.css` moved.** The site's own build and this package's meet in one\n `utilities` layer, and this package emits a bare `.grid-cols-6` as the base of the\n `lg:` variant four of its Blocks use. That bare rule landed after the site's\n `sm:grid-cols-11`, so every colour ramp on the Foundations page was drawing six\n columns at every width above 640 against a comment beside the class saying\n eleven. Whether `check-utility-cascade.mjs` saw it depended on which order two\n content-hashed CSS chunks sorted in, which this change perturbed and which nothing\n about the tree controls. Neither side is wrong, so the site's variant is restated\n in the `site-variants` layer that already exists for six other collisions. The\n repaired behaviour is on the documentation site only; nothing in this package\n changes.\n- 6dc7bb8: Ship the interface face and its metric-adjusted fallback, and stop the site loading its own\n \n `@nanisoft/prism-ui` shipped three Inter weights and no fallback face, so a reader\n waiting on the web font saw the platform's UI face jump into place when Inter\n arrived. The stylesheet now also ships a local Arial carrying Inter's metrics\n through `size-adjust`, `ascent-override`, `descent-override` and\n `line-gap-override`, and `--font-sans` names it immediately after Inter, so the\n swap window occupies Inter's own line box. The four numbers are measured off the\n shipped face rather than chosen, and they are the ones `next/font` measured for the\n same typeface, so the line box is unchanged from what the documentation site had.\n \n A fourth face ships beside them: Inter italic at weight 400, the Latin subset of\n the same release, covering the same 230 codepoints as its upright siblings.\n `Prose` sets `blockquote` in italic and the surface had been asking for a style no\n shipped file provided, so every consumer has been reading a synthesized oblique.\n One real face covers every weight a browser asks for, because the font matcher\n takes the closest available weight rather than synthesizing once a face exists.\n The whole set is 93.5 KB, up 22.8 KB, and no consumer has to do anything.\n \n **The documentation site no longer loads a face of its own.** It shipped a 723 KiB\n unsubsetted variable Inter and its italic through `next/font/local`, preloaded both\n in all 581 exported documents, and applied the result to `<body>` as a directly set\n `--font-sans`. A directly applied custom property outranks an inherited one, so the\n library's faces were never fetched on the site that documents them: a missing or\n corrupt shipped font rendered perfectly there with every gate green. The site now\n resolves the same stack you do, which is the only way it can be evidence for the\n package. If you were relying on the site to look right while your own copy of Inter\n was broken, that is now a failure you can see.\n\n## 0.10.0\n\n### Minor Changes\n\n- 75434f8: Add a running figure to the system, and the ambient cycle scale that prices it\n \n The system had one law of motion: motion is state feedback, on an 80/160/280\n millisecond scale, and there are no keyframes anywhere. That law was right and\n it held. It also meant a product page could say what its product does and show\n nothing about it, which is what happened to four sites at once.\n \n This adds the second law, and it is a separate law rather than an exception to\n the first. Feedback is a response to something the reader did. A cycle is a\n demonstration of something the system does, and no reader is waiting for it.\n The test for which one a motion is does not require taste: stop the animation\n and ask whether the figure is still true and still legible.\n \n **New Components**\n \n - `PulseGraph` draws a running system: named nodes, the relations between them, a\n marker travelling a rail, and a breathing focus. A node's `lane` says it is a\n stage in a sequence, which draws the rail; a node with no lane is a field.\n `carries` on a relation says the line is carrying something, and draws a head\n at its end so the claim survives motion being off.\n - `PulseSeries` draws a live instrument: columns rising out of a caller-named\n baseline, with a reticle crossing them once per cycle. The values are the\n caller's own and the tallest sets the scale.\n - `SignalField` is a field of marks: the atmosphere a figure is drawn over.\n Decorative by default, still unless asked to drift, and honestly named as the\n one component in the system that is texture.\n \n **`Hero01` gains the band it was missing**\n \n All four sites hand-wrote the same two-column grid, each with its own gap and its\n own breakpoint. It is now the `instrument` slot, and an action naming an `href`\n renders as a real link rather than a button that goes nowhere. The centred form\n is unchanged, so this is additive.\n \n **New tokens**\n \n `ambient` and `ambient-ease` are two closed groups measured in seconds, separate\n from `duration` because folding them in would have made one name mean both 280ms\n and 7.2s. The emitted contract asserts the closed set and fails any cycle under\n one second, because a sub-second cycle is a flicker and no value in the 80 to\n 280ms band could ever have caught that.\n \n **What this does not change**\n \n Nothing is hidden. No ambient rule sets `opacity: 0` and nothing waits for a\n script, an intersection or a timer, so every figure is complete at first paint\n and a consumer needs no exception to enable the `hidden-state` gate. Reduced\n motion is one `animation: none`, and because every resting state is the full\n form, that reader gets the same figure, still. All three are server Components,\n so this adds zero bytes of client JavaScript.\n\n## 0.6.0\n\n### Minor Changes\n\n- 4f9ce77: A server-rendered pack boundary can now be mode-correct\n \n The only dark selector for a pack was `[data-pack=\"<id>\"].dark`, which requires\n the mode class on the same element. A server cannot know the reader's mode, so a\n subtree carrying a second pack rendered in that pack's light values on a dark\n page. The token build now emits the two-member list\n \n ```\n [data-pack=\"<id>\"].dark, .dark [data-pack=\"<id>\"]\n ```\n \n as one rule with one declaration block, for all five packs. The compound half is\n retained, because it is what a dark surface on a light page is and it is a\n capability the package already published.\n \n Consumers get a second change alongside it. `themeAttributes` now takes `mode` as\n optional, because a required one is that same impossibility written into the type:\n the only way to ask for a boundary with no mode class of its own was to pass\n `'light'` as a guess, and the guess is wrong for half of readers. Omitting `mode`\n is the honest spelling of \"wear the mode of the element carrying `.dark`\". The\n change is a widening, so every existing caller still compiles.\n \n The theme-boundary law in `DESIGN.md` is rewritten to match. It previously\n instructed an implementer to put `data-pack` and `class=\"dark\"` on a boundary\n together, which works for a client-applied boundary and cannot work for a\n server-rendered one.\n- 5dcb347: Every colour role now has a contrast row or a stated reason, and there is a brand ink\n \n The contrast gate decided what to measure by a naming convention: any root token ending in\n `-foreground` had to be the first element of a pair. Six of the seven roles that were in no row at\n all could not be reported by that rule, because `chart-1` through `chart-5` and `sidebar-border`\n do not end in `-foreground`, and `sidebar-ring` does not either. A new role therefore defaulted to\n silence. The gate now walks the token source instead of the table, so a role nobody has written\n down is a failure rather than an absence, and the role set is read from `src/semantic/` rather than\n from the pair table, or the walk would be checking the table against itself.\n \n `sidebar-ring` is what the walk immediately found. It had no row at all, and its shipped values\n measured 2.42:1 in the base pack's light mode, 1.73:1 in its dark mode, 2.86:1 in Mint and 2.97:1\n in Sky against the sidebar surface: a focus indicator four of twelve pack and mode combinations\n could not be seen against, reported as compliant because it was not measured. The values moved, not\n the row. The base pack goes from neutral 400 and neutral 700 to neutral 500 in both modes, because\n the base pack's ring is one ramp rather than two; the pastels go from brand 500 to brand 600 in\n light, because the sidebar is neutral 50 rather than neutral 0 and brand 500 is one step short of\n 3:1 in two packs. The dark pastels keep brand 300 at 8.06:1 or better. `sidebar-border` gains an\n advisory row on the sidebar surface with the same judgement `border` and `input` already carry,\n which is how its 1.18:1 to 1.37:1 became a number on the record rather than an unmeasured value.\n \n `chart-1` through `chart-5` are exempted with a reason rather than measured against a ground. A\n series is a graphic object: shadcn consumes the five as the stroke and fill of a series and reads\n its tooltip and legend text from `foreground` and `muted-foreground`, which are each gated against\n their own ground. What binds a five-way set is that no two series resolve to the same value, so\n that is what the gate asserts instead, in every pack and both modes. The measured tightest pair is\n 37/255 in a channel, Peach in dark mode between `chart-1` and `chart-5`. No minimum separation is\n asserted, because a threshold with no standard behind it is a number that gets bent. `radius` is\n exempted as a length rather than a colour, and the exclusion is a declared entry with a reason\n instead of a name filter buried in a loop. Every exemption is printed on every run with the roles\n that share it.\n \n **`brand-ink` is a new semantic role: a brand hue read as text.** Three sites want a brand colour\n they can read on a surface and `primary` cannot serve it, because `primary` is a fill: a pastel\n brand 400 measures 2.20:1 to 2.44:1 on its own pack's page and 1.90:1 to 2.05:1 on the accent\n surface. It is brand 700 in light and brand 200 in dark, in all five packs and the base, and it is\n gated at 4.5:1 against three grounds rather than one: the page, a card, and the accent surface.\n The accent is the ground that decides the step in both directions, because a light ink has to\n survive the pack's own brand 100 and a dark ink has to survive brand 800, and brand 600 and brand\n 400 respectively are the steps that fail each. The measured worst case across all six sources, both\n modes and all three grounds is 5.57:1 in Mint's light mode. The dark step is brand 200 rather than\n the brand 300 the accent ground would have permitted, because brand 300 is `primary` in every\n pack's dark mode and a brand ink that is the brand fill is the confusion the role exists to end. A\n test asserts that `brand-ink` never resolves to the same value as `primary`, `primary-foreground`\n or `foreground` in any source or mode.\n \n **The two values that were already in the contract are the two answers this gate rules out, and\n both are written into `DESIGN.md` as prohibitions with a number attached.** `primary-foreground` is\n the pack's brand 950 step, in both modes and all five packs, published under three further names in\n light mode, and it is the value a reader of the contract reaches for when they want the brand colour\n as text. It clears its own row by 7.39:1 to 11.06:1 and it measures 1.00:1 to 1.82:1 on the dark\n page ground, the dark card and the dark accent surface. In light mode it reads 15.05:1 to 18.05:1\n on the page, so half the modes it looks right and half it disappears, and no row in the contract\n could see the difference. `ring` is the other: the pack's brand hue as a non-text boundary, gated\n at 3:1 against the page ground alone, and it measures 2.66:1 to 4.35:1 on the accent surface in all\n six light-mode sources. Neither was wrong arithmetically. Both are invisible on the ground a brand\n ink lands on, and both now have a role that is measured there.\n \n **A role is measured in both modes or the run fails.** Every colour role, exempt ones included, has\n to resolve to an sRGB value in light and in dark in every pack, so an exemption buys a role freedom\n from a ground and never from a mode. This is what a role present in one mode and absent from the\n other looks like, and the build's own key-set guard does not reach it because it compares light\n against light.\n \n The old `-foreground` name test is gone rather than kept beside the walk. It was never a second\n opinion on coverage, which the walk settles, and its only additional content is that a role named\n `*-foreground` must be the foreground of a row rather than the background of one, which is a claim\n about the shadcn naming convention and belongs to the Contract Rule.\n \n No consumer action is required. `brand-ink` is additive: it is a new custom property, the stylesheet\n gains a `text-brand-ink` utility, and no existing token changed except `sidebar-ring`, whose value\n moved in the base pack and in the light mode of every pastel. A consumer that styled its own sidebar\n focus ring from `--sidebar-ring` will see a more visible ring, which is the point.\n\n## 0.5.1\n\n### Patch Changes\n\n- 2fc6a0a: Retune `muted-foreground` so muted text clears 4.5:1 on the muted surface\n \n The base pack's `muted-foreground` moves from neutral 500 to neutral 600. It\n cleared 4.5:1 on the page background but fell to 4.34:1 on `muted`, where the\n pill, avatar fallback, kbd and tab-list pattern place it. The contrast gate now\n also checks `muted-foreground` on `muted`. The pastel packs were already pinned\n to their neutral 700 and are unchanged.\n\n## 0.5.0\n\nPrism was rebuilt from a fresh repository on a clean break. The token pipeline is\nDTCG 2025.10 compiled with Style Dictionary, the semantic names keep the shadcn\ncontract verbatim, and motion, typography, spacing, shadows, breakpoints and\ncontainers all reach CSS through a single `@theme static` block.\n\nChangesets are appended above this entry for every published change.\n",
|
|
7913
8018
|
"mirror": "/changelogs/prism-tokens.md"
|
|
7914
8019
|
},
|
|
7915
8020
|
{
|
|
@@ -7917,9 +8022,9 @@
|
|
|
7917
8022
|
"slug": "prism-ui",
|
|
7918
8023
|
"section": "changelogs",
|
|
7919
8024
|
"title": "@nanisoft/prism-ui",
|
|
7920
|
-
"description": "1.3.0, 1.2.0, 1.1.0, 1.0.0, 0.16.1, 0.16.0, 0.15.0, 0.14.0, 0.13.0, 0.12.0, 0.11.0, 0.10.2, 0.10.1, 0.10.0, 0.9.0, 0.8.0, 0.7.0, 0.6.0, 0.5.1, 0.5.0",
|
|
8025
|
+
"description": "1.5.0, 1.4.0, 1.3.0, 1.2.0, 1.1.0, 1.0.0, 0.16.1, 0.16.0, 0.15.0, 0.14.0, 0.13.0, 0.12.0, 0.11.0, 0.10.2, 0.10.1, 0.10.0, 0.9.0, 0.8.0, 0.7.0, 0.6.0, 0.5.1, 0.5.0",
|
|
7921
8026
|
"url": "/changelogs/prism-ui",
|
|
7922
|
-
"markdown": "# @nanisoft/prism-ui\n\n## 1.3.0\n\n### Minor Changes\n\n- 250b34e: SiteFooter takes an `about` slot: a word from the site under the mark, for the\n sentence or two a footer carries about what the site is and who publishes it.\n Rendered under the lockup at the measure with no colour of its own, so the\n muted treatment and any headings stay the caller's. The design system's own\n site composes this footer now, and the two labelled facts its hand-written\n footer carried for years ride in that slot.\n- 290642e: Let `SiteNavbar`'s `nav` carry groups, rendered as one trigger that opens a menu\n \n A `nav` entry may now be a group: `{ label, children }` beside the existing\n link shape. On the row a group renders as the row's one control, a trigger\n whose menu lists its children as real anchors, marked with `aria-current` when\n the reader is on one; below the row's threshold the panel lists the same group\n flat, as a label above its links, so no destination is two disclosures deep.\n The current mark now reads the route as a place rather than a string, so a\n reader on `/components/button` finds `Components` marked rather than nothing.\n A flat `nav` renders exactly as before, and the new `SiteNavGroup` and\n `SiteNavEntry` types are exported beside `SiteNavLink`.\n- 8265662: A `ProductMark` takes a `mark`: the product's own drawing, in place of the\n disc. The node is the consumer's and fills the sized slot the disc occupies,\n so one drawing arrives at every drawn size; the name beside it is unchanged\n and still the announced half. `SiteNavbar` and `SiteFooter` carry it through\n the `product` prop they already take, so a site's logo is the sign in the\n bar, in the mobile panel and at the foot of the page from one fact, and the\n two-colour disc remains the mark of a product whose identity is a hue. The\n design system's own site draws its beam-and-refraction in both places now,\n in place of the spectrum disc.\n\n### Patch Changes\n\n- 290642e: Render `Sparkline`'s hidden numbers table without the `Table` Component's scroll container\n \n The readings behind a `Sparkline` are published to assistive technology as a\n table in a visually hidden wrapper. The wrapper is one pixel wide, so the\n horizontal scroll container `Table` wraps its table in always computed\n scrollable and never focusable, which the `scrollable-region-focusable` rule\n fails on wherever a `Sparkline` renders. The hidden table is now plain `table`\n markup with the same caption, row heads and cells: a table nobody can see\n cannot be panned, and the semantics are the whole of what it owes.\n- 290642e: Lay `Calendar`'s month grid as a table again: the `flex` came off its cells\n \n The weekday headings and the day cells carried `display: flex`, and a flexed\n `th` or `td` is pulled out of the table's column layout: the browser wraps each\n one in its own anonymous cell, and the seven-column month collapsed into a\n single stacked column at four times its height. In `Calendar` the day cell is\n the `td` itself, so the whole grid fell; in `MiniCalendar` the day is a button\n inside the cell, so the weekday row alone stacked above a surviving grid, and\n `DatePicker` and the `Calendar01` Block inherit whichever they compose. The\n cells are centred by the table instead (`text-center` with `align-middle` and\n `p-0`), which keeps the seven columns, the 36px cell, the 44px coarse-pointer\n floor and every state exactly as they were designed, and the weekday heading\n now sits over the column it names.\n- 290642e: Align `PulseSeries`'s scan reticle with the columns it reads\n \n The wash behind the reticle stopped short of the baseline while its own line\n crossed it, and the line rode inside the wash at a third of its width rather\n than at its centre. The wash is now one column slot wide and the line runs\n down its centre, so at every instant the wash covers exactly the column the\n line is crossing, and both shapes meet the baseline. The travel is the plot's\n own width, so the reticle enters and leaves aligned with the columns' field\n and its opacity ramps are spent on the margins beside the end columns.\n\n## 1.2.0\n\n### Minor Changes\n\n- Two new packs: Cyan and Gold\n \n The family's product map asked for two hues the palette did not carry, and a\n product's ground is a fact about the system rather than about the site that\n wears it, so the hues arrive here instead of as consumer overrides.\n \n `cyan` is the Nexus pack: pastel cyan-blue, generated at hue 215 in OKLCH with\n the same perceptually even lightness ramp as the other five, at the crispest\n radius in the set (0.5rem) beside Sky. `gold` is the AlphaLens pack: pastel\n amber-gold at hue 80, at the roomiest radius (1rem) beside Peach. Both carry\n the full shadcn-compatible contract, tinted neutrals at the family's chroma,\n and pass the contrast gate at 38/38 required pairs in both modes.\n \n `PACKS` in the theming module and `STORE_PACKS` in the llms corpus carry the\n two new ids, so a consumer's ground-pack validation and every agent reading\n the corpus see the same vocabulary. Peach returns to reserved. The published\n pack count is now seven pastels plus the neutral default.\n\n### Patch Changes\n\n- Updated dependencies\n - @nanisoft/prism-tokens@1.2.0\n\n## 1.1.0\n\n### Minor Changes\n\n- ad69fa2: A message carries the caller's per-message metadata\n \n `ChatMessage` gains an optional `meta: ReactNode`, drawn in the message's header\n beside the sender and the time. It carries the caller's own per-message metadata:\n the model a provider stated for a turn, a token count it reported, and the cost or\n the latency the caller derived. It is a node and not a set of Prism fields because\n no provider read publishes a cost or a latency, so the surface states no model, no\n count, no cost and no latency for itself and draws only what the caller hands it.\n The parts stay the way content is carried. The roster does not change: no new Item,\n Kind, Page or catalogue entry appears.\n- ad69fa2: The date view takes a per-item move control\n \n `Calendar01` exposes a per-item `handle`, a node the caller fills with their own\n control and wires to whatever owns their schedule. It exists for the one\n interaction a date view has that the Block does not own: a reschedule, which is a\n drag with a drop target, a new order, a write and a conflict, every one of them the\n consumer's. The Block places the node at the leading edge of the item's row and\n owns nothing beyond it: no drag, no drop, no order and no move callback, which is\n the arrangement `Kanban01` already takes for a board's drag.\n \n A `CalendarItem` gains an optional `handle: ReactNode`; `Calendar01`'s props are\n otherwise unchanged. The roster does not change: no new Item, Kind, Page or\n catalogue entry appears.\n- ad69fa2: The index gains a picture arrangement and a write grid\n \n Two coverage gaps in the record-index territory are closed by authoring two Blocks\n rather than by widening a union, and each is the arrangement `DESIGN.md` already\n named as forcing a second Item.\n \n **`CardIndex01` is the record index as pictures.** It draws a set of records as a\n grid of cards rather than as rows, with the caller composing each card's picture,\n name and price through `renderCard` and its per card actions through\n `renderCardActions`. It shares the record index's selection and announcement\n contract, which ticket 166 made the law for the index family: the same `selectable`\n union, the same held set of keys, the same three-state header control, the same\n single polite live region carrying a count, the same batch bar around the caller's\n own `batchActions` node, and the same `selectionScope` distinguishing a page-wide\n selection from a filter-wide one. It fetches, sorts, filters and slices nothing.\n \n **`RecordGrid01` is a grid edited in place.** Its columns are the shared\n `FieldSpec`, because a cell that writes is a control and the only vocabulary this\n package publishes for a control is the field specification; `ColumnKind` is the\n vocabulary for a value a reader reads and is held disjoint from it. The Block seeds\n each cell from the caller's row, holds the in-progress values so a cell redraws as\n it is typed, and reports every edit through a required `onCellChange` as the row\n identity and the column key. It fetches nothing, sorts nothing and pages nothing,\n and it owns no command.\n \n **Why `ColumnKind` was not widened.** The obvious alternative was an editable cell\n arm on `ColumnKind` plus a change callback. That arm would name a field Component\n (`Input`, `Select` and their kin), which is a member of `FieldKind`, and\n `scripts/check-spec-unions.mjs` reports it as an `overlapping-union` between the\n read union and the write union, in the same run that would otherwise pass. The\n disjointness the gate protects is exactly the fact that a cell that writes is a\n different job from a cell that reads, so the answer is a new Item that takes the\n field union rather than a wider column union.\n- ad69fa2: The record detail draws a dated trail as one of its relations\n \n `RelationKind` gains the `ActivityFeed01` arm, so a record's event history is a\n first-class relation drawn over `EventSpec` values rather than the generic `slot`\n arm. The union previously offered only `Timeline`, whose `TimelineEntry` carries a\n `duration` and a `state` and no `at`, so a record's dated occurrences had no\n relation to draw them. A `Timeline` is an instrument and a trail is not, so the fix\n is a new arm and not a moment on `TimelineEntry`: one Component carrying both a\n moment and a duration would be a second answer to a question that already has one.\n \n `RecordDetail01` draws the arm with the shared `ActivityFeed01` Block, exactly as\n it draws the other arms, and its relations are unchanged otherwise. The roster does\n not change: this widens an existing union and an existing Block, and no new Item,\n Kind, Page or catalogue entry appears.\n\n## 1.0.0\n\n### Major Changes\n\n- dbc8a65: Every superseded figure declaration is deleted\n \n The five local declarations of a reading over a population are gone from the\n published interface, and `MetricSpec` from `@nanisoft/prism-ui/spec` is the one\n shape a figure is declared with in this package. The expand step made each of the\n five an alias of the shared type so an old name resolved while its callers moved;\n this is the contract step, and it deletes every one of them now that a search of\n the whole tree, the documentation and the corpus finds no caller left. Deleted:\n \n - `Dashboard01Metric`, from `@nanisoft/prism-ui/blocks/dashboard-01`.\n - `ProjectDashboard01Figure`, from `@nanisoft/prism-ui/blocks/project-dashboard-01`.\n - `ChartCard01Reading`, from `@nanisoft/prism-ui/blocks/chart-card-01`.\n - `Trend01Item`, from `@nanisoft/prism-ui/blocks/trend-01`.\n - `Stat`, from `@nanisoft/prism-ui/blocks/stats-01`.\n \n `Dashboard01`, `ProjectDashboard01`, `ChartCard01`, `Trend01` and `Stats01` keep\n their own names, kinds and arrangements; only the shape is shared, because a shape\n is shared and a screen is not. This is the close of the metric expand-contract\n sequence, and the count of declarations describing a reading over a population in\n this package is one.\n \n The process stage shape, `ProvisioningStep`, stays declared with `Provisioning01`\n and is not migrated. It is the inside of one Item rather than a shape two Items\n would each declare, and a declared stage has not happened, so it is not migrated\n onto the event specification either.\n \n # Migration\n \n - Replace every use of `Dashboard01Metric`, `ProjectDashboard01Figure`,\n `ChartCard01Reading`, `Trend01Item` and `Stat` with `MetricSpec`, imported from\n `@nanisoft/prism-ui/spec`.\n - The shape is unchanged, so the migration is a rename of the type reference. A\n figure or row still carries `key`, `label` and `value`, and the optional `delta`,\n `deltaFormat`, `hint`, `series` with `seriesLabel`, and `href` with `hrefLabel`.\n - A build that reports one of the five names as missing is reporting a name that\n was deleted here rather than an exported entry that moved.\n- e9e73d6: Four forms take the shared field specification\n \n `AuthForm01`, `Contact01`, `Provisioning01` and `Signup01` now take the same\n `FieldSpec` and `FieldSpecGroup` a record write form takes, so the vocabulary a\n consumer learns on one screen is the vocabulary on all five. Each Block's own\n field declaration goes from the published interface, and a control Prism does not\n ship arrives as the `slot` arm of `FieldKind` rather than as a property on a\n per-Block type.\n \n - **`AuthForm01`**: `fields: AuthFormField[]` becomes `groups: readonly\n FieldSpecGroup[]`. The controlled `value` and `onChange` on a field go, because\n the specification is data and holds no callback; the Block is uncontrolled and\n the caller reads the values out of the form element. `error` becomes\n `submitError`, and `errorId` becomes an entry in the new `issues` list keyed by\n field. `AuthFormField` is removed and `AuthForm01Issue` is added.\n - **`Contact01`**: `fields: ContactField[]` becomes `groups: readonly\n FieldSpecGroup[]`, and a new `issues` list draws a message under a field.\n `ContactField` and `Contact01Option` are removed and `Contact01Issue` is added.\n - **`Provisioning01`**: a step's `fields: readonly ProvisioningField[]` becomes\n `fields: readonly FieldSpec[]`. `ProvisioningField`, `ProvisioningFieldType` and\n `ProvisioningOption` are removed; `ProvisioningStep`, `ProvisioningValue` and\n `Provisioning01Status` are unchanged.\n - **`Signup01`**: `fields: readonly SignupField[]` becomes `groups: readonly\n FieldSpecGroup[]`. `SignupField` and `SignupOption` are removed; the `password`\n arrangement, `SignupValue`, `Signup01Password`, `Signup01Reading`,\n `Signup01Status` and `Signup01SignIn` are unchanged.\n \n `FieldCommon` gains an optional `autoComplete`, the platform's autofill hint, once\n for every screen rather than in a second declaration per Block. Requiredness stays\n a rendering fact and an HTML attribute; no specification carries a rule.\n \n # Migration\n \n Replace a field list with groups of `FieldSpec` from `@nanisoft/prism-ui/spec`:\n \n - Rename a field's `id` to `key`, its `type` to a `kind` naming a Prism control\n (`'text'`, `'email'` and `'tel'` become `'Input'`, `'password'` becomes\n `'PasswordField'`, `'textarea'` becomes `'Textarea'`, `'select'` becomes\n `'NativeSelect'`), and its `description` to `help`.\n - Move a choice's `options` under its kind, unchanged in shape.\n - Wrap the list in one or more `{ fields: [...] }` groups and pass it as `groups`.\n - Pass a control Prism does not ship as `{ kind: 'slot', control: <your control /> }`.\n - On `AuthForm01`, replace `error` with `submitError` and, where you marked a\n field, pass `issues={[{ field: key, message }]}` instead of `errorId`.\n - Read `onSubmit` values by each field's `key`. `AuthForm01` is uncontrolled: read\n `new FormData(event.currentTarget)` rather than a controlled `value`/`onChange`.\n - Pass `autoComplete` on the fields a password manager or a phone keyboard fills.\n- dbc8a65: The activity trail takes the shared event specification\n \n `ActivityFeed01.events` is now an `EventSpec[]` from `@nanisoft/prism-ui/spec`\n rather than the Block's own `ActivityFeed01Event`, and that local type is gone from\n the published interface. The trail is the activity feed Block widened, so no new\n Item appears beside it: a shipment's chain of events, an issue's whole history and a\n transaction's trail are record details, and the trail's own entry declaration is the\n one declaration of a dated attributed occurrence every Block in this package takes.\n \n An entry carries a stable `key` that is never the words of a label, a moment printed\n exactly as the caller passed it with the machine value on the element, a required\n `actor` and `action`, an optional `target`, an optional `detail` node, a `tone` whose\n `toneLabel` is required wherever the tone is set, and a destination whose words are\n required wherever it is set.\n \n The specification names no vocabulary of what happened: there is no `kind`, because\n a closed union is legitimate only when its members are Components this package\n ships, and a caller needing one puts the word in `action` or composes a status into\n `detail`. Immutability is a rendering fact and nothing more, so there is no\n `immutable` prop, no seal glyph and no verification mark. A trail draws a moment and\n never a length, so there is no scale, no range and no axis, and the Block owns no\n retention window, archive, purge, legal hold, export or as-of clock.\n \n # Migration\n \n - Replace `ActivityFeed01Event` with `EventSpec`, imported from\n `@nanisoft/prism-ui/spec`, on the `events` prop.\n - Rename each entry's `id` to `key`.\n - Move the tone's label to `toneLabel` wherever `tone` is set.\n - Remove any `kind`, `immutable`, seal, verification, scale, range, axis or\n retention member: the specification does not carry them and does not accept them.\n Put a happened- word in `action`, and compose a status into `detail`.\n- dbc8a65: The settings panel takes the shared field specification\n \n `SettingsPanel01` now takes the same `FieldSpec`, `FieldSpecGroup` and `FieldKind`\n a record write form takes, and its own field declaration, `SettingsPanelField`, is\n gone from the published interface. The archetype is answered by the record write\n form Block and earns no Item of its own: the difference between a form and a setting\n is about what sits behind the surface, not about the surface.\n \n Two things change in the props, and the second is breaking.\n \n - `sections` is now `groups`, and a field is a `FieldSpec` with a stable `key`, a\n required `label` and a required `kind`. The `slot` arm means a control this\n package does not ship has somewhere to go, and a field's shell stays Prism's\n either way.\n - `secondaryAction` is gone. That position is now `footerStart`, a node the caller\n wrote, placed without styling. A caller who passed a label and no function could\n previously produce a focusable control that activated to nothing; a `CtaLink` with\n its required `href` cannot.\n \n `submitLabel` is now one arm of a save union: `action`, `onSubmit` or `submit`, each\n forbidding the others, exactly as the write form takes them. A region that applies\n on change passes `values` and an `onValueChange` that applies the mutation, and no\n save arm at all, and the Block draws no footer. Whether a setting leaves on change,\n on blur or at a save is the caller's own handler and the caller's own submission\n arm, and no prop here decides it.\n \n The Block renders no revert control, no reset to defaults control and no storage of\n any kind, and it draws no tab strip and no rail.\n \n # Migration\n \n - Rename the `sections` prop to `groups`, and pass each field as a `FieldSpec` with\n a stable `key`, a required `label` and a required `kind`, imported from\n `@nanisoft/prism-ui/spec`. A control this package does not ship goes in the\n `slot` arm.\n - Replace `secondaryAction` with `footerStart`, a node you write, placed without\n styling. A label with no function no longer compiles into a control that does\n nothing.\n - Replace `submitLabel` with one arm of the save union: `action` for a destination,\n `onSubmit` for a handler, or `submit` for a node you write. The three forbid one\n another. A region that applies on change passes `values` and `onValueChange` and\n no save arm, and draws no footer.\n - Remove any use of `SettingsPanelField`; it is gone from the published interface.\n\n### Minor Changes\n\n- f11799a: Add `emptied-by-reader` to `EMPTY_REASONS`, for a region the reader emptied\n \n `EmptyState01` takes a fourth reason beside the three it takes today: a region\n whose records were there and that this reader's own earlier action moved every one\n of them out of. A bin, an archive or a recycle view a reader has just cleared is\n the case it exists for.\n \n **A member on a published union, not a new Block and not a catalogue row.** The\n existing `EmptyReason` type gains its fourth member and the changeset says so\n because a consumer's own code that enumerates the union now has one more case to\n handle. `EMPTY_REASONS` is published through\n `@nanisoft/prism-ui/blocks/empty-state-01` beside the Block, and `catalog.ts`\n holds the same number of rows it held before, because a union member is a value on\n an existing exported type rather than an Item: it has no slug, no page, no Demo\n and no corpus entry of its own.\n \n **It is a fourth reason rather than a fourth Block because it is a statement about\n a collection, which is what the other three are.** Each of them answers what\n happened to the set the region would have drawn, so the frame they share stays\n true of all four: there is nothing here to read, and the frame makes no claim\n about why. A bin the reader emptied has nothing readable beside it, so the shared\n frame is honest there. That is the whole difference from `NothingChosen01`, which\n is a statement about a reader's pointer at a record and whose records are all\n still there: it draws no frame, it is a separate Block, and the two arguments\n argue the same line from opposite sides.\n \n **Two of the four are the reader's own doing and they are not the same doing.**\n `no-match` hides rows out of a set that is still there, so clearing it brings them\n back. `emptied-by-reader` is a record gone from the live set rather than hidden in\n it, and the reader put it there. Every saved view a caller builds out of the filter\n values it holds is already `no-match` whenever it comes back empty; a trash view\n is the one that differs in kind, because it fetches a different collection rather\n than narrowing this one. So the two members differ in exactly the fact a reader\n must be told honestly, and one sentence for both would tell a reader who has\n deleted everything that their own search found nothing. Both are named in the\n JSDoc on the union, which the emitted declarations preserve and the corpus reads.\n \n **It renders no control, and the Block already had that arm.** The reason takes\n the caller's `title` and an optional `body` and nothing else, because a reader who\n has deleted nothing has nothing to restore and the records a restore would bring\n back are the caller's own nodes in the index around this region. `EmptyState01`\n has never rendered an action without an `actionLabel`, so the member adds no\n render branch, no prop and no gate exemption: `scripts/check-block-controls.mjs`\n is green on the source exactly as it was, and the one control in it is still the\n `Button` behind that label.\n \n It carries no ink either, for the reason the Block gives: nothing is broken,\n nothing is missing, and nobody is blocked. `REASON_INK` is unchanged, so the only\n reason drawn in `muted-foreground` is still the permission boundary.\n- e9e73d6: Four more figure owners take the shared metric specification\n \n `ChartCard01`, `ProjectDashboard01`, `Stats01` and `Trend01` now take the same\n `MetricSpec` the metric summary takes, so a figure a consumer declares on one of\n these surfaces is the same figure it declares on the others. This is the migrate\n step of a breaking consolidation: five shapes are declared across this package\n for a reading over a population, and they disagree about whether a delta is a\n number or a percentage, about `deltaFormat`, about `hint`, and about whether a\n series and a destination are part of a figure at all. This release moves the four\n remaining owners onto `MetricSpec`, so one shape is left in use. Their superseded\n local declarations stay exported as deprecated aliases until the contract ticket\n deletes them, so an existing name continues to resolve.\n \n **The shape every one of the four now takes.** A stable `key` that is never the\n words of the label, a required `label` (a `ReactNode`) and `value` (the caller's\n own node), a `delta` whose sign is the direction, an optional `deltaFormat` (the\n caller's own words for the change, a `ReactNode` rather than a callback), an\n optional `hint` for the period or the caveat, and an optional `series` with its\n `seriesLabel` and an optional `href` with its `hrefLabel`. The two pairs are held\n by the type: a series set without its name, or a destination set without its\n words, does not compile.\n \n **What changes per Block, and the arm that replaces each prop that goes.**\n \n - **`ChartCard01`**: `ChartCard01Reading` becomes an alias of `MetricSpec`. A\n reading's `deltaFormat` is now the caller's own words rather than a `(value:\n number) => string` callback, so pass a node, and `key` is now required. The\n reading line draws the series and the destination too.\n - **`ProjectDashboard01`**: `ProjectDashboard01Figure` becomes an alias of\n `MetricSpec`, `key` is now required, and a figure can now carry `deltaFormat`,\n `hint`, `series` and `href`. The bare `0.12` a delta without a formatter used to\n print is now a decision rather than a drift.\n - **`Stats01`**: `Stat` becomes an alias of `MetricSpec`, `key` is now required,\n and a delta no longer has a percent sign appended to it. A delta with no\n formatter prints the number the caller passed, and a delta with a formatter\n prints the caller's own words.\n - **`Trend01`**: `Trend01Item` becomes an alias of `MetricSpec`, a row's `id`\n becomes `key`, and `deltaLabel` becomes the `deltaFormat` node. A row can now\n carry a series and a destination, and a delta with no formatter prints the\n number rather than making the Block throw.\n \n **What is kept, and what is still not owned.** Each Block keeps its own name,\n kind and arrangement. `ChartGroup01`, the page fixtures and the site landing page\n were updated at their call sites, because this package resolves the shared type\n through the component package. No Block performs aggregation, derives a rate of\n change, owns a period, a unit, a threshold, a target, a severity or a freshness\n state, and none draws a period switcher, a comparison toggle or an export. A\n metric's period stays the caller's own words, carried by `deltaFormat` and `hint`.\n \n `@nanisoft/prism-ui/spec` is the type to import: `MetricSpec`, from\n `@nanisoft/prism-ui/spec`, replaces `ChartCard01Reading`, `ProjectDashboard01Figure`,\n `Stat` and `Trend01Item` at every call site.\n- f11799a: Add `@nanisoft/prism-ui/spec`: one shared specification module, and the gate for it\n \n This adds published surface and changes no existing one. Nothing you compose\n differs today; what changes is that five screens worth of declared content now\n have one declaration each instead of several that disagree, and that the\n declarations are held by a gate rather than by review.\n \n **The five specifications.** `FieldSpec` and `FieldSpecGroup` describe the fields\n a write form, a wizard or a settings region renders, in the order the array is\n written. `ColumnSpec` describes a column of a record index. `RelationSpec`\n describes one relationship of a record, one ring deep. `MetricSpec` describes a\n figure taken over a population. `EventSpec` describes one dated, attributed\n occurrence. Every one of them is plain serialisable data: no validator, no\n constraint object, no schema, no pattern, no callback. That is what lets an agent\n read one out of a document, and it is why validation is yours entirely.\n \n **Three closed unions, and the one arm that is an escape rather than a catch-all.**\n `FieldKind` names the input Components this package ships, `ColumnKind` the\n reading Components, `RelationKind` the collection arrangements already drawn. Each\n carries a `slot` member for the control, cell or arrangement you draw yourself, and\n Prism still draws the shell around it: the label, the help, the error, the header,\n the alignment, the frame. A `props` bag would have been the alternative and would\n have put the hole back.\n \n **What is deliberately absent, so that you are not waiting for it.** A\n specification carries no rule and no validator, because a Block ships no behaviour\n and evaluating a rule is behaviour; `required` stays, and it is the mark and the\n HTML attribute rather than a check. A metric carries no period, because a\n comparison needs a period and a billing screen's month is not a trading screen's\n week: the words for it go in `deltaFormat` and `hint`, in your own units. An event\n carries no `kind`, no immutability, no retention window and no time axis, because\n no enumeration of what your product calls a thing that happened is ours to publish\n and because a drawing has no authority over the store behind it. A relation holds\n members and never another relation; a descendant is reached by a link you pass.\n \n **The gate.** `packages/ui/scripts/check-spec-unions.mjs` reads the emitted\n declarations rather than the source, because that is the seam your editor\n resolves. It fails when a union member names something this package does not\n ship, when two of the three unions claim the same member, when a specification\n grows a validator, a rule, a schema, a pattern, an immutability claim, a retention\n window or a time axis, when an event grows a `kind`, and when a relation holds a\n relation. Before it existed, every one of those was accepted by every gate in the\n repository, because a specification type is not an Item and no catalogue gate reads\n the module at all.\n \n **One thing it does not hold, so you know what a green run means.** It does not\n hold that there is one shape per concept: a second declaration of the field, column,\n relation, metric or event shape would be named differently from the first, and the\n rule that would catch it would be a list somebody maintains. That is held by review.\n \n **The surface gate's internal boundary moved with it, in one direction only.**\n `packages/ui/src/lib/` is where the internal helpers live, but the `exports` map is\n what decides: a declaration under it that a published entry reaches is public\n surface and the surface gate reads it, and one that no entry reaches is still\n internal and still has to be declared. `dist/lib/spec.d.ts` is the first such file.\n Nothing under `dist/lib` was declared internal and then undeclared, and no published\n subpath was removed.\n- caee732: `Directory01` gains a per-member node\n \n `Directory01` takes a new `renderMemberAction` prop: a function from a member to a\n `ReactNode` the Block places at the member's trailing edge, in both the card and the\n row arrangements, and draws nothing of its own where it is not passed.\n \n **The control and its handler are the caller's, and that is why it is a node.** A\n directory is a browse, so a plugin or model row's enable is a property of a member\n rather than a command on a selection, and `scripts/check-block-controls.mjs`\n classifies `Switch` and fails a Block that renders one without its handler. A Block\n ships no behaviour and so cannot supply one, so the enable arrives as the caller's\n own node, in the shape `DataTable01`'s `renderRowActions` already takes.\n \n No new Item and no new catalogue entry: the change is a prop on an Item that ships,\n with its JSDoc, its Demo and its test.\n- caee732: `DocumentPage`, the document and research workspace\n \n A new Page arrives at `@nanisoft/prism-ui/pages/document-page`. It draws one\n document read in full, its body held to the reading measure, and one or more\n regions beside it.\n \n **It is authored beside `DocsShell`, and neither of that Page's two rules is\n relaxed.** A section stays a label and not a control, and a group with no index\n stays a label and not a route, because both are statements about a documentation\n site's navigation tree and this workspace never asks that question. A status\n belongs to a region of one document, and a route is the document's own contents.\n No consumer of `DocsShell` is affected.\n \n **One Page draws all seven screens.** The document is `RecordDetail01` and the\n body is `Prose`; a version list is an `activity` region, a source or output list\n is a `table` region, a comment thread is a `members` or a `table` region, and the\n evidence comparison matrix is a `comparison` region drawn by `Compare01`. The\n region union is by arrangement and not by subject matter, so the Page mints no\n document, version, comment or source type and ships no sentence. The editor, the\n fetch, the retention, the share, the export and the permission are the consumer's.\n \n The Page ships with a catalogue entry, a JSDoc block, an Item page and a Demo, a\n corpus and store entry, a composition test and an accessibility test over its\n heading structure and named regions.\n- f11799a: Add `NothingChosen01`, the resting state of a detail pane\n \n `NothingChosen01` draws a region that can hold a record and has none chosen in it\n yet: the caller's words, an optional sentence under them, and an optional mark of\n the caller's choosing. It is a new Block beside `EmptyState01` rather than a fourth\n reason on it, and the difference is the shape of the claim rather than a matter of\n taste.\n \n The three reasons `EmptyState01` takes are all statements about a collection:\n nothing ever existed in it, the reader's own filter emptied it, or this reader may\n not see it. The dashed frame and the height floor those three share assert\n something that follows from all three together: there is nothing here to read.\n Nothing chosen is not that. No collection has been emptied, filtered or hidden, the\n records are in the index a few centimetres away, and what is missing is the\n reader's pointer at one of them. A fourth arm would have had to keep that frame\n honest while a readable region sat next to it, which is the failure the separate\n Block exists rather than to commit.\n \n So the Block ships no reason value, because one cause is not a choice a caller\n makes; no default sentence and no fallback, because the words are entirely yours;\n no frame, for the reason above; and no action. That last one is not an omission. A\n Block ships no behaviour, so a control here would be either one this Block cannot\n wire or a second copy of an affordance the index beside it already draws for the\n same reader, and `scripts/check-block-controls.mjs` has nothing to catch in a\n source that contains no control.\n \n It also declares no height floor. A pane in a grid is already as tall as its\n neighbour, so a `min-h` authored here would be a second answer to a question the\n split's tracks have already answered. What the Block does is fill the height it is\n given and centre itself in it.\n \n Two things it inherits rather than re-derives: the icon is a slot drawn\n `aria-hidden`, because a mark a screen reader reads is a second announcement of\n something the words already say, and the title is a `<p>` rather than a heading,\n because the pane outlives its contents and an outline entry that vanishes on the\n first click is an entry a reader navigating by heading cannot follow.\n \n The split's tracks hold their ratio at every selection state, so the list never\n moves out from under the hand that clicked it. What changes is content, and a\n caller who wants the index louder makes the index denser, which is the index's own\n decision to take.\n- dbc8a65: The issue detail Block is deprecated in favour of the record detail Block\n \n `IssueDetail01` is deprecated, not merged and not deleted. It is the same job as\n `RecordDetail01` under a name that is wrong the moment the record is an invoice, an\n order or a subscription, and a name that has to be wrong about nine tenths of its\n uses is a name that fails the test that its name survives being wrong. The Item\n still renders and is still supported for now, but nothing new should be started on\n it, and a consumer composing a record detail today composes `RecordDetail01`.\n \n **The published interface is unchanged.** This release marks the Item and changes\n its JSDoc and its documentation; no export is removed, renamed or re-shaped, so a\n consumer already composing `IssueDetail01` keeps composing it and can read the\n release without moving. `IssueDetail01` and its four types stay exported from\n `@nanisoft/prism-ui/blocks/issue-detail-01`. Its own field declaration stays with\n it and is not migrated onto the shared field specification, because it is a label\n and a value node a reader is entitled to be told about a record rather than an\n input.\n \n The catalogue entry carries the `deprecated` status, so the Item page shows a\n deprecated mark, and the Item page and the record detail Block's documentation each\n name the other, so a reader arriving at either arrives at the answer.\n- dbc8a65: Add `IndexDetail01`, the master and detail split\n \n `IndexDetail01` is a composition Block that renders an index pane beside a detail\n pane and holds no selection state at all. Selection, routing and which record is\n open belong to the consumer, because Prism never imports a router. Nothing in the\n Block connects the two panes: the index reports every selection change through its\n own callback, and reacting to it is the caller's own read of that callback and a\n pass of the record it wants into `detail`.\n \n The tracks hold their ratio at every selection state, so clicking a row does not\n move the list out from under the hand that clicked it. The pane before anything is\n selected holds what the caller placed, or nothing at all, so the Block ships no\n fallback sentence, no frame of its own and no loading mark. The detail slot is a\n required node, because a record that is gone, one this reader may not see and one\n whose fetch failed are three causes sharing one address and three different\n sentences.\n \n The split does not collapse at a narrow viewport. Which pane survives is the\n caller's composition rather than a media query deciding a shape, so the narrow\n arrangement is one the caller writes. The Block renders no control of its own.\n \n This change also corrects the record index section of the design document, which\n claimed the split was already authored while the roster counted from source held no\n such Item.\n- caee732: `MessageThread01`, the live Kind's second family\n \n A new `live` Item, the message-thread surface, arrives at\n `@nanisoft/prism-ui/live/message-thread-01`. It renders a conversation whose\n messages arrive over a `subscribe` function the consumer supplies and it opens no\n connection of its own, so a WebSocket, an `EventSource`, a polling timer and a test\n array are all the same shape to it.\n \n **A message list with ordered parts, and not an event list or a turn list.** A\n message has a stable identity, a sender and an ordered list of parts, and a part is\n text, a tool call, a reasoning block, a citation or an attachment. The message and\n part vocabulary lives with the surface, beside `RunEvent` and `ToolCall`, and is\n deliberately outside the shared specification module, so no sixth specification\n member and no `CONTEXT.md` word is added.\n \n **It reuses what the Kind already names rather than minting a second answer.** A\n tool-call part is drawn through `ToolLedger01`'s own row, so there is no second\n tool-call shape. The status tiers are `RunStatus`, published once and shared by both\n live surfaces. The run controls are `PromptComposer`'s send, stop, retry and attach,\n composed beside the thread rather than a second set drawn inside it. The one surface\n it does not reuse is `RunStream01`, because a run's log is a sequence of occurrences\n and a conversation is a sequence of messages.\n \n **Prism holds a bounded in-memory window and promises no resumption.** The durable\n resumption buffer that outlives a mount is the consumer's, through the same `initial`\n and `resubscribeKey` seam `RunStream01` already documents: a surface opened\n mid-thread is seeded from the consumer's own record, and a reconnect is a new key\n over a fresh seed. The surface sorts by `at` with a tie on `id`, bounds its window,\n announces a growing list once rather than reading it whole on every arrival, and\n holds no control it cannot act on.\n \n The Item ships with a catalogue entry, a JSDoc block, an Item page and a Demo, a\n corpus and store entry, and a boundary test in the shape of the `RunStream01` one.\n- 1079d02: The metric summary Block takes the shared metric specification\n \n `Dashboard01.metrics` is now a `MetricSpec[]` from `@nanisoft/prism-ui/spec`\n rather than the Block's own figure shape. This is the first step of a breaking\n consolidation: five metric shapes are declared across this package today and they\n already disagree about whether a delta is a number or a percentage, about\n `deltaFormat`, about `hint` and about whether a series and its name are part of a\n figure at all. `MetricSpec` is the one shape, and this release makes the metric\n summary Block the first surface to take it. The other four owners are migrated in\n later batches, so this is one Block wider rather than five replaced at once.\n \n **What a figure is now.** Every entry is a `MetricSpec`: a stable `key` that is\n never the words of the label, a required `label` and `value`, a `delta` whose sign\n is the direction, an optional `deltaFormat` carrying the caller's own words for\n that change, an optional `hint` for the period or the caveat, and an optional\n `series` with its `seriesLabel` and an optional `href` with its `hrefLabel`. The\n two pairs are held by the type: a series set without its name, or a destination set\n without its words, does not compile.\n \n **What changes for a consumer.** `label` is now a `ReactNode`, so a caller that\n passed a string passes the same string and a caller that passed a node keeps it.\n `deltaFormat` is the caller's own words rather than a `(value: number) => string`\n callback, so `deltaFormat: (v) => String(Math.abs(v))` becomes\n `deltaFormat: '3 fewer than yesterday'` and the direction mark still comes from the\n sign of `delta`. `sparkline` and `sparklineLabel` are now `series` and\n `seriesLabel`, and `key` is now required. `Metric` takes the words as a node when\n the Block hands them over, so a delta with a formatter prints the caller's words\n and a delta without one prints the number the caller passed.\n \n **`Dashboard01Metric` is kept as an alias** of `MetricSpec` so an existing name\n resolves while the other four figure owners are migrated. It is a synonym on its\n way out, not a second shape, and a later contract release removes it alongside the\n superseded `ProjectDashboard01Figure`, `ChartCard01Reading`, `Trend01Item` and\n `Stat`.\n \n **Nothing else is owned.** The Block still performs no aggregation, owns no period,\n no unit, no threshold, no target, no severity and no freshness, and draws no period\n switcher, no comparison toggle and no export.\n- dbc8a65: Add `RecordWizard01`, the multi-step record write form\n \n `RecordWizard01` is the write form's second arrangement rather than a prop on the\n plain form. A wizard draws one group of fields at a time above a rail and asks a\n decision the plain form never asks, which is whether a step may be left forwards, so\n it earns its own name: it is a property of how many fields a record has, not one of\n two renderings a consumer picks between on purpose.\n \n The step is the consumer's. `current` is controlled and `onStepChange` is required,\n and the Block draws the step it was handed and no reachability at all. The\n specification is per step, and a step is a `FieldSpecGroup` from\n `@nanisoft/prism-ui/spec` with a stable `id` and a string `label`, so nothing new is\n declared in order to hold the steps and a group's heading and its rail label are one\n word. One specification sliced by a caller grouping is refused.\n \n Every field on the stepped arm is controlled and carries the consumer's value and\n change handler, so the values are the consumer's from the first keystroke. Back, the\n forward control and the rail are `FormWizard`'s, and this Block renders none of\n them, declares no submission arm of its own, and leaves an issue naming a key that\n is not on the step being drawn undrawn without moving the reader.\n- 1079d02: The record detail draws one record and its relationships\n \n `RecordDetail01` is a new Block: one record in full, its relationships drawn open\n as one ordered list, and its own actions beside its identity. It is a Block rather\n than a Page because the frame and the claim belong to a caller and a Page owns no\n URL, so a detail Page would have to own the selection the consumer holds. It is\n authored beside `Summary01` and `QuickView01` rather than by widening either,\n because a summary exists to add up and a quick view is a transient dialog.\n \n **Relationships are one declared list on one frame, not a card per relation.**\n Each entry is a `RelationSpec` from `@nanisoft/prism-ui/spec` plus the words its\n own empty frame needs, drawn in the declared order under one heading level inside\n one bounded frame with one rule between them. The `kind` names a collection\n arrangement this package already draws: `Table`, `ListPanel`, `Timeline`,\n `AvatarGroup` or the caller's own `slot`. **Depth is one, and the bound is in the\n type**: a relation's members never name `RelationSpec`, so a related record with\n its own related records is a graph the consumer walks, reached by the caller's\n `href` rather than by depth. The Block composes no disclosure anywhere.\n \n **The record's actions are one optional node, placed in the record's own band.**\n The Block renders no control of its own, there is no declared action union and no\n destination arm, and the confirmation for a destructive action is the caller's own\n `AlertDialog` composed around their control in the same slot. **A relation with no\n members draws `EmptyState01` at a reason the caller names**, in the relation's own\n frame, because a customer with no invoices is a collection that is empty and\n `NothingChosen01` is a statement about a reader's pointer at the index beside the\n pane, which this Block cannot be in. **The Block takes no total, no count and no\n sum**: a relation's `count` is the caller's own string and is never derived from\n the members, and a screen that wants a figure composes `Summary01` or `Stats01`\n into the Block's `children`.\n- 1079d02: The record index groups by a key on a row and announces the two selection scopes differently\n \n `DataTable01` widens again rather than adding an Item, and the roster does not\n change: a grouped index is the same job with a `groupBy` on a row, not a second\n arrangement, so no `-02` is earned.\n \n **Grouping is a key on a row.** `groupBy` takes a row and returns the key it\n belongs to, or `undefined` for an ungrouped row, and `groupLabel` names the\n heading the Block draws above each run of rows that shares one. The two are a\n single arm: `groupLabel` is required wherever `groupBy` is set, because a heading\n with no words is a blank row. The Block never reorders the rows it was handed, so\n same-key rows must arrive adjacent and a run of them is one group. A group heading\n is a row in the same body as any other, and it is not a record: it is never in\n the selection set, never in the count and never in what the header's page scope\n covers, so forty headings over four hundred rows select four hundred records and\n say so.\n \n **The two selection scopes are announced differently, and the filter scope's\n number is the caller's.** `selectionScope` is new and is a union. Its page arm is\n the default and the Block speaks `labels.selectedCount` over the keys it holds.\n Its filter arm reports that the whole matching population is selected and carries\n a required `count` from the caller, and the Block speaks\n `labels.selectedAllMatching(count)` rather than printing the page as if it were\n the total. The Block owns no control that selects a population it cannot see: the\n caller selects in its own store and reports the scope here.\n \n **What is new on the surface:**\n \n - `groupBy` and `groupLabel`, together, for a grouped body.\n - `selectionScope`, a `DataTable01SelectionScope` union, for the page or the\n filter scope.\n - `selectedAllMatching`, a required `labels` member: the summary and announced\n sentence for the filter scope. `selectedCount` is now the page-scope sentence,\n and it should say the page.\n \n The per-row prop that went and the node that replaced it are named in the record\n index selection entry in this release: `rowActions`, a declared list of action\n objects with an optional handler, is gone, and `renderRowActions`, a node per row,\n replace it, so a row that looks like a command and carries none cannot be built.\n- 1079d02: The record index draws typed columns and holds the selection set it was given\n \n `DataTable01` widens rather than adding an Item, and the roster does not change:\n every one of the in-scope screens is one job, a reader scanning rows and acting on\n them, differing only in its columns, its keys, its filters and its sort key. No\n `-02` is earned by it.\n \n **The columns are typed data now, and the callback is gone.** `columns` is a\n `readonly ColumnSpec[]` from `@nanisoft/prism-ui/spec` rather than an array of\n `{ id, header, cell }`. `key`, `header` and `kind` are required on each column,\n because a column a reader reaches without a name is a column every row announces\n as untitled, and `kind` is drawn from Prism's own cell vocabulary rather than from\n an HTML attribute. The row value at the column's key is the cell's content: a\n single-node kind draws it as that Component's child, a structured kind takes it as\n that Component's props, and the `slot` kind draws it as your own node inside the\n header, alignment and sort affordance the Block draws. `DataTableColumn` and\n `DataTableRowAction` are removed with it.\n \n **Selection is required whenever the index is selectable.** `getRowId` is required\n on the Block, because sorting, filtering, paging and deleting all move a row's\n index and an index-keyed selection silently changes which records a batch action\n acts on. `onSelectedIdsChange` is required on the selectable arm and `batchActions`\n with it; the Block holds the set on its uncontrolled arm and reports every change\n through the one callback, so mirroring the set is one line and ignoring it is zero.\n \n **What is new on the surface:**\n \n - `columns`, now `ColumnSpec[]`. `sortable` draws `TableSort` and reports the next\n direction through the new `sort` and `onSortChange`.\n - `getRowId`, now required.\n - `onSelectedIdsChange`, required on the selectable arm.\n - `batchActions`, required on the selectable arm: one `ReactNode` the Block places\n in a bar that mounts only while the set is non-empty and displaces the filter\n controls. The Block draws the count and the dismiss and no command of its own.\n - `renderRowActions`, replacing `rowActions`. A node per row rather than a declared\n action list whose handler was optional, so a row that looks like a command and\n carries none cannot be built.\n - `sort` and `onSortChange`, for the ordered column.\n - `selectRow` is now `(row) => string` so every checkbox names its record,\n `selectedCount` is the count the live region speaks, and `clearedSelection` and\n `dismissSelection` join it. `sort` is the words for the next direction.\n \n **One polite live region carries the count**, never an assertive one, mounted while\n there is an announcement; it speaks the count rather than a list of labels, and it\n speaks the emptying as its own sentence rather than a count of zero. The visible\n summary and the announced number are the same string.\n \n **Nothing else is owned.** The Block fetches nothing, sorts nothing, filters\n nothing and slices nothing, owns no action, no aggregate, no address and no second\n pane. The detail pane reacts through your read of `onSelectedIdsChange`; no bridge\n is built between the two.\n- 1079d02: The record write form renders a field specification whole\n \n `RecordForm01` is a new Block: a create or edit screen rendered from a typed,\n ordered field specification. It takes `FieldSpecGroup[]` from\n `@nanisoft/prism-ui/spec` and draws Prism's own controls, each inside one shell of\n label, help and error. Every field carries a stable `key`, a required `label`, a\n required `kind` and optional help, hint, placeholder, disabled flag and starting\n value; a control the package does not ship is the `slot` arm, and the Block still\n draws the shell around the caller's node.\n \n **The error state is an issue list keyed by field**, the shape `FormDialog` and\n `FormWizard` already publish, and a message about the submission rather than about\n a control is `submitError`, which names no field. **The save union has three arms**\n and the compiler holds it: `action` posts to a URL, `onSubmit` hands over the\n form's element, and `submit` is a slot for the caller's own control. The save\n control is `type=\"submit\"` inside the `<form>` the Block renders. **Layout is\n derived**: `columns` is `1 | 2` at the form level, one column again at the narrow\n breakpoint, and there is no per-field layout prop and no slot per region.\n \n **One piece of per-kind rendering data is added.** The `MoneyField` arm of\n `FieldSpec` gains a required `currency` and an optional `locale`, because an\n amount is meaningless without the currency it is in and no locale can supply it.\n Nothing else in the shared module changes, and the module stays plain serialisable\n data with no rule, no validator and no callback on it.\n\n### Patch Changes\n\n- f11799a: The gate that holds a Block's controls now reads the menu row and the switch\n \n **No published surface changed.** Nothing in this entry is a prop, a Component or a\n behaviour you compose differently: the change is to `scripts/check-block-controls.mjs`,\n which is a repository gate and is not in the published tarball. It is here because the\n law it holds reaches a consumer as the shape their own Block has to take, and a\n consumer reading the changelog should be able to find that law without reading this\n repository.\n \n That gate classified `Button` and `CtaLink`, and neither of them is a menu row. The\n shipped record index takes a declared list of per-row actions whose handler is\n optional and renders each entry as a `DropdownMenuItem`, so a caller who passes a\n label and no handler gets a row that sits in the menu's keyboard order, is announced\n as a menu item, and activates to nothing. The gate could not see that Component, so\n every gate in the repository was green while it shipped, and a clean run was the\n absence of a check rather than the presence of a correct one.\n \n Four Components are classified now, each with the arms that make it act:\n \n - `Button`: `onClick`, `type=\"submit\"`, `type=\"reset\"`. Unchanged.\n - `CtaLink`: `href`, which is required, so that arm is the compiler's. Unchanged.\n - `DropdownMenuItem`: `onClick`, or `render` together with `href` in the element it\n renders as. Both words rather than either one, because `render={<span />}`\n navigates to nothing and a bare `href` would accept a `data-href` as a destination.\n - `Switch`: `onCheckedChange`, and nothing else. A switch does not navigate and is\n meaningless without the handler, because the state it changes is the consumer's\n setting rather than anything a Block holds.\n \n Every `DropdownMenuItem` and every `Switch` the tree renders already carries one of\n those arms, so nothing shipped failed and no Block moved. What the widening buys is\n the next one: a menu row or a switch a Block renders with nothing behind it is a\n finding naming the file and the line, where yesterday it was invisible.\n \n **Two limits are printed on every run and neither is new.** A `{...rest}` spread\n inside a rendered control reads as carrying no handler, and a handler forwarded from a\n declared optional prop reads as present, because resolving whether the caller passed\n it is reading a type rather than a file. The second is why the shipped row action list\n is still a finding for a reader and not for this gate: the optional handler behind the\n attribute is invisible to a source scan. Removing that declared list is the authoring\n work, and this gate is what makes the next one of them fail the run.\n \n **A Block that holds a disclosure itself is still not caught, and that gap was left\n open on purpose.** `Collapsible` and `Accordion` put none of the four classified\n Components in a Block's source, so a Block rendering one with a `defaultOpen` and no\n change handler reads as clean while the state is still state a Block holds. A\n disclosure's activation lives inside the disclosure rather than on an attribute of the\n row that opens it, so there is no arm for this gate to read, and a rule that inferred\n one would report the Blocks that compose `Collapsible` correctly. The prohibition\n stays a design rule in `DESIGN.md`, an auditor checks it by looking for the absence,\n and the run prints that it is not a finding so a clean run is not read as more than it\n is.\n- dbc8a65: The log and event stream is the record index drawing event rows\n \n A delivery log or a console events screen is the record index, `DataTable01`,\n drawing rows from the shared event specification, `EventSpec`. No Item, Kind or\n Page is added: a log gains a filter region, a second axis on which entries are\n compared and a page boundary, and every one of those already belongs to the record\n index rather than to a rail. The row type is the shared event specification, and no\n sixth event shape is minted for a log or a console.\n \n A screen that wants the events of one record composes `ActivityFeed01`; a screen\n that wants to find one entry among many composes `DataTable01`. The Block composes\n no disclosure: seeing one entry in full is a destination the caller names with its\n required words, expanding a payload in place is the caller's own `Collapsible`\n placed in a `slot` cell, and taking an identifier away is the caller's own control\n beside a code block. No prop changed; this release adds the Item documentation, the\n Demo and the JSDoc that record the arrangement.\n\n## 0.16.1\n\n### Patch Changes\n\n- bf34d82: `DocsShell` drew its navigation twice in 0.16.0 and draws it once again\n \n **0.16.0 introduced a regression and this release removes it.** To give a reader below `lg` some navigation, 0.16.0 drew each tree a second time behind a `<details>`: one copy in the rail for a wide screen and one behind a disclosure for a narrow one. Only one of the two was displayed at any width, so the accessibility tree was never wrong and a reader never met two copies at once. Every documentation page in the family still shipped two of everything, and the payload of that is paid on the largest surface the system has.\n \n Three consumer test files failed against 0.16.0 as published, and two distinct defects produced every one of those failures. Two files failed on a count that came back doubled, and two failed on a `getByRole('navigation', { name })` that had two answers where it expects one: the disclosure drew its own `<nav>` under the rail's own accessible name, and that name is how a consumer's suite reaches the rail at all. A suite that holds that a documentation rail contains no disclosure control was never the finding, because the `<details>` was a sibling of the rail in the same frame and never inside it.\n \n **What a consumer measured on 0.16.0, in three built exports.** The Nexus page `/docs/architecture/kanban-service` carried 3 `aria-current=\"page\"` markers where 2 is right, because the third belongs to that site's own bar and the other two are this Page drawing the same page twice. It carried 8 `<nav>` elements where 6 is right, and 52 `data-slot=\"docs-nav-page\"` rows on a corpus of 26. The AlphaLens index at `/docs` carried 12 `data-slot=\"docs-nav-label\"` spans on a six section corpus. The Prism preview route that renders this Page, `apps/site/out/preview/docs-shell.html`, carried 22 `docs-nav-page` rows on an 11 row outline and 5 `<nav>` elements where 3 is right.\n \n **What changes.** Each tree is drawn once, at every width. A documentation page carries one `aria-current` marker, one navigation list per tree and one row per document. Below `lg` the frame is a single column and the rail is its first child, so a reader at a phone or tablet width meets the navigation before the document rather than after it, which is the gap 0.16.0 was reaching for. The Page is still a server Component: no `'use client'`, no hook, no context, no handler, no runtime token read, and no new client JavaScript on any documentation page.\n \n **The fade is now bound to the cut it marks, and it was not.** The fade element was left unprefixed while the `padding-bottom` reserving the band beneath it was already `lg:`-prefixed. The cut exists only where the rail scrolls, which is `lg` and up, so below `lg` the band went inert and the gradient kept painting: the rail's last 2rem carried a fade over the last row or two, permanently, at exactly the width this release starts serving the tree at. Both numbers still matched and no row lost legibility, which is why it survived every lane that was looking. The fade and the band are now written against the same `lg:` as the `max-h`, the `overflow-y-auto` and the `sticky`, so a fade with nothing to fade is no longer painted. **The price, stated rather than left to be found:** below `lg` the rail is bounded by its content rather than by the viewport, so a long tree runs past the fold of a phone screen with nothing marking where it ends. No cut to mark, no band to reserve and no scroll region to scroll inside. That is the honest limit of one copy of the tree at every width, and it is now written into the Page's own documentation beside the rule that causes it.\n \n **The published surface loses two slots.** `data-slot=\"docs-nav-compact\"` and `data-slot=\"docs-nav-disclosure\"` were added in 0.16.0 and are removed here. They were published for exactly one release, and no call site in the four downstream repositories addresses either by name, so nothing there has to change. A call site that reached for one now finds nothing rather than finding an empty region, which is the honest outcome and the reason this is called out. Every slot that existed before 0.16.0 stays, `docs-rail-fade` included.\n \n **What it costs, with the number.** A phone reader now scrolls past the whole navigation to reach the article. One consumer's rail measured 1,318 pixels of content in a built export at 1280 by 680, and those rows wrap on a phone, so the scroll a narrow reader now makes is a thousand pixels or more. That is a real regression for that reader and it is the reason a disclosure was reached for in the first place. It is paid rather than hidden, because the two arrangements that avoid it are both worse: a rail at `lg` and nothing below it reopens the gap 0.16.0 closed, and drawing the tree twice to keep the scroll short is what broke three consumer test files here.\n \n **What would actually remove that scroll.** A collapsed rail below `lg` needs an open state, and a server Component cannot hold one. `<details>` is the only disclosure the platform holds on the Page's behalf, so a control that opens one has to be a client island inside the Page. That is separate work with its own budget and it is not built here: shipping a client boundary to save one scroll is a different trade from the one above, and it belongs in its own change rather than as a side effect of removing a duplicate.\n \n **One Item document was corrected, and no gate reads it.** This Page's own documentation said under Best practices that an entry with no words is a label. That was true before this work and became false the moment the Page began dropping a nameless entry rather than rendering one, and it shipped false through 0.16.0 because nothing in the tree compares that bullet with the Page's own source. The bullet now states what the Page does, and `docs/quality-gates.md` records why no gate was added for it rather than leaving the question open: a gate over prose claims has to enumerate the claims, and a list somebody maintains is the shape that already left the heading gate holding one Block and passing over four, or it has to match on wording, which fires on a rewrite and not on a lie. The decidable half of that failure class has a gate, in the heading-scale table comparison. This half is a reader's, and the hole is now named in the document that collects them.\n \n **Three things 0.16.0 got right and this release leaves as they were, with the fade's bound corrected above.** The rail fade, pinned to a non-scrolling sticky wrapper with the band it covers reserved in `padding-bottom`, both written against the `lg:` that creates the cut; the sticky `max-height` arithmetic in `100svh` rather than `dvh`, which cannot exceed the space it is pinned in; and nameless entries dropped rather than rendered as an empty `<li>`, including the group-label and empty-group arms, counted on each rail as `data-unnamed-entries`.\n\n## 0.16.0\n\n### Minor Changes\n\n- 1a0ba35: A page `h1` is now visibly larger than the `h2` sections under it\n \n A page `h1` rendered at 1.875rem and every section heading under it at 1.5rem, so\n the two were one fifth apart and the page had no top to its hierarchy. The fix is\n in the type scale rather than in the Component, because the scale stopped at `4xl`\n and there was no step for a page's own claim to take.\n \n **Two steps are authored, and they are the two widest gaps in the group.**\n `5xl` is 3rem and `6xl` is 3.75rem, both at a line-height of 1. Every other step\n in the `text` group is a whole multiple of 0.125rem and these two are too;\n `4xl` to `5xl` is four thirds, the first gap in the group wider than any gap it\n already had, and `5xl` to `6xl` is five fourths. They are authored as a pair\n rather than as one step because every rung of the heading ladder is a pair, a base\n step and one step up at `sm`, so a single step above `4xl` would have made the\n page heading the one heading in the system that does not grow at a width.\n \n Both steps reach CSS and the published pack contract the way every other authored\n step does, so `text-5xl` and `text-6xl` are utilities a consumer can compose, and\n neither is a value a consumer has to write.\n \n **The heading ladder now descends from the new ceiling, one authored step per\n level, and every level holds a step of its own.** There are exactly six authored\n steps at or above Body, so the table lands on the floor with a distinct step at\n every rung, where before `h4` through `h6` shared one. `h1` is at `5xl`, rising to\n `6xl` at `sm`; `h2` is at `4xl`, rising to `5xl`; and the table descends to `lg` at\n `h6`, which is the step Body is set at. **What a consumer sees is every section\n title moving up one step and every page heading moving up two**: a section title\n goes from 1.5rem to 2.25rem and from 1.875rem to 3rem at `sm`, and a page `h1` goes\n from 1.875rem to 3rem and from 2.25rem to 3.75rem at `sm`. No call site changes,\n because the level a Block was already passing is still what decides its size.\n \n **Five surfaces that render a page's own `h1` now ask the same table for its size\n rather than naming a step.** `page-header-01`, `docs-shell`, `blog-post-page`,\n `not-found-page` and `error-page` each wrote a fixed step into a heading that was\n meant to be a page's top-level claim, so with the ceiling lifted they would have\n rendered a page heading a rung or two below the section titles under it. They ask\n `headingSizeClass`, which is what `SectionHeading` and `Cta01` already asked, and\n `Cta01` still carries no literal.\n \n **Four more surfaces that resolved their own heading size, found the same way.**\n A Block that takes a `headingLevel` and writes the size beside it forwards the level\n and changes nothing a reader can see, which is the defect above at the scale of one\n Block. `data-table-01` said `text-lg`, `run-console-01` said `text-base`, and both\n moved the outline without moving the size. `run-stream-01` renders a literal `h2`\n at `text-lg`, which was the floor of the old scale, so a live region's `h2` came\n out below the `h3` introducing the section holding it. `/foundation/themes` said\n `text-3xl`, which had been Display and is now the `h3` step, so that page's `h1`\n rendered a rung below the section titles under it. All four ask `headingSizeClass`\n now, and the three Blocks carry `text-balance` with the size they gained: the ladder\n states weight, tracking and balance as one package and `SectionHeading` carries all\n three at every rung, so a title lifted from 16 or 18 pixels to 24 or 36 is worse\n unwrapped than the one it replaced.\n \n **The Item page's appendix heading is on the article's ladder, and asking the Block\n ladder for it was the wrong correction.** It said `text-xl`, which put an `h2` at 20\n pixels under the prose `h2` at 30 and below the prose `h3` at 24 above it. Asking\n `headingSizeClass` removed the third tier and created a worse one: \"API reference\"\n is a section of an article exactly as \"Overview\" is, so it takes the article's\n ladder, and the Block ladder is a step and a half above it at `sm`. The Item page\n then carried three sections at 30 pixels and an appendix at 48, and the appendix was\n the second largest thing on the page. It is now a bare `<h2>` inside the same\n `.prose` wrapper as the Item's own MDX body, so the article's rules size it at 30\n pixels and it carries no class of its own: every section heading in that article is\n a bare tag the article styles, and a heading naming its own step would be the second\n answer to a question the article has already answered. The table stays outside\n `.prose`, whose descendant rules would put its cells at the body step in the muted\n colour, and the gap above it is the article's own 16 pixels rather than a second\n region's 32.\n \n **The five theme names are card titles, and they compose `CardTitle` to say so.**\n They carried `font-medium` and no size, so they rendered at 16 pixels and 500 under\n a 60 pixel `h1`. Asking the ladder for the size would have been wrong: five themes at\n the `h2` step is five 48 pixel headings over five cards on a page whose own claim is\n the `h1` above them. `CardTitle` already holds the answer the library needs: a card\n title sits at body size, and the element and the visual step are separate questions.\n So the size stands, the tag stands at `h2` because the grid draws no section heading\n of its own, and the weight now comes from the one Component that owns it rather than\n from a literal. What a consumer sees is five theme names one step heavier.\n \n **The two Foundation readers took opposite answers, and the reason is worth more\n than either change.** Both render inside an MDX body, which the site wraps in\n `.prose`, and both used to carry literals, which a utility layer lets win over the\n article's own component-layer rule: `/foundation/colors` published an `h2` at 18\n pixels and an `h3` at 14, below the `h3` above them.\n \n `ColorTokens`' four headings are sections. Two divisions of an article and a\n partition under one of them is a structure, not a list of labels, so they name no\n step and the article decides, which is the one copy of the ladder there is.\n \n `TokenBrowser`'s are not sections, and dropping its literals let the wrong authority\n decide them: every group name came out at the prose `h2`, 30 pixels, which is more\n than twice the 14 pixels the navigation rail's own section titles are set at, on the\n page whose whole job is being navigated. Each of those 184 names is a table's name\n and nothing else, so they are captions: `h3` at `text-sm`, in the mono face because a\n token key is machine notation, at the same 14 pixels and 500 as the column heads of\n the table underneath. That is the caption `ApiTable` already renders for a compound\n export's part names on every Item page, so this is the repository's own treatment\n rather than a third one. One heading on the page, \"Semantic contract\", is a division\n of the article rather than a label, and it keeps naming no step.\n \n **`Prose` walks the same ladder from one rung down, and a prose heading therefore\n sits below a Block heading at the same level.** Its child treatments now set `h1`\n at `4xl` and descend one authored step per level to `lg` at `h5` and `h6`,\n which is the same scale and the same floor entered at the rung that fits the\n reading measure: Display is 3rem, and a 42rem column is fourteen of those. The new\n `h5` and `h6` treatments are new, so a document that reached past `h4` before now\n has an answer rather than inheriting nothing. **The consequence is stated rather\n than left to be found.** A Block-composed `h2` is 2.25rem and 3rem above `sm`; a\n prose `h2` is 1.875rem and does not grow. They are 1.2 apart at the base width and\n 1.6 apart above `sm`, where they were 1.25 apart before this change, so a consumer\n who sets a `Prose` section beside a Block section sees two tiers rather than one.\n `DESIGN.md` under Typography, Hierarchy now says so, and says which to reach for.\n `DocsShell` runs `Prose` at `fullWidth`, so the frame decides the width: 35rem of\n article track at a viewport of 1216px or wider, and less below that, which makes\n the case for entering one rung down stronger there rather than weaker.\n \n **`Heading`'s free `size` prop is unchanged, and this is stated rather than\n implied.** `size` is a choice made beside the tag, for a heading the surrounding\n document has already placed and wants attuned, and its largest arm is a step of\n the scale rather than a rung of the ladder. Its two largest arms were not removed\n and no new ones were added, so no consumer's call site changes and there is still\n no second route to a page `h1` from inside the library. `search-page` and\n `status-page` are the two surfaces that take a heading's *tag* from\n `childLevel(headingLevel)` and its size from this prop, and they are left as they\n are: those are labels over a list of results and over a list of incidents, and a\n caption is not a rung.\n \n **`scripts/check-heading-scale.mjs` now holds three things it described but did not\n check.** Its rule 4 claimed a table that jumps is a finding and only tested for a\n table that rises, so a table that skipped a rung passed it: descending, reaching\n the ceiling and flooring at or above Body, and rendering the page `h1` one gap\n above its own `h2`, which is the defect these steps exist to remove. And the\n literal Display step it refused on `Cta01` was written out as `text-3xl` and\n `sm:text-4xl`, which stopped being Display the moment the ceiling moved. Both are\n now read from the authored scale and the shipped table rather than restated, and\n both have a staged fixture in `scripts/__tests__/heading-scale.test.mjs`. The third\n is the Item rule below, which is new rather than a restatement.\n \n **The Item document's own table was the retired ladder, and it now states this\n one.** `apps/site/items/component/layout/section/section.mdx` is the page a reader\n is on when they ask what `as` resolves to, and it listed `h1` at `text-3xl` and\n `h4` through `h6` all at `text-lg`: the table as it was before this change. It now\n states the ladder the Component renders, and the sentence under it about where the\n scale stops has been corrected with it, because a corrected table beside a stale\n paragraph is the same disagreement one row down.\n \n **The gate also reads the Item document now, and that is the third copy of the\n table.** `apps/site/items/component/layout/section/section.mdx` states the ladder\n in its prose, and neither the declaration build nor the corpus reaches it, so when\n the ceiling moved it kept publishing the retired table. The new rule compares it to\n the same `HEADING_SIZE` the JSDoc is compared to, by the same cell-for-cell shape,\n so it names no step of its own; it reads the whole Item rather than a slice, so a\n second table in that prose is a finding; and it fails an Item that states no table\n at all, because a comparison with no rows finds nothing to disagree with. Three\n staged fixtures cover the three cases.\n \n **What the gate still does not hold is written down rather than left to be\n discovered.** `Cta01` is named as a root because it is the one Block whose heading\n cannot be composed from `SectionHeading` at all, so a literal step on it has no\n table to fall back to. Beyond that file the gate cannot go, and the reason is\n stated rather than left to be found: whether a heading a Block renders is a rung of\n the ladder or a caption inside that Block is not decidable from source.\n Forty-eight headings across `packages/ui/src` and `apps/site/src` name a step of\n their own, the eleven rungs among them now ask the table, and the thirty-seven that\n remain are captions. Two surfaces bind a caption to `headingLevel` rather than to\n `childLevel`, so a rule reading the tag cannot sort them, and a root list holding\n the rungs would be a list somebody maintains. That is how the `Cta01`-only rule\n came to hold the one Block already fixed and pass over the four that were not, and\n the count is written into `docs/quality-gates.md` so the next reader starts from it.\n `TokenBrowser` adds one more to that population, which is the point of naming a\n caption's step rather than inheriting it: the scan can see it, and it is below the\n floor by arithmetic.\n \n **The limit of the scan that took that count is now written down too, and it is the\n more useful half of it.** The count came from looking for a literal size utility\n beside a heading tag, and a heading whose size it inherits names nothing: the five\n theme names above carried no size at all, and two scans of exactly that shape passed\n them at 16 pixels and 500. A size that is inherited is a size nobody chose, and \"no\n literal\" is invisible to a scan that looks for literals, so that count has never\n been a count of every heading that names a step, and the number nobody chose is the\n half of it that reached a reader. What the count is good for is the arithmetic: a\n heading it reports is a rung if the step it names is at or above the step Body is set\n at, and a caption if it is below, so the two sort without anybody deciding which is\n which. The remedy for what the scan cannot see is on the surfaces rather than in the\n gate, and it is the one the token inventory now uses: a caption names its own step,\n so it is visible to the scan and it is below the floor by definition.\n \n The bump is a minor in both packages rather than a patch because a new authored\n step is a new public contract: `--text-5xl` and `--text-6xl` with their\n `--line-height` companions enter the emitted CSS and the published pack contract,\n and every heading on every page composed from a Block moves.\n- 1a0ba35: `BlogPostPage` refuses a post whose display date is its machine date\n \n Measured on `www.nanisoft.com/blog/where-the-products-stand`: the page rendered the date\n as `2026-09-26`, the raw machine value. All four NaniSoft sites pass one raw ISO string to\n both `date` and `dateTime`, so the reader was handed a machine value and the `datetime`\n attribute was handed the same one.\n \n **The contract stands and the Page now holds it.** `date` is the words a reader sees and\n `dateTime` is the machine value on the `time` element, and they are separate props\n precisely so the two can differ. Formatting is a locale decision, so it belongs to\n whoever owns the fact, and Prism renders `date` verbatim. What was missing was any way for\n the library to notice when a caller did not do that: a prop called `date` that takes a\n `string` is not obviously wrong, the defect is invisible to every reviewer who does not\n open the page, and a JSDoc block is read only by whoever is integrating the library. So\n the Page now throws when `date` and `dateTime` are the same string, naming the fix in the\n message. `StackGrid01` refuses its own missing `ownLabel` for the same reason and by the\n same mechanism, and a rule that is only written down is a rule four repositories read and\n none of them are caught by.\n \n **The refusal is on the pair, not on the shape of either value.** A site that writes\n `2 Sep 2026` and a site that writes `2026-09-26` both pass. What fails is handing the\n Page no display formatting at all, and the machine value is already on the element in the\n `datetime` attribute for every feed reader and every index, so displaying it a second\n time is a redundancy rather than a choice.\n \n **`dateTime` is typed as the shape it is**, `` `${number}-${number}-${number}` ``, so a\n formatted string passed there is a compile error rather than an attribute no reader can\n check. `date` stays a `string`, and the reason is in the Component's JSDoc: no TypeScript\n type separates `21 September 2026` from `2026-09-26`, which is exactly why the refusal\n above has to exist at run time.\n \n **This is a breaking change and it is released as a minor.** It breaks every consumer at\n build time, so it is a break and not an additive change, and each of the four call sites\n below has to change in the same release:\n \n - `alphalens/app/blog/[[...slug]]/page.tsx`, lines 121 and 122\n - `atlas/app/blog/[[...slug]]/page.tsx`, lines 137 and 138\n - `landing-page/app/blog/[[...slug]]/page.tsx`, lines 119 and 120\n - `nexus/app/blog/[[...slug]]/page.tsx`, lines 135 and 136\n \n Each is the same two lines, `date={page.data.date}` and `dateTime={page.data.date}`, and\n each needs a formatted string in the first. `CONTRIBUTING.md` says a breaking change is a\n `major`; `DESIGN.md` records the decision that a minor is the right line for a break from\n a `0.x` version, with the reasoning that taking `1.0.0` would declare the public API stable\n rather than describe the change. That reasoning is dated and deliberate, so the minor is\n taken here and the disagreement with the contributing guide is recorded rather than\n silently resolved.\n \n `DESIGN.md` states the rule once, in **The composition layers** beside the other Page\n rules. Nothing else about the Page changed: same props, same elements, same `data-slot`\n values, and it is still a server Component with no client code and no router.\n\n### Patch Changes\n\n- 1a0ba35: `Diagram` labels stay legible on a phone, and no stroke crosses one\n \n Two defects, both measured on the five-node architecture drawing the company\n site renders. Its node names were drawing at 6.95 CSS pixels and its relation\n words at 5.88 in the 342 pixels a phone leaves the drawing, and ten of the\n twenty-five label and stroke pairs on that drawing intersected: all five node\n names were struck through by an edge leaving their own node, and all five\n relation words sat on their own line.\n \n **Labels now have a floor, and it is the drawing's own coordinate space.** Type\n inside a scaled `<svg>` is sized in user units, so a label's rendered size is its\n size in user units times the rendered width over the canvas. That is why the\n canvas is fixed, and it is not the whole answer, because it has no floor in it.\n The drawing is now never rendered narrower than 640 pixels, one user unit is\n therefore never less than one pixel, and the smallest label the drawing sets is\n twelve units, which is what `text-xs` resolves to. A container that cannot give\n it that much scrolls sideways rather than letting the labels fall through it,\n which is the arrangement `Table` already makes with seven columns and `Gantt01`\n with a schedule of names and bars.\n \n The cost is stated rather than hidden: on a phone a reader sees about half the\n drawing at a time and pans for the rest. The alternative was the whole drawing at\n 5.88-pixel type, and `DESIGN.md` records that trade under **The figure floor**.\n Nothing above 640 pixels of container changes at all, so no desktop or tablet\n rendering moves.\n \n **Strokes now stop where the ink runs out, and each label is asked on its own.**\n A node's name is printed under its mark, so a relation drawn centre to centre ran\n through the name under whichever node it left downward. The first version of this\n fixed that by trimming every stroke back to the box enclosing the mark and every\n label beside it, which is also wrong in the other direction: a relation along a\n row never touches the name printed under its mark, so backing it off by the\n half-width of that name detached it from the mark it connects. On three things on\n one baseline with names at length, every end of every relation stood sixty units\n clear of its mark. A relation is now trimmed against whichever comes first, its\n own mark's edge plus the clearance or the far side of an individual label the\n stroke actually runs through, so a relation leaving sideways reaches its mark and\n one leaving downward still stops below the name in front of it. The whole line\n between the two marks is divided once rather than each end trimmed alone, because\n a name and a subtitle sit below their mark at two depths and a stroke arriving\n from below runs through the first and back into the second.\n \n The box is computable from a server Component because every label in a drawing is\n set in the monospaced face, where a label's width is its character count rather\n than a measurement. That is what `DESIGN.md` records under **The ink-avoidance\n rule**, with the reason a halo was rejected and the direction clause that the\n first form of the rule was missing.\n \n **The drawing is wrapped in a container.** It carries `data-slot=\"diagram\"` and\n still scrolls, and the wrapper carries `data-slot=\"diagram-container\"`, following\n `Table`'s `table-container`. Every existing `data-slot` value, the derived or\n passed `aria-label`, `role=\"img\"`, and `data-unresolved-relations` are unchanged,\n so no call site and no test that addresses the drawing by slot needs to move. The\n Component is still a server Component with no client code.\n- 1a0ba35: `FactList` sets its values flush left, and says why there is no numeric mode\n \n Rendered on `www.nanisoft.com/about`: a three-row fact list where every value was set\n `text-align: right`. The middle answer is long enough to wrap, so its second line began\n wherever the first one ended and the column read as ragged-left, which is what a mistake\n looks like rather than what a decision looks like.\n \n **Right alignment is the right answer in a column of figures, and `FactList` has no\n figures.** That was the question to settle before changing anything, because the two\n answers need opposite fixes. `FactList` takes `label` and `value` and nothing else: there\n is no `numeric` prop, no `tabular-nums` anywhere in the module, no monospaced arm and no\n per-row alignment of any kind. So nothing in it is a figure, nothing lines up on its last\n digit, and what right alignment moved was the first letter of each answer to a different\n place on every row. The suite asserts the absence as a fact rather than restating a prop\n list, so a `numeric` prop added later fails there instead of quietly making the default\n wrong for every caller that never passes it.\n \n The value is now `text-left`, and `text-balance` stays, because it is the same reason it\n was there: a two-line answer should break where it breaks well rather than where the\n measure runs out.\n \n A caller that genuinely wants a right-aligned column of figures is not refused anything.\n `value` is a `ReactNode`, so the caller can pass the span it wants, and Prism holds no\n opinion about a value it did not lay out.\n \n Nothing else about the Component changed: same element, same props, same `data-slot`\n values, same server Component with no client code.\n \n **One thing found and not changed, because it is a different claim.** The Component's own\n JSDoc says the term column is a fixed fraction rather than a content width, and the code\n says otherwise: the `dt` carries `shrink-0` and no basis, so the term column is sized by\n its content and the left edge of the value column is therefore not straight down the\n list. That is a real mismatch between a documentation source and the thing it documents,\n and left-aligning the value does not make it worse or better. Fixing the code to match\n the sentence would move every fact list in four consumer repositories, so it is a decision\n for the maintainer and not a side effect of this one. Recorded here rather than made.\n- 1a0ba35: `StackGrid01` lifts both arms of the grid to the type floors\n \n Measured on `nexus.nanisoft.com`: the in-house arm set its tile names at 14 pixels and\n the line under them at 12, so a heading on this Block rendered smaller than the copy\n around it and the tile read as a caption rather than as a tile.\n \n **Both arms are raised, and the reason is a floor rather than a comparison.** `DESIGN.md`\n already states both, which is why this is a Component coming into line rather than a new\n rule:\n \n - A tile name is a card title, and `CardTitle` says a card title sits at Body. Body is\n `text-lg` in this system, 18 pixels, so a name at 14 was below it.\n - A role and a blurb are supporting copy, and `DESIGN.md` gives supporting copy\n `text-sm`, 14 pixels, so a line at 12 was below that too.\n \n The first is the floor the heading ladder stops to avoid: `lg` is the deepest authored\n step that is not smaller than Body, precisely so that a heading never renders smaller\n than the copy it introduces. A tile name at 14 against copy at 18 broke exactly that.\n \n **The two arms set the same steps in both, which is a fact and not a comparison.**\n They already agreed here, so this was not a reconciliation and the reported arms\n never disagreed: what agreed was below both floors. The difference the two arms are\n supposed to carry is a claim about provenance, and it is carried in weight and ink,\n the muted tile against the page with a `border-primary` hairline and the `md` shadow\n step. A second group set quieter than the first would be a grid claiming the in-house\n work matters less than the assembled parts, which is the opposite of what the group is\n for. The assertion in the suite states the agreement as a fact so it cannot drift.\n \n The mono annotation over an in-house name and under a codename is left at `text-xs` on\n purpose. That is a machine annotation in the platform monospace stack, which is what\n `DESIGN.md` uses the mono face for, not a run of prose, and it is the only type on the\n tile that is not prose.\n \n No `data-slot` changed, no prop changed, no field was added to either arm and neither\n arm was widened, and the Block is still a server Component with no client code. The\n tile's own height grows by the four pixels of the name plus the two of the line, which is\n what the floors cost and not a change of rhythm: the grid was already `h-full` per row, so\n a row of tiles is as tall as its tallest and both arms move together.\n- 1a0ba35: `SiteFooter` links are a 24 pixel target instead of a 17 pixel one\n \n Measured in the built exports of three consumer sites: six to eight destinations per\n footer, every one of them 17 pixels tall (`39x17`, `30x17`, `42x17`, `33x17`, `70x17`,\n `37x17`), with the code a reader can see at\n `https://www.w3.org/TR/WCAG22/#target-size-minimum` asking for 24 by 24.\n \n **The link had no box.** A bare inline anchor is not a 17 pixel target by design; it\n has no box at all. What a pointer aims at on one is the font's content area, and for\n the shipped face at `text-sm` that is 16.94 pixels: fourteen pixels of type times the\n face's own ascent and descent, times its size-adjust. Those three numbers are not a\n guess, they are read out of the `@font-face` this package ships for `Inter Fallback`,\n and reading them is what produced 16.94 against the 17 in the export. The social row\n was the other shape and the other number: an `inline-flex` box, so its height is its\n line box, 20.01 pixels.\n \n **The floor is paid on the box, so the link looks the same.** Both links are now\n `flex min-h-6 items-center`: a block at least `--spacing-6`, 24 pixels, tall, with the\n same fourteen-pixel type at the same weight in the same colour inside it. It is the\n arrangement `mobile-nav` already uses for a stacked navigation link at the\n coarse-pointer floor, one step lower and at every pointer rather than only on a coarse\n one. `--spacing-6` is arithmetic on the authored spacing multiplier the way every\n `size-*` and `h-*` in this package is, so a retune of the scale moves the floor and the\n Block could not have pinned it.\n \n **No height anywhere moves.** A footer column link sat in a line box built from the\n footer's inherited sixteen-pixel body, so each row was already 24 pixels tall before the\n floor and each row is the same 24 after it; only the 17 pixels of target inside the row\n grew. The footer's own height is unchanged at both widths, which is the outcome four\n consumer pages are composed against, and the assertion that says so is in the suite.\n \n The 24 pixel circle exception in the criterion arguably held, because consecutive rows\n in a footer column stand 32 pixels apart centre to centre and a 24 pixel circle on\n either clears the other. The fix is made anyway, and `DESIGN.md` records why under\n **The target-size floor**: a target whose size depends on the gap beside it is one edit\n away from failing and nothing in this repository would report it.\n \n No `data-slot` changed, no prop changed, no call site moves, and the Block is still a\n server Component with no client code. `DESIGN.md` states the rule once, in the section\n beside **The coarse-pointer floor** it is deliberately not merged with.\n- 1a0ba35: `DocsShell` marks where its rails cut off, drops the rows it cannot name, and reaches a tablet\n \n Three defects on the one documentation screen the other NaniSoft sites import.\n \n **The contents rail hid a third to a half of the navigation behind an invisible\n scroll.** The rail is capped against the viewport and scrolls inside that cap, and\n Chrome's scrollbars are overlay scrollbars, so they appear only once the reader has\n already scrolled. Measured in two consumers' built exports at 1280 by 680, the rail's\n content stood at 912 and 1318 pixels in a 568 pixel box: 344 and 750 pixels of a\n primary documentation navigation below the fold with `mask-image: none`, no\n persistent scrollbar, no fade and no count. The last visible row was cut mid-word\n above dead space, which reads as a rendering fault rather than as a scroll region.\n A cap nobody can see is not a cap.\n \n **The affordance is a fade across the foot of each rail, and the space it covers is\n reserved rather than borrowed.** The sticky offset moved to a wrapper that does not\n scroll, because nothing inside a scroll container stays put while that container\n scrolls: a gradient or a mask written into the scrollport travels with the content\n and marks nothing. The wrapper travels, so the gradient is pinned to its foot, and\n the `<nav>` inside keeps the cap, the independent scrolling and the whole of its\n class contract. The rail reserves `padding-bottom` of the same height as the fade,\n which is the half that makes it honest: at rest the gradient falls on reserved space\n and the last row is fully legible, and only while there is more below does it fall on\n content. Without that padding the same gradient washes the last row permanently,\n which is the usual cost of this answer and the reason it is not optional.\n \n Two alternatives were rejected for stated reasons rather than passed over. A\n persistent themed scrollbar is truthful at both ends and costs nothing to\n legibility, but it says \"there is more\" without saying which way, and it needs its\n gutter reserved in a 15rem rail so a short tree becoming a tall one does not shift\n the content. A collapse control, and an \"N more\" control, both need state or a count\n this Page cannot have: one is a client Component and the other is a measurement a\n server Component cannot take.\n \n The fade costs a keyboard and a screen-reader reader nothing. It is an empty element\n with `aria-hidden` and `pointer-events-none`, inside the `<nav>` and outside the\n `<ul>`, so it is not a list item, it is never announced, it never takes a press and it\n never reaches a Tab stop. The region still scrolls on its own, so the arrow keys walk\n every entry exactly as before.\n \n **The Page can emit a navigation row containing nothing, and now refuses to.** An\n entry whose `title` is the empty string rendered as\n `<li data-slot=\"docs-nav-page\"><span data-slot=\"docs-nav-label\"></span></li>`: a row of\n nothing inside a `<nav>`, which one consumer measured at 102 across 25 of its 31\n documentation pages, because that site maps a heading whose title arrives as a React\n element to `''`. A blank row is not a destination, and it is not a label either,\n because a label is words. The Page now drops the entry, drops a group label it cannot\n name while keeping the pages under that group, and drops a group that has neither\n words nor pages. Nothing is swallowed: the rail carries the tally as\n `data-unnamed-entries`, the same answer `Diagram` gives a relation it cannot resolve,\n so a consumer can tell \"nothing was wrong\" from \"the Page stopped counting\".\n \n A blank `title` and a blank `href` are still answered differently, because they have\n different amounts left. A page with an address and no words is a real page with one\n unrenderable field, so losing the row costs a reader nothing while taking the screen\n down over it would cost them everything. A page with words and no address is a row\n whose entire content is a route that does not exist, and that is still refused by\n name.\n \n **Below `lg` there was no navigation at all.** At 768 and at 1024 both rails were\n `hidden` and the only navigation left inside the article was the two-item pager at\n its foot, so a reader on a thirty-one document reference at tablet width had no\n contents, no on-this-page and no way to a sibling page except Previous and Next. Each\n tree is now also drawn behind a `<details>`, first in the document so it is what a\n narrow reader meets before the text. `<details>` is the disclosure that needs no\n runtime: the platform holds the expanded state, the control is in the Tab order with\n a real expanded state, and the Page stays a server Component with no hook, no context\n and no handler. The summary carries `navLabel` or `tocLabel`, which is the caller's\n own word for that navigation, so the Page ships no reader-facing copy and a\n consumer that files its documentation in another language gets the control in that\n language by having named the region once.\n \n The tree is therefore in the document twice, and only one copy is displayed at any\n width: the rail is `hidden lg:block` and the disclosures are `lg:hidden`, so the\n other is `display: none` and is out of the accessibility tree and out of the tab\n order rather than a second list a reader meets.\n \n **What did not change, and what a consumer may notice.** Every existing `data-slot`\n value stays, so no call site and no test that addresses the Page by slot needs to\n move; three new slots are added, `docs-rail-fade`, `docs-nav-compact` and\n `docs-nav-disclosure`. The rail's `<nav>` now sits inside a wrapper, which is what\n lets the fade stay put, so a consumer stylesheet reaching for it as a direct child of\n `[data-slot=\"docs-rail\"]` reaches the wrapper instead. `data-slot` is documented as\n markup metadata rather than a styling API, and `Diagram` and `PulseGraph` took the\n same wrapper for the same reason in the same release line.\n \n The bump is a patch rather than a minor because nothing a consumer composes through\n widens: no prop is added, no export is added, no token and no name in the contract\n move, and no call site changes. What changes is what the Page draws from data it\n already accepted.\n \n **The one thing this leaves alone is stated rather than left to be found.** Below\n `lg` the frame is a single column and this Page runs `Prose` at `fullWidth`, so the\n frame decides the track: the article column is the whole container, which is 45rem at\n 768 and 61rem at 1024, against the 35rem it gets at 1216 and above. That is a wider\n measure than the one the reading column is held to, in the band between the two\n breakpoints, and it is a frame decision rather than a `Prose` one. A phone at 390\n gets 21rem and is inside the measure. Capping the article track below `lg` is a\n deliberate layout choice with two honest forms, a centred column at the measure or a\n two-column frame from `md`, and either one changes what a narrow reader sees on\n every documentation page in the family, so it is left to the maintainer rather than\n taken here beside two defects that had no such choice in them.\n- 1a0ba35: `PulseGraph` labels stay legible on a phone, and no stroke crosses one\n \n Three defects, all measured on a four-stage pipeline the way `Diagram`'s two were\n measured on the five-node architecture drawing the company site renders.\n \n **Labels now have a floor, and it is the drawing's own coordinate space.** Type\n inside a scaled `<svg>` is sized in user units, so a label's rendered size is its\n size in user units times the rendered width over the canvas. That is why the\n canvas is fixed, and it is not the whole answer, because it has no floor in it. In\n the 356 pixels a phone leaves the drawing, a stage name was rendering at 7.23 CSS\n pixels and a node's note at 5.56, and ten user units was the smallest type the\n whole package emitted anywhere, including inside a figure. The drawing is now\n never rendered narrower than 640 pixels, one user unit is therefore never less\n than one pixel, and the smallest label the drawing sets is twelve units, which is\n what `text-xs` resolves to. A container that cannot give it that much scrolls\n sideways rather than letting the labels fall through it, which is the arrangement\n `Table` already makes with seven columns. Nothing above 640 pixels of container\n changes at all, so no desktop or tablet rendering moves.\n \n **Connectors now stop where the ink runs out, and each label is asked on its own.**\n A node's name is printed under its mark, so an edge drawn centre to centre ran\n through the name and the note under whichever node it left downward. On the\n pipeline this fix lands on, eight of the thirty-two label and connector pairs\n intersected: the two stages nearest the top of the canvas, each with its own name\n and its own note struck through by the two edges that met it. The first version\n of this trimmed every connector back to the box enclosing the mark and every\n label beside it, which fixes the crossings and detaches the drawing instead: a\n stage's name and note both sit below its mark, so a connector along the rail\n touches neither, and backing it off by the half-width of the widest name at the\n node left four of the five connectors on the six-stage figure floating free of\n both their marks, by up to fifty-five units. A connector is now trimmed against\n whichever comes first, its own mark's edge plus the clearance or the far side of\n an individual label it actually runs through, so all five connectors on that\n six-stage figure touch both their marks with the clearance between them and the\n stroke is thirty to fifty units longer, and a connector leaving a node downward\n still stops below the name in front of it. The whole line between two marks is\n divided once rather than each end trimmed alone, because a name and a note sit\n below their mark at two depths and a connector arriving from below runs through\n the first and back into the second.\n \n You do not have to space your nodes to avoid this, and you always had to space\n them to keep two names from touching each other.\n \n **The rail no longer sits on a note.** The rail was a fixed distance up from the\n bottom of the canvas while a node's note reached a fixed distance below its mark,\n so the two met on one range of mark positions: a lane whose mark landed low enough\n put its note under the rail's own stroke. The fitted area now stops above the band\n the rail reserves, which is the rail's distance up from the bottom plus its own\n thickness, the drawing's ink clearance, and the deepest ink a node draws. A graph\n with no lanes has no rail, so nothing reserves the foot and the fit is unchanged.\n \n **The drawing is wrapped in a container.** It carries `data-slot=\"pulse-graph\"` and\n now scrolls, and the wrapper carries `data-slot=\"pulse-graph-container\"`, following\n `Table`'s `table-container` and `Diagram`'s `diagram-container`. Every existing\n `data-slot` value, the `role=\"img\"`, the passed `aria-label`, `data-lanes` and\n `data-unresolved-relations` are unchanged, so no call site and no test that\n addresses the drawing by slot needs to move. The Component is still a server\n Component with no client code.\n \n `DESIGN.md` records the two rules this applies under **The figure floor** and **The\n ink-avoidance rule**, the latter with the direction clause that the first form of\n the trim was missing, and the geometry both Components now share is one internal\n module rather than a second copy in each.\n- Updated dependencies [1a0ba35]\n - @nanisoft/prism-tokens@0.16.0\n\n## 0.15.0\n\n### Minor Changes\n\n- 6dc7bb8: A Block action is a destination or your own control, and a gate now holds it\n \n **Five Blocks shipped a control that could not be acted on, and the previous release\n fixed three of them without the law.** `Hero01`, `Hero02` and `Hero03` each declared\n an action union whose `href`-less arm rendered a bare `Button`, and that release closed\n the arm and then said, in its own changeset, that seven sibling Blocks still had it and\n that \"a law held by a type in three Blocks is three types\". That was the right\n reasoning and it was also the reason the hole stayed open: the next audit found five\n more Blocks, and one of them had a shape nobody had looked for.\n \n **`About01` and `Showcase01`** declared the same two-arm union as the heroes and\n rendered a `Button` on the arm with no `href`. Each one's JSDoc called it \"inert by\n design\" and defended that as a Block shipping no behaviour, which is true and is not an\n answer. Both are now a union of two named arms: `AboutLinkAction` /\n `AboutSlotAction` and `ShowcaseLinkAction` / `ShowcaseSlotAction`. `href` is required\n on the link arm; the slot arm carries the caller's own `ReactNode` and forbids `label`,\n `href`, `newTab` and `variant`.\n \n **`PageHeader01` was worse, and its own type said so.** `PageHeaderAction` had **no\n `href` member at all**, so there was nothing to make optional: `{ label: 'Share' }` was\n the only value the type could express and every value rendered a `Button` that\n activated to nothing, at the top of every screen a consumer rendered. It is now the\n same two-arm union. Its `actionsSlot` sibling prop is **gone**, and that is the one\n structural change here: a sibling slot renders before the row's own actions and cannot\n say which position it fills, so a caller who wanted a primary menu trigger at the front\n and a link behind it had to put the link first and accept the order, or pass the trigger\n in `actions` where it was a dead button. As an arm, position and element are the same\n value.\n \n **`Pricing01` was the one no audit had found.** `Plan` carried a required `cta: string`\n and the Block rendered it as a `Button`, so every pricing table in every consumer's\n product shipped a focusable control, announced as a button, that activated to nothing, on\n the card a reader was about to decide on. `cta` is replaced by `action`, a two-arm union,\n and a plan whose control is a checkout trigger is the `slot` arm. The featured plan\n still draws a filled anchor and the rest an outlined one, so the recommendation still\n reads. `pricing-01/index.tsx` also exported the value alone until now, so a consumer\n could not name `Plan` at all; `Plan`, `Pricing01Props` and the three action types are on\n the surface.\n \n **`Plan` now carries a required `id`, which is the second change to one record and was\n deferred from this entry once.** The first version of this changeset recorded the\n observation and the reason for leaving it: *\"a second breaking change to a record this\n release already changes, and bundling two migrations into one entry makes both harder to\n read. It is a follow-up, not an oversight.\"* Both halves of that reasoning have since\n stopped applying, and the second one was never quite true.\n \n The `id` is real. `Pricing01` keyed its cards on `plan.name`, which is the word a reader\n reads on the card, so a monthly and an annual row of the same tier produced a React\n duplicate key and cards the renderer had been told are ambiguous, and a rename in the\n copy was a rename in the key, which loses a caller's saved selection over an edit to a\n sentence. `Careers01` and every other Block in this package key on an `id` and say why;\n `Pricing01` was the last one that did not.\n \n The bundling reason does not survive contact with the release. There is no released\n boundary between the two changes, because both are in this same unreleased body of work,\n so \"bundling\" describes nothing a reader would ever have to unpick: they upgrade once\n and read one entry either way. And one `Plan` with two migrations in one place is more\n readable than one `Plan` with two migrations in two places, because the second entry has\n to re-establish that it is talking about the same record. It is in this entry for that\n reason and the deferral was the mistake, not the delay.\n \n **`Waitlist01` is the fifth, and it is not a union.** `Waitlist01Referral` declared\n `copyLabel` and `copied`: the accessible name of a copy control and the state to draw on\n it. The Block then rendered `<Button type=\"button\">{copyLabel}</Button>` with **no\n handler**, beside a read-only field holding the code it was labelled as copying. The\n JSDoc described a second weight and a `data-copied` attribute for a control that never\n copied anything. Both props are gone and `copyControl` is a `ReactNode`. Prism will not\n call `navigator.clipboard` on a consumer's behalf and then report a success it cannot\n verify: the permission is the caller's to grant, the secure context is the caller's to\n know about, and a copy can fail. The sentence that says it worked belongs in `status`,\n which is where it already belonged.\n \n **The four Blocks whose union renders nothing on the `href`-less arm were already\n correct, and the audit that named them was wrong.** `Careers01`, `CaseStudies01`,\n `Industries01` and `Services01` each declare an item union whose no-link arm renders **no\n control at all**: the `CtaLink` is inside `isLink ? … : null`. That is the right shape\n for a card, where a link is an addition rather than a required part, and there is nothing\n to repair. `Gallery01` is the same story with a different reason: its tile is a real\n `<button>` with an `onClick` that opens the lightbox, in a `'use client'` Block. Five of\n the nine names in the earlier changeset were false positives, and saying so is part of\n the record: a list of sites is a claim about a tree, and the tree is what settles it.\n \n **`scripts/check-block-controls.mjs` is the gate, and it is green.** The previous release\n declined to write one because it would have failed on five real Blocks, and that was the\n right call at the time. It can be green now, so it exists. It reads `packages/ui/src/blocks`\n and `packages/ui/src/pages` and fails on a rendered `Button` or `CtaLink` that carries\n none of `onClick`, `type=\"submit\"` or `type=\"reset\"`, and no `href`. It deliberately does\n **not** read `apps/site/items`, because the documentation Demo for `Button` renders\n `<Button>Save changes</Button>` with no handler and showing the control is the whole\n point of showing it; the demos are held by the compiler, because a demo passes a Block's\n props and fails to build the moment `AboutAction` stops accepting a dead action. The\n gate's own proof is `scripts/__tests__/block-controls.test.mjs`, which plants each of the\n five shipped shapes as a fixture, because the five Blocks that shipped them have since\n been fixed and a gate whose only evidence is a clean run is a gate nobody has watched\n fail.\n \n Two things the first version of that gate got wrong are recorded because they are the\n kind of defect a clean run hides. It deleted comments rather than blanking them, which\n removed their newlines and printed every finding below the first JSDoc block on the\n wrong line. And it matched a tag's attributes with `<Name([^<>]*)>`, which stops at the\n first `<` or `>` in the attribute text, which in JSX is very often not the end of the tag:\n `onClick={() => step(-1)}` contains `=>`, `disabled={position <= 0}` contains `<=`, and\n `disabled={currentPage >= pageCount}` contains `>=`. Each truncated the attributes before\n the handler two lines further down, so three Blocks with working handlers were reported\n as shipping dead buttons. The tag end is found by brace and paren balance now.\n \n **The bump is `minor` rather than `major`, and the argument is the version line rather\n than the severity.** Five Blocks' public prop types change and two of them (`Plan.cta`\n and `Waitlist01Referral`) do not compile against a value that compiled a release ago.\n That is breaking by any ordinary reading and this entry says so plainly. `major` in this\n repository publishes `1.0.0`, and every release to date has been `minor` on a `0.y.z`\n line where the `y` is already the breaking-equivalent slot; publishing 1.0.0 with this in\n it would assert an API stability guarantee this library has not earned, and it would be\n the first release in the project's history to use the bump at all. The same argument\n carried the hero change and the `Cta01` change at 0.6.0. The break is real and is the\n subject of this entry; the number it lands on is the line's decision.\n \n Migration, per Block:\n \n ```tsx\n // About01, Showcase01, PageHeader01: an action with no href\n // Before\n actions={[{ label: 'Start free' }, { label: 'Docs', href: '/docs' }]}\n // After\n actions={[\n { slot: <NextLink href=\"/start\">Start free</NextLink> },\n { label: 'Docs', href: '/docs' },\n ]\n \n // PageHeader01 only: actionsSlot is gone, and the node moves into the row\n // Before\n <PageHeader01 actions={[{ label: 'New deployment' }]} actionsSlot={<Menu />} />\n // After\n <PageHeader01 actions={[{ slot: <Menu /> }, { label: 'New deployment', href: '/deployments/new' }]} />\n \n // Pricing01: `cta: string` becomes `action`, and the card is keyed on an `id`\n // Before\n plans={[{ name: 'Team', price: '$24', features: [], cta: 'Choose Team' }]}\n // After\n plans={[{ id: 'team', name: 'Team', price: '$24', features: [], action: { label: 'Choose Team', href: '/signup?plan=team' } }]\n \n // Waitlist01: the copy control is the caller's, whole\n // Before\n referral={{ value: CODE, label: 'Your referral code', copyLabel: 'Copy code', copied }}\n // After\n referral={{\n value: CODE,\n label: 'Your referral code',\n copyControl: <Button onClick={() => navigator.clipboard.writeText(CODE)}>Copy code</Button>,\n }}\n ```\n \n Every one of these was a compile error before and is a compile error now, which is the\n point: a consumer upgrading finds a failed build rather than a page that silently renders\n nothing.\n \n **One thing about the `Pricing01` migration is separate from the rest of it.** Every other\n line above changed a member a consumer had to add. The `id` also changes what a consumer\n has to add to the same object, so a plan that compiles after this release and renders two\n cards correctly is a plan that passed an `id`. Nothing here needs a second entry, and the\n observation that started this is in the entry above rather than at the end of this one.\n- 6dc7bb8: A disclosure opens at the step the motion scale gives it, and the rail says why it is still a width\n \n `DESIGN.md` has said for a while that `duration-slow` is \"transform or layout\n state such as a disclosure\". Three disclosures animated a height or a width at\n `duration-base`, which is 160ms against the 280ms the scale assigns them, and both\n chevrons rotated at `base` beside a panel that now takes longer than the icon does.\n \n **`AccordionContent`, `CollapsibleContent` and `Sidebar` are on `duration-slow`,\n and so are the two chevrons.** A chevron and the panel it opens are one motion, and\n a 160ms icon against a 280ms panel is two events where the reader is watching one.\n \n **`Progress` stays at `duration-base` and this is the one exception the scale has.**\n A disclosure is a spatial transition that happens once, on a reader's click, and\n 280ms is what makes it read as opening. A progress indicator's value changes on\n every tick of a running job, often several times a second, and the reader did not\n cause it: at 280ms the bar lags the work it is reporting. It is the same\n distinction the two laws of motion draw, applied inside the feedback scale. An\n answer wants the long end of the band; a reading wants the middle of it.\n \n **`Sidebar`'s rail still animates `width`, and it is a named exception rather than\n an unexplained gap.** `Progress` and `RangeField` both express their value as a\n `scaleX` about the inline start, and a rail cannot: it carries a mark, a list of\n items with a 16px icon, each item's label and a trailing count, and a `scaleX`\n scales every one of them into ovals, condensed labels and unreadable numbers. The\n decisive part is that a transform cannot reflow text. At `scaleX(0.25)` the labels\n are still laid out for a 16rem column, so the rail's contents would never reflow\n into the 4rem column they are supposed to occupy, and the only property that does\n that is the one being animated.\n \n The cost is stated rather than argued away: it is a main-thread layout pass, once\n per reader's click, over one element with a small subtree. That is a different\n order of cost from a bar that advances on every frame of a running job, which is\n why the two were not the same decision.\n \n `DESIGN.md`'s motion section records all of it, so the table and the call sites no\n longer disagree.\n- 6dc7bb8: A form-level error marks the field it is about, not every field\n \n `AuthForm01` set `aria-invalid` on every field whenever a form-level `error` existed,\n so a reader tabbing through a sign-in card heard \"invalid\" on a correctly filled\n email address because the password was wrong. A form-level message is usually not\n about a control at all: bad credentials, a locked account and a rate limit are three\n things no field is wrong about, and the first thing a screen reader says about each\n field is the state, so a reader told two correct answers are wrong stops trusting the\n rest of the form.\n \n `AuthFormField` gains `errorId`. The Block marks the field whose `id` the caller\n names there and no other, and a caller with nothing to name leaves every field\n unmarked, which is the honest state: the message is still drawn in an `Alert`, which\n is announced when it enters, and WCAG 3.3.1 is answered by the message rather than by\n a mark on a control that is not the problem. Omitting `errorId` on every field is now\n the way to say \"this is about the submission\", and the JSDoc says so.\n \n **A field's description was drawn and never announced.** The `FieldDescription`\n carried no id and the input carried no `aria-describedby`, so a hint about a format\n or a constraint was on the page for a sighted reader alone. The id is derived from\n the field's own `id`, which is already the one stable caller-owned string on the\n field, so the reference costs a template and cannot collide. `login-01` and\n `signup-01` already wired the pair; this was the Block that had not.\n \n **`SettingsNotifications01` had the same unlinked description.** Each event's\n `FieldDescription` was drawn under the control's visible label and referred to by\n nothing, while the section note was referred to by every read-only switch. The one\n `aria-describedby` now carries both: the event's own description and, on a read-only\n control, the note that says why it will not move. A reader who meets one without the\n other has to work out which half they are missing.\n \n Two notes for a caller. `AuthForm01` renders no `aria-invalid` on any field unless a\n field names itself, so a form that relied on every field being marked has to name the\n one that is. And `SettingsNotifications01` writes the joined pair as a template rather\n than through a `join`, which is the same reason `MultiCombobox` does: a bare\n separator literal is a string the copy gate reads as a space somebody typed.\n \n No prop is removed and no import breaks.\n- 97d9b99: A heading's size follows its level, so a page `h1` and its section titles are no longer one size\n \n **If you composed a page from Blocks, every section title on it just got smaller.**\n An `h2` section title moves from 1.875rem to 1.5rem, and above `sm` from 2.25rem to\n 1.875rem. A page `h1` is unchanged at 1.875rem, and 2.25rem above `sm`. Nothing\n about the props you pass changes, and nothing about the outline changes: the same\n `headingLevel` you already pass now decides the size as well as the tag.\n \n `DESIGN.md` gave Display two roles at once, \"section titles, the CTA banner\n heading, and every page `h1`\", and `SectionHeading` implemented that literally by\n writing one class string for all six levels. So an `h1` and an `h2` came out\n byte-identical, and a landing page of a hero plus six Blocks showed one `h1` and\n six section titles at 36 pixels, with no hierarchy between what the page claims\n and what it elaborates. The level was already on every Block as `headingLevel`\n and `childLevel()` already existed to carry it down a level, so the document\n decided where each heading sits in the outline and the visual size simply never\n followed it.\n \n **The table, and where it stops.**\n \n | level | step | value | above `sm` |\n | --- | --- | --- | --- |\n | `h1` | `text-3xl` | 1.875rem | 2.25rem |\n | `h2` | `text-2xl` | 1.5rem | 1.875rem |\n | `h3` | `text-xl` | 1.25rem | 1.5rem |\n | `h4` | `text-lg` | 1.125rem | 1.25rem |\n | `h5` | `text-lg` | 1.125rem | 1.25rem |\n | `h6` | `text-lg` | 1.125rem | 1.25rem |\n \n It is the authored scale walked down one step per level, and it floors at `h4`.\n `lg` is the deepest authored step that is not smaller than Body, which is 400 at\n 1.125rem, so a heading one step further down would render smaller than the copy it\n introduces and read as a caption. `h5` and `h6` hold at that step rather than\n wrapping, which is the same trade `childLevel()` makes when it clamps at `h6`. A\n heading at the floor is still a heading: it keeps `font-semibold`,\n `tracking-tight` and `text-balance` at every step, so weight and tracking tell it\n apart from body copy where size no longer can.\n \n **Nothing goes above `4xl`, and no scale changed.** The step above Display does\n not exist and this does not invent one, so `DESIGN.md`'s ceiling holds untouched\n and `@nanisoft/prism-tokens` is unchanged. `h1` is Display because that is the\n role the document gives it, and `h2` is the step below.\n \n **No new prop.** The alternative was a `size` on `SectionHeading` and on every\n Block that renders one, which widens the public surface across more than a\n hundred call sites for a decision the surrounding document has already made by\n choosing a level. You get the hierarchy by passing the level you were already\n passing, and a Block moved from an `h2` section into an `h3` one carries its size\n with it, which is what `headingLevel` was introduced to do. Nothing about the\n no-override-path rule changes: a consumer who wanted Display for their thesis\n still has it, at `h1`.\n \n **`Cta01` follows its level too, and this is the one Block outside\n `SectionHeading`.** It draws a centred title on a filled primary panel with\n nothing under it, so the muted description colour and `gap-4` are wrong there and\n it resolves its own heading element. It was carrying the same hardcoded size, so\n fixing the Component and not it would have left a closing banner one step above\n every other section title on the page and level with the page's own `h1`. Its\n banner moves with everything else: an `h2` closing banner is 1.5rem, and at `h1`\n it is 1.875rem.\n \n **The blast radius, measured rather than guessed.** 132 `SectionHeading` render\n sites across 115 files resolve to this table. 6 are at `h1` and are unchanged.\n 125 are at `h2` and each moves from `text-3xl sm:text-4xl` to\n `text-2xl sm:text-3xl`. 1 is at `h3` and moves to `text-xl sm:text-2xl`. That is\n the whole change, and it is deliberately not softened: a documentation site\n whose own prose hierarchy shifts is a visible change, and the point of the fix is\n that the shift is the hierarchy coming back.\n \n **A gate now holds the table, and proof it fires.**\n `scripts/check-heading-scale.mjs` reads the table out of `section.tsx`, judges\n every step in it against the `text` group in the token source rather than a list\n beside the gate, and holds it against the markdown table the Component's own\n JSDoc states, which is the documentation source the declaration build preserves\n and the corpus reads. It fails on a level with no size, on a step the token source\n does not author, on `h1` and `h2` sharing one step, on a step-down table that\n rises, on a level deeper than the page heading rendering above it, on a floor below\n Body, on the heading losing its weight, tracking or balance, on the JSDoc and the\n code disagreeing, and on `DESIGN.md` giving Display the section title as well as\n the `h1`. `scripts/__tests__/heading-scale.test.mjs` stages each of those against\n a tree the gate reads, including the table the Component actually shipped before\n this change, and asserts the gate is red on it.\n- 6dc7bb8: A hero action is a destination or your own control, and never an inert button\n \n `Hero01`, `Hero02` and `Hero03` accepted an action of `{ label }` with no\n destination, and each Block rendered that as a bare `Button`. Every one of those is\n a focusable control, announced as a button, that activates to nothing, and it sat\n where a reader looks first: the primary call to action of a marketing hero, in\n three published Blocks, so every consumer of any of them shipped a dead button\n where the most important action should have been. Each Block's JSDoc called the\n button \"inert by design\" and defended that as a Block shipping no behaviour, which\n is true and is not an answer. A Block that ships no behaviour cannot make a control\n work, so it should not render a control at all.\n \n **The defect was in the type, not in the render, so the fix is a type.** The\n rendering half had already been repaired once: `CtaLink` renders a native anchor\n and `Cta01` requires `href`, which is why the three heroes, predating both, kept\n the old shape. What survived was the third arm, and it could only be closed by\n removing it.\n \n `HeroAction`, `Hero02Action` and `Hero03Action` are now a union of two named arms\n rather than one shape with an optional `href`:\n \n - `HeroLinkAction` (and its two siblings) **requires** `href`, so a value that is\n sometimes a string and sometimes `undefined` is a compile error rather than a\n control that navigates on the renders where the address happens to be there.\n `slot` is forbidden on this arm.\n - `HeroSlotAction` (and its two siblings) **carries `slot`**, the caller's own\n control, and forbids `label`, `href`, `newTab` and `variant`. It exists because\n a server Component cannot receive an `onClick`, so the honest form of \"this does\n something\" in a composed section is a node the caller renders. This is\n `Cta01`'s `actionSlot` moved inside the row, because a hero carries an ordered\n list of up to two actions and a sibling prop cannot say which position it fills.\n `check-block-imports.mjs` already holds that a `ReactNode` slot is how a consumer\n injects an interactive child without the Block owning any state.\n \n The Block places a slot and adds no class to it, because a class it adds to a node\n it does not render is a style the caller cannot see and cannot remove, and this\n package has no override path.\n \n **Nothing else moved.** The anchor is the element the link arm always produced, at\n the same size and the same variant, and the positional default variant still\n follows the position: the first action in the row is filled and the rest are\n outlined, whether the first one is an anchor or a caller's control. The forward\n arrow still marks the row's one primary destination and now, with every action\n Prism renders being a link, the position is the whole of the test. A `slot` at the\n front of the row wears no arrow, because the control the caller drew owns its own\n marks. Each Block is still a server Component.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** `{ label: 'Coming soon' }` compiled a release ago and\n does not now, which is breaking by any ordinary reading, and this entry says so\n plainly. `major` in this repository publishes `1.0.0`, and every release to date\n has been `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent\n slot. Publishing 1.0.0 with this change in it would assert an API stability\n guarantee this library has not earned, and it would be the first release in the\n project's history to use the bump at all. The same argument carried the `Cta01`\n change at 0.6.0. The break is real and is the subject of this entry; the number it\n lands on is the line's decision and the line is pre-1.0.\n \n Migration: every action in a hero now carries either a destination or your own\n control. Where you passed a label alone and meant a link, give it the `href` it\n was always declared with. Where you meant a control that is not a link, a router's\n own `Link`, a submit button or a menu trigger, move it into `slot` as the element\n itself rather than as a label:\n \n ```tsx\n // Before\n actions={[{ label: 'Start free' }, { label: 'Sign in', href: '/sign-in' }]}\n \n // After\n actions={[\n { slot: <NextLink href=\"/start\">Start free</NextLink> },\n { label: 'Sign in', href: '/sign-in' },\n ]}\n ```\n \n Both mistakes are now compile errors, which is the point: a consumer upgrading\n finds a failed build rather than a site that silently renders nothing.\n \n **The same arm was still in seven sibling Blocks when this was written, and\n `a-block-action-is-a-destination-or-your-own-control.md` closes them.** The audit\n behind this entry named `About01`, `Careers01`, `CaseStudies01`, `Gallery01`,\n `Industries01`, `Services01` and `Showcase01` as carrying the same two-arm union with\n a rendered `Button` on the arm that has no `href`, and `PageHeader01` as rendering\n its declared `actions` as buttons with no destination at all. **Five of those nine\n were correct and four were defective, and the later entry records which is which**:\n `About01` and `Showcase01` had the union, `PageHeader01` was worse and had no\n destination member at all, and `Pricing01` and `Waitlist01` were not on the list at\n all. `Careers01`, `CaseStudies01`, `Industries01` and `Services01` render **nothing**\n on their `href`-less arm, which is the right shape for a card, and `Gallery01`'s tile\n is a real button with an `onClick` in a client Block.\n \n This entry stands as the record of the heroes. The shape every Block now takes is the\n one adopted here, and `scripts/check-block-controls.mjs` is what holds it: the earlier\n reasoning in this paragraph, that \"a law held by a type in three Blocks is three\n types\", was right about the risk and wrong about the remedy, and the remedy is a gate\n rather than a shared type.\n- 6dc7bb8: A popup that matched nothing is still an open popup, and its message is not an option\n \n Four Components said \"collapsed\" while a popup was on the page, and two of them put\n the sentence saying so inside the listbox.\n \n `aria-expanded` was reading \"are there results\" rather than \"is the popup displayed\",\n and the two came apart in exactly the state a search field exists to render: a query\n that matched nothing. `CommandPalette` and `Combobox` both drew a bordered panel\n carrying the caller's empty sentence and told the field there was nothing to reach.\n `aria-expanded` is now the open state in all four, because that is what the\n attribute is for and because closing on a non-match tells the reader their keystroke\n broke the control, which `Combobox` already argued in prose.\n \n `aria-controls` pointed at a listbox that was not rendered. `Combobox`,\n `MultiCombobox` and `CreatableCombobox` omit the listbox entirely when nothing\n matched and left the reference written anyway, so the field pointed at an id nothing\n carried. **It now points at the popup rather than at the listbox inside it**, which\n is the decision the existing axe run forced and the right one on its own terms:\n `aria-controls` is a required attribute on an expanded `combobox`, so a field that is\n expanded and carries no reference is itself a violation, and the panel is what the\n field opened either way. `CommandPalette` points at the palette itself for the same\n reason. All four write the reference only while the popup is displayed, so a closed\n field carries none.\n \n **A listbox owns `option` and `group` and nothing else.** `CommandPalette` drew its\n no-results `<p>` inside the listbox and its group headings in a bare `<div>`, so the\n message was a row the index counted and no reader could choose, and the rows under a\n heading could not say what they belonged to. `CreatableCombobox` drew its\n `<p role=\"status\">` in the same place. In all three the empty state is now a sibling\n of the listbox rather than a child of it, which is the arrangement `Combobox` and\n `MultiCombobox` already shared, and each group wrapper is a `group` named by the\n heading already drawn above it through `aria-labelledby`.\n \n `CommandPalette` gains two things to know about: the two branches carry the same box,\n so the panel does not change size or position when the last result is filtered out,\n and its listbox now appears only when there is something in it, so a caller selecting\n `[data-slot=\"command-palette-list\"]` finds nothing in the empty state. The empty\n state carries `data-slot=\"command-palette-empty-state\"` on its box and the message\n keeps `data-slot=\"command-palette-empty\"`.\n \n No prop changes and no import breaks.\n- 6dc7bb8: `Prose` gives a table a scroll container, and the two search fields stop zooming an iPhone\n \n Four defects, three of them narrow-viewport, one of them about a reader's own\n settings. A consumer sees all four.\n \n **A table in a Prose widened the page instead of scrolling.** The Component's\n JSDoc has always listed tables among the content it accepts, and the code fence\n beside them has always scrolled, but the table itself could not: `overflow`\n applies to block containers, and a `display: table` element is not one. A table\n box cannot be a scroll container, so a four-column table in a Markdown document\n pushed the whole page sideways rather than offering a scrollbar.\n \n A `<table>` passed straight to a Prose now gets `display: block`, which is what\n makes it a block container, and the same `overflow-x: auto` the code fence has.\n The grid inside now sizes to its content up to the measure rather than stretching\n to fill it, which is the trade the technique always carries and is the one worth\n knowing about before you rely on it. Collapsed borders are unaffected: the row\n groups are wrapped in an anonymous table box that still inherits\n `border-collapse`.\n \n The treatment is a child selector rather than a descendant one, on purpose. A\n descendant selector would reach a `Table` you have already put in a scroll\n container of your own and set `display: block` on the table inside it, which\n moves the caption off being a caption and stops the grid filling its wrapper. A\n table you have wrapped in your own element is left alone as it was; give that\n element `overflow-x-auto` and it is what scrolls. For a table whose markup you\n control, `Table` remains the right answer.\n \n **`SearchDialog` and `CommandPalette` zoomed the page on focus.** Both drew their\n search field at 14 pixels. iOS Safari zooms the viewport on a focused input whose\n font size is under 16 pixels and does not zoom back out, so a reader who opened\n either surface on a phone was left on a magnified page with no way off it. Both\n now carry the `text-base` then `md:text-sm` pair that `Input`, `Textarea`,\n `Combobox`, `Form` and `NumberField` already carried, which is the same 16 pixels\n at a phone's width and the same 14 everywhere else.\n \n **The bump is `minor` rather than `patch`.** A table in a Prose renders\n differently: a narrow one no longer stretches to the measure. That is a visible\n change to a published Component rather than a fix nobody notices, and a site that\n was relying on a prose table filling its column is the one thing that needs to\n look at it.\n- 6dc7bb8: A tree is one Tab stop, and no node claims to collapse\n \n `Tree` documented a mechanism it did not have and shipped a state it could not\n honour. Its JSDoc said \"a tree is one Tab stop, and the arrow keys move inside it.\n That is the ARIA pattern\" and then described the roving tabindex that makes it true,\n and no `tabIndex` appeared anywhere in the file: a forty-node rail was forty Tab\n stops. A comment in the same file argued the other way, and it was wrong on its own\n terms, because it deferred the keyboard model to \"a caller's own keyboard model\" in a\n Component that does not forward `onKeyDown` and therefore has no caller's model to\n defer to.\n \n The roving tabindex is now implemented, in the shape `ToggleGroup` already uses: the\n stop is an address rather than an index, so a caller who reorders or removes nodes\n does not strand the reader; it lands on the current address when the tree has one and\n on the first destination otherwise; and it follows the reader once the arrows move\n it, so Tab away and back returns them to where they were. The fallback and the\n current address are not two answers: a `currentHref` naming another page is ordinary,\n and reading either rule alone would give the tree two tab stops.\n \n **Nothing collapses, so nothing claims to.** Every group with children carried\n `aria-expanded=\"true\"`, hard-coded, which promises a second press that folds nothing\n away. A node that cannot expand omits the attribute, and `aria-level` is what places\n a node in the tree for a reader who wants that instead.\n \n **Two regions announced their bare role name.** `SelectionToolbar` drew the words\n \"3 selected\" in its leading label and pointed at nothing, so a reader tabbing onto\n the row heard \"toolbar\" and no more; it is now named by `aria-labelledby` on the\n label, which works for the `ReactNode` label the count requires and cannot drift from\n the words on screen. `ToggleGroup` declared `aria-label` optional while its own JSDoc\n said the prop was required, so TypeScript enforced nothing and a group shipped\n unnamed in both of its roles. See the separate entry for the type change.\n \n To know before you style against it: `Tree` now writes a `tabindex` on every\n destination, so a page with two trees has two Tab stops where it had one per node.\n- 6dc7bb8: `Button` is `type=\"button\"`, the carousel leaves the caret keys, and a number field takes one\n \n **`Button` defaults `type` to `\"button\"`.** A `<button>` with no `type` is a submit\n button as far as the HTML is concerned, so the default was that every Prism `Button`\n a consumer placed in their own form submitted it: a Cancel, a Close, a second step of\n a wizard, anything that was not the one control that should. The failure is silent\n and it costs data, which is why the default is stated on the Component rather than\n left to the HTML.\n \n Nothing in this repository changed, and that is worth saying with the evidence rather\n than as an assurance: every `<form>` in `packages/ui` and `apps/site` already states\n its submit button's `type`, which is why no call site moved. The one behaviour change\n is for a consumer who relied on the old default, and the migration is one token:\n \n ```tsx\n // a control that used to submit by default\n <Button>Save</Button>\n \n // now\n <Button type=\"submit\">Save</Button>\n ```\n \n The trade is deliberate. A submit button is a decision someone makes about a form and\n this Component cannot see the form, so making the destructive default the safe one\n and the deliberate act the explicit one is the only arrangement where the mistake is\n the one you have to write on purpose. `ToggleGroupItem`, `MultiCombobox` and\n `SelectionToolbar` already wrote `type=\"button\"` by hand for the same reason.\n \n **`Carousel` answered the arrow keys for whatever was inside a slide.** The key\n handler sits on the region, so every key that bubbled out of a slide reached it, and a\n slide is the caller's slot: it can hold a field, a number field, a `select` or an\n editable region, and in all four Left, Right, Home and End belong to the value. The\n Component was calling `preventDefault()` on them unconditionally, so a reader typing\n a caption moved the carousel instead. It now answers a key only when nothing inside\n is using it, which is the discipline `ResizableHandle` applies to the same keys on\n the other axis. A reader arriving on the carousel or on one of its own controls is\n unaffected.\n \n **`NumberField` documented a route to the unit that its props did not carry.** The\n drawn unit is `aria-hidden`, on the reasoning that a screen reader already says the\n unit when the caller puts it in the field's description, and there was no\n `aria-describedby` on the props to put it in, so a value of 1,250 was announced as\n 1,250. The prop is there now and is forwarded to the input. The unit stays hidden: a\n reference already says it once, and two announcements of the same word is one too\n many. The unit belongs in the description and not in the accessible name, because\n `aria-label` is optional, so a field with a real `<label>` has no name to put it in,\n and a name reading \"Parcel weight, kg\" against a visible label reading \"Parcel\n weight\" is what WCAG 2.5.3 forbids and what voice control cannot activate.\n \n `aria-describedby` is an addition, so nothing breaks. `Button` and `Carousel` change\n behaviour, which is why they are here rather than described as a patch.\n- 6dc7bb8: Coarse-pointer controls take the 44px floor, and the focus gate reads the whole package\n \n Thirteen controls were drawing targets between 16 and 36 pixels with nothing done\n about it on touch input, so a consumer composing the bare primitive shipped a\n target that fails WCAG 2.2 SC 2.5.8 and falls short of this package's own 44px\n standard. All of them are fixed, and **the desktop metrics are untouched**: a mouse\n and a trackpad see exactly what they saw before, and every change is inside\n `@media (pointer: coarse)`.\n \n `Checkbox`, `RadioGroupItem`, `Calendar`'s day cells and paging controls,\n `Dialog`'s trigger and close control, and `SearchDialog`'s close control grow to\n 44 by 44. `Slider`'s and `RangeField`'s thumbs, `Switch`, and `DatePicker`'s clear\n control take a transparent 44 by 44 band around the control instead, because\n growing them would draw something the design does not have: a 44px ball on a six\n pixel rail, a switch that is not a switch. The band is a pseudo-element, so the\n box the value is computed from is the box you see.\n \n **`NumberField` turns its two steppers side by side on a coarse pointer.** This is\n the one place the floor changes an arrangement rather than a size. Two 44px targets\n stacked in the split column need an 88px field, and two 44px bands on two 18px rows\n overlap so much that the lower stepper takes the boundary between them, so a press\n aimed at increment would step down.\n \n **`Calendar` now matches `MiniCalendar` on the same controls.** The identical\n control in one package at two target sizes is the defect, not the 32px.\n \n Three things to know before you style against these:\n \n - A `Calendar` on a phone is six rows of 44px rather than six of 36px, so the panel\n is taller. It is the densest control in the package and the one a finger is least\n able to aim at.\n - `Checkbox` and `RadioGroupItem` draw a 44px bordered box on touch input, and the\n row, column or field around them grows to hold it. That is the price\n `tag-group.tsx` already records for its chips.\n - On a `Slider` and a `RangeField`, a press inside a thumb's band drags the thumb\n from where it was rather than jumping the value to where you pressed. The arrow\n keys and the range input behind the track still reach every value. Two bounds\n closer together than the band is wide are dragged by the one used last until Tab\n moves to the other, so give a range a `minGap` a finger can pinch apart.\n \n `DESIGN.md` states both patterns and the three conditions that decide between them.\n \n **The focus-indicator gate read 132 of the package's 400 source files.** It read\n `components/ui` and nothing else, so it saw no Block, no Page, neither `live`\n surface and no provider, and it skipped any file whose JSDoc made no keyboard claim\n before reading a single class string in it. It now reads `components`, `blocks`,\n `pages`, `live` and `provider`, recursively, and judges every class string in the\n scope. It does not read `apps/site`: that is another package with its own `check`\n chain.\n \n **Its exclusion for menu and listbox options was a suffix match, and is now a\n roster.** `/(?:-item|-sub-trigger)$/` is a statement about spelling rather than\n about menus, and over the Blocks it swallowed `radio-group-item`,\n `toggle-group-item`, `accordion-item`, `breadcrumb-item`, `tree-item` and about\n thirty more, every one of which is an ordinary focusable control.\n \n **Two scanning defects are fixed, and both were making the gate report green over\n a file it had not read.** A block comment inside a `cn()` call held an apostrophe,\n which the scanner read as an opening quote and which hid every class after it; and\n a module-level `const` was sliced to the end of its file rather than to the end of\n its value, so a menu item read as carrying a full-strength ring that belonged to a\n component further down the same module. Both are proved by fixtures that fail\n without the fix. The shipped tree has no genuine findings; a planted one fires in a\n Block and in a file whose JSDoc makes no claim, which is the negative control that\n makes the clean run mean something.\n \n **The bump is `minor` rather than `patch`.** No prop, import or rendered element\n changes, and no desktop metric changes, so nothing breaks a call site. It is\n `minor` because thirteen controls now measure differently on a coarse pointer, and a\n consumer who styles any of them has to know that: a `Calendar` panel is taller, a\n `Checkbox` box and a `RadioGroup` column are bigger, and a `NumberField` lays its\n steppers out differently. That is a visible metric change on the platform most of\n these controls are used on, and describing it as a patch would describe something\n nobody sees.\n- 6dc7bb8: `DocsShell` lays out three columns from `lg` rather than two\n \n The documentation frame's third track named a screen the emitted theme closes, so\n the class compiled to nothing and the contents rail fell into an implicit `auto`\n track: two columns at every width from 1024 pixels, with the article column about\n 340 pixels wide at 1024, on this site and in all four consumers. The rail is\n 15rem, the document takes the rest, and the contents rail is 13rem, all from `lg`,\n which is where both rails already appeared. No prop, import or rendered element\n changes, and the third track still exists only when you pass `toc`.\n \n **`check-breakpoint-variants.mjs` is new, and it is a gate rather than a fix.** It\n reads every responsive variant out of the class strings in the component package\n and the site and compares it to the screens the token package emits, read from\n `layout.tokens.json` and from the build that closes `xl` and `2xl`. A class\n written against a screen the theme does not have is now a finding with a file and\n a line, and every other gate was green through the one above because each of them\n held the value rather than the class.\n- 6dc7bb8: Nineteen Blocks name their table, and the name is the heading they already draw\n \n A `<table>` is named by a caption, an `aria-label` or an `aria-labelledby`, and by\n nothing else. A heading that happens to sit above it does not name it: no assistive\n technology derives a table's accessible name from a neighbouring heading. Nineteen\n Blocks drew a real `<table>` under a caller-owned `SectionHeading` and gave the table\n no name at all, so a reader listing the tables on a page found nineteen anonymous\n entries while every element around them was named. `Table`'s own JSDoc asks for one\n of the three.\n \n A Block cannot compose the words itself, so each one takes the name from the heading\n it already draws, by reference. `SectionHeading` gains an optional `id` for the\n handle; a generated id would be no handle at all, since nothing could be written\n down to point at it. No new prop on any Block, no string shipped, and no caption\n drawn: a visible line repeating the heading is noise, and a reference cannot drift\n from the heading it names.\n \n **Three of them write the reference only while the heading is drawn.** `Compare01`,\n `PricingCompare01`, `RateCard01` and `DataTable01` render their heading only when the\n caller passed a `title`, so an unconditional reference would point at an element that\n is not on the page, which is the defect this same release removed from\n `CommandPalette`'s listbox. In the one state where a caller passed no title the table\n is therefore unnamed, which is the honest trade and the caller's own choice: the\n alternative is a reference to nothing.\n \n `DataTable01` also has a `caption` prop, and where a caller passes one the caption\n wins, because a reference outranks a `<caption>` in the accessible name algorithm.\n Writing both would quietly make the heading the name and the caller's own words the\n thing only read on request. `Gantt01` already captioned its table and is unchanged.\n \n The rule is held by a test rather than a gate, and the reason is in that test's\n header: whether a table is named is a runtime property, because the reference has to\n resolve in every state a caller can reach. A source scan would only prove the\n attribute is written, and this repository does not accept a report-only gate.\n \n No import breaks and no prop is removed.\n- 6dc7bb8: `OverflowActions` and `ResizableHandle` read layout when it can have changed\n \n Neither Component's rendered output changes for a caller who composes them the way\n their own documentation says. What changes is how often each of them asks the browser\n to settle layout, and a table of rows or a page of dragged panes pays that on the main\n thread in the middle of a reader's work.\n \n `OverflowActions` measured itself after every render of every row, reading the row's\n own box, the cap and every drawn action, and writing the answer back as state. **A\n pass now runs on mount, when the actions' ids or their labels change, when `className`\n changes, when the row's own box changes, and when the page's fonts have finished\n loading.** Passing a fresh `actions` array on every render now costs nothing, which is\n the ordinary case in a table cell under a filter box. **The fonts are in the list\n because a self-hosted face swaps in after the first paint and changes the width of\n every drawn action without changing the row's width**, so nothing else reported it, and\n a row measured in the fallback face was a row whose widths were never true of the page\n the reader was looking at.\n \n `ResizableHandle` read the group's box on every `pointermove`, immediately after the\n frame before had written the new position and therefore written new styles: one forced\n synchronous layout per frame of every drag. **The box is read once at `pointerdown`,\n and every frame after that is arithmetic.** A drag measures from the press rather than\n from the divider's own box, which is what a divider a pixel wide and a reader takes\n hold of wherever their pointer lands requires, and it did that before.\n \n Two cases are pinned rather than handled, and both are written down in the JSDoc and\n the site page:\n \n - A group resized by something else part way through a drag finishes that drag against\n the travel it started with. A caller who needs a drag that follows a changing group\n ends that drag and starts it again.\n - `OverflowActions` cannot see a change that leaves both the row's box and its drawn\n content exactly as they were: an ancestor's font size changing, or an action's mark\n swapped for a different-width one while that action is drawn. Both used to be\n corrected by the row's next render and neither is now.\n \n The bump is `minor` because those two are behaviour a consumer can observe, even though\n no prop, import or rendered element changes.\n- 6dc7bb8: There is one container namespace, and `max-w-6xl` no longer resolves\n \n **If you wrote `max-w-6xl`, `max-w-2xl`, `max-w-xl`, `max-w-lg`, `max-w-md`,\n `max-w-sm` or `max-w-5xl` against this package, that class now produces no rule at\n all.** Nothing errors and nothing warns: the element simply loses its cap and lays\n out at whatever its parent gives it. Grep your own repository for `max-w-` before\n you upgrade. The replacements are in the list below and every one of them resolves.\n \n The reason is the defect this release ends. `@nanisoft/prism-tokens` authors three\n container widths and Tailwind ships thirteen of its own, and until now both shipped\n in the same stylesheet. Three of Tailwind's were numerically identical to three of\n ours: its largest at 72rem beside `--container-page`, its fifth at 42rem beside\n `--container-measure`, its fourth at 36rem beside `--container-measure-narrow`.\n Nothing rendered differently, so nothing failed and every gate was green. What it\n cost was that one width had two names, and the first retune of an authored token\n would have moved every surface reaching it by one spelling and left every surface\n reaching it by the other exactly where it was. `Section`, `SiteHeader`,\n `SiteFooter`, `ChartCard01`, `DocsShell` and `QuickView01` all used the Tailwind\n spelling of the page column; `Cta01` and `SectionHeading` used the Tailwind\n spellings of the two measures.\n \n The token build now closes the whole namespace, with\n `--container-*: initial` ahead of the authored entries rather than a list of the\n seven steps that happened to be emitted. A wildcard is the load-bearing word: the\n thirteen names it retires are `3xs 2xs xs sm md lg xl 2xl 3xl 4xl 5xl 6xl 7xl`,\n and a list would have been a second list to keep in step with a dependency's\n default theme, which is the event the line exists to survive. Order matters and is\n asserted: Tailwind resolves a theme in source order, and the same declaration\n written after the authored entries clears all eight of ours and ships no container\n at all.\n \n **What to write instead**\n \n | You wrote | Write | Value |\n | --- | --- | --- |\n | `max-w-6xl` | `max-w-page` | 72rem |\n | `max-w-2xl` | `max-w-measure` | 42rem |\n | `max-w-xl` | `max-w-measure-narrow` | 36rem |\n | `max-w-md` on a dialog or an alert | `max-w-overlay-dialog` | 28rem |\n | `max-w-lg` on a form dialog | `max-w-overlay-form` | 32rem |\n | `max-w-xl` on a command palette or a search | `max-w-overlay-palette` | 36rem |\n | `max-w-sm` on a side sheet or a drawer | `max-w-overlay-panel` | 24rem |\n | `max-w-5xl` on a lightbox | `max-w-overlay-media` | 64rem |\n \n **An overlay's width is a property of the kind of surface it is, not of the page\n grid underneath it**, and that is why the five overlay widths are authored rather\n than carried on a Tailwind name. A retune of the reading measure must not move a\n dialog and a retune of the page column must not move either, so the two families\n are named apart inside the one container group. They share a group because\n Tailwind 4's `max-w-*` resolves `--spacing-*` and `--container-*` and nothing\n else, so a maximum width that is to be a token has to live in one of those two\n namespaces or it is a value written in a Component. `overlay-palette` and\n `measure-narrow` are both 36rem; that is two measurements that happen to agree and\n not one measurement with two names, and the token source says so and the emitted\n contract holds it to saying so.\n \n **Nothing is supposed to change where you can see it.** Every migration above\n preserves the exact rendered width, so a page that only consumed this stylesheet\n looks the same before and after. What changes is the next retune: it now moves one\n surface rather than half of them.\n \n **A width that was arithmetic on the spacing base is left alone, deliberately.**\n `max-w-96`, `max-w-72`, `max-w-64`, `max-w-32` and `max-w-20` resolve to\n `calc(var(--spacing) * N)` and never touched the container namespace at all. They\n are how a chart's axis band, a token table's column and a documentation frame are\n sized, where nobody took a decision and naming one would invent it. They stay.\n \n **Two gates now hold this down, because the question has two sides.**\n `scripts/check-elevation-layout.mjs` reads every `w-*` and `max-w-*` name out of\n the class strings and holds it to the container group read out of the token source,\n so a width nobody authored is a finding with a file and a line; it reads\n `apps/site/items` as well as `apps/site/src` and `packages/ui/src`, because that is\n where the documentation Demos live and where the site's own build scans.\n `packages/ui/scripts/check-container-namespace.mjs` reads the built stylesheet and\n holds the artefact: that the theme block declares exactly the authored containers,\n that none of Tailwind's thirteen steps is declared or read, that the close is\n written ahead of the entries rather than after them, and that every container width\n this package writes is emitted as a utility reading its own variable. It reads the\n step list out of the installed `tailwindcss/theme.css` rather than restating it, so\n a dependency that adds a step is covered without an edit. Tailwind's extractor\n reads this package's comments as well as its code, so a retired class name written\n down in a JSDoc block used to emit a utility; there is now a rule about that too.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** A class a consumer wrote stops producing a rule with no\n error, which is breaking by any ordinary reading, and this entry says so plainly.\n `major` in this repository publishes `1.0.0`, and every release to date has been\n `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent slot.\n Publishing 1.0.0 with this change in it would assert an API stability guarantee\n this library has not earned, and it would be the first release in the project's\n history to use the bump at all. The break is real and is the subject of this\n entry; the number it lands on is the line's decision and the line is pre-1.0.\n \n **One collateral repair on the documentation site, recorded here because it is\n why `globals.css` moved.** The site's own build and this package's meet in one\n `utilities` layer, and this package emits a bare `.grid-cols-6` as the base of the\n `lg:` variant four of its Blocks use. That bare rule landed after the site's\n `sm:grid-cols-11`, so every colour ramp on the Foundations page was drawing six\n columns at every width above 640 against a comment beside the class saying\n eleven. Whether `check-utility-cascade.mjs` saw it depended on which order two\n content-hashed CSS chunks sorted in, which this change perturbed and which nothing\n about the tree controls. Neither side is wrong, so the site's variant is restated\n in the `site-variants` layer that already exists for six other collisions. The\n repaired behaviour is on the documentation site only; nothing in this package\n changes.\n- 6dc7bb8: Ship the interface face and its metric-adjusted fallback, and stop the site loading its own\n \n `@nanisoft/prism-ui` shipped three Inter weights and no fallback face, so a reader\n waiting on the web font saw the platform's UI face jump into place when Inter\n arrived. The stylesheet now also ships a local Arial carrying Inter's metrics\n through `size-adjust`, `ascent-override`, `descent-override` and\n `line-gap-override`, and `--font-sans` names it immediately after Inter, so the\n swap window occupies Inter's own line box. The four numbers are measured off the\n shipped face rather than chosen, and they are the ones `next/font` measured for the\n same typeface, so the line box is unchanged from what the documentation site had.\n \n A fourth face ships beside them: Inter italic at weight 400, the Latin subset of\n the same release, covering the same 230 codepoints as its upright siblings.\n `Prose` sets `blockquote` in italic and the surface had been asking for a style no\n shipped file provided, so every consumer has been reading a synthesized oblique.\n One real face covers every weight a browser asks for, because the font matcher\n takes the closest available weight rather than synthesizing once a face exists.\n The whole set is 93.5 KB, up 22.8 KB, and no consumer has to do anything.\n \n **The documentation site no longer loads a face of its own.** It shipped a 723 KiB\n unsubsetted variable Inter and its italic through `next/font/local`, preloaded both\n in all 581 exported documents, and applied the result to `<body>` as a directly set\n `--font-sans`. A directly applied custom property outranks an inherited one, so the\n library's faces were never fetched on the site that documents them: a missing or\n corrupt shipped font rendered perfectly there with every gate green. The site now\n resolves the same stack you do, which is the only way it can be evidence for the\n package. If you were relying on the site to look right while your own copy of Inter\n was broken, that is now a failure you can see.\n- 6dc7bb8: `Progress` announces a value text again, and `getAriaValueText` works\n \n Every `Progress` in every consumer was announcing a bare number. `aria-valuenow`\n carried the position and `aria-valuetext` was absent from the tree altogether, so\n the sentence a screen reader says for a bar was a number with nothing to say what\n range it was a number in. A bar with `value={null}` was worse than bare: with no\n `aria-valuenow` to fall back on, its entire value was nothing.\n \n **What changed.** `aria-valuetext` is now passed only when you pass `valueText`, so\n the default that Base UI computes survives. You get it for free:\n \n - a determinate bar announces its value as a percentage of the range, so\n `value={68}` is announced as `68%`;\n - a bar of unknown length announces `indeterminate progress`.\n \n **`getAriaValueText` was dead and is not any more.** It was called on every render\n and its return value was then discarded, so a consumer who wrote one was paying for\n it and hearing nothing. It receives the formatted percentage first and the raw\n `value` second, unchanged.\n \n **`format` and `locale` had no observable effect either, and now do.** Both exist\n only to shape the announced string, so with the attribute absent there was nothing\n for them to shape. One trap worth knowing: `format` is applied to the raw value on\n the scale, not to the share of it, so `format={{ style: 'percent' }}` on a\n zero-to-hundred range reads as `4050%`. The unformatted default is the only path\n that asks for a percentage of the fraction.\n \n **Nothing you passed changes meaning.** An explicit `valueText` still wins, an\n `aria-valuetext` written on the Component itself still wins over both, and a bar\n given both `valueText` and `getAriaValueText` still throws. What a consumer hears\n on a bar that passes neither of them is the whole of this change, and it is a\n change to every bar a consumer that passes neither already has.\n \n The bump is `minor` because it changes what a screen reader says in every\n composition of this Component. No prop, import or exported name changes.\n- 6dc7bb8: `Progress` expresses its value as a transform rather than as a width\n \n The fill was transitioned with `transition-[width]`, and `width` is a layout\n property, so every frame of every advancing bar made the browser settle layout on\n the main thread. `Progress` is one of the two or three most-composed Components in\n this package, which is what makes it worth a release of its own: the cost was not\n one bar on one page, it was every bar on every page, in every consumer that\n composes one.\n \n **The indicator is now as wide as the track at every value and is scaled along the\n reading axis.** `aria-valuenow` still comes from `ProgressRoot` and still reports\n `value`, `min` and `max`; the scale factor is the same three numbers read a second\n time, and the test suite holds the two to each other by asserting the transform\n against the announced value rather than against a copy of the arithmetic.\n \n Three things to know:\n \n - **A rule that sized `[data-slot=\"progress-indicator\"]` is now sizing a box this\n Component does not resize.** Style the track's `height`, the track's `width`, or\n the indicator's `transform` instead. The indicator's own inline `width` is `100%`\n and outranks any class, because Base UI writes a percentage width onto the\n element as an inline declaration and nothing in a class list can beat that.\n - **The fill grows from the inline start edge in both directions.** `origin-left`\n under a left-to-right `dir` and `rtl:origin-right` under a right-to-left one, so\n the bar reads from the same end it always did. It takes the `rtl:` variant rather\n than a logical property because CSS has no logical keyword for\n `transform-origin` and Tailwind's `origin` utility ships the nine physical\n positions and nothing else.\n - **At a low value the leading cap is flatter than it was.** A scaled shape's corner\n radius is scaled with it, so a bar at five percent is a short pill with a\n compressed cap rather than a five percent slice of a round one. It is the standing\n trade for a compositor animation, and it is the same one every other\n compositor-priced fill in this package already makes.\n \n `value={null}` is unchanged: an unknown length renders no transform at all, so it is\n the resting state it has always been rather than a zero to animate away from when\n the length becomes known.\n \n The bump is `minor` rather than `patch` because a consumer who styles the indicator\n has to move with it. No prop, import or announced value changes.\n- 6dc7bb8: Declare `react` and `react-dom` as peer dependencies\n \n `react` was already a peer dependency in fact: every module in the package imports\n it, and nothing declared it. What that cost was not a failed install but silence.\n A package with no peer range cannot be incompatible with a React version, so npm\n and pnpm had nothing to warn about, and the manifest never said which React this\n library is written for. Both are now `^19.2.0`, the range this repository builds\n and tests against, and the site already resolves. `react` and `react-dom` stay in\n `devDependencies` so the package still builds and tests locally.\n \n `react-dom` is a peer even though no module here imports it, because\n `@base-ui/react` does, for the floating elements the Dialog, the Popover, the Menu\n and the Tooltip render through. Two places already treated it as your copy rather\n than the package's, the client budget's shared runtime and the registry's implicit\n set; this is the manifest catching up with those two decisions.\n \n If your application is on React 18, install React 19. Nothing in the package uses a\n React 19 only API, but every gate, test and build in this repository runs on 19, so\n a narrower claim would not be one this repository can back.\n- 6dc7bb8: Reduced motion is now one unlayered rule in the stylesheet, and it stops everything\n \n If you have the `prefers-reduced-motion` setting on, this release changes what you\n see in a way you will notice the first time you open one of our pages.\n \n **What a reader with the setting on gets now.** No animation runs and no transition\n runs, anywhere in the component library. A dialog appears already open instead of\n fading up, a hover changes colour instantly instead of over 80ms, a disclosure\n opens at once instead of growing, a `Spinner` ring stands still instead of turning,\n and a `Timeline`'s running mark holds one opacity instead of breathing. Everything\n that is not movement is unchanged: nothing is hidden, no figure loses a state, and\n every announcement is said exactly as before.\n \n **What this replaces.** The stylesheet already carried a `prefers-reduced-motion`\n rule, and it did two things: it set `animation: none`, and it named the seven\n `prism-ambient-*` classes rather than every element. So every `transition-*` in the\n package ran at full duration, and two unbounded loops ran indefinitely: `Spinner`'s\n ring turned for as long as the work did, and a running `Timeline` step breathed for\n as long as the step did. The comment above that rule described the opposite, which\n is the defect class this repository keeps finding, so the rule and its comment are\n now the same document.\n \n **Why one rule rather than a guard at each call site.** A `motion-safe:` variant\n ADDS a rule inside `prefers-reduced-motion: no-preference`; it never removes one.\n Three Components were guarding a transition per call site and all three were\n different shapes: `Drawer` and `ImageZoom` guarded a duration as well as the\n property, `Lightbox` guarded the property and not the duration, and `RangeField`\n guarded nothing at all while animating `left`, `right` and `width`. The one place\n that decides is the stylesheet, and `scripts/check-motion.mjs` now fails on a\n `motion-safe:` or `motion-reduce:` variant on a motion utility, so a call site\n cannot quietly start answering for itself again.\n \n **What it means for a consumer.** Your own stylesheet is unaffected where it is\n qualified by a class, an id or an inline style: the rule is unlayered and universal\n rather than `!important`, so a class-qualified rule of yours outranks it and still\n wins for your own elements. What changes is the library's motion, which is the\n motion you inherited by installing it.\n \n **One Component had to be repaired before this was safe, and that is the load-bearing\n part.** A stopped transition starts nothing and so never sends `transitionend`, and\n an exit waiting on that event strands whatever it was hiding. `Toast` now asks the\n browser what is actually running on its root and hands over on the same commit when\n the answer is nothing, so a `Toast` still leaves under this rule and under your own\n `transition: none`. The overlays are Base UI's and Base UI settles every popup on\n `getAnimations()`, which resolves immediately when nothing is running.\n \n The bump is `minor` because every consumer of this package inherits the change and\n one stylesheet is the whole surface it lands on. No prop, import or exported name\n changes.\n- 6dc7bb8: `SearchDialog` is a real modal, and `Toast` dismisses when no transition runs\n \n Two accessibility defects, and a consumer sees both.\n \n **`SearchDialog` claimed modality it did not have.** It rendered `aria-modal=\"true\"`\n with no focus trap, so Tab walked out of the dialog and into the page behind it;\n it took no focus back, so a keyboard reader who opened search, pressed Escape and\n kept tabbing had lost their place entirely; its field suppressed the browser\n outline and drew no replacement, so the one control holding focus showed nothing;\n and it had no portal and no scroll lock, so the panel scrolled with the document\n and could be clipped by an ancestor.\n \n It is now composed from `Dialog`, so the focus trap, the page scroll lock, the\n portal, the dismissal on Escape and on the scrim, and the return of focus to the\n control that opened it all come from the one modal implementation this package has.\n There is one modal in the package rather than one and a claim. Its props, its\n ranking, its result list and its live region are unchanged.\n \n Two things about the composition are worth knowing:\n \n - `onClose` now fires once the dialog has finished leaving rather than at the\n moment it was asked to. The modal owns the reader's focus while it is closing,\n so handing over earlier would take the focus return with it. Unmount from\n `onClose` exactly as before.\n - The panel and the scrim are now `Dialog`'s rather than the search dialog's own.\n The scrim is a blurred `bg-background/80` wash instead of a `bg-foreground/40`\n one, and the panel is centred in the viewport rather than pinned 15 percent from\n the top. A site that styled either through its own sheet will want to know.\n \n **`Toast` dismissal depended on a transition ending.** The leave handed over on the\n end of its own opacity transition, so a global reduced-motion kill written as\n `transition: none` rather than as a `0.01ms` duration meant the event never fired,\n `onDismiss` was never called, and the toast was stranded in `data-phase=\"leaving\"`\n for good: visible, undismissable, still taking the pointer, and a live region that\n never stopped announcing. This is the one place in the system where a global\n motion kill destroyed state rather than merely removing movement, and\n `packages/ui/src/styles.css` states the invariant it broke.\n \n The leave now asks the browser what is actually running on the toast rather than\n waiting for an event a killed transition never sends. It waits for the fade when\n there is a fade, it hands over on the same commit when there is not, and\n `onDismiss` is called exactly once either way. Nothing else about the contract\n moved, and the length of the leave is still `duration-slow` from the stylesheet\n rather than a number in this package.\n \n **`Dialog` takes an optional `initialFocus`.** Left out, the panel focuses its\n first tabbable element, which is unchanged. It is named for the dialog whose first\n tabbable element is not the control the reader came for, which is what\n `SearchDialog` now uses to put the cursor in the field.\n \n **The bump is `minor` rather than `patch`, for two reasons.** `Dialog` gains a\n public prop, which is new surface rather than a correction. And `SearchDialog`'s\n panel and scrim visibly change to `Dialog`'s, which is a behaviour a consumer can\n see on their site rather than a fix they never notice. Both are additive and\n neither breaks a call site.\n- 6dc7bb8: `SiteNavbar` is opaque, and `sticky` can be turned off\n \n The bar was `bg-background/80` with a `backdrop-blur`. A backdrop filter is\n evaluated against everything painted behind the element, and this element is a\n full-viewport-width sticky bar, so on a scrolling page the browser re-sampled and\n re-blurred the backdrop on every frame of the scroll, on every page of every site\n that composes it. The bar is now `bg-background` with a `border-border` hairline,\n which is what `SiteHeader` has always shipped.\n \n **The blur was not kept behind an `@supports` check, because a capability check does\n not pay for it.** `@supports (backdrop-filter: blur(1px))` is false only in a\n browser that was not compositing anything, and true in every browser that does the\n expensive thing, so it changes no reader's cost. It would also have left a third\n outcome in the world and the worst of the three: a bar that is a flat eighty\n percent veil with unblurred text passing under it.\n \n **Opaque is also the more legible of the candidates, and that is the part a token\n can be held to.** An opaque bar makes the bar's own contrast the pair\n `muted-foreground` on `background`, in every pack and in both modes, which is the\n pair the contrast gate already checks. An eighty percent background has no token\n pair at all, because the colour under it is whatever the reader's scroll position\n has brought there and no token in this system describes it.\n \n `will-change` is not on the bar and is not proposed for it. Nothing on the bar\n animates, so a compositing hint applied at rest has no frame to be ready for and\n nothing that would release it. This package ships no `will-change` anywhere.\n \n **`sticky={false}` now works.** The prop was destructured and never reached the\n class list, so a site that passed it was given the sticky bar it had asked not to\n have. The default is unchanged and for the reason it was there: the bar is how a\n reader leaves the page they are on, and it takes that with them on exactly the long\n pages where it is needed.\n \n Two things to know if you were styling against the old bar:\n \n - A rule that set the bar's background translucency has nothing to sit on now. Set\n the colour on the page behind the bar, or leave the bar the way `SiteHeader` has\n it.\n - A rule that relied on the bar being translucent to hide a heading as it scrolled\n under will no longer hide it, which is the point.\n \n The bump is `minor` because the bar's default appearance changes on every page of\n every consumer. No prop, import or landmark changes.\n- 6dc7bb8: The elevation scale is three steps or it is nothing, and the gate now says so\n \n `SearchDialog` shipped `shadow-lg`, and `DESIGN.md` said no shipped component\n uses one. Both were true once and neither was true when the sentence was last\n read: the Component took `shadow-lg` as an override on top of the `shadow-md` its\n own `DialogContent` already draws, and the site's skip link took `focus:shadow-lg`\n under a heading that named \"the theme disclosure and mobile nav panels\" as the\n only places it was allowed.\n \n **`shadow-lg` is not an authored step, so neither of those was drawing an authored\n shadow.** The token package emits `--shadow-xs`, `--shadow-sm` and `--shadow-md`\n and nothing else, so `shadow-lg` resolved against Tailwind's own stock theme. That\n is the second source of truth this package exists to prevent, and the reason it\n was invisible is that Tailwind's `shadow-lg` is a perfectly good black-alpha\n shadow: nothing looked wrong. What was wrong is that a retune of the elevation\n scale would have moved every lifted surface in every consumer and left that panel\n exactly where it was.\n \n **`SearchDialog` now says nothing about elevation at all.** The panel is the one\n lifted element over a modal scrim, which is what `--shadow-md` is for, and\n `DialogContent` already draws it, so the override is deleted rather than\n replaced. A consumer who styles `DialogContent` keeps styling it, which was not\n true of a hardcoded `shadow-lg` sitting on top of it.\n \n **`scripts/check-elevation-layout.mjs` now fails on any `shadow-*` step the token\n source did not author**, in every root it reads, the site's own source included.\n It reads the authored set from `shadow.tokens.json` rather than from a list beside\n the gate, for the reason the width rule beside it reads the container names from\n `layout.tokens.json`: a gate whose subject is the authored scale cannot be\n answered by a second copy of it. `shadow-none` is allowed, because the absence of\n a shadow is not one more step of the scale, and `shadow-inner` is not, because it\n is a shadow nobody authored.\n \n **There is no docs-only exception any more, and that is the part worth arguing\n with.** The exemption said site apparatus is not installable surface. The site's\n own Tailwind build is a second consumer of the same token package, so \"the site\"\n is not outside the system, it is inside it twice: a site class resolving a shadow\n out of Tailwind is the second source of truth wearing the word apparatus. The\n site's skip link draws `shadow-md` now, which is what it should have drawn.\n \n The bump is `minor` rather than `patch` because a consumer who styles the search\n panel's elevation sees it move from Tailwind's `lg` to Prism's `md`, which is a\n visible change to a published surface and the right one.\n- 6dc7bb8: `RangeField` expresses its band as a transform rather than as three layout properties\n \n The band between two bounds was transitioned with `transition-[left,right,width]`.\n Base UI positions that element with a logical `inset-inline-start` and a `width`,\n so `left` and `right` were properties the browser checked on every frame in order\n to discover they had not moved, and `width` was a layout-and-paint animation on\n every frame of every drag. A range is the one control in this package whose value\n changes on every pointer move rather than on every commit, which is why the cost\n was larger here than on the Component that retired the same mechanism a release\n ago.\n \n **The band is now as wide as the track and is scaled about the inline start.**\n `aria-valuenow` still comes from Base UI and still reports each bound; the scale\n factor is the same division read a second time, and the test suite holds the two to\n each other by asserting the transform against what the two thumbs announce.\n \n Three things to know:\n \n - **A rule that sized or positioned `[data-slot=\"range-field-indicator\"]` is now\n sizing or positioning a box this Component does not resize.** Style its\n `transform`, or style the track. Its own inline `width` is `100%` and outranks any\n class, because Base UI writes a percentage width onto the element as an inline\n declaration. Its POSITION is still Base UI's, untouched: the element keeps the\n logical `inset-inline-start` that places the band's left edge on the lower bound\n under a left-to-right `dir` and on the same distance from the other end under a\n right-to-left one, so a `dir=\"rtl\"` band needs no arithmetic of its own.\n - **A partial band has a straight edge where it stops, and no rounded edge.** The\n previous fill carried `rounded-full`, and a `scaleX` scales the shape it is applied\n to, including its own corners, so a band at 40 percent drew a 3px cap squashed to\n 1.2px on one axis and left at 3px on the other. The ends of a band are the two\n round thumbs drawn on top of it, so the radius was a second, smaller circle under a\n larger one and squashing it landed on exactly the edge the reader is looking at.\n The track still carries `rounded-full` with `overflow-hidden`, so a band at full\n span has both of its ends rounded. This is the one place the shape differs from\n `Progress`, and the reason is on the Component.\n - **A vertical field scales on `scaleY` about the bottom edge**, chosen by the\n `orientation` the Component already forwards, rather than having the horizontal\n animation applied to a vertical box.\n \n `RangeField` also carried a `motion-safe:` variant on that transition, which was\n doing nothing: the variant adds a rule rather than removing one, so the unguarded\n transition ran at every setting. See the reduced-motion changeset for where the\n policy now lives.\n \n The bump is `minor` because a consumer who styles the indicator has to move with it.\n No prop, import or announced value changes.\n- 6dc7bb8: The rest of the controls take the coarse-pointer floor, and the divider explains itself\n \n Fifteen controls were drawing targets between 28 and 40 pixels with nothing done about\n it on touch input, and three more were found by going looking rather than by reading a\n list. All of them now take the 44px floor, and **the desktop metrics are untouched**: a\n mouse and a trackpad see exactly what they saw before, and every change is inside\n `@media (pointer: coarse)`.\n \n **Sixteen take a step, and the step is the default for all of them.** A popup trigger\n (`PopoverTrigger`, `DropdownMenuTrigger`, `NavigationMenuTrigger`, `MenubarTrigger`,\n `AlertDialogTrigger`), an alert dialog's confirm and dismiss pair, a page link\n (`PaginationLink` and the previous and next controls), a tab and its list, a select\n trigger and a native select, a `Toggle`, a `ToggleGroupItem` and the sidebar's toggle\n all grow to 44 tall through `pointer-coarse:h-11`. `Carousel`'s two controls, both of\n `Lightbox`'s, both of `ImageZoom`'s, the drawer's close control and `PaginationLink`'s\n `icon` size take `pointer-coarse:size-11` and are 44 by 44.\n \n **Four of them take `min-w-11` on the second axis as well, because a floor paid on one\n axis is not a floor.** A one-digit page link at 44 tall is 20 wide. An icon-only\n `Toggle` is `size-4` plus `px-2.5`. An icon-only `ToggleGroupItem` is the same. This is\n the arrangement `Button` already states and the reason it states it, and the page-link\n case is the one worth naming: `PaginationPrevious` and `PaginationNext` pass\n `size=\"default\"` and override the padding to `px-2.5`, so on a phone the word is hidden\n and the control is an icon with a 40 pixel width until `min-w-11` says otherwise.\n \n **`TabsList` grows too, and that is the half that is easy to forget.** The list is\n `h-9 p-1`, so its content box is 28 tall and a trigger grown to 44 would not fit inside\n it. It takes `pointer-coarse:h-13`, which is the trigger's 44 plus the list's own `p-1`\n on each side. Both halves are asserted in the test, because either alone leaves a\n control under the floor or a list that clips one.\n \n **Two of the fifteen are not targets and take nothing, and both are worth stating\n because each looks exactly like every other finding here.**\n \n `steps.tsx`'s marker is a `<span>` with no role, no `tabIndex` and no handler. It is not\n focusable, it is announced by nothing, and a press on it falls through to the step's own\n text. The rail is a reading structure: the `<ol>` carries `aria-label`, the `<li>`\n carries `aria-current=\"step\"`, and the connecting rule is `aria-hidden`. Growing a 32px\n disc to 44 would pay the floor on a decoration and push the step's text down the page.\n \n `pagination.tsx`'s ellipsis is a `<span aria-hidden=\"true\">`, for the same reason. The\n page links around it are the targets and they take the floor in `PaginationLink`. The\n floor is a claim about targets a finger is asked to hit, and neither of these is one.\n \n **`ResizableHandle` takes a band, and it is the only control here where a step is\n arithmetically impossible rather than merely wrong.** The panes are given\n `flexBasis: <size>%` with `flexShrink: 0`, so the two of them already sum to the group's\n whole width and the divider is what overflows it, by exactly its own one pixel. A `w-11`\n divider would overflow the group by 44, which on a phone pushes the right pane's edge off\n the screen, and making room would mean changing what `size` means.\n \n The band works because three facts hold that the three conditions in `DESIGN.md` ask\n about. It is a pseudo-element, so the box the drag measures stays the drawn box:\n `onPointerDown` reads the **group's** rect and `onPointerMove` divides by the group's\n travel, so nothing reads this element's own size at all, which is the failure `DESIGN.md`\n warns about for a `Slider` thumb and which does not apply here. It paints above both\n panes without a `z-index`, because the handle is `relative` with `z-index: auto` and the\n panes are static, and a positioned descendant paints after non-positioned siblings. And\n it is 44 by 44 rather than 44 by the handle's length, which is what keeps the overlap\n local: along the split the handle already spans the group so 44 there is free, and across\n the split it reaches 21 pixels into each pane over a 44 pixel stretch of the line rather\n than the whole height of the group.\n \n **The cost is stated at the class rather than hidden in a document.** A press within 21\n pixels of the line, over a 44 pixel stretch of it, grabs the divider rather than the\n pane, and `touch-none` travels with the band because the pseudo-element resolves its\n `touch-action` from this element. Text selection near the divider is unavailable inside\n that patch. That is the trade a resize gutter makes on every platform, and it is bounded\n to a patch rather than run the length of both panes.\n \n Three things to know before you style against these:\n \n - A tab list, a menubar, a navigation bar and a segmented toggle group are 44 tall on\n touch rather than 28 to 36. A `Tabs` panel is that much further down the page.\n - A page header's row of links, an alert dialog's footer and a carousel's control row are\n each 8 to 16 pixels taller on touch. Nothing shifts off an edge: every one of them is\n content-sized and centred rather than flush.\n - On a resizable split, a press inside 21 pixels of the divider grabs the divider. The\n arrow keys, Home and End still move it, and `aria-valuenow` still reports where it is.\n \n **Three controls the earlier list did not name, found by searching for the two patterns\n rather than reading a line number.** `Toggle`, `SelectTrigger` and `NativeSelect` were\n all drawing 32 or 36 pixel targets and all three are the same family as controls the\n earlier release had already fixed. They are included because a trigger that opens a\n popup is a button, not a text field: it has no caret, takes no typed input, and its job\n is to be pressed.\n \n **One family was found and deliberately left, and the reason is worth having in writing.**\n `Input`, `Textarea`, `Form`'s `FieldControl`, `Combobox`'s input and the draft field in\n `CreatableCombobox` all draw 36 tall on a coarse pointer. Growing them is a different\n decision from every one above rather than an omission: a text field is the one control\n whose height also sets the line box the caret sits in, it is composed into dozens of\n Blocks, and this package already carries a deliberate narrow-viewport decision on exactly\n these elements (the `text-base md:text-sm` pair that stops iOS zooming the page on focus).\n The line this release draws is **triggers and buttons take the floor; text entry does\n not, in this sweep**, and the text-entry family is a separate piece of work rather than a\n gap in this one.\n \n **The bump is `minor` rather than `patch`.** No prop, import or rendered element changes,\n and no desktop metric changes, so nothing breaks a call site. It is `minor` because\n eighteen controls now measure differently on a coarse pointer, and a consumer who styles\n any of them has to know that: a tab list is taller, a navigation bar is taller, a\n segmented group is taller, a resizable split claims 21 pixels either side of its line.\n That is a visible metric change on the platform most of these controls are used on, and\n describing it as a patch would describe something nobody sees. This is the same judgement\n the earlier release made for the thirteen controls it fixed.\n \n `DESIGN.md` states both patterns and the three conditions that decide between them, and\n `test/coarse-pointer-floor.test.tsx` asserts the class each floor is written as. **What\n that test does not do is prove a size of 44 pixels, and the file says so in its first\n paragraph:** jsdom has no CSS engine, no cascade and no box, so `@media (pointer: coarse)`\n is never evaluated here and a coarse-pointer variant is a string that a test can only\n assert is present on the element the Component rendered. The assertions query by role and\n name rather than searching the source, so they catch a floor that was moved onto a\n wrapper or dropped in a refactor; they do not and cannot catch a browser that lays the\n element out at 30 pixels. `apps/site/e2e/display.spec.ts` over a real browser is where\n that is checked.\n- 6dc7bb8: `SiteFooter`, `DataTable01` and `RunConsole01` take a heading level\n \n Three Blocks drew a hard-coded `<h2>`. A fixed level is right for a Block at the top\n of a page and wrong everywhere else, and all three are Blocks a product opens inside\n something: a footer as the last region of a settings page, a table in a drawer, a run\n console in a panel. In each case the heading became a sibling of the heading the\n surrounding section already had, so \"skip to the next heading at or below level two\"\n walks straight past the thing the reader came to read.\n \n All three take `headingLevel?: HeadingLevel` and default to `'h2'`, which is the\n arrangement every other Block in this package already uses and which\n `childLevel()` exists to compose. Nothing rendered changes for a caller that passes\n nothing. `DataTable01`'s JSDoc had claimed the fix was needed, so this is the code\n catching up to its own documentation.\n \n **The audit behind this said `SiteFooter` was the only Block in the tree with no\n `headingLevel` prop. It was three.** The count matters because it changes the shape of\n the fix: a single instance reads as a judgement call about that Block and a class of\n three reads as a rule the package had not finished applying, which is what this\n release says.\n- 6dc7bb8: `ToggleGroup` requires the accessible name its role already needed\n \n `aria-label` was declared optional on `ToggleGroupProps` while the JSDoc directly\n above it said \"Required rather than defaulted\". TypeScript enforced nothing, so a\n group shipped unnamed, and both roles this Component draws are ones ARIA puts a MUST\n on: a reader tabbing onto an unnamed `toolbar` is told \"toolbar\" and cannot ask which\n set of controls they have reached, and an unnamed `radiogroup` says nothing about\n which question its radios are answering. `TextFormatToolbar` draws the same role with\n a required `label`, which is the shape this now matches.\n \n **This is a compile-time break, and the fix is one prop.** A `ToggleGroup` that\n passed no name now fails to build:\n \n ```tsx\n // before\n <ToggleGroup value={range} onValueChange={setRange}>\n \n // after\n <ToggleGroup aria-label=\"Date range\" value={range} onValueChange={setRange}>\n ```\n \n `aria-labelledby` still arrives through the forwarded props, so a caller whose name\n is already drawn somewhere can point at it rather than repeat it.\n \n **Why `minor` and not `major`.** The bump line for this repository is `0.y.z` and the\n package is at 0.7, so `major` would be 1.0.0 and would claim a stability promise for\n a design system whose catalogue gained 130 Items in the release before this one. The\n published precedent is in `packages/ui/CHANGELOG.md`: the `*Variant` to `*Form`\n renames and the prop removals in 0.7.0 shipped on the `minor` line, each with its\n reason in the entry. This is the same shape of change, and it is described here in\n full rather than left to be discovered by a compiler.\n\n### Patch Changes\n\n- Updated dependencies [6dc7bb8]\n- Updated dependencies [6dc7bb8]\n - @nanisoft/prism-tokens@0.15.0\n\n## 0.14.0\n\n### Minor Changes\n\n- 4e40c8f: Nine Components, and the naming law that the next thousand will follow\n \n Closes the gap in Component coverage: a `Pill` beside `tag-group`, a `Drawer`\n that is a Sheet with a gesture rather than an overlay, an `ImageZoom` that is a\n `Lightbox` with the dialog taken out, a `VideoPlayer` that owns no engine, an\n `EmojiPicker` that ships no emoji table, a `RepoStars` whose mark is a slot\n because a host logo is a licensed asset, a `ChoiceCard` built on real radios\n rather than ARIA, a `BillingSource` that has no `mask` prop on purpose, and a\n `PackSwitcher` that makes Prism's pack axis switchable at runtime, which nothing\n did before.\n \n Four of the nine ship no JavaScript at all. That is the number worth reading\n against the roster: nine catalogue Items cost 19.3 KB of the deduplicated client\n bundle rather than the sum of their rows, and `VideoPlayer` in particular is a\n server Component that a page can carry a dozen of for free.\n \n `DESIGN.md` gains the naming law this roster needed earlier: a slug says what a\n Component does, and the only number it may carry is a variant ordinal on a name\n that is already true. `button-47` tells a reader nothing; `split-button` tells\n them what it is and what happens if it changes. Five upstream-shaped items are\n refused by that law and each refusal is recorded with its reason rather than left\n to be rediscovered.\n \n The client ceiling moves from 260 KB to 280 KB, measured at 269.7 KB over 177\n entry points, and the gate's header says plainly that this is the third kind of\n number it has printed: not a correction of a bad measurement, and not a forecast\n of weight that had not landed, but a report on weight that is here. It also names\n the pattern, because five moves in a row triggered by the same event is the\n finding: a ceiling that moves once per batch of work is recording a history rather\n than measuring a policy.\n \n `Progress`, `dayKey` and the `live` subpath fix from the previous release are\n unchanged. No consumer import breaks; every name here is an addition, and the\n four `*Variant` to `*Form` renames from that release are still additive because\n nothing imported the old names.\n- 829ae83: Take the catalogue from 106 Items to 236\n \n Adds 130 Items in Prism's own vocabulary: 23 foundation Components, 96 Blocks, 10\n Pages and one live surface. Every name is an addition, so no consumer import\n breaks, and every Item follows the rules the package already held rather than\n relaxing them: no shipped copy, no fetching, no raw colour, and motion by token.\n \n The shapes that were not upstream's are the ones worth naming, because each is a\n translation rather than a copy. Upstream's storefront comparison is\n `offering-categories-01` and its spec sheet is `spec-table-01`, because what a\n buyer compares in a pipeline product is a connector's declared bounds rather than\n a plan's feature list. `tool-ledger-01` is the second live surface and takes a\n `subscribe` function the consumer supplies, so Prism still owns no socket.\n `directory-01`, `project-dashboard-01`, `ops-checklist-01` and `handoff-01` were\n added because a Block already in the roster reached for something absent.\n \n Two names move because nothing had imported them. `CalendarBlock01` is now\n `Calendar01`, and the deferred `input-otp` resolved to the published\n `one-time-code`. Four `*Variant` types are now `*Form`, since the surface gate\n rejects an exported union under a name that reads as a styling axis.\n \n `Progress` gains a string `valueText` prop for the server Block that cannot carry\n a callback across the boundary, `dayKey` is exported from `lib/utils` for the\n Blocks that share a date key, and the emitted `live` subpath now resolves its\n relative specifiers, which it did not.\n \n The client bundle is 250.4 KB against a ceiling moved from 208 KB to 260 KB. The\n move is a forecast about weight that had not landed, and the gate's own comment\n records why it differs from the two earlier corrections. `docs/history/roster-expansion.md`\n holds the decisions, and `DESIGN.md` records that the v1.1 deferred tail has\n shipped.\n- 4e40c8f: Thirty-four Components, and the audit that refused the other thousand\n \n Component coverage of the upstream catalogue is complete. 1,026 variants were\n audited against the naming law and 34 shipped: 392 were a cross-product of tone,\n size, shape, icon or state that a shipped Component already expresses as props, and\n the rest were motion refused by a law that predates this work.\n \n The zeroes are the result. `form` (85) and `select` (51) produced nothing, because\n a contact form and a settings form differ only in the data a consumer passes.\n `table` (38), `avatar` (34), `skeleton` (30), `dropdown-menu` (30) and `sheet`\n (30) produced nothing for the same reason, and `skeleton` at 30 is six groups of\n five, each one a different arrangement of the same placeholder rectangles.\n \n New: `reorderable-list`, `checklist`, `form-dialog`, `form-wizard`, `stateful-table`,\n `nested-tabs`, `mega-menu`, `task-progress`, `split-button`, `stepper`,\n `overflow-actions`, `selection-toolbar`, `money-field`, `intensity-grid`,\n `proportion-list`, `cohort-grid`, `multi-combobox`, `creatable-combobox`,\n `range-field`, `platform-modifier-key`, `image-list-field`, `repeatable-rows`,\n `text-format-toolbar`, `prompt-composer`, `lifecycle-button`, and the nine from the\n category sweep: `pack-switcher`, `pill`, `billing-source`, `drawer`, `image-zoom`,\n `video-player`, `emoji-picker`, `repo-stars`, `choice-card`.\n \n Five of the new ones exist because two Components disagree and the disagreement\n is the defect: `split-button` joins a trigger and a menu trigger into one shape,\n `stepper` clamps two controls at one bound, `overflow-actions` moves actions into\n a menu when the row runs out, `selection-toolbar` and `text-format-toolbar` take\n opposite positions on whether focus may move, and `nested-tabs` orders two tab\n axes that look identical. A boolean cannot express a conflict.\n \n **`form-dialog` is the largest single job found, at 22 upstream variants** across\n `dialog`, `alert-dialog` and `popover`. All three ship a container and none owns\n what happens between the reader pressing submit and being able to act on the\n answer.\n \n **`pack-switcher` makes the pack axis switchable at runtime**, which is the piece\n a DTCG system was missing. `video-player` owns no player engine and ships no\n JavaScript. `money-field` holds a number for the caller and a locale-formatted\n string for the reader at the same time, and the round trip is the Component.\n \n Four defects the audit found in work already merged are fixed in this release, and\n each was found by a job that did not exist to find it: `button` never had a\n loading state and two documents claimed it did; `cohort-grid` documented a scale\n knob that cannot be moved because a retention row's first column is a hundred by\n definition; `check-item-docs` could not see a generic Component, so a module with\n a full JSDoc block read as declaring no Item rather than as undocumented; and a\n byte order mark on an MDX makes its frontmatter parse as empty with no gate\n noticing, which is what `scripts/check-encoding.mjs` now exists to prevent.\n \n The catalogue is 270 Items and the client bundle is 286.2 KB, inside the 300 KB\n ceiling without moving it, because nine of the thirteen newest compose Components\n the bundle already carries. 34 Components across two passes cost 47.6 KB of\n aggregate where the sum of their individual measurements is several times that.\n \n No consumer import breaks. Every name added is an addition.\n- 4e40c8f: The component sweep: 21 Components from 555 audited upstream variants\n \n The naming law in DESIGN.md says a slug describes the job and a variant that\n differs from its siblings only by a prop is a call site. Applied strictly to 555\n upstream variants, 21 survived: 340 were a cross-product of tone, size, icon and\n state that existing Components already express as props, and 122 were motion\n refused by a law that predates this work rather than by a new one. The two zeroes\n are the finding: `form` at 85 and `select` at 51 produced nothing, because a\n contact form and a settings form differ only in the data a consumer passes.\n \n New: `pack-switcher`, which makes the pack axis switchable at runtime and is the\n piece a DTCG system was missing; `pill`, `drawer`, `image-zoom`, `video-player`,\n `emoji-picker`, `repo-stars`, `choice-card`, `billing-source`; `intensity-grid`,\n `proportion-list`, `cohort-grid`; `multi-combobox`, `creatable-combobox`,\n `range-field`, `platform-modifier-key`; `image-list-field`, `repeatable-rows`,\n `text-format-toolbar`, `prompt-composer`, `lifecycle-button`.\n \n Four ship no JavaScript. The catalogue is 257 Items and the client bundle is 278.5\n KB.\n \n **Three defects the audit found in work already merged.** `button` never had a\n loading state and the catalogue and its MDX both said it did; the claim is\n corrected and `lifecycle-button` is now where an outcome after a press lives.\n `cohort-grid` documented `max` as the way to narrow its scale, which is impossible\n because a retention row's first column is a hundred by definition, so the knob is\n `min` and the limit is now stated. And `check-item-docs` could not see a generic\n Component, so a module with a full JSDoc block was reported as declaring no Item\n rather than as undocumented; the pattern is widened, which widens the law's reach\n rather than relaxing it.\n \n **`scripts/check-encoding.mjs` is new.** A byte order mark on an MDX makes its\n frontmatter parse as empty, and no existing gate noticed: the file had a JSDoc\n module, a Demo beside it, all-props strings, and a clean catalogue join. It\n surfaced as seventeen unrelated failures in two files. The class has now bitten\n this repository twice. The gate reports and does not repair, and it is proven to\n fire.\n \n **The client ceiling moves to 300 KB.** It has now moved six times and every move\n was triggered by a batch of new Items, which the gate's header records as the\n finding rather than hiding. A roster-derived ceiling was rejected because a\n ceiling that scales with the catalogue is the same as no ceiling. Deciding one\n fixed number on what a consumer can actually load is a product judgement about\n the four downstream repositories and is the one open decision here.\n \n No consumer import breaks. Every name added is an addition.\n\n## 0.13.0\n\n### Minor Changes\n\n- 66787b0: Add `SiteNavbar`, the site bar with the controls a reader needs on every page\n \n `SiteHeader` is a lockup, a product switcher and a slot. It is not a search box, a\n menu of the family's sites, a colour chooser, a light and dark control, or a\n navigation that survives the width at which a horizontal row stops fitting, and all\n five NaniSoft sites had written the missing parts themselves: Prism's own site had\n search and a colour chooser and no way to reach its four siblings, and each of the\n four had a row of five product marks where a menu belongs and no search, no mode\n control and no navigation at all below its own row's threshold.\n \n `SiteNavbar` is that bar. `search`, `sites`, `theme`, `mode` and `nav` are each\n optional, so a site that owns none of the four controls renders the whole bar as a\n server Component and ships no client JavaScript for it. Every string is a prop: the\n Block ships no site name, no default navigation, no \"Search\" and no list of packs or\n sites.\n \n ```tsx\n import { SiteNavbar } from '@nanisoft/prism-ui/blocks/site-navbar'\n \n <SiteNavbar\n product={{ id: 'nexus', name: 'Nexus', pack: 'lavender' }}\n defaultPack=\"lavender\"\n defaultMode=\"dark\"\n navLabel=\"Site\"\n mobileLabels={{ open: 'Open menu', close: 'Close menu' }}\n nav={NAV}\n currentSiteId=\"nexus\"\n sitesLabel=\"The family\"\n sites={SITES}\n search={{ indexUrl: '/api/search', label: 'Search documentation', messages: COPY }}\n mode={{ lightLabel: 'Switch to dark mode', darkLabel: 'Switch to light mode' }}\n />\n ```\n \n `currentPath` is the alternative to marking `current` on each link, for a site that\n composes the bar once in a root layout and is therefore handed no pathname.\n \n Also new: `SearchDialog`, the search the bar opens. It fetches a static JSON index\n on the first activation and ranks it in the browser, and the ranking is a scorer\n rather than an inverted index: every query token has to match somewhere, so a\n two-word query narrows instead of listing most of a documentation set. There is no\n stemming and no typo tolerance, which is a deliberate trade against taking a search\n dependency into a package that otherwise depends on a design system and a token\n pipeline and nothing else.\n \n `DropdownMenuItem` now takes `render`, so a menu item can be the link it navigates to\n rather than a focusable element wrapped around one. Two tab stops for one row, and a\n reader who middle-clicks a row that only looks like a link, are what that fixes.\n\n## 0.12.0\n\n### Minor Changes\n\n- 26a6fdc: Export the `Stat` and `Stats01Props` types from `stats-01`\n \n A caller that builds the `stats` array could not name the type it was building,\n so it had to infer it or annotate the array as `any`, and a rename of a field on\n `Stat` then stopped being a compile error at every call site. Both types are\n now exported from the subpath, alongside the Block.\n\n### Patch Changes\n\n- 9f3dd2a: Align `FeatureGrid01` and `Pricing01` section headings left\n \n Both Blocks rendered a centred heading over a grid of cards, and `SectionHeading`\n states that `left` is right for a section with content under it. Every other Block\n that opens with a heading already aligned left, so a page composed from Blocks put\n these two headings out of step with the rest of the page. No prop changed, so no\n call site does; a consumer relying on the centred heading was overriding a default\n rather than setting a value.\n\n## 0.11.0\n\n### Minor Changes\n\n- 3260391: A card title is a heading again, at a level the Block derives rather than guesses\n \n **The defect.** `CardTitle` renders a `div`, and that is correct: four cards in a\n grid are four titles under one section heading, and four `h2`s under one `h2` is\n four sections. But that reasoning is about the **element**, and the catalogue was\n reading it as a reason about the **outline** too. Every Block that drew a titled card\n drew it as plain text, so a page kept its `h2` and lost every heading below it. A\n feature grid of five cards was unreadable by heading navigation while looking\n identical. Found while migrating the company site, whose feature section had five\n `h4`s before the migration and five `<div>`s after.\n \n **The answer is one function, and it is the same answer in all six places.**\n `childLevel(headingLevel)` steps one level down, so a Block that draws a Section\n heading and a set of card titles puts the titles *under* the section, and the whole\n set moves together when the block is composed one level deeper than it was written\n for. A Block that draws no Section heading passes `headingLevel` directly, because\n there the card title is the one heading it renders. Both answers are right, and the\n test carries which is which rather than assuming one for all six.\n \n This is what answers the ticket's third criterion, that the same question be\n answered for *every* Item that draws a titled card. The sites are `FeatureGrid01`,\n `Pricing01`, `StackGrid01`, `AuthForm01`, `SettingsPanel01` and `AuthPage`, and the\n test that holds them is one file, because the consistency is the thing under test:\n five files with one case each would let the next Block answer differently and still\n pass, which is the failure this was filed about.\n \n **`CardTitle` gains `as`, defaulting to `div`.** The change is additive and no\n existing rendering moves. Its JSDoc previously told a caller to pass Prism's\n `Heading` inside it, and that was wrong: `Heading` is a step of the *type* scale\n whose floor is `lg`, and a card title sits at body size, so the only way to use it\n was to override the size back down, which asks a type-scale component to have no\n opinion about type. The element and the visual step are separate questions and this\n is the one that answers the element.\n \n **Two levels in one Block that had one hardcoded.** `SettingsPanel01` drew its\n group headings as a literal `<h3>`, so a panel composed under an `h4` in a document\n whose sections were `h4` announced its groups as siblings of the panel that\n introduces them. They are now one step below the panel title, so the panel's whole\n outline moves when the panel does.\n \n **`h6` holds rather than wraps.** A section already at `h6` has no child level.\n Wrapping to `h1` would put a card title *above* the section that introduces it, which\n a reader navigating by heading would meet first; holding at `h6` costs a repeated\n level, which a screen reader announces as the same depth rather than as a break in\n the outline. The clamp is asserted so a later change to the order is caught.\n \n **The proof that the test can fail.** Replacing `childLevel(headingLevel)` with a\n hardcoded `h3` in `FeatureGrid01` fails two cases, the default and the derived one,\n which is the exact defect the ticket names. A test that asserts \"is a heading\" would\n have passed that.\n- 932ce96: Add `ChartFrame` and `Sidebar`, the two token families that shipped with no consumer\n \n `chart-1` through `chart-5` and the eight `sidebar-*` roles were in the contract\n and used by nothing. These two Components are what consume them honestly.\n \n **`ChartFrame` puts the table inside the frame rather than beside it.** Marks and\n cells are two renderings of one `series` array, so drift is structurally\n impossible; a table passed as a sibling prop is a table that says last quarter.\n The table is always in the document and `table=\"visible\" | \"hidden\"` only decides\n whether it is *seen*, because `hidden` is `sr-only`: it is still announced and\n still findable in the page.\n \n **One frame means every chart in a product aligns to the same plot box.** Five\n charts each drawing their own axes is five sets of numbers that do not line up, and\n a dashboard where two charts disagree by six pixels looks broken in a way nobody\n can name. `mark=\"line\" | \"bar\"` is a prop and the marks are internal on purpose: a\n mark with no axis, no gridline and no baseline is a bar that lies, and exporting\n one for a caller to place is the exact failure the frame exists to prevent.\n \n **`Sidebar`'s collapsed state is the same rail at a smaller width, not a second\n Component.** Hover surface, focus ring, current marking and accessible name are\n all present in both states and only the words and the padding change. The name\n moves to `sr-only` rather than unmounting and the trailing count is `aria-hidden`,\n so the name is the same *string* at both widths and a reader who collapses the rail\n loses nothing.\n \n ## The measured findings behind both\n \n **The sidebar needs its own ring, and the numbers are in the tests.** `ring` on a\n `sidebar` surface measures 2.86:1 on Mint light, 2.97 on Sky, 3.13 on Peach, 3.16 on\n Lavender and 3.22 on Blush. Four of the six light-mode packs would have shipped a\n sub-3:1 focus indicator had the rail inherited the page ring, which is the\n measured justification for the whole `sidebar-ring` family existing.\n \n **The chart tokens are below 3:1 and that is why the legend is not optional.** In\n light mode `chart-2` measures 2.49:1 against `card` and `chart-3` measures 2.15:1.\n Both are exempt from the contrast gate as graphic objects, so a mark in either is\n found by position and shape rather than by colour. That is why the legend is\n mandatory, the stroke is 2px, and the table is not optional. Past about three\n series hue is the only channel left, which the documentation states as a consumer\n decision rather than hiding.\n \n **One pair in this family is unmeasured.** `sidebar-ring` against `sidebar-accent`,\n the surface a focused item sits on while hovered or selected, is 3.19:1 in the\n base pack's dark mode and 3.82 to 4.21 in the light packs. It passes everywhere\n and it is the tightest pair in the family with no row in `check-contrast.mjs`, so it\n is reported rather than papered over.\n \n ## Two gate gaps found and closed\n \n **`check-variant-ink` listed eleven foreground roles and omitted the two sidebar\n ones.** `sidebar-primary-foreground` and `sidebar-accent-foreground` are both\n required 4.5:1 rows in the contrast table, so the list was telling the `Sidebar`\n that its own required pairing was an inherited one. It is the first Component in\n the package to put a `sidebar-*` fill in a variant, so nothing had hit it. The\n alternative was to add a decoy `text-sidebar-foreground` to silence the gate, which\n would have been a false pass.\n \n **`check-focus-indicators` recognised `ring-ring` only**, so a `ring-sidebar-ring`\n would have been reported non-compliant. The `Sidebar` therefore keeps the browser's\n outline as well as drawing its ring, which is asserted by a test so it stays a\n deliberate guarantee rather than an accident. Both ring roles are now accepted.\n- 4650aff: Add `CommandPalette`, so a reader can reach any command by typing three letters\n \n Prism had a Select, a DropdownMenu and a set of Tabs, and nothing for the fastest\n route to an action that lives behind three levels of menu.\n \n ```tsx\n <CommandPalette\n open={open}\n onOpenChange={setOpen}\n label=\"Commands\"\n inputLabel=\"Search commands\"\n groups={[\n {\n id: 'appearance',\n label: 'Appearance',\n items: [\n {\n id: 'theme',\n label: 'Toggle theme',\n hint: 'Cmd K T',\n keywords: ['dark', 'light', 'colour'],\n onSelect: setTheme,\n },\n ],\n },\n ]}\n empty={{ message: (query) => `Nothing matches ${query}` }}\n />\n ```\n \n **It ranks, and that is the difference between this and a filtered menu.** Matches\n are scored by where the query falls: the start of the name beats the start of a\n word inside it, which beats a match further along, which beats a keyword-only hit.\n A palette that filters without ordering shows every command containing the query in\n declaration order, so the command the reader meant sits below one that merely\n mentions their query and they scroll.\n \n **The groups are ordered by their best match too.** Ranking only *within* a group\n leaves the premise broken, because an exact match in a late group still sits below\n a poor match in an early one. Ordering the groups by their strongest member puts\n the best answer at the top and keeps the headings, which is the whole of what a\n grouped list is for.\n \n **It is composed on the Dialog rather than beside it.** Focus trapping, Escape, the\n portal, the scroll lock and the return of focus are five behaviours that are correct\n in the Dialog and would be five chances to get one wrong here. A palette that rolled\n its own overlay would be a second answer to all five questions, and the second\n answer is the one that ships the bug.\n \n **The matched run is emphasised by weight, not by a background**, which is a\n deliberate difference from `Mark`. A Mark is right in a list of search results,\n where the match is the reason the row is there. In a palette the match is a hint\n while the label is what is being read, and a saturated background on every matched\n character fights the text it sits inside. Two surfaces, two treatments, one reason.\n \n Three behaviours are decisions rather than defaults and are asserted in the tests:\n \n - **Enter never runs a command the reader did not point at.** With nothing\n highlighted it does nothing. This is the one outcome a palette must never\n produce, and it is invisible in a screenshot because nothing on screen changes\n when it happens.\n - **The arrows wrap.** A palette is a transient surface where overshoot is common,\n and a reader who overshot should not have to press Up to come back.\n - **The highlight is clamped, not reset, as the list changes.** A reader who arrows\n down three rows and then types one more character is choosing from a list that\n moved under them, and yanking the highlight to the top discards where they were.\n \n `keywords` is what makes the palette good rather than merely present: a reader who\n has to name a command exactly already knows it exists, which defeats the surface.\n `suggest` covers the rest, because a palette that opens onto a bare list makes a\n reader type before they know what is available. `empty` takes the query so the\n sentence can be the caller's, which is the only way \"no results\" avoids shipping in\n English.\n- 932ce96: Add the data display substrate: item, scroll area, carousel, toggle and the rest\n \n Nine Items, and the one that matters most is `item`, because it is the row almost\n every list in the package hand-rolls today.\n \n **`item` is the row and not the list.** A caller writes the `ul`, the `dl` or the\n grid and composes rows into it, because the same row appears in four different\n parents and a Component that owned the list would own all four.\n \n **`scroll-area` keeps the browser scrolling and takes over only the appearance of\n the bar.** The alternative is a transform on a `div`, which looks identical in a\n screenshot and has no scroll position, so no keyboard, no `scrollIntoView` and\n nothing to announce. The custom bar is also a real accessibility obligation rather\n than a decoration: a scrollbar is a control, and a control made of `div`s is worse\n than the one the browser shipped.\n \n **`aspect-ratio` takes the ratio as a prop rather than as a class.** Prism's\n stylesheet scans only Prism's own source, so a consumer's `aspect-[4/3]` is a rule\n the shipped sheet does not contain, and the box silently collapses.\n \n **`carousel` is never the only route to its content.** Every slide stays in the\n DOM, the controls state where the reader is through a required `position` function,\n and there is a focus handoff: a control that has just been disabled leaves the tab\n order without giving up focus, so a reader who pressed \"next\" onto the last slide\n would otherwise be stranded inside the carousel.\n \n **`toggle` is pressed; a `switch` takes effect at once.** The JSDoc says so and one\n test asserts the three controls side by side, because confusing them is the usual\n failure and the confusion is a bug report rather than a compile error.\n \n **`toggle-group` changes the role, not the look.** `single` is a `radiogroup` of\n `role=\"radio\"` with `aria-checked`; `multiple` is a `toolbar` of pressed buttons\n where the arrows move the highlight without pressing it. A `group` carrying\n `aria-orientation` is an axe violation, so `group` was not implementable as the\n issue's phrasing suggested, and the toolbar is the more accurate role anyway.\n \n **`native-select` is an addition and not a rival.** The existing `Select` is Base\n UI: a `button role=\"combobox\"` with a portalled popup, not a `select` element. A\n native select is right when the platform picker beats anything this package could\n draw, and wrong the moment an option needs to be more than a string. The test\n renders both and asserts one is a `SELECT` and the other a `BUTTON`.\n \n **`button-group` draws the focus ring once around the group** and the members\n suppress their own, with the click-versus-keyboard trade stated rather than hidden.\n \n **`table-sort` announces the direction it will go next, not the one it is in**, and\n its cycle returns to unsorted in three clicks, so the caller's original order is\n reachable from the control the reader already knows.\n- 3260391: `DiagramNode` carries a subtitle and a pack, and an unlabelled `Diagram` is named by its nodes\n \n **The subtitle is drawn, not only announced.** `DiagramNode` had one line of text\n and `DiagramProps` forced `label` on the non-decorative arm, so the only channel\n to a screen reader was the label. That is a text-only channel: a sighted reader\n looking at the picture got nothing, and the drawn name and the announced label\n were two strings a caller had to keep in step by hand. The subtitle is a second\n `<text>` under the name, and a node without one emits no `<text>` at all, so an\n absent subtitle is not an empty line.\n \n **A derived name makes the drift structurally impossible rather than discouraged.**\n `DiagramProps` gains a third union arm in which `label` is absent and the name is\n built from the nodes in draw order: `Tokens (the shared language), Pipeline (the\n engine), Twins`. The arm is `label?: never` and not `label?: string`, so an unnamed\n diagram and a diagram named with an empty string cannot be confused, and a blank\n label falls back to the derived name rather than announcing an unnamed image.\n \n Relation words are deliberately **not** in a derived name. Inventing a sentence out\n of the edges would make `Diagram` write words, which this system does not do; a\n caller who needs the relations announced passes `label`, and the JSDoc says so.\n \n **`pack` is per node and typed `PackId`, not `string`.** A free-text field would\n accept a name matching no emitted rule and the mark would silently keep the pack\n above it. The closed six-pack union is the one `ProductMark` already uses. It is\n per node rather than per diagram because a `pack` is a boundary on a **mark**, and\n a mark in a diagram is a node; a pack on the whole diagram would be a boundary on\n the arrangement, which is the thing the boundary law is not for. It is set on the\n `<circle>` rather than the `<svg>` or the node's `<g>`, so it re-inks that one mark\n and moves nothing else, and a circle is a shape the pack cannot re-round.\n `'default'` is the absence of the attribute, as `ProductMark` spells it.\n \n `check-pack-boundary` now reads three boundaries and reports 0 findings.\n- 4650aff: Add `Diff`, where the bar measures how much of a line changed\n \n A conventional diff marks every changed line with the same coloured wash and a\n rail. That tells a reader a line changed and nothing about how much, so rewriting\n one identifier in a long line leaves the same mark as replacing the line outright,\n and the reader's eye, which is fast at finding saturated bands, is drawn to the\n least interesting change in the file.\n \n ```tsx\n <Diff\n lines={[\n { kind: 'context', oldNumber: 1, newNumber: 1, content: 'export function run() {' },\n { kind: 'removed', oldNumber: 2, content: ' const limit = 100', changed: [[8, 13]] },\n { kind: 'added', newNumber: 2, content: ' const limit = 250', changed: [[16, 19]] },\n ]}\n label=\"Changes to run\"\n labels={{ added: 'Added', removed: 'Removed', context: 'Unchanged' }}\n file=\"src/run.ts\"\n summary=\"1 addition, 1 deletion\"\n />\n ```\n \n **The bar is change density.** Its width is the fraction of the line that actually\n changed, so a one-character edit in a long line is a sliver and a rewritten line\n fills the gutter. The marks that used to be a wash become a measurement, and the\n eye goes where the reviewer's attention belongs.\n \n **The line numbers carry the side, not the colour.** An added line has a new number\n and no old one, a removed line the reverse. That asymmetry is structural, it is how\n every diff reader a developer has used distinguishes the two, and it does not depend\n on telling red from green. The colour is redundant on top of it, which is the right\n way round: shape carries the meaning, colour reinforces it.\n \n **The changed words are emphasised by weight, not by tint.** A diff that coloured\n them green and red would spend the two hues a reader is most likely to be unable to\n distinguish, and would also fight the line's own state colour. Weight survives\n greyscale, which is the condition any encoding here has to survive eventually.\n \n **It is a table**, because a diff is two columns of numbers beside a column of\n text, and row and column navigation then come from the semantics rather than from a\n grid of divs.\n \n `labels` is required, for the same reason a Dialog's close label is a prop: a\n shared library cannot know whether the word for this is \"added\", \"ajoute\" or\n \"hinzugefugt\". `summary` is the caller's too, and a diff of a rename has no\n additions and no deletions and is still a change, which is the case a computed\n sentence gets wrong. The counts are exposed as `data-added` and `data-removed` so\n a caller can style them without the Component shipping a sentence.\n \n A test caught a real flaw in the first draft. A minimum bar width was applied to\n every changed line to keep one visible, which meant a one-character change in a\n long line was drawn at twelve percent when the data said half a percent. A\n measurement is not allowed to overstate what it measured, so the floor now applies\n only where the density is genuinely unknown, which is a changed line the caller\n gave no ranges for.\n- 3260391: `DocsShell` refuses a page with no address, and renders a link with no words as a label\n \n **This is a behavioural change and the honest reading of it is that a previously\n rendered page now throws.** Two leaks fed it. `DocsNavGroup.href === undefined` was\n the only guard, so `href: ''` fell to the anchor arm on both arms of the union, and\n `flatten` copied the same value into `Neighbour`, so one bad row published a second\n broken link in the pager. `title: string` admitted `''` on both arms, rendering a\n link announced as \"link\" and nothing else. Nothing in the type could stop either,\n and the Component's own JSDoc claimed that it could.\n \n An anchor with an empty `href` is a control a keyboard can reach and cannot operate,\n so the fix is at the tree rather than at the rendering: one pass over both `nav` and\n `toc` at the top of `DocsShell`, before anything renders, and a **page** with a\n blank address throws, naming the tree and the entry. Only the page arm is refused. A\n group with no address has a documented rendering, and a page has nothing to render\n in place of the link, so a label there would hide a page the tree is missing rather\n than report it.\n \n **Blank counts as absent, and that is required rather than pedantic.** All three\n consumer adapters write `url: node.index?.url ?? ''` for a folder with no index, so\n on the group arm `''` is the documented \"no route\" state and must render as a label.\n \n **A second bug surfaced while fixing the first.** `containsHref` read the group's\n address directly, and `under('', currentHref)` is `currentHref.startsWith('/')`,\n which is true for every absolute address. Treating `''` as \"no address\" in the\n renderer without fixing this would have made every label-only section claim to be\n the current one at once, so it reads through the same helper the renderer uses.\n \n The rail and the pager now ask one function whether an entry is a destination, so a\n row that is a label on the rail cannot become a neighbour in the pager.\n- 932ce96: Add the feedback substrate: Spinner, Toast and the Empty State Block\n \n Three Items, and the one that changes an existing Component's behaviour is the\n `Toast`.\n \n **A pause holds the remaining time rather than restarting it.** A reader who\n hovers or focuses a toast for ten seconds gets the four seconds they had left, not\n four fresh ones, because a rest is a fact about the reader and the countdown is\n about the content. Restarting the clock on every pointer move means a toast can be\n held open indefinitely by a reader who simply rests on it, which is the opposite of\n what a pause is for.\n \n **`duration` defaults to 4000 with its reason, and `0` turns the clock off.** A\n toast that vanishes before a screen reader has finished announcing it is worse than\n no toast at all, and a toast that never leaves strands the reader. Both are\n judgements about the reader rather than about the content, which is why the\n default is stated and overridable rather than fixed.\n \n **`closeLabel` is required with no default**, and it is the only surface in the\n package where a default is wrong rather than merely unhelpful. A toast arrives on\n its own initiative, with no button the reader pressed and no context to carry an\n implication about the product's language, so four products in two languages cannot\n all be told the control says \"Close\". A default is right when the caller may\n override it; here the string is the whole of the contract and guessing it is\n guessing a product's words.\n \n **The enter and leave are three phases with the handoff on the element's own\n `transitionend`**, filtered to `opacity` and with no second clock. Two timers for\n one animation is two things to keep in step, and a mismatch shows as a toast that\n fades and never goes.\n \n **`Spinner` is a server Component.** It holds no state, runs no hook and attaches\n no handler, so the client directive would be a claim about work it does not do. Its\n JSDoc states the three-way difference from `Progress` and `Skeleton`, because\n \"reports a position\", \"has the shape of what is arriving\" and \"something is\n happening\" are three different claims and the usual failure is using the third when\n the first or second is what the reader needed. The honest reason a spinner is often\n wrong is layout shift: it hides the shape of what is coming.\n \n **`EmptyState01` requires a `reason` closed to three values**, because\n `first-run`, `no-match` and `not-permitted` want different words and different\n actions, and a consumer who cannot say which kind of empty they have writes \"No\n data\", which is the state this Block exists to end. It throws rather than shipping\n a button that lies, following the `instrument-panel-01` precedent.\n \n ## Nothing was deleted, and that is a finding\n \n The fourth part of this ticket was to remove a superseded `empty` entry. The search\n was exhaustive across `packages/tokens/src`, the emitted tokens, the catalogue, the\n registry and the whole tree, and there is no `empty` token, no `empty` Component and\n no `empty` catalogue entry to remove. It was already resolved: `DESIGN.md` records\n that the old `empty` Component \"returns as `empty-state-01`, a Block\", and the\n change note that renamed it is in the history. Nothing was invented in order to\n have something to delete.\n- 932ce96: Add the form substrate: combobox, calendar, date picker, number field and the rest\n \n Nine Items, and the one that changes an existing Component's behaviour is the\n `Combobox`.\n \n **Typing never discards a choice.** A field showing exactly the chosen label is\n not narrowing anything, so \"chosen and filtered out\" is not a reachable state: the\n answer changes only on an explicit choose or an explicit clear. A combobox that\n empties its own selection as a reader types is the usual failure, and it loses\n data silently.\n \n **`Command` is the leaf row and it ranks nothing.** `matchRange` is a prop, so\n whoever filtered the list decides where the match fell, and one command object can\n be spread onto the row with its `keywords` accepted and never rendered.\n \n **`Calendar` keeps a disabled date drawn and in the arrow path, and refuses to be\n chosen.** A date that is simply absent is indistinguishable from a grid that\n failed to render that week, and a reader paging with the arrows should not have\n the path change under them.\n \n **`DatePicker` reseeds the month on show from the chosen value**, so re-picking a\n date in another month does not silently reset the grid to the month the caller\n created the field in.\n \n **`InputGroup` puts the focus on the control and the ring on the control.** The\n frame is a `div` and is never focused, because putting the ring on a wrapper is how\n an indicator ends up around the wrong box.\n \n **`NumberField` clamps a value as it arrives, not only as it is typed**, and an\n empty field reports `null` rather than zero. Base UI clamps typing and refuses the\n steppers past a bound but lets an out-of-range `value` straight through, so the\n clamp is applied on the way in and the consequence for a controlled consumer is\n documented: `onValueChange` is the authority.\n \n **`OneTimeCode` holds the code as one value, not six.** Backspace removes a\n character from the string and the rest close up, which is the only rule for an\n empty box that is not wrong for somebody. Base UI's own field strips the label from\n the first segment on purpose, so a visually hidden `<label for>` is rendered\n instead, and its steppers are put back in the tab order because an announced\n control a keyboard cannot reach is worse than a second stop.\n \n **`Form` makes the error and the way out of it one unit.** `FormError` draws the\n message and the caller's `action` in a single live region that is in the document\n before either of them, so a server error that arrives with the form is announced\n rather than appearing silently.\n \n **`Label` keeps the required mark as decoration.** The control's own `required` is\n what is announced and what the form enforces, so a second signal that says\n something slightly different is the thing to avoid.\n \n ## The ranking is now one module, not two surfaces' private copy\n \n `locate` and `RANKS` moved to `packages/ui/src/lib/rank.ts`, and the `Combobox`\n imports them from there rather than from the `CommandPalette`. A command palette\n and a combobox that each carried their own scorer would agree for a month and then\n diverge on the case nobody thought about, and a reader would find a query that\n floats to the top in one surface and sinks in the other. The first version put the\n scorer inside the palette and the combobox reached into a composite for it, which\n compiles and typechecks and is still wrong: the next surface to need one has no\n honest module to import, and the tempting answer is to copy the one it can see.\n- 3260391: A hero action is the element you asked for, and a product row can carry its own sentence\n \n **`HeroAction` is a union, so a wrong action is a compile error.** It was one shape\n with an optional `href`, which made both likely mistakes silent. `{ label: 'Start\n free' }` compiled and rendered a primary button that went nowhere, and a hero's\n first action is almost always a link, so that is the likely one and it looks\n correct on the page. `{ label: 'Start free', href: maybeUrl }` compiled too, and\n rendered a button whenever `maybeUrl` was `undefined`, which is a runtime branch the\n type said nothing about.\n \n So the link arm **requires** `href` and the button arm **forbids** it, as\n `href?: never`. The second is the one worth having: a value that is sometimes a\n string and sometimes `undefined` is now a type error rather than a button. That is\n the ticket's \"a caller should not be able to get it wrong silently\", and it is a\n type rather than a runtime branch, which is the criterion the ticket asked for.\n \n **The forward arrow now follows the element, not the position.** It was\n `index === 0`, so an inert button was the one control in the action row wearing the\n mark that says it can be followed. A first action that is genuinely a button gets no\n arrow, and a second action gets none even when it is a link, because the arrow marks\n the row's one primary destination.\n \n The default **variant** stays positional, deliberately, and the two now move\n separately. What looks primary is a fact about the row's shape; whether it navigates\n is a fact about the element. Asserting they do not move together is the point,\n because the arrow used to follow position for both.\n \n The ticket's claim that the Block \"renders every action as a plain `<Button>`\" was\n stale, the way the Cta01 one was: `CtaLink` had already fixed the rendering. What\n was left was the half that had not been fixed, the type, plus the arrow and a test\n file, which did not exist. Eight tests assert the rendered element for each arm, the\n negative as well as the positive, the arrow on a link and its absence on a button,\n and `newTab` with its `rel`.\n \n **`ProductGrid01Product` gains an optional `detail`, and the JSDoc settles which\n level a sentence belongs at.** The documentation previously said a second line\n \"belongs above the grid in `description`, where it applies to the set\". That is true\n of a sentence about the set and false of a sentence about one product, and the\n company site had five products each carrying a sentence that was true of one and\n vacuous beside the other four. The migration folded nothing in, so three published\n sentences were dropped and the ledger recorded the loss as a catalogue gap rather\n than papering over it with a copy edit.\n \n Both levels now exist and the question to ask is written down: **does the sentence\n survive its neighbours?** A `description` would be equally true if you deleted any\n one row. A `detail` is false, or vacuous, beside the other four. A tagline is the\n shortest true thing about a member and reads the same in every row, which is what\n distinguishes it from a `detail`.\n \n A `detail` is drawn only when passed. A row that reserved the line would push every\n row below it down by one, and this is a stack of full-width rules where a ragged\n left edge is the most visible thing on the page. Six tests render both shapes,\n because a test asserting only the new field would pass on a Block that had quietly\n stopped drawing the grid-level description, which is the half every existing caller\n depends on.\n \n **Two proofs that the new tests can fail.** Reverting the arrow to `index === 0`\n fails exactly one case, the one the defect lives in. Reverting\n `childLevel(headingLevel)` to a hardcoded `h3` in the card-title work fails two.\n Neither defect would have been caught by an assertion of the shape the ticket\n described.\n- 4650aff: Add `Meter`, so a bounded measurement can show the limits it is approaching\n \n A disk at 95 percent and a test at 95 percent are the same reading until you say\n where the line is. Prism had a Progress for a task moving toward an end whose\n length is not known in advance, and nothing for a quantity that already has an\n answer against a limit the caller knows.\n \n ```tsx\n <Meter\n value={97}\n label=\"Storage used\"\n valueText=\"97 gigabytes of 100\"\n thresholds={[\n { at: 80, tone: 'warning' },\n { at: 95, tone: 'destructive' },\n ]}\n >\n <span>97 of 100 GB</span>\n </Meter>\n ```\n \n **The thresholds are the design.** A fill on its own is one number, and a number\n with no limit beside it cannot be acted on. Drawing the caller's own limits as\n notches on the track puts the boundary next to the reading, so a consumer passing\n a latency budget gets a latency budget rather than a generic bar filled to a\n similar fraction. The Component has no opinion about which numbers matter.\n \n **It stays neutral below every threshold.** It can see that a value is 40 percent\n of a maximum; it cannot see that 40 percent is a problem. The same number is\n routine on a latency budget and urgent on a disk quota, so a Component that\n coloured itself would be making a claim about the caller's product on the\n caller's behalf.\n \n **It is drawn as a hairline with ticks, not as a bar.** The Progress in this system\n is a two-pixel rounded trough, and a Meter that looked like one would be read as\n one. The difference is visible before it is read.\n \n `role=\"meter\"` rather than `progressbar`, which is the ARIA distinction the whole\n Component turns on, and assistive technology reports the two differently. It is\n not a live region: the change that moved the reading is usually the thing worth\n announcing, so a consumer that wants it announced wraps it in a `LiveRegion`.\n \n Two failures are handled rather than rendered, because neither is visible in a\n screenshot. A value past the maximum is clamped to the track instead of drawn\n past its end, and a scale whose minimum equals its maximum draws an empty track\n instead of a `NaN` width, while still measuring.\n- 932ce96: Add the overlay and menu substrate: alert dialog, context menu, menubar, sheet\n \n The eight Items that every menu-shaped surface in a product otherwise hand-rolls.\n Each is composed on the Base UI primitive rather than reimplemented, because focus\n trapping, portals, dismiss-on-outside-press and escape handling are four behaviours\n that are already correct there and four chances to get one wrong here.\n \n Each one makes a decision a consumer would otherwise make badly:\n \n - **`alert-dialog`** removes dismissal from the *type*, not just the default.\n `modal` and `disablePointerDismissal` are omitted from its props, so a caller\n cannot pass a value that re-enables outside-press dismissal on a surface whose\n subject is a decision. Escape still closes, and there is no corner X, because an X\n is a third answer to a question with two. `AlertDialogAction` is its own part,\n styled destructive by default, because the one button that must exist is that one.\n - **`context-menu`** knows which rows are commands and which are destinations.\n `ContextMenuItem` is a `div role=menuitem`; `ContextMenuLinkItem` is a native\n anchor. Two parts rather than one, so the \"open in a new tab\" case cannot be a div.\n - **`hover-card`** can never be the only route to what it shows, which is what makes\n its delay safe. The trigger is a real anchor, so a long delay costs a reader\n nothing, and `delay` is a prop because a pointer rest is a fact about the reader.\n - **`menubar`** is one Tab stop and owns the arrows inside itself, and it requires a\n `label` because nothing announces a bar until focus lands on it.\n - **`navigation-menu`** knows it holds links, so it ships no command part at all, and\n a closed group holds no links with `keepMounted` as the stated seam.\n - **`sheet`** is the Dialog with an edge. `side` is required and excludes `center`.\n All six overlay behaviours are inherited, not reimplemented, and a sheet dismisses\n on an outside press, which is the contrast with the alert dialog.\n - **`collapsible`** wires the trigger and the region with the library's own\n `aria-controls` and `aria-expanded`, and unmounts the closed panel so find-in-page\n and a screen reader see the same page the reader does.\n - **`resizable`** puts the whole behaviour on a focusable `separator` with a value,\n and remembers nothing: the position is a prop and a callback, because a design\n system cannot know a reader's panes.\n \n **`resizable` is not composed on Base UI, because Base UI 1.8.0 ships none.** The\n package's exports were enumerated and there is no `./resizable` and no `Resizable*`\n symbol, so the separator role, the focusable-divider keyboard model and the pointer\n drag are authored here. That is the one hand-rolled overlay in the batch, and it is\n confined to the three things an overlay must not reimplement, none of which a\n divider needs. It should be revisited when Base UI ships one.\n \n The focus-indicators gate test no longer pins which slot happens to sort first in\n the composite-widget bucket, nor the exact excluded total. Every additional menu in\n the package adds members to that bucket, so both were tests that failed when a\n correct Component was added. The count is now bounded from below, which is the\n invariant that holds as the tree grows.\n- e14a903: `ProcessFlow01`: a pipeline of any length, drawn in order across as many lines as it needs\n \n `ProcessRail01` holds two, three or four steps and refuses a fifth in its type, and\n that decision is not in question. A rail is a claim about a sequence on one line, and\n a rail that quietly dropped a step to fit a width would be a diagram of a process that\n is not the process. A company site states six stages and was drawing them as a grid of\n short points, where the six ordinals and the terminal label were lost: a set read\n where a sequence had been.\n \n So the repair is a second shape rather than a wider tuple. Raising the rail's ceiling\n to six would have made the fourth column unrepresentable as a type error, which is\n the property that made the original decision good.\n \n **It is one ordered list, and the layout is built around that.** A flow that wrapped\n into a list per line would be several lists, and a screen reader would announce three\n lists of two, which is precisely the set-where-a-sequence-was this Block exists to\n prevent. So the stages are one `<ol>` and the wrapping is done by the grid. That has a\n consequence worth stating: the Block never learns where a line broke, and therefore\n cannot draw anything that is only correct on the widest screen.\n \n **The ordinal is the continuity mechanism, and it is treated as one.** It runs\n continuously from `01` to the last stage and it is stated as text rather than only\n drawn, so a reader who lands on stage four hears `04` and knows it continues stage\n three at whatever width they are using. Nothing else in the Block is load-bearing for\n the sequence, and that is why nothing else needs to know where the line broke.\n \n **The thread is a line through the stages, not a box around each one.** Each stage\n draws a top border and the grid separates them by a single pixel, so a run of stages\n on one line reads as one line broken by hairline gaps and the gap between two lines is\n wider. It is the rail's technique, and it is chosen here for one reason: it holds at\n any column count, including the single column a phone gets, without the Block knowing\n anything about it. A connector drawn between a stage and its successor would need the\n column count to be right and would be wrong at every width the type does not describe.\n \n **`stages` is an array and `columns` is a prop**, which is the split the ticket asks\n for. The length is content and the grid carries it; how many sit on one line is\n layout, and a caller who has to state both has to keep them in step by hand.\n \n **A flow of fewer than two stages throws.** One stage is a label, not a sequence, and\n the line the Block draws through it would claim a sequence that is not there. The\n message names `ProcessRail01` as the answer for two.\n \n ## The proof that the tests can fail\n \n The two assertions that matter are structural rather than visual, and each was proved\n by writing the mistake it exists to catch:\n \n - An ordinal that restarts per row, which is what a per-row implementation does, fails\n two cases: the continuous-ordinal assertion and the terminal-label one, because the\n last stage is then no longer the one the label is attached to.\n - A list nested per group of three, which is the set-where-a-sequence-was failure\n arriving through the markup, fails four.\n \n An assertion that the flow \"looks like\" a sequence would have passed both.\n \n The stage names are deliberately **not** headings. The sequence is the list, and\n promoting six stages to headings would put six entries in the outline for one process,\n so the section title is the only heading this Block renders.\n- 4650aff: Add `RunConsole01`, a run as a heading, a budget and the steps that got there\n \n The surface the fourth Kind exists for, and a Block rather than a Component\n because a run console is not one thing: it is a measurement beside a sequence\n beside a stream, and a reader needs all three in the same frame to answer the\n only question they have, which is whether the run will finish and what it is\n costing.\n \n ```tsx\n <RunConsole01\n title=\"Nightly reconcile\"\n streaming={running}\n copy={{\n budgetLabel: 'Budget spent',\n budgetValue: (value, max) => `${value} of ${max} credits`,\n stepsLabel: 'Run steps',\n title: 'Run',\n waiting: 'Waiting for the first step',\n }}\n budget={{ value: 4200, max: 5000, thresholds: [{ at: 4500, tone: 'warning' }] }}\n steps={steps}\n />\n ```\n \n **It reimplements nothing.** The budget is a `Meter`, which draws the limits the\n caller named and stays neutral below all of them. The steps are a `Timeline`, which\n draws each duration to scale against the slowest one. What the Block adds is the\n arrangement, and two facts belonging to neither component: a run with no budget\n gets no budget meter, because a local run has no ceiling to be near and an empty\n meter is a measurement of nothing that is invisible because an empty meter looks\n like a meter; and the budget sits above the steps, because the first question about\n a run is whether it will finish and two columns would make the reader choose.\n \n **The live region exists only while the run is arriving.** A test caught this. The\n first draft wrapped the steps unconditionally, which left a live region on the\n page for a run that had already finished: it announces nothing on mount, but it is\n still there, and a later re-render with different steps is an unrelated change it\n will announce. Mounting it when the run starts is also the order that does not\n lose an announcement, because the region is on the page before the first event\n arrives, which is when a screen reader is listening. It wraps the steps and not the\n budget, so an append does not re-read the spending every time.\n \n **`LiveRegion` is a server Component, and that is a correction.** It shipped in\n 0.8.0 carrying `'use client'` while reading its props, holding no state, running no\n hook and taking no event handler. The directive was a claim about work the\n Component does not do, and sixteen of forty-one components in this package are\n client. A live region is announced by the browser's own mutation observer rather\n than by JavaScript, which is the reason it is a good primitive: it works in a\n server-rendered page.\n \n ## The client budget\n \n The whole-tree client bundle measures **108.1 KB** with this branch's components\n in it, against a 116 KB ceiling, and the five components here cost about **0.1 KB**.\n \n That is worth stating plainly, because the number looked very different for a\n while. An earlier draft of this branch reported that the components added 2.3 KB,\n crossed a 92 KB ceiling, and needed the ceiling raised. That measurement was taken\n against a gate whose roster read two directories, and it was wrong for the same\n reason the old ceiling was: 53 emitted client modules were never read. The roster\n has since been widened to the whole tree, the honest figure is 108 KB, and these\n components land inside the existing ceiling with about 8 KB of headroom.\n \n **No ceiling change is proposed here.** The one that was drafted has been dropped\n rather than applied, because it would have moved a number to accommodate a\n measurement that was itself measuring the wrong set of files.\n- 3260391: `StackGrid01` names the real product behind a codename, and `Cta01` states the rule it enforces\n \n **`StackPart` gains an optional `realName`.** The value goes inside the tile the\n Block already draws, so it is an additive optional field, matching `ProductGrid01`'s\n `pack` and `StatusLedger01`'s `detail`. It renders only when passed: a tile that\n reserves a line for absent content is a layout shift on the first thing a reader\n scrolls to, and because the grid is a grid, an empty line in one column would also\n knock the row's baseline out for every tile beside it. Both are asserted, the\n second by counting elements rather than by checking a class.\n \n **`Cta01`'s JSDoc told consumers the opposite of the truth.** It said a panel wants\n `default` or `secondary` and never `outline`, which was the answer *before* the\n `outline` variant was given its ink, and it contradicted the code forty lines below\n it, where the second action defaults to `outline`. A consumer reading the type would\n have been actively misled. The settled rule is the inverse on the first variant:\n `secondary` and `outline` are both fine and `default` is the one to avoid, because\n the band is `bg-primary` and `default` fills with `--primary`, so a default action\n on this band is the band's own colour against the band's own colour. Its label is\n legible, because `primary-foreground` on `primary` is a gated pair, and the control\n has no edge, which is a different defect and the reason the sentence is about the\n fill rather than the text.\n \n **A test that measured a pair it believed in.** The existing resolved-ink test\n asserted the pair the test itself thought the defaults resolve to, so it stayed green\n on a Block that had stopped asking for it. The new case renders `Cta01` with no\n explicit variant, reads the utilities off the two anchors, maps each to the token it\n names, and resolves those tokens from the emitted CSS. It asserts the resolved pair\n rather than the class string, because a class-string assertion passes on a token\n change, and it adds two things a ratio cannot see: that the ink is stated rather than\n inherited, and that the fill is one the band is not.\n \n The ticket's single lavender-dark figure understated the blast radius. The base pack\n failed in **both** modes, at 1.00:1 and 1.01:1, and seven of twelve combinations were\n affected. It is 13.59:1 or better in all twelve now.\n \n **The acceptance criterion asked for the wrong instrument.** It asked for a row in\n the token contrast gate, and that gate structurally cannot see this class of defect:\n it measures token pairs, and what failed was a component declining to use a value it\n was relying on by accident. `check-variant-ink.mjs` is the gate that holds it, and it\n already existed.\n- e14a903: The fourth Kind: `live`, a surface whose content changes without a navigation event\n \n **This is a breaking change, released as a minor: 0.11.0.** `CATALOG_KINDS` gains a\n member, so a consumer switching exhaustively over `kind` is broken. A minor is the\n right line for a breaking change from a `0.x` version, where anything may change at\n any time, and taking 1.0.0 would declare the public API stable rather than describe\n this change. `CONTRIBUTING.md` says a breaking change is a `major` bump, so this is\n the one place the convention and the record disagree; the disagreement is deliberate\n and `DESIGN.md` carries it with the reasoning.\n \n **A `live` surface is what the other three cannot express.** A Component, a Block and\n a Page are all rendered from props, and props arrive when the caller says so. A run's\n event log, a monitoring view, anything fed by a socket, changes on its own. That is\n the whole of the fourth Kind, and it is why `RunStream01` is the first client\n Component in the package: a surface that receives events owns the subscription that\n delivers them, and that is not an accident of implementation.\n \n **Prism owns the surface, the consumer owns the transport.** `subscribe` is a\n function the consumer supplies and a function it tears down. There is no socket, no\n endpoint, no retry policy, no persistence and no provider to mount, so a consumer\n that already has a connection passes it in and one that does not has not acquired a\n client runtime by importing this.\n \n **Four transcriptions were found by the compiler rather than by a reader**, which is\n the argument for the ties that already existed. `STORE_KINDS`'s `_KindsMatch`\n assertion fired on the first edit. The exhaustive `KIND_LABELS` record added\n earlier fired the next, and adding a Kind is now a compile error in both places. The\n corpus builder's `KIND_SEGMENT` produced a mirror path of `undefined` and the\n `llms` gate reported it. And the site's three copies of the Kind labels were\n collapsed into one table, so the fourth Kind is one edit rather than three.\n \n **One divergence is deliberate and named.** A `live` surface ships as\n `registry:block`, because the `type` field belongs to the shadcn registry schema,\n which has no live surface and rejects a type it does not know. The **Kind** is ours\n and is `live` in `CATALOG_KINDS`, the corpus, the MCP tools and the site's Sections.\n The registry is a derived internal artefact, never served and never an install lane,\n so the two vocabularies are allowed to differ because only one of them is ours. The\n catalogue gate carries it as a named exception keyed on the Item's own `source`, so\n the exception cannot drift from the thing it excuses, and the other 103 items are\n still compared strictly.\n \n **The Section plurality rule is gone, and two Sections is why.** It read that a\n Section holding Items is plural. `patterns` broke it from the prose side and `live`\n from the catalogue side, because `components`, `blocks` and `pages` are plural nouns\n and `live` is an adjective with no plural. A rule two of nine Sections decline is a\n convention, not a law, and a second exception would have invited a third. What is\n asserted instead is the fact underneath it: every Section is registered once, in the\n root ordering, at a segment the manifest agrees with.\n \n ## The proof that the tests can fail\n \n `RunStream01`'s fourteen tests are proved by writing the mistakes they exist to catch.\n Appending instead of sorting, which is the reconnect bug, fails the ordering cases.\n And three defects were found by the tests rather than reasoned about in advance:\n \n - **`statusLabel` rendered twice.** The heading fell back to it when no `title` was\n given, so \"Failed after 3 attempts\" was the surface's subject *and* its state. A\n status is not a heading.\n - **`initial` was a trap.** It read like the current event list and quietly seeded\n only once. It now also *reseeds* when `resubscribeKey` changes, which is what a\n second run in one surface means, and the log clears with it rather than the old\n run's events growing the new run's.\n - **A `setNewest` before its declaration**, which the run-switch test caught as a\n temporal-dead-zone error and which no type checker in this repository would.\n \n **A `live` surface is budgeted, not exempted.** A Block composes Components, so its\n own client figure restates theirs. A live surface is the only client code a consumer\n pulls in *for itself*, so it has a row: 10 KB, of which `ScrollArea` is 8.1. The\n figure is set above the measured size, because a budget below it is a wish.\n- 4650aff: Add `Timeline`, so a run of events can be read for where the time went\n \n A run's events in order answer \"what happened\" and are silent on the only other\n question anyone has about an agent run, a deploy or an import: which step was\n slow. Today that means reading every number and doing the arithmetic.\n \n ```tsx\n <Timeline\n entries={[\n { id: 'plan', title: 'Plan', duration: 900 },\n { id: 'read', title: 'Read feed', duration: 4200 },\n { id: 'edit', title: 'Edit source', duration: 1200 },\n { id: 'done', title: 'Ready' },\n ]}\n label=\"Run steps\"\n />\n ```\n \n **The bars share a left edge and are scaled against the longest step in the run.**\n That is the whole idea, and the relative scale is the deliberate choice: the shape\n of the run is the question, and a shared zero baseline would be a second axis\n nobody reads. The consequence worth stating is that the bars answer \"which step was\n slow\" and deliberately do not answer \"how long did the run take\", because only the\n caller knows whether its steps ran sequentially or overlapped.\n \n **A step with no duration draws no bar.** A run's opening event has no length, and\n a zero-width bar beside it would read as \"this was instant\" rather than \"this has\n no duration\", which are different claims and only one is true.\n \n **It is an ordered list, so the sequence and the count are in the accessibility\n tree.** The marks and spine are `aria-hidden` on the rail that holds both, and the\n entry's own words carry the state, so nothing depends on a reader distinguishing\n the marks. It is not a live region: a run still arriving is normally wrapped in a\n `LiveRegion` by the caller, because announcing the list itself would re-announce\n the whole run on every append.\n \n Three degeneracies are handled rather than rendered, and all three are invisible in\n a screenshot: a run where no step reports a duration draws no bars rather than\n dividing by nothing, a zero duration does not win the longest-step comparison and\n scale everything else away, and the spine stops at the last entry rather than\n trailing past the end of the run as though a step had not arrived.\n \n The four states are a different vocabulary from the four product-capability tiers\n a `StatusLedger01` row carries, and they are two lists about two subjects rather\n than two lists about one: a `planned` agent run is a category error and a `failed`\n capability is a category error, which is the test that tells them apart.\n\n## 0.10.2\n\n### Patch Changes\n\n- `Hero01`'s headline and supporting line take a node rather than a string\n \n `SectionHeading` has always taken a `ReactNode` for both, so a `string` here made\n this Block the odd one out rather than making it stricter. Three of the five\n product sites set the emphasised word in their own mark inside the headline, so\n the narrow type would have cost each of them the emphasis or a second component.\n \n A widening, and additive. Every string a site renders is still the site's own, and\n `check-block-copy.mjs` still holds this Block to shipping no copy of its own.\n\n## 0.10.1\n\n### Patch Changes\n\n- A drawing's data props accept a read-only array\n \n `PulseGraph`, `PulseSeries` and `Diagram` took `SomeNode[]` rather than\n `readonly SomeNode[]`, so a consumer whose content is held as `as const` had to\n copy the array at every call site to satisfy the type. Every NaniSoft product site\n holds its landing content as `as const`, so the copy was not optional: the first\n consumer to compose a running hero had to write `nodes={[...FIGURE.nodes]}` to get\n it to compile, which is a cost in exchange for nothing.\n \n The props now take a read-only array. This is a widening, so it is additive rather\n than breaking, and it is the correct shape for a prop that only reads its data: a\n Component that mutates an array a caller passed is a Component that has taken\n ownership of it, and none of these three do.\n\n## 0.10.0\n\n### Minor Changes\n\n- 75434f8: Add a running figure to the system, and the ambient cycle scale that prices it\n \n The system had one law of motion: motion is state feedback, on an 80/160/280\n millisecond scale, and there are no keyframes anywhere. That law was right and\n it held. It also meant a product page could say what its product does and show\n nothing about it, which is what happened to four sites at once.\n \n This adds the second law, and it is a separate law rather than an exception to\n the first. Feedback is a response to something the reader did. A cycle is a\n demonstration of something the system does, and no reader is waiting for it.\n The test for which one a motion is does not require taste: stop the animation\n and ask whether the figure is still true and still legible.\n \n **New Components**\n \n - `PulseGraph` draws a running system: named nodes, the relations between them, a\n marker travelling a rail, and a breathing focus. A node's `lane` says it is a\n stage in a sequence, which draws the rail; a node with no lane is a field.\n `carries` on a relation says the line is carrying something, and draws a head\n at its end so the claim survives motion being off.\n - `PulseSeries` draws a live instrument: columns rising out of a caller-named\n baseline, with a reticle crossing them once per cycle. The values are the\n caller's own and the tallest sets the scale.\n - `SignalField` is a field of marks: the atmosphere a figure is drawn over.\n Decorative by default, still unless asked to drift, and honestly named as the\n one component in the system that is texture.\n \n **`Hero01` gains the band it was missing**\n \n All four sites hand-wrote the same two-column grid, each with its own gap and its\n own breakpoint. It is now the `instrument` slot, and an action naming an `href`\n renders as a real link rather than a button that goes nowhere. The centred form\n is unchanged, so this is additive.\n \n **New tokens**\n \n `ambient` and `ambient-ease` are two closed groups measured in seconds, separate\n from `duration` because folding them in would have made one name mean both 280ms\n and 7.2s. The emitted contract asserts the closed set and fails any cycle under\n one second, because a sub-second cycle is a flicker and no value in the 80 to\n 280ms band could ever have caught that.\n \n **What this does not change**\n \n Nothing is hidden. No ambient rule sets `opacity: 0` and nothing waits for a\n script, an intersection or a timer, so every figure is complete at first paint\n and a consumer needs no exception to enable the `hidden-state` gate. Reduced\n motion is one `animation: none`, and because every resting state is the full\n form, that reader gets the same figure, still. All three are server Components,\n so this adds zero bytes of client JavaScript.\n\n### Patch Changes\n\n- cb16073: A variant that sets its own fill sets its own ink, and `outline` now says which\n \n `Button`'s and `CtaLink`'s `outline` variant carried `bg-background` with no `text-` for the life of the package. On the page ground and on a card the omission was invisible, because the ink a control inherits there is `foreground` and `card-foreground` is equal to it in all twelve pack and mode combinations, so the variant was correct everywhere the design system itself placed it and wrong everywhere else.\n \n `Cta01` draws its band as a filled `bg-primary` surface with `--primary-foreground` as the band's ink, and defaulted its second action to `outline`. The second action was therefore `--background` behind `--primary-foreground` and measured **1.00:1 to 1.10:1 in seven of the twelve combinations**: the base pack in both modes, where light mode pairs `#ffffff` with `#ffffff`, and all five pastel packs in dark mode. The button was in the document, focusable, announced, and unreadable. It was found by resolving computed values in a browser, because neither the class name nor the token contract says anything about it.\n \n The fix is `text-foreground` on the variant, which is a no-op on the page ground and on a card and correct everywhere else. Measured across all twelve after the change: lowest is 13.59:1.\n \n A consumer that passed `variant=\"secondary\"` on that action, as the product site did, saw no difference and needs no change. The default is unchanged, deliberately: `outline` reads as one filled action and one alternative, and it is now a safe default because it carries its own ink rather than because it was made to stop.\n \n Two gates hold it, and neither could have caught the original:\n \n - `check:variant-ink` fails a variant whose own fill has no `text-` utility of its own, read from the source. It reads source rather than the emitted stylesheet on purpose: Tailwind only emits a variant something uses, so the sheet is a record of what was chosen rather than of what is available. A state fill such as `hover:bg-accent` is exempt, because exempting it is the difference between a rule that finds the defect and a rule nobody can run.\n - a test resolves the pair from the emitted token CSS for all twelve combinations rather than asserting a class name, and pins the seven that used to fail by name. A test written over class names would have passed against the broken button, because the class name never changed; only the resolved pair did.\n \n `Cta01Action`'s `variant` now states which weight a panel wants and why, with the measurement, so a consumer does not have to discover it.\n- Updated dependencies [75434f8]\n - @nanisoft/prism-tokens@0.10.0\n\n## 0.9.0\n\n### Minor Changes\n\n- 93758a6: The face ships with the package, so a site writes one property for its typeface\n \n `@nanisoft/prism-ui` declared `--font-sans: Inter, ui-sans-serif, system-ui, ...` and shipped no font file. Naming a family is not shipping it: the first entry resolved to nothing and every page fell through to the platform's UI face, which is the one face a design system never means by its first choice. All four consumer sites were compensating with their own custom property and a build-time network fetch, and the company site had already shipped that way by accident.\n \n The face is here now, as three latin roman weights with the licence beside them, and the `@font-face` sources are relative to the emitted stylesheet so they resolve from a consumer's install rather than from the consumer's origin. That last point is the old line's failure exactly: it emitted the face at an absolute path, which resolved against whichever site imported it, and the face silently failed in all four repositories.\n \n A consumer's own override is now unnecessary rather than merely discouraged. `--font-sans` resolves to a face this package ships, so a site that deletes its `--font-sans` override still renders in the design system's face rather than in the platform's.\n \n Three gates hold the arrangement, and none of them is discipline:\n \n - `check:typeface.mjs` fails a token family that names no face this package ships and no family the reader's own machine provides, read from the **emitted** stylesheet rather than the source. The original defect was invisible in source: the rules shipped, the subsets were preloaded, and only the class carrying the variable was missing, so the family resolved to nothing at computed-value time with no error anywhere.\n - the same gate fails an `@font-face` source that is absolute or a web-root path, which is how the old line failed silently.\n - the build refuses to emit a stylesheet that names a face it did not copy, so a package cannot ship a sheet pointing at a file that is not in the tarball.\n \n The face is 70.7 KB and every consumer installs it whether or not it renders a page, because a subpath does not reduce install size. That is the accepted price of one package instead of two.\n \n The weight axis is absent, deliberately. The design system retired the width axis, and optical sizing, which it approximated, is the browser's initial behaviour, so a file carrying a width range is not the file this design asked for. 700 is absent for a second reason: the authored scale emits it and the shipped source renders it nowhere, and a 23.8 KB file no page renders is 23.8 KB every consumer downloads forever.\n\n## 0.8.0\n\n### Minor Changes\n\n- 86cbc20: The accessible name a control announces is now a prop, so a consumer can localise it\n \n `Pagination`, `Dialog` and `Breadcrumb` each shipped an `aria-label` that a\n consumer could not change, and `ProductSwitcher` shipped a `label` default that\n put the word \"Products\" into every consumer's navigation. A consumer could\n localise the visible text of a pagination step and not the name a screen reader\n announced for it, which left a control that looked localised and was half of it.\n \n What changed:\n \n - `Pagination` takes `label` for the region name, and each of `PaginationPrevious`\n and `PaginationNext` takes `label` beside its existing `text`, so the visible\n word and the announced name move together.\n - `Dialog` takes `closeLabel` for the built-in close control, so a localised\n dialog no longer announces \"Close\" in a product that never uses that word.\n - `Breadcrumb` takes `label` for its region name.\n - `ProductSwitcher` no longer defaults `label`. A switcher with no `label`\n renders a navigation with no accessible name, which is a real state the caller\n resolves, rather than one that is quietly wrong.\n \n Every new prop keeps the word Prism would have used as its default, so nothing\n changes for a consumer that never set one. The one behaviour that does change is\n `ProductSwitcher`'s, and it is the point: the default was a claim about a\n consumer's product that the prop existed so they would not have to make.\n- b31e444: Ship the consumer gate kit, so a cross-repository law reaches a consumer in one release\n \n The four repositories that consume this package coordinated through a prose\n contract mirrored in four files. It could not be enforced, because prose cannot\n fail, and it had already drifted: two sites that mattered held three\n implementations of one rule and one of the three rules was false.\n \n The laws are now the failure messages of gates in this package, and a consumer's\n repository holds only its own data.\n \n ```sh\n pnpm exec prism-gates # every gate prism-gates.json names\n pnpm exec prism-gates --gate=links # one gate\n pnpm exec prism-gates --json # machine-readable, for a test\n ```\n \n Configure it with a `prism-gates.json` at your repository root. A consumer holds\n its stylesheets, its pack map, its coverage floors, the destinations its own\n corpus gets wrong, and the custom properties its own build supplies. It does not\n hold the wording of a rule, because a wording held in four places is four rules\n that will disagree.\n \n The gates are `pin` (the design system is an exact version, and the token package\n is this package's dependency rather than yours), `retired-line`, `stylesheet-ownership`\n with `token-read`, `links`, `pack-boundary`, `hidden-state` and `runtime-token-read`.\n \n **This release adds the `gates` export subtree and a `prism-gates` binary, and\n adds `gates` to `files`.** It also means a consumer no longer needs to declare\n `@nanisoft/prism-tokens`: resolve it through this package, which requires it at an\n exact version, and the pair cannot be mismatched. Removing the token package from\n a consumer's `package.json` and from its `pnpm-workspace.yaml` is part of the\n adoption; the `pin` gate fails on either.\n \n The kit is outside `dist/` on purpose. It is a build-time program for another\n repository and must never enter a consumer's module graph or its bundle.\n- 86cbc20: Add `LiveRegion`, a region that announces what just changed in it\n \n Prism had no live region at all, so a result list that filtered as you type, a\n search whose count updated, and any stream that appended to the page all changed\n silently for anyone using a screen reader. The fourth Kind the design rules\n already decide, `live`, is specified to own an event log surface, and an event\n log surface is a live region, so this is the prerequisite rather than a\n convenience.\n \n ```tsx\n <LiveRegion busy={streaming} label=\"Run output\">\n {lines.map((line) => (\n <p key={line.id}>{line.text}</p>\n ))}\n </LiveRegion>\n ```\n \n Three decisions are in the Component rather than left to each consumer to make\n the same way:\n \n - **It renders nothing when it has nothing to say.** A live region that is\n always present announces every unrelated state change of its ancestors, so an\n empty region is not rendered at all rather than rendered as an empty element.\n The test is \"renders nothing\" rather than a list of the nothing values, because\n an empty string, a null state and a false condition are three routes to it and a\n list of three is a list a fourth route would miss.\n - **The politeness default is the least interruptive value still announced.** A\n run log that announces assertively interrupts a screen reader mid-sentence, and a\n consumer with a genuinely urgent event passes `politeness=\"assertive\"`.\n - **`aria-busy` is about the caller's knowledge, not about an animation.** A busy\n region is still readable, and a caller that sets it permanently has told\n assistive technology the stream never ends.\n \n The component owns the region and not the transport. A consumer owns the socket,\n the retry, the persistence and the order events arrive in, which is the same split\n the documentation Page makes with its navigation, and it is why Prism stays\n transport-agnostic and no consumer inherits a connection it did not ask for.\n- 86cbc20: Add `Mark`, so a search result can show which characters matched\n \n A command palette and a search result list both need to show why a result\n matched, and until this existed the only way to do that inside Prism was to drop a\n span with hand-written styling into a consumer's own markup, which the authoring\n contract does not permit.\n \n ```tsx\n <Mark text=\"component library\" ranges={[{ start: 0, end: 9 }]} />\n ```\n \n It takes the string and the ranges rather than pre-split nodes, because a caller\n that assembled the nodes had to compute the ranges and this Component has the\n string. The ranges are handled rather than refused: they are sorted, overlapping\n runs are merged, and a range that runs past the end of the string is clamped. Each\n is a thing a search backend does as a matter of course, and a Component that threw\n on any of them would be one a consumer has to wrap in a try, which is a worse\n failure than a highlight that stops at the last character.\n \n **It adds no semantics, and that is the decision rather than an omission.** A\n consumer marking a hit wants a visual difference, not a screen reader announcing\n \"highlighted\" between every character of a result. So it styles a `mark` and puts\n nothing else on it, and a consumer who wants the announcement writes it.\n \n The treatment is the existing warning token pair rather than a new role. A mark\n sits behind the text it marks, so it needs a ground and a foreground, and that\n pair is already measured against every surface in every pack and both modes by the\n token contrast gate. A search hit is emphasis and not a caution, and the two are\n kept apart by spending the existing token rather than by adding a role that means\n \"highlighted\" and will be re-pointed at something else within a year.\n- 86cbc20: Add `Tree`, so a file browser, a category tree and an outline have something to build from\n \n Prism had a working tree and could not reach it. The implementation existed as\n two private renderers inside the documentation Page, with its depth rule, its\n current-item marking and its rule that a group with no index renders a label.\n What was missing was that it was unreachable, and that its data type was bound to\n documentation navigation rather than being a tree's own vocabulary, so a file\n tree, a category tree, an outline and a knowledge base all had nothing to build\n from.\n \n ```tsx\n <Tree\n label=\"Documentation\"\n currentHref=\"/foundation/colors\"\n nodes={[\n { type: 'group', title: 'Foundation', items: [\n { type: 'page', title: 'Colors', href: '/foundation/colors' },\n ] },\n { type: 'divider', title: '' },\n { type: 'page', title: 'Overview', href: '/overview' },\n ]}\n />\n ```\n \n **It is navigable by the arrow keys**, which is the part a consumer cannot\n assemble for themselves. A tree is one Tab stop and the arrows move inside it.\n Composing the markup gives the Tab order of the document instead, which is every\n node in the tree and none of the arrows. The flat order is collected from the DOM\n rather than computed from the node data, so there is no second list to fall behind\n the first, and a label is skipped rather than focused and doing nothing.\n \n **A flat list renders a flat tree.** An outline is a tree of depth one, so a caller\n passes leaves and gets leaves. A Component that required nesting to express a flat\n structure would make the common case the awkward one.\n \n **A group with no index is a label, not a link.** This is the rule the\n documentation Page already reasoned at length and it is carried across rather than\n reinvented: a group with an index is a destination and renders an anchor, a group\n without one renders a span that carries no `href`, is not focusable, and cannot be\n reached by Tab. Inventing a route for it would publish an address that resolves to\n nothing. The empty case is authored too, because a tree that renders nothing is a\n control with nothing in it.\n \n The documentation Page keeps its own renderers and does not change. Its two rules\n are specific to a documentation rail, which is one shape a tree takes rather than\n the shape. Whether the Page later composes this Component is a separate decision\n this does not make.\n\n## 0.7.0\n\n### Minor Changes\n\n- 5c43520: Add `DocsShell`, a documentation page that takes a site's navigation as data\n \n `DocsShell` is a documentation screen: a navigation rail, the document, a\n contents rail, and a pager to the neighbouring pages. It replaces the unstyled\n `docs-shell` the retired line published, and it is the Page the three NaniSoft\n product sites already import by that name.\n \n The navigation is data. A tree of one page and a tree of twenty-seven render the\n same way, at any depth, and the frame is sized by what the caller passed rather\n than by a fixed arrangement, so three sites whose documentation sets differ in\n shape share one Page with no per-site fork.\n \n - `nav` and `toc` are closed unions of `page`, `group` and `divider`. Both are\n read in the order given: nothing is sorted or alphabetised, so a site that\n files its rail as a pipeline and its contents as a reference has both in one\n render.\n - The pager is derived from `nav` and `currentHref`. There is no `neighbours`\n prop, so a neighbour that is not in the tree cannot be expressed, and each\n consumer loses one derivation of its own.\n - A group with no `href` renders a `span` rather than an anchor with no\n destination. The three sites spell that case today as an empty string, and two\n of them carry a stylesheet rule that styles the result back into a label; those\n rules are now deletable.\n - A section carries no status, no badge, no count, no collapse and no sort. A\n status written into a title stays in the title as words.\n - Both rails are bounded against the viewport, because one site's tree is half\n again as tall as another's.\n \n The corpus fix that shipped with it: the props extractor read each declaration\n body twice with two depth counters that disagreed, so a Page whose `index.tsx`\n re-exported a value and a type from one sibling published every prop twice and\n swept the members of its navigation union into its own prop list. It now reads\n each body once.\n\n## 0.6.0\n\n### Minor Changes\n\n- 956fc8f: A call to action with a destination renders a link, and an icon is required only where a tile is\n \n Every primary call to action in the Block family declared an `href` its type then\n never rendered. The control was a `<button>` with no handler: the type said the\n component could navigate, the documentation implied it, and the markup could not\n do either. Sixteen of them across the family, five of them to another origin.\n \n **`CtaLink` is a new Component.** It renders a native `<a>` and carries the\n visual weight of a call to action, so assistive technology announces a link and\n the browser's own affordances all work: a status bar showing the destination, a\n context menu to copy it, middle-click to open a new tab. It is a Component in its\n own right rather than an `as` or `href` prop on `Button`, which is the answer\n `BreadcrumbLink` and `PaginationLink` already give twice in this package. It is a\n server component, so the swap costs no client JavaScript.\n \n `href` is required on `CtaLink`: an anchor without one is not a link. `newTab` is\n a declared prop and the only way to open a new browsing context, because `target`\n is not accepted, so the relationship cannot be opted out of by accident. When it\n is set, `rel` defaults to `noopener noreferrer`; a consumer that needs a\n different relationship passes `rel` and it wins. Prism does not parse the\n destination and does not decide what counts as external.\n \n **`Cta01` no longer offers an action that navigates nothing.** `Cta01Action`'s\n `href` is required, there is no optional arm and no dead-button arm, and a new\n `actionSlot` takes a control the Block does not own, such as a client router's\n link. The Block makes no guess about cross-origin. The one rendered change in\n this release is a button becoming the link its type always said it was: the link\n is styled from the same recipe as the button it replaces, at every shared variant\n and size, and a test asserts that class-for-class.\n \n **`FeatureGrid01`'s icon is required only where a tile is rendered.** The\n requirement is a union discriminated on `variant` rather than an optional prop,\n because an optional prop turns a safe-at-render condition into an unsafe one and\n permits an empty accent tile per feature. The `icon` variant is the one an\n omitted `variant` selects, so the default and the required icon live on the same\n arm; the `bare` variant renders no tile and requires no icon, and the type says\n so rather than the renderer finding out. `IconFeature` and `BareFeature` are\n exported, and the union is the type the site's API table is extracted from.\n \n **The version the four downstream site plans pin is `0.6.0`.** It is\n `@nanisoft/prism-ui@0.6.0`, and `@nanisoft/prism-tokens@0.6.0` with it, because the\n two are linked in `.changeset/config.json` and the pending\n `a-server-rendered-pack-boundary-can-be-mode-correct` changeset already declares a\n minor for both from `0.5.1`. This one declares a minor as well and so does not\n raise it.\n \n Migration: a `Cta01` action needs a destination. Where the action was a label\n alone, give it the `href` it was always declared with, or move the control into\n the new `actionSlot`. A `FeatureGrid01` in the `bare` variant can now drop the\n `icon` its type used to demand; the `icon` variant still demands one per feature.\n- d001983: A diagram is a Component, and a product's mark is one item rather than two copies\n \n Three of the four product sites draw something in the position where an empty box\n is most visible, and all of it was a canvas. A canvas reads computed style from\n one element while a pack is an attribute on an ancestor, so a scoped `data-pack`\n boundary hands it the pack's light values on a dark page, and no token gate can\n see it: the value the drawing holds is a legal token value, applied to the wrong\n pack. Cutting the canvases without a replacement is the alternative, and it\n leaves three heroes with an empty slot.\n \n `Diagram` draws named things and the labelled relations between them as inline\n vector markup. Every stroke and every fill is a semantic utility, never a\n resolved value, so a boundary restyles the drawing through the cascade exactly as\n it restyles a heading. It is a server Component: no hook, no mode, no context, no\n prop spread, and no client code, so a consumer renders it from a server file\n with no provider mounted. Its node mark is a circle and a relation is a path\n because both are shapes with no radius concept, which is what keeps a boundary\n from moving anything but colour; a `<rect>` was rejected because its corner\n attribute is a CSS property and no utility pins it. Every mark names an explicit\n stroke width, an explicit stroke and an explicit fill, because a shape left to\n its defaults either paints opaque black over its own labels or resolves its edge\n to the surface it sits on. A relation naming a node it does not have is dropped,\n and the count of dropped relations is on the element rather than swallowed.\n \n `ProductMark` promotes the two-part mark the product row and the product switcher\n each hand-wrote into one catalogue item. The ring is `brand-ink` and the core is\n `primary`, which is not a preference: a pastel brand value is a fill and never\n text, so the core may be `primary` and the name may not, and a wordmark reads\n `brand-ink` rather than `foreground` or `primary-foreground`. A hand-written copy\n gets one of those two wrong and nothing in the rendered result says which. The\n mark carries its own `data-pack` boundary, so a product's hue follows from the\n pack the caller passed rather than from whichever pack the page is wearing, and\n the boundary sits on an element with no radius utility, which is the placement\n the pack-boundary law allows. A product with no pack of its own is drawn as the\n full spectrum rather than as a colourless mark, built from the five series tokens\n because they are the only five-way colour set the contract publishes.\n \n `scripts/check-vector-ink.mjs` holds both Components to the emitted contract. It\n fails a literal colour, a gradient whose stops are not contract references, a\n `var(--x)` naming a property the contract does not publish, and a paint utility\n whose suffix is not a contract role, which is the last of the four a colour\n regex cannot see because a Tailwind class is a name and not a value. Its scanned\n set is a declared list of two file names rather than a pattern, a Component that\n is not on disk fails the run rather than emptying it, and every exclusion carries\n a reason and is printed on every run, with an exclusion that resolves to nothing\n a finding rather than a line that quietly stops appearing.\n- 4f9ce77: A server-rendered pack boundary can now be mode-correct\n \n The only dark selector for a pack was `[data-pack=\"<id>\"].dark`, which requires\n the mode class on the same element. A server cannot know the reader's mode, so a\n subtree carrying a second pack rendered in that pack's light values on a dark\n page. The token build now emits the two-member list\n \n ```\n [data-pack=\"<id>\"].dark, .dark [data-pack=\"<id>\"]\n ```\n \n as one rule with one declaration block, for all five packs. The compound half is\n retained, because it is what a dark surface on a light page is and it is a\n capability the package already published.\n \n Consumers get a second change alongside it. `themeAttributes` now takes `mode` as\n optional, because a required one is that same impossibility written into the type:\n the only way to ask for a boundary with no mode class of its own was to pass\n `'light'` as a guess, and the guess is wrong for half of readers. Omitting `mode`\n is the honest spelling of \"wear the mode of the element carrying `.dark`\". The\n change is a widening, so every existing caller still compiles.\n \n The theme-boundary law in `DESIGN.md` is rewritten to match. It previously\n instructed an implementer to put `data-pack` and `class=\"dark\"` on a boundary\n together, which works for a client-applied boundary and cannot work for a\n server-rendered one.\n- c126ee3: A stored theme is written when a reader decides, and not before\n \n `PrismProvider` wrote `prism-theme` on every resolved change, including the\n first resolution from the server-rendered attributes or from its own defaults. A\n first-time visitor therefore ended the load with a stored value they never chose,\n and that value outranked the site's default on every later visit, so a site that\n changed its default could not reach anyone who had ever loaded the page.\n \n The write is now gated on a decision. It happens when a caller changes the pack,\n the mode or the toggle, and when the library recovers a decision from\n `prism-theme-mode`, a key earlier lines wrote and this one does not. It does not\n happen on a first resolution from the document or the defaults, so a first visit\n ends with an empty store and a returning visit keeps its choice.\n \n Two readers of one stored value now share one rule. `resolveTheme` in\n `@nanisoft/prism-ui/theming` is the rule, `PrismProvider` calls it, and the boot\n script implements it as a string. The script cannot import the function, because\n the build minifies and a renamed identifier inside its own `try` would throw\n there and fail open, so the two are held together by a build-failing equivalence\n gate that runs the emitted string and the rule over one table of stored values:\n nothing stored, a valid pair, a valid mode with a retired pack, a valid pack with\n a retired mode, garbage, a JSON array, a JSON string, `null`, an empty object, a\n partial, a server-rendered pack and mode, and the retired key. Every fall-through\n is asserted to be whole, which is the divergence this fixes: the old string\n validated the pack and the mode independently, so a stored `{ mode: 'dark' }` with\n no pack half-applied there and fell through whole in the rule.\n \n A value that is present but is not a pair is no longer repaired field by field,\n and it is not cleared. It is the only record that this reader ever chose anything.\n \n The boot script records where the theme came from in `data-theme-origin` on\n `<html>`, and it is the only writer: the provider never touches it, because two\n writers would make the origin a race rather than a record. The value is one of\n \n | Value | Meaning |\n | --- | --- |\n | `stored` | the key held a valid `{ pack, mode }` pair |\n | `legacy` | the key was empty and `prism-theme-mode` held a recoverable mode |\n | `unparsed` | the key held a value that is not a pair, and the theme fell through whole |\n | `document` | the key was empty and the root element's own attributes supplied the theme |\n | `default` | neither did, so the consumer's defaults apply |\n \n It is written on every path the script reaches, so an absent origin is never an\n unread one. It is not `data-theme`, which the migration guide retires; it records\n the provenance of a resolution and never a pack.\n \n The migration clause reads `prism-theme-mode`, puts the recovered decision in\n `prism-theme` and removes the key it read, in one guarded step. That is how it\n expires: it fires only while a retired key still holds a mode, at most once per\n reader, and it is retired for good by deleting the last entry of\n `LEGACY_MODE_STORAGE_KEYS`. There is no date and no version constant in it.\n \n The pre-paint string gets its own ceiling in raw bytes,\n `check-boot-budget.mjs`, because it runs before paint on every page and adding it\n to the per-component client budget would corrupt the unit that table measures. The\n ceiling is derived by addition: the pinned non-migration base, plus the measured\n clause, plus one migration generation.\n- 76c8d96: Fourteen items, four catalogue gates, and a Block that publishes its interface\n \n **Fourteen items ship.** Three Components (`Prose`, `FactList`, `ProductSwitcher`),\n nine Blocks (`ProcessRail01`, `StatusLedger01`, `ProductGrid01`, `StackGrid01`,\n `LogoStrip01`, `NoteGrid01`, `InstrumentPanel01`, `SiteHeader`, `SiteFooter`) and two\n Pages (`NotFoundPage`, `BlogPostPage`). Every one of them earns its place by the\n growth rule rather than by taste: a marketing Block when at least three of the four\n sites compose that section today, a Component when it has one job no existing\n Component can express. The evidence is in the ticket and in each item's own\n JSDoc, which names the sites and the line numbers.\n \n **The three contested shapes are settled as specified, and the specifications\n held.** A process rail admits four steps and refuses five, in the type: `steps` is\n a tuple union, so a five-element array is a compile error and not a fifth column. A\n status ledger has four tiers rather than five, and the words for a tier are the\n caller's: the four sites between them use eight words for those four states\n (`live`, `available` and `complete` are one; `approved`, `specified` and `designed`\n are one; then `planned`; then `direction`), and the vocabulary is what `statusLabel`\n is for. A product row's mark is two parts because a filled dot fails contrast on a\n light card, so the mark is `ProductMark`: a hairline ring in `brand-ink` around a\n core in the product's own `primary` fill, with the name in the brand ink beside it.\n \n **Four gates the catalogue's own rules depend on now exist and are wired into\n `check` and, because they read only `src/`, into `build`.** `check-item-docs`\n holds AGENTS.md's claim that a documentation comment is the only path from an Item\n to its published interface. `check-block-copy` holds DESIGN.md's claim that a\n Block ships no copy and no sample data. `check-item-category` holds CONTEXT.md's\n closed set of seven Categories and the sentence that a Block and a Page have none.\n `check-block-imports` holds the claim that a Block fetches nothing and a Page\n imports no router and no data client. Each resolves its roots from its own\n location, fails a root that resolves to nothing, prints its coverage and every\n exclusion on every run, has no report-only mode, and is proved red by a fixture\n before it is proved green.\n \n **Two of those four gates were red on the tree they were written against, and both\n findings were real.** `check-item-docs` found seven exported functions whose\n documentation comment was not attached to the declaration: `CardHeader`,\n `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `SectionHeading` and\n `Hero01`. A comment three functions above a declaration does not reach the emitted\n `.d.ts`, so those seven were absent from the corpus. `check-block-copy` found\n `pricing-01` rendering the word \"Popular\" on a featured plan that passed no badge:\n a hardcoded claim every consumer of the Block inherited, published by the corpus as\n part of the Block's own documentation. The fallback is gone; a featured plan with\n no badge now renders no badge.\n \n **Five additive fields on four shipped items**, each with three of the four\n consumers passing a value the item could not take, counted per consumer with a\n file and a line and never per string. `SectionHeading.index`, for the mono ordinal\n all four sites render above every section head (www `Landing.tsx:49`, atlas\n `landing.tsx:48`, nexus `Landing.tsx:38`, alphalens `Landing.tsx:66`).\n `Cta01.secondaryAction`, for the pair of actions all four closing bands render\n (www `Landing.tsx:186`, atlas `landing.tsx:243`, nexus `Landing.tsx:221`,\n alphalens `Landing.tsx:285`). `Cta01.note`, for the footnote three of the four\n render under it (atlas `landing.tsx:250`, nexus `Landing.tsx:228`, alphalens\n `Landing.tsx:292`). `FeatureGrid01.numbered`, for the ordinal all four render on\n every card (www `Landing.tsx:166`, atlas `landing.tsx:116`, nexus `Landing.tsx:132`,\n alphalens `Landing.tsx:142`). `Hero01.align`, for the left-aligned hero three of\n the four compose (atlas `landing.tsx:69`, nexus `Landing.tsx:63`, alphalens\n `Landing.tsx:87`). All five are additive: nothing changed shape and nothing was\n removed.\n \n **A Block and a Page publish both an interface section and a composition section.**\n `packages/llms` read the extractor's output for a Block and threw the interface half\n away, so the tool told an agent a Block had no own props. The extractor reads a\n named props type off an emitted declaration whether that declaration belongs to a\n Component or to a Block, so both sections are now built and both are assembled into\n the mirror file, `llms-full.txt` and the Store's `doc`.\n \n The Store now carries both fields for every kind, and `get_item_props` prints\n both when it has both. The sentence \"a Block has no own props\" is gone from the\n tool as well as from the published document. It was the one place the two\n sections were still exclusive, because that tool branched on `item.props` and\n fell back to the composition section, so a Block was answered with its\n composition and a denial of the interface it publishes. The test asserts the\n sentence's absence, so a refactor that restores it fails even if the composition\n is still printed, and a second test covers the one-present case so the\n both-present branch cannot swallow it.\n \n **Four accessible names became required props** on `SiteHeader`, `LogoStrip01`,\n `NotFoundPage` and `BlogPostPage`, and a Block that ships no copy ships no\n reader-facing copy either. `navLabel`, `productsLabel`, `label`, `linksLabel` and\n `trailLabel` are words a screen reader reads out, and no Block should be choosing\n them for a consumer whose site calls its own landmarks something else. This is the\n one breaking change in the release, and it is on four new Blocks and two new Pages\n plus one new required prop on `SiteHeader`, all of which are new in the same\n release, so no existing call site changes.\n \n **`ProductSwitcher.label` has no default** for the same reason: a consumer whose set\n is not a set of products should pass its own word rather than accept a claim about\n what the set is.\n\n### Patch Changes\n\n- 8d24f32: A pack boundary's effect on corner radius is stated, and gated\n \n A scoped `data-pack` boundary resolves the pack's colour AND the pack's corner\n radius beneath it, because radius is the only non-colour member of a pack block\n and the token build emits it the same way. The radius range across the five packs\n is 0.5rem to 1rem, so a row of elements each carrying its own pack shows a spread\n of corner radii, and a small pill becomes a different pill five times over. The\n governing law stated the colour half and was silent on the shape half, so an\n implementer following it exactly produced mismatched corners and called it\n correct.\n \n The law now states both halves and where a boundary may sit. The agent-facing\n surface no longer says the attribute belongs on the root element, which it did\n while listing the radius among the values it will pick up: a tool that shows the\n value and denies the subtree is worse than a tool that is silent.\n \n `check-pack-boundary.mjs` holds the law. It reads the emitted `--radius-*`\n bindings rather than a list of class names, which is the part that matters: a\n scale step like `rounded-xl` is `calc(var(--radius) * 1.4)`, so an element\n carrying one is pinned to a step and the step is pack-relative. The first version\n of the gate treated \"carries a radius utility\" as the safe case and passed\n exactly the defect the ticket is about. The gate also reads the colour contract\n from the token source, so a second per-pack axis the token build has not been\n asked about is a finding rather than a property silently counted as a colour.\n \n One live boundary was found and repaired: the themes page scoped five cards, one\n per pack, each with `rounded-xl`, so the page a reader uses to compare packs was\n comparing shape as well as colour. The cards now carry a literal radius and the\n pack's radius is shown in the row's own label, where it is information rather\n than an accident.\n- feb1384: The catalogue is a set comparison between three lists, not a count\n \n A module on disk with no catalogue entry left every count correct. It shipped,\n `shadcn add` installed it, and `registry.json` listed it, so it was absent from\n the corpus, the documentation site and every agent-facing tool with a fully\n green gate run. The two lists a reader trusts, the catalogue and the registry,\n were on the wrong side of that hole, and the checks that believed they covered it\n compared lengths or compared a file against something derived from itself.\n `validate-registry.mjs` holds the registry against the disk and the manifest and\n never reads the catalogue. `sync-registry.mjs` reads the disk, so it cannot\n disagree with the disk about what is on it. The site's\n `test/catalogue-order.test.ts` holds the catalogue against the generated\n ordering and the documentation tree, and both are built from the catalogue, so a\n disagreement there is impossible by construction.\n \n `scripts/check-catalogue.mjs` reads the three sets independently and compares\n them in both directions, name for name, with one printed line per offending\n item. A length assertion cannot report which item is wrong, so nothing in the\n run reports a length on its own: the three set sizes print as supplementary\n coverage underneath the findings that carry the detail. All six directions a\n three-set comparison implies are implemented, plus the kind of an item in each\n pair of sets, the three naming rules that reconcile the three conventions, and a\n claim that a canonical key is held by only one entry in each set.\n \n The registry generator deliberately keeps reading the disk. Deriving the\n registry from the catalogue is the obvious way to remove the second list, and it\n would delete the disagreement the check exists to see, because the generator\n would then agree with the catalogue by construction.\n \n The version and the roster are two claims on two labelled lines, so a version\n bump and a roster error cannot produce the same output. The run states its\n coverage every time: roots resolved, files walked, files actually read, and how\n many comparisons passed. Roots resolve from the script's own location and never\n from the working directory, and a root that resolves to nothing fails the run\n naming both causes.\n \n `PRISM_CATALOGUE_ROOT` points a run at another package root. It changes which\n directory is read and nothing else, which is what lets the tests run the real\n command over a fixture tree and read the real output. The gate fails on a\n catalogue it cannot read, including an empty one: a roster that cannot be read\n and a roster with nothing in it are different facts, and only one of them is a\n green run.\n- 4744cce: The Slider thumb rings at full strength, and the surface check now reads both ways\n \n Two changes, and only two, in the component package.\n \n **The focus indicator.** The Slider's thumb suppressed the browser outline and\n drew its ring at `ring-ring/50`, so the element a keyboard user lands on carried\n an indicator that composites to between 1.14:1 and 2.74:1 against every surface\n in all six themes. That fails WCAG 1.4.11 on the one indicator a keyboard user\n has, and the Slider's own JSDoc says it is reachable by Tab. Thirteen other\n components already drew that ring at full strength; the Slider was the one that\n was missed. Nothing said so, because the contrast gate measures the `ring`\n TOKEN and the alpha is applied in the component rather than in the token, so it\n reads the Slider's ring as compliant.\n \n `packages/ui/scripts/check-focus-indicators.mjs` is the new gate. It reads the\n class strings on the elements of every shipped Component, derives the set of\n components whose own JSDoc claims keyboard reach or operation, and fails any of\n them that suppresses a focus style without declaring a `ring-ring` colour at\n full alpha alongside a `focus-visible:ring-N` width. The claim is read from the\n JSDoc because `AGENTS.md` names that as this repository's documentation source,\n so a component that stops claiming to be focusable is a documentation change and\n shows up in the table the run prints rather than in a list someone maintains\n beside the code. The unit is a class string on one element, so a Dialog's panel\n or a Tooltip's trigger can suppress the outline legitimately; every such\n exclusion is declared with its reason, printed with the slots it resolved to,\n and fails the run if it stops matching anything. The run fails if the table\n empties, and states how much it read, classified, tabled and excluded.\n \n **The surface check.** `scripts/check-surface.mjs` compared the published\n surface against the source in one direction for two of its three rules. A\n wildcard subpath that resolved to zero declarations used to leave those two\n rules reading no file at all and the run still printed its success line, which is\n how a published surface drifts from its source without a consumer failing first.\n A wildcard target that matches nothing is now a finding naming the target. The\n internal side, which the gate deliberately did not check, is now a declared\n boundary checked in both directions: a new file under `dist/lib` is a finding\n naming it, and a declared internal declaration that is no longer emitted is a\n finding naming it too. The boundary and the per-target match counts are printed\n on every run. The three existing rules and their messages are unchanged.\n \n **The version the four downstream site plans pin is `0.6.0`.** It is\n `@nanisoft/prism-ui@0.6.0`, and `@nanisoft/prism-tokens@0.6.0` with it, because\n the two are linked in `.changeset/config.json` and the pending\n `a-server-rendered-pack-boundary-can-be-mode-correct` changeset declares a minor\n for both from `0.5.1`. The patch declared here does not raise it: changesets\n takes the highest bump in the queue, and a minor is already queued. A site plan\n that pins `0.6.0` therefore resolves every subpath the export map publishes,\n including `./components`, `./blocks`, `./pages`, `./provider`, `./theming`,\n `./catalog` and `./styles.css`.\n \n No public prop, no type and no export changed. The one rendering change is a\n ring alpha on the Slider thumb, in both modes.\n- Updated dependencies [4f9ce77]\n- Updated dependencies [5dcb347]\n - @nanisoft/prism-tokens@0.6.0\n\n## 0.5.1\n\n### Patch Changes\n\n- 2fc6a0a: Retune `muted-foreground` so muted text clears 4.5:1 on the muted surface\n \n The base pack's `muted-foreground` moves from neutral 500 to neutral 600. It\n cleared 4.5:1 on the page background but fell to 4.34:1 on `muted`, where the\n pill, avatar fallback, kbd and tab-list pattern place it. The contrast gate now\n also checks `muted-foreground` on `muted`. The pastel packs were already pinned\n to their neutral 700 and are unchanged.\n- Updated dependencies [2fc6a0a]\n - @nanisoft/prism-tokens@0.5.1\n\n## 0.5.0\n\nPrism was rebuilt from a fresh repository on a clean break. The package is now a\npublished React library a consumer installs and composes, not component source to\ncopy. It ships one precompiled `styles.css`, the optional `PrismProvider` and the\n`data-pack` plus `dark` theme axes, and the Components, Blocks and Pages\ntaxonomy. There is no override path.\n\nChangesets are appended above this entry for every published change.\n",
|
|
8027
|
+
"markdown": "# @nanisoft/prism-ui\n\n## 1.5.0\n\n### Minor Changes\n\n- Add the third law of motion: arrivals, living fields, and the reading rail\n \n Motion now has three laws. Feedback and the ambient cycles are unchanged; the\n new law admits three authored mechanisms, each shipped as primitives rather\n than opened as a licence:\n \n - `Reveal` wraps a band in a scroll-driven arrival bound to the reader's own\n scroll through `prism-reveal` utilities behind a view-timeline capability\n check. No script arms it, a browser without the timeline ships no rule at\n all, and the universal reduced-motion rule hands that reader the visible\n page.\n - `ParticleField` is the one script-driven figure: a linked, pointer-answering\n medium whose complete still field renders on the server, whose loop stops off\n screen, and whose palette is re-read when the document's pack or mode\n changes. `AuroraWash` is the still counterpart: translucent planes in the\n pack's own colours on the new `aurora` cycle, transform-only.\n - `SiteNavbar` gains a reading rail on its sticky hairline, `scrollRail` on by\n default: a spectrum line filling with the page's own scroll, drawing nothing\n in a browser that cannot run the timeline. The current link's filled pill\n becomes a persistent spectrum underline, and `Hero01` gains a `backdrop` slot\n so a hero composes its own ground.\n \n `Backdrop01` offers `field=\"aurora\"`, and `prism-text-spectrum` is the one\n admitted motion on text: display ink lit at the sweep tempo with foreground\n ends, so it holds contrast in every pack and both modes.\n \n `scripts/check-motion.mjs` holds the new layer: a reveal or rail selector\n outside the `@supports` block naming its timeline fails the build, because an\n animation whose timeline fails to resolve pauses at its invisible keyframe.\n\n### Patch Changes\n\n- Updated dependencies\n - @nanisoft/prism-tokens@1.5.0\n\n## 1.4.0\n\n### Minor Changes\n\n- 3f9cc78: Add `MediaBand01`, an editorial band with a caller-owned media slot, and a `size`\n prop on `Section` for compact, default and tall vertical rhythms.\n \n The band owns the frame, the ratio, the surface and the order, and it never puts\n the copy on top of an arbitrary photograph. The media is the caller's node, so an\n image, a video poster or a composed figure can sit in the frame. The Section size\n keeps the existing default byte-identical and gives a page three authored rhythms\n instead of five site stylesheets inventing their own.\n\n## 1.3.0\n\n### Minor Changes\n\n- 250b34e: SiteFooter takes an `about` slot: a word from the site under the mark, for the\n sentence or two a footer carries about what the site is and who publishes it.\n Rendered under the lockup at the measure with no colour of its own, so the\n muted treatment and any headings stay the caller's. The design system's own\n site composes this footer now, and the two labelled facts its hand-written\n footer carried for years ride in that slot.\n- 290642e: Let `SiteNavbar`'s `nav` carry groups, rendered as one trigger that opens a menu\n \n A `nav` entry may now be a group: `{ label, children }` beside the existing\n link shape. On the row a group renders as the row's one control, a trigger\n whose menu lists its children as real anchors, marked with `aria-current` when\n the reader is on one; below the row's threshold the panel lists the same group\n flat, as a label above its links, so no destination is two disclosures deep.\n The current mark now reads the route as a place rather than a string, so a\n reader on `/components/button` finds `Components` marked rather than nothing.\n A flat `nav` renders exactly as before, and the new `SiteNavGroup` and\n `SiteNavEntry` types are exported beside `SiteNavLink`.\n- 8265662: A `ProductMark` takes a `mark`: the product's own drawing, in place of the\n disc. The node is the consumer's and fills the sized slot the disc occupies,\n so one drawing arrives at every drawn size; the name beside it is unchanged\n and still the announced half. `SiteNavbar` and `SiteFooter` carry it through\n the `product` prop they already take, so a site's logo is the sign in the\n bar, in the mobile panel and at the foot of the page from one fact, and the\n two-colour disc remains the mark of a product whose identity is a hue. The\n design system's own site draws its beam-and-refraction in both places now,\n in place of the spectrum disc.\n\n### Patch Changes\n\n- 290642e: Render `Sparkline`'s hidden numbers table without the `Table` Component's scroll container\n \n The readings behind a `Sparkline` are published to assistive technology as a\n table in a visually hidden wrapper. The wrapper is one pixel wide, so the\n horizontal scroll container `Table` wraps its table in always computed\n scrollable and never focusable, which the `scrollable-region-focusable` rule\n fails on wherever a `Sparkline` renders. The hidden table is now plain `table`\n markup with the same caption, row heads and cells: a table nobody can see\n cannot be panned, and the semantics are the whole of what it owes.\n- 290642e: Lay `Calendar`'s month grid as a table again: the `flex` came off its cells\n \n The weekday headings and the day cells carried `display: flex`, and a flexed\n `th` or `td` is pulled out of the table's column layout: the browser wraps each\n one in its own anonymous cell, and the seven-column month collapsed into a\n single stacked column at four times its height. In `Calendar` the day cell is\n the `td` itself, so the whole grid fell; in `MiniCalendar` the day is a button\n inside the cell, so the weekday row alone stacked above a surviving grid, and\n `DatePicker` and the `Calendar01` Block inherit whichever they compose. The\n cells are centred by the table instead (`text-center` with `align-middle` and\n `p-0`), which keeps the seven columns, the 36px cell, the 44px coarse-pointer\n floor and every state exactly as they were designed, and the weekday heading\n now sits over the column it names.\n- 290642e: Align `PulseSeries`'s scan reticle with the columns it reads\n \n The wash behind the reticle stopped short of the baseline while its own line\n crossed it, and the line rode inside the wash at a third of its width rather\n than at its centre. The wash is now one column slot wide and the line runs\n down its centre, so at every instant the wash covers exactly the column the\n line is crossing, and both shapes meet the baseline. The travel is the plot's\n own width, so the reticle enters and leaves aligned with the columns' field\n and its opacity ramps are spent on the margins beside the end columns.\n\n## 1.2.0\n\n### Minor Changes\n\n- Two new packs: Cyan and Gold\n \n The family's product map asked for two hues the palette did not carry, and a\n product's ground is a fact about the system rather than about the site that\n wears it, so the hues arrive here instead of as consumer overrides.\n \n `cyan` is the Nexus pack: pastel cyan-blue, generated at hue 215 in OKLCH with\n the same perceptually even lightness ramp as the other five, at the crispest\n radius in the set (0.5rem) beside Sky. `gold` is the AlphaLens pack: pastel\n amber-gold at hue 80, at the roomiest radius (1rem) beside Peach. Both carry\n the full shadcn-compatible contract, tinted neutrals at the family's chroma,\n and pass the contrast gate at 38/38 required pairs in both modes.\n \n `PACKS` in the theming module and `STORE_PACKS` in the llms corpus carry the\n two new ids, so a consumer's ground-pack validation and every agent reading\n the corpus see the same vocabulary. Peach returns to reserved. The published\n pack count is now seven pastels plus the neutral default.\n\n### Patch Changes\n\n- Updated dependencies\n - @nanisoft/prism-tokens@1.2.0\n\n## 1.1.0\n\n### Minor Changes\n\n- ad69fa2: A message carries the caller's per-message metadata\n \n `ChatMessage` gains an optional `meta: ReactNode`, drawn in the message's header\n beside the sender and the time. It carries the caller's own per-message metadata:\n the model a provider stated for a turn, a token count it reported, and the cost or\n the latency the caller derived. It is a node and not a set of Prism fields because\n no provider read publishes a cost or a latency, so the surface states no model, no\n count, no cost and no latency for itself and draws only what the caller hands it.\n The parts stay the way content is carried. The roster does not change: no new Item,\n Kind, Page or catalogue entry appears.\n- ad69fa2: The date view takes a per-item move control\n \n `Calendar01` exposes a per-item `handle`, a node the caller fills with their own\n control and wires to whatever owns their schedule. It exists for the one\n interaction a date view has that the Block does not own: a reschedule, which is a\n drag with a drop target, a new order, a write and a conflict, every one of them the\n consumer's. The Block places the node at the leading edge of the item's row and\n owns nothing beyond it: no drag, no drop, no order and no move callback, which is\n the arrangement `Kanban01` already takes for a board's drag.\n \n A `CalendarItem` gains an optional `handle: ReactNode`; `Calendar01`'s props are\n otherwise unchanged. The roster does not change: no new Item, Kind, Page or\n catalogue entry appears.\n- ad69fa2: The index gains a picture arrangement and a write grid\n \n Two coverage gaps in the record-index territory are closed by authoring two Blocks\n rather than by widening a union, and each is the arrangement `DESIGN.md` already\n named as forcing a second Item.\n \n **`CardIndex01` is the record index as pictures.** It draws a set of records as a\n grid of cards rather than as rows, with the caller composing each card's picture,\n name and price through `renderCard` and its per card actions through\n `renderCardActions`. It shares the record index's selection and announcement\n contract, which ticket 166 made the law for the index family: the same `selectable`\n union, the same held set of keys, the same three-state header control, the same\n single polite live region carrying a count, the same batch bar around the caller's\n own `batchActions` node, and the same `selectionScope` distinguishing a page-wide\n selection from a filter-wide one. It fetches, sorts, filters and slices nothing.\n \n **`RecordGrid01` is a grid edited in place.** Its columns are the shared\n `FieldSpec`, because a cell that writes is a control and the only vocabulary this\n package publishes for a control is the field specification; `ColumnKind` is the\n vocabulary for a value a reader reads and is held disjoint from it. The Block seeds\n each cell from the caller's row, holds the in-progress values so a cell redraws as\n it is typed, and reports every edit through a required `onCellChange` as the row\n identity and the column key. It fetches nothing, sorts nothing and pages nothing,\n and it owns no command.\n \n **Why `ColumnKind` was not widened.** The obvious alternative was an editable cell\n arm on `ColumnKind` plus a change callback. That arm would name a field Component\n (`Input`, `Select` and their kin), which is a member of `FieldKind`, and\n `scripts/check-spec-unions.mjs` reports it as an `overlapping-union` between the\n read union and the write union, in the same run that would otherwise pass. The\n disjointness the gate protects is exactly the fact that a cell that writes is a\n different job from a cell that reads, so the answer is a new Item that takes the\n field union rather than a wider column union.\n- ad69fa2: The record detail draws a dated trail as one of its relations\n \n `RelationKind` gains the `ActivityFeed01` arm, so a record's event history is a\n first-class relation drawn over `EventSpec` values rather than the generic `slot`\n arm. The union previously offered only `Timeline`, whose `TimelineEntry` carries a\n `duration` and a `state` and no `at`, so a record's dated occurrences had no\n relation to draw them. A `Timeline` is an instrument and a trail is not, so the fix\n is a new arm and not a moment on `TimelineEntry`: one Component carrying both a\n moment and a duration would be a second answer to a question that already has one.\n \n `RecordDetail01` draws the arm with the shared `ActivityFeed01` Block, exactly as\n it draws the other arms, and its relations are unchanged otherwise. The roster does\n not change: this widens an existing union and an existing Block, and no new Item,\n Kind, Page or catalogue entry appears.\n\n## 1.0.0\n\n### Major Changes\n\n- dbc8a65: Every superseded figure declaration is deleted\n \n The five local declarations of a reading over a population are gone from the\n published interface, and `MetricSpec` from `@nanisoft/prism-ui/spec` is the one\n shape a figure is declared with in this package. The expand step made each of the\n five an alias of the shared type so an old name resolved while its callers moved;\n this is the contract step, and it deletes every one of them now that a search of\n the whole tree, the documentation and the corpus finds no caller left. Deleted:\n \n - `Dashboard01Metric`, from `@nanisoft/prism-ui/blocks/dashboard-01`.\n - `ProjectDashboard01Figure`, from `@nanisoft/prism-ui/blocks/project-dashboard-01`.\n - `ChartCard01Reading`, from `@nanisoft/prism-ui/blocks/chart-card-01`.\n - `Trend01Item`, from `@nanisoft/prism-ui/blocks/trend-01`.\n - `Stat`, from `@nanisoft/prism-ui/blocks/stats-01`.\n \n `Dashboard01`, `ProjectDashboard01`, `ChartCard01`, `Trend01` and `Stats01` keep\n their own names, kinds and arrangements; only the shape is shared, because a shape\n is shared and a screen is not. This is the close of the metric expand-contract\n sequence, and the count of declarations describing a reading over a population in\n this package is one.\n \n The process stage shape, `ProvisioningStep`, stays declared with `Provisioning01`\n and is not migrated. It is the inside of one Item rather than a shape two Items\n would each declare, and a declared stage has not happened, so it is not migrated\n onto the event specification either.\n \n # Migration\n \n - Replace every use of `Dashboard01Metric`, `ProjectDashboard01Figure`,\n `ChartCard01Reading`, `Trend01Item` and `Stat` with `MetricSpec`, imported from\n `@nanisoft/prism-ui/spec`.\n - The shape is unchanged, so the migration is a rename of the type reference. A\n figure or row still carries `key`, `label` and `value`, and the optional `delta`,\n `deltaFormat`, `hint`, `series` with `seriesLabel`, and `href` with `hrefLabel`.\n - A build that reports one of the five names as missing is reporting a name that\n was deleted here rather than an exported entry that moved.\n- e9e73d6: Four forms take the shared field specification\n \n `AuthForm01`, `Contact01`, `Provisioning01` and `Signup01` now take the same\n `FieldSpec` and `FieldSpecGroup` a record write form takes, so the vocabulary a\n consumer learns on one screen is the vocabulary on all five. Each Block's own\n field declaration goes from the published interface, and a control Prism does not\n ship arrives as the `slot` arm of `FieldKind` rather than as a property on a\n per-Block type.\n \n - **`AuthForm01`**: `fields: AuthFormField[]` becomes `groups: readonly\n FieldSpecGroup[]`. The controlled `value` and `onChange` on a field go, because\n the specification is data and holds no callback; the Block is uncontrolled and\n the caller reads the values out of the form element. `error` becomes\n `submitError`, and `errorId` becomes an entry in the new `issues` list keyed by\n field. `AuthFormField` is removed and `AuthForm01Issue` is added.\n - **`Contact01`**: `fields: ContactField[]` becomes `groups: readonly\n FieldSpecGroup[]`, and a new `issues` list draws a message under a field.\n `ContactField` and `Contact01Option` are removed and `Contact01Issue` is added.\n - **`Provisioning01`**: a step's `fields: readonly ProvisioningField[]` becomes\n `fields: readonly FieldSpec[]`. `ProvisioningField`, `ProvisioningFieldType` and\n `ProvisioningOption` are removed; `ProvisioningStep`, `ProvisioningValue` and\n `Provisioning01Status` are unchanged.\n - **`Signup01`**: `fields: readonly SignupField[]` becomes `groups: readonly\n FieldSpecGroup[]`. `SignupField` and `SignupOption` are removed; the `password`\n arrangement, `SignupValue`, `Signup01Password`, `Signup01Reading`,\n `Signup01Status` and `Signup01SignIn` are unchanged.\n \n `FieldCommon` gains an optional `autoComplete`, the platform's autofill hint, once\n for every screen rather than in a second declaration per Block. Requiredness stays\n a rendering fact and an HTML attribute; no specification carries a rule.\n \n # Migration\n \n Replace a field list with groups of `FieldSpec` from `@nanisoft/prism-ui/spec`:\n \n - Rename a field's `id` to `key`, its `type` to a `kind` naming a Prism control\n (`'text'`, `'email'` and `'tel'` become `'Input'`, `'password'` becomes\n `'PasswordField'`, `'textarea'` becomes `'Textarea'`, `'select'` becomes\n `'NativeSelect'`), and its `description` to `help`.\n - Move a choice's `options` under its kind, unchanged in shape.\n - Wrap the list in one or more `{ fields: [...] }` groups and pass it as `groups`.\n - Pass a control Prism does not ship as `{ kind: 'slot', control: <your control /> }`.\n - On `AuthForm01`, replace `error` with `submitError` and, where you marked a\n field, pass `issues={[{ field: key, message }]}` instead of `errorId`.\n - Read `onSubmit` values by each field's `key`. `AuthForm01` is uncontrolled: read\n `new FormData(event.currentTarget)` rather than a controlled `value`/`onChange`.\n - Pass `autoComplete` on the fields a password manager or a phone keyboard fills.\n- dbc8a65: The activity trail takes the shared event specification\n \n `ActivityFeed01.events` is now an `EventSpec[]` from `@nanisoft/prism-ui/spec`\n rather than the Block's own `ActivityFeed01Event`, and that local type is gone from\n the published interface. The trail is the activity feed Block widened, so no new\n Item appears beside it: a shipment's chain of events, an issue's whole history and a\n transaction's trail are record details, and the trail's own entry declaration is the\n one declaration of a dated attributed occurrence every Block in this package takes.\n \n An entry carries a stable `key` that is never the words of a label, a moment printed\n exactly as the caller passed it with the machine value on the element, a required\n `actor` and `action`, an optional `target`, an optional `detail` node, a `tone` whose\n `toneLabel` is required wherever the tone is set, and a destination whose words are\n required wherever it is set.\n \n The specification names no vocabulary of what happened: there is no `kind`, because\n a closed union is legitimate only when its members are Components this package\n ships, and a caller needing one puts the word in `action` or composes a status into\n `detail`. Immutability is a rendering fact and nothing more, so there is no\n `immutable` prop, no seal glyph and no verification mark. A trail draws a moment and\n never a length, so there is no scale, no range and no axis, and the Block owns no\n retention window, archive, purge, legal hold, export or as-of clock.\n \n # Migration\n \n - Replace `ActivityFeed01Event` with `EventSpec`, imported from\n `@nanisoft/prism-ui/spec`, on the `events` prop.\n - Rename each entry's `id` to `key`.\n - Move the tone's label to `toneLabel` wherever `tone` is set.\n - Remove any `kind`, `immutable`, seal, verification, scale, range, axis or\n retention member: the specification does not carry them and does not accept them.\n Put a happened- word in `action`, and compose a status into `detail`.\n- dbc8a65: The settings panel takes the shared field specification\n \n `SettingsPanel01` now takes the same `FieldSpec`, `FieldSpecGroup` and `FieldKind`\n a record write form takes, and its own field declaration, `SettingsPanelField`, is\n gone from the published interface. The archetype is answered by the record write\n form Block and earns no Item of its own: the difference between a form and a setting\n is about what sits behind the surface, not about the surface.\n \n Two things change in the props, and the second is breaking.\n \n - `sections` is now `groups`, and a field is a `FieldSpec` with a stable `key`, a\n required `label` and a required `kind`. The `slot` arm means a control this\n package does not ship has somewhere to go, and a field's shell stays Prism's\n either way.\n - `secondaryAction` is gone. That position is now `footerStart`, a node the caller\n wrote, placed without styling. A caller who passed a label and no function could\n previously produce a focusable control that activated to nothing; a `CtaLink` with\n its required `href` cannot.\n \n `submitLabel` is now one arm of a save union: `action`, `onSubmit` or `submit`, each\n forbidding the others, exactly as the write form takes them. A region that applies\n on change passes `values` and an `onValueChange` that applies the mutation, and no\n save arm at all, and the Block draws no footer. Whether a setting leaves on change,\n on blur or at a save is the caller's own handler and the caller's own submission\n arm, and no prop here decides it.\n \n The Block renders no revert control, no reset to defaults control and no storage of\n any kind, and it draws no tab strip and no rail.\n \n # Migration\n \n - Rename the `sections` prop to `groups`, and pass each field as a `FieldSpec` with\n a stable `key`, a required `label` and a required `kind`, imported from\n `@nanisoft/prism-ui/spec`. A control this package does not ship goes in the\n `slot` arm.\n - Replace `secondaryAction` with `footerStart`, a node you write, placed without\n styling. A label with no function no longer compiles into a control that does\n nothing.\n - Replace `submitLabel` with one arm of the save union: `action` for a destination,\n `onSubmit` for a handler, or `submit` for a node you write. The three forbid one\n another. A region that applies on change passes `values` and `onValueChange` and\n no save arm, and draws no footer.\n - Remove any use of `SettingsPanelField`; it is gone from the published interface.\n\n### Minor Changes\n\n- f11799a: Add `emptied-by-reader` to `EMPTY_REASONS`, for a region the reader emptied\n \n `EmptyState01` takes a fourth reason beside the three it takes today: a region\n whose records were there and that this reader's own earlier action moved every one\n of them out of. A bin, an archive or a recycle view a reader has just cleared is\n the case it exists for.\n \n **A member on a published union, not a new Block and not a catalogue row.** The\n existing `EmptyReason` type gains its fourth member and the changeset says so\n because a consumer's own code that enumerates the union now has one more case to\n handle. `EMPTY_REASONS` is published through\n `@nanisoft/prism-ui/blocks/empty-state-01` beside the Block, and `catalog.ts`\n holds the same number of rows it held before, because a union member is a value on\n an existing exported type rather than an Item: it has no slug, no page, no Demo\n and no corpus entry of its own.\n \n **It is a fourth reason rather than a fourth Block because it is a statement about\n a collection, which is what the other three are.** Each of them answers what\n happened to the set the region would have drawn, so the frame they share stays\n true of all four: there is nothing here to read, and the frame makes no claim\n about why. A bin the reader emptied has nothing readable beside it, so the shared\n frame is honest there. That is the whole difference from `NothingChosen01`, which\n is a statement about a reader's pointer at a record and whose records are all\n still there: it draws no frame, it is a separate Block, and the two arguments\n argue the same line from opposite sides.\n \n **Two of the four are the reader's own doing and they are not the same doing.**\n `no-match` hides rows out of a set that is still there, so clearing it brings them\n back. `emptied-by-reader` is a record gone from the live set rather than hidden in\n it, and the reader put it there. Every saved view a caller builds out of the filter\n values it holds is already `no-match` whenever it comes back empty; a trash view\n is the one that differs in kind, because it fetches a different collection rather\n than narrowing this one. So the two members differ in exactly the fact a reader\n must be told honestly, and one sentence for both would tell a reader who has\n deleted everything that their own search found nothing. Both are named in the\n JSDoc on the union, which the emitted declarations preserve and the corpus reads.\n \n **It renders no control, and the Block already had that arm.** The reason takes\n the caller's `title` and an optional `body` and nothing else, because a reader who\n has deleted nothing has nothing to restore and the records a restore would bring\n back are the caller's own nodes in the index around this region. `EmptyState01`\n has never rendered an action without an `actionLabel`, so the member adds no\n render branch, no prop and no gate exemption: `scripts/check-block-controls.mjs`\n is green on the source exactly as it was, and the one control in it is still the\n `Button` behind that label.\n \n It carries no ink either, for the reason the Block gives: nothing is broken,\n nothing is missing, and nobody is blocked. `REASON_INK` is unchanged, so the only\n reason drawn in `muted-foreground` is still the permission boundary.\n- e9e73d6: Four more figure owners take the shared metric specification\n \n `ChartCard01`, `ProjectDashboard01`, `Stats01` and `Trend01` now take the same\n `MetricSpec` the metric summary takes, so a figure a consumer declares on one of\n these surfaces is the same figure it declares on the others. This is the migrate\n step of a breaking consolidation: five shapes are declared across this package\n for a reading over a population, and they disagree about whether a delta is a\n number or a percentage, about `deltaFormat`, about `hint`, and about whether a\n series and a destination are part of a figure at all. This release moves the four\n remaining owners onto `MetricSpec`, so one shape is left in use. Their superseded\n local declarations stay exported as deprecated aliases until the contract ticket\n deletes them, so an existing name continues to resolve.\n \n **The shape every one of the four now takes.** A stable `key` that is never the\n words of the label, a required `label` (a `ReactNode`) and `value` (the caller's\n own node), a `delta` whose sign is the direction, an optional `deltaFormat` (the\n caller's own words for the change, a `ReactNode` rather than a callback), an\n optional `hint` for the period or the caveat, and an optional `series` with its\n `seriesLabel` and an optional `href` with its `hrefLabel`. The two pairs are held\n by the type: a series set without its name, or a destination set without its\n words, does not compile.\n \n **What changes per Block, and the arm that replaces each prop that goes.**\n \n - **`ChartCard01`**: `ChartCard01Reading` becomes an alias of `MetricSpec`. A\n reading's `deltaFormat` is now the caller's own words rather than a `(value:\n number) => string` callback, so pass a node, and `key` is now required. The\n reading line draws the series and the destination too.\n - **`ProjectDashboard01`**: `ProjectDashboard01Figure` becomes an alias of\n `MetricSpec`, `key` is now required, and a figure can now carry `deltaFormat`,\n `hint`, `series` and `href`. The bare `0.12` a delta without a formatter used to\n print is now a decision rather than a drift.\n - **`Stats01`**: `Stat` becomes an alias of `MetricSpec`, `key` is now required,\n and a delta no longer has a percent sign appended to it. A delta with no\n formatter prints the number the caller passed, and a delta with a formatter\n prints the caller's own words.\n - **`Trend01`**: `Trend01Item` becomes an alias of `MetricSpec`, a row's `id`\n becomes `key`, and `deltaLabel` becomes the `deltaFormat` node. A row can now\n carry a series and a destination, and a delta with no formatter prints the\n number rather than making the Block throw.\n \n **What is kept, and what is still not owned.** Each Block keeps its own name,\n kind and arrangement. `ChartGroup01`, the page fixtures and the site landing page\n were updated at their call sites, because this package resolves the shared type\n through the component package. No Block performs aggregation, derives a rate of\n change, owns a period, a unit, a threshold, a target, a severity or a freshness\n state, and none draws a period switcher, a comparison toggle or an export. A\n metric's period stays the caller's own words, carried by `deltaFormat` and `hint`.\n \n `@nanisoft/prism-ui/spec` is the type to import: `MetricSpec`, from\n `@nanisoft/prism-ui/spec`, replaces `ChartCard01Reading`, `ProjectDashboard01Figure`,\n `Stat` and `Trend01Item` at every call site.\n- f11799a: Add `@nanisoft/prism-ui/spec`: one shared specification module, and the gate for it\n \n This adds published surface and changes no existing one. Nothing you compose\n differs today; what changes is that five screens worth of declared content now\n have one declaration each instead of several that disagree, and that the\n declarations are held by a gate rather than by review.\n \n **The five specifications.** `FieldSpec` and `FieldSpecGroup` describe the fields\n a write form, a wizard or a settings region renders, in the order the array is\n written. `ColumnSpec` describes a column of a record index. `RelationSpec`\n describes one relationship of a record, one ring deep. `MetricSpec` describes a\n figure taken over a population. `EventSpec` describes one dated, attributed\n occurrence. Every one of them is plain serialisable data: no validator, no\n constraint object, no schema, no pattern, no callback. That is what lets an agent\n read one out of a document, and it is why validation is yours entirely.\n \n **Three closed unions, and the one arm that is an escape rather than a catch-all.**\n `FieldKind` names the input Components this package ships, `ColumnKind` the\n reading Components, `RelationKind` the collection arrangements already drawn. Each\n carries a `slot` member for the control, cell or arrangement you draw yourself, and\n Prism still draws the shell around it: the label, the help, the error, the header,\n the alignment, the frame. A `props` bag would have been the alternative and would\n have put the hole back.\n \n **What is deliberately absent, so that you are not waiting for it.** A\n specification carries no rule and no validator, because a Block ships no behaviour\n and evaluating a rule is behaviour; `required` stays, and it is the mark and the\n HTML attribute rather than a check. A metric carries no period, because a\n comparison needs a period and a billing screen's month is not a trading screen's\n week: the words for it go in `deltaFormat` and `hint`, in your own units. An event\n carries no `kind`, no immutability, no retention window and no time axis, because\n no enumeration of what your product calls a thing that happened is ours to publish\n and because a drawing has no authority over the store behind it. A relation holds\n members and never another relation; a descendant is reached by a link you pass.\n \n **The gate.** `packages/ui/scripts/check-spec-unions.mjs` reads the emitted\n declarations rather than the source, because that is the seam your editor\n resolves. It fails when a union member names something this package does not\n ship, when two of the three unions claim the same member, when a specification\n grows a validator, a rule, a schema, a pattern, an immutability claim, a retention\n window or a time axis, when an event grows a `kind`, and when a relation holds a\n relation. Before it existed, every one of those was accepted by every gate in the\n repository, because a specification type is not an Item and no catalogue gate reads\n the module at all.\n \n **One thing it does not hold, so you know what a green run means.** It does not\n hold that there is one shape per concept: a second declaration of the field, column,\n relation, metric or event shape would be named differently from the first, and the\n rule that would catch it would be a list somebody maintains. That is held by review.\n \n **The surface gate's internal boundary moved with it, in one direction only.**\n `packages/ui/src/lib/` is where the internal helpers live, but the `exports` map is\n what decides: a declaration under it that a published entry reaches is public\n surface and the surface gate reads it, and one that no entry reaches is still\n internal and still has to be declared. `dist/lib/spec.d.ts` is the first such file.\n Nothing under `dist/lib` was declared internal and then undeclared, and no published\n subpath was removed.\n- caee732: `Directory01` gains a per-member node\n \n `Directory01` takes a new `renderMemberAction` prop: a function from a member to a\n `ReactNode` the Block places at the member's trailing edge, in both the card and the\n row arrangements, and draws nothing of its own where it is not passed.\n \n **The control and its handler are the caller's, and that is why it is a node.** A\n directory is a browse, so a plugin or model row's enable is a property of a member\n rather than a command on a selection, and `scripts/check-block-controls.mjs`\n classifies `Switch` and fails a Block that renders one without its handler. A Block\n ships no behaviour and so cannot supply one, so the enable arrives as the caller's\n own node, in the shape `DataTable01`'s `renderRowActions` already takes.\n \n No new Item and no new catalogue entry: the change is a prop on an Item that ships,\n with its JSDoc, its Demo and its test.\n- caee732: `DocumentPage`, the document and research workspace\n \n A new Page arrives at `@nanisoft/prism-ui/pages/document-page`. It draws one\n document read in full, its body held to the reading measure, and one or more\n regions beside it.\n \n **It is authored beside `DocsShell`, and neither of that Page's two rules is\n relaxed.** A section stays a label and not a control, and a group with no index\n stays a label and not a route, because both are statements about a documentation\n site's navigation tree and this workspace never asks that question. A status\n belongs to a region of one document, and a route is the document's own contents.\n No consumer of `DocsShell` is affected.\n \n **One Page draws all seven screens.** The document is `RecordDetail01` and the\n body is `Prose`; a version list is an `activity` region, a source or output list\n is a `table` region, a comment thread is a `members` or a `table` region, and the\n evidence comparison matrix is a `comparison` region drawn by `Compare01`. The\n region union is by arrangement and not by subject matter, so the Page mints no\n document, version, comment or source type and ships no sentence. The editor, the\n fetch, the retention, the share, the export and the permission are the consumer's.\n \n The Page ships with a catalogue entry, a JSDoc block, an Item page and a Demo, a\n corpus and store entry, a composition test and an accessibility test over its\n heading structure and named regions.\n- f11799a: Add `NothingChosen01`, the resting state of a detail pane\n \n `NothingChosen01` draws a region that can hold a record and has none chosen in it\n yet: the caller's words, an optional sentence under them, and an optional mark of\n the caller's choosing. It is a new Block beside `EmptyState01` rather than a fourth\n reason on it, and the difference is the shape of the claim rather than a matter of\n taste.\n \n The three reasons `EmptyState01` takes are all statements about a collection:\n nothing ever existed in it, the reader's own filter emptied it, or this reader may\n not see it. The dashed frame and the height floor those three share assert\n something that follows from all three together: there is nothing here to read.\n Nothing chosen is not that. No collection has been emptied, filtered or hidden, the\n records are in the index a few centimetres away, and what is missing is the\n reader's pointer at one of them. A fourth arm would have had to keep that frame\n honest while a readable region sat next to it, which is the failure the separate\n Block exists rather than to commit.\n \n So the Block ships no reason value, because one cause is not a choice a caller\n makes; no default sentence and no fallback, because the words are entirely yours;\n no frame, for the reason above; and no action. That last one is not an omission. A\n Block ships no behaviour, so a control here would be either one this Block cannot\n wire or a second copy of an affordance the index beside it already draws for the\n same reader, and `scripts/check-block-controls.mjs` has nothing to catch in a\n source that contains no control.\n \n It also declares no height floor. A pane in a grid is already as tall as its\n neighbour, so a `min-h` authored here would be a second answer to a question the\n split's tracks have already answered. What the Block does is fill the height it is\n given and centre itself in it.\n \n Two things it inherits rather than re-derives: the icon is a slot drawn\n `aria-hidden`, because a mark a screen reader reads is a second announcement of\n something the words already say, and the title is a `<p>` rather than a heading,\n because the pane outlives its contents and an outline entry that vanishes on the\n first click is an entry a reader navigating by heading cannot follow.\n \n The split's tracks hold their ratio at every selection state, so the list never\n moves out from under the hand that clicked it. What changes is content, and a\n caller who wants the index louder makes the index denser, which is the index's own\n decision to take.\n- dbc8a65: The issue detail Block is deprecated in favour of the record detail Block\n \n `IssueDetail01` is deprecated, not merged and not deleted. It is the same job as\n `RecordDetail01` under a name that is wrong the moment the record is an invoice, an\n order or a subscription, and a name that has to be wrong about nine tenths of its\n uses is a name that fails the test that its name survives being wrong. The Item\n still renders and is still supported for now, but nothing new should be started on\n it, and a consumer composing a record detail today composes `RecordDetail01`.\n \n **The published interface is unchanged.** This release marks the Item and changes\n its JSDoc and its documentation; no export is removed, renamed or re-shaped, so a\n consumer already composing `IssueDetail01` keeps composing it and can read the\n release without moving. `IssueDetail01` and its four types stay exported from\n `@nanisoft/prism-ui/blocks/issue-detail-01`. Its own field declaration stays with\n it and is not migrated onto the shared field specification, because it is a label\n and a value node a reader is entitled to be told about a record rather than an\n input.\n \n The catalogue entry carries the `deprecated` status, so the Item page shows a\n deprecated mark, and the Item page and the record detail Block's documentation each\n name the other, so a reader arriving at either arrives at the answer.\n- dbc8a65: Add `IndexDetail01`, the master and detail split\n \n `IndexDetail01` is a composition Block that renders an index pane beside a detail\n pane and holds no selection state at all. Selection, routing and which record is\n open belong to the consumer, because Prism never imports a router. Nothing in the\n Block connects the two panes: the index reports every selection change through its\n own callback, and reacting to it is the caller's own read of that callback and a\n pass of the record it wants into `detail`.\n \n The tracks hold their ratio at every selection state, so clicking a row does not\n move the list out from under the hand that clicked it. The pane before anything is\n selected holds what the caller placed, or nothing at all, so the Block ships no\n fallback sentence, no frame of its own and no loading mark. The detail slot is a\n required node, because a record that is gone, one this reader may not see and one\n whose fetch failed are three causes sharing one address and three different\n sentences.\n \n The split does not collapse at a narrow viewport. Which pane survives is the\n caller's composition rather than a media query deciding a shape, so the narrow\n arrangement is one the caller writes. The Block renders no control of its own.\n \n This change also corrects the record index section of the design document, which\n claimed the split was already authored while the roster counted from source held no\n such Item.\n- caee732: `MessageThread01`, the live Kind's second family\n \n A new `live` Item, the message-thread surface, arrives at\n `@nanisoft/prism-ui/live/message-thread-01`. It renders a conversation whose\n messages arrive over a `subscribe` function the consumer supplies and it opens no\n connection of its own, so a WebSocket, an `EventSource`, a polling timer and a test\n array are all the same shape to it.\n \n **A message list with ordered parts, and not an event list or a turn list.** A\n message has a stable identity, a sender and an ordered list of parts, and a part is\n text, a tool call, a reasoning block, a citation or an attachment. The message and\n part vocabulary lives with the surface, beside `RunEvent` and `ToolCall`, and is\n deliberately outside the shared specification module, so no sixth specification\n member and no `CONTEXT.md` word is added.\n \n **It reuses what the Kind already names rather than minting a second answer.** A\n tool-call part is drawn through `ToolLedger01`'s own row, so there is no second\n tool-call shape. The status tiers are `RunStatus`, published once and shared by both\n live surfaces. The run controls are `PromptComposer`'s send, stop, retry and attach,\n composed beside the thread rather than a second set drawn inside it. The one surface\n it does not reuse is `RunStream01`, because a run's log is a sequence of occurrences\n and a conversation is a sequence of messages.\n \n **Prism holds a bounded in-memory window and promises no resumption.** The durable\n resumption buffer that outlives a mount is the consumer's, through the same `initial`\n and `resubscribeKey` seam `RunStream01` already documents: a surface opened\n mid-thread is seeded from the consumer's own record, and a reconnect is a new key\n over a fresh seed. The surface sorts by `at` with a tie on `id`, bounds its window,\n announces a growing list once rather than reading it whole on every arrival, and\n holds no control it cannot act on.\n \n The Item ships with a catalogue entry, a JSDoc block, an Item page and a Demo, a\n corpus and store entry, and a boundary test in the shape of the `RunStream01` one.\n- 1079d02: The metric summary Block takes the shared metric specification\n \n `Dashboard01.metrics` is now a `MetricSpec[]` from `@nanisoft/prism-ui/spec`\n rather than the Block's own figure shape. This is the first step of a breaking\n consolidation: five metric shapes are declared across this package today and they\n already disagree about whether a delta is a number or a percentage, about\n `deltaFormat`, about `hint` and about whether a series and its name are part of a\n figure at all. `MetricSpec` is the one shape, and this release makes the metric\n summary Block the first surface to take it. The other four owners are migrated in\n later batches, so this is one Block wider rather than five replaced at once.\n \n **What a figure is now.** Every entry is a `MetricSpec`: a stable `key` that is\n never the words of the label, a required `label` and `value`, a `delta` whose sign\n is the direction, an optional `deltaFormat` carrying the caller's own words for\n that change, an optional `hint` for the period or the caveat, and an optional\n `series` with its `seriesLabel` and an optional `href` with its `hrefLabel`. The\n two pairs are held by the type: a series set without its name, or a destination set\n without its words, does not compile.\n \n **What changes for a consumer.** `label` is now a `ReactNode`, so a caller that\n passed a string passes the same string and a caller that passed a node keeps it.\n `deltaFormat` is the caller's own words rather than a `(value: number) => string`\n callback, so `deltaFormat: (v) => String(Math.abs(v))` becomes\n `deltaFormat: '3 fewer than yesterday'` and the direction mark still comes from the\n sign of `delta`. `sparkline` and `sparklineLabel` are now `series` and\n `seriesLabel`, and `key` is now required. `Metric` takes the words as a node when\n the Block hands them over, so a delta with a formatter prints the caller's words\n and a delta without one prints the number the caller passed.\n \n **`Dashboard01Metric` is kept as an alias** of `MetricSpec` so an existing name\n resolves while the other four figure owners are migrated. It is a synonym on its\n way out, not a second shape, and a later contract release removes it alongside the\n superseded `ProjectDashboard01Figure`, `ChartCard01Reading`, `Trend01Item` and\n `Stat`.\n \n **Nothing else is owned.** The Block still performs no aggregation, owns no period,\n no unit, no threshold, no target, no severity and no freshness, and draws no period\n switcher, no comparison toggle and no export.\n- dbc8a65: Add `RecordWizard01`, the multi-step record write form\n \n `RecordWizard01` is the write form's second arrangement rather than a prop on the\n plain form. A wizard draws one group of fields at a time above a rail and asks a\n decision the plain form never asks, which is whether a step may be left forwards, so\n it earns its own name: it is a property of how many fields a record has, not one of\n two renderings a consumer picks between on purpose.\n \n The step is the consumer's. `current` is controlled and `onStepChange` is required,\n and the Block draws the step it was handed and no reachability at all. The\n specification is per step, and a step is a `FieldSpecGroup` from\n `@nanisoft/prism-ui/spec` with a stable `id` and a string `label`, so nothing new is\n declared in order to hold the steps and a group's heading and its rail label are one\n word. One specification sliced by a caller grouping is refused.\n \n Every field on the stepped arm is controlled and carries the consumer's value and\n change handler, so the values are the consumer's from the first keystroke. Back, the\n forward control and the rail are `FormWizard`'s, and this Block renders none of\n them, declares no submission arm of its own, and leaves an issue naming a key that\n is not on the step being drawn undrawn without moving the reader.\n- 1079d02: The record detail draws one record and its relationships\n \n `RecordDetail01` is a new Block: one record in full, its relationships drawn open\n as one ordered list, and its own actions beside its identity. It is a Block rather\n than a Page because the frame and the claim belong to a caller and a Page owns no\n URL, so a detail Page would have to own the selection the consumer holds. It is\n authored beside `Summary01` and `QuickView01` rather than by widening either,\n because a summary exists to add up and a quick view is a transient dialog.\n \n **Relationships are one declared list on one frame, not a card per relation.**\n Each entry is a `RelationSpec` from `@nanisoft/prism-ui/spec` plus the words its\n own empty frame needs, drawn in the declared order under one heading level inside\n one bounded frame with one rule between them. The `kind` names a collection\n arrangement this package already draws: `Table`, `ListPanel`, `Timeline`,\n `AvatarGroup` or the caller's own `slot`. **Depth is one, and the bound is in the\n type**: a relation's members never name `RelationSpec`, so a related record with\n its own related records is a graph the consumer walks, reached by the caller's\n `href` rather than by depth. The Block composes no disclosure anywhere.\n \n **The record's actions are one optional node, placed in the record's own band.**\n The Block renders no control of its own, there is no declared action union and no\n destination arm, and the confirmation for a destructive action is the caller's own\n `AlertDialog` composed around their control in the same slot. **A relation with no\n members draws `EmptyState01` at a reason the caller names**, in the relation's own\n frame, because a customer with no invoices is a collection that is empty and\n `NothingChosen01` is a statement about a reader's pointer at the index beside the\n pane, which this Block cannot be in. **The Block takes no total, no count and no\n sum**: a relation's `count` is the caller's own string and is never derived from\n the members, and a screen that wants a figure composes `Summary01` or `Stats01`\n into the Block's `children`.\n- 1079d02: The record index groups by a key on a row and announces the two selection scopes differently\n \n `DataTable01` widens again rather than adding an Item, and the roster does not\n change: a grouped index is the same job with a `groupBy` on a row, not a second\n arrangement, so no `-02` is earned.\n \n **Grouping is a key on a row.** `groupBy` takes a row and returns the key it\n belongs to, or `undefined` for an ungrouped row, and `groupLabel` names the\n heading the Block draws above each run of rows that shares one. The two are a\n single arm: `groupLabel` is required wherever `groupBy` is set, because a heading\n with no words is a blank row. The Block never reorders the rows it was handed, so\n same-key rows must arrive adjacent and a run of them is one group. A group heading\n is a row in the same body as any other, and it is not a record: it is never in\n the selection set, never in the count and never in what the header's page scope\n covers, so forty headings over four hundred rows select four hundred records and\n say so.\n \n **The two selection scopes are announced differently, and the filter scope's\n number is the caller's.** `selectionScope` is new and is a union. Its page arm is\n the default and the Block speaks `labels.selectedCount` over the keys it holds.\n Its filter arm reports that the whole matching population is selected and carries\n a required `count` from the caller, and the Block speaks\n `labels.selectedAllMatching(count)` rather than printing the page as if it were\n the total. The Block owns no control that selects a population it cannot see: the\n caller selects in its own store and reports the scope here.\n \n **What is new on the surface:**\n \n - `groupBy` and `groupLabel`, together, for a grouped body.\n - `selectionScope`, a `DataTable01SelectionScope` union, for the page or the\n filter scope.\n - `selectedAllMatching`, a required `labels` member: the summary and announced\n sentence for the filter scope. `selectedCount` is now the page-scope sentence,\n and it should say the page.\n \n The per-row prop that went and the node that replaced it are named in the record\n index selection entry in this release: `rowActions`, a declared list of action\n objects with an optional handler, is gone, and `renderRowActions`, a node per row,\n replace it, so a row that looks like a command and carries none cannot be built.\n- 1079d02: The record index draws typed columns and holds the selection set it was given\n \n `DataTable01` widens rather than adding an Item, and the roster does not change:\n every one of the in-scope screens is one job, a reader scanning rows and acting on\n them, differing only in its columns, its keys, its filters and its sort key. No\n `-02` is earned by it.\n \n **The columns are typed data now, and the callback is gone.** `columns` is a\n `readonly ColumnSpec[]` from `@nanisoft/prism-ui/spec` rather than an array of\n `{ id, header, cell }`. `key`, `header` and `kind` are required on each column,\n because a column a reader reaches without a name is a column every row announces\n as untitled, and `kind` is drawn from Prism's own cell vocabulary rather than from\n an HTML attribute. The row value at the column's key is the cell's content: a\n single-node kind draws it as that Component's child, a structured kind takes it as\n that Component's props, and the `slot` kind draws it as your own node inside the\n header, alignment and sort affordance the Block draws. `DataTableColumn` and\n `DataTableRowAction` are removed with it.\n \n **Selection is required whenever the index is selectable.** `getRowId` is required\n on the Block, because sorting, filtering, paging and deleting all move a row's\n index and an index-keyed selection silently changes which records a batch action\n acts on. `onSelectedIdsChange` is required on the selectable arm and `batchActions`\n with it; the Block holds the set on its uncontrolled arm and reports every change\n through the one callback, so mirroring the set is one line and ignoring it is zero.\n \n **What is new on the surface:**\n \n - `columns`, now `ColumnSpec[]`. `sortable` draws `TableSort` and reports the next\n direction through the new `sort` and `onSortChange`.\n - `getRowId`, now required.\n - `onSelectedIdsChange`, required on the selectable arm.\n - `batchActions`, required on the selectable arm: one `ReactNode` the Block places\n in a bar that mounts only while the set is non-empty and displaces the filter\n controls. The Block draws the count and the dismiss and no command of its own.\n - `renderRowActions`, replacing `rowActions`. A node per row rather than a declared\n action list whose handler was optional, so a row that looks like a command and\n carries none cannot be built.\n - `sort` and `onSortChange`, for the ordered column.\n - `selectRow` is now `(row) => string` so every checkbox names its record,\n `selectedCount` is the count the live region speaks, and `clearedSelection` and\n `dismissSelection` join it. `sort` is the words for the next direction.\n \n **One polite live region carries the count**, never an assertive one, mounted while\n there is an announcement; it speaks the count rather than a list of labels, and it\n speaks the emptying as its own sentence rather than a count of zero. The visible\n summary and the announced number are the same string.\n \n **Nothing else is owned.** The Block fetches nothing, sorts nothing, filters\n nothing and slices nothing, owns no action, no aggregate, no address and no second\n pane. The detail pane reacts through your read of `onSelectedIdsChange`; no bridge\n is built between the two.\n- 1079d02: The record write form renders a field specification whole\n \n `RecordForm01` is a new Block: a create or edit screen rendered from a typed,\n ordered field specification. It takes `FieldSpecGroup[]` from\n `@nanisoft/prism-ui/spec` and draws Prism's own controls, each inside one shell of\n label, help and error. Every field carries a stable `key`, a required `label`, a\n required `kind` and optional help, hint, placeholder, disabled flag and starting\n value; a control the package does not ship is the `slot` arm, and the Block still\n draws the shell around the caller's node.\n \n **The error state is an issue list keyed by field**, the shape `FormDialog` and\n `FormWizard` already publish, and a message about the submission rather than about\n a control is `submitError`, which names no field. **The save union has three arms**\n and the compiler holds it: `action` posts to a URL, `onSubmit` hands over the\n form's element, and `submit` is a slot for the caller's own control. The save\n control is `type=\"submit\"` inside the `<form>` the Block renders. **Layout is\n derived**: `columns` is `1 | 2` at the form level, one column again at the narrow\n breakpoint, and there is no per-field layout prop and no slot per region.\n \n **One piece of per-kind rendering data is added.** The `MoneyField` arm of\n `FieldSpec` gains a required `currency` and an optional `locale`, because an\n amount is meaningless without the currency it is in and no locale can supply it.\n Nothing else in the shared module changes, and the module stays plain serialisable\n data with no rule, no validator and no callback on it.\n\n### Patch Changes\n\n- f11799a: The gate that holds a Block's controls now reads the menu row and the switch\n \n **No published surface changed.** Nothing in this entry is a prop, a Component or a\n behaviour you compose differently: the change is to `scripts/check-block-controls.mjs`,\n which is a repository gate and is not in the published tarball. It is here because the\n law it holds reaches a consumer as the shape their own Block has to take, and a\n consumer reading the changelog should be able to find that law without reading this\n repository.\n \n That gate classified `Button` and `CtaLink`, and neither of them is a menu row. The\n shipped record index takes a declared list of per-row actions whose handler is\n optional and renders each entry as a `DropdownMenuItem`, so a caller who passes a\n label and no handler gets a row that sits in the menu's keyboard order, is announced\n as a menu item, and activates to nothing. The gate could not see that Component, so\n every gate in the repository was green while it shipped, and a clean run was the\n absence of a check rather than the presence of a correct one.\n \n Four Components are classified now, each with the arms that make it act:\n \n - `Button`: `onClick`, `type=\"submit\"`, `type=\"reset\"`. Unchanged.\n - `CtaLink`: `href`, which is required, so that arm is the compiler's. Unchanged.\n - `DropdownMenuItem`: `onClick`, or `render` together with `href` in the element it\n renders as. Both words rather than either one, because `render={<span />}`\n navigates to nothing and a bare `href` would accept a `data-href` as a destination.\n - `Switch`: `onCheckedChange`, and nothing else. A switch does not navigate and is\n meaningless without the handler, because the state it changes is the consumer's\n setting rather than anything a Block holds.\n \n Every `DropdownMenuItem` and every `Switch` the tree renders already carries one of\n those arms, so nothing shipped failed and no Block moved. What the widening buys is\n the next one: a menu row or a switch a Block renders with nothing behind it is a\n finding naming the file and the line, where yesterday it was invisible.\n \n **Two limits are printed on every run and neither is new.** A `{...rest}` spread\n inside a rendered control reads as carrying no handler, and a handler forwarded from a\n declared optional prop reads as present, because resolving whether the caller passed\n it is reading a type rather than a file. The second is why the shipped row action list\n is still a finding for a reader and not for this gate: the optional handler behind the\n attribute is invisible to a source scan. Removing that declared list is the authoring\n work, and this gate is what makes the next one of them fail the run.\n \n **A Block that holds a disclosure itself is still not caught, and that gap was left\n open on purpose.** `Collapsible` and `Accordion` put none of the four classified\n Components in a Block's source, so a Block rendering one with a `defaultOpen` and no\n change handler reads as clean while the state is still state a Block holds. A\n disclosure's activation lives inside the disclosure rather than on an attribute of the\n row that opens it, so there is no arm for this gate to read, and a rule that inferred\n one would report the Blocks that compose `Collapsible` correctly. The prohibition\n stays a design rule in `DESIGN.md`, an auditor checks it by looking for the absence,\n and the run prints that it is not a finding so a clean run is not read as more than it\n is.\n- dbc8a65: The log and event stream is the record index drawing event rows\n \n A delivery log or a console events screen is the record index, `DataTable01`,\n drawing rows from the shared event specification, `EventSpec`. No Item, Kind or\n Page is added: a log gains a filter region, a second axis on which entries are\n compared and a page boundary, and every one of those already belongs to the record\n index rather than to a rail. The row type is the shared event specification, and no\n sixth event shape is minted for a log or a console.\n \n A screen that wants the events of one record composes `ActivityFeed01`; a screen\n that wants to find one entry among many composes `DataTable01`. The Block composes\n no disclosure: seeing one entry in full is a destination the caller names with its\n required words, expanding a payload in place is the caller's own `Collapsible`\n placed in a `slot` cell, and taking an identifier away is the caller's own control\n beside a code block. No prop changed; this release adds the Item documentation, the\n Demo and the JSDoc that record the arrangement.\n\n## 0.16.1\n\n### Patch Changes\n\n- bf34d82: `DocsShell` drew its navigation twice in 0.16.0 and draws it once again\n \n **0.16.0 introduced a regression and this release removes it.** To give a reader below `lg` some navigation, 0.16.0 drew each tree a second time behind a `<details>`: one copy in the rail for a wide screen and one behind a disclosure for a narrow one. Only one of the two was displayed at any width, so the accessibility tree was never wrong and a reader never met two copies at once. Every documentation page in the family still shipped two of everything, and the payload of that is paid on the largest surface the system has.\n \n Three consumer test files failed against 0.16.0 as published, and two distinct defects produced every one of those failures. Two files failed on a count that came back doubled, and two failed on a `getByRole('navigation', { name })` that had two answers where it expects one: the disclosure drew its own `<nav>` under the rail's own accessible name, and that name is how a consumer's suite reaches the rail at all. A suite that holds that a documentation rail contains no disclosure control was never the finding, because the `<details>` was a sibling of the rail in the same frame and never inside it.\n \n **What a consumer measured on 0.16.0, in three built exports.** The Nexus page `/docs/architecture/kanban-service` carried 3 `aria-current=\"page\"` markers where 2 is right, because the third belongs to that site's own bar and the other two are this Page drawing the same page twice. It carried 8 `<nav>` elements where 6 is right, and 52 `data-slot=\"docs-nav-page\"` rows on a corpus of 26. The AlphaLens index at `/docs` carried 12 `data-slot=\"docs-nav-label\"` spans on a six section corpus. The Prism preview route that renders this Page, `apps/site/out/preview/docs-shell.html`, carried 22 `docs-nav-page` rows on an 11 row outline and 5 `<nav>` elements where 3 is right.\n \n **What changes.** Each tree is drawn once, at every width. A documentation page carries one `aria-current` marker, one navigation list per tree and one row per document. Below `lg` the frame is a single column and the rail is its first child, so a reader at a phone or tablet width meets the navigation before the document rather than after it, which is the gap 0.16.0 was reaching for. The Page is still a server Component: no `'use client'`, no hook, no context, no handler, no runtime token read, and no new client JavaScript on any documentation page.\n \n **The fade is now bound to the cut it marks, and it was not.** The fade element was left unprefixed while the `padding-bottom` reserving the band beneath it was already `lg:`-prefixed. The cut exists only where the rail scrolls, which is `lg` and up, so below `lg` the band went inert and the gradient kept painting: the rail's last 2rem carried a fade over the last row or two, permanently, at exactly the width this release starts serving the tree at. Both numbers still matched and no row lost legibility, which is why it survived every lane that was looking. The fade and the band are now written against the same `lg:` as the `max-h`, the `overflow-y-auto` and the `sticky`, so a fade with nothing to fade is no longer painted. **The price, stated rather than left to be found:** below `lg` the rail is bounded by its content rather than by the viewport, so a long tree runs past the fold of a phone screen with nothing marking where it ends. No cut to mark, no band to reserve and no scroll region to scroll inside. That is the honest limit of one copy of the tree at every width, and it is now written into the Page's own documentation beside the rule that causes it.\n \n **The published surface loses two slots.** `data-slot=\"docs-nav-compact\"` and `data-slot=\"docs-nav-disclosure\"` were added in 0.16.0 and are removed here. They were published for exactly one release, and no call site in the four downstream repositories addresses either by name, so nothing there has to change. A call site that reached for one now finds nothing rather than finding an empty region, which is the honest outcome and the reason this is called out. Every slot that existed before 0.16.0 stays, `docs-rail-fade` included.\n \n **What it costs, with the number.** A phone reader now scrolls past the whole navigation to reach the article. One consumer's rail measured 1,318 pixels of content in a built export at 1280 by 680, and those rows wrap on a phone, so the scroll a narrow reader now makes is a thousand pixels or more. That is a real regression for that reader and it is the reason a disclosure was reached for in the first place. It is paid rather than hidden, because the two arrangements that avoid it are both worse: a rail at `lg` and nothing below it reopens the gap 0.16.0 closed, and drawing the tree twice to keep the scroll short is what broke three consumer test files here.\n \n **What would actually remove that scroll.** A collapsed rail below `lg` needs an open state, and a server Component cannot hold one. `<details>` is the only disclosure the platform holds on the Page's behalf, so a control that opens one has to be a client island inside the Page. That is separate work with its own budget and it is not built here: shipping a client boundary to save one scroll is a different trade from the one above, and it belongs in its own change rather than as a side effect of removing a duplicate.\n \n **One Item document was corrected, and no gate reads it.** This Page's own documentation said under Best practices that an entry with no words is a label. That was true before this work and became false the moment the Page began dropping a nameless entry rather than rendering one, and it shipped false through 0.16.0 because nothing in the tree compares that bullet with the Page's own source. The bullet now states what the Page does, and `docs/quality-gates.md` records why no gate was added for it rather than leaving the question open: a gate over prose claims has to enumerate the claims, and a list somebody maintains is the shape that already left the heading gate holding one Block and passing over four, or it has to match on wording, which fires on a rewrite and not on a lie. The decidable half of that failure class has a gate, in the heading-scale table comparison. This half is a reader's, and the hole is now named in the document that collects them.\n \n **Three things 0.16.0 got right and this release leaves as they were, with the fade's bound corrected above.** The rail fade, pinned to a non-scrolling sticky wrapper with the band it covers reserved in `padding-bottom`, both written against the `lg:` that creates the cut; the sticky `max-height` arithmetic in `100svh` rather than `dvh`, which cannot exceed the space it is pinned in; and nameless entries dropped rather than rendered as an empty `<li>`, including the group-label and empty-group arms, counted on each rail as `data-unnamed-entries`.\n\n## 0.16.0\n\n### Minor Changes\n\n- 1a0ba35: A page `h1` is now visibly larger than the `h2` sections under it\n \n A page `h1` rendered at 1.875rem and every section heading under it at 1.5rem, so\n the two were one fifth apart and the page had no top to its hierarchy. The fix is\n in the type scale rather than in the Component, because the scale stopped at `4xl`\n and there was no step for a page's own claim to take.\n \n **Two steps are authored, and they are the two widest gaps in the group.**\n `5xl` is 3rem and `6xl` is 3.75rem, both at a line-height of 1. Every other step\n in the `text` group is a whole multiple of 0.125rem and these two are too;\n `4xl` to `5xl` is four thirds, the first gap in the group wider than any gap it\n already had, and `5xl` to `6xl` is five fourths. They are authored as a pair\n rather than as one step because every rung of the heading ladder is a pair, a base\n step and one step up at `sm`, so a single step above `4xl` would have made the\n page heading the one heading in the system that does not grow at a width.\n \n Both steps reach CSS and the published pack contract the way every other authored\n step does, so `text-5xl` and `text-6xl` are utilities a consumer can compose, and\n neither is a value a consumer has to write.\n \n **The heading ladder now descends from the new ceiling, one authored step per\n level, and every level holds a step of its own.** There are exactly six authored\n steps at or above Body, so the table lands on the floor with a distinct step at\n every rung, where before `h4` through `h6` shared one. `h1` is at `5xl`, rising to\n `6xl` at `sm`; `h2` is at `4xl`, rising to `5xl`; and the table descends to `lg` at\n `h6`, which is the step Body is set at. **What a consumer sees is every section\n title moving up one step and every page heading moving up two**: a section title\n goes from 1.5rem to 2.25rem and from 1.875rem to 3rem at `sm`, and a page `h1` goes\n from 1.875rem to 3rem and from 2.25rem to 3.75rem at `sm`. No call site changes,\n because the level a Block was already passing is still what decides its size.\n \n **Five surfaces that render a page's own `h1` now ask the same table for its size\n rather than naming a step.** `page-header-01`, `docs-shell`, `blog-post-page`,\n `not-found-page` and `error-page` each wrote a fixed step into a heading that was\n meant to be a page's top-level claim, so with the ceiling lifted they would have\n rendered a page heading a rung or two below the section titles under it. They ask\n `headingSizeClass`, which is what `SectionHeading` and `Cta01` already asked, and\n `Cta01` still carries no literal.\n \n **Four more surfaces that resolved their own heading size, found the same way.**\n A Block that takes a `headingLevel` and writes the size beside it forwards the level\n and changes nothing a reader can see, which is the defect above at the scale of one\n Block. `data-table-01` said `text-lg`, `run-console-01` said `text-base`, and both\n moved the outline without moving the size. `run-stream-01` renders a literal `h2`\n at `text-lg`, which was the floor of the old scale, so a live region's `h2` came\n out below the `h3` introducing the section holding it. `/foundation/themes` said\n `text-3xl`, which had been Display and is now the `h3` step, so that page's `h1`\n rendered a rung below the section titles under it. All four ask `headingSizeClass`\n now, and the three Blocks carry `text-balance` with the size they gained: the ladder\n states weight, tracking and balance as one package and `SectionHeading` carries all\n three at every rung, so a title lifted from 16 or 18 pixels to 24 or 36 is worse\n unwrapped than the one it replaced.\n \n **The Item page's appendix heading is on the article's ladder, and asking the Block\n ladder for it was the wrong correction.** It said `text-xl`, which put an `h2` at 20\n pixels under the prose `h2` at 30 and below the prose `h3` at 24 above it. Asking\n `headingSizeClass` removed the third tier and created a worse one: \"API reference\"\n is a section of an article exactly as \"Overview\" is, so it takes the article's\n ladder, and the Block ladder is a step and a half above it at `sm`. The Item page\n then carried three sections at 30 pixels and an appendix at 48, and the appendix was\n the second largest thing on the page. It is now a bare `<h2>` inside the same\n `.prose` wrapper as the Item's own MDX body, so the article's rules size it at 30\n pixels and it carries no class of its own: every section heading in that article is\n a bare tag the article styles, and a heading naming its own step would be the second\n answer to a question the article has already answered. The table stays outside\n `.prose`, whose descendant rules would put its cells at the body step in the muted\n colour, and the gap above it is the article's own 16 pixels rather than a second\n region's 32.\n \n **The five theme names are card titles, and they compose `CardTitle` to say so.**\n They carried `font-medium` and no size, so they rendered at 16 pixels and 500 under\n a 60 pixel `h1`. Asking the ladder for the size would have been wrong: five themes at\n the `h2` step is five 48 pixel headings over five cards on a page whose own claim is\n the `h1` above them. `CardTitle` already holds the answer the library needs: a card\n title sits at body size, and the element and the visual step are separate questions.\n So the size stands, the tag stands at `h2` because the grid draws no section heading\n of its own, and the weight now comes from the one Component that owns it rather than\n from a literal. What a consumer sees is five theme names one step heavier.\n \n **The two Foundation readers took opposite answers, and the reason is worth more\n than either change.** Both render inside an MDX body, which the site wraps in\n `.prose`, and both used to carry literals, which a utility layer lets win over the\n article's own component-layer rule: `/foundation/colors` published an `h2` at 18\n pixels and an `h3` at 14, below the `h3` above them.\n \n `ColorTokens`' four headings are sections. Two divisions of an article and a\n partition under one of them is a structure, not a list of labels, so they name no\n step and the article decides, which is the one copy of the ladder there is.\n \n `TokenBrowser`'s are not sections, and dropping its literals let the wrong authority\n decide them: every group name came out at the prose `h2`, 30 pixels, which is more\n than twice the 14 pixels the navigation rail's own section titles are set at, on the\n page whose whole job is being navigated. Each of those 184 names is a table's name\n and nothing else, so they are captions: `h3` at `text-sm`, in the mono face because a\n token key is machine notation, at the same 14 pixels and 500 as the column heads of\n the table underneath. That is the caption `ApiTable` already renders for a compound\n export's part names on every Item page, so this is the repository's own treatment\n rather than a third one. One heading on the page, \"Semantic contract\", is a division\n of the article rather than a label, and it keeps naming no step.\n \n **`Prose` walks the same ladder from one rung down, and a prose heading therefore\n sits below a Block heading at the same level.** Its child treatments now set `h1`\n at `4xl` and descend one authored step per level to `lg` at `h5` and `h6`,\n which is the same scale and the same floor entered at the rung that fits the\n reading measure: Display is 3rem, and a 42rem column is fourteen of those. The new\n `h5` and `h6` treatments are new, so a document that reached past `h4` before now\n has an answer rather than inheriting nothing. **The consequence is stated rather\n than left to be found.** A Block-composed `h2` is 2.25rem and 3rem above `sm`; a\n prose `h2` is 1.875rem and does not grow. They are 1.2 apart at the base width and\n 1.6 apart above `sm`, where they were 1.25 apart before this change, so a consumer\n who sets a `Prose` section beside a Block section sees two tiers rather than one.\n `DESIGN.md` under Typography, Hierarchy now says so, and says which to reach for.\n `DocsShell` runs `Prose` at `fullWidth`, so the frame decides the width: 35rem of\n article track at a viewport of 1216px or wider, and less below that, which makes\n the case for entering one rung down stronger there rather than weaker.\n \n **`Heading`'s free `size` prop is unchanged, and this is stated rather than\n implied.** `size` is a choice made beside the tag, for a heading the surrounding\n document has already placed and wants attuned, and its largest arm is a step of\n the scale rather than a rung of the ladder. Its two largest arms were not removed\n and no new ones were added, so no consumer's call site changes and there is still\n no second route to a page `h1` from inside the library. `search-page` and\n `status-page` are the two surfaces that take a heading's *tag* from\n `childLevel(headingLevel)` and its size from this prop, and they are left as they\n are: those are labels over a list of results and over a list of incidents, and a\n caption is not a rung.\n \n **`scripts/check-heading-scale.mjs` now holds three things it described but did not\n check.** Its rule 4 claimed a table that jumps is a finding and only tested for a\n table that rises, so a table that skipped a rung passed it: descending, reaching\n the ceiling and flooring at or above Body, and rendering the page `h1` one gap\n above its own `h2`, which is the defect these steps exist to remove. And the\n literal Display step it refused on `Cta01` was written out as `text-3xl` and\n `sm:text-4xl`, which stopped being Display the moment the ceiling moved. Both are\n now read from the authored scale and the shipped table rather than restated, and\n both have a staged fixture in `scripts/__tests__/heading-scale.test.mjs`. The third\n is the Item rule below, which is new rather than a restatement.\n \n **The Item document's own table was the retired ladder, and it now states this\n one.** `apps/site/items/component/layout/section/section.mdx` is the page a reader\n is on when they ask what `as` resolves to, and it listed `h1` at `text-3xl` and\n `h4` through `h6` all at `text-lg`: the table as it was before this change. It now\n states the ladder the Component renders, and the sentence under it about where the\n scale stops has been corrected with it, because a corrected table beside a stale\n paragraph is the same disagreement one row down.\n \n **The gate also reads the Item document now, and that is the third copy of the\n table.** `apps/site/items/component/layout/section/section.mdx` states the ladder\n in its prose, and neither the declaration build nor the corpus reaches it, so when\n the ceiling moved it kept publishing the retired table. The new rule compares it to\n the same `HEADING_SIZE` the JSDoc is compared to, by the same cell-for-cell shape,\n so it names no step of its own; it reads the whole Item rather than a slice, so a\n second table in that prose is a finding; and it fails an Item that states no table\n at all, because a comparison with no rows finds nothing to disagree with. Three\n staged fixtures cover the three cases.\n \n **What the gate still does not hold is written down rather than left to be\n discovered.** `Cta01` is named as a root because it is the one Block whose heading\n cannot be composed from `SectionHeading` at all, so a literal step on it has no\n table to fall back to. Beyond that file the gate cannot go, and the reason is\n stated rather than left to be found: whether a heading a Block renders is a rung of\n the ladder or a caption inside that Block is not decidable from source.\n Forty-eight headings across `packages/ui/src` and `apps/site/src` name a step of\n their own, the eleven rungs among them now ask the table, and the thirty-seven that\n remain are captions. Two surfaces bind a caption to `headingLevel` rather than to\n `childLevel`, so a rule reading the tag cannot sort them, and a root list holding\n the rungs would be a list somebody maintains. That is how the `Cta01`-only rule\n came to hold the one Block already fixed and pass over the four that were not, and\n the count is written into `docs/quality-gates.md` so the next reader starts from it.\n `TokenBrowser` adds one more to that population, which is the point of naming a\n caption's step rather than inheriting it: the scan can see it, and it is below the\n floor by arithmetic.\n \n **The limit of the scan that took that count is now written down too, and it is the\n more useful half of it.** The count came from looking for a literal size utility\n beside a heading tag, and a heading whose size it inherits names nothing: the five\n theme names above carried no size at all, and two scans of exactly that shape passed\n them at 16 pixels and 500. A size that is inherited is a size nobody chose, and \"no\n literal\" is invisible to a scan that looks for literals, so that count has never\n been a count of every heading that names a step, and the number nobody chose is the\n half of it that reached a reader. What the count is good for is the arithmetic: a\n heading it reports is a rung if the step it names is at or above the step Body is set\n at, and a caption if it is below, so the two sort without anybody deciding which is\n which. The remedy for what the scan cannot see is on the surfaces rather than in the\n gate, and it is the one the token inventory now uses: a caption names its own step,\n so it is visible to the scan and it is below the floor by definition.\n \n The bump is a minor in both packages rather than a patch because a new authored\n step is a new public contract: `--text-5xl` and `--text-6xl` with their\n `--line-height` companions enter the emitted CSS and the published pack contract,\n and every heading on every page composed from a Block moves.\n- 1a0ba35: `BlogPostPage` refuses a post whose display date is its machine date\n \n Measured on `www.nanisoft.com/blog/where-the-products-stand`: the page rendered the date\n as `2026-09-26`, the raw machine value. All four NaniSoft sites pass one raw ISO string to\n both `date` and `dateTime`, so the reader was handed a machine value and the `datetime`\n attribute was handed the same one.\n \n **The contract stands and the Page now holds it.** `date` is the words a reader sees and\n `dateTime` is the machine value on the `time` element, and they are separate props\n precisely so the two can differ. Formatting is a locale decision, so it belongs to\n whoever owns the fact, and Prism renders `date` verbatim. What was missing was any way for\n the library to notice when a caller did not do that: a prop called `date` that takes a\n `string` is not obviously wrong, the defect is invisible to every reviewer who does not\n open the page, and a JSDoc block is read only by whoever is integrating the library. So\n the Page now throws when `date` and `dateTime` are the same string, naming the fix in the\n message. `StackGrid01` refuses its own missing `ownLabel` for the same reason and by the\n same mechanism, and a rule that is only written down is a rule four repositories read and\n none of them are caught by.\n \n **The refusal is on the pair, not on the shape of either value.** A site that writes\n `2 Sep 2026` and a site that writes `2026-09-26` both pass. What fails is handing the\n Page no display formatting at all, and the machine value is already on the element in the\n `datetime` attribute for every feed reader and every index, so displaying it a second\n time is a redundancy rather than a choice.\n \n **`dateTime` is typed as the shape it is**, `` `${number}-${number}-${number}` ``, so a\n formatted string passed there is a compile error rather than an attribute no reader can\n check. `date` stays a `string`, and the reason is in the Component's JSDoc: no TypeScript\n type separates `21 September 2026` from `2026-09-26`, which is exactly why the refusal\n above has to exist at run time.\n \n **This is a breaking change and it is released as a minor.** It breaks every consumer at\n build time, so it is a break and not an additive change, and each of the four call sites\n below has to change in the same release:\n \n - `alphalens/app/blog/[[...slug]]/page.tsx`, lines 121 and 122\n - `atlas/app/blog/[[...slug]]/page.tsx`, lines 137 and 138\n - `landing-page/app/blog/[[...slug]]/page.tsx`, lines 119 and 120\n - `nexus/app/blog/[[...slug]]/page.tsx`, lines 135 and 136\n \n Each is the same two lines, `date={page.data.date}` and `dateTime={page.data.date}`, and\n each needs a formatted string in the first. `CONTRIBUTING.md` says a breaking change is a\n `major`; `DESIGN.md` records the decision that a minor is the right line for a break from\n a `0.x` version, with the reasoning that taking `1.0.0` would declare the public API stable\n rather than describe the change. That reasoning is dated and deliberate, so the minor is\n taken here and the disagreement with the contributing guide is recorded rather than\n silently resolved.\n \n `DESIGN.md` states the rule once, in **The composition layers** beside the other Page\n rules. Nothing else about the Page changed: same props, same elements, same `data-slot`\n values, and it is still a server Component with no client code and no router.\n\n### Patch Changes\n\n- 1a0ba35: `Diagram` labels stay legible on a phone, and no stroke crosses one\n \n Two defects, both measured on the five-node architecture drawing the company\n site renders. Its node names were drawing at 6.95 CSS pixels and its relation\n words at 5.88 in the 342 pixels a phone leaves the drawing, and ten of the\n twenty-five label and stroke pairs on that drawing intersected: all five node\n names were struck through by an edge leaving their own node, and all five\n relation words sat on their own line.\n \n **Labels now have a floor, and it is the drawing's own coordinate space.** Type\n inside a scaled `<svg>` is sized in user units, so a label's rendered size is its\n size in user units times the rendered width over the canvas. That is why the\n canvas is fixed, and it is not the whole answer, because it has no floor in it.\n The drawing is now never rendered narrower than 640 pixels, one user unit is\n therefore never less than one pixel, and the smallest label the drawing sets is\n twelve units, which is what `text-xs` resolves to. A container that cannot give\n it that much scrolls sideways rather than letting the labels fall through it,\n which is the arrangement `Table` already makes with seven columns and `Gantt01`\n with a schedule of names and bars.\n \n The cost is stated rather than hidden: on a phone a reader sees about half the\n drawing at a time and pans for the rest. The alternative was the whole drawing at\n 5.88-pixel type, and `DESIGN.md` records that trade under **The figure floor**.\n Nothing above 640 pixels of container changes at all, so no desktop or tablet\n rendering moves.\n \n **Strokes now stop where the ink runs out, and each label is asked on its own.**\n A node's name is printed under its mark, so a relation drawn centre to centre ran\n through the name under whichever node it left downward. The first version of this\n fixed that by trimming every stroke back to the box enclosing the mark and every\n label beside it, which is also wrong in the other direction: a relation along a\n row never touches the name printed under its mark, so backing it off by the\n half-width of that name detached it from the mark it connects. On three things on\n one baseline with names at length, every end of every relation stood sixty units\n clear of its mark. A relation is now trimmed against whichever comes first, its\n own mark's edge plus the clearance or the far side of an individual label the\n stroke actually runs through, so a relation leaving sideways reaches its mark and\n one leaving downward still stops below the name in front of it. The whole line\n between the two marks is divided once rather than each end trimmed alone, because\n a name and a subtitle sit below their mark at two depths and a stroke arriving\n from below runs through the first and back into the second.\n \n The box is computable from a server Component because every label in a drawing is\n set in the monospaced face, where a label's width is its character count rather\n than a measurement. That is what `DESIGN.md` records under **The ink-avoidance\n rule**, with the reason a halo was rejected and the direction clause that the\n first form of the rule was missing.\n \n **The drawing is wrapped in a container.** It carries `data-slot=\"diagram\"` and\n still scrolls, and the wrapper carries `data-slot=\"diagram-container\"`, following\n `Table`'s `table-container`. Every existing `data-slot` value, the derived or\n passed `aria-label`, `role=\"img\"`, and `data-unresolved-relations` are unchanged,\n so no call site and no test that addresses the drawing by slot needs to move. The\n Component is still a server Component with no client code.\n- 1a0ba35: `FactList` sets its values flush left, and says why there is no numeric mode\n \n Rendered on `www.nanisoft.com/about`: a three-row fact list where every value was set\n `text-align: right`. The middle answer is long enough to wrap, so its second line began\n wherever the first one ended and the column read as ragged-left, which is what a mistake\n looks like rather than what a decision looks like.\n \n **Right alignment is the right answer in a column of figures, and `FactList` has no\n figures.** That was the question to settle before changing anything, because the two\n answers need opposite fixes. `FactList` takes `label` and `value` and nothing else: there\n is no `numeric` prop, no `tabular-nums` anywhere in the module, no monospaced arm and no\n per-row alignment of any kind. So nothing in it is a figure, nothing lines up on its last\n digit, and what right alignment moved was the first letter of each answer to a different\n place on every row. The suite asserts the absence as a fact rather than restating a prop\n list, so a `numeric` prop added later fails there instead of quietly making the default\n wrong for every caller that never passes it.\n \n The value is now `text-left`, and `text-balance` stays, because it is the same reason it\n was there: a two-line answer should break where it breaks well rather than where the\n measure runs out.\n \n A caller that genuinely wants a right-aligned column of figures is not refused anything.\n `value` is a `ReactNode`, so the caller can pass the span it wants, and Prism holds no\n opinion about a value it did not lay out.\n \n Nothing else about the Component changed: same element, same props, same `data-slot`\n values, same server Component with no client code.\n \n **One thing found and not changed, because it is a different claim.** The Component's own\n JSDoc says the term column is a fixed fraction rather than a content width, and the code\n says otherwise: the `dt` carries `shrink-0` and no basis, so the term column is sized by\n its content and the left edge of the value column is therefore not straight down the\n list. That is a real mismatch between a documentation source and the thing it documents,\n and left-aligning the value does not make it worse or better. Fixing the code to match\n the sentence would move every fact list in four consumer repositories, so it is a decision\n for the maintainer and not a side effect of this one. Recorded here rather than made.\n- 1a0ba35: `StackGrid01` lifts both arms of the grid to the type floors\n \n Measured on `nexus.nanisoft.com`: the in-house arm set its tile names at 14 pixels and\n the line under them at 12, so a heading on this Block rendered smaller than the copy\n around it and the tile read as a caption rather than as a tile.\n \n **Both arms are raised, and the reason is a floor rather than a comparison.** `DESIGN.md`\n already states both, which is why this is a Component coming into line rather than a new\n rule:\n \n - A tile name is a card title, and `CardTitle` says a card title sits at Body. Body is\n `text-lg` in this system, 18 pixels, so a name at 14 was below it.\n - A role and a blurb are supporting copy, and `DESIGN.md` gives supporting copy\n `text-sm`, 14 pixels, so a line at 12 was below that too.\n \n The first is the floor the heading ladder stops to avoid: `lg` is the deepest authored\n step that is not smaller than Body, precisely so that a heading never renders smaller\n than the copy it introduces. A tile name at 14 against copy at 18 broke exactly that.\n \n **The two arms set the same steps in both, which is a fact and not a comparison.**\n They already agreed here, so this was not a reconciliation and the reported arms\n never disagreed: what agreed was below both floors. The difference the two arms are\n supposed to carry is a claim about provenance, and it is carried in weight and ink,\n the muted tile against the page with a `border-primary` hairline and the `md` shadow\n step. A second group set quieter than the first would be a grid claiming the in-house\n work matters less than the assembled parts, which is the opposite of what the group is\n for. The assertion in the suite states the agreement as a fact so it cannot drift.\n \n The mono annotation over an in-house name and under a codename is left at `text-xs` on\n purpose. That is a machine annotation in the platform monospace stack, which is what\n `DESIGN.md` uses the mono face for, not a run of prose, and it is the only type on the\n tile that is not prose.\n \n No `data-slot` changed, no prop changed, no field was added to either arm and neither\n arm was widened, and the Block is still a server Component with no client code. The\n tile's own height grows by the four pixels of the name plus the two of the line, which is\n what the floors cost and not a change of rhythm: the grid was already `h-full` per row, so\n a row of tiles is as tall as its tallest and both arms move together.\n- 1a0ba35: `SiteFooter` links are a 24 pixel target instead of a 17 pixel one\n \n Measured in the built exports of three consumer sites: six to eight destinations per\n footer, every one of them 17 pixels tall (`39x17`, `30x17`, `42x17`, `33x17`, `70x17`,\n `37x17`), with the code a reader can see at\n `https://www.w3.org/TR/WCAG22/#target-size-minimum` asking for 24 by 24.\n \n **The link had no box.** A bare inline anchor is not a 17 pixel target by design; it\n has no box at all. What a pointer aims at on one is the font's content area, and for\n the shipped face at `text-sm` that is 16.94 pixels: fourteen pixels of type times the\n face's own ascent and descent, times its size-adjust. Those three numbers are not a\n guess, they are read out of the `@font-face` this package ships for `Inter Fallback`,\n and reading them is what produced 16.94 against the 17 in the export. The social row\n was the other shape and the other number: an `inline-flex` box, so its height is its\n line box, 20.01 pixels.\n \n **The floor is paid on the box, so the link looks the same.** Both links are now\n `flex min-h-6 items-center`: a block at least `--spacing-6`, 24 pixels, tall, with the\n same fourteen-pixel type at the same weight in the same colour inside it. It is the\n arrangement `mobile-nav` already uses for a stacked navigation link at the\n coarse-pointer floor, one step lower and at every pointer rather than only on a coarse\n one. `--spacing-6` is arithmetic on the authored spacing multiplier the way every\n `size-*` and `h-*` in this package is, so a retune of the scale moves the floor and the\n Block could not have pinned it.\n \n **No height anywhere moves.** A footer column link sat in a line box built from the\n footer's inherited sixteen-pixel body, so each row was already 24 pixels tall before the\n floor and each row is the same 24 after it; only the 17 pixels of target inside the row\n grew. The footer's own height is unchanged at both widths, which is the outcome four\n consumer pages are composed against, and the assertion that says so is in the suite.\n \n The 24 pixel circle exception in the criterion arguably held, because consecutive rows\n in a footer column stand 32 pixels apart centre to centre and a 24 pixel circle on\n either clears the other. The fix is made anyway, and `DESIGN.md` records why under\n **The target-size floor**: a target whose size depends on the gap beside it is one edit\n away from failing and nothing in this repository would report it.\n \n No `data-slot` changed, no prop changed, no call site moves, and the Block is still a\n server Component with no client code. `DESIGN.md` states the rule once, in the section\n beside **The coarse-pointer floor** it is deliberately not merged with.\n- 1a0ba35: `DocsShell` marks where its rails cut off, drops the rows it cannot name, and reaches a tablet\n \n Three defects on the one documentation screen the other NaniSoft sites import.\n \n **The contents rail hid a third to a half of the navigation behind an invisible\n scroll.** The rail is capped against the viewport and scrolls inside that cap, and\n Chrome's scrollbars are overlay scrollbars, so they appear only once the reader has\n already scrolled. Measured in two consumers' built exports at 1280 by 680, the rail's\n content stood at 912 and 1318 pixels in a 568 pixel box: 344 and 750 pixels of a\n primary documentation navigation below the fold with `mask-image: none`, no\n persistent scrollbar, no fade and no count. The last visible row was cut mid-word\n above dead space, which reads as a rendering fault rather than as a scroll region.\n A cap nobody can see is not a cap.\n \n **The affordance is a fade across the foot of each rail, and the space it covers is\n reserved rather than borrowed.** The sticky offset moved to a wrapper that does not\n scroll, because nothing inside a scroll container stays put while that container\n scrolls: a gradient or a mask written into the scrollport travels with the content\n and marks nothing. The wrapper travels, so the gradient is pinned to its foot, and\n the `<nav>` inside keeps the cap, the independent scrolling and the whole of its\n class contract. The rail reserves `padding-bottom` of the same height as the fade,\n which is the half that makes it honest: at rest the gradient falls on reserved space\n and the last row is fully legible, and only while there is more below does it fall on\n content. Without that padding the same gradient washes the last row permanently,\n which is the usual cost of this answer and the reason it is not optional.\n \n Two alternatives were rejected for stated reasons rather than passed over. A\n persistent themed scrollbar is truthful at both ends and costs nothing to\n legibility, but it says \"there is more\" without saying which way, and it needs its\n gutter reserved in a 15rem rail so a short tree becoming a tall one does not shift\n the content. A collapse control, and an \"N more\" control, both need state or a count\n this Page cannot have: one is a client Component and the other is a measurement a\n server Component cannot take.\n \n The fade costs a keyboard and a screen-reader reader nothing. It is an empty element\n with `aria-hidden` and `pointer-events-none`, inside the `<nav>` and outside the\n `<ul>`, so it is not a list item, it is never announced, it never takes a press and it\n never reaches a Tab stop. The region still scrolls on its own, so the arrow keys walk\n every entry exactly as before.\n \n **The Page can emit a navigation row containing nothing, and now refuses to.** An\n entry whose `title` is the empty string rendered as\n `<li data-slot=\"docs-nav-page\"><span data-slot=\"docs-nav-label\"></span></li>`: a row of\n nothing inside a `<nav>`, which one consumer measured at 102 across 25 of its 31\n documentation pages, because that site maps a heading whose title arrives as a React\n element to `''`. A blank row is not a destination, and it is not a label either,\n because a label is words. The Page now drops the entry, drops a group label it cannot\n name while keeping the pages under that group, and drops a group that has neither\n words nor pages. Nothing is swallowed: the rail carries the tally as\n `data-unnamed-entries`, the same answer `Diagram` gives a relation it cannot resolve,\n so a consumer can tell \"nothing was wrong\" from \"the Page stopped counting\".\n \n A blank `title` and a blank `href` are still answered differently, because they have\n different amounts left. A page with an address and no words is a real page with one\n unrenderable field, so losing the row costs a reader nothing while taking the screen\n down over it would cost them everything. A page with words and no address is a row\n whose entire content is a route that does not exist, and that is still refused by\n name.\n \n **Below `lg` there was no navigation at all.** At 768 and at 1024 both rails were\n `hidden` and the only navigation left inside the article was the two-item pager at\n its foot, so a reader on a thirty-one document reference at tablet width had no\n contents, no on-this-page and no way to a sibling page except Previous and Next. Each\n tree is now also drawn behind a `<details>`, first in the document so it is what a\n narrow reader meets before the text. `<details>` is the disclosure that needs no\n runtime: the platform holds the expanded state, the control is in the Tab order with\n a real expanded state, and the Page stays a server Component with no hook, no context\n and no handler. The summary carries `navLabel` or `tocLabel`, which is the caller's\n own word for that navigation, so the Page ships no reader-facing copy and a\n consumer that files its documentation in another language gets the control in that\n language by having named the region once.\n \n The tree is therefore in the document twice, and only one copy is displayed at any\n width: the rail is `hidden lg:block` and the disclosures are `lg:hidden`, so the\n other is `display: none` and is out of the accessibility tree and out of the tab\n order rather than a second list a reader meets.\n \n **What did not change, and what a consumer may notice.** Every existing `data-slot`\n value stays, so no call site and no test that addresses the Page by slot needs to\n move; three new slots are added, `docs-rail-fade`, `docs-nav-compact` and\n `docs-nav-disclosure`. The rail's `<nav>` now sits inside a wrapper, which is what\n lets the fade stay put, so a consumer stylesheet reaching for it as a direct child of\n `[data-slot=\"docs-rail\"]` reaches the wrapper instead. `data-slot` is documented as\n markup metadata rather than a styling API, and `Diagram` and `PulseGraph` took the\n same wrapper for the same reason in the same release line.\n \n The bump is a patch rather than a minor because nothing a consumer composes through\n widens: no prop is added, no export is added, no token and no name in the contract\n move, and no call site changes. What changes is what the Page draws from data it\n already accepted.\n \n **The one thing this leaves alone is stated rather than left to be found.** Below\n `lg` the frame is a single column and this Page runs `Prose` at `fullWidth`, so the\n frame decides the track: the article column is the whole container, which is 45rem at\n 768 and 61rem at 1024, against the 35rem it gets at 1216 and above. That is a wider\n measure than the one the reading column is held to, in the band between the two\n breakpoints, and it is a frame decision rather than a `Prose` one. A phone at 390\n gets 21rem and is inside the measure. Capping the article track below `lg` is a\n deliberate layout choice with two honest forms, a centred column at the measure or a\n two-column frame from `md`, and either one changes what a narrow reader sees on\n every documentation page in the family, so it is left to the maintainer rather than\n taken here beside two defects that had no such choice in them.\n- 1a0ba35: `PulseGraph` labels stay legible on a phone, and no stroke crosses one\n \n Three defects, all measured on a four-stage pipeline the way `Diagram`'s two were\n measured on the five-node architecture drawing the company site renders.\n \n **Labels now have a floor, and it is the drawing's own coordinate space.** Type\n inside a scaled `<svg>` is sized in user units, so a label's rendered size is its\n size in user units times the rendered width over the canvas. That is why the\n canvas is fixed, and it is not the whole answer, because it has no floor in it. In\n the 356 pixels a phone leaves the drawing, a stage name was rendering at 7.23 CSS\n pixels and a node's note at 5.56, and ten user units was the smallest type the\n whole package emitted anywhere, including inside a figure. The drawing is now\n never rendered narrower than 640 pixels, one user unit is therefore never less\n than one pixel, and the smallest label the drawing sets is twelve units, which is\n what `text-xs` resolves to. A container that cannot give it that much scrolls\n sideways rather than letting the labels fall through it, which is the arrangement\n `Table` already makes with seven columns. Nothing above 640 pixels of container\n changes at all, so no desktop or tablet rendering moves.\n \n **Connectors now stop where the ink runs out, and each label is asked on its own.**\n A node's name is printed under its mark, so an edge drawn centre to centre ran\n through the name and the note under whichever node it left downward. On the\n pipeline this fix lands on, eight of the thirty-two label and connector pairs\n intersected: the two stages nearest the top of the canvas, each with its own name\n and its own note struck through by the two edges that met it. The first version\n of this trimmed every connector back to the box enclosing the mark and every\n label beside it, which fixes the crossings and detaches the drawing instead: a\n stage's name and note both sit below its mark, so a connector along the rail\n touches neither, and backing it off by the half-width of the widest name at the\n node left four of the five connectors on the six-stage figure floating free of\n both their marks, by up to fifty-five units. A connector is now trimmed against\n whichever comes first, its own mark's edge plus the clearance or the far side of\n an individual label it actually runs through, so all five connectors on that\n six-stage figure touch both their marks with the clearance between them and the\n stroke is thirty to fifty units longer, and a connector leaving a node downward\n still stops below the name in front of it. The whole line between two marks is\n divided once rather than each end trimmed alone, because a name and a note sit\n below their mark at two depths and a connector arriving from below runs through\n the first and back into the second.\n \n You do not have to space your nodes to avoid this, and you always had to space\n them to keep two names from touching each other.\n \n **The rail no longer sits on a note.** The rail was a fixed distance up from the\n bottom of the canvas while a node's note reached a fixed distance below its mark,\n so the two met on one range of mark positions: a lane whose mark landed low enough\n put its note under the rail's own stroke. The fitted area now stops above the band\n the rail reserves, which is the rail's distance up from the bottom plus its own\n thickness, the drawing's ink clearance, and the deepest ink a node draws. A graph\n with no lanes has no rail, so nothing reserves the foot and the fit is unchanged.\n \n **The drawing is wrapped in a container.** It carries `data-slot=\"pulse-graph\"` and\n now scrolls, and the wrapper carries `data-slot=\"pulse-graph-container\"`, following\n `Table`'s `table-container` and `Diagram`'s `diagram-container`. Every existing\n `data-slot` value, the `role=\"img\"`, the passed `aria-label`, `data-lanes` and\n `data-unresolved-relations` are unchanged, so no call site and no test that\n addresses the drawing by slot needs to move. The Component is still a server\n Component with no client code.\n \n `DESIGN.md` records the two rules this applies under **The figure floor** and **The\n ink-avoidance rule**, the latter with the direction clause that the first form of\n the trim was missing, and the geometry both Components now share is one internal\n module rather than a second copy in each.\n- Updated dependencies [1a0ba35]\n - @nanisoft/prism-tokens@0.16.0\n\n## 0.15.0\n\n### Minor Changes\n\n- 6dc7bb8: A Block action is a destination or your own control, and a gate now holds it\n \n **Five Blocks shipped a control that could not be acted on, and the previous release\n fixed three of them without the law.** `Hero01`, `Hero02` and `Hero03` each declared\n an action union whose `href`-less arm rendered a bare `Button`, and that release closed\n the arm and then said, in its own changeset, that seven sibling Blocks still had it and\n that \"a law held by a type in three Blocks is three types\". That was the right\n reasoning and it was also the reason the hole stayed open: the next audit found five\n more Blocks, and one of them had a shape nobody had looked for.\n \n **`About01` and `Showcase01`** declared the same two-arm union as the heroes and\n rendered a `Button` on the arm with no `href`. Each one's JSDoc called it \"inert by\n design\" and defended that as a Block shipping no behaviour, which is true and is not an\n answer. Both are now a union of two named arms: `AboutLinkAction` /\n `AboutSlotAction` and `ShowcaseLinkAction` / `ShowcaseSlotAction`. `href` is required\n on the link arm; the slot arm carries the caller's own `ReactNode` and forbids `label`,\n `href`, `newTab` and `variant`.\n \n **`PageHeader01` was worse, and its own type said so.** `PageHeaderAction` had **no\n `href` member at all**, so there was nothing to make optional: `{ label: 'Share' }` was\n the only value the type could express and every value rendered a `Button` that\n activated to nothing, at the top of every screen a consumer rendered. It is now the\n same two-arm union. Its `actionsSlot` sibling prop is **gone**, and that is the one\n structural change here: a sibling slot renders before the row's own actions and cannot\n say which position it fills, so a caller who wanted a primary menu trigger at the front\n and a link behind it had to put the link first and accept the order, or pass the trigger\n in `actions` where it was a dead button. As an arm, position and element are the same\n value.\n \n **`Pricing01` was the one no audit had found.** `Plan` carried a required `cta: string`\n and the Block rendered it as a `Button`, so every pricing table in every consumer's\n product shipped a focusable control, announced as a button, that activated to nothing, on\n the card a reader was about to decide on. `cta` is replaced by `action`, a two-arm union,\n and a plan whose control is a checkout trigger is the `slot` arm. The featured plan\n still draws a filled anchor and the rest an outlined one, so the recommendation still\n reads. `pricing-01/index.tsx` also exported the value alone until now, so a consumer\n could not name `Plan` at all; `Plan`, `Pricing01Props` and the three action types are on\n the surface.\n \n **`Plan` now carries a required `id`, which is the second change to one record and was\n deferred from this entry once.** The first version of this changeset recorded the\n observation and the reason for leaving it: *\"a second breaking change to a record this\n release already changes, and bundling two migrations into one entry makes both harder to\n read. It is a follow-up, not an oversight.\"* Both halves of that reasoning have since\n stopped applying, and the second one was never quite true.\n \n The `id` is real. `Pricing01` keyed its cards on `plan.name`, which is the word a reader\n reads on the card, so a monthly and an annual row of the same tier produced a React\n duplicate key and cards the renderer had been told are ambiguous, and a rename in the\n copy was a rename in the key, which loses a caller's saved selection over an edit to a\n sentence. `Careers01` and every other Block in this package key on an `id` and say why;\n `Pricing01` was the last one that did not.\n \n The bundling reason does not survive contact with the release. There is no released\n boundary between the two changes, because both are in this same unreleased body of work,\n so \"bundling\" describes nothing a reader would ever have to unpick: they upgrade once\n and read one entry either way. And one `Plan` with two migrations in one place is more\n readable than one `Plan` with two migrations in two places, because the second entry has\n to re-establish that it is talking about the same record. It is in this entry for that\n reason and the deferral was the mistake, not the delay.\n \n **`Waitlist01` is the fifth, and it is not a union.** `Waitlist01Referral` declared\n `copyLabel` and `copied`: the accessible name of a copy control and the state to draw on\n it. The Block then rendered `<Button type=\"button\">{copyLabel}</Button>` with **no\n handler**, beside a read-only field holding the code it was labelled as copying. The\n JSDoc described a second weight and a `data-copied` attribute for a control that never\n copied anything. Both props are gone and `copyControl` is a `ReactNode`. Prism will not\n call `navigator.clipboard` on a consumer's behalf and then report a success it cannot\n verify: the permission is the caller's to grant, the secure context is the caller's to\n know about, and a copy can fail. The sentence that says it worked belongs in `status`,\n which is where it already belonged.\n \n **The four Blocks whose union renders nothing on the `href`-less arm were already\n correct, and the audit that named them was wrong.** `Careers01`, `CaseStudies01`,\n `Industries01` and `Services01` each declare an item union whose no-link arm renders **no\n control at all**: the `CtaLink` is inside `isLink ? … : null`. That is the right shape\n for a card, where a link is an addition rather than a required part, and there is nothing\n to repair. `Gallery01` is the same story with a different reason: its tile is a real\n `<button>` with an `onClick` that opens the lightbox, in a `'use client'` Block. Five of\n the nine names in the earlier changeset were false positives, and saying so is part of\n the record: a list of sites is a claim about a tree, and the tree is what settles it.\n \n **`scripts/check-block-controls.mjs` is the gate, and it is green.** The previous release\n declined to write one because it would have failed on five real Blocks, and that was the\n right call at the time. It can be green now, so it exists. It reads `packages/ui/src/blocks`\n and `packages/ui/src/pages` and fails on a rendered `Button` or `CtaLink` that carries\n none of `onClick`, `type=\"submit\"` or `type=\"reset\"`, and no `href`. It deliberately does\n **not** read `apps/site/items`, because the documentation Demo for `Button` renders\n `<Button>Save changes</Button>` with no handler and showing the control is the whole\n point of showing it; the demos are held by the compiler, because a demo passes a Block's\n props and fails to build the moment `AboutAction` stops accepting a dead action. The\n gate's own proof is `scripts/__tests__/block-controls.test.mjs`, which plants each of the\n five shipped shapes as a fixture, because the five Blocks that shipped them have since\n been fixed and a gate whose only evidence is a clean run is a gate nobody has watched\n fail.\n \n Two things the first version of that gate got wrong are recorded because they are the\n kind of defect a clean run hides. It deleted comments rather than blanking them, which\n removed their newlines and printed every finding below the first JSDoc block on the\n wrong line. And it matched a tag's attributes with `<Name([^<>]*)>`, which stops at the\n first `<` or `>` in the attribute text, which in JSX is very often not the end of the tag:\n `onClick={() => step(-1)}` contains `=>`, `disabled={position <= 0}` contains `<=`, and\n `disabled={currentPage >= pageCount}` contains `>=`. Each truncated the attributes before\n the handler two lines further down, so three Blocks with working handlers were reported\n as shipping dead buttons. The tag end is found by brace and paren balance now.\n \n **The bump is `minor` rather than `major`, and the argument is the version line rather\n than the severity.** Five Blocks' public prop types change and two of them (`Plan.cta`\n and `Waitlist01Referral`) do not compile against a value that compiled a release ago.\n That is breaking by any ordinary reading and this entry says so plainly. `major` in this\n repository publishes `1.0.0`, and every release to date has been `minor` on a `0.y.z`\n line where the `y` is already the breaking-equivalent slot; publishing 1.0.0 with this in\n it would assert an API stability guarantee this library has not earned, and it would be\n the first release in the project's history to use the bump at all. The same argument\n carried the hero change and the `Cta01` change at 0.6.0. The break is real and is the\n subject of this entry; the number it lands on is the line's decision.\n \n Migration, per Block:\n \n ```tsx\n // About01, Showcase01, PageHeader01: an action with no href\n // Before\n actions={[{ label: 'Start free' }, { label: 'Docs', href: '/docs' }]}\n // After\n actions={[\n { slot: <NextLink href=\"/start\">Start free</NextLink> },\n { label: 'Docs', href: '/docs' },\n ]\n \n // PageHeader01 only: actionsSlot is gone, and the node moves into the row\n // Before\n <PageHeader01 actions={[{ label: 'New deployment' }]} actionsSlot={<Menu />} />\n // After\n <PageHeader01 actions={[{ slot: <Menu /> }, { label: 'New deployment', href: '/deployments/new' }]} />\n \n // Pricing01: `cta: string` becomes `action`, and the card is keyed on an `id`\n // Before\n plans={[{ name: 'Team', price: '$24', features: [], cta: 'Choose Team' }]}\n // After\n plans={[{ id: 'team', name: 'Team', price: '$24', features: [], action: { label: 'Choose Team', href: '/signup?plan=team' } }]\n \n // Waitlist01: the copy control is the caller's, whole\n // Before\n referral={{ value: CODE, label: 'Your referral code', copyLabel: 'Copy code', copied }}\n // After\n referral={{\n value: CODE,\n label: 'Your referral code',\n copyControl: <Button onClick={() => navigator.clipboard.writeText(CODE)}>Copy code</Button>,\n }}\n ```\n \n Every one of these was a compile error before and is a compile error now, which is the\n point: a consumer upgrading finds a failed build rather than a page that silently renders\n nothing.\n \n **One thing about the `Pricing01` migration is separate from the rest of it.** Every other\n line above changed a member a consumer had to add. The `id` also changes what a consumer\n has to add to the same object, so a plan that compiles after this release and renders two\n cards correctly is a plan that passed an `id`. Nothing here needs a second entry, and the\n observation that started this is in the entry above rather than at the end of this one.\n- 6dc7bb8: A disclosure opens at the step the motion scale gives it, and the rail says why it is still a width\n \n `DESIGN.md` has said for a while that `duration-slow` is \"transform or layout\n state such as a disclosure\". Three disclosures animated a height or a width at\n `duration-base`, which is 160ms against the 280ms the scale assigns them, and both\n chevrons rotated at `base` beside a panel that now takes longer than the icon does.\n \n **`AccordionContent`, `CollapsibleContent` and `Sidebar` are on `duration-slow`,\n and so are the two chevrons.** A chevron and the panel it opens are one motion, and\n a 160ms icon against a 280ms panel is two events where the reader is watching one.\n \n **`Progress` stays at `duration-base` and this is the one exception the scale has.**\n A disclosure is a spatial transition that happens once, on a reader's click, and\n 280ms is what makes it read as opening. A progress indicator's value changes on\n every tick of a running job, often several times a second, and the reader did not\n cause it: at 280ms the bar lags the work it is reporting. It is the same\n distinction the two laws of motion draw, applied inside the feedback scale. An\n answer wants the long end of the band; a reading wants the middle of it.\n \n **`Sidebar`'s rail still animates `width`, and it is a named exception rather than\n an unexplained gap.** `Progress` and `RangeField` both express their value as a\n `scaleX` about the inline start, and a rail cannot: it carries a mark, a list of\n items with a 16px icon, each item's label and a trailing count, and a `scaleX`\n scales every one of them into ovals, condensed labels and unreadable numbers. The\n decisive part is that a transform cannot reflow text. At `scaleX(0.25)` the labels\n are still laid out for a 16rem column, so the rail's contents would never reflow\n into the 4rem column they are supposed to occupy, and the only property that does\n that is the one being animated.\n \n The cost is stated rather than argued away: it is a main-thread layout pass, once\n per reader's click, over one element with a small subtree. That is a different\n order of cost from a bar that advances on every frame of a running job, which is\n why the two were not the same decision.\n \n `DESIGN.md`'s motion section records all of it, so the table and the call sites no\n longer disagree.\n- 6dc7bb8: A form-level error marks the field it is about, not every field\n \n `AuthForm01` set `aria-invalid` on every field whenever a form-level `error` existed,\n so a reader tabbing through a sign-in card heard \"invalid\" on a correctly filled\n email address because the password was wrong. A form-level message is usually not\n about a control at all: bad credentials, a locked account and a rate limit are three\n things no field is wrong about, and the first thing a screen reader says about each\n field is the state, so a reader told two correct answers are wrong stops trusting the\n rest of the form.\n \n `AuthFormField` gains `errorId`. The Block marks the field whose `id` the caller\n names there and no other, and a caller with nothing to name leaves every field\n unmarked, which is the honest state: the message is still drawn in an `Alert`, which\n is announced when it enters, and WCAG 3.3.1 is answered by the message rather than by\n a mark on a control that is not the problem. Omitting `errorId` on every field is now\n the way to say \"this is about the submission\", and the JSDoc says so.\n \n **A field's description was drawn and never announced.** The `FieldDescription`\n carried no id and the input carried no `aria-describedby`, so a hint about a format\n or a constraint was on the page for a sighted reader alone. The id is derived from\n the field's own `id`, which is already the one stable caller-owned string on the\n field, so the reference costs a template and cannot collide. `login-01` and\n `signup-01` already wired the pair; this was the Block that had not.\n \n **`SettingsNotifications01` had the same unlinked description.** Each event's\n `FieldDescription` was drawn under the control's visible label and referred to by\n nothing, while the section note was referred to by every read-only switch. The one\n `aria-describedby` now carries both: the event's own description and, on a read-only\n control, the note that says why it will not move. A reader who meets one without the\n other has to work out which half they are missing.\n \n Two notes for a caller. `AuthForm01` renders no `aria-invalid` on any field unless a\n field names itself, so a form that relied on every field being marked has to name the\n one that is. And `SettingsNotifications01` writes the joined pair as a template rather\n than through a `join`, which is the same reason `MultiCombobox` does: a bare\n separator literal is a string the copy gate reads as a space somebody typed.\n \n No prop is removed and no import breaks.\n- 97d9b99: A heading's size follows its level, so a page `h1` and its section titles are no longer one size\n \n **If you composed a page from Blocks, every section title on it just got smaller.**\n An `h2` section title moves from 1.875rem to 1.5rem, and above `sm` from 2.25rem to\n 1.875rem. A page `h1` is unchanged at 1.875rem, and 2.25rem above `sm`. Nothing\n about the props you pass changes, and nothing about the outline changes: the same\n `headingLevel` you already pass now decides the size as well as the tag.\n \n `DESIGN.md` gave Display two roles at once, \"section titles, the CTA banner\n heading, and every page `h1`\", and `SectionHeading` implemented that literally by\n writing one class string for all six levels. So an `h1` and an `h2` came out\n byte-identical, and a landing page of a hero plus six Blocks showed one `h1` and\n six section titles at 36 pixels, with no hierarchy between what the page claims\n and what it elaborates. The level was already on every Block as `headingLevel`\n and `childLevel()` already existed to carry it down a level, so the document\n decided where each heading sits in the outline and the visual size simply never\n followed it.\n \n **The table, and where it stops.**\n \n | level | step | value | above `sm` |\n | --- | --- | --- | --- |\n | `h1` | `text-3xl` | 1.875rem | 2.25rem |\n | `h2` | `text-2xl` | 1.5rem | 1.875rem |\n | `h3` | `text-xl` | 1.25rem | 1.5rem |\n | `h4` | `text-lg` | 1.125rem | 1.25rem |\n | `h5` | `text-lg` | 1.125rem | 1.25rem |\n | `h6` | `text-lg` | 1.125rem | 1.25rem |\n \n It is the authored scale walked down one step per level, and it floors at `h4`.\n `lg` is the deepest authored step that is not smaller than Body, which is 400 at\n 1.125rem, so a heading one step further down would render smaller than the copy it\n introduces and read as a caption. `h5` and `h6` hold at that step rather than\n wrapping, which is the same trade `childLevel()` makes when it clamps at `h6`. A\n heading at the floor is still a heading: it keeps `font-semibold`,\n `tracking-tight` and `text-balance` at every step, so weight and tracking tell it\n apart from body copy where size no longer can.\n \n **Nothing goes above `4xl`, and no scale changed.** The step above Display does\n not exist and this does not invent one, so `DESIGN.md`'s ceiling holds untouched\n and `@nanisoft/prism-tokens` is unchanged. `h1` is Display because that is the\n role the document gives it, and `h2` is the step below.\n \n **No new prop.** The alternative was a `size` on `SectionHeading` and on every\n Block that renders one, which widens the public surface across more than a\n hundred call sites for a decision the surrounding document has already made by\n choosing a level. You get the hierarchy by passing the level you were already\n passing, and a Block moved from an `h2` section into an `h3` one carries its size\n with it, which is what `headingLevel` was introduced to do. Nothing about the\n no-override-path rule changes: a consumer who wanted Display for their thesis\n still has it, at `h1`.\n \n **`Cta01` follows its level too, and this is the one Block outside\n `SectionHeading`.** It draws a centred title on a filled primary panel with\n nothing under it, so the muted description colour and `gap-4` are wrong there and\n it resolves its own heading element. It was carrying the same hardcoded size, so\n fixing the Component and not it would have left a closing banner one step above\n every other section title on the page and level with the page's own `h1`. Its\n banner moves with everything else: an `h2` closing banner is 1.5rem, and at `h1`\n it is 1.875rem.\n \n **The blast radius, measured rather than guessed.** 132 `SectionHeading` render\n sites across 115 files resolve to this table. 6 are at `h1` and are unchanged.\n 125 are at `h2` and each moves from `text-3xl sm:text-4xl` to\n `text-2xl sm:text-3xl`. 1 is at `h3` and moves to `text-xl sm:text-2xl`. That is\n the whole change, and it is deliberately not softened: a documentation site\n whose own prose hierarchy shifts is a visible change, and the point of the fix is\n that the shift is the hierarchy coming back.\n \n **A gate now holds the table, and proof it fires.**\n `scripts/check-heading-scale.mjs` reads the table out of `section.tsx`, judges\n every step in it against the `text` group in the token source rather than a list\n beside the gate, and holds it against the markdown table the Component's own\n JSDoc states, which is the documentation source the declaration build preserves\n and the corpus reads. It fails on a level with no size, on a step the token source\n does not author, on `h1` and `h2` sharing one step, on a step-down table that\n rises, on a level deeper than the page heading rendering above it, on a floor below\n Body, on the heading losing its weight, tracking or balance, on the JSDoc and the\n code disagreeing, and on `DESIGN.md` giving Display the section title as well as\n the `h1`. `scripts/__tests__/heading-scale.test.mjs` stages each of those against\n a tree the gate reads, including the table the Component actually shipped before\n this change, and asserts the gate is red on it.\n- 6dc7bb8: A hero action is a destination or your own control, and never an inert button\n \n `Hero01`, `Hero02` and `Hero03` accepted an action of `{ label }` with no\n destination, and each Block rendered that as a bare `Button`. Every one of those is\n a focusable control, announced as a button, that activates to nothing, and it sat\n where a reader looks first: the primary call to action of a marketing hero, in\n three published Blocks, so every consumer of any of them shipped a dead button\n where the most important action should have been. Each Block's JSDoc called the\n button \"inert by design\" and defended that as a Block shipping no behaviour, which\n is true and is not an answer. A Block that ships no behaviour cannot make a control\n work, so it should not render a control at all.\n \n **The defect was in the type, not in the render, so the fix is a type.** The\n rendering half had already been repaired once: `CtaLink` renders a native anchor\n and `Cta01` requires `href`, which is why the three heroes, predating both, kept\n the old shape. What survived was the third arm, and it could only be closed by\n removing it.\n \n `HeroAction`, `Hero02Action` and `Hero03Action` are now a union of two named arms\n rather than one shape with an optional `href`:\n \n - `HeroLinkAction` (and its two siblings) **requires** `href`, so a value that is\n sometimes a string and sometimes `undefined` is a compile error rather than a\n control that navigates on the renders where the address happens to be there.\n `slot` is forbidden on this arm.\n - `HeroSlotAction` (and its two siblings) **carries `slot`**, the caller's own\n control, and forbids `label`, `href`, `newTab` and `variant`. It exists because\n a server Component cannot receive an `onClick`, so the honest form of \"this does\n something\" in a composed section is a node the caller renders. This is\n `Cta01`'s `actionSlot` moved inside the row, because a hero carries an ordered\n list of up to two actions and a sibling prop cannot say which position it fills.\n `check-block-imports.mjs` already holds that a `ReactNode` slot is how a consumer\n injects an interactive child without the Block owning any state.\n \n The Block places a slot and adds no class to it, because a class it adds to a node\n it does not render is a style the caller cannot see and cannot remove, and this\n package has no override path.\n \n **Nothing else moved.** The anchor is the element the link arm always produced, at\n the same size and the same variant, and the positional default variant still\n follows the position: the first action in the row is filled and the rest are\n outlined, whether the first one is an anchor or a caller's control. The forward\n arrow still marks the row's one primary destination and now, with every action\n Prism renders being a link, the position is the whole of the test. A `slot` at the\n front of the row wears no arrow, because the control the caller drew owns its own\n marks. Each Block is still a server Component.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** `{ label: 'Coming soon' }` compiled a release ago and\n does not now, which is breaking by any ordinary reading, and this entry says so\n plainly. `major` in this repository publishes `1.0.0`, and every release to date\n has been `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent\n slot. Publishing 1.0.0 with this change in it would assert an API stability\n guarantee this library has not earned, and it would be the first release in the\n project's history to use the bump at all. The same argument carried the `Cta01`\n change at 0.6.0. The break is real and is the subject of this entry; the number it\n lands on is the line's decision and the line is pre-1.0.\n \n Migration: every action in a hero now carries either a destination or your own\n control. Where you passed a label alone and meant a link, give it the `href` it\n was always declared with. Where you meant a control that is not a link, a router's\n own `Link`, a submit button or a menu trigger, move it into `slot` as the element\n itself rather than as a label:\n \n ```tsx\n // Before\n actions={[{ label: 'Start free' }, { label: 'Sign in', href: '/sign-in' }]}\n \n // After\n actions={[\n { slot: <NextLink href=\"/start\">Start free</NextLink> },\n { label: 'Sign in', href: '/sign-in' },\n ]}\n ```\n \n Both mistakes are now compile errors, which is the point: a consumer upgrading\n finds a failed build rather than a site that silently renders nothing.\n \n **The same arm was still in seven sibling Blocks when this was written, and\n `a-block-action-is-a-destination-or-your-own-control.md` closes them.** The audit\n behind this entry named `About01`, `Careers01`, `CaseStudies01`, `Gallery01`,\n `Industries01`, `Services01` and `Showcase01` as carrying the same two-arm union with\n a rendered `Button` on the arm that has no `href`, and `PageHeader01` as rendering\n its declared `actions` as buttons with no destination at all. **Five of those nine\n were correct and four were defective, and the later entry records which is which**:\n `About01` and `Showcase01` had the union, `PageHeader01` was worse and had no\n destination member at all, and `Pricing01` and `Waitlist01` were not on the list at\n all. `Careers01`, `CaseStudies01`, `Industries01` and `Services01` render **nothing**\n on their `href`-less arm, which is the right shape for a card, and `Gallery01`'s tile\n is a real button with an `onClick` in a client Block.\n \n This entry stands as the record of the heroes. The shape every Block now takes is the\n one adopted here, and `scripts/check-block-controls.mjs` is what holds it: the earlier\n reasoning in this paragraph, that \"a law held by a type in three Blocks is three\n types\", was right about the risk and wrong about the remedy, and the remedy is a gate\n rather than a shared type.\n- 6dc7bb8: A popup that matched nothing is still an open popup, and its message is not an option\n \n Four Components said \"collapsed\" while a popup was on the page, and two of them put\n the sentence saying so inside the listbox.\n \n `aria-expanded` was reading \"are there results\" rather than \"is the popup displayed\",\n and the two came apart in exactly the state a search field exists to render: a query\n that matched nothing. `CommandPalette` and `Combobox` both drew a bordered panel\n carrying the caller's empty sentence and told the field there was nothing to reach.\n `aria-expanded` is now the open state in all four, because that is what the\n attribute is for and because closing on a non-match tells the reader their keystroke\n broke the control, which `Combobox` already argued in prose.\n \n `aria-controls` pointed at a listbox that was not rendered. `Combobox`,\n `MultiCombobox` and `CreatableCombobox` omit the listbox entirely when nothing\n matched and left the reference written anyway, so the field pointed at an id nothing\n carried. **It now points at the popup rather than at the listbox inside it**, which\n is the decision the existing axe run forced and the right one on its own terms:\n `aria-controls` is a required attribute on an expanded `combobox`, so a field that is\n expanded and carries no reference is itself a violation, and the panel is what the\n field opened either way. `CommandPalette` points at the palette itself for the same\n reason. All four write the reference only while the popup is displayed, so a closed\n field carries none.\n \n **A listbox owns `option` and `group` and nothing else.** `CommandPalette` drew its\n no-results `<p>` inside the listbox and its group headings in a bare `<div>`, so the\n message was a row the index counted and no reader could choose, and the rows under a\n heading could not say what they belonged to. `CreatableCombobox` drew its\n `<p role=\"status\">` in the same place. In all three the empty state is now a sibling\n of the listbox rather than a child of it, which is the arrangement `Combobox` and\n `MultiCombobox` already shared, and each group wrapper is a `group` named by the\n heading already drawn above it through `aria-labelledby`.\n \n `CommandPalette` gains two things to know about: the two branches carry the same box,\n so the panel does not change size or position when the last result is filtered out,\n and its listbox now appears only when there is something in it, so a caller selecting\n `[data-slot=\"command-palette-list\"]` finds nothing in the empty state. The empty\n state carries `data-slot=\"command-palette-empty-state\"` on its box and the message\n keeps `data-slot=\"command-palette-empty\"`.\n \n No prop changes and no import breaks.\n- 6dc7bb8: `Prose` gives a table a scroll container, and the two search fields stop zooming an iPhone\n \n Four defects, three of them narrow-viewport, one of them about a reader's own\n settings. A consumer sees all four.\n \n **A table in a Prose widened the page instead of scrolling.** The Component's\n JSDoc has always listed tables among the content it accepts, and the code fence\n beside them has always scrolled, but the table itself could not: `overflow`\n applies to block containers, and a `display: table` element is not one. A table\n box cannot be a scroll container, so a four-column table in a Markdown document\n pushed the whole page sideways rather than offering a scrollbar.\n \n A `<table>` passed straight to a Prose now gets `display: block`, which is what\n makes it a block container, and the same `overflow-x: auto` the code fence has.\n The grid inside now sizes to its content up to the measure rather than stretching\n to fill it, which is the trade the technique always carries and is the one worth\n knowing about before you rely on it. Collapsed borders are unaffected: the row\n groups are wrapped in an anonymous table box that still inherits\n `border-collapse`.\n \n The treatment is a child selector rather than a descendant one, on purpose. A\n descendant selector would reach a `Table` you have already put in a scroll\n container of your own and set `display: block` on the table inside it, which\n moves the caption off being a caption and stops the grid filling its wrapper. A\n table you have wrapped in your own element is left alone as it was; give that\n element `overflow-x-auto` and it is what scrolls. For a table whose markup you\n control, `Table` remains the right answer.\n \n **`SearchDialog` and `CommandPalette` zoomed the page on focus.** Both drew their\n search field at 14 pixels. iOS Safari zooms the viewport on a focused input whose\n font size is under 16 pixels and does not zoom back out, so a reader who opened\n either surface on a phone was left on a magnified page with no way off it. Both\n now carry the `text-base` then `md:text-sm` pair that `Input`, `Textarea`,\n `Combobox`, `Form` and `NumberField` already carried, which is the same 16 pixels\n at a phone's width and the same 14 everywhere else.\n \n **The bump is `minor` rather than `patch`.** A table in a Prose renders\n differently: a narrow one no longer stretches to the measure. That is a visible\n change to a published Component rather than a fix nobody notices, and a site that\n was relying on a prose table filling its column is the one thing that needs to\n look at it.\n- 6dc7bb8: A tree is one Tab stop, and no node claims to collapse\n \n `Tree` documented a mechanism it did not have and shipped a state it could not\n honour. Its JSDoc said \"a tree is one Tab stop, and the arrow keys move inside it.\n That is the ARIA pattern\" and then described the roving tabindex that makes it true,\n and no `tabIndex` appeared anywhere in the file: a forty-node rail was forty Tab\n stops. A comment in the same file argued the other way, and it was wrong on its own\n terms, because it deferred the keyboard model to \"a caller's own keyboard model\" in a\n Component that does not forward `onKeyDown` and therefore has no caller's model to\n defer to.\n \n The roving tabindex is now implemented, in the shape `ToggleGroup` already uses: the\n stop is an address rather than an index, so a caller who reorders or removes nodes\n does not strand the reader; it lands on the current address when the tree has one and\n on the first destination otherwise; and it follows the reader once the arrows move\n it, so Tab away and back returns them to where they were. The fallback and the\n current address are not two answers: a `currentHref` naming another page is ordinary,\n and reading either rule alone would give the tree two tab stops.\n \n **Nothing collapses, so nothing claims to.** Every group with children carried\n `aria-expanded=\"true\"`, hard-coded, which promises a second press that folds nothing\n away. A node that cannot expand omits the attribute, and `aria-level` is what places\n a node in the tree for a reader who wants that instead.\n \n **Two regions announced their bare role name.** `SelectionToolbar` drew the words\n \"3 selected\" in its leading label and pointed at nothing, so a reader tabbing onto\n the row heard \"toolbar\" and no more; it is now named by `aria-labelledby` on the\n label, which works for the `ReactNode` label the count requires and cannot drift from\n the words on screen. `ToggleGroup` declared `aria-label` optional while its own JSDoc\n said the prop was required, so TypeScript enforced nothing and a group shipped\n unnamed in both of its roles. See the separate entry for the type change.\n \n To know before you style against it: `Tree` now writes a `tabindex` on every\n destination, so a page with two trees has two Tab stops where it had one per node.\n- 6dc7bb8: `Button` is `type=\"button\"`, the carousel leaves the caret keys, and a number field takes one\n \n **`Button` defaults `type` to `\"button\"`.** A `<button>` with no `type` is a submit\n button as far as the HTML is concerned, so the default was that every Prism `Button`\n a consumer placed in their own form submitted it: a Cancel, a Close, a second step of\n a wizard, anything that was not the one control that should. The failure is silent\n and it costs data, which is why the default is stated on the Component rather than\n left to the HTML.\n \n Nothing in this repository changed, and that is worth saying with the evidence rather\n than as an assurance: every `<form>` in `packages/ui` and `apps/site` already states\n its submit button's `type`, which is why no call site moved. The one behaviour change\n is for a consumer who relied on the old default, and the migration is one token:\n \n ```tsx\n // a control that used to submit by default\n <Button>Save</Button>\n \n // now\n <Button type=\"submit\">Save</Button>\n ```\n \n The trade is deliberate. A submit button is a decision someone makes about a form and\n this Component cannot see the form, so making the destructive default the safe one\n and the deliberate act the explicit one is the only arrangement where the mistake is\n the one you have to write on purpose. `ToggleGroupItem`, `MultiCombobox` and\n `SelectionToolbar` already wrote `type=\"button\"` by hand for the same reason.\n \n **`Carousel` answered the arrow keys for whatever was inside a slide.** The key\n handler sits on the region, so every key that bubbled out of a slide reached it, and a\n slide is the caller's slot: it can hold a field, a number field, a `select` or an\n editable region, and in all four Left, Right, Home and End belong to the value. The\n Component was calling `preventDefault()` on them unconditionally, so a reader typing\n a caption moved the carousel instead. It now answers a key only when nothing inside\n is using it, which is the discipline `ResizableHandle` applies to the same keys on\n the other axis. A reader arriving on the carousel or on one of its own controls is\n unaffected.\n \n **`NumberField` documented a route to the unit that its props did not carry.** The\n drawn unit is `aria-hidden`, on the reasoning that a screen reader already says the\n unit when the caller puts it in the field's description, and there was no\n `aria-describedby` on the props to put it in, so a value of 1,250 was announced as\n 1,250. The prop is there now and is forwarded to the input. The unit stays hidden: a\n reference already says it once, and two announcements of the same word is one too\n many. The unit belongs in the description and not in the accessible name, because\n `aria-label` is optional, so a field with a real `<label>` has no name to put it in,\n and a name reading \"Parcel weight, kg\" against a visible label reading \"Parcel\n weight\" is what WCAG 2.5.3 forbids and what voice control cannot activate.\n \n `aria-describedby` is an addition, so nothing breaks. `Button` and `Carousel` change\n behaviour, which is why they are here rather than described as a patch.\n- 6dc7bb8: Coarse-pointer controls take the 44px floor, and the focus gate reads the whole package\n \n Thirteen controls were drawing targets between 16 and 36 pixels with nothing done\n about it on touch input, so a consumer composing the bare primitive shipped a\n target that fails WCAG 2.2 SC 2.5.8 and falls short of this package's own 44px\n standard. All of them are fixed, and **the desktop metrics are untouched**: a mouse\n and a trackpad see exactly what they saw before, and every change is inside\n `@media (pointer: coarse)`.\n \n `Checkbox`, `RadioGroupItem`, `Calendar`'s day cells and paging controls,\n `Dialog`'s trigger and close control, and `SearchDialog`'s close control grow to\n 44 by 44. `Slider`'s and `RangeField`'s thumbs, `Switch`, and `DatePicker`'s clear\n control take a transparent 44 by 44 band around the control instead, because\n growing them would draw something the design does not have: a 44px ball on a six\n pixel rail, a switch that is not a switch. The band is a pseudo-element, so the\n box the value is computed from is the box you see.\n \n **`NumberField` turns its two steppers side by side on a coarse pointer.** This is\n the one place the floor changes an arrangement rather than a size. Two 44px targets\n stacked in the split column need an 88px field, and two 44px bands on two 18px rows\n overlap so much that the lower stepper takes the boundary between them, so a press\n aimed at increment would step down.\n \n **`Calendar` now matches `MiniCalendar` on the same controls.** The identical\n control in one package at two target sizes is the defect, not the 32px.\n \n Three things to know before you style against these:\n \n - A `Calendar` on a phone is six rows of 44px rather than six of 36px, so the panel\n is taller. It is the densest control in the package and the one a finger is least\n able to aim at.\n - `Checkbox` and `RadioGroupItem` draw a 44px bordered box on touch input, and the\n row, column or field around them grows to hold it. That is the price\n `tag-group.tsx` already records for its chips.\n - On a `Slider` and a `RangeField`, a press inside a thumb's band drags the thumb\n from where it was rather than jumping the value to where you pressed. The arrow\n keys and the range input behind the track still reach every value. Two bounds\n closer together than the band is wide are dragged by the one used last until Tab\n moves to the other, so give a range a `minGap` a finger can pinch apart.\n \n `DESIGN.md` states both patterns and the three conditions that decide between them.\n \n **The focus-indicator gate read 132 of the package's 400 source files.** It read\n `components/ui` and nothing else, so it saw no Block, no Page, neither `live`\n surface and no provider, and it skipped any file whose JSDoc made no keyboard claim\n before reading a single class string in it. It now reads `components`, `blocks`,\n `pages`, `live` and `provider`, recursively, and judges every class string in the\n scope. It does not read `apps/site`: that is another package with its own `check`\n chain.\n \n **Its exclusion for menu and listbox options was a suffix match, and is now a\n roster.** `/(?:-item|-sub-trigger)$/` is a statement about spelling rather than\n about menus, and over the Blocks it swallowed `radio-group-item`,\n `toggle-group-item`, `accordion-item`, `breadcrumb-item`, `tree-item` and about\n thirty more, every one of which is an ordinary focusable control.\n \n **Two scanning defects are fixed, and both were making the gate report green over\n a file it had not read.** A block comment inside a `cn()` call held an apostrophe,\n which the scanner read as an opening quote and which hid every class after it; and\n a module-level `const` was sliced to the end of its file rather than to the end of\n its value, so a menu item read as carrying a full-strength ring that belonged to a\n component further down the same module. Both are proved by fixtures that fail\n without the fix. The shipped tree has no genuine findings; a planted one fires in a\n Block and in a file whose JSDoc makes no claim, which is the negative control that\n makes the clean run mean something.\n \n **The bump is `minor` rather than `patch`.** No prop, import or rendered element\n changes, and no desktop metric changes, so nothing breaks a call site. It is\n `minor` because thirteen controls now measure differently on a coarse pointer, and a\n consumer who styles any of them has to know that: a `Calendar` panel is taller, a\n `Checkbox` box and a `RadioGroup` column are bigger, and a `NumberField` lays its\n steppers out differently. That is a visible metric change on the platform most of\n these controls are used on, and describing it as a patch would describe something\n nobody sees.\n- 6dc7bb8: `DocsShell` lays out three columns from `lg` rather than two\n \n The documentation frame's third track named a screen the emitted theme closes, so\n the class compiled to nothing and the contents rail fell into an implicit `auto`\n track: two columns at every width from 1024 pixels, with the article column about\n 340 pixels wide at 1024, on this site and in all four consumers. The rail is\n 15rem, the document takes the rest, and the contents rail is 13rem, all from `lg`,\n which is where both rails already appeared. No prop, import or rendered element\n changes, and the third track still exists only when you pass `toc`.\n \n **`check-breakpoint-variants.mjs` is new, and it is a gate rather than a fix.** It\n reads every responsive variant out of the class strings in the component package\n and the site and compares it to the screens the token package emits, read from\n `layout.tokens.json` and from the build that closes `xl` and `2xl`. A class\n written against a screen the theme does not have is now a finding with a file and\n a line, and every other gate was green through the one above because each of them\n held the value rather than the class.\n- 6dc7bb8: Nineteen Blocks name their table, and the name is the heading they already draw\n \n A `<table>` is named by a caption, an `aria-label` or an `aria-labelledby`, and by\n nothing else. A heading that happens to sit above it does not name it: no assistive\n technology derives a table's accessible name from a neighbouring heading. Nineteen\n Blocks drew a real `<table>` under a caller-owned `SectionHeading` and gave the table\n no name at all, so a reader listing the tables on a page found nineteen anonymous\n entries while every element around them was named. `Table`'s own JSDoc asks for one\n of the three.\n \n A Block cannot compose the words itself, so each one takes the name from the heading\n it already draws, by reference. `SectionHeading` gains an optional `id` for the\n handle; a generated id would be no handle at all, since nothing could be written\n down to point at it. No new prop on any Block, no string shipped, and no caption\n drawn: a visible line repeating the heading is noise, and a reference cannot drift\n from the heading it names.\n \n **Three of them write the reference only while the heading is drawn.** `Compare01`,\n `PricingCompare01`, `RateCard01` and `DataTable01` render their heading only when the\n caller passed a `title`, so an unconditional reference would point at an element that\n is not on the page, which is the defect this same release removed from\n `CommandPalette`'s listbox. In the one state where a caller passed no title the table\n is therefore unnamed, which is the honest trade and the caller's own choice: the\n alternative is a reference to nothing.\n \n `DataTable01` also has a `caption` prop, and where a caller passes one the caption\n wins, because a reference outranks a `<caption>` in the accessible name algorithm.\n Writing both would quietly make the heading the name and the caller's own words the\n thing only read on request. `Gantt01` already captioned its table and is unchanged.\n \n The rule is held by a test rather than a gate, and the reason is in that test's\n header: whether a table is named is a runtime property, because the reference has to\n resolve in every state a caller can reach. A source scan would only prove the\n attribute is written, and this repository does not accept a report-only gate.\n \n No import breaks and no prop is removed.\n- 6dc7bb8: `OverflowActions` and `ResizableHandle` read layout when it can have changed\n \n Neither Component's rendered output changes for a caller who composes them the way\n their own documentation says. What changes is how often each of them asks the browser\n to settle layout, and a table of rows or a page of dragged panes pays that on the main\n thread in the middle of a reader's work.\n \n `OverflowActions` measured itself after every render of every row, reading the row's\n own box, the cap and every drawn action, and writing the answer back as state. **A\n pass now runs on mount, when the actions' ids or their labels change, when `className`\n changes, when the row's own box changes, and when the page's fonts have finished\n loading.** Passing a fresh `actions` array on every render now costs nothing, which is\n the ordinary case in a table cell under a filter box. **The fonts are in the list\n because a self-hosted face swaps in after the first paint and changes the width of\n every drawn action without changing the row's width**, so nothing else reported it, and\n a row measured in the fallback face was a row whose widths were never true of the page\n the reader was looking at.\n \n `ResizableHandle` read the group's box on every `pointermove`, immediately after the\n frame before had written the new position and therefore written new styles: one forced\n synchronous layout per frame of every drag. **The box is read once at `pointerdown`,\n and every frame after that is arithmetic.** A drag measures from the press rather than\n from the divider's own box, which is what a divider a pixel wide and a reader takes\n hold of wherever their pointer lands requires, and it did that before.\n \n Two cases are pinned rather than handled, and both are written down in the JSDoc and\n the site page:\n \n - A group resized by something else part way through a drag finishes that drag against\n the travel it started with. A caller who needs a drag that follows a changing group\n ends that drag and starts it again.\n - `OverflowActions` cannot see a change that leaves both the row's box and its drawn\n content exactly as they were: an ancestor's font size changing, or an action's mark\n swapped for a different-width one while that action is drawn. Both used to be\n corrected by the row's next render and neither is now.\n \n The bump is `minor` because those two are behaviour a consumer can observe, even though\n no prop, import or rendered element changes.\n- 6dc7bb8: There is one container namespace, and `max-w-6xl` no longer resolves\n \n **If you wrote `max-w-6xl`, `max-w-2xl`, `max-w-xl`, `max-w-lg`, `max-w-md`,\n `max-w-sm` or `max-w-5xl` against this package, that class now produces no rule at\n all.** Nothing errors and nothing warns: the element simply loses its cap and lays\n out at whatever its parent gives it. Grep your own repository for `max-w-` before\n you upgrade. The replacements are in the list below and every one of them resolves.\n \n The reason is the defect this release ends. `@nanisoft/prism-tokens` authors three\n container widths and Tailwind ships thirteen of its own, and until now both shipped\n in the same stylesheet. Three of Tailwind's were numerically identical to three of\n ours: its largest at 72rem beside `--container-page`, its fifth at 42rem beside\n `--container-measure`, its fourth at 36rem beside `--container-measure-narrow`.\n Nothing rendered differently, so nothing failed and every gate was green. What it\n cost was that one width had two names, and the first retune of an authored token\n would have moved every surface reaching it by one spelling and left every surface\n reaching it by the other exactly where it was. `Section`, `SiteHeader`,\n `SiteFooter`, `ChartCard01`, `DocsShell` and `QuickView01` all used the Tailwind\n spelling of the page column; `Cta01` and `SectionHeading` used the Tailwind\n spellings of the two measures.\n \n The token build now closes the whole namespace, with\n `--container-*: initial` ahead of the authored entries rather than a list of the\n seven steps that happened to be emitted. A wildcard is the load-bearing word: the\n thirteen names it retires are `3xs 2xs xs sm md lg xl 2xl 3xl 4xl 5xl 6xl 7xl`,\n and a list would have been a second list to keep in step with a dependency's\n default theme, which is the event the line exists to survive. Order matters and is\n asserted: Tailwind resolves a theme in source order, and the same declaration\n written after the authored entries clears all eight of ours and ships no container\n at all.\n \n **What to write instead**\n \n | You wrote | Write | Value |\n | --- | --- | --- |\n | `max-w-6xl` | `max-w-page` | 72rem |\n | `max-w-2xl` | `max-w-measure` | 42rem |\n | `max-w-xl` | `max-w-measure-narrow` | 36rem |\n | `max-w-md` on a dialog or an alert | `max-w-overlay-dialog` | 28rem |\n | `max-w-lg` on a form dialog | `max-w-overlay-form` | 32rem |\n | `max-w-xl` on a command palette or a search | `max-w-overlay-palette` | 36rem |\n | `max-w-sm` on a side sheet or a drawer | `max-w-overlay-panel` | 24rem |\n | `max-w-5xl` on a lightbox | `max-w-overlay-media` | 64rem |\n \n **An overlay's width is a property of the kind of surface it is, not of the page\n grid underneath it**, and that is why the five overlay widths are authored rather\n than carried on a Tailwind name. A retune of the reading measure must not move a\n dialog and a retune of the page column must not move either, so the two families\n are named apart inside the one container group. They share a group because\n Tailwind 4's `max-w-*` resolves `--spacing-*` and `--container-*` and nothing\n else, so a maximum width that is to be a token has to live in one of those two\n namespaces or it is a value written in a Component. `overlay-palette` and\n `measure-narrow` are both 36rem; that is two measurements that happen to agree and\n not one measurement with two names, and the token source says so and the emitted\n contract holds it to saying so.\n \n **Nothing is supposed to change where you can see it.** Every migration above\n preserves the exact rendered width, so a page that only consumed this stylesheet\n looks the same before and after. What changes is the next retune: it now moves one\n surface rather than half of them.\n \n **A width that was arithmetic on the spacing base is left alone, deliberately.**\n `max-w-96`, `max-w-72`, `max-w-64`, `max-w-32` and `max-w-20` resolve to\n `calc(var(--spacing) * N)` and never touched the container namespace at all. They\n are how a chart's axis band, a token table's column and a documentation frame are\n sized, where nobody took a decision and naming one would invent it. They stay.\n \n **Two gates now hold this down, because the question has two sides.**\n `scripts/check-elevation-layout.mjs` reads every `w-*` and `max-w-*` name out of\n the class strings and holds it to the container group read out of the token source,\n so a width nobody authored is a finding with a file and a line; it reads\n `apps/site/items` as well as `apps/site/src` and `packages/ui/src`, because that is\n where the documentation Demos live and where the site's own build scans.\n `packages/ui/scripts/check-container-namespace.mjs` reads the built stylesheet and\n holds the artefact: that the theme block declares exactly the authored containers,\n that none of Tailwind's thirteen steps is declared or read, that the close is\n written ahead of the entries rather than after them, and that every container width\n this package writes is emitted as a utility reading its own variable. It reads the\n step list out of the installed `tailwindcss/theme.css` rather than restating it, so\n a dependency that adds a step is covered without an edit. Tailwind's extractor\n reads this package's comments as well as its code, so a retired class name written\n down in a JSDoc block used to emit a utility; there is now a rule about that too.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** A class a consumer wrote stops producing a rule with no\n error, which is breaking by any ordinary reading, and this entry says so plainly.\n `major` in this repository publishes `1.0.0`, and every release to date has been\n `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent slot.\n Publishing 1.0.0 with this change in it would assert an API stability guarantee\n this library has not earned, and it would be the first release in the project's\n history to use the bump at all. The break is real and is the subject of this\n entry; the number it lands on is the line's decision and the line is pre-1.0.\n \n **One collateral repair on the documentation site, recorded here because it is\n why `globals.css` moved.** The site's own build and this package's meet in one\n `utilities` layer, and this package emits a bare `.grid-cols-6` as the base of the\n `lg:` variant four of its Blocks use. That bare rule landed after the site's\n `sm:grid-cols-11`, so every colour ramp on the Foundations page was drawing six\n columns at every width above 640 against a comment beside the class saying\n eleven. Whether `check-utility-cascade.mjs` saw it depended on which order two\n content-hashed CSS chunks sorted in, which this change perturbed and which nothing\n about the tree controls. Neither side is wrong, so the site's variant is restated\n in the `site-variants` layer that already exists for six other collisions. The\n repaired behaviour is on the documentation site only; nothing in this package\n changes.\n- 6dc7bb8: Ship the interface face and its metric-adjusted fallback, and stop the site loading its own\n \n `@nanisoft/prism-ui` shipped three Inter weights and no fallback face, so a reader\n waiting on the web font saw the platform's UI face jump into place when Inter\n arrived. The stylesheet now also ships a local Arial carrying Inter's metrics\n through `size-adjust`, `ascent-override`, `descent-override` and\n `line-gap-override`, and `--font-sans` names it immediately after Inter, so the\n swap window occupies Inter's own line box. The four numbers are measured off the\n shipped face rather than chosen, and they are the ones `next/font` measured for the\n same typeface, so the line box is unchanged from what the documentation site had.\n \n A fourth face ships beside them: Inter italic at weight 400, the Latin subset of\n the same release, covering the same 230 codepoints as its upright siblings.\n `Prose` sets `blockquote` in italic and the surface had been asking for a style no\n shipped file provided, so every consumer has been reading a synthesized oblique.\n One real face covers every weight a browser asks for, because the font matcher\n takes the closest available weight rather than synthesizing once a face exists.\n The whole set is 93.5 KB, up 22.8 KB, and no consumer has to do anything.\n \n **The documentation site no longer loads a face of its own.** It shipped a 723 KiB\n unsubsetted variable Inter and its italic through `next/font/local`, preloaded both\n in all 581 exported documents, and applied the result to `<body>` as a directly set\n `--font-sans`. A directly applied custom property outranks an inherited one, so the\n library's faces were never fetched on the site that documents them: a missing or\n corrupt shipped font rendered perfectly there with every gate green. The site now\n resolves the same stack you do, which is the only way it can be evidence for the\n package. If you were relying on the site to look right while your own copy of Inter\n was broken, that is now a failure you can see.\n- 6dc7bb8: `Progress` announces a value text again, and `getAriaValueText` works\n \n Every `Progress` in every consumer was announcing a bare number. `aria-valuenow`\n carried the position and `aria-valuetext` was absent from the tree altogether, so\n the sentence a screen reader says for a bar was a number with nothing to say what\n range it was a number in. A bar with `value={null}` was worse than bare: with no\n `aria-valuenow` to fall back on, its entire value was nothing.\n \n **What changed.** `aria-valuetext` is now passed only when you pass `valueText`, so\n the default that Base UI computes survives. You get it for free:\n \n - a determinate bar announces its value as a percentage of the range, so\n `value={68}` is announced as `68%`;\n - a bar of unknown length announces `indeterminate progress`.\n \n **`getAriaValueText` was dead and is not any more.** It was called on every render\n and its return value was then discarded, so a consumer who wrote one was paying for\n it and hearing nothing. It receives the formatted percentage first and the raw\n `value` second, unchanged.\n \n **`format` and `locale` had no observable effect either, and now do.** Both exist\n only to shape the announced string, so with the attribute absent there was nothing\n for them to shape. One trap worth knowing: `format` is applied to the raw value on\n the scale, not to the share of it, so `format={{ style: 'percent' }}` on a\n zero-to-hundred range reads as `4050%`. The unformatted default is the only path\n that asks for a percentage of the fraction.\n \n **Nothing you passed changes meaning.** An explicit `valueText` still wins, an\n `aria-valuetext` written on the Component itself still wins over both, and a bar\n given both `valueText` and `getAriaValueText` still throws. What a consumer hears\n on a bar that passes neither of them is the whole of this change, and it is a\n change to every bar a consumer that passes neither already has.\n \n The bump is `minor` because it changes what a screen reader says in every\n composition of this Component. No prop, import or exported name changes.\n- 6dc7bb8: `Progress` expresses its value as a transform rather than as a width\n \n The fill was transitioned with `transition-[width]`, and `width` is a layout\n property, so every frame of every advancing bar made the browser settle layout on\n the main thread. `Progress` is one of the two or three most-composed Components in\n this package, which is what makes it worth a release of its own: the cost was not\n one bar on one page, it was every bar on every page, in every consumer that\n composes one.\n \n **The indicator is now as wide as the track at every value and is scaled along the\n reading axis.** `aria-valuenow` still comes from `ProgressRoot` and still reports\n `value`, `min` and `max`; the scale factor is the same three numbers read a second\n time, and the test suite holds the two to each other by asserting the transform\n against the announced value rather than against a copy of the arithmetic.\n \n Three things to know:\n \n - **A rule that sized `[data-slot=\"progress-indicator\"]` is now sizing a box this\n Component does not resize.** Style the track's `height`, the track's `width`, or\n the indicator's `transform` instead. The indicator's own inline `width` is `100%`\n and outranks any class, because Base UI writes a percentage width onto the\n element as an inline declaration and nothing in a class list can beat that.\n - **The fill grows from the inline start edge in both directions.** `origin-left`\n under a left-to-right `dir` and `rtl:origin-right` under a right-to-left one, so\n the bar reads from the same end it always did. It takes the `rtl:` variant rather\n than a logical property because CSS has no logical keyword for\n `transform-origin` and Tailwind's `origin` utility ships the nine physical\n positions and nothing else.\n - **At a low value the leading cap is flatter than it was.** A scaled shape's corner\n radius is scaled with it, so a bar at five percent is a short pill with a\n compressed cap rather than a five percent slice of a round one. It is the standing\n trade for a compositor animation, and it is the same one every other\n compositor-priced fill in this package already makes.\n \n `value={null}` is unchanged: an unknown length renders no transform at all, so it is\n the resting state it has always been rather than a zero to animate away from when\n the length becomes known.\n \n The bump is `minor` rather than `patch` because a consumer who styles the indicator\n has to move with it. No prop, import or announced value changes.\n- 6dc7bb8: Declare `react` and `react-dom` as peer dependencies\n \n `react` was already a peer dependency in fact: every module in the package imports\n it, and nothing declared it. What that cost was not a failed install but silence.\n A package with no peer range cannot be incompatible with a React version, so npm\n and pnpm had nothing to warn about, and the manifest never said which React this\n library is written for. Both are now `^19.2.0`, the range this repository builds\n and tests against, and the site already resolves. `react` and `react-dom` stay in\n `devDependencies` so the package still builds and tests locally.\n \n `react-dom` is a peer even though no module here imports it, because\n `@base-ui/react` does, for the floating elements the Dialog, the Popover, the Menu\n and the Tooltip render through. Two places already treated it as your copy rather\n than the package's, the client budget's shared runtime and the registry's implicit\n set; this is the manifest catching up with those two decisions.\n \n If your application is on React 18, install React 19. Nothing in the package uses a\n React 19 only API, but every gate, test and build in this repository runs on 19, so\n a narrower claim would not be one this repository can back.\n- 6dc7bb8: Reduced motion is now one unlayered rule in the stylesheet, and it stops everything\n \n If you have the `prefers-reduced-motion` setting on, this release changes what you\n see in a way you will notice the first time you open one of our pages.\n \n **What a reader with the setting on gets now.** No animation runs and no transition\n runs, anywhere in the component library. A dialog appears already open instead of\n fading up, a hover changes colour instantly instead of over 80ms, a disclosure\n opens at once instead of growing, a `Spinner` ring stands still instead of turning,\n and a `Timeline`'s running mark holds one opacity instead of breathing. Everything\n that is not movement is unchanged: nothing is hidden, no figure loses a state, and\n every announcement is said exactly as before.\n \n **What this replaces.** The stylesheet already carried a `prefers-reduced-motion`\n rule, and it did two things: it set `animation: none`, and it named the seven\n `prism-ambient-*` classes rather than every element. So every `transition-*` in the\n package ran at full duration, and two unbounded loops ran indefinitely: `Spinner`'s\n ring turned for as long as the work did, and a running `Timeline` step breathed for\n as long as the step did. The comment above that rule described the opposite, which\n is the defect class this repository keeps finding, so the rule and its comment are\n now the same document.\n \n **Why one rule rather than a guard at each call site.** A `motion-safe:` variant\n ADDS a rule inside `prefers-reduced-motion: no-preference`; it never removes one.\n Three Components were guarding a transition per call site and all three were\n different shapes: `Drawer` and `ImageZoom` guarded a duration as well as the\n property, `Lightbox` guarded the property and not the duration, and `RangeField`\n guarded nothing at all while animating `left`, `right` and `width`. The one place\n that decides is the stylesheet, and `scripts/check-motion.mjs` now fails on a\n `motion-safe:` or `motion-reduce:` variant on a motion utility, so a call site\n cannot quietly start answering for itself again.\n \n **What it means for a consumer.** Your own stylesheet is unaffected where it is\n qualified by a class, an id or an inline style: the rule is unlayered and universal\n rather than `!important`, so a class-qualified rule of yours outranks it and still\n wins for your own elements. What changes is the library's motion, which is the\n motion you inherited by installing it.\n \n **One Component had to be repaired before this was safe, and that is the load-bearing\n part.** A stopped transition starts nothing and so never sends `transitionend`, and\n an exit waiting on that event strands whatever it was hiding. `Toast` now asks the\n browser what is actually running on its root and hands over on the same commit when\n the answer is nothing, so a `Toast` still leaves under this rule and under your own\n `transition: none`. The overlays are Base UI's and Base UI settles every popup on\n `getAnimations()`, which resolves immediately when nothing is running.\n \n The bump is `minor` because every consumer of this package inherits the change and\n one stylesheet is the whole surface it lands on. No prop, import or exported name\n changes.\n- 6dc7bb8: `SearchDialog` is a real modal, and `Toast` dismisses when no transition runs\n \n Two accessibility defects, and a consumer sees both.\n \n **`SearchDialog` claimed modality it did not have.** It rendered `aria-modal=\"true\"`\n with no focus trap, so Tab walked out of the dialog and into the page behind it;\n it took no focus back, so a keyboard reader who opened search, pressed Escape and\n kept tabbing had lost their place entirely; its field suppressed the browser\n outline and drew no replacement, so the one control holding focus showed nothing;\n and it had no portal and no scroll lock, so the panel scrolled with the document\n and could be clipped by an ancestor.\n \n It is now composed from `Dialog`, so the focus trap, the page scroll lock, the\n portal, the dismissal on Escape and on the scrim, and the return of focus to the\n control that opened it all come from the one modal implementation this package has.\n There is one modal in the package rather than one and a claim. Its props, its\n ranking, its result list and its live region are unchanged.\n \n Two things about the composition are worth knowing:\n \n - `onClose` now fires once the dialog has finished leaving rather than at the\n moment it was asked to. The modal owns the reader's focus while it is closing,\n so handing over earlier would take the focus return with it. Unmount from\n `onClose` exactly as before.\n - The panel and the scrim are now `Dialog`'s rather than the search dialog's own.\n The scrim is a blurred `bg-background/80` wash instead of a `bg-foreground/40`\n one, and the panel is centred in the viewport rather than pinned 15 percent from\n the top. A site that styled either through its own sheet will want to know.\n \n **`Toast` dismissal depended on a transition ending.** The leave handed over on the\n end of its own opacity transition, so a global reduced-motion kill written as\n `transition: none` rather than as a `0.01ms` duration meant the event never fired,\n `onDismiss` was never called, and the toast was stranded in `data-phase=\"leaving\"`\n for good: visible, undismissable, still taking the pointer, and a live region that\n never stopped announcing. This is the one place in the system where a global\n motion kill destroyed state rather than merely removing movement, and\n `packages/ui/src/styles.css` states the invariant it broke.\n \n The leave now asks the browser what is actually running on the toast rather than\n waiting for an event a killed transition never sends. It waits for the fade when\n there is a fade, it hands over on the same commit when there is not, and\n `onDismiss` is called exactly once either way. Nothing else about the contract\n moved, and the length of the leave is still `duration-slow` from the stylesheet\n rather than a number in this package.\n \n **`Dialog` takes an optional `initialFocus`.** Left out, the panel focuses its\n first tabbable element, which is unchanged. It is named for the dialog whose first\n tabbable element is not the control the reader came for, which is what\n `SearchDialog` now uses to put the cursor in the field.\n \n **The bump is `minor` rather than `patch`, for two reasons.** `Dialog` gains a\n public prop, which is new surface rather than a correction. And `SearchDialog`'s\n panel and scrim visibly change to `Dialog`'s, which is a behaviour a consumer can\n see on their site rather than a fix they never notice. Both are additive and\n neither breaks a call site.\n- 6dc7bb8: `SiteNavbar` is opaque, and `sticky` can be turned off\n \n The bar was `bg-background/80` with a `backdrop-blur`. A backdrop filter is\n evaluated against everything painted behind the element, and this element is a\n full-viewport-width sticky bar, so on a scrolling page the browser re-sampled and\n re-blurred the backdrop on every frame of the scroll, on every page of every site\n that composes it. The bar is now `bg-background` with a `border-border` hairline,\n which is what `SiteHeader` has always shipped.\n \n **The blur was not kept behind an `@supports` check, because a capability check does\n not pay for it.** `@supports (backdrop-filter: blur(1px))` is false only in a\n browser that was not compositing anything, and true in every browser that does the\n expensive thing, so it changes no reader's cost. It would also have left a third\n outcome in the world and the worst of the three: a bar that is a flat eighty\n percent veil with unblurred text passing under it.\n \n **Opaque is also the more legible of the candidates, and that is the part a token\n can be held to.** An opaque bar makes the bar's own contrast the pair\n `muted-foreground` on `background`, in every pack and in both modes, which is the\n pair the contrast gate already checks. An eighty percent background has no token\n pair at all, because the colour under it is whatever the reader's scroll position\n has brought there and no token in this system describes it.\n \n `will-change` is not on the bar and is not proposed for it. Nothing on the bar\n animates, so a compositing hint applied at rest has no frame to be ready for and\n nothing that would release it. This package ships no `will-change` anywhere.\n \n **`sticky={false}` now works.** The prop was destructured and never reached the\n class list, so a site that passed it was given the sticky bar it had asked not to\n have. The default is unchanged and for the reason it was there: the bar is how a\n reader leaves the page they are on, and it takes that with them on exactly the long\n pages where it is needed.\n \n Two things to know if you were styling against the old bar:\n \n - A rule that set the bar's background translucency has nothing to sit on now. Set\n the colour on the page behind the bar, or leave the bar the way `SiteHeader` has\n it.\n - A rule that relied on the bar being translucent to hide a heading as it scrolled\n under will no longer hide it, which is the point.\n \n The bump is `minor` because the bar's default appearance changes on every page of\n every consumer. No prop, import or landmark changes.\n- 6dc7bb8: The elevation scale is three steps or it is nothing, and the gate now says so\n \n `SearchDialog` shipped `shadow-lg`, and `DESIGN.md` said no shipped component\n uses one. Both were true once and neither was true when the sentence was last\n read: the Component took `shadow-lg` as an override on top of the `shadow-md` its\n own `DialogContent` already draws, and the site's skip link took `focus:shadow-lg`\n under a heading that named \"the theme disclosure and mobile nav panels\" as the\n only places it was allowed.\n \n **`shadow-lg` is not an authored step, so neither of those was drawing an authored\n shadow.** The token package emits `--shadow-xs`, `--shadow-sm` and `--shadow-md`\n and nothing else, so `shadow-lg` resolved against Tailwind's own stock theme. That\n is the second source of truth this package exists to prevent, and the reason it\n was invisible is that Tailwind's `shadow-lg` is a perfectly good black-alpha\n shadow: nothing looked wrong. What was wrong is that a retune of the elevation\n scale would have moved every lifted surface in every consumer and left that panel\n exactly where it was.\n \n **`SearchDialog` now says nothing about elevation at all.** The panel is the one\n lifted element over a modal scrim, which is what `--shadow-md` is for, and\n `DialogContent` already draws it, so the override is deleted rather than\n replaced. A consumer who styles `DialogContent` keeps styling it, which was not\n true of a hardcoded `shadow-lg` sitting on top of it.\n \n **`scripts/check-elevation-layout.mjs` now fails on any `shadow-*` step the token\n source did not author**, in every root it reads, the site's own source included.\n It reads the authored set from `shadow.tokens.json` rather than from a list beside\n the gate, for the reason the width rule beside it reads the container names from\n `layout.tokens.json`: a gate whose subject is the authored scale cannot be\n answered by a second copy of it. `shadow-none` is allowed, because the absence of\n a shadow is not one more step of the scale, and `shadow-inner` is not, because it\n is a shadow nobody authored.\n \n **There is no docs-only exception any more, and that is the part worth arguing\n with.** The exemption said site apparatus is not installable surface. The site's\n own Tailwind build is a second consumer of the same token package, so \"the site\"\n is not outside the system, it is inside it twice: a site class resolving a shadow\n out of Tailwind is the second source of truth wearing the word apparatus. The\n site's skip link draws `shadow-md` now, which is what it should have drawn.\n \n The bump is `minor` rather than `patch` because a consumer who styles the search\n panel's elevation sees it move from Tailwind's `lg` to Prism's `md`, which is a\n visible change to a published surface and the right one.\n- 6dc7bb8: `RangeField` expresses its band as a transform rather than as three layout properties\n \n The band between two bounds was transitioned with `transition-[left,right,width]`.\n Base UI positions that element with a logical `inset-inline-start` and a `width`,\n so `left` and `right` were properties the browser checked on every frame in order\n to discover they had not moved, and `width` was a layout-and-paint animation on\n every frame of every drag. A range is the one control in this package whose value\n changes on every pointer move rather than on every commit, which is why the cost\n was larger here than on the Component that retired the same mechanism a release\n ago.\n \n **The band is now as wide as the track and is scaled about the inline start.**\n `aria-valuenow` still comes from Base UI and still reports each bound; the scale\n factor is the same division read a second time, and the test suite holds the two to\n each other by asserting the transform against what the two thumbs announce.\n \n Three things to know:\n \n - **A rule that sized or positioned `[data-slot=\"range-field-indicator\"]` is now\n sizing or positioning a box this Component does not resize.** Style its\n `transform`, or style the track. Its own inline `width` is `100%` and outranks any\n class, because Base UI writes a percentage width onto the element as an inline\n declaration. Its POSITION is still Base UI's, untouched: the element keeps the\n logical `inset-inline-start` that places the band's left edge on the lower bound\n under a left-to-right `dir` and on the same distance from the other end under a\n right-to-left one, so a `dir=\"rtl\"` band needs no arithmetic of its own.\n - **A partial band has a straight edge where it stops, and no rounded edge.** The\n previous fill carried `rounded-full`, and a `scaleX` scales the shape it is applied\n to, including its own corners, so a band at 40 percent drew a 3px cap squashed to\n 1.2px on one axis and left at 3px on the other. The ends of a band are the two\n round thumbs drawn on top of it, so the radius was a second, smaller circle under a\n larger one and squashing it landed on exactly the edge the reader is looking at.\n The track still carries `rounded-full` with `overflow-hidden`, so a band at full\n span has both of its ends rounded. This is the one place the shape differs from\n `Progress`, and the reason is on the Component.\n - **A vertical field scales on `scaleY` about the bottom edge**, chosen by the\n `orientation` the Component already forwards, rather than having the horizontal\n animation applied to a vertical box.\n \n `RangeField` also carried a `motion-safe:` variant on that transition, which was\n doing nothing: the variant adds a rule rather than removing one, so the unguarded\n transition ran at every setting. See the reduced-motion changeset for where the\n policy now lives.\n \n The bump is `minor` because a consumer who styles the indicator has to move with it.\n No prop, import or announced value changes.\n- 6dc7bb8: The rest of the controls take the coarse-pointer floor, and the divider explains itself\n \n Fifteen controls were drawing targets between 28 and 40 pixels with nothing done about\n it on touch input, and three more were found by going looking rather than by reading a\n list. All of them now take the 44px floor, and **the desktop metrics are untouched**: a\n mouse and a trackpad see exactly what they saw before, and every change is inside\n `@media (pointer: coarse)`.\n \n **Sixteen take a step, and the step is the default for all of them.** A popup trigger\n (`PopoverTrigger`, `DropdownMenuTrigger`, `NavigationMenuTrigger`, `MenubarTrigger`,\n `AlertDialogTrigger`), an alert dialog's confirm and dismiss pair, a page link\n (`PaginationLink` and the previous and next controls), a tab and its list, a select\n trigger and a native select, a `Toggle`, a `ToggleGroupItem` and the sidebar's toggle\n all grow to 44 tall through `pointer-coarse:h-11`. `Carousel`'s two controls, both of\n `Lightbox`'s, both of `ImageZoom`'s, the drawer's close control and `PaginationLink`'s\n `icon` size take `pointer-coarse:size-11` and are 44 by 44.\n \n **Four of them take `min-w-11` on the second axis as well, because a floor paid on one\n axis is not a floor.** A one-digit page link at 44 tall is 20 wide. An icon-only\n `Toggle` is `size-4` plus `px-2.5`. An icon-only `ToggleGroupItem` is the same. This is\n the arrangement `Button` already states and the reason it states it, and the page-link\n case is the one worth naming: `PaginationPrevious` and `PaginationNext` pass\n `size=\"default\"` and override the padding to `px-2.5`, so on a phone the word is hidden\n and the control is an icon with a 40 pixel width until `min-w-11` says otherwise.\n \n **`TabsList` grows too, and that is the half that is easy to forget.** The list is\n `h-9 p-1`, so its content box is 28 tall and a trigger grown to 44 would not fit inside\n it. It takes `pointer-coarse:h-13`, which is the trigger's 44 plus the list's own `p-1`\n on each side. Both halves are asserted in the test, because either alone leaves a\n control under the floor or a list that clips one.\n \n **Two of the fifteen are not targets and take nothing, and both are worth stating\n because each looks exactly like every other finding here.**\n \n `steps.tsx`'s marker is a `<span>` with no role, no `tabIndex` and no handler. It is not\n focusable, it is announced by nothing, and a press on it falls through to the step's own\n text. The rail is a reading structure: the `<ol>` carries `aria-label`, the `<li>`\n carries `aria-current=\"step\"`, and the connecting rule is `aria-hidden`. Growing a 32px\n disc to 44 would pay the floor on a decoration and push the step's text down the page.\n \n `pagination.tsx`'s ellipsis is a `<span aria-hidden=\"true\">`, for the same reason. The\n page links around it are the targets and they take the floor in `PaginationLink`. The\n floor is a claim about targets a finger is asked to hit, and neither of these is one.\n \n **`ResizableHandle` takes a band, and it is the only control here where a step is\n arithmetically impossible rather than merely wrong.** The panes are given\n `flexBasis: <size>%` with `flexShrink: 0`, so the two of them already sum to the group's\n whole width and the divider is what overflows it, by exactly its own one pixel. A `w-11`\n divider would overflow the group by 44, which on a phone pushes the right pane's edge off\n the screen, and making room would mean changing what `size` means.\n \n The band works because three facts hold that the three conditions in `DESIGN.md` ask\n about. It is a pseudo-element, so the box the drag measures stays the drawn box:\n `onPointerDown` reads the **group's** rect and `onPointerMove` divides by the group's\n travel, so nothing reads this element's own size at all, which is the failure `DESIGN.md`\n warns about for a `Slider` thumb and which does not apply here. It paints above both\n panes without a `z-index`, because the handle is `relative` with `z-index: auto` and the\n panes are static, and a positioned descendant paints after non-positioned siblings. And\n it is 44 by 44 rather than 44 by the handle's length, which is what keeps the overlap\n local: along the split the handle already spans the group so 44 there is free, and across\n the split it reaches 21 pixels into each pane over a 44 pixel stretch of the line rather\n than the whole height of the group.\n \n **The cost is stated at the class rather than hidden in a document.** A press within 21\n pixels of the line, over a 44 pixel stretch of it, grabs the divider rather than the\n pane, and `touch-none` travels with the band because the pseudo-element resolves its\n `touch-action` from this element. Text selection near the divider is unavailable inside\n that patch. That is the trade a resize gutter makes on every platform, and it is bounded\n to a patch rather than run the length of both panes.\n \n Three things to know before you style against these:\n \n - A tab list, a menubar, a navigation bar and a segmented toggle group are 44 tall on\n touch rather than 28 to 36. A `Tabs` panel is that much further down the page.\n - A page header's row of links, an alert dialog's footer and a carousel's control row are\n each 8 to 16 pixels taller on touch. Nothing shifts off an edge: every one of them is\n content-sized and centred rather than flush.\n - On a resizable split, a press inside 21 pixels of the divider grabs the divider. The\n arrow keys, Home and End still move it, and `aria-valuenow` still reports where it is.\n \n **Three controls the earlier list did not name, found by searching for the two patterns\n rather than reading a line number.** `Toggle`, `SelectTrigger` and `NativeSelect` were\n all drawing 32 or 36 pixel targets and all three are the same family as controls the\n earlier release had already fixed. They are included because a trigger that opens a\n popup is a button, not a text field: it has no caret, takes no typed input, and its job\n is to be pressed.\n \n **One family was found and deliberately left, and the reason is worth having in writing.**\n `Input`, `Textarea`, `Form`'s `FieldControl`, `Combobox`'s input and the draft field in\n `CreatableCombobox` all draw 36 tall on a coarse pointer. Growing them is a different\n decision from every one above rather than an omission: a text field is the one control\n whose height also sets the line box the caret sits in, it is composed into dozens of\n Blocks, and this package already carries a deliberate narrow-viewport decision on exactly\n these elements (the `text-base md:text-sm` pair that stops iOS zooming the page on focus).\n The line this release draws is **triggers and buttons take the floor; text entry does\n not, in this sweep**, and the text-entry family is a separate piece of work rather than a\n gap in this one.\n \n **The bump is `minor` rather than `patch`.** No prop, import or rendered element changes,\n and no desktop metric changes, so nothing breaks a call site. It is `minor` because\n eighteen controls now measure differently on a coarse pointer, and a consumer who styles\n any of them has to know that: a tab list is taller, a navigation bar is taller, a\n segmented group is taller, a resizable split claims 21 pixels either side of its line.\n That is a visible metric change on the platform most of these controls are used on, and\n describing it as a patch would describe something nobody sees. This is the same judgement\n the earlier release made for the thirteen controls it fixed.\n \n `DESIGN.md` states both patterns and the three conditions that decide between them, and\n `test/coarse-pointer-floor.test.tsx` asserts the class each floor is written as. **What\n that test does not do is prove a size of 44 pixels, and the file says so in its first\n paragraph:** jsdom has no CSS engine, no cascade and no box, so `@media (pointer: coarse)`\n is never evaluated here and a coarse-pointer variant is a string that a test can only\n assert is present on the element the Component rendered. The assertions query by role and\n name rather than searching the source, so they catch a floor that was moved onto a\n wrapper or dropped in a refactor; they do not and cannot catch a browser that lays the\n element out at 30 pixels. `apps/site/e2e/display.spec.ts` over a real browser is where\n that is checked.\n- 6dc7bb8: `SiteFooter`, `DataTable01` and `RunConsole01` take a heading level\n \n Three Blocks drew a hard-coded `<h2>`. A fixed level is right for a Block at the top\n of a page and wrong everywhere else, and all three are Blocks a product opens inside\n something: a footer as the last region of a settings page, a table in a drawer, a run\n console in a panel. In each case the heading became a sibling of the heading the\n surrounding section already had, so \"skip to the next heading at or below level two\"\n walks straight past the thing the reader came to read.\n \n All three take `headingLevel?: HeadingLevel` and default to `'h2'`, which is the\n arrangement every other Block in this package already uses and which\n `childLevel()` exists to compose. Nothing rendered changes for a caller that passes\n nothing. `DataTable01`'s JSDoc had claimed the fix was needed, so this is the code\n catching up to its own documentation.\n \n **The audit behind this said `SiteFooter` was the only Block in the tree with no\n `headingLevel` prop. It was three.** The count matters because it changes the shape of\n the fix: a single instance reads as a judgement call about that Block and a class of\n three reads as a rule the package had not finished applying, which is what this\n release says.\n- 6dc7bb8: `ToggleGroup` requires the accessible name its role already needed\n \n `aria-label` was declared optional on `ToggleGroupProps` while the JSDoc directly\n above it said \"Required rather than defaulted\". TypeScript enforced nothing, so a\n group shipped unnamed, and both roles this Component draws are ones ARIA puts a MUST\n on: a reader tabbing onto an unnamed `toolbar` is told \"toolbar\" and cannot ask which\n set of controls they have reached, and an unnamed `radiogroup` says nothing about\n which question its radios are answering. `TextFormatToolbar` draws the same role with\n a required `label`, which is the shape this now matches.\n \n **This is a compile-time break, and the fix is one prop.** A `ToggleGroup` that\n passed no name now fails to build:\n \n ```tsx\n // before\n <ToggleGroup value={range} onValueChange={setRange}>\n \n // after\n <ToggleGroup aria-label=\"Date range\" value={range} onValueChange={setRange}>\n ```\n \n `aria-labelledby` still arrives through the forwarded props, so a caller whose name\n is already drawn somewhere can point at it rather than repeat it.\n \n **Why `minor` and not `major`.** The bump line for this repository is `0.y.z` and the\n package is at 0.7, so `major` would be 1.0.0 and would claim a stability promise for\n a design system whose catalogue gained 130 Items in the release before this one. The\n published precedent is in `packages/ui/CHANGELOG.md`: the `*Variant` to `*Form`\n renames and the prop removals in 0.7.0 shipped on the `minor` line, each with its\n reason in the entry. This is the same shape of change, and it is described here in\n full rather than left to be discovered by a compiler.\n\n### Patch Changes\n\n- Updated dependencies [6dc7bb8]\n- Updated dependencies [6dc7bb8]\n - @nanisoft/prism-tokens@0.15.0\n\n## 0.14.0\n\n### Minor Changes\n\n- 4e40c8f: Nine Components, and the naming law that the next thousand will follow\n \n Closes the gap in Component coverage: a `Pill` beside `tag-group`, a `Drawer`\n that is a Sheet with a gesture rather than an overlay, an `ImageZoom` that is a\n `Lightbox` with the dialog taken out, a `VideoPlayer` that owns no engine, an\n `EmojiPicker` that ships no emoji table, a `RepoStars` whose mark is a slot\n because a host logo is a licensed asset, a `ChoiceCard` built on real radios\n rather than ARIA, a `BillingSource` that has no `mask` prop on purpose, and a\n `PackSwitcher` that makes Prism's pack axis switchable at runtime, which nothing\n did before.\n \n Four of the nine ship no JavaScript at all. That is the number worth reading\n against the roster: nine catalogue Items cost 19.3 KB of the deduplicated client\n bundle rather than the sum of their rows, and `VideoPlayer` in particular is a\n server Component that a page can carry a dozen of for free.\n \n `DESIGN.md` gains the naming law this roster needed earlier: a slug says what a\n Component does, and the only number it may carry is a variant ordinal on a name\n that is already true. `button-47` tells a reader nothing; `split-button` tells\n them what it is and what happens if it changes. Five upstream-shaped items are\n refused by that law and each refusal is recorded with its reason rather than left\n to be rediscovered.\n \n The client ceiling moves from 260 KB to 280 KB, measured at 269.7 KB over 177\n entry points, and the gate's header says plainly that this is the third kind of\n number it has printed: not a correction of a bad measurement, and not a forecast\n of weight that had not landed, but a report on weight that is here. It also names\n the pattern, because five moves in a row triggered by the same event is the\n finding: a ceiling that moves once per batch of work is recording a history rather\n than measuring a policy.\n \n `Progress`, `dayKey` and the `live` subpath fix from the previous release are\n unchanged. No consumer import breaks; every name here is an addition, and the\n four `*Variant` to `*Form` renames from that release are still additive because\n nothing imported the old names.\n- 829ae83: Take the catalogue from 106 Items to 236\n \n Adds 130 Items in Prism's own vocabulary: 23 foundation Components, 96 Blocks, 10\n Pages and one live surface. Every name is an addition, so no consumer import\n breaks, and every Item follows the rules the package already held rather than\n relaxing them: no shipped copy, no fetching, no raw colour, and motion by token.\n \n The shapes that were not upstream's are the ones worth naming, because each is a\n translation rather than a copy. Upstream's storefront comparison is\n `offering-categories-01` and its spec sheet is `spec-table-01`, because what a\n buyer compares in a pipeline product is a connector's declared bounds rather than\n a plan's feature list. `tool-ledger-01` is the second live surface and takes a\n `subscribe` function the consumer supplies, so Prism still owns no socket.\n `directory-01`, `project-dashboard-01`, `ops-checklist-01` and `handoff-01` were\n added because a Block already in the roster reached for something absent.\n \n Two names move because nothing had imported them. `CalendarBlock01` is now\n `Calendar01`, and the deferred `input-otp` resolved to the published\n `one-time-code`. Four `*Variant` types are now `*Form`, since the surface gate\n rejects an exported union under a name that reads as a styling axis.\n \n `Progress` gains a string `valueText` prop for the server Block that cannot carry\n a callback across the boundary, `dayKey` is exported from `lib/utils` for the\n Blocks that share a date key, and the emitted `live` subpath now resolves its\n relative specifiers, which it did not.\n \n The client bundle is 250.4 KB against a ceiling moved from 208 KB to 260 KB. The\n move is a forecast about weight that had not landed, and the gate's own comment\n records why it differs from the two earlier corrections. `docs/history/roster-expansion.md`\n holds the decisions, and `DESIGN.md` records that the v1.1 deferred tail has\n shipped.\n- 4e40c8f: Thirty-four Components, and the audit that refused the other thousand\n \n Component coverage of the upstream catalogue is complete. 1,026 variants were\n audited against the naming law and 34 shipped: 392 were a cross-product of tone,\n size, shape, icon or state that a shipped Component already expresses as props, and\n the rest were motion refused by a law that predates this work.\n \n The zeroes are the result. `form` (85) and `select` (51) produced nothing, because\n a contact form and a settings form differ only in the data a consumer passes.\n `table` (38), `avatar` (34), `skeleton` (30), `dropdown-menu` (30) and `sheet`\n (30) produced nothing for the same reason, and `skeleton` at 30 is six groups of\n five, each one a different arrangement of the same placeholder rectangles.\n \n New: `reorderable-list`, `checklist`, `form-dialog`, `form-wizard`, `stateful-table`,\n `nested-tabs`, `mega-menu`, `task-progress`, `split-button`, `stepper`,\n `overflow-actions`, `selection-toolbar`, `money-field`, `intensity-grid`,\n `proportion-list`, `cohort-grid`, `multi-combobox`, `creatable-combobox`,\n `range-field`, `platform-modifier-key`, `image-list-field`, `repeatable-rows`,\n `text-format-toolbar`, `prompt-composer`, `lifecycle-button`, and the nine from the\n category sweep: `pack-switcher`, `pill`, `billing-source`, `drawer`, `image-zoom`,\n `video-player`, `emoji-picker`, `repo-stars`, `choice-card`.\n \n Five of the new ones exist because two Components disagree and the disagreement\n is the defect: `split-button` joins a trigger and a menu trigger into one shape,\n `stepper` clamps two controls at one bound, `overflow-actions` moves actions into\n a menu when the row runs out, `selection-toolbar` and `text-format-toolbar` take\n opposite positions on whether focus may move, and `nested-tabs` orders two tab\n axes that look identical. A boolean cannot express a conflict.\n \n **`form-dialog` is the largest single job found, at 22 upstream variants** across\n `dialog`, `alert-dialog` and `popover`. All three ship a container and none owns\n what happens between the reader pressing submit and being able to act on the\n answer.\n \n **`pack-switcher` makes the pack axis switchable at runtime**, which is the piece\n a DTCG system was missing. `video-player` owns no player engine and ships no\n JavaScript. `money-field` holds a number for the caller and a locale-formatted\n string for the reader at the same time, and the round trip is the Component.\n \n Four defects the audit found in work already merged are fixed in this release, and\n each was found by a job that did not exist to find it: `button` never had a\n loading state and two documents claimed it did; `cohort-grid` documented a scale\n knob that cannot be moved because a retention row's first column is a hundred by\n definition; `check-item-docs` could not see a generic Component, so a module with\n a full JSDoc block read as declaring no Item rather than as undocumented; and a\n byte order mark on an MDX makes its frontmatter parse as empty with no gate\n noticing, which is what `scripts/check-encoding.mjs` now exists to prevent.\n \n The catalogue is 270 Items and the client bundle is 286.2 KB, inside the 300 KB\n ceiling without moving it, because nine of the thirteen newest compose Components\n the bundle already carries. 34 Components across two passes cost 47.6 KB of\n aggregate where the sum of their individual measurements is several times that.\n \n No consumer import breaks. Every name added is an addition.\n- 4e40c8f: The component sweep: 21 Components from 555 audited upstream variants\n \n The naming law in DESIGN.md says a slug describes the job and a variant that\n differs from its siblings only by a prop is a call site. Applied strictly to 555\n upstream variants, 21 survived: 340 were a cross-product of tone, size, icon and\n state that existing Components already express as props, and 122 were motion\n refused by a law that predates this work rather than by a new one. The two zeroes\n are the finding: `form` at 85 and `select` at 51 produced nothing, because a\n contact form and a settings form differ only in the data a consumer passes.\n \n New: `pack-switcher`, which makes the pack axis switchable at runtime and is the\n piece a DTCG system was missing; `pill`, `drawer`, `image-zoom`, `video-player`,\n `emoji-picker`, `repo-stars`, `choice-card`, `billing-source`; `intensity-grid`,\n `proportion-list`, `cohort-grid`; `multi-combobox`, `creatable-combobox`,\n `range-field`, `platform-modifier-key`; `image-list-field`, `repeatable-rows`,\n `text-format-toolbar`, `prompt-composer`, `lifecycle-button`.\n \n Four ship no JavaScript. The catalogue is 257 Items and the client bundle is 278.5\n KB.\n \n **Three defects the audit found in work already merged.** `button` never had a\n loading state and the catalogue and its MDX both said it did; the claim is\n corrected and `lifecycle-button` is now where an outcome after a press lives.\n `cohort-grid` documented `max` as the way to narrow its scale, which is impossible\n because a retention row's first column is a hundred by definition, so the knob is\n `min` and the limit is now stated. And `check-item-docs` could not see a generic\n Component, so a module with a full JSDoc block was reported as declaring no Item\n rather than as undocumented; the pattern is widened, which widens the law's reach\n rather than relaxing it.\n \n **`scripts/check-encoding.mjs` is new.** A byte order mark on an MDX makes its\n frontmatter parse as empty, and no existing gate noticed: the file had a JSDoc\n module, a Demo beside it, all-props strings, and a clean catalogue join. It\n surfaced as seventeen unrelated failures in two files. The class has now bitten\n this repository twice. The gate reports and does not repair, and it is proven to\n fire.\n \n **The client ceiling moves to 300 KB.** It has now moved six times and every move\n was triggered by a batch of new Items, which the gate's header records as the\n finding rather than hiding. A roster-derived ceiling was rejected because a\n ceiling that scales with the catalogue is the same as no ceiling. Deciding one\n fixed number on what a consumer can actually load is a product judgement about\n the four downstream repositories and is the one open decision here.\n \n No consumer import breaks. Every name added is an addition.\n\n## 0.13.0\n\n### Minor Changes\n\n- 66787b0: Add `SiteNavbar`, the site bar with the controls a reader needs on every page\n \n `SiteHeader` is a lockup, a product switcher and a slot. It is not a search box, a\n menu of the family's sites, a colour chooser, a light and dark control, or a\n navigation that survives the width at which a horizontal row stops fitting, and all\n five NaniSoft sites had written the missing parts themselves: Prism's own site had\n search and a colour chooser and no way to reach its four siblings, and each of the\n four had a row of five product marks where a menu belongs and no search, no mode\n control and no navigation at all below its own row's threshold.\n \n `SiteNavbar` is that bar. `search`, `sites`, `theme`, `mode` and `nav` are each\n optional, so a site that owns none of the four controls renders the whole bar as a\n server Component and ships no client JavaScript for it. Every string is a prop: the\n Block ships no site name, no default navigation, no \"Search\" and no list of packs or\n sites.\n \n ```tsx\n import { SiteNavbar } from '@nanisoft/prism-ui/blocks/site-navbar'\n \n <SiteNavbar\n product={{ id: 'nexus', name: 'Nexus', pack: 'lavender' }}\n defaultPack=\"lavender\"\n defaultMode=\"dark\"\n navLabel=\"Site\"\n mobileLabels={{ open: 'Open menu', close: 'Close menu' }}\n nav={NAV}\n currentSiteId=\"nexus\"\n sitesLabel=\"The family\"\n sites={SITES}\n search={{ indexUrl: '/api/search', label: 'Search documentation', messages: COPY }}\n mode={{ lightLabel: 'Switch to dark mode', darkLabel: 'Switch to light mode' }}\n />\n ```\n \n `currentPath` is the alternative to marking `current` on each link, for a site that\n composes the bar once in a root layout and is therefore handed no pathname.\n \n Also new: `SearchDialog`, the search the bar opens. It fetches a static JSON index\n on the first activation and ranks it in the browser, and the ranking is a scorer\n rather than an inverted index: every query token has to match somewhere, so a\n two-word query narrows instead of listing most of a documentation set. There is no\n stemming and no typo tolerance, which is a deliberate trade against taking a search\n dependency into a package that otherwise depends on a design system and a token\n pipeline and nothing else.\n \n `DropdownMenuItem` now takes `render`, so a menu item can be the link it navigates to\n rather than a focusable element wrapped around one. Two tab stops for one row, and a\n reader who middle-clicks a row that only looks like a link, are what that fixes.\n\n## 0.12.0\n\n### Minor Changes\n\n- 26a6fdc: Export the `Stat` and `Stats01Props` types from `stats-01`\n \n A caller that builds the `stats` array could not name the type it was building,\n so it had to infer it or annotate the array as `any`, and a rename of a field on\n `Stat` then stopped being a compile error at every call site. Both types are\n now exported from the subpath, alongside the Block.\n\n### Patch Changes\n\n- 9f3dd2a: Align `FeatureGrid01` and `Pricing01` section headings left\n \n Both Blocks rendered a centred heading over a grid of cards, and `SectionHeading`\n states that `left` is right for a section with content under it. Every other Block\n that opens with a heading already aligned left, so a page composed from Blocks put\n these two headings out of step with the rest of the page. No prop changed, so no\n call site does; a consumer relying on the centred heading was overriding a default\n rather than setting a value.\n\n## 0.11.0\n\n### Minor Changes\n\n- 3260391: A card title is a heading again, at a level the Block derives rather than guesses\n \n **The defect.** `CardTitle` renders a `div`, and that is correct: four cards in a\n grid are four titles under one section heading, and four `h2`s under one `h2` is\n four sections. But that reasoning is about the **element**, and the catalogue was\n reading it as a reason about the **outline** too. Every Block that drew a titled card\n drew it as plain text, so a page kept its `h2` and lost every heading below it. A\n feature grid of five cards was unreadable by heading navigation while looking\n identical. Found while migrating the company site, whose feature section had five\n `h4`s before the migration and five `<div>`s after.\n \n **The answer is one function, and it is the same answer in all six places.**\n `childLevel(headingLevel)` steps one level down, so a Block that draws a Section\n heading and a set of card titles puts the titles *under* the section, and the whole\n set moves together when the block is composed one level deeper than it was written\n for. A Block that draws no Section heading passes `headingLevel` directly, because\n there the card title is the one heading it renders. Both answers are right, and the\n test carries which is which rather than assuming one for all six.\n \n This is what answers the ticket's third criterion, that the same question be\n answered for *every* Item that draws a titled card. The sites are `FeatureGrid01`,\n `Pricing01`, `StackGrid01`, `AuthForm01`, `SettingsPanel01` and `AuthPage`, and the\n test that holds them is one file, because the consistency is the thing under test:\n five files with one case each would let the next Block answer differently and still\n pass, which is the failure this was filed about.\n \n **`CardTitle` gains `as`, defaulting to `div`.** The change is additive and no\n existing rendering moves. Its JSDoc previously told a caller to pass Prism's\n `Heading` inside it, and that was wrong: `Heading` is a step of the *type* scale\n whose floor is `lg`, and a card title sits at body size, so the only way to use it\n was to override the size back down, which asks a type-scale component to have no\n opinion about type. The element and the visual step are separate questions and this\n is the one that answers the element.\n \n **Two levels in one Block that had one hardcoded.** `SettingsPanel01` drew its\n group headings as a literal `<h3>`, so a panel composed under an `h4` in a document\n whose sections were `h4` announced its groups as siblings of the panel that\n introduces them. They are now one step below the panel title, so the panel's whole\n outline moves when the panel does.\n \n **`h6` holds rather than wraps.** A section already at `h6` has no child level.\n Wrapping to `h1` would put a card title *above* the section that introduces it, which\n a reader navigating by heading would meet first; holding at `h6` costs a repeated\n level, which a screen reader announces as the same depth rather than as a break in\n the outline. The clamp is asserted so a later change to the order is caught.\n \n **The proof that the test can fail.** Replacing `childLevel(headingLevel)` with a\n hardcoded `h3` in `FeatureGrid01` fails two cases, the default and the derived one,\n which is the exact defect the ticket names. A test that asserts \"is a heading\" would\n have passed that.\n- 932ce96: Add `ChartFrame` and `Sidebar`, the two token families that shipped with no consumer\n \n `chart-1` through `chart-5` and the eight `sidebar-*` roles were in the contract\n and used by nothing. These two Components are what consume them honestly.\n \n **`ChartFrame` puts the table inside the frame rather than beside it.** Marks and\n cells are two renderings of one `series` array, so drift is structurally\n impossible; a table passed as a sibling prop is a table that says last quarter.\n The table is always in the document and `table=\"visible\" | \"hidden\"` only decides\n whether it is *seen*, because `hidden` is `sr-only`: it is still announced and\n still findable in the page.\n \n **One frame means every chart in a product aligns to the same plot box.** Five\n charts each drawing their own axes is five sets of numbers that do not line up, and\n a dashboard where two charts disagree by six pixels looks broken in a way nobody\n can name. `mark=\"line\" | \"bar\"` is a prop and the marks are internal on purpose: a\n mark with no axis, no gridline and no baseline is a bar that lies, and exporting\n one for a caller to place is the exact failure the frame exists to prevent.\n \n **`Sidebar`'s collapsed state is the same rail at a smaller width, not a second\n Component.** Hover surface, focus ring, current marking and accessible name are\n all present in both states and only the words and the padding change. The name\n moves to `sr-only` rather than unmounting and the trailing count is `aria-hidden`,\n so the name is the same *string* at both widths and a reader who collapses the rail\n loses nothing.\n \n ## The measured findings behind both\n \n **The sidebar needs its own ring, and the numbers are in the tests.** `ring` on a\n `sidebar` surface measures 2.86:1 on Mint light, 2.97 on Sky, 3.13 on Peach, 3.16 on\n Lavender and 3.22 on Blush. Four of the six light-mode packs would have shipped a\n sub-3:1 focus indicator had the rail inherited the page ring, which is the\n measured justification for the whole `sidebar-ring` family existing.\n \n **The chart tokens are below 3:1 and that is why the legend is not optional.** In\n light mode `chart-2` measures 2.49:1 against `card` and `chart-3` measures 2.15:1.\n Both are exempt from the contrast gate as graphic objects, so a mark in either is\n found by position and shape rather than by colour. That is why the legend is\n mandatory, the stroke is 2px, and the table is not optional. Past about three\n series hue is the only channel left, which the documentation states as a consumer\n decision rather than hiding.\n \n **One pair in this family is unmeasured.** `sidebar-ring` against `sidebar-accent`,\n the surface a focused item sits on while hovered or selected, is 3.19:1 in the\n base pack's dark mode and 3.82 to 4.21 in the light packs. It passes everywhere\n and it is the tightest pair in the family with no row in `check-contrast.mjs`, so it\n is reported rather than papered over.\n \n ## Two gate gaps found and closed\n \n **`check-variant-ink` listed eleven foreground roles and omitted the two sidebar\n ones.** `sidebar-primary-foreground` and `sidebar-accent-foreground` are both\n required 4.5:1 rows in the contrast table, so the list was telling the `Sidebar`\n that its own required pairing was an inherited one. It is the first Component in\n the package to put a `sidebar-*` fill in a variant, so nothing had hit it. The\n alternative was to add a decoy `text-sidebar-foreground` to silence the gate, which\n would have been a false pass.\n \n **`check-focus-indicators` recognised `ring-ring` only**, so a `ring-sidebar-ring`\n would have been reported non-compliant. The `Sidebar` therefore keeps the browser's\n outline as well as drawing its ring, which is asserted by a test so it stays a\n deliberate guarantee rather than an accident. Both ring roles are now accepted.\n- 4650aff: Add `CommandPalette`, so a reader can reach any command by typing three letters\n \n Prism had a Select, a DropdownMenu and a set of Tabs, and nothing for the fastest\n route to an action that lives behind three levels of menu.\n \n ```tsx\n <CommandPalette\n open={open}\n onOpenChange={setOpen}\n label=\"Commands\"\n inputLabel=\"Search commands\"\n groups={[\n {\n id: 'appearance',\n label: 'Appearance',\n items: [\n {\n id: 'theme',\n label: 'Toggle theme',\n hint: 'Cmd K T',\n keywords: ['dark', 'light', 'colour'],\n onSelect: setTheme,\n },\n ],\n },\n ]}\n empty={{ message: (query) => `Nothing matches ${query}` }}\n />\n ```\n \n **It ranks, and that is the difference between this and a filtered menu.** Matches\n are scored by where the query falls: the start of the name beats the start of a\n word inside it, which beats a match further along, which beats a keyword-only hit.\n A palette that filters without ordering shows every command containing the query in\n declaration order, so the command the reader meant sits below one that merely\n mentions their query and they scroll.\n \n **The groups are ordered by their best match too.** Ranking only *within* a group\n leaves the premise broken, because an exact match in a late group still sits below\n a poor match in an early one. Ordering the groups by their strongest member puts\n the best answer at the top and keeps the headings, which is the whole of what a\n grouped list is for.\n \n **It is composed on the Dialog rather than beside it.** Focus trapping, Escape, the\n portal, the scroll lock and the return of focus are five behaviours that are correct\n in the Dialog and would be five chances to get one wrong here. A palette that rolled\n its own overlay would be a second answer to all five questions, and the second\n answer is the one that ships the bug.\n \n **The matched run is emphasised by weight, not by a background**, which is a\n deliberate difference from `Mark`. A Mark is right in a list of search results,\n where the match is the reason the row is there. In a palette the match is a hint\n while the label is what is being read, and a saturated background on every matched\n character fights the text it sits inside. Two surfaces, two treatments, one reason.\n \n Three behaviours are decisions rather than defaults and are asserted in the tests:\n \n - **Enter never runs a command the reader did not point at.** With nothing\n highlighted it does nothing. This is the one outcome a palette must never\n produce, and it is invisible in a screenshot because nothing on screen changes\n when it happens.\n - **The arrows wrap.** A palette is a transient surface where overshoot is common,\n and a reader who overshot should not have to press Up to come back.\n - **The highlight is clamped, not reset, as the list changes.** A reader who arrows\n down three rows and then types one more character is choosing from a list that\n moved under them, and yanking the highlight to the top discards where they were.\n \n `keywords` is what makes the palette good rather than merely present: a reader who\n has to name a command exactly already knows it exists, which defeats the surface.\n `suggest` covers the rest, because a palette that opens onto a bare list makes a\n reader type before they know what is available. `empty` takes the query so the\n sentence can be the caller's, which is the only way \"no results\" avoids shipping in\n English.\n- 932ce96: Add the data display substrate: item, scroll area, carousel, toggle and the rest\n \n Nine Items, and the one that matters most is `item`, because it is the row almost\n every list in the package hand-rolls today.\n \n **`item` is the row and not the list.** A caller writes the `ul`, the `dl` or the\n grid and composes rows into it, because the same row appears in four different\n parents and a Component that owned the list would own all four.\n \n **`scroll-area` keeps the browser scrolling and takes over only the appearance of\n the bar.** The alternative is a transform on a `div`, which looks identical in a\n screenshot and has no scroll position, so no keyboard, no `scrollIntoView` and\n nothing to announce. The custom bar is also a real accessibility obligation rather\n than a decoration: a scrollbar is a control, and a control made of `div`s is worse\n than the one the browser shipped.\n \n **`aspect-ratio` takes the ratio as a prop rather than as a class.** Prism's\n stylesheet scans only Prism's own source, so a consumer's `aspect-[4/3]` is a rule\n the shipped sheet does not contain, and the box silently collapses.\n \n **`carousel` is never the only route to its content.** Every slide stays in the\n DOM, the controls state where the reader is through a required `position` function,\n and there is a focus handoff: a control that has just been disabled leaves the tab\n order without giving up focus, so a reader who pressed \"next\" onto the last slide\n would otherwise be stranded inside the carousel.\n \n **`toggle` is pressed; a `switch` takes effect at once.** The JSDoc says so and one\n test asserts the three controls side by side, because confusing them is the usual\n failure and the confusion is a bug report rather than a compile error.\n \n **`toggle-group` changes the role, not the look.** `single` is a `radiogroup` of\n `role=\"radio\"` with `aria-checked`; `multiple` is a `toolbar` of pressed buttons\n where the arrows move the highlight without pressing it. A `group` carrying\n `aria-orientation` is an axe violation, so `group` was not implementable as the\n issue's phrasing suggested, and the toolbar is the more accurate role anyway.\n \n **`native-select` is an addition and not a rival.** The existing `Select` is Base\n UI: a `button role=\"combobox\"` with a portalled popup, not a `select` element. A\n native select is right when the platform picker beats anything this package could\n draw, and wrong the moment an option needs to be more than a string. The test\n renders both and asserts one is a `SELECT` and the other a `BUTTON`.\n \n **`button-group` draws the focus ring once around the group** and the members\n suppress their own, with the click-versus-keyboard trade stated rather than hidden.\n \n **`table-sort` announces the direction it will go next, not the one it is in**, and\n its cycle returns to unsorted in three clicks, so the caller's original order is\n reachable from the control the reader already knows.\n- 3260391: `DiagramNode` carries a subtitle and a pack, and an unlabelled `Diagram` is named by its nodes\n \n **The subtitle is drawn, not only announced.** `DiagramNode` had one line of text\n and `DiagramProps` forced `label` on the non-decorative arm, so the only channel\n to a screen reader was the label. That is a text-only channel: a sighted reader\n looking at the picture got nothing, and the drawn name and the announced label\n were two strings a caller had to keep in step by hand. The subtitle is a second\n `<text>` under the name, and a node without one emits no `<text>` at all, so an\n absent subtitle is not an empty line.\n \n **A derived name makes the drift structurally impossible rather than discouraged.**\n `DiagramProps` gains a third union arm in which `label` is absent and the name is\n built from the nodes in draw order: `Tokens (the shared language), Pipeline (the\n engine), Twins`. The arm is `label?: never` and not `label?: string`, so an unnamed\n diagram and a diagram named with an empty string cannot be confused, and a blank\n label falls back to the derived name rather than announcing an unnamed image.\n \n Relation words are deliberately **not** in a derived name. Inventing a sentence out\n of the edges would make `Diagram` write words, which this system does not do; a\n caller who needs the relations announced passes `label`, and the JSDoc says so.\n \n **`pack` is per node and typed `PackId`, not `string`.** A free-text field would\n accept a name matching no emitted rule and the mark would silently keep the pack\n above it. The closed six-pack union is the one `ProductMark` already uses. It is\n per node rather than per diagram because a `pack` is a boundary on a **mark**, and\n a mark in a diagram is a node; a pack on the whole diagram would be a boundary on\n the arrangement, which is the thing the boundary law is not for. It is set on the\n `<circle>` rather than the `<svg>` or the node's `<g>`, so it re-inks that one mark\n and moves nothing else, and a circle is a shape the pack cannot re-round.\n `'default'` is the absence of the attribute, as `ProductMark` spells it.\n \n `check-pack-boundary` now reads three boundaries and reports 0 findings.\n- 4650aff: Add `Diff`, where the bar measures how much of a line changed\n \n A conventional diff marks every changed line with the same coloured wash and a\n rail. That tells a reader a line changed and nothing about how much, so rewriting\n one identifier in a long line leaves the same mark as replacing the line outright,\n and the reader's eye, which is fast at finding saturated bands, is drawn to the\n least interesting change in the file.\n \n ```tsx\n <Diff\n lines={[\n { kind: 'context', oldNumber: 1, newNumber: 1, content: 'export function run() {' },\n { kind: 'removed', oldNumber: 2, content: ' const limit = 100', changed: [[8, 13]] },\n { kind: 'added', newNumber: 2, content: ' const limit = 250', changed: [[16, 19]] },\n ]}\n label=\"Changes to run\"\n labels={{ added: 'Added', removed: 'Removed', context: 'Unchanged' }}\n file=\"src/run.ts\"\n summary=\"1 addition, 1 deletion\"\n />\n ```\n \n **The bar is change density.** Its width is the fraction of the line that actually\n changed, so a one-character edit in a long line is a sliver and a rewritten line\n fills the gutter. The marks that used to be a wash become a measurement, and the\n eye goes where the reviewer's attention belongs.\n \n **The line numbers carry the side, not the colour.** An added line has a new number\n and no old one, a removed line the reverse. That asymmetry is structural, it is how\n every diff reader a developer has used distinguishes the two, and it does not depend\n on telling red from green. The colour is redundant on top of it, which is the right\n way round: shape carries the meaning, colour reinforces it.\n \n **The changed words are emphasised by weight, not by tint.** A diff that coloured\n them green and red would spend the two hues a reader is most likely to be unable to\n distinguish, and would also fight the line's own state colour. Weight survives\n greyscale, which is the condition any encoding here has to survive eventually.\n \n **It is a table**, because a diff is two columns of numbers beside a column of\n text, and row and column navigation then come from the semantics rather than from a\n grid of divs.\n \n `labels` is required, for the same reason a Dialog's close label is a prop: a\n shared library cannot know whether the word for this is \"added\", \"ajoute\" or\n \"hinzugefugt\". `summary` is the caller's too, and a diff of a rename has no\n additions and no deletions and is still a change, which is the case a computed\n sentence gets wrong. The counts are exposed as `data-added` and `data-removed` so\n a caller can style them without the Component shipping a sentence.\n \n A test caught a real flaw in the first draft. A minimum bar width was applied to\n every changed line to keep one visible, which meant a one-character change in a\n long line was drawn at twelve percent when the data said half a percent. A\n measurement is not allowed to overstate what it measured, so the floor now applies\n only where the density is genuinely unknown, which is a changed line the caller\n gave no ranges for.\n- 3260391: `DocsShell` refuses a page with no address, and renders a link with no words as a label\n \n **This is a behavioural change and the honest reading of it is that a previously\n rendered page now throws.** Two leaks fed it. `DocsNavGroup.href === undefined` was\n the only guard, so `href: ''` fell to the anchor arm on both arms of the union, and\n `flatten` copied the same value into `Neighbour`, so one bad row published a second\n broken link in the pager. `title: string` admitted `''` on both arms, rendering a\n link announced as \"link\" and nothing else. Nothing in the type could stop either,\n and the Component's own JSDoc claimed that it could.\n \n An anchor with an empty `href` is a control a keyboard can reach and cannot operate,\n so the fix is at the tree rather than at the rendering: one pass over both `nav` and\n `toc` at the top of `DocsShell`, before anything renders, and a **page** with a\n blank address throws, naming the tree and the entry. Only the page arm is refused. A\n group with no address has a documented rendering, and a page has nothing to render\n in place of the link, so a label there would hide a page the tree is missing rather\n than report it.\n \n **Blank counts as absent, and that is required rather than pedantic.** All three\n consumer adapters write `url: node.index?.url ?? ''` for a folder with no index, so\n on the group arm `''` is the documented \"no route\" state and must render as a label.\n \n **A second bug surfaced while fixing the first.** `containsHref` read the group's\n address directly, and `under('', currentHref)` is `currentHref.startsWith('/')`,\n which is true for every absolute address. Treating `''` as \"no address\" in the\n renderer without fixing this would have made every label-only section claim to be\n the current one at once, so it reads through the same helper the renderer uses.\n \n The rail and the pager now ask one function whether an entry is a destination, so a\n row that is a label on the rail cannot become a neighbour in the pager.\n- 932ce96: Add the feedback substrate: Spinner, Toast and the Empty State Block\n \n Three Items, and the one that changes an existing Component's behaviour is the\n `Toast`.\n \n **A pause holds the remaining time rather than restarting it.** A reader who\n hovers or focuses a toast for ten seconds gets the four seconds they had left, not\n four fresh ones, because a rest is a fact about the reader and the countdown is\n about the content. Restarting the clock on every pointer move means a toast can be\n held open indefinitely by a reader who simply rests on it, which is the opposite of\n what a pause is for.\n \n **`duration` defaults to 4000 with its reason, and `0` turns the clock off.** A\n toast that vanishes before a screen reader has finished announcing it is worse than\n no toast at all, and a toast that never leaves strands the reader. Both are\n judgements about the reader rather than about the content, which is why the\n default is stated and overridable rather than fixed.\n \n **`closeLabel` is required with no default**, and it is the only surface in the\n package where a default is wrong rather than merely unhelpful. A toast arrives on\n its own initiative, with no button the reader pressed and no context to carry an\n implication about the product's language, so four products in two languages cannot\n all be told the control says \"Close\". A default is right when the caller may\n override it; here the string is the whole of the contract and guessing it is\n guessing a product's words.\n \n **The enter and leave are three phases with the handoff on the element's own\n `transitionend`**, filtered to `opacity` and with no second clock. Two timers for\n one animation is two things to keep in step, and a mismatch shows as a toast that\n fades and never goes.\n \n **`Spinner` is a server Component.** It holds no state, runs no hook and attaches\n no handler, so the client directive would be a claim about work it does not do. Its\n JSDoc states the three-way difference from `Progress` and `Skeleton`, because\n \"reports a position\", \"has the shape of what is arriving\" and \"something is\n happening\" are three different claims and the usual failure is using the third when\n the first or second is what the reader needed. The honest reason a spinner is often\n wrong is layout shift: it hides the shape of what is coming.\n \n **`EmptyState01` requires a `reason` closed to three values**, because\n `first-run`, `no-match` and `not-permitted` want different words and different\n actions, and a consumer who cannot say which kind of empty they have writes \"No\n data\", which is the state this Block exists to end. It throws rather than shipping\n a button that lies, following the `instrument-panel-01` precedent.\n \n ## Nothing was deleted, and that is a finding\n \n The fourth part of this ticket was to remove a superseded `empty` entry. The search\n was exhaustive across `packages/tokens/src`, the emitted tokens, the catalogue, the\n registry and the whole tree, and there is no `empty` token, no `empty` Component and\n no `empty` catalogue entry to remove. It was already resolved: `DESIGN.md` records\n that the old `empty` Component \"returns as `empty-state-01`, a Block\", and the\n change note that renamed it is in the history. Nothing was invented in order to\n have something to delete.\n- 932ce96: Add the form substrate: combobox, calendar, date picker, number field and the rest\n \n Nine Items, and the one that changes an existing Component's behaviour is the\n `Combobox`.\n \n **Typing never discards a choice.** A field showing exactly the chosen label is\n not narrowing anything, so \"chosen and filtered out\" is not a reachable state: the\n answer changes only on an explicit choose or an explicit clear. A combobox that\n empties its own selection as a reader types is the usual failure, and it loses\n data silently.\n \n **`Command` is the leaf row and it ranks nothing.** `matchRange` is a prop, so\n whoever filtered the list decides where the match fell, and one command object can\n be spread onto the row with its `keywords` accepted and never rendered.\n \n **`Calendar` keeps a disabled date drawn and in the arrow path, and refuses to be\n chosen.** A date that is simply absent is indistinguishable from a grid that\n failed to render that week, and a reader paging with the arrows should not have\n the path change under them.\n \n **`DatePicker` reseeds the month on show from the chosen value**, so re-picking a\n date in another month does not silently reset the grid to the month the caller\n created the field in.\n \n **`InputGroup` puts the focus on the control and the ring on the control.** The\n frame is a `div` and is never focused, because putting the ring on a wrapper is how\n an indicator ends up around the wrong box.\n \n **`NumberField` clamps a value as it arrives, not only as it is typed**, and an\n empty field reports `null` rather than zero. Base UI clamps typing and refuses the\n steppers past a bound but lets an out-of-range `value` straight through, so the\n clamp is applied on the way in and the consequence for a controlled consumer is\n documented: `onValueChange` is the authority.\n \n **`OneTimeCode` holds the code as one value, not six.** Backspace removes a\n character from the string and the rest close up, which is the only rule for an\n empty box that is not wrong for somebody. Base UI's own field strips the label from\n the first segment on purpose, so a visually hidden `<label for>` is rendered\n instead, and its steppers are put back in the tab order because an announced\n control a keyboard cannot reach is worse than a second stop.\n \n **`Form` makes the error and the way out of it one unit.** `FormError` draws the\n message and the caller's `action` in a single live region that is in the document\n before either of them, so a server error that arrives with the form is announced\n rather than appearing silently.\n \n **`Label` keeps the required mark as decoration.** The control's own `required` is\n what is announced and what the form enforces, so a second signal that says\n something slightly different is the thing to avoid.\n \n ## The ranking is now one module, not two surfaces' private copy\n \n `locate` and `RANKS` moved to `packages/ui/src/lib/rank.ts`, and the `Combobox`\n imports them from there rather than from the `CommandPalette`. A command palette\n and a combobox that each carried their own scorer would agree for a month and then\n diverge on the case nobody thought about, and a reader would find a query that\n floats to the top in one surface and sinks in the other. The first version put the\n scorer inside the palette and the combobox reached into a composite for it, which\n compiles and typechecks and is still wrong: the next surface to need one has no\n honest module to import, and the tempting answer is to copy the one it can see.\n- 3260391: A hero action is the element you asked for, and a product row can carry its own sentence\n \n **`HeroAction` is a union, so a wrong action is a compile error.** It was one shape\n with an optional `href`, which made both likely mistakes silent. `{ label: 'Start\n free' }` compiled and rendered a primary button that went nowhere, and a hero's\n first action is almost always a link, so that is the likely one and it looks\n correct on the page. `{ label: 'Start free', href: maybeUrl }` compiled too, and\n rendered a button whenever `maybeUrl` was `undefined`, which is a runtime branch the\n type said nothing about.\n \n So the link arm **requires** `href` and the button arm **forbids** it, as\n `href?: never`. The second is the one worth having: a value that is sometimes a\n string and sometimes `undefined` is now a type error rather than a button. That is\n the ticket's \"a caller should not be able to get it wrong silently\", and it is a\n type rather than a runtime branch, which is the criterion the ticket asked for.\n \n **The forward arrow now follows the element, not the position.** It was\n `index === 0`, so an inert button was the one control in the action row wearing the\n mark that says it can be followed. A first action that is genuinely a button gets no\n arrow, and a second action gets none even when it is a link, because the arrow marks\n the row's one primary destination.\n \n The default **variant** stays positional, deliberately, and the two now move\n separately. What looks primary is a fact about the row's shape; whether it navigates\n is a fact about the element. Asserting they do not move together is the point,\n because the arrow used to follow position for both.\n \n The ticket's claim that the Block \"renders every action as a plain `<Button>`\" was\n stale, the way the Cta01 one was: `CtaLink` had already fixed the rendering. What\n was left was the half that had not been fixed, the type, plus the arrow and a test\n file, which did not exist. Eight tests assert the rendered element for each arm, the\n negative as well as the positive, the arrow on a link and its absence on a button,\n and `newTab` with its `rel`.\n \n **`ProductGrid01Product` gains an optional `detail`, and the JSDoc settles which\n level a sentence belongs at.** The documentation previously said a second line\n \"belongs above the grid in `description`, where it applies to the set\". That is true\n of a sentence about the set and false of a sentence about one product, and the\n company site had five products each carrying a sentence that was true of one and\n vacuous beside the other four. The migration folded nothing in, so three published\n sentences were dropped and the ledger recorded the loss as a catalogue gap rather\n than papering over it with a copy edit.\n \n Both levels now exist and the question to ask is written down: **does the sentence\n survive its neighbours?** A `description` would be equally true if you deleted any\n one row. A `detail` is false, or vacuous, beside the other four. A tagline is the\n shortest true thing about a member and reads the same in every row, which is what\n distinguishes it from a `detail`.\n \n A `detail` is drawn only when passed. A row that reserved the line would push every\n row below it down by one, and this is a stack of full-width rules where a ragged\n left edge is the most visible thing on the page. Six tests render both shapes,\n because a test asserting only the new field would pass on a Block that had quietly\n stopped drawing the grid-level description, which is the half every existing caller\n depends on.\n \n **Two proofs that the new tests can fail.** Reverting the arrow to `index === 0`\n fails exactly one case, the one the defect lives in. Reverting\n `childLevel(headingLevel)` to a hardcoded `h3` in the card-title work fails two.\n Neither defect would have been caught by an assertion of the shape the ticket\n described.\n- 4650aff: Add `Meter`, so a bounded measurement can show the limits it is approaching\n \n A disk at 95 percent and a test at 95 percent are the same reading until you say\n where the line is. Prism had a Progress for a task moving toward an end whose\n length is not known in advance, and nothing for a quantity that already has an\n answer against a limit the caller knows.\n \n ```tsx\n <Meter\n value={97}\n label=\"Storage used\"\n valueText=\"97 gigabytes of 100\"\n thresholds={[\n { at: 80, tone: 'warning' },\n { at: 95, tone: 'destructive' },\n ]}\n >\n <span>97 of 100 GB</span>\n </Meter>\n ```\n \n **The thresholds are the design.** A fill on its own is one number, and a number\n with no limit beside it cannot be acted on. Drawing the caller's own limits as\n notches on the track puts the boundary next to the reading, so a consumer passing\n a latency budget gets a latency budget rather than a generic bar filled to a\n similar fraction. The Component has no opinion about which numbers matter.\n \n **It stays neutral below every threshold.** It can see that a value is 40 percent\n of a maximum; it cannot see that 40 percent is a problem. The same number is\n routine on a latency budget and urgent on a disk quota, so a Component that\n coloured itself would be making a claim about the caller's product on the\n caller's behalf.\n \n **It is drawn as a hairline with ticks, not as a bar.** The Progress in this system\n is a two-pixel rounded trough, and a Meter that looked like one would be read as\n one. The difference is visible before it is read.\n \n `role=\"meter\"` rather than `progressbar`, which is the ARIA distinction the whole\n Component turns on, and assistive technology reports the two differently. It is\n not a live region: the change that moved the reading is usually the thing worth\n announcing, so a consumer that wants it announced wraps it in a `LiveRegion`.\n \n Two failures are handled rather than rendered, because neither is visible in a\n screenshot. A value past the maximum is clamped to the track instead of drawn\n past its end, and a scale whose minimum equals its maximum draws an empty track\n instead of a `NaN` width, while still measuring.\n- 932ce96: Add the overlay and menu substrate: alert dialog, context menu, menubar, sheet\n \n The eight Items that every menu-shaped surface in a product otherwise hand-rolls.\n Each is composed on the Base UI primitive rather than reimplemented, because focus\n trapping, portals, dismiss-on-outside-press and escape handling are four behaviours\n that are already correct there and four chances to get one wrong here.\n \n Each one makes a decision a consumer would otherwise make badly:\n \n - **`alert-dialog`** removes dismissal from the *type*, not just the default.\n `modal` and `disablePointerDismissal` are omitted from its props, so a caller\n cannot pass a value that re-enables outside-press dismissal on a surface whose\n subject is a decision. Escape still closes, and there is no corner X, because an X\n is a third answer to a question with two. `AlertDialogAction` is its own part,\n styled destructive by default, because the one button that must exist is that one.\n - **`context-menu`** knows which rows are commands and which are destinations.\n `ContextMenuItem` is a `div role=menuitem`; `ContextMenuLinkItem` is a native\n anchor. Two parts rather than one, so the \"open in a new tab\" case cannot be a div.\n - **`hover-card`** can never be the only route to what it shows, which is what makes\n its delay safe. The trigger is a real anchor, so a long delay costs a reader\n nothing, and `delay` is a prop because a pointer rest is a fact about the reader.\n - **`menubar`** is one Tab stop and owns the arrows inside itself, and it requires a\n `label` because nothing announces a bar until focus lands on it.\n - **`navigation-menu`** knows it holds links, so it ships no command part at all, and\n a closed group holds no links with `keepMounted` as the stated seam.\n - **`sheet`** is the Dialog with an edge. `side` is required and excludes `center`.\n All six overlay behaviours are inherited, not reimplemented, and a sheet dismisses\n on an outside press, which is the contrast with the alert dialog.\n - **`collapsible`** wires the trigger and the region with the library's own\n `aria-controls` and `aria-expanded`, and unmounts the closed panel so find-in-page\n and a screen reader see the same page the reader does.\n - **`resizable`** puts the whole behaviour on a focusable `separator` with a value,\n and remembers nothing: the position is a prop and a callback, because a design\n system cannot know a reader's panes.\n \n **`resizable` is not composed on Base UI, because Base UI 1.8.0 ships none.** The\n package's exports were enumerated and there is no `./resizable` and no `Resizable*`\n symbol, so the separator role, the focusable-divider keyboard model and the pointer\n drag are authored here. That is the one hand-rolled overlay in the batch, and it is\n confined to the three things an overlay must not reimplement, none of which a\n divider needs. It should be revisited when Base UI ships one.\n \n The focus-indicators gate test no longer pins which slot happens to sort first in\n the composite-widget bucket, nor the exact excluded total. Every additional menu in\n the package adds members to that bucket, so both were tests that failed when a\n correct Component was added. The count is now bounded from below, which is the\n invariant that holds as the tree grows.\n- e14a903: `ProcessFlow01`: a pipeline of any length, drawn in order across as many lines as it needs\n \n `ProcessRail01` holds two, three or four steps and refuses a fifth in its type, and\n that decision is not in question. A rail is a claim about a sequence on one line, and\n a rail that quietly dropped a step to fit a width would be a diagram of a process that\n is not the process. A company site states six stages and was drawing them as a grid of\n short points, where the six ordinals and the terminal label were lost: a set read\n where a sequence had been.\n \n So the repair is a second shape rather than a wider tuple. Raising the rail's ceiling\n to six would have made the fourth column unrepresentable as a type error, which is\n the property that made the original decision good.\n \n **It is one ordered list, and the layout is built around that.** A flow that wrapped\n into a list per line would be several lists, and a screen reader would announce three\n lists of two, which is precisely the set-where-a-sequence-was this Block exists to\n prevent. So the stages are one `<ol>` and the wrapping is done by the grid. That has a\n consequence worth stating: the Block never learns where a line broke, and therefore\n cannot draw anything that is only correct on the widest screen.\n \n **The ordinal is the continuity mechanism, and it is treated as one.** It runs\n continuously from `01` to the last stage and it is stated as text rather than only\n drawn, so a reader who lands on stage four hears `04` and knows it continues stage\n three at whatever width they are using. Nothing else in the Block is load-bearing for\n the sequence, and that is why nothing else needs to know where the line broke.\n \n **The thread is a line through the stages, not a box around each one.** Each stage\n draws a top border and the grid separates them by a single pixel, so a run of stages\n on one line reads as one line broken by hairline gaps and the gap between two lines is\n wider. It is the rail's technique, and it is chosen here for one reason: it holds at\n any column count, including the single column a phone gets, without the Block knowing\n anything about it. A connector drawn between a stage and its successor would need the\n column count to be right and would be wrong at every width the type does not describe.\n \n **`stages` is an array and `columns` is a prop**, which is the split the ticket asks\n for. The length is content and the grid carries it; how many sit on one line is\n layout, and a caller who has to state both has to keep them in step by hand.\n \n **A flow of fewer than two stages throws.** One stage is a label, not a sequence, and\n the line the Block draws through it would claim a sequence that is not there. The\n message names `ProcessRail01` as the answer for two.\n \n ## The proof that the tests can fail\n \n The two assertions that matter are structural rather than visual, and each was proved\n by writing the mistake it exists to catch:\n \n - An ordinal that restarts per row, which is what a per-row implementation does, fails\n two cases: the continuous-ordinal assertion and the terminal-label one, because the\n last stage is then no longer the one the label is attached to.\n - A list nested per group of three, which is the set-where-a-sequence-was failure\n arriving through the markup, fails four.\n \n An assertion that the flow \"looks like\" a sequence would have passed both.\n \n The stage names are deliberately **not** headings. The sequence is the list, and\n promoting six stages to headings would put six entries in the outline for one process,\n so the section title is the only heading this Block renders.\n- 4650aff: Add `RunConsole01`, a run as a heading, a budget and the steps that got there\n \n The surface the fourth Kind exists for, and a Block rather than a Component\n because a run console is not one thing: it is a measurement beside a sequence\n beside a stream, and a reader needs all three in the same frame to answer the\n only question they have, which is whether the run will finish and what it is\n costing.\n \n ```tsx\n <RunConsole01\n title=\"Nightly reconcile\"\n streaming={running}\n copy={{\n budgetLabel: 'Budget spent',\n budgetValue: (value, max) => `${value} of ${max} credits`,\n stepsLabel: 'Run steps',\n title: 'Run',\n waiting: 'Waiting for the first step',\n }}\n budget={{ value: 4200, max: 5000, thresholds: [{ at: 4500, tone: 'warning' }] }}\n steps={steps}\n />\n ```\n \n **It reimplements nothing.** The budget is a `Meter`, which draws the limits the\n caller named and stays neutral below all of them. The steps are a `Timeline`, which\n draws each duration to scale against the slowest one. What the Block adds is the\n arrangement, and two facts belonging to neither component: a run with no budget\n gets no budget meter, because a local run has no ceiling to be near and an empty\n meter is a measurement of nothing that is invisible because an empty meter looks\n like a meter; and the budget sits above the steps, because the first question about\n a run is whether it will finish and two columns would make the reader choose.\n \n **The live region exists only while the run is arriving.** A test caught this. The\n first draft wrapped the steps unconditionally, which left a live region on the\n page for a run that had already finished: it announces nothing on mount, but it is\n still there, and a later re-render with different steps is an unrelated change it\n will announce. Mounting it when the run starts is also the order that does not\n lose an announcement, because the region is on the page before the first event\n arrives, which is when a screen reader is listening. It wraps the steps and not the\n budget, so an append does not re-read the spending every time.\n \n **`LiveRegion` is a server Component, and that is a correction.** It shipped in\n 0.8.0 carrying `'use client'` while reading its props, holding no state, running no\n hook and taking no event handler. The directive was a claim about work the\n Component does not do, and sixteen of forty-one components in this package are\n client. A live region is announced by the browser's own mutation observer rather\n than by JavaScript, which is the reason it is a good primitive: it works in a\n server-rendered page.\n \n ## The client budget\n \n The whole-tree client bundle measures **108.1 KB** with this branch's components\n in it, against a 116 KB ceiling, and the five components here cost about **0.1 KB**.\n \n That is worth stating plainly, because the number looked very different for a\n while. An earlier draft of this branch reported that the components added 2.3 KB,\n crossed a 92 KB ceiling, and needed the ceiling raised. That measurement was taken\n against a gate whose roster read two directories, and it was wrong for the same\n reason the old ceiling was: 53 emitted client modules were never read. The roster\n has since been widened to the whole tree, the honest figure is 108 KB, and these\n components land inside the existing ceiling with about 8 KB of headroom.\n \n **No ceiling change is proposed here.** The one that was drafted has been dropped\n rather than applied, because it would have moved a number to accommodate a\n measurement that was itself measuring the wrong set of files.\n- 3260391: `StackGrid01` names the real product behind a codename, and `Cta01` states the rule it enforces\n \n **`StackPart` gains an optional `realName`.** The value goes inside the tile the\n Block already draws, so it is an additive optional field, matching `ProductGrid01`'s\n `pack` and `StatusLedger01`'s `detail`. It renders only when passed: a tile that\n reserves a line for absent content is a layout shift on the first thing a reader\n scrolls to, and because the grid is a grid, an empty line in one column would also\n knock the row's baseline out for every tile beside it. Both are asserted, the\n second by counting elements rather than by checking a class.\n \n **`Cta01`'s JSDoc told consumers the opposite of the truth.** It said a panel wants\n `default` or `secondary` and never `outline`, which was the answer *before* the\n `outline` variant was given its ink, and it contradicted the code forty lines below\n it, where the second action defaults to `outline`. A consumer reading the type would\n have been actively misled. The settled rule is the inverse on the first variant:\n `secondary` and `outline` are both fine and `default` is the one to avoid, because\n the band is `bg-primary` and `default` fills with `--primary`, so a default action\n on this band is the band's own colour against the band's own colour. Its label is\n legible, because `primary-foreground` on `primary` is a gated pair, and the control\n has no edge, which is a different defect and the reason the sentence is about the\n fill rather than the text.\n \n **A test that measured a pair it believed in.** The existing resolved-ink test\n asserted the pair the test itself thought the defaults resolve to, so it stayed green\n on a Block that had stopped asking for it. The new case renders `Cta01` with no\n explicit variant, reads the utilities off the two anchors, maps each to the token it\n names, and resolves those tokens from the emitted CSS. It asserts the resolved pair\n rather than the class string, because a class-string assertion passes on a token\n change, and it adds two things a ratio cannot see: that the ink is stated rather than\n inherited, and that the fill is one the band is not.\n \n The ticket's single lavender-dark figure understated the blast radius. The base pack\n failed in **both** modes, at 1.00:1 and 1.01:1, and seven of twelve combinations were\n affected. It is 13.59:1 or better in all twelve now.\n \n **The acceptance criterion asked for the wrong instrument.** It asked for a row in\n the token contrast gate, and that gate structurally cannot see this class of defect:\n it measures token pairs, and what failed was a component declining to use a value it\n was relying on by accident. `check-variant-ink.mjs` is the gate that holds it, and it\n already existed.\n- e14a903: The fourth Kind: `live`, a surface whose content changes without a navigation event\n \n **This is a breaking change, released as a minor: 0.11.0.** `CATALOG_KINDS` gains a\n member, so a consumer switching exhaustively over `kind` is broken. A minor is the\n right line for a breaking change from a `0.x` version, where anything may change at\n any time, and taking 1.0.0 would declare the public API stable rather than describe\n this change. `CONTRIBUTING.md` says a breaking change is a `major` bump, so this is\n the one place the convention and the record disagree; the disagreement is deliberate\n and `DESIGN.md` carries it with the reasoning.\n \n **A `live` surface is what the other three cannot express.** A Component, a Block and\n a Page are all rendered from props, and props arrive when the caller says so. A run's\n event log, a monitoring view, anything fed by a socket, changes on its own. That is\n the whole of the fourth Kind, and it is why `RunStream01` is the first client\n Component in the package: a surface that receives events owns the subscription that\n delivers them, and that is not an accident of implementation.\n \n **Prism owns the surface, the consumer owns the transport.** `subscribe` is a\n function the consumer supplies and a function it tears down. There is no socket, no\n endpoint, no retry policy, no persistence and no provider to mount, so a consumer\n that already has a connection passes it in and one that does not has not acquired a\n client runtime by importing this.\n \n **Four transcriptions were found by the compiler rather than by a reader**, which is\n the argument for the ties that already existed. `STORE_KINDS`'s `_KindsMatch`\n assertion fired on the first edit. The exhaustive `KIND_LABELS` record added\n earlier fired the next, and adding a Kind is now a compile error in both places. The\n corpus builder's `KIND_SEGMENT` produced a mirror path of `undefined` and the\n `llms` gate reported it. And the site's three copies of the Kind labels were\n collapsed into one table, so the fourth Kind is one edit rather than three.\n \n **One divergence is deliberate and named.** A `live` surface ships as\n `registry:block`, because the `type` field belongs to the shadcn registry schema,\n which has no live surface and rejects a type it does not know. The **Kind** is ours\n and is `live` in `CATALOG_KINDS`, the corpus, the MCP tools and the site's Sections.\n The registry is a derived internal artefact, never served and never an install lane,\n so the two vocabularies are allowed to differ because only one of them is ours. The\n catalogue gate carries it as a named exception keyed on the Item's own `source`, so\n the exception cannot drift from the thing it excuses, and the other 103 items are\n still compared strictly.\n \n **The Section plurality rule is gone, and two Sections is why.** It read that a\n Section holding Items is plural. `patterns` broke it from the prose side and `live`\n from the catalogue side, because `components`, `blocks` and `pages` are plural nouns\n and `live` is an adjective with no plural. A rule two of nine Sections decline is a\n convention, not a law, and a second exception would have invited a third. What is\n asserted instead is the fact underneath it: every Section is registered once, in the\n root ordering, at a segment the manifest agrees with.\n \n ## The proof that the tests can fail\n \n `RunStream01`'s fourteen tests are proved by writing the mistakes they exist to catch.\n Appending instead of sorting, which is the reconnect bug, fails the ordering cases.\n And three defects were found by the tests rather than reasoned about in advance:\n \n - **`statusLabel` rendered twice.** The heading fell back to it when no `title` was\n given, so \"Failed after 3 attempts\" was the surface's subject *and* its state. A\n status is not a heading.\n - **`initial` was a trap.** It read like the current event list and quietly seeded\n only once. It now also *reseeds* when `resubscribeKey` changes, which is what a\n second run in one surface means, and the log clears with it rather than the old\n run's events growing the new run's.\n - **A `setNewest` before its declaration**, which the run-switch test caught as a\n temporal-dead-zone error and which no type checker in this repository would.\n \n **A `live` surface is budgeted, not exempted.** A Block composes Components, so its\n own client figure restates theirs. A live surface is the only client code a consumer\n pulls in *for itself*, so it has a row: 10 KB, of which `ScrollArea` is 8.1. The\n figure is set above the measured size, because a budget below it is a wish.\n- 4650aff: Add `Timeline`, so a run of events can be read for where the time went\n \n A run's events in order answer \"what happened\" and are silent on the only other\n question anyone has about an agent run, a deploy or an import: which step was\n slow. Today that means reading every number and doing the arithmetic.\n \n ```tsx\n <Timeline\n entries={[\n { id: 'plan', title: 'Plan', duration: 900 },\n { id: 'read', title: 'Read feed', duration: 4200 },\n { id: 'edit', title: 'Edit source', duration: 1200 },\n { id: 'done', title: 'Ready' },\n ]}\n label=\"Run steps\"\n />\n ```\n \n **The bars share a left edge and are scaled against the longest step in the run.**\n That is the whole idea, and the relative scale is the deliberate choice: the shape\n of the run is the question, and a shared zero baseline would be a second axis\n nobody reads. The consequence worth stating is that the bars answer \"which step was\n slow\" and deliberately do not answer \"how long did the run take\", because only the\n caller knows whether its steps ran sequentially or overlapped.\n \n **A step with no duration draws no bar.** A run's opening event has no length, and\n a zero-width bar beside it would read as \"this was instant\" rather than \"this has\n no duration\", which are different claims and only one is true.\n \n **It is an ordered list, so the sequence and the count are in the accessibility\n tree.** The marks and spine are `aria-hidden` on the rail that holds both, and the\n entry's own words carry the state, so nothing depends on a reader distinguishing\n the marks. It is not a live region: a run still arriving is normally wrapped in a\n `LiveRegion` by the caller, because announcing the list itself would re-announce\n the whole run on every append.\n \n Three degeneracies are handled rather than rendered, and all three are invisible in\n a screenshot: a run where no step reports a duration draws no bars rather than\n dividing by nothing, a zero duration does not win the longest-step comparison and\n scale everything else away, and the spine stops at the last entry rather than\n trailing past the end of the run as though a step had not arrived.\n \n The four states are a different vocabulary from the four product-capability tiers\n a `StatusLedger01` row carries, and they are two lists about two subjects rather\n than two lists about one: a `planned` agent run is a category error and a `failed`\n capability is a category error, which is the test that tells them apart.\n\n## 0.10.2\n\n### Patch Changes\n\n- `Hero01`'s headline and supporting line take a node rather than a string\n \n `SectionHeading` has always taken a `ReactNode` for both, so a `string` here made\n this Block the odd one out rather than making it stricter. Three of the five\n product sites set the emphasised word in their own mark inside the headline, so\n the narrow type would have cost each of them the emphasis or a second component.\n \n A widening, and additive. Every string a site renders is still the site's own, and\n `check-block-copy.mjs` still holds this Block to shipping no copy of its own.\n\n## 0.10.1\n\n### Patch Changes\n\n- A drawing's data props accept a read-only array\n \n `PulseGraph`, `PulseSeries` and `Diagram` took `SomeNode[]` rather than\n `readonly SomeNode[]`, so a consumer whose content is held as `as const` had to\n copy the array at every call site to satisfy the type. Every NaniSoft product site\n holds its landing content as `as const`, so the copy was not optional: the first\n consumer to compose a running hero had to write `nodes={[...FIGURE.nodes]}` to get\n it to compile, which is a cost in exchange for nothing.\n \n The props now take a read-only array. This is a widening, so it is additive rather\n than breaking, and it is the correct shape for a prop that only reads its data: a\n Component that mutates an array a caller passed is a Component that has taken\n ownership of it, and none of these three do.\n\n## 0.10.0\n\n### Minor Changes\n\n- 75434f8: Add a running figure to the system, and the ambient cycle scale that prices it\n \n The system had one law of motion: motion is state feedback, on an 80/160/280\n millisecond scale, and there are no keyframes anywhere. That law was right and\n it held. It also meant a product page could say what its product does and show\n nothing about it, which is what happened to four sites at once.\n \n This adds the second law, and it is a separate law rather than an exception to\n the first. Feedback is a response to something the reader did. A cycle is a\n demonstration of something the system does, and no reader is waiting for it.\n The test for which one a motion is does not require taste: stop the animation\n and ask whether the figure is still true and still legible.\n \n **New Components**\n \n - `PulseGraph` draws a running system: named nodes, the relations between them, a\n marker travelling a rail, and a breathing focus. A node's `lane` says it is a\n stage in a sequence, which draws the rail; a node with no lane is a field.\n `carries` on a relation says the line is carrying something, and draws a head\n at its end so the claim survives motion being off.\n - `PulseSeries` draws a live instrument: columns rising out of a caller-named\n baseline, with a reticle crossing them once per cycle. The values are the\n caller's own and the tallest sets the scale.\n - `SignalField` is a field of marks: the atmosphere a figure is drawn over.\n Decorative by default, still unless asked to drift, and honestly named as the\n one component in the system that is texture.\n \n **`Hero01` gains the band it was missing**\n \n All four sites hand-wrote the same two-column grid, each with its own gap and its\n own breakpoint. It is now the `instrument` slot, and an action naming an `href`\n renders as a real link rather than a button that goes nowhere. The centred form\n is unchanged, so this is additive.\n \n **New tokens**\n \n `ambient` and `ambient-ease` are two closed groups measured in seconds, separate\n from `duration` because folding them in would have made one name mean both 280ms\n and 7.2s. The emitted contract asserts the closed set and fails any cycle under\n one second, because a sub-second cycle is a flicker and no value in the 80 to\n 280ms band could ever have caught that.\n \n **What this does not change**\n \n Nothing is hidden. No ambient rule sets `opacity: 0` and nothing waits for a\n script, an intersection or a timer, so every figure is complete at first paint\n and a consumer needs no exception to enable the `hidden-state` gate. Reduced\n motion is one `animation: none`, and because every resting state is the full\n form, that reader gets the same figure, still. All three are server Components,\n so this adds zero bytes of client JavaScript.\n\n### Patch Changes\n\n- cb16073: A variant that sets its own fill sets its own ink, and `outline` now says which\n \n `Button`'s and `CtaLink`'s `outline` variant carried `bg-background` with no `text-` for the life of the package. On the page ground and on a card the omission was invisible, because the ink a control inherits there is `foreground` and `card-foreground` is equal to it in all twelve pack and mode combinations, so the variant was correct everywhere the design system itself placed it and wrong everywhere else.\n \n `Cta01` draws its band as a filled `bg-primary` surface with `--primary-foreground` as the band's ink, and defaulted its second action to `outline`. The second action was therefore `--background` behind `--primary-foreground` and measured **1.00:1 to 1.10:1 in seven of the twelve combinations**: the base pack in both modes, where light mode pairs `#ffffff` with `#ffffff`, and all five pastel packs in dark mode. The button was in the document, focusable, announced, and unreadable. It was found by resolving computed values in a browser, because neither the class name nor the token contract says anything about it.\n \n The fix is `text-foreground` on the variant, which is a no-op on the page ground and on a card and correct everywhere else. Measured across all twelve after the change: lowest is 13.59:1.\n \n A consumer that passed `variant=\"secondary\"` on that action, as the product site did, saw no difference and needs no change. The default is unchanged, deliberately: `outline` reads as one filled action and one alternative, and it is now a safe default because it carries its own ink rather than because it was made to stop.\n \n Two gates hold it, and neither could have caught the original:\n \n - `check:variant-ink` fails a variant whose own fill has no `text-` utility of its own, read from the source. It reads source rather than the emitted stylesheet on purpose: Tailwind only emits a variant something uses, so the sheet is a record of what was chosen rather than of what is available. A state fill such as `hover:bg-accent` is exempt, because exempting it is the difference between a rule that finds the defect and a rule nobody can run.\n - a test resolves the pair from the emitted token CSS for all twelve combinations rather than asserting a class name, and pins the seven that used to fail by name. A test written over class names would have passed against the broken button, because the class name never changed; only the resolved pair did.\n \n `Cta01Action`'s `variant` now states which weight a panel wants and why, with the measurement, so a consumer does not have to discover it.\n- Updated dependencies [75434f8]\n - @nanisoft/prism-tokens@0.10.0\n\n## 0.9.0\n\n### Minor Changes\n\n- 93758a6: The face ships with the package, so a site writes one property for its typeface\n \n `@nanisoft/prism-ui` declared `--font-sans: Inter, ui-sans-serif, system-ui, ...` and shipped no font file. Naming a family is not shipping it: the first entry resolved to nothing and every page fell through to the platform's UI face, which is the one face a design system never means by its first choice. All four consumer sites were compensating with their own custom property and a build-time network fetch, and the company site had already shipped that way by accident.\n \n The face is here now, as three latin roman weights with the licence beside them, and the `@font-face` sources are relative to the emitted stylesheet so they resolve from a consumer's install rather than from the consumer's origin. That last point is the old line's failure exactly: it emitted the face at an absolute path, which resolved against whichever site imported it, and the face silently failed in all four repositories.\n \n A consumer's own override is now unnecessary rather than merely discouraged. `--font-sans` resolves to a face this package ships, so a site that deletes its `--font-sans` override still renders in the design system's face rather than in the platform's.\n \n Three gates hold the arrangement, and none of them is discipline:\n \n - `check:typeface.mjs` fails a token family that names no face this package ships and no family the reader's own machine provides, read from the **emitted** stylesheet rather than the source. The original defect was invisible in source: the rules shipped, the subsets were preloaded, and only the class carrying the variable was missing, so the family resolved to nothing at computed-value time with no error anywhere.\n - the same gate fails an `@font-face` source that is absolute or a web-root path, which is how the old line failed silently.\n - the build refuses to emit a stylesheet that names a face it did not copy, so a package cannot ship a sheet pointing at a file that is not in the tarball.\n \n The face is 70.7 KB and every consumer installs it whether or not it renders a page, because a subpath does not reduce install size. That is the accepted price of one package instead of two.\n \n The weight axis is absent, deliberately. The design system retired the width axis, and optical sizing, which it approximated, is the browser's initial behaviour, so a file carrying a width range is not the file this design asked for. 700 is absent for a second reason: the authored scale emits it and the shipped source renders it nowhere, and a 23.8 KB file no page renders is 23.8 KB every consumer downloads forever.\n\n## 0.8.0\n\n### Minor Changes\n\n- 86cbc20: The accessible name a control announces is now a prop, so a consumer can localise it\n \n `Pagination`, `Dialog` and `Breadcrumb` each shipped an `aria-label` that a\n consumer could not change, and `ProductSwitcher` shipped a `label` default that\n put the word \"Products\" into every consumer's navigation. A consumer could\n localise the visible text of a pagination step and not the name a screen reader\n announced for it, which left a control that looked localised and was half of it.\n \n What changed:\n \n - `Pagination` takes `label` for the region name, and each of `PaginationPrevious`\n and `PaginationNext` takes `label` beside its existing `text`, so the visible\n word and the announced name move together.\n - `Dialog` takes `closeLabel` for the built-in close control, so a localised\n dialog no longer announces \"Close\" in a product that never uses that word.\n - `Breadcrumb` takes `label` for its region name.\n - `ProductSwitcher` no longer defaults `label`. A switcher with no `label`\n renders a navigation with no accessible name, which is a real state the caller\n resolves, rather than one that is quietly wrong.\n \n Every new prop keeps the word Prism would have used as its default, so nothing\n changes for a consumer that never set one. The one behaviour that does change is\n `ProductSwitcher`'s, and it is the point: the default was a claim about a\n consumer's product that the prop existed so they would not have to make.\n- b31e444: Ship the consumer gate kit, so a cross-repository law reaches a consumer in one release\n \n The four repositories that consume this package coordinated through a prose\n contract mirrored in four files. It could not be enforced, because prose cannot\n fail, and it had already drifted: two sites that mattered held three\n implementations of one rule and one of the three rules was false.\n \n The laws are now the failure messages of gates in this package, and a consumer's\n repository holds only its own data.\n \n ```sh\n pnpm exec prism-gates # every gate prism-gates.json names\n pnpm exec prism-gates --gate=links # one gate\n pnpm exec prism-gates --json # machine-readable, for a test\n ```\n \n Configure it with a `prism-gates.json` at your repository root. A consumer holds\n its stylesheets, its pack map, its coverage floors, the destinations its own\n corpus gets wrong, and the custom properties its own build supplies. It does not\n hold the wording of a rule, because a wording held in four places is four rules\n that will disagree.\n \n The gates are `pin` (the design system is an exact version, and the token package\n is this package's dependency rather than yours), `retired-line`, `stylesheet-ownership`\n with `token-read`, `links`, `pack-boundary`, `hidden-state` and `runtime-token-read`.\n \n **This release adds the `gates` export subtree and a `prism-gates` binary, and\n adds `gates` to `files`.** It also means a consumer no longer needs to declare\n `@nanisoft/prism-tokens`: resolve it through this package, which requires it at an\n exact version, and the pair cannot be mismatched. Removing the token package from\n a consumer's `package.json` and from its `pnpm-workspace.yaml` is part of the\n adoption; the `pin` gate fails on either.\n \n The kit is outside `dist/` on purpose. It is a build-time program for another\n repository and must never enter a consumer's module graph or its bundle.\n- 86cbc20: Add `LiveRegion`, a region that announces what just changed in it\n \n Prism had no live region at all, so a result list that filtered as you type, a\n search whose count updated, and any stream that appended to the page all changed\n silently for anyone using a screen reader. The fourth Kind the design rules\n already decide, `live`, is specified to own an event log surface, and an event\n log surface is a live region, so this is the prerequisite rather than a\n convenience.\n \n ```tsx\n <LiveRegion busy={streaming} label=\"Run output\">\n {lines.map((line) => (\n <p key={line.id}>{line.text}</p>\n ))}\n </LiveRegion>\n ```\n \n Three decisions are in the Component rather than left to each consumer to make\n the same way:\n \n - **It renders nothing when it has nothing to say.** A live region that is\n always present announces every unrelated state change of its ancestors, so an\n empty region is not rendered at all rather than rendered as an empty element.\n The test is \"renders nothing\" rather than a list of the nothing values, because\n an empty string, a null state and a false condition are three routes to it and a\n list of three is a list a fourth route would miss.\n - **The politeness default is the least interruptive value still announced.** A\n run log that announces assertively interrupts a screen reader mid-sentence, and a\n consumer with a genuinely urgent event passes `politeness=\"assertive\"`.\n - **`aria-busy` is about the caller's knowledge, not about an animation.** A busy\n region is still readable, and a caller that sets it permanently has told\n assistive technology the stream never ends.\n \n The component owns the region and not the transport. A consumer owns the socket,\n the retry, the persistence and the order events arrive in, which is the same split\n the documentation Page makes with its navigation, and it is why Prism stays\n transport-agnostic and no consumer inherits a connection it did not ask for.\n- 86cbc20: Add `Mark`, so a search result can show which characters matched\n \n A command palette and a search result list both need to show why a result\n matched, and until this existed the only way to do that inside Prism was to drop a\n span with hand-written styling into a consumer's own markup, which the authoring\n contract does not permit.\n \n ```tsx\n <Mark text=\"component library\" ranges={[{ start: 0, end: 9 }]} />\n ```\n \n It takes the string and the ranges rather than pre-split nodes, because a caller\n that assembled the nodes had to compute the ranges and this Component has the\n string. The ranges are handled rather than refused: they are sorted, overlapping\n runs are merged, and a range that runs past the end of the string is clamped. Each\n is a thing a search backend does as a matter of course, and a Component that threw\n on any of them would be one a consumer has to wrap in a try, which is a worse\n failure than a highlight that stops at the last character.\n \n **It adds no semantics, and that is the decision rather than an omission.** A\n consumer marking a hit wants a visual difference, not a screen reader announcing\n \"highlighted\" between every character of a result. So it styles a `mark` and puts\n nothing else on it, and a consumer who wants the announcement writes it.\n \n The treatment is the existing warning token pair rather than a new role. A mark\n sits behind the text it marks, so it needs a ground and a foreground, and that\n pair is already measured against every surface in every pack and both modes by the\n token contrast gate. A search hit is emphasis and not a caution, and the two are\n kept apart by spending the existing token rather than by adding a role that means\n \"highlighted\" and will be re-pointed at something else within a year.\n- 86cbc20: Add `Tree`, so a file browser, a category tree and an outline have something to build from\n \n Prism had a working tree and could not reach it. The implementation existed as\n two private renderers inside the documentation Page, with its depth rule, its\n current-item marking and its rule that a group with no index renders a label.\n What was missing was that it was unreachable, and that its data type was bound to\n documentation navigation rather than being a tree's own vocabulary, so a file\n tree, a category tree, an outline and a knowledge base all had nothing to build\n from.\n \n ```tsx\n <Tree\n label=\"Documentation\"\n currentHref=\"/foundation/colors\"\n nodes={[\n { type: 'group', title: 'Foundation', items: [\n { type: 'page', title: 'Colors', href: '/foundation/colors' },\n ] },\n { type: 'divider', title: '' },\n { type: 'page', title: 'Overview', href: '/overview' },\n ]}\n />\n ```\n \n **It is navigable by the arrow keys**, which is the part a consumer cannot\n assemble for themselves. A tree is one Tab stop and the arrows move inside it.\n Composing the markup gives the Tab order of the document instead, which is every\n node in the tree and none of the arrows. The flat order is collected from the DOM\n rather than computed from the node data, so there is no second list to fall behind\n the first, and a label is skipped rather than focused and doing nothing.\n \n **A flat list renders a flat tree.** An outline is a tree of depth one, so a caller\n passes leaves and gets leaves. A Component that required nesting to express a flat\n structure would make the common case the awkward one.\n \n **A group with no index is a label, not a link.** This is the rule the\n documentation Page already reasoned at length and it is carried across rather than\n reinvented: a group with an index is a destination and renders an anchor, a group\n without one renders a span that carries no `href`, is not focusable, and cannot be\n reached by Tab. Inventing a route for it would publish an address that resolves to\n nothing. The empty case is authored too, because a tree that renders nothing is a\n control with nothing in it.\n \n The documentation Page keeps its own renderers and does not change. Its two rules\n are specific to a documentation rail, which is one shape a tree takes rather than\n the shape. Whether the Page later composes this Component is a separate decision\n this does not make.\n\n## 0.7.0\n\n### Minor Changes\n\n- 5c43520: Add `DocsShell`, a documentation page that takes a site's navigation as data\n \n `DocsShell` is a documentation screen: a navigation rail, the document, a\n contents rail, and a pager to the neighbouring pages. It replaces the unstyled\n `docs-shell` the retired line published, and it is the Page the three NaniSoft\n product sites already import by that name.\n \n The navigation is data. A tree of one page and a tree of twenty-seven render the\n same way, at any depth, and the frame is sized by what the caller passed rather\n than by a fixed arrangement, so three sites whose documentation sets differ in\n shape share one Page with no per-site fork.\n \n - `nav` and `toc` are closed unions of `page`, `group` and `divider`. Both are\n read in the order given: nothing is sorted or alphabetised, so a site that\n files its rail as a pipeline and its contents as a reference has both in one\n render.\n - The pager is derived from `nav` and `currentHref`. There is no `neighbours`\n prop, so a neighbour that is not in the tree cannot be expressed, and each\n consumer loses one derivation of its own.\n - A group with no `href` renders a `span` rather than an anchor with no\n destination. The three sites spell that case today as an empty string, and two\n of them carry a stylesheet rule that styles the result back into a label; those\n rules are now deletable.\n - A section carries no status, no badge, no count, no collapse and no sort. A\n status written into a title stays in the title as words.\n - Both rails are bounded against the viewport, because one site's tree is half\n again as tall as another's.\n \n The corpus fix that shipped with it: the props extractor read each declaration\n body twice with two depth counters that disagreed, so a Page whose `index.tsx`\n re-exported a value and a type from one sibling published every prop twice and\n swept the members of its navigation union into its own prop list. It now reads\n each body once.\n\n## 0.6.0\n\n### Minor Changes\n\n- 956fc8f: A call to action with a destination renders a link, and an icon is required only where a tile is\n \n Every primary call to action in the Block family declared an `href` its type then\n never rendered. The control was a `<button>` with no handler: the type said the\n component could navigate, the documentation implied it, and the markup could not\n do either. Sixteen of them across the family, five of them to another origin.\n \n **`CtaLink` is a new Component.** It renders a native `<a>` and carries the\n visual weight of a call to action, so assistive technology announces a link and\n the browser's own affordances all work: a status bar showing the destination, a\n context menu to copy it, middle-click to open a new tab. It is a Component in its\n own right rather than an `as` or `href` prop on `Button`, which is the answer\n `BreadcrumbLink` and `PaginationLink` already give twice in this package. It is a\n server component, so the swap costs no client JavaScript.\n \n `href` is required on `CtaLink`: an anchor without one is not a link. `newTab` is\n a declared prop and the only way to open a new browsing context, because `target`\n is not accepted, so the relationship cannot be opted out of by accident. When it\n is set, `rel` defaults to `noopener noreferrer`; a consumer that needs a\n different relationship passes `rel` and it wins. Prism does not parse the\n destination and does not decide what counts as external.\n \n **`Cta01` no longer offers an action that navigates nothing.** `Cta01Action`'s\n `href` is required, there is no optional arm and no dead-button arm, and a new\n `actionSlot` takes a control the Block does not own, such as a client router's\n link. The Block makes no guess about cross-origin. The one rendered change in\n this release is a button becoming the link its type always said it was: the link\n is styled from the same recipe as the button it replaces, at every shared variant\n and size, and a test asserts that class-for-class.\n \n **`FeatureGrid01`'s icon is required only where a tile is rendered.** The\n requirement is a union discriminated on `variant` rather than an optional prop,\n because an optional prop turns a safe-at-render condition into an unsafe one and\n permits an empty accent tile per feature. The `icon` variant is the one an\n omitted `variant` selects, so the default and the required icon live on the same\n arm; the `bare` variant renders no tile and requires no icon, and the type says\n so rather than the renderer finding out. `IconFeature` and `BareFeature` are\n exported, and the union is the type the site's API table is extracted from.\n \n **The version the four downstream site plans pin is `0.6.0`.** It is\n `@nanisoft/prism-ui@0.6.0`, and `@nanisoft/prism-tokens@0.6.0` with it, because the\n two are linked in `.changeset/config.json` and the pending\n `a-server-rendered-pack-boundary-can-be-mode-correct` changeset already declares a\n minor for both from `0.5.1`. This one declares a minor as well and so does not\n raise it.\n \n Migration: a `Cta01` action needs a destination. Where the action was a label\n alone, give it the `href` it was always declared with, or move the control into\n the new `actionSlot`. A `FeatureGrid01` in the `bare` variant can now drop the\n `icon` its type used to demand; the `icon` variant still demands one per feature.\n- d001983: A diagram is a Component, and a product's mark is one item rather than two copies\n \n Three of the four product sites draw something in the position where an empty box\n is most visible, and all of it was a canvas. A canvas reads computed style from\n one element while a pack is an attribute on an ancestor, so a scoped `data-pack`\n boundary hands it the pack's light values on a dark page, and no token gate can\n see it: the value the drawing holds is a legal token value, applied to the wrong\n pack. Cutting the canvases without a replacement is the alternative, and it\n leaves three heroes with an empty slot.\n \n `Diagram` draws named things and the labelled relations between them as inline\n vector markup. Every stroke and every fill is a semantic utility, never a\n resolved value, so a boundary restyles the drawing through the cascade exactly as\n it restyles a heading. It is a server Component: no hook, no mode, no context, no\n prop spread, and no client code, so a consumer renders it from a server file\n with no provider mounted. Its node mark is a circle and a relation is a path\n because both are shapes with no radius concept, which is what keeps a boundary\n from moving anything but colour; a `<rect>` was rejected because its corner\n attribute is a CSS property and no utility pins it. Every mark names an explicit\n stroke width, an explicit stroke and an explicit fill, because a shape left to\n its defaults either paints opaque black over its own labels or resolves its edge\n to the surface it sits on. A relation naming a node it does not have is dropped,\n and the count of dropped relations is on the element rather than swallowed.\n \n `ProductMark` promotes the two-part mark the product row and the product switcher\n each hand-wrote into one catalogue item. The ring is `brand-ink` and the core is\n `primary`, which is not a preference: a pastel brand value is a fill and never\n text, so the core may be `primary` and the name may not, and a wordmark reads\n `brand-ink` rather than `foreground` or `primary-foreground`. A hand-written copy\n gets one of those two wrong and nothing in the rendered result says which. The\n mark carries its own `data-pack` boundary, so a product's hue follows from the\n pack the caller passed rather than from whichever pack the page is wearing, and\n the boundary sits on an element with no radius utility, which is the placement\n the pack-boundary law allows. A product with no pack of its own is drawn as the\n full spectrum rather than as a colourless mark, built from the five series tokens\n because they are the only five-way colour set the contract publishes.\n \n `scripts/check-vector-ink.mjs` holds both Components to the emitted contract. It\n fails a literal colour, a gradient whose stops are not contract references, a\n `var(--x)` naming a property the contract does not publish, and a paint utility\n whose suffix is not a contract role, which is the last of the four a colour\n regex cannot see because a Tailwind class is a name and not a value. Its scanned\n set is a declared list of two file names rather than a pattern, a Component that\n is not on disk fails the run rather than emptying it, and every exclusion carries\n a reason and is printed on every run, with an exclusion that resolves to nothing\n a finding rather than a line that quietly stops appearing.\n- 4f9ce77: A server-rendered pack boundary can now be mode-correct\n \n The only dark selector for a pack was `[data-pack=\"<id>\"].dark`, which requires\n the mode class on the same element. A server cannot know the reader's mode, so a\n subtree carrying a second pack rendered in that pack's light values on a dark\n page. The token build now emits the two-member list\n \n ```\n [data-pack=\"<id>\"].dark, .dark [data-pack=\"<id>\"]\n ```\n \n as one rule with one declaration block, for all five packs. The compound half is\n retained, because it is what a dark surface on a light page is and it is a\n capability the package already published.\n \n Consumers get a second change alongside it. `themeAttributes` now takes `mode` as\n optional, because a required one is that same impossibility written into the type:\n the only way to ask for a boundary with no mode class of its own was to pass\n `'light'` as a guess, and the guess is wrong for half of readers. Omitting `mode`\n is the honest spelling of \"wear the mode of the element carrying `.dark`\". The\n change is a widening, so every existing caller still compiles.\n \n The theme-boundary law in `DESIGN.md` is rewritten to match. It previously\n instructed an implementer to put `data-pack` and `class=\"dark\"` on a boundary\n together, which works for a client-applied boundary and cannot work for a\n server-rendered one.\n- c126ee3: A stored theme is written when a reader decides, and not before\n \n `PrismProvider` wrote `prism-theme` on every resolved change, including the\n first resolution from the server-rendered attributes or from its own defaults. A\n first-time visitor therefore ended the load with a stored value they never chose,\n and that value outranked the site's default on every later visit, so a site that\n changed its default could not reach anyone who had ever loaded the page.\n \n The write is now gated on a decision. It happens when a caller changes the pack,\n the mode or the toggle, and when the library recovers a decision from\n `prism-theme-mode`, a key earlier lines wrote and this one does not. It does not\n happen on a first resolution from the document or the defaults, so a first visit\n ends with an empty store and a returning visit keeps its choice.\n \n Two readers of one stored value now share one rule. `resolveTheme` in\n `@nanisoft/prism-ui/theming` is the rule, `PrismProvider` calls it, and the boot\n script implements it as a string. The script cannot import the function, because\n the build minifies and a renamed identifier inside its own `try` would throw\n there and fail open, so the two are held together by a build-failing equivalence\n gate that runs the emitted string and the rule over one table of stored values:\n nothing stored, a valid pair, a valid mode with a retired pack, a valid pack with\n a retired mode, garbage, a JSON array, a JSON string, `null`, an empty object, a\n partial, a server-rendered pack and mode, and the retired key. Every fall-through\n is asserted to be whole, which is the divergence this fixes: the old string\n validated the pack and the mode independently, so a stored `{ mode: 'dark' }` with\n no pack half-applied there and fell through whole in the rule.\n \n A value that is present but is not a pair is no longer repaired field by field,\n and it is not cleared. It is the only record that this reader ever chose anything.\n \n The boot script records where the theme came from in `data-theme-origin` on\n `<html>`, and it is the only writer: the provider never touches it, because two\n writers would make the origin a race rather than a record. The value is one of\n \n | Value | Meaning |\n | --- | --- |\n | `stored` | the key held a valid `{ pack, mode }` pair |\n | `legacy` | the key was empty and `prism-theme-mode` held a recoverable mode |\n | `unparsed` | the key held a value that is not a pair, and the theme fell through whole |\n | `document` | the key was empty and the root element's own attributes supplied the theme |\n | `default` | neither did, so the consumer's defaults apply |\n \n It is written on every path the script reaches, so an absent origin is never an\n unread one. It is not `data-theme`, which the migration guide retires; it records\n the provenance of a resolution and never a pack.\n \n The migration clause reads `prism-theme-mode`, puts the recovered decision in\n `prism-theme` and removes the key it read, in one guarded step. That is how it\n expires: it fires only while a retired key still holds a mode, at most once per\n reader, and it is retired for good by deleting the last entry of\n `LEGACY_MODE_STORAGE_KEYS`. There is no date and no version constant in it.\n \n The pre-paint string gets its own ceiling in raw bytes,\n `check-boot-budget.mjs`, because it runs before paint on every page and adding it\n to the per-component client budget would corrupt the unit that table measures. The\n ceiling is derived by addition: the pinned non-migration base, plus the measured\n clause, plus one migration generation.\n- 76c8d96: Fourteen items, four catalogue gates, and a Block that publishes its interface\n \n **Fourteen items ship.** Three Components (`Prose`, `FactList`, `ProductSwitcher`),\n nine Blocks (`ProcessRail01`, `StatusLedger01`, `ProductGrid01`, `StackGrid01`,\n `LogoStrip01`, `NoteGrid01`, `InstrumentPanel01`, `SiteHeader`, `SiteFooter`) and two\n Pages (`NotFoundPage`, `BlogPostPage`). Every one of them earns its place by the\n growth rule rather than by taste: a marketing Block when at least three of the four\n sites compose that section today, a Component when it has one job no existing\n Component can express. The evidence is in the ticket and in each item's own\n JSDoc, which names the sites and the line numbers.\n \n **The three contested shapes are settled as specified, and the specifications\n held.** A process rail admits four steps and refuses five, in the type: `steps` is\n a tuple union, so a five-element array is a compile error and not a fifth column. A\n status ledger has four tiers rather than five, and the words for a tier are the\n caller's: the four sites between them use eight words for those four states\n (`live`, `available` and `complete` are one; `approved`, `specified` and `designed`\n are one; then `planned`; then `direction`), and the vocabulary is what `statusLabel`\n is for. A product row's mark is two parts because a filled dot fails contrast on a\n light card, so the mark is `ProductMark`: a hairline ring in `brand-ink` around a\n core in the product's own `primary` fill, with the name in the brand ink beside it.\n \n **Four gates the catalogue's own rules depend on now exist and are wired into\n `check` and, because they read only `src/`, into `build`.** `check-item-docs`\n holds AGENTS.md's claim that a documentation comment is the only path from an Item\n to its published interface. `check-block-copy` holds DESIGN.md's claim that a\n Block ships no copy and no sample data. `check-item-category` holds CONTEXT.md's\n closed set of seven Categories and the sentence that a Block and a Page have none.\n `check-block-imports` holds the claim that a Block fetches nothing and a Page\n imports no router and no data client. Each resolves its roots from its own\n location, fails a root that resolves to nothing, prints its coverage and every\n exclusion on every run, has no report-only mode, and is proved red by a fixture\n before it is proved green.\n \n **Two of those four gates were red on the tree they were written against, and both\n findings were real.** `check-item-docs` found seven exported functions whose\n documentation comment was not attached to the declaration: `CardHeader`,\n `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `SectionHeading` and\n `Hero01`. A comment three functions above a declaration does not reach the emitted\n `.d.ts`, so those seven were absent from the corpus. `check-block-copy` found\n `pricing-01` rendering the word \"Popular\" on a featured plan that passed no badge:\n a hardcoded claim every consumer of the Block inherited, published by the corpus as\n part of the Block's own documentation. The fallback is gone; a featured plan with\n no badge now renders no badge.\n \n **Five additive fields on four shipped items**, each with three of the four\n consumers passing a value the item could not take, counted per consumer with a\n file and a line and never per string. `SectionHeading.index`, for the mono ordinal\n all four sites render above every section head (www `Landing.tsx:49`, atlas\n `landing.tsx:48`, nexus `Landing.tsx:38`, alphalens `Landing.tsx:66`).\n `Cta01.secondaryAction`, for the pair of actions all four closing bands render\n (www `Landing.tsx:186`, atlas `landing.tsx:243`, nexus `Landing.tsx:221`,\n alphalens `Landing.tsx:285`). `Cta01.note`, for the footnote three of the four\n render under it (atlas `landing.tsx:250`, nexus `Landing.tsx:228`, alphalens\n `Landing.tsx:292`). `FeatureGrid01.numbered`, for the ordinal all four render on\n every card (www `Landing.tsx:166`, atlas `landing.tsx:116`, nexus `Landing.tsx:132`,\n alphalens `Landing.tsx:142`). `Hero01.align`, for the left-aligned hero three of\n the four compose (atlas `landing.tsx:69`, nexus `Landing.tsx:63`, alphalens\n `Landing.tsx:87`). All five are additive: nothing changed shape and nothing was\n removed.\n \n **A Block and a Page publish both an interface section and a composition section.**\n `packages/llms` read the extractor's output for a Block and threw the interface half\n away, so the tool told an agent a Block had no own props. The extractor reads a\n named props type off an emitted declaration whether that declaration belongs to a\n Component or to a Block, so both sections are now built and both are assembled into\n the mirror file, `llms-full.txt` and the Store's `doc`.\n \n The Store now carries both fields for every kind, and `get_item_props` prints\n both when it has both. The sentence \"a Block has no own props\" is gone from the\n tool as well as from the published document. It was the one place the two\n sections were still exclusive, because that tool branched on `item.props` and\n fell back to the composition section, so a Block was answered with its\n composition and a denial of the interface it publishes. The test asserts the\n sentence's absence, so a refactor that restores it fails even if the composition\n is still printed, and a second test covers the one-present case so the\n both-present branch cannot swallow it.\n \n **Four accessible names became required props** on `SiteHeader`, `LogoStrip01`,\n `NotFoundPage` and `BlogPostPage`, and a Block that ships no copy ships no\n reader-facing copy either. `navLabel`, `productsLabel`, `label`, `linksLabel` and\n `trailLabel` are words a screen reader reads out, and no Block should be choosing\n them for a consumer whose site calls its own landmarks something else. This is the\n one breaking change in the release, and it is on four new Blocks and two new Pages\n plus one new required prop on `SiteHeader`, all of which are new in the same\n release, so no existing call site changes.\n \n **`ProductSwitcher.label` has no default** for the same reason: a consumer whose set\n is not a set of products should pass its own word rather than accept a claim about\n what the set is.\n\n### Patch Changes\n\n- 8d24f32: A pack boundary's effect on corner radius is stated, and gated\n \n A scoped `data-pack` boundary resolves the pack's colour AND the pack's corner\n radius beneath it, because radius is the only non-colour member of a pack block\n and the token build emits it the same way. The radius range across the five packs\n is 0.5rem to 1rem, so a row of elements each carrying its own pack shows a spread\n of corner radii, and a small pill becomes a different pill five times over. The\n governing law stated the colour half and was silent on the shape half, so an\n implementer following it exactly produced mismatched corners and called it\n correct.\n \n The law now states both halves and where a boundary may sit. The agent-facing\n surface no longer says the attribute belongs on the root element, which it did\n while listing the radius among the values it will pick up: a tool that shows the\n value and denies the subtree is worse than a tool that is silent.\n \n `check-pack-boundary.mjs` holds the law. It reads the emitted `--radius-*`\n bindings rather than a list of class names, which is the part that matters: a\n scale step like `rounded-xl` is `calc(var(--radius) * 1.4)`, so an element\n carrying one is pinned to a step and the step is pack-relative. The first version\n of the gate treated \"carries a radius utility\" as the safe case and passed\n exactly the defect the ticket is about. The gate also reads the colour contract\n from the token source, so a second per-pack axis the token build has not been\n asked about is a finding rather than a property silently counted as a colour.\n \n One live boundary was found and repaired: the themes page scoped five cards, one\n per pack, each with `rounded-xl`, so the page a reader uses to compare packs was\n comparing shape as well as colour. The cards now carry a literal radius and the\n pack's radius is shown in the row's own label, where it is information rather\n than an accident.\n- feb1384: The catalogue is a set comparison between three lists, not a count\n \n A module on disk with no catalogue entry left every count correct. It shipped,\n `shadcn add` installed it, and `registry.json` listed it, so it was absent from\n the corpus, the documentation site and every agent-facing tool with a fully\n green gate run. The two lists a reader trusts, the catalogue and the registry,\n were on the wrong side of that hole, and the checks that believed they covered it\n compared lengths or compared a file against something derived from itself.\n `validate-registry.mjs` holds the registry against the disk and the manifest and\n never reads the catalogue. `sync-registry.mjs` reads the disk, so it cannot\n disagree with the disk about what is on it. The site's\n `test/catalogue-order.test.ts` holds the catalogue against the generated\n ordering and the documentation tree, and both are built from the catalogue, so a\n disagreement there is impossible by construction.\n \n `scripts/check-catalogue.mjs` reads the three sets independently and compares\n them in both directions, name for name, with one printed line per offending\n item. A length assertion cannot report which item is wrong, so nothing in the\n run reports a length on its own: the three set sizes print as supplementary\n coverage underneath the findings that carry the detail. All six directions a\n three-set comparison implies are implemented, plus the kind of an item in each\n pair of sets, the three naming rules that reconcile the three conventions, and a\n claim that a canonical key is held by only one entry in each set.\n \n The registry generator deliberately keeps reading the disk. Deriving the\n registry from the catalogue is the obvious way to remove the second list, and it\n would delete the disagreement the check exists to see, because the generator\n would then agree with the catalogue by construction.\n \n The version and the roster are two claims on two labelled lines, so a version\n bump and a roster error cannot produce the same output. The run states its\n coverage every time: roots resolved, files walked, files actually read, and how\n many comparisons passed. Roots resolve from the script's own location and never\n from the working directory, and a root that resolves to nothing fails the run\n naming both causes.\n \n `PRISM_CATALOGUE_ROOT` points a run at another package root. It changes which\n directory is read and nothing else, which is what lets the tests run the real\n command over a fixture tree and read the real output. The gate fails on a\n catalogue it cannot read, including an empty one: a roster that cannot be read\n and a roster with nothing in it are different facts, and only one of them is a\n green run.\n- 4744cce: The Slider thumb rings at full strength, and the surface check now reads both ways\n \n Two changes, and only two, in the component package.\n \n **The focus indicator.** The Slider's thumb suppressed the browser outline and\n drew its ring at `ring-ring/50`, so the element a keyboard user lands on carried\n an indicator that composites to between 1.14:1 and 2.74:1 against every surface\n in all six themes. That fails WCAG 1.4.11 on the one indicator a keyboard user\n has, and the Slider's own JSDoc says it is reachable by Tab. Thirteen other\n components already drew that ring at full strength; the Slider was the one that\n was missed. Nothing said so, because the contrast gate measures the `ring`\n TOKEN and the alpha is applied in the component rather than in the token, so it\n reads the Slider's ring as compliant.\n \n `packages/ui/scripts/check-focus-indicators.mjs` is the new gate. It reads the\n class strings on the elements of every shipped Component, derives the set of\n components whose own JSDoc claims keyboard reach or operation, and fails any of\n them that suppresses a focus style without declaring a `ring-ring` colour at\n full alpha alongside a `focus-visible:ring-N` width. The claim is read from the\n JSDoc because `AGENTS.md` names that as this repository's documentation source,\n so a component that stops claiming to be focusable is a documentation change and\n shows up in the table the run prints rather than in a list someone maintains\n beside the code. The unit is a class string on one element, so a Dialog's panel\n or a Tooltip's trigger can suppress the outline legitimately; every such\n exclusion is declared with its reason, printed with the slots it resolved to,\n and fails the run if it stops matching anything. The run fails if the table\n empties, and states how much it read, classified, tabled and excluded.\n \n **The surface check.** `scripts/check-surface.mjs` compared the published\n surface against the source in one direction for two of its three rules. A\n wildcard subpath that resolved to zero declarations used to leave those two\n rules reading no file at all and the run still printed its success line, which is\n how a published surface drifts from its source without a consumer failing first.\n A wildcard target that matches nothing is now a finding naming the target. The\n internal side, which the gate deliberately did not check, is now a declared\n boundary checked in both directions: a new file under `dist/lib` is a finding\n naming it, and a declared internal declaration that is no longer emitted is a\n finding naming it too. The boundary and the per-target match counts are printed\n on every run. The three existing rules and their messages are unchanged.\n \n **The version the four downstream site plans pin is `0.6.0`.** It is\n `@nanisoft/prism-ui@0.6.0`, and `@nanisoft/prism-tokens@0.6.0` with it, because\n the two are linked in `.changeset/config.json` and the pending\n `a-server-rendered-pack-boundary-can-be-mode-correct` changeset declares a minor\n for both from `0.5.1`. The patch declared here does not raise it: changesets\n takes the highest bump in the queue, and a minor is already queued. A site plan\n that pins `0.6.0` therefore resolves every subpath the export map publishes,\n including `./components`, `./blocks`, `./pages`, `./provider`, `./theming`,\n `./catalog` and `./styles.css`.\n \n No public prop, no type and no export changed. The one rendering change is a\n ring alpha on the Slider thumb, in both modes.\n- Updated dependencies [4f9ce77]\n- Updated dependencies [5dcb347]\n - @nanisoft/prism-tokens@0.6.0\n\n## 0.5.1\n\n### Patch Changes\n\n- 2fc6a0a: Retune `muted-foreground` so muted text clears 4.5:1 on the muted surface\n \n The base pack's `muted-foreground` moves from neutral 500 to neutral 600. It\n cleared 4.5:1 on the page background but fell to 4.34:1 on `muted`, where the\n pill, avatar fallback, kbd and tab-list pattern place it. The contrast gate now\n also checks `muted-foreground` on `muted`. The pastel packs were already pinned\n to their neutral 700 and are unchanged.\n- Updated dependencies [2fc6a0a]\n - @nanisoft/prism-tokens@0.5.1\n\n## 0.5.0\n\nPrism was rebuilt from a fresh repository on a clean break. The package is now a\npublished React library a consumer installs and composes, not component source to\ncopy. It ships one precompiled `styles.css`, the optional `PrismProvider` and the\n`data-pack` plus `dark` theme axes, and the Components, Blocks and Pages\ntaxonomy. There is no override path.\n\nChangesets are appended above this entry for every published change.\n",
|
|
7923
8028
|
"mirror": "/changelogs/prism-ui.md"
|
|
7924
8029
|
}
|
|
7925
8030
|
],
|
|
@@ -7930,6 +8035,7 @@
|
|
|
7930
8035
|
"route": "/changelogs/prism-llms",
|
|
7931
8036
|
"title": "@nanisoft/prism-llms",
|
|
7932
8037
|
"versions": [
|
|
8038
|
+
"0.6.0",
|
|
7933
8039
|
"0.5.2",
|
|
7934
8040
|
"0.5.1",
|
|
7935
8041
|
"0.5.0",
|
|
@@ -7937,6 +8043,10 @@
|
|
|
7937
8043
|
"0.1.0"
|
|
7938
8044
|
],
|
|
7939
8045
|
"releases": [
|
|
8046
|
+
{
|
|
8047
|
+
"version": "0.6.0",
|
|
8048
|
+
"body": "### Minor Changes\n\n- Regenerate the corpus for the third law of motion\n \n The corpus picks up the `ParticleField`, `AuroraWash` and `Reveal` entries, the\n `backdrop` slot on `Hero01`, the `aurora` field on `Backdrop01`, the\n `scrollRail` prop on `SiteNavbar`, and the rewritten Motion documentation,\n whose third law the new items document in their own JSDoc."
|
|
8049
|
+
},
|
|
7940
8050
|
{
|
|
7941
8051
|
"version": "0.5.2",
|
|
7942
8052
|
"body": "### Patch Changes\n\n- 250b34e: `llms.txt` lists the Live Section: the three live surfaces were in the Store\n and in the Markdown mirror all along, but the index grouped only three of the\n four kinds, so an agent reading the index could not find them by listing. The\n agent skill's own count line now names the fourth kind and the Patterns\n Section, both of which it already shipped. The pack list the token projection\n reads is the Store's own `STORE_PACKS` rather than a second list in the build\n script, which still named six packs past the release that added Cyan and Gold,\n so every token answer an agent read described a family two members short."
|
|
@@ -7958,7 +8068,7 @@
|
|
|
7958
8068
|
"body": "The first versioned line of the rebuilt Prism agent corpus. One build-fresh\n`dist/` carries `llms.txt`, `llms-full.txt`, `prism-skill.md`, the per-item\nMarkdown mirror and `data.json`, the `PrismDocsStore` projection.\n\nChangesets are appended above this entry for every published change."
|
|
7959
8069
|
}
|
|
7960
8070
|
],
|
|
7961
|
-
"text": "# @nanisoft/prism-llms\n\n## 0.5.2\n\n### Patch Changes\n\n- 250b34e: `llms.txt` lists the Live Section: the three live surfaces were in the Store\n and in the Markdown mirror all along, but the index grouped only three of the\n four kinds, so an agent reading the index could not find them by listing. The\n agent skill's own count line now names the fourth kind and the Patterns\n Section, both of which it already shipped. The pack list the token projection\n reads is the Store's own `STORE_PACKS` rather than a second list in the build\n script, which still named six packs past the release that added Cyan and Gold,\n so every token answer an agent read described a family two members short.\n\n## 0.5.1\n\n### Patch Changes\n\n- 5c43520: Add `DocsShell`, a documentation page that takes a site's navigation as data\n \n `DocsShell` is a documentation screen: a navigation rail, the document, a\n contents rail, and a pager to the neighbouring pages. It replaces the unstyled\n `docs-shell` the retired line published, and it is the Page the three NaniSoft\n product sites already import by that name.\n \n The navigation is data. A tree of one page and a tree of twenty-seven render the\n same way, at any depth, and the frame is sized by what the caller passed rather\n than by a fixed arrangement, so three sites whose documentation sets differ in\n shape share one Page with no per-site fork.\n \n - `nav` and `toc` are closed unions of `page`, `group` and `divider`. Both are\n read in the order given: nothing is sorted or alphabetised, so a site that\n files its rail as a pipeline and its contents as a reference has both in one\n render.\n - The pager is derived from `nav` and `currentHref`. There is no `neighbours`\n prop, so a neighbour that is not in the tree cannot be expressed, and each\n consumer loses one derivation of its own.\n - A group with no `href` renders a `span` rather than an anchor with no\n destination. The three sites spell that case today as an empty string, and two\n of them carry a stylesheet rule that styles the result back into a label; those\n rules are now deletable.\n - A section carries no status, no badge, no count, no collapse and no sort. A\n status written into a title stays in the title as words.\n - Both rails are bounded against the viewport, because one site's tree is half\n again as tall as another's.\n \n The corpus fix that shipped with it: the props extractor read each declaration\n body twice with two depth counters that disagreed, so a Page whose `index.tsx`\n re-exported a value and a type from one sibling published every prop twice and\n swept the members of its navigation union into its own prop list. It now reads\n each body once.\n\n## 0.5.0\n\n### Minor Changes\n\n- 76c8d96: Fourteen items, four catalogue gates, and a Block that publishes its interface\n \n **Fourteen items ship.** Three Components (`Prose`, `FactList`, `ProductSwitcher`),\n nine Blocks (`ProcessRail01`, `StatusLedger01`, `ProductGrid01`, `StackGrid01`,\n `LogoStrip01`, `NoteGrid01`, `InstrumentPanel01`, `SiteHeader`, `SiteFooter`) and two\n Pages (`NotFoundPage`, `BlogPostPage`). Every one of them earns its place by the\n growth rule rather than by taste: a marketing Block when at least three of the four\n sites compose that section today, a Component when it has one job no existing\n Component can express. The evidence is in the ticket and in each item's own\n JSDoc, which names the sites and the line numbers.\n \n **The three contested shapes are settled as specified, and the specifications\n held.** A process rail admits four steps and refuses five, in the type: `steps` is\n a tuple union, so a five-element array is a compile error and not a fifth column. A\n status ledger has four tiers rather than five, and the words for a tier are the\n caller's: the four sites between them use eight words for those four states\n (`live`, `available` and `complete` are one; `approved`, `specified` and `designed`\n are one; then `planned`; then `direction`), and the vocabulary is what `statusLabel`\n is for. A product row's mark is two parts because a filled dot fails contrast on a\n light card, so the mark is `ProductMark`: a hairline ring in `brand-ink` around a\n core in the product's own `primary` fill, with the name in the brand ink beside it.\n \n **Four gates the catalogue's own rules depend on now exist and are wired into\n `check` and, because they read only `src/`, into `build`.** `check-item-docs`\n holds AGENTS.md's claim that a documentation comment is the only path from an Item\n to its published interface. `check-block-copy` holds DESIGN.md's claim that a\n Block ships no copy and no sample data. `check-item-category` holds CONTEXT.md's\n closed set of seven Categories and the sentence that a Block and a Page have none.\n `check-block-imports` holds the claim that a Block fetches nothing and a Page\n imports no router and no data client. Each resolves its roots from its own\n location, fails a root that resolves to nothing, prints its coverage and every\n exclusion on every run, has no report-only mode, and is proved red by a fixture\n before it is proved green.\n \n **Two of those four gates were red on the tree they were written against, and both\n findings were real.** `check-item-docs` found seven exported functions whose\n documentation comment was not attached to the declaration: `CardHeader`,\n `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `SectionHeading` and\n `Hero01`. A comment three functions above a declaration does not reach the emitted\n `.d.ts`, so those seven were absent from the corpus. `check-block-copy` found\n `pricing-01` rendering the word \"Popular\" on a featured plan that passed no badge:\n a hardcoded claim every consumer of the Block inherited, published by the corpus as\n part of the Block's own documentation. The fallback is gone; a featured plan with\n no badge now renders no badge.\n \n **Five additive fields on four shipped items**, each with three of the four\n consumers passing a value the item could not take, counted per consumer with a\n file and a line and never per string. `SectionHeading.index`, for the mono ordinal\n all four sites render above every section head (www `Landing.tsx:49`, atlas\n `landing.tsx:48`, nexus `Landing.tsx:38`, alphalens `Landing.tsx:66`).\n `Cta01.secondaryAction`, for the pair of actions all four closing bands render\n (www `Landing.tsx:186`, atlas `landing.tsx:243`, nexus `Landing.tsx:221`,\n alphalens `Landing.tsx:285`). `Cta01.note`, for the footnote three of the four\n render under it (atlas `landing.tsx:250`, nexus `Landing.tsx:228`, alphalens\n `Landing.tsx:292`). `FeatureGrid01.numbered`, for the ordinal all four render on\n every card (www `Landing.tsx:166`, atlas `landing.tsx:116`, nexus `Landing.tsx:132`,\n alphalens `Landing.tsx:142`). `Hero01.align`, for the left-aligned hero three of\n the four compose (atlas `landing.tsx:69`, nexus `Landing.tsx:63`, alphalens\n `Landing.tsx:87`). All five are additive: nothing changed shape and nothing was\n removed.\n \n **A Block and a Page publish both an interface section and a composition section.**\n `packages/llms` read the extractor's output for a Block and threw the interface half\n away, so the tool told an agent a Block had no own props. The extractor reads a\n named props type off an emitted declaration whether that declaration belongs to a\n Component or to a Block, so both sections are now built and both are assembled into\n the mirror file, `llms-full.txt` and the Store's `doc`.\n \n The Store now carries both fields for every kind, and `get_item_props` prints\n both when it has both. The sentence \"a Block has no own props\" is gone from the\n tool as well as from the published document. It was the one place the two\n sections were still exclusive, because that tool branched on `item.props` and\n fell back to the composition section, so a Block was answered with its\n composition and a denial of the interface it publishes. The test asserts the\n sentence's absence, so a refactor that restores it fails even if the composition\n is still printed, and a second test covers the one-present case so the\n both-present branch cannot swallow it.\n \n **Four accessible names became required props** on `SiteHeader`, `LogoStrip01`,\n `NotFoundPage` and `BlogPostPage`, and a Block that ships no copy ships no\n reader-facing copy either. `navLabel`, `productsLabel`, `label`, `linksLabel` and\n `trailLabel` are words a screen reader reads out, and no Block should be choosing\n them for a consumer whose site calls its own landmarks something else. This is the\n one breaking change in the release, and it is on four new Blocks and two new Pages\n plus one new required prop on `SiteHeader`, all of which are new in the same\n release, so no existing call site changes.\n \n **`ProductSwitcher.label` has no default** for the same reason: a consumer whose set\n is not a set of products should pass its own word rather than accept a claim about\n what the set is.\n\n### Patch Changes\n\n- 4832359: Build the corpus after the changelog routes are copied, and declare what it reads\n \n The corpus asserts that every published package shipping a changelog has a route\n in the site's content tree, and those routes are written by a copy step that\n belongs to the site package. The copy ran after this package was built, so on a\n cold machine the build failed with a message that read like a changelog file had\n been deleted, and a warm machine never saw the failure at all.\n \n The copy is now the task `@nanisoft/site#copy-changelogs`, which this build\n depends on in the task graph, and this build declares the content tree, the Item\n documentation tree, the two site script modules the builder imports, the\n workspace globs, the published manifests and the changelogs as its inputs, so a\n change to any of them rebuilds the corpus instead of replaying a cached one.\n \n The assertion is unchanged and still fails a published package the site does not\n publish, which is how a changelog path the corpus advertised while the site\n returned 404 for it was found. Its message now says which of the two causes it\n is. No corpus byte changes: the emitted corpus is the same.\n\n## 0.2.0\n\n### Minor Changes\n\n- c0cc0a9: Publish seven Sections, and keep every published URL resolving\n \n `STORE_SECTIONS` is `overview`, `foundation`, `content`, `changelogs` where it\n was `docs`, `foundations`, `content`, `changelogs`, and `STORE_SECTION_TITLES`\n carries the Section's own name beside its directory. The two are a pair for a\n reason: the directory is the route a page is addressed by and `get_page` matches\n on it, and the name is what `llms.txt` groups pages under and what `list_pages`\n counts. A Section renamed without its label renamed would be advertised under two\n names in two artifacts, and a page whose `section` no longer names a live\n directory is a page no tool can reach.\n \n The reading order of the prose Sections is the site's: Overview, then Foundation,\n then Content, then the catalogue, then the Changelogs. The corpus is regenerated\n at build, so `llms.txt`, `llms-full.txt`, `data.json` and every mirror under `md/`\n advertise the new routes, and the site Worker answers each old route with a\n permanent redirect generated from the same manifest. An agent holding a cached\n `llms.txt` from before this release resolves every URL it names and is told\n nothing about the move.\n \n Nothing in the item surface moves. The forty-two catalogue routes are the same\n forty-two addresses, their mirrors are byte-identical, and the nine tools keep\n their names and their positions; `list_pages` and `get_page` are unchanged apart\n from the Section names they group and describe, which is a parameter on the\n existing tools and not a rename of any of them.\n- b1125a0: Publish the Changelogs Section, and put the changelogs in the agent surface\n \n There was no Changelogs Section, so the only record of a change in a published\n package was a file inside its tarball. A reader now has one route per published\n package and an agent can ask for it. Both halves land together, so a Section never\n exists without being in the Corpus.\n \n The per-package pages are byte-for-byte copies of each published package's own\n `CHANGELOG.md`, written by a copy step inside the build the site already runs.\n That is the whole reason for the shape: a hand-maintained copy of a changelog\n drifts from what was published while continuing to look correct, and it is a third\n place to remember alongside the changeset and the JSDoc. The published packages\n are discovered from the workspace, so publishing a package is what adds a route,\n and a package that ships a changelog and no route fails the build in three\n places. The reference design system this is modelled on has reader-facing\n changelog pages and no such gate, which is how every one of them can be deleted\n and leave its continuous integration green.\n \n `STORE_SECTIONS` gains `changelogs`, so `llms.txt`, `llms-full.txt` and the\n Markdown mirror carry a Changelogs group, and the content walk reads the second\n extension the Section needs: a copied changelog is plain Markdown, and a walk\n filtered to `.mdx` would have left the whole Section out of the agent surface\n with a green build. The walk already recursed, so a page nested inside a Section\n reaches the Corpus the same way it did before.\n \n `PrismDocsStore` gains a required `changelogs` field carrying each package's\n bytes, its route and its version entries, and the MCP server gains one read-only\n tool, `get_changelog(package, version?)`, registered last. The eight existing\n tools keep their names and their positions: a Section joining the Corpus is a\n parameter on `list_pages` and `get_page`, which now read the Sections from the\n Store rather than from a list of the three prose Sections, and not a rename of\n anything. `get_changelog` returns the package's own file, because a generated\n summary would drift the moment a release was published, and a breaking change in\n a published package is exactly what a consuming agent has to be able to find.\n\n### Patch Changes\n\n- cbf442f: Make the corpus walk the content tree, and make a missing Section fail\n \n The generator read one directory per Section and filtered it to `.mdx`, so a page\n one folder deeper was absent from `llms.txt`, from `llms-full.txt`, from the\n `PrismDocsStore` and from every read-only MCP tool, with a green build. The walk\n now recurses, and a page's route, its `slug` and its Markdown mirror all come\n from where the page sits in the tree, so nesting content publishes a page\n instead of losing it.\n \n The same file guarded the Section loop with `if (!existsSync(dir)) continue`, so a\n Section that was declared and not on disk, which is what renaming one does, was\n skipped silently and deleted from every agent surface. It now throws.\n \n `llms.txt`, `llms-full.txt`, `data.json` and `md/**` are byte-for-byte unchanged\n for the current flat tree.\n\n## 0.1.0\n\nThe first versioned line of the rebuilt Prism agent corpus. One build-fresh\n`dist/` carries `llms.txt`, `llms-full.txt`, `prism-skill.md`, the per-item\nMarkdown mirror and `data.json`, the `PrismDocsStore` projection.\n\nChangesets are appended above this entry for every published change.\n"
|
|
8071
|
+
"text": "# @nanisoft/prism-llms\n\n## 0.6.0\n\n### Minor Changes\n\n- Regenerate the corpus for the third law of motion\n \n The corpus picks up the `ParticleField`, `AuroraWash` and `Reveal` entries, the\n `backdrop` slot on `Hero01`, the `aurora` field on `Backdrop01`, the\n `scrollRail` prop on `SiteNavbar`, and the rewritten Motion documentation,\n whose third law the new items document in their own JSDoc.\n\n## 0.5.2\n\n### Patch Changes\n\n- 250b34e: `llms.txt` lists the Live Section: the three live surfaces were in the Store\n and in the Markdown mirror all along, but the index grouped only three of the\n four kinds, so an agent reading the index could not find them by listing. The\n agent skill's own count line now names the fourth kind and the Patterns\n Section, both of which it already shipped. The pack list the token projection\n reads is the Store's own `STORE_PACKS` rather than a second list in the build\n script, which still named six packs past the release that added Cyan and Gold,\n so every token answer an agent read described a family two members short.\n\n## 0.5.1\n\n### Patch Changes\n\n- 5c43520: Add `DocsShell`, a documentation page that takes a site's navigation as data\n \n `DocsShell` is a documentation screen: a navigation rail, the document, a\n contents rail, and a pager to the neighbouring pages. It replaces the unstyled\n `docs-shell` the retired line published, and it is the Page the three NaniSoft\n product sites already import by that name.\n \n The navigation is data. A tree of one page and a tree of twenty-seven render the\n same way, at any depth, and the frame is sized by what the caller passed rather\n than by a fixed arrangement, so three sites whose documentation sets differ in\n shape share one Page with no per-site fork.\n \n - `nav` and `toc` are closed unions of `page`, `group` and `divider`. Both are\n read in the order given: nothing is sorted or alphabetised, so a site that\n files its rail as a pipeline and its contents as a reference has both in one\n render.\n - The pager is derived from `nav` and `currentHref`. There is no `neighbours`\n prop, so a neighbour that is not in the tree cannot be expressed, and each\n consumer loses one derivation of its own.\n - A group with no `href` renders a `span` rather than an anchor with no\n destination. The three sites spell that case today as an empty string, and two\n of them carry a stylesheet rule that styles the result back into a label; those\n rules are now deletable.\n - A section carries no status, no badge, no count, no collapse and no sort. A\n status written into a title stays in the title as words.\n - Both rails are bounded against the viewport, because one site's tree is half\n again as tall as another's.\n \n The corpus fix that shipped with it: the props extractor read each declaration\n body twice with two depth counters that disagreed, so a Page whose `index.tsx`\n re-exported a value and a type from one sibling published every prop twice and\n swept the members of its navigation union into its own prop list. It now reads\n each body once.\n\n## 0.5.0\n\n### Minor Changes\n\n- 76c8d96: Fourteen items, four catalogue gates, and a Block that publishes its interface\n \n **Fourteen items ship.** Three Components (`Prose`, `FactList`, `ProductSwitcher`),\n nine Blocks (`ProcessRail01`, `StatusLedger01`, `ProductGrid01`, `StackGrid01`,\n `LogoStrip01`, `NoteGrid01`, `InstrumentPanel01`, `SiteHeader`, `SiteFooter`) and two\n Pages (`NotFoundPage`, `BlogPostPage`). Every one of them earns its place by the\n growth rule rather than by taste: a marketing Block when at least three of the four\n sites compose that section today, a Component when it has one job no existing\n Component can express. The evidence is in the ticket and in each item's own\n JSDoc, which names the sites and the line numbers.\n \n **The three contested shapes are settled as specified, and the specifications\n held.** A process rail admits four steps and refuses five, in the type: `steps` is\n a tuple union, so a five-element array is a compile error and not a fifth column. A\n status ledger has four tiers rather than five, and the words for a tier are the\n caller's: the four sites between them use eight words for those four states\n (`live`, `available` and `complete` are one; `approved`, `specified` and `designed`\n are one; then `planned`; then `direction`), and the vocabulary is what `statusLabel`\n is for. A product row's mark is two parts because a filled dot fails contrast on a\n light card, so the mark is `ProductMark`: a hairline ring in `brand-ink` around a\n core in the product's own `primary` fill, with the name in the brand ink beside it.\n \n **Four gates the catalogue's own rules depend on now exist and are wired into\n `check` and, because they read only `src/`, into `build`.** `check-item-docs`\n holds AGENTS.md's claim that a documentation comment is the only path from an Item\n to its published interface. `check-block-copy` holds DESIGN.md's claim that a\n Block ships no copy and no sample data. `check-item-category` holds CONTEXT.md's\n closed set of seven Categories and the sentence that a Block and a Page have none.\n `check-block-imports` holds the claim that a Block fetches nothing and a Page\n imports no router and no data client. Each resolves its roots from its own\n location, fails a root that resolves to nothing, prints its coverage and every\n exclusion on every run, has no report-only mode, and is proved red by a fixture\n before it is proved green.\n \n **Two of those four gates were red on the tree they were written against, and both\n findings were real.** `check-item-docs` found seven exported functions whose\n documentation comment was not attached to the declaration: `CardHeader`,\n `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `SectionHeading` and\n `Hero01`. A comment three functions above a declaration does not reach the emitted\n `.d.ts`, so those seven were absent from the corpus. `check-block-copy` found\n `pricing-01` rendering the word \"Popular\" on a featured plan that passed no badge:\n a hardcoded claim every consumer of the Block inherited, published by the corpus as\n part of the Block's own documentation. The fallback is gone; a featured plan with\n no badge now renders no badge.\n \n **Five additive fields on four shipped items**, each with three of the four\n consumers passing a value the item could not take, counted per consumer with a\n file and a line and never per string. `SectionHeading.index`, for the mono ordinal\n all four sites render above every section head (www `Landing.tsx:49`, atlas\n `landing.tsx:48`, nexus `Landing.tsx:38`, alphalens `Landing.tsx:66`).\n `Cta01.secondaryAction`, for the pair of actions all four closing bands render\n (www `Landing.tsx:186`, atlas `landing.tsx:243`, nexus `Landing.tsx:221`,\n alphalens `Landing.tsx:285`). `Cta01.note`, for the footnote three of the four\n render under it (atlas `landing.tsx:250`, nexus `Landing.tsx:228`, alphalens\n `Landing.tsx:292`). `FeatureGrid01.numbered`, for the ordinal all four render on\n every card (www `Landing.tsx:166`, atlas `landing.tsx:116`, nexus `Landing.tsx:132`,\n alphalens `Landing.tsx:142`). `Hero01.align`, for the left-aligned hero three of\n the four compose (atlas `landing.tsx:69`, nexus `Landing.tsx:63`, alphalens\n `Landing.tsx:87`). All five are additive: nothing changed shape and nothing was\n removed.\n \n **A Block and a Page publish both an interface section and a composition section.**\n `packages/llms` read the extractor's output for a Block and threw the interface half\n away, so the tool told an agent a Block had no own props. The extractor reads a\n named props type off an emitted declaration whether that declaration belongs to a\n Component or to a Block, so both sections are now built and both are assembled into\n the mirror file, `llms-full.txt` and the Store's `doc`.\n \n The Store now carries both fields for every kind, and `get_item_props` prints\n both when it has both. The sentence \"a Block has no own props\" is gone from the\n tool as well as from the published document. It was the one place the two\n sections were still exclusive, because that tool branched on `item.props` and\n fell back to the composition section, so a Block was answered with its\n composition and a denial of the interface it publishes. The test asserts the\n sentence's absence, so a refactor that restores it fails even if the composition\n is still printed, and a second test covers the one-present case so the\n both-present branch cannot swallow it.\n \n **Four accessible names became required props** on `SiteHeader`, `LogoStrip01`,\n `NotFoundPage` and `BlogPostPage`, and a Block that ships no copy ships no\n reader-facing copy either. `navLabel`, `productsLabel`, `label`, `linksLabel` and\n `trailLabel` are words a screen reader reads out, and no Block should be choosing\n them for a consumer whose site calls its own landmarks something else. This is the\n one breaking change in the release, and it is on four new Blocks and two new Pages\n plus one new required prop on `SiteHeader`, all of which are new in the same\n release, so no existing call site changes.\n \n **`ProductSwitcher.label` has no default** for the same reason: a consumer whose set\n is not a set of products should pass its own word rather than accept a claim about\n what the set is.\n\n### Patch Changes\n\n- 4832359: Build the corpus after the changelog routes are copied, and declare what it reads\n \n The corpus asserts that every published package shipping a changelog has a route\n in the site's content tree, and those routes are written by a copy step that\n belongs to the site package. The copy ran after this package was built, so on a\n cold machine the build failed with a message that read like a changelog file had\n been deleted, and a warm machine never saw the failure at all.\n \n The copy is now the task `@nanisoft/site#copy-changelogs`, which this build\n depends on in the task graph, and this build declares the content tree, the Item\n documentation tree, the two site script modules the builder imports, the\n workspace globs, the published manifests and the changelogs as its inputs, so a\n change to any of them rebuilds the corpus instead of replaying a cached one.\n \n The assertion is unchanged and still fails a published package the site does not\n publish, which is how a changelog path the corpus advertised while the site\n returned 404 for it was found. Its message now says which of the two causes it\n is. No corpus byte changes: the emitted corpus is the same.\n\n## 0.2.0\n\n### Minor Changes\n\n- c0cc0a9: Publish seven Sections, and keep every published URL resolving\n \n `STORE_SECTIONS` is `overview`, `foundation`, `content`, `changelogs` where it\n was `docs`, `foundations`, `content`, `changelogs`, and `STORE_SECTION_TITLES`\n carries the Section's own name beside its directory. The two are a pair for a\n reason: the directory is the route a page is addressed by and `get_page` matches\n on it, and the name is what `llms.txt` groups pages under and what `list_pages`\n counts. A Section renamed without its label renamed would be advertised under two\n names in two artifacts, and a page whose `section` no longer names a live\n directory is a page no tool can reach.\n \n The reading order of the prose Sections is the site's: Overview, then Foundation,\n then Content, then the catalogue, then the Changelogs. The corpus is regenerated\n at build, so `llms.txt`, `llms-full.txt`, `data.json` and every mirror under `md/`\n advertise the new routes, and the site Worker answers each old route with a\n permanent redirect generated from the same manifest. An agent holding a cached\n `llms.txt` from before this release resolves every URL it names and is told\n nothing about the move.\n \n Nothing in the item surface moves. The forty-two catalogue routes are the same\n forty-two addresses, their mirrors are byte-identical, and the nine tools keep\n their names and their positions; `list_pages` and `get_page` are unchanged apart\n from the Section names they group and describe, which is a parameter on the\n existing tools and not a rename of any of them.\n- b1125a0: Publish the Changelogs Section, and put the changelogs in the agent surface\n \n There was no Changelogs Section, so the only record of a change in a published\n package was a file inside its tarball. A reader now has one route per published\n package and an agent can ask for it. Both halves land together, so a Section never\n exists without being in the Corpus.\n \n The per-package pages are byte-for-byte copies of each published package's own\n `CHANGELOG.md`, written by a copy step inside the build the site already runs.\n That is the whole reason for the shape: a hand-maintained copy of a changelog\n drifts from what was published while continuing to look correct, and it is a third\n place to remember alongside the changeset and the JSDoc. The published packages\n are discovered from the workspace, so publishing a package is what adds a route,\n and a package that ships a changelog and no route fails the build in three\n places. The reference design system this is modelled on has reader-facing\n changelog pages and no such gate, which is how every one of them can be deleted\n and leave its continuous integration green.\n \n `STORE_SECTIONS` gains `changelogs`, so `llms.txt`, `llms-full.txt` and the\n Markdown mirror carry a Changelogs group, and the content walk reads the second\n extension the Section needs: a copied changelog is plain Markdown, and a walk\n filtered to `.mdx` would have left the whole Section out of the agent surface\n with a green build. The walk already recursed, so a page nested inside a Section\n reaches the Corpus the same way it did before.\n \n `PrismDocsStore` gains a required `changelogs` field carrying each package's\n bytes, its route and its version entries, and the MCP server gains one read-only\n tool, `get_changelog(package, version?)`, registered last. The eight existing\n tools keep their names and their positions: a Section joining the Corpus is a\n parameter on `list_pages` and `get_page`, which now read the Sections from the\n Store rather than from a list of the three prose Sections, and not a rename of\n anything. `get_changelog` returns the package's own file, because a generated\n summary would drift the moment a release was published, and a breaking change in\n a published package is exactly what a consuming agent has to be able to find.\n\n### Patch Changes\n\n- cbf442f: Make the corpus walk the content tree, and make a missing Section fail\n \n The generator read one directory per Section and filtered it to `.mdx`, so a page\n one folder deeper was absent from `llms.txt`, from `llms-full.txt`, from the\n `PrismDocsStore` and from every read-only MCP tool, with a green build. The walk\n now recurses, and a page's route, its `slug` and its Markdown mirror all come\n from where the page sits in the tree, so nesting content publishes a page\n instead of losing it.\n \n The same file guarded the Section loop with `if (!existsSync(dir)) continue`, so a\n Section that was declared and not on disk, which is what renaming one does, was\n skipped silently and deleted from every agent surface. It now throws.\n \n `llms.txt`, `llms-full.txt`, `data.json` and `md/**` are byte-for-byte unchanged\n for the current flat tree.\n\n## 0.1.0\n\nThe first versioned line of the rebuilt Prism agent corpus. One build-fresh\n`dist/` carries `llms.txt`, `llms-full.txt`, `prism-skill.md`, the per-item\nMarkdown mirror and `data.json`, the `PrismDocsStore` projection.\n\nChangesets are appended above this entry for every published change.\n"
|
|
7962
8072
|
},
|
|
7963
8073
|
{
|
|
7964
8074
|
"package": "@nanisoft/prism-mcp-server",
|
|
@@ -7966,6 +8076,7 @@
|
|
|
7966
8076
|
"route": "/changelogs/prism-mcp-server",
|
|
7967
8077
|
"title": "@nanisoft/prism-mcp-server",
|
|
7968
8078
|
"versions": [
|
|
8079
|
+
"0.4.3",
|
|
7969
8080
|
"0.4.2",
|
|
7970
8081
|
"0.4.1",
|
|
7971
8082
|
"0.4.0",
|
|
@@ -7973,6 +8084,10 @@
|
|
|
7973
8084
|
"0.1.0"
|
|
7974
8085
|
],
|
|
7975
8086
|
"releases": [
|
|
8087
|
+
{
|
|
8088
|
+
"version": "0.4.3",
|
|
8089
|
+
"body": "### Patch Changes\n\n- Updated dependencies\n - @nanisoft/prism-llms@0.6.0"
|
|
8090
|
+
},
|
|
7976
8091
|
{
|
|
7977
8092
|
"version": "0.4.2",
|
|
7978
8093
|
"body": "### Patch Changes\n\n- Updated dependencies [250b34e]\n - @nanisoft/prism-llms@0.5.2"
|
|
@@ -7994,7 +8109,7 @@
|
|
|
7994
8109
|
"body": "The first versioned line of the rebuilt Prism MCP surface. The package is\ntransport-free: `createPrismMcpServer(store, options)` returns an `McpServer`\nthat the site Worker connects to the Agents SDK at `/mcp`. Eight read-only\nretrieval tools answer over the bundled `PrismDocsStore`.\n\nChangesets are appended above this entry for every published change."
|
|
7995
8110
|
}
|
|
7996
8111
|
],
|
|
7997
|
-
"text": "# @nanisoft/prism-mcp-server\n\n## 0.4.2\n\n### Patch Changes\n\n- Updated dependencies [250b34e]\n - @nanisoft/prism-llms@0.5.2\n\n## 0.4.1\n\n### Patch Changes\n\n- Updated dependencies [5c43520]\n - @nanisoft/prism-llms@0.5.1\n\n## 0.4.0\n\n### Patch Changes\n\n- Updated dependencies [4832359]\n- Updated dependencies [76c8d96]\n - @nanisoft/prism-llms@0.3.0\n\n## 0.2.0\n\n### Minor Changes\n\n- c0cc0a9: Publish seven Sections, and keep every published URL resolving\n \n `STORE_SECTIONS` is `overview`, `foundation`, `content`, `changelogs` where it\n was `docs`, `foundations`, `content`, `changelogs`, and `STORE_SECTION_TITLES`\n carries the Section's own name beside its directory. The two are a pair for a\n reason: the directory is the route a page is addressed by and `get_page` matches\n on it, and the name is what `llms.txt` groups pages under and what `list_pages`\n counts. A Section renamed without its label renamed would be advertised under two\n names in two artifacts, and a page whose `section` no longer names a live\n directory is a page no tool can reach.\n \n The reading order of the prose Sections is the site's: Overview, then Foundation,\n then Content, then the catalogue, then the Changelogs. The corpus is regenerated\n at build, so `llms.txt`, `llms-full.txt`, `data.json` and every mirror under `md/`\n advertise the new routes, and the site Worker answers each old route with a\n permanent redirect generated from the same manifest. An agent holding a cached\n `llms.txt` from before this release resolves every URL it names and is told\n nothing about the move.\n \n Nothing in the item surface moves. The forty-two catalogue routes are the same\n forty-two addresses, their mirrors are byte-identical, and the nine tools keep\n their names and their positions; `list_pages` and `get_page` are unchanged apart\n from the Section names they group and describe, which is a parameter on the\n existing tools and not a rename of any of them.\n- b1125a0: Publish the Changelogs Section, and put the changelogs in the agent surface\n \n There was no Changelogs Section, so the only record of a change in a published\n package was a file inside its tarball. A reader now has one route per published\n package and an agent can ask for it. Both halves land together, so a Section never\n exists without being in the Corpus.\n \n The per-package pages are byte-for-byte copies of each published package's own\n `CHANGELOG.md`, written by a copy step inside the build the site already runs.\n That is the whole reason for the shape: a hand-maintained copy of a changelog\n drifts from what was published while continuing to look correct, and it is a third\n place to remember alongside the changeset and the JSDoc. The published packages\n are discovered from the workspace, so publishing a package is what adds a route,\n and a package that ships a changelog and no route fails the build in three\n places. The reference design system this is modelled on has reader-facing\n changelog pages and no such gate, which is how every one of them can be deleted\n and leave its continuous integration green.\n \n `STORE_SECTIONS` gains `changelogs`, so `llms.txt`, `llms-full.txt` and the\n Markdown mirror carry a Changelogs group, and the content walk reads the second\n extension the Section needs: a copied changelog is plain Markdown, and a walk\n filtered to `.mdx` would have left the whole Section out of the agent surface\n with a green build. The walk already recursed, so a page nested inside a Section\n reaches the Corpus the same way it did before.\n \n `PrismDocsStore` gains a required `changelogs` field carrying each package's\n bytes, its route and its version entries, and the MCP server gains one read-only\n tool, `get_changelog(package, version?)`, registered last. The eight existing\n tools keep their names and their positions: a Section joining the Corpus is a\n parameter on `list_pages` and `get_page`, which now read the Sections from the\n Store rather than from a list of the three prose Sections, and not a rename of\n anything. `get_changelog` returns the package's own file, because a generated\n summary would drift the moment a release was published, and a breaking change in\n a published package is exactly what a consuming agent has to be able to find.\n\n### Patch Changes\n\n- Updated dependencies [cbf442f]\n- Updated dependencies [c0cc0a9]\n- Updated dependencies [b1125a0]\n - @nanisoft/prism-llms@0.2.0\n\n## 0.1.0\n\nThe first versioned line of the rebuilt Prism MCP surface. The package is\ntransport-free: `createPrismMcpServer(store, options)` returns an `McpServer`\nthat the site Worker connects to the Agents SDK at `/mcp`. Eight read-only\nretrieval tools answer over the bundled `PrismDocsStore`.\n\nChangesets are appended above this entry for every published change.\n"
|
|
8112
|
+
"text": "# @nanisoft/prism-mcp-server\n\n## 0.4.3\n\n### Patch Changes\n\n- Updated dependencies\n - @nanisoft/prism-llms@0.6.0\n\n## 0.4.2\n\n### Patch Changes\n\n- Updated dependencies [250b34e]\n - @nanisoft/prism-llms@0.5.2\n\n## 0.4.1\n\n### Patch Changes\n\n- Updated dependencies [5c43520]\n - @nanisoft/prism-llms@0.5.1\n\n## 0.4.0\n\n### Patch Changes\n\n- Updated dependencies [4832359]\n- Updated dependencies [76c8d96]\n - @nanisoft/prism-llms@0.3.0\n\n## 0.2.0\n\n### Minor Changes\n\n- c0cc0a9: Publish seven Sections, and keep every published URL resolving\n \n `STORE_SECTIONS` is `overview`, `foundation`, `content`, `changelogs` where it\n was `docs`, `foundations`, `content`, `changelogs`, and `STORE_SECTION_TITLES`\n carries the Section's own name beside its directory. The two are a pair for a\n reason: the directory is the route a page is addressed by and `get_page` matches\n on it, and the name is what `llms.txt` groups pages under and what `list_pages`\n counts. A Section renamed without its label renamed would be advertised under two\n names in two artifacts, and a page whose `section` no longer names a live\n directory is a page no tool can reach.\n \n The reading order of the prose Sections is the site's: Overview, then Foundation,\n then Content, then the catalogue, then the Changelogs. The corpus is regenerated\n at build, so `llms.txt`, `llms-full.txt`, `data.json` and every mirror under `md/`\n advertise the new routes, and the site Worker answers each old route with a\n permanent redirect generated from the same manifest. An agent holding a cached\n `llms.txt` from before this release resolves every URL it names and is told\n nothing about the move.\n \n Nothing in the item surface moves. The forty-two catalogue routes are the same\n forty-two addresses, their mirrors are byte-identical, and the nine tools keep\n their names and their positions; `list_pages` and `get_page` are unchanged apart\n from the Section names they group and describe, which is a parameter on the\n existing tools and not a rename of any of them.\n- b1125a0: Publish the Changelogs Section, and put the changelogs in the agent surface\n \n There was no Changelogs Section, so the only record of a change in a published\n package was a file inside its tarball. A reader now has one route per published\n package and an agent can ask for it. Both halves land together, so a Section never\n exists without being in the Corpus.\n \n The per-package pages are byte-for-byte copies of each published package's own\n `CHANGELOG.md`, written by a copy step inside the build the site already runs.\n That is the whole reason for the shape: a hand-maintained copy of a changelog\n drifts from what was published while continuing to look correct, and it is a third\n place to remember alongside the changeset and the JSDoc. The published packages\n are discovered from the workspace, so publishing a package is what adds a route,\n and a package that ships a changelog and no route fails the build in three\n places. The reference design system this is modelled on has reader-facing\n changelog pages and no such gate, which is how every one of them can be deleted\n and leave its continuous integration green.\n \n `STORE_SECTIONS` gains `changelogs`, so `llms.txt`, `llms-full.txt` and the\n Markdown mirror carry a Changelogs group, and the content walk reads the second\n extension the Section needs: a copied changelog is plain Markdown, and a walk\n filtered to `.mdx` would have left the whole Section out of the agent surface\n with a green build. The walk already recursed, so a page nested inside a Section\n reaches the Corpus the same way it did before.\n \n `PrismDocsStore` gains a required `changelogs` field carrying each package's\n bytes, its route and its version entries, and the MCP server gains one read-only\n tool, `get_changelog(package, version?)`, registered last. The eight existing\n tools keep their names and their positions: a Section joining the Corpus is a\n parameter on `list_pages` and `get_page`, which now read the Sections from the\n Store rather than from a list of the three prose Sections, and not a rename of\n anything. `get_changelog` returns the package's own file, because a generated\n summary would drift the moment a release was published, and a breaking change in\n a published package is exactly what a consuming agent has to be able to find.\n\n### Patch Changes\n\n- Updated dependencies [cbf442f]\n- Updated dependencies [c0cc0a9]\n- Updated dependencies [b1125a0]\n - @nanisoft/prism-llms@0.2.0\n\n## 0.1.0\n\nThe first versioned line of the rebuilt Prism MCP surface. The package is\ntransport-free: `createPrismMcpServer(store, options)` returns an `McpServer`\nthat the site Worker connects to the Agents SDK at `/mcp`. Eight read-only\nretrieval tools answer over the bundled `PrismDocsStore`.\n\nChangesets are appended above this entry for every published change.\n"
|
|
7998
8113
|
},
|
|
7999
8114
|
{
|
|
8000
8115
|
"package": "@nanisoft/prism-tokens",
|
|
@@ -8002,6 +8117,7 @@
|
|
|
8002
8117
|
"route": "/changelogs/prism-tokens",
|
|
8003
8118
|
"title": "@nanisoft/prism-tokens",
|
|
8004
8119
|
"versions": [
|
|
8120
|
+
"1.5.0",
|
|
8005
8121
|
"1.2.0",
|
|
8006
8122
|
"0.16.0",
|
|
8007
8123
|
"0.15.0",
|
|
@@ -8011,6 +8127,10 @@
|
|
|
8011
8127
|
"0.5.0"
|
|
8012
8128
|
],
|
|
8013
8129
|
"releases": [
|
|
8130
|
+
{
|
|
8131
|
+
"version": "1.5.0",
|
|
8132
|
+
"body": "### Minor Changes\n\n- Add the `aurora` ambient cycle at 24000ms\n \n The new member measures light crossing a ground: the washes a hero stands on.\n It rides `ambient-ease-drift`, holds to the one-second floor with the rest of\n the group, and is admitted into the closed set the emitted-contract gate holds."
|
|
8133
|
+
},
|
|
8014
8134
|
{
|
|
8015
8135
|
"version": "1.2.0",
|
|
8016
8136
|
"body": "### Minor Changes\n\n- Two new packs: Cyan and Gold\n \n The family's product map asked for two hues the palette did not carry, and a\n product's ground is a fact about the system rather than about the site that\n wears it, so the hues arrive here instead of as consumer overrides.\n \n `cyan` is the Nexus pack: pastel cyan-blue, generated at hue 215 in OKLCH with\n the same perceptually even lightness ramp as the other five, at the crispest\n radius in the set (0.5rem) beside Sky. `gold` is the AlphaLens pack: pastel\n amber-gold at hue 80, at the roomiest radius (1rem) beside Peach. Both carry\n the full shadcn-compatible contract, tinted neutrals at the family's chroma,\n and pass the contrast gate at 38/38 required pairs in both modes.\n \n `PACKS` in the theming module and `STORE_PACKS` in the llms corpus carry the\n two new ids, so a consumer's ground-pack validation and every agent reading\n the corpus see the same vocabulary. Peach returns to reserved. The published\n pack count is now seven pastels plus the neutral default."
|
|
@@ -8040,7 +8160,7 @@
|
|
|
8040
8160
|
"body": "Prism was rebuilt from a fresh repository on a clean break. The token pipeline is\nDTCG 2025.10 compiled with Style Dictionary, the semantic names keep the shadcn\ncontract verbatim, and motion, typography, spacing, shadows, breakpoints and\ncontainers all reach CSS through a single `@theme static` block.\n\nChangesets are appended above this entry for every published change."
|
|
8041
8161
|
}
|
|
8042
8162
|
],
|
|
8043
|
-
"text": "# @nanisoft/prism-tokens\n\n## 1.2.0\n\n### Minor Changes\n\n- Two new packs: Cyan and Gold\n \n The family's product map asked for two hues the palette did not carry, and a\n product's ground is a fact about the system rather than about the site that\n wears it, so the hues arrive here instead of as consumer overrides.\n \n `cyan` is the Nexus pack: pastel cyan-blue, generated at hue 215 in OKLCH with\n the same perceptually even lightness ramp as the other five, at the crispest\n radius in the set (0.5rem) beside Sky. `gold` is the AlphaLens pack: pastel\n amber-gold at hue 80, at the roomiest radius (1rem) beside Peach. Both carry\n the full shadcn-compatible contract, tinted neutrals at the family's chroma,\n and pass the contrast gate at 38/38 required pairs in both modes.\n \n `PACKS` in the theming module and `STORE_PACKS` in the llms corpus carry the\n two new ids, so a consumer's ground-pack validation and every agent reading\n the corpus see the same vocabulary. Peach returns to reserved. The published\n pack count is now seven pastels plus the neutral default.\n\n## 0.16.0\n\n### Minor Changes\n\n- 1a0ba35: A page `h1` is now visibly larger than the `h2` sections under it\n \n A page `h1` rendered at 1.875rem and every section heading under it at 1.5rem, so\n the two were one fifth apart and the page had no top to its hierarchy. The fix is\n in the type scale rather than in the Component, because the scale stopped at `4xl`\n and there was no step for a page's own claim to take.\n \n **Two steps are authored, and they are the two widest gaps in the group.**\n `5xl` is 3rem and `6xl` is 3.75rem, both at a line-height of 1. Every other step\n in the `text` group is a whole multiple of 0.125rem and these two are too;\n `4xl` to `5xl` is four thirds, the first gap in the group wider than any gap it\n already had, and `5xl` to `6xl` is five fourths. They are authored as a pair\n rather than as one step because every rung of the heading ladder is a pair, a base\n step and one step up at `sm`, so a single step above `4xl` would have made the\n page heading the one heading in the system that does not grow at a width.\n \n Both steps reach CSS and the published pack contract the way every other authored\n step does, so `text-5xl` and `text-6xl` are utilities a consumer can compose, and\n neither is a value a consumer has to write.\n \n **The heading ladder now descends from the new ceiling, one authored step per\n level, and every level holds a step of its own.** There are exactly six authored\n steps at or above Body, so the table lands on the floor with a distinct step at\n every rung, where before `h4` through `h6` shared one. `h1` is at `5xl`, rising to\n `6xl` at `sm`; `h2` is at `4xl`, rising to `5xl`; and the table descends to `lg` at\n `h6`, which is the step Body is set at. **What a consumer sees is every section\n title moving up one step and every page heading moving up two**: a section title\n goes from 1.5rem to 2.25rem and from 1.875rem to 3rem at `sm`, and a page `h1` goes\n from 1.875rem to 3rem and from 2.25rem to 3.75rem at `sm`. No call site changes,\n because the level a Block was already passing is still what decides its size.\n \n **Five surfaces that render a page's own `h1` now ask the same table for its size\n rather than naming a step.** `page-header-01`, `docs-shell`, `blog-post-page`,\n `not-found-page` and `error-page` each wrote a fixed step into a heading that was\n meant to be a page's top-level claim, so with the ceiling lifted they would have\n rendered a page heading a rung or two below the section titles under it. They ask\n `headingSizeClass`, which is what `SectionHeading` and `Cta01` already asked, and\n `Cta01` still carries no literal.\n \n **Four more surfaces that resolved their own heading size, found the same way.**\n A Block that takes a `headingLevel` and writes the size beside it forwards the level\n and changes nothing a reader can see, which is the defect above at the scale of one\n Block. `data-table-01` said `text-lg`, `run-console-01` said `text-base`, and both\n moved the outline without moving the size. `run-stream-01` renders a literal `h2`\n at `text-lg`, which was the floor of the old scale, so a live region's `h2` came\n out below the `h3` introducing the section holding it. `/foundation/themes` said\n `text-3xl`, which had been Display and is now the `h3` step, so that page's `h1`\n rendered a rung below the section titles under it. All four ask `headingSizeClass`\n now, and the three Blocks carry `text-balance` with the size they gained: the ladder\n states weight, tracking and balance as one package and `SectionHeading` carries all\n three at every rung, so a title lifted from 16 or 18 pixels to 24 or 36 is worse\n unwrapped than the one it replaced.\n \n **The Item page's appendix heading is on the article's ladder, and asking the Block\n ladder for it was the wrong correction.** It said `text-xl`, which put an `h2` at 20\n pixels under the prose `h2` at 30 and below the prose `h3` at 24 above it. Asking\n `headingSizeClass` removed the third tier and created a worse one: \"API reference\"\n is a section of an article exactly as \"Overview\" is, so it takes the article's\n ladder, and the Block ladder is a step and a half above it at `sm`. The Item page\n then carried three sections at 30 pixels and an appendix at 48, and the appendix was\n the second largest thing on the page. It is now a bare `<h2>` inside the same\n `.prose` wrapper as the Item's own MDX body, so the article's rules size it at 30\n pixels and it carries no class of its own: every section heading in that article is\n a bare tag the article styles, and a heading naming its own step would be the second\n answer to a question the article has already answered. The table stays outside\n `.prose`, whose descendant rules would put its cells at the body step in the muted\n colour, and the gap above it is the article's own 16 pixels rather than a second\n region's 32.\n \n **The five theme names are card titles, and they compose `CardTitle` to say so.**\n They carried `font-medium` and no size, so they rendered at 16 pixels and 500 under\n a 60 pixel `h1`. Asking the ladder for the size would have been wrong: five themes at\n the `h2` step is five 48 pixel headings over five cards on a page whose own claim is\n the `h1` above them. `CardTitle` already holds the answer the library needs: a card\n title sits at body size, and the element and the visual step are separate questions.\n So the size stands, the tag stands at `h2` because the grid draws no section heading\n of its own, and the weight now comes from the one Component that owns it rather than\n from a literal. What a consumer sees is five theme names one step heavier.\n \n **The two Foundation readers took opposite answers, and the reason is worth more\n than either change.** Both render inside an MDX body, which the site wraps in\n `.prose`, and both used to carry literals, which a utility layer lets win over the\n article's own component-layer rule: `/foundation/colors` published an `h2` at 18\n pixels and an `h3` at 14, below the `h3` above them.\n \n `ColorTokens`' four headings are sections. Two divisions of an article and a\n partition under one of them is a structure, not a list of labels, so they name no\n step and the article decides, which is the one copy of the ladder there is.\n \n `TokenBrowser`'s are not sections, and dropping its literals let the wrong authority\n decide them: every group name came out at the prose `h2`, 30 pixels, which is more\n than twice the 14 pixels the navigation rail's own section titles are set at, on the\n page whose whole job is being navigated. Each of those 184 names is a table's name\n and nothing else, so they are captions: `h3` at `text-sm`, in the mono face because a\n token key is machine notation, at the same 14 pixels and 500 as the column heads of\n the table underneath. That is the caption `ApiTable` already renders for a compound\n export's part names on every Item page, so this is the repository's own treatment\n rather than a third one. One heading on the page, \"Semantic contract\", is a division\n of the article rather than a label, and it keeps naming no step.\n \n **`Prose` walks the same ladder from one rung down, and a prose heading therefore\n sits below a Block heading at the same level.** Its child treatments now set `h1`\n at `4xl` and descend one authored step per level to `lg` at `h5` and `h6`,\n which is the same scale and the same floor entered at the rung that fits the\n reading measure: Display is 3rem, and a 42rem column is fourteen of those. The new\n `h5` and `h6` treatments are new, so a document that reached past `h4` before now\n has an answer rather than inheriting nothing. **The consequence is stated rather\n than left to be found.** A Block-composed `h2` is 2.25rem and 3rem above `sm`; a\n prose `h2` is 1.875rem and does not grow. They are 1.2 apart at the base width and\n 1.6 apart above `sm`, where they were 1.25 apart before this change, so a consumer\n who sets a `Prose` section beside a Block section sees two tiers rather than one.\n `DESIGN.md` under Typography, Hierarchy now says so, and says which to reach for.\n `DocsShell` runs `Prose` at `fullWidth`, so the frame decides the width: 35rem of\n article track at a viewport of 1216px or wider, and less below that, which makes\n the case for entering one rung down stronger there rather than weaker.\n \n **`Heading`'s free `size` prop is unchanged, and this is stated rather than\n implied.** `size` is a choice made beside the tag, for a heading the surrounding\n document has already placed and wants attuned, and its largest arm is a step of\n the scale rather than a rung of the ladder. Its two largest arms were not removed\n and no new ones were added, so no consumer's call site changes and there is still\n no second route to a page `h1` from inside the library. `search-page` and\n `status-page` are the two surfaces that take a heading's *tag* from\n `childLevel(headingLevel)` and its size from this prop, and they are left as they\n are: those are labels over a list of results and over a list of incidents, and a\n caption is not a rung.\n \n **`scripts/check-heading-scale.mjs` now holds three things it described but did not\n check.** Its rule 4 claimed a table that jumps is a finding and only tested for a\n table that rises, so a table that skipped a rung passed it: descending, reaching\n the ceiling and flooring at or above Body, and rendering the page `h1` one gap\n above its own `h2`, which is the defect these steps exist to remove. And the\n literal Display step it refused on `Cta01` was written out as `text-3xl` and\n `sm:text-4xl`, which stopped being Display the moment the ceiling moved. Both are\n now read from the authored scale and the shipped table rather than restated, and\n both have a staged fixture in `scripts/__tests__/heading-scale.test.mjs`. The third\n is the Item rule below, which is new rather than a restatement.\n \n **The Item document's own table was the retired ladder, and it now states this\n one.** `apps/site/items/component/layout/section/section.mdx` is the page a reader\n is on when they ask what `as` resolves to, and it listed `h1` at `text-3xl` and\n `h4` through `h6` all at `text-lg`: the table as it was before this change. It now\n states the ladder the Component renders, and the sentence under it about where the\n scale stops has been corrected with it, because a corrected table beside a stale\n paragraph is the same disagreement one row down.\n \n **The gate also reads the Item document now, and that is the third copy of the\n table.** `apps/site/items/component/layout/section/section.mdx` states the ladder\n in its prose, and neither the declaration build nor the corpus reaches it, so when\n the ceiling moved it kept publishing the retired table. The new rule compares it to\n the same `HEADING_SIZE` the JSDoc is compared to, by the same cell-for-cell shape,\n so it names no step of its own; it reads the whole Item rather than a slice, so a\n second table in that prose is a finding; and it fails an Item that states no table\n at all, because a comparison with no rows finds nothing to disagree with. Three\n staged fixtures cover the three cases.\n \n **What the gate still does not hold is written down rather than left to be\n discovered.** `Cta01` is named as a root because it is the one Block whose heading\n cannot be composed from `SectionHeading` at all, so a literal step on it has no\n table to fall back to. Beyond that file the gate cannot go, and the reason is\n stated rather than left to be found: whether a heading a Block renders is a rung of\n the ladder or a caption inside that Block is not decidable from source.\n Forty-eight headings across `packages/ui/src` and `apps/site/src` name a step of\n their own, the eleven rungs among them now ask the table, and the thirty-seven that\n remain are captions. Two surfaces bind a caption to `headingLevel` rather than to\n `childLevel`, so a rule reading the tag cannot sort them, and a root list holding\n the rungs would be a list somebody maintains. That is how the `Cta01`-only rule\n came to hold the one Block already fixed and pass over the four that were not, and\n the count is written into `docs/quality-gates.md` so the next reader starts from it.\n `TokenBrowser` adds one more to that population, which is the point of naming a\n caption's step rather than inheriting it: the scan can see it, and it is below the\n floor by arithmetic.\n \n **The limit of the scan that took that count is now written down too, and it is the\n more useful half of it.** The count came from looking for a literal size utility\n beside a heading tag, and a heading whose size it inherits names nothing: the five\n theme names above carried no size at all, and two scans of exactly that shape passed\n them at 16 pixels and 500. A size that is inherited is a size nobody chose, and \"no\n literal\" is invisible to a scan that looks for literals, so that count has never\n been a count of every heading that names a step, and the number nobody chose is the\n half of it that reached a reader. What the count is good for is the arithmetic: a\n heading it reports is a rung if the step it names is at or above the step Body is set\n at, and a caption if it is below, so the two sort without anybody deciding which is\n which. The remedy for what the scan cannot see is on the surfaces rather than in the\n gate, and it is the one the token inventory now uses: a caption names its own step,\n so it is visible to the scan and it is below the floor by definition.\n \n The bump is a minor in both packages rather than a patch because a new authored\n step is a new public contract: `--text-5xl` and `--text-6xl` with their\n `--line-height` companions enter the emitted CSS and the published pack contract,\n and every heading on every page composed from a Block moves.\n\n## 0.15.0\n\n### Minor Changes\n\n- 6dc7bb8: There is one container namespace, and `max-w-6xl` no longer resolves\n \n **If you wrote `max-w-6xl`, `max-w-2xl`, `max-w-xl`, `max-w-lg`, `max-w-md`,\n `max-w-sm` or `max-w-5xl` against this package, that class now produces no rule at\n all.** Nothing errors and nothing warns: the element simply loses its cap and lays\n out at whatever its parent gives it. Grep your own repository for `max-w-` before\n you upgrade. The replacements are in the list below and every one of them resolves.\n \n The reason is the defect this release ends. `@nanisoft/prism-tokens` authors three\n container widths and Tailwind ships thirteen of its own, and until now both shipped\n in the same stylesheet. Three of Tailwind's were numerically identical to three of\n ours: its largest at 72rem beside `--container-page`, its fifth at 42rem beside\n `--container-measure`, its fourth at 36rem beside `--container-measure-narrow`.\n Nothing rendered differently, so nothing failed and every gate was green. What it\n cost was that one width had two names, and the first retune of an authored token\n would have moved every surface reaching it by one spelling and left every surface\n reaching it by the other exactly where it was. `Section`, `SiteHeader`,\n `SiteFooter`, `ChartCard01`, `DocsShell` and `QuickView01` all used the Tailwind\n spelling of the page column; `Cta01` and `SectionHeading` used the Tailwind\n spellings of the two measures.\n \n The token build now closes the whole namespace, with\n `--container-*: initial` ahead of the authored entries rather than a list of the\n seven steps that happened to be emitted. A wildcard is the load-bearing word: the\n thirteen names it retires are `3xs 2xs xs sm md lg xl 2xl 3xl 4xl 5xl 6xl 7xl`,\n and a list would have been a second list to keep in step with a dependency's\n default theme, which is the event the line exists to survive. Order matters and is\n asserted: Tailwind resolves a theme in source order, and the same declaration\n written after the authored entries clears all eight of ours and ships no container\n at all.\n \n **What to write instead**\n \n | You wrote | Write | Value |\n | --- | --- | --- |\n | `max-w-6xl` | `max-w-page` | 72rem |\n | `max-w-2xl` | `max-w-measure` | 42rem |\n | `max-w-xl` | `max-w-measure-narrow` | 36rem |\n | `max-w-md` on a dialog or an alert | `max-w-overlay-dialog` | 28rem |\n | `max-w-lg` on a form dialog | `max-w-overlay-form` | 32rem |\n | `max-w-xl` on a command palette or a search | `max-w-overlay-palette` | 36rem |\n | `max-w-sm` on a side sheet or a drawer | `max-w-overlay-panel` | 24rem |\n | `max-w-5xl` on a lightbox | `max-w-overlay-media` | 64rem |\n \n **An overlay's width is a property of the kind of surface it is, not of the page\n grid underneath it**, and that is why the five overlay widths are authored rather\n than carried on a Tailwind name. A retune of the reading measure must not move a\n dialog and a retune of the page column must not move either, so the two families\n are named apart inside the one container group. They share a group because\n Tailwind 4's `max-w-*` resolves `--spacing-*` and `--container-*` and nothing\n else, so a maximum width that is to be a token has to live in one of those two\n namespaces or it is a value written in a Component. `overlay-palette` and\n `measure-narrow` are both 36rem; that is two measurements that happen to agree and\n not one measurement with two names, and the token source says so and the emitted\n contract holds it to saying so.\n \n **Nothing is supposed to change where you can see it.** Every migration above\n preserves the exact rendered width, so a page that only consumed this stylesheet\n looks the same before and after. What changes is the next retune: it now moves one\n surface rather than half of them.\n \n **A width that was arithmetic on the spacing base is left alone, deliberately.**\n `max-w-96`, `max-w-72`, `max-w-64`, `max-w-32` and `max-w-20` resolve to\n `calc(var(--spacing) * N)` and never touched the container namespace at all. They\n are how a chart's axis band, a token table's column and a documentation frame are\n sized, where nobody took a decision and naming one would invent it. They stay.\n \n **Two gates now hold this down, because the question has two sides.**\n `scripts/check-elevation-layout.mjs` reads every `w-*` and `max-w-*` name out of\n the class strings and holds it to the container group read out of the token source,\n so a width nobody authored is a finding with a file and a line; it reads\n `apps/site/items` as well as `apps/site/src` and `packages/ui/src`, because that is\n where the documentation Demos live and where the site's own build scans.\n `packages/ui/scripts/check-container-namespace.mjs` reads the built stylesheet and\n holds the artefact: that the theme block declares exactly the authored containers,\n that none of Tailwind's thirteen steps is declared or read, that the close is\n written ahead of the entries rather than after them, and that every container width\n this package writes is emitted as a utility reading its own variable. It reads the\n step list out of the installed `tailwindcss/theme.css` rather than restating it, so\n a dependency that adds a step is covered without an edit. Tailwind's extractor\n reads this package's comments as well as its code, so a retired class name written\n down in a JSDoc block used to emit a utility; there is now a rule about that too.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** A class a consumer wrote stops producing a rule with no\n error, which is breaking by any ordinary reading, and this entry says so plainly.\n `major` in this repository publishes `1.0.0`, and every release to date has been\n `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent slot.\n Publishing 1.0.0 with this change in it would assert an API stability guarantee\n this library has not earned, and it would be the first release in the project's\n history to use the bump at all. The break is real and is the subject of this\n entry; the number it lands on is the line's decision and the line is pre-1.0.\n \n **One collateral repair on the documentation site, recorded here because it is\n why `globals.css` moved.** The site's own build and this package's meet in one\n `utilities` layer, and this package emits a bare `.grid-cols-6` as the base of the\n `lg:` variant four of its Blocks use. That bare rule landed after the site's\n `sm:grid-cols-11`, so every colour ramp on the Foundations page was drawing six\n columns at every width above 640 against a comment beside the class saying\n eleven. Whether `check-utility-cascade.mjs` saw it depended on which order two\n content-hashed CSS chunks sorted in, which this change perturbed and which nothing\n about the tree controls. Neither side is wrong, so the site's variant is restated\n in the `site-variants` layer that already exists for six other collisions. The\n repaired behaviour is on the documentation site only; nothing in this package\n changes.\n- 6dc7bb8: Ship the interface face and its metric-adjusted fallback, and stop the site loading its own\n \n `@nanisoft/prism-ui` shipped three Inter weights and no fallback face, so a reader\n waiting on the web font saw the platform's UI face jump into place when Inter\n arrived. The stylesheet now also ships a local Arial carrying Inter's metrics\n through `size-adjust`, `ascent-override`, `descent-override` and\n `line-gap-override`, and `--font-sans` names it immediately after Inter, so the\n swap window occupies Inter's own line box. The four numbers are measured off the\n shipped face rather than chosen, and they are the ones `next/font` measured for the\n same typeface, so the line box is unchanged from what the documentation site had.\n \n A fourth face ships beside them: Inter italic at weight 400, the Latin subset of\n the same release, covering the same 230 codepoints as its upright siblings.\n `Prose` sets `blockquote` in italic and the surface had been asking for a style no\n shipped file provided, so every consumer has been reading a synthesized oblique.\n One real face covers every weight a browser asks for, because the font matcher\n takes the closest available weight rather than synthesizing once a face exists.\n The whole set is 93.5 KB, up 22.8 KB, and no consumer has to do anything.\n \n **The documentation site no longer loads a face of its own.** It shipped a 723 KiB\n unsubsetted variable Inter and its italic through `next/font/local`, preloaded both\n in all 581 exported documents, and applied the result to `<body>` as a directly set\n `--font-sans`. A directly applied custom property outranks an inherited one, so the\n library's faces were never fetched on the site that documents them: a missing or\n corrupt shipped font rendered perfectly there with every gate green. The site now\n resolves the same stack you do, which is the only way it can be evidence for the\n package. If you were relying on the site to look right while your own copy of Inter\n was broken, that is now a failure you can see.\n\n## 0.10.0\n\n### Minor Changes\n\n- 75434f8: Add a running figure to the system, and the ambient cycle scale that prices it\n \n The system had one law of motion: motion is state feedback, on an 80/160/280\n millisecond scale, and there are no keyframes anywhere. That law was right and\n it held. It also meant a product page could say what its product does and show\n nothing about it, which is what happened to four sites at once.\n \n This adds the second law, and it is a separate law rather than an exception to\n the first. Feedback is a response to something the reader did. A cycle is a\n demonstration of something the system does, and no reader is waiting for it.\n The test for which one a motion is does not require taste: stop the animation\n and ask whether the figure is still true and still legible.\n \n **New Components**\n \n - `PulseGraph` draws a running system: named nodes, the relations between them, a\n marker travelling a rail, and a breathing focus. A node's `lane` says it is a\n stage in a sequence, which draws the rail; a node with no lane is a field.\n `carries` on a relation says the line is carrying something, and draws a head\n at its end so the claim survives motion being off.\n - `PulseSeries` draws a live instrument: columns rising out of a caller-named\n baseline, with a reticle crossing them once per cycle. The values are the\n caller's own and the tallest sets the scale.\n - `SignalField` is a field of marks: the atmosphere a figure is drawn over.\n Decorative by default, still unless asked to drift, and honestly named as the\n one component in the system that is texture.\n \n **`Hero01` gains the band it was missing**\n \n All four sites hand-wrote the same two-column grid, each with its own gap and its\n own breakpoint. It is now the `instrument` slot, and an action naming an `href`\n renders as a real link rather than a button that goes nowhere. The centred form\n is unchanged, so this is additive.\n \n **New tokens**\n \n `ambient` and `ambient-ease` are two closed groups measured in seconds, separate\n from `duration` because folding them in would have made one name mean both 280ms\n and 7.2s. The emitted contract asserts the closed set and fails any cycle under\n one second, because a sub-second cycle is a flicker and no value in the 80 to\n 280ms band could ever have caught that.\n \n **What this does not change**\n \n Nothing is hidden. No ambient rule sets `opacity: 0` and nothing waits for a\n script, an intersection or a timer, so every figure is complete at first paint\n and a consumer needs no exception to enable the `hidden-state` gate. Reduced\n motion is one `animation: none`, and because every resting state is the full\n form, that reader gets the same figure, still. All three are server Components,\n so this adds zero bytes of client JavaScript.\n\n## 0.6.0\n\n### Minor Changes\n\n- 4f9ce77: A server-rendered pack boundary can now be mode-correct\n \n The only dark selector for a pack was `[data-pack=\"<id>\"].dark`, which requires\n the mode class on the same element. A server cannot know the reader's mode, so a\n subtree carrying a second pack rendered in that pack's light values on a dark\n page. The token build now emits the two-member list\n \n ```\n [data-pack=\"<id>\"].dark, .dark [data-pack=\"<id>\"]\n ```\n \n as one rule with one declaration block, for all five packs. The compound half is\n retained, because it is what a dark surface on a light page is and it is a\n capability the package already published.\n \n Consumers get a second change alongside it. `themeAttributes` now takes `mode` as\n optional, because a required one is that same impossibility written into the type:\n the only way to ask for a boundary with no mode class of its own was to pass\n `'light'` as a guess, and the guess is wrong for half of readers. Omitting `mode`\n is the honest spelling of \"wear the mode of the element carrying `.dark`\". The\n change is a widening, so every existing caller still compiles.\n \n The theme-boundary law in `DESIGN.md` is rewritten to match. It previously\n instructed an implementer to put `data-pack` and `class=\"dark\"` on a boundary\n together, which works for a client-applied boundary and cannot work for a\n server-rendered one.\n- 5dcb347: Every colour role now has a contrast row or a stated reason, and there is a brand ink\n \n The contrast gate decided what to measure by a naming convention: any root token ending in\n `-foreground` had to be the first element of a pair. Six of the seven roles that were in no row at\n all could not be reported by that rule, because `chart-1` through `chart-5` and `sidebar-border`\n do not end in `-foreground`, and `sidebar-ring` does not either. A new role therefore defaulted to\n silence. The gate now walks the token source instead of the table, so a role nobody has written\n down is a failure rather than an absence, and the role set is read from `src/semantic/` rather than\n from the pair table, or the walk would be checking the table against itself.\n \n `sidebar-ring` is what the walk immediately found. It had no row at all, and its shipped values\n measured 2.42:1 in the base pack's light mode, 1.73:1 in its dark mode, 2.86:1 in Mint and 2.97:1\n in Sky against the sidebar surface: a focus indicator four of twelve pack and mode combinations\n could not be seen against, reported as compliant because it was not measured. The values moved, not\n the row. The base pack goes from neutral 400 and neutral 700 to neutral 500 in both modes, because\n the base pack's ring is one ramp rather than two; the pastels go from brand 500 to brand 600 in\n light, because the sidebar is neutral 50 rather than neutral 0 and brand 500 is one step short of\n 3:1 in two packs. The dark pastels keep brand 300 at 8.06:1 or better. `sidebar-border` gains an\n advisory row on the sidebar surface with the same judgement `border` and `input` already carry,\n which is how its 1.18:1 to 1.37:1 became a number on the record rather than an unmeasured value.\n \n `chart-1` through `chart-5` are exempted with a reason rather than measured against a ground. A\n series is a graphic object: shadcn consumes the five as the stroke and fill of a series and reads\n its tooltip and legend text from `foreground` and `muted-foreground`, which are each gated against\n their own ground. What binds a five-way set is that no two series resolve to the same value, so\n that is what the gate asserts instead, in every pack and both modes. The measured tightest pair is\n 37/255 in a channel, Peach in dark mode between `chart-1` and `chart-5`. No minimum separation is\n asserted, because a threshold with no standard behind it is a number that gets bent. `radius` is\n exempted as a length rather than a colour, and the exclusion is a declared entry with a reason\n instead of a name filter buried in a loop. Every exemption is printed on every run with the roles\n that share it.\n \n **`brand-ink` is a new semantic role: a brand hue read as text.** Three sites want a brand colour\n they can read on a surface and `primary` cannot serve it, because `primary` is a fill: a pastel\n brand 400 measures 2.20:1 to 2.44:1 on its own pack's page and 1.90:1 to 2.05:1 on the accent\n surface. It is brand 700 in light and brand 200 in dark, in all five packs and the base, and it is\n gated at 4.5:1 against three grounds rather than one: the page, a card, and the accent surface.\n The accent is the ground that decides the step in both directions, because a light ink has to\n survive the pack's own brand 100 and a dark ink has to survive brand 800, and brand 600 and brand\n 400 respectively are the steps that fail each. The measured worst case across all six sources, both\n modes and all three grounds is 5.57:1 in Mint's light mode. The dark step is brand 200 rather than\n the brand 300 the accent ground would have permitted, because brand 300 is `primary` in every\n pack's dark mode and a brand ink that is the brand fill is the confusion the role exists to end. A\n test asserts that `brand-ink` never resolves to the same value as `primary`, `primary-foreground`\n or `foreground` in any source or mode.\n \n **The two values that were already in the contract are the two answers this gate rules out, and\n both are written into `DESIGN.md` as prohibitions with a number attached.** `primary-foreground` is\n the pack's brand 950 step, in both modes and all five packs, published under three further names in\n light mode, and it is the value a reader of the contract reaches for when they want the brand colour\n as text. It clears its own row by 7.39:1 to 11.06:1 and it measures 1.00:1 to 1.82:1 on the dark\n page ground, the dark card and the dark accent surface. In light mode it reads 15.05:1 to 18.05:1\n on the page, so half the modes it looks right and half it disappears, and no row in the contract\n could see the difference. `ring` is the other: the pack's brand hue as a non-text boundary, gated\n at 3:1 against the page ground alone, and it measures 2.66:1 to 4.35:1 on the accent surface in all\n six light-mode sources. Neither was wrong arithmetically. Both are invisible on the ground a brand\n ink lands on, and both now have a role that is measured there.\n \n **A role is measured in both modes or the run fails.** Every colour role, exempt ones included, has\n to resolve to an sRGB value in light and in dark in every pack, so an exemption buys a role freedom\n from a ground and never from a mode. This is what a role present in one mode and absent from the\n other looks like, and the build's own key-set guard does not reach it because it compares light\n against light.\n \n The old `-foreground` name test is gone rather than kept beside the walk. It was never a second\n opinion on coverage, which the walk settles, and its only additional content is that a role named\n `*-foreground` must be the foreground of a row rather than the background of one, which is a claim\n about the shadcn naming convention and belongs to the Contract Rule.\n \n No consumer action is required. `brand-ink` is additive: it is a new custom property, the stylesheet\n gains a `text-brand-ink` utility, and no existing token changed except `sidebar-ring`, whose value\n moved in the base pack and in the light mode of every pastel. A consumer that styled its own sidebar\n focus ring from `--sidebar-ring` will see a more visible ring, which is the point.\n\n## 0.5.1\n\n### Patch Changes\n\n- 2fc6a0a: Retune `muted-foreground` so muted text clears 4.5:1 on the muted surface\n \n The base pack's `muted-foreground` moves from neutral 500 to neutral 600. It\n cleared 4.5:1 on the page background but fell to 4.34:1 on `muted`, where the\n pill, avatar fallback, kbd and tab-list pattern place it. The contrast gate now\n also checks `muted-foreground` on `muted`. The pastel packs were already pinned\n to their neutral 700 and are unchanged.\n\n## 0.5.0\n\nPrism was rebuilt from a fresh repository on a clean break. The token pipeline is\nDTCG 2025.10 compiled with Style Dictionary, the semantic names keep the shadcn\ncontract verbatim, and motion, typography, spacing, shadows, breakpoints and\ncontainers all reach CSS through a single `@theme static` block.\n\nChangesets are appended above this entry for every published change.\n"
|
|
8163
|
+
"text": "# @nanisoft/prism-tokens\n\n## 1.5.0\n\n### Minor Changes\n\n- Add the `aurora` ambient cycle at 24000ms\n \n The new member measures light crossing a ground: the washes a hero stands on.\n It rides `ambient-ease-drift`, holds to the one-second floor with the rest of\n the group, and is admitted into the closed set the emitted-contract gate holds.\n\n## 1.2.0\n\n### Minor Changes\n\n- Two new packs: Cyan and Gold\n \n The family's product map asked for two hues the palette did not carry, and a\n product's ground is a fact about the system rather than about the site that\n wears it, so the hues arrive here instead of as consumer overrides.\n \n `cyan` is the Nexus pack: pastel cyan-blue, generated at hue 215 in OKLCH with\n the same perceptually even lightness ramp as the other five, at the crispest\n radius in the set (0.5rem) beside Sky. `gold` is the AlphaLens pack: pastel\n amber-gold at hue 80, at the roomiest radius (1rem) beside Peach. Both carry\n the full shadcn-compatible contract, tinted neutrals at the family's chroma,\n and pass the contrast gate at 38/38 required pairs in both modes.\n \n `PACKS` in the theming module and `STORE_PACKS` in the llms corpus carry the\n two new ids, so a consumer's ground-pack validation and every agent reading\n the corpus see the same vocabulary. Peach returns to reserved. The published\n pack count is now seven pastels plus the neutral default.\n\n## 0.16.0\n\n### Minor Changes\n\n- 1a0ba35: A page `h1` is now visibly larger than the `h2` sections under it\n \n A page `h1` rendered at 1.875rem and every section heading under it at 1.5rem, so\n the two were one fifth apart and the page had no top to its hierarchy. The fix is\n in the type scale rather than in the Component, because the scale stopped at `4xl`\n and there was no step for a page's own claim to take.\n \n **Two steps are authored, and they are the two widest gaps in the group.**\n `5xl` is 3rem and `6xl` is 3.75rem, both at a line-height of 1. Every other step\n in the `text` group is a whole multiple of 0.125rem and these two are too;\n `4xl` to `5xl` is four thirds, the first gap in the group wider than any gap it\n already had, and `5xl` to `6xl` is five fourths. They are authored as a pair\n rather than as one step because every rung of the heading ladder is a pair, a base\n step and one step up at `sm`, so a single step above `4xl` would have made the\n page heading the one heading in the system that does not grow at a width.\n \n Both steps reach CSS and the published pack contract the way every other authored\n step does, so `text-5xl` and `text-6xl` are utilities a consumer can compose, and\n neither is a value a consumer has to write.\n \n **The heading ladder now descends from the new ceiling, one authored step per\n level, and every level holds a step of its own.** There are exactly six authored\n steps at or above Body, so the table lands on the floor with a distinct step at\n every rung, where before `h4` through `h6` shared one. `h1` is at `5xl`, rising to\n `6xl` at `sm`; `h2` is at `4xl`, rising to `5xl`; and the table descends to `lg` at\n `h6`, which is the step Body is set at. **What a consumer sees is every section\n title moving up one step and every page heading moving up two**: a section title\n goes from 1.5rem to 2.25rem and from 1.875rem to 3rem at `sm`, and a page `h1` goes\n from 1.875rem to 3rem and from 2.25rem to 3.75rem at `sm`. No call site changes,\n because the level a Block was already passing is still what decides its size.\n \n **Five surfaces that render a page's own `h1` now ask the same table for its size\n rather than naming a step.** `page-header-01`, `docs-shell`, `blog-post-page`,\n `not-found-page` and `error-page` each wrote a fixed step into a heading that was\n meant to be a page's top-level claim, so with the ceiling lifted they would have\n rendered a page heading a rung or two below the section titles under it. They ask\n `headingSizeClass`, which is what `SectionHeading` and `Cta01` already asked, and\n `Cta01` still carries no literal.\n \n **Four more surfaces that resolved their own heading size, found the same way.**\n A Block that takes a `headingLevel` and writes the size beside it forwards the level\n and changes nothing a reader can see, which is the defect above at the scale of one\n Block. `data-table-01` said `text-lg`, `run-console-01` said `text-base`, and both\n moved the outline without moving the size. `run-stream-01` renders a literal `h2`\n at `text-lg`, which was the floor of the old scale, so a live region's `h2` came\n out below the `h3` introducing the section holding it. `/foundation/themes` said\n `text-3xl`, which had been Display and is now the `h3` step, so that page's `h1`\n rendered a rung below the section titles under it. All four ask `headingSizeClass`\n now, and the three Blocks carry `text-balance` with the size they gained: the ladder\n states weight, tracking and balance as one package and `SectionHeading` carries all\n three at every rung, so a title lifted from 16 or 18 pixels to 24 or 36 is worse\n unwrapped than the one it replaced.\n \n **The Item page's appendix heading is on the article's ladder, and asking the Block\n ladder for it was the wrong correction.** It said `text-xl`, which put an `h2` at 20\n pixels under the prose `h2` at 30 and below the prose `h3` at 24 above it. Asking\n `headingSizeClass` removed the third tier and created a worse one: \"API reference\"\n is a section of an article exactly as \"Overview\" is, so it takes the article's\n ladder, and the Block ladder is a step and a half above it at `sm`. The Item page\n then carried three sections at 30 pixels and an appendix at 48, and the appendix was\n the second largest thing on the page. It is now a bare `<h2>` inside the same\n `.prose` wrapper as the Item's own MDX body, so the article's rules size it at 30\n pixels and it carries no class of its own: every section heading in that article is\n a bare tag the article styles, and a heading naming its own step would be the second\n answer to a question the article has already answered. The table stays outside\n `.prose`, whose descendant rules would put its cells at the body step in the muted\n colour, and the gap above it is the article's own 16 pixels rather than a second\n region's 32.\n \n **The five theme names are card titles, and they compose `CardTitle` to say so.**\n They carried `font-medium` and no size, so they rendered at 16 pixels and 500 under\n a 60 pixel `h1`. Asking the ladder for the size would have been wrong: five themes at\n the `h2` step is five 48 pixel headings over five cards on a page whose own claim is\n the `h1` above them. `CardTitle` already holds the answer the library needs: a card\n title sits at body size, and the element and the visual step are separate questions.\n So the size stands, the tag stands at `h2` because the grid draws no section heading\n of its own, and the weight now comes from the one Component that owns it rather than\n from a literal. What a consumer sees is five theme names one step heavier.\n \n **The two Foundation readers took opposite answers, and the reason is worth more\n than either change.** Both render inside an MDX body, which the site wraps in\n `.prose`, and both used to carry literals, which a utility layer lets win over the\n article's own component-layer rule: `/foundation/colors` published an `h2` at 18\n pixels and an `h3` at 14, below the `h3` above them.\n \n `ColorTokens`' four headings are sections. Two divisions of an article and a\n partition under one of them is a structure, not a list of labels, so they name no\n step and the article decides, which is the one copy of the ladder there is.\n \n `TokenBrowser`'s are not sections, and dropping its literals let the wrong authority\n decide them: every group name came out at the prose `h2`, 30 pixels, which is more\n than twice the 14 pixels the navigation rail's own section titles are set at, on the\n page whose whole job is being navigated. Each of those 184 names is a table's name\n and nothing else, so they are captions: `h3` at `text-sm`, in the mono face because a\n token key is machine notation, at the same 14 pixels and 500 as the column heads of\n the table underneath. That is the caption `ApiTable` already renders for a compound\n export's part names on every Item page, so this is the repository's own treatment\n rather than a third one. One heading on the page, \"Semantic contract\", is a division\n of the article rather than a label, and it keeps naming no step.\n \n **`Prose` walks the same ladder from one rung down, and a prose heading therefore\n sits below a Block heading at the same level.** Its child treatments now set `h1`\n at `4xl` and descend one authored step per level to `lg` at `h5` and `h6`,\n which is the same scale and the same floor entered at the rung that fits the\n reading measure: Display is 3rem, and a 42rem column is fourteen of those. The new\n `h5` and `h6` treatments are new, so a document that reached past `h4` before now\n has an answer rather than inheriting nothing. **The consequence is stated rather\n than left to be found.** A Block-composed `h2` is 2.25rem and 3rem above `sm`; a\n prose `h2` is 1.875rem and does not grow. They are 1.2 apart at the base width and\n 1.6 apart above `sm`, where they were 1.25 apart before this change, so a consumer\n who sets a `Prose` section beside a Block section sees two tiers rather than one.\n `DESIGN.md` under Typography, Hierarchy now says so, and says which to reach for.\n `DocsShell` runs `Prose` at `fullWidth`, so the frame decides the width: 35rem of\n article track at a viewport of 1216px or wider, and less below that, which makes\n the case for entering one rung down stronger there rather than weaker.\n \n **`Heading`'s free `size` prop is unchanged, and this is stated rather than\n implied.** `size` is a choice made beside the tag, for a heading the surrounding\n document has already placed and wants attuned, and its largest arm is a step of\n the scale rather than a rung of the ladder. Its two largest arms were not removed\n and no new ones were added, so no consumer's call site changes and there is still\n no second route to a page `h1` from inside the library. `search-page` and\n `status-page` are the two surfaces that take a heading's *tag* from\n `childLevel(headingLevel)` and its size from this prop, and they are left as they\n are: those are labels over a list of results and over a list of incidents, and a\n caption is not a rung.\n \n **`scripts/check-heading-scale.mjs` now holds three things it described but did not\n check.** Its rule 4 claimed a table that jumps is a finding and only tested for a\n table that rises, so a table that skipped a rung passed it: descending, reaching\n the ceiling and flooring at or above Body, and rendering the page `h1` one gap\n above its own `h2`, which is the defect these steps exist to remove. And the\n literal Display step it refused on `Cta01` was written out as `text-3xl` and\n `sm:text-4xl`, which stopped being Display the moment the ceiling moved. Both are\n now read from the authored scale and the shipped table rather than restated, and\n both have a staged fixture in `scripts/__tests__/heading-scale.test.mjs`. The third\n is the Item rule below, which is new rather than a restatement.\n \n **The Item document's own table was the retired ladder, and it now states this\n one.** `apps/site/items/component/layout/section/section.mdx` is the page a reader\n is on when they ask what `as` resolves to, and it listed `h1` at `text-3xl` and\n `h4` through `h6` all at `text-lg`: the table as it was before this change. It now\n states the ladder the Component renders, and the sentence under it about where the\n scale stops has been corrected with it, because a corrected table beside a stale\n paragraph is the same disagreement one row down.\n \n **The gate also reads the Item document now, and that is the third copy of the\n table.** `apps/site/items/component/layout/section/section.mdx` states the ladder\n in its prose, and neither the declaration build nor the corpus reaches it, so when\n the ceiling moved it kept publishing the retired table. The new rule compares it to\n the same `HEADING_SIZE` the JSDoc is compared to, by the same cell-for-cell shape,\n so it names no step of its own; it reads the whole Item rather than a slice, so a\n second table in that prose is a finding; and it fails an Item that states no table\n at all, because a comparison with no rows finds nothing to disagree with. Three\n staged fixtures cover the three cases.\n \n **What the gate still does not hold is written down rather than left to be\n discovered.** `Cta01` is named as a root because it is the one Block whose heading\n cannot be composed from `SectionHeading` at all, so a literal step on it has no\n table to fall back to. Beyond that file the gate cannot go, and the reason is\n stated rather than left to be found: whether a heading a Block renders is a rung of\n the ladder or a caption inside that Block is not decidable from source.\n Forty-eight headings across `packages/ui/src` and `apps/site/src` name a step of\n their own, the eleven rungs among them now ask the table, and the thirty-seven that\n remain are captions. Two surfaces bind a caption to `headingLevel` rather than to\n `childLevel`, so a rule reading the tag cannot sort them, and a root list holding\n the rungs would be a list somebody maintains. That is how the `Cta01`-only rule\n came to hold the one Block already fixed and pass over the four that were not, and\n the count is written into `docs/quality-gates.md` so the next reader starts from it.\n `TokenBrowser` adds one more to that population, which is the point of naming a\n caption's step rather than inheriting it: the scan can see it, and it is below the\n floor by arithmetic.\n \n **The limit of the scan that took that count is now written down too, and it is the\n more useful half of it.** The count came from looking for a literal size utility\n beside a heading tag, and a heading whose size it inherits names nothing: the five\n theme names above carried no size at all, and two scans of exactly that shape passed\n them at 16 pixels and 500. A size that is inherited is a size nobody chose, and \"no\n literal\" is invisible to a scan that looks for literals, so that count has never\n been a count of every heading that names a step, and the number nobody chose is the\n half of it that reached a reader. What the count is good for is the arithmetic: a\n heading it reports is a rung if the step it names is at or above the step Body is set\n at, and a caption if it is below, so the two sort without anybody deciding which is\n which. The remedy for what the scan cannot see is on the surfaces rather than in the\n gate, and it is the one the token inventory now uses: a caption names its own step,\n so it is visible to the scan and it is below the floor by definition.\n \n The bump is a minor in both packages rather than a patch because a new authored\n step is a new public contract: `--text-5xl` and `--text-6xl` with their\n `--line-height` companions enter the emitted CSS and the published pack contract,\n and every heading on every page composed from a Block moves.\n\n## 0.15.0\n\n### Minor Changes\n\n- 6dc7bb8: There is one container namespace, and `max-w-6xl` no longer resolves\n \n **If you wrote `max-w-6xl`, `max-w-2xl`, `max-w-xl`, `max-w-lg`, `max-w-md`,\n `max-w-sm` or `max-w-5xl` against this package, that class now produces no rule at\n all.** Nothing errors and nothing warns: the element simply loses its cap and lays\n out at whatever its parent gives it. Grep your own repository for `max-w-` before\n you upgrade. The replacements are in the list below and every one of them resolves.\n \n The reason is the defect this release ends. `@nanisoft/prism-tokens` authors three\n container widths and Tailwind ships thirteen of its own, and until now both shipped\n in the same stylesheet. Three of Tailwind's were numerically identical to three of\n ours: its largest at 72rem beside `--container-page`, its fifth at 42rem beside\n `--container-measure`, its fourth at 36rem beside `--container-measure-narrow`.\n Nothing rendered differently, so nothing failed and every gate was green. What it\n cost was that one width had two names, and the first retune of an authored token\n would have moved every surface reaching it by one spelling and left every surface\n reaching it by the other exactly where it was. `Section`, `SiteHeader`,\n `SiteFooter`, `ChartCard01`, `DocsShell` and `QuickView01` all used the Tailwind\n spelling of the page column; `Cta01` and `SectionHeading` used the Tailwind\n spellings of the two measures.\n \n The token build now closes the whole namespace, with\n `--container-*: initial` ahead of the authored entries rather than a list of the\n seven steps that happened to be emitted. A wildcard is the load-bearing word: the\n thirteen names it retires are `3xs 2xs xs sm md lg xl 2xl 3xl 4xl 5xl 6xl 7xl`,\n and a list would have been a second list to keep in step with a dependency's\n default theme, which is the event the line exists to survive. Order matters and is\n asserted: Tailwind resolves a theme in source order, and the same declaration\n written after the authored entries clears all eight of ours and ships no container\n at all.\n \n **What to write instead**\n \n | You wrote | Write | Value |\n | --- | --- | --- |\n | `max-w-6xl` | `max-w-page` | 72rem |\n | `max-w-2xl` | `max-w-measure` | 42rem |\n | `max-w-xl` | `max-w-measure-narrow` | 36rem |\n | `max-w-md` on a dialog or an alert | `max-w-overlay-dialog` | 28rem |\n | `max-w-lg` on a form dialog | `max-w-overlay-form` | 32rem |\n | `max-w-xl` on a command palette or a search | `max-w-overlay-palette` | 36rem |\n | `max-w-sm` on a side sheet or a drawer | `max-w-overlay-panel` | 24rem |\n | `max-w-5xl` on a lightbox | `max-w-overlay-media` | 64rem |\n \n **An overlay's width is a property of the kind of surface it is, not of the page\n grid underneath it**, and that is why the five overlay widths are authored rather\n than carried on a Tailwind name. A retune of the reading measure must not move a\n dialog and a retune of the page column must not move either, so the two families\n are named apart inside the one container group. They share a group because\n Tailwind 4's `max-w-*` resolves `--spacing-*` and `--container-*` and nothing\n else, so a maximum width that is to be a token has to live in one of those two\n namespaces or it is a value written in a Component. `overlay-palette` and\n `measure-narrow` are both 36rem; that is two measurements that happen to agree and\n not one measurement with two names, and the token source says so and the emitted\n contract holds it to saying so.\n \n **Nothing is supposed to change where you can see it.** Every migration above\n preserves the exact rendered width, so a page that only consumed this stylesheet\n looks the same before and after. What changes is the next retune: it now moves one\n surface rather than half of them.\n \n **A width that was arithmetic on the spacing base is left alone, deliberately.**\n `max-w-96`, `max-w-72`, `max-w-64`, `max-w-32` and `max-w-20` resolve to\n `calc(var(--spacing) * N)` and never touched the container namespace at all. They\n are how a chart's axis band, a token table's column and a documentation frame are\n sized, where nobody took a decision and naming one would invent it. They stay.\n \n **Two gates now hold this down, because the question has two sides.**\n `scripts/check-elevation-layout.mjs` reads every `w-*` and `max-w-*` name out of\n the class strings and holds it to the container group read out of the token source,\n so a width nobody authored is a finding with a file and a line; it reads\n `apps/site/items` as well as `apps/site/src` and `packages/ui/src`, because that is\n where the documentation Demos live and where the site's own build scans.\n `packages/ui/scripts/check-container-namespace.mjs` reads the built stylesheet and\n holds the artefact: that the theme block declares exactly the authored containers,\n that none of Tailwind's thirteen steps is declared or read, that the close is\n written ahead of the entries rather than after them, and that every container width\n this package writes is emitted as a utility reading its own variable. It reads the\n step list out of the installed `tailwindcss/theme.css` rather than restating it, so\n a dependency that adds a step is covered without an edit. Tailwind's extractor\n reads this package's comments as well as its code, so a retired class name written\n down in a JSDoc block used to emit a utility; there is now a rule about that too.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** A class a consumer wrote stops producing a rule with no\n error, which is breaking by any ordinary reading, and this entry says so plainly.\n `major` in this repository publishes `1.0.0`, and every release to date has been\n `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent slot.\n Publishing 1.0.0 with this change in it would assert an API stability guarantee\n this library has not earned, and it would be the first release in the project's\n history to use the bump at all. The break is real and is the subject of this\n entry; the number it lands on is the line's decision and the line is pre-1.0.\n \n **One collateral repair on the documentation site, recorded here because it is\n why `globals.css` moved.** The site's own build and this package's meet in one\n `utilities` layer, and this package emits a bare `.grid-cols-6` as the base of the\n `lg:` variant four of its Blocks use. That bare rule landed after the site's\n `sm:grid-cols-11`, so every colour ramp on the Foundations page was drawing six\n columns at every width above 640 against a comment beside the class saying\n eleven. Whether `check-utility-cascade.mjs` saw it depended on which order two\n content-hashed CSS chunks sorted in, which this change perturbed and which nothing\n about the tree controls. Neither side is wrong, so the site's variant is restated\n in the `site-variants` layer that already exists for six other collisions. The\n repaired behaviour is on the documentation site only; nothing in this package\n changes.\n- 6dc7bb8: Ship the interface face and its metric-adjusted fallback, and stop the site loading its own\n \n `@nanisoft/prism-ui` shipped three Inter weights and no fallback face, so a reader\n waiting on the web font saw the platform's UI face jump into place when Inter\n arrived. The stylesheet now also ships a local Arial carrying Inter's metrics\n through `size-adjust`, `ascent-override`, `descent-override` and\n `line-gap-override`, and `--font-sans` names it immediately after Inter, so the\n swap window occupies Inter's own line box. The four numbers are measured off the\n shipped face rather than chosen, and they are the ones `next/font` measured for the\n same typeface, so the line box is unchanged from what the documentation site had.\n \n A fourth face ships beside them: Inter italic at weight 400, the Latin subset of\n the same release, covering the same 230 codepoints as its upright siblings.\n `Prose` sets `blockquote` in italic and the surface had been asking for a style no\n shipped file provided, so every consumer has been reading a synthesized oblique.\n One real face covers every weight a browser asks for, because the font matcher\n takes the closest available weight rather than synthesizing once a face exists.\n The whole set is 93.5 KB, up 22.8 KB, and no consumer has to do anything.\n \n **The documentation site no longer loads a face of its own.** It shipped a 723 KiB\n unsubsetted variable Inter and its italic through `next/font/local`, preloaded both\n in all 581 exported documents, and applied the result to `<body>` as a directly set\n `--font-sans`. A directly applied custom property outranks an inherited one, so the\n library's faces were never fetched on the site that documents them: a missing or\n corrupt shipped font rendered perfectly there with every gate green. The site now\n resolves the same stack you do, which is the only way it can be evidence for the\n package. If you were relying on the site to look right while your own copy of Inter\n was broken, that is now a failure you can see.\n\n## 0.10.0\n\n### Minor Changes\n\n- 75434f8: Add a running figure to the system, and the ambient cycle scale that prices it\n \n The system had one law of motion: motion is state feedback, on an 80/160/280\n millisecond scale, and there are no keyframes anywhere. That law was right and\n it held. It also meant a product page could say what its product does and show\n nothing about it, which is what happened to four sites at once.\n \n This adds the second law, and it is a separate law rather than an exception to\n the first. Feedback is a response to something the reader did. A cycle is a\n demonstration of something the system does, and no reader is waiting for it.\n The test for which one a motion is does not require taste: stop the animation\n and ask whether the figure is still true and still legible.\n \n **New Components**\n \n - `PulseGraph` draws a running system: named nodes, the relations between them, a\n marker travelling a rail, and a breathing focus. A node's `lane` says it is a\n stage in a sequence, which draws the rail; a node with no lane is a field.\n `carries` on a relation says the line is carrying something, and draws a head\n at its end so the claim survives motion being off.\n - `PulseSeries` draws a live instrument: columns rising out of a caller-named\n baseline, with a reticle crossing them once per cycle. The values are the\n caller's own and the tallest sets the scale.\n - `SignalField` is a field of marks: the atmosphere a figure is drawn over.\n Decorative by default, still unless asked to drift, and honestly named as the\n one component in the system that is texture.\n \n **`Hero01` gains the band it was missing**\n \n All four sites hand-wrote the same two-column grid, each with its own gap and its\n own breakpoint. It is now the `instrument` slot, and an action naming an `href`\n renders as a real link rather than a button that goes nowhere. The centred form\n is unchanged, so this is additive.\n \n **New tokens**\n \n `ambient` and `ambient-ease` are two closed groups measured in seconds, separate\n from `duration` because folding them in would have made one name mean both 280ms\n and 7.2s. The emitted contract asserts the closed set and fails any cycle under\n one second, because a sub-second cycle is a flicker and no value in the 80 to\n 280ms band could ever have caught that.\n \n **What this does not change**\n \n Nothing is hidden. No ambient rule sets `opacity: 0` and nothing waits for a\n script, an intersection or a timer, so every figure is complete at first paint\n and a consumer needs no exception to enable the `hidden-state` gate. Reduced\n motion is one `animation: none`, and because every resting state is the full\n form, that reader gets the same figure, still. All three are server Components,\n so this adds zero bytes of client JavaScript.\n\n## 0.6.0\n\n### Minor Changes\n\n- 4f9ce77: A server-rendered pack boundary can now be mode-correct\n \n The only dark selector for a pack was `[data-pack=\"<id>\"].dark`, which requires\n the mode class on the same element. A server cannot know the reader's mode, so a\n subtree carrying a second pack rendered in that pack's light values on a dark\n page. The token build now emits the two-member list\n \n ```\n [data-pack=\"<id>\"].dark, .dark [data-pack=\"<id>\"]\n ```\n \n as one rule with one declaration block, for all five packs. The compound half is\n retained, because it is what a dark surface on a light page is and it is a\n capability the package already published.\n \n Consumers get a second change alongside it. `themeAttributes` now takes `mode` as\n optional, because a required one is that same impossibility written into the type:\n the only way to ask for a boundary with no mode class of its own was to pass\n `'light'` as a guess, and the guess is wrong for half of readers. Omitting `mode`\n is the honest spelling of \"wear the mode of the element carrying `.dark`\". The\n change is a widening, so every existing caller still compiles.\n \n The theme-boundary law in `DESIGN.md` is rewritten to match. It previously\n instructed an implementer to put `data-pack` and `class=\"dark\"` on a boundary\n together, which works for a client-applied boundary and cannot work for a\n server-rendered one.\n- 5dcb347: Every colour role now has a contrast row or a stated reason, and there is a brand ink\n \n The contrast gate decided what to measure by a naming convention: any root token ending in\n `-foreground` had to be the first element of a pair. Six of the seven roles that were in no row at\n all could not be reported by that rule, because `chart-1` through `chart-5` and `sidebar-border`\n do not end in `-foreground`, and `sidebar-ring` does not either. A new role therefore defaulted to\n silence. The gate now walks the token source instead of the table, so a role nobody has written\n down is a failure rather than an absence, and the role set is read from `src/semantic/` rather than\n from the pair table, or the walk would be checking the table against itself.\n \n `sidebar-ring` is what the walk immediately found. It had no row at all, and its shipped values\n measured 2.42:1 in the base pack's light mode, 1.73:1 in its dark mode, 2.86:1 in Mint and 2.97:1\n in Sky against the sidebar surface: a focus indicator four of twelve pack and mode combinations\n could not be seen against, reported as compliant because it was not measured. The values moved, not\n the row. The base pack goes from neutral 400 and neutral 700 to neutral 500 in both modes, because\n the base pack's ring is one ramp rather than two; the pastels go from brand 500 to brand 600 in\n light, because the sidebar is neutral 50 rather than neutral 0 and brand 500 is one step short of\n 3:1 in two packs. The dark pastels keep brand 300 at 8.06:1 or better. `sidebar-border` gains an\n advisory row on the sidebar surface with the same judgement `border` and `input` already carry,\n which is how its 1.18:1 to 1.37:1 became a number on the record rather than an unmeasured value.\n \n `chart-1` through `chart-5` are exempted with a reason rather than measured against a ground. A\n series is a graphic object: shadcn consumes the five as the stroke and fill of a series and reads\n its tooltip and legend text from `foreground` and `muted-foreground`, which are each gated against\n their own ground. What binds a five-way set is that no two series resolve to the same value, so\n that is what the gate asserts instead, in every pack and both modes. The measured tightest pair is\n 37/255 in a channel, Peach in dark mode between `chart-1` and `chart-5`. No minimum separation is\n asserted, because a threshold with no standard behind it is a number that gets bent. `radius` is\n exempted as a length rather than a colour, and the exclusion is a declared entry with a reason\n instead of a name filter buried in a loop. Every exemption is printed on every run with the roles\n that share it.\n \n **`brand-ink` is a new semantic role: a brand hue read as text.** Three sites want a brand colour\n they can read on a surface and `primary` cannot serve it, because `primary` is a fill: a pastel\n brand 400 measures 2.20:1 to 2.44:1 on its own pack's page and 1.90:1 to 2.05:1 on the accent\n surface. It is brand 700 in light and brand 200 in dark, in all five packs and the base, and it is\n gated at 4.5:1 against three grounds rather than one: the page, a card, and the accent surface.\n The accent is the ground that decides the step in both directions, because a light ink has to\n survive the pack's own brand 100 and a dark ink has to survive brand 800, and brand 600 and brand\n 400 respectively are the steps that fail each. The measured worst case across all six sources, both\n modes and all three grounds is 5.57:1 in Mint's light mode. The dark step is brand 200 rather than\n the brand 300 the accent ground would have permitted, because brand 300 is `primary` in every\n pack's dark mode and a brand ink that is the brand fill is the confusion the role exists to end. A\n test asserts that `brand-ink` never resolves to the same value as `primary`, `primary-foreground`\n or `foreground` in any source or mode.\n \n **The two values that were already in the contract are the two answers this gate rules out, and\n both are written into `DESIGN.md` as prohibitions with a number attached.** `primary-foreground` is\n the pack's brand 950 step, in both modes and all five packs, published under three further names in\n light mode, and it is the value a reader of the contract reaches for when they want the brand colour\n as text. It clears its own row by 7.39:1 to 11.06:1 and it measures 1.00:1 to 1.82:1 on the dark\n page ground, the dark card and the dark accent surface. In light mode it reads 15.05:1 to 18.05:1\n on the page, so half the modes it looks right and half it disappears, and no row in the contract\n could see the difference. `ring` is the other: the pack's brand hue as a non-text boundary, gated\n at 3:1 against the page ground alone, and it measures 2.66:1 to 4.35:1 on the accent surface in all\n six light-mode sources. Neither was wrong arithmetically. Both are invisible on the ground a brand\n ink lands on, and both now have a role that is measured there.\n \n **A role is measured in both modes or the run fails.** Every colour role, exempt ones included, has\n to resolve to an sRGB value in light and in dark in every pack, so an exemption buys a role freedom\n from a ground and never from a mode. This is what a role present in one mode and absent from the\n other looks like, and the build's own key-set guard does not reach it because it compares light\n against light.\n \n The old `-foreground` name test is gone rather than kept beside the walk. It was never a second\n opinion on coverage, which the walk settles, and its only additional content is that a role named\n `*-foreground` must be the foreground of a row rather than the background of one, which is a claim\n about the shadcn naming convention and belongs to the Contract Rule.\n \n No consumer action is required. `brand-ink` is additive: it is a new custom property, the stylesheet\n gains a `text-brand-ink` utility, and no existing token changed except `sidebar-ring`, whose value\n moved in the base pack and in the light mode of every pastel. A consumer that styled its own sidebar\n focus ring from `--sidebar-ring` will see a more visible ring, which is the point.\n\n## 0.5.1\n\n### Patch Changes\n\n- 2fc6a0a: Retune `muted-foreground` so muted text clears 4.5:1 on the muted surface\n \n The base pack's `muted-foreground` moves from neutral 500 to neutral 600. It\n cleared 4.5:1 on the page background but fell to 4.34:1 on `muted`, where the\n pill, avatar fallback, kbd and tab-list pattern place it. The contrast gate now\n also checks `muted-foreground` on `muted`. The pastel packs were already pinned\n to their neutral 700 and are unchanged.\n\n## 0.5.0\n\nPrism was rebuilt from a fresh repository on a clean break. The token pipeline is\nDTCG 2025.10 compiled with Style Dictionary, the semantic names keep the shadcn\ncontract verbatim, and motion, typography, spacing, shadows, breakpoints and\ncontainers all reach CSS through a single `@theme static` block.\n\nChangesets are appended above this entry for every published change.\n"
|
|
8044
8164
|
},
|
|
8045
8165
|
{
|
|
8046
8166
|
"package": "@nanisoft/prism-ui",
|
|
@@ -8048,6 +8168,8 @@
|
|
|
8048
8168
|
"route": "/changelogs/prism-ui",
|
|
8049
8169
|
"title": "@nanisoft/prism-ui",
|
|
8050
8170
|
"versions": [
|
|
8171
|
+
"1.5.0",
|
|
8172
|
+
"1.4.0",
|
|
8051
8173
|
"1.3.0",
|
|
8052
8174
|
"1.2.0",
|
|
8053
8175
|
"1.1.0",
|
|
@@ -8070,6 +8192,14 @@
|
|
|
8070
8192
|
"0.5.0"
|
|
8071
8193
|
],
|
|
8072
8194
|
"releases": [
|
|
8195
|
+
{
|
|
8196
|
+
"version": "1.5.0",
|
|
8197
|
+
"body": "### Minor Changes\n\n- Add the third law of motion: arrivals, living fields, and the reading rail\n \n Motion now has three laws. Feedback and the ambient cycles are unchanged; the\n new law admits three authored mechanisms, each shipped as primitives rather\n than opened as a licence:\n \n - `Reveal` wraps a band in a scroll-driven arrival bound to the reader's own\n scroll through `prism-reveal` utilities behind a view-timeline capability\n check. No script arms it, a browser without the timeline ships no rule at\n all, and the universal reduced-motion rule hands that reader the visible\n page.\n - `ParticleField` is the one script-driven figure: a linked, pointer-answering\n medium whose complete still field renders on the server, whose loop stops off\n screen, and whose palette is re-read when the document's pack or mode\n changes. `AuroraWash` is the still counterpart: translucent planes in the\n pack's own colours on the new `aurora` cycle, transform-only.\n - `SiteNavbar` gains a reading rail on its sticky hairline, `scrollRail` on by\n default: a spectrum line filling with the page's own scroll, drawing nothing\n in a browser that cannot run the timeline. The current link's filled pill\n becomes a persistent spectrum underline, and `Hero01` gains a `backdrop` slot\n so a hero composes its own ground.\n \n `Backdrop01` offers `field=\"aurora\"`, and `prism-text-spectrum` is the one\n admitted motion on text: display ink lit at the sweep tempo with foreground\n ends, so it holds contrast in every pack and both modes.\n \n `scripts/check-motion.mjs` holds the new layer: a reveal or rail selector\n outside the `@supports` block naming its timeline fails the build, because an\n animation whose timeline fails to resolve pauses at its invisible keyframe.\n\n### Patch Changes\n\n- Updated dependencies\n - @nanisoft/prism-tokens@1.5.0"
|
|
8198
|
+
},
|
|
8199
|
+
{
|
|
8200
|
+
"version": "1.4.0",
|
|
8201
|
+
"body": "### Minor Changes\n\n- 3f9cc78: Add `MediaBand01`, an editorial band with a caller-owned media slot, and a `size`\n prop on `Section` for compact, default and tall vertical rhythms.\n \n The band owns the frame, the ratio, the surface and the order, and it never puts\n the copy on top of an arbitrary photograph. The media is the caller's node, so an\n image, a video poster or a composed figure can sit in the frame. The Section size\n keeps the existing default byte-identical and gives a page three authored rhythms\n instead of five site stylesheets inventing their own."
|
|
8202
|
+
},
|
|
8073
8203
|
{
|
|
8074
8204
|
"version": "1.3.0",
|
|
8075
8205
|
"body": "### Minor Changes\n\n- 250b34e: SiteFooter takes an `about` slot: a word from the site under the mark, for the\n sentence or two a footer carries about what the site is and who publishes it.\n Rendered under the lockup at the measure with no colour of its own, so the\n muted treatment and any headings stay the caller's. The design system's own\n site composes this footer now, and the two labelled facts its hand-written\n footer carried for years ride in that slot.\n- 290642e: Let `SiteNavbar`'s `nav` carry groups, rendered as one trigger that opens a menu\n \n A `nav` entry may now be a group: `{ label, children }` beside the existing\n link shape. On the row a group renders as the row's one control, a trigger\n whose menu lists its children as real anchors, marked with `aria-current` when\n the reader is on one; below the row's threshold the panel lists the same group\n flat, as a label above its links, so no destination is two disclosures deep.\n The current mark now reads the route as a place rather than a string, so a\n reader on `/components/button` finds `Components` marked rather than nothing.\n A flat `nav` renders exactly as before, and the new `SiteNavGroup` and\n `SiteNavEntry` types are exported beside `SiteNavLink`.\n- 8265662: A `ProductMark` takes a `mark`: the product's own drawing, in place of the\n disc. The node is the consumer's and fills the sized slot the disc occupies,\n so one drawing arrives at every drawn size; the name beside it is unchanged\n and still the announced half. `SiteNavbar` and `SiteFooter` carry it through\n the `product` prop they already take, so a site's logo is the sign in the\n bar, in the mobile panel and at the foot of the page from one fact, and the\n two-colour disc remains the mark of a product whose identity is a hue. The\n design system's own site draws its beam-and-refraction in both places now,\n in place of the spectrum disc.\n\n### Patch Changes\n\n- 290642e: Render `Sparkline`'s hidden numbers table without the `Table` Component's scroll container\n \n The readings behind a `Sparkline` are published to assistive technology as a\n table in a visually hidden wrapper. The wrapper is one pixel wide, so the\n horizontal scroll container `Table` wraps its table in always computed\n scrollable and never focusable, which the `scrollable-region-focusable` rule\n fails on wherever a `Sparkline` renders. The hidden table is now plain `table`\n markup with the same caption, row heads and cells: a table nobody can see\n cannot be panned, and the semantics are the whole of what it owes.\n- 290642e: Lay `Calendar`'s month grid as a table again: the `flex` came off its cells\n \n The weekday headings and the day cells carried `display: flex`, and a flexed\n `th` or `td` is pulled out of the table's column layout: the browser wraps each\n one in its own anonymous cell, and the seven-column month collapsed into a\n single stacked column at four times its height. In `Calendar` the day cell is\n the `td` itself, so the whole grid fell; in `MiniCalendar` the day is a button\n inside the cell, so the weekday row alone stacked above a surviving grid, and\n `DatePicker` and the `Calendar01` Block inherit whichever they compose. The\n cells are centred by the table instead (`text-center` with `align-middle` and\n `p-0`), which keeps the seven columns, the 36px cell, the 44px coarse-pointer\n floor and every state exactly as they were designed, and the weekday heading\n now sits over the column it names.\n- 290642e: Align `PulseSeries`'s scan reticle with the columns it reads\n \n The wash behind the reticle stopped short of the baseline while its own line\n crossed it, and the line rode inside the wash at a third of its width rather\n than at its centre. The wash is now one column slot wide and the line runs\n down its centre, so at every instant the wash covers exactly the column the\n line is crossing, and both shapes meet the baseline. The travel is the plot's\n own width, so the reticle enters and leaves aligned with the columns' field\n and its opacity ramps are spent on the margins beside the end columns."
|
|
@@ -8151,7 +8281,7 @@
|
|
|
8151
8281
|
"body": "Prism was rebuilt from a fresh repository on a clean break. The package is now a\npublished React library a consumer installs and composes, not component source to\ncopy. It ships one precompiled `styles.css`, the optional `PrismProvider` and the\n`data-pack` plus `dark` theme axes, and the Components, Blocks and Pages\ntaxonomy. There is no override path.\n\nChangesets are appended above this entry for every published change."
|
|
8152
8282
|
}
|
|
8153
8283
|
],
|
|
8154
|
-
"text": "# @nanisoft/prism-ui\n\n## 1.3.0\n\n### Minor Changes\n\n- 250b34e: SiteFooter takes an `about` slot: a word from the site under the mark, for the\n sentence or two a footer carries about what the site is and who publishes it.\n Rendered under the lockup at the measure with no colour of its own, so the\n muted treatment and any headings stay the caller's. The design system's own\n site composes this footer now, and the two labelled facts its hand-written\n footer carried for years ride in that slot.\n- 290642e: Let `SiteNavbar`'s `nav` carry groups, rendered as one trigger that opens a menu\n \n A `nav` entry may now be a group: `{ label, children }` beside the existing\n link shape. On the row a group renders as the row's one control, a trigger\n whose menu lists its children as real anchors, marked with `aria-current` when\n the reader is on one; below the row's threshold the panel lists the same group\n flat, as a label above its links, so no destination is two disclosures deep.\n The current mark now reads the route as a place rather than a string, so a\n reader on `/components/button` finds `Components` marked rather than nothing.\n A flat `nav` renders exactly as before, and the new `SiteNavGroup` and\n `SiteNavEntry` types are exported beside `SiteNavLink`.\n- 8265662: A `ProductMark` takes a `mark`: the product's own drawing, in place of the\n disc. The node is the consumer's and fills the sized slot the disc occupies,\n so one drawing arrives at every drawn size; the name beside it is unchanged\n and still the announced half. `SiteNavbar` and `SiteFooter` carry it through\n the `product` prop they already take, so a site's logo is the sign in the\n bar, in the mobile panel and at the foot of the page from one fact, and the\n two-colour disc remains the mark of a product whose identity is a hue. The\n design system's own site draws its beam-and-refraction in both places now,\n in place of the spectrum disc.\n\n### Patch Changes\n\n- 290642e: Render `Sparkline`'s hidden numbers table without the `Table` Component's scroll container\n \n The readings behind a `Sparkline` are published to assistive technology as a\n table in a visually hidden wrapper. The wrapper is one pixel wide, so the\n horizontal scroll container `Table` wraps its table in always computed\n scrollable and never focusable, which the `scrollable-region-focusable` rule\n fails on wherever a `Sparkline` renders. The hidden table is now plain `table`\n markup with the same caption, row heads and cells: a table nobody can see\n cannot be panned, and the semantics are the whole of what it owes.\n- 290642e: Lay `Calendar`'s month grid as a table again: the `flex` came off its cells\n \n The weekday headings and the day cells carried `display: flex`, and a flexed\n `th` or `td` is pulled out of the table's column layout: the browser wraps each\n one in its own anonymous cell, and the seven-column month collapsed into a\n single stacked column at four times its height. In `Calendar` the day cell is\n the `td` itself, so the whole grid fell; in `MiniCalendar` the day is a button\n inside the cell, so the weekday row alone stacked above a surviving grid, and\n `DatePicker` and the `Calendar01` Block inherit whichever they compose. The\n cells are centred by the table instead (`text-center` with `align-middle` and\n `p-0`), which keeps the seven columns, the 36px cell, the 44px coarse-pointer\n floor and every state exactly as they were designed, and the weekday heading\n now sits over the column it names.\n- 290642e: Align `PulseSeries`'s scan reticle with the columns it reads\n \n The wash behind the reticle stopped short of the baseline while its own line\n crossed it, and the line rode inside the wash at a third of its width rather\n than at its centre. The wash is now one column slot wide and the line runs\n down its centre, so at every instant the wash covers exactly the column the\n line is crossing, and both shapes meet the baseline. The travel is the plot's\n own width, so the reticle enters and leaves aligned with the columns' field\n and its opacity ramps are spent on the margins beside the end columns.\n\n## 1.2.0\n\n### Minor Changes\n\n- Two new packs: Cyan and Gold\n \n The family's product map asked for two hues the palette did not carry, and a\n product's ground is a fact about the system rather than about the site that\n wears it, so the hues arrive here instead of as consumer overrides.\n \n `cyan` is the Nexus pack: pastel cyan-blue, generated at hue 215 in OKLCH with\n the same perceptually even lightness ramp as the other five, at the crispest\n radius in the set (0.5rem) beside Sky. `gold` is the AlphaLens pack: pastel\n amber-gold at hue 80, at the roomiest radius (1rem) beside Peach. Both carry\n the full shadcn-compatible contract, tinted neutrals at the family's chroma,\n and pass the contrast gate at 38/38 required pairs in both modes.\n \n `PACKS` in the theming module and `STORE_PACKS` in the llms corpus carry the\n two new ids, so a consumer's ground-pack validation and every agent reading\n the corpus see the same vocabulary. Peach returns to reserved. The published\n pack count is now seven pastels plus the neutral default.\n\n### Patch Changes\n\n- Updated dependencies\n - @nanisoft/prism-tokens@1.2.0\n\n## 1.1.0\n\n### Minor Changes\n\n- ad69fa2: A message carries the caller's per-message metadata\n \n `ChatMessage` gains an optional `meta: ReactNode`, drawn in the message's header\n beside the sender and the time. It carries the caller's own per-message metadata:\n the model a provider stated for a turn, a token count it reported, and the cost or\n the latency the caller derived. It is a node and not a set of Prism fields because\n no provider read publishes a cost or a latency, so the surface states no model, no\n count, no cost and no latency for itself and draws only what the caller hands it.\n The parts stay the way content is carried. The roster does not change: no new Item,\n Kind, Page or catalogue entry appears.\n- ad69fa2: The date view takes a per-item move control\n \n `Calendar01` exposes a per-item `handle`, a node the caller fills with their own\n control and wires to whatever owns their schedule. It exists for the one\n interaction a date view has that the Block does not own: a reschedule, which is a\n drag with a drop target, a new order, a write and a conflict, every one of them the\n consumer's. The Block places the node at the leading edge of the item's row and\n owns nothing beyond it: no drag, no drop, no order and no move callback, which is\n the arrangement `Kanban01` already takes for a board's drag.\n \n A `CalendarItem` gains an optional `handle: ReactNode`; `Calendar01`'s props are\n otherwise unchanged. The roster does not change: no new Item, Kind, Page or\n catalogue entry appears.\n- ad69fa2: The index gains a picture arrangement and a write grid\n \n Two coverage gaps in the record-index territory are closed by authoring two Blocks\n rather than by widening a union, and each is the arrangement `DESIGN.md` already\n named as forcing a second Item.\n \n **`CardIndex01` is the record index as pictures.** It draws a set of records as a\n grid of cards rather than as rows, with the caller composing each card's picture,\n name and price through `renderCard` and its per card actions through\n `renderCardActions`. It shares the record index's selection and announcement\n contract, which ticket 166 made the law for the index family: the same `selectable`\n union, the same held set of keys, the same three-state header control, the same\n single polite live region carrying a count, the same batch bar around the caller's\n own `batchActions` node, and the same `selectionScope` distinguishing a page-wide\n selection from a filter-wide one. It fetches, sorts, filters and slices nothing.\n \n **`RecordGrid01` is a grid edited in place.** Its columns are the shared\n `FieldSpec`, because a cell that writes is a control and the only vocabulary this\n package publishes for a control is the field specification; `ColumnKind` is the\n vocabulary for a value a reader reads and is held disjoint from it. The Block seeds\n each cell from the caller's row, holds the in-progress values so a cell redraws as\n it is typed, and reports every edit through a required `onCellChange` as the row\n identity and the column key. It fetches nothing, sorts nothing and pages nothing,\n and it owns no command.\n \n **Why `ColumnKind` was not widened.** The obvious alternative was an editable cell\n arm on `ColumnKind` plus a change callback. That arm would name a field Component\n (`Input`, `Select` and their kin), which is a member of `FieldKind`, and\n `scripts/check-spec-unions.mjs` reports it as an `overlapping-union` between the\n read union and the write union, in the same run that would otherwise pass. The\n disjointness the gate protects is exactly the fact that a cell that writes is a\n different job from a cell that reads, so the answer is a new Item that takes the\n field union rather than a wider column union.\n- ad69fa2: The record detail draws a dated trail as one of its relations\n \n `RelationKind` gains the `ActivityFeed01` arm, so a record's event history is a\n first-class relation drawn over `EventSpec` values rather than the generic `slot`\n arm. The union previously offered only `Timeline`, whose `TimelineEntry` carries a\n `duration` and a `state` and no `at`, so a record's dated occurrences had no\n relation to draw them. A `Timeline` is an instrument and a trail is not, so the fix\n is a new arm and not a moment on `TimelineEntry`: one Component carrying both a\n moment and a duration would be a second answer to a question that already has one.\n \n `RecordDetail01` draws the arm with the shared `ActivityFeed01` Block, exactly as\n it draws the other arms, and its relations are unchanged otherwise. The roster does\n not change: this widens an existing union and an existing Block, and no new Item,\n Kind, Page or catalogue entry appears.\n\n## 1.0.0\n\n### Major Changes\n\n- dbc8a65: Every superseded figure declaration is deleted\n \n The five local declarations of a reading over a population are gone from the\n published interface, and `MetricSpec` from `@nanisoft/prism-ui/spec` is the one\n shape a figure is declared with in this package. The expand step made each of the\n five an alias of the shared type so an old name resolved while its callers moved;\n this is the contract step, and it deletes every one of them now that a search of\n the whole tree, the documentation and the corpus finds no caller left. Deleted:\n \n - `Dashboard01Metric`, from `@nanisoft/prism-ui/blocks/dashboard-01`.\n - `ProjectDashboard01Figure`, from `@nanisoft/prism-ui/blocks/project-dashboard-01`.\n - `ChartCard01Reading`, from `@nanisoft/prism-ui/blocks/chart-card-01`.\n - `Trend01Item`, from `@nanisoft/prism-ui/blocks/trend-01`.\n - `Stat`, from `@nanisoft/prism-ui/blocks/stats-01`.\n \n `Dashboard01`, `ProjectDashboard01`, `ChartCard01`, `Trend01` and `Stats01` keep\n their own names, kinds and arrangements; only the shape is shared, because a shape\n is shared and a screen is not. This is the close of the metric expand-contract\n sequence, and the count of declarations describing a reading over a population in\n this package is one.\n \n The process stage shape, `ProvisioningStep`, stays declared with `Provisioning01`\n and is not migrated. It is the inside of one Item rather than a shape two Items\n would each declare, and a declared stage has not happened, so it is not migrated\n onto the event specification either.\n \n # Migration\n \n - Replace every use of `Dashboard01Metric`, `ProjectDashboard01Figure`,\n `ChartCard01Reading`, `Trend01Item` and `Stat` with `MetricSpec`, imported from\n `@nanisoft/prism-ui/spec`.\n - The shape is unchanged, so the migration is a rename of the type reference. A\n figure or row still carries `key`, `label` and `value`, and the optional `delta`,\n `deltaFormat`, `hint`, `series` with `seriesLabel`, and `href` with `hrefLabel`.\n - A build that reports one of the five names as missing is reporting a name that\n was deleted here rather than an exported entry that moved.\n- e9e73d6: Four forms take the shared field specification\n \n `AuthForm01`, `Contact01`, `Provisioning01` and `Signup01` now take the same\n `FieldSpec` and `FieldSpecGroup` a record write form takes, so the vocabulary a\n consumer learns on one screen is the vocabulary on all five. Each Block's own\n field declaration goes from the published interface, and a control Prism does not\n ship arrives as the `slot` arm of `FieldKind` rather than as a property on a\n per-Block type.\n \n - **`AuthForm01`**: `fields: AuthFormField[]` becomes `groups: readonly\n FieldSpecGroup[]`. The controlled `value` and `onChange` on a field go, because\n the specification is data and holds no callback; the Block is uncontrolled and\n the caller reads the values out of the form element. `error` becomes\n `submitError`, and `errorId` becomes an entry in the new `issues` list keyed by\n field. `AuthFormField` is removed and `AuthForm01Issue` is added.\n - **`Contact01`**: `fields: ContactField[]` becomes `groups: readonly\n FieldSpecGroup[]`, and a new `issues` list draws a message under a field.\n `ContactField` and `Contact01Option` are removed and `Contact01Issue` is added.\n - **`Provisioning01`**: a step's `fields: readonly ProvisioningField[]` becomes\n `fields: readonly FieldSpec[]`. `ProvisioningField`, `ProvisioningFieldType` and\n `ProvisioningOption` are removed; `ProvisioningStep`, `ProvisioningValue` and\n `Provisioning01Status` are unchanged.\n - **`Signup01`**: `fields: readonly SignupField[]` becomes `groups: readonly\n FieldSpecGroup[]`. `SignupField` and `SignupOption` are removed; the `password`\n arrangement, `SignupValue`, `Signup01Password`, `Signup01Reading`,\n `Signup01Status` and `Signup01SignIn` are unchanged.\n \n `FieldCommon` gains an optional `autoComplete`, the platform's autofill hint, once\n for every screen rather than in a second declaration per Block. Requiredness stays\n a rendering fact and an HTML attribute; no specification carries a rule.\n \n # Migration\n \n Replace a field list with groups of `FieldSpec` from `@nanisoft/prism-ui/spec`:\n \n - Rename a field's `id` to `key`, its `type` to a `kind` naming a Prism control\n (`'text'`, `'email'` and `'tel'` become `'Input'`, `'password'` becomes\n `'PasswordField'`, `'textarea'` becomes `'Textarea'`, `'select'` becomes\n `'NativeSelect'`), and its `description` to `help`.\n - Move a choice's `options` under its kind, unchanged in shape.\n - Wrap the list in one or more `{ fields: [...] }` groups and pass it as `groups`.\n - Pass a control Prism does not ship as `{ kind: 'slot', control: <your control /> }`.\n - On `AuthForm01`, replace `error` with `submitError` and, where you marked a\n field, pass `issues={[{ field: key, message }]}` instead of `errorId`.\n - Read `onSubmit` values by each field's `key`. `AuthForm01` is uncontrolled: read\n `new FormData(event.currentTarget)` rather than a controlled `value`/`onChange`.\n - Pass `autoComplete` on the fields a password manager or a phone keyboard fills.\n- dbc8a65: The activity trail takes the shared event specification\n \n `ActivityFeed01.events` is now an `EventSpec[]` from `@nanisoft/prism-ui/spec`\n rather than the Block's own `ActivityFeed01Event`, and that local type is gone from\n the published interface. The trail is the activity feed Block widened, so no new\n Item appears beside it: a shipment's chain of events, an issue's whole history and a\n transaction's trail are record details, and the trail's own entry declaration is the\n one declaration of a dated attributed occurrence every Block in this package takes.\n \n An entry carries a stable `key` that is never the words of a label, a moment printed\n exactly as the caller passed it with the machine value on the element, a required\n `actor` and `action`, an optional `target`, an optional `detail` node, a `tone` whose\n `toneLabel` is required wherever the tone is set, and a destination whose words are\n required wherever it is set.\n \n The specification names no vocabulary of what happened: there is no `kind`, because\n a closed union is legitimate only when its members are Components this package\n ships, and a caller needing one puts the word in `action` or composes a status into\n `detail`. Immutability is a rendering fact and nothing more, so there is no\n `immutable` prop, no seal glyph and no verification mark. A trail draws a moment and\n never a length, so there is no scale, no range and no axis, and the Block owns no\n retention window, archive, purge, legal hold, export or as-of clock.\n \n # Migration\n \n - Replace `ActivityFeed01Event` with `EventSpec`, imported from\n `@nanisoft/prism-ui/spec`, on the `events` prop.\n - Rename each entry's `id` to `key`.\n - Move the tone's label to `toneLabel` wherever `tone` is set.\n - Remove any `kind`, `immutable`, seal, verification, scale, range, axis or\n retention member: the specification does not carry them and does not accept them.\n Put a happened- word in `action`, and compose a status into `detail`.\n- dbc8a65: The settings panel takes the shared field specification\n \n `SettingsPanel01` now takes the same `FieldSpec`, `FieldSpecGroup` and `FieldKind`\n a record write form takes, and its own field declaration, `SettingsPanelField`, is\n gone from the published interface. The archetype is answered by the record write\n form Block and earns no Item of its own: the difference between a form and a setting\n is about what sits behind the surface, not about the surface.\n \n Two things change in the props, and the second is breaking.\n \n - `sections` is now `groups`, and a field is a `FieldSpec` with a stable `key`, a\n required `label` and a required `kind`. The `slot` arm means a control this\n package does not ship has somewhere to go, and a field's shell stays Prism's\n either way.\n - `secondaryAction` is gone. That position is now `footerStart`, a node the caller\n wrote, placed without styling. A caller who passed a label and no function could\n previously produce a focusable control that activated to nothing; a `CtaLink` with\n its required `href` cannot.\n \n `submitLabel` is now one arm of a save union: `action`, `onSubmit` or `submit`, each\n forbidding the others, exactly as the write form takes them. A region that applies\n on change passes `values` and an `onValueChange` that applies the mutation, and no\n save arm at all, and the Block draws no footer. Whether a setting leaves on change,\n on blur or at a save is the caller's own handler and the caller's own submission\n arm, and no prop here decides it.\n \n The Block renders no revert control, no reset to defaults control and no storage of\n any kind, and it draws no tab strip and no rail.\n \n # Migration\n \n - Rename the `sections` prop to `groups`, and pass each field as a `FieldSpec` with\n a stable `key`, a required `label` and a required `kind`, imported from\n `@nanisoft/prism-ui/spec`. A control this package does not ship goes in the\n `slot` arm.\n - Replace `secondaryAction` with `footerStart`, a node you write, placed without\n styling. A label with no function no longer compiles into a control that does\n nothing.\n - Replace `submitLabel` with one arm of the save union: `action` for a destination,\n `onSubmit` for a handler, or `submit` for a node you write. The three forbid one\n another. A region that applies on change passes `values` and `onValueChange` and\n no save arm, and draws no footer.\n - Remove any use of `SettingsPanelField`; it is gone from the published interface.\n\n### Minor Changes\n\n- f11799a: Add `emptied-by-reader` to `EMPTY_REASONS`, for a region the reader emptied\n \n `EmptyState01` takes a fourth reason beside the three it takes today: a region\n whose records were there and that this reader's own earlier action moved every one\n of them out of. A bin, an archive or a recycle view a reader has just cleared is\n the case it exists for.\n \n **A member on a published union, not a new Block and not a catalogue row.** The\n existing `EmptyReason` type gains its fourth member and the changeset says so\n because a consumer's own code that enumerates the union now has one more case to\n handle. `EMPTY_REASONS` is published through\n `@nanisoft/prism-ui/blocks/empty-state-01` beside the Block, and `catalog.ts`\n holds the same number of rows it held before, because a union member is a value on\n an existing exported type rather than an Item: it has no slug, no page, no Demo\n and no corpus entry of its own.\n \n **It is a fourth reason rather than a fourth Block because it is a statement about\n a collection, which is what the other three are.** Each of them answers what\n happened to the set the region would have drawn, so the frame they share stays\n true of all four: there is nothing here to read, and the frame makes no claim\n about why. A bin the reader emptied has nothing readable beside it, so the shared\n frame is honest there. That is the whole difference from `NothingChosen01`, which\n is a statement about a reader's pointer at a record and whose records are all\n still there: it draws no frame, it is a separate Block, and the two arguments\n argue the same line from opposite sides.\n \n **Two of the four are the reader's own doing and they are not the same doing.**\n `no-match` hides rows out of a set that is still there, so clearing it brings them\n back. `emptied-by-reader` is a record gone from the live set rather than hidden in\n it, and the reader put it there. Every saved view a caller builds out of the filter\n values it holds is already `no-match` whenever it comes back empty; a trash view\n is the one that differs in kind, because it fetches a different collection rather\n than narrowing this one. So the two members differ in exactly the fact a reader\n must be told honestly, and one sentence for both would tell a reader who has\n deleted everything that their own search found nothing. Both are named in the\n JSDoc on the union, which the emitted declarations preserve and the corpus reads.\n \n **It renders no control, and the Block already had that arm.** The reason takes\n the caller's `title` and an optional `body` and nothing else, because a reader who\n has deleted nothing has nothing to restore and the records a restore would bring\n back are the caller's own nodes in the index around this region. `EmptyState01`\n has never rendered an action without an `actionLabel`, so the member adds no\n render branch, no prop and no gate exemption: `scripts/check-block-controls.mjs`\n is green on the source exactly as it was, and the one control in it is still the\n `Button` behind that label.\n \n It carries no ink either, for the reason the Block gives: nothing is broken,\n nothing is missing, and nobody is blocked. `REASON_INK` is unchanged, so the only\n reason drawn in `muted-foreground` is still the permission boundary.\n- e9e73d6: Four more figure owners take the shared metric specification\n \n `ChartCard01`, `ProjectDashboard01`, `Stats01` and `Trend01` now take the same\n `MetricSpec` the metric summary takes, so a figure a consumer declares on one of\n these surfaces is the same figure it declares on the others. This is the migrate\n step of a breaking consolidation: five shapes are declared across this package\n for a reading over a population, and they disagree about whether a delta is a\n number or a percentage, about `deltaFormat`, about `hint`, and about whether a\n series and a destination are part of a figure at all. This release moves the four\n remaining owners onto `MetricSpec`, so one shape is left in use. Their superseded\n local declarations stay exported as deprecated aliases until the contract ticket\n deletes them, so an existing name continues to resolve.\n \n **The shape every one of the four now takes.** A stable `key` that is never the\n words of the label, a required `label` (a `ReactNode`) and `value` (the caller's\n own node), a `delta` whose sign is the direction, an optional `deltaFormat` (the\n caller's own words for the change, a `ReactNode` rather than a callback), an\n optional `hint` for the period or the caveat, and an optional `series` with its\n `seriesLabel` and an optional `href` with its `hrefLabel`. The two pairs are held\n by the type: a series set without its name, or a destination set without its\n words, does not compile.\n \n **What changes per Block, and the arm that replaces each prop that goes.**\n \n - **`ChartCard01`**: `ChartCard01Reading` becomes an alias of `MetricSpec`. A\n reading's `deltaFormat` is now the caller's own words rather than a `(value:\n number) => string` callback, so pass a node, and `key` is now required. The\n reading line draws the series and the destination too.\n - **`ProjectDashboard01`**: `ProjectDashboard01Figure` becomes an alias of\n `MetricSpec`, `key` is now required, and a figure can now carry `deltaFormat`,\n `hint`, `series` and `href`. The bare `0.12` a delta without a formatter used to\n print is now a decision rather than a drift.\n - **`Stats01`**: `Stat` becomes an alias of `MetricSpec`, `key` is now required,\n and a delta no longer has a percent sign appended to it. A delta with no\n formatter prints the number the caller passed, and a delta with a formatter\n prints the caller's own words.\n - **`Trend01`**: `Trend01Item` becomes an alias of `MetricSpec`, a row's `id`\n becomes `key`, and `deltaLabel` becomes the `deltaFormat` node. A row can now\n carry a series and a destination, and a delta with no formatter prints the\n number rather than making the Block throw.\n \n **What is kept, and what is still not owned.** Each Block keeps its own name,\n kind and arrangement. `ChartGroup01`, the page fixtures and the site landing page\n were updated at their call sites, because this package resolves the shared type\n through the component package. No Block performs aggregation, derives a rate of\n change, owns a period, a unit, a threshold, a target, a severity or a freshness\n state, and none draws a period switcher, a comparison toggle or an export. A\n metric's period stays the caller's own words, carried by `deltaFormat` and `hint`.\n \n `@nanisoft/prism-ui/spec` is the type to import: `MetricSpec`, from\n `@nanisoft/prism-ui/spec`, replaces `ChartCard01Reading`, `ProjectDashboard01Figure`,\n `Stat` and `Trend01Item` at every call site.\n- f11799a: Add `@nanisoft/prism-ui/spec`: one shared specification module, and the gate for it\n \n This adds published surface and changes no existing one. Nothing you compose\n differs today; what changes is that five screens worth of declared content now\n have one declaration each instead of several that disagree, and that the\n declarations are held by a gate rather than by review.\n \n **The five specifications.** `FieldSpec` and `FieldSpecGroup` describe the fields\n a write form, a wizard or a settings region renders, in the order the array is\n written. `ColumnSpec` describes a column of a record index. `RelationSpec`\n describes one relationship of a record, one ring deep. `MetricSpec` describes a\n figure taken over a population. `EventSpec` describes one dated, attributed\n occurrence. Every one of them is plain serialisable data: no validator, no\n constraint object, no schema, no pattern, no callback. That is what lets an agent\n read one out of a document, and it is why validation is yours entirely.\n \n **Three closed unions, and the one arm that is an escape rather than a catch-all.**\n `FieldKind` names the input Components this package ships, `ColumnKind` the\n reading Components, `RelationKind` the collection arrangements already drawn. Each\n carries a `slot` member for the control, cell or arrangement you draw yourself, and\n Prism still draws the shell around it: the label, the help, the error, the header,\n the alignment, the frame. A `props` bag would have been the alternative and would\n have put the hole back.\n \n **What is deliberately absent, so that you are not waiting for it.** A\n specification carries no rule and no validator, because a Block ships no behaviour\n and evaluating a rule is behaviour; `required` stays, and it is the mark and the\n HTML attribute rather than a check. A metric carries no period, because a\n comparison needs a period and a billing screen's month is not a trading screen's\n week: the words for it go in `deltaFormat` and `hint`, in your own units. An event\n carries no `kind`, no immutability, no retention window and no time axis, because\n no enumeration of what your product calls a thing that happened is ours to publish\n and because a drawing has no authority over the store behind it. A relation holds\n members and never another relation; a descendant is reached by a link you pass.\n \n **The gate.** `packages/ui/scripts/check-spec-unions.mjs` reads the emitted\n declarations rather than the source, because that is the seam your editor\n resolves. It fails when a union member names something this package does not\n ship, when two of the three unions claim the same member, when a specification\n grows a validator, a rule, a schema, a pattern, an immutability claim, a retention\n window or a time axis, when an event grows a `kind`, and when a relation holds a\n relation. Before it existed, every one of those was accepted by every gate in the\n repository, because a specification type is not an Item and no catalogue gate reads\n the module at all.\n \n **One thing it does not hold, so you know what a green run means.** It does not\n hold that there is one shape per concept: a second declaration of the field, column,\n relation, metric or event shape would be named differently from the first, and the\n rule that would catch it would be a list somebody maintains. That is held by review.\n \n **The surface gate's internal boundary moved with it, in one direction only.**\n `packages/ui/src/lib/` is where the internal helpers live, but the `exports` map is\n what decides: a declaration under it that a published entry reaches is public\n surface and the surface gate reads it, and one that no entry reaches is still\n internal and still has to be declared. `dist/lib/spec.d.ts` is the first such file.\n Nothing under `dist/lib` was declared internal and then undeclared, and no published\n subpath was removed.\n- caee732: `Directory01` gains a per-member node\n \n `Directory01` takes a new `renderMemberAction` prop: a function from a member to a\n `ReactNode` the Block places at the member's trailing edge, in both the card and the\n row arrangements, and draws nothing of its own where it is not passed.\n \n **The control and its handler are the caller's, and that is why it is a node.** A\n directory is a browse, so a plugin or model row's enable is a property of a member\n rather than a command on a selection, and `scripts/check-block-controls.mjs`\n classifies `Switch` and fails a Block that renders one without its handler. A Block\n ships no behaviour and so cannot supply one, so the enable arrives as the caller's\n own node, in the shape `DataTable01`'s `renderRowActions` already takes.\n \n No new Item and no new catalogue entry: the change is a prop on an Item that ships,\n with its JSDoc, its Demo and its test.\n- caee732: `DocumentPage`, the document and research workspace\n \n A new Page arrives at `@nanisoft/prism-ui/pages/document-page`. It draws one\n document read in full, its body held to the reading measure, and one or more\n regions beside it.\n \n **It is authored beside `DocsShell`, and neither of that Page's two rules is\n relaxed.** A section stays a label and not a control, and a group with no index\n stays a label and not a route, because both are statements about a documentation\n site's navigation tree and this workspace never asks that question. A status\n belongs to a region of one document, and a route is the document's own contents.\n No consumer of `DocsShell` is affected.\n \n **One Page draws all seven screens.** The document is `RecordDetail01` and the\n body is `Prose`; a version list is an `activity` region, a source or output list\n is a `table` region, a comment thread is a `members` or a `table` region, and the\n evidence comparison matrix is a `comparison` region drawn by `Compare01`. The\n region union is by arrangement and not by subject matter, so the Page mints no\n document, version, comment or source type and ships no sentence. The editor, the\n fetch, the retention, the share, the export and the permission are the consumer's.\n \n The Page ships with a catalogue entry, a JSDoc block, an Item page and a Demo, a\n corpus and store entry, a composition test and an accessibility test over its\n heading structure and named regions.\n- f11799a: Add `NothingChosen01`, the resting state of a detail pane\n \n `NothingChosen01` draws a region that can hold a record and has none chosen in it\n yet: the caller's words, an optional sentence under them, and an optional mark of\n the caller's choosing. It is a new Block beside `EmptyState01` rather than a fourth\n reason on it, and the difference is the shape of the claim rather than a matter of\n taste.\n \n The three reasons `EmptyState01` takes are all statements about a collection:\n nothing ever existed in it, the reader's own filter emptied it, or this reader may\n not see it. The dashed frame and the height floor those three share assert\n something that follows from all three together: there is nothing here to read.\n Nothing chosen is not that. No collection has been emptied, filtered or hidden, the\n records are in the index a few centimetres away, and what is missing is the\n reader's pointer at one of them. A fourth arm would have had to keep that frame\n honest while a readable region sat next to it, which is the failure the separate\n Block exists rather than to commit.\n \n So the Block ships no reason value, because one cause is not a choice a caller\n makes; no default sentence and no fallback, because the words are entirely yours;\n no frame, for the reason above; and no action. That last one is not an omission. A\n Block ships no behaviour, so a control here would be either one this Block cannot\n wire or a second copy of an affordance the index beside it already draws for the\n same reader, and `scripts/check-block-controls.mjs` has nothing to catch in a\n source that contains no control.\n \n It also declares no height floor. A pane in a grid is already as tall as its\n neighbour, so a `min-h` authored here would be a second answer to a question the\n split's tracks have already answered. What the Block does is fill the height it is\n given and centre itself in it.\n \n Two things it inherits rather than re-derives: the icon is a slot drawn\n `aria-hidden`, because a mark a screen reader reads is a second announcement of\n something the words already say, and the title is a `<p>` rather than a heading,\n because the pane outlives its contents and an outline entry that vanishes on the\n first click is an entry a reader navigating by heading cannot follow.\n \n The split's tracks hold their ratio at every selection state, so the list never\n moves out from under the hand that clicked it. What changes is content, and a\n caller who wants the index louder makes the index denser, which is the index's own\n decision to take.\n- dbc8a65: The issue detail Block is deprecated in favour of the record detail Block\n \n `IssueDetail01` is deprecated, not merged and not deleted. It is the same job as\n `RecordDetail01` under a name that is wrong the moment the record is an invoice, an\n order or a subscription, and a name that has to be wrong about nine tenths of its\n uses is a name that fails the test that its name survives being wrong. The Item\n still renders and is still supported for now, but nothing new should be started on\n it, and a consumer composing a record detail today composes `RecordDetail01`.\n \n **The published interface is unchanged.** This release marks the Item and changes\n its JSDoc and its documentation; no export is removed, renamed or re-shaped, so a\n consumer already composing `IssueDetail01` keeps composing it and can read the\n release without moving. `IssueDetail01` and its four types stay exported from\n `@nanisoft/prism-ui/blocks/issue-detail-01`. Its own field declaration stays with\n it and is not migrated onto the shared field specification, because it is a label\n and a value node a reader is entitled to be told about a record rather than an\n input.\n \n The catalogue entry carries the `deprecated` status, so the Item page shows a\n deprecated mark, and the Item page and the record detail Block's documentation each\n name the other, so a reader arriving at either arrives at the answer.\n- dbc8a65: Add `IndexDetail01`, the master and detail split\n \n `IndexDetail01` is a composition Block that renders an index pane beside a detail\n pane and holds no selection state at all. Selection, routing and which record is\n open belong to the consumer, because Prism never imports a router. Nothing in the\n Block connects the two panes: the index reports every selection change through its\n own callback, and reacting to it is the caller's own read of that callback and a\n pass of the record it wants into `detail`.\n \n The tracks hold their ratio at every selection state, so clicking a row does not\n move the list out from under the hand that clicked it. The pane before anything is\n selected holds what the caller placed, or nothing at all, so the Block ships no\n fallback sentence, no frame of its own and no loading mark. The detail slot is a\n required node, because a record that is gone, one this reader may not see and one\n whose fetch failed are three causes sharing one address and three different\n sentences.\n \n The split does not collapse at a narrow viewport. Which pane survives is the\n caller's composition rather than a media query deciding a shape, so the narrow\n arrangement is one the caller writes. The Block renders no control of its own.\n \n This change also corrects the record index section of the design document, which\n claimed the split was already authored while the roster counted from source held no\n such Item.\n- caee732: `MessageThread01`, the live Kind's second family\n \n A new `live` Item, the message-thread surface, arrives at\n `@nanisoft/prism-ui/live/message-thread-01`. It renders a conversation whose\n messages arrive over a `subscribe` function the consumer supplies and it opens no\n connection of its own, so a WebSocket, an `EventSource`, a polling timer and a test\n array are all the same shape to it.\n \n **A message list with ordered parts, and not an event list or a turn list.** A\n message has a stable identity, a sender and an ordered list of parts, and a part is\n text, a tool call, a reasoning block, a citation or an attachment. The message and\n part vocabulary lives with the surface, beside `RunEvent` and `ToolCall`, and is\n deliberately outside the shared specification module, so no sixth specification\n member and no `CONTEXT.md` word is added.\n \n **It reuses what the Kind already names rather than minting a second answer.** A\n tool-call part is drawn through `ToolLedger01`'s own row, so there is no second\n tool-call shape. The status tiers are `RunStatus`, published once and shared by both\n live surfaces. The run controls are `PromptComposer`'s send, stop, retry and attach,\n composed beside the thread rather than a second set drawn inside it. The one surface\n it does not reuse is `RunStream01`, because a run's log is a sequence of occurrences\n and a conversation is a sequence of messages.\n \n **Prism holds a bounded in-memory window and promises no resumption.** The durable\n resumption buffer that outlives a mount is the consumer's, through the same `initial`\n and `resubscribeKey` seam `RunStream01` already documents: a surface opened\n mid-thread is seeded from the consumer's own record, and a reconnect is a new key\n over a fresh seed. The surface sorts by `at` with a tie on `id`, bounds its window,\n announces a growing list once rather than reading it whole on every arrival, and\n holds no control it cannot act on.\n \n The Item ships with a catalogue entry, a JSDoc block, an Item page and a Demo, a\n corpus and store entry, and a boundary test in the shape of the `RunStream01` one.\n- 1079d02: The metric summary Block takes the shared metric specification\n \n `Dashboard01.metrics` is now a `MetricSpec[]` from `@nanisoft/prism-ui/spec`\n rather than the Block's own figure shape. This is the first step of a breaking\n consolidation: five metric shapes are declared across this package today and they\n already disagree about whether a delta is a number or a percentage, about\n `deltaFormat`, about `hint` and about whether a series and its name are part of a\n figure at all. `MetricSpec` is the one shape, and this release makes the metric\n summary Block the first surface to take it. The other four owners are migrated in\n later batches, so this is one Block wider rather than five replaced at once.\n \n **What a figure is now.** Every entry is a `MetricSpec`: a stable `key` that is\n never the words of the label, a required `label` and `value`, a `delta` whose sign\n is the direction, an optional `deltaFormat` carrying the caller's own words for\n that change, an optional `hint` for the period or the caveat, and an optional\n `series` with its `seriesLabel` and an optional `href` with its `hrefLabel`. The\n two pairs are held by the type: a series set without its name, or a destination set\n without its words, does not compile.\n \n **What changes for a consumer.** `label` is now a `ReactNode`, so a caller that\n passed a string passes the same string and a caller that passed a node keeps it.\n `deltaFormat` is the caller's own words rather than a `(value: number) => string`\n callback, so `deltaFormat: (v) => String(Math.abs(v))` becomes\n `deltaFormat: '3 fewer than yesterday'` and the direction mark still comes from the\n sign of `delta`. `sparkline` and `sparklineLabel` are now `series` and\n `seriesLabel`, and `key` is now required. `Metric` takes the words as a node when\n the Block hands them over, so a delta with a formatter prints the caller's words\n and a delta without one prints the number the caller passed.\n \n **`Dashboard01Metric` is kept as an alias** of `MetricSpec` so an existing name\n resolves while the other four figure owners are migrated. It is a synonym on its\n way out, not a second shape, and a later contract release removes it alongside the\n superseded `ProjectDashboard01Figure`, `ChartCard01Reading`, `Trend01Item` and\n `Stat`.\n \n **Nothing else is owned.** The Block still performs no aggregation, owns no period,\n no unit, no threshold, no target, no severity and no freshness, and draws no period\n switcher, no comparison toggle and no export.\n- dbc8a65: Add `RecordWizard01`, the multi-step record write form\n \n `RecordWizard01` is the write form's second arrangement rather than a prop on the\n plain form. A wizard draws one group of fields at a time above a rail and asks a\n decision the plain form never asks, which is whether a step may be left forwards, so\n it earns its own name: it is a property of how many fields a record has, not one of\n two renderings a consumer picks between on purpose.\n \n The step is the consumer's. `current` is controlled and `onStepChange` is required,\n and the Block draws the step it was handed and no reachability at all. The\n specification is per step, and a step is a `FieldSpecGroup` from\n `@nanisoft/prism-ui/spec` with a stable `id` and a string `label`, so nothing new is\n declared in order to hold the steps and a group's heading and its rail label are one\n word. One specification sliced by a caller grouping is refused.\n \n Every field on the stepped arm is controlled and carries the consumer's value and\n change handler, so the values are the consumer's from the first keystroke. Back, the\n forward control and the rail are `FormWizard`'s, and this Block renders none of\n them, declares no submission arm of its own, and leaves an issue naming a key that\n is not on the step being drawn undrawn without moving the reader.\n- 1079d02: The record detail draws one record and its relationships\n \n `RecordDetail01` is a new Block: one record in full, its relationships drawn open\n as one ordered list, and its own actions beside its identity. It is a Block rather\n than a Page because the frame and the claim belong to a caller and a Page owns no\n URL, so a detail Page would have to own the selection the consumer holds. It is\n authored beside `Summary01` and `QuickView01` rather than by widening either,\n because a summary exists to add up and a quick view is a transient dialog.\n \n **Relationships are one declared list on one frame, not a card per relation.**\n Each entry is a `RelationSpec` from `@nanisoft/prism-ui/spec` plus the words its\n own empty frame needs, drawn in the declared order under one heading level inside\n one bounded frame with one rule between them. The `kind` names a collection\n arrangement this package already draws: `Table`, `ListPanel`, `Timeline`,\n `AvatarGroup` or the caller's own `slot`. **Depth is one, and the bound is in the\n type**: a relation's members never name `RelationSpec`, so a related record with\n its own related records is a graph the consumer walks, reached by the caller's\n `href` rather than by depth. The Block composes no disclosure anywhere.\n \n **The record's actions are one optional node, placed in the record's own band.**\n The Block renders no control of its own, there is no declared action union and no\n destination arm, and the confirmation for a destructive action is the caller's own\n `AlertDialog` composed around their control in the same slot. **A relation with no\n members draws `EmptyState01` at a reason the caller names**, in the relation's own\n frame, because a customer with no invoices is a collection that is empty and\n `NothingChosen01` is a statement about a reader's pointer at the index beside the\n pane, which this Block cannot be in. **The Block takes no total, no count and no\n sum**: a relation's `count` is the caller's own string and is never derived from\n the members, and a screen that wants a figure composes `Summary01` or `Stats01`\n into the Block's `children`.\n- 1079d02: The record index groups by a key on a row and announces the two selection scopes differently\n \n `DataTable01` widens again rather than adding an Item, and the roster does not\n change: a grouped index is the same job with a `groupBy` on a row, not a second\n arrangement, so no `-02` is earned.\n \n **Grouping is a key on a row.** `groupBy` takes a row and returns the key it\n belongs to, or `undefined` for an ungrouped row, and `groupLabel` names the\n heading the Block draws above each run of rows that shares one. The two are a\n single arm: `groupLabel` is required wherever `groupBy` is set, because a heading\n with no words is a blank row. The Block never reorders the rows it was handed, so\n same-key rows must arrive adjacent and a run of them is one group. A group heading\n is a row in the same body as any other, and it is not a record: it is never in\n the selection set, never in the count and never in what the header's page scope\n covers, so forty headings over four hundred rows select four hundred records and\n say so.\n \n **The two selection scopes are announced differently, and the filter scope's\n number is the caller's.** `selectionScope` is new and is a union. Its page arm is\n the default and the Block speaks `labels.selectedCount` over the keys it holds.\n Its filter arm reports that the whole matching population is selected and carries\n a required `count` from the caller, and the Block speaks\n `labels.selectedAllMatching(count)` rather than printing the page as if it were\n the total. The Block owns no control that selects a population it cannot see: the\n caller selects in its own store and reports the scope here.\n \n **What is new on the surface:**\n \n - `groupBy` and `groupLabel`, together, for a grouped body.\n - `selectionScope`, a `DataTable01SelectionScope` union, for the page or the\n filter scope.\n - `selectedAllMatching`, a required `labels` member: the summary and announced\n sentence for the filter scope. `selectedCount` is now the page-scope sentence,\n and it should say the page.\n \n The per-row prop that went and the node that replaced it are named in the record\n index selection entry in this release: `rowActions`, a declared list of action\n objects with an optional handler, is gone, and `renderRowActions`, a node per row,\n replace it, so a row that looks like a command and carries none cannot be built.\n- 1079d02: The record index draws typed columns and holds the selection set it was given\n \n `DataTable01` widens rather than adding an Item, and the roster does not change:\n every one of the in-scope screens is one job, a reader scanning rows and acting on\n them, differing only in its columns, its keys, its filters and its sort key. No\n `-02` is earned by it.\n \n **The columns are typed data now, and the callback is gone.** `columns` is a\n `readonly ColumnSpec[]` from `@nanisoft/prism-ui/spec` rather than an array of\n `{ id, header, cell }`. `key`, `header` and `kind` are required on each column,\n because a column a reader reaches without a name is a column every row announces\n as untitled, and `kind` is drawn from Prism's own cell vocabulary rather than from\n an HTML attribute. The row value at the column's key is the cell's content: a\n single-node kind draws it as that Component's child, a structured kind takes it as\n that Component's props, and the `slot` kind draws it as your own node inside the\n header, alignment and sort affordance the Block draws. `DataTableColumn` and\n `DataTableRowAction` are removed with it.\n \n **Selection is required whenever the index is selectable.** `getRowId` is required\n on the Block, because sorting, filtering, paging and deleting all move a row's\n index and an index-keyed selection silently changes which records a batch action\n acts on. `onSelectedIdsChange` is required on the selectable arm and `batchActions`\n with it; the Block holds the set on its uncontrolled arm and reports every change\n through the one callback, so mirroring the set is one line and ignoring it is zero.\n \n **What is new on the surface:**\n \n - `columns`, now `ColumnSpec[]`. `sortable` draws `TableSort` and reports the next\n direction through the new `sort` and `onSortChange`.\n - `getRowId`, now required.\n - `onSelectedIdsChange`, required on the selectable arm.\n - `batchActions`, required on the selectable arm: one `ReactNode` the Block places\n in a bar that mounts only while the set is non-empty and displaces the filter\n controls. The Block draws the count and the dismiss and no command of its own.\n - `renderRowActions`, replacing `rowActions`. A node per row rather than a declared\n action list whose handler was optional, so a row that looks like a command and\n carries none cannot be built.\n - `sort` and `onSortChange`, for the ordered column.\n - `selectRow` is now `(row) => string` so every checkbox names its record,\n `selectedCount` is the count the live region speaks, and `clearedSelection` and\n `dismissSelection` join it. `sort` is the words for the next direction.\n \n **One polite live region carries the count**, never an assertive one, mounted while\n there is an announcement; it speaks the count rather than a list of labels, and it\n speaks the emptying as its own sentence rather than a count of zero. The visible\n summary and the announced number are the same string.\n \n **Nothing else is owned.** The Block fetches nothing, sorts nothing, filters\n nothing and slices nothing, owns no action, no aggregate, no address and no second\n pane. The detail pane reacts through your read of `onSelectedIdsChange`; no bridge\n is built between the two.\n- 1079d02: The record write form renders a field specification whole\n \n `RecordForm01` is a new Block: a create or edit screen rendered from a typed,\n ordered field specification. It takes `FieldSpecGroup[]` from\n `@nanisoft/prism-ui/spec` and draws Prism's own controls, each inside one shell of\n label, help and error. Every field carries a stable `key`, a required `label`, a\n required `kind` and optional help, hint, placeholder, disabled flag and starting\n value; a control the package does not ship is the `slot` arm, and the Block still\n draws the shell around the caller's node.\n \n **The error state is an issue list keyed by field**, the shape `FormDialog` and\n `FormWizard` already publish, and a message about the submission rather than about\n a control is `submitError`, which names no field. **The save union has three arms**\n and the compiler holds it: `action` posts to a URL, `onSubmit` hands over the\n form's element, and `submit` is a slot for the caller's own control. The save\n control is `type=\"submit\"` inside the `<form>` the Block renders. **Layout is\n derived**: `columns` is `1 | 2` at the form level, one column again at the narrow\n breakpoint, and there is no per-field layout prop and no slot per region.\n \n **One piece of per-kind rendering data is added.** The `MoneyField` arm of\n `FieldSpec` gains a required `currency` and an optional `locale`, because an\n amount is meaningless without the currency it is in and no locale can supply it.\n Nothing else in the shared module changes, and the module stays plain serialisable\n data with no rule, no validator and no callback on it.\n\n### Patch Changes\n\n- f11799a: The gate that holds a Block's controls now reads the menu row and the switch\n \n **No published surface changed.** Nothing in this entry is a prop, a Component or a\n behaviour you compose differently: the change is to `scripts/check-block-controls.mjs`,\n which is a repository gate and is not in the published tarball. It is here because the\n law it holds reaches a consumer as the shape their own Block has to take, and a\n consumer reading the changelog should be able to find that law without reading this\n repository.\n \n That gate classified `Button` and `CtaLink`, and neither of them is a menu row. The\n shipped record index takes a declared list of per-row actions whose handler is\n optional and renders each entry as a `DropdownMenuItem`, so a caller who passes a\n label and no handler gets a row that sits in the menu's keyboard order, is announced\n as a menu item, and activates to nothing. The gate could not see that Component, so\n every gate in the repository was green while it shipped, and a clean run was the\n absence of a check rather than the presence of a correct one.\n \n Four Components are classified now, each with the arms that make it act:\n \n - `Button`: `onClick`, `type=\"submit\"`, `type=\"reset\"`. Unchanged.\n - `CtaLink`: `href`, which is required, so that arm is the compiler's. Unchanged.\n - `DropdownMenuItem`: `onClick`, or `render` together with `href` in the element it\n renders as. Both words rather than either one, because `render={<span />}`\n navigates to nothing and a bare `href` would accept a `data-href` as a destination.\n - `Switch`: `onCheckedChange`, and nothing else. A switch does not navigate and is\n meaningless without the handler, because the state it changes is the consumer's\n setting rather than anything a Block holds.\n \n Every `DropdownMenuItem` and every `Switch` the tree renders already carries one of\n those arms, so nothing shipped failed and no Block moved. What the widening buys is\n the next one: a menu row or a switch a Block renders with nothing behind it is a\n finding naming the file and the line, where yesterday it was invisible.\n \n **Two limits are printed on every run and neither is new.** A `{...rest}` spread\n inside a rendered control reads as carrying no handler, and a handler forwarded from a\n declared optional prop reads as present, because resolving whether the caller passed\n it is reading a type rather than a file. The second is why the shipped row action list\n is still a finding for a reader and not for this gate: the optional handler behind the\n attribute is invisible to a source scan. Removing that declared list is the authoring\n work, and this gate is what makes the next one of them fail the run.\n \n **A Block that holds a disclosure itself is still not caught, and that gap was left\n open on purpose.** `Collapsible` and `Accordion` put none of the four classified\n Components in a Block's source, so a Block rendering one with a `defaultOpen` and no\n change handler reads as clean while the state is still state a Block holds. A\n disclosure's activation lives inside the disclosure rather than on an attribute of the\n row that opens it, so there is no arm for this gate to read, and a rule that inferred\n one would report the Blocks that compose `Collapsible` correctly. The prohibition\n stays a design rule in `DESIGN.md`, an auditor checks it by looking for the absence,\n and the run prints that it is not a finding so a clean run is not read as more than it\n is.\n- dbc8a65: The log and event stream is the record index drawing event rows\n \n A delivery log or a console events screen is the record index, `DataTable01`,\n drawing rows from the shared event specification, `EventSpec`. No Item, Kind or\n Page is added: a log gains a filter region, a second axis on which entries are\n compared and a page boundary, and every one of those already belongs to the record\n index rather than to a rail. The row type is the shared event specification, and no\n sixth event shape is minted for a log or a console.\n \n A screen that wants the events of one record composes `ActivityFeed01`; a screen\n that wants to find one entry among many composes `DataTable01`. The Block composes\n no disclosure: seeing one entry in full is a destination the caller names with its\n required words, expanding a payload in place is the caller's own `Collapsible`\n placed in a `slot` cell, and taking an identifier away is the caller's own control\n beside a code block. No prop changed; this release adds the Item documentation, the\n Demo and the JSDoc that record the arrangement.\n\n## 0.16.1\n\n### Patch Changes\n\n- bf34d82: `DocsShell` drew its navigation twice in 0.16.0 and draws it once again\n \n **0.16.0 introduced a regression and this release removes it.** To give a reader below `lg` some navigation, 0.16.0 drew each tree a second time behind a `<details>`: one copy in the rail for a wide screen and one behind a disclosure for a narrow one. Only one of the two was displayed at any width, so the accessibility tree was never wrong and a reader never met two copies at once. Every documentation page in the family still shipped two of everything, and the payload of that is paid on the largest surface the system has.\n \n Three consumer test files failed against 0.16.0 as published, and two distinct defects produced every one of those failures. Two files failed on a count that came back doubled, and two failed on a `getByRole('navigation', { name })` that had two answers where it expects one: the disclosure drew its own `<nav>` under the rail's own accessible name, and that name is how a consumer's suite reaches the rail at all. A suite that holds that a documentation rail contains no disclosure control was never the finding, because the `<details>` was a sibling of the rail in the same frame and never inside it.\n \n **What a consumer measured on 0.16.0, in three built exports.** The Nexus page `/docs/architecture/kanban-service` carried 3 `aria-current=\"page\"` markers where 2 is right, because the third belongs to that site's own bar and the other two are this Page drawing the same page twice. It carried 8 `<nav>` elements where 6 is right, and 52 `data-slot=\"docs-nav-page\"` rows on a corpus of 26. The AlphaLens index at `/docs` carried 12 `data-slot=\"docs-nav-label\"` spans on a six section corpus. The Prism preview route that renders this Page, `apps/site/out/preview/docs-shell.html`, carried 22 `docs-nav-page` rows on an 11 row outline and 5 `<nav>` elements where 3 is right.\n \n **What changes.** Each tree is drawn once, at every width. A documentation page carries one `aria-current` marker, one navigation list per tree and one row per document. Below `lg` the frame is a single column and the rail is its first child, so a reader at a phone or tablet width meets the navigation before the document rather than after it, which is the gap 0.16.0 was reaching for. The Page is still a server Component: no `'use client'`, no hook, no context, no handler, no runtime token read, and no new client JavaScript on any documentation page.\n \n **The fade is now bound to the cut it marks, and it was not.** The fade element was left unprefixed while the `padding-bottom` reserving the band beneath it was already `lg:`-prefixed. The cut exists only where the rail scrolls, which is `lg` and up, so below `lg` the band went inert and the gradient kept painting: the rail's last 2rem carried a fade over the last row or two, permanently, at exactly the width this release starts serving the tree at. Both numbers still matched and no row lost legibility, which is why it survived every lane that was looking. The fade and the band are now written against the same `lg:` as the `max-h`, the `overflow-y-auto` and the `sticky`, so a fade with nothing to fade is no longer painted. **The price, stated rather than left to be found:** below `lg` the rail is bounded by its content rather than by the viewport, so a long tree runs past the fold of a phone screen with nothing marking where it ends. No cut to mark, no band to reserve and no scroll region to scroll inside. That is the honest limit of one copy of the tree at every width, and it is now written into the Page's own documentation beside the rule that causes it.\n \n **The published surface loses two slots.** `data-slot=\"docs-nav-compact\"` and `data-slot=\"docs-nav-disclosure\"` were added in 0.16.0 and are removed here. They were published for exactly one release, and no call site in the four downstream repositories addresses either by name, so nothing there has to change. A call site that reached for one now finds nothing rather than finding an empty region, which is the honest outcome and the reason this is called out. Every slot that existed before 0.16.0 stays, `docs-rail-fade` included.\n \n **What it costs, with the number.** A phone reader now scrolls past the whole navigation to reach the article. One consumer's rail measured 1,318 pixels of content in a built export at 1280 by 680, and those rows wrap on a phone, so the scroll a narrow reader now makes is a thousand pixels or more. That is a real regression for that reader and it is the reason a disclosure was reached for in the first place. It is paid rather than hidden, because the two arrangements that avoid it are both worse: a rail at `lg` and nothing below it reopens the gap 0.16.0 closed, and drawing the tree twice to keep the scroll short is what broke three consumer test files here.\n \n **What would actually remove that scroll.** A collapsed rail below `lg` needs an open state, and a server Component cannot hold one. `<details>` is the only disclosure the platform holds on the Page's behalf, so a control that opens one has to be a client island inside the Page. That is separate work with its own budget and it is not built here: shipping a client boundary to save one scroll is a different trade from the one above, and it belongs in its own change rather than as a side effect of removing a duplicate.\n \n **One Item document was corrected, and no gate reads it.** This Page's own documentation said under Best practices that an entry with no words is a label. That was true before this work and became false the moment the Page began dropping a nameless entry rather than rendering one, and it shipped false through 0.16.0 because nothing in the tree compares that bullet with the Page's own source. The bullet now states what the Page does, and `docs/quality-gates.md` records why no gate was added for it rather than leaving the question open: a gate over prose claims has to enumerate the claims, and a list somebody maintains is the shape that already left the heading gate holding one Block and passing over four, or it has to match on wording, which fires on a rewrite and not on a lie. The decidable half of that failure class has a gate, in the heading-scale table comparison. This half is a reader's, and the hole is now named in the document that collects them.\n \n **Three things 0.16.0 got right and this release leaves as they were, with the fade's bound corrected above.** The rail fade, pinned to a non-scrolling sticky wrapper with the band it covers reserved in `padding-bottom`, both written against the `lg:` that creates the cut; the sticky `max-height` arithmetic in `100svh` rather than `dvh`, which cannot exceed the space it is pinned in; and nameless entries dropped rather than rendered as an empty `<li>`, including the group-label and empty-group arms, counted on each rail as `data-unnamed-entries`.\n\n## 0.16.0\n\n### Minor Changes\n\n- 1a0ba35: A page `h1` is now visibly larger than the `h2` sections under it\n \n A page `h1` rendered at 1.875rem and every section heading under it at 1.5rem, so\n the two were one fifth apart and the page had no top to its hierarchy. The fix is\n in the type scale rather than in the Component, because the scale stopped at `4xl`\n and there was no step for a page's own claim to take.\n \n **Two steps are authored, and they are the two widest gaps in the group.**\n `5xl` is 3rem and `6xl` is 3.75rem, both at a line-height of 1. Every other step\n in the `text` group is a whole multiple of 0.125rem and these two are too;\n `4xl` to `5xl` is four thirds, the first gap in the group wider than any gap it\n already had, and `5xl` to `6xl` is five fourths. They are authored as a pair\n rather than as one step because every rung of the heading ladder is a pair, a base\n step and one step up at `sm`, so a single step above `4xl` would have made the\n page heading the one heading in the system that does not grow at a width.\n \n Both steps reach CSS and the published pack contract the way every other authored\n step does, so `text-5xl` and `text-6xl` are utilities a consumer can compose, and\n neither is a value a consumer has to write.\n \n **The heading ladder now descends from the new ceiling, one authored step per\n level, and every level holds a step of its own.** There are exactly six authored\n steps at or above Body, so the table lands on the floor with a distinct step at\n every rung, where before `h4` through `h6` shared one. `h1` is at `5xl`, rising to\n `6xl` at `sm`; `h2` is at `4xl`, rising to `5xl`; and the table descends to `lg` at\n `h6`, which is the step Body is set at. **What a consumer sees is every section\n title moving up one step and every page heading moving up two**: a section title\n goes from 1.5rem to 2.25rem and from 1.875rem to 3rem at `sm`, and a page `h1` goes\n from 1.875rem to 3rem and from 2.25rem to 3.75rem at `sm`. No call site changes,\n because the level a Block was already passing is still what decides its size.\n \n **Five surfaces that render a page's own `h1` now ask the same table for its size\n rather than naming a step.** `page-header-01`, `docs-shell`, `blog-post-page`,\n `not-found-page` and `error-page` each wrote a fixed step into a heading that was\n meant to be a page's top-level claim, so with the ceiling lifted they would have\n rendered a page heading a rung or two below the section titles under it. They ask\n `headingSizeClass`, which is what `SectionHeading` and `Cta01` already asked, and\n `Cta01` still carries no literal.\n \n **Four more surfaces that resolved their own heading size, found the same way.**\n A Block that takes a `headingLevel` and writes the size beside it forwards the level\n and changes nothing a reader can see, which is the defect above at the scale of one\n Block. `data-table-01` said `text-lg`, `run-console-01` said `text-base`, and both\n moved the outline without moving the size. `run-stream-01` renders a literal `h2`\n at `text-lg`, which was the floor of the old scale, so a live region's `h2` came\n out below the `h3` introducing the section holding it. `/foundation/themes` said\n `text-3xl`, which had been Display and is now the `h3` step, so that page's `h1`\n rendered a rung below the section titles under it. All four ask `headingSizeClass`\n now, and the three Blocks carry `text-balance` with the size they gained: the ladder\n states weight, tracking and balance as one package and `SectionHeading` carries all\n three at every rung, so a title lifted from 16 or 18 pixels to 24 or 36 is worse\n unwrapped than the one it replaced.\n \n **The Item page's appendix heading is on the article's ladder, and asking the Block\n ladder for it was the wrong correction.** It said `text-xl`, which put an `h2` at 20\n pixels under the prose `h2` at 30 and below the prose `h3` at 24 above it. Asking\n `headingSizeClass` removed the third tier and created a worse one: \"API reference\"\n is a section of an article exactly as \"Overview\" is, so it takes the article's\n ladder, and the Block ladder is a step and a half above it at `sm`. The Item page\n then carried three sections at 30 pixels and an appendix at 48, and the appendix was\n the second largest thing on the page. It is now a bare `<h2>` inside the same\n `.prose` wrapper as the Item's own MDX body, so the article's rules size it at 30\n pixels and it carries no class of its own: every section heading in that article is\n a bare tag the article styles, and a heading naming its own step would be the second\n answer to a question the article has already answered. The table stays outside\n `.prose`, whose descendant rules would put its cells at the body step in the muted\n colour, and the gap above it is the article's own 16 pixels rather than a second\n region's 32.\n \n **The five theme names are card titles, and they compose `CardTitle` to say so.**\n They carried `font-medium` and no size, so they rendered at 16 pixels and 500 under\n a 60 pixel `h1`. Asking the ladder for the size would have been wrong: five themes at\n the `h2` step is five 48 pixel headings over five cards on a page whose own claim is\n the `h1` above them. `CardTitle` already holds the answer the library needs: a card\n title sits at body size, and the element and the visual step are separate questions.\n So the size stands, the tag stands at `h2` because the grid draws no section heading\n of its own, and the weight now comes from the one Component that owns it rather than\n from a literal. What a consumer sees is five theme names one step heavier.\n \n **The two Foundation readers took opposite answers, and the reason is worth more\n than either change.** Both render inside an MDX body, which the site wraps in\n `.prose`, and both used to carry literals, which a utility layer lets win over the\n article's own component-layer rule: `/foundation/colors` published an `h2` at 18\n pixels and an `h3` at 14, below the `h3` above them.\n \n `ColorTokens`' four headings are sections. Two divisions of an article and a\n partition under one of them is a structure, not a list of labels, so they name no\n step and the article decides, which is the one copy of the ladder there is.\n \n `TokenBrowser`'s are not sections, and dropping its literals let the wrong authority\n decide them: every group name came out at the prose `h2`, 30 pixels, which is more\n than twice the 14 pixels the navigation rail's own section titles are set at, on the\n page whose whole job is being navigated. Each of those 184 names is a table's name\n and nothing else, so they are captions: `h3` at `text-sm`, in the mono face because a\n token key is machine notation, at the same 14 pixels and 500 as the column heads of\n the table underneath. That is the caption `ApiTable` already renders for a compound\n export's part names on every Item page, so this is the repository's own treatment\n rather than a third one. One heading on the page, \"Semantic contract\", is a division\n of the article rather than a label, and it keeps naming no step.\n \n **`Prose` walks the same ladder from one rung down, and a prose heading therefore\n sits below a Block heading at the same level.** Its child treatments now set `h1`\n at `4xl` and descend one authored step per level to `lg` at `h5` and `h6`,\n which is the same scale and the same floor entered at the rung that fits the\n reading measure: Display is 3rem, and a 42rem column is fourteen of those. The new\n `h5` and `h6` treatments are new, so a document that reached past `h4` before now\n has an answer rather than inheriting nothing. **The consequence is stated rather\n than left to be found.** A Block-composed `h2` is 2.25rem and 3rem above `sm`; a\n prose `h2` is 1.875rem and does not grow. They are 1.2 apart at the base width and\n 1.6 apart above `sm`, where they were 1.25 apart before this change, so a consumer\n who sets a `Prose` section beside a Block section sees two tiers rather than one.\n `DESIGN.md` under Typography, Hierarchy now says so, and says which to reach for.\n `DocsShell` runs `Prose` at `fullWidth`, so the frame decides the width: 35rem of\n article track at a viewport of 1216px or wider, and less below that, which makes\n the case for entering one rung down stronger there rather than weaker.\n \n **`Heading`'s free `size` prop is unchanged, and this is stated rather than\n implied.** `size` is a choice made beside the tag, for a heading the surrounding\n document has already placed and wants attuned, and its largest arm is a step of\n the scale rather than a rung of the ladder. Its two largest arms were not removed\n and no new ones were added, so no consumer's call site changes and there is still\n no second route to a page `h1` from inside the library. `search-page` and\n `status-page` are the two surfaces that take a heading's *tag* from\n `childLevel(headingLevel)` and its size from this prop, and they are left as they\n are: those are labels over a list of results and over a list of incidents, and a\n caption is not a rung.\n \n **`scripts/check-heading-scale.mjs` now holds three things it described but did not\n check.** Its rule 4 claimed a table that jumps is a finding and only tested for a\n table that rises, so a table that skipped a rung passed it: descending, reaching\n the ceiling and flooring at or above Body, and rendering the page `h1` one gap\n above its own `h2`, which is the defect these steps exist to remove. And the\n literal Display step it refused on `Cta01` was written out as `text-3xl` and\n `sm:text-4xl`, which stopped being Display the moment the ceiling moved. Both are\n now read from the authored scale and the shipped table rather than restated, and\n both have a staged fixture in `scripts/__tests__/heading-scale.test.mjs`. The third\n is the Item rule below, which is new rather than a restatement.\n \n **The Item document's own table was the retired ladder, and it now states this\n one.** `apps/site/items/component/layout/section/section.mdx` is the page a reader\n is on when they ask what `as` resolves to, and it listed `h1` at `text-3xl` and\n `h4` through `h6` all at `text-lg`: the table as it was before this change. It now\n states the ladder the Component renders, and the sentence under it about where the\n scale stops has been corrected with it, because a corrected table beside a stale\n paragraph is the same disagreement one row down.\n \n **The gate also reads the Item document now, and that is the third copy of the\n table.** `apps/site/items/component/layout/section/section.mdx` states the ladder\n in its prose, and neither the declaration build nor the corpus reaches it, so when\n the ceiling moved it kept publishing the retired table. The new rule compares it to\n the same `HEADING_SIZE` the JSDoc is compared to, by the same cell-for-cell shape,\n so it names no step of its own; it reads the whole Item rather than a slice, so a\n second table in that prose is a finding; and it fails an Item that states no table\n at all, because a comparison with no rows finds nothing to disagree with. Three\n staged fixtures cover the three cases.\n \n **What the gate still does not hold is written down rather than left to be\n discovered.** `Cta01` is named as a root because it is the one Block whose heading\n cannot be composed from `SectionHeading` at all, so a literal step on it has no\n table to fall back to. Beyond that file the gate cannot go, and the reason is\n stated rather than left to be found: whether a heading a Block renders is a rung of\n the ladder or a caption inside that Block is not decidable from source.\n Forty-eight headings across `packages/ui/src` and `apps/site/src` name a step of\n their own, the eleven rungs among them now ask the table, and the thirty-seven that\n remain are captions. Two surfaces bind a caption to `headingLevel` rather than to\n `childLevel`, so a rule reading the tag cannot sort them, and a root list holding\n the rungs would be a list somebody maintains. That is how the `Cta01`-only rule\n came to hold the one Block already fixed and pass over the four that were not, and\n the count is written into `docs/quality-gates.md` so the next reader starts from it.\n `TokenBrowser` adds one more to that population, which is the point of naming a\n caption's step rather than inheriting it: the scan can see it, and it is below the\n floor by arithmetic.\n \n **The limit of the scan that took that count is now written down too, and it is the\n more useful half of it.** The count came from looking for a literal size utility\n beside a heading tag, and a heading whose size it inherits names nothing: the five\n theme names above carried no size at all, and two scans of exactly that shape passed\n them at 16 pixels and 500. A size that is inherited is a size nobody chose, and \"no\n literal\" is invisible to a scan that looks for literals, so that count has never\n been a count of every heading that names a step, and the number nobody chose is the\n half of it that reached a reader. What the count is good for is the arithmetic: a\n heading it reports is a rung if the step it names is at or above the step Body is set\n at, and a caption if it is below, so the two sort without anybody deciding which is\n which. The remedy for what the scan cannot see is on the surfaces rather than in the\n gate, and it is the one the token inventory now uses: a caption names its own step,\n so it is visible to the scan and it is below the floor by definition.\n \n The bump is a minor in both packages rather than a patch because a new authored\n step is a new public contract: `--text-5xl` and `--text-6xl` with their\n `--line-height` companions enter the emitted CSS and the published pack contract,\n and every heading on every page composed from a Block moves.\n- 1a0ba35: `BlogPostPage` refuses a post whose display date is its machine date\n \n Measured on `www.nanisoft.com/blog/where-the-products-stand`: the page rendered the date\n as `2026-09-26`, the raw machine value. All four NaniSoft sites pass one raw ISO string to\n both `date` and `dateTime`, so the reader was handed a machine value and the `datetime`\n attribute was handed the same one.\n \n **The contract stands and the Page now holds it.** `date` is the words a reader sees and\n `dateTime` is the machine value on the `time` element, and they are separate props\n precisely so the two can differ. Formatting is a locale decision, so it belongs to\n whoever owns the fact, and Prism renders `date` verbatim. What was missing was any way for\n the library to notice when a caller did not do that: a prop called `date` that takes a\n `string` is not obviously wrong, the defect is invisible to every reviewer who does not\n open the page, and a JSDoc block is read only by whoever is integrating the library. So\n the Page now throws when `date` and `dateTime` are the same string, naming the fix in the\n message. `StackGrid01` refuses its own missing `ownLabel` for the same reason and by the\n same mechanism, and a rule that is only written down is a rule four repositories read and\n none of them are caught by.\n \n **The refusal is on the pair, not on the shape of either value.** A site that writes\n `2 Sep 2026` and a site that writes `2026-09-26` both pass. What fails is handing the\n Page no display formatting at all, and the machine value is already on the element in the\n `datetime` attribute for every feed reader and every index, so displaying it a second\n time is a redundancy rather than a choice.\n \n **`dateTime` is typed as the shape it is**, `` `${number}-${number}-${number}` ``, so a\n formatted string passed there is a compile error rather than an attribute no reader can\n check. `date` stays a `string`, and the reason is in the Component's JSDoc: no TypeScript\n type separates `21 September 2026` from `2026-09-26`, which is exactly why the refusal\n above has to exist at run time.\n \n **This is a breaking change and it is released as a minor.** It breaks every consumer at\n build time, so it is a break and not an additive change, and each of the four call sites\n below has to change in the same release:\n \n - `alphalens/app/blog/[[...slug]]/page.tsx`, lines 121 and 122\n - `atlas/app/blog/[[...slug]]/page.tsx`, lines 137 and 138\n - `landing-page/app/blog/[[...slug]]/page.tsx`, lines 119 and 120\n - `nexus/app/blog/[[...slug]]/page.tsx`, lines 135 and 136\n \n Each is the same two lines, `date={page.data.date}` and `dateTime={page.data.date}`, and\n each needs a formatted string in the first. `CONTRIBUTING.md` says a breaking change is a\n `major`; `DESIGN.md` records the decision that a minor is the right line for a break from\n a `0.x` version, with the reasoning that taking `1.0.0` would declare the public API stable\n rather than describe the change. That reasoning is dated and deliberate, so the minor is\n taken here and the disagreement with the contributing guide is recorded rather than\n silently resolved.\n \n `DESIGN.md` states the rule once, in **The composition layers** beside the other Page\n rules. Nothing else about the Page changed: same props, same elements, same `data-slot`\n values, and it is still a server Component with no client code and no router.\n\n### Patch Changes\n\n- 1a0ba35: `Diagram` labels stay legible on a phone, and no stroke crosses one\n \n Two defects, both measured on the five-node architecture drawing the company\n site renders. Its node names were drawing at 6.95 CSS pixels and its relation\n words at 5.88 in the 342 pixels a phone leaves the drawing, and ten of the\n twenty-five label and stroke pairs on that drawing intersected: all five node\n names were struck through by an edge leaving their own node, and all five\n relation words sat on their own line.\n \n **Labels now have a floor, and it is the drawing's own coordinate space.** Type\n inside a scaled `<svg>` is sized in user units, so a label's rendered size is its\n size in user units times the rendered width over the canvas. That is why the\n canvas is fixed, and it is not the whole answer, because it has no floor in it.\n The drawing is now never rendered narrower than 640 pixels, one user unit is\n therefore never less than one pixel, and the smallest label the drawing sets is\n twelve units, which is what `text-xs` resolves to. A container that cannot give\n it that much scrolls sideways rather than letting the labels fall through it,\n which is the arrangement `Table` already makes with seven columns and `Gantt01`\n with a schedule of names and bars.\n \n The cost is stated rather than hidden: on a phone a reader sees about half the\n drawing at a time and pans for the rest. The alternative was the whole drawing at\n 5.88-pixel type, and `DESIGN.md` records that trade under **The figure floor**.\n Nothing above 640 pixels of container changes at all, so no desktop or tablet\n rendering moves.\n \n **Strokes now stop where the ink runs out, and each label is asked on its own.**\n A node's name is printed under its mark, so a relation drawn centre to centre ran\n through the name under whichever node it left downward. The first version of this\n fixed that by trimming every stroke back to the box enclosing the mark and every\n label beside it, which is also wrong in the other direction: a relation along a\n row never touches the name printed under its mark, so backing it off by the\n half-width of that name detached it from the mark it connects. On three things on\n one baseline with names at length, every end of every relation stood sixty units\n clear of its mark. A relation is now trimmed against whichever comes first, its\n own mark's edge plus the clearance or the far side of an individual label the\n stroke actually runs through, so a relation leaving sideways reaches its mark and\n one leaving downward still stops below the name in front of it. The whole line\n between the two marks is divided once rather than each end trimmed alone, because\n a name and a subtitle sit below their mark at two depths and a stroke arriving\n from below runs through the first and back into the second.\n \n The box is computable from a server Component because every label in a drawing is\n set in the monospaced face, where a label's width is its character count rather\n than a measurement. That is what `DESIGN.md` records under **The ink-avoidance\n rule**, with the reason a halo was rejected and the direction clause that the\n first form of the rule was missing.\n \n **The drawing is wrapped in a container.** It carries `data-slot=\"diagram\"` and\n still scrolls, and the wrapper carries `data-slot=\"diagram-container\"`, following\n `Table`'s `table-container`. Every existing `data-slot` value, the derived or\n passed `aria-label`, `role=\"img\"`, and `data-unresolved-relations` are unchanged,\n so no call site and no test that addresses the drawing by slot needs to move. The\n Component is still a server Component with no client code.\n- 1a0ba35: `FactList` sets its values flush left, and says why there is no numeric mode\n \n Rendered on `www.nanisoft.com/about`: a three-row fact list where every value was set\n `text-align: right`. The middle answer is long enough to wrap, so its second line began\n wherever the first one ended and the column read as ragged-left, which is what a mistake\n looks like rather than what a decision looks like.\n \n **Right alignment is the right answer in a column of figures, and `FactList` has no\n figures.** That was the question to settle before changing anything, because the two\n answers need opposite fixes. `FactList` takes `label` and `value` and nothing else: there\n is no `numeric` prop, no `tabular-nums` anywhere in the module, no monospaced arm and no\n per-row alignment of any kind. So nothing in it is a figure, nothing lines up on its last\n digit, and what right alignment moved was the first letter of each answer to a different\n place on every row. The suite asserts the absence as a fact rather than restating a prop\n list, so a `numeric` prop added later fails there instead of quietly making the default\n wrong for every caller that never passes it.\n \n The value is now `text-left`, and `text-balance` stays, because it is the same reason it\n was there: a two-line answer should break where it breaks well rather than where the\n measure runs out.\n \n A caller that genuinely wants a right-aligned column of figures is not refused anything.\n `value` is a `ReactNode`, so the caller can pass the span it wants, and Prism holds no\n opinion about a value it did not lay out.\n \n Nothing else about the Component changed: same element, same props, same `data-slot`\n values, same server Component with no client code.\n \n **One thing found and not changed, because it is a different claim.** The Component's own\n JSDoc says the term column is a fixed fraction rather than a content width, and the code\n says otherwise: the `dt` carries `shrink-0` and no basis, so the term column is sized by\n its content and the left edge of the value column is therefore not straight down the\n list. That is a real mismatch between a documentation source and the thing it documents,\n and left-aligning the value does not make it worse or better. Fixing the code to match\n the sentence would move every fact list in four consumer repositories, so it is a decision\n for the maintainer and not a side effect of this one. Recorded here rather than made.\n- 1a0ba35: `StackGrid01` lifts both arms of the grid to the type floors\n \n Measured on `nexus.nanisoft.com`: the in-house arm set its tile names at 14 pixels and\n the line under them at 12, so a heading on this Block rendered smaller than the copy\n around it and the tile read as a caption rather than as a tile.\n \n **Both arms are raised, and the reason is a floor rather than a comparison.** `DESIGN.md`\n already states both, which is why this is a Component coming into line rather than a new\n rule:\n \n - A tile name is a card title, and `CardTitle` says a card title sits at Body. Body is\n `text-lg` in this system, 18 pixels, so a name at 14 was below it.\n - A role and a blurb are supporting copy, and `DESIGN.md` gives supporting copy\n `text-sm`, 14 pixels, so a line at 12 was below that too.\n \n The first is the floor the heading ladder stops to avoid: `lg` is the deepest authored\n step that is not smaller than Body, precisely so that a heading never renders smaller\n than the copy it introduces. A tile name at 14 against copy at 18 broke exactly that.\n \n **The two arms set the same steps in both, which is a fact and not a comparison.**\n They already agreed here, so this was not a reconciliation and the reported arms\n never disagreed: what agreed was below both floors. The difference the two arms are\n supposed to carry is a claim about provenance, and it is carried in weight and ink,\n the muted tile against the page with a `border-primary` hairline and the `md` shadow\n step. A second group set quieter than the first would be a grid claiming the in-house\n work matters less than the assembled parts, which is the opposite of what the group is\n for. The assertion in the suite states the agreement as a fact so it cannot drift.\n \n The mono annotation over an in-house name and under a codename is left at `text-xs` on\n purpose. That is a machine annotation in the platform monospace stack, which is what\n `DESIGN.md` uses the mono face for, not a run of prose, and it is the only type on the\n tile that is not prose.\n \n No `data-slot` changed, no prop changed, no field was added to either arm and neither\n arm was widened, and the Block is still a server Component with no client code. The\n tile's own height grows by the four pixels of the name plus the two of the line, which is\n what the floors cost and not a change of rhythm: the grid was already `h-full` per row, so\n a row of tiles is as tall as its tallest and both arms move together.\n- 1a0ba35: `SiteFooter` links are a 24 pixel target instead of a 17 pixel one\n \n Measured in the built exports of three consumer sites: six to eight destinations per\n footer, every one of them 17 pixels tall (`39x17`, `30x17`, `42x17`, `33x17`, `70x17`,\n `37x17`), with the code a reader can see at\n `https://www.w3.org/TR/WCAG22/#target-size-minimum` asking for 24 by 24.\n \n **The link had no box.** A bare inline anchor is not a 17 pixel target by design; it\n has no box at all. What a pointer aims at on one is the font's content area, and for\n the shipped face at `text-sm` that is 16.94 pixels: fourteen pixels of type times the\n face's own ascent and descent, times its size-adjust. Those three numbers are not a\n guess, they are read out of the `@font-face` this package ships for `Inter Fallback`,\n and reading them is what produced 16.94 against the 17 in the export. The social row\n was the other shape and the other number: an `inline-flex` box, so its height is its\n line box, 20.01 pixels.\n \n **The floor is paid on the box, so the link looks the same.** Both links are now\n `flex min-h-6 items-center`: a block at least `--spacing-6`, 24 pixels, tall, with the\n same fourteen-pixel type at the same weight in the same colour inside it. It is the\n arrangement `mobile-nav` already uses for a stacked navigation link at the\n coarse-pointer floor, one step lower and at every pointer rather than only on a coarse\n one. `--spacing-6` is arithmetic on the authored spacing multiplier the way every\n `size-*` and `h-*` in this package is, so a retune of the scale moves the floor and the\n Block could not have pinned it.\n \n **No height anywhere moves.** A footer column link sat in a line box built from the\n footer's inherited sixteen-pixel body, so each row was already 24 pixels tall before the\n floor and each row is the same 24 after it; only the 17 pixels of target inside the row\n grew. The footer's own height is unchanged at both widths, which is the outcome four\n consumer pages are composed against, and the assertion that says so is in the suite.\n \n The 24 pixel circle exception in the criterion arguably held, because consecutive rows\n in a footer column stand 32 pixels apart centre to centre and a 24 pixel circle on\n either clears the other. The fix is made anyway, and `DESIGN.md` records why under\n **The target-size floor**: a target whose size depends on the gap beside it is one edit\n away from failing and nothing in this repository would report it.\n \n No `data-slot` changed, no prop changed, no call site moves, and the Block is still a\n server Component with no client code. `DESIGN.md` states the rule once, in the section\n beside **The coarse-pointer floor** it is deliberately not merged with.\n- 1a0ba35: `DocsShell` marks where its rails cut off, drops the rows it cannot name, and reaches a tablet\n \n Three defects on the one documentation screen the other NaniSoft sites import.\n \n **The contents rail hid a third to a half of the navigation behind an invisible\n scroll.** The rail is capped against the viewport and scrolls inside that cap, and\n Chrome's scrollbars are overlay scrollbars, so they appear only once the reader has\n already scrolled. Measured in two consumers' built exports at 1280 by 680, the rail's\n content stood at 912 and 1318 pixels in a 568 pixel box: 344 and 750 pixels of a\n primary documentation navigation below the fold with `mask-image: none`, no\n persistent scrollbar, no fade and no count. The last visible row was cut mid-word\n above dead space, which reads as a rendering fault rather than as a scroll region.\n A cap nobody can see is not a cap.\n \n **The affordance is a fade across the foot of each rail, and the space it covers is\n reserved rather than borrowed.** The sticky offset moved to a wrapper that does not\n scroll, because nothing inside a scroll container stays put while that container\n scrolls: a gradient or a mask written into the scrollport travels with the content\n and marks nothing. The wrapper travels, so the gradient is pinned to its foot, and\n the `<nav>` inside keeps the cap, the independent scrolling and the whole of its\n class contract. The rail reserves `padding-bottom` of the same height as the fade,\n which is the half that makes it honest: at rest the gradient falls on reserved space\n and the last row is fully legible, and only while there is more below does it fall on\n content. Without that padding the same gradient washes the last row permanently,\n which is the usual cost of this answer and the reason it is not optional.\n \n Two alternatives were rejected for stated reasons rather than passed over. A\n persistent themed scrollbar is truthful at both ends and costs nothing to\n legibility, but it says \"there is more\" without saying which way, and it needs its\n gutter reserved in a 15rem rail so a short tree becoming a tall one does not shift\n the content. A collapse control, and an \"N more\" control, both need state or a count\n this Page cannot have: one is a client Component and the other is a measurement a\n server Component cannot take.\n \n The fade costs a keyboard and a screen-reader reader nothing. It is an empty element\n with `aria-hidden` and `pointer-events-none`, inside the `<nav>` and outside the\n `<ul>`, so it is not a list item, it is never announced, it never takes a press and it\n never reaches a Tab stop. The region still scrolls on its own, so the arrow keys walk\n every entry exactly as before.\n \n **The Page can emit a navigation row containing nothing, and now refuses to.** An\n entry whose `title` is the empty string rendered as\n `<li data-slot=\"docs-nav-page\"><span data-slot=\"docs-nav-label\"></span></li>`: a row of\n nothing inside a `<nav>`, which one consumer measured at 102 across 25 of its 31\n documentation pages, because that site maps a heading whose title arrives as a React\n element to `''`. A blank row is not a destination, and it is not a label either,\n because a label is words. The Page now drops the entry, drops a group label it cannot\n name while keeping the pages under that group, and drops a group that has neither\n words nor pages. Nothing is swallowed: the rail carries the tally as\n `data-unnamed-entries`, the same answer `Diagram` gives a relation it cannot resolve,\n so a consumer can tell \"nothing was wrong\" from \"the Page stopped counting\".\n \n A blank `title` and a blank `href` are still answered differently, because they have\n different amounts left. A page with an address and no words is a real page with one\n unrenderable field, so losing the row costs a reader nothing while taking the screen\n down over it would cost them everything. A page with words and no address is a row\n whose entire content is a route that does not exist, and that is still refused by\n name.\n \n **Below `lg` there was no navigation at all.** At 768 and at 1024 both rails were\n `hidden` and the only navigation left inside the article was the two-item pager at\n its foot, so a reader on a thirty-one document reference at tablet width had no\n contents, no on-this-page and no way to a sibling page except Previous and Next. Each\n tree is now also drawn behind a `<details>`, first in the document so it is what a\n narrow reader meets before the text. `<details>` is the disclosure that needs no\n runtime: the platform holds the expanded state, the control is in the Tab order with\n a real expanded state, and the Page stays a server Component with no hook, no context\n and no handler. The summary carries `navLabel` or `tocLabel`, which is the caller's\n own word for that navigation, so the Page ships no reader-facing copy and a\n consumer that files its documentation in another language gets the control in that\n language by having named the region once.\n \n The tree is therefore in the document twice, and only one copy is displayed at any\n width: the rail is `hidden lg:block` and the disclosures are `lg:hidden`, so the\n other is `display: none` and is out of the accessibility tree and out of the tab\n order rather than a second list a reader meets.\n \n **What did not change, and what a consumer may notice.** Every existing `data-slot`\n value stays, so no call site and no test that addresses the Page by slot needs to\n move; three new slots are added, `docs-rail-fade`, `docs-nav-compact` and\n `docs-nav-disclosure`. The rail's `<nav>` now sits inside a wrapper, which is what\n lets the fade stay put, so a consumer stylesheet reaching for it as a direct child of\n `[data-slot=\"docs-rail\"]` reaches the wrapper instead. `data-slot` is documented as\n markup metadata rather than a styling API, and `Diagram` and `PulseGraph` took the\n same wrapper for the same reason in the same release line.\n \n The bump is a patch rather than a minor because nothing a consumer composes through\n widens: no prop is added, no export is added, no token and no name in the contract\n move, and no call site changes. What changes is what the Page draws from data it\n already accepted.\n \n **The one thing this leaves alone is stated rather than left to be found.** Below\n `lg` the frame is a single column and this Page runs `Prose` at `fullWidth`, so the\n frame decides the track: the article column is the whole container, which is 45rem at\n 768 and 61rem at 1024, against the 35rem it gets at 1216 and above. That is a wider\n measure than the one the reading column is held to, in the band between the two\n breakpoints, and it is a frame decision rather than a `Prose` one. A phone at 390\n gets 21rem and is inside the measure. Capping the article track below `lg` is a\n deliberate layout choice with two honest forms, a centred column at the measure or a\n two-column frame from `md`, and either one changes what a narrow reader sees on\n every documentation page in the family, so it is left to the maintainer rather than\n taken here beside two defects that had no such choice in them.\n- 1a0ba35: `PulseGraph` labels stay legible on a phone, and no stroke crosses one\n \n Three defects, all measured on a four-stage pipeline the way `Diagram`'s two were\n measured on the five-node architecture drawing the company site renders.\n \n **Labels now have a floor, and it is the drawing's own coordinate space.** Type\n inside a scaled `<svg>` is sized in user units, so a label's rendered size is its\n size in user units times the rendered width over the canvas. That is why the\n canvas is fixed, and it is not the whole answer, because it has no floor in it. In\n the 356 pixels a phone leaves the drawing, a stage name was rendering at 7.23 CSS\n pixels and a node's note at 5.56, and ten user units was the smallest type the\n whole package emitted anywhere, including inside a figure. The drawing is now\n never rendered narrower than 640 pixels, one user unit is therefore never less\n than one pixel, and the smallest label the drawing sets is twelve units, which is\n what `text-xs` resolves to. A container that cannot give it that much scrolls\n sideways rather than letting the labels fall through it, which is the arrangement\n `Table` already makes with seven columns. Nothing above 640 pixels of container\n changes at all, so no desktop or tablet rendering moves.\n \n **Connectors now stop where the ink runs out, and each label is asked on its own.**\n A node's name is printed under its mark, so an edge drawn centre to centre ran\n through the name and the note under whichever node it left downward. On the\n pipeline this fix lands on, eight of the thirty-two label and connector pairs\n intersected: the two stages nearest the top of the canvas, each with its own name\n and its own note struck through by the two edges that met it. The first version\n of this trimmed every connector back to the box enclosing the mark and every\n label beside it, which fixes the crossings and detaches the drawing instead: a\n stage's name and note both sit below its mark, so a connector along the rail\n touches neither, and backing it off by the half-width of the widest name at the\n node left four of the five connectors on the six-stage figure floating free of\n both their marks, by up to fifty-five units. A connector is now trimmed against\n whichever comes first, its own mark's edge plus the clearance or the far side of\n an individual label it actually runs through, so all five connectors on that\n six-stage figure touch both their marks with the clearance between them and the\n stroke is thirty to fifty units longer, and a connector leaving a node downward\n still stops below the name in front of it. The whole line between two marks is\n divided once rather than each end trimmed alone, because a name and a note sit\n below their mark at two depths and a connector arriving from below runs through\n the first and back into the second.\n \n You do not have to space your nodes to avoid this, and you always had to space\n them to keep two names from touching each other.\n \n **The rail no longer sits on a note.** The rail was a fixed distance up from the\n bottom of the canvas while a node's note reached a fixed distance below its mark,\n so the two met on one range of mark positions: a lane whose mark landed low enough\n put its note under the rail's own stroke. The fitted area now stops above the band\n the rail reserves, which is the rail's distance up from the bottom plus its own\n thickness, the drawing's ink clearance, and the deepest ink a node draws. A graph\n with no lanes has no rail, so nothing reserves the foot and the fit is unchanged.\n \n **The drawing is wrapped in a container.** It carries `data-slot=\"pulse-graph\"` and\n now scrolls, and the wrapper carries `data-slot=\"pulse-graph-container\"`, following\n `Table`'s `table-container` and `Diagram`'s `diagram-container`. Every existing\n `data-slot` value, the `role=\"img\"`, the passed `aria-label`, `data-lanes` and\n `data-unresolved-relations` are unchanged, so no call site and no test that\n addresses the drawing by slot needs to move. The Component is still a server\n Component with no client code.\n \n `DESIGN.md` records the two rules this applies under **The figure floor** and **The\n ink-avoidance rule**, the latter with the direction clause that the first form of\n the trim was missing, and the geometry both Components now share is one internal\n module rather than a second copy in each.\n- Updated dependencies [1a0ba35]\n - @nanisoft/prism-tokens@0.16.0\n\n## 0.15.0\n\n### Minor Changes\n\n- 6dc7bb8: A Block action is a destination or your own control, and a gate now holds it\n \n **Five Blocks shipped a control that could not be acted on, and the previous release\n fixed three of them without the law.** `Hero01`, `Hero02` and `Hero03` each declared\n an action union whose `href`-less arm rendered a bare `Button`, and that release closed\n the arm and then said, in its own changeset, that seven sibling Blocks still had it and\n that \"a law held by a type in three Blocks is three types\". That was the right\n reasoning and it was also the reason the hole stayed open: the next audit found five\n more Blocks, and one of them had a shape nobody had looked for.\n \n **`About01` and `Showcase01`** declared the same two-arm union as the heroes and\n rendered a `Button` on the arm with no `href`. Each one's JSDoc called it \"inert by\n design\" and defended that as a Block shipping no behaviour, which is true and is not an\n answer. Both are now a union of two named arms: `AboutLinkAction` /\n `AboutSlotAction` and `ShowcaseLinkAction` / `ShowcaseSlotAction`. `href` is required\n on the link arm; the slot arm carries the caller's own `ReactNode` and forbids `label`,\n `href`, `newTab` and `variant`.\n \n **`PageHeader01` was worse, and its own type said so.** `PageHeaderAction` had **no\n `href` member at all**, so there was nothing to make optional: `{ label: 'Share' }` was\n the only value the type could express and every value rendered a `Button` that\n activated to nothing, at the top of every screen a consumer rendered. It is now the\n same two-arm union. Its `actionsSlot` sibling prop is **gone**, and that is the one\n structural change here: a sibling slot renders before the row's own actions and cannot\n say which position it fills, so a caller who wanted a primary menu trigger at the front\n and a link behind it had to put the link first and accept the order, or pass the trigger\n in `actions` where it was a dead button. As an arm, position and element are the same\n value.\n \n **`Pricing01` was the one no audit had found.** `Plan` carried a required `cta: string`\n and the Block rendered it as a `Button`, so every pricing table in every consumer's\n product shipped a focusable control, announced as a button, that activated to nothing, on\n the card a reader was about to decide on. `cta` is replaced by `action`, a two-arm union,\n and a plan whose control is a checkout trigger is the `slot` arm. The featured plan\n still draws a filled anchor and the rest an outlined one, so the recommendation still\n reads. `pricing-01/index.tsx` also exported the value alone until now, so a consumer\n could not name `Plan` at all; `Plan`, `Pricing01Props` and the three action types are on\n the surface.\n \n **`Plan` now carries a required `id`, which is the second change to one record and was\n deferred from this entry once.** The first version of this changeset recorded the\n observation and the reason for leaving it: *\"a second breaking change to a record this\n release already changes, and bundling two migrations into one entry makes both harder to\n read. It is a follow-up, not an oversight.\"* Both halves of that reasoning have since\n stopped applying, and the second one was never quite true.\n \n The `id` is real. `Pricing01` keyed its cards on `plan.name`, which is the word a reader\n reads on the card, so a monthly and an annual row of the same tier produced a React\n duplicate key and cards the renderer had been told are ambiguous, and a rename in the\n copy was a rename in the key, which loses a caller's saved selection over an edit to a\n sentence. `Careers01` and every other Block in this package key on an `id` and say why;\n `Pricing01` was the last one that did not.\n \n The bundling reason does not survive contact with the release. There is no released\n boundary between the two changes, because both are in this same unreleased body of work,\n so \"bundling\" describes nothing a reader would ever have to unpick: they upgrade once\n and read one entry either way. And one `Plan` with two migrations in one place is more\n readable than one `Plan` with two migrations in two places, because the second entry has\n to re-establish that it is talking about the same record. It is in this entry for that\n reason and the deferral was the mistake, not the delay.\n \n **`Waitlist01` is the fifth, and it is not a union.** `Waitlist01Referral` declared\n `copyLabel` and `copied`: the accessible name of a copy control and the state to draw on\n it. The Block then rendered `<Button type=\"button\">{copyLabel}</Button>` with **no\n handler**, beside a read-only field holding the code it was labelled as copying. The\n JSDoc described a second weight and a `data-copied` attribute for a control that never\n copied anything. Both props are gone and `copyControl` is a `ReactNode`. Prism will not\n call `navigator.clipboard` on a consumer's behalf and then report a success it cannot\n verify: the permission is the caller's to grant, the secure context is the caller's to\n know about, and a copy can fail. The sentence that says it worked belongs in `status`,\n which is where it already belonged.\n \n **The four Blocks whose union renders nothing on the `href`-less arm were already\n correct, and the audit that named them was wrong.** `Careers01`, `CaseStudies01`,\n `Industries01` and `Services01` each declare an item union whose no-link arm renders **no\n control at all**: the `CtaLink` is inside `isLink ? … : null`. That is the right shape\n for a card, where a link is an addition rather than a required part, and there is nothing\n to repair. `Gallery01` is the same story with a different reason: its tile is a real\n `<button>` with an `onClick` that opens the lightbox, in a `'use client'` Block. Five of\n the nine names in the earlier changeset were false positives, and saying so is part of\n the record: a list of sites is a claim about a tree, and the tree is what settles it.\n \n **`scripts/check-block-controls.mjs` is the gate, and it is green.** The previous release\n declined to write one because it would have failed on five real Blocks, and that was the\n right call at the time. It can be green now, so it exists. It reads `packages/ui/src/blocks`\n and `packages/ui/src/pages` and fails on a rendered `Button` or `CtaLink` that carries\n none of `onClick`, `type=\"submit\"` or `type=\"reset\"`, and no `href`. It deliberately does\n **not** read `apps/site/items`, because the documentation Demo for `Button` renders\n `<Button>Save changes</Button>` with no handler and showing the control is the whole\n point of showing it; the demos are held by the compiler, because a demo passes a Block's\n props and fails to build the moment `AboutAction` stops accepting a dead action. The\n gate's own proof is `scripts/__tests__/block-controls.test.mjs`, which plants each of the\n five shipped shapes as a fixture, because the five Blocks that shipped them have since\n been fixed and a gate whose only evidence is a clean run is a gate nobody has watched\n fail.\n \n Two things the first version of that gate got wrong are recorded because they are the\n kind of defect a clean run hides. It deleted comments rather than blanking them, which\n removed their newlines and printed every finding below the first JSDoc block on the\n wrong line. And it matched a tag's attributes with `<Name([^<>]*)>`, which stops at the\n first `<` or `>` in the attribute text, which in JSX is very often not the end of the tag:\n `onClick={() => step(-1)}` contains `=>`, `disabled={position <= 0}` contains `<=`, and\n `disabled={currentPage >= pageCount}` contains `>=`. Each truncated the attributes before\n the handler two lines further down, so three Blocks with working handlers were reported\n as shipping dead buttons. The tag end is found by brace and paren balance now.\n \n **The bump is `minor` rather than `major`, and the argument is the version line rather\n than the severity.** Five Blocks' public prop types change and two of them (`Plan.cta`\n and `Waitlist01Referral`) do not compile against a value that compiled a release ago.\n That is breaking by any ordinary reading and this entry says so plainly. `major` in this\n repository publishes `1.0.0`, and every release to date has been `minor` on a `0.y.z`\n line where the `y` is already the breaking-equivalent slot; publishing 1.0.0 with this in\n it would assert an API stability guarantee this library has not earned, and it would be\n the first release in the project's history to use the bump at all. The same argument\n carried the hero change and the `Cta01` change at 0.6.0. The break is real and is the\n subject of this entry; the number it lands on is the line's decision.\n \n Migration, per Block:\n \n ```tsx\n // About01, Showcase01, PageHeader01: an action with no href\n // Before\n actions={[{ label: 'Start free' }, { label: 'Docs', href: '/docs' }]}\n // After\n actions={[\n { slot: <NextLink href=\"/start\">Start free</NextLink> },\n { label: 'Docs', href: '/docs' },\n ]\n \n // PageHeader01 only: actionsSlot is gone, and the node moves into the row\n // Before\n <PageHeader01 actions={[{ label: 'New deployment' }]} actionsSlot={<Menu />} />\n // After\n <PageHeader01 actions={[{ slot: <Menu /> }, { label: 'New deployment', href: '/deployments/new' }]} />\n \n // Pricing01: `cta: string` becomes `action`, and the card is keyed on an `id`\n // Before\n plans={[{ name: 'Team', price: '$24', features: [], cta: 'Choose Team' }]}\n // After\n plans={[{ id: 'team', name: 'Team', price: '$24', features: [], action: { label: 'Choose Team', href: '/signup?plan=team' } }]\n \n // Waitlist01: the copy control is the caller's, whole\n // Before\n referral={{ value: CODE, label: 'Your referral code', copyLabel: 'Copy code', copied }}\n // After\n referral={{\n value: CODE,\n label: 'Your referral code',\n copyControl: <Button onClick={() => navigator.clipboard.writeText(CODE)}>Copy code</Button>,\n }}\n ```\n \n Every one of these was a compile error before and is a compile error now, which is the\n point: a consumer upgrading finds a failed build rather than a page that silently renders\n nothing.\n \n **One thing about the `Pricing01` migration is separate from the rest of it.** Every other\n line above changed a member a consumer had to add. The `id` also changes what a consumer\n has to add to the same object, so a plan that compiles after this release and renders two\n cards correctly is a plan that passed an `id`. Nothing here needs a second entry, and the\n observation that started this is in the entry above rather than at the end of this one.\n- 6dc7bb8: A disclosure opens at the step the motion scale gives it, and the rail says why it is still a width\n \n `DESIGN.md` has said for a while that `duration-slow` is \"transform or layout\n state such as a disclosure\". Three disclosures animated a height or a width at\n `duration-base`, which is 160ms against the 280ms the scale assigns them, and both\n chevrons rotated at `base` beside a panel that now takes longer than the icon does.\n \n **`AccordionContent`, `CollapsibleContent` and `Sidebar` are on `duration-slow`,\n and so are the two chevrons.** A chevron and the panel it opens are one motion, and\n a 160ms icon against a 280ms panel is two events where the reader is watching one.\n \n **`Progress` stays at `duration-base` and this is the one exception the scale has.**\n A disclosure is a spatial transition that happens once, on a reader's click, and\n 280ms is what makes it read as opening. A progress indicator's value changes on\n every tick of a running job, often several times a second, and the reader did not\n cause it: at 280ms the bar lags the work it is reporting. It is the same\n distinction the two laws of motion draw, applied inside the feedback scale. An\n answer wants the long end of the band; a reading wants the middle of it.\n \n **`Sidebar`'s rail still animates `width`, and it is a named exception rather than\n an unexplained gap.** `Progress` and `RangeField` both express their value as a\n `scaleX` about the inline start, and a rail cannot: it carries a mark, a list of\n items with a 16px icon, each item's label and a trailing count, and a `scaleX`\n scales every one of them into ovals, condensed labels and unreadable numbers. The\n decisive part is that a transform cannot reflow text. At `scaleX(0.25)` the labels\n are still laid out for a 16rem column, so the rail's contents would never reflow\n into the 4rem column they are supposed to occupy, and the only property that does\n that is the one being animated.\n \n The cost is stated rather than argued away: it is a main-thread layout pass, once\n per reader's click, over one element with a small subtree. That is a different\n order of cost from a bar that advances on every frame of a running job, which is\n why the two were not the same decision.\n \n `DESIGN.md`'s motion section records all of it, so the table and the call sites no\n longer disagree.\n- 6dc7bb8: A form-level error marks the field it is about, not every field\n \n `AuthForm01` set `aria-invalid` on every field whenever a form-level `error` existed,\n so a reader tabbing through a sign-in card heard \"invalid\" on a correctly filled\n email address because the password was wrong. A form-level message is usually not\n about a control at all: bad credentials, a locked account and a rate limit are three\n things no field is wrong about, and the first thing a screen reader says about each\n field is the state, so a reader told two correct answers are wrong stops trusting the\n rest of the form.\n \n `AuthFormField` gains `errorId`. The Block marks the field whose `id` the caller\n names there and no other, and a caller with nothing to name leaves every field\n unmarked, which is the honest state: the message is still drawn in an `Alert`, which\n is announced when it enters, and WCAG 3.3.1 is answered by the message rather than by\n a mark on a control that is not the problem. Omitting `errorId` on every field is now\n the way to say \"this is about the submission\", and the JSDoc says so.\n \n **A field's description was drawn and never announced.** The `FieldDescription`\n carried no id and the input carried no `aria-describedby`, so a hint about a format\n or a constraint was on the page for a sighted reader alone. The id is derived from\n the field's own `id`, which is already the one stable caller-owned string on the\n field, so the reference costs a template and cannot collide. `login-01` and\n `signup-01` already wired the pair; this was the Block that had not.\n \n **`SettingsNotifications01` had the same unlinked description.** Each event's\n `FieldDescription` was drawn under the control's visible label and referred to by\n nothing, while the section note was referred to by every read-only switch. The one\n `aria-describedby` now carries both: the event's own description and, on a read-only\n control, the note that says why it will not move. A reader who meets one without the\n other has to work out which half they are missing.\n \n Two notes for a caller. `AuthForm01` renders no `aria-invalid` on any field unless a\n field names itself, so a form that relied on every field being marked has to name the\n one that is. And `SettingsNotifications01` writes the joined pair as a template rather\n than through a `join`, which is the same reason `MultiCombobox` does: a bare\n separator literal is a string the copy gate reads as a space somebody typed.\n \n No prop is removed and no import breaks.\n- 97d9b99: A heading's size follows its level, so a page `h1` and its section titles are no longer one size\n \n **If you composed a page from Blocks, every section title on it just got smaller.**\n An `h2` section title moves from 1.875rem to 1.5rem, and above `sm` from 2.25rem to\n 1.875rem. A page `h1` is unchanged at 1.875rem, and 2.25rem above `sm`. Nothing\n about the props you pass changes, and nothing about the outline changes: the same\n `headingLevel` you already pass now decides the size as well as the tag.\n \n `DESIGN.md` gave Display two roles at once, \"section titles, the CTA banner\n heading, and every page `h1`\", and `SectionHeading` implemented that literally by\n writing one class string for all six levels. So an `h1` and an `h2` came out\n byte-identical, and a landing page of a hero plus six Blocks showed one `h1` and\n six section titles at 36 pixels, with no hierarchy between what the page claims\n and what it elaborates. The level was already on every Block as `headingLevel`\n and `childLevel()` already existed to carry it down a level, so the document\n decided where each heading sits in the outline and the visual size simply never\n followed it.\n \n **The table, and where it stops.**\n \n | level | step | value | above `sm` |\n | --- | --- | --- | --- |\n | `h1` | `text-3xl` | 1.875rem | 2.25rem |\n | `h2` | `text-2xl` | 1.5rem | 1.875rem |\n | `h3` | `text-xl` | 1.25rem | 1.5rem |\n | `h4` | `text-lg` | 1.125rem | 1.25rem |\n | `h5` | `text-lg` | 1.125rem | 1.25rem |\n | `h6` | `text-lg` | 1.125rem | 1.25rem |\n \n It is the authored scale walked down one step per level, and it floors at `h4`.\n `lg` is the deepest authored step that is not smaller than Body, which is 400 at\n 1.125rem, so a heading one step further down would render smaller than the copy it\n introduces and read as a caption. `h5` and `h6` hold at that step rather than\n wrapping, which is the same trade `childLevel()` makes when it clamps at `h6`. A\n heading at the floor is still a heading: it keeps `font-semibold`,\n `tracking-tight` and `text-balance` at every step, so weight and tracking tell it\n apart from body copy where size no longer can.\n \n **Nothing goes above `4xl`, and no scale changed.** The step above Display does\n not exist and this does not invent one, so `DESIGN.md`'s ceiling holds untouched\n and `@nanisoft/prism-tokens` is unchanged. `h1` is Display because that is the\n role the document gives it, and `h2` is the step below.\n \n **No new prop.** The alternative was a `size` on `SectionHeading` and on every\n Block that renders one, which widens the public surface across more than a\n hundred call sites for a decision the surrounding document has already made by\n choosing a level. You get the hierarchy by passing the level you were already\n passing, and a Block moved from an `h2` section into an `h3` one carries its size\n with it, which is what `headingLevel` was introduced to do. Nothing about the\n no-override-path rule changes: a consumer who wanted Display for their thesis\n still has it, at `h1`.\n \n **`Cta01` follows its level too, and this is the one Block outside\n `SectionHeading`.** It draws a centred title on a filled primary panel with\n nothing under it, so the muted description colour and `gap-4` are wrong there and\n it resolves its own heading element. It was carrying the same hardcoded size, so\n fixing the Component and not it would have left a closing banner one step above\n every other section title on the page and level with the page's own `h1`. Its\n banner moves with everything else: an `h2` closing banner is 1.5rem, and at `h1`\n it is 1.875rem.\n \n **The blast radius, measured rather than guessed.** 132 `SectionHeading` render\n sites across 115 files resolve to this table. 6 are at `h1` and are unchanged.\n 125 are at `h2` and each moves from `text-3xl sm:text-4xl` to\n `text-2xl sm:text-3xl`. 1 is at `h3` and moves to `text-xl sm:text-2xl`. That is\n the whole change, and it is deliberately not softened: a documentation site\n whose own prose hierarchy shifts is a visible change, and the point of the fix is\n that the shift is the hierarchy coming back.\n \n **A gate now holds the table, and proof it fires.**\n `scripts/check-heading-scale.mjs` reads the table out of `section.tsx`, judges\n every step in it against the `text` group in the token source rather than a list\n beside the gate, and holds it against the markdown table the Component's own\n JSDoc states, which is the documentation source the declaration build preserves\n and the corpus reads. It fails on a level with no size, on a step the token source\n does not author, on `h1` and `h2` sharing one step, on a step-down table that\n rises, on a level deeper than the page heading rendering above it, on a floor below\n Body, on the heading losing its weight, tracking or balance, on the JSDoc and the\n code disagreeing, and on `DESIGN.md` giving Display the section title as well as\n the `h1`. `scripts/__tests__/heading-scale.test.mjs` stages each of those against\n a tree the gate reads, including the table the Component actually shipped before\n this change, and asserts the gate is red on it.\n- 6dc7bb8: A hero action is a destination or your own control, and never an inert button\n \n `Hero01`, `Hero02` and `Hero03` accepted an action of `{ label }` with no\n destination, and each Block rendered that as a bare `Button`. Every one of those is\n a focusable control, announced as a button, that activates to nothing, and it sat\n where a reader looks first: the primary call to action of a marketing hero, in\n three published Blocks, so every consumer of any of them shipped a dead button\n where the most important action should have been. Each Block's JSDoc called the\n button \"inert by design\" and defended that as a Block shipping no behaviour, which\n is true and is not an answer. A Block that ships no behaviour cannot make a control\n work, so it should not render a control at all.\n \n **The defect was in the type, not in the render, so the fix is a type.** The\n rendering half had already been repaired once: `CtaLink` renders a native anchor\n and `Cta01` requires `href`, which is why the three heroes, predating both, kept\n the old shape. What survived was the third arm, and it could only be closed by\n removing it.\n \n `HeroAction`, `Hero02Action` and `Hero03Action` are now a union of two named arms\n rather than one shape with an optional `href`:\n \n - `HeroLinkAction` (and its two siblings) **requires** `href`, so a value that is\n sometimes a string and sometimes `undefined` is a compile error rather than a\n control that navigates on the renders where the address happens to be there.\n `slot` is forbidden on this arm.\n - `HeroSlotAction` (and its two siblings) **carries `slot`**, the caller's own\n control, and forbids `label`, `href`, `newTab` and `variant`. It exists because\n a server Component cannot receive an `onClick`, so the honest form of \"this does\n something\" in a composed section is a node the caller renders. This is\n `Cta01`'s `actionSlot` moved inside the row, because a hero carries an ordered\n list of up to two actions and a sibling prop cannot say which position it fills.\n `check-block-imports.mjs` already holds that a `ReactNode` slot is how a consumer\n injects an interactive child without the Block owning any state.\n \n The Block places a slot and adds no class to it, because a class it adds to a node\n it does not render is a style the caller cannot see and cannot remove, and this\n package has no override path.\n \n **Nothing else moved.** The anchor is the element the link arm always produced, at\n the same size and the same variant, and the positional default variant still\n follows the position: the first action in the row is filled and the rest are\n outlined, whether the first one is an anchor or a caller's control. The forward\n arrow still marks the row's one primary destination and now, with every action\n Prism renders being a link, the position is the whole of the test. A `slot` at the\n front of the row wears no arrow, because the control the caller drew owns its own\n marks. Each Block is still a server Component.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** `{ label: 'Coming soon' }` compiled a release ago and\n does not now, which is breaking by any ordinary reading, and this entry says so\n plainly. `major` in this repository publishes `1.0.0`, and every release to date\n has been `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent\n slot. Publishing 1.0.0 with this change in it would assert an API stability\n guarantee this library has not earned, and it would be the first release in the\n project's history to use the bump at all. The same argument carried the `Cta01`\n change at 0.6.0. The break is real and is the subject of this entry; the number it\n lands on is the line's decision and the line is pre-1.0.\n \n Migration: every action in a hero now carries either a destination or your own\n control. Where you passed a label alone and meant a link, give it the `href` it\n was always declared with. Where you meant a control that is not a link, a router's\n own `Link`, a submit button or a menu trigger, move it into `slot` as the element\n itself rather than as a label:\n \n ```tsx\n // Before\n actions={[{ label: 'Start free' }, { label: 'Sign in', href: '/sign-in' }]}\n \n // After\n actions={[\n { slot: <NextLink href=\"/start\">Start free</NextLink> },\n { label: 'Sign in', href: '/sign-in' },\n ]}\n ```\n \n Both mistakes are now compile errors, which is the point: a consumer upgrading\n finds a failed build rather than a site that silently renders nothing.\n \n **The same arm was still in seven sibling Blocks when this was written, and\n `a-block-action-is-a-destination-or-your-own-control.md` closes them.** The audit\n behind this entry named `About01`, `Careers01`, `CaseStudies01`, `Gallery01`,\n `Industries01`, `Services01` and `Showcase01` as carrying the same two-arm union with\n a rendered `Button` on the arm that has no `href`, and `PageHeader01` as rendering\n its declared `actions` as buttons with no destination at all. **Five of those nine\n were correct and four were defective, and the later entry records which is which**:\n `About01` and `Showcase01` had the union, `PageHeader01` was worse and had no\n destination member at all, and `Pricing01` and `Waitlist01` were not on the list at\n all. `Careers01`, `CaseStudies01`, `Industries01` and `Services01` render **nothing**\n on their `href`-less arm, which is the right shape for a card, and `Gallery01`'s tile\n is a real button with an `onClick` in a client Block.\n \n This entry stands as the record of the heroes. The shape every Block now takes is the\n one adopted here, and `scripts/check-block-controls.mjs` is what holds it: the earlier\n reasoning in this paragraph, that \"a law held by a type in three Blocks is three\n types\", was right about the risk and wrong about the remedy, and the remedy is a gate\n rather than a shared type.\n- 6dc7bb8: A popup that matched nothing is still an open popup, and its message is not an option\n \n Four Components said \"collapsed\" while a popup was on the page, and two of them put\n the sentence saying so inside the listbox.\n \n `aria-expanded` was reading \"are there results\" rather than \"is the popup displayed\",\n and the two came apart in exactly the state a search field exists to render: a query\n that matched nothing. `CommandPalette` and `Combobox` both drew a bordered panel\n carrying the caller's empty sentence and told the field there was nothing to reach.\n `aria-expanded` is now the open state in all four, because that is what the\n attribute is for and because closing on a non-match tells the reader their keystroke\n broke the control, which `Combobox` already argued in prose.\n \n `aria-controls` pointed at a listbox that was not rendered. `Combobox`,\n `MultiCombobox` and `CreatableCombobox` omit the listbox entirely when nothing\n matched and left the reference written anyway, so the field pointed at an id nothing\n carried. **It now points at the popup rather than at the listbox inside it**, which\n is the decision the existing axe run forced and the right one on its own terms:\n `aria-controls` is a required attribute on an expanded `combobox`, so a field that is\n expanded and carries no reference is itself a violation, and the panel is what the\n field opened either way. `CommandPalette` points at the palette itself for the same\n reason. All four write the reference only while the popup is displayed, so a closed\n field carries none.\n \n **A listbox owns `option` and `group` and nothing else.** `CommandPalette` drew its\n no-results `<p>` inside the listbox and its group headings in a bare `<div>`, so the\n message was a row the index counted and no reader could choose, and the rows under a\n heading could not say what they belonged to. `CreatableCombobox` drew its\n `<p role=\"status\">` in the same place. In all three the empty state is now a sibling\n of the listbox rather than a child of it, which is the arrangement `Combobox` and\n `MultiCombobox` already shared, and each group wrapper is a `group` named by the\n heading already drawn above it through `aria-labelledby`.\n \n `CommandPalette` gains two things to know about: the two branches carry the same box,\n so the panel does not change size or position when the last result is filtered out,\n and its listbox now appears only when there is something in it, so a caller selecting\n `[data-slot=\"command-palette-list\"]` finds nothing in the empty state. The empty\n state carries `data-slot=\"command-palette-empty-state\"` on its box and the message\n keeps `data-slot=\"command-palette-empty\"`.\n \n No prop changes and no import breaks.\n- 6dc7bb8: `Prose` gives a table a scroll container, and the two search fields stop zooming an iPhone\n \n Four defects, three of them narrow-viewport, one of them about a reader's own\n settings. A consumer sees all four.\n \n **A table in a Prose widened the page instead of scrolling.** The Component's\n JSDoc has always listed tables among the content it accepts, and the code fence\n beside them has always scrolled, but the table itself could not: `overflow`\n applies to block containers, and a `display: table` element is not one. A table\n box cannot be a scroll container, so a four-column table in a Markdown document\n pushed the whole page sideways rather than offering a scrollbar.\n \n A `<table>` passed straight to a Prose now gets `display: block`, which is what\n makes it a block container, and the same `overflow-x: auto` the code fence has.\n The grid inside now sizes to its content up to the measure rather than stretching\n to fill it, which is the trade the technique always carries and is the one worth\n knowing about before you rely on it. Collapsed borders are unaffected: the row\n groups are wrapped in an anonymous table box that still inherits\n `border-collapse`.\n \n The treatment is a child selector rather than a descendant one, on purpose. A\n descendant selector would reach a `Table` you have already put in a scroll\n container of your own and set `display: block` on the table inside it, which\n moves the caption off being a caption and stops the grid filling its wrapper. A\n table you have wrapped in your own element is left alone as it was; give that\n element `overflow-x-auto` and it is what scrolls. For a table whose markup you\n control, `Table` remains the right answer.\n \n **`SearchDialog` and `CommandPalette` zoomed the page on focus.** Both drew their\n search field at 14 pixels. iOS Safari zooms the viewport on a focused input whose\n font size is under 16 pixels and does not zoom back out, so a reader who opened\n either surface on a phone was left on a magnified page with no way off it. Both\n now carry the `text-base` then `md:text-sm` pair that `Input`, `Textarea`,\n `Combobox`, `Form` and `NumberField` already carried, which is the same 16 pixels\n at a phone's width and the same 14 everywhere else.\n \n **The bump is `minor` rather than `patch`.** A table in a Prose renders\n differently: a narrow one no longer stretches to the measure. That is a visible\n change to a published Component rather than a fix nobody notices, and a site that\n was relying on a prose table filling its column is the one thing that needs to\n look at it.\n- 6dc7bb8: A tree is one Tab stop, and no node claims to collapse\n \n `Tree` documented a mechanism it did not have and shipped a state it could not\n honour. Its JSDoc said \"a tree is one Tab stop, and the arrow keys move inside it.\n That is the ARIA pattern\" and then described the roving tabindex that makes it true,\n and no `tabIndex` appeared anywhere in the file: a forty-node rail was forty Tab\n stops. A comment in the same file argued the other way, and it was wrong on its own\n terms, because it deferred the keyboard model to \"a caller's own keyboard model\" in a\n Component that does not forward `onKeyDown` and therefore has no caller's model to\n defer to.\n \n The roving tabindex is now implemented, in the shape `ToggleGroup` already uses: the\n stop is an address rather than an index, so a caller who reorders or removes nodes\n does not strand the reader; it lands on the current address when the tree has one and\n on the first destination otherwise; and it follows the reader once the arrows move\n it, so Tab away and back returns them to where they were. The fallback and the\n current address are not two answers: a `currentHref` naming another page is ordinary,\n and reading either rule alone would give the tree two tab stops.\n \n **Nothing collapses, so nothing claims to.** Every group with children carried\n `aria-expanded=\"true\"`, hard-coded, which promises a second press that folds nothing\n away. A node that cannot expand omits the attribute, and `aria-level` is what places\n a node in the tree for a reader who wants that instead.\n \n **Two regions announced their bare role name.** `SelectionToolbar` drew the words\n \"3 selected\" in its leading label and pointed at nothing, so a reader tabbing onto\n the row heard \"toolbar\" and no more; it is now named by `aria-labelledby` on the\n label, which works for the `ReactNode` label the count requires and cannot drift from\n the words on screen. `ToggleGroup` declared `aria-label` optional while its own JSDoc\n said the prop was required, so TypeScript enforced nothing and a group shipped\n unnamed in both of its roles. See the separate entry for the type change.\n \n To know before you style against it: `Tree` now writes a `tabindex` on every\n destination, so a page with two trees has two Tab stops where it had one per node.\n- 6dc7bb8: `Button` is `type=\"button\"`, the carousel leaves the caret keys, and a number field takes one\n \n **`Button` defaults `type` to `\"button\"`.** A `<button>` with no `type` is a submit\n button as far as the HTML is concerned, so the default was that every Prism `Button`\n a consumer placed in their own form submitted it: a Cancel, a Close, a second step of\n a wizard, anything that was not the one control that should. The failure is silent\n and it costs data, which is why the default is stated on the Component rather than\n left to the HTML.\n \n Nothing in this repository changed, and that is worth saying with the evidence rather\n than as an assurance: every `<form>` in `packages/ui` and `apps/site` already states\n its submit button's `type`, which is why no call site moved. The one behaviour change\n is for a consumer who relied on the old default, and the migration is one token:\n \n ```tsx\n // a control that used to submit by default\n <Button>Save</Button>\n \n // now\n <Button type=\"submit\">Save</Button>\n ```\n \n The trade is deliberate. A submit button is a decision someone makes about a form and\n this Component cannot see the form, so making the destructive default the safe one\n and the deliberate act the explicit one is the only arrangement where the mistake is\n the one you have to write on purpose. `ToggleGroupItem`, `MultiCombobox` and\n `SelectionToolbar` already wrote `type=\"button\"` by hand for the same reason.\n \n **`Carousel` answered the arrow keys for whatever was inside a slide.** The key\n handler sits on the region, so every key that bubbled out of a slide reached it, and a\n slide is the caller's slot: it can hold a field, a number field, a `select` or an\n editable region, and in all four Left, Right, Home and End belong to the value. The\n Component was calling `preventDefault()` on them unconditionally, so a reader typing\n a caption moved the carousel instead. It now answers a key only when nothing inside\n is using it, which is the discipline `ResizableHandle` applies to the same keys on\n the other axis. A reader arriving on the carousel or on one of its own controls is\n unaffected.\n \n **`NumberField` documented a route to the unit that its props did not carry.** The\n drawn unit is `aria-hidden`, on the reasoning that a screen reader already says the\n unit when the caller puts it in the field's description, and there was no\n `aria-describedby` on the props to put it in, so a value of 1,250 was announced as\n 1,250. The prop is there now and is forwarded to the input. The unit stays hidden: a\n reference already says it once, and two announcements of the same word is one too\n many. The unit belongs in the description and not in the accessible name, because\n `aria-label` is optional, so a field with a real `<label>` has no name to put it in,\n and a name reading \"Parcel weight, kg\" against a visible label reading \"Parcel\n weight\" is what WCAG 2.5.3 forbids and what voice control cannot activate.\n \n `aria-describedby` is an addition, so nothing breaks. `Button` and `Carousel` change\n behaviour, which is why they are here rather than described as a patch.\n- 6dc7bb8: Coarse-pointer controls take the 44px floor, and the focus gate reads the whole package\n \n Thirteen controls were drawing targets between 16 and 36 pixels with nothing done\n about it on touch input, so a consumer composing the bare primitive shipped a\n target that fails WCAG 2.2 SC 2.5.8 and falls short of this package's own 44px\n standard. All of them are fixed, and **the desktop metrics are untouched**: a mouse\n and a trackpad see exactly what they saw before, and every change is inside\n `@media (pointer: coarse)`.\n \n `Checkbox`, `RadioGroupItem`, `Calendar`'s day cells and paging controls,\n `Dialog`'s trigger and close control, and `SearchDialog`'s close control grow to\n 44 by 44. `Slider`'s and `RangeField`'s thumbs, `Switch`, and `DatePicker`'s clear\n control take a transparent 44 by 44 band around the control instead, because\n growing them would draw something the design does not have: a 44px ball on a six\n pixel rail, a switch that is not a switch. The band is a pseudo-element, so the\n box the value is computed from is the box you see.\n \n **`NumberField` turns its two steppers side by side on a coarse pointer.** This is\n the one place the floor changes an arrangement rather than a size. Two 44px targets\n stacked in the split column need an 88px field, and two 44px bands on two 18px rows\n overlap so much that the lower stepper takes the boundary between them, so a press\n aimed at increment would step down.\n \n **`Calendar` now matches `MiniCalendar` on the same controls.** The identical\n control in one package at two target sizes is the defect, not the 32px.\n \n Three things to know before you style against these:\n \n - A `Calendar` on a phone is six rows of 44px rather than six of 36px, so the panel\n is taller. It is the densest control in the package and the one a finger is least\n able to aim at.\n - `Checkbox` and `RadioGroupItem` draw a 44px bordered box on touch input, and the\n row, column or field around them grows to hold it. That is the price\n `tag-group.tsx` already records for its chips.\n - On a `Slider` and a `RangeField`, a press inside a thumb's band drags the thumb\n from where it was rather than jumping the value to where you pressed. The arrow\n keys and the range input behind the track still reach every value. Two bounds\n closer together than the band is wide are dragged by the one used last until Tab\n moves to the other, so give a range a `minGap` a finger can pinch apart.\n \n `DESIGN.md` states both patterns and the three conditions that decide between them.\n \n **The focus-indicator gate read 132 of the package's 400 source files.** It read\n `components/ui` and nothing else, so it saw no Block, no Page, neither `live`\n surface and no provider, and it skipped any file whose JSDoc made no keyboard claim\n before reading a single class string in it. It now reads `components`, `blocks`,\n `pages`, `live` and `provider`, recursively, and judges every class string in the\n scope. It does not read `apps/site`: that is another package with its own `check`\n chain.\n \n **Its exclusion for menu and listbox options was a suffix match, and is now a\n roster.** `/(?:-item|-sub-trigger)$/` is a statement about spelling rather than\n about menus, and over the Blocks it swallowed `radio-group-item`,\n `toggle-group-item`, `accordion-item`, `breadcrumb-item`, `tree-item` and about\n thirty more, every one of which is an ordinary focusable control.\n \n **Two scanning defects are fixed, and both were making the gate report green over\n a file it had not read.** A block comment inside a `cn()` call held an apostrophe,\n which the scanner read as an opening quote and which hid every class after it; and\n a module-level `const` was sliced to the end of its file rather than to the end of\n its value, so a menu item read as carrying a full-strength ring that belonged to a\n component further down the same module. Both are proved by fixtures that fail\n without the fix. The shipped tree has no genuine findings; a planted one fires in a\n Block and in a file whose JSDoc makes no claim, which is the negative control that\n makes the clean run mean something.\n \n **The bump is `minor` rather than `patch`.** No prop, import or rendered element\n changes, and no desktop metric changes, so nothing breaks a call site. It is\n `minor` because thirteen controls now measure differently on a coarse pointer, and a\n consumer who styles any of them has to know that: a `Calendar` panel is taller, a\n `Checkbox` box and a `RadioGroup` column are bigger, and a `NumberField` lays its\n steppers out differently. That is a visible metric change on the platform most of\n these controls are used on, and describing it as a patch would describe something\n nobody sees.\n- 6dc7bb8: `DocsShell` lays out three columns from `lg` rather than two\n \n The documentation frame's third track named a screen the emitted theme closes, so\n the class compiled to nothing and the contents rail fell into an implicit `auto`\n track: two columns at every width from 1024 pixels, with the article column about\n 340 pixels wide at 1024, on this site and in all four consumers. The rail is\n 15rem, the document takes the rest, and the contents rail is 13rem, all from `lg`,\n which is where both rails already appeared. No prop, import or rendered element\n changes, and the third track still exists only when you pass `toc`.\n \n **`check-breakpoint-variants.mjs` is new, and it is a gate rather than a fix.** It\n reads every responsive variant out of the class strings in the component package\n and the site and compares it to the screens the token package emits, read from\n `layout.tokens.json` and from the build that closes `xl` and `2xl`. A class\n written against a screen the theme does not have is now a finding with a file and\n a line, and every other gate was green through the one above because each of them\n held the value rather than the class.\n- 6dc7bb8: Nineteen Blocks name their table, and the name is the heading they already draw\n \n A `<table>` is named by a caption, an `aria-label` or an `aria-labelledby`, and by\n nothing else. A heading that happens to sit above it does not name it: no assistive\n technology derives a table's accessible name from a neighbouring heading. Nineteen\n Blocks drew a real `<table>` under a caller-owned `SectionHeading` and gave the table\n no name at all, so a reader listing the tables on a page found nineteen anonymous\n entries while every element around them was named. `Table`'s own JSDoc asks for one\n of the three.\n \n A Block cannot compose the words itself, so each one takes the name from the heading\n it already draws, by reference. `SectionHeading` gains an optional `id` for the\n handle; a generated id would be no handle at all, since nothing could be written\n down to point at it. No new prop on any Block, no string shipped, and no caption\n drawn: a visible line repeating the heading is noise, and a reference cannot drift\n from the heading it names.\n \n **Three of them write the reference only while the heading is drawn.** `Compare01`,\n `PricingCompare01`, `RateCard01` and `DataTable01` render their heading only when the\n caller passed a `title`, so an unconditional reference would point at an element that\n is not on the page, which is the defect this same release removed from\n `CommandPalette`'s listbox. In the one state where a caller passed no title the table\n is therefore unnamed, which is the honest trade and the caller's own choice: the\n alternative is a reference to nothing.\n \n `DataTable01` also has a `caption` prop, and where a caller passes one the caption\n wins, because a reference outranks a `<caption>` in the accessible name algorithm.\n Writing both would quietly make the heading the name and the caller's own words the\n thing only read on request. `Gantt01` already captioned its table and is unchanged.\n \n The rule is held by a test rather than a gate, and the reason is in that test's\n header: whether a table is named is a runtime property, because the reference has to\n resolve in every state a caller can reach. A source scan would only prove the\n attribute is written, and this repository does not accept a report-only gate.\n \n No import breaks and no prop is removed.\n- 6dc7bb8: `OverflowActions` and `ResizableHandle` read layout when it can have changed\n \n Neither Component's rendered output changes for a caller who composes them the way\n their own documentation says. What changes is how often each of them asks the browser\n to settle layout, and a table of rows or a page of dragged panes pays that on the main\n thread in the middle of a reader's work.\n \n `OverflowActions` measured itself after every render of every row, reading the row's\n own box, the cap and every drawn action, and writing the answer back as state. **A\n pass now runs on mount, when the actions' ids or their labels change, when `className`\n changes, when the row's own box changes, and when the page's fonts have finished\n loading.** Passing a fresh `actions` array on every render now costs nothing, which is\n the ordinary case in a table cell under a filter box. **The fonts are in the list\n because a self-hosted face swaps in after the first paint and changes the width of\n every drawn action without changing the row's width**, so nothing else reported it, and\n a row measured in the fallback face was a row whose widths were never true of the page\n the reader was looking at.\n \n `ResizableHandle` read the group's box on every `pointermove`, immediately after the\n frame before had written the new position and therefore written new styles: one forced\n synchronous layout per frame of every drag. **The box is read once at `pointerdown`,\n and every frame after that is arithmetic.** A drag measures from the press rather than\n from the divider's own box, which is what a divider a pixel wide and a reader takes\n hold of wherever their pointer lands requires, and it did that before.\n \n Two cases are pinned rather than handled, and both are written down in the JSDoc and\n the site page:\n \n - A group resized by something else part way through a drag finishes that drag against\n the travel it started with. A caller who needs a drag that follows a changing group\n ends that drag and starts it again.\n - `OverflowActions` cannot see a change that leaves both the row's box and its drawn\n content exactly as they were: an ancestor's font size changing, or an action's mark\n swapped for a different-width one while that action is drawn. Both used to be\n corrected by the row's next render and neither is now.\n \n The bump is `minor` because those two are behaviour a consumer can observe, even though\n no prop, import or rendered element changes.\n- 6dc7bb8: There is one container namespace, and `max-w-6xl` no longer resolves\n \n **If you wrote `max-w-6xl`, `max-w-2xl`, `max-w-xl`, `max-w-lg`, `max-w-md`,\n `max-w-sm` or `max-w-5xl` against this package, that class now produces no rule at\n all.** Nothing errors and nothing warns: the element simply loses its cap and lays\n out at whatever its parent gives it. Grep your own repository for `max-w-` before\n you upgrade. The replacements are in the list below and every one of them resolves.\n \n The reason is the defect this release ends. `@nanisoft/prism-tokens` authors three\n container widths and Tailwind ships thirteen of its own, and until now both shipped\n in the same stylesheet. Three of Tailwind's were numerically identical to three of\n ours: its largest at 72rem beside `--container-page`, its fifth at 42rem beside\n `--container-measure`, its fourth at 36rem beside `--container-measure-narrow`.\n Nothing rendered differently, so nothing failed and every gate was green. What it\n cost was that one width had two names, and the first retune of an authored token\n would have moved every surface reaching it by one spelling and left every surface\n reaching it by the other exactly where it was. `Section`, `SiteHeader`,\n `SiteFooter`, `ChartCard01`, `DocsShell` and `QuickView01` all used the Tailwind\n spelling of the page column; `Cta01` and `SectionHeading` used the Tailwind\n spellings of the two measures.\n \n The token build now closes the whole namespace, with\n `--container-*: initial` ahead of the authored entries rather than a list of the\n seven steps that happened to be emitted. A wildcard is the load-bearing word: the\n thirteen names it retires are `3xs 2xs xs sm md lg xl 2xl 3xl 4xl 5xl 6xl 7xl`,\n and a list would have been a second list to keep in step with a dependency's\n default theme, which is the event the line exists to survive. Order matters and is\n asserted: Tailwind resolves a theme in source order, and the same declaration\n written after the authored entries clears all eight of ours and ships no container\n at all.\n \n **What to write instead**\n \n | You wrote | Write | Value |\n | --- | --- | --- |\n | `max-w-6xl` | `max-w-page` | 72rem |\n | `max-w-2xl` | `max-w-measure` | 42rem |\n | `max-w-xl` | `max-w-measure-narrow` | 36rem |\n | `max-w-md` on a dialog or an alert | `max-w-overlay-dialog` | 28rem |\n | `max-w-lg` on a form dialog | `max-w-overlay-form` | 32rem |\n | `max-w-xl` on a command palette or a search | `max-w-overlay-palette` | 36rem |\n | `max-w-sm` on a side sheet or a drawer | `max-w-overlay-panel` | 24rem |\n | `max-w-5xl` on a lightbox | `max-w-overlay-media` | 64rem |\n \n **An overlay's width is a property of the kind of surface it is, not of the page\n grid underneath it**, and that is why the five overlay widths are authored rather\n than carried on a Tailwind name. A retune of the reading measure must not move a\n dialog and a retune of the page column must not move either, so the two families\n are named apart inside the one container group. They share a group because\n Tailwind 4's `max-w-*` resolves `--spacing-*` and `--container-*` and nothing\n else, so a maximum width that is to be a token has to live in one of those two\n namespaces or it is a value written in a Component. `overlay-palette` and\n `measure-narrow` are both 36rem; that is two measurements that happen to agree and\n not one measurement with two names, and the token source says so and the emitted\n contract holds it to saying so.\n \n **Nothing is supposed to change where you can see it.** Every migration above\n preserves the exact rendered width, so a page that only consumed this stylesheet\n looks the same before and after. What changes is the next retune: it now moves one\n surface rather than half of them.\n \n **A width that was arithmetic on the spacing base is left alone, deliberately.**\n `max-w-96`, `max-w-72`, `max-w-64`, `max-w-32` and `max-w-20` resolve to\n `calc(var(--spacing) * N)` and never touched the container namespace at all. They\n are how a chart's axis band, a token table's column and a documentation frame are\n sized, where nobody took a decision and naming one would invent it. They stay.\n \n **Two gates now hold this down, because the question has two sides.**\n `scripts/check-elevation-layout.mjs` reads every `w-*` and `max-w-*` name out of\n the class strings and holds it to the container group read out of the token source,\n so a width nobody authored is a finding with a file and a line; it reads\n `apps/site/items` as well as `apps/site/src` and `packages/ui/src`, because that is\n where the documentation Demos live and where the site's own build scans.\n `packages/ui/scripts/check-container-namespace.mjs` reads the built stylesheet and\n holds the artefact: that the theme block declares exactly the authored containers,\n that none of Tailwind's thirteen steps is declared or read, that the close is\n written ahead of the entries rather than after them, and that every container width\n this package writes is emitted as a utility reading its own variable. It reads the\n step list out of the installed `tailwindcss/theme.css` rather than restating it, so\n a dependency that adds a step is covered without an edit. Tailwind's extractor\n reads this package's comments as well as its code, so a retired class name written\n down in a JSDoc block used to emit a utility; there is now a rule about that too.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** A class a consumer wrote stops producing a rule with no\n error, which is breaking by any ordinary reading, and this entry says so plainly.\n `major` in this repository publishes `1.0.0`, and every release to date has been\n `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent slot.\n Publishing 1.0.0 with this change in it would assert an API stability guarantee\n this library has not earned, and it would be the first release in the project's\n history to use the bump at all. The break is real and is the subject of this\n entry; the number it lands on is the line's decision and the line is pre-1.0.\n \n **One collateral repair on the documentation site, recorded here because it is\n why `globals.css` moved.** The site's own build and this package's meet in one\n `utilities` layer, and this package emits a bare `.grid-cols-6` as the base of the\n `lg:` variant four of its Blocks use. That bare rule landed after the site's\n `sm:grid-cols-11`, so every colour ramp on the Foundations page was drawing six\n columns at every width above 640 against a comment beside the class saying\n eleven. Whether `check-utility-cascade.mjs` saw it depended on which order two\n content-hashed CSS chunks sorted in, which this change perturbed and which nothing\n about the tree controls. Neither side is wrong, so the site's variant is restated\n in the `site-variants` layer that already exists for six other collisions. The\n repaired behaviour is on the documentation site only; nothing in this package\n changes.\n- 6dc7bb8: Ship the interface face and its metric-adjusted fallback, and stop the site loading its own\n \n `@nanisoft/prism-ui` shipped three Inter weights and no fallback face, so a reader\n waiting on the web font saw the platform's UI face jump into place when Inter\n arrived. The stylesheet now also ships a local Arial carrying Inter's metrics\n through `size-adjust`, `ascent-override`, `descent-override` and\n `line-gap-override`, and `--font-sans` names it immediately after Inter, so the\n swap window occupies Inter's own line box. The four numbers are measured off the\n shipped face rather than chosen, and they are the ones `next/font` measured for the\n same typeface, so the line box is unchanged from what the documentation site had.\n \n A fourth face ships beside them: Inter italic at weight 400, the Latin subset of\n the same release, covering the same 230 codepoints as its upright siblings.\n `Prose` sets `blockquote` in italic and the surface had been asking for a style no\n shipped file provided, so every consumer has been reading a synthesized oblique.\n One real face covers every weight a browser asks for, because the font matcher\n takes the closest available weight rather than synthesizing once a face exists.\n The whole set is 93.5 KB, up 22.8 KB, and no consumer has to do anything.\n \n **The documentation site no longer loads a face of its own.** It shipped a 723 KiB\n unsubsetted variable Inter and its italic through `next/font/local`, preloaded both\n in all 581 exported documents, and applied the result to `<body>` as a directly set\n `--font-sans`. A directly applied custom property outranks an inherited one, so the\n library's faces were never fetched on the site that documents them: a missing or\n corrupt shipped font rendered perfectly there with every gate green. The site now\n resolves the same stack you do, which is the only way it can be evidence for the\n package. If you were relying on the site to look right while your own copy of Inter\n was broken, that is now a failure you can see.\n- 6dc7bb8: `Progress` announces a value text again, and `getAriaValueText` works\n \n Every `Progress` in every consumer was announcing a bare number. `aria-valuenow`\n carried the position and `aria-valuetext` was absent from the tree altogether, so\n the sentence a screen reader says for a bar was a number with nothing to say what\n range it was a number in. A bar with `value={null}` was worse than bare: with no\n `aria-valuenow` to fall back on, its entire value was nothing.\n \n **What changed.** `aria-valuetext` is now passed only when you pass `valueText`, so\n the default that Base UI computes survives. You get it for free:\n \n - a determinate bar announces its value as a percentage of the range, so\n `value={68}` is announced as `68%`;\n - a bar of unknown length announces `indeterminate progress`.\n \n **`getAriaValueText` was dead and is not any more.** It was called on every render\n and its return value was then discarded, so a consumer who wrote one was paying for\n it and hearing nothing. It receives the formatted percentage first and the raw\n `value` second, unchanged.\n \n **`format` and `locale` had no observable effect either, and now do.** Both exist\n only to shape the announced string, so with the attribute absent there was nothing\n for them to shape. One trap worth knowing: `format` is applied to the raw value on\n the scale, not to the share of it, so `format={{ style: 'percent' }}` on a\n zero-to-hundred range reads as `4050%`. The unformatted default is the only path\n that asks for a percentage of the fraction.\n \n **Nothing you passed changes meaning.** An explicit `valueText` still wins, an\n `aria-valuetext` written on the Component itself still wins over both, and a bar\n given both `valueText` and `getAriaValueText` still throws. What a consumer hears\n on a bar that passes neither of them is the whole of this change, and it is a\n change to every bar a consumer that passes neither already has.\n \n The bump is `minor` because it changes what a screen reader says in every\n composition of this Component. No prop, import or exported name changes.\n- 6dc7bb8: `Progress` expresses its value as a transform rather than as a width\n \n The fill was transitioned with `transition-[width]`, and `width` is a layout\n property, so every frame of every advancing bar made the browser settle layout on\n the main thread. `Progress` is one of the two or three most-composed Components in\n this package, which is what makes it worth a release of its own: the cost was not\n one bar on one page, it was every bar on every page, in every consumer that\n composes one.\n \n **The indicator is now as wide as the track at every value and is scaled along the\n reading axis.** `aria-valuenow` still comes from `ProgressRoot` and still reports\n `value`, `min` and `max`; the scale factor is the same three numbers read a second\n time, and the test suite holds the two to each other by asserting the transform\n against the announced value rather than against a copy of the arithmetic.\n \n Three things to know:\n \n - **A rule that sized `[data-slot=\"progress-indicator\"]` is now sizing a box this\n Component does not resize.** Style the track's `height`, the track's `width`, or\n the indicator's `transform` instead. The indicator's own inline `width` is `100%`\n and outranks any class, because Base UI writes a percentage width onto the\n element as an inline declaration and nothing in a class list can beat that.\n - **The fill grows from the inline start edge in both directions.** `origin-left`\n under a left-to-right `dir` and `rtl:origin-right` under a right-to-left one, so\n the bar reads from the same end it always did. It takes the `rtl:` variant rather\n than a logical property because CSS has no logical keyword for\n `transform-origin` and Tailwind's `origin` utility ships the nine physical\n positions and nothing else.\n - **At a low value the leading cap is flatter than it was.** A scaled shape's corner\n radius is scaled with it, so a bar at five percent is a short pill with a\n compressed cap rather than a five percent slice of a round one. It is the standing\n trade for a compositor animation, and it is the same one every other\n compositor-priced fill in this package already makes.\n \n `value={null}` is unchanged: an unknown length renders no transform at all, so it is\n the resting state it has always been rather than a zero to animate away from when\n the length becomes known.\n \n The bump is `minor` rather than `patch` because a consumer who styles the indicator\n has to move with it. No prop, import or announced value changes.\n- 6dc7bb8: Declare `react` and `react-dom` as peer dependencies\n \n `react` was already a peer dependency in fact: every module in the package imports\n it, and nothing declared it. What that cost was not a failed install but silence.\n A package with no peer range cannot be incompatible with a React version, so npm\n and pnpm had nothing to warn about, and the manifest never said which React this\n library is written for. Both are now `^19.2.0`, the range this repository builds\n and tests against, and the site already resolves. `react` and `react-dom` stay in\n `devDependencies` so the package still builds and tests locally.\n \n `react-dom` is a peer even though no module here imports it, because\n `@base-ui/react` does, for the floating elements the Dialog, the Popover, the Menu\n and the Tooltip render through. Two places already treated it as your copy rather\n than the package's, the client budget's shared runtime and the registry's implicit\n set; this is the manifest catching up with those two decisions.\n \n If your application is on React 18, install React 19. Nothing in the package uses a\n React 19 only API, but every gate, test and build in this repository runs on 19, so\n a narrower claim would not be one this repository can back.\n- 6dc7bb8: Reduced motion is now one unlayered rule in the stylesheet, and it stops everything\n \n If you have the `prefers-reduced-motion` setting on, this release changes what you\n see in a way you will notice the first time you open one of our pages.\n \n **What a reader with the setting on gets now.** No animation runs and no transition\n runs, anywhere in the component library. A dialog appears already open instead of\n fading up, a hover changes colour instantly instead of over 80ms, a disclosure\n opens at once instead of growing, a `Spinner` ring stands still instead of turning,\n and a `Timeline`'s running mark holds one opacity instead of breathing. Everything\n that is not movement is unchanged: nothing is hidden, no figure loses a state, and\n every announcement is said exactly as before.\n \n **What this replaces.** The stylesheet already carried a `prefers-reduced-motion`\n rule, and it did two things: it set `animation: none`, and it named the seven\n `prism-ambient-*` classes rather than every element. So every `transition-*` in the\n package ran at full duration, and two unbounded loops ran indefinitely: `Spinner`'s\n ring turned for as long as the work did, and a running `Timeline` step breathed for\n as long as the step did. The comment above that rule described the opposite, which\n is the defect class this repository keeps finding, so the rule and its comment are\n now the same document.\n \n **Why one rule rather than a guard at each call site.** A `motion-safe:` variant\n ADDS a rule inside `prefers-reduced-motion: no-preference`; it never removes one.\n Three Components were guarding a transition per call site and all three were\n different shapes: `Drawer` and `ImageZoom` guarded a duration as well as the\n property, `Lightbox` guarded the property and not the duration, and `RangeField`\n guarded nothing at all while animating `left`, `right` and `width`. The one place\n that decides is the stylesheet, and `scripts/check-motion.mjs` now fails on a\n `motion-safe:` or `motion-reduce:` variant on a motion utility, so a call site\n cannot quietly start answering for itself again.\n \n **What it means for a consumer.** Your own stylesheet is unaffected where it is\n qualified by a class, an id or an inline style: the rule is unlayered and universal\n rather than `!important`, so a class-qualified rule of yours outranks it and still\n wins for your own elements. What changes is the library's motion, which is the\n motion you inherited by installing it.\n \n **One Component had to be repaired before this was safe, and that is the load-bearing\n part.** A stopped transition starts nothing and so never sends `transitionend`, and\n an exit waiting on that event strands whatever it was hiding. `Toast` now asks the\n browser what is actually running on its root and hands over on the same commit when\n the answer is nothing, so a `Toast` still leaves under this rule and under your own\n `transition: none`. The overlays are Base UI's and Base UI settles every popup on\n `getAnimations()`, which resolves immediately when nothing is running.\n \n The bump is `minor` because every consumer of this package inherits the change and\n one stylesheet is the whole surface it lands on. No prop, import or exported name\n changes.\n- 6dc7bb8: `SearchDialog` is a real modal, and `Toast` dismisses when no transition runs\n \n Two accessibility defects, and a consumer sees both.\n \n **`SearchDialog` claimed modality it did not have.** It rendered `aria-modal=\"true\"`\n with no focus trap, so Tab walked out of the dialog and into the page behind it;\n it took no focus back, so a keyboard reader who opened search, pressed Escape and\n kept tabbing had lost their place entirely; its field suppressed the browser\n outline and drew no replacement, so the one control holding focus showed nothing;\n and it had no portal and no scroll lock, so the panel scrolled with the document\n and could be clipped by an ancestor.\n \n It is now composed from `Dialog`, so the focus trap, the page scroll lock, the\n portal, the dismissal on Escape and on the scrim, and the return of focus to the\n control that opened it all come from the one modal implementation this package has.\n There is one modal in the package rather than one and a claim. Its props, its\n ranking, its result list and its live region are unchanged.\n \n Two things about the composition are worth knowing:\n \n - `onClose` now fires once the dialog has finished leaving rather than at the\n moment it was asked to. The modal owns the reader's focus while it is closing,\n so handing over earlier would take the focus return with it. Unmount from\n `onClose` exactly as before.\n - The panel and the scrim are now `Dialog`'s rather than the search dialog's own.\n The scrim is a blurred `bg-background/80` wash instead of a `bg-foreground/40`\n one, and the panel is centred in the viewport rather than pinned 15 percent from\n the top. A site that styled either through its own sheet will want to know.\n \n **`Toast` dismissal depended on a transition ending.** The leave handed over on the\n end of its own opacity transition, so a global reduced-motion kill written as\n `transition: none` rather than as a `0.01ms` duration meant the event never fired,\n `onDismiss` was never called, and the toast was stranded in `data-phase=\"leaving\"`\n for good: visible, undismissable, still taking the pointer, and a live region that\n never stopped announcing. This is the one place in the system where a global\n motion kill destroyed state rather than merely removing movement, and\n `packages/ui/src/styles.css` states the invariant it broke.\n \n The leave now asks the browser what is actually running on the toast rather than\n waiting for an event a killed transition never sends. It waits for the fade when\n there is a fade, it hands over on the same commit when there is not, and\n `onDismiss` is called exactly once either way. Nothing else about the contract\n moved, and the length of the leave is still `duration-slow` from the stylesheet\n rather than a number in this package.\n \n **`Dialog` takes an optional `initialFocus`.** Left out, the panel focuses its\n first tabbable element, which is unchanged. It is named for the dialog whose first\n tabbable element is not the control the reader came for, which is what\n `SearchDialog` now uses to put the cursor in the field.\n \n **The bump is `minor` rather than `patch`, for two reasons.** `Dialog` gains a\n public prop, which is new surface rather than a correction. And `SearchDialog`'s\n panel and scrim visibly change to `Dialog`'s, which is a behaviour a consumer can\n see on their site rather than a fix they never notice. Both are additive and\n neither breaks a call site.\n- 6dc7bb8: `SiteNavbar` is opaque, and `sticky` can be turned off\n \n The bar was `bg-background/80` with a `backdrop-blur`. A backdrop filter is\n evaluated against everything painted behind the element, and this element is a\n full-viewport-width sticky bar, so on a scrolling page the browser re-sampled and\n re-blurred the backdrop on every frame of the scroll, on every page of every site\n that composes it. The bar is now `bg-background` with a `border-border` hairline,\n which is what `SiteHeader` has always shipped.\n \n **The blur was not kept behind an `@supports` check, because a capability check does\n not pay for it.** `@supports (backdrop-filter: blur(1px))` is false only in a\n browser that was not compositing anything, and true in every browser that does the\n expensive thing, so it changes no reader's cost. It would also have left a third\n outcome in the world and the worst of the three: a bar that is a flat eighty\n percent veil with unblurred text passing under it.\n \n **Opaque is also the more legible of the candidates, and that is the part a token\n can be held to.** An opaque bar makes the bar's own contrast the pair\n `muted-foreground` on `background`, in every pack and in both modes, which is the\n pair the contrast gate already checks. An eighty percent background has no token\n pair at all, because the colour under it is whatever the reader's scroll position\n has brought there and no token in this system describes it.\n \n `will-change` is not on the bar and is not proposed for it. Nothing on the bar\n animates, so a compositing hint applied at rest has no frame to be ready for and\n nothing that would release it. This package ships no `will-change` anywhere.\n \n **`sticky={false}` now works.** The prop was destructured and never reached the\n class list, so a site that passed it was given the sticky bar it had asked not to\n have. The default is unchanged and for the reason it was there: the bar is how a\n reader leaves the page they are on, and it takes that with them on exactly the long\n pages where it is needed.\n \n Two things to know if you were styling against the old bar:\n \n - A rule that set the bar's background translucency has nothing to sit on now. Set\n the colour on the page behind the bar, or leave the bar the way `SiteHeader` has\n it.\n - A rule that relied on the bar being translucent to hide a heading as it scrolled\n under will no longer hide it, which is the point.\n \n The bump is `minor` because the bar's default appearance changes on every page of\n every consumer. No prop, import or landmark changes.\n- 6dc7bb8: The elevation scale is three steps or it is nothing, and the gate now says so\n \n `SearchDialog` shipped `shadow-lg`, and `DESIGN.md` said no shipped component\n uses one. Both were true once and neither was true when the sentence was last\n read: the Component took `shadow-lg` as an override on top of the `shadow-md` its\n own `DialogContent` already draws, and the site's skip link took `focus:shadow-lg`\n under a heading that named \"the theme disclosure and mobile nav panels\" as the\n only places it was allowed.\n \n **`shadow-lg` is not an authored step, so neither of those was drawing an authored\n shadow.** The token package emits `--shadow-xs`, `--shadow-sm` and `--shadow-md`\n and nothing else, so `shadow-lg` resolved against Tailwind's own stock theme. That\n is the second source of truth this package exists to prevent, and the reason it\n was invisible is that Tailwind's `shadow-lg` is a perfectly good black-alpha\n shadow: nothing looked wrong. What was wrong is that a retune of the elevation\n scale would have moved every lifted surface in every consumer and left that panel\n exactly where it was.\n \n **`SearchDialog` now says nothing about elevation at all.** The panel is the one\n lifted element over a modal scrim, which is what `--shadow-md` is for, and\n `DialogContent` already draws it, so the override is deleted rather than\n replaced. A consumer who styles `DialogContent` keeps styling it, which was not\n true of a hardcoded `shadow-lg` sitting on top of it.\n \n **`scripts/check-elevation-layout.mjs` now fails on any `shadow-*` step the token\n source did not author**, in every root it reads, the site's own source included.\n It reads the authored set from `shadow.tokens.json` rather than from a list beside\n the gate, for the reason the width rule beside it reads the container names from\n `layout.tokens.json`: a gate whose subject is the authored scale cannot be\n answered by a second copy of it. `shadow-none` is allowed, because the absence of\n a shadow is not one more step of the scale, and `shadow-inner` is not, because it\n is a shadow nobody authored.\n \n **There is no docs-only exception any more, and that is the part worth arguing\n with.** The exemption said site apparatus is not installable surface. The site's\n own Tailwind build is a second consumer of the same token package, so \"the site\"\n is not outside the system, it is inside it twice: a site class resolving a shadow\n out of Tailwind is the second source of truth wearing the word apparatus. The\n site's skip link draws `shadow-md` now, which is what it should have drawn.\n \n The bump is `minor` rather than `patch` because a consumer who styles the search\n panel's elevation sees it move from Tailwind's `lg` to Prism's `md`, which is a\n visible change to a published surface and the right one.\n- 6dc7bb8: `RangeField` expresses its band as a transform rather than as three layout properties\n \n The band between two bounds was transitioned with `transition-[left,right,width]`.\n Base UI positions that element with a logical `inset-inline-start` and a `width`,\n so `left` and `right` were properties the browser checked on every frame in order\n to discover they had not moved, and `width` was a layout-and-paint animation on\n every frame of every drag. A range is the one control in this package whose value\n changes on every pointer move rather than on every commit, which is why the cost\n was larger here than on the Component that retired the same mechanism a release\n ago.\n \n **The band is now as wide as the track and is scaled about the inline start.**\n `aria-valuenow` still comes from Base UI and still reports each bound; the scale\n factor is the same division read a second time, and the test suite holds the two to\n each other by asserting the transform against what the two thumbs announce.\n \n Three things to know:\n \n - **A rule that sized or positioned `[data-slot=\"range-field-indicator\"]` is now\n sizing or positioning a box this Component does not resize.** Style its\n `transform`, or style the track. Its own inline `width` is `100%` and outranks any\n class, because Base UI writes a percentage width onto the element as an inline\n declaration. Its POSITION is still Base UI's, untouched: the element keeps the\n logical `inset-inline-start` that places the band's left edge on the lower bound\n under a left-to-right `dir` and on the same distance from the other end under a\n right-to-left one, so a `dir=\"rtl\"` band needs no arithmetic of its own.\n - **A partial band has a straight edge where it stops, and no rounded edge.** The\n previous fill carried `rounded-full`, and a `scaleX` scales the shape it is applied\n to, including its own corners, so a band at 40 percent drew a 3px cap squashed to\n 1.2px on one axis and left at 3px on the other. The ends of a band are the two\n round thumbs drawn on top of it, so the radius was a second, smaller circle under a\n larger one and squashing it landed on exactly the edge the reader is looking at.\n The track still carries `rounded-full` with `overflow-hidden`, so a band at full\n span has both of its ends rounded. This is the one place the shape differs from\n `Progress`, and the reason is on the Component.\n - **A vertical field scales on `scaleY` about the bottom edge**, chosen by the\n `orientation` the Component already forwards, rather than having the horizontal\n animation applied to a vertical box.\n \n `RangeField` also carried a `motion-safe:` variant on that transition, which was\n doing nothing: the variant adds a rule rather than removing one, so the unguarded\n transition ran at every setting. See the reduced-motion changeset for where the\n policy now lives.\n \n The bump is `minor` because a consumer who styles the indicator has to move with it.\n No prop, import or announced value changes.\n- 6dc7bb8: The rest of the controls take the coarse-pointer floor, and the divider explains itself\n \n Fifteen controls were drawing targets between 28 and 40 pixels with nothing done about\n it on touch input, and three more were found by going looking rather than by reading a\n list. All of them now take the 44px floor, and **the desktop metrics are untouched**: a\n mouse and a trackpad see exactly what they saw before, and every change is inside\n `@media (pointer: coarse)`.\n \n **Sixteen take a step, and the step is the default for all of them.** A popup trigger\n (`PopoverTrigger`, `DropdownMenuTrigger`, `NavigationMenuTrigger`, `MenubarTrigger`,\n `AlertDialogTrigger`), an alert dialog's confirm and dismiss pair, a page link\n (`PaginationLink` and the previous and next controls), a tab and its list, a select\n trigger and a native select, a `Toggle`, a `ToggleGroupItem` and the sidebar's toggle\n all grow to 44 tall through `pointer-coarse:h-11`. `Carousel`'s two controls, both of\n `Lightbox`'s, both of `ImageZoom`'s, the drawer's close control and `PaginationLink`'s\n `icon` size take `pointer-coarse:size-11` and are 44 by 44.\n \n **Four of them take `min-w-11` on the second axis as well, because a floor paid on one\n axis is not a floor.** A one-digit page link at 44 tall is 20 wide. An icon-only\n `Toggle` is `size-4` plus `px-2.5`. An icon-only `ToggleGroupItem` is the same. This is\n the arrangement `Button` already states and the reason it states it, and the page-link\n case is the one worth naming: `PaginationPrevious` and `PaginationNext` pass\n `size=\"default\"` and override the padding to `px-2.5`, so on a phone the word is hidden\n and the control is an icon with a 40 pixel width until `min-w-11` says otherwise.\n \n **`TabsList` grows too, and that is the half that is easy to forget.** The list is\n `h-9 p-1`, so its content box is 28 tall and a trigger grown to 44 would not fit inside\n it. It takes `pointer-coarse:h-13`, which is the trigger's 44 plus the list's own `p-1`\n on each side. Both halves are asserted in the test, because either alone leaves a\n control under the floor or a list that clips one.\n \n **Two of the fifteen are not targets and take nothing, and both are worth stating\n because each looks exactly like every other finding here.**\n \n `steps.tsx`'s marker is a `<span>` with no role, no `tabIndex` and no handler. It is not\n focusable, it is announced by nothing, and a press on it falls through to the step's own\n text. The rail is a reading structure: the `<ol>` carries `aria-label`, the `<li>`\n carries `aria-current=\"step\"`, and the connecting rule is `aria-hidden`. Growing a 32px\n disc to 44 would pay the floor on a decoration and push the step's text down the page.\n \n `pagination.tsx`'s ellipsis is a `<span aria-hidden=\"true\">`, for the same reason. The\n page links around it are the targets and they take the floor in `PaginationLink`. The\n floor is a claim about targets a finger is asked to hit, and neither of these is one.\n \n **`ResizableHandle` takes a band, and it is the only control here where a step is\n arithmetically impossible rather than merely wrong.** The panes are given\n `flexBasis: <size>%` with `flexShrink: 0`, so the two of them already sum to the group's\n whole width and the divider is what overflows it, by exactly its own one pixel. A `w-11`\n divider would overflow the group by 44, which on a phone pushes the right pane's edge off\n the screen, and making room would mean changing what `size` means.\n \n The band works because three facts hold that the three conditions in `DESIGN.md` ask\n about. It is a pseudo-element, so the box the drag measures stays the drawn box:\n `onPointerDown` reads the **group's** rect and `onPointerMove` divides by the group's\n travel, so nothing reads this element's own size at all, which is the failure `DESIGN.md`\n warns about for a `Slider` thumb and which does not apply here. It paints above both\n panes without a `z-index`, because the handle is `relative` with `z-index: auto` and the\n panes are static, and a positioned descendant paints after non-positioned siblings. And\n it is 44 by 44 rather than 44 by the handle's length, which is what keeps the overlap\n local: along the split the handle already spans the group so 44 there is free, and across\n the split it reaches 21 pixels into each pane over a 44 pixel stretch of the line rather\n than the whole height of the group.\n \n **The cost is stated at the class rather than hidden in a document.** A press within 21\n pixels of the line, over a 44 pixel stretch of it, grabs the divider rather than the\n pane, and `touch-none` travels with the band because the pseudo-element resolves its\n `touch-action` from this element. Text selection near the divider is unavailable inside\n that patch. That is the trade a resize gutter makes on every platform, and it is bounded\n to a patch rather than run the length of both panes.\n \n Three things to know before you style against these:\n \n - A tab list, a menubar, a navigation bar and a segmented toggle group are 44 tall on\n touch rather than 28 to 36. A `Tabs` panel is that much further down the page.\n - A page header's row of links, an alert dialog's footer and a carousel's control row are\n each 8 to 16 pixels taller on touch. Nothing shifts off an edge: every one of them is\n content-sized and centred rather than flush.\n - On a resizable split, a press inside 21 pixels of the divider grabs the divider. The\n arrow keys, Home and End still move it, and `aria-valuenow` still reports where it is.\n \n **Three controls the earlier list did not name, found by searching for the two patterns\n rather than reading a line number.** `Toggle`, `SelectTrigger` and `NativeSelect` were\n all drawing 32 or 36 pixel targets and all three are the same family as controls the\n earlier release had already fixed. They are included because a trigger that opens a\n popup is a button, not a text field: it has no caret, takes no typed input, and its job\n is to be pressed.\n \n **One family was found and deliberately left, and the reason is worth having in writing.**\n `Input`, `Textarea`, `Form`'s `FieldControl`, `Combobox`'s input and the draft field in\n `CreatableCombobox` all draw 36 tall on a coarse pointer. Growing them is a different\n decision from every one above rather than an omission: a text field is the one control\n whose height also sets the line box the caret sits in, it is composed into dozens of\n Blocks, and this package already carries a deliberate narrow-viewport decision on exactly\n these elements (the `text-base md:text-sm` pair that stops iOS zooming the page on focus).\n The line this release draws is **triggers and buttons take the floor; text entry does\n not, in this sweep**, and the text-entry family is a separate piece of work rather than a\n gap in this one.\n \n **The bump is `minor` rather than `patch`.** No prop, import or rendered element changes,\n and no desktop metric changes, so nothing breaks a call site. It is `minor` because\n eighteen controls now measure differently on a coarse pointer, and a consumer who styles\n any of them has to know that: a tab list is taller, a navigation bar is taller, a\n segmented group is taller, a resizable split claims 21 pixels either side of its line.\n That is a visible metric change on the platform most of these controls are used on, and\n describing it as a patch would describe something nobody sees. This is the same judgement\n the earlier release made for the thirteen controls it fixed.\n \n `DESIGN.md` states both patterns and the three conditions that decide between them, and\n `test/coarse-pointer-floor.test.tsx` asserts the class each floor is written as. **What\n that test does not do is prove a size of 44 pixels, and the file says so in its first\n paragraph:** jsdom has no CSS engine, no cascade and no box, so `@media (pointer: coarse)`\n is never evaluated here and a coarse-pointer variant is a string that a test can only\n assert is present on the element the Component rendered. The assertions query by role and\n name rather than searching the source, so they catch a floor that was moved onto a\n wrapper or dropped in a refactor; they do not and cannot catch a browser that lays the\n element out at 30 pixels. `apps/site/e2e/display.spec.ts` over a real browser is where\n that is checked.\n- 6dc7bb8: `SiteFooter`, `DataTable01` and `RunConsole01` take a heading level\n \n Three Blocks drew a hard-coded `<h2>`. A fixed level is right for a Block at the top\n of a page and wrong everywhere else, and all three are Blocks a product opens inside\n something: a footer as the last region of a settings page, a table in a drawer, a run\n console in a panel. In each case the heading became a sibling of the heading the\n surrounding section already had, so \"skip to the next heading at or below level two\"\n walks straight past the thing the reader came to read.\n \n All three take `headingLevel?: HeadingLevel` and default to `'h2'`, which is the\n arrangement every other Block in this package already uses and which\n `childLevel()` exists to compose. Nothing rendered changes for a caller that passes\n nothing. `DataTable01`'s JSDoc had claimed the fix was needed, so this is the code\n catching up to its own documentation.\n \n **The audit behind this said `SiteFooter` was the only Block in the tree with no\n `headingLevel` prop. It was three.** The count matters because it changes the shape of\n the fix: a single instance reads as a judgement call about that Block and a class of\n three reads as a rule the package had not finished applying, which is what this\n release says.\n- 6dc7bb8: `ToggleGroup` requires the accessible name its role already needed\n \n `aria-label` was declared optional on `ToggleGroupProps` while the JSDoc directly\n above it said \"Required rather than defaulted\". TypeScript enforced nothing, so a\n group shipped unnamed, and both roles this Component draws are ones ARIA puts a MUST\n on: a reader tabbing onto an unnamed `toolbar` is told \"toolbar\" and cannot ask which\n set of controls they have reached, and an unnamed `radiogroup` says nothing about\n which question its radios are answering. `TextFormatToolbar` draws the same role with\n a required `label`, which is the shape this now matches.\n \n **This is a compile-time break, and the fix is one prop.** A `ToggleGroup` that\n passed no name now fails to build:\n \n ```tsx\n // before\n <ToggleGroup value={range} onValueChange={setRange}>\n \n // after\n <ToggleGroup aria-label=\"Date range\" value={range} onValueChange={setRange}>\n ```\n \n `aria-labelledby` still arrives through the forwarded props, so a caller whose name\n is already drawn somewhere can point at it rather than repeat it.\n \n **Why `minor` and not `major`.** The bump line for this repository is `0.y.z` and the\n package is at 0.7, so `major` would be 1.0.0 and would claim a stability promise for\n a design system whose catalogue gained 130 Items in the release before this one. The\n published precedent is in `packages/ui/CHANGELOG.md`: the `*Variant` to `*Form`\n renames and the prop removals in 0.7.0 shipped on the `minor` line, each with its\n reason in the entry. This is the same shape of change, and it is described here in\n full rather than left to be discovered by a compiler.\n\n### Patch Changes\n\n- Updated dependencies [6dc7bb8]\n- Updated dependencies [6dc7bb8]\n - @nanisoft/prism-tokens@0.15.0\n\n## 0.14.0\n\n### Minor Changes\n\n- 4e40c8f: Nine Components, and the naming law that the next thousand will follow\n \n Closes the gap in Component coverage: a `Pill` beside `tag-group`, a `Drawer`\n that is a Sheet with a gesture rather than an overlay, an `ImageZoom` that is a\n `Lightbox` with the dialog taken out, a `VideoPlayer` that owns no engine, an\n `EmojiPicker` that ships no emoji table, a `RepoStars` whose mark is a slot\n because a host logo is a licensed asset, a `ChoiceCard` built on real radios\n rather than ARIA, a `BillingSource` that has no `mask` prop on purpose, and a\n `PackSwitcher` that makes Prism's pack axis switchable at runtime, which nothing\n did before.\n \n Four of the nine ship no JavaScript at all. That is the number worth reading\n against the roster: nine catalogue Items cost 19.3 KB of the deduplicated client\n bundle rather than the sum of their rows, and `VideoPlayer` in particular is a\n server Component that a page can carry a dozen of for free.\n \n `DESIGN.md` gains the naming law this roster needed earlier: a slug says what a\n Component does, and the only number it may carry is a variant ordinal on a name\n that is already true. `button-47` tells a reader nothing; `split-button` tells\n them what it is and what happens if it changes. Five upstream-shaped items are\n refused by that law and each refusal is recorded with its reason rather than left\n to be rediscovered.\n \n The client ceiling moves from 260 KB to 280 KB, measured at 269.7 KB over 177\n entry points, and the gate's header says plainly that this is the third kind of\n number it has printed: not a correction of a bad measurement, and not a forecast\n of weight that had not landed, but a report on weight that is here. It also names\n the pattern, because five moves in a row triggered by the same event is the\n finding: a ceiling that moves once per batch of work is recording a history rather\n than measuring a policy.\n \n `Progress`, `dayKey` and the `live` subpath fix from the previous release are\n unchanged. No consumer import breaks; every name here is an addition, and the\n four `*Variant` to `*Form` renames from that release are still additive because\n nothing imported the old names.\n- 829ae83: Take the catalogue from 106 Items to 236\n \n Adds 130 Items in Prism's own vocabulary: 23 foundation Components, 96 Blocks, 10\n Pages and one live surface. Every name is an addition, so no consumer import\n breaks, and every Item follows the rules the package already held rather than\n relaxing them: no shipped copy, no fetching, no raw colour, and motion by token.\n \n The shapes that were not upstream's are the ones worth naming, because each is a\n translation rather than a copy. Upstream's storefront comparison is\n `offering-categories-01` and its spec sheet is `spec-table-01`, because what a\n buyer compares in a pipeline product is a connector's declared bounds rather than\n a plan's feature list. `tool-ledger-01` is the second live surface and takes a\n `subscribe` function the consumer supplies, so Prism still owns no socket.\n `directory-01`, `project-dashboard-01`, `ops-checklist-01` and `handoff-01` were\n added because a Block already in the roster reached for something absent.\n \n Two names move because nothing had imported them. `CalendarBlock01` is now\n `Calendar01`, and the deferred `input-otp` resolved to the published\n `one-time-code`. Four `*Variant` types are now `*Form`, since the surface gate\n rejects an exported union under a name that reads as a styling axis.\n \n `Progress` gains a string `valueText` prop for the server Block that cannot carry\n a callback across the boundary, `dayKey` is exported from `lib/utils` for the\n Blocks that share a date key, and the emitted `live` subpath now resolves its\n relative specifiers, which it did not.\n \n The client bundle is 250.4 KB against a ceiling moved from 208 KB to 260 KB. The\n move is a forecast about weight that had not landed, and the gate's own comment\n records why it differs from the two earlier corrections. `docs/history/roster-expansion.md`\n holds the decisions, and `DESIGN.md` records that the v1.1 deferred tail has\n shipped.\n- 4e40c8f: Thirty-four Components, and the audit that refused the other thousand\n \n Component coverage of the upstream catalogue is complete. 1,026 variants were\n audited against the naming law and 34 shipped: 392 were a cross-product of tone,\n size, shape, icon or state that a shipped Component already expresses as props, and\n the rest were motion refused by a law that predates this work.\n \n The zeroes are the result. `form` (85) and `select` (51) produced nothing, because\n a contact form and a settings form differ only in the data a consumer passes.\n `table` (38), `avatar` (34), `skeleton` (30), `dropdown-menu` (30) and `sheet`\n (30) produced nothing for the same reason, and `skeleton` at 30 is six groups of\n five, each one a different arrangement of the same placeholder rectangles.\n \n New: `reorderable-list`, `checklist`, `form-dialog`, `form-wizard`, `stateful-table`,\n `nested-tabs`, `mega-menu`, `task-progress`, `split-button`, `stepper`,\n `overflow-actions`, `selection-toolbar`, `money-field`, `intensity-grid`,\n `proportion-list`, `cohort-grid`, `multi-combobox`, `creatable-combobox`,\n `range-field`, `platform-modifier-key`, `image-list-field`, `repeatable-rows`,\n `text-format-toolbar`, `prompt-composer`, `lifecycle-button`, and the nine from the\n category sweep: `pack-switcher`, `pill`, `billing-source`, `drawer`, `image-zoom`,\n `video-player`, `emoji-picker`, `repo-stars`, `choice-card`.\n \n Five of the new ones exist because two Components disagree and the disagreement\n is the defect: `split-button` joins a trigger and a menu trigger into one shape,\n `stepper` clamps two controls at one bound, `overflow-actions` moves actions into\n a menu when the row runs out, `selection-toolbar` and `text-format-toolbar` take\n opposite positions on whether focus may move, and `nested-tabs` orders two tab\n axes that look identical. A boolean cannot express a conflict.\n \n **`form-dialog` is the largest single job found, at 22 upstream variants** across\n `dialog`, `alert-dialog` and `popover`. All three ship a container and none owns\n what happens between the reader pressing submit and being able to act on the\n answer.\n \n **`pack-switcher` makes the pack axis switchable at runtime**, which is the piece\n a DTCG system was missing. `video-player` owns no player engine and ships no\n JavaScript. `money-field` holds a number for the caller and a locale-formatted\n string for the reader at the same time, and the round trip is the Component.\n \n Four defects the audit found in work already merged are fixed in this release, and\n each was found by a job that did not exist to find it: `button` never had a\n loading state and two documents claimed it did; `cohort-grid` documented a scale\n knob that cannot be moved because a retention row's first column is a hundred by\n definition; `check-item-docs` could not see a generic Component, so a module with\n a full JSDoc block read as declaring no Item rather than as undocumented; and a\n byte order mark on an MDX makes its frontmatter parse as empty with no gate\n noticing, which is what `scripts/check-encoding.mjs` now exists to prevent.\n \n The catalogue is 270 Items and the client bundle is 286.2 KB, inside the 300 KB\n ceiling without moving it, because nine of the thirteen newest compose Components\n the bundle already carries. 34 Components across two passes cost 47.6 KB of\n aggregate where the sum of their individual measurements is several times that.\n \n No consumer import breaks. Every name added is an addition.\n- 4e40c8f: The component sweep: 21 Components from 555 audited upstream variants\n \n The naming law in DESIGN.md says a slug describes the job and a variant that\n differs from its siblings only by a prop is a call site. Applied strictly to 555\n upstream variants, 21 survived: 340 were a cross-product of tone, size, icon and\n state that existing Components already express as props, and 122 were motion\n refused by a law that predates this work rather than by a new one. The two zeroes\n are the finding: `form` at 85 and `select` at 51 produced nothing, because a\n contact form and a settings form differ only in the data a consumer passes.\n \n New: `pack-switcher`, which makes the pack axis switchable at runtime and is the\n piece a DTCG system was missing; `pill`, `drawer`, `image-zoom`, `video-player`,\n `emoji-picker`, `repo-stars`, `choice-card`, `billing-source`; `intensity-grid`,\n `proportion-list`, `cohort-grid`; `multi-combobox`, `creatable-combobox`,\n `range-field`, `platform-modifier-key`; `image-list-field`, `repeatable-rows`,\n `text-format-toolbar`, `prompt-composer`, `lifecycle-button`.\n \n Four ship no JavaScript. The catalogue is 257 Items and the client bundle is 278.5\n KB.\n \n **Three defects the audit found in work already merged.** `button` never had a\n loading state and the catalogue and its MDX both said it did; the claim is\n corrected and `lifecycle-button` is now where an outcome after a press lives.\n `cohort-grid` documented `max` as the way to narrow its scale, which is impossible\n because a retention row's first column is a hundred by definition, so the knob is\n `min` and the limit is now stated. And `check-item-docs` could not see a generic\n Component, so a module with a full JSDoc block was reported as declaring no Item\n rather than as undocumented; the pattern is widened, which widens the law's reach\n rather than relaxing it.\n \n **`scripts/check-encoding.mjs` is new.** A byte order mark on an MDX makes its\n frontmatter parse as empty, and no existing gate noticed: the file had a JSDoc\n module, a Demo beside it, all-props strings, and a clean catalogue join. It\n surfaced as seventeen unrelated failures in two files. The class has now bitten\n this repository twice. The gate reports and does not repair, and it is proven to\n fire.\n \n **The client ceiling moves to 300 KB.** It has now moved six times and every move\n was triggered by a batch of new Items, which the gate's header records as the\n finding rather than hiding. A roster-derived ceiling was rejected because a\n ceiling that scales with the catalogue is the same as no ceiling. Deciding one\n fixed number on what a consumer can actually load is a product judgement about\n the four downstream repositories and is the one open decision here.\n \n No consumer import breaks. Every name added is an addition.\n\n## 0.13.0\n\n### Minor Changes\n\n- 66787b0: Add `SiteNavbar`, the site bar with the controls a reader needs on every page\n \n `SiteHeader` is a lockup, a product switcher and a slot. It is not a search box, a\n menu of the family's sites, a colour chooser, a light and dark control, or a\n navigation that survives the width at which a horizontal row stops fitting, and all\n five NaniSoft sites had written the missing parts themselves: Prism's own site had\n search and a colour chooser and no way to reach its four siblings, and each of the\n four had a row of five product marks where a menu belongs and no search, no mode\n control and no navigation at all below its own row's threshold.\n \n `SiteNavbar` is that bar. `search`, `sites`, `theme`, `mode` and `nav` are each\n optional, so a site that owns none of the four controls renders the whole bar as a\n server Component and ships no client JavaScript for it. Every string is a prop: the\n Block ships no site name, no default navigation, no \"Search\" and no list of packs or\n sites.\n \n ```tsx\n import { SiteNavbar } from '@nanisoft/prism-ui/blocks/site-navbar'\n \n <SiteNavbar\n product={{ id: 'nexus', name: 'Nexus', pack: 'lavender' }}\n defaultPack=\"lavender\"\n defaultMode=\"dark\"\n navLabel=\"Site\"\n mobileLabels={{ open: 'Open menu', close: 'Close menu' }}\n nav={NAV}\n currentSiteId=\"nexus\"\n sitesLabel=\"The family\"\n sites={SITES}\n search={{ indexUrl: '/api/search', label: 'Search documentation', messages: COPY }}\n mode={{ lightLabel: 'Switch to dark mode', darkLabel: 'Switch to light mode' }}\n />\n ```\n \n `currentPath` is the alternative to marking `current` on each link, for a site that\n composes the bar once in a root layout and is therefore handed no pathname.\n \n Also new: `SearchDialog`, the search the bar opens. It fetches a static JSON index\n on the first activation and ranks it in the browser, and the ranking is a scorer\n rather than an inverted index: every query token has to match somewhere, so a\n two-word query narrows instead of listing most of a documentation set. There is no\n stemming and no typo tolerance, which is a deliberate trade against taking a search\n dependency into a package that otherwise depends on a design system and a token\n pipeline and nothing else.\n \n `DropdownMenuItem` now takes `render`, so a menu item can be the link it navigates to\n rather than a focusable element wrapped around one. Two tab stops for one row, and a\n reader who middle-clicks a row that only looks like a link, are what that fixes.\n\n## 0.12.0\n\n### Minor Changes\n\n- 26a6fdc: Export the `Stat` and `Stats01Props` types from `stats-01`\n \n A caller that builds the `stats` array could not name the type it was building,\n so it had to infer it or annotate the array as `any`, and a rename of a field on\n `Stat` then stopped being a compile error at every call site. Both types are\n now exported from the subpath, alongside the Block.\n\n### Patch Changes\n\n- 9f3dd2a: Align `FeatureGrid01` and `Pricing01` section headings left\n \n Both Blocks rendered a centred heading over a grid of cards, and `SectionHeading`\n states that `left` is right for a section with content under it. Every other Block\n that opens with a heading already aligned left, so a page composed from Blocks put\n these two headings out of step with the rest of the page. No prop changed, so no\n call site does; a consumer relying on the centred heading was overriding a default\n rather than setting a value.\n\n## 0.11.0\n\n### Minor Changes\n\n- 3260391: A card title is a heading again, at a level the Block derives rather than guesses\n \n **The defect.** `CardTitle` renders a `div`, and that is correct: four cards in a\n grid are four titles under one section heading, and four `h2`s under one `h2` is\n four sections. But that reasoning is about the **element**, and the catalogue was\n reading it as a reason about the **outline** too. Every Block that drew a titled card\n drew it as plain text, so a page kept its `h2` and lost every heading below it. A\n feature grid of five cards was unreadable by heading navigation while looking\n identical. Found while migrating the company site, whose feature section had five\n `h4`s before the migration and five `<div>`s after.\n \n **The answer is one function, and it is the same answer in all six places.**\n `childLevel(headingLevel)` steps one level down, so a Block that draws a Section\n heading and a set of card titles puts the titles *under* the section, and the whole\n set moves together when the block is composed one level deeper than it was written\n for. A Block that draws no Section heading passes `headingLevel` directly, because\n there the card title is the one heading it renders. Both answers are right, and the\n test carries which is which rather than assuming one for all six.\n \n This is what answers the ticket's third criterion, that the same question be\n answered for *every* Item that draws a titled card. The sites are `FeatureGrid01`,\n `Pricing01`, `StackGrid01`, `AuthForm01`, `SettingsPanel01` and `AuthPage`, and the\n test that holds them is one file, because the consistency is the thing under test:\n five files with one case each would let the next Block answer differently and still\n pass, which is the failure this was filed about.\n \n **`CardTitle` gains `as`, defaulting to `div`.** The change is additive and no\n existing rendering moves. Its JSDoc previously told a caller to pass Prism's\n `Heading` inside it, and that was wrong: `Heading` is a step of the *type* scale\n whose floor is `lg`, and a card title sits at body size, so the only way to use it\n was to override the size back down, which asks a type-scale component to have no\n opinion about type. The element and the visual step are separate questions and this\n is the one that answers the element.\n \n **Two levels in one Block that had one hardcoded.** `SettingsPanel01` drew its\n group headings as a literal `<h3>`, so a panel composed under an `h4` in a document\n whose sections were `h4` announced its groups as siblings of the panel that\n introduces them. They are now one step below the panel title, so the panel's whole\n outline moves when the panel does.\n \n **`h6` holds rather than wraps.** A section already at `h6` has no child level.\n Wrapping to `h1` would put a card title *above* the section that introduces it, which\n a reader navigating by heading would meet first; holding at `h6` costs a repeated\n level, which a screen reader announces as the same depth rather than as a break in\n the outline. The clamp is asserted so a later change to the order is caught.\n \n **The proof that the test can fail.** Replacing `childLevel(headingLevel)` with a\n hardcoded `h3` in `FeatureGrid01` fails two cases, the default and the derived one,\n which is the exact defect the ticket names. A test that asserts \"is a heading\" would\n have passed that.\n- 932ce96: Add `ChartFrame` and `Sidebar`, the two token families that shipped with no consumer\n \n `chart-1` through `chart-5` and the eight `sidebar-*` roles were in the contract\n and used by nothing. These two Components are what consume them honestly.\n \n **`ChartFrame` puts the table inside the frame rather than beside it.** Marks and\n cells are two renderings of one `series` array, so drift is structurally\n impossible; a table passed as a sibling prop is a table that says last quarter.\n The table is always in the document and `table=\"visible\" | \"hidden\"` only decides\n whether it is *seen*, because `hidden` is `sr-only`: it is still announced and\n still findable in the page.\n \n **One frame means every chart in a product aligns to the same plot box.** Five\n charts each drawing their own axes is five sets of numbers that do not line up, and\n a dashboard where two charts disagree by six pixels looks broken in a way nobody\n can name. `mark=\"line\" | \"bar\"` is a prop and the marks are internal on purpose: a\n mark with no axis, no gridline and no baseline is a bar that lies, and exporting\n one for a caller to place is the exact failure the frame exists to prevent.\n \n **`Sidebar`'s collapsed state is the same rail at a smaller width, not a second\n Component.** Hover surface, focus ring, current marking and accessible name are\n all present in both states and only the words and the padding change. The name\n moves to `sr-only` rather than unmounting and the trailing count is `aria-hidden`,\n so the name is the same *string* at both widths and a reader who collapses the rail\n loses nothing.\n \n ## The measured findings behind both\n \n **The sidebar needs its own ring, and the numbers are in the tests.** `ring` on a\n `sidebar` surface measures 2.86:1 on Mint light, 2.97 on Sky, 3.13 on Peach, 3.16 on\n Lavender and 3.22 on Blush. Four of the six light-mode packs would have shipped a\n sub-3:1 focus indicator had the rail inherited the page ring, which is the\n measured justification for the whole `sidebar-ring` family existing.\n \n **The chart tokens are below 3:1 and that is why the legend is not optional.** In\n light mode `chart-2` measures 2.49:1 against `card` and `chart-3` measures 2.15:1.\n Both are exempt from the contrast gate as graphic objects, so a mark in either is\n found by position and shape rather than by colour. That is why the legend is\n mandatory, the stroke is 2px, and the table is not optional. Past about three\n series hue is the only channel left, which the documentation states as a consumer\n decision rather than hiding.\n \n **One pair in this family is unmeasured.** `sidebar-ring` against `sidebar-accent`,\n the surface a focused item sits on while hovered or selected, is 3.19:1 in the\n base pack's dark mode and 3.82 to 4.21 in the light packs. It passes everywhere\n and it is the tightest pair in the family with no row in `check-contrast.mjs`, so it\n is reported rather than papered over.\n \n ## Two gate gaps found and closed\n \n **`check-variant-ink` listed eleven foreground roles and omitted the two sidebar\n ones.** `sidebar-primary-foreground` and `sidebar-accent-foreground` are both\n required 4.5:1 rows in the contrast table, so the list was telling the `Sidebar`\n that its own required pairing was an inherited one. It is the first Component in\n the package to put a `sidebar-*` fill in a variant, so nothing had hit it. The\n alternative was to add a decoy `text-sidebar-foreground` to silence the gate, which\n would have been a false pass.\n \n **`check-focus-indicators` recognised `ring-ring` only**, so a `ring-sidebar-ring`\n would have been reported non-compliant. The `Sidebar` therefore keeps the browser's\n outline as well as drawing its ring, which is asserted by a test so it stays a\n deliberate guarantee rather than an accident. Both ring roles are now accepted.\n- 4650aff: Add `CommandPalette`, so a reader can reach any command by typing three letters\n \n Prism had a Select, a DropdownMenu and a set of Tabs, and nothing for the fastest\n route to an action that lives behind three levels of menu.\n \n ```tsx\n <CommandPalette\n open={open}\n onOpenChange={setOpen}\n label=\"Commands\"\n inputLabel=\"Search commands\"\n groups={[\n {\n id: 'appearance',\n label: 'Appearance',\n items: [\n {\n id: 'theme',\n label: 'Toggle theme',\n hint: 'Cmd K T',\n keywords: ['dark', 'light', 'colour'],\n onSelect: setTheme,\n },\n ],\n },\n ]}\n empty={{ message: (query) => `Nothing matches ${query}` }}\n />\n ```\n \n **It ranks, and that is the difference between this and a filtered menu.** Matches\n are scored by where the query falls: the start of the name beats the start of a\n word inside it, which beats a match further along, which beats a keyword-only hit.\n A palette that filters without ordering shows every command containing the query in\n declaration order, so the command the reader meant sits below one that merely\n mentions their query and they scroll.\n \n **The groups are ordered by their best match too.** Ranking only *within* a group\n leaves the premise broken, because an exact match in a late group still sits below\n a poor match in an early one. Ordering the groups by their strongest member puts\n the best answer at the top and keeps the headings, which is the whole of what a\n grouped list is for.\n \n **It is composed on the Dialog rather than beside it.** Focus trapping, Escape, the\n portal, the scroll lock and the return of focus are five behaviours that are correct\n in the Dialog and would be five chances to get one wrong here. A palette that rolled\n its own overlay would be a second answer to all five questions, and the second\n answer is the one that ships the bug.\n \n **The matched run is emphasised by weight, not by a background**, which is a\n deliberate difference from `Mark`. A Mark is right in a list of search results,\n where the match is the reason the row is there. In a palette the match is a hint\n while the label is what is being read, and a saturated background on every matched\n character fights the text it sits inside. Two surfaces, two treatments, one reason.\n \n Three behaviours are decisions rather than defaults and are asserted in the tests:\n \n - **Enter never runs a command the reader did not point at.** With nothing\n highlighted it does nothing. This is the one outcome a palette must never\n produce, and it is invisible in a screenshot because nothing on screen changes\n when it happens.\n - **The arrows wrap.** A palette is a transient surface where overshoot is common,\n and a reader who overshot should not have to press Up to come back.\n - **The highlight is clamped, not reset, as the list changes.** A reader who arrows\n down three rows and then types one more character is choosing from a list that\n moved under them, and yanking the highlight to the top discards where they were.\n \n `keywords` is what makes the palette good rather than merely present: a reader who\n has to name a command exactly already knows it exists, which defeats the surface.\n `suggest` covers the rest, because a palette that opens onto a bare list makes a\n reader type before they know what is available. `empty` takes the query so the\n sentence can be the caller's, which is the only way \"no results\" avoids shipping in\n English.\n- 932ce96: Add the data display substrate: item, scroll area, carousel, toggle and the rest\n \n Nine Items, and the one that matters most is `item`, because it is the row almost\n every list in the package hand-rolls today.\n \n **`item` is the row and not the list.** A caller writes the `ul`, the `dl` or the\n grid and composes rows into it, because the same row appears in four different\n parents and a Component that owned the list would own all four.\n \n **`scroll-area` keeps the browser scrolling and takes over only the appearance of\n the bar.** The alternative is a transform on a `div`, which looks identical in a\n screenshot and has no scroll position, so no keyboard, no `scrollIntoView` and\n nothing to announce. The custom bar is also a real accessibility obligation rather\n than a decoration: a scrollbar is a control, and a control made of `div`s is worse\n than the one the browser shipped.\n \n **`aspect-ratio` takes the ratio as a prop rather than as a class.** Prism's\n stylesheet scans only Prism's own source, so a consumer's `aspect-[4/3]` is a rule\n the shipped sheet does not contain, and the box silently collapses.\n \n **`carousel` is never the only route to its content.** Every slide stays in the\n DOM, the controls state where the reader is through a required `position` function,\n and there is a focus handoff: a control that has just been disabled leaves the tab\n order without giving up focus, so a reader who pressed \"next\" onto the last slide\n would otherwise be stranded inside the carousel.\n \n **`toggle` is pressed; a `switch` takes effect at once.** The JSDoc says so and one\n test asserts the three controls side by side, because confusing them is the usual\n failure and the confusion is a bug report rather than a compile error.\n \n **`toggle-group` changes the role, not the look.** `single` is a `radiogroup` of\n `role=\"radio\"` with `aria-checked`; `multiple` is a `toolbar` of pressed buttons\n where the arrows move the highlight without pressing it. A `group` carrying\n `aria-orientation` is an axe violation, so `group` was not implementable as the\n issue's phrasing suggested, and the toolbar is the more accurate role anyway.\n \n **`native-select` is an addition and not a rival.** The existing `Select` is Base\n UI: a `button role=\"combobox\"` with a portalled popup, not a `select` element. A\n native select is right when the platform picker beats anything this package could\n draw, and wrong the moment an option needs to be more than a string. The test\n renders both and asserts one is a `SELECT` and the other a `BUTTON`.\n \n **`button-group` draws the focus ring once around the group** and the members\n suppress their own, with the click-versus-keyboard trade stated rather than hidden.\n \n **`table-sort` announces the direction it will go next, not the one it is in**, and\n its cycle returns to unsorted in three clicks, so the caller's original order is\n reachable from the control the reader already knows.\n- 3260391: `DiagramNode` carries a subtitle and a pack, and an unlabelled `Diagram` is named by its nodes\n \n **The subtitle is drawn, not only announced.** `DiagramNode` had one line of text\n and `DiagramProps` forced `label` on the non-decorative arm, so the only channel\n to a screen reader was the label. That is a text-only channel: a sighted reader\n looking at the picture got nothing, and the drawn name and the announced label\n were two strings a caller had to keep in step by hand. The subtitle is a second\n `<text>` under the name, and a node without one emits no `<text>` at all, so an\n absent subtitle is not an empty line.\n \n **A derived name makes the drift structurally impossible rather than discouraged.**\n `DiagramProps` gains a third union arm in which `label` is absent and the name is\n built from the nodes in draw order: `Tokens (the shared language), Pipeline (the\n engine), Twins`. The arm is `label?: never` and not `label?: string`, so an unnamed\n diagram and a diagram named with an empty string cannot be confused, and a blank\n label falls back to the derived name rather than announcing an unnamed image.\n \n Relation words are deliberately **not** in a derived name. Inventing a sentence out\n of the edges would make `Diagram` write words, which this system does not do; a\n caller who needs the relations announced passes `label`, and the JSDoc says so.\n \n **`pack` is per node and typed `PackId`, not `string`.** A free-text field would\n accept a name matching no emitted rule and the mark would silently keep the pack\n above it. The closed six-pack union is the one `ProductMark` already uses. It is\n per node rather than per diagram because a `pack` is a boundary on a **mark**, and\n a mark in a diagram is a node; a pack on the whole diagram would be a boundary on\n the arrangement, which is the thing the boundary law is not for. It is set on the\n `<circle>` rather than the `<svg>` or the node's `<g>`, so it re-inks that one mark\n and moves nothing else, and a circle is a shape the pack cannot re-round.\n `'default'` is the absence of the attribute, as `ProductMark` spells it.\n \n `check-pack-boundary` now reads three boundaries and reports 0 findings.\n- 4650aff: Add `Diff`, where the bar measures how much of a line changed\n \n A conventional diff marks every changed line with the same coloured wash and a\n rail. That tells a reader a line changed and nothing about how much, so rewriting\n one identifier in a long line leaves the same mark as replacing the line outright,\n and the reader's eye, which is fast at finding saturated bands, is drawn to the\n least interesting change in the file.\n \n ```tsx\n <Diff\n lines={[\n { kind: 'context', oldNumber: 1, newNumber: 1, content: 'export function run() {' },\n { kind: 'removed', oldNumber: 2, content: ' const limit = 100', changed: [[8, 13]] },\n { kind: 'added', newNumber: 2, content: ' const limit = 250', changed: [[16, 19]] },\n ]}\n label=\"Changes to run\"\n labels={{ added: 'Added', removed: 'Removed', context: 'Unchanged' }}\n file=\"src/run.ts\"\n summary=\"1 addition, 1 deletion\"\n />\n ```\n \n **The bar is change density.** Its width is the fraction of the line that actually\n changed, so a one-character edit in a long line is a sliver and a rewritten line\n fills the gutter. The marks that used to be a wash become a measurement, and the\n eye goes where the reviewer's attention belongs.\n \n **The line numbers carry the side, not the colour.** An added line has a new number\n and no old one, a removed line the reverse. That asymmetry is structural, it is how\n every diff reader a developer has used distinguishes the two, and it does not depend\n on telling red from green. The colour is redundant on top of it, which is the right\n way round: shape carries the meaning, colour reinforces it.\n \n **The changed words are emphasised by weight, not by tint.** A diff that coloured\n them green and red would spend the two hues a reader is most likely to be unable to\n distinguish, and would also fight the line's own state colour. Weight survives\n greyscale, which is the condition any encoding here has to survive eventually.\n \n **It is a table**, because a diff is two columns of numbers beside a column of\n text, and row and column navigation then come from the semantics rather than from a\n grid of divs.\n \n `labels` is required, for the same reason a Dialog's close label is a prop: a\n shared library cannot know whether the word for this is \"added\", \"ajoute\" or\n \"hinzugefugt\". `summary` is the caller's too, and a diff of a rename has no\n additions and no deletions and is still a change, which is the case a computed\n sentence gets wrong. The counts are exposed as `data-added` and `data-removed` so\n a caller can style them without the Component shipping a sentence.\n \n A test caught a real flaw in the first draft. A minimum bar width was applied to\n every changed line to keep one visible, which meant a one-character change in a\n long line was drawn at twelve percent when the data said half a percent. A\n measurement is not allowed to overstate what it measured, so the floor now applies\n only where the density is genuinely unknown, which is a changed line the caller\n gave no ranges for.\n- 3260391: `DocsShell` refuses a page with no address, and renders a link with no words as a label\n \n **This is a behavioural change and the honest reading of it is that a previously\n rendered page now throws.** Two leaks fed it. `DocsNavGroup.href === undefined` was\n the only guard, so `href: ''` fell to the anchor arm on both arms of the union, and\n `flatten` copied the same value into `Neighbour`, so one bad row published a second\n broken link in the pager. `title: string` admitted `''` on both arms, rendering a\n link announced as \"link\" and nothing else. Nothing in the type could stop either,\n and the Component's own JSDoc claimed that it could.\n \n An anchor with an empty `href` is a control a keyboard can reach and cannot operate,\n so the fix is at the tree rather than at the rendering: one pass over both `nav` and\n `toc` at the top of `DocsShell`, before anything renders, and a **page** with a\n blank address throws, naming the tree and the entry. Only the page arm is refused. A\n group with no address has a documented rendering, and a page has nothing to render\n in place of the link, so a label there would hide a page the tree is missing rather\n than report it.\n \n **Blank counts as absent, and that is required rather than pedantic.** All three\n consumer adapters write `url: node.index?.url ?? ''` for a folder with no index, so\n on the group arm `''` is the documented \"no route\" state and must render as a label.\n \n **A second bug surfaced while fixing the first.** `containsHref` read the group's\n address directly, and `under('', currentHref)` is `currentHref.startsWith('/')`,\n which is true for every absolute address. Treating `''` as \"no address\" in the\n renderer without fixing this would have made every label-only section claim to be\n the current one at once, so it reads through the same helper the renderer uses.\n \n The rail and the pager now ask one function whether an entry is a destination, so a\n row that is a label on the rail cannot become a neighbour in the pager.\n- 932ce96: Add the feedback substrate: Spinner, Toast and the Empty State Block\n \n Three Items, and the one that changes an existing Component's behaviour is the\n `Toast`.\n \n **A pause holds the remaining time rather than restarting it.** A reader who\n hovers or focuses a toast for ten seconds gets the four seconds they had left, not\n four fresh ones, because a rest is a fact about the reader and the countdown is\n about the content. Restarting the clock on every pointer move means a toast can be\n held open indefinitely by a reader who simply rests on it, which is the opposite of\n what a pause is for.\n \n **`duration` defaults to 4000 with its reason, and `0` turns the clock off.** A\n toast that vanishes before a screen reader has finished announcing it is worse than\n no toast at all, and a toast that never leaves strands the reader. Both are\n judgements about the reader rather than about the content, which is why the\n default is stated and overridable rather than fixed.\n \n **`closeLabel` is required with no default**, and it is the only surface in the\n package where a default is wrong rather than merely unhelpful. A toast arrives on\n its own initiative, with no button the reader pressed and no context to carry an\n implication about the product's language, so four products in two languages cannot\n all be told the control says \"Close\". A default is right when the caller may\n override it; here the string is the whole of the contract and guessing it is\n guessing a product's words.\n \n **The enter and leave are three phases with the handoff on the element's own\n `transitionend`**, filtered to `opacity` and with no second clock. Two timers for\n one animation is two things to keep in step, and a mismatch shows as a toast that\n fades and never goes.\n \n **`Spinner` is a server Component.** It holds no state, runs no hook and attaches\n no handler, so the client directive would be a claim about work it does not do. Its\n JSDoc states the three-way difference from `Progress` and `Skeleton`, because\n \"reports a position\", \"has the shape of what is arriving\" and \"something is\n happening\" are three different claims and the usual failure is using the third when\n the first or second is what the reader needed. The honest reason a spinner is often\n wrong is layout shift: it hides the shape of what is coming.\n \n **`EmptyState01` requires a `reason` closed to three values**, because\n `first-run`, `no-match` and `not-permitted` want different words and different\n actions, and a consumer who cannot say which kind of empty they have writes \"No\n data\", which is the state this Block exists to end. It throws rather than shipping\n a button that lies, following the `instrument-panel-01` precedent.\n \n ## Nothing was deleted, and that is a finding\n \n The fourth part of this ticket was to remove a superseded `empty` entry. The search\n was exhaustive across `packages/tokens/src`, the emitted tokens, the catalogue, the\n registry and the whole tree, and there is no `empty` token, no `empty` Component and\n no `empty` catalogue entry to remove. It was already resolved: `DESIGN.md` records\n that the old `empty` Component \"returns as `empty-state-01`, a Block\", and the\n change note that renamed it is in the history. Nothing was invented in order to\n have something to delete.\n- 932ce96: Add the form substrate: combobox, calendar, date picker, number field and the rest\n \n Nine Items, and the one that changes an existing Component's behaviour is the\n `Combobox`.\n \n **Typing never discards a choice.** A field showing exactly the chosen label is\n not narrowing anything, so \"chosen and filtered out\" is not a reachable state: the\n answer changes only on an explicit choose or an explicit clear. A combobox that\n empties its own selection as a reader types is the usual failure, and it loses\n data silently.\n \n **`Command` is the leaf row and it ranks nothing.** `matchRange` is a prop, so\n whoever filtered the list decides where the match fell, and one command object can\n be spread onto the row with its `keywords` accepted and never rendered.\n \n **`Calendar` keeps a disabled date drawn and in the arrow path, and refuses to be\n chosen.** A date that is simply absent is indistinguishable from a grid that\n failed to render that week, and a reader paging with the arrows should not have\n the path change under them.\n \n **`DatePicker` reseeds the month on show from the chosen value**, so re-picking a\n date in another month does not silently reset the grid to the month the caller\n created the field in.\n \n **`InputGroup` puts the focus on the control and the ring on the control.** The\n frame is a `div` and is never focused, because putting the ring on a wrapper is how\n an indicator ends up around the wrong box.\n \n **`NumberField` clamps a value as it arrives, not only as it is typed**, and an\n empty field reports `null` rather than zero. Base UI clamps typing and refuses the\n steppers past a bound but lets an out-of-range `value` straight through, so the\n clamp is applied on the way in and the consequence for a controlled consumer is\n documented: `onValueChange` is the authority.\n \n **`OneTimeCode` holds the code as one value, not six.** Backspace removes a\n character from the string and the rest close up, which is the only rule for an\n empty box that is not wrong for somebody. Base UI's own field strips the label from\n the first segment on purpose, so a visually hidden `<label for>` is rendered\n instead, and its steppers are put back in the tab order because an announced\n control a keyboard cannot reach is worse than a second stop.\n \n **`Form` makes the error and the way out of it one unit.** `FormError` draws the\n message and the caller's `action` in a single live region that is in the document\n before either of them, so a server error that arrives with the form is announced\n rather than appearing silently.\n \n **`Label` keeps the required mark as decoration.** The control's own `required` is\n what is announced and what the form enforces, so a second signal that says\n something slightly different is the thing to avoid.\n \n ## The ranking is now one module, not two surfaces' private copy\n \n `locate` and `RANKS` moved to `packages/ui/src/lib/rank.ts`, and the `Combobox`\n imports them from there rather than from the `CommandPalette`. A command palette\n and a combobox that each carried their own scorer would agree for a month and then\n diverge on the case nobody thought about, and a reader would find a query that\n floats to the top in one surface and sinks in the other. The first version put the\n scorer inside the palette and the combobox reached into a composite for it, which\n compiles and typechecks and is still wrong: the next surface to need one has no\n honest module to import, and the tempting answer is to copy the one it can see.\n- 3260391: A hero action is the element you asked for, and a product row can carry its own sentence\n \n **`HeroAction` is a union, so a wrong action is a compile error.** It was one shape\n with an optional `href`, which made both likely mistakes silent. `{ label: 'Start\n free' }` compiled and rendered a primary button that went nowhere, and a hero's\n first action is almost always a link, so that is the likely one and it looks\n correct on the page. `{ label: 'Start free', href: maybeUrl }` compiled too, and\n rendered a button whenever `maybeUrl` was `undefined`, which is a runtime branch the\n type said nothing about.\n \n So the link arm **requires** `href` and the button arm **forbids** it, as\n `href?: never`. The second is the one worth having: a value that is sometimes a\n string and sometimes `undefined` is now a type error rather than a button. That is\n the ticket's \"a caller should not be able to get it wrong silently\", and it is a\n type rather than a runtime branch, which is the criterion the ticket asked for.\n \n **The forward arrow now follows the element, not the position.** It was\n `index === 0`, so an inert button was the one control in the action row wearing the\n mark that says it can be followed. A first action that is genuinely a button gets no\n arrow, and a second action gets none even when it is a link, because the arrow marks\n the row's one primary destination.\n \n The default **variant** stays positional, deliberately, and the two now move\n separately. What looks primary is a fact about the row's shape; whether it navigates\n is a fact about the element. Asserting they do not move together is the point,\n because the arrow used to follow position for both.\n \n The ticket's claim that the Block \"renders every action as a plain `<Button>`\" was\n stale, the way the Cta01 one was: `CtaLink` had already fixed the rendering. What\n was left was the half that had not been fixed, the type, plus the arrow and a test\n file, which did not exist. Eight tests assert the rendered element for each arm, the\n negative as well as the positive, the arrow on a link and its absence on a button,\n and `newTab` with its `rel`.\n \n **`ProductGrid01Product` gains an optional `detail`, and the JSDoc settles which\n level a sentence belongs at.** The documentation previously said a second line\n \"belongs above the grid in `description`, where it applies to the set\". That is true\n of a sentence about the set and false of a sentence about one product, and the\n company site had five products each carrying a sentence that was true of one and\n vacuous beside the other four. The migration folded nothing in, so three published\n sentences were dropped and the ledger recorded the loss as a catalogue gap rather\n than papering over it with a copy edit.\n \n Both levels now exist and the question to ask is written down: **does the sentence\n survive its neighbours?** A `description` would be equally true if you deleted any\n one row. A `detail` is false, or vacuous, beside the other four. A tagline is the\n shortest true thing about a member and reads the same in every row, which is what\n distinguishes it from a `detail`.\n \n A `detail` is drawn only when passed. A row that reserved the line would push every\n row below it down by one, and this is a stack of full-width rules where a ragged\n left edge is the most visible thing on the page. Six tests render both shapes,\n because a test asserting only the new field would pass on a Block that had quietly\n stopped drawing the grid-level description, which is the half every existing caller\n depends on.\n \n **Two proofs that the new tests can fail.** Reverting the arrow to `index === 0`\n fails exactly one case, the one the defect lives in. Reverting\n `childLevel(headingLevel)` to a hardcoded `h3` in the card-title work fails two.\n Neither defect would have been caught by an assertion of the shape the ticket\n described.\n- 4650aff: Add `Meter`, so a bounded measurement can show the limits it is approaching\n \n A disk at 95 percent and a test at 95 percent are the same reading until you say\n where the line is. Prism had a Progress for a task moving toward an end whose\n length is not known in advance, and nothing for a quantity that already has an\n answer against a limit the caller knows.\n \n ```tsx\n <Meter\n value={97}\n label=\"Storage used\"\n valueText=\"97 gigabytes of 100\"\n thresholds={[\n { at: 80, tone: 'warning' },\n { at: 95, tone: 'destructive' },\n ]}\n >\n <span>97 of 100 GB</span>\n </Meter>\n ```\n \n **The thresholds are the design.** A fill on its own is one number, and a number\n with no limit beside it cannot be acted on. Drawing the caller's own limits as\n notches on the track puts the boundary next to the reading, so a consumer passing\n a latency budget gets a latency budget rather than a generic bar filled to a\n similar fraction. The Component has no opinion about which numbers matter.\n \n **It stays neutral below every threshold.** It can see that a value is 40 percent\n of a maximum; it cannot see that 40 percent is a problem. The same number is\n routine on a latency budget and urgent on a disk quota, so a Component that\n coloured itself would be making a claim about the caller's product on the\n caller's behalf.\n \n **It is drawn as a hairline with ticks, not as a bar.** The Progress in this system\n is a two-pixel rounded trough, and a Meter that looked like one would be read as\n one. The difference is visible before it is read.\n \n `role=\"meter\"` rather than `progressbar`, which is the ARIA distinction the whole\n Component turns on, and assistive technology reports the two differently. It is\n not a live region: the change that moved the reading is usually the thing worth\n announcing, so a consumer that wants it announced wraps it in a `LiveRegion`.\n \n Two failures are handled rather than rendered, because neither is visible in a\n screenshot. A value past the maximum is clamped to the track instead of drawn\n past its end, and a scale whose minimum equals its maximum draws an empty track\n instead of a `NaN` width, while still measuring.\n- 932ce96: Add the overlay and menu substrate: alert dialog, context menu, menubar, sheet\n \n The eight Items that every menu-shaped surface in a product otherwise hand-rolls.\n Each is composed on the Base UI primitive rather than reimplemented, because focus\n trapping, portals, dismiss-on-outside-press and escape handling are four behaviours\n that are already correct there and four chances to get one wrong here.\n \n Each one makes a decision a consumer would otherwise make badly:\n \n - **`alert-dialog`** removes dismissal from the *type*, not just the default.\n `modal` and `disablePointerDismissal` are omitted from its props, so a caller\n cannot pass a value that re-enables outside-press dismissal on a surface whose\n subject is a decision. Escape still closes, and there is no corner X, because an X\n is a third answer to a question with two. `AlertDialogAction` is its own part,\n styled destructive by default, because the one button that must exist is that one.\n - **`context-menu`** knows which rows are commands and which are destinations.\n `ContextMenuItem` is a `div role=menuitem`; `ContextMenuLinkItem` is a native\n anchor. Two parts rather than one, so the \"open in a new tab\" case cannot be a div.\n - **`hover-card`** can never be the only route to what it shows, which is what makes\n its delay safe. The trigger is a real anchor, so a long delay costs a reader\n nothing, and `delay` is a prop because a pointer rest is a fact about the reader.\n - **`menubar`** is one Tab stop and owns the arrows inside itself, and it requires a\n `label` because nothing announces a bar until focus lands on it.\n - **`navigation-menu`** knows it holds links, so it ships no command part at all, and\n a closed group holds no links with `keepMounted` as the stated seam.\n - **`sheet`** is the Dialog with an edge. `side` is required and excludes `center`.\n All six overlay behaviours are inherited, not reimplemented, and a sheet dismisses\n on an outside press, which is the contrast with the alert dialog.\n - **`collapsible`** wires the trigger and the region with the library's own\n `aria-controls` and `aria-expanded`, and unmounts the closed panel so find-in-page\n and a screen reader see the same page the reader does.\n - **`resizable`** puts the whole behaviour on a focusable `separator` with a value,\n and remembers nothing: the position is a prop and a callback, because a design\n system cannot know a reader's panes.\n \n **`resizable` is not composed on Base UI, because Base UI 1.8.0 ships none.** The\n package's exports were enumerated and there is no `./resizable` and no `Resizable*`\n symbol, so the separator role, the focusable-divider keyboard model and the pointer\n drag are authored here. That is the one hand-rolled overlay in the batch, and it is\n confined to the three things an overlay must not reimplement, none of which a\n divider needs. It should be revisited when Base UI ships one.\n \n The focus-indicators gate test no longer pins which slot happens to sort first in\n the composite-widget bucket, nor the exact excluded total. Every additional menu in\n the package adds members to that bucket, so both were tests that failed when a\n correct Component was added. The count is now bounded from below, which is the\n invariant that holds as the tree grows.\n- e14a903: `ProcessFlow01`: a pipeline of any length, drawn in order across as many lines as it needs\n \n `ProcessRail01` holds two, three or four steps and refuses a fifth in its type, and\n that decision is not in question. A rail is a claim about a sequence on one line, and\n a rail that quietly dropped a step to fit a width would be a diagram of a process that\n is not the process. A company site states six stages and was drawing them as a grid of\n short points, where the six ordinals and the terminal label were lost: a set read\n where a sequence had been.\n \n So the repair is a second shape rather than a wider tuple. Raising the rail's ceiling\n to six would have made the fourth column unrepresentable as a type error, which is\n the property that made the original decision good.\n \n **It is one ordered list, and the layout is built around that.** A flow that wrapped\n into a list per line would be several lists, and a screen reader would announce three\n lists of two, which is precisely the set-where-a-sequence-was this Block exists to\n prevent. So the stages are one `<ol>` and the wrapping is done by the grid. That has a\n consequence worth stating: the Block never learns where a line broke, and therefore\n cannot draw anything that is only correct on the widest screen.\n \n **The ordinal is the continuity mechanism, and it is treated as one.** It runs\n continuously from `01` to the last stage and it is stated as text rather than only\n drawn, so a reader who lands on stage four hears `04` and knows it continues stage\n three at whatever width they are using. Nothing else in the Block is load-bearing for\n the sequence, and that is why nothing else needs to know where the line broke.\n \n **The thread is a line through the stages, not a box around each one.** Each stage\n draws a top border and the grid separates them by a single pixel, so a run of stages\n on one line reads as one line broken by hairline gaps and the gap between two lines is\n wider. It is the rail's technique, and it is chosen here for one reason: it holds at\n any column count, including the single column a phone gets, without the Block knowing\n anything about it. A connector drawn between a stage and its successor would need the\n column count to be right and would be wrong at every width the type does not describe.\n \n **`stages` is an array and `columns` is a prop**, which is the split the ticket asks\n for. The length is content and the grid carries it; how many sit on one line is\n layout, and a caller who has to state both has to keep them in step by hand.\n \n **A flow of fewer than two stages throws.** One stage is a label, not a sequence, and\n the line the Block draws through it would claim a sequence that is not there. The\n message names `ProcessRail01` as the answer for two.\n \n ## The proof that the tests can fail\n \n The two assertions that matter are structural rather than visual, and each was proved\n by writing the mistake it exists to catch:\n \n - An ordinal that restarts per row, which is what a per-row implementation does, fails\n two cases: the continuous-ordinal assertion and the terminal-label one, because the\n last stage is then no longer the one the label is attached to.\n - A list nested per group of three, which is the set-where-a-sequence-was failure\n arriving through the markup, fails four.\n \n An assertion that the flow \"looks like\" a sequence would have passed both.\n \n The stage names are deliberately **not** headings. The sequence is the list, and\n promoting six stages to headings would put six entries in the outline for one process,\n so the section title is the only heading this Block renders.\n- 4650aff: Add `RunConsole01`, a run as a heading, a budget and the steps that got there\n \n The surface the fourth Kind exists for, and a Block rather than a Component\n because a run console is not one thing: it is a measurement beside a sequence\n beside a stream, and a reader needs all three in the same frame to answer the\n only question they have, which is whether the run will finish and what it is\n costing.\n \n ```tsx\n <RunConsole01\n title=\"Nightly reconcile\"\n streaming={running}\n copy={{\n budgetLabel: 'Budget spent',\n budgetValue: (value, max) => `${value} of ${max} credits`,\n stepsLabel: 'Run steps',\n title: 'Run',\n waiting: 'Waiting for the first step',\n }}\n budget={{ value: 4200, max: 5000, thresholds: [{ at: 4500, tone: 'warning' }] }}\n steps={steps}\n />\n ```\n \n **It reimplements nothing.** The budget is a `Meter`, which draws the limits the\n caller named and stays neutral below all of them. The steps are a `Timeline`, which\n draws each duration to scale against the slowest one. What the Block adds is the\n arrangement, and two facts belonging to neither component: a run with no budget\n gets no budget meter, because a local run has no ceiling to be near and an empty\n meter is a measurement of nothing that is invisible because an empty meter looks\n like a meter; and the budget sits above the steps, because the first question about\n a run is whether it will finish and two columns would make the reader choose.\n \n **The live region exists only while the run is arriving.** A test caught this. The\n first draft wrapped the steps unconditionally, which left a live region on the\n page for a run that had already finished: it announces nothing on mount, but it is\n still there, and a later re-render with different steps is an unrelated change it\n will announce. Mounting it when the run starts is also the order that does not\n lose an announcement, because the region is on the page before the first event\n arrives, which is when a screen reader is listening. It wraps the steps and not the\n budget, so an append does not re-read the spending every time.\n \n **`LiveRegion` is a server Component, and that is a correction.** It shipped in\n 0.8.0 carrying `'use client'` while reading its props, holding no state, running no\n hook and taking no event handler. The directive was a claim about work the\n Component does not do, and sixteen of forty-one components in this package are\n client. A live region is announced by the browser's own mutation observer rather\n than by JavaScript, which is the reason it is a good primitive: it works in a\n server-rendered page.\n \n ## The client budget\n \n The whole-tree client bundle measures **108.1 KB** with this branch's components\n in it, against a 116 KB ceiling, and the five components here cost about **0.1 KB**.\n \n That is worth stating plainly, because the number looked very different for a\n while. An earlier draft of this branch reported that the components added 2.3 KB,\n crossed a 92 KB ceiling, and needed the ceiling raised. That measurement was taken\n against a gate whose roster read two directories, and it was wrong for the same\n reason the old ceiling was: 53 emitted client modules were never read. The roster\n has since been widened to the whole tree, the honest figure is 108 KB, and these\n components land inside the existing ceiling with about 8 KB of headroom.\n \n **No ceiling change is proposed here.** The one that was drafted has been dropped\n rather than applied, because it would have moved a number to accommodate a\n measurement that was itself measuring the wrong set of files.\n- 3260391: `StackGrid01` names the real product behind a codename, and `Cta01` states the rule it enforces\n \n **`StackPart` gains an optional `realName`.** The value goes inside the tile the\n Block already draws, so it is an additive optional field, matching `ProductGrid01`'s\n `pack` and `StatusLedger01`'s `detail`. It renders only when passed: a tile that\n reserves a line for absent content is a layout shift on the first thing a reader\n scrolls to, and because the grid is a grid, an empty line in one column would also\n knock the row's baseline out for every tile beside it. Both are asserted, the\n second by counting elements rather than by checking a class.\n \n **`Cta01`'s JSDoc told consumers the opposite of the truth.** It said a panel wants\n `default` or `secondary` and never `outline`, which was the answer *before* the\n `outline` variant was given its ink, and it contradicted the code forty lines below\n it, where the second action defaults to `outline`. A consumer reading the type would\n have been actively misled. The settled rule is the inverse on the first variant:\n `secondary` and `outline` are both fine and `default` is the one to avoid, because\n the band is `bg-primary` and `default` fills with `--primary`, so a default action\n on this band is the band's own colour against the band's own colour. Its label is\n legible, because `primary-foreground` on `primary` is a gated pair, and the control\n has no edge, which is a different defect and the reason the sentence is about the\n fill rather than the text.\n \n **A test that measured a pair it believed in.** The existing resolved-ink test\n asserted the pair the test itself thought the defaults resolve to, so it stayed green\n on a Block that had stopped asking for it. The new case renders `Cta01` with no\n explicit variant, reads the utilities off the two anchors, maps each to the token it\n names, and resolves those tokens from the emitted CSS. It asserts the resolved pair\n rather than the class string, because a class-string assertion passes on a token\n change, and it adds two things a ratio cannot see: that the ink is stated rather than\n inherited, and that the fill is one the band is not.\n \n The ticket's single lavender-dark figure understated the blast radius. The base pack\n failed in **both** modes, at 1.00:1 and 1.01:1, and seven of twelve combinations were\n affected. It is 13.59:1 or better in all twelve now.\n \n **The acceptance criterion asked for the wrong instrument.** It asked for a row in\n the token contrast gate, and that gate structurally cannot see this class of defect:\n it measures token pairs, and what failed was a component declining to use a value it\n was relying on by accident. `check-variant-ink.mjs` is the gate that holds it, and it\n already existed.\n- e14a903: The fourth Kind: `live`, a surface whose content changes without a navigation event\n \n **This is a breaking change, released as a minor: 0.11.0.** `CATALOG_KINDS` gains a\n member, so a consumer switching exhaustively over `kind` is broken. A minor is the\n right line for a breaking change from a `0.x` version, where anything may change at\n any time, and taking 1.0.0 would declare the public API stable rather than describe\n this change. `CONTRIBUTING.md` says a breaking change is a `major` bump, so this is\n the one place the convention and the record disagree; the disagreement is deliberate\n and `DESIGN.md` carries it with the reasoning.\n \n **A `live` surface is what the other three cannot express.** A Component, a Block and\n a Page are all rendered from props, and props arrive when the caller says so. A run's\n event log, a monitoring view, anything fed by a socket, changes on its own. That is\n the whole of the fourth Kind, and it is why `RunStream01` is the first client\n Component in the package: a surface that receives events owns the subscription that\n delivers them, and that is not an accident of implementation.\n \n **Prism owns the surface, the consumer owns the transport.** `subscribe` is a\n function the consumer supplies and a function it tears down. There is no socket, no\n endpoint, no retry policy, no persistence and no provider to mount, so a consumer\n that already has a connection passes it in and one that does not has not acquired a\n client runtime by importing this.\n \n **Four transcriptions were found by the compiler rather than by a reader**, which is\n the argument for the ties that already existed. `STORE_KINDS`'s `_KindsMatch`\n assertion fired on the first edit. The exhaustive `KIND_LABELS` record added\n earlier fired the next, and adding a Kind is now a compile error in both places. The\n corpus builder's `KIND_SEGMENT` produced a mirror path of `undefined` and the\n `llms` gate reported it. And the site's three copies of the Kind labels were\n collapsed into one table, so the fourth Kind is one edit rather than three.\n \n **One divergence is deliberate and named.** A `live` surface ships as\n `registry:block`, because the `type` field belongs to the shadcn registry schema,\n which has no live surface and rejects a type it does not know. The **Kind** is ours\n and is `live` in `CATALOG_KINDS`, the corpus, the MCP tools and the site's Sections.\n The registry is a derived internal artefact, never served and never an install lane,\n so the two vocabularies are allowed to differ because only one of them is ours. The\n catalogue gate carries it as a named exception keyed on the Item's own `source`, so\n the exception cannot drift from the thing it excuses, and the other 103 items are\n still compared strictly.\n \n **The Section plurality rule is gone, and two Sections is why.** It read that a\n Section holding Items is plural. `patterns` broke it from the prose side and `live`\n from the catalogue side, because `components`, `blocks` and `pages` are plural nouns\n and `live` is an adjective with no plural. A rule two of nine Sections decline is a\n convention, not a law, and a second exception would have invited a third. What is\n asserted instead is the fact underneath it: every Section is registered once, in the\n root ordering, at a segment the manifest agrees with.\n \n ## The proof that the tests can fail\n \n `RunStream01`'s fourteen tests are proved by writing the mistakes they exist to catch.\n Appending instead of sorting, which is the reconnect bug, fails the ordering cases.\n And three defects were found by the tests rather than reasoned about in advance:\n \n - **`statusLabel` rendered twice.** The heading fell back to it when no `title` was\n given, so \"Failed after 3 attempts\" was the surface's subject *and* its state. A\n status is not a heading.\n - **`initial` was a trap.** It read like the current event list and quietly seeded\n only once. It now also *reseeds* when `resubscribeKey` changes, which is what a\n second run in one surface means, and the log clears with it rather than the old\n run's events growing the new run's.\n - **A `setNewest` before its declaration**, which the run-switch test caught as a\n temporal-dead-zone error and which no type checker in this repository would.\n \n **A `live` surface is budgeted, not exempted.** A Block composes Components, so its\n own client figure restates theirs. A live surface is the only client code a consumer\n pulls in *for itself*, so it has a row: 10 KB, of which `ScrollArea` is 8.1. The\n figure is set above the measured size, because a budget below it is a wish.\n- 4650aff: Add `Timeline`, so a run of events can be read for where the time went\n \n A run's events in order answer \"what happened\" and are silent on the only other\n question anyone has about an agent run, a deploy or an import: which step was\n slow. Today that means reading every number and doing the arithmetic.\n \n ```tsx\n <Timeline\n entries={[\n { id: 'plan', title: 'Plan', duration: 900 },\n { id: 'read', title: 'Read feed', duration: 4200 },\n { id: 'edit', title: 'Edit source', duration: 1200 },\n { id: 'done', title: 'Ready' },\n ]}\n label=\"Run steps\"\n />\n ```\n \n **The bars share a left edge and are scaled against the longest step in the run.**\n That is the whole idea, and the relative scale is the deliberate choice: the shape\n of the run is the question, and a shared zero baseline would be a second axis\n nobody reads. The consequence worth stating is that the bars answer \"which step was\n slow\" and deliberately do not answer \"how long did the run take\", because only the\n caller knows whether its steps ran sequentially or overlapped.\n \n **A step with no duration draws no bar.** A run's opening event has no length, and\n a zero-width bar beside it would read as \"this was instant\" rather than \"this has\n no duration\", which are different claims and only one is true.\n \n **It is an ordered list, so the sequence and the count are in the accessibility\n tree.** The marks and spine are `aria-hidden` on the rail that holds both, and the\n entry's own words carry the state, so nothing depends on a reader distinguishing\n the marks. It is not a live region: a run still arriving is normally wrapped in a\n `LiveRegion` by the caller, because announcing the list itself would re-announce\n the whole run on every append.\n \n Three degeneracies are handled rather than rendered, and all three are invisible in\n a screenshot: a run where no step reports a duration draws no bars rather than\n dividing by nothing, a zero duration does not win the longest-step comparison and\n scale everything else away, and the spine stops at the last entry rather than\n trailing past the end of the run as though a step had not arrived.\n \n The four states are a different vocabulary from the four product-capability tiers\n a `StatusLedger01` row carries, and they are two lists about two subjects rather\n than two lists about one: a `planned` agent run is a category error and a `failed`\n capability is a category error, which is the test that tells them apart.\n\n## 0.10.2\n\n### Patch Changes\n\n- `Hero01`'s headline and supporting line take a node rather than a string\n \n `SectionHeading` has always taken a `ReactNode` for both, so a `string` here made\n this Block the odd one out rather than making it stricter. Three of the five\n product sites set the emphasised word in their own mark inside the headline, so\n the narrow type would have cost each of them the emphasis or a second component.\n \n A widening, and additive. Every string a site renders is still the site's own, and\n `check-block-copy.mjs` still holds this Block to shipping no copy of its own.\n\n## 0.10.1\n\n### Patch Changes\n\n- A drawing's data props accept a read-only array\n \n `PulseGraph`, `PulseSeries` and `Diagram` took `SomeNode[]` rather than\n `readonly SomeNode[]`, so a consumer whose content is held as `as const` had to\n copy the array at every call site to satisfy the type. Every NaniSoft product site\n holds its landing content as `as const`, so the copy was not optional: the first\n consumer to compose a running hero had to write `nodes={[...FIGURE.nodes]}` to get\n it to compile, which is a cost in exchange for nothing.\n \n The props now take a read-only array. This is a widening, so it is additive rather\n than breaking, and it is the correct shape for a prop that only reads its data: a\n Component that mutates an array a caller passed is a Component that has taken\n ownership of it, and none of these three do.\n\n## 0.10.0\n\n### Minor Changes\n\n- 75434f8: Add a running figure to the system, and the ambient cycle scale that prices it\n \n The system had one law of motion: motion is state feedback, on an 80/160/280\n millisecond scale, and there are no keyframes anywhere. That law was right and\n it held. It also meant a product page could say what its product does and show\n nothing about it, which is what happened to four sites at once.\n \n This adds the second law, and it is a separate law rather than an exception to\n the first. Feedback is a response to something the reader did. A cycle is a\n demonstration of something the system does, and no reader is waiting for it.\n The test for which one a motion is does not require taste: stop the animation\n and ask whether the figure is still true and still legible.\n \n **New Components**\n \n - `PulseGraph` draws a running system: named nodes, the relations between them, a\n marker travelling a rail, and a breathing focus. A node's `lane` says it is a\n stage in a sequence, which draws the rail; a node with no lane is a field.\n `carries` on a relation says the line is carrying something, and draws a head\n at its end so the claim survives motion being off.\n - `PulseSeries` draws a live instrument: columns rising out of a caller-named\n baseline, with a reticle crossing them once per cycle. The values are the\n caller's own and the tallest sets the scale.\n - `SignalField` is a field of marks: the atmosphere a figure is drawn over.\n Decorative by default, still unless asked to drift, and honestly named as the\n one component in the system that is texture.\n \n **`Hero01` gains the band it was missing**\n \n All four sites hand-wrote the same two-column grid, each with its own gap and its\n own breakpoint. It is now the `instrument` slot, and an action naming an `href`\n renders as a real link rather than a button that goes nowhere. The centred form\n is unchanged, so this is additive.\n \n **New tokens**\n \n `ambient` and `ambient-ease` are two closed groups measured in seconds, separate\n from `duration` because folding them in would have made one name mean both 280ms\n and 7.2s. The emitted contract asserts the closed set and fails any cycle under\n one second, because a sub-second cycle is a flicker and no value in the 80 to\n 280ms band could ever have caught that.\n \n **What this does not change**\n \n Nothing is hidden. No ambient rule sets `opacity: 0` and nothing waits for a\n script, an intersection or a timer, so every figure is complete at first paint\n and a consumer needs no exception to enable the `hidden-state` gate. Reduced\n motion is one `animation: none`, and because every resting state is the full\n form, that reader gets the same figure, still. All three are server Components,\n so this adds zero bytes of client JavaScript.\n\n### Patch Changes\n\n- cb16073: A variant that sets its own fill sets its own ink, and `outline` now says which\n \n `Button`'s and `CtaLink`'s `outline` variant carried `bg-background` with no `text-` for the life of the package. On the page ground and on a card the omission was invisible, because the ink a control inherits there is `foreground` and `card-foreground` is equal to it in all twelve pack and mode combinations, so the variant was correct everywhere the design system itself placed it and wrong everywhere else.\n \n `Cta01` draws its band as a filled `bg-primary` surface with `--primary-foreground` as the band's ink, and defaulted its second action to `outline`. The second action was therefore `--background` behind `--primary-foreground` and measured **1.00:1 to 1.10:1 in seven of the twelve combinations**: the base pack in both modes, where light mode pairs `#ffffff` with `#ffffff`, and all five pastel packs in dark mode. The button was in the document, focusable, announced, and unreadable. It was found by resolving computed values in a browser, because neither the class name nor the token contract says anything about it.\n \n The fix is `text-foreground` on the variant, which is a no-op on the page ground and on a card and correct everywhere else. Measured across all twelve after the change: lowest is 13.59:1.\n \n A consumer that passed `variant=\"secondary\"` on that action, as the product site did, saw no difference and needs no change. The default is unchanged, deliberately: `outline` reads as one filled action and one alternative, and it is now a safe default because it carries its own ink rather than because it was made to stop.\n \n Two gates hold it, and neither could have caught the original:\n \n - `check:variant-ink` fails a variant whose own fill has no `text-` utility of its own, read from the source. It reads source rather than the emitted stylesheet on purpose: Tailwind only emits a variant something uses, so the sheet is a record of what was chosen rather than of what is available. A state fill such as `hover:bg-accent` is exempt, because exempting it is the difference between a rule that finds the defect and a rule nobody can run.\n - a test resolves the pair from the emitted token CSS for all twelve combinations rather than asserting a class name, and pins the seven that used to fail by name. A test written over class names would have passed against the broken button, because the class name never changed; only the resolved pair did.\n \n `Cta01Action`'s `variant` now states which weight a panel wants and why, with the measurement, so a consumer does not have to discover it.\n- Updated dependencies [75434f8]\n - @nanisoft/prism-tokens@0.10.0\n\n## 0.9.0\n\n### Minor Changes\n\n- 93758a6: The face ships with the package, so a site writes one property for its typeface\n \n `@nanisoft/prism-ui` declared `--font-sans: Inter, ui-sans-serif, system-ui, ...` and shipped no font file. Naming a family is not shipping it: the first entry resolved to nothing and every page fell through to the platform's UI face, which is the one face a design system never means by its first choice. All four consumer sites were compensating with their own custom property and a build-time network fetch, and the company site had already shipped that way by accident.\n \n The face is here now, as three latin roman weights with the licence beside them, and the `@font-face` sources are relative to the emitted stylesheet so they resolve from a consumer's install rather than from the consumer's origin. That last point is the old line's failure exactly: it emitted the face at an absolute path, which resolved against whichever site imported it, and the face silently failed in all four repositories.\n \n A consumer's own override is now unnecessary rather than merely discouraged. `--font-sans` resolves to a face this package ships, so a site that deletes its `--font-sans` override still renders in the design system's face rather than in the platform's.\n \n Three gates hold the arrangement, and none of them is discipline:\n \n - `check:typeface.mjs` fails a token family that names no face this package ships and no family the reader's own machine provides, read from the **emitted** stylesheet rather than the source. The original defect was invisible in source: the rules shipped, the subsets were preloaded, and only the class carrying the variable was missing, so the family resolved to nothing at computed-value time with no error anywhere.\n - the same gate fails an `@font-face` source that is absolute or a web-root path, which is how the old line failed silently.\n - the build refuses to emit a stylesheet that names a face it did not copy, so a package cannot ship a sheet pointing at a file that is not in the tarball.\n \n The face is 70.7 KB and every consumer installs it whether or not it renders a page, because a subpath does not reduce install size. That is the accepted price of one package instead of two.\n \n The weight axis is absent, deliberately. The design system retired the width axis, and optical sizing, which it approximated, is the browser's initial behaviour, so a file carrying a width range is not the file this design asked for. 700 is absent for a second reason: the authored scale emits it and the shipped source renders it nowhere, and a 23.8 KB file no page renders is 23.8 KB every consumer downloads forever.\n\n## 0.8.0\n\n### Minor Changes\n\n- 86cbc20: The accessible name a control announces is now a prop, so a consumer can localise it\n \n `Pagination`, `Dialog` and `Breadcrumb` each shipped an `aria-label` that a\n consumer could not change, and `ProductSwitcher` shipped a `label` default that\n put the word \"Products\" into every consumer's navigation. A consumer could\n localise the visible text of a pagination step and not the name a screen reader\n announced for it, which left a control that looked localised and was half of it.\n \n What changed:\n \n - `Pagination` takes `label` for the region name, and each of `PaginationPrevious`\n and `PaginationNext` takes `label` beside its existing `text`, so the visible\n word and the announced name move together.\n - `Dialog` takes `closeLabel` for the built-in close control, so a localised\n dialog no longer announces \"Close\" in a product that never uses that word.\n - `Breadcrumb` takes `label` for its region name.\n - `ProductSwitcher` no longer defaults `label`. A switcher with no `label`\n renders a navigation with no accessible name, which is a real state the caller\n resolves, rather than one that is quietly wrong.\n \n Every new prop keeps the word Prism would have used as its default, so nothing\n changes for a consumer that never set one. The one behaviour that does change is\n `ProductSwitcher`'s, and it is the point: the default was a claim about a\n consumer's product that the prop existed so they would not have to make.\n- b31e444: Ship the consumer gate kit, so a cross-repository law reaches a consumer in one release\n \n The four repositories that consume this package coordinated through a prose\n contract mirrored in four files. It could not be enforced, because prose cannot\n fail, and it had already drifted: two sites that mattered held three\n implementations of one rule and one of the three rules was false.\n \n The laws are now the failure messages of gates in this package, and a consumer's\n repository holds only its own data.\n \n ```sh\n pnpm exec prism-gates # every gate prism-gates.json names\n pnpm exec prism-gates --gate=links # one gate\n pnpm exec prism-gates --json # machine-readable, for a test\n ```\n \n Configure it with a `prism-gates.json` at your repository root. A consumer holds\n its stylesheets, its pack map, its coverage floors, the destinations its own\n corpus gets wrong, and the custom properties its own build supplies. It does not\n hold the wording of a rule, because a wording held in four places is four rules\n that will disagree.\n \n The gates are `pin` (the design system is an exact version, and the token package\n is this package's dependency rather than yours), `retired-line`, `stylesheet-ownership`\n with `token-read`, `links`, `pack-boundary`, `hidden-state` and `runtime-token-read`.\n \n **This release adds the `gates` export subtree and a `prism-gates` binary, and\n adds `gates` to `files`.** It also means a consumer no longer needs to declare\n `@nanisoft/prism-tokens`: resolve it through this package, which requires it at an\n exact version, and the pair cannot be mismatched. Removing the token package from\n a consumer's `package.json` and from its `pnpm-workspace.yaml` is part of the\n adoption; the `pin` gate fails on either.\n \n The kit is outside `dist/` on purpose. It is a build-time program for another\n repository and must never enter a consumer's module graph or its bundle.\n- 86cbc20: Add `LiveRegion`, a region that announces what just changed in it\n \n Prism had no live region at all, so a result list that filtered as you type, a\n search whose count updated, and any stream that appended to the page all changed\n silently for anyone using a screen reader. The fourth Kind the design rules\n already decide, `live`, is specified to own an event log surface, and an event\n log surface is a live region, so this is the prerequisite rather than a\n convenience.\n \n ```tsx\n <LiveRegion busy={streaming} label=\"Run output\">\n {lines.map((line) => (\n <p key={line.id}>{line.text}</p>\n ))}\n </LiveRegion>\n ```\n \n Three decisions are in the Component rather than left to each consumer to make\n the same way:\n \n - **It renders nothing when it has nothing to say.** A live region that is\n always present announces every unrelated state change of its ancestors, so an\n empty region is not rendered at all rather than rendered as an empty element.\n The test is \"renders nothing\" rather than a list of the nothing values, because\n an empty string, a null state and a false condition are three routes to it and a\n list of three is a list a fourth route would miss.\n - **The politeness default is the least interruptive value still announced.** A\n run log that announces assertively interrupts a screen reader mid-sentence, and a\n consumer with a genuinely urgent event passes `politeness=\"assertive\"`.\n - **`aria-busy` is about the caller's knowledge, not about an animation.** A busy\n region is still readable, and a caller that sets it permanently has told\n assistive technology the stream never ends.\n \n The component owns the region and not the transport. A consumer owns the socket,\n the retry, the persistence and the order events arrive in, which is the same split\n the documentation Page makes with its navigation, and it is why Prism stays\n transport-agnostic and no consumer inherits a connection it did not ask for.\n- 86cbc20: Add `Mark`, so a search result can show which characters matched\n \n A command palette and a search result list both need to show why a result\n matched, and until this existed the only way to do that inside Prism was to drop a\n span with hand-written styling into a consumer's own markup, which the authoring\n contract does not permit.\n \n ```tsx\n <Mark text=\"component library\" ranges={[{ start: 0, end: 9 }]} />\n ```\n \n It takes the string and the ranges rather than pre-split nodes, because a caller\n that assembled the nodes had to compute the ranges and this Component has the\n string. The ranges are handled rather than refused: they are sorted, overlapping\n runs are merged, and a range that runs past the end of the string is clamped. Each\n is a thing a search backend does as a matter of course, and a Component that threw\n on any of them would be one a consumer has to wrap in a try, which is a worse\n failure than a highlight that stops at the last character.\n \n **It adds no semantics, and that is the decision rather than an omission.** A\n consumer marking a hit wants a visual difference, not a screen reader announcing\n \"highlighted\" between every character of a result. So it styles a `mark` and puts\n nothing else on it, and a consumer who wants the announcement writes it.\n \n The treatment is the existing warning token pair rather than a new role. A mark\n sits behind the text it marks, so it needs a ground and a foreground, and that\n pair is already measured against every surface in every pack and both modes by the\n token contrast gate. A search hit is emphasis and not a caution, and the two are\n kept apart by spending the existing token rather than by adding a role that means\n \"highlighted\" and will be re-pointed at something else within a year.\n- 86cbc20: Add `Tree`, so a file browser, a category tree and an outline have something to build from\n \n Prism had a working tree and could not reach it. The implementation existed as\n two private renderers inside the documentation Page, with its depth rule, its\n current-item marking and its rule that a group with no index renders a label.\n What was missing was that it was unreachable, and that its data type was bound to\n documentation navigation rather than being a tree's own vocabulary, so a file\n tree, a category tree, an outline and a knowledge base all had nothing to build\n from.\n \n ```tsx\n <Tree\n label=\"Documentation\"\n currentHref=\"/foundation/colors\"\n nodes={[\n { type: 'group', title: 'Foundation', items: [\n { type: 'page', title: 'Colors', href: '/foundation/colors' },\n ] },\n { type: 'divider', title: '' },\n { type: 'page', title: 'Overview', href: '/overview' },\n ]}\n />\n ```\n \n **It is navigable by the arrow keys**, which is the part a consumer cannot\n assemble for themselves. A tree is one Tab stop and the arrows move inside it.\n Composing the markup gives the Tab order of the document instead, which is every\n node in the tree and none of the arrows. The flat order is collected from the DOM\n rather than computed from the node data, so there is no second list to fall behind\n the first, and a label is skipped rather than focused and doing nothing.\n \n **A flat list renders a flat tree.** An outline is a tree of depth one, so a caller\n passes leaves and gets leaves. A Component that required nesting to express a flat\n structure would make the common case the awkward one.\n \n **A group with no index is a label, not a link.** This is the rule the\n documentation Page already reasoned at length and it is carried across rather than\n reinvented: a group with an index is a destination and renders an anchor, a group\n without one renders a span that carries no `href`, is not focusable, and cannot be\n reached by Tab. Inventing a route for it would publish an address that resolves to\n nothing. The empty case is authored too, because a tree that renders nothing is a\n control with nothing in it.\n \n The documentation Page keeps its own renderers and does not change. Its two rules\n are specific to a documentation rail, which is one shape a tree takes rather than\n the shape. Whether the Page later composes this Component is a separate decision\n this does not make.\n\n## 0.7.0\n\n### Minor Changes\n\n- 5c43520: Add `DocsShell`, a documentation page that takes a site's navigation as data\n \n `DocsShell` is a documentation screen: a navigation rail, the document, a\n contents rail, and a pager to the neighbouring pages. It replaces the unstyled\n `docs-shell` the retired line published, and it is the Page the three NaniSoft\n product sites already import by that name.\n \n The navigation is data. A tree of one page and a tree of twenty-seven render the\n same way, at any depth, and the frame is sized by what the caller passed rather\n than by a fixed arrangement, so three sites whose documentation sets differ in\n shape share one Page with no per-site fork.\n \n - `nav` and `toc` are closed unions of `page`, `group` and `divider`. Both are\n read in the order given: nothing is sorted or alphabetised, so a site that\n files its rail as a pipeline and its contents as a reference has both in one\n render.\n - The pager is derived from `nav` and `currentHref`. There is no `neighbours`\n prop, so a neighbour that is not in the tree cannot be expressed, and each\n consumer loses one derivation of its own.\n - A group with no `href` renders a `span` rather than an anchor with no\n destination. The three sites spell that case today as an empty string, and two\n of them carry a stylesheet rule that styles the result back into a label; those\n rules are now deletable.\n - A section carries no status, no badge, no count, no collapse and no sort. A\n status written into a title stays in the title as words.\n - Both rails are bounded against the viewport, because one site's tree is half\n again as tall as another's.\n \n The corpus fix that shipped with it: the props extractor read each declaration\n body twice with two depth counters that disagreed, so a Page whose `index.tsx`\n re-exported a value and a type from one sibling published every prop twice and\n swept the members of its navigation union into its own prop list. It now reads\n each body once.\n\n## 0.6.0\n\n### Minor Changes\n\n- 956fc8f: A call to action with a destination renders a link, and an icon is required only where a tile is\n \n Every primary call to action in the Block family declared an `href` its type then\n never rendered. The control was a `<button>` with no handler: the type said the\n component could navigate, the documentation implied it, and the markup could not\n do either. Sixteen of them across the family, five of them to another origin.\n \n **`CtaLink` is a new Component.** It renders a native `<a>` and carries the\n visual weight of a call to action, so assistive technology announces a link and\n the browser's own affordances all work: a status bar showing the destination, a\n context menu to copy it, middle-click to open a new tab. It is a Component in its\n own right rather than an `as` or `href` prop on `Button`, which is the answer\n `BreadcrumbLink` and `PaginationLink` already give twice in this package. It is a\n server component, so the swap costs no client JavaScript.\n \n `href` is required on `CtaLink`: an anchor without one is not a link. `newTab` is\n a declared prop and the only way to open a new browsing context, because `target`\n is not accepted, so the relationship cannot be opted out of by accident. When it\n is set, `rel` defaults to `noopener noreferrer`; a consumer that needs a\n different relationship passes `rel` and it wins. Prism does not parse the\n destination and does not decide what counts as external.\n \n **`Cta01` no longer offers an action that navigates nothing.** `Cta01Action`'s\n `href` is required, there is no optional arm and no dead-button arm, and a new\n `actionSlot` takes a control the Block does not own, such as a client router's\n link. The Block makes no guess about cross-origin. The one rendered change in\n this release is a button becoming the link its type always said it was: the link\n is styled from the same recipe as the button it replaces, at every shared variant\n and size, and a test asserts that class-for-class.\n \n **`FeatureGrid01`'s icon is required only where a tile is rendered.** The\n requirement is a union discriminated on `variant` rather than an optional prop,\n because an optional prop turns a safe-at-render condition into an unsafe one and\n permits an empty accent tile per feature. The `icon` variant is the one an\n omitted `variant` selects, so the default and the required icon live on the same\n arm; the `bare` variant renders no tile and requires no icon, and the type says\n so rather than the renderer finding out. `IconFeature` and `BareFeature` are\n exported, and the union is the type the site's API table is extracted from.\n \n **The version the four downstream site plans pin is `0.6.0`.** It is\n `@nanisoft/prism-ui@0.6.0`, and `@nanisoft/prism-tokens@0.6.0` with it, because the\n two are linked in `.changeset/config.json` and the pending\n `a-server-rendered-pack-boundary-can-be-mode-correct` changeset already declares a\n minor for both from `0.5.1`. This one declares a minor as well and so does not\n raise it.\n \n Migration: a `Cta01` action needs a destination. Where the action was a label\n alone, give it the `href` it was always declared with, or move the control into\n the new `actionSlot`. A `FeatureGrid01` in the `bare` variant can now drop the\n `icon` its type used to demand; the `icon` variant still demands one per feature.\n- d001983: A diagram is a Component, and a product's mark is one item rather than two copies\n \n Three of the four product sites draw something in the position where an empty box\n is most visible, and all of it was a canvas. A canvas reads computed style from\n one element while a pack is an attribute on an ancestor, so a scoped `data-pack`\n boundary hands it the pack's light values on a dark page, and no token gate can\n see it: the value the drawing holds is a legal token value, applied to the wrong\n pack. Cutting the canvases without a replacement is the alternative, and it\n leaves three heroes with an empty slot.\n \n `Diagram` draws named things and the labelled relations between them as inline\n vector markup. Every stroke and every fill is a semantic utility, never a\n resolved value, so a boundary restyles the drawing through the cascade exactly as\n it restyles a heading. It is a server Component: no hook, no mode, no context, no\n prop spread, and no client code, so a consumer renders it from a server file\n with no provider mounted. Its node mark is a circle and a relation is a path\n because both are shapes with no radius concept, which is what keeps a boundary\n from moving anything but colour; a `<rect>` was rejected because its corner\n attribute is a CSS property and no utility pins it. Every mark names an explicit\n stroke width, an explicit stroke and an explicit fill, because a shape left to\n its defaults either paints opaque black over its own labels or resolves its edge\n to the surface it sits on. A relation naming a node it does not have is dropped,\n and the count of dropped relations is on the element rather than swallowed.\n \n `ProductMark` promotes the two-part mark the product row and the product switcher\n each hand-wrote into one catalogue item. The ring is `brand-ink` and the core is\n `primary`, which is not a preference: a pastel brand value is a fill and never\n text, so the core may be `primary` and the name may not, and a wordmark reads\n `brand-ink` rather than `foreground` or `primary-foreground`. A hand-written copy\n gets one of those two wrong and nothing in the rendered result says which. The\n mark carries its own `data-pack` boundary, so a product's hue follows from the\n pack the caller passed rather than from whichever pack the page is wearing, and\n the boundary sits on an element with no radius utility, which is the placement\n the pack-boundary law allows. A product with no pack of its own is drawn as the\n full spectrum rather than as a colourless mark, built from the five series tokens\n because they are the only five-way colour set the contract publishes.\n \n `scripts/check-vector-ink.mjs` holds both Components to the emitted contract. It\n fails a literal colour, a gradient whose stops are not contract references, a\n `var(--x)` naming a property the contract does not publish, and a paint utility\n whose suffix is not a contract role, which is the last of the four a colour\n regex cannot see because a Tailwind class is a name and not a value. Its scanned\n set is a declared list of two file names rather than a pattern, a Component that\n is not on disk fails the run rather than emptying it, and every exclusion carries\n a reason and is printed on every run, with an exclusion that resolves to nothing\n a finding rather than a line that quietly stops appearing.\n- 4f9ce77: A server-rendered pack boundary can now be mode-correct\n \n The only dark selector for a pack was `[data-pack=\"<id>\"].dark`, which requires\n the mode class on the same element. A server cannot know the reader's mode, so a\n subtree carrying a second pack rendered in that pack's light values on a dark\n page. The token build now emits the two-member list\n \n ```\n [data-pack=\"<id>\"].dark, .dark [data-pack=\"<id>\"]\n ```\n \n as one rule with one declaration block, for all five packs. The compound half is\n retained, because it is what a dark surface on a light page is and it is a\n capability the package already published.\n \n Consumers get a second change alongside it. `themeAttributes` now takes `mode` as\n optional, because a required one is that same impossibility written into the type:\n the only way to ask for a boundary with no mode class of its own was to pass\n `'light'` as a guess, and the guess is wrong for half of readers. Omitting `mode`\n is the honest spelling of \"wear the mode of the element carrying `.dark`\". The\n change is a widening, so every existing caller still compiles.\n \n The theme-boundary law in `DESIGN.md` is rewritten to match. It previously\n instructed an implementer to put `data-pack` and `class=\"dark\"` on a boundary\n together, which works for a client-applied boundary and cannot work for a\n server-rendered one.\n- c126ee3: A stored theme is written when a reader decides, and not before\n \n `PrismProvider` wrote `prism-theme` on every resolved change, including the\n first resolution from the server-rendered attributes or from its own defaults. A\n first-time visitor therefore ended the load with a stored value they never chose,\n and that value outranked the site's default on every later visit, so a site that\n changed its default could not reach anyone who had ever loaded the page.\n \n The write is now gated on a decision. It happens when a caller changes the pack,\n the mode or the toggle, and when the library recovers a decision from\n `prism-theme-mode`, a key earlier lines wrote and this one does not. It does not\n happen on a first resolution from the document or the defaults, so a first visit\n ends with an empty store and a returning visit keeps its choice.\n \n Two readers of one stored value now share one rule. `resolveTheme` in\n `@nanisoft/prism-ui/theming` is the rule, `PrismProvider` calls it, and the boot\n script implements it as a string. The script cannot import the function, because\n the build minifies and a renamed identifier inside its own `try` would throw\n there and fail open, so the two are held together by a build-failing equivalence\n gate that runs the emitted string and the rule over one table of stored values:\n nothing stored, a valid pair, a valid mode with a retired pack, a valid pack with\n a retired mode, garbage, a JSON array, a JSON string, `null`, an empty object, a\n partial, a server-rendered pack and mode, and the retired key. Every fall-through\n is asserted to be whole, which is the divergence this fixes: the old string\n validated the pack and the mode independently, so a stored `{ mode: 'dark' }` with\n no pack half-applied there and fell through whole in the rule.\n \n A value that is present but is not a pair is no longer repaired field by field,\n and it is not cleared. It is the only record that this reader ever chose anything.\n \n The boot script records where the theme came from in `data-theme-origin` on\n `<html>`, and it is the only writer: the provider never touches it, because two\n writers would make the origin a race rather than a record. The value is one of\n \n | Value | Meaning |\n | --- | --- |\n | `stored` | the key held a valid `{ pack, mode }` pair |\n | `legacy` | the key was empty and `prism-theme-mode` held a recoverable mode |\n | `unparsed` | the key held a value that is not a pair, and the theme fell through whole |\n | `document` | the key was empty and the root element's own attributes supplied the theme |\n | `default` | neither did, so the consumer's defaults apply |\n \n It is written on every path the script reaches, so an absent origin is never an\n unread one. It is not `data-theme`, which the migration guide retires; it records\n the provenance of a resolution and never a pack.\n \n The migration clause reads `prism-theme-mode`, puts the recovered decision in\n `prism-theme` and removes the key it read, in one guarded step. That is how it\n expires: it fires only while a retired key still holds a mode, at most once per\n reader, and it is retired for good by deleting the last entry of\n `LEGACY_MODE_STORAGE_KEYS`. There is no date and no version constant in it.\n \n The pre-paint string gets its own ceiling in raw bytes,\n `check-boot-budget.mjs`, because it runs before paint on every page and adding it\n to the per-component client budget would corrupt the unit that table measures. The\n ceiling is derived by addition: the pinned non-migration base, plus the measured\n clause, plus one migration generation.\n- 76c8d96: Fourteen items, four catalogue gates, and a Block that publishes its interface\n \n **Fourteen items ship.** Three Components (`Prose`, `FactList`, `ProductSwitcher`),\n nine Blocks (`ProcessRail01`, `StatusLedger01`, `ProductGrid01`, `StackGrid01`,\n `LogoStrip01`, `NoteGrid01`, `InstrumentPanel01`, `SiteHeader`, `SiteFooter`) and two\n Pages (`NotFoundPage`, `BlogPostPage`). Every one of them earns its place by the\n growth rule rather than by taste: a marketing Block when at least three of the four\n sites compose that section today, a Component when it has one job no existing\n Component can express. The evidence is in the ticket and in each item's own\n JSDoc, which names the sites and the line numbers.\n \n **The three contested shapes are settled as specified, and the specifications\n held.** A process rail admits four steps and refuses five, in the type: `steps` is\n a tuple union, so a five-element array is a compile error and not a fifth column. A\n status ledger has four tiers rather than five, and the words for a tier are the\n caller's: the four sites between them use eight words for those four states\n (`live`, `available` and `complete` are one; `approved`, `specified` and `designed`\n are one; then `planned`; then `direction`), and the vocabulary is what `statusLabel`\n is for. A product row's mark is two parts because a filled dot fails contrast on a\n light card, so the mark is `ProductMark`: a hairline ring in `brand-ink` around a\n core in the product's own `primary` fill, with the name in the brand ink beside it.\n \n **Four gates the catalogue's own rules depend on now exist and are wired into\n `check` and, because they read only `src/`, into `build`.** `check-item-docs`\n holds AGENTS.md's claim that a documentation comment is the only path from an Item\n to its published interface. `check-block-copy` holds DESIGN.md's claim that a\n Block ships no copy and no sample data. `check-item-category` holds CONTEXT.md's\n closed set of seven Categories and the sentence that a Block and a Page have none.\n `check-block-imports` holds the claim that a Block fetches nothing and a Page\n imports no router and no data client. Each resolves its roots from its own\n location, fails a root that resolves to nothing, prints its coverage and every\n exclusion on every run, has no report-only mode, and is proved red by a fixture\n before it is proved green.\n \n **Two of those four gates were red on the tree they were written against, and both\n findings were real.** `check-item-docs` found seven exported functions whose\n documentation comment was not attached to the declaration: `CardHeader`,\n `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `SectionHeading` and\n `Hero01`. A comment three functions above a declaration does not reach the emitted\n `.d.ts`, so those seven were absent from the corpus. `check-block-copy` found\n `pricing-01` rendering the word \"Popular\" on a featured plan that passed no badge:\n a hardcoded claim every consumer of the Block inherited, published by the corpus as\n part of the Block's own documentation. The fallback is gone; a featured plan with\n no badge now renders no badge.\n \n **Five additive fields on four shipped items**, each with three of the four\n consumers passing a value the item could not take, counted per consumer with a\n file and a line and never per string. `SectionHeading.index`, for the mono ordinal\n all four sites render above every section head (www `Landing.tsx:49`, atlas\n `landing.tsx:48`, nexus `Landing.tsx:38`, alphalens `Landing.tsx:66`).\n `Cta01.secondaryAction`, for the pair of actions all four closing bands render\n (www `Landing.tsx:186`, atlas `landing.tsx:243`, nexus `Landing.tsx:221`,\n alphalens `Landing.tsx:285`). `Cta01.note`, for the footnote three of the four\n render under it (atlas `landing.tsx:250`, nexus `Landing.tsx:228`, alphalens\n `Landing.tsx:292`). `FeatureGrid01.numbered`, for the ordinal all four render on\n every card (www `Landing.tsx:166`, atlas `landing.tsx:116`, nexus `Landing.tsx:132`,\n alphalens `Landing.tsx:142`). `Hero01.align`, for the left-aligned hero three of\n the four compose (atlas `landing.tsx:69`, nexus `Landing.tsx:63`, alphalens\n `Landing.tsx:87`). All five are additive: nothing changed shape and nothing was\n removed.\n \n **A Block and a Page publish both an interface section and a composition section.**\n `packages/llms` read the extractor's output for a Block and threw the interface half\n away, so the tool told an agent a Block had no own props. The extractor reads a\n named props type off an emitted declaration whether that declaration belongs to a\n Component or to a Block, so both sections are now built and both are assembled into\n the mirror file, `llms-full.txt` and the Store's `doc`.\n \n The Store now carries both fields for every kind, and `get_item_props` prints\n both when it has both. The sentence \"a Block has no own props\" is gone from the\n tool as well as from the published document. It was the one place the two\n sections were still exclusive, because that tool branched on `item.props` and\n fell back to the composition section, so a Block was answered with its\n composition and a denial of the interface it publishes. The test asserts the\n sentence's absence, so a refactor that restores it fails even if the composition\n is still printed, and a second test covers the one-present case so the\n both-present branch cannot swallow it.\n \n **Four accessible names became required props** on `SiteHeader`, `LogoStrip01`,\n `NotFoundPage` and `BlogPostPage`, and a Block that ships no copy ships no\n reader-facing copy either. `navLabel`, `productsLabel`, `label`, `linksLabel` and\n `trailLabel` are words a screen reader reads out, and no Block should be choosing\n them for a consumer whose site calls its own landmarks something else. This is the\n one breaking change in the release, and it is on four new Blocks and two new Pages\n plus one new required prop on `SiteHeader`, all of which are new in the same\n release, so no existing call site changes.\n \n **`ProductSwitcher.label` has no default** for the same reason: a consumer whose set\n is not a set of products should pass its own word rather than accept a claim about\n what the set is.\n\n### Patch Changes\n\n- 8d24f32: A pack boundary's effect on corner radius is stated, and gated\n \n A scoped `data-pack` boundary resolves the pack's colour AND the pack's corner\n radius beneath it, because radius is the only non-colour member of a pack block\n and the token build emits it the same way. The radius range across the five packs\n is 0.5rem to 1rem, so a row of elements each carrying its own pack shows a spread\n of corner radii, and a small pill becomes a different pill five times over. The\n governing law stated the colour half and was silent on the shape half, so an\n implementer following it exactly produced mismatched corners and called it\n correct.\n \n The law now states both halves and where a boundary may sit. The agent-facing\n surface no longer says the attribute belongs on the root element, which it did\n while listing the radius among the values it will pick up: a tool that shows the\n value and denies the subtree is worse than a tool that is silent.\n \n `check-pack-boundary.mjs` holds the law. It reads the emitted `--radius-*`\n bindings rather than a list of class names, which is the part that matters: a\n scale step like `rounded-xl` is `calc(var(--radius) * 1.4)`, so an element\n carrying one is pinned to a step and the step is pack-relative. The first version\n of the gate treated \"carries a radius utility\" as the safe case and passed\n exactly the defect the ticket is about. The gate also reads the colour contract\n from the token source, so a second per-pack axis the token build has not been\n asked about is a finding rather than a property silently counted as a colour.\n \n One live boundary was found and repaired: the themes page scoped five cards, one\n per pack, each with `rounded-xl`, so the page a reader uses to compare packs was\n comparing shape as well as colour. The cards now carry a literal radius and the\n pack's radius is shown in the row's own label, where it is information rather\n than an accident.\n- feb1384: The catalogue is a set comparison between three lists, not a count\n \n A module on disk with no catalogue entry left every count correct. It shipped,\n `shadcn add` installed it, and `registry.json` listed it, so it was absent from\n the corpus, the documentation site and every agent-facing tool with a fully\n green gate run. The two lists a reader trusts, the catalogue and the registry,\n were on the wrong side of that hole, and the checks that believed they covered it\n compared lengths or compared a file against something derived from itself.\n `validate-registry.mjs` holds the registry against the disk and the manifest and\n never reads the catalogue. `sync-registry.mjs` reads the disk, so it cannot\n disagree with the disk about what is on it. The site's\n `test/catalogue-order.test.ts` holds the catalogue against the generated\n ordering and the documentation tree, and both are built from the catalogue, so a\n disagreement there is impossible by construction.\n \n `scripts/check-catalogue.mjs` reads the three sets independently and compares\n them in both directions, name for name, with one printed line per offending\n item. A length assertion cannot report which item is wrong, so nothing in the\n run reports a length on its own: the three set sizes print as supplementary\n coverage underneath the findings that carry the detail. All six directions a\n three-set comparison implies are implemented, plus the kind of an item in each\n pair of sets, the three naming rules that reconcile the three conventions, and a\n claim that a canonical key is held by only one entry in each set.\n \n The registry generator deliberately keeps reading the disk. Deriving the\n registry from the catalogue is the obvious way to remove the second list, and it\n would delete the disagreement the check exists to see, because the generator\n would then agree with the catalogue by construction.\n \n The version and the roster are two claims on two labelled lines, so a version\n bump and a roster error cannot produce the same output. The run states its\n coverage every time: roots resolved, files walked, files actually read, and how\n many comparisons passed. Roots resolve from the script's own location and never\n from the working directory, and a root that resolves to nothing fails the run\n naming both causes.\n \n `PRISM_CATALOGUE_ROOT` points a run at another package root. It changes which\n directory is read and nothing else, which is what lets the tests run the real\n command over a fixture tree and read the real output. The gate fails on a\n catalogue it cannot read, including an empty one: a roster that cannot be read\n and a roster with nothing in it are different facts, and only one of them is a\n green run.\n- 4744cce: The Slider thumb rings at full strength, and the surface check now reads both ways\n \n Two changes, and only two, in the component package.\n \n **The focus indicator.** The Slider's thumb suppressed the browser outline and\n drew its ring at `ring-ring/50`, so the element a keyboard user lands on carried\n an indicator that composites to between 1.14:1 and 2.74:1 against every surface\n in all six themes. That fails WCAG 1.4.11 on the one indicator a keyboard user\n has, and the Slider's own JSDoc says it is reachable by Tab. Thirteen other\n components already drew that ring at full strength; the Slider was the one that\n was missed. Nothing said so, because the contrast gate measures the `ring`\n TOKEN and the alpha is applied in the component rather than in the token, so it\n reads the Slider's ring as compliant.\n \n `packages/ui/scripts/check-focus-indicators.mjs` is the new gate. It reads the\n class strings on the elements of every shipped Component, derives the set of\n components whose own JSDoc claims keyboard reach or operation, and fails any of\n them that suppresses a focus style without declaring a `ring-ring` colour at\n full alpha alongside a `focus-visible:ring-N` width. The claim is read from the\n JSDoc because `AGENTS.md` names that as this repository's documentation source,\n so a component that stops claiming to be focusable is a documentation change and\n shows up in the table the run prints rather than in a list someone maintains\n beside the code. The unit is a class string on one element, so a Dialog's panel\n or a Tooltip's trigger can suppress the outline legitimately; every such\n exclusion is declared with its reason, printed with the slots it resolved to,\n and fails the run if it stops matching anything. The run fails if the table\n empties, and states how much it read, classified, tabled and excluded.\n \n **The surface check.** `scripts/check-surface.mjs` compared the published\n surface against the source in one direction for two of its three rules. A\n wildcard subpath that resolved to zero declarations used to leave those two\n rules reading no file at all and the run still printed its success line, which is\n how a published surface drifts from its source without a consumer failing first.\n A wildcard target that matches nothing is now a finding naming the target. The\n internal side, which the gate deliberately did not check, is now a declared\n boundary checked in both directions: a new file under `dist/lib` is a finding\n naming it, and a declared internal declaration that is no longer emitted is a\n finding naming it too. The boundary and the per-target match counts are printed\n on every run. The three existing rules and their messages are unchanged.\n \n **The version the four downstream site plans pin is `0.6.0`.** It is\n `@nanisoft/prism-ui@0.6.0`, and `@nanisoft/prism-tokens@0.6.0` with it, because\n the two are linked in `.changeset/config.json` and the pending\n `a-server-rendered-pack-boundary-can-be-mode-correct` changeset declares a minor\n for both from `0.5.1`. The patch declared here does not raise it: changesets\n takes the highest bump in the queue, and a minor is already queued. A site plan\n that pins `0.6.0` therefore resolves every subpath the export map publishes,\n including `./components`, `./blocks`, `./pages`, `./provider`, `./theming`,\n `./catalog` and `./styles.css`.\n \n No public prop, no type and no export changed. The one rendering change is a\n ring alpha on the Slider thumb, in both modes.\n- Updated dependencies [4f9ce77]\n- Updated dependencies [5dcb347]\n - @nanisoft/prism-tokens@0.6.0\n\n## 0.5.1\n\n### Patch Changes\n\n- 2fc6a0a: Retune `muted-foreground` so muted text clears 4.5:1 on the muted surface\n \n The base pack's `muted-foreground` moves from neutral 500 to neutral 600. It\n cleared 4.5:1 on the page background but fell to 4.34:1 on `muted`, where the\n pill, avatar fallback, kbd and tab-list pattern place it. The contrast gate now\n also checks `muted-foreground` on `muted`. The pastel packs were already pinned\n to their neutral 700 and are unchanged.\n- Updated dependencies [2fc6a0a]\n - @nanisoft/prism-tokens@0.5.1\n\n## 0.5.0\n\nPrism was rebuilt from a fresh repository on a clean break. The package is now a\npublished React library a consumer installs and composes, not component source to\ncopy. It ships one precompiled `styles.css`, the optional `PrismProvider` and the\n`data-pack` plus `dark` theme axes, and the Components, Blocks and Pages\ntaxonomy. There is no override path.\n\nChangesets are appended above this entry for every published change.\n"
|
|
8284
|
+
"text": "# @nanisoft/prism-ui\n\n## 1.5.0\n\n### Minor Changes\n\n- Add the third law of motion: arrivals, living fields, and the reading rail\n \n Motion now has three laws. Feedback and the ambient cycles are unchanged; the\n new law admits three authored mechanisms, each shipped as primitives rather\n than opened as a licence:\n \n - `Reveal` wraps a band in a scroll-driven arrival bound to the reader's own\n scroll through `prism-reveal` utilities behind a view-timeline capability\n check. No script arms it, a browser without the timeline ships no rule at\n all, and the universal reduced-motion rule hands that reader the visible\n page.\n - `ParticleField` is the one script-driven figure: a linked, pointer-answering\n medium whose complete still field renders on the server, whose loop stops off\n screen, and whose palette is re-read when the document's pack or mode\n changes. `AuroraWash` is the still counterpart: translucent planes in the\n pack's own colours on the new `aurora` cycle, transform-only.\n - `SiteNavbar` gains a reading rail on its sticky hairline, `scrollRail` on by\n default: a spectrum line filling with the page's own scroll, drawing nothing\n in a browser that cannot run the timeline. The current link's filled pill\n becomes a persistent spectrum underline, and `Hero01` gains a `backdrop` slot\n so a hero composes its own ground.\n \n `Backdrop01` offers `field=\"aurora\"`, and `prism-text-spectrum` is the one\n admitted motion on text: display ink lit at the sweep tempo with foreground\n ends, so it holds contrast in every pack and both modes.\n \n `scripts/check-motion.mjs` holds the new layer: a reveal or rail selector\n outside the `@supports` block naming its timeline fails the build, because an\n animation whose timeline fails to resolve pauses at its invisible keyframe.\n\n### Patch Changes\n\n- Updated dependencies\n - @nanisoft/prism-tokens@1.5.0\n\n## 1.4.0\n\n### Minor Changes\n\n- 3f9cc78: Add `MediaBand01`, an editorial band with a caller-owned media slot, and a `size`\n prop on `Section` for compact, default and tall vertical rhythms.\n \n The band owns the frame, the ratio, the surface and the order, and it never puts\n the copy on top of an arbitrary photograph. The media is the caller's node, so an\n image, a video poster or a composed figure can sit in the frame. The Section size\n keeps the existing default byte-identical and gives a page three authored rhythms\n instead of five site stylesheets inventing their own.\n\n## 1.3.0\n\n### Minor Changes\n\n- 250b34e: SiteFooter takes an `about` slot: a word from the site under the mark, for the\n sentence or two a footer carries about what the site is and who publishes it.\n Rendered under the lockup at the measure with no colour of its own, so the\n muted treatment and any headings stay the caller's. The design system's own\n site composes this footer now, and the two labelled facts its hand-written\n footer carried for years ride in that slot.\n- 290642e: Let `SiteNavbar`'s `nav` carry groups, rendered as one trigger that opens a menu\n \n A `nav` entry may now be a group: `{ label, children }` beside the existing\n link shape. On the row a group renders as the row's one control, a trigger\n whose menu lists its children as real anchors, marked with `aria-current` when\n the reader is on one; below the row's threshold the panel lists the same group\n flat, as a label above its links, so no destination is two disclosures deep.\n The current mark now reads the route as a place rather than a string, so a\n reader on `/components/button` finds `Components` marked rather than nothing.\n A flat `nav` renders exactly as before, and the new `SiteNavGroup` and\n `SiteNavEntry` types are exported beside `SiteNavLink`.\n- 8265662: A `ProductMark` takes a `mark`: the product's own drawing, in place of the\n disc. The node is the consumer's and fills the sized slot the disc occupies,\n so one drawing arrives at every drawn size; the name beside it is unchanged\n and still the announced half. `SiteNavbar` and `SiteFooter` carry it through\n the `product` prop they already take, so a site's logo is the sign in the\n bar, in the mobile panel and at the foot of the page from one fact, and the\n two-colour disc remains the mark of a product whose identity is a hue. The\n design system's own site draws its beam-and-refraction in both places now,\n in place of the spectrum disc.\n\n### Patch Changes\n\n- 290642e: Render `Sparkline`'s hidden numbers table without the `Table` Component's scroll container\n \n The readings behind a `Sparkline` are published to assistive technology as a\n table in a visually hidden wrapper. The wrapper is one pixel wide, so the\n horizontal scroll container `Table` wraps its table in always computed\n scrollable and never focusable, which the `scrollable-region-focusable` rule\n fails on wherever a `Sparkline` renders. The hidden table is now plain `table`\n markup with the same caption, row heads and cells: a table nobody can see\n cannot be panned, and the semantics are the whole of what it owes.\n- 290642e: Lay `Calendar`'s month grid as a table again: the `flex` came off its cells\n \n The weekday headings and the day cells carried `display: flex`, and a flexed\n `th` or `td` is pulled out of the table's column layout: the browser wraps each\n one in its own anonymous cell, and the seven-column month collapsed into a\n single stacked column at four times its height. In `Calendar` the day cell is\n the `td` itself, so the whole grid fell; in `MiniCalendar` the day is a button\n inside the cell, so the weekday row alone stacked above a surviving grid, and\n `DatePicker` and the `Calendar01` Block inherit whichever they compose. The\n cells are centred by the table instead (`text-center` with `align-middle` and\n `p-0`), which keeps the seven columns, the 36px cell, the 44px coarse-pointer\n floor and every state exactly as they were designed, and the weekday heading\n now sits over the column it names.\n- 290642e: Align `PulseSeries`'s scan reticle with the columns it reads\n \n The wash behind the reticle stopped short of the baseline while its own line\n crossed it, and the line rode inside the wash at a third of its width rather\n than at its centre. The wash is now one column slot wide and the line runs\n down its centre, so at every instant the wash covers exactly the column the\n line is crossing, and both shapes meet the baseline. The travel is the plot's\n own width, so the reticle enters and leaves aligned with the columns' field\n and its opacity ramps are spent on the margins beside the end columns.\n\n## 1.2.0\n\n### Minor Changes\n\n- Two new packs: Cyan and Gold\n \n The family's product map asked for two hues the palette did not carry, and a\n product's ground is a fact about the system rather than about the site that\n wears it, so the hues arrive here instead of as consumer overrides.\n \n `cyan` is the Nexus pack: pastel cyan-blue, generated at hue 215 in OKLCH with\n the same perceptually even lightness ramp as the other five, at the crispest\n radius in the set (0.5rem) beside Sky. `gold` is the AlphaLens pack: pastel\n amber-gold at hue 80, at the roomiest radius (1rem) beside Peach. Both carry\n the full shadcn-compatible contract, tinted neutrals at the family's chroma,\n and pass the contrast gate at 38/38 required pairs in both modes.\n \n `PACKS` in the theming module and `STORE_PACKS` in the llms corpus carry the\n two new ids, so a consumer's ground-pack validation and every agent reading\n the corpus see the same vocabulary. Peach returns to reserved. The published\n pack count is now seven pastels plus the neutral default.\n\n### Patch Changes\n\n- Updated dependencies\n - @nanisoft/prism-tokens@1.2.0\n\n## 1.1.0\n\n### Minor Changes\n\n- ad69fa2: A message carries the caller's per-message metadata\n \n `ChatMessage` gains an optional `meta: ReactNode`, drawn in the message's header\n beside the sender and the time. It carries the caller's own per-message metadata:\n the model a provider stated for a turn, a token count it reported, and the cost or\n the latency the caller derived. It is a node and not a set of Prism fields because\n no provider read publishes a cost or a latency, so the surface states no model, no\n count, no cost and no latency for itself and draws only what the caller hands it.\n The parts stay the way content is carried. The roster does not change: no new Item,\n Kind, Page or catalogue entry appears.\n- ad69fa2: The date view takes a per-item move control\n \n `Calendar01` exposes a per-item `handle`, a node the caller fills with their own\n control and wires to whatever owns their schedule. It exists for the one\n interaction a date view has that the Block does not own: a reschedule, which is a\n drag with a drop target, a new order, a write and a conflict, every one of them the\n consumer's. The Block places the node at the leading edge of the item's row and\n owns nothing beyond it: no drag, no drop, no order and no move callback, which is\n the arrangement `Kanban01` already takes for a board's drag.\n \n A `CalendarItem` gains an optional `handle: ReactNode`; `Calendar01`'s props are\n otherwise unchanged. The roster does not change: no new Item, Kind, Page or\n catalogue entry appears.\n- ad69fa2: The index gains a picture arrangement and a write grid\n \n Two coverage gaps in the record-index territory are closed by authoring two Blocks\n rather than by widening a union, and each is the arrangement `DESIGN.md` already\n named as forcing a second Item.\n \n **`CardIndex01` is the record index as pictures.** It draws a set of records as a\n grid of cards rather than as rows, with the caller composing each card's picture,\n name and price through `renderCard` and its per card actions through\n `renderCardActions`. It shares the record index's selection and announcement\n contract, which ticket 166 made the law for the index family: the same `selectable`\n union, the same held set of keys, the same three-state header control, the same\n single polite live region carrying a count, the same batch bar around the caller's\n own `batchActions` node, and the same `selectionScope` distinguishing a page-wide\n selection from a filter-wide one. It fetches, sorts, filters and slices nothing.\n \n **`RecordGrid01` is a grid edited in place.** Its columns are the shared\n `FieldSpec`, because a cell that writes is a control and the only vocabulary this\n package publishes for a control is the field specification; `ColumnKind` is the\n vocabulary for a value a reader reads and is held disjoint from it. The Block seeds\n each cell from the caller's row, holds the in-progress values so a cell redraws as\n it is typed, and reports every edit through a required `onCellChange` as the row\n identity and the column key. It fetches nothing, sorts nothing and pages nothing,\n and it owns no command.\n \n **Why `ColumnKind` was not widened.** The obvious alternative was an editable cell\n arm on `ColumnKind` plus a change callback. That arm would name a field Component\n (`Input`, `Select` and their kin), which is a member of `FieldKind`, and\n `scripts/check-spec-unions.mjs` reports it as an `overlapping-union` between the\n read union and the write union, in the same run that would otherwise pass. The\n disjointness the gate protects is exactly the fact that a cell that writes is a\n different job from a cell that reads, so the answer is a new Item that takes the\n field union rather than a wider column union.\n- ad69fa2: The record detail draws a dated trail as one of its relations\n \n `RelationKind` gains the `ActivityFeed01` arm, so a record's event history is a\n first-class relation drawn over `EventSpec` values rather than the generic `slot`\n arm. The union previously offered only `Timeline`, whose `TimelineEntry` carries a\n `duration` and a `state` and no `at`, so a record's dated occurrences had no\n relation to draw them. A `Timeline` is an instrument and a trail is not, so the fix\n is a new arm and not a moment on `TimelineEntry`: one Component carrying both a\n moment and a duration would be a second answer to a question that already has one.\n \n `RecordDetail01` draws the arm with the shared `ActivityFeed01` Block, exactly as\n it draws the other arms, and its relations are unchanged otherwise. The roster does\n not change: this widens an existing union and an existing Block, and no new Item,\n Kind, Page or catalogue entry appears.\n\n## 1.0.0\n\n### Major Changes\n\n- dbc8a65: Every superseded figure declaration is deleted\n \n The five local declarations of a reading over a population are gone from the\n published interface, and `MetricSpec` from `@nanisoft/prism-ui/spec` is the one\n shape a figure is declared with in this package. The expand step made each of the\n five an alias of the shared type so an old name resolved while its callers moved;\n this is the contract step, and it deletes every one of them now that a search of\n the whole tree, the documentation and the corpus finds no caller left. Deleted:\n \n - `Dashboard01Metric`, from `@nanisoft/prism-ui/blocks/dashboard-01`.\n - `ProjectDashboard01Figure`, from `@nanisoft/prism-ui/blocks/project-dashboard-01`.\n - `ChartCard01Reading`, from `@nanisoft/prism-ui/blocks/chart-card-01`.\n - `Trend01Item`, from `@nanisoft/prism-ui/blocks/trend-01`.\n - `Stat`, from `@nanisoft/prism-ui/blocks/stats-01`.\n \n `Dashboard01`, `ProjectDashboard01`, `ChartCard01`, `Trend01` and `Stats01` keep\n their own names, kinds and arrangements; only the shape is shared, because a shape\n is shared and a screen is not. This is the close of the metric expand-contract\n sequence, and the count of declarations describing a reading over a population in\n this package is one.\n \n The process stage shape, `ProvisioningStep`, stays declared with `Provisioning01`\n and is not migrated. It is the inside of one Item rather than a shape two Items\n would each declare, and a declared stage has not happened, so it is not migrated\n onto the event specification either.\n \n # Migration\n \n - Replace every use of `Dashboard01Metric`, `ProjectDashboard01Figure`,\n `ChartCard01Reading`, `Trend01Item` and `Stat` with `MetricSpec`, imported from\n `@nanisoft/prism-ui/spec`.\n - The shape is unchanged, so the migration is a rename of the type reference. A\n figure or row still carries `key`, `label` and `value`, and the optional `delta`,\n `deltaFormat`, `hint`, `series` with `seriesLabel`, and `href` with `hrefLabel`.\n - A build that reports one of the five names as missing is reporting a name that\n was deleted here rather than an exported entry that moved.\n- e9e73d6: Four forms take the shared field specification\n \n `AuthForm01`, `Contact01`, `Provisioning01` and `Signup01` now take the same\n `FieldSpec` and `FieldSpecGroup` a record write form takes, so the vocabulary a\n consumer learns on one screen is the vocabulary on all five. Each Block's own\n field declaration goes from the published interface, and a control Prism does not\n ship arrives as the `slot` arm of `FieldKind` rather than as a property on a\n per-Block type.\n \n - **`AuthForm01`**: `fields: AuthFormField[]` becomes `groups: readonly\n FieldSpecGroup[]`. The controlled `value` and `onChange` on a field go, because\n the specification is data and holds no callback; the Block is uncontrolled and\n the caller reads the values out of the form element. `error` becomes\n `submitError`, and `errorId` becomes an entry in the new `issues` list keyed by\n field. `AuthFormField` is removed and `AuthForm01Issue` is added.\n - **`Contact01`**: `fields: ContactField[]` becomes `groups: readonly\n FieldSpecGroup[]`, and a new `issues` list draws a message under a field.\n `ContactField` and `Contact01Option` are removed and `Contact01Issue` is added.\n - **`Provisioning01`**: a step's `fields: readonly ProvisioningField[]` becomes\n `fields: readonly FieldSpec[]`. `ProvisioningField`, `ProvisioningFieldType` and\n `ProvisioningOption` are removed; `ProvisioningStep`, `ProvisioningValue` and\n `Provisioning01Status` are unchanged.\n - **`Signup01`**: `fields: readonly SignupField[]` becomes `groups: readonly\n FieldSpecGroup[]`. `SignupField` and `SignupOption` are removed; the `password`\n arrangement, `SignupValue`, `Signup01Password`, `Signup01Reading`,\n `Signup01Status` and `Signup01SignIn` are unchanged.\n \n `FieldCommon` gains an optional `autoComplete`, the platform's autofill hint, once\n for every screen rather than in a second declaration per Block. Requiredness stays\n a rendering fact and an HTML attribute; no specification carries a rule.\n \n # Migration\n \n Replace a field list with groups of `FieldSpec` from `@nanisoft/prism-ui/spec`:\n \n - Rename a field's `id` to `key`, its `type` to a `kind` naming a Prism control\n (`'text'`, `'email'` and `'tel'` become `'Input'`, `'password'` becomes\n `'PasswordField'`, `'textarea'` becomes `'Textarea'`, `'select'` becomes\n `'NativeSelect'`), and its `description` to `help`.\n - Move a choice's `options` under its kind, unchanged in shape.\n - Wrap the list in one or more `{ fields: [...] }` groups and pass it as `groups`.\n - Pass a control Prism does not ship as `{ kind: 'slot', control: <your control /> }`.\n - On `AuthForm01`, replace `error` with `submitError` and, where you marked a\n field, pass `issues={[{ field: key, message }]}` instead of `errorId`.\n - Read `onSubmit` values by each field's `key`. `AuthForm01` is uncontrolled: read\n `new FormData(event.currentTarget)` rather than a controlled `value`/`onChange`.\n - Pass `autoComplete` on the fields a password manager or a phone keyboard fills.\n- dbc8a65: The activity trail takes the shared event specification\n \n `ActivityFeed01.events` is now an `EventSpec[]` from `@nanisoft/prism-ui/spec`\n rather than the Block's own `ActivityFeed01Event`, and that local type is gone from\n the published interface. The trail is the activity feed Block widened, so no new\n Item appears beside it: a shipment's chain of events, an issue's whole history and a\n transaction's trail are record details, and the trail's own entry declaration is the\n one declaration of a dated attributed occurrence every Block in this package takes.\n \n An entry carries a stable `key` that is never the words of a label, a moment printed\n exactly as the caller passed it with the machine value on the element, a required\n `actor` and `action`, an optional `target`, an optional `detail` node, a `tone` whose\n `toneLabel` is required wherever the tone is set, and a destination whose words are\n required wherever it is set.\n \n The specification names no vocabulary of what happened: there is no `kind`, because\n a closed union is legitimate only when its members are Components this package\n ships, and a caller needing one puts the word in `action` or composes a status into\n `detail`. Immutability is a rendering fact and nothing more, so there is no\n `immutable` prop, no seal glyph and no verification mark. A trail draws a moment and\n never a length, so there is no scale, no range and no axis, and the Block owns no\n retention window, archive, purge, legal hold, export or as-of clock.\n \n # Migration\n \n - Replace `ActivityFeed01Event` with `EventSpec`, imported from\n `@nanisoft/prism-ui/spec`, on the `events` prop.\n - Rename each entry's `id` to `key`.\n - Move the tone's label to `toneLabel` wherever `tone` is set.\n - Remove any `kind`, `immutable`, seal, verification, scale, range, axis or\n retention member: the specification does not carry them and does not accept them.\n Put a happened- word in `action`, and compose a status into `detail`.\n- dbc8a65: The settings panel takes the shared field specification\n \n `SettingsPanel01` now takes the same `FieldSpec`, `FieldSpecGroup` and `FieldKind`\n a record write form takes, and its own field declaration, `SettingsPanelField`, is\n gone from the published interface. The archetype is answered by the record write\n form Block and earns no Item of its own: the difference between a form and a setting\n is about what sits behind the surface, not about the surface.\n \n Two things change in the props, and the second is breaking.\n \n - `sections` is now `groups`, and a field is a `FieldSpec` with a stable `key`, a\n required `label` and a required `kind`. The `slot` arm means a control this\n package does not ship has somewhere to go, and a field's shell stays Prism's\n either way.\n - `secondaryAction` is gone. That position is now `footerStart`, a node the caller\n wrote, placed without styling. A caller who passed a label and no function could\n previously produce a focusable control that activated to nothing; a `CtaLink` with\n its required `href` cannot.\n \n `submitLabel` is now one arm of a save union: `action`, `onSubmit` or `submit`, each\n forbidding the others, exactly as the write form takes them. A region that applies\n on change passes `values` and an `onValueChange` that applies the mutation, and no\n save arm at all, and the Block draws no footer. Whether a setting leaves on change,\n on blur or at a save is the caller's own handler and the caller's own submission\n arm, and no prop here decides it.\n \n The Block renders no revert control, no reset to defaults control and no storage of\n any kind, and it draws no tab strip and no rail.\n \n # Migration\n \n - Rename the `sections` prop to `groups`, and pass each field as a `FieldSpec` with\n a stable `key`, a required `label` and a required `kind`, imported from\n `@nanisoft/prism-ui/spec`. A control this package does not ship goes in the\n `slot` arm.\n - Replace `secondaryAction` with `footerStart`, a node you write, placed without\n styling. A label with no function no longer compiles into a control that does\n nothing.\n - Replace `submitLabel` with one arm of the save union: `action` for a destination,\n `onSubmit` for a handler, or `submit` for a node you write. The three forbid one\n another. A region that applies on change passes `values` and `onValueChange` and\n no save arm, and draws no footer.\n - Remove any use of `SettingsPanelField`; it is gone from the published interface.\n\n### Minor Changes\n\n- f11799a: Add `emptied-by-reader` to `EMPTY_REASONS`, for a region the reader emptied\n \n `EmptyState01` takes a fourth reason beside the three it takes today: a region\n whose records were there and that this reader's own earlier action moved every one\n of them out of. A bin, an archive or a recycle view a reader has just cleared is\n the case it exists for.\n \n **A member on a published union, not a new Block and not a catalogue row.** The\n existing `EmptyReason` type gains its fourth member and the changeset says so\n because a consumer's own code that enumerates the union now has one more case to\n handle. `EMPTY_REASONS` is published through\n `@nanisoft/prism-ui/blocks/empty-state-01` beside the Block, and `catalog.ts`\n holds the same number of rows it held before, because a union member is a value on\n an existing exported type rather than an Item: it has no slug, no page, no Demo\n and no corpus entry of its own.\n \n **It is a fourth reason rather than a fourth Block because it is a statement about\n a collection, which is what the other three are.** Each of them answers what\n happened to the set the region would have drawn, so the frame they share stays\n true of all four: there is nothing here to read, and the frame makes no claim\n about why. A bin the reader emptied has nothing readable beside it, so the shared\n frame is honest there. That is the whole difference from `NothingChosen01`, which\n is a statement about a reader's pointer at a record and whose records are all\n still there: it draws no frame, it is a separate Block, and the two arguments\n argue the same line from opposite sides.\n \n **Two of the four are the reader's own doing and they are not the same doing.**\n `no-match` hides rows out of a set that is still there, so clearing it brings them\n back. `emptied-by-reader` is a record gone from the live set rather than hidden in\n it, and the reader put it there. Every saved view a caller builds out of the filter\n values it holds is already `no-match` whenever it comes back empty; a trash view\n is the one that differs in kind, because it fetches a different collection rather\n than narrowing this one. So the two members differ in exactly the fact a reader\n must be told honestly, and one sentence for both would tell a reader who has\n deleted everything that their own search found nothing. Both are named in the\n JSDoc on the union, which the emitted declarations preserve and the corpus reads.\n \n **It renders no control, and the Block already had that arm.** The reason takes\n the caller's `title` and an optional `body` and nothing else, because a reader who\n has deleted nothing has nothing to restore and the records a restore would bring\n back are the caller's own nodes in the index around this region. `EmptyState01`\n has never rendered an action without an `actionLabel`, so the member adds no\n render branch, no prop and no gate exemption: `scripts/check-block-controls.mjs`\n is green on the source exactly as it was, and the one control in it is still the\n `Button` behind that label.\n \n It carries no ink either, for the reason the Block gives: nothing is broken,\n nothing is missing, and nobody is blocked. `REASON_INK` is unchanged, so the only\n reason drawn in `muted-foreground` is still the permission boundary.\n- e9e73d6: Four more figure owners take the shared metric specification\n \n `ChartCard01`, `ProjectDashboard01`, `Stats01` and `Trend01` now take the same\n `MetricSpec` the metric summary takes, so a figure a consumer declares on one of\n these surfaces is the same figure it declares on the others. This is the migrate\n step of a breaking consolidation: five shapes are declared across this package\n for a reading over a population, and they disagree about whether a delta is a\n number or a percentage, about `deltaFormat`, about `hint`, and about whether a\n series and a destination are part of a figure at all. This release moves the four\n remaining owners onto `MetricSpec`, so one shape is left in use. Their superseded\n local declarations stay exported as deprecated aliases until the contract ticket\n deletes them, so an existing name continues to resolve.\n \n **The shape every one of the four now takes.** A stable `key` that is never the\n words of the label, a required `label` (a `ReactNode`) and `value` (the caller's\n own node), a `delta` whose sign is the direction, an optional `deltaFormat` (the\n caller's own words for the change, a `ReactNode` rather than a callback), an\n optional `hint` for the period or the caveat, and an optional `series` with its\n `seriesLabel` and an optional `href` with its `hrefLabel`. The two pairs are held\n by the type: a series set without its name, or a destination set without its\n words, does not compile.\n \n **What changes per Block, and the arm that replaces each prop that goes.**\n \n - **`ChartCard01`**: `ChartCard01Reading` becomes an alias of `MetricSpec`. A\n reading's `deltaFormat` is now the caller's own words rather than a `(value:\n number) => string` callback, so pass a node, and `key` is now required. The\n reading line draws the series and the destination too.\n - **`ProjectDashboard01`**: `ProjectDashboard01Figure` becomes an alias of\n `MetricSpec`, `key` is now required, and a figure can now carry `deltaFormat`,\n `hint`, `series` and `href`. The bare `0.12` a delta without a formatter used to\n print is now a decision rather than a drift.\n - **`Stats01`**: `Stat` becomes an alias of `MetricSpec`, `key` is now required,\n and a delta no longer has a percent sign appended to it. A delta with no\n formatter prints the number the caller passed, and a delta with a formatter\n prints the caller's own words.\n - **`Trend01`**: `Trend01Item` becomes an alias of `MetricSpec`, a row's `id`\n becomes `key`, and `deltaLabel` becomes the `deltaFormat` node. A row can now\n carry a series and a destination, and a delta with no formatter prints the\n number rather than making the Block throw.\n \n **What is kept, and what is still not owned.** Each Block keeps its own name,\n kind and arrangement. `ChartGroup01`, the page fixtures and the site landing page\n were updated at their call sites, because this package resolves the shared type\n through the component package. No Block performs aggregation, derives a rate of\n change, owns a period, a unit, a threshold, a target, a severity or a freshness\n state, and none draws a period switcher, a comparison toggle or an export. A\n metric's period stays the caller's own words, carried by `deltaFormat` and `hint`.\n \n `@nanisoft/prism-ui/spec` is the type to import: `MetricSpec`, from\n `@nanisoft/prism-ui/spec`, replaces `ChartCard01Reading`, `ProjectDashboard01Figure`,\n `Stat` and `Trend01Item` at every call site.\n- f11799a: Add `@nanisoft/prism-ui/spec`: one shared specification module, and the gate for it\n \n This adds published surface and changes no existing one. Nothing you compose\n differs today; what changes is that five screens worth of declared content now\n have one declaration each instead of several that disagree, and that the\n declarations are held by a gate rather than by review.\n \n **The five specifications.** `FieldSpec` and `FieldSpecGroup` describe the fields\n a write form, a wizard or a settings region renders, in the order the array is\n written. `ColumnSpec` describes a column of a record index. `RelationSpec`\n describes one relationship of a record, one ring deep. `MetricSpec` describes a\n figure taken over a population. `EventSpec` describes one dated, attributed\n occurrence. Every one of them is plain serialisable data: no validator, no\n constraint object, no schema, no pattern, no callback. That is what lets an agent\n read one out of a document, and it is why validation is yours entirely.\n \n **Three closed unions, and the one arm that is an escape rather than a catch-all.**\n `FieldKind` names the input Components this package ships, `ColumnKind` the\n reading Components, `RelationKind` the collection arrangements already drawn. Each\n carries a `slot` member for the control, cell or arrangement you draw yourself, and\n Prism still draws the shell around it: the label, the help, the error, the header,\n the alignment, the frame. A `props` bag would have been the alternative and would\n have put the hole back.\n \n **What is deliberately absent, so that you are not waiting for it.** A\n specification carries no rule and no validator, because a Block ships no behaviour\n and evaluating a rule is behaviour; `required` stays, and it is the mark and the\n HTML attribute rather than a check. A metric carries no period, because a\n comparison needs a period and a billing screen's month is not a trading screen's\n week: the words for it go in `deltaFormat` and `hint`, in your own units. An event\n carries no `kind`, no immutability, no retention window and no time axis, because\n no enumeration of what your product calls a thing that happened is ours to publish\n and because a drawing has no authority over the store behind it. A relation holds\n members and never another relation; a descendant is reached by a link you pass.\n \n **The gate.** `packages/ui/scripts/check-spec-unions.mjs` reads the emitted\n declarations rather than the source, because that is the seam your editor\n resolves. It fails when a union member names something this package does not\n ship, when two of the three unions claim the same member, when a specification\n grows a validator, a rule, a schema, a pattern, an immutability claim, a retention\n window or a time axis, when an event grows a `kind`, and when a relation holds a\n relation. Before it existed, every one of those was accepted by every gate in the\n repository, because a specification type is not an Item and no catalogue gate reads\n the module at all.\n \n **One thing it does not hold, so you know what a green run means.** It does not\n hold that there is one shape per concept: a second declaration of the field, column,\n relation, metric or event shape would be named differently from the first, and the\n rule that would catch it would be a list somebody maintains. That is held by review.\n \n **The surface gate's internal boundary moved with it, in one direction only.**\n `packages/ui/src/lib/` is where the internal helpers live, but the `exports` map is\n what decides: a declaration under it that a published entry reaches is public\n surface and the surface gate reads it, and one that no entry reaches is still\n internal and still has to be declared. `dist/lib/spec.d.ts` is the first such file.\n Nothing under `dist/lib` was declared internal and then undeclared, and no published\n subpath was removed.\n- caee732: `Directory01` gains a per-member node\n \n `Directory01` takes a new `renderMemberAction` prop: a function from a member to a\n `ReactNode` the Block places at the member's trailing edge, in both the card and the\n row arrangements, and draws nothing of its own where it is not passed.\n \n **The control and its handler are the caller's, and that is why it is a node.** A\n directory is a browse, so a plugin or model row's enable is a property of a member\n rather than a command on a selection, and `scripts/check-block-controls.mjs`\n classifies `Switch` and fails a Block that renders one without its handler. A Block\n ships no behaviour and so cannot supply one, so the enable arrives as the caller's\n own node, in the shape `DataTable01`'s `renderRowActions` already takes.\n \n No new Item and no new catalogue entry: the change is a prop on an Item that ships,\n with its JSDoc, its Demo and its test.\n- caee732: `DocumentPage`, the document and research workspace\n \n A new Page arrives at `@nanisoft/prism-ui/pages/document-page`. It draws one\n document read in full, its body held to the reading measure, and one or more\n regions beside it.\n \n **It is authored beside `DocsShell`, and neither of that Page's two rules is\n relaxed.** A section stays a label and not a control, and a group with no index\n stays a label and not a route, because both are statements about a documentation\n site's navigation tree and this workspace never asks that question. A status\n belongs to a region of one document, and a route is the document's own contents.\n No consumer of `DocsShell` is affected.\n \n **One Page draws all seven screens.** The document is `RecordDetail01` and the\n body is `Prose`; a version list is an `activity` region, a source or output list\n is a `table` region, a comment thread is a `members` or a `table` region, and the\n evidence comparison matrix is a `comparison` region drawn by `Compare01`. The\n region union is by arrangement and not by subject matter, so the Page mints no\n document, version, comment or source type and ships no sentence. The editor, the\n fetch, the retention, the share, the export and the permission are the consumer's.\n \n The Page ships with a catalogue entry, a JSDoc block, an Item page and a Demo, a\n corpus and store entry, a composition test and an accessibility test over its\n heading structure and named regions.\n- f11799a: Add `NothingChosen01`, the resting state of a detail pane\n \n `NothingChosen01` draws a region that can hold a record and has none chosen in it\n yet: the caller's words, an optional sentence under them, and an optional mark of\n the caller's choosing. It is a new Block beside `EmptyState01` rather than a fourth\n reason on it, and the difference is the shape of the claim rather than a matter of\n taste.\n \n The three reasons `EmptyState01` takes are all statements about a collection:\n nothing ever existed in it, the reader's own filter emptied it, or this reader may\n not see it. The dashed frame and the height floor those three share assert\n something that follows from all three together: there is nothing here to read.\n Nothing chosen is not that. No collection has been emptied, filtered or hidden, the\n records are in the index a few centimetres away, and what is missing is the\n reader's pointer at one of them. A fourth arm would have had to keep that frame\n honest while a readable region sat next to it, which is the failure the separate\n Block exists rather than to commit.\n \n So the Block ships no reason value, because one cause is not a choice a caller\n makes; no default sentence and no fallback, because the words are entirely yours;\n no frame, for the reason above; and no action. That last one is not an omission. A\n Block ships no behaviour, so a control here would be either one this Block cannot\n wire or a second copy of an affordance the index beside it already draws for the\n same reader, and `scripts/check-block-controls.mjs` has nothing to catch in a\n source that contains no control.\n \n It also declares no height floor. A pane in a grid is already as tall as its\n neighbour, so a `min-h` authored here would be a second answer to a question the\n split's tracks have already answered. What the Block does is fill the height it is\n given and centre itself in it.\n \n Two things it inherits rather than re-derives: the icon is a slot drawn\n `aria-hidden`, because a mark a screen reader reads is a second announcement of\n something the words already say, and the title is a `<p>` rather than a heading,\n because the pane outlives its contents and an outline entry that vanishes on the\n first click is an entry a reader navigating by heading cannot follow.\n \n The split's tracks hold their ratio at every selection state, so the list never\n moves out from under the hand that clicked it. What changes is content, and a\n caller who wants the index louder makes the index denser, which is the index's own\n decision to take.\n- dbc8a65: The issue detail Block is deprecated in favour of the record detail Block\n \n `IssueDetail01` is deprecated, not merged and not deleted. It is the same job as\n `RecordDetail01` under a name that is wrong the moment the record is an invoice, an\n order or a subscription, and a name that has to be wrong about nine tenths of its\n uses is a name that fails the test that its name survives being wrong. The Item\n still renders and is still supported for now, but nothing new should be started on\n it, and a consumer composing a record detail today composes `RecordDetail01`.\n \n **The published interface is unchanged.** This release marks the Item and changes\n its JSDoc and its documentation; no export is removed, renamed or re-shaped, so a\n consumer already composing `IssueDetail01` keeps composing it and can read the\n release without moving. `IssueDetail01` and its four types stay exported from\n `@nanisoft/prism-ui/blocks/issue-detail-01`. Its own field declaration stays with\n it and is not migrated onto the shared field specification, because it is a label\n and a value node a reader is entitled to be told about a record rather than an\n input.\n \n The catalogue entry carries the `deprecated` status, so the Item page shows a\n deprecated mark, and the Item page and the record detail Block's documentation each\n name the other, so a reader arriving at either arrives at the answer.\n- dbc8a65: Add `IndexDetail01`, the master and detail split\n \n `IndexDetail01` is a composition Block that renders an index pane beside a detail\n pane and holds no selection state at all. Selection, routing and which record is\n open belong to the consumer, because Prism never imports a router. Nothing in the\n Block connects the two panes: the index reports every selection change through its\n own callback, and reacting to it is the caller's own read of that callback and a\n pass of the record it wants into `detail`.\n \n The tracks hold their ratio at every selection state, so clicking a row does not\n move the list out from under the hand that clicked it. The pane before anything is\n selected holds what the caller placed, or nothing at all, so the Block ships no\n fallback sentence, no frame of its own and no loading mark. The detail slot is a\n required node, because a record that is gone, one this reader may not see and one\n whose fetch failed are three causes sharing one address and three different\n sentences.\n \n The split does not collapse at a narrow viewport. Which pane survives is the\n caller's composition rather than a media query deciding a shape, so the narrow\n arrangement is one the caller writes. The Block renders no control of its own.\n \n This change also corrects the record index section of the design document, which\n claimed the split was already authored while the roster counted from source held no\n such Item.\n- caee732: `MessageThread01`, the live Kind's second family\n \n A new `live` Item, the message-thread surface, arrives at\n `@nanisoft/prism-ui/live/message-thread-01`. It renders a conversation whose\n messages arrive over a `subscribe` function the consumer supplies and it opens no\n connection of its own, so a WebSocket, an `EventSource`, a polling timer and a test\n array are all the same shape to it.\n \n **A message list with ordered parts, and not an event list or a turn list.** A\n message has a stable identity, a sender and an ordered list of parts, and a part is\n text, a tool call, a reasoning block, a citation or an attachment. The message and\n part vocabulary lives with the surface, beside `RunEvent` and `ToolCall`, and is\n deliberately outside the shared specification module, so no sixth specification\n member and no `CONTEXT.md` word is added.\n \n **It reuses what the Kind already names rather than minting a second answer.** A\n tool-call part is drawn through `ToolLedger01`'s own row, so there is no second\n tool-call shape. The status tiers are `RunStatus`, published once and shared by both\n live surfaces. The run controls are `PromptComposer`'s send, stop, retry and attach,\n composed beside the thread rather than a second set drawn inside it. The one surface\n it does not reuse is `RunStream01`, because a run's log is a sequence of occurrences\n and a conversation is a sequence of messages.\n \n **Prism holds a bounded in-memory window and promises no resumption.** The durable\n resumption buffer that outlives a mount is the consumer's, through the same `initial`\n and `resubscribeKey` seam `RunStream01` already documents: a surface opened\n mid-thread is seeded from the consumer's own record, and a reconnect is a new key\n over a fresh seed. The surface sorts by `at` with a tie on `id`, bounds its window,\n announces a growing list once rather than reading it whole on every arrival, and\n holds no control it cannot act on.\n \n The Item ships with a catalogue entry, a JSDoc block, an Item page and a Demo, a\n corpus and store entry, and a boundary test in the shape of the `RunStream01` one.\n- 1079d02: The metric summary Block takes the shared metric specification\n \n `Dashboard01.metrics` is now a `MetricSpec[]` from `@nanisoft/prism-ui/spec`\n rather than the Block's own figure shape. This is the first step of a breaking\n consolidation: five metric shapes are declared across this package today and they\n already disagree about whether a delta is a number or a percentage, about\n `deltaFormat`, about `hint` and about whether a series and its name are part of a\n figure at all. `MetricSpec` is the one shape, and this release makes the metric\n summary Block the first surface to take it. The other four owners are migrated in\n later batches, so this is one Block wider rather than five replaced at once.\n \n **What a figure is now.** Every entry is a `MetricSpec`: a stable `key` that is\n never the words of the label, a required `label` and `value`, a `delta` whose sign\n is the direction, an optional `deltaFormat` carrying the caller's own words for\n that change, an optional `hint` for the period or the caveat, and an optional\n `series` with its `seriesLabel` and an optional `href` with its `hrefLabel`. The\n two pairs are held by the type: a series set without its name, or a destination set\n without its words, does not compile.\n \n **What changes for a consumer.** `label` is now a `ReactNode`, so a caller that\n passed a string passes the same string and a caller that passed a node keeps it.\n `deltaFormat` is the caller's own words rather than a `(value: number) => string`\n callback, so `deltaFormat: (v) => String(Math.abs(v))` becomes\n `deltaFormat: '3 fewer than yesterday'` and the direction mark still comes from the\n sign of `delta`. `sparkline` and `sparklineLabel` are now `series` and\n `seriesLabel`, and `key` is now required. `Metric` takes the words as a node when\n the Block hands them over, so a delta with a formatter prints the caller's words\n and a delta without one prints the number the caller passed.\n \n **`Dashboard01Metric` is kept as an alias** of `MetricSpec` so an existing name\n resolves while the other four figure owners are migrated. It is a synonym on its\n way out, not a second shape, and a later contract release removes it alongside the\n superseded `ProjectDashboard01Figure`, `ChartCard01Reading`, `Trend01Item` and\n `Stat`.\n \n **Nothing else is owned.** The Block still performs no aggregation, owns no period,\n no unit, no threshold, no target, no severity and no freshness, and draws no period\n switcher, no comparison toggle and no export.\n- dbc8a65: Add `RecordWizard01`, the multi-step record write form\n \n `RecordWizard01` is the write form's second arrangement rather than a prop on the\n plain form. A wizard draws one group of fields at a time above a rail and asks a\n decision the plain form never asks, which is whether a step may be left forwards, so\n it earns its own name: it is a property of how many fields a record has, not one of\n two renderings a consumer picks between on purpose.\n \n The step is the consumer's. `current` is controlled and `onStepChange` is required,\n and the Block draws the step it was handed and no reachability at all. The\n specification is per step, and a step is a `FieldSpecGroup` from\n `@nanisoft/prism-ui/spec` with a stable `id` and a string `label`, so nothing new is\n declared in order to hold the steps and a group's heading and its rail label are one\n word. One specification sliced by a caller grouping is refused.\n \n Every field on the stepped arm is controlled and carries the consumer's value and\n change handler, so the values are the consumer's from the first keystroke. Back, the\n forward control and the rail are `FormWizard`'s, and this Block renders none of\n them, declares no submission arm of its own, and leaves an issue naming a key that\n is not on the step being drawn undrawn without moving the reader.\n- 1079d02: The record detail draws one record and its relationships\n \n `RecordDetail01` is a new Block: one record in full, its relationships drawn open\n as one ordered list, and its own actions beside its identity. It is a Block rather\n than a Page because the frame and the claim belong to a caller and a Page owns no\n URL, so a detail Page would have to own the selection the consumer holds. It is\n authored beside `Summary01` and `QuickView01` rather than by widening either,\n because a summary exists to add up and a quick view is a transient dialog.\n \n **Relationships are one declared list on one frame, not a card per relation.**\n Each entry is a `RelationSpec` from `@nanisoft/prism-ui/spec` plus the words its\n own empty frame needs, drawn in the declared order under one heading level inside\n one bounded frame with one rule between them. The `kind` names a collection\n arrangement this package already draws: `Table`, `ListPanel`, `Timeline`,\n `AvatarGroup` or the caller's own `slot`. **Depth is one, and the bound is in the\n type**: a relation's members never name `RelationSpec`, so a related record with\n its own related records is a graph the consumer walks, reached by the caller's\n `href` rather than by depth. The Block composes no disclosure anywhere.\n \n **The record's actions are one optional node, placed in the record's own band.**\n The Block renders no control of its own, there is no declared action union and no\n destination arm, and the confirmation for a destructive action is the caller's own\n `AlertDialog` composed around their control in the same slot. **A relation with no\n members draws `EmptyState01` at a reason the caller names**, in the relation's own\n frame, because a customer with no invoices is a collection that is empty and\n `NothingChosen01` is a statement about a reader's pointer at the index beside the\n pane, which this Block cannot be in. **The Block takes no total, no count and no\n sum**: a relation's `count` is the caller's own string and is never derived from\n the members, and a screen that wants a figure composes `Summary01` or `Stats01`\n into the Block's `children`.\n- 1079d02: The record index groups by a key on a row and announces the two selection scopes differently\n \n `DataTable01` widens again rather than adding an Item, and the roster does not\n change: a grouped index is the same job with a `groupBy` on a row, not a second\n arrangement, so no `-02` is earned.\n \n **Grouping is a key on a row.** `groupBy` takes a row and returns the key it\n belongs to, or `undefined` for an ungrouped row, and `groupLabel` names the\n heading the Block draws above each run of rows that shares one. The two are a\n single arm: `groupLabel` is required wherever `groupBy` is set, because a heading\n with no words is a blank row. The Block never reorders the rows it was handed, so\n same-key rows must arrive adjacent and a run of them is one group. A group heading\n is a row in the same body as any other, and it is not a record: it is never in\n the selection set, never in the count and never in what the header's page scope\n covers, so forty headings over four hundred rows select four hundred records and\n say so.\n \n **The two selection scopes are announced differently, and the filter scope's\n number is the caller's.** `selectionScope` is new and is a union. Its page arm is\n the default and the Block speaks `labels.selectedCount` over the keys it holds.\n Its filter arm reports that the whole matching population is selected and carries\n a required `count` from the caller, and the Block speaks\n `labels.selectedAllMatching(count)` rather than printing the page as if it were\n the total. The Block owns no control that selects a population it cannot see: the\n caller selects in its own store and reports the scope here.\n \n **What is new on the surface:**\n \n - `groupBy` and `groupLabel`, together, for a grouped body.\n - `selectionScope`, a `DataTable01SelectionScope` union, for the page or the\n filter scope.\n - `selectedAllMatching`, a required `labels` member: the summary and announced\n sentence for the filter scope. `selectedCount` is now the page-scope sentence,\n and it should say the page.\n \n The per-row prop that went and the node that replaced it are named in the record\n index selection entry in this release: `rowActions`, a declared list of action\n objects with an optional handler, is gone, and `renderRowActions`, a node per row,\n replace it, so a row that looks like a command and carries none cannot be built.\n- 1079d02: The record index draws typed columns and holds the selection set it was given\n \n `DataTable01` widens rather than adding an Item, and the roster does not change:\n every one of the in-scope screens is one job, a reader scanning rows and acting on\n them, differing only in its columns, its keys, its filters and its sort key. No\n `-02` is earned by it.\n \n **The columns are typed data now, and the callback is gone.** `columns` is a\n `readonly ColumnSpec[]` from `@nanisoft/prism-ui/spec` rather than an array of\n `{ id, header, cell }`. `key`, `header` and `kind` are required on each column,\n because a column a reader reaches without a name is a column every row announces\n as untitled, and `kind` is drawn from Prism's own cell vocabulary rather than from\n an HTML attribute. The row value at the column's key is the cell's content: a\n single-node kind draws it as that Component's child, a structured kind takes it as\n that Component's props, and the `slot` kind draws it as your own node inside the\n header, alignment and sort affordance the Block draws. `DataTableColumn` and\n `DataTableRowAction` are removed with it.\n \n **Selection is required whenever the index is selectable.** `getRowId` is required\n on the Block, because sorting, filtering, paging and deleting all move a row's\n index and an index-keyed selection silently changes which records a batch action\n acts on. `onSelectedIdsChange` is required on the selectable arm and `batchActions`\n with it; the Block holds the set on its uncontrolled arm and reports every change\n through the one callback, so mirroring the set is one line and ignoring it is zero.\n \n **What is new on the surface:**\n \n - `columns`, now `ColumnSpec[]`. `sortable` draws `TableSort` and reports the next\n direction through the new `sort` and `onSortChange`.\n - `getRowId`, now required.\n - `onSelectedIdsChange`, required on the selectable arm.\n - `batchActions`, required on the selectable arm: one `ReactNode` the Block places\n in a bar that mounts only while the set is non-empty and displaces the filter\n controls. The Block draws the count and the dismiss and no command of its own.\n - `renderRowActions`, replacing `rowActions`. A node per row rather than a declared\n action list whose handler was optional, so a row that looks like a command and\n carries none cannot be built.\n - `sort` and `onSortChange`, for the ordered column.\n - `selectRow` is now `(row) => string` so every checkbox names its record,\n `selectedCount` is the count the live region speaks, and `clearedSelection` and\n `dismissSelection` join it. `sort` is the words for the next direction.\n \n **One polite live region carries the count**, never an assertive one, mounted while\n there is an announcement; it speaks the count rather than a list of labels, and it\n speaks the emptying as its own sentence rather than a count of zero. The visible\n summary and the announced number are the same string.\n \n **Nothing else is owned.** The Block fetches nothing, sorts nothing, filters\n nothing and slices nothing, owns no action, no aggregate, no address and no second\n pane. The detail pane reacts through your read of `onSelectedIdsChange`; no bridge\n is built between the two.\n- 1079d02: The record write form renders a field specification whole\n \n `RecordForm01` is a new Block: a create or edit screen rendered from a typed,\n ordered field specification. It takes `FieldSpecGroup[]` from\n `@nanisoft/prism-ui/spec` and draws Prism's own controls, each inside one shell of\n label, help and error. Every field carries a stable `key`, a required `label`, a\n required `kind` and optional help, hint, placeholder, disabled flag and starting\n value; a control the package does not ship is the `slot` arm, and the Block still\n draws the shell around the caller's node.\n \n **The error state is an issue list keyed by field**, the shape `FormDialog` and\n `FormWizard` already publish, and a message about the submission rather than about\n a control is `submitError`, which names no field. **The save union has three arms**\n and the compiler holds it: `action` posts to a URL, `onSubmit` hands over the\n form's element, and `submit` is a slot for the caller's own control. The save\n control is `type=\"submit\"` inside the `<form>` the Block renders. **Layout is\n derived**: `columns` is `1 | 2` at the form level, one column again at the narrow\n breakpoint, and there is no per-field layout prop and no slot per region.\n \n **One piece of per-kind rendering data is added.** The `MoneyField` arm of\n `FieldSpec` gains a required `currency` and an optional `locale`, because an\n amount is meaningless without the currency it is in and no locale can supply it.\n Nothing else in the shared module changes, and the module stays plain serialisable\n data with no rule, no validator and no callback on it.\n\n### Patch Changes\n\n- f11799a: The gate that holds a Block's controls now reads the menu row and the switch\n \n **No published surface changed.** Nothing in this entry is a prop, a Component or a\n behaviour you compose differently: the change is to `scripts/check-block-controls.mjs`,\n which is a repository gate and is not in the published tarball. It is here because the\n law it holds reaches a consumer as the shape their own Block has to take, and a\n consumer reading the changelog should be able to find that law without reading this\n repository.\n \n That gate classified `Button` and `CtaLink`, and neither of them is a menu row. The\n shipped record index takes a declared list of per-row actions whose handler is\n optional and renders each entry as a `DropdownMenuItem`, so a caller who passes a\n label and no handler gets a row that sits in the menu's keyboard order, is announced\n as a menu item, and activates to nothing. The gate could not see that Component, so\n every gate in the repository was green while it shipped, and a clean run was the\n absence of a check rather than the presence of a correct one.\n \n Four Components are classified now, each with the arms that make it act:\n \n - `Button`: `onClick`, `type=\"submit\"`, `type=\"reset\"`. Unchanged.\n - `CtaLink`: `href`, which is required, so that arm is the compiler's. Unchanged.\n - `DropdownMenuItem`: `onClick`, or `render` together with `href` in the element it\n renders as. Both words rather than either one, because `render={<span />}`\n navigates to nothing and a bare `href` would accept a `data-href` as a destination.\n - `Switch`: `onCheckedChange`, and nothing else. A switch does not navigate and is\n meaningless without the handler, because the state it changes is the consumer's\n setting rather than anything a Block holds.\n \n Every `DropdownMenuItem` and every `Switch` the tree renders already carries one of\n those arms, so nothing shipped failed and no Block moved. What the widening buys is\n the next one: a menu row or a switch a Block renders with nothing behind it is a\n finding naming the file and the line, where yesterday it was invisible.\n \n **Two limits are printed on every run and neither is new.** A `{...rest}` spread\n inside a rendered control reads as carrying no handler, and a handler forwarded from a\n declared optional prop reads as present, because resolving whether the caller passed\n it is reading a type rather than a file. The second is why the shipped row action list\n is still a finding for a reader and not for this gate: the optional handler behind the\n attribute is invisible to a source scan. Removing that declared list is the authoring\n work, and this gate is what makes the next one of them fail the run.\n \n **A Block that holds a disclosure itself is still not caught, and that gap was left\n open on purpose.** `Collapsible` and `Accordion` put none of the four classified\n Components in a Block's source, so a Block rendering one with a `defaultOpen` and no\n change handler reads as clean while the state is still state a Block holds. A\n disclosure's activation lives inside the disclosure rather than on an attribute of the\n row that opens it, so there is no arm for this gate to read, and a rule that inferred\n one would report the Blocks that compose `Collapsible` correctly. The prohibition\n stays a design rule in `DESIGN.md`, an auditor checks it by looking for the absence,\n and the run prints that it is not a finding so a clean run is not read as more than it\n is.\n- dbc8a65: The log and event stream is the record index drawing event rows\n \n A delivery log or a console events screen is the record index, `DataTable01`,\n drawing rows from the shared event specification, `EventSpec`. No Item, Kind or\n Page is added: a log gains a filter region, a second axis on which entries are\n compared and a page boundary, and every one of those already belongs to the record\n index rather than to a rail. The row type is the shared event specification, and no\n sixth event shape is minted for a log or a console.\n \n A screen that wants the events of one record composes `ActivityFeed01`; a screen\n that wants to find one entry among many composes `DataTable01`. The Block composes\n no disclosure: seeing one entry in full is a destination the caller names with its\n required words, expanding a payload in place is the caller's own `Collapsible`\n placed in a `slot` cell, and taking an identifier away is the caller's own control\n beside a code block. No prop changed; this release adds the Item documentation, the\n Demo and the JSDoc that record the arrangement.\n\n## 0.16.1\n\n### Patch Changes\n\n- bf34d82: `DocsShell` drew its navigation twice in 0.16.0 and draws it once again\n \n **0.16.0 introduced a regression and this release removes it.** To give a reader below `lg` some navigation, 0.16.0 drew each tree a second time behind a `<details>`: one copy in the rail for a wide screen and one behind a disclosure for a narrow one. Only one of the two was displayed at any width, so the accessibility tree was never wrong and a reader never met two copies at once. Every documentation page in the family still shipped two of everything, and the payload of that is paid on the largest surface the system has.\n \n Three consumer test files failed against 0.16.0 as published, and two distinct defects produced every one of those failures. Two files failed on a count that came back doubled, and two failed on a `getByRole('navigation', { name })` that had two answers where it expects one: the disclosure drew its own `<nav>` under the rail's own accessible name, and that name is how a consumer's suite reaches the rail at all. A suite that holds that a documentation rail contains no disclosure control was never the finding, because the `<details>` was a sibling of the rail in the same frame and never inside it.\n \n **What a consumer measured on 0.16.0, in three built exports.** The Nexus page `/docs/architecture/kanban-service` carried 3 `aria-current=\"page\"` markers where 2 is right, because the third belongs to that site's own bar and the other two are this Page drawing the same page twice. It carried 8 `<nav>` elements where 6 is right, and 52 `data-slot=\"docs-nav-page\"` rows on a corpus of 26. The AlphaLens index at `/docs` carried 12 `data-slot=\"docs-nav-label\"` spans on a six section corpus. The Prism preview route that renders this Page, `apps/site/out/preview/docs-shell.html`, carried 22 `docs-nav-page` rows on an 11 row outline and 5 `<nav>` elements where 3 is right.\n \n **What changes.** Each tree is drawn once, at every width. A documentation page carries one `aria-current` marker, one navigation list per tree and one row per document. Below `lg` the frame is a single column and the rail is its first child, so a reader at a phone or tablet width meets the navigation before the document rather than after it, which is the gap 0.16.0 was reaching for. The Page is still a server Component: no `'use client'`, no hook, no context, no handler, no runtime token read, and no new client JavaScript on any documentation page.\n \n **The fade is now bound to the cut it marks, and it was not.** The fade element was left unprefixed while the `padding-bottom` reserving the band beneath it was already `lg:`-prefixed. The cut exists only where the rail scrolls, which is `lg` and up, so below `lg` the band went inert and the gradient kept painting: the rail's last 2rem carried a fade over the last row or two, permanently, at exactly the width this release starts serving the tree at. Both numbers still matched and no row lost legibility, which is why it survived every lane that was looking. The fade and the band are now written against the same `lg:` as the `max-h`, the `overflow-y-auto` and the `sticky`, so a fade with nothing to fade is no longer painted. **The price, stated rather than left to be found:** below `lg` the rail is bounded by its content rather than by the viewport, so a long tree runs past the fold of a phone screen with nothing marking where it ends. No cut to mark, no band to reserve and no scroll region to scroll inside. That is the honest limit of one copy of the tree at every width, and it is now written into the Page's own documentation beside the rule that causes it.\n \n **The published surface loses two slots.** `data-slot=\"docs-nav-compact\"` and `data-slot=\"docs-nav-disclosure\"` were added in 0.16.0 and are removed here. They were published for exactly one release, and no call site in the four downstream repositories addresses either by name, so nothing there has to change. A call site that reached for one now finds nothing rather than finding an empty region, which is the honest outcome and the reason this is called out. Every slot that existed before 0.16.0 stays, `docs-rail-fade` included.\n \n **What it costs, with the number.** A phone reader now scrolls past the whole navigation to reach the article. One consumer's rail measured 1,318 pixels of content in a built export at 1280 by 680, and those rows wrap on a phone, so the scroll a narrow reader now makes is a thousand pixels or more. That is a real regression for that reader and it is the reason a disclosure was reached for in the first place. It is paid rather than hidden, because the two arrangements that avoid it are both worse: a rail at `lg` and nothing below it reopens the gap 0.16.0 closed, and drawing the tree twice to keep the scroll short is what broke three consumer test files here.\n \n **What would actually remove that scroll.** A collapsed rail below `lg` needs an open state, and a server Component cannot hold one. `<details>` is the only disclosure the platform holds on the Page's behalf, so a control that opens one has to be a client island inside the Page. That is separate work with its own budget and it is not built here: shipping a client boundary to save one scroll is a different trade from the one above, and it belongs in its own change rather than as a side effect of removing a duplicate.\n \n **One Item document was corrected, and no gate reads it.** This Page's own documentation said under Best practices that an entry with no words is a label. That was true before this work and became false the moment the Page began dropping a nameless entry rather than rendering one, and it shipped false through 0.16.0 because nothing in the tree compares that bullet with the Page's own source. The bullet now states what the Page does, and `docs/quality-gates.md` records why no gate was added for it rather than leaving the question open: a gate over prose claims has to enumerate the claims, and a list somebody maintains is the shape that already left the heading gate holding one Block and passing over four, or it has to match on wording, which fires on a rewrite and not on a lie. The decidable half of that failure class has a gate, in the heading-scale table comparison. This half is a reader's, and the hole is now named in the document that collects them.\n \n **Three things 0.16.0 got right and this release leaves as they were, with the fade's bound corrected above.** The rail fade, pinned to a non-scrolling sticky wrapper with the band it covers reserved in `padding-bottom`, both written against the `lg:` that creates the cut; the sticky `max-height` arithmetic in `100svh` rather than `dvh`, which cannot exceed the space it is pinned in; and nameless entries dropped rather than rendered as an empty `<li>`, including the group-label and empty-group arms, counted on each rail as `data-unnamed-entries`.\n\n## 0.16.0\n\n### Minor Changes\n\n- 1a0ba35: A page `h1` is now visibly larger than the `h2` sections under it\n \n A page `h1` rendered at 1.875rem and every section heading under it at 1.5rem, so\n the two were one fifth apart and the page had no top to its hierarchy. The fix is\n in the type scale rather than in the Component, because the scale stopped at `4xl`\n and there was no step for a page's own claim to take.\n \n **Two steps are authored, and they are the two widest gaps in the group.**\n `5xl` is 3rem and `6xl` is 3.75rem, both at a line-height of 1. Every other step\n in the `text` group is a whole multiple of 0.125rem and these two are too;\n `4xl` to `5xl` is four thirds, the first gap in the group wider than any gap it\n already had, and `5xl` to `6xl` is five fourths. They are authored as a pair\n rather than as one step because every rung of the heading ladder is a pair, a base\n step and one step up at `sm`, so a single step above `4xl` would have made the\n page heading the one heading in the system that does not grow at a width.\n \n Both steps reach CSS and the published pack contract the way every other authored\n step does, so `text-5xl` and `text-6xl` are utilities a consumer can compose, and\n neither is a value a consumer has to write.\n \n **The heading ladder now descends from the new ceiling, one authored step per\n level, and every level holds a step of its own.** There are exactly six authored\n steps at or above Body, so the table lands on the floor with a distinct step at\n every rung, where before `h4` through `h6` shared one. `h1` is at `5xl`, rising to\n `6xl` at `sm`; `h2` is at `4xl`, rising to `5xl`; and the table descends to `lg` at\n `h6`, which is the step Body is set at. **What a consumer sees is every section\n title moving up one step and every page heading moving up two**: a section title\n goes from 1.5rem to 2.25rem and from 1.875rem to 3rem at `sm`, and a page `h1` goes\n from 1.875rem to 3rem and from 2.25rem to 3.75rem at `sm`. No call site changes,\n because the level a Block was already passing is still what decides its size.\n \n **Five surfaces that render a page's own `h1` now ask the same table for its size\n rather than naming a step.** `page-header-01`, `docs-shell`, `blog-post-page`,\n `not-found-page` and `error-page` each wrote a fixed step into a heading that was\n meant to be a page's top-level claim, so with the ceiling lifted they would have\n rendered a page heading a rung or two below the section titles under it. They ask\n `headingSizeClass`, which is what `SectionHeading` and `Cta01` already asked, and\n `Cta01` still carries no literal.\n \n **Four more surfaces that resolved their own heading size, found the same way.**\n A Block that takes a `headingLevel` and writes the size beside it forwards the level\n and changes nothing a reader can see, which is the defect above at the scale of one\n Block. `data-table-01` said `text-lg`, `run-console-01` said `text-base`, and both\n moved the outline without moving the size. `run-stream-01` renders a literal `h2`\n at `text-lg`, which was the floor of the old scale, so a live region's `h2` came\n out below the `h3` introducing the section holding it. `/foundation/themes` said\n `text-3xl`, which had been Display and is now the `h3` step, so that page's `h1`\n rendered a rung below the section titles under it. All four ask `headingSizeClass`\n now, and the three Blocks carry `text-balance` with the size they gained: the ladder\n states weight, tracking and balance as one package and `SectionHeading` carries all\n three at every rung, so a title lifted from 16 or 18 pixels to 24 or 36 is worse\n unwrapped than the one it replaced.\n \n **The Item page's appendix heading is on the article's ladder, and asking the Block\n ladder for it was the wrong correction.** It said `text-xl`, which put an `h2` at 20\n pixels under the prose `h2` at 30 and below the prose `h3` at 24 above it. Asking\n `headingSizeClass` removed the third tier and created a worse one: \"API reference\"\n is a section of an article exactly as \"Overview\" is, so it takes the article's\n ladder, and the Block ladder is a step and a half above it at `sm`. The Item page\n then carried three sections at 30 pixels and an appendix at 48, and the appendix was\n the second largest thing on the page. It is now a bare `<h2>` inside the same\n `.prose` wrapper as the Item's own MDX body, so the article's rules size it at 30\n pixels and it carries no class of its own: every section heading in that article is\n a bare tag the article styles, and a heading naming its own step would be the second\n answer to a question the article has already answered. The table stays outside\n `.prose`, whose descendant rules would put its cells at the body step in the muted\n colour, and the gap above it is the article's own 16 pixels rather than a second\n region's 32.\n \n **The five theme names are card titles, and they compose `CardTitle` to say so.**\n They carried `font-medium` and no size, so they rendered at 16 pixels and 500 under\n a 60 pixel `h1`. Asking the ladder for the size would have been wrong: five themes at\n the `h2` step is five 48 pixel headings over five cards on a page whose own claim is\n the `h1` above them. `CardTitle` already holds the answer the library needs: a card\n title sits at body size, and the element and the visual step are separate questions.\n So the size stands, the tag stands at `h2` because the grid draws no section heading\n of its own, and the weight now comes from the one Component that owns it rather than\n from a literal. What a consumer sees is five theme names one step heavier.\n \n **The two Foundation readers took opposite answers, and the reason is worth more\n than either change.** Both render inside an MDX body, which the site wraps in\n `.prose`, and both used to carry literals, which a utility layer lets win over the\n article's own component-layer rule: `/foundation/colors` published an `h2` at 18\n pixels and an `h3` at 14, below the `h3` above them.\n \n `ColorTokens`' four headings are sections. Two divisions of an article and a\n partition under one of them is a structure, not a list of labels, so they name no\n step and the article decides, which is the one copy of the ladder there is.\n \n `TokenBrowser`'s are not sections, and dropping its literals let the wrong authority\n decide them: every group name came out at the prose `h2`, 30 pixels, which is more\n than twice the 14 pixels the navigation rail's own section titles are set at, on the\n page whose whole job is being navigated. Each of those 184 names is a table's name\n and nothing else, so they are captions: `h3` at `text-sm`, in the mono face because a\n token key is machine notation, at the same 14 pixels and 500 as the column heads of\n the table underneath. That is the caption `ApiTable` already renders for a compound\n export's part names on every Item page, so this is the repository's own treatment\n rather than a third one. One heading on the page, \"Semantic contract\", is a division\n of the article rather than a label, and it keeps naming no step.\n \n **`Prose` walks the same ladder from one rung down, and a prose heading therefore\n sits below a Block heading at the same level.** Its child treatments now set `h1`\n at `4xl` and descend one authored step per level to `lg` at `h5` and `h6`,\n which is the same scale and the same floor entered at the rung that fits the\n reading measure: Display is 3rem, and a 42rem column is fourteen of those. The new\n `h5` and `h6` treatments are new, so a document that reached past `h4` before now\n has an answer rather than inheriting nothing. **The consequence is stated rather\n than left to be found.** A Block-composed `h2` is 2.25rem and 3rem above `sm`; a\n prose `h2` is 1.875rem and does not grow. They are 1.2 apart at the base width and\n 1.6 apart above `sm`, where they were 1.25 apart before this change, so a consumer\n who sets a `Prose` section beside a Block section sees two tiers rather than one.\n `DESIGN.md` under Typography, Hierarchy now says so, and says which to reach for.\n `DocsShell` runs `Prose` at `fullWidth`, so the frame decides the width: 35rem of\n article track at a viewport of 1216px or wider, and less below that, which makes\n the case for entering one rung down stronger there rather than weaker.\n \n **`Heading`'s free `size` prop is unchanged, and this is stated rather than\n implied.** `size` is a choice made beside the tag, for a heading the surrounding\n document has already placed and wants attuned, and its largest arm is a step of\n the scale rather than a rung of the ladder. Its two largest arms were not removed\n and no new ones were added, so no consumer's call site changes and there is still\n no second route to a page `h1` from inside the library. `search-page` and\n `status-page` are the two surfaces that take a heading's *tag* from\n `childLevel(headingLevel)` and its size from this prop, and they are left as they\n are: those are labels over a list of results and over a list of incidents, and a\n caption is not a rung.\n \n **`scripts/check-heading-scale.mjs` now holds three things it described but did not\n check.** Its rule 4 claimed a table that jumps is a finding and only tested for a\n table that rises, so a table that skipped a rung passed it: descending, reaching\n the ceiling and flooring at or above Body, and rendering the page `h1` one gap\n above its own `h2`, which is the defect these steps exist to remove. And the\n literal Display step it refused on `Cta01` was written out as `text-3xl` and\n `sm:text-4xl`, which stopped being Display the moment the ceiling moved. Both are\n now read from the authored scale and the shipped table rather than restated, and\n both have a staged fixture in `scripts/__tests__/heading-scale.test.mjs`. The third\n is the Item rule below, which is new rather than a restatement.\n \n **The Item document's own table was the retired ladder, and it now states this\n one.** `apps/site/items/component/layout/section/section.mdx` is the page a reader\n is on when they ask what `as` resolves to, and it listed `h1` at `text-3xl` and\n `h4` through `h6` all at `text-lg`: the table as it was before this change. It now\n states the ladder the Component renders, and the sentence under it about where the\n scale stops has been corrected with it, because a corrected table beside a stale\n paragraph is the same disagreement one row down.\n \n **The gate also reads the Item document now, and that is the third copy of the\n table.** `apps/site/items/component/layout/section/section.mdx` states the ladder\n in its prose, and neither the declaration build nor the corpus reaches it, so when\n the ceiling moved it kept publishing the retired table. The new rule compares it to\n the same `HEADING_SIZE` the JSDoc is compared to, by the same cell-for-cell shape,\n so it names no step of its own; it reads the whole Item rather than a slice, so a\n second table in that prose is a finding; and it fails an Item that states no table\n at all, because a comparison with no rows finds nothing to disagree with. Three\n staged fixtures cover the three cases.\n \n **What the gate still does not hold is written down rather than left to be\n discovered.** `Cta01` is named as a root because it is the one Block whose heading\n cannot be composed from `SectionHeading` at all, so a literal step on it has no\n table to fall back to. Beyond that file the gate cannot go, and the reason is\n stated rather than left to be found: whether a heading a Block renders is a rung of\n the ladder or a caption inside that Block is not decidable from source.\n Forty-eight headings across `packages/ui/src` and `apps/site/src` name a step of\n their own, the eleven rungs among them now ask the table, and the thirty-seven that\n remain are captions. Two surfaces bind a caption to `headingLevel` rather than to\n `childLevel`, so a rule reading the tag cannot sort them, and a root list holding\n the rungs would be a list somebody maintains. That is how the `Cta01`-only rule\n came to hold the one Block already fixed and pass over the four that were not, and\n the count is written into `docs/quality-gates.md` so the next reader starts from it.\n `TokenBrowser` adds one more to that population, which is the point of naming a\n caption's step rather than inheriting it: the scan can see it, and it is below the\n floor by arithmetic.\n \n **The limit of the scan that took that count is now written down too, and it is the\n more useful half of it.** The count came from looking for a literal size utility\n beside a heading tag, and a heading whose size it inherits names nothing: the five\n theme names above carried no size at all, and two scans of exactly that shape passed\n them at 16 pixels and 500. A size that is inherited is a size nobody chose, and \"no\n literal\" is invisible to a scan that looks for literals, so that count has never\n been a count of every heading that names a step, and the number nobody chose is the\n half of it that reached a reader. What the count is good for is the arithmetic: a\n heading it reports is a rung if the step it names is at or above the step Body is set\n at, and a caption if it is below, so the two sort without anybody deciding which is\n which. The remedy for what the scan cannot see is on the surfaces rather than in the\n gate, and it is the one the token inventory now uses: a caption names its own step,\n so it is visible to the scan and it is below the floor by definition.\n \n The bump is a minor in both packages rather than a patch because a new authored\n step is a new public contract: `--text-5xl` and `--text-6xl` with their\n `--line-height` companions enter the emitted CSS and the published pack contract,\n and every heading on every page composed from a Block moves.\n- 1a0ba35: `BlogPostPage` refuses a post whose display date is its machine date\n \n Measured on `www.nanisoft.com/blog/where-the-products-stand`: the page rendered the date\n as `2026-09-26`, the raw machine value. All four NaniSoft sites pass one raw ISO string to\n both `date` and `dateTime`, so the reader was handed a machine value and the `datetime`\n attribute was handed the same one.\n \n **The contract stands and the Page now holds it.** `date` is the words a reader sees and\n `dateTime` is the machine value on the `time` element, and they are separate props\n precisely so the two can differ. Formatting is a locale decision, so it belongs to\n whoever owns the fact, and Prism renders `date` verbatim. What was missing was any way for\n the library to notice when a caller did not do that: a prop called `date` that takes a\n `string` is not obviously wrong, the defect is invisible to every reviewer who does not\n open the page, and a JSDoc block is read only by whoever is integrating the library. So\n the Page now throws when `date` and `dateTime` are the same string, naming the fix in the\n message. `StackGrid01` refuses its own missing `ownLabel` for the same reason and by the\n same mechanism, and a rule that is only written down is a rule four repositories read and\n none of them are caught by.\n \n **The refusal is on the pair, not on the shape of either value.** A site that writes\n `2 Sep 2026` and a site that writes `2026-09-26` both pass. What fails is handing the\n Page no display formatting at all, and the machine value is already on the element in the\n `datetime` attribute for every feed reader and every index, so displaying it a second\n time is a redundancy rather than a choice.\n \n **`dateTime` is typed as the shape it is**, `` `${number}-${number}-${number}` ``, so a\n formatted string passed there is a compile error rather than an attribute no reader can\n check. `date` stays a `string`, and the reason is in the Component's JSDoc: no TypeScript\n type separates `21 September 2026` from `2026-09-26`, which is exactly why the refusal\n above has to exist at run time.\n \n **This is a breaking change and it is released as a minor.** It breaks every consumer at\n build time, so it is a break and not an additive change, and each of the four call sites\n below has to change in the same release:\n \n - `alphalens/app/blog/[[...slug]]/page.tsx`, lines 121 and 122\n - `atlas/app/blog/[[...slug]]/page.tsx`, lines 137 and 138\n - `landing-page/app/blog/[[...slug]]/page.tsx`, lines 119 and 120\n - `nexus/app/blog/[[...slug]]/page.tsx`, lines 135 and 136\n \n Each is the same two lines, `date={page.data.date}` and `dateTime={page.data.date}`, and\n each needs a formatted string in the first. `CONTRIBUTING.md` says a breaking change is a\n `major`; `DESIGN.md` records the decision that a minor is the right line for a break from\n a `0.x` version, with the reasoning that taking `1.0.0` would declare the public API stable\n rather than describe the change. That reasoning is dated and deliberate, so the minor is\n taken here and the disagreement with the contributing guide is recorded rather than\n silently resolved.\n \n `DESIGN.md` states the rule once, in **The composition layers** beside the other Page\n rules. Nothing else about the Page changed: same props, same elements, same `data-slot`\n values, and it is still a server Component with no client code and no router.\n\n### Patch Changes\n\n- 1a0ba35: `Diagram` labels stay legible on a phone, and no stroke crosses one\n \n Two defects, both measured on the five-node architecture drawing the company\n site renders. Its node names were drawing at 6.95 CSS pixels and its relation\n words at 5.88 in the 342 pixels a phone leaves the drawing, and ten of the\n twenty-five label and stroke pairs on that drawing intersected: all five node\n names were struck through by an edge leaving their own node, and all five\n relation words sat on their own line.\n \n **Labels now have a floor, and it is the drawing's own coordinate space.** Type\n inside a scaled `<svg>` is sized in user units, so a label's rendered size is its\n size in user units times the rendered width over the canvas. That is why the\n canvas is fixed, and it is not the whole answer, because it has no floor in it.\n The drawing is now never rendered narrower than 640 pixels, one user unit is\n therefore never less than one pixel, and the smallest label the drawing sets is\n twelve units, which is what `text-xs` resolves to. A container that cannot give\n it that much scrolls sideways rather than letting the labels fall through it,\n which is the arrangement `Table` already makes with seven columns and `Gantt01`\n with a schedule of names and bars.\n \n The cost is stated rather than hidden: on a phone a reader sees about half the\n drawing at a time and pans for the rest. The alternative was the whole drawing at\n 5.88-pixel type, and `DESIGN.md` records that trade under **The figure floor**.\n Nothing above 640 pixels of container changes at all, so no desktop or tablet\n rendering moves.\n \n **Strokes now stop where the ink runs out, and each label is asked on its own.**\n A node's name is printed under its mark, so a relation drawn centre to centre ran\n through the name under whichever node it left downward. The first version of this\n fixed that by trimming every stroke back to the box enclosing the mark and every\n label beside it, which is also wrong in the other direction: a relation along a\n row never touches the name printed under its mark, so backing it off by the\n half-width of that name detached it from the mark it connects. On three things on\n one baseline with names at length, every end of every relation stood sixty units\n clear of its mark. A relation is now trimmed against whichever comes first, its\n own mark's edge plus the clearance or the far side of an individual label the\n stroke actually runs through, so a relation leaving sideways reaches its mark and\n one leaving downward still stops below the name in front of it. The whole line\n between the two marks is divided once rather than each end trimmed alone, because\n a name and a subtitle sit below their mark at two depths and a stroke arriving\n from below runs through the first and back into the second.\n \n The box is computable from a server Component because every label in a drawing is\n set in the monospaced face, where a label's width is its character count rather\n than a measurement. That is what `DESIGN.md` records under **The ink-avoidance\n rule**, with the reason a halo was rejected and the direction clause that the\n first form of the rule was missing.\n \n **The drawing is wrapped in a container.** It carries `data-slot=\"diagram\"` and\n still scrolls, and the wrapper carries `data-slot=\"diagram-container\"`, following\n `Table`'s `table-container`. Every existing `data-slot` value, the derived or\n passed `aria-label`, `role=\"img\"`, and `data-unresolved-relations` are unchanged,\n so no call site and no test that addresses the drawing by slot needs to move. The\n Component is still a server Component with no client code.\n- 1a0ba35: `FactList` sets its values flush left, and says why there is no numeric mode\n \n Rendered on `www.nanisoft.com/about`: a three-row fact list where every value was set\n `text-align: right`. The middle answer is long enough to wrap, so its second line began\n wherever the first one ended and the column read as ragged-left, which is what a mistake\n looks like rather than what a decision looks like.\n \n **Right alignment is the right answer in a column of figures, and `FactList` has no\n figures.** That was the question to settle before changing anything, because the two\n answers need opposite fixes. `FactList` takes `label` and `value` and nothing else: there\n is no `numeric` prop, no `tabular-nums` anywhere in the module, no monospaced arm and no\n per-row alignment of any kind. So nothing in it is a figure, nothing lines up on its last\n digit, and what right alignment moved was the first letter of each answer to a different\n place on every row. The suite asserts the absence as a fact rather than restating a prop\n list, so a `numeric` prop added later fails there instead of quietly making the default\n wrong for every caller that never passes it.\n \n The value is now `text-left`, and `text-balance` stays, because it is the same reason it\n was there: a two-line answer should break where it breaks well rather than where the\n measure runs out.\n \n A caller that genuinely wants a right-aligned column of figures is not refused anything.\n `value` is a `ReactNode`, so the caller can pass the span it wants, and Prism holds no\n opinion about a value it did not lay out.\n \n Nothing else about the Component changed: same element, same props, same `data-slot`\n values, same server Component with no client code.\n \n **One thing found and not changed, because it is a different claim.** The Component's own\n JSDoc says the term column is a fixed fraction rather than a content width, and the code\n says otherwise: the `dt` carries `shrink-0` and no basis, so the term column is sized by\n its content and the left edge of the value column is therefore not straight down the\n list. That is a real mismatch between a documentation source and the thing it documents,\n and left-aligning the value does not make it worse or better. Fixing the code to match\n the sentence would move every fact list in four consumer repositories, so it is a decision\n for the maintainer and not a side effect of this one. Recorded here rather than made.\n- 1a0ba35: `StackGrid01` lifts both arms of the grid to the type floors\n \n Measured on `nexus.nanisoft.com`: the in-house arm set its tile names at 14 pixels and\n the line under them at 12, so a heading on this Block rendered smaller than the copy\n around it and the tile read as a caption rather than as a tile.\n \n **Both arms are raised, and the reason is a floor rather than a comparison.** `DESIGN.md`\n already states both, which is why this is a Component coming into line rather than a new\n rule:\n \n - A tile name is a card title, and `CardTitle` says a card title sits at Body. Body is\n `text-lg` in this system, 18 pixels, so a name at 14 was below it.\n - A role and a blurb are supporting copy, and `DESIGN.md` gives supporting copy\n `text-sm`, 14 pixels, so a line at 12 was below that too.\n \n The first is the floor the heading ladder stops to avoid: `lg` is the deepest authored\n step that is not smaller than Body, precisely so that a heading never renders smaller\n than the copy it introduces. A tile name at 14 against copy at 18 broke exactly that.\n \n **The two arms set the same steps in both, which is a fact and not a comparison.**\n They already agreed here, so this was not a reconciliation and the reported arms\n never disagreed: what agreed was below both floors. The difference the two arms are\n supposed to carry is a claim about provenance, and it is carried in weight and ink,\n the muted tile against the page with a `border-primary` hairline and the `md` shadow\n step. A second group set quieter than the first would be a grid claiming the in-house\n work matters less than the assembled parts, which is the opposite of what the group is\n for. The assertion in the suite states the agreement as a fact so it cannot drift.\n \n The mono annotation over an in-house name and under a codename is left at `text-xs` on\n purpose. That is a machine annotation in the platform monospace stack, which is what\n `DESIGN.md` uses the mono face for, not a run of prose, and it is the only type on the\n tile that is not prose.\n \n No `data-slot` changed, no prop changed, no field was added to either arm and neither\n arm was widened, and the Block is still a server Component with no client code. The\n tile's own height grows by the four pixels of the name plus the two of the line, which is\n what the floors cost and not a change of rhythm: the grid was already `h-full` per row, so\n a row of tiles is as tall as its tallest and both arms move together.\n- 1a0ba35: `SiteFooter` links are a 24 pixel target instead of a 17 pixel one\n \n Measured in the built exports of three consumer sites: six to eight destinations per\n footer, every one of them 17 pixels tall (`39x17`, `30x17`, `42x17`, `33x17`, `70x17`,\n `37x17`), with the code a reader can see at\n `https://www.w3.org/TR/WCAG22/#target-size-minimum` asking for 24 by 24.\n \n **The link had no box.** A bare inline anchor is not a 17 pixel target by design; it\n has no box at all. What a pointer aims at on one is the font's content area, and for\n the shipped face at `text-sm` that is 16.94 pixels: fourteen pixels of type times the\n face's own ascent and descent, times its size-adjust. Those three numbers are not a\n guess, they are read out of the `@font-face` this package ships for `Inter Fallback`,\n and reading them is what produced 16.94 against the 17 in the export. The social row\n was the other shape and the other number: an `inline-flex` box, so its height is its\n line box, 20.01 pixels.\n \n **The floor is paid on the box, so the link looks the same.** Both links are now\n `flex min-h-6 items-center`: a block at least `--spacing-6`, 24 pixels, tall, with the\n same fourteen-pixel type at the same weight in the same colour inside it. It is the\n arrangement `mobile-nav` already uses for a stacked navigation link at the\n coarse-pointer floor, one step lower and at every pointer rather than only on a coarse\n one. `--spacing-6` is arithmetic on the authored spacing multiplier the way every\n `size-*` and `h-*` in this package is, so a retune of the scale moves the floor and the\n Block could not have pinned it.\n \n **No height anywhere moves.** A footer column link sat in a line box built from the\n footer's inherited sixteen-pixel body, so each row was already 24 pixels tall before the\n floor and each row is the same 24 after it; only the 17 pixels of target inside the row\n grew. The footer's own height is unchanged at both widths, which is the outcome four\n consumer pages are composed against, and the assertion that says so is in the suite.\n \n The 24 pixel circle exception in the criterion arguably held, because consecutive rows\n in a footer column stand 32 pixels apart centre to centre and a 24 pixel circle on\n either clears the other. The fix is made anyway, and `DESIGN.md` records why under\n **The target-size floor**: a target whose size depends on the gap beside it is one edit\n away from failing and nothing in this repository would report it.\n \n No `data-slot` changed, no prop changed, no call site moves, and the Block is still a\n server Component with no client code. `DESIGN.md` states the rule once, in the section\n beside **The coarse-pointer floor** it is deliberately not merged with.\n- 1a0ba35: `DocsShell` marks where its rails cut off, drops the rows it cannot name, and reaches a tablet\n \n Three defects on the one documentation screen the other NaniSoft sites import.\n \n **The contents rail hid a third to a half of the navigation behind an invisible\n scroll.** The rail is capped against the viewport and scrolls inside that cap, and\n Chrome's scrollbars are overlay scrollbars, so they appear only once the reader has\n already scrolled. Measured in two consumers' built exports at 1280 by 680, the rail's\n content stood at 912 and 1318 pixels in a 568 pixel box: 344 and 750 pixels of a\n primary documentation navigation below the fold with `mask-image: none`, no\n persistent scrollbar, no fade and no count. The last visible row was cut mid-word\n above dead space, which reads as a rendering fault rather than as a scroll region.\n A cap nobody can see is not a cap.\n \n **The affordance is a fade across the foot of each rail, and the space it covers is\n reserved rather than borrowed.** The sticky offset moved to a wrapper that does not\n scroll, because nothing inside a scroll container stays put while that container\n scrolls: a gradient or a mask written into the scrollport travels with the content\n and marks nothing. The wrapper travels, so the gradient is pinned to its foot, and\n the `<nav>` inside keeps the cap, the independent scrolling and the whole of its\n class contract. The rail reserves `padding-bottom` of the same height as the fade,\n which is the half that makes it honest: at rest the gradient falls on reserved space\n and the last row is fully legible, and only while there is more below does it fall on\n content. Without that padding the same gradient washes the last row permanently,\n which is the usual cost of this answer and the reason it is not optional.\n \n Two alternatives were rejected for stated reasons rather than passed over. A\n persistent themed scrollbar is truthful at both ends and costs nothing to\n legibility, but it says \"there is more\" without saying which way, and it needs its\n gutter reserved in a 15rem rail so a short tree becoming a tall one does not shift\n the content. A collapse control, and an \"N more\" control, both need state or a count\n this Page cannot have: one is a client Component and the other is a measurement a\n server Component cannot take.\n \n The fade costs a keyboard and a screen-reader reader nothing. It is an empty element\n with `aria-hidden` and `pointer-events-none`, inside the `<nav>` and outside the\n `<ul>`, so it is not a list item, it is never announced, it never takes a press and it\n never reaches a Tab stop. The region still scrolls on its own, so the arrow keys walk\n every entry exactly as before.\n \n **The Page can emit a navigation row containing nothing, and now refuses to.** An\n entry whose `title` is the empty string rendered as\n `<li data-slot=\"docs-nav-page\"><span data-slot=\"docs-nav-label\"></span></li>`: a row of\n nothing inside a `<nav>`, which one consumer measured at 102 across 25 of its 31\n documentation pages, because that site maps a heading whose title arrives as a React\n element to `''`. A blank row is not a destination, and it is not a label either,\n because a label is words. The Page now drops the entry, drops a group label it cannot\n name while keeping the pages under that group, and drops a group that has neither\n words nor pages. Nothing is swallowed: the rail carries the tally as\n `data-unnamed-entries`, the same answer `Diagram` gives a relation it cannot resolve,\n so a consumer can tell \"nothing was wrong\" from \"the Page stopped counting\".\n \n A blank `title` and a blank `href` are still answered differently, because they have\n different amounts left. A page with an address and no words is a real page with one\n unrenderable field, so losing the row costs a reader nothing while taking the screen\n down over it would cost them everything. A page with words and no address is a row\n whose entire content is a route that does not exist, and that is still refused by\n name.\n \n **Below `lg` there was no navigation at all.** At 768 and at 1024 both rails were\n `hidden` and the only navigation left inside the article was the two-item pager at\n its foot, so a reader on a thirty-one document reference at tablet width had no\n contents, no on-this-page and no way to a sibling page except Previous and Next. Each\n tree is now also drawn behind a `<details>`, first in the document so it is what a\n narrow reader meets before the text. `<details>` is the disclosure that needs no\n runtime: the platform holds the expanded state, the control is in the Tab order with\n a real expanded state, and the Page stays a server Component with no hook, no context\n and no handler. The summary carries `navLabel` or `tocLabel`, which is the caller's\n own word for that navigation, so the Page ships no reader-facing copy and a\n consumer that files its documentation in another language gets the control in that\n language by having named the region once.\n \n The tree is therefore in the document twice, and only one copy is displayed at any\n width: the rail is `hidden lg:block` and the disclosures are `lg:hidden`, so the\n other is `display: none` and is out of the accessibility tree and out of the tab\n order rather than a second list a reader meets.\n \n **What did not change, and what a consumer may notice.** Every existing `data-slot`\n value stays, so no call site and no test that addresses the Page by slot needs to\n move; three new slots are added, `docs-rail-fade`, `docs-nav-compact` and\n `docs-nav-disclosure`. The rail's `<nav>` now sits inside a wrapper, which is what\n lets the fade stay put, so a consumer stylesheet reaching for it as a direct child of\n `[data-slot=\"docs-rail\"]` reaches the wrapper instead. `data-slot` is documented as\n markup metadata rather than a styling API, and `Diagram` and `PulseGraph` took the\n same wrapper for the same reason in the same release line.\n \n The bump is a patch rather than a minor because nothing a consumer composes through\n widens: no prop is added, no export is added, no token and no name in the contract\n move, and no call site changes. What changes is what the Page draws from data it\n already accepted.\n \n **The one thing this leaves alone is stated rather than left to be found.** Below\n `lg` the frame is a single column and this Page runs `Prose` at `fullWidth`, so the\n frame decides the track: the article column is the whole container, which is 45rem at\n 768 and 61rem at 1024, against the 35rem it gets at 1216 and above. That is a wider\n measure than the one the reading column is held to, in the band between the two\n breakpoints, and it is a frame decision rather than a `Prose` one. A phone at 390\n gets 21rem and is inside the measure. Capping the article track below `lg` is a\n deliberate layout choice with two honest forms, a centred column at the measure or a\n two-column frame from `md`, and either one changes what a narrow reader sees on\n every documentation page in the family, so it is left to the maintainer rather than\n taken here beside two defects that had no such choice in them.\n- 1a0ba35: `PulseGraph` labels stay legible on a phone, and no stroke crosses one\n \n Three defects, all measured on a four-stage pipeline the way `Diagram`'s two were\n measured on the five-node architecture drawing the company site renders.\n \n **Labels now have a floor, and it is the drawing's own coordinate space.** Type\n inside a scaled `<svg>` is sized in user units, so a label's rendered size is its\n size in user units times the rendered width over the canvas. That is why the\n canvas is fixed, and it is not the whole answer, because it has no floor in it. In\n the 356 pixels a phone leaves the drawing, a stage name was rendering at 7.23 CSS\n pixels and a node's note at 5.56, and ten user units was the smallest type the\n whole package emitted anywhere, including inside a figure. The drawing is now\n never rendered narrower than 640 pixels, one user unit is therefore never less\n than one pixel, and the smallest label the drawing sets is twelve units, which is\n what `text-xs` resolves to. A container that cannot give it that much scrolls\n sideways rather than letting the labels fall through it, which is the arrangement\n `Table` already makes with seven columns. Nothing above 640 pixels of container\n changes at all, so no desktop or tablet rendering moves.\n \n **Connectors now stop where the ink runs out, and each label is asked on its own.**\n A node's name is printed under its mark, so an edge drawn centre to centre ran\n through the name and the note under whichever node it left downward. On the\n pipeline this fix lands on, eight of the thirty-two label and connector pairs\n intersected: the two stages nearest the top of the canvas, each with its own name\n and its own note struck through by the two edges that met it. The first version\n of this trimmed every connector back to the box enclosing the mark and every\n label beside it, which fixes the crossings and detaches the drawing instead: a\n stage's name and note both sit below its mark, so a connector along the rail\n touches neither, and backing it off by the half-width of the widest name at the\n node left four of the five connectors on the six-stage figure floating free of\n both their marks, by up to fifty-five units. A connector is now trimmed against\n whichever comes first, its own mark's edge plus the clearance or the far side of\n an individual label it actually runs through, so all five connectors on that\n six-stage figure touch both their marks with the clearance between them and the\n stroke is thirty to fifty units longer, and a connector leaving a node downward\n still stops below the name in front of it. The whole line between two marks is\n divided once rather than each end trimmed alone, because a name and a note sit\n below their mark at two depths and a connector arriving from below runs through\n the first and back into the second.\n \n You do not have to space your nodes to avoid this, and you always had to space\n them to keep two names from touching each other.\n \n **The rail no longer sits on a note.** The rail was a fixed distance up from the\n bottom of the canvas while a node's note reached a fixed distance below its mark,\n so the two met on one range of mark positions: a lane whose mark landed low enough\n put its note under the rail's own stroke. The fitted area now stops above the band\n the rail reserves, which is the rail's distance up from the bottom plus its own\n thickness, the drawing's ink clearance, and the deepest ink a node draws. A graph\n with no lanes has no rail, so nothing reserves the foot and the fit is unchanged.\n \n **The drawing is wrapped in a container.** It carries `data-slot=\"pulse-graph\"` and\n now scrolls, and the wrapper carries `data-slot=\"pulse-graph-container\"`, following\n `Table`'s `table-container` and `Diagram`'s `diagram-container`. Every existing\n `data-slot` value, the `role=\"img\"`, the passed `aria-label`, `data-lanes` and\n `data-unresolved-relations` are unchanged, so no call site and no test that\n addresses the drawing by slot needs to move. The Component is still a server\n Component with no client code.\n \n `DESIGN.md` records the two rules this applies under **The figure floor** and **The\n ink-avoidance rule**, the latter with the direction clause that the first form of\n the trim was missing, and the geometry both Components now share is one internal\n module rather than a second copy in each.\n- Updated dependencies [1a0ba35]\n - @nanisoft/prism-tokens@0.16.0\n\n## 0.15.0\n\n### Minor Changes\n\n- 6dc7bb8: A Block action is a destination or your own control, and a gate now holds it\n \n **Five Blocks shipped a control that could not be acted on, and the previous release\n fixed three of them without the law.** `Hero01`, `Hero02` and `Hero03` each declared\n an action union whose `href`-less arm rendered a bare `Button`, and that release closed\n the arm and then said, in its own changeset, that seven sibling Blocks still had it and\n that \"a law held by a type in three Blocks is three types\". That was the right\n reasoning and it was also the reason the hole stayed open: the next audit found five\n more Blocks, and one of them had a shape nobody had looked for.\n \n **`About01` and `Showcase01`** declared the same two-arm union as the heroes and\n rendered a `Button` on the arm with no `href`. Each one's JSDoc called it \"inert by\n design\" and defended that as a Block shipping no behaviour, which is true and is not an\n answer. Both are now a union of two named arms: `AboutLinkAction` /\n `AboutSlotAction` and `ShowcaseLinkAction` / `ShowcaseSlotAction`. `href` is required\n on the link arm; the slot arm carries the caller's own `ReactNode` and forbids `label`,\n `href`, `newTab` and `variant`.\n \n **`PageHeader01` was worse, and its own type said so.** `PageHeaderAction` had **no\n `href` member at all**, so there was nothing to make optional: `{ label: 'Share' }` was\n the only value the type could express and every value rendered a `Button` that\n activated to nothing, at the top of every screen a consumer rendered. It is now the\n same two-arm union. Its `actionsSlot` sibling prop is **gone**, and that is the one\n structural change here: a sibling slot renders before the row's own actions and cannot\n say which position it fills, so a caller who wanted a primary menu trigger at the front\n and a link behind it had to put the link first and accept the order, or pass the trigger\n in `actions` where it was a dead button. As an arm, position and element are the same\n value.\n \n **`Pricing01` was the one no audit had found.** `Plan` carried a required `cta: string`\n and the Block rendered it as a `Button`, so every pricing table in every consumer's\n product shipped a focusable control, announced as a button, that activated to nothing, on\n the card a reader was about to decide on. `cta` is replaced by `action`, a two-arm union,\n and a plan whose control is a checkout trigger is the `slot` arm. The featured plan\n still draws a filled anchor and the rest an outlined one, so the recommendation still\n reads. `pricing-01/index.tsx` also exported the value alone until now, so a consumer\n could not name `Plan` at all; `Plan`, `Pricing01Props` and the three action types are on\n the surface.\n \n **`Plan` now carries a required `id`, which is the second change to one record and was\n deferred from this entry once.** The first version of this changeset recorded the\n observation and the reason for leaving it: *\"a second breaking change to a record this\n release already changes, and bundling two migrations into one entry makes both harder to\n read. It is a follow-up, not an oversight.\"* Both halves of that reasoning have since\n stopped applying, and the second one was never quite true.\n \n The `id` is real. `Pricing01` keyed its cards on `plan.name`, which is the word a reader\n reads on the card, so a monthly and an annual row of the same tier produced a React\n duplicate key and cards the renderer had been told are ambiguous, and a rename in the\n copy was a rename in the key, which loses a caller's saved selection over an edit to a\n sentence. `Careers01` and every other Block in this package key on an `id` and say why;\n `Pricing01` was the last one that did not.\n \n The bundling reason does not survive contact with the release. There is no released\n boundary between the two changes, because both are in this same unreleased body of work,\n so \"bundling\" describes nothing a reader would ever have to unpick: they upgrade once\n and read one entry either way. And one `Plan` with two migrations in one place is more\n readable than one `Plan` with two migrations in two places, because the second entry has\n to re-establish that it is talking about the same record. It is in this entry for that\n reason and the deferral was the mistake, not the delay.\n \n **`Waitlist01` is the fifth, and it is not a union.** `Waitlist01Referral` declared\n `copyLabel` and `copied`: the accessible name of a copy control and the state to draw on\n it. The Block then rendered `<Button type=\"button\">{copyLabel}</Button>` with **no\n handler**, beside a read-only field holding the code it was labelled as copying. The\n JSDoc described a second weight and a `data-copied` attribute for a control that never\n copied anything. Both props are gone and `copyControl` is a `ReactNode`. Prism will not\n call `navigator.clipboard` on a consumer's behalf and then report a success it cannot\n verify: the permission is the caller's to grant, the secure context is the caller's to\n know about, and a copy can fail. The sentence that says it worked belongs in `status`,\n which is where it already belonged.\n \n **The four Blocks whose union renders nothing on the `href`-less arm were already\n correct, and the audit that named them was wrong.** `Careers01`, `CaseStudies01`,\n `Industries01` and `Services01` each declare an item union whose no-link arm renders **no\n control at all**: the `CtaLink` is inside `isLink ? … : null`. That is the right shape\n for a card, where a link is an addition rather than a required part, and there is nothing\n to repair. `Gallery01` is the same story with a different reason: its tile is a real\n `<button>` with an `onClick` that opens the lightbox, in a `'use client'` Block. Five of\n the nine names in the earlier changeset were false positives, and saying so is part of\n the record: a list of sites is a claim about a tree, and the tree is what settles it.\n \n **`scripts/check-block-controls.mjs` is the gate, and it is green.** The previous release\n declined to write one because it would have failed on five real Blocks, and that was the\n right call at the time. It can be green now, so it exists. It reads `packages/ui/src/blocks`\n and `packages/ui/src/pages` and fails on a rendered `Button` or `CtaLink` that carries\n none of `onClick`, `type=\"submit\"` or `type=\"reset\"`, and no `href`. It deliberately does\n **not** read `apps/site/items`, because the documentation Demo for `Button` renders\n `<Button>Save changes</Button>` with no handler and showing the control is the whole\n point of showing it; the demos are held by the compiler, because a demo passes a Block's\n props and fails to build the moment `AboutAction` stops accepting a dead action. The\n gate's own proof is `scripts/__tests__/block-controls.test.mjs`, which plants each of the\n five shipped shapes as a fixture, because the five Blocks that shipped them have since\n been fixed and a gate whose only evidence is a clean run is a gate nobody has watched\n fail.\n \n Two things the first version of that gate got wrong are recorded because they are the\n kind of defect a clean run hides. It deleted comments rather than blanking them, which\n removed their newlines and printed every finding below the first JSDoc block on the\n wrong line. And it matched a tag's attributes with `<Name([^<>]*)>`, which stops at the\n first `<` or `>` in the attribute text, which in JSX is very often not the end of the tag:\n `onClick={() => step(-1)}` contains `=>`, `disabled={position <= 0}` contains `<=`, and\n `disabled={currentPage >= pageCount}` contains `>=`. Each truncated the attributes before\n the handler two lines further down, so three Blocks with working handlers were reported\n as shipping dead buttons. The tag end is found by brace and paren balance now.\n \n **The bump is `minor` rather than `major`, and the argument is the version line rather\n than the severity.** Five Blocks' public prop types change and two of them (`Plan.cta`\n and `Waitlist01Referral`) do not compile against a value that compiled a release ago.\n That is breaking by any ordinary reading and this entry says so plainly. `major` in this\n repository publishes `1.0.0`, and every release to date has been `minor` on a `0.y.z`\n line where the `y` is already the breaking-equivalent slot; publishing 1.0.0 with this in\n it would assert an API stability guarantee this library has not earned, and it would be\n the first release in the project's history to use the bump at all. The same argument\n carried the hero change and the `Cta01` change at 0.6.0. The break is real and is the\n subject of this entry; the number it lands on is the line's decision.\n \n Migration, per Block:\n \n ```tsx\n // About01, Showcase01, PageHeader01: an action with no href\n // Before\n actions={[{ label: 'Start free' }, { label: 'Docs', href: '/docs' }]}\n // After\n actions={[\n { slot: <NextLink href=\"/start\">Start free</NextLink> },\n { label: 'Docs', href: '/docs' },\n ]\n \n // PageHeader01 only: actionsSlot is gone, and the node moves into the row\n // Before\n <PageHeader01 actions={[{ label: 'New deployment' }]} actionsSlot={<Menu />} />\n // After\n <PageHeader01 actions={[{ slot: <Menu /> }, { label: 'New deployment', href: '/deployments/new' }]} />\n \n // Pricing01: `cta: string` becomes `action`, and the card is keyed on an `id`\n // Before\n plans={[{ name: 'Team', price: '$24', features: [], cta: 'Choose Team' }]}\n // After\n plans={[{ id: 'team', name: 'Team', price: '$24', features: [], action: { label: 'Choose Team', href: '/signup?plan=team' } }]\n \n // Waitlist01: the copy control is the caller's, whole\n // Before\n referral={{ value: CODE, label: 'Your referral code', copyLabel: 'Copy code', copied }}\n // After\n referral={{\n value: CODE,\n label: 'Your referral code',\n copyControl: <Button onClick={() => navigator.clipboard.writeText(CODE)}>Copy code</Button>,\n }}\n ```\n \n Every one of these was a compile error before and is a compile error now, which is the\n point: a consumer upgrading finds a failed build rather than a page that silently renders\n nothing.\n \n **One thing about the `Pricing01` migration is separate from the rest of it.** Every other\n line above changed a member a consumer had to add. The `id` also changes what a consumer\n has to add to the same object, so a plan that compiles after this release and renders two\n cards correctly is a plan that passed an `id`. Nothing here needs a second entry, and the\n observation that started this is in the entry above rather than at the end of this one.\n- 6dc7bb8: A disclosure opens at the step the motion scale gives it, and the rail says why it is still a width\n \n `DESIGN.md` has said for a while that `duration-slow` is \"transform or layout\n state such as a disclosure\". Three disclosures animated a height or a width at\n `duration-base`, which is 160ms against the 280ms the scale assigns them, and both\n chevrons rotated at `base` beside a panel that now takes longer than the icon does.\n \n **`AccordionContent`, `CollapsibleContent` and `Sidebar` are on `duration-slow`,\n and so are the two chevrons.** A chevron and the panel it opens are one motion, and\n a 160ms icon against a 280ms panel is two events where the reader is watching one.\n \n **`Progress` stays at `duration-base` and this is the one exception the scale has.**\n A disclosure is a spatial transition that happens once, on a reader's click, and\n 280ms is what makes it read as opening. A progress indicator's value changes on\n every tick of a running job, often several times a second, and the reader did not\n cause it: at 280ms the bar lags the work it is reporting. It is the same\n distinction the two laws of motion draw, applied inside the feedback scale. An\n answer wants the long end of the band; a reading wants the middle of it.\n \n **`Sidebar`'s rail still animates `width`, and it is a named exception rather than\n an unexplained gap.** `Progress` and `RangeField` both express their value as a\n `scaleX` about the inline start, and a rail cannot: it carries a mark, a list of\n items with a 16px icon, each item's label and a trailing count, and a `scaleX`\n scales every one of them into ovals, condensed labels and unreadable numbers. The\n decisive part is that a transform cannot reflow text. At `scaleX(0.25)` the labels\n are still laid out for a 16rem column, so the rail's contents would never reflow\n into the 4rem column they are supposed to occupy, and the only property that does\n that is the one being animated.\n \n The cost is stated rather than argued away: it is a main-thread layout pass, once\n per reader's click, over one element with a small subtree. That is a different\n order of cost from a bar that advances on every frame of a running job, which is\n why the two were not the same decision.\n \n `DESIGN.md`'s motion section records all of it, so the table and the call sites no\n longer disagree.\n- 6dc7bb8: A form-level error marks the field it is about, not every field\n \n `AuthForm01` set `aria-invalid` on every field whenever a form-level `error` existed,\n so a reader tabbing through a sign-in card heard \"invalid\" on a correctly filled\n email address because the password was wrong. A form-level message is usually not\n about a control at all: bad credentials, a locked account and a rate limit are three\n things no field is wrong about, and the first thing a screen reader says about each\n field is the state, so a reader told two correct answers are wrong stops trusting the\n rest of the form.\n \n `AuthFormField` gains `errorId`. The Block marks the field whose `id` the caller\n names there and no other, and a caller with nothing to name leaves every field\n unmarked, which is the honest state: the message is still drawn in an `Alert`, which\n is announced when it enters, and WCAG 3.3.1 is answered by the message rather than by\n a mark on a control that is not the problem. Omitting `errorId` on every field is now\n the way to say \"this is about the submission\", and the JSDoc says so.\n \n **A field's description was drawn and never announced.** The `FieldDescription`\n carried no id and the input carried no `aria-describedby`, so a hint about a format\n or a constraint was on the page for a sighted reader alone. The id is derived from\n the field's own `id`, which is already the one stable caller-owned string on the\n field, so the reference costs a template and cannot collide. `login-01` and\n `signup-01` already wired the pair; this was the Block that had not.\n \n **`SettingsNotifications01` had the same unlinked description.** Each event's\n `FieldDescription` was drawn under the control's visible label and referred to by\n nothing, while the section note was referred to by every read-only switch. The one\n `aria-describedby` now carries both: the event's own description and, on a read-only\n control, the note that says why it will not move. A reader who meets one without the\n other has to work out which half they are missing.\n \n Two notes for a caller. `AuthForm01` renders no `aria-invalid` on any field unless a\n field names itself, so a form that relied on every field being marked has to name the\n one that is. And `SettingsNotifications01` writes the joined pair as a template rather\n than through a `join`, which is the same reason `MultiCombobox` does: a bare\n separator literal is a string the copy gate reads as a space somebody typed.\n \n No prop is removed and no import breaks.\n- 97d9b99: A heading's size follows its level, so a page `h1` and its section titles are no longer one size\n \n **If you composed a page from Blocks, every section title on it just got smaller.**\n An `h2` section title moves from 1.875rem to 1.5rem, and above `sm` from 2.25rem to\n 1.875rem. A page `h1` is unchanged at 1.875rem, and 2.25rem above `sm`. Nothing\n about the props you pass changes, and nothing about the outline changes: the same\n `headingLevel` you already pass now decides the size as well as the tag.\n \n `DESIGN.md` gave Display two roles at once, \"section titles, the CTA banner\n heading, and every page `h1`\", and `SectionHeading` implemented that literally by\n writing one class string for all six levels. So an `h1` and an `h2` came out\n byte-identical, and a landing page of a hero plus six Blocks showed one `h1` and\n six section titles at 36 pixels, with no hierarchy between what the page claims\n and what it elaborates. The level was already on every Block as `headingLevel`\n and `childLevel()` already existed to carry it down a level, so the document\n decided where each heading sits in the outline and the visual size simply never\n followed it.\n \n **The table, and where it stops.**\n \n | level | step | value | above `sm` |\n | --- | --- | --- | --- |\n | `h1` | `text-3xl` | 1.875rem | 2.25rem |\n | `h2` | `text-2xl` | 1.5rem | 1.875rem |\n | `h3` | `text-xl` | 1.25rem | 1.5rem |\n | `h4` | `text-lg` | 1.125rem | 1.25rem |\n | `h5` | `text-lg` | 1.125rem | 1.25rem |\n | `h6` | `text-lg` | 1.125rem | 1.25rem |\n \n It is the authored scale walked down one step per level, and it floors at `h4`.\n `lg` is the deepest authored step that is not smaller than Body, which is 400 at\n 1.125rem, so a heading one step further down would render smaller than the copy it\n introduces and read as a caption. `h5` and `h6` hold at that step rather than\n wrapping, which is the same trade `childLevel()` makes when it clamps at `h6`. A\n heading at the floor is still a heading: it keeps `font-semibold`,\n `tracking-tight` and `text-balance` at every step, so weight and tracking tell it\n apart from body copy where size no longer can.\n \n **Nothing goes above `4xl`, and no scale changed.** The step above Display does\n not exist and this does not invent one, so `DESIGN.md`'s ceiling holds untouched\n and `@nanisoft/prism-tokens` is unchanged. `h1` is Display because that is the\n role the document gives it, and `h2` is the step below.\n \n **No new prop.** The alternative was a `size` on `SectionHeading` and on every\n Block that renders one, which widens the public surface across more than a\n hundred call sites for a decision the surrounding document has already made by\n choosing a level. You get the hierarchy by passing the level you were already\n passing, and a Block moved from an `h2` section into an `h3` one carries its size\n with it, which is what `headingLevel` was introduced to do. Nothing about the\n no-override-path rule changes: a consumer who wanted Display for their thesis\n still has it, at `h1`.\n \n **`Cta01` follows its level too, and this is the one Block outside\n `SectionHeading`.** It draws a centred title on a filled primary panel with\n nothing under it, so the muted description colour and `gap-4` are wrong there and\n it resolves its own heading element. It was carrying the same hardcoded size, so\n fixing the Component and not it would have left a closing banner one step above\n every other section title on the page and level with the page's own `h1`. Its\n banner moves with everything else: an `h2` closing banner is 1.5rem, and at `h1`\n it is 1.875rem.\n \n **The blast radius, measured rather than guessed.** 132 `SectionHeading` render\n sites across 115 files resolve to this table. 6 are at `h1` and are unchanged.\n 125 are at `h2` and each moves from `text-3xl sm:text-4xl` to\n `text-2xl sm:text-3xl`. 1 is at `h3` and moves to `text-xl sm:text-2xl`. That is\n the whole change, and it is deliberately not softened: a documentation site\n whose own prose hierarchy shifts is a visible change, and the point of the fix is\n that the shift is the hierarchy coming back.\n \n **A gate now holds the table, and proof it fires.**\n `scripts/check-heading-scale.mjs` reads the table out of `section.tsx`, judges\n every step in it against the `text` group in the token source rather than a list\n beside the gate, and holds it against the markdown table the Component's own\n JSDoc states, which is the documentation source the declaration build preserves\n and the corpus reads. It fails on a level with no size, on a step the token source\n does not author, on `h1` and `h2` sharing one step, on a step-down table that\n rises, on a level deeper than the page heading rendering above it, on a floor below\n Body, on the heading losing its weight, tracking or balance, on the JSDoc and the\n code disagreeing, and on `DESIGN.md` giving Display the section title as well as\n the `h1`. `scripts/__tests__/heading-scale.test.mjs` stages each of those against\n a tree the gate reads, including the table the Component actually shipped before\n this change, and asserts the gate is red on it.\n- 6dc7bb8: A hero action is a destination or your own control, and never an inert button\n \n `Hero01`, `Hero02` and `Hero03` accepted an action of `{ label }` with no\n destination, and each Block rendered that as a bare `Button`. Every one of those is\n a focusable control, announced as a button, that activates to nothing, and it sat\n where a reader looks first: the primary call to action of a marketing hero, in\n three published Blocks, so every consumer of any of them shipped a dead button\n where the most important action should have been. Each Block's JSDoc called the\n button \"inert by design\" and defended that as a Block shipping no behaviour, which\n is true and is not an answer. A Block that ships no behaviour cannot make a control\n work, so it should not render a control at all.\n \n **The defect was in the type, not in the render, so the fix is a type.** The\n rendering half had already been repaired once: `CtaLink` renders a native anchor\n and `Cta01` requires `href`, which is why the three heroes, predating both, kept\n the old shape. What survived was the third arm, and it could only be closed by\n removing it.\n \n `HeroAction`, `Hero02Action` and `Hero03Action` are now a union of two named arms\n rather than one shape with an optional `href`:\n \n - `HeroLinkAction` (and its two siblings) **requires** `href`, so a value that is\n sometimes a string and sometimes `undefined` is a compile error rather than a\n control that navigates on the renders where the address happens to be there.\n `slot` is forbidden on this arm.\n - `HeroSlotAction` (and its two siblings) **carries `slot`**, the caller's own\n control, and forbids `label`, `href`, `newTab` and `variant`. It exists because\n a server Component cannot receive an `onClick`, so the honest form of \"this does\n something\" in a composed section is a node the caller renders. This is\n `Cta01`'s `actionSlot` moved inside the row, because a hero carries an ordered\n list of up to two actions and a sibling prop cannot say which position it fills.\n `check-block-imports.mjs` already holds that a `ReactNode` slot is how a consumer\n injects an interactive child without the Block owning any state.\n \n The Block places a slot and adds no class to it, because a class it adds to a node\n it does not render is a style the caller cannot see and cannot remove, and this\n package has no override path.\n \n **Nothing else moved.** The anchor is the element the link arm always produced, at\n the same size and the same variant, and the positional default variant still\n follows the position: the first action in the row is filled and the rest are\n outlined, whether the first one is an anchor or a caller's control. The forward\n arrow still marks the row's one primary destination and now, with every action\n Prism renders being a link, the position is the whole of the test. A `slot` at the\n front of the row wears no arrow, because the control the caller drew owns its own\n marks. Each Block is still a server Component.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** `{ label: 'Coming soon' }` compiled a release ago and\n does not now, which is breaking by any ordinary reading, and this entry says so\n plainly. `major` in this repository publishes `1.0.0`, and every release to date\n has been `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent\n slot. Publishing 1.0.0 with this change in it would assert an API stability\n guarantee this library has not earned, and it would be the first release in the\n project's history to use the bump at all. The same argument carried the `Cta01`\n change at 0.6.0. The break is real and is the subject of this entry; the number it\n lands on is the line's decision and the line is pre-1.0.\n \n Migration: every action in a hero now carries either a destination or your own\n control. Where you passed a label alone and meant a link, give it the `href` it\n was always declared with. Where you meant a control that is not a link, a router's\n own `Link`, a submit button or a menu trigger, move it into `slot` as the element\n itself rather than as a label:\n \n ```tsx\n // Before\n actions={[{ label: 'Start free' }, { label: 'Sign in', href: '/sign-in' }]}\n \n // After\n actions={[\n { slot: <NextLink href=\"/start\">Start free</NextLink> },\n { label: 'Sign in', href: '/sign-in' },\n ]}\n ```\n \n Both mistakes are now compile errors, which is the point: a consumer upgrading\n finds a failed build rather than a site that silently renders nothing.\n \n **The same arm was still in seven sibling Blocks when this was written, and\n `a-block-action-is-a-destination-or-your-own-control.md` closes them.** The audit\n behind this entry named `About01`, `Careers01`, `CaseStudies01`, `Gallery01`,\n `Industries01`, `Services01` and `Showcase01` as carrying the same two-arm union with\n a rendered `Button` on the arm that has no `href`, and `PageHeader01` as rendering\n its declared `actions` as buttons with no destination at all. **Five of those nine\n were correct and four were defective, and the later entry records which is which**:\n `About01` and `Showcase01` had the union, `PageHeader01` was worse and had no\n destination member at all, and `Pricing01` and `Waitlist01` were not on the list at\n all. `Careers01`, `CaseStudies01`, `Industries01` and `Services01` render **nothing**\n on their `href`-less arm, which is the right shape for a card, and `Gallery01`'s tile\n is a real button with an `onClick` in a client Block.\n \n This entry stands as the record of the heroes. The shape every Block now takes is the\n one adopted here, and `scripts/check-block-controls.mjs` is what holds it: the earlier\n reasoning in this paragraph, that \"a law held by a type in three Blocks is three\n types\", was right about the risk and wrong about the remedy, and the remedy is a gate\n rather than a shared type.\n- 6dc7bb8: A popup that matched nothing is still an open popup, and its message is not an option\n \n Four Components said \"collapsed\" while a popup was on the page, and two of them put\n the sentence saying so inside the listbox.\n \n `aria-expanded` was reading \"are there results\" rather than \"is the popup displayed\",\n and the two came apart in exactly the state a search field exists to render: a query\n that matched nothing. `CommandPalette` and `Combobox` both drew a bordered panel\n carrying the caller's empty sentence and told the field there was nothing to reach.\n `aria-expanded` is now the open state in all four, because that is what the\n attribute is for and because closing on a non-match tells the reader their keystroke\n broke the control, which `Combobox` already argued in prose.\n \n `aria-controls` pointed at a listbox that was not rendered. `Combobox`,\n `MultiCombobox` and `CreatableCombobox` omit the listbox entirely when nothing\n matched and left the reference written anyway, so the field pointed at an id nothing\n carried. **It now points at the popup rather than at the listbox inside it**, which\n is the decision the existing axe run forced and the right one on its own terms:\n `aria-controls` is a required attribute on an expanded `combobox`, so a field that is\n expanded and carries no reference is itself a violation, and the panel is what the\n field opened either way. `CommandPalette` points at the palette itself for the same\n reason. All four write the reference only while the popup is displayed, so a closed\n field carries none.\n \n **A listbox owns `option` and `group` and nothing else.** `CommandPalette` drew its\n no-results `<p>` inside the listbox and its group headings in a bare `<div>`, so the\n message was a row the index counted and no reader could choose, and the rows under a\n heading could not say what they belonged to. `CreatableCombobox` drew its\n `<p role=\"status\">` in the same place. In all three the empty state is now a sibling\n of the listbox rather than a child of it, which is the arrangement `Combobox` and\n `MultiCombobox` already shared, and each group wrapper is a `group` named by the\n heading already drawn above it through `aria-labelledby`.\n \n `CommandPalette` gains two things to know about: the two branches carry the same box,\n so the panel does not change size or position when the last result is filtered out,\n and its listbox now appears only when there is something in it, so a caller selecting\n `[data-slot=\"command-palette-list\"]` finds nothing in the empty state. The empty\n state carries `data-slot=\"command-palette-empty-state\"` on its box and the message\n keeps `data-slot=\"command-palette-empty\"`.\n \n No prop changes and no import breaks.\n- 6dc7bb8: `Prose` gives a table a scroll container, and the two search fields stop zooming an iPhone\n \n Four defects, three of them narrow-viewport, one of them about a reader's own\n settings. A consumer sees all four.\n \n **A table in a Prose widened the page instead of scrolling.** The Component's\n JSDoc has always listed tables among the content it accepts, and the code fence\n beside them has always scrolled, but the table itself could not: `overflow`\n applies to block containers, and a `display: table` element is not one. A table\n box cannot be a scroll container, so a four-column table in a Markdown document\n pushed the whole page sideways rather than offering a scrollbar.\n \n A `<table>` passed straight to a Prose now gets `display: block`, which is what\n makes it a block container, and the same `overflow-x: auto` the code fence has.\n The grid inside now sizes to its content up to the measure rather than stretching\n to fill it, which is the trade the technique always carries and is the one worth\n knowing about before you rely on it. Collapsed borders are unaffected: the row\n groups are wrapped in an anonymous table box that still inherits\n `border-collapse`.\n \n The treatment is a child selector rather than a descendant one, on purpose. A\n descendant selector would reach a `Table` you have already put in a scroll\n container of your own and set `display: block` on the table inside it, which\n moves the caption off being a caption and stops the grid filling its wrapper. A\n table you have wrapped in your own element is left alone as it was; give that\n element `overflow-x-auto` and it is what scrolls. For a table whose markup you\n control, `Table` remains the right answer.\n \n **`SearchDialog` and `CommandPalette` zoomed the page on focus.** Both drew their\n search field at 14 pixels. iOS Safari zooms the viewport on a focused input whose\n font size is under 16 pixels and does not zoom back out, so a reader who opened\n either surface on a phone was left on a magnified page with no way off it. Both\n now carry the `text-base` then `md:text-sm` pair that `Input`, `Textarea`,\n `Combobox`, `Form` and `NumberField` already carried, which is the same 16 pixels\n at a phone's width and the same 14 everywhere else.\n \n **The bump is `minor` rather than `patch`.** A table in a Prose renders\n differently: a narrow one no longer stretches to the measure. That is a visible\n change to a published Component rather than a fix nobody notices, and a site that\n was relying on a prose table filling its column is the one thing that needs to\n look at it.\n- 6dc7bb8: A tree is one Tab stop, and no node claims to collapse\n \n `Tree` documented a mechanism it did not have and shipped a state it could not\n honour. Its JSDoc said \"a tree is one Tab stop, and the arrow keys move inside it.\n That is the ARIA pattern\" and then described the roving tabindex that makes it true,\n and no `tabIndex` appeared anywhere in the file: a forty-node rail was forty Tab\n stops. A comment in the same file argued the other way, and it was wrong on its own\n terms, because it deferred the keyboard model to \"a caller's own keyboard model\" in a\n Component that does not forward `onKeyDown` and therefore has no caller's model to\n defer to.\n \n The roving tabindex is now implemented, in the shape `ToggleGroup` already uses: the\n stop is an address rather than an index, so a caller who reorders or removes nodes\n does not strand the reader; it lands on the current address when the tree has one and\n on the first destination otherwise; and it follows the reader once the arrows move\n it, so Tab away and back returns them to where they were. The fallback and the\n current address are not two answers: a `currentHref` naming another page is ordinary,\n and reading either rule alone would give the tree two tab stops.\n \n **Nothing collapses, so nothing claims to.** Every group with children carried\n `aria-expanded=\"true\"`, hard-coded, which promises a second press that folds nothing\n away. A node that cannot expand omits the attribute, and `aria-level` is what places\n a node in the tree for a reader who wants that instead.\n \n **Two regions announced their bare role name.** `SelectionToolbar` drew the words\n \"3 selected\" in its leading label and pointed at nothing, so a reader tabbing onto\n the row heard \"toolbar\" and no more; it is now named by `aria-labelledby` on the\n label, which works for the `ReactNode` label the count requires and cannot drift from\n the words on screen. `ToggleGroup` declared `aria-label` optional while its own JSDoc\n said the prop was required, so TypeScript enforced nothing and a group shipped\n unnamed in both of its roles. See the separate entry for the type change.\n \n To know before you style against it: `Tree` now writes a `tabindex` on every\n destination, so a page with two trees has two Tab stops where it had one per node.\n- 6dc7bb8: `Button` is `type=\"button\"`, the carousel leaves the caret keys, and a number field takes one\n \n **`Button` defaults `type` to `\"button\"`.** A `<button>` with no `type` is a submit\n button as far as the HTML is concerned, so the default was that every Prism `Button`\n a consumer placed in their own form submitted it: a Cancel, a Close, a second step of\n a wizard, anything that was not the one control that should. The failure is silent\n and it costs data, which is why the default is stated on the Component rather than\n left to the HTML.\n \n Nothing in this repository changed, and that is worth saying with the evidence rather\n than as an assurance: every `<form>` in `packages/ui` and `apps/site` already states\n its submit button's `type`, which is why no call site moved. The one behaviour change\n is for a consumer who relied on the old default, and the migration is one token:\n \n ```tsx\n // a control that used to submit by default\n <Button>Save</Button>\n \n // now\n <Button type=\"submit\">Save</Button>\n ```\n \n The trade is deliberate. A submit button is a decision someone makes about a form and\n this Component cannot see the form, so making the destructive default the safe one\n and the deliberate act the explicit one is the only arrangement where the mistake is\n the one you have to write on purpose. `ToggleGroupItem`, `MultiCombobox` and\n `SelectionToolbar` already wrote `type=\"button\"` by hand for the same reason.\n \n **`Carousel` answered the arrow keys for whatever was inside a slide.** The key\n handler sits on the region, so every key that bubbled out of a slide reached it, and a\n slide is the caller's slot: it can hold a field, a number field, a `select` or an\n editable region, and in all four Left, Right, Home and End belong to the value. The\n Component was calling `preventDefault()` on them unconditionally, so a reader typing\n a caption moved the carousel instead. It now answers a key only when nothing inside\n is using it, which is the discipline `ResizableHandle` applies to the same keys on\n the other axis. A reader arriving on the carousel or on one of its own controls is\n unaffected.\n \n **`NumberField` documented a route to the unit that its props did not carry.** The\n drawn unit is `aria-hidden`, on the reasoning that a screen reader already says the\n unit when the caller puts it in the field's description, and there was no\n `aria-describedby` on the props to put it in, so a value of 1,250 was announced as\n 1,250. The prop is there now and is forwarded to the input. The unit stays hidden: a\n reference already says it once, and two announcements of the same word is one too\n many. The unit belongs in the description and not in the accessible name, because\n `aria-label` is optional, so a field with a real `<label>` has no name to put it in,\n and a name reading \"Parcel weight, kg\" against a visible label reading \"Parcel\n weight\" is what WCAG 2.5.3 forbids and what voice control cannot activate.\n \n `aria-describedby` is an addition, so nothing breaks. `Button` and `Carousel` change\n behaviour, which is why they are here rather than described as a patch.\n- 6dc7bb8: Coarse-pointer controls take the 44px floor, and the focus gate reads the whole package\n \n Thirteen controls were drawing targets between 16 and 36 pixels with nothing done\n about it on touch input, so a consumer composing the bare primitive shipped a\n target that fails WCAG 2.2 SC 2.5.8 and falls short of this package's own 44px\n standard. All of them are fixed, and **the desktop metrics are untouched**: a mouse\n and a trackpad see exactly what they saw before, and every change is inside\n `@media (pointer: coarse)`.\n \n `Checkbox`, `RadioGroupItem`, `Calendar`'s day cells and paging controls,\n `Dialog`'s trigger and close control, and `SearchDialog`'s close control grow to\n 44 by 44. `Slider`'s and `RangeField`'s thumbs, `Switch`, and `DatePicker`'s clear\n control take a transparent 44 by 44 band around the control instead, because\n growing them would draw something the design does not have: a 44px ball on a six\n pixel rail, a switch that is not a switch. The band is a pseudo-element, so the\n box the value is computed from is the box you see.\n \n **`NumberField` turns its two steppers side by side on a coarse pointer.** This is\n the one place the floor changes an arrangement rather than a size. Two 44px targets\n stacked in the split column need an 88px field, and two 44px bands on two 18px rows\n overlap so much that the lower stepper takes the boundary between them, so a press\n aimed at increment would step down.\n \n **`Calendar` now matches `MiniCalendar` on the same controls.** The identical\n control in one package at two target sizes is the defect, not the 32px.\n \n Three things to know before you style against these:\n \n - A `Calendar` on a phone is six rows of 44px rather than six of 36px, so the panel\n is taller. It is the densest control in the package and the one a finger is least\n able to aim at.\n - `Checkbox` and `RadioGroupItem` draw a 44px bordered box on touch input, and the\n row, column or field around them grows to hold it. That is the price\n `tag-group.tsx` already records for its chips.\n - On a `Slider` and a `RangeField`, a press inside a thumb's band drags the thumb\n from where it was rather than jumping the value to where you pressed. The arrow\n keys and the range input behind the track still reach every value. Two bounds\n closer together than the band is wide are dragged by the one used last until Tab\n moves to the other, so give a range a `minGap` a finger can pinch apart.\n \n `DESIGN.md` states both patterns and the three conditions that decide between them.\n \n **The focus-indicator gate read 132 of the package's 400 source files.** It read\n `components/ui` and nothing else, so it saw no Block, no Page, neither `live`\n surface and no provider, and it skipped any file whose JSDoc made no keyboard claim\n before reading a single class string in it. It now reads `components`, `blocks`,\n `pages`, `live` and `provider`, recursively, and judges every class string in the\n scope. It does not read `apps/site`: that is another package with its own `check`\n chain.\n \n **Its exclusion for menu and listbox options was a suffix match, and is now a\n roster.** `/(?:-item|-sub-trigger)$/` is a statement about spelling rather than\n about menus, and over the Blocks it swallowed `radio-group-item`,\n `toggle-group-item`, `accordion-item`, `breadcrumb-item`, `tree-item` and about\n thirty more, every one of which is an ordinary focusable control.\n \n **Two scanning defects are fixed, and both were making the gate report green over\n a file it had not read.** A block comment inside a `cn()` call held an apostrophe,\n which the scanner read as an opening quote and which hid every class after it; and\n a module-level `const` was sliced to the end of its file rather than to the end of\n its value, so a menu item read as carrying a full-strength ring that belonged to a\n component further down the same module. Both are proved by fixtures that fail\n without the fix. The shipped tree has no genuine findings; a planted one fires in a\n Block and in a file whose JSDoc makes no claim, which is the negative control that\n makes the clean run mean something.\n \n **The bump is `minor` rather than `patch`.** No prop, import or rendered element\n changes, and no desktop metric changes, so nothing breaks a call site. It is\n `minor` because thirteen controls now measure differently on a coarse pointer, and a\n consumer who styles any of them has to know that: a `Calendar` panel is taller, a\n `Checkbox` box and a `RadioGroup` column are bigger, and a `NumberField` lays its\n steppers out differently. That is a visible metric change on the platform most of\n these controls are used on, and describing it as a patch would describe something\n nobody sees.\n- 6dc7bb8: `DocsShell` lays out three columns from `lg` rather than two\n \n The documentation frame's third track named a screen the emitted theme closes, so\n the class compiled to nothing and the contents rail fell into an implicit `auto`\n track: two columns at every width from 1024 pixels, with the article column about\n 340 pixels wide at 1024, on this site and in all four consumers. The rail is\n 15rem, the document takes the rest, and the contents rail is 13rem, all from `lg`,\n which is where both rails already appeared. No prop, import or rendered element\n changes, and the third track still exists only when you pass `toc`.\n \n **`check-breakpoint-variants.mjs` is new, and it is a gate rather than a fix.** It\n reads every responsive variant out of the class strings in the component package\n and the site and compares it to the screens the token package emits, read from\n `layout.tokens.json` and from the build that closes `xl` and `2xl`. A class\n written against a screen the theme does not have is now a finding with a file and\n a line, and every other gate was green through the one above because each of them\n held the value rather than the class.\n- 6dc7bb8: Nineteen Blocks name their table, and the name is the heading they already draw\n \n A `<table>` is named by a caption, an `aria-label` or an `aria-labelledby`, and by\n nothing else. A heading that happens to sit above it does not name it: no assistive\n technology derives a table's accessible name from a neighbouring heading. Nineteen\n Blocks drew a real `<table>` under a caller-owned `SectionHeading` and gave the table\n no name at all, so a reader listing the tables on a page found nineteen anonymous\n entries while every element around them was named. `Table`'s own JSDoc asks for one\n of the three.\n \n A Block cannot compose the words itself, so each one takes the name from the heading\n it already draws, by reference. `SectionHeading` gains an optional `id` for the\n handle; a generated id would be no handle at all, since nothing could be written\n down to point at it. No new prop on any Block, no string shipped, and no caption\n drawn: a visible line repeating the heading is noise, and a reference cannot drift\n from the heading it names.\n \n **Three of them write the reference only while the heading is drawn.** `Compare01`,\n `PricingCompare01`, `RateCard01` and `DataTable01` render their heading only when the\n caller passed a `title`, so an unconditional reference would point at an element that\n is not on the page, which is the defect this same release removed from\n `CommandPalette`'s listbox. In the one state where a caller passed no title the table\n is therefore unnamed, which is the honest trade and the caller's own choice: the\n alternative is a reference to nothing.\n \n `DataTable01` also has a `caption` prop, and where a caller passes one the caption\n wins, because a reference outranks a `<caption>` in the accessible name algorithm.\n Writing both would quietly make the heading the name and the caller's own words the\n thing only read on request. `Gantt01` already captioned its table and is unchanged.\n \n The rule is held by a test rather than a gate, and the reason is in that test's\n header: whether a table is named is a runtime property, because the reference has to\n resolve in every state a caller can reach. A source scan would only prove the\n attribute is written, and this repository does not accept a report-only gate.\n \n No import breaks and no prop is removed.\n- 6dc7bb8: `OverflowActions` and `ResizableHandle` read layout when it can have changed\n \n Neither Component's rendered output changes for a caller who composes them the way\n their own documentation says. What changes is how often each of them asks the browser\n to settle layout, and a table of rows or a page of dragged panes pays that on the main\n thread in the middle of a reader's work.\n \n `OverflowActions` measured itself after every render of every row, reading the row's\n own box, the cap and every drawn action, and writing the answer back as state. **A\n pass now runs on mount, when the actions' ids or their labels change, when `className`\n changes, when the row's own box changes, and when the page's fonts have finished\n loading.** Passing a fresh `actions` array on every render now costs nothing, which is\n the ordinary case in a table cell under a filter box. **The fonts are in the list\n because a self-hosted face swaps in after the first paint and changes the width of\n every drawn action without changing the row's width**, so nothing else reported it, and\n a row measured in the fallback face was a row whose widths were never true of the page\n the reader was looking at.\n \n `ResizableHandle` read the group's box on every `pointermove`, immediately after the\n frame before had written the new position and therefore written new styles: one forced\n synchronous layout per frame of every drag. **The box is read once at `pointerdown`,\n and every frame after that is arithmetic.** A drag measures from the press rather than\n from the divider's own box, which is what a divider a pixel wide and a reader takes\n hold of wherever their pointer lands requires, and it did that before.\n \n Two cases are pinned rather than handled, and both are written down in the JSDoc and\n the site page:\n \n - A group resized by something else part way through a drag finishes that drag against\n the travel it started with. A caller who needs a drag that follows a changing group\n ends that drag and starts it again.\n - `OverflowActions` cannot see a change that leaves both the row's box and its drawn\n content exactly as they were: an ancestor's font size changing, or an action's mark\n swapped for a different-width one while that action is drawn. Both used to be\n corrected by the row's next render and neither is now.\n \n The bump is `minor` because those two are behaviour a consumer can observe, even though\n no prop, import or rendered element changes.\n- 6dc7bb8: There is one container namespace, and `max-w-6xl` no longer resolves\n \n **If you wrote `max-w-6xl`, `max-w-2xl`, `max-w-xl`, `max-w-lg`, `max-w-md`,\n `max-w-sm` or `max-w-5xl` against this package, that class now produces no rule at\n all.** Nothing errors and nothing warns: the element simply loses its cap and lays\n out at whatever its parent gives it. Grep your own repository for `max-w-` before\n you upgrade. The replacements are in the list below and every one of them resolves.\n \n The reason is the defect this release ends. `@nanisoft/prism-tokens` authors three\n container widths and Tailwind ships thirteen of its own, and until now both shipped\n in the same stylesheet. Three of Tailwind's were numerically identical to three of\n ours: its largest at 72rem beside `--container-page`, its fifth at 42rem beside\n `--container-measure`, its fourth at 36rem beside `--container-measure-narrow`.\n Nothing rendered differently, so nothing failed and every gate was green. What it\n cost was that one width had two names, and the first retune of an authored token\n would have moved every surface reaching it by one spelling and left every surface\n reaching it by the other exactly where it was. `Section`, `SiteHeader`,\n `SiteFooter`, `ChartCard01`, `DocsShell` and `QuickView01` all used the Tailwind\n spelling of the page column; `Cta01` and `SectionHeading` used the Tailwind\n spellings of the two measures.\n \n The token build now closes the whole namespace, with\n `--container-*: initial` ahead of the authored entries rather than a list of the\n seven steps that happened to be emitted. A wildcard is the load-bearing word: the\n thirteen names it retires are `3xs 2xs xs sm md lg xl 2xl 3xl 4xl 5xl 6xl 7xl`,\n and a list would have been a second list to keep in step with a dependency's\n default theme, which is the event the line exists to survive. Order matters and is\n asserted: Tailwind resolves a theme in source order, and the same declaration\n written after the authored entries clears all eight of ours and ships no container\n at all.\n \n **What to write instead**\n \n | You wrote | Write | Value |\n | --- | --- | --- |\n | `max-w-6xl` | `max-w-page` | 72rem |\n | `max-w-2xl` | `max-w-measure` | 42rem |\n | `max-w-xl` | `max-w-measure-narrow` | 36rem |\n | `max-w-md` on a dialog or an alert | `max-w-overlay-dialog` | 28rem |\n | `max-w-lg` on a form dialog | `max-w-overlay-form` | 32rem |\n | `max-w-xl` on a command palette or a search | `max-w-overlay-palette` | 36rem |\n | `max-w-sm` on a side sheet or a drawer | `max-w-overlay-panel` | 24rem |\n | `max-w-5xl` on a lightbox | `max-w-overlay-media` | 64rem |\n \n **An overlay's width is a property of the kind of surface it is, not of the page\n grid underneath it**, and that is why the five overlay widths are authored rather\n than carried on a Tailwind name. A retune of the reading measure must not move a\n dialog and a retune of the page column must not move either, so the two families\n are named apart inside the one container group. They share a group because\n Tailwind 4's `max-w-*` resolves `--spacing-*` and `--container-*` and nothing\n else, so a maximum width that is to be a token has to live in one of those two\n namespaces or it is a value written in a Component. `overlay-palette` and\n `measure-narrow` are both 36rem; that is two measurements that happen to agree and\n not one measurement with two names, and the token source says so and the emitted\n contract holds it to saying so.\n \n **Nothing is supposed to change where you can see it.** Every migration above\n preserves the exact rendered width, so a page that only consumed this stylesheet\n looks the same before and after. What changes is the next retune: it now moves one\n surface rather than half of them.\n \n **A width that was arithmetic on the spacing base is left alone, deliberately.**\n `max-w-96`, `max-w-72`, `max-w-64`, `max-w-32` and `max-w-20` resolve to\n `calc(var(--spacing) * N)` and never touched the container namespace at all. They\n are how a chart's axis band, a token table's column and a documentation frame are\n sized, where nobody took a decision and naming one would invent it. They stay.\n \n **Two gates now hold this down, because the question has two sides.**\n `scripts/check-elevation-layout.mjs` reads every `w-*` and `max-w-*` name out of\n the class strings and holds it to the container group read out of the token source,\n so a width nobody authored is a finding with a file and a line; it reads\n `apps/site/items` as well as `apps/site/src` and `packages/ui/src`, because that is\n where the documentation Demos live and where the site's own build scans.\n `packages/ui/scripts/check-container-namespace.mjs` reads the built stylesheet and\n holds the artefact: that the theme block declares exactly the authored containers,\n that none of Tailwind's thirteen steps is declared or read, that the close is\n written ahead of the entries rather than after them, and that every container width\n this package writes is emitted as a utility reading its own variable. It reads the\n step list out of the installed `tailwindcss/theme.css` rather than restating it, so\n a dependency that adds a step is covered without an edit. Tailwind's extractor\n reads this package's comments as well as its code, so a retired class name written\n down in a JSDoc block used to emit a utility; there is now a rule about that too.\n \n **The bump is `minor` rather than `major`, and the argument is the version line\n rather than the severity.** A class a consumer wrote stops producing a rule with no\n error, which is breaking by any ordinary reading, and this entry says so plainly.\n `major` in this repository publishes `1.0.0`, and every release to date has been\n `minor` on a `0.y.z` line where the `y` is already the breaking-equivalent slot.\n Publishing 1.0.0 with this change in it would assert an API stability guarantee\n this library has not earned, and it would be the first release in the project's\n history to use the bump at all. The break is real and is the subject of this\n entry; the number it lands on is the line's decision and the line is pre-1.0.\n \n **One collateral repair on the documentation site, recorded here because it is\n why `globals.css` moved.** The site's own build and this package's meet in one\n `utilities` layer, and this package emits a bare `.grid-cols-6` as the base of the\n `lg:` variant four of its Blocks use. That bare rule landed after the site's\n `sm:grid-cols-11`, so every colour ramp on the Foundations page was drawing six\n columns at every width above 640 against a comment beside the class saying\n eleven. Whether `check-utility-cascade.mjs` saw it depended on which order two\n content-hashed CSS chunks sorted in, which this change perturbed and which nothing\n about the tree controls. Neither side is wrong, so the site's variant is restated\n in the `site-variants` layer that already exists for six other collisions. The\n repaired behaviour is on the documentation site only; nothing in this package\n changes.\n- 6dc7bb8: Ship the interface face and its metric-adjusted fallback, and stop the site loading its own\n \n `@nanisoft/prism-ui` shipped three Inter weights and no fallback face, so a reader\n waiting on the web font saw the platform's UI face jump into place when Inter\n arrived. The stylesheet now also ships a local Arial carrying Inter's metrics\n through `size-adjust`, `ascent-override`, `descent-override` and\n `line-gap-override`, and `--font-sans` names it immediately after Inter, so the\n swap window occupies Inter's own line box. The four numbers are measured off the\n shipped face rather than chosen, and they are the ones `next/font` measured for the\n same typeface, so the line box is unchanged from what the documentation site had.\n \n A fourth face ships beside them: Inter italic at weight 400, the Latin subset of\n the same release, covering the same 230 codepoints as its upright siblings.\n `Prose` sets `blockquote` in italic and the surface had been asking for a style no\n shipped file provided, so every consumer has been reading a synthesized oblique.\n One real face covers every weight a browser asks for, because the font matcher\n takes the closest available weight rather than synthesizing once a face exists.\n The whole set is 93.5 KB, up 22.8 KB, and no consumer has to do anything.\n \n **The documentation site no longer loads a face of its own.** It shipped a 723 KiB\n unsubsetted variable Inter and its italic through `next/font/local`, preloaded both\n in all 581 exported documents, and applied the result to `<body>` as a directly set\n `--font-sans`. A directly applied custom property outranks an inherited one, so the\n library's faces were never fetched on the site that documents them: a missing or\n corrupt shipped font rendered perfectly there with every gate green. The site now\n resolves the same stack you do, which is the only way it can be evidence for the\n package. If you were relying on the site to look right while your own copy of Inter\n was broken, that is now a failure you can see.\n- 6dc7bb8: `Progress` announces a value text again, and `getAriaValueText` works\n \n Every `Progress` in every consumer was announcing a bare number. `aria-valuenow`\n carried the position and `aria-valuetext` was absent from the tree altogether, so\n the sentence a screen reader says for a bar was a number with nothing to say what\n range it was a number in. A bar with `value={null}` was worse than bare: with no\n `aria-valuenow` to fall back on, its entire value was nothing.\n \n **What changed.** `aria-valuetext` is now passed only when you pass `valueText`, so\n the default that Base UI computes survives. You get it for free:\n \n - a determinate bar announces its value as a percentage of the range, so\n `value={68}` is announced as `68%`;\n - a bar of unknown length announces `indeterminate progress`.\n \n **`getAriaValueText` was dead and is not any more.** It was called on every render\n and its return value was then discarded, so a consumer who wrote one was paying for\n it and hearing nothing. It receives the formatted percentage first and the raw\n `value` second, unchanged.\n \n **`format` and `locale` had no observable effect either, and now do.** Both exist\n only to shape the announced string, so with the attribute absent there was nothing\n for them to shape. One trap worth knowing: `format` is applied to the raw value on\n the scale, not to the share of it, so `format={{ style: 'percent' }}` on a\n zero-to-hundred range reads as `4050%`. The unformatted default is the only path\n that asks for a percentage of the fraction.\n \n **Nothing you passed changes meaning.** An explicit `valueText` still wins, an\n `aria-valuetext` written on the Component itself still wins over both, and a bar\n given both `valueText` and `getAriaValueText` still throws. What a consumer hears\n on a bar that passes neither of them is the whole of this change, and it is a\n change to every bar a consumer that passes neither already has.\n \n The bump is `minor` because it changes what a screen reader says in every\n composition of this Component. No prop, import or exported name changes.\n- 6dc7bb8: `Progress` expresses its value as a transform rather than as a width\n \n The fill was transitioned with `transition-[width]`, and `width` is a layout\n property, so every frame of every advancing bar made the browser settle layout on\n the main thread. `Progress` is one of the two or three most-composed Components in\n this package, which is what makes it worth a release of its own: the cost was not\n one bar on one page, it was every bar on every page, in every consumer that\n composes one.\n \n **The indicator is now as wide as the track at every value and is scaled along the\n reading axis.** `aria-valuenow` still comes from `ProgressRoot` and still reports\n `value`, `min` and `max`; the scale factor is the same three numbers read a second\n time, and the test suite holds the two to each other by asserting the transform\n against the announced value rather than against a copy of the arithmetic.\n \n Three things to know:\n \n - **A rule that sized `[data-slot=\"progress-indicator\"]` is now sizing a box this\n Component does not resize.** Style the track's `height`, the track's `width`, or\n the indicator's `transform` instead. The indicator's own inline `width` is `100%`\n and outranks any class, because Base UI writes a percentage width onto the\n element as an inline declaration and nothing in a class list can beat that.\n - **The fill grows from the inline start edge in both directions.** `origin-left`\n under a left-to-right `dir` and `rtl:origin-right` under a right-to-left one, so\n the bar reads from the same end it always did. It takes the `rtl:` variant rather\n than a logical property because CSS has no logical keyword for\n `transform-origin` and Tailwind's `origin` utility ships the nine physical\n positions and nothing else.\n - **At a low value the leading cap is flatter than it was.** A scaled shape's corner\n radius is scaled with it, so a bar at five percent is a short pill with a\n compressed cap rather than a five percent slice of a round one. It is the standing\n trade for a compositor animation, and it is the same one every other\n compositor-priced fill in this package already makes.\n \n `value={null}` is unchanged: an unknown length renders no transform at all, so it is\n the resting state it has always been rather than a zero to animate away from when\n the length becomes known.\n \n The bump is `minor` rather than `patch` because a consumer who styles the indicator\n has to move with it. No prop, import or announced value changes.\n- 6dc7bb8: Declare `react` and `react-dom` as peer dependencies\n \n `react` was already a peer dependency in fact: every module in the package imports\n it, and nothing declared it. What that cost was not a failed install but silence.\n A package with no peer range cannot be incompatible with a React version, so npm\n and pnpm had nothing to warn about, and the manifest never said which React this\n library is written for. Both are now `^19.2.0`, the range this repository builds\n and tests against, and the site already resolves. `react` and `react-dom` stay in\n `devDependencies` so the package still builds and tests locally.\n \n `react-dom` is a peer even though no module here imports it, because\n `@base-ui/react` does, for the floating elements the Dialog, the Popover, the Menu\n and the Tooltip render through. Two places already treated it as your copy rather\n than the package's, the client budget's shared runtime and the registry's implicit\n set; this is the manifest catching up with those two decisions.\n \n If your application is on React 18, install React 19. Nothing in the package uses a\n React 19 only API, but every gate, test and build in this repository runs on 19, so\n a narrower claim would not be one this repository can back.\n- 6dc7bb8: Reduced motion is now one unlayered rule in the stylesheet, and it stops everything\n \n If you have the `prefers-reduced-motion` setting on, this release changes what you\n see in a way you will notice the first time you open one of our pages.\n \n **What a reader with the setting on gets now.** No animation runs and no transition\n runs, anywhere in the component library. A dialog appears already open instead of\n fading up, a hover changes colour instantly instead of over 80ms, a disclosure\n opens at once instead of growing, a `Spinner` ring stands still instead of turning,\n and a `Timeline`'s running mark holds one opacity instead of breathing. Everything\n that is not movement is unchanged: nothing is hidden, no figure loses a state, and\n every announcement is said exactly as before.\n \n **What this replaces.** The stylesheet already carried a `prefers-reduced-motion`\n rule, and it did two things: it set `animation: none`, and it named the seven\n `prism-ambient-*` classes rather than every element. So every `transition-*` in the\n package ran at full duration, and two unbounded loops ran indefinitely: `Spinner`'s\n ring turned for as long as the work did, and a running `Timeline` step breathed for\n as long as the step did. The comment above that rule described the opposite, which\n is the defect class this repository keeps finding, so the rule and its comment are\n now the same document.\n \n **Why one rule rather than a guard at each call site.** A `motion-safe:` variant\n ADDS a rule inside `prefers-reduced-motion: no-preference`; it never removes one.\n Three Components were guarding a transition per call site and all three were\n different shapes: `Drawer` and `ImageZoom` guarded a duration as well as the\n property, `Lightbox` guarded the property and not the duration, and `RangeField`\n guarded nothing at all while animating `left`, `right` and `width`. The one place\n that decides is the stylesheet, and `scripts/check-motion.mjs` now fails on a\n `motion-safe:` or `motion-reduce:` variant on a motion utility, so a call site\n cannot quietly start answering for itself again.\n \n **What it means for a consumer.** Your own stylesheet is unaffected where it is\n qualified by a class, an id or an inline style: the rule is unlayered and universal\n rather than `!important`, so a class-qualified rule of yours outranks it and still\n wins for your own elements. What changes is the library's motion, which is the\n motion you inherited by installing it.\n \n **One Component had to be repaired before this was safe, and that is the load-bearing\n part.** A stopped transition starts nothing and so never sends `transitionend`, and\n an exit waiting on that event strands whatever it was hiding. `Toast` now asks the\n browser what is actually running on its root and hands over on the same commit when\n the answer is nothing, so a `Toast` still leaves under this rule and under your own\n `transition: none`. The overlays are Base UI's and Base UI settles every popup on\n `getAnimations()`, which resolves immediately when nothing is running.\n \n The bump is `minor` because every consumer of this package inherits the change and\n one stylesheet is the whole surface it lands on. No prop, import or exported name\n changes.\n- 6dc7bb8: `SearchDialog` is a real modal, and `Toast` dismisses when no transition runs\n \n Two accessibility defects, and a consumer sees both.\n \n **`SearchDialog` claimed modality it did not have.** It rendered `aria-modal=\"true\"`\n with no focus trap, so Tab walked out of the dialog and into the page behind it;\n it took no focus back, so a keyboard reader who opened search, pressed Escape and\n kept tabbing had lost their place entirely; its field suppressed the browser\n outline and drew no replacement, so the one control holding focus showed nothing;\n and it had no portal and no scroll lock, so the panel scrolled with the document\n and could be clipped by an ancestor.\n \n It is now composed from `Dialog`, so the focus trap, the page scroll lock, the\n portal, the dismissal on Escape and on the scrim, and the return of focus to the\n control that opened it all come from the one modal implementation this package has.\n There is one modal in the package rather than one and a claim. Its props, its\n ranking, its result list and its live region are unchanged.\n \n Two things about the composition are worth knowing:\n \n - `onClose` now fires once the dialog has finished leaving rather than at the\n moment it was asked to. The modal owns the reader's focus while it is closing,\n so handing over earlier would take the focus return with it. Unmount from\n `onClose` exactly as before.\n - The panel and the scrim are now `Dialog`'s rather than the search dialog's own.\n The scrim is a blurred `bg-background/80` wash instead of a `bg-foreground/40`\n one, and the panel is centred in the viewport rather than pinned 15 percent from\n the top. A site that styled either through its own sheet will want to know.\n \n **`Toast` dismissal depended on a transition ending.** The leave handed over on the\n end of its own opacity transition, so a global reduced-motion kill written as\n `transition: none` rather than as a `0.01ms` duration meant the event never fired,\n `onDismiss` was never called, and the toast was stranded in `data-phase=\"leaving\"`\n for good: visible, undismissable, still taking the pointer, and a live region that\n never stopped announcing. This is the one place in the system where a global\n motion kill destroyed state rather than merely removing movement, and\n `packages/ui/src/styles.css` states the invariant it broke.\n \n The leave now asks the browser what is actually running on the toast rather than\n waiting for an event a killed transition never sends. It waits for the fade when\n there is a fade, it hands over on the same commit when there is not, and\n `onDismiss` is called exactly once either way. Nothing else about the contract\n moved, and the length of the leave is still `duration-slow` from the stylesheet\n rather than a number in this package.\n \n **`Dialog` takes an optional `initialFocus`.** Left out, the panel focuses its\n first tabbable element, which is unchanged. It is named for the dialog whose first\n tabbable element is not the control the reader came for, which is what\n `SearchDialog` now uses to put the cursor in the field.\n \n **The bump is `minor` rather than `patch`, for two reasons.** `Dialog` gains a\n public prop, which is new surface rather than a correction. And `SearchDialog`'s\n panel and scrim visibly change to `Dialog`'s, which is a behaviour a consumer can\n see on their site rather than a fix they never notice. Both are additive and\n neither breaks a call site.\n- 6dc7bb8: `SiteNavbar` is opaque, and `sticky` can be turned off\n \n The bar was `bg-background/80` with a `backdrop-blur`. A backdrop filter is\n evaluated against everything painted behind the element, and this element is a\n full-viewport-width sticky bar, so on a scrolling page the browser re-sampled and\n re-blurred the backdrop on every frame of the scroll, on every page of every site\n that composes it. The bar is now `bg-background` with a `border-border` hairline,\n which is what `SiteHeader` has always shipped.\n \n **The blur was not kept behind an `@supports` check, because a capability check does\n not pay for it.** `@supports (backdrop-filter: blur(1px))` is false only in a\n browser that was not compositing anything, and true in every browser that does the\n expensive thing, so it changes no reader's cost. It would also have left a third\n outcome in the world and the worst of the three: a bar that is a flat eighty\n percent veil with unblurred text passing under it.\n \n **Opaque is also the more legible of the candidates, and that is the part a token\n can be held to.** An opaque bar makes the bar's own contrast the pair\n `muted-foreground` on `background`, in every pack and in both modes, which is the\n pair the contrast gate already checks. An eighty percent background has no token\n pair at all, because the colour under it is whatever the reader's scroll position\n has brought there and no token in this system describes it.\n \n `will-change` is not on the bar and is not proposed for it. Nothing on the bar\n animates, so a compositing hint applied at rest has no frame to be ready for and\n nothing that would release it. This package ships no `will-change` anywhere.\n \n **`sticky={false}` now works.** The prop was destructured and never reached the\n class list, so a site that passed it was given the sticky bar it had asked not to\n have. The default is unchanged and for the reason it was there: the bar is how a\n reader leaves the page they are on, and it takes that with them on exactly the long\n pages where it is needed.\n \n Two things to know if you were styling against the old bar:\n \n - A rule that set the bar's background translucency has nothing to sit on now. Set\n the colour on the page behind the bar, or leave the bar the way `SiteHeader` has\n it.\n - A rule that relied on the bar being translucent to hide a heading as it scrolled\n under will no longer hide it, which is the point.\n \n The bump is `minor` because the bar's default appearance changes on every page of\n every consumer. No prop, import or landmark changes.\n- 6dc7bb8: The elevation scale is three steps or it is nothing, and the gate now says so\n \n `SearchDialog` shipped `shadow-lg`, and `DESIGN.md` said no shipped component\n uses one. Both were true once and neither was true when the sentence was last\n read: the Component took `shadow-lg` as an override on top of the `shadow-md` its\n own `DialogContent` already draws, and the site's skip link took `focus:shadow-lg`\n under a heading that named \"the theme disclosure and mobile nav panels\" as the\n only places it was allowed.\n \n **`shadow-lg` is not an authored step, so neither of those was drawing an authored\n shadow.** The token package emits `--shadow-xs`, `--shadow-sm` and `--shadow-md`\n and nothing else, so `shadow-lg` resolved against Tailwind's own stock theme. That\n is the second source of truth this package exists to prevent, and the reason it\n was invisible is that Tailwind's `shadow-lg` is a perfectly good black-alpha\n shadow: nothing looked wrong. What was wrong is that a retune of the elevation\n scale would have moved every lifted surface in every consumer and left that panel\n exactly where it was.\n \n **`SearchDialog` now says nothing about elevation at all.** The panel is the one\n lifted element over a modal scrim, which is what `--shadow-md` is for, and\n `DialogContent` already draws it, so the override is deleted rather than\n replaced. A consumer who styles `DialogContent` keeps styling it, which was not\n true of a hardcoded `shadow-lg` sitting on top of it.\n \n **`scripts/check-elevation-layout.mjs` now fails on any `shadow-*` step the token\n source did not author**, in every root it reads, the site's own source included.\n It reads the authored set from `shadow.tokens.json` rather than from a list beside\n the gate, for the reason the width rule beside it reads the container names from\n `layout.tokens.json`: a gate whose subject is the authored scale cannot be\n answered by a second copy of it. `shadow-none` is allowed, because the absence of\n a shadow is not one more step of the scale, and `shadow-inner` is not, because it\n is a shadow nobody authored.\n \n **There is no docs-only exception any more, and that is the part worth arguing\n with.** The exemption said site apparatus is not installable surface. The site's\n own Tailwind build is a second consumer of the same token package, so \"the site\"\n is not outside the system, it is inside it twice: a site class resolving a shadow\n out of Tailwind is the second source of truth wearing the word apparatus. The\n site's skip link draws `shadow-md` now, which is what it should have drawn.\n \n The bump is `minor` rather than `patch` because a consumer who styles the search\n panel's elevation sees it move from Tailwind's `lg` to Prism's `md`, which is a\n visible change to a published surface and the right one.\n- 6dc7bb8: `RangeField` expresses its band as a transform rather than as three layout properties\n \n The band between two bounds was transitioned with `transition-[left,right,width]`.\n Base UI positions that element with a logical `inset-inline-start` and a `width`,\n so `left` and `right` were properties the browser checked on every frame in order\n to discover they had not moved, and `width` was a layout-and-paint animation on\n every frame of every drag. A range is the one control in this package whose value\n changes on every pointer move rather than on every commit, which is why the cost\n was larger here than on the Component that retired the same mechanism a release\n ago.\n \n **The band is now as wide as the track and is scaled about the inline start.**\n `aria-valuenow` still comes from Base UI and still reports each bound; the scale\n factor is the same division read a second time, and the test suite holds the two to\n each other by asserting the transform against what the two thumbs announce.\n \n Three things to know:\n \n - **A rule that sized or positioned `[data-slot=\"range-field-indicator\"]` is now\n sizing or positioning a box this Component does not resize.** Style its\n `transform`, or style the track. Its own inline `width` is `100%` and outranks any\n class, because Base UI writes a percentage width onto the element as an inline\n declaration. Its POSITION is still Base UI's, untouched: the element keeps the\n logical `inset-inline-start` that places the band's left edge on the lower bound\n under a left-to-right `dir` and on the same distance from the other end under a\n right-to-left one, so a `dir=\"rtl\"` band needs no arithmetic of its own.\n - **A partial band has a straight edge where it stops, and no rounded edge.** The\n previous fill carried `rounded-full`, and a `scaleX` scales the shape it is applied\n to, including its own corners, so a band at 40 percent drew a 3px cap squashed to\n 1.2px on one axis and left at 3px on the other. The ends of a band are the two\n round thumbs drawn on top of it, so the radius was a second, smaller circle under a\n larger one and squashing it landed on exactly the edge the reader is looking at.\n The track still carries `rounded-full` with `overflow-hidden`, so a band at full\n span has both of its ends rounded. This is the one place the shape differs from\n `Progress`, and the reason is on the Component.\n - **A vertical field scales on `scaleY` about the bottom edge**, chosen by the\n `orientation` the Component already forwards, rather than having the horizontal\n animation applied to a vertical box.\n \n `RangeField` also carried a `motion-safe:` variant on that transition, which was\n doing nothing: the variant adds a rule rather than removing one, so the unguarded\n transition ran at every setting. See the reduced-motion changeset for where the\n policy now lives.\n \n The bump is `minor` because a consumer who styles the indicator has to move with it.\n No prop, import or announced value changes.\n- 6dc7bb8: The rest of the controls take the coarse-pointer floor, and the divider explains itself\n \n Fifteen controls were drawing targets between 28 and 40 pixels with nothing done about\n it on touch input, and three more were found by going looking rather than by reading a\n list. All of them now take the 44px floor, and **the desktop metrics are untouched**: a\n mouse and a trackpad see exactly what they saw before, and every change is inside\n `@media (pointer: coarse)`.\n \n **Sixteen take a step, and the step is the default for all of them.** A popup trigger\n (`PopoverTrigger`, `DropdownMenuTrigger`, `NavigationMenuTrigger`, `MenubarTrigger`,\n `AlertDialogTrigger`), an alert dialog's confirm and dismiss pair, a page link\n (`PaginationLink` and the previous and next controls), a tab and its list, a select\n trigger and a native select, a `Toggle`, a `ToggleGroupItem` and the sidebar's toggle\n all grow to 44 tall through `pointer-coarse:h-11`. `Carousel`'s two controls, both of\n `Lightbox`'s, both of `ImageZoom`'s, the drawer's close control and `PaginationLink`'s\n `icon` size take `pointer-coarse:size-11` and are 44 by 44.\n \n **Four of them take `min-w-11` on the second axis as well, because a floor paid on one\n axis is not a floor.** A one-digit page link at 44 tall is 20 wide. An icon-only\n `Toggle` is `size-4` plus `px-2.5`. An icon-only `ToggleGroupItem` is the same. This is\n the arrangement `Button` already states and the reason it states it, and the page-link\n case is the one worth naming: `PaginationPrevious` and `PaginationNext` pass\n `size=\"default\"` and override the padding to `px-2.5`, so on a phone the word is hidden\n and the control is an icon with a 40 pixel width until `min-w-11` says otherwise.\n \n **`TabsList` grows too, and that is the half that is easy to forget.** The list is\n `h-9 p-1`, so its content box is 28 tall and a trigger grown to 44 would not fit inside\n it. It takes `pointer-coarse:h-13`, which is the trigger's 44 plus the list's own `p-1`\n on each side. Both halves are asserted in the test, because either alone leaves a\n control under the floor or a list that clips one.\n \n **Two of the fifteen are not targets and take nothing, and both are worth stating\n because each looks exactly like every other finding here.**\n \n `steps.tsx`'s marker is a `<span>` with no role, no `tabIndex` and no handler. It is not\n focusable, it is announced by nothing, and a press on it falls through to the step's own\n text. The rail is a reading structure: the `<ol>` carries `aria-label`, the `<li>`\n carries `aria-current=\"step\"`, and the connecting rule is `aria-hidden`. Growing a 32px\n disc to 44 would pay the floor on a decoration and push the step's text down the page.\n \n `pagination.tsx`'s ellipsis is a `<span aria-hidden=\"true\">`, for the same reason. The\n page links around it are the targets and they take the floor in `PaginationLink`. The\n floor is a claim about targets a finger is asked to hit, and neither of these is one.\n \n **`ResizableHandle` takes a band, and it is the only control here where a step is\n arithmetically impossible rather than merely wrong.** The panes are given\n `flexBasis: <size>%` with `flexShrink: 0`, so the two of them already sum to the group's\n whole width and the divider is what overflows it, by exactly its own one pixel. A `w-11`\n divider would overflow the group by 44, which on a phone pushes the right pane's edge off\n the screen, and making room would mean changing what `size` means.\n \n The band works because three facts hold that the three conditions in `DESIGN.md` ask\n about. It is a pseudo-element, so the box the drag measures stays the drawn box:\n `onPointerDown` reads the **group's** rect and `onPointerMove` divides by the group's\n travel, so nothing reads this element's own size at all, which is the failure `DESIGN.md`\n warns about for a `Slider` thumb and which does not apply here. It paints above both\n panes without a `z-index`, because the handle is `relative` with `z-index: auto` and the\n panes are static, and a positioned descendant paints after non-positioned siblings. And\n it is 44 by 44 rather than 44 by the handle's length, which is what keeps the overlap\n local: along the split the handle already spans the group so 44 there is free, and across\n the split it reaches 21 pixels into each pane over a 44 pixel stretch of the line rather\n than the whole height of the group.\n \n **The cost is stated at the class rather than hidden in a document.** A press within 21\n pixels of the line, over a 44 pixel stretch of it, grabs the divider rather than the\n pane, and `touch-none` travels with the band because the pseudo-element resolves its\n `touch-action` from this element. Text selection near the divider is unavailable inside\n that patch. That is the trade a resize gutter makes on every platform, and it is bounded\n to a patch rather than run the length of both panes.\n \n Three things to know before you style against these:\n \n - A tab list, a menubar, a navigation bar and a segmented toggle group are 44 tall on\n touch rather than 28 to 36. A `Tabs` panel is that much further down the page.\n - A page header's row of links, an alert dialog's footer and a carousel's control row are\n each 8 to 16 pixels taller on touch. Nothing shifts off an edge: every one of them is\n content-sized and centred rather than flush.\n - On a resizable split, a press inside 21 pixels of the divider grabs the divider. The\n arrow keys, Home and End still move it, and `aria-valuenow` still reports where it is.\n \n **Three controls the earlier list did not name, found by searching for the two patterns\n rather than reading a line number.** `Toggle`, `SelectTrigger` and `NativeSelect` were\n all drawing 32 or 36 pixel targets and all three are the same family as controls the\n earlier release had already fixed. They are included because a trigger that opens a\n popup is a button, not a text field: it has no caret, takes no typed input, and its job\n is to be pressed.\n \n **One family was found and deliberately left, and the reason is worth having in writing.**\n `Input`, `Textarea`, `Form`'s `FieldControl`, `Combobox`'s input and the draft field in\n `CreatableCombobox` all draw 36 tall on a coarse pointer. Growing them is a different\n decision from every one above rather than an omission: a text field is the one control\n whose height also sets the line box the caret sits in, it is composed into dozens of\n Blocks, and this package already carries a deliberate narrow-viewport decision on exactly\n these elements (the `text-base md:text-sm` pair that stops iOS zooming the page on focus).\n The line this release draws is **triggers and buttons take the floor; text entry does\n not, in this sweep**, and the text-entry family is a separate piece of work rather than a\n gap in this one.\n \n **The bump is `minor` rather than `patch`.** No prop, import or rendered element changes,\n and no desktop metric changes, so nothing breaks a call site. It is `minor` because\n eighteen controls now measure differently on a coarse pointer, and a consumer who styles\n any of them has to know that: a tab list is taller, a navigation bar is taller, a\n segmented group is taller, a resizable split claims 21 pixels either side of its line.\n That is a visible metric change on the platform most of these controls are used on, and\n describing it as a patch would describe something nobody sees. This is the same judgement\n the earlier release made for the thirteen controls it fixed.\n \n `DESIGN.md` states both patterns and the three conditions that decide between them, and\n `test/coarse-pointer-floor.test.tsx` asserts the class each floor is written as. **What\n that test does not do is prove a size of 44 pixels, and the file says so in its first\n paragraph:** jsdom has no CSS engine, no cascade and no box, so `@media (pointer: coarse)`\n is never evaluated here and a coarse-pointer variant is a string that a test can only\n assert is present on the element the Component rendered. The assertions query by role and\n name rather than searching the source, so they catch a floor that was moved onto a\n wrapper or dropped in a refactor; they do not and cannot catch a browser that lays the\n element out at 30 pixels. `apps/site/e2e/display.spec.ts` over a real browser is where\n that is checked.\n- 6dc7bb8: `SiteFooter`, `DataTable01` and `RunConsole01` take a heading level\n \n Three Blocks drew a hard-coded `<h2>`. A fixed level is right for a Block at the top\n of a page and wrong everywhere else, and all three are Blocks a product opens inside\n something: a footer as the last region of a settings page, a table in a drawer, a run\n console in a panel. In each case the heading became a sibling of the heading the\n surrounding section already had, so \"skip to the next heading at or below level two\"\n walks straight past the thing the reader came to read.\n \n All three take `headingLevel?: HeadingLevel` and default to `'h2'`, which is the\n arrangement every other Block in this package already uses and which\n `childLevel()` exists to compose. Nothing rendered changes for a caller that passes\n nothing. `DataTable01`'s JSDoc had claimed the fix was needed, so this is the code\n catching up to its own documentation.\n \n **The audit behind this said `SiteFooter` was the only Block in the tree with no\n `headingLevel` prop. It was three.** The count matters because it changes the shape of\n the fix: a single instance reads as a judgement call about that Block and a class of\n three reads as a rule the package had not finished applying, which is what this\n release says.\n- 6dc7bb8: `ToggleGroup` requires the accessible name its role already needed\n \n `aria-label` was declared optional on `ToggleGroupProps` while the JSDoc directly\n above it said \"Required rather than defaulted\". TypeScript enforced nothing, so a\n group shipped unnamed, and both roles this Component draws are ones ARIA puts a MUST\n on: a reader tabbing onto an unnamed `toolbar` is told \"toolbar\" and cannot ask which\n set of controls they have reached, and an unnamed `radiogroup` says nothing about\n which question its radios are answering. `TextFormatToolbar` draws the same role with\n a required `label`, which is the shape this now matches.\n \n **This is a compile-time break, and the fix is one prop.** A `ToggleGroup` that\n passed no name now fails to build:\n \n ```tsx\n // before\n <ToggleGroup value={range} onValueChange={setRange}>\n \n // after\n <ToggleGroup aria-label=\"Date range\" value={range} onValueChange={setRange}>\n ```\n \n `aria-labelledby` still arrives through the forwarded props, so a caller whose name\n is already drawn somewhere can point at it rather than repeat it.\n \n **Why `minor` and not `major`.** The bump line for this repository is `0.y.z` and the\n package is at 0.7, so `major` would be 1.0.0 and would claim a stability promise for\n a design system whose catalogue gained 130 Items in the release before this one. The\n published precedent is in `packages/ui/CHANGELOG.md`: the `*Variant` to `*Form`\n renames and the prop removals in 0.7.0 shipped on the `minor` line, each with its\n reason in the entry. This is the same shape of change, and it is described here in\n full rather than left to be discovered by a compiler.\n\n### Patch Changes\n\n- Updated dependencies [6dc7bb8]\n- Updated dependencies [6dc7bb8]\n - @nanisoft/prism-tokens@0.15.0\n\n## 0.14.0\n\n### Minor Changes\n\n- 4e40c8f: Nine Components, and the naming law that the next thousand will follow\n \n Closes the gap in Component coverage: a `Pill` beside `tag-group`, a `Drawer`\n that is a Sheet with a gesture rather than an overlay, an `ImageZoom` that is a\n `Lightbox` with the dialog taken out, a `VideoPlayer` that owns no engine, an\n `EmojiPicker` that ships no emoji table, a `RepoStars` whose mark is a slot\n because a host logo is a licensed asset, a `ChoiceCard` built on real radios\n rather than ARIA, a `BillingSource` that has no `mask` prop on purpose, and a\n `PackSwitcher` that makes Prism's pack axis switchable at runtime, which nothing\n did before.\n \n Four of the nine ship no JavaScript at all. That is the number worth reading\n against the roster: nine catalogue Items cost 19.3 KB of the deduplicated client\n bundle rather than the sum of their rows, and `VideoPlayer` in particular is a\n server Component that a page can carry a dozen of for free.\n \n `DESIGN.md` gains the naming law this roster needed earlier: a slug says what a\n Component does, and the only number it may carry is a variant ordinal on a name\n that is already true. `button-47` tells a reader nothing; `split-button` tells\n them what it is and what happens if it changes. Five upstream-shaped items are\n refused by that law and each refusal is recorded with its reason rather than left\n to be rediscovered.\n \n The client ceiling moves from 260 KB to 280 KB, measured at 269.7 KB over 177\n entry points, and the gate's header says plainly that this is the third kind of\n number it has printed: not a correction of a bad measurement, and not a forecast\n of weight that had not landed, but a report on weight that is here. It also names\n the pattern, because five moves in a row triggered by the same event is the\n finding: a ceiling that moves once per batch of work is recording a history rather\n than measuring a policy.\n \n `Progress`, `dayKey` and the `live` subpath fix from the previous release are\n unchanged. No consumer import breaks; every name here is an addition, and the\n four `*Variant` to `*Form` renames from that release are still additive because\n nothing imported the old names.\n- 829ae83: Take the catalogue from 106 Items to 236\n \n Adds 130 Items in Prism's own vocabulary: 23 foundation Components, 96 Blocks, 10\n Pages and one live surface. Every name is an addition, so no consumer import\n breaks, and every Item follows the rules the package already held rather than\n relaxing them: no shipped copy, no fetching, no raw colour, and motion by token.\n \n The shapes that were not upstream's are the ones worth naming, because each is a\n translation rather than a copy. Upstream's storefront comparison is\n `offering-categories-01` and its spec sheet is `spec-table-01`, because what a\n buyer compares in a pipeline product is a connector's declared bounds rather than\n a plan's feature list. `tool-ledger-01` is the second live surface and takes a\n `subscribe` function the consumer supplies, so Prism still owns no socket.\n `directory-01`, `project-dashboard-01`, `ops-checklist-01` and `handoff-01` were\n added because a Block already in the roster reached for something absent.\n \n Two names move because nothing had imported them. `CalendarBlock01` is now\n `Calendar01`, and the deferred `input-otp` resolved to the published\n `one-time-code`. Four `*Variant` types are now `*Form`, since the surface gate\n rejects an exported union under a name that reads as a styling axis.\n \n `Progress` gains a string `valueText` prop for the server Block that cannot carry\n a callback across the boundary, `dayKey` is exported from `lib/utils` for the\n Blocks that share a date key, and the emitted `live` subpath now resolves its\n relative specifiers, which it did not.\n \n The client bundle is 250.4 KB against a ceiling moved from 208 KB to 260 KB. The\n move is a forecast about weight that had not landed, and the gate's own comment\n records why it differs from the two earlier corrections. `docs/history/roster-expansion.md`\n holds the decisions, and `DESIGN.md` records that the v1.1 deferred tail has\n shipped.\n- 4e40c8f: Thirty-four Components, and the audit that refused the other thousand\n \n Component coverage of the upstream catalogue is complete. 1,026 variants were\n audited against the naming law and 34 shipped: 392 were a cross-product of tone,\n size, shape, icon or state that a shipped Component already expresses as props, and\n the rest were motion refused by a law that predates this work.\n \n The zeroes are the result. `form` (85) and `select` (51) produced nothing, because\n a contact form and a settings form differ only in the data a consumer passes.\n `table` (38), `avatar` (34), `skeleton` (30), `dropdown-menu` (30) and `sheet`\n (30) produced nothing for the same reason, and `skeleton` at 30 is six groups of\n five, each one a different arrangement of the same placeholder rectangles.\n \n New: `reorderable-list`, `checklist`, `form-dialog`, `form-wizard`, `stateful-table`,\n `nested-tabs`, `mega-menu`, `task-progress`, `split-button`, `stepper`,\n `overflow-actions`, `selection-toolbar`, `money-field`, `intensity-grid`,\n `proportion-list`, `cohort-grid`, `multi-combobox`, `creatable-combobox`,\n `range-field`, `platform-modifier-key`, `image-list-field`, `repeatable-rows`,\n `text-format-toolbar`, `prompt-composer`, `lifecycle-button`, and the nine from the\n category sweep: `pack-switcher`, `pill`, `billing-source`, `drawer`, `image-zoom`,\n `video-player`, `emoji-picker`, `repo-stars`, `choice-card`.\n \n Five of the new ones exist because two Components disagree and the disagreement\n is the defect: `split-button` joins a trigger and a menu trigger into one shape,\n `stepper` clamps two controls at one bound, `overflow-actions` moves actions into\n a menu when the row runs out, `selection-toolbar` and `text-format-toolbar` take\n opposite positions on whether focus may move, and `nested-tabs` orders two tab\n axes that look identical. A boolean cannot express a conflict.\n \n **`form-dialog` is the largest single job found, at 22 upstream variants** across\n `dialog`, `alert-dialog` and `popover`. All three ship a container and none owns\n what happens between the reader pressing submit and being able to act on the\n answer.\n \n **`pack-switcher` makes the pack axis switchable at runtime**, which is the piece\n a DTCG system was missing. `video-player` owns no player engine and ships no\n JavaScript. `money-field` holds a number for the caller and a locale-formatted\n string for the reader at the same time, and the round trip is the Component.\n \n Four defects the audit found in work already merged are fixed in this release, and\n each was found by a job that did not exist to find it: `button` never had a\n loading state and two documents claimed it did; `cohort-grid` documented a scale\n knob that cannot be moved because a retention row's first column is a hundred by\n definition; `check-item-docs` could not see a generic Component, so a module with\n a full JSDoc block read as declaring no Item rather than as undocumented; and a\n byte order mark on an MDX makes its frontmatter parse as empty with no gate\n noticing, which is what `scripts/check-encoding.mjs` now exists to prevent.\n \n The catalogue is 270 Items and the client bundle is 286.2 KB, inside the 300 KB\n ceiling without moving it, because nine of the thirteen newest compose Components\n the bundle already carries. 34 Components across two passes cost 47.6 KB of\n aggregate where the sum of their individual measurements is several times that.\n \n No consumer import breaks. Every name added is an addition.\n- 4e40c8f: The component sweep: 21 Components from 555 audited upstream variants\n \n The naming law in DESIGN.md says a slug describes the job and a variant that\n differs from its siblings only by a prop is a call site. Applied strictly to 555\n upstream variants, 21 survived: 340 were a cross-product of tone, size, icon and\n state that existing Components already express as props, and 122 were motion\n refused by a law that predates this work rather than by a new one. The two zeroes\n are the finding: `form` at 85 and `select` at 51 produced nothing, because a\n contact form and a settings form differ only in the data a consumer passes.\n \n New: `pack-switcher`, which makes the pack axis switchable at runtime and is the\n piece a DTCG system was missing; `pill`, `drawer`, `image-zoom`, `video-player`,\n `emoji-picker`, `repo-stars`, `choice-card`, `billing-source`; `intensity-grid`,\n `proportion-list`, `cohort-grid`; `multi-combobox`, `creatable-combobox`,\n `range-field`, `platform-modifier-key`; `image-list-field`, `repeatable-rows`,\n `text-format-toolbar`, `prompt-composer`, `lifecycle-button`.\n \n Four ship no JavaScript. The catalogue is 257 Items and the client bundle is 278.5\n KB.\n \n **Three defects the audit found in work already merged.** `button` never had a\n loading state and the catalogue and its MDX both said it did; the claim is\n corrected and `lifecycle-button` is now where an outcome after a press lives.\n `cohort-grid` documented `max` as the way to narrow its scale, which is impossible\n because a retention row's first column is a hundred by definition, so the knob is\n `min` and the limit is now stated. And `check-item-docs` could not see a generic\n Component, so a module with a full JSDoc block was reported as declaring no Item\n rather than as undocumented; the pattern is widened, which widens the law's reach\n rather than relaxing it.\n \n **`scripts/check-encoding.mjs` is new.** A byte order mark on an MDX makes its\n frontmatter parse as empty, and no existing gate noticed: the file had a JSDoc\n module, a Demo beside it, all-props strings, and a clean catalogue join. It\n surfaced as seventeen unrelated failures in two files. The class has now bitten\n this repository twice. The gate reports and does not repair, and it is proven to\n fire.\n \n **The client ceiling moves to 300 KB.** It has now moved six times and every move\n was triggered by a batch of new Items, which the gate's header records as the\n finding rather than hiding. A roster-derived ceiling was rejected because a\n ceiling that scales with the catalogue is the same as no ceiling. Deciding one\n fixed number on what a consumer can actually load is a product judgement about\n the four downstream repositories and is the one open decision here.\n \n No consumer import breaks. Every name added is an addition.\n\n## 0.13.0\n\n### Minor Changes\n\n- 66787b0: Add `SiteNavbar`, the site bar with the controls a reader needs on every page\n \n `SiteHeader` is a lockup, a product switcher and a slot. It is not a search box, a\n menu of the family's sites, a colour chooser, a light and dark control, or a\n navigation that survives the width at which a horizontal row stops fitting, and all\n five NaniSoft sites had written the missing parts themselves: Prism's own site had\n search and a colour chooser and no way to reach its four siblings, and each of the\n four had a row of five product marks where a menu belongs and no search, no mode\n control and no navigation at all below its own row's threshold.\n \n `SiteNavbar` is that bar. `search`, `sites`, `theme`, `mode` and `nav` are each\n optional, so a site that owns none of the four controls renders the whole bar as a\n server Component and ships no client JavaScript for it. Every string is a prop: the\n Block ships no site name, no default navigation, no \"Search\" and no list of packs or\n sites.\n \n ```tsx\n import { SiteNavbar } from '@nanisoft/prism-ui/blocks/site-navbar'\n \n <SiteNavbar\n product={{ id: 'nexus', name: 'Nexus', pack: 'lavender' }}\n defaultPack=\"lavender\"\n defaultMode=\"dark\"\n navLabel=\"Site\"\n mobileLabels={{ open: 'Open menu', close: 'Close menu' }}\n nav={NAV}\n currentSiteId=\"nexus\"\n sitesLabel=\"The family\"\n sites={SITES}\n search={{ indexUrl: '/api/search', label: 'Search documentation', messages: COPY }}\n mode={{ lightLabel: 'Switch to dark mode', darkLabel: 'Switch to light mode' }}\n />\n ```\n \n `currentPath` is the alternative to marking `current` on each link, for a site that\n composes the bar once in a root layout and is therefore handed no pathname.\n \n Also new: `SearchDialog`, the search the bar opens. It fetches a static JSON index\n on the first activation and ranks it in the browser, and the ranking is a scorer\n rather than an inverted index: every query token has to match somewhere, so a\n two-word query narrows instead of listing most of a documentation set. There is no\n stemming and no typo tolerance, which is a deliberate trade against taking a search\n dependency into a package that otherwise depends on a design system and a token\n pipeline and nothing else.\n \n `DropdownMenuItem` now takes `render`, so a menu item can be the link it navigates to\n rather than a focusable element wrapped around one. Two tab stops for one row, and a\n reader who middle-clicks a row that only looks like a link, are what that fixes.\n\n## 0.12.0\n\n### Minor Changes\n\n- 26a6fdc: Export the `Stat` and `Stats01Props` types from `stats-01`\n \n A caller that builds the `stats` array could not name the type it was building,\n so it had to infer it or annotate the array as `any`, and a rename of a field on\n `Stat` then stopped being a compile error at every call site. Both types are\n now exported from the subpath, alongside the Block.\n\n### Patch Changes\n\n- 9f3dd2a: Align `FeatureGrid01` and `Pricing01` section headings left\n \n Both Blocks rendered a centred heading over a grid of cards, and `SectionHeading`\n states that `left` is right for a section with content under it. Every other Block\n that opens with a heading already aligned left, so a page composed from Blocks put\n these two headings out of step with the rest of the page. No prop changed, so no\n call site does; a consumer relying on the centred heading was overriding a default\n rather than setting a value.\n\n## 0.11.0\n\n### Minor Changes\n\n- 3260391: A card title is a heading again, at a level the Block derives rather than guesses\n \n **The defect.** `CardTitle` renders a `div`, and that is correct: four cards in a\n grid are four titles under one section heading, and four `h2`s under one `h2` is\n four sections. But that reasoning is about the **element**, and the catalogue was\n reading it as a reason about the **outline** too. Every Block that drew a titled card\n drew it as plain text, so a page kept its `h2` and lost every heading below it. A\n feature grid of five cards was unreadable by heading navigation while looking\n identical. Found while migrating the company site, whose feature section had five\n `h4`s before the migration and five `<div>`s after.\n \n **The answer is one function, and it is the same answer in all six places.**\n `childLevel(headingLevel)` steps one level down, so a Block that draws a Section\n heading and a set of card titles puts the titles *under* the section, and the whole\n set moves together when the block is composed one level deeper than it was written\n for. A Block that draws no Section heading passes `headingLevel` directly, because\n there the card title is the one heading it renders. Both answers are right, and the\n test carries which is which rather than assuming one for all six.\n \n This is what answers the ticket's third criterion, that the same question be\n answered for *every* Item that draws a titled card. The sites are `FeatureGrid01`,\n `Pricing01`, `StackGrid01`, `AuthForm01`, `SettingsPanel01` and `AuthPage`, and the\n test that holds them is one file, because the consistency is the thing under test:\n five files with one case each would let the next Block answer differently and still\n pass, which is the failure this was filed about.\n \n **`CardTitle` gains `as`, defaulting to `div`.** The change is additive and no\n existing rendering moves. Its JSDoc previously told a caller to pass Prism's\n `Heading` inside it, and that was wrong: `Heading` is a step of the *type* scale\n whose floor is `lg`, and a card title sits at body size, so the only way to use it\n was to override the size back down, which asks a type-scale component to have no\n opinion about type. The element and the visual step are separate questions and this\n is the one that answers the element.\n \n **Two levels in one Block that had one hardcoded.** `SettingsPanel01` drew its\n group headings as a literal `<h3>`, so a panel composed under an `h4` in a document\n whose sections were `h4` announced its groups as siblings of the panel that\n introduces them. They are now one step below the panel title, so the panel's whole\n outline moves when the panel does.\n \n **`h6` holds rather than wraps.** A section already at `h6` has no child level.\n Wrapping to `h1` would put a card title *above* the section that introduces it, which\n a reader navigating by heading would meet first; holding at `h6` costs a repeated\n level, which a screen reader announces as the same depth rather than as a break in\n the outline. The clamp is asserted so a later change to the order is caught.\n \n **The proof that the test can fail.** Replacing `childLevel(headingLevel)` with a\n hardcoded `h3` in `FeatureGrid01` fails two cases, the default and the derived one,\n which is the exact defect the ticket names. A test that asserts \"is a heading\" would\n have passed that.\n- 932ce96: Add `ChartFrame` and `Sidebar`, the two token families that shipped with no consumer\n \n `chart-1` through `chart-5` and the eight `sidebar-*` roles were in the contract\n and used by nothing. These two Components are what consume them honestly.\n \n **`ChartFrame` puts the table inside the frame rather than beside it.** Marks and\n cells are two renderings of one `series` array, so drift is structurally\n impossible; a table passed as a sibling prop is a table that says last quarter.\n The table is always in the document and `table=\"visible\" | \"hidden\"` only decides\n whether it is *seen*, because `hidden` is `sr-only`: it is still announced and\n still findable in the page.\n \n **One frame means every chart in a product aligns to the same plot box.** Five\n charts each drawing their own axes is five sets of numbers that do not line up, and\n a dashboard where two charts disagree by six pixels looks broken in a way nobody\n can name. `mark=\"line\" | \"bar\"` is a prop and the marks are internal on purpose: a\n mark with no axis, no gridline and no baseline is a bar that lies, and exporting\n one for a caller to place is the exact failure the frame exists to prevent.\n \n **`Sidebar`'s collapsed state is the same rail at a smaller width, not a second\n Component.** Hover surface, focus ring, current marking and accessible name are\n all present in both states and only the words and the padding change. The name\n moves to `sr-only` rather than unmounting and the trailing count is `aria-hidden`,\n so the name is the same *string* at both widths and a reader who collapses the rail\n loses nothing.\n \n ## The measured findings behind both\n \n **The sidebar needs its own ring, and the numbers are in the tests.** `ring` on a\n `sidebar` surface measures 2.86:1 on Mint light, 2.97 on Sky, 3.13 on Peach, 3.16 on\n Lavender and 3.22 on Blush. Four of the six light-mode packs would have shipped a\n sub-3:1 focus indicator had the rail inherited the page ring, which is the\n measured justification for the whole `sidebar-ring` family existing.\n \n **The chart tokens are below 3:1 and that is why the legend is not optional.** In\n light mode `chart-2` measures 2.49:1 against `card` and `chart-3` measures 2.15:1.\n Both are exempt from the contrast gate as graphic objects, so a mark in either is\n found by position and shape rather than by colour. That is why the legend is\n mandatory, the stroke is 2px, and the table is not optional. Past about three\n series hue is the only channel left, which the documentation states as a consumer\n decision rather than hiding.\n \n **One pair in this family is unmeasured.** `sidebar-ring` against `sidebar-accent`,\n the surface a focused item sits on while hovered or selected, is 3.19:1 in the\n base pack's dark mode and 3.82 to 4.21 in the light packs. It passes everywhere\n and it is the tightest pair in the family with no row in `check-contrast.mjs`, so it\n is reported rather than papered over.\n \n ## Two gate gaps found and closed\n \n **`check-variant-ink` listed eleven foreground roles and omitted the two sidebar\n ones.** `sidebar-primary-foreground` and `sidebar-accent-foreground` are both\n required 4.5:1 rows in the contrast table, so the list was telling the `Sidebar`\n that its own required pairing was an inherited one. It is the first Component in\n the package to put a `sidebar-*` fill in a variant, so nothing had hit it. The\n alternative was to add a decoy `text-sidebar-foreground` to silence the gate, which\n would have been a false pass.\n \n **`check-focus-indicators` recognised `ring-ring` only**, so a `ring-sidebar-ring`\n would have been reported non-compliant. The `Sidebar` therefore keeps the browser's\n outline as well as drawing its ring, which is asserted by a test so it stays a\n deliberate guarantee rather than an accident. Both ring roles are now accepted.\n- 4650aff: Add `CommandPalette`, so a reader can reach any command by typing three letters\n \n Prism had a Select, a DropdownMenu and a set of Tabs, and nothing for the fastest\n route to an action that lives behind three levels of menu.\n \n ```tsx\n <CommandPalette\n open={open}\n onOpenChange={setOpen}\n label=\"Commands\"\n inputLabel=\"Search commands\"\n groups={[\n {\n id: 'appearance',\n label: 'Appearance',\n items: [\n {\n id: 'theme',\n label: 'Toggle theme',\n hint: 'Cmd K T',\n keywords: ['dark', 'light', 'colour'],\n onSelect: setTheme,\n },\n ],\n },\n ]}\n empty={{ message: (query) => `Nothing matches ${query}` }}\n />\n ```\n \n **It ranks, and that is the difference between this and a filtered menu.** Matches\n are scored by where the query falls: the start of the name beats the start of a\n word inside it, which beats a match further along, which beats a keyword-only hit.\n A palette that filters without ordering shows every command containing the query in\n declaration order, so the command the reader meant sits below one that merely\n mentions their query and they scroll.\n \n **The groups are ordered by their best match too.** Ranking only *within* a group\n leaves the premise broken, because an exact match in a late group still sits below\n a poor match in an early one. Ordering the groups by their strongest member puts\n the best answer at the top and keeps the headings, which is the whole of what a\n grouped list is for.\n \n **It is composed on the Dialog rather than beside it.** Focus trapping, Escape, the\n portal, the scroll lock and the return of focus are five behaviours that are correct\n in the Dialog and would be five chances to get one wrong here. A palette that rolled\n its own overlay would be a second answer to all five questions, and the second\n answer is the one that ships the bug.\n \n **The matched run is emphasised by weight, not by a background**, which is a\n deliberate difference from `Mark`. A Mark is right in a list of search results,\n where the match is the reason the row is there. In a palette the match is a hint\n while the label is what is being read, and a saturated background on every matched\n character fights the text it sits inside. Two surfaces, two treatments, one reason.\n \n Three behaviours are decisions rather than defaults and are asserted in the tests:\n \n - **Enter never runs a command the reader did not point at.** With nothing\n highlighted it does nothing. This is the one outcome a palette must never\n produce, and it is invisible in a screenshot because nothing on screen changes\n when it happens.\n - **The arrows wrap.** A palette is a transient surface where overshoot is common,\n and a reader who overshot should not have to press Up to come back.\n - **The highlight is clamped, not reset, as the list changes.** A reader who arrows\n down three rows and then types one more character is choosing from a list that\n moved under them, and yanking the highlight to the top discards where they were.\n \n `keywords` is what makes the palette good rather than merely present: a reader who\n has to name a command exactly already knows it exists, which defeats the surface.\n `suggest` covers the rest, because a palette that opens onto a bare list makes a\n reader type before they know what is available. `empty` takes the query so the\n sentence can be the caller's, which is the only way \"no results\" avoids shipping in\n English.\n- 932ce96: Add the data display substrate: item, scroll area, carousel, toggle and the rest\n \n Nine Items, and the one that matters most is `item`, because it is the row almost\n every list in the package hand-rolls today.\n \n **`item` is the row and not the list.** A caller writes the `ul`, the `dl` or the\n grid and composes rows into it, because the same row appears in four different\n parents and a Component that owned the list would own all four.\n \n **`scroll-area` keeps the browser scrolling and takes over only the appearance of\n the bar.** The alternative is a transform on a `div`, which looks identical in a\n screenshot and has no scroll position, so no keyboard, no `scrollIntoView` and\n nothing to announce. The custom bar is also a real accessibility obligation rather\n than a decoration: a scrollbar is a control, and a control made of `div`s is worse\n than the one the browser shipped.\n \n **`aspect-ratio` takes the ratio as a prop rather than as a class.** Prism's\n stylesheet scans only Prism's own source, so a consumer's `aspect-[4/3]` is a rule\n the shipped sheet does not contain, and the box silently collapses.\n \n **`carousel` is never the only route to its content.** Every slide stays in the\n DOM, the controls state where the reader is through a required `position` function,\n and there is a focus handoff: a control that has just been disabled leaves the tab\n order without giving up focus, so a reader who pressed \"next\" onto the last slide\n would otherwise be stranded inside the carousel.\n \n **`toggle` is pressed; a `switch` takes effect at once.** The JSDoc says so and one\n test asserts the three controls side by side, because confusing them is the usual\n failure and the confusion is a bug report rather than a compile error.\n \n **`toggle-group` changes the role, not the look.** `single` is a `radiogroup` of\n `role=\"radio\"` with `aria-checked`; `multiple` is a `toolbar` of pressed buttons\n where the arrows move the highlight without pressing it. A `group` carrying\n `aria-orientation` is an axe violation, so `group` was not implementable as the\n issue's phrasing suggested, and the toolbar is the more accurate role anyway.\n \n **`native-select` is an addition and not a rival.** The existing `Select` is Base\n UI: a `button role=\"combobox\"` with a portalled popup, not a `select` element. A\n native select is right when the platform picker beats anything this package could\n draw, and wrong the moment an option needs to be more than a string. The test\n renders both and asserts one is a `SELECT` and the other a `BUTTON`.\n \n **`button-group` draws the focus ring once around the group** and the members\n suppress their own, with the click-versus-keyboard trade stated rather than hidden.\n \n **`table-sort` announces the direction it will go next, not the one it is in**, and\n its cycle returns to unsorted in three clicks, so the caller's original order is\n reachable from the control the reader already knows.\n- 3260391: `DiagramNode` carries a subtitle and a pack, and an unlabelled `Diagram` is named by its nodes\n \n **The subtitle is drawn, not only announced.** `DiagramNode` had one line of text\n and `DiagramProps` forced `label` on the non-decorative arm, so the only channel\n to a screen reader was the label. That is a text-only channel: a sighted reader\n looking at the picture got nothing, and the drawn name and the announced label\n were two strings a caller had to keep in step by hand. The subtitle is a second\n `<text>` under the name, and a node without one emits no `<text>` at all, so an\n absent subtitle is not an empty line.\n \n **A derived name makes the drift structurally impossible rather than discouraged.**\n `DiagramProps` gains a third union arm in which `label` is absent and the name is\n built from the nodes in draw order: `Tokens (the shared language), Pipeline (the\n engine), Twins`. The arm is `label?: never` and not `label?: string`, so an unnamed\n diagram and a diagram named with an empty string cannot be confused, and a blank\n label falls back to the derived name rather than announcing an unnamed image.\n \n Relation words are deliberately **not** in a derived name. Inventing a sentence out\n of the edges would make `Diagram` write words, which this system does not do; a\n caller who needs the relations announced passes `label`, and the JSDoc says so.\n \n **`pack` is per node and typed `PackId`, not `string`.** A free-text field would\n accept a name matching no emitted rule and the mark would silently keep the pack\n above it. The closed six-pack union is the one `ProductMark` already uses. It is\n per node rather than per diagram because a `pack` is a boundary on a **mark**, and\n a mark in a diagram is a node; a pack on the whole diagram would be a boundary on\n the arrangement, which is the thing the boundary law is not for. It is set on the\n `<circle>` rather than the `<svg>` or the node's `<g>`, so it re-inks that one mark\n and moves nothing else, and a circle is a shape the pack cannot re-round.\n `'default'` is the absence of the attribute, as `ProductMark` spells it.\n \n `check-pack-boundary` now reads three boundaries and reports 0 findings.\n- 4650aff: Add `Diff`, where the bar measures how much of a line changed\n \n A conventional diff marks every changed line with the same coloured wash and a\n rail. That tells a reader a line changed and nothing about how much, so rewriting\n one identifier in a long line leaves the same mark as replacing the line outright,\n and the reader's eye, which is fast at finding saturated bands, is drawn to the\n least interesting change in the file.\n \n ```tsx\n <Diff\n lines={[\n { kind: 'context', oldNumber: 1, newNumber: 1, content: 'export function run() {' },\n { kind: 'removed', oldNumber: 2, content: ' const limit = 100', changed: [[8, 13]] },\n { kind: 'added', newNumber: 2, content: ' const limit = 250', changed: [[16, 19]] },\n ]}\n label=\"Changes to run\"\n labels={{ added: 'Added', removed: 'Removed', context: 'Unchanged' }}\n file=\"src/run.ts\"\n summary=\"1 addition, 1 deletion\"\n />\n ```\n \n **The bar is change density.** Its width is the fraction of the line that actually\n changed, so a one-character edit in a long line is a sliver and a rewritten line\n fills the gutter. The marks that used to be a wash become a measurement, and the\n eye goes where the reviewer's attention belongs.\n \n **The line numbers carry the side, not the colour.** An added line has a new number\n and no old one, a removed line the reverse. That asymmetry is structural, it is how\n every diff reader a developer has used distinguishes the two, and it does not depend\n on telling red from green. The colour is redundant on top of it, which is the right\n way round: shape carries the meaning, colour reinforces it.\n \n **The changed words are emphasised by weight, not by tint.** A diff that coloured\n them green and red would spend the two hues a reader is most likely to be unable to\n distinguish, and would also fight the line's own state colour. Weight survives\n greyscale, which is the condition any encoding here has to survive eventually.\n \n **It is a table**, because a diff is two columns of numbers beside a column of\n text, and row and column navigation then come from the semantics rather than from a\n grid of divs.\n \n `labels` is required, for the same reason a Dialog's close label is a prop: a\n shared library cannot know whether the word for this is \"added\", \"ajoute\" or\n \"hinzugefugt\". `summary` is the caller's too, and a diff of a rename has no\n additions and no deletions and is still a change, which is the case a computed\n sentence gets wrong. The counts are exposed as `data-added` and `data-removed` so\n a caller can style them without the Component shipping a sentence.\n \n A test caught a real flaw in the first draft. A minimum bar width was applied to\n every changed line to keep one visible, which meant a one-character change in a\n long line was drawn at twelve percent when the data said half a percent. A\n measurement is not allowed to overstate what it measured, so the floor now applies\n only where the density is genuinely unknown, which is a changed line the caller\n gave no ranges for.\n- 3260391: `DocsShell` refuses a page with no address, and renders a link with no words as a label\n \n **This is a behavioural change and the honest reading of it is that a previously\n rendered page now throws.** Two leaks fed it. `DocsNavGroup.href === undefined` was\n the only guard, so `href: ''` fell to the anchor arm on both arms of the union, and\n `flatten` copied the same value into `Neighbour`, so one bad row published a second\n broken link in the pager. `title: string` admitted `''` on both arms, rendering a\n link announced as \"link\" and nothing else. Nothing in the type could stop either,\n and the Component's own JSDoc claimed that it could.\n \n An anchor with an empty `href` is a control a keyboard can reach and cannot operate,\n so the fix is at the tree rather than at the rendering: one pass over both `nav` and\n `toc` at the top of `DocsShell`, before anything renders, and a **page** with a\n blank address throws, naming the tree and the entry. Only the page arm is refused. A\n group with no address has a documented rendering, and a page has nothing to render\n in place of the link, so a label there would hide a page the tree is missing rather\n than report it.\n \n **Blank counts as absent, and that is required rather than pedantic.** All three\n consumer adapters write `url: node.index?.url ?? ''` for a folder with no index, so\n on the group arm `''` is the documented \"no route\" state and must render as a label.\n \n **A second bug surfaced while fixing the first.** `containsHref` read the group's\n address directly, and `under('', currentHref)` is `currentHref.startsWith('/')`,\n which is true for every absolute address. Treating `''` as \"no address\" in the\n renderer without fixing this would have made every label-only section claim to be\n the current one at once, so it reads through the same helper the renderer uses.\n \n The rail and the pager now ask one function whether an entry is a destination, so a\n row that is a label on the rail cannot become a neighbour in the pager.\n- 932ce96: Add the feedback substrate: Spinner, Toast and the Empty State Block\n \n Three Items, and the one that changes an existing Component's behaviour is the\n `Toast`.\n \n **A pause holds the remaining time rather than restarting it.** A reader who\n hovers or focuses a toast for ten seconds gets the four seconds they had left, not\n four fresh ones, because a rest is a fact about the reader and the countdown is\n about the content. Restarting the clock on every pointer move means a toast can be\n held open indefinitely by a reader who simply rests on it, which is the opposite of\n what a pause is for.\n \n **`duration` defaults to 4000 with its reason, and `0` turns the clock off.** A\n toast that vanishes before a screen reader has finished announcing it is worse than\n no toast at all, and a toast that never leaves strands the reader. Both are\n judgements about the reader rather than about the content, which is why the\n default is stated and overridable rather than fixed.\n \n **`closeLabel` is required with no default**, and it is the only surface in the\n package where a default is wrong rather than merely unhelpful. A toast arrives on\n its own initiative, with no button the reader pressed and no context to carry an\n implication about the product's language, so four products in two languages cannot\n all be told the control says \"Close\". A default is right when the caller may\n override it; here the string is the whole of the contract and guessing it is\n guessing a product's words.\n \n **The enter and leave are three phases with the handoff on the element's own\n `transitionend`**, filtered to `opacity` and with no second clock. Two timers for\n one animation is two things to keep in step, and a mismatch shows as a toast that\n fades and never goes.\n \n **`Spinner` is a server Component.** It holds no state, runs no hook and attaches\n no handler, so the client directive would be a claim about work it does not do. Its\n JSDoc states the three-way difference from `Progress` and `Skeleton`, because\n \"reports a position\", \"has the shape of what is arriving\" and \"something is\n happening\" are three different claims and the usual failure is using the third when\n the first or second is what the reader needed. The honest reason a spinner is often\n wrong is layout shift: it hides the shape of what is coming.\n \n **`EmptyState01` requires a `reason` closed to three values**, because\n `first-run`, `no-match` and `not-permitted` want different words and different\n actions, and a consumer who cannot say which kind of empty they have writes \"No\n data\", which is the state this Block exists to end. It throws rather than shipping\n a button that lies, following the `instrument-panel-01` precedent.\n \n ## Nothing was deleted, and that is a finding\n \n The fourth part of this ticket was to remove a superseded `empty` entry. The search\n was exhaustive across `packages/tokens/src`, the emitted tokens, the catalogue, the\n registry and the whole tree, and there is no `empty` token, no `empty` Component and\n no `empty` catalogue entry to remove. It was already resolved: `DESIGN.md` records\n that the old `empty` Component \"returns as `empty-state-01`, a Block\", and the\n change note that renamed it is in the history. Nothing was invented in order to\n have something to delete.\n- 932ce96: Add the form substrate: combobox, calendar, date picker, number field and the rest\n \n Nine Items, and the one that changes an existing Component's behaviour is the\n `Combobox`.\n \n **Typing never discards a choice.** A field showing exactly the chosen label is\n not narrowing anything, so \"chosen and filtered out\" is not a reachable state: the\n answer changes only on an explicit choose or an explicit clear. A combobox that\n empties its own selection as a reader types is the usual failure, and it loses\n data silently.\n \n **`Command` is the leaf row and it ranks nothing.** `matchRange` is a prop, so\n whoever filtered the list decides where the match fell, and one command object can\n be spread onto the row with its `keywords` accepted and never rendered.\n \n **`Calendar` keeps a disabled date drawn and in the arrow path, and refuses to be\n chosen.** A date that is simply absent is indistinguishable from a grid that\n failed to render that week, and a reader paging with the arrows should not have\n the path change under them.\n \n **`DatePicker` reseeds the month on show from the chosen value**, so re-picking a\n date in another month does not silently reset the grid to the month the caller\n created the field in.\n \n **`InputGroup` puts the focus on the control and the ring on the control.** The\n frame is a `div` and is never focused, because putting the ring on a wrapper is how\n an indicator ends up around the wrong box.\n \n **`NumberField` clamps a value as it arrives, not only as it is typed**, and an\n empty field reports `null` rather than zero. Base UI clamps typing and refuses the\n steppers past a bound but lets an out-of-range `value` straight through, so the\n clamp is applied on the way in and the consequence for a controlled consumer is\n documented: `onValueChange` is the authority.\n \n **`OneTimeCode` holds the code as one value, not six.** Backspace removes a\n character from the string and the rest close up, which is the only rule for an\n empty box that is not wrong for somebody. Base UI's own field strips the label from\n the first segment on purpose, so a visually hidden `<label for>` is rendered\n instead, and its steppers are put back in the tab order because an announced\n control a keyboard cannot reach is worse than a second stop.\n \n **`Form` makes the error and the way out of it one unit.** `FormError` draws the\n message and the caller's `action` in a single live region that is in the document\n before either of them, so a server error that arrives with the form is announced\n rather than appearing silently.\n \n **`Label` keeps the required mark as decoration.** The control's own `required` is\n what is announced and what the form enforces, so a second signal that says\n something slightly different is the thing to avoid.\n \n ## The ranking is now one module, not two surfaces' private copy\n \n `locate` and `RANKS` moved to `packages/ui/src/lib/rank.ts`, and the `Combobox`\n imports them from there rather than from the `CommandPalette`. A command palette\n and a combobox that each carried their own scorer would agree for a month and then\n diverge on the case nobody thought about, and a reader would find a query that\n floats to the top in one surface and sinks in the other. The first version put the\n scorer inside the palette and the combobox reached into a composite for it, which\n compiles and typechecks and is still wrong: the next surface to need one has no\n honest module to import, and the tempting answer is to copy the one it can see.\n- 3260391: A hero action is the element you asked for, and a product row can carry its own sentence\n \n **`HeroAction` is a union, so a wrong action is a compile error.** It was one shape\n with an optional `href`, which made both likely mistakes silent. `{ label: 'Start\n free' }` compiled and rendered a primary button that went nowhere, and a hero's\n first action is almost always a link, so that is the likely one and it looks\n correct on the page. `{ label: 'Start free', href: maybeUrl }` compiled too, and\n rendered a button whenever `maybeUrl` was `undefined`, which is a runtime branch the\n type said nothing about.\n \n So the link arm **requires** `href` and the button arm **forbids** it, as\n `href?: never`. The second is the one worth having: a value that is sometimes a\n string and sometimes `undefined` is now a type error rather than a button. That is\n the ticket's \"a caller should not be able to get it wrong silently\", and it is a\n type rather than a runtime branch, which is the criterion the ticket asked for.\n \n **The forward arrow now follows the element, not the position.** It was\n `index === 0`, so an inert button was the one control in the action row wearing the\n mark that says it can be followed. A first action that is genuinely a button gets no\n arrow, and a second action gets none even when it is a link, because the arrow marks\n the row's one primary destination.\n \n The default **variant** stays positional, deliberately, and the two now move\n separately. What looks primary is a fact about the row's shape; whether it navigates\n is a fact about the element. Asserting they do not move together is the point,\n because the arrow used to follow position for both.\n \n The ticket's claim that the Block \"renders every action as a plain `<Button>`\" was\n stale, the way the Cta01 one was: `CtaLink` had already fixed the rendering. What\n was left was the half that had not been fixed, the type, plus the arrow and a test\n file, which did not exist. Eight tests assert the rendered element for each arm, the\n negative as well as the positive, the arrow on a link and its absence on a button,\n and `newTab` with its `rel`.\n \n **`ProductGrid01Product` gains an optional `detail`, and the JSDoc settles which\n level a sentence belongs at.** The documentation previously said a second line\n \"belongs above the grid in `description`, where it applies to the set\". That is true\n of a sentence about the set and false of a sentence about one product, and the\n company site had five products each carrying a sentence that was true of one and\n vacuous beside the other four. The migration folded nothing in, so three published\n sentences were dropped and the ledger recorded the loss as a catalogue gap rather\n than papering over it with a copy edit.\n \n Both levels now exist and the question to ask is written down: **does the sentence\n survive its neighbours?** A `description` would be equally true if you deleted any\n one row. A `detail` is false, or vacuous, beside the other four. A tagline is the\n shortest true thing about a member and reads the same in every row, which is what\n distinguishes it from a `detail`.\n \n A `detail` is drawn only when passed. A row that reserved the line would push every\n row below it down by one, and this is a stack of full-width rules where a ragged\n left edge is the most visible thing on the page. Six tests render both shapes,\n because a test asserting only the new field would pass on a Block that had quietly\n stopped drawing the grid-level description, which is the half every existing caller\n depends on.\n \n **Two proofs that the new tests can fail.** Reverting the arrow to `index === 0`\n fails exactly one case, the one the defect lives in. Reverting\n `childLevel(headingLevel)` to a hardcoded `h3` in the card-title work fails two.\n Neither defect would have been caught by an assertion of the shape the ticket\n described.\n- 4650aff: Add `Meter`, so a bounded measurement can show the limits it is approaching\n \n A disk at 95 percent and a test at 95 percent are the same reading until you say\n where the line is. Prism had a Progress for a task moving toward an end whose\n length is not known in advance, and nothing for a quantity that already has an\n answer against a limit the caller knows.\n \n ```tsx\n <Meter\n value={97}\n label=\"Storage used\"\n valueText=\"97 gigabytes of 100\"\n thresholds={[\n { at: 80, tone: 'warning' },\n { at: 95, tone: 'destructive' },\n ]}\n >\n <span>97 of 100 GB</span>\n </Meter>\n ```\n \n **The thresholds are the design.** A fill on its own is one number, and a number\n with no limit beside it cannot be acted on. Drawing the caller's own limits as\n notches on the track puts the boundary next to the reading, so a consumer passing\n a latency budget gets a latency budget rather than a generic bar filled to a\n similar fraction. The Component has no opinion about which numbers matter.\n \n **It stays neutral below every threshold.** It can see that a value is 40 percent\n of a maximum; it cannot see that 40 percent is a problem. The same number is\n routine on a latency budget and urgent on a disk quota, so a Component that\n coloured itself would be making a claim about the caller's product on the\n caller's behalf.\n \n **It is drawn as a hairline with ticks, not as a bar.** The Progress in this system\n is a two-pixel rounded trough, and a Meter that looked like one would be read as\n one. The difference is visible before it is read.\n \n `role=\"meter\"` rather than `progressbar`, which is the ARIA distinction the whole\n Component turns on, and assistive technology reports the two differently. It is\n not a live region: the change that moved the reading is usually the thing worth\n announcing, so a consumer that wants it announced wraps it in a `LiveRegion`.\n \n Two failures are handled rather than rendered, because neither is visible in a\n screenshot. A value past the maximum is clamped to the track instead of drawn\n past its end, and a scale whose minimum equals its maximum draws an empty track\n instead of a `NaN` width, while still measuring.\n- 932ce96: Add the overlay and menu substrate: alert dialog, context menu, menubar, sheet\n \n The eight Items that every menu-shaped surface in a product otherwise hand-rolls.\n Each is composed on the Base UI primitive rather than reimplemented, because focus\n trapping, portals, dismiss-on-outside-press and escape handling are four behaviours\n that are already correct there and four chances to get one wrong here.\n \n Each one makes a decision a consumer would otherwise make badly:\n \n - **`alert-dialog`** removes dismissal from the *type*, not just the default.\n `modal` and `disablePointerDismissal` are omitted from its props, so a caller\n cannot pass a value that re-enables outside-press dismissal on a surface whose\n subject is a decision. Escape still closes, and there is no corner X, because an X\n is a third answer to a question with two. `AlertDialogAction` is its own part,\n styled destructive by default, because the one button that must exist is that one.\n - **`context-menu`** knows which rows are commands and which are destinations.\n `ContextMenuItem` is a `div role=menuitem`; `ContextMenuLinkItem` is a native\n anchor. Two parts rather than one, so the \"open in a new tab\" case cannot be a div.\n - **`hover-card`** can never be the only route to what it shows, which is what makes\n its delay safe. The trigger is a real anchor, so a long delay costs a reader\n nothing, and `delay` is a prop because a pointer rest is a fact about the reader.\n - **`menubar`** is one Tab stop and owns the arrows inside itself, and it requires a\n `label` because nothing announces a bar until focus lands on it.\n - **`navigation-menu`** knows it holds links, so it ships no command part at all, and\n a closed group holds no links with `keepMounted` as the stated seam.\n - **`sheet`** is the Dialog with an edge. `side` is required and excludes `center`.\n All six overlay behaviours are inherited, not reimplemented, and a sheet dismisses\n on an outside press, which is the contrast with the alert dialog.\n - **`collapsible`** wires the trigger and the region with the library's own\n `aria-controls` and `aria-expanded`, and unmounts the closed panel so find-in-page\n and a screen reader see the same page the reader does.\n - **`resizable`** puts the whole behaviour on a focusable `separator` with a value,\n and remembers nothing: the position is a prop and a callback, because a design\n system cannot know a reader's panes.\n \n **`resizable` is not composed on Base UI, because Base UI 1.8.0 ships none.** The\n package's exports were enumerated and there is no `./resizable` and no `Resizable*`\n symbol, so the separator role, the focusable-divider keyboard model and the pointer\n drag are authored here. That is the one hand-rolled overlay in the batch, and it is\n confined to the three things an overlay must not reimplement, none of which a\n divider needs. It should be revisited when Base UI ships one.\n \n The focus-indicators gate test no longer pins which slot happens to sort first in\n the composite-widget bucket, nor the exact excluded total. Every additional menu in\n the package adds members to that bucket, so both were tests that failed when a\n correct Component was added. The count is now bounded from below, which is the\n invariant that holds as the tree grows.\n- e14a903: `ProcessFlow01`: a pipeline of any length, drawn in order across as many lines as it needs\n \n `ProcessRail01` holds two, three or four steps and refuses a fifth in its type, and\n that decision is not in question. A rail is a claim about a sequence on one line, and\n a rail that quietly dropped a step to fit a width would be a diagram of a process that\n is not the process. A company site states six stages and was drawing them as a grid of\n short points, where the six ordinals and the terminal label were lost: a set read\n where a sequence had been.\n \n So the repair is a second shape rather than a wider tuple. Raising the rail's ceiling\n to six would have made the fourth column unrepresentable as a type error, which is\n the property that made the original decision good.\n \n **It is one ordered list, and the layout is built around that.** A flow that wrapped\n into a list per line would be several lists, and a screen reader would announce three\n lists of two, which is precisely the set-where-a-sequence-was this Block exists to\n prevent. So the stages are one `<ol>` and the wrapping is done by the grid. That has a\n consequence worth stating: the Block never learns where a line broke, and therefore\n cannot draw anything that is only correct on the widest screen.\n \n **The ordinal is the continuity mechanism, and it is treated as one.** It runs\n continuously from `01` to the last stage and it is stated as text rather than only\n drawn, so a reader who lands on stage four hears `04` and knows it continues stage\n three at whatever width they are using. Nothing else in the Block is load-bearing for\n the sequence, and that is why nothing else needs to know where the line broke.\n \n **The thread is a line through the stages, not a box around each one.** Each stage\n draws a top border and the grid separates them by a single pixel, so a run of stages\n on one line reads as one line broken by hairline gaps and the gap between two lines is\n wider. It is the rail's technique, and it is chosen here for one reason: it holds at\n any column count, including the single column a phone gets, without the Block knowing\n anything about it. A connector drawn between a stage and its successor would need the\n column count to be right and would be wrong at every width the type does not describe.\n \n **`stages` is an array and `columns` is a prop**, which is the split the ticket asks\n for. The length is content and the grid carries it; how many sit on one line is\n layout, and a caller who has to state both has to keep them in step by hand.\n \n **A flow of fewer than two stages throws.** One stage is a label, not a sequence, and\n the line the Block draws through it would claim a sequence that is not there. The\n message names `ProcessRail01` as the answer for two.\n \n ## The proof that the tests can fail\n \n The two assertions that matter are structural rather than visual, and each was proved\n by writing the mistake it exists to catch:\n \n - An ordinal that restarts per row, which is what a per-row implementation does, fails\n two cases: the continuous-ordinal assertion and the terminal-label one, because the\n last stage is then no longer the one the label is attached to.\n - A list nested per group of three, which is the set-where-a-sequence-was failure\n arriving through the markup, fails four.\n \n An assertion that the flow \"looks like\" a sequence would have passed both.\n \n The stage names are deliberately **not** headings. The sequence is the list, and\n promoting six stages to headings would put six entries in the outline for one process,\n so the section title is the only heading this Block renders.\n- 4650aff: Add `RunConsole01`, a run as a heading, a budget and the steps that got there\n \n The surface the fourth Kind exists for, and a Block rather than a Component\n because a run console is not one thing: it is a measurement beside a sequence\n beside a stream, and a reader needs all three in the same frame to answer the\n only question they have, which is whether the run will finish and what it is\n costing.\n \n ```tsx\n <RunConsole01\n title=\"Nightly reconcile\"\n streaming={running}\n copy={{\n budgetLabel: 'Budget spent',\n budgetValue: (value, max) => `${value} of ${max} credits`,\n stepsLabel: 'Run steps',\n title: 'Run',\n waiting: 'Waiting for the first step',\n }}\n budget={{ value: 4200, max: 5000, thresholds: [{ at: 4500, tone: 'warning' }] }}\n steps={steps}\n />\n ```\n \n **It reimplements nothing.** The budget is a `Meter`, which draws the limits the\n caller named and stays neutral below all of them. The steps are a `Timeline`, which\n draws each duration to scale against the slowest one. What the Block adds is the\n arrangement, and two facts belonging to neither component: a run with no budget\n gets no budget meter, because a local run has no ceiling to be near and an empty\n meter is a measurement of nothing that is invisible because an empty meter looks\n like a meter; and the budget sits above the steps, because the first question about\n a run is whether it will finish and two columns would make the reader choose.\n \n **The live region exists only while the run is arriving.** A test caught this. The\n first draft wrapped the steps unconditionally, which left a live region on the\n page for a run that had already finished: it announces nothing on mount, but it is\n still there, and a later re-render with different steps is an unrelated change it\n will announce. Mounting it when the run starts is also the order that does not\n lose an announcement, because the region is on the page before the first event\n arrives, which is when a screen reader is listening. It wraps the steps and not the\n budget, so an append does not re-read the spending every time.\n \n **`LiveRegion` is a server Component, and that is a correction.** It shipped in\n 0.8.0 carrying `'use client'` while reading its props, holding no state, running no\n hook and taking no event handler. The directive was a claim about work the\n Component does not do, and sixteen of forty-one components in this package are\n client. A live region is announced by the browser's own mutation observer rather\n than by JavaScript, which is the reason it is a good primitive: it works in a\n server-rendered page.\n \n ## The client budget\n \n The whole-tree client bundle measures **108.1 KB** with this branch's components\n in it, against a 116 KB ceiling, and the five components here cost about **0.1 KB**.\n \n That is worth stating plainly, because the number looked very different for a\n while. An earlier draft of this branch reported that the components added 2.3 KB,\n crossed a 92 KB ceiling, and needed the ceiling raised. That measurement was taken\n against a gate whose roster read two directories, and it was wrong for the same\n reason the old ceiling was: 53 emitted client modules were never read. The roster\n has since been widened to the whole tree, the honest figure is 108 KB, and these\n components land inside the existing ceiling with about 8 KB of headroom.\n \n **No ceiling change is proposed here.** The one that was drafted has been dropped\n rather than applied, because it would have moved a number to accommodate a\n measurement that was itself measuring the wrong set of files.\n- 3260391: `StackGrid01` names the real product behind a codename, and `Cta01` states the rule it enforces\n \n **`StackPart` gains an optional `realName`.** The value goes inside the tile the\n Block already draws, so it is an additive optional field, matching `ProductGrid01`'s\n `pack` and `StatusLedger01`'s `detail`. It renders only when passed: a tile that\n reserves a line for absent content is a layout shift on the first thing a reader\n scrolls to, and because the grid is a grid, an empty line in one column would also\n knock the row's baseline out for every tile beside it. Both are asserted, the\n second by counting elements rather than by checking a class.\n \n **`Cta01`'s JSDoc told consumers the opposite of the truth.** It said a panel wants\n `default` or `secondary` and never `outline`, which was the answer *before* the\n `outline` variant was given its ink, and it contradicted the code forty lines below\n it, where the second action defaults to `outline`. A consumer reading the type would\n have been actively misled. The settled rule is the inverse on the first variant:\n `secondary` and `outline` are both fine and `default` is the one to avoid, because\n the band is `bg-primary` and `default` fills with `--primary`, so a default action\n on this band is the band's own colour against the band's own colour. Its label is\n legible, because `primary-foreground` on `primary` is a gated pair, and the control\n has no edge, which is a different defect and the reason the sentence is about the\n fill rather than the text.\n \n **A test that measured a pair it believed in.** The existing resolved-ink test\n asserted the pair the test itself thought the defaults resolve to, so it stayed green\n on a Block that had stopped asking for it. The new case renders `Cta01` with no\n explicit variant, reads the utilities off the two anchors, maps each to the token it\n names, and resolves those tokens from the emitted CSS. It asserts the resolved pair\n rather than the class string, because a class-string assertion passes on a token\n change, and it adds two things a ratio cannot see: that the ink is stated rather than\n inherited, and that the fill is one the band is not.\n \n The ticket's single lavender-dark figure understated the blast radius. The base pack\n failed in **both** modes, at 1.00:1 and 1.01:1, and seven of twelve combinations were\n affected. It is 13.59:1 or better in all twelve now.\n \n **The acceptance criterion asked for the wrong instrument.** It asked for a row in\n the token contrast gate, and that gate structurally cannot see this class of defect:\n it measures token pairs, and what failed was a component declining to use a value it\n was relying on by accident. `check-variant-ink.mjs` is the gate that holds it, and it\n already existed.\n- e14a903: The fourth Kind: `live`, a surface whose content changes without a navigation event\n \n **This is a breaking change, released as a minor: 0.11.0.** `CATALOG_KINDS` gains a\n member, so a consumer switching exhaustively over `kind` is broken. A minor is the\n right line for a breaking change from a `0.x` version, where anything may change at\n any time, and taking 1.0.0 would declare the public API stable rather than describe\n this change. `CONTRIBUTING.md` says a breaking change is a `major` bump, so this is\n the one place the convention and the record disagree; the disagreement is deliberate\n and `DESIGN.md` carries it with the reasoning.\n \n **A `live` surface is what the other three cannot express.** A Component, a Block and\n a Page are all rendered from props, and props arrive when the caller says so. A run's\n event log, a monitoring view, anything fed by a socket, changes on its own. That is\n the whole of the fourth Kind, and it is why `RunStream01` is the first client\n Component in the package: a surface that receives events owns the subscription that\n delivers them, and that is not an accident of implementation.\n \n **Prism owns the surface, the consumer owns the transport.** `subscribe` is a\n function the consumer supplies and a function it tears down. There is no socket, no\n endpoint, no retry policy, no persistence and no provider to mount, so a consumer\n that already has a connection passes it in and one that does not has not acquired a\n client runtime by importing this.\n \n **Four transcriptions were found by the compiler rather than by a reader**, which is\n the argument for the ties that already existed. `STORE_KINDS`'s `_KindsMatch`\n assertion fired on the first edit. The exhaustive `KIND_LABELS` record added\n earlier fired the next, and adding a Kind is now a compile error in both places. The\n corpus builder's `KIND_SEGMENT` produced a mirror path of `undefined` and the\n `llms` gate reported it. And the site's three copies of the Kind labels were\n collapsed into one table, so the fourth Kind is one edit rather than three.\n \n **One divergence is deliberate and named.** A `live` surface ships as\n `registry:block`, because the `type` field belongs to the shadcn registry schema,\n which has no live surface and rejects a type it does not know. The **Kind** is ours\n and is `live` in `CATALOG_KINDS`, the corpus, the MCP tools and the site's Sections.\n The registry is a derived internal artefact, never served and never an install lane,\n so the two vocabularies are allowed to differ because only one of them is ours. The\n catalogue gate carries it as a named exception keyed on the Item's own `source`, so\n the exception cannot drift from the thing it excuses, and the other 103 items are\n still compared strictly.\n \n **The Section plurality rule is gone, and two Sections is why.** It read that a\n Section holding Items is plural. `patterns` broke it from the prose side and `live`\n from the catalogue side, because `components`, `blocks` and `pages` are plural nouns\n and `live` is an adjective with no plural. A rule two of nine Sections decline is a\n convention, not a law, and a second exception would have invited a third. What is\n asserted instead is the fact underneath it: every Section is registered once, in the\n root ordering, at a segment the manifest agrees with.\n \n ## The proof that the tests can fail\n \n `RunStream01`'s fourteen tests are proved by writing the mistakes they exist to catch.\n Appending instead of sorting, which is the reconnect bug, fails the ordering cases.\n And three defects were found by the tests rather than reasoned about in advance:\n \n - **`statusLabel` rendered twice.** The heading fell back to it when no `title` was\n given, so \"Failed after 3 attempts\" was the surface's subject *and* its state. A\n status is not a heading.\n - **`initial` was a trap.** It read like the current event list and quietly seeded\n only once. It now also *reseeds* when `resubscribeKey` changes, which is what a\n second run in one surface means, and the log clears with it rather than the old\n run's events growing the new run's.\n - **A `setNewest` before its declaration**, which the run-switch test caught as a\n temporal-dead-zone error and which no type checker in this repository would.\n \n **A `live` surface is budgeted, not exempted.** A Block composes Components, so its\n own client figure restates theirs. A live surface is the only client code a consumer\n pulls in *for itself*, so it has a row: 10 KB, of which `ScrollArea` is 8.1. The\n figure is set above the measured size, because a budget below it is a wish.\n- 4650aff: Add `Timeline`, so a run of events can be read for where the time went\n \n A run's events in order answer \"what happened\" and are silent on the only other\n question anyone has about an agent run, a deploy or an import: which step was\n slow. Today that means reading every number and doing the arithmetic.\n \n ```tsx\n <Timeline\n entries={[\n { id: 'plan', title: 'Plan', duration: 900 },\n { id: 'read', title: 'Read feed', duration: 4200 },\n { id: 'edit', title: 'Edit source', duration: 1200 },\n { id: 'done', title: 'Ready' },\n ]}\n label=\"Run steps\"\n />\n ```\n \n **The bars share a left edge and are scaled against the longest step in the run.**\n That is the whole idea, and the relative scale is the deliberate choice: the shape\n of the run is the question, and a shared zero baseline would be a second axis\n nobody reads. The consequence worth stating is that the bars answer \"which step was\n slow\" and deliberately do not answer \"how long did the run take\", because only the\n caller knows whether its steps ran sequentially or overlapped.\n \n **A step with no duration draws no bar.** A run's opening event has no length, and\n a zero-width bar beside it would read as \"this was instant\" rather than \"this has\n no duration\", which are different claims and only one is true.\n \n **It is an ordered list, so the sequence and the count are in the accessibility\n tree.** The marks and spine are `aria-hidden` on the rail that holds both, and the\n entry's own words carry the state, so nothing depends on a reader distinguishing\n the marks. It is not a live region: a run still arriving is normally wrapped in a\n `LiveRegion` by the caller, because announcing the list itself would re-announce\n the whole run on every append.\n \n Three degeneracies are handled rather than rendered, and all three are invisible in\n a screenshot: a run where no step reports a duration draws no bars rather than\n dividing by nothing, a zero duration does not win the longest-step comparison and\n scale everything else away, and the spine stops at the last entry rather than\n trailing past the end of the run as though a step had not arrived.\n \n The four states are a different vocabulary from the four product-capability tiers\n a `StatusLedger01` row carries, and they are two lists about two subjects rather\n than two lists about one: a `planned` agent run is a category error and a `failed`\n capability is a category error, which is the test that tells them apart.\n\n## 0.10.2\n\n### Patch Changes\n\n- `Hero01`'s headline and supporting line take a node rather than a string\n \n `SectionHeading` has always taken a `ReactNode` for both, so a `string` here made\n this Block the odd one out rather than making it stricter. Three of the five\n product sites set the emphasised word in their own mark inside the headline, so\n the narrow type would have cost each of them the emphasis or a second component.\n \n A widening, and additive. Every string a site renders is still the site's own, and\n `check-block-copy.mjs` still holds this Block to shipping no copy of its own.\n\n## 0.10.1\n\n### Patch Changes\n\n- A drawing's data props accept a read-only array\n \n `PulseGraph`, `PulseSeries` and `Diagram` took `SomeNode[]` rather than\n `readonly SomeNode[]`, so a consumer whose content is held as `as const` had to\n copy the array at every call site to satisfy the type. Every NaniSoft product site\n holds its landing content as `as const`, so the copy was not optional: the first\n consumer to compose a running hero had to write `nodes={[...FIGURE.nodes]}` to get\n it to compile, which is a cost in exchange for nothing.\n \n The props now take a read-only array. This is a widening, so it is additive rather\n than breaking, and it is the correct shape for a prop that only reads its data: a\n Component that mutates an array a caller passed is a Component that has taken\n ownership of it, and none of these three do.\n\n## 0.10.0\n\n### Minor Changes\n\n- 75434f8: Add a running figure to the system, and the ambient cycle scale that prices it\n \n The system had one law of motion: motion is state feedback, on an 80/160/280\n millisecond scale, and there are no keyframes anywhere. That law was right and\n it held. It also meant a product page could say what its product does and show\n nothing about it, which is what happened to four sites at once.\n \n This adds the second law, and it is a separate law rather than an exception to\n the first. Feedback is a response to something the reader did. A cycle is a\n demonstration of something the system does, and no reader is waiting for it.\n The test for which one a motion is does not require taste: stop the animation\n and ask whether the figure is still true and still legible.\n \n **New Components**\n \n - `PulseGraph` draws a running system: named nodes, the relations between them, a\n marker travelling a rail, and a breathing focus. A node's `lane` says it is a\n stage in a sequence, which draws the rail; a node with no lane is a field.\n `carries` on a relation says the line is carrying something, and draws a head\n at its end so the claim survives motion being off.\n - `PulseSeries` draws a live instrument: columns rising out of a caller-named\n baseline, with a reticle crossing them once per cycle. The values are the\n caller's own and the tallest sets the scale.\n - `SignalField` is a field of marks: the atmosphere a figure is drawn over.\n Decorative by default, still unless asked to drift, and honestly named as the\n one component in the system that is texture.\n \n **`Hero01` gains the band it was missing**\n \n All four sites hand-wrote the same two-column grid, each with its own gap and its\n own breakpoint. It is now the `instrument` slot, and an action naming an `href`\n renders as a real link rather than a button that goes nowhere. The centred form\n is unchanged, so this is additive.\n \n **New tokens**\n \n `ambient` and `ambient-ease` are two closed groups measured in seconds, separate\n from `duration` because folding them in would have made one name mean both 280ms\n and 7.2s. The emitted contract asserts the closed set and fails any cycle under\n one second, because a sub-second cycle is a flicker and no value in the 80 to\n 280ms band could ever have caught that.\n \n **What this does not change**\n \n Nothing is hidden. No ambient rule sets `opacity: 0` and nothing waits for a\n script, an intersection or a timer, so every figure is complete at first paint\n and a consumer needs no exception to enable the `hidden-state` gate. Reduced\n motion is one `animation: none`, and because every resting state is the full\n form, that reader gets the same figure, still. All three are server Components,\n so this adds zero bytes of client JavaScript.\n\n### Patch Changes\n\n- cb16073: A variant that sets its own fill sets its own ink, and `outline` now says which\n \n `Button`'s and `CtaLink`'s `outline` variant carried `bg-background` with no `text-` for the life of the package. On the page ground and on a card the omission was invisible, because the ink a control inherits there is `foreground` and `card-foreground` is equal to it in all twelve pack and mode combinations, so the variant was correct everywhere the design system itself placed it and wrong everywhere else.\n \n `Cta01` draws its band as a filled `bg-primary` surface with `--primary-foreground` as the band's ink, and defaulted its second action to `outline`. The second action was therefore `--background` behind `--primary-foreground` and measured **1.00:1 to 1.10:1 in seven of the twelve combinations**: the base pack in both modes, where light mode pairs `#ffffff` with `#ffffff`, and all five pastel packs in dark mode. The button was in the document, focusable, announced, and unreadable. It was found by resolving computed values in a browser, because neither the class name nor the token contract says anything about it.\n \n The fix is `text-foreground` on the variant, which is a no-op on the page ground and on a card and correct everywhere else. Measured across all twelve after the change: lowest is 13.59:1.\n \n A consumer that passed `variant=\"secondary\"` on that action, as the product site did, saw no difference and needs no change. The default is unchanged, deliberately: `outline` reads as one filled action and one alternative, and it is now a safe default because it carries its own ink rather than because it was made to stop.\n \n Two gates hold it, and neither could have caught the original:\n \n - `check:variant-ink` fails a variant whose own fill has no `text-` utility of its own, read from the source. It reads source rather than the emitted stylesheet on purpose: Tailwind only emits a variant something uses, so the sheet is a record of what was chosen rather than of what is available. A state fill such as `hover:bg-accent` is exempt, because exempting it is the difference between a rule that finds the defect and a rule nobody can run.\n - a test resolves the pair from the emitted token CSS for all twelve combinations rather than asserting a class name, and pins the seven that used to fail by name. A test written over class names would have passed against the broken button, because the class name never changed; only the resolved pair did.\n \n `Cta01Action`'s `variant` now states which weight a panel wants and why, with the measurement, so a consumer does not have to discover it.\n- Updated dependencies [75434f8]\n - @nanisoft/prism-tokens@0.10.0\n\n## 0.9.0\n\n### Minor Changes\n\n- 93758a6: The face ships with the package, so a site writes one property for its typeface\n \n `@nanisoft/prism-ui` declared `--font-sans: Inter, ui-sans-serif, system-ui, ...` and shipped no font file. Naming a family is not shipping it: the first entry resolved to nothing and every page fell through to the platform's UI face, which is the one face a design system never means by its first choice. All four consumer sites were compensating with their own custom property and a build-time network fetch, and the company site had already shipped that way by accident.\n \n The face is here now, as three latin roman weights with the licence beside them, and the `@font-face` sources are relative to the emitted stylesheet so they resolve from a consumer's install rather than from the consumer's origin. That last point is the old line's failure exactly: it emitted the face at an absolute path, which resolved against whichever site imported it, and the face silently failed in all four repositories.\n \n A consumer's own override is now unnecessary rather than merely discouraged. `--font-sans` resolves to a face this package ships, so a site that deletes its `--font-sans` override still renders in the design system's face rather than in the platform's.\n \n Three gates hold the arrangement, and none of them is discipline:\n \n - `check:typeface.mjs` fails a token family that names no face this package ships and no family the reader's own machine provides, read from the **emitted** stylesheet rather than the source. The original defect was invisible in source: the rules shipped, the subsets were preloaded, and only the class carrying the variable was missing, so the family resolved to nothing at computed-value time with no error anywhere.\n - the same gate fails an `@font-face` source that is absolute or a web-root path, which is how the old line failed silently.\n - the build refuses to emit a stylesheet that names a face it did not copy, so a package cannot ship a sheet pointing at a file that is not in the tarball.\n \n The face is 70.7 KB and every consumer installs it whether or not it renders a page, because a subpath does not reduce install size. That is the accepted price of one package instead of two.\n \n The weight axis is absent, deliberately. The design system retired the width axis, and optical sizing, which it approximated, is the browser's initial behaviour, so a file carrying a width range is not the file this design asked for. 700 is absent for a second reason: the authored scale emits it and the shipped source renders it nowhere, and a 23.8 KB file no page renders is 23.8 KB every consumer downloads forever.\n\n## 0.8.0\n\n### Minor Changes\n\n- 86cbc20: The accessible name a control announces is now a prop, so a consumer can localise it\n \n `Pagination`, `Dialog` and `Breadcrumb` each shipped an `aria-label` that a\n consumer could not change, and `ProductSwitcher` shipped a `label` default that\n put the word \"Products\" into every consumer's navigation. A consumer could\n localise the visible text of a pagination step and not the name a screen reader\n announced for it, which left a control that looked localised and was half of it.\n \n What changed:\n \n - `Pagination` takes `label` for the region name, and each of `PaginationPrevious`\n and `PaginationNext` takes `label` beside its existing `text`, so the visible\n word and the announced name move together.\n - `Dialog` takes `closeLabel` for the built-in close control, so a localised\n dialog no longer announces \"Close\" in a product that never uses that word.\n - `Breadcrumb` takes `label` for its region name.\n - `ProductSwitcher` no longer defaults `label`. A switcher with no `label`\n renders a navigation with no accessible name, which is a real state the caller\n resolves, rather than one that is quietly wrong.\n \n Every new prop keeps the word Prism would have used as its default, so nothing\n changes for a consumer that never set one. The one behaviour that does change is\n `ProductSwitcher`'s, and it is the point: the default was a claim about a\n consumer's product that the prop existed so they would not have to make.\n- b31e444: Ship the consumer gate kit, so a cross-repository law reaches a consumer in one release\n \n The four repositories that consume this package coordinated through a prose\n contract mirrored in four files. It could not be enforced, because prose cannot\n fail, and it had already drifted: two sites that mattered held three\n implementations of one rule and one of the three rules was false.\n \n The laws are now the failure messages of gates in this package, and a consumer's\n repository holds only its own data.\n \n ```sh\n pnpm exec prism-gates # every gate prism-gates.json names\n pnpm exec prism-gates --gate=links # one gate\n pnpm exec prism-gates --json # machine-readable, for a test\n ```\n \n Configure it with a `prism-gates.json` at your repository root. A consumer holds\n its stylesheets, its pack map, its coverage floors, the destinations its own\n corpus gets wrong, and the custom properties its own build supplies. It does not\n hold the wording of a rule, because a wording held in four places is four rules\n that will disagree.\n \n The gates are `pin` (the design system is an exact version, and the token package\n is this package's dependency rather than yours), `retired-line`, `stylesheet-ownership`\n with `token-read`, `links`, `pack-boundary`, `hidden-state` and `runtime-token-read`.\n \n **This release adds the `gates` export subtree and a `prism-gates` binary, and\n adds `gates` to `files`.** It also means a consumer no longer needs to declare\n `@nanisoft/prism-tokens`: resolve it through this package, which requires it at an\n exact version, and the pair cannot be mismatched. Removing the token package from\n a consumer's `package.json` and from its `pnpm-workspace.yaml` is part of the\n adoption; the `pin` gate fails on either.\n \n The kit is outside `dist/` on purpose. It is a build-time program for another\n repository and must never enter a consumer's module graph or its bundle.\n- 86cbc20: Add `LiveRegion`, a region that announces what just changed in it\n \n Prism had no live region at all, so a result list that filtered as you type, a\n search whose count updated, and any stream that appended to the page all changed\n silently for anyone using a screen reader. The fourth Kind the design rules\n already decide, `live`, is specified to own an event log surface, and an event\n log surface is a live region, so this is the prerequisite rather than a\n convenience.\n \n ```tsx\n <LiveRegion busy={streaming} label=\"Run output\">\n {lines.map((line) => (\n <p key={line.id}>{line.text}</p>\n ))}\n </LiveRegion>\n ```\n \n Three decisions are in the Component rather than left to each consumer to make\n the same way:\n \n - **It renders nothing when it has nothing to say.** A live region that is\n always present announces every unrelated state change of its ancestors, so an\n empty region is not rendered at all rather than rendered as an empty element.\n The test is \"renders nothing\" rather than a list of the nothing values, because\n an empty string, a null state and a false condition are three routes to it and a\n list of three is a list a fourth route would miss.\n - **The politeness default is the least interruptive value still announced.** A\n run log that announces assertively interrupts a screen reader mid-sentence, and a\n consumer with a genuinely urgent event passes `politeness=\"assertive\"`.\n - **`aria-busy` is about the caller's knowledge, not about an animation.** A busy\n region is still readable, and a caller that sets it permanently has told\n assistive technology the stream never ends.\n \n The component owns the region and not the transport. A consumer owns the socket,\n the retry, the persistence and the order events arrive in, which is the same split\n the documentation Page makes with its navigation, and it is why Prism stays\n transport-agnostic and no consumer inherits a connection it did not ask for.\n- 86cbc20: Add `Mark`, so a search result can show which characters matched\n \n A command palette and a search result list both need to show why a result\n matched, and until this existed the only way to do that inside Prism was to drop a\n span with hand-written styling into a consumer's own markup, which the authoring\n contract does not permit.\n \n ```tsx\n <Mark text=\"component library\" ranges={[{ start: 0, end: 9 }]} />\n ```\n \n It takes the string and the ranges rather than pre-split nodes, because a caller\n that assembled the nodes had to compute the ranges and this Component has the\n string. The ranges are handled rather than refused: they are sorted, overlapping\n runs are merged, and a range that runs past the end of the string is clamped. Each\n is a thing a search backend does as a matter of course, and a Component that threw\n on any of them would be one a consumer has to wrap in a try, which is a worse\n failure than a highlight that stops at the last character.\n \n **It adds no semantics, and that is the decision rather than an omission.** A\n consumer marking a hit wants a visual difference, not a screen reader announcing\n \"highlighted\" between every character of a result. So it styles a `mark` and puts\n nothing else on it, and a consumer who wants the announcement writes it.\n \n The treatment is the existing warning token pair rather than a new role. A mark\n sits behind the text it marks, so it needs a ground and a foreground, and that\n pair is already measured against every surface in every pack and both modes by the\n token contrast gate. A search hit is emphasis and not a caution, and the two are\n kept apart by spending the existing token rather than by adding a role that means\n \"highlighted\" and will be re-pointed at something else within a year.\n- 86cbc20: Add `Tree`, so a file browser, a category tree and an outline have something to build from\n \n Prism had a working tree and could not reach it. The implementation existed as\n two private renderers inside the documentation Page, with its depth rule, its\n current-item marking and its rule that a group with no index renders a label.\n What was missing was that it was unreachable, and that its data type was bound to\n documentation navigation rather than being a tree's own vocabulary, so a file\n tree, a category tree, an outline and a knowledge base all had nothing to build\n from.\n \n ```tsx\n <Tree\n label=\"Documentation\"\n currentHref=\"/foundation/colors\"\n nodes={[\n { type: 'group', title: 'Foundation', items: [\n { type: 'page', title: 'Colors', href: '/foundation/colors' },\n ] },\n { type: 'divider', title: '' },\n { type: 'page', title: 'Overview', href: '/overview' },\n ]}\n />\n ```\n \n **It is navigable by the arrow keys**, which is the part a consumer cannot\n assemble for themselves. A tree is one Tab stop and the arrows move inside it.\n Composing the markup gives the Tab order of the document instead, which is every\n node in the tree and none of the arrows. The flat order is collected from the DOM\n rather than computed from the node data, so there is no second list to fall behind\n the first, and a label is skipped rather than focused and doing nothing.\n \n **A flat list renders a flat tree.** An outline is a tree of depth one, so a caller\n passes leaves and gets leaves. A Component that required nesting to express a flat\n structure would make the common case the awkward one.\n \n **A group with no index is a label, not a link.** This is the rule the\n documentation Page already reasoned at length and it is carried across rather than\n reinvented: a group with an index is a destination and renders an anchor, a group\n without one renders a span that carries no `href`, is not focusable, and cannot be\n reached by Tab. Inventing a route for it would publish an address that resolves to\n nothing. The empty case is authored too, because a tree that renders nothing is a\n control with nothing in it.\n \n The documentation Page keeps its own renderers and does not change. Its two rules\n are specific to a documentation rail, which is one shape a tree takes rather than\n the shape. Whether the Page later composes this Component is a separate decision\n this does not make.\n\n## 0.7.0\n\n### Minor Changes\n\n- 5c43520: Add `DocsShell`, a documentation page that takes a site's navigation as data\n \n `DocsShell` is a documentation screen: a navigation rail, the document, a\n contents rail, and a pager to the neighbouring pages. It replaces the unstyled\n `docs-shell` the retired line published, and it is the Page the three NaniSoft\n product sites already import by that name.\n \n The navigation is data. A tree of one page and a tree of twenty-seven render the\n same way, at any depth, and the frame is sized by what the caller passed rather\n than by a fixed arrangement, so three sites whose documentation sets differ in\n shape share one Page with no per-site fork.\n \n - `nav` and `toc` are closed unions of `page`, `group` and `divider`. Both are\n read in the order given: nothing is sorted or alphabetised, so a site that\n files its rail as a pipeline and its contents as a reference has both in one\n render.\n - The pager is derived from `nav` and `currentHref`. There is no `neighbours`\n prop, so a neighbour that is not in the tree cannot be expressed, and each\n consumer loses one derivation of its own.\n - A group with no `href` renders a `span` rather than an anchor with no\n destination. The three sites spell that case today as an empty string, and two\n of them carry a stylesheet rule that styles the result back into a label; those\n rules are now deletable.\n - A section carries no status, no badge, no count, no collapse and no sort. A\n status written into a title stays in the title as words.\n - Both rails are bounded against the viewport, because one site's tree is half\n again as tall as another's.\n \n The corpus fix that shipped with it: the props extractor read each declaration\n body twice with two depth counters that disagreed, so a Page whose `index.tsx`\n re-exported a value and a type from one sibling published every prop twice and\n swept the members of its navigation union into its own prop list. It now reads\n each body once.\n\n## 0.6.0\n\n### Minor Changes\n\n- 956fc8f: A call to action with a destination renders a link, and an icon is required only where a tile is\n \n Every primary call to action in the Block family declared an `href` its type then\n never rendered. The control was a `<button>` with no handler: the type said the\n component could navigate, the documentation implied it, and the markup could not\n do either. Sixteen of them across the family, five of them to another origin.\n \n **`CtaLink` is a new Component.** It renders a native `<a>` and carries the\n visual weight of a call to action, so assistive technology announces a link and\n the browser's own affordances all work: a status bar showing the destination, a\n context menu to copy it, middle-click to open a new tab. It is a Component in its\n own right rather than an `as` or `href` prop on `Button`, which is the answer\n `BreadcrumbLink` and `PaginationLink` already give twice in this package. It is a\n server component, so the swap costs no client JavaScript.\n \n `href` is required on `CtaLink`: an anchor without one is not a link. `newTab` is\n a declared prop and the only way to open a new browsing context, because `target`\n is not accepted, so the relationship cannot be opted out of by accident. When it\n is set, `rel` defaults to `noopener noreferrer`; a consumer that needs a\n different relationship passes `rel` and it wins. Prism does not parse the\n destination and does not decide what counts as external.\n \n **`Cta01` no longer offers an action that navigates nothing.** `Cta01Action`'s\n `href` is required, there is no optional arm and no dead-button arm, and a new\n `actionSlot` takes a control the Block does not own, such as a client router's\n link. The Block makes no guess about cross-origin. The one rendered change in\n this release is a button becoming the link its type always said it was: the link\n is styled from the same recipe as the button it replaces, at every shared variant\n and size, and a test asserts that class-for-class.\n \n **`FeatureGrid01`'s icon is required only where a tile is rendered.** The\n requirement is a union discriminated on `variant` rather than an optional prop,\n because an optional prop turns a safe-at-render condition into an unsafe one and\n permits an empty accent tile per feature. The `icon` variant is the one an\n omitted `variant` selects, so the default and the required icon live on the same\n arm; the `bare` variant renders no tile and requires no icon, and the type says\n so rather than the renderer finding out. `IconFeature` and `BareFeature` are\n exported, and the union is the type the site's API table is extracted from.\n \n **The version the four downstream site plans pin is `0.6.0`.** It is\n `@nanisoft/prism-ui@0.6.0`, and `@nanisoft/prism-tokens@0.6.0` with it, because the\n two are linked in `.changeset/config.json` and the pending\n `a-server-rendered-pack-boundary-can-be-mode-correct` changeset already declares a\n minor for both from `0.5.1`. This one declares a minor as well and so does not\n raise it.\n \n Migration: a `Cta01` action needs a destination. Where the action was a label\n alone, give it the `href` it was always declared with, or move the control into\n the new `actionSlot`. A `FeatureGrid01` in the `bare` variant can now drop the\n `icon` its type used to demand; the `icon` variant still demands one per feature.\n- d001983: A diagram is a Component, and a product's mark is one item rather than two copies\n \n Three of the four product sites draw something in the position where an empty box\n is most visible, and all of it was a canvas. A canvas reads computed style from\n one element while a pack is an attribute on an ancestor, so a scoped `data-pack`\n boundary hands it the pack's light values on a dark page, and no token gate can\n see it: the value the drawing holds is a legal token value, applied to the wrong\n pack. Cutting the canvases without a replacement is the alternative, and it\n leaves three heroes with an empty slot.\n \n `Diagram` draws named things and the labelled relations between them as inline\n vector markup. Every stroke and every fill is a semantic utility, never a\n resolved value, so a boundary restyles the drawing through the cascade exactly as\n it restyles a heading. It is a server Component: no hook, no mode, no context, no\n prop spread, and no client code, so a consumer renders it from a server file\n with no provider mounted. Its node mark is a circle and a relation is a path\n because both are shapes with no radius concept, which is what keeps a boundary\n from moving anything but colour; a `<rect>` was rejected because its corner\n attribute is a CSS property and no utility pins it. Every mark names an explicit\n stroke width, an explicit stroke and an explicit fill, because a shape left to\n its defaults either paints opaque black over its own labels or resolves its edge\n to the surface it sits on. A relation naming a node it does not have is dropped,\n and the count of dropped relations is on the element rather than swallowed.\n \n `ProductMark` promotes the two-part mark the product row and the product switcher\n each hand-wrote into one catalogue item. The ring is `brand-ink` and the core is\n `primary`, which is not a preference: a pastel brand value is a fill and never\n text, so the core may be `primary` and the name may not, and a wordmark reads\n `brand-ink` rather than `foreground` or `primary-foreground`. A hand-written copy\n gets one of those two wrong and nothing in the rendered result says which. The\n mark carries its own `data-pack` boundary, so a product's hue follows from the\n pack the caller passed rather than from whichever pack the page is wearing, and\n the boundary sits on an element with no radius utility, which is the placement\n the pack-boundary law allows. A product with no pack of its own is drawn as the\n full spectrum rather than as a colourless mark, built from the five series tokens\n because they are the only five-way colour set the contract publishes.\n \n `scripts/check-vector-ink.mjs` holds both Components to the emitted contract. It\n fails a literal colour, a gradient whose stops are not contract references, a\n `var(--x)` naming a property the contract does not publish, and a paint utility\n whose suffix is not a contract role, which is the last of the four a colour\n regex cannot see because a Tailwind class is a name and not a value. Its scanned\n set is a declared list of two file names rather than a pattern, a Component that\n is not on disk fails the run rather than emptying it, and every exclusion carries\n a reason and is printed on every run, with an exclusion that resolves to nothing\n a finding rather than a line that quietly stops appearing.\n- 4f9ce77: A server-rendered pack boundary can now be mode-correct\n \n The only dark selector for a pack was `[data-pack=\"<id>\"].dark`, which requires\n the mode class on the same element. A server cannot know the reader's mode, so a\n subtree carrying a second pack rendered in that pack's light values on a dark\n page. The token build now emits the two-member list\n \n ```\n [data-pack=\"<id>\"].dark, .dark [data-pack=\"<id>\"]\n ```\n \n as one rule with one declaration block, for all five packs. The compound half is\n retained, because it is what a dark surface on a light page is and it is a\n capability the package already published.\n \n Consumers get a second change alongside it. `themeAttributes` now takes `mode` as\n optional, because a required one is that same impossibility written into the type:\n the only way to ask for a boundary with no mode class of its own was to pass\n `'light'` as a guess, and the guess is wrong for half of readers. Omitting `mode`\n is the honest spelling of \"wear the mode of the element carrying `.dark`\". The\n change is a widening, so every existing caller still compiles.\n \n The theme-boundary law in `DESIGN.md` is rewritten to match. It previously\n instructed an implementer to put `data-pack` and `class=\"dark\"` on a boundary\n together, which works for a client-applied boundary and cannot work for a\n server-rendered one.\n- c126ee3: A stored theme is written when a reader decides, and not before\n \n `PrismProvider` wrote `prism-theme` on every resolved change, including the\n first resolution from the server-rendered attributes or from its own defaults. A\n first-time visitor therefore ended the load with a stored value they never chose,\n and that value outranked the site's default on every later visit, so a site that\n changed its default could not reach anyone who had ever loaded the page.\n \n The write is now gated on a decision. It happens when a caller changes the pack,\n the mode or the toggle, and when the library recovers a decision from\n `prism-theme-mode`, a key earlier lines wrote and this one does not. It does not\n happen on a first resolution from the document or the defaults, so a first visit\n ends with an empty store and a returning visit keeps its choice.\n \n Two readers of one stored value now share one rule. `resolveTheme` in\n `@nanisoft/prism-ui/theming` is the rule, `PrismProvider` calls it, and the boot\n script implements it as a string. The script cannot import the function, because\n the build minifies and a renamed identifier inside its own `try` would throw\n there and fail open, so the two are held together by a build-failing equivalence\n gate that runs the emitted string and the rule over one table of stored values:\n nothing stored, a valid pair, a valid mode with a retired pack, a valid pack with\n a retired mode, garbage, a JSON array, a JSON string, `null`, an empty object, a\n partial, a server-rendered pack and mode, and the retired key. Every fall-through\n is asserted to be whole, which is the divergence this fixes: the old string\n validated the pack and the mode independently, so a stored `{ mode: 'dark' }` with\n no pack half-applied there and fell through whole in the rule.\n \n A value that is present but is not a pair is no longer repaired field by field,\n and it is not cleared. It is the only record that this reader ever chose anything.\n \n The boot script records where the theme came from in `data-theme-origin` on\n `<html>`, and it is the only writer: the provider never touches it, because two\n writers would make the origin a race rather than a record. The value is one of\n \n | Value | Meaning |\n | --- | --- |\n | `stored` | the key held a valid `{ pack, mode }` pair |\n | `legacy` | the key was empty and `prism-theme-mode` held a recoverable mode |\n | `unparsed` | the key held a value that is not a pair, and the theme fell through whole |\n | `document` | the key was empty and the root element's own attributes supplied the theme |\n | `default` | neither did, so the consumer's defaults apply |\n \n It is written on every path the script reaches, so an absent origin is never an\n unread one. It is not `data-theme`, which the migration guide retires; it records\n the provenance of a resolution and never a pack.\n \n The migration clause reads `prism-theme-mode`, puts the recovered decision in\n `prism-theme` and removes the key it read, in one guarded step. That is how it\n expires: it fires only while a retired key still holds a mode, at most once per\n reader, and it is retired for good by deleting the last entry of\n `LEGACY_MODE_STORAGE_KEYS`. There is no date and no version constant in it.\n \n The pre-paint string gets its own ceiling in raw bytes,\n `check-boot-budget.mjs`, because it runs before paint on every page and adding it\n to the per-component client budget would corrupt the unit that table measures. The\n ceiling is derived by addition: the pinned non-migration base, plus the measured\n clause, plus one migration generation.\n- 76c8d96: Fourteen items, four catalogue gates, and a Block that publishes its interface\n \n **Fourteen items ship.** Three Components (`Prose`, `FactList`, `ProductSwitcher`),\n nine Blocks (`ProcessRail01`, `StatusLedger01`, `ProductGrid01`, `StackGrid01`,\n `LogoStrip01`, `NoteGrid01`, `InstrumentPanel01`, `SiteHeader`, `SiteFooter`) and two\n Pages (`NotFoundPage`, `BlogPostPage`). Every one of them earns its place by the\n growth rule rather than by taste: a marketing Block when at least three of the four\n sites compose that section today, a Component when it has one job no existing\n Component can express. The evidence is in the ticket and in each item's own\n JSDoc, which names the sites and the line numbers.\n \n **The three contested shapes are settled as specified, and the specifications\n held.** A process rail admits four steps and refuses five, in the type: `steps` is\n a tuple union, so a five-element array is a compile error and not a fifth column. A\n status ledger has four tiers rather than five, and the words for a tier are the\n caller's: the four sites between them use eight words for those four states\n (`live`, `available` and `complete` are one; `approved`, `specified` and `designed`\n are one; then `planned`; then `direction`), and the vocabulary is what `statusLabel`\n is for. A product row's mark is two parts because a filled dot fails contrast on a\n light card, so the mark is `ProductMark`: a hairline ring in `brand-ink` around a\n core in the product's own `primary` fill, with the name in the brand ink beside it.\n \n **Four gates the catalogue's own rules depend on now exist and are wired into\n `check` and, because they read only `src/`, into `build`.** `check-item-docs`\n holds AGENTS.md's claim that a documentation comment is the only path from an Item\n to its published interface. `check-block-copy` holds DESIGN.md's claim that a\n Block ships no copy and no sample data. `check-item-category` holds CONTEXT.md's\n closed set of seven Categories and the sentence that a Block and a Page have none.\n `check-block-imports` holds the claim that a Block fetches nothing and a Page\n imports no router and no data client. Each resolves its roots from its own\n location, fails a root that resolves to nothing, prints its coverage and every\n exclusion on every run, has no report-only mode, and is proved red by a fixture\n before it is proved green.\n \n **Two of those four gates were red on the tree they were written against, and both\n findings were real.** `check-item-docs` found seven exported functions whose\n documentation comment was not attached to the declaration: `CardHeader`,\n `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `SectionHeading` and\n `Hero01`. A comment three functions above a declaration does not reach the emitted\n `.d.ts`, so those seven were absent from the corpus. `check-block-copy` found\n `pricing-01` rendering the word \"Popular\" on a featured plan that passed no badge:\n a hardcoded claim every consumer of the Block inherited, published by the corpus as\n part of the Block's own documentation. The fallback is gone; a featured plan with\n no badge now renders no badge.\n \n **Five additive fields on four shipped items**, each with three of the four\n consumers passing a value the item could not take, counted per consumer with a\n file and a line and never per string. `SectionHeading.index`, for the mono ordinal\n all four sites render above every section head (www `Landing.tsx:49`, atlas\n `landing.tsx:48`, nexus `Landing.tsx:38`, alphalens `Landing.tsx:66`).\n `Cta01.secondaryAction`, for the pair of actions all four closing bands render\n (www `Landing.tsx:186`, atlas `landing.tsx:243`, nexus `Landing.tsx:221`,\n alphalens `Landing.tsx:285`). `Cta01.note`, for the footnote three of the four\n render under it (atlas `landing.tsx:250`, nexus `Landing.tsx:228`, alphalens\n `Landing.tsx:292`). `FeatureGrid01.numbered`, for the ordinal all four render on\n every card (www `Landing.tsx:166`, atlas `landing.tsx:116`, nexus `Landing.tsx:132`,\n alphalens `Landing.tsx:142`). `Hero01.align`, for the left-aligned hero three of\n the four compose (atlas `landing.tsx:69`, nexus `Landing.tsx:63`, alphalens\n `Landing.tsx:87`). All five are additive: nothing changed shape and nothing was\n removed.\n \n **A Block and a Page publish both an interface section and a composition section.**\n `packages/llms` read the extractor's output for a Block and threw the interface half\n away, so the tool told an agent a Block had no own props. The extractor reads a\n named props type off an emitted declaration whether that declaration belongs to a\n Component or to a Block, so both sections are now built and both are assembled into\n the mirror file, `llms-full.txt` and the Store's `doc`.\n \n The Store now carries both fields for every kind, and `get_item_props` prints\n both when it has both. The sentence \"a Block has no own props\" is gone from the\n tool as well as from the published document. It was the one place the two\n sections were still exclusive, because that tool branched on `item.props` and\n fell back to the composition section, so a Block was answered with its\n composition and a denial of the interface it publishes. The test asserts the\n sentence's absence, so a refactor that restores it fails even if the composition\n is still printed, and a second test covers the one-present case so the\n both-present branch cannot swallow it.\n \n **Four accessible names became required props** on `SiteHeader`, `LogoStrip01`,\n `NotFoundPage` and `BlogPostPage`, and a Block that ships no copy ships no\n reader-facing copy either. `navLabel`, `productsLabel`, `label`, `linksLabel` and\n `trailLabel` are words a screen reader reads out, and no Block should be choosing\n them for a consumer whose site calls its own landmarks something else. This is the\n one breaking change in the release, and it is on four new Blocks and two new Pages\n plus one new required prop on `SiteHeader`, all of which are new in the same\n release, so no existing call site changes.\n \n **`ProductSwitcher.label` has no default** for the same reason: a consumer whose set\n is not a set of products should pass its own word rather than accept a claim about\n what the set is.\n\n### Patch Changes\n\n- 8d24f32: A pack boundary's effect on corner radius is stated, and gated\n \n A scoped `data-pack` boundary resolves the pack's colour AND the pack's corner\n radius beneath it, because radius is the only non-colour member of a pack block\n and the token build emits it the same way. The radius range across the five packs\n is 0.5rem to 1rem, so a row of elements each carrying its own pack shows a spread\n of corner radii, and a small pill becomes a different pill five times over. The\n governing law stated the colour half and was silent on the shape half, so an\n implementer following it exactly produced mismatched corners and called it\n correct.\n \n The law now states both halves and where a boundary may sit. The agent-facing\n surface no longer says the attribute belongs on the root element, which it did\n while listing the radius among the values it will pick up: a tool that shows the\n value and denies the subtree is worse than a tool that is silent.\n \n `check-pack-boundary.mjs` holds the law. It reads the emitted `--radius-*`\n bindings rather than a list of class names, which is the part that matters: a\n scale step like `rounded-xl` is `calc(var(--radius) * 1.4)`, so an element\n carrying one is pinned to a step and the step is pack-relative. The first version\n of the gate treated \"carries a radius utility\" as the safe case and passed\n exactly the defect the ticket is about. The gate also reads the colour contract\n from the token source, so a second per-pack axis the token build has not been\n asked about is a finding rather than a property silently counted as a colour.\n \n One live boundary was found and repaired: the themes page scoped five cards, one\n per pack, each with `rounded-xl`, so the page a reader uses to compare packs was\n comparing shape as well as colour. The cards now carry a literal radius and the\n pack's radius is shown in the row's own label, where it is information rather\n than an accident.\n- feb1384: The catalogue is a set comparison between three lists, not a count\n \n A module on disk with no catalogue entry left every count correct. It shipped,\n `shadcn add` installed it, and `registry.json` listed it, so it was absent from\n the corpus, the documentation site and every agent-facing tool with a fully\n green gate run. The two lists a reader trusts, the catalogue and the registry,\n were on the wrong side of that hole, and the checks that believed they covered it\n compared lengths or compared a file against something derived from itself.\n `validate-registry.mjs` holds the registry against the disk and the manifest and\n never reads the catalogue. `sync-registry.mjs` reads the disk, so it cannot\n disagree with the disk about what is on it. The site's\n `test/catalogue-order.test.ts` holds the catalogue against the generated\n ordering and the documentation tree, and both are built from the catalogue, so a\n disagreement there is impossible by construction.\n \n `scripts/check-catalogue.mjs` reads the three sets independently and compares\n them in both directions, name for name, with one printed line per offending\n item. A length assertion cannot report which item is wrong, so nothing in the\n run reports a length on its own: the three set sizes print as supplementary\n coverage underneath the findings that carry the detail. All six directions a\n three-set comparison implies are implemented, plus the kind of an item in each\n pair of sets, the three naming rules that reconcile the three conventions, and a\n claim that a canonical key is held by only one entry in each set.\n \n The registry generator deliberately keeps reading the disk. Deriving the\n registry from the catalogue is the obvious way to remove the second list, and it\n would delete the disagreement the check exists to see, because the generator\n would then agree with the catalogue by construction.\n \n The version and the roster are two claims on two labelled lines, so a version\n bump and a roster error cannot produce the same output. The run states its\n coverage every time: roots resolved, files walked, files actually read, and how\n many comparisons passed. Roots resolve from the script's own location and never\n from the working directory, and a root that resolves to nothing fails the run\n naming both causes.\n \n `PRISM_CATALOGUE_ROOT` points a run at another package root. It changes which\n directory is read and nothing else, which is what lets the tests run the real\n command over a fixture tree and read the real output. The gate fails on a\n catalogue it cannot read, including an empty one: a roster that cannot be read\n and a roster with nothing in it are different facts, and only one of them is a\n green run.\n- 4744cce: The Slider thumb rings at full strength, and the surface check now reads both ways\n \n Two changes, and only two, in the component package.\n \n **The focus indicator.** The Slider's thumb suppressed the browser outline and\n drew its ring at `ring-ring/50`, so the element a keyboard user lands on carried\n an indicator that composites to between 1.14:1 and 2.74:1 against every surface\n in all six themes. That fails WCAG 1.4.11 on the one indicator a keyboard user\n has, and the Slider's own JSDoc says it is reachable by Tab. Thirteen other\n components already drew that ring at full strength; the Slider was the one that\n was missed. Nothing said so, because the contrast gate measures the `ring`\n TOKEN and the alpha is applied in the component rather than in the token, so it\n reads the Slider's ring as compliant.\n \n `packages/ui/scripts/check-focus-indicators.mjs` is the new gate. It reads the\n class strings on the elements of every shipped Component, derives the set of\n components whose own JSDoc claims keyboard reach or operation, and fails any of\n them that suppresses a focus style without declaring a `ring-ring` colour at\n full alpha alongside a `focus-visible:ring-N` width. The claim is read from the\n JSDoc because `AGENTS.md` names that as this repository's documentation source,\n so a component that stops claiming to be focusable is a documentation change and\n shows up in the table the run prints rather than in a list someone maintains\n beside the code. The unit is a class string on one element, so a Dialog's panel\n or a Tooltip's trigger can suppress the outline legitimately; every such\n exclusion is declared with its reason, printed with the slots it resolved to,\n and fails the run if it stops matching anything. The run fails if the table\n empties, and states how much it read, classified, tabled and excluded.\n \n **The surface check.** `scripts/check-surface.mjs` compared the published\n surface against the source in one direction for two of its three rules. A\n wildcard subpath that resolved to zero declarations used to leave those two\n rules reading no file at all and the run still printed its success line, which is\n how a published surface drifts from its source without a consumer failing first.\n A wildcard target that matches nothing is now a finding naming the target. The\n internal side, which the gate deliberately did not check, is now a declared\n boundary checked in both directions: a new file under `dist/lib` is a finding\n naming it, and a declared internal declaration that is no longer emitted is a\n finding naming it too. The boundary and the per-target match counts are printed\n on every run. The three existing rules and their messages are unchanged.\n \n **The version the four downstream site plans pin is `0.6.0`.** It is\n `@nanisoft/prism-ui@0.6.0`, and `@nanisoft/prism-tokens@0.6.0` with it, because\n the two are linked in `.changeset/config.json` and the pending\n `a-server-rendered-pack-boundary-can-be-mode-correct` changeset declares a minor\n for both from `0.5.1`. The patch declared here does not raise it: changesets\n takes the highest bump in the queue, and a minor is already queued. A site plan\n that pins `0.6.0` therefore resolves every subpath the export map publishes,\n including `./components`, `./blocks`, `./pages`, `./provider`, `./theming`,\n `./catalog` and `./styles.css`.\n \n No public prop, no type and no export changed. The one rendering change is a\n ring alpha on the Slider thumb, in both modes.\n- Updated dependencies [4f9ce77]\n- Updated dependencies [5dcb347]\n - @nanisoft/prism-tokens@0.6.0\n\n## 0.5.1\n\n### Patch Changes\n\n- 2fc6a0a: Retune `muted-foreground` so muted text clears 4.5:1 on the muted surface\n \n The base pack's `muted-foreground` moves from neutral 500 to neutral 600. It\n cleared 4.5:1 on the page background but fell to 4.34:1 on `muted`, where the\n pill, avatar fallback, kbd and tab-list pattern place it. The contrast gate now\n also checks `muted-foreground` on `muted`. The pastel packs were already pinned\n to their neutral 700 and are unchanged.\n- Updated dependencies [2fc6a0a]\n - @nanisoft/prism-tokens@0.5.1\n\n## 0.5.0\n\nPrism was rebuilt from a fresh repository on a clean break. The package is now a\npublished React library a consumer installs and composes, not component source to\ncopy. It ships one precompiled `styles.css`, the optional `PrismProvider` and the\n`data-pack` plus `dark` theme axes, and the Components, Blocks and Pages\ntaxonomy. There is no override path.\n\nChangesets are appended above this entry for every published change.\n"
|
|
8155
8285
|
}
|
|
8156
8286
|
],
|
|
8157
8287
|
"tokens": {
|