@clovis500c/figma-bridge 1.14.0 → 1.14.2

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
@@ -214,7 +214,7 @@ var init_setup = __esm(() => {
214
214
  });
215
215
 
216
216
  // package.json
217
- var version = "1.14.0";
217
+ var version = "1.14.2";
218
218
  var init_package = () => {};
219
219
 
220
220
  // node_modules/zod/v3/helpers/util.js
@@ -27735,6 +27735,8 @@ function decide(n, opts) {
27735
27735
  const strokes = visible(n.strokes);
27736
27736
  const other = (n.effects ?? []).filter((e) => e.type !== "DROP_SHADOW");
27737
27737
  const hasChildren = !!n.children?.some((c) => !c.hidden);
27738
+ if (n.rasterize && raster !== "none")
27739
+ return { kind: "picture", approximations: ["image file unreadable: pictured as it looks"] };
27738
27740
  if (n.vector || VECTOR_TYPES.includes(n.type)) {
27739
27741
  return raster === "none" ? { kind: "frame", approximations: ["vector shape skipped (rasterize is none)"] } : { kind: "picture", approximations };
27740
27742
  }
@@ -27896,7 +27898,10 @@ class Mapper {
27896
27898
  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)]];
27897
27899
  top.props = top.props.filter(([k]) => !["AnchorPoint", "Position", "Size", "LayoutOrder", "ZIndex", "AutomaticSize"].includes(k));
27898
27900
  top.props.splice(1, 0, ...geo);
27899
- top.children.push(this.inst("UIAspectRatioConstraint", "UIAspectRatioConstraint", [["AspectRatio", F(root.w / root.h)]]));
27901
+ const ratio = [["AspectRatio", F(root.w / root.h)]];
27902
+ if (this.opts.mode !== "offset" && !fullScreen)
27903
+ ratio.push(["DominantAxis", E("DominantAxis", "Height")]);
27904
+ top.children.push(this.inst("UIAspectRatioConstraint", "UIAspectRatioConstraint", ratio));
27900
27905
  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
27906
  dedupe(out);
27902
27907
  return out;
@@ -27934,7 +27939,12 @@ class Mapper {
27934
27939
  out.push(["LayoutOrder", I(index + 1)]);
27935
27940
  return out;
27936
27941
  }
27937
- const c = n.constraints ?? { h: "MIN", v: "MIN" };
27942
+ const c = { ...n.constraints ?? { h: "MIN", v: "MIN" } };
27943
+ const centered = this.centered(n, parent);
27944
+ if (centered.x && c.h === "MIN")
27945
+ c.h = "CENTER";
27946
+ if (centered.y && c.v === "MIN")
27947
+ c.v = "CENTER";
27938
27948
  const axis = (pos, size, total, k) => {
27939
27949
  const end = total - pos - size;
27940
27950
  if (k === "SCALE" || f.scale) {
@@ -27961,6 +27971,15 @@ class Mapper {
27961
27971
  out.push(["ZIndex", I(index + 1)]);
27962
27972
  return out;
27963
27973
  }
27974
+ centered(n, parent) {
27975
+ if (!parent || parent.layout && !n.absolute)
27976
+ return { x: false, y: false };
27977
+ const t = n.text?.autoResize;
27978
+ const hugX = t === "WIDTH_AND_HEIGHT" || !n.text && n.sizing?.h === "HUG";
27979
+ const hugY = t === "WIDTH_AND_HEIGHT" || t === "HEIGHT" || !n.text && n.sizing?.v === "HUG";
27980
+ const near = (pos, size, total) => Math.abs(pos + size / 2 - total / 2) <= 1.5 && size < total;
27981
+ return { x: hugX && near(n.x, n.w, parent.w), y: hugY && near(n.y, n.h, parent.h) };
27982
+ }
27964
27983
  automaticSize(n, d) {
27965
27984
  const hug = this.hugs(n, d);
27966
27985
  if (!hug.w && !hug.h)
@@ -28181,6 +28200,9 @@ class Mapper {
28181
28200
  const isButton = !!n.clickable || BUTTON_NAME.test(n.name);
28182
28201
  className = isButton ? "TextButton" : "TextLabel";
28183
28202
  props = this.textProps(n);
28203
+ const centered = this.centered(n, parent);
28204
+ const auto = n.text.autoResize === "WIDTH_AND_HEIGHT" || n.text.autoResize === "HEIGHT";
28205
+ props = props.map(([k, v]) => k === "TextXAlignment" && centered.x ? [k, E("TextXAlignment", "Center")] : k === "TextYAlignment" && auto && n.text.valign === "TOP" ? [k, E("TextYAlignment", "Center")] : [k, v]);
28184
28206
  if (isButton)
28185
28207
  props.push(["AutoButtonColor", B(false)]);
28186
28208
  if ((n.effects ?? []).some((e) => e.type === "DROP_SHADOW"))
@@ -28188,7 +28210,8 @@ class Mapper {
28188
28210
  const size = props.find(([k]) => k === "TextSize")[1].v;
28189
28211
  const tight = n.text.autoResize === "WIDTH_AND_HEIGHT" || n.text.autoResize === "HEIGHT";
28190
28212
  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)))]]));
