@motion-proto/live-tokens 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -4
- package/dist-plugin/index.cjs +77 -3
- package/dist-plugin/index.js +77 -3
- package/package.json +10 -5
- package/src/component-editor/DialogEditor.svelte +2 -2
- package/src/component-editor/NotificationEditor.svelte +2 -2
- package/src/component-editor/scaffolding/ComponentFileManager.svelte +4 -1
- package/src/component-editor/scaffolding/SaveAsDialog.svelte +24 -7
- package/src/lib/ColumnsOverlay.svelte +1 -1
- package/src/lib/componentPersist.ts +65 -0
- package/src/lib/presetService.ts +121 -1
- package/src/lib/productionPulse.ts +32 -0
- package/src/pages/ComponentEditorPage.svelte +33 -1
- package/src/pages/Editor.svelte +8 -2
- package/src/pages/EditorShell.svelte +25 -0
- package/src/styles/site.css +138 -0
- package/src/styles/tokens.css +24 -0
- package/src/styles/ui-form-controls.css +186 -0
- package/src/ui/FontStackEditor.svelte +1 -1
- package/src/ui/PresetFileManager.svelte +763 -216
- package/src/ui/ProjectFontsSection.svelte +4 -4
- package/src/ui/ThemeFileManager.svelte +557 -307
- package/src/ui/UnsavedComponentsDialog.svelte +315 -0
- package/src/styles/form-controls.css +0 -188
package/README.md
CHANGED
|
@@ -127,13 +127,56 @@ The components carry their own design-token aliases (declared inside each `.svel
|
|
|
127
127
|
|
|
128
128
|
### Styles
|
|
129
129
|
|
|
130
|
+
Editor chrome (`ui-editor.css`, `ui-form-controls.css`) and the icon font are
|
|
131
|
+
**auto-loaded by the editor pages themselves**; you don't import them. The
|
|
132
|
+
only stylesheet a consumer needs is a `tokens.css` declaring the design-token
|
|
133
|
+
CSS variables on `:root`.
|
|
134
|
+
|
|
135
|
+
You can use the package's default as a starting point:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import '@motion-proto/live-tokens/starter/tokens.css';
|
|
139
|
+
import '@motion-proto/live-tokens/starter/site.css'; // optional: themed h1/p/a styles
|
|
140
|
+
import '@motion-proto/live-tokens/starter/fonts.css'; // optional: Fraunces + Manrope @font-face
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
…or copy `node_modules/@motion-proto/live-tokens/src/styles/tokens.css` into
|
|
144
|
+
your project and edit. The editor will seed `themes/default.json` on first
|
|
145
|
+
run and you can promote your edits back into the file.
|
|
146
|
+
|
|
147
|
+
## Consuming live-tokens from scratch
|
|
148
|
+
|
|
149
|
+
The minimum a consumer needs after `npm install @motion-proto/live-tokens`:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// src/main.ts
|
|
153
|
+
import '@motion-proto/live-tokens/starter/tokens.css';
|
|
154
|
+
import { mount } from 'svelte';
|
|
155
|
+
import App from './App.svelte';
|
|
156
|
+
|
|
157
|
+
mount(App, { target: document.getElementById('app')! });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```svelte
|
|
161
|
+
<!-- src/App.svelte -->
|
|
162
|
+
<script lang="ts">
|
|
163
|
+
import Editor from '@motion-proto/live-tokens/editor';
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<Editor />
|
|
167
|
+
```
|
|
168
|
+
|
|
130
169
|
```ts
|
|
131
|
-
|
|
132
|
-
import '
|
|
133
|
-
import '@
|
|
170
|
+
// vite.config.ts
|
|
171
|
+
import { defineConfig } from 'vite';
|
|
172
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
173
|
+
|
|
174
|
+
export default defineConfig({
|
|
175
|
+
plugins: [svelte()],
|
|
176
|
+
});
|
|
134
177
|
```
|
|
135
178
|
|
|
136
|
-
|
|
179
|
+
No `css: 'injected'` workaround, no `optimizeDeps` excludes — `vite build` works as-is. (You'll want the full `themeFileApi` plugin from the Quick install section above when you're ready to persist edits to disk.)
|
|
137
180
|
|
|
138
181
|
## Greenfield? Use the starter
|
|
139
182
|
|
package/dist-plugin/index.cjs
CHANGED
|
@@ -168,7 +168,7 @@ function themeFileApi(opts) {
|
|
|
168
168
|
themesResource.ensureDir();
|
|
169
169
|
if (!import_fs2.default.existsSync(import_path2.default.join(THEMES_DIR, "default.json"))) {
|
|
170
170
|
const defaultTheme = {
|
|
171
|
-
name: "Default",
|
|
171
|
+
name: "Default Theme",
|
|
172
172
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
173
173
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
174
174
|
editorConfigs: {},
|
|
@@ -369,7 +369,7 @@ ${newVars}
|
|
|
369
369
|
}
|
|
370
370
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
371
371
|
const defaultPreset = {
|
|
372
|
-
name: "Default",
|
|
372
|
+
name: "Default Preset",
|
|
373
373
|
createdAt: now,
|
|
374
374
|
updatedAt: now,
|
|
375
375
|
theme: themesResource.getActiveName(),
|
|
@@ -444,6 +444,7 @@ ${COMPONENT_OVERRIDES_END}
|
|
|
444
444
|
const COMPONENT_CONFIGS_ROUTE = `${API_BASE}/component-configs`;
|
|
445
445
|
const PRESETS_ROUTE = `${API_BASE}/presets`;
|
|
446
446
|
const PRESETS_ACTIVE_ROUTE = `${API_BASE}/presets/active`;
|
|
447
|
+
const PRESETS_PRODUCTION_ROUTE = `${API_BASE}/presets/production`;
|
|
447
448
|
const THEME_BY_NAME_REGEX = new RegExp(`^${escapedBase}/themes/([a-z0-9\\-_]+)$`);
|
|
448
449
|
const COMP_LIST_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)$`);
|
|
449
450
|
const COMP_ACTIVE_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/active$`);
|
|
@@ -754,6 +755,73 @@ ${COMPONENT_OVERRIDES_END}
|
|
|
754
755
|
presetsResource.setActiveName(fileName);
|
|
755
756
|
jsonResponse(res, 200, { ok: true, activeFile: fileName });
|
|
756
757
|
}
|
|
758
|
+
async function handleGetProductionPreset({ res }) {
|
|
759
|
+
const prodFile = presetsResource.getProductionName();
|
|
760
|
+
const filePath = presetsResource.filePath(prodFile);
|
|
761
|
+
if (!import_fs2.default.existsSync(filePath)) {
|
|
762
|
+
jsonResponse(res, 200, {
|
|
763
|
+
fileName: prodFile,
|
|
764
|
+
name: prodFile,
|
|
765
|
+
theme: "default",
|
|
766
|
+
componentConfigs: {},
|
|
767
|
+
updatedAt: ""
|
|
768
|
+
});
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const data = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
|
|
772
|
+
jsonResponse(res, 200, {
|
|
773
|
+
fileName: prodFile,
|
|
774
|
+
name: data.name || prodFile,
|
|
775
|
+
theme: data.theme || "default",
|
|
776
|
+
componentConfigs: data.componentConfigs || {},
|
|
777
|
+
updatedAt: data.updatedAt || ""
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
async function handleSetProductionPreset({ req, res }) {
|
|
781
|
+
const body = JSON.parse(await readBody(req));
|
|
782
|
+
const fileName = sanitizeFileName(body.name || "default");
|
|
783
|
+
const presetPath = presetsResource.filePath(fileName);
|
|
784
|
+
if (!import_fs2.default.existsSync(presetPath)) {
|
|
785
|
+
jsonResponse(res, 404, { error: "Preset not found" });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const preset = JSON.parse(import_fs2.default.readFileSync(presetPath, "utf-8"));
|
|
789
|
+
const themeName = sanitizeFileName(preset.theme || "default");
|
|
790
|
+
if (!import_fs2.default.existsSync(themesResource.filePath(themeName))) {
|
|
791
|
+
jsonResponse(res, 422, { error: `Preset references missing theme: ${themeName}` });
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const knownComponents = new Set(listComponentNames());
|
|
795
|
+
const componentConfigs = preset.componentConfigs ?? {};
|
|
796
|
+
const apply = [];
|
|
797
|
+
for (const [comp, configFile] of Object.entries(componentConfigs)) {
|
|
798
|
+
if (!knownComponents.has(comp)) continue;
|
|
799
|
+
const sanitized = sanitizeFileName(String(configFile) || "default");
|
|
800
|
+
if (!import_fs2.default.existsSync(componentResource(comp).filePath(sanitized))) {
|
|
801
|
+
jsonResponse(res, 422, {
|
|
802
|
+
error: `Preset references missing config: ${comp}/${sanitized}`
|
|
803
|
+
});
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
apply.push([comp, sanitized]);
|
|
807
|
+
}
|
|
808
|
+
themesResource.setProductionName(themeName);
|
|
809
|
+
for (const [comp, configFile] of apply) {
|
|
810
|
+
componentResource(comp).setProductionName(configFile);
|
|
811
|
+
}
|
|
812
|
+
presetsResource.setProductionName(fileName);
|
|
813
|
+
syncTokensToCss(themeName);
|
|
814
|
+
syncFontsToCss(themeName);
|
|
815
|
+
syncComponentsToCss();
|
|
816
|
+
jsonResponse(res, 200, {
|
|
817
|
+
ok: true,
|
|
818
|
+
fileName,
|
|
819
|
+
name: preset.name || fileName,
|
|
820
|
+
theme: themeName,
|
|
821
|
+
componentConfigs: Object.fromEntries(apply),
|
|
822
|
+
updatedAt: preset.updatedAt || ""
|
|
823
|
+
});
|
|
824
|
+
}
|
|
757
825
|
async function handlePresetByName({ params, req, res }) {
|
|
758
826
|
const [fileName] = params;
|
|
759
827
|
const filePath = presetsResource.filePath(fileName);
|
|
@@ -768,6 +836,10 @@ ${COMPONENT_OVERRIDES_END}
|
|
|
768
836
|
return;
|
|
769
837
|
}
|
|
770
838
|
if (req.method === "PUT") {
|
|
839
|
+
if (fileName === "default") {
|
|
840
|
+
jsonResponse(res, 403, { error: "Cannot overwrite the default preset" });
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
771
843
|
const body = JSON.parse(await readBody(req));
|
|
772
844
|
body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
773
845
|
if (import_fs2.default.existsSync(filePath)) {
|
|
@@ -884,10 +956,12 @@ ${COMPONENT_OVERRIDES_END}
|
|
|
884
956
|
{ method: "GET", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
|
|
885
957
|
{ method: "PUT", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
|
|
886
958
|
{ method: "DELETE", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
|
|
887
|
-
// Presets — list / active are exact strings, must run before regexes
|
|
959
|
+
// Presets — list / active / production are exact strings, must run before regexes
|
|
888
960
|
{ method: "GET", pattern: PRESETS_ROUTE, handler: handleListPresets },
|
|
889
961
|
{ method: "GET", pattern: PRESETS_ACTIVE_ROUTE, handler: handleGetActivePreset },
|
|
890
962
|
{ method: "PUT", pattern: PRESETS_ACTIVE_ROUTE, handler: handleSetActivePreset },
|
|
963
|
+
{ method: "GET", pattern: PRESETS_PRODUCTION_ROUTE, handler: handleGetProductionPreset },
|
|
964
|
+
{ method: "PUT", pattern: PRESETS_PRODUCTION_ROUTE, handler: handleSetProductionPreset },
|
|
891
965
|
// Presets — :name/apply (more specific than :name)
|
|
892
966
|
{ method: "PUT", pattern: PRESET_APPLY_REGEX, handler: handleApplyPreset },
|
|
893
967
|
{ method: "POST", pattern: PRESET_APPLY_REGEX, handler: methodNotAllowed },
|
package/dist-plugin/index.js
CHANGED
|
@@ -132,7 +132,7 @@ function themeFileApi(opts) {
|
|
|
132
132
|
themesResource.ensureDir();
|
|
133
133
|
if (!fs2.existsSync(path2.join(THEMES_DIR, "default.json"))) {
|
|
134
134
|
const defaultTheme = {
|
|
135
|
-
name: "Default",
|
|
135
|
+
name: "Default Theme",
|
|
136
136
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
137
137
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
138
138
|
editorConfigs: {},
|
|
@@ -333,7 +333,7 @@ ${newVars}
|
|
|
333
333
|
}
|
|
334
334
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
335
335
|
const defaultPreset = {
|
|
336
|
-
name: "Default",
|
|
336
|
+
name: "Default Preset",
|
|
337
337
|
createdAt: now,
|
|
338
338
|
updatedAt: now,
|
|
339
339
|
theme: themesResource.getActiveName(),
|
|
@@ -408,6 +408,7 @@ ${COMPONENT_OVERRIDES_END}
|
|
|
408
408
|
const COMPONENT_CONFIGS_ROUTE = `${API_BASE}/component-configs`;
|
|
409
409
|
const PRESETS_ROUTE = `${API_BASE}/presets`;
|
|
410
410
|
const PRESETS_ACTIVE_ROUTE = `${API_BASE}/presets/active`;
|
|
411
|
+
const PRESETS_PRODUCTION_ROUTE = `${API_BASE}/presets/production`;
|
|
411
412
|
const THEME_BY_NAME_REGEX = new RegExp(`^${escapedBase}/themes/([a-z0-9\\-_]+)$`);
|
|
412
413
|
const COMP_LIST_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)$`);
|
|
413
414
|
const COMP_ACTIVE_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/active$`);
|
|
@@ -718,6 +719,73 @@ ${COMPONENT_OVERRIDES_END}
|
|
|
718
719
|
presetsResource.setActiveName(fileName);
|
|
719
720
|
jsonResponse(res, 200, { ok: true, activeFile: fileName });
|
|
720
721
|
}
|
|
722
|
+
async function handleGetProductionPreset({ res }) {
|
|
723
|
+
const prodFile = presetsResource.getProductionName();
|
|
724
|
+
const filePath = presetsResource.filePath(prodFile);
|
|
725
|
+
if (!fs2.existsSync(filePath)) {
|
|
726
|
+
jsonResponse(res, 200, {
|
|
727
|
+
fileName: prodFile,
|
|
728
|
+
name: prodFile,
|
|
729
|
+
theme: "default",
|
|
730
|
+
componentConfigs: {},
|
|
731
|
+
updatedAt: ""
|
|
732
|
+
});
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const data = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
736
|
+
jsonResponse(res, 200, {
|
|
737
|
+
fileName: prodFile,
|
|
738
|
+
name: data.name || prodFile,
|
|
739
|
+
theme: data.theme || "default",
|
|
740
|
+
componentConfigs: data.componentConfigs || {},
|
|
741
|
+
updatedAt: data.updatedAt || ""
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
async function handleSetProductionPreset({ req, res }) {
|
|
745
|
+
const body = JSON.parse(await readBody(req));
|
|
746
|
+
const fileName = sanitizeFileName(body.name || "default");
|
|
747
|
+
const presetPath = presetsResource.filePath(fileName);
|
|
748
|
+
if (!fs2.existsSync(presetPath)) {
|
|
749
|
+
jsonResponse(res, 404, { error: "Preset not found" });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const preset = JSON.parse(fs2.readFileSync(presetPath, "utf-8"));
|
|
753
|
+
const themeName = sanitizeFileName(preset.theme || "default");
|
|
754
|
+
if (!fs2.existsSync(themesResource.filePath(themeName))) {
|
|
755
|
+
jsonResponse(res, 422, { error: `Preset references missing theme: ${themeName}` });
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const knownComponents = new Set(listComponentNames());
|
|
759
|
+
const componentConfigs = preset.componentConfigs ?? {};
|
|
760
|
+
const apply = [];
|
|
761
|
+
for (const [comp, configFile] of Object.entries(componentConfigs)) {
|
|
762
|
+
if (!knownComponents.has(comp)) continue;
|
|
763
|
+
const sanitized = sanitizeFileName(String(configFile) || "default");
|
|
764
|
+
if (!fs2.existsSync(componentResource(comp).filePath(sanitized))) {
|
|
765
|
+
jsonResponse(res, 422, {
|
|
766
|
+
error: `Preset references missing config: ${comp}/${sanitized}`
|
|
767
|
+
});
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
apply.push([comp, sanitized]);
|
|
771
|
+
}
|
|
772
|
+
themesResource.setProductionName(themeName);
|
|
773
|
+
for (const [comp, configFile] of apply) {
|
|
774
|
+
componentResource(comp).setProductionName(configFile);
|
|
775
|
+
}
|
|
776
|
+
presetsResource.setProductionName(fileName);
|
|
777
|
+
syncTokensToCss(themeName);
|
|
778
|
+
syncFontsToCss(themeName);
|
|
779
|
+
syncComponentsToCss();
|
|
780
|
+
jsonResponse(res, 200, {
|
|
781
|
+
ok: true,
|
|
782
|
+
fileName,
|
|
783
|
+
name: preset.name || fileName,
|
|
784
|
+
theme: themeName,
|
|
785
|
+
componentConfigs: Object.fromEntries(apply),
|
|
786
|
+
updatedAt: preset.updatedAt || ""
|
|
787
|
+
});
|
|
788
|
+
}
|
|
721
789
|
async function handlePresetByName({ params, req, res }) {
|
|
722
790
|
const [fileName] = params;
|
|
723
791
|
const filePath = presetsResource.filePath(fileName);
|
|
@@ -732,6 +800,10 @@ ${COMPONENT_OVERRIDES_END}
|
|
|
732
800
|
return;
|
|
733
801
|
}
|
|
734
802
|
if (req.method === "PUT") {
|
|
803
|
+
if (fileName === "default") {
|
|
804
|
+
jsonResponse(res, 403, { error: "Cannot overwrite the default preset" });
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
735
807
|
const body = JSON.parse(await readBody(req));
|
|
736
808
|
body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
737
809
|
if (fs2.existsSync(filePath)) {
|
|
@@ -848,10 +920,12 @@ ${COMPONENT_OVERRIDES_END}
|
|
|
848
920
|
{ method: "GET", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
|
|
849
921
|
{ method: "PUT", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
|
|
850
922
|
{ method: "DELETE", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
|
|
851
|
-
// Presets — list / active are exact strings, must run before regexes
|
|
923
|
+
// Presets — list / active / production are exact strings, must run before regexes
|
|
852
924
|
{ method: "GET", pattern: PRESETS_ROUTE, handler: handleListPresets },
|
|
853
925
|
{ method: "GET", pattern: PRESETS_ACTIVE_ROUTE, handler: handleGetActivePreset },
|
|
854
926
|
{ method: "PUT", pattern: PRESETS_ACTIVE_ROUTE, handler: handleSetActivePreset },
|
|
927
|
+
{ method: "GET", pattern: PRESETS_PRODUCTION_ROUTE, handler: handleGetProductionPreset },
|
|
928
|
+
{ method: "PUT", pattern: PRESETS_PRODUCTION_ROUTE, handler: handleSetProductionPreset },
|
|
855
929
|
// Presets — :name/apply (more specific than :name)
|
|
856
930
|
{ method: "PUT", pattern: PRESET_APPLY_REGEX, handler: handleApplyPreset },
|
|
857
931
|
{ method: "POST", pattern: PRESET_APPLY_REGEX, handler: methodNotAllowed },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motion-proto/live-tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 6/7.",
|
|
6
6
|
"keywords": [
|
|
@@ -29,11 +29,12 @@
|
|
|
29
29
|
"src/pages/ComponentEditorPage.svelte",
|
|
30
30
|
"src/pages/ComponentEditorPage.svelte.d.ts",
|
|
31
31
|
"src/styles/ui-editor.css",
|
|
32
|
-
"src/styles/form-controls.css",
|
|
32
|
+
"src/styles/ui-form-controls.css",
|
|
33
33
|
"src/styles/fonts.css",
|
|
34
34
|
"src/styles/fonts",
|
|
35
35
|
"src/styles/_padding.scss",
|
|
36
36
|
"src/styles/tokens.css",
|
|
37
|
+
"src/styles/site.css",
|
|
37
38
|
"src/data",
|
|
38
39
|
"src/assets",
|
|
39
40
|
"dist-plugin",
|
|
@@ -77,8 +78,9 @@
|
|
|
77
78
|
"require": "./dist-plugin/index.cjs"
|
|
78
79
|
},
|
|
79
80
|
"./styles/ui-editor.css": "./src/styles/ui-editor.css",
|
|
80
|
-
"./
|
|
81
|
-
"./
|
|
81
|
+
"./starter/tokens.css": "./src/styles/tokens.css",
|
|
82
|
+
"./starter/site.css": "./src/styles/site.css",
|
|
83
|
+
"./starter/fonts.css": "./src/styles/fonts.css"
|
|
82
84
|
},
|
|
83
85
|
"scripts": {
|
|
84
86
|
"dev": "vite",
|
|
@@ -90,7 +92,10 @@
|
|
|
90
92
|
"build:plugin": "tsup src/vite-plugin/index.ts --out-dir dist-plugin --format esm,cjs --dts --external vite --platform node --clean",
|
|
91
93
|
"build:lib": "npm run build:plugin",
|
|
92
94
|
"deploy:local": "bash scripts/deploy-local.sh",
|
|
93
|
-
"
|
|
95
|
+
"check:no-style-imports": "node scripts/check-no-style-imports.mjs",
|
|
96
|
+
"check:editor-font-isolation": "node scripts/check-editor-font-isolation.mjs",
|
|
97
|
+
"check:smoke-install": "bash scripts/smoke-install.sh",
|
|
98
|
+
"prepublishOnly": "npm run check:no-style-imports && npm run check:editor-font-isolation && npm run build:lib && npm run check:smoke-install"
|
|
94
99
|
},
|
|
95
100
|
"peerDependencies": {
|
|
96
101
|
"sass": "^1.0",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
|
|
118
118
|
<label>
|
|
119
119
|
<span>Cancel button (left)</span>
|
|
120
|
-
<select class="form-select" value={cancelVariant} onchange={setCancelVariant}>
|
|
120
|
+
<select class="ui-form-select" value={cancelVariant} onchange={setCancelVariant}>
|
|
121
121
|
{#each BUTTON_VARIANTS as v}
|
|
122
122
|
<option value={v}>{variantLabel(v)}</option>
|
|
123
123
|
{/each}
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
</label>
|
|
126
126
|
<label>
|
|
127
127
|
<span>Confirm button (right)</span>
|
|
128
|
-
<select class="form-select" value={confirmVariant} onchange={setConfirmVariant}>
|
|
128
|
+
<select class="ui-form-select" value={confirmVariant} onchange={setConfirmVariant}>
|
|
129
129
|
{#each BUTTON_VARIANTS as v}
|
|
130
130
|
<option value={v}>{variantLabel(v)}</option>
|
|
131
131
|
{/each}
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
</label>
|
|
123
123
|
<label>
|
|
124
124
|
<span>Right button</span>
|
|
125
|
-
<select class="form-select" bind:value={rightOption}>
|
|
125
|
+
<select class="ui-form-select" bind:value={rightOption}>
|
|
126
126
|
{#each BUTTON_VARIANT_OPTIONS as v}
|
|
127
127
|
<option value={v}>{variantLabel(v)}</option>
|
|
128
128
|
{/each}
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
</label>
|
|
131
131
|
<label>
|
|
132
132
|
<span>Left button</span>
|
|
133
|
-
<select class="form-select" bind:value={leftOption}>
|
|
133
|
+
<select class="ui-form-select" bind:value={leftOption}>
|
|
134
134
|
{#each BUTTON_VARIANT_OPTIONS as v}
|
|
135
135
|
<option value={v}>{variantLabel(v)}</option>
|
|
136
136
|
{/each}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
loadComponentActive,
|
|
22
22
|
markComponentSaved,
|
|
23
23
|
} from '../../lib/editorStore';
|
|
24
|
+
import { bumpProductionRevision } from '../../lib/productionPulse';
|
|
24
25
|
import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../../lib/migrations';
|
|
25
26
|
import type { CssVarRef } from '../../lib/editorTypes';
|
|
26
27
|
import { safeFetch } from '../../lib/storage';
|
|
@@ -295,6 +296,7 @@
|
|
|
295
296
|
}
|
|
296
297
|
await setComponentProductionFile(component, activeFileName);
|
|
297
298
|
await refreshProduction();
|
|
299
|
+
bumpProductionRevision();
|
|
298
300
|
adoptFeedback = wasDirty
|
|
299
301
|
? `Saved "${adoptingName}" and adopted`
|
|
300
302
|
: `Adopted "${adoptingName}"`;
|
|
@@ -499,7 +501,7 @@
|
|
|
499
501
|
|
|
500
502
|
.cfm-title-row {
|
|
501
503
|
display: flex;
|
|
502
|
-
align-items:
|
|
504
|
+
align-items: center;
|
|
503
505
|
gap: var(--ui-space-12);
|
|
504
506
|
flex-wrap: wrap;
|
|
505
507
|
}
|
|
@@ -518,6 +520,7 @@
|
|
|
518
520
|
display: inline-flex;
|
|
519
521
|
align-items: center;
|
|
520
522
|
gap: var(--ui-space-6);
|
|
523
|
+
margin-left: 2.5rem;
|
|
521
524
|
height: 26px;
|
|
522
525
|
padding: 0 14px;
|
|
523
526
|
font-size: var(--ui-font-size-xs);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { run } from 'svelte/legacy';
|
|
3
3
|
|
|
4
|
-
import type { ComponentConfigMeta } from '../../lib/themeTypes';
|
|
5
4
|
import { sanitizeFileName } from '../../lib/themeService';
|
|
6
5
|
import UIDialog from '../../ui/UIDialog.svelte';
|
|
7
6
|
|
|
@@ -10,12 +9,30 @@
|
|
|
10
9
|
show?: boolean;
|
|
11
10
|
/** Display name to seed the input with when the dialog opens. */
|
|
12
11
|
currentDisplayName?: string;
|
|
13
|
-
/** Existing files used by the increment helper to find the next available `_NN` suffix.
|
|
14
|
-
|
|
12
|
+
/** Existing files used by the increment helper to find the next available `_NN` suffix.
|
|
13
|
+
* Only `fileName` is read, so this accepts any shape with that field
|
|
14
|
+
* (ComponentConfigMeta, PresetMeta, …). */
|
|
15
|
+
files?: { fileName: string }[];
|
|
16
|
+
/** Dialog title — defaults to "Save As". Overridable so callers can use
|
|
17
|
+
* context-specific framing (e.g. "Save Preset As"). */
|
|
18
|
+
title?: string;
|
|
19
|
+
/** Placeholder shown in the empty input. */
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
/** Error message shown when the user types the reserved "default" name.
|
|
22
|
+
* Default copy references components; presets should override. */
|
|
23
|
+
reservedNameMessage?: string;
|
|
15
24
|
onsave?: (payload: { displayName: string; fileName: string }) => void;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
|
-
let {
|
|
27
|
+
let {
|
|
28
|
+
show = $bindable(false),
|
|
29
|
+
currentDisplayName = '',
|
|
30
|
+
files = [],
|
|
31
|
+
title = 'Save As',
|
|
32
|
+
placeholder = 'Config name…',
|
|
33
|
+
reservedNameMessage = 'The name "default" is reserved for the core component definition.',
|
|
34
|
+
onsave,
|
|
35
|
+
}: Props = $props();
|
|
19
36
|
|
|
20
37
|
let saveAsName = $state('');
|
|
21
38
|
let saveAsInput: HTMLInputElement | undefined = $state();
|
|
@@ -73,7 +90,7 @@
|
|
|
73
90
|
const trimmed = saveAsName.trim();
|
|
74
91
|
if (!trimmed) return '';
|
|
75
92
|
if (sanitizeFileName(trimmed) === 'default') {
|
|
76
|
-
return
|
|
93
|
+
return reservedNameMessage;
|
|
77
94
|
}
|
|
78
95
|
return '';
|
|
79
96
|
})());
|
|
@@ -81,7 +98,7 @@
|
|
|
81
98
|
|
|
82
99
|
<UIDialog
|
|
83
100
|
bind:show
|
|
84
|
-
title
|
|
101
|
+
{title}
|
|
85
102
|
cancelLabel="Cancel"
|
|
86
103
|
confirmLabel="Save"
|
|
87
104
|
confirmDisabled={!saveAsName.trim() || !!saveAsError}
|
|
@@ -97,7 +114,7 @@
|
|
|
97
114
|
bind:value={saveAsName}
|
|
98
115
|
bind:this={saveAsInput}
|
|
99
116
|
onkeydown={handleKeydown}
|
|
100
|
-
placeholder
|
|
117
|
+
{placeholder}
|
|
101
118
|
/>
|
|
102
119
|
<button
|
|
103
120
|
type="button"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { get } from 'svelte/store';
|
|
2
|
+
import type { ComponentConfig } from './themeTypes';
|
|
3
|
+
import { editorState, markComponentSaved } from './editorStore';
|
|
4
|
+
import type { CssVarRef } from './editorTypes';
|
|
5
|
+
import { CURRENT_COMPONENT_SCHEMA_VERSION } from './migrations';
|
|
6
|
+
import {
|
|
7
|
+
listComponentConfigs,
|
|
8
|
+
saveComponentConfig,
|
|
9
|
+
setActiveComponentFile,
|
|
10
|
+
} from './componentConfigService';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Save the current in-memory state of a component to its active file. Mirrors
|
|
14
|
+
* the `persist` flow inside `ComponentFileManager.svelte` so callers without a
|
|
15
|
+
* file-manager instance (e.g. the unsaved-components dialog in PresetFileManager)
|
|
16
|
+
* can save a dirty component without duplicating the schema-version + aliases
|
|
17
|
+
* stringification logic.
|
|
18
|
+
*
|
|
19
|
+
* Refuses to overwrite `default` — that's the protected source-derived snapshot.
|
|
20
|
+
* Callers that hit `{ ok: false, reason: 'default' }` should prompt for a new
|
|
21
|
+
* name and re-route to a Save-As flow.
|
|
22
|
+
*/
|
|
23
|
+
export type SaveActiveComponentResult =
|
|
24
|
+
| { ok: true; fileName: string; displayName: string }
|
|
25
|
+
| { ok: false; reason: 'default' | 'no-state' | 'error'; error?: unknown };
|
|
26
|
+
|
|
27
|
+
function refToString(ref: CssVarRef): string {
|
|
28
|
+
return ref.kind === 'token' ? ref.name : ref.value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function saveActiveComponentConfig(
|
|
32
|
+
component: string,
|
|
33
|
+
): Promise<SaveActiveComponentResult> {
|
|
34
|
+
const slice = get(editorState).components[component];
|
|
35
|
+
if (!slice) return { ok: false, reason: 'no-state' };
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const list = await listComponentConfigs(component);
|
|
39
|
+
const fileName = list.activeFile;
|
|
40
|
+
if (fileName === 'default') return { ok: false, reason: 'default' };
|
|
41
|
+
|
|
42
|
+
const active = list.files.find((f) => f.fileName === fileName);
|
|
43
|
+
const displayName = active?.name ?? fileName;
|
|
44
|
+
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
const aliases: Record<string, string> = {};
|
|
47
|
+
for (const [k, ref] of Object.entries(slice.aliases)) aliases[k] = refToString(ref);
|
|
48
|
+
|
|
49
|
+
const data: ComponentConfig = {
|
|
50
|
+
name: displayName,
|
|
51
|
+
component,
|
|
52
|
+
createdAt: now,
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
aliases,
|
|
55
|
+
config: { ...slice.config },
|
|
56
|
+
schemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
|
|
57
|
+
};
|
|
58
|
+
await saveComponentConfig(component, fileName, data);
|
|
59
|
+
await setActiveComponentFile(component, fileName);
|
|
60
|
+
markComponentSaved(component);
|
|
61
|
+
return { ok: true, fileName, displayName };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { ok: false, reason: 'error', error };
|
|
64
|
+
}
|
|
65
|
+
}
|