@clovis500c/figma-bridge 1.12.0 → 1.14.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/server.js CHANGED
@@ -46,8 +46,175 @@ var __esm = (fn, res, err) => () => {
46
46
  };
47
47
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
48
48
 
49
+ // src/setup.ts
50
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
51
+ import { dirname, join } from "node:path";
52
+ function clientList(home, appData) {
53
+ return [
54
+ { id: "claude-code", label: "Claude Code", files: [join(home, ".claude.json")], format: "json", extra: { type: "stdio" } },
55
+ { id: "claude-desktop", label: "Claude Desktop", files: [join(appData, "Claude", "claude_desktop_config.json")], format: "json" },
56
+ { id: "codex", label: "Codex", files: [join(home, ".codex", "config.toml")], format: "toml" },
57
+ {
58
+ id: "antigravity",
59
+ label: "Antigravity",
60
+ files: [join(home, ".gemini", "antigravity", "mcp_config.json"), join(home, ".gemini", "config", "mcp_config.json")],
61
+ format: "json"
62
+ },
63
+ { id: "gemini-cli", label: "Gemini CLI", files: [join(home, ".gemini", "settings.json")], format: "json" },
64
+ { id: "cursor", label: "Cursor", files: [join(home, ".cursor", "mcp.json")], format: "json" },
65
+ { id: "windsurf", label: "Windsurf", files: [join(home, ".codeium", "windsurf", "mcp_config.json")], format: "json" }
66
+ ];
67
+ }
68
+ function backup(path, text) {
69
+ writeFileSync(path + ".bak-figma-bridge", text);
70
+ }
71
+ function writeJson(path, entry) {
72
+ const text = existsSync(path) ? readFileSync(path, "utf8") : "";
73
+ const config = text.trim() ? JSON.parse(text) : {};
74
+ if (text.trim())
75
+ backup(path, text);
76
+ config.mcpServers = { ...config.mcpServers, [SERVER_NAME]: entry };
77
+ mkdirSync(dirname(path), { recursive: true });
78
+ writeFileSync(path, JSON.stringify(config, null, 2) + `
79
+ `);
80
+ }
81
+ function tomlBlock(command, args) {
82
+ return [
83
+ `[mcp_servers.${SERVER_NAME}]`,
84
+ `command = '${command}'`,
85
+ `args = [${args.map((a) => `'${a}'`).join(", ")}]`,
86
+ "startup_timeout_sec = 60",
87
+ "tool_timeout_sec = 120"
88
+ ].join(`
89
+ `);
90
+ }
91
+ function writeToml(path, command, args) {
92
+ const text = existsSync(path) ? readFileSync(path, "utf8") : "";
93
+ if (text.trim())
94
+ backup(path, text);
95
+ const kept = [];
96
+ let skipping = false;
97
+ for (const line of text.split(/\r?\n/)) {
98
+ const header = /^\s*\[([^\]]+)\]\s*$/.exec(line);
99
+ if (header)
100
+ skipping = header[1] === `mcp_servers.${SERVER_NAME}` || header[1].startsWith(`mcp_servers.${SERVER_NAME}.`);
101
+ if (!skipping)
102
+ kept.push(line);
103
+ }
104
+ const body = kept.join(`
105
+ `).trimEnd();
106
+ mkdirSync(dirname(path), { recursive: true });
107
+ writeFileSync(path, (body ? body + `
108
+
109
+ ` : "") + tomlBlock(command, args) + `
110
+ `);
111
+ }
112
+ function runSetup(opts) {
113
+ const all = clientList(opts.home, opts.appData);
114
+ const forced = opts.clients ?? [];
115
+ const unknown = forced.filter((id) => !all.some((c) => c.id === id));
116
+ if (unknown.length)
117
+ throw new Error(`Unknown client(s): ${unknown.join(", ")}. Known: ${all.map((c) => c.id).join(", ")}`);
118
+ const result = { updated: [], failed: [], missing: [] };
119
+ for (const client of all) {
120
+ const detected = client.files.filter((f) => existsSync(dirname(f)) && (client.id !== "gemini-cli" || existsSync(f)));
121
+ const targets = forced.length ? forced.includes(client.id) ? detected.length ? detected : [client.files[0]] : [] : detected;
122
+ if (!targets.length) {
123
+ if (!forced.length)
124
+ result.missing.push(client.label);
125
+ continue;
126
+ }
127
+ for (const file of targets) {
128
+ try {
129
+ if (client.format === "toml")
130
+ writeToml(file, opts.command, opts.args);
131
+ else
132
+ writeJson(file, { ...client.extra, command: opts.command, args: opts.args });
133
+ result.updated.push({ client: client.id, label: client.label, file });
134
+ } catch (e) {
135
+ result.failed.push({ client: client.id, label: client.label, file, error: e.message });
136
+ }
137
+ }
138
+ }
139
+ return result;
140
+ }
141
+ function installPlugin(from, to, version) {
142
+ mkdirSync(to, { recursive: true });
143
+ for (const f of PLUGIN_FILES)
144
+ copyFileSync(join(from, f), join(to, f));
145
+ if (version)
146
+ writeFileSync(join(to, "version.json"), JSON.stringify({ version }) + `
147
+ `);
148
+ return join(to, "manifest.json");
149
+ }
150
+ function compareVersions(a, b) {
151
+ const pa = a.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
152
+ const pb = b.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
153
+ for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
154
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
155
+ if (d)
156
+ return d;
157
+ }
158
+ return 0;
159
+ }
160
+ function syncPlugin(from, to, version) {
161
+ if (!PLUGIN_FILES.every((f) => existsSync(join(from, f))))
162
+ return "skipped";
163
+ const had = existsSync(join(to, "manifest.json"));
164
+ let installed = "";
165
+ try {
166
+ installed = JSON.parse(readFileSync(join(to, "version.json"), "utf8")).version || "";
167
+ } catch {}
168
+ if (installed) {
169
+ const order = compareVersions(installed, version);
170
+ if (order > 0)
171
+ return "newer";
172
+ if (order === 0 && PLUGIN_FILES.every((f) => existsSync(join(to, f))))
173
+ return "current";
174
+ }
175
+ installPlugin(from, to, version);
176
+ return had ? "updated" : "installed";
177
+ }
178
+ function printSetup(r, rerun) {
179
+ for (const label of r.missing)
180
+ console.log(dim(` – ${label.padEnd(15)} not found`));
181
+ for (const u of r.updated)
182
+ console.log(green(` ✔ ${u.label.padEnd(15)}`) + dim(u.file));
183
+ for (const f of r.failed)
184
+ console.log(yellow(` ! ${f.label.padEnd(15)}could not update ${f.file}: ${f.error}`));
185
+ if (!r.updated.length)
186
+ console.log(yellow(`
187
+ No AI client found. Use \`${rerun} --client <name>\` or \`${rerun} --print\` for manual config.`));
188
+ }
189
+ function printSnippets(command, args) {
190
+ console.log(bold("JSON clients") + dim(" (Claude, Antigravity, Gemini CLI, Cursor, Windsurf…)"));
191
+ console.log(JSON.stringify({ mcpServers: { [SERVER_NAME]: { command, args } } }, null, 2));
192
+ console.log(bold(`
193
+ Codex`) + dim(" (~/.codex/config.toml)"));
194
+ console.log(tomlBlock(command, args) + `
195
+ `);
196
+ }
197
+ function parseClients(argv) {
198
+ const out = [];
199
+ argv.forEach((a, i) => {
200
+ if (a === "--client" && argv[i + 1])
201
+ out.push(...argv[i + 1].split(","));
202
+ else if (a.startsWith("--client="))
203
+ out.push(...a.slice(9).split(","));
204
+ });
205
+ return out;
206
+ }
207
+ var SERVER_NAME = "FigmaBridge", PLUGIN_FILES, color = (code) => (s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s, bold, green, dim, yellow;
208
+ var init_setup = __esm(() => {
209
+ PLUGIN_FILES = ["manifest.json", "code.js", "ui.html"];
210
+ bold = color(1);
211
+ green = color(32);
212
+ dim = color(2);
213
+ yellow = color(33);
214
+ });
215
+
49
216
  // package.json
50
- var version = "1.12.0";
217
+ var version = "1.14.0";
51
218
  var init_package = () => {};
52
219
 
53
220
  // node_modules/zod/v3/helpers/util.js
@@ -27221,13 +27388,13 @@ Turn ${opt(a.target, "the selected frame")} into code for the project at ${opt(a
27221
27388
  name: "figma-to-roblox",
27222
27389
  title: "Figma to Roblox",
27223
27390
  description: "Turn a frame into Roblox UI and place it in Studio with a Roblox Studio MCP.",
27224
- args: { target: "Layer name or id (default: the selection)", mode: "scale, offset or hybrid (default hybrid)" },
27391
+ args: { target: "Layer name or id (default: the selection)", mode: "scale (default), offset or hybrid" },
27225
27392
  render: (a) => `
27226
27393
  Turn ${opt(a.target, "the selected frame")} into Roblox UI that looks the same, and put it in the open Roblox Studio place.
27227
- 1. export_roblox {${a.target ? `nodeId:"<id of ${a.target}>", ` : ""}mode:"${opt(a.mode, "hybrid")}"}. Read the warnings (approximations, font substitutions).
27394
+ 1. export_roblox {${a.target ? `nodeId:"<id of ${a.target}>", ` : ""}mode:"${opt(a.mode, "scale")}"}. Read the warnings (approximations, font substitutions).
27228
27395
  2. If assets are not uploaded (placeholders rbxassetid://PENDING_n): upload each file in assets[] with the Roblox Studio MCP (e.g. upload_image), and replace each placeholder in the Luau with the returned id.
27229
27396
  3. Run the Luau with execute_luau: it creates the ScreenGui under StarterGui (replacing an earlier copy) and returns it.
27230
- 4. Take a Studio screenshot and compare it with screenshot {returnImage:true} of the Figma frame. Fix differences in the Luau (or in Figma, then export again).
27397
+ 4. Take a Studio screenshot and compare it with screenshot {returnImage:true} of the Figma frame. Fix differences in the Luau (or in Figma, then export again), keeping every size, position, padding and gap in scale (no offsets) and the PascalCase names.
27231
27398
  5. Report the result and anything Roblox could not reproduce.`
27232
27399
  },
27233
27400
  {
@@ -27246,6 +27413,218 @@ Audit and improve this file${a.target ? ` (fixes limited to ${a.target})` : ""}.
27246
27413
  ];
27247
27414
  });
27248
27415
 
27416
+ // plugin/lib/naming.ts
27417
+ function isGenericName(n) {
27418
+ const name = n.name.trim();
27419
+ if (!name || GENERIC_NAME.test(name))
27420
+ return true;
27421
+ if (n.kind === "text" && n.text !== undefined) {
27422
+ const t = n.text.trim();
27423
+ const a = name.toLowerCase();
27424
+ const b = t.toLowerCase();
27425
+ return a === b || a.length >= 12 && b.indexOf(a) === 0;
27426
+ }
27427
+ return false;
27428
+ }
27429
+ function visibleKids(n) {
27430
+ return n.children.filter(function(c) {
27431
+ return !c.hidden;
27432
+ });
27433
+ }
27434
+ function maxFont(n) {
27435
+ let m = n.kind === "text" && n.fontSize ? n.fontSize : 0;
27436
+ for (let i = 0;i < n.children.length; i++) {
27437
+ const c = n.children[i];
27438
+ if (c && !c.hidden)
27439
+ m = Math.max(m, maxFont(c));
27440
+ }
27441
+ return m;
27442
+ }
27443
+ function onlyKinds(list, kinds) {
27444
+ for (let i = 0;i < list.length; i++) {
27445
+ const c = list[i];
27446
+ if (c && kinds.indexOf(c.kind) === -1)
27447
+ return false;
27448
+ }
27449
+ return list.length > 0;
27450
+ }
27451
+ function buttonLike(n) {
27452
+ const kids = visibleKids(n);
27453
+ if (!n.background || n.h > 72 || kids.length === 0 || kids.length > 3)
27454
+ return false;
27455
+ let texts = 0;
27456
+ for (let i = 0;i < kids.length; i++) {
27457
+ const c = kids[i];
27458
+ if (!c)
27459
+ continue;
27460
+ if (c.kind === "text") {
27461
+ texts++;
27462
+ if ((c.text || "").length > 32)
27463
+ return false;
27464
+ } else if (c.kind !== "vector" && c.kind !== "image")
27465
+ return false;
27466
+ }
27467
+ return texts === 1;
27468
+ }
27469
+ function isScreen(n) {
27470
+ return SCREEN_W.indexOf(Math.round(n.w)) !== -1 && n.h >= 640 || n.w >= 1024 && n.h >= 600;
27471
+ }
27472
+ function largestText(n, parent) {
27473
+ const others = parent ? visibleKids(parent).filter(function(c) {
27474
+ return c !== n && c.kind === "text";
27475
+ }) : [];
27476
+ const size = n.fontSize || 14;
27477
+ return others.length > 0 && others.every(function(c) {
27478
+ return (c.fontSize || 14) < size;
27479
+ });
27480
+ }
27481
+ function covers(n, p) {
27482
+ return Math.abs(n.x) <= 1 && Math.abs(n.y) <= 1 && n.w >= p.w - 1 && n.h >= p.h - 1;
27483
+ }
27484
+ function roleOf(n, parent, root, top, inButton) {
27485
+ const kids = visibleKids(n);
27486
+ if (n.kind === "text") {
27487
+ const t = (n.text || "").trim();
27488
+ const size = n.fontSize || 14;
27489
+ if (inButton)
27490
+ return "Label";
27491
+ if (NUMERIC.test(t))
27492
+ return "Value";
27493
+ if (size >= top - 0.5 && size >= 18)
27494
+ return "Title";
27495
+ if (size >= 16 && largestText(n, parent))
27496
+ return "Title";
27497
+ if (size >= 20)
27498
+ return "Heading";
27499
+ if (t.length > 60)
27500
+ return "Description";
27501
+ if (size <= 12)
27502
+ return "Caption";
27503
+ if (t.length > 30)
27504
+ return "Description";
27505
+ return "Label";
27506
+ }
27507
+ if (n.kind === "vector")
27508
+ return "Icon";
27509
+ if (n.kind === "image" && kids.length === 0) {
27510
+ const round = n.ellipse || n.radius !== undefined && Math.abs(n.w - n.h) <= 1 && n.radius >= n.w / 2 - 1;
27511
+ return round ? "Avatar" : "Image";
27512
+ }
27513
+ if (kids.length === 0) {
27514
+ if (Math.min(n.w, n.h) <= 2 && Math.max(n.w, n.h) > 8)
27515
+ return "Divider";
27516
+ if (parent && covers(n, parent))
27517
+ return "Background";
27518
+ if (n.clickable)
27519
+ return "Button";
27520
+ if (n.ellipse)
27521
+ return n.w <= 16 ? "Dot" : "Circle";
27522
+ return "Shape";
27523
+ }
27524
+ if (n.clickable)
27525
+ return "Button";
27526
+ if (buttonLike(n)) {
27527
+ const label = kids.filter(function(c) {
27528
+ return c.kind === "text";
27529
+ })[0];
27530
+ return n.h <= 24 || label && (label.fontSize || 14) <= 12 ? "Badge" : "Button";
27531
+ }
27532
+ if (!parent) {
27533
+ if (isScreen(n))
27534
+ return "Screen";
27535
+ } else if (parent === root && isScreen(root)) {
27536
+ if (n.w >= root.w * 0.9 && n.y <= root.h * 0.1 && n.h <= root.h * 0.25)
27537
+ return "Header";
27538
+ if (n.w >= root.w * 0.9 && n.y + n.h >= root.h * 0.9 && n.h <= root.h * 0.25)
27539
+ return "Footer";
27540
+ if (n.h >= root.h * 0.9 && n.w <= root.w * 0.35 && n.x <= 1)
27541
+ return "Sidebar";
27542
+ }
27543
+ const flow = kids.filter(function(c) {
27544
+ return !c.absolute;
27545
+ });
27546
+ if (flow.length >= 2) {
27547
+ const r = baseName(flow[0]);
27548
+ const same = flow.every(function(c) {
27549
+ return baseName(c) === r;
27550
+ });
27551
+ if (same && r && r !== "Label" && r !== "Value" && r !== "Divider") {
27552
+ if (r === "Button")
27553
+ return "Actions";
27554
+ return r + (n.layout === "GRID" || n.wrap ? "Grid" : "List");
27555
+ }
27556
+ }
27557
+ if (n.background)
27558
+ return n.radius ? "Card" : "Panel";
27559
+ if (onlyKinds(kids, ["text"]))
27560
+ return "TextGroup";
27561
+ if (n.layout === "HORIZONTAL")
27562
+ return "Row";
27563
+ if (n.layout === "GRID")
27564
+ return "Grid";
27565
+ return "Container";
27566
+ }
27567
+ function baseName(n) {
27568
+ return pascalName(n.role || n.name).replace(/\d+$/, "");
27569
+ }
27570
+ function inferRoles(root) {
27571
+ const top = maxFont(root);
27572
+ const walk = function(n, parent, inButton) {
27573
+ const button = n.kind !== "text" && (n.clickable || buttonLike(n) || /button|btn/i.test(n.name));
27574
+ for (let i = 0;i < n.children.length; i++) {
27575
+ const c = n.children[i];
27576
+ if (c)
27577
+ walk(c, n, inButton || button);
27578
+ }
27579
+ if (isGenericName(n))
27580
+ n.role = roleOf(n, parent, root, top, inButton);
27581
+ };
27582
+ walk(root, null, false);
27583
+ }
27584
+ function pascalName(name) {
27585
+ let segs = name.split("/").map(function(s) {
27586
+ return s.replace(/^[\w-]+:/, "").trim();
27587
+ }).filter(function(s) {
27588
+ return s && !/^default$/i.test(s);
27589
+ });
27590
+ if (segs.length > 1)
27591
+ segs = segs.slice(1).concat([segs[0]]);
27592
+ const words = segs.join(" ").normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
27593
+ const out = words.map(function(w) {
27594
+ return w === w.toUpperCase() && w.length <= 4 ? w : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
27595
+ }).join("");
27596
+ return out.slice(0, 48);
27597
+ }
27598
+ function endsWithWord(name, words) {
27599
+ for (let i = 0;i < words.length; i++) {
27600
+ const w = words[i];
27601
+ if (name.length >= w.length && name.slice(-w.length) === w)
27602
+ return true;
27603
+ }
27604
+ return false;
27605
+ }
27606
+ function uniqueSiblingNames(names) {
27607
+ const counts = {};
27608
+ for (let i = 0;i < names.length; i++) {
27609
+ const k = names[i];
27610
+ counts[k] = (counts[k] || 0) + 1;
27611
+ }
27612
+ const seen = {};
27613
+ return names.map(function(k) {
27614
+ if ((counts[k] || 0) < 2)
27615
+ return k;
27616
+ seen[k] = (seen[k] || 0) + 1;
27617
+ return k + seen[k];
27618
+ });
27619
+ }
27620
+ var GENERIC_NAME, NUMERIC, SCREEN_W, ROLE_WORDS;
27621
+ var init_naming = __esm(() => {
27622
+ GENERIC_NAME = /^(frame|rectangle|ellipse|group|vector|text|line|polygon|star|component|instance|section|image|auto ?layout|union|subtract|intersect|exclude|boolean|layer|shape|slice|mask|container|wrapper|div|span|box)(\s*\d+)?(\s+copy(\s*\d+)?)?$/i;
27623
+ NUMERIC = /^[\s$€£¥+\-−]*[\d][\d\s.,:%/×x+\-−]*[\s$€£¥%kKmMbB]*$/;
27624
+ SCREEN_W = [360, 375, 390, 393, 402, 412, 414, 428, 430, 440];
27625
+ ROLE_WORDS = ("Actions Avatar Background Badge Banner Bar Body Button Card Column Container Content Controls Details Dialog Divider Footer Frame Grid Group Gui " + "Header Hero Hud HUD Info Item List Menu Modal Nav Navbar Navigation Overlay Panel Popup Row Screen Section Sidebar Slot Stack Stats Tab Tabs Tile " + "Toolbar Tooltip Window Wrapper").split(" ");
27626
+ });
27627
+
27249
27628
  // src/roblox/fonts.ts
27250
27629
  function robloxFamily(family, overrides = {}) {
27251
27630
  const key = family.trim().toLowerCase();
@@ -27350,26 +27729,24 @@ function visible(list) {
27350
27729
  function decide(n, opts) {
27351
27730
  const approximations = [];
27352
27731
  if (n.text)
27353
- return { kind: "text", shadow: false, approximations };
27732
+ return { kind: "text", approximations };
27354
27733
  const raster = opts.rasterize;
27355
27734
  const fills = visible(n.fills);
27356
27735
  const strokes = visible(n.strokes);
27357
- const effects = n.effects ?? [];
27358
- const drop = effects.filter((e) => e.type === "DROP_SHADOW");
27359
- const other = effects.filter((e) => e.type !== "DROP_SHADOW");
27736
+ const other = (n.effects ?? []).filter((e) => e.type !== "DROP_SHADOW");
27360
27737
  const hasChildren = !!n.children?.some((c) => !c.hidden);
27361
- if (n.vector || ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"].includes(n.type)) {
27362
- return raster === "none" ? { kind: "frame", shadow: false, approximations: ["vector shape skipped (rasterize is none)"] } : { kind: "picture", shadow: false, approximations };
27738
+ if (n.vector || VECTOR_TYPES.includes(n.type)) {
27739
+ return raster === "none" ? { kind: "frame", approximations: ["vector shape skipped (rasterize is none)"] } : { kind: "picture", approximations };
27363
27740
  }
27364
27741
  const image = fills.length === 1 && fills[0].type === "IMAGE";
27365
- if (image && !hasChildren && !strokes.length && !effects.length && raster !== "all")
27366
- return { kind: "image", shadow: false, approximations };
27742
+ if (image && !hasChildren && !strokes.length && !other.length && raster !== "all")
27743
+ return { kind: "image", approximations };
27367
27744
  const reasons = [];
27368
27745
  if (fills.length > 1)
27369
27746
  reasons.push("several fills");
27370
27747
  if (fills.length === 1 && !["SOLID", "GRADIENT_LINEAR", "IMAGE"].includes(fills[0].type))
27371
27748
  reasons.push(`${fills[0].type.toLowerCase().replace("_", " ")} fill`);
27372
- if (image && (strokes.length || effects.length))
27749
+ if (image && (strokes.length || other.length))
27373
27750
  reasons.push("image with stroke or effects");
27374
27751
  if (strokes.length > 1 || strokes[0] && strokes[0].type !== "SOLID")
27375
27752
  reasons.push("complex stroke");
@@ -27379,18 +27756,16 @@ function decide(n, opts) {
27379
27756
  reasons.push(other.map((e) => e.type.toLowerCase().replace(/_/g, " ")).join(", "));
27380
27757
  if (n.type === "ELLIPSE" && Math.abs(n.w - n.h) > 0.5)
27381
27758
  reasons.push("oval");
27382
- const styled = fills.length + strokes.length + effects.length > 0;
27759
+ const styled = fills.length + strokes.length + (n.effects ?? []).length > 0;
27383
27760
  if ((reasons.length || raster === "all" && styled) && raster !== "none") {
27384
- return { kind: hasChildren ? "panel" : "picture", shadow: false, approximations };
27761
+ return { kind: hasChildren ? "panel" : "picture", approximations };
27385
27762
  }
27386
27763
  if (reasons.length)
27387
27764
  approximations.push(`${reasons.join(", ")}: approximated (rasterize is none)`);
27388
27765
  const r = n.radius ?? [];
27389
27766
  if (r.length && r.some((v) => Math.abs(v - r[0]) > 0.5))
27390
27767
  approximations.push("different corner radii: used the largest");
27391
- if (drop.length && raster === "none")
27392
- approximations.push("drop shadow skipped (rasterize is none)");
27393
- return { kind: "frame", shadow: drop.length > 0 && raster !== "none", approximations };
27768
+ return { kind: "frame", approximations };
27394
27769
  }
27395
27770
  function pictureRequests(root, opts) {
27396
27771
  const out = [];
@@ -27402,8 +27777,6 @@ function pictureRequests(root, opts) {
27402
27777
  out.push({ id: n.id, mode: "full" });
27403
27778
  if (d.kind === "panel")
27404
27779
  out.push({ id: n.id, mode: "panel" });
27405
- if (d.shadow)
27406
- out.push({ id: n.id, mode: "shadow" });
27407
27780
  if (d.kind !== "picture")
27408
27781
  for (const c of n.children ?? [])
27409
27782
  walk(c);
@@ -27423,6 +27796,56 @@ function imageHashes(root) {
27423
27796
  walk(root);
27424
27797
  return [...out];
27425
27798
  }
27799
+ function robloxName(base, flavor) {
27800
+ switch (flavor) {
27801
+ case "text":
27802
+ return /Label$/.test(base) ? base : `${base}Label`;
27803
+ case "button": {
27804
+ const b = base.replace(/Btn$/, "Button");
27805
+ return /Button$/.test(b) ? b : `${b}Button`;
27806
+ }
27807
+ case "icon":
27808
+ return endsWithWord(base, ICON_WORDS) ? base : `${base}Icon`;
27809
+ case "image":
27810
+ return endsWithWord(base, IMAGE_WORDS) ? base : `${base}Image`;
27811
+ default:
27812
+ return endsWithWord(base, ROLE_WORDS) ? base : `${base}${flavor === "frame" ? "Frame" : "Container"}`;
27813
+ }
27814
+ }
27815
+ function roleTree(n) {
27816
+ const fills = visible(n.fills);
27817
+ const kind = n.text ? "text" : n.vector || VECTOR_TYPES.includes(n.type) ? "vector" : fills.length === 1 && fills[0].type === "IMAGE" && !n.children?.length ? "image" : n.children?.length || n.layout ? "frame" : "shape";
27818
+ return {
27819
+ id: n.id,
27820
+ kind,
27821
+ name: n.name,
27822
+ x: n.x,
27823
+ y: n.y,
27824
+ w: n.w,
27825
+ h: n.h,
27826
+ hidden: n.hidden,
27827
+ text: n.text?.characters,
27828
+ fontSize: n.text ? Math.max(0, ...n.text.segments.map((s) => s.size)) : undefined,
27829
+ layout: n.layout?.mode,
27830
+ wrap: n.layout?.wrap,
27831
+ background: fills.length > 0 || visible(n.strokes).length > 0,
27832
+ radius: Math.max(0, ...n.radius ?? []),
27833
+ ellipse: n.type === "ELLIPSE",
27834
+ clickable: n.clickable,
27835
+ absolute: n.absolute,
27836
+ children: (n.children ?? []).map(roleTree)
27837
+ };
27838
+ }
27839
+ function dedupe(i) {
27840
+ const layers = i.children.filter((c) => !/^UI/.test(c.className));
27841
+ const names = uniqueSiblingNames(layers.map((c) => c.name));
27842
+ layers.forEach((c, k) => {
27843
+ c.name = names[k];
27844
+ c.props[0] = ["Name", S(c.name)];
27845
+ });
27846
+ for (const c of i.children)
27847
+ dedupe(c);
27848
+ }
27426
27849
 
27427
27850
  class Mapper {
27428
27851
  opts;
@@ -27431,48 +27854,82 @@ class Mapper {
27431
27854
  fontSubstitutions = new Map;
27432
27855
  counts = { instances: 0, pictures: 0, skippedHidden: 0 };
27433
27856
  textFactor = 1;
27857
+ names = new Map;
27858
+ roles = new Map;
27434
27859
  constructor(opts, assets) {
27435
27860
  this.opts = opts;
27436
27861
  this.assets = assets;
27437
27862
  }
27863
+ get scaleOnly() {
27864
+ return this.opts.mode === "scale";
27865
+ }
27438
27866
  warn(n, msg) {
27439
27867
  if (this.warnings.size < 60)
27440
27868
  this.warnings.add(n ? `${n.name}: ${msg}` : msg);
27441
27869
  }
27870
+ nameLayers(root) {
27871
+ const tree = roleTree(root);
27872
+ inferRoles(tree);
27873
+ const walk = (r) => {
27874
+ const base = pascalName(r.role ?? r.name) || pascalName(r.role ?? "") || "Layer";
27875
+ this.names.set(r.id, base);
27876
+ if (r.role)
27877
+ this.roles.set(r.id, r.role);
27878
+ for (const c of r.children)
27879
+ walk(c);
27880
+ };
27881
+ walk(tree);
27882
+ }
27883
+ label(n, flavor) {
27884
+ return robloxName(this.names.get(n.id) ?? (pascalName(n.name) || "Layer"), flavor);
27885
+ }
27886
+ isButton(n) {
27887
+ return !!n.clickable || BUTTON_NAME.test(n.name) || this.roles.get(n.id) === "Button";
27888
+ }
27442
27889
  map(root) {
27890
+ this.nameLayers(root);
27443
27891
  const [tw, th] = this.opts.targetResolution;
27444
27892
  const fullScreen = Math.abs(root.w / root.h - tw / th) < 0.1 * (tw / th) && root.w >= tw * 0.4;
27445
27893
  if (fullScreen && this.opts.mode !== "offset")
27446
27894
  this.textFactor = tw / root.w;
27447
27895
  const top = this.node(root, null, 0);
27448
27896
  const geo = this.opts.mode === "offset" ? [["AnchorPoint", V2(0.5, 0.5)], ["Position", U2(0.5, 0, 0.5, 0)], ["Size", U2(0, root.w, 0, root.h)]] : fullScreen ? [["AnchorPoint", V2(0.5, 0.5)], ["Position", U2(0.5, 0, 0.5, 0)], ["Size", U2(1, 0, 1, 0)]] : [["AnchorPoint", V2(0.5, 0.5)], ["Position", U2(0.5, 0, 0.5, 0)], ["Size", U2(root.w / tw, 0, root.h / th, 0)]];
27449
- top.props = top.props.filter(([k]) => !["AnchorPoint", "Position", "Size", "LayoutOrder", "ZIndex"].includes(k));
27897
+ top.props = top.props.filter(([k]) => !["AnchorPoint", "Position", "Size", "LayoutOrder", "ZIndex", "AutomaticSize"].includes(k));
27450
27898
  top.props.splice(1, 0, ...geo);
27451
- top.children.push(this.inst("UIAspectRatioConstraint", "AspectRatio", [["AspectRatio", F(root.w / root.h)]]));
27452
- if (this.opts.asRootFrame)
27453
- return top;
27454
- return this.inst("ScreenGui", root.name, [["ResetOnSpawn", B(false)], ["ZIndexBehavior", E("ZIndexBehavior", "Sibling")], ["IgnoreGuiInset", B(true)]], [top]);
27899
+ top.children.push(this.inst("UIAspectRatioConstraint", "UIAspectRatioConstraint", [["AspectRatio", F(root.w / root.h)]]));
27900
+ const out = this.opts.asRootFrame ? top : this.inst("ScreenGui", `${(this.names.get(root.id) ?? "Ui").replace(/Gui$/, "")}Gui`, [["ResetOnSpawn", B(false)], ["ZIndexBehavior", E("ZIndexBehavior", "Sibling")], ["IgnoreGuiInset", B(true)]], [top]);
27901
+ dedupe(out);
27902
+ return out;
27455
27903
  }
27456
27904
  inst(className, name, props = [], children = [], source) {
27457
27905
  this.counts.instances++;
27458
27906
  return { className, name, props: [["Name", S(name)], ...props], children, ...source ? { source } : {} };
27459
27907
  }
27908
+ frameOf(n, parent) {
27909
+ const inFlow = !!parent.layout && !n.absolute;
27910
+ const scale = this.opts.mode === "scale" || this.opts.mode === "hybrid" && !inFlow;
27911
+ if (!inFlow)
27912
+ return { scale, w: Math.max(1, parent.w), h: Math.max(1, parent.h), inFlow };
27913
+ const p = parent.layout.padding;
27914
+ return { scale, w: Math.max(1, parent.w - (p[1] ?? 0) - (p[3] ?? 0)), h: Math.max(1, parent.h - (p[0] ?? 0) - (p[2] ?? 0)), inFlow };
27915
+ }
27916
+ hugs(n, d) {
27917
+ if (this.scaleOnly)
27918
+ return { w: false, h: false };
27919
+ const content = d.kind === "text" || !!n.layout;
27920
+ return { w: n.sizing?.h === "HUG" && content, h: n.sizing?.v === "HUG" && content };
27921
+ }
27460
27922
  geometry(n, parent, index, d) {
27461
27923
  const out = [];
27462
27924
  if (!parent)
27463
27925
  return out;
27464
- const inFlow = !!parent.layout && !n.absolute;
27465
- const useScale = this.opts.mode === "scale" || this.opts.mode === "hybrid" && !inFlow;
27466
- const hugW = n.sizing?.h === "HUG" && (d.kind === "text" || !!n.layout);
27467
- const hugH = n.sizing?.v === "HUG" && (d.kind === "text" || !!n.layout);
27468
- if (inFlow) {
27469
- const l = parent.layout;
27470
- const horizontal = l.mode === "HORIZONTAL";
27471
- const pw = parent.w - (l.padding[1] ?? 0) - (l.padding[3] ?? 0);
27472
- const ph = parent.h - (l.padding[0] ?? 0) - (l.padding[2] ?? 0);
27926
+ const f = this.frameOf(n, parent);
27927
+ const hug = this.hugs(n, d);
27928
+ if (f.inFlow) {
27929
+ const horizontal = parent.layout.mode === "HORIZONTAL";
27473
27930
  const crossFill = horizontal ? n.sizing?.v === "FILL" || n.stretch : n.sizing?.h === "FILL" || n.stretch;
27474
- const w = (horizontal ? false : crossFill) ? U(1, 0) : hugW ? U(0, 0) : useScale && l.mode !== "GRID" ? U(n.w / Math.max(1, pw), 0) : U(0, n.w);
27475
- const h = (horizontal ? crossFill : false) ? U(1, 0) : hugH ? U(0, 0) : useScale && l.mode !== "GRID" ? U(n.h / Math.max(1, ph), 0) : U(0, n.h);
27931
+ const w = !horizontal && crossFill ? U(1, 0) : hug.w ? U(0, 0) : f.scale ? U(n.w / f.w, 0) : U(0, n.w);
27932
+ const h = horizontal && crossFill ? U(1, 0) : hug.h ? U(0, 0) : f.scale ? U(n.h / f.h, 0) : U(0, n.h);
27476
27933
  out.push(["Size", { t: "UDim2", xs: w.s, xo: w.o, ys: h.s, yo: h.o }]);
27477
27934
  out.push(["LayoutOrder", I(index + 1)]);
27478
27935
  return out;
@@ -27480,7 +27937,7 @@ class Mapper {
27480
27937
  const c = n.constraints ?? { h: "MIN", v: "MIN" };
27481
27938
  const axis = (pos, size, total, k) => {
27482
27939
  const end = total - pos - size;
27483
- if (k === "SCALE" || useScale) {
27940
+ if (k === "SCALE" || f.scale) {
27484
27941
  if (k === "MAX")
27485
27942
  return { anchor: 1, p: [(pos + size) / total, 0], s: [size / total, 0] };
27486
27943
  if (k === "CENTER")
@@ -27495,39 +27952,63 @@ class Mapper {
27495
27952
  return { anchor: 0, p: [0, pos], s: [1, -(pos + end)] };
27496
27953
  return { anchor: 0, p: [0, pos], s: [0, size] };
27497
27954
  };
27498
- const x = axis(n.x, n.w, Math.max(1, parent.w), c.h);
27499
- const y = axis(n.y, n.h, Math.max(1, parent.h), c.v);
27955
+ const x = axis(n.x, n.w, f.w, c.h);
27956
+ const y = axis(n.y, n.h, f.h, c.v);
27500
27957
  if (x.anchor || y.anchor)
27501
27958
  out.push(["AnchorPoint", V2(x.anchor, y.anchor)]);
27502
27959
  out.push(["Position", U2(x.p[0], x.p[1], y.p[0], y.p[1])]);
27503
- out.push(["Size", U2(hugW ? 0 : x.s[0], hugW ? 0 : x.s[1], hugH ? 0 : y.s[0], hugH ? 0 : y.s[1])]);
27960
+ out.push(["Size", U2(hug.w ? 0 : x.s[0], hug.w ? 0 : x.s[1], hug.h ? 0 : y.s[0], hug.h ? 0 : y.s[1])]);
27504
27961
  out.push(["ZIndex", I(index + 1)]);
27505
27962
  return out;
27506
27963
  }
27507
27964
  automaticSize(n, d) {
27508
- const hugW = n.sizing?.h === "HUG" && (d.kind === "text" || !!n.layout);
27509
- const hugH = n.sizing?.v === "HUG" && (d.kind === "text" || !!n.layout);
27510
- if (!hugW && !hugH)
27965
+ const hug = this.hugs(n, d);
27966
+ if (!hug.w && !hug.h)
27511
27967
  return [];
27512
- return [["AutomaticSize", E("AutomaticSize", hugW && hugH ? "XY" : hugW ? "X" : "Y")]];
27968
+ return [["AutomaticSize", E("AutomaticSize", hug.w && hug.h ? "XY" : hug.w ? "X" : "Y")]];
27513
27969
  }
27514
27970
  corner(n) {
27515
27971
  const r = Math.max(0, ...n.radius ?? []);
27516
27972
  if (!r && !(n.type === "ELLIPSE"))
27517
27973
  return null;
27518
- const pill = n.type === "ELLIPSE" || r >= Math.min(n.w, n.h) / 2 - 0.5;
27519
- return this.inst("UICorner", "UICorner", [["CornerRadius", pill ? U(0.5, 0) : U(0, r)]]);
27974
+ const short = Math.max(1, Math.min(n.w, n.h));
27975
+ const pill = n.type === "ELLIPSE" || r >= short / 2 - 0.5;
27976
+ return this.inst("UICorner", "UICorner", [["CornerRadius", pill ? U(0.5, 0) : this.scaleOnly ? U(r / short, 0) : U(0, r)]]);
27520
27977
  }
27521
27978
  stroke(n) {
27522
27979
  const s = visible(n.strokes)[0];
27523
27980
  if (!s || s.type !== "SOLID" || !n.strokeWeight)
27524
27981
  return null;
27525
- return this.inst("UIStroke", "UIStroke", [
27982
+ const short = Math.max(1, Math.min(n.w, n.h));
27983
+ const props = [
27526
27984
  ["ApplyStrokeMode", E("ApplyStrokeMode", "Border")],
27527
27985
  ["Color", C3(s.color)],
27528
- ["Thickness", F(n.strokeWeight)],
27986
+ ["Thickness", F(this.scaleOnly ? n.strokeWeight / short : n.strokeWeight)],
27529
27987
  ["Transparency", F(1 - s.opacity)],
27530
- ["LineJoinMode", E("LineJoinMode", "Round")]
27988
+ ["LineJoinMode", E("LineJoinMode", "Round")],
27989
+ ["BorderStrokePosition", E("BorderStrokePosition", n.strokeAlign === "OUTSIDE" ? "Outer" : n.strokeAlign === "CENTER" ? "Center" : "Inner")]
27990
+ ];
27991
+ if (this.scaleOnly)
27992
+ props.push(["StrokeSizingMode", E("StrokeSizingMode", "ScaledSize")]);
27993
+ return this.inst("UIStroke", "UIStroke", props);
27994
+ }
27995
+ shadow(n) {
27996
+ const list = (n.effects ?? []).filter((e) => e.type === "DROP_SHADOW");
27997
+ if (!list.length)
27998
+ return null;
27999
+ if (list.length > 1)
28000
+ this.warn(n, "several drop shadows: kept the largest");
28001
+ const e = list.reduce((a, b) => b.blur + (b.spread ?? 0) > a.blur + (a.spread ?? 0) ? b : a);
28002
+ const w = Math.max(1, n.w);
28003
+ const h = Math.max(1, n.h);
28004
+ const spread = (e.spread ?? 0) * 2;
28005
+ const s = this.scaleOnly;
28006
+ return this.inst("UIShadow", "UIShadow", [
28007
+ ["Color", C3(e.color ?? "#000000")],
28008
+ ["Transparency", F(1 - (e.alpha ?? 0.25))],
28009
+ ["BlurRadius", s ? U(e.blur / Math.min(w, h), 0) : U(0, e.blur)],
28010
+ ["Offset", s ? U2((e.x ?? 0) / w, 0, (e.y ?? 0) / h, 0) : U2(0, e.x ?? 0, 0, e.y ?? 0)],
28011
+ ["Spread", s ? U2(spread / w, 0, spread / h, 0) : U2(0, spread, 0, spread)]
27531
28012
  ]);
27532
28013
  }
27533
28014
  gradient(p) {
@@ -27550,18 +28031,26 @@ class Mapper {
27550
28031
  }
27551
28032
  layoutChildren(n) {
27552
28033
  const l = n.layout;
28034
+ const s = this.scaleOnly;
27553
28035
  const out = [];
27554
- const [t, r, b, lft] = l.padding;
28036
+ const [t = 0, r = 0, b = 0, lft = 0] = l.padding;
28037
+ const w = Math.max(1, n.w);
28038
+ const h = Math.max(1, n.h);
28039
+ const cw = Math.max(1, w - r - lft);
28040
+ const ch = Math.max(1, h - t - b);
28041
+ const pad = (v, total) => s ? U(v / total, 0) : U(0, v);
27555
28042
  if (t || r || b || lft)
27556
- out.push(this.inst("UIPadding", "UIPadding", [["PaddingTop", U(0, t)], ["PaddingRight", U(0, r)], ["PaddingBottom", U(0, b)], ["PaddingLeft", U(0, lft)]]));
28043
+ out.push(this.inst("UIPadding", "UIPadding", [["PaddingTop", pad(t, h)], ["PaddingRight", pad(r, w)], ["PaddingBottom", pad(b, h)], ["PaddingLeft", pad(lft, w)]]));
27557
28044
  if (l.mode === "GRID") {
27558
28045
  const kids = (n.children ?? []).filter((c) => !c.hidden && !c.absolute);
27559
28046
  const first = kids[0];
27560
28047
  if (kids.some((k) => Math.abs(k.w - (first?.w ?? 0)) > 1 || Math.abs(k.h - (first?.h ?? 0)) > 1))
27561
28048
  this.warn(n, "grid cells of different sizes: UIGridLayout uses the first cell's size");
28049
+ const fw = first?.w ?? 100;
28050
+ const fh = first?.h ?? 100;
27562
28051
  out.push(this.inst("UIGridLayout", "UIGridLayout", [
27563
- ["CellSize", U2(0, first?.w ?? 100, 0, first?.h ?? 100)],
27564
- ["CellPadding", U2(0, l.columnGap ?? 0, 0, l.rowGap ?? 0)],
28052
+ ["CellSize", s ? U2(fw / cw, 0, fh / ch, 0) : U2(0, fw, 0, fh)],
28053
+ ["CellPadding", s ? U2((l.columnGap ?? 0) / cw, 0, (l.rowGap ?? 0) / ch, 0) : U2(0, l.columnGap ?? 0, 0, l.rowGap ?? 0)],
27565
28054
  ["FillDirectionMaxCells", I(l.columns ?? 0)],
27566
28055
  ["SortOrder", E("SortOrder", "LayoutOrder")]
27567
28056
  ]));
@@ -27570,10 +28059,11 @@ class Mapper {
27570
28059
  const horizontal = l.mode === "HORIZONTAL";
27571
28060
  const main = l.primary === "CENTER" ? "Center" : l.primary === "MAX" ? horizontal ? "Right" : "Bottom" : horizontal ? "Left" : "Top";
27572
28061
  const cross = l.counter === "CENTER" ? "Center" : l.counter === "MAX" ? horizontal ? "Bottom" : "Right" : horizontal ? "Top" : "Left";
28062
+ const gap = l.primary === "SPACE_BETWEEN" ? 0 : l.gap;
27573
28063
  const props = [
27574
28064
  ["FillDirection", E("FillDirection", horizontal ? "Horizontal" : "Vertical")],
27575
28065
  ["SortOrder", E("SortOrder", "LayoutOrder")],
27576
- ["Padding", U(0, l.primary === "SPACE_BETWEEN" ? 0 : l.gap)],
28066
+ ["Padding", pad(gap, horizontal ? cw : ch)],
27577
28067
  ["HorizontalAlignment", E("HorizontalAlignment", horizontal ? main === "Right" ? "Right" : main === "Center" ? "Center" : "Left" : cross === "Right" ? "Right" : cross === "Center" ? "Center" : "Left")],
27578
28068
  ["VerticalAlignment", E("VerticalAlignment", horizontal ? cross === "Bottom" ? "Bottom" : cross === "Center" ? "Center" : "Top" : main === "Bottom" ? "Bottom" : main === "Center" ? "Center" : "Top")]
27579
28069
  ];
@@ -27636,10 +28126,12 @@ class Mapper {
27636
28126
  props.push(["LineHeight", F(Math.max(1, Math.min(3, base.lineHeight / (base.size * 1.2))))]);
27637
28127
  if (t.maxLines)
27638
28128
  props.push(["TextTruncate", E("TextTruncate", "AtEnd")]);
27639
- if (this.opts.textScaled)
28129
+ if (this.opts.textScaled || this.scaleOnly)
27640
28130
  props.push(["TextScaled", B(true)]);
27641
28131
  if (segs.some((s) => s.letterSpacing))
27642
28132
  this.warn(n, "letter spacing has no Roblox equivalent: ignored");
28133
+ if (rich && this.scaleOnly && segs.some((s) => s.size !== base.size))
28134
+ this.warn(n, "several text sizes with TextScaled: the size tags may not scale with the label");
27643
28135
  return props;
27644
28136
  }
27645
28137
  richRun(s, base) {
@@ -27677,7 +28169,7 @@ class Mapper {
27677
28169
  const d = decide(n, this.opts);
27678
28170
  for (const a of d.approximations)
27679
28171
  this.warn(n, a);
27680
- const button = !!parent && (n.clickable || BUTTON_NAME.test(n.name)) && d.kind !== "text";
28172
+ const button = !!parent && this.isButton(n) && d.kind !== "text";
27681
28173
  const geo = this.geometry(n, parent, index, d);
27682
28174
  const common = [...geo, ...this.automaticSize(n, d)];
27683
28175
  if (n.rotation)
@@ -27686,26 +28178,32 @@ class Mapper {
27686
28178
  let className = "Frame";
27687
28179
  let props = [];
27688
28180
  if (d.kind === "text") {
27689
- className = n.clickable || BUTTON_NAME.test(n.name) ? "TextButton" : "TextLabel";
28181
+ const isButton = !!n.clickable || BUTTON_NAME.test(n.name);
28182
+ className = isButton ? "TextButton" : "TextLabel";
27690
28183
  props = this.textProps(n);
27691
- if (className === "TextButton")
28184
+ if (isButton)
27692
28185
  props.push(["AutoButtonColor", B(false)]);
28186
+ if ((n.effects ?? []).some((e) => e.type === "DROP_SHADOW"))
28187
+ this.warn(n, "text shadow has no Roblox equivalent: ignored");
27693
28188
  const size = props.find(([k]) => k === "TextSize")[1].v;
27694
- children.push(this.inst("UITextSizeConstraint", "UITextSizeConstraint", [["MaxTextSize", I(size)], ["MinTextSize", I(Math.max(1, Math.round(size * 0.5)))]]));
27695
- return this.inst(className, n.name, [...common, ...props], children, n.id);
28189
+ const tight = n.text.autoResize === "WIDTH_AND_HEIGHT" || n.text.autoResize === "HEIGHT";
28190
+ const max = this.scaleOnly && tight ? size * 2 : size;
28191
+ children.push(this.inst("UITextSizeConstraint", "UITextSizeConstraint", [["MaxTextSize", I(max)], ["MinTextSize", I(Math.max(1, Math.round(size * 0.5)))]]));
28192
+ return this.inst(className, this.label(n, isButton ? "button" : "text"), [...common, ...props], children, n.id);
27696
28193
  }
27697
28194
  if (d.kind === "picture") {
27698
28195
  this.counts.pictures++;
27699
28196
  const a = this.assetUrl(`${n.id}:full`, n);
27700
28197
  className = button ? "ImageButton" : "ImageLabel";
27701
- const pic = this.pictureGeometry(n, a, geo);
28198
+ const pic = parent ? this.pictureGeometry(n, a, geo, parent) : geo;
27702
28199
  props = [["BackgroundTransparency", F(1)], ["Image", { t: "Content", url: a.url }], ["ScaleType", E("ScaleType", "Stretch")]];
27703
28200
  if (n.opacity !== undefined && n.opacity < 1)
27704
28201
  props.push(["ImageTransparency", F(1 - n.opacity)]);
27705
28202
  if (button)
27706
28203
  props.push(["AutoButtonColor", B(false)]);
27707
28204
  children.push(this.inst("UIAspectRatioConstraint", "UIAspectRatioConstraint", [["AspectRatio", F((a.size?.w ?? n.w) / Math.max(1, a.size?.h ?? n.h))]]));
27708
- return this.inst(className, n.name, [...pic, ...props], children, n.id);
28205
+ const flavor = button ? "button" : n.vector || VECTOR_TYPES.includes(n.type) ? "icon" : "image";
28206
+ return this.inst(className, this.label(n, flavor), [...pic, ...props], children, n.id);
27709
28207
  }
27710
28208
  const fills = visible(n.fills);
27711
28209
  const opacity = n.opacity ?? 1;
@@ -27723,8 +28221,11 @@ class Mapper {
27723
28221
  const corner = this.corner(n);
27724
28222
  if (corner)
27725
28223
  children.push(corner);
28224
+ const shadow = this.shadow(n);
28225
+ if (shadow)
28226
+ children.push(shadow);
27726
28227
  children.push(this.inst("UIAspectRatioConstraint", "UIAspectRatioConstraint", [["AspectRatio", F(n.w / Math.max(1, n.h))]]));
27727
- return this.inst(className, n.name, [...common, ...props], children, n.id);
28228
+ return this.inst(className, this.label(n, button ? "button" : "image"), [...common, ...props], children, n.id);
27728
28229
  }
27729
28230
  if (d.kind === "panel") {
27730
28231
  this.counts.pictures++;
@@ -27747,6 +28248,13 @@ class Mapper {
27747
28248
  props.push(["ImageTransparency", F(1 - opacity)]);
27748
28249
  if (button)
27749
28250
  props.push(["AutoButtonColor", B(false)]);
28251
+ const shadow = this.shadow(n);
28252
+ if (shadow) {
28253
+ const corner = this.corner(n);
28254
+ if (corner)
28255
+ children.push(corner);
28256
+ children.push(shadow);
28257
+ }
27750
28258
  } else {
27751
28259
  className = button ? "TextButton" : hasKids && opacity < 1 ? "CanvasGroup" : "Frame";
27752
28260
  const f = fills[0];
@@ -27763,7 +28271,7 @@ class Mapper {
27763
28271
  } else if (f.type === "IMAGE") {
27764
28272
  props.push(["BackgroundTransparency", F(1)]);
27765
28273
  const a = this.assetUrl(`image:${f.imageHash}`, n);
27766
- const bg = this.inst("ImageLabel", "Background", [["Size", U2(1, 0, 1, 0)], ["ZIndex", I(0)], ["BackgroundTransparency", F(1)], ["Image", { t: "Content", url: a.url }], ["ScaleType", E("ScaleType", f.scaleMode === "FIT" ? "Fit" : "Crop")]]);
28274
+ const bg = this.inst("ImageLabel", "BackgroundImage", [["Size", U2(1, 0, 1, 0)], ["ZIndex", I(0)], ["BackgroundTransparency", F(1)], ["Image", { t: "Content", url: a.url }], ["ScaleType", E("ScaleType", f.scaleMode === "FIT" ? "Fit" : "Crop")]]);
27767
28275
  const corner = this.corner(n);
27768
28276
  if (corner)
27769
28277
  bg.children.push(corner);
@@ -27779,6 +28287,9 @@ class Mapper {
27779
28287
  const stroke = this.stroke(n);
27780
28288
  if (stroke)
27781
28289
  children.push(stroke);
28290
+ const shadow = this.shadow(n);
28291
+ if (shadow)
28292
+ children.push(shadow);
27782
28293
  }
27783
28294
  props.push(["BorderSizePixel", I(0)]);
27784
28295
  if (n.clip)
@@ -27787,8 +28298,7 @@ class Mapper {
27787
28298
  const flow = n.layout ? kids.filter((k) => !k.absolute) : kids;
27788
28299
  const loose = n.layout ? kids.filter((k) => k.absolute) : [];
27789
28300
  const me = { w: n.w, h: n.h, layout: n.layout };
27790
- const mapped = (list, p) => list.map((k, i) => this.node(k, p, i)).filter((x) => !!x);
27791
- const flowMapped = mapped(flow, me);
28301
+ const flowMapped = flow.map((k, i) => this.node(k, me, i)).filter((x) => !!x);
27792
28302
  if (n.layout && n.layout.mode !== "GRID") {
27793
28303
  for (const k of flow) {
27794
28304
  const target = k.grow && !k.hidden ? flowMapped.find((c) => c.source === k.id) : undefined;
@@ -27806,44 +28316,24 @@ class Mapper {
27806
28316
  children.push(...this.layoutChildren(n));
27807
28317
  children.push(...flowMapped);
27808
28318
  }
27809
- const self = this.inst(className, n.name, [...common, ...props], children, n.id);
27810
- if (!d.shadow)
27811
- return self;
27812
- this.counts.pictures++;
27813
- const a = this.assetUrl(`${n.id}:shadow`, n);
27814
- const scale = this.opts.scale ?? 2;
27815
- const sh = (n.effects ?? []).filter((e) => e.type === "DROP_SHADOW");
27816
- const reach = Math.max(...sh.map((e) => e.blur + (e.spread ?? 0) + Math.max(Math.abs(e.x ?? 0), Math.abs(e.y ?? 0))));
27817
- const off = a.offset ?? { x: 0, y: 0 };
27818
- const size = a.size ?? { w: n.w, h: n.h };
27819
- const inset = Math.ceil((Math.max(0, ...n.radius ?? [0]) + reach) * scale);
27820
- const pw = Math.round(size.w * scale);
27821
- const ph = Math.round(size.h * scale);
27822
- const ix = Math.min(inset, Math.floor(pw / 2) - 1);
27823
- const iy = Math.min(inset, Math.floor(ph / 2) - 1);
27824
- const shadow = this.inst("ImageLabel", "Shadow", [
27825
- ["Position", U2(0, off.x, 0, off.y)],
27826
- ["Size", U2(1, size.w - n.w, 1, size.h - n.h)],
27827
- ["ZIndex", I(1)],
27828
- ["BackgroundTransparency", F(1)],
27829
- ["Image", { t: "Content", url: a.url }],
27830
- ["ScaleType", E("ScaleType", "Slice")],
27831
- ["SliceCenter", { t: "Rect", x0: ix, y0: iy, x1: pw - ix, y1: ph - iy }],
27832
- ["SliceScale", F(1 / scale)]
27833
- ]);
27834
- self.props = self.props.filter(([k]) => !["AnchorPoint", "Position", "Size", "LayoutOrder", "AutomaticSize"].includes(k));
27835
- self.props.splice(1, 0, ["Size", U2(1, 0, 1, 0)], ["ZIndex", I(2)]);
27836
- return this.inst("Frame", n.name, [...common.filter(([k]) => k !== "AutomaticSize"), ["BackgroundTransparency", F(1)], ["BorderSizePixel", I(0)]], [shadow, self], n.id);
28319
+ const flavor = button ? "button" : fills.length || visible(n.strokes).length || d.kind === "panel" ? "frame" : "container";
28320
+ return this.inst(className, this.label(n, flavor), [...common, ...props], children, n.id);
27837
28321
  }
27838
- pictureGeometry(n, a, geo) {
28322
+ pictureGeometry(n, a, geo, parent) {
27839
28323
  if (!a.offset || !a.size || Math.abs(a.offset.x) < 0.5 && Math.abs(a.offset.y) < 0.5 && Math.abs(a.size.w - n.w) < 0.5 && Math.abs(a.size.h - n.h) < 0.5)
27840
28324
  return geo;
28325
+ const f = this.frameOf(n, parent);
28326
+ const dw = a.size.w - n.w;
28327
+ const dh = a.size.h - n.h;
28328
+ const ox = a.offset.x;
28329
+ const oy = a.offset.y;
27841
28330
  return geo.map(([k, v]) => {
27842
- if (k === "Size" && v.t === "UDim2")
27843
- return [k, { ...v, xo: Math.round(v.xo + a.size.w - n.w), yo: Math.round(v.yo + a.size.h - n.h) }];
27844
- if (k === "Position" && v.t === "UDim2")
27845
- return [k, { ...v, xo: Math.round(v.xo + a.offset.x), yo: Math.round(v.yo + a.offset.y) }];
27846
- return [k, v];
28331
+ if (v.t !== "UDim2" || k !== "Size" && k !== "Position")
28332
+ return [k, v];
28333
+ if (f.scale) {
28334
+ return k === "Size" ? [k, { ...v, xs: num(v.xs + dw / f.w), ys: num(v.ys + dh / f.h) }] : [k, { ...v, xs: num(v.xs + ox / f.w), ys: num(v.ys + oy / f.h) }];
28335
+ }
28336
+ return k === "Size" ? [k, { ...v, xo: Math.round(v.xo + dw), yo: Math.round(v.yo + dh) }] : [k, { ...v, xo: Math.round(v.xo + ox), yo: Math.round(v.yo + oy) }];
27847
28337
  });
27848
28338
  }
27849
28339
  }
@@ -27854,10 +28344,14 @@ function mapToRoblox(root, opts, assets) {
27854
28344
  m.warn(null, `${m.counts.skippedHidden} hidden layer(s) skipped`);
27855
28345
  return { root: out, warnings: [...m.warnings], fontSubstitutions: Object.fromEntries(m.fontSubstitutions), counts: m.counts };
27856
28346
  }
27857
- var num = (v) => Math.round(v * 1000) / 1000, S = (v) => ({ t: "string", v }), B = (v) => ({ t: "bool", v }), I = (v) => ({ t: "int", v: Math.round(v) }), F = (v) => ({ t: "float", v: num(v) }), E = (e, v) => ({ t: "enum", e, v }), U = (s, o) => ({ t: "UDim", s: num(s), o: Math.round(o) }), U2 = (xs, xo, ys, yo) => ({ t: "UDim2", xs: num(xs), xo: Math.round(xo), ys: num(ys), yo: Math.round(yo) }), V2 = (x, y) => ({ t: "Vector2", x: num(x), y: num(y) }), C3 = (hex) => ({ t: "Color3", ...color3(hex) }), BUTTON_NAME;
28347
+ var num = (v) => Math.round(v * 1e4) / 1e4, S = (v) => ({ t: "string", v }), B = (v) => ({ t: "bool", v }), I = (v) => ({ t: "int", v: Math.round(v) }), F = (v) => ({ t: "float", v: num(v) }), E = (e, v) => ({ t: "enum", e, v }), U = (s, o) => ({ t: "UDim", s: num(s), o: Math.round(o) }), U2 = (xs, xo, ys, yo) => ({ t: "UDim2", xs: num(xs), xo: Math.round(xo), ys: num(ys), yo: Math.round(yo) }), V2 = (x, y) => ({ t: "Vector2", x: num(x), y: num(y) }), C3 = (hex) => ({ t: "Color3", ...color3(hex) }), BUTTON_NAME, VECTOR_TYPES, ICON_WORDS, IMAGE_WORDS;
27858
28348
  var init_map2 = __esm(() => {
28349
+ init_naming();
27859
28350
  init_fonts();
27860
- BUTTON_NAME = /\b(button|btn|cta)\b/i;
28351
+ BUTTON_NAME = /button|btn|\bcta\b/i;
28352
+ VECTOR_TYPES = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
28353
+ ICON_WORDS = ["Icon", "Logo", "Image", "Illustration", "Avatar", "Emoji"];
28354
+ IMAGE_WORDS = ["Image", "Icon", "Avatar", "Logo", "Thumbnail", "Picture", "Photo", "Banner", "Background", "Illustration", "Art", "Cover"];
27861
28355
  });
27862
28356
 
27863
28357
  // src/roblox/output.ts
@@ -28019,7 +28513,7 @@ return ${rootVar}
28019
28513
  function substituteAssets(text, ids) {
28020
28514
  return text.replace(/rbxassetid:\/\/PENDING_\d+/g, (m) => ids[m] ? `rbxassetid://${ids[m]}` : m);
28021
28515
  }
28022
- var ENUMS, n = (v) => String(Math.round(v * 1000) / 1000), xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"), KEYWORDS, rgb = (v) => Math.round(v * 255);
28516
+ var ENUMS, n = (v) => String(Math.round(v * 1e4) / 1e4), xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"), KEYWORDS, rgb = (v) => Math.round(v * 255);
28023
28517
  var init_output = __esm(() => {
28024
28518
  ENUMS = {
28025
28519
  ZIndexBehavior: { Global: 0, Sibling: 1 },
@@ -28035,7 +28529,9 @@ var init_output = __esm(() => {
28035
28529
  ScaleType: { Stretch: 0, Slice: 1, Tile: 2, Fit: 3, Crop: 4 },
28036
28530
  ApplyStrokeMode: { Contextual: 0, Border: 1 },
28037
28531
  LineJoinMode: { Round: 0, Bevel: 1, Miter: 2 },
28038
- TextTruncate: { None: 0, AtEnd: 1, SplitWord: 2 }
28532
+ TextTruncate: { None: 0, AtEnd: 1, SplitWord: 2 },
28533
+ StrokeSizingMode: { FixedSize: 0, ScaledSize: 1 },
28534
+ BorderStrokePosition: { Outer: 0, Center: 1, Inner: 2 }
28039
28535
  };
28040
28536
  KEYWORDS = new Set(["and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while", "continue", "export", "type", "game", "workspace", "script", "PARENT", "existing"]);
28041
28537
  });
@@ -28141,7 +28637,7 @@ import { join as join2 } from "node:path";
28141
28637
  async function exportRoblox(args, deps) {
28142
28638
  const env = deps.env ?? process.env;
28143
28639
  const opts = {
28144
- mode: args.mode ?? "hybrid",
28640
+ mode: args.mode ?? "scale",
28145
28641
  targetResolution: args.targetResolution ?? [1920, 1080],
28146
28642
  rasterize: args.rasterize ?? "auto",
28147
28643
  asRootFrame: args.asRootFrame,
@@ -30585,6 +31081,8 @@ function snapshot(opts) {
30585
31081
  }
30586
31082
 
30587
31083
  // src/web/browser.ts
31084
+ import { existsSync as existsSync5 } from "node:fs";
31085
+ import { join as join8 } from "node:path";
30588
31086
  import { pathToFileURL } from "node:url";
30589
31087
  async function loadPlaywright() {
30590
31088
  try {
@@ -30593,12 +31091,39 @@ async function loadPlaywright() {
30593
31091
  throw new WebImportError("import_web needs the optional package playwright-core, which is not installed. Reinstall without --no-optional, or run `npm install -g playwright-core` (it uses your installed Chrome or Edge; no browser is downloaded).", "MISSING_DEPENDENCY");
30594
31092
  }
30595
31093
  }
31094
+ function otherChromiumBrowsers() {
31095
+ const env = process.env;
31096
+ const roots = process.platform === "win32" ? [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA, "C:\\Program Files", "C:\\Program Files (x86)"].filter(Boolean) : [];
31097
+ const candidates = process.platform === "win32" ? [
31098
+ { label: "Brave", paths: roots.map((r) => join8(r, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")) },
31099
+ { label: "Chromium", paths: roots.map((r) => join8(r, "Chromium", "Application", "chrome.exe")) },
31100
+ { label: "Vivaldi", paths: roots.map((r) => join8(r, "Vivaldi", "Application", "vivaldi.exe")) },
31101
+ { label: "Opera", paths: roots.map((r) => join8(r, "Programs", "Opera", "opera.exe")) }
31102
+ ] : process.platform === "darwin" ? [
31103
+ { label: "Brave", paths: ["/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"] },
31104
+ { label: "Chromium", paths: ["/Applications/Chromium.app/Contents/MacOS/Chromium"] },
31105
+ { label: "Arc", paths: ["/Applications/Arc.app/Contents/MacOS/Arc"] }
31106
+ ] : [
31107
+ { label: "Brave", paths: ["/usr/bin/brave-browser", "/usr/bin/brave"] },
31108
+ { label: "Chromium", paths: ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium"] }
31109
+ ];
31110
+ const found = [];
31111
+ for (const c of candidates) {
31112
+ const path = c.paths.find((p) => existsSync5(p));
31113
+ if (path)
31114
+ found.push({ label: c.label, path });
31115
+ }
31116
+ return found;
31117
+ }
30596
31118
  async function launchBrowser() {
30597
31119
  const { chromium } = await loadPlaywright();
30598
31120
  const tries = [];
30599
31121
  if (process.env.FIGMA_BRIDGE_BROWSER)
30600
31122
  tries.push({ label: process.env.FIGMA_BRIDGE_BROWSER, opts: { executablePath: process.env.FIGMA_BRIDGE_BROWSER } });
30601
- tries.push({ label: "Chrome", opts: { channel: "chrome" } }, { label: "Edge", opts: { channel: "msedge" } }, { label: "Playwright Chromium", opts: {} });
31123
+ tries.push({ label: "Chrome", opts: { channel: "chrome" } }, { label: "Edge", opts: { channel: "msedge" } });
31124
+ for (const b of otherChromiumBrowsers())
31125
+ tries.push({ label: b.label, opts: { executablePath: b.path } });
31126
+ tries.push({ label: "Playwright Chromium", opts: {} });
30602
31127
  const errors = [];
30603
31128
  for (const t of tries) {
30604
31129
  try {
@@ -30608,7 +31133,7 @@ async function launchBrowser() {
30608
31133
  `)[0]}`);
30609
31134
  }
30610
31135
  }
30611
- throw new WebImportError(`No browser found to render the page. Install Google Chrome or Microsoft Edge, or set FIGMA_BRIDGE_BROWSER to a Chromium-based browser. (${errors.join("; ")})`, "NO_BROWSER");
31136
+ throw new WebImportError(`No browser found to render the page. Install Chrome, Edge or Brave, or set FIGMA_BRIDGE_BROWSER to a Chromium-based browser. (${errors.join("; ")})`, "NO_BROWSER");
30612
31137
  }
30613
31138
  async function render(source, opts) {
30614
31139
  const given = [source.url, source.html, source.path].filter((v) => v !== undefined).length;
@@ -31647,7 +32172,7 @@ var init_import = __esm(() => {
31647
32172
  import { createHash as createHash2 } from "node:crypto";
31648
32173
  import { mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync, statSync as statSync2, writeFileSync as writeFileSync5 } from "node:fs";
31649
32174
  import { homedir as homedir2 } from "node:os";
31650
- import { join as join8 } from "node:path";
32175
+ import { join as join9 } from "node:path";
31651
32176
  function ensureDir() {
31652
32177
  mkdirSync5(SNIPPETS_DIR, { recursive: true });
31653
32178
  }
@@ -31664,14 +32189,14 @@ function files() {
31664
32189
  }
31665
32190
  function listSnippets() {
31666
32191
  return files().map((f) => {
31667
- const source = readFileSync8(join8(SNIPPETS_DIR, f), "utf8");
32192
+ const source = readFileSync8(join9(SNIPPETS_DIR, f), "utf8");
31668
32193
  return { name: f.slice(0, -3), description: header2(source, "description"), usage: header2(source, "usage") };
31669
32194
  });
31670
32195
  }
31671
32196
  function getSnippet(name) {
31672
32197
  checkName(name);
31673
32198
  try {
31674
- return readFileSync8(join8(SNIPPETS_DIR, `${name}.js`), "utf8");
32199
+ return readFileSync8(join9(SNIPPETS_DIR, `${name}.js`), "utf8");
31675
32200
  } catch {
31676
32201
  throw new BridgeError(`No snippet named "${name}".`, "NOT_FOUND");
31677
32202
  }
@@ -31684,37 +32209,37 @@ function saveSnippet(name, code, description = "", usage = "") {
31684
32209
  // @usage ${usage.replace(/\n/g, " ") || `await lib.${name}(args)`}
31685
32210
  ${body}
31686
32211
  `;
31687
- const path = join8(SNIPPETS_DIR, `${name}.js`);
32212
+ const path = join9(SNIPPETS_DIR, `${name}.js`);
31688
32213
  writeFileSync5(path, text);
31689
32214
  return path;
31690
32215
  }
31691
32216
  function deleteSnippet(name) {
31692
32217
  checkName(name);
31693
- rmSync(join8(SNIPPETS_DIR, `${name}.js`), { force: true });
32218
+ rmSync(join9(SNIPPETS_DIR, `${name}.js`), { force: true });
31694
32219
  }
31695
32220
  function loadLibrary() {
31696
32221
  const list = files();
31697
- const stamp = list.map((f) => `${f}:${statSync2(join8(SNIPPETS_DIR, f)).mtimeMs}`).join("|");
32222
+ const stamp = list.map((f) => `${f}:${statSync2(join9(SNIPPETS_DIR, f)).mtimeMs}`).join("|");
31698
32223
  if (cached2 && cached2.stamp === stamp)
31699
32224
  return cached2;
31700
32225
  const lib = {};
31701
32226
  for (const f of list)
31702
- lib[f.slice(0, -3)] = readFileSync8(join8(SNIPPETS_DIR, f), "utf8");
32227
+ lib[f.slice(0, -3)] = readFileSync8(join9(SNIPPETS_DIR, f), "utf8");
31703
32228
  cached2 = { stamp, hash: stamp ? createHash2("sha1").update(stamp + JSON.stringify(lib)).digest("base64url").slice(0, 12) : "empty", lib };
31704
32229
  return cached2;
31705
32230
  }
31706
32231
  var SNIPPETS_DIR, NAME, cached2 = null;
31707
32232
  var init_snippets = __esm(() => {
31708
32233
  init_bridge();
31709
- SNIPPETS_DIR = process.env.FIGMA_BRIDGE_SNIPPETS || join8(homedir2(), ".figma-bridge", "snippets");
32234
+ SNIPPETS_DIR = process.env.FIGMA_BRIDGE_SNIPPETS || join9(homedir2(), ".figma-bridge", "snippets");
31710
32235
  NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
31711
32236
  });
31712
32237
 
31713
32238
  // src/server.ts
31714
32239
  import { appendFileSync, mkdirSync as mkdirSync6, statSync as statSync3, truncateSync, writeFileSync as writeFileSync6 } from "node:fs";
31715
32240
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
31716
- import { tmpdir } from "node:os";
31717
- import { dirname as dirname4, join as join9 } from "node:path";
32241
+ import { homedir as homedir3, tmpdir } from "node:os";
32242
+ import { dirname as dirname4, join as join10 } from "node:path";
31718
32243
  import { fileURLToPath } from "node:url";
31719
32244
  function log(line) {
31720
32245
  const text = `[${new Date().toISOString().slice(11, 23)}] ${line}`;
@@ -31882,7 +32407,7 @@ async function layerNames(session, ids) {
31882
32407
  return bridge.request("node_info", { ids: [...new Set(ids)] }, 20000).catch(() => ({}));
31883
32408
  }
31884
32409
  var PORT, CHANNEL, OUT_DIR, LOG_FILE, MAX_OUTPUT_CHARS = 60000, MAX_IMAGE_BYTES, FIGMA_MAX_IMAGE_DIM = 4096, VIEW_MAX_DIM = 1568, bridge, ok = (value) => ({ content: [{ type: "text", text: json(value) }] }), plain = (text) => ({ content: [{ type: "text", text }] }), oneLine = (s, max = 120) => s.replace(/\s+/g, " ").trim().slice(0, max), safeName = (s) => s.replace(/[^\w.-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "node", INSTRUCTIONS, BUILD_DESCRIPTION = `Create a whole layout in ONE call from a declarative spec. Fonts load automatically, icons are fetched, images loaded. Much faster and safer than run_script for new UI.
31885
- Node: {type?, name?, ...props, children?: Node[]}. type is inferred (text/spans→text, icon→icon, src→image, component→instance, variants→componentSet, else frame). Types: frame, component, componentSet, text, rect, ellipse, line, icon, image, svg, instance.
32410
+ Node: {type?, name?, ...props, children?: Node[]}. Name layers by role (Header, Card, CardList, Title, Description, PrimaryButton, Icon); unnamed layers get a role name (Container, Row, Card, Title, Label, Button, Icon, Divider…). type is inferred (text/spans→text, icon→icon, src→image, component→instance, variants→componentSet, else frame). Types: frame, component, componentSet, text, rect, ellipse, line, icon, image, svg, instance.
31886
32411
  FRAME: layout "row"|"column"|"grid" (auto-layout; omit for free positioning), gap (number|"auto"), padding (n | [v,h] | [t,r,b,l]), align (cross axis: start|center|end|baseline), justify (main axis: start|center|end|between), wrap, rowGap, clip. Frames have no fill unless set.
31887
32412
  GRID: layout:"grid", columns (count | tracks like [200,"1fr","2fr","hug"]), rows (same; default: enough rows, hugging), columnGap, rowGap (or gap). Children: span:[rows,cols] or colSpan/rowSpan, cellAlign/cellValign. Give the grid a w for "fr" columns.
31888
32413
  SIZE: w / h: number (fixed) | "fill" (stretch inside an auto-layout parent) | "hug". Auto-layout frames hug by default. grow:true. absolute:true with x/y inside auto-layout; x/y for children of free frames.
@@ -31918,12 +32443,13 @@ var init_server3 = __esm(async () => {
31918
32443
  init_tokens();
31919
32444
  init_tokens_export();
31920
32445
  init_import();
32446
+ init_setup();
31921
32447
  init_snippets();
31922
32448
  init_package();
31923
32449
  PORT = Number(process.env.FIGMA_BRIDGE_PORT) || 3055;
31924
32450
  CHANNEL = process.env.FIGMA_BRIDGE_CHANNEL || "default";
31925
- OUT_DIR = process.env.FIGMA_BRIDGE_OUT || join9(tmpdir(), "figma-bridge");
31926
- LOG_FILE = join9(OUT_DIR, "bridge.log");
32451
+ OUT_DIR = process.env.FIGMA_BRIDGE_OUT || join10(tmpdir(), "figma-bridge");
32452
+ LOG_FILE = join10(OUT_DIR, "bridge.log");
31927
32453
  MAX_IMAGE_BYTES = 50 << 20;
31928
32454
  mkdirSync6(OUT_DIR, { recursive: true });
31929
32455
  try {
@@ -31932,6 +32458,16 @@ var init_server3 = __esm(async () => {
31932
32458
  } catch {}
31933
32459
  bridge = new Bridge({ port: PORT, channel: CHANNEL, version, log });
31934
32460
  bridge.start();
32461
+ if (process.env.FIGMA_BRIDGE_PLUGIN_SYNC !== "0") {
32462
+ try {
32463
+ const target = process.env.FIGMA_BRIDGE_PLUGIN_DIR || join10(homedir3(), ".figma-bridge", "plugin");
32464
+ const status = syncPlugin(join10(dirname4(fileURLToPath(import.meta.url)), "..", "plugin"), target, version);
32465
+ if (status === "installed" || status === "updated")
32466
+ log(`Figma plugin ${status} to v${version} in ${target}; reopen it in Figma`);
32467
+ } catch (e) {
32468
+ log(`Could not update the Figma plugin copy: ${e.message}`);
32469
+ }
32470
+ }
31935
32471
  INSTRUCTIONS = [
31936
32472
  "Figma Bridge drives the user's Figma desktop app through a local plugin. No rate limits: iterate freely.",
31937
32473
  "Workflow:",
@@ -31950,7 +32486,8 @@ var init_server3 = __esm(async () => {
31950
32486
  "Rules: load fonts before editing text; pages load on demand (await figma.setCurrentPageAsync / getNodeByIdAsync); keep results small.",
31951
32487
  "The user can cancel a command from the plugin (code CANCELLED): build stops, a script may still finish, so check the file before retrying.",
31952
32488
  "With FIGMA_TOKEN set: comments reads and posts file comments pinned to layers, versions lists the history and diffs a version against now.",
31953
- "Roblox: export_roblox turns a frame into a ScreenGui (.rbxmx + Luau for execute_luau + PNG assets); with a Roblox Studio MCP, upload the assets, run the Luau and check a Studio screenshot.",
32489
+ "Names: give every layer you create a professional name for its role, as a design system or a developer would: Screen, Header, Navbar, Sidebar, Section, Container, Card, CardList, Row, Grid, Title, Subtitle, Description, Label, Price, PrimaryButton, Icon, Avatar, Badge, Input, Divider, Background. Never leave Frame 12, Rectangle 3 or a text named after its content; build names unnamed layers by role and audit {fix:true} renames default names.",
32490
+ "Roblox: export_roblox turns a frame into a ScreenGui (.rbxmx + Luau for execute_luau + PNG assets), in scale only (no offsets) with PascalCase names (ShopGui, ItemCard, TitleLabel, BuyButton); with a Roblox Studio MCP, upload the assets, run the Luau and check a Studio screenshot. Any Roblox UI you write by hand follows the same rules: UDim2.fromScale, scale padding and gaps, UIStroke ScaledSize, TextScaled + UITextSizeConstraint, UIAspectRatioConstraint on the root.",
31954
32491
  "Prompts new-screen, apply-design-system, reproduce-screenshot, import-website, figma-to-code, audit-and-fix and figma-to-roblox are ready-made workflows; the resources figma-bridge://docs/build-spec and figma-bridge://docs/workflow are the full reference.",
31955
32492
  "If no file is connected, tell the user to run Plugins → Development → Figma Bridge in Figma (Ctrl+Alt+P re-runs it). If list_sessions reports versionMismatch, ask them to reopen or update the plugin."
31956
32493
  ];
@@ -32076,7 +32613,7 @@ ${f.content}`).join(`
32076
32613
  const single = files.length === 1 && /\.[a-z0-9]+$/i.test(args.path);
32077
32614
  const written = [];
32078
32615
  for (const f of files) {
32079
- const target = single ? args.path : join9(args.path, f.path);
32616
+ const target = single ? args.path : join10(args.path, f.path);
32080
32617
  written.push({ format: f.format, path: target, status: await writeIfChanged(target, f.content) });
32081
32618
  }
32082
32619
  return ok({ counts, files: written, ...warnings.length ? { warnings } : {} });
@@ -32114,7 +32651,7 @@ ${f.content}`).join(`
32114
32651
  const bytes = Buffer.from(r.bytesB64, "base64");
32115
32652
  const info = imageInfo(bytes);
32116
32653
  const ext = r.format === "JPG" ? "jpg" : "png";
32117
- const path = join9(OUT_DIR, `${safeName(r.name)}-${safeName(r.nodeId)}-${Date.now()}.${ext}`);
32654
+ const path = join10(OUT_DIR, `${safeName(r.name)}-${safeName(r.nodeId)}-${Date.now()}.${ext}`);
32118
32655
  await writeFile(path, bytes);
32119
32656
  const meta = { path, nodeId: r.nodeId, name: r.name, width: info?.width, height: info?.height, bytes: bytes.length };
32120
32657
  const content = [{ type: "text", text: json(meta) }];
@@ -32141,7 +32678,7 @@ ${f.content}`).join(`
32141
32678
  const diff = await bridge.request("compare_images", { actual: shot.bytesB64, reference: Buffer.from(refBytes).toString("base64"), referenceMime: info.mime, threshold: args.threshold, sideBySide: !!args.returnImage }, 90000);
32142
32679
  const pct = (v) => Math.round(v * 1e4) / 100;
32143
32680
  const toLayer = (v, axis) => Math.round((v / shot.scale + shot.offset[axis]) * 10) / 10;
32144
- const heatmapPath = join9(OUT_DIR, `compare-${safeName(shot.name)}-${Date.now()}.png`);
32681
+ const heatmapPath = join10(OUT_DIR, `compare-${safeName(shot.name)}-${Date.now()}.png`);
32145
32682
  await writeFile(heatmapPath, Buffer.from(diff.heatmap, "base64"));
32146
32683
  const refRatio = diff.reference.width / diff.reference.height;
32147
32684
  const ratio = diff.width / diff.height;
@@ -32215,7 +32752,7 @@ Example: {"url":"https://example.com","viewports":[1440,390]}`,
32215
32752
  const built = await bridge.request("build", { spec: v.spec, images, parentId: args.parentId, x, y, select: false, label: "Web import" }, 120000);
32216
32753
  let reference;
32217
32754
  if (v.screenshot) {
32218
- reference = join9(OUT_DIR, `import-${safeName(v.title || "page")}-${v.viewport}-${Date.now()}.png`);
32755
+ reference = join10(OUT_DIR, `import-${safeName(v.title || "page")}-${v.viewport}-${Date.now()}.png`);
32219
32756
  await writeFile(reference, v.screenshot);
32220
32757
  }
32221
32758
  frames.push({ viewport: v.viewport, rootId: built.rootId, name: v.spec.name, layers: built.created, size: { width: v.width, height: v.height }, reference });
@@ -32285,7 +32822,7 @@ Links are added to existing interactions unless replace:true; clear:[ids] remove
32285
32822
  }
32286
32823
  }, ({ nodeId, projectPath, framework, styling, name, outDir, write, overwrite }) => track("export_code", `${nodeId ?? "(selection)"} ${projectPath ?? `${framework ?? "html"}/${styling ?? "css"}`}`, async () => {
32287
32824
  const r = await bridge.request("export_tree", { nodeId }, 120000);
32288
- const dir = join9(OUT_DIR, `export-${safeName(r.tree.name)}-${Date.now()}`);
32825
+ const dir = join10(OUT_DIR, `export-${safeName(r.tree.name)}-${Date.now()}`);
32289
32826
  const finalName = {};
32290
32827
  const assetBytes = {};
32291
32828
  for (const [file, a] of Object.entries(r.assets)) {
@@ -32301,8 +32838,8 @@ Links are added to existing interactions unless replace:true; clear:[ids] remove
32301
32838
  if (projectPath) {
32302
32839
  const plan = planProjectExport(projectPath, tree, assetBytes, { framework, styling, name, outDir });
32303
32840
  for (const f of plan.files) {
32304
- mkdirSync6(dirname4(join9(dir, f.path)), { recursive: true });
32305
- writeFileSync6(join9(dir, f.path), typeof f.content === "string" ? f.content : Buffer.from(f.content));
32841
+ mkdirSync6(dirname4(join10(dir, f.path)), { recursive: true });
32842
+ writeFileSync6(join10(dir, f.path), typeof f.content === "string" ? f.content : Buffer.from(f.content));
32306
32843
  }
32307
32844
  const written = write ? writePlan(projectPath, plan.files, !!overwrite) : null;
32308
32845
  const s = plan.stack;
@@ -32327,13 +32864,13 @@ ${f.content}`).join(`
32327
32864
  … truncated: the full code is in ${dir}`;
32328
32865
  return { content: [{ type: "text", text: json(meta) }, { type: "text", text }] };
32329
32866
  }
32330
- mkdirSync6(join9(dir, "assets"), { recursive: true });
32867
+ mkdirSync6(join10(dir, "assets"), { recursive: true });
32331
32868
  for (const [file, bytes] of Object.entries(assetBytes))
32332
- writeFileSync6(join9(dir, "assets", file), bytes);
32869
+ writeFileSync6(join10(dir, "assets", file), bytes);
32333
32870
  const fw = framework ?? "html";
32334
32871
  const code = generateCode(tree, { framework: fw, styling: styling ?? "css", assetPath: (f) => `assets/${f}` });
32335
32872
  for (const f of code.files)
32336
- writeFileSync6(join9(dir, f.path), f.content);
32873
+ writeFileSync6(join10(dir, f.path), f.content);
32337
32874
  warnings.push(...code.warnings);
32338
32875
  const meta = {
32339
32876
  dir,
@@ -32485,21 +33022,22 @@ ${f.content}`).join(`
32485
33022
  server.registerTool("export_roblox", {
32486
33023
  title: "Export a frame to Roblox UI",
32487
33024
  description: "Turn a Figma frame (default: selection) into Roblox UI that looks the same: a .rbxmx model (a ScreenGui, or a Frame with asRootFrame), an equivalent Luau builder " + `script for execute_luau (creates the UI under PARENT, default StarterGui, replaces a previous copy with the same name, returns the root), and the PNG assets.
32488
- ` + "Frames become Frames with UICorner, UIStroke, UIGradient, UIPadding; auto-layout becomes UIListLayout (UIFlexItem for fill, AutomaticSize for hug) or UIGridLayout; " + "constraints become AnchorPoint + UDim2; text becomes TextLabel with FontFace (Inter → Builder Sans…), RichText for mixed styles, UITextSizeConstraint; clickable or " + "*Button layers become TextButton/ImageButton. What Roblox can't draw (vectors, icons, blurs, several fills, radial gradients) is rasterized at 2×; panels whose background " + `can't be native become 9-slice images with their content on top; drop shadows become a 9-slice image behind the panel.
32489
- ` + "mode: scale (UDim2 scale everywhere), offset (pixels), hybrid (default: scale for free-positioned layers, pixels inside auto-layout). " + "Assets: with upload:true and ROBLOX_API_KEY + ROBLOX_CREATOR_ID set, pictures are uploaded through Open Cloud and their ids written in; otherwise each asset has a " + "placeholder (rbxassetid://PENDING_n): upload the files with your Roblox Studio tools (e.g. upload_image), replace the placeholders in the Luau, run it with execute_luau and check a Studio screenshot. " + 'Example: {"nodeId":"12:34","mode":"hybrid"}',
33025
+ ` + "Everything is in SCALE by default: sizes, positions, UIPadding, list gaps, grid cells, UICorner, UIStroke (StrokeSizingMode ScaledSize) and UIShadow are relative, " + "text uses TextScaled with a UITextSizeConstraint, and nothing is in offset. Instances get professional PascalCase names (ShopGui, ItemCard, TitleLabel, BuyButton, CoinIcon, " + `CardList, Header): the layer's own name, or its role when Figma named it (Frame 12 → Card, Container, Row…).
33026
+ ` + "Frames become Frames with UICorner, UIStroke, UIGradient, UIShadow, UIPadding; auto-layout becomes UIListLayout (UIFlexItem for fill) or UIGridLayout; " + "constraints become AnchorPoint + UDim2; text becomes TextLabel with FontFace (Inter → Builder Sans…), RichText for mixed styles; clickable or " + "*Button layers become TextButton/ImageButton. What Roblox can't draw (vectors, icons, blurs, several fills, radial gradients) is rasterized at 2×; panels whose background " + `can't be native become 9-slice images with their content on top.
33027
+ ` + "mode: scale (default, recommended); offset (pixels) or hybrid (pixels inside auto-layout) only when the user asks for them. " + "Assets: with upload:true and ROBLOX_API_KEY + ROBLOX_CREATOR_ID set, pictures are uploaded through Open Cloud and their ids written in; otherwise each asset has a " + "placeholder (rbxassetid://PENDING_n): upload the files with your Roblox Studio tools (e.g. upload_image), replace the placeholders in the Luau, run it with execute_luau and check a Studio screenshot. " + 'When writing Roblox UI yourself, follow the same rules: scale only, PascalCase role names. Example: {"nodeId":"12:34"}',
32490
33028
  inputSchema: {
32491
33029
  nodeId: string2().optional(),
32492
- mode: _enum(["scale", "offset", "hybrid"]).optional().describe("Default hybrid"),
33030
+ mode: _enum(["scale", "offset", "hybrid"]).optional().describe("Default scale (no offsets); offset or hybrid only if the user asks"),
32493
33031
  targetResolution: array(number2().int().min(100).max(8000)).length(2).optional().describe("Screen size the UI is designed for, default [1920, 1080]"),
32494
33032
  rasterize: _enum(["auto", "none", "all"]).optional().describe("auto (default): pictures only where Roblox can't draw it; none: approximate instead; all: every styled layer"),
32495
33033
  outDir: string2().optional().describe("Folder for the files (default: a new temp folder)"),
32496
33034
  upload: boolean2().optional().describe("Upload the pictures through Open Cloud (needs ROBLOX_API_KEY and ROBLOX_CREATOR_ID)"),
32497
33035
  asRootFrame: boolean2().optional().describe("Root is a Frame instead of a ScreenGui"),
32498
- textScaled: boolean2().optional().describe("TextScaled on labels (UITextSizeConstraint keeps the design size as the maximum)"),
33036
+ textScaled: boolean2().optional().describe("TextScaled on labels in offset or hybrid mode (always on in scale mode)"),
32499
33037
  fonts: object2({}).passthrough().optional().describe('Figma family → Roblox family or rbxasset URL, e.g. {"Inter":"GothamSSm"}'),
32500
33038
  parent: string2().optional().describe('Luau expression for the parent, default game:GetService("StarterGui")')
32501
33039
  }
32502
- }, (args) => track("export_roblox", `${args.nodeId ?? "(selection)"} ${args.mode ?? "hybrid"}`, async () => {
33040
+ }, (args) => track("export_roblox", `${args.nodeId ?? "(selection)"} ${args.mode ?? "scale"}`, async () => {
32503
33041
  const r = await exportRoblox({ ...args, targetResolution: args.targetResolution, fonts: args.fonts }, { request: (m, p, t) => bridge.request(m, p, t), outDir: OUT_DIR });
32504
33042
  let luau = r.luau;
32505
33043
  if (luau.length > MAX_OUTPUT_CHARS)
@@ -32631,154 +33169,20 @@ ${f.content}`).join(`
32631
33169
  });
32632
33170
 
32633
33171
  // src/cli.ts
32634
- import { homedir as homedir3 } from "node:os";
32635
- import { dirname as dirname5, join as join10 } from "node:path";
32636
- import { fileURLToPath as fileURLToPath2 } from "node:url";
32637
-
32638
- // src/setup.ts
32639
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
32640
- import { dirname, join } from "node:path";
32641
- var SERVER_NAME = "FigmaBridge";
32642
- function clientList(home, appData) {
32643
- return [
32644
- { id: "claude-code", label: "Claude Code", files: [join(home, ".claude.json")], format: "json", extra: { type: "stdio" } },
32645
- { id: "claude-desktop", label: "Claude Desktop", files: [join(appData, "Claude", "claude_desktop_config.json")], format: "json" },
32646
- { id: "codex", label: "Codex", files: [join(home, ".codex", "config.toml")], format: "toml" },
32647
- {
32648
- id: "antigravity",
32649
- label: "Antigravity",
32650
- files: [join(home, ".gemini", "antigravity", "mcp_config.json"), join(home, ".gemini", "config", "mcp_config.json")],
32651
- format: "json"
32652
- },
32653
- { id: "gemini-cli", label: "Gemini CLI", files: [join(home, ".gemini", "settings.json")], format: "json" },
32654
- { id: "cursor", label: "Cursor", files: [join(home, ".cursor", "mcp.json")], format: "json" },
32655
- { id: "windsurf", label: "Windsurf", files: [join(home, ".codeium", "windsurf", "mcp_config.json")], format: "json" }
32656
- ];
32657
- }
32658
- function backup(path, text) {
32659
- writeFileSync(path + ".bak-figma-bridge", text);
32660
- }
32661
- function writeJson(path, entry) {
32662
- const text = existsSync(path) ? readFileSync(path, "utf8") : "";
32663
- const config = text.trim() ? JSON.parse(text) : {};
32664
- if (text.trim())
32665
- backup(path, text);
32666
- config.mcpServers = { ...config.mcpServers, [SERVER_NAME]: entry };
32667
- mkdirSync(dirname(path), { recursive: true });
32668
- writeFileSync(path, JSON.stringify(config, null, 2) + `
32669
- `);
32670
- }
32671
- function tomlBlock(command, args) {
32672
- return [
32673
- `[mcp_servers.${SERVER_NAME}]`,
32674
- `command = '${command}'`,
32675
- `args = [${args.map((a) => `'${a}'`).join(", ")}]`,
32676
- "startup_timeout_sec = 60",
32677
- "tool_timeout_sec = 120"
32678
- ].join(`
32679
- `);
32680
- }
32681
- function writeToml(path, command, args) {
32682
- const text = existsSync(path) ? readFileSync(path, "utf8") : "";
32683
- if (text.trim())
32684
- backup(path, text);
32685
- const kept = [];
32686
- let skipping = false;
32687
- for (const line of text.split(/\r?\n/)) {
32688
- const header = /^\s*\[([^\]]+)\]\s*$/.exec(line);
32689
- if (header)
32690
- skipping = header[1] === `mcp_servers.${SERVER_NAME}` || header[1].startsWith(`mcp_servers.${SERVER_NAME}.`);
32691
- if (!skipping)
32692
- kept.push(line);
32693
- }
32694
- const body = kept.join(`
32695
- `).trimEnd();
32696
- mkdirSync(dirname(path), { recursive: true });
32697
- writeFileSync(path, (body ? body + `
32698
-
32699
- ` : "") + tomlBlock(command, args) + `
32700
- `);
32701
- }
32702
- function runSetup(opts) {
32703
- const all = clientList(opts.home, opts.appData);
32704
- const forced = opts.clients ?? [];
32705
- const unknown = forced.filter((id) => !all.some((c) => c.id === id));
32706
- if (unknown.length)
32707
- throw new Error(`Unknown client(s): ${unknown.join(", ")}. Known: ${all.map((c) => c.id).join(", ")}`);
32708
- const result = { updated: [], failed: [], missing: [] };
32709
- for (const client of all) {
32710
- const detected = client.files.filter((f) => existsSync(dirname(f)) && (client.id !== "gemini-cli" || existsSync(f)));
32711
- const targets = forced.length ? forced.includes(client.id) ? detected.length ? detected : [client.files[0]] : [] : detected;
32712
- if (!targets.length) {
32713
- if (!forced.length)
32714
- result.missing.push(client.label);
32715
- continue;
32716
- }
32717
- for (const file of targets) {
32718
- try {
32719
- if (client.format === "toml")
32720
- writeToml(file, opts.command, opts.args);
32721
- else
32722
- writeJson(file, { ...client.extra, command: opts.command, args: opts.args });
32723
- result.updated.push({ client: client.id, label: client.label, file });
32724
- } catch (e) {
32725
- result.failed.push({ client: client.id, label: client.label, file, error: e.message });
32726
- }
32727
- }
32728
- }
32729
- return result;
32730
- }
32731
- function installPlugin(from, to) {
32732
- mkdirSync(to, { recursive: true });
32733
- for (const f of ["manifest.json", "code.js", "ui.html"])
32734
- copyFileSync(join(from, f), join(to, f));
32735
- return join(to, "manifest.json");
32736
- }
32737
- var color = (code) => (s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s;
32738
- var bold = color(1);
32739
- var green = color(32);
32740
- var dim = color(2);
32741
- var yellow = color(33);
32742
- function printSetup(r, rerun) {
32743
- for (const label of r.missing)
32744
- console.log(dim(` – ${label.padEnd(15)} not found`));
32745
- for (const u of r.updated)
32746
- console.log(green(` ✔ ${u.label.padEnd(15)}`) + dim(u.file));
32747
- for (const f of r.failed)
32748
- console.log(yellow(` ! ${f.label.padEnd(15)}could not update ${f.file}: ${f.error}`));
32749
- if (!r.updated.length)
32750
- console.log(yellow(`
32751
- No AI client found. Use \`${rerun} --client <name>\` or \`${rerun} --print\` for manual config.`));
32752
- }
32753
- function printSnippets(command, args) {
32754
- console.log(bold("JSON clients") + dim(" (Claude, Antigravity, Gemini CLI, Cursor, Windsurf…)"));
32755
- console.log(JSON.stringify({ mcpServers: { [SERVER_NAME]: { command, args } } }, null, 2));
32756
- console.log(bold(`
32757
- Codex`) + dim(" (~/.codex/config.toml)"));
32758
- console.log(tomlBlock(command, args) + `
32759
- `);
32760
- }
32761
- function parseClients(argv) {
32762
- const out = [];
32763
- argv.forEach((a, i) => {
32764
- if (a === "--client" && argv[i + 1])
32765
- out.push(...argv[i + 1].split(","));
32766
- else if (a.startsWith("--client="))
32767
- out.push(...a.slice(9).split(","));
32768
- });
32769
- return out;
32770
- }
32771
-
32772
- // src/cli.ts
33172
+ init_setup();
32773
33173
  init_package();
33174
+ import { homedir as homedir4 } from "node:os";
33175
+ import { dirname as dirname5, join as join11 } from "node:path";
33176
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
32774
33177
  var PACKAGE = "@clovis500c/figma-bridge";
32775
33178
  var [cmd, ...rest] = process.argv.slice(2);
32776
- var home = homedir3();
32777
- var pluginSource = join10(dirname5(fileURLToPath2(import.meta.url)), "..", "plugin");
32778
- var pluginTarget = join10(home, ".figma-bridge", "plugin");
32779
- var npx = process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", "-y", PACKAGE] } : { command: "npx", args: ["-y", PACKAGE] };
33179
+ var home = homedir4();
33180
+ var pluginSource = join11(dirname5(fileURLToPath2(import.meta.url)), "..", "plugin");
33181
+ var pluginTarget = join11(home, ".figma-bridge", "plugin");
33182
+ var spec = `${PACKAGE}@latest`;
33183
+ var npx = process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", "-y", spec] } : { command: "npx", args: ["-y", spec] };
32780
33184
  function plugin() {
32781
- const manifest = installPlugin(pluginSource, pluginTarget);
33185
+ const manifest = installPlugin(pluginSource, pluginTarget, version);
32782
33186
  console.log(`${bold("Figma plugin")} copied to ${dim(pluginTarget)}`);
32783
33187
  return manifest;
32784
33188
  }
@@ -32787,7 +33191,7 @@ function next(manifest) {
32787
33191
  ${bold("Next")}
32788
33192
  1. Figma desktop → Plugins → Development → Import plugin from manifest…
32789
33193
  ${green(manifest)}
32790
- (once; after an update, run ${bold(`npx ${PACKAGE} plugin`)} again and reopen the plugin)
33194
+ (once: updates install themselves when your AI client restarts; then reopen the plugin)
32791
33195
  2. Restart your AI client, then run Plugins → Development → Figma Bridge in a Figma file.
32792
33196
  `);
32793
33197
  }
@@ -32805,7 +33209,7 @@ Figma Bridge setup
32805
33209
  break;
32806
33210
  }
32807
33211
  try {
32808
- const appData = process.env.APPDATA || join10(home, "AppData", "Roaming");
33212
+ const appData = process.env.APPDATA || join11(home, "AppData", "Roaming");
32809
33213
  printSetup(runSetup({ home, appData, command: npx.command, args: npx.args, clients: parseClients(rest) }), `npx ${PACKAGE} setup`);
32810
33214
  } catch (e) {
32811
33215
  console.log(yellow(e.message));