actual-css 0.11.0 → 0.12.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +29 -5
  3. package/components.json +15 -17
  4. package/dist/actual.full.css +403 -227
  5. package/dist/actual.full.min.css +1 -1
  6. package/docs/brand/favicon.svg +5 -0
  7. package/docs/brand/lockup.svg +12 -0
  8. package/docs/brand/logo-dark.svg +9 -0
  9. package/docs/brand/logo-light.svg +9 -0
  10. package/docs/brand/logo.svg +11 -0
  11. package/docs/brand/mark.svg +11 -0
  12. package/docs/navigation.json +4 -2
  13. package/docs/pages/components/alert.md +2 -10
  14. package/docs/pages/components/badge.md +3 -9
  15. package/docs/pages/components/bleed.md +5 -4
  16. package/docs/pages/components/card.md +31 -2
  17. package/docs/pages/components/close.md +9 -3
  18. package/docs/pages/components/dialog.md +248 -31
  19. package/docs/pages/components/drawer.md +2 -2
  20. package/docs/pages/components/meter.md +2 -0
  21. package/docs/pages/components/navbar.md +8 -9
  22. package/docs/pages/components/table.md +3 -3
  23. package/docs/pages/enhancements/flyout.md +2 -2
  24. package/docs/pages/enhancements/scrollspy.md +1 -1
  25. package/docs/pages/enhancements/status-bar.md +2 -2
  26. package/docs/pages/examples/overview.md +3 -1
  27. package/docs/pages/forms/date-picker.md +90 -0
  28. package/docs/pages/forms/detached-actions.md +37 -30
  29. package/docs/pages/forms/form-actions.md +61 -28
  30. package/docs/pages/forms/input-policy.md +1 -1
  31. package/docs/pages/forms/overview.md +6 -5
  32. package/docs/pages/forms/sticky-actions.md +85 -83
  33. package/docs/pages/forms/validation.md +5 -5
  34. package/docs/pages/foundations/philosophy.md +5 -3
  35. package/docs/pages/foundations/theming.md +15 -4
  36. package/docs/pages/foundations/tokens.md +50 -33
  37. package/docs/pages/foundations/typography.md +4 -7
  38. package/docs/pages/guides/bootstrap.md +2 -2
  39. package/docs/pages/guides/design-tools.md +36 -0
  40. package/docs/pages/guides/integrating-actual-css.md +5 -1
  41. package/docs/pages/guides/introduction.md +3 -4
  42. package/docs/pages/guides/modular-import.md +3 -0
  43. package/docs/pages/guides/progressive-enhancement.md +2 -2
  44. package/docs/pages/guides/tailwind.md +70 -0
  45. package/docs/pages/guides/upgrading.md +74 -0
  46. package/docs/pages/layout/cluster.md +28 -2
  47. package/docs/pages/layout/overview.md +1 -1
  48. package/docs/pages/layout/stack.md +21 -16
  49. package/docs/pages/layout/topbar.md +1 -1
  50. package/docs/pages/patterns/navigation-list.md +16 -46
  51. package/docs/pages/patterns/overview.md +3 -4
  52. package/docs/pages/utilities/flex.md +30 -20
  53. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  54. package/docs/pages/utilities/spacing.md +4 -3
  55. package/package.json +3 -2
  56. package/reserved-classes.json +3 -3
  57. package/skills/actual-css/references/enhancements.md +38 -0
  58. package/src/cli/actual-css.js +75 -6
  59. package/src/css/components/avatar.css +16 -9
  60. package/src/css/components/badge.css +1 -5
  61. package/src/css/components/bleed.css +5 -5
  62. package/src/css/components/button.css +17 -10
  63. package/src/css/components/card.css +18 -10
  64. package/src/css/components/chat.css +4 -5
  65. package/src/css/components/close.css +17 -6
  66. package/src/css/components/drawer.css +19 -3
  67. package/src/css/components/flyout.css +3 -0
  68. package/src/css/components/index.css +0 -1
  69. package/src/css/components/meter.css +5 -2
  70. package/src/css/components/modal.css +157 -69
  71. package/src/css/components/navbar.css +14 -25
  72. package/src/css/core/base.css +1 -1
  73. package/src/css/core/reset.css +2 -1
  74. package/src/css/core/theme.css +4 -0
  75. package/src/css/core/tokens.css +28 -10
  76. package/src/css/core/variants.css +41 -14
  77. package/src/css/forms/form-actions.css +15 -10
  78. package/src/css/forms/native.css +5 -1
  79. package/src/css/layout/cluster.css +11 -0
  80. package/src/css/layout/topbar.css +12 -6
  81. package/src/css/utilities/base.css +5 -0
  82. package/src/css/utilities/extra.css +3 -7
  83. package/src/tooling/color.js +65 -0
  84. package/src/tooling/design-components.js +286 -0
  85. package/src/tooling/design-export.js +69 -0
  86. package/src/tooling/design-tokens.js +474 -0
  87. package/src/tooling/penpot-plugin/manifest.json +7 -0
  88. package/src/tooling/penpot-plugin/plugin.js +272 -0
  89. package/src/tooling/penpot-plugin/ui.html +58 -0
  90. package/src/tooling/theme-contrast.js +209 -0
  91. package/docs/pages/patterns/actions.md +0 -52
  92. package/src/css/components/actions.css +0 -27
