haus-tokens 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -13
- package/dist/brand.css +97 -0
- package/dist/brands/vault.css +125 -0
- package/dist/guard.cjs +34 -0
- package/dist/guard.cjs.map +1 -0
- package/dist/guard.d.cts +82 -0
- package/dist/guard.d.ts +82 -0
- package/dist/guard.js +31 -0
- package/dist/guard.js.map +1 -0
- package/dist/index.cjs +252 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +17 -0
- package/dist/index.d.cts +256 -0
- package/dist/index.d.ts +87 -17
- package/dist/index.js +79 -18
- package/dist/index.js.map +1 -1
- package/dist/layers.css +15 -0
- package/dist/motion.css +29 -38
- package/dist/primitives.css +120 -117
- package/dist/semantics.css +268 -148
- package/dist/tokens.json +764 -203
- package/package.json +22 -6
package/README.md
CHANGED
|
@@ -44,11 +44,21 @@ Two kinds of primitive read are documented rather than hidden. Sizes: 31
|
|
|
44
44
|
declarations across the haus components take a size off the space ladder, on
|
|
45
45
|
`height`, `width`, `min-*`/`max-*` and `transform` offsets, because a size is a
|
|
46
46
|
value rather than a role. They are the avatar sizes, the checkbox and radio
|
|
47
|
-
boxes, the toggle track and thumb, and a few min/max bounds. And
|
|
48
|
-
because the primitive's own name already is the role:
|
|
49
|
-
`--
|
|
50
|
-
`--
|
|
51
|
-
|
|
47
|
+
boxes, the toggle track and thumb, and a few min/max bounds. And 41 read a primitive that has no semantic
|
|
48
|
+
alias because the primitive's own name already is the role: 32 read
|
|
49
|
+
`--haus-font-sans`, five a control height, three `--haus-icon-sm` and one
|
|
50
|
+
`--haus-tracking-normal`. A font family is not a role and there is no honest
|
|
51
|
+
alias to invent for it. No component reads a colour, radius, shadow, stacking or
|
|
52
|
+
motion primitive, and a test in `haus-components` holds that line.
|
|
53
|
+
|
|
54
|
+
That second number went 61 to 77 when the six overlay components landed, then
|
|
55
|
+
**77 to 41 at the 1.0 cut**, and the fall is the part worth reading. `Popover`
|
|
56
|
+
and `Tooltip` had to reach for `--haus-z-*` directly because eight z-index
|
|
57
|
+
primitives had no role between them, and a product wanting the stacking order
|
|
58
|
+
had to read `primitives.css` for it, which is why drift and vault each settled
|
|
59
|
+
one for themselves. haus#27 put the ladders in `primitives.css` and the names in
|
|
60
|
+
`semantics.css`. **No component declaration changed**: the same reads resolve
|
|
61
|
+
one layer higher.
|
|
52
62
|
|
|
53
63
|
## Typed constants
|
|
54
64
|
|
|
@@ -80,17 +90,80 @@ numbers, a composite is an alias:
|
|
|
80
90
|
"motion": { "fade-in": { "$type": "string", "$value": "{duration.normal} {easing.enter}" } }
|
|
81
91
|
```
|
|
82
92
|
|
|
93
|
+
## The guard
|
|
94
|
+
|
|
95
|
+
`var(--x)` for an undefined `--x` is invalid at computed-value time: the
|
|
96
|
+
declaration is dropped and the property inherits. No console warning, no build
|
|
97
|
+
error, nothing in review: a focus ring is simply absent, and a missing duration
|
|
98
|
+
looks like a design choice.
|
|
99
|
+
|
|
100
|
+
That makes *have you loaded what my components read* a question worth failing a
|
|
101
|
+
build over, and it is a question this package can answer because this package
|
|
102
|
+
defines the contract. Import it from your own suite:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { readFileSync } from 'node:fs'
|
|
106
|
+
import { findUndefinedTokens } from 'haus-tokens/guard'
|
|
107
|
+
|
|
108
|
+
const read = (p: string) => readFileSync(p, 'utf8')
|
|
109
|
+
|
|
110
|
+
it('reads no role this app does not load', () => {
|
|
111
|
+
const { missing, read: reads } = findUndefinedTokens({
|
|
112
|
+
reads: [read('node_modules/haus-components/dist/styles.css')],
|
|
113
|
+
defines: [
|
|
114
|
+
read('node_modules/haus-tokens/dist/primitives.css'),
|
|
115
|
+
read('node_modules/haus-tokens/dist/motion.css'),
|
|
116
|
+
read('node_modules/haus-tokens/dist/semantics.css'),
|
|
117
|
+
read('src/tokens/overrides.css'),
|
|
118
|
+
],
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Assert the floor as well as the failure: a wrong path makes the check pass
|
|
122
|
+
// by finding nothing.
|
|
123
|
+
expect(reads.length).toBeGreaterThan(50)
|
|
124
|
+
expect(missing).toEqual([])
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
It is **pure and does no file reading**, so it runs anywhere and this package
|
|
129
|
+
gains no dependency on `node:fs`. You know which files you load; it only knows
|
|
130
|
+
what the contract is.
|
|
131
|
+
|
|
132
|
+
`alsoDefined` takes properties set outside CSS, a component doing
|
|
133
|
+
`style={{ '--haus-avatar-bg': v }}` defines one that no stylesheet will show.
|
|
134
|
+
|
|
135
|
+
A read carrying a fallback, `var(--x, 0.2s)`, is a real value either way and is
|
|
136
|
+
not a failure. `findFallbackTokens` lists those separately, because a fallback
|
|
137
|
+
that never loses is a hardcoded value wearing a token's clothes and that is
|
|
138
|
+
worth reading occasionally rather than failing a build over.
|
|
139
|
+
|
|
140
|
+
**Why it exists.** drift wrote this check for itself and it caught five roles
|
|
141
|
+
before they reached a screen: `--color-ink-on-primary`, `--elevation-floating`,
|
|
142
|
+
`--motion-duration-emphasis`, `--radius-marker` and `--haus-focus-ring-error`.
|
|
143
|
+
Every consumer after it would have written the same test or shipped the same
|
|
144
|
+
silent hole.
|
|
145
|
+
|
|
146
|
+
It is also run against this package. `src/guard.test.ts` checks `semantics.css`
|
|
147
|
+
and `brands/vault.css` resolve against the layers below them: which is how the
|
|
148
|
+
guard's own regex bug was found, a missing `m` flag that reported 41 undefined
|
|
149
|
+
roles in a file that has none.
|
|
150
|
+
|
|
83
151
|
## Three copies, one truth
|
|
84
152
|
|
|
85
|
-
Stating the same tokens
|
|
86
|
-
system exists to prevent
|
|
87
|
-
`tokens.
|
|
88
|
-
from
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
153
|
+
Stating the same tokens more than once invites exactly the drift this design
|
|
154
|
+
system exists to prevent, so most of the restatements are not stated at all.
|
|
155
|
+
`scripts/build-tokens.ts` writes five files: `primitives.css` and `motion.css`
|
|
156
|
+
from `tokens.json`, `index.ts` as the typed export, `brand.ts` as the `BrandMap`
|
|
157
|
+
type from `brand.css`, and `tokens.json`'s own `semantic.color` block from
|
|
158
|
+
`brand.css` as well.
|
|
159
|
+
|
|
160
|
+
`pnpm run tokens:check` regenerates all five in memory and fails if what is
|
|
161
|
+
committed differs, which is the first step CI runs. Change a source and
|
|
162
|
+
`pnpm run tokens` writes the derived files; change a derived file by hand and CI
|
|
163
|
+
says so, which has already caught someone editing a comment inside one.
|
|
92
164
|
|
|
93
|
-
|
|
165
|
+
Two token files are written by hand. `brand.css` says which primitive each role
|
|
166
|
+
takes, and `semantics.css` says what each role means, because a role is a
|
|
94
167
|
decision rather than a derivation. `semantics.test.ts` holds it to the rules
|
|
95
168
|
this package states: every token it reads is declared in the layers below, no
|
|
96
169
|
declaration carries a raw value, every subtle and default surface has its
|
package/dist/brand.css
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* ─── haus / brand ───────────────────────────────────────────────────────────
|
|
2
|
+
Which primitive each role takes. The one file a consumer replaces.
|
|
3
|
+
|
|
4
|
+
It contains nothing else: no new names, no raw values, no structure. A brand
|
|
5
|
+
author sees a flat list of ramp choices; a component author sees an unchanged
|
|
6
|
+
role name in semantics.css. That separation is the whole of ruling A3 — before
|
|
7
|
+
it, the brand and the role system were the same file, so taking haus's roles
|
|
8
|
+
meant taking haus's palette, and two products declared their own tokens rather
|
|
9
|
+
than do that.
|
|
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().
|
|
14
|
+
|
|
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.
|
|
18
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
@layer haus.brand {
|
|
21
|
+
|
|
22
|
+
:root {
|
|
23
|
+
|
|
24
|
+
/* ── Surfaces ────────────────────────────────────────────────────────── */
|
|
25
|
+
--haus-brand-surface-default: var(--haus-damson-0);
|
|
26
|
+
--haus-brand-surface-subtle: var(--haus-damson-100);
|
|
27
|
+
--haus-brand-surface-raised: var(--haus-damson-0); /* elevation via shadow, not colour */
|
|
28
|
+
--haus-brand-surface-overlay: var(--haus-damson-50); /* modal / popover, barely off-white */
|
|
29
|
+
--haus-brand-surface-sunken: var(--haus-damson-200);
|
|
30
|
+
--haus-brand-surface-inverse: var(--haus-damson-900); /* dark chip, tooltip bg in light mode */
|
|
31
|
+
--haus-brand-surface-disabled: var(--haus-damson-100);
|
|
32
|
+
--haus-brand-surface-inverse-hover: oklch(from var(--haus-damson-0) l c h / 0.10);
|
|
33
|
+
|
|
34
|
+
/* ── Ink ─────────────────────────────────────────────────────────────── */
|
|
35
|
+
--haus-brand-ink-primary: var(--haus-damson-900);
|
|
36
|
+
--haus-brand-ink-secondary: var(--haus-damson-700);
|
|
37
|
+
--haus-brand-ink-tertiary: var(--haus-damson-500);
|
|
38
|
+
--haus-brand-ink-disabled: var(--haus-damson-400);
|
|
39
|
+
--haus-brand-ink-inverse: var(--haus-damson-0);
|
|
40
|
+
--haus-brand-ink-link: var(--haus-aronia-500);
|
|
41
|
+
--haus-brand-ink-on-primary: var(--haus-damson-0);
|
|
42
|
+
|
|
43
|
+
/* ── Borders ─────────────────────────────────────────────────────────── */
|
|
44
|
+
--haus-brand-border-subtle: var(--haus-damson-100);
|
|
45
|
+
--haus-brand-border-default: var(--haus-damson-200);
|
|
46
|
+
--haus-brand-border-strong: var(--haus-damson-300);
|
|
47
|
+
--haus-brand-border-disabled: var(--haus-damson-200);
|
|
48
|
+
--haus-brand-border-inverse: oklch(from var(--haus-damson-0) l c h / 0.30);
|
|
49
|
+
--haus-brand-border-inverse-hover: oklch(from var(--haus-damson-0) l c h / 0.50);
|
|
50
|
+
|
|
51
|
+
/* ── Primary ─────────────────────────────────────────────────────────── */
|
|
52
|
+
--haus-brand-primary-default: var(--haus-aronia-500);
|
|
53
|
+
--haus-brand-primary-hover: var(--haus-aronia-600);
|
|
54
|
+
--haus-brand-primary-pressed: var(--haus-aronia-700);
|
|
55
|
+
--haus-brand-primary-subtle: var(--haus-aronia-100);
|
|
56
|
+
--haus-brand-primary-on-subtle: var(--haus-aronia-700);
|
|
57
|
+
--haus-brand-primary-disabled: var(--haus-damson-200);
|
|
58
|
+
|
|
59
|
+
/* ── Info ────────────────────────────────────────────────────────────── */
|
|
60
|
+
--haus-brand-info-subtle: var(--haus-elderberry-100);
|
|
61
|
+
--haus-brand-info-border: var(--haus-elderberry-200);
|
|
62
|
+
--haus-brand-info-default: var(--haus-elderberry-500);
|
|
63
|
+
--haus-brand-info-on-subtle: var(--haus-elderberry-700);
|
|
64
|
+
--haus-brand-info-on-default: var(--haus-damson-0);
|
|
65
|
+
--haus-brand-info-emphasis: var(--haus-elderberry-900);
|
|
66
|
+
|
|
67
|
+
/* ── Success ─────────────────────────────────────────────────────────── */
|
|
68
|
+
--haus-brand-success-subtle: var(--haus-greengage-100);
|
|
69
|
+
--haus-brand-success-border: var(--haus-greengage-200);
|
|
70
|
+
--haus-brand-success-default: var(--haus-greengage-500);
|
|
71
|
+
--haus-brand-success-on-subtle: var(--haus-greengage-700);
|
|
72
|
+
--haus-brand-success-on-default: var(--haus-damson-900); /* dark text, because greengage-500 fails with white */
|
|
73
|
+
--haus-brand-success-solid: var(--haus-greengage-700); /* 700-level: white text passes at 6.82:1 */
|
|
74
|
+
--haus-brand-success-emphasis: var(--haus-greengage-900);
|
|
75
|
+
|
|
76
|
+
/* ── Warning ─────────────────────────────────────────────────────────── */
|
|
77
|
+
--haus-brand-warning-subtle: var(--haus-mango-100);
|
|
78
|
+
--haus-brand-warning-border: var(--haus-mango-200);
|
|
79
|
+
--haus-brand-warning-default: var(--haus-mango-500);
|
|
80
|
+
--haus-brand-warning-on-subtle: var(--haus-mango-700);
|
|
81
|
+
--haus-brand-warning-on-default: var(--haus-damson-900); /* dark text, because mango-500 fails with white */
|
|
82
|
+
--haus-brand-warning-solid: var(--haus-mango-700); /* 700-level: white text passes at 7.05:1 */
|
|
83
|
+
--haus-brand-warning-emphasis: var(--haus-mango-900);
|
|
84
|
+
|
|
85
|
+
/* ── Error ───────────────────────────────────────────────────────────── */
|
|
86
|
+
--haus-brand-error-subtle: var(--haus-cherry-100);
|
|
87
|
+
--haus-brand-error-border: var(--haus-cherry-200);
|
|
88
|
+
--haus-brand-error-default: var(--haus-cherry-500);
|
|
89
|
+
--haus-brand-error-on-subtle: var(--haus-cherry-700);
|
|
90
|
+
--haus-brand-error-on-default: var(--haus-damson-0);
|
|
91
|
+
--haus-brand-error-emphasis: var(--haus-cherry-900);
|
|
92
|
+
|
|
93
|
+
/* ── Everything else ─────────────────────────────────────────────────── */
|
|
94
|
+
--haus-brand-backdrop: oklch(from var(--haus-damson-950) l c h / var(--haus-opacity-60));
|
|
95
|
+
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* ─── haus / brands / vault ──────────────────────────────────────────────────
|
|
2
|
+
A second, complete brand, and the reason it exists is that **a contract with
|
|
3
|
+
one implementation is not a contract**. The split in brand.css only proves
|
|
4
|
+
anything if a different brand can be written against it without touching
|
|
5
|
+
semantics.css or a single component, and this is that proof.
|
|
6
|
+
|
|
7
|
+
It is vault's ruby, not an invention. vault is a shipped product with its own
|
|
8
|
+
palette that declared 159 custom properties rather than take haus's, and its
|
|
9
|
+
migration is a later step, so writing the brand here is rehearsal rather than
|
|
10
|
+
decoration.
|
|
11
|
+
|
|
12
|
+
**The brand is `vault` and the ramp stays `ruby`, renamed at the 1.0 cut.**
|
|
13
|
+
Brand answers *whose* and ramp answers *which colour*. vault ships six
|
|
14
|
+
gemstone ramps, ruby among them, so naming the ramp `--haus-vault-500` would
|
|
15
|
+
claim vault has one colour. `[data-haus-theme='vault']` holding
|
|
16
|
+
`--haus-ruby-500` reads correctly in both directions.
|
|
17
|
+
|
|
18
|
+
The ten ramp steps gained the `--haus-` prefix in the same cut. They were the
|
|
19
|
+
only unprefixed custom properties in the system, and the prefix is not
|
|
20
|
+
tidiness: an unprefixed `--ruby-500` collides with a consumer running
|
|
21
|
+
Tailwind.
|
|
22
|
+
|
|
23
|
+
Applied by attribute, and it nests:
|
|
24
|
+
|
|
25
|
+
<div data-haus-theme="vault"> … </div>
|
|
26
|
+
|
|
27
|
+
Every entry brand.css declares appears here. That is not tidiness: `BrandMap`
|
|
28
|
+
is generated from brand.css, so an omission is a type error rather than an
|
|
29
|
+
unresolved var() that silently drops a declaration.
|
|
30
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
31
|
+
|
|
32
|
+
@layer haus.brand {
|
|
33
|
+
|
|
34
|
+
[data-haus-theme='vault'] {
|
|
35
|
+
|
|
36
|
+
/* ── The ruby palette ──────────────────────────────────────────────────
|
|
37
|
+
Declared here rather than in primitives.css: a brand brings its own ramp.
|
|
38
|
+
Values are vault's, unchanged — this is a real brand rather than a
|
|
39
|
+
plausible one, which is the point of shipping it. */
|
|
40
|
+
--haus-ruby-100: oklch(96.5% 0.016 3);
|
|
41
|
+
--haus-ruby-200: oklch(91% 0.044 3);
|
|
42
|
+
--haus-ruby-300: oklch(82% 0.082 3);
|
|
43
|
+
--haus-ruby-400: oklch(64% 0.15 3);
|
|
44
|
+
--haus-ruby-500: oklch(48.2% 0.186 2.5);
|
|
45
|
+
--haus-ruby-600: oklch(44% 0.176 1.7);
|
|
46
|
+
--haus-ruby-700: oklch(40.5% 0.164 0.9);
|
|
47
|
+
--haus-ruby-800: oklch(33% 0.116 0);
|
|
48
|
+
--haus-ruby-900: oklch(23% 0.09 358);
|
|
49
|
+
--haus-ruby-950: oklch(15% 0.056 356);
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
/* ── Surface ───────────────────────────────────────────────────────── */
|
|
53
|
+
--haus-brand-surface-default: var(--haus-damson-0);
|
|
54
|
+
--haus-brand-surface-subtle: var(--haus-damson-100);
|
|
55
|
+
--haus-brand-surface-raised: var(--haus-damson-0);
|
|
56
|
+
--haus-brand-surface-overlay: var(--haus-damson-50);
|
|
57
|
+
--haus-brand-surface-sunken: var(--haus-damson-200);
|
|
58
|
+
--haus-brand-surface-inverse: var(--haus-damson-900);
|
|
59
|
+
--haus-brand-surface-disabled: var(--haus-damson-100);
|
|
60
|
+
--haus-brand-surface-inverse-hover: oklch(from var(--haus-damson-0) l c h / 0.10);
|
|
61
|
+
|
|
62
|
+
/* ── Ink ───────────────────────────────────────────────────────────── */
|
|
63
|
+
--haus-brand-ink-primary: var(--haus-damson-900);
|
|
64
|
+
--haus-brand-ink-secondary: var(--haus-damson-700);
|
|
65
|
+
--haus-brand-ink-tertiary: var(--haus-damson-500);
|
|
66
|
+
--haus-brand-ink-disabled: var(--haus-damson-400);
|
|
67
|
+
--haus-brand-ink-inverse: var(--haus-damson-0);
|
|
68
|
+
--haus-brand-ink-link: var(--haus-ruby-500);
|
|
69
|
+
--haus-brand-ink-on-primary: var(--haus-damson-0);
|
|
70
|
+
|
|
71
|
+
/* ── Border ────────────────────────────────────────────────────────── */
|
|
72
|
+
--haus-brand-border-subtle: var(--haus-damson-100);
|
|
73
|
+
--haus-brand-border-default: var(--haus-damson-200);
|
|
74
|
+
--haus-brand-border-strong: var(--haus-damson-300);
|
|
75
|
+
--haus-brand-border-disabled: var(--haus-damson-200);
|
|
76
|
+
--haus-brand-border-inverse: oklch(from var(--haus-damson-0) l c h / 0.30);
|
|
77
|
+
--haus-brand-border-inverse-hover: oklch(from var(--haus-damson-0) l c h / 0.50);
|
|
78
|
+
|
|
79
|
+
/* ── Primary ───────────────────────────────────────────────────────── */
|
|
80
|
+
--haus-brand-primary-default: var(--haus-ruby-500);
|
|
81
|
+
--haus-brand-primary-hover: var(--haus-ruby-600);
|
|
82
|
+
--haus-brand-primary-pressed: var(--haus-ruby-700);
|
|
83
|
+
--haus-brand-primary-subtle: var(--haus-ruby-100);
|
|
84
|
+
--haus-brand-primary-on-subtle: var(--haus-ruby-700);
|
|
85
|
+
--haus-brand-primary-disabled: var(--haus-damson-200);
|
|
86
|
+
|
|
87
|
+
/* ── Info ──────────────────────────────────────────────────────────── */
|
|
88
|
+
--haus-brand-info-subtle: var(--haus-elderberry-100);
|
|
89
|
+
--haus-brand-info-border: var(--haus-elderberry-200);
|
|
90
|
+
--haus-brand-info-default: var(--haus-elderberry-500);
|
|
91
|
+
--haus-brand-info-on-subtle: var(--haus-elderberry-700);
|
|
92
|
+
--haus-brand-info-on-default: var(--haus-damson-0);
|
|
93
|
+
--haus-brand-info-emphasis: var(--haus-elderberry-900);
|
|
94
|
+
|
|
95
|
+
/* ── Success ───────────────────────────────────────────────────────── */
|
|
96
|
+
--haus-brand-success-subtle: var(--haus-greengage-100);
|
|
97
|
+
--haus-brand-success-border: var(--haus-greengage-200);
|
|
98
|
+
--haus-brand-success-default: var(--haus-greengage-500);
|
|
99
|
+
--haus-brand-success-on-subtle: var(--haus-greengage-700);
|
|
100
|
+
--haus-brand-success-on-default: var(--haus-damson-900);
|
|
101
|
+
--haus-brand-success-solid: var(--haus-greengage-700);
|
|
102
|
+
--haus-brand-success-emphasis: var(--haus-greengage-900);
|
|
103
|
+
|
|
104
|
+
/* ── Warning ───────────────────────────────────────────────────────── */
|
|
105
|
+
--haus-brand-warning-subtle: var(--haus-mango-100);
|
|
106
|
+
--haus-brand-warning-border: var(--haus-mango-200);
|
|
107
|
+
--haus-brand-warning-default: var(--haus-mango-500);
|
|
108
|
+
--haus-brand-warning-on-subtle: var(--haus-mango-700);
|
|
109
|
+
--haus-brand-warning-on-default: var(--haus-damson-900);
|
|
110
|
+
--haus-brand-warning-solid: var(--haus-mango-700);
|
|
111
|
+
--haus-brand-warning-emphasis: var(--haus-mango-900);
|
|
112
|
+
|
|
113
|
+
/* ── Error ─────────────────────────────────────────────────────────── */
|
|
114
|
+
--haus-brand-error-subtle: var(--haus-cherry-100);
|
|
115
|
+
--haus-brand-error-border: var(--haus-cherry-200);
|
|
116
|
+
--haus-brand-error-default: var(--haus-cherry-500);
|
|
117
|
+
--haus-brand-error-on-subtle: var(--haus-cherry-700);
|
|
118
|
+
--haus-brand-error-on-default: var(--haus-damson-0);
|
|
119
|
+
--haus-brand-error-emphasis: var(--haus-cherry-900);
|
|
120
|
+
|
|
121
|
+
/* ── Backdrop ──────────────────────────────────────────────────────── */
|
|
122
|
+
--haus-brand-backdrop: oklch(from var(--haus-damson-950) l c h / var(--haus-opacity-60));
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
}
|
package/dist/guard.cjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/guard.ts
|
|
4
|
+
var READ_NO_FALLBACK = /var\(\s*(--[a-zA-Z0-9-]+)\s*\)/g;
|
|
5
|
+
var READ_WITH_FALLBACK = /var\(\s*(--[a-zA-Z0-9-]+)\s*,/g;
|
|
6
|
+
var DECLARATION = /(?:^|[;{])\s*(--[a-zA-Z0-9-]+)\s*:/gm;
|
|
7
|
+
function collect(sources, pattern) {
|
|
8
|
+
const found = /* @__PURE__ */ new Set();
|
|
9
|
+
for (const css of sources) {
|
|
10
|
+
pattern.lastIndex = 0;
|
|
11
|
+
for (const m of css.matchAll(pattern)) found.add(m[1]);
|
|
12
|
+
}
|
|
13
|
+
return found;
|
|
14
|
+
}
|
|
15
|
+
function findUndefinedTokens(input) {
|
|
16
|
+
const read = collect(input.reads, READ_NO_FALLBACK);
|
|
17
|
+
const defined = collect(input.defines, DECLARATION);
|
|
18
|
+
for (const name of input.alsoDefined ?? []) defined.add(name);
|
|
19
|
+
return {
|
|
20
|
+
missing: [...read].filter((name) => !defined.has(name)).sort(),
|
|
21
|
+
read: [...read].sort(),
|
|
22
|
+
defined: [...defined].sort()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function findFallbackTokens(input) {
|
|
26
|
+
const withFallback = collect(input.reads, READ_WITH_FALLBACK);
|
|
27
|
+
const defined = collect(input.defines, DECLARATION);
|
|
28
|
+
return [...withFallback].filter((name) => !defined.has(name)).sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
exports.findFallbackTokens = findFallbackTokens;
|
|
32
|
+
exports.findUndefinedTokens = findUndefinedTokens;
|
|
33
|
+
//# sourceMappingURL=guard.cjs.map
|
|
34
|
+
//# sourceMappingURL=guard.cjs.map
|
|
@@ -0,0 +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"]}
|
package/dist/guard.d.cts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The token guard: every custom property a stylesheet reads must be defined
|
|
3
|
+
* somewhere the consumer actually loads.
|
|
4
|
+
*
|
|
5
|
+
* CSS fails silently here, which is what makes this worth shipping. `var(--x)`
|
|
6
|
+
* for an undefined `--x` is invalid at computed-value time: the declaration is
|
|
7
|
+
* dropped and the property inherits. No console warning, no build error,
|
|
8
|
+
* nothing in review: a focus ring is simply absent, and a missing duration
|
|
9
|
+
* looks like a design choice.
|
|
10
|
+
*
|
|
11
|
+
* drift wrote this check for itself and it caught five roles before they
|
|
12
|
+
* reached a screen: `--color-ink-on-primary`, `--elevation-floating`,
|
|
13
|
+
* `--motion-duration-emphasis`, `--radius-marker` and `--haus-focus-ring-error`.
|
|
14
|
+
* The contract is defined in this package, so the check belongs here rather
|
|
15
|
+
* than being rewritten by every consumer (haus#19). Shipping it is also the
|
|
16
|
+
* point: a design system that can say *you have not loaded what my components
|
|
17
|
+
* read* is a different thing from one that hopes you did.
|
|
18
|
+
*
|
|
19
|
+
* **Pure, and it does no file reading.** It takes CSS as strings, so it runs in
|
|
20
|
+
* a Vitest suite, a Node script, a build step or a browser without this package
|
|
21
|
+
* growing a dependency on `node:fs`. The consumer knows which files it loads;
|
|
22
|
+
* this only knows what the contract is.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { readFileSync } from 'node:fs'
|
|
27
|
+
* import { findUndefinedTokens } from 'haus-tokens/guard'
|
|
28
|
+
*
|
|
29
|
+
* const read = (p: string) => readFileSync(p, 'utf8')
|
|
30
|
+
*
|
|
31
|
+
* it('reads no role this app does not load', () => {
|
|
32
|
+
* const { missing } = findUndefinedTokens({
|
|
33
|
+
* reads: [read('node_modules/haus-components/dist/styles.css')],
|
|
34
|
+
* defines: [
|
|
35
|
+
* read('node_modules/haus-tokens/dist/primitives.css'),
|
|
36
|
+
* read('node_modules/haus-tokens/dist/semantics.css'),
|
|
37
|
+
* read('node_modules/haus-tokens/dist/motion.css'),
|
|
38
|
+
* read('src/tokens/overrides.css'),
|
|
39
|
+
* ],
|
|
40
|
+
* })
|
|
41
|
+
* expect(missing).toEqual([])
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
interface TokenGuardInput {
|
|
46
|
+
/** The CSS doing the reading: the component stylesheet, your own modules. */
|
|
47
|
+
reads: string[];
|
|
48
|
+
/** The CSS doing the defining: the token layers you load, plus your own. */
|
|
49
|
+
defines: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Names defined outside CSS. A component that sets `style={{ '--x': v }}`
|
|
52
|
+
* defines the property on the element, and no stylesheet will show it.
|
|
53
|
+
*/
|
|
54
|
+
alsoDefined?: string[];
|
|
55
|
+
}
|
|
56
|
+
interface TokenGuardResult {
|
|
57
|
+
/** Read with no fallback and defined nowhere. Sorted, so a diff is stable. */
|
|
58
|
+
missing: string[];
|
|
59
|
+
/** Every property read without a fallback. */
|
|
60
|
+
read: string[];
|
|
61
|
+
/** Every property declared across `defines` and `alsoDefined`. */
|
|
62
|
+
defined: string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Which properties are read but never defined.
|
|
66
|
+
*
|
|
67
|
+
* An empty `missing` is the assertion. The other two fields are for a consumer
|
|
68
|
+
* that wants to report rather than fail: `read.length` is also worth asserting
|
|
69
|
+
* as a floor, because a wrong path makes every check pass by finding nothing.
|
|
70
|
+
*/
|
|
71
|
+
declare function findUndefinedTokens(input: TokenGuardInput): TokenGuardResult;
|
|
72
|
+
/**
|
|
73
|
+
* Which properties are only ever read with a fallback.
|
|
74
|
+
*
|
|
75
|
+
* Not a failure, and deliberately a separate function so it cannot be mistaken
|
|
76
|
+
* for one. A fallback that never loses is a hardcoded value wearing a token's
|
|
77
|
+
* clothes, and the list is worth reading occasionally rather than failing a
|
|
78
|
+
* build over.
|
|
79
|
+
*/
|
|
80
|
+
declare function findFallbackTokens(input: Pick<TokenGuardInput, 'reads' | 'defines'>): string[];
|
|
81
|
+
|
|
82
|
+
export { type TokenGuardInput, type TokenGuardResult, findFallbackTokens, findUndefinedTokens };
|
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The token guard: every custom property a stylesheet reads must be defined
|
|
3
|
+
* somewhere the consumer actually loads.
|
|
4
|
+
*
|
|
5
|
+
* CSS fails silently here, which is what makes this worth shipping. `var(--x)`
|
|
6
|
+
* for an undefined `--x` is invalid at computed-value time: the declaration is
|
|
7
|
+
* dropped and the property inherits. No console warning, no build error,
|
|
8
|
+
* nothing in review: a focus ring is simply absent, and a missing duration
|
|
9
|
+
* looks like a design choice.
|
|
10
|
+
*
|
|
11
|
+
* drift wrote this check for itself and it caught five roles before they
|
|
12
|
+
* reached a screen: `--color-ink-on-primary`, `--elevation-floating`,
|
|
13
|
+
* `--motion-duration-emphasis`, `--radius-marker` and `--haus-focus-ring-error`.
|
|
14
|
+
* The contract is defined in this package, so the check belongs here rather
|
|
15
|
+
* than being rewritten by every consumer (haus#19). Shipping it is also the
|
|
16
|
+
* point: a design system that can say *you have not loaded what my components
|
|
17
|
+
* read* is a different thing from one that hopes you did.
|
|
18
|
+
*
|
|
19
|
+
* **Pure, and it does no file reading.** It takes CSS as strings, so it runs in
|
|
20
|
+
* a Vitest suite, a Node script, a build step or a browser without this package
|
|
21
|
+
* growing a dependency on `node:fs`. The consumer knows which files it loads;
|
|
22
|
+
* this only knows what the contract is.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { readFileSync } from 'node:fs'
|
|
27
|
+
* import { findUndefinedTokens } from 'haus-tokens/guard'
|
|
28
|
+
*
|
|
29
|
+
* const read = (p: string) => readFileSync(p, 'utf8')
|
|
30
|
+
*
|
|
31
|
+
* it('reads no role this app does not load', () => {
|
|
32
|
+
* const { missing } = findUndefinedTokens({
|
|
33
|
+
* reads: [read('node_modules/haus-components/dist/styles.css')],
|
|
34
|
+
* defines: [
|
|
35
|
+
* read('node_modules/haus-tokens/dist/primitives.css'),
|
|
36
|
+
* read('node_modules/haus-tokens/dist/semantics.css'),
|
|
37
|
+
* read('node_modules/haus-tokens/dist/motion.css'),
|
|
38
|
+
* read('src/tokens/overrides.css'),
|
|
39
|
+
* ],
|
|
40
|
+
* })
|
|
41
|
+
* expect(missing).toEqual([])
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
interface TokenGuardInput {
|
|
46
|
+
/** The CSS doing the reading: the component stylesheet, your own modules. */
|
|
47
|
+
reads: string[];
|
|
48
|
+
/** The CSS doing the defining: the token layers you load, plus your own. */
|
|
49
|
+
defines: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Names defined outside CSS. A component that sets `style={{ '--x': v }}`
|
|
52
|
+
* defines the property on the element, and no stylesheet will show it.
|
|
53
|
+
*/
|
|
54
|
+
alsoDefined?: string[];
|
|
55
|
+
}
|
|
56
|
+
interface TokenGuardResult {
|
|
57
|
+
/** Read with no fallback and defined nowhere. Sorted, so a diff is stable. */
|
|
58
|
+
missing: string[];
|
|
59
|
+
/** Every property read without a fallback. */
|
|
60
|
+
read: string[];
|
|
61
|
+
/** Every property declared across `defines` and `alsoDefined`. */
|
|
62
|
+
defined: string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Which properties are read but never defined.
|
|
66
|
+
*
|
|
67
|
+
* An empty `missing` is the assertion. The other two fields are for a consumer
|
|
68
|
+
* that wants to report rather than fail: `read.length` is also worth asserting
|
|
69
|
+
* as a floor, because a wrong path makes every check pass by finding nothing.
|
|
70
|
+
*/
|
|
71
|
+
declare function findUndefinedTokens(input: TokenGuardInput): TokenGuardResult;
|
|
72
|
+
/**
|
|
73
|
+
* Which properties are only ever read with a fallback.
|
|
74
|
+
*
|
|
75
|
+
* Not a failure, and deliberately a separate function so it cannot be mistaken
|
|
76
|
+
* for one. A fallback that never loses is a hardcoded value wearing a token's
|
|
77
|
+
* clothes, and the list is worth reading occasionally rather than failing a
|
|
78
|
+
* build over.
|
|
79
|
+
*/
|
|
80
|
+
declare function findFallbackTokens(input: Pick<TokenGuardInput, 'reads' | 'defines'>): string[];
|
|
81
|
+
|
|
82
|
+
export { type TokenGuardInput, type TokenGuardResult, findFallbackTokens, findUndefinedTokens };
|
package/dist/guard.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/guard.ts
|
|
2
|
+
var READ_NO_FALLBACK = /var\(\s*(--[a-zA-Z0-9-]+)\s*\)/g;
|
|
3
|
+
var READ_WITH_FALLBACK = /var\(\s*(--[a-zA-Z0-9-]+)\s*,/g;
|
|
4
|
+
var DECLARATION = /(?:^|[;{])\s*(--[a-zA-Z0-9-]+)\s*:/gm;
|
|
5
|
+
function collect(sources, pattern) {
|
|
6
|
+
const found = /* @__PURE__ */ new Set();
|
|
7
|
+
for (const css of sources) {
|
|
8
|
+
pattern.lastIndex = 0;
|
|
9
|
+
for (const m of css.matchAll(pattern)) found.add(m[1]);
|
|
10
|
+
}
|
|
11
|
+
return found;
|
|
12
|
+
}
|
|
13
|
+
function findUndefinedTokens(input) {
|
|
14
|
+
const read = collect(input.reads, READ_NO_FALLBACK);
|
|
15
|
+
const defined = collect(input.defines, DECLARATION);
|
|
16
|
+
for (const name of input.alsoDefined ?? []) defined.add(name);
|
|
17
|
+
return {
|
|
18
|
+
missing: [...read].filter((name) => !defined.has(name)).sort(),
|
|
19
|
+
read: [...read].sort(),
|
|
20
|
+
defined: [...defined].sort()
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function findFallbackTokens(input) {
|
|
24
|
+
const withFallback = collect(input.reads, READ_WITH_FALLBACK);
|
|
25
|
+
const defined = collect(input.defines, DECLARATION);
|
|
26
|
+
return [...withFallback].filter((name) => !defined.has(name)).sort();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { findFallbackTokens, findUndefinedTokens };
|
|
30
|
+
//# sourceMappingURL=guard.js.map
|
|
31
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +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"]}
|