@weatherboard/gyde-design 0.3.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/LICENSE +21 -0
- package/README.md +264 -0
- package/adoption.mjs +138 -0
- package/agentdocs.mjs +246 -0
- package/boundaries.mjs +350 -0
- package/catalogue.mjs +506 -0
- package/cli.mjs +723 -0
- package/clientboundary.mjs +399 -0
- package/compound.mjs +123 -0
- package/docdrift.mjs +439 -0
- package/emit.mjs +862 -0
- package/enforcement.mjs +100 -0
- package/index.mjs +40 -0
- package/markup.mjs +177 -0
- package/migration.mjs +148 -0
- package/normalise.mjs +416 -0
- package/package.json +59 -0
- package/props.mjs +255 -0
- package/ratchet.mjs +290 -0
- package/rules.mjs +258 -0
- package/scan.mjs +291 -0
- package/stylex.mjs +178 -0
- package/tailwind.mjs +238 -0
- package/tokens.mjs +398 -0
- package/upgrade.mjs +344 -0
- package/usage.mjs +245 -0
- package/wiring.mjs +297 -0
- package/workflow.mjs +221 -0
- package/workspace.mjs +318 -0
package/tokens.mjs
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G-48 — the token dictionary: its shape, its generator, and its guarantees.
|
|
3
|
+
*
|
|
4
|
+
* WHAT GYDE OWNS HERE, AND WHAT IT DOES NOT.
|
|
5
|
+
*
|
|
6
|
+
* Gyde owns the SHAPE — which categories exist, that roles are named for their
|
|
7
|
+
* purpose, that radius is closed, that a theme is complete, that the generated
|
|
8
|
+
* output is checked against its source. The product owns every VALUE. The seed
|
|
9
|
+
* dictionary below is a placeholder palette on purpose: it is replaced on day
|
|
10
|
+
* one and none of the guarantees care what it was replaced with (CHARTER §3).
|
|
11
|
+
*
|
|
12
|
+
* WHY THIS SHAPE.
|
|
13
|
+
*
|
|
14
|
+
* Both mature systems converged on it independently, which is the only reason
|
|
15
|
+
* to be opinionated about any of it:
|
|
16
|
+
*
|
|
17
|
+
* - **Radius closes to three semantic values.** System B and System A both
|
|
18
|
+
* arrived at `card` / `control` / `pill`, separately, with the same names.
|
|
19
|
+
* System A also measured that radius had NOT drifted — 2 distinct
|
|
20
|
+
* values across 807 lines — while type and space had (8 and 15). So closing
|
|
21
|
+
* radius costs nothing and should be first; type and space need budget.
|
|
22
|
+
* - **One shadow.** System A shipped with zero and a test asserting so,
|
|
23
|
+
* precisely so that adding one had to be argued. The console cashed it in for
|
|
24
|
+
* a dock floating over a live product, and the test now guards that there is
|
|
25
|
+
* still exactly one.
|
|
26
|
+
* - **Roles, not appearances.** *"A token whose comment says 'grey' gets used
|
|
27
|
+
* for the wrong grey… If you cannot write what a token is FOR, it is not a
|
|
28
|
+
* token yet."* `describe()` enforces the readable half of this.
|
|
29
|
+
* - **Only colour is themed.** A 16px gap is 16px at night. Emitting space and
|
|
30
|
+
* radius into both theme blocks doubles the file and creates two places for
|
|
31
|
+
* one decision to drift.
|
|
32
|
+
*
|
|
33
|
+
* WHAT IS DELIBERATELY ABSENT: a z-index scale and motion tokens. System B has
|
|
34
|
+
* motion; System A deferred both rather than invent them ahead of need.
|
|
35
|
+
* A scaffolder that emits a category nobody uses has created drift, not
|
|
36
|
+
* structure — the unused parts are the parts that rot.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** A value that differs by theme. Only colour uses this. */
|
|
40
|
+
export const themed = (light, dark) => ({ light, dark });
|
|
41
|
+
const isThemed = (v) => v !== null && typeof v === "object" && "light" in v && "dark" in v;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Words that describe how a value LOOKS rather than what it is FOR.
|
|
45
|
+
*
|
|
46
|
+
* A token called `grey` is a token that will be used for the wrong grey, and no
|
|
47
|
+
* rule downstream can recover from a dictionary whose names are appearances.
|
|
48
|
+
* Checked at generation time so the failure lands on whoever wrote the name.
|
|
49
|
+
*/
|
|
50
|
+
const APPEARANCE_WORDS = new Set([
|
|
51
|
+
"grey", "gray", "red", "blue", "green", "yellow", "orange", "purple", "pink",
|
|
52
|
+
"black", "white", "teal", "cyan", "dark", "light", "big", "small", "tiny",
|
|
53
|
+
"large", "huge", "bold", "thin", "round", "rounded", "square",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The seed dictionary. Every value is a placeholder; every NAME is the contract.
|
|
58
|
+
*
|
|
59
|
+
* The role vocabulary is not invented here — it is System A's, and it
|
|
60
|
+
* earned its place by surviving a second application with a completely
|
|
61
|
+
* different palette mapping onto it exactly: an instrument panel's
|
|
62
|
+
* `live`, `attention`, `good`, `bad` and `unknown` landed on `accent`,
|
|
63
|
+
* `warning`, `success`, `danger` and `faint` with nothing left over.
|
|
64
|
+
*/
|
|
65
|
+
export const SEED = {
|
|
66
|
+
color: {
|
|
67
|
+
surface: { value: themed("#ffffff", "#0b0d10"), role: "the page itself" },
|
|
68
|
+
raised: { value: themed("#f7f8fa", "#12151a"), role: "a panel sitting above the page" },
|
|
69
|
+
inset: { value: themed("#eef0f4", "#171b21"), role: "a well sunk into a panel" },
|
|
70
|
+
canvas: { value: themed("#e9ebef", "#e9ebef"), role: "the area a drawing or map is rendered onto" },
|
|
71
|
+
text: { value: themed("#14181f", "#dfe4ec"), role: "body copy and anything that must be read" },
|
|
72
|
+
muted: { value: themed("#5b6472", "#7d879a"), role: "supporting text that is still information" },
|
|
73
|
+
faint: { value: themed("#8b94a3", "#566072"), role: "present but not information — a placeholder, an absent value" },
|
|
74
|
+
border: { value: themed("#d6dae1", "#242a33"), role: "the line between two surfaces" },
|
|
75
|
+
accent: { value: themed("#1a6dff", "#4aa3ff"), role: "the one action this view wants" },
|
|
76
|
+
onAccent: { value: themed("#ffffff", "#0b0d10"), role: "text drawn on top of accent" },
|
|
77
|
+
success: { value: themed("#1c7f52", "#46c08a"), role: "a thing that finished and was correct" },
|
|
78
|
+
warning: { value: themed("#9a6b12", "#e0a63c"), role: "a thing that needs attention but has not failed" },
|
|
79
|
+
danger: { value: themed("#b03a32", "#e56a63"), role: "a thing that failed, or an action that destroys" },
|
|
80
|
+
},
|
|
81
|
+
space: {
|
|
82
|
+
1: { value: "2px", role: "the gap inside a control, against its own border" },
|
|
83
|
+
2: { value: "4px", role: "between two things that are one thing" },
|
|
84
|
+
3: { value: "8px", role: "between related items in a list" },
|
|
85
|
+
4: { value: "12px", role: "the default gap" },
|
|
86
|
+
5: { value: "16px", role: "padding inside a panel" },
|
|
87
|
+
6: { value: "24px", role: "between sections of a page" },
|
|
88
|
+
7: { value: "32px", role: "between a page's major regions" },
|
|
89
|
+
8: { value: "48px", role: "page margin at the top of a view" },
|
|
90
|
+
},
|
|
91
|
+
radius: {
|
|
92
|
+
card: { value: "8px", role: "anything that is a surface" },
|
|
93
|
+
control: { value: "6px", role: "anything you can click or type into" },
|
|
94
|
+
pill: { value: "999px", role: "a badge or a toggle, where the shape IS the affordance" },
|
|
95
|
+
},
|
|
96
|
+
text: {
|
|
97
|
+
micro: { value: "11px", role: "metadata that is not read, only glanced at" },
|
|
98
|
+
small: { value: "13px", role: "supporting text beside a control" },
|
|
99
|
+
body: { value: "15px", role: "prose meant to be read" },
|
|
100
|
+
lead: { value: "17px", role: "the first line of a section" },
|
|
101
|
+
title: { value: "20px", role: "the name of a panel" },
|
|
102
|
+
display: { value: "28px", role: "the name of a page" },
|
|
103
|
+
},
|
|
104
|
+
weight: { regular: { value: "400", role: "everything" }, medium: { value: "500", role: "a label against its value" }, strong: { value: "650", role: "a heading" } },
|
|
105
|
+
lineHeight: { body: { value: "1.5", role: "anything with more than one line" }, flush: { value: "1.1", role: "a heading, where the leading would read as a gap" } },
|
|
106
|
+
border: { hair: { value: "1px", role: "the ordinary line between surfaces" }, marker: { value: "2px", role: "a line that means selected or active" } },
|
|
107
|
+
shadow: { overlay: { value: themed("0 8px 24px rgb(16 20 26 / 0.14)", "0 8px 24px rgb(0 0 0 / 0.5)"), role: "a surface floating above content it does not belong to" } },
|
|
108
|
+
font: { sans: { value: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif', role: "everything" }, mono: { value: 'ui-monospace, SFMono-Regular, Menlo, monospace', role: "anything the machine wrote" } },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** `lineHeight` → `line-height`, `onAccent` → `on-accent`. */
|
|
112
|
+
export const kebab = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
113
|
+
|
|
114
|
+
/** The custom-property name for a token. This is the contract components read. */
|
|
115
|
+
export const varName = (group, name) => `--${kebab(group)}-${kebab(name)}`;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Structural checks on a dictionary, run before anything is generated.
|
|
119
|
+
*
|
|
120
|
+
* These are Gyde's guarantees, so they run against the PRODUCT's dictionary
|
|
121
|
+
* after it has replaced every value — not just against the seed.
|
|
122
|
+
*/
|
|
123
|
+
export function validate(dict) {
|
|
124
|
+
const problems = [];
|
|
125
|
+
const need = (cond, msg) => { if (!cond) problems.push(msg); };
|
|
126
|
+
|
|
127
|
+
need(dict.color, "no `color` group: a dictionary without colour roles is not a dictionary");
|
|
128
|
+
need(dict.radius, "no `radius` group");
|
|
129
|
+
need(dict.space, "no `space` group");
|
|
130
|
+
need(dict.text, "no `text` group");
|
|
131
|
+
|
|
132
|
+
// Closed sets. The point of a closed set is that widening it is a decision
|
|
133
|
+
// somebody makes on purpose, in a diff, with this message in front of them.
|
|
134
|
+
if (dict.radius) {
|
|
135
|
+
const keys = Object.keys(dict.radius).sort();
|
|
136
|
+
need(
|
|
137
|
+
JSON.stringify(keys) === JSON.stringify(["card", "control", "pill"]),
|
|
138
|
+
`radius must be exactly card/control/pill, got ${keys.join("/")} — ` +
|
|
139
|
+
"two independent systems closed to these three and neither has needed a fourth",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (dict.shadow) {
|
|
143
|
+
need(
|
|
144
|
+
Object.keys(dict.shadow).length <= 1,
|
|
145
|
+
`shadow has ${Object.keys(dict.shadow).length} entries — depth is a semantic decision; ` +
|
|
146
|
+
"adding a second should be argued, not defaulted",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const [group, entries] of Object.entries(dict)) {
|
|
151
|
+
// `$`-prefixed keys are dictionary CONFIGURATION, not token groups. Without
|
|
152
|
+
// this the validator walked `$sameInBothThemes` — an array — and reported
|
|
153
|
+
// its elements as tokens with no value, so declaring a deliberate choice
|
|
154
|
+
// made the dictionary invalid. Config and content must not share a namespace
|
|
155
|
+
// without a marker, and this is the marker.
|
|
156
|
+
if (group.startsWith("$")) continue;
|
|
157
|
+
for (const [name, token] of Object.entries(entries)) {
|
|
158
|
+
const where = `${group}.${name}`;
|
|
159
|
+
if (!token || token.value === undefined) { problems.push(`${where}: no value`); continue; }
|
|
160
|
+
if (!token.role || String(token.role).trim().length < 8) {
|
|
161
|
+
problems.push(`${where}: no role. If you cannot write what a token is FOR, it is not a token yet`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const words = String(token.role).toLowerCase().split(/[^a-z]+/);
|
|
165
|
+
const appearance = words.find((w) => APPEARANCE_WORDS.has(w));
|
|
166
|
+
if (appearance) {
|
|
167
|
+
problems.push(`${where}: role describes appearance ("${appearance}"), not purpose — ` +
|
|
168
|
+
"a token whose role says how it looks gets used for the wrong one");
|
|
169
|
+
}
|
|
170
|
+
// A themed value missing half a theme renders as nothing in that theme,
|
|
171
|
+
// silently. System A's dictionary test catches this and so does ours.
|
|
172
|
+
if (isThemed(token.value)) {
|
|
173
|
+
need(token.value.light && token.value.dark, `${where}: themed value is missing a theme`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A colour identical in both themes is usually a copy-paste, occasionally
|
|
179
|
+
// deliberate. Deliberate must be declared, so the accident cannot hide.
|
|
180
|
+
const deliberate = new Set(dict.$sameInBothThemes || ["canvas"]);
|
|
181
|
+
for (const [name, token] of Object.entries(dict.color || {})) {
|
|
182
|
+
if (isThemed(token.value) && token.value.light === token.value.dark && !deliberate.has(name)) {
|
|
183
|
+
problems.push(`color.${name}: identical in both themes — declare it in $sameInBothThemes if that is intended`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return problems;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The header is deliberately NOT "generated by Gyde".
|
|
192
|
+
*
|
|
193
|
+
* This file lives in the product's repository and the product owns it, so it
|
|
194
|
+
* says what it is rather than who made it. The wording is also load-bearing:
|
|
195
|
+
* the emitted generator produces the same header, and the emitted test asserts
|
|
196
|
+
* the committed stylesheet equals what that generator produces. When the two
|
|
197
|
+
* headers differed, a freshly scaffolded repository shipped a token test that
|
|
198
|
+
* failed on its first run — the generated CSS and the generator disagreed about
|
|
199
|
+
* one line of comment.
|
|
200
|
+
*/
|
|
201
|
+
export const CSS_HEADER = `/* GENERATED — DO NOT EDIT.
|
|
202
|
+
*
|
|
203
|
+
* Edit the dictionary and regenerate. This file is committed so the CSS a
|
|
204
|
+
* reviewer approves is the CSS that ships, and a test asserts it still matches
|
|
205
|
+
* its source — a build-time generation step would mean the reviewed artifact
|
|
206
|
+
* and the shipped one are different files.
|
|
207
|
+
*/`;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Generate the stylesheet.
|
|
211
|
+
*
|
|
212
|
+
* `[data-theme]` is set on a CONTAINER, not only on `:root`. That is not a
|
|
213
|
+
* detail: it is what lets the catalogue render both themes side by side on one
|
|
214
|
+
* page, and what lets a portalled popup find the theme of the surface that
|
|
215
|
+
* opened it (`trigger.closest("[data-theme]")`) instead of rendering light on
|
|
216
|
+
* a dark panel — a real defect the deployed catalogue caught.
|
|
217
|
+
*/
|
|
218
|
+
export function generateCss(dict = SEED) {
|
|
219
|
+
const problems = validate(dict);
|
|
220
|
+
if (problems.length) {
|
|
221
|
+
throw new Error("Refusing to generate CSS from an invalid dictionary:\n " + problems.join("\n "));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const flat = [];
|
|
225
|
+
for (const [group, entries] of Object.entries(dict)) {
|
|
226
|
+
if (group.startsWith("$")) continue;
|
|
227
|
+
for (const [name, token] of Object.entries(entries)) {
|
|
228
|
+
flat.push({ name: varName(group, name), value: token.value });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const decls = (theme) => flat
|
|
233
|
+
.filter((t) => theme === "light" || isThemed(t.value))
|
|
234
|
+
.map((t) => ` ${t.name}: ${isThemed(t.value) ? t.value[theme] : t.value};`)
|
|
235
|
+
.join("\n");
|
|
236
|
+
|
|
237
|
+
return [
|
|
238
|
+
CSS_HEADER,
|
|
239
|
+
"",
|
|
240
|
+
":root {",
|
|
241
|
+
" color-scheme: light dark;",
|
|
242
|
+
decls("light"),
|
|
243
|
+
"}",
|
|
244
|
+
"",
|
|
245
|
+
"/* The system preference, unless a container has explicitly opted out. */",
|
|
246
|
+
'@media (prefers-color-scheme: dark) {',
|
|
247
|
+
' :root:not([data-theme="light"]) {',
|
|
248
|
+
decls("dark").split("\n").map((l) => " " + l).join("\n"),
|
|
249
|
+
" }",
|
|
250
|
+
"}",
|
|
251
|
+
"",
|
|
252
|
+
"/* An explicit choice, on any element — which is what makes both themes",
|
|
253
|
+
" renderable on one page, and what a portalled popup looks up. */",
|
|
254
|
+
'[data-theme="dark"] {',
|
|
255
|
+
decls("dark"),
|
|
256
|
+
"}",
|
|
257
|
+
"",
|
|
258
|
+
'[data-theme="light"] {',
|
|
259
|
+
decls("light"),
|
|
260
|
+
"}",
|
|
261
|
+
"",
|
|
262
|
+
].join("\n");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The plain-constant export.
|
|
267
|
+
*
|
|
268
|
+
* A decision learned rather than designed: **tokens have
|
|
269
|
+
* consumers that are not components.** An annotation SDK drawing an overlay
|
|
270
|
+
* inside somebody else's page cannot read our custom properties — they do not
|
|
271
|
+
* exist there — and anything plotting to a canvas needs the value, not the
|
|
272
|
+
* `var()`. System B ships plain constants beside its StyleX vars for exactly this.
|
|
273
|
+
*
|
|
274
|
+
* Generated from the same dictionary rather than maintained beside it. System B
|
|
275
|
+
* maintains both by hand, because `defineVars` cannot import, and its own
|
|
276
|
+
* comment concedes the two are kept in step "by hand until fixture tests
|
|
277
|
+
* enforce it". Generating both from one source is the whole reason not to
|
|
278
|
+
* require a compiler here.
|
|
279
|
+
*/
|
|
280
|
+
export function generateConstants(dict = SEED, { theme = "light" } = {}) {
|
|
281
|
+
const out = {};
|
|
282
|
+
for (const [group, entries] of Object.entries(dict)) {
|
|
283
|
+
if (group.startsWith("$")) continue;
|
|
284
|
+
out[group] = {};
|
|
285
|
+
for (const [name, token] of Object.entries(entries)) {
|
|
286
|
+
out[group][name] = isThemed(token.value) ? token.value[theme] : token.value;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Emit the W3C Design Tokens (DTCG) form.
|
|
294
|
+
*
|
|
295
|
+
* The prior-art survey found the format reached a first stable release and is
|
|
296
|
+
* supported by Style Dictionary and Tokens Studio, so emitting it costs little
|
|
297
|
+
* and buys interoperability with tooling we are not going to write. It is an
|
|
298
|
+
* OUTPUT, not the source: the source stays a plain object because that is what
|
|
299
|
+
* carries the `role` prose the validator depends on.
|
|
300
|
+
*
|
|
301
|
+
* NOTE: the DTCG version and its adoption were reported by the survey and are
|
|
302
|
+
* not verified here. Confirm before treating this output as conformant.
|
|
303
|
+
*/
|
|
304
|
+
export function generateDtcg(dict = SEED, { theme = "light" } = {}) {
|
|
305
|
+
const TYPE = { color: "color", space: "dimension", radius: "dimension", text: "dimension", border: "dimension", shadow: "shadow", font: "fontFamily", weight: "fontWeight", lineHeight: "number" };
|
|
306
|
+
const out = {};
|
|
307
|
+
for (const [group, entries] of Object.entries(dict)) {
|
|
308
|
+
if (group.startsWith("$")) continue;
|
|
309
|
+
out[group] = { $type: TYPE[group] ?? "other" };
|
|
310
|
+
for (const [name, token] of Object.entries(entries)) {
|
|
311
|
+
out[group][name] = {
|
|
312
|
+
$value: isThemed(token.value) ? token.value[theme] : token.value,
|
|
313
|
+
$description: token.role,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Emit the StyleX form: `defineVars` per group, plus the dark theme.
|
|
322
|
+
*
|
|
323
|
+
* WHY THIS EXISTS, AND WHY IT IS THE ARGUMENT FOR G-100.
|
|
324
|
+
*
|
|
325
|
+
* G-58 rejected mandating StyleX partly on a measured cost: `tokens.stylex.ts`
|
|
326
|
+
* cannot import `index.ts`, because `defineVars` must resolve statically
|
|
327
|
+
* without executing the module. A consumer therefore maintains the same token
|
|
328
|
+
* values by hand in two files, conceding in their own comment that they are
|
|
329
|
+
* kept in step "by hand until the fixture tests enforce it."
|
|
330
|
+
*
|
|
331
|
+
* That is a real defect and it was the strongest thing said against the
|
|
332
|
+
* mandate. It is also not a property of StyleX — it is a property of writing
|
|
333
|
+
* the StyleX file by hand. G-58 said so itself: "Gyde's generator emits CSS,
|
|
334
|
+
* plain constants and DTCG from one dictionary, which is the problem not
|
|
335
|
+
* existing rather than being solved."
|
|
336
|
+
*
|
|
337
|
+
* So this is the problem not existing. Both files come from the same
|
|
338
|
+
* dictionary, in the same run, and cannot disagree — there is no second copy of
|
|
339
|
+
* the values for anybody to keep in step, by hand or otherwise.
|
|
340
|
+
*
|
|
341
|
+
* The static constraint is respected rather than worked around: the emitted
|
|
342
|
+
* module is literal values and `defineVars` calls, importing nothing. Anything
|
|
343
|
+
* that needed a runtime lookup would reintroduce exactly the failure this
|
|
344
|
+
* removes.
|
|
345
|
+
*
|
|
346
|
+
* @gyde-emits-source-for-another-repo
|
|
347
|
+
*/
|
|
348
|
+
export function generateStyleX(dict = SEED) {
|
|
349
|
+
const problems = validate(dict);
|
|
350
|
+
if (problems.length) {
|
|
351
|
+
throw new Error("Refusing to generate StyleX vars from an invalid dictionary:\n " + problems.join("\n "));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const groups = Object.keys(dict).filter((g) => !g.startsWith("$"));
|
|
355
|
+
const L = [];
|
|
356
|
+
|
|
357
|
+
L.push("// GENERATED — DO NOT EDIT.");
|
|
358
|
+
L.push("//");
|
|
359
|
+
L.push("// Emitted from the same dictionary as the CSS, in the same run. There is no");
|
|
360
|
+
L.push("// second copy of these values to keep in step: editing this file by hand is");
|
|
361
|
+
L.push("// how the two-file drift this generator exists to remove comes back.");
|
|
362
|
+
L.push("//");
|
|
363
|
+
L.push("// Imports nothing on purpose. `defineVars` must resolve statically without");
|
|
364
|
+
L.push("// executing the module, so a value read from another module cannot appear here.");
|
|
365
|
+
L.push("");
|
|
366
|
+
L.push('import { defineVars, createTheme } from "@stylexjs/stylex";');
|
|
367
|
+
L.push("");
|
|
368
|
+
|
|
369
|
+
for (const group of groups) {
|
|
370
|
+
const entries = Object.entries(dict[group]);
|
|
371
|
+
L.push(`export const ${group} = defineVars({`);
|
|
372
|
+
for (const [name, token] of entries) {
|
|
373
|
+
const v = isThemed(token.value) ? token.value.light : token.value;
|
|
374
|
+
L.push(` ${name}: ${JSON.stringify(String(v))},${token.role ? ` // ${token.role}` : ""}`);
|
|
375
|
+
}
|
|
376
|
+
L.push("});");
|
|
377
|
+
L.push("");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Only groups that actually carry a dark value get a theme. Emitting an
|
|
381
|
+
// empty createTheme for `space` would be a file saying something it does not
|
|
382
|
+
// mean, and the next reader would look for the dark spacing scale.
|
|
383
|
+
const themedGroups = groups.filter((g) => Object.values(dict[g]).some((t) => isThemed(t.value)));
|
|
384
|
+
if (themedGroups.length) {
|
|
385
|
+
L.push("// The dark theme, applied by putting this class on a container.");
|
|
386
|
+
for (const group of themedGroups) {
|
|
387
|
+
L.push(`export const ${group}Dark = createTheme(${group}, {`);
|
|
388
|
+
for (const [name, token] of Object.entries(dict[group])) {
|
|
389
|
+
if (!isThemed(token.value)) continue;
|
|
390
|
+
L.push(` ${name}: ${JSON.stringify(String(token.value.dark))},`);
|
|
391
|
+
}
|
|
392
|
+
L.push("});");
|
|
393
|
+
L.push("");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return L.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
398
|
+
}
|