@nanisoft/prism-llms 0.5.0 → 0.5.1

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0",
2
+ "version": "0.7.0",
3
3
  "items": [
4
4
  {
5
5
  "id": "accordion",
@@ -532,8 +532,8 @@
532
532
  "ProductMark"
533
533
  ],
534
534
  "importLine": "import { ProductMark } from '@nanisoft/prism-ui/components/product-mark'",
535
- "doc": "# ProductMark\n\nA product's two-part colour mark: a brand-ink ring around the pack's own fill, beside the product's name in the brand ink.\n\n```tsx\nimport { ProductMark } from '@nanisoft/prism-ui/components/product-mark'\n```\n\n## Overview\n\nA ProductMark is a product's two-part colour mark: a ring in the brand ink around\na core in the product's own pack fill, beside the product's name in the same\nbrand ink. It is a server component and ships no client code.\n\nThe mark is promoted to a component because a product row and a product switcher\nboth need it, and two hand-written copies is the arrangement this design system\nexists to remove. One implementation also means one place where the two colour\nlaws are applied, and those laws are not obvious:\n\n**The ring and the name are ink; the core is a fill.** A pastel brand value is a\nfill and never text, so the core takes `primary`. The ring and the name take\n`brand-ink`, which is the role for something that must read *as the brand*:\ngated at 4.5:1 against the page, a card and the accent surface, in both modes of\nall six packs, worst measured 5.57:1. Plain `foreground` is not a brand ink, and\nneither is a foreground paired with a fill, so a mark drawn in `primary` text is\nthe wrong answer twice over.\n\n**A product with no pack wears the spectrum, not a colourless mark.** A mark with\nno `pack` is drawn as a conic sweep of all five chart steps. A colourless mark\nwould take the reading page's accent and change per site, so a product with no\nsingle colour would be a different colour on every site that listed it. The one\nentry with no pack is the company root, and the spectrum says so once rather\nthan five times.\n\n## Guidelines\n\n### When to use\n\nUse a ProductMark wherever a reader is being shown which product they are in, or\nwhich of a set of products a link goes to: a product row, a product switcher, a\nsite header's brand lockup, a footer grid. Wherever a site draws the same mark\ntwice, it should be drawing this.\n\nPass the product's `id` as the key within the set it is distinguished in. It is\ncarried on the DOM as `data-product`, and it is what makes two marks in the same\nview traceable to their product.\n\n### When not to use\n\nDo not use a ProductMark to colour a section, a surface or a heading. The mark\nidentifies a member of a set, and the design system keeps mark and ground on\nseparate axes for exactly that reason: a pack is a signal, never a ground.\n\nDo not put a boundary on a ProductMark's disc. A pack boundary moves the pack's\ncorner radius as well as its colour, and the mark is a fully-rounded element, so\na boundary on it changes nothing a reader can see. Carry the pack through `pack`\ninstead, which is the axis the mark actually reads.\n\nDo not hand-write a second mark. If a surface needs a mark this component does\nnot cover, extend this one.\n\n### Best practices\n\n- Give every product in a set a distinct pack, or let one wear the spectrum. A\n set where two members share a pack has two marks that say the same thing.\n- Use one size across a row. `sm` beside a hairline row, `md` in body content,\n `lg` only in a page header; a row of mixed sizes reads as a hierarchy that is\n not there.\n- The mark is `aria-hidden` and the name beside it is the accessible content. Do\n not put meaning in the ring's colour that is not also in the name.\n- Prefer `md`. It is the size that reads at body text, and a mark is a\n co-occurrence with a name rather than a thing to look at.\n\n### Content guidelines\n\n- The name is the product's own name, in the case it uses everywhere else.\n- Keep the name to one or two words. The mark carries the identity; the name\n carries the label.\n- Never abbreviate a product's name to fit beside its mark. Drop the mark, or\n shorten the row, or let the name wrap.\n\n**The mark at each size, including one with no pack.**\n\n```tsx\nimport { ProductMark } from '@nanisoft/prism-ui/components/product-mark'\n\n/**\n * The mark at each size, including one with no pack.\n *\n * The last row is the case that matters: a product with no pack of its own wears\n * the full spectrum rather than a colourless mark, because a colourless mark\n * would take the reading page's accent and change per site.\n */\nexport default function ProductMarkDemo() {\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col items-start gap-3\">\n <ProductMark id=\"nexus\" name=\"Nexus\" pack=\"lavender\" size=\"lg\" />\n <ProductMark id=\"atlas\" name=\"Atlas\" pack=\"mint\" size=\"md\" />\n <ProductMark id=\"alphalens\" name=\"AlphaLens\" pack=\"blush\" size=\"sm\" />\n </div>\n <ProductMark id=\"www\" name=\"NaniSoft\" size=\"md\" />\n </div>\n )\n}\n```\n\n## Props\n\n**`id`** `string` · required · default: — — The product's key within the set it is distinguished in. It is carried on the markup as `data-product` so a test can name one mark rather than the first one, and so two marks in one document are individually addressable.\n**`name`** `string` · required · default: — — The product's name, which is the mark's second part and the word that carries the identity. The mark sets it in `brand-ink` rather than in `foreground`, which is DESIGN.md's Brand Ink Rule: a name that has to read as the brand reads `brand-ink`, and `foreground` is a tinted neutral with almost no chroma by design.\n**`pack`** `PackId | null` · optional · default: — — The product's pack, which is the mark's hue. Omit it, or pass `null`, for the one entry in a set that has no pack of its own: that entry is drawn as the full spectrum rather than as a colourless mark. See `SPECTRUM` above. `default` is a real answer and not the same as the empty one. The base pack is a pack a product can wear, and it is the absence of a `data-pack` attribute, so a product whose pack is `default` carries no attribute and its core resolves the base pack's `primary` from the page.\n**`size`** `ProductMarkSize` · optional · default: `'md'` — Which of the three drawn sizes to use.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited, and this Component spreads no other prop, so there is no second route to its ink.\n**`stored`** `string | null` · required · default: — — Raw contents of the key this line writes, or `null` when it is absent.\n**`legacy`** `string | null` · required · default: — — Raw contents of one key this line no longer writes, or `null`.\n**`documentPack`** `string | null` · required · default: — — `data-pack` on the root element as rendered, valid or not.\n**`documentDark`** `boolean` · required · default: — — Whether the root element carries `.dark` as rendered.\n**`defaultPack`** `PackId` · required · default: —\n**`defaultMode`** `Mode` · required · default: —\n**`mode`** `Mode` · required · default: —\n**`origin`** `ThemeOrigin` · required · default: — — Which source named the pair, for `THEME_ORIGIN_ATTRIBUTE`.\n**`key`** `string` · required · default: —\n",
536
- "props": "## Props\n\n**`id`** `string` · required · default: — — The product's key within the set it is distinguished in. It is carried on the markup as `data-product` so a test can name one mark rather than the first one, and so two marks in one document are individually addressable.\n**`name`** `string` · required · default: — — The product's name, which is the mark's second part and the word that carries the identity. The mark sets it in `brand-ink` rather than in `foreground`, which is DESIGN.md's Brand Ink Rule: a name that has to read as the brand reads `brand-ink`, and `foreground` is a tinted neutral with almost no chroma by design.\n**`pack`** `PackId | null` · optional · default: — — The product's pack, which is the mark's hue. Omit it, or pass `null`, for the one entry in a set that has no pack of its own: that entry is drawn as the full spectrum rather than as a colourless mark. See `SPECTRUM` above. `default` is a real answer and not the same as the empty one. The base pack is a pack a product can wear, and it is the absence of a `data-pack` attribute, so a product whose pack is `default` carries no attribute and its core resolves the base pack's `primary` from the page.\n**`size`** `ProductMarkSize` · optional · default: `'md'` — Which of the three drawn sizes to use.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited, and this Component spreads no other prop, so there is no second route to its ink.\n**`stored`** `string | null` · required · default: — — Raw contents of the key this line writes, or `null` when it is absent.\n**`legacy`** `string | null` · required · default: — — Raw contents of one key this line no longer writes, or `null`.\n**`documentPack`** `string | null` · required · default: — — `data-pack` on the root element as rendered, valid or not.\n**`documentDark`** `boolean` · required · default: — — Whether the root element carries `.dark` as rendered.\n**`defaultPack`** `PackId` · required · default: —\n**`defaultMode`** `Mode` · required · default: —\n**`mode`** `Mode` · required · default: —\n**`origin`** `ThemeOrigin` · required · default: — — Which source named the pair, for `THEME_ORIGIN_ATTRIBUTE`.\n**`key`** `string` · required · default: —",
535
+ "doc": "# ProductMark\n\nA product's two-part colour mark: a brand-ink ring around the pack's own fill, beside the product's name in the brand ink.\n\n```tsx\nimport { ProductMark } from '@nanisoft/prism-ui/components/product-mark'\n```\n\n## Overview\n\nA ProductMark is a product's two-part colour mark: a ring in the brand ink around\na core in the product's own pack fill, beside the product's name in the same\nbrand ink. It is a server component and ships no client code.\n\nThe mark is promoted to a component because a product row and a product switcher\nboth need it, and two hand-written copies is the arrangement this design system\nexists to remove. One implementation also means one place where the two colour\nlaws are applied, and those laws are not obvious:\n\n**The ring and the name are ink; the core is a fill.** A pastel brand value is a\nfill and never text, so the core takes `primary`. The ring and the name take\n`brand-ink`, which is the role for something that must read *as the brand*:\ngated at 4.5:1 against the page, a card and the accent surface, in both modes of\nall six packs, worst measured 5.57:1. Plain `foreground` is not a brand ink, and\nneither is a foreground paired with a fill, so a mark drawn in `primary` text is\nthe wrong answer twice over.\n\n**A product with no pack wears the spectrum, not a colourless mark.** A mark with\nno `pack` is drawn as a conic sweep of all five chart steps. A colourless mark\nwould take the reading page's accent and change per site, so a product with no\nsingle colour would be a different colour on every site that listed it. The one\nentry with no pack is the company root, and the spectrum says so once rather\nthan five times.\n\n## Guidelines\n\n### When to use\n\nUse a ProductMark wherever a reader is being shown which product they are in, or\nwhich of a set of products a link goes to: a product row, a product switcher, a\nsite header's brand lockup, a footer grid. Wherever a site draws the same mark\ntwice, it should be drawing this.\n\nPass the product's `id` as the key within the set it is distinguished in. It is\ncarried on the DOM as `data-product`, and it is what makes two marks in the same\nview traceable to their product.\n\n### When not to use\n\nDo not use a ProductMark to colour a section, a surface or a heading. The mark\nidentifies a member of a set, and the design system keeps mark and ground on\nseparate axes for exactly that reason: a pack is a signal, never a ground.\n\nDo not put a boundary on a ProductMark's disc. A pack boundary moves the pack's\ncorner radius as well as its colour, and the mark is a fully-rounded element, so\na boundary on it changes nothing a reader can see. Carry the pack through `pack`\ninstead, which is the axis the mark actually reads.\n\nDo not hand-write a second mark. If a surface needs a mark this component does\nnot cover, extend this one.\n\n### Best practices\n\n- Give every product in a set a distinct pack, or let one wear the spectrum. A\n set where two members share a pack has two marks that say the same thing.\n- Use one size across a row. `sm` beside a hairline row, `md` in body content,\n `lg` only in a page header; a row of mixed sizes reads as a hierarchy that is\n not there.\n- The mark is `aria-hidden` and the name beside it is the accessible content. Do\n not put meaning in the ring's colour that is not also in the name.\n- Prefer `md`. It is the size that reads at body text, and a mark is a\n co-occurrence with a name rather than a thing to look at.\n\n### Content guidelines\n\n- The name is the product's own name, in the case it uses everywhere else.\n- Keep the name to one or two words. The mark carries the identity; the name\n carries the label.\n- Never abbreviate a product's name to fit beside its mark. Drop the mark, or\n shorten the row, or let the name wrap.\n\n**The mark at each size, including one with no pack.**\n\n```tsx\nimport { ProductMark } from '@nanisoft/prism-ui/components/product-mark'\n\n/**\n * The mark at each size, including one with no pack.\n *\n * The last row is the case that matters: a product with no pack of its own wears\n * the full spectrum rather than a colourless mark, because a colourless mark\n * would take the reading page's accent and change per site.\n */\nexport default function ProductMarkDemo() {\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col items-start gap-3\">\n <ProductMark id=\"nexus\" name=\"Nexus\" pack=\"lavender\" size=\"lg\" />\n <ProductMark id=\"atlas\" name=\"Atlas\" pack=\"mint\" size=\"md\" />\n <ProductMark id=\"alphalens\" name=\"AlphaLens\" pack=\"blush\" size=\"sm\" />\n </div>\n <ProductMark id=\"www\" name=\"NaniSoft\" size=\"md\" />\n </div>\n )\n}\n```\n\n## Props\n\n**`id`** `string` · required · default: — — The product's key within the set it is distinguished in. It is carried on the markup as `data-product` so a test can name one mark rather than the first one, and so two marks in one document are individually addressable.\n**`name`** `string` · required · default: — — The product's name, which is the mark's second part and the word that carries the identity. The mark sets it in `brand-ink` rather than in `foreground`, which is DESIGN.md's Brand Ink Rule: a name that has to read as the brand reads `brand-ink`, and `foreground` is a tinted neutral with almost no chroma by design.\n**`pack`** `PackId | null` · optional · default: — — The product's pack, which is the mark's hue. Omit it, or pass `null`, for the one entry in a set that has no pack of its own: that entry is drawn as the full spectrum rather than as a colourless mark. See `SPECTRUM` above. `default` is a real answer and not the same as the empty one. The base pack is a pack a product can wear, and it is the absence of a `data-pack` attribute, so a product whose pack is `default` carries no attribute and its core resolves the base pack's `primary` from the page.\n**`size`** `ProductMarkSize` · optional · default: `'md'` — Which of the three drawn sizes to use.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited, and this Component spreads no other prop, so there is no second route to its ink.\n",
536
+ "props": "## Props\n\n**`id`** `string` · required · default: — — The product's key within the set it is distinguished in. It is carried on the markup as `data-product` so a test can name one mark rather than the first one, and so two marks in one document are individually addressable.\n**`name`** `string` · required · default: — — The product's name, which is the mark's second part and the word that carries the identity. The mark sets it in `brand-ink` rather than in `foreground`, which is DESIGN.md's Brand Ink Rule: a name that has to read as the brand reads `brand-ink`, and `foreground` is a tinted neutral with almost no chroma by design.\n**`pack`** `PackId | null` · optional · default: — — The product's pack, which is the mark's hue. Omit it, or pass `null`, for the one entry in a set that has no pack of its own: that entry is drawn as the full spectrum rather than as a colourless mark. See `SPECTRUM` above. `default` is a real answer and not the same as the empty one. The base pack is a pack a product can wear, and it is the absence of a `data-pack` attribute, so a product whose pack is `default` carries no attribute and its core resolves the base pack's `primary` from the page.\n**`size`** `ProductMarkSize` · optional · default: `'md'` — Which of the three drawn sizes to use.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited, and this Component spreads no other prop, so there is no second route to its ink.",
537
537
  "example": {
538
538
  "title": "The mark at each size, including one with no pack.",
539
539
  "code": "import { ProductMark } from '@nanisoft/prism-ui/components/product-mark'\n\n/**\n * The mark at each size, including one with no pack.\n *\n * The last row is the case that matters: a product with no pack of its own wears\n * the full spectrum rather than a colourless mark, because a colourless mark\n * would take the reading page's accent and change per site.\n */\nexport default function ProductMarkDemo() {\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col items-start gap-3\">\n <ProductMark id=\"nexus\" name=\"Nexus\" pack=\"lavender\" size=\"lg\" />\n <ProductMark id=\"atlas\" name=\"Atlas\" pack=\"mint\" size=\"md\" />\n <ProductMark id=\"alphalens\" name=\"AlphaLens\" pack=\"blush\" size=\"sm\" />\n </div>\n <ProductMark id=\"www\" name=\"NaniSoft\" size=\"md\" />\n </div>\n )\n}\n"
@@ -697,8 +697,8 @@
697
697
  "SelectSeparator"
698
698
  ],
699
699
  "importLine": "import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator } from '@nanisoft/prism-ui/components/select'",
700
- "doc": "# Select\n\nThe control that chooses one option from a list that opens on demand.\n\n```tsx\nimport { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator } from '@nanisoft/prism-ui/components/select'\n```\n\n## Overview\n\nA Select chooses one option from a list that opens on demand. It ships as\n`Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`,\n`SelectGroup`, `SelectLabel` and `SelectSeparator`. `Select` holds the value,\n`SelectTrigger` shows the current choice, and `SelectContent` carries the\noptions. Pass the options to the root as `items` too, so the trigger can render\na label before any option has mounted.\n\nThe trigger is a button with a combobox role, so it is reachable by Tab. In the\nopen popup the arrow keys move through the options, typing letters jumps to a\nmatch, Enter chooses, and Escape closes without changing the value.\n\n## Guidelines\n\n### When to use\n\nUse a Select when the option set is long enough that a radio group would\noverwhelm the form, or when only the chosen value needs to stay visible. It fits\nsettings and filters where the reader picks one value from a known list.\n\n### When not to use\n\nDo not use a Select for two or three mutually exclusive options. A RadioGroup\nshows every choice at once and needs no opening click.\n\nDo not use it when the reader may choose more than one value, or for navigation.\nCheckboxes state independence; a link handles a route change.\n\n### Best practices\n\n- Pair the Select with a `FieldLabel` whose `htmlFor` matches the trigger's\n `id`. The value shows in the trigger, so the label names the field.\n- Pass `items` so the trigger can resolve a label before an option has mounted.\n Without it, Base UI falls back to the raw value.\n- Give `SelectValue` a placeholder that says what to do, such as \"Choose a\n region\", rather than naming a default the reader did not choose.\n- Add `SelectGroup` and `SelectLabel` once a list passes roughly eight options,\n so the reader can scan by group.\n- A disabled option is still readable. Explain why it cannot be chosen in\n adjacent text rather than leaving a dead row unexplained.\n\n### Content guidelines\n\n- Write option labels as sentence-case nouns: \"Europe West\", not \"EUROPE WEST\".\n- Order options by the reader's expectation: size, plan, region, or product.\n- Reserve the first option for a real choice. Do not add an empty \"Select one\"\n row when the placeholder already asks the question.\n\n**A labelled Select with a grouped option list and a controlled value.**\n\n```tsx\n'use client'\n\nimport { useState } from 'react'\n\nimport { Field, FieldLabel } from '@nanisoft/prism-ui/components/field'\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectTrigger,\n SelectValue,\n} from '@nanisoft/prism-ui/components/select'\n\n/** A labelled Select with a grouped option list and a controlled value. */\nexport default function SelectDemo() {\n const [role, setRole] = useState('editor')\n\n return (\n <div className=\"flex max-w-measure-narrow flex-col gap-6\">\n <Field>\n <FieldLabel htmlFor=\"select-role\">Role</FieldLabel>\n <Select\n value={role}\n onValueChange={(value) => setRole(value ?? '')}\n items={{ viewer: 'Viewer', editor: 'Editor', admin: 'Admin' }}\n >\n <SelectTrigger id=\"select-role\" className=\"w-full\">\n <SelectValue placeholder=\"Choose a role\" />\n </SelectTrigger>\n <SelectContent>\n <SelectGroup>\n <SelectLabel>Team roles</SelectLabel>\n <SelectItem value=\"viewer\">Viewer</SelectItem>\n <SelectItem value=\"editor\">Editor</SelectItem>\n <SelectItem value=\"admin\">Admin</SelectItem>\n </SelectGroup>\n </SelectContent>\n </Select>\n </Field>\n <Field>\n <FieldLabel htmlFor=\"select-region\">Region</FieldLabel>\n <Select\n defaultValue=\"eu-west\"\n items={{\n 'eu-west': 'Europe West',\n 'us-east': 'US East',\n 'ap-south': 'Asia South',\n }}\n >\n <SelectTrigger id=\"select-region\" className=\"w-full\">\n <SelectValue placeholder=\"Choose a region\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"eu-west\">Europe West</SelectItem>\n <SelectItem value=\"us-east\">US East</SelectItem>\n <SelectItem value=\"ap-south\" disabled>\n Asia South (paused)\n </SelectItem>\n </SelectContent>\n </Select>\n </Field>\n </div>\n )\n}\n```\n\n## Props\n\n**`value`** `string` · optional · default: — — The selected value, when the select is controlled.\n**`defaultValue`** `string` · optional · default: — — The value selected initially, for an uncontrolled select.\n**`onValueChange`** `(value: string | null) => void` · optional · default: — — Called when the selected value changes.\n**`open`** `boolean` · optional · default: — — The controlled open state of the popup.\n**`defaultOpen`** `boolean` · optional · default: — — The initially open state of the popup.\n**`onOpenChange`** `(open: boolean) => void` · optional · default: — — Called when the popup opens or closes.\n**`name`** `string` · optional · default: — — The form field name submitted with the form.\n**`form`** `string` · optional · default: — — Identifies the form that owns the hidden input.\n**`required`** `boolean` · optional · default: — — Whether a value must be chosen before the form submits.\n**`disabled`** `boolean` · optional · default: — — Whether the select ignores user interaction.\n**`readOnly`** `boolean` · optional · default: — — Whether the user may open the popup but not change the value.\n**`items`** `Record<string, React.ReactNode> | ReadonlyArray<{ label: React.ReactNode; value: string; }>` · optional · default: — — The options, keyed by value, so the trigger can render a label before any item has mounted. Without it, `SelectValue` shows the raw value.\n**`itemToStringLabel`** `(value: string) => string` · optional · default: — — Converts an item value to the label shown in the trigger.\n**`children`** `React.ReactNode` · optional · default: — — The trigger and the popup content.\n**`label`** `React.ReactNode` · required · default: —\n**`size`** `'sm' | 'default'` · optional · default: `default` — The trigger's height.\n**`placeholder`** `React.ReactNode` · optional · default: — — Shown when no value is selected.\n**`side`** `'top' | 'right' | 'bottom' | 'left'` · optional · default: `bottom` — Which side of the trigger the list is placed on.\n**`align`** `'start' | 'center' | 'end'` · optional · default: `center` — How the list aligns against the trigger.\n**`sideOffset`** `number` · optional · default: `4` — The distance in pixels between the trigger and the list.\n",
701
- "props": "## Props\n\n**`value`** `string` · optional · default: — — The selected value, when the select is controlled.\n**`defaultValue`** `string` · optional · default: — — The value selected initially, for an uncontrolled select.\n**`onValueChange`** `(value: string | null) => void` · optional · default: — — Called when the selected value changes.\n**`open`** `boolean` · optional · default: — — The controlled open state of the popup.\n**`defaultOpen`** `boolean` · optional · default: — — The initially open state of the popup.\n**`onOpenChange`** `(open: boolean) => void` · optional · default: — — Called when the popup opens or closes.\n**`name`** `string` · optional · default: — — The form field name submitted with the form.\n**`form`** `string` · optional · default: — — Identifies the form that owns the hidden input.\n**`required`** `boolean` · optional · default: — — Whether a value must be chosen before the form submits.\n**`disabled`** `boolean` · optional · default: — — Whether the select ignores user interaction.\n**`readOnly`** `boolean` · optional · default: — — Whether the user may open the popup but not change the value.\n**`items`** `Record<string, React.ReactNode> | ReadonlyArray<{ label: React.ReactNode; value: string; }>` · optional · default: — — The options, keyed by value, so the trigger can render a label before any item has mounted. Without it, `SelectValue` shows the raw value.\n**`itemToStringLabel`** `(value: string) => string` · optional · default: — — Converts an item value to the label shown in the trigger.\n**`children`** `React.ReactNode` · optional · default: — — The trigger and the popup content.\n**`label`** `React.ReactNode` · required · default: —\n**`size`** `'sm' | 'default'` · optional · default: `default` — The trigger's height.\n**`placeholder`** `React.ReactNode` · optional · default: — — Shown when no value is selected.\n**`side`** `'top' | 'right' | 'bottom' | 'left'` · optional · default: `bottom` — Which side of the trigger the list is placed on.\n**`align`** `'start' | 'center' | 'end'` · optional · default: `center` — How the list aligns against the trigger.\n**`sideOffset`** `number` · optional · default: `4` — The distance in pixels between the trigger and the list.",
700
+ "doc": "# Select\n\nThe control that chooses one option from a list that opens on demand.\n\n```tsx\nimport { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator } from '@nanisoft/prism-ui/components/select'\n```\n\n## Overview\n\nA Select chooses one option from a list that opens on demand. It ships as\n`Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`,\n`SelectGroup`, `SelectLabel` and `SelectSeparator`. `Select` holds the value,\n`SelectTrigger` shows the current choice, and `SelectContent` carries the\noptions. Pass the options to the root as `items` too, so the trigger can render\na label before any option has mounted.\n\nThe trigger is a button with a combobox role, so it is reachable by Tab. In the\nopen popup the arrow keys move through the options, typing letters jumps to a\nmatch, Enter chooses, and Escape closes without changing the value.\n\n## Guidelines\n\n### When to use\n\nUse a Select when the option set is long enough that a radio group would\noverwhelm the form, or when only the chosen value needs to stay visible. It fits\nsettings and filters where the reader picks one value from a known list.\n\n### When not to use\n\nDo not use a Select for two or three mutually exclusive options. A RadioGroup\nshows every choice at once and needs no opening click.\n\nDo not use it when the reader may choose more than one value, or for navigation.\nCheckboxes state independence; a link handles a route change.\n\n### Best practices\n\n- Pair the Select with a `FieldLabel` whose `htmlFor` matches the trigger's\n `id`. The value shows in the trigger, so the label names the field.\n- Pass `items` so the trigger can resolve a label before an option has mounted.\n Without it, Base UI falls back to the raw value.\n- Give `SelectValue` a placeholder that says what to do, such as \"Choose a\n region\", rather than naming a default the reader did not choose.\n- Add `SelectGroup` and `SelectLabel` once a list passes roughly eight options,\n so the reader can scan by group.\n- A disabled option is still readable. Explain why it cannot be chosen in\n adjacent text rather than leaving a dead row unexplained.\n\n### Content guidelines\n\n- Write option labels as sentence-case nouns: \"Europe West\", not \"EUROPE WEST\".\n- Order options by the reader's expectation: size, plan, region, or product.\n- Reserve the first option for a real choice. Do not add an empty \"Select one\"\n row when the placeholder already asks the question.\n\n**A labelled Select with a grouped option list and a controlled value.**\n\n```tsx\n'use client'\n\nimport { useState } from 'react'\n\nimport { Field, FieldLabel } from '@nanisoft/prism-ui/components/field'\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectTrigger,\n SelectValue,\n} from '@nanisoft/prism-ui/components/select'\n\n/** A labelled Select with a grouped option list and a controlled value. */\nexport default function SelectDemo() {\n const [role, setRole] = useState('editor')\n\n return (\n <div className=\"flex max-w-measure-narrow flex-col gap-6\">\n <Field>\n <FieldLabel htmlFor=\"select-role\">Role</FieldLabel>\n <Select\n value={role}\n onValueChange={(value) => setRole(value ?? '')}\n items={{ viewer: 'Viewer', editor: 'Editor', admin: 'Admin' }}\n >\n <SelectTrigger id=\"select-role\" className=\"w-full\">\n <SelectValue placeholder=\"Choose a role\" />\n </SelectTrigger>\n <SelectContent>\n <SelectGroup>\n <SelectLabel>Team roles</SelectLabel>\n <SelectItem value=\"viewer\">Viewer</SelectItem>\n <SelectItem value=\"editor\">Editor</SelectItem>\n <SelectItem value=\"admin\">Admin</SelectItem>\n </SelectGroup>\n </SelectContent>\n </Select>\n </Field>\n <Field>\n <FieldLabel htmlFor=\"select-region\">Region</FieldLabel>\n <Select\n defaultValue=\"eu-west\"\n items={{\n 'eu-west': 'Europe West',\n 'us-east': 'US East',\n 'ap-south': 'Asia South',\n }}\n >\n <SelectTrigger id=\"select-region\" className=\"w-full\">\n <SelectValue placeholder=\"Choose a region\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"eu-west\">Europe West</SelectItem>\n <SelectItem value=\"us-east\">US East</SelectItem>\n <SelectItem value=\"ap-south\" disabled>\n Asia South (paused)\n </SelectItem>\n </SelectContent>\n </Select>\n </Field>\n </div>\n )\n}\n```\n\n## Props\n\n**`value`** `string` · optional · default: — — The selected value, when the select is controlled.\n**`defaultValue`** `string` · optional · default: — — The value selected initially, for an uncontrolled select.\n**`onValueChange`** `(value: string | null) => void` · optional · default: — — Called when the selected value changes.\n**`open`** `boolean` · optional · default: — — The controlled open state of the popup.\n**`defaultOpen`** `boolean` · optional · default: — — The initially open state of the popup.\n**`onOpenChange`** `(open: boolean) => void` · optional · default: — — Called when the popup opens or closes.\n**`name`** `string` · optional · default: — — The form field name submitted with the form.\n**`form`** `string` · optional · default: — — Identifies the form that owns the hidden input.\n**`required`** `boolean` · optional · default: — — Whether a value must be chosen before the form submits.\n**`disabled`** `boolean` · optional · default: — — Whether the select ignores user interaction.\n**`readOnly`** `boolean` · optional · default: — — Whether the user may open the popup but not change the value.\n**`items`** `Record<string, React.ReactNode> | ReadonlyArray<{ label: React.ReactNode; value: string; }>` · optional · default: — — The options, keyed by value, so the trigger can render a label before any item has mounted. Without it, `SelectValue` shows the raw value.\n**`itemToStringLabel`** `(value: string) => string` · optional · default: — — Converts an item value to the label shown in the trigger.\n**`children`** `React.ReactNode` · optional · default: — — The trigger and the popup content.\n**`size`** `'sm' | 'default'` · optional · default: `default` — The trigger's height.\n**`placeholder`** `React.ReactNode` · optional · default: — — Shown when no value is selected.\n**`side`** `'top' | 'right' | 'bottom' | 'left'` · optional · default: `bottom` — Which side of the trigger the list is placed on.\n**`align`** `'start' | 'center' | 'end'` · optional · default: `center` — How the list aligns against the trigger.\n**`sideOffset`** `number` · optional · default: `4` — The distance in pixels between the trigger and the list.\n**`label`** `string` · optional · default: — — A text label used for keyboard typeahead when the option text differs.\n",
701
+ "props": "## Props\n\n**`value`** `string` · optional · default: — — The selected value, when the select is controlled.\n**`defaultValue`** `string` · optional · default: — — The value selected initially, for an uncontrolled select.\n**`onValueChange`** `(value: string | null) => void` · optional · default: — — Called when the selected value changes.\n**`open`** `boolean` · optional · default: — — The controlled open state of the popup.\n**`defaultOpen`** `boolean` · optional · default: — — The initially open state of the popup.\n**`onOpenChange`** `(open: boolean) => void` · optional · default: — — Called when the popup opens or closes.\n**`name`** `string` · optional · default: — — The form field name submitted with the form.\n**`form`** `string` · optional · default: — — Identifies the form that owns the hidden input.\n**`required`** `boolean` · optional · default: — — Whether a value must be chosen before the form submits.\n**`disabled`** `boolean` · optional · default: — — Whether the select ignores user interaction.\n**`readOnly`** `boolean` · optional · default: — — Whether the user may open the popup but not change the value.\n**`items`** `Record<string, React.ReactNode> | ReadonlyArray<{ label: React.ReactNode; value: string; }>` · optional · default: — — The options, keyed by value, so the trigger can render a label before any item has mounted. Without it, `SelectValue` shows the raw value.\n**`itemToStringLabel`** `(value: string) => string` · optional · default: — — Converts an item value to the label shown in the trigger.\n**`children`** `React.ReactNode` · optional · default: — — The trigger and the popup content.\n**`size`** `'sm' | 'default'` · optional · default: `default` — The trigger's height.\n**`placeholder`** `React.ReactNode` · optional · default: — — Shown when no value is selected.\n**`side`** `'top' | 'right' | 'bottom' | 'left'` · optional · default: `bottom` — Which side of the trigger the list is placed on.\n**`align`** `'start' | 'center' | 'end'` · optional · default: `center` — How the list aligns against the trigger.\n**`sideOffset`** `number` · optional · default: `4` — The distance in pixels between the trigger and the list.\n**`label`** `string` · optional · default: — — A text label used for keyboard typeahead when the option text differs.",
702
702
  "example": {
703
703
  "title": "A labelled Select with a grouped option list and a controlled value.",
704
704
  "code": "'use client'\n\nimport { useState } from 'react'\n\nimport { Field, FieldLabel } from '@nanisoft/prism-ui/components/field'\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectTrigger,\n SelectValue,\n} from '@nanisoft/prism-ui/components/select'\n\n/** A labelled Select with a grouped option list and a controlled value. */\nexport default function SelectDemo() {\n const [role, setRole] = useState('editor')\n\n return (\n <div className=\"flex max-w-measure-narrow flex-col gap-6\">\n <Field>\n <FieldLabel htmlFor=\"select-role\">Role</FieldLabel>\n <Select\n value={role}\n onValueChange={(value) => setRole(value ?? '')}\n items={{ viewer: 'Viewer', editor: 'Editor', admin: 'Admin' }}\n >\n <SelectTrigger id=\"select-role\" className=\"w-full\">\n <SelectValue placeholder=\"Choose a role\" />\n </SelectTrigger>\n <SelectContent>\n <SelectGroup>\n <SelectLabel>Team roles</SelectLabel>\n <SelectItem value=\"viewer\">Viewer</SelectItem>\n <SelectItem value=\"editor\">Editor</SelectItem>\n <SelectItem value=\"admin\">Admin</SelectItem>\n </SelectGroup>\n </SelectContent>\n </Select>\n </Field>\n <Field>\n <FieldLabel htmlFor=\"select-region\">Region</FieldLabel>\n <Select\n defaultValue=\"eu-west\"\n items={{\n 'eu-west': 'Europe West',\n 'us-east': 'US East',\n 'ap-south': 'Asia South',\n }}\n >\n <SelectTrigger id=\"select-region\" className=\"w-full\">\n <SelectValue placeholder=\"Choose a region\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"eu-west\">Europe West</SelectItem>\n <SelectItem value=\"us-east\">US East</SelectItem>\n <SelectItem value=\"ap-south\" disabled>\n Asia South (paused)\n </SelectItem>\n </SelectContent>\n </Select>\n </Field>\n </div>\n )\n}\n"
@@ -1186,8 +1186,8 @@
1186
1186
  "NoteGrid01"
1187
1187
  ],
1188
1188
  "importLine": "import { NoteGrid01 } from '@nanisoft/prism-ui/blocks/note-grid-01'",
1189
- "doc": "# NoteGrid01\n\nA grid of short points: a title and a sentence or two, with no tile and no icon.\n\n```tsx\nimport { NoteGrid01 } from '@nanisoft/prism-ui/blocks/note-grid-01'\n```\n\n## Overview\n\nNoteGrid01 is a grid of short points: a title and a sentence or two, on a hairline\nabove each one, with no tile and no icon.\n\nThree of the four NaniSoft sites compose this section, immediately after the\nprocess rail and before the next numbered band, and each one drew it as the same\nthing: a multi-column set of short points, each a bold title and a short\nparagraph, with no card and no icon. All three also composed it *twice* - once\nunder the rail and once under a different section - so three hand-written grids\nbecame six, each with its own column count and its own spacing.\n\nIt is a `dl` rather than a grid of `div`s, because each note is a term and its\nexplanation and a definition list is what says so.\n\n## Guidelines\n\n### When to use\n\nUse NoteGrid01 where a section needs to make several points that each need a\nsentence, and the points are peers.\n\nUse it where a card would be too tight for the words. That is the whole\ndifference from `FeatureGrid01`: a card carries a title and a fragment, and a\nnote carries a title and a paragraph.\n\n### When not to use\n\nDo not use it for features. `FeatureGrid01` is for that, with its icon tiles. A\nnote with an icon in front of it reads as a feature and is read as one, and a\nsection of twelve features is a page that argues rather than explains.\n\nDo not use it where one of the points is much longer than the others. The grid is\nscanned by row, and one long body makes its own row the only one read.\n\n### Best practices\n\n- Keep a body to three lines at the widest viewport. Past that the detail belongs\n in the section's `description`, where it applies to the set.\n- Keep every title to one line. The grid is scanned down its left edge, and a\n title that wraps costs every other row its alignment.\n- Four notes is the comfortable case. Six is the limit at the largest viewport.\n- Use `caption` for the sentence that qualifies the set. It is the honest place\n for \"and here is what it does not do\".\n\n### Content guidelines\n\n- Write a title as a short noun phrase, not a sentence.\n- Write a body as one or two sentences that stand alone. A reader may read one row\n and stop.\n- Do not start every title with the same word. Six titles that all begin \"One\" are\n a list of the same claim.\n\n**Three points, each a title and a sentence, on a hairline and no tile.**\n\n```tsx\nimport { NoteGrid01 } from '@nanisoft/prism-ui/blocks/note-grid-01'\n\n/** Three points, each a title and a sentence, on a hairline and no tile. */\nexport default function NoteGrid01Demo() {\n return (\n <NoteGrid01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"What the feed guarantees\"\n description=\"Three properties, and the test that holds each one.\"\n notes={[\n { title: 'One canonical timestamp', body: 'Every row carries the same clock, set once at capture.' },\n { title: 'One symbol form', body: 'The same instrument is spelled the same way in every table.' },\n { title: 'A pinned column list', body: 'Held by a test rather than by a convention nobody reads.' },\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`** `string` · optional · default: — — The section title. Omit it for a grid composed under its own heading.\n**`description`** `string` · optional · default: — — One or two sentences under the title.\n**`notes`** `readonly NoteGrid01Note[]` · required · default: — — The notes, in the order a reader should meet them. Order is the caller's because it is a claim about which point matters first.\n**`caption`** `string` · optional · default: — — The line under the grid, for the sentence that qualifies it: what the points do not cover, or where to read more.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the section title. See `HeadingLevel`.\n**`body`** `string` · required · default: — — One or two sentences about the point.\n**`children`** `[title ? (_jsx(SectionHeading, { as: headingLevel, align: \"left\", eyebrow: eyebrow, title: title, description: description, className: \"mb-12\" })) : null, _jsx(\"dl\", { \"data-slot\": \"note-grid\", className: \"grid gap-8 sm:grid-cols-2 lg:grid-cols-3\", children: notes.map((note) => (_jsxs(\"div\", { \"data-slot\": \"note-grid-note\", className: \"border-border flex flex-col gap-2 border-t pt-4\", children: [_jsx(\"dt\", { className: \"text-sm font-semibold\", children: note.title }), _jsx(\"dd\", { className: \"text-muted-foreground text-pretty text-sm\", children: note.body })] }, note.title))) }), caption ? _jsx(\"p\", { className: \"text-muted-foreground mt-6 text-pretty text-sm\", children: caption }) : null]` · required · default: —\n**`as`** `headingLevel, align: \"left\", eyebrow: eyebrow, title: title, description: description, className: \"mb-12\"` · required · default: —\n**`className`** `\"text-sm font-semibold\", children: note.title` · required · default: —\n\n## Composition\n\n**`exports`** `NoteGrid01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/note-grid-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `Section` — the public Prism exports this item composes\n",
1190
- "props": "## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the section title. See the No-Default-Eyebrow Rule.\n**`title`** `string` · optional · default: — — The section title. Omit it for a grid composed under its own heading.\n**`description`** `string` · optional · default: — — One or two sentences under the title.\n**`notes`** `readonly NoteGrid01Note[]` · required · default: — — The notes, in the order a reader should meet them. Order is the caller's because it is a claim about which point matters first.\n**`caption`** `string` · optional · default: — — The line under the grid, for the sentence that qualifies it: what the points do not cover, or where to read more.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the section title. See `HeadingLevel`.\n**`body`** `string` · required · default: — — One or two sentences about the point.\n**`children`** `[title ? (_jsx(SectionHeading, { as: headingLevel, align: \"left\", eyebrow: eyebrow, title: title, description: description, className: \"mb-12\" })) : null, _jsx(\"dl\", { \"data-slot\": \"note-grid\", className: \"grid gap-8 sm:grid-cols-2 lg:grid-cols-3\", children: notes.map((note) => (_jsxs(\"div\", { \"data-slot\": \"note-grid-note\", className: \"border-border flex flex-col gap-2 border-t pt-4\", children: [_jsx(\"dt\", { className: \"text-sm font-semibold\", children: note.title }), _jsx(\"dd\", { className: \"text-muted-foreground text-pretty text-sm\", children: note.body })] }, note.title))) }), caption ? _jsx(\"p\", { className: \"text-muted-foreground mt-6 text-pretty text-sm\", children: caption }) : null]` · required · default: —\n**`as`** `headingLevel, align: \"left\", eyebrow: eyebrow, title: title, description: description, className: \"mb-12\"` · required · default: —\n**`className`** `\"text-sm font-semibold\", children: note.title` · required · default: —",
1189
+ "doc": "# NoteGrid01\n\nA grid of short points: a title and a sentence or two, with no tile and no icon.\n\n```tsx\nimport { NoteGrid01 } from '@nanisoft/prism-ui/blocks/note-grid-01'\n```\n\n## Overview\n\nNoteGrid01 is a grid of short points: a title and a sentence or two, on a hairline\nabove each one, with no tile and no icon.\n\nThree of the four NaniSoft sites compose this section, immediately after the\nprocess rail and before the next numbered band, and each one drew it as the same\nthing: a multi-column set of short points, each a bold title and a short\nparagraph, with no card and no icon. All three also composed it *twice* - once\nunder the rail and once under a different section - so three hand-written grids\nbecame six, each with its own column count and its own spacing.\n\nIt is a `dl` rather than a grid of `div`s, because each note is a term and its\nexplanation and a definition list is what says so.\n\n## Guidelines\n\n### When to use\n\nUse NoteGrid01 where a section needs to make several points that each need a\nsentence, and the points are peers.\n\nUse it where a card would be too tight for the words. That is the whole\ndifference from `FeatureGrid01`: a card carries a title and a fragment, and a\nnote carries a title and a paragraph.\n\n### When not to use\n\nDo not use it for features. `FeatureGrid01` is for that, with its icon tiles. A\nnote with an icon in front of it reads as a feature and is read as one, and a\nsection of twelve features is a page that argues rather than explains.\n\nDo not use it where one of the points is much longer than the others. The grid is\nscanned by row, and one long body makes its own row the only one read.\n\n### Best practices\n\n- Keep a body to three lines at the widest viewport. Past that the detail belongs\n in the section's `description`, where it applies to the set.\n- Keep every title to one line. The grid is scanned down its left edge, and a\n title that wraps costs every other row its alignment.\n- Four notes is the comfortable case. Six is the limit at the largest viewport.\n- Use `caption` for the sentence that qualifies the set. It is the honest place\n for \"and here is what it does not do\".\n\n### Content guidelines\n\n- Write a title as a short noun phrase, not a sentence.\n- Write a body as one or two sentences that stand alone. A reader may read one row\n and stop.\n- Do not start every title with the same word. Six titles that all begin \"One\" are\n a list of the same claim.\n\n**Three points, each a title and a sentence, on a hairline and no tile.**\n\n```tsx\nimport { NoteGrid01 } from '@nanisoft/prism-ui/blocks/note-grid-01'\n\n/** Three points, each a title and a sentence, on a hairline and no tile. */\nexport default function NoteGrid01Demo() {\n return (\n <NoteGrid01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"What the feed guarantees\"\n description=\"Three properties, and the test that holds each one.\"\n notes={[\n { title: 'One canonical timestamp', body: 'Every row carries the same clock, set once at capture.' },\n { title: 'One symbol form', body: 'The same instrument is spelled the same way in every table.' },\n { title: 'A pinned column list', body: 'Held by a test rather than by a convention nobody reads.' },\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`** `string` · optional · default: — — The section title. Omit it for a grid composed under its own heading.\n**`description`** `string` · optional · default: — — One or two sentences under the title.\n**`notes`** `readonly NoteGrid01Note[]` · required · default: — — The notes, in the order a reader should meet them. Order is the caller's because it is a claim about which point matters first.\n**`caption`** `string` · optional · default: — — The line under the grid, for the sentence that qualifies it: what the points do not cover, or where to read more.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the section title. See `HeadingLevel`.\n\n## Composition\n\n**`exports`** `NoteGrid01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/note-grid-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `Section` — the public Prism exports this item composes\n",
1190
+ "props": "## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the section title. See the No-Default-Eyebrow Rule.\n**`title`** `string` · optional · default: — — The section title. Omit it for a grid composed under its own heading.\n**`description`** `string` · optional · default: — — One or two sentences under the title.\n**`notes`** `readonly NoteGrid01Note[]` · required · default: — — The notes, in the order a reader should meet them. Order is the caller's because it is a claim about which point matters first.\n**`caption`** `string` · optional · default: — — The line under the grid, for the sentence that qualifies it: what the points do not cover, or where to read more.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the section title. See `HeadingLevel`.",
1191
1191
  "composition": "## Composition\n\n**`exports`** `NoteGrid01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/note-grid-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `Section` — the public Prism exports this item composes",
1192
1192
  "example": {
1193
1193
  "title": "Three points, each a title and a sentence, on a hairline and no tile.",
@@ -1294,8 +1294,8 @@
1294
1294
  "ProductGrid01"
1295
1295
  ],
1296
1296
  "importLine": "import { ProductGrid01 } from '@nanisoft/prism-ui/blocks/product-grid-01'",
1297
- "doc": "# ProductGrid01\n\nA set of products as hairline rows: a two-part ProductMark, a name, one tagline and a destination per row.\n\n```tsx\nimport { ProductGrid01 } from '@nanisoft/prism-ui/blocks/product-grid-01'\n```\n\n## Overview\n\nProductGrid01 is a set of products as a stack of hairline rows: a two-part mark, a\nname, one tagline and a destination per row.\n\nAll four NaniSoft sites publish this section, and each one drew it by hand with\nthe same three parts in the same order: a colour mark for the product, the\nproduct's name, and one line about what it is, on a hairline row with the whole\nrow a link. Each site also resolved the mark's ink in JavaScript from the live\nmode, which is the part that is a second source of truth.\n\n**The mark is two parts, and it is `ProductMark`.** The ring is `brand-ink` and\nthe core is the product's own `primary`, with the name in the brand ink beside it.\nThat split is settled by a measurement rather than by taste: a pastel brand value\nis a fill and never text, so the core may be `primary` and the name may not, and a\n*filled* mark on a light card measures under the threshold a mark has to clear, so\nthe mark is a hairline ring around the fill rather than a disc of it.\n\n## Guidelines\n\n### When to use\n\nUse ProductGrid01 where the reader's question is \"which of these should I open\".\nThat is a family of products, a set of integrations, a set of sibling sites.\n\nUse it when each member is one line long. A member that needs a paragraph is a\n`FeatureGrid01` card, and a paragraph in a row makes the row a column of\nparagraphs.\n\n### When not to use\n\nDo not use it for a set of things that are not navigable. A grid of capabilities\nis a `FeatureGrid01`; there is nothing to click, and a row that pretends to be a\nlink teaches a reader that everything on a page is.\n\nDo not use it for two members. Two products side by side is a hero with two\nthings in it, and a hairline list of two is a list.\n\n### Best practices\n\n- Order the rows by how likely a reader is to want each one. Order is a claim and\n it is yours.\n- Pass `newTab` per row rather than globally. Prism does not decide what counts as\n external, and a same-site link that opens a tab is a reader who has lost the\n page.\n- Leave `pack` off the one member that has no pack of its own. That member wears\n the spectrum.\n- Use one mark size across the grid. The grid passes one size to every mark, so a\n mixed set is not reachable from here.\n\n### Content guidelines\n\n- Keep a tagline to one line. It is what a reader scans.\n- Write the tagline as what the product is, not as what it does for the reader.\n- Use the product's own name, in the case it uses everywhere else.\n\n**Two members, one wearing a pack and one wearing the spectrum.**\n\n```tsx\nimport { ProductGrid01 } from '@nanisoft/prism-ui/blocks/product-grid-01'\n\n/** Two members, one wearing a pack and one wearing the spectrum. */\nexport default function ProductGrid01Demo() {\n return (\n <ProductGrid01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"Built on one platform\"\n description=\"One line about the set, above the rows rather than inside one.\"\n caption=\"The company root has no pack of its own, so its mark wears the spectrum.\"\n products={[\n {\n id: 'nexus',\n name: 'Nexus',\n pack: 'lavender',\n tagline: 'The agent factory: an issue in, a reviewed change out.',\n href: '#nexus',\n },\n { id: 'www', name: 'NaniSoft', tagline: 'The company behind the platform.', href: '#www' },\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`** `string` · optional · default: — — The section title. Omit it for a grid composed under its own heading.\n**`description`** `string` · optional · default: — — One or two sentences under the title.\n**`products`** `readonly ProductGrid01Product[]` · required · default: — — The products, in the order a reader should meet them. Order is the caller's because it is a claim about which product matters first.\n**`caption`** `string` · optional · default: — — The line under the grid, for the sentence that qualifies it: what the set does not include, or where to read more.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the section title. See `HeadingLevel`.\n**`id`** `string` · required · default: — — The product's key within the set. Carried on the markup as `data-product` through `ProductMark`, so a test can name one product rather than the first one, and two products in one document are individually addressable.\n**`name`** `string` · required · default: — — The product's own name, which is the second part of its mark. It is the anchor of the row and the first thing a reader reads.\n**`pack`** `ProductMarkProps['pack']` · optional · default: — — The product's pack, which is the hue of the mark's core. Omit it, or pass `null`, for the one member of a set that has no pack of its own: that member wears the full spectrum rather than a colourless mark.\n**`tagline`** `string` · required · default: — — One line about what the product is.\n**`href`** `string` · required · default: — — Where the row goes. Rendered as a native anchor's `href`, so the row is a link with the browser's own affordances rather than a click handler.\n**`newTab`** `boolean` · optional · default: — — Opens the destination in a new browsing context, which defaults the link relationship to `noopener noreferrer`. Prism does not decide what counts as external, so the caller declares it.\n\n## Composition\n\n**`exports`** `ProductGrid01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/product-grid-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `ProductMark, Section` — the public Prism exports this item composes\n",
1298
- "props": "## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the section title. See the No-Default-Eyebrow Rule.\n**`title`** `string` · optional · default: — — The section title. Omit it for a grid composed under its own heading.\n**`description`** `string` · optional · default: — — One or two sentences under the title.\n**`products`** `readonly ProductGrid01Product[]` · required · default: — — The products, in the order a reader should meet them. Order is the caller's because it is a claim about which product matters first.\n**`caption`** `string` · optional · default: — — The line under the grid, for the sentence that qualifies it: what the set does not include, or where to read more.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the section title. See `HeadingLevel`.\n**`id`** `string` · required · default: — — The product's key within the set. Carried on the markup as `data-product` through `ProductMark`, so a test can name one product rather than the first one, and two products in one document are individually addressable.\n**`name`** `string` · required · default: — — The product's own name, which is the second part of its mark. It is the anchor of the row and the first thing a reader reads.\n**`pack`** `ProductMarkProps['pack']` · optional · default: — — The product's pack, which is the hue of the mark's core. Omit it, or pass `null`, for the one member of a set that has no pack of its own: that member wears the full spectrum rather than a colourless mark.\n**`tagline`** `string` · required · default: — — One line about what the product is.\n**`href`** `string` · required · default: — — Where the row goes. Rendered as a native anchor's `href`, so the row is a link with the browser's own affordances rather than a click handler.\n**`newTab`** `boolean` · optional · default: — — Opens the destination in a new browsing context, which defaults the link relationship to `noopener noreferrer`. Prism does not decide what counts as external, so the caller declares it.",
1297
+ "doc": "# ProductGrid01\n\nA set of products as hairline rows: a two-part ProductMark, a name, one tagline and a destination per row.\n\n```tsx\nimport { ProductGrid01 } from '@nanisoft/prism-ui/blocks/product-grid-01'\n```\n\n## Overview\n\nProductGrid01 is a set of products as a stack of hairline rows: a two-part mark, a\nname, one tagline and a destination per row.\n\nAll four NaniSoft sites publish this section, and each one drew it by hand with\nthe same three parts in the same order: a colour mark for the product, the\nproduct's name, and one line about what it is, on a hairline row with the whole\nrow a link. Each site also resolved the mark's ink in JavaScript from the live\nmode, which is the part that is a second source of truth.\n\n**The mark is two parts, and it is `ProductMark`.** The ring is `brand-ink` and\nthe core is the product's own `primary`, with the name in the brand ink beside it.\nThat split is settled by a measurement rather than by taste: a pastel brand value\nis a fill and never text, so the core may be `primary` and the name may not, and a\n*filled* mark on a light card measures under the threshold a mark has to clear, so\nthe mark is a hairline ring around the fill rather than a disc of it.\n\n## Guidelines\n\n### When to use\n\nUse ProductGrid01 where the reader's question is \"which of these should I open\".\nThat is a family of products, a set of integrations, a set of sibling sites.\n\nUse it when each member is one line long. A member that needs a paragraph is a\n`FeatureGrid01` card, and a paragraph in a row makes the row a column of\nparagraphs.\n\n### When not to use\n\nDo not use it for a set of things that are not navigable. A grid of capabilities\nis a `FeatureGrid01`; there is nothing to click, and a row that pretends to be a\nlink teaches a reader that everything on a page is.\n\nDo not use it for two members. Two products side by side is a hero with two\nthings in it, and a hairline list of two is a list.\n\n### Best practices\n\n- Order the rows by how likely a reader is to want each one. Order is a claim and\n it is yours.\n- Pass `newTab` per row rather than globally. Prism does not decide what counts as\n external, and a same-site link that opens a tab is a reader who has lost the\n page.\n- Leave `pack` off the one member that has no pack of its own. That member wears\n the spectrum.\n- Use one mark size across the grid. The grid passes one size to every mark, so a\n mixed set is not reachable from here.\n\n### Content guidelines\n\n- Keep a tagline to one line. It is what a reader scans.\n- Write the tagline as what the product is, not as what it does for the reader.\n- Use the product's own name, in the case it uses everywhere else.\n\n**Two members, one wearing a pack and one wearing the spectrum.**\n\n```tsx\nimport { ProductGrid01 } from '@nanisoft/prism-ui/blocks/product-grid-01'\n\n/** Two members, one wearing a pack and one wearing the spectrum. */\nexport default function ProductGrid01Demo() {\n return (\n <ProductGrid01\n headingLevel=\"h3\"\n eyebrow=\"Preview\"\n title=\"Built on one platform\"\n description=\"One line about the set, above the rows rather than inside one.\"\n caption=\"The company root has no pack of its own, so its mark wears the spectrum.\"\n products={[\n {\n id: 'nexus',\n name: 'Nexus',\n pack: 'lavender',\n tagline: 'The agent factory: an issue in, a reviewed change out.',\n href: '#nexus',\n },\n { id: 'www', name: 'NaniSoft', tagline: 'The company behind the platform.', href: '#www' },\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`** `string` · optional · default: — — The section title. Omit it for a grid composed under its own heading.\n**`description`** `string` · optional · default: — — One or two sentences under the title.\n**`products`** `readonly ProductGrid01Product[]` · required · default: — — The products, in the order a reader should meet them. Order is the caller's because it is a claim about which product matters first.\n**`caption`** `string` · optional · default: — — The line under the grid, for the sentence that qualifies it: what the set does not include, or where to read more.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the section title. See `HeadingLevel`.\n\n## Composition\n\n**`exports`** `ProductGrid01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/product-grid-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `ProductMark, Section` — the public Prism exports this item composes\n",
1298
+ "props": "## Props\n\n**`eyebrow`** `string` · optional · default: — — Optional label above the section title. See the No-Default-Eyebrow Rule.\n**`title`** `string` · optional · default: — — The section title. Omit it for a grid composed under its own heading.\n**`description`** `string` · optional · default: — — One or two sentences under the title.\n**`products`** `readonly ProductGrid01Product[]` · required · default: — — The products, in the order a reader should meet them. Order is the caller's because it is a claim about which product matters first.\n**`caption`** `string` · optional · default: — — The line under the grid, for the sentence that qualifies it: what the set does not include, or where to read more.\n**`headingLevel`** `HeadingLevel` · optional · default: — — Heading level for the section title. See `HeadingLevel`.",
1299
1299
  "composition": "## Composition\n\n**`exports`** `ProductGrid01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/product-grid-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `ProductMark, Section` — the public Prism exports this item composes",
1300
1300
  "example": {
1301
1301
  "title": "Two members, one wearing a pack and one wearing the spectrum.",
@@ -1321,8 +1321,8 @@
1321
1321
  "SettingsPanel01"
1322
1322
  ],
1323
1323
  "importLine": "import { SettingsPanel01 } from '@nanisoft/prism-ui/blocks/settings-panel-01'",
1324
- "doc": "# SettingsPanel01\n\nA settings form grouped into sections of text, select and switch fields.\n\n```tsx\nimport { SettingsPanel01 } from '@nanisoft/prism-ui/blocks/settings-panel-01'\n```\n\n## Overview\n\nSettingsPanel01 is a settings form grouped into named sections. The card holds\nthe title and description, each section holds a heading and its fields, and the\nfooter holds the save and cancel controls, with a rule between sections.\n\nThe block composes these components:\n\n| Part | Component | Role |\n| --- | --- | --- |\n| Surface | Card, Separator | the panel and the rule between sections |\n| Fields | Field, Input, Textarea | labelled text controls |\n| Choices | Select, Switch | one-of-many and on-or-off fields |\n| Footer | Button | the save and secondary actions |\n\nA field is a small object that describes one control: its `kind`, its label, an\noptional description and its value handlers. The block owns no values and no\ncopy. Omitting `submitLabel` renders no footer, the right shape for a read-only\nview.\n\n## Guidelines\n\n### When to use\n\nUse SettingsPanel01 when a screen changes several settings that share one save.\nIt works when the groups are stable and a reader scans them top to bottom.\n\n### When not to use\n\nDo not use it for a single setting. One field in a card is a heavier frame than\nthe choice deserves; put the control in the page.\n\nDo not use it when each change should apply immediately with no save. A switch\ninside a saved form reads as applied; use standalone switches with their own\nhandler instead.\n\n### Best practices\n\n- Give every field a stable `id`, so the label is linked to the control.\n- Keep one save per panel. A mixed model, where some fields save and others do\n not, makes the footer's promise unclear.\n- Put the consequential choice first in its section, so a reader does not hunt\n for the setting that matters most.\n- Disable the submit control while a save is in flight.\n\n### Content guidelines\n\n- A section title is one or two words in sentence case: \"General\",\n \"Notifications\".\n- A field label is a short noun phrase and does not end in a colon.\n\n**A grouped settings form with text, select and switch fields.**\n\n```tsx\n'use client'\n\nimport { SettingsPanel01 } from '@nanisoft/prism-ui/blocks/settings-panel-01'\n\n/** A grouped settings form with text, select and switch fields. */\nexport default function SettingsPanel01Demo() {\n return (\n <SettingsPanel01\n title=\"Workspace settings\"\n description=\"Manage how this workspace behaves for everyone on the team.\"\n submitLabel=\"Save changes\"\n secondaryAction={{ label: 'Cancel' }}\n sections={[\n {\n id: 'general',\n title: 'General',\n description: 'Shown to everyone you collaborate with.',\n fields: [\n {\n kind: 'text',\n id: 'workspace-name',\n label: 'Workspace name',\n defaultValue: 'Northwind',\n },\n {\n kind: 'textarea',\n id: 'workspace-description',\n label: 'Description',\n placeholder: 'What does this workspace do?',\n rows: 3,\n },\n {\n kind: 'select',\n id: 'workspace-region',\n label: 'Data region',\n placeholder: 'Choose a region',\n defaultValue: 'eu-west',\n options: [\n { label: 'Europe West', value: 'eu-west' },\n { label: 'US East', value: 'us-east' },\n ],\n },\n ],\n },\n {\n id: 'notifications',\n title: 'Notifications',\n fields: [\n {\n kind: 'switch',\n id: 'weekly-digest',\n label: 'Weekly digest',\n description: 'A summary of activity every Monday.',\n defaultChecked: true,\n },\n {\n kind: 'switch',\n id: 'mention-alerts',\n label: 'Mention alerts',\n description: 'Notify me when someone mentions my name.',\n },\n ],\n },\n ]}\n />\n )\n}\n```\n\n## Props\n\n**`title`** `string` · required · default: —\n**`description`** `string` · optional · default: —\n**`sections`** `SettingsPanelSection[]` · required · default: — — The settings groups, each rendered with a heading and its own fields.\n**`submitLabel`** `string` · optional · default: — — The submit button label. Omit to render no footer.\n**`onSubmit`** `(event: FormEvent<HTMLFormElement>) => void` · optional · default: —\n**`secondaryAction`** `{ label: string; onClick?: () => void; }` · optional · default: — — A secondary control, such as Cancel.\n**`footer`** `ReactNode` · optional · default: — — Extra content in the footer, e.g. a status note.\n**`label`** `string` · required · default: —\n**`onClick`** `() => void` · optional · default: —\n\n## Composition\n\n**`exports`** `SettingsPanel01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/settings-panel-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `Button, Card, Field, Input, Select, Separator, Switch, Textarea` — the public Prism exports this item composes\n",
1325
- "props": "## Props\n\n**`title`** `string` · required · default: —\n**`description`** `string` · optional · default: —\n**`sections`** `SettingsPanelSection[]` · required · default: — — The settings groups, each rendered with a heading and its own fields.\n**`submitLabel`** `string` · optional · default: — — The submit button label. Omit to render no footer.\n**`onSubmit`** `(event: FormEvent<HTMLFormElement>) => void` · optional · default: —\n**`secondaryAction`** `{ label: string; onClick?: () => void; }` · optional · default: — — A secondary control, such as Cancel.\n**`footer`** `ReactNode` · optional · default: — — Extra content in the footer, e.g. a status note.\n**`label`** `string` · required · default: —\n**`onClick`** `() => void` · optional · default: —",
1324
+ "doc": "# SettingsPanel01\n\nA settings form grouped into sections of text, select and switch fields.\n\n```tsx\nimport { SettingsPanel01 } from '@nanisoft/prism-ui/blocks/settings-panel-01'\n```\n\n## Overview\n\nSettingsPanel01 is a settings form grouped into named sections. The card holds\nthe title and description, each section holds a heading and its fields, and the\nfooter holds the save and cancel controls, with a rule between sections.\n\nThe block composes these components:\n\n| Part | Component | Role |\n| --- | --- | --- |\n| Surface | Card, Separator | the panel and the rule between sections |\n| Fields | Field, Input, Textarea | labelled text controls |\n| Choices | Select, Switch | one-of-many and on-or-off fields |\n| Footer | Button | the save and secondary actions |\n\nA field is a small object that describes one control: its `kind`, its label, an\noptional description and its value handlers. The block owns no values and no\ncopy. Omitting `submitLabel` renders no footer, the right shape for a read-only\nview.\n\n## Guidelines\n\n### When to use\n\nUse SettingsPanel01 when a screen changes several settings that share one save.\nIt works when the groups are stable and a reader scans them top to bottom.\n\n### When not to use\n\nDo not use it for a single setting. One field in a card is a heavier frame than\nthe choice deserves; put the control in the page.\n\nDo not use it when each change should apply immediately with no save. A switch\ninside a saved form reads as applied; use standalone switches with their own\nhandler instead.\n\n### Best practices\n\n- Give every field a stable `id`, so the label is linked to the control.\n- Keep one save per panel. A mixed model, where some fields save and others do\n not, makes the footer's promise unclear.\n- Put the consequential choice first in its section, so a reader does not hunt\n for the setting that matters most.\n- Disable the submit control while a save is in flight.\n\n### Content guidelines\n\n- A section title is one or two words in sentence case: \"General\",\n \"Notifications\".\n- A field label is a short noun phrase and does not end in a colon.\n\n**A grouped settings form with text, select and switch fields.**\n\n```tsx\n'use client'\n\nimport { SettingsPanel01 } from '@nanisoft/prism-ui/blocks/settings-panel-01'\n\n/** A grouped settings form with text, select and switch fields. */\nexport default function SettingsPanel01Demo() {\n return (\n <SettingsPanel01\n title=\"Workspace settings\"\n description=\"Manage how this workspace behaves for everyone on the team.\"\n submitLabel=\"Save changes\"\n secondaryAction={{ label: 'Cancel' }}\n sections={[\n {\n id: 'general',\n title: 'General',\n description: 'Shown to everyone you collaborate with.',\n fields: [\n {\n kind: 'text',\n id: 'workspace-name',\n label: 'Workspace name',\n defaultValue: 'Northwind',\n },\n {\n kind: 'textarea',\n id: 'workspace-description',\n label: 'Description',\n placeholder: 'What does this workspace do?',\n rows: 3,\n },\n {\n kind: 'select',\n id: 'workspace-region',\n label: 'Data region',\n placeholder: 'Choose a region',\n defaultValue: 'eu-west',\n options: [\n { label: 'Europe West', value: 'eu-west' },\n { label: 'US East', value: 'us-east' },\n ],\n },\n ],\n },\n {\n id: 'notifications',\n title: 'Notifications',\n fields: [\n {\n kind: 'switch',\n id: 'weekly-digest',\n label: 'Weekly digest',\n description: 'A summary of activity every Monday.',\n defaultChecked: true,\n },\n {\n kind: 'switch',\n id: 'mention-alerts',\n label: 'Mention alerts',\n description: 'Notify me when someone mentions my name.',\n },\n ],\n },\n ]}\n />\n )\n}\n```\n\n## Props\n\n**`title`** `string` · required · default: —\n**`description`** `string` · optional · default: —\n**`sections`** `SettingsPanelSection[]` · required · default: — — The settings groups, each rendered with a heading and its own fields.\n**`submitLabel`** `string` · optional · default: — — The submit button label. Omit to render no footer.\n**`onSubmit`** `(event: FormEvent<HTMLFormElement>) => void` · optional · default: —\n**`secondaryAction`** `{ label: string; onClick?: () => void; }` · optional · default: — — A secondary control, such as Cancel.\n**`footer`** `ReactNode` · optional · default: — — Extra content in the footer, e.g. a status note.\n\n## Composition\n\n**`exports`** `SettingsPanel01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/settings-panel-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `Button, Card, Field, Input, Select, Separator, Switch, Textarea` — the public Prism exports this item composes\n",
1325
+ "props": "## Props\n\n**`title`** `string` · required · default: —\n**`description`** `string` · optional · default: —\n**`sections`** `SettingsPanelSection[]` · required · default: — — The settings groups, each rendered with a heading and its own fields.\n**`submitLabel`** `string` · optional · default: — — The submit button label. Omit to render no footer.\n**`onSubmit`** `(event: FormEvent<HTMLFormElement>) => void` · optional · default: —\n**`secondaryAction`** `{ label: string; onClick?: () => void; }` · optional · default: — — A secondary control, such as Cancel.\n**`footer`** `ReactNode` · optional · default: — — Extra content in the footer, e.g. a status note.",
1326
1326
  "composition": "## Composition\n\n**`exports`** `SettingsPanel01` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/settings-panel-01/index.tsx` — the file this Block is defined in\n**`dependencies`** `Button, Card, Field, Input, Select, Separator, Switch, Textarea` — the public Prism exports this item composes",
1327
1327
  "example": {
1328
1328
  "title": "A grouped settings form with text, select and switch fields.",
@@ -1375,8 +1375,8 @@
1375
1375
  "SiteHeader"
1376
1376
  ],
1377
1377
  "importLine": "import { SiteHeader } from '@nanisoft/prism-ui/blocks/site-header'",
1378
- "doc": "# SiteHeader\n\nThe bar at the top of a product site: a brand lockup, an optional product switcher, the site navigation and a slot for application controls.\n\n```tsx\nimport { SiteHeader } from '@nanisoft/prism-ui/blocks/site-header'\n```\n\n## Overview\n\nSiteHeader is the bar at the top of a product site: a brand lockup, an optional\nproduct switcher, the site's own navigation, and a slot for the controls your\nproduct owns.\n\nAll four NaniSoft sites compose this exact bar in their root layout, and each one\nwrote its own: the mark in one place, the switcher in a popover in another, the\nnavigation in a third, and each wrote the focus and current-page wiring separately.\n\nIt is a Block and not a Component because it is a region of a page rather than a\ncontrol: it composes a `ProductMark`, a `ProductSwitcher` and a set of links, and\nit owns the bar they sit in.\n\n**There is no product directory in this package.** The product is passed as the\nthree facts its mark is drawn from rather than as an id this package would have to\nresolve. That is the same answer `ProductMark` gives, and for the same reason: a\nconsumer that composes a set of its own products composes the header too.\n\n## Guidelines\n\n### When to use\n\nUse SiteHeader as the chrome around a whole product site. One header per site, in\nthe root layout, above the main region.\n\nUse it on the landing page and on every documentation and blog page alike. A\nheader that changes between sections of one site is two sites' worth of chrome.\n\n### When not to use\n\nDo not use it inside an application screen. `AppShell01` is that: a rail, a top\nbar and a mobile sheet, for signed-in state rather than for a public site.\n\nDo not use it for a product site's in-page navigation. That is a `Tabs` or a\n`Breadcrumb`.\n\n### Best practices\n\n- Pass `product`, and pass `products` only when the site is one of several. A set\n of one renders one mark, so pass nothing rather than a switcher with a single\n member.\n- Mark the current page with `current` on the one link whose destination is the\n current route. The header marks it with `aria-current=\"page\"`; it does not colour\n it, because a colour is not a state a screen reader can read.\n- `navLabel` and `productsLabel` are required, because they are words a reader\n hears. A Block that ships no copy ships no reader-facing copy either, so the name\n of your own landmarks is yours to pass.\n- Put interactive controls in `actions`. It is a slot rather than a prop, because\n an account menu, a sign-in link and a mode toggle are all application state and\n none of them is a fact a header can be told.\n- Use `sticky` only where the page scrolls. A sticky header on a page with no\n scroll costs the reader a strip of viewport for nothing.\n- The bar is one flex row that wraps. The switcher and the navigation wrap onto a\n second line rather than overflowing, so a narrow viewport gets a taller header\n and never a clipped one.\n\n### Content guidelines\n\n- Keep a nav label to one word. It is a bar.\n- Use each product'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\n**The site chrome: a brand lockup, the product set, the site nav and a slot.**\n\n```tsx\nimport { SiteHeader } from '@nanisoft/prism-ui/blocks/site-header'\n\n/** The site chrome: a brand lockup, the product set, the site nav and a slot. */\nexport default function SiteHeaderDemo() {\n return (\n <SiteHeader\n product={{ id: 'nexus', name: 'Nexus', pack: 'lavender' }}\n navLabel=\"Site\"\n productsLabel=\"Products\"\n products={[\n { id: 'nexus', name: 'Nexus', pack: 'lavender', href: '#nexus' },\n { id: 'atlas', name: 'Atlas', pack: 'mint', href: '#atlas' },\n { id: 'alphalens', name: 'AlphaLens', pack: 'blush', href: '#alphalens' },\n ]}\n nav={[\n { label: 'Docs', href: '#docs' },\n { label: 'Blog', href: '#blog' },\n { label: 'About', href: '#about', current: true },\n ]}\n actions={<span className=\"text-muted-foreground text-xs\">A control you own</span>}\n />\n )\n}\n```\n\n## Props\n\n**`product`** `Pick<ProductMarkProps, 'id' | 'name' | 'pack'>` · required · default: — — The product this header belongs to: the id, name and pack its `ProductMark` is drawn from. The header renders the mark itself rather than a slot, because a header whose brand lockup is a slot is a header every consumer has to reimplement, and that is the one part of a site header that is never product-specific.\n**`products`** `readonly SwitcherProduct[]` · optional · default: — — The set of products to move between, rendered as a `ProductSwitcher` between the mark and the navigation. Omit it on a site that is the only product, and pass it on a site that is one of several. A set of one renders one mark, so pass nothing rather than a switcher with a single member.\n**`nav`** `readonly SiteHeaderLink[]` · optional · default: — — The header's own destinations: the sections of this site. Omit it for a header that carries only the brand lockup.\n**`navLabel`** `string` · required · default: — — The accessible name of the site navigation, which is also the visible word a screen reader announces before the list. It is required rather than defaulted because it is a word a reader hears, and a Block that ships no copy ships no reader-facing copy either. A header that defaulted this would put its own name on every consumer's site navigation, and \"Site\" is right for a company site and wrong for a product that has a docs section it wants called something else.\n**`productsLabel`** `string` · optional · default: — — The accessible name of the product switcher. Required whenever `products` is passed, for the same reason `navLabel` is: it is a word a reader hears, and a Block that ships no copy ships none of those either. Omit it and the switcher takes the site navigation's name, which is right when the two are the same set of places and wrong when they are not.\n**`actions`** `ReactNode` · optional · default: — — A slot for the controls a product owns on the right: an account menu, a sign in link, a mode toggle, a theme picker. A slot rather than a prop, because every one of those is application state and none of them is a fact a header can be told.\n**`sticky`** `boolean` · optional · default: — — Keeps the header at the top of the viewport as the page scrolls. Off by default, because a sticky header is a decision about a page rather than about a header, and a header that is sticky on a page with no scroll costs the reader a strip of their viewport.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited.\n**`label`** `string` · required · default: — — The text of the link.\n**`href`** `string` · required · default: — — Where the link goes.\n**`current`** `boolean` · optional · default: — — Marks this link as the page the reader is on. Set it on the one link whose destination is the current route; leave it off every other link.\n**`newTab`** `boolean` · optional · default: — — Opens the destination in a new browsing context, which defaults the link relationship to `noopener noreferrer`. Prism does not decide what counts as external: a cross-origin destination is not by itself a reason to open a tab.\n\n## Composition\n\n**`exports`** `SiteHeader` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/site-header/index.tsx` — the file this Block is defined in\n**`dependencies`** `ProductMark, ProductSwitcher` — the public Prism exports this item composes\n",
1379
- "props": "## Props\n\n**`product`** `Pick<ProductMarkProps, 'id' | 'name' | 'pack'>` · required · default: — — The product this header belongs to: the id, name and pack its `ProductMark` is drawn from. The header renders the mark itself rather than a slot, because a header whose brand lockup is a slot is a header every consumer has to reimplement, and that is the one part of a site header that is never product-specific.\n**`products`** `readonly SwitcherProduct[]` · optional · default: — — The set of products to move between, rendered as a `ProductSwitcher` between the mark and the navigation. Omit it on a site that is the only product, and pass it on a site that is one of several. A set of one renders one mark, so pass nothing rather than a switcher with a single member.\n**`nav`** `readonly SiteHeaderLink[]` · optional · default: — — The header's own destinations: the sections of this site. Omit it for a header that carries only the brand lockup.\n**`navLabel`** `string` · required · default: — — The accessible name of the site navigation, which is also the visible word a screen reader announces before the list. It is required rather than defaulted because it is a word a reader hears, and a Block that ships no copy ships no reader-facing copy either. A header that defaulted this would put its own name on every consumer's site navigation, and \"Site\" is right for a company site and wrong for a product that has a docs section it wants called something else.\n**`productsLabel`** `string` · optional · default: — — The accessible name of the product switcher. Required whenever `products` is passed, for the same reason `navLabel` is: it is a word a reader hears, and a Block that ships no copy ships none of those either. Omit it and the switcher takes the site navigation's name, which is right when the two are the same set of places and wrong when they are not.\n**`actions`** `ReactNode` · optional · default: — — A slot for the controls a product owns on the right: an account menu, a sign in link, a mode toggle, a theme picker. A slot rather than a prop, because every one of those is application state and none of them is a fact a header can be told.\n**`sticky`** `boolean` · optional · default: — — Keeps the header at the top of the viewport as the page scrolls. Off by default, because a sticky header is a decision about a page rather than about a header, and a header that is sticky on a page with no scroll costs the reader a strip of their viewport.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited.\n**`label`** `string` · required · default: — — The text of the link.\n**`href`** `string` · required · default: — — Where the link goes.\n**`current`** `boolean` · optional · default: — — Marks this link as the page the reader is on. Set it on the one link whose destination is the current route; leave it off every other link.\n**`newTab`** `boolean` · optional · default: — — Opens the destination in a new browsing context, which defaults the link relationship to `noopener noreferrer`. Prism does not decide what counts as external: a cross-origin destination is not by itself a reason to open a tab.",
1378
+ "doc": "# SiteHeader\n\nThe bar at the top of a product site: a brand lockup, an optional product switcher, the site navigation and a slot for application controls.\n\n```tsx\nimport { SiteHeader } from '@nanisoft/prism-ui/blocks/site-header'\n```\n\n## Overview\n\nSiteHeader is the bar at the top of a product site: a brand lockup, an optional\nproduct switcher, the site's own navigation, and a slot for the controls your\nproduct owns.\n\nAll four NaniSoft sites compose this exact bar in their root layout, and each one\nwrote its own: the mark in one place, the switcher in a popover in another, the\nnavigation in a third, and each wrote the focus and current-page wiring separately.\n\nIt is a Block and not a Component because it is a region of a page rather than a\ncontrol: it composes a `ProductMark`, a `ProductSwitcher` and a set of links, and\nit owns the bar they sit in.\n\n**There is no product directory in this package.** The product is passed as the\nthree facts its mark is drawn from rather than as an id this package would have to\nresolve. That is the same answer `ProductMark` gives, and for the same reason: a\nconsumer that composes a set of its own products composes the header too.\n\n## Guidelines\n\n### When to use\n\nUse SiteHeader as the chrome around a whole product site. One header per site, in\nthe root layout, above the main region.\n\nUse it on the landing page and on every documentation and blog page alike. A\nheader that changes between sections of one site is two sites' worth of chrome.\n\n### When not to use\n\nDo not use it inside an application screen. `AppShell01` is that: a rail, a top\nbar and a mobile sheet, for signed-in state rather than for a public site.\n\nDo not use it for a product site's in-page navigation. That is a `Tabs` or a\n`Breadcrumb`.\n\n### Best practices\n\n- Pass `product`, and pass `products` only when the site is one of several. A set\n of one renders one mark, so pass nothing rather than a switcher with a single\n member.\n- Mark the current page with `current` on the one link whose destination is the\n current route. The header marks it with `aria-current=\"page\"`; it does not colour\n it, because a colour is not a state a screen reader can read.\n- `navLabel` and `productsLabel` are required, because they are words a reader\n hears. A Block that ships no copy ships no reader-facing copy either, so the name\n of your own landmarks is yours to pass.\n- Put interactive controls in `actions`. It is a slot rather than a prop, because\n an account menu, a sign-in link and a mode toggle are all application state and\n none of them is a fact a header can be told.\n- Use `sticky` only where the page scrolls. A sticky header on a page with no\n scroll costs the reader a strip of viewport for nothing.\n- The bar is one flex row that wraps. The switcher and the navigation wrap onto a\n second line rather than overflowing, so a narrow viewport gets a taller header\n and never a clipped one.\n\n### Content guidelines\n\n- Keep a nav label to one word. It is a bar.\n- Use each product'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\n**The site chrome: a brand lockup, the product set, the site nav and a slot.**\n\n```tsx\nimport { SiteHeader } from '@nanisoft/prism-ui/blocks/site-header'\n\n/** The site chrome: a brand lockup, the product set, the site nav and a slot. */\nexport default function SiteHeaderDemo() {\n return (\n <SiteHeader\n product={{ id: 'nexus', name: 'Nexus', pack: 'lavender' }}\n navLabel=\"Site\"\n productsLabel=\"Products\"\n products={[\n { id: 'nexus', name: 'Nexus', pack: 'lavender', href: '#nexus' },\n { id: 'atlas', name: 'Atlas', pack: 'mint', href: '#atlas' },\n { id: 'alphalens', name: 'AlphaLens', pack: 'blush', href: '#alphalens' },\n ]}\n nav={[\n { label: 'Docs', href: '#docs' },\n { label: 'Blog', href: '#blog' },\n { label: 'About', href: '#about', current: true },\n ]}\n actions={<span className=\"text-muted-foreground text-xs\">A control you own</span>}\n />\n )\n}\n```\n\n## Props\n\n**`product`** `Pick<ProductMarkProps, 'id' | 'name' | 'pack'>` · required · default: — — The product this header belongs to: the id, name and pack its `ProductMark` is drawn from. The header renders the mark itself rather than a slot, because a header whose brand lockup is a slot is a header every consumer has to reimplement, and that is the one part of a site header that is never product-specific.\n**`products`** `readonly SwitcherProduct[]` · optional · default: — — The set of products to move between, rendered as a `ProductSwitcher` between the mark and the navigation. Omit it on a site that is the only product, and pass it on a site that is one of several. A set of one renders one mark, so pass nothing rather than a switcher with a single member.\n**`nav`** `readonly SiteHeaderLink[]` · optional · default: — — The header's own destinations: the sections of this site. Omit it for a header that carries only the brand lockup.\n**`navLabel`** `string` · required · default: — — The accessible name of the site navigation, which is also the visible word a screen reader announces before the list. It is required rather than defaulted because it is a word a reader hears, and a Block that ships no copy ships no reader-facing copy either. A header that defaulted this would put its own name on every consumer's site navigation, and \"Site\" is right for a company site and wrong for a product that has a docs section it wants called something else.\n**`productsLabel`** `string` · optional · default: — — The accessible name of the product switcher. Required whenever `products` is passed, for the same reason `navLabel` is: it is a word a reader hears, and a Block that ships no copy ships none of those either. Omit it and the switcher takes the site navigation's name, which is right when the two are the same set of places and wrong when they are not.\n**`actions`** `ReactNode` · optional · default: — — A slot for the controls a product owns on the right: an account menu, a sign in link, a mode toggle, a theme picker. A slot rather than a prop, because every one of those is application state and none of them is a fact a header can be told.\n**`sticky`** `boolean` · optional · default: — — Keeps the header at the top of the viewport as the page scrolls. Off by default, because a sticky header is a decision about a page rather than about a header, and a header that is sticky on a page with no scroll costs the reader a strip of their viewport.\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`** `SiteHeader` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/site-header/index.tsx` — the file this Block is defined in\n**`dependencies`** `ProductMark, ProductSwitcher` — the public Prism exports this item composes\n",
1379
+ "props": "## Props\n\n**`product`** `Pick<ProductMarkProps, 'id' | 'name' | 'pack'>` · required · default: — — The product this header belongs to: the id, name and pack its `ProductMark` is drawn from. The header renders the mark itself rather than a slot, because a header whose brand lockup is a slot is a header every consumer has to reimplement, and that is the one part of a site header that is never product-specific.\n**`products`** `readonly SwitcherProduct[]` · optional · default: — — The set of products to move between, rendered as a `ProductSwitcher` between the mark and the navigation. Omit it on a site that is the only product, and pass it on a site that is one of several. A set of one renders one mark, so pass nothing rather than a switcher with a single member.\n**`nav`** `readonly SiteHeaderLink[]` · optional · default: — — The header's own destinations: the sections of this site. Omit it for a header that carries only the brand lockup.\n**`navLabel`** `string` · required · default: — — The accessible name of the site navigation, which is also the visible word a screen reader announces before the list. It is required rather than defaulted because it is a word a reader hears, and a Block that ships no copy ships no reader-facing copy either. A header that defaulted this would put its own name on every consumer's site navigation, and \"Site\" is right for a company site and wrong for a product that has a docs section it wants called something else.\n**`productsLabel`** `string` · optional · default: — — The accessible name of the product switcher. Required whenever `products` is passed, for the same reason `navLabel` is: it is a word a reader hears, and a Block that ships no copy ships none of those either. Omit it and the switcher takes the site navigation's name, which is right when the two are the same set of places and wrong when they are not.\n**`actions`** `ReactNode` · optional · default: — — A slot for the controls a product owns on the right: an account menu, a sign in link, a mode toggle, a theme picker. A slot rather than a prop, because every one of those is application state and none of them is a fact a header can be told.\n**`sticky`** `boolean` · optional · default: — — Keeps the header at the top of the viewport as the page scrolls. Off by default, because a sticky header is a decision about a page rather than about a header, and a header that is sticky on a page with no scroll costs the reader a strip of their viewport.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited.",
1380
1380
  "composition": "## Composition\n\n**`exports`** `SiteHeader` — the public runtime names this Block promises\n**`source`** `packages/ui/src/blocks/site-header/index.tsx` — the file this Block is defined in\n**`dependencies`** `ProductMark, ProductSwitcher` — the public Prism exports this item composes",
1381
1381
  "example": {
1382
1382
  "title": "The site chrome: a brand lockup, the product set, the site nav and a slot.",
@@ -1556,6 +1556,33 @@
1556
1556
  "pages": []
1557
1557
  }
1558
1558
  },
