haus-tokens 2.0.0 → 2.2.1

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 CHANGED
@@ -129,6 +129,44 @@ It is **pure and does no file reading**, so it runs anywhere and this package
129
129
  gains no dependency on `node:fs`. You know which files you load; it only knows
130
130
  what the contract is.
131
131
 
132
+ ### The second check: values you already have
133
+
134
+ A property you declare that this package also declares, at the value this package
135
+ already gives, is not an override. It is a copy, and it renders perfectly until
136
+ one of the two moves.
137
+
138
+ ```ts
139
+ import { findRestatedTokens } from 'haus-tokens/guard'
140
+
141
+ it('restates no value haus already ships', () => {
142
+ const copies = findRestatedTokens({
143
+ defines: [read('src/tokens/semantics.css')],
144
+ upstream: [
145
+ read('node_modules/haus-tokens/dist/primitives.css'),
146
+ read('node_modules/haus-tokens/dist/motion.css'),
147
+ read('node_modules/haus-tokens/dist/semantics.css'),
148
+ ],
149
+ })
150
+
151
+ expect(copies.map((c) => `${c.name} (${c.kind})`)).toEqual([])
152
+ })
153
+ ```
154
+
155
+ Each result is tagged. `identical` is the same declaration text on both sides.
156
+ **`resolved` is the one worth having**: the text differs and the value does not,
157
+ once `var()` chains are followed. `--haus-z-modal: 400` against our
158
+ `var(--haus-z-400)`, where `--haus-z-400` is `400`, is how a consumer opts out of
159
+ a scale while still appearing to be on it.
160
+
161
+ **Which files you pass as `upstream` decides the question.** Include `brand.css`
162
+ and you are asking *do I restate anything haus ships, its colour choices
163
+ included*. Leave it out and you are asking *do I restate anything structural*,
164
+ with your palette treated as your business. Against drift the two answers are 143
165
+ and 89, and the 54 in the gap are exactly its brand.
166
+
167
+ Overriding a role is fine and several consumers should. Overriding it to our
168
+ value is the thing this reports.
169
+
132
170
  `alsoDefined` takes properties set outside CSS, a component doing
133
171
  `style={{ '--haus-avatar-bg': v }}` defines one that no stylesheet will show.
134
172
 
package/dist/brand.css CHANGED
@@ -8,19 +8,49 @@
8
8
  meant taking haus's palette, and two products declared their own tokens rather
9
9
  than do that.
10
10
 
11
- Every entry here is required. `BrandMap` in the package's TypeScript export is
12
- generated from this file, so a brand that omits a role or misnames one fails a
13
- consumer's build instead of rendering an unresolved var().
11
+ TWO TIERS, since haus#52. The base tier is required: every product has
12
+ surfaces, text, borders and one primary colour, and a brand that omits any of
13
+ them renders unstyled. The feedback tier is optional: info, success, warning
14
+ and error are the four semantics of a notification, and a product whose
15
+ statuses are not those four should not be made to invent them.
14
16
 
15
- The default brand applies at :root, so the common case needs no attribute. A
16
- named brand applies at [data-haus-theme="<name>"] and nests, because custom
17
- properties inherit. See brands/vault.css for a complete second brand.
17
+ The optional half needs no machinery, because the cascade already does it. The
18
+ default brand applies at :root and a named brand at [data-haus-theme="<name>"],
19
+ and custom properties inherit, so a brand declaring only the base tier inherits
20
+ these feedback entries from :root. That was true before it was named. What was
21
+ missing is that nothing said so, `BrandMap` demanded all 54, and brand.test.ts
22
+ failed a brand that supplied 28. The mechanism worked and the contract around
23
+ it did not.
24
+
25
+ `BrandMap` in the package's TypeScript export is generated from this file:
26
+ base entries required, feedback entries optional, so a brand omitting a base
27
+ role fails a consumer's build instead of rendering an unresolved var().
28
+
29
+ One rule holds the optional half honest, in brand.test.ts: a brand that
30
+ supplies any entry of a status ramp supplies all of that ramp. Half a ramp is
31
+ worse than none, because the half that is missing falls back to haus's hue and
32
+ the result is a two-coloured error state nothing reports.
33
+
34
+ A THIRD TIER, form, since haus#53. Colour is not the only thing that makes two
35
+ products look different. Measured across drift and vault, the things a consumer
36
+ actually re-decides are its palette, its typeface, its shadow ramp and its
37
+ corner radii, and nothing else: neither product re-decides the spacing rhythm
38
+ or the stacking order, and drift's apparent overrides of those were the literal
39
+ values of haus's own primitives, typed out. So form is radius and elevation,
40
+ optional like feedback, and all or nothing per group for the same reason.
41
+
42
+ See brands/vault.css for a complete second brand.
18
43
  ─────────────────────────────────────────────────────────────────────────── */
19
44
 
20
45
  @layer haus.brand {
21
46
 
22
47
  :root {
23
48
 
49
+ /* ══ BASE TIER, required ═══════════════════════════════════════════════
50
+ Twenty-eight entries. Every product has these, whatever it is for. A
51
+ brand must supply all of them.
52
+ ════════════════════════════════════════════════════════════════════ */
53
+
24
54
  /* ── Surfaces ────────────────────────────────────────────────────────── */
25
55
  --haus-brand-surface-default: var(--haus-damson-0);
26
56
  --haus-brand-surface-subtle: var(--haus-damson-100);
@@ -56,6 +86,19 @@
56
86
  --haus-brand-primary-on-subtle: var(--haus-aronia-700);
57
87
  --haus-brand-primary-disabled: var(--haus-damson-200);
58
88
 
89
+ /* ── Backdrop ────────────────────────────────────────────────────────── */
90
+ --haus-brand-backdrop: oklch(from var(--haus-damson-950) l c h / var(--haus-opacity-60));
91
+
92
+ /* ══ FEEDBACK TIER, optional ═══════════════════════════════════════════
93
+ Twenty-six entries across four ramps. These are the four semantics of a
94
+ notification, and not every product has them: core's statuses are the
95
+ states of a decision, proposed, accepted, rejected, superseded and
96
+ deprecated, which are five things that are not these four. A brand may
97
+ omit this tier and inherit it from here.
98
+
99
+ All or nothing PER RAMP. Supply every entry of `warning` or none of them.
100
+ ════════════════════════════════════════════════════════════════════ */
101
+
59
102
  /* ── Info ────────────────────────────────────────────────────────────── */
60
103
  --haus-brand-info-subtle: var(--haus-elderberry-100);
61
104
  --haus-brand-info-border: var(--haus-elderberry-200);
@@ -90,8 +133,55 @@
90
133
  --haus-brand-error-on-default: var(--haus-damson-0);
91
134
  --haus-brand-error-emphasis: var(--haus-cherry-900);
92
135
 
93
- /* ── Everything else ─────────────────────────────────────────────────── */
94
- --haus-brand-backdrop: oklch(from var(--haus-damson-950) l c h / var(--haus-opacity-60));
136
+ /* ══ FORM TIER, optional ═══════════════════════════════════════════════
137
+ Seven entries. What a product re-decides that is not a colour, taken
138
+ from what drift and vault actually changed rather than from what a brand
139
+ contract could theoretically cover.
140
+
141
+ drift sits one radius step tighter than haus at all three sizes and puts
142
+ elevation on its own four-step shadow ramp, which is two of the six names
143
+ it deliberately did not rename in the 1.0 migration, because pointing them
144
+ at haus's would have changed the product's face and its shadow ramp while
145
+ every test stayed green.
146
+
147
+ Not here, deliberately: z-index, opacity, border-width and the space
148
+ ladder. drift appeared to override twelve of those and every one resolved
149
+ to the value haus's own primitive already gives, so the override was
150
+ typing rather than a decision. Nobody re-decides the spacing rhythm.
151
+
152
+ All or nothing per group.
153
+ ════════════════════════════════════════════════════════════════════ */
154
+
155
+ /* ── Radius ──────────────────────────────────────────────────────────── */
156
+ --haus-brand-radius-control: var(--haus-radius-md); /* Button, Input, Select, Textarea */
157
+ --haus-brand-radius-surface: var(--haus-radius-lg); /* Card, Toast */
158
+ --haus-brand-radius-overlay: var(--haus-radius-xl); /* Modal */
159
+ --haus-brand-radius-marker: var(--haus-radius-sm); /* Checkbox, Modal/Toast icons */
160
+
161
+ /* radius-pill stays out of the brand: it is var(--haus-radius-full), which
162
+ is not a step on a scale but "as round as the box allows". A brand that
163
+ set it to something else would not have a rounder pill, it would have a
164
+ Badge that is no longer a pill. */
165
+
166
+ /* ── Elevation ───────────────────────────────────────────────────────── */
167
+ --haus-brand-elevation-raised: var(--haus-shadow-sm); /* Toggle knob */
168
+ --haus-brand-elevation-floating: var(--haus-shadow-lg); /* Card, Toast */
169
+ --haus-brand-elevation-overlay: var(--haus-shadow-xl); /* Modal */
170
+
171
+ /* ── Modal width ─────────────────────────────────────────────────────── */
172
+ /* Modal's three sizes were 400px, 560px and 720px written into
173
+ Modal.module.css, invisible to the hardcoded-value gate because it has no
174
+ rule for width at all, the same blind spot haus#43 found for icons.
175
+
176
+ A brand entry rather than a primitive, because two products disagree and
177
+ both are right: haus's are a web app's, and vault ships four narrower
178
+ steps for a dense desktop app, 360, 420, 480 and 600. A width is a
179
+ proportion of the product's own layout, which is what makes it brand
180
+ rather than scale. vault's fourth stays vault's: a contract cannot have an
181
+ optional member. */
182
+ --haus-brand-modal-width-sm: var(--haus-overlay-width-sm); /* 400px: confirm, short form */
183
+ --haus-brand-modal-width-md: var(--haus-overlay-width-md); /* 560px: the default */
184
+ --haus-brand-modal-width-lg: var(--haus-overlay-width-lg); /* 720px: code, tables, previews */
95
185
 
96
186
  }
97
187
  }