@@ -0,0 +1,474 @@
1
+ /*
2
+ * Design-tool token export: resolves a theme's tokens in headless Chrome and
3
+ * writes them for Figma (DTCG files, one per mode) and Penpot (one token file
4
+ * with its sets and themes).
5
+ *
6
+ * Values are read from the browser, never from the CSS text: tokens are built
7
+ * with light-dark(), color-mix() and var() chains that only the browser
8
+ * evaluates faithfully. The CSS source only tells which declarations are pure
9
+ * aliases (`--a: var(--b)`), so the design tool keeps the relationship instead
10
+ * of two unrelated equal values. Names stay the CSS names without `--`.
11
+ * Bun only: Bun.WebView drives the browser.
12
+ */
13
+ import { readFile } from "node:fs/promises";
14
+ import { stripComments } from "./css-bundle.js";
15
+
16
+ const CORE_TOKEN_FILES = ["../css/core/tokens.css", "../css/core/theme.css"];
17
+
18
+ /* Painted colors, one value per mode. The knobs that produce derived colors
19
+ (mix ratios, backdrop color and opacity) are not exported: the design tool
20
+ receives what is painted, not how it was computed. */
21
+ export const THEME_COLORS = [
22
+ "primary",
23
+ "primary-fg",
24
+ "secondary",
25
+ "secondary-fg",
26
+ "success",
27
+ "success-fg",
28
+ "warning",
29
+ "warning-fg",
30
+ "danger",
31
+ "danger-fg",
32
+ "neutral",
33
+ "neutral-fg",
34
+ "surface",
35
+ "surface-raised",
36
+ "surface-subtle",
37
+ "surface-solid",
38
+ "surface-solid-fg",
39
+ "surface-opaque",
40
+ "text",
41
+ "text-muted",
42
+ "text-subtle",
43
+ "heading",
44
+ "border",
45
+ "focus",
46
+ "hover-overlay",
47
+ "hover-overlay-solid",
48
+ "shadow-color",
49
+ "selection-bg",
50
+ "selection-fg",
51
+ "state-selected",
52
+ "state-selected-fg",
53
+ "state-disabled",
54
+ "indicator-ring",
55
+ "backdrop-fill",
56
+ ];
57
+
58
+ /* Mode-independent tokens by role. The role tells a design tool where a value
59
+ applies (Penpot token types); every role but font-weight is a length. */
60
+ export const FOUNDATIONS = {
61
+ spacing: [
62
+ "space-10",
63
+ "space-20",
64
+ "space-30",
65
+ "space-40",
66
+ "space-50",
67
+ "space-60",
68
+ "gap",
69
+ "control-pad-x-sm",
70
+ "control-pad-x-md",
71
+ "control-pad-x-lg",
72
+ "control-pad-x",
73
+ ],
74
+ radius: ["radius-sm", "radius", "radius-lg", "radius-full"],
75
+ "border-width": ["border-width", "focus-ring-width"],
76
+ "font-size": [
77
+ "font-size-xs",
78
+ "font-size-sm",
79
+ "font-size-md",
80
+ "font-size-lg",
81
+ "control-font-size",
82
+ ],
83
+ size: ["control-size-sm", "control-size-md", "control-size-lg", "control-size", "bar-height"],
84
+ offset: ["focus-outline-offset"],
85
+ "font-weight": [
86
+ "font-weight-normal",
87
+ "font-weight-medium",
88
+ "font-weight-semibold",
89
+ "font-weight-bold",
90
+ ],
91
+ };
92
+
93
+ /* Every custom property declared by the core token files is exported or
94
+ listed here (tests/design-tokens.test.js), so a new token cannot silently
95
+ stay out of the design tools. */
96
+ export const NOT_EXPORTED = {
97
+ "font stack; a design tool font token holds one family": ["font-sans", "font-mono"],
98
+ "internal 0/1 weights selecting a control step; the steps are exported": [
99
+ "control-step-sm",
100
+ "control-step-md",
101
+ "control-step-lg",
102
+ ],
103
+ "no design tool equivalent": [
104
+ "font-width",
105
+ "line-height",
106
+ "line-height-tight",
107
+ "line-height-relaxed",
108
+ "shadow",
109
+ "shadow-popout",
110
+ "duration",
111
+ "duration-fast",
112
+ "duration-slow",
113
+ "duration-spin",
114
+ "duration-shimmer",
115
+ "ease-enter",
116
+ "ease-exit",
117
+ "viewport-block",
118
+ "viewport-inline",
119
+ "z-sticky",
120
+ "z-menu",
121
+ "z-status",
122
+ "icon-chevron",
123
+ "icon-check",
124
+ "icon-close",
125
+ "icon-plus",
126
+ ],
127
+ "input of a derived value": [
128
+ "backdrop-color",
129
+ "backdrop-opacity",
130
+ "soft-bg-mix",
131
+ "soft-border-mix",
132
+ "soft-hover-alpha",
133
+ "soft-fg-mix",
134
+ "focus-outline",
135
+ "focus-outline-color",
136
+ ],
137
+ "soft recipe, exported with the components": [
138
+ "primary-soft-fg",
139
+ "secondary-soft-fg",
140
+ "success-soft-fg",
141
+ "warning-soft-fg",
142
+ "danger-soft-fg",
143
+ "neutral-soft-fg",
144
+ ],
145
+ "internal or plumbing": [
146
+ "density-space",
147
+ "disabled-opacity",
148
+ "indicator-offset",
149
+ "choice-control-size",
150
+ "hgroup-gap",
151
+ "tone-fg",
152
+ ],
153
+ };
154
+
155
+ const WEIGHTS = FOUNDATIONS["font-weight"];
156
+ const LENGTHS = Object.entries(FOUNDATIONS).flatMap(([role, names]) =>
157
+ role === "font-weight" ? [] : names,
158
+ );
159
+ const ROLE_OF = new Map(
160
+ Object.entries(FOUNDATIONS).flatMap(([role, names]) => names.map((n) => [n, role])),
161
+ );
162
+
163
+ const DECLARATION_RE = /(?<![-\w])--([a-z0-9-]+)\s*:\s*([^;{}]*);/gi;
164
+ const ALIAS_RE = /^var\(\s*--([a-z0-9-]+)\s*\)$/i;
165
+ const THEME_NAME_RE = /\[data-theme="([^"]+)"\]/g;
166
+
167
+ export function declaredTokens(css) {
168
+ return new Set([...stripComments(css).matchAll(DECLARATION_RE)].map((m) => m[1]));
169
+ }
170
+
171
+ /* Every `--a: var(--b)` declaration, per name. Only candidates: the source
172
+ cannot tell which one applies (a theme may alias a token the core declares
173
+ literally, a forced-colors block may replace an alias), so the resolved
174
+ values decide. */
175
+ export function aliasCandidates(...sources) {
176
+ const aliases = new Map();
177
+ for (const css of sources) {
178
+ for (const [, name, value] of stripComments(css).matchAll(DECLARATION_RE)) {
179
+ const target = value.trim().match(ALIAS_RE)?.[1];
180
+ if (target) aliases.set(name, [...(aliases.get(name) ?? []), target]);
181
+ }
182
+ }
183
+ return aliases;
184
+ }
185
+
186
+ /* The one data-theme value a theme file declares, light/dark excluded. */
187
+ export function themeNameOf(css) {
188
+ const names = new Set(
189
+ [...css.matchAll(THEME_NAME_RE)].map((m) => m[1]).filter((n) => n !== "light" && n !== "dark"),
190
+ );
191
+ if (names.size !== 1) {
192
+ throw new Error(
193
+ `Expected one [data-theme="…"] name in the theme, found ${names.size === 0 ? "none" : [...names].join(", ")}; pass the name explicitly.`,
194
+ );
195
+ }
196
+ return [...names][0];
197
+ }
198
+
199
+ export async function coreTokenCss() {
200
+ const files = CORE_TOKEN_FILES.map((path) => readFile(new URL(path, import.meta.url), "utf8"));
201
+ return (await Promise.all(files)).join("\n");
202
+ }
203
+
204
+ /*
205
+ * Runs in the page. Each island is one mode: `color-scheme` on the island is
206
+ * what light-dark() resolves against. Colors go through color-mix(in srgb) so
207
+ * every color syntax computes to the same `color(srgb r g b / a)` form.
208
+ */
209
+ function readIslands({ css, islands, colors, lengths, weights }) {
210
+ const style = document.createElement("style");
211
+ style.textContent = css;
212
+ document.head.append(style);
213
+
214
+ const result = [];
215
+ for (const { attribute, scheme } of islands) {
216
+ const island = document.createElement("div");
217
+ if (attribute) island.dataset.theme = attribute;
218
+ if (scheme) island.style.colorScheme = scheme;
219
+ const probe = document.createElement("div");
220
+ probe.style.position = "absolute";
221
+ island.append(probe);
222
+ document.body.append(island);
223
+
224
+ const computed = getComputedStyle(probe);
225
+ const values = { colors: {}, lengths: {}, weights: {}, missing: [] };
226
+ const read = (name) => {
227
+ const raw = computed.getPropertyValue(`--${name}`).trim();
228
+ if (!raw) values.missing.push(name);
229
+ return raw;
230
+ };
231
+
232
+ for (const name of colors) {
233
+ if (!read(name)) continue;
234
+ probe.style.color = `color-mix(in srgb, var(--${name}), var(--${name}))`;
235
+ values.colors[name] = computed.color;
236
+ }
237
+ for (const name of lengths) {
238
+ const raw = read(name);
239
+ if (!raw) continue;
240
+ // column-gap reports the computed length; width would report the used
241
+ // one, snapped to 1/64px by layout (0.65rem came out as 10.39).
242
+ probe.style.columnGap = `var(--${name})`;
243
+ values.lengths[name] = { px: Number.parseFloat(computed.columnGap), css: raw };
244
+ }
245
+ for (const name of weights) {
246
+ const raw = read(name);
247
+ if (raw) values.weights[name] = Number(raw);
248
+ }
249
+ values.scheme = attribute ? getComputedStyle(island).colorScheme : null;
250
+ result.push(values);
251
+ island.remove();
252
+ }
253
+ return result;
254
+ }
255
+
256
+ /* Runs `fn(input)` in a blank headless page. `fn` is serialized, so it must be
257
+ self-contained: no closure over module scope. */
258
+ export async function inPage(fn, input) {
259
+ await using view = new Bun.WebView({ backend: "chrome" });
260
+ await view.navigate("about:blank");
261
+ // `return await`: a bare return lets `await using` close the view first.
262
+ return await view.evaluate(`(${fn.toString()})(${JSON.stringify(input)})`);
263
+ }
264
+
265
+ const evaluateIslands = (input) => inPage(readIslands, input);
266
+
267
+ /*
268
+ * Resolves one theme. `css` is the full stylesheet (framework + theme, imports
269
+ * inlined). Without `theme`, the default theme is read through its light and
270
+ * dark boundaries. A named theme exports the schemes it declares: `light dark`
271
+ * gives two modes, anything else one. `islands` is returned so component
272
+ * measurements read the same boundaries.
273
+ */
274
+ export async function resolveTokens({ css, theme, aliasSources = [] }) {
275
+ let islands = [
276
+ { mode: "light", attribute: "light" },
277
+ { mode: "dark", attribute: "dark" },
278
+ ];
279
+ if (theme) {
280
+ const [declared] = await evaluateIslands({
281
+ css,
282
+ islands: [{ attribute: theme }],
283
+ colors: [],
284
+ lengths: [],
285
+ weights: [],
286
+ });
287
+ const schemes = declared.scheme.split(/\s+/).filter((s) => s === "light" || s === "dark");
288
+ islands = (schemes.length > 0 ? schemes : ["light"]).map((mode) => ({
289
+ mode,
290
+ attribute: theme,
291
+ scheme: mode,
292
+ }));
293
+ }
294
+
295
+ const read = await evaluateIslands({
296
+ css,
297
+ islands,
298
+ colors: THEME_COLORS,
299
+ lengths: LENGTHS,
300
+ weights: WEIGHTS,
301
+ });
302
+ const missing = [...new Set(read.flatMap((values) => values.missing))];
303
+ if (missing.length > 0) {
304
+ throw new Error(`Tokens without a value: ${missing.map((n) => `--${n}`).join(", ")}`);
305
+ }
306
+
307
+ const modes = Object.fromEntries(
308
+ islands.map(({ mode }, i) => [
309
+ mode,
310
+ Object.fromEntries(Object.entries(read[i].colors).map(([n, c]) => [n, parseSrgb(c)])),
311
+ ]),
312
+ );
313
+ // Geometry does not follow the color scheme; the first mode is the reference.
314
+ const [{ lengths, weights }] = read;
315
+ return { islands, modes, lengths, weights, aliases: aliasCandidates(...aliasSources) };
316
+ }
317
+
318
+ export function parseSrgb(value) {
319
+ const match = value.match(/^color\(srgb ([^ ]+) ([^ ]+) ([^ )/]+)(?: \/ ([^ )]+))?\)$/);
320
+ if (!match) throw new Error(`Unexpected computed color: ${value}`);
321
+ const clamp = (n) => Math.min(1, Math.max(0, Number(n)));
322
+ return {
323
+ components: match.slice(1, 4).map((n) => round(clamp(n))),
324
+ alpha: match[4] === undefined ? 1 : round(clamp(match[4])),
325
+ };
326
+ }
327
+
328
+ const round = (n) => Math.round(n * 10000) / 10000;
329
+
330
+ export function hexOf({ components, alpha }) {
331
+ const byte = (n) =>
332
+ Math.round(n * 255)
333
+ .toString(16)
334
+ .padStart(2, "0");
335
+ return `#${components.map(byte).join("")}${alpha < 1 ? byte(alpha) : ""}`;
336
+ }
337
+
338
+ const colorsEqual = (a, b) =>
339
+ a.alpha === b.alpha && a.components.every((n, i) => n === b.components[i]);
340
+
341
+ /* A candidate becomes an alias only when its target is exported in the same
342
+ group and holds the same value in every mode; otherwise the theme overrode
343
+ the token literally. */
344
+ function aliasOf(name, valueSets, aliases, same) {
345
+ return aliases.get(name)?.find((t) => valueSets.every((v) => t in v && same(v[t], v[name])));
346
+ }
347
+
348
+ /*
349
+ * The target-neutral model both writers read: per mode, each color is
350
+ * `{ alias }` or `{ color, hex }`; each foundation carries its role and is
351
+ * `{ alias }`, `{ px, css }` or `{ weight }`.
352
+ */
353
+ export function linkTokens({ modes, lengths, weights, aliases }) {
354
+ const modeSets = Object.values(modes);
355
+ const linkedModes = Object.fromEntries(
356
+ Object.entries(modes).map(([mode, colors]) => [
357
+ mode,
358
+ Object.fromEntries(
359
+ Object.entries(colors).map(([name, color]) => {
360
+ const alias = aliasOf(name, modeSets, aliases, colorsEqual);
361
+ return [name, alias ? { alias } : { color, hex: hexOf(color) }];
362
+ }),
363
+ ),
364
+ ]),
365
+ );
366
+
367
+ const foundations = {};
368
+ for (const [name, length] of Object.entries(lengths)) {
369
+ const alias = aliasOf(name, [lengths], aliases, (a, b) => a.px === b.px);
370
+ foundations[name] = { role: ROLE_OF.get(name), ...(alias ? { alias } : length) };
371
+ }
372
+ for (const [name, weight] of Object.entries(weights)) {
373
+ foundations[name] = { role: "font-weight", weight };
374
+ }
375
+ return { modes: linkedModes, foundations };
376
+ }
377
+
378
+ const EXTENSION = "actual-css";
379
+
380
+ /*
381
+ * Figma: DTCG documents keyed by file name, `foundations` plus one per color
382
+ * mode. Figma turns each imported file into a collection whose single mode is
383
+ * named after the file, hence `light` / `dark`. Recipe colors (see
384
+ * design-components.js) join the mode files so they switch with the mode.
385
+ */
386
+ export function toFigma({ modes, foundations }, recipes = {}) {
387
+ const files = {};
388
+ for (const [mode, colors] of Object.entries(modes)) {
389
+ files[mode] = Object.fromEntries(
390
+ Object.entries({ ...colors, ...recipes[mode] }).map(([name, token]) => [
391
+ name,
392
+ {
393
+ $type: "color",
394
+ $value: token.alias
395
+ ? `{${token.alias}}`
396
+ : { colorSpace: "srgb", ...token.color, hex: token.hex },
397
+ },
398
+ ]),
399
+ );
400
+ }
401
+
402
+ files.foundations = Object.fromEntries(
403
+ Object.entries(foundations).map(([name, token]) => {
404
+ // `number`, not DTCG `fontWeight`: Figma's import drops fontWeight
405
+ // tokens, and a number variable is what binds to a text layer's weight.
406
+ if (token.role === "font-weight") return [name, { $type: "number", $value: token.weight }];
407
+ const $value = token.alias ? `{${token.alias}}` : { value: token.px, unit: "px" };
408
+ const doc = { $type: "dimension", $value };
409
+ if (token.css) doc.$extensions = { [EXTENSION]: { css: token.css } };
410
+ return [name, doc];
411
+ }),
412
+ );
413
+ return files;
414
+ }
415
+
416
+ /* Penpot token types per role: the type decides which property a token can
417
+ be applied to in the Penpot UI. */
418
+ const PENPOT_TYPES = {
419
+ spacing: "spacing",
420
+ radius: "borderRadius",
421
+ "border-width": "borderWidth",
422
+ "font-size": "fontSizes",
423
+ size: "sizing",
424
+ offset: "dimension",
425
+ "font-weight": "fontWeights",
426
+ };
427
+
428
+ const PENPOT_THEME_GROUP = "Mode";
429
+
430
+ /*
431
+ * Penpot: one token file holding every set plus `$themes` and `$metadata`.
432
+ * Sets are `foundations` and `mode/<scheme>`, recipe colors included (their
433
+ * names keep them apart, and they always switch with the mode); each theme of
434
+ * the Mode group enables foundations and one scheme, so switching theme only
435
+ * swaps colors. Values are plain strings (hex, `16px`): Penpot does not read
436
+ * the DTCG object forms.
437
+ */
438
+ export function toPenpot({ modes, foundations }, recipes = {}) {
439
+ const doc = {
440
+ foundations: Object.fromEntries(
441
+ Object.entries(foundations).map(([name, token]) => {
442
+ const $type = PENPOT_TYPES[token.role];
443
+ if (token.alias) return [name, { $type, $value: `{${token.alias}}` }];
444
+ return [name, { $type, $value: token.weight ?? `${token.px}px` }];
445
+ }),
446
+ ),
447
+ };
448
+
449
+ const colorSet = (colors) =>
450
+ Object.fromEntries(
451
+ Object.entries(colors).map(([name, token]) => [
452
+ name,
453
+ { $type: "color", $value: token.alias ? `{${token.alias}}` : token.hex },
454
+ ]),
455
+ );
456
+ const schemes = Object.keys(modes);
457
+ for (const mode of schemes) {
458
+ doc[`mode/${mode}`] = colorSet({ ...modes[mode], ...recipes[mode] });
459
+ }
460
+
461
+ const title = (mode) => mode[0].toUpperCase() + mode.slice(1);
462
+ doc.$themes = schemes.map((mode) => ({
463
+ id: `mode-${mode}`,
464
+ name: title(mode),
465
+ group: PENPOT_THEME_GROUP,
466
+ selectedTokenSets: { foundations: "enabled", [`mode/${mode}`]: "enabled" },
467
+ }));
468
+ doc.$metadata = {
469
+ tokenSetOrder: ["foundations", ...schemes.map((mode) => `mode/${mode}`)],
470
+ activeThemes: [`${PENPOT_THEME_GROUP}/${title(schemes[0])}`],
471
+ activeSets: ["foundations", `mode/${schemes[0]}`],
472
+ };
473
+ return doc;
474
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "Actual CSS components",
3
+ "description": "Builds Actual CSS components from a measured spec and binds them to the imported tokens.",
4
+ "version": 2,
5
+ "code": "plugin.js",
6
+ "permissions": ["content:read", "content:write", "library:read", "library:write"]
7
+ }