1559
+ {
1560
+ "id": "docs-shell",
1561
+ "slug": "docs-shell",
1562
+ "kind": "page",
1563
+ "name": "DocsShell",
1564
+ "category": null,
1565
+ "status": "stable",
1566
+ "description": "A documentation screen: a navigation rail whose shape arrives as data, the document at the measure, a contents rail, and a pager derived from the navigation rather than passed.",
1567
+ "url": "/pages/docs-shell",
1568
+ "mirror": "/pages/docs-shell.md",
1569
+ "source": "src/pages/docs-shell/index.tsx",
1570
+ "exports": [
1571
+ "DocsShell"
1572
+ ],
1573
+ "importLine": "import { DocsShell } from '@nanisoft/prism-ui/pages/docs-shell'",
1574
+ "doc": "# DocsShell\n\nA documentation screen: a navigation rail whose shape arrives as data, the document at the measure, a contents rail, and a pager derived from the navigation rather than passed.\n\n```tsx\nimport { DocsShell } from '@nanisoft/prism-ui/pages/docs-shell'\n```\n\n## Overview\n\nDocsShell is a documentation screen: a navigation rail, the document, a contents\nrail, and the pager to the neighbouring pages.\n\n**It is the Page the other three NaniSoft sites import, and the name is theirs.**\nAll three already read `import { DocsShell, type DocsNavEntry } from\n'@nanisoft/prism-ui/pages'` in their own document template, and all three map\ntheir page tree into that shape in their own `lib/to-prism-tree`. A published\nname cannot be cheaply changed and there is no redirect lane for item routes, so\nthe name follows those three import lines rather than this file's shape. It was\n`docs-shell` in the retired line too, where it was an empty `div` and a class\nname each site then had to style from scratch.\n\n**The shape arrives as data.** The three sites publish full documentation sets\nand none of them agrees on the shape:\n\n- Nexus files six sections of five pages, each named for a documentation genre,\n each with an index.\n- Atlas files four sections, one of which holds nine pages under a single\n heading, and not one of them has an index.\n- AlphaLens files six sections separated by rules, named for subject matter\n rather than for a genre, with a status inside three of the titles.\n\nThe tree is a prop and the Page renders it, so none of the three had to be told\nwhich of the others it looks like.\n\n## Guidelines\n\n### When to use\n\nUse DocsShell as the body of the route that renders one documentation page,\nhanded the page tree, the document's own outline, and the address being rendered.\n\nUse it for every page of a documentation set. A documentation site whose pages\neach carry their own rail is a site whose rail drifts.\n\n### When not to use\n\nDo not use it for a single-page site. It is a whole screen around a tree, and a\nsite with one page has no tree.\n\nDo not use it for a blog. A post is a dated argument about one thing, and\n`BlogPostPage` is that screen.\n\nDo not hand it a `nav` you built by parsing a folder tree at render time. Map\nthe tree once, in your own adapter, and pass the result: a Page that walked a\nfilesystem would be a Page that fetches, and a Page imports no router and no data\nclient.\n\n### Best practices\n\n- **A section is a label, not a control.** It carries no status, no badge, no\n count, no collapse and no sort, and that is the whole of its interface. AlphaLens\n files its documentation as a pipeline, where \"Unified data contract (approved)\"\n is a stage of the work and not a topic a reader may visit in any order, and\n every structural affordance says the opposite. A status in the title is part of\n the title and stays there as words: the Page reads no parentheses, matches no\n vocabulary, and renders no pill beside a heading that has one.\n- **Omit `href` on a group that has no index.** That absence is what makes the\n heading a label rather than a route, and it is the same thing all three sites\n already spell as `url: node.index?.url ?? ''`, which two of them then restyle\n back into a label in their own stylesheets. Naming the case in the type makes\n those rules dead rather than load-bearing, and the type is the only thing that\n had to change.\n- **Do not pass a previous and a next.** The pager is derived from `nav` and\n `currentHref`, which removes one derivation from every consumer. It is derived\n rather than passed for a second reason: a caller who passes neighbours can pass\n a neighbour that is not in the tree, and this is the one arrangement where a\n reader would notice.\n- **Give the rail and the contents their own orders, and let both stand.** `nav`\n is in the order the rail shows and `toc` is in the order the contents rail\n shows. Nothing is sorted, deduplicated or alphabetised. A site that files its\n rail as a pipeline and its contents as a reference has both in one render.\n- Pass `toc` whole or not at all. A partial outline is a contents list that lies\n about the page.\n- Omit `title` when your document owns its own heading. The Page renders no empty\n `h1` to fill the gap, so a document with no title keeps exactly the outline its\n own content declares.\n- Name the three regions. `navLabel`, `tocLabel` and `pagerLabel` are required\n because they are words a reader hears, and a Page that ships no copy ships no\n reader-facing copy either. A site that files its pages as a pipeline and its\n contents as a reference names the three regions three different ways.\n\n### Content guidelines\n\n- Put a section's stage in its title, as AlphaLens does, and let it stay a\n sentence. The Page will not pull it out into a badge for you, and a badge is a\n state the reader can act on, which a stage of a pipeline is not.\n- Keep a rule's title short or leave it empty. A rule with words is a label a\n reader reads twice: once in the rule and once on the section beneath it.\n- Keep a section index page's title the same as its section's title, so a reader\n who arrives at the index and a reader who is already inside the section are\n reading the same words.\n\n**A documentation screen: the rail, the document, the contents and the pager.**\n\n```tsx\nimport { DocsShell, type DocsNavEntry } from '@nanisoft/prism-ui/pages/docs-shell'\n\n/**\n * The shape the three consumer sites actually publish, taken from AlphaLens\n * because it is the one that exercises every part of the interface at once: a\n * status inside a section title, a rule between sections, and a section whose\n * own index is a page a reader lands on.\n */\nconst NAV: DocsNavEntry[] = [\n { type: 'page', title: 'AlphaLens', href: '#alphalens' },\n { type: 'divider', title: 'Data platform (live)' },\n {\n type: 'group',\n title: 'Data platform (live)',\n href: '#data-platform',\n items: [\n { type: 'page', title: 'Capture', href: '#capture' },\n { type: 'page', title: 'Storage', href: '#storage' },\n { type: 'page', title: 'Operations', href: '#platform-operations' },\n ],\n },\n { type: 'divider', title: 'Unified data contract (approved)' },\n {\n type: 'group',\n title: 'Unified data contract (approved)',\n href: '#data-contract',\n items: [\n { type: 'page', title: 'Data feed view', href: '#feed-view' },\n { type: 'page', title: 'Timestamps and symbols', href: '#timestamps' },\n ],\n },\n { type: 'divider', title: 'Research pipeline (designed)' },\n {\n // No `href`: this group is a label, because its folder holds no index page.\n type: 'group',\n title: 'Research pipeline (designed)',\n items: [\n { type: 'page', title: 'Anatomy', href: '#anatomy' },\n { type: 'page', title: 'NSE portability', href: '#nse-portability' },\n ],\n },\n]\n\n/** The headings inside this page. Your own pipeline knows them; Prism does not. */\nconst TOC: DocsNavEntry[] = [\n { type: 'page', title: 'The capture path', href: '#the-capture-path' },\n { type: 'page', title: 'What the store holds', href: '#what-the-store-holds' },\n { type: 'page', title: 'Where this is heading', href: '#where-this-is-heading' },\n]\n\n/** A documentation screen: the rail, the document, the contents and the pager. */\nexport default function DocsShellDemo() {\n return (\n <DocsShell\n title=\"Storage\"\n description=\"Where a full chain of bars lands, and how it is partitioned.\"\n nav={NAV}\n toc={TOC}\n currentHref=\"#storage\"\n navLabel=\"AlphaLens documentation\"\n tocLabel=\"On this page\"\n pagerLabel=\"AlphaLens pages\"\n pagerLabels={{ previous: 'Previous', next: 'Next' }}\n >\n <p>\n The chain is written in columnar blocks, one per session per symbol, and\n nothing is rewritten to correct it. What lands is what the exchange sent.\n </p>\n <h2 id=\"the-capture-path\">The capture path</h2>\n <p>\n A capture is a session and a symbol, never a whole day. A day is a\n partition over sessions, which is what makes a backtest cheap to repeat\n and expensive to store twice.\n </p>\n <h2 id=\"what-the-store-holds\">What the store holds</h2>\n <ul>\n <li>Every strike, call and put, at the exchange clock.</li>\n <li>Open interest and the implied volatility of each strike.</li>\n <li>The index future and the spot it settled against.</li>\n </ul>\n <h2 id=\"where-this-is-heading\">Where this is heading</h2>\n <p>\n The same path, ported to .NET, is designed rather than built. The stage it\n is at is in its title, because a stage of a pipeline is not a topic a\n reader may visit in any order.\n </p>\n </DocsShell>\n )\n}\n```\n\n## Props\n\n**`title`** `string` · optional · default: — — The page's title, and the document's `h1`. It is optional because the three consumer sites do not agree about whether a documentation page has one: two render the frontmatter title as the page heading, and one lets the document's own Markdown own it. When it is absent the Page renders no heading at all rather than an empty one, so the document keeps exactly the outline its own content declares.\n**`description`** `ReactNode` · optional · default: — — One or two sentences under the title: what this part of the documentation covers. A `ReactNode` because a site that files a page for a pipeline states the pipeline's stage as markup rather than as a sentence.\n**`nav`** `readonly DocsNavEntry[]` · optional · default: — — The documentation tree, in the order a reader should meet it. The whole shape arrives here, so the Page holds no opinion about how many sections a site has, how deep they nest, or what a section is called.\n**`toc`** `readonly DocsNavEntry[]` · optional · default: — — The headings inside this page, in document order, for the contents rail. It is the consumer's document that knows its own outline, because Prism does not parse Markdown and a Page that guessed at it would guess wrong on every document rather than on some. Pass the whole outline or none: a partial one is a contents list that lies about the page.\n**`currentHref`** `string` · optional · default: — — The address of the page being rendered. The rail marks it with `aria-current=\"page\"` and the pager derives its two neighbours from it, so the same navigation renders a different pager for every page without the caller deriving anything.\n**`navLabel`** `string` · required · default: — — The accessible name of the rail. Required whenever `nav` is passed, because it is a word a reader hears and a Page that ships no copy ships no reader-facing copy either.\n**`tocLabel`** `string` · required · default: — — The accessible name of the contents rail. Required whenever `toc` is passed, for the same reason `navLabel` is.\n**`pagerLabel`** `string` · required · default: — — The accessible name of the pager. Required whenever there is a neighbour to reach, for the same reason `navLabel` is.\n**`pagerLabels`** `DocsPagerLabels` · required · default: — — The two words the pager renders above the neighbouring pages' titles. Required whenever either neighbour exists, for the same reason `navLabel` is.\n**`children`** `ReactNode` · required · default: — — The document's own body. Any flow content: paragraphs, headings, lists, code, images, tables. `Prose` holds it to the reading measure and to the block rhythm, and it is passed `fullWidth` so the document fills the column the frame gives it rather than sitting in a second measure inside one.\n**`header`** `ReactNode` · optional · default: — — A slot above the frame, inside the container: a breadcrumb trail, a version picker, a search field. A slot rather than a prop because each of those is a Block, and a Page that took one per prop would be a Page whose interface lists the whole catalogue.\n**`footer`** `ReactNode` · optional · default: — — A slot below the frame, inside the container: a feedback prompt, a link back to the section index, a support address.\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`** `DocsShell` — the public runtime names this Page promises\n**`source`** `packages/ui/src/pages/docs-shell/index.tsx` — the file this Page is defined in\n**`dependencies`** `Prose` — the public Prism exports this item composes\n",
1575
+ "props": "## Props\n\n**`title`** `string` · optional · default: — — The page's title, and the document's `h1`. It is optional because the three consumer sites do not agree about whether a documentation page has one: two render the frontmatter title as the page heading, and one lets the document's own Markdown own it. When it is absent the Page renders no heading at all rather than an empty one, so the document keeps exactly the outline its own content declares.\n**`description`** `ReactNode` · optional · default: — — One or two sentences under the title: what this part of the documentation covers. A `ReactNode` because a site that files a page for a pipeline states the pipeline's stage as markup rather than as a sentence.\n**`nav`** `readonly DocsNavEntry[]` · optional · default: — — The documentation tree, in the order a reader should meet it. The whole shape arrives here, so the Page holds no opinion about how many sections a site has, how deep they nest, or what a section is called.\n**`toc`** `readonly DocsNavEntry[]` · optional · default: — — The headings inside this page, in document order, for the contents rail. It is the consumer's document that knows its own outline, because Prism does not parse Markdown and a Page that guessed at it would guess wrong on every document rather than on some. Pass the whole outline or none: a partial one is a contents list that lies about the page.\n**`currentHref`** `string` · optional · default: — — The address of the page being rendered. The rail marks it with `aria-current=\"page\"` and the pager derives its two neighbours from it, so the same navigation renders a different pager for every page without the caller deriving anything.\n**`navLabel`** `string` · required · default: — — The accessible name of the rail. Required whenever `nav` is passed, because it is a word a reader hears and a Page that ships no copy ships no reader-facing copy either.\n**`tocLabel`** `string` · required · default: — — The accessible name of the contents rail. Required whenever `toc` is passed, for the same reason `navLabel` is.\n**`pagerLabel`** `string` · required · default: — — The accessible name of the pager. Required whenever there is a neighbour to reach, for the same reason `navLabel` is.\n**`pagerLabels`** `DocsPagerLabels` · required · default: — — The two words the pager renders above the neighbouring pages' titles. Required whenever either neighbour exists, for the same reason `navLabel` is.\n**`children`** `ReactNode` · required · default: — — The document's own body. Any flow content: paragraphs, headings, lists, code, images, tables. `Prose` holds it to the reading measure and to the block rhythm, and it is passed `fullWidth` so the document fills the column the frame gives it rather than sitting in a second measure inside one.\n**`header`** `ReactNode` · optional · default: — — A slot above the frame, inside the container: a breadcrumb trail, a version picker, a search field. A slot rather than a prop because each of those is a Block, and a Page that took one per prop would be a Page whose interface lists the whole catalogue.\n**`footer`** `ReactNode` · optional · default: — — A slot below the frame, inside the container: a feedback prompt, a link back to the section index, a support address.\n**`className`** `string` · optional · default: — — Layout only, exactly as on every Component. Changing a Prism-owned visual property from here is prohibited.",
1576
+ "composition": "## Composition\n\n**`exports`** `DocsShell` — the public runtime names this Page promises\n**`source`** `packages/ui/src/pages/docs-shell/index.tsx` — the file this Page is defined in\n**`dependencies`** `Prose` — the public Prism exports this item composes",
1577
+ "example": {
1578
+ "title": "A documentation screen: the rail, the document, the contents and the pager.",
1579
+ "code": "import { DocsShell, type DocsNavEntry } from '@nanisoft/prism-ui/pages/docs-shell'\n\n/**\n * The shape the three consumer sites actually publish, taken from AlphaLens\n * because it is the one that exercises every part of the interface at once: a\n * status inside a section title, a rule between sections, and a section whose\n * own index is a page a reader lands on.\n */\nconst NAV: DocsNavEntry[] = [\n { type: 'page', title: 'AlphaLens', href: '#alphalens' },\n { type: 'divider', title: 'Data platform (live)' },\n {\n type: 'group',\n title: 'Data platform (live)',\n href: '#data-platform',\n items: [\n { type: 'page', title: 'Capture', href: '#capture' },\n { type: 'page', title: 'Storage', href: '#storage' },\n { type: 'page', title: 'Operations', href: '#platform-operations' },\n ],\n },\n { type: 'divider', title: 'Unified data contract (approved)' },\n {\n type: 'group',\n title: 'Unified data contract (approved)',\n href: '#data-contract',\n items: [\n { type: 'page', title: 'Data feed view', href: '#feed-view' },\n { type: 'page', title: 'Timestamps and symbols', href: '#timestamps' },\n ],\n },\n { type: 'divider', title: 'Research pipeline (designed)' },\n {\n // No `href`: this group is a label, because its folder holds no index page.\n type: 'group',\n title: 'Research pipeline (designed)',\n items: [\n { type: 'page', title: 'Anatomy', href: '#anatomy' },\n { type: 'page', title: 'NSE portability', href: '#nse-portability' },\n ],\n },\n]\n\n/** The headings inside this page. Your own pipeline knows them; Prism does not. */\nconst TOC: DocsNavEntry[] = [\n { type: 'page', title: 'The capture path', href: '#the-capture-path' },\n { type: 'page', title: 'What the store holds', href: '#what-the-store-holds' },\n { type: 'page', title: 'Where this is heading', href: '#where-this-is-heading' },\n]\n\n/** A documentation screen: the rail, the document, the contents and the pager. */\nexport default function DocsShellDemo() {\n return (\n <DocsShell\n title=\"Storage\"\n description=\"Where a full chain of bars lands, and how it is partitioned.\"\n nav={NAV}\n toc={TOC}\n currentHref=\"#storage\"\n navLabel=\"AlphaLens documentation\"\n tocLabel=\"On this page\"\n pagerLabel=\"AlphaLens pages\"\n pagerLabels={{ previous: 'Previous', next: 'Next' }}\n >\n <p>\n The chain is written in columnar blocks, one per session per symbol, and\n nothing is rewritten to correct it. What lands is what the exchange sent.\n </p>\n <h2 id=\"the-capture-path\">The capture path</h2>\n <p>\n A capture is a session and a symbol, never a whole day. A day is a\n partition over sessions, which is what makes a backtest cheap to repeat\n and expensive to store twice.\n </p>\n <h2 id=\"what-the-store-holds\">What the store holds</h2>\n <ul>\n <li>Every strike, call and put, at the exchange clock.</li>\n <li>Open interest and the implied volatility of each strike.</li>\n <li>The index future and the spot it settled against.</li>\n </ul>\n <h2 id=\"where-this-is-heading\">Where this is heading</h2>\n <p>\n The same path, ported to .NET, is designed rather than built. The stage it\n is at is in its title, because a stage of a pipeline is not a topic a\n reader may visit in any order.\n </p>\n </DocsShell>\n )\n}\n"
1580
+ },
1581
+ "references": {
1582
+ "blocks": [],
1583
+ "pages": []
1584
+ }
1585
+ },
1559
1586
  {
1560
1587
  "id": "marketing-page",
1561
1588
  "slug": "marketing-page",
@@ -1854,9 +1881,9 @@
1854
1881
  "slug": "prism-llms",
1855
1882
  "section": "changelogs",
1856
1883
  "title": "@nanisoft/prism-llms",
1857
- "description": "0.5.0, 0.2.0, 0.1.0",
1884
+ "description": "0.5.1, 0.5.0, 0.2.0, 0.1.0",
1858
1885
  "url": "/changelogs/prism-llms",
1859
- "markdown": "# @nanisoft/prism-llms\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",
1886
+ "markdown": "# @nanisoft/prism-llms\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",
1860
1887
  "mirror": "/changelogs/prism-llms.md"
1861
1888
  },
