foss-design 0.1.0 → 0.3.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.
Files changed (133) hide show
  1. package/README.md +6 -3
  2. package/dist/cli.js +1700 -217
  3. package/dist/runtime/index.js +490 -3
  4. package/dist/viewer/assets/index-ChcZNlVM.css +2 -0
  5. package/dist/viewer/assets/index-CzXF5TnZ.js +157 -0
  6. package/dist/viewer/index.html +2 -2
  7. package/dist/viewer-lib/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  8. package/dist/viewer-lib/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  9. package/dist/viewer-lib/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  10. package/dist/viewer-lib/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  11. package/dist/viewer-lib/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  12. package/dist/viewer-lib/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  13. package/dist/viewer-lib/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  14. package/dist/viewer-lib/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  15. package/dist/viewer-lib/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  16. package/dist/viewer-lib/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  17. package/dist/viewer-lib/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  18. package/dist/viewer-lib/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  19. package/dist/viewer-lib/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  20. package/dist/viewer-lib/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  21. package/dist/viewer-lib/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  22. package/dist/viewer-lib/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  23. package/dist/viewer-lib/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  24. package/dist/viewer-lib/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  25. package/dist/viewer-lib/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  26. package/dist/viewer-lib/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  27. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  28. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  29. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
  30. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
  31. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  32. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  33. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  34. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  35. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
  36. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
  37. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  38. package/dist/viewer-lib/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  39. package/dist/viewer-lib/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  40. package/dist/viewer-lib/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  41. package/dist/viewer-lib/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
  42. package/dist/viewer-lib/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
  43. package/dist/viewer-lib/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  44. package/dist/viewer-lib/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  45. package/dist/viewer-lib/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  46. package/dist/viewer-lib/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  47. package/dist/viewer-lib/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  48. package/dist/viewer-lib/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  49. package/dist/viewer-lib/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  50. package/dist/viewer-lib/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  51. package/dist/viewer-lib/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  52. package/dist/viewer-lib/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  53. package/dist/viewer-lib/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
  54. package/dist/viewer-lib/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
  55. package/dist/viewer-lib/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  56. package/dist/viewer-lib/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  57. package/dist/viewer-lib/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  58. package/dist/viewer-lib/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  59. package/dist/viewer-lib/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
  60. package/dist/viewer-lib/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
  61. package/dist/viewer-lib/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  62. package/dist/viewer-lib/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  63. package/dist/viewer-lib/index.d.ts +1 -0
  64. package/dist/viewer-lib/index.js +17581 -0
  65. package/dist/viewer-lib/styles.css +3090 -0
  66. package/dist/viewer-lib/types/src/shared/devices.d.ts +46 -0
  67. package/dist/viewer-lib/types/src/shared/types.d.ts +355 -0
  68. package/dist/viewer-lib/types/viewer/src/app.d.ts +20 -0
  69. package/dist/viewer-lib/types/viewer/src/components/auto-frame.d.ts +13 -0
  70. package/dist/viewer-lib/types/viewer/src/components/code-view.d.ts +6 -0
  71. package/dist/viewer-lib/types/viewer/src/components/command-palette.d.ts +2 -0
  72. package/dist/viewer-lib/types/viewer/src/components/inspector.d.ts +29 -0
  73. package/dist/viewer-lib/types/viewer/src/components/issues.d.ts +10 -0
  74. package/dist/viewer-lib/types/viewer/src/components/markdown.d.ts +8 -0
  75. package/dist/viewer-lib/types/viewer/src/components/search-button.d.ts +6 -0
  76. package/dist/viewer-lib/types/viewer/src/components/slide-panel.d.ts +11 -0
  77. package/dist/viewer-lib/types/viewer/src/components/theme-menu.d.ts +1 -0
  78. package/dist/viewer-lib/types/viewer/src/components/topbar.d.ts +8 -0
  79. package/dist/viewer-lib/types/viewer/src/components/wordmark.d.ts +7 -0
  80. package/dist/viewer-lib/types/viewer/src/lib/api.d.ts +14 -0
  81. package/dist/viewer-lib/types/viewer/src/lib/cn.d.ts +1 -0
  82. package/dist/viewer-lib/types/viewer/src/lib/copy.d.ts +1 -0
  83. package/dist/viewer-lib/types/viewer/src/lib/focus.d.ts +2 -0
  84. package/dist/viewer-lib/types/viewer/src/lib/format.d.ts +3 -0
  85. package/dist/viewer-lib/types/viewer/src/lib/frames.d.ts +25 -0
  86. package/dist/viewer-lib/types/viewer/src/lib/platform.d.ts +3 -0
  87. package/dist/viewer-lib/types/viewer/src/lib/source.d.ts +43 -0
  88. package/dist/viewer-lib/types/viewer/src/lib/store.d.ts +10 -0
  89. package/dist/viewer-lib/types/viewer/src/lib/theme.d.ts +6 -0
  90. package/dist/viewer-lib/types/viewer/src/lib/viewer.d.ts +44 -0
  91. package/dist/viewer-lib/types/viewer/src/library.d.ts +25 -0
  92. package/dist/viewer-lib/types/viewer/src/pages/canvas/camera.d.ts +43 -0
  93. package/dist/viewer-lib/types/viewer/src/pages/canvas/canvas-bar.d.ts +29 -0
  94. package/dist/viewer-lib/types/viewer/src/pages/canvas/canvas-page.d.ts +4 -0
  95. package/dist/viewer-lib/types/viewer/src/pages/canvas/items.d.ts +47 -0
  96. package/dist/viewer-lib/types/viewer/src/pages/canvas/layout.d.ts +30 -0
  97. package/dist/viewer-lib/types/viewer/src/pages/canvas/overlay.d.ts +19 -0
  98. package/dist/viewer-lib/types/viewer/src/pages/canvas/play-page.d.ts +4 -0
  99. package/dist/viewer-lib/types/viewer/src/pages/canvas/sidebar.d.ts +10 -0
  100. package/dist/viewer-lib/types/viewer/src/pages/canvas/view-state.d.ts +30 -0
  101. package/dist/viewer-lib/types/viewer/src/pages/canvas/viewport.d.ts +37 -0
  102. package/dist/viewer-lib/types/viewer/src/pages/home.d.ts +1 -0
  103. package/dist/viewer-lib/types/viewer/src/pages/not-found.d.ts +4 -0
  104. package/dist/viewer-lib/types/viewer/src/pages/system/assets.d.ts +4 -0
  105. package/dist/viewer-lib/types/viewer/src/pages/system/colors.d.ts +4 -0
  106. package/dist/viewer-lib/types/viewer/src/pages/system/component-page.d.ts +5 -0
  107. package/dist/viewer-lib/types/viewer/src/pages/system/guideline.d.ts +5 -0
  108. package/dist/viewer-lib/types/viewer/src/pages/system/motion.d.ts +7 -0
  109. package/dist/viewer-lib/types/viewer/src/pages/system/nav.d.ts +5 -0
  110. package/dist/viewer-lib/types/viewer/src/pages/system/other-tokens.d.ts +4 -0
  111. package/dist/viewer-lib/types/viewer/src/pages/system/overview.d.ts +4 -0
  112. package/dist/viewer-lib/types/viewer/src/pages/system/shape.d.ts +4 -0
  113. package/dist/viewer-lib/types/viewer/src/pages/system/shared.d.ts +34 -0
  114. package/dist/viewer-lib/types/viewer/src/pages/system/system-routes.d.ts +2 -0
  115. package/dist/viewer-lib/types/viewer/src/pages/system/typography.d.ts +4 -0
  116. package/dist/viewer-lib/types/viewer/src/ui/avatar.d.ts +14 -0
  117. package/dist/viewer-lib/types/viewer/src/ui/badge.d.ts +11 -0
  118. package/dist/viewer-lib/types/viewer/src/ui/button.d.ts +37 -0
  119. package/dist/viewer-lib/types/viewer/src/ui/choice.d.ts +19 -0
  120. package/dist/viewer-lib/types/viewer/src/ui/dialog.d.ts +18 -0
  121. package/dist/viewer-lib/types/viewer/src/ui/icon-paths.d.ts +87 -0
  122. package/dist/viewer-lib/types/viewer/src/ui/icon.d.ts +8 -0
  123. package/dist/viewer-lib/types/viewer/src/ui/menu.d.ts +27 -0
  124. package/dist/viewer-lib/types/viewer/src/ui/meter.d.ts +15 -0
  125. package/dist/viewer-lib/types/viewer/src/ui/page.d.ts +39 -0
  126. package/dist/viewer-lib/types/viewer/src/ui/panel.d.ts +72 -0
  127. package/dist/viewer-lib/types/viewer/src/ui/text-field.d.ts +19 -0
  128. package/dist/viewer-lib/types/viewer/src/ui/text.d.ts +22 -0
  129. package/dist/viewer-lib/types/viewer/src/ui/toast.d.ts +5 -0
  130. package/dist/viewer-lib/types/viewer/src/ui/tooltip.d.ts +13 -0
  131. package/package.json +20 -2
  132. package/dist/viewer/assets/index-Blh90C72.css +0 -2
  133. package/dist/viewer/assets/index-CN6gdSK-.js +0 -166
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 path18 from "path";
4
+ import path25 from "path";
5
5
  import { parseArgs } from "util";
6
6
 
7
7
  // src/core/paths.ts
@@ -14,8 +14,8 @@ import { fileURLToPath } from "url";
14
14
  import path from "path";
15
15
  function titleize(stem) {
16
16
  const words = stem.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_.]+/g, " ").trim().split(/\s+/);
17
- const text = words.join(" ").toLowerCase();
18
- return text.charAt(0).toUpperCase() + text.slice(1);
17
+ const text2 = words.join(" ").toLowerCase();
18
+ return text2.charAt(0).toUpperCase() + text2.slice(1);
19
19
  }
20
20
  function slugify(stem) {
21
21
  return stem.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase() || "item";
@@ -64,8 +64,8 @@ function firstHeading(markdown) {
64
64
  const match = /^#\s+(.+)$/m.exec(markdown);
65
65
  return match ? match[1].trim() : null;
66
66
  }
67
- function escapeHtml(text) {
68
- return text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
67
+ function escapeHtml(text2) {
68
+ return text2.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
69
69
  }
70
70
  function scriptJson(value) {
71
71
  return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
@@ -134,21 +134,185 @@ function resolveFrom(anchor, request) {
134
134
 
135
135
  // src/cli/commands/build.ts
136
136
  import { spawnSync } from "child_process";
137
- import fs9 from "fs";
137
+ import fs10 from "fs";
138
138
  import path11 from "path";
139
139
 
140
140
  // src/build/static.ts
141
- import fs8 from "fs";
141
+ import fs9 from "fs";
142
142
  import path10 from "path";
143
143
  import { build } from "vite";
144
144
 
145
+ // src/core/png.ts
146
+ import fs2 from "fs";
147
+ import zlib from "zlib";
148
+ var SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
149
+ var THUMB_WIDTH = 320;
150
+ function decode(png) {
151
+ if (!png.subarray(0, 8).equals(SIGNATURE)) return null;
152
+ let width = 0;
153
+ let height = 0;
154
+ let channels = 0;
155
+ const idat = [];
156
+ for (let pos = 8; pos + 8 <= png.length; ) {
157
+ const length = png.readUInt32BE(pos);
158
+ const type = png.toString("ascii", pos + 4, pos + 8);
159
+ const body = png.subarray(pos + 8, pos + 8 + length);
160
+ pos += 12 + length;
161
+ if (type === "IHDR") {
162
+ width = body.readUInt32BE(0);
163
+ height = body.readUInt32BE(4);
164
+ const [depth, color2, , , interlace] = [body[8], body[9], body[10], body[11], body[12]];
165
+ if (depth !== 8 || interlace !== 0 || color2 !== 2 && color2 !== 6) return null;
166
+ channels = color2 === 6 ? 4 : 3;
167
+ } else if (type === "IDAT") idat.push(body);
168
+ else if (type === "IEND") break;
169
+ }
170
+ if (!width || !height || !channels) return null;
171
+ const raw = zlib.inflateSync(Buffer.concat(idat));
172
+ const stride = width * channels;
173
+ const data = new Uint8Array(width * height * 4);
174
+ let prev = new Uint8Array(stride);
175
+ let line = new Uint8Array(stride);
176
+ for (let y = 0, at = 0; y < height; y++) {
177
+ const filter = raw[at++];
178
+ for (let x = 0; x < stride; x++, at++) {
179
+ const left = x >= channels ? line[x - channels] : 0;
180
+ const up = prev[x];
181
+ const corner = x >= channels ? prev[x - channels] : 0;
182
+ let value = raw[at];
183
+ if (filter === 1) value += left;
184
+ else if (filter === 2) value += up;
185
+ else if (filter === 3) value += left + up >> 1;
186
+ else if (filter === 4) {
187
+ const p = left + up - corner;
188
+ const pa = Math.abs(p - left);
189
+ const pb = Math.abs(p - up);
190
+ const pc = Math.abs(p - corner);
191
+ value += pa <= pb && pa <= pc ? left : pb <= pc ? up : corner;
192
+ }
193
+ line[x] = value & 255;
194
+ }
195
+ for (let x = 0; x < width; x++) {
196
+ const to = (y * width + x) * 4;
197
+ const from = x * channels;
198
+ data[to] = line[from];
199
+ data[to + 1] = line[from + 1];
200
+ data[to + 2] = line[from + 2];
201
+ data[to + 3] = channels === 4 ? line[from + 3] : 255;
202
+ }
203
+ ;
204
+ [prev, line] = [line, prev];
205
+ }
206
+ return { width, height, data };
207
+ }
208
+ function shrink(image, width) {
209
+ const height = Math.max(1, Math.round(image.height * width / image.width));
210
+ const data = new Uint8Array(width * height * 4);
211
+ const sx = image.width / width;
212
+ const sy = image.height / height;
213
+ for (let y = 0; y < height; y++) {
214
+ const y0 = Math.floor(y * sy);
215
+ const y1 = Math.max(y0 + 1, Math.floor((y + 1) * sy));
216
+ for (let x = 0; x < width; x++) {
217
+ const x0 = Math.floor(x * sx);
218
+ const x1 = Math.max(x0 + 1, Math.floor((x + 1) * sx));
219
+ let r = 0;
220
+ let g = 0;
221
+ let b = 0;
222
+ let a = 0;
223
+ for (let yy = y0; yy < y1; yy++) {
224
+ for (let xx = x0; xx < x1; xx++) {
225
+ const at = (yy * image.width + xx) * 4;
226
+ r += image.data[at];
227
+ g += image.data[at + 1];
228
+ b += image.data[at + 2];
229
+ a += image.data[at + 3];
230
+ }
231
+ }
232
+ const n = (y1 - y0) * (x1 - x0);
233
+ const to = (y * width + x) * 4;
234
+ data[to] = Math.round(r / n);
235
+ data[to + 1] = Math.round(g / n);
236
+ data[to + 2] = Math.round(b / n);
237
+ data[to + 3] = Math.round(a / n);
238
+ }
239
+ }
240
+ return { width, height, data };
241
+ }
242
+ var CRC_TABLE = (() => {
243
+ const table = new Uint32Array(256);
244
+ for (let n = 0; n < 256; n++) {
245
+ let c = n;
246
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
247
+ table[n] = c >>> 0;
248
+ }
249
+ return table;
250
+ })();
251
+ function crc32(buffer) {
252
+ let crc = 4294967295;
253
+ for (const byte of buffer) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
254
+ return (crc ^ 4294967295) >>> 0;
255
+ }
256
+ function chunk(type, body) {
257
+ const head = Buffer.alloc(8);
258
+ head.writeUInt32BE(body.length, 0);
259
+ head.write(type, 4, "ascii");
260
+ const crc = Buffer.alloc(4);
261
+ crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), body])), 0);
262
+ return Buffer.concat([head, body, crc]);
263
+ }
264
+ function encode(image) {
265
+ const ihdr = Buffer.alloc(13);
266
+ ihdr.writeUInt32BE(image.width, 0);
267
+ ihdr.writeUInt32BE(image.height, 4);
268
+ ihdr[8] = 8;
269
+ ihdr[9] = 6;
270
+ const stride = image.width * 4;
271
+ const raw = Buffer.alloc((stride + 1) * image.height);
272
+ for (let y = 0; y < image.height; y++) {
273
+ const row = y * (stride + 1);
274
+ raw[row] = 1;
275
+ for (let x = 0; x < stride; x++) {
276
+ const value = image.data[y * stride + x];
277
+ const left = x >= 4 ? image.data[y * stride + x - 4] : 0;
278
+ raw[row + 1 + x] = value - left & 255;
279
+ }
280
+ }
281
+ return Buffer.concat([
282
+ SIGNATURE,
283
+ chunk("IHDR", ihdr),
284
+ chunk("IDAT", zlib.deflateSync(raw, { level: 9 })),
285
+ chunk("IEND", Buffer.alloc(0))
286
+ ]);
287
+ }
288
+ function thumbnail(png, width = THUMB_WIDTH) {
289
+ const image = decode(png);
290
+ if (!image || image.width <= width) return null;
291
+ return encode(shrink(image, width));
292
+ }
293
+ var thumbPath = (file) => file.replace(/\.png$/, ".thumb.png");
294
+ function ensureThumb(file) {
295
+ const thumb = thumbPath(file);
296
+ try {
297
+ const source = fs2.statSync(file);
298
+ const existing = fs2.statSync(thumb, { throwIfNoEntry: false });
299
+ if (existing && existing.mtimeMs >= source.mtimeMs) return thumb;
300
+ const small = thumbnail(fs2.readFileSync(file));
301
+ if (!small) return file;
302
+ fs2.writeFileSync(thumb, small);
303
+ return thumb;
304
+ } catch {
305
+ return file;
306
+ }
307
+ }
308
+
145
309
  // src/core/project.ts
146
- import fs5 from "fs";
310
+ import fs6 from "fs";
147
311
  import path6 from "path";
148
312
 
149
313
  // src/core/canvas.ts
150
314
  import { createHash } from "crypto";
151
- import fs2 from "fs";
315
+ import fs3 from "fs";
152
316
  import path3 from "path";
153
317
 
154
318
  // src/shared/devices.ts
@@ -264,9 +428,9 @@ var CanvasSchema = z.strictObject({
264
428
  pages: z.array(z.unknown()).min(1, "add at least one page")
265
429
  });
266
430
  var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
