zz-meridian 0.16.0 → 0.16.2
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/README.md +3 -1
- package/dist/adopt.js +39 -15
- package/dist/cli.js +7 -0
- package/dist/command-flags.js +38 -0
- package/dist/files.js +17 -4
- package/dist/migrations.js +54 -6
- package/dist/rebrand.js +13 -2
- package/dist/session.js +4 -1
- package/package.json +1 -1
- package/payload/CHANGELOG.md +57 -1
- package/payload/CONTRIBUTING.md +1 -1
- package/payload/app/(dashboard)/(home)/README.md +2 -2
- package/payload/app/(dashboard)/engineering/README.md +1 -1
- package/payload/app/(dashboard)/keys/page.tsx +2 -2
- package/payload/app/(dashboard)/members/page.tsx +2 -2
- package/payload/app/(dashboard)/operations-tickets/page.tsx +3 -3
- package/payload/app/embed/error.tsx +30 -0
- package/payload/app/embed/home/page.tsx +17 -3
- package/payload/app/embed/home/view.tsx +7 -3
- package/payload/app/embed/now/page.tsx +16 -3
- package/payload/app/embed/requests/page.tsx +16 -3
- package/payload/app/system/(atlas)/page.tsx +17 -6
- package/payload/app/system/preview/[section]/[card]/stage.tsx +13 -0
- package/payload/decisions/0006-quiet-light.md +1 -1
- package/payload/docs/distribution.md +4 -3
- package/payload/docs/product-workspaces.md +2 -2
- package/payload/docs/surfaces.md +5 -1
- package/payload/package.json +1 -1
- package/payload/scripts/assistant.ts +1 -1
- package/payload/scripts/audit.ts +24 -11
- package/payload/scripts/check.ts +6 -2
- package/payload/scripts/keyboard.ts +1 -1
- package/payload/scripts/lib/data-url.ts +86 -0
- package/payload/scripts/verify.config.ts +2 -2
- package/payload/scripts/verify.ts +7 -19
- package/payload/scripts/vitals.ts +40 -14
- package/payload/skills/zz-meridian/references/agents.md +2 -2
- package/payload/skills/zz-meridian/references/cache.md +7 -5
- package/payload/skills/zz-meridian/references/existing-project.md +2 -1
- package/payload/skills/zz-meridian/references/standard.md +1 -1
- package/payload/skills/zz-meridian/references/update.md +2 -1
- package/payload/skills/zz-meridian/references/validation.md +9 -5
- package/payload/src/app.config.ts +4 -2
- package/payload/src/components/base/app-mark/index.tsx +11 -2
- package/payload/src/components/base/shell/index.tsx +11 -18
- package/payload/src/components/base/use-nav-drawer.ts +32 -0
- package/payload/src/components/charts/column-chart/preview.tsx +5 -6
- package/payload/src/components/charts/scale.ts +19 -3
- package/payload/src/components/charts/trend-chart/README.md +22 -7
- package/payload/src/components/charts/trend-chart/index.tsx +103 -23
- package/payload/src/components/charts/trend-chart/preview.tsx +31 -0
- package/payload/src/components/patterns/command-palette/index.tsx +5 -3
- package/payload/src/components/patterns/data-table/README.md +3 -2
- package/payload/src/components/patterns/data-table/index.tsx +24 -10
- package/payload/src/components/patterns/embed-frame/README.md +2 -0
- package/payload/src/components/patterns/embed-frame/index.tsx +71 -0
- package/payload/src/components/patterns/metric-tile/README.md +2 -1
- package/payload/src/components/patterns/metric-tile/index.tsx +8 -4
- package/payload/src/components/patterns/rail/README.md +1 -1
- package/payload/src/components/patterns/rail/index.tsx +6 -4
- package/payload/src/components/patterns/shell-tools/index.tsx +1 -1
- package/payload/src/components/ui/copy-field/index.tsx +13 -1
- package/payload/src/components/ui/dialog/README.md +1 -2
- package/payload/src/components/ui/pagination/README.md +1 -1
- package/payload/src/components/ui/select/index.tsx +5 -3
- package/payload/src/components/ui/skeleton/README.md +1 -1
- package/payload/src/components/ui/table/README.md +1 -1
- package/payload/src/components/ui/table/index.tsx +15 -3
- package/payload/src/components/ui/table/preview.tsx +2 -2
- package/payload/src/components/ui/tabs/README.md +1 -1
- package/payload/src/components/ui/tabs/index.tsx +4 -3
- package/payload/src/data/metrics.ts +6 -4
- package/payload/src/data/read.ts +7 -5
- package/payload/src/data/requests.ts +12 -6
- package/payload/src/lib/assistant/respond.ts +5 -0
- package/payload/src/lib/explore.ts +22 -4
- package/payload/src/lib/facts.ts +6 -2
- package/payload/src/lib/format-date.ts +9 -0
- package/payload/src/lib/format.ts +6 -4
- package/payload/src/styles/base.css +7 -0
- package/payload/src/styles/motion.css +2 -1
- package/payload/src/system/atlas-shell.tsx +12 -14
- package/payload/src/views/home-context.ts +39 -11
- package/payload/src/views/home-parts.ts +12 -4
- package/payload/src/views/home.tsx +17 -11
- package/payload/src/views/keys.tsx +3 -2
- package/payload/src/views/members.tsx +13 -2
- package/payload/src/views/now-context.ts +3 -3
- package/payload/src/views/now-parts.ts +3 -1
- package/payload/src/views/now.tsx +18 -4
- package/payload/src/views/requests.tsx +4 -3
- package/payload/src/views/revenue-context.ts +4 -3
- package/payload/src/views/revenue.tsx +4 -5
- package/payload/src/views/services.tsx +11 -2
- package/payload/src/views/settings.tsx +20 -5
- package/payload/src/views/tickets.tsx +13 -1
- package/payload/src/views/tools.ts +2 -2
- package/payload/src/views/usage-context.ts +2 -3
- package/payload/src/views/usage-parts.ts +2 -1
- package/payload/src/views/usage.tsx +5 -5
- package/payload/tests/app-mark-sheen.test.tsx +28 -0
- package/payload/tests/assistant-access.test.ts +12 -2
- package/payload/tests/assistant-respond.test.ts +31 -0
- package/payload/tests/assistant-route.test.ts +4 -0
- package/payload/tests/assistant-thread-scope.test.tsx +2 -1
- package/payload/tests/atlas-template-contracts.test.tsx +2 -2
- package/payload/tests/chart-scale.test.ts +41 -0
- package/payload/tests/copy-field.test.tsx +48 -0
- package/payload/tests/customer-figures.test.ts +5 -2
- package/payload/tests/data-table.test.tsx +24 -4
- package/payload/tests/embed-page-failure.test.tsx +25 -0
- package/payload/tests/engineering-workflows.test.ts +2 -13
- package/payload/tests/explore-answer-length.test.ts +83 -0
- package/payload/tests/explore-runner.test.ts +1 -1
- package/payload/tests/facts.test.ts +15 -0
- package/payload/tests/feature-definitions.test.ts +2 -4
- package/payload/tests/featured-metric.test.tsx +1 -1
- package/payload/tests/format.test.ts +5 -0
- package/payload/tests/forms.test.tsx +20 -0
- package/payload/tests/key-value.test.tsx +7 -1
- package/payload/tests/keys-gate.test.tsx +7 -1
- package/payload/tests/member-mutations.test.ts +6 -2
- package/payload/tests/member-roster-boundary.test.ts +73 -0
- package/payload/tests/members-optimistic.test.tsx +8 -3
- package/payload/tests/navigate-menu-start.test.ts +6 -4
- package/payload/tests/no-access.test.tsx +12 -1
- package/payload/tests/page-gates.test.tsx +7 -1
- package/payload/tests/pages-read-every-row.test.tsx +91 -0
- package/payload/tests/prose-memo.test.tsx +16 -4
- package/payload/tests/rail-marker.test.tsx +11 -2
- package/payload/tests/read-all-query.test.ts +43 -0
- package/payload/tests/read-every-row.test.ts +66 -0
- package/payload/tests/requests-export.test.ts +13 -0
- package/payload/tests/requests-query.test.ts +15 -0
- package/payload/tests/sample.test.ts +1 -1
- package/payload/tests/settings-access.test.tsx +3 -0
- package/payload/tests/shared-context.test.tsx +30 -10
- package/payload/tests/titles.test.ts +7 -0
- package/payload/tests/trend-chart-band.test.tsx +117 -0
- package/payload/tests/uptime-bars-end.test.tsx +6 -1
- package/payload/tests/usage-summary.test.tsx +3 -1
- package/payload/tests/verify-data-url.test.ts +122 -0
- package/payload/tests/verify-personas.test.ts +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,9 @@ Brand flags: `--name "Acme Ops"`, `--hex '#2E6BE4'` (or `--accent indigo|cobalt|
|
|
|
39
39
|
in a page's kicker ("Production"); it does not touch the navigation workspaces.
|
|
40
40
|
|
|
41
41
|
**adopt** copies Meridian's tokens, styles, components, gates and scripts into `src/`, `tokens/` and `scripts/`;
|
|
42
|
-
merges the dependencies and scripts it needs into your `package.json`;
|
|
42
|
+
merges the dependencies and scripts it needs into your `package.json`; adds the `@meridian/*` alias to your
|
|
43
|
+
`tsconfig.json` (yours is kept as `tsconfig.before.json` when it carries comments or trailing commas, which JSON
|
|
44
|
+
cannot hold); replaces your global stylesheet (yours is kept
|
|
43
45
|
beside it as `*.before.css`); brands it; adds Meridian's managed block to your `AGENTS.md` between two markers, keeping your own text there byte
|
|
44
46
|
for byte; writes an empty `docs/brief.md` (your product, users, data, decisions and glossary) if you have none; installs the skill into
|
|
45
47
|
`.agents/skills/` (Codex) and `.claude/skills/` (Claude Code); records every Meridian file it copied in `.meridian/manifest.json`;
|
package/dist/adopt.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { VERSION, brandArgs, effectiveBrand, inside, installSkill, packageManager, payloadBytes, payloadFiles, readJsonc, readPayload, relativeImports, run, runTool, sha256, writeIn, writeManifest, } from './files.js';
|
|
11
|
+
import { VERSION, brandArgs, effectiveBrand, inside, installSkill, jsoncExtras, packageManager, payloadBytes, payloadFiles, readJsonc, readPayload, relativeImports, run, runTool, sha256, writeIn, writeManifest, } from './files.js';
|
|
12
12
|
import { BRIEF_TEMPLATE, managedBlock, upsertManagedBlock } from './context.js';
|
|
13
13
|
import { FIXED, adoptSetOf, managedPaths } from './ownership.js';
|
|
14
14
|
/** The template's dependencies a dashboard built from these files needs. */
|
|
@@ -123,6 +123,39 @@ export default defineConfig({
|
|
|
123
123
|
});
|
|
124
124
|
`;
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* What `adopt` writes for a team's `tsconfig.json`, and what it must say about it: the file Meridian's code needs (its
|
|
128
|
+
* scripts import with `.ts` extensions, its code is ES2022, `@meridian/*` reaches its files), the team's own kept beside
|
|
129
|
+
* it when it carries what `JSON.stringify` cannot hold, and what their `@/*` alias stands for. A function rather than a
|
|
130
|
+
* block inside `adopt` so a test can hold it to those three: a test that ran the whole `adopt` would need the template
|
|
131
|
+
* the payload carries, which a checkout has only after a build, and it would be testing the copy, not this decision.
|
|
132
|
+
*/
|
|
133
|
+
export function tsconfigWrites(tsPath) {
|
|
134
|
+
const writes = new Map();
|
|
135
|
+
const notes = [];
|
|
136
|
+
const ts = readJsonc(tsPath);
|
|
137
|
+
ts.compilerOptions ??= {};
|
|
138
|
+
ts.compilerOptions.allowImportingTsExtensions = true;
|
|
139
|
+
ts.compilerOptions.noEmit = true;
|
|
140
|
+
if (!/^es20(2[2-9]|[3-9]\d)|^esnext$/i.test(ts.compilerOptions.target ?? '')) {
|
|
141
|
+
notes.push(`raised tsconfig target from ${ts.compilerOptions.target ?? 'its default'} to ES2022`);
|
|
142
|
+
ts.compilerOptions.target = 'ES2022';
|
|
143
|
+
}
|
|
144
|
+
ts.compilerOptions.paths ??= {};
|
|
145
|
+
ts.compilerOptions.paths['@meridian/*'] = ['./src/*'];
|
|
146
|
+
const at = ts.compilerOptions.paths['@/*']?.[0];
|
|
147
|
+
const atAlias = typeof at === 'string' ? at.replace(/\/\*$/, '').replace(/^\.\//, '') || '.' : null;
|
|
148
|
+
// A tsconfig may carry comments and trailing commas, which `next dev` keeps (`comment-json`) and `JSON.stringify`
|
|
149
|
+
// cannot hold. What the team wrote is kept beside the rewritten file, as the stylesheet and the postcss config are,
|
|
150
|
+
// rather than dropped in silence — the note says so, and there is no backup to explain when there was nothing to lose.
|
|
151
|
+
const was = fs.readFileSync(tsPath, 'utf8');
|
|
152
|
+
writes.set('tsconfig.json', JSON.stringify(ts, null, 2) + '\n');
|
|
153
|
+
if (jsoncExtras(was)) {
|
|
154
|
+
writes.set('tsconfig.before.json', was);
|
|
155
|
+
notes.push('tsconfig.json carried comments or trailing commas, which the rewritten file cannot: yours is tsconfig.before.json');
|
|
156
|
+
}
|
|
157
|
+
return { writes, notes, atAlias };
|
|
158
|
+
}
|
|
126
159
|
export function adopt(o) {
|
|
127
160
|
const { root } = o;
|
|
128
161
|
const say = (s) => console.log(s);
|
|
@@ -230,20 +263,11 @@ Rename or move them, then run adopt again.`);
|
|
|
230
263
|
const tsPath = path.join(root, 'tsconfig.json');
|
|
231
264
|
let atAlias = null;
|
|
232
265
|
if (fs.existsSync(tsPath)) {
|
|
233
|
-
const ts =
|
|
234
|
-
ts.
|
|
235
|
-
|
|
236
|
-
ts.
|
|
237
|
-
|
|
238
|
-
notes.push(`raised tsconfig target from ${ts.compilerOptions.target ?? 'its default'} to ES2022`);
|
|
239
|
-
ts.compilerOptions.target = 'ES2022';
|
|
240
|
-
}
|
|
241
|
-
ts.compilerOptions.paths ??= {};
|
|
242
|
-
ts.compilerOptions.paths['@meridian/*'] = ['./src/*'];
|
|
243
|
-
const at = ts.compilerOptions.paths['@/*']?.[0];
|
|
244
|
-
if (typeof at === 'string')
|
|
245
|
-
atAlias = at.replace(/\/\*$/, '').replace(/^\.\//, '') || '.';
|
|
246
|
-
writes.set('tsconfig.json', JSON.stringify(ts, null, 2) + '\n');
|
|
266
|
+
const ts = tsconfigWrites(tsPath);
|
|
267
|
+
for (const [f, text] of ts.writes)
|
|
268
|
+
writes.set(f, text);
|
|
269
|
+
notes.push(...ts.notes);
|
|
270
|
+
atAlias = ts.atAlias;
|
|
247
271
|
}
|
|
248
272
|
else {
|
|
249
273
|
return fail('no tsconfig.json: Meridian is TypeScript. Add one (next dev writes it), then run adopt again.');
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import os from 'node:os';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
9
|
import { adopt } from './adopt.js';
|
|
10
|
+
import { misapplied } from './command-flags.js';
|
|
10
11
|
import { create } from './create.js';
|
|
11
12
|
import { BRAND_FLAGS, VERSION, installSkill } from './files.js';
|
|
12
13
|
import { brandCommand } from './rebrand.js';
|
|
@@ -75,6 +76,12 @@ function main() {
|
|
|
75
76
|
console.log(HELP);
|
|
76
77
|
return command || values.help ? 0 : 1;
|
|
77
78
|
}
|
|
79
|
+
// A flag a command does not read is refused, never ignored: `adopt --dry-run` wrote the project rather than previewing it.
|
|
80
|
+
const wrong = misapplied(command, flags);
|
|
81
|
+
if (wrong) {
|
|
82
|
+
console.error(`zz-meridian ${command}: --${wrong.flag} belongs to ${wrong.belongsTo}`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
78
85
|
if (command === 'adopt')
|
|
79
86
|
return adopt({ root: process.cwd(), brand, allowDirty: Boolean(values['allow-dirty']), install });
|
|
80
87
|
if (command === 'create') {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which flags each command reads. A flag that names a mode or a value belongs to the command that acts on it: anywhere
|
|
3
|
+
* else it is refused by name rather than accepted and ignored, so `adopt --dry-run` never modifies a project while the
|
|
4
|
+
* person waits for a preview.
|
|
5
|
+
*/
|
|
6
|
+
import { BRAND_FLAGS } from './files.js';
|
|
7
|
+
/** Every command reads these. */
|
|
8
|
+
const EVERYWHERE = ['help', 'version'];
|
|
9
|
+
/** The brand flags, read by the commands that write a brand. */
|
|
10
|
+
const BRAND = BRAND_FLAGS;
|
|
11
|
+
/** What each command reads, beyond the two above. */
|
|
12
|
+
const ACCEPTS = {
|
|
13
|
+
adopt: [...BRAND, 'allow-dirty', 'no-install'],
|
|
14
|
+
create: [...BRAND, 'no-install'],
|
|
15
|
+
update: ['allow-dirty', 'no-install', 'dry-run', 'resume', 'finalize', 'abort', 'verify', 'verbose'],
|
|
16
|
+
brand: [...BRAND, 'allow-dirty'],
|
|
17
|
+
skill: ['global'],
|
|
18
|
+
};
|
|
19
|
+
/** The commands that read one flag, in the CLI's own order, for the sentence that sends the person to the right one. */
|
|
20
|
+
function commandsReading(flag) {
|
|
21
|
+
return Object.keys(ACCEPTS).filter((c) => ACCEPTS[c].includes(flag));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The first flag a command does not read, with the command it belongs to; null when every flag is the command's own.
|
|
25
|
+
* `update` is the one command with no spelling of its own, so a flag only it reads is named as its own.
|
|
26
|
+
*/
|
|
27
|
+
export function misapplied(command, flags) {
|
|
28
|
+
const accepted = ACCEPTS[command];
|
|
29
|
+
if (!accepted)
|
|
30
|
+
return null;
|
|
31
|
+
for (const [flag, value] of Object.entries(flags)) {
|
|
32
|
+
if (value === undefined || EVERYWHERE.includes(flag) || accepted.includes(flag))
|
|
33
|
+
continue;
|
|
34
|
+
const others = commandsReading(flag);
|
|
35
|
+
return { flag, belongsTo: others.length ? `zz-meridian ${others.join(' or ')}` : 'no command' };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
package/dist/files.js
CHANGED
|
@@ -82,10 +82,10 @@ export function runTool(root, entry, args, quiet = true) {
|
|
|
82
82
|
return spawnSync(process.execPath, [path.join(root, 'node_modules', entry), ...args], { cwd: root, encoding: 'utf8', stdio: quiet ? 'pipe' : 'inherit' });
|
|
83
83
|
}
|
|
84
84
|
export const hasCommand = (cmd) => spawnSync(cmd, ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' }).status === 0;
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
const text = fs.readFileSync(file, 'utf8');
|
|
85
|
+
/** A JSONC document's text with the comments dropped, and whether anything had to be dropped to read it. */
|
|
86
|
+
function strippedJsonc(text) {
|
|
88
87
|
let out = '';
|
|
88
|
+
let dropped = false;
|
|
89
89
|
for (let i = 0, str = false; i < text.length; i++) {
|
|
90
90
|
const c = text[i];
|
|
91
91
|
if (str) {
|
|
@@ -102,12 +102,14 @@ export function readJsonc(file) {
|
|
|
102
102
|
continue;
|
|
103
103
|
}
|
|
104
104
|
if (c === '/' && text[i + 1] === '/') {
|
|
105
|
+
dropped = true;
|
|
105
106
|
while (i < text.length && text[i] !== '\n')
|
|
106
107
|
i++;
|
|
107
108
|
out += '\n';
|
|
108
109
|
continue;
|
|
109
110
|
}
|
|
110
111
|
if (c === '/' && text[i + 1] === '*') {
|
|
112
|
+
dropped = true;
|
|
111
113
|
i += 2;
|
|
112
114
|
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
|
|
113
115
|
i++;
|
|
@@ -116,8 +118,19 @@ export function readJsonc(file) {
|
|
|
116
118
|
}
|
|
117
119
|
out += c;
|
|
118
120
|
}
|
|
119
|
-
|
|
121
|
+
const json = out.replace(/,(\s*[}\]])/g, '$1');
|
|
122
|
+
return { json, dropped: dropped || json.length !== out.length };
|
|
120
123
|
}
|
|
124
|
+
/** JSON with the comments and trailing commas a tsconfig may carry. */
|
|
125
|
+
export function readJsonc(file) {
|
|
126
|
+
return JSON.parse(strippedJsonc(fs.readFileSync(file, 'utf8')).json);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Whether a project's file carries what plain JSON cannot hold: a comment, or a trailing comma. `next dev` writes a
|
|
130
|
+
* tsconfig through `comment-json`, which keeps both, so a tool that re-serialises one drops what the team's own
|
|
131
|
+
* toolchain preserves — and this is the question to ask before doing it.
|
|
132
|
+
*/
|
|
133
|
+
export const jsoncExtras = (text) => strippedJsonc(text).dropped;
|
|
121
134
|
/**
|
|
122
135
|
* Meridian's own `@/` imports, made relative to the file. An adopting project may map `@/` somewhere else, or already
|
|
123
136
|
* have a `components/ui/button`; a relative import can never resolve to the team's file instead of Meridian's.
|
package/dist/migrations.js
CHANGED
|
@@ -422,7 +422,23 @@ export const RELEASE_MIGRATIONS = [
|
|
|
422
422
|
instructions: 'The release renamed them with no redirect, and an update writes none of the team\'s pages, views or tests. Each file listed is one of them or names one: a page or a view, the view tools, `src/app.config.ts`\'s nav and the feature ids it names, the routes of `scripts/verify.config.ts`, or a shipped test that expects them. A project that kept the sample\'s pages as they were brings the release\'s over: `app/(dashboard)/` and `app/embed/`, `src/views/`, `src/data/`, `src/lib/assistant/` beside `prompt.ts`, `src/app.config.ts` with its own `app` kept, and each shipped test as the release has it; a file listed here that the release no longer has is deleted. A project that made the pages its own keeps its own names, makes the changes this release\'s other migrations list in them, and reads "not applicable" for the assistant walk-through\'s steps on Home and Now (`scripts/lib/sample.ts`).',
|
|
423
423
|
checks: GATE_AND_BUILD,
|
|
424
424
|
},
|
|
425
|
+
{
|
|
426
|
+
id: 'marker-measured-on-observe',
|
|
427
|
+
since: '0.16.2',
|
|
428
|
+
applies: railObserverNeverAnswers,
|
|
429
|
+
summary: 'A test mounts the rail over a `ResizeObserver` whose `observe` answers nothing: the rail takes its first measurement from the observer now, where it took one by hand.',
|
|
430
|
+
instructions: 'Bring `tests/rail-marker.test.tsx` over from the release as it has it. Since 0.16.2 `src/components/patterns/rail/index.tsx` reads no offset of its own — a rail mounts inside the tap that opens the phone drawer, and a read there lays the page out inside that tap — so the marker\'s first position is the one `observe` delivers, after the effect that called it. A test whose fake answers `observe` with an empty body expects the marker before any delivery and fails; the release\'s fake answers it with the callback, `observe() { for (const cb of observers) queueMicrotask(cb); }`, and reads the marker after that delivery. A test of your own that mounts the rail changes the same way.',
|
|
431
|
+
checks: ['gate'],
|
|
432
|
+
},
|
|
425
433
|
];
|
|
434
|
+
/**
|
|
435
|
+
* A test of the rail that stands over a fake `ResizeObserver` answering `observe` with nothing (0.16.2). The lookbehind
|
|
436
|
+
* keeps `unobserve() {}` out of it: a fake that answers the callback and also stubs `unobserve` is the release's own
|
|
437
|
+
* shape, and the one thing to bring over.
|
|
438
|
+
*/
|
|
439
|
+
function railObserverNeverAnswers(root) {
|
|
440
|
+
return matching(root, (rel, src) => /\.(?:test|spec)\.tsx?$/.test(rel) && /\bRail\b/.test(src) && /\bResizeObserver\b/.test(src) && /(?<![\w$.])observe\s*\(\s*\)\s*\{\s*\}/.test(src));
|
|
441
|
+
}
|
|
426
442
|
/** The team's files that import an export 0.16.0 removed from a managed module, by name or through a namespace. */
|
|
427
443
|
function removedExportReaders(root) {
|
|
428
444
|
const removed = { 'src/lib/insight': ['deviations', 'largest', 'runs', 'weekendRatio'], 'src/lib/collection': ['queryInput'] };
|
|
@@ -511,6 +527,37 @@ function refusalUndeclared(root) {
|
|
|
511
527
|
return null;
|
|
512
528
|
return /^export const refusal\b/m.test(body) ? null : [rel];
|
|
513
529
|
}
|
|
530
|
+
/** The nav item objects of a nav array, by brace: an object naming an `href` of its own, however many lines it is over. Braces inside a quoted value are not braces. */
|
|
531
|
+
function navItemObjects(nav) {
|
|
532
|
+
const found = [];
|
|
533
|
+
const open = [];
|
|
534
|
+
let quote = null;
|
|
535
|
+
for (let i = 0; i < nav.length; i++) {
|
|
536
|
+
const ch = nav[i];
|
|
537
|
+
if (quote) {
|
|
538
|
+
if (ch === '\\')
|
|
539
|
+
i++;
|
|
540
|
+
else if (ch === quote)
|
|
541
|
+
quote = null;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (ch === "'" || ch === '"' || ch === '`')
|
|
545
|
+
quote = ch;
|
|
546
|
+
else if (ch === '{')
|
|
547
|
+
open.push(i);
|
|
548
|
+
else if (ch === '}') {
|
|
549
|
+
const start = open.pop();
|
|
550
|
+
if (start === undefined)
|
|
551
|
+
continue;
|
|
552
|
+
const inner = nav.slice(start + 1, i);
|
|
553
|
+
// What is left once the objects inside it are taken out: a group's `href`s are all in its items, so a group has
|
|
554
|
+
// none of its own left and is not an item.
|
|
555
|
+
if (/\bhref\s*:/.test(inner.replace(/\{[^{}]*\}/g, '')))
|
|
556
|
+
found.push(inner);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return found;
|
|
560
|
+
}
|
|
514
561
|
/** The project's own nav, with any item still missing the `needs` that 0.13.0 requires of it. */
|
|
515
562
|
function navItemsWithoutNeeds(root) {
|
|
516
563
|
const rel = 'src/app.config.ts';
|
|
@@ -520,16 +567,17 @@ function navItemsWithoutNeeds(root) {
|
|
|
520
567
|
const nav = /export const nav\b[^=]*=\s*\[([\s\S]*?)\n\];/.exec(body)?.[1];
|
|
521
568
|
if (nav === undefined)
|
|
522
569
|
return null;
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
|
|
570
|
+
// The item is the object naming its `href`, read by its braces rather than by its line: a team that spreads an item
|
|
571
|
+
// over its keys put the `needs` two lines below the `href`, and a line-based read reported the migration for a file
|
|
572
|
+
// with none of the old shape left in it. A group is not a destination and carries no `needs`: its own `href`s belong
|
|
573
|
+
// to the items inside it, so an object whose braces hold only other objects is not read. Anything the file does
|
|
574
|
+
// beyond that is the team's own shape, and none of it is read.
|
|
575
|
+
const items = navItemObjects(nav);
|
|
528
576
|
// A project of 0.12.3's vintage also declares `NavItem` itself, without the property: an item that names a `needs` its
|
|
529
577
|
// own type does not have is the type error the gate prints next, so the change is not done until the type carries it.
|
|
530
578
|
const declaredType = /export type NavItem[^;]*\};/.test(body);
|
|
531
579
|
const typeHasNeeds = /export type NavItem[^;]*\bneeds\s*:/.test(body);
|
|
532
|
-
return items.some((
|
|
580
|
+
return items.some((item) => !/\bneeds\s*:/.test(item)) || (declaredType && !typeHasNeeds) ? [rel] : null;
|
|
533
581
|
}
|
|
534
582
|
/**
|
|
535
583
|
* The page specifications under a route group of the project's own, at the top of its app folder, that lack the part the
|
package/dist/rebrand.js
CHANGED
|
@@ -165,8 +165,19 @@ function attempt(ctx, change, allowDirty) {
|
|
|
165
165
|
}
|
|
166
166
|
/** The command: the real context, which replays the running release and runs its branding script in scratch. */
|
|
167
167
|
export function brandCommand({ root, flags, allowDirty }) {
|
|
168
|
-
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'zz-meridian-brand-'));
|
|
169
168
|
const log = (line) => (line.startsWith('zz-meridian brand:') ? console.error(line) : console.log(line));
|
|
169
|
+
// The flags are read before anything is copied, and a value the branding script refuses is refused here with the
|
|
170
|
+
// command's own sentence: `adopt` and `create` read the same flags through `effectiveBrand` and answer its Error
|
|
171
|
+
// (`adopt.ts:153`, `create.ts:19`), where `brand` let it out as a stack trace.
|
|
172
|
+
let brand;
|
|
173
|
+
try {
|
|
174
|
+
brand = effectiveBrand(flags);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
log(`zz-meridian brand: ${e.message}`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'zz-meridian-brand-'));
|
|
170
181
|
const replays = new Map();
|
|
171
182
|
let n = 0;
|
|
172
183
|
try {
|
|
@@ -204,7 +215,7 @@ export function brandCommand({ root, flags, allowDirty }) {
|
|
|
204
215
|
return fs.readFileSync(path.join(copy, APP_CONFIG), 'utf8');
|
|
205
216
|
},
|
|
206
217
|
};
|
|
207
|
-
return rebrand(ctx,
|
|
218
|
+
return rebrand(ctx, brand, { allowDirty });
|
|
208
219
|
}
|
|
209
220
|
finally {
|
|
210
221
|
fs.rmSync(scratch, { recursive: true, force: true });
|
package/dist/session.js
CHANGED
|
@@ -265,8 +265,11 @@ function install(ctx, session, j, pm) {
|
|
|
265
265
|
const { root } = ctx;
|
|
266
266
|
const lock = lockfileFor(root, pm);
|
|
267
267
|
const prior = j.operations.find((o) => o.path === lock && o.disposition === 'team-preserved' && o.applied);
|
|
268
|
-
const before = prior ? prior.base : probe(root, lock).side;
|
|
269
268
|
const backup = path.join(session, 'backup', lock);
|
|
269
|
+
// The preimage is what the lockfile was before this update's install, and the first run wrote it here before running
|
|
270
|
+
// one. A resumed run that probed the project instead takes whatever a killed install left behind as "ours", and an
|
|
271
|
+
// abort would then put a half-written lockfile back in place of the team's.
|
|
272
|
+
const before = prior ? prior.base : fs.existsSync(backup) ? sideOf(sha(fs.readFileSync(backup))) : probe(root, lock).side;
|
|
270
273
|
if (before.exists && !fs.existsSync(backup))
|
|
271
274
|
writeAtomic(backup, fs.readFileSync(inside(root, lock)));
|
|
272
275
|
const t0 = performance.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
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,63 @@
|
|
|
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.16.2] · 2026-10-16
|
|
6
|
+
|
|
7
|
+
### Breaking
|
|
8
|
+
|
|
9
|
+
- **A created project's shipped test of the rail fails until it is brought over.** `tests/rail-marker.test.tsx` — the template's test of the rail's marker — stands over a `ResizeObserver` whose `observe` answers with nothing, which only worked because the rail read its own offsets as well. That read is what 0.16.2 removed (see **Changed**), so the file now asserts a marker position that nothing has measured yet. The file is the project's own from the moment it is created, so the update does not write it: it reports it as the migration `marker-measured-on-observe`, with the fix — bring the release's `tests/rail-marker.test.tsx` over as the release has it. Its fake answers `observe` with the callback and reads the marker after that delivery; a test of your own that mounts the rail answers `observe` the same way.
|
|
10
|
+
- **The Web Vitals check fails on a dropped frame.** `scripts/vitals.ts` counted frames over 50 ms while its taps played out and printed the number on every line without ever failing on it, so a `verify --full` that passed was not being held to the floor its own header states. It fails now: `frames over 50 ms` must read `0 of N`. Two things about how it counts changed with it. Frames are read **in the tap's own window and no other** — the recording used to run from the moment the page opened, straight through the Escape that closes what a tap opened and the settle after it, which the INP measurement in the same loop already excludes because it is the check's own work and not the person's. And a page that drops one frame is **measured a second time in full** before it is failed, because a single frame in two hundred is inside the noise of the host a four-times throttle is measuring: a page that drops one twice is a page that stutters. A run that was passing still passes; a run that was failing now says so, and names the control the frame was dropped on.
|
|
11
|
+
|
|
12
|
+
### New
|
|
13
|
+
|
|
14
|
+
- **`useNavDrawer(asked)`** (`src/components/base/use-nav-drawer.ts`) — whether a navigation drawer is open, for the frame that owns one. It carries the bookkeeping both shells had written for themselves: the drawer opens for every press of the top bar's button that it has not answered yet, remembers the page it opened on so a navigation closes it, and forgets that page when the address moves. The Atlas's copy was missing the last of those, so opening the contents on a phone, following a link and pressing Back re-opened the drawer with nobody pressing anything. A product that wrote the same bookkeeping for a drawer of its own should read it from here.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **The rail reads its own geometry from the observer's first delivery, not by hand.** The rail measured itself with an `offsetTop` read as well as through its `ResizeObserver`, and a rail mounts inside the tap that opens the phone drawer — so that read made the browser lay the whole page out inside the tap: measured at a 50 ms frame on a throttled phone, against 33 ms for the mount alone. The observer's first delivery reports the same numbers after the browser has laid out for itself. Nothing moves while a measurement is missing: the current item draws the same pill itself.
|
|
19
|
+
- **The Atlas's front page names the latest release, not the work toward the next one.** It read the first section of `CHANGELOG.md` — which is `[Unreleased]` while a release is pending, and has no version — so the card showed pending work under the version it was going to be released as. It also read the version, the date and the summary with one pattern that refused the `#` of `### Breaking`, the first line under every heading in that file: the date fell to an empty fallback and the card printed `0.16.2 · ` with nothing after the separator, on every release. The heading and the paragraph are read separately now, and the "Full changelog" link no longer shares the kicker's line.
|
|
20
|
+
- **`AppMark` names its own gradient.** The mark is a client component now, because each draws a sheen whose id has to be its own: a page holds more than one mark (the Atlas draws one in the phone's bar and one in the shelf, both mounted at every width), and a fixed `id` was a duplicate on every one of those pages, with a `url(#…)` naming two elements. A product that overrode the mark's markup rather than its `app.logo` is unaffected; the first load of `/embed/now` in this template moved 809 → 811 KiB.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **A new dashboard's own `pnpm verify` failed on a test that named the template.** `tests/embed-page-failure.test.tsx` asserts the embed's way out, and wrote the copy out as `Open in ZZ Meridian` — the template's name, while the frame draws `Open in ${app.name}`. Every dashboard is created under its own name, so the test passed in this repository and failed in every project made from it, at the first `pnpm verify` a team runs. It reads the name from `src/app.config.ts` now.
|
|
25
|
+
|
|
26
|
+
## [0.16.1] · 2026-10-16
|
|
27
|
+
|
|
28
|
+
### Breaking
|
|
29
|
+
|
|
30
|
+
- **The literal-colour rule reads more forms.** `scripts/check.ts` matched `#rrggbb`, `#rgb`, `rgb(`/`rgba(` and `hsl(`/`hsla(`. It now reads a hex of four or eight digits too — `#0a5F`, `#0a58caFF` — and `oklch()`, `oklab()`, `lab()`, `lch()` and `color()`. A gate that passed can fail on a line that writes one of them: use a role or a token (`text-ink`, `border-line`, `var(--…)`), or mark the line `allow-literal-colour` where the colour is genuinely a literal of its own, as an embed's simulated host and a third party's brand are. A colour composed from tokens is not a literal and is not reported, so `color-mix(in oklch, var(--ink) 40%, transparent)` stays as it is.
|
|
31
|
+
- **A flag a command does not read is refused, not ignored.** `zz-meridian adopt --dry-run` — and any other flag named outside the command that reads it — exits 1, naming the command it belongs to. A CI step that passed one did nothing quietly; it fails now, which is what it should have done.
|
|
32
|
+
- **A figure nobody measured is `null`, not zero.** `RequestSummary.p95` and `RequestSummary.errorShare` (`src/data/requests.ts`) are `null` when the set is empty, and `MetricTile.points` is `(number | null)[]`; `NowData`'s per-model `p95` and `errorRate` are `null` for a model with no usage row. A page, a fixture or a test of yours that builds one of them adds the `null` case; a tile whose point is `null` draws a gap where it drew a fall to zero, and the request log says "not measured" where it said `0ms` and `0.0%`.
|
|
33
|
+
- **The audit's two truncated-value rules mean what the card contract says.** `scripts/audit.ts` accepted any `title` anywhere near a cut value without comparing it with the text, and read `aria-labelledby` as the attribute's own value instead of resolving the ids it names. Both compare now, so a `verify --full` that passed can fail: put the whole of a cut value in a `title` that *holds* it (a title of something else keeps none of it), and name a real element with `aria-labelledby` — an id that is nowhere is a control with no name. `CONTRIBUTING.md` promised both all along.
|
|
34
|
+
- **The backend gate refuses a data URL it cannot read the host of.** `unsafeBackend` (`scripts/verify.ts`) skipped a value it could not read the host of, which is how a DSN written without a scheme (`user:pass@db.prod.example.com:5432/app`), a password holding a `/` or a `#`, or a libpq line (`host=… user=…`) reached the presses. The authority is read with its credentials dropped, a value whose host still cannot be read is refused rather than skipped, and a host named in the query string is read the way libpq reads it — `postgres:///app?host=db.prod.example.com` reaches that host, since libpq takes the host from the query and lets it override the authority, dials `hostaddr` when both are named, keeps the last value of a keyword it is given twice, and treats `/…` as a Unix socket, which stays local. A file scheme that names a host is another machine's file (`file://db.prod.example.com/share/app.db`) or a database over the network (`libsql://…`) rather than a file of this machine's. A refusal names the way out: point the value at a local copy for the run, or set `allowRemoteData: true` in `scripts/verify.config.ts`.
|
|
35
|
+
|
|
36
|
+
### New
|
|
37
|
+
|
|
38
|
+
- **`readAll(name, q)`** (`src/data/read.ts`) answers with every row a collection holds, asking the same question of every page: a list that counts, totals or claims "every" starts there, and a `limit` passed to it is ignored, because its answer means every row. `readEndpoints`, `readServices` and the incident read that joins each ticket go through it, and so do the Members and the Keys view tools and the three pages that list them: the endpoint list, the incidents the bell and Services count, every member, every key and every service are now all of them, where each was the first hundred. A page of yours that read one page of a collection and called it every row reads through `readAll` too.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **Home says no service is monitored yet.** A services read that succeeded and returned no rows had the page's title and its line say "every service is operational" while the page's own fact said "0 of 0 operational". Both say the empty state now, in the words the Services and Now pages already use.
|
|
43
|
+
- **`adopt` keeps a commented tsconfig beside the rewritten one.** `adopt` rewrites `tsconfig.json` to raise the target and add the `@meridian/*` path, and `JSON.stringify` cannot hold a comment or a trailing comma. When the team's file carries either, it is kept as `tsconfig.before.json` and the notes say so; a plain tsconfig gets no backup.
|
|
44
|
+
- **`brand` reports a flag it cannot use.** `brand --theme purple` printed an uncaught `Error` where `adopt` and `create` answer the same value with a sentence. All three report it the same way now, before anything is copied.
|
|
45
|
+
- **The standard names three motion loops.** The token layer defines three (`--dur-loop-spin`, `--dur-loop-shimmer`, `--dur-loop-pulse`) and the Motion floor and the Skeleton card named two. All three are named everywhere now, and `tests/motion-loops.test.ts` holds the count to the tokens.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **A held-back answer agrees with itself.** The line an answer ends with when the 3,000-character cap holds part of it back read "1 more line are not shown."; it reads "1 more line is not shown.", and the row messages follow.
|
|
50
|
+
- **A resumed update keeps the lockfile preimage a killed install left behind.** `--resume` probed the project for the team's lockfile, so a lockfile a killed install had rewritten stood in as "ours" and an abort would have put a half-written file back in place of the team's. It comes from the session's backup now.
|
|
51
|
+
- **The prepared query's 3,000-character cap bounds the whole answer.** It bounded the drilled rows, the one part it could shorten, so a summary over budget by itself came back over.
|
|
52
|
+
- **`nav-needs` reads a nav item by its braces.** An item a team had spread over its keys was reported as missing a `needs` that was two lines below its `href`.
|
|
53
|
+
- **Copy says "Copied" only when the copy happened.** A refused clipboard write left the button saying it had copied.
|
|
54
|
+
- **The short date is one function.** `formatShortDate` (`01 Oct`) and `formatDay` (`3 Oct`) in `src/lib/format-date.ts`; six copies of the same expression in five views and a chart preview are gone, and four sentences that wrote the day in two different ways agree.
|
|
55
|
+
- **A project created in another timezone passes its own gate.** A shipped test pinned the template's timezone; it states the project's own now, so `create --timezone Europe/London` no longer fails the gate it ships with.
|
|
56
|
+
- **A credit reads as a credit.** `formatCost` wrote `$-0.0050` for an amount under a cent; the sign goes before the currency, as `-$0.0050`.
|
|
57
|
+
- **Home's focus line states no ratio against a median of zero**, where it printed `NaN×`.
|
|
58
|
+
- **A required control inside a `Field` is announced as required.** A `Select` given `required`, or inside `<Field required>`, carries `aria-required` on its trigger, where nothing announced it.
|
|
59
|
+
- **Three checks in `scripts/` say what they measured.** A failed performance run shows in the coverage line rather than only in the phase; the keyboard walk prints "and the same in reverse" only when it compared the reverse walk; and `audit --atlas` reads its key list from this repository rather than from the folder it was run in. The pagination card's README no longer claims a page size in the address that the table does not keep.
|
|
60
|
+
- **The 0.16.0 section below was corrected in place.** Revenue's own needs were stated twice and one of them wrongly; nothing a product renders changed.
|
|
61
|
+
|
|
5
62
|
## [0.16.0] · 2026-10-15
|
|
6
63
|
|
|
7
64
|
### Breaking
|
|
@@ -34,7 +91,6 @@ Every release of ZZ Meridian, newest first. Versions follow semver: a removed or
|
|
|
34
91
|
- **A request is priced at its model's price.** `usageOf()` prices a request from the `models` records and from no list of its own: its tokens at its model's prices a million, the price of a call when it reached no model, nothing when it was refused. The list of $3 and $15 a million tokens and $0.10 a thousand calls is gone, so every cost on a request's page moved.
|
|
35
92
|
- **The ticket boundary's refusal for a fixed field has new words.** It reads "A ticket's id, its times and its incident are set by the system and cannot be changed." A test of yours that matches the sentence matches the new one.
|
|
36
93
|
- **Home, Usage and Revenue hand their pages a `head`.** `HomeData`, `UsageData` and `RevenueData` each hold `head`, what the page states at its head: `{ title, facts, unknown? }` (`PageHead` in `src/views/fact-insight.ts`), worked out on the server by the view tool's read. Home's `head.facts` are the facts of a year, and `HomeData` holds `line`, the line under its title, where it held `unwell`. A fixture or a test of yours that builds one of the three adds them: `head: { title: null, facts: [] }` draws a page headed by its name, and `unwell: null` says nothing of the services.
|
|
37
|
-
- **Revenue asks for the days.** `FEATURES.revenue.needs` is `{ allOf: ['customers:read', 'days:read'] }`, because its title reads them. Every role of the sample that reads customers reads the days too. A role of yours that holds `customers:read` alone no longer opens `/business`: grant it `days:read`.
|
|
38
94
|
- **Now and Services hand their pages a `head`.** `NowData` and `ServicesData` each hold `head`, the page's title and the days inside the band (`PageHead` in `src/views/fact-insight.ts`), worked out on the server by the view tool's read. A fixture or a test of yours that builds one of them adds it: `head: { title: null, facts: [] }` draws a page headed by its name.
|
|
39
95
|
- **Now and Services ask for more.** `FEATURES.now.needs` is `{ allOf: ['services:read', 'incidents:read', 'releases:read', 'tests:read', 'days:read', 'usage:read', 'models:read', 'compute:read', 'endpoints:read', 'tickets:read', 'activity:read'] }` and `FEATURES.services.needs` is `{ allOf: ['services:read', 'incidents:read', 'days:read'] }`: Now's title counts the releases in testing, which their verification runs decide, and both contexts count the days inside the band. Every role of the sample that reads the services holds them. A role of yours that does not no longer opens `/engineering` or `/services`: grant it what is missing. Now's eleven are what its read states beside the services, the releases and the days: each model, compute in use, the slowest endpoints, the work and the latest changes.
|
|
40
96
|
- **Now's data states what engineering's parts draw.** `NowData` keeps `services`, `head`, `updatedAt` and `now`, loses `current` and `alsoOpen` (`incidents.open` holds every open incident, the most severe first), and adds `line`, `incidents`, `band`, `models`, `compute`, `shipping`, `work`, `endpoints`, `month`, `runs`, `activity` and `unread`, a part being `null` when its read failed. Build it with `nowData(reads)` from `src/views/now-parts.ts`, as `nowTool` does: a fixture or a test of yours that builds a `NowData` by hand builds it there instead. Now's body and its embed take it packed: `PackedNowBody` in `src/views/now.tsx` and `EmbedNow` in `app/embed/now/view.tsx` take `packNow(data)` from `src/views/now-context.ts`, which sends each strip to the browser one character a day; a page or a test of yours that renders either passes its `NowData` through `packNow` first. Now's context no longer lists every service with its latency: it states the services as their summary and each part's figures.
|
package/payload/CONTRIBUTING.md
CHANGED
|
@@ -93,7 +93,7 @@ Write the status on the line under the title: `Status: beta`.
|
|
|
93
93
|
| `pnpm typecheck`, `pnpm test` | TypeScript and the behaviour tests | Gate |
|
|
94
94
|
| `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). It measures inside open shadow roots too. Fails on sideways scroll, text clipped without an ellipsis, text cut with an ellipsis that keeps the whole of it in no `title`, two pieces of text drawn in the same place or a stroked line drawn through one, a control with no accessible name, more than one page scroller, rendered text under its WCAG minimum (an SVG's text too, read by its `fill` against what the SVG paints behind it), a Tab stop with no focus ring (a change of outline, shadow or border when focus arrives), a custom element it cannot look inside (`unmeasured:`), and any uncaught exception or console error. Prints the design metrics: type sizes, weights, radii and the hierarchy ratio | Before release |
|
|
95
95
|
| `node scripts/audit.ts --atlas` | With the app running: the same audit over the Design Atlas, at 1440 and 390px in both themes: every card's page and its bare preview, and every other entry the Atlas serves (the token views, the page specs, the guides). Its first line says what it reached, by kind, and `--list` prints the routes without opening a browser. Not part of `pnpm verify`, which stays fast: a card's preview is where a defect in its own box shows, so run it after changing a component | After changing a component |
|
|
96
|
-
| `node scripts/vitals.ts [--atlas]` | With a production build running: LCP, INP and
|
|
96
|
+
| `node scripts/vitals.ts [--atlas]` | With a production build running: LCP, INP, CLS and dropped frames on a mid-range phone for every static page, the Atlas's front page and one card page among them. `--atlas` measures every Atlas entry instead, about fifteen minutes, and is not part of `pnpm verify` | Inside `pnpm verify --full`; `--atlas` after changing what every card page shares |
|
|
97
97
|
| `node scripts/interactions.ts` | With the app running: presses every control on every page (mouse at 1440px, taps at 390px) and follows every link. Reaches controls inside open shadow roots. Fails on a control that changes nothing, one something covers, and a link that answers 4xx | Before release |
|
|
98
98
|
| `node scripts/keyboard.ts` | With the app running: Tab through each page until focus comes back round, then Shift+Tab back. Fails on a stop with no focus ring, a control hidden under the sticky bar, a visible control never reached, and a reverse order that differs | Before release |
|
|
99
99
|
| `pnpm verify` | The gate once, one production build, the route policy, the size checks and a smoke of up to three routes on desktop and the phone drawer; ends with a coverage line, and the report is in `out/verify.txt` | Before finishing any change |
|
|
@@ -35,14 +35,14 @@ The page is headed by what its revenue says for the period chosen, worked out by
|
|
|
35
35
|
| The period is a record: its revenue is higher than that of every earlier whole period of its length, and at least six such periods are kept (`facts.recordNeedsPeriods` in `src/app.config.ts`) | "The highest 30 days yet: $3,300,438.74", then ", the 3rd record in a row" when the periods before it were each a record too. A record's title has no clause about the services |
|
|
36
36
|
| Otherwise, with a period before it | "Revenue is up 25.8% on the 90 days before" ("down"; "Revenue is level with the 90 days before" when the change rounds to 0.0%), then the services' clause |
|
|
37
37
|
| No period comes before it (All) | "$27,710,624.39 of revenue in 360 days", then the services' clause |
|
|
38
|
-
| The services' clause | ", and every service is operational"
|
|
38
|
+
| The services' clause | As the last clause, ", and every service is operational". As its own sentence after the figures when something is wrong, ". Inference API is degraded" (or "is down") for the one service in the worst state any is in, ". 2 services are degraded" for several, and ". No service is monitored yet" for a read that returned none: the one thing a person may have to act on is not the fourth item of a list. Left out when the services could not be read |
|
|
39
39
|
| The history could not be read, or no day is kept | The page is headed by its name |
|
|
40
40
|
|
|
41
41
|
In the sample the four periods read "The highest 7 days yet: $810,137.13", "The highest 30 days yet: $3,300,438.74, the 3rd record in a row", "Revenue is up 25.8% on the 90 days before, and Inference API is degraded" and "$27,710,624.39 of revenue in 360 days, and Inference API is degraded". 90 days and All state no record: 360 days hold four whole periods of 90 and one of 360, fewer than a record needs.
|
|
42
42
|
|
|
43
43
|
### The line under the title
|
|
44
44
|
|
|
45
|
-
One line, worked out by `homeLine` in `src/views/home-context.ts`: the customers that have not left, the period's requests and their change, gross margin for a person who may read what compute cost,
|
|
45
|
+
One line, worked out by `homeLine` in `src/views/home-context.ts`: the customers that have not left, the period's requests and their change, and gross margin for a person who may read what compute cost, with the services last. Counts are written at a glance (`formatCompact`). A service that is not operational, or a product monitoring none, is the line's own second sentence, after the figures: on a day whose title states a record it is the only place on the page that says what is wrong today. In the sample at 30 days an Owner reads "320 customers, 1.1B requests in 30 days (up 4.7%), and a gross margin of 62.4%. Inference API is degraded." and a Member or a Viewer the same line without the margin clause. A clause whose figures could not be read is left out.
|
|
46
46
|
|
|
47
47
|
The revenue card and the tiles share the page's Meridian: its dates are the period's complete days, then the day in progress. Point at a day in the revenue chart and the featured figure reads it; at the day in progress its pill says "so far". A tile reads the pointed day only when its points are the Meridian's dates (the Metric tile's rule). While a day is in progress, the Requests, Compute cost and Gross margin tiles have a point for each complete day and none for the day in progress, one short of the Meridian's dates, so they keep the period's figure and a part-day is never read as a whole one. When the latest day kept is complete, no day is in progress, their points are the Meridian's dates, and pointing at a day reads it in those three tiles too.
|
|
48
48
|
|
|
@@ -11,7 +11,7 @@ Thirteen parts, in this order. Every chart and list is the card named, the page
|
|
|
11
11
|
| Part | Split | Pattern | Holds |
|
|
12
12
|
|---|---|---|---|
|
|
13
13
|
| 1. Head | | Page frame | Kicker `ZZ Meridian · Production`; the title, a sentence about the services and the releases (below), with the page's name kept in the heading; the line under it (below), then who Engineering is for (`workspaces.engineering.audience`, read from the declaration); Freshness. No action |
|
|
14
|
-
| 2. Days inside the band | full | Featured metric with a Trend chart, line, with `height="fill"` | The page's protagonist, marked `data-protagonist="Days inside the band"` on a wrapper whose display is `contents`: the days inside the band over every complete day kept, 357, its kicker naming of how many ("Days inside the band · of 360 complete days kept"); the steady fact as its finding, the line the context carries ("357 of 360 days inside the band of p95 under 350 ms and errors under 1%; outside it on 17 Aug 2026, 21 Sept 2026 and 22 Sept 2026"); and the p95 of each of the last 360 complete days as its chart. Its Ask asks what stands out in the days inside the band. With the days not read, its place says so; with none kept, it is not drawn |
|
|
14
|
+
| 2. Days inside the band | full | Featured metric with a Trend chart, line, with `height="fill"` | The page's protagonist, marked `data-protagonist="Days inside the band"` on a wrapper whose display is `contents`: the days inside the band over every complete day kept, 357, its kicker naming of how many ("Days inside the band · of 360 complete days kept"); the steady fact as its finding, the line the context carries ("357 of 360 days inside the band of p95 under 350 ms and errors under 1%; outside it on 17 Aug 2026, 21 Sept 2026 and 22 Sept 2026"); and the p95 of each of the last 360 complete days as its chart, with the band drawn as the region under its ceiling (`band`: `{ value: 350, label: 'Band: p95 under 350 ms' }` in `src/app.config.ts`) and a mark on each day outside it (`marks`, from the strip's own states, so the day that left the band on its error rate is marked where its p95 stands inside the region). Its Ask asks what stands out in the days inside the band. With the days not read, its place says so; with none kept, it is not drawn |
|
|
15
15
|
| 3. Services, now | 2/3 | Card with a Status list, `run` | Every service in the team's order with its state, its last 90 days and how long it has run ("Operational for 69 days · its longest in 90"). Past eight services, the ones not operational, then the three with the longest current runs (below). Always a link to Services that names how many there are: "All 6 services" |
|
|
16
16
|
| 4. Open incident | 1/3 | Incident card, under the part's own heading | The incident nobody has resolved, each update timed from its start ("at the start", "after 1.1h"), its owner and its ticket, which opens the Tickets page on it. With several, "Open incidents": one card each, the most severe first and then the newest. With none, one card says so with when the last one was resolved and how long it lasted ("the last was resolved on 17 Aug 2026 and lasted 2.4h") |
|
|
17
17
|
| 5. N of M days inside the band | full | Card with Uptime bars, `lg`, and a list | Titled by its figure, "357 of 360 days inside the band". The last 360 complete days, ending on the latest complete day, a mark for each day outside the band (`facts.band` in `src/app.config.ts`); the share of the days shown inside it, read "99.17% inside the band"; then four facts: the days outside by date, the year of requests beside the range of the median day's p95, p95 on the busiest day yet, and the requests answered without an error |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { gate, gatedMetadata, may } from '@/data/access';
|
|
2
2
|
import { ACTIONS } from '@/data/features';
|
|
3
|
-
import {
|
|
3
|
+
import { readAll } from '@/data/read';
|
|
4
4
|
import { KeysView } from '@/views/keys';
|
|
5
5
|
import type { ApiKey } from '@/data/sample';
|
|
6
6
|
import { createKey, revokeKey } from './actions';
|
|
@@ -22,7 +22,7 @@ export function generateMetadata() {
|
|
|
22
22
|
export default async function KeysPage() {
|
|
23
23
|
const denied = await gate('keys');
|
|
24
24
|
if (denied) return denied;
|
|
25
|
-
const { rows, observedAt } = await
|
|
25
|
+
const { rows, observedAt } = await readAll('keys');
|
|
26
26
|
const mayCreate = await may(ACTIONS['create-key'].needs);
|
|
27
27
|
const mayRevoke = await may(ACTIONS['revoke-key'].needs);
|
|
28
28
|
return (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { gate, gatedMetadata, may } from '@/data/access';
|
|
2
2
|
import { invitableRoles, memberAccess, type MemberRowAccess } from '@/data/member-mutations';
|
|
3
|
-
import { read } from '@/data/read';
|
|
3
|
+
import { read, readAll } from '@/data/read';
|
|
4
4
|
import { assignRole, inviteMember, removeMember, setMemberStatus } from './actions';
|
|
5
5
|
import { MembersView } from '@/views/members';
|
|
6
6
|
import type { Member } from '@/data/sample';
|
|
@@ -34,7 +34,7 @@ async function workspaceName(): Promise<string | undefined> {
|
|
|
34
34
|
export default async function MembersPage() {
|
|
35
35
|
const denied = await gate('members');
|
|
36
36
|
if (denied) return denied;
|
|
37
|
-
const { rows, observedAt } = await
|
|
37
|
+
const { rows, observedAt } = await readAll('members', { sort: { field: 'joined', dir: 'desc' } });
|
|
38
38
|
const members = rows as Member[];
|
|
39
39
|
const [access, roles, workspace] = await Promise.all([
|
|
40
40
|
Promise.all(members.map(async (m): Promise<readonly [string, MemberRowAccess]> => [m.id, await memberAccess(m.id)])).then((entries) => Object.fromEntries(entries)),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { gate, gatedMetadata, may } from '@/data/access';
|
|
2
2
|
import { ACTIONS } from '@/data/features';
|
|
3
|
-
import {
|
|
3
|
+
import { readAll } from '@/data/read';
|
|
4
4
|
import { readTickets } from '@/data/tickets';
|
|
5
5
|
import { TicketsView } from '@/views/tickets';
|
|
6
6
|
import { createTicket, updateTicket } from './actions';
|
|
@@ -27,9 +27,9 @@ export default async function TicketsPage() {
|
|
|
27
27
|
may(ACTIONS['create-ticket'].needs),
|
|
28
28
|
may(ACTIONS['update-ticket'].needs),
|
|
29
29
|
may('members:read'),
|
|
30
|
-
|
|
30
|
+
readAll('services'),
|
|
31
31
|
]);
|
|
32
|
-
const people = mayReadMembers ? (await
|
|
32
|
+
const people = mayReadMembers ? (await readAll('members')).rows : [];
|
|
33
33
|
return (
|
|
34
34
|
<TicketsView
|
|
35
35
|
rows={rows}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { app } from '@/app.config';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The last net in an embed: something threw that the page's own read did not catch (a view that fails to render). It
|
|
7
|
+
* imports nothing else, and that is the design, not an oversight.
|
|
8
|
+
*
|
|
9
|
+
* Measured: an error boundary here that drew the view's frame and an empty state cost 28 KiB on every embed route's
|
|
10
|
+
* first load — `/embed/now` went 14 KiB past the 820 KiB budget and all three embeds grew 5.8–7.7% — while the same
|
|
11
|
+
* boundary importing nothing costs none of it, because a segment's error boundary lands in the route's entry chunk and
|
|
12
|
+
* everything it imports lands with it. So the framed failure is the page's own (it catches a read that threw and draws
|
|
13
|
+
* the frame and a card), and this is the document underneath it: the product's name, one sentence, and Try again.
|
|
14
|
+
* `app.config` is the one import, and it is already in the entry chunk of every route that draws the product's mark.
|
|
15
|
+
*/
|
|
16
|
+
export default function EmbedError({ reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
|
17
|
+
return (
|
|
18
|
+
<main className="mx-auto flex min-h-dvh max-w-prose flex-col items-start justify-center gap-3 p-8">
|
|
19
|
+
<p className="t-kicker">{app.name}</p>
|
|
20
|
+
<p className="text-sm text-ink">This view did not load, and nothing was changed.</p>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={reset}
|
|
24
|
+
className="press hit rounded-md border border-line-strong px-3 py-1.5 text-sm font-medium text-ink hover:bg-fill-hover"
|
|
25
|
+
>
|
|
26
|
+
Try again
|
|
27
|
+
</button>
|
|
28
|
+
</main>
|
|
29
|
+
);
|
|
30
|
+
}
|