@motion-proto/live-tokens 0.49.0 → 0.50.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/CHANGELOG.md +36 -0
- package/dist-plugin/generateColorsAndType/index.cjs +13 -2
- package/dist-plugin/generateColorsAndType/index.js +13 -2
- package/dist-plugin/index.cjs +2 -0
- package/dist-plugin/index.js +2 -0
- package/package.json +6 -1
- package/src/editor/bootstrap.ts +2 -0
- package/src/editor/component-editor/ButtonEditor.svelte +1 -1
- package/src/editor/component-editor/CardEditor.svelte +5 -2
- package/src/editor/component-editor/IconButtonEditor.svelte +1 -1
- package/src/editor/component-editor/ImageEditor.svelte +1 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +4 -1
- package/src/editor/component-editor/SideNavigationEditor.svelte +6 -0
- package/src/editor/component-editor/TabBarEditor.svelte +1 -5
- package/src/editor/component-editor/scaffolding/ComponentEditorBase.svelte +8 -1
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -32
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +2 -0
- package/src/editor/component-editor/scaffolding/TypeEditor.svelte +3 -1
- package/src/editor/component-editor/scaffolding/types.ts +3 -0
- package/src/editor/core/components/componentConfigService.ts +25 -0
- package/src/editor/core/cssVarSync.ts +65 -7
- package/src/editor/core/fonts/fontLoader.ts +12 -3
- package/src/editor/core/fonts/fontWeightAvailability.ts +97 -0
- package/src/editor/core/preview/lookPreview.ts +23 -18
- package/src/editor/core/store/editorRenderer.ts +22 -7
- package/src/editor/core/store/editorStore.ts +31 -0
- package/src/editor/core/store/gradientSource.ts +5 -0
- package/src/editor/core/themes/colorsAndTypeService.ts +2 -12
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +4 -1
- package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +4 -1
- package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +8 -0
- package/src/editor/core/themes/slices/fonts.ts +4 -5
- package/src/editor/core/themes/themeDocumentSync.ts +66 -0
- package/src/editor/core/themes/themeInit.ts +7 -10
- package/src/editor/core/themes/themeService.ts +10 -3
- package/src/editor/docs/content/themes-workflow.md +14 -11
- package/src/editor/docs/content/where-themes-live.md +7 -2
- package/src/editor/docs/content.generated.ts +2 -2
- package/src/editor/ui/FileLoadList.svelte +1 -0
- package/src/editor/ui/FontStackEditor.svelte +1 -2
- package/src/editor/ui/GradientEditor.svelte +5 -1
- package/src/editor/ui/ProjectFontsSection.svelte +0 -5
- package/src/editor/ui/ThemePanel.svelte +206 -42
- package/src/editor/ui/UIDialog.svelte +7 -1
- package/src/editor/ui/UIFontFamilySelector.svelte +13 -13
- package/src/editor/ui/UIFontSizeSelector.svelte +5 -5
- package/src/editor/ui/UIFontWeightSelector.svelte +52 -2
- package/src/editor/ui/UIOptionItem.svelte +14 -1
- package/src/editor/ui/UIPaddingSelector.svelte +4 -3
- package/src/editor/ui/UIPaletteSelector.svelte +20 -13
- package/src/editor/ui/UIRelinkConfirmDialog.svelte +18 -3
- package/src/editor/ui/UITextTransformSelector.svelte +5 -5
- package/src/editor/ui/UITokenSelector.svelte +29 -7
- package/src/live-tokens/data/themes/spring-meadow.json +14 -12
- package/src/system/components/Button.svelte +2 -1
- package/src/system/components/FloatingTokenTags.css +2 -1
- package/src/system/components/FloatingTokenTags.svelte +11 -0
- package/src/system/components/IconButton.svelte +2 -1
- package/src/system/components/Image.svelte +9 -3
- package/src/system/components/ImageLightbox.svelte +2 -0
- package/src/system/components/SectionDivider.svelte +20 -2
- package/src/system/components/SideNavigation.svelte +15 -1
- package/src/system/styles/fonts.css +6 -6
|
@@ -34,8 +34,8 @@ deleting anything else never breaks it.
|
|
|
34
34
|
|
|
35
35
|
- **Editing** changes the page through CSS variables. The editor keeps your
|
|
36
36
|
edits in the browser as you work and writes them to the `_working.json`
|
|
37
|
-
buffers when you save a component.
|
|
38
|
-
|
|
37
|
+
buffers when you save a component. When the Theme panel finds several dirty
|
|
38
|
+
components, **Save all** writes those buffers together.
|
|
39
39
|
- **Save** captures the buffers into the open theme's file. That file is the
|
|
40
40
|
durable copy of your look; matching buffers are then removed.
|
|
41
41
|
- **Load** clears the buffers and points `themes/_active.json` at the theme you
|
|
@@ -49,6 +49,11 @@ The `default.json` files are the shipped baseline. The editor derives them at
|
|
|
49
49
|
boot and refreshes them when the package updates; it never saves your work
|
|
50
50
|
over them.
|
|
51
51
|
|
|
52
|
+
Projects upgraded from 0.48 may initially contain working files copied from the
|
|
53
|
+
active theme. On the first dev-server boot, exact copies are removed
|
|
54
|
+
automatically. Any file that differs is kept as unsaved work, so no migration
|
|
55
|
+
command is required.
|
|
56
|
+
|
|
52
57
|
## What to commit
|
|
53
58
|
|
|
54
59
|
All of it. The data tree is designed to live in git: themes diff readably, the
|
|
@@ -6,6 +6,6 @@ export const docContent: Record<string, string> = {
|
|
|
6
6
|
"creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
|
|
7
7
|
"editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has two views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Two curves shape how lightness and saturation fall off across the\n ramp. Drag the handles to bias it darker, lighter, or more saturated.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Overlays and gradients\n\n- **Overlays** are translucent tints layered over surfaces, like the subtle\n tint a card gets on hover. Set a colour and opacity per state.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nThe page-grid overlay. Set column count, gutter, and outer margin, and toggle\nthe visual guide with `Cmd/Ctrl+G`. Pages built on the column system reflow\nlive.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
|
|
8
8
|
"getting-started": "# Getting started\n\nScaffold a live token site in a moments. You need Node 20 or later, a\npackage manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.\n\n## Scaffold a new app\n\n```bash\nnpm create @motion-proto/live-tokens@latest my-app\ncd my-app\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints (usually `http://localhost:5173`). You get a\none-page Svelte + Vite app that depends on the published package, with the\neditor wired up and the full component set ready to import.\n\n`npx @motion-proto/live-tokens create my-app` runs the same scaffold without\nthe initialiser package.\n\n### What the scaffold gives you\n\nEvery editable file lives under `src/` and is committed, so `npm install` and\nversion upgrades never touch your styles. The package code stays in\n`node_modules`.\n\n| Path | What it is |\n|------|------------|\n| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |\n| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds dev-only routes under a reserved `/live-tokens/*` namespace: `/live-tokens/editor`, `/live-tokens/components`, and `/live-tokens/docs`. |\n| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |\n| `src/styles/site.css` | Themed page typography, yours to edit. |\n\n## Your first edit\n\n1. Run `npm run dev` and open the home page.\n2. Click **Open Token Editor**, or visit `/live-tokens/editor`. The editor opens beside\n the page.\n3. Open **Palettes**, pick **Brand**, and change the base hex. The page\n repaints as you type.\n4. In the **Theme** panel at the foot of the sidebar, choose **Save As**. Your\n theme appears as JSON under `src/live-tokens/data/themes/`.\n5. Reload. The editor reopens on your theme, so the page returns as you left\n it.\n\n## What you just changed\n\nEvery edit sets a CSS custom property on `:root`. Your components read those\nproperties through `var(--...)`. There is no token build step and no\npreprocessor rewriting your code: the page renders against plain CSS variables\nthe editor swaps live.\n\nTo ship, click **Adopt** in the Theme panel. That saves the open theme and bakes\nit into `src/live-tokens/data/tokens.generated.css`, which your build bundles\nalongside `tokens.css`. Adopt is the only action that changes what your site\nships, so try any look you like first. The editor itself never reaches\nproduction.\n\nAlready have a Svelte 5 + Vite app? The\n[README](https://github.com/motionproto/live-tokens#readme) covers installing\ninto an existing project.\n\n## Where to go next\n\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own component\n editable.\n",
|
|
9
|
-
"themes-workflow": "# Themes\n\nSave your work, switch between looks, and ship one to production.\n\n## The Theme panel\n\nThe **Theme** panel at the foot of the editor sidebar holds the whole look:\ncolors, type, and a setting for every component you changed, in one file. It\ncarries the name the look ships under, whether production is running it, and\n**Adopt**. Two parts sit under it, each a read-out rather than a file to manage.\n\n- **Colors & Type** holds the design tokens. Components read those tokens to\n define their appearance. It names the two faces the page is showing.\n- **Components** counts how many components run something the theme does not\n carry, and opens the component editors.\n\nA theme holds its own copy of every part, so one theme can never break another.\n\n## How themes work\n\nA theme is a document, and the editor works the way any editor does.\n\n- **A theme** is a named JSON file in `src/live-tokens/data/themes/`. It carries\n the whole look: the colors and type plus a setting for every component you\n changed.\n- **The open theme** is the one the editor is working on, named in\n `themes/_active.json`. One at a time.\n- **Your unsaved edits** are what the page shows right now. The editor keeps\n them in your browser as you work and writes them to a buffer, `_working.json`,\n one slot per part of the look. **Save** captures that buffer into the open\n theme.\n- **The production theme** is the one your site ships, named in\n `themes/_production.json`.
|
|
10
|
-
"where-themes-live": "# Where themes live\n\nEverything the editor writes is plain JSON and CSS inside your project. There\nis no database and no hidden state: the files are the storage, and git is the\nhistory.\n\n## The data tree\n\n```\nsrc/live-tokens/data/\n themes/\n _active.json names the theme the editor has open\n _production.json names the theme your site ships\n default.json Motion Proto, the built-in look, rewritten at boot\n my-brand.json a saved theme: the whole look in one file\n colors-and-type/\n _working.json unsaved colors and type edits\n component-configs/\n button/\n default.json Button's shipped settings, derived at boot\n _working.json unsaved Button edits\n my-button.json a preset you saved from the Button editor\n tokens.generated.css the baked CSS your production build ships\nsrc/system/styles/\n tokens.css your token vocabulary, hand-authored, never written\n fonts.css font imports, rewritten when you Adopt\n```\n\nA saved theme carries the whole look by value: the colors and type plus a\nsetting for every component you changed. It depends on no other file, so\ndeleting anything else never breaks it.\n\n## What writes when\n\n- **Editing** changes the page through CSS variables. The editor keeps your\n edits in the browser as you work and writes them to the `_working.json`\n buffers when you save a component.
|
|
9
|
+
"themes-workflow": "# Themes\n\nSave your work, switch between looks, and ship one to production.\n\n## The Theme panel\n\nThe **Theme** panel at the foot of the editor sidebar holds the whole look:\ncolors, type, and a setting for every component you changed, in one file. It\ncarries the name the look ships under, whether production is running it, and\n**Adopt**. Two parts sit under it, each a read-out rather than a file to manage.\n\n- **Colors & Type** holds the design tokens. Components read those tokens to\n define their appearance. It names the two faces the page is showing.\n- **Components** counts how many components run something the theme does not\n carry, and opens the component editors.\n\nA theme holds its own copy of every part, so one theme can never break another.\n\n## How themes work\n\nA theme is a document, and the editor works the way any editor does.\n\n- **A theme** is a named JSON file in `src/live-tokens/data/themes/`. It carries\n the whole look: the colors and type plus a setting for every component you\n changed.\n- **The open theme** is the one the editor is working on, named in\n `themes/_active.json`. One at a time.\n- **Your unsaved edits** are what the page shows right now. The editor keeps\n them in your browser as you work and writes them to a buffer, `_working.json`,\n one slot per part of the look. **Save** captures that buffer into the open\n theme.\n- **The production theme** is the one your site ships, named in\n `themes/_production.json`. **Adopt** changes it; saving a preset in the Theme\n Picker performs that Adopt for you.\n\nAbsence is the answer for anything untouched: a buffer exists only where the\nlive look diverges from the active theme, so a newly opened theme has none.\n\n## Saving\n\nIn the Theme panel:\n\n- **Save** captures the look on screen into the open theme. Your colors and type\n go in as part of it, so there is nothing to save first.\n- **Save As** names a new theme. Use it for your first save and for forking.\n\nComponent editors keep their own unsaved state. If one or more components are\nwaiting when you use **Save**, **Save As**, or **Adopt**, the Theme panel offers\nto save all of them before continuing. You can accept once instead of visiting\neach component, or cancel to review them individually. A component editor's\n**Save As** creates a reusable component preset.\n\nNames are tidied to lowercase with hyphens, so \"My Brand!\" becomes `my-brand`,\nand a leading underscore is dropped: those names are reserved for the buffer.\n**Motion Proto** is the built-in theme and is read-only. You can always return\nto it, and the editor never overwrites it, so start your own with **Save As**.\n\n## Switching\n\n**Load**—or clicking the active theme's name—opens the Theme Picker. Picking a\ntheme shows it on the page as a preview with nothing written to disk, so you can\ntry each look and compare. **Save** in that window opens and adopts the previewed\ntheme in one step: the active pointer changes, the buffers clear, components it\ndoes not carry fall through to their defaults, the editor works on it, and\nproduction ships it. **Cancel** returns you to where you were. Previewing alone\nnever changes what your site ships.\n\n**Colors and type only. Keep my shapes.** narrows the load to the palette and\nthe fonts: your component settings stay as they are and the theme you have open\nstays open. Saved colors and type files are listed there too, marked *colors &\ntype*, and picking one is always that narrower load.\n\n## Shipping\n\n**Adopt**, in the Theme panel, is the \"ship it\" step, and it ships the whole\nlook. It saves the open theme, then bakes that theme into\n`src/live-tokens/data/tokens.generated.css`, which your build bundles alongside\n`tokens.css`: the colors and type plus every component the theme carries. Fonts\nregenerate to match. The line under the theme name says whether production is\nrunning this theme.\n\nProduction is one saved theme, so nothing else publishes. Trying a look, moving\na token, saving a theme: all of it leaves the generated CSS alone until you\nAdopt. A component editor's Adopt runs the same whole-look step, because a\ncomponent never ships alone. Adopting while Motion Proto is open saves your look\nas a theme of your own first, since the built-in one is read-only.\n\nProduction builds (`npm run build`) ship only that plain CSS and your\ncomponents. No editor, no JSON loading, no runtime indirection.\n\n## Keeping your work safe\n\nEverything under `src/live-tokens/data/` is plain JSON, so commit it. Themes show\nup as readable diffs you can review per branch, and the buffer shows up as the\nwork you have not saved into a theme yet. Nothing is backed up anywhere else:\ngit is your safety net. To experiment freely, **Save As** a new name first, then\nedit.\n\n## Where to go next\n\n- **[Where themes live](where-themes-live.md)**: the files behind all of this,\n and what writes each one.\n- **[Creating components](creating-components.md)**: make your own components\n editable in the same editor.\n",
|
|
10
|
+
"where-themes-live": "# Where themes live\n\nEverything the editor writes is plain JSON and CSS inside your project. There\nis no database and no hidden state: the files are the storage, and git is the\nhistory.\n\n## The data tree\n\n```\nsrc/live-tokens/data/\n themes/\n _active.json names the theme the editor has open\n _production.json names the theme your site ships\n default.json Motion Proto, the built-in look, rewritten at boot\n my-brand.json a saved theme: the whole look in one file\n colors-and-type/\n _working.json unsaved colors and type edits\n component-configs/\n button/\n default.json Button's shipped settings, derived at boot\n _working.json unsaved Button edits\n my-button.json a preset you saved from the Button editor\n tokens.generated.css the baked CSS your production build ships\nsrc/system/styles/\n tokens.css your token vocabulary, hand-authored, never written\n fonts.css font imports, rewritten when you Adopt\n```\n\nA saved theme carries the whole look by value: the colors and type plus a\nsetting for every component you changed. It depends on no other file, so\ndeleting anything else never breaks it.\n\n## What writes when\n\n- **Editing** changes the page through CSS variables. The editor keeps your\n edits in the browser as you work and writes them to the `_working.json`\n buffers when you save a component. When the Theme panel finds several dirty\n components, **Save all** writes those buffers together.\n- **Save** captures the buffers into the open theme's file. That file is the\n durable copy of your look; matching buffers are then removed.\n- **Load** clears the buffers and points `themes/_active.json` at the theme you\n picked. Live reads fall through to that file. Nothing else changes, so trying\n looks is free and ordinary switching changes only the pointer.\n- **Adopt** points `themes/_production.json` at the open theme, bakes it into\n `tokens.generated.css`, and rewrites `fonts.css` to match. It is the only\n action that changes what your site ships.\n\nThe `default.json` files are the shipped baseline. The editor derives them at\nboot and refreshes them when the package updates; it never saves your work\nover them.\n\nProjects upgraded from 0.48 may initially contain working files copied from the\nactive theme. On the first dev-server boot, exact copies are removed\nautomatically. Any file that differs is kept as unsaved work, so no migration\ncommand is required.\n\n## What to commit\n\nAll of it. The data tree is designed to live in git: themes diff readably, the\ntwo pointers say what is open and what ships, and a `_working.json` in a diff\nis exactly the work you have not yet saved into a theme. Nothing is backed up\nanywhere else.\n\n## Where to go next\n\n- **[Themes](themes-workflow.md)**: the workflow built on these files: saving,\n loading, and shipping.\n",
|
|
11
11
|
};
|
|
@@ -167,6 +167,7 @@
|
|
|
167
167
|
{@const protectedRow = isProtected(file)}
|
|
168
168
|
<div
|
|
169
169
|
class="load-item"
|
|
170
|
+
data-file-name={file.fileName}
|
|
170
171
|
class:active={file.fileName === activeFileName}
|
|
171
172
|
class:selected={file.fileName === selectedFileName}
|
|
172
173
|
class:protected={protectedRow}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
SystemCascadePreset,
|
|
12
12
|
} from '../core/themes/themeTypes';
|
|
13
13
|
import { editorState, setFontStacks } from '../core/store/editorStore';
|
|
14
|
-
import {
|
|
14
|
+
import { SYSTEM_CASCADES } from '../core/fonts/fontLoader';
|
|
15
15
|
|
|
16
16
|
const SYSTEM_PRESETS: SystemCascadePreset[] = ['system-ui-sans', 'system-ui-serif', 'system-ui-mono'];
|
|
17
17
|
// `cursive` and `fantasy` are CSS-spec generics whose rendering varies wildly
|
|
@@ -110,7 +110,6 @@
|
|
|
110
110
|
function updateStack(variable: FontStackVariable, updater: (slots: FontStackSlot[]) => FontStackSlot[]) {
|
|
111
111
|
const next = stacks.map((s) => (s.variable === variable ? { ...s, slots: updater([...s.slots]) } : s));
|
|
112
112
|
setFontStacks(next);
|
|
113
|
-
applyFontStacks(next, fontSourcesList);
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
function handleSelectChange(variable: FontStackVariable, index: number, value: string) {
|
|
@@ -274,7 +274,11 @@
|
|
|
274
274
|
</script>
|
|
275
275
|
|
|
276
276
|
{#if gradient}
|
|
277
|
-
<div
|
|
277
|
+
<div
|
|
278
|
+
class="gradient-editor"
|
|
279
|
+
class:has-pad={hasAside}
|
|
280
|
+
data-token-variable={gradientSource.targetVariable}
|
|
281
|
+
>
|
|
278
282
|
{#if sectionLabel || hasAside}
|
|
279
283
|
<div class="editor-header editor-section-left">
|
|
280
284
|
<span class="editor-section-label">{sectionLabel ?? ''}</span>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { FontFamily, FontSource } from '../core/themes/themeTypes';
|
|
3
3
|
import { editorState, setFontSources, transaction } from '../core/store/editorStore';
|
|
4
|
-
import { applyFontSources, applyFontStacks } from '../core/fonts/fontLoader';
|
|
5
4
|
import {
|
|
6
5
|
buildSourceFromFontFaceText,
|
|
7
6
|
buildSourceFromUrl,
|
|
@@ -69,8 +68,6 @@
|
|
|
69
68
|
|
|
70
69
|
function commitSources(next: FontSource[]) {
|
|
71
70
|
setFontSources(next);
|
|
72
|
-
applyFontSources(next);
|
|
73
|
-
applyFontStacks(fontStacksList, next);
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
/** One paste field → sniff whether it's @font-face or a URL/embed and
|
|
@@ -220,8 +217,6 @@
|
|
|
220
217
|
s.fonts.sources = next;
|
|
221
218
|
s.fonts.stacks = updatedStacks;
|
|
222
219
|
});
|
|
223
|
-
applyFontSources(next);
|
|
224
|
-
applyFontStacks(updatedStacks, next);
|
|
225
220
|
}
|
|
226
221
|
|
|
227
222
|
/** Resolve a clickable target for the row. We prefer the human-readable
|
|
@@ -22,8 +22,12 @@
|
|
|
22
22
|
exportTheme,
|
|
23
23
|
importTheme,
|
|
24
24
|
} from '../core/themes/themeService';
|
|
25
|
+
import {
|
|
26
|
+
THEME_APPLIED_EVENT,
|
|
27
|
+
type AppliedThemeDetail,
|
|
28
|
+
} from '../core/themes/themeDocumentSync';
|
|
25
29
|
import { countComponentsOffLook, lookProductionState } from '../core/themes/lookSummary';
|
|
26
|
-
import { previewTheme, previewColorsAndType, revertPreview } from '../core/preview/lookPreview';
|
|
30
|
+
import { commitPreview, previewTheme, previewColorsAndType, revertPreview } from '../core/preview/lookPreview';
|
|
27
31
|
import {
|
|
28
32
|
deleteColorsAndType,
|
|
29
33
|
hydrateColorsAndType,
|
|
@@ -41,10 +45,17 @@
|
|
|
41
45
|
} from '../core/themes/loadRows';
|
|
42
46
|
import {
|
|
43
47
|
listComponents,
|
|
48
|
+
componentConfigFromState,
|
|
49
|
+
writeWorkingComponentConfig,
|
|
44
50
|
type ComponentSummary,
|
|
45
51
|
} from '../core/components/componentConfigService';
|
|
46
52
|
import { fontPairingLabel } from '../core/fonts/fontPairing';
|
|
47
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
componentDirty,
|
|
55
|
+
editorState,
|
|
56
|
+
colorsAndTypeDirty,
|
|
57
|
+
markComponentSaved,
|
|
58
|
+
} from '../core/store/editorStore';
|
|
48
59
|
import { openThemeSlug } from '../core/store/editorConfigStore';
|
|
49
60
|
import { editorView } from '../core/store/editorViewStore';
|
|
50
61
|
import {
|
|
@@ -53,12 +64,14 @@
|
|
|
53
64
|
bumpProductionRevision,
|
|
54
65
|
liveMovedSinceBake,
|
|
55
66
|
productionTheme,
|
|
67
|
+
bumpComponentActiveRevision,
|
|
56
68
|
} from '../core/productionPulse';
|
|
57
69
|
import { flashStatus } from '../core/flashStatus';
|
|
58
70
|
import UIInfoPopover from './UIInfoPopover.svelte';
|
|
59
71
|
import UIPillButton from './UIPillButton.svelte';
|
|
60
72
|
import FileLoadList from './FileLoadList.svelte';
|
|
61
73
|
import FilePill from './FilePill.svelte';
|
|
74
|
+
import UIDialog from './UIDialog.svelte';
|
|
62
75
|
import SaveAsDialog from '../component-editor/scaffolding/SaveAsDialog.svelte';
|
|
63
76
|
|
|
64
77
|
interface Props {
|
|
@@ -75,6 +88,9 @@
|
|
|
75
88
|
let rows = $derived(buildLoadRows(files, colorsFiles));
|
|
76
89
|
let showFileList = $state(false);
|
|
77
90
|
let saveAsDialog = $state(false);
|
|
91
|
+
let saveComponentsDialog = $state(false);
|
|
92
|
+
let pendingComponentSave: (() => Promise<void>) | null = null;
|
|
93
|
+
let pendingComponentCount = $state(0);
|
|
78
94
|
let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = $state('idle');
|
|
79
95
|
|
|
80
96
|
let currentDisplayName = $state('Motion Proto');
|
|
@@ -171,6 +187,19 @@
|
|
|
171
187
|
await refreshProduction();
|
|
172
188
|
});
|
|
173
189
|
|
|
190
|
+
// A consumer can load a theme from the host while this iframe stays open.
|
|
191
|
+
// The cross-document bridge hydrates this document's typed store; keep the
|
|
192
|
+
// panel's local identity/summary state on that same payload as well.
|
|
193
|
+
onMount(() => {
|
|
194
|
+
const handleThemeApplied = (event: Event) => {
|
|
195
|
+
const { result } = (event as CustomEvent<AppliedThemeDetail>).detail;
|
|
196
|
+
currentDisplayName = result.theme.name;
|
|
197
|
+
lookConfigs = result.theme.componentConfigs;
|
|
198
|
+
};
|
|
199
|
+
document.addEventListener(THEME_APPLIED_EVENT, handleThemeApplied);
|
|
200
|
+
return () => document.removeEventListener(THEME_APPLIED_EVENT, handleThemeApplied);
|
|
201
|
+
});
|
|
202
|
+
|
|
174
203
|
// Re-read whenever an Adopt fires, here or in a component editor: it saves
|
|
175
204
|
// the open theme and moves the production pointer, so the identity, the
|
|
176
205
|
// component summary and the production state shown here all need to track.
|
|
@@ -188,10 +217,10 @@
|
|
|
188
217
|
refreshComponents();
|
|
189
218
|
});
|
|
190
219
|
|
|
191
|
-
// A capture reads the server's live state, so
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
220
|
+
// A capture reads the server's live state, so colors and type go to their
|
|
221
|
+
// buffer first. Component editors retain their own save boundary; when any
|
|
222
|
+
// are dirty, the theme panel offers to flush all of them in one explicit
|
|
223
|
+
// step before continuing.
|
|
195
224
|
//
|
|
196
225
|
// The gate is `colorsAndTypeDirty`, not history: history counts every entry,
|
|
197
226
|
// components included, so it would flush the colors over component-only work.
|
|
@@ -200,20 +229,45 @@
|
|
|
200
229
|
await persistColorsAndType(get(editorState), currentDisplayName);
|
|
201
230
|
}
|
|
202
231
|
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
232
|
+
async function flushComponents(displayName = currentDisplayName): Promise<void> {
|
|
233
|
+
const dirtyComponents = Object.entries(get(componentDirty))
|
|
234
|
+
.filter(([, dirty]) => dirty)
|
|
235
|
+
.map(([component]) => component);
|
|
236
|
+
if (dirtyComponents.length === 0) return;
|
|
237
|
+
|
|
238
|
+
const state = get(editorState);
|
|
239
|
+
await Promise.all(dirtyComponents.map((component) =>
|
|
240
|
+
writeWorkingComponentConfig(
|
|
241
|
+
component,
|
|
242
|
+
componentConfigFromState(state, component, displayName),
|
|
243
|
+
),
|
|
244
|
+
));
|
|
245
|
+
for (const component of dirtyComponents) markComponentSaved(component);
|
|
246
|
+
bumpComponentActiveRevision();
|
|
210
247
|
}
|
|
211
248
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (
|
|
249
|
+
function continueWithComponentChoice(action: () => Promise<void>): void {
|
|
250
|
+
const dirtyCount = Object.values(get(componentDirty)).filter(Boolean).length;
|
|
251
|
+
if (dirtyCount === 0) {
|
|
252
|
+
void action();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
pendingComponentCount = dirtyCount;
|
|
256
|
+
pendingComponentSave = action;
|
|
257
|
+
saveComponentsDialog = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function confirmSaveAllComponents(): Promise<void> {
|
|
261
|
+
const action = pendingComponentSave;
|
|
262
|
+
pendingComponentSave = null;
|
|
263
|
+
saveComponentsDialog = false;
|
|
264
|
+
if (action) await action();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function runSave(saveComponents: boolean) {
|
|
215
268
|
saveStatus = 'saving';
|
|
216
269
|
try {
|
|
270
|
+
if (saveComponents) await flushComponents();
|
|
217
271
|
await flushColors();
|
|
218
272
|
await saveActiveTheme(currentDisplayName);
|
|
219
273
|
await refreshActive();
|
|
@@ -223,10 +277,21 @@
|
|
|
223
277
|
}
|
|
224
278
|
}
|
|
225
279
|
|
|
280
|
+
function handleSave() {
|
|
281
|
+
if (activeIsProtected) return;
|
|
282
|
+
continueWithComponentChoice(() => runSave(true));
|
|
283
|
+
}
|
|
284
|
+
|
|
226
285
|
function openSaveAs() {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
286
|
+
continueWithComponentChoice(async () => {
|
|
287
|
+
try {
|
|
288
|
+
await flushComponents();
|
|
289
|
+
showFileList = false;
|
|
290
|
+
saveAsDialog = true;
|
|
291
|
+
} catch {
|
|
292
|
+
flashStatus(setSaveStatus, 'error');
|
|
293
|
+
}
|
|
294
|
+
});
|
|
230
295
|
}
|
|
231
296
|
|
|
232
297
|
async function confirmSaveAs(detail: { displayName: string; fileName: string }) {
|
|
@@ -266,10 +331,9 @@
|
|
|
266
331
|
return 'Save this theme and ship it to production';
|
|
267
332
|
});
|
|
268
333
|
|
|
269
|
-
|
|
334
|
+
function handleAdopt() {
|
|
270
335
|
if (production.inProduction || adoptStatus === 'adopting') return;
|
|
271
|
-
|
|
272
|
-
await runAdopt();
|
|
336
|
+
continueWithComponentChoice(() => runAdopt(true));
|
|
273
337
|
}
|
|
274
338
|
|
|
275
339
|
/**
|
|
@@ -278,9 +342,10 @@
|
|
|
278
342
|
* Adopt, and which file holds the look is bookkeeping they should not have
|
|
279
343
|
* to think about, so it forks to a theme of their own first.
|
|
280
344
|
*/
|
|
281
|
-
async function runAdopt() {
|
|
345
|
+
async function runAdopt(saveComponents = false) {
|
|
282
346
|
adoptStatus = 'adopting';
|
|
283
347
|
try {
|
|
348
|
+
if (saveComponents) await flushComponents();
|
|
284
349
|
await flushColors();
|
|
285
350
|
if (activeIsProtected) {
|
|
286
351
|
const taken = new Set((await listThemes()).map((m) => m.fileName));
|
|
@@ -329,6 +394,13 @@
|
|
|
329
394
|
previewLayer = null;
|
|
330
395
|
}
|
|
331
396
|
|
|
397
|
+
function acceptPreview() {
|
|
398
|
+
commitPreview();
|
|
399
|
+
previewRow = null;
|
|
400
|
+
previewLook = null;
|
|
401
|
+
previewLayer = null;
|
|
402
|
+
}
|
|
403
|
+
|
|
332
404
|
/** Paint whatever is selected, through the engine the mode calls for. A
|
|
333
405
|
* theme's colors and type are the slice embedded in it, so one look
|
|
334
406
|
* previews either way. */
|
|
@@ -386,26 +458,54 @@
|
|
|
386
458
|
if (!row) return;
|
|
387
459
|
if (unsavedEdits) {
|
|
388
460
|
const ok = window.confirm(
|
|
389
|
-
'Loading a theme will
|
|
461
|
+
'Loading a theme will replace the editor’s current state and discard unsaved changes. Continue?',
|
|
390
462
|
);
|
|
391
463
|
if (!ok) return;
|
|
392
464
|
}
|
|
393
|
-
// The
|
|
394
|
-
//
|
|
465
|
+
// The exact theme being opened is already painted. Hand that preview to
|
|
466
|
+
// the store load without restoring the old look across the request.
|
|
467
|
+
acceptPreview();
|
|
468
|
+
let result: Awaited<ReturnType<typeof applyTheme>>;
|
|
395
469
|
try {
|
|
396
|
-
|
|
470
|
+
result = await applyTheme(row.slug);
|
|
397
471
|
if (result.skippedComponents.length > 0) {
|
|
398
472
|
window.alert(
|
|
399
473
|
`Loaded "${row.name}". These components are not installed here, so their `
|
|
400
474
|
+ `saved settings were skipped:\n\n${result.skippedComponents.join(', ')}`,
|
|
401
475
|
);
|
|
402
476
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
477
|
+
currentDisplayName = result.theme.name;
|
|
478
|
+
lookConfigs = result.theme.componentConfigs;
|
|
479
|
+
showFileList = false;
|
|
406
480
|
} catch (err) {
|
|
407
481
|
window.alert(`Failed to load theme: ${(err as Error).message}`);
|
|
482
|
+
// The selection remains visible after a failed request; put its preview
|
|
483
|
+
// back so the dialog and page continue to agree.
|
|
484
|
+
previewRow = row;
|
|
485
|
+
previewLook = row.kind === 'look' ? await loadTheme(row.slug).catch(() => null) : null;
|
|
486
|
+
if (previewLook) await repaint();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// The picker is a one-step choose-and-ship flow. Package/user presets are
|
|
491
|
+
// already saved documents, so adopting can publish them directly. The
|
|
492
|
+
// protected baseline cannot be production's editable document; mirror the
|
|
493
|
+
// normal Adopt behavior and fork it first when it is selected.
|
|
494
|
+
adoptStatus = 'adopting';
|
|
495
|
+
try {
|
|
496
|
+
if (row.slug === 'default') {
|
|
497
|
+
const taken = new Set((await listThemes()).map((m) => m.fileName));
|
|
498
|
+
await saveAsTheme(freshName('my-theme', taken), 'My Theme');
|
|
499
|
+
await refreshActive();
|
|
500
|
+
}
|
|
501
|
+
await adoptLook();
|
|
502
|
+
bumpProductionRevision();
|
|
503
|
+
flashStatus(setAdoptStatus, 'done');
|
|
504
|
+
} catch (err) {
|
|
505
|
+
window.alert(`Theme loaded, but could not be adopted: ${(err as Error).message}`);
|
|
506
|
+
flashStatus(setAdoptStatus, 'error', { durationMs: 3000 });
|
|
408
507
|
}
|
|
508
|
+
await Promise.all([refreshFiles(), refreshComponents(), refreshProduction()]);
|
|
409
509
|
}
|
|
410
510
|
|
|
411
511
|
/**
|
|
@@ -554,6 +654,11 @@
|
|
|
554
654
|
if (showFileList) refreshFiles();
|
|
555
655
|
}
|
|
556
656
|
|
|
657
|
+
function openThemePicker() {
|
|
658
|
+
showFileList = true;
|
|
659
|
+
refreshFiles();
|
|
660
|
+
}
|
|
661
|
+
|
|
557
662
|
function rowBadge(row: LoadRow) {
|
|
558
663
|
return row.kind === 'layer'
|
|
559
664
|
? { label: 'colors & type', title: 'Holds colors and type only, no shapes' }
|
|
@@ -575,7 +680,7 @@
|
|
|
575
680
|
<strong>Load</strong> opens the list. Picking a theme shows it on the page as a preview, so you can try each look with nothing written to disk. Pick another to compare, or <strong>Cancel</strong> to go back to where you were.
|
|
576
681
|
</p>
|
|
577
682
|
<p>
|
|
578
|
-
<strong>Save</strong> opens the previewed theme: the editor works on it from then on
|
|
683
|
+
<strong>Save</strong> opens and adopts the previewed theme: the editor works on it from then on and production ships it immediately. Components it does not carry go back to their defaults. Previewing and cancelling never change what your site ships.
|
|
579
684
|
</p>
|
|
580
685
|
<p>
|
|
581
686
|
<strong>Colors and type only</strong> in that window takes the palette and the fonts and leaves your shapes and component settings alone.
|
|
@@ -584,7 +689,7 @@
|
|
|
584
689
|
The <strong>active</strong> theme is the one the editor has open. <strong>Adopt</strong> saves it and ships it to production, colors and type plus every component you changed. The line under the name says whether production is running this theme.
|
|
585
690
|
</p>
|
|
586
691
|
<p>
|
|
587
|
-
|
|
692
|
+
If components have unsaved edits, Save and Adopt offer to save all of them before continuing. You can cancel to review or save components individually.
|
|
588
693
|
</p>
|
|
589
694
|
<p>
|
|
590
695
|
<strong>Motion Proto</strong> is the protected baseline. To start customizing, <strong>Save As</strong> a new theme first.
|
|
@@ -603,13 +708,21 @@
|
|
|
603
708
|
</span>
|
|
604
709
|
{/if}
|
|
605
710
|
</div>
|
|
606
|
-
<
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
title=
|
|
611
|
-
|
|
612
|
-
|
|
711
|
+
<button
|
|
712
|
+
type="button"
|
|
713
|
+
class="theme-name-trigger"
|
|
714
|
+
onclick={openThemePicker}
|
|
715
|
+
title="Open the Theme Picker"
|
|
716
|
+
aria-label={`Open Theme Picker. Current theme: ${currentDisplayName}`}
|
|
717
|
+
>
|
|
718
|
+
<FilePill
|
|
719
|
+
name={currentDisplayName}
|
|
720
|
+
isProtected={activeIsProtected}
|
|
721
|
+
protectedTitle="Protected default theme"
|
|
722
|
+
title={currentDisplayName}
|
|
723
|
+
style="display: flex;"
|
|
724
|
+
/>
|
|
725
|
+
</button>
|
|
613
726
|
<span class="mfm-prod-status" class:applied={production.inProduction}>
|
|
614
727
|
<i class="mfm-status-dot" aria-hidden="true"></i>
|
|
615
728
|
<span>
|
|
@@ -668,7 +781,7 @@
|
|
|
668
781
|
class="mfm-btn mfm-btn-row"
|
|
669
782
|
class:active={showFileList}
|
|
670
783
|
onclick={toggleFileList}
|
|
671
|
-
title="
|
|
784
|
+
title="Open the Theme Picker"
|
|
672
785
|
>
|
|
673
786
|
<i class="fas fa-folder-open"></i>
|
|
674
787
|
<span>Load…</span>
|
|
@@ -741,13 +854,29 @@
|
|
|
741
854
|
style="display: none;"
|
|
742
855
|
/>
|
|
743
856
|
|
|
857
|
+
<UIDialog
|
|
858
|
+
bind:show={saveComponentsDialog}
|
|
859
|
+
title="Unsaved component edits"
|
|
860
|
+
cancelLabel="Cancel"
|
|
861
|
+
confirmLabel={pendingComponentCount === 1 ? 'Save component' : 'Save all components'}
|
|
862
|
+
onconfirm={confirmSaveAllComponents}
|
|
863
|
+
width="400px"
|
|
864
|
+
>
|
|
865
|
+
<p class="save-components-message">
|
|
866
|
+
{pendingComponentCount === 1
|
|
867
|
+
? '1 component has unsaved edits.'
|
|
868
|
+
: `${pendingComponentCount} components have unsaved edits.`}
|
|
869
|
+
Save {pendingComponentCount === 1 ? 'it' : 'all of them'} and continue?
|
|
870
|
+
</p>
|
|
871
|
+
</UIDialog>
|
|
872
|
+
|
|
744
873
|
<FileLoadList
|
|
745
874
|
bind:show={showFileList}
|
|
746
|
-
title="
|
|
875
|
+
title="Theme Picker"
|
|
747
876
|
files={rows}
|
|
748
877
|
activeFileName={activeRowId}
|
|
749
878
|
selectedFileName={previewRow?.fileName ?? null}
|
|
750
|
-
selectedBadge={{ label: 'Preview', title: 'Shown on the page now. Save to
|
|
879
|
+
selectedBadge={{ label: 'Preview', title: 'Shown on the page now. Save to use and publish it.' }}
|
|
751
880
|
{rowBadge}
|
|
752
881
|
cancelLabel={previewRow ? 'Cancel' : 'Close'}
|
|
753
882
|
confirmLabel={previewRow ? 'Save' : ''}
|
|
@@ -788,6 +917,13 @@
|
|
|
788
917
|
/>
|
|
789
918
|
|
|
790
919
|
<style>
|
|
920
|
+
.save-components-message {
|
|
921
|
+
margin: 0;
|
|
922
|
+
color: var(--ui-text-secondary);
|
|
923
|
+
font-size: var(--ui-font-size-sm);
|
|
924
|
+
line-height: 1.5;
|
|
925
|
+
}
|
|
926
|
+
|
|
791
927
|
.look-panel {
|
|
792
928
|
--mfm-active: #5aa85e;
|
|
793
929
|
--mfm-rail-neutral: var(--ui-border);
|
|
@@ -850,6 +986,34 @@
|
|
|
850
986
|
color: var(--ui-text-tertiary);
|
|
851
987
|
}
|
|
852
988
|
|
|
989
|
+
.theme-name-trigger {
|
|
990
|
+
display: block;
|
|
991
|
+
width: 100%;
|
|
992
|
+
padding: 0;
|
|
993
|
+
border: 0;
|
|
994
|
+
background: transparent;
|
|
995
|
+
color: inherit;
|
|
996
|
+
font: inherit;
|
|
997
|
+
text-align: inherit;
|
|
998
|
+
cursor: pointer;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.theme-name-trigger :global(.file-pill) {
|
|
1002
|
+
width: 100%;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
.theme-name-trigger:hover :global(.file-pill),
|
|
1006
|
+
.theme-name-trigger:focus-visible :global(.file-pill) {
|
|
1007
|
+
border-color: var(--ui-border-higher);
|
|
1008
|
+
box-shadow: inset 0 0 0 1px var(--ui-border-high);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
.theme-name-trigger:focus-visible {
|
|
1012
|
+
outline: 2px solid var(--ui-text-accent);
|
|
1013
|
+
outline-offset: 2px;
|
|
1014
|
+
border-radius: var(--ui-radius-md);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
853
1017
|
.mfm-badge {
|
|
854
1018
|
display: inline-flex;
|
|
855
1019
|
align-items: center;
|
|
@@ -55,7 +55,13 @@
|
|
|
55
55
|
{#if show}
|
|
56
56
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions, a11y_no_static_element_interactions -->
|
|
57
57
|
<div class="ui-dialog-backdrop" onclick={self(handleClose)}>
|
|
58
|
-
<div
|
|
58
|
+
<div
|
|
59
|
+
class="ui-dialog"
|
|
60
|
+
role="dialog"
|
|
61
|
+
aria-modal="true"
|
|
62
|
+
aria-label={title || 'Dialog'}
|
|
63
|
+
style="width: {width}; max-width: {width};"
|
|
64
|
+
>
|
|
59
65
|
{#if title}
|
|
60
66
|
<div class="ui-dialog-header">
|
|
61
67
|
<h3 class="ui-dialog-title">{title}</h3>
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { onMount, onDestroy } from 'svelte';
|
|
2
|
+
import { onMount, onDestroy, untrack } from 'svelte';
|
|
5
3
|
import { resolveAliasChain } from '../core/palettes/tokenRegistry';
|
|
6
4
|
import { editorState } from '../core/store/editorStore';
|
|
7
|
-
import {
|
|
5
|
+
import { CSS_VARS_CHANGE_EVENT, type CssVarsChangeDetail } from '../core/cssVarSync';
|
|
8
6
|
import type { FontFamily, FontSource, FontSourceKind } from '../core/themes/themeTypes';
|
|
9
7
|
import UITokenSelector from './UITokenSelector.svelte';
|
|
10
8
|
import UIOptionList from './UIOptionList.svelte';
|
|
@@ -93,8 +91,8 @@
|
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
function handleVarChange(e: Event) {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
94
|
+
const names = (e as CustomEvent<CssVarsChangeDetail>).detail?.names ?? [];
|
|
95
|
+
if (names.some((name) => name.startsWith('--font-'))) readResolved();
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
function initFromCurrent() {
|
|
@@ -160,19 +158,21 @@
|
|
|
160
158
|
|
|
161
159
|
// Re-derive `chosenKey` / `chosenFamilyId` when `variable` changes (the
|
|
162
160
|
// VariantGroup tabs view reuses the same selector instance across states).
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
161
|
+
$effect(() => {
|
|
162
|
+
// A tab can reuse this instance with a different variable. Track only the
|
|
163
|
+
// prop; initialization writes local selector state and is intentionally
|
|
164
|
+
// untracked to prevent a self-triggering effect.
|
|
165
|
+
variable;
|
|
166
|
+
untrack(() => {
|
|
167
167
|
initFromCurrent();
|
|
168
|
-
}
|
|
168
|
+
});
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
onMount(() => {
|
|
172
|
-
document.addEventListener(
|
|
172
|
+
document.addEventListener(CSS_VARS_CHANGE_EVENT, handleVarChange);
|
|
173
173
|
});
|
|
174
174
|
onDestroy(() => {
|
|
175
|
-
document.removeEventListener(
|
|
175
|
+
document.removeEventListener(CSS_VARS_CHANGE_EVENT, handleVarChange);
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
let activeLabel = $derived(chosenFamilyId
|