28213
+ const min = this.scaleOnly ? 1 : Math.max(1, Math.round(size * 0.5));
28214
+ children.push(this.inst("UITextSizeConstraint", "UITextSizeConstraint", [["MaxTextSize", I(max)], ["MinTextSize", I(min)]]));
28192
28215
  return this.inst(className, this.label(n, isButton ? "button" : "text"), [...common, ...props], children, n.id);
28193
28216
  }
28194
28217
  if (d.kind === "picture") {
@@ -28230,20 +28253,14 @@ class Mapper {
28230
28253
  if (d.kind === "panel") {
28231
28254
  this.counts.pictures++;
28232
28255
  const a = this.assetUrl(`${n.id}:panel`, n);
28233
- const scale = this.opts.scale ?? 2;
28256
+ const scale = a.scale ?? this.opts.scale ?? 2;
28234
28257
  const inset = Math.ceil((Math.max(0, ...n.radius ?? [0]) + (n.strokeWeight ?? 0) + 2) * scale);
28235
- const pw = Math.round((a.size?.w ?? n.w) * scale);
28236
- const ph = Math.round((a.size?.h ?? n.h) * scale);
28258
+ const pw = a.px?.w ?? Math.round((a.size?.w ?? n.w) * scale);
28259
+ const ph = a.px?.h ?? Math.round((a.size?.h ?? n.h) * scale);
28237
28260
  const ix = Math.min(inset, Math.floor(pw / 2) - 1);
28238
28261
  const iy = Math.min(inset, Math.floor(ph / 2) - 1);
28239
28262
  className = button ? "ImageButton" : "ImageLabel";
28240
- props = [
28241
- ["BackgroundTransparency", F(1)],
28242
- ["Image", { t: "Content", url: a.url }],
28243
- ["ScaleType", E("ScaleType", "Slice")],
28244
- ["SliceCenter", { t: "Rect", x0: ix, y0: iy, x1: pw - ix, y1: ph - iy }],
28245
- ["SliceScale", F(1 / scale)]
28246
- ];
28263
+ props = [["BackgroundTransparency", F(1)], ["Image", { t: "Content", url: a.url }], ...this.imageScaling(n, ix, iy, pw, ph, scale)];
28247
28264
  if (opacity < 1)
28248
28265
  props.push(["ImageTransparency", F(1 - opacity)]);
28249
28266
  if (button)
@@ -28319,14 +28336,27 @@ class Mapper {
28319
28336
  const flavor = button ? "button" : fills.length || visible(n.strokes).length || d.kind === "panel" ? "frame" : "container";
28320
28337
  return this.inst(className, this.label(n, flavor), [...common, ...props], children, n.id);
28321
28338
  }
28339
+ imageScaling(n, ix, iy, pw, ph, scale) {
28340
+ if (this.scaleOnly)
28341
+ return [["ScaleType", E("ScaleType", "Stretch")]];
28342
+ const resizes = n.grow || n.stretch || n.sizing?.h === "FILL" || n.sizing?.v === "FILL" || n.sizing?.h === "HUG" || n.sizing?.v === "HUG";
28343
+ if (this.opts.mode === "offset" && !resizes)
28344
+ return [["ScaleType", E("ScaleType", "Stretch")]];
28345
+ return [
28346
+ ["ScaleType", E("ScaleType", "Slice")],
28347
+ ["SliceCenter", { t: "Rect", x0: ix, y0: iy, x1: pw - ix, y1: ph - iy }],
28348
+ ["SliceScale", F(1 / scale)]
28349
+ ];
28350
+ }
28322
28351
  pictureGeometry(n, a, geo, parent) {
28323
28352
  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)
28324
28353
  return geo;
28325
28354
  const f = this.frameOf(n, parent);
28326
28355
  const dw = a.size.w - n.w;
28327
28356
  const dh = a.size.h - n.h;
28328
- const ox = a.offset.x;
28329
- const oy = a.offset.y;
28357
+ const anchor = geo.find(([k]) => k === "AnchorPoint")?.[1];
28358
+ const ox = a.offset.x + (anchor?.x ?? 0) * dw;
28359
+ const oy = a.offset.y + (anchor?.y ?? 0) * dh;
28330
28360
  return geo.map(([k, v]) => {
28331
28361
  if (v.t !== "UDim2" || k !== "Size" && k !== "Position")
28332
28362
  return [k, v];
@@ -28348,7 +28378,7 @@ var num = (v) => Math.round(v * 1e4) / 1e4, S = (v) => ({ t: "string", v }), B =
28348
28378
  var init_map2 = __esm(() => {
28349
28379
  init_naming();
28350
28380
  init_fonts();
28351
- BUTTON_NAME = /button|btn|\bcta\b/i;
28381
+ BUTTON_NAME = /button|btn|\bcta\b|^(close|back|buy|confirm|cancel|next|previous|prev|exit|play|claim|equip|purchase)$/i;
28352
28382
  VECTOR_TYPES = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
28353
28383
  ICON_WORDS = ["Icon", "Logo", "Image", "Illustration", "Avatar", "Emoji"];
28354
28384
  IMAGE_WORDS = ["Image", "Icon", "Avatar", "Logo", "Thumbnail", "Picture", "Photo", "Banner", "Background", "Illustration", "Art", "Cover"];
@@ -28476,15 +28506,23 @@ function toLuau(root, opts = {}) {
28476
28506
  const lines = [];
28477
28507
  const emit = (i, parentVar, depth) => {
28478
28508
  const v = identifier(i.name, i.className, used);
28509
+ const outer = "\t".repeat(Math.max(0, depth - 1));
28510
+ const tab = "\t".repeat(depth);
28511
+ const add = (line) => lines.push(tab + line.replace(/\n/g, `
28512
+ ${tab}`));
28479
28513
  if (!/^UI/.test(i.className) && depth <= 2 && parentVar)
28480
- lines.push("", `-- ${i.name.replace(/\n/g, " ")}`);
28481
- lines.push(`local ${v} = Instance.new(${luauString(i.className)})`);
28514
+ lines.push("", `${outer}-- ${i.name.replace(/\n/g, " ")}`);
28515
+ if (parentVar)
28516
+ lines.push(`${outer}do`);
28517
+ add(`local ${v} = Instance.new(${luauString(i.className)})`);
28482
28518
  for (const [k, val] of i.props)
28483
- lines.push(`${v}.${k} = ${luauValue(val)}`);
28519
+ add(`${v}.${k} = ${luauValue(val)}`);
28484
28520
  for (const c of i.children)
28485
28521
  emit(c, v, depth + 1);
28486
- if (parentVar)
28487
- lines.push(`${v}.Parent = ${parentVar}`);
28522
+ if (parentVar) {
28523
+ add(`${v}.Parent = ${parentVar}`);
28524
+ lines.push(`${outer}end`);
28525
+ }
28488
28526
  return v;
28489
28527
  };
28490
28528
  const header = [
@@ -28516,6 +28554,7 @@ function substituteAssets(text, ids) {
28516
28554
  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);
28517
28555
  var init_output = __esm(() => {
28518
28556
  ENUMS = {
28557
+ DominantAxis: { Width: 0, Height: 1 },
28519
28558
  ZIndexBehavior: { Global: 0, Sibling: 1 },
28520
28559
  FillDirection: { Horizontal: 0, Vertical: 1 },
28521
28560
  HorizontalAlignment: { Center: 0, Left: 1, Right: 2 },
@@ -28655,7 +28694,7 @@ async function exportRoblox(args, deps) {
28655
28694
  const r = await deps.request("roblox_images", { items: wanted.slice(i, i + 20).map((w) => ({ ...w, scale: opts.scale })) }, 120000);
28656
28695
  for (const img of r.images) {
28657
28696
  if (img.b64)
28658
- pictures.set(`${img.id}:${img.mode}`, { b64: img.b64, offset: img.offset, size: img.size });
28697
+ pictures.set(`${img.id}:${img.mode}`, { b64: img.b64, offset: img.offset, size: img.size, scale: img.scale });
28659
28698
  else
28660
28699
  warnings.push(`Picture of ${img.id} failed: ${img.error}`);
28661
28700
  }
@@ -28681,7 +28720,14 @@ async function exportRoblox(args, deps) {
28681
28720
  const p = pictures.get(`${w.id}:${w.mode}`);
28682
28721
  if (!p)
28683
28722
  continue;
28684
- add(`${w.id}:${w.mode}`, new Uint8Array(Buffer.from(p.b64, "base64")), { nodeId: w.id, layer: names.get(w.id) ?? w.id, kind: "picture", mode: w.mode }, "png", { offset: p.offset, size: p.size });
28723
+ const bytes = new Uint8Array(Buffer.from(p.b64, "base64"));
28724
+ const info = imageInfo(bytes);
28725
+ add(`${w.id}:${w.mode}`, bytes, { nodeId: w.id, layer: names.get(w.id) ?? w.id, kind: "picture", mode: w.mode }, "png", {
28726
+ offset: p.offset,
28727
+ size: p.size,
28728
+ scale: p.scale,
28729
+ ...info ? { px: { w: info.width, h: info.height } } : {}
28730
+ });
28685
28731
  }
28686
28732
  for (const hash of imageHashes(t.tree)) {
28687
28733
  const b64 = t.images[hash];
@@ -30770,6 +30816,15 @@ function snapshot(opts) {
30770
30816
  return false;
30771
30817
  }
30772
30818
  const SKIP = ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "HEAD", "META", "LINK", "TITLE", "BASE"];
30819
+ const skewed = (t) => {
30820
+ if (!t || t === "none")
30821
+ return false;
30822
+ const m = /^matrix(([^)]+))$/.exec(t);
30823
+ if (!m)
30824
+ return true;
30825
+ const v = m[1].split(",").map(Number);
30826
+ return Math.abs(v[1]) > 0.001 || Math.abs(v[2]) > 0.001;
30827
+ };
30773
30828
  const RASTER = ["CANVAS", "VIDEO", "IFRAME", "OBJECT", "EMBED", "AUDIO", "MATH"];
30774
30829
  function isPlainInline(el, cs) {
30775
30830
  if (cs.display !== "inline" || el.tagName === "IMG" || el.tagName === "svg" || el.tagName === "INPUT" || el.tagName === "BUTTON" || el.tagName === "SELECT" || el.tagName === "TEXTAREA")
@@ -30976,10 +31031,11 @@ function snapshot(opts) {
30976
31031
  return [{ kind: "image", tag, name, box, css: cssOf(cs), children: [], src: img.currentSrc || img.src }];
30977
31032
  }
30978
31033
  const nativeControl = tag === "input" && /^(checkbox|radio|range|color|file)$/i.test(el.type) || tag === "select" && cs.appearance !== "none";
30979
- if (RASTER.indexOf(el.tagName) !== -1 || nativeControl) {
31034
+ if (RASTER.indexOf(el.tagName) !== -1 || nativeControl || skewed(cs.transform) && !(el.textContent || "").trim()) {
30980
31035
  if (box.w < 0.5 || box.h < 0.5)
30981
31036
  return [];
30982
- return [{ kind: "raster", tag, name, box, css: cssOf(cs), children: [] }];
31037
+ el.setAttribute("data-fb-pic", String(count));
31038
+ return [{ kind: "raster", tag, name, box, css: cssOf(cs), children: [], ref: count }];
30983
31039
  }
30984
31040
  const node = { kind: "box", tag, name, box, css: cssOf(cs), children: [] };
30985
31041
  if (tag === "input" || tag === "textarea") {
@@ -31161,6 +31217,7 @@ async function render(source, opts) {
31161
31217
  screenshot = opts.selector ? await page.locator(opts.selector).first().screenshot() : await page.screenshot({ fullPage: true, clip });
31162
31218
  } catch {}
31163
31219
  const r = { viewport: width, snap, screenshot };
31220
+ let queue = Promise.resolve();
31164
31221
  await opts.withPage(r, {
31165
31222
  fetch: async (u) => {
31166
31223
  const res = await context.request.get(u, { timeout: 20000 });
@@ -31168,7 +31225,24 @@ async function render(source, opts) {
31168
31225
  throw new Error(`HTTP ${res.status()}`);
31169
31226
  return new Uint8Array(await res.body());
31170
31227
  },
31171
- picture: async (b) => new Uint8Array(await page.screenshot({ fullPage: true, clip: { x: b.x, y: b.y, width: Math.max(1, b.w), height: Math.max(1, b.h) }, omitBackground: true }))
31228
+ picture: (b, ref) => {
31229
+ const shot = queue.then(async () => {
31230
+ const clip = { x: b.x, y: b.y, width: Math.max(1, b.w), height: Math.max(1, b.h) };
31231
+ if (ref === undefined)
31232
+ return new Uint8Array(await page.screenshot({ fullPage: true, clip, omitBackground: true }));
31233
+ const sel = `[data-fb-pic="${ref}"]`;
31234
+ const style = await page.addStyleTag({
31235
+ content: `html,body{background:transparent!important}body *{visibility:hidden!important}${sel},${sel} *{visibility:visible!important}`
31236
+ });
31237
+ try {
31238
+ return new Uint8Array(await page.screenshot({ fullPage: true, clip, omitBackground: true }));
31239
+ } finally {
31240
+ await style.evaluate((el) => el.remove()).catch(() => {});
31241
+ }
31242
+ });
31243
+ queue = shot.catch(() => {});
31244
+ return shot;
31245
+ }
31172
31246
  });
31173
31247
  out.push(r);
31174
31248
  } finally {
@@ -31402,13 +31476,13 @@ class Converter {
31402
31476
  if (this.warnings.size < 40)
31403
31477
  this.warnings.add(msg);
31404
31478
  }
31405
- asset(url, box, layer) {
31479
+ asset(url, box, layer, ref) {
31406
31480
  if (url && !layer && this.urlKeys.has(url))
31407
31481
  return this.urlKeys.get(url);
31408
31482
  if (url && layer && this.urlKeys.has(url) && this.assets[this.urlKeys.get(url)].layer)
31409
31483
  return this.urlKeys.get(url);
31410
31484
  const key = `w${this.n++}`;
31411
- this.assets[key] = { url, box, layer };
31485
+ this.assets[key] = ref === undefined ? { url, box, layer } : { url, box, layer, ref };
31412
31486
  if (url)
31413
31487
  this.urlKeys.set(url, key);
31414
31488
  return key;
@@ -31470,7 +31544,7 @@ class Converter {
31470
31544
  if (n.kind === "image" || n.kind === "raster") {
31471
31545
  this.stats.images++;
31472
31546
  const fits = { contain: "fit", cover: "fill", none: "crop", "scale-down": "fit" };
31473
- const key = this.asset(n.kind === "image" ? n.src : undefined, n.box, true);
31547
+ const key = this.asset(n.kind === "image" ? n.src : undefined, n.box, true, n.ref);
31474
31548
  return {
31475
31549
  type: "image",
31476
31550
  name: n.name || TAG_NAMES[n.tag] || "Image",
@@ -31516,7 +31590,9 @@ class Converter {
31516
31590
  spec.clip = true;
31517
31591
  const flow = n.children.filter((c) => !outOfFlow(c));
31518
31592
  const positioned = n.children.filter(outOfFlow);
31519
- const layout = flow.length ? this.layout(n, flow) : null;
31593
+ let layout = flow.length ? this.layout(n, flow) : null;
31594
+ if (layout && layout.dir === "grid" && !this.opts.grid)
31595
+ layout = null;
31520
31596
  const children = [];
31521
31597
  if (layout) {
31522
31598
  this.stats.autoLayout++;
@@ -31539,6 +31615,8 @@ class Converter {
31539
31615
  child.colSpan = item.colSpan;
31540
31616
  if (item.rowSpan && item.rowSpan > 1)
31541
31617
  child.rowSpan = item.rowSpan;
31618
+ if (layout.dir === "grid")
31619
+ child.place = [r1(item.node.box.x - n.box.x), r1(item.node.box.y - n.box.y), r1(item.node.box.w), r1(item.node.box.h)];
31542
31620
  if (item.wrap) {
31543
31621
  this.stats.layers++;
31544
31622
  children.push({
@@ -31554,28 +31632,32 @@ class Converter {
31554
31632
  }
31555
31633
  spec.w = layout.fixedW || children.some((c) => c.w === "fill") ? n.box.w : "hug";
31556
31634
  spec.h = layout.fixedH || children.some((c) => c.h === "fill") ? n.box.h : "hug";
31635
+ if (layout.dir === "grid")
31636
+ spec.gridSize = [r1(n.box.w), r1(n.box.h)];
31557
31637
  } else {
31558
31638
  if (flow.length)
31559
31639
  this.stats.free++;
31560
31640
  spec.w = n.box.w;
31561
31641
  spec.h = n.box.h;
31562
- for (const c of paintOrder(flow)) {
31563
- const child = this.node(c);
31564
- if (!child)
31565
- break;
31566
- children.push(this.placeAt(child, c, n));
31567
- }
31568
31642
  }
31569
- for (const c of paintOrder(positioned)) {
31643
+ const free = layout ? positioned : n.children;
31644
+ const floor = layout ? Math.min(...flow.map(stackLevel)) : Infinity;
31645
+ const below = [];
31646
+ for (const c of paintOrder(free)) {
31570
31647
  const child = this.node(c);
31571
31648
  if (!child)
31572
31649
  break;
31573
- children.push({ ...this.placeAt(child, c, n), ...layout ? { absolute: true } : {} });
31650
+ const placed = { ...this.placeAt(child, c, n), ...layout ? { absolute: true } : {} };
31651
+ if (layout && stackLevel(c) < floor)
31652
+ below.push(placed);
31653
+ else
31654
+ children.push(placed);
31574
31655
  }
31656
+ children.unshift(...below);
31575
31657
  if (children.length)
31576
31658
  spec.children = children;
31577
31659
  if (!spec.name)
31578
- spec.name = layout ? layout.dir === "grid" ? "Grid" : layout.dir === "row" ? "Row" : "Column" : "Frame";
31660
+ spec.name = layout ? layout.dir === "grid" ? "Grid" : layout.dir === "row" ? "Row" : "Column" : "Container";
31579
31661
  return spec;
31580
31662
  }
31581
31663
  placeAt(child, c, parent) {
@@ -31639,7 +31721,7 @@ class Converter {
31639
31721
  return span;
31640
31722
  });
31641
31723
  }
31642
- if (!n.autoWidth || (n.lines ?? 1) > 1)
31724
+ if ((n.lines ?? 1) > 1 || !n.autoWidth && align)
31643
31725
  spec.w = n.box.w;
31644
31726
  return spec;
31645
31727
  }
@@ -31674,9 +31756,17 @@ function label(n) {
31674
31756
  function decorationOf(v) {
31675
31757
  return /underline/.test(v) ? "underline" : /line-through/.test(v) ? "strike" : undefined;
31676
31758
  }
31759
+ function stackLevel(n) {
31760
+ const z = n.css.zIndex && n.css.zIndex !== "auto" ? Number(n.css.zIndex) || 0 : null;
31761
+ if (z !== null && z < 0)
31762
+ return z - 1;
31763
+ const positioned = !!n.css.position && n.css.position !== "static";
31764
+ if (!positioned && z === null)
31765
+ return 0;
31766
+ return 1 + Math.max(0, z ?? 0);
31767
+ }
31677
31768
  function paintOrder(list) {
31678
- const z = (n) => n.css.zIndex && n.css.zIndex !== "auto" ? Number(n.css.zIndex) || 0 : 0;
31679
- return list.map((n, i) => ({ n, i })).sort((a, b) => z(a.n) - z(b.n) || a.i - b.i).map((x) => x.n);
31769
+ return list.map((n, i) => ({ n, i })).sort((a, b) => stackLevel(a.n) - stackLevel(b.n) || a.i - b.i).map((x) => x.n);
31680
31770
  }
31681
31771
  function padOf(n) {
31682
31772
  const b = borderWidths(n.css);
@@ -32126,7 +32216,7 @@ async function importWeb(opts, deps) {
32126
32216
  }
32127
32217
  if (a.layer) {
32128
32218
  try {
32129
- images[key] = await page.picture(a.box);
32219
+ images[key] = await page.picture(a.box, a.ref);
32130
32220
  return;
32131
32221
  } catch {}
32132
32222
  }
@@ -32473,6 +32563,7 @@ var init_server3 = __esm(async () => {
32473
32563
  "Workflow:",
32474
32564
  "1. get_context, then get_design_system when the file has styles, variables or components: reuse them instead of raw values. find locates layers by name, text, type, style or component across pages.",
32475
32565
  "2. New UI → build (one call per screen or section, with auto-layout; grid, rich text spans, component sets with variants, prototype reactions). Editing existing design → describe it first, then build into it (parentId) or run_script.",
32566
+ "Name every layer by its role, like a design system would: Header, NavBar, HeroSection, Content, PricingCard, CardTitle, Price, PrimaryButton, Avatar & Info, Icon/Search; components as Category/Variant (Button/Primary). Never leave Frame 12 or Rectangle 3: names become class names in code and instance names in Roblox.",
32476
32567
  "3. Design system → design_tokens writes variables (with Light/Dark modes) and styles from simple JSON, W3C tokens or a Tailwind theme; build uses them via var:, style: and modes.",
32477
32568
  "4. Verify → screenshot with returnImage:true, and audit to catch contrast, overflow and naming issues. Fix, then check again. audit {scope:'design-system'} scores the file's design system; audit {fix:true} binds raw values to variables and styles.",
32478
32569
  "Reproduce a mockup or screenshot: build it at the mockup's size → compare {nodeId, reference, returnImage:true} → fix the largest regions → compare again until mismatchPercent stops dropping.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clovis500c/figma-bridge",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "type": "module",
5
5
  "description": "Local MCP bridge that lets any AI agent design in the Figma desktop app, with no rate limits.",
6
6
  "keywords": [
package/plugin/code.js CHANGED
@@ -2599,6 +2599,8 @@
2599
2599
  if (mode === "GRID") {
2600
2600
  if (await setupGrid(f, s, ctx, path))
2601
2601
  return;
2602
+ if (Array.isArray(s.gridSize))
2603
+ return placeGridItems(f, s);
2602
2604
  mode = "HORIZONTAL";
2603
2605
  s = Object.assign({}, s, { wrap: true, rowGap: s.rowGap !== undefined ? s.rowGap : s.gap, gap: s.columnGap !== undefined ? s.columnGap : s.gap });
2604
2606
  }
@@ -2643,7 +2645,28 @@
2643
2645
  return { type: "FIXED", value: parseFloat(px[1]) };
2644
2646
  return fallback === "FLEX" ? { type: "FLEX", value: 1 } : { type: "HUG" };
2645
2647
  }
2648
+ var GRID_FALLBACK = `Figma's grid track API failed (a Figma bug: "invalid id"): grids were built as wrapping rows, or at their measured positions for imported pages.`;
2649
+ var gridTracksBroken = false;
2650
+ function tracksReadable(f) {
2651
+ try {
2652
+ const t = f.gridColumnSizes[0];
2653
+ return !!t && typeof t.type === "string";
2654
+ } catch (e) {
2655
+ return false;
2656
+ }
2657
+ }
2646
2658
  async function setupGrid(f, s, ctx, path) {
2659
+ const fallback = function() {
2660
+ gridTracksBroken = true;
2661
+ try {
2662
+ f.layoutMode = "NONE";
2663
+ } catch (e) {}
2664
+ if (ctx.warnings.indexOf(GRID_FALLBACK) === -1)
2665
+ ctx.warnings.push(GRID_FALLBACK);
2666
+ return false;
2667
+ };
2668
+ if (gridTracksBroken)
2669
+ return fallback();
2647
2670
  try {
2648
2671
  f.layoutMode = "GRID";
2649
2672
  } catch (e) {
@@ -2673,14 +2696,16 @@
2673
2696
  set("rows", function() {
2674
2697
  f.gridRowCount = rowCount;
2675
2698
  });
2676
- set("column sizes", function() {
2699
+ if (!tracksReadable(f))
2700
+ return fallback();
2701
+ try {
2677
2702
  for (let i = 0;i < colCount; i++)
2678
- applyTrack(f.gridColumnSizes[i], track(cols ? cols[i] : undefined, fixedW ? "FLEX" : "HUG"));
2679
- });
2680
- set("row sizes", function() {
2703
+ applyTrack(f, "gridColumnSizes", i, track(cols ? cols[i] : undefined, fixedW ? "FLEX" : "HUG"));
2681
2704
  for (let i = 0;i < rowCount; i++)
2682
- applyTrack(f.gridRowSizes[i], track(rows ? rows[i] : undefined, fixedH ? "FLEX" : "HUG"));
2683
- });
2705
+ applyTrack(f, "gridRowSizes", i, track(rows ? rows[i] : undefined, fixedH ? "FLEX" : "HUG"));
2706
+ } catch (e) {
2707
+ return fallback();
2708
+ }
2684
2709
  if (!fixedW)
2685
2710
  set("sizing", function() {
2686
2711
  f.layoutSizingHorizontal = "HUG";
@@ -2703,12 +2728,30 @@
2703
2728
  await setPadding(f, s.padding);
2704
2729
  return true;
2705
2730
  }
2706
- function applyTrack(t, spec) {
2707
- if (!t)
2731
+ function placeGridItems(f, s) {
2732
+ if (s.w === "hug" || s.w === undefined)
2733
+ s.w = s.gridSize[0];
2734
+ if (s.h === "hug" || s.h === undefined)
2735
+ s.h = s.gridSize[1];
2736
+ const kids = Array.isArray(s.children) ? s.children : [];
2737
+ for (let i = 0;i < kids.length; i++) {
2738
+ const p = kids[i] && kids[i].place;
2739
+ if (!Array.isArray(p))
2740
+ continue;
2741
+ kids[i].x = p[0];
2742
+ kids[i].y = p[1];
2743
+ kids[i].w = p[2];
2744
+ if (kids[i].type !== "text" || kids[i].h !== undefined)
2745
+ kids[i].h = p[3];
2746
+ }
2747
+ }
2748
+ function applyTrack(f, field, i, spec) {
2749
+ if (!f[field][i])
2708
2750
  return;
2709
- t.type = spec.type;
2710
- if (spec.value !== undefined && spec.type !== "HUG")
2711
- t.value = spec.value;
2751
+ if (f[field][i].type !== spec.type)
2752
+ f[field][i].type = spec.type;
2753
+ if (spec.value !== undefined && spec.type !== "HUG" && f[field][i].value !== spec.value)
2754
+ f[field][i].value = spec.value;
2712
2755
  }
2713
2756
  function spanOf(s, axis) {
2714
2757
  if (!s || typeof s !== "object")
@@ -3127,10 +3170,13 @@
3127
3170
  const right = v[1] === undefined ? v[0] : v[1];
3128
3171
  const bottom = v[2] === undefined ? v[0] : v[2];
3129
3172
  const left = v[3] === undefined ? right : v[3];
3130
- await setNumber(f, "paddingTop", top);
3131
- await setNumber(f, "paddingRight", right);
3132
- await setNumber(f, "paddingBottom", bottom);
3133
- await setNumber(f, "paddingLeft", left);
3173
+ const clamp = function(x) {
3174
+ return typeof x === "number" ? Math.max(0, x) : x;
3175
+ };
3176
+ await setNumber(f, "paddingTop", clamp(top));
3177
+ await setNumber(f, "paddingRight", clamp(right));
3178
+ await setNumber(f, "paddingBottom", clamp(bottom));
3179
+ await setNumber(f, "paddingLeft", clamp(left));
3134
3180
  }
3135
3181
  function lineHeight(v) {
3136
3182
  if (v === "auto")
@@ -4314,7 +4360,13 @@
4314
4360
  const image = figma.getImageByHash(h);
4315
4361
  if (!image)
4316
4362
  continue;
4317
- const bytes = await image.getBytesAsync();
4363
+ let bytes;
4364
+ try {
4365
+ bytes = await image.getBytesAsync();
4366
+ } catch (e) {
4367
+ out.rasterize = true;
4368
+ continue;
4369
+ }
4318
4370
  if (ctx.imageBytes + bytes.length > 40 << 20)
4319
4371
  continue;
4320
4372
  ctx.imageBytes += bytes.length;
@@ -4386,22 +4438,39 @@
4386
4438
  })
4387
4439
  };
4388
4440
  }
4441
+ function withTimeout(p, ms, message) {
4442
+ return new Promise(function(resolve, reject) {
4443
+ const timer = setTimeout(function() {
4444
+ reject(new Error(message));
4445
+ }, ms);
4446
+ p.then(function(v) {
4447
+ clearTimeout(timer);
4448
+ resolve(v);
4449
+ }, function(e) {
4450
+ clearTimeout(timer);
4451
+ reject(e);
4452
+ });
4453
+ });
4454
+ }
4389
4455
  async function robloxImages(p) {
4390
4456
  const items = Array.isArray(p.items) ? p.items : [];
4391
4457
  const out = [];
4392
4458
  for (let i = 0;i < items.length; i++) {
4393
4459
  const it = items[i];
4394
- const scale = Math.max(1, Math.min(4, Number(it.scale) || 2));
4460
+ let scale = Math.max(1, Math.min(4, Number(it.scale) || 2));
4395
4461
  let temp = null;
4396
4462
  try {
4397
4463
  let node = await getNode(it.id);
4398
4464
  if (it.mode === "panel" || it.mode === "shadow") {
4399
4465
  temp = node.clone();
4466
+ figma.currentPage.appendChild(temp);
4400
4467
  if ("children" in temp)
4401
4468
  for (let k = temp.children.length - 1;k >= 0; k--)
4402
4469
  temp.children[k].remove();
4403
4470
  if (it.mode === "panel")
4404
- temp.effects = [];
4471
+ temp.effects = node.effects.filter(function(e) {
4472
+ return e.type !== "DROP_SHADOW";
4473
+ });
4405
4474
  else {
4406
4475
  temp.effects = node.effects.filter(function(e) {
4407
4476
  return e.type === "DROP_SHADOW" && e.visible !== false;
@@ -4410,7 +4479,10 @@
4410
4479
  }
4411
4480
  node = temp;
4412
4481
  }
4413
- const bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: scale } });
4482
+ const box = node.absoluteRenderBounds || node.absoluteBoundingBox;
4483
+ if (box)
4484
+ scale = Math.min(scale, Math.floor(1024 / Math.max(1, box.width, box.height) * 1000) / 1000);
4485
+ const bytes = await withTimeout(node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: scale } }), 20000, "export timed out");
4414
4486
  const rb = node.absoluteRenderBounds || node.absoluteBoundingBox;
4415
4487
  const bb = node.absoluteBoundingBox;
4416
4488
  out.push({
@@ -4500,7 +4572,7 @@
4500
4572
  finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
4501
4573
  }
4502
4574
  // package.json
4503
- var version = "1.14.0";
4575
+ var version = "1.14.2";
4504
4576
 
4505
4577
  // plugin/code.ts
4506
4578
  var DEFAULT_SIZE = { width: 340, height: 540 };
package/plugin/ui.html CHANGED
@@ -523,7 +523,8 @@ const partials = new Map();
523
523
  function encode(msg) {
524
524
  const text = JSON.stringify(msg);
525
525
  if (text.length <= CHUNK) return [text];
526
- const cid = crypto.randomUUID();
526
+ // Not crypto.randomUUID: the plugin iframe is not a secure context, where it is undefined.
527
+ const cid = Date.now().toString(36) + Math.random().toString(36).slice(2);
527
528
  const n = Math.ceil(text.length / CHUNK);
528
529
  const frames = [];
529
530
  for (let i = 0; i < n; i++) frames.push(JSON.stringify({ t: "chunk", cid, i, n, d: text.slice(i * CHUNK, (i + 1) * CHUNK) }));
@@ -541,7 +542,15 @@ function decode(raw) {
541
542
  }
542
543
  function send(msg) {
543
544
  if (!ws || ws.readyState !== WebSocket.OPEN) return false;
544
- for (const f of encode(msg)) ws.send(f);
545
+ let frames;
546
+ try {
547
+ frames = encode(msg);
548
+ } catch (e) {
549
+ // A reply that can't be encoded still answers the request, so the agent doesn't wait for a timeout.
550
+ if (msg.t !== "res") return false;
551
+ frames = encode({ t: "res", id: msg.id, ok: false, error: "Could not send the result: " + ((e && e.message) || e), code: "PLUGIN_ERROR" });
552
+ }
553
+ for (const f of frames) ws.send(f);
545
554
  return true;
546
555
  }
547
556