267
- function formatPath(base, path19) {
431
+ function formatPath(base, path26) {
268
432
  let out = base;
269
- for (const part of path19) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
433
+ for (const part of path26) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
270
434
  return out;
271
435
  }
272
436
  function describeZodError(error, base = "") {
@@ -277,8 +441,8 @@ function describeZodError(error, base = "") {
277
441
  var MODULE_EXT = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".mjs"]);
278
442
  var HTML_EXT = /* @__PURE__ */ new Set([".html", ".htm"]);
279
443
  function listCanvasIds(paths) {
280
- if (!fs2.existsSync(paths.canvases)) return [];
281
- return fs2.readdirSync(paths.canvases, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
444
+ if (!fs3.existsSync(paths.canvases)) return [];
445
+ return fs3.readdirSync(paths.canvases, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
282
446
  }
283
447
  function frameOf(input) {
284
448
  const custom = input.width !== void 0 || input.height !== void 0;
@@ -300,8 +464,8 @@ function loadCanvas(paths, canvasId, urls, snapshots) {
300
464
  }
301
465
  let source;
302
466
  try {
303
- source = fs2.readFileSync(file, "utf8");
304
- doc.updatedAt = fs2.statSync(file).mtimeMs;
467
+ source = fs3.readFileSync(file, "utf8");
468
+ doc.updatedAt = fs3.statSync(file).mtimeMs;
305
469
  } catch {
306
470
  report("error", "canvas.json is missing");
307
471
  return { doc, screens };
@@ -356,7 +520,7 @@ function loadCanvas(paths, canvasId, urls, snapshots) {
356
520
  return null;
357
521
  }
358
522
  if (!isInside(paths.design, abs2)) report("warning", `"${item.src}" is outside .design`, `${at}.src`);
359
- const missing2 = !fs2.existsSync(abs2);
523
+ const missing2 = !fs3.existsSync(abs2);
360
524
  if (missing2) report("error", `file not found: ${item.src}`, `${at}.src`);
361
525
  const id3 = claimId(item.id ?? slugify(fileStem(item.src)), item.id !== void 0, at);
362
526
  const title = item.title ?? titleize(fileStem(item.src));
@@ -393,6 +557,7 @@ function loadCanvas(paths, canvasId, urls, snapshots) {
393
557
  missing: missing2 || void 0,
394
558
  rev,
395
559
  snapshots: snap?.urls,
560
+ thumbs: snap?.thumbs,
396
561
  measuredHeight: snap?.height
397
562
  };
398
563
  }
@@ -424,7 +589,7 @@ function loadCanvas(paths, canvasId, urls, snapshots) {
424
589
  }
425
590
  const remote = /^https?:\/\//.test(item.src);
426
591
  const abs = remote ? "" : path3.resolve(dir, item.src);
427
- const missing = !remote && !fs2.existsSync(abs);
592
+ const missing = !remote && !fs3.existsSync(abs);
428
593
  if (missing) report("error", `image not found: ${item.src}`, `${at}.src`);
429
594
  const id2 = claimId(item.id ?? slugify(fileStem(item.src)), item.id !== void 0, at);
430
595
  return {
@@ -518,11 +683,11 @@ function summarize(canvas) {
518
683
  }
519
684
 
520
685
  // src/core/system.ts
521
- import fs4 from "fs";
686
+ import fs5 from "fs";
522
687
  import path5 from "path";
523
688
 
524
689
  // src/core/tokens.ts
525
- import fs3 from "fs";
690
+ import fs4 from "fs";
526
691
  import path4 from "path";
527
692
  import postcss from "postcss";
528
693
  var NAMESPACES = [
@@ -643,9 +808,9 @@ function collect(css2, from) {
643
808
  const nodes = container.nodes ?? [];
644
809
  for (let i = 0; i < nodes.length; i++) {
645
810
  const node = nodes[i];
646
- const text = commentText(node);
647
- if (text !== null) {
648
- const marker = /^@group\s+(.+)$/.exec(text);
811
+ const text2 = commentText(node);
812
+ if (text2 !== null) {
813
+ const marker = /^@group\s+(.+)$/.exec(text2);
649
814
  if (marker) {
650
815
  group = marker[1].trim();
651
816
  pending = void 0;
@@ -654,10 +819,10 @@ function collect(css2, from) {
654
819
  const prev = nodes[i - 1];
655
820
  if (prev?.type === "decl" && !/\n/.test(node.raws.before ?? "")) {
656
821
  const last = out[out.length - 1];
657
- if (last && last.name === prev.prop && !last.description) last.description = text;
822
+ if (last && last.name === prev.prop && !last.description) last.description = text2;
658
823
  continue;
659
824
  }
660
- pending = text;
825
+ pending = text2;
661
826
  continue;
662
827
  }
663
828
  if (node.type === "decl") {
@@ -681,24 +846,24 @@ function collect(css2, from) {
681
846
  function readStylesheets(entry) {
682
847
  const seen = /* @__PURE__ */ new Set();
683
848
  const files = [];
684
- const walk = (file) => {
685
- if (seen.has(file) || !fs3.existsSync(file)) return;
849
+ const walk2 = (file) => {
850
+ if (seen.has(file) || !fs4.existsSync(file)) return;
686
851
  seen.add(file);
687
- const css2 = fs3.readFileSync(file, "utf8");
852
+ const css2 = fs4.readFileSync(file, "utf8");
688
853
  const imports = [...css2.matchAll(/@import\s+(?:url\()?\s*["']([^"']+)["']/g)].map((m) => m[1]);
689
854
  for (const spec of imports) {
690
- if (spec.startsWith("./") || spec.startsWith("../")) walk(path4.resolve(path4.dirname(file), spec));
855
+ if (spec.startsWith("./") || spec.startsWith("../")) walk2(path4.resolve(path4.dirname(file), spec));
691
856
  }
692
857
  files.push({ file, css: css2 });
693
858
  };
694
- walk(entry);
859
+ walk2(entry);
695
860
  return files;
696
861
  }
697
- function closingParen(text, start) {
862
+ function closingParen(text2, start) {
698
863
  let depth = 1;
699
- for (let i = start; i < text.length; i++) {
700
- if (text[i] === "(") depth++;
701
- else if (text[i] === ")" && --depth === 0) return i;
864
+ for (let i = start; i < text2.length; i++) {
865
+ if (text2[i] === "(") depth++;
866
+ else if (text2[i] === ")" && --depth === 0) return i;
702
867
  }
703
868
  return -1;
704
869
  }
@@ -823,9 +988,9 @@ var COMPONENT_EXT = [".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".html"];
823
988
  var IMAGE_EXT = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico"]);
824
989
  var FONT_EXT = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf"]);
825
990
  function listFiles(dir, deep = false) {
826
- if (!fs4.existsSync(dir)) return [];
991
+ if (!fs5.existsSync(dir)) return [];
827
992
  const out = [];
828
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
993
+ for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
829
994
  if (entry.name.startsWith(".")) continue;
830
995
  const full = path5.join(dir, entry.name);
831
996
  if (entry.isDirectory()) {
@@ -835,13 +1000,13 @@ function listFiles(dir, deep = false) {
835
1000
  return out.sort();
836
1001
  }
837
1002
  function guessSources(componentsDir, stem) {
838
- if (!fs4.existsSync(componentsDir)) return [];
1003
+ if (!fs5.existsSync(componentsDir)) return [];
839
1004
  const wanted = slugify(stem);
840
1005
  for (const file of listFiles(componentsDir)) {
841
1006
  const ext = path5.extname(file);
842
1007
  if (COMPONENT_EXT.includes(ext) && slugify(path5.basename(file, ext)) === wanted) return [file];
843
1008
  }
844
- for (const entry of fs4.readdirSync(componentsDir, { withFileTypes: true })) {
1009
+ for (const entry of fs5.readdirSync(componentsDir, { withFileTypes: true })) {
845
1010
  if (entry.isDirectory() && slugify(entry.name) === wanted) {
846
1011
  const files = listFiles(path5.join(componentsDir, entry.name)).filter(
847
1012
  (f) => COMPONENT_EXT.includes(path5.extname(f))
@@ -852,14 +1017,14 @@ function guessSources(componentsDir, stem) {
852
1017
  return [];
853
1018
  }
854
1019
  function loadSystem(paths, urls) {
855
- if (!fs4.existsSync(paths.system)) return null;
1020
+ if (!fs5.existsSync(paths.system)) return null;
856
1021
  const issues = [];
857
1022
  const configFile = path5.join(paths.system, "system.json");
858
1023
  const report = (severity, file, message, at) => issues.push({ severity, file: relToRoot(paths, file), message, at });
859
1024
  let config = null;
860
- if (fs4.existsSync(configFile)) {
1025
+ if (fs5.existsSync(configFile)) {
861
1026
  try {
862
- const parsed = SystemConfigSchema.safeParse(JSON.parse(fs4.readFileSync(configFile, "utf8")));
1027
+ const parsed = SystemConfigSchema.safeParse(JSON.parse(fs5.readFileSync(configFile, "utf8")));
863
1028
  if (parsed.success) config = parsed.data;
864
1029
  else for (const issue of describeZodError(parsed.error)) report("error", configFile, issue.message, issue.at);
865
1030
  } catch (error) {
@@ -872,12 +1037,12 @@ function loadSystem(paths, urls) {
872
1037
  let stylesheet = null;
873
1038
  if (config?.stylesheet) {
874
1039
  stylesheet = path5.resolve(paths.system, config.stylesheet);
875
- if (!fs4.existsSync(stylesheet)) {
1040
+ if (!fs5.existsSync(stylesheet)) {
876
1041
  report("error", configFile, `stylesheet not found: ${config.stylesheet}`, "stylesheet");
877
1042
  stylesheet = null;
878
1043
  }
879
1044
  }
880
- const tokenEntry = stylesheet ?? (fs4.existsSync(tokensFile) ? tokensFile : null);
1045
+ const tokenEntry = stylesheet ?? (fs5.existsSync(tokensFile) ? tokensFile : null);
881
1046
  const tokens = tokenEntry ? parseTokens(readStylesheets(tokenEntry)) : [];
882
1047
  if (!tokenEntry) report("warning", tokensFile, "no tokens.css: screens get the default Tailwind theme");
883
1048
  const specimensDir = path5.join(paths.system, "specimens");
@@ -892,12 +1057,12 @@ function loadSystem(paths, urls) {
892
1057
  let id2 = slugify(stem);
893
1058
  if (usedIds.has(id2)) id2 = `${id2}-${usedIds.size + 1}`;
894
1059
  usedIds.add(id2);
895
- const tags = parseDocTags(fs4.readFileSync(file, "utf8"));
1060
+ const tags = parseDocTags(fs5.readFileSync(file, "utf8"));
896
1061
  const sources = (tags.source ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean).map((value) => path5.resolve(path5.dirname(file), value));
897
1062
  for (const source of sources) {
898
- if (!fs4.existsSync(source)) report("warning", file, `@source not found: ${path5.relative(paths.system, source)}`);
1063
+ if (!fs5.existsSync(source)) report("warning", file, `@source not found: ${path5.relative(paths.system, source)}`);
899
1064
  }
900
- const found = sources.filter((source) => fs4.existsSync(source));
1065
+ const found = sources.filter((source) => fs5.existsSync(source));
901
1066
  const title = tags.title?.[0] || titleize(stem);
902
1067
  const format = ext === ".html" ? "html" : "module";
903
1068
  components.push({
@@ -926,7 +1091,7 @@ function loadSystem(paths, urls) {
926
1091
  for (const file of listFiles(path5.join(paths.system, "guidelines"))) {
927
1092
  if (path5.extname(file).toLowerCase() !== ".md") continue;
928
1093
  const stem = path5.basename(file, ".md");
929
- const { data, body } = parseFrontmatter(fs4.readFileSync(file, "utf8"));
1094
+ const { data, body } = parseFrontmatter(fs5.readFileSync(file, "utf8"));
930
1095
  const prefix = /^(\d+)[-_. ]+(.*)$/.exec(stem);
931
1096
  const order = data.order !== void 0 ? Number(data.order) : prefix ? Number(prefix[1]) : 1e3;
932
1097
  guidelines.push({
@@ -945,7 +1110,7 @@ function loadSystem(paths, urls) {
945
1110
  name: path5.basename(file),
946
1111
  path: path5.relative(paths.system, file).split(path5.sep).join("/"),
947
1112
  url: urls.file(relToDesign(paths, file)),
948
- size: fs4.statSync(file).size,
1113
+ size: fs5.statSync(file).size,
949
1114
  kind: IMAGE_EXT.has(ext) ? "image" : FONT_EXT.has(ext) ? "font" : "other"
950
1115
  });
951
1116
  }
@@ -981,7 +1146,7 @@ function loadSystem(paths, urls) {
981
1146
  },
982
1147
  config,
983
1148
  stylesheet,
984
- tokensFile: fs4.existsSync(tokensFile) ? tokensFile : null,
1149
+ tokensFile: fs5.existsSync(tokensFile) ? tokensFile : null,
985
1150
  specimens
986
1151
  };
987
1152
  }
@@ -1014,9 +1179,9 @@ var DesignProject = class {
1014
1179
  const issues = [];
1015
1180
  let config = {};
1016
1181
  const file = this.paths.config;
1017
- if (fs5.existsSync(file)) {
1182
+ if (fs6.existsSync(file)) {
1018
1183
  try {
1019
- const parsed = DesignConfigSchema.safeParse(JSON.parse(fs5.readFileSync(file, "utf8")));
1184
+ const parsed = DesignConfigSchema.safeParse(JSON.parse(fs6.readFileSync(file, "utf8")));
1020
1185
  if (parsed.success) config = parsed.data;
1021
1186
  else
1022
1187
  for (const issue of describeZodError(parsed.error))
@@ -1036,7 +1201,7 @@ var DesignProject = class {
1036
1201
  const configured = this.config().config.name;
1037
1202
  if (configured) return configured;
1038
1203
  try {
1039
- const manifest = JSON.parse(fs5.readFileSync(path6.join(this.paths.root, "package.json"), "utf8"));
1204
+ const manifest = JSON.parse(fs6.readFileSync(path6.join(this.paths.root, "package.json"), "utf8"));
1040
1205
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
1041
1206
  } catch {
1042
1207
  }
@@ -1052,7 +1217,7 @@ var DesignProject = class {
1052
1217
  canvas(id2) {
1053
1218
  const cached = this.canvasCache.get(id2);
1054
1219
  if (cached) return cached;
1055
- if (!fs5.existsSync(path6.join(this.paths.canvases, id2))) return null;
1220
+ if (!fs6.existsSync(path6.join(this.paths.canvases, id2))) return null;
1056
1221
  const canvas = loadCanvas(this.paths, id2, this.urls, this.snapshots);
1057
1222
  this.canvasCache.set(id2, canvas);
1058
1223
  return canvas;
@@ -1119,17 +1284,17 @@ var STATIC_URLS = {
1119
1284
  };
1120
1285
 
1121
1286
  // src/server/entries.ts
1122
- import fs6 from "fs";
1287
+ import fs7 from "fs";
1123
1288
  import path7 from "path";
1124
1289
  var SYSTEM_CSS = "system.css";
1125
1290
  var HTML_BOOT = "html-boot.js";
1126
1291
  function writeIfChanged(file, content) {
1127
1292
  try {
1128
- if (fs6.readFileSync(file, "utf8") === content) return false;
1293
+ if (fs7.readFileSync(file, "utf8") === content) return false;
1129
1294
  } catch {
1130
1295
  }
1131
- fs6.mkdirSync(path7.dirname(file), { recursive: true });
1132
- fs6.writeFileSync(file, content);
1296
+ fs7.mkdirSync(path7.dirname(file), { recursive: true });
1297
+ fs7.writeFileSync(file, content);
1133
1298
  return true;
1134
1299
  }
1135
1300
  function importPath(fromDir, file) {
@@ -1222,13 +1387,13 @@ hmr(import.meta.hot)
1222
1387
  return this.project.sources().filter((source) => source.format === "module").map((source) => this.entryFile(source));
1223
1388
  }
1224
1389
  prune(dir, wanted) {
1225
- if (!fs6.existsSync(dir)) return;
1226
- for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
1390
+ if (!fs7.existsSync(dir)) return;
1391
+ for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
1227
1392
  const full = path7.join(dir, entry.name);
1228
1393
  if (entry.isDirectory()) {
1229
1394
  this.prune(full, wanted);
1230
- if (fs6.readdirSync(full).length === 0) fs6.rmdirSync(full);
1231
- } else if (!wanted.has(full)) fs6.rmSync(full);
1395
+ if (fs7.readdirSync(full).length === 0) fs7.rmdirSync(full);
1396
+ } else if (!wanted.has(full)) fs7.rmSync(full);
1232
1397
  }
1233
1398
  }
1234
1399
  };
@@ -1295,7 +1460,7 @@ ${html}
1295
1460
  }
1296
1461
 
1297
1462
  // src/server/links.ts
1298
- import fs7 from "fs";
1463
+ import fs8 from "fs";
1299
1464
  import path8 from "path";
1300
1465
  var SHIPPED_PACKAGES = ["react", "react-dom", "motion", "lucide-react", "clsx", "tailwind-merge"];
1301
1466
  function findPackageDir(fromDir, name, skip) {
@@ -1303,7 +1468,7 @@ function findPackageDir(fromDir, name, skip) {
1303
1468
  for (; ; ) {
1304
1469
  const modules = path8.join(dir, "node_modules");
1305
1470
  const candidate = path8.join(modules, name);
1306
- if (modules !== skip && fs7.existsSync(path8.join(candidate, "package.json"))) return candidate;
1471
+ if (modules !== skip && fs8.existsSync(path8.join(candidate, "package.json"))) return candidate;
1307
1472
  const parent = path8.dirname(dir);
1308
1473
  if (parent === dir) return null;
1309
1474
  dir = parent;
@@ -1311,7 +1476,7 @@ function findPackageDir(fromDir, name, skip) {
1311
1476
  }
1312
1477
  function isLink(file) {
1313
1478
  try {
1314
- return fs7.lstatSync(file).isSymbolicLink();
1479
+ return fs8.lstatSync(file).isSymbolicLink();
1315
1480
  } catch {
1316
1481
  return false;
1317
1482
  }
@@ -1322,41 +1487,41 @@ function linkShippedPackages(paths) {
1322
1487
  const allow = /* @__PURE__ */ new Set();
1323
1488
  for (const name of SHIPPED_PACKAGES) {
1324
1489
  const link = path8.join(modules, name);
1325
- const placedByUser = fs7.existsSync(link) && !isLink(link);
1490
+ const placedByUser = fs8.existsSync(link) && !isLink(link);
1326
1491
  const own = placedByUser ? link : findPackageDir(paths.design, name, modules);
1327
1492
  if (own) {
1328
- allow.add(path8.dirname(fs7.realpathSync(own)));
1329
- if (isLink(link)) fs7.unlinkSync(link);
1493
+ allow.add(path8.dirname(fs8.realpathSync(own)));
1494
+ if (isLink(link)) fs8.unlinkSync(link);
1330
1495
  continue;
1331
1496
  }
1332
1497
  const shipped = findPackageDir(PKG_ROOT, name);
1333
1498
  if (!shipped) continue;
1334
- const target2 = fs7.realpathSync(shipped);
1499
+ const target2 = fs8.realpathSync(shipped);
1335
1500
  allow.add(path8.dirname(target2));
1336
1501
  let current2 = null;
1337
1502
  try {
1338
- current2 = fs7.realpathSync(link);
1503
+ current2 = fs8.realpathSync(link);
1339
1504
  } catch {
1340
1505
  }
1341
1506
  if (current2 !== target2) {
1342
- if (isLink(link)) fs7.unlinkSync(link);
1343
- fs7.mkdirSync(path8.dirname(link), { recursive: true });
1344
- fs7.symlinkSync(target2, link, process.platform === "win32" ? "junction" : "dir");
1507
+ if (isLink(link)) fs8.unlinkSync(link);
1508
+ fs8.mkdirSync(path8.dirname(link), { recursive: true });
1509
+ fs8.symlinkSync(target2, link, process.platform === "win32" ? "junction" : "dir");
1345
1510
  }
1346
1511
  linked.push(name);
1347
1512
  }
1348
1513
  const self = path8.join(modules, "foss-design");
1349
- const target = fs7.realpathSync(PKG_ROOT);
1514
+ const target = fs8.realpathSync(PKG_ROOT);
1350
1515
  let current = null;
1351
1516
  try {
1352
- current = fs7.realpathSync(self);
1517
+ current = fs8.realpathSync(self);
1353
1518
  } catch {
1354
1519
  }
1355
1520
  if (current !== target) {
1356
- if (isLink(self)) fs7.unlinkSync(self);
1357
- if (!fs7.existsSync(self)) {
1358
- fs7.mkdirSync(modules, { recursive: true });
1359
- fs7.symlinkSync(target, self, process.platform === "win32" ? "junction" : "dir");
1521
+ if (isLink(self)) fs8.unlinkSync(self);
1522
+ if (!fs8.existsSync(self)) {
1523
+ fs8.mkdirSync(modules, { recursive: true });
1524
+ fs8.symlinkSync(target, self, process.platform === "win32" ? "junction" : "dir");
1360
1525
  }
1361
1526
  }
1362
1527
  allow.add(target);
@@ -1367,7 +1532,7 @@ function linkShippedPackages(paths) {
1367
1532
  import path9 from "path";
1368
1533
  import tailwindcss from "@tailwindcss/vite";
1369
1534
  import react from "@vitejs/plugin-react";
1370
- var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1535
+ var escapeRegExp = (text2) => text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1371
1536
  function aliases(project) {
1372
1537
  const { paths } = project;
1373
1538
  const list = [
@@ -1418,7 +1583,7 @@ function staticSnapshots(paths) {
1418
1583
  let data = metaCache.get(canvas);
1419
1584
  if (!data) {
1420
1585
  try {
1421
- data = JSON.parse(fs8.readFileSync(path10.join(paths.snapshots, canvas, "meta.json"), "utf8"));
1586
+ data = JSON.parse(fs9.readFileSync(path10.join(paths.snapshots, canvas, "meta.json"), "utf8"));
1422
1587
  } catch {
1423
1588
  }
1424
1589
  data ??= {};
@@ -1430,14 +1595,19 @@ function staticSnapshots(paths) {
1430
1595
  const entry = meta(canvas)[id2];
1431
1596
  if (!entry) return null;
1432
1597
  const urls = {};
1598
+ const thumbs = {};
1433
1599
  for (const theme2 of THEMES) {
1434
1600
  const file = path10.join(paths.snapshots, canvas, `${id2}.${theme2}.png`);
1435
- if (!entry[theme2] || !fs8.existsSync(file)) continue;
1601
+ if (!entry[theme2] || !fs9.existsSync(file)) continue;
1436
1602
  const rel = `_snap/${canvas}/${id2}.${theme2}.png`;
1437
1603
  copies.set(file, rel);
1438
1604
  urls[theme2] = `${encodePath2(rel)}?v=${entry[theme2]}`;
1605
+ const thumb = ensureThumb(file);
1606
+ const thumbRel = thumb === file ? rel : `_snap/${canvas}/${id2}.${theme2}.thumb.png`;
1607
+ copies.set(thumb, thumbRel);
1608
+ thumbs[theme2] = `${encodePath2(thumbRel)}?v=${entry[theme2]}`;
1439
1609
  }
1440
- return { urls, height: entry.height };
1610
+ return { urls, thumbs, height: entry.height };
1441
1611
  };
1442
1612
  return { lookup, copies };
1443
1613
  }
@@ -1453,53 +1623,55 @@ function rewriteRefs(html, originalDir, stagedDir) {
1453
1623
  if (!value || /^(?:[a-z][a-z0-9+.-]*:|\/|#|\{)/i.test(value)) return whole;
1454
1624
  const [pathname = "", suffix = ""] = /^([^?#]*)(.*)$/.exec(value).slice(1);
1455
1625
  const target = path10.resolve(originalDir, decodeURIComponent(pathname));
1456
- if (!fs8.existsSync(target)) return whole;
1626
+ if (!fs9.existsSync(target)) return whole;
1457
1627
  return `${attr}${quote}${relUrl(stagedDir, target)}${suffix}${quote}`;
1458
1628
  }
1459
1629
  );
1460
1630
  });
1461
1631
  }
1462
1632
  function copyTree(from, to, filter) {
1463
- if (!fs8.existsSync(from)) return;
1464
- for (const entry of fs8.readdirSync(from, { withFileTypes: true })) {
1633
+ if (!fs9.existsSync(from)) return;
1634
+ for (const entry of fs9.readdirSync(from, { withFileTypes: true })) {
1465
1635
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1466
1636
  const source = path10.join(from, entry.name);
1467
1637
  const target = path10.join(to, entry.name);
1468
1638
  if (entry.isDirectory()) copyTree(source, target, filter);
1469
- else if (filter(source) && !fs8.existsSync(target)) {
1470
- fs8.mkdirSync(path10.dirname(target), { recursive: true });
1471
- fs8.copyFileSync(source, target);
1639
+ else if (filter(source) && !fs9.existsSync(target)) {
1640
+ fs9.mkdirSync(path10.dirname(target), { recursive: true });
1641
+ fs9.copyFileSync(source, target);
1472
1642
  }
1473
1643
  }
1474
1644
  }
1475
1645
  function writeJson(file, value) {
1476
- fs8.mkdirSync(path10.dirname(file), { recursive: true });
1477
- fs8.writeFileSync(file, JSON.stringify(value));
1646
+ fs9.mkdirSync(path10.dirname(file), { recursive: true });
1647
+ fs9.writeFileSync(file, JSON.stringify(value));
1478
1648
  }
1479
- async function buildSite(paths, only, out) {
1649
+ async function buildSite(paths, out, options = {}) {
1480
1650
  const snapshots = staticSnapshots(paths);
1481
1651
  const project = new DesignProject(paths, STATIC_URLS, snapshots.lookup);
1482
1652
  const known = project.canvasIds();
1483
- const unknown = only.filter((id2) => !known.includes(id2));
1653
+ const only = options.canvases;
1654
+ const unknown = (only ?? []).filter((id2) => !known.includes(id2));
1484
1655
  if (unknown.length) throw new Error(`No canvas ${unknown.map((id2) => `"${id2}"`).join(", ")}`);
1485
- const canvasIds = only.length ? only : known;
1656
+ const canvasIds = only ?? known;
1486
1657
  const canvases = canvasIds.map((id2) => project.canvas(id2)).filter((c) => c !== null);
1487
- const system = project.system();
1658
+ const loadedSystem = project.system();
1659
+ const system = options.includeSystem === false ? null : loadedSystem;
1488
1660
  const entries = new Entries(project);
1489
1661
  entries.sync();
1490
1662
  linkShippedPackages(paths);
1491
1663
  const staging = path10.join(paths.cache, "build", "src");
1492
- fs8.rmSync(staging, { recursive: true, force: true });
1493
- const fonts = system?.doc.fonts ?? [];
1664
+ fs9.rmSync(staging, { recursive: true, force: true });
1665
+ const fonts = loadedSystem?.doc.fonts ?? [];
1494
1666
  const sources = [...system?.specimens ?? [], ...canvases.flatMap((canvas) => canvas.screens)];
1495
1667
  const inputs = [];
1496
1668
  const htmlScreens = [];
1497
1669
  for (const source of sources) {
1498
- if (!source.builtin && !fs8.existsSync(source.file)) continue;
1670
+ if (!source.builtin && !fs9.existsSync(source.file)) continue;
1499
1671
  const siteDir = path10.join("_s", source.canvas, source.id);
1500
1672
  const dir = path10.join(staging, siteDir);
1501
1673
  const file = path10.join(dir, "index.html");
1502
- fs8.mkdirSync(dir, { recursive: true });
1674
+ fs9.mkdirSync(dir, { recursive: true });
1503
1675
  const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
1504
1676
  const head = {
1505
1677
  source,
@@ -1509,21 +1681,21 @@ async function buildSite(paths, only, out) {
1509
1681
  autoHeight: source.autoHeight ?? false
1510
1682
  };
1511
1683
  if (source.format === "module") {
1512
- fs8.writeFileSync(file, moduleShell(head, relUrl(dir, entries.entryFile(source))));
1684
+ fs9.writeFileSync(file, moduleShell(head, relUrl(dir, entries.entryFile(source))));
1513
1685
  } else {
1514
- const raw = rewriteRefs(fs8.readFileSync(source.file, "utf8"), path10.dirname(source.file), dir);
1686
+ const raw = rewriteRefs(fs9.readFileSync(source.file, "utf8"), path10.dirname(source.file), dir);
1515
1687
  const withSystem = source.system && !optsOutOfSystem(raw);
1516
1688
  const html = injectIntoHtml(raw, {
1517
1689
  ...head,
1518
1690
  systemCss: withSystem ? relUrl(dir, entries.systemCssFile()) : void 0,
1519
1691
  boot: relUrl(dir, path10.join(entries.dir, HTML_BOOT))
1520
1692
  });
1521
- fs8.writeFileSync(file, html);
1693
+ fs9.writeFileSync(file, html);
1522
1694
  htmlScreens.push({ source, dir: siteDir });
1523
1695
  }
1524
1696
  inputs.push(file);
1525
1697
  }
1526
- fs8.mkdirSync(out, { recursive: true });
1698
+ fs9.mkdirSync(out, { recursive: true });
1527
1699
  if (inputs.length) {
1528
1700
  const base = baseConfig(project);
1529
1701
  await build({
@@ -1542,8 +1714,8 @@ async function buildSite(paths, only, out) {
1542
1714
  }
1543
1715
  });
1544
1716
  } else {
1545
- fs8.rmSync(out, { recursive: true, force: true });
1546
- fs8.mkdirSync(out, { recursive: true });
1717
+ fs9.rmSync(out, { recursive: true, force: true });
1718
+ fs9.mkdirSync(out, { recursive: true });
1547
1719
  }
1548
1720
  for (const { source, dir } of htmlScreens) {
1549
1721
  copyTree(
@@ -1562,17 +1734,20 @@ async function buildSite(paths, only, out) {
1562
1734
  for (const url of files) {
1563
1735
  const rel = url.slice("_f/".length).split("/").map(decodeURIComponent).join("/");
1564
1736
  const from = path10.join(paths.design, rel);
1565
- if (!fs8.existsSync(from)) continue;
1737
+ if (!fs9.existsSync(from)) continue;
1566
1738
  const to = path10.join(out, "_f", rel);
1567
- fs8.mkdirSync(path10.dirname(to), { recursive: true });
1568
- fs8.copyFileSync(from, to);
1739
+ fs9.mkdirSync(path10.dirname(to), { recursive: true });
1740
+ fs9.copyFileSync(from, to);
1569
1741
  }
1742
+ const info = project.info(true);
1743
+ const snapshotDirs = canvasIds.map((id2) => `_snap/${id2}/`);
1570
1744
  for (const [from, rel] of snapshots.copies) {
1745
+ if (!snapshotDirs.some((dir) => rel.startsWith(dir))) continue;
1571
1746
  const to = path10.join(out, rel);
1572
- fs8.mkdirSync(path10.dirname(to), { recursive: true });
1573
- fs8.copyFileSync(from, to);
1747
+ fs9.mkdirSync(path10.dirname(to), { recursive: true });
1748
+ fs9.copyFileSync(from, to);
1574
1749
  }
1575
- const info = project.info(true);
1750
+ if (!system) info.system = null;
1576
1751
  info.canvases = info.canvases.filter((canvas) => canvasIds.includes(canvas.id));
1577
1752
  writeJson(path10.join(out, "api", "project.json"), info);
1578
1753
  for (const canvas of canvases) writeJson(path10.join(out, "api", "canvas", `${canvas.doc.id}.json`), canvas.doc);
@@ -1582,35 +1757,36 @@ async function buildSite(paths, only, out) {
1582
1757
  for (const component of system.doc.components) {
1583
1758
  for (const rel of [component.specimen, ...component.sources]) {
1584
1759
  const file = path10.join(paths.root, rel);
1585
- if (!(rel in texts) && fs8.existsSync(file)) texts[rel] = fs8.readFileSync(file, "utf8");
1760
+ if (!(rel in texts) && fs9.existsSync(file)) texts[rel] = fs9.readFileSync(file, "utf8");
1586
1761
  }
1587
1762
  }
1588
1763
  writeJson(path10.join(out, "api", "sources.json"), texts);
1589
1764
  }
1590
- fs8.rmSync(staging, { recursive: true, force: true });
1765
+ fs9.rmSync(staging, { recursive: true, force: true });
1591
1766
  const errors = [
1592
- ...only.length ? [] : project.config().issues,
1767
+ ...only ? [] : project.config().issues,
1593
1768
  ...system?.doc.issues ?? [],
1594
1769
  ...canvases.flatMap((canvas) => canvas.doc.issues)
1595
1770
  ].filter((issue) => issue.severity === "error").length;
1596
- return { out, canvases: canvasIds, frames: inputs.length, errors };
1771
+ const { absoluteAssets } = options.includeViewer === false ? { absoluteAssets: false } : copyViewer(PKG.viewer, out);
1772
+ return { out, canvases: canvasIds, frames: inputs.length, errors, absoluteAssets };
1597
1773
  }
1598
1774
  function copyViewer(viewerDir, out) {
1599
1775
  copyTree(viewerDir, out, () => true);
1600
1776
  const index = path10.join(out, "index.html");
1601
- const html = fs8.readFileSync(index, "utf8");
1602
- const flag = "<script>window.__DESIGN_STATIC__=true</script>";
1777
+ const html = fs9.readFileSync(index, "utf8");
1778
+ const flag2 = "<script>window.__DESIGN_STATIC__=true</script>";
1603
1779
  const head = /<head(\s[^>]*)?>/i.exec(html);
1604
- fs8.writeFileSync(
1780
+ fs9.writeFileSync(
1605
1781
  index,
1606
- head ? html.slice(0, head.index + head[0].length) + flag + html.slice(head.index + head[0].length) : flag + html
1782
+ head ? html.slice(0, head.index + head[0].length) + flag2 + html.slice(head.index + head[0].length) : flag2 + html
1607
1783
  );
1608
1784
  return { absoluteAssets: /(?:src|href)=["']\/(?!\/)/.test(html) };
1609
1785
  }
1610
1786
 
1611
1787
  // src/cli/log.ts
1612
1788
  var color = process.stdout.isTTY && !process.env.NO_COLOR;
1613
- var wrap = (code) => (text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
1789
+ var wrap = (code) => (text2) => color ? `\x1B[${code}m${text2}\x1B[0m` : text2;
1614
1790
  var bold = wrap("1");
1615
1791
  var dim = wrap("2");
1616
1792
  var red = wrap("31");
@@ -1642,8 +1818,8 @@ function warn(line) {
1642
1818
  // src/cli/commands/build.ts
1643
1819
  function pack(out) {
1644
1820
  const archive = `${out.replace(/[\\/]+$/, "")}.tar.gz`;
1645
- fs9.rmSync(archive, { force: true });
1646
- const entries = fs9.readdirSync(out);
1821
+ fs10.rmSync(archive, { force: true });
1822
+ const entries = fs10.readdirSync(out);
1647
1823
  const result = spawnSync("tar", ["-czf", archive, "-C", out, ...entries], {
1648
1824
  stdio: ["ignore", "ignore", "pipe"],
1649
1825
  env: { ...process.env, COPYFILE_DISABLE: "1" }
@@ -1653,7 +1829,7 @@ function pack(out) {
1653
1829
  return archive;
1654
1830
  }
1655
1831
  async function runBuild(paths, canvases, options) {
1656
- if (!fs9.existsSync(path11.join(PKG.viewer, "index.html"))) {
1832
+ if (!fs10.existsSync(path11.join(PKG.viewer, "index.html"))) {
1657
1833
  throw new CliError("The viewer is missing from this foss-design install (dist/viewer). Rebuild or reinstall it.");
1658
1834
  }
1659
1835
  const out = path11.resolve(options.out ?? path11.join(paths.cache, "site"));
@@ -1661,12 +1837,12 @@ async function runBuild(paths, canvases, options) {
1661
1837
  print(dim(`Building ${canvases.length ? canvases.join(", ") : "every canvas"} and the design system\u2026`));
1662
1838
  let result;
1663
1839
  try {
1664
- result = await buildSite(paths, canvases, out);
1840
+ result = await buildSite(paths, out, { canvases: canvases.length ? canvases : void 0 });
1665
1841
  } catch (error) {
1666
1842
  throw new CliError(error.message);
1667
1843
  }
1668
- const { absoluteAssets } = copyViewer(PKG.viewer, out);
1669
- if (absoluteAssets) warn("the viewer references assets by absolute path; the site only works at the root of a host");
1844
+ if (result.absoluteAssets)
1845
+ warn("the viewer references assets by absolute path; the site only works at the root of a host");
1670
1846
  print(`${bold("Site")} ${out}`);
1671
1847
  print(
1672
1848
  `${bold("Frames")} ${result.frames} ${dim(`(${result.canvases.length} canvas${result.canvases.length === 1 ? "" : "es"})`)}`
@@ -1677,7 +1853,7 @@ async function runBuild(paths, canvases, options) {
1677
1853
  }
1678
1854
 
1679
1855
  // src/capture/chrome.ts
1680
- import fs10 from "fs";
1856
+ import fs11 from "fs";
1681
1857
  var CANDIDATES = {
1682
1858
  darwin: [
1683
1859
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
@@ -1701,8 +1877,8 @@ var CANDIDATES = {
1701
1877
  };
1702
1878
  function findChrome() {
1703
1879
  const configured = process.env.DESIGN_CHROME;
1704
- if (configured && fs10.existsSync(configured)) return configured;
1705
- for (const candidate of CANDIDATES[process.platform] ?? []) if (fs10.existsSync(candidate)) return candidate;
1880
+ if (configured && fs11.existsSync(configured)) return configured;
1881
+ for (const candidate of CANDIDATES[process.platform] ?? []) if (fs11.existsSync(candidate)) return candidate;
1706
1882
  return null;
1707
1883
  }
1708
1884
  async function launchChrome() {
@@ -1740,10 +1916,10 @@ async function openFrame(page, url, options = {}) {
1740
1916
  return el?.shadowRoot?.querySelector(".message")?.textContent ?? null;
1741
1917
  }).catch(() => null);
1742
1918
  if (overlay) errors.unshift(overlay.trim());
1743
- const unique = errors.filter(
1919
+ const unique2 = errors.filter(
1744
1920
  (error, index) => error && !errors.some((other, j) => j !== index && other && other.length < error.length && error.includes(other))
1745
1921
  );
1746
- return { errors: [...new Set(unique)], ready };
1922
+ return { errors: [...new Set(unique2)], ready };
1747
1923
  }
1748
1924
 
1749
1925
  // src/capture/frames.ts
@@ -1819,26 +1995,26 @@ async function mapLimit(items, limit, fn) {
1819
1995
  }
1820
1996
 
1821
1997
  // src/server/state.ts
1822
- import fs11 from "fs";
1998
+ import fs12 from "fs";
1823
1999
  import path12 from "path";
1824
2000
  var stateFile = (paths) => path12.join(paths.cache, "server.json");
1825
2001
  var logFile = (paths) => path12.join(paths.cache, "server.log");
1826
2002
  function readState(paths) {
1827
2003
  try {
1828
- return JSON.parse(fs11.readFileSync(stateFile(paths), "utf8"));
2004
+ return JSON.parse(fs12.readFileSync(stateFile(paths), "utf8"));
1829
2005
  } catch {
1830
2006
  return null;
1831
2007
  }
1832
2008
  }
1833
2009
  function writeState(paths, state) {
1834
- fs11.mkdirSync(paths.cache, { recursive: true });
1835
- fs11.writeFileSync(stateFile(paths), `${JSON.stringify(state, null, 2)}
2010
+ fs12.mkdirSync(paths.cache, { recursive: true });
2011
+ fs12.writeFileSync(stateFile(paths), `${JSON.stringify(state, null, 2)}
1836
2012
  `);
1837
2013
  }
1838
2014
  function clearState(paths, pid) {
1839
2015
  const state = readState(paths);
1840
2016
  if (pid !== void 0 && state && state.pid !== pid) return;
1841
- fs11.rmSync(stateFile(paths), { force: true });
2017
+ fs12.rmSync(stateFile(paths), { force: true });
1842
2018
  }
1843
2019
  function pidAlive(pid) {
1844
2020
  try {
@@ -1863,17 +2039,17 @@ async function liveServer(paths) {
1863
2039
 
1864
2040
  // src/cli/commands/preview.ts
1865
2041
  import { spawn } from "child_process";
1866
- import fs15 from "fs";
2042
+ import fs16 from "fs";
1867
2043
 
1868
2044
  // src/server/index.ts
1869
- import fs14 from "fs";
2045
+ import fs15 from "fs";
1870
2046
  import http from "http";
1871
2047
  import path15 from "path";
1872
2048
  import sirv from "sirv";
1873
2049
  import { createServer, searchForWorkspaceRoot } from "vite";
1874
2050
 
1875
2051
  // src/server/api.ts
1876
- import fs12 from "fs";
2052
+ import fs13 from "fs";
1877
2053
  import path13 from "path";
1878
2054
  function sendJson(res, status, body) {
1879
2055
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
@@ -1883,12 +2059,12 @@ function readBody(req, limit = 25 * 1024 * 1024) {
1883
2059
  return new Promise((resolve, reject) => {
1884
2060
  const chunks = [];
1885
2061
  let size2 = 0;
1886
- req.on("data", (chunk) => {
1887
- size2 += chunk.length;
2062
+ req.on("data", (chunk2) => {
2063
+ size2 += chunk2.length;
1888
2064
  if (size2 > limit) {
1889
2065
  reject(new Error("body too large"));
1890
2066
  req.destroy();
1891
- } else chunks.push(chunk);
2067
+ } else chunks.push(chunk2);
1892
2068
  });
1893
2069
  req.on("end", () => resolve(Buffer.concat(chunks)));
1894
2070
  req.on("error", reject);
@@ -1926,16 +2102,16 @@ async function handleApi(ctx, req, res, url) {
1926
2102
  if (route === "/source") {
1927
2103
  const rel = url.searchParams.get("path") ?? "";
1928
2104
  const file = path13.resolve(project.paths.root, rel);
1929
- if (!rel || !isInside(project.paths.root, file) || !fs12.existsSync(file) || fs12.statSync(file).isDirectory()) {
2105
+ if (!rel || !isInside(project.paths.root, file) || !fs13.existsSync(file) || fs13.statSync(file).isDirectory()) {
1930
2106
  sendJson(res, 404, { error: "not found" });
1931
2107
  return true;
1932
2108
  }
1933
- if (fs12.statSync(file).size > 2 * 1024 * 1024) {
2109
+ if (fs13.statSync(file).size > 2 * 1024 * 1024) {
1934
2110
  sendJson(res, 413, { error: "file too large to show" });
1935
2111
  return true;
1936
2112
  }
1937
2113
  res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
1938
- res.end(fs12.readFileSync(file, "utf8"));
2114
+ res.end(fs13.readFileSync(file, "utf8"));
1939
2115
  return true;
1940
2116
  }
1941
2117
  const snapshotPost = /^\/snapshots\/([^/]+)\/([^/]+)$/.exec(route);
@@ -1965,7 +2141,7 @@ async function handleApi(ctx, req, res, url) {
1965
2141
  return true;
1966
2142
  }
1967
2143
  res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
1968
- fs12.createReadStream(file).pipe(res);
2144
+ fs13.createReadStream(file).pipe(res);
1969
2145
  return true;
1970
2146
  }
1971
2147
  return false;
@@ -2005,7 +2181,7 @@ var EventHub = class {
2005
2181
  };
2006
2182
 
2007
2183
  // src/server/snapshots.ts
2008
- import fs13 from "fs";
2184
+ import fs14 from "fs";
2009
2185
  import path14 from "path";
2010
2186
  var SAFE = /^@?[a-z0-9][a-z0-9_-]*$/i;
2011
2187
  function isSafeSegment(value) {
@@ -2027,41 +2203,48 @@ var SnapshotStore = class {
2027
2203
  if (cached) return cached;
2028
2204
  let data = {};
2029
2205
  try {
2030
- data = JSON.parse(fs13.readFileSync(this.metaFile(canvas), "utf8"));
2206
+ data = JSON.parse(fs14.readFileSync(this.metaFile(canvas), "utf8"));
2031
2207
  } catch {
2032
2208
  }
2033
2209
  this.meta.set(canvas, data);
2034
2210
  return data;
2035
2211
  }
2036
- url(canvas, id2, theme2, version) {
2037
- return `${this.urlBase}/${encodeURIComponent(canvas)}/${encodeURIComponent(id2)}.${theme2}.png?v=${version}`;
2212
+ url(canvas, id2, theme2, version, thumb = false) {
2213
+ const name = `${encodeURIComponent(id2)}.${theme2}${thumb ? ".thumb" : ""}.png`;
2214
+ return `${this.urlBase}/${encodeURIComponent(canvas)}/${name}?v=${version}`;
2038
2215
  }
2039
2216
  lookup = (canvas, id2) => {
2040
2217
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return null;
2041
2218
  const entry = this.load(canvas)[id2];
2042
2219
  if (!entry) return null;
2043
2220
  const urls = {};
2044
- if (entry.light) urls.light = this.url(canvas, id2, "light", entry.light);
2045
- if (entry.dark) urls.dark = this.url(canvas, id2, "dark", entry.dark);
2046
- return { urls, height: entry.height };
2221
+ const thumbs = {};
2222
+ for (const theme2 of ["light", "dark"]) {
2223
+ const version = entry[theme2];
2224
+ if (!version) continue;
2225
+ urls[theme2] = this.url(canvas, id2, theme2, version);
2226
+ thumbs[theme2] = this.url(canvas, id2, theme2, version, true);
2227
+ }
2228
+ return { urls, thumbs, height: entry.height };
2047
2229
  };
2048
2230
  save(canvas, id2, theme2, png, height) {
2049
2231
  if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
2050
2232
  const dir = path14.join(this.dir, canvas);
2051
- fs13.mkdirSync(dir, { recursive: true });
2052
- fs13.writeFileSync(path14.join(dir, `${id2}.${theme2}.png`), png);
2233
+ fs14.mkdirSync(dir, { recursive: true });
2234
+ fs14.writeFileSync(path14.join(dir, `${id2}.${theme2}.png`), png);
2053
2235
  const data = this.load(canvas);
2054
2236
  const version = Date.now();
2055
2237
  data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
2056
- fs13.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
2238
+ fs14.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
2057
2239
  return { url: this.url(canvas, id2, theme2, version) };
2058
2240
  }
2059
- /** Absolute path of `<id>.<theme>.png`, or null. */
2241
+ /** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
2060
2242
  file(canvas, name) {
2061
- const match = /^(@?[a-z0-9][a-z0-9_-]*)\.(light|dark)\.png$/i.exec(name);
2243
+ const match = /^(@?[a-z0-9][a-z0-9_-]*)\.(light|dark)(\.thumb)?\.png$/i.exec(name);
2062
2244
  if (!isSafeSegment(canvas) || !match) return null;
2063
- const file = path14.join(this.dir, canvas, name);
2064
- return fs13.existsSync(file) ? file : null;
2245
+ const file = path14.join(this.dir, canvas, `${match[1]}.${match[2]}.png`);
2246
+ if (!fs14.existsSync(file)) return null;
2247
+ return match[3] ? ensureThumb(file) : file;
2065
2248
  }
2066
2249
  };
2067
2250
 
@@ -2091,16 +2274,16 @@ async function listen(server, port, host) {
2091
2274
  }
2092
2275
  throw new Error(`No free port between ${port} and ${port + 40}`);
2093
2276
  }
2094
- function notFoundPage(res, title, text) {
2277
+ function notFoundPage(res, title, text2) {
2095
2278
  res.writeHead(404, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
2096
2279
  res.end(
2097
- `<!doctype html><meta charset="utf-8"><body style="margin:0;display:grid;place-items:center;min-height:100vh;font:14px/1.5 ui-monospace,Menlo,monospace;background:#f5f6f8;color:#3a414a"><div style="max-width:520px;padding:24px"><div style="font-weight:600;margin-bottom:6px">${escapeHtml(title)}</div>${escapeHtml(text)}</div></body>`
2280
+ `<!doctype html><meta charset="utf-8"><body style="margin:0;display:grid;place-items:center;min-height:100vh;font:14px/1.5 ui-monospace,Menlo,monospace;background:#f5f6f8;color:#3a414a"><div style="max-width:520px;padding:24px"><div style="font-weight:600;margin-bottom:6px">${escapeHtml(title)}</div>${escapeHtml(text2)}</div></body>`
2098
2281
  );
2099
2282
  }
2100
2283
  async function startDevServer(options) {
2101
2284
  const paths = designPaths(options.root);
2102
- if (!fs14.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
2103
- fs14.mkdirSync(paths.cache, { recursive: true });
2285
+ if (!fs15.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
2286
+ fs15.mkdirSync(paths.cache, { recursive: true });
2104
2287
  const snapshots = new SnapshotStore(paths.snapshots);
2105
2288
  const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
2106
2289
  const entries = new Entries(project);
@@ -2129,10 +2312,10 @@ async function startDevServer(options) {
2129
2312
  entries: entries.files().map((file) => toPosix(path15.relative(paths.design, file)))
2130
2313
  }
2131
2314
  });
2132
- const viewer = fs14.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
2315
+ const viewer = fs15.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
2133
2316
  const viewerIndex = path15.join(PKG.viewer, "index.html");
2134
2317
  const serveViewerIndex = (res) => {
2135
- const html = fs14.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
2318
+ const html = fs15.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
2136
2319
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
2137
2320
  res.end(html);
2138
2321
  };
@@ -2157,7 +2340,7 @@ async function startDevServer(options) {
2157
2340
  notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
2158
2341
  return;
2159
2342
  }
2160
- if (!fs14.existsSync(entries.entryFile(source))) entries.sync();
2343
+ if (!fs15.existsSync(entries.entryFile(source))) entries.sync();
2161
2344
  const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
2162
2345
  const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
2163
2346
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
@@ -2167,7 +2350,7 @@ async function startDevServer(options) {
2167
2350
  const serveHtmlScreen = async (res, url2) => {
2168
2351
  const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
2169
2352
  const file = path15.resolve(paths.design, rel);
2170
- if (!isInside(paths.design, file) || !fs14.existsSync(file)) {
2353
+ if (!isInside(paths.design, file) || !fs15.existsSync(file)) {
2171
2354
  notFoundPage(res, "Screen not found", `No file .design/${rel}`);
2172
2355
  return;
2173
2356
  }
@@ -2183,7 +2366,7 @@ async function startDevServer(options) {
2183
2366
  props: {},
2184
2367
  system: true
2185
2368
  };
2186
- const raw = fs14.readFileSync(file, "utf8");
2369
+ const raw = fs15.readFileSync(file, "utf8");
2187
2370
  const withSystem = source.system && !optsOutOfSystem(raw);
2188
2371
  const html = injectIntoHtml(
2189
2372
  raw,
@@ -2351,17 +2534,20 @@ async function stopServer(paths) {
2351
2534
  }
2352
2535
  async function ensureServer(paths, options) {
2353
2536
  const live = await liveServer(paths);
2354
- if (live && !options.restart) return live;
2537
+ const stale = live && live.version !== packageVersion();
2538
+ if (live && !options.restart && !stale) return live;
2539
+ if (stale)
2540
+ print(dim(`Restarting the preview server: it runs foss-design ${live.version}, this is ${packageVersion()}.`));
2355
2541
  if (live) await stopServer(paths);
2356
- fs15.mkdirSync(paths.cache, { recursive: true });
2357
- const log = fs15.openSync(logFile(paths), "w");
2542
+ fs16.mkdirSync(paths.cache, { recursive: true });
2543
+ const log = fs16.openSync(logFile(paths), "w");
2358
2544
  const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
2359
2545
  if (options.port) args.push("--port", String(options.port));
2360
2546
  const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", log, log], cwd: paths.root });
2361
2547
  child.unref();
2362
2548
  const state = await waitForServer(paths, 4e4);
2363
2549
  if (!state) {
2364
- const tail = fs15.existsSync(logFile(paths)) ? fs15.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
2550
+ const tail = fs16.existsSync(logFile(paths)) ? fs16.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
2365
2551
  throw new CliError(`The preview server did not start.
2366
2552
  ${tail.join("\n")}`);
2367
2553
  }
@@ -2477,9 +2663,711 @@ async function runCheck(paths, canvases, options) {
2477
2663
  if (errors || broken.length) process.exitCode = 1;
2478
2664
  }
2479
2665
 
2480
- // src/cli/commands/scaffold.ts
2481
- import fs16 from "fs";
2666
+ // src/cli/commands/cloud.ts
2667
+ import fs20 from "fs";
2668
+ import os3 from "os";
2669
+ import path19 from "path";
2670
+
2671
+ // src/cloud/client.ts
2672
+ import os from "os";
2673
+
2674
+ // src/cloud/units.ts
2675
+ import { createHash as createHash2 } from "crypto";
2676
+ import fs17 from "fs";
2482
2677
  import path16 from "path";
2678
+ var SYSTEM_UNIT = "system";
2679
+ var CANVAS_PREFIX = "canvas/";
2680
+ var LOCAL_ONLY = /* @__PURE__ */ new Set(["cloud.json", "node_modules"]);
2681
+ var canvasUnit = (id2) => `${CANVAS_PREFIX}${id2}`;
2682
+ function unitCanvasId(key) {
2683
+ return key.startsWith(CANVAS_PREFIX) ? key.slice(CANVAS_PREFIX.length) : null;
2684
+ }
2685
+ function isUnitKey(key) {
2686
+ if (key === SYSTEM_UNIT) return true;
2687
+ const id2 = unitCanvasId(key);
2688
+ return id2 !== null && ID_PATTERN.test(id2);
2689
+ }
2690
+ function caseClashes(keys) {
2691
+ const byLower = /* @__PURE__ */ new Map();
2692
+ const clashes = /* @__PURE__ */ new Map();
2693
+ for (const key of new Set(keys)) {
2694
+ const other = byLower.get(key.toLowerCase());
2695
+ if (other === void 0) byLower.set(key.toLowerCase(), key);
2696
+ else {
2697
+ clashes.set(key, other);
2698
+ clashes.set(other, key);
2699
+ }
2700
+ }
2701
+ return clashes;
2702
+ }
2703
+ function unitRoot(paths, key) {
2704
+ const id2 = unitCanvasId(key);
2705
+ return id2 === null ? paths.design : path16.join(paths.canvases, id2);
2706
+ }
2707
+ function unitRelative(key, rel) {
2708
+ const id2 = unitCanvasId(key);
2709
+ return id2 === null ? rel : rel.slice(`${CANVAS_PREFIX}${id2}/`.length);
2710
+ }
2711
+ function isUnitPath(key, rel) {
2712
+ if (!rel || rel.includes("\\") || rel.includes("\0") || rel.startsWith("/")) return false;
2713
+ const segments = rel.split("/");
2714
+ if (segments.some((part) => !part || part === ".." || part.startsWith(".") || part === "node_modules")) return false;
2715
+ if (key === SYSTEM_UNIT) return rel === "design.json" || segments[0] === "system" && segments.length > 1;
2716
+ const id2 = unitCanvasId(key);
2717
+ return id2 !== null && segments[0] === "canvas" && segments[1] === id2 && segments.length > 2;
2718
+ }
2719
+ function hashBuffer(data) {
2720
+ return createHash2("sha256").update(data).digest("hex");
2721
+ }
2722
+ function hashFile(file) {
2723
+ const data = fs17.readFileSync(file);
2724
+ return { hash: hashBuffer(data), size: data.length };
2725
+ }
2726
+ var warnedLinks = /* @__PURE__ */ new Set();
2727
+ function warnLink(rel) {
2728
+ if (warnedLinks.has(rel)) return;
2729
+ warnedLinks.add(rel);
2730
+ process.stderr.write(`warning: .design/${rel} links outside .design; it is not synced
2731
+ `);
2732
+ }
2733
+ function walk(dir, base, out) {
2734
+ let entries;
2735
+ try {
2736
+ entries = fs17.readdirSync(dir, { withFileTypes: true });
2737
+ } catch {
2738
+ return;
2739
+ }
2740
+ for (const entry of entries) {
2741
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2742
+ const full = path16.join(dir, entry.name);
2743
+ let isFile = entry.isFile();
2744
+ let isDir = entry.isDirectory();
2745
+ if (entry.isSymbolicLink()) {
2746
+ isDir = false;
2747
+ isFile = false;
2748
+ try {
2749
+ const target = fs17.realpathSync(full);
2750
+ const root = fs17.realpathSync(base);
2751
+ if (target.startsWith(root + path16.sep)) isFile = fs17.statSync(target).isFile();
2752
+ else warnLink(toPosix(path16.relative(base, full)));
2753
+ } catch {
2754
+ }
2755
+ }
2756
+ if (isDir) walk(full, base, out);
2757
+ else if (isFile) out[toPosix(path16.relative(base, full))] = hashFile(full);
2758
+ }
2759
+ }
2760
+ function unitManifest(paths, key) {
2761
+ const manifest = {};
2762
+ if (key === SYSTEM_UNIT) {
2763
+ if (fs17.existsSync(paths.config) && fs17.statSync(paths.config).isFile())
2764
+ manifest["design.json"] = hashFile(paths.config);
2765
+ walk(paths.system, paths.design, manifest);
2766
+ } else walk(unitRoot(paths, key), paths.design, manifest);
2767
+ return sortManifest(manifest);
2768
+ }
2769
+ function buildManifest(dir) {
2770
+ const manifest = {};
2771
+ const visit = (current) => {
2772
+ for (const entry of fs17.readdirSync(current, { withFileTypes: true })) {
2773
+ const full = path16.join(current, entry.name);
2774
+ if (entry.isDirectory()) visit(full);
2775
+ else if (entry.isFile()) manifest[toPosix(path16.relative(dir, full))] = hashFile(full);
2776
+ }
2777
+ };
2778
+ if (fs17.existsSync(dir)) visit(dir);
2779
+ return sortManifest(manifest);
2780
+ }
2781
+ function sortManifest(manifest) {
2782
+ return Object.fromEntries(Object.entries(manifest).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
2783
+ }
2784
+ function scanLocal(paths) {
2785
+ const units = /* @__PURE__ */ new Map();
2786
+ const notSynced = [];
2787
+ if (!fs17.existsSync(paths.design)) return { units, notSynced };
2788
+ const system = unitManifest(paths, SYSTEM_UNIT);
2789
+ if (Object.keys(system).length) units.set(SYSTEM_UNIT, { key: SYSTEM_UNIT, kind: "system", manifest: system });
2790
+ for (const entry of fs17.readdirSync(paths.design, { withFileTypes: true })) {
2791
+ const { name } = entry;
2792
+ if (name.startsWith(".") || LOCAL_ONLY.has(name)) continue;
2793
+ if (name === "design.json" && entry.isFile()) continue;
2794
+ if ((name === "system" || name === "canvas") && entry.isDirectory()) continue;
2795
+ notSynced.push(entry.isDirectory() ? `${name}/` : name);
2796
+ }
2797
+ if (fs17.existsSync(paths.canvases) && fs17.statSync(paths.canvases).isDirectory()) {
2798
+ for (const entry of fs17.readdirSync(paths.canvases, { withFileTypes: true })) {
2799
+ if (entry.name.startsWith(".")) continue;
2800
+ if (!entry.isDirectory() || !ID_PATTERN.test(entry.name)) {
2801
+ notSynced.push(`canvas/${entry.name}${entry.isDirectory() ? "/" : ""}`);
2802
+ continue;
2803
+ }
2804
+ const key = canvasUnit(entry.name);
2805
+ const manifest = unitManifest(paths, key);
2806
+ if (Object.keys(manifest).length) units.set(key, { key, kind: "canvas", manifest });
2807
+ }
2808
+ }
2809
+ return { units, notSynced: notSynced.sort() };
2810
+ }
2811
+ function unitBuildDir(paths, key) {
2812
+ return path16.join(paths.cache, "cloud", "build", ...key.split("/"));
2813
+ }
2814
+ async function buildUnit(paths, key, out = unitBuildDir(paths, key)) {
2815
+ const id2 = unitCanvasId(key);
2816
+ fs17.rmSync(out, { recursive: true, force: true });
2817
+ return buildSite(paths, out, {
2818
+ canvases: id2 === null ? [] : [id2],
2819
+ includeSystem: id2 === null,
2820
+ includeViewer: false
2821
+ });
2822
+ }
2823
+
2824
+ // src/cloud/client.ts
2825
+ var CloudError = class extends CliError {
2826
+ constructor(message, status, error, limit, exitCode = 1) {
2827
+ super(message, exitCode);
2828
+ this.status = status;
2829
+ this.error = error;
2830
+ this.limit = limit;
2831
+ }
2832
+ status;
2833
+ error;
2834
+ limit;
2835
+ };
2836
+ var DEFAULT_MESSAGE = {
2837
+ 400: "The cloud refused the request",
2838
+ 403: "Not allowed",
2839
+ 404: "Not found, or you are not a member of this project",
2840
+ 409: "Changed in the cloud since your last pull",
2841
+ 410: "No longer available",
2842
+ 413: "Too large for your limits",
2843
+ 423: "Archived; unarchive it in the web app first",
2844
+ 429: "Limit reached; try again later",
2845
+ 451: "Blocked by moderation",
2846
+ 507: "The cloud is out of storage"
2847
+ };
2848
+ var JSON_TIMEOUT = 6e4;
2849
+ var BLOB_TIMEOUT = 6e5;
2850
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2851
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
2852
+ var str = (value) => typeof value === "string" ? value : void 0;
2853
+ var CONTROL = /[\u0000-\u001f\u007f-\u009f]/g;
2854
+ var plainText = (value) => value.replace(CONTROL, "");
2855
+ var text = (value) => {
2856
+ const found = str(value);
2857
+ return found === void 0 ? void 0 : plainText(found);
2858
+ };
2859
+ var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
2860
+ var flag = (...values) => values.some((value) => value === true || typeof value === "string" && !!value);
2861
+ var clientName = () => `foss-design ${packageVersion()}`;
2862
+ function errorFor(host, status, body, retryAfter) {
2863
+ const data = record(body);
2864
+ const code = str(data.error) ?? `http_${status}`;
2865
+ const limit = str(data.limit) ?? (num(data.limit) !== void 0 ? String(data.limit) : void 0);
2866
+ if (status === 401)
2867
+ return new CloudError(`Not signed in to ${host}, or the token was revoked. Run \`design login\`.`, status, code);
2868
+ let message = text(data.message) ?? DEFAULT_MESSAGE[status] ?? `${host} answered ${status}`;
2869
+ if (code === "account_banned") message = `Your account is banned: ${message}`;
2870
+ if (limit) message += ` (limit: ${limit})`;
2871
+ if (status === 429 && retryAfter) message += `; retry in ${retryAfter}s`;
2872
+ if (status >= 500 && status !== 507) message = `${host} failed (${status}): ${message}`;
2873
+ return new CloudError(message, status, code, limit, status === 409 ? 2 : 1);
2874
+ }
2875
+ function parseManifest(value) {
2876
+ const manifest = {};
2877
+ for (const [file, entry] of Object.entries(record(value))) {
2878
+ const item = typeof entry === "string" ? { hash: entry, size: -1 } : str(record(entry).hash) ? { hash: str(record(entry).hash), size: num(record(entry).size) ?? -1 } : null;
2879
+ if (item) manifest[file] = item;
2880
+ }
2881
+ return manifest;
2882
+ }
2883
+ function parseProject(value, fallbackRole) {
2884
+ const data = record(value);
2885
+ const id2 = text(data.id) ?? (num(data.id) !== void 0 ? String(data.id) : void 0);
2886
+ if (!id2) return null;
2887
+ return {
2888
+ id: id2,
2889
+ name: text(data.name) ?? id2,
2890
+ role: text(data.role) ?? fallbackRole,
2891
+ archived: flag(data.archived, data.archivedAt, data.archived_at),
2892
+ banned: flag(data.banned, data.bannedAt, data.banned_at)
2893
+ };
2894
+ }
2895
+ function parseUnit(value) {
2896
+ const data = record(value);
2897
+ const key = str(data.key);
2898
+ if (!key || !isUnitKey(key)) return null;
2899
+ return {
2900
+ key,
2901
+ title: text(data.title) ?? key,
2902
+ headRev: num(data.headRev) ?? num(data.head_rev) ?? num(data.head) ?? 0,
2903
+ archived: flag(data.archived, data.archivedAt, data.archived_at),
2904
+ banned: flag(data.banned, data.bannedAt, data.banned_at),
2905
+ manifest: parseManifest(data.sourceManifest ?? data.source_manifest ?? data.source),
2906
+ systemRev: num(data.systemRev) ?? num(data.system_rev) ?? null
2907
+ };
2908
+ }
2909
+ async function withRetries(fn, retries = 3) {
2910
+ for (let attempt = 0; ; attempt++) {
2911
+ try {
2912
+ return await fn();
2913
+ } catch (error) {
2914
+ const status = error instanceof CloudError ? error.status : 0;
2915
+ const retriable = status === 0 || status >= 500 && status !== 507;
2916
+ if (!retriable || attempt >= retries) throw error;
2917
+ await sleep(500 * 2 ** attempt);
2918
+ }
2919
+ }
2920
+ }
2921
+ var CloudClient = class {
2922
+ constructor(host, token) {
2923
+ this.host = host;
2924
+ this.token = token;
2925
+ }
2926
+ host;
2927
+ token;
2928
+ async send(method, path26, init) {
2929
+ const headers = {
2930
+ "User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
2931
+ Accept: "application/json"
2932
+ };
2933
+ if (this.token) headers.Authorization = `Bearer ${this.token}`;
2934
+ if (init.type) headers["Content-Type"] = init.type;
2935
+ try {
2936
+ return await fetch(`${this.host}/api${path26}`, {
2937
+ method,
2938
+ headers,
2939
+ body: init.body,
2940
+ signal: AbortSignal.timeout(init.timeout)
2941
+ });
2942
+ } catch (error) {
2943
+ const cause = error.cause;
2944
+ const reason = error.name === "TimeoutError" ? "timed out" : cause?.code ?? cause?.message ?? error.message;
2945
+ throw new CloudError(`Could not reach ${this.host}: ${reason}`, 0, "network");
2946
+ }
2947
+ }
2948
+ async fail(response) {
2949
+ let body = null;
2950
+ try {
2951
+ body = await response.json();
2952
+ } catch {
2953
+ }
2954
+ throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
2955
+ }
2956
+ /** JSON in, JSON out; any non-2xx answer throws a CloudError. */
2957
+ async request(method, path26, body) {
2958
+ const response = await this.send(method, path26, {
2959
+ body: body === void 0 ? void 0 : JSON.stringify(body),
2960
+ type: body === void 0 ? void 0 : "application/json",
2961
+ timeout: JSON_TIMEOUT
2962
+ });
2963
+ if (!response.ok) await this.fail(response);
2964
+ if (response.status === 204) return null;
2965
+ const text2 = await response.text();
2966
+ if (!text2) return null;
2967
+ try {
2968
+ return JSON.parse(text2);
2969
+ } catch {
2970
+ throw new CloudError(`${this.host} answered ${method} ${path26} with something other than JSON`, 0, "bad_response");
2971
+ }
2972
+ }
2973
+ /** POST /device/code. */
2974
+ deviceCode(hostname) {
2975
+ return this.request("POST", "/device/code", { client_name: clientName(), hostname });
2976
+ }
2977
+ /** POST /device/token: the token, or the RFC 8628 error code while it is not there yet. */
2978
+ async deviceToken(deviceCode) {
2979
+ const response = await this.send("POST", "/device/token", {
2980
+ body: JSON.stringify({ device_code: deviceCode }),
2981
+ type: "application/json",
2982
+ timeout: JSON_TIMEOUT
2983
+ });
2984
+ let body = {};
2985
+ try {
2986
+ body = record(await response.json());
2987
+ } catch {
2988
+ }
2989
+ const token = str(body.access_token);
2990
+ if (response.ok && token) return { token };
2991
+ const error = str(body.error);
2992
+ if (error) return { error, message: text(body.message) ?? text(body.error_description) };
2993
+ return this.fail(response);
2994
+ }
2995
+ async me() {
2996
+ const body = record(await this.request("GET", "/me"));
2997
+ const user = record(body.user ?? body);
2998
+ return { email: text(user.email) ?? "", name: text(user.name) };
2999
+ }
3000
+ async projects() {
3001
+ const body = record(await this.request("GET", "/projects"));
3002
+ const list = (value, role) => (Array.isArray(value) ? value : []).map((item) => parseProject(item, role)).filter((item) => item !== null);
3003
+ return { owned: list(body.owned, "owner"), shared: list(body.shared, "member") };
3004
+ }
3005
+ async createProject(name) {
3006
+ const body = record(await this.request("POST", "/projects", { name }));
3007
+ const project = parseProject(body.project ?? body, "owner");
3008
+ if (!project) throw new CloudError(`${this.host} did not return the new project`, 0, "bad_response");
3009
+ return project;
3010
+ }
3011
+ async units(projectId) {
3012
+ const body = await this.request("GET", `/projects/${encodeURIComponent(projectId)}/units`);
3013
+ const list = Array.isArray(body) ? body : record(body).units;
3014
+ const units = /* @__PURE__ */ new Map();
3015
+ for (const item of Array.isArray(list) ? list : []) {
3016
+ const unit = parseUnit(item);
3017
+ if (unit) units.set(unit.key, unit);
3018
+ }
3019
+ return units;
3020
+ }
3021
+ /** POST /blobs/missing, in batches: the hashes the cloud does not have yet. */
3022
+ async missing(projectId, hashes2) {
3023
+ const missing = [];
3024
+ for (let start = 0; start < hashes2.length; start += 1e3) {
3025
+ const body = record(
3026
+ await this.request("POST", `/projects/${encodeURIComponent(projectId)}/blobs/missing`, {
3027
+ hashes: hashes2.slice(start, start + 1e3)
3028
+ })
3029
+ );
3030
+ if (Array.isArray(body.missing)) missing.push(...body.missing.filter((hash) => typeof hash === "string"));
3031
+ }
3032
+ return missing;
3033
+ }
3034
+ putBlob(projectId, hash, data) {
3035
+ return withRetries(async () => {
3036
+ const response = await this.send("PUT", `/projects/${encodeURIComponent(projectId)}/blobs/${hash}`, {
3037
+ body: new Uint8Array(data),
3038
+ type: "application/octet-stream",
3039
+ timeout: BLOB_TIMEOUT
3040
+ });
3041
+ if (!response.ok) await this.fail(response);
3042
+ await response.arrayBuffer().catch(() => {
3043
+ });
3044
+ });
3045
+ }
3046
+ getBlob(projectId, hash) {
3047
+ return withRetries(async () => {
3048
+ const response = await this.send("GET", `/projects/${encodeURIComponent(projectId)}/blobs/${hash}`, {
3049
+ timeout: BLOB_TIMEOUT
3050
+ });
3051
+ if (!response.ok) await this.fail(response);
3052
+ const data = Buffer.from(await response.arrayBuffer());
3053
+ if (hashBuffer(data) !== hash)
3054
+ throw new CloudError(`Blob ${hash.slice(0, 12)} arrived damaged (hash mismatch)`, 0, "bad_blob");
3055
+ return data;
3056
+ });
3057
+ }
3058
+ /** POST /push: the new head of every pushed unit. */
3059
+ async push(projectId, units) {
3060
+ const body = await this.request("POST", `/projects/${encodeURIComponent(projectId)}/push`, { units });
3061
+ const data = record(body);
3062
+ const list = Array.isArray(body) ? body : data.units ?? data.results;
3063
+ const revs2 = /* @__PURE__ */ new Map();
3064
+ for (const item of Array.isArray(list) ? list : []) {
3065
+ const entry = record(item);
3066
+ const key = str(entry.key);
3067
+ const rev = num(entry.rev) ?? num(entry.headRev);
3068
+ if (key && rev !== void 0) revs2.set(key, rev);
3069
+ }
3070
+ return revs2;
3071
+ }
3072
+ };
3073
+
3074
+ // src/cloud/credentials.ts
3075
+ import fs18 from "fs";
3076
+ import os2 from "os";
3077
+ import path17 from "path";
3078
+ var DEFAULT_CLOUD = "https://app.fossdesign.dev";
3079
+ function normalizeHost(value) {
3080
+ let url;
3081
+ try {
3082
+ url = new URL(value);
3083
+ } catch {
3084
+ throw new CliError(`"${value}" is not a URL (FOSS_DESIGN_CLOUD takes one like https://app.fossdesign.dev)`);
3085
+ }
3086
+ if (url.protocol !== "https:" && url.protocol !== "http:")
3087
+ throw new CliError(`"${value}" is not an http(s) URL (FOSS_DESIGN_CLOUD takes one like https://app.fossdesign.dev)`);
3088
+ return url.origin;
3089
+ }
3090
+ function cloudHost() {
3091
+ return normalizeHost(process.env.FOSS_DESIGN_CLOUD || DEFAULT_CLOUD);
3092
+ }
3093
+ function credentialsFile() {
3094
+ const base = process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : process.env.XDG_CONFIG_HOME || path17.join(os2.homedir(), ".config");
3095
+ return path17.join(base, "foss-design", "credentials.json");
3096
+ }
3097
+ function readAll() {
3098
+ const file = credentialsFile();
3099
+ if (!fs18.existsSync(file)) return {};
3100
+ try {
3101
+ const data = JSON.parse(fs18.readFileSync(file, "utf8"));
3102
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
3103
+ } catch (error) {
3104
+ throw new CliError(`${file} is not valid JSON (${error.message}); fix or delete it`);
3105
+ }
3106
+ }
3107
+ function writeAll(data) {
3108
+ const file = credentialsFile();
3109
+ fs18.mkdirSync(path17.dirname(file), { recursive: true, mode: 448 });
3110
+ const temp = `${file}.${process.pid}.tmp`;
3111
+ fs18.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
3112
+ `, { mode: 384 });
3113
+ fs18.chmodSync(temp, 384);
3114
+ fs18.renameSync(temp, file);
3115
+ }
3116
+ function getCredential(host) {
3117
+ const entry = readAll()[host];
3118
+ return entry && typeof entry.token === "string" && entry.token ? entry : null;
3119
+ }
3120
+ function saveCredential(host, credential) {
3121
+ writeAll({ ...readAll(), [host]: credential });
3122
+ }
3123
+ function removeCredential(host) {
3124
+ const all = readAll();
3125
+ if (!(host in all)) return false;
3126
+ delete all[host];
3127
+ writeAll(all);
3128
+ return true;
3129
+ }
3130
+ function requireCredential(host) {
3131
+ const credential = getCredential(host);
3132
+ if (!credential) throw new CliError(`Not signed in to ${host}. Run \`design login\`.`);
3133
+ return credential;
3134
+ }
3135
+
3136
+ // src/cloud/state.ts
3137
+ import fs19 from "fs";
3138
+ import path18 from "path";
3139
+ var linkFile = (paths) => path18.join(paths.design, "cloud.json");
3140
+ var cloudCache = (paths) => path18.join(paths.cache, "cloud");
3141
+ var incomingDir = (paths, key) => path18.join(cloudCache(paths), "incoming", ...key.split("/"));
3142
+ function readLink(paths) {
3143
+ const file = linkFile(paths);
3144
+ if (!fs19.existsSync(file)) return null;
3145
+ let data;
3146
+ try {
3147
+ data = JSON.parse(fs19.readFileSync(file, "utf8"));
3148
+ } catch (error) {
3149
+ throw new CliError(`${file} is not valid JSON (${error.message}); run \`design link <project>\` again`);
3150
+ }
3151
+ if (typeof data?.host !== "string" || typeof data.project !== "string")
3152
+ throw new CliError(`${file} has no host or project; run \`design link <project>\` again`);
3153
+ const units = {};
3154
+ for (const [key, base] of Object.entries(data.units ?? {})) {
3155
+ if (!isUnitKey(key) || typeof base?.rev !== "number") continue;
3156
+ units[key] = { rev: base.rev, files: base.files && typeof base.files === "object" ? base.files : {} };
3157
+ }
3158
+ const conflicts = {};
3159
+ for (const [key, rev] of Object.entries(data.conflicts ?? {})) if (typeof rev === "number") conflicts[key] = rev;
3160
+ return { host: data.host, project: data.project, units, conflicts };
3161
+ }
3162
+ function requireLink(paths) {
3163
+ const link = readLink(paths);
3164
+ if (!link) throw new CliError("This .design is not linked to a cloud project. Run `design link` to pick one.");
3165
+ return link;
3166
+ }
3167
+ function writeLink(paths, link) {
3168
+ const sorted = (record2) => Object.fromEntries(Object.entries(record2).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
3169
+ const file = linkFile(paths);
3170
+ const temp = `${file}.${process.pid}.tmp`;
3171
+ const data = {
3172
+ host: link.host,
3173
+ project: link.project,
3174
+ units: sorted(link.units),
3175
+ conflicts: sorted(link.conflicts)
3176
+ };
3177
+ fs19.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
3178
+ `);
3179
+ fs19.renameSync(temp, file);
3180
+ }
3181
+ var hashes = (manifest) => Object.fromEntries(Object.entries(manifest).map(([file, entry]) => [file, entry.hash]));
3182
+ function diffFiles(a, b) {
3183
+ const out = [];
3184
+ for (const file of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) if (a[file] !== b[file]) out.push(file);
3185
+ return out.sort();
3186
+ }
3187
+ function unitStatus(key, link, local, remote) {
3188
+ const base = link.units[key];
3189
+ const baseRev = base?.rev ?? 0;
3190
+ const localFiles = hashes(local ?? {});
3191
+ const remoteUnit = remote?.get(key);
3192
+ const remoteFiles = remoteUnit ? hashes(remoteUnit.manifest) : {};
3193
+ const deletedRemotely = !!remote && !remoteUnit && baseRev > 0;
3194
+ const ahead = remote ? remoteUnit ? remoteUnit.headRev > baseRev : baseRev > 0 : false;
3195
+ let changed = diffFiles(localFiles, base?.files ?? {});
3196
+ if (ahead) changed = changed.filter((file) => localFiles[file] !== remoteFiles[file]);
3197
+ const remoteChanged = ahead ? diffFiles(remoteFiles, base?.files ?? {}) : [];
3198
+ const conflictRev = link.conflicts[key];
3199
+ let state;
3200
+ if (remoteUnit?.archived) state = "archived";
3201
+ else if (conflictRev !== void 0 || changed.length && remoteChanged.length && !deletedRemotely) state = "conflict";
3202
+ else if (ahead) state = "remote_ahead";
3203
+ else if (changed.length) state = "local_changes";
3204
+ else state = "in_sync";
3205
+ return {
3206
+ key,
3207
+ state,
3208
+ baseRev,
3209
+ headRev: remoteUnit ? remoteUnit.headRev : null,
3210
+ local: Object.keys(localFiles).length > 0,
3211
+ changed,
3212
+ ahead,
3213
+ remoteChanged,
3214
+ deletedRemotely,
3215
+ conflictRev
3216
+ };
3217
+ }
3218
+ function allUnitKeys(link, local, remote) {
3219
+ const keys = /* @__PURE__ */ new Set([...local.keys(), ...Object.keys(link.units), ...Object.keys(link.conflicts)]);
3220
+ for (const key of remote?.keys() ?? []) keys.add(key);
3221
+ return [...keys].sort((a, b) => a === "system" ? -1 : b === "system" ? 1 : a < b ? -1 : a > b ? 1 : 0);
3222
+ }
3223
+
3224
+ // src/cloud/sync.ts
3225
+ function openSync(paths, log) {
3226
+ const link = requireLink(paths);
3227
+ const credential = requireCredential(link.host);
3228
+ return { paths, link, client: new CloudClient(link.host, credential.token), log };
3229
+ }
3230
+ function newReport(command, link) {
3231
+ return { ok: true, command, host: link.host, project: link.project, units: [], hints: [] };
3232
+ }
3233
+ function unitArg(value) {
3234
+ if (value === SYSTEM_UNIT || value === "@system") return SYSTEM_UNIT;
3235
+ const key = value.startsWith("canvas/") ? value : canvasUnit(value);
3236
+ if (!isUnitKey(key)) throw new CliError(`"${value}" is not a unit: use system, a canvas id or canvas/<id>`);
3237
+ return key;
3238
+ }
3239
+ var unique = (keys) => [...new Set(keys)];
3240
+ var byUnit = (a, b) => a === SYSTEM_UNIT ? -1 : b === SYSTEM_UNIT ? 1 : a < b ? -1 : a > b ? 1 : 0;
3241
+ var plural2 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
3242
+
3243
+ // src/cli/commands/cloud.ts
3244
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3245
+ async function runLogin() {
3246
+ const host = cloudHost();
3247
+ const client = new CloudClient(host, null);
3248
+ const code = await client.deviceCode(os3.hostname());
3249
+ const url = new URL(code.verification_uri_complete || code.verification_uri, host).toString();
3250
+ const expiresIn = code.expires_in ?? 600;
3251
+ print(`${bold("Sign in to foss-design Cloud")} ${dim(host)}`);
3252
+ print();
3253
+ print(` Open this link ${cyan(url)}`);
3254
+ print(` Code ${bold(code.user_code)} ${dim("(the page must show the same code)")}`);
3255
+ if (!code.verification_uri_complete) print(dim(" Enter the code on that page."));
3256
+ print();
3257
+ if (process.stdout.isTTY) openBrowser(url);
3258
+ print(dim(`Waiting for approval in the browser\u2026 (the code expires in ${Math.round(expiresIn / 60)} minutes)`));
3259
+ let interval = Math.max(1, code.interval ?? 5) * 1e3;
3260
+ const deadline = Date.now() + expiresIn * 1e3;
3261
+ let token = null;
3262
+ while (!token) {
3263
+ await sleep2(interval);
3264
+ if (Date.now() > deadline) throw new CliError("The sign-in code expired. Run `design login` again.");
3265
+ let result;
3266
+ try {
3267
+ result = await client.deviceToken(code.device_code);
3268
+ } catch (error) {
3269
+ if (error instanceof CloudError && error.status === 0) continue;
3270
+ throw error;
3271
+ }
3272
+ if ("token" in result) {
3273
+ token = result.token;
3274
+ break;
3275
+ }
3276
+ if (result.error === "authorization_pending") continue;
3277
+ if (result.error === "slow_down") interval += 5e3;
3278
+ else if (result.error === "expired_token") throw new CliError("The sign-in code expired. Run `design login` again.");
3279
+ else if (result.error === "access_denied") throw new CliError("Sign-in was declined in the browser.");
3280
+ else throw new CliError(`Sign-in failed: ${result.message ?? result.error}`);
3281
+ }
3282
+ const me = await new CloudClient(host, token).me();
3283
+ saveCredential(host, { token, email: me.email });
3284
+ print(
3285
+ `${green("\u2713")} Signed in as ${bold(me.email || "unknown")} on ${host} ${dim(`(token in ${credentialsFile()})`)}`
3286
+ );
3287
+ }
3288
+ function runLogout() {
3289
+ const host = cloudHost();
3290
+ if (!removeCredential(host)) return print(dim(`Not signed in to ${host}.`));
3291
+ print(`${green("\u2713")} Signed out of ${host} on this machine.`);
3292
+ print(dim(`The token still exists in the cloud until you revoke it: ${host}/profile`));
3293
+ }
3294
+ function printProjects(title, projects, linked) {
3295
+ print(bold(title));
3296
+ if (!projects.length) return print(dim(" none"));
3297
+ const width = Math.max(...projects.map((project) => project.id.length));
3298
+ const nameWidth = Math.max(...projects.map((project) => project.name.length));
3299
+ for (const project of projects) {
3300
+ const flags = [project.archived && yellow("archived"), project.banned && yellow("banned")].filter(Boolean);
3301
+ print(
3302
+ ` ${project.id.padEnd(width)} ${project.name.padEnd(nameWidth)} ${dim(project.role.padEnd(6))}` + (flags.length ? ` ${flags.join(" ")}` : "") + (project.id === linked ? ` ${green("linked")}` : "")
3303
+ );
3304
+ }
3305
+ }
3306
+ async function runLink(paths, wanted, options) {
3307
+ if (wanted && options.newName !== void 0)
3308
+ throw new CliError("Pass a project or --new <name>, not both. `design link` lists your projects.");
3309
+ const host = cloudHost();
3310
+ const client = new CloudClient(host, requireCredential(host).token);
3311
+ const current = readLink(paths);
3312
+ let project;
3313
+ if (options.newName !== void 0) {
3314
+ const name = options.newName.trim();
3315
+ if (!name) throw new CliError("--new takes a project name");
3316
+ project = await client.createProject(name);
3317
+ print(`${green("created")} ${project.name} ${dim(`(${project.id})`)}`);
3318
+ } else {
3319
+ const { owned, shared } = await client.projects();
3320
+ if (!wanted) {
3321
+ const linked = current?.host === host ? current.project : void 0;
3322
+ print(`${bold("Projects on")} ${host}`);
3323
+ print();
3324
+ printProjects("Yours", owned, linked);
3325
+ printProjects("Shared with you", shared, linked);
3326
+ print();
3327
+ print(dim("Link one with `design link <id>`, or make a new one with `design link --new <name>`."));
3328
+ return;
3329
+ }
3330
+ const all = [...owned, ...shared];
3331
+ const byName = all.filter((item) => item.name === wanted);
3332
+ const found = all.find((item) => item.id === wanted) ?? (byName.length === 1 ? byName[0] : void 0);
3333
+ if (!found)
3334
+ throw new CliError(
3335
+ byName.length > 1 ? `Several projects are called "${wanted}"; link by id (\`design link\` lists them).` : `No project "${wanted}" among yours on ${host}. \`design link\` lists them.`
3336
+ );
3337
+ project = found;
3338
+ }
3339
+ if (project.banned) throw new CliError(`${project.name} is blocked by moderation and cannot be linked.`);
3340
+ if (current?.host === host && current.project === project.id) {
3341
+ print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
3342
+ } else {
3343
+ writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
3344
+ fs20.rmSync(path19.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
3345
+ const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
3346
+ print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
3347
+ }
3348
+ if (project.archived) print(yellow("The project is archived: unarchive it in the web app before pushing or pulling."));
3349
+ const link = readLink(paths);
3350
+ const remote = project.archived ? null : await client.units(project.id);
3351
+ const scan = scanLocal(paths);
3352
+ const states = allUnitKeys(link, scan.units, remote).map(
3353
+ (key) => unitStatus(key, link, scan.units.get(key)?.manifest, remote).state
3354
+ );
3355
+ const count = (state) => states.filter((item) => item === state).length;
3356
+ const ahead = count("remote_ahead");
3357
+ const conflicts = count("conflict");
3358
+ const local = count("local_changes");
3359
+ if (ahead || conflicts)
3360
+ print(
3361
+ `${plural2(ahead + conflicts, "unit")} in the cloud ${ahead + conflicts === 1 ? "is" : "are"} ahead of this .design` + (conflicts ? dim(` (${conflicts} also changed here)`) : "") + ". Next: `design pull`."
3362
+ );
3363
+ else if (local)
3364
+ print(`${plural2(local, "unit")} here ${local === 1 ? "is" : "are"} not in the cloud yet. Next: \`design push\`.`);
3365
+ else if (remote) print(dim("Everything is in sync."));
3366
+ }
3367
+
3368
+ // src/cli/commands/scaffold.ts
3369
+ import fs21 from "fs";
3370
+ import path20 from "path";
2483
3371
 
2484
3372
  // src/cli/templates.ts
2485
3373
  var TOKENS_CSS = `/*
@@ -2690,40 +3578,40 @@ function mainScreen(canvasId) {
2690
3578
 
2691
3579
  // src/cli/commands/scaffold.ts
2692
3580
  function write(file, content, root) {
2693
- if (fs16.existsSync(file)) {
2694
- print(` ${dim("exists ")} ${path16.relative(root, file)}`);
3581
+ if (fs21.existsSync(file)) {
3582
+ print(` ${dim("exists ")} ${path20.relative(root, file)}`);
2695
3583
  return false;
2696
3584
  }
2697
- fs16.mkdirSync(path16.dirname(file), { recursive: true });
2698
- fs16.writeFileSync(file, content);
2699
- print(` ${green("created")} ${path16.relative(root, file)}`);
3585
+ fs21.mkdirSync(path20.dirname(file), { recursive: true });
3586
+ fs21.writeFileSync(file, content);
3587
+ print(` ${green("created")} ${path20.relative(root, file)}`);
2700
3588
  return true;
2701
3589
  }
2702
3590
  function ensureGitignored(root) {
2703
- const file = path16.join(root, ".gitignore");
2704
- const current = fs16.existsSync(file) ? fs16.readFileSync(file, "utf8") : "";
3591
+ const file = path20.join(root, ".gitignore");
3592
+ const current = fs21.existsSync(file) ? fs21.readFileSync(file, "utf8") : "";
2705
3593
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
2706
3594
  if (covered) return "present";
2707
3595
  const prefix = current && !current.endsWith("\n") ? "\n" : "";
2708
- fs16.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
3596
+ fs21.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
2709
3597
  .design/
2710
3598
  `);
2711
3599
  return "added";
2712
3600
  }
2713
3601
  function projectName(root) {
2714
3602
  try {
2715
- const manifest = JSON.parse(fs16.readFileSync(path16.join(root, "package.json"), "utf8"));
3603
+ const manifest = JSON.parse(fs21.readFileSync(path20.join(root, "package.json"), "utf8"));
2716
3604
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
2717
3605
  } catch {
2718
3606
  }
2719
- return path16.basename(root);
3607
+ return path20.basename(root);
2720
3608
  }
2721
3609
  function runInit(root, options) {
2722
3610
  const paths = designPaths(root);
2723
3611
  print(`Setting up ${DESIGN_DIR} in ${root}`);
2724
3612
  write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
2725
3613
  `, root);
2726
- fs16.mkdirSync(paths.canvases, { recursive: true });
3614
+ fs21.mkdirSync(paths.canvases, { recursive: true });
2727
3615
  if (options.gitignore) {
2728
3616
  const result = ensureGitignored(root);
2729
3617
  print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
@@ -2735,44 +3623,44 @@ function runSystemInit(root, options) {
2735
3623
  const paths = designPaths(root);
2736
3624
  let base = projectName(root);
2737
3625
  try {
2738
- base = JSON.parse(fs16.readFileSync(paths.config, "utf8")).name || base;
3626
+ base = JSON.parse(fs21.readFileSync(paths.config, "utf8")).name || base;
2739
3627
  } catch {
2740
3628
  }
2741
3629
  const name = options.name ?? `${base} design system`;
2742
- print(`Scaffolding the design system in ${path16.relative(root, paths.system)}`);
2743
- write(path16.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
3630
+ print(`Scaffolding the design system in ${path20.relative(root, paths.system)}`);
3631
+ write(path20.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
2744
3632
  `, root);
2745
- write(path16.join(paths.system, "tokens.css"), TOKENS_CSS, root);
3633
+ write(path20.join(paths.system, "tokens.css"), TOKENS_CSS, root);
2746
3634
  if (!options.empty) {
2747
- write(path16.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
2748
- write(path16.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
2749
- write(path16.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
3635
+ write(path20.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
3636
+ write(path20.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
3637
+ write(path20.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
2750
3638
  }
2751
3639
  }
2752
3640
  function runNew(root, id2, options) {
2753
3641
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
2754
3642
  const paths = designPaths(root);
2755
- const dir = path16.join(paths.canvases, id2);
2756
- if (fs16.existsSync(path16.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
3643
+ const dir = path20.join(paths.canvases, id2);
3644
+ if (fs21.existsSync(path20.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
2757
3645
  print(`Creating canvas "${id2}"`);
2758
3646
  const title = options.title ?? titleize(id2);
2759
3647
  if (options.empty) {
2760
3648
  write(
2761
- path16.join(dir, "canvas.json"),
3649
+ path20.join(dir, "canvas.json"),
2762
3650
  `${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
2763
3651
  `,
2764
3652
  root
2765
3653
  );
2766
- fs16.mkdirSync(path16.join(dir, "screens"), { recursive: true });
3654
+ fs21.mkdirSync(path20.join(dir, "screens"), { recursive: true });
2767
3655
  } else {
2768
- write(path16.join(dir, "canvas.json"), canvasJson(title), root);
2769
- write(path16.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
3656
+ write(path20.join(dir, "canvas.json"), canvasJson(title), root);
3657
+ write(path20.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
2770
3658
  }
2771
3659
  }
2772
3660
 
2773
3661
  // src/cli/commands/shot.ts
2774
- import fs17 from "fs";
2775
- import path17 from "path";
3662
+ import fs22 from "fs";
3663
+ import path21 from "path";
2776
3664
  var OVERVIEW_TIMEOUT = 9e4;
2777
3665
  async function runShot(paths, target, options) {
2778
3666
  const [canvasId = "", screenId] = target.split("/");
@@ -2788,15 +3676,15 @@ async function runShot(paths, target, options) {
2788
3676
  throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
2789
3677
  }
2790
3678
  const theme2 = options.theme ?? doc.theme ?? "light";
2791
- const outBase = path17.resolve(options.out ?? paths.shots);
2792
- const outDir = path17.join(outBase, canvasId);
2793
- fs17.mkdirSync(outDir, { recursive: true });
3679
+ const outBase = path21.resolve(options.out ?? paths.shots);
3680
+ const outDir = path21.join(outBase, canvasId);
3681
+ fs22.mkdirSync(outDir, { recursive: true });
2794
3682
  const base = await serverBase(paths);
2795
3683
  const browser = await launchChrome();
2796
3684
  let failed = false;
2797
3685
  try {
2798
3686
  if (options.overview) {
2799
- if (!fs17.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
3687
+ if (!fs22.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
2800
3688
  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);
2801
3689
  for (const pageId of pages) {
2802
3690
  const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
@@ -2812,7 +3700,7 @@ async function runShot(paths, target, options) {
2812
3700
  warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
2813
3701
  }
2814
3702
  await page.waitForTimeout(400);
2815
- const file = path17.join(outDir, `${pageId}.overview.png`);
3703
+ const file = path21.join(outDir, `${pageId}.overview.png`);
2816
3704
  await page.screenshot({ path: file });
2817
3705
  await page.close();
2818
3706
  print(file);
@@ -2841,7 +3729,7 @@ async function shootFrames(browser, jobs, outDir) {
2841
3729
  let failed = false;
2842
3730
  const results = await mapLimit(jobs, 4, async (job) => {
2843
3731
  const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
2844
- const file = path17.join(outDir, `${job.id}.${job.theme}.png`);
3732
+ const file = path21.join(outDir, `${job.id}.${job.theme}.png`);
2845
3733
  try {
2846
3734
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
2847
3735
  await page.screenshot({ path: file, fullPage: job.fullPage });
@@ -2873,8 +3761,8 @@ async function shootSystem(paths, project, id2, options) {
2873
3761
  if (id2 && !all.some((job) => job.id === id2)) {
2874
3762
  throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
2875
3763
  }
2876
- const outDir = path17.join(path17.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
2877
- fs17.mkdirSync(outDir, { recursive: true });
3764
+ const outDir = path21.join(path21.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
3765
+ fs22.mkdirSync(outDir, { recursive: true });
2878
3766
  const base = await serverBase(paths);
2879
3767
  const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
2880
3768
  const browser = await launchChrome();
@@ -2885,14 +3773,100 @@ async function shootSystem(paths, project, id2, options) {
2885
3773
  }
2886
3774
  }
2887
3775
 
3776
+ // src/cli/commands/cloud-status.ts
3777
+ import path22 from "path";
3778
+ var STATE_TEXT = {
3779
+ in_sync: green,
3780
+ local_changes: yellow,
3781
+ remote_ahead: yellow,
3782
+ conflict: red,
3783
+ archived: dim
3784
+ };
3785
+ function detail(paths, status) {
3786
+ const parts = [];
3787
+ if (status.state === "remote_ahead" || status.state === "conflict")
3788
+ parts.push(
3789
+ status.deletedRemotely ? "deleted in the cloud" : `r${status.baseRev} \u2192 r${status.conflictRev ?? status.headRev}`
3790
+ );
3791
+ else if (status.baseRev) parts.push(`r${status.baseRev}`);
3792
+ if (status.state === "local_changes" || status.state === "conflict") {
3793
+ if (!status.baseRev && status.headRev === null) parts.push("new");
3794
+ else if (!status.local) parts.push("deleted here");
3795
+ else if (status.changed.length) parts.push(plural2(status.changed.length, "file"));
3796
+ }
3797
+ if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
3798
+ if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
3799
+ parts.push(`${plural2(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
3800
+ if (status.conflictRev !== void 0) parts.push(`merge ${path22.relative(paths.root, incomingDir(paths, status.key))}`);
3801
+ return parts.join(" \xB7 ");
3802
+ }
3803
+ async function printCloudStatus(paths, label) {
3804
+ let link;
3805
+ try {
3806
+ link = readLink(paths);
3807
+ } catch (error) {
3808
+ print(`${label("Cloud")}${red(error.message)}`);
3809
+ return;
3810
+ }
3811
+ const host = link?.host ?? cloudHost();
3812
+ const credential = getCredential(host);
3813
+ const client = credential ? new CloudClient(host, credential.token) : null;
3814
+ let remote = null;
3815
+ let problem = null;
3816
+ if (!client || !credential) {
3817
+ problem = "not signed in";
3818
+ print(`${label("Cloud")}${dim(`not signed in to ${host} \u2014 \`design login\``)}`);
3819
+ } else {
3820
+ try {
3821
+ const me = await client.me();
3822
+ print(`${label("Cloud")}${me.email || credential.email} ${dim(host)}`);
3823
+ if (link) remote = await client.units(link.project);
3824
+ } catch (error) {
3825
+ problem = error.message;
3826
+ if (error instanceof CloudError && error.status === 401)
3827
+ print(`${label("Cloud")}${red(`the token for ${host} was rejected \u2014 \`design login\``)}`);
3828
+ else print(`${label("Cloud")}${credential.email} ${dim(host)}`);
3829
+ }
3830
+ }
3831
+ if (!link) {
3832
+ print(`${label("Linked")}${dim("no \u2014 `design link` lists your cloud projects")}`);
3833
+ return;
3834
+ }
3835
+ let name = link.project;
3836
+ if (client && !problem) {
3837
+ try {
3838
+ const { owned, shared } = await client.projects();
3839
+ const found = [...owned, ...shared].find((project) => project.id === link.project);
3840
+ if (found) name = `${found.name} ${dim(`(${found.id}, ${found.role}${found.archived ? ", archived" : ""})`)}`;
3841
+ } catch {
3842
+ }
3843
+ }
3844
+ print(`${label("Linked")}${name}`);
3845
+ if (problem) print(`${" ".repeat(9)}${yellow(`cloud state unknown (${problem}); local changes only`)}`);
3846
+ const scan = scanLocal(paths);
3847
+ const statuses = allUnitKeys(link, scan.units, remote).map(
3848
+ (key) => unitStatus(key, link, scan.units.get(key)?.manifest, remote)
3849
+ );
3850
+ if (!statuses.length) print(`${label("Units")}${dim("none yet")}`);
3851
+ else {
3852
+ print(label("Units"));
3853
+ const width = Math.max(...statuses.map((status) => status.key.length));
3854
+ for (const status of statuses) {
3855
+ const state = STATE_TEXT[status.state](status.state.replaceAll("_", " ").padEnd(13));
3856
+ print(` ${status.key.padEnd(width)} ${state} ${dim(detail(paths, status))}`);
3857
+ }
3858
+ }
3859
+ if (scan.notSynced.length) print(`${label("Unsynced")}${dim(`${scan.notSynced.join(", ")} (not part of any unit)`)}`);
3860
+ }
3861
+
2888
3862
  // src/cli/commands/status.ts
2889
- var plural2 = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
3863
+ var plural3 = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
2890
3864
  async function runStatus(paths) {
2891
3865
  const project = new DesignProject(paths, DEV_URLS);
2892
3866
  const server = await liveServer(paths);
2893
- const label = (text) => bold(text.padEnd(9));
3867
+ const label = (text2) => bold(text2.padEnd(9));
2894
3868
  print(
2895
- server ? `${label("Preview")}${green("running")} ${server.url} ${dim(`(pid ${server.pid}, since ${new Date(server.startedAt).toLocaleString()})`)}` : `${label("Preview")}${dim("not running \u2014 `design preview` starts it")}`
3869
+ server ? `${label("Preview")}${green("running")} ${server.url} ${dim(`(foss-design ${server.version}, pid ${server.pid}, since ${new Date(server.startedAt).toLocaleString()})`)}` : `${label("Preview")}${dim("not running \u2014 `design preview` starts it")}`
2896
3870
  );
2897
3871
  print(`${label("Project")}${project.name()} ${dim(paths.root)}`);
2898
3872
  const system = project.system();
@@ -2900,7 +3874,7 @@ async function runStatus(paths) {
2900
3874
  const { doc } = system;
2901
3875
  print(
2902
3876
  `${label("System")}${doc.name} ${dim(
2903
- `\u2014 ${plural2(doc.tokens.length, "token")}, ${plural2(doc.components.length, "component")}, ${plural2(doc.guidelines.length, "guideline")}`
3877
+ `\u2014 ${plural3(doc.tokens.length, "token")}, ${plural3(doc.components.length, "component")}, ${plural3(doc.guidelines.length, "guideline")}`
2904
3878
  )}`
2905
3879
  );
2906
3880
  } else print(`${label("System")}${dim("none \u2014 `design system init` scaffolds one")}`);
@@ -2915,17 +3889,493 @@ async function runStatus(paths) {
2915
3889
  const errors2 = doc.issues.filter((issue) => issue.severity === "error").length;
2916
3890
  const url = server ? dim(` ${server.url}/c/${doc.id}`) : "";
2917
3891
  print(
2918
- ` ${doc.id.padEnd(width)} ${doc.title} ${dim(`${plural2(doc.pages.length, "page")} \xB7 ${plural2(frames, "screen")}`)}` + (errors2 ? ` ${red(plural2(errors2, "error"))}` : "") + url
3892
+ ` ${doc.id.padEnd(width)} ${doc.title} ${dim(`${plural3(doc.pages.length, "page")} \xB7 ${plural3(frames, "screen")}`)}` + (errors2 ? ` ${red(plural3(errors2, "error"))}` : "") + url
2919
3893
  );
2920
3894
  }
2921
3895
  }
2922
3896
  const issues = project.issues();
2923
3897
  const errors = issues.filter((issue) => issue.severity === "error").length;
2924
3898
  const warnings = issues.length - errors;
2925
- const summary = `${plural2(errors, "error")}, ${plural2(warnings, "warning")}`;
3899
+ const summary = `${plural3(errors, "error")}, ${plural3(warnings, "warning")}`;
2926
3900
  print(
2927
3901
  `${label("Issues")}${errors ? red(summary) : warnings ? yellow(summary) : green(summary)}` + (issues.length ? dim(" \u2014 `design check` lists them") : "")
2928
3902
  );
3903
+ await printCloudStatus(paths, label);
3904
+ }
3905
+
3906
+ // src/cloud/pull.ts
3907
+ import fs23 from "fs";
3908
+ import path23 from "path";
3909
+ var HASH = /^[0-9a-f]{64}$/;
3910
+ function checkManifest(key, manifest) {
3911
+ for (const [rel, entry] of Object.entries(manifest)) {
3912
+ if (!isUnitPath(key, rel)) throw new CliError(`The cloud sent a path that does not belong to ${key}: "${rel}"`);
3913
+ if (!HASH.test(entry.hash)) throw new CliError(`The cloud sent a bad hash for ${rel} in ${key}`);
3914
+ }
3915
+ }
3916
+ function writeInside(root, rel, data) {
3917
+ const file = path23.join(root, rel);
3918
+ if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
3919
+ fs23.mkdirSync(path23.dirname(file), { recursive: true });
3920
+ if (!isInside(fs23.realpathSync(root), fs23.realpathSync(path23.dirname(file))))
3921
+ throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
3922
+ if (fs23.existsSync(file) && fs23.lstatSync(file).isSymbolicLink()) fs23.unlinkSync(file);
3923
+ fs23.writeFileSync(file, data);
3924
+ }
3925
+ function pruneEmpty(dir, stop) {
3926
+ let current = dir;
3927
+ while (isInside(stop, current) && current !== stop) {
3928
+ try {
3929
+ if (fs23.readdirSync(current).length) return;
3930
+ fs23.rmdirSync(current);
3931
+ } catch {
3932
+ return;
3933
+ }
3934
+ current = path23.dirname(current);
3935
+ }
3936
+ }
3937
+ function applyUnit(paths, key, local, target, blobs) {
3938
+ const counts = { added: 0, changed: 0, removed: 0 };
3939
+ const stop = key === SYSTEM_UNIT ? paths.design : path23.dirname(unitRoot(paths, key));
3940
+ for (const rel of Object.keys(local)) {
3941
+ if (rel in target) continue;
3942
+ const file = path23.join(paths.design, rel);
3943
+ fs23.rmSync(file, { force: true });
3944
+ pruneEmpty(path23.dirname(file), stop);
3945
+ counts.removed++;
3946
+ }
3947
+ for (const [rel, entry] of Object.entries(target)) {
3948
+ if (local[rel]?.hash === entry.hash) continue;
3949
+ writeInside(paths.design, rel, blobs.get(entry.hash));
3950
+ if (local[rel]) counts.changed++;
3951
+ else counts.added++;
3952
+ }
3953
+ return counts;
3954
+ }
3955
+ async function pull(ctx, options) {
3956
+ const { paths, client, log } = ctx;
3957
+ const link = structuredClone(ctx.link);
3958
+ const report = newReport("pull", link);
3959
+ const selected = unique(options.units.map(unitArg));
3960
+ const theirs = unique(options.theirs.map(unitArg));
3961
+ const scan = scanLocal(paths);
3962
+ const remote = await client.units(link.project);
3963
+ const local = (key) => scan.units.get(key)?.manifest ?? {};
3964
+ const keys = selected.length || theirs.length ? unique([...selected, ...theirs]) : allUnitKeys(link, /* @__PURE__ */ new Map(), remote);
3965
+ const clashes = caseClashes([...remote.keys(), ...scan.units.keys(), ...Object.keys(link.units)]);
3966
+ const plans = [];
3967
+ for (const key of keys.sort(byUnit)) {
3968
+ const clash = clashes.get(key);
3969
+ if (clash !== void 0) {
3970
+ report.units.push({
3971
+ unit: key,
3972
+ action: "conflict",
3973
+ message: `differs from ${clash} only by letter case; rename one of them before pulling`
3974
+ });
3975
+ continue;
3976
+ }
3977
+ const unit = remote.get(key) ?? null;
3978
+ const s = unitStatus(key, link, scan.units.get(key)?.manifest, remote);
3979
+ if (!unit && !link.units[key] && link.conflicts[key] === void 0) {
3980
+ if (selected.includes(key) || theirs.includes(key))
3981
+ report.units.push({ unit: key, action: "local_only", message: "not in the cloud; `design push` creates it" });
3982
+ continue;
3983
+ }
3984
+ if (unit?.archived || unit?.banned) {
3985
+ report.units.push({
3986
+ unit: key,
3987
+ action: unit.archived ? "archived" : "banned",
3988
+ baseRev: s.baseRev,
3989
+ headRev: unit.headRev,
3990
+ message: unit.archived ? "archived in the cloud; left as it is" : "blocked by moderation; left as it is"
3991
+ });
3992
+ continue;
3993
+ }
3994
+ const reportFor = (action) => ({
3995
+ unit: key,
3996
+ action,
3997
+ rev: unit?.headRev,
3998
+ baseRev: s.baseRev
3999
+ });
4000
+ if (theirs.includes(key)) {
4001
+ const entry = reportFor(unit ? s.local ? "updated" : "created" : "deleted");
4002
+ entry.message = "local changes discarded";
4003
+ plans.push({ key, kind: "apply", remote: unit, report: entry });
4004
+ } else if (!s.ahead) {
4005
+ if (s.conflictRev !== void 0) {
4006
+ delete link.conflicts[key];
4007
+ fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4008
+ }
4009
+ report.units.push({ ...reportFor("up_to_date"), rev: s.baseRev });
4010
+ } else if (!s.changed.length) {
4011
+ plans.push({
4012
+ key,
4013
+ kind: "apply",
4014
+ remote: unit,
4015
+ report: reportFor(!unit ? "deleted" : !s.local && !s.baseRev ? "created" : "updated")
4016
+ });
4017
+ } else if (unit && !s.remoteChanged.length) {
4018
+ link.units[key] = { rev: unit.headRev, files: hashes(unit.manifest) };
4019
+ report.units.push({
4020
+ ...reportFor("updated"),
4021
+ files: { added: 0, changed: 0, removed: 0 },
4022
+ message: "rebuilt in the cloud with the same files; your local changes are kept"
4023
+ });
4024
+ } else if (!unit) {
4025
+ delete link.units[key];
4026
+ report.units.push({
4027
+ ...reportFor("kept"),
4028
+ message: "deleted in the cloud; your local changes are kept and `design push` creates it again"
4029
+ });
4030
+ } else {
4031
+ const entry = {
4032
+ ...reportFor("conflict"),
4033
+ rev: void 0,
4034
+ headRev: unit.headRev,
4035
+ incoming: incomingDir(paths, key)
4036
+ };
4037
+ entry.message = `changed here and in the cloud; the cloud version (r${unit.headRev}) is in ${path23.relative(paths.root, entry.incoming)}`;
4038
+ if (s.conflictRev === unit.headRev && fs23.existsSync(entry.incoming)) report.units.push(entry);
4039
+ else plans.push({ key, kind: "incoming", remote: unit, report: entry });
4040
+ }
4041
+ }
4042
+ const byHash = /* @__PURE__ */ new Map();
4043
+ for (const unit of scan.units.values())
4044
+ for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path23.join(paths.design, rel));
4045
+ const needed = /* @__PURE__ */ new Set();
4046
+ for (const plan of plans) {
4047
+ if (!plan.remote) continue;
4048
+ checkManifest(plan.key, plan.remote.manifest);
4049
+ for (const entry of Object.values(plan.remote.manifest)) needed.add(entry.hash);
4050
+ }
4051
+ const blobs = /* @__PURE__ */ new Map();
4052
+ const download = [];
4053
+ for (const hash of needed) {
4054
+ const file = byHash.get(hash);
4055
+ if (file) blobs.set(hash, fs23.readFileSync(file));
4056
+ else download.push(hash);
4057
+ }
4058
+ if (download.length) {
4059
+ log(`Downloading ${plural2(download.length, "file")}\u2026`);
4060
+ await mapLimit(download, 4, async (hash) => {
4061
+ blobs.set(hash, await client.getBlob(link.project, hash));
4062
+ });
4063
+ }
4064
+ for (const plan of plans) {
4065
+ const { key, report: entry } = plan;
4066
+ if (plan.kind === "apply") {
4067
+ entry.files = applyUnit(paths, key, local(key), plan.remote?.manifest ?? {}, blobs);
4068
+ if (plan.remote) link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
4069
+ else delete link.units[key];
4070
+ delete link.conflicts[key];
4071
+ fs23.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4072
+ } else {
4073
+ const dir = incomingDir(paths, key);
4074
+ fs23.rmSync(dir, { recursive: true, force: true });
4075
+ fs23.mkdirSync(dir, { recursive: true });
4076
+ for (const [rel, file] of Object.entries(plan.remote.manifest))
4077
+ writeInside(dir, unitRelative(key, rel), blobs.get(file.hash));
4078
+ link.conflicts[key] = plan.remote.headRev;
4079
+ }
4080
+ report.units.push(entry);
4081
+ }
4082
+ writeLink(paths, link);
4083
+ report.units.sort((a, b) => byUnit(a.unit, b.unit));
4084
+ const conflicts = report.units.filter((entry) => entry.action === "conflict");
4085
+ if (conflicts.length) {
4086
+ report.ok = false;
4087
+ report.error = {
4088
+ code: "conflict",
4089
+ message: `${plural2(conflicts.length, "unit")} changed both here and in the cloud. Merge the cloud version from incoming/ into your files, then \`design push --resolved <unit>\`; or \`design pull --theirs <unit>\` to take the cloud version.`
4090
+ };
4091
+ }
4092
+ return report;
4093
+ }
4094
+
4095
+ // src/cloud/push.ts
4096
+ import fs24 from "fs";
4097
+ import path24 from "path";
4098
+ var megabytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
4099
+ function blockedReport(status, message) {
4100
+ return {
4101
+ unit: status.key,
4102
+ action: status.conflictRev !== void 0 ? "conflict" : "remote_ahead",
4103
+ baseRev: status.baseRev,
4104
+ headRev: status.conflictRev ?? status.headRev ?? void 0,
4105
+ message
4106
+ };
4107
+ }
4108
+ async function push(ctx, options) {
4109
+ const { paths, client, log } = ctx;
4110
+ const link = structuredClone(ctx.link);
4111
+ const report = newReport("push", link);
4112
+ const selected = unique(options.units.map(unitArg));
4113
+ const resolved = unique(options.resolved.map(unitArg));
4114
+ const scan = scanLocal(paths);
4115
+ const remote = await client.units(link.project);
4116
+ const clashes = caseClashes([...scan.units.keys(), ...remote.keys()]);
4117
+ if (clashes.size) {
4118
+ const pairs = [...clashes].filter(([a, b]) => a < b).map(([a, b]) => `${a} / ${b}`);
4119
+ throw new CliError(
4120
+ `These canvases differ only by letter case: ${pairs.join(", ")}. On macOS and Windows they share one folder; rename one of them.`
4121
+ );
4122
+ }
4123
+ for (const key of resolved) {
4124
+ const head = link.conflicts[key];
4125
+ if (head === void 0)
4126
+ throw new CliError(`${key} has no pending conflict; --resolved is for units \`design pull\` reported in conflict`);
4127
+ link.units[key] = { rev: head, files: link.units[key]?.files ?? {} };
4128
+ delete link.conflicts[key];
4129
+ }
4130
+ const status = (key) => unitStatus(key, link, scan.units.get(key)?.manifest, remote);
4131
+ for (const key of selected) {
4132
+ if (!scan.units.has(key) && !remote.has(key) && !link.units[key]) {
4133
+ const known = [...scan.units.keys()].join(", ") || "none";
4134
+ throw new CliError(`No ${key} here or in the cloud. Units here: ${known}`);
4135
+ }
4136
+ }
4137
+ let candidates;
4138
+ if (selected.length) {
4139
+ const system = status(SYSTEM_UNIT);
4140
+ if (!selected.includes(SYSTEM_UNIT) && scan.units.has(SYSTEM_UNIT) && system.changed.length)
4141
+ throw new CliError(
4142
+ "The design system has changes that are not in the cloud, and canvases are built against it. Push it too: `design push system <canvas\u2026>`, or `design push` for everything."
4143
+ );
4144
+ candidates = unique([...selected, ...resolved]);
4145
+ } else {
4146
+ candidates = [...resolved];
4147
+ for (const key of scan.units.keys()) {
4148
+ const s = status(key);
4149
+ if (s.changed.length || s.conflictRev !== void 0) candidates.push(key);
4150
+ }
4151
+ candidates = unique(candidates);
4152
+ }
4153
+ const toPush = [];
4154
+ for (const key of candidates.sort(byUnit)) {
4155
+ const s = status(key);
4156
+ if (!scan.units.has(key)) {
4157
+ report.units.push({ unit: key, action: "missing_locally", message: "not here; push never deletes in the cloud" });
4158
+ continue;
4159
+ }
4160
+ const unit = remote.get(key);
4161
+ if (unit?.archived || unit?.banned) {
4162
+ report.units.push({
4163
+ unit: key,
4164
+ action: unit.archived ? "archived" : "banned",
4165
+ baseRev: s.baseRev,
4166
+ message: unit.archived ? "archived in the cloud; unarchive it in the web app to push" : "blocked by moderation"
4167
+ });
4168
+ continue;
4169
+ }
4170
+ toPush.push(key);
4171
+ }
4172
+ const blocked = [];
4173
+ const systemPush = toPush.includes(SYSTEM_UNIT);
4174
+ const activeRemoteCanvases = [...remote.values()].filter(
4175
+ (unit) => unit.key !== SYSTEM_UNIT && !unit.archived && !unit.banned
4176
+ );
4177
+ if (systemPush) {
4178
+ for (const unit of activeRemoteCanvases) {
4179
+ if (scan.units.has(unit.key)) continue;
4180
+ const s = status(unit.key);
4181
+ blocked.push(
4182
+ blockedReport(
4183
+ s,
4184
+ s.baseRev ? `deleted here but active in the cloud, and a system push rebuilds every active canvas; restore it with \`design pull --theirs ${unit.key}\` or archive it in the web app` : "in the cloud but not pulled here, and a system push rebuilds every active canvas; run `design pull`"
4185
+ )
4186
+ );
4187
+ }
4188
+ for (const key of scan.units.keys()) {
4189
+ const unit = remote.get(key);
4190
+ if (key !== SYSTEM_UNIT && !unit?.archived && !unit?.banned && !toPush.includes(key)) toPush.push(key);
4191
+ }
4192
+ toPush.sort(byUnit);
4193
+ } else if (toPush.length) {
4194
+ const system = status(SYSTEM_UNIT);
4195
+ if (system.ahead)
4196
+ blocked.push(
4197
+ blockedReport(system, "the design system changed in the cloud, and canvases are built against it; pull first")
4198
+ );
4199
+ }
4200
+ for (const key of toPush) {
4201
+ const s = status(key);
4202
+ if (s.conflictRev !== void 0) {
4203
+ const dir = path24.relative(paths.root, incomingDir(paths, key));
4204
+ blocked.push(
4205
+ blockedReport(s, `pull left the cloud version in ${dir}; merge it, then \`design push --resolved ${key}\``)
4206
+ );
4207
+ } else if (s.ahead) {
4208
+ blocked.push(
4209
+ blockedReport(
4210
+ s,
4211
+ s.deletedRemotely ? "deleted in the cloud; `design pull` keeps your changes as a new canvas" : `changed in the cloud since your last pull (r${s.baseRev} \u2192 r${s.headRev}); run \`design pull\``
4212
+ )
4213
+ );
4214
+ }
4215
+ }
4216
+ for (const unit of activeRemoteCanvases)
4217
+ if (!scan.units.has(unit.key) && !blocked.some((entry) => entry.unit === unit.key))
4218
+ report.hints.push(
4219
+ `${unit.key} is in the cloud but not here: \`design pull\` fetches it. Push never deletes cloud canvases; archive or delete them in the web app.`
4220
+ );
4221
+ if (blocked.length) {
4222
+ report.ok = false;
4223
+ report.units.push(...blocked.sort((a, b) => byUnit(a.unit, b.unit)));
4224
+ const conflicts = blocked.some((entry) => entry.action === "conflict");
4225
+ report.error = conflicts ? {
4226
+ code: "unresolved_conflict",
4227
+ message: "Nothing was pushed: merge the pending conflicts, then `design push --resolved <unit>`."
4228
+ } : {
4229
+ code: "pull_first",
4230
+ message: "Nothing was pushed: the cloud has newer revisions. Run `design pull`, then push again."
4231
+ };
4232
+ return report;
4233
+ }
4234
+ const pushing = toPush.filter((key) => {
4235
+ const s = status(key);
4236
+ if (systemPush || resolved.includes(key) || s.changed.length || !remote.has(key)) return true;
4237
+ report.units.push({ unit: key, action: "unchanged", rev: s.baseRev });
4238
+ return false;
4239
+ });
4240
+ if (!pushing.length) return report;
4241
+ const project = new DesignProject(paths, STATIC_URLS);
4242
+ const systemRev = remote.get(SYSTEM_UNIT)?.headRev ?? 0;
4243
+ const units = [];
4244
+ const files = /* @__PURE__ */ new Map();
4245
+ const addFiles = (manifest, dir) => {
4246
+ for (const [rel, entry] of Object.entries(manifest))
4247
+ if (!files.has(entry.hash)) files.set(entry.hash, path24.join(dir, rel));
4248
+ };
4249
+ for (const key of pushing) {
4250
+ const id2 = unitCanvasId(key);
4251
+ log(`Building ${key}\u2026`);
4252
+ let result;
4253
+ try {
4254
+ result = await buildUnit(paths, key);
4255
+ } catch (error) {
4256
+ throw new CliError(`Building ${key} failed: ${error.message}`);
4257
+ }
4258
+ if (result.errors)
4259
+ report.hints.push(
4260
+ `${key} has ${plural2(result.errors, "error")}; \`design check\` lists them. It was pushed as it is.`
4261
+ );
4262
+ const source = scan.units.get(key).manifest;
4263
+ const build2 = buildManifest(unitBuildDir(paths, key));
4264
+ addFiles(source, paths.design);
4265
+ addFiles(build2, unitBuildDir(paths, key));
4266
+ units.push({
4267
+ key,
4268
+ baseRev: status(key).baseRev,
4269
+ title: id2 === null ? project.system()?.doc.name ?? "Design system" : project.canvas(id2)?.doc.title ?? id2,
4270
+ source,
4271
+ build: build2,
4272
+ systemRev: id2 === null ? null : systemRev
4273
+ });
4274
+ }
4275
+ const missing = await client.missing(link.project, [...files.keys()]);
4276
+ if (missing.length) {
4277
+ const unknown = missing.find((hash) => !files.has(hash));
4278
+ if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
4279
+ const bytes = missing.reduce((sum, hash) => sum + fs24.statSync(files.get(hash)).size, 0);
4280
+ log(`Uploading ${plural2(missing.length, "file")} (${megabytes(bytes)})\u2026`);
4281
+ await mapLimit(missing, 4, async (hash) => {
4282
+ await client.putBlob(link.project, hash, fs24.readFileSync(files.get(hash)));
4283
+ });
4284
+ }
4285
+ log(`Pushing ${plural2(units.length, "unit")}\u2026`);
4286
+ const revs2 = await client.push(link.project, units);
4287
+ for (const unit of units) {
4288
+ const rev = revs2.get(unit.key) ?? unit.baseRev + 1;
4289
+ link.units[unit.key] = { rev, files: hashes(unit.source) };
4290
+ report.units.push({ unit: unit.key, action: unit.baseRev ? "pushed" : "created", rev, baseRev: unit.baseRev });
4291
+ }
4292
+ for (const key of resolved) fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
4293
+ writeLink(paths, link);
4294
+ report.units.sort((a, b) => byUnit(a.unit, b.unit));
4295
+ return report;
4296
+ }
4297
+
4298
+ // src/cli/commands/sync.ts
4299
+ var GOOD = /* @__PURE__ */ new Set(["pushed", "created", "updated", "deleted"]);
4300
+ var BAD = /* @__PURE__ */ new Set(["conflict", "remote_ahead"]);
4301
+ var QUIET = /* @__PURE__ */ new Set(["up_to_date", "unchanged"]);
4302
+ function revs(entry) {
4303
+ if (entry.action === "created") return entry.rev !== void 0 ? `r${entry.rev}` : "";
4304
+ if (entry.action === "deleted") return "";
4305
+ if (GOOD.has(entry.action))
4306
+ return entry.baseRev && entry.baseRev !== entry.rev ? `r${entry.baseRev} \u2192 r${entry.rev}` : `r${entry.rev}`;
4307
+ if (BAD.has(entry.action) && entry.headRev !== void 0) return `r${entry.baseRev ?? 0} \u2192 r${entry.headRev}`;
4308
+ return "";
4309
+ }
4310
+ function printReport(report) {
4311
+ const verb = report.command === "push" ? "Pushed to" : "Pulled from";
4312
+ const shown = report.units.filter((entry) => !QUIET.has(entry.action));
4313
+ const quiet = report.units.length - shown.length;
4314
+ const moved = report.units.some((entry) => GOOD.has(entry.action));
4315
+ if (report.ok && !moved) print(`${bold(`Nothing to ${report.command}`)} ${dim(`${report.project} \xB7 ${report.host}`)}`);
4316
+ else if (report.ok) print(`${bold(verb)} ${report.project} ${dim(report.host)}`);
4317
+ else print(`${bold(report.command === "push" ? "Push stopped" : "Pull finished with conflicts")} ${dim(report.host)}`);
4318
+ const width = Math.max(0, ...shown.map((entry) => entry.unit.length));
4319
+ for (const entry of shown) {
4320
+ const mark = GOOD.has(entry.action) ? green("\u2713") : BAD.has(entry.action) ? red("\u2717") : yellow("!");
4321
+ const files = entry.files ? dim(
4322
+ [
4323
+ entry.files.added && `+${entry.files.added}`,
4324
+ entry.files.changed && `~${entry.files.changed}`,
4325
+ entry.files.removed && `-${entry.files.removed}`
4326
+ ].filter(Boolean).join(" ")
4327
+ ) : "";
4328
+ const detail2 = [entry.action.replaceAll("_", " "), revs(entry), files].filter(Boolean).join(" ");
4329
+ print(` ${mark} ${entry.unit.padEnd(width)} ${detail2}`);
4330
+ if (entry.message) print(` ${dim(entry.message)}`);
4331
+ }
4332
+ if (quiet) print(dim(` ${plural2(quiet, "unit")} ${report.command === "push" ? "unchanged" : "up to date"}`));
4333
+ for (const hint of report.hints) print(dim(hint));
4334
+ }
4335
+ async function runSync(paths, command, json, run) {
4336
+ let report;
4337
+ try {
4338
+ report = await run(openSync(paths, json ? () => {
4339
+ } : (line) => print(dim(line))));
4340
+ } catch (error) {
4341
+ if (!json) throw error;
4342
+ let link = null;
4343
+ try {
4344
+ link = readLink(paths);
4345
+ } catch {
4346
+ }
4347
+ const cloud = error instanceof CloudError ? error : null;
4348
+ const failure = {
4349
+ ok: false,
4350
+ command,
4351
+ host: link?.host ?? "",
4352
+ project: link?.project ?? "",
4353
+ units: [],
4354
+ hints: [],
4355
+ error: {
4356
+ code: cloud?.error ?? (error instanceof CliError ? "error" : "internal"),
4357
+ message: error.message,
4358
+ status: cloud?.status || void 0,
4359
+ limit: cloud?.limit
4360
+ }
4361
+ };
4362
+ print(JSON.stringify(failure, null, 2));
4363
+ process.exitCode = error instanceof CliError ? error.code : 1;
4364
+ return;
4365
+ }
4366
+ if (json) {
4367
+ print(JSON.stringify(report, null, 2));
4368
+ if (!report.ok) process.exitCode = 2;
4369
+ return;
4370
+ }
4371
+ printReport(report);
4372
+ if (!report.ok && report.error) throw new CliError(report.error.message, 2);
4373
+ }
4374
+ function runPush(paths, units, options) {
4375
+ return runSync(paths, "push", options.json, (ctx) => push(ctx, { units, resolved: options.resolved }));
4376
+ }
4377
+ function runPull(paths, units, options) {
4378
+ return runSync(paths, "pull", options.json, (ctx) => pull(ctx, { units, theirs: options.theirs }));
2929
4379
  }
2930
4380
 
2931
4381
  // src/cli/index.ts
@@ -2940,10 +4390,13 @@ ${bold("Project")}
2940
4390
 
2941
4391
  ${bold("Preview")}
2942
4392
  preview [--open [path]] [--port <n>] Start the viewer in the background on a free port (or reuse
2943
- it) and print its URL; --open also opens a browser
4393
+ it; one from another version restarts) and print its URL;
4394
+ --open also opens a browser
2944
4395
  [--restart] [--foreground]
2945
4396
  stop Stop the viewer
2946
- status Viewer state and a project summary
4397
+ status Viewer state, a project summary and, with a cloud link, the
4398
+ account, the linked project and each unit: in sync, local
4399
+ changes, remote ahead, conflict or archived
2947
4400
 
2948
4401
  ${bold("Verify")}
2949
4402
  check [canvas\u2026] [--render] [--json] Validate canvas.json and the system; --render loads every
@@ -2955,13 +4408,29 @@ ${bold("Verify")}
2955
4408
  ${bold("Share")}
2956
4409
  build [canvas\u2026] [--out <dir>] [--tar] Static site of the canvases and the design system
2957
4410
 
4411
+ ${bold("Cloud")} ${dim("units: system (design.json + system/) and canvas/<id>; exit 2 = conflict")}
4412
+ login Sign in to foss-design Cloud; prints a link to open (the
4413
+ code is in the link) and waits for approval
4414
+ logout Forget this machine's token for the cloud
4415
+ link [<project>] [--new <name>] No args: list your projects. Link .design to a cloud project
4416
+ (linking to another project replaces the link)
4417
+ push [canvas\u2026] [--json] Build and upload what changed here (or the named units); a
4418
+ [--resolved <unit>] system change pushes every active canvas with it. Stops if
4419
+ the cloud is ahead; --resolved marks a merged conflict
4420
+ pull [canvas\u2026] [--json] Take cloud changes into .design; a unit changed on both sides
4421
+ [--theirs <unit>] goes to .design/.cache/cloud/incoming/<unit> (exit 2);
4422
+ --theirs takes the cloud version and drops local changes
4423
+
2958
4424
  ${bold("Options")}
2959
4425
  --root <dir> Project folder (default: the nearest folder with .design, or the current one)
2960
4426
  -h, --help This help
2961
4427
  -v, --version Version
4428
+
4429
+ ${bold("Environment")}
4430
+ FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
2962
4431
  `;
2963
4432
  function projectPaths(rootOption, requireDesign = true) {
2964
- const found = rootOption ? path18.resolve(rootOption) : findProjectRoot();
4433
+ const found = rootOption ? path25.resolve(rootOption) : findProjectRoot();
2965
4434
  if (!found) {
2966
4435
  if (!requireDesign) return designPaths(process.cwd());
2967
4436
  throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
@@ -2991,6 +4460,9 @@ async function main(argv) {
2991
4460
  out: { type: "string" },
2992
4461
  overview: { type: "boolean" },
2993
4462
  tar: { type: "boolean" },
4463
+ new: { type: "string" },
4464
+ resolved: { type: "string", multiple: true },
4465
+ theirs: { type: "string", multiple: true },
2994
4466
  help: { type: "boolean", short: "h" },
2995
4467
  version: { type: "boolean", short: "v" }
2996
4468
  }
@@ -3004,7 +4476,7 @@ async function main(argv) {
3004
4476
  if (values.theme && !theme2) throw new CliError("--theme is light or dark");
3005
4477
  switch (command) {
3006
4478
  case "init":
3007
- return runInit(values.root ? path18.resolve(values.root) : process.cwd(), {
4479
+ return runInit(values.root ? path25.resolve(values.root) : process.cwd(), {
3008
4480
  name: values.name,
3009
4481
  gitignore: !values["no-gitignore"]
3010
4482
  });
@@ -3042,6 +4514,17 @@ async function main(argv) {
3042
4514
  }
3043
4515
  case "build":
3044
4516
  return runBuild(projectPaths(values.root), rest, { out: values.out, tar: !!values.tar });
4517
+ case "login":
4518
+ return runLogin();
4519
+ case "logout":
4520
+ return runLogout();
4521
+ case "link":
4522
+ if (rest.length > 1) throw new CliError("Usage: design link [<project>] [--new <name>]");
4523
+ return runLink(projectPaths(values.root), rest[0], { newName: values.new });
4524
+ case "push":
4525
+ return runPush(projectPaths(values.root), rest, { resolved: values.resolved ?? [], json: !!values.json });
4526
+ case "pull":
4527
+ return runPull(projectPaths(values.root), rest, { theirs: values.theirs ?? [], json: !!values.json });
3045
4528
  default:
3046
4529
  throw new CliError(`Unknown command "${command}". Run \`design --help\`.`);
3047
4530
  }