zz-meridian 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/adopt.js +1 -1
  2. package/package.json +1 -1
  3. package/payload/.env.example +2 -0
  4. package/payload/CHANGELOG.md +104 -1
  5. package/payload/CONTRIBUTING.md +1 -1
  6. package/payload/README.md +1 -1
  7. package/payload/app/(dashboard)/README.md +2 -2
  8. package/payload/app/(dashboard)/error.tsx +10 -2
  9. package/payload/app/(dashboard)/keys/actions.ts +2 -1
  10. package/payload/app/not-found/README.md +4 -4
  11. package/payload/app/system/preview/[section]/[card]/page.tsx +1 -1
  12. package/payload/decisions/0001-react-and-the-repository.md +1 -1
  13. package/payload/decisions/{0002-zandro-register.md → 0002-the-register.md} +3 -3
  14. package/payload/decisions/0006-quiet-light.md +1 -1
  15. package/payload/decisions/0007-the-name.md +1 -1
  16. package/payload/decisions/0009-a-package-that-copies.md +2 -2
  17. package/payload/docs/assistant.md +1 -1
  18. package/payload/docs/benchmark.md +6 -6
  19. package/payload/docs/distribution.md +18 -16
  20. package/payload/docs/surfaces.md +10 -5
  21. package/payload/next.config.ts +1 -1
  22. package/payload/package.json +1 -1
  23. package/payload/scripts/assistant.ts +26 -3
  24. package/payload/scripts/brand.ts +0 -1
  25. package/payload/scripts/check.ts +71 -1
  26. package/payload/scripts/contrast.ts +4 -4
  27. package/payload/scripts/fake-llm.ts +16 -2
  28. package/payload/scripts/keyboard.ts +31 -4
  29. package/payload/scripts/registry.ts +3 -3
  30. package/payload/scripts/tokens.ts +5 -5
  31. package/payload/scripts/verify.config.ts +14 -1
  32. package/payload/scripts/verify.ts +75 -3
  33. package/payload/skills/zz-meridian/references/customize.md +13 -0
  34. package/payload/skills/zz-meridian/references/existing-project.md +17 -0
  35. package/payload/skills/zz-meridian/references/validation.md +4 -0
  36. package/payload/src/components/base/app-mark/preview.tsx +4 -1
  37. package/payload/src/components/base/motion/preview.tsx +4 -1
  38. package/payload/src/components/base/providers.tsx +19 -7
  39. package/payload/src/components/base/shell/README.md +2 -1
  40. package/payload/src/components/base/shell/index.tsx +14 -4
  41. package/payload/src/components/base/surface/index.tsx +1 -1
  42. package/payload/src/components/charts/sparkline/index.tsx +3 -0
  43. package/payload/src/components/charts/timeline/README.md +3 -1
  44. package/payload/src/components/charts/timeline/index.tsx +9 -1
  45. package/payload/src/components/patterns/appearance-menu/index.tsx +1 -1
  46. package/payload/src/components/patterns/ask-about/index.tsx +1 -1
  47. package/payload/src/components/patterns/assistant/README.md +9 -2
  48. package/payload/src/components/patterns/assistant/index.tsx +23 -20
  49. package/payload/src/components/patterns/assistant/launcher.tsx +28 -0
  50. package/payload/src/components/patterns/assistant/preview.tsx +4 -2
  51. package/payload/src/components/patterns/assistant/text.tsx +25 -0
  52. package/payload/src/components/patterns/command-palette/README.md +1 -1
  53. package/payload/src/components/patterns/data-table/README.md +6 -5
  54. package/payload/src/components/patterns/data-table/index.tsx +92 -84
  55. package/payload/src/components/patterns/export-button/README.md +1 -1
  56. package/payload/src/components/patterns/featured-metric/README.md +1 -1
  57. package/payload/src/components/patterns/featured-metric/index.tsx +8 -15
  58. package/payload/src/components/patterns/filter-bar/README.md +3 -3
  59. package/payload/src/components/patterns/filter-bar/index.tsx +12 -8
  60. package/payload/src/components/patterns/form-section/README.md +5 -1
  61. package/payload/src/components/patterns/form-section/index.tsx +71 -36
  62. package/payload/src/components/patterns/form-section/preview.tsx +17 -0
  63. package/payload/src/components/patterns/metric-tile/index.tsx +2 -9
  64. package/payload/src/components/patterns/period-select/index.tsx +4 -3
  65. package/payload/src/components/patterns/rail/README.md +2 -0
  66. package/payload/src/components/patterns/shell-tools/index.tsx +1 -1
  67. package/payload/src/components/ui/banner/README.md +1 -1
  68. package/payload/src/components/ui/button/README.md +5 -5
  69. package/payload/src/components/ui/button/preview.tsx +1 -1
  70. package/payload/src/components/ui/card/README.md +4 -4
  71. package/payload/src/components/ui/card/index.tsx +29 -3
  72. package/payload/src/components/ui/card/preview.tsx +25 -0
  73. package/payload/src/components/ui/dialog/README.md +1 -1
  74. package/payload/src/components/ui/icon-button/README.md +3 -3
  75. package/payload/src/components/ui/input/README.md +4 -4
  76. package/payload/src/components/ui/input/preview.tsx +1 -1
  77. package/payload/src/components/ui/menu/README.md +2 -2
  78. package/payload/src/components/ui/pagination/README.md +1 -1
  79. package/payload/src/components/ui/popover/README.md +2 -2
  80. package/payload/src/components/ui/popover/index.tsx +0 -1
  81. package/payload/src/components/ui/segmented/README.md +3 -3
  82. package/payload/src/components/ui/segmented/index.tsx +4 -1
  83. package/payload/src/components/ui/select/README.md +1 -1
  84. package/payload/src/components/ui/sheet/README.md +1 -1
  85. package/payload/src/components/ui/sheet/index.tsx +1 -1
  86. package/payload/src/components/ui/skeleton/README.md +1 -1
  87. package/payload/src/components/ui/table/README.md +2 -2
  88. package/payload/src/components/ui/table/index.tsx +2 -2
  89. package/payload/src/components/ui/textarea/README.md +1 -1
  90. package/payload/src/components/ui/toast/README.md +1 -1
  91. package/payload/src/components/ui/tooltip/README.md +1 -1
  92. package/payload/src/data/collections.ts +11 -0
  93. package/payload/src/lib/collection.ts +29 -5
  94. package/payload/src/lib/format.ts +22 -4
  95. package/payload/src/lib/host.ts +10 -0
  96. package/payload/src/lib/period.ts +11 -0
  97. package/payload/src/lib/safe-markdown.ts +9 -1
  98. package/payload/src/styles/motion.css +0 -5
  99. package/payload/src/styles/tokens.css +0 -1
  100. package/payload/src/system/content.ts +7 -6
  101. package/payload/src/system/fixtures/sample-records.ts +1 -1
  102. package/payload/src/system/fixtures/sample.ts +1 -2
  103. package/payload/src/system/markdown.tsx +1 -1
  104. package/payload/src/system/page-stage.tsx +1 -1
  105. package/payload/src/system/registry.ts +1 -1
  106. package/payload/src/system/token-view.tsx +1 -1
  107. package/payload/src/system/tokens-data.ts +1 -1
  108. package/payload/src/views/not-found-address.tsx +4 -4
  109. package/payload/src/views/not-found.tsx +10 -2
  110. package/payload/tests/agents.test.tsx +29 -0
  111. package/payload/tests/assistant-text.test.tsx +40 -0
  112. package/payload/tests/collection.test.ts +19 -0
  113. package/payload/tests/data-table.test.tsx +48 -0
  114. package/payload/tests/format.test.ts +19 -1
  115. package/payload/tests/preferences.test.tsx +41 -0
  116. package/payload/tests/prose.test.tsx +11 -0
  117. package/payload/tests/sparkline.test.tsx +30 -0
  118. package/payload/tokens/core.tokens.json +0 -8