package/dist/guard.cjs CHANGED
@@ -27,8 +27,49 @@ function findFallbackTokens(input) {
27
27
  const defined = collect(input.defines, DECLARATION);
28
28
  return [...withFallback].filter((name) => !defined.has(name)).sort();
29
29
  }
30
+ function normalise(value) {
31
+ return value.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
32
+ }
33
+ function declarations(sources) {
34
+ const out = [];
35
+ for (const css of sources) {
36
+ for (const m of css.matchAll(/(?:^|[;{])\s*(--[a-zA-Z0-9-]+)\s*:\s*([^;}]+)/gm)) {
37
+ out.push({ name: m[1], value: normalise(m[2]) });
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+ function resolve(value, table, depth = 0) {
43
+ if (depth > 10) return value;
44
+ const next = value.replace(/var\(\s*(--[a-zA-Z0-9-]+)\s*\)/g, (whole, name) => {
45
+ const found = table.get(name);
46
+ return found === void 0 ? whole : found;
47
+ });
48
+ return next === value ? value : resolve(next, table, depth + 1);
49
+ }
50
+ function findRestatedTokens(input) {
51
+ const table = /* @__PURE__ */ new Map();
52
+ for (const { name, value } of declarations(input.upstream)) {
53
+ if (!table.has(name)) table.set(name, value);
54
+ }
55
+ const seen = /* @__PURE__ */ new Set();
56
+ const found = [];
57
+ for (const { name, value } of declarations(input.defines)) {
58
+ const upstream = table.get(name);
59
+ if (upstream === void 0 || seen.has(name)) continue;
60
+ let kind;
61
+ if (value === upstream) kind = "identical";
62
+ else if (resolve(value, table) === resolve(upstream, table)) kind = "resolved";
63
+ if (kind) {
64
+ seen.add(name);
65
+ found.push({ name, kind, value, upstream });
66
+ }
67
+ }
68
+ return found.sort((a, b) => a.name.localeCompare(b.name));
69
+ }
30
70
 
31
71
  exports.findFallbackTokens = findFallbackTokens;
72
+ exports.findRestatedTokens = findRestatedTokens;
32
73
  exports.findUndefinedTokens = findUndefinedTokens;
33
74
  //# sourceMappingURL=guard.cjs.map
34
75
  //# sourceMappingURL=guard.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/guard.ts"],"names":[],"mappings":";;;AAuDA,IAAM,gBAAA,GAAmB,iCAAA;AACzB,IAAM,kBAAA,GAAqB,gCAAA;AAK3B,IAAM,WAAA,GAAc,sCAAA;AAuBpB,SAAS,OAAA,CAAQ,SAAmB,OAAA,EAA8B;AAChE,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAY;AAC9B,EAAA,KAAA,MAAW,OAAO,OAAA,EAAS;AAGzB,IAAA,OAAA,CAAQ,SAAA,GAAY,CAAA;AACpB,IAAA,KAAA,MAAW,CAAA,IAAK,IAAI,QAAA,CAAS,OAAO,GAAG,KAAA,CAAM,GAAA,CAAI,CAAA,CAAE,CAAC,CAAE,CAAA;AAAA,EACxD;AACA,EAAA,OAAO,KAAA;AACT;AASO,SAAS,oBAAoB,KAAA,EAA0C;AAC5E,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,KAAA,CAAM,KAAA,EAAO,gBAAgB,CAAA;AAClD,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,KAAA,CAAM,OAAA,EAAS,WAAW,CAAA;AAClD,EAAA,KAAA,MAAW,QAAQ,KAAA,CAAM,WAAA,IAAe,EAAC,EAAG,OAAA,CAAQ,IAAI,IAAI,CAAA;AAE5D,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,CAAC,GAAG,IAAI,EAAE,MAAA,CAAO,CAAC,IAAA,KAAS,CAAC,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAC,EAAE,IAAA,EAAK;AAAA,IAC7D,IAAA,EAAM,CAAC,GAAG,IAAI,EAAE,IAAA,EAAK;AAAA,IACrB,OAAA,EAAS,CAAC,GAAG,OAAO,EAAE,IAAA;AAAK,GAC7B;AACF;AAUO,SAAS,mBAAmB,KAAA,EAA6D;AAC9F,EAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,KAAA,CAAM,KAAA,EAAO,kBAAkB,CAAA;AAC5D,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,KAAA,CAAM,OAAA,EAAS,WAAW,CAAA;AAClD,EAAA,OAAO,CAAC,GAAG,YAAY,CAAA,CAAE,MAAA,CAAO,CAAC,IAAA,KAAS,CAAC,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAC,EAAE,IAAA,EAAK;AACrE","file":"guard.cjs","sourcesContent":["/**\n * The token guard: every custom property a stylesheet reads must be defined\n * somewhere the consumer actually loads.\n *\n * CSS fails silently here, which is what makes this worth shipping. `var(--x)`\n * for an undefined `--x` is invalid at computed-value time: the declaration is\n * dropped and the property inherits. No console warning, no build error,\n * nothing in review: a focus ring is simply absent, and a missing duration\n * looks like a design choice.\n *\n * drift wrote this check for itself and it caught five roles before they\n * reached a screen: `--color-ink-on-primary`, `--elevation-floating`,\n * `--motion-duration-emphasis`, `--radius-marker` and `--haus-focus-ring-error`.\n * The contract is defined in this package, so the check belongs here rather\n * than being rewritten by every consumer (haus#19). Shipping it is also the\n * point: a design system that can say *you have not loaded what my components\n * read* is a different thing from one that hopes you did.\n *\n * **Pure, and it does no file reading.** It takes CSS as strings, so it runs in\n * a Vitest suite, a Node script, a build step or a browser without this package\n * growing a dependency on `node:fs`. The consumer knows which files it loads;\n * this only knows what the contract is.\n *\n * @example\n * ```ts\n * import { readFileSync } from 'node:fs'\n * import { findUndefinedTokens } from 'haus-tokens/guard'\n *\n * const read = (p: string) => readFileSync(p, 'utf8')\n *\n * it('reads no role this app does not load', () => {\n * const { missing } = findUndefinedTokens({\n * reads: [read('node_modules/haus-components/dist/styles.css')],\n * defines: [\n * read('node_modules/haus-tokens/dist/primitives.css'),\n * read('node_modules/haus-tokens/dist/semantics.css'),\n * read('node_modules/haus-tokens/dist/motion.css'),\n * read('src/tokens/overrides.css'),\n * ],\n * })\n * expect(missing).toEqual([])\n * })\n * ```\n */\n\n/**\n * `var(--x)` with no fallback.\n *\n * A reference with a fallback, `var(--x, 0.2s)`, is a real value whether or\n * not the property is set, so it cannot fail at computed-value time and is\n * excluded. It is still usually a sign the name is wrong: a fallback that never\n * loses is a hardcoded value wearing a token's clothes. `findFallbackTokens`\n * below is for looking at those deliberately, rather than failing a build on\n * something that works.\n */\nconst READ_NO_FALLBACK = /var\\(\\s*(--[a-zA-Z0-9-]+)\\s*\\)/g\nconst READ_WITH_FALLBACK = /var\\(\\s*(--[a-zA-Z0-9-]+)\\s*,/g\n// `m` is load-bearing: without it `^` only matches the start of the whole\n// string, so every declaration on its own indented line after a newline is\n// missed and only the first in each block is seen. Caught by this package's\n// own guard test: semantics.css came back with 41 undefined roles.\nconst DECLARATION = /(?:^|[;{])\\s*(--[a-zA-Z0-9-]+)\\s*:/gm\n\nexport interface TokenGuardInput {\n /** The CSS doing the reading: the component stylesheet, your own modules. */\n reads: string[]\n /** The CSS doing the defining: the token layers you load, plus your own. */\n defines: string[]\n /**\n * Names defined outside CSS. A component that sets `style={{ '--x': v }}`\n * defines the property on the element, and no stylesheet will show it.\n */\n alsoDefined?: string[]\n}\n\nexport interface TokenGuardResult {\n /** Read with no fallback and defined nowhere. Sorted, so a diff is stable. */\n missing: string[]\n /** Every property read without a fallback. */\n read: string[]\n /** Every property declared across `defines` and `alsoDefined`. */\n defined: string[]\n}\n\nfunction collect(sources: string[], pattern: RegExp): Set<string> {\n const found = new Set<string>()\n for (const css of sources) {\n // A fresh lastIndex per source: a /g regex is stateful, and reusing one\n // across inputs silently skips the start of every source after the first.\n pattern.lastIndex = 0\n for (const m of css.matchAll(pattern)) found.add(m[1]!)\n }\n return found\n}\n\n/**\n * Which properties are read but never defined.\n *\n * An empty `missing` is the assertion. The other two fields are for a consumer\n * that wants to report rather than fail: `read.length` is also worth asserting\n * as a floor, because a wrong path makes every check pass by finding nothing.\n */\nexport function findUndefinedTokens(input: TokenGuardInput): TokenGuardResult {\n const read = collect(input.reads, READ_NO_FALLBACK)\n const defined = collect(input.defines, DECLARATION)\n for (const name of input.alsoDefined ?? []) defined.add(name)\n\n return {\n missing: [...read].filter((name) => !defined.has(name)).sort(),\n read: [...read].sort(),\n defined: [...defined].sort(),\n }\n}\n\n/**\n * Which properties are only ever read with a fallback.\n *\n * Not a failure, and deliberately a separate function so it cannot be mistaken\n * for one. A fallback that never loses is a hardcoded value wearing a token's\n * clothes, and the list is worth reading occasionally rather than failing a\n * build over.\n */\nexport function findFallbackTokens(input: Pick<TokenGuardInput, 'reads' | 'defines'>): string[] {\n const withFallback = collect(input.reads, READ_WITH_FALLBACK)\n const defined = collect(input.defines, DECLARATION)\n return [...withFallback].filter((name) => !defined.has(name)).sort()\n}\n"]}
1
+ {"version":3,"sources":["../src/guard.ts"],"names":[],"mappings":"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","file":"guard.cjs","sourcesContent":["/**\n * The token guard: every custom property a stylesheet reads must be defined\n * somewhere the consumer actually loads.\n *\n * CSS fails silently here, which is what makes this worth shipping. `var(--x)`\n * for an undefined `--x` is invalid at computed-value time: the declaration is\n * dropped and the property inherits. No console warning, no build error,\n * nothing in review: a focus ring is simply absent, and a missing duration\n * looks like a design choice.\n *\n * drift wrote this check for itself and it caught five roles before they\n * reached a screen: `--color-ink-on-primary`, `--elevation-floating`,\n * `--motion-duration-emphasis`, `--radius-marker` and `--haus-focus-ring-error`.\n * The contract is defined in this package, so the check belongs here rather\n * than being rewritten by every consumer (haus#19). Shipping it is also the\n * point: a design system that can say *you have not loaded what my components\n * read* is a different thing from one that hopes you did.\n *\n * **Pure, and it does no file reading.** It takes CSS as strings, so it runs in\n * a Vitest suite, a Node script, a build step or a browser without this package\n * growing a dependency on `node:fs`. The consumer knows which files it loads;\n * this only knows what the contract is.\n *\n * @example\n * ```ts\n * import { readFileSync } from 'node:fs'\n * import { findUndefinedTokens } from 'haus-tokens/guard'\n *\n * const read = (p: string) => readFileSync(p, 'utf8')\n *\n * it('reads no role this app does not load', () => {\n * const { missing } = findUndefinedTokens({\n * reads: [read('node_modules/haus-components/dist/styles.css')],\n * defines: [\n * read('node_modules/haus-tokens/dist/primitives.css'),\n * read('node_modules/haus-tokens/dist/semantics.css'),\n * read('node_modules/haus-tokens/dist/motion.css'),\n * read('src/tokens/overrides.css'),\n * ],\n * })\n * expect(missing).toEqual([])\n * })\n * ```\n */\n\n/**\n * `var(--x)` with no fallback.\n *\n * A reference with a fallback, `var(--x, 0.2s)`, is a real value whether or\n * not the property is set, so it cannot fail at computed-value time and is\n * excluded. It is still usually a sign the name is wrong: a fallback that never\n * loses is a hardcoded value wearing a token's clothes. `findFallbackTokens`\n * below is for looking at those deliberately, rather than failing a build on\n * something that works.\n */\nconst READ_NO_FALLBACK = /var\\(\\s*(--[a-zA-Z0-9-]+)\\s*\\)/g\nconst READ_WITH_FALLBACK = /var\\(\\s*(--[a-zA-Z0-9-]+)\\s*,/g\n// `m` is load-bearing: without it `^` only matches the start of the whole\n// string, so every declaration on its own indented line after a newline is\n// missed and only the first in each block is seen. Caught by this package's\n// own guard test: semantics.css came back with 41 undefined roles.\nconst DECLARATION = /(?:^|[;{])\\s*(--[a-zA-Z0-9-]+)\\s*:/gm\n\nexport interface TokenGuardInput {\n /** The CSS doing the reading: the component stylesheet, your own modules. */\n reads: string[]\n /** The CSS doing the defining: the token layers you load, plus your own. */\n defines: string[]\n /**\n * Names defined outside CSS. A component that sets `style={{ '--x': v }}`\n * defines the property on the element, and no stylesheet will show it.\n */\n alsoDefined?: string[]\n}\n\nexport interface TokenGuardResult {\n /** Read with no fallback and defined nowhere. Sorted, so a diff is stable. */\n missing: string[]\n /** Every property read without a fallback. */\n read: string[]\n /** Every property declared across `defines` and `alsoDefined`. */\n defined: string[]\n}\n\nfunction collect(sources: string[], pattern: RegExp): Set<string> {\n const found = new Set<string>()\n for (const css of sources) {\n // A fresh lastIndex per source: a /g regex is stateful, and reusing one\n // across inputs silently skips the start of every source after the first.\n pattern.lastIndex = 0\n for (const m of css.matchAll(pattern)) found.add(m[1]!)\n }\n return found\n}\n\n/**\n * Which properties are read but never defined.\n *\n * An empty `missing` is the assertion. The other two fields are for a consumer\n * that wants to report rather than fail: `read.length` is also worth asserting\n * as a floor, because a wrong path makes every check pass by finding nothing.\n */\nexport function findUndefinedTokens(input: TokenGuardInput): TokenGuardResult {\n const read = collect(input.reads, READ_NO_FALLBACK)\n const defined = collect(input.defines, DECLARATION)\n for (const name of input.alsoDefined ?? []) defined.add(name)\n\n return {\n missing: [...read].filter((name) => !defined.has(name)).sort(),\n read: [...read].sort(),\n defined: [...defined].sort(),\n }\n}\n\n/**\n * Which properties are only ever read with a fallback.\n *\n * Not a failure, and deliberately a separate function so it cannot be mistaken\n * for one. A fallback that never loses is a hardcoded value wearing a token's\n * clothes, and the list is worth reading occasionally rather than failing a\n * build over.\n */\nexport function findFallbackTokens(input: Pick<TokenGuardInput, 'reads' | 'defines'>): string[] {\n const withFallback = collect(input.reads, READ_WITH_FALLBACK)\n const defined = collect(input.defines, DECLARATION)\n return [...withFallback].filter((name) => !defined.has(name)).sort()\n}\n\n/* ─── Restatement: a consumer keeping its own copy of a value we ship ────────\n *\n * The second failure this package can see and a consumer cannot. An unresolved\n * var() at least renders wrong; a restated value renders perfectly, and is only\n * wrong from the moment one of the two sides moves.\n *\n * vault carried 141 of them and closed it as `vault#25`, then wrote this rule by\n * hand so it could not come back. drift has 89 and never had the rule: it was\n * written in the repository next door and never ported, which is why it ships\n * from here now (haus#53).\n *\n * TWO KINDS, and the second is the one a hand-written version missed.\n *\n * identical the declaration is the same text on both sides.\n * `--haus-space-inset-md: var(--haus-space-4)` written twice.\n * 77 of drift's.\n *\n * resolved the text differs and the value does not. `--haus-z-modal: 400`\n * against this package's `var(--haus-z-400)`, where `--haus-z-400`\n * is `400`. 12 of drift's, and vault's hand-written rule compares\n * declaration text so it would have found none of them.\n *\n * The second kind is worth the resolver: hardcoding the number a token resolves\n * to is how a consumer opts out of a scale while appearing to be on it.\n */\n\n/** One `--x: value` declaration, in source order. */\ntype Declaration = { name: string; value: string }\n\n/** Comments out, whitespace flattened, so two spellings of one value compare equal. */\nfunction normalise(value: string): string {\n return value\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\nfunction declarations(sources: string[]): Declaration[] {\n const out: Declaration[] = []\n for (const css of sources) {\n for (const m of css.matchAll(/(?:^|[;{])\\s*(--[a-zA-Z0-9-]+)\\s*:\\s*([^;}]+)/gm)) {\n out.push({ name: m[1]!, value: normalise(m[2]!) })\n }\n }\n return out\n}\n\n/**\n * Follow `var()` references until nothing is left to follow.\n *\n * Depth-limited rather than cycle-tracked: a token graph is shallow, and a\n * limit is one line where a visited-set is five. Hitting the limit returns the\n * partially resolved value, which compares unequal and so reports nothing,\n * because a guard that guesses is worse than one that misses.\n */\nfunction resolve(value: string, table: Map<string, string>, depth = 0): string {\n if (depth > 10) return value\n const next = value.replace(/var\\(\\s*(--[a-zA-Z0-9-]+)\\s*\\)/g, (whole, name: string) => {\n const found = table.get(name)\n return found === undefined ? whole : found\n })\n return next === value ? value : resolve(next, table, depth + 1)\n}\n\nexport interface Restatement {\n /** The custom property the consumer declares. */\n name: string\n /** `identical` if the declaration matches ours as text, `resolved` if only the value does. */\n kind: 'identical' | 'resolved'\n /** What the consumer wrote. */\n value: string\n /** What this package already says, as written. */\n upstream: string\n}\n\nexport interface RestatementInput {\n /** The consumer's own token CSS: the file where it overrides or adds. */\n defines: string[]\n /** This package's layers, as the consumer actually loads them. */\n upstream: string[]\n}\n\n/**\n * Which of a consumer's declarations this package already ships.\n *\n * An empty array is the assertion. A consumer may override any role it likes,\n * and several should: what it may not do is override a role to the value the\n * package already gives, because that is not an override, it is a copy, and a\n * copy is correct only until one side moves.\n *\n * Read the package from `node_modules` rather than a fixture. The point is to\n * compare against the value that will actually load.\n *\n * WHICH FILES TO PASS AS `upstream`, because it changes the question asked.\n *\n * Include `brand.css` and the answer is *does this consumer restate anything we\n * ship, including our brand's choices*. Leave it out and the answer is *does it\n * restate anything structural*, with its own colour choices treated as its\n * business.\n *\n * Measured against drift, whose colour overrides are a brand written as role\n * overrides: **143 with `brand.css`, 89 without.** The 54 in the gap are exactly\n * its brand. So a consumer that has a brand file, or intends one, leaves\n * `brand.css` out and gets only the copies; a consumer with no brand of its own\n * puts it in and is told that its colour overrides agree with ours, which is a\n * different and also useful thing to know.\n *\n * @example\n * ```ts\n * import { findRestatedTokens } from 'haus-tokens/guard'\n *\n * it('restates no value haus already ships', () => {\n * const copies = findRestatedTokens({\n * defines: [read('src/tokens/semantics.css')],\n * upstream: HAUS_FILES.map(read),\n * })\n * expect(copies.map((c) => `${c.name} (${c.kind})`)).toEqual([])\n * })\n * ```\n */\nexport function findRestatedTokens(input: RestatementInput): Restatement[] {\n const table = new Map<string, string>()\n for (const { name, value } of declarations(input.upstream)) {\n // First declaration wins, matching the cascade order the consumer loads in.\n if (!table.has(name)) table.set(name, value)\n }\n\n const seen = new Set<string>()\n const found: Restatement[] = []\n for (const { name, value } of declarations(input.defines)) {\n const upstream = table.get(name)\n if (upstream === undefined || seen.has(name)) continue\n\n let kind: Restatement['kind'] | undefined\n if (value === upstream) kind = 'identical'\n else if (resolve(value, table) === resolve(upstream, table)) kind = 'resolved'\n\n if (kind) {\n seen.add(name)\n found.push({ name, kind, value, upstream })\n }\n }\n return found.sort((a, b) => a.name.localeCompare(b.name))\n}\n"]}
package/dist/guard.d.cts CHANGED
@@ -78,5 +78,60 @@ declare function findUndefinedTokens(input: TokenGuardInput): TokenGuardResult;
78
78
  * build over.
79
79
  */
80
80
  declare function findFallbackTokens(input: Pick<TokenGuardInput, 'reads' | 'defines'>): string[];
81
+ interface Restatement {
82
+ /** The custom property the consumer declares. */
83
+ name: string;
84
+ /** `identical` if the declaration matches ours as text, `resolved` if only the value does. */
85
+ kind: 'identical' | 'resolved';
86
+ /** What the consumer wrote. */
87
+ value: string;
88
+ /** What this package already says, as written. */
89
+ upstream: string;
90
+ }
91
+ interface RestatementInput {
92
+ /** The consumer's own token CSS: the file where it overrides or adds. */
93
+ defines: string[];
94
+ /** This package's layers, as the consumer actually loads them. */
95
+ upstream: string[];
96
+ }
97
+ /**
98
+ * Which of a consumer's declarations this package already ships.
99
+ *
100
+ * An empty array is the assertion. A consumer may override any role it likes,
101
+ * and several should: what it may not do is override a role to the value the
102
+ * package already gives, because that is not an override, it is a copy, and a
103
+ * copy is correct only until one side moves.
104
+ *
105
+ * Read the package from `node_modules` rather than a fixture. The point is to
106
+ * compare against the value that will actually load.
107
+ *
108
+ * WHICH FILES TO PASS AS `upstream`, because it changes the question asked.
109
+ *
110
+ * Include `brand.css` and the answer is *does this consumer restate anything we
111
+ * ship, including our brand's choices*. Leave it out and the answer is *does it
112
+ * restate anything structural*, with its own colour choices treated as its
113
+ * business.
114
+ *
115
+ * Measured against drift, whose colour overrides are a brand written as role
116
+ * overrides: **143 with `brand.css`, 89 without.** The 54 in the gap are exactly
117
+ * its brand. So a consumer that has a brand file, or intends one, leaves
118
+ * `brand.css` out and gets only the copies; a consumer with no brand of its own
119
+ * puts it in and is told that its colour overrides agree with ours, which is a
120
+ * different and also useful thing to know.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * import { findRestatedTokens } from 'haus-tokens/guard'
125
+ *
126
+ * it('restates no value haus already ships', () => {
127
+ * const copies = findRestatedTokens({
128
+ * defines: [read('src/tokens/semantics.css')],
129
+ * upstream: HAUS_FILES.map(read),
130
+ * })
131
+ * expect(copies.map((c) => `${c.name} (${c.kind})`)).toEqual([])
132
+ * })
133
+ * ```
134
+ */
135
+ declare function findRestatedTokens(input: RestatementInput): Restatement[];
81
136
 
82
- export { type TokenGuardInput, type TokenGuardResult, findFallbackTokens, findUndefinedTokens };
137
+ export { type Restatement, type RestatementInput, type TokenGuardInput, type TokenGuardResult, findFallbackTokens, findRestatedTokens, findUndefinedTokens };
package/dist/guard.d.ts CHANGED
@@ -78,5 +78,60 @@ declare function findUndefinedTokens(input: TokenGuardInput): TokenGuardResult;
78
78
  * build over.
79
79
  */
80
80
  declare function findFallbackTokens(input: Pick<TokenGuardInput, 'reads' | 'defines'>): string[];
81
+ interface Restatement {
82
+ /** The custom property the consumer declares. */
83
+ name: string;
84
+ /** `identical` if the declaration matches ours as text, `resolved` if only the value does. */
85
+ kind: 'identical' | 'resolved';
86
+ /** What the consumer wrote. */
87
+ value: string;
88
+ /** What this package already says, as written. */
89
+ upstream: string;
90
+ }
91
+ interface RestatementInput {
92
+ /** The consumer's own token CSS: the file where it overrides or adds. */
93
+ defines: string[];
94
+ /** This package's layers, as the consumer actually loads them. */
95
+ upstream: string[];
96
+ }
97
+ /**
98
+ * Which of a consumer's declarations this package already ships.
99
+ *
100
+ * An empty array is the assertion. A consumer may override any role it likes,
101
+ * and several should: what it may not do is override a role to the value the
102
+ * package already gives, because that is not an override, it is a copy, and a
103
+ * copy is correct only until one side moves.
104
+ *
105
+ * Read the package from `node_modules` rather than a fixture. The point is to
106
+ * compare against the value that will actually load.
107
+ *
108
+ * WHICH FILES TO PASS AS `upstream`, because it changes the question asked.
109
+ *
110
+ * Include `brand.css` and the answer is *does this consumer restate anything we
111
+ * ship, including our brand's choices*. Leave it out and the answer is *does it
112
+ * restate anything structural*, with its own colour choices treated as its
113
+ * business.
114
+ *
115
+ * Measured against drift, whose colour overrides are a brand written as role
116
+ * overrides: **143 with `brand.css`, 89 without.** The 54 in the gap are exactly
117
+ * its brand. So a consumer that has a brand file, or intends one, leaves
118
+ * `brand.css` out and gets only the copies; a consumer with no brand of its own
119
+ * puts it in and is told that its colour overrides agree with ours, which is a
120
+ * different and also useful thing to know.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * import { findRestatedTokens } from 'haus-tokens/guard'
125
+ *
126
+ * it('restates no value haus already ships', () => {
127
+ * const copies = findRestatedTokens({
128
+ * defines: [read('src/tokens/semantics.css')],
129
+ * upstream: HAUS_FILES.map(read),
130
+ * })
131
+ * expect(copies.map((c) => `${c.name} (${c.kind})`)).toEqual([])
132
+ * })
133
+ * ```
134
+ */
135
+ declare function findRestatedTokens(input: RestatementInput): Restatement[];
81
136
 
82
- export { type TokenGuardInput, type TokenGuardResult, findFallbackTokens, findUndefinedTokens };
137
+ export { type Restatement, type RestatementInput, type TokenGuardInput, type TokenGuardResult, findFallbackTokens, findRestatedTokens, findUndefinedTokens };
package/dist/guard.js CHANGED
@@ -25,7 +25,47 @@ function findFallbackTokens(input) {
25
25
  const defined = collect(input.defines, DECLARATION);
26
26
  return [...withFallback].filter((name) => !defined.has(name)).sort();
27
27
  }
28
+ function normalise(value) {
29
+ return value.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
30
+ }
31
+ function declarations(sources) {
32
+ const out = [];
33
+ for (const css of sources) {
34
+ for (const m of css.matchAll(/(?:^|[;{])\s*(--[a-zA-Z0-9-]+)\s*:\s*([^;}]+)/gm)) {
35
+ out.push({ name: m[1], value: normalise(m[2]) });
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+ function resolve(value, table, depth = 0) {
41
+ if (depth > 10) return value;
42
+ const next = value.replace(/var\(\s*(--[a-zA-Z0-9-]+)\s*\)/g, (whole, name) => {
43
+ const found = table.get(name);
44
+ return found === void 0 ? whole : found;
45
+ });
46
+ return next === value ? value : resolve(next, table, depth + 1);
47
+ }
48
+ function findRestatedTokens(input) {
49
+ const table = /* @__PURE__ */ new Map();
50
+ for (const { name, value } of declarations(input.upstream)) {
51
+ if (!table.has(name)) table.set(name, value);
52
+ }
53
+ const seen = /* @__PURE__ */ new Set();
54
+ const found = [];
55
+ for (const { name, value } of declarations(input.defines)) {
56
+ const upstream = table.get(name);
57
+ if (upstream === void 0 || seen.has(name)) continue;
58
+ let kind;
59
+ if (value === upstream) kind = "identical";
60
+ else if (resolve(value, table) === resolve(upstream, table)) kind = "resolved";
61
+ if (kind) {
62
+ seen.add(name);
63
+ found.push({ name, kind, value, upstream });
64
+ }
65
+ }
66
+ return found.sort((a, b) => a.name.localeCompare(b.name));
67
+ }
28
68
 
29
- export { findFallbackTokens, findUndefinedTokens };
69
+ export { findFallbackTokens, findRestatedTokens, findUndefinedTokens };
30
70
  //# sourceMappingURL=guard.js.map
31
71
  //# sourceMappingURL=guard.js.map
package/dist/guard.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/guard.ts"],"names":[],"mappings":";AAuDA,IAAM,gBAAA,GAAmB,iCAAA;AACzB,IAAM,kBAAA,GAAqB,gCAAA;AAK3B,IAAM,WAAA,GAAc,sCAAA;AAuBpB,SAAS,OAAA,CAAQ,SAAmB,OAAA,EAA8B;AAChE,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAY;AAC9B,EAAA,KAAA,MAAW,OAAO,OAAA,EAAS;AAGzB,IAAA,OAAA,CAAQ,SAAA,GAAY,CAAA;AACpB,IAAA,KAAA,MAAW,CAAA,IAAK,IAAI,QAAA,CAAS,OAAO,GAAG,KAAA,CAAM,GAAA,CAAI,CAAA,CAAE,CAAC,CAAE,CAAA;AAAA,EACxD;AACA,EAAA,OAAO,KAAA;AACT;AASO,SAAS,oBAAoB,KAAA,EAA0C;AAC5E,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,KAAA,CAAM,KAAA,EAAO,gBAAgB,CAAA;AAClD,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,KAAA,CAAM,OAAA,EAAS,WAAW,CAAA;AAClD,EAAA,KAAA,MAAW,QAAQ,KAAA,CAAM,WAAA,IAAe,EAAC,EAAG,OAAA,CAAQ,IAAI,IAAI,CAAA;AAE5D,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,CAAC,GAAG,IAAI,EAAE,MAAA,CAAO,CAAC,IAAA,KAAS,CAAC,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAC,EAAE,IAAA,EAAK;AAAA,IAC7D,IAAA,EAAM,CAAC,GAAG,IAAI,EAAE,IAAA,EAAK;AAAA,IACrB,OAAA,EAAS,CAAC,GAAG,OAAO,EAAE,IAAA;AAAK,GAC7B;AACF;AAUO,SAAS,mBAAmB,KAAA,EAA6D;AAC9F,EAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,KAAA,CAAM,KAAA,EAAO,kBAAkB,CAAA;AAC5D,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,KAAA,CAAM,OAAA,EAAS,WAAW,CAAA;AAClD,EAAA,OAAO,CAAC,GAAG,YAAY,CAAA,CAAE,MAAA,CAAO,CAAC,IAAA,KAAS,CAAC,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAC,EAAE,IAAA,EAAK;AACrE","file":"guard.js","sourcesContent":["/**\n * The token guard: every custom property a stylesheet reads must be defined\n * somewhere the consumer actually loads.\n *\n * CSS fails silently here, which is what makes this worth shipping. `var(--x)`\n * for an undefined `--x` is invalid at computed-value time: the declaration is\n * dropped and the property inherits. No console warning, no build error,\n * nothing in review: a focus ring is simply absent, and a missing duration\n * looks like a design choice.\n *\n * drift wrote this check for itself and it caught five roles before they\n * reached a screen: `--color-ink-on-primary`, `--elevation-floating`,\n * `--motion-duration-emphasis`, `--radius-marker` and `--haus-focus-ring-error`.\n * The contract is defined in this package, so the check belongs here rather\n * than being rewritten by every consumer (haus#19). Shipping it is also the\n * point: a design system that can say *you have not loaded what my components\n * read* is a different thing from one that hopes you did.\n *\n * **Pure, and it does no file reading.** It takes CSS as strings, so it runs in\n * a Vitest suite, a Node script, a build step or a browser without this package\n * growing a dependency on `node:fs`. The consumer knows which files it loads;\n * this only knows what the contract is.\n *\n * @example\n * ```ts\n * import { readFileSync } from 'node:fs'\n * import { findUndefinedTokens } from 'haus-tokens/guard'\n *\n * const read = (p: string) => readFileSync(p, 'utf8')\n *\n * it('reads no role this app does not load', () => {\n * const { missing } = findUndefinedTokens({\n * reads: [read('node_modules/haus-components/dist/styles.css')],\n * defines: [\n * read('node_modules/haus-tokens/dist/primitives.css'),\n * read('node_modules/haus-tokens/dist/semantics.css'),\n * read('node_modules/haus-tokens/dist/motion.css'),\n * read('src/tokens/overrides.css'),\n * ],\n * })\n * expect(missing).toEqual([])\n * })\n * ```\n */\n\n/**\n * `var(--x)` with no fallback.\n *\n * A reference with a fallback, `var(--x, 0.2s)`, is a real value whether or\n * not the property is set, so it cannot fail at computed-value time and is\n * excluded. It is still usually a sign the name is wrong: a fallback that never\n * loses is a hardcoded value wearing a token's clothes. `findFallbackTokens`\n * below is for looking at those deliberately, rather than failing a build on\n * something that works.\n */\nconst READ_NO_FALLBACK = /var\\(\\s*(--[a-zA-Z0-9-]+)\\s*\\)/g\nconst READ_WITH_FALLBACK = /var\\(\\s*(--[a-zA-Z0-9-]+)\\s*,/g\n// `m` is load-bearing: without it `^` only matches the start of the whole\n// string, so every declaration on its own indented line after a newline is\n// missed and only the first in each block is seen. Caught by this package's\n// own guard test: semantics.css came back with 41 undefined roles.\nconst DECLARATION = /(?:^|[;{])\\s*(--[a-zA-Z0-9-]+)\\s*:/gm\n\nexport interface TokenGuardInput {\n /** The CSS doing the reading: the component stylesheet, your own modules. */\n reads: string[]\n /** The CSS doing the defining: the token layers you load, plus your own. */\n defines: string[]\n /**\n * Names defined outside CSS. A component that sets `style={{ '--x': v }}`\n * defines the property on the element, and no stylesheet will show it.\n */\n alsoDefined?: string[]\n}\n\nexport interface TokenGuardResult {\n /** Read with no fallback and defined nowhere. Sorted, so a diff is stable. */\n missing: string[]\n /** Every property read without a fallback. */\n read: string[]\n /** Every property declared across `defines` and `alsoDefined`. */\n defined: string[]\n}\n\nfunction collect(sources: string[], pattern: RegExp): Set<string> {\n const found = new Set<string>()\n for (const css of sources) {\n // A fresh lastIndex per source: a /g regex is stateful, and reusing one\n // across inputs silently skips the start of every source after the first.\n pattern.lastIndex = 0\n for (const m of css.matchAll(pattern)) found.add(m[1]!)\n }\n return found\n}\n\n/**\n * Which properties are read but never defined.\n *\n * An empty `missing` is the assertion. The other two fields are for a consumer\n * that wants to report rather than fail: `read.length` is also worth asserting\n * as a floor, because a wrong path makes every check pass by finding nothing.\n */\nexport function findUndefinedTokens(input: TokenGuardInput): TokenGuardResult {\n const read = collect(input.reads, READ_NO_FALLBACK)\n const defined = collect(input.defines, DECLARATION)\n for (const name of input.alsoDefined ?? []) defined.add(name)\n\n return {\n missing: [...read].filter((name) => !defined.has(name)).sort(),\n read: [...read].sort(),\n defined: [...defined].sort(),\n }\n}\n\n/**\n * Which properties are only ever read with a fallback.\n *\n * Not a failure, and deliberately a separate function so it cannot be mistaken\n * for one. A fallback that never loses is a hardcoded value wearing a token's\n * clothes, and the list is worth reading occasionally rather than failing a\n * build over.\n */\nexport function findFallbackTokens(input: Pick<TokenGuardInput, 'reads' | 'defines'>): string[] {\n const withFallback = collect(input.reads, READ_WITH_FALLBACK)\n const defined = collect(input.defines, DECLARATION)\n return [...withFallback].filter((name) => !defined.has(name)).sort()\n}\n"]}
1
+ {"version":3,"sources":["../src/guard.ts"],"names":[],"mappings":"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","file":"guard.js","sourcesContent":["/**\n * The token guard: every custom property a stylesheet reads must be defined\n * somewhere the consumer actually loads.\n *\n * CSS fails silently here, which is what makes this worth shipping. `var(--x)`\n * for an undefined `--x` is invalid at computed-value time: the declaration is\n * dropped and the property inherits. No console warning, no build error,\n * nothing in review: a focus ring is simply absent, and a missing duration\n * looks like a design choice.\n *\n * drift wrote this check for itself and it caught five roles before they\n * reached a screen: `--color-ink-on-primary`, `--elevation-floating`,\n * `--motion-duration-emphasis`, `--radius-marker` and `--haus-focus-ring-error`.\n * The contract is defined in this package, so the check belongs here rather\n * than being rewritten by every consumer (haus#19). Shipping it is also the\n * point: a design system that can say *you have not loaded what my components\n * read* is a different thing from one that hopes you did.\n *\n * **Pure, and it does no file reading.** It takes CSS as strings, so it runs in\n * a Vitest suite, a Node script, a build step or a browser without this package\n * growing a dependency on `node:fs`. The consumer knows which files it loads;\n * this only knows what the contract is.\n *\n * @example\n * ```ts\n * import { readFileSync } from 'node:fs'\n * import { findUndefinedTokens } from 'haus-tokens/guard'\n *\n * const read = (p: string) => readFileSync(p, 'utf8')\n *\n * it('reads no role this app does not load', () => {\n * const { missing } = findUndefinedTokens({\n * reads: [read('node_modules/haus-components/dist/styles.css')],\n * defines: [\n * read('node_modules/haus-tokens/dist/primitives.css'),\n * read('node_modules/haus-tokens/dist/semantics.css'),\n * read('node_modules/haus-tokens/dist/motion.css'),\n * read('src/tokens/overrides.css'),\n * ],\n * })\n * expect(missing).toEqual([])\n * })\n * ```\n */\n\n/**\n * `var(--x)` with no fallback.\n *\n * A reference with a fallback, `var(--x, 0.2s)`, is a real value whether or\n * not the property is set, so it cannot fail at computed-value time and is\n * excluded. It is still usually a sign the name is wrong: a fallback that never\n * loses is a hardcoded value wearing a token's clothes. `findFallbackTokens`\n * below is for looking at those deliberately, rather than failing a build on\n * something that works.\n */\nconst READ_NO_FALLBACK = /var\\(\\s*(--[a-zA-Z0-9-]+)\\s*\\)/g\nconst READ_WITH_FALLBACK = /var\\(\\s*(--[a-zA-Z0-9-]+)\\s*,/g\n// `m` is load-bearing: without it `^` only matches the start of the whole\n// string, so every declaration on its own indented line after a newline is\n// missed and only the first in each block is seen. Caught by this package's\n// own guard test: semantics.css came back with 41 undefined roles.\nconst DECLARATION = /(?:^|[;{])\\s*(--[a-zA-Z0-9-]+)\\s*:/gm\n\nexport interface TokenGuardInput {\n /** The CSS doing the reading: the component stylesheet, your own modules. */\n reads: string[]\n /** The CSS doing the defining: the token layers you load, plus your own. */\n defines: string[]\n /**\n * Names defined outside CSS. A component that sets `style={{ '--x': v }}`\n * defines the property on the element, and no stylesheet will show it.\n */\n alsoDefined?: string[]\n}\n\nexport interface TokenGuardResult {\n /** Read with no fallback and defined nowhere. Sorted, so a diff is stable. */\n missing: string[]\n /** Every property read without a fallback. */\n read: string[]\n /** Every property declared across `defines` and `alsoDefined`. */\n defined: string[]\n}\n\nfunction collect(sources: string[], pattern: RegExp): Set<string> {\n const found = new Set<string>()\n for (const css of sources) {\n // A fresh lastIndex per source: a /g regex is stateful, and reusing one\n // across inputs silently skips the start of every source after the first.\n pattern.lastIndex = 0\n for (const m of css.matchAll(pattern)) found.add(m[1]!)\n }\n return found\n}\n\n/**\n * Which properties are read but never defined.\n *\n * An empty `missing` is the assertion. The other two fields are for a consumer\n * that wants to report rather than fail: `read.length` is also worth asserting\n * as a floor, because a wrong path makes every check pass by finding nothing.\n */\nexport function findUndefinedTokens(input: TokenGuardInput): TokenGuardResult {\n const read = collect(input.reads, READ_NO_FALLBACK)\n const defined = collect(input.defines, DECLARATION)\n for (const name of input.alsoDefined ?? []) defined.add(name)\n\n return {\n missing: [...read].filter((name) => !defined.has(name)).sort(),\n read: [...read].sort(),\n defined: [...defined].sort(),\n }\n}\n\n/**\n * Which properties are only ever read with a fallback.\n *\n * Not a failure, and deliberately a separate function so it cannot be mistaken\n * for one. A fallback that never loses is a hardcoded value wearing a token's\n * clothes, and the list is worth reading occasionally rather than failing a\n * build over.\n */\nexport function findFallbackTokens(input: Pick<TokenGuardInput, 'reads' | 'defines'>): string[] {\n const withFallback = collect(input.reads, READ_WITH_FALLBACK)\n const defined = collect(input.defines, DECLARATION)\n return [...withFallback].filter((name) => !defined.has(name)).sort()\n}\n\n/* ─── Restatement: a consumer keeping its own copy of a value we ship ────────\n *\n * The second failure this package can see and a consumer cannot. An unresolved\n * var() at least renders wrong; a restated value renders perfectly, and is only\n * wrong from the moment one of the two sides moves.\n *\n * vault carried 141 of them and closed it as `vault#25`, then wrote this rule by\n * hand so it could not come back. drift has 89 and never had the rule: it was\n * written in the repository next door and never ported, which is why it ships\n * from here now (haus#53).\n *\n * TWO KINDS, and the second is the one a hand-written version missed.\n *\n * identical the declaration is the same text on both sides.\n * `--haus-space-inset-md: var(--haus-space-4)` written twice.\n * 77 of drift's.\n *\n * resolved the text differs and the value does not. `--haus-z-modal: 400`\n * against this package's `var(--haus-z-400)`, where `--haus-z-400`\n * is `400`. 12 of drift's, and vault's hand-written rule compares\n * declaration text so it would have found none of them.\n *\n * The second kind is worth the resolver: hardcoding the number a token resolves\n * to is how a consumer opts out of a scale while appearing to be on it.\n */\n\n/** One `--x: value` declaration, in source order. */\ntype Declaration = { name: string; value: string }\n\n/** Comments out, whitespace flattened, so two spellings of one value compare equal. */\nfunction normalise(value: string): string {\n return value\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\nfunction declarations(sources: string[]): Declaration[] {\n const out: Declaration[] = []\n for (const css of sources) {\n for (const m of css.matchAll(/(?:^|[;{])\\s*(--[a-zA-Z0-9-]+)\\s*:\\s*([^;}]+)/gm)) {\n out.push({ name: m[1]!, value: normalise(m[2]!) })\n }\n }\n return out\n}\n\n/**\n * Follow `var()` references until nothing is left to follow.\n *\n * Depth-limited rather than cycle-tracked: a token graph is shallow, and a\n * limit is one line where a visited-set is five. Hitting the limit returns the\n * partially resolved value, which compares unequal and so reports nothing,\n * because a guard that guesses is worse than one that misses.\n */\nfunction resolve(value: string, table: Map<string, string>, depth = 0): string {\n if (depth > 10) return value\n const next = value.replace(/var\\(\\s*(--[a-zA-Z0-9-]+)\\s*\\)/g, (whole, name: string) => {\n const found = table.get(name)\n return found === undefined ? whole : found\n })\n return next === value ? value : resolve(next, table, depth + 1)\n}\n\nexport interface Restatement {\n /** The custom property the consumer declares. */\n name: string\n /** `identical` if the declaration matches ours as text, `resolved` if only the value does. */\n kind: 'identical' | 'resolved'\n /** What the consumer wrote. */\n value: string\n /** What this package already says, as written. */\n upstream: string\n}\n\nexport interface RestatementInput {\n /** The consumer's own token CSS: the file where it overrides or adds. */\n defines: string[]\n /** This package's layers, as the consumer actually loads them. */\n upstream: string[]\n}\n\n/**\n * Which of a consumer's declarations this package already ships.\n *\n * An empty array is the assertion. A consumer may override any role it likes,\n * and several should: what it may not do is override a role to the value the\n * package already gives, because that is not an override, it is a copy, and a\n * copy is correct only until one side moves.\n *\n * Read the package from `node_modules` rather than a fixture. The point is to\n * compare against the value that will actually load.\n *\n * WHICH FILES TO PASS AS `upstream`, because it changes the question asked.\n *\n * Include `brand.css` and the answer is *does this consumer restate anything we\n * ship, including our brand's choices*. Leave it out and the answer is *does it\n * restate anything structural*, with its own colour choices treated as its\n * business.\n *\n * Measured against drift, whose colour overrides are a brand written as role\n * overrides: **143 with `brand.css`, 89 without.** The 54 in the gap are exactly\n * its brand. So a consumer that has a brand file, or intends one, leaves\n * `brand.css` out and gets only the copies; a consumer with no brand of its own\n * puts it in and is told that its colour overrides agree with ours, which is a\n * different and also useful thing to know.\n *\n * @example\n * ```ts\n * import { findRestatedTokens } from 'haus-tokens/guard'\n *\n * it('restates no value haus already ships', () => {\n * const copies = findRestatedTokens({\n * defines: [read('src/tokens/semantics.css')],\n * upstream: HAUS_FILES.map(read),\n * })\n * expect(copies.map((c) => `${c.name} (${c.kind})`)).toEqual([])\n * })\n * ```\n */\nexport function findRestatedTokens(input: RestatementInput): Restatement[] {\n const table = new Map<string, string>()\n for (const { name, value } of declarations(input.upstream)) {\n // First declaration wins, matching the cascade order the consumer loads in.\n if (!table.has(name)) table.set(name, value)\n }\n\n const seen = new Set<string>()\n const found: Restatement[] = []\n for (const { name, value } of declarations(input.defines)) {\n const upstream = table.get(name)\n if (upstream === undefined || seen.has(name)) continue\n\n let kind: Restatement['kind'] | undefined\n if (value === upstream) kind = 'identical'\n else if (resolve(value, table) === resolve(upstream, table)) kind = 'resolved'\n\n if (kind) {\n seen.add(name)\n found.push({ name, kind, value, upstream })\n }\n }\n return found.sort((a, b) => a.name.localeCompare(b.name))\n}\n"]}
package/dist/index.cjs CHANGED
@@ -29,6 +29,7 @@ var brandRoles = [
29
29
  "--haus-brand-primary-subtle",
30
30
  "--haus-brand-primary-on-subtle",
31
31
  "--haus-brand-primary-disabled",
32
+ "--haus-brand-backdrop",
32
33
  "--haus-brand-info-subtle",
33
34
  "--haus-brand-info-border",
34
35
  "--haus-brand-info-default",
@@ -55,8 +56,87 @@ var brandRoles = [
55
56
  "--haus-brand-error-on-subtle",
56
57
  "--haus-brand-error-on-default",
57
58
  "--haus-brand-error-emphasis",
59
+ "--haus-brand-radius-control",
60
+ "--haus-brand-radius-surface",
61
+ "--haus-brand-radius-overlay",
62
+ "--haus-brand-radius-marker",
63
+ "--haus-brand-elevation-raised",
64
+ "--haus-brand-elevation-floating",
65
+ "--haus-brand-elevation-overlay",
66
+ "--haus-brand-modal-width-sm",
67
+ "--haus-brand-modal-width-md",
68
+ "--haus-brand-modal-width-lg"
69
+ ];
70
+ var brandRolesBase = [
71
+ "--haus-brand-surface-default",
72
+ "--haus-brand-surface-subtle",
73
+ "--haus-brand-surface-raised",
74
+ "--haus-brand-surface-overlay",
75
+ "--haus-brand-surface-sunken",
76
+ "--haus-brand-surface-inverse",
77
+ "--haus-brand-surface-disabled",
78
+ "--haus-brand-surface-inverse-hover",
79
+ "--haus-brand-ink-primary",
80
+ "--haus-brand-ink-secondary",
81
+ "--haus-brand-ink-tertiary",
82
+ "--haus-brand-ink-disabled",
83
+ "--haus-brand-ink-inverse",
84
+ "--haus-brand-ink-link",
85
+ "--haus-brand-ink-on-primary",
86
+ "--haus-brand-border-subtle",
87
+ "--haus-brand-border-default",
88
+ "--haus-brand-border-strong",
89
+ "--haus-brand-border-disabled",
90
+ "--haus-brand-border-inverse",
91
+ "--haus-brand-border-inverse-hover",
92
+ "--haus-brand-primary-default",
93
+ "--haus-brand-primary-hover",
94
+ "--haus-brand-primary-pressed",
95
+ "--haus-brand-primary-subtle",
96
+ "--haus-brand-primary-on-subtle",
97
+ "--haus-brand-primary-disabled",
58
98
  "--haus-brand-backdrop"
59
99
  ];
100
+ var brandRolesFeedback = [
101
+ "--haus-brand-info-subtle",
102
+ "--haus-brand-info-border",
103
+ "--haus-brand-info-default",
104
+ "--haus-brand-info-on-subtle",
105
+ "--haus-brand-info-on-default",
106
+ "--haus-brand-info-emphasis",
107
+ "--haus-brand-success-subtle",
108
+ "--haus-brand-success-border",
109
+ "--haus-brand-success-default",
110
+ "--haus-brand-success-on-subtle",
111
+ "--haus-brand-success-on-default",
112
+ "--haus-brand-success-solid",
113
+ "--haus-brand-success-emphasis",
114
+ "--haus-brand-warning-subtle",
115
+ "--haus-brand-warning-border",
116
+ "--haus-brand-warning-default",
117
+ "--haus-brand-warning-on-subtle",
118
+ "--haus-brand-warning-on-default",
119
+ "--haus-brand-warning-solid",
120
+ "--haus-brand-warning-emphasis",
121
+ "--haus-brand-error-subtle",
122
+ "--haus-brand-error-border",
123
+ "--haus-brand-error-default",
124
+ "--haus-brand-error-on-subtle",
125
+ "--haus-brand-error-on-default",
126
+ "--haus-brand-error-emphasis"
127
+ ];
128
+ var brandRolesForm = [
129
+ "--haus-brand-radius-control",
130
+ "--haus-brand-radius-surface",
131
+ "--haus-brand-radius-overlay",
132
+ "--haus-brand-radius-marker",
133
+ "--haus-brand-elevation-raised",
134
+ "--haus-brand-elevation-floating",
135
+ "--haus-brand-elevation-overlay",
136
+ "--haus-brand-modal-width-sm",
137
+ "--haus-brand-modal-width-md",
138
+ "--haus-brand-modal-width-lg"
139
+ ];
60
140
 
61
141
  // src/index.ts
62
142
  var tokens = {
@@ -250,6 +330,9 @@ var tokens = {
250
330
  };
251
331
 
252
332
  exports.brandRoles = brandRoles;
333
+ exports.brandRolesBase = brandRolesBase;
334
+ exports.brandRolesFeedback = brandRolesFeedback;
335
+ exports.brandRolesForm = brandRolesForm;
253
336
  exports.tokens = tokens;
254
337
  //# sourceMappingURL=index.cjs.map
255
338
  //# sourceMappingURL=index.cjs.map