foss-design 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import path26 from "path";
4
+ import path28 from "path";
5
5
  import { parseArgs } from "util";
6
6
 
7
7
  // src/core/paths.ts
@@ -346,8 +346,8 @@ function routeError(pattern) {
346
346
  }
347
347
  return null;
348
348
  }
349
- function split(path27) {
350
- return path27.split("/").filter(Boolean);
349
+ function split(path29) {
350
+ return path29.split("/").filter(Boolean);
351
351
  }
352
352
  function compileRoute(pattern) {
353
353
  return {
@@ -366,8 +366,8 @@ function decode2(part) {
366
366
  return part;
367
367
  }
368
368
  }
369
- function scoreRoute(route2, path27) {
370
- const parts = split(path27.split(/[?#]/)[0] ?? "").map(decode2);
369
+ function scoreRoute(route2, path29) {
370
+ const parts = split(path29.split(/[?#]/)[0] ?? "").map(decode2);
371
371
  const walk2 = (s, p) => {
372
372
  if (s === route2.segments.length) return p === parts.length ? 0 : null;
373
373
  const segment = route2.segments[s];
@@ -385,11 +385,11 @@ function scoreRoute(route2, path27) {
385
385
  };
386
386
  return walk2(0, 0);
387
387
  }
388
- function matchRoute(entries, path27) {
388
+ function matchRoute(entries, path29) {
389
389
  let best = null;
390
390
  for (const entry of entries) {
391
391
  for (const route2 of entry.routes) {
392
- const score = scoreRoute(route2, path27);
392
+ const score = scoreRoute(route2, path29);
393
393
  if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
394
394
  }
395
395
  }
@@ -512,9 +512,9 @@ var CanvasSchema = z.strictObject({
512
512
  pages: z.array(z.unknown()).min(1, "add at least one page")
513
513
  });
514
514
  var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
515
- function formatPath(base, path27) {
515
+ function formatPath(base, path29) {
516
516
  let out = base;
517
- for (const part of path27) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
517
+ for (const part of path29) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
518
518
  return out;
519
519
  }
520
520
  function describeZodError(error, base = "") {
@@ -1874,12 +1874,12 @@ function rewriteRefs(html, originalDir, stagedDir) {
1874
1874
  return tag;
1875
1875
  return tag.replace(
1876
1876
  /(\s(?:src|href|poster)\s*=\s*)(["'])([^"']*)\2/gi,
1877
- (whole, attr, quote, value) => {
1877
+ (whole, attr2, quote, value) => {
1878
1878
  if (!value || /^(?:[a-z][a-z0-9+.-]*:|\/|#|\{)/i.test(value)) return whole;
1879
1879
  const [pathname = "", suffix = ""] = /^([^?#]*)(.*)$/.exec(value).slice(1);
1880
1880
  const target = path11.resolve(originalDir, decodeURIComponent(pathname));
1881
1881
  if (!fs11.existsSync(target)) return whole;
1882
- return `${attr}${quote}${relUrl(stagedDir, target)}${suffix}${quote}`;
1882
+ return `${attr2}${quote}${relUrl(stagedDir, target)}${suffix}${quote}`;
1883
1883
  }
1884
1884
  );
1885
1885
  });
@@ -2438,7 +2438,7 @@ var CloudClient = class {
2438
2438
  }
2439
2439
  host;
2440
2440
  token;
2441
- async send(method, path27, init) {
2441
+ async send(method, path29, init) {
2442
2442
  const headers = {
2443
2443
  "User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
2444
2444
  Accept: "application/json"
@@ -2446,7 +2446,7 @@ var CloudClient = class {
2446
2446
  if (this.token) headers.Authorization = `Bearer ${this.token}`;
2447
2447
  if (init.type) headers["Content-Type"] = init.type;
2448
2448
  try {
2449
- return await fetch(`${this.host}/api${path27}`, {
2449
+ return await fetch(`${this.host}/api${path29}`, {
2450
2450
  method,
2451
2451
  headers,
2452
2452
  body: init.body,
@@ -2467,8 +2467,8 @@ var CloudClient = class {
2467
2467
  throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
2468
2468
  }
2469
2469
  /** JSON in, JSON out; any non-2xx answer throws a CloudError. */
2470
- async request(method, path27, body) {
2471
- const response = await this.send(method, path27, {
2470
+ async request(method, path29, body) {
2471
+ const response = await this.send(method, path29, {
2472
2472
  body: body === void 0 ? void 0 : JSON.stringify(body),
2473
2473
  type: body === void 0 ? void 0 : "application/json",
2474
2474
  timeout: JSON_TIMEOUT
@@ -2480,7 +2480,7 @@ var CloudClient = class {
2480
2480
  try {
2481
2481
  return JSON.parse(text2);
2482
2482
  } catch {
2483
- throw new CloudError(`${this.host} answered ${method} ${path27} with something other than JSON`, 0, "bad_response");
2483
+ throw new CloudError(`${this.host} answered ${method} ${path29} with something other than JSON`, 0, "bad_response");
2484
2484
  }
2485
2485
  }
2486
2486
  /** POST /device/code. */
@@ -3676,13 +3676,19 @@ async function openFrame(page, url, options = {}) {
3676
3676
  record2(text2);
3677
3677
  });
3678
3678
  let ready = false;
3679
+ let reloads = 0;
3680
+ const onNavigate = (frame) => {
3681
+ if (frame === page.mainFrame()) reloads++;
3682
+ };
3679
3683
  for (let attempt = 0; attempt < 2; attempt++) {
3680
3684
  errors = [];
3685
+ reloads = 0;
3681
3686
  ready = options.waitForReady === false;
3682
3687
  const turnedAway = new Promise((resolve) => {
3683
3688
  outdated = () => resolve("outdated");
3684
3689
  });
3685
3690
  await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
3691
+ page.on("framenavigated", onNavigate);
3686
3692
  if (!ready) {
3687
3693
  const signal = page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
3688
3694
  timeout: options.timeoutMs ?? 2e4
@@ -3691,9 +3697,12 @@ async function openFrame(page, url, options = {}) {
3691
3697
  });
3692
3698
  ready = await Promise.race([signal, turnedAway]).catch(() => null) === "ready";
3693
3699
  }
3694
- if (!errors.some((error) => OUTDATED_DEP.test(error))) break;
3700
+ if (ready || options.waitForReady === false) await page.waitForTimeout(options.settleMs ?? 700);
3701
+ page.off("framenavigated", onNavigate);
3702
+ if (!reloads && !errors.some((error) => OUTDATED_DEP.test(error))) break;
3695
3703
  }
3696
- await page.waitForTimeout(options.settleMs ?? 700);
3704
+ if (reloads) errors.push(`the page reloaded itself ${reloads === 1 ? "once" : `${reloads} times`} while it loaded`);
3705
+ const held = ready ? 0 : await page.evaluate(() => window.__DESIGN_HOLDS__ ?? 0).catch(() => 0);
3697
3706
  const runtimeErrors = await page.evaluate(() => window.__DESIGN_ERRORS__ ?? []).catch(() => []);
3698
3707
  for (const error of runtimeErrors) if (!errors.includes(error)) errors.push(error);
3699
3708
  const overlay = await page.evaluate(() => {
@@ -3704,7 +3713,7 @@ async function openFrame(page, url, options = {}) {
3704
3713
  const unique2 = errors.filter(
3705
3714
  (error, index) => error && !errors.some((other, j) => j !== index && other && other.length < error.length && error.includes(other))
3706
3715
  );
3707
- return { errors: [...new Set(unique2)], ready };
3716
+ return { errors: [...new Set(unique2)], ready, ...held ? { held } : {} };
3708
3717
  }
3709
3718
 
3710
3719
  // src/server/state.ts
@@ -4019,8 +4028,17 @@ async function startDevServer(options) {
4019
4028
  allow: [searchForWorkspaceRoot(paths.root), paths.root, paths.design, PKG.root, ...links.allow]
4020
4029
  },
4021
4030
  watch: {
4022
- ignored: ["**/.cache/vite/**", "**/.cache/snapshots/**", "**/.cache/shots/**", "**/.cache/build/**"]
4023
- }
4031
+ // `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
4032
+ // generated entries are modules the frames load, so those stay watched.
4033
+ ignored: (file) => {
4034
+ const rel = toPosix(path21.relative(paths.cache, file));
4035
+ const inCache = rel !== "" && !rel.startsWith("..") && !path21.isAbsolute(rel);
4036
+ return inCache && rel !== "entries" && !rel.startsWith("entries/");
4037
+ }
4038
+ },
4039
+ // Vite forwards frames' console warnings to the server when an agent runs it; the runtime
4040
+ // already reports errors, and the log they would fill sits in `.cache`.
4041
+ forwardConsole: false
4024
4042
  },
4025
4043
  optimizeDeps: {
4026
4044
  entries: entries.files().map((file) => toPosix(path21.relative(paths.design, file)))
@@ -4330,7 +4348,7 @@ async function renderFrames(jobs) {
4330
4348
  try {
4331
4349
  report = await openFrame(page, job.url, { waitForReady: job.waitForReady, settleMs: 300 });
4332
4350
  links = await page.evaluate(
4333
- () => [...document.querySelectorAll("a[href], area[href]")].map((link) => ({
4351
+ () => [...document.querySelectorAll("a[href]:not([download]), area[href]:not([download])")].map((link) => ({
4334
4352
  href: link.getAttribute("href") ?? "",
4335
4353
  text: (link.textContent || link.getAttribute("aria-label") || "").replace(/\s+/g, " ").trim().slice(0, 40)
4336
4354
  }))
@@ -4341,7 +4359,7 @@ async function renderFrames(jobs) {
4341
4359
  await page.close().catch(() => {
4342
4360
  });
4343
4361
  }
4344
- return { key: job.key, url: job.url, ready: report.ready, errors: report.errors, links };
4362
+ return { key: job.key, url: job.url, ready: report.ready, held: report.held, errors: report.errors, links };
4345
4363
  });
4346
4364
  } finally {
4347
4365
  await browser.close();
@@ -4384,7 +4402,8 @@ async function runCheck(paths, canvases, options) {
4384
4402
  for (const frame of frames) {
4385
4403
  const ok = frame.ready && !frame.errors.length;
4386
4404
  print(`${ok ? green("\u2713") : red("\u2717")} ${frame.key} ${dim(frame.url)}`);
4387
- if (!frame.ready) print(` ${red("did not finish rendering (no ready signal within 20s)")}`);
4405
+ if (frame.held) print(` ${red(`holdReady() was not released within 20s (${frame.held} still held)`)}`);
4406
+ else if (!frame.ready) print(` ${red("did not finish rendering (no ready signal within 20s)")}`);
4388
4407
  for (const error of frame.errors) print(` ${red(error.split("\n")[0])}`);
4389
4408
  }
4390
4409
  }
@@ -4585,10 +4604,747 @@ async function runLink(paths, wanted, options) {
4585
4604
  else if (remote) print(dim("Everything is in sync."));
4586
4605
  }
4587
4606
 
4588
- // src/cli/commands/scaffold.ts
4607
+ // src/cli/commands/import.ts
4608
+ import fs26 from "fs";
4609
+ import path24 from "path";
4610
+
4611
+ // src/import/claude-design.ts
4589
4612
  import fs25 from "fs";
4590
4613
  import path23 from "path";
4591
4614
 
4615
+ // src/import/markup.ts
4616
+ var VOID = /* @__PURE__ */ new Set([
4617
+ "area",
4618
+ "base",
4619
+ "br",
4620
+ "col",
4621
+ "embed",
4622
+ "hr",
4623
+ "img",
4624
+ "input",
4625
+ "link",
4626
+ "meta",
4627
+ "source",
4628
+ "track",
4629
+ "wbr"
4630
+ ]);
4631
+ var RAW = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
4632
+ var ATTR = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/y;
4633
+ function parseMarkup(source) {
4634
+ const root = { type: "element", tag: "#root", attrs: [], children: [] };
4635
+ const stack = [root];
4636
+ const top = () => stack[stack.length - 1];
4637
+ let i = 0;
4638
+ while (i < source.length) {
4639
+ const lt = source.indexOf("<", i);
4640
+ if (lt === -1) {
4641
+ top().children.push({ type: "text", text: source.slice(i) });
4642
+ break;
4643
+ }
4644
+ if (lt > i) top().children.push({ type: "text", text: source.slice(i, lt) });
4645
+ if (source.startsWith("<!--", lt)) {
4646
+ const end = source.indexOf("-->", lt + 4);
4647
+ i = end === -1 ? source.length : end + 3;
4648
+ continue;
4649
+ }
4650
+ if (source[lt + 1] === "!" || source[lt + 1] === "?") {
4651
+ const end = source.indexOf(">", lt);
4652
+ i = end === -1 ? source.length : end + 1;
4653
+ continue;
4654
+ }
4655
+ if (source[lt + 1] === "/") {
4656
+ const end = source.indexOf(">", lt);
4657
+ const tag = source.slice(lt + 2, end === -1 ? source.length : end).trim();
4658
+ i = end === -1 ? source.length : end + 1;
4659
+ for (let depth = stack.length - 1; depth > 0; depth--) {
4660
+ if (stack[depth].tag.toLowerCase() === tag.toLowerCase()) {
4661
+ stack.length = depth;
4662
+ break;
4663
+ }
4664
+ }
4665
+ continue;
4666
+ }
4667
+ const name = /^[A-Za-z][\w:-]*/.exec(source.slice(lt + 1));
4668
+ if (!name) {
4669
+ top().children.push({ type: "text", text: "<" });
4670
+ i = lt + 1;
4671
+ continue;
4672
+ }
4673
+ const element = { type: "element", tag: name[0], attrs: [], children: [] };
4674
+ let at = lt + 1 + name[0].length;
4675
+ let selfClosing = false;
4676
+ for (; ; ) {
4677
+ while (at < source.length && /\s/.test(source[at])) at++;
4678
+ if (at >= source.length) break;
4679
+ if (source[at] === ">") {
4680
+ at++;
4681
+ break;
4682
+ }
4683
+ if (source.startsWith("/>", at)) {
4684
+ selfClosing = true;
4685
+ at += 2;
4686
+ break;
4687
+ }
4688
+ ATTR.lastIndex = at;
4689
+ const match = ATTR.exec(source);
4690
+ if (!match) {
4691
+ at++;
4692
+ continue;
4693
+ }
4694
+ element.attrs.push([match[1], match[2] ?? match[3] ?? match[4] ?? null]);
4695
+ at = ATTR.lastIndex;
4696
+ }
4697
+ top().children.push(element);
4698
+ const lower = element.tag.toLowerCase();
4699
+ if (RAW.has(lower) && !selfClosing) {
4700
+ const close = source.toLowerCase().indexOf(`</${lower}`, at);
4701
+ const end = close === -1 ? source.length : close;
4702
+ element.children.push({ type: "text", text: source.slice(at, end) });
4703
+ const gt = close === -1 ? -1 : source.indexOf(">", close);
4704
+ i = gt === -1 ? source.length : gt + 1;
4705
+ continue;
4706
+ }
4707
+ if (!selfClosing && !VOID.has(lower)) stack.push(element);
4708
+ i = at;
4709
+ }
4710
+ return root;
4711
+ }
4712
+ function findElement(node, test) {
4713
+ for (const child of node.children) {
4714
+ if (child.type !== "element") continue;
4715
+ if (test(child)) return child;
4716
+ const found = findElement(child, test);
4717
+ if (found) return found;
4718
+ }
4719
+ return null;
4720
+ }
4721
+ var attr = (element, name) => element.attrs.find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
4722
+ var NAMED = {
4723
+ amp: "&",
4724
+ lt: "<",
4725
+ gt: ">",
4726
+ quot: '"',
4727
+ apos: "'",
4728
+ nbsp: "\xA0",
4729
+ thinsp: "\u2009",
4730
+ ensp: "\u2002",
4731
+ emsp: "\u2003",
4732
+ mdash: "\u2014",
4733
+ ndash: "\u2013",
4734
+ minus: "\u2212",
4735
+ hellip: "\u2026",
4736
+ middot: "\xB7",
4737
+ bull: "\u2022",
4738
+ laquo: "\xAB",
4739
+ raquo: "\xBB",
4740
+ lsquo: "\u2018",
4741
+ rsquo: "\u2019",
4742
+ ldquo: "\u201C",
4743
+ rdquo: "\u201D",
4744
+ times: "\xD7",
4745
+ divide: "\xF7",
4746
+ deg: "\xB0",
4747
+ copy: "\xA9",
4748
+ reg: "\xAE",
4749
+ trade: "\u2122",
4750
+ euro: "\u20AC",
4751
+ pound: "\xA3",
4752
+ yen: "\xA5",
4753
+ cent: "\xA2",
4754
+ sect: "\xA7",
4755
+ para: "\xB6",
4756
+ plusmn: "\xB1",
4757
+ le: "\u2264",
4758
+ ge: "\u2265",
4759
+ ne: "\u2260",
4760
+ larr: "\u2190",
4761
+ rarr: "\u2192",
4762
+ uarr: "\u2191",
4763
+ darr: "\u2193",
4764
+ harr: "\u2194",
4765
+ check: "\u2713",
4766
+ zwj: "\u200D",
4767
+ zwnj: "\u200C",
4768
+ shy: "\xAD"
4769
+ };
4770
+ function decodeEntities(text2) {
4771
+ return text2.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]*);/gi, (whole, body) => {
4772
+ if (body[0] === "#") {
4773
+ const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number(body.slice(1));
4774
+ return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
4775
+ }
4776
+ return NAMED[body.toLowerCase()] ?? whole;
4777
+ });
4778
+ }
4779
+
4780
+ // src/import/claude-design.ts
4781
+ var EXT = ".dc.html";
4782
+ var kebab = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
4783
+ var identifier = (name) => {
4784
+ const clean = name.replace(/[^\w$]/g, "_");
4785
+ return /^[A-Za-z_$]/.test(clean) ? clean : `_${clean}`;
4786
+ };
4787
+ var KEYWORDS = new Set(
4788
+ "break case catch class const continue debugger default delete do else export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static await async of undefined NaN Infinity arguments eval Math JSON Number String Boolean Object Array Date Intl RegExp Error Map Set Symbol Promise BigInt console window document globalThis location navigator parseInt parseFloat isNaN isFinite encodeURIComponent decodeURIComponent encodeURI decodeURI setTimeout clearTimeout setInterval clearInterval".split(" ")
4789
+ );
4790
+ function freeNames(expression, into) {
4791
+ const code = expression.replace(/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/g, '""').replace(
4792
+ /`(?:\\.|\$\{([^}]*)\}|[^`\\])*`/g,
4793
+ (whole) => [...whole.matchAll(/\$\{([^}]*)\}/g)].map((match) => ` ${match[1]} `).join("")
4794
+ );
4795
+ for (const match of code.matchAll(/(?<![.\w$])[A-Za-z_$][\w$]*/g)) {
4796
+ const name = match[0];
4797
+ const after = code.slice(match.index + name.length);
4798
+ if (/^\s*:(?!:)/.test(after) && /[{,]\s*$/.test(code.slice(0, match.index))) continue;
4799
+ if (!KEYWORDS.has(name)) into.add(name);
4800
+ }
4801
+ }
4802
+ var WHOLE = /^\s*\{\{([\s\S]+?)\}\}\s*$/;
4803
+ var PARTS = /\{\{([\s\S]+?)\}\}/g;
4804
+ var RENAME = {
4805
+ class: "className",
4806
+ for: "htmlFor",
4807
+ tabindex: "tabIndex",
4808
+ readonly: "readOnly",
4809
+ maxlength: "maxLength",
4810
+ minlength: "minLength",
4811
+ colspan: "colSpan",
4812
+ rowspan: "rowSpan",
4813
+ autocomplete: "autoComplete",
4814
+ autocapitalize: "autoCapitalize",
4815
+ autocorrect: "autoCorrect",
4816
+ autosave: "autoSave",
4817
+ enterkeyhint: "enterKeyHint",
4818
+ accesskey: "accessKey",
4819
+ charset: "charSet",
4820
+ formaction: "formAction",
4821
+ formmethod: "formMethod",
4822
+ formnovalidate: "formNoValidate",
4823
+ formtarget: "formTarget",
4824
+ hreflang: "hrefLang",
4825
+ itemprop: "itemProp",
4826
+ itemscope: "itemScope",
4827
+ itemtype: "itemType",
4828
+ marginheight: "marginHeight",
4829
+ marginwidth: "marginWidth",
4830
+ mediagroup: "mediaGroup",
4831
+ nomodule: "noModule",
4832
+ radiogroup: "radioGroup",
4833
+ srcdoc: "srcDoc",
4834
+ srclang: "srcLang",
4835
+ popovertarget: "popoverTarget",
4836
+ popovertargetaction: "popoverTargetAction",
4837
+ autofocus: "autoFocus",
4838
+ autoplay: "autoPlay",
4839
+ crossorigin: "crossOrigin",
4840
+ srcset: "srcSet",
4841
+ enctype: "encType",
4842
+ novalidate: "noValidate",
4843
+ contenteditable: "contentEditable",
4844
+ spellcheck: "spellCheck",
4845
+ datetime: "dateTime",
4846
+ allowfullscreen: "allowFullScreen",
4847
+ frameborder: "frameBorder",
4848
+ cellpadding: "cellPadding",
4849
+ cellspacing: "cellSpacing",
4850
+ inputmode: "inputMode",
4851
+ usemap: "useMap",
4852
+ referrerpolicy: "referrerPolicy",
4853
+ playsinline: "playsInline",
4854
+ viewbox: "viewBox",
4855
+ "accept-charset": "acceptCharset",
4856
+ "http-equiv": "httpEquiv",
4857
+ "xlink:href": "xlinkHref",
4858
+ "xmlns:xlink": "xmlnsXlink",
4859
+ "xml:space": "xmlSpace",
4860
+ "xml:lang": "xmlLang"
4861
+ };
4862
+ var BOOLEAN = /* @__PURE__ */ new Set([
4863
+ "checked",
4864
+ "disabled",
4865
+ "selected",
4866
+ "readonly",
4867
+ "hidden",
4868
+ "open",
4869
+ "required",
4870
+ "multiple",
4871
+ "autofocus",
4872
+ "novalidate",
4873
+ "playsinline",
4874
+ "muted",
4875
+ "loop",
4876
+ "autoplay",
4877
+ "controls",
4878
+ "default",
4879
+ "reversed",
4880
+ "allowfullscreen",
4881
+ "async",
4882
+ "defer",
4883
+ "inert"
4884
+ ]);
4885
+ var NO_TEXT = /* @__PURE__ */ new Set(["table", "thead", "tbody", "tfoot", "tr", "colgroup", "select", "optgroup", "datalist"]);
4886
+ function propName(name) {
4887
+ const lower = name.toLowerCase();
4888
+ if (RENAME[lower]) return RENAME[lower];
4889
+ if (/^on[A-Z]/.test(name)) return name;
4890
+ if (lower.startsWith("aria-") || lower.startsWith("data-")) return lower;
4891
+ if (!/^[A-Za-z][\w:-]*$/.test(name)) return null;
4892
+ return name.includes("-") ? name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) : name;
4893
+ }
4894
+ var str2 = (value) => JSON.stringify(value);
4895
+ function interpolated(raw, reads) {
4896
+ if (!raw.includes("{{")) return str2(decodeEntities(raw));
4897
+ let out = "`";
4898
+ let last = 0;
4899
+ for (const match of raw.matchAll(PARTS)) {
4900
+ out += decodeEntities(raw.slice(last, match.index)).replace(/[`\\]|\$\{/g, (c) => `\\${c}`);
4901
+ freeNames(match[1], reads);
4902
+ out += "${dc.text(" + match[1].trim() + ")}";
4903
+ last = match.index + match[0].length;
4904
+ }
4905
+ return `${out + decodeEntities(raw.slice(last)).replace(/[`\\]|\$\{/g, (c) => `\\${c}`)}\``;
4906
+ }
4907
+ function linkTarget(value, ctx) {
4908
+ const match = /^(?:\.\/)?([^/?#]+)\.dc\.html(#.*)?$/.exec(value);
4909
+ if (!match) return null;
4910
+ const route2 = ctx.routes.get(match[1]);
4911
+ if (route2) return route2;
4912
+ ctx.warn(`links to ${match[1]}${EXT}, which is no board; the link goes nowhere (#)`);
4913
+ return "#";
4914
+ }
4915
+ function attrsJsx(element, ctx, extra = []) {
4916
+ const tag = element.tag.toLowerCase();
4917
+ const handled = new Set(element.attrs.map(([name]) => name));
4918
+ const hasHandler = [...handled].some((name) => /^on(Change|Input)$/.test(name));
4919
+ const out = [...extra];
4920
+ for (const [name, raw] of element.attrs) {
4921
+ const lower = name.toLowerCase();
4922
+ if (lower.startsWith("hint-") || lower === "xmlns") continue;
4923
+ let prop = propName(name);
4924
+ if (!prop) {
4925
+ ctx.warn(`dropped the attribute "${name}"`);
4926
+ continue;
4927
+ }
4928
+ if (!hasHandler && (tag === "input" || tag === "textarea") && (prop === "value" || prop === "checked"))
4929
+ prop = prop === "value" ? "defaultValue" : "defaultChecked";
4930
+ if (tag === "option" && prop === "selected") continue;
4931
+ if (raw === null) {
4932
+ out.push(BOOLEAN.has(lower) ? prop : `${prop}=""`);
4933
+ continue;
4934
+ }
4935
+ const whole = WHOLE.exec(raw);
4936
+ if (prop === "style") {
4937
+ const body = interpolated(raw, ctx.reads);
4938
+ out.push(`style={dc.css(${body})}`);
4939
+ continue;
4940
+ }
4941
+ if (whole) {
4942
+ const expression = whole[1].trim();
4943
+ freeNames(expression, ctx.reads);
4944
+ if (/^on[A-Z]/.test(prop)) out.push(`${prop}={${expression}}`);
4945
+ else if (BOOLEAN.has(lower)) out.push(`${prop}={dc.flag(${expression})}`);
4946
+ else out.push(`${prop}={dc.value(${expression})}`);
4947
+ continue;
4948
+ }
4949
+ if (/^on[a-z]/.test(name)) {
4950
+ ctx.warn(`dropped the inline handler ${name}="\u2026"; handlers come from renderVals()`);
4951
+ continue;
4952
+ }
4953
+ if (BOOLEAN.has(lower)) {
4954
+ out.push(prop);
4955
+ continue;
4956
+ }
4957
+ if (lower === "href" && !raw.includes("{{")) {
4958
+ const target = linkTarget(raw, ctx);
4959
+ if (target !== null) {
4960
+ out.push(`href=${str2(target)}`);
4961
+ continue;
4962
+ }
4963
+ }
4964
+ if (raw.includes("/_blob/"))
4965
+ ctx.warn("uses an uploaded Claude Design asset (/_blob/\u2026) that the export does not contain");
4966
+ out.push(`${prop}={${interpolated(raw, ctx.reads)}}`);
4967
+ }
4968
+ return out.length ? ` ${out.join(" ")}` : "";
4969
+ }
4970
+ function selectedOption(select) {
4971
+ const option = findElement(select, (el) => el.tag.toLowerCase() === "option" && attr(el, "selected") !== void 0);
4972
+ if (!option) return null;
4973
+ const value = attr(option, "value");
4974
+ if (value !== void 0 && value !== null) return value;
4975
+ return option.children.map((child) => child.type === "text" ? child.text : "").join("").trim();
4976
+ }
4977
+ function childrenJsx(nodes, ctx, parent) {
4978
+ const out = [];
4979
+ for (const node of nodes) {
4980
+ if (node.type === "text") {
4981
+ if (!node.text.trim()) {
4982
+ if (node.text && !NO_TEXT.has(parent)) out.push('{" "}');
4983
+ continue;
4984
+ }
4985
+ if (NO_TEXT.has(parent)) continue;
4986
+ let last = 0;
4987
+ for (const match of node.text.matchAll(PARTS)) {
4988
+ const before = node.text.slice(last, match.index);
4989
+ if (before) out.push(`{${str2(decodeEntities(before))}}`);
4990
+ freeNames(match[1], ctx.reads);
4991
+ out.push(`{dc.text(${match[1].trim()})}`);
4992
+ last = match.index + match[0].length;
4993
+ }
4994
+ const rest = node.text.slice(last);
4995
+ if (rest) out.push(`{${str2(decodeEntities(rest))}}`);
4996
+ continue;
4997
+ }
4998
+ out.push(elementJsx(node, ctx));
4999
+ }
5000
+ while (out[0] === '{" "}') out.shift();
5001
+ while (out[out.length - 1] === '{" "}') out.pop();
5002
+ return out.join("");
5003
+ }
5004
+ function elementJsx(element, ctx) {
5005
+ const tag = element.tag;
5006
+ const lower = tag.toLowerCase();
5007
+ if (lower === "sc-for") {
5008
+ const list = attr(element, "list") ?? "[]";
5009
+ const as = attr(element, "as") || "item";
5010
+ const expression = (WHOLE.exec(list)?.[1] ?? list).trim();
5011
+ freeNames(expression, ctx.reads);
5012
+ const body = childrenJsx(element.children, ctx, lower);
5013
+ return `{dc.list(${expression}).map((${identifier(as)}, index) => (<dc.Fragment key={index}>${body}</dc.Fragment>))}`;
5014
+ }
5015
+ if (lower === "sc-if") {
5016
+ const value = attr(element, "value") ?? "false";
5017
+ const expression = (WHOLE.exec(value)?.[1] ?? value).trim();
5018
+ freeNames(expression, ctx.reads);
5019
+ return `{dc.flag(${expression}) ? (<>${childrenJsx(element.children, ctx, lower)}</>) : null}`;
5020
+ }
5021
+ if (lower === "dc-import") {
5022
+ const name = attr(element, "name") ?? "";
5023
+ if (!ctx.known.has(name)) {
5024
+ ctx.warn(`imports ${name}${EXT}, which is not in the export`);
5025
+ return "";
5026
+ }
5027
+ const component = `DC_${identifier(name)}`;
5028
+ ctx.imports.add(name);
5029
+ const props = element.attrs.filter(([key]) => key !== "name" && !key.startsWith("hint-")).map(([key, raw]) => {
5030
+ if (raw === null) return `${str2(key)}: true`;
5031
+ const whole = WHOLE.exec(raw);
5032
+ if (whole) {
5033
+ freeNames(whole[1], ctx.reads);
5034
+ return `${str2(key)}: (${whole[1].trim()})`;
5035
+ }
5036
+ return `${str2(key)}: ${interpolated(raw, ctx.reads)}`;
5037
+ });
5038
+ return `<${component} {...{${props.join(", ")}}} />`;
5039
+ }
5040
+ if (lower === "script") {
5041
+ ctx.warn("dropped a <script> inside the template");
5042
+ return "";
5043
+ }
5044
+ if (lower === "title" || lower === "helmet") return "";
5045
+ if (lower === "style") {
5046
+ const css2 = element.children.map((child) => child.type === "text" ? child.text : "").join("");
5047
+ return `<style>{${interpolated(css2, ctx.reads)}}</style>`;
5048
+ }
5049
+ if (lower === "textarea") {
5050
+ const text2 = element.children.map((child) => child.type === "text" ? child.text : "").join("");
5051
+ const extra2 = text2 ? [`defaultValue={${interpolated(text2, ctx.reads)}}`] : [];
5052
+ return `<textarea${attrsJsx(element, ctx, extra2)} />`;
5053
+ }
5054
+ const extra = [];
5055
+ if (lower === "select") {
5056
+ const selected = selectedOption(element);
5057
+ if (selected !== null && attr(element, "value") === void 0)
5058
+ extra.push(`defaultValue={${interpolated(selected, ctx.reads)}}`);
5059
+ }
5060
+ const attrs = attrsJsx(element, ctx, extra);
5061
+ const children = childrenJsx(element.children, ctx, lower);
5062
+ return children ? `<${tag}${attrs}>${children}</${tag}>` : `<${tag}${attrs} />`;
5063
+ }
5064
+ function convertFile(name, source, ctx) {
5065
+ const document2 = parseMarkup(source);
5066
+ const root = findElement(document2, (el) => el.tag.toLowerCase() === "x-dc");
5067
+ const script = findElement(
5068
+ document2,
5069
+ (el) => el.tag.toLowerCase() === "script" && attr(el, "data-dc-script") !== void 0
5070
+ );
5071
+ const file = { ...ctx, name, reads: /* @__PURE__ */ new Set(), imports: /* @__PURE__ */ new Set() };
5072
+ if (!root) throw new Error("no <x-dc> element");
5073
+ const helmet = /<helmet>([\s\S]*?)<\/helmet>/i.exec(source)?.[1]?.trim() ?? "";
5074
+ const body = childrenJsx(root.children, file, "x-dc");
5075
+ let spec = {};
5076
+ const rawSpec = script ? attr(script, "data-props") : null;
5077
+ if (rawSpec) {
5078
+ try {
5079
+ spec = JSON.parse(decodeEntities(rawSpec));
5080
+ } catch {
5081
+ file.warn("has data-props that are not JSON; the props keep no defaults");
5082
+ }
5083
+ }
5084
+ const code = script?.children.map((child) => child.type === "text" ? child.text : "").join("") ?? "";
5085
+ const className = /class\s+([A-Za-z_$][\w$]*)\s+extends\s+DCLogic/.exec(code)?.[1];
5086
+ const logic = className ? `const __Logic = (() => {
5087
+ ${code.trim()}
5088
+ return ${className}
5089
+ })()` : "const __Logic = null";
5090
+ const reads = [...file.reads].filter((read) => !read.startsWith("__") && read !== "dc").sort();
5091
+ const lines = [
5092
+ `// Imported from Claude Design (${name}${EXT}).`,
5093
+ `import * as dc from './dc'`,
5094
+ ...[...file.imports].sort().map((dep) => `import DC_${identifier(dep)} from './${dep}'`),
5095
+ `const DCLogic = dc.DCLogic`,
5096
+ "",
5097
+ helmet ? `dc.head(${str2(helmet)})
5098
+ ` : "",
5099
+ `const __spec = ${JSON.stringify(spec)}`,
5100
+ "",
5101
+ logic,
5102
+ "",
5103
+ `export default function ${identifier(name)}(__props) {`,
5104
+ ` const __scope = dc.useLogic(__Logic, __spec, __props)`,
5105
+ reads.length ? ` const { ${reads.join(", ")} } = __scope` : "",
5106
+ ` return (<>${body}</>)`,
5107
+ "}",
5108
+ ""
5109
+ ];
5110
+ return lines.filter((line, index) => line !== "" || lines[index - 1] !== "").join("\n");
5111
+ }
5112
+ var DC_HELPERS = `// Imported from Claude Design: what the converted screens need. This file is the canvas's own.
5113
+ import { Fragment, useReducer, useRef } from 'react'
5114
+ import { useTheme } from '@design/runtime'
5115
+
5116
+ export { Fragment }
5117
+
5118
+ /** The base of each screen's logic class: props, state and setState, as Claude Design had them. */
5119
+ export class DCLogic {
5120
+ constructor(props) {
5121
+ this.props = props
5122
+ this.state = {}
5123
+ }
5124
+ setState(patch) {
5125
+ const next = typeof patch === 'function' ? patch(this.state, this.props) : patch
5126
+ this.state = { ...this.state, ...next }
5127
+ this.__update?.()
5128
+ }
5129
+ renderVals() {
5130
+ return {}
5131
+ }
5132
+ }
5133
+
5134
+ const themed = (spec) => Array.isArray(spec?.theme?.options) && spec.theme.options.includes('dark')
5135
+
5136
+ function convert(field, value) {
5137
+ if (!field || typeof value !== 'string') return value
5138
+ if (field.editor === 'int' || field.editor === 'number') return Number(value)
5139
+ if (field.editor === 'bool' || field.editor === 'boolean') return value === 'true' || value === ''
5140
+ return value
5141
+ }
5142
+
5143
+ /** Props (defaults, then what the canvas or a parent passed), the logic's values, for the template. */
5144
+ export function useLogic(Logic, spec, given) {
5145
+ const theme = useTheme()
5146
+ const [, update] = useReducer((n) => n + 1, 0)
5147
+ const props = {}
5148
+ for (const [key, field] of Object.entries(spec ?? {}))
5149
+ if (!key.startsWith('$') && field && typeof field === 'object' && 'default' in field) props[key] = field.default
5150
+ for (const [key, value] of Object.entries(given ?? {})) props[key] = convert(spec?.[key], value)
5151
+ // A theme prop follows the viewer's light/dark switch unless the canvas pins it.
5152
+ if (themed(spec) && given?.theme === undefined) props.theme = theme
5153
+ const logic = useRef(null)
5154
+ if (Logic && !logic.current) logic.current = new Logic(props)
5155
+ let vals = {}
5156
+ if (logic.current) {
5157
+ logic.current.props = props
5158
+ logic.current.__update = update
5159
+ vals = logic.current.renderVals?.() ?? {}
5160
+ }
5161
+ return { props, ...props, ...vals }
5162
+ }
5163
+
5164
+ const heads = new Set()
5165
+ /** The template's <helmet>: fonts and styles, added to the page once. */
5166
+ export function head(html) {
5167
+ if (heads.has(html) || typeof document === 'undefined') return
5168
+ heads.add(html)
5169
+ document.head.insertAdjacentHTML('beforeend', html)
5170
+ }
5171
+
5172
+ /** An inline style string as a React style object. */
5173
+ export function css(text) {
5174
+ const style = {}
5175
+ let depth = 0
5176
+ let quote = ''
5177
+ let start = 0
5178
+ const declarations = []
5179
+ for (let i = 0; i <= text.length; i++) {
5180
+ const c = text[i]
5181
+ if (quote) {
5182
+ if (c === quote) quote = ''
5183
+ } else if (c === '"' || c === "'") quote = c
5184
+ else if (c === '(') depth++
5185
+ else if (c === ')') depth--
5186
+ else if ((c === ';' && depth === 0) || i === text.length) {
5187
+ declarations.push(text.slice(start, i))
5188
+ start = i + 1
5189
+ }
5190
+ }
5191
+ for (const declaration of declarations) {
5192
+ const colon = declaration.indexOf(':')
5193
+ if (colon === -1) continue
5194
+ const name = declaration.slice(0, colon).trim()
5195
+ const value = declaration.slice(colon + 1).trim()
5196
+ if (!name || !value) continue
5197
+ const key = name.startsWith('--') ? name : name.toLowerCase().replace(/^-ms-/, 'ms-').replace(/-([a-z])/g, (_, c) => c.toUpperCase())
5198
+ style[key] = value
5199
+ }
5200
+ return style
5201
+ }
5202
+
5203
+ /** A template value in text: nothing for null, undefined and false. */
5204
+ export const text = (value) => (value == null || value === false ? '' : value)
5205
+ /** An attribute value: left out for null, undefined and false. */
5206
+ export const value = (v) => (v == null || v === false ? undefined : v)
5207
+ /** A boolean attribute or condition. */
5208
+ export const flag = (v) => v === true || v === 'true' || (!!v && v !== 'false')
5209
+ /** What a loop runs over. */
5210
+ export const list = (v) => (Array.isArray(v) ? v : v == null ? [] : Array.from(v))
5211
+ `;
5212
+ function boardId(file, used) {
5213
+ let base = kebab(file.slice(0, -EXT.length)) || "screen";
5214
+ if (!ID_PATTERN.test(base)) base = `s-${base}`;
5215
+ let id2 = base;
5216
+ for (let n2 = 2; used.has(id2); n2++) id2 = `${base}-${n2}`;
5217
+ used.add(id2);
5218
+ return id2;
5219
+ }
5220
+ function importClaudeDesign(dir, options = {}) {
5221
+ const canvasFile = path23.join(dir, "canvas.json");
5222
+ if (!fs25.existsSync(canvasFile))
5223
+ throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
5224
+ const source = JSON.parse(fs25.readFileSync(canvasFile, "utf8"));
5225
+ const templates = fs25.readdirSync(dir).filter((file) => file.endsWith(EXT));
5226
+ if (!templates.length) throw new Error(`No ${EXT} files in ${dir}`);
5227
+ const known = new Set(templates.map((file) => file.slice(0, -EXT.length)));
5228
+ const warnings = /* @__PURE__ */ new Map();
5229
+ const boards = source.boards ?? {};
5230
+ const order = [...(source.order ?? []).filter((file) => file in boards), ...Object.keys(boards)];
5231
+ const used = /* @__PURE__ */ new Set();
5232
+ const ids = /* @__PURE__ */ new Map();
5233
+ const routes = /* @__PURE__ */ new Map();
5234
+ for (const file of new Set(order)) {
5235
+ if (!known.has(file.slice(0, -EXT.length))) continue;
5236
+ const id2 = boardId(file, used);
5237
+ ids.set(file, id2);
5238
+ routes.set(file.slice(0, -EXT.length), `/${id2}`);
5239
+ }
5240
+ const files = /* @__PURE__ */ new Map();
5241
+ files.set("dc.jsx", DC_HELPERS);
5242
+ for (const file of templates) {
5243
+ const name = file.slice(0, -EXT.length);
5244
+ const warn2 = (message) => {
5245
+ const set = warnings.get(message) ?? /* @__PURE__ */ new Set();
5246
+ set.add(file);
5247
+ warnings.set(message, set);
5248
+ };
5249
+ try {
5250
+ files.set(
5251
+ `${name}.jsx`,
5252
+ convertFile(name, fs25.readFileSync(path23.join(dir, file), "utf8"), { known, routes, warn: warn2 })
5253
+ );
5254
+ } catch (error) {
5255
+ warn2(`could not be converted: ${error.message}`);
5256
+ }
5257
+ }
5258
+ const pageList = source.pages?.length ? source.pages : [{ id: "main", name: source.title ?? "Main" }];
5259
+ const pages = pageList.map((page, index) => ({
5260
+ id: ID_PATTERN.test(page.id) ? page.id : `page-${index + 1}`,
5261
+ title: page.name || page.id,
5262
+ items: []
5263
+ }));
5264
+ const pageOf = (id2) => pages[Math.max(
5265
+ 0,
5266
+ pageList.findIndex((page) => page.id === id2)
5267
+ )];
5268
+ let notes = 0;
5269
+ for (const [key, note] of Object.entries(source.notes ?? {})) {
5270
+ if (!note.text) continue;
5271
+ const heading = note.kind === "title1" ? "# " : note.kind === "title2" ? "## " : note.kind === "title3" ? "### " : "";
5272
+ pageOf(note.page).items.push({
5273
+ id: `note-${boardId(`${key}${EXT}`, used)}`,
5274
+ type: "note",
5275
+ tone: heading ? "plain" : "note",
5276
+ text: `${heading}${note.text}`,
5277
+ width: Math.round(Math.min(Math.max(note.maxW ?? note.w ?? 480, 80), 4e3)),
5278
+ x: note.x ?? 0,
5279
+ y: note.y ?? 0
5280
+ });
5281
+ notes++;
5282
+ }
5283
+ let screens = 0;
5284
+ for (const [file, id2] of ids) {
5285
+ const board = boards[file];
5286
+ if (!files.has(`${file.slice(0, -EXT.length)}.jsx`)) continue;
5287
+ pageOf(board.page).items.push({
5288
+ id: id2,
5289
+ src: `screens/${file.slice(0, -EXT.length)}.jsx`,
5290
+ title: board.title || file.slice(0, -EXT.length),
5291
+ route: routes.get(file.slice(0, -EXT.length)),
5292
+ width: Math.round(Math.min(Math.max(board.w ?? 1440, 80), 8e3)),
5293
+ height: Math.round(Math.min(Math.max(board.h ?? 900, 80), 4e4)),
5294
+ x: board.x ?? 0,
5295
+ y: board.y ?? 0
5296
+ });
5297
+ screens++;
5298
+ }
5299
+ const canvas = {
5300
+ title: options.title ?? source.title ?? path23.basename(path23.resolve(dir)),
5301
+ description: "Imported from Claude Design.",
5302
+ // The templates bring their own styles; the design system's reset would restyle them.
5303
+ system: false,
5304
+ pages: pages.filter((page) => page.items.length)
5305
+ };
5306
+ return { canvas, files, screens, notes, warnings };
5307
+ }
5308
+
5309
+ // src/cli/commands/import.ts
5310
+ var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
5311
+ function runImport(paths, from, options) {
5312
+ const dir = path24.resolve(from);
5313
+ if (!fs26.existsSync(dir) || !fs26.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
5314
+ let result;
5315
+ try {
5316
+ result = importClaudeDesign(dir, { title: options.title });
5317
+ } catch (error) {
5318
+ throw new CliError(error.message);
5319
+ }
5320
+ const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path24.basename(dir)) || "imported");
5321
+ if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is no canvas id; pass one with --canvas <id>`);
5322
+ const out = path24.join(paths.canvases, id2);
5323
+ if (fs26.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
5324
+ fs26.mkdirSync(path24.join(out, "screens"), { recursive: true });
5325
+ for (const [name, content] of result.files) fs26.writeFileSync(path24.join(out, "screens", name), content);
5326
+ fs26.writeFileSync(path24.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
5327
+ `);
5328
+ print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
5329
+ print(
5330
+ ` ${result.screens} screens and ${result.notes} notes on ${result.canvas.pages.length} pages; ${result.files.size - 1} templates as React modules in screens/, helpers in screens/dc.jsx`
5331
+ );
5332
+ if (result.warnings.size) {
5333
+ print();
5334
+ for (const [message, files] of result.warnings) {
5335
+ const list = [...files];
5336
+ print(`${yellow("warning")} ${message}`);
5337
+ print(` ${dim(`${list.slice(0, 6).join(", ")}${list.length > 6 ? ` and ${list.length - 6} more` : ""}`)}`);
5338
+ }
5339
+ }
5340
+ print();
5341
+ print(`Next: \`design check ${id2} --render\`, then \`design preview --open /c/${id2}\`.`);
5342
+ }
5343
+
5344
+ // src/cli/commands/scaffold.ts
5345
+ import fs27 from "fs";
5346
+ import path25 from "path";
5347
+
4592
5348
  // src/cli/templates.ts
4593
5349
  var TOKENS_CSS = `/*
4594
5350
  * Design tokens. Every screen gets Tailwind v4 with these as utilities:
@@ -4798,40 +5554,40 @@ function mainScreen(canvasId) {
4798
5554
 
4799
5555
  // src/cli/commands/scaffold.ts
4800
5556
  function write(file, content, root) {
4801
- if (fs25.existsSync(file)) {
4802
- print(` ${dim("exists ")} ${path23.relative(root, file)}`);
5557
+ if (fs27.existsSync(file)) {
5558
+ print(` ${dim("exists ")} ${path25.relative(root, file)}`);
4803
5559
  return false;
4804
5560
  }
4805
- fs25.mkdirSync(path23.dirname(file), { recursive: true });
4806
- fs25.writeFileSync(file, content);
4807
- print(` ${green("created")} ${path23.relative(root, file)}`);
5561
+ fs27.mkdirSync(path25.dirname(file), { recursive: true });
5562
+ fs27.writeFileSync(file, content);
5563
+ print(` ${green("created")} ${path25.relative(root, file)}`);
4808
5564
  return true;
4809
5565
  }
4810
5566
  function ensureGitignored(root) {
4811
- const file = path23.join(root, ".gitignore");
4812
- const current = fs25.existsSync(file) ? fs25.readFileSync(file, "utf8") : "";
5567
+ const file = path25.join(root, ".gitignore");
5568
+ const current = fs27.existsSync(file) ? fs27.readFileSync(file, "utf8") : "";
4813
5569
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
4814
5570
  if (covered) return "present";
4815
5571
  const prefix = current && !current.endsWith("\n") ? "\n" : "";
4816
- fs25.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
5572
+ fs27.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
4817
5573
  .design/
4818
5574
  `);
4819
5575
  return "added";
4820
5576
  }
4821
5577
  function projectName(root) {
4822
5578
  try {
4823
- const manifest = JSON.parse(fs25.readFileSync(path23.join(root, "package.json"), "utf8"));
5579
+ const manifest = JSON.parse(fs27.readFileSync(path25.join(root, "package.json"), "utf8"));
4824
5580
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
4825
5581
  } catch {
4826
5582
  }
4827
- return path23.basename(root);
5583
+ return path25.basename(root);
4828
5584
  }
4829
5585
  function runInit(root, options) {
4830
5586
  const paths = designPaths(root);
4831
5587
  print(`Setting up ${DESIGN_DIR} in ${root}`);
4832
5588
  write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
4833
5589
  `, root);
4834
- fs25.mkdirSync(paths.canvases, { recursive: true });
5590
+ fs27.mkdirSync(paths.canvases, { recursive: true });
4835
5591
  if (options.gitignore) {
4836
5592
  const result = ensureGitignored(root);
4837
5593
  print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
@@ -4843,44 +5599,44 @@ function runSystemInit(root, options) {
4843
5599
  const paths = designPaths(root);
4844
5600
  let base = projectName(root);
4845
5601
  try {
4846
- base = JSON.parse(fs25.readFileSync(paths.config, "utf8")).name || base;
5602
+ base = JSON.parse(fs27.readFileSync(paths.config, "utf8")).name || base;
4847
5603
  } catch {
4848
5604
  }
4849
5605
  const name = options.name ?? `${base} design system`;
4850
- print(`Scaffolding the design system in ${path23.relative(root, paths.system)}`);
4851
- write(path23.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
5606
+ print(`Scaffolding the design system in ${path25.relative(root, paths.system)}`);
5607
+ write(path25.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
4852
5608
  `, root);
4853
- write(path23.join(paths.system, "tokens.css"), TOKENS_CSS, root);
5609
+ write(path25.join(paths.system, "tokens.css"), TOKENS_CSS, root);
4854
5610
  if (!options.empty) {
4855
- write(path23.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
4856
- write(path23.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
4857
- write(path23.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
5611
+ write(path25.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
5612
+ write(path25.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
5613
+ write(path25.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
4858
5614
  }
4859
5615
  }
4860
5616
  function runNew(root, id2, options) {
4861
5617
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
4862
5618
  const paths = designPaths(root);
4863
- const dir = path23.join(paths.canvases, id2);
4864
- if (fs25.existsSync(path23.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
5619
+ const dir = path25.join(paths.canvases, id2);
5620
+ if (fs27.existsSync(path25.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
4865
5621
  print(`Creating canvas "${id2}"`);
4866
5622
  const title = options.title ?? titleize(id2);
4867
5623
  if (options.empty) {
4868
5624
  write(
4869
- path23.join(dir, "canvas.json"),
5625
+ path25.join(dir, "canvas.json"),
4870
5626
  `${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
4871
5627
  `,
4872
5628
  root
4873
5629
  );
4874
- fs25.mkdirSync(path23.join(dir, "screens"), { recursive: true });
5630
+ fs27.mkdirSync(path25.join(dir, "screens"), { recursive: true });
4875
5631
  } else {
4876
- write(path23.join(dir, "canvas.json"), canvasJson(title), root);
4877
- write(path23.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
5632
+ write(path25.join(dir, "canvas.json"), canvasJson(title), root);
5633
+ write(path25.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
4878
5634
  }
4879
5635
  }
4880
5636
 
4881
5637
  // src/cli/commands/shot.ts
4882
- import fs26 from "fs";
4883
- import path24 from "path";
5638
+ import fs28 from "fs";
5639
+ import path26 from "path";
4884
5640
  var OVERVIEW_TIMEOUT = 9e4;
4885
5641
  async function runShot(paths, target, options) {
4886
5642
  const [canvasId = "", screenId] = target.split("/");
@@ -4896,15 +5652,15 @@ async function runShot(paths, target, options) {
4896
5652
  throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
4897
5653
  }
4898
5654
  const theme2 = options.theme ?? doc.theme ?? "light";
4899
- const outBase = path24.resolve(options.out ?? paths.shots);
4900
- const outDir = path24.join(outBase, canvasId);
4901
- fs26.mkdirSync(outDir, { recursive: true });
5655
+ const outBase = path26.resolve(options.out ?? paths.shots);
5656
+ const outDir = path26.join(outBase, canvasId);
5657
+ fs28.mkdirSync(outDir, { recursive: true });
4902
5658
  const base = await serverBase(paths);
4903
5659
  const browser = await launchChrome();
4904
5660
  let failed = false;
4905
5661
  try {
4906
5662
  if (options.overview) {
4907
- if (!fs26.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
5663
+ if (!fs28.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
4908
5664
  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);
4909
5665
  for (const pageId of pages) {
4910
5666
  const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
@@ -4920,7 +5676,7 @@ async function runShot(paths, target, options) {
4920
5676
  warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
4921
5677
  }
4922
5678
  await page.waitForTimeout(400);
4923
- const file = path24.join(outDir, `${pageId}.overview.png`);
5679
+ const file = path26.join(outDir, `${pageId}.overview.png`);
4924
5680
  await page.screenshot({ path: file });
4925
5681
  await page.close();
4926
5682
  print(file);
@@ -4949,7 +5705,7 @@ async function shootFrames(browser, jobs, outDir) {
4949
5705
  let failed = false;
4950
5706
  const results = await mapLimit(jobs, 4, async (job) => {
4951
5707
  const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
4952
- const file = path24.join(outDir, `${job.id}.${job.theme}.png`);
5708
+ const file = path26.join(outDir, `${job.id}.${job.theme}.png`);
4953
5709
  try {
4954
5710
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
4955
5711
  await page.screenshot({ path: file, fullPage: job.fullPage });
@@ -4981,8 +5737,8 @@ async function shootSystem(paths, project, id2, options) {
4981
5737
  if (id2 && !all.some((job) => job.id === id2)) {
4982
5738
  throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
4983
5739
  }
4984
- const outDir = path24.join(path24.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
4985
- fs26.mkdirSync(outDir, { recursive: true });
5740
+ const outDir = path26.join(path26.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
5741
+ fs28.mkdirSync(outDir, { recursive: true });
4986
5742
  const base = await serverBase(paths);
4987
5743
  const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
4988
5744
  const browser = await launchChrome();
@@ -4994,7 +5750,7 @@ async function shootSystem(paths, project, id2, options) {
4994
5750
  }
4995
5751
 
4996
5752
  // src/cli/commands/cloud-status.ts
4997
- import path25 from "path";
5753
+ import path27 from "path";
4998
5754
  var STATE_TEXT = {
4999
5755
  in_sync: green,
5000
5756
  local_changes: yellow,
@@ -5017,7 +5773,7 @@ function detail(paths, status) {
5017
5773
  if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
5018
5774
  if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
5019
5775
  parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
5020
- if (status.conflictRev !== void 0) parts.push(`merge ${path25.relative(paths.root, incomingDir(paths, status.key))}`);
5776
+ if (status.conflictRev !== void 0) parts.push(`merge ${path27.relative(paths.root, incomingDir(paths, status.key))}`);
5021
5777
  return parts.join(" \xB7 ");
5022
5778
  }
5023
5779
  async function printCloudStatus(paths, label) {
@@ -5132,6 +5888,8 @@ ${bold("Project")}
5132
5888
  init [--name <name>] [--no-gitignore] Create .design and add it to .gitignore
5133
5889
  system init [--name <name>] [--empty] Scaffold .design/system: tokens, a guideline, a component
5134
5890
  new <canvas> [--title <title>] [--empty] Scaffold .design/canvas/<canvas>
5891
+ import <folder> [--canvas <id>] Turn an exported Claude Design project (canvas.json and
5892
+ [--title <title>] *.dc.html) into a canvas of React screens, once
5135
5893
  canvases [--json] Every canvas: pages and screens; with a cloud link also its
5136
5894
  revision, sync state, web link and public link
5137
5895
 
@@ -5191,7 +5949,7 @@ ${bold("Environment")}
5191
5949
  FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
5192
5950
  `;
5193
5951
  function projectPaths(rootOption, requireDesign = true) {
5194
- const found = rootOption ? path26.resolve(rootOption) : findProjectRoot();
5952
+ const found = rootOption ? path28.resolve(rootOption) : findProjectRoot();
5195
5953
  if (!found) {
5196
5954
  if (!requireDesign) return designPaths(process.cwd());
5197
5955
  throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
@@ -5208,6 +5966,7 @@ async function main(argv) {
5208
5966
  root: { type: "string" },
5209
5967
  name: { type: "string" },
5210
5968
  title: { type: "string" },
5969
+ canvas: { type: "string" },
5211
5970
  "no-gitignore": { type: "boolean" },
5212
5971
  empty: { type: "boolean" },
5213
5972
  open: { type: "boolean" },
@@ -5237,7 +5996,7 @@ async function main(argv) {
5237
5996
  if (values.theme && !theme2) throw new CliError("--theme is light or dark");
5238
5997
  switch (command) {
5239
5998
  case "init":
5240
- return runInit(values.root ? path26.resolve(values.root) : process.cwd(), {
5999
+ return runInit(values.root ? path28.resolve(values.root) : process.cwd(), {
5241
6000
  name: values.name,
5242
6001
  gitignore: !values["no-gitignore"]
5243
6002
  });
@@ -5249,6 +6008,10 @@ async function main(argv) {
5249
6008
  if (!rest[0]) throw new CliError("Usage: design new <canvas> [--title <title>]");
5250
6009
  return runNew(projectPaths(values.root).root, rest[0], { title: values.title, empty: !!values.empty });
5251
6010
  }
6011
+ case "import": {
6012
+ if (!rest[0]) throw new CliError("Usage: design import <folder> [--canvas <id>] [--title <title>]");
6013
+ return runImport(projectPaths(values.root), rest[0], { canvas: values.canvas, title: values.title });
6014
+ }
5252
6015
  case "preview": {
5253
6016
  const openPath = rest[0]?.startsWith("/") ? rest[0] : void 0;
5254
6017
  return runPreview(projectPaths(values.root), {