@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.
@@ -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 pageSources, 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.
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
- - Mount routes dynamically in `App.svelte` with `$derived.by(() => import(...))`. Static top-level imports evaluate every page module at boot and leak page CSS into editor routes.
22
- - Register each route in `<LiveEditorOverlay pageSources={...} />` so the "Page Source" button opens the file in VS Code.
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 `pageSources` entry). `ColumnsOverlay` (Cmd+G) shows content sitting inside `--columns-max-width`.
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
- - **Preset bundles** — capture a whole site configuration (active theme + every component's active config) as a single portable artifact. Drop a preset into a new project to restore the full styling in one step.
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 if you need a custom shell or non-standard route dispatch.
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
 
@@ -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({ dir: import_path3.default.join(COMPONENT_CONFIGS_DIR, comp) });
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({ dir: MANIFESTS_DIR });
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 themePath = import_path3.default.join(THEMES_DIR, `${productionThemeName}.json`);
964
+ const themeData = themesResource.readJson(productionThemeName);
923
965
  let themeVarCount = 0;
924
- if (import_fs3.default.existsSync(themePath)) {
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 themePath = import_path3.default.join(THEMES_DIR, `${fileName}.json`);
991
- if (!import_fs3.default.existsSync(themePath)) return;
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 (!import_fs3.default.existsSync(manifestsResource.filePath(activeName))) {
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 JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
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 = import_fs3.default.readdirSync(THEMES_DIR).filter((f) => f.endsWith(".json") && !f.startsWith("_")).map((f) => {
1267
- const filePath = import_path3.default.join(THEMES_DIR, f);
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.name || fileName,
1284
+ name: data?.name || fileName,
1272
1285
  fileName,
1273
- updatedAt: data.updatedAt || "",
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 filePath = themesResource.filePath(activeFile);
1282
- if (!import_fs3.default.existsSync(filePath)) {
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(JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8")));
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 (!import_fs3.default.existsSync(themesResource.filePath(fileName))) {
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 filePath = themesResource.filePath(prodFile);
1303
- if (!import_fs3.default.existsSync(filePath)) {
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(JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8")));
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 (!import_fs3.default.existsSync(themesResource.filePath(fileName))) {
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 = JSON.parse(import_fs3.default.readFileSync(themesResource.filePath(fileName), "utf-8"));
1347
+ const data = themesResource.readJson(fileName);
1335
1348
  jsonResponse(res, 200, {
1336
1349
  ok: true,
1337
1350
  fileName,
1338
- name: data.name || fileName,
1339
- updatedAt: data.updatedAt || ""
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
- if (!import_fs3.default.existsSync(filePath)) {
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(JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8")));
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
- const configPath = r.filePath(fileName);
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
- const configPath = r.filePath(fileName);
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
- if (!import_fs3.default.existsSync(configPath)) {
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 = JSON.parse(import_fs3.default.readFileSync(configPath, "utf-8"));
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 = import_fs3.default.readdirSync(r.dir).filter((f) => f.endsWith(".json") && !f.startsWith("_")).map((f) => {
1541
- const filePath = import_path3.default.join(r.dir, f);
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.name || fileName,
1560
+ name: data?.name || fileName,
1546
1561
  fileName,
1547
- updatedAt: data.updatedAt || "",
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 = import_fs3.default.readdirSync(MANIFESTS_DIR).filter((f) => f.endsWith(".json") && !f.startsWith("_")).map((f) => {
1557
- const filePath = import_path3.default.join(MANIFESTS_DIR, f);
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.name || fileName,
1574
+ name: data?.name || fileName,
1562
1575
  fileName,
1563
- updatedAt: data.updatedAt || "",
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 filePath = manifestsResource.filePath(activeFile);
1573
- if (!import_fs3.default.existsSync(filePath)) {
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 = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
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 (!import_fs3.default.existsSync(manifestsResource.filePath(fileName))) {
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
- if (!import_fs3.default.existsSync(filePath)) {
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 = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
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 manifestPath = manifestsResource.filePath(fileName);
1641
- if (!import_fs3.default.existsSync(manifestPath)) {
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
- const themePath = themesResource.filePath(themeName);
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
- const cfgPath = r.filePath(sanitized);
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(JSON.parse(import_fs3.default.readFileSync(themePath, "utf-8")));
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 manifestPath = manifestsResource.filePath(fileName);
1700
- if (!import_fs3.default.existsSync(manifestPath)) {
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 themePath = themesResource.filePath(themeName);
1707
- if (!import_fs3.default.existsSync(themePath)) {
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 cfgPath = componentResource(comp).filePath(sanitized);
1719
- if (!import_fs3.default.existsSync(cfgPath)) {
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(resourceFilePath, baseName) {
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.filePath(n),
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.filePath(n),
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.filePath(n),
1836
+ (n) => manifestsResource.existingPath(n) !== null,
1832
1837
  originalManifestName
1833
1838
  );
1834
1839
  if (finalManifestName !== originalManifestName) {