package/dist/adopt.js CHANGED
@@ -12,7 +12,7 @@ import { PAYLOAD, VERSION, brandArgs, inside, installSkill, packageManager, payl
12
12
  /** The library modules Meridian's components and gates import; the rest of src/lib belongs to the template's pages. */
13
13
  const LIB = ['cn', 'format', 'format-date', 'period', 'color', 'host', 'preferences', 'csv', 'safe-markdown'].map((n) => `src/lib/${n}.ts`);
14
14
  /** What adopt copies from the template, as payload paths. The fixture build in CI is what keeps this list complete. */
15
- export function adoptSet() {
15
+ function adoptSet() {
16
16
  const own = (f) => !/(^|\/)(README\.md|preview\.tsx)$/.test(f);
17
17
  return [
18
18
  ...payloadFiles('tokens'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Bring ZZ Meridian, a dashboard design system, into a Next.js project, or start a new dashboard on it. Copies the files in; nothing depends on this package at runtime.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,6 +8,8 @@
8
8
  # ASSISTANT_API_KEY=
9
9
 
10
10
  # The model's id, as the provider names it. It must be able to call tools.
11
+ # A gateway (LiteLLM, OpenRouter and the like) often names models "<provider>.<model>": copy the id
12
+ # from the gateway's GET /models, and take it exactly as it comes — the prefix is part of the name.
11
13
  # ASSISTANT_MODEL=
12
14
 
13
15
  # The provider's address. Required for "openai-compatible"; optional for "anthropic".
@@ -2,6 +2,109 @@
2
2
 
3
3
  Every release of ZZ Meridian, newest first. Versions follow semver: a removed or renamed token, prop or card is major; a new card, token or variant is minor; a corrected value is a patch. Each entry says what breaks and what to do instead.
4
4
 
5
+ ## [0.4.0] · 2026-10-05
6
+
7
+ A quality pass over the whole repository, and a manual pass through the running product — every page, embed, Atlas
8
+ view, CLI command and API route, at both widths and both themes. Twenty-two defects fixed, six of them found only by
9
+ auditing the Atlas, which nothing had done before. Both field reports on #6 and #7 are in.
10
+
11
+ ### Security
12
+
13
+ - **The markdown URL policy could be bypassed with a control character** (`src/lib/safe-markdown.ts`). The scheme was read from the raw string, but a browser removes tab, newline and the other controls from a URL before it reads one: `java\tscript:alert(1)` IS `javascript:` to the browser, so a link an agent, a document or a note wrote could reach a script — the one thing this module exists to stop. The scheme is now read from a copy with the controls and spaces taken out, and only the probe is stripped: a URL that passes is returned exactly as written. `tests/prose.test.tsx` covers the obfuscated forms.
14
+
15
+ ### Fixed
16
+
17
+ - **`scripts/keyboard.ts` could fail a page that was right.** It judged a focused control two frames after the key event, and the skip link is drawn off the top edge until `:focus-visible` moves it in — so a measurement taken in between clamped to the page's corner, found the sticky bar there, and reported `hidden under div.flex.h-16: Skip to content`. That failed the `gates` job of the 0.4.0 release, on `/keys`, while the same page passed here and in the dry run: a race, not a covered control. It now takes a second look 150 ms later before calling a control covered, which a control genuinely under something cannot pass.
18
+ - **`scripts/keyboard.ts` failed a page that was right.** Its control selector asked for `button:not([disabled])`, `input:not([disabled])` and so on — the *attribute*, which only a control's own markup carries — so a control disabled by a `<fieldset disabled>` was counted as one the keyboard should reach, and Tab (which correctly skips it) never arrived. A `FormSection` in its read-only or saving state is exactly that, so its Atlas page and its preview both failed with "never reached: INPUT" — a check crying wolf on the component it ships. The selector asks for the state now (`:disabled`), which is what the audit's own target check already did. Both routes pass; reverting the selector fails them again.
19
+ - **The assistant's whole panel was in every page's first load** (issue #7). `AppShell` loaded `AssistantColumn` *and* `AssistantLauncher` through `dynamic(() => import('@/components/patterns/assistant'))` — the same barrel — so the chunk a page fetched to draw its launcher button carried `useChat`, the AI SDK's client and its zod schemas, `react-markdown`, `remark-gfm` and `micromark`: 448 KB in the reporting product's build, on every page, whether or not anybody opened the panel. The launcher is its own module now (`assistant/launcher.tsx`, which imports only the Agent Mark), and the column mounts the first time the panel is opened and stays mounted after, so the thread still survives closing and reopening. The measurement, from the report: 1,152-1,219 KB of JS per page before, 696-761 KB after, with 456 KB arriving on the first open.
20
+ - **`verify`'s "assistant off" start could be turned back on by `.env.local`** (issue #7). It deleted every `ASSISTANT_*` variable from the environment it passed on, but `next start` loads `.env.local` itself and Next does not overwrite a variable that is already set — so a person keeping their own model there got "/ has 1 assistant element(s)" while nothing was wrong. The variables are blanked instead, which is what `assistantConfig` reads as unset.
21
+ - **Nothing checked what a page actually downloads.** `vitals.ts` measures LCP, INP and CLS, and all three stayed green while the panel's chunk rode along on every page. `scripts/assistant.ts` measures the split directly: it lists the page's scripts with the panel closed, opens the panel, and fails when opening it fetched no script the closed page lacked — which is exactly what "the launcher carries the panel" looks like from the browser.
22
+ - **The guides said nothing about reading a table once per request** (issue #7). A console page asks for the same records from several places — the shell tools, a page strip, the page and its freshness stamp — which is free over the sample's fixtures and a round trip each over a network. `src/data/collections.ts`'s header, `references/customize.md` and `references/existing-project.md` now say to wrap the read in `cache()` from `react`, to keep the write path reading directly so validation never sees an earlier answer, and to raise `pg`'s ten-second idle timeout. The reporting product's three heaviest pages answered 44-46% sooner for the first.
23
+ - **A DataTable drew both of its layouts on every render** (issue #6). The component rendered the table *and* a `<ul>` of the same records, and let CSS hide one: every cell function ran twice on mount and again on every filter, sort or page change, both trees reached the DOM at every viewport, and each device downloaded a tree it could never show — on the reporting product's `/activity` that was 201 of 711 elements hidden and unusable. It draws one tree now: the real `<table>` at every width, its rows laid out as cards below 768px in CSS. Each cell carries `data-mobile` — `check`, `title`, `status`, `fact` or `hidden` — naming its part of the card, and the row becomes a six-column grid. Nothing is measured, so there is no hydration swap and no flash: a phone gets the card in the server's HTML exactly as before. A column the table dropped for width (`hideBelow`) comes back in the card, which is what the old list did. The only render left over is the phone wording for a column that declares `mobileCell` ("Used 1 min ago" beside "1 min ago") — a few words, never a second copy of the row. `tests/data-table.test.tsx` holds it.
24
+ - **Two tabs of the same dashboard disagreed about the theme, accent and density** (`Providers`). The stored choice was read once on mount and never again, so a person who switched to the light theme in one tab kept the dark one in the other until it reloaded. `Providers` now follows the `storage` event, which fires only in the tabs that did not write — exactly the ones that need it. `tests/preferences.test.tsx` holds it (and fails without the listener).
25
+ - **`Sparkline` drew a stray filled triangle for fewer than two values.** `Math.min()`/`Math.max()` over an empty array give ±Infinity, and the area path closed with them; the README promised "under two values, render nothing". It renders nothing now, and `tests/sparkline.test.tsx` holds it — with the guard removed it fails on exactly the degenerate path (`d="L120,36L0,36Z"`), which a real ResizeObserver would have drawn.
26
+ - **`formatCompact` rendered a negative in full** — `formatCompact(-1_500_000)` was "-1,500,000" in a tile while an axis rendered the same number as "-1.5M". Every branch turns on the magnitude now, as the axis formatter already did.
27
+ - **`FeaturedMetric` crashed on a figure its splitter did not recognise.** `text.match(...)` was dereferenced without a null test, so a formatter returning "—", a negative, or any currency symbol other than `$`/`€`/`£` threw. Both it and `MetricTile` now share one total parser, `splitFigure` in `src/lib/format.ts`, which never fails to match.
28
+ - **The MCP Apps host bridge never removed its `window` listener.** `HostBridge.dispose()` releases it, and `EmbedSurface` calls it on unmount — a bridge that is never disposed keeps its listener, and everything it closes over, alive for the life of the page. `tests/agents.test.tsx` holds it twice over: that a disposed bridge delivers nothing to its listeners, and that the handler it added to `window` is the one it takes off (the second is what catches the leak — the first passes on `listeners.clear()` alone).
29
+ - **`scripts/brand.ts` silently did nothing for part of a new accent.** It patched an `ACCENT_SWATCH` map that no longer exists in `src/lib/preferences.ts`; a `String.replace` with no match writes nothing. Removed.
30
+ - **`scripts/keyboard.ts` ignored `--extra`.** `pnpm verify --extra /orders/1` walked the configured detail pages here while the audit, the presses and vitals walked the ones that were asked for.
31
+ - **`scripts/verify.ts` looked for the assistant only in `app/`.** A project that keeps its routes under `src/app` — which `adopt` supports, and which `APP_DIR` already models — would have had its whole assistant walk-through skipped, silently.
32
+ - **`scripts/fake-llm.ts` used the older entry-point guard** while every other script uses `import.meta.main`.
33
+ - **`app/(dashboard)/keys/actions.ts` stamped every key's owner as "Maya Chen"** instead of the product's own person (`app.user.name`).
34
+ - **The Atlas did not list the Members page.** `/system/pages/members` did not exist, so the page's own specification was unreachable from the Atlas; `docs/surfaces.md`'s page inventory omitted Members and API keys.
35
+ - **`next.config.ts` did not trace `app/**/*.md` for `/system`.** The Atlas reads its page specifications from `app/`, and today those routes are static — a runtime render would have read them as empty.
36
+ - **`docs/surfaces.md`'s page inventory named two embed views that do not exist** (`request`, `customer`) and left out the one that does (`/embed/proposal`). Every page's own specification already said "not offered" for those two; the table agrees with them now and lists the proposal view. The same guide credited the token bridge to `EmbedFrame`; `EmbedSurface` is what applies it, on every embed route. `check.ts` has a rule now, so this cannot drift again: every `/embed/<name>` a document writes is a route, and every view the inventory's own column offers is one.
37
+ - **Twenty-four sizes written into the specifications were the tokens' old values.** The radius scale was retuned (4/6/8/12/16 to 5/8/10/16/24), the control heights with it (30/36/44 to 32/38/46) and the row height (36/48 to 38/52), and eighteen specifications kept the old numbers: Banner, Button, Card, Dialog, Icon button, Input, Menu, Pagination, Popover, Segmented, Select, Skeleton, Table, Textarea, Toast, Tooltip, Export button and `docs/surfaces.md`. A product sizing a corner, a control or a row from the specs — the card contract is what CONTRIBUTING points a reader to — was a few pixels out, and nothing said so. `check.ts` has a rule now: a token annotated with a px value must be that value, comfortable or compact, and a token whose own value is not a plain px (a clamp, a var) is left alone.
38
+ - **Four controls were under 44px on a phone**, which the standard requires of every control on a coarse pointer (`.hit` gives a control a 44px target; its siblings already carried it). The Appearance menu's trigger (`size-8`), the alerts panel's "Mark all read" and the filter bar's phone-only "Clear" — text buttons, which the box-link rule never covered — and `AskAbout`'s button (`h-7`). Nothing caught them because the audit only measures a control when it is on screen, and on a product page these live in a closed drawer, popover or sheet: the components' own previews, which nothing audited, are where they showed.
39
+ - **The App Mark preview drew its light specimen as light text on a light ground** (1.05:1 — the audit fails that contrast). `data-theme` redefines the variables in its own scope, but `color` had already been resolved on `body` from the page's theme; the Planes preview carries `text-ink` for exactly this reason and the App Mark preview did not.
40
+ - **The Motion preview demonstrated `.link` as a box control.** `.link` is a link inside a sentence — that is why it is exempt from the 44px rule — and shown bare in a flex row it blockifies, losing both the exemption and the box-link hit area, so the specimen stood for a control the class is not. A box link is `.link inline-flex`, as the Atlas's own links are.
41
+
42
+ ### Changed
43
+
44
+ - **One parser splits both figures.** `FeaturedMetric` had its own regex with a hard-coded `$€£`; it and `MetricTile` share `splitFigure` now, which is total and takes any leading symbol.
45
+ - **The card's interactive hover is the shadow its own spec, preview and token catalogue name** (`shadow-raise`, "an interactive card under the pointer") — the code had used `shadow-halo`, which belongs to the featured card.
46
+ - **The button's hover documentation matches the button**: a 5% brightness step over `dur-hover`, because the fill is a gradient image that a colour change cannot show through. The README and the preview said `accent-hover`.
47
+ - **The Sheet's close fade and the token catalogue's growing bar use the duration tokens they had written as literals.** The sheet's leave faded over a hard-coded `160ms` — the *hover* duration — while its own scrim and every other overlay going away use `--dur-exit`; it does now too, so a sheet and the scrim under it finish together. `token-view.tsx`'s bar grew over `900ms`; it uses `--dur-grow`. `docs/surfaces.md`'s token bridge lists `surface-raised` and `radius-md`, which the bridge has always mapped.
48
+
49
+ ### Removed
50
+
51
+ - **`.sheet-right-in`, `.sheet-up-in` and the `m-sheet-right` keyframe** from `motion.css`: nothing referenced them, and the Sheet animates inline (its README claimed motion.css had no right-edge keyframe; it did).
52
+ - **`PopoverAnchor`**, exported and named nowhere (the preview uses `PopoverClose`, which stays; the README now names it).
53
+ - **Dormant exports**: `ROOT`/`Token`/`BRIDGE`/`buildCss`/`buildTheme` in `scripts/tokens.ts`, `PAIRS`/`context`/`resolve`/`colorOf` in `scripts/contrast.ts`, `LAYERS`/`CardEntry` in `scripts/registry.ts`, `VerifyConfig` in `scripts/verify.config.ts`, `adoptSet` in `cli/src/adopt.ts`, and the Atlas's internal types (`TOKEN_VIEWS`, `DOCS`, `parseSpec`, `SectionId`, `Entry`, `slug`, `HostSimulator`, `Card`, `TokenMeta`).
54
+ - **The `rail-collapsed` token**: declared since the first commit, referenced by no component, spec, bridge or script.
55
+ - **`DEMO_STALE_AFTER_MS`** (and the unused `Customer`/`StatusClass` type exports): nothing imported them.
56
+
57
+ ### Breaking
58
+
59
+ - **The `rail-collapsed` token is gone** (`tokens/core.tokens.json`, and `src/styles/tokens.css` with it). Nothing a
60
+ product keeps referenced it — no component, specification, bridge or script — so a stylesheet of your own that reads
61
+ `var(--rail-collapsed)` was already falling back to nothing. The rail's width is `rail-width`; below 1024px it is a
62
+ drawer, which is not a collapsed rail.
63
+ - **Dormant exports removed from Meridian's own scripts**: `ROOT`, `Token`, `BRIDGE`, `buildCss` and `buildTheme` in
64
+ `scripts/tokens.ts`; `PAIRS`, `context`, `resolve` and `colorOf` in `scripts/contrast.ts`; `LAYERS` and `CardEntry` in
65
+ `scripts/registry.ts`; `VerifyConfig` in `scripts/verify.config.ts`; `adoptSet` in `cli/src/adopt.ts`; and the Atlas's
66
+ internal types. A product that imported one of these was reaching into a generator; nothing that ships depends on
67
+ them.
68
+ - **`PopoverAnchor` is gone**, and `.sheet-right-in`/`.sheet-up-in` with the `m-sheet-right` keyframe. The first was
69
+ exported and named nowhere; the others were referenced by nothing, and the Sheet animates inline.
70
+ - **`DEMO_STALE_AFTER_MS`** and the unused `Customer`/`StatusClass` type exports are gone from the sample fixtures.
71
+
72
+ Nothing a product renders changes shape because of any of this: `pnpm gate` proves that every export of `src/lib` and
73
+ `src/data` is imported by a file a product keeps, and it passes.
74
+
75
+ ## [0.3.0] · 2026-10-04
76
+
77
+ Three field reports from products built on Meridian (issues #3, #4 and #5), taken as proposed where the proposal held and differently where it did not. Everything here is a fix to what the template ships, or a hole an adopting product could not fill itself.
78
+
79
+ ### Added
80
+
81
+ - **The assistant renders a reply as markdown.** A real model answers in markdown, and the panel was showing it literally — `**High risk:**`, `- ` bullets and `|---|` table rules on screen. Each text part goes through `Prose` at `sm` (`src/components/patterns/assistant/text.tsx`), the same reader the rest of the product uses, so raw HTML stays text and every URL passes `safeMarkdownUrl`. The reply block carries `data-assistant-text`, a stable handle for a check; the person's own message is still plain text, as typed. `scripts/fake-llm.ts` gained one scripted markdown reply and `scripts/assistant.ts` asserts the list, the bold and the table are elements with the markup characters gone.
82
+ - **`CardHeader` `wrap`**, for a title that is the point of the card — an objective, a record's name — rather than a label in a list where one line and an ellipsis is right.
83
+ - **`FormSection as="div"` and `flush`.** A settings page that holds a table had nowhere to put it: `FormSection` always wrapped its card in a `<form>` with a save bar, and its body was a padded `fieldset`, so it could hold neither a table that runs edge to edge nor a form of its own (forms cannot nest). `as="div"` is the same head and card with no `<form>`, and `flush` drops the body's padding for the table. `SettingRow` sits outside `FormSection` unchanged, for a switch that applies at once.
84
+ - **Collections: `derived`, and the write shapes a real data layer can honour.** `create` and `update` took `Omit<T, K>`, which demands every field of the record — including ones a write never takes (a worked-out score, a band, joined data) — so a database-backed product had to cast around the type. They now take a plain record validated by `fields`, which is what the store always did at runtime. `derived` names the read-only fields: a page reads them, the assistant may filter on them, and `patchOf` keeps them out of every change.
85
+ - **`verify` refuses to run against a data URL that is not on this machine.** It presses every control, Delete included, and an adopted app's `DATABASE_URL` comes from `.env` — so on a first outage, or any day, those presses landed on whatever that URL names. Name extra variables in `dataUrls`, and set `allowRemoteData: true` only when you know what the presses reach. verify also prints an estimate and each phase as it goes, and names `--quick` and `--no-vitals` up front.
86
+ - **`PERIOD_SHORT`** in `src/lib/period.ts`, beside `PERIOD_LABEL`: a product that adds its own period (a 24-hour one) edits that one file, and `PeriodSelect` follows — it no longer carries a `Record<Period, string>` of its own that fails to type check the moment the vocabulary moves.
87
+ - **A timeline bar that continues past a fixed window is squared off** on the side that continues. Work that started before `from` or runs past `to` used to read as work that began at the window's edge.
88
+
89
+ ### Changed
90
+
91
+ - **`FilterBar` reads its own width, not the window's** (a container query, `@max-[52rem]`). With the assistant's column open at 1440px the bar sat in about 900px and still laid out for a wide window: the search shrank to a few characters while every filter stayed. This is the rule the table already followed.
92
+ - **`Segmented` scrolls sideways with the edge fade** when its labels are wider than the track, rather than running off the card. Six options with counts in their labels ("All 37 · Idea 3 · Scored 25") no longer clip at 390px.
93
+ - **`CardBody flush` clips a `Table` as its first child and drops the header row's top border.** Directly under a card's own edge there was a second line, and the header's sunk fill squared off the card's rounded top corners — most visible in dark. A second table in the same body keeps its border.
94
+ - **The not-found screen and the error view read the home page's name from `nav`**, never "Overview". A product whose front page is a ranked list names it once in `src/app.config.ts`, and the buttons follow. The error view's second way out is `Check Health` where the product has that page and the home page where it does not.
95
+ - **`check.ts` treats `src/lib/format.ts` and `src/lib/color.ts` as the toolkit they are.** A product that re-syncs `src/lib` and removes the samples was failing Meridian's own gate on Meridian's own files (`formatCost`, `oklchToRgb`: "nothing a product keeps imports"), and had to strip the export keywords to get through. Every file under `src/lib` that Meridian ships now passes in a product, unchanged.
96
+
97
+ ### Fixed
98
+
99
+ - The assistant labelled a question with the masthead `h1`, which on a detail page is the record's name with its status badge run into it ("REC-1042high risk · 0.64"). The label now comes from the route's own `document.title` ("Record REC-1042"), with the masthead as the fallback.
100
+ - A keyboard focus that scrolled into view could land under the 56px sticky top bar. The scroll region now carries `scroll-pt-16`, so a focused control comes to rest clear of it (WCAG 2.4.11).
101
+ - `docs/assistant.md` and `.env.example` say that a gateway names models `<provider>.<model>` and to copy the id from its `GET /models` — the prefix is part of the name.
102
+
103
+ ### Breaking
104
+
105
+ - `Collection.create` and `Collection.update` (`src/lib/collection.ts`) take `Record<string, unknown>` instead of `Omit<T, K>` and `Partial<Omit<T, K>>`. A caller that passed a fully typed record still compiles; a data layer that had to cast now does not. `derived` is new and optional.
106
+ - `FormSection` gained `as` and `flush`; both default to today's behaviour, so nothing that does not pass them changes.
107
+
5
108
  ## [0.2.0] · 2026-10-04
6
109
 
7
110
  The first release on npm. A team brings Meridian into its own dashboard with one sentence to its coding agent:
@@ -61,7 +164,7 @@ projects (issues #1 and #2), and the console's own assistant.
61
164
 
62
165
  ## [0.1.0] · 2026-10-03
63
166
 
64
- The first release: a dashboard design system and a working template, built on the Zandro pattern and register.
167
+ The first release: a dashboard design system and a working template, built on the layered-card pattern and the dark, lit register of 0002.
65
168
 
66
169
  ### Added
67
170
 
@@ -84,7 +84,7 @@ Write the status on the line under the title: `Status: beta`.
84
84
  | `pnpm tokens` | Generates the token CSS and the Tailwind bridge from `tokens/` (`--check` only compares) | After any token change |
85
85
  | `node scripts/contrast.ts [--all]` | Every specified foreground on its background, in every theme and accent, and the chart palette's colour-vision checks | Gate |
86
86
  | `node scripts/registry.ts` | Regenerates the Atlas registry from the card folders (`--check` only compares) | After adding or renaming a card |
87
- | `node scripts/check.ts` | Every card has its README and preview, every README follows the anatomy, no literal colour in components, every token a spec names exists | Gate |
87
+ | `node scripts/check.ts` | Every card has its README and preview, every README follows the anatomy, no literal colour in components, every token a spec names exists and is written with its own value, and every embed view a document offers is a route | Gate |
88
88
  | `pnpm typecheck`, `pnpm test` | TypeScript and the behaviour tests | Gate |
89
89
  | `node scripts/audit.ts` | With the app running: every page and embed view at 2560, 1440, 1024, 768 and 390px in both themes (embeds at 720 and 420 on a simulated host ground). Fails on sideways scroll, text clipped without an ellipsis, a control with no accessible name, more than one page scroller, rendered text under its WCAG minimum, a Tab stop with no visible focus ring (it presses Tab through the page), and any uncaught exception or console error. Prints the design metrics: type sizes, weights, radii and the hierarchy ratio | Before release |
90
90
  | `node scripts/interactions.ts` | With the app running: presses every control on every page (mouse at 1440px, taps at 390px) and follows every link. Fails on a control that changes nothing, one something covers, and a link that answers 4xx. |
package/payload/README.md CHANGED
@@ -72,7 +72,7 @@ Dark is the default, written on `:root`; the light theme follows the operating s
72
72
  | `scripts/` | Generators, gates, `brand.ts` and `verify.ts` (see `CONTRIBUTING.md`) |
73
73
  | `skills/zz-meridian/` | The agent skill (Claude Code, Codex) that builds dashboards on this template or brings it into yours |
74
74
  | `cli/` | The `zz-meridian` npm package: `create`, `adopt` and `skill`, its build, its smoke test and the fixture app (`docs/distribution.md`) |
75
- | `.github/workflows/release.yml` | The release: gates, the consumer path from the tarball, then npm with provenance, then the tag (`.claude/commands/release-meridian.md`) |
75
+ | `.github/workflows/release.yml` | The release: gates, the consumer path from the tarball, then npm with provenance, then the tag (`.claude/commands/release.md`) |
76
76
 
77
77
  ## Bring Meridian into your dashboard, in one sentence
78
78
 
@@ -17,7 +17,7 @@ Status: beta
17
17
 
18
18
  Everything in rows 1 and 2 shares one Meridian: point at a day in either chart and the featured figure, the tiles and both charts read that day.
19
19
 
20
- Below 1024px every row stacks: the featured card first, then the tiles (two across from 34rem, one below), then the cards. At 390px the masthead's actions wrap under the title and the period select stays visible; Export moves into the command palette.
20
+ Below 1024px every row stacks: the featured card first, then the tiles (two across from 34rem, one below), then the cards. At 390px the masthead's actions wrap under the title and the period select stays visible; Export is not offered on a phone (`max-sm:hidden`), and the command palette carries destinations and appearance, not the page's actions.
21
21
 
22
22
  ## States
23
23
 
@@ -27,7 +27,7 @@ Below 1024px every row stacks: the featured card first, then the tiles (two acro
27
27
  | Empty (no traffic yet) | The featured card says "No requests yet" with a link to API keys; tiles show dashes, never zeros |
28
28
  | Partial (a series missing) | That tile shows a dash and "Not measured"; the chart breaks its line over missing days |
29
29
  | Stale | Freshness turns to warning: "Stale · updated 47 min ago" |
30
- | Error | `error.tsx`, inside the shell at the data width, like every page: the title says the view did not load and nothing was changed; one card says retrying usually works, with Retry (primary), Check Health, and the reference |
30
+ | Error | `error.tsx`, inside the shell at the data width, like every page: the title says the view did not load and nothing was changed; one card says retrying usually works, with Retry (primary), a second way out read from `nav` (Check Health where the product has one, the home page where it does not), and the reference |
31
31
 
32
32
  ## Data
33
33
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import Link from 'next/link';
4
4
  import { RotateCw } from 'lucide-react';
5
+ import { nav } from '@/app.config';
5
6
  import { PageFrame } from '@/components/base/shell';
6
7
  import { Button } from '@/components/ui/button';
7
8
  import { Card } from '@/components/ui/card';
@@ -10,8 +11,15 @@ import { EmptyState } from '@/components/ui/empty-state';
10
11
  /**
11
12
  * A page that failed to load: in place, inside the shell, so the rail and the way out stay where they were. The title
12
13
  * says what happened once; the card says what to do about it, with the reference support will ask for.
14
+ *
15
+ * The second way out is read from `nav`, never written here: not every product has a Health page, and one that does not
16
+ * would otherwise offer a button to an address that leads nowhere while the reader is already having a bad day.
13
17
  */
14
18
  export default function DashboardError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
19
+ const items = nav.flatMap((g) => g.items);
20
+ const health = items.find((i) => i.href === '/health');
21
+ const home = items.find((i) => i.href === '/');
22
+ const elsewhere = health ?? home;
15
23
  return (
16
24
  <PageFrame kicker="Something failed" title="This view did not load" description="The data behind it did not arrive. Nothing was changed.">
17
25
  <Card className="arrive">
@@ -21,11 +29,11 @@ export default function DashboardError({ error, reset }: { error: Error & { dige
21
29
  action={
22
30
  <>
23
31
  <Button variant="primary" icon={<RotateCw />} onClick={reset}>Retry</Button>
24
- <Button asChild><Link href="/health">Check Health</Link></Button>
32
+ {elsewhere ? <Button asChild><Link href={elsewhere.href}>{health ? `Check ${health.label}` : `Go to ${elsewhere.label}`}</Link></Button> : null}
25
33
  </>
26
34
  }
27
35
  >
28
- If it fails again, Health says whether an incident is under way. Reference <span className="font-mono text-ink">{error.digest ?? 'client'}</span>
36
+ {health ? `If it fails again, ${health.label} says whether an incident is under way.` : 'If it fails again, it is likely to stay that way until someone looks at the data behind it.'} Reference <span className="font-mono text-ink">{error.digest ?? 'client'}</span>
29
37
  </EmptyState>
30
38
  </Card>
31
39
  </PageFrame>
@@ -4,6 +4,7 @@
4
4
  // check as the layout's and the assistant route's.
5
5
  import { randomBytes } from 'node:crypto';
6
6
  import { z } from 'zod';
7
+ import { app } from '@/app.config';
7
8
  import { clock, keys } from '@/data/collections';
8
9
  import type { ApiKey } from '@/system/fixtures/sample-records';
9
10
 
@@ -16,7 +17,7 @@ const draft = z.object({
16
17
  /** Creates a key and returns it: the only moment its full secret leaves the server for a banner. */
17
18
  export async function createKey(input: z.input<typeof draft>): Promise<ApiKey> {
18
19
  const { name, env, scopes } = draft.parse(input);
19
- return keys.create!({ name, env, scopes, owner: 'Maya Chen', created: clock().toISOString(), lastUsed: null, secret: `zzm_${env}_${randomBytes(16).toString('hex')}` });
20
+ return keys.create!({ name, env, scopes, owner: app.user.name, created: clock().toISOString(), lastUsed: null, secret: `zzm_${env}_${randomBytes(16).toString('hex')}` });
20
21
  }
21
22
 
22
23
  export async function revokeKey(id: string): Promise<void> {
@@ -15,19 +15,19 @@ For an address outside the console, a standalone screen on the lit ground, from
15
15
  | Sentence | "This page isn't here." at `text-display`: the protagonist |
16
16
  | Lead | "The link may be mistyped or out of date, or what it pointed to has been removed." |
17
17
  | Address | A sunk field, body-size mono: the part that exists as a link to that page, the part that does not under a dashed critical rule, and one line saying which is which |
18
- | Actions | Back to the nearest page (primary, `lg`), then Go to Overview when that is somewhere else |
18
+ | Actions | Back to the nearest page (primary, `lg`), then a link to the home page when that is somewhere else (`nav`'s label for `/`) |
19
19
 
20
20
  Inside the console the screen is `MissingPage` (`src/views/missing-page.tsx`): a record page renders it itself when its ID is missing (`/requests/<id>`), and `app/(dashboard)/not-found.tsx` renders it for any other `notFound()`. A record page renders rather than throws because a `notFound()` thrown inside the dashboard's loading boundary shows only once the JavaScript arrives, which on a slow phone put the largest paint past 2.5s; the page keeps `noindex`. The rail stays; the sentence becomes the page title over the same lead at the data width, on the same left edge as every page, and one card holds the address at `text-xl` as the protagonist and the same actions (`md`). A miss at the root there offers Search pages (the command palette) as the second action.
21
21
 
22
- The words come from `src/views/not-found.tsx`; the address and the actions from `src/views/not-found-address.tsx`, once.
22
+ The words come from `src/views/not-found.tsx`; the address and the actions from `src/views/not-found-address.tsx`, once. The home page's own name is read from `nav` (`homeLabel`), not written here: a product whose front page is a ranked list, not "Overview", names it once in `src/app.config.ts` and both buttons follow.
23
23
 
24
24
  ## States
25
25
 
26
26
  | State | What shows |
27
27
  |---|---|
28
- | A missing record (`/requests/req_9x7k`) | `/requests` links to Requests; `/req_9x7k` is marked; "Requests is still here. Nothing in it answers to req_9x7k."; Back to Requests, Go to Overview |
28
+ | A missing record (`/requests/req_9x7k`) | `/requests` links to Requests; `/req_9x7k` is marked; "Requests is still here. Nothing in it answers to req_9x7k."; Back to Requests, Go to the home page (`nav`'s label: Overview here) |
29
29
  | A deeper miss (`/settings/billing/invoices/2026`) | `/settings` links to Settings; the rest is marked; "Settings is still here; the rest of the address leads nowhere." |
30
- | No part exists (`/this-page-does-not-exist`) | `/` links home; the rest is marked; "No page in ZZ Meridian lives at this address."; Go to Overview (and, in the console, Search pages) |
30
+ | No part exists (`/this-page-does-not-exist`) | `/` links home; the rest is marked; "No page in ZZ Meridian lives at this address."; Go to the home page (and, in the console, Search pages) |
31
31
  | A long ID (60 characters or more) | The address breaks anywhere rather than overflowing; the sentence says "the address above" instead of repeating an ID over 32 characters |
32
32
  | Percent-escapes (`/requests/a%20b`) | Decoded for reading where they decode, shown as typed where they do not |
33
33
  | A trailing slash | Ignored: `/requests/` is Requests itself, not a miss |
@@ -2,7 +2,7 @@ import { notFound } from 'next/navigation';
2
2
  import { CARDS } from '@/system/registry';
3
3
  import { PreviewStage } from './stage';
4
4
 
5
- /** One card's preview, bare: what the Atlas frames, and what screenshots and audits open. ?theme=dark&accent=iris&density=compact */
5
+ /** One card's preview, bare: what the Atlas frames, and what screenshots and audits open. ?theme=dark&accent=indigo&density=compact */
6
6
  export default async function Page({ params }: { params: Promise<{ section: string; card: string }> }) {
7
7
  const { section, card } = await params;
8
8
  const c = CARDS.find((x) => x.section === section && x.id === card);
@@ -4,7 +4,7 @@ Date: 2026-10-03 · Status: accepted
4
4
 
5
5
  ## Context
6
6
 
7
- Meridian follows the Zandro design system's pattern: layered cards, each specified before it is built, tokens in DTCG, gates that compute what can be computed. Zandro ships framework-agnostic CSS with HTML previews, because it is rebuilt on several platforms. Every dashboard Meridian serves is built with Next.js, React and Tailwind, so a CSS-only system would be ported by hand into every new dashboard.
7
+ Meridian follows the pattern of the design system that came before it: layered cards, each specified before it is built, tokens in DTCG, gates that compute what can be computed. That system ships framework-agnostic CSS with HTML previews, because it is built for several platforms. Every dashboard Meridian serves is built with Next.js, React and Tailwind, so a CSS-only system would be ported by hand into every new dashboard.
8
8
 
9
9
  ## Decision
10
10
 
@@ -1,10 +1,10 @@
1
- # 0002 · Zandro's register: dark first, lit, one protagonist
1
+ # 0002 · The register: dark first, lit, one protagonist
2
2
 
3
3
  Date: 2026-10-03 · Status: accepted
4
4
 
5
5
  ## Context
6
6
 
7
- The first rendering of Meridian used a neutral light canvas, a 28px page title and four equal tiles: tidy, correct, and indistinguishable from any dashboard of the last decade. The owner rejected it as dated and asked for the language of the Zandro design system, which reads at a glance as a current, premium product.
7
+ The first rendering of Meridian used a neutral light canvas, a 28px page title and four equal tiles: tidy, correct, and indistinguishable from any dashboard of the last decade. The owner rejected it as dated and asked for the register of the earlier design system (0001), which reads at a glance as a current, premium product.
8
8
 
9
9
  ## Decision
10
10
 
@@ -16,5 +16,5 @@ The first rendering of Meridian used a neutral light canvas, a 28px page title a
16
16
 
17
17
  ## Consequences
18
18
 
19
- - The system shares a family resemblance with Zandro; its own identity is the Meridian cursor, the three surfaces and the agentic layer.
19
+ - The system shares that family resemblance; its own identity is the Meridian cursor, the three surfaces and the agentic layer.
20
20
  - Every component is checked in both themes, as before; the dark theme is the one quoted in specifications.
@@ -4,7 +4,7 @@ Date: 2026-10-03 · Status: accepted · Amends 0002
4
4
 
5
5
  ## Context
6
6
 
7
- After 0002, the owner asked for the overall feeling to be more refined and less loud: premium, modern and elegant without showing off. They pointed to crypto-zentry, whose calm comes from soft, diffuse light everywhere and nothing loud anywhere: low-alpha orbs on the ground, translucent glass cards that let the light through, a faint halo around a card and a gradient border that appears only under the pointer, plain 2px chart lines, a gentle two-stop gradient on the one primary action, and gradient text only on the hero phrase. The first Meridian line glow read as a smudge on white.
7
+ After 0002, the owner asked for the overall feeling to be more refined and less loud: premium, modern and elegant without showing off. They pointed to the calm of the earlier system — soft, diffuse light everywhere and nothing loud anywhere: low-alpha orbs on the ground, translucent glass cards that let the light through, a faint halo around a card and a gradient border that appears only under the pointer, plain 2px chart lines, a gentle two-stop gradient on the one primary action, and gradient text only on the hero phrase. The first Meridian line glow read as a smudge on white.
8
8
 
9
9
  ## Decision
10
10
 
@@ -4,7 +4,7 @@ Date: 2026-10-03 · Status: accepted
4
4
 
5
5
  ## Context
6
6
 
7
- The system was first named Meridian on its own, after its signature: one time cursor, a meridian line, shared by every chart and tile on a page. The owner's products form one family (zz-stack, the zz-stack console, this repository), and a standalone brand made the system read as someone else's.
7
+ The system was first named Meridian on its own, after its signature: one time cursor, a meridian line, shared by every chart and tile on a page. The owner's products form one family, and a standalone brand made the system read as someone else's.
8
8
 
9
9
  ## Decision
10
10
 
@@ -24,8 +24,8 @@ so a team that adopted Meridian never receives a fix.
24
24
  - Every copy is recorded in `.meridian/manifest.json` in the project (the version, each file's hash, the brand
25
25
  arguments), so a later `npx zz-meridian@latest update` can tell an untouched file from one the team changed.
26
26
  - The design system and the package share one version, under the semver rules in `CHANGELOG.md`.
27
- - The package is published by CI (npm trusted publishing with provenance), following the release pipeline of
28
- multi-model-agent, with the tag created last.
27
+ - The package is published by CI (npm trusted publishing with provenance), following the release pipeline of the
28
+ owner's earlier packages, with the tag created last.
29
29
 
30
30
  ## Consequences
31
31
 
@@ -14,7 +14,7 @@ Four variables, read from the environment on every request, so one build serves
14
14
  |---|---|
15
15
  | `ASSISTANT_PROVIDER` | `anthropic`, or `openai-compatible` for any service that speaks the OpenAI chat-completions format |
16
16
  | `ASSISTANT_API_KEY` | The provider's key. Required: the approval secret is derived from it. |
17
- | `ASSISTANT_MODEL` | The model's id, as the provider names it |
17
+ | `ASSISTANT_MODEL` | The model's id, as the provider names it. A gateway such as LiteLLM names models `<provider>.<model>` — copy the id exactly as its `GET /models` lists it, and take it as it comes. |
18
18
  | `ASSISTANT_BASE_URL` | The provider's address. Required for `openai-compatible`; optional for `anthropic`. |
19
19
 
20
20
  If the key or the model is missing, or the provider is anything else, or `openai-compatible` has no address, the assistant is off. `.env.example` lists the four, commented, with no values.
@@ -1,6 +1,6 @@
1
1
  # Benchmark
2
2
 
3
- Meridian held to the bar of award-winning sites (Awwwards, Webby, FWA) and of the systems it learned from (Zandro, crypto-zentry, the zz-stack console), criterion by criterion: what we did, the evidence, and what is still open. Measured on 2026-10-03 with `node scripts/audit.ts` (every page and embed view at 2560, 1440, 1024, 768 and 390px, both themes) and `node scripts/contrast.ts` (every pair in every theme and accent).
3
+ Meridian held to the bar of award-winning sites (Awwwards, Webby, FWA) and of the systems it learned from, criterion by criterion: what we did, the evidence, and what is still open. Measured on 2026-10-03 with `node scripts/audit.ts` (every page and embed view at 2560, 1440, 1024, 768 and 390px, both themes) and `node scripts/contrast.ts` (every pair in every theme and accent).
4
4
 
5
5
  ## Scorecard
6
6
 
@@ -17,8 +17,8 @@ Meridian held to the bar of award-winning sites (Awwwards, Webby, FWA) and of th
17
17
 
18
18
  ## What changed because of the benchmark
19
19
 
20
- - **The register** (decision 0002). The first render, a neutral light canvas with 28px titles and four equal tiles, was rejected as dated. Meridian adopted Zandro's register: dark first, a lit ground, a dramatic type scale, one featured card.
21
- - **Quiet light** (decision 0006). Learning from crypto-zentry, the glow moved from the data to the frame: translucent surfaces, a lit edge and a faint halo on the featured card, a whisper of light under the line (14% on light, 24% on dark, set 2px below it), a primary action turning toward violet, one solid accent phrase per screen. The louder feature shadow, the solid accent borders and the grain were removed.
20
+ - **The register** (decision 0002). The first render, a neutral light canvas with 28px titles and four equal tiles, was rejected as dated. Meridian adopted that register: dark first, a lit ground, a dramatic type scale, one featured card.
21
+ - **Quiet light** (decision 0006). Learning from that calm, the glow moved from the data to the frame: translucent surfaces, a lit edge and a faint halo on the featured card, a whisper of light under the line (14% on light, 24% on dark, set 2px below it), a primary action turning toward violet, one solid accent phrase per screen. The louder feature shadow, the solid accent borders and the grain were removed.
22
22
  - **Tables.** The lead column no longer takes all the slack (about a third, the rest spread by content); columns sit 32px apart; a text column after a number gets 16px more; a fixed-width method chip lines up every route.
23
23
  - **Performance found by the audit.** A repeated grain texture under translucent cards stalled rasterisation at device scale (a 2× capture timed out past 30 seconds; without it, under a second), and a blend mode on a full-screen layer left charts painted stale. Both are gone.
24
24
  - **Motion, evaluated with motion on.** The light under the featured line now draws with the line instead of fading in ahead of it; line, light and area arrive together over 820ms (93% drawn by 250ms, settled by 550ms). The Atlas hero's cursor sweeps the month once and rests, instead of looping.
@@ -30,11 +30,11 @@ Meridian held to the bar of award-winning sites (Awwwards, Webby, FWA) and of th
30
30
  - **A last look, by eye, at 1440 and 390px in both themes.** Three things the audit cannot see were fixed. A sparkline's troughs ran along the card's bottom edge and into its rounded corner; the lowest point now floats a quarter of the height above the floor while the area still fills to it. An incident title broke after the hyphen in "eu-west-1"; hyphenated identifiers in titles now hold together. The incident timeline set its times as spaced mono capitals ("3 6 M I N"); they are now captions with tabular figures. On phones, the uptime card's service grid cut "Inference API" short to make room for a latency figure; the grid now shows names only there, and latency stays in the service list just below.
31
31
 
32
32
  - **Pressing everything, not only measuring it.** The owner found that the bell did nothing and the tab had no icon. A probe that pressed every control on every page (mouse at 1440px, taps at 390px) then found more that the audit could not see, because the audit measured pages and never used them: the workspace switcher, the alerts bell, every Export, Subscribe to updates, Invite customer and Continue with SSO did nothing; the tiles' info buttons opened only on hover, so a phone could never read them; two items in a request's menu only announced themselves in a toast. Each now does its job (a workspace menu, an alerts panel, a CSV download, a toggle, an invite sheet, an email-first SSO step, a tap-to-open explanation, a copied link). `scripts/interactions.ts` presses every control and follows every link on every page, and `pnpm verify` fails on any that does nothing.
33
- - **Glass that was not glass.** Meridian resets Tailwind's blur scale and never defined its own, so every `backdrop-blur` rendered nothing: the stuck top bar, the rail and the shell tools were tinted, not frosted, and text showed through them. Blur is now a token (`blur-sm` 8px, `blur-md` 12px, `blur-xl` 24px), and the gate fails a blur utility outside it.
33
+ - **Glass that was not glass.** Meridian resets Tailwind's blur scale and never defined its own, so every `backdrop-blur` rendered nothing: the stuck top bar, the rail and the shell tools were tinted, not frosted, and text showed through them. Blur is now a token (`blur-md` 12px, `blur-xl` 24px), and the gate fails a blur utility outside it.
34
34
  - **Tables that clipped their last column.** Columns dropped by the window's width, which ignores the rail, so between 768 and 1440px the API keys table (and Customers and Requests at 1024 to 1280px) ran wider than its card and the card cut the actions off; on a phone the Analytics endpoints table cut off p95. Columns now drop by the table's own width (a container query), a lead column needs 160px on a narrow table, and the audit fails any table wider than its frame.
35
35
  - **A tab icon.** `app/icon.ts` draws the App mark in the default accent, read from the tokens at build, so a new brand repaints it.
36
36
 
37
- - **An independent critic, against the juries' criteria.** A reviewer that had not built Meridian rendered all 15 routes at 360, 768 and 1440px in both themes and scored them against the Awwwards, Webby, CSSDA and FWA criteria and Zandro's eight craft criteria. Scores: typography 8, whitespace 6, hierarchy 8, colour 7, motion 7, micro-interaction 7, responsiveness 6, originality 7. It failed three criteria: colour keeps its job, one type scale, and motion and speed. Fixed from its list:
37
+ - **An independent critic, against the juries' criteria.** A reviewer that had not built Meridian rendered all 15 routes at 360, 768 and 1440px in both themes and scored them against the Awwwards, Webby, CSSDA and FWA criteria and the earlier system's eight craft criteria. Scores: typography 8, whitespace 6, hierarchy 8, colour 7, motion 7, micro-interaction 7, responsiveness 6, originality 7. It failed three criteria: colour keeps its job, one type scale, and motion and speed. Fixed from its list:
38
38
  - **Honesty:** uptime on a phone showed 30 bars beside the 90-day figure, so every day now shows. The trend's "Errors × 20" put a scaled number in a tooltip, so it is gone.
39
39
  - **Composition:**
40
40
  - the Atlas home's empty column and its raw changelog;
@@ -45,7 +45,7 @@ Meridian held to the bar of award-winning sites (Awwwards, Webby, FWA) and of th
45
45
  - a sign-in page with no proof (it now carries a live Meridian trend).
46
46
  - **Colour:** success rows are quiet and only 4xx, 5xx, past due and trial are coloured; the accent is off 35 "beta" marks and every customer sparkline; one freshness claim per view; one solid accent phrase instead of gradient text.
47
47
  - **Type:** headings descend one level at a time, and the audit fails a skip.
48
- - **Motion:** every press control animates its press. Motion literals are tokens, and the gate fails a literal duration or a press transition without transform.
48
+ - **Motion:** every press control animates its press. Motion literals are tokens, and the gate fails a literal duration in `base.css`/`motion.css` or a press transition without transform.
49
49
  - **Touch:** every control and box link answers 44px on a coarse pointer, phones are emulated as touch, and the audit fails a smaller target.
50
50
  - **Small things:** embed heads keep Freshness whole; hour ticks follow 00/06/12/18; an in-app link uses → and a link out uses ↗.
51
51
 
@@ -1,6 +1,6 @@
1
1
  # Distribution: the `zz-meridian` package
2
2
 
3
- Status: v1 shipped in 0.2.0 (decision 0009). `update` is v2. Releasing: `.claude/commands/release-meridian.md`.
3
+ Status: v1 shipped in 0.2.0 (decision 0009). `update` is v2. Releasing: the `/release` command (`.claude/commands/release.md`).
4
4
 
5
5
  ## The one sentence
6
6
 
@@ -20,7 +20,7 @@ rebuilding each page on Meridian's components, the fake API, and reading what `p
20
20
  - **Name** `zz-meridian`, unscoped (free on npm, and the shortest sentence). The command is `zz-meridian`
21
21
  (`bin: { "zz-meridian": "dist/cli.js" }`); a different bin name breaks `npx zz-meridian`.
22
22
  - **Where it lives**: `cli/` in this repository, its own `package.json`, built with `tsc` to `cli/dist/`; not a
23
- workspace member, and outside the root's type check and lint. The repository root stays the template app, `private`,
23
+ workspace member, and outside the root's type check (the root lint does cover `cli/**`). The repository root stays the template app, `private`,
24
24
  renamed `zz-meridian-template`.
25
25
  - **Payload**: a snapshot of the template, built by `cli/scripts/build-payload.ts` into `cli/payload/` with `git archive
26
26
  HEAD`, never from a walk of the folder, so a build output, a local `.env` or an uncommitted edit cannot ship
@@ -43,7 +43,7 @@ clone-then-delete.
43
43
  Brings Meridian into the project in the current folder. It refuses, and copies nothing, when:
44
44
 
45
45
  - the git tree is dirty (`--allow-dirty` overrides): every change it makes must be reviewable as one diff, and revertable;
46
- - the project is not Next.js with the App Router and React 18 or newer: it exits with the Route B or C guidance from
46
+ - the project is not Next.js with the App Router: it exits with the Route B or C guidance from
47
47
  `references/existing-project.md` instead.
48
48
 
49
49
  Then, in this order:
@@ -54,7 +54,7 @@ Then, in this order:
54
54
  fixture test (below) is what keeps it complete. A file of the same name that differs is never overwritten: `adopt`
55
55
  lists the conflicts and stops before writing anything.
56
56
  2. Merges dependencies and scripts into their `package.json`, keeping their versions where newer and compatible; adds
57
- the `@/* → src/*` alias to `tsconfig.json` when theirs differs.
57
+ the `@meridian/* → ./src/*` alias to `tsconfig.json` when theirs differs (their own `@/*` is left as it is).
58
58
  3. Replaces their global stylesheet with the template's `app/globals.css`, keeping theirs as `app/globals.before.css`
59
59
  for the agent to port from.
60
60
  4. Runs `scripts/brand.ts --existing` with the brand arguments given (or the name from their `package.json`): the name,
@@ -114,11 +114,11 @@ checks.
114
114
 
115
115
  ## Release pipeline
116
116
 
117
- Modelled on multi-model-agent's (`.github/workflows/release.yml` there), one package instead of two:
117
+ Modelled on the release pipeline of the owner's earlier packages, one package instead of two:
118
118
 
119
119
  1. **Dispatch**: `gh workflow run release.yml -f version=<v> [-f dry_run=true]`, from `master`. The version must equal
120
120
  `cli/package.json`'s and the tag must be unused.
121
- 2. **Gates** (ubuntu): `pnpm gate`, `next build`, and the CLI's own tests.
121
+ 2. **Gates** (ubuntu): `pnpm gate`, `next build`, and the consumer smoke from the built tarball (step 4).
122
122
  3. **Pack and assert, before anything is irreversible**: `pnpm pack` in `cli/`, then on the tarball: the bin has its
123
123
  `#!/usr/bin/env node` line; `payload/skills/zz-meridian/SKILL.md` and its references are there; the component count
124
124
  matches the repository; no `node_modules/`, `tests/` of the CLI, `out/` or `.next/`.
@@ -128,27 +128,29 @@ Modelled on multi-model-agent's (`.github/workflows/release.yml` there), one pac
128
128
  - `create` into a clean folder, then `pnpm verify --quick --no-vitals`. Chrome is on ubuntu runners; Web Vitals
129
129
  measure the machine, so CI leaves them to the local run (`--no-vitals` is new in v1).
130
130
  5. **Publish** the tarball with `npm` 11.5.1 or newer through trusted publishing (OIDC), with `--provenance`. `pnpm
131
- publish` does not perform the OIDC exchange; multi-model-agent learned this at 5.16.1.
131
+ publish` does not perform the OIDC exchange.
132
132
  6. **Tag `v<version>` last**, then the GitHub Release with the version's `CHANGELOG.md` section as its body.
133
133
 
134
- `dry_run` stops after step 4. A `/release-meridian` runbook (`.claude/commands/`) holds the judgement before dispatch:
135
- the version, the changelog section, the docs sweep, and the local `pnpm verify` with Web Vitals. A `scripts/set-version.ts`
134
+ `dry_run` stops after step 4. A `/release` runbook (`.claude/commands/release.md`) holds the judgement before dispatch:
135
+ the version, the changelog section, the docs sweep, and the local `pnpm verify` with Web Vitals. A `cli/scripts/set-version.ts`
136
136
  writes the version into `cli/package.json` and checks the root agrees.
137
137
 
138
138
  ## One-time setup (the maintainer, once)
139
139
 
140
- npm configures a trusted publisher only on a package that exists, so 0.2.0 was published from a laptop under the
141
- maintainer's npm account, with 2FA: the exact tarball the dry run tested, downloaded from its run. Then npmjs.com →
142
- `zz-meridian` → Settings → Trusted publisher: GitHub Actions, `zhixuan312` / `zz-meridian` / `release.yml`, and
143
- Publishing access set to require 2FA and disallow tokens. The real dispatch of 0.2.0 then found the version on the
144
- registry, skipped the publish, and finished the consumer check, the tag and the Release. From 0.2.1 on, CI publishes
145
- with provenance.
140
+ npm configures a trusted publisher only on a package that exists. The maintainer created `zz-meridian` with a
141
+ `0.0.0-stage` placeholder, then on npmjs.com → `zz-meridian` → Settings set the trusted publisher (GitHub Actions,
142
+ `zhixuan312` / `zz-meridian` / `release.yml`, environment `npm`, "Allow npm publish" on) and Publishing access to
143
+ require 2FA and disallow tokens. 0.2.0 itself was published from the maintainer's laptop, with 2FA, before the
144
+ publisher was set: the exact tarball the dry run had tested (its sha512 matches), so it carries no provenance. The
145
+ release run then found it on the registry, skipped the publish, and finished the consumer check, the tag and the
146
+ Release. From 0.2.1 on, only the workflow, from `master` through the `npm` environment, can publish, and every
147
+ version carries provenance. The placeholder can be deprecated.
146
148
 
147
149
  ## Versioning
148
150
 
149
151
  The design system and the package share one version, under the rules at the top of `CHANGELOG.md`: a removed or renamed
150
152
  token, prop or card is major; a new card, token or variant is minor; a corrected value is a patch. The current
151
- `[Unreleased]` section becomes the first release, `0.2.0`.
153
+ `[Unreleased]` section becomes the next release.
152
154
 
153
155
  ## Phases
154
156
 
@@ -30,7 +30,7 @@ Below 1024px the rail becomes a drawer (the same node, so nothing is defined twi
30
30
  | Menus and selects | Popover | Popover, at least 44px rows on touch | Menu, Select |
31
31
  | Charts | Full height, 6 to 8 date labels | Shorter (180px), 3 to 4 date labels; the Meridian follows the finger and the tooltip pins to the top edge | TrendChart |
32
32
  | Toolbar | Search, filters and view controls in one row | Search full width; filters behind one Filters button that opens a sheet | FilterBar |
33
- | Touch targets | 36px default control | Every control at least 44px tall where it is the main interaction (`control-lg`) | Button, Field |
33
+ | Touch targets | 38px default control | Every control at least 44px tall where it is the main interaction (`control-lg`) | Button, Field |
34
34
 
35
35
  ### Embed (MCP Apps)
36
36
 
@@ -46,12 +46,13 @@ Meridian's rules for the embed surface:
46
46
  6. **Respect the safe area.** Padding adds `safeAreaInsets` on mobile hosts.
47
47
  7. **Act through the host.** A button in an embed calls a tool or sends a message; it never navigates the frame. A destructive action is never a bare button: it is a Proposal marked critical, and it runs only when the person approves.
48
48
 
49
- The token bridge, applied by `EmbedFrame` (`data-surface="embed"`), maps the host's variables onto Meridian's roles and falls back to Meridian's own value when the host sends nothing:
49
+ The token bridge, applied by `EmbedSurface` on every embed route (`app/embed/layout.tsx`, `data-surface="embed"`), maps the host's variables onto Meridian's roles and falls back to Meridian's own value when the host sends nothing:
50
50
 
51
51
  | Meridian role | Host variable |
52
52
  |---|---|
53
53
  | `ground` | transparent |
54
54
  | `surface` | `--color-background-primary` |
55
+ | `surface-raised` | `--color-background-primary` |
55
56
  | `surface-sunk` | `--color-background-secondary` |
56
57
  | `ink` | `--color-text-primary` |
57
58
  | `ink-2` | `--color-text-secondary` |
@@ -59,6 +60,7 @@ The token bridge, applied by `EmbedFrame` (`data-surface="embed"`), maps the hos
59
60
  | `line` | `--color-border-primary` |
60
61
  | `line-strong` | `--color-border-secondary` |
61
62
  | `font-sans` | `--font-sans` |
63
+ | `radius-md` | `--border-radius-md` |
62
64
  | `radius-lg` | `--border-radius-lg` |
63
65
 
64
66
  What does not bridge: the accent, the status colours and the chart slots (they carry meaning, and the host has no equivalent), and figures (always Meridian's semi-condensed setting, so a number reads the same in the console and in a chat).
@@ -105,7 +107,7 @@ What exists, by layer, and how it changes per surface. "Same" means the card nee
105
107
 
106
108
  | Group | Cards |
107
109
  |---|---|
108
- | Actions | Button, Icon button, Menu, Link |
110
+ | Actions | Button, Icon button, Menu |
109
111
  | Input | Field, Input, Textarea, Select, Checkbox, Radio group, Switch, Segmented, Search input |
110
112
  | Display | Card, Badge, Status dot, Delta, Avatar, Tooltip, Progress, Key value, Copy field, Kbd |
111
113
  | Navigation | Tabs, Breadcrumb, Pagination |
@@ -148,12 +150,15 @@ All components are surface-agnostic by construction: they size from control toke
148
150
  |---|---|---|---|---|
149
151
  | Overview | Dashboard: tiles, a trend, breakdowns | Tiles · trend 2/3 + ranked list 1/3 · composition 1/2 + activity 1/2 | One column | `overview` inline: three tiles and the trend; fullscreen: the page |
150
152
  | Requests | List: filter bar, data table, detail on select | Table | Card list | `requests` inline: the five latest that match the tool's query |
151
- | Request | Detail: head, facts, timeline | Facts 2/3 + context 1/3 | One column | `request` inline: the record card |
153
+ | Request | Detail: head, facts, timeline | Facts 2/3 + context 1/3 | One column | Not offered; the inline list links here |
152
154
  | Analytics | Two-chart: heatmap, breakdowns | Heatmap full width · 1/2 breakdowns | Grouped heatmap | Fullscreen only |
153
155
  | Health | Operational: status list, incidents | Status 2/3 + incident 1/3 | One column | `health` inline: the status list |
154
- | Customers | List with sparklines | Table | Card list | `customer` inline: one customer's card |
156
+ | Customers | List with sparklines | Table | Card list | Not offered |
157
+ | API keys | List with destructive actions | Table | Card list | Not offered |
158
+ | Members | List with destructive actions | Table | Card list | Not offered |
155
159
  | Settings | Reader: form sections | 832px column | One column | Not offered |
156
160
  | Sign in, Not found, Error | Standalone: one sentence at poster size | Centred | Same | Not offered |
161
+ | Agent proposal | Showcase: a Proposal end to end | The embed view itself | One column | `proposal`: one Proposal card with its states and Approve (no Expand — the card is the whole view) |
157
162
 
158
163
  ## Adding to the inventory
159
164
 
@@ -4,7 +4,7 @@ const config: NextConfig = {
4
4
  reactStrictMode: true,
5
5
  devIndicators: false,
6
6
  // Card specifications (README.md next to each component) are read at build time by the Design Atlas.
7
- outputFileTracingIncludes: { '/system/**': ['./src/**/*.md', './docs/**/*.md', './decisions/**/*.md', './*.md', './tokens/**/*.json'], '/icon': ['./tokens/**/*.json'] },
7
+ outputFileTracingIncludes: { '/system/**': ['./src/**/*.md', './app/**/*.md', './docs/**/*.md', './decisions/**/*.md', './*.md', './tokens/**/*.json'], '/icon': ['./tokens/**/*.json'] },
8
8
  };
9
9
 
10
10
  export default config;