foss-design 0.7.0 → 0.8.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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import path28 from "path";
4
+ import path30 from "path";
5
5
  import { parseArgs } from "util";
6
6
 
7
7
  // src/core/paths.ts
@@ -134,12 +134,12 @@ function resolveFrom(anchor, request) {
134
134
 
135
135
  // src/cli/commands/build.ts
136
136
  import { spawnSync } from "child_process";
137
- import fs12 from "fs";
138
- import path12 from "path";
137
+ import fs13 from "fs";
138
+ import path13 from "path";
139
139
 
140
140
  // src/build/static.ts
141
- import fs11 from "fs";
142
- import path11 from "path";
141
+ import fs12 from "fs";
142
+ import path12 from "path";
143
143
  import { build } from "vite";
144
144
 
145
145
  // src/core/png.ts
@@ -307,8 +307,8 @@ function ensureThumb(file) {
307
307
  }
308
308
 
309
309
  // src/core/project.ts
310
- import fs7 from "fs";
311
- import path7 from "path";
310
+ import fs8 from "fs";
311
+ import path8 from "path";
312
312
 
313
313
  // src/core/canvas.ts
314
314
  import { createHash } from "crypto";
@@ -346,8 +346,8 @@ function routeError(pattern) {
346
346
  }
347
347
  return null;
348
348
  }
