@nurix/etna 0.20.17 → 0.20.18

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "2923378ebe87",
2
+ "fingerprint": "993a4863fa1f",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.16.5",
4
+ "version": "3.16.6",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -25,5 +25,5 @@
25
25
  }
26
26
  ],
27
27
  "docsRoot": "docs",
28
- "digest": "2e50abdd4ddd"
28
+ "digest": "79daa8edeb53"
29
29
  }
@@ -36,6 +36,8 @@ personality:
36
36
  Each prevents a known silent-failure class:
37
37
 
38
38
  - **Specificity armor.** Theme blocks are `:root:not(.dark)` and `:root.dark` — both specificity (0,2,0) — keyed on the `.dark` class the library compiles into its own stylesheet. A `data-theme` attribute keys none of the library's `dark:` variants, its theme toggle, or its toaster: an app on the attribute inverts its tokens and renders mixed library chrome.
39
+ - **Theme-invariant tokens sit in a bare `:root`.** Only the colour roles are themed. Font stacks, the type ramp, radii, shadows, motion and layout emit into a bare `:root` block ahead of the two theme blocks. Behind `:root:not(.dark)` they match nothing once `.dark` is on the root, and every `font-size: var(--text-…)` collapses to an inherited 16px in dark mode while the colours still look right.
40
+ - **The ramp emits Tailwind's composite sub-keys.** `--text-<step>` with `--line-height`, `--font-weight` and `--letter-spacing` sub-keys are exactly what the `text-<step>` utility applies. A `-size`/`-line`/`-weight` suffix form generates junk utilities and leaves `text-<step>` undefined. The `--font-feature-settings` and `--font-family` sub-keys are inert for the utility and are read through `var()`. The dense operator tier's steps are optional: absent steps emit nothing and warn once per package (`dmw-08`).
39
41
  - **The tone trio.** `success`, `warning`, and `info` reach the library as `--tone-success|warning|info` — what a status dot (`<Chip prefix="dot" color="var(--tone-success)">`) and the copy button's check read, falling back to the library's own hue when a role is absent. Declare all three, in both themes — a missing `info` (`dmw-06`) puts the library's blue beside the package's green and amber.
40
42
  - **Font binding by derivation.** Font stacks bind to installed packages under **registered family names** (`"Inter Variable"`, not `"Inter"`), each with its declared fallback stack — otherwise the flagship font silently never renders. Install the font packages the design.md declares (e.g. Fontsource) and verify the name matches what the package registers. The library's `font-sans` / `font-mono` / `font-display` utilities compile to `inherit` on purpose (a ramp, never a face): the app declares its own theme keys in a plain `@theme` pointing at the emitted slots — `--font-body: var(--font-sans)`, `--font-heading: var(--font-display)`, `--font-code: var(--font-mono)` — and sets the ambient face on `body` in `@layer base`.
41
43
  - **Every authored token group reaches a named consumer.** An authored ramp with no emitter vanishes without a trace — wire each group (colors → theme classes/utilities, ramp → text styles, elevation → radius/shadow utilities, motion → duration/easing vars) and let the ship gate's reach check prove it. The personality families reach the library by construction; the app's `--duration-*` / `--ease-*` reach app-owned animation code, or they are unused.
@@ -105,14 +105,15 @@
105
105
  // background (checked only where chroma ≥ 0.01 — hue is noise near gray).
106
106
  // dmw-07 accent chroma delta between themes > 0.06 (identity likely not held across themes).
107
107
  //
108
- // Emitted artifact shape (deterministic, byte-stable): a `:root:not(.dark)` block and a
109
- // `:root.dark` block (specificity armor — both selectors reach 0,2,0 via the attribute
110
- // form, so a component library's later bare `:root` cannot silently defeat a theme). The light block
111
- // carries the 36 roles as --<role> plus every theme-invariant group: --font-display/sans/mono (registered
112
- // names quoted, declared fallback stacks appended), --text-<step>-size/-line/-weight (+ -family/-tracking/
113
- // -features where authored), --radius-*, --shadow-*, --duration-*, --ease-*, and the layout vars
114
- // (--space-base, --sidebar-width[-collapsed], --topbar-height, --side-panel-width[-wide],
115
- // --content-max-width; unmapped keys emit as --layout-<key>). The dark block carries the 36 dark roles.
108
+ // dmw-08 the dense operator tier is unadopted: typography.ramp omits optional steps (one warn per package).
109
+ //
110
+ // Emitted artifact shape (deterministic, byte-stable): THREE blocks. A bare `:root` carries every
111
+ // theme-invariant group — --font-display/sans/mono (registered names quoted, fallback stacks appended),
112
+ // the ramp as Tailwind composite keys --text-<step> + --line-height/--font-weight/--letter-spacing
113
+ // sub-keys, --radius-*, --shadow-*, --duration-*, --ease-*, and the layout vars (unmapped keys emit as
114
+ // --layout-<key>). Then `:root:not(.dark)` and `:root.dark` (both 0,2,0) carry the colour roles.
115
+ // Theme-invariant tokens must never move behind `:root:not(.dark)`: it matches nothing once `.dark` is on
116
+ // the root, and the whole ramp collapses to inherited 16px in dark mode (references/emission.md).
116
117
 
