@norskvideo/ctl-dev-kit 0.2.108 → 0.2.110
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/conventions/DESIGN.family.md +73 -0
- package/conventions/check-drift.ts +100 -0
- package/conventions/sync-drift.ts +21 -1
- package/package.json +6 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!-- BEGIN ctl-shared-design v1 -->
|
|
2
|
+
<!--
|
|
3
|
+
This block is the SHARED family design core for Norsk operator consoles,
|
|
4
|
+
single-sourced in @norskvideo/ctl-dev-kit (conventions/DESIGN.family.md). It
|
|
5
|
+
is copied verbatim into every product dashboard's DESIGN.md
|
|
6
|
+
(dashboards/<name>/DESIGN.md) between these fenced markers, and the drift
|
|
7
|
+
gate fails CI if a copy diverges. Do NOT hand-edit a copy: edit the dev-kit
|
|
8
|
+
source and re-sync. The console's own north star, accent, components and
|
|
9
|
+
product rules live OUTSIDE the markers and are yours to own.
|
|
10
|
+
-->
|
|
11
|
+
|
|
12
|
+
## Family rules (shared by every Norsk operator console — do not edit in place)
|
|
13
|
+
|
|
14
|
+
An operator moving between consoles (a commentary gallery, a playout desk) must
|
|
15
|
+
read on-air, health and interaction identically. These rules and the shared
|
|
16
|
+
palette below hold on every console. The operator-UI principles behind them live
|
|
17
|
+
in norsk-studio `docs/operator-ui-principles.md`; change a principle there first,
|
|
18
|
+
then here.
|
|
19
|
+
|
|
20
|
+
### Shared palette
|
|
21
|
+
|
|
22
|
+
Status and neutral tokens are the same on every console. Each console's
|
|
23
|
+
frontmatter `colors:` must carry exactly these values (the drift gate checks);
|
|
24
|
+
its `accent` tokens are its own.
|
|
25
|
+
|
|
26
|
+
| Token | Value | Role |
|
|
27
|
+
| --------- | --------- | ----------------------------------------------------------- |
|
|
28
|
+
| `live` | `#ff4557` | On-Air Red: live / on air. The only colour allowed to glow. |
|
|
29
|
+
| `ok` | `#35d29a` | Healthy. |
|
|
30
|
+
| `warn` | `#f4b63f` | At risk. |
|
|
31
|
+
| `crit` | `#ff7a4d` | Failed. |
|
|
32
|
+
| `bg` | `#0d1014` | Background. |
|
|
33
|
+
| `panel` | `#141a21` | Panel tone 1. |
|
|
34
|
+
| `panel-2` | `#1a2129` | Panel tone 2. |
|
|
35
|
+
| `panel-3` | `#202932` | Panel tone 3. |
|
|
36
|
+
| `line` | `#262f3b` | Hairline border. |
|
|
37
|
+
| `ink` | `#e8edf3` | Primary text. |
|
|
38
|
+
| `ink-mut` | `#9aa6b5` | Secondary text. |
|
|
39
|
+
| `ink-dim` | `#8794a3` | Tertiary text; still clears 4.5:1. |
|
|
40
|
+
|
|
41
|
+
### Family named rules
|
|
42
|
+
|
|
43
|
+
**The Reserved Accent Rule.** The console's accent means _interactive_. Status
|
|
44
|
+
never borrows it: health, metering and channel state live entirely in the
|
|
45
|
+
red/green/amber/coral system. If a swatch of the accent is not clickable or a
|
|
46
|
+
focus state, it is wrong.
|
|
47
|
+
|
|
48
|
+
**The One Glow Rule.** Only On-Air Red is allowed a glow (`box-shadow` bloom),
|
|
49
|
+
and it sits on the console's on-air programme view. Everything else is flat. A
|
|
50
|
+
glow anywhere else dilutes the one signal that must never be missed.
|
|
51
|
+
|
|
52
|
+
**The Tabular Rule.** Any column or readout of numbers (timecode, durations,
|
|
53
|
+
countdowns, meter scales, dB, ports) is `font-variant-numeric: tabular-nums`, so
|
|
54
|
+
digits never jitter as they tick.
|
|
55
|
+
|
|
56
|
+
**The Flat-By-Default Rule.** New surfaces get a tone step and a 1px border.
|
|
57
|
+
Reach for `box-shadow` only for the On-Air glow or the focus ring; never to lift
|
|
58
|
+
a card.
|
|
59
|
+
|
|
60
|
+
**The Immediate-PGM, Honest-OUT Rule.** Show an operator's decision the moment
|
|
61
|
+
it is decided, and keep a separate, true view of what is going out. A desk that
|
|
62
|
+
cuts or takes programme carries two labelled views: **PGM**, which changes the
|
|
63
|
+
instant a cut or take is committed, using a preview the desk already has; and
|
|
64
|
+
**OUT**, the real encoded output with graphics and everything else applied
|
|
65
|
+
downstream, whose transport lag is labelled as expected rather than styled as a
|
|
66
|
+
fault. Only a committed decision is immediate: a guarded action changes nothing
|
|
67
|
+
on PGM until it is confirmed, and a load or a source still arriving shows its
|
|
68
|
+
pending state. Never simulate downstream processing in PGM; OUT is the authority
|
|
69
|
+
on what aired. When both views are present, the ON AIR pill and the One Glow sit
|
|
70
|
+
on PGM, following the gallery tally convention. A desk with only one programme
|
|
71
|
+
view shows the true output and labels its lag.
|
|
72
|
+
|
|
73
|
+
<!-- END ctl-shared-design v1 -->
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
// tsconfig.base.json, the .gitignore core block, scripts/demo (only a
|
|
6
6
|
// product with tests/demo.spec.ts is asked for it), publish-docs-site.yml
|
|
7
7
|
// (only a repo with a docs site may carry it, and never a turnkey)
|
|
8
|
+
// - verbatim marker block + palette: each dashboards/*/DESIGN.md embeds the
|
|
9
|
+
// DESIGN.family.md block, and its frontmatter colors: carry that block's
|
|
10
|
+
// shared palette (no DESIGN.md, no requirement)
|
|
8
11
|
// - masked per-repo line: checks.yml, bump.yml, publish-docs.yml and the
|
|
9
12
|
// other dispatching workflows (`product:` key)
|
|
10
13
|
// - retired: upgrade-latest.yml, sync-dev-kit.yml, sync-ctl-packages.yml,
|
|
@@ -255,6 +258,97 @@ function coreProblem(claude: string, canonicalCore: string): string | undefined
|
|
|
255
258
|
return `CLAUDE.md fenced core has drifted from @norskvideo/ctl-dev-kit conventions/CLAUDE.core.md (${firstDiffLine(region, canonicalCore.trimEnd())}). ${RESYNC}`;
|
|
256
259
|
}
|
|
257
260
|
|
|
261
|
+
// Each operator console's dashboards/<name>/DESIGN.md embeds the family design
|
|
262
|
+
// block verbatim between these markers, and its frontmatter `colors:` must carry
|
|
263
|
+
// the shared palette that block's table defines. The table is the one source of
|
|
264
|
+
// the expected values, so editing DESIGN.family.md is the only change needed.
|
|
265
|
+
export const DESIGN_BEGIN = "<!-- BEGIN ctl-shared-design v1 -->";
|
|
266
|
+
export const DESIGN_END = "<!-- END ctl-shared-design v1 -->";
|
|
267
|
+
const DESIGN_SOURCE = "@norskvideo/ctl-dev-kit conventions/DESIGN.family.md";
|
|
268
|
+
|
|
269
|
+
export function designDocs(repoRoot: string): string[] {
|
|
270
|
+
const dir = join(repoRoot, "dashboards");
|
|
271
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
272
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
273
|
+
.filter((d) => d.isDirectory() && existsSync(join(dir, d.name, "DESIGN.md")))
|
|
274
|
+
.map((d) => `dashboards/${d.name}/DESIGN.md`)
|
|
275
|
+
.sort();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const PALETTE_ROW = /^\|\s*`([a-z0-9-]+)`\s*\|\s*`([^`]+)`\s*\|/gm;
|
|
279
|
+
|
|
280
|
+
export function sharedPalette(canonicalDesign: string): Map<string, string> {
|
|
281
|
+
return new Map([...canonicalDesign.matchAll(PALETTE_ROW)].map((m) => [m[1] as string, m[2] as string]));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const unquote = (v: string): string => {
|
|
285
|
+
const m = /^(["'])(.*)\1(\s+#.*)?$/.exec(v);
|
|
286
|
+
return m ? (m[2] as string) : v;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Just enough YAML for a flat `colors:` map in the leading frontmatter: no
|
|
290
|
+
// dependency, since this script ships to products and runs from node_modules.
|
|
291
|
+
export function frontmatterColors(text: string): Map<string, string> | undefined {
|
|
292
|
+
const lines = text.split("\n");
|
|
293
|
+
if (lines[0]?.trim() !== "---") return undefined;
|
|
294
|
+
const close = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
295
|
+
if (close === -1) return undefined;
|
|
296
|
+
const start = lines.findIndex((l, i) => i < close && /^colors:\s*$/.test(l));
|
|
297
|
+
if (start === -1) return undefined;
|
|
298
|
+
const colors = new Map<string, string>();
|
|
299
|
+
for (const line of lines.slice(start + 1, close)) {
|
|
300
|
+
if (line.trim() === "") continue;
|
|
301
|
+
if (!/^\s/.test(line)) break;
|
|
302
|
+
const m = /^\s+([A-Za-z0-9_-]+):\s*(.*?)\s*$/.exec(line);
|
|
303
|
+
if (m) colors.set(m[1] as string, unquote(m[2] as string));
|
|
304
|
+
}
|
|
305
|
+
return colors;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function designBlockProblem(rel: string, text: string, canonicalDesign: string): string | undefined {
|
|
309
|
+
if (text.includes(canonicalDesign.trimEnd())) return undefined;
|
|
310
|
+
const begin = text.indexOf(DESIGN_BEGIN);
|
|
311
|
+
const end = text.indexOf(DESIGN_END);
|
|
312
|
+
if (begin === -1 || end === -1 || end < begin) {
|
|
313
|
+
return `${rel} is missing the '${DESIGN_BEGIN}' / '${DESIGN_END}' marker block. Copy conventions/DESIGN.family.md from @norskvideo/ctl-dev-kit verbatim between the markers.`;
|
|
314
|
+
}
|
|
315
|
+
const region = text.slice(begin, end + DESIGN_END.length);
|
|
316
|
+
return `${rel} fenced family design block has drifted from ${DESIGN_SOURCE} (${firstDiffLine(region, canonicalDesign.trimEnd())}). ${RESYNC}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function paletteProblems(rel: string, text: string, canonicalDesign: string): string[] {
|
|
320
|
+
const expected = sharedPalette(canonicalDesign);
|
|
321
|
+
if (expected.size === 0) throw new Error("canonical DESIGN.family.md has no shared palette table");
|
|
322
|
+
const colors = frontmatterColors(text);
|
|
323
|
+
if (colors === undefined) {
|
|
324
|
+
return [
|
|
325
|
+
`${rel} has no frontmatter colors: map; it must carry the shared palette tokens (${[...expected.keys()].join(", ")}) from ${DESIGN_SOURCE}.`,
|
|
326
|
+
];
|
|
327
|
+
}
|
|
328
|
+
const problems: string[] = [];
|
|
329
|
+
for (const [token, want] of expected) {
|
|
330
|
+
const got = colors.get(token);
|
|
331
|
+
if (got === undefined || got === "") {
|
|
332
|
+
problems.push(
|
|
333
|
+
`${rel} frontmatter colors: has no \`${token}\` — the shared palette in ${DESIGN_SOURCE} sets it to ${JSON.stringify(want)}.`,
|
|
334
|
+
);
|
|
335
|
+
} else if (got.toLowerCase() !== want.toLowerCase()) {
|
|
336
|
+
problems.push(
|
|
337
|
+
`${rel} frontmatter colors.${token} is ${JSON.stringify(got)}, but the shared palette in ${DESIGN_SOURCE} sets it to ${JSON.stringify(want)}. Status and neutral tokens are the family's; only accent tokens are the console's own.`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return problems;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function designProblems(repoRoot: string, canonicalDesign: string): string[] {
|
|
345
|
+
return designDocs(repoRoot).flatMap((rel) => {
|
|
346
|
+
const text = readFileSync(join(repoRoot, rel), "utf8");
|
|
347
|
+
const block = designBlockProblem(rel, text, canonicalDesign);
|
|
348
|
+
return [...(block ? [block] : []), ...paletteProblems(rel, text, canonicalDesign)];
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
258
352
|
export interface CanonicalBytes {
|
|
259
353
|
core: string;
|
|
260
354
|
/** build/flake.nix — the starter flake a Studio product begins from, and what
|
|
@@ -316,6 +410,10 @@ export interface CanonicalBytes {
|
|
|
316
410
|
* pins step has no Studio pair or manifest.seed.json, and bumps the norsk
|
|
317
411
|
* family off the SDK nightly. Optional for the same reason as flakeSdkApp. */
|
|
318
412
|
bumpSdkApp?: string;
|
|
413
|
+
/** conventions/DESIGN.family.md -- the family design block each
|
|
414
|
+
* dashboards/<name>/DESIGN.md embeds between DESIGN_BEGIN/DESIGN_END, whose
|
|
415
|
+
* palette table its frontmatter colors: must match. */
|
|
416
|
+
design: string;
|
|
319
417
|
}
|
|
320
418
|
|
|
321
419
|
export const SERVER_KIT_COPIES = [
|
|
@@ -927,6 +1025,7 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
|
|
|
927
1025
|
for (const p of productLogsProblems(repoRoot)) push(p);
|
|
928
1026
|
for (const p of bunVersionProblems(repoRoot, canonical.ctlShell)) push(p);
|
|
929
1027
|
for (const p of ctlPinProblems(repoRoot)) push(p);
|
|
1028
|
+
for (const p of designProblems(repoRoot, canonical.design)) push(p);
|
|
930
1029
|
|
|
931
1030
|
return { ok: problems.length === 0, problems };
|
|
932
1031
|
}
|
|
@@ -958,6 +1057,7 @@ if (import.meta.main) {
|
|
|
958
1057
|
flakeSdkApp: readFileSync(join(import.meta.dir, "..", "build", "flake.sdk-app.nix"), "utf8"),
|
|
959
1058
|
checksSdkApp: readFileSync(join(import.meta.dir, "checks.sdk-app.yml"), "utf8"),
|
|
960
1059
|
bumpSdkApp: readFileSync(join(import.meta.dir, "bump.sdk-app.yml"), "utf8"),
|
|
1060
|
+
design: readFileSync(join(import.meta.dir, "DESIGN.family.md"), "utf8"),
|
|
961
1061
|
};
|
|
962
1062
|
const report = checkDrift(repoRoot, canonical, {
|
|
963
1063
|
repoName: resolveRepoName(repoRoot),
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
// sync-ctl-packages) are DELETED
|
|
16
16
|
// - nix/ctl-shell.nix -> overwrite with canonical (created if absent)
|
|
17
17
|
// - flake.nix (legacy verbatim copy only) -> the starter, repo's floating ctl pin kept
|
|
18
|
-
// - marker-block (CLAUDE core, dprint, .gitignore
|
|
18
|
+
// - marker-block (CLAUDE core, dprint, .gitignore, -> replace the shared block, keep the rest
|
|
19
|
+
// dashboards/*/DESIGN.md family block) (DESIGN.md: appended where absent)
|
|
19
20
|
// - publish-docs-site.yml -> verbatim; CREATED where the repo has a
|
|
20
21
|
// docs site and is not a turnkey
|
|
21
22
|
// Structural per-repo files (root tsconfig.json, deployment/build-image.sh,
|
|
@@ -37,8 +38,11 @@ import {
|
|
|
37
38
|
CTL_SHELL_PATH,
|
|
38
39
|
CTL_VERSION_LINE,
|
|
39
40
|
carriesIntegrationMarker,
|
|
41
|
+
DESIGN_BEGIN,
|
|
42
|
+
DESIGN_END,
|
|
40
43
|
DOCS_SITE_DIR,
|
|
41
44
|
DOCS_SITE_WORKFLOW,
|
|
45
|
+
designDocs,
|
|
42
46
|
END,
|
|
43
47
|
GITIGNORE_BEGIN,
|
|
44
48
|
GITIGNORE_END,
|
|
@@ -285,6 +289,20 @@ function syncClaude(repoRoot: string, core: string, r: SyncReport): void {
|
|
|
285
289
|
writeIfChanged(path, out, "CLAUDE.md", r.written);
|
|
286
290
|
}
|
|
287
291
|
|
|
292
|
+
// dashboards/<name>/DESIGN.md: the family design block is fenced like the CLAUDE
|
|
293
|
+
// core; a console that has none yet gets it appended. The frontmatter palette is
|
|
294
|
+
// the product's to fix (the gate names each token), so it is never rewritten.
|
|
295
|
+
function syncDesign(repoRoot: string, design: string, r: SyncReport): void {
|
|
296
|
+
for (const rel of designDocs(repoRoot)) {
|
|
297
|
+
const path = join(repoRoot, rel);
|
|
298
|
+
const existing = readFileSync(path, "utf8");
|
|
299
|
+
const out =
|
|
300
|
+
replaceMarkerBlock(existing, DESIGN_BEGIN, DESIGN_END, design) ??
|
|
301
|
+
`${existing.replace(/\n*$/, "\n\n")}${design.trimEnd()}\n`;
|
|
302
|
+
writeIfChanged(path, out, rel, r.written);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
288
306
|
// .gitignore: the shared block is marker-delimited; repo-specific ignores live
|
|
289
307
|
// outside it. Replace the block, or prepend it if the repo has none yet (the
|
|
290
308
|
// one-time migration case).
|
|
@@ -489,6 +507,7 @@ export function syncDrift(
|
|
|
489
507
|
syncPackageManager(repoRoot, canonical.ctlShell, r);
|
|
490
508
|
syncFlake(repoRoot, flakeCanon, r);
|
|
491
509
|
syncClaude(repoRoot, canonical.core, r);
|
|
510
|
+
syncDesign(repoRoot, canonical.design, r);
|
|
492
511
|
syncGitignore(repoRoot, canonical.gitignoreCore, r);
|
|
493
512
|
syncDprint(repoRoot, canonical.dprint, r);
|
|
494
513
|
syncDemoShim(repoRoot, canonical.demoShim, r);
|
|
@@ -528,6 +547,7 @@ if (import.meta.main) {
|
|
|
528
547
|
flakeSdkApp: readFileSync(join(dir, "..", "build", "flake.sdk-app.nix"), "utf8"),
|
|
529
548
|
checksSdkApp: readFileSync(join(dir, "checks.sdk-app.yml"), "utf8"),
|
|
530
549
|
bumpSdkApp: readFileSync(join(dir, "bump.sdk-app.yml"), "utf8"),
|
|
550
|
+
design: readFileSync(join(dir, "DESIGN.family.md"), "utf8"),
|
|
531
551
|
};
|
|
532
552
|
const report = syncDrift(repoRoot, canonical, resolveShape(repoRoot), { repoName: resolveRepoName(repoRoot) });
|
|
533
553
|
if (report.written.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-dev-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.110",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/id3as/norsk-ctl.git",
|
|
8
|
+
"directory": "packages/dev-kit"
|
|
9
|
+
},
|
|
5
10
|
"exports": {
|
|
6
11
|
"./create-product": "./create-product/create-product.ts",
|
|
7
12
|
"./doc-guide": "./doc-guide/doc-guide.js",
|