foss-design 0.6.1 → 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,10 +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
  }
3704
+ if (reloads) errors.push(`the page reloaded itself ${reloads === 1 ? "once" : `${reloads} times`} while it loaded`);
3696
3705
  const held = ready ? 0 : await page.evaluate(() => window.__DESIGN_HOLDS__ ?? 0).catch(() => 0);
3697
- await page.waitForTimeout(options.settleMs ?? 700);
3698
3706
  const runtimeErrors = await page.evaluate(() => window.__DESIGN_ERRORS__ ?? []).catch(() => []);
3699
3707
  for (const error of runtimeErrors) if (!errors.includes(error)) errors.push(error);
3700
3708
  const overlay = await page.evaluate(() => {
@@ -4020,8 +4028,17 @@ async function startDevServer(options) {
4020
4028
  allow: [searchForWorkspaceRoot(paths.root), paths.root, paths.design, PKG.root, ...links.allow]
4021
4029
  },
4022
4030
  watch: {
4023
- ignored: ["**/.cache/vite/**", "**/.cache/snapshots/**", "**/.cache/shots/**", "**/.cache/build/**"]
4024
- }
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
4025
4042
  },
4026
4043
  optimizeDeps: {
4027
4044
  entries: entries.files().map((file) => toPosix(path21.relative(paths.design, file)))
@@ -4331,7 +4348,7 @@ async function renderFrames(jobs) {
4331
4348
  try {
4332
4349
  report = await openFrame(page, job.url, { waitForReady: job.waitForReady, settleMs: 300 });
4333
4350
  links = await page.evaluate(
4334
- () => [...document.querySelectorAll("a[href], area[href]")].map((link) => ({
4351
+ () => [...document.querySelectorAll("a[href]:not([download]), area[href]:not([download])")].map((link) => ({
4335
4352
  href: link.getAttribute("href") ?? "",
4336
4353
  text: (link.textContent || link.getAttribute("aria-label") || "").replace(/\s+/g, " ").trim().slice(0, 40)
4337
4354
  }))
@@ -4587,10 +4604,747 @@ async function runLink(paths, wanted, options) {
4587
4604
  else if (remote) print(dim("Everything is in sync."));
4588
4605
  }
4589
4606
 
4590
- // 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
4591
4612
  import fs25 from "fs";
4592
4613
  import path23 from "path";
4593
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
+
4594
5348
  // src/cli/templates.ts
4595
5349
  var TOKENS_CSS = `/*
4596
5350
  * Design tokens. Every screen gets Tailwind v4 with these as utilities:
@@ -4800,40 +5554,40 @@ function mainScreen(canvasId) {
4800
5554
 
4801
5555
  // src/cli/commands/scaffold.ts
4802
5556
  function write(file, content, root) {
4803
- if (fs25.existsSync(file)) {
4804
- print(` ${dim("exists ")} ${path23.relative(root, file)}`);
5557
+ if (fs27.existsSync(file)) {
5558
+ print(` ${dim("exists ")} ${path25.relative(root, file)}`);
4805
5559
  return false;
4806
5560
  }
4807
- fs25.mkdirSync(path23.dirname(file), { recursive: true });
4808
- fs25.writeFileSync(file, content);
4809
- 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)}`);
4810
5564
  return true;
4811
5565
  }
4812
5566
  function ensureGitignored(root) {
4813
- const file = path23.join(root, ".gitignore");
4814
- 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") : "";
4815
5569
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
4816
5570
  if (covered) return "present";
4817
5571
  const prefix = current && !current.endsWith("\n") ? "\n" : "";
4818
- fs25.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
5572
+ fs27.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
4819
5573
  .design/
4820
5574
  `);
4821
5575
  return "added";
4822
5576
  }
4823
5577
  function projectName(root) {
4824
5578
  try {
4825
- 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"));
4826
5580
  if (typeof manifest.name === "string" && manifest.name) return manifest.name;
4827
5581
  } catch {
4828
5582
  }
4829
- return path23.basename(root);
5583
+ return path25.basename(root);
4830
5584
  }
4831
5585
  function runInit(root, options) {
4832
5586
  const paths = designPaths(root);
4833
5587
  print(`Setting up ${DESIGN_DIR} in ${root}`);
4834
5588
  write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
4835
5589
  `, root);
4836
- fs25.mkdirSync(paths.canvases, { recursive: true });
5590
+ fs27.mkdirSync(paths.canvases, { recursive: true });
4837
5591
  if (options.gitignore) {
4838
5592
  const result = ensureGitignored(root);
4839
5593
  print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
@@ -4845,44 +5599,44 @@ function runSystemInit(root, options) {
4845
5599
  const paths = designPaths(root);
4846
5600
  let base = projectName(root);
4847
5601
  try {
4848
- base = JSON.parse(fs25.readFileSync(paths.config, "utf8")).name || base;
5602
+ base = JSON.parse(fs27.readFileSync(paths.config, "utf8")).name || base;
4849
5603
  } catch {
4850
5604
  }
4851
5605
  const name = options.name ?? `${base} design system`;
4852
- print(`Scaffolding the design system in ${path23.relative(root, paths.system)}`);
4853
- 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)}
4854
5608
  `, root);
4855
- write(path23.join(paths.system, "tokens.css"), TOKENS_CSS, root);
5609
+ write(path25.join(paths.system, "tokens.css"), TOKENS_CSS, root);
4856
5610
  if (!options.empty) {
4857
- write(path23.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
4858
- write(path23.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
4859
- 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);
4860
5614
  }
4861
5615
  }
4862
5616
  function runNew(root, id2, options) {
4863
5617
  if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
4864
5618
  const paths = designPaths(root);
4865
- const dir = path23.join(paths.canvases, id2);
4866
- 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}`);
4867
5621
  print(`Creating canvas "${id2}"`);
4868
5622
  const title = options.title ?? titleize(id2);
4869
5623
  if (options.empty) {
4870
5624
  write(
4871
- path23.join(dir, "canvas.json"),
5625
+ path25.join(dir, "canvas.json"),
4872
5626
  `${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
4873
5627
  `,
4874
5628
  root
4875
5629
  );
4876
- fs25.mkdirSync(path23.join(dir, "screens"), { recursive: true });
5630
+ fs27.mkdirSync(path25.join(dir, "screens"), { recursive: true });
4877
5631
  } else {
4878
- write(path23.join(dir, "canvas.json"), canvasJson(title), root);
4879
- 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);
4880
5634
  }
4881
5635
  }
4882
5636
 
4883
5637
  // src/cli/commands/shot.ts
4884
- import fs26 from "fs";
4885
- import path24 from "path";
5638
+ import fs28 from "fs";
5639
+ import path26 from "path";
4886
5640
  var OVERVIEW_TIMEOUT = 9e4;
4887
5641
  async function runShot(paths, target, options) {
4888
5642
  const [canvasId = "", screenId] = target.split("/");
@@ -4898,15 +5652,15 @@ async function runShot(paths, target, options) {
4898
5652
  throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
4899
5653
  }
4900
5654
  const theme2 = options.theme ?? doc.theme ?? "light";
4901
- const outBase = path24.resolve(options.out ?? paths.shots);
4902
- const outDir = path24.join(outBase, canvasId);
4903
- 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 });
4904
5658
  const base = await serverBase(paths);
4905
5659
  const browser = await launchChrome();
4906
5660
  let failed = false;
4907
5661
  try {
4908
5662
  if (options.overview) {
4909
- 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.");
4910
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);
4911
5665
  for (const pageId of pages) {
4912
5666
  const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
@@ -4922,7 +5676,7 @@ async function runShot(paths, target, options) {
4922
5676
  warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
4923
5677
  }
4924
5678
  await page.waitForTimeout(400);
4925
- const file = path24.join(outDir, `${pageId}.overview.png`);
5679
+ const file = path26.join(outDir, `${pageId}.overview.png`);
4926
5680
  await page.screenshot({ path: file });
4927
5681
  await page.close();
4928
5682
  print(file);
@@ -4951,7 +5705,7 @@ async function shootFrames(browser, jobs, outDir) {
4951
5705
  let failed = false;
4952
5706
  const results = await mapLimit(jobs, 4, async (job) => {
4953
5707
  const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
4954
- const file = path24.join(outDir, `${job.id}.${job.theme}.png`);
5708
+ const file = path26.join(outDir, `${job.id}.${job.theme}.png`);
4955
5709
  try {
4956
5710
  const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
4957
5711
  await page.screenshot({ path: file, fullPage: job.fullPage });
@@ -4983,8 +5737,8 @@ async function shootSystem(paths, project, id2, options) {
4983
5737
  if (id2 && !all.some((job) => job.id === id2)) {
4984
5738
  throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
4985
5739
  }
4986
- const outDir = path24.join(path24.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
4987
- fs26.mkdirSync(outDir, { recursive: true });
5740
+ const outDir = path26.join(path26.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
5741
+ fs28.mkdirSync(outDir, { recursive: true });
4988
5742
  const base = await serverBase(paths);
4989
5743
  const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
4990
5744
  const browser = await launchChrome();
@@ -4996,7 +5750,7 @@ async function shootSystem(paths, project, id2, options) {
4996
5750
  }
4997
5751
 
4998
5752
  // src/cli/commands/cloud-status.ts
4999
- import path25 from "path";
5753
+ import path27 from "path";
5000
5754
  var STATE_TEXT = {
5001
5755
  in_sync: green,
5002
5756
  local_changes: yellow,
@@ -5019,7 +5773,7 @@ function detail(paths, status) {
5019
5773
  if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
5020
5774
  if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
5021
5775
  parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
5022
- 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))}`);
5023
5777
  return parts.join(" \xB7 ");
5024
5778
  }
5025
5779
  async function printCloudStatus(paths, label) {
@@ -5134,6 +5888,8 @@ ${bold("Project")}
5134
5888
  init [--name <name>] [--no-gitignore] Create .design and add it to .gitignore
5135
5889
  system init [--name <name>] [--empty] Scaffold .design/system: tokens, a guideline, a component
5136
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
5137
5893
  canvases [--json] Every canvas: pages and screens; with a cloud link also its
5138
5894
  revision, sync state, web link and public link
5139
5895
 
@@ -5193,7 +5949,7 @@ ${bold("Environment")}
5193
5949
  FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
5194
5950
  `;
5195
5951
  function projectPaths(rootOption, requireDesign = true) {
5196
- const found = rootOption ? path26.resolve(rootOption) : findProjectRoot();
5952
+ const found = rootOption ? path28.resolve(rootOption) : findProjectRoot();
5197
5953
  if (!found) {
5198
5954
  if (!requireDesign) return designPaths(process.cwd());
5199
5955
  throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
@@ -5210,6 +5966,7 @@ async function main(argv) {
5210
5966
  root: { type: "string" },
5211
5967
  name: { type: "string" },
5212
5968
  title: { type: "string" },
5969
+ canvas: { type: "string" },
5213
5970
  "no-gitignore": { type: "boolean" },
5214
5971
  empty: { type: "boolean" },
5215
5972
  open: { type: "boolean" },
@@ -5239,7 +5996,7 @@ async function main(argv) {
5239
5996
  if (values.theme && !theme2) throw new CliError("--theme is light or dark");
5240
5997
  switch (command) {
5241
5998
  case "init":
5242
- return runInit(values.root ? path26.resolve(values.root) : process.cwd(), {
5999
+ return runInit(values.root ? path28.resolve(values.root) : process.cwd(), {
5243
6000
  name: values.name,
5244
6001
  gitignore: !values["no-gitignore"]
5245
6002
  });
@@ -5251,6 +6008,10 @@ async function main(argv) {
5251
6008
  if (!rest[0]) throw new CliError("Usage: design new <canvas> [--title <title>]");
5252
6009
  return runNew(projectPaths(values.root).root, rest[0], { title: values.title, empty: !!values.empty });
5253
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
+ }
5254
6015
  case "preview": {
5255
6016
  const openPath = rest[0]?.startsWith("/") ? rest[0] : void 0;
5256
6017
  return runPreview(projectPaths(values.root), {