@ttsalpha/mirra 0.0.0-stage → 1.0.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.
@@ -0,0 +1,1291 @@
1
+ "use strict";
2
+ (() => {
3
+ var __typeError = (msg) => {
4
+ throw TypeError(msg);
5
+ };
6
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
7
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
8
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
9
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
10
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
11
+
12
+ // ../schema/dist/constants.js
13
+ var PROTOCOL_VERSION = 1;
14
+
15
+ // src/file-info.ts
16
+ function readFileKey() {
17
+ try {
18
+ return figma.fileKey ?? null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ // src/figma/tokens.ts
25
+ function toRef(name) {
26
+ return `$${name}`;
27
+ }
28
+ function fromRef(ref) {
29
+ return ref.startsWith("$") ? ref.slice(1) : ref;
30
+ }
31
+ function isRef(value) {
32
+ return typeof value === "string" && value.startsWith("$");
33
+ }
34
+ var _variables, _textStyles;
35
+ var TokenIndex = class {
36
+ constructor() {
37
+ __privateAdd(this, _variables, null);
38
+ __privateAdd(this, _textStyles, null);
39
+ }
40
+ async variable(ref) {
41
+ if (!__privateGet(this, _variables)) {
42
+ const all = await figma.variables.getLocalVariablesAsync();
43
+ __privateSet(this, _variables, new Map(all.map((variable) => [variable.name, variable])));
44
+ }
45
+ const found = __privateGet(this, _variables).get(fromRef(ref));
46
+ if (!found) throw new Error(`No variable named "${fromRef(ref)}" in this file`);
47
+ return found;
48
+ }
49
+ async textStyle(ref) {
50
+ if (!__privateGet(this, _textStyles)) {
51
+ const all = await figma.getLocalTextStylesAsync();
52
+ __privateSet(this, _textStyles, new Map(all.map((style) => [style.name, style])));
53
+ }
54
+ const found = __privateGet(this, _textStyles).get(fromRef(ref));
55
+ if (!found) throw new Error(`No text style named "${fromRef(ref)}" in this file`);
56
+ return found;
57
+ }
58
+ };
59
+ _variables = new WeakMap();
60
+ _textStyles = new WeakMap();
61
+ var EMPTY_NAMES = { variable: () => void 0, style: () => void 0 };
62
+ async function buildNameIndex() {
63
+ const variables = /* @__PURE__ */ new Map();
64
+ const styles = /* @__PURE__ */ new Map();
65
+ for (const variable of await figma.variables.getLocalVariablesAsync()) {
66
+ variables.set(variable.id, toRef(variable.name));
67
+ }
68
+ for (const style of await figma.getLocalTextStylesAsync()) {
69
+ styles.set(style.id, toRef(style.name));
70
+ }
71
+ return { variable: (id) => variables.get(id), style: (id) => styles.get(id) };
72
+ }
73
+ async function readTokens() {
74
+ const variables = await figma.variables.getLocalVariablesAsync();
75
+ const byId = new Map(variables.map((variable) => [variable.id, variable]));
76
+ const collections = await figma.variables.getLocalVariableCollectionsAsync();
77
+ return {
78
+ collections: collections.map((collection) => ({
79
+ name: collection.name,
80
+ modes: collection.modes.map((mode) => mode.name),
81
+ variables: variables.filter((variable) => variable.variableCollectionId === collection.id).map((variable) => ({
82
+ ref: toRef(variable.name),
83
+ type: variable.resolvedType,
84
+ values: valuesByMode(variable, collection, byId)
85
+ }))
86
+ })),
87
+ styles: {
88
+ paint: (await figma.getLocalPaintStylesAsync()).map((style) => ({
89
+ ref: toRef(style.name),
90
+ ...solidHex(style.paints) ? { fill: solidHex(style.paints) } : {}
91
+ })),
92
+ text: (await figma.getLocalTextStylesAsync()).map((style) => ({
93
+ ref: toRef(style.name),
94
+ font: `${style.fontName.family} ${style.fontName.style}`,
95
+ size: style.fontSize
96
+ })),
97
+ effect: (await figma.getLocalEffectStylesAsync()).map((style) => ({
98
+ ref: toRef(style.name)
99
+ }))
100
+ }
101
+ };
102
+ }
103
+ function valuesByMode(variable, collection, byId) {
104
+ const values = {};
105
+ for (const mode of collection.modes) {
106
+ const raw = variable.valuesByMode[mode.modeId];
107
+ if (raw === void 0) continue;
108
+ if (typeof raw === "object" && "type" in raw && raw.type === "VARIABLE_ALIAS") {
109
+ const alias = byId.get(raw.id);
110
+ values[mode.name] = alias ? toRef(alias.name) : `$unknown:${raw.id}`;
111
+ } else if (typeof raw === "object" && "r" in raw) {
112
+ values[mode.name] = hex(raw);
113
+ } else {
114
+ values[mode.name] = raw;
115
+ }
116
+ }
117
+ return values;
118
+ }
119
+ function solidHex(paints) {
120
+ const paint = paints[0];
121
+ return paint && paint.type === "SOLID" ? hex({ ...paint.color, a: paint.opacity ?? 1 }) : void 0;
122
+ }
123
+ function hex({ r, g, b, a = 1 }) {
124
+ const channel = (value) => Math.round(value * 255).toString(16).padStart(2, "0");
125
+ const rgb = `#${channel(r)}${channel(g)}${channel(b)}`;
126
+ return a >= 1 ? rgb : `${rgb}${channel(a)}`;
127
+ }
128
+
129
+ // src/figma/values.ts
130
+ function rgba(hex2) {
131
+ const digits = hex2.slice(1);
132
+ const full = digits.length === 3 ? digits.split("").map((digit) => digit + digit).join("") : digits;
133
+ const value = (start) => parseInt(full.slice(start, start + 2), 16) / 255;
134
+ return { r: value(0), g: value(2), b: value(4), a: full.length === 8 ? value(6) : 1 };
135
+ }
136
+ function toPaints(fill) {
137
+ if (typeof fill === "string") return [solid(fill)];
138
+ if (fill.type === "solid") return [solid(fill.color, fill.opacity)];
139
+ return [
140
+ {
141
+ type: fill.kind === "radial" ? "GRADIENT_RADIAL" : "GRADIENT_LINEAR",
142
+ gradientTransform: gradientTransform(fill.angle ?? 90),
143
+ gradientStops: fill.stops.map((stop) => ({ position: stop.at, color: rgba(stop.color) }))
144
+ }
145
+ ];
146
+ }
147
+ function solid(hex2, opacity) {
148
+ const { r, g, b, a } = rgba(hex2);
149
+ return { type: "SOLID", color: { r, g, b }, opacity: opacity ?? a };
150
+ }
151
+ function gradientTransform(angleDeg) {
152
+ const radians = angleDeg * Math.PI / 180;
153
+ const cos = Math.cos(radians);
154
+ const sin = Math.sin(radians);
155
+ return [
156
+ [cos, -sin, 0.5 - 0.5 * cos + 0.5 * sin],
157
+ [sin, cos, 0.5 - 0.5 * sin - 0.5 * cos]
158
+ ];
159
+ }
160
+ function toStrokes(stroke) {
161
+ const align = (stroke.align ?? "inside").toUpperCase();
162
+ return {
163
+ paints: [solid(stroke.color)],
164
+ weight: stroke.width ?? 1,
165
+ align
166
+ };
167
+ }
168
+ function toEffects(effects) {
169
+ return effects.map((effect) => {
170
+ switch (effect.type) {
171
+ case "layer-blur":
172
+ return { type: "LAYER_BLUR", blurType: "NORMAL", radius: effect.radius, visible: true };
173
+ case "background-blur":
174
+ return {
175
+ type: "BACKGROUND_BLUR",
176
+ blurType: "NORMAL",
177
+ radius: effect.radius,
178
+ visible: true
179
+ };
180
+ case "shadow":
181
+ return { type: "DROP_SHADOW", ...shadow(effect) };
182
+ case "inner-shadow":
183
+ return { type: "INNER_SHADOW", ...shadow(effect) };
184
+ }
185
+ });
186
+ }
187
+ function shadow(effect) {
188
+ return {
189
+ color: rgba(effect.color ?? "#00000040"),
190
+ offset: { x: effect.x ?? 0, y: effect.y ?? 0 },
191
+ radius: effect.blur ?? 0,
192
+ spread: effect.spread ?? 0,
193
+ visible: true,
194
+ blendMode: "NORMAL"
195
+ };
196
+ }
197
+ function toPadding(pad) {
198
+ if (typeof pad === "number") return { top: pad, right: pad, bottom: pad, left: pad };
199
+ if (pad.length === 2) {
200
+ const [vertical, horizontal] = pad;
201
+ return { top: vertical, right: horizontal, bottom: vertical, left: horizontal };
202
+ }
203
+ const [top, right, bottom, left] = pad;
204
+ return { top, right, bottom, left };
205
+ }
206
+ function toRadius(radius) {
207
+ return typeof radius === "number" ? [radius, radius, radius, radius] : radius;
208
+ }
209
+ var PRIMARY_AXIS = {
210
+ start: "MIN",
211
+ center: "CENTER",
212
+ end: "MAX",
213
+ "space-between": "SPACE_BETWEEN",
214
+ "space-around": "SPACE_AROUND",
215
+ "space-evenly": "SPACE_EVENLY"
216
+ };
217
+ var COUNTER_AXIS = { start: "MIN", center: "CENTER", end: "MAX", baseline: "BASELINE" };
218
+ function toPrimaryAxisAlign(justify) {
219
+ return PRIMARY_AXIS[justify];
220
+ }
221
+ function toCounterAxisAlign(align) {
222
+ return COUNTER_AXIS[align];
223
+ }
224
+ function toHex({ r, g, b }, alpha = 1) {
225
+ const channel = (value) => Math.round(value * 255).toString(16).padStart(2, "0");
226
+ const rgb = `#${channel(r)}${channel(g)}${channel(b)}`;
227
+ return alpha >= 1 ? rgb : `${rgb}${channel(alpha)}`;
228
+ }
229
+ function fromPaints(paints, names = EMPTY_NAMES) {
230
+ if (!Array.isArray(paints)) return void 0;
231
+ const paint = paints.find((candidate) => candidate.visible !== false);
232
+ if (!paint) return void 0;
233
+ if (paint.type === "SOLID") {
234
+ const bound = paint.boundVariables?.color;
235
+ const ref = bound ? names.variable(bound.id) : void 0;
236
+ return ref ?? toHex(paint.color, paint.opacity ?? 1);
237
+ }
238
+ if (paint.type !== "GRADIENT_LINEAR" && paint.type !== "GRADIENT_RADIAL") return void 0;
239
+ return {
240
+ type: "gradient",
241
+ kind: paint.type === "GRADIENT_RADIAL" ? "radial" : "linear",
242
+ angle: fromGradientTransform(paint.gradientTransform),
243
+ stops: paint.gradientStops.map((stop) => ({
244
+ at: stop.position,
245
+ color: toHex(stop.color, stop.color.a)
246
+ }))
247
+ };
248
+ }
249
+ function fromGradientTransform(transform) {
250
+ const cos = transform[0][0];
251
+ const sin = transform[1][0];
252
+ const degrees = Math.atan2(sin, cos) * 180 / Math.PI;
253
+ return Math.round(degrees * 1e3) / 1e3;
254
+ }
255
+ function fromEffects(effects) {
256
+ const specs = effects.filter((effect) => effect.visible !== false).map(fromEffect);
257
+ return specs.length ? specs : void 0;
258
+ }
259
+ function fromEffect(effect) {
260
+ switch (effect.type) {
261
+ case "LAYER_BLUR":
262
+ return { type: "layer-blur", radius: effect.radius };
263
+ case "BACKGROUND_BLUR":
264
+ return { type: "background-blur", radius: effect.radius };
265
+ case "DROP_SHADOW":
266
+ case "INNER_SHADOW":
267
+ return {
268
+ type: effect.type === "DROP_SHADOW" ? "shadow" : "inner-shadow",
269
+ x: effect.offset.x,
270
+ y: effect.offset.y,
271
+ blur: effect.radius,
272
+ spread: effect.spread ?? 0,
273
+ color: toHex(effect.color, effect.color.a)
274
+ };
275
+ default:
276
+ return { type: "layer-blur", radius: 0 };
277
+ }
278
+ }
279
+ function fromPadding(sides) {
280
+ const { top, right, bottom, left } = sides;
281
+ if (top === right && right === bottom && bottom === left) return top === 0 ? void 0 : top;
282
+ if (top === bottom && left === right) return [top, right];
283
+ return [top, right, bottom, left];
284
+ }
285
+ function fromRadius(corners) {
286
+ const { topLeft, topRight, bottomRight, bottomLeft } = corners;
287
+ if (topLeft === topRight && topRight === bottomRight && bottomRight === bottomLeft) {
288
+ return topLeft === 0 ? void 0 : topLeft;
289
+ }
290
+ return [topLeft, topRight, bottomRight, bottomLeft];
291
+ }
292
+ var PRIMARY_AXIS_BACK = {
293
+ MIN: "start",
294
+ CENTER: "center",
295
+ MAX: "end",
296
+ SPACE_BETWEEN: "space-between",
297
+ SPACE_AROUND: "space-around",
298
+ SPACE_EVENLY: "space-evenly"
299
+ };
300
+ var COUNTER_AXIS_BACK = {
301
+ MIN: "start",
302
+ CENTER: "center",
303
+ MAX: "end",
304
+ BASELINE: "baseline"
305
+ };
306
+ function fromPrimaryAxisAlign(value) {
307
+ return PRIMARY_AXIS_BACK[value];
308
+ }
309
+ function fromCounterAxisAlign(value) {
310
+ return COUNTER_AXIS_BACK[value];
311
+ }
312
+
313
+ // src/ops/define.ts
314
+ var TYPES = {
315
+ color: "COLOR",
316
+ number: "FLOAT",
317
+ string: "STRING",
318
+ boolean: "BOOLEAN"
319
+ };
320
+ async function define(params) {
321
+ const styles = await defineStyles(params.styles);
322
+ if (!params.collection) {
323
+ return { modes: [], styles, created: [], updated: [], notes: [] };
324
+ }
325
+ const collection = await collectionNamed(params.collection);
326
+ const notes = addModes(collection, params.modes ?? []);
327
+ const existing = new Map(
328
+ (await figma.variables.getLocalVariablesAsync()).filter((variable) => variable.variableCollectionId === collection.id).map((variable) => [variable.name, variable])
329
+ );
330
+ const created = [];
331
+ const updated = [];
332
+ for (const spec of params.variables ?? []) {
333
+ let variable = existing.get(spec.name);
334
+ if (variable) {
335
+ updated.push(spec.name);
336
+ } else {
337
+ variable = figma.variables.createVariable(spec.name, collection, TYPES[spec.type]);
338
+ existing.set(spec.name, variable);
339
+ created.push(spec.name);
340
+ }
341
+ for (const mode of collection.modes) {
342
+ const raw = perMode(spec.value, mode.name);
343
+ if (raw === void 0) continue;
344
+ variable.setValueForMode(mode.modeId, await toValue(raw, spec.type, existing));
345
+ }
346
+ }
347
+ figma.commitUndo();
348
+ return {
349
+ collection: collection.name,
350
+ modes: collection.modes.map((mode) => mode.name),
351
+ styles,
352
+ created,
353
+ updated,
354
+ notes
355
+ };
356
+ }
357
+ async function collectionNamed(name) {
358
+ const collections = await figma.variables.getLocalVariableCollectionsAsync();
359
+ return collections.find((candidate) => candidate.name === name) ?? figma.variables.createVariableCollection(name);
360
+ }
361
+ function addModes(collection, modes) {
362
+ const notes = [];
363
+ for (const [index, name] of modes.entries()) {
364
+ const existing = collection.modes[index];
365
+ if (existing) {
366
+ if (existing.name !== name) collection.renameMode(existing.modeId, name);
367
+ continue;
368
+ }
369
+ try {
370
+ collection.addMode(name);
371
+ } catch (error) {
372
+ notes.push(`mode "${name}" not added: ${error instanceof Error ? error.message : error}`);
373
+ }
374
+ }
375
+ return notes;
376
+ }
377
+ function perMode(value, mode) {
378
+ if (typeof value !== "object") return value;
379
+ return value[mode] ?? value["default"] ?? Object.values(value)[0];
380
+ }
381
+ async function defineStyles(defs) {
382
+ if (!defs) return [];
383
+ const touched = [];
384
+ for (const spec of defs.paint ?? []) {
385
+ const style = (await figma.getLocalPaintStylesAsync()).find((candidate) => candidate.name === spec.name) ?? figma.createPaintStyle();
386
+ style.name = spec.name;
387
+ style.paints = toPaints(spec.fill);
388
+ touched.push(toRef(spec.name));
389
+ }
390
+ for (const spec of defs.text ?? []) {
391
+ const style = (await figma.getLocalTextStylesAsync()).find((candidate) => candidate.name === spec.name) ?? figma.createTextStyle();
392
+ const font = { family: spec.font.family, style: spec.font.style ?? "Regular" };
393
+ await figma.loadFontAsync(font);
394
+ style.name = spec.name;
395
+ style.fontName = font;
396
+ style.fontSize = spec.size;
397
+ if (spec.lineHeight !== void 0) {
398
+ style.lineHeight = spec.lineHeight === "auto" ? { unit: "AUTO" } : { unit: "PIXELS", value: spec.lineHeight };
399
+ }
400
+ if (spec.letterSpacing !== void 0) {
401
+ style.letterSpacing = { unit: "PIXELS", value: spec.letterSpacing };
402
+ }
403
+ touched.push(toRef(spec.name));
404
+ }
405
+ for (const spec of defs.effect ?? []) {
406
+ const style = (await figma.getLocalEffectStylesAsync()).find((candidate) => candidate.name === spec.name) ?? figma.createEffectStyle();
407
+ style.name = spec.name;
408
+ style.effects = toEffects(spec.effects);
409
+ touched.push(toRef(spec.name));
410
+ }
411
+ return touched;
412
+ }
413
+ async function toValue(raw, type, known) {
414
+ if (isRef(raw)) {
415
+ const name = fromRef(raw);
416
+ const target = known.get(name) ?? (await figma.variables.getLocalVariablesAsync()).find((variable) => variable.name === name);
417
+ if (!target) throw new Error(`No variable named "${name}" to alias`);
418
+ return figma.variables.createVariableAlias(target);
419
+ }
420
+ if (type === "color") {
421
+ if (typeof raw !== "string")
422
+ throw new Error(`A colour variable needs a hex string, got ${raw}`);
423
+ return rgba(raw);
424
+ }
425
+ return raw;
426
+ }
427
+
428
+ // src/ops/exec.ts
429
+ var allowed = false;
430
+ function setExecAllowed(value) {
431
+ allowed = value;
432
+ }
433
+ async function exec(params) {
434
+ if (!allowed) {
435
+ throw new Error(
436
+ 'Code execution is off. Tick "Allow code execution" in the Mirra panel in Figma.'
437
+ );
438
+ }
439
+ let run;
440
+ try {
441
+ run = new Function("figma", `"use strict"; return (async () => { ${params.code} })();`);
442
+ } catch (error) {
443
+ throw new Error(`Could not compile the code: ${message(error)}`);
444
+ }
445
+ const result = await run(figma);
446
+ figma.commitUndo();
447
+ return { result: describe(result) };
448
+ }
449
+ function describe(value) {
450
+ if (value === void 0) return "undefined";
451
+ try {
452
+ return JSON.stringify(value, replacer, 2) ?? String(value);
453
+ } catch {
454
+ return String(value);
455
+ }
456
+ }
457
+ function replacer(_key, value) {
458
+ if (value && typeof value === "object" && "id" in value && "type" in value) {
459
+ const node = value;
460
+ return `${node.type} ${node.id}${node.name ? ` (${node.name})` : ""}`;
461
+ }
462
+ return value;
463
+ }
464
+ function message(error) {
465
+ return error instanceof Error ? error.message : String(error);
466
+ }
467
+
468
+ // src/figma/build.ts
469
+ var KEY_FIELD = "mirra:key";
470
+ var SVG_FIELD = "mirra:svg";
471
+ var SRC_FIELD = "mirra:src";
472
+ var COMPONENT_FIELD = "mirra:component";
473
+ var MAX_REMEMBERED_SOURCE = 5e4;
474
+ async function buildNode(spec, parent, keys, tokens2 = new TokenIndex(), deferred = []) {
475
+ const node = await createNode(spec, parent);
476
+ if (spec.key) {
477
+ node.setPluginData(KEY_FIELD, spec.key);
478
+ keys[spec.key] = node.id;
479
+ }
480
+ await applyProps(node, spec, tokens2);
481
+ if (spec.type === "frame") {
482
+ for (const child of spec.children ?? []) {
483
+ await buildNode(child, node, keys, tokens2, deferred);
484
+ }
485
+ }
486
+ applyBounds(node, spec);
487
+ applyConstraints(node, spec);
488
+ if (spec.at) deferred.push(() => pin(node, spec.at));
489
+ const fallback = spec.type === "frame" && spec.layout ? "hug" : void 0;
490
+ applySizing(node, spec.w ?? fallback, "Horizontal");
491
+ applySizing(node, spec.h ?? fallback, "Vertical");
492
+ return node;
493
+ }
494
+ async function createNode(spec, parent) {
495
+ const node = await make(spec);
496
+ parent.appendChild(node);
497
+ if (spec.type === "svg" || spec.type === "image") remember(node, spec);
498
+ if (spec.type === "instance") node.setPluginData(COMPONENT_FIELD, spec.component);
499
+ return node;
500
+ }
501
+ async function make(spec) {
502
+ switch (spec.type) {
503
+ case "frame":
504
+ return figma.createFrame();
505
+ case "text":
506
+ return figma.createText();
507
+ case "rect":
508
+ return figma.createRectangle();
509
+ case "ellipse":
510
+ return figma.createEllipse();
511
+ case "instance":
512
+ return (await findComponent(spec.component)).createInstance();
513
+ case "svg":
514
+ return figma.createNodeFromSvg(spec.src);
515
+ case "image":
516
+ return imageRect(spec.src, spec.fit);
517
+ }
518
+ }
519
+ async function imageRect(src, fit) {
520
+ const image = figma.createImage(figma.base64Decode(stripDataUri(src)));
521
+ const rect = figma.createRectangle();
522
+ rect.fills = [{ type: "IMAGE", imageHash: image.hash, scaleMode: SCALE_MODES[fit ?? "fill"] }];
523
+ const { width, height } = await image.getSizeAsync();
524
+ rect.resize(width, height);
525
+ return rect;
526
+ }
527
+ var SCALE_MODES = {
528
+ fill: "FILL",
529
+ fit: "FIT",
530
+ crop: "CROP",
531
+ tile: "TILE"
532
+ };
533
+ function stripDataUri(src) {
534
+ const comma = src.indexOf(",");
535
+ return src.startsWith("data:") && comma !== -1 ? src.slice(comma + 1) : src;
536
+ }
537
+ function remember(node, spec) {
538
+ const field = spec.type === "svg" ? SVG_FIELD : SRC_FIELD;
539
+ if (spec.srcRef) node.setPluginData(field, spec.srcRef);
540
+ else if (spec.src.length <= MAX_REMEMBERED_SOURCE) node.setPluginData(field, spec.src);
541
+ }
542
+ async function findComponent(name) {
543
+ const components = figma.currentPage.findAllWithCriteria({ types: ["COMPONENT"] });
544
+ const match = components.find((component) => componentPath(component) === name);
545
+ if (!match) {
546
+ const available = components.map(componentPath).slice(0, 10).join(", ");
547
+ throw new Error(
548
+ `No component named "${name}" on this page${available ? `. Found: ${available}` : ""}`
549
+ );
550
+ }
551
+ return match;
552
+ }
553
+ function componentPath(component) {
554
+ const parent = component.parent;
555
+ return parent && parent.type === "COMPONENT_SET" ? `${parent.name}/${component.name}` : component.name;
556
+ }
557
+ async function applyProps(node, props, tokens2 = new TokenIndex()) {
558
+ if (props.name !== void 0) node.name = props.name;
559
+ if (props.opacity !== void 0) node.opacity = props.opacity;
560
+ if (props.visible !== void 0) node.visible = props.visible;
561
+ if (props.layout !== void 0 || props.clip !== void 0) {
562
+ if (node.type !== "FRAME") {
563
+ throw new Error(`layout and clip only apply to a frame, not a ${low(node)}`);
564
+ }
565
+ applyLayout(node, props.layout, props.clip);
566
+ }
567
+ await applyPaint(node, props, tokens2);
568
+ if (props.properties) {
569
+ if (node.type !== "INSTANCE") {
570
+ throw new Error(`component properties only apply to an instance, not a ${low(node)}`);
571
+ }
572
+ node.setProperties(resolveProperties(node, props.properties));
573
+ }
574
+ const typography = props.text !== void 0 || props.font !== void 0 || props.size !== void 0 || props.lineHeight !== void 0 || props.letterSpacing !== void 0 || props.align !== void 0 || props.color !== void 0 || props.style !== void 0;
575
+ if (typography) {
576
+ if (node.type !== "TEXT") {
577
+ throw new Error(`text properties only apply to a text node, not a ${low(node)}`);
578
+ }
579
+ await applyText(node, props, tokens2);
580
+ }
581
+ }
582
+ async function applyText(text, props, tokens2) {
583
+ const style = props.style ? await tokens2.textStyle(props.style) : null;
584
+ if (style) {
585
+ await figma.loadFontAsync(style.fontName);
586
+ await text.setTextStyleIdAsync(style.id);
587
+ }
588
+ if (props.font) {
589
+ const fallback = text.fontName === figma.mixed ? "Regular" : text.fontName.style;
590
+ const font = { family: props.font.family, style: props.font.style ?? fallback };
591
+ await figma.loadFontAsync(font);
592
+ text.fontName = font;
593
+ } else if (!style) {
594
+ const current = text.fontName === figma.mixed ? { family: "Inter", style: "Regular" } : text.fontName;
595
+ await figma.loadFontAsync(current);
596
+ }
597
+ if (props.text !== void 0) text.characters = props.text;
598
+ if (props.size !== void 0) text.fontSize = props.size;
599
+ if (props.lineHeight !== void 0) {
600
+ text.lineHeight = props.lineHeight === "auto" ? { unit: "AUTO" } : { unit: "PIXELS", value: props.lineHeight };
601
+ }
602
+ if (props.letterSpacing !== void 0) {
603
+ text.letterSpacing = { unit: "PIXELS", value: props.letterSpacing };
604
+ }
605
+ if (props.align) {
606
+ text.textAlignHorizontal = props.align.toUpperCase();
607
+ }
608
+ if (props.color) {
609
+ text.fills = isRef(props.color) ? [await boundPaint(props.color, tokens2)] : toPaints(props.color);
610
+ }
611
+ }
612
+ function applyLayout(frame, layout, clip) {
613
+ if (clip !== void 0) frame.clipsContent = clip;
614
+ if (!layout) return;
615
+ const { dir, gap, pad, justify, align, wrap } = layout;
616
+ frame.layoutMode = dir === "horizontal" ? "HORIZONTAL" : "VERTICAL";
617
+ if (gap !== void 0) frame.itemSpacing = gap;
618
+ if (pad !== void 0) {
619
+ const { top, right, bottom, left } = toPadding(pad);
620
+ frame.paddingTop = top;
621
+ frame.paddingRight = right;
622
+ frame.paddingBottom = bottom;
623
+ frame.paddingLeft = left;
624
+ }
625
+ if (wrap !== void 0) frame.layoutWrap = wrap ? "WRAP" : "NO_WRAP";
626
+ if (justify) frame.primaryAxisAlignItems = toPrimaryAxisAlign(justify);
627
+ if (align) frame.counterAxisAlignItems = toCounterAxisAlign(align);
628
+ }
629
+ async function applyPaint(node, props, tokens2) {
630
+ const shape = node;
631
+ if (!props.fill && node.type === "FRAME") shape.fills = [];
632
+ if (props.fill && node.type !== "TEXT") {
633
+ shape.fills = isRef(props.fill) ? [await boundPaint(props.fill, tokens2)] : toPaints(props.fill);
634
+ }
635
+ if (props.stroke) {
636
+ const { paints, weight, align } = toStrokes(props.stroke);
637
+ shape.strokes = paints;
638
+ shape.strokeWeight = weight;
639
+ shape.strokeAlign = align;
640
+ }
641
+ if (props.radius !== void 0) applyRadius(shape, props.radius);
642
+ if (props.effects) shape.effects = toEffects(props.effects);
643
+ }
644
+ function resolveProperties(node, properties) {
645
+ const available = node.componentProperties;
646
+ const resolved = {};
647
+ for (const [name, value] of Object.entries(properties)) {
648
+ const match = name in available ? name : Object.keys(available).find((candidate) => candidate.split("#")[0] === name);
649
+ if (!match) {
650
+ const names = Object.keys(available).map((candidate) => candidate.split("#")[0]).join(", ");
651
+ throw new Error(
652
+ `"${node.name}" has no component property "${name}"${names ? `. It has: ${names}` : ""}`
653
+ );
654
+ }
655
+ resolved[match] = typeof value === "number" ? String(value) : value;
656
+ }
657
+ return resolved;
658
+ }
659
+ async function boundPaint(ref, tokens2) {
660
+ const variable = await tokens2.variable(ref);
661
+ const base = { type: "SOLID", color: { r: 0, g: 0, b: 0 } };
662
+ return figma.variables.setBoundVariableForPaint(base, "color", variable);
663
+ }
664
+ function low(node) {
665
+ return node.type.toLowerCase();
666
+ }
667
+ function applyRadius(node, radius) {
668
+ const [topLeft, topRight, bottomRight, bottomLeft] = toRadius(radius);
669
+ if (!("topLeftRadius" in node)) {
670
+ node.cornerRadius = topLeft;
671
+ return;
672
+ }
673
+ const corners = node;
674
+ corners.topLeftRadius = topLeft;
675
+ corners.topRightRadius = topRight;
676
+ corners.bottomRightRadius = bottomRight;
677
+ corners.bottomLeftRadius = bottomLeft;
678
+ }
679
+ var CONSTRAINTS = {
680
+ min: "MIN",
681
+ center: "CENTER",
682
+ max: "MAX",
683
+ stretch: "STRETCH",
684
+ scale: "SCALE"
685
+ };
686
+ function applyBounds(node, props) {
687
+ const box = node;
688
+ if (props.minW !== void 0) box.minWidth = props.minW;
689
+ if (props.maxW !== void 0) box.maxWidth = props.maxW;
690
+ if (props.minH !== void 0) box.minHeight = props.minH;
691
+ if (props.maxH !== void 0) box.maxHeight = props.maxH;
692
+ }
693
+ function applyConstraints(node, props) {
694
+ if (!props.constraints) return;
695
+ const target = node;
696
+ target.constraints = {
697
+ horizontal: CONSTRAINTS[props.constraints.h],
698
+ vertical: CONSTRAINTS[props.constraints.v]
699
+ };
700
+ }
701
+ function pin(node, at) {
702
+ if (!at) return;
703
+ const parent = node.parent;
704
+ const inFlow = parent !== null && "layoutMode" in parent && parent.layoutMode !== "NONE";
705
+ if (inFlow)
706
+ node.layoutPositioning = "ABSOLUTE";
707
+ const placed = node;
708
+ placed.x = at.x;
709
+ placed.y = at.y;
710
+ }
711
+ function applySizing(node, sizing2, axis) {
712
+ if (sizing2 === void 0) return;
713
+ if (typeof sizing2 === "number") {
714
+ const box = node;
715
+ if (axis === "Horizontal") box.resize(sizing2, box.height);
716
+ else box.resize(box.width, sizing2);
717
+ return;
718
+ }
719
+ const layoutNode = node;
720
+ const field = axis === "Horizontal" ? "layoutSizingHorizontal" : "layoutSizingVertical";
721
+ if (!(field in layoutNode)) {
722
+ throw new Error(`"${sizing2}" sizing is not available on a ${node.type.toLowerCase()} node`);
723
+ }
724
+ layoutNode[field] = sizing2 === "fill" ? "FILL" : "HUG";
725
+ }
726
+
727
+ // src/figma/target.ts
728
+ var _byKey, _NodeIndex_instances, index_fn;
729
+ var NodeIndex = class {
730
+ constructor() {
731
+ __privateAdd(this, _NodeIndex_instances);
732
+ __privateAdd(this, _byKey, null);
733
+ }
734
+ invalidate() {
735
+ __privateSet(this, _byKey, null);
736
+ }
737
+ async resolve(target) {
738
+ if ("id" in target) {
739
+ const node = await figma.getNodeByIdAsync(target.id);
740
+ if (!node) throw new Error(`No node with id ${target.id} in this file`);
741
+ if (node.type === "DOCUMENT" || node.type === "PAGE") {
742
+ throw new Error(`${node.type} is not a layer: target a frame or a shape`);
743
+ }
744
+ return node;
745
+ }
746
+ const found = __privateMethod(this, _NodeIndex_instances, index_fn).call(this).get(target.key);
747
+ if (!found) {
748
+ throw new Error(
749
+ `No node with key "${target.key}" on this page. Keys live on the nodes mirra rendered; mirra_read reports the ones that are there.`
750
+ );
751
+ }
752
+ return found;
753
+ }
754
+ };
755
+ _byKey = new WeakMap();
756
+ _NodeIndex_instances = new WeakSet();
757
+ index_fn = function() {
758
+ if (__privateGet(this, _byKey)) return __privateGet(this, _byKey);
759
+ const byKey = /* @__PURE__ */ new Map();
760
+ for (const node of figma.currentPage.findAllWithCriteria({
761
+ pluginData: { keys: [KEY_FIELD] }
762
+ })) {
763
+ byKey.set(node.getPluginData(KEY_FIELD), node);
764
+ }
765
+ __privateSet(this, _byKey, byKey);
766
+ return byKey;
767
+ };
768
+
769
+ // src/ops/export.ts
770
+ async function exportAssets(params) {
771
+ const index = new NodeIndex();
772
+ const format = params.format ?? "png";
773
+ const assets = [];
774
+ for (const target of params.targets) {
775
+ const node = await index.resolve(target);
776
+ const bytes = await node.exportAsync(
777
+ format === "svg" ? { format: "SVG" } : { format: "PNG", constraint: { type: "SCALE", value: params.scale ?? 1 } }
778
+ );
779
+ assets.push({
780
+ id: node.id,
781
+ name: node.name,
782
+ extension: format,
783
+ data: figma.base64Encode(bytes)
784
+ });
785
+ }
786
+ return { assets };
787
+ }
788
+
789
+ // src/ops/find.ts
790
+ var DEFAULT_LIMIT = 50;
791
+ var KINDS = {
792
+ frame: ["FRAME"],
793
+ text: ["TEXT"],
794
+ rect: ["RECTANGLE"],
795
+ ellipse: ["ELLIPSE"],
796
+ instance: ["INSTANCE"],
797
+ component: ["COMPONENT", "COMPONENT_SET"],
798
+ group: ["GROUP"],
799
+ vector: ["VECTOR"]
800
+ };
801
+ async function find(params) {
802
+ const pages2 = params.scope === "document" ? (await figma.loadAllPagesAsync(), figma.root.children) : [figma.currentPage];
803
+ const limit = params.limit ?? DEFAULT_LIMIT;
804
+ const needle = params.name?.toLowerCase();
805
+ const kinds = params.kind && params.kind !== "any" ? KINDS[params.kind] : void 0;
806
+ const matches = [];
807
+ let truncated = false;
808
+ for (const page of pages2) {
809
+ for (const node of page.findAll(() => true)) {
810
+ if (kinds && !kinds.includes(node.type)) continue;
811
+ if (needle && !node.name.toLowerCase().includes(needle)) continue;
812
+ const key = node.getPluginData(KEY_FIELD);
813
+ if (params.key && key !== params.key) continue;
814
+ if (matches.length >= limit) {
815
+ truncated = true;
816
+ break;
817
+ }
818
+ matches.push({
819
+ id: node.id,
820
+ name: node.name,
821
+ kind: node.type,
822
+ page: page.name,
823
+ ...key ? { key } : {}
824
+ });
825
+ }
826
+ }
827
+ if (params.select && matches.length) {
828
+ const selected = matches.map((match) => figma.currentPage.findOne((node) => node.id === match.id)).filter((node) => node !== null);
829
+ if (selected.length) {
830
+ figma.currentPage.selection = selected;
831
+ figma.viewport.scrollAndZoomIntoView(selected);
832
+ }
833
+ }
834
+ return { nodes: matches, truncated };
835
+ }
836
+
837
+ // src/ops/pages.ts
838
+ async function pages(params) {
839
+ await figma.loadAllPagesAsync();
840
+ if (params.select) {
841
+ const wanted = figma.root.children.find(
842
+ (page) => page.id === params.select || page.name === params.select
843
+ );
844
+ if (!wanted) {
845
+ const names = figma.root.children.map((page) => page.name).join(", ");
846
+ throw new Error(`No page called "${params.select}". This file has: ${names}`);
847
+ }
848
+ await figma.setCurrentPageAsync(wanted);
849
+ }
850
+ return {
851
+ pages: figma.root.children.map((page) => ({
852
+ id: page.id,
853
+ name: page.name,
854
+ current: page.id === figma.currentPage.id
855
+ }))
856
+ };
857
+ }
858
+
859
+ // src/ops/patch.ts
860
+ async function patch(params) {
861
+ const index = new NodeIndex();
862
+ const keys = {};
863
+ const touched = [];
864
+ const deferred = [];
865
+ for (const op of params.ops) {
866
+ const node = await index.resolve(op.target);
867
+ switch (op.op) {
868
+ case "set": {
869
+ await applyProps(node, op.props);
870
+ applyBounds(node, op.props);
871
+ applyConstraints(node, op.props);
872
+ applySizing(node, op.props.w, "Horizontal");
873
+ applySizing(node, op.props.h, "Vertical");
874
+ if (op.props.at) deferred.push(() => pin(node, op.props.at));
875
+ touched.push(node);
876
+ break;
877
+ }
878
+ case "append": {
879
+ const child = await buildNode(op.tree, asContainer(node), keys, void 0, deferred);
880
+ if (op.index !== void 0) asContainer(node).insertChild(op.index, child);
881
+ index.invalidate();
882
+ touched.push(child);
883
+ break;
884
+ }
885
+ case "replace": {
886
+ const parent = node.parent;
887
+ if (!parent || !("insertChild" in parent)) {
888
+ throw new Error(`${node.name} has no parent to replace it in`);
889
+ }
890
+ const at = parent.children.indexOf(node);
891
+ const created = await buildNode(op.tree, parent, keys, void 0, deferred);
892
+ parent.insertChild(at, created);
893
+ node.remove();
894
+ index.invalidate();
895
+ touched.push(created);
896
+ break;
897
+ }
898
+ case "move": {
899
+ const parent = op.parent ? await index.resolve(op.parent) : node.parent;
900
+ if (!parent || !("insertChild" in parent)) {
901
+ throw new Error(`${node.name} has nowhere to move to`);
902
+ }
903
+ const container = parent;
904
+ const at = op.index ?? container.children.length;
905
+ container.insertChild(Math.min(at, container.children.length), node);
906
+ index.invalidate();
907
+ touched.push(node);
908
+ break;
909
+ }
910
+ case "remove": {
911
+ node.remove();
912
+ index.invalidate();
913
+ break;
914
+ }
915
+ }
916
+ }
917
+ for (const place of deferred) place();
918
+ if (params.reveal) {
919
+ const alive = touched.filter((node) => !node.removed);
920
+ if (alive.length) {
921
+ figma.currentPage.selection = alive;
922
+ figma.viewport.scrollAndZoomIntoView(alive);
923
+ }
924
+ }
925
+ figma.commitUndo();
926
+ return { applied: params.ops.length, keys };
927
+ }
928
+ function asContainer(node) {
929
+ if (!("appendChild" in node)) {
930
+ throw new Error(`A ${node.type.toLowerCase()} cannot contain children`);
931
+ }
932
+ return node;
933
+ }
934
+
935
+ // src/ops/ping.ts
936
+ function ping() {
937
+ return {
938
+ fileName: figma.root.name,
939
+ fileKey: readFileKey(),
940
+ currentPage: figma.currentPage.name,
941
+ selectionCount: figma.currentPage.selection.length
942
+ };
943
+ }
944
+
945
+ // src/figma/read.ts
946
+ function readNode(node, depth, names = EMPTY_NAMES) {
947
+ const skipped = { count: 0 };
948
+ const tree = toSpec(node, depth, skipped, names);
949
+ return { tree, skipped: skipped.count };
950
+ }
951
+ function toSpec(node, depth, skipped, names) {
952
+ const svg = node.getPluginData(SVG_FIELD);
953
+ if (svg) return { type: "svg", ...common(node), src: svg };
954
+ const src = node.getPluginData(SRC_FIELD);
955
+ if (src) return { type: "image", ...common(node), ...painted(node, names), src };
956
+ switch (node.type) {
957
+ case "FRAME":
958
+ return frameSpec(node, depth, skipped, names);
959
+ case "TEXT":
960
+ return textSpec(node, names);
961
+ case "RECTANGLE":
962
+ return { type: "rect", ...common(node), ...painted(node, names) };
963
+ case "ELLIPSE":
964
+ return { type: "ellipse", ...common(node), ...painted(node, names) };
965
+ case "INSTANCE":
966
+ return instanceSpec(node);
967
+ default:
968
+ skipped.count += 1;
969
+ return null;
970
+ }
971
+ }
972
+ function frameSpec(node, depth, skipped, names) {
973
+ const children = [];
974
+ if (depth > 0) {
975
+ for (const child of node.children) {
976
+ const spec = toSpec(child, depth - 1, skipped, names);
977
+ if (spec) children.push(spec);
978
+ }
979
+ } else if (node.children.length) {
980
+ skipped.count += node.children.length;
981
+ }
982
+ return {
983
+ type: "frame",
984
+ ...common(node),
985
+ ...painted(node, names),
986
+ ...node.clipsContent ? {} : { clip: false },
987
+ ...layoutSpec(node) ? { layout: layoutSpec(node) } : {},
988
+ ...children.length ? { children } : {}
989
+ };
990
+ }
991
+ function layoutSpec(node) {
992
+ if (node.layoutMode === "NONE") return void 0;
993
+ const pad = fromPadding({
994
+ top: node.paddingTop,
995
+ right: node.paddingRight,
996
+ bottom: node.paddingBottom,
997
+ left: node.paddingLeft
998
+ });
999
+ return {
1000
+ dir: node.layoutMode === "HORIZONTAL" ? "horizontal" : "vertical",
1001
+ ...node.itemSpacing ? { gap: node.itemSpacing } : {},
1002
+ ...pad === void 0 ? {} : { pad },
1003
+ ...node.primaryAxisAlignItems === "MIN" ? {} : { justify: fromPrimaryAxisAlign(node.primaryAxisAlignItems) },
1004
+ ...node.counterAxisAlignItems === "MIN" ? {} : { align: fromCounterAxisAlign(node.counterAxisAlignItems) },
1005
+ ...node.layoutWrap === "WRAP" ? { wrap: true } : {}
1006
+ };
1007
+ }
1008
+ function instanceSpec(node) {
1009
+ const properties = {};
1010
+ for (const [name, property] of Object.entries(node.componentProperties)) {
1011
+ if (typeof property.value === "string" || typeof property.value === "boolean") {
1012
+ properties[name.split("#")[0] ?? name] = property.value;
1013
+ }
1014
+ }
1015
+ return {
1016
+ type: "instance",
1017
+ ...common(node),
1018
+ component: node.getPluginData(COMPONENT_FIELD) || node.name,
1019
+ ...Object.keys(properties).length ? { properties } : {}
1020
+ };
1021
+ }
1022
+ function textSpec(node, names) {
1023
+ const font = node.fontName === figma.mixed ? void 0 : node.fontName;
1024
+ const lineHeight = node.lineHeight === figma.mixed ? void 0 : node.lineHeight;
1025
+ const letterSpacing = node.letterSpacing === figma.mixed ? void 0 : node.letterSpacing;
1026
+ const color = fromPaints(node.fills, names);
1027
+ const style = typeof node.textStyleId === "string" ? names.style(node.textStyleId) : void 0;
1028
+ return {
1029
+ type: "text",
1030
+ ...common(node),
1031
+ text: node.characters,
1032
+ ...style ? { style } : {},
1033
+ ...font ? { font: { family: font.family, style: font.style } } : {},
1034
+ ...node.fontSize === figma.mixed ? {} : { size: node.fontSize },
1035
+ ...lineHeight && lineHeight.unit !== "PERCENT" ? { lineHeight: lineHeight.unit === "AUTO" ? "auto" : lineHeight.value } : {},
1036
+ ...letterSpacing && letterSpacing.unit === "PIXELS" && letterSpacing.value !== 0 ? { letterSpacing: letterSpacing.value } : {},
1037
+ ...node.textAlignHorizontal === "LEFT" ? {} : { align: node.textAlignHorizontal.toLowerCase() },
1038
+ ...color ? { color } : {}
1039
+ };
1040
+ }
1041
+ var CONSTRAINTS_BACK = {
1042
+ MIN: "min",
1043
+ CENTER: "center",
1044
+ MAX: "max",
1045
+ STRETCH: "stretch",
1046
+ SCALE: "scale"
1047
+ };
1048
+ function bounds(node) {
1049
+ const box = node;
1050
+ return {
1051
+ ...box.minWidth == null ? {} : { minW: box.minWidth },
1052
+ ...box.maxWidth == null ? {} : { maxW: box.maxWidth },
1053
+ ...box.minHeight == null ? {} : { minH: box.minHeight },
1054
+ ...box.maxHeight == null ? {} : { maxH: box.maxHeight }
1055
+ };
1056
+ }
1057
+ function placement(node) {
1058
+ const placed = node;
1059
+ const pinned = placed.layoutPositioning === "ABSOLUTE";
1060
+ const constraints = placed.constraints;
1061
+ const custom = constraints && (constraints.horizontal !== "MIN" || constraints.vertical !== "MIN");
1062
+ return {
1063
+ ...pinned ? { at: { x: round(placed.x), y: round(placed.y) } } : {},
1064
+ ...custom && constraints ? {
1065
+ constraints: {
1066
+ h: CONSTRAINTS_BACK[constraints.horizontal],
1067
+ v: CONSTRAINTS_BACK[constraints.vertical]
1068
+ }
1069
+ } : {}
1070
+ };
1071
+ }
1072
+ function common(node) {
1073
+ const key = node.getPluginData(KEY_FIELD);
1074
+ const blend = node;
1075
+ return {
1076
+ ...key ? { key } : {},
1077
+ name: node.name,
1078
+ ...sizing(node),
1079
+ ...bounds(node),
1080
+ ...placement(node),
1081
+ ...blend.opacity === 1 ? {} : { opacity: blend.opacity },
1082
+ ...node.visible ? {} : { visible: false }
1083
+ };
1084
+ }
1085
+ function sizing(node) {
1086
+ const box = node;
1087
+ const parent = node.parent;
1088
+ const inAutoLayout = parent !== null && "layoutMode" in parent && parent.layoutMode !== "NONE";
1089
+ const ownsLayout = node.type === "FRAME" && node.layoutMode !== "NONE";
1090
+ const axis = (fixed, mode) => {
1091
+ if (mode === "HUG" && (inAutoLayout || ownsLayout)) return "hug";
1092
+ if (mode === "FILL" && inAutoLayout) return "fill";
1093
+ return round(fixed);
1094
+ };
1095
+ return {
1096
+ w: axis(box.width, box.layoutSizingHorizontal),
1097
+ h: axis(box.height, box.layoutSizingVertical)
1098
+ };
1099
+ }
1100
+ function painted(node, names) {
1101
+ const shape = node;
1102
+ const fill = fromPaints(shape.fills, names);
1103
+ const effects = fromEffects(shape.effects ?? []);
1104
+ const corners = node;
1105
+ const radius = "topLeftRadius" in corners ? fromRadius({
1106
+ topLeft: corners.topLeftRadius,
1107
+ topRight: corners.topRightRadius,
1108
+ bottomRight: corners.bottomRightRadius,
1109
+ bottomLeft: corners.bottomLeftRadius
1110
+ }) : void 0;
1111
+ const stroke = strokeSpec(shape);
1112
+ return {
1113
+ ...fill ? { fill } : {},
1114
+ ...stroke ? { stroke } : {},
1115
+ ...radius === void 0 ? {} : { radius },
1116
+ ...effects ? { effects } : {}
1117
+ };
1118
+ }
1119
+ function strokeSpec(node) {
1120
+ const paint = Array.isArray(node.strokes) ? node.strokes[0] : void 0;
1121
+ if (!paint || paint.type !== "SOLID") return void 0;
1122
+ const weight = typeof node.strokeWeight === "number" ? node.strokeWeight : 1;
1123
+ const color = fromPaints([paint]);
1124
+ if (typeof color !== "string") return void 0;
1125
+ return {
1126
+ color,
1127
+ ...weight === 1 ? {} : { width: weight },
1128
+ ...node.strokeAlign === "INSIDE" ? {} : { align: node.strokeAlign.toLowerCase() }
1129
+ };
1130
+ }
1131
+ function round(value) {
1132
+ return Math.round(value * 100) / 100;
1133
+ }
1134
+
1135
+ // src/ops/read.ts
1136
+ var DEFAULT_DEPTH = 6;
1137
+ async function read(params) {
1138
+ const targets = params.nodeId ? [await resolve(params.nodeId)] : [...figma.currentPage.selection];
1139
+ if (!targets.length) {
1140
+ throw new Error("Nothing is selected: select a layer in Figma, or pass a nodeId");
1141
+ }
1142
+ const names = await buildNameIndex();
1143
+ const nodes = [];
1144
+ let skipped = 0;
1145
+ for (const target of targets) {
1146
+ const outcome = readNode(target, params.depth ?? DEFAULT_DEPTH, names);
1147
+ skipped += outcome.skipped;
1148
+ if (outcome.tree) nodes.push({ id: target.id, tree: outcome.tree });
1149
+ else skipped += 1;
1150
+ }
1151
+ return { nodes, skipped };
1152
+ }
1153
+ async function resolve(nodeId) {
1154
+ const node = await figma.getNodeByIdAsync(nodeId);
1155
+ if (!node) throw new Error(`No node with id ${nodeId} in this file`);
1156
+ if (node.type === "DOCUMENT" || node.type === "PAGE") {
1157
+ throw new Error(`${node.type} is not a layer: pass a frame or a shape`);
1158
+ }
1159
+ return node;
1160
+ }
1161
+
1162
+ // src/ops/render.ts
1163
+ async function render(params) {
1164
+ const parent = params.parentId ? await resolveParent(params.parentId) : figma.currentPage;
1165
+ const keys = {};
1166
+ const deferred = [];
1167
+ const root = await buildNode(params.tree, parent, keys, void 0, deferred);
1168
+ for (const place of deferred) place();
1169
+ const at = params.at ?? centreOfViewport(root);
1170
+ if ("x" in root && "y" in root && parent.type === "PAGE") {
1171
+ root.x = at.x;
1172
+ root.y = at.y;
1173
+ }
1174
+ if (params.reveal !== false) {
1175
+ figma.currentPage.selection = [root];
1176
+ figma.viewport.scrollAndZoomIntoView([root]);
1177
+ }
1178
+ figma.commitUndo();
1179
+ return { rootId: root.id, nodeCount: countNodes(params.tree), keys };
1180
+ }
1181
+ async function resolveParent(parentId) {
1182
+ const node = await figma.getNodeByIdAsync(parentId);
1183
+ if (!node) throw new Error(`No node with id ${parentId} in this file`);
1184
+ if (!("appendChild" in node)) throw new Error(`${node.type} cannot contain children`);
1185
+ return node;
1186
+ }
1187
+ function centreOfViewport(node) {
1188
+ const { center } = figma.viewport;
1189
+ return { x: Math.round(center.x - node.width / 2), y: Math.round(center.y - node.height / 2) };
1190
+ }
1191
+ function countNodes(spec) {
1192
+ if (spec.type !== "frame") return 1;
1193
+ return 1 + (spec.children ?? []).reduce((total, child) => total + countNodes(child), 0);
1194
+ }
1195
+
1196
+ // src/ops/screenshot.ts
1197
+ async function screenshot(params) {
1198
+ const node = params.target ? await new NodeIndex().resolve(params.target) : figma.currentPage.selection[0];
1199
+ if (!node) {
1200
+ throw new Error("Nothing is selected: select a layer in Figma, or pass a target");
1201
+ }
1202
+ const bytes = await node.exportAsync({
1203
+ format: "PNG",
1204
+ constraint: { type: "SCALE", value: params.scale ?? 1 }
1205
+ });
1206
+ const box = node;
1207
+ return {
1208
+ id: node.id,
1209
+ name: node.name,
1210
+ width: Math.round(box.width),
1211
+ height: Math.round(box.height),
1212
+ png: figma.base64Encode(bytes)
1213
+ };
1214
+ }
1215
+
1216
+ // src/ops/tokens.ts
1217
+ async function tokens() {
1218
+ return readTokens();
1219
+ }
1220
+
1221
+ // src/ops/index.ts
1222
+ async function runOp(op, raw) {
1223
+ const params = raw ?? {};
1224
+ switch (op) {
1225
+ case "ping":
1226
+ return ping();
1227
+ case "render":
1228
+ return render(params);
1229
+ case "read":
1230
+ return read(params);
1231
+ case "patch":
1232
+ return patch(params);
1233
+ case "screenshot":
1234
+ return screenshot(params);
1235
+ case "tokens":
1236
+ return tokens();
1237
+ case "export":
1238
+ return exportAssets(params);
1239
+ case "find":
1240
+ return find(params);
1241
+ case "pages":
1242
+ return pages(params);
1243
+ case "exec":
1244
+ return exec(params);
1245
+ case "define":
1246
+ return define(params);
1247
+ default: {
1248
+ const exhaustive = op;
1249
+ throw new Error(`Unknown op: ${String(exhaustive)}`);
1250
+ }
1251
+ }
1252
+ }
1253
+
1254
+ // src/main.ts
1255
+ var PLUGIN_VERSION = "1.0.0";
1256
+ figma.showUI(__html__, { width: 300, height: 300, themeColors: true });
1257
+ var queue = Promise.resolve();
1258
+ figma.ui.onmessage = (message2) => {
1259
+ if (message2.kind === "ready") return send(context());
1260
+ if (message2.kind === "allow-exec") return setExecAllowed(message2.value);
1261
+ if (message2.kind !== "request") return;
1262
+ queue = queue.then(async () => {
1263
+ try {
1264
+ const result = await runOp(message2.op, message2.params);
1265
+ send({ kind: "response", id: message2.id, ok: true, result });
1266
+ } catch (error) {
1267
+ send({
1268
+ kind: "response",
1269
+ id: message2.id,
1270
+ ok: false,
1271
+ error: {
1272
+ code: "op_failed",
1273
+ message: error instanceof Error ? error.message : String(error)
1274
+ }
1275
+ });
1276
+ }
1277
+ });
1278
+ };
1279
+ function context() {
1280
+ return {
1281
+ kind: "context",
1282
+ pluginVersion: PLUGIN_VERSION,
1283
+ protocolVersion: PROTOCOL_VERSION,
1284
+ fileName: figma.root.name,
1285
+ fileKey: readFileKey()
1286
+ };
1287
+ }
1288
+ function send(message2) {
1289
+ figma.ui.postMessage(message2);
1290
+ }
1291
+ })();