1862
1889
  {
@@ -1864,9 +1891,9 @@
1864
1891
  "slug": "prism-mcp-server",
1865
1892
  "section": "changelogs",
1866
1893
  "title": "@nanisoft/prism-mcp-server",
1867
- "description": "0.4.0, 0.2.0, 0.1.0",
1894
+ "description": "0.4.1, 0.4.0, 0.2.0, 0.1.0",
1868
1895
  "url": "/changelogs/prism-mcp-server",
1869
- "markdown": "# @nanisoft/prism-mcp-server\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",
1896
+ "markdown": "# @nanisoft/prism-mcp-server\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",
1870
1897
  "mirror": "/changelogs/prism-mcp-server.md"
1871
1898
  },
1872
1899
  {
@@ -1884,9 +1911,9 @@
1884
1911
  "slug": "prism-ui",
1885
1912
  "section": "changelogs",
1886
1913
  "title": "@nanisoft/prism-ui",
1887
- "description": "0.6.0, 0.5.1, 0.5.0",
1914
+ "description": "0.7.0, 0.6.0, 0.5.1, 0.5.0",
1888
1915
  "url": "/changelogs/prism-ui",
1889
- "markdown": "# @nanisoft/prism-ui\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",
1916
+ "markdown": "# @nanisoft/prism-ui\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",
1890
1917
  "mirror": "/changelogs/prism-ui.md"
1891
1918
  }
