@motion-proto/live-tokens 0.94.2 → 0.95.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-page/SKILL.md +7 -3
- package/CHANGELOG.md +108 -0
- package/README.md +6 -6
- package/dist-plugin/{chunk-7XSUHJUH.js → chunk-4J6JPSVS.js} +53 -2
- package/dist-plugin/{chunk-W5VQV32K.js → chunk-GYRULB2X.js} +1 -1
- package/dist-plugin/{chunk-ONYAIXCZ.js → chunk-QQASYPW7.js} +32 -9
- package/dist-plugin/index.cjs +598 -17
- package/dist-plugin/index.d.cts +9 -1
- package/dist-plugin/index.d.ts +9 -1
- package/dist-plugin/index.js +513 -6
- package/dist-plugin/migrateData/index.cjs +32 -9
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/setColors/index.cjs +32 -22
- package/dist-plugin/setColors/index.js +2 -15
- package/dist-plugin/setGeometry/index.cjs +32 -9
- package/dist-plugin/setGeometry/index.js +1 -1
- package/dist-plugin/tokensCssMigrations/index.cjs +53 -2
- package/dist-plugin/tokensCssMigrations/index.d.cts +9 -2
- package/dist-plugin/tokensCssMigrations/index.d.ts +9 -2
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +8 -3
- package/src/demo/ChromeSelect.svelte +111 -0
- package/src/demo/Demo.svelte +39 -5
- package/src/demo/DemoBar.svelte +33 -14
- package/src/demo/FloatingTokenTags.css +33 -4
- package/src/demo/FloatingTokenTags.svelte +81 -68
- package/src/demo/Section.svelte +1 -7
- package/src/demo/SketchSelect.svelte +36 -12
- package/src/demo/SourceLinks.svelte +5 -4
- package/src/demo/TestingLoops.svelte +128 -19
- package/src/demo/ThemeSelect.svelte +34 -11
- package/src/demo/evalResults.ts +7 -7
- package/src/demo/sections/SectionClaude.svelte +20 -13
- package/src/demo/sections/SectionFooter.svelte +4 -5
- package/src/demo/sections/SectionGetStarted.svelte +5 -6
- package/src/demo/sections/SectionHero.svelte +43 -23
- package/src/demo/sections/SectionKit.svelte +141 -45
- package/src/demo/sections/SectionLayers.svelte +3 -3
- package/src/demo/sections/SectionOffering.svelte +8 -23
- package/src/editor/bootstrap.ts +12 -6
- package/src/editor/component-editor/ImageEditor.svelte +1 -1
- package/src/editor/component-editor/ImageLightboxEditor.svelte +3 -4
- package/src/editor/component-editor/registration.ts +36 -0
- package/src/editor/component-editor/registry.ts +5 -12
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +11 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +7 -0
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +1 -2
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +17 -2
- package/src/editor/core/store/editorPersistence.ts +6 -1
- package/src/editor/core/store/editorRenderer.ts +1 -4
- package/src/editor/core/store/editorStore.ts +5 -9
- package/src/editor/core/store/editorTypes.ts +2 -2
- package/src/editor/core/themes/migrations/2026-10-15-responsive-columns.ts +40 -0
- package/src/editor/core/themes/migrations/index.ts +2 -0
- package/src/editor/core/themes/slices/columns.ts +112 -42
- package/src/editor/docs/Docs.svelte +25 -22
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/editing-tokens.md +5 -3
- package/src/editor/docs/content/production-css.md +116 -0
- package/src/editor/docs/content/themes-workflow.md +2 -0
- package/src/editor/docs/content.generated.ts +3 -2
- package/src/editor/index.ts +2 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +67 -19
- package/src/editor/overlay/LiveTokensRouter.svelte +8 -4
- package/src/editor/pages/Editor.svelte +15 -1
- package/src/editor/pages/EditorShell.svelte +312 -99
- package/src/editor/skill-atlas/SkillAtlas.svelte +111 -24
- package/src/editor/skill-atlas/TreeCanvas.svelte +14 -0
- package/src/editor/skill-atlas/skillSources.generated.ts +1 -1
- package/src/editor/skill-atlas/trees/create-page.ts +24 -24
- package/src/editor/skill-atlas/wireLayout.ts +62 -0
- package/src/editor/ui/BezierCurveEditor.svelte +3 -0
- package/src/editor/ui/FontStackEditor.svelte +2 -2
- package/src/editor/ui/PaletteEditor.svelte +64 -6
- package/src/editor/ui/UIPaletteSelector.svelte +2 -0
- package/src/editor/ui/colors/ColorsTab.svelte +5 -2
- package/src/editor/ui/palette/OverridesPanel.svelte +1 -0
- package/src/editor/ui/palette/ScaleCurveEditor.svelte +127 -18
- package/src/editor/ui/sections/ColumnsSection.svelte +79 -49
- package/src/editor/ui/sections/ShadowsSection.svelte +14 -0
- package/src/editor/ui/sections/WashesSection.svelte +11 -0
- package/src/editor/ui/sketch/SketchTab.svelte +1 -1
- package/src/live-tokens/data/colors-and-type/autumn.json +1 -4
- package/src/live-tokens/data/colors-and-type/default.json +1 -4
- package/src/live-tokens/data/colors-and-type/halloween.json +1 -4
- package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -4
- package/src/live-tokens/data/colors-and-type/ocean.json +1 -4
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -4
- package/src/live-tokens/data/colors-and-type/sketchy.json +1 -4
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -4
- package/src/live-tokens/data/colors-and-type/sunset.json +1 -4
- package/src/live-tokens/data/themes/autumn.json +1 -4
- package/src/live-tokens/data/themes/halloween.json +1 -4
- package/src/live-tokens/data/themes/midnight-study.json +1 -4
- package/src/live-tokens/data/themes/ocean.json +1 -4
- package/src/live-tokens/data/themes/royal-velvet.json +1 -4
- package/src/live-tokens/data/themes/sketchy.json +1 -4
- package/src/live-tokens/data/themes/spring-meadow.json +1 -4
- package/src/live-tokens/data/themes/sunset.json +1 -4
- package/src/live-tokens/data/tokens.generated.css +1 -4
- package/src/system/components/SectionDivider.svelte +4 -0
- package/src/system/styles/CONVENTIONS.md +1 -1
- package/src/system/styles/tokens.css +30 -4
- package/template/src/main.ts +2 -2
- package/template/src/pages/Home.svelte +11 -8
- package/template/vite.config.ts +7 -3
- package/src/system/assets/offering.webp +0 -0
- /package/dist-plugin/{chunk-O6GQ6GBH.js → chunk-A4TBP4MM.js} +0 -0
|
@@ -83,12 +83,21 @@
|
|
|
83
83
|
grid-area: controls;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/* Narrow: the preview leads, since it is what the pinned stage is for, and a
|
|
87
|
+
component wider than the stage scrolls inside it. */
|
|
86
88
|
@container variant-group (max-width: 32rem) {
|
|
89
|
+
/* Holds its height so the pinned stage scrolls to the controls rather than
|
|
90
|
+
squeezing the preview under them. */
|
|
87
91
|
.shadow-backdrop.with-controls {
|
|
92
|
+
flex-shrink: 0;
|
|
88
93
|
grid-template-columns: minmax(0, 1fr);
|
|
89
94
|
grid-template-areas:
|
|
90
|
-
"
|
|
91
|
-
"
|
|
95
|
+
"preview"
|
|
96
|
+
"controls";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.shadow-backdrop-content {
|
|
100
|
+
overflow-x: auto;
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
</style>
|
|
@@ -268,6 +268,13 @@
|
|
|
268
268
|
align-items: stretch;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/* Sections stack one per line here, so each heading rule spans the card. */
|
|
272
|
+
@container variant-group (max-width: 32rem) {
|
|
273
|
+
.element-section {
|
|
274
|
+
flex: 1 1 100%;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
271
278
|
.element-section .state-type-groups {
|
|
272
279
|
display: flex;
|
|
273
280
|
flex-direction: row;
|
|
@@ -436,10 +436,9 @@
|
|
|
436
436
|
|
|
437
437
|
@container variant-group (max-width: 380px) {
|
|
438
438
|
.token-grid {
|
|
439
|
-
grid-template-columns: max-content 1fr;
|
|
439
|
+
grid-template-columns: max-content var(--token-selector-w) minmax(0, 1fr);
|
|
440
440
|
column-gap: var(--ui-space-6);
|
|
441
441
|
}
|
|
442
|
-
.token-grid :global(.ui-token-selector) { grid-column: 2; }
|
|
443
442
|
}
|
|
444
443
|
|
|
445
444
|
.zone-divider {
|
|
@@ -567,7 +567,7 @@
|
|
|
567
567
|
pinned and always leaves the controls a viewport to live in. */
|
|
568
568
|
.tabs-preview {
|
|
569
569
|
position: sticky;
|
|
570
|
-
top:
|
|
570
|
+
top: var(--editor-sticky-top, 0px);
|
|
571
571
|
z-index: 2;
|
|
572
572
|
display: flex;
|
|
573
573
|
flex-direction: column;
|
|
@@ -598,6 +598,20 @@
|
|
|
598
598
|
flex: none;
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
+
/* A capped stage nests a scroller inside the page scroll, which a phone
|
|
602
|
+
handles badly, and an uncapped pinned band covers the controls. The band
|
|
603
|
+
scrolls with the page at full height instead. */
|
|
604
|
+
@media (max-width: 600px) {
|
|
605
|
+
.tabs-preview {
|
|
606
|
+
position: relative;
|
|
607
|
+
max-height: none;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.tabs-preview::after {
|
|
611
|
+
display: none;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
601
615
|
/* Soft fade at the bottom of the sticky band so property rows scrolling
|
|
602
616
|
up don't sharply cut against the pinned preview. Greyscale (no accent).
|
|
603
617
|
Sits below the preview body, above scrolling content. */
|
|
@@ -910,8 +924,9 @@
|
|
|
910
924
|
/* Direct child of .variant-group; needs its own margin since flex gap doesn't apply across siblings. */
|
|
911
925
|
.properties-header {
|
|
912
926
|
display: flex;
|
|
927
|
+
flex-wrap: wrap;
|
|
913
928
|
align-items: center;
|
|
914
|
-
gap: var(--ui-space-16);
|
|
929
|
+
gap: var(--ui-space-8) var(--ui-space-16);
|
|
915
930
|
margin-top: var(--ui-space-8);
|
|
916
931
|
margin-bottom: var(--ui-space-8);
|
|
917
932
|
}
|
|
@@ -21,6 +21,7 @@ import { quietGet, quietSet } from '../storage/storage';
|
|
|
21
21
|
import { isGradientSlot, makeDefaultGradients } from '../themes/slices/gradients';
|
|
22
22
|
import { seedShadowsFromDom } from '../themes/slices/shadows';
|
|
23
23
|
import { makeDefaultWashScale, type WashFamily } from '../themes/slices/washes';
|
|
24
|
+
import { normalizeColumnsState } from '../themes/slices/columns';
|
|
24
25
|
import { sanitizeHarmonyAxes } from '../palettes/colorHarmony';
|
|
25
26
|
import {
|
|
26
27
|
renameBackgroundPaletteKey,
|
|
@@ -189,6 +190,10 @@ export function normalizePaletteBasis(state: EditorState): EditorState {
|
|
|
189
190
|
return { ...state, palettes: migratePaletteColorsToOklch(raw) };
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
export function normalizeColumns(state: EditorState): EditorState {
|
|
194
|
+
return { ...state, columns: normalizeColumnsState(state.columns) };
|
|
195
|
+
}
|
|
196
|
+
|
|
192
197
|
export function hydrate(): void {
|
|
193
198
|
// Corrupt state, missing key, or unavailable storage all return null;
|
|
194
199
|
// the editor falls through to the empty default in that case.
|
|
@@ -200,7 +205,7 @@ export function hydrate(): void {
|
|
|
200
205
|
store.set(
|
|
201
206
|
normalizeHarmonyAxes(
|
|
202
207
|
normalizeBaseAnchors(
|
|
203
|
-
normalizePaletteBasis(normalizePaletteLabels(normalizeComponents(normalizeWashes(migrateGradients(merged))))),
|
|
208
|
+
normalizePaletteBasis(normalizePaletteLabels(normalizeComponents(normalizeColumns(normalizeWashes(migrateGradients(merged)))))),
|
|
204
209
|
),
|
|
205
210
|
),
|
|
206
211
|
);
|
|
@@ -18,7 +18,6 @@ import { palettesToVars } from '../palettes/paletteDerivation';
|
|
|
18
18
|
import {
|
|
19
19
|
editorState,
|
|
20
20
|
columnsToVars,
|
|
21
|
-
columnsEqualsDefault,
|
|
22
21
|
washesToVars,
|
|
23
22
|
shadowsToVars,
|
|
24
23
|
gradientsToVars,
|
|
@@ -27,9 +26,7 @@ import {
|
|
|
27
26
|
|
|
28
27
|
export function deriveCssVars(state: EditorState): Record<string, string> {
|
|
29
28
|
const out: Record<string, string> = { ...state.cssVars };
|
|
30
|
-
|
|
31
|
-
Object.assign(out, columnsToVars(state.columns));
|
|
32
|
-
}
|
|
29
|
+
Object.assign(out, columnsToVars(state.columns));
|
|
33
30
|
Object.assign(out, washesToVars(state.washes));
|
|
34
31
|
if (state.shadows.tokens.length > 0) {
|
|
35
32
|
Object.assign(out, shadowsToVars(state.shadows));
|
|
@@ -56,8 +56,7 @@ import {
|
|
|
56
56
|
initializeEditorStore,
|
|
57
57
|
} from './editorPersistence';
|
|
58
58
|
import {
|
|
59
|
-
|
|
60
|
-
columnsEqualsDefault,
|
|
59
|
+
defaultColumns,
|
|
61
60
|
columnsToVars,
|
|
62
61
|
loadColumnsFromVars,
|
|
63
62
|
} from '../themes/slices/columns';
|
|
@@ -100,7 +99,7 @@ function emptyState(): EditorState {
|
|
|
100
99
|
overrides: {},
|
|
101
100
|
},
|
|
102
101
|
washes: makeDefaultWashesState(),
|
|
103
|
-
columns:
|
|
102
|
+
columns: defaultColumns(),
|
|
104
103
|
components: {},
|
|
105
104
|
gradients: { tokens: makeDefaultGradients() },
|
|
106
105
|
harmonyAxes: defaultHarmonyAxes(),
|
|
@@ -143,10 +142,9 @@ export {
|
|
|
143
142
|
|
|
144
143
|
export {
|
|
145
144
|
columnsToVars,
|
|
146
|
-
|
|
145
|
+
columnsFromVars,
|
|
147
146
|
COLUMN_VAR_NAMES,
|
|
148
|
-
|
|
149
|
-
parseColumnVars,
|
|
147
|
+
defaultColumns,
|
|
150
148
|
} from '../themes/slices/columns';
|
|
151
149
|
|
|
152
150
|
export {
|
|
@@ -430,9 +428,7 @@ export function loadFromFile(colorsAndType: ColorsAndType): void {
|
|
|
430
428
|
*/
|
|
431
429
|
function colorsAndTypeContent(state: EditorState): Omit<ColorsAndType, 'name' | 'createdAt' | 'updatedAt'> {
|
|
432
430
|
const cssVariables: Record<string, string> = { ...state.cssVars };
|
|
433
|
-
|
|
434
|
-
Object.assign(cssVariables, columnsToVars(state.columns));
|
|
435
|
-
}
|
|
431
|
+
Object.assign(cssVariables, columnsToVars(state.columns));
|
|
436
432
|
Object.assign(cssVariables, washesToVars(state.washes));
|
|
437
433
|
if (state.shadows.tokens.length > 0) {
|
|
438
434
|
Object.assign(cssVariables, shadowsToVars(state.shadows));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PaletteConfig, FontSource, FontStack } from '../themes/themeTypes';
|
|
2
2
|
import type { GradientStop, GradientValue, LinearDirection } from '../themes/parsers/gradient';
|
|
3
3
|
import type { HarmonyAxis } from '../palettes/colorHarmony';
|
|
4
|
+
import type { ColumnViewport, ColumnViewportValues } from '../themes/slices/columns';
|
|
4
5
|
|
|
5
6
|
export interface ShadowGlobals {
|
|
6
7
|
angle: number;
|
|
@@ -40,8 +41,7 @@ export interface WashScale {
|
|
|
40
41
|
export interface ColumnsState {
|
|
41
42
|
count: number;
|
|
42
43
|
maxWidth: number;
|
|
43
|
-
|
|
44
|
-
margin: number;
|
|
44
|
+
viewports: Record<ColumnViewport, ColumnViewportValues>;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** Gradient render mode.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The column grid's stored gutter and margin move to their desktop names
|
|
5
|
+
* (2026-10-15).
|
|
6
|
+
*
|
|
7
|
+
* tokens.css points `--columns-gutter` and `--columns-margin` at a
|
|
8
|
+
* per-viewport value for the window's width. A theme that stores either
|
|
9
|
+
* overrides every breakpoint, so each moves to its desktop name with its
|
|
10
|
+
* value. `--columns-count` stays one value at every width and keeps its name.
|
|
11
|
+
*
|
|
12
|
+
* A stored value equal to the one every shipped theme held (`12`, `32px`,
|
|
13
|
+
* `0px` or `0`) counts as unset and is dropped, so the theme inherits the
|
|
14
|
+
* tokens.css default. The theme stores no tablet or phone value. The names
|
|
15
|
+
* and values freeze those of 2026-10-15.
|
|
16
|
+
*/
|
|
17
|
+
const RENAMES: Record<string, string> = {
|
|
18
|
+
'--columns-gutter': '--columns-desktop-gutter',
|
|
19
|
+
'--columns-margin': '--columns-desktop-margin',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SHIPPED: Record<string, readonly string[]> = {
|
|
23
|
+
'--columns-count': ['12'],
|
|
24
|
+
'--columns-gutter': ['32px'],
|
|
25
|
+
'--columns-margin': ['0px', '0'],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const colorsAndTypeMigration_2026_10_15_responsiveColumns: Migration = {
|
|
29
|
+
id: '2026-10-15-responsive-columns',
|
|
30
|
+
fromVersion: 9,
|
|
31
|
+
toVersion: 10,
|
|
32
|
+
appliesTo: 'colors-and-type',
|
|
33
|
+
apply(rawVars) {
|
|
34
|
+
return Object.fromEntries(
|
|
35
|
+
Object.entries(rawVars)
|
|
36
|
+
.filter(([name, value]) => !SHIPPED[name]?.includes(value.trim()))
|
|
37
|
+
.map(([name, value]) => [RENAMES[name] ?? name, value]),
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -82,6 +82,7 @@ import { componentMigration_2026_09_13_sectiondividerSurface } from './2026-09-1
|
|
|
82
82
|
import { componentMigration_2026_09_20_imagelightboxScrim } from './2026-09-20-imagelightbox-scrim';
|
|
83
83
|
import { componentMigration_2026_09_20_scrimColorAndOpacity } from './2026-09-20-scrim-color-and-opacity';
|
|
84
84
|
import { colorsAndTypeMigration_2026_09_21_washColorAndOpacity } from './2026-09-21-wash-color-and-opacity';
|
|
85
|
+
import { colorsAndTypeMigration_2026_10_15_responsiveColumns } from './2026-10-15-responsive-columns';
|
|
85
86
|
import { componentMigration_2026_09_21_scrimSurface } from './2026-09-21-scrim-surface';
|
|
86
87
|
import { componentMigration_2026_09_13_cornerbadgePrefix } from './2026-09-13-cornerbadge-prefix';
|
|
87
88
|
import { componentMigration_2026_09_13_toggleLabel } from './2026-09-13-toggle-label';
|
|
@@ -140,6 +141,7 @@ export const MIGRATIONS: Migration[] = [
|
|
|
140
141
|
componentMigration_2026_09_20_scrimColorAndOpacity,
|
|
141
142
|
colorsAndTypeMigration_2026_09_21_washColorAndOpacity,
|
|
142
143
|
componentMigration_2026_09_21_scrimSurface,
|
|
144
|
+
colorsAndTypeMigration_2026_10_15_responsiveColumns,
|
|
143
145
|
];
|
|
144
146
|
|
|
145
147
|
function countFor(kind: 'colors-and-type' | 'component-config'): number {
|
|
@@ -1,59 +1,129 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Columns slice
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Columns slice. The column grid holds one count and one max width, and a
|
|
3
|
+
* gutter and a margin per viewport. `--columns-gutter` and `--columns-margin`
|
|
4
|
+
* are structural: tokens.css points them at the viewport values for the
|
|
5
|
+
* window's width, and the editor never writes them.
|
|
6
6
|
*/
|
|
7
|
-
import type { ColumnsState } from '../../store/editorTypes';
|
|
7
|
+
import type { ColumnsState, EditorState } from '../../store/editorTypes';
|
|
8
8
|
|
|
9
|
-
export const
|
|
9
|
+
export const COLUMN_VIEWPORTS = ['desktop', 'tablet', 'phone'] as const;
|
|
10
|
+
export type ColumnViewport = (typeof COLUMN_VIEWPORTS)[number];
|
|
10
11
|
|
|
11
|
-
export
|
|
12
|
+
export interface ColumnViewportValues {
|
|
13
|
+
gutter: number;
|
|
14
|
+
margin: number;
|
|
15
|
+
}
|
|
16
|
+
export type ColumnProperty = keyof ColumnViewportValues;
|
|
17
|
+
export const COLUMN_PROPERTIES: readonly ColumnProperty[] = ['gutter', 'margin'];
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
/** The widest window each narrower viewport covers, as tokens.css's `@media (max-width)` blocks declare it. */
|
|
20
|
+
export const COLUMN_BREAKPOINTS: Record<Exclude<ColumnViewport, 'desktop'>, number> = {
|
|
21
|
+
tablet: 767,
|
|
22
|
+
phone: 480,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const COLUMN_COUNT_NAME = '--columns-count';
|
|
26
|
+
export const COLUMN_MAX_WIDTH_NAME = '--columns-max-width';
|
|
27
|
+
|
|
28
|
+
export function columnVarName(viewport: ColumnViewport, property: ColumnProperty): string {
|
|
29
|
+
return `--columns-${viewport}-${property}`;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
export const VIEWPORT_COLUMN_NAMES: readonly string[] = COLUMN_VIEWPORTS.flatMap((viewport) =>
|
|
33
|
+
COLUMN_PROPERTIES.map((property) => columnVarName(viewport, property)),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export const STRUCTURAL_COLUMN_NAMES = ['--columns-gutter', '--columns-margin'] as const;
|
|
37
|
+
|
|
38
|
+
export const COLUMN_VAR_NAMES: readonly string[] = [COLUMN_COUNT_NAME, COLUMN_MAX_WIDTH_NAME, ...VIEWPORT_COLUMN_NAMES];
|
|
39
|
+
|
|
40
|
+
export const COLUMN_DEFAULTS: Readonly<ColumnsState> = {
|
|
41
|
+
count: 12,
|
|
42
|
+
maxWidth: 1440,
|
|
43
|
+
viewports: {
|
|
44
|
+
desktop: { gutter: 32, margin: 32 },
|
|
45
|
+
tablet: { gutter: 24, margin: 24 },
|
|
46
|
+
phone: { gutter: 16, margin: 16 },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function defaultColumns(): ColumnsState {
|
|
51
|
+
return structuredClone(COLUMN_DEFAULTS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Each name appears only when its value differs from the tokens.css default. */
|
|
55
|
+
export function columnsToVars(c: ColumnsState): Record<string, string> {
|
|
56
|
+
const out: Record<string, string> = {};
|
|
57
|
+
if (c.count !== COLUMN_DEFAULTS.count) out[COLUMN_COUNT_NAME] = String(c.count);
|
|
58
|
+
if (c.maxWidth !== COLUMN_DEFAULTS.maxWidth) out[COLUMN_MAX_WIDTH_NAME] = `${c.maxWidth}px`;
|
|
59
|
+
for (const viewport of COLUMN_VIEWPORTS) {
|
|
60
|
+
for (const property of COLUMN_PROPERTIES) {
|
|
61
|
+
const value = c.viewports[viewport][property];
|
|
62
|
+
if (value !== COLUMN_DEFAULTS.viewports[viewport][property]) {
|
|
63
|
+
out[columnVarName(viewport, property)] = `${value}px`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
44
67
|
return out;
|
|
45
68
|
}
|
|
46
69
|
|
|
70
|
+
function parseLength(raw: string | undefined): number | undefined {
|
|
71
|
+
const length = parseFloat(raw ?? '');
|
|
72
|
+
return Number.isFinite(length) ? Math.round(length) : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A name the bag lacks, or holds in a form the editor cannot edit, keeps its default. */
|
|
76
|
+
export function columnsFromVars(vars: Record<string, string>): ColumnsState {
|
|
77
|
+
const columns = defaultColumns();
|
|
78
|
+
const count = parseInt(vars[COLUMN_COUNT_NAME] ?? '', 10);
|
|
79
|
+
if (Number.isFinite(count) && count > 0) columns.count = count;
|
|
80
|
+
const maxWidth = parseLength(vars[COLUMN_MAX_WIDTH_NAME]);
|
|
81
|
+
if (maxWidth !== undefined) columns.maxWidth = maxWidth;
|
|
82
|
+
for (const viewport of COLUMN_VIEWPORTS) {
|
|
83
|
+
for (const property of COLUMN_PROPERTIES) {
|
|
84
|
+
const value = parseLength(vars[columnVarName(viewport, property)]);
|
|
85
|
+
if (value !== undefined) columns.viewports[viewport][property] = value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return columns;
|
|
89
|
+
}
|
|
90
|
+
|
|
47
91
|
/**
|
|
48
92
|
* Loader: route the relevant entries from a freshly-loaded theme's vars bag
|
|
49
93
|
* into `next.columns` and remove them from the bag so derivation stays
|
|
50
94
|
* single-source. Mutates `next` and `rawVars` in place.
|
|
51
95
|
*/
|
|
52
|
-
export function loadColumnsFromVars(
|
|
53
|
-
next
|
|
54
|
-
rawVars: Record<string, string>,
|
|
55
|
-
): void {
|
|
56
|
-
const overrides = parseColumnVars(rawVars);
|
|
57
|
-
next.columns = { ...DEFAULT_COLUMNS, ...overrides };
|
|
96
|
+
export function loadColumnsFromVars(next: EditorState, rawVars: Record<string, string>): void {
|
|
97
|
+
next.columns = columnsFromVars(rawVars);
|
|
58
98
|
for (const name of COLUMN_VAR_NAMES) delete rawVars[name];
|
|
59
99
|
}
|
|
100
|
+
|
|
101
|
+
interface FlatColumnsState {
|
|
102
|
+
count: number;
|
|
103
|
+
maxWidth: number;
|
|
104
|
+
gutter: number;
|
|
105
|
+
margin: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The flat state's old defaults rendered nothing, so a session sitting on them
|
|
109
|
+
// takes today's defaults instead of pinning a 16px desktop gutter.
|
|
110
|
+
const FLAT_DEFAULTS: FlatColumnsState = { count: 12, maxWidth: 1440, gutter: 16, margin: 0 };
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A session persisted before the per-viewport state holds one flat set, which
|
|
114
|
+
* the renderer wrote to the names that are now the count, the max width and
|
|
115
|
+
* the desktop gutter and margin. Its `0` margin is the one every shipped theme
|
|
116
|
+
* held, so it takes today's default as a stored theme's does.
|
|
117
|
+
*/
|
|
118
|
+
export function normalizeColumnsState(raw: ColumnsState | FlatColumnsState): ColumnsState {
|
|
119
|
+
if ('viewports' in raw) return raw;
|
|
120
|
+
const columns = defaultColumns();
|
|
121
|
+
const isFlatDefault = (Object.keys(FLAT_DEFAULTS) as (keyof FlatColumnsState)[])
|
|
122
|
+
.every((key) => raw[key] === FLAT_DEFAULTS[key]);
|
|
123
|
+
if (isFlatDefault) return columns;
|
|
124
|
+
columns.count = raw.count;
|
|
125
|
+
columns.maxWidth = raw.maxWidth;
|
|
126
|
+
columns.viewports.desktop.gutter = raw.gutter;
|
|
127
|
+
if (raw.margin !== FLAT_DEFAULTS.margin) columns.viewports.desktop.margin = raw.margin;
|
|
128
|
+
return columns;
|
|
129
|
+
}
|
|
@@ -94,16 +94,7 @@
|
|
|
94
94
|
{
|
|
95
95
|
path: 'guide',
|
|
96
96
|
title: 'Guide',
|
|
97
|
-
items:
|
|
98
|
-
{ path: '01-overview', title: 'Overview' },
|
|
99
|
-
{ path: 'getting-started', title: 'Getting started' },
|
|
100
|
-
{ path: 'editing-tokens', title: 'Editing tokens' },
|
|
101
|
-
{ path: 'sketch-mode', title: 'Sketch mode' },
|
|
102
|
-
{ path: 'themes-workflow', title: 'Themes' },
|
|
103
|
-
{ path: 'where-themes-live', title: 'Where themes live' },
|
|
104
|
-
{ path: 'light-and-dark', title: 'Light and dark' },
|
|
105
|
-
{ path: 'creating-components', title: 'Creating components' },
|
|
106
|
-
],
|
|
97
|
+
items: chapters.map((c) => ({ path: c.id, title: c.title })),
|
|
107
98
|
},
|
|
108
99
|
];
|
|
109
100
|
|
|
@@ -368,18 +359,27 @@
|
|
|
368
359
|
color: var(--text-primary);
|
|
369
360
|
min-height: 100vh;
|
|
370
361
|
}
|
|
362
|
+
/* The open rail spans three columns of the page grid and the closed rail one.
|
|
363
|
+
A percentage resolves against the shell's content box, and the rail's own
|
|
364
|
+
width tokens resolve 100cqw against the same box, so track and rail tween
|
|
365
|
+
together. The rail's width excludes its border-right. */
|
|
371
366
|
.docs-shell {
|
|
367
|
+
--docs-column: calc((100% - (var(--columns-count) - 1) * var(--columns-gutter)) / var(--columns-count));
|
|
368
|
+
--docs-rail-column: calc((100cqw - (var(--columns-count) - 1) * var(--columns-gutter)) / var(--columns-count));
|
|
369
|
+
--sidenavigation-panel-open-width: calc(3 * var(--docs-rail-column) + 2 * var(--columns-gutter) - var(--sidenavigation-panel-border-width));
|
|
370
|
+
--sidenavigation-panel-closed-width: calc(var(--docs-rail-column) - var(--sidenavigation-panel-border-width));
|
|
371
|
+
container-type: inline-size;
|
|
372
372
|
display: grid;
|
|
373
|
-
grid-template-columns: var(--
|
|
373
|
+
grid-template-columns: calc(3 * var(--docs-column) + 2 * var(--columns-gutter)) minmax(0, 1fr);
|
|
374
|
+
column-gap: var(--columns-gutter);
|
|
375
|
+
box-sizing: content-box;
|
|
374
376
|
max-width: var(--columns-max-width, 1440px);
|
|
375
377
|
margin: 0 auto;
|
|
376
|
-
padding:
|
|
378
|
+
padding-inline: var(--columns-margin);
|
|
377
379
|
transition: grid-template-columns var(--duration-200, 200ms) var(--ease-in-out-sine, ease);
|
|
378
380
|
}
|
|
379
|
-
/* The rail's own width tween (its width tokens) drives the column; keep the
|
|
380
|
-
grid track locked to the closed width so content reflows in step. */
|
|
381
381
|
.docs-shell.collapsed {
|
|
382
|
-
grid-template-columns: var(--
|
|
382
|
+
grid-template-columns: var(--docs-column) minmax(0, 1fr);
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
.docs-main {
|
|
@@ -390,7 +390,7 @@
|
|
|
390
390
|
/* -------------------------------------------------------------- Top header */
|
|
391
391
|
.docs-page-header {
|
|
392
392
|
border-bottom: var(--border-width-1, 1px) solid var(--border-neutral-faint, #1c2327);
|
|
393
|
-
padding: var(--space-48, 3rem) 0 var(--space-32, 2rem)
|
|
393
|
+
padding: var(--space-48, 3rem) 0 var(--space-32, 2rem);
|
|
394
394
|
background: linear-gradient(
|
|
395
395
|
180deg,
|
|
396
396
|
color-mix(in srgb, var(--surface-neutral-lower, #162027) 50%, transparent),
|
|
@@ -471,7 +471,7 @@
|
|
|
471
471
|
min-width: 0;
|
|
472
472
|
max-width: 760px;
|
|
473
473
|
outline: none;
|
|
474
|
-
padding: var(--space-32, 2rem) 0 0
|
|
474
|
+
padding: var(--space-32, 2rem) 0 0;
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
.loading-row {
|
|
@@ -664,12 +664,15 @@
|
|
|
664
664
|
.docs-shell :global(.docs-sidebar) { height: 100%; }
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
-
/* Below 960px the rail stacks above the content; the page
|
|
667
|
+
/* Below 960px the open rail stacks full width above the content; the page
|
|
668
|
+
scrolls naturally. */
|
|
668
669
|
@media (max-width: 960px) {
|
|
670
|
+
.docs-shell { --sidenavigation-panel-open-width: calc(100cqw - var(--sidenavigation-panel-border-width)); }
|
|
669
671
|
.docs-shell,
|
|
670
|
-
.docs-shell.collapsed {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
.docs-shell
|
|
672
|
+
.docs-shell.collapsed {
|
|
673
|
+
grid-template-columns: repeat(var(--columns-count), minmax(0, 1fr));
|
|
674
|
+
}
|
|
675
|
+
.docs-shell > :global(.docs-sidebar),
|
|
676
|
+
.docs-main { grid-column: 1 / -1; }
|
|
674
677
|
}
|
|
675
678
|
</style>
|
|
@@ -18,6 +18,7 @@ export const chapters: Chapter[] = [
|
|
|
18
18
|
{ id: 'where-themes-live', title: 'Where themes live' },
|
|
19
19
|
{ id: 'light-and-dark', title: 'Light and dark' },
|
|
20
20
|
{ id: 'creating-components', title: 'Creating components' },
|
|
21
|
+
{ id: 'production-css', title: 'Production CSS' },
|
|
21
22
|
];
|
|
22
23
|
|
|
23
24
|
export const chapterIds = chapters.map((c) => c.id);
|
|
@@ -68,9 +68,11 @@ Change a step and every element using it repaints.
|
|
|
68
68
|
|
|
69
69
|
## Columns
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
Pages lay out on the column grid. The column count and the max width hold at
|
|
72
|
+
every width. Desktop, Tablet, and Phone each set a gutter and an outer margin,
|
|
73
|
+
and the grid takes the tablet values at 767px and narrower and the phone values
|
|
74
|
+
at 480px and narrower. The columns button in the editor bar draws the grid over
|
|
75
|
+
the page, and a page on the grid reflows as you edit.
|
|
74
76
|
|
|
75
77
|
## Saving
|
|
76
78
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Production CSS
|
|
2
|
+
|
|
3
|
+
In dev, every design token sits on `:root` as a CSS variable, because the
|
|
4
|
+
editor can rewrite any of them while the page runs. A production build runs no
|
|
5
|
+
editor, so most of those values never change. `vite build` rewrites the CSS it
|
|
6
|
+
emits to match: a token stays a variable only where its value can change at
|
|
7
|
+
runtime, and every other `var()` gives way to the value it reads.
|
|
8
|
+
|
|
9
|
+
The dev server keeps every variable. Your source files keep every token too:
|
|
10
|
+
the build rewrites only the files it emits, so `tokens.css`,
|
|
11
|
+
`tokens.generated.css` and your components still name each token, and the
|
|
12
|
+
token names stay the API you write against.
|
|
13
|
+
|
|
14
|
+
## The build step
|
|
15
|
+
|
|
16
|
+
`themeFileApi` runs the step at the end of every `vite build`, after Vite has
|
|
17
|
+
bundled and minified the CSS. It reads every stylesheet and script in the
|
|
18
|
+
bundle, including lazy chunks, and then:
|
|
19
|
+
|
|
20
|
+
- **removes** each custom property that no rule reads, directly or through
|
|
21
|
+
another property;
|
|
22
|
+
- **substitutes** each `var()` that reads a static property with the text of
|
|
23
|
+
that property's value, so `calc()` and `color-mix()` still resolve on the
|
|
24
|
+
element that uses them;
|
|
25
|
+
- **declares** each remaining property once on `:root`, with the value that
|
|
26
|
+
wins the cascade, so DevTools lists it once.
|
|
27
|
+
|
|
28
|
+
The build prints one line with the result:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
live-tokens: the production CSS declares 76 of 848 custom properties and is 61.3 kB smaller.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A file's hashed name follows its content. Each stylesheet the build rewrites
|
|
35
|
+
takes a new hash, and so does each script that loads it, so a cache that keeps
|
|
36
|
+
hashed files forever serves each release its own files. A file name pattern
|
|
37
|
+
without `[hash]` keeps every name.
|
|
38
|
+
|
|
39
|
+
## The dynamic rule
|
|
40
|
+
|
|
41
|
+
A property stays a variable when its value can change while the page runs, or
|
|
42
|
+
when the build cannot prove it never does. A property that changes and loses
|
|
43
|
+
its variable breaks a page, and an extra variable costs a few bytes, so a case
|
|
44
|
+
that could fall either way stays a variable. A property is **dynamic** when any
|
|
45
|
+
of these hold:
|
|
46
|
+
|
|
47
|
+
- A rule declares it under a selector besides a root selector such as `:root`
|
|
48
|
+
or `:root:root`, or inside `@media`, `@supports` or `@container`. The size
|
|
49
|
+
tokens and the column gutter and margin in the three responsive `@media`
|
|
50
|
+
blocks of `tokens.css` stay variables this way.
|
|
51
|
+
- Its name appears in a script or HTML page the build emits. That covers
|
|
52
|
+
`style:--name` directives, `setProperty`, `getPropertyValue` and inline
|
|
53
|
+
`style` attributes. Card sets `--card-hover-border-enabled` through a
|
|
54
|
+
`style:` directive, so the token stays a variable.
|
|
55
|
+
- Its name appears in an at-rule's prelude, such as a `style()` container
|
|
56
|
+
query.
|
|
57
|
+
- An `@property` rule registers it.
|
|
58
|
+
- The `keep` option lists it.
|
|
59
|
+
- Its value reads a dynamic property.
|
|
60
|
+
- Its winning value is a CSS-wide keyword such as `initial`, or it reads
|
|
61
|
+
itself through a loop of `var()` references.
|
|
62
|
+
- Two stylesheets give it different `:root` values.
|
|
63
|
+
- Its value reads a name that no stylesheet declares and no script or `keep`
|
|
64
|
+
entry names.
|
|
65
|
+
- Only a lazily loaded stylesheet declares it, and another stylesheet reads
|
|
66
|
+
it.
|
|
67
|
+
|
|
68
|
+
Every other property is **static**: one value on `:root` for the life of the
|
|
69
|
+
page.
|
|
70
|
+
|
|
71
|
+
## Computed names
|
|
72
|
+
|
|
73
|
+
The build finds a name in a script only when the script spells it out. A
|
|
74
|
+
script that assembles a name while the page runs, such as
|
|
75
|
+
`` `--color-${family}-500` ``, hides it. The build then substitutes or removes
|
|
76
|
+
that token, so a read returns an empty string and a write changes nothing. List
|
|
77
|
+
each such name under `keep`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
themeFileApi({
|
|
81
|
+
tokensCssPath: 'src/system/styles/tokens.css',
|
|
82
|
+
productionCss: { keep: ['--color-brand-500', '--color-accent-500'] },
|
|
83
|
+
}),
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`keep` takes exact names. The same goes for a stylesheet outside the bundle,
|
|
87
|
+
such as one in `public/`: the build never reads it, so list each token it
|
|
88
|
+
reads under `keep`.
|
|
89
|
+
|
|
90
|
+
## Options
|
|
91
|
+
|
|
92
|
+
`productionCss` on `themeFileApi` sets the step:
|
|
93
|
+
|
|
94
|
+
- **Default.** The build removes, substitutes and declares, as above.
|
|
95
|
+
- **`productionCss: { keep: [...] }`.** Each listed name stays a variable.
|
|
96
|
+
- **`productionCss: { substitute: false }`.** The build removes properties no
|
|
97
|
+
rule reads and keeps every other `var()`, so DevTools shows token names while
|
|
98
|
+
you debug. It still declares each property once on `:root`.
|
|
99
|
+
- **`productionCss: false`.** The build keeps every variable, as dev does.
|
|
100
|
+
|
|
101
|
+
`keep` and `substitute` combine in one object. When a page renders differently
|
|
102
|
+
in production, build it with `productionCss: false`. A difference that goes
|
|
103
|
+
away points at a token the rule missed, and `keep` fixes it.
|
|
104
|
+
|
|
105
|
+
## Limits
|
|
106
|
+
|
|
107
|
+
- **Values a property rejects.** In dev, `width: var(--color-brand-500)` passes
|
|
108
|
+
the parser, and when the color reaches `width` the browser treats the
|
|
109
|
+
declaration as `unset`. After substitution, `width` holds a color, the parser
|
|
110
|
+
drops the declaration, and an earlier `width` rule applies. Keep each token's
|
|
111
|
+
value valid for every property that reads it.
|
|
112
|
+
- **Fixed stylesheet names.** The build renames each stylesheet it rewrites
|
|
113
|
+
through the hash in the build's `assetFileNames` pattern. A plugin that emits
|
|
114
|
+
a stylesheet under a fixed `fileName` breaks that pattern, and the build
|
|
115
|
+
stops with `live-tokens: <file> does not follow the output pattern
|
|
116
|
+
"<pattern>"`. Set `productionCss: false` for that build.
|