@motion-proto/live-tokens 0.56.1 → 0.58.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-create-component/SKILL.md +13 -2
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +202 -0
- package/.claude/skills/live-tokens-pair-fonts/SKILL.md +2 -2
- package/.claude/skills/live-tokens-pick-component/SKILL.md +1 -1
- package/CHANGELOG.md +192 -0
- package/README.md +1 -1
- package/bin/cli.mjs +2 -1
- package/dist-plugin/adjust/index.cjs +1 -0
- package/dist-plugin/adjust/index.d.cts +2 -2
- package/dist-plugin/adjust/index.d.ts +2 -2
- 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 +4 -2
- package/dist-plugin/fontPairing/index.d.cts +3 -3
- package/dist-plugin/fontPairing/index.d.ts +3 -3
- package/dist-plugin/fontPairing/index.js +4 -3
- package/dist-plugin/generateColorsAndType/index.cjs +22 -9
- package/dist-plugin/generateColorsAndType/index.d.cts +2 -2
- package/dist-plugin/generateColorsAndType/index.d.ts +2 -2
- package/dist-plugin/generateColorsAndType/index.js +8 -5
- 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/{themeTypes-BAqtv4XO.d.cts → themeTypes-DSV3Zisf.d.cts} +12 -1
- package/dist-plugin/{themeTypes-BAqtv4XO.d.ts → themeTypes-DSV3Zisf.d.ts} +12 -1
- 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 +3 -1
- package/src/editor/core/fonts/applyFontPairing.ts +3 -2
- package/src/editor/core/fonts/fontLoader.ts +1 -0
- package/src/editor/core/fonts/fontMigration.ts +31 -1
- package/src/editor/core/palettes/paletteDerivation.ts +9 -2
- package/src/editor/core/sketch/maskField.ts +385 -0
- package/src/editor/core/sketch/sketchLayer.ts +1106 -0
- package/src/editor/core/sketch/sketchPresetService.ts +65 -0
- package/src/editor/core/sketch/sketchPresets.ts +345 -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 +183 -0
- package/src/editor/docs/content.generated.ts +3 -2
- package/src/editor/overlay/LiveEditorOverlay.svelte +15 -0
- package/src/editor/pages/EditorShell.svelte +43 -0
- package/src/editor/ui/EditorViewSwitcher.svelte +30 -17
- package/src/editor/ui/FontStackEditor.svelte +3 -0
- package/src/editor/ui/GradientEditor.svelte +38 -1
- 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 +1264 -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/sketchy.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/sketchy.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/components/Card.svelte +12 -1
- package/src/system/components/SectionDivider.svelte +4 -0
- package/src/system/styles/tokens.css +15 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PaletteConfig, FontSource, FontStack } from '../themes/themeTypes';
|
|
2
|
-
import type { GradientStop, GradientValue } from '../themes/parsers/gradient';
|
|
2
|
+
import type { GradientStop, GradientValue, LinearDirection } from '../themes/parsers/gradient';
|
|
3
3
|
import type { HarmonyAxis } from '../palettes/colorHarmony';
|
|
4
4
|
|
|
5
5
|
export interface ShadowGlobals {
|
|
@@ -57,6 +57,9 @@ export interface GradientToken {
|
|
|
57
57
|
type: GradientType;
|
|
58
58
|
/** Degrees, applies to linear only. */
|
|
59
59
|
angle: number;
|
|
60
|
+
/** `to <side-or-corner>`, emitted instead of `angle` on a linear gradient.
|
|
61
|
+
* Tracks the box's aspect the way a fixed angle cannot. */
|
|
62
|
+
direction?: LinearDirection;
|
|
60
63
|
/** Pixel radius for radial gradients. When absent or zero, the renderer
|
|
61
64
|
* emits CSS's default ellipse/farthest-corner shape. */
|
|
62
65
|
radius?: number;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { writable } from 'svelte/store';
|
|
2
2
|
|
|
3
|
-
export type EditorView = 'tokens' | 'components' | 'colors';
|
|
3
|
+
export type EditorView = 'tokens' | 'components' | 'colors' | 'sketch';
|
|
4
4
|
export type SidebarCondensed = boolean | 'auto';
|
|
5
5
|
|
|
6
6
|
const VIEW_KEY = 'lt.editorView';
|
|
7
7
|
const CONDENSED_KEY = 'lt.sidebarCondensed';
|
|
8
8
|
|
|
9
9
|
function isEditorView(v: unknown): v is EditorView {
|
|
10
|
-
return v === 'tokens' || v === 'components' || v === 'colors';
|
|
10
|
+
return v === 'tokens' || v === 'components' || v === 'colors' || v === 'sketch';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// Session-scoped, not persistent: opening the editor fresh always starts on
|
|
@@ -24,13 +24,17 @@ import {
|
|
|
24
24
|
setGradientStop,
|
|
25
25
|
addGradientStop,
|
|
26
26
|
removeGradientStop,
|
|
27
|
+
setGradientDirection,
|
|
27
28
|
} from '../themes/slices/gradients';
|
|
29
|
+
import type { LinearDirection } from '../themes/parsers/gradient';
|
|
28
30
|
import { setComponentAlias } from '../themes/slices/components';
|
|
29
31
|
import { mutate } from './editorCore';
|
|
30
32
|
|
|
31
33
|
export interface GradientSourceSnapshot {
|
|
32
34
|
type: GradientType;
|
|
33
35
|
angle: number;
|
|
36
|
+
/** `to <side-or-corner>` heading; overrides `angle` when set. */
|
|
37
|
+
direction?: LinearDirection;
|
|
34
38
|
/** Horizontal center for radial gradients, 0–100. */
|
|
35
39
|
centerX?: number;
|
|
36
40
|
/** Per-axis stretch factors for the radial ellipse (1 = unscaled). */
|
|
@@ -48,6 +52,7 @@ export interface GradientSource {
|
|
|
48
52
|
setAll(next: GradientSourceSnapshot): void;
|
|
49
53
|
setType(type: GradientType): void;
|
|
50
54
|
setAngle(angle: number): void;
|
|
55
|
+
setDirection(direction: LinearDirection | undefined): void;
|
|
51
56
|
setCenterX(centerX: number): void;
|
|
52
57
|
setAspect(aspect: { x: number; y: number }): void;
|
|
53
58
|
setStop(index: number, partial: Partial<GradientTokenStop>): void;
|
|
@@ -63,6 +68,7 @@ export function colorsAndTypeGradientSource(variable: string): GradientSource {
|
|
|
63
68
|
return {
|
|
64
69
|
type: t.type,
|
|
65
70
|
angle: t.angle,
|
|
71
|
+
direction: t.direction,
|
|
66
72
|
centerX: t.centerX,
|
|
67
73
|
aspectX: t.aspectX,
|
|
68
74
|
aspectY: t.aspectY,
|
|
@@ -78,6 +84,7 @@ export function colorsAndTypeGradientSource(variable: string): GradientSource {
|
|
|
78
84
|
setCenterX: (x) => setGradientCenterX(variable, x),
|
|
79
85
|
setAspect: (a) => setGradientAspect(variable, a),
|
|
80
86
|
setStop: (i, p) => setGradientStop(variable, i, p),
|
|
87
|
+
setDirection: (d) => setGradientDirection(variable, d),
|
|
81
88
|
addStop: (s) => addGradientStop(variable, s),
|
|
82
89
|
removeStop: (i) => removeGradientStop(variable, i),
|
|
83
90
|
};
|
|
@@ -130,6 +137,7 @@ export function componentGradientSource(component: string, varName: string): Gra
|
|
|
130
137
|
return {
|
|
131
138
|
type: value.type,
|
|
132
139
|
angle: value.angle,
|
|
140
|
+
direction: value.direction,
|
|
133
141
|
centerX: value.centerX,
|
|
134
142
|
aspectX: value.aspectX,
|
|
135
143
|
aspectY: value.aspectY,
|
|
@@ -151,6 +159,7 @@ export function componentGradientSource(component: string, varName: string): Gra
|
|
|
151
159
|
? {
|
|
152
160
|
type: ref.value.type,
|
|
153
161
|
angle: ref.value.angle,
|
|
162
|
+
...(ref.value.direction !== undefined ? { direction: ref.value.direction } : {}),
|
|
154
163
|
...(ref.value.centerX !== undefined ? { centerX: ref.value.centerX } : {}),
|
|
155
164
|
...(ref.value.aspectX !== undefined ? { aspectX: ref.value.aspectX } : {}),
|
|
156
165
|
...(ref.value.aspectY !== undefined ? { aspectY: ref.value.aspectY } : {}),
|
|
@@ -167,6 +176,7 @@ export function componentGradientSource(component: string, varName: string): Gra
|
|
|
167
176
|
setAll: (next) => writeComponentGradient(component, varName, {
|
|
168
177
|
type: next.type,
|
|
169
178
|
angle: next.angle,
|
|
179
|
+
...(next.direction !== undefined ? { direction: next.direction } : {}),
|
|
170
180
|
...(next.centerX !== undefined ? { centerX: next.centerX } : {}),
|
|
171
181
|
...(next.aspectX !== undefined ? { aspectX: next.aspectX } : {}),
|
|
172
182
|
...(next.aspectY !== undefined ? { aspectY: next.aspectY } : {}),
|
|
@@ -181,7 +191,11 @@ export function componentGradientSource(component: string, varName: string): Gra
|
|
|
181
191
|
}
|
|
182
192
|
g.type = t;
|
|
183
193
|
}),
|
|
184
|
-
setAngle: (a) => update(`set gradient angle ${varName}`, (g) => {
|
|
194
|
+
setAngle: (a) => update(`set gradient angle ${varName}`, (g) => {
|
|
195
|
+
g.angle = a;
|
|
196
|
+
g.direction = undefined;
|
|
197
|
+
}),
|
|
198
|
+
setDirection: (d) => update(`set gradient direction ${varName}`, (g) => { g.direction = d; }),
|
|
185
199
|
setCenterX: (x) => update(`set gradient center ${varName}`, (g) => { g.centerX = x; }),
|
|
186
200
|
setAspect: (a) => update(`set gradient aspect ${varName}`, (g) => {
|
|
187
201
|
// Drop axes that equal 1 so persisted JSON stays minimal and pre-aspect
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* A component alias that owns a gradient serializes to one of two shapes:
|
|
4
4
|
*
|
|
5
5
|
* linear-gradient(<angle>deg, <stop>, <stop>, …)
|
|
6
|
+
* linear-gradient(<to side-or-corner>, <stop>, <stop>, …)
|
|
6
7
|
* radial-gradient(<shape> at <cx>% 50%, <stop>, <stop>, …)
|
|
7
8
|
*
|
|
8
9
|
* `solid` and `none` are NOT represented here. `solid` renders as its first
|
|
@@ -35,6 +36,47 @@
|
|
|
35
36
|
*/
|
|
36
37
|
import { parseColorOpacity, formatColorOpacity } from './colorOpacity';
|
|
37
38
|
|
|
39
|
+
/** The `to <side-or-corner>` keywords CSS accepts in place of an angle. A
|
|
40
|
+
* keyword angles the gradient line off the box's own diagonal, so it tracks
|
|
41
|
+
* the element's aspect ratio where a fixed angle cannot: `to bottom right`
|
|
42
|
+
* reads as ~95deg across a wide heading and ~111deg once that heading wraps.
|
|
43
|
+
* That is the whole reason to carry them — a degree cannot express it. */
|
|
44
|
+
export const LINEAR_DIRECTIONS = [
|
|
45
|
+
'to top',
|
|
46
|
+
'to top right',
|
|
47
|
+
'to right',
|
|
48
|
+
'to bottom right',
|
|
49
|
+
'to bottom',
|
|
50
|
+
'to bottom left',
|
|
51
|
+
'to left',
|
|
52
|
+
'to top left',
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
export type LinearDirection = (typeof LINEAR_DIRECTIONS)[number];
|
|
56
|
+
|
|
57
|
+
/** The angle each keyword equals on a square box. Sides are exact at any
|
|
58
|
+
* aspect (`to right` is 90deg, always); corners hold only at 1:1, which is
|
|
59
|
+
* the point of the keyword. Stored alongside a direction so the dial shows
|
|
60
|
+
* something honest and clearing the keyword lands on the nearest angle
|
|
61
|
+
* rather than snapping to 0deg (bottom-to-top). */
|
|
62
|
+
export const DIRECTION_ANGLES: Record<LinearDirection, number> = {
|
|
63
|
+
'to top': 0,
|
|
64
|
+
'to top right': 45,
|
|
65
|
+
'to right': 90,
|
|
66
|
+
'to bottom right': 135,
|
|
67
|
+
'to bottom': 180,
|
|
68
|
+
'to bottom left': 225,
|
|
69
|
+
'to left': 270,
|
|
70
|
+
'to top left': 315,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function parseDirection(raw: string): LinearDirection | null {
|
|
74
|
+
const norm = raw.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
75
|
+
return (LINEAR_DIRECTIONS as readonly string[]).includes(norm)
|
|
76
|
+
? (norm as LinearDirection)
|
|
77
|
+
: null;
|
|
78
|
+
}
|
|
79
|
+
|
|
38
80
|
export interface GradientStop {
|
|
39
81
|
/** 0–100 percentage along the gradient axis. */
|
|
40
82
|
position: number;
|
|
@@ -54,6 +96,10 @@ export interface GradientStop {
|
|
|
54
96
|
export interface GradientValue {
|
|
55
97
|
type: 'linear' | 'radial' | 'solid' | 'none';
|
|
56
98
|
angle: number;
|
|
99
|
+
/** Emitted in place of `angle` on a `linear` gradient. `angle` is retained
|
|
100
|
+
* underneath so clearing the direction restores the user's degrees, the
|
|
101
|
+
* same way a radial keeps its angle. */
|
|
102
|
+
direction?: LinearDirection;
|
|
57
103
|
radius?: number;
|
|
58
104
|
centerX?: number;
|
|
59
105
|
aspectX?: number;
|
|
@@ -101,7 +147,8 @@ function formatRadialShape(v: GradientValue): string {
|
|
|
101
147
|
* into a CSS background declaration.
|
|
102
148
|
* - `none` → `transparent` (no background paint).
|
|
103
149
|
* - `solid` → the first stop's colour (no gradient function).
|
|
104
|
-
* - `linear` → `linear-gradient(<angle>deg, <stops>)
|
|
150
|
+
* - `linear` → `linear-gradient(<angle>deg, <stops>)`, or
|
|
151
|
+
* `linear-gradient(<direction>, <stops>)` when `direction` is set.
|
|
105
152
|
* - `radial` → `radial-gradient(<shape> at <centerX>% 50%, <stops>)`, where
|
|
106
153
|
* shape is `circle [radius]` at aspect 1 and `ellipse rx ry` otherwise.
|
|
107
154
|
* Aspects are independent stretch factors, not an area-preserving ratio:
|
|
@@ -113,12 +160,16 @@ export function formatGradientValue(v: GradientValue): string {
|
|
|
113
160
|
return first ? formatStopColor(first) : 'transparent';
|
|
114
161
|
}
|
|
115
162
|
const stops = formatGradientStops(v.stops);
|
|
116
|
-
if (v.type === 'linear')
|
|
163
|
+
if (v.type === 'linear') {
|
|
164
|
+
return `linear-gradient(${v.direction ?? `${v.angle}deg`}, ${stops})`;
|
|
165
|
+
}
|
|
117
166
|
return `radial-gradient(${formatRadialShape(v)} at ${v.centerX ?? 50}% 50%, ${stops})`;
|
|
118
167
|
}
|
|
119
168
|
|
|
120
169
|
const NUM = String.raw`-?\d+(?:\.\d+)?`;
|
|
121
|
-
const
|
|
170
|
+
const HEADING = String.raw`${NUM}deg|to\s+\w+(?:\s+\w+)?`;
|
|
171
|
+
const LINEAR_RE = new RegExp(String.raw`^linear-gradient\(\s*(${HEADING})\s*,\s*(.+)\)$`, 'i');
|
|
172
|
+
const DEG_RE = new RegExp(String.raw`^(${NUM})deg$`, 'i');
|
|
122
173
|
const RADIAL_RE = new RegExp(
|
|
123
174
|
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*(.+)\)$`,
|
|
124
175
|
'i',
|
|
@@ -192,8 +243,15 @@ export function parseGradientValue(value: string): GradientValue | null {
|
|
|
192
243
|
const css = value.trim();
|
|
193
244
|
const linear = css.match(LINEAR_RE);
|
|
194
245
|
if (linear) {
|
|
246
|
+
const deg = linear[1].match(DEG_RE);
|
|
247
|
+
const direction = deg ? null : parseDirection(linear[1]);
|
|
248
|
+
// A heading that is neither degrees nor a keyword we know is not our form.
|
|
249
|
+
if (!deg && !direction) return null;
|
|
195
250
|
const stops = parseStops(linear[2]);
|
|
196
|
-
|
|
251
|
+
if (!stops) return null;
|
|
252
|
+
return deg
|
|
253
|
+
? { type: 'linear', angle: parseFloat(deg[1]), stops }
|
|
254
|
+
: { type: 'linear', angle: 0, direction: direction!, stops };
|
|
197
255
|
}
|
|
198
256
|
const radial = css.match(RADIAL_RE);
|
|
199
257
|
if (!radial) return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Gradients slice —
|
|
2
|
+
* Gradients slice — an open `--gradient-N` library seeded with four slots,
|
|
3
3
|
* each rendering to a single CSS var. Stops carry token-name references
|
|
4
4
|
* (`--color-brand-500`); the renderer wraps them in `var(...)` so palette
|
|
5
5
|
* edits flow through.
|
|
@@ -8,15 +8,34 @@ import type { EditorState, GradientToken, GradientTokenStop, GradientType } from
|
|
|
8
8
|
import type { GradientDiskToken } from '../themeTypes';
|
|
9
9
|
import { mutate } from '../../store/editorCore';
|
|
10
10
|
import { formatGradientValue, formatGradientStops as formatStopList } from '../parsers/gradient';
|
|
11
|
+
import { DIRECTION_ANGLES, type LinearDirection } from '../parsers/gradient';
|
|
11
12
|
|
|
12
13
|
export { formatGradientValue };
|
|
13
14
|
|
|
15
|
+
/** A well-formed library slot: `--gradient-` followed by a number. Anything
|
|
16
|
+
* else is pre-numbering data the loader and the persistence guard reject. */
|
|
17
|
+
export const GRADIENT_SLOT_RE = /^--gradient-\d+$/;
|
|
18
|
+
|
|
19
|
+
export function isGradientSlot(variable: string): boolean {
|
|
20
|
+
return GRADIENT_SLOT_RE.test(variable);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The next free slot name, so callers never collide with an existing one. */
|
|
24
|
+
export function nextGradientSlot(tokens: readonly GradientToken[]): string {
|
|
25
|
+
const used = new Set(tokens.map((t) => t.variable));
|
|
26
|
+
for (let n = 1; ; n++) {
|
|
27
|
+
const name = `--gradient-${n}`;
|
|
28
|
+
if (!used.has(name)) return name;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
export function makeDefaultGradients(): GradientToken[] {
|
|
15
33
|
return [
|
|
16
34
|
{
|
|
17
35
|
variable: '--gradient-1',
|
|
18
36
|
type: 'linear',
|
|
19
|
-
angle:
|
|
37
|
+
angle: DIRECTION_ANGLES['to right'],
|
|
38
|
+
direction: 'to right',
|
|
20
39
|
stops: [
|
|
21
40
|
{ position: 0, color: '--color-brand-500' },
|
|
22
41
|
{ position: 100, color: '--color-accent-500' },
|
|
@@ -34,7 +53,8 @@ export function makeDefaultGradients(): GradientToken[] {
|
|
|
34
53
|
{
|
|
35
54
|
variable: '--gradient-3',
|
|
36
55
|
type: 'linear',
|
|
37
|
-
angle:
|
|
56
|
+
angle: DIRECTION_ANGLES['to right'],
|
|
57
|
+
direction: 'to right',
|
|
38
58
|
stops: [
|
|
39
59
|
{ position: 0, color: '--color-success-500' },
|
|
40
60
|
{ position: 100, color: '--color-info-500' },
|
|
@@ -57,20 +77,29 @@ export function makeDefaultGradients(): GradientToken[] {
|
|
|
57
77
|
* field and scrub the rendered `--gradient-N` strings from the vars bag — they
|
|
58
78
|
* are a projection kept for production CSS, never a basis. Files without the
|
|
59
79
|
* field (saved before gradients round-tripped) keep the seeded defaults, which
|
|
60
|
-
* match what those files rendered.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
80
|
+
* match what those files rendered.
|
|
81
|
+
*
|
|
82
|
+
* The library is open-ended: any number of `--gradient-N` slots loads, so a
|
|
83
|
+
* project can carry as many as its design needs. What still fails the guard is
|
|
84
|
+
* a stale *shape* — a token named `--gradient-progress`, from before the slots
|
|
85
|
+
* were numbered — which keeps defaults, mirroring `migrateGradients`.
|
|
63
86
|
*/
|
|
64
87
|
export function loadGradientsFromFile(
|
|
65
88
|
next: EditorState,
|
|
66
89
|
gradients: GradientDiskToken[] | undefined,
|
|
67
90
|
rawVars: Record<string, string>,
|
|
68
91
|
): void {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
for (const g of makeDefaultGradients()) {
|
|
93
|
+
delete rawVars[g.variable];
|
|
94
|
+
delete rawVars[stopsVariable(g.variable)];
|
|
95
|
+
}
|
|
96
|
+
for (const t of gradients ?? []) {
|
|
97
|
+
delete rawVars[t.variable];
|
|
98
|
+
delete rawVars[stopsVariable(t.variable)];
|
|
99
|
+
}
|
|
100
|
+
if (gradients?.length && gradients.every((g) => isGradientSlot(g.variable))) {
|
|
101
|
+
next.gradients.tokens = structuredClone(gradients) as GradientToken[];
|
|
102
|
+
}
|
|
74
103
|
}
|
|
75
104
|
|
|
76
105
|
/** Stops portion only — used by the palette selector to materialize a
|
|
@@ -84,6 +113,7 @@ function formatGradient(t: GradientToken): string {
|
|
|
84
113
|
return formatGradientValue({
|
|
85
114
|
type: t.type,
|
|
86
115
|
angle: t.angle,
|
|
116
|
+
direction: t.direction,
|
|
87
117
|
centerX: t.centerX,
|
|
88
118
|
aspectX: t.aspectX,
|
|
89
119
|
aspectY: t.aspectY,
|
|
@@ -91,9 +121,22 @@ function formatGradient(t: GradientToken): string {
|
|
|
91
121
|
});
|
|
92
122
|
}
|
|
93
123
|
|
|
124
|
+
/** The suffix carrying a token's stop list on its own, so a consumer can keep
|
|
125
|
+
* the theme's colours while supplying its own geometry:
|
|
126
|
+
* `linear-gradient(to top, var(--gradient-5-stops))`. The same stops then
|
|
127
|
+
* serve every direction a design needs without duplicating a token per angle. */
|
|
128
|
+
export const STOPS_VAR_SUFFIX = '-stops';
|
|
129
|
+
|
|
130
|
+
export function stopsVariable(variable: string): string {
|
|
131
|
+
return `${variable}${STOPS_VAR_SUFFIX}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
94
134
|
export function gradientsToVars(g: EditorState['gradients']): Record<string, string> {
|
|
95
135
|
const out: Record<string, string> = {};
|
|
96
|
-
for (const t of g.tokens)
|
|
136
|
+
for (const t of g.tokens) {
|
|
137
|
+
out[t.variable] = formatGradient(t);
|
|
138
|
+
out[stopsVariable(t.variable)] = formatStopList(t.stops);
|
|
139
|
+
}
|
|
97
140
|
return out;
|
|
98
141
|
}
|
|
99
142
|
|
|
@@ -105,13 +148,14 @@ function findGradient(s: EditorState, variable: string): GradientToken | undefin
|
|
|
105
148
|
* Used by the editor to restore a pre-edit snapshot on Cancel. */
|
|
106
149
|
export function setGradient(
|
|
107
150
|
variable: string,
|
|
108
|
-
next: { type: GradientType; angle: number; centerX?: number; aspectX?: number; aspectY?: number; stops: GradientTokenStop[] },
|
|
151
|
+
next: { type: GradientType; angle: number; direction?: LinearDirection; centerX?: number; aspectX?: number; aspectY?: number; stops: GradientTokenStop[] },
|
|
109
152
|
): void {
|
|
110
153
|
mutate(`replace gradient ${variable}`, (s) => {
|
|
111
154
|
const t = findGradient(s, variable);
|
|
112
155
|
if (!t) return;
|
|
113
156
|
t.type = next.type;
|
|
114
157
|
t.angle = next.angle;
|
|
158
|
+
t.direction = next.direction;
|
|
115
159
|
t.centerX = next.centerX;
|
|
116
160
|
t.aspectX = next.aspectX;
|
|
117
161
|
t.aspectY = next.aspectY;
|
|
@@ -126,10 +170,27 @@ export function setGradientType(variable: string, type: GradientType): void {
|
|
|
126
170
|
});
|
|
127
171
|
}
|
|
128
172
|
|
|
173
|
+
/** Setting degrees clears any direction keyword: the two are alternative
|
|
174
|
+
* headings for the same gradient, and the one you just set is the one you
|
|
175
|
+
* meant. The angle underneath a direction is preserved until then. */
|
|
129
176
|
export function setGradientAngle(variable: string, angle: number): void {
|
|
130
177
|
mutate(`set gradient angle ${variable}`, (s) => {
|
|
131
178
|
const t = findGradient(s, variable);
|
|
132
|
-
if (t)
|
|
179
|
+
if (!t) return;
|
|
180
|
+
t.angle = angle;
|
|
181
|
+
t.direction = undefined;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `undefined` drops back to the stored `angle`. Setting a keyword also moves
|
|
186
|
+
* `angle` to that keyword's square-box equivalent, so the dial reflects the
|
|
187
|
+
* gradient and clearing the keyword keeps roughly the same direction. */
|
|
188
|
+
export function setGradientDirection(variable: string, direction: LinearDirection | undefined): void {
|
|
189
|
+
mutate(`set gradient direction ${variable}`, (s) => {
|
|
190
|
+
const t = findGradient(s, variable);
|
|
191
|
+
if (!t) return;
|
|
192
|
+
t.direction = direction;
|
|
193
|
+
if (direction) t.angle = DIRECTION_ANGLES[direction];
|
|
133
194
|
});
|
|
134
195
|
}
|
|
135
196
|
|
|
@@ -111,7 +111,12 @@ export interface FontSource {
|
|
|
111
111
|
|
|
112
112
|
export type SystemCascadePreset = 'system-ui-sans' | 'system-ui-serif' | 'system-ui-mono';
|
|
113
113
|
export type GenericFamily = 'sans-serif' | 'serif' | 'monospace' | 'cursive' | 'fantasy';
|
|
114
|
-
export type FontStackVariable =
|
|
114
|
+
export type FontStackVariable =
|
|
115
|
+
| '--font-display'
|
|
116
|
+
| '--font-sans'
|
|
117
|
+
| '--font-serif'
|
|
118
|
+
| '--font-mono'
|
|
119
|
+
| '--font-editorial';
|
|
115
120
|
|
|
116
121
|
export type FontStackSlot =
|
|
117
122
|
| { kind: 'project'; familyId: string }
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
{ path: '01-overview', title: 'Overview' },
|
|
99
99
|
{ path: 'getting-started', title: 'Getting started' },
|
|
100
100
|
{ path: 'editing-tokens', title: 'Editing tokens' },
|
|
101
|
+
{ path: 'sketch-mode', title: 'Sketch mode' },
|
|
101
102
|
{ path: 'themes-workflow', title: 'Themes' },
|
|
102
103
|
{ path: 'where-themes-live', title: 'Where themes live' },
|
|
103
104
|
{ path: 'creating-components', title: 'Creating components' },
|
|
@@ -254,7 +255,7 @@
|
|
|
254
255
|
<SideNavigation
|
|
255
256
|
class="docs-sidebar"
|
|
256
257
|
sections={navSections}
|
|
257
|
-
titleLabel="
|
|
258
|
+
titleLabel="LiveTokens"
|
|
258
259
|
titleHref="#01-overview"
|
|
259
260
|
currentPath={parsedHash.chapter}
|
|
260
261
|
open={sidebarOpen}
|
|
@@ -283,11 +284,11 @@
|
|
|
283
284
|
<div class="docs-main" bind:this={scrollPane}>
|
|
284
285
|
<header class="docs-page-header">
|
|
285
286
|
<div class="title-block">
|
|
286
|
-
<p class="eyebrow">
|
|
287
|
+
<p class="eyebrow">LiveTokens</p>
|
|
287
288
|
<h1>Documentation</h1>
|
|
288
289
|
<p class="lede">
|
|
289
290
|
Set up a project, edit your tokens live, and ship a theme. A short
|
|
290
|
-
guide to styling and building with
|
|
291
|
+
guide to styling and building with LiveTokens.
|
|
291
292
|
</p>
|
|
292
293
|
</div>
|
|
293
294
|
</header>
|
|
@@ -13,6 +13,7 @@ export const chapters: Chapter[] = [
|
|
|
13
13
|
{ id: '01-overview', title: 'Overview' },
|
|
14
14
|
{ id: 'getting-started', title: 'Getting started' },
|
|
15
15
|
{ id: 'editing-tokens', title: 'Editing tokens' },
|
|
16
|
+
{ id: 'sketch-mode', title: 'Sketch mode' },
|
|
16
17
|
{ id: 'themes-workflow', title: 'Themes' },
|
|
17
18
|
{ id: 'where-themes-live', title: 'Where themes live' },
|
|
18
19
|
{ id: 'creating-components', title: 'Creating components' },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Overview
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
LiveTokens is a design system for building Svelte microsites quickly. You
|
|
4
4
|
style your site by editing tokens and components in a live editor. When it looks right, you save the theme and ship it.
|
|
5
5
|
|
|
6
6
|
## How it works
|
|
@@ -27,6 +27,7 @@ style your site by editing tokens and components in a live editor. When it looks
|
|
|
27
27
|
- **[Getting started](getting-started.md)**: scaffold a project and make your
|
|
28
28
|
first edit.
|
|
29
29
|
- **[Editing tokens](editing-tokens.md)**: a tour of the editor.
|
|
30
|
+
- **[Sketch mode](sketch-mode.md)**: redraw the page by hand.
|
|
30
31
|
- **[Themes](themes-workflow.md)**: save, switch, and ship.
|
|
31
32
|
- **[Creating components](creating-components.md)**: make your own components
|
|
32
33
|
editable.
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
A tour of the editor. The page behind it repaints on every change; saving
|
|
4
4
|
writes a theme file you can reload later.
|
|
5
5
|
|
|
6
|
-
The editor has
|
|
6
|
+
The editor has four views:
|
|
7
7
|
|
|
8
8
|
- **Tokens**: the design-system primitives (colour, type, spacing, and so on).
|
|
9
9
|
They apply everywhere your site uses them.
|
|
10
|
+
- **Color Wheel**: the harmony wheel, the palette curves, and the story your colours
|
|
11
|
+
tell across a page.
|
|
10
12
|
- **Components**: per-component editors. Re-Assign what tokens a component uses
|
|
11
13
|
without changing the underlying system.
|
|
14
|
+
- **Sketch Style**: an effect layer that redraws the page by hand. See
|
|
15
|
+
[Sketch mode](sketch-mode.md).
|
|
12
16
|
|
|
13
17
|
This page covers **Tokens**. For components, see
|
|
14
18
|
[Creating components](creating-components.md).
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Sketch mode
|
|
2
|
+
|
|
3
|
+
Sketch mode redraws your whole page as if it had been drawn by hand. Every
|
|
4
|
+
component keeps its own colours, spacing and corners; what changes is the line
|
|
5
|
+
they are drawn with.
|
|
6
|
+
|
|
7
|
+
It is an effect layer, not a set of token values. It reads nothing from your
|
|
8
|
+
theme and writes nothing back, so it never touches a token, never lands in a
|
|
9
|
+
theme file, and never reaches the CSS you ship. Turn it off and every trace of
|
|
10
|
+
it goes.
|
|
11
|
+
|
|
12
|
+
Open the **Sketch Style** view in the editor and switch **Sketch mode** on. The effect
|
|
13
|
+
applies to the page behind the editor as well as to the preview, so what you see
|
|
14
|
+
in context is what it does.
|
|
15
|
+
|
|
16
|
+
## What it draws
|
|
17
|
+
|
|
18
|
+
Each component's fill and outline are repainted from the tokens that component
|
|
19
|
+
already owns. The real background and border are hidden behind them, then both
|
|
20
|
+
are pushed around one shared field of noise. Because every component samples the
|
|
21
|
+
same field, the whole page reads as one drawing rather than as a set of
|
|
22
|
+
separately wobbled boxes.
|
|
23
|
+
|
|
24
|
+
## The presets
|
|
25
|
+
|
|
26
|
+
Seven looks ship with the package, and each is a complete set of dials rather
|
|
27
|
+
than a style name:
|
|
28
|
+
|
|
29
|
+
- **Pencil.** Two graphite passes on their own seeds, so the outline disagrees
|
|
30
|
+
with itself the way a hand coming back round does.
|
|
31
|
+
- **Marker.** A broad translucent nib gone round twice on the same line, so the
|
|
32
|
+
overlap darkens and the ink pools where it slows.
|
|
33
|
+
- **Whiteboard.** The fattest nib on glass, with a mask that streaks the fill
|
|
34
|
+
like a half-wiped board.
|
|
35
|
+
- **Hatched.** An etching. The fill is angled shading and the outline a single
|
|
36
|
+
hard-edged scratch.
|
|
37
|
+
- **Dashed.** A drafting outline: one slow drift along the ruler, broken into
|
|
38
|
+
strokes. The clean pole.
|
|
39
|
+
- **Napkin.** Ballpoint in a hurry. Everything loose at once.
|
|
40
|
+
- **Dry marker.** Ink that ran out. One scratchy pass over a mostly eaten fill.
|
|
41
|
+
|
|
42
|
+
Pick one, then move whatever you like. **Save** keeps your dials under a name of
|
|
43
|
+
your own, alongside the shipped seven, as a file under
|
|
44
|
+
`src/live-tokens/data/sketch-presets/`.
|
|
45
|
+
|
|
46
|
+
## The dials
|
|
47
|
+
|
|
48
|
+
- **Border.** How far the outline travels and how long its wave is, then its
|
|
49
|
+
width, ink, pressure and pooling. A second pass either copies the first line a
|
|
50
|
+
few pixels off or runs it through the pen again on its own seed.
|
|
51
|
+
- **Fill.** Solid or hatched, how far the fill's edge travels, and how far each
|
|
52
|
+
instance is offset, rotated and scaled from its neighbours. **Ink coverage**
|
|
53
|
+
thins the fill with a field of blotches: set their size, how many levels of
|
|
54
|
+
detail, how pale and how dense they go, and how soft their edges are.
|
|
55
|
+
- **Shape.** **Corner spread** rounds each corner by its own share of the dial,
|
|
56
|
+
so no two match. **Corner travel** leans the drawn box into a quadrilateral
|
|
57
|
+
with no two sides parallel. This is the dial that stops a component reading as
|
|
58
|
+
a rectangle.
|
|
59
|
+
- **Icons and SVG.** Glyph travel and wavelength on their own scale. A glyph is
|
|
60
|
+
all curves already, so it needs more travel than a card's long straight edge
|
|
61
|
+
before the wobble reads at all.
|
|
62
|
+
- **Noise.** The shared field itself: its wavelength, how many layers of detail
|
|
63
|
+
sit on it, and the shape of its wave. A square wave sends nearly every edge to
|
|
64
|
+
full travel, which is what makes the effect stronger rather than bigger.
|
|
65
|
+
|
|
66
|
+
## Where the settings live
|
|
67
|
+
|
|
68
|
+
The dials you are moving live in your browser, so the effect follows you across
|
|
69
|
+
reloads and stays off everyone else's screen. **Save** writes a named preset to
|
|
70
|
+
`src/live-tokens/data/sketch-presets/`, which is the only thing that reaches
|
|
71
|
+
disk.
|
|
72
|
+
|
|
73
|
+
Sketch mode is a tool for looking at the page, not a layer the page can ship.
|
|
74
|
+
Nothing is written into a theme, `tokens.generated.css` never sees it, and a
|
|
75
|
+
production build has no sketch layer in it at all.
|
|
76
|
+
|
|
77
|
+
## Drawing your own elements
|
|
78
|
+
|
|
79
|
+
The layer draws a fixed set of parts: the shipped components, and four classes
|
|
80
|
+
it reserves for you. Nothing else is touched, so a page element or a
|
|
81
|
+
consumer-authored component is left crisp until it carries one of them.
|
|
82
|
+
|
|
83
|
+
| Class | For |
|
|
84
|
+
|---------------------|-----------------------------------------------------------|
|
|
85
|
+
| `sketch-surface` | A box. The default treatment. |
|
|
86
|
+
| `sketch-container` | A large box. Tilts less, so the type inside stays readable. |
|
|
87
|
+
| `sketch-chip` | A small box. Finer fill mask, more rotation, less travel. |
|
|
88
|
+
| `sketch-rule` | A line rather than a box. No rotation, no rounded ends. |
|
|
89
|
+
|
|
90
|
+
Pick by size, not by kind: a card and a modal both take `sketch-container`, a
|
|
91
|
+
badge and a pill both take `sketch-chip`.
|
|
92
|
+
|
|
93
|
+
The class opts the element in; it names no colours, so the element states its
|
|
94
|
+
own. `--sketch-fill`, `--sketch-stroke`, `--sketch-hatch-color`,
|
|
95
|
+
`--sketch-radius` and `--sketch-shadow` name the fill, the outline, the hatching
|
|
96
|
+
ink, the corners and the shadow for one element and everything inside it. The
|
|
97
|
+
layer blanks the real background and border, so an element whose fill matters
|
|
98
|
+
under Sketch mode has to name it here as well as paint it.
|
|
99
|
+
|
|
100
|
+
The layer also paints on the element's `::before` and `::after`, forces its
|
|
101
|
+
`overflow` visible, and gives it a stacking context of its own. Keep the class
|
|
102
|
+
off anything that owns a pseudo-element, clips its content, or is positioned
|
|
103
|
+
absolutely, and put it on a wrapper instead.
|
|
104
|
+
|
|
105
|
+
```css
|
|
106
|
+
.my-callout {
|
|
107
|
+
background: var(--surface-brand-lowest);
|
|
108
|
+
border: var(--border-width-1) solid var(--border-brand);
|
|
109
|
+
border-radius: var(--radius-xl);
|
|
110
|
+
|
|
111
|
+
--sketch-fill: var(--surface-brand-lowest);
|
|
112
|
+
--sketch-stroke: var(--border-brand);
|
|
113
|
+
--sketch-radius: var(--radius-xl);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A gradient is a valid fill: the shorthand's last layer takes a colour or an
|
|
118
|
+
image, so `--sketch-fill` accepts either. States work the same way, since
|
|
119
|
+
nothing is competing with you for the value:
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
.my-callout:hover { --sketch-stroke: var(--border-brand-strong); }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Images inside a drawn part
|
|
126
|
+
|
|
127
|
+
A drawn part's `overflow` is forced visible, because the fill and outline are
|
|
128
|
+
painted on pseudo-elements that travel past the box and would otherwise be cut
|
|
129
|
+
off at its edge. A background that bleeds is the effect working. An image that
|
|
130
|
+
bleeds is not: it keeps its square corners while the card around it turns.
|
|
131
|
+
|
|
132
|
+
Media that runs to a part's edge therefore has to carry that part's corners
|
|
133
|
+
itself. `--sketch-radius` is the radius the layer drew, and it inherits, so a
|
|
134
|
+
child can read it and fall back to its own value when Sketch mode is off:
|
|
135
|
+
|
|
136
|
+
```css
|
|
137
|
+
.cover {
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
border-top-left-radius: var(--sketch-radius, var(--card-default-radius));
|
|
140
|
+
border-top-right-radius: var(--sketch-radius, var(--card-default-radius));
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Corner spread is per-corner and per-instance, so at high spread the crop is the
|
|
145
|
+
mean rather than an exact trace of the drawn edge.
|
|
146
|
+
|
|
147
|
+
A rule made from a `border` is not a box and cannot be displaced. Make it an
|
|
148
|
+
element, give it `sketch-rule`, and name its ink:
|
|
149
|
+
|
|
150
|
+
```html
|
|
151
|
+
<div class="rule sketch-rule"></div>
|
|
152
|
+
```
|
|
153
|
+
```css
|
|
154
|
+
.rule {
|
|
155
|
+
height: var(--border-width-2);
|
|
156
|
+
background: var(--border-brand);
|
|
157
|
+
--sketch-fill: var(--border-brand);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Icons and inline SVG take the wobble directly, since a glyph has no box to
|
|
162
|
+
redraw. Body type is left alone: an icon is a shape and survives a wobble, a
|
|
163
|
+
paragraph is not.
|
|
164
|
+
|
|
165
|
+
`--sketch-icon-off` names what a subtree's glyphs are drawn with instead. It
|
|
166
|
+
inherits, so one declaration covers everything under it:
|
|
167
|
+
|
|
168
|
+
```css
|
|
169
|
+
/* Crisp. Chrome, a logo, anything that has to stay exact. */
|
|
170
|
+
.app-bar { --sketch-icon-off: none; }
|
|
171
|
+
|
|
172
|
+
/* Drawn back rather than off, at a third of the travel. Small artwork, and
|
|
173
|
+
type set as an SVG, which the layer reads as one large glyph. */
|
|
174
|
+
.wordmark { --sketch-icon-off: var(--sketch-icon-soft); }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The **Blotch size** dial under Icons and SVG is a share of the glyph rather than
|
|
178
|
+
a px size, because no px size is right for both a 16px icon and a page-wide
|
|
179
|
+
drawing. At 100% every glyph gets one period of the field across it whatever its
|
|
180
|
+
size. Below that the field repeats inside the glyph and the blotches get finer.
|
|
181
|
+
Above it a glyph reads part of one blotch, so the mask thins the whole glyph
|
|
182
|
+
unevenly instead of breaking it up. The fill's blotches stay in px, since a
|
|
183
|
+
component does have a size to state one against.
|