1892
1919
  ],
@@ -1897,11 +1924,16 @@
1897
1924
  "route": "/changelogs/prism-llms",
1898
1925
  "title": "@nanisoft/prism-llms",
1899
1926
  "versions": [
1927
+ "0.5.1",
1900
1928
  "0.5.0",
1901
1929
  "0.2.0",
1902
1930
  "0.1.0"
1903
1931
  ],
1904
1932
  "releases": [
1933
+ {
1934
+ "version": "0.5.1",
1935
+ "body": "### 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."
1936
+ },
1905
1937
  {
1906
1938
  "version": "0.5.0",
1907
1939
  "body": "### 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."
@@ -1915,7 +1947,7 @@
1915
1947
  "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."
1916
1948
  }
1917
1949
  ],
1918
- "text": "# @nanisoft/prism-llms\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"
1950
+ "text": "# @nanisoft/prism-llms\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"
1919
1951
  },
1920
1952
  {
1921
1953
  "package": "@nanisoft/prism-mcp-server",
@@ -1923,11 +1955,16 @@
1923
1955
  "route": "/changelogs/prism-mcp-server",
1924
1956
  "title": "@nanisoft/prism-mcp-server",
1925
1957
  "versions": [
1958
+ "0.4.1",
1926
1959
  "0.4.0",
1927
1960
  "0.2.0",
1928
1961
  "0.1.0"
1929
1962
  ],
1930
1963
  "releases": [
1964
+ {
1965
+ "version": "0.4.1",
1966
+ "body": "### Patch Changes\n\n- Updated dependencies [5c43520]\n - @nanisoft/prism-llms@0.5.1"
1967
+ },
1931
1968
  {
1932
1969
  "version": "0.4.0",
1933
1970
  "body": "### Patch Changes\n\n- Updated dependencies [4832359]\n- Updated dependencies [76c8d96]\n - @nanisoft/prism-llms@0.3.0"
@@ -1941,7 +1978,7 @@
1941
1978
  "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."
1942
1979
  }
1943
1980
  ],
1944
- "text": "# @nanisoft/prism-mcp-server\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"
1981
+ "text": "# @nanisoft/prism-mcp-server\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"
1945
1982
  },
1946
1983
  {
1947
1984
  "package": "@nanisoft/prism-tokens",
@@ -1975,11 +2012,16 @@
1975
2012
  "route": "/changelogs/prism-ui",
1976
2013
  "title": "@nanisoft/prism-ui",
1977
2014
  "versions": [
2015
+ "0.7.0",
1978
2016
  "0.6.0",
1979
2017
  "0.5.1",
1980
2018
  "0.5.0"
1981
2019
  ],
1982
2020
  "releases": [
2021
+ {
2022
+ "version": "0.7.0",
2023
+ "body": "### 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."
2024
+ },
1983
2025
  {
1984
2026
  "version": "0.6.0",
1985
2027
  "body": "### 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"
@@ -1993,7 +2035,7 @@
1993
2035
  "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."
1994
2036
  }
1995
2037
  ],
1996
- "text": "# @nanisoft/prism-ui\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"
2038
+ "text": "# @nanisoft/prism-ui\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"
1997
2039
  }
1998
2040
  ],
1999
2041
  "tokens": {