@motion-proto/live-tokens 0.67.1 → 0.68.1
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/CHANGELOG.md +32 -0
- package/bin/generate-theme.mjs +5 -4
- package/dist-plugin/adjust/index.d.cts +1 -1
- package/dist-plugin/adjust/index.d.ts +1 -1
- package/dist-plugin/{chunk-J2JT4UEA.js → chunk-2UX6EVVA.js} +25 -25
- package/dist-plugin/{dataPaths-DZUzVv8H.d.cts → dataPaths-bJTCEO4H.d.cts} +1 -1
- package/dist-plugin/{dataPaths-DZUzVv8H.d.ts → dataPaths-bJTCEO4H.d.ts} +1 -1
- package/dist-plugin/fontPairing/index.d.cts +1 -1
- package/dist-plugin/fontPairing/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/index.cjs +27 -27
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +5 -5
- package/dist-plugin/migrateData/index.cjs +25 -25
- 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/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/editor/bootstrap.ts +6 -6
- package/src/editor/component-editor/ImageLightboxEditor.svelte +1 -1
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +4 -4
- package/src/editor/core/fonts/fontLoader.ts +2 -2
- package/src/editor/core/preview/{lookPreview.ts → themePreview.ts} +27 -27
- package/src/editor/core/productionPulse.ts +3 -3
- package/src/editor/core/sketch/index.ts +52 -44
- package/src/editor/core/sketch/maskField.ts +9 -9
- package/src/editor/core/sketch/sketchLayer.ts +9 -9
- package/src/editor/core/sketch/sketchRegistry.ts +34 -34
- package/src/editor/core/sketch/sketchStore.ts +98 -83
- package/src/editor/core/sketch/sketchStyleService.ts +4 -4
- package/src/editor/core/sketch/sketchStyles.ts +28 -28
- package/src/editor/core/themes/colorsAndTypeService.ts +1 -1
- package/src/editor/core/themes/loadRows.ts +8 -8
- package/src/editor/core/themes/themeDocumentSync.ts +2 -2
- package/src/editor/core/themes/themeInit.ts +2 -2
- package/src/editor/core/themes/themeService.ts +17 -17
- package/src/editor/core/themes/{lookSummary.ts → themeSummary.ts} +9 -9
- package/src/editor/core/themes/themeTypes.ts +10 -6
- package/src/editor/docs/content/getting-started.md +1 -1
- package/src/editor/docs/content/sketch-mode.md +22 -17
- package/src/editor/docs/content/themes-workflow.md +11 -11
- package/src/editor/docs/content/where-themes-live.md +6 -6
- package/src/editor/docs/content.generated.ts +4 -4
- package/src/editor/index.ts +2 -2
- package/src/editor/ui/ThemePanel.svelte +52 -52
- package/src/editor/ui/sketch/SketchPreview.svelte +2 -2
- package/src/editor/ui/sketch/SketchTab.svelte +64 -38
- package/src/live-tokens/data/sketch-styles/dry.json +5 -5
- package/src/live-tokens/data/sketch-styles/hatched.json +9 -9
- package/src/live-tokens/data/themes/autumn.json +1 -1
- package/src/live-tokens/data/themes/halloween.json +1 -1
- package/src/live-tokens/data/themes/midnight-study.json +1 -1
- package/src/live-tokens/data/themes/ocean.json +1 -1
- package/src/live-tokens/data/themes/royal-velvet.json +1 -1
- package/src/live-tokens/data/themes/sketchy.json +1 -1
- package/src/live-tokens/data/themes/spring-meadow.json +1 -1
- package/src/live-tokens/data/themes/sunset.json +1 -1
- package/src/system/backdrop/backdrop.ts +1 -1
- package/src/system/components/SectionDivider.svelte +1 -1
|
@@ -6,7 +6,7 @@ import dashed from '../../../live-tokens/data/sketch-styles/dashed.json';
|
|
|
6
6
|
import napkin from '../../../live-tokens/data/sketch-styles/napkin.json';
|
|
7
7
|
import dry from '../../../live-tokens/data/sketch-styles/dry.json';
|
|
8
8
|
|
|
9
|
-
export interface
|
|
9
|
+
export interface SketchStyleSettings {
|
|
10
10
|
label: string;
|
|
11
11
|
blurb: string;
|
|
12
12
|
/** How far the fill's edge travels at its furthest, in px. Every dial that
|
|
@@ -80,7 +80,7 @@ export interface SketchStyle {
|
|
|
80
80
|
/** Whether the two move together. Unlinked they part, and the field comes out
|
|
81
81
|
stretched: blobs wider than they are tall read as a wash dragged sideways,
|
|
82
82
|
the way ink pulled across a page does. Stored rather than inferred from
|
|
83
|
-
the pair matching, so a
|
|
83
|
+
the pair matching, so a sketchstyle that stretches to exactly square keeps its
|
|
84
84
|
dials apart. */
|
|
85
85
|
maskBlobLinked: boolean;
|
|
86
86
|
/** Output levels on the coverage field, 0 to 1: the palest the fill gets and
|
|
@@ -144,40 +144,40 @@ export interface SketchStyle {
|
|
|
144
144
|
/**
|
|
145
145
|
* The shipped sketchstyles, read from the files the package distributes. The
|
|
146
146
|
* files are the source: a project shadows one by saving a sketchstyle under the
|
|
147
|
-
* same id, the editor restores a shipped
|
|
147
|
+
* same id, the editor restores a shipped sketchstyle by deleting that file, and
|
|
148
148
|
* `themeFileApi` serves these as the read-only fallback behind the project's own
|
|
149
|
-
* directory. Editing a
|
|
149
|
+
* directory. Editing a sketchstyle here means editing its JSON, which is what the
|
|
150
150
|
* Sketchstyle view already writes.
|
|
151
151
|
*
|
|
152
152
|
* Each file carries every dial, so there is nothing to merge a default into.
|
|
153
153
|
* `sketchStyles.test.ts` pins that: the seven key sets have to match, and a dial
|
|
154
|
-
* added to `
|
|
154
|
+
* added to `SketchStyleSettings` has to reach all seven before the suite goes green.
|
|
155
155
|
*
|
|
156
156
|
* Order is picker order.
|
|
157
157
|
*/
|
|
158
158
|
const SHIPPED_FILES = { pencil, marker, whiteboard, hatched, dashed, napkin, dry };
|
|
159
159
|
|
|
160
|
-
export const
|
|
161
|
-
Object.entries(SHIPPED_FILES).map(([id, file]) => [id, file.settings as unknown as
|
|
160
|
+
export const SHIPPED_SKETCH_SETTINGS: Record<string, SketchStyleSettings> = Object.fromEntries(
|
|
161
|
+
Object.entries(SHIPPED_FILES).map(([id, file]) => [id, file.settings as unknown as SketchStyleSettings]),
|
|
162
162
|
);
|
|
163
163
|
|
|
164
164
|
export const DEFAULT_SKETCH_STYLE = 'marker';
|
|
165
165
|
|
|
166
|
-
/** The id
|
|
166
|
+
/** The id for sketch settings a theme carries, in the same id namespace as the shipped
|
|
167
167
|
sketchstyles so one picker row and one `setSketch` call cover both. Never a
|
|
168
|
-
key of `
|
|
169
|
-
own
|
|
168
|
+
key of `SHIPPED_SKETCH_SETTINGS`: a shipped style claiming it would shadow the theme's
|
|
169
|
+
own sketchstyle in every picker. `index.test.ts` pins that. */
|
|
170
170
|
export const THEME_SKETCH_ID = 'theme';
|
|
171
171
|
|
|
172
172
|
/** Reconciled against a full sketchstyle in both directions: a value stored before a
|
|
173
173
|
control existed picks up the default, and a value stored for a control since
|
|
174
174
|
retired is dropped. Without the drop, a stale key survives every spread and
|
|
175
175
|
makes the settings compare unequal to any baseline forever. */
|
|
176
|
-
export function
|
|
177
|
-
const base =
|
|
178
|
-
const stored = (raw ?? {}) as Partial<
|
|
176
|
+
export function hydrateSketchSettings(raw: unknown): SketchStyleSettings {
|
|
177
|
+
const base = SHIPPED_SKETCH_SETTINGS[DEFAULT_SKETCH_STYLE];
|
|
178
|
+
const stored = (raw ?? {}) as Partial<SketchStyleSettings>;
|
|
179
179
|
const out = { ...base };
|
|
180
|
-
for (const key of Object.keys(base) as (keyof
|
|
180
|
+
for (const key of Object.keys(base) as (keyof SketchStyleSettings)[]) {
|
|
181
181
|
if (stored[key] !== undefined) (out[key] as unknown) = stored[key];
|
|
182
182
|
}
|
|
183
183
|
// A retired option: the fill's presence belongs to the theme, not the effect.
|
|
@@ -194,9 +194,9 @@ export function hydrateSketchStyle(raw: unknown): SketchStyle {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/** The second pass used to sit at a distance derived from the stroke width,
|
|
197
|
-
with no dial of its own. A
|
|
197
|
+
with no dial of its own. A sketchstyle stored before the dial comes back at that
|
|
198
198
|
distance rather than at whatever the fallback sketchstyle happens to carry. */
|
|
199
|
-
function restoreDerivedRetrace(stored: Record<string, unknown>, out:
|
|
199
|
+
function restoreDerivedRetrace(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
200
200
|
if (stored.retraceOffset === undefined) {
|
|
201
201
|
out.retraceOffset = Number(Math.max(1.2, out.strokeWidth * 0.55).toFixed(2));
|
|
202
202
|
}
|
|
@@ -204,7 +204,7 @@ function restoreDerivedRetrace(stored: Record<string, unknown>, out: SketchStyle
|
|
|
204
204
|
|
|
205
205
|
/** The four displacement dials used to be stated as the map's own `scale`,
|
|
206
206
|
which is the full swing: the number on the dial was twice the furthest
|
|
207
|
-
anything actually moved. They are peak travel in px now, so a
|
|
207
|
+
anything actually moved. They are peak travel in px now, so a sketchstyle stored
|
|
208
208
|
under the old names comes back halved and renders identically. */
|
|
209
209
|
const SWING_DIALS = {
|
|
210
210
|
fillScale: 'fillTravel',
|
|
@@ -216,11 +216,11 @@ const SWING_DIALS = {
|
|
|
216
216
|
/** The pen wobble used to be stated as `frequency`, in cycles per px, which is
|
|
217
217
|
the number the filter wants and not one anybody can picture. It is a
|
|
218
218
|
wavelength in px now, the way the mask states its blobs. */
|
|
219
|
-
function convertCyclesToWavelength(stored: Record<string, unknown>, out:
|
|
219
|
+
function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
220
220
|
const cycles = stored.frequency;
|
|
221
221
|
if (typeof cycles === 'number' && cycles > 0) out.wobble = Math.round(1 / cycles);
|
|
222
222
|
// The layer count was `octaves`, and its dial ran to 5. The top two moved
|
|
223
|
-
// nothing, so a
|
|
223
|
+
// nothing, so a sketchstyle stored there comes back at the roughest that reads.
|
|
224
224
|
// Both were multipliers on the frequency, so they ran the other way.
|
|
225
225
|
for (const [legacy, key] of [
|
|
226
226
|
['borderFrequency', 'borderWavelength'], ['iconFrequency', 'iconWavelength'],
|
|
@@ -234,9 +234,9 @@ function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchS
|
|
|
234
234
|
if (typeof layers === 'number') out.roughness = Math.min(3, Math.max(1, layers));
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
/** The blob size was one number for both axes. A
|
|
238
|
-
comes back square, with the two dials linked, which is the
|
|
239
|
-
function splitBlobAxes(stored: Record<string, unknown>, out:
|
|
237
|
+
/** The blob size was one number for both axes. A sketchstyle stored before the split
|
|
238
|
+
comes back square, with the two dials linked, which is the shape it had. */
|
|
239
|
+
function splitBlobAxes(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
240
240
|
const blob = stored.maskBlob;
|
|
241
241
|
if (typeof blob !== 'number') return;
|
|
242
242
|
out.maskBlobX = blob;
|
|
@@ -249,13 +249,13 @@ function splitBlobAxes(stored: Record<string, unknown>, out: SketchStyle): void
|
|
|
249
249
|
of the field, so it came out either untouched or gone. It is a share of the
|
|
250
250
|
glyph now, and the old default reads as one period across the glyph, which
|
|
251
251
|
is what that size was aiming at. */
|
|
252
|
-
function convertIconTileToScale(stored: Record<string, unknown>, out:
|
|
252
|
+
function convertIconTileToScale(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
253
253
|
const tile = stored.iconMaskTile;
|
|
254
254
|
if (typeof tile !== 'number') return;
|
|
255
255
|
out.iconMaskScale = Math.min(5, Math.max(0.5, Number((tile / 90).toFixed(2))));
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
function halveSwingDials(stored: Record<string, unknown>, out:
|
|
258
|
+
function halveSwingDials(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
259
259
|
for (const [legacy, key] of Object.entries(SWING_DIALS)) {
|
|
260
260
|
const value = stored[legacy];
|
|
261
261
|
if (typeof value === 'number') out[key as 'fillTravel'] = value / 2;
|
|
@@ -265,7 +265,7 @@ function halveSwingDials(stored: Record<string, unknown>, out: SketchStyle): voi
|
|
|
265
265
|
/** The mask used to be a 600-unit tile painted at `maskScale` px, with the
|
|
266
266
|
coverage point buried in the hardness slope. Recover page-px blobs and
|
|
267
267
|
softness, and the levels the old slope and floor put the edge at. */
|
|
268
|
-
function convertTiledMask(stored: Record<string, unknown>, out:
|
|
268
|
+
function convertTiledMask(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
269
269
|
const scale = stored.maskScale;
|
|
270
270
|
if (typeof scale !== 'number') return;
|
|
271
271
|
const freq = stored.maskFrequency;
|
|
@@ -283,7 +283,7 @@ function convertTiledMask(stored: Record<string, unknown>, out: SketchStyle): vo
|
|
|
283
283
|
* levels the pair put the edge between, and rescale them onto the field as it
|
|
284
284
|
* is now: stretched onto its full range before the levels see it.
|
|
285
285
|
*/
|
|
286
|
-
function convertCutToLevels(stored: Record<string, unknown>, out:
|
|
286
|
+
function convertCutToLevels(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
287
287
|
const contrast = stored.maskContrast;
|
|
288
288
|
const coverage = stored.maskCoverage;
|
|
289
289
|
if (typeof contrast !== 'number' || typeof coverage !== 'number') return;
|
|
@@ -297,9 +297,9 @@ function convertCutToLevels(stored: Record<string, unknown>, out: SketchStyle):
|
|
|
297
297
|
|
|
298
298
|
/** The pair were input levels, a cut through the field, and are output levels
|
|
299
299
|
now, the palest and densest the fill gets. The same two numbers carry over:
|
|
300
|
-
a
|
|
300
|
+
a sketchstyle cut between 20% and 50% comes back as a wash between 20% and 50% ink,
|
|
301
301
|
which keeps its spread and loses only the hole. */
|
|
302
|
-
function carryLevelsToOutput(stored: Record<string, unknown>, out:
|
|
302
|
+
function carryLevelsToOutput(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
303
303
|
if (typeof stored.maskLevelMin === 'number') out.maskOutputMin = stored.maskLevelMin;
|
|
304
304
|
if (typeof stored.maskLevelMax === 'number') out.maskOutputMax = stored.maskLevelMax;
|
|
305
305
|
}
|
|
@@ -65,7 +65,7 @@ export const sanitizeFileName = sanitizeFileNameImpl;
|
|
|
65
65
|
|
|
66
66
|
/** Flush the editor state to the buffer under `displayName`, the name of the
|
|
67
67
|
* theme the buffer belongs to, and clear the dirty flag. A capture reads the
|
|
68
|
-
* buffer back, so this is what makes Save and Adopt mean the
|
|
68
|
+
* buffer back, so this is what makes Save and Adopt mean the theme on screen. */
|
|
69
69
|
export async function persistColorsAndType(
|
|
70
70
|
state: EditorState,
|
|
71
71
|
displayName: string,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ThemeMeta, ColorsAndTypeMeta } from './themeTypes';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The one Load list. A theme is the whole
|
|
4
|
+
* The one Load list. A theme is the whole theme; a colors and type file is a
|
|
5
5
|
* preset holding that half of one. Both belong in the same window, told apart
|
|
6
6
|
* by a badge rather than by living in separate managers.
|
|
7
7
|
*
|
|
@@ -10,7 +10,7 @@ import type { ThemeMeta, ColorsAndTypeMeta } from './themeTypes';
|
|
|
10
10
|
* that copy is exactly the file the list must keep reachable.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export type LoadRowKind = '
|
|
13
|
+
export type LoadRowKind = 'theme' | 'layer';
|
|
14
14
|
|
|
15
15
|
export interface LoadRow {
|
|
16
16
|
/** `<kind>:<slug>`. The two kinds are separate resources with separate name
|
|
@@ -25,12 +25,12 @@ export interface LoadRow {
|
|
|
25
25
|
|
|
26
26
|
export const loadRowId = (kind: LoadRowKind, slug: string): string => `${kind}:${slug}`;
|
|
27
27
|
|
|
28
|
-
export function buildLoadRows(
|
|
29
|
-
const
|
|
28
|
+
export function buildLoadRows(themes: ThemeMeta[], layers: ColorsAndTypeMeta[]): LoadRow[] {
|
|
29
|
+
const themeRows: LoadRow[] = themes
|
|
30
30
|
.map((f): LoadRow => ({
|
|
31
|
-
fileName: loadRowId('
|
|
31
|
+
fileName: loadRowId('theme', f.fileName),
|
|
32
32
|
slug: f.fileName,
|
|
33
|
-
kind: '
|
|
33
|
+
kind: 'theme',
|
|
34
34
|
name: f.name,
|
|
35
35
|
updatedAt: f.updatedAt,
|
|
36
36
|
isProtected: f.isProtected,
|
|
@@ -46,12 +46,12 @@ export function buildLoadRows(looks: ThemeMeta[], layers: ColorsAndTypeMeta[]):
|
|
|
46
46
|
updatedAt: f.updatedAt,
|
|
47
47
|
isProtected: false,
|
|
48
48
|
}));
|
|
49
|
-
return [...
|
|
49
|
+
return [...themeRows, ...layerRows];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Whether picking this row loads colors and type alone. A layer file holds
|
|
54
|
-
* nothing else, so it ignores the toggle; a
|
|
54
|
+
* nothing else, so it ignores the toggle; a theme honors it.
|
|
55
55
|
*/
|
|
56
56
|
export function isColorsOnly(row: LoadRow | null, colorsOnly: boolean): boolean {
|
|
57
57
|
if (row?.kind === 'layer') return true;
|
|
@@ -2,7 +2,7 @@ import { liveMovedSinceBake } from '../productionPulse';
|
|
|
2
2
|
import { openThemeSlug } from '../store/editorConfigStore';
|
|
3
3
|
import { loadThemeFromApi } from '../store/editorStore';
|
|
4
4
|
import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
|
|
5
|
-
import {
|
|
5
|
+
import { openThemeSketchSettings } from '../sketch/sketchStore';
|
|
6
6
|
import type { ApplyThemeResult } from './themeService';
|
|
7
7
|
|
|
8
8
|
const CHANNEL_NAME = 'live-tokens:active-theme:v1';
|
|
@@ -43,7 +43,7 @@ export function hydrateAppliedTheme(fileName: string, result: ApplyThemeResult):
|
|
|
43
43
|
migrateColorsAndTypeFonts(colorsAndType);
|
|
44
44
|
loadThemeFromApi(colorsAndType, structuredClone(result.componentConfigs));
|
|
45
45
|
openThemeSlug.set(fileName);
|
|
46
|
-
|
|
46
|
+
openThemeSketchSettings(result.theme.sketchSettings);
|
|
47
47
|
liveMovedSinceBake.set(false);
|
|
48
48
|
if (typeof document !== 'undefined') {
|
|
49
49
|
document.dispatchEvent(new CustomEvent<AppliedThemeDetail>(THEME_APPLIED_EVENT, {
|
|
@@ -65,9 +65,9 @@ export async function initializeTheme(): Promise<void> {
|
|
|
65
65
|
|
|
66
66
|
const active = await safeFetch<Theme>(`${API_BASE}/themes/active`);
|
|
67
67
|
// A failed fetch is not "the theme carries no sketchstyle": treating null as
|
|
68
|
-
// absent would tell the panel the
|
|
68
|
+
// absent would tell the panel the sketchstyle is off the theme, or hand a fresh
|
|
69
69
|
// browser a blank buffer, over a fetch that will likely succeed next time.
|
|
70
70
|
// The same call a built site makes (`@motion-proto/live-tokens/sketch`), so
|
|
71
71
|
// one rule decides what a theme's sketchstyle means at boot in both.
|
|
72
|
-
if (active) seedSketchFromTheme(active.
|
|
72
|
+
if (active) seedSketchFromTheme(active.sketchSettings);
|
|
73
73
|
}
|
|
@@ -7,15 +7,15 @@ import { getActiveColorsAndType } from './colorsAndTypeService';
|
|
|
7
7
|
import { broadcastAppliedTheme, hydrateAppliedTheme } from './themeDocumentSync';
|
|
8
8
|
import { CURRENT_COMPONENT_SCHEMA_VERSION } from './migrations';
|
|
9
9
|
import { THEME_SCHEMA_VERSION } from './themeTypes';
|
|
10
|
-
import {
|
|
10
|
+
import { liveSketchSettings, themeSketchSettings } from '../sketch/sketchStore';
|
|
11
11
|
|
|
12
12
|
export type { ThemeFillReport };
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* REST client for theme files, the documents of the editor. A theme carries a
|
|
16
|
-
* whole
|
|
16
|
+
* whole content by value: the colors and type plus a config for every component
|
|
17
17
|
* this install has. One theme is open (`themes/_active.json`) and one is
|
|
18
|
-
* published (`themes/_production.json`); the live
|
|
18
|
+
* published (`themes/_production.json`); the live content is the open theme plus
|
|
19
19
|
* whatever the `_working` buffers hold over it.
|
|
20
20
|
*
|
|
21
21
|
* `default` is the protected baseline — cannot be overwritten or deleted, and
|
|
@@ -75,7 +75,7 @@ export interface ApplyThemeResult {
|
|
|
75
75
|
* Open a theme: the server clears the `_working` buffers, points
|
|
76
76
|
* `themes/_active.json` at it, and returns the resolved state in one payload.
|
|
77
77
|
* Live reads then fall through to the theme's embedded layers.
|
|
78
|
-
* Production is untouched, so trying a
|
|
78
|
+
* Production is untouched, so trying a content cannot change what the site ships.
|
|
79
79
|
* The resolved response hydrates the caller immediately, then broadcasts the
|
|
80
80
|
* same typed state to every already-open same-origin host/editor document.
|
|
81
81
|
* Each store renderer projects that state into its own document, so a theme
|
|
@@ -96,7 +96,7 @@ export async function applyTheme(fileName: string): Promise<ApplyThemeResult> {
|
|
|
96
96
|
return result;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
export interface
|
|
99
|
+
export interface AdoptThemeResult {
|
|
100
100
|
ok: boolean;
|
|
101
101
|
/** The theme production now ships. */
|
|
102
102
|
productionTheme: { fileName: string; name: string; updatedAt: string };
|
|
@@ -111,7 +111,7 @@ export interface AdoptLookResult {
|
|
|
111
111
|
* Answers 409 `ACTIVE_IS_PROTECTED` while the Default theme is open, which the
|
|
112
112
|
* caller recovers from by forking it under a name of the user's own.
|
|
113
113
|
*/
|
|
114
|
-
export async function
|
|
114
|
+
export async function adoptTheme(): Promise<AdoptThemeResult> {
|
|
115
115
|
const res = await fetch(`${API_BASE}/production`, { method: 'PUT' });
|
|
116
116
|
if (!res.ok) {
|
|
117
117
|
const body = await res.json().catch(() => ({}));
|
|
@@ -135,7 +135,7 @@ function withoutLiveMarkers<T extends { _fileName?: string; _source?: unknown }>
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
* The
|
|
138
|
+
* The content as it stands: the live colors and type plus the live config of
|
|
139
139
|
* every component this install has, all by value. A theme is a complete
|
|
140
140
|
* document (`docs/plans/theme-completeness.md`), so the capture carries every
|
|
141
141
|
* component, not just the ones off their default.
|
|
@@ -145,7 +145,7 @@ function withoutLiveMarkers<T extends { _fileName?: string; _source?: unknown }>
|
|
|
145
145
|
* way out of `GET /colors-and-type/active`, which matters: the server trusts an
|
|
146
146
|
* already-embedded copy and runs no migrations over it on write.
|
|
147
147
|
*/
|
|
148
|
-
async function
|
|
148
|
+
async function captureThemeContent(): Promise<Pick<Theme, 'colorsAndType' | 'componentConfigs' | 'sketchSettings'>> {
|
|
149
149
|
const liveColorsAndType = await getActiveColorsAndType();
|
|
150
150
|
if (!liveColorsAndType) {
|
|
151
151
|
throw new Error('No live colors and type to capture');
|
|
@@ -159,11 +159,11 @@ async function captureLook(): Promise<Pick<Theme, 'colorsAndType' | 'componentCo
|
|
|
159
159
|
});
|
|
160
160
|
// Sketchstyle has no server door of its own (RJC 8): the live buffer, not a
|
|
161
161
|
// fetch, is the source of truth for what the dials currently say.
|
|
162
|
-
return { colorsAndType: withoutLiveMarkers(liveColorsAndType), componentConfigs,
|
|
162
|
+
return { colorsAndType: withoutLiveMarkers(liveColorsAndType), componentConfigs, sketchSettings: liveSketchSettings() };
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
* Capture the current
|
|
166
|
+
* Capture the current content into a new theme file and open it. Used by the
|
|
167
167
|
* theme panel's Save As action and by the fork-then-Adopt flow the protected
|
|
168
168
|
* Default theme forces.
|
|
169
169
|
*/
|
|
@@ -171,7 +171,7 @@ export async function saveAsTheme(
|
|
|
171
171
|
fileName: string,
|
|
172
172
|
displayName: string,
|
|
173
173
|
): Promise<void> {
|
|
174
|
-
const
|
|
174
|
+
const content = await captureThemeContent();
|
|
175
175
|
const now = new Date().toISOString();
|
|
176
176
|
await saveTheme(fileName, {
|
|
177
177
|
name: displayName,
|
|
@@ -179,15 +179,15 @@ export async function saveAsTheme(
|
|
|
179
179
|
updatedAt: now,
|
|
180
180
|
schemaVersion: THEME_SCHEMA_VERSION,
|
|
181
181
|
componentSchemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
|
|
182
|
-
...
|
|
182
|
+
...content,
|
|
183
183
|
});
|
|
184
|
-
|
|
184
|
+
themeSketchSettings.set(content.sketchSettings);
|
|
185
185
|
liveMovedSinceBake.set(true);
|
|
186
186
|
await setActiveTheme(fileName);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
|
-
* Re-capture the current
|
|
190
|
+
* Re-capture the current content into the open theme file. Used by the theme
|
|
191
191
|
* panel's Save action and by both Adopt paths, which publish what is saved.
|
|
192
192
|
* The server rejects with 403 while the protected `default` theme is open.
|
|
193
193
|
*/
|
|
@@ -196,16 +196,16 @@ export async function saveActiveTheme(displayName?: string): Promise<void> {
|
|
|
196
196
|
if (!active || !active._fileName) {
|
|
197
197
|
throw new Error('No active theme');
|
|
198
198
|
}
|
|
199
|
-
const
|
|
199
|
+
const content = await captureThemeContent();
|
|
200
200
|
await saveTheme(active._fileName, {
|
|
201
201
|
name: displayName ?? active.name,
|
|
202
202
|
createdAt: active.createdAt,
|
|
203
203
|
updatedAt: new Date().toISOString(),
|
|
204
204
|
schemaVersion: THEME_SCHEMA_VERSION,
|
|
205
205
|
componentSchemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
|
|
206
|
-
...
|
|
206
|
+
...content,
|
|
207
207
|
});
|
|
208
|
-
|
|
208
|
+
themeSketchSettings.set(content.sketchSettings);
|
|
209
209
|
liveMovedSinceBake.set(true);
|
|
210
210
|
}
|
|
211
211
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { ComponentSummary } from '../components/componentConfigService';
|
|
2
2
|
|
|
3
|
-
export interface
|
|
3
|
+
export interface ThemeProductionInput {
|
|
4
4
|
/** Slug of the theme the editor has open. */
|
|
5
5
|
openTheme: string;
|
|
6
6
|
/** Slug of the published theme, or null while the read has not landed. */
|
|
7
7
|
productionTheme: string | null;
|
|
8
|
-
/** Live
|
|
8
|
+
/** Live theme moved past what the published theme holds: unsaved edits, or a
|
|
9
9
|
* save since the last Adopt. */
|
|
10
10
|
unpublished: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export interface
|
|
14
|
-
/** True when production is known to ship the
|
|
13
|
+
export interface ThemeProductionState {
|
|
14
|
+
/** True when production is known to ship the theme on screen. */
|
|
15
15
|
inProduction: boolean;
|
|
16
16
|
/** True while the production theme is unread: neither claim can be made. */
|
|
17
17
|
unknown: boolean;
|
|
@@ -22,9 +22,9 @@ export interface LookProductionState {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Whether production is running the
|
|
25
|
+
* Whether production is running the theme on screen. Production is one saved
|
|
26
26
|
* theme, so the first half is an identity check against the open one; the
|
|
27
|
-
* second half is the live
|
|
27
|
+
* second half is the live theme sitting ahead of what was published, which
|
|
28
28
|
* `unpublished` carries.
|
|
29
29
|
*
|
|
30
30
|
* A null production read is not an answer, so it is neither state: `unknown`
|
|
@@ -32,11 +32,11 @@ export interface LookProductionState {
|
|
|
32
32
|
* neutral state, which keeps a mount from flashing the alarm without letting a
|
|
33
33
|
* read that never lands read as shipped forever.
|
|
34
34
|
*/
|
|
35
|
-
export function
|
|
35
|
+
export function themeProductionState({
|
|
36
36
|
openTheme,
|
|
37
37
|
productionTheme,
|
|
38
38
|
unpublished,
|
|
39
|
-
}:
|
|
39
|
+
}: ThemeProductionInput): ThemeProductionState {
|
|
40
40
|
const unknown = productionTheme === null;
|
|
41
41
|
const themeOff = !unknown && productionTheme !== openTheme;
|
|
42
42
|
return {
|
|
@@ -48,6 +48,6 @@ export function lookProductionState({
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** How many components run an unsaved buffer, diverging from what the open theme carries. */
|
|
51
|
-
export function
|
|
51
|
+
export function countComponentsOffTheme(components: ComponentSummary[]): number {
|
|
52
52
|
return components.filter((c) => c.source === 'working').length;
|
|
53
53
|
}
|
|
@@ -2,13 +2,17 @@ import type { CurveAnchor } from '../../ui/curveEngine';
|
|
|
2
2
|
import type { GradientValue } from './parsers/gradient';
|
|
3
3
|
import type { Oklch } from '../palettes/oklch';
|
|
4
4
|
import type { HarmonyAxis } from '../palettes/colorHarmony';
|
|
5
|
-
import type {
|
|
5
|
+
import type { SketchStyleSettings } from '../sketch/sketchStyles';
|
|
6
6
|
/** Single source of truth for the theme schema version
|
|
7
7
|
* (docs/plans/theme-completeness.md, Wave 2 step 5). It lives here, on the
|
|
8
8
|
* shipped side: `vite-plugin/` is build tooling and is not in the tarball, so
|
|
9
9
|
* anything under `src/` that reaches into it resolves in this repo and
|
|
10
10
|
* nowhere else. `normalizeTheme.ts` re-exports this. */
|
|
11
|
-
|
|
11
|
+
/** 5 renamed the sketch field from `sketchStyle` to `sketchSettings`. The bump
|
|
12
|
+
* is what makes the boot pass rewrite a theme still on the old key: the read
|
|
13
|
+
* accepts both, but a built site reads its theme JSON raw and would see
|
|
14
|
+
* `undefined` until the file itself was converted. */
|
|
15
|
+
export const THEME_SCHEMA_VERSION = 5;
|
|
12
16
|
|
|
13
17
|
/** What the completeness fill had to add to reach a whole theme. `components`
|
|
14
18
|
* names each component the input carried no entry for; `aliases` counts keys
|
|
@@ -240,7 +244,7 @@ export interface ComponentConfigMeta {
|
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
/**
|
|
243
|
-
* A saved
|
|
247
|
+
* A saved theme, encapsulated: the colors and type plus a config for every
|
|
244
248
|
* component this install has, all carried by value. Themes are the documents
|
|
245
249
|
* of the editor. Applying one opens it by clearing the reserved `_working`
|
|
246
250
|
* buffers and updating `themes/_active.json`; live reads fall through to its
|
|
@@ -267,7 +271,7 @@ export interface Theme {
|
|
|
267
271
|
* component schema and then fills any gap from the local default, on every
|
|
268
272
|
* read (Waves 1 and 2 of `docs/plans/theme-completeness.md`); both the
|
|
269
273
|
* migration and the fill are persisted on the next write, so a theme is a
|
|
270
|
-
* whole-
|
|
274
|
+
* whole-theme document rather than a diff against a moving baseline.
|
|
271
275
|
* `component-configs/<id>/default.json` is the derivation product of that
|
|
272
276
|
* component's `:global(:root)`, not a resolution layer this map defers to
|
|
273
277
|
* — a config for a component this install does not have, or an alias key
|
|
@@ -280,10 +284,10 @@ export interface Theme {
|
|
|
280
284
|
* buy no isolation. `normalizeTheme` sets this on every read; an embedded
|
|
281
285
|
* config's own `schemaVersion` (if hand-authored) is ignored and stripped. */
|
|
282
286
|
componentSchemaVersion: number;
|
|
283
|
-
/** The sketchstyle this
|
|
287
|
+
/** The sketchstyle this theme paints, by value. Absent means the theme is
|
|
284
288
|
* crisp: presence is the on state, so there is no separate flag that can
|
|
285
289
|
* disagree with the dials beside it (RJC 1). */
|
|
286
|
-
|
|
290
|
+
sketchSettings?: SketchStyleSettings;
|
|
287
291
|
/** Server-attached file-name marker. Same role as `ColorsAndType._fileName`. */
|
|
288
292
|
_fileName?: string;
|
|
289
293
|
}
|
|
@@ -54,7 +54,7 @@ the editor swaps live.
|
|
|
54
54
|
To ship, click **Adopt** in the Theme panel. That saves the open theme and bakes
|
|
55
55
|
it into `src/live-tokens/data/tokens.generated.css`, which your build bundles
|
|
56
56
|
alongside `tokens.css`. Adopt is the only action that changes what your site
|
|
57
|
-
ships, so try any
|
|
57
|
+
ships, so try any theme you like first. The editor itself never reaches
|
|
58
58
|
production.
|
|
59
59
|
|
|
60
60
|
Already have a Svelte 5 + Vite app? The
|
|
@@ -22,7 +22,7 @@ separately wobbled boxes.
|
|
|
22
22
|
|
|
23
23
|
## The sketchstyles
|
|
24
24
|
|
|
25
|
-
Seven
|
|
25
|
+
Seven sketchstyles ship with the package, and each is a complete set of dials rather
|
|
26
26
|
than just a name:
|
|
27
27
|
|
|
28
28
|
- **Pencil.** Two graphite passes on their own seeds, so the outline disagrees
|
|
@@ -38,8 +38,8 @@ than just a name:
|
|
|
38
38
|
- **Napkin.** Ballpoint in a hurry. Everything loose at once.
|
|
39
39
|
- **Dry marker.** Ink that ran out. One scratchy pass over a mostly eaten fill.
|
|
40
40
|
|
|
41
|
-
All seven ship as files, one per
|
|
42
|
-
`src/live-tokens/data/sketch-styles/` in the package. There is no
|
|
41
|
+
All seven ship as files, one per sketchstyle, under
|
|
42
|
+
`src/live-tokens/data/sketch-styles/` in the package. There is no sketchstyle that
|
|
43
43
|
exists only as code, so every one of them can be read, copied and edited.
|
|
44
44
|
|
|
45
45
|
Pick one, then move whatever you like. **Save As** keeps your dials under a name
|
|
@@ -51,6 +51,11 @@ project's own copy under the same name, which takes its place in the list;
|
|
|
51
51
|
delete that copy and the shipped file behind it comes back. Both are a
|
|
52
52
|
different gesture from saving a theme; see "Where the settings live" below.
|
|
53
53
|
|
|
54
|
+
A theme carries its sketch settings by value, so it can hold a set that belongs
|
|
55
|
+
to no file. The list shows that set as **Unsaved**. It owns no file, so there is
|
|
56
|
+
nothing for Save to write over; **Save As** turns it into a sketchstyle like any
|
|
57
|
+
other, which you can then save over and delete.
|
|
58
|
+
|
|
54
59
|
Your sketchstyles and the shipped ones are one list. A sketchstyle named after
|
|
55
60
|
a shipped one replaces it, keeping its place in the list, so a project that
|
|
56
61
|
wants its own Pencil saves one and every picker shows that one instead.
|
|
@@ -97,9 +102,9 @@ is disabled there; use **Save As** to fold the dials into a theme of your
|
|
|
97
102
|
own.
|
|
98
103
|
|
|
99
104
|
**Save** and **Save As** in the **Sketchstyle** view are a different gesture.
|
|
100
|
-
They write a named sketchstyle to `src/live-tokens/data/sketch-styles/`, a
|
|
105
|
+
They write a named sketchstyle to `src/live-tokens/data/sketch-styles/`, a sketchstyle
|
|
101
106
|
you can pick from any theme. Neither touches the open theme, and neither marks
|
|
102
|
-
the
|
|
107
|
+
the theme as changed.
|
|
103
108
|
|
|
104
109
|
## Shipping the layer
|
|
105
110
|
|
|
@@ -110,11 +115,11 @@ has no server to ask, so it hands the field over itself:
|
|
|
110
115
|
import { seedSketchFromTheme } from '@motion-proto/live-tokens/sketch';
|
|
111
116
|
import theme from './live-tokens/data/themes/sketchy.json';
|
|
112
117
|
|
|
113
|
-
seedSketchFromTheme(theme.
|
|
118
|
+
seedSketchFromTheme(theme.sketchSettings);
|
|
114
119
|
await bootLiveTokens(App, '#app');
|
|
115
120
|
```
|
|
116
121
|
|
|
117
|
-
Call it before mounting, so the
|
|
122
|
+
Call it before mounting, so the sketch layer is up on the first frame. Pass the field
|
|
118
123
|
raw. A theme written against older dial names is carried forward on the way in,
|
|
119
124
|
the same reconciliation the dev server runs on every theme it reads.
|
|
120
125
|
|
|
@@ -122,7 +127,7 @@ Nothing is baked. `tokens.generated.css` still holds token values only, and the
|
|
|
122
127
|
layer stays JavaScript the page runs, because it builds an SVG filter bank
|
|
123
128
|
rather than a set of custom properties.
|
|
124
129
|
|
|
125
|
-
A visitor who has picked a
|
|
130
|
+
A visitor who has picked a sketchstyle of their own keeps it, None included. The theme
|
|
126
131
|
seeds a browser that has decided nothing and never overwrites one that has, so
|
|
127
132
|
calling this on every boot is safe.
|
|
128
133
|
|
|
@@ -138,26 +143,26 @@ const files = import.meta.glob<{ name?: string; settings: unknown }>(
|
|
|
138
143
|
);
|
|
139
144
|
|
|
140
145
|
await bootLiveTokens(App, '#app', {
|
|
141
|
-
|
|
146
|
+
sketchStyles: Object.entries(files).map(([path, file]) => {
|
|
142
147
|
const id = path.split('/').pop()!.replace('.json', '');
|
|
143
148
|
return { id, label: file.name || id, settings: file.settings };
|
|
144
149
|
}),
|
|
145
150
|
});
|
|
146
151
|
```
|
|
147
152
|
|
|
148
|
-
Projects made with `create` ship this already. The file's slug is the
|
|
153
|
+
Projects made with `create` ship this already. The file's slug is the sketchstyle's
|
|
149
154
|
id, so a sketchstyle picked in the editor keeps working once the site is built.
|
|
150
155
|
|
|
151
156
|
### Building a picker
|
|
152
157
|
|
|
153
|
-
`
|
|
154
|
-
each row `setSketch(
|
|
155
|
-
the effect rather than one of the
|
|
158
|
+
`sketchStyles` is every sketchstyle on offer, shipped and your own, as a store. Give
|
|
159
|
+
each row `setSketch(style.id)`, and add your own **None** row: off is a state of
|
|
160
|
+
the effect rather than one of the sketchstyles.
|
|
156
161
|
|
|
157
|
-
`
|
|
158
|
-
theme carries none, and null when what it carries
|
|
159
|
-
`
|
|
160
|
-
other, so a visitor who wanders off the theme's
|
|
162
|
+
`unsavedSketchStyle` is the settings the theme carries as one more row. It is null when the
|
|
163
|
+
theme carries none, and null when what it carries matches a sketchstyle already in
|
|
164
|
+
`sketchStyles`, since that row names it. Its id goes to `setSketch` like any
|
|
165
|
+
other, so a visitor who wanders off the theme's own settings can come back to it.
|
|
161
166
|
|
|
162
167
|
## Drawing your own elements
|
|
163
168
|
|