@motion-proto/live-tokens 0.55.1 → 0.57.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/.claude/skills/{live-tokens-adjust-shape-space → live-tokens-adjust-geometry}/SKILL.md +5 -5
- package/.claude/skills/live-tokens-build-page/SKILL.md +2 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +20 -357
- package/.claude/skills/live-tokens-create-component/references/fixed-overlays.md +3 -0
- package/.claude/skills/live-tokens-create-component/references/intrinsics.md +58 -0
- package/.claude/skills/live-tokens-create-component/references/linked-siblings.md +70 -0
- package/.claude/skills/live-tokens-generate-theme/SKILL.md +79 -98
- package/.claude/skills/live-tokens-generate-theme/references/named-themes.md +18 -0
- package/.claude/skills/live-tokens-pair-fonts/SKILL.md +88 -0
- package/.claude/skills/live-tokens-pick-component/SKILL.md +4 -1
- package/CHANGELOG.md +197 -0
- package/README.md +21 -8
- package/bin/cli.mjs +42 -3
- package/bin/set-fonts.mjs +280 -0
- package/dist-plugin/adjust/index.cjs +1 -0
- package/dist-plugin/adjust/index.d.cts +3 -3
- package/dist-plugin/adjust/index.d.ts +3 -3
- package/dist-plugin/adjust/index.js +1 -1
- package/dist-plugin/{chunk-TKZBVIW5.js → chunk-E5QYON4L.js} +59 -2
- package/dist-plugin/{chunk-PB2JTK2H.js → chunk-LW4SR7AZ.js} +39 -6
- package/dist-plugin/{chunk-YR6GPXW2.js → chunk-XWXIMTWZ.js} +0 -5
- package/dist-plugin/{chunk-D3ZVKOR4.js → chunk-Y5CNFSSV.js} +1 -0
- package/dist-plugin/{dataPaths-DBN0RPuT.d.cts → dataPaths-CRfD1LdA.d.cts} +3 -0
- package/dist-plugin/{dataPaths-DBN0RPuT.d.ts → dataPaths-CRfD1LdA.d.ts} +3 -0
- package/dist-plugin/fontPairing/index.cjs +413 -0
- package/dist-plugin/fontPairing/index.d.cts +109 -0
- package/dist-plugin/fontPairing/index.d.ts +109 -0
- package/dist-plugin/fontPairing/index.js +321 -0
- package/dist-plugin/generateColorsAndType/index.cjs +22 -9
- package/dist-plugin/generateColorsAndType/index.d.cts +3 -3
- package/dist-plugin/generateColorsAndType/index.d.ts +3 -3
- package/dist-plugin/generateColorsAndType/index.js +8 -5
- package/dist-plugin/index-4N-Orzzi.d.cts +3 -0
- package/dist-plugin/index-4N-Orzzi.d.ts +3 -0
- package/dist-plugin/index.cjs +158 -9
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +71 -4
- package/dist-plugin/migrateData/index.cjs +1 -0
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +1 -1
- package/dist-plugin/{index-DpTIRZ2H.d.cts → themeTypes-DSV3Zisf.d.cts} +13 -4
- package/dist-plugin/{index-DpTIRZ2H.d.ts → themeTypes-DSV3Zisf.d.ts} +13 -4
- package/dist-plugin/tokensCssMigrations/index.cjs +58 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/dist-plugin/tokensCssMigrations/index.js +3 -3
- package/package.json +5 -2
- package/src/editor/core/fonts/applyFontPairing.ts +158 -0
- package/src/editor/core/fonts/fontLoader.ts +1 -0
- package/src/editor/core/fonts/fontMigration.ts +31 -1
- package/src/editor/core/fonts/fontPairing.ts +13 -6
- package/src/editor/core/fonts/googleFontsUrl.ts +123 -0
- package/src/editor/core/fonts/weightCoverage.ts +92 -0
- package/src/editor/core/palettes/paletteDerivation.ts +9 -2
- package/src/editor/core/sketch/maskField.ts +381 -0
- package/src/editor/core/sketch/sketchLayer.ts +1028 -0
- package/src/editor/core/sketch/sketchPresetService.ts +65 -0
- package/src/editor/core/sketch/sketchPresets.ts +328 -0
- package/src/editor/core/sketch/sketchStore.ts +190 -0
- package/src/editor/core/store/editorPersistence.ts +28 -12
- package/src/editor/core/store/editorTypes.ts +4 -1
- package/src/editor/core/store/editorViewStore.ts +2 -2
- package/src/editor/core/store/gradientSource.ts +15 -1
- package/src/editor/core/themes/parsers/gradient.ts +62 -4
- package/src/editor/core/themes/slices/gradients.ts +75 -14
- package/src/editor/core/themes/themeTypes.ts +6 -1
- package/src/editor/docs/Docs.svelte +4 -3
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/01-overview.md +2 -1
- package/src/editor/docs/content/editing-tokens.md +5 -1
- package/src/editor/docs/content/sketch-mode.md +89 -0
- package/src/editor/docs/content/themes-workflow.md +39 -0
- package/src/editor/docs/content.generated.ts +4 -3
- package/src/editor/overlay/LiveEditorOverlay.svelte +15 -0
- package/src/editor/pages/EditorShell.svelte +43 -0
- package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
- package/src/editor/ui/FontStackEditor.svelte +3 -0
- package/src/editor/ui/GradientEditor.svelte +38 -1
- package/src/editor/ui/ProjectFontsSection.svelte +24 -36
- package/src/editor/ui/UIReveal.svelte +7 -1
- package/src/editor/ui/UISegmentedControl.svelte +4 -8
- package/src/editor/ui/sections/GradientsSection.svelte +47 -1
- package/src/editor/ui/sketch/SketchDial.svelte +124 -0
- package/src/editor/ui/sketch/SketchPreview.svelte +119 -0
- package/src/editor/ui/sketch/SketchRange.svelte +185 -0
- package/src/editor/ui/sketch/SketchTab.svelte +1263 -0
- package/src/live-tokens/data/colors-and-type/autumn.json +17 -0
- package/src/live-tokens/data/colors-and-type/default.json +17 -0
- package/src/live-tokens/data/colors-and-type/halloween.json +17 -0
- package/src/live-tokens/data/colors-and-type/midnight-study.json +17 -0
- package/src/live-tokens/data/colors-and-type/ocean.json +17 -0
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +17 -0
- package/src/live-tokens/data/colors-and-type/sketches.json +2520 -0
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +17 -0
- package/src/live-tokens/data/colors-and-type/sunset.json +17 -0
- package/src/live-tokens/data/themes/autumn.json +17 -0
- package/src/live-tokens/data/themes/halloween.json +17 -0
- package/src/live-tokens/data/themes/midnight-study.json +17 -0
- package/src/live-tokens/data/themes/ocean.json +17 -0
- package/src/live-tokens/data/themes/royal-velvet.json +17 -0
- package/src/live-tokens/data/themes/sketches.json +3984 -0
- package/src/live-tokens/data/themes/spring-meadow.json +17 -0
- package/src/live-tokens/data/themes/sunset.json +17 -0
- package/src/live-tokens/data/tokens.generated.css +1 -0
- package/src/system/styles/tokens.css +15 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# LiveTokens
|
|
2
2
|
|
|
3
3
|
A design system for styling and building Svelte + Vite microsites. Edit tokens and components in a dev-only editor and watch the running site repaint on every input. Save the result as a theme file and carry it between projects.
|
|
4
4
|
|
|
@@ -17,7 +17,7 @@ The editor is dev-only. Production builds get plain CSS variables and the compon
|
|
|
17
17
|
- **Themes.** A theme is a whole look in one file: colors and type plus a config for every component, stored by value. Loading one changes a single pointer file, and nothing your site ships changes until you Adopt. Export a theme and import it into another project to restore the look in one step.
|
|
18
18
|
- **Seven example looks.** Autumn, Halloween, Midnight Study, Ocean, Royal Velvet, Spring Meadow, and Sunset each ship as a full theme: colors, a Google Fonts pairing, and a shape personality of radius, padding, gap, and border-width aliases. They ship inside the package, so trying one needs no local files. Load Motion Proto to return to the default. Saving over a preset writes a local copy that shadows the shipped one; delete the copy and the shipped version returns.
|
|
19
19
|
- **Vite plugin.** Hosts the `/api/live-tokens/{colors-and-type,component-configs,themes}/*` routes the editor reads and writes through. The single namespace keeps these routes clear of anything your app serves under `/api`.
|
|
20
|
-
- **Claude Code skills.**
|
|
20
|
+
- **Claude Code skills.** Six bundled skills that drive the package from plain English. See [Claude Code skills](#claude-code-skills).
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
@@ -267,13 +267,14 @@ npx @motion-proto/live-tokens <command>
|
|
|
267
267
|
| `check-component <id>` | Validate a component's runtime, editor, and registration against the authoring contract. |
|
|
268
268
|
| `generate-theme <brief.json> [--no-activate] [--dry-run] [--carry-from <name>]` | Build a full theme from a 10-seed OKLCH brief, enforce AA contrast, write `themes/<slug>.json`, and open it. |
|
|
269
269
|
| `adjust <ops.json> [--dry-run]` | Move radius, padding, gap, and border-width aliases along their token scales. |
|
|
270
|
+
| `set-fonts <brief.json> [--dry-run] [--no-verify]` | Bind Google Fonts families to the theme's font stacks, verified against the API. |
|
|
270
271
|
| `migrate [--check] [--write] [--tokens <path>]` | Reconcile the project with the installed package: additive `tokens.css` migrations, the pre-0.48 data-tree move, and a report on source references to the routes that moved in 0.35.0. |
|
|
271
272
|
|
|
272
273
|
Once installed in a project, the same commands are available as `npx live-tokens <command>`.
|
|
273
274
|
|
|
274
275
|
## Claude Code skills
|
|
275
276
|
|
|
276
|
-
The package bundles
|
|
277
|
+
The package bundles six Claude Code skills. They encode the conventions this README cannot carry in full: which component fits a need, how a page is wired, what a valid theme looks like in OKLCH, how two typefaces sit together, and how geometry moves along the token scales. Each triggers from an ordinary request, so there are no slash commands to learn.
|
|
277
278
|
|
|
278
279
|
### Install
|
|
279
280
|
|
|
@@ -303,13 +304,25 @@ The skill composes the page from shipped components, styles every value with `va
|
|
|
303
304
|
|
|
304
305
|
Ask for a look: "a dark, moody night theme", "a St Patrick's Day theme in green and gold", "warmer", "more contrast", "calmer".
|
|
305
306
|
|
|
306
|
-
|
|
307
|
+
A theme is three decisions made from one brief: color, type, and geometry. The skill owns color and delegates the other two to `live-tokens-pair-fonts` and `live-tokens-adjust-geometry`, so the whole look comes from the same reading of the brief.
|
|
307
308
|
|
|
308
|
-
|
|
309
|
+
For color it translates the brief into ten OKLCH seeds (Brand, Accent, Special, Canvas, Neutral, Alternate, Info, Success, Warning, Danger) plus a light or dark scheme, then runs `npx live-tokens generate-theme <brief.json>`. The CLI assembles the curves, enforces AA contrast on derived text tokens and auto-corrects where it can, writes `themes/<slug>.json`, opens it, and prints a contrast report. Exit 1 means the seeds themselves are unworkable, and each failure line names the seed to change.
|
|
309
310
|
|
|
310
|
-
|
|
311
|
+
Most of the skill is the judgment the generator cannot supply: a chroma budget scaled to how much screen area each palette covers, per-role lightness and hue bands for each scheme, gamut guardrails against impossible seeds, harmony modes, the optional canvas gradient, and a voice-to-shape table for the shape step. OKLCH anchors for named holidays and seasons live in a reference file the skill reads on demand.
|
|
311
312
|
|
|
312
|
-
|
|
313
|
+
Color lands in the theme file; type and shape land in the unsaved buffers, and one Save keeps all three. `--dry-run` prints the report without writing; `--no-activate` writes without opening. Opening a theme never changes what your site ships; Adopt does. Regenerating replaces that theme's whole color state, including palette edits made in the editor since the last run, and carries the live buffers forward, so re-rolling color after setting fonts and shape keeps both.
|
|
314
|
+
|
|
315
|
+
### `live-tokens-pair-fonts`
|
|
316
|
+
|
|
317
|
+
Ask for type: "pair some fonts for this theme", "what font should the headings use?", "make the type more editorial", "something friendlier", "a serif for headings".
|
|
318
|
+
|
|
319
|
+
The skill chooses the families and runs `npx live-tokens set-fonts <brief.json>`, which binds each one to `--font-display`, `--font-sans`, `--font-serif`, or `--font-mono`. Every family is checked against the Google Fonts API before it is written, and the URL is built from the weights that family actually has: a range for a variable font, an enumeration for a static one, a bare URL for a single-weight display face. The report names the weights your typography tokens ask for and the family does not carry.
|
|
320
|
+
|
|
321
|
+
The judgment is the skill's half. It anchors on the body face, because that is most of the words on the page and text faces survive small sizes where display faces do not. It classifies both candidates by form model (dynamic, rational, geometric) and applies the font matrix: two faces sharing a skeleton under different surfaces pair reliably, two faces sharing a surface over different skeletons fight, and two faces far apart on both read as a decision. It also carries the screen test a body face has to pass, a voice table from brief to type, and the Google Fonts superfamilies for when the type should stay quiet.
|
|
322
|
+
|
|
323
|
+
Scope: type only, and never color. Edits land in the colors-and-type `_working.json` buffer, so save the open theme to keep them. `--dry-run` reports without writing.
|
|
324
|
+
|
|
325
|
+
### `live-tokens-adjust-geometry`
|
|
313
326
|
|
|
314
327
|
Ask for shape or space: "make the buttons pill shaped", "sharper corners on the cards", "space it out", "tighter", "thinner borders".
|
|
315
328
|
|
|
@@ -323,7 +336,7 @@ Edits land in each affected component's `_working.json` buffer, which is what th
|
|
|
323
336
|
|
|
324
337
|
Ask for something the catalogue lacks: "author a Rating component", "make my Chip component editable in the editor".
|
|
325
338
|
|
|
326
|
-
The skill covers the
|
|
339
|
+
The skill covers the recipe: the runtime `.svelte` file with its `:global(:root)` token block, the editor `.svelte` file exporting `allTokens` and its variant groups, the `registerComponent()` call, and the catalogue entry that keeps `live-tokens-pick-component` current. It carries the naming scheme, the token suffix vocabulary, the state model (component states such as selected and disabled are separate from interaction states such as hover), and the public-imports rule, and points at the shipped `Toggle` in `node_modules` as the worked example. Linked siblings, intrinsics, and the fixed-overlay portal rule sit in reference files the skill reads only when a component needs them.
|
|
327
340
|
|
|
328
341
|
Verify the result:
|
|
329
342
|
|
package/bin/cli.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// check-component <id> Validate a component against the add-component skill contract.
|
|
7
7
|
// generate-theme <brief> Build a theme from a 10-seed OKLCH brief and open it.
|
|
8
8
|
// adjust <ops.json> Apply radius/padding/gap/border-width ops to the open buffer.
|
|
9
|
+
// set-fonts <brief.json> Bind Google Fonts families to the theme's font stacks.
|
|
9
10
|
// migrate [...] Reconcile tokens.css, the data tree, and route references.
|
|
10
11
|
|
|
11
12
|
import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
@@ -23,6 +24,7 @@ import { runMigrateRoutes, formatRouteResult } from './migrate-routes.mjs';
|
|
|
23
24
|
import { runCreate, formatCreateResult } from './create.mjs';
|
|
24
25
|
import { runGenerateTheme, formatGenerateThemeResult } from './generate-theme.mjs';
|
|
25
26
|
import { runAdjust, formatAdjustResult } from './adjust.mjs';
|
|
27
|
+
import { runSetFonts, formatSetFontsResult } from './set-fonts.mjs';
|
|
26
28
|
|
|
27
29
|
const USAGE = `Usage: npx @motion-proto/live-tokens <command> [options]
|
|
28
30
|
|
|
@@ -47,11 +49,23 @@ Commands:
|
|
|
47
49
|
adjust <ops.json> [--dry-run]
|
|
48
50
|
Move radius, padding, gap, and border-width
|
|
49
51
|
aliases along their token scales (see the
|
|
50
|
-
live-tokens-adjust-
|
|
52
|
+
live-tokens-adjust-geometry skill). Reads each
|
|
51
53
|
component's live config and writes the result to
|
|
52
54
|
that component's unsaved buffer, so save the open
|
|
53
55
|
theme in the editor to keep it. --dry-run prints
|
|
54
56
|
the report without writing.
|
|
57
|
+
set-fonts <brief.json> [--dry-run] [--no-verify]
|
|
58
|
+
Bind Google Fonts families to --font-display,
|
|
59
|
+
--font-sans, --font-serif, --font-mono and
|
|
60
|
+
--font-editorial (see
|
|
61
|
+
the live-tokens-pair-fonts skill). Each family is
|
|
62
|
+
verified against the Google Fonts API and the URL
|
|
63
|
+
is negotiated from the weights it actually has.
|
|
64
|
+
Writes the result to the unsaved colors-and-type
|
|
65
|
+
buffer, so save the open theme in the editor to
|
|
66
|
+
keep it. --dry-run prints the report without
|
|
67
|
+
writing; --no-verify skips the network and
|
|
68
|
+
requires an explicit URL per family.
|
|
55
69
|
migrate [--check] [--write] [--tokens <path>]
|
|
56
70
|
Reconcile your project with the installed package:
|
|
57
71
|
applies additive tokens.css migrations, moves a
|
|
@@ -148,6 +162,30 @@ if (command === 'adjust') {
|
|
|
148
162
|
}
|
|
149
163
|
}
|
|
150
164
|
|
|
165
|
+
if (command === 'set-fonts') {
|
|
166
|
+
const briefPath = rest.find((a) => !a.startsWith('-'));
|
|
167
|
+
if (!briefPath) {
|
|
168
|
+
fail(`Usage: npx @motion-proto/live-tokens set-fonts <brief.json> [--dry-run] [--no-verify]`);
|
|
169
|
+
}
|
|
170
|
+
if (rest.includes('--no-activate')) {
|
|
171
|
+
fail(
|
|
172
|
+
`set-fonts has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
|
|
173
|
+
`Drop the flag and re-run.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const result = await runSetFonts({
|
|
178
|
+
briefPath,
|
|
179
|
+
dryRun: rest.includes('--dry-run'),
|
|
180
|
+
verify: !rest.includes('--no-verify'),
|
|
181
|
+
});
|
|
182
|
+
console.log(formatSetFontsResult(result));
|
|
183
|
+
process.exit(0);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
fail(`set-fonts failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
151
189
|
if (command === 'migrate') {
|
|
152
190
|
const check = rest.includes('--check');
|
|
153
191
|
const write = rest.includes('--write');
|
|
@@ -229,8 +267,9 @@ const SAMPLE_PROMPTS = {
|
|
|
229
267
|
'live-tokens-build-page': 'build a pricing page using live-tokens components',
|
|
230
268
|
'live-tokens-pick-component': "what's the difference between TabBar and SegmentedControl?",
|
|
231
269
|
'live-tokens-create-component': 'author a new Toggle component for my live-tokens project',
|
|
232
|
-
'live-tokens-generate-theme': 'make me a bright and cheerful
|
|
233
|
-
'live-tokens-adjust-
|
|
270
|
+
'live-tokens-generate-theme': 'make me a bright and cheerful theme',
|
|
271
|
+
'live-tokens-adjust-geometry': 'make the buttons pill shaped',
|
|
272
|
+
'live-tokens-pair-fonts': 'pair some fonts for this theme',
|
|
234
273
|
};
|
|
235
274
|
|
|
236
275
|
const installedSamples = skills
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// `live-tokens set-fonts` worker.
|
|
2
|
+
//
|
|
3
|
+
// Reads a pairing brief (JSON), verifies each family against Google Fonts,
|
|
4
|
+
// binds it to a font stack via the compiled engine (dist-plugin/fontPairing —
|
|
5
|
+
// the CLI never imports TS sources), and writes the result into
|
|
6
|
+
// `colors-and-type/_working.json`: the same buffer the editor's own font edits
|
|
7
|
+
// land in, so a retype is an unsaved edit the user saves into a theme when they
|
|
8
|
+
// want to keep it. Named colors-and-type files, themes, tokens.css and
|
|
9
|
+
// fonts.css are never touched, and nothing is activated.
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
import { resolveTokensCssPath } from './migrate.mjs';
|
|
16
|
+
|
|
17
|
+
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const ENGINE = resolve(pkgRoot, 'dist-plugin/fontPairing/index.js');
|
|
19
|
+
const packageDataDir = join(pkgRoot, 'src/live-tokens/data');
|
|
20
|
+
|
|
21
|
+
// Google serves a different stylesheet per user agent; ask as a browser would
|
|
22
|
+
// so the weight census matches what the page will actually load.
|
|
23
|
+
const BROWSER_UA =
|
|
24
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36';
|
|
25
|
+
|
|
26
|
+
const SOURCE_LABELS = {
|
|
27
|
+
working: 'your unsaved edits',
|
|
28
|
+
theme: 'the open theme',
|
|
29
|
+
default: 'the package default',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
async function loadEngine() {
|
|
33
|
+
if (!existsSync(ENGINE)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`font pairing engine not found at ${relative(process.cwd(), ENGINE)}. ` +
|
|
36
|
+
`Build the plugin first (npm run build:plugin).`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return import(ENGINE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readJson(path) {
|
|
43
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJsonIfExists(path) {
|
|
47
|
+
return existsSync(path) ? readJson(path) : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function browserFetch(url) {
|
|
51
|
+
return fetch(url, { headers: { 'user-agent': BROWSER_UA } });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The open theme, read the way every other door reads it: the local file
|
|
55
|
+
* first, then the copy the installed package ships. */
|
|
56
|
+
function readActiveTheme(themesDir) {
|
|
57
|
+
const slug = readJsonIfExists(join(themesDir, '_active.json'))?.activeFile ?? 'default';
|
|
58
|
+
const theme =
|
|
59
|
+
readJsonIfExists(join(themesDir, `${slug}.json`)) ??
|
|
60
|
+
readJsonIfExists(join(packageDataDir, 'themes', `${slug}.json`));
|
|
61
|
+
return theme ? { slug, theme } : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Mirrors the dev server's live colors-and-type resolution: the unsaved
|
|
65
|
+
* buffer, then the open theme's own copy, then the package default. */
|
|
66
|
+
function readLiveColorsAndType(colorsAndTypeDir, active) {
|
|
67
|
+
const working = readJsonIfExists(join(colorsAndTypeDir, '_working.json'));
|
|
68
|
+
if (working) return { colorsAndType: working, source: 'working' };
|
|
69
|
+
if (active?.theme?.colorsAndType) return { colorsAndType: active.theme.colorsAndType, source: 'theme' };
|
|
70
|
+
const shipped =
|
|
71
|
+
readJsonIfExists(join(colorsAndTypeDir, 'default.json')) ??
|
|
72
|
+
readJsonIfExists(join(packageDataDir, 'colors-and-type', 'default.json'));
|
|
73
|
+
if (!shipped) throw new Error(`no colors and type to read at ${colorsAndTypeDir}`);
|
|
74
|
+
return { colorsAndType: shipped, source: 'default' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeFace(slot, value) {
|
|
78
|
+
if (typeof value === 'string') return { name: value };
|
|
79
|
+
if (value && typeof value === 'object' && typeof value.name === 'string') {
|
|
80
|
+
return { name: value.name, ...(value.url ? { url: value.url } : {}) };
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`brief slot "${slot}" must be a family name or { "name": "...", "url": "..." }`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sameJson(a, b) {
|
|
86
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `engine` and `fetcher` are test seams; the CLI always runs the compiled
|
|
90
|
+
* bundle against the live API. */
|
|
91
|
+
export async function runSetFonts({
|
|
92
|
+
briefPath,
|
|
93
|
+
dryRun = false,
|
|
94
|
+
verify = true,
|
|
95
|
+
root = process.cwd(),
|
|
96
|
+
colorsAndTypeDir,
|
|
97
|
+
themesDir,
|
|
98
|
+
tokensCssPath,
|
|
99
|
+
engine,
|
|
100
|
+
fetcher = browserFetch,
|
|
101
|
+
} = {}) {
|
|
102
|
+
const {
|
|
103
|
+
applyFontPairing,
|
|
104
|
+
resolveGoogleFont,
|
|
105
|
+
requiredWeights,
|
|
106
|
+
weightCoverage,
|
|
107
|
+
SLOT_ORDER,
|
|
108
|
+
SLOT_VARIABLES,
|
|
109
|
+
readLiveTokensConfig,
|
|
110
|
+
resolveDataDirs,
|
|
111
|
+
} = engine ?? (await loadEngine());
|
|
112
|
+
|
|
113
|
+
const briefFull = resolve(root, briefPath);
|
|
114
|
+
if (!existsSync(briefFull)) throw new Error(`brief file not found at ${relative(root, briefFull)}`);
|
|
115
|
+
let brief;
|
|
116
|
+
try {
|
|
117
|
+
brief = readJson(briefFull);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
throw new Error(`brief file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const requested = SLOT_ORDER.filter((slot) => brief[slot] !== undefined).map((slot) => ({
|
|
123
|
+
slot,
|
|
124
|
+
face: normalizeFace(slot, brief[slot]),
|
|
125
|
+
}));
|
|
126
|
+
if (requested.length === 0) {
|
|
127
|
+
throw new Error(`brief names no slot. Use one or more of: ${SLOT_ORDER.join(', ')}.`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const resolved = colorsAndTypeDir && themesDir ? null : resolveDataDirs();
|
|
131
|
+
const colorsDir = colorsAndTypeDir ?? resolved.colorsAndTypeDir;
|
|
132
|
+
const themes = themesDir ?? resolved.themesDir;
|
|
133
|
+
if (!existsSync(colorsDir)) {
|
|
134
|
+
throw new Error(`no colors and type at ${relative(root, colorsDir)}. Run the dev server once to create it.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const active = readActiveTheme(themes);
|
|
138
|
+
const { colorsAndType, source } = readLiveColorsAndType(colorsDir, active);
|
|
139
|
+
|
|
140
|
+
const pairing = {};
|
|
141
|
+
const faces = [];
|
|
142
|
+
for (const { slot, face } of requested) {
|
|
143
|
+
if (!verify) {
|
|
144
|
+
if (!face.url) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`--no-verify needs an explicit URL for "${slot}". Give it as ` +
|
|
147
|
+
`{ "name": "${face.name}", "url": "https://fonts.googleapis.com/css2?..." } ` +
|
|
148
|
+
`or drop the flag and let the CLI negotiate one.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
pairing[slot] = { name: face.name, url: face.url };
|
|
152
|
+
faces.push({ slot, name: face.name, url: face.url, weights: [], italics: false, verified: false });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const found = face.url
|
|
156
|
+
? { name: face.name, url: face.url, weights: [], italics: false }
|
|
157
|
+
: await resolveGoogleFont(face.name, fetcher);
|
|
158
|
+
pairing[slot] = { name: found.name, url: found.url, weights: found.weights, italics: found.italics };
|
|
159
|
+
faces.push({ slot, ...found, verified: true, pinned: Boolean(face.url) });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const { colorsAndType: next, report } = applyFontPairing(colorsAndType, pairing);
|
|
163
|
+
|
|
164
|
+
const tokensPath = resolveTokensCssPath(tokensCssPath, readLiveTokensConfig().tokensCssPath, root);
|
|
165
|
+
let coverage = [];
|
|
166
|
+
if (tokensPath && existsSync(tokensPath)) {
|
|
167
|
+
const covered = {};
|
|
168
|
+
for (const face of faces) {
|
|
169
|
+
if (!face.verified || face.weights.length === 0) continue;
|
|
170
|
+
covered[SLOT_VARIABLES[face.slot]] = { name: face.name, weights: face.weights, italics: face.italics };
|
|
171
|
+
}
|
|
172
|
+
coverage = weightCoverage(
|
|
173
|
+
requiredWeights(readFileSync(tokensPath, 'utf8'), next.cssVariables ?? {}),
|
|
174
|
+
covered,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const workingPath = join(colorsDir, '_working.json');
|
|
179
|
+
const savedColorsAndType =
|
|
180
|
+
active?.theme?.colorsAndType ??
|
|
181
|
+
readJsonIfExists(join(colorsDir, 'default.json')) ??
|
|
182
|
+
readJsonIfExists(join(packageDataDir, 'colors-and-type', 'default.json'));
|
|
183
|
+
|
|
184
|
+
// Returning the buffer to what the open theme already holds is a discard,
|
|
185
|
+
// not an edit — the same call the dev server's own PUT makes.
|
|
186
|
+
const backToSaved = savedColorsAndType !== null && sameJson(next, savedColorsAndType);
|
|
187
|
+
let wrote = null;
|
|
188
|
+
if (!dryRun && report.changed) {
|
|
189
|
+
if (backToSaved) {
|
|
190
|
+
if (existsSync(workingPath)) rmSync(workingPath);
|
|
191
|
+
wrote = 'cleared';
|
|
192
|
+
} else {
|
|
193
|
+
writeFileSync(workingPath, JSON.stringify(next, null, 2) + '\n');
|
|
194
|
+
wrote = 'buffer';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
briefPath: briefFull,
|
|
200
|
+
colorsAndTypeDir: colorsDir,
|
|
201
|
+
workingPath,
|
|
202
|
+
openTheme: active?.slug ?? null,
|
|
203
|
+
source,
|
|
204
|
+
verified: verify,
|
|
205
|
+
dryRun,
|
|
206
|
+
faces,
|
|
207
|
+
changes: report.changes,
|
|
208
|
+
dropped: report.dropped,
|
|
209
|
+
coverage,
|
|
210
|
+
changed: report.changed,
|
|
211
|
+
wrote,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function formatSetFontsResult(result) {
|
|
216
|
+
const root = process.cwd();
|
|
217
|
+
const lines = [];
|
|
218
|
+
|
|
219
|
+
if (!result.changed) {
|
|
220
|
+
lines.push('Nothing to change: those faces are already bound to those stacks.');
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const from =
|
|
225
|
+
result.source === 'theme' && result.openTheme
|
|
226
|
+
? `theme "${result.openTheme}"`
|
|
227
|
+
: SOURCE_LABELS[result.source];
|
|
228
|
+
const verb = result.dryRun ? 'Would set' : 'Set';
|
|
229
|
+
lines.push(`${verb} ${result.changes.length} font stack(s), reading from ${from}.`);
|
|
230
|
+
|
|
231
|
+
const width = Math.max(0, ...result.changes.map((c) => c.variable.length));
|
|
232
|
+
for (const change of result.changes) {
|
|
233
|
+
lines.push(` ${change.variable.padEnd(width)} ${change.from ?? '(none)'} → ${change.to}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const face of result.faces) {
|
|
237
|
+
if (!face.verified) continue;
|
|
238
|
+
const weights = face.weights.length > 0 ? `${face.weights.join(', ')}` : 'single weight';
|
|
239
|
+
lines.push(`\n${face.name} (${face.slot})`);
|
|
240
|
+
lines.push(` weights: ${weights}${face.italics ? ' + italics' : ', no italics'}`);
|
|
241
|
+
lines.push(` ${face.url}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (result.dropped.length > 0) {
|
|
245
|
+
lines.push(
|
|
246
|
+
`\nDropped ${result.dropped.length} source(s) no stack references any more: ` +
|
|
247
|
+
`${result.dropped.map((d) => d.names.join(', ')).join('; ')}.`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const gaps = result.coverage.filter((c) => c.missing.length > 0);
|
|
252
|
+
if (gaps.length > 0) {
|
|
253
|
+
lines.push('\nWeight coverage:');
|
|
254
|
+
for (const gap of gaps) {
|
|
255
|
+
lines.push(
|
|
256
|
+
` ${gap.family} (${gap.stack}) has no ${gap.missing.join(', ')}; ` +
|
|
257
|
+
`tokens ask for ${gap.required.join(', ')}. The browser will synthesize the rest.`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const body = result.coverage.find((c) => c.stack === '--font-sans');
|
|
262
|
+
if (body && !body.italics) {
|
|
263
|
+
lines.push(`\n${body.family} has no italics. Emphasis in body text will be synthesized.`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (result.wrote === 'buffer') {
|
|
267
|
+
lines.push(
|
|
268
|
+
`\nReload the app to see it. This is an unsaved edit: save the open theme in the ` +
|
|
269
|
+
`editor's Theme panel to keep it, or load a theme to discard it.`,
|
|
270
|
+
);
|
|
271
|
+
} else if (result.wrote === 'cleared') {
|
|
272
|
+
lines.push(
|
|
273
|
+
`\nThat is what the open theme already holds, so the unsaved buffer was discarded. ` +
|
|
274
|
+
`Reload the app to see it.`,
|
|
275
|
+
);
|
|
276
|
+
} else if (result.dryRun) {
|
|
277
|
+
lines.push(`\nDry run: nothing written under ${relative(root, result.colorsAndTypeDir)}.`);
|
|
278
|
+
}
|
|
279
|
+
return lines.join('\n');
|
|
280
|
+
}
|
|
@@ -270,6 +270,7 @@ function resolveDataDirs(opts = {}) {
|
|
|
270
270
|
colorsAndTypeDir: opts.colorsAndTypeDir ? import_path.default.resolve(opts.colorsAndTypeDir) : fileConfig.colorsAndTypeDir ? import_path.default.resolve(fileConfig.colorsAndTypeDir) : sub("colors-and-type"),
|
|
271
271
|
componentConfigsDir: opts.componentConfigsDir ? import_path.default.resolve(opts.componentConfigsDir) : fileConfig.componentConfigsDir ? import_path.default.resolve(fileConfig.componentConfigsDir) : sub("component-configs"),
|
|
272
272
|
themesDir: opts.themesDir ? import_path.default.resolve(opts.themesDir) : fileConfig.themesDir ? import_path.default.resolve(fileConfig.themesDir) : sub("themes"),
|
|
273
|
+
sketchPresetsDir: sub("sketch-presets"),
|
|
273
274
|
legacyManifestsDir: fileConfig.manifestsDir ? import_path.default.resolve(fileConfig.manifestsDir) : void 0
|
|
274
275
|
};
|
|
275
276
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as AliasDiskValue,
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
1
|
+
import { A as AliasDiskValue, a as ComponentConfig } from '../themeTypes-DSV3Zisf.cjs';
|
|
2
|
+
export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-CRfD1LdA.cjs';
|
|
3
|
+
export { C as CURRENT_COMPONENT_SCHEMA_VERSION } from '../index-4N-Orzzi.cjs';
|
|
4
4
|
|
|
5
5
|
type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
|
|
6
6
|
interface AdjustOp {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as AliasDiskValue,
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
1
|
+
import { A as AliasDiskValue, a as ComponentConfig } from '../themeTypes-DSV3Zisf.js';
|
|
2
|
+
export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-CRfD1LdA.js';
|
|
3
|
+
export { C as CURRENT_COMPONENT_SCHEMA_VERSION } from '../index-4N-Orzzi.js';
|
|
4
4
|
|
|
5
5
|
type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
|
|
6
6
|
interface AdjustOp {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
hexToOklch,
|
|
3
3
|
oklchToCss
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XWXIMTWZ.js";
|
|
5
5
|
|
|
6
6
|
// src/editor/core/themes/parsers/globalRootBlock.ts
|
|
7
7
|
function extractGlobalRootBody(source) {
|
|
@@ -375,6 +375,61 @@ var tokensCssMigration_2026_08_19_oklchColorValues = {
|
|
|
375
375
|
}
|
|
376
376
|
};
|
|
377
377
|
|
|
378
|
+
// vite-plugin/tokensCssMigrations/migrations/2026-08-25-editorial-type-role.ts
|
|
379
|
+
var STACK = [{ name: "--font-editorial", value: "var(--font-sans)" }];
|
|
380
|
+
var BUNDLE2 = [
|
|
381
|
+
{ name: "--editorial-font-family", value: "var(--font-editorial)" },
|
|
382
|
+
{ name: "--editorial-font-size", value: "var(--font-size-md)" },
|
|
383
|
+
{ name: "--editorial-font-weight", value: "var(--font-weight-normal)" },
|
|
384
|
+
{ name: "--editorial-line-height", value: "var(--line-height-normal)" },
|
|
385
|
+
{ name: "--editorial-letter-spacing", value: "var(--letter-spacing-normal)" }
|
|
386
|
+
];
|
|
387
|
+
var tokensCssMigration_2026_08_25_editorialTypeRole = {
|
|
388
|
+
id: "2026-08-25-editorial-type-role",
|
|
389
|
+
kind: "additive",
|
|
390
|
+
description: "Add the editorial type role (--font-editorial and the --editorial-* bundle)",
|
|
391
|
+
apply(css) {
|
|
392
|
+
const withStack = ensureScale(css, {
|
|
393
|
+
anchorPrefixes: ["--font-mono", "--font-serif", "--font-sans", "--font-display"],
|
|
394
|
+
entries: STACK
|
|
395
|
+
});
|
|
396
|
+
return ensureScale(withStack, {
|
|
397
|
+
sectionComment: "Editorial \u2014 the long-reading role, body face until repointed",
|
|
398
|
+
anchorPrefixes: [
|
|
399
|
+
"--eyebrow-",
|
|
400
|
+
"--code-",
|
|
401
|
+
"--body-",
|
|
402
|
+
"--heading-",
|
|
403
|
+
"--letter-spacing-",
|
|
404
|
+
"--line-height-",
|
|
405
|
+
"--font-weight-",
|
|
406
|
+
"--font-size-",
|
|
407
|
+
"--font-"
|
|
408
|
+
],
|
|
409
|
+
entries: BUNDLE2
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// vite-plugin/tokensCssMigrations/migrations/2026-08-26-gradient-stops.ts
|
|
415
|
+
var STOPS_RE = /^(?:linear|radial)-gradient\(\s*[^,]+,\s*(.+)\)$/i;
|
|
416
|
+
var tokensCssMigration_2026_08_26_gradientStops = {
|
|
417
|
+
id: "2026-08-26-gradient-stops",
|
|
418
|
+
kind: "additive",
|
|
419
|
+
description: "Add --gradient-N-stops companions to the gradient slots",
|
|
420
|
+
apply(css) {
|
|
421
|
+
const values = collectTokenValues(css);
|
|
422
|
+
const entries = [];
|
|
423
|
+
for (const [name, value] of values) {
|
|
424
|
+
if (!/^--gradient-\d+$/.test(name)) continue;
|
|
425
|
+
const stops = value.match(STOPS_RE);
|
|
426
|
+
if (stops) entries.push({ name: `${name}-stops`, value: stops[1].trim() });
|
|
427
|
+
}
|
|
428
|
+
if (entries.length === 0) return css;
|
|
429
|
+
return ensureScale(css, { anchorPrefixes: ["--gradient-"], entries });
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
378
433
|
// vite-plugin/tokensCssMigrations/index.ts
|
|
379
434
|
var TOKENS_CSS_MIGRATIONS = [
|
|
380
435
|
tokensCssMigration_2026_05_29_typographyScaleAdditions,
|
|
@@ -384,7 +439,9 @@ var TOKENS_CSS_MIGRATIONS = [
|
|
|
384
439
|
tokensCssMigration_2026_06_04_easingColorAndTypescaleAdditions,
|
|
385
440
|
tokensCssMigration_2026_07_20_lineHeightRename,
|
|
386
441
|
tokensCssMigration_2026_07_20_semanticTextStyles,
|
|
387
|
-
tokensCssMigration_2026_08_19_oklchColorValues
|
|
442
|
+
tokensCssMigration_2026_08_19_oklchColorValues,
|
|
443
|
+
tokensCssMigration_2026_08_25_editorialTypeRole,
|
|
444
|
+
tokensCssMigration_2026_08_26_gradientStops
|
|
388
445
|
];
|
|
389
446
|
function runTokensCssMigrations(css) {
|
|
390
447
|
return foldMigrations(css, () => true);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cssColorToOklch,
|
|
3
3
|
gamutClamp,
|
|
4
|
-
|
|
5
|
-
} from "./chunk-
|
|
4
|
+
oklchToCss
|
|
5
|
+
} from "./chunk-XWXIMTWZ.js";
|
|
6
6
|
|
|
7
7
|
// src/editor/core/storage/files/versionedFileResourceClient.ts
|
|
8
8
|
function sanitizeFileName(name) {
|
|
@@ -21,6 +21,30 @@ function formatColorOpacity(name, opacity) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// src/editor/core/themes/parsers/gradient.ts
|
|
24
|
+
var LINEAR_DIRECTIONS = [
|
|
25
|
+
"to top",
|
|
26
|
+
"to top right",
|
|
27
|
+
"to right",
|
|
28
|
+
"to bottom right",
|
|
29
|
+
"to bottom",
|
|
30
|
+
"to bottom left",
|
|
31
|
+
"to left",
|
|
32
|
+
"to top left"
|
|
33
|
+
];
|
|
34
|
+
var DIRECTION_ANGLES = {
|
|
35
|
+
"to top": 0,
|
|
36
|
+
"to top right": 45,
|
|
37
|
+
"to right": 90,
|
|
38
|
+
"to bottom right": 135,
|
|
39
|
+
"to bottom": 180,
|
|
40
|
+
"to bottom left": 225,
|
|
41
|
+
"to left": 270,
|
|
42
|
+
"to top left": 315
|
|
43
|
+
};
|
|
44
|
+
function parseDirection(raw) {
|
|
45
|
+
const norm = raw.trim().toLowerCase().replace(/\s+/g, " ");
|
|
46
|
+
return LINEAR_DIRECTIONS.includes(norm) ? norm : null;
|
|
47
|
+
}
|
|
24
48
|
var RADIAL_BASE_PX = 100;
|
|
25
49
|
function formatStopColor(s) {
|
|
26
50
|
const opacity = s.opacity ?? 100;
|
|
@@ -48,11 +72,15 @@ function formatGradientValue(v) {
|
|
|
48
72
|
return first ? formatStopColor(first) : "transparent";
|
|
49
73
|
}
|
|
50
74
|
const stops = formatGradientStops(v.stops);
|
|
51
|
-
if (v.type === "linear")
|
|
75
|
+
if (v.type === "linear") {
|
|
76
|
+
return `linear-gradient(${v.direction ?? `${v.angle}deg`}, ${stops})`;
|
|
77
|
+
}
|
|
52
78
|
return `radial-gradient(${formatRadialShape(v)} at ${v.centerX ?? 50}% 50%, ${stops})`;
|
|
53
79
|
}
|
|
54
80
|
var NUM = String.raw`-?\d+(?:\.\d+)?`;
|
|
55
|
-
var
|
|
81
|
+
var HEADING = String.raw`${NUM}deg|to\s+\w+(?:\s+\w+)?`;
|
|
82
|
+
var LINEAR_RE = new RegExp(String.raw`^linear-gradient\(\s*(${HEADING})\s*,\s*(.+)\)$`, "i");
|
|
83
|
+
var DEG_RE = new RegExp(String.raw`^(${NUM})deg$`, "i");
|
|
56
84
|
var RADIAL_RE = new RegExp(
|
|
57
85
|
String.raw`^radial-gradient\(\s*(circle|circle\s+${NUM}px|ellipse\s+${NUM}px\s+${NUM}px)\s+at\s+(${NUM})%\s+50%\s*,\s*(.+)\)$`,
|
|
58
86
|
"i"
|
|
@@ -111,8 +139,12 @@ function parseGradientValue(value) {
|
|
|
111
139
|
const css = value.trim();
|
|
112
140
|
const linear = css.match(LINEAR_RE);
|
|
113
141
|
if (linear) {
|
|
142
|
+
const deg = linear[1].match(DEG_RE);
|
|
143
|
+
const direction = deg ? null : parseDirection(linear[1]);
|
|
144
|
+
if (!deg && !direction) return null;
|
|
114
145
|
const stops2 = parseStops(linear[2]);
|
|
115
|
-
|
|
146
|
+
if (!stops2) return null;
|
|
147
|
+
return deg ? { type: "linear", angle: parseFloat(deg[1]), stops: stops2 } : { type: "linear", angle: 0, direction, stops: stops2 };
|
|
116
148
|
}
|
|
117
149
|
const radial = css.match(RADIAL_RE);
|
|
118
150
|
if (!radial) return null;
|
|
@@ -307,7 +339,7 @@ function scaleStepToX(step, scale) {
|
|
|
307
339
|
return scale.steps.length > 1 ? idx / (scale.steps.length - 1) * 100 : 50;
|
|
308
340
|
}
|
|
309
341
|
function serializeDerivedValue(value) {
|
|
310
|
-
return value.kind === "raw" ? value.css :
|
|
342
|
+
return value.kind === "raw" ? value.css : oklchToCss(value.l, value.c, value.h);
|
|
311
343
|
}
|
|
312
344
|
function computePaletteOklch(index, base, lightnessCurve, saturationCurve, curveOffset, hueCurve) {
|
|
313
345
|
const { c: baseC } = base;
|
|
@@ -587,6 +619,7 @@ function reconcilePalettesFromCssVars(palettes, cssVars) {
|
|
|
587
619
|
|
|
588
620
|
export {
|
|
589
621
|
parseColorOpacity,
|
|
622
|
+
DIRECTION_ANGLES,
|
|
590
623
|
formatGradientValue,
|
|
591
624
|
parseGradientValue,
|
|
592
625
|
sanitizeFileName,
|