@motion-proto/live-tokens 0.94.3 → 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 +97 -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/Demo.svelte +39 -5
- package/src/demo/DemoBar.svelte +23 -5
- package/src/demo/FloatingTokenTags.css +33 -4
- package/src/demo/FloatingTokenTags.svelte +81 -68
- package/src/demo/Section.svelte +1 -7
- package/src/demo/SourceLinks.svelte +5 -4
- package/src/demo/TestingLoops.svelte +128 -19
- 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/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
|
@@ -17,13 +17,15 @@
|
|
|
17
17
|
.offering {
|
|
18
18
|
grid-column: 1 / -1;
|
|
19
19
|
display: grid;
|
|
20
|
-
grid-template-columns:
|
|
21
|
-
column-gap: var(--columns-gutter);
|
|
20
|
+
grid-template-columns: subgrid;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
.offering-quote {
|
|
25
|
-
grid-column:
|
|
24
|
+
grid-column: var(--kit-narrow);
|
|
25
|
+
justify-self: center;
|
|
26
|
+
max-width: 24em;
|
|
26
27
|
margin: 0;
|
|
28
|
+
font-size: var(--heading-lg-font-size);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
.offering-quote blockquote {
|
|
@@ -38,12 +40,13 @@
|
|
|
38
40
|
|
|
39
41
|
.offering-quote p {
|
|
40
42
|
font-family: var(--font-serif);
|
|
41
|
-
font-size: var(--font-size
|
|
43
|
+
font-size: var(--heading-lg-font-size);
|
|
42
44
|
font-weight: var(--font-weight-normal);
|
|
43
45
|
color: var(--text-primary);
|
|
44
46
|
line-height: 1.2;
|
|
45
47
|
margin: 0;
|
|
46
48
|
hyphens: none;
|
|
49
|
+
text-wrap: pretty;
|
|
47
50
|
text-indent: -0.55em;
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -65,29 +68,11 @@
|
|
|
65
68
|
margin-top: 0;
|
|
66
69
|
font-family: var(--font-serif);
|
|
67
70
|
font-weight: var(--font-weight-normal);
|
|
68
|
-
font-size: var(--font-size
|
|
71
|
+
font-size: var(--heading-lg-font-size);
|
|
69
72
|
font-variation-settings: 'opsz' 144, 'SOFT' 100;
|
|
70
73
|
letter-spacing: 0.01em;
|
|
71
74
|
color: var(--text-primary);
|
|
72
75
|
text-align: right;
|
|
73
76
|
padding-right: 0.4em;
|
|
74
77
|
}
|
|
75
|
-
|
|
76
|
-
@media (max-width: 960px) {
|
|
77
|
-
.offering-quote {
|
|
78
|
-
grid-column: 2 / -2;
|
|
79
|
-
}
|
|
80
|
-
.offering-quote p {
|
|
81
|
-
font-size: var(--font-size-xl);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@media (max-width: 600px) {
|
|
86
|
-
.offering-quote {
|
|
87
|
-
grid-column: 1 / -1;
|
|
88
|
-
}
|
|
89
|
-
.offering-quote p {
|
|
90
|
-
font-size: var(--font-size-4xl);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
78
|
</style>
|
package/src/editor/bootstrap.ts
CHANGED
|
@@ -18,11 +18,8 @@ import { mount, type Component } from 'svelte';
|
|
|
18
18
|
import * as cssVarSync from './core/cssVarSync';
|
|
19
19
|
import * as router from './core/routing/router';
|
|
20
20
|
import * as columnsOverlay from './overlay/columnsOverlay';
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import * as liveStateStream from './core/themes/liveStateStream';
|
|
24
|
-
import { initializeTheme } from './core/themes/themeInit';
|
|
25
|
-
import { registerComponent, type RegisterComponentEntry } from './component-editor/registry';
|
|
21
|
+
import { registerComponent } from './component-editor/registration';
|
|
22
|
+
import type { RegisterComponentEntry } from './component-editor/registry';
|
|
26
23
|
import { registerSketchStyle, type RegisterSketchStyleInput } from './core/sketch/sketchRegistry';
|
|
27
24
|
|
|
28
25
|
export interface BootLiveTokensOptions {
|
|
@@ -43,7 +40,6 @@ export async function bootLiveTokens(
|
|
|
43
40
|
cssVarSync.init();
|
|
44
41
|
router.init();
|
|
45
42
|
columnsOverlay.init();
|
|
46
|
-
editorStore.init();
|
|
47
43
|
|
|
48
44
|
// Before the DEV block: `initializeTheme` recovers the open theme's style by
|
|
49
45
|
// name against the pool, so the pool has to be whole by the time it runs.
|
|
@@ -52,6 +48,16 @@ export async function bootLiveTokens(
|
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
if (import.meta.env.DEV) {
|
|
51
|
+
// Imported here, never statically, so a build drops the editor. The
|
|
52
|
+
// registry alone imports every shipped component and editor.
|
|
53
|
+
const [editorStore, themeDocumentSync, liveStateStream, { initializeTheme }] = await Promise.all([
|
|
54
|
+
import('./core/store/editorStore'),
|
|
55
|
+
import('./core/themes/themeDocumentSync'),
|
|
56
|
+
import('./core/themes/liveStateStream'),
|
|
57
|
+
import('./core/themes/themeInit'),
|
|
58
|
+
import('./component-editor/registry'),
|
|
59
|
+
]);
|
|
60
|
+
editorStore.init();
|
|
55
61
|
themeDocumentSync.init();
|
|
56
62
|
liveStateStream.init();
|
|
57
63
|
if (opts.components) {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
import VariantGroup from './scaffolding/VariantGroup.svelte';
|
|
51
51
|
import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
|
|
52
52
|
import { buildSiblings } from './scaffolding/siblings';
|
|
53
|
-
import demoImageUrl from '../../system/assets/
|
|
53
|
+
import demoImageUrl from '../../system/assets/newspaper.webp';
|
|
54
54
|
import { editorState, setComponentAlias } from '../core/store/editorStore';
|
|
55
55
|
|
|
56
56
|
let aliases = $derived(
|
|
@@ -45,13 +45,12 @@
|
|
|
45
45
|
import ImageLightbox from '../../system/components/ImageLightbox.svelte';
|
|
46
46
|
import VariantGroup from './scaffolding/VariantGroup.svelte';
|
|
47
47
|
import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
|
|
48
|
-
import offeringUrl from '../../system/assets/offering.webp';
|
|
49
48
|
import newspaperUrl from '../../system/assets/newspaper.webp';
|
|
50
49
|
|
|
51
50
|
const demoImages = [
|
|
52
|
-
//
|
|
53
|
-
// them to exercise self-measure from the loaded image.
|
|
54
|
-
{ src:
|
|
51
|
+
// The first entry carries explicit dimensions (the no-reflow path); the second
|
|
52
|
+
// omits them to exercise self-measure from the loaded image.
|
|
53
|
+
{ src: newspaperUrl, alt: 'Newspaper', width: 2048, height: 1152 },
|
|
55
54
|
{ src: newspaperUrl, alt: 'Newspaper' },
|
|
56
55
|
];
|
|
57
56
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RegisterComponentEntry } from './registry';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* `registerComponent` lives apart from `registry.ts` so a production build can
|
|
5
|
+
* call it without bundling the registry, which imports every shipped component
|
|
6
|
+
* and editor. Entries wait here until the registry loads, which happens in dev
|
|
7
|
+
* only.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type Sink = (entry: RegisterComponentEntry) => void;
|
|
11
|
+
|
|
12
|
+
const pending: RegisterComponentEntry[] = [];
|
|
13
|
+
let sink: Sink | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a consumer-authored component with the editor. Call from `main.ts`
|
|
17
|
+
* before app mount.
|
|
18
|
+
*
|
|
19
|
+
* Collision rule: if `entry.id` matches a built-in id, a warning is logged and
|
|
20
|
+
* the custom entry wins (the custom editor and schema replace the built-in for
|
|
21
|
+
* the rest of the session).
|
|
22
|
+
*
|
|
23
|
+
* Side effect: registers the schema with the editor store so reset-to-default
|
|
24
|
+
* and sibling-group resolution work for the new component.
|
|
25
|
+
*/
|
|
26
|
+
export function registerComponent(entry: RegisterComponentEntry): void {
|
|
27
|
+
if (sink) sink(entry);
|
|
28
|
+
else pending.push(entry);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Called once by `registry.ts` as it loads. Drains the entries registered
|
|
32
|
+
before it, in call order, and takes every later one directly. */
|
|
33
|
+
export function attachRegistry(next: Sink): void {
|
|
34
|
+
sink = next;
|
|
35
|
+
for (const entry of pending.splice(0)) next(entry);
|
|
36
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Component } from 'svelte';
|
|
2
2
|
import type { CatalogueEntry, Token, IntrinsicSpec } from './scaffolding/types';
|
|
3
3
|
import { registerComponentSchema } from '../core/store/editorStore';
|
|
4
|
+
import { attachRegistry } from './registration';
|
|
5
|
+
|
|
6
|
+
export { registerComponent } from './registration';
|
|
4
7
|
|
|
5
8
|
import BadgeEditor, { allTokens as badgeTokens } from './BadgeEditor.svelte';
|
|
6
9
|
import { catalogue as badgeCatalogue } from '../../system/components/Badge.svelte';
|
|
@@ -433,18 +436,7 @@ const customRegistry = new Map<string, RegistryEntry>();
|
|
|
433
436
|
/** Argument shape for `registerComponent()`. `origin` is set internally to `'custom'`. */
|
|
434
437
|
export type RegisterComponentEntry = Omit<RegistryEntry, 'origin'>;
|
|
435
438
|
|
|
436
|
-
|
|
437
|
-
* Register a consumer-authored component at runtime. Call from `main.ts`
|
|
438
|
-
* before app mount.
|
|
439
|
-
*
|
|
440
|
-
* Collision rule: if `entry.id` matches a built-in id, a warning is logged and
|
|
441
|
-
* the custom entry wins (the custom editor and schema replace the built-in for
|
|
442
|
-
* the rest of the session).
|
|
443
|
-
*
|
|
444
|
-
* Side effect: registers the schema with the editor store so reset-to-default
|
|
445
|
-
* and sibling-group resolution work for the new component.
|
|
446
|
-
*/
|
|
447
|
-
export function registerComponent(entry: RegisterComponentEntry): void {
|
|
439
|
+
function addCustomComponent(entry: RegisterComponentEntry): void {
|
|
448
440
|
if (entry.id in builtInRegistry) {
|
|
449
441
|
console.warn(
|
|
450
442
|
`[registerComponent] custom component "${entry.id}" overrides a built-in. The custom editor will be used.`,
|
|
@@ -496,6 +488,7 @@ export function getComponentIds(): ReadonlyArray<string> {
|
|
|
496
488
|
for (const entry of Object.values(builtInRegistry)) {
|
|
497
489
|
registerComponentSchema(entry.id, entry.schema);
|
|
498
490
|
}
|
|
491
|
+
attachRegistry(addCustomComponent);
|
|
499
492
|
|
|
500
493
|
/**
|
|
501
494
|
* Validate that the server's filesystem scan matches the merged registry's id list.
|
|
@@ -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
|
+
}
|