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.
- package/dist/adopt.js +1 -1
- package/package.json +1 -1
- package/payload/.env.example +2 -0
- package/payload/CHANGELOG.md +104 -1
- package/payload/CONTRIBUTING.md +1 -1
- package/payload/README.md +1 -1
- package/payload/app/(dashboard)/README.md +2 -2
- package/payload/app/(dashboard)/error.tsx +10 -2
- package/payload/app/(dashboard)/keys/actions.ts +2 -1
- package/payload/app/not-found/README.md +4 -4
- 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/assistant.md +1 -1
- package/payload/docs/benchmark.md +6 -6
- package/payload/docs/distribution.md +18 -16
- 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 +26 -3
- package/payload/scripts/brand.ts +0 -1
- package/payload/scripts/check.ts +71 -1
- package/payload/scripts/contrast.ts +4 -4
- package/payload/scripts/fake-llm.ts +16 -2
- package/payload/scripts/keyboard.ts +31 -4
- package/payload/scripts/registry.ts +3 -3
- package/payload/scripts/tokens.ts +5 -5
- package/payload/scripts/verify.config.ts +14 -1
- package/payload/scripts/verify.ts +75 -3
- package/payload/skills/zz-meridian/references/customize.md +13 -0
- package/payload/skills/zz-meridian/references/existing-project.md +17 -0
- package/payload/skills/zz-meridian/references/validation.md +4 -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 +2 -1
- package/payload/src/components/base/shell/index.tsx +14 -4
- package/payload/src/components/base/surface/index.tsx +1 -1
- package/payload/src/components/charts/sparkline/index.tsx +3 -0
- package/payload/src/components/charts/timeline/README.md +3 -1
- package/payload/src/components/charts/timeline/index.tsx +9 -1
- 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 +9 -2
- package/payload/src/components/patterns/assistant/index.tsx +23 -20
- package/payload/src/components/patterns/assistant/launcher.tsx +28 -0
- package/payload/src/components/patterns/assistant/preview.tsx +4 -2
- package/payload/src/components/patterns/assistant/text.tsx +25 -0
- package/payload/src/components/patterns/command-palette/README.md +1 -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/README.md +3 -3
- package/payload/src/components/patterns/filter-bar/index.tsx +12 -8
- package/payload/src/components/patterns/form-section/README.md +5 -1
- package/payload/src/components/patterns/form-section/index.tsx +71 -36
- package/payload/src/components/patterns/form-section/preview.tsx +17 -0
- package/payload/src/components/patterns/metric-tile/index.tsx +2 -9
- package/payload/src/components/patterns/period-select/index.tsx +4 -3
- package/payload/src/components/patterns/rail/README.md +2 -0
- 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 +4 -4
- package/payload/src/components/ui/card/index.tsx +29 -3
- package/payload/src/components/ui/card/preview.tsx +25 -0
- 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 +3 -3
- package/payload/src/components/ui/segmented/index.tsx +4 -1
- 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/collection.ts +29 -5
- package/payload/src/lib/format.ts +22 -4
- package/payload/src/lib/host.ts +10 -0
- package/payload/src/lib/period.ts +11 -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/src/views/not-found-address.tsx +4 -4
- package/payload/src/views/not-found.tsx +10 -2
- package/payload/tests/agents.test.tsx +29 -0
- package/payload/tests/assistant-text.test.tsx +40 -0
- package/payload/tests/collection.test.ts +19 -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/.env.example
CHANGED
|
@@ -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".
|
package/payload/CHANGELOG.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
|
@@ -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=
|
|
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,
|
|
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:
|
|
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
|
|
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
|
|
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=
|
|
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
|
|
|
@@ -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
|
|
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,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
|
|
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)
|
|
139
139
|
|
|
140
|
-
npm configures a trusted publisher only on a package that exists
|
|
141
|
-
|
|
142
|
-
`
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
153
|
+
`[Unreleased]` section becomes the next release.
|
|
152
154
|
|
|
153
155
|
## Phases
|
|
154
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;
|