117
118
  import { readFileSync, writeFileSync, existsSync, statSync, readdirSync, realpathSync } from "node:fs";
118
119
  import { join, resolve, basename, dirname, relative, sep } from "node:path";
@@ -140,7 +141,11 @@ const SECTIONS = [
140
141
  "11 Responsive", "12 Agent Prompt Guide",
141
142
  ];
142
143
 
143
- const RAMP_STEPS = ["display", "h1", "h2", "body", "small", "caption", "data"];
144
+ // The dense operator tier is optional: every catalog package authors only the seven required steps, and
145
+ // each new value would be a taste decision per identity. Absent optional steps warn once (dmw-08) and emit nothing.
146
+ const RAMP_STEPS_REQUIRED = ["display", "h1", "h2", "body", "small", "caption", "data"];
147
+ const RAMP_STEPS_OPTIONAL = ["title", "h3", "prose", "control-lg", "dense", "control", "meta", "micro", "code", "ident"];
148
+ const RAMP_STEPS = [...RAMP_STEPS_REQUIRED, ...RAMP_STEPS_OPTIONAL];
144
149
  const FONT_SLOTS = ["display", "sans", "mono"];
145
150
  // Optional color roles: emitted only when declared (both themes or neither). `info` completes the
146
151
  // success/warning/info trio @nurix/components' status dots read through --tone-*.
@@ -688,9 +693,17 @@ function checkCoverage(values, report) {
688
693
  for (const slot of FONT_SLOTS) if (!isObj(typo[slot])) missing(`typography.${slot}`);
689
694
  if (!isObj(typo.ramp)) missing("typography.ramp");
690
695
  else {
696
+ // One warn per package, not one per step, so the taste warns stay readable across the catalog.
697
+ const unadopted = RAMP_STEPS_OPTIONAL.filter((s) => !isObj(typo.ramp[s]));
698
+ if (unadopted.length) {
699
+ warn(report, "dmw-08", `dense operator tier unadopted — typography.ramp missing ${unadopted.length}/${RAMP_STEPS_OPTIONAL.length}: ${unadopted.join(", ")}`);
700
+ }
691
701
  for (const step of RAMP_STEPS) {
692
702
  const r = typo.ramp[step];
693
- if (!isObj(r)) { missing(`typography.ramp.${step}`); continue; }
703
+ if (!isObj(r)) {
704
+ if (!RAMP_STEPS_OPTIONAL.includes(step)) missing(`typography.ramp.${step}`);
705
+ continue;
706
+ }
694
707
  for (const k of ["size", "line", "weight"]) if (r[k] == null) missing(`typography.ramp.${step}.${k}`);
695
708
  if (r.family == null) missing(`typography.ramp.${step}.family`);
696
709
  else if (!FONT_SLOTS.includes(r.family)) {
@@ -997,30 +1010,27 @@ export function emitTokens(front, values) {
997
1010
  const out = [];
998
1011
  out.push(`/* design.tokens.css — compiled from ${id} (design-md/v2) by validate-design-md.mjs — derived artifact: edit the design.md, never this file */`);
999
1012
  out.push("");
1000
- out.push(':root:not(.dark) {');
1001
- out.push(" /* color roles */");
1002
- for (const role of COLOR_ROLES) out.push(` --${role}: ${cssValue(req(values, ["color", "light", role]))};`);
1003
- for (const role of OPTIONAL_COLOR_ROLES) {
1004
- const v = values.color.light[role];
1005
- if (v != null) out.push(` --${role}: ${cssValue(v)};`);
1006
- }
1007
- out.push("");
1013
+ // Theme-invariant tokens sit in a bare `:root`; behind `:root:not(.dark)` they would not exist in dark mode.
1014
+ out.push(":root {");
1008
1015
  out.push(" /* typography */");
1009
1016
  for (const slot of FONT_SLOTS) {
1010
1017
  const f = req(values, ["typography", slot]);
1011
1018
  if (f.family == null || f.fallback == null) throw new Error(`cannot emit: typography.${slot} family/fallback missing`);
1012
1019
  out.push(` --font-${slot}: "${f.family}", ${f.fallback};`);
1013
1020
  }
1021
+ const ramp = req(values, ["typography", "ramp"]);
1014
1022
  for (const step of RAMP_STEPS) {
1023
+ if (ramp[step] == null && RAMP_STEPS_OPTIONAL.includes(step)) continue;
1015
1024
  const r = req(values, ["typography", "ramp", step]);
1016
- out.push(` --text-${step}-size: ${cssValue(req(r, ["size"]))};`);
1017
- out.push(` --text-${step}-line: ${cssValue(req(r, ["line"]))};`);
1018
- out.push(` --text-${step}-weight: ${cssValue(req(r, ["weight"]))};`);
1019
- if (r.family != null) out.push(` --text-${step}-family: var(--font-${r.family});`);
1020
- if (r.tracking != null) out.push(` --text-${step}-tracking: ${cssValue(r.tracking)};`);
1025
+ // Tailwind's `text-*` utility reads exactly --line-height, --letter-spacing and --font-weight sub-keys.
1026
+ out.push(` --text-${step}: ${cssValue(req(r, ["size"]))};`);
1027
+ out.push(` --text-${step}--line-height: ${cssValue(req(r, ["line"]))};`);
1028
+ out.push(` --text-${step}--font-weight: ${cssValue(req(r, ["weight"]))};`);
1029
+ if (r.tracking != null) out.push(` --text-${step}--letter-spacing: ${cssValue(r.tracking)};`);
1021
1030
  if (Array.isArray(r.features) && r.features.length) {
1022
- out.push(` --text-${step}-features: ${r.features.map((x) => `"${x}"`).join(", ")};`);
1031
+ out.push(` --text-${step}--font-feature-settings: ${r.features.map((x) => `"${x}"`).join(", ")}; /* inert for text-*; read via var() */`);
1023
1032
  }
1033
+ if (r.family != null) out.push(` --text-${step}--font-family: var(--font-${r.family}); /* inert for text-*; read via var() */`);
1024
1034
  }
1025
1035
  out.push("");
1026
1036
  out.push(" /* elevation */");
@@ -1043,15 +1053,18 @@ export function emitTokens(front, values) {
1043
1053
  }
1044
1054
  }
1045
1055
  out.push("}");
1046
- out.push("");
1047
- out.push(':root.dark {');
1048
- out.push(" /* color roles */");
1049
- for (const role of COLOR_ROLES) out.push(` --${role}: ${cssValue(req(values, ["color", "dark", role]))};`);
1050
- for (const role of OPTIONAL_COLOR_ROLES) {
1051
- const v = values.color.dark[role];
1052
- if (v != null) out.push(` --${role}: ${cssValue(v)};`);
1056
+ // Both colour blocks reach (0,2,0), one notch above a library's unlayered `:root`/`.dark`, whatever the import order.
1057
+ for (const [selector, theme] of [[":root:not(.dark)", "light"], [":root.dark", "dark"]]) {
1058
+ out.push("");
1059
+ out.push(`${selector} {`);
1060
+ out.push(` /* color roles — ${theme} */`);
1061
+ for (const role of COLOR_ROLES) out.push(` --${role}: ${cssValue(req(values, ["color", theme, role]))};`);
1062
+ for (const role of OPTIONAL_COLOR_ROLES) {
1063
+ const v = values.color[theme][role];
1064
+ if (v != null) out.push(` --${role}: ${cssValue(v)};`);
1065
+ }
1066
+ out.push("}");
1053
1067
  }
1054
- out.push("}");
1055
1068
  return out.join("\n") + "\n";
1056
1069
  }
1057
1070
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.20.17",
3
+ "version": "0.20.18",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,5 +42,5 @@
42
42
  "agent",
43
43
  "plugin"
44
44
  ],
45
- "etnaFingerprint": "2923378ebe87"
45
+ "etnaFingerprint": "993a4863fa1f"
46
46
  }