349
- function split(path29) {
350
- return path29.split("/").filter(Boolean);
349
+ function split(path31) {
350
+ return path31.split("/").filter(Boolean);
351
351
  }
352
352
  function compileRoute(pattern) {
353
353
  return {
@@ -366,8 +366,8 @@ function decode2(part) {
366
366
  return part;
367
367
  }
368
368
  }
369
- function scoreRoute(route2, path29) {
370
- const parts = split(path29.split(/[?#]/)[0] ?? "").map(decode2);
369
+ function scoreRoute(route2, path31) {
370
+ const parts = split(path31.split(/[?#]/)[0] ?? "").map(decode2);
371
371
  const walk2 = (s, p) => {
372
372
  if (s === route2.segments.length) return p === parts.length ? 0 : null;
373
373
  const segment = route2.segments[s];
@@ -385,11 +385,11 @@ function scoreRoute(route2, path29) {
385
385
  };
386
386
  return walk2(0, 0);
387
387
  }
388
- function matchRoute(entries, path29) {
388
+ function matchRoute(entries, path31) {
389
389
  let best = null;
390
390
  for (const entry of entries) {
391
391
  for (const route2 of entry.routes) {
392
- const score = scoreRoute(route2, path29);
392
+ const score = scoreRoute(route2, path31);
393
393
  if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
394
394
  }
395
395
  }
@@ -426,6 +426,8 @@ var DesignConfigSchema = z.strictObject({
426
426
  alias: z.record(z.string(), z.string()).optional(),
427
427
  /** Extra folders Tailwind scans for class names, relative to the project root. */
428
428
  sources: z.array(z.string()).optional(),
429
+ /** A monorepo's app package, relative to the project root: screens import its dependencies and its React. */
430
+ app: z.string().min(1).optional(),
429
431
  /** A folder served at the root of every screen, like Vite's `public`, relative to the project root. */
430
432
  public: z.string().min(1).optional(),
431
433
  port: z.number().int().min(1).max(65535).optional()
@@ -512,9 +514,9 @@ var CanvasSchema = z.strictObject({
512
514
  pages: z.array(z.unknown()).min(1, "add at least one page")
513
515
  });
514
516
  var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
515
- function formatPath(base, path29) {
517
+ function formatPath(base, path31) {
516
518
  let out = base;
517
- for (const part of path29) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
519
+ for (const part of path31) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
518
520
  return out;
519
521
  }
520
522
  function describeZodError(error, base = "") {
@@ -831,33 +833,80 @@ function summarize(canvas) {
831
833
  };
832
834
  }
833
835
 
834
- // src/core/public.ts
836
+ // src/core/icon.ts
835
837
  import fs4 from "fs";
836
838
  import path4 from "path";
839
+ var ICON_EXTENSIONS = ["svg", "png", "webp"];
840
+ var MAX_ICON_BYTES = 256 * 1024;
841
+ var iconName = (ext) => `icon.${ext}`;
842
+ function iconFile(paths) {
843
+ for (const ext of ICON_EXTENSIONS) {
844
+ const file = path4.join(paths.system, iconName(ext));
845
+ if (fs4.existsSync(file) && fs4.statSync(file).isFile()) return file;
846
+ }
847
+ return null;
848
+ }
849
+ function sniffIcon(data) {
850
+ if (data.length >= 8 && data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
851
+ return "png";
852
+ if (data.length >= 12 && data.toString("latin1", 0, 4) === "RIFF" && data.toString("latin1", 8, 12) === "WEBP")
853
+ return "webp";
854
+ const head = data.subarray(0, 4096).toString("utf8").replace(/^/, "");
855
+ const rest = head.replace(/^(?:\s|<\?xml[\s\S]*?\?>|<!--[\s\S]*?-->|<!DOCTYPE[^>]*>)*/i, "");
856
+ return /^<svg[\s>]/i.test(rest) ? "svg" : null;
857
+ }
858
+ function iconProblem(data, ext) {
859
+ const kind = sniffIcon(data);
860
+ if (!kind) return "the icon must be an SVG, PNG or WebP image";
861
+ if (ext && ext !== kind) return `the file is ${kind.toUpperCase()}, not ${ext.toUpperCase()}`;
862
+ if (data.length > MAX_ICON_BYTES)
863
+ return `the icon is ${Math.ceil(data.length / 1024)} KB; the limit is ${MAX_ICON_BYTES / 1024} KB`;
864
+ return null;
865
+ }
866
+ function writeIcon(paths, data) {
867
+ for (const ext2 of ICON_EXTENSIONS) fs4.rmSync(path4.join(paths.system, iconName(ext2)), { force: true });
868
+ if (!data) return null;
869
+ const ext = sniffIcon(data);
870
+ fs4.mkdirSync(paths.system, { recursive: true });
871
+ const file = path4.join(paths.system, iconName(ext));
872
+ fs4.writeFileSync(file, data);
873
+ return file;
874
+ }
875
+ function onlyIcon(dir) {
876
+ try {
877
+ return fs4.readdirSync(dir).filter((name) => !name.startsWith(".")).every((name) => ICON_EXTENSIONS.some((ext) => name === iconName(ext)));
878
+ } catch {
879
+ return false;
880
+ }
881
+ }
882
+
883
+ // src/core/public.ts
884
+ import fs5 from "fs";
885
+ import path5 from "path";
837
886
  function publicFiles(dir) {
838
887
  const files = [];
839
888
  let root;
840
889
  try {
841
- root = fs4.realpathSync(dir);
890
+ root = fs5.realpathSync(dir);
842
891
  } catch {
843
892
  return files;
844
893
  }
845
894
  const visit = (current) => {
846
895
  let entries;
847
896
  try {
848
- entries = fs4.readdirSync(current, { withFileTypes: true });
897
+ entries = fs5.readdirSync(current, { withFileTypes: true });
849
898
  } catch {
850
899
  return;
851
900
  }
852
901
  for (const entry of entries) {
853
902
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
854
- const full = path4.join(current, entry.name);
903
+ const full = path5.join(current, entry.name);
855
904
  if (entry.isDirectory()) visit(full);
856
- else if (entry.isFile()) files.push(toPosix(path4.relative(dir, full)));
905
+ else if (entry.isFile()) files.push(toPosix(path5.relative(dir, full)));
857
906
  else if (entry.isSymbolicLink()) {
858
907
  try {
859
- const target = fs4.realpathSync(full);
860
- if (isInside(root, target) && fs4.statSync(target).isFile()) files.push(toPosix(path4.relative(dir, full)));
908
+ const target = fs5.realpathSync(full);
909
+ if (isInside(root, target) && fs5.statSync(target).isFile()) files.push(toPosix(path5.relative(dir, full)));
861
910
  } catch {
862
911
  }
863
912
  }
@@ -868,12 +917,12 @@ function publicFiles(dir) {
868
917
  }
869
918
 
870
919
  // src/core/system.ts
871
- import fs6 from "fs";
872
- import path6 from "path";
920
+ import fs7 from "fs";
921
+ import path7 from "path";
873
922
 
874
923
  // src/core/tokens.ts
875
- import fs5 from "fs";
876
- import path5 from "path";
924
+ import fs6 from "fs";
925
+ import path6 from "path";
877
926
  import postcss from "postcss";
878
927
  var NAMESPACES = [
879
928
  ["--color-", "color"],
@@ -1032,12 +1081,12 @@ function readStylesheets(entry) {
1032
1081
  const seen = /* @__PURE__ */ new Set();
1033
1082
  const files = [];
1034
1083
  const walk2 = (file) => {
1035
- if (seen.has(file) || !fs5.existsSync(file)) return;
1084
+ if (seen.has(file) || !fs6.existsSync(file)) return;
1036
1085
  seen.add(file);
1037
- const css2 = fs5.readFileSync(file, "utf8");
1086
+ const css2 = fs6.readFileSync(file, "utf8");
1038
1087
  const imports = [...css2.matchAll(/@import\s+(?:url\()?\s*["']([^"']+)["']/g)].map((m) => m[1]);
1039
1088
  for (const spec of imports) {
1040
- if (spec.startsWith("./") || spec.startsWith("../")) walk2(path5.resolve(path5.dirname(file), spec));
1089
+ if (spec.startsWith("./") || spec.startsWith("../")) walk2(path6.resolve(path6.dirname(file), spec));
1041
1090
  }
1042
1091
  files.push({ file, css: css2 });
1043
1092
  };
@@ -1173,11 +1222,11 @@ var COMPONENT_EXT = [".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".html"];
1173
1222
  var IMAGE_EXT = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico"]);
1174
1223
  var FONT_EXT = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf"]);
1175
1224
  function listFiles(dir, deep = false) {
1176
- if (!fs6.existsSync(dir)) return [];
1225
+ if (!fs7.existsSync(dir)) return [];
1177
1226
  const out = [];
1178
- for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
1227
+ for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
1179
1228
  if (entry.name.startsWith(".")) continue;
1180
- const full = path6.join(dir, entry.name);
1229
+ const full = path7.join(dir, entry.name);
1181
1230
  if (entry.isDirectory()) {
1182
1231
  if (deep) out.push(...listFiles(full, true));
1183
1232
  } else out.push(full);
@@ -1185,16 +1234,16 @@ function listFiles(dir, deep = false) {
1185
1234
  return out.sort();
1186
1235
  }
1187
1236
  function guessSources(componentsDir, stem) {
1188
- if (!fs6.existsSync(componentsDir)) return [];
1237
+ if (!fs7.existsSync(componentsDir)) return [];
1189
1238
  const wanted = slugify(stem);
1190
1239
  for (const file of listFiles(componentsDir)) {
1191
- const ext = path6.extname(file);
1192
- if (COMPONENT_EXT.includes(ext) && slugify(path6.basename(file, ext)) === wanted) return [file];
1240
+ const ext = path7.extname(file);
1241
+ if (COMPONENT_EXT.includes(ext) && slugify(path7.basename(file, ext)) === wanted) return [file];
1193
1242
  }
1194
- for (const entry of fs6.readdirSync(componentsDir, { withFileTypes: true })) {
1243
+ for (const entry of fs7.readdirSync(componentsDir, { withFileTypes: true })) {
1195
1244
  if (entry.isDirectory() && slugify(entry.name) === wanted) {
1196
- const files = listFiles(path6.join(componentsDir, entry.name)).filter(
1197
- (f) => COMPONENT_EXT.includes(path6.extname(f))
1245
+ const files = listFiles(path7.join(componentsDir, entry.name)).filter(
1246
+ (f) => COMPONENT_EXT.includes(path7.extname(f))
1198
1247
  );
1199
1248
  if (files.length) return files;
1200
1249
  }
@@ -1202,14 +1251,14 @@ function guessSources(componentsDir, stem) {
1202
1251
  return [];
1203
1252
  }
1204
1253
  function loadSystem(paths, urls) {
1205
- if (!fs6.existsSync(paths.system)) return null;
1254
+ if (!fs7.existsSync(paths.system) || onlyIcon(paths.system)) return null;
1206
1255
  const issues = [];
1207
- const configFile = path6.join(paths.system, "system.json");
1256
+ const configFile = path7.join(paths.system, "system.json");
1208
1257
  const report = (severity, file, message, at) => issues.push({ severity, file: relToRoot(paths, file), message, at });
1209
1258
  let config = null;
1210
- if (fs6.existsSync(configFile)) {
1259
+ if (fs7.existsSync(configFile)) {
1211
1260
  try {
1212
- const parsed = SystemConfigSchema.safeParse(JSON.parse(fs6.readFileSync(configFile, "utf8")));
1261
+ const parsed = SystemConfigSchema.safeParse(JSON.parse(fs7.readFileSync(configFile, "utf8")));
1213
1262
  if (parsed.success) config = parsed.data;
1214
1263
  else for (const issue of describeZodError(parsed.error)) report("error", configFile, issue.message, issue.at);
1215
1264
  } catch (error) {
@@ -1218,36 +1267,36 @@ function loadSystem(paths, urls) {
1218
1267
  } else {
1219
1268
  report("warning", configFile, 'system.json is missing; run `design system init` or add { "name": "\u2026" }');
1220
1269
  }
1221
- const tokensFile = path6.join(paths.system, "tokens.css");
1270
+ const tokensFile = path7.join(paths.system, "tokens.css");
1222
1271
  let stylesheet = null;
1223
1272
  if (config?.stylesheet) {
1224
- stylesheet = path6.resolve(paths.system, config.stylesheet);
1225
- if (!fs6.existsSync(stylesheet)) {
1273
+ stylesheet = path7.resolve(paths.system, config.stylesheet);
1274
+ if (!fs7.existsSync(stylesheet)) {
1226
1275
  report("error", configFile, `stylesheet not found: ${config.stylesheet}`, "stylesheet");
1227
1276
  stylesheet = null;
1228
1277
  }
1229
1278
  }
1230
- const tokenEntry = stylesheet ?? (fs6.existsSync(tokensFile) ? tokensFile : null);
1279
+ const tokenEntry = stylesheet ?? (fs7.existsSync(tokensFile) ? tokensFile : null);
1231
1280
  const tokens = tokenEntry ? parseTokens(readStylesheets(tokenEntry)) : [];
1232
1281
  if (!tokenEntry) report("warning", tokensFile, "no tokens.css: screens get the default Tailwind theme");
1233
- const specimensDir = path6.join(paths.system, "specimens");
1234
- const componentsDir = path6.join(paths.system, "components");
1282
+ const specimensDir = path7.join(paths.system, "specimens");
1283
+ const componentsDir = path7.join(paths.system, "components");
1235
1284
  const components = [];
1236
1285
  const specimens = [];
1237
1286
  const usedIds = /* @__PURE__ */ new Set();
1238
1287
  for (const file of listFiles(specimensDir)) {
1239
- const ext = path6.extname(file).toLowerCase();
1288
+ const ext = path7.extname(file).toLowerCase();
1240
1289
  if (!SPECIMEN_EXT.has(ext)) continue;
1241
- const stem = path6.basename(file, ext);
1290
+ const stem = path7.basename(file, ext);
1242
1291
  let id2 = slugify(stem);
1243
1292
  if (usedIds.has(id2)) id2 = `${id2}-${usedIds.size + 1}`;
1244
1293
  usedIds.add(id2);
1245
- const tags = parseDocTags(fs6.readFileSync(file, "utf8"));
1246
- const sources = (tags.source ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean).map((value) => path6.resolve(path6.dirname(file), value));
1294
+ const tags = parseDocTags(fs7.readFileSync(file, "utf8"));
1295
+ const sources = (tags.source ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean).map((value) => path7.resolve(path7.dirname(file), value));
1247
1296
  for (const source of sources) {
1248
- if (!fs6.existsSync(source)) report("warning", file, `@source not found: ${path6.relative(paths.system, source)}`);
1297
+ if (!fs7.existsSync(source)) report("warning", file, `@source not found: ${path7.relative(paths.system, source)}`);
1249
1298
  }
1250
- const found = sources.filter((source) => fs6.existsSync(source));
1299
+ const found = sources.filter((source) => fs7.existsSync(source));
1251
1300
  const title = tags.title?.[0] || titleize(stem);
1252
1301
  const format = ext === ".html" ? "html" : "module";
1253
1302
  components.push({
@@ -1273,10 +1322,10 @@ function loadSystem(paths, urls) {
1273
1322
  });
1274
1323
  }
1275
1324
  const guidelines = [];
1276
- for (const file of listFiles(path6.join(paths.system, "guidelines"))) {
1277
- if (path6.extname(file).toLowerCase() !== ".md") continue;
1278
- const stem = path6.basename(file, ".md");
1279
- const { data, body } = parseFrontmatter(fs6.readFileSync(file, "utf8"));
1325
+ for (const file of listFiles(path7.join(paths.system, "guidelines"))) {
1326
+ if (path7.extname(file).toLowerCase() !== ".md") continue;
1327
+ const stem = path7.basename(file, ".md");
1328
+ const { data, body } = parseFrontmatter(fs7.readFileSync(file, "utf8"));
1280
1329
  const prefix = /^(\d+)[-_. ]+(.*)$/.exec(stem);
1281
1330
  const order = data.order !== void 0 ? Number(data.order) : prefix ? Number(prefix[1]) : 1e3;
1282
1331
  guidelines.push({
@@ -1288,14 +1337,14 @@ function loadSystem(paths, urls) {
1288
1337
  }
1289
1338
  guidelines.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
1290
1339
  const assets = [];
1291
- const assetsDir = path6.join(paths.system, "assets");
1340
+ const assetsDir = path7.join(paths.system, "assets");
1292
1341
  for (const file of listFiles(assetsDir, true)) {
1293
- const ext = path6.extname(file).toLowerCase();
1342
+ const ext = path7.extname(file).toLowerCase();
1294
1343
  assets.push({
1295
- name: path6.basename(file),
1296
- path: path6.relative(paths.system, file).split(path6.sep).join("/"),
1344
+ name: path7.basename(file),
1345
+ path: path7.relative(paths.system, file).split(path7.sep).join("/"),
1297
1346
  url: urls.file(relToDesign(paths, file)),
1298
- size: fs6.statSync(file).size,
1347
+ size: fs7.statSync(file).size,
1299
1348
  kind: IMAGE_EXT.has(ext) ? "image" : FONT_EXT.has(ext) ? "font" : "other"
1300
1349
  });
1301
1350
  }
@@ -1331,7 +1380,7 @@ function loadSystem(paths, urls) {
1331
1380
  },
1332
1381
  config,
1333
1382
  stylesheet,
1334
- tokensFile: fs6.existsSync(tokensFile) ? tokensFile : null,
1383
+ tokensFile: fs7.existsSync(tokensFile) ? tokensFile : null,
1335
1384
  specimens
1336
1385
  };
1337
1386
  }
@@ -1348,10 +1397,10 @@ function summarizeSystem(system) {
1348
1397
 
1349
1398
  // src/core/project.ts
1350
1399
  function publicProblem(root, value) {
1351
- const dir = path7.resolve(root, value);
1352
- if (!fs7.existsSync(dir) || !fs7.statSync(dir).isDirectory()) return `no folder "${value}"`;
1353
- const real = fs7.realpathSync(dir);
1354
- const realRoot = fs7.realpathSync(root);
1400
+ const dir = path8.resolve(root, value);
1401
+ if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) return `no folder "${value}"`;
1402
+ const real = fs8.realpathSync(dir);
1403
+ const realRoot = fs8.realpathSync(root);
1355
1404
  if (real === realRoot || !isInside(realRoot, real)) return `"${value}" is not a folder inside the project`;
1356
1405
  return null;
1357
1406
  }
@@ -1373,9 +1422,9 @@ var DesignProject = class {
1373
1422
  const issues = [];
1374
1423
  let config = {};
1375
1424
  const file = this.paths.config;
1376
- if (fs7.existsSync(file)) {
1425
+ if (fs8.existsSync(file)) {
1377
1426
  try {
1378
- const parsed = DesignConfigSchema.safeParse(JSON.parse(fs7.readFileSync(file, "utf8")));
1427
+ const parsed = DesignConfigSchema.safeParse(JSON.parse(fs8.readFileSync(file, "utf8")));
1379
1428
  if (parsed.success) config = parsed.data;
1380
1429
  else
1381
1430
  for (const issue of describeZodError(parsed.error))
@@ -1388,6 +1437,14 @@ var DesignProject = class {
1388
1437
  });
1389
1438
  }
1390
1439
  }
1440
+ if (config.app !== void 0) {
1441
+ const dir = path8.resolve(this.paths.root, config.app);
1442
+ const problem = !fs8.existsSync(path8.join(dir, "node_modules")) ? `no node_modules in "${config.app}"; install the app's dependencies there` : isInside(this.paths.root, dir) ? null : `"${config.app}" is not a folder inside the project`;
1443
+ if (problem) {
1444
+ issues.push({ severity: "error", file: relToRoot(this.paths, file), at: "app", message: problem });
1445
+ config = { ...config, app: void 0 };
1446
+ }
1447
+ }
1391
1448
  if (config.public !== void 0) {
1392
1449
  const problem = publicProblem(this.paths.root, config.public);
1393
1450
  if (problem) {
@@ -1398,10 +1455,15 @@ var DesignProject = class {
1398
1455
  this.configCache = { config, issues };
1399
1456
  return this.configCache;
1400
1457
  }
1458
+ /** The app package from design.json `app`, when it is usable. */
1459
+ appDir() {
1460
+ const configured = this.config().config.app;
1461
+ return configured === void 0 ? null : path8.resolve(this.paths.root, configured);
1462
+ }
1401
1463
  /** The public folder from design.json, when it is a folder inside the project. */
1402
1464
  publicDir() {
1403
1465
  const configured = this.config().config.public;
1404
- return configured === void 0 ? null : path7.resolve(this.paths.root, configured);
1466
+ return configured === void 0 ? null : path8.resolve(this.paths.root, configured);
1405
1467
  }
1406
1468
  /** The public folder's files, relative to it; `invalidatePublic` when they change. */
1407
1469
  publicFiles() {
@@ -1421,11 +1483,11 @@ var DesignProject = class {
1421
1483
  const configured = this.config().config.name;
1422
1484
  if (configured) return configured;
1423
1485
  try {
1424
- const manifest = JSON.parse(fs7.readFileSync(path7.join(this.paths.root, "package.json"), "utf8"));
1486
+ const manifest = JSON.parse(fs8.readFileSync(path8.join(this.paths.root, "package.json"), "utf8"));
1425
1487
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
1426
1488
  } catch {
1427
1489
  }
1428
- return path7.basename(this.paths.root);
1490
+ return path8.basename(this.paths.root);
1429
1491
  }
1430
1492
  system() {
1431
1493
  if (this.systemCache === void 0) this.systemCache = loadSystem(this.paths, this.urls);
@@ -1437,7 +1499,7 @@ var DesignProject = class {
1437
1499
  canvas(id2) {
1438
1500
  const cached = this.canvasCache.get(id2);
1439
1501
  if (cached) return cached;
1440
- if (!fs7.existsSync(path7.join(this.paths.canvases, id2))) return null;
1502
+ if (!fs8.existsSync(path8.join(this.paths.canvases, id2))) return null;
1441
1503
  const canvas = loadCanvas(this.paths, id2, this.urls, this.snapshots);
1442
1504
  this.canvasCache.set(id2, canvas);
1443
1505
  return canvas;
@@ -1462,13 +1524,26 @@ var DesignProject = class {
1462
1524
  version: packageVersion(),
1463
1525
  system: system ? summarizeSystem(system) : null,
1464
1526
  canvases: this.canvases().map(summarize),
1465
- issues: this.config().issues
1527
+ issues: this.config().issues,
1528
+ icon: this.iconUrl()
1466
1529
  };
1467
1530
  }
1531
+ /** The icon's URL, with its change time so a new icon is fetched anew. */
1532
+ iconUrl() {
1533
+ const file = iconFile(this.paths);
1534
+ if (!file) return null;
1535
+ return `${this.urls.file(relToDesign(this.paths, file))}?v=${Math.round(fs8.statSync(file).mtimeMs)}`;
1536
+ }
1537
+ iconIssues() {
1538
+ const file = iconFile(this.paths);
1539
+ const problem = file ? iconProblem(fs8.readFileSync(file), path8.extname(file).slice(1)) : null;
1540
+ return problem ? [{ severity: "error", file: relToRoot(this.paths, file), message: problem }] : [];
1541
+ }
1468
1542
  /** Every problem in the project, for `design check`. */
1469
1543
  issues() {
1470
1544
  return [
1471
1545
  ...this.config().issues,
1546
+ ...this.iconIssues(),
1472
1547
  ...this.system()?.doc.issues ?? [],
1473
1548
  ...this.canvases().flatMap((canvas) => canvas.doc.issues)
1474
1549
  ];
@@ -1508,21 +1583,21 @@ var STATIC_URLS = {
1508
1583
  };
1509
1584
 
1510
1585
  // src/server/entries.ts
1511
- import fs8 from "fs";
1512
- import path8 from "path";
1586
+ import fs9 from "fs";
1587
+ import path9 from "path";
1513
1588
  var SYSTEM_CSS = "system.css";
1514
1589
  var HTML_BOOT = "html-boot.js";
1515
1590
  function writeIfChanged(file, content) {
1516
1591
  try {
1517
- if (fs8.readFileSync(file, "utf8") === content) return false;
1592
+ if (fs9.readFileSync(file, "utf8") === content) return false;
1518
1593
  } catch {
1519
1594
  }
1520
- fs8.mkdirSync(path8.dirname(file), { recursive: true });
1521
- fs8.writeFileSync(file, content);
1595
+ fs9.mkdirSync(path9.dirname(file), { recursive: true });
1596
+ fs9.writeFileSync(file, content);
1522
1597
  return true;
1523
1598
  }
1524
1599
  function importPath(fromDir, file) {
1525
- const rel = toPosix(path8.relative(fromDir, file));
1600
+ const rel = toPosix(path9.relative(fromDir, file));
1526
1601
  return rel.startsWith(".") ? rel : `./${rel}`;
1527
1602
  }
1528
1603
  var css = (file) => JSON.stringify(toPosix(file));
@@ -1535,10 +1610,10 @@ var Entries = class {
1535
1610
  return this.project.paths.entries;
1536
1611
  }
1537
1612
  systemCssFile() {
1538
- return path8.join(this.dir, SYSTEM_CSS);
1613
+ return path9.join(this.dir, SYSTEM_CSS);
1539
1614
  }
1540
1615
  entryFile(source) {
1541
- return path8.join(this.dir, source.canvas, `${source.id}.js`);
1616
+ return path9.join(this.dir, source.canvas, `${source.id}.js`);
1542
1617
  }
1543
1618
  /** Tailwind with the design tokens, or the stylesheet system.json names; scans every file in `.design`. */
1544
1619
  systemCss() {
@@ -1555,14 +1630,14 @@ var Entries = class {
1555
1630
  if (system?.tokensFile) lines.push(`@import ${css(system.tokensFile)};`);
1556
1631
  }
1557
1632
  lines.push(`@source ${css(paths.design)};`, `@source not ${css(paths.cache)};`);
1558
- lines.push(`@source not ${css(path8.join(paths.design, "node_modules"))};`);
1559
- for (const extra of config.sources ?? []) lines.push(`@source ${css(path8.resolve(paths.root, extra))};`);
1633
+ lines.push(`@source not ${css(path9.join(paths.design, "node_modules"))};`);
1634
+ for (const extra of config.sources ?? []) lines.push(`@source ${css(path9.resolve(paths.root, extra))};`);
1560
1635
  return `${lines.join("\n")}
1561
1636
  `;
1562
1637
  }
1563
1638
  entrySource(source) {
1564
1639
  const file = this.entryFile(source);
1565
- const dir = path8.dirname(file);
1640
+ const dir = path9.dirname(file);
1566
1641
  const lines = [];
1567
1642
  if (source.system) lines.push(`import ${JSON.stringify(importPath(dir, this.systemCssFile()))}`);
1568
1643
  if (source.builtin === "typography") {
@@ -1577,7 +1652,7 @@ var Entries = class {
1577
1652
  `import * as screen from ${JSON.stringify(importPath(dir, source.file))}`,
1578
1653
  "",
1579
1654
  `const Screen = screen.default ?? Object.values(screen).find((value) => typeof value === 'function')`,
1580
- "if (!Screen) throw new Error(" + JSON.stringify(`${path8.basename(source.file)} has no component to render: export one as default`) + ")",
1655
+ "if (!Screen) throw new Error(" + JSON.stringify(`${path9.basename(source.file)} has no component to render: export one as default`) + ")",
1581
1656
  "hmr(import.meta.hot)",
1582
1657
  "mount(Screen)"
1583
1658
  );
@@ -1590,14 +1665,14 @@ ${lines.join("\n")}
1590
1665
  sync() {
1591
1666
  writeIfChanged(this.systemCssFile(), this.systemCss());
1592
1667
  writeIfChanged(
1593
- path8.join(this.dir, HTML_BOOT),
1668
+ path9.join(this.dir, HTML_BOOT),
1594
1669
  `// Generated by foss-design.
1595
1670
  import { boot, hmr } from '@design/runtime'
1596
1671
  boot()
1597
1672
  hmr(import.meta.hot)
1598
1673
  `
1599
1674
  );
1600
- const wanted = /* @__PURE__ */ new Set([this.systemCssFile(), path8.join(this.dir, HTML_BOOT)]);
1675
+ const wanted = /* @__PURE__ */ new Set([this.systemCssFile(), path9.join(this.dir, HTML_BOOT)]);
1601
1676
  for (const source of this.project.sources()) {
1602
1677
  if (source.format !== "module") continue;
1603
1678
  const file = this.entryFile(source);
@@ -1612,21 +1687,21 @@ hmr(import.meta.hot)
1612
1687
  * Vite bundle again and fail the requests in flight (504 Outdated Optimize Dep).
1613
1688
  */
1614
1689
  files() {
1615
- const files = [path8.join(this.dir, HTML_BOOT)];
1690
+ const files = [path9.join(this.dir, HTML_BOOT)];
1616
1691
  for (const source of this.project.sources()) {
1617
1692
  if (source.format === "module") files.push(this.entryFile(source));
1618
- else if (!source.builtin && fs8.existsSync(source.file)) files.push(source.file);
1693
+ else if (!source.builtin && fs9.existsSync(source.file)) files.push(source.file);
1619
1694
  }
1620
1695
  return files;
1621
1696
  }
1622
1697
  prune(dir, wanted) {
1623
- if (!fs8.existsSync(dir)) return;
1624
- for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
1625
- const full = path8.join(dir, entry.name);
1698
+ if (!fs9.existsSync(dir)) return;
1699
+ for (const entry of fs9.readdirSync(dir, { withFileTypes: true })) {
1700
+ const full = path9.join(dir, entry.name);
1626
1701
  if (entry.isDirectory()) {
1627
1702
  this.prune(full, wanted);
1628
- if (fs8.readdirSync(full).length === 0) fs8.rmdirSync(full);
1629
- } else if (!wanted.has(full)) fs8.rmSync(full);
1703
+ if (fs9.readdirSync(full).length === 0) fs9.rmdirSync(full);
1704
+ } else if (!wanted.has(full)) fs9.rmSync(full);
1630
1705
  }
1631
1706
  }
1632
1707
  };
@@ -1694,68 +1769,132 @@ ${html}
1694
1769
  }
1695
1770
 
1696
1771
  // src/server/links.ts
1697
- import fs9 from "fs";
1698
- import path9 from "path";
1772
+ import fs10 from "fs";
1773
+ import path10 from "path";
1699
1774
  var SHIPPED_PACKAGES = ["react", "react-dom", "motion", "lucide-react", "clsx", "tailwind-merge"];
1700
1775
  function findPackageDir(fromDir, name, skip) {
1701
- let dir = path9.resolve(fromDir);
1776
+ let dir = path10.resolve(fromDir);
1702
1777
  for (; ; ) {
1703
- const modules = path9.join(dir, "node_modules");
1704
- const candidate = path9.join(modules, name);
1705
- if (modules !== skip && fs9.existsSync(path9.join(candidate, "package.json"))) return candidate;
1706
- const parent = path9.dirname(dir);
1778
+ const modules = path10.join(dir, "node_modules");
1779
+ const candidate = path10.join(modules, name);
1780
+ if (modules !== skip && fs10.existsSync(path10.join(candidate, "package.json"))) return candidate;
1781
+ const parent = path10.dirname(dir);
1707
1782
  if (parent === dir) return null;
1708
1783
  dir = parent;
1709
1784
  }
1710
1785
  }
1711
1786
  function isLink(file) {
1712
1787
  try {
1713
- return fs9.lstatSync(file).isSymbolicLink();
1788
+ return fs10.lstatSync(file).isSymbolicLink();
1714
1789
  } catch {
1715
1790
  return false;
1716
1791
  }
1717
1792
  }
1718
- function linkShippedPackages(paths) {
1719
- const modules = path9.join(paths.design, "node_modules");
1793
+ function ensureLink(link, target) {
1794
+ let current = null;
1795
+ try {
1796
+ current = fs10.realpathSync(link);
1797
+ } catch {
1798
+ }
1799
+ if (current === target) return;
1800
+ if (isLink(link)) fs10.unlinkSync(link);
1801
+ fs10.mkdirSync(path10.dirname(link), { recursive: true });
1802
+ fs10.symlinkSync(target, link, process.platform === "win32" ? "junction" : "dir");
1803
+ }
1804
+ function appPackages(appDir) {
1805
+ const found = /* @__PURE__ */ new Map();
1806
+ const modules = path10.join(appDir, "node_modules");
1807
+ const add = (name, dir) => {
1808
+ try {
1809
+ if (fs10.existsSync(path10.join(dir, "package.json"))) found.set(name, fs10.realpathSync(dir));
1810
+ } catch {
1811
+ }
1812
+ };
1813
+ let entries = [];
1814
+ try {
1815
+ entries = fs10.readdirSync(modules, { withFileTypes: true });
1816
+ } catch {
1817
+ return found;
1818
+ }
1819
+ for (const entry of entries) {
1820
+ if (entry.name.startsWith(".")) continue;
1821
+ const dir = path10.join(modules, entry.name);
1822
+ if (!entry.name.startsWith("@")) {
1823
+ add(entry.name, dir);
1824
+ continue;
1825
+ }
1826
+ try {
1827
+ for (const scoped of fs10.readdirSync(dir))
1828
+ if (!scoped.startsWith(".")) add(`${entry.name}/${scoped}`, path10.join(dir, scoped));
1829
+ } catch {
1830
+ }
1831
+ }
1832
+ return found;
1833
+ }
1834
+ var APP_LINKS = ".foss-design-app.json";
1835
+ function linkShippedPackages(paths, appDir = null) {
1836
+ const modules = path10.join(paths.design, "node_modules");
1720
1837
  const linked = [];
1721
1838
  const allow = /* @__PURE__ */ new Set();
1839
+ const app = appDir ? appPackages(appDir) : /* @__PURE__ */ new Map();
1840
+ const placedByUser = (link) => fs10.existsSync(link) && !isLink(link);
1722
1841
  for (const name of SHIPPED_PACKAGES) {
1723
- const link = path9.join(modules, name);
1724
- const placedByUser = fs9.existsSync(link) && !isLink(link);
1725
- const own = placedByUser ? link : findPackageDir(paths.design, name, modules);
1842
+ const link = path10.join(modules, name);
1843
+ if (app.has(name) && !placedByUser(link)) continue;
1844
+ const own = placedByUser(link) ? link : findPackageDir(paths.design, name, modules);
1726
1845
  if (own) {
1727
- allow.add(path9.dirname(fs9.realpathSync(own)));
1728
- if (isLink(link)) fs9.unlinkSync(link);
1846
+ allow.add(path10.dirname(fs10.realpathSync(own)));
1847
+ if (isLink(link)) fs10.unlinkSync(link);
1729
1848
  continue;
1730
1849
  }
1731
1850
  const shipped = findPackageDir(PKG_ROOT, name);
1732
1851
  if (!shipped) continue;
1733
- const target2 = fs9.realpathSync(shipped);
1734
- allow.add(path9.dirname(target2));
1735
- let current2 = null;
1736
- try {
1737
- current2 = fs9.realpathSync(link);
1738
- } catch {
1739
- }
1740
- if (current2 !== target2) {
1741
- if (isLink(link)) fs9.unlinkSync(link);
1742
- fs9.mkdirSync(path9.dirname(link), { recursive: true });
1743
- fs9.symlinkSync(target2, link, process.platform === "win32" ? "junction" : "dir");
1744
- }
1852
+ const target2 = fs10.realpathSync(shipped);
1853
+ allow.add(path10.dirname(target2));
1854
+ ensureLink(link, target2);
1745
1855
  linked.push(name);
1746
1856
  }
1747
- const self = path9.join(modules, "foss-design");
1748
- const target = fs9.realpathSync(PKG_ROOT);
1857
+ const fromApp = [];
1858
+ for (const [name, target2] of app) {
1859
+ const link = path10.join(modules, name);
1860
+ if (placedByUser(link)) continue;
1861
+ ensureLink(link, target2);
1862
+ allow.add(path10.dirname(target2));
1863
+ fromApp.push(name);
1864
+ }
1865
+ const record2 = path10.join(modules, APP_LINKS);
1866
+ let before = [];
1867
+ try {
1868
+ before = JSON.parse(fs10.readFileSync(record2, "utf8"));
1869
+ } catch {
1870
+ }
1871
+ for (const name of before) {
1872
+ const link = path10.join(modules, name);
1873
+ if (!app.has(name) && !SHIPPED_PACKAGES.includes(name) && isLink(link)) {
1874
+ fs10.unlinkSync(link);
1875
+ if (name.startsWith("@"))
1876
+ try {
1877
+ fs10.rmdirSync(path10.dirname(link));
1878
+ } catch {
1879
+ }
1880
+ }
1881
+ }
1882
+ if (fromApp.length || before.length) {
1883
+ fs10.mkdirSync(modules, { recursive: true });
1884
+ fs10.writeFileSync(record2, JSON.stringify(fromApp.sort()));
1885
+ }
1886
+ const self = path10.join(modules, "foss-design");
1887
+ const target = fs10.realpathSync(PKG_ROOT);
1749
1888
  let current = null;
1750
1889
  try {
1751
- current = fs9.realpathSync(self);
1890
+ current = fs10.realpathSync(self);
1752
1891
  } catch {
1753
1892
  }
1754
1893
  if (current !== target) {
1755
- if (isLink(self)) fs9.unlinkSync(self);
1756
- if (!fs9.existsSync(self)) {
1757
- fs9.mkdirSync(modules, { recursive: true });
1758
- fs9.symlinkSync(target, self, process.platform === "win32" ? "junction" : "dir");
1894
+ if (isLink(self)) fs10.unlinkSync(self);
1895
+ if (!fs10.existsSync(self)) {
1896
+ fs10.mkdirSync(modules, { recursive: true });
1897
+ fs10.symlinkSync(target, self, process.platform === "win32" ? "junction" : "dir");
1759
1898
  }
1760
1899
  }
1761
1900
  allow.add(target);
@@ -1764,8 +1903,8 @@ function linkShippedPackages(paths) {
1764
1903
 
1765
1904
  // src/server/vite.ts
1766
1905
  import { createHash as createHash2 } from "crypto";
1767
- import fs10 from "fs";
1768
- import path10 from "path";
1906
+ import fs11 from "fs";
1907
+ import path11 from "path";
1769
1908
  import tailwindcss from "@tailwindcss/vite";
1770
1909
  import react from "@vitejs/plugin-react";
1771
1910
  var escapeRegExp = (text2) => text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1777,30 +1916,31 @@ function aliases(project) {
1777
1916
  { find: /^@system\//, replacement: `${toPosix(paths.system)}/` }
1778
1917
  ];
1779
1918
  for (const [key, target] of Object.entries(project.config().config.alias ?? {})) {
1780
- const replacement = toPosix(path10.resolve(paths.root, target));
1919
+ const replacement = toPosix(path11.resolve(paths.root, target));
1781
1920
  list.push(
1782
1921
  key.endsWith("/") ? { find: new RegExp(`^${escapeRegExp(key)}`), replacement: `${replacement.replace(/\/$/, "")}/` } : { find: new RegExp(`^${escapeRegExp(key)}(?=/|$)`), replacement }
1783
1922
  );
1784
1923
  }
1785
1924
  return list;
1786
1925
  }
1787
- function dropStaleDepCache(paths) {
1788
- const dir = path10.join(paths.cache, "vite");
1789
- const stamp = path10.join(paths.cache, "vite-runtime");
1926
+ function dropStaleDepCache(paths, appDir = null) {
1927
+ const dir = path11.join(paths.cache, "vite");
1928
+ const stamp = path11.join(paths.cache, "vite-runtime");
1790
1929
  let runtime = "";
1791
1930
  try {
1792
- runtime = createHash2("sha256").update(fs10.readFileSync(PKG.runtime)).digest("hex");
1931
+ runtime = createHash2("sha256").update(fs11.readFileSync(PKG.runtime)).digest("hex");
1793
1932
  } catch {
1794
1933
  }
1934
+ if (appDir) runtime += ` ${appDir}`;
1795
1935
  let seen = "";
1796
1936
  try {
1797
- seen = fs10.readFileSync(stamp, "utf8");
1937
+ seen = fs11.readFileSync(stamp, "utf8");
1798
1938
  } catch {
1799
1939
  }
1800
1940
  if (seen === runtime) return;
1801
- fs10.rmSync(dir, { recursive: true, force: true });
1802
- fs10.mkdirSync(paths.cache, { recursive: true });
1803
- fs10.writeFileSync(stamp, runtime);
1941
+ fs11.rmSync(dir, { recursive: true, force: true });
1942
+ fs11.mkdirSync(paths.cache, { recursive: true });
1943
+ fs11.writeFileSync(stamp, runtime);
1804
1944
  }
1805
1945
  function baseConfig(project) {
1806
1946
  const { paths } = project;
@@ -1808,7 +1948,7 @@ function baseConfig(project) {
1808
1948
  configFile: false,
1809
1949
  envDir: false,
1810
1950
  root: paths.design,
1811
- cacheDir: path10.join(paths.cache, "vite"),
1951
+ cacheDir: path11.join(paths.cache, "vite"),
1812
1952
  // design.json `public`: served under the frames' base, so HTML and CSS paths such as `/logo.png` resolve.
1813
1953
  publicDir: project.publicDir() ?? false,
1814
1954
  logLevel: "warn",
@@ -1828,7 +1968,7 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".jsx", ".mts", ".cts",
1828
1968
  var BUNDLED_REF = /<(script|link|img|source|video|audio|image|use|input)\b[^>]*>/gi;
1829
1969
  var encodePath2 = (rel) => rel.split("/").map(encodeURIComponent).join("/");
1830
1970
  function relUrl(fromDir, file) {
1831
- const rel = toPosix(path11.relative(fromDir, file));
1971
+ const rel = toPosix(path12.relative(fromDir, file));
1832
1972
  return rel.startsWith(".") ? rel : `./${rel}`;
1833
1973
  }
1834
1974
  function staticSnapshots(paths) {
@@ -1838,7 +1978,7 @@ function staticSnapshots(paths) {
1838
1978
  let data = metaCache.get(canvas);
1839
1979
  if (!data) {
1840
1980
  try {
1841
- data = JSON.parse(fs11.readFileSync(path11.join(paths.snapshots, canvas, "meta.json"), "utf8"));
1981
+ data = JSON.parse(fs12.readFileSync(path12.join(paths.snapshots, canvas, "meta.json"), "utf8"));
1842
1982
  } catch {
1843
1983
  }
1844
1984
  data ??= {};
@@ -1852,8 +1992,8 @@ function staticSnapshots(paths) {
1852
1992
  const urls = {};
1853
1993
  const thumbs = {};
1854
1994
  for (const theme2 of THEMES) {
1855
- const file = path11.join(paths.snapshots, canvas, `${id2}.${theme2}.png`);
1856
- if (!entry[theme2] || !fs11.existsSync(file)) continue;
1995
+ const file = path12.join(paths.snapshots, canvas, `${id2}.${theme2}.png`);
1996
+ if (!entry[theme2] || !fs12.existsSync(file)) continue;
1857
1997
  const rel = `_snap/${canvas}/${id2}.${theme2}.png`;
1858
1998
  copies.set(file, rel);
1859
1999
  urls[theme2] = `${encodePath2(rel)}?v=${entry[theme2]}`;
@@ -1877,29 +2017,29 @@ function rewriteRefs(html, originalDir, stagedDir) {
1877
2017
  (whole, attr2, quote, value) => {
1878
2018
  if (!value || /^(?:[a-z][a-z0-9+.-]*:|\/|#|\{)/i.test(value)) return whole;
1879
2019
  const [pathname = "", suffix = ""] = /^([^?#]*)(.*)$/.exec(value).slice(1);
1880
- const target = path11.resolve(originalDir, decodeURIComponent(pathname));
1881
- if (!fs11.existsSync(target)) return whole;
2020
+ const target = path12.resolve(originalDir, decodeURIComponent(pathname));
2021
+ if (!fs12.existsSync(target)) return whole;
1882
2022
  return `${attr2}${quote}${relUrl(stagedDir, target)}${suffix}${quote}`;
1883
2023
  }
1884
2024
  );
1885
2025
  });
1886
2026
  }
1887
2027
  function copyTree(from, to, filter) {
1888
- if (!fs11.existsSync(from)) return;
1889
- for (const entry of fs11.readdirSync(from, { withFileTypes: true })) {
2028
+ if (!fs12.existsSync(from)) return;
2029
+ for (const entry of fs12.readdirSync(from, { withFileTypes: true })) {
1890
2030
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1891
- const source = path11.join(from, entry.name);
1892
- const target = path11.join(to, entry.name);
2031
+ const source = path12.join(from, entry.name);
2032
+ const target = path12.join(to, entry.name);
1893
2033
  if (entry.isDirectory()) copyTree(source, target, filter);
1894
- else if (filter(source) && !fs11.existsSync(target)) {
1895
- fs11.mkdirSync(path11.dirname(target), { recursive: true });
1896
- fs11.copyFileSync(source, target);
2034
+ else if (filter(source) && !fs12.existsSync(target)) {
2035
+ fs12.mkdirSync(path12.dirname(target), { recursive: true });
2036
+ fs12.copyFileSync(source, target);
1897
2037
  }
1898
2038
  }
1899
2039
  }
1900
2040
  function writeJson(file, value) {
1901
- fs11.mkdirSync(path11.dirname(file), { recursive: true });
1902
- fs11.writeFileSync(file, JSON.stringify(value));
2041
+ fs12.mkdirSync(path12.dirname(file), { recursive: true });
2042
+ fs12.writeFileSync(file, JSON.stringify(value));
1903
2043
  }
1904
2044
  async function buildSite(paths, out, options = {}) {
1905
2045
  const snapshots = staticSnapshots(paths);
@@ -1914,21 +2054,21 @@ async function buildSite(paths, out, options = {}) {
1914
2054
  const system = options.includeSystem === false ? null : loadedSystem;
1915
2055
  const entries = new Entries(project);
1916
2056
  entries.sync();
1917
- linkShippedPackages(paths);
2057
+ linkShippedPackages(paths, project.appDir());
1918
2058
  const publicDir = project.publicDir();
1919
2059
  const publicFiles2 = project.publicFiles();
1920
- const staging = path11.join(paths.cache, "build", "src");
1921
- fs11.rmSync(staging, { recursive: true, force: true });
2060
+ const staging = path12.join(paths.cache, "build", "src");
2061
+ fs12.rmSync(staging, { recursive: true, force: true });
1922
2062
  const fonts = loadedSystem?.doc.fonts ?? [];
1923
2063
  const sources = [...system?.specimens ?? [], ...canvases.flatMap((canvas) => canvas.screens)];
1924
2064
  const inputs = [];
1925
2065
  const htmlScreens = [];
1926
2066
  for (const source of sources) {
1927
- if (!source.builtin && !fs11.existsSync(source.file)) continue;
1928
- const siteDir = path11.join("_s", source.canvas, source.id);
1929
- const dir = path11.join(staging, siteDir);
1930
- const file = path11.join(dir, "index.html");
1931
- fs11.mkdirSync(dir, { recursive: true });
2067
+ if (!source.builtin && !fs12.existsSync(source.file)) continue;
2068
+ const siteDir = path12.join("_s", source.canvas, source.id);
2069
+ const dir = path12.join(staging, siteDir);
2070
+ const file = path12.join(dir, "index.html");
2071
+ fs12.mkdirSync(dir, { recursive: true });
1932
2072
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
1933
2073
  const head = {
1934
2074
  source,
@@ -1937,24 +2077,24 @@ async function buildSite(paths, out, options = {}) {
1937
2077
  snapshots: false,
1938
2078
  autoHeight: source.autoHeight ?? false,
1939
2079
  // The public folder lands at the site root, which frames reach through `../`.
1940
- public: publicFiles2.length ? { base: `${toPosix(path11.relative(dir, staging))}/`, files: publicFiles2 } : void 0
2080
+ public: publicFiles2.length ? { base: `${toPosix(path12.relative(dir, staging))}/`, files: publicFiles2 } : void 0
1941
2081
  };
1942
2082
  if (source.format === "module") {
1943
- fs11.writeFileSync(file, moduleShell(head, relUrl(dir, entries.entryFile(source))));
2083
+ fs12.writeFileSync(file, moduleShell(head, relUrl(dir, entries.entryFile(source))));
1944
2084
  } else {
1945
- const raw = rewriteRefs(fs11.readFileSync(source.file, "utf8"), path11.dirname(source.file), dir);
2085
+ const raw = rewriteRefs(fs12.readFileSync(source.file, "utf8"), path12.dirname(source.file), dir);
1946
2086
  const withSystem = source.system && !optsOutOfSystem(raw);
1947
2087
  const html = injectIntoHtml(raw, {
1948
2088
  ...head,
1949
2089
  systemCss: withSystem ? relUrl(dir, entries.systemCssFile()) : void 0,
1950
- boot: relUrl(dir, path11.join(entries.dir, HTML_BOOT))
2090
+ boot: relUrl(dir, path12.join(entries.dir, HTML_BOOT))
1951
2091
  });
1952
- fs11.writeFileSync(file, html);
2092
+ fs12.writeFileSync(file, html);
1953
2093
  htmlScreens.push({ source, dir: siteDir });
1954
2094
  }
1955
2095
  inputs.push(file);
1956
2096
  }
1957
- fs11.mkdirSync(out, { recursive: true });
2097
+ fs12.mkdirSync(out, { recursive: true });
1958
2098
  if (inputs.length) {
1959
2099
  const base = baseConfig(project);
1960
2100
  await build({
@@ -1975,14 +2115,14 @@ async function buildSite(paths, out, options = {}) {
1975
2115
  }
1976
2116
  });
1977
2117
  } else {
1978
- fs11.rmSync(out, { recursive: true, force: true });
1979
- fs11.mkdirSync(out, { recursive: true });
2118
+ fs12.rmSync(out, { recursive: true, force: true });
2119
+ fs12.mkdirSync(out, { recursive: true });
1980
2120
  }
1981
2121
  for (const { source, dir } of htmlScreens) {
1982
2122
  copyTree(
1983
- path11.dirname(source.file),
1984
- path11.join(out, dir),
1985
- (file) => !SOURCE_EXT.has(path11.extname(file).toLowerCase())
2123
+ path12.dirname(source.file),
2124
+ path12.join(out, dir),
2125
+ (file) => !SOURCE_EXT.has(path12.extname(file).toLowerCase())
1986
2126
  );
1987
2127
  }
1988
2128
  const files = /* @__PURE__ */ new Set();
@@ -1992,38 +2132,40 @@ async function buildSite(paths, out, options = {}) {
1992
2132
  for (const item of section.items) if (item.kind === "image" && item.url.startsWith("_f/")) files.add(item.url);
1993
2133
  }
1994
2134
  for (const asset of system?.doc.assets ?? []) files.add(asset.url);
2135
+ const icon = project.iconUrl();
2136
+ if (icon) files.add(icon.split("?")[0]);
1995
2137
  for (const url of files) {
1996
2138
  const rel = url.slice("_f/".length).split("/").map(decodeURIComponent).join("/");
1997
- const from = path11.join(paths.design, rel);
1998
- if (!fs11.existsSync(from)) continue;
1999
- const to = path11.join(out, "_f", rel);
2000
- fs11.mkdirSync(path11.dirname(to), { recursive: true });
2001
- fs11.copyFileSync(from, to);
2139
+ const from = path12.join(paths.design, rel);
2140
+ if (!fs12.existsSync(from)) continue;
2141
+ const to = path12.join(out, "_f", rel);
2142
+ fs12.mkdirSync(path12.dirname(to), { recursive: true });
2143
+ fs12.copyFileSync(from, to);
2002
2144
  }
2003
2145
  const info = project.info(true);
2004
2146
  const snapshotDirs = canvasIds.map((id2) => `_snap/${id2}/`);
2005
2147
  for (const [from, rel] of snapshots.copies) {
2006
2148
  if (!snapshotDirs.some((dir) => rel.startsWith(dir))) continue;
2007
- const to = path11.join(out, rel);
2008
- fs11.mkdirSync(path11.dirname(to), { recursive: true });
2009
- fs11.copyFileSync(from, to);
2149
+ const to = path12.join(out, rel);
2150
+ fs12.mkdirSync(path12.dirname(to), { recursive: true });
2151
+ fs12.copyFileSync(from, to);
2010
2152
  }
2011
2153
  if (!system) info.system = null;
2012
2154
  info.canvases = info.canvases.filter((canvas) => canvasIds.includes(canvas.id));
2013
- writeJson(path11.join(out, "api", "project.json"), info);
2014
- for (const canvas of canvases) writeJson(path11.join(out, "api", "canvas", `${canvas.doc.id}.json`), canvas.doc);
2155
+ writeJson(path12.join(out, "api", "project.json"), info);
2156
+ for (const canvas of canvases) writeJson(path12.join(out, "api", "canvas", `${canvas.doc.id}.json`), canvas.doc);
2015
2157
  if (system) {
2016
- writeJson(path11.join(out, "api", "system.json"), system.doc);
2158
+ writeJson(path12.join(out, "api", "system.json"), system.doc);
2017
2159
  const texts = {};
2018
2160
  for (const component of system.doc.components) {
2019
2161
  for (const rel of [component.specimen, ...component.sources]) {
2020
- const file = path11.join(paths.root, rel);
2021
- if (!(rel in texts) && fs11.existsSync(file)) texts[rel] = fs11.readFileSync(file, "utf8");
2162
+ const file = path12.join(paths.root, rel);
2163
+ if (!(rel in texts) && fs12.existsSync(file)) texts[rel] = fs12.readFileSync(file, "utf8");
2022
2164
  }
2023
2165
  }
2024
- writeJson(path11.join(out, "api", "sources.json"), texts);
2166
+ writeJson(path12.join(out, "api", "sources.json"), texts);
2025
2167
  }
2026
- fs11.rmSync(staging, { recursive: true, force: true });
2168
+ fs12.rmSync(staging, { recursive: true, force: true });
2027
2169
  const errors = [
2028
2170
  ...only ? [] : project.config().issues,
2029
2171
  ...system?.doc.issues ?? [],
@@ -2032,21 +2174,21 @@ async function buildSite(paths, out, options = {}) {
2032
2174
  const { absoluteAssets } = options.includeViewer === false ? { absoluteAssets: false } : copyViewer(PKG.viewer, out);
2033
2175
  if (publicDir) {
2034
2176
  for (const rel of publicFiles2) {
2035
- const to = path11.join(out, rel);
2036
- if (fs11.existsSync(to)) continue;
2037
- fs11.mkdirSync(path11.dirname(to), { recursive: true });
2038
- fs11.copyFileSync(path11.join(publicDir, rel), to);
2177
+ const to = path12.join(out, rel);
2178
+ if (fs12.existsSync(to)) continue;
2179
+ fs12.mkdirSync(path12.dirname(to), { recursive: true });
2180
+ fs12.copyFileSync(path12.join(publicDir, rel), to);
2039
2181
  }
2040
2182
  }
2041
2183
  return { out, canvases: canvasIds, frames: inputs.length, errors, absoluteAssets };
2042
2184
  }
2043
2185
  function copyViewer(viewerDir, out) {
2044
2186
  copyTree(viewerDir, out, () => true);
2045
- const index = path11.join(out, "index.html");
2046
- const html = fs11.readFileSync(index, "utf8");
2187
+ const index = path12.join(out, "index.html");
2188
+ const html = fs12.readFileSync(index, "utf8");
2047
2189
  const flag2 = "<script>window.__DESIGN_STATIC__=true</script>";
2048
2190
  const head = /<head(\s[^>]*)?>/i.exec(html);
2049
- fs11.writeFileSync(
2191
+ fs12.writeFileSync(
2050
2192
  index,
2051
2193
  head ? html.slice(0, head.index + head[0].length) + flag2 + html.slice(head.index + head[0].length) : flag2 + html
2052
2194
  );
@@ -2128,8 +2270,8 @@ function progress() {
2128
2270
  // src/cli/commands/build.ts
2129
2271
  function pack(out) {
2130
2272
  const archive = `${out.replace(/[\\/]+$/, "")}.tar.gz`;
2131
- fs12.rmSync(archive, { force: true });
2132
- const entries = fs12.readdirSync(out);
2273
+ fs13.rmSync(archive, { force: true });
2274
+ const entries = fs13.readdirSync(out);
2133
2275
  const result = spawnSync("tar", ["-czf", archive, "-C", out, ...entries], {
2134
2276
  stdio: ["ignore", "ignore", "pipe"],
2135
2277
  env: { ...process.env, COPYFILE_DISABLE: "1" }
@@ -2139,10 +2281,10 @@ function pack(out) {
2139
2281
  return archive;
2140
2282
  }
2141
2283
  async function runBuild(paths, canvases, options) {
2142
- if (!fs12.existsSync(path12.join(PKG.viewer, "index.html"))) {
2284
+ if (!fs13.existsSync(path13.join(PKG.viewer, "index.html"))) {
2143
2285
  throw new CliError("The viewer is missing from this foss-design install (dist/viewer). Rebuild or reinstall it.");
2144
2286
  }
2145
- const out = path12.resolve(options.out ?? path12.join(paths.cache, "site"));
2287
+ const out = path13.resolve(options.out ?? path13.join(paths.cache, "site"));
2146
2288
  if (out === paths.root || out === paths.design) throw new CliError(`Refusing to build into ${out}`);
2147
2289
  print(dim(`Building ${canvases.length ? canvases.join(", ") : "every canvas"} and the design system\u2026`));
2148
2290
  let result;
@@ -2167,8 +2309,8 @@ import os from "os";
2167
2309
 
2168
2310
  // src/cloud/units.ts
2169
2311
  import { createHash as createHash3 } from "crypto";
2170
- import fs13 from "fs";
2171
- import path13 from "path";
2312
+ import fs14 from "fs";
2313
+ import path14 from "path";
2172
2314
  var SYSTEM_UNIT = "system";
2173
2315
  var CANVAS_PREFIX = "canvas/";
2174
2316
  var LOCAL_ONLY = /* @__PURE__ */ new Set(["cloud.json", "node_modules"]);
@@ -2196,7 +2338,7 @@ function caseClashes(keys) {
2196
2338
  }
2197
2339
  function unitRoot(paths, key) {
2198
2340
  const id2 = unitCanvasId(key);
2199
- return id2 === null ? paths.design : path13.join(paths.canvases, id2);
2341
+ return id2 === null ? paths.design : path14.join(paths.canvases, id2);
2200
2342
  }
2201
2343
  function unitRelative(key, rel) {
2202
2344
  const id2 = unitCanvasId(key);
@@ -2214,7 +2356,7 @@ function hashBuffer(data) {
2214
2356
  return createHash3("sha256").update(data).digest("hex");
2215
2357
  }
2216
2358
  function hashFile(file) {
2217
- const data = fs13.readFileSync(file);
2359
+ const data = fs14.readFileSync(file);
2218
2360
  return { hash: hashBuffer(data), size: data.length };
2219
2361
  }
2220
2362
  var warnedLinks = /* @__PURE__ */ new Set();
@@ -2227,34 +2369,34 @@ function warnLink(rel) {
2227
2369
  function walk(dir, base, out) {
2228
2370
  let entries;
2229
2371
  try {
2230
- entries = fs13.readdirSync(dir, { withFileTypes: true });
2372
+ entries = fs14.readdirSync(dir, { withFileTypes: true });
2231
2373
  } catch {
2232
2374
  return;
2233
2375
  }
2234
2376
  for (const entry of entries) {
2235
2377
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2236
- const full = path13.join(dir, entry.name);
2378
+ const full = path14.join(dir, entry.name);
2237
2379
  let isFile = entry.isFile();
2238
2380
  let isDir = entry.isDirectory();
2239
2381
  if (entry.isSymbolicLink()) {
2240
2382
  isDir = false;
2241
2383
  isFile = false;
2242
2384
  try {
2243
- const target = fs13.realpathSync(full);
2244
- const root = fs13.realpathSync(base);
2245
- if (target.startsWith(root + path13.sep)) isFile = fs13.statSync(target).isFile();
2246
- else warnLink(toPosix(path13.relative(base, full)));
2385
+ const target = fs14.realpathSync(full);
2386
+ const root = fs14.realpathSync(base);
2387
+ if (target.startsWith(root + path14.sep)) isFile = fs14.statSync(target).isFile();
2388
+ else warnLink(toPosix(path14.relative(base, full)));
2247
2389
  } catch {
2248
2390
  }
2249
2391
  }
2250
2392
  if (isDir) walk(full, base, out);
2251
- else if (isFile) out[toPosix(path13.relative(base, full))] = hashFile(full);
2393
+ else if (isFile) out[toPosix(path14.relative(base, full))] = hashFile(full);
2252
2394
  }
2253
2395
  }
2254
2396
  function unitManifest(paths, key) {
2255
2397
  const manifest = {};
2256
2398
  if (key === SYSTEM_UNIT) {
2257
- if (fs13.existsSync(paths.config) && fs13.statSync(paths.config).isFile())
2399
+ if (fs14.existsSync(paths.config) && fs14.statSync(paths.config).isFile())
2258
2400
  manifest["design.json"] = hashFile(paths.config);
2259
2401
  walk(paths.system, paths.design, manifest);
2260
2402
  } else walk(unitRoot(paths, key), paths.design, manifest);
@@ -2263,13 +2405,13 @@ function unitManifest(paths, key) {
2263
2405
  function buildManifest(dir) {
2264
2406
  const manifest = {};
2265
2407
  const visit = (current) => {
2266
- for (const entry of fs13.readdirSync(current, { withFileTypes: true })) {
2267
- const full = path13.join(current, entry.name);
2408
+ for (const entry of fs14.readdirSync(current, { withFileTypes: true })) {
2409
+ const full = path14.join(current, entry.name);
2268
2410
  if (entry.isDirectory()) visit(full);
2269
- else if (entry.isFile()) manifest[toPosix(path13.relative(dir, full))] = hashFile(full);
2411
+ else if (entry.isFile()) manifest[toPosix(path14.relative(dir, full))] = hashFile(full);
2270
2412
  }
2271
2413
  };
2272
- if (fs13.existsSync(dir)) visit(dir);
2414
+ if (fs14.existsSync(dir)) visit(dir);
2273
2415
  return sortManifest(manifest);
2274
2416
  }
2275
2417
  function sortManifest(manifest) {
@@ -2278,18 +2420,18 @@ function sortManifest(manifest) {
2278
2420
  function scanLocal(paths) {
2279
2421
  const units = /* @__PURE__ */ new Map();
2280
2422
  const notSynced = [];
2281
- if (!fs13.existsSync(paths.design)) return { units, notSynced };
2423
+ if (!fs14.existsSync(paths.design)) return { units, notSynced };
2282
2424
  const system = unitManifest(paths, SYSTEM_UNIT);
2283
2425
  if (Object.keys(system).length) units.set(SYSTEM_UNIT, { key: SYSTEM_UNIT, kind: "system", manifest: system });
2284
- for (const entry of fs13.readdirSync(paths.design, { withFileTypes: true })) {
2426
+ for (const entry of fs14.readdirSync(paths.design, { withFileTypes: true })) {
2285
2427
  const { name } = entry;
2286
2428
  if (name.startsWith(".") || LOCAL_ONLY.has(name)) continue;
2287
2429
  if (name === "design.json" && entry.isFile()) continue;
2288
2430
  if ((name === "system" || name === "canvas") && entry.isDirectory()) continue;
2289
2431
  notSynced.push(entry.isDirectory() ? `${name}/` : name);
2290
2432
  }
2291
- if (fs13.existsSync(paths.canvases) && fs13.statSync(paths.canvases).isDirectory()) {
2292
- for (const entry of fs13.readdirSync(paths.canvases, { withFileTypes: true })) {
2433
+ if (fs14.existsSync(paths.canvases) && fs14.statSync(paths.canvases).isDirectory()) {
2434
+ for (const entry of fs14.readdirSync(paths.canvases, { withFileTypes: true })) {
2293
2435
  if (entry.name.startsWith(".")) continue;
2294
2436
  if (!entry.isDirectory() || !ID_PATTERN.test(entry.name)) {
2295
2437
  notSynced.push(`canvas/${entry.name}${entry.isDirectory() ? "/" : ""}`);
@@ -2303,11 +2445,11 @@ function scanLocal(paths) {
2303
2445
  return { units, notSynced: notSynced.sort() };
2304
2446
  }
2305
2447
  function unitBuildDir(paths, key) {
2306
- return path13.join(paths.cache, "cloud", "build", ...key.split("/"));
2448
+ return path14.join(paths.cache, "cloud", "build", ...key.split("/"));
2307
2449
  }
2308
2450
  async function buildUnit(paths, key, out = unitBuildDir(paths, key)) {
2309
2451
  const id2 = unitCanvasId(key);
2310
- fs13.rmSync(out, { recursive: true, force: true });
2452
+ fs14.rmSync(out, { recursive: true, force: true });
2311
2453
  return buildSite(paths, out, {
2312
2454
  canvases: id2 === null ? [] : [id2],
2313
2455
  includeSystem: id2 === null,
@@ -2438,7 +2580,7 @@ var CloudClient = class {
2438
2580
  }
2439
2581
  host;
2440
2582
  token;
2441
- async send(method, path29, init) {
2583
+ async send(method, path31, init) {
2442
2584
  const headers = {
2443
2585
  "User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
2444
2586
  Accept: "application/json"
@@ -2446,7 +2588,7 @@ var CloudClient = class {
2446
2588
  if (this.token) headers.Authorization = `Bearer ${this.token}`;
2447
2589
  if (init.type) headers["Content-Type"] = init.type;
2448
2590
  try {
2449
- return await fetch(`${this.host}/api${path29}`, {
2591
+ return await fetch(`${this.host}/api${path31}`, {
2450
2592
  method,
2451
2593
  headers,
2452
2594
  body: init.body,
@@ -2467,8 +2609,8 @@ var CloudClient = class {
2467
2609
  throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
2468
2610
  }
2469
2611
  /** JSON in, JSON out; any non-2xx answer throws a CloudError. */
2470
- async request(method, path29, body) {
2471
- const response = await this.send(method, path29, {
2612
+ async request(method, path31, body) {
2613
+ const response = await this.send(method, path31, {
2472
2614
  body: body === void 0 ? void 0 : JSON.stringify(body),
2473
2615
  type: body === void 0 ? void 0 : "application/json",
2474
2616
  timeout: JSON_TIMEOUT
@@ -2480,7 +2622,7 @@ var CloudClient = class {
2480
2622
  try {
2481
2623
  return JSON.parse(text2);
2482
2624
  } catch {
2483
- throw new CloudError(`${this.host} answered ${method} ${path29} with something other than JSON`, 0, "bad_response");
2625
+ throw new CloudError(`${this.host} answered ${method} ${path31} with something other than JSON`, 0, "bad_response");
2484
2626
  }
2485
2627
  }
2486
2628
  /** POST /device/code. */
@@ -2614,9 +2756,9 @@ var CloudClient = class {
2614
2756
  };
2615
2757
 
2616
2758
  // src/cloud/credentials.ts
2617
- import fs14 from "fs";
2759
+ import fs15 from "fs";
2618
2760
  import os2 from "os";
2619
- import path14 from "path";
2761
+ import path15 from "path";
2620
2762
  var DEFAULT_CLOUD = "https://app.fossdesign.dev";
2621
2763
  function normalizeHost(value) {
2622
2764
  let url;
@@ -2633,14 +2775,14 @@ function cloudHost() {
2633
2775
  return normalizeHost(process.env.FOSS_DESIGN_CLOUD || DEFAULT_CLOUD);
2634
2776
  }
2635
2777
  function credentialsFile() {
2636
- const base = process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : process.env.XDG_CONFIG_HOME || path14.join(os2.homedir(), ".config");
2637
- return path14.join(base, "foss-design", "credentials.json");
2778
+ const base = process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : process.env.XDG_CONFIG_HOME || path15.join(os2.homedir(), ".config");
2779
+ return path15.join(base, "foss-design", "credentials.json");
2638
2780
  }
2639
2781
  function readAll() {
2640
2782
  const file = credentialsFile();
2641
- if (!fs14.existsSync(file)) return {};
2783
+ if (!fs15.existsSync(file)) return {};
2642
2784
  try {
2643
- const data = JSON.parse(fs14.readFileSync(file, "utf8"));
2785
+ const data = JSON.parse(fs15.readFileSync(file, "utf8"));
2644
2786
  return data && typeof data === "object" && !Array.isArray(data) ? data : {};
2645
2787
  } catch (error) {
2646
2788
  throw new CliError(`${file} is not valid JSON (${error.message}); fix or delete it`);
@@ -2648,12 +2790,12 @@ function readAll() {
2648
2790
  }
2649
2791
  function writeAll(data) {
2650
2792
  const file = credentialsFile();
2651
- fs14.mkdirSync(path14.dirname(file), { recursive: true, mode: 448 });
2793
+ fs15.mkdirSync(path15.dirname(file), { recursive: true, mode: 448 });
2652
2794
  const temp = `${file}.${process.pid}.tmp`;
2653
- fs14.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
2795
+ fs15.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
2654
2796
  `, { mode: 384 });
2655
- fs14.chmodSync(temp, 384);
2656
- fs14.renameSync(temp, file);
2797
+ fs15.chmodSync(temp, 384);
2798
+ fs15.renameSync(temp, file);
2657
2799
  }
2658
2800
  function getCredential(host) {
2659
2801
  const entry = readAll()[host];
@@ -2676,17 +2818,17 @@ function requireCredential(host) {
2676
2818
  }
2677
2819
 
2678
2820
  // src/cloud/state.ts
2679
- import fs15 from "fs";
2680
- import path15 from "path";
2681
- var linkFile = (paths) => path15.join(paths.design, "cloud.json");
2682
- var cloudCache = (paths) => path15.join(paths.cache, "cloud");
2683
- var incomingDir = (paths, key) => path15.join(cloudCache(paths), "incoming", ...key.split("/"));
2821
+ import fs16 from "fs";
2822
+ import path16 from "path";
2823
+ var linkFile = (paths) => path16.join(paths.design, "cloud.json");
2824
+ var cloudCache = (paths) => path16.join(paths.cache, "cloud");
2825
+ var incomingDir = (paths, key) => path16.join(cloudCache(paths), "incoming", ...key.split("/"));
2684
2826
  function readLink(paths) {
2685
2827
  const file = linkFile(paths);
2686
- if (!fs15.existsSync(file)) return null;
2828
+ if (!fs16.existsSync(file)) return null;
2687
2829
  let data;
2688
2830
  try {
2689
- data = JSON.parse(fs15.readFileSync(file, "utf8"));
2831
+ data = JSON.parse(fs16.readFileSync(file, "utf8"));
2690
2832
  } catch (error) {
2691
2833
  throw new CliError(`${file} is not valid JSON (${error.message}); run \`design link <project>\` again`);
2692
2834
  }
@@ -2716,9 +2858,9 @@ function writeLink(paths, link) {
2716
2858
  units: sorted(link.units),
2717
2859
  conflicts: sorted(link.conflicts)
2718
2860
  };
2719
- fs15.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
2861
+ fs16.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
2720
2862
  `);
2721
- fs15.renameSync(temp, file);
2863
+ fs16.renameSync(temp, file);
2722
2864
  }
2723
2865
  var hashes = (manifest) => Object.fromEntries(Object.entries(manifest).map(([file, entry]) => [file, entry.hash]));
2724
2866
  function diffFiles(a, b) {
@@ -2796,8 +2938,8 @@ var byUnit = (a, b) => a === SYSTEM_UNIT ? -1 : b === SYSTEM_UNIT ? 1 : a < b ?
2796
2938
  var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
2797
2939
 
2798
2940
  // src/cloud/pull.ts
2799
- import fs16 from "fs";
2800
- import path16 from "path";
2941
+ import fs17 from "fs";
2942
+ import path17 from "path";
2801
2943
 
2802
2944
  // src/capture/frames.ts
2803
2945
  var AUTO_VIEWPORT_HEIGHT = 900;
@@ -2880,34 +3022,34 @@ function checkManifest(key, manifest) {
2880
3022
  }
2881
3023
  }
2882
3024
  function writeInside(root, rel, data) {
2883
- const file = path16.join(root, rel);
3025
+ const file = path17.join(root, rel);
2884
3026
  if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
2885
- fs16.mkdirSync(path16.dirname(file), { recursive: true });
2886
- if (!isInside(fs16.realpathSync(root), fs16.realpathSync(path16.dirname(file))))
3027
+ fs17.mkdirSync(path17.dirname(file), { recursive: true });
3028
+ if (!isInside(fs17.realpathSync(root), fs17.realpathSync(path17.dirname(file))))
2887
3029
  throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
2888
- if (fs16.existsSync(file) && fs16.lstatSync(file).isSymbolicLink()) fs16.unlinkSync(file);
2889
- fs16.writeFileSync(file, data);
3030
+ if (fs17.existsSync(file) && fs17.lstatSync(file).isSymbolicLink()) fs17.unlinkSync(file);
3031
+ fs17.writeFileSync(file, data);
2890
3032
  }
2891
3033
  function pruneEmpty(dir, stop) {
2892
3034
  let current = dir;
2893
3035
  while (isInside(stop, current) && current !== stop) {
2894
3036
  try {
2895
- if (fs16.readdirSync(current).length) return;
2896
- fs16.rmdirSync(current);
3037
+ if (fs17.readdirSync(current).length) return;
3038
+ fs17.rmdirSync(current);
2897
3039
  } catch {
2898
3040
  return;
2899
3041
  }
2900
- current = path16.dirname(current);
3042
+ current = path17.dirname(current);
2901
3043
  }
2902
3044
  }
2903
3045
  function applyUnit(paths, key, local, target, blobs) {
2904
3046
  const counts = { added: 0, changed: 0, removed: 0 };
2905
- const stop = key === SYSTEM_UNIT ? paths.design : path16.dirname(unitRoot(paths, key));
3047
+ const stop = key === SYSTEM_UNIT ? paths.design : path17.dirname(unitRoot(paths, key));
2906
3048
  for (const rel of Object.keys(local)) {
2907
3049
  if (rel in target) continue;
2908
- const file = path16.join(paths.design, rel);
2909
- fs16.rmSync(file, { force: true });
2910
- pruneEmpty(path16.dirname(file), stop);
3050
+ const file = path17.join(paths.design, rel);
3051
+ fs17.rmSync(file, { force: true });
3052
+ pruneEmpty(path17.dirname(file), stop);
2911
3053
  counts.removed++;
2912
3054
  }
2913
3055
  for (const [rel, entry] of Object.entries(target)) {
@@ -2970,7 +3112,7 @@ async function pull(ctx, options) {
2970
3112
  } else if (!s.ahead) {
2971
3113
  if (s.conflictRev !== void 0) {
2972
3114
  delete link.conflicts[key];
2973
- fs16.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3115
+ fs17.rmSync(incomingDir(paths, key), { recursive: true, force: true });
2974
3116
  }
2975
3117
  report.units.push({ ...reportFor("up_to_date"), rev: s.baseRev });
2976
3118
  } else if (!s.changed.length) {
@@ -3000,14 +3142,14 @@ async function pull(ctx, options) {
3000
3142
  headRev: unit.headRev,
3001
3143
  incoming: incomingDir(paths, key)
3002
3144
  };
3003
- entry.message = `changed here and in the cloud; the cloud version (r${unit.headRev}) is in ${path16.relative(paths.root, entry.incoming)}`;
3004
- if (s.conflictRev === unit.headRev && fs16.existsSync(entry.incoming)) report.units.push(entry);
3145
+ entry.message = `changed here and in the cloud; the cloud version (r${unit.headRev}) is in ${path17.relative(paths.root, entry.incoming)}`;
3146
+ if (s.conflictRev === unit.headRev && fs17.existsSync(entry.incoming)) report.units.push(entry);
3005
3147
  else plans.push({ key, kind: "incoming", remote: unit, report: entry });
3006
3148
  }
3007
3149
  }
3008
3150
  const byHash = /* @__PURE__ */ new Map();
3009
3151
  for (const unit of scan.units.values())
3010
- for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path16.join(paths.design, rel));
3152
+ for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path17.join(paths.design, rel));
3011
3153
  const needed = /* @__PURE__ */ new Set();
3012
3154
  for (const plan of plans) {
3013
3155
  if (!plan.remote) continue;
@@ -3018,7 +3160,7 @@ async function pull(ctx, options) {
3018
3160
  const download = [];
3019
3161
  for (const hash of needed) {
3020
3162
  const file = byHash.get(hash);
3021
- if (file) blobs.set(hash, fs16.readFileSync(file));
3163
+ if (file) blobs.set(hash, fs17.readFileSync(file));
3022
3164
  else download.push(hash);
3023
3165
  }
3024
3166
  if (download.length) {
@@ -3044,11 +3186,11 @@ async function pull(ctx, options) {
3044
3186
  if (plan.remote) link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
3045
3187
  else delete link.units[key];
3046
3188
  delete link.conflicts[key];
3047
- fs16.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3189
+ fs17.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3048
3190
  } else {
3049
3191
  const dir = incomingDir(paths, key);
3050
- fs16.rmSync(dir, { recursive: true, force: true });
3051
- fs16.mkdirSync(dir, { recursive: true });
3192
+ fs17.rmSync(dir, { recursive: true, force: true });
3193
+ fs17.mkdirSync(dir, { recursive: true });
3052
3194
  for (const [rel, file] of Object.entries(plan.remote.manifest))
3053
3195
  writeInside(dir, unitRelative(key, rel), blobs.get(file.hash));
3054
3196
  link.conflicts[key] = plan.remote.headRev;
@@ -3069,8 +3211,8 @@ async function pull(ctx, options) {
3069
3211
  }
3070
3212
 
3071
3213
  // src/cloud/push.ts
3072
- import fs17 from "fs";
3073
- import path17 from "path";
3214
+ import fs18 from "fs";
3215
+ import path18 from "path";
3074
3216
  function blockedReport(status, message) {
3075
3217
  return {
3076
3218
  unit: status.key,
@@ -3176,7 +3318,7 @@ async function push(ctx, options) {
3176
3318
  for (const key of toPush) {
3177
3319
  const s = status(key);
3178
3320
  if (s.conflictRev !== void 0) {
3179
- const dir = path17.relative(paths.root, incomingDir(paths, key));
3321
+ const dir = path18.relative(paths.root, incomingDir(paths, key));
3180
3322
  blocked.push(
3181
3323
  blockedReport(s, `pull left the cloud version in ${dir}; merge it, then \`design push --resolved ${key}\``)
3182
3324
  );
@@ -3220,7 +3362,7 @@ async function push(ctx, options) {
3220
3362
  const files = /* @__PURE__ */ new Map();
3221
3363
  const addFiles = (manifest, dir) => {
3222
3364
  for (const [rel, entry] of Object.entries(manifest))
3223
- if (!files.has(entry.hash)) files.set(entry.hash, path17.join(dir, rel));
3365
+ if (!files.has(entry.hash)) files.set(entry.hash, path18.join(dir, rel));
3224
3366
  };
3225
3367
  for (const key of pushing) {
3226
3368
  const id2 = unitCanvasId(key);
@@ -3252,7 +3394,7 @@ async function push(ctx, options) {
3252
3394
  if (missing.length) {
3253
3395
  const unknown = missing.find((hash) => !files.has(hash));
3254
3396
  if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
3255
- const bytes = missing.reduce((sum, hash) => sum + fs17.statSync(files.get(hash)).size, 0);
3397
+ const bytes = missing.reduce((sum, hash) => sum + fs18.statSync(files.get(hash)).size, 0);
3256
3398
  const bar = ctx.progress();
3257
3399
  const started = Date.now();
3258
3400
  let sentFiles = 0;
@@ -3261,12 +3403,12 @@ async function push(ctx, options) {
3261
3403
  bar.update(line());
3262
3404
  await mapLimit(missing, TRANSFERS, async (hash) => {
3263
3405
  const file = files.get(hash);
3264
- const data = fs17.readFileSync(file);
3406
+ const data = fs18.readFileSync(file);
3265
3407
  try {
3266
3408
  await client.putBlob(link.project, hash, data);
3267
3409
  } catch (error) {
3268
3410
  if (error instanceof CloudError && error.status === 413)
3269
- throw new CliError(`${path17.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
3411
+ throw new CliError(`${path18.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
3270
3412
  throw error;
3271
3413
  }
3272
3414
  sentFiles++;
@@ -3288,7 +3430,7 @@ async function push(ctx, options) {
3288
3430
  url: webUrl(link, unit.key)
3289
3431
  });
3290
3432
  }
3291
- for (const key of resolved) fs17.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3433
+ for (const key of resolved) fs18.rmSync(incomingDir(paths, key), { recursive: true, force: true });
3292
3434
  writeLink(paths, link);
3293
3435
  report.units.sort((a, b) => byUnit(a.unit, b.unit));
3294
3436
  return report;
@@ -3299,7 +3441,7 @@ function refuseLegacyGo(paths, keys) {
3299
3441
  for (const key of keys) {
3300
3442
  if (!key.startsWith("canvas/")) continue;
3301
3443
  const id2 = key.slice("canvas/".length);
3302
- for (const file of canvasCodeFiles(path17.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
3444
+ for (const file of canvasCodeFiles(path18.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
3303
3445
  for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
3304
3446
  }
3305
3447
  }
@@ -3620,7 +3762,7 @@ async function runMe(paths, options) {
3620
3762
  }
3621
3763
 
3622
3764
  // src/capture/chrome.ts
3623
- import fs18 from "fs";
3765
+ import fs19 from "fs";
3624
3766
  var CANDIDATES = {
3625
3767
  darwin: [
3626
3768
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
@@ -3644,8 +3786,8 @@ var CANDIDATES = {
3644
3786
  };
3645
3787
  function findChrome() {
3646
3788
  const configured = process.env.DESIGN_CHROME;
3647
- if (configured && fs18.existsSync(configured)) return configured;
3648
- for (const candidate of CANDIDATES[process.platform] ?? []) if (fs18.existsSync(candidate)) return candidate;
3789
+ if (configured && fs19.existsSync(configured)) return configured;
3790
+ for (const candidate of CANDIDATES[process.platform] ?? []) if (fs19.existsSync(candidate)) return candidate;
3649
3791
  return null;
3650
3792
  }
3651
3793
  async function launchChrome() {
@@ -3717,26 +3859,26 @@ async function openFrame(page, url, options = {}) {
3717
3859
  }
3718
3860
 
3719
3861
  // src/server/state.ts
3720
- import fs19 from "fs";
3721
- import path18 from "path";
3722
- var stateFile = (paths) => path18.join(paths.cache, "server.json");
3723
- var logFile = (paths) => path18.join(paths.cache, "server.log");
3862
+ import fs20 from "fs";
3863
+ import path19 from "path";
3864
+ var stateFile = (paths) => path19.join(paths.cache, "server.json");
3865
+ var logFile = (paths) => path19.join(paths.cache, "server.log");
3724
3866
  function readState(paths) {
3725
3867
  try {
3726
- return JSON.parse(fs19.readFileSync(stateFile(paths), "utf8"));
3868
+ return JSON.parse(fs20.readFileSync(stateFile(paths), "utf8"));
3727
3869
  } catch {
3728
3870
  return null;
3729
3871
  }
3730
3872
  }
3731
3873
  function writeState(paths, state) {
3732
- fs19.mkdirSync(paths.cache, { recursive: true });
3733
- fs19.writeFileSync(stateFile(paths), `${JSON.stringify(state, null, 2)}
3874
+ fs20.mkdirSync(paths.cache, { recursive: true });
3875
+ fs20.writeFileSync(stateFile(paths), `${JSON.stringify(state, null, 2)}
3734
3876
  `);
3735
3877
  }
3736
3878
  function clearState(paths, pid) {
3737
3879
  const state = readState(paths);
3738
3880
  if (pid !== void 0 && state && state.pid !== pid) return;
3739
- fs19.rmSync(stateFile(paths), { force: true });
3881
+ fs20.rmSync(stateFile(paths), { force: true });
3740
3882
  }
3741
3883
  function pidAlive(pid) {
3742
3884
  try {
@@ -3761,18 +3903,18 @@ async function liveServer(paths) {
3761
3903
 
3762
3904
  // src/cli/commands/preview.ts
3763
3905
  import { spawn } from "child_process";
3764
- import fs23 from "fs";
3906
+ import fs24 from "fs";
3765
3907
 
3766
3908
  // src/server/index.ts
3767
- import fs22 from "fs";
3909
+ import fs23 from "fs";
3768
3910
  import http from "http";
3769
- import path21 from "path";
3911
+ import path22 from "path";
3770
3912
  import sirv from "sirv";
3771
3913
  import { createServer, searchForWorkspaceRoot } from "vite";
3772
3914
 
3773
3915
  // src/server/api.ts
3774
- import fs20 from "fs";
3775
- import path19 from "path";
3916
+ import fs21 from "fs";
3917
+ import path20 from "path";
3776
3918
  function sendJson(res, status, body) {
3777
3919
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
3778
3920
  res.end(JSON.stringify(body));
@@ -3823,17 +3965,34 @@ async function handleApi(ctx, req, res, url) {
3823
3965
  }
3824
3966
  if (route2 === "/source") {
3825
3967
  const rel = url.searchParams.get("path") ?? "";
3826
- const file = path19.resolve(project.paths.root, rel);
3827
- if (!rel || !isInside(project.paths.root, file) || !fs20.existsSync(file) || fs20.statSync(file).isDirectory()) {
3968
+ const file = path20.resolve(project.paths.root, rel);
3969
+ if (!rel || !isInside(project.paths.root, file) || !fs21.existsSync(file) || fs21.statSync(file).isDirectory()) {
3828
3970
  sendJson(res, 404, { error: "not found" });
3829
3971
  return true;
3830
3972
  }
3831
- if (fs20.statSync(file).size > 2 * 1024 * 1024) {
3973
+ if (fs21.statSync(file).size > 2 * 1024 * 1024) {
3832
3974
  sendJson(res, 413, { error: "file too large to show" });
3833
3975
  return true;
3834
3976
  }
3835
3977
  res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
3836
- res.end(fs20.readFileSync(file, "utf8"));
3978
+ res.end(fs21.readFileSync(file, "utf8"));
3979
+ return true;
3980
+ }
3981
+ if (route2 === "/icon" && (method === "PUT" || method === "DELETE")) {
3982
+ try {
3983
+ const body = method === "PUT" ? await readBody(req, MAX_ICON_BYTES * 4) : null;
3984
+ const problem = body ? iconProblem(body) : null;
3985
+ if (problem) {
3986
+ sendJson(res, 400, { error: problem });
3987
+ return true;
3988
+ }
3989
+ writeIcon(project.paths, body);
3990
+ project.invalidateSystem();
3991
+ events.send({ type: "project" });
3992
+ sendJson(res, 200, { icon: project.iconUrl() });
3993
+ } catch (error) {
3994
+ sendJson(res, 400, { error: error.message });
3995
+ }
3837
3996
  return true;
3838
3997
  }
3839
3998
  const snapshotPost = /^\/snapshots\/([^/]+)\/([^/]+)$/.exec(route2);
@@ -3863,7 +4022,7 @@ async function handleApi(ctx, req, res, url) {
3863
4022
  return true;
3864
4023
  }
3865
4024
  res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
3866
- fs20.createReadStream(file).pipe(res);
4025
+ fs21.createReadStream(file).pipe(res);
3867
4026
  return true;
3868
4027
  }
3869
4028
  return false;
@@ -3903,8 +4062,8 @@ var EventHub = class {
3903
4062
  };
3904
4063
 
3905
4064
  // src/server/snapshots.ts
3906
- import fs21 from "fs";
3907
- import path20 from "path";
4065
+ import fs22 from "fs";
4066
+ import path21 from "path";
3908
4067
  var SAFE = /^@?[a-z0-9][a-z0-9_-]*$/i;
3909
4068
  function isSafeSegment(value) {
3910
4069
  return SAFE.test(value);
@@ -3918,14 +4077,14 @@ var SnapshotStore = class {
3918
4077
  urlBase;
3919
4078
  meta = /* @__PURE__ */ new Map();
3920
4079
  metaFile(canvas) {
3921
- return path20.join(this.dir, canvas, "meta.json");
4080
+ return path21.join(this.dir, canvas, "meta.json");
3922
4081
  }
3923
4082
  load(canvas) {
3924
4083
  const cached = this.meta.get(canvas);
3925
4084
  if (cached) return cached;
3926
4085
  let data = {};
3927
4086
  try {
3928
- data = JSON.parse(fs21.readFileSync(this.metaFile(canvas), "utf8"));
4087
+ data = JSON.parse(fs22.readFileSync(this.metaFile(canvas), "utf8"));
3929
4088
  } catch {
3930
4089
  }
3931
4090
  this.meta.set(canvas, data);
@@ -3951,21 +4110,21 @@ var SnapshotStore = class {
3951
4110
  };
3952
4111
  save(canvas, id2, theme2, png, height) {
3953
4112
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
3954
- const dir = path20.join(this.dir, canvas);
3955
- fs21.mkdirSync(dir, { recursive: true });
3956
- fs21.writeFileSync(path20.join(dir, `${id2}.${theme2}.png`), png);
4113
+ const dir = path21.join(this.dir, canvas);
4114
+ fs22.mkdirSync(dir, { recursive: true });
4115
+ fs22.writeFileSync(path21.join(dir, `${id2}.${theme2}.png`), png);
3957
4116
  const data = this.load(canvas);
3958
4117
  const version = Date.now();
3959
4118
  data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
3960
- fs21.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
4119
+ fs22.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
3961
4120
  return { url: this.url(canvas, id2, theme2, version) };
3962
4121
  }
3963
4122
  /** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
3964
4123
  file(canvas, name) {
3965
4124
  const match = /^(@?[a-z0-9][a-z0-9_-]*)\.(light|dark)(\.thumb)?\.png$/i.exec(name);
3966
4125
  if (!isSafeSegment(canvas) || !match) return null;
3967
- const file = path20.join(this.dir, canvas, `${match[1]}.${match[2]}.png`);
3968
- if (!fs21.existsSync(file)) return null;
4126
+ const file = path21.join(this.dir, canvas, `${match[1]}.${match[2]}.png`);
4127
+ if (!fs22.existsSync(file)) return null;
3969
4128
  return match[3] ? ensureThumb(file) : file;
3970
4129
  }
3971
4130
  };
@@ -4004,16 +4163,16 @@ function notFoundPage(res, title, text2) {
4004
4163
  }
4005
4164
  async function startDevServer(options) {
4006
4165
  const paths = designPaths(options.root);
4007
- if (!fs22.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
4008
- fs22.mkdirSync(paths.cache, { recursive: true });
4166
+ if (!fs23.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
4167
+ fs23.mkdirSync(paths.cache, { recursive: true });
4009
4168
  const snapshots = new SnapshotStore(paths.snapshots);
4010
4169
  const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
4011
4170
  const entries = new Entries(project);
4012
4171
  entries.sync();
4013
- const links = linkShippedPackages(paths);
4172
+ const links = linkShippedPackages(paths, project.appDir());
4014
4173
  const events = new EventHub();
4015
4174
  const httpServer = http.createServer();
4016
- dropStaleDepCache(paths);
4175
+ dropStaleDepCache(paths, project.appDir());
4017
4176
  const base = baseConfig(project);
4018
4177
  const vite = await createServer({
4019
4178
  ...base,
@@ -4031,8 +4190,8 @@ async function startDevServer(options) {
4031
4190
  // `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
4032
4191
  // generated entries are modules the frames load, so those stay watched.
4033
4192
  ignored: (file) => {
4034
- const rel = toPosix(path21.relative(paths.cache, file));
4035
- const inCache = rel !== "" && !rel.startsWith("..") && !path21.isAbsolute(rel);
4193
+ const rel = toPosix(path22.relative(paths.cache, file));
4194
+ const inCache = rel !== "" && !rel.startsWith("..") && !path22.isAbsolute(rel);
4036
4195
  return inCache && rel !== "entries" && !rel.startsWith("entries/");
4037
4196
  }
4038
4197
  },
@@ -4041,7 +4200,7 @@ async function startDevServer(options) {
4041
4200
  forwardConsole: false
4042
4201
  },
4043
4202
  optimizeDeps: {
4044
- entries: entries.files().map((file) => toPosix(path21.relative(paths.design, file)))
4203
+ entries: entries.files().map((file) => toPosix(path22.relative(paths.design, file)))
4045
4204
  }
4046
4205
  });
4047
4206
  const publicDir = project.publicDir();
@@ -4053,16 +4212,16 @@ async function startDevServer(options) {
4053
4212
  return null;
4054
4213
  }
4055
4214
  };
4056
- const viewer = fs22.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
4057
- const viewerIndex = path21.join(PKG.viewer, "index.html");
4215
+ const viewer = fs23.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
4216
+ const viewerIndex = path22.join(PKG.viewer, "index.html");
4058
4217
  const serveViewerIndex = (res) => {
4059
- const html = fs22.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4218
+ const html = fs23.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
4060
4219
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
4061
4220
  res.end(html);
4062
4221
  };
4063
4222
  const ctx = { project, events, snapshots };
4064
4223
  const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
4065
- const bootUrl = `/${relToDesign(paths, path21.join(entries.dir, HTML_BOOT))}`;
4224
+ const bootUrl = `/${relToDesign(paths, path22.join(entries.dir, HTML_BOOT))}`;
4066
4225
  const frameHead = (source, extra = {}) => {
4067
4226
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
4068
4227
  return {
@@ -4083,7 +4242,7 @@ async function startDevServer(options) {
4083
4242
  notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
4084
4243
  return;
4085
4244
  }
4086
- if (!fs22.existsSync(entries.entryFile(source))) entries.sync();
4245
+ if (!fs23.existsSync(entries.entryFile(source))) entries.sync();
4087
4246
  const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
4088
4247
  const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
4089
4248
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
@@ -4092,8 +4251,8 @@ async function startDevServer(options) {
4092
4251
  };
4093
4252
  const serveHtmlScreen = async (res, url2) => {
4094
4253
  const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
4095
- const file = path21.resolve(paths.design, rel);
4096
- if (!isInside(paths.design, file) || !fs22.existsSync(file)) {
4254
+ const file = path22.resolve(paths.design, rel);
4255
+ if (!isInside(paths.design, file) || !fs23.existsSync(file)) {
4097
4256
  notFoundPage(res, "Screen not found", `No file .design/${rel}`);
4098
4257
  return;
4099
4258
  }
@@ -4103,13 +4262,13 @@ async function startDevServer(options) {
4103
4262
  key: `${canvas}/${id2}`,
4104
4263
  canvas,
4105
4264
  id: id2,
4106
- title: path21.basename(file),
4265
+ title: path22.basename(file),
4107
4266
  file,
4108
4267
  format: "html",
4109
4268
  props: {},
4110
4269
  system: true
4111
4270
  };
4112
- const raw = fs22.readFileSync(file, "utf8");
4271
+ const raw = fs23.readFileSync(file, "utf8");
4113
4272
  const withSystem = source.system && !optsOutOfSystem(raw);
4114
4273
  const html = injectIntoHtml(
4115
4274
  raw,
@@ -4142,7 +4301,7 @@ async function startDevServer(options) {
4142
4301
  }
4143
4302
  if (url2.pathname.startsWith("/_fs/")) {
4144
4303
  const rel = publicRel(url2.pathname.slice("/_fs/".length));
4145
- if (publicDir && rel !== null && !project.isPublicFile(rel) && fs22.existsSync(path21.join(publicDir, rel))) {
4304
+ if (publicDir && rel !== null && !project.isPublicFile(rel) && fs23.existsSync(path22.join(publicDir, rel))) {
4146
4305
  notFoundPage(res, "Not found", url2.pathname);
4147
4306
  return;
4148
4307
  }
@@ -4154,7 +4313,7 @@ async function startDevServer(options) {
4154
4313
  const rel = publicRel(url2.pathname.slice(1));
4155
4314
  if (servePublic && rel !== null && project.isPublicFile(rel))
4156
4315
  servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
4157
- else if (path21.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4316
+ else if (path22.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
4158
4317
  else serveViewerIndex(res);
4159
4318
  };
4160
4319
  if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
@@ -4182,7 +4341,7 @@ async function startDevServer(options) {
4182
4341
  };
4183
4342
  vite.watcher.on("all", (event, file) => {
4184
4343
  if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
4185
- const rel = toPosix(path21.relative(paths.design, file));
4344
+ const rel = toPosix(path22.relative(paths.design, file));
4186
4345
  if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
4187
4346
  if (rel === "design.json") {
4188
4347
  queue("config", () => {
@@ -4290,15 +4449,15 @@ async function ensureServer(paths, options) {
4290
4449
  if (stale)
4291
4450
  print(dim(`Restarting the preview server: it runs foss-design ${live2.version}, this is ${packageVersion()}.`));
4292
4451
  if (live2) await stopServer(paths);
4293
- fs23.mkdirSync(paths.cache, { recursive: true });
4294
- const log = fs23.openSync(logFile(paths), "w");
4452
+ fs24.mkdirSync(paths.cache, { recursive: true });
4453
+ const log = fs24.openSync(logFile(paths), "w");
4295
4454
  const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
4296
4455
  if (options.port) args.push("--port", String(options.port));
4297
4456
  const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", log, log], cwd: paths.root });
4298
4457
  child.unref();
4299
4458
  const state = await waitForServer(paths, 4e4);
4300
4459
  if (!state) {
4301
- const tail = fs23.existsSync(logFile(paths)) ? fs23.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4460
+ const tail = fs24.existsSync(logFile(paths)) ? fs24.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
4302
4461
  throw new CliError(`The preview server did not start.
4303
4462
  ${tail.join("\n")}`);
4304
4463
  }
@@ -4477,9 +4636,9 @@ function printLinkIssues(issues, project) {
4477
4636
  }
4478
4637
 
4479
4638
  // src/cli/commands/cloud.ts
4480
- import fs24 from "fs";
4639
+ import fs25 from "fs";
4481
4640
  import os3 from "os";
4482
- import path22 from "path";
4641
+ import path23 from "path";
4483
4642
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4484
4643
  async function runLogin() {
4485
4644
  const host = cloudHost();
@@ -4580,7 +4739,7 @@ async function runLink(paths, wanted, options) {
4580
4739
  print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
4581
4740
  } else {
4582
4741
  writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
4583
- fs24.rmSync(path22.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
4742
+ fs25.rmSync(path23.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
4584
4743
  const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
4585
4744
  print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
4586
4745
  }
@@ -4604,13 +4763,41 @@ async function runLink(paths, wanted, options) {
4604
4763
  else if (remote) print(dim("Everything is in sync."));
4605
4764
  }
4606
4765
 
4607
- // src/cli/commands/import.ts
4766
+ // src/cli/commands/icon.ts
4608
4767
  import fs26 from "fs";
4609
4768
  import path24 from "path";
4769
+ function runIcon(paths, file, options) {
4770
+ if (options.remove) {
4771
+ const had = iconFile(paths);
4772
+ writeIcon(paths, null);
4773
+ print(had ? `${green("Removed")} ${path24.relative(process.cwd(), had)}` : "The project has no icon.");
4774
+ return;
4775
+ }
4776
+ if (!file) {
4777
+ const current = iconFile(paths);
4778
+ if (!current) return print("The project has no icon. Set one with `design icon <file.svg|png|webp>`.");
4779
+ return print(`${path24.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs26.statSync(current).size / 1024)} KB`)}`);
4780
+ }
4781
+ let data;
4782
+ try {
4783
+ data = fs26.readFileSync(file);
4784
+ } catch {
4785
+ throw new CliError(`No file ${file}`);
4786
+ }
4787
+ const problem = iconProblem(data, path24.extname(file).slice(1).toLowerCase() || void 0);
4788
+ if (problem) throw new CliError(`${file}: ${problem}`);
4789
+ const written = writeIcon(paths, data);
4790
+ print(`${green("Set")} the project icon: ${path24.relative(process.cwd(), written)}`);
4791
+ print(dim("It syncs with the design system unit: `design push` takes it to the cloud."));
4792
+ }
4793
+
4794
+ // src/cli/commands/import.ts
4795
+ import fs28 from "fs";
4796
+ import path26 from "path";
4610
4797
 
4611
4798
  // src/import/claude-design.ts
4612
- import fs25 from "fs";
4613
- import path23 from "path";
4799
+ import fs27 from "fs";
4800
+ import path25 from "path";
4614
4801
 
4615
4802
  // src/import/markup.ts
4616
4803
  var VOID = /* @__PURE__ */ new Set([
@@ -5218,11 +5405,11 @@ function boardId(file, used) {
5218
5405
  return id2;
5219
5406
  }
5220
5407
  function importClaudeDesign(dir, options = {}) {
5221
- const canvasFile = path23.join(dir, "canvas.json");
5222
- if (!fs25.existsSync(canvasFile))
5408
+ const canvasFile = path25.join(dir, "canvas.json");
5409
+ if (!fs27.existsSync(canvasFile))
5223
5410
  throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
5224
- const source = JSON.parse(fs25.readFileSync(canvasFile, "utf8"));
5225
- const templates = fs25.readdirSync(dir).filter((file) => file.endsWith(EXT));
5411
+ const source = JSON.parse(fs27.readFileSync(canvasFile, "utf8"));
5412
+ const templates = fs27.readdirSync(dir).filter((file) => file.endsWith(EXT));
5226
5413
  if (!templates.length) throw new Error(`No ${EXT} files in ${dir}`);
5227
5414
  const known = new Set(templates.map((file) => file.slice(0, -EXT.length)));
5228
5415
  const warnings = /* @__PURE__ */ new Map();
@@ -5249,7 +5436,7 @@ function importClaudeDesign(dir, options = {}) {
5249
5436
  try {
5250
5437
  files.set(
5251
5438
  `${name}.jsx`,
5252
- convertFile(name, fs25.readFileSync(path23.join(dir, file), "utf8"), { known, routes, warn: warn2 })
5439
+ convertFile(name, fs27.readFileSync(path25.join(dir, file), "utf8"), { known, routes, warn: warn2 })
5253
5440
  );
5254
5441
  } catch (error) {
5255
5442
  warn2(`could not be converted: ${error.message}`);
@@ -5297,7 +5484,7 @@ function importClaudeDesign(dir, options = {}) {
5297
5484
  screens++;
5298
5485
  }
5299
5486
  const canvas = {
5300
- title: options.title ?? source.title ?? path23.basename(path23.resolve(dir)),
5487
+ title: options.title ?? source.title ?? path25.basename(path25.resolve(dir)),
5301
5488
  description: "Imported from Claude Design.",
5302
5489
  // The templates bring their own styles; the design system's reset would restyle them.
5303
5490
  system: false,
@@ -5309,21 +5496,21 @@ function importClaudeDesign(dir, options = {}) {
5309
5496
  // src/cli/commands/import.ts
5310
5497
  var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
5311
5498
  function runImport(paths, from, options) {
5312
- const dir = path24.resolve(from);
5313
- if (!fs26.existsSync(dir) || !fs26.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
5499
+ const dir = path26.resolve(from);
5500
+ if (!fs28.existsSync(dir) || !fs28.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
5314
5501
  let result;
5315
5502
  try {
5316
5503
  result = importClaudeDesign(dir, { title: options.title });
5317
5504
  } catch (error) {
5318
5505
  throw new CliError(error.message);
5319
5506
  }
5320
- const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path24.basename(dir)) || "imported");
5507
+ const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path26.basename(dir)) || "imported");
5321
5508
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is no canvas id; pass one with --canvas <id>`);
5322
- const out = path24.join(paths.canvases, id2);
5323
- if (fs26.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
5324
- fs26.mkdirSync(path24.join(out, "screens"), { recursive: true });
5325
- for (const [name, content] of result.files) fs26.writeFileSync(path24.join(out, "screens", name), content);
5326
- fs26.writeFileSync(path24.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
5509
+ const out = path26.join(paths.canvases, id2);
5510
+ if (fs28.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
5511
+ fs28.mkdirSync(path26.join(out, "screens"), { recursive: true });
5512
+ for (const [name, content] of result.files) fs28.writeFileSync(path26.join(out, "screens", name), content);
5513
+ fs28.writeFileSync(path26.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
5327
5514
  `);
5328
5515
  print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
5329
5516
  print(
@@ -5342,8 +5529,8 @@ function runImport(paths, from, options) {
5342
5529
  }
5343
5530
 
5344
5531
  // src/cli/commands/scaffold.ts
5345
- import fs27 from "fs";
5346
- import path25 from "path";
5532
+ import fs29 from "fs";
5533
+ import path27 from "path";
5347
5534
 
5348
5535
  // src/cli/templates.ts
5349
5536
  var TOKENS_CSS = `/*
@@ -5554,89 +5741,91 @@ function mainScreen(canvasId) {
5554
5741
 
5555
5742
  // src/cli/commands/scaffold.ts
5556
5743
  function write(file, content, root) {
5557
- if (fs27.existsSync(file)) {
5558
- print(` ${dim("exists ")} ${path25.relative(root, file)}`);
5744
+ if (fs29.existsSync(file)) {
5745
+ print(` ${dim("exists ")} ${path27.relative(root, file)}`);
5559
5746
  return false;
5560
5747
  }
5561
- fs27.mkdirSync(path25.dirname(file), { recursive: true });
5562
- fs27.writeFileSync(file, content);
5563
- print(` ${green("created")} ${path25.relative(root, file)}`);
5748
+ fs29.mkdirSync(path27.dirname(file), { recursive: true });
5749
+ fs29.writeFileSync(file, content);
5750
+ print(` ${green("created")} ${path27.relative(root, file)}`);
5564
5751
  return true;
5565
5752
  }
5566
5753
  function ensureGitignored(root) {
5567
- const file = path25.join(root, ".gitignore");
5568
- const current = fs27.existsSync(file) ? fs27.readFileSync(file, "utf8") : "";
5754
+ const file = path27.join(root, ".gitignore");
5755
+ const current = fs29.existsSync(file) ? fs29.readFileSync(file, "utf8") : "";
5569
5756
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
5570
5757
  if (covered) return "present";
5571
5758
  const prefix = current && !current.endsWith("\n") ? "\n" : "";
5572
- fs27.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
5759
+ fs29.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
5573
5760
  .design/
5574
5761
  `);
5575
5762
  return "added";
5576
5763
  }
5577
5764
  function projectName(root) {
5578
5765
  try {
5579
- const manifest = JSON.parse(fs27.readFileSync(path25.join(root, "package.json"), "utf8"));
5766
+ const manifest = JSON.parse(fs29.readFileSync(path27.join(root, "package.json"), "utf8"));
5580
5767
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
5581
5768
  } catch {
5582
5769
  }
5583
- return path25.basename(root);
5770
+ return path27.basename(root);
5584
5771
  }
5585
5772
  function runInit(root, options) {
5586
5773
  const paths = designPaths(root);
5587
5774
  print(`Setting up ${DESIGN_DIR} in ${root}`);
5588
5775
  write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
5589
5776
  `, root);
5590
- fs27.mkdirSync(paths.canvases, { recursive: true });
5777
+ fs29.mkdirSync(paths.canvases, { recursive: true });
5591
5778
  if (options.gitignore) {
5592
5779
  const result = ensureGitignored(root);
5593
5780
  print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
5594
5781
  }
5595
5782
  print();
5596
- print("Next: `design system init` for a design system, `design new <canvas>` for a canvas, `design preview` to look.");
5783
+ print(
5784
+ "Next: `design system init` for a design system, `design new <canvas>` for a canvas, `design icon <file.svg>` for the project icon, `design preview` to look."
5785
+ );
5597
5786
  }
5598
5787
  function runSystemInit(root, options) {
5599
5788
  const paths = designPaths(root);
5600
5789
  let base = projectName(root);
5601
5790
  try {
5602
- base = JSON.parse(fs27.readFileSync(paths.config, "utf8")).name || base;
5791
+ base = JSON.parse(fs29.readFileSync(paths.config, "utf8")).name || base;
5603
5792
  } catch {
5604
5793
  }
5605
5794
  const name = options.name ?? `${base} design system`;
5606
- print(`Scaffolding the design system in ${path25.relative(root, paths.system)}`);
5607
- write(path25.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
5795
+ print(`Scaffolding the design system in ${path27.relative(root, paths.system)}`);
5796
+ write(path27.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
5608
5797
  `, root);
5609
- write(path25.join(paths.system, "tokens.css"), TOKENS_CSS, root);
5798
+ write(path27.join(paths.system, "tokens.css"), TOKENS_CSS, root);
5610
5799
  if (!options.empty) {
5611
- write(path25.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
5612
- write(path25.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
5613
- write(path25.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
5800
+ write(path27.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
5801
+ write(path27.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
5802
+ write(path27.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
5614
5803
  }
5615
5804
  }
5616
5805
  function runNew(root, id2, options) {
5617
5806
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
5618
5807
  const paths = designPaths(root);
5619
- const dir = path25.join(paths.canvases, id2);
5620
- if (fs27.existsSync(path25.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
5808
+ const dir = path27.join(paths.canvases, id2);
5809
+ if (fs29.existsSync(path27.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
5621
5810
  print(`Creating canvas "${id2}"`);
5622
5811
  const title = options.title ?? titleize(id2);
5623
5812
  if (options.empty) {
5624
5813
  write(
5625
- path25.join(dir, "canvas.json"),
5814
+ path27.join(dir, "canvas.json"),
5626
5815
  `${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
5627
5816
  `,
5628
5817
  root
5629
5818
  );
5630
- fs27.mkdirSync(path25.join(dir, "screens"), { recursive: true });
5819
+ fs29.mkdirSync(path27.join(dir, "screens"), { recursive: true });
5631
5820
  } else {
5632
- write(path25.join(dir, "canvas.json"), canvasJson(title), root);
5633
- write(path25.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
5821
+ write(path27.join(dir, "canvas.json"), canvasJson(title), root);
5822
+ write(path27.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
5634
5823
  }
5635
5824
  }
5636
5825
 
5637
5826
  // src/cli/commands/shot.ts
5638
- import fs28 from "fs";
5639
- import path26 from "path";
5827
+ import fs30 from "fs";
5828
+ import path28 from "path";
5640
5829
  var OVERVIEW_TIMEOUT = 9e4;
5641
5830
  async function runShot(paths, target, options) {
5642
5831
  const [canvasId = "", screenId] = target.split("/");
@@ -5652,15 +5841,15 @@ async function runShot(paths, target, options) {
5652
5841
  throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
5653
5842
  }
5654
5843
  const theme2 = options.theme ?? doc.theme ?? "light";
5655
- const outBase = path26.resolve(options.out ?? paths.shots);
5656
- const outDir = path26.join(outBase, canvasId);
5657
- fs28.mkdirSync(outDir, { recursive: true });
5844
+ const outBase = path28.resolve(options.out ?? paths.shots);
5845
+ const outDir = path28.join(outBase, canvasId);
5846
+ fs30.mkdirSync(outDir, { recursive: true });
5658
5847
  const base = await serverBase(paths);
5659
5848
  const browser = await launchChrome();
5660
5849
  let failed = false;
5661
5850
  try {
5662
5851
  if (options.overview) {
5663
- if (!fs28.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
5852
+ if (!fs30.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
5664
5853
  const pages = options.page ? [options.page] : screenId ? doc.pages.filter((page) => page.sections.some((section) => section.items.some((item) => item.id === screenId))).map((page) => page.id) : doc.pages.map((page) => page.id);
5665
5854
  for (const pageId of pages) {
5666
5855
  const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
@@ -5676,7 +5865,7 @@ async function runShot(paths, target, options) {
5676
5865
  warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
5677
5866
  }
5678
5867
  await page.waitForTimeout(400);
5679
- const file = path26.join(outDir, `${pageId}.overview.png`);
5868
+ const file = path28.join(outDir, `${pageId}.overview.png`);
5680
5869
  await page.screenshot({ path: file });
5681
5870
  await page.close();
5682
5871
  print(file);
@@ -5705,7 +5894,7 @@ async function shootFrames(browser, jobs, outDir) {
5705
5894
  let failed = false;
5706
5895
  const results = await mapLimit(jobs, 4, async (job) => {
5707
5896
  const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
5708
- const file = path26.join(outDir, `${job.id}.${job.theme}.png`);
5897
+ const file = path28.join(outDir, `${job.id}.${job.theme}.png`);
5709
5898
  try {
5710
5899
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
5711
5900
  await page.screenshot({ path: file, fullPage: job.fullPage });
@@ -5737,8 +5926,8 @@ async function shootSystem(paths, project, id2, options) {
5737
5926
  if (id2 && !all.some((job) => job.id === id2)) {
5738
5927
  throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
5739
5928
  }
5740
- const outDir = path26.join(path26.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
5741
- fs28.mkdirSync(outDir, { recursive: true });
5929
+ const outDir = path28.join(path28.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
5930
+ fs30.mkdirSync(outDir, { recursive: true });
5742
5931
  const base = await serverBase(paths);
5743
5932
  const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
5744
5933
  const browser = await launchChrome();
@@ -5750,7 +5939,7 @@ async function shootSystem(paths, project, id2, options) {
5750
5939
  }
5751
5940
 
5752
5941
  // src/cli/commands/cloud-status.ts
5753
- import path27 from "path";
5942
+ import path29 from "path";
5754
5943
  var STATE_TEXT = {
5755
5944
  in_sync: green,
5756
5945
  local_changes: yellow,
@@ -5773,7 +5962,7 @@ function detail(paths, status) {
5773
5962
  if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
5774
5963
  if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
5775
5964
  parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
5776
- if (status.conflictRev !== void 0) parts.push(`merge ${path27.relative(paths.root, incomingDir(paths, status.key))}`);
5965
+ if (status.conflictRev !== void 0) parts.push(`merge ${path29.relative(paths.root, incomingDir(paths, status.key))}`);
5777
5966
  return parts.join(" \xB7 ");
5778
5967
  }
5779
5968
  async function printCloudStatus(paths, label) {
@@ -5888,6 +6077,9 @@ ${bold("Project")}
5888
6077
  init [--name <name>] [--no-gitignore] Create .design and add it to .gitignore
5889
6078
  system init [--name <name>] [--empty] Scaffold .design/system: tokens, a guideline, a component
5890
6079
  new <canvas> [--title <title>] [--empty] Scaffold .design/canvas/<canvas>
6080
+ icon [<file>] [--remove] Show, set or remove the project icon: an SVG, PNG or WebP of
6081
+ at most 256 KB, kept as .design/system/icon.* and pushed
6082
+ with the design system
5891
6083
  import <folder> [--canvas <id>] Turn an exported Claude Design project (canvas.json and
5892
6084
  [--title <title>] *.dc.html) into a canvas of React screens, once
5893
6085
  canvases [--json] Every canvas: pages and screens; with a cloud link also its
@@ -5949,7 +6141,7 @@ ${bold("Environment")}
5949
6141
  FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
5950
6142
  `;
5951
6143
  function projectPaths(rootOption, requireDesign = true) {
5952
- const found = rootOption ? path28.resolve(rootOption) : findProjectRoot();
6144
+ const found = rootOption ? path30.resolve(rootOption) : findProjectRoot();
5953
6145
  if (!found) {
5954
6146
  if (!requireDesign) return designPaths(process.cwd());
5955
6147
  throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
@@ -5967,6 +6159,7 @@ async function main(argv) {
5967
6159
  name: { type: "string" },
5968
6160
  title: { type: "string" },
5969
6161
  canvas: { type: "string" },
6162
+ remove: { type: "boolean" },
5970
6163
  "no-gitignore": { type: "boolean" },
5971
6164
  empty: { type: "boolean" },
5972
6165
  open: { type: "boolean" },
@@ -5996,7 +6189,7 @@ async function main(argv) {
5996
6189
  if (values.theme && !theme2) throw new CliError("--theme is light or dark");
5997
6190
  switch (command) {
5998
6191
  case "init":
5999
- return runInit(values.root ? path28.resolve(values.root) : process.cwd(), {
6192
+ return runInit(values.root ? path30.resolve(values.root) : process.cwd(), {
6000
6193
  name: values.name,
6001
6194
  gitignore: !values["no-gitignore"]
6002
6195
  });
@@ -6008,6 +6201,8 @@ async function main(argv) {
6008
6201
  if (!rest[0]) throw new CliError("Usage: design new <canvas> [--title <title>]");
6009
6202
  return runNew(projectPaths(values.root).root, rest[0], { title: values.title, empty: !!values.empty });
6010
6203
  }
6204
+ case "icon":
6205
+ return runIcon(projectPaths(values.root), rest[0], { remove: !!values.remove });
6011
6206
  case "import": {
6012
6207
  if (!rest[0]) throw new CliError("Usage: design import <folder> [--canvas <id>] [--title <title>]");
6013
6208
  return runImport(projectPaths(values.root), rest[0], { canvas: values.canvas, title: values.title });