zz-meridian 0.3.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.
- package/dist/adopt.js +1 -1
- package/package.json +1 -1
- package/payload/CHANGELOG.md +71 -1
- package/payload/CONTRIBUTING.md +1 -1
- package/payload/README.md +1 -1
- package/payload/app/(dashboard)/README.md +1 -1
- package/payload/app/(dashboard)/keys/actions.ts +2 -1
- package/payload/app/system/preview/[section]/[card]/page.tsx +1 -1
- package/payload/decisions/0001-react-and-the-repository.md +1 -1
- package/payload/decisions/{0002-zandro-register.md → 0002-the-register.md} +3 -3
- package/payload/decisions/0006-quiet-light.md +1 -1
- package/payload/decisions/0007-the-name.md +1 -1
- package/payload/decisions/0009-a-package-that-copies.md +2 -2
- package/payload/docs/benchmark.md +6 -6
- package/payload/docs/distribution.md +10 -10
- package/payload/docs/surfaces.md +10 -5
- package/payload/next.config.ts +1 -1
- package/payload/package.json +1 -1
- package/payload/scripts/assistant.ts +9 -0
- package/payload/scripts/brand.ts +0 -1
- package/payload/scripts/check.ts +61 -0
- package/payload/scripts/contrast.ts +4 -4
- package/payload/scripts/fake-llm.ts +1 -2
- package/payload/scripts/keyboard.ts +21 -4
- package/payload/scripts/registry.ts +3 -3
- package/payload/scripts/tokens.ts +5 -5
- package/payload/scripts/verify.config.ts +1 -1
- package/payload/scripts/verify.ts +10 -3
- package/payload/skills/zz-meridian/references/customize.md +5 -0
- package/payload/skills/zz-meridian/references/existing-project.md +6 -0
- package/payload/src/components/base/app-mark/preview.tsx +4 -1
- package/payload/src/components/base/motion/preview.tsx +4 -1
- package/payload/src/components/base/providers.tsx +19 -7
- package/payload/src/components/base/shell/README.md +1 -1
- package/payload/src/components/base/shell/index.tsx +10 -3
- package/payload/src/components/base/surface/index.tsx +1 -1
- package/payload/src/components/charts/sparkline/index.tsx +3 -0
- package/payload/src/components/patterns/appearance-menu/index.tsx +1 -1
- package/payload/src/components/patterns/ask-about/index.tsx +1 -1
- package/payload/src/components/patterns/assistant/README.md +7 -0
- package/payload/src/components/patterns/assistant/index.tsx +0 -17
- package/payload/src/components/patterns/assistant/launcher.tsx +28 -0
- package/payload/src/components/patterns/assistant/preview.tsx +2 -1
- package/payload/src/components/patterns/data-table/README.md +6 -5
- package/payload/src/components/patterns/data-table/index.tsx +92 -84
- package/payload/src/components/patterns/export-button/README.md +1 -1
- package/payload/src/components/patterns/featured-metric/README.md +1 -1
- package/payload/src/components/patterns/featured-metric/index.tsx +8 -15
- package/payload/src/components/patterns/filter-bar/index.tsx +1 -1
- package/payload/src/components/patterns/metric-tile/index.tsx +2 -9
- package/payload/src/components/patterns/shell-tools/index.tsx +1 -1
- package/payload/src/components/ui/banner/README.md +1 -1
- package/payload/src/components/ui/button/README.md +5 -5
- package/payload/src/components/ui/button/preview.tsx +1 -1
- package/payload/src/components/ui/card/README.md +2 -2
- package/payload/src/components/ui/card/index.tsx +1 -1
- package/payload/src/components/ui/dialog/README.md +1 -1
- package/payload/src/components/ui/icon-button/README.md +3 -3
- package/payload/src/components/ui/input/README.md +4 -4
- package/payload/src/components/ui/input/preview.tsx +1 -1
- package/payload/src/components/ui/menu/README.md +2 -2
- package/payload/src/components/ui/pagination/README.md +1 -1
- package/payload/src/components/ui/popover/README.md +2 -2
- package/payload/src/components/ui/popover/index.tsx +0 -1
- package/payload/src/components/ui/segmented/README.md +2 -2
- package/payload/src/components/ui/select/README.md +1 -1
- package/payload/src/components/ui/sheet/README.md +1 -1
- package/payload/src/components/ui/sheet/index.tsx +1 -1
- package/payload/src/components/ui/skeleton/README.md +1 -1
- package/payload/src/components/ui/table/README.md +2 -2
- package/payload/src/components/ui/table/index.tsx +2 -2
- package/payload/src/components/ui/textarea/README.md +1 -1
- package/payload/src/components/ui/toast/README.md +1 -1
- package/payload/src/components/ui/tooltip/README.md +1 -1
- package/payload/src/data/collections.ts +11 -0
- package/payload/src/lib/format.ts +22 -4
- package/payload/src/lib/host.ts +10 -0
- package/payload/src/lib/safe-markdown.ts +9 -1
- package/payload/src/styles/motion.css +0 -5
- package/payload/src/styles/tokens.css +0 -1
- package/payload/src/system/content.ts +7 -6
- package/payload/src/system/fixtures/sample-records.ts +1 -1
- package/payload/src/system/fixtures/sample.ts +1 -2
- package/payload/src/system/markdown.tsx +1 -1
- package/payload/src/system/page-stage.tsx +1 -1
- package/payload/src/system/registry.ts +1 -1
- package/payload/src/system/token-view.tsx +1 -1
- package/payload/src/system/tokens-data.ts +1 -1
- package/payload/tests/agents.test.tsx +29 -0
- package/payload/tests/data-table.test.tsx +48 -0
- package/payload/tests/format.test.ts +19 -1
- package/payload/tests/preferences.test.tsx +41 -0
- package/payload/tests/prose.test.tsx +11 -0
- package/payload/tests/sparkline.test.tsx +30 -0
- 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
|
-
|
|
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.
|
|
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",
|
package/payload/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,76 @@
|
|
|
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
|
+
|
|
5
75
|
## [0.3.0] · 2026-10-04
|
|
6
76
|
|
|
7
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.
|
|
@@ -94,7 +164,7 @@ projects (issues #1 and #2), and the console's own assistant.
|
|
|
94
164
|
|
|
95
165
|
## [0.1.0] · 2026-10-03
|
|
96
166
|
|
|
97
|
-
The first release: a dashboard design system and a working template, built on the
|
|
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.
|
|
98
168
|
|
|
99
169
|
### Added
|
|
100
170
|
|
package/payload/CONTRIBUTING.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
|
@@ -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:
|
|
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> {
|
|
@@ -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=
|
|
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
|
|
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 ·
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
|
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
|
|
21
|
-
- **Quiet light** (decision 0006). Learning from
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,11 +128,11 @@ 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
|
|
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
|
|
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)
|
|
@@ -150,7 +150,7 @@ version carries provenance. The placeholder can be deprecated.
|
|
|
150
150
|
|
|
151
151
|
The design system and the package share one version, under the rules at the top of `CHANGELOG.md`: a removed or renamed
|
|
152
152
|
token, prop or card is major; a new card, token or variant is minor; a corrected value is a patch. The current
|
|
153
|
-
`[Unreleased]` section becomes the
|
|
153
|
+
`[Unreleased]` section becomes the next release.
|
|
154
154
|
|
|
155
155
|
## Phases
|
|
156
156
|
|
package/payload/docs/surfaces.md
CHANGED
|
@@ -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 |
|
|
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 `
|
|
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
|
|
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 |
|
|
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 |
|
|
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
|
|
package/payload/next.config.ts
CHANGED
|
@@ -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;
|
package/payload/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "ZZ Meridian: a dashboard design system and starter. DTCG tokens, five layers of React components and a Design Atlas, in a light and a dark theme.",
|
|
@@ -90,8 +90,17 @@ try {
|
|
|
90
90
|
if (!(await page.eval<boolean>(`!!document.querySelector('${LAUNCHER}')`))) fail('no launcher on /');
|
|
91
91
|
ok('the launcher is on /');
|
|
92
92
|
|
|
93
|
+
// The panel is a SEPARATE CHUNK, and this is what says so. The page is measured with the panel closed, then again
|
|
94
|
+
// with it open: the scripts that appear in between are the panel's. If the launcher's own chunk carried the panel
|
|
95
|
+
// — which it did until issue #7 — opening it fetches nothing new, because every page already had all of it.
|
|
96
|
+
const loadedScripts = () => page.eval<string[]>(`performance.getEntriesByType('resource').filter((e) => e.name.endsWith('.js')).map((e) => e.name)`);
|
|
97
|
+
const beforeOpen = await loadedScripts();
|
|
98
|
+
|
|
93
99
|
await page.eval(`document.querySelector('${LAUNCHER}').click()`);
|
|
94
100
|
await until('the panel to open', () => page.eval<boolean>(`!!document.querySelector('aside[data-assistant]')`), Boolean);
|
|
101
|
+
const panelChunks = (await loadedScripts()).filter((u) => !beforeOpen.includes(u));
|
|
102
|
+
if (!panelChunks.length) fail('opening the panel downloaded no script a page with it closed had not already fetched, so the panel is in every page\'s first load');
|
|
103
|
+
ok(`the panel is its own chunk: opening it fetched ${panelChunks.length} script(s) that a page with it closed never downloads`);
|
|
95
104
|
ok('pressing the launcher opens the panel');
|
|
96
105
|
|
|
97
106
|
await page.eval(`document.querySelector('aside[data-assistant] textarea[aria-label="Message"]').focus()`);
|
package/payload/scripts/brand.ts
CHANGED
|
@@ -124,7 +124,6 @@ if (hue !== undefined) {
|
|
|
124
124
|
let prefs = read('src/lib/preferences.ts');
|
|
125
125
|
if (!prefs.includes(`'${id}'`)) {
|
|
126
126
|
prefs = prefs.replace(/export const ACCENTS = \[([^\]]*)\] as const;/, (_, list) => `export const ACCENTS = [${list}, '${id}'] as const;`);
|
|
127
|
-
prefs = prefs.replace(/(export const ACCENT_SWATCH[^{]*\{)([^}]*)\}/, (_, head, body) => `${head} ${body.trim()}, ${id}: 'oklch(0.56 ${c} ${h})' }`);
|
|
128
127
|
write('src/lib/preferences.ts', prefs);
|
|
129
128
|
}
|
|
130
129
|
defaultAccent(id);
|
package/payload/scripts/check.ts
CHANGED
|
@@ -66,6 +66,67 @@ for (const f of SPECS) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// ── A specification that annotates a token with its value gets the value right ───────────────────────
|
|
70
|
+
// The specs write sizes as `` `radius-lg` 16px `` and `` `control-md` 38px ``, which is a claim about the token. When
|
|
71
|
+
// the radius scale was retuned (4/6/8/12/16 to 5/8/10/16/24) and the control heights with it (30/36/44 to 32/38/46),
|
|
72
|
+
// nineteen of those annotations were left behind — so a product sizing a control, a corner or a row from the specs was
|
|
73
|
+
// a few pixels out, and nothing said so. A token whose FIRST definition is not a plain px (a clamp, a var, a calc) has
|
|
74
|
+
// no single number to compare and is skipped; where a later definition is a plain px it joins the accepted set, so a
|
|
75
|
+
// density variant may be written as either.
|
|
76
|
+
const LITERAL = new Map<string, Set<number>>();
|
|
77
|
+
{
|
|
78
|
+
const first = new Map<string, string>();
|
|
79
|
+
for (const m of read('src/styles/tokens.css').matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+)/g)) {
|
|
80
|
+
if (!first.has(m[1])) first.set(m[1], m[2].trim());
|
|
81
|
+
if (/^[0-9.]+px$/.test(m[2].trim())) {
|
|
82
|
+
if (!LITERAL.has(m[1])) LITERAL.set(m[1], new Set());
|
|
83
|
+
LITERAL.get(m[1])!.add(Number(m[2].trim().replace('px', '')));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const [name, def] of first) if (!/^[0-9.]+px$/.test(def)) LITERAL.delete(name);
|
|
87
|
+
}
|
|
88
|
+
for (const f of SPECS) {
|
|
89
|
+
for (const m of read(f).matchAll(/`([a-z][a-z0-9-]*)`[ ]*\(?[ ]*([0-9.]+)px/g)) {
|
|
90
|
+
const values = LITERAL.get(m[1]);
|
|
91
|
+
if (values && !values.has(Number(m[2]))) problems.push(`${f}: \`${m[1]}\` is written as ${m[2]}px; the token is ${[...values].join(' or ')}px`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── An embed view a document names exists ────────────────────────────────────────────────────────────
|
|
96
|
+
// Two shapes name an embed view, and both have drifted from the routes:
|
|
97
|
+
//
|
|
98
|
+
// - the literal route, in prose: `/embed/orders`. A name followed by a dot is a FILE in that folder
|
|
99
|
+
// (`app/embed/layout.tsx`), not a route, so it is left alone.
|
|
100
|
+
// - the surfaces inventory's own column (`docs/surfaces.md`, Layer 4), which names the view: `` `overview` ``. It
|
|
101
|
+
// offered a `request` and a `customer` view that had never been built and left out the `proposal` one that had, and
|
|
102
|
+
// nothing noticed: a reader following the guide went looking for a view that is not there.
|
|
103
|
+
const isEmbed = (name: string) => fs.existsSync(path.join(ROOT, APP_DIR, 'embed', name, 'page.tsx'));
|
|
104
|
+
for (const f of [...SPECS, ...walk('skills', /\.md$/)]) {
|
|
105
|
+
for (const m of new Set([...read(f).matchAll(/\/embed\/([a-z0-9-]+)(?![\w.-])/g)].map((x) => x[1]))) {
|
|
106
|
+
if (!isEmbed(m)) problems.push(`${f}: names the embed /embed/${m}, which is not a route`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
{
|
|
110
|
+
// A product built by `brand.ts --product` has no `docs/` at all, so the file is read only when it is there.
|
|
111
|
+
const file = 'docs/surfaces.md';
|
|
112
|
+
if (fs.existsSync(path.join(ROOT, file))) {
|
|
113
|
+
const lines = read(file).split('\n');
|
|
114
|
+
const after = lines.slice(lines.findIndex((l) => l.startsWith('### Layer 4')) + 1);
|
|
115
|
+
const rows: string[] = [];
|
|
116
|
+
for (const l of after) {
|
|
117
|
+
if (l.startsWith('|')) rows.push(l);
|
|
118
|
+
else if (rows.length) break;
|
|
119
|
+
}
|
|
120
|
+
for (const row of rows.slice(2)) {
|
|
121
|
+
const cell = row.split('|').slice(-2)[0]?.trim() ?? '';
|
|
122
|
+
if (!cell || /Not offered|Fullscreen only/.test(cell)) continue;
|
|
123
|
+
for (const m of new Set([...cell.matchAll(/`([a-z0-9-]+)`/g)].map((x) => x[1]))) {
|
|
124
|
+
if (!isEmbed(m)) problems.push(`${file}: the inventory offers a \`${m}\` embed view, which is not a route`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
69
130
|
// ── Hand-written styles: motion from tokens ──────────────────────────────────────────────────────────
|
|
70
131
|
for (const f of ['src/styles/base.css', 'src/styles/motion.css']) {
|
|
71
132
|
read(f).split('\n').forEach((line, i) => {
|
|
@@ -13,7 +13,7 @@ import { parse, over, ratio, simulate, deltaE, oklchOf, hex } from '../src/lib/c
|
|
|
13
13
|
const TEXT = 4.5, UI = 3;
|
|
14
14
|
type Pair = [fg: string, bg: string | string[], min: number, what: string];
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
const PAIRS: Pair[] = [
|
|
17
17
|
['ink', 'ground', TEXT, 'body text on the page'],
|
|
18
18
|
['ink', 'surface', TEXT, 'body text in a card'],
|
|
19
19
|
['ink', 'surface-raised', TEXT, 'text in a menu or dialog'],
|
|
@@ -60,7 +60,7 @@ export const PAIRS: Pair[] = [
|
|
|
60
60
|
];
|
|
61
61
|
|
|
62
62
|
/** Every custom property of one context, as raw CSS strings. */
|
|
63
|
-
|
|
63
|
+
function context(theme: string, accent: string): Record<string, string> {
|
|
64
64
|
const { sets, mods } = resolver();
|
|
65
65
|
const env: Record<string, string> = {};
|
|
66
66
|
for (const f of sets) if (!f.startsWith('palette')) for (const t of tokensOf(load(f))) env[t.name] = cssValue(t);
|
|
@@ -71,7 +71,7 @@ export function context(theme: string, accent: string): Record<string, string> {
|
|
|
71
71
|
return env;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
function resolve(env: Record<string, string>, name: string, depth = 0): string {
|
|
75
75
|
if (depth > 8) throw new Error(`cycle at --${name}`);
|
|
76
76
|
let v = env[name];
|
|
77
77
|
if (v === undefined) throw new Error(`--${name} is not defined`);
|
|
@@ -79,7 +79,7 @@ export function resolve(env: Record<string, string>, name: string, depth = 0): s
|
|
|
79
79
|
return v.replace(/calc\(([\d.]+)\s*\*\s*([\d.]+)\)/g, (_, x, y) => String(Number(x) * Number(y)));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
const colorOf = (env: Record<string, string>, name: string) => parse(resolve(env, name));
|
|
83
83
|
|
|
84
84
|
function measure(env: Record<string, string>, fg: string, bg: string | string[]) {
|
|
85
85
|
let back = colorOf(env, 'ground');
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import http from 'node:http';
|
|
16
16
|
import type { AddressInfo } from 'node:net';
|
|
17
|
-
import { pathToFileURL } from 'node:url';
|
|
18
17
|
|
|
19
18
|
type Message = { role?: string; content?: unknown };
|
|
20
19
|
type Call = { name: string; args: object };
|
|
@@ -171,7 +170,7 @@ export async function startFakeLlm({ port }: { port: number }) {
|
|
|
171
170
|
};
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
if (
|
|
173
|
+
if (import.meta.main) {
|
|
175
174
|
const i = process.argv.indexOf('--port');
|
|
176
175
|
const port = i >= 0 ? Number(process.argv[i + 1]) : 0;
|
|
177
176
|
if (!Number.isInteger(port) || port < 0) { console.error('usage: node scripts/fake-llm.ts --port <n>'); process.exit(1); }
|