@evinvest/uikit 0.17.1 → 0.19.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 +204 -13
- package/dist/chunk-3IPB4QQJ.js +306 -0
- package/dist/chunk-3IPB4QQJ.js.map +1 -0
- package/dist/index.d.ts +102 -28
- package/dist/index.js +1569 -1387
- package/dist/index.js.map +1 -1
- package/dist/palette-cli.js +41 -0
- package/dist/palette-cli.js.map +1 -0
- package/dist/palette.d.ts +80 -0
- package/dist/palette.js +25 -0
- package/dist/palette.js.map +1 -0
- package/package.json +13 -4
- package/styles/ev.css +73 -0
- package/styles/theme.css +470 -0
- package/styles/tokens.css +133 -76
package/README.md
CHANGED
|
@@ -47,22 +47,35 @@ your Tailwind v4 entrypoint — this is the load-bearing part of the kit:
|
|
|
47
47
|
|
|
48
48
|
| group | names |
|
|
49
49
|
|---|---|
|
|
50
|
-
| scope | `:root` · `.
|
|
50
|
+
| scope | `:root` · `[data-brand="…"]` — a theme root; `.light` · `.dark` — a polarity beneath it |
|
|
51
51
|
| surfaces | `background` `card` `popover` `muted` `hover` |
|
|
52
52
|
| ink | `ink` `ink-mid` `ink-soft` — hierarchy, loudest first |
|
|
53
53
|
| lines | `border` `input` `ring` |
|
|
54
|
-
| roles | `brand` `primary` `secondary` `positive` `accent-trace` `accent-debug` `accent-info` `accent-warn` `accent-error`, each with `on-*` where it gets filled |
|
|
54
|
+
| roles | `brand` `primary` `secondary` `positive` `accent-trace` `accent-debug` `accent-info` `accent-warn` `accent-error`, each with `on-*` where it gets filled; `primary-ink` — the primary role as ink on a surface (`#128377` fill under a white `on-primary`, `#2a9d8f` ink) |
|
|
55
55
|
| scalars | `radius` `control-radius` `control-py` `display-scale` `band-py` `page-max` `page-px` `shadow-*` `font-*` |
|
|
56
56
|
| charts | `chart-1` … `chart-5` |
|
|
57
57
|
|
|
58
58
|
Custom properties inherit, so a scope class on a `<section>` re-themes its
|
|
59
59
|
subtree — `bg-card text-ink` is correct on both polarities with no prop. That is
|
|
60
|
-
what `<Section polarity="dark">` does.
|
|
60
|
+
what `<Section polarity="dark">` does. Without `polarity` a `Section` sets no
|
|
61
|
+
scope and inherits its parent's, so a dark-first brand stays dark; pass
|
|
62
|
+
`"light"` or `"dark"` only where a band flips.
|
|
61
63
|
|
|
62
64
|
A surface takes its ink from the scope, so it carries no `-foreground`. A
|
|
63
65
|
**filled role** does not — gold wants black and navy wants white regardless of
|
|
64
66
|
polarity — so `on-{role}` exists exactly there.
|
|
65
67
|
|
|
68
|
+
A fill and an ink are different jobs, and `primary` is the one role that
|
|
69
|
+
names them separately: `primary` is the fill (`bg-primary text-on-primary` on
|
|
70
|
+
a button, a checked switch or checkbox — `#128377`, white reads on it at
|
|
71
|
+
4.63:1 and it clears 3:1 against `background`, `secondary`, `card` and
|
|
72
|
+
`popover`) and `primary-ink` is the same hue as ink (`text-primary-ink` on a
|
|
73
|
+
link or eyebrow, a radio dot, a slider range, `border-primary-ink` on a checked
|
|
74
|
+
outline, the focus `ring` — `#2a9d8f`, 5.85:1 on `background`, 4.72:1 on
|
|
75
|
+
`card`). Every other role holds its own label and reads as ink with one value.
|
|
76
|
+
A consumer sheet must define `--primary-ink`; see
|
|
77
|
+
[`docs/spec/accents.md`](../../docs/spec/accents.md).
|
|
78
|
+
|
|
66
79
|
Accents are decorative and **ordered by significance**, quiet to loud. Pick a
|
|
67
80
|
rung by how loud the thing should be, never by what it means; see
|
|
68
81
|
[`docs/spec/accents.md`](../../docs/spec/accents.md). Every consumer defines all
|
|
@@ -74,14 +87,185 @@ state, never its appearance (`Red`) and never its selection (`Default`); see
|
|
|
74
87
|
[`docs/spec/variants.md`](../../docs/spec/variants.md).
|
|
75
88
|
|
|
76
89
|
`styles/tokens.css` is generated by `nix run .#gen` from the repo-root
|
|
77
|
-
`tokens.css` shared with the Rust feature — flattened, so it is self-contained
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
`tokens.css` shared with the Rust feature — flattened, so it is self-contained.
|
|
91
|
+
It has two halves, each also shipped as a flat sheet of its own —
|
|
92
|
+
`styles/theme.css` (the contract) and `styles/ev.css` (EV's palette):
|
|
93
|
+
|
|
94
|
+
- **the contract** — the `@theme inline` mapping, the geometry, and the tokens
|
|
95
|
+
*derived* from a palette (`hover`, `ink-mid`, `ink-soft`, `border`, `input`
|
|
96
|
+
from `ink`; `ring` from `primary-ink`). The derived ones are redeclared on
|
|
97
|
+
`:root`, `[data-brand]`, `.light` and `.dark` at zero specificity, so a brand
|
|
98
|
+
scope gets its own borders, hover and focus ring rather than inheriting the
|
|
99
|
+
root's, and a palette may still pin any of them outright. The geometry
|
|
100
|
+
(`radius`, `page-px`, `band-py`, …) and its responsive scale-ups restart on
|
|
101
|
+
every `[data-brand]` the same way.
|
|
102
|
+
- **EV's palette** — a single dark palette on `:where(:root, [data-brand="ev"])`.
|
|
103
|
+
It binds neither polarity class, so a `.dark` section inside another brand
|
|
104
|
+
never picks EV's values up, and `<Section polarity="light">` under EV is a
|
|
105
|
+
no-op (EV is single-polarity).
|
|
106
|
+
|
|
107
|
+
### Brand palettes
|
|
108
|
+
|
|
109
|
+
A second brand is a palette against the same names, scoped to
|
|
110
|
+
`[data-brand="<slug>"]`; light is its default and `.light` / `.dark` beneath it
|
|
111
|
+
pick a polarity, so a `<Section polarity="dark">` inside the brand takes the
|
|
112
|
+
brand's dark values. The package ships a generator for it, reading the
|
|
113
|
+
`brand.toml` shape (`[colors.light]` and `[colors.dark]`, keyed by token name;
|
|
114
|
+
optional top-level `mark` and `mark-aspect` become `--brand-mark` and
|
|
115
|
+
`--brand-aspect`; the family names in `[fonts]` — `sans` (or aquafix's `text`),
|
|
116
|
+
`display`, `mono` — become the [font parameters](#fonts); other keys and tables
|
|
117
|
+
are ignored). Each polarity block also declares `--scheme: light | dark`, which
|
|
118
|
+
the platform-drawn parts of the kit read (a `NativeSelect`'s popup):
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
npx evinvest-palette --brand aquafix --out app/brand.css assets/brand.toml
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
It fails — naming every hole — unless **both** polarities declare every name the
|
|
125
|
+
contract requires: Tailwind answers an undefined token with no rule at all, so a
|
|
126
|
+
hole would be silent on the page. Values must be `#hex` or `var(--token)`. The
|
|
127
|
+
list of required names is read from the installed kit's own `styles/theme.css`,
|
|
128
|
+
never restated. The same functions are importable for a build script or a Vite
|
|
129
|
+
plugin from `@evinvest/uikit/palette` (Node-only; see `example/vite.config.ts`).
|
|
130
|
+
The generator checks completeness, not contrast: measure a new palette against
|
|
131
|
+
the floors in `test/tokens.test.ts`, which runs them over every palette the kit
|
|
132
|
+
knows.
|
|
133
|
+
|
|
134
|
+
```css
|
|
135
|
+
@import "tailwindcss";
|
|
136
|
+
@import "@evinvest/uikit/styles/tokens.css";
|
|
137
|
+
@import "./brand.css"; /* order does not matter: EV's palette has zero specificity */
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<html data-brand="aquafix">
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
An app that never shows EV's palette imports the contract alone, so a name its
|
|
145
|
+
palette misses stays a visible hole instead of falling back to EV's value:
|
|
146
|
+
|
|
147
|
+
```css
|
|
148
|
+
@import "tailwindcss";
|
|
149
|
+
@import "@evinvest/uikit/styles/theme.css";
|
|
150
|
+
@import "./brand.css";
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Overlays (`Dialog`, `Select`, `Popover`, `Tooltip`, menus, …) portal to
|
|
154
|
+
`document.body` and would leave the brand scope there. Wrap the scope's content
|
|
155
|
+
in `PortalProvider` with an element **inside** the scope — and inside its
|
|
156
|
+
polarity, or a dialog opened from a dark band comes up light:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
const [overlays, setOverlays] = useState<HTMLDivElement | null>(null);
|
|
160
|
+
|
|
161
|
+
<section data-brand="aquafix" className="dark">
|
|
162
|
+
<PortalProvider container={overlays}>{children}</PortalProvider>
|
|
163
|
+
<div ref={setOverlays} />
|
|
164
|
+
</section>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Put the container as a direct child of the `[data-brand]` (or polarity)
|
|
168
|
+
element, and never under an ancestor with `overflow: hidden`, a `transform`,
|
|
169
|
+
`filter` or `perspective`, or anything else that makes a containing block or
|
|
170
|
+
an isolated stacking context (`contain`, `isolation: isolate`, a `z-index`ed
|
|
171
|
+
positioned box): overlays are positioned `fixed`/`absolute` against the
|
|
172
|
+
viewport and stacked by `z-index`, so any of those clips them, offsets them, or
|
|
173
|
+
buries them under the page.
|
|
174
|
+
|
|
175
|
+
A brand on `<html>` needs no provider: `document.body` is already inside it.
|
|
176
|
+
Brand scopes do not nest — CSS has no "nearest ancestor", so a polarity class
|
|
177
|
+
under a brand under another brand may match either.
|
|
178
|
+
|
|
179
|
+
### Fonts
|
|
180
|
+
|
|
181
|
+
The families are parameters, one per role; unset, each is EV's:
|
|
182
|
+
|
|
183
|
+
| parameter | utilities | default (EV) |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `--brand-font-sans` | `font-sans`, the page's base family | `--font-inter`, else `"Inter"` |
|
|
186
|
+
| `--brand-font-display` | `font-display`, `font-serif` | `--font-playfair`, else `"Playfair Display"` |
|
|
187
|
+
| `--brand-font-mono` | `font-mono` | `--font-inter`, else `"Inter"` |
|
|
188
|
+
|
|
189
|
+
Declare them on the brand scope (the palette generator writes them from
|
|
190
|
+
`[fonts]`) and load the files yourself — `next/font`, or your own `@font-face`;
|
|
191
|
+
the kit ships no font files:
|
|
192
|
+
|
|
193
|
+
```css
|
|
194
|
+
[data-brand="aquafix"] {
|
|
195
|
+
--brand-font-display: "Archivo";
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The utilities resolve the chain on the element, so a scope re-fonts everything
|
|
200
|
+
under it that carries one. The page's base family resolves once, on `<html>`: a
|
|
201
|
+
brand scoped lower than `<html>` puts `font-sans` on its own element so plain
|
|
202
|
+
text picks the brand's family up.
|
|
203
|
+
|
|
204
|
+
## Forms that submit before hydration
|
|
205
|
+
|
|
206
|
+
A landing page's form has to work with scripting off and keep its labels wired
|
|
207
|
+
through hydration. Three parts of the kit make that hold:
|
|
208
|
+
|
|
209
|
+
- **`NativeSelect`** is a real `<select name>` wearing the kit's control — the
|
|
210
|
+
kit's arrow instead of the OS one, token colours, the popup following the
|
|
211
|
+
palette's `--scheme`. It posts with the form without any script, and the
|
|
212
|
+
popup is the platform's (keyboard, type-ahead, the mobile picker).
|
|
213
|
+
`NativeSelectOption` / `NativeSelectGroup` are `<option>` / `<optgroup>`.
|
|
214
|
+
`placeholder` adds an empty, unpickable first option, selected until a value
|
|
215
|
+
is chosen and styled `text-ink-soft`; with `required`, the browser refuses to
|
|
216
|
+
submit it. `className` styles the `<select>`, `wrapperClassName` the box that
|
|
217
|
+
holds it and the arrow. That box is `w-full`, like `Input`, so the two line
|
|
218
|
+
up in a form column; narrow it with `wrapperClassName`, not `className`.
|
|
219
|
+
|
|
220
|
+
It is a component of its own rather than a mode of `Select`: none of
|
|
221
|
+
`SelectTrigger` / `SelectValue` / `SelectContent` has a native counterpart.
|
|
222
|
+
`Select` stays the scripted combobox for what a platform popup cannot draw
|
|
223
|
+
(rich rows, a check mark, brand-styled menus everywhere).
|
|
224
|
+
- **`Field` mints the id** with `React.useId` — positional, so the server and the
|
|
225
|
+
hydrating client agree — and hands it to its `FieldLabel` as `for` and to the
|
|
226
|
+
kit's `Input`, `Textarea`, `NativeSelect`, `SelectTrigger`, `Checkbox` and
|
|
227
|
+
`Switch` as `id`, each only when the caller passed none. No `FormControl`
|
|
228
|
+
wrapper is needed. Want your own id — pass `controlId` to the `Field`, and
|
|
229
|
+
both ends take it. One control per `Field`: when it holds more, give the
|
|
230
|
+
others an `id` of their own (a development warning names the collision).
|
|
231
|
+
A `FieldLabel` that wraps its control (`<FieldLabel><input type="checkbox" />
|
|
232
|
+
Accept</FieldLabel>`, or a nested `Field` — the choice card) gets no `for`:
|
|
233
|
+
the wrapping already labels it. A control hidden inside a component of yours
|
|
234
|
+
is invisible to that check; pass `htmlFor={null}` there to drop the `for`.
|
|
235
|
+
(`FormItem` / `FormControl` keep their own `useId` wiring for forms that
|
|
236
|
+
want `aria-describedby` too.)
|
|
237
|
+
- **`SelectValue` shows the label**, not the stored value: the matching
|
|
238
|
+
`SelectItem`'s `textValue`, else the text of its children — text, so an id in
|
|
239
|
+
the item's markup is not rendered a second time in the trigger. It is read off
|
|
240
|
+
the element tree, so it is right on the server and before the popover first
|
|
241
|
+
opens. An item rendered by a component of yours is learnt when it mounts (on
|
|
242
|
+
the first open), and the trigger updates then; pass `SelectValue` children to
|
|
243
|
+
show something else outright.
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
<form action="/lead" method="post">
|
|
247
|
+
<Field>
|
|
248
|
+
<FieldLabel>Service</FieldLabel>
|
|
249
|
+
<NativeSelect name="service" placeholder="Pick one" required>
|
|
250
|
+
<NativeSelectOption value="leak">Leak</NativeSelectOption>
|
|
251
|
+
<NativeSelectOption value="boiler">Boiler</NativeSelectOption>
|
|
252
|
+
</NativeSelect>
|
|
253
|
+
</Field>
|
|
254
|
+
<Field>
|
|
255
|
+
<FieldLabel>Phone</FieldLabel>
|
|
256
|
+
<Input name="phone" type="tel" />
|
|
257
|
+
</Field>
|
|
258
|
+
<Button type="submit">Request a call</Button>
|
|
259
|
+
</form>
|
|
260
|
+
```
|
|
80
261
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
262
|
+
**Focus.** A control that paints a fill — `Button` `primary` / `secondary` /
|
|
263
|
+
`destructive` or any accent fill, a filled `Badge`, `Switch`, `Checkbox` —
|
|
264
|
+
shows focus as the solid `ring` standing 2px off it, so both edges of the
|
|
265
|
+
indicator are against the surface: the ring reads on every surface at ≥ 3:1
|
|
266
|
+
and so does the fill, on every palette and polarity `test/tokens.test.ts`
|
|
267
|
+
measures. Controls that sit on the surface (`outline`, `ghost`, inputs) keep
|
|
268
|
+
the `ring` border plus its halo.
|
|
85
269
|
|
|
86
270
|
## Usage
|
|
87
271
|
|
|
@@ -129,11 +313,12 @@ element across the two ports.
|
|
|
129
313
|
| overlay placement | inline `position:fixed` + `data-side` | `Portal` + `useFloating` |
|
|
130
314
|
| dismiss / focus trap | full-screen backdrop / native order | `useDismissableLayer` / `useFocusScope` |
|
|
131
315
|
|
|
132
|
-
### Component inventory (all
|
|
316
|
+
### Component inventory (all 63 bricks)
|
|
133
317
|
|
|
134
|
-
- **Tier A — static (
|
|
318
|
+
- **Tier A — static (23):** badge, button, button-group, card, input, textarea,
|
|
135
319
|
label, field, separator, skeleton, spinner, kbd, table, container, alert,
|
|
136
|
-
breadcrumb, empty, item, input-group, avatar, progress, pagination
|
|
320
|
+
breadcrumb, empty, item, input-group, avatar, progress, pagination,
|
|
321
|
+
native-select (TS-only).
|
|
137
322
|
- **Tier B — interactive (13):** accordion, collapsible, tabs, toggle,
|
|
138
323
|
toggle-group, switch, checkbox, radio-group, slider, sidebar, scroll-area,
|
|
139
324
|
carousel, input-otp.
|
|
@@ -236,6 +421,12 @@ measuring needs host-only `web-sys`). Known gaps:
|
|
|
236
421
|
`max-h-*` on `DrawerContent` and let the body scroll by itself.
|
|
237
422
|
- **sidebar:** the mobile-sheet integration, cookie persistence, and keyboard
|
|
238
423
|
shortcut are omitted.
|
|
424
|
+
- **footer:** brings no content of its own — every string, the mark and the
|
|
425
|
+
lock-up are the caller's, and each column renders only when it has content.
|
|
426
|
+
The lock-up is composed from `mark` + `brand` (display family) + `tagline`,
|
|
427
|
+
or passed whole as `lockup`. Brand-coloured text reads `primary-ink`.
|
|
428
|
+
- **TS-only for now:** `NativeSelect`, the `Field` id hand-off and
|
|
429
|
+
`SelectValue` labels.
|
|
239
430
|
- **brand chrome (header / footer / status pages):** TS routes links through an
|
|
240
431
|
optional `linkComponent` (e.g. `next/link`) for soft navigation; Rust renders
|
|
241
432
|
plain `<a>` (a full document load). The Dioxus header drives its scroll state,
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// src/palette/contract.ts
|
|
2
|
+
function references(value) {
|
|
3
|
+
return [...value.matchAll(/var\(--([A-Za-z0-9-]+)/g)].flatMap((m) => m[1] ? [m[1]] : []);
|
|
4
|
+
}
|
|
5
|
+
var DERIVED_SCOPE = ":where(:root, .dark, .light, [data-brand])";
|
|
6
|
+
function readRules(css) {
|
|
7
|
+
const rules = [];
|
|
8
|
+
const src = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
9
|
+
const stack = [];
|
|
10
|
+
let start = 0;
|
|
11
|
+
for (let i = 0; i < src.length; i++) {
|
|
12
|
+
const c = src[i];
|
|
13
|
+
if (c === "{") {
|
|
14
|
+
stack.push(src.slice(start, i).trim());
|
|
15
|
+
start = i + 1;
|
|
16
|
+
} else if (c === "}") {
|
|
17
|
+
const selector = stack.pop();
|
|
18
|
+
if (selector === void 0) throw new Error("unbalanced `}` in token sheet");
|
|
19
|
+
const body = src.slice(start, i);
|
|
20
|
+
if (!body.includes("{")) rules.push({ selector: normalise(selector), declarations: declarations(body) });
|
|
21
|
+
start = i + 1;
|
|
22
|
+
} else if (c === ";" && stack.length === 0) {
|
|
23
|
+
start = i + 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (stack.length > 0) throw new Error("unbalanced `{` in token sheet");
|
|
27
|
+
return rules;
|
|
28
|
+
}
|
|
29
|
+
function declarations(body) {
|
|
30
|
+
const out = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const decl of body.split(";")) {
|
|
32
|
+
const match = /^\s*--([A-Za-z0-9-]+)\s*:\s*([\s\S]+?)\s*$/.exec(decl);
|
|
33
|
+
if (match?.[1] && match[2]) out.set(match[1], match[2]);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function normalise(selector) {
|
|
38
|
+
return selector.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim();
|
|
39
|
+
}
|
|
40
|
+
function readContract(css) {
|
|
41
|
+
const rules = readRules(css);
|
|
42
|
+
const theme = rules.find((r) => r.selector === "@theme inline");
|
|
43
|
+
if (!theme) throw new Error("token sheet has no `@theme inline` block \u2014 not the kit's contract");
|
|
44
|
+
const painted = [];
|
|
45
|
+
for (const [name, value] of theme.declarations) {
|
|
46
|
+
const match = /^color-(.+)$/.exec(name);
|
|
47
|
+
if (match?.[1] && value === `var(--${match[1]})`) painted.push(match[1]);
|
|
48
|
+
}
|
|
49
|
+
const derivedRule = rules.find((r) => r.selector === DERIVED_SCOPE);
|
|
50
|
+
if (!derivedRule) throw new Error(`token sheet has no \`${DERIVED_SCOPE}\` rule \u2014 not the kit's contract`);
|
|
51
|
+
const derived = [...derivedRule.declarations.keys()];
|
|
52
|
+
const stray = derived.filter((d) => !painted.includes(d));
|
|
53
|
+
if (stray.length > 0) throw new Error(`derived tokens with no @theme mapping: ${stray.join(", ")}`);
|
|
54
|
+
const derivedFrom = Object.fromEntries([...derivedRule.declarations].map(([name, value]) => [name, references(value)]));
|
|
55
|
+
return { required: painted.filter((p) => !derived.includes(p)), derived, derivedFrom };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/palette/toml.ts
|
|
59
|
+
var BARE_KEY = /^[A-Za-z0-9_-]+$/;
|
|
60
|
+
function parseToml(text) {
|
|
61
|
+
const root = {};
|
|
62
|
+
let table2 = root;
|
|
63
|
+
const headers = /* @__PURE__ */ new Set();
|
|
64
|
+
text.split(/\r?\n/).forEach((raw, index) => {
|
|
65
|
+
const line = stripComment(raw).trim();
|
|
66
|
+
if (line === "") return;
|
|
67
|
+
const fail = (why) => {
|
|
68
|
+
throw new Error(`TOML line ${index + 1}: ${why}: ${raw.trim()}`);
|
|
69
|
+
};
|
|
70
|
+
if (line.startsWith("[")) {
|
|
71
|
+
if (!line.endsWith("]") || line.startsWith("[[")) fail("only plain [table] headers are supported");
|
|
72
|
+
const path = line.slice(1, -1).trim().split(".").map((part) => part.trim());
|
|
73
|
+
if (headers.has(path.join("."))) fail("duplicate table header");
|
|
74
|
+
headers.add(path.join("."));
|
|
75
|
+
table2 = descend(root, path, fail);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const eq = line.indexOf("=");
|
|
79
|
+
if (eq < 0) fail("expected `key = value`");
|
|
80
|
+
const key = line.slice(0, eq).trim();
|
|
81
|
+
if (!BARE_KEY.test(key)) fail("only bare keys are supported");
|
|
82
|
+
if (key in table2) fail(`duplicate key \`${key}\``);
|
|
83
|
+
table2[key] = parseValue(line.slice(eq + 1).trim(), fail);
|
|
84
|
+
});
|
|
85
|
+
return root;
|
|
86
|
+
}
|
|
87
|
+
function descend(root, path, fail) {
|
|
88
|
+
let table2 = root;
|
|
89
|
+
for (const part of path) {
|
|
90
|
+
const key = part.trim();
|
|
91
|
+
if (!BARE_KEY.test(key)) fail("only bare keys are supported in a header");
|
|
92
|
+
const next = table2[key] ?? {};
|
|
93
|
+
if (typeof next !== "object" || Array.isArray(next)) fail(`\`${key}\` is already a value`);
|
|
94
|
+
table2[key] = next;
|
|
95
|
+
table2 = next;
|
|
96
|
+
}
|
|
97
|
+
return table2;
|
|
98
|
+
}
|
|
99
|
+
function stripComment(line) {
|
|
100
|
+
let quoted = false;
|
|
101
|
+
for (let i = 0; i < line.length; i++) {
|
|
102
|
+
const c = line[i];
|
|
103
|
+
if (c === "\\" && quoted) i++;
|
|
104
|
+
else if (c === '"') quoted = !quoted;
|
|
105
|
+
else if (c === "#" && !quoted) return line.slice(0, i);
|
|
106
|
+
}
|
|
107
|
+
return line;
|
|
108
|
+
}
|
|
109
|
+
function parseValue(src, fail) {
|
|
110
|
+
if (src.startsWith('"')) {
|
|
111
|
+
if (src.length < 2 || !src.endsWith('"')) fail("unterminated string");
|
|
112
|
+
const body = src.slice(1, -1);
|
|
113
|
+
if (/(^|[^\\])(\\\\)*"/.test(body)) fail("one string per value");
|
|
114
|
+
if (/\\(?!["\\nt])/.test(body.replace(/\\\\/g, ""))) fail("unsupported escape");
|
|
115
|
+
return body.replace(/\\(["\\nt])/g, (_, c) => ({ n: "\n", t: " " })[c] ?? c);
|
|
116
|
+
}
|
|
117
|
+
if (src.startsWith("[")) {
|
|
118
|
+
if (!src.endsWith("]")) fail("arrays must close on the same line");
|
|
119
|
+
const inner = src.slice(1, -1).trim();
|
|
120
|
+
if (inner === "") return [];
|
|
121
|
+
return splitTopLevel(inner).map((item) => parseValue(item.trim(), fail));
|
|
122
|
+
}
|
|
123
|
+
if (src === "true" || src === "false") return src === "true";
|
|
124
|
+
if (/^[+-]?\d+(\.\d+)?$/.test(src)) return Number(src);
|
|
125
|
+
return fail("unsupported value");
|
|
126
|
+
}
|
|
127
|
+
function splitTopLevel(inner) {
|
|
128
|
+
const items = [];
|
|
129
|
+
let quoted = false;
|
|
130
|
+
let start = 0;
|
|
131
|
+
for (let i = 0; i < inner.length; i++) {
|
|
132
|
+
const c = inner[i];
|
|
133
|
+
if (c === "\\" && quoted) i++;
|
|
134
|
+
else if (c === '"') quoted = !quoted;
|
|
135
|
+
else if (c === "," && !quoted) {
|
|
136
|
+
items.push(inner.slice(start, i));
|
|
137
|
+
start = i + 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const last = inner.slice(start);
|
|
141
|
+
if (last.trim() !== "") items.push(last);
|
|
142
|
+
return items;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/palette/brand.ts
|
|
146
|
+
var POLARITIES = ["light", "dark"];
|
|
147
|
+
var FONT_ROLES = ["sans", "display", "mono"];
|
|
148
|
+
var FONT_KEYS = { sans: "sans", text: "sans", display: "display", mono: "mono" };
|
|
149
|
+
var PaletteError = class extends Error {
|
|
150
|
+
constructor(problems) {
|
|
151
|
+
super(`invalid brand palette:
|
|
152
|
+
- ${problems.join("\n - ")}`);
|
|
153
|
+
this.problems = problems;
|
|
154
|
+
this.name = "PaletteError";
|
|
155
|
+
}
|
|
156
|
+
problems;
|
|
157
|
+
};
|
|
158
|
+
var COLOUR = /^(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{8}|var\(--[a-z0-9-]+\))$/;
|
|
159
|
+
var SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
160
|
+
var ASPECT = /^\d+(\.\d+)?\s*\/\s*\d+(\.\d+)?$/;
|
|
161
|
+
var MARK_URL = /^[^"'`\\\s()<>]+$/;
|
|
162
|
+
var FAMILY = /^[A-Za-z0-9][A-Za-z0-9 _-]*$/;
|
|
163
|
+
function brandFromToml(text) {
|
|
164
|
+
const doc = parseToml(text);
|
|
165
|
+
const problems = [];
|
|
166
|
+
const colors = table(doc["colors"]);
|
|
167
|
+
if (!colors) problems.push("no [colors] table");
|
|
168
|
+
const scope = (name) => {
|
|
169
|
+
const t = colors ? table(colors[name]) : void 0;
|
|
170
|
+
if (colors && !t) problems.push(`no [colors.${name}] table`);
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const [key, value] of Object.entries(t ?? {})) {
|
|
173
|
+
if (typeof value === "string") out[key] = value;
|
|
174
|
+
else problems.push(`colors.${name}.${key} is not a string`);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
};
|
|
178
|
+
for (const key of Object.keys(colors ?? {})) {
|
|
179
|
+
if (!POLARITIES.includes(key)) problems.push(`[colors.${key}] is not a polarity (light, dark)`);
|
|
180
|
+
}
|
|
181
|
+
const config = { colors: { light: scope("light"), dark: scope("dark") } };
|
|
182
|
+
const mark = optionalString(doc["mark"], "mark", problems);
|
|
183
|
+
const markAspect = optionalString(doc["mark-aspect"], "mark-aspect", problems);
|
|
184
|
+
if (mark !== void 0) config.mark = mark;
|
|
185
|
+
if (markAspect !== void 0) config.markAspect = markAspect;
|
|
186
|
+
const fonts = table(doc["fonts"]);
|
|
187
|
+
for (const [key, role] of Object.entries(FONT_KEYS)) {
|
|
188
|
+
const family = optionalString(fonts?.[key], `fonts.${key}`, problems);
|
|
189
|
+
if (family !== void 0) config.fonts = { ...config.fonts, [role]: family };
|
|
190
|
+
}
|
|
191
|
+
if (problems.length > 0) throw new PaletteError(problems);
|
|
192
|
+
return config;
|
|
193
|
+
}
|
|
194
|
+
function table(value) {
|
|
195
|
+
return typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
196
|
+
}
|
|
197
|
+
function optionalString(value, key, problems) {
|
|
198
|
+
if (value === void 0 || typeof value === "string") return value;
|
|
199
|
+
problems.push(`${key} is not a string`);
|
|
200
|
+
return void 0;
|
|
201
|
+
}
|
|
202
|
+
function validateBrand(config, contract) {
|
|
203
|
+
const problems = [];
|
|
204
|
+
const known = /* @__PURE__ */ new Set([...contract.required, ...contract.derived]);
|
|
205
|
+
for (const polarity of POLARITIES) {
|
|
206
|
+
const values = config.colors[polarity];
|
|
207
|
+
const missing = contract.required.filter((t) => !(t in values));
|
|
208
|
+
if (missing.length > 0) problems.push(`${polarity} is missing ${missing.join(", ")}`);
|
|
209
|
+
for (const [key, value] of Object.entries(values)) {
|
|
210
|
+
if (!known.has(key)) problems.push(`${polarity}.${key} is not a token of the contract`);
|
|
211
|
+
else if (!COLOUR.test(value)) problems.push(`${polarity}.${key} = ${JSON.stringify(value)} is not #hex or var(--token)`);
|
|
212
|
+
else {
|
|
213
|
+
for (const ref of references(value)) {
|
|
214
|
+
if (!known.has(ref)) problems.push(`${polarity}.${key} refers to --${ref}, which is not a token of the contract`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const cycle = findCycle(values, contract, known);
|
|
219
|
+
if (cycle) problems.push(`${polarity} has a reference cycle: ${cycle.map((t) => `--${t}`).join(" \u2192 ")}`);
|
|
220
|
+
}
|
|
221
|
+
if (config.mark !== void 0 && !MARK_URL.test(config.mark)) problems.push(`mark ${JSON.stringify(config.mark)} is not a plain URL`);
|
|
222
|
+
if (config.markAspect !== void 0 && !ASPECT.test(config.markAspect)) {
|
|
223
|
+
problems.push(`mark-aspect ${JSON.stringify(config.markAspect)} is not "width / height"`);
|
|
224
|
+
}
|
|
225
|
+
for (const [role, family] of Object.entries(config.fonts ?? {})) {
|
|
226
|
+
if (!FAMILY.test(family)) problems.push(`fonts.${role} ${JSON.stringify(family)} is not a plain family name`);
|
|
227
|
+
}
|
|
228
|
+
return problems;
|
|
229
|
+
}
|
|
230
|
+
function findCycle(values, contract, known) {
|
|
231
|
+
const edges = (name) => {
|
|
232
|
+
const value = values[name];
|
|
233
|
+
if (value !== void 0) return references(value).filter((r) => known.has(r));
|
|
234
|
+
return contract.derivedFrom[name] ?? [];
|
|
235
|
+
};
|
|
236
|
+
const done = /* @__PURE__ */ new Set();
|
|
237
|
+
const path = [];
|
|
238
|
+
const visit = (name) => {
|
|
239
|
+
const at = path.indexOf(name);
|
|
240
|
+
if (at >= 0) return [...path.slice(at), name];
|
|
241
|
+
if (done.has(name)) return void 0;
|
|
242
|
+
path.push(name);
|
|
243
|
+
for (const next of edges(name)) {
|
|
244
|
+
const cycle = visit(next);
|
|
245
|
+
if (cycle) return cycle;
|
|
246
|
+
}
|
|
247
|
+
path.pop();
|
|
248
|
+
done.add(name);
|
|
249
|
+
return void 0;
|
|
250
|
+
};
|
|
251
|
+
for (const name of known) {
|
|
252
|
+
const cycle = visit(name);
|
|
253
|
+
if (cycle) return cycle;
|
|
254
|
+
}
|
|
255
|
+
return void 0;
|
|
256
|
+
}
|
|
257
|
+
function renderPalette(brand, config, contract, source = "a brand file") {
|
|
258
|
+
const problems = SLUG.test(brand) ? [] : [`brand ${JSON.stringify(brand)} is not a lowercase slug`];
|
|
259
|
+
problems.push(...validateBrand(config, contract));
|
|
260
|
+
if (problems.length > 0) throw new PaletteError(problems);
|
|
261
|
+
const at = `[data-brand="${brand}"]`;
|
|
262
|
+
const order = [...contract.required, ...contract.derived];
|
|
263
|
+
const block = (selectors, polarity) => {
|
|
264
|
+
const values = config.colors[polarity];
|
|
265
|
+
const body = order.filter((t) => t in values).map((t) => ` --${t}: ${values[t]};
|
|
266
|
+
`);
|
|
267
|
+
return `${selectors.join(",\n")} {
|
|
268
|
+
--scheme: ${polarity};
|
|
269
|
+
${body.join("")}}
|
|
270
|
+
`;
|
|
271
|
+
};
|
|
272
|
+
const extras = [];
|
|
273
|
+
if (config.mark !== void 0) extras.push(` --brand-mark: url("${config.mark}");
|
|
274
|
+
`);
|
|
275
|
+
if (config.markAspect !== void 0) extras.push(` --brand-aspect: ${config.markAspect};
|
|
276
|
+
`);
|
|
277
|
+
for (const role of FONT_ROLES) {
|
|
278
|
+
const family = config.fonts?.[role];
|
|
279
|
+
if (family !== void 0) extras.push(` --brand-font-${role}: "${family}";
|
|
280
|
+
`);
|
|
281
|
+
}
|
|
282
|
+
return [
|
|
283
|
+
// `source` is a file name, free text as far as CSS is concerned: it must not close the comment.
|
|
284
|
+
`/* GENERATED by evinvest-palette from ${source.replaceAll("*/", "*\\/")} \u2014 do not edit. */
|
|
285
|
+
`,
|
|
286
|
+
block([at, `${at}.light`, `${at} .light`], "light"),
|
|
287
|
+
block([`${at}.dark`, `${at} .dark`], "dark"),
|
|
288
|
+
...extras.length > 0 ? [`${at} {
|
|
289
|
+
${extras.join("")}}
|
|
290
|
+
`] : []
|
|
291
|
+
].join("\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export {
|
|
295
|
+
DERIVED_SCOPE,
|
|
296
|
+
readRules,
|
|
297
|
+
readContract,
|
|
298
|
+
parseToml,
|
|
299
|
+
POLARITIES,
|
|
300
|
+
FONT_ROLES,
|
|
301
|
+
PaletteError,
|
|
302
|
+
brandFromToml,
|
|
303
|
+
validateBrand,
|
|
304
|
+
renderPalette
|
|
305
|
+
};
|
|
306
|
+
//# sourceMappingURL=chunk-3IPB4QQJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/palette/contract.ts","../src/palette/toml.ts","../src/palette/brand.ts"],"sourcesContent":["/**\n * The colour contract, read out of the kit's own token sheet rather than\n * restated here: `@theme inline` maps every name the class tables can paint\n * (`--color-X: var(--X)`), and the derived rule lists the ones the contract\n * computes from the rest. A palette owes the difference.\n */\nexport interface Contract {\n /** Names every palette scope must declare. */\n required: readonly string[];\n /** Names the contract derives from the palette; a palette may override them. */\n derived: readonly string[];\n /** The tokens each derived default reads, so a palette's references can be checked for cycles through them. */\n derivedFrom: Readonly<Record<string, readonly string[]>>;\n}\n\n/** The `--name`s a value reads through `var()`. */\nexport function references(value: string): string[] {\n return [...value.matchAll(/var\\(--([A-Za-z0-9-]+)/g)].flatMap((m) => (m[1] ? [m[1]] : []));\n}\n\n/** The rule the derived tokens live on — see `theme.css`, the contract half of `tokens.css`. */\nexport const DERIVED_SCOPE = \":where(:root, .dark, .light, [data-brand])\";\n\nexport interface CssRule {\n selector: string;\n /** Custom properties declared directly in the rule, in source order. */\n declarations: Map<string, string>;\n}\n\n/**\n * Top-level and `@media`-nested rules of a flat sheet, comments stripped. Only\n * as much CSS as a token sheet is made of: no strings containing braces.\n */\nexport function readRules(css: string): CssRule[] {\n const rules: CssRule[] = [];\n const src = css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n const stack: string[] = [];\n let start = 0;\n for (let i = 0; i < src.length; i++) {\n const c = src[i];\n if (c === \"{\") {\n stack.push(src.slice(start, i).trim());\n start = i + 1;\n } else if (c === \"}\") {\n const selector = stack.pop();\n if (selector === undefined) throw new Error(\"unbalanced `}` in token sheet\");\n const body = src.slice(start, i);\n if (!body.includes(\"{\")) rules.push({ selector: normalise(selector), declarations: declarations(body) });\n start = i + 1;\n } else if (c === \";\" && stack.length === 0) {\n start = i + 1;\n }\n }\n if (stack.length > 0) throw new Error(\"unbalanced `{` in token sheet\");\n return rules;\n}\n\nfunction declarations(body: string): Map<string, string> {\n const out = new Map<string, string>();\n for (const decl of body.split(\";\")) {\n const match = /^\\s*--([A-Za-z0-9-]+)\\s*:\\s*([\\s\\S]+?)\\s*$/.exec(decl);\n if (match?.[1] && match[2]) out.set(match[1], match[2]);\n }\n return out;\n}\n\nexport function normalise(selector: string): string {\n return selector.replace(/\\s+/g, \" \").replace(/\\s*,\\s*/g, \", \").trim();\n}\n\nexport function readContract(css: string): Contract {\n const rules = readRules(css);\n const theme = rules.find((r) => r.selector === \"@theme inline\");\n if (!theme) throw new Error(\"token sheet has no `@theme inline` block — not the kit's contract\");\n const painted: string[] = [];\n for (const [name, value] of theme.declarations) {\n const match = /^color-(.+)$/.exec(name);\n if (match?.[1] && value === `var(--${match[1]})`) painted.push(match[1]);\n }\n const derivedRule = rules.find((r) => r.selector === DERIVED_SCOPE);\n if (!derivedRule) throw new Error(`token sheet has no \\`${DERIVED_SCOPE}\\` rule — not the kit's contract`);\n const derived = [...derivedRule.declarations.keys()];\n const stray = derived.filter((d) => !painted.includes(d));\n if (stray.length > 0) throw new Error(`derived tokens with no @theme mapping: ${stray.join(\", \")}`);\n const derivedFrom = Object.fromEntries([...derivedRule.declarations].map(([name, value]) => [name, references(value)]));\n return { required: painted.filter((p) => !derived.includes(p)), derived, derivedFrom };\n}\n","/**\n * The subset of TOML a brand file is written in: `[table.headers]`, bare keys,\n * and single-line values that are a basic string, a number, a boolean, or a\n * flat array of those. Anything else is an error naming the line, rather than a\n * silent misread — the kit stays dependency-free, and a brand file has no use\n * for the rest of the language.\n */\n\nexport type TomlValue = string | number | boolean | TomlValue[];\nexport interface TomlTable {\n [key: string]: TomlValue | TomlTable;\n}\n\nconst BARE_KEY = /^[A-Za-z0-9_-]+$/;\n\nexport function parseToml(text: string): TomlTable {\n const root: TomlTable = {};\n let table = root;\n const headers = new Set<string>();\n text.split(/\\r?\\n/).forEach((raw, index) => {\n const line = stripComment(raw).trim();\n if (line === \"\") return;\n const fail = (why: string): never => {\n throw new Error(`TOML line ${index + 1}: ${why}: ${raw.trim()}`);\n };\n\n if (line.startsWith(\"[\")) {\n if (!line.endsWith(\"]\") || line.startsWith(\"[[\")) fail(\"only plain [table] headers are supported\");\n const path = line.slice(1, -1).trim().split(\".\").map((part) => part.trim());\n // Re-opening a table would merge two halves of it silently.\n if (headers.has(path.join(\".\"))) fail(\"duplicate table header\");\n headers.add(path.join(\".\"));\n table = descend(root, path, fail);\n return;\n }\n\n const eq = line.indexOf(\"=\");\n if (eq < 0) fail(\"expected `key = value`\");\n const key = line.slice(0, eq).trim();\n if (!BARE_KEY.test(key)) fail(\"only bare keys are supported\");\n if (key in table) fail(`duplicate key \\`${key}\\``);\n table[key] = parseValue(line.slice(eq + 1).trim(), fail);\n });\n return root;\n}\n\nfunction descend(root: TomlTable, path: string[], fail: (why: string) => never): TomlTable {\n let table = root;\n for (const part of path) {\n const key = part.trim();\n if (!BARE_KEY.test(key)) fail(\"only bare keys are supported in a header\");\n const next = table[key] ?? {};\n if (typeof next !== \"object\" || Array.isArray(next)) fail(`\\`${key}\\` is already a value`);\n table[key] = next;\n table = next as TomlTable;\n }\n return table;\n}\n\n// A `#` inside a string is not a comment; the brand files carry `\"#rrggbb\"`.\nfunction stripComment(line: string): string {\n let quoted = false;\n for (let i = 0; i < line.length; i++) {\n const c = line[i];\n if (c === \"\\\\\" && quoted) i++;\n else if (c === '\"') quoted = !quoted;\n else if (c === \"#\" && !quoted) return line.slice(0, i);\n }\n return line;\n}\n\nfunction parseValue(src: string, fail: (why: string) => never): TomlValue {\n if (src.startsWith('\"')) {\n if (src.length < 2 || !src.endsWith('\"')) fail(\"unterminated string\");\n const body = src.slice(1, -1);\n if (/(^|[^\\\\])(\\\\\\\\)*\"/.test(body)) fail(\"one string per value\");\n // Only the escapes decoded below; `\\u…` and friends would otherwise pass through as literal text.\n if (/\\\\(?![\"\\\\nt])/.test(body.replace(/\\\\\\\\/g, \"\"))) fail(\"unsupported escape\");\n return body.replace(/\\\\([\"\\\\nt])/g, (_, c: string) => ({ n: \"\\n\", t: \"\\t\" })[c] ?? c);\n }\n if (src.startsWith(\"[\")) {\n if (!src.endsWith(\"]\")) fail(\"arrays must close on the same line\");\n const inner = src.slice(1, -1).trim();\n if (inner === \"\") return [];\n return splitTopLevel(inner).map((item) => parseValue(item.trim(), fail));\n }\n if (src === \"true\" || src === \"false\") return src === \"true\";\n if (/^[+-]?\\d+(\\.\\d+)?$/.test(src)) return Number(src);\n return fail(\"unsupported value\");\n}\n\nfunction splitTopLevel(inner: string): string[] {\n const items: string[] = [];\n let quoted = false;\n let start = 0;\n for (let i = 0; i < inner.length; i++) {\n const c = inner[i];\n if (c === \"\\\\\" && quoted) i++;\n else if (c === '\"') quoted = !quoted;\n else if (c === \",\" && !quoted) {\n items.push(inner.slice(start, i));\n start = i + 1;\n }\n }\n const last = inner.slice(start);\n if (last.trim() !== \"\") items.push(last);\n return items;\n}\n","import { references, type Contract } from \"./contract\";\nimport { parseToml, type TomlTable, type TomlValue } from \"./toml\";\n\nexport type PolarityName = \"light\" | \"dark\";\nexport const POLARITIES: readonly PolarityName[] = [\"light\", \"dark\"];\n\n/** A brand's palette: one value per contract name, for each polarity. */\nexport interface BrandConfig {\n colors: Record<PolarityName, Record<string, string>>;\n /** `--brand-mark`: the URL `<Logo>` masks. */\n mark?: string;\n /** `--brand-aspect`: the mark's width / height, e.g. `\"387 / 335\"`. */\n markAspect?: string;\n /** `--brand-font-<role>`: a family name per role; unset roles keep the kit's. */\n fonts?: Partial<Record<FontRole, string>>;\n}\n\nexport type FontRole = \"sans\" | \"display\" | \"mono\";\nexport const FONT_ROLES: readonly FontRole[] = [\"sans\", \"display\", \"mono\"];\n// aquafix's file calls the body family `text`.\nconst FONT_KEYS: Record<string, FontRole> = { sans: \"sans\", text: \"sans\", display: \"display\", mono: \"mono\" };\n\n/** Every problem with a brand file at once, so one run fixes them all. */\nexport class PaletteError extends Error {\n constructor(readonly problems: readonly string[]) {\n super(`invalid brand palette:\\n - ${problems.join(\"\\n - \")}`);\n this.name = \"PaletteError\";\n }\n}\n\n// Written into CSS verbatim, so a value is held to a shape that cannot close\n// the declaration: a hex colour, or a reference to another token.\nconst COLOUR = /^(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{8}|var\\(--[a-z0-9-]+\\))$/;\nconst SLUG = /^[a-z0-9][a-z0-9-]*$/;\nconst ASPECT = /^\\d+(\\.\\d+)?\\s*\\/\\s*\\d+(\\.\\d+)?$/;\n// Written inside `url(\"…\")`: nothing that closes the string or the function,\n// and no markup characters for a sheet that ends up inlined in HTML.\nconst MARK_URL = /^[^\"'`\\\\\\s()<>]+$/;\nconst FAMILY = /^[A-Za-z0-9][A-Za-z0-9 _-]*$/;\n\n/**\n * Reads the aquafix `brand.toml` shape: `[colors.light]` and `[colors.dark]`\n * keyed by contract name, optional top-level `mark` / `mark-aspect`, and the\n * family names in `[fonts]` (`sans` or `text`, `display`, `mono`). The rest of\n * `[fonts]` and every other table (`[print]`, …) belong to other tools and are\n * ignored — loading the font files is the site's job, not the palette's.\n */\nexport function brandFromToml(text: string): BrandConfig {\n const doc = parseToml(text);\n const problems: string[] = [];\n const colors = table(doc[\"colors\"]);\n if (!colors) problems.push(\"no [colors] table\");\n const scope = (name: PolarityName): Record<string, string> => {\n const t = colors ? table(colors[name]) : undefined;\n if (colors && !t) problems.push(`no [colors.${name}] table`);\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(t ?? {})) {\n if (typeof value === \"string\") out[key] = value;\n else problems.push(`colors.${name}.${key} is not a string`);\n }\n return out;\n };\n for (const key of Object.keys(colors ?? {})) {\n if (!(POLARITIES as readonly string[]).includes(key)) problems.push(`[colors.${key}] is not a polarity (light, dark)`);\n }\n const config: BrandConfig = { colors: { light: scope(\"light\"), dark: scope(\"dark\") } };\n const mark = optionalString(doc[\"mark\"], \"mark\", problems);\n const markAspect = optionalString(doc[\"mark-aspect\"], \"mark-aspect\", problems);\n if (mark !== undefined) config.mark = mark;\n if (markAspect !== undefined) config.markAspect = markAspect;\n const fonts = table(doc[\"fonts\"]);\n for (const [key, role] of Object.entries(FONT_KEYS)) {\n const family = optionalString(fonts?.[key], `fonts.${key}`, problems);\n if (family !== undefined) config.fonts = { ...config.fonts, [role]: family };\n }\n if (problems.length > 0) throw new PaletteError(problems);\n return config;\n}\n\nfunction table(value: TomlValue | TomlTable | undefined): TomlTable | undefined {\n return typeof value === \"object\" && !Array.isArray(value) ? value : undefined;\n}\n\nfunction optionalString(value: TomlValue | TomlTable | undefined, key: string, problems: string[]) {\n if (value === undefined || typeof value === \"string\") return value;\n problems.push(`${key} is not a string`);\n return undefined;\n}\n\n/** The problems with `config` against `contract`; empty when it is complete. */\nexport function validateBrand(config: BrandConfig, contract: Contract): string[] {\n const problems: string[] = [];\n const known = new Set([...contract.required, ...contract.derived]);\n for (const polarity of POLARITIES) {\n const values = config.colors[polarity];\n const missing = contract.required.filter((t) => !(t in values));\n if (missing.length > 0) problems.push(`${polarity} is missing ${missing.join(\", \")}`);\n for (const [key, value] of Object.entries(values)) {\n if (!known.has(key)) problems.push(`${polarity}.${key} is not a token of the contract`);\n else if (!COLOUR.test(value)) problems.push(`${polarity}.${key} = ${JSON.stringify(value)} is not #hex or var(--token)`);\n else {\n for (const ref of references(value)) {\n if (!known.has(ref)) problems.push(`${polarity}.${key} refers to --${ref}, which is not a token of the contract`);\n }\n }\n }\n const cycle = findCycle(values, contract, known);\n if (cycle) problems.push(`${polarity} has a reference cycle: ${cycle.map((t) => `--${t}`).join(\" → \")}`);\n }\n if (config.mark !== undefined && !MARK_URL.test(config.mark)) problems.push(`mark ${JSON.stringify(config.mark)} is not a plain URL`);\n if (config.markAspect !== undefined && !ASPECT.test(config.markAspect)) {\n problems.push(`mark-aspect ${JSON.stringify(config.markAspect)} is not \"width / height\"`);\n }\n for (const [role, family] of Object.entries(config.fonts ?? {})) {\n if (!FAMILY.test(family)) problems.push(`fonts.${role} ${JSON.stringify(family)} is not a plain family name`);\n }\n return problems;\n}\n\n/**\n * A `var()` cycle makes every token on it invalid at computed-value time — the\n * same silent hole as a missing token. Edges are the palette's own references,\n * and for a derived token it leaves to the contract, the contract's formula\n * (`border` reads `ink`), so `ink = \"var(--border)\"` is caught too.\n */\nfunction findCycle(values: Record<string, string>, contract: Contract, known: Set<string>): string[] | undefined {\n const edges = (name: string): readonly string[] => {\n const value = values[name];\n if (value !== undefined) return references(value).filter((r) => known.has(r));\n return contract.derivedFrom[name] ?? [];\n };\n const done = new Set<string>();\n const path: string[] = [];\n const visit = (name: string): string[] | undefined => {\n const at = path.indexOf(name);\n if (at >= 0) return [...path.slice(at), name];\n if (done.has(name)) return undefined;\n path.push(name);\n for (const next of edges(name)) {\n const cycle = visit(next);\n if (cycle) return cycle;\n }\n path.pop();\n done.add(name);\n return undefined;\n };\n for (const name of known) {\n const cycle = visit(name);\n if (cycle) return cycle;\n }\n return undefined;\n}\n\n/**\n * A flat sheet scoping `config` under `[data-brand=\"<brand>\"]`: light is the\n * brand's default and `.light` / `.dark` beneath it pick a polarity. Selectors\n * outrank the zero-specificity EV palette, so import order does not matter.\n * Throws a {@link PaletteError} when a scope is incomplete — Tailwind answers an\n * undefined token with no rule at all, so a hole would be silent on the page.\n */\nexport function renderPalette(brand: string, config: BrandConfig, contract: Contract, source = \"a brand file\"): string {\n const problems = SLUG.test(brand) ? [] : [`brand ${JSON.stringify(brand)} is not a lowercase slug`];\n problems.push(...validateBrand(config, contract));\n if (problems.length > 0) throw new PaletteError(problems);\n\n const at = `[data-brand=\"${brand}\"]`;\n const order = [...contract.required, ...contract.derived];\n const block = (selectors: string[], polarity: PolarityName) => {\n const values = config.colors[polarity];\n const body = order.filter((t) => t in values).map((t) => ` --${t}: ${values[t]};\\n`);\n return `${selectors.join(\",\\n\")} {\\n --scheme: ${polarity};\\n${body.join(\"\")}}\\n`;\n };\n const extras: string[] = [];\n if (config.mark !== undefined) extras.push(` --brand-mark: url(\"${config.mark}\");\\n`);\n if (config.markAspect !== undefined) extras.push(` --brand-aspect: ${config.markAspect};\\n`);\n for (const role of FONT_ROLES) {\n const family = config.fonts?.[role];\n if (family !== undefined) extras.push(` --brand-font-${role}: \"${family}\";\\n`);\n }\n\n return [\n // `source` is a file name, free text as far as CSS is concerned: it must not close the comment.\n `/* GENERATED by evinvest-palette from ${source.replaceAll(\"*/\", \"*\\\\/\")} — do not edit. */\\n`,\n block([at, `${at}.light`, `${at} .light`], \"light\"),\n block([`${at}.dark`, `${at} .dark`], \"dark\"),\n ...(extras.length > 0 ? [`${at} {\\n${extras.join(\"\")}}\\n`] : []),\n ].join(\"\\n\");\n}\n"],"mappings":"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","names":["table"]}
|