@motion-proto/live-tokens 0.25.1 → 0.28.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-build-page/SKILL.md +6 -4
- package/README.md +28 -3
- package/dist-plugin/index.cjs +120 -115
- package/dist-plugin/index.js +120 -115
- package/package.json +14 -5
- package/src/editor/core/store/editorPersistence.ts +23 -1
- package/src/editor/docs/CodeBlock.svelte +92 -0
- package/src/editor/docs/Docs.svelte +658 -0
- package/src/editor/docs/Docs.svelte.d.ts +2 -0
- package/src/editor/docs/chapters.ts +44 -0
- package/src/editor/docs/content/01-overview.md +31 -0
- package/src/editor/docs/content/creating-components.md +40 -0
- package/src/editor/docs/content/editing-tokens.md +74 -0
- package/src/editor/docs/content/getting-started.md +67 -0
- package/src/editor/docs/content/themes-workflow.md +60 -0
- package/src/editor/overlay/LiveTokensRouter.svelte +71 -13
- package/src/editor/pages/ComponentEditorPage.svelte +0 -11
- package/src/editor/ui/ManifestFileManager.svelte +15 -5
- package/src/editor/ui/ThemeFileManager.svelte +6 -2
- package/src/live-tokens/data/manifests/default.json +35 -0
- package/src/live-tokens/data/themes/default.json +2295 -0
- package/src/live-tokens/data/tokens.generated.css +9 -9
- package/src/system/components/CodeSnippet.svelte +4 -0
- package/src/system/components/SideNavigation.svelte +13 -13
- package/template/README.md +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: live-tokens-build-page
|
|
3
|
-
description: Apply the @motion-proto/live-tokens project conventions when building a page: use shipped components from the catalogue, reference theme tokens (never hex/pixel literals), mount routes dynamically, register
|
|
3
|
+
description: Apply the @motion-proto/live-tokens project conventions when building a page: use shipped components from the catalogue, reference theme tokens (never hex/pixel literals), mount routes dynamically, register each route's page source, and import site.css per-page. Use when the user asks to build / create / lay out a page, route, hero, marketing page, landing page, dashboard, settings screen, or pricing page; add a route; place / drop / use an existing component on a page; or assemble a screen from the live-tokens catalogue. For component-choice decisions, see live-tokens-pick-component. For authoring a brand-new component, see live-tokens-create-component.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Building pages in a live-tokens project
|
|
@@ -18,8 +18,10 @@ To place children at specific page-column positions, span the parent grid (`grid
|
|
|
18
18
|
|
|
19
19
|
## Wiring
|
|
20
20
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
21
|
+
- Add the route the way `App.svelte` already wires routes:
|
|
22
|
+
- **`<LiveTokensRouter pages={...}>`** (the usual case): add a `pages` entry as `lazy: () => import('./YourPage.svelte')` with a `source: 'src/...'` (and a `label`/`icon` to show it in the nav rail). For a route you can't enumerate (a `/:id`, a path prefix, a gated page), add a `resolve(path) => RouteEntry | null` instead of a `pages` key; same entry shape, so `props` and `source` (hence "Page Source") work identically.
|
|
23
|
+
- **Manual `<LiveEditorOverlay>`**: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.
|
|
24
|
+
Either way use `lazy`, not a static top-level import: static imports evaluate every page module at boot and leak page CSS into the editor routes.
|
|
23
25
|
- Import `site.css` from each page's `<script>` block, never from `main.ts` (would leak into editor routes).
|
|
24
26
|
|
|
25
27
|
## Avoid
|
|
@@ -32,4 +34,4 @@ To place children at specific page-column positions, span the parent grid (`grid
|
|
|
32
34
|
|
|
33
35
|
## Verify
|
|
34
36
|
|
|
35
|
-
In dev: change a colour in `/editor` and confirm your page repaints (proves token usage). The overlay's "Page Source" button on the new route opens the page in VS Code (proves the `
|
|
37
|
+
In dev: change a colour in `/editor` and confirm your page repaints (proves token usage). The overlay's "Page Source" button on the new route opens the page in VS Code (proves the route's `source`). `ColumnsOverlay` (Cmd+G) shows content sitting inside `--columns-max-width`.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A foundational design system for quickly styling and building Svelte + Vite micr
|
|
|
11
11
|
- **Theme editor** (`/editor` route, dev-only) — the home of real-time token editing. Save themes to disk as JSON, promote one to "production" to bake it into a static `tokens.css` for the build.
|
|
12
12
|
- **Per-component editor** (`/components` route, dev-only) — the home of real-time component-alias editing. Pick token aliases per component without writing CSS.
|
|
13
13
|
- **Live editor overlay** — pins to the top-right of every dev page. Opens the editor in a side panel or floating window so you edit *on the page you're styling*, not in a separate tab. Includes a "Page Source" button that opens the current page's `.svelte` file in VS Code.
|
|
14
|
-
- **
|
|
14
|
+
- **Manifests** — a manifest captures a whole site configuration as one portable artifact: the theme in one slot, every component in its own slot, each holding either the shipped default or a custom file. Export it as a bundle and import it into another project to restore the full styling in one step.
|
|
15
15
|
- **Vite plugin** — hosts the `/api/live-tokens/{themes,component-configs,manifests}/*` routes that persist your edits to disk as you make them. The single namespace keeps live-tokens' routes from colliding with anything your app serves under `/api`.
|
|
16
16
|
- **Claude Code skill suite** — three bundled skills so you can drive the package in plain English. `build-page` composes pages from the shipped components. `pick-component` decides between confusing pairs (TabBar vs SegmentedControl, Card vs CollapsibleSection). `create-component` authors a new editable component against the project's naming, state-model, and import rules. One command to install all three: `npx @motion-proto/live-tokens setup-claude`. See [Claude Code skills](#claude-code-skills) below.
|
|
17
17
|
|
|
@@ -122,7 +122,7 @@ bootLiveTokens(App, '#app', {
|
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
`<LiveTokensRouter>` owns the dev overlay (`<LiveEditorOverlay>` +
|
|
125
|
-
`<ColumnsOverlay>`), the editor routes (`/editor`, `/components`), the
|
|
125
|
+
`<ColumnsOverlay>`), the editor routes (`/editor`, `/components`, `/docs`), the
|
|
126
126
|
in-app link-click interception, and the nav-rail/page-source plumbing the
|
|
127
127
|
overlay needs. Each entry in `pages` is one of your routes; entries with a
|
|
128
128
|
`label` appear in the overlay's nav rail. Pass pages as `lazy: () => import('./Page.svelte')`
|
|
@@ -131,6 +131,28 @@ visited; pass `component: PageComponent` instead for an eagerly-imported
|
|
|
131
131
|
page. The editor routes are dispatched internally, so you don't have to
|
|
132
132
|
dynamic-import the library's editor pages yourself.
|
|
133
133
|
|
|
134
|
+
For routes you can't enumerate ahead of time (a `/:id` or `/:slug`, a path
|
|
135
|
+
prefix, or a page shown only when some condition holds), add a `resolve`
|
|
136
|
+
function from the current path to a `RouteEntry`; return `null` to fall
|
|
137
|
+
through. It's plain code, so params, prefixes, and gating are a regex and an
|
|
138
|
+
`if`, and the package ships no route syntax of its own. Resolution order is
|
|
139
|
+
`pages[path]`, then `resolve(path)`, then the `pages['/']` fallback, so adding
|
|
140
|
+
`resolve` never changes how existing `pages` entries match. A resolved entry
|
|
141
|
+
can carry `props`, letting one page component serve many paths (such as the
|
|
142
|
+
matched id), and its `source` gives the dynamic route a working "Page Source"
|
|
143
|
+
button just like a static one.
|
|
144
|
+
|
|
145
|
+
```svelte
|
|
146
|
+
<LiveTokensRouter
|
|
147
|
+
pages={{ '/': { lazy: () => import('./Home.svelte'), label: 'Home' } }}
|
|
148
|
+
resolve={(path) => {
|
|
149
|
+
const m = path.match(/^\/module\/(.+)$/);
|
|
150
|
+
if (!m) return null;
|
|
151
|
+
return { lazy: () => import('./ModulePage.svelte'), props: { id: m[1] }, source: 'src/ModulePage.svelte' };
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
```
|
|
155
|
+
|
|
134
156
|
You can also relocate or disable a default editor route via the
|
|
135
157
|
`editorRoutes` prop: `<LiveTokensRouter pages={…} editorRoutes={{ editor: '/admin/editor', components: false }} />`.
|
|
136
158
|
Pass a string to move a route; pass `false` to remove the route entirely
|
|
@@ -147,7 +169,10 @@ individual init functions (`initCssVarSync`, `initRouter`,
|
|
|
147
169
|
`<LiveEditorOverlay>`, `<ColumnsOverlay>`, and the editor page exports
|
|
148
170
|
(`@motion-proto/live-tokens/editor`,
|
|
149
171
|
`@motion-proto/live-tokens/component-editor-page`) all stay exported. Use
|
|
150
|
-
them directly
|
|
172
|
+
them directly to build a custom shell: render arbitrary markup per route, host
|
|
173
|
+
a foreign matcher, or drive the overlay yourself. You do **not** need this for
|
|
174
|
+
dynamic or gated routes; reach for `resolve` above, which keeps the overlay,
|
|
175
|
+
nav rail, and page-source intact.
|
|
151
176
|
|
|
152
177
|
### Use components
|
|
153
178
|
|
package/dist-plugin/index.cjs
CHANGED
|
@@ -477,6 +477,8 @@ function versionedFileResourceServer(opts) {
|
|
|
477
477
|
const activePath = import_path.default.join(dir, "_active.json");
|
|
478
478
|
const productionPath = import_path.default.join(dir, "_production.json");
|
|
479
479
|
const defaultName = opts.defaultName ?? "default";
|
|
480
|
+
const resolvedDir = import_path.default.resolve(dir);
|
|
481
|
+
const resolvedPackageDir = opts.packageDir ? import_path.default.resolve(opts.packageDir) : null;
|
|
480
482
|
function ensureDir() {
|
|
481
483
|
if (!import_fs.default.existsSync(dir)) {
|
|
482
484
|
import_fs.default.mkdirSync(dir, { recursive: true });
|
|
@@ -493,6 +495,37 @@ function versionedFileResourceServer(opts) {
|
|
|
493
495
|
function filePath(name) {
|
|
494
496
|
return import_path.default.join(dir, `${name}.json`);
|
|
495
497
|
}
|
|
498
|
+
function existingPath(name) {
|
|
499
|
+
const local = filePath(name);
|
|
500
|
+
if (import_fs.default.existsSync(local)) return local;
|
|
501
|
+
if (resolvedPackageDir) {
|
|
502
|
+
const pkg = import_path.default.join(resolvedPackageDir, `${name}.json`);
|
|
503
|
+
if (import_fs.default.existsSync(pkg)) return pkg;
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
function readJson(name) {
|
|
508
|
+
const resolved = existingPath(name);
|
|
509
|
+
if (!resolved) return null;
|
|
510
|
+
return JSON.parse(import_fs.default.readFileSync(resolved, "utf-8"));
|
|
511
|
+
}
|
|
512
|
+
function listNames() {
|
|
513
|
+
const names = [];
|
|
514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
515
|
+
const collect = (d) => {
|
|
516
|
+
if (!d || !import_fs.default.existsSync(d)) return;
|
|
517
|
+
for (const f of import_fs.default.readdirSync(d)) {
|
|
518
|
+
if (!f.endsWith(".json") || f.startsWith("_")) continue;
|
|
519
|
+
const name = f.slice(0, -".json".length);
|
|
520
|
+
if (seen.has(name)) continue;
|
|
521
|
+
seen.add(name);
|
|
522
|
+
names.push(name);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
collect(resolvedDir);
|
|
526
|
+
if (resolvedPackageDir && resolvedPackageDir !== resolvedDir) collect(resolvedPackageDir);
|
|
527
|
+
return names;
|
|
528
|
+
}
|
|
496
529
|
function getActiveName() {
|
|
497
530
|
try {
|
|
498
531
|
const data = JSON.parse(import_fs.default.readFileSync(activePath, "utf-8"));
|
|
@@ -522,6 +555,9 @@ function versionedFileResourceServer(opts) {
|
|
|
522
555
|
ensureDir,
|
|
523
556
|
ensureMeta,
|
|
524
557
|
filePath,
|
|
558
|
+
existingPath,
|
|
559
|
+
readJson,
|
|
560
|
+
listNames,
|
|
525
561
|
getActiveName,
|
|
526
562
|
getProductionName,
|
|
527
563
|
setActiveName,
|
|
@@ -826,36 +862,42 @@ function themeFileApi(opts) {
|
|
|
826
862
|
"system",
|
|
827
863
|
"components"
|
|
828
864
|
);
|
|
865
|
+
const packageDataDir = import_path3.default.resolve(
|
|
866
|
+
import_path3.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)),
|
|
867
|
+
"..",
|
|
868
|
+
"src",
|
|
869
|
+
"live-tokens",
|
|
870
|
+
"data"
|
|
871
|
+
);
|
|
872
|
+
const packageThemesDir = import_path3.default.join(packageDataDir, "themes");
|
|
873
|
+
const packageManifestsDir = import_path3.default.join(packageDataDir, "manifests");
|
|
874
|
+
const packageComponentConfigsDir = import_path3.default.join(packageDataDir, "component-configs");
|
|
829
875
|
const COMPONENTS_SCAN_DIRS = [...consumerComponentDirs];
|
|
830
876
|
if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && import_fs3.default.existsSync(packageComponentsDir)) {
|
|
831
877
|
COMPONENTS_SCAN_DIRS.push(packageComponentsDir);
|
|
832
878
|
}
|
|
833
|
-
const LEGACY_PRESETS_DIR = import_path3.default.resolve("presets");
|
|
834
879
|
const themesResource = versionedFileResourceServer({
|
|
835
|
-
dir: THEMES_DIR
|
|
880
|
+
dir: THEMES_DIR,
|
|
881
|
+
packageDir: packageThemesDir
|
|
836
882
|
});
|
|
837
883
|
const componentResourceCache = /* @__PURE__ */ new Map();
|
|
838
884
|
function componentResource(comp) {
|
|
839
885
|
let r = componentResourceCache.get(comp);
|
|
840
886
|
if (!r) {
|
|
841
|
-
r = versionedFileResourceServer({
|
|
887
|
+
r = versionedFileResourceServer({
|
|
888
|
+
dir: import_path3.default.join(COMPONENT_CONFIGS_DIR, comp),
|
|
889
|
+
packageDir: import_path3.default.join(packageComponentConfigsDir, comp)
|
|
890
|
+
});
|
|
842
891
|
componentResourceCache.set(comp, r);
|
|
843
892
|
}
|
|
844
893
|
return r;
|
|
845
894
|
}
|
|
846
|
-
const manifestsResource = versionedFileResourceServer({
|
|
895
|
+
const manifestsResource = versionedFileResourceServer({
|
|
896
|
+
dir: MANIFESTS_DIR,
|
|
897
|
+
packageDir: packageManifestsDir
|
|
898
|
+
});
|
|
847
899
|
function ensureThemesDir() {
|
|
848
900
|
themesResource.ensureDir();
|
|
849
|
-
if (!import_fs3.default.existsSync(import_path3.default.join(THEMES_DIR, "default.json"))) {
|
|
850
|
-
const defaultTheme = {
|
|
851
|
-
name: "Default Theme",
|
|
852
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
853
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
854
|
-
editorConfigs: {},
|
|
855
|
-
cssVariables: {}
|
|
856
|
-
};
|
|
857
|
-
import_fs3.default.writeFileSync(import_path3.default.join(THEMES_DIR, "default.json"), JSON.stringify(defaultTheme, null, 2));
|
|
858
|
-
}
|
|
859
901
|
themesResource.ensureMeta();
|
|
860
902
|
}
|
|
861
903
|
function readBody(req) {
|
|
@@ -919,10 +961,9 @@ function themeFileApi(opts) {
|
|
|
919
961
|
lines.push("/* tokens.css holds developer-authored defaults; this file holds editor overrides. */");
|
|
920
962
|
lines.push("");
|
|
921
963
|
const productionThemeName = themesResource.getProductionName();
|
|
922
|
-
const
|
|
964
|
+
const themeData = themesResource.readJson(productionThemeName);
|
|
923
965
|
let themeVarCount = 0;
|
|
924
|
-
if (
|
|
925
|
-
const themeData = JSON.parse(import_fs3.default.readFileSync(themePath, "utf-8"));
|
|
966
|
+
if (themeData) {
|
|
926
967
|
const cssVars = { ...themeData.cssVariables || {} };
|
|
927
968
|
Object.assign(cssVars, palettesToVars(themeData.editorConfigs ?? {}));
|
|
928
969
|
const resolvedFontVars = resolveFontStacks(themeData);
|
|
@@ -987,9 +1028,8 @@ function themeFileApi(opts) {
|
|
|
987
1028
|
regenerateTokensCss();
|
|
988
1029
|
}
|
|
989
1030
|
function syncFontsToCss(fileName) {
|
|
990
|
-
const
|
|
991
|
-
if (!
|
|
992
|
-
const themeData = JSON.parse(import_fs3.default.readFileSync(themePath, "utf-8"));
|
|
1031
|
+
const themeData = themesResource.readJson(fileName);
|
|
1032
|
+
if (!themeData) return;
|
|
993
1033
|
const sources = themeData.fontSources;
|
|
994
1034
|
if (!sources) return;
|
|
995
1035
|
const lines = [];
|
|
@@ -1149,28 +1189,7 @@ ${lines.join("\n")}
|
|
|
1149
1189
|
}
|
|
1150
1190
|
}
|
|
1151
1191
|
function ensureManifestsDir() {
|
|
1152
|
-
if (!import_fs3.default.existsSync(MANIFESTS_DIR) && import_fs3.default.existsSync(LEGACY_PRESETS_DIR)) {
|
|
1153
|
-
import_fs3.default.renameSync(LEGACY_PRESETS_DIR, MANIFESTS_DIR);
|
|
1154
|
-
const legacyProd = import_path3.default.join(MANIFESTS_DIR, "_production.json");
|
|
1155
|
-
if (import_fs3.default.existsSync(legacyProd)) import_fs3.default.unlinkSync(legacyProd);
|
|
1156
|
-
}
|
|
1157
1192
|
manifestsResource.ensureDir();
|
|
1158
|
-
const defaultPath = import_path3.default.join(MANIFESTS_DIR, "default.json");
|
|
1159
|
-
if (!import_fs3.default.existsSync(defaultPath)) {
|
|
1160
|
-
const componentConfigs = {};
|
|
1161
|
-
for (const comp of listComponentNames()) {
|
|
1162
|
-
componentConfigs[comp] = componentResource(comp).getActiveName();
|
|
1163
|
-
}
|
|
1164
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1165
|
-
const defaultManifest = {
|
|
1166
|
-
name: "Default",
|
|
1167
|
-
createdAt: now,
|
|
1168
|
-
updatedAt: now,
|
|
1169
|
-
theme: themesResource.getActiveName(),
|
|
1170
|
-
componentConfigs
|
|
1171
|
-
};
|
|
1172
|
-
import_fs3.default.writeFileSync(defaultPath, JSON.stringify(defaultManifest, null, 2));
|
|
1173
|
-
}
|
|
1174
1193
|
if (!import_fs3.default.existsSync(manifestsResource.activePath)) {
|
|
1175
1194
|
import_fs3.default.writeFileSync(
|
|
1176
1195
|
manifestsResource.activePath,
|
|
@@ -1178,12 +1197,10 @@ ${lines.join("\n")}
|
|
|
1178
1197
|
);
|
|
1179
1198
|
} else {
|
|
1180
1199
|
const activeName = manifestsResource.getActiveName();
|
|
1181
|
-
if (
|
|
1200
|
+
if (manifestsResource.existingPath(activeName) === null) {
|
|
1182
1201
|
manifestsResource.setActiveName("default");
|
|
1183
1202
|
}
|
|
1184
1203
|
}
|
|
1185
|
-
const stragglerProd = import_path3.default.join(MANIFESTS_DIR, "_production.json");
|
|
1186
|
-
if (import_fs3.default.existsSync(stragglerProd)) import_fs3.default.unlinkSync(stragglerProd);
|
|
1187
1204
|
}
|
|
1188
1205
|
function patchActiveManifest(field, comp, fileName) {
|
|
1189
1206
|
const activeFile = manifestsResource.getActiveName();
|
|
@@ -1234,10 +1251,8 @@ ${lines.join("\n")}
|
|
|
1234
1251
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
1235
1252
|
}
|
|
1236
1253
|
function readComponentConfig(comp, name) {
|
|
1237
|
-
const filePath = componentResource(comp).filePath(name);
|
|
1238
|
-
if (!import_fs3.default.existsSync(filePath)) return null;
|
|
1239
1254
|
try {
|
|
1240
|
-
return
|
|
1255
|
+
return componentResource(comp).readJson(name);
|
|
1241
1256
|
} catch {
|
|
1242
1257
|
return null;
|
|
1243
1258
|
}
|
|
@@ -1263,14 +1278,12 @@ ${lines.join("\n")}
|
|
|
1263
1278
|
const MANIFEST_BY_NAME_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)$`);
|
|
1264
1279
|
async function handleListThemes(_ctx) {
|
|
1265
1280
|
const activeFile = themesResource.getActiveName();
|
|
1266
|
-
const files =
|
|
1267
|
-
const
|
|
1268
|
-
const data = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
|
|
1269
|
-
const fileName = f.replace(".json", "");
|
|
1281
|
+
const files = themesResource.listNames().map((fileName) => {
|
|
1282
|
+
const data = themesResource.readJson(fileName);
|
|
1270
1283
|
return {
|
|
1271
|
-
name: data
|
|
1284
|
+
name: data?.name || fileName,
|
|
1272
1285
|
fileName,
|
|
1273
|
-
updatedAt: data
|
|
1286
|
+
updatedAt: data?.updatedAt || "",
|
|
1274
1287
|
isActive: fileName === activeFile
|
|
1275
1288
|
};
|
|
1276
1289
|
});
|
|
@@ -1278,19 +1291,19 @@ ${lines.join("\n")}
|
|
|
1278
1291
|
}
|
|
1279
1292
|
async function handleGetActiveTheme({ res }) {
|
|
1280
1293
|
const activeFile = themesResource.getActiveName();
|
|
1281
|
-
const
|
|
1282
|
-
if (!
|
|
1294
|
+
const raw = themesResource.readJson(activeFile);
|
|
1295
|
+
if (!raw) {
|
|
1283
1296
|
jsonResponse(res, 404, { error: "Active theme not found" });
|
|
1284
1297
|
return;
|
|
1285
1298
|
}
|
|
1286
|
-
const data = normalizeTheme(
|
|
1299
|
+
const data = normalizeTheme(raw);
|
|
1287
1300
|
data._fileName = activeFile;
|
|
1288
1301
|
jsonResponse(res, 200, data);
|
|
1289
1302
|
}
|
|
1290
1303
|
async function handleSetActiveTheme({ req, res }) {
|
|
1291
1304
|
const body = JSON.parse(await readBody(req));
|
|
1292
1305
|
const fileName = sanitizeFileName(body.name || "default");
|
|
1293
|
-
if (
|
|
1306
|
+
if (themesResource.existingPath(fileName) === null) {
|
|
1294
1307
|
jsonResponse(res, 404, { error: "Theme not found" });
|
|
1295
1308
|
return;
|
|
1296
1309
|
}
|
|
@@ -1299,12 +1312,12 @@ ${lines.join("\n")}
|
|
|
1299
1312
|
}
|
|
1300
1313
|
async function handleGetProductionTheme({ res }) {
|
|
1301
1314
|
const prodFile = themesResource.getProductionName();
|
|
1302
|
-
const
|
|
1303
|
-
if (!
|
|
1315
|
+
const raw = themesResource.readJson(prodFile);
|
|
1316
|
+
if (!raw) {
|
|
1304
1317
|
jsonResponse(res, 200, { fileName: prodFile, name: prodFile, cssVariables: {} });
|
|
1305
1318
|
return;
|
|
1306
1319
|
}
|
|
1307
|
-
const data = normalizeTheme(
|
|
1320
|
+
const data = normalizeTheme(raw);
|
|
1308
1321
|
jsonResponse(res, 200, {
|
|
1309
1322
|
fileName: prodFile,
|
|
1310
1323
|
name: data.name || prodFile,
|
|
@@ -1315,7 +1328,7 @@ ${lines.join("\n")}
|
|
|
1315
1328
|
async function handleSetProductionTheme({ req, res }) {
|
|
1316
1329
|
const body = JSON.parse(await readBody(req));
|
|
1317
1330
|
const fileName = sanitizeFileName(body.name || "default");
|
|
1318
|
-
if (
|
|
1331
|
+
if (themesResource.existingPath(fileName) === null) {
|
|
1319
1332
|
jsonResponse(res, 404, { error: "Theme not found" });
|
|
1320
1333
|
return;
|
|
1321
1334
|
}
|
|
@@ -1331,28 +1344,33 @@ ${lines.join("\n")}
|
|
|
1331
1344
|
syncFontsToCss(fileName);
|
|
1332
1345
|
syncComponentsToCss();
|
|
1333
1346
|
patchActiveManifest("theme", null, fileName);
|
|
1334
|
-
const data =
|
|
1347
|
+
const data = themesResource.readJson(fileName);
|
|
1335
1348
|
jsonResponse(res, 200, {
|
|
1336
1349
|
ok: true,
|
|
1337
1350
|
fileName,
|
|
1338
|
-
name: data
|
|
1339
|
-
updatedAt: data
|
|
1351
|
+
name: data?.name || fileName,
|
|
1352
|
+
updatedAt: data?.updatedAt || ""
|
|
1340
1353
|
});
|
|
1341
1354
|
}
|
|
1342
1355
|
async function handleThemeByName({ params, req, res }) {
|
|
1343
1356
|
const [fileName] = params;
|
|
1344
1357
|
const filePath = themesResource.filePath(fileName);
|
|
1345
1358
|
if (req.method === "GET") {
|
|
1346
|
-
|
|
1359
|
+
const raw = themesResource.readJson(fileName);
|
|
1360
|
+
if (!raw) {
|
|
1347
1361
|
jsonResponse(res, 404, { error: "Not found" });
|
|
1348
1362
|
return;
|
|
1349
1363
|
}
|
|
1350
|
-
const data = normalizeTheme(
|
|
1364
|
+
const data = normalizeTheme(raw);
|
|
1351
1365
|
data._fileName = fileName;
|
|
1352
1366
|
jsonResponse(res, 200, data);
|
|
1353
1367
|
return;
|
|
1354
1368
|
}
|
|
1355
1369
|
if (req.method === "PUT") {
|
|
1370
|
+
if (fileName === "default") {
|
|
1371
|
+
jsonResponse(res, 403, { error: "Cannot overwrite the default theme (live from package)" });
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1356
1374
|
const body = JSON.parse(await readBody(req));
|
|
1357
1375
|
body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1358
1376
|
if (import_fs3.default.existsSync(filePath)) {
|
|
@@ -1421,8 +1439,7 @@ ${lines.join("\n")}
|
|
|
1421
1439
|
const body = JSON.parse(await readBody(req));
|
|
1422
1440
|
const fileName = sanitizeFileName(body.name || "default");
|
|
1423
1441
|
const r = componentResource(comp);
|
|
1424
|
-
|
|
1425
|
-
if (!import_fs3.default.existsSync(configPath)) {
|
|
1442
|
+
if (r.existingPath(fileName) === null) {
|
|
1426
1443
|
jsonResponse(res, 404, { error: "Config not found" });
|
|
1427
1444
|
return;
|
|
1428
1445
|
}
|
|
@@ -1446,8 +1463,7 @@ ${lines.join("\n")}
|
|
|
1446
1463
|
const body = JSON.parse(await readBody(req));
|
|
1447
1464
|
const fileName = sanitizeFileName(body.name || "default");
|
|
1448
1465
|
const r = componentResource(comp);
|
|
1449
|
-
|
|
1450
|
-
if (!import_fs3.default.existsSync(configPath)) {
|
|
1466
|
+
if (r.existingPath(fileName) === null) {
|
|
1451
1467
|
jsonResponse(res, 404, { error: "Config not found" });
|
|
1452
1468
|
return;
|
|
1453
1469
|
}
|
|
@@ -1475,11 +1491,12 @@ ${lines.join("\n")}
|
|
|
1475
1491
|
const r = componentResource(comp);
|
|
1476
1492
|
const configPath = r.filePath(name);
|
|
1477
1493
|
if (req.method === "GET") {
|
|
1478
|
-
|
|
1494
|
+
const raw = r.readJson(name);
|
|
1495
|
+
if (!raw) {
|
|
1479
1496
|
jsonResponse(res, 404, { error: "Not found" });
|
|
1480
1497
|
return;
|
|
1481
1498
|
}
|
|
1482
|
-
const data =
|
|
1499
|
+
const data = raw;
|
|
1483
1500
|
data._fileName = name;
|
|
1484
1501
|
jsonResponse(res, 200, data);
|
|
1485
1502
|
return;
|
|
@@ -1537,14 +1554,12 @@ ${lines.join("\n")}
|
|
|
1537
1554
|
}
|
|
1538
1555
|
const activeFile = r.getActiveName();
|
|
1539
1556
|
const productionFile = r.getProductionName();
|
|
1540
|
-
const files =
|
|
1541
|
-
const
|
|
1542
|
-
const data = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
|
|
1543
|
-
const fileName = f.replace(".json", "");
|
|
1557
|
+
const files = r.listNames().map((fileName) => {
|
|
1558
|
+
const data = r.readJson(fileName);
|
|
1544
1559
|
return {
|
|
1545
|
-
name: data
|
|
1560
|
+
name: data?.name || fileName,
|
|
1546
1561
|
fileName,
|
|
1547
|
-
updatedAt: data
|
|
1562
|
+
updatedAt: data?.updatedAt || "",
|
|
1548
1563
|
isActive: fileName === activeFile,
|
|
1549
1564
|
isProduction: fileName === productionFile
|
|
1550
1565
|
};
|
|
@@ -1553,14 +1568,12 @@ ${lines.join("\n")}
|
|
|
1553
1568
|
}
|
|
1554
1569
|
async function handleListManifests({ res }) {
|
|
1555
1570
|
const activeFile = manifestsResource.getActiveName();
|
|
1556
|
-
const files =
|
|
1557
|
-
const
|
|
1558
|
-
const data = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
|
|
1559
|
-
const fileName = f.replace(".json", "");
|
|
1571
|
+
const files = manifestsResource.listNames().map((fileName) => {
|
|
1572
|
+
const data = manifestsResource.readJson(fileName);
|
|
1560
1573
|
return {
|
|
1561
|
-
name: data
|
|
1574
|
+
name: data?.name || fileName,
|
|
1562
1575
|
fileName,
|
|
1563
|
-
updatedAt: data
|
|
1576
|
+
updatedAt: data?.updatedAt || "",
|
|
1564
1577
|
isActive: fileName === activeFile,
|
|
1565
1578
|
isProtected: fileName === "default"
|
|
1566
1579
|
};
|
|
@@ -1569,19 +1582,19 @@ ${lines.join("\n")}
|
|
|
1569
1582
|
}
|
|
1570
1583
|
async function handleGetActiveManifest({ res }) {
|
|
1571
1584
|
const activeFile = manifestsResource.getActiveName();
|
|
1572
|
-
const
|
|
1573
|
-
if (!
|
|
1585
|
+
const raw = manifestsResource.readJson(activeFile);
|
|
1586
|
+
if (!raw) {
|
|
1574
1587
|
jsonResponse(res, 404, { error: "Active manifest not found" });
|
|
1575
1588
|
return;
|
|
1576
1589
|
}
|
|
1577
|
-
const data =
|
|
1590
|
+
const data = raw;
|
|
1578
1591
|
data._fileName = activeFile;
|
|
1579
1592
|
jsonResponse(res, 200, data);
|
|
1580
1593
|
}
|
|
1581
1594
|
async function handleSetActiveManifest({ req, res }) {
|
|
1582
1595
|
const body = JSON.parse(await readBody(req));
|
|
1583
1596
|
const fileName = sanitizeFileName(body.name || "default");
|
|
1584
|
-
if (
|
|
1597
|
+
if (manifestsResource.existingPath(fileName) === null) {
|
|
1585
1598
|
jsonResponse(res, 404, { error: "Manifest not found" });
|
|
1586
1599
|
return;
|
|
1587
1600
|
}
|
|
@@ -1592,11 +1605,12 @@ ${lines.join("\n")}
|
|
|
1592
1605
|
const [fileName] = params;
|
|
1593
1606
|
const filePath = manifestsResource.filePath(fileName);
|
|
1594
1607
|
if (req.method === "GET") {
|
|
1595
|
-
|
|
1608
|
+
const raw = manifestsResource.readJson(fileName);
|
|
1609
|
+
if (!raw) {
|
|
1596
1610
|
jsonResponse(res, 404, { error: "Not found" });
|
|
1597
1611
|
return;
|
|
1598
1612
|
}
|
|
1599
|
-
const data =
|
|
1613
|
+
const data = raw;
|
|
1600
1614
|
data._fileName = fileName;
|
|
1601
1615
|
jsonResponse(res, 200, data);
|
|
1602
1616
|
return;
|
|
@@ -1637,15 +1651,13 @@ ${lines.join("\n")}
|
|
|
1637
1651
|
}
|
|
1638
1652
|
async function handleApplyManifest({ params, res }) {
|
|
1639
1653
|
const [fileName] = params;
|
|
1640
|
-
const
|
|
1641
|
-
if (!
|
|
1654
|
+
const manifest = manifestsResource.readJson(fileName);
|
|
1655
|
+
if (!manifest) {
|
|
1642
1656
|
jsonResponse(res, 404, { error: "Manifest not found" });
|
|
1643
1657
|
return;
|
|
1644
1658
|
}
|
|
1645
|
-
const manifest = JSON.parse(import_fs3.default.readFileSync(manifestPath, "utf-8"));
|
|
1646
1659
|
const themeName = sanitizeFileName(manifest.theme || "default");
|
|
1647
|
-
|
|
1648
|
-
if (!import_fs3.default.existsSync(themePath)) {
|
|
1660
|
+
if (themesResource.existingPath(themeName) === null) {
|
|
1649
1661
|
jsonResponse(res, 422, { error: `Manifest references missing theme: ${themeName}` });
|
|
1650
1662
|
return;
|
|
1651
1663
|
}
|
|
@@ -1657,8 +1669,7 @@ ${lines.join("\n")}
|
|
|
1657
1669
|
if (!knownComponents.has(comp)) continue;
|
|
1658
1670
|
const sanitized = sanitizeFileName(String(configFile) || "default");
|
|
1659
1671
|
const r = componentResource(comp);
|
|
1660
|
-
|
|
1661
|
-
if (!import_fs3.default.existsSync(cfgPath)) {
|
|
1672
|
+
if (r.existingPath(sanitized) === null) {
|
|
1662
1673
|
jsonResponse(res, 422, {
|
|
1663
1674
|
error: `Manifest references missing config: ${comp}/${sanitized}`
|
|
1664
1675
|
});
|
|
@@ -1670,7 +1681,7 @@ ${lines.join("\n")}
|
|
|
1670
1681
|
themesResource.setProductionName(themeName);
|
|
1671
1682
|
syncTokensToCss(themeName);
|
|
1672
1683
|
syncFontsToCss(themeName);
|
|
1673
|
-
const themeData = normalizeTheme(
|
|
1684
|
+
const themeData = normalizeTheme(themesResource.readJson(themeName));
|
|
1674
1685
|
themeData._fileName = themeName;
|
|
1675
1686
|
for (const [comp, configFile] of apply) {
|
|
1676
1687
|
const r = componentResource(comp);
|
|
@@ -1696,33 +1707,30 @@ ${lines.join("\n")}
|
|
|
1696
1707
|
}
|
|
1697
1708
|
async function handleExportManifest({ params, res }) {
|
|
1698
1709
|
const [fileName] = params;
|
|
1699
|
-
const
|
|
1700
|
-
if (!
|
|
1710
|
+
const manifest = manifestsResource.readJson(fileName);
|
|
1711
|
+
if (!manifest) {
|
|
1701
1712
|
jsonResponse(res, 404, { error: "Manifest not found" });
|
|
1702
1713
|
return;
|
|
1703
1714
|
}
|
|
1704
|
-
const manifest = JSON.parse(import_fs3.default.readFileSync(manifestPath, "utf-8"));
|
|
1705
1715
|
const themeName = sanitizeFileName(manifest.theme || "default");
|
|
1706
|
-
const
|
|
1707
|
-
if (!
|
|
1716
|
+
const theme = themesResource.readJson(themeName);
|
|
1717
|
+
if (!theme) {
|
|
1708
1718
|
jsonResponse(res, 422, { error: `Manifest references missing theme: ${themeName}` });
|
|
1709
1719
|
return;
|
|
1710
1720
|
}
|
|
1711
|
-
const theme = JSON.parse(import_fs3.default.readFileSync(themePath, "utf-8"));
|
|
1712
1721
|
const knownComponents = new Set(listComponentNames());
|
|
1713
1722
|
const componentConfigs = {};
|
|
1714
1723
|
for (const [comp, configFile] of Object.entries(manifest.componentConfigs ?? {})) {
|
|
1715
1724
|
if (!knownComponents.has(comp)) continue;
|
|
1716
1725
|
const sanitized = sanitizeFileName(String(configFile) || "default");
|
|
1717
1726
|
if (sanitized === "default") continue;
|
|
1718
|
-
const
|
|
1719
|
-
if (!
|
|
1727
|
+
const cfg = componentResource(comp).readJson(sanitized);
|
|
1728
|
+
if (!cfg) {
|
|
1720
1729
|
jsonResponse(res, 422, {
|
|
1721
1730
|
error: `Manifest references missing config: ${comp}/${sanitized}`
|
|
1722
1731
|
});
|
|
1723
1732
|
return;
|
|
1724
1733
|
}
|
|
1725
|
-
const cfg = JSON.parse(import_fs3.default.readFileSync(cfgPath, "utf-8"));
|
|
1726
1734
|
componentConfigs[`${comp}/${sanitized}`] = cfg;
|
|
1727
1735
|
}
|
|
1728
1736
|
const bundle = {
|
|
@@ -1742,11 +1750,8 @@ ${lines.join("\n")}
|
|
|
1742
1750
|
);
|
|
1743
1751
|
res.end(JSON.stringify(bundle, null, 2));
|
|
1744
1752
|
}
|
|
1745
|
-
function nextAvailableName2(
|
|
1746
|
-
return nextAvailableName(
|
|
1747
|
-
(n) => import_fs3.default.existsSync(resourceFilePath(n)),
|
|
1748
|
-
sanitizeFileName(baseName)
|
|
1749
|
-
);
|
|
1753
|
+
function nextAvailableName2(exists, baseName) {
|
|
1754
|
+
return nextAvailableName(exists, sanitizeFileName(baseName));
|
|
1750
1755
|
}
|
|
1751
1756
|
async function handleImportManifest({ req, res }) {
|
|
1752
1757
|
let bundle;
|
|
@@ -1776,7 +1781,7 @@ ${lines.join("\n")}
|
|
|
1776
1781
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1777
1782
|
const originalThemeName = sanitizeFileName(bundle.manifest.theme || "default");
|
|
1778
1783
|
const finalThemeName = nextAvailableName2(
|
|
1779
|
-
(n) => themesResource.
|
|
1784
|
+
(n) => themesResource.existingPath(n) !== null,
|
|
1780
1785
|
originalThemeName
|
|
1781
1786
|
);
|
|
1782
1787
|
if (finalThemeName !== originalThemeName) {
|
|
@@ -1795,7 +1800,7 @@ ${lines.join("\n")}
|
|
|
1795
1800
|
if (!knownComponents.has(comp)) continue;
|
|
1796
1801
|
const r = componentResource(comp);
|
|
1797
1802
|
const finalName = nextAvailableName2(
|
|
1798
|
-
(n) => r.
|
|
1803
|
+
(n) => r.existingPath(n) !== null,
|
|
1799
1804
|
originalName
|
|
1800
1805
|
);
|
|
1801
1806
|
if (finalName !== originalName) {
|
|
@@ -1828,7 +1833,7 @@ ${lines.join("\n")}
|
|
|
1828
1833
|
if (!rewrittenManifest.createdAt) rewrittenManifest.createdAt = now;
|
|
1829
1834
|
const originalManifestName = sanitizeFileName(bundle.manifest.name || "imported");
|
|
1830
1835
|
const finalManifestName = nextAvailableName2(
|
|
1831
|
-
(n) => manifestsResource.
|
|
1836
|
+
(n) => manifestsResource.existingPath(n) !== null,
|
|
1832
1837
|
originalManifestName
|
|
1833
1838
|
);
|
|
1834
1839
|
if (finalManifestName !== originalManifestName) {
|