@dannote/figma-use 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ import type { FigmaVariable } from './vars.ts'
2
+
3
+ export type StyleValue = string | number | boolean | FigmaVariable
4
+
5
+ export interface StyleProps {
6
+ // Size
7
+ w?: number
8
+ h?: number
9
+ width?: number
10
+ height?: number
11
+ // Colors
12
+ bg?: string | FigmaVariable
13
+ backgroundColor?: string | FigmaVariable
14
+ // Border
15
+ rounded?: number
16
+ borderRadius?: number
17
+ // Padding
18
+ p?: number
19
+ pt?: number
20
+ pr?: number
21
+ pb?: number
22
+ pl?: number
23
+ px?: number
24
+ py?: number
25
+ padding?: number
26
+ paddingTop?: number
27
+ paddingRight?: number
28
+ paddingBottom?: number
29
+ paddingLeft?: number
30
+ // Text
31
+ size?: number
32
+ font?: string
33
+ weight?: string | number
34
+ fontSize?: number
35
+ fontFamily?: string
36
+ fontWeight?: string | number
37
+ // Layout
38
+ flex?: 'row' | 'col' | 'column'
39
+ flexDirection?: 'row' | 'column'
40
+ justify?: 'start' | 'end' | 'center' | 'between' | 'evenly'
41
+ justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-evenly'
42
+ items?: 'start' | 'end' | 'center' | 'stretch'
43
+ alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch'
44
+ gap?: number
45
+ // Other
46
+ opacity?: number
47
+ color?: string
48
+ [key: string]: StyleValue | undefined
49
+ }
50
+
51
+ /** Simple shorthand → full property mapping */
52
+ export const shorthands: Record<string, string> = {
53
+ w: 'width',
54
+ h: 'height',
55
+ bg: 'backgroundColor',
56
+ rounded: 'borderRadius',
57
+ p: 'padding',
58
+ pt: 'paddingTop',
59
+ pr: 'paddingRight',
60
+ pb: 'paddingBottom',
61
+ pl: 'paddingLeft',
62
+ size: 'fontSize',
63
+ font: 'fontFamily',
64
+ weight: 'fontWeight'
65
+ }
66
+
67
+ /** Value transformations for specific properties */
68
+ export const valueTransforms: Record<string, Record<string, string>> = {
69
+ flexDirection: { col: 'column' },
70
+ justifyContent: {
71
+ start: 'flex-start',
72
+ end: 'flex-end',
73
+ between: 'space-between',
74
+ evenly: 'space-evenly'
75
+ },
76
+ alignItems: { start: 'flex-start', end: 'flex-end' }
77
+ }
78
+
79
+ /** Normalize Tailwind-like shorthand style props to full names */
80
+ export function normalizeStyle(style: StyleProps): StyleProps {
81
+ const result: StyleProps = {}
82
+
83
+ // First pass: expand simple shorthands and copy non-shorthand keys
84
+ for (const [key, value] of Object.entries(style)) {
85
+ // Skip special shorthands handled separately
86
+ if (key === 'px' || key === 'py' || key === 'flex' || key === 'justify' || key === 'items') {
87
+ continue
88
+ }
89
+
90
+ const fullKey = shorthands[key] || key
91
+ // Full property takes precedence - check if it exists in original style
92
+ if (shorthands[key] && style[fullKey] !== undefined) {
93
+ continue
94
+ }
95
+ if (result[fullKey] === undefined) {
96
+ const transform = valueTransforms[fullKey]
97
+ result[fullKey] = transform?.[value as string] ?? value
98
+ }
99
+ }
100
+
101
+ // Expand px/py to individual padding properties
102
+ if (style.px !== undefined) {
103
+ if (result.paddingLeft === undefined) result.paddingLeft = style.px
104
+ if (result.paddingRight === undefined) result.paddingRight = style.px
105
+ }
106
+ if (style.py !== undefined) {
107
+ if (result.paddingTop === undefined) result.paddingTop = style.py
108
+ if (result.paddingBottom === undefined) result.paddingBottom = style.py
109
+ }
110
+
111
+ // Expand flex shorthand
112
+ if (style.flex !== undefined && result.flexDirection === undefined) {
113
+ const transform = valueTransforms.flexDirection
114
+ const value = transform?.[style.flex] ?? style.flex
115
+ result.flexDirection = value === 'col' ? 'column' : (value as StyleProps['flexDirection'])
116
+ }
117
+
118
+ // Expand justify/items shorthands
119
+ if (style.justify !== undefined && result.justifyContent === undefined) {
120
+ const transform = valueTransforms.justifyContent
121
+ result.justifyContent = (transform?.[style.justify] ?? style.justify) as StyleProps['justifyContent']
122
+ }
123
+ if (style.items !== undefined && result.alignItems === undefined) {
124
+ const transform = valueTransforms.alignItems
125
+ result.alignItems = (transform?.[style.items] ?? style.items) as StyleProps['alignItems']
126
+ }
127
+
128
+ return result
129
+ }
@@ -20,7 +20,6 @@
20
20
  * ```
21
21
  */
22
22
 
23
- import { parseColor } from '../color.ts'
24
23
 
25
24
  const VAR_SYMBOL = Symbol.for('figma.variable')
26
25
 
@@ -68,8 +67,8 @@ export function loadVariablesIntoRegistry(variables: Array<{ id: string; name: s
68
67
  if (match) {
69
68
  variableRegistry.set(v.name, {
70
69
  id: v.id,
71
- sessionID: parseInt(match[1], 10),
72
- localID: parseInt(match[2], 10)
70
+ sessionID: parseInt(match[1]!, 10),
71
+ localID: parseInt(match[2]!, 10)
73
72
  })
74
73
  }
75
74
  }
@@ -90,8 +89,8 @@ export function resolveVariable(variable: FigmaVariable): ResolvedVariable {
90
89
  if (idMatch) {
91
90
  const resolved = {
92
91
  id: `VariableID:${idMatch[1]}:${idMatch[2]}`,
93
- sessionID: parseInt(idMatch[1], 10),
94
- localID: parseInt(idMatch[2], 10)
92
+ sessionID: parseInt(idMatch[1]!, 10),
93
+ localID: parseInt(idMatch[2]!, 10)
95
94
  }
96
95
  variable._resolved = resolved
97
96
  return resolved
@@ -1085,6 +1085,20 @@
1085
1085
  // src/main.ts
1086
1086
  var import_svgpath = __toESM(require_svgpath2(), 1);
1087
1087
  console.log("[Figma Bridge] Plugin main loaded at", (/* @__PURE__ */ new Date()).toISOString());
1088
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1089
+ function retry(fn, maxAttempts = 10, delayMs = 50, backoff = "fixed") {
1090
+ return __async(this, null, function* () {
1091
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1092
+ const result = yield fn();
1093
+ if (result) return result;
1094
+ if (attempt < maxAttempts - 1) {
1095
+ const delay = backoff === "linear" ? delayMs * (attempt + 1) : backoff === "exponential" ? delayMs * Math.pow(2, attempt) : delayMs;
1096
+ yield sleep(delay);
1097
+ }
1098
+ }
1099
+ return null;
1100
+ });
1101
+ }
1088
1102
  figma.showUI(__html__, { width: 300, height: 200 });
1089
1103
  var loadedFonts = /* @__PURE__ */ new Set();
1090
1104
  var fontLoadPromises = /* @__PURE__ */ new Map();
@@ -1113,7 +1127,6 @@
1113
1127
  width,
1114
1128
  height,
1115
1129
  name,
1116
- parentId,
1117
1130
  fill,
1118
1131
  stroke,
1119
1132
  strokeWeight,
@@ -1135,8 +1148,8 @@
1135
1148
  frame.y = y;
1136
1149
  frame.resize(width || 100, height || 100);
1137
1150
  if (name) frame.name = name;
1138
- if (fill) frame.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
1139
- if (stroke) frame.strokes = [{ type: "SOLID", color: hexToRgb(stroke) }];
1151
+ if (fill) frame.fills = [{ type: "SOLID", color: hexToRgb(getHexColor(fill)) }];
1152
+ if (stroke) frame.strokes = [{ type: "SOLID", color: hexToRgb(getHexColor(stroke)) }];
1140
1153
  if (strokeWeight) frame.strokeWeight = strokeWeight;
1141
1154
  if (typeof radius === "number") frame.cornerRadius = radius;
1142
1155
  if (typeof opacity === "number") frame.opacity = opacity;
@@ -1157,8 +1170,8 @@
1157
1170
  rect.y = y;
1158
1171
  rect.resize(width || 100, height || 100);
1159
1172
  if (name) rect.name = name;
1160
- if (fill) rect.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
1161
- if (stroke) rect.strokes = [{ type: "SOLID", color: hexToRgb(stroke) }];
1173
+ if (fill) rect.fills = [{ type: "SOLID", color: hexToRgb(getHexColor(fill)) }];
1174
+ if (stroke) rect.strokes = [{ type: "SOLID", color: hexToRgb(getHexColor(stroke)) }];
1162
1175
  if (strokeWeight) rect.strokeWeight = strokeWeight;
1163
1176
  if (typeof radius === "number") rect.cornerRadius = radius;
1164
1177
  if (typeof opacity === "number") rect.opacity = opacity;
@@ -1171,8 +1184,8 @@
1171
1184
  ellipse.y = y;
1172
1185
  ellipse.resize(width || 100, height || 100);
1173
1186
  if (name) ellipse.name = name;
1174
- if (fill) ellipse.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
1175
- if (stroke) ellipse.strokes = [{ type: "SOLID", color: hexToRgb(stroke) }];
1187
+ if (fill) ellipse.fills = [{ type: "SOLID", color: hexToRgb(getHexColor(fill)) }];
1188
+ if (stroke) ellipse.strokes = [{ type: "SOLID", color: hexToRgb(getHexColor(stroke)) }];
1176
1189
  if (strokeWeight) ellipse.strokeWeight = strokeWeight;
1177
1190
  if (typeof opacity === "number") ellipse.opacity = opacity;
1178
1191
  node = ellipse;
@@ -1189,7 +1202,7 @@
1189
1202
  textNode.y = y;
1190
1203
  if (name) textNode.name = name;
1191
1204
  if (fontSize) textNode.fontSize = fontSize;
1192
- if (fill) textNode.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
1205
+ if (fill) textNode.fills = [{ type: "SOLID", color: hexToRgb(getHexColor(fill)) }];
1193
1206
  if (typeof opacity === "number") textNode.opacity = opacity;
1194
1207
  node = textNode;
1195
1208
  break;
@@ -1489,8 +1502,8 @@
1489
1502
  rect.y = y;
1490
1503
  rect.resize(width, height);
1491
1504
  if (name) rect.name = name;
1492
- if (fill) rect.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
1493
- if (stroke) rect.strokes = [{ type: "SOLID", color: hexToRgb(stroke) }];
1505
+ if (fill) rect.fills = [yield createSolidPaint(fill)];
1506
+ if (stroke) rect.strokes = [yield createSolidPaint(stroke)];
1494
1507
  if (strokeWeight !== void 0) rect.strokeWeight = strokeWeight;
1495
1508
  if (radius !== void 0) rect.cornerRadius = radius;
1496
1509
  if (opacity !== void 0) rect.opacity = opacity;
@@ -1504,21 +1517,23 @@
1504
1517
  ellipse.y = y;
1505
1518
  ellipse.resize(width, height);
1506
1519
  if (name) ellipse.name = name;
1507
- if (fill) ellipse.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
1508
- if (stroke) ellipse.strokes = [{ type: "SOLID", color: hexToRgb(stroke) }];
1520
+ if (fill) ellipse.fills = [yield createSolidPaint(fill)];
1521
+ if (stroke) ellipse.strokes = [yield createSolidPaint(stroke)];
1509
1522
  if (strokeWeight !== void 0) ellipse.strokeWeight = strokeWeight;
1510
1523
  if (opacity !== void 0) ellipse.opacity = opacity;
1511
1524
  yield appendToParent(ellipse, parentId);
1512
1525
  return serializeNode(ellipse);
1513
1526
  }
1514
1527
  case "create-line": {
1515
- const { x, y, length, rotation, name, parentId } = args;
1528
+ const { x, y, length, rotation, name, parentId, stroke, strokeWeight } = args;
1516
1529
  const line = figma.createLine();
1517
1530
  line.x = x;
1518
1531
  line.y = y;
1519
1532
  line.resize(length, 0);
1520
1533
  if (rotation) line.rotation = rotation;
1521
1534
  if (name) line.name = name;
1535
+ if (stroke) line.strokes = [yield createSolidPaint(stroke)];
1536
+ if (strokeWeight !== void 0) line.strokeWeight = strokeWeight;
1522
1537
  yield appendToParent(line, parentId);
1523
1538
  return serializeNode(line);
1524
1539
  }
@@ -1577,8 +1592,8 @@
1577
1592
  frame.y = y;
1578
1593
  frame.resize(width, height);
1579
1594
  if (name) frame.name = name;
1580
- if (fill) frame.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
1581
- if (stroke) frame.strokes = [{ type: "SOLID", color: hexToRgb(stroke) }];
1595
+ if (fill) frame.fills = [yield createSolidPaint(fill)];
1596
+ if (stroke) frame.strokes = [yield createSolidPaint(stroke)];
1582
1597
  if (strokeWeight !== void 0) frame.strokeWeight = strokeWeight;
1583
1598
  if (radius !== void 0) frame.cornerRadius = radius;
1584
1599
  if (opacity !== void 0) frame.opacity = opacity;
@@ -1625,7 +1640,7 @@
1625
1640
  textNode.fontName = { family, style };
1626
1641
  textNode.characters = text;
1627
1642
  if (fontSize) textNode.fontSize = fontSize;
1628
- if (fill) textNode.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
1643
+ if (fill) textNode.fills = [yield createSolidPaint(fill)];
1629
1644
  if (opacity !== void 0) textNode.opacity = opacity;
1630
1645
  if (name) textNode.name = name;
1631
1646
  yield appendToParent(textNode, parentId);
@@ -1643,9 +1658,13 @@
1643
1658
  return serializeNode(instance);
1644
1659
  }
1645
1660
  case "create-component": {
1646
- const { name, parentId } = args;
1661
+ const { name, parentId, x, y, width, height, fill } = args;
1647
1662
  const component = figma.createComponent();
1648
1663
  component.name = name;
1664
+ if (x !== void 0) component.x = x;
1665
+ if (y !== void 0) component.y = y;
1666
+ if (width && height) component.resize(width, height);
1667
+ if (fill) component.fills = [yield createSolidPaint(fill)];
1649
1668
  yield appendToParent(component, parentId);
1650
1669
  return serializeNode(component);
1651
1670
  }
@@ -1656,12 +1675,19 @@
1656
1675
  const clone = node.clone();
1657
1676
  return serializeNode(clone);
1658
1677
  }
1678
+ case "convert-to-component": {
1679
+ const { id } = args;
1680
+ const node = yield figma.getNodeByIdAsync(id);
1681
+ if (!node) throw new Error("Node not found");
1682
+ const component = figma.createComponentFromNode(node);
1683
+ return serializeNode(component);
1684
+ }
1659
1685
  // ==================== CREATE STYLES ====================
1660
1686
  case "create-paint-style": {
1661
1687
  const { name, color } = args;
1662
1688
  const style = figma.createPaintStyle();
1663
1689
  style.name = name;
1664
- style.paints = [{ type: "SOLID", color: hexToRgb(color) }];
1690
+ style.paints = [yield createSolidPaint(color)];
1665
1691
  return { id: style.id, name: style.name, key: style.key };
1666
1692
  }
1667
1693
  case "create-text-style": {
@@ -1730,14 +1756,14 @@
1730
1756
  const { id, color } = args;
1731
1757
  const node = yield figma.getNodeByIdAsync(id);
1732
1758
  if (!node || !("fills" in node)) throw new Error("Node not found");
1733
- node.fills = [{ type: "SOLID", color: hexToRgb(color) }];
1759
+ node.fills = [yield createSolidPaint(color)];
1734
1760
  return serializeNode(node);
1735
1761
  }
1736
1762
  case "set-stroke-color": {
1737
1763
  const { id, color, weight, align } = args;
1738
1764
  const node = yield figma.getNodeByIdAsync(id);
1739
1765
  if (!node || !("strokes" in node)) throw new Error("Node not found");
1740
- node.strokes = [{ type: "SOLID", color: hexToRgb(color) }];
1766
+ node.strokes = [yield createSolidPaint(color)];
1741
1767
  if (weight !== void 0 && "strokeWeight" in node) node.strokeWeight = weight;
1742
1768
  if (align && "strokeAlign" in node)
1743
1769
  node.strokeAlign = align;
@@ -1790,6 +1816,34 @@
1790
1816
  node.name = name;
1791
1817
  return serializeNode(node);
1792
1818
  }
1819
+ case "bind-fill-variable-by-name": {
1820
+ let bindFills2 = function(n) {
1821
+ if ("fills" in n && Array.isArray(n.fills) && n.fills.length > 0) {
1822
+ const fills = [...n.fills];
1823
+ for (let i = 0; i < fills.length; i++) {
1824
+ if (fills[i].type === "SOLID") {
1825
+ fills[i] = figma.variables.setBoundVariableForPaint(fills[i], "color", variable);
1826
+ }
1827
+ }
1828
+ ;
1829
+ n.fills = fills;
1830
+ }
1831
+ if (recursive && "children" in n) {
1832
+ for (const child of n.children) {
1833
+ bindFills2(child);
1834
+ }
1835
+ }
1836
+ };
1837
+ var bindFills = bindFills2;
1838
+ const { id, variableName, recursive } = args;
1839
+ const node = yield figma.getNodeByIdAsync(id);
1840
+ if (!node) throw new Error("Node not found");
1841
+ const variables = yield figma.variables.getLocalVariablesAsync("COLOR");
1842
+ const variable = variables.find((v) => v.name === variableName);
1843
+ if (!variable) throw new Error(`Variable "${variableName}" not found`);
1844
+ bindFills2(node);
1845
+ return serializeNode(node);
1846
+ }
1793
1847
  case "set-visible": {
1794
1848
  const { id, visible } = args;
1795
1849
  const node = yield figma.getNodeByIdAsync(id);
@@ -1843,12 +1897,13 @@
1843
1897
  return serializeNode(node);
1844
1898
  }
1845
1899
  case "import-svg": {
1846
- const { svg, x, y, name, parentId } = args;
1900
+ const { svg, x, y, name, parentId, noFill, insertIndex } = args;
1847
1901
  const node = figma.createNodeFromSvg(svg);
1848
1902
  if (x !== void 0) node.x = x;
1849
1903
  if (y !== void 0) node.y = y;
1850
1904
  if (name) node.name = name;
1851
- yield appendToParent(node, parentId);
1905
+ if (noFill) node.fills = [];
1906
+ yield appendToParent(node, parentId, insertIndex);
1852
1907
  return serializeNode(node);
1853
1908
  }
1854
1909
  case "set-font": {
@@ -1878,8 +1933,7 @@
1878
1933
  node.setRangeFontSize(start, end, size);
1879
1934
  }
1880
1935
  if (color) {
1881
- const rgb = hexToRgb(color);
1882
- node.setRangeFills(start, end, [{ type: "SOLID", color: rgb }]);
1936
+ node.setRangeFills(start, end, [yield createSolidPaint(color)]);
1883
1937
  }
1884
1938
  return serializeNode(node);
1885
1939
  }
@@ -2405,12 +2459,12 @@
2405
2459
  // ==================== LAYOUT ====================
2406
2460
  case "trigger-layout": {
2407
2461
  const { nodeId, pendingComponentSetInstances } = args;
2408
- let root = null;
2409
- for (let i = 0; i < 10; i++) {
2410
- root = yield figma.getNodeByIdAsync(nodeId);
2411
- if (root) break;
2412
- yield new Promise((r) => setTimeout(r, 100 * (i + 1)));
2413
- }
2462
+ const root = yield retry(
2463
+ () => figma.getNodeByIdAsync(nodeId),
2464
+ 10,
2465
+ 100,
2466
+ "linear"
2467
+ );
2414
2468
  if (!root) return null;
2415
2469
  if (pendingComponentSetInstances && pendingComponentSetInstances.length > 0) {
2416
2470
  for (const pending of pendingComponentSetInstances) {
@@ -2611,14 +2665,23 @@
2611
2665
  }
2612
2666
  });
2613
2667
  }
2614
- function appendToParent(node, parentId) {
2668
+ function appendToParent(node, parentId, insertIndex) {
2615
2669
  return __async(this, null, function* () {
2616
2670
  if (parentId) {
2617
- const parent = yield figma.getNodeByIdAsync(parentId);
2671
+ const parent = yield retry(
2672
+ () => figma.getNodeByIdAsync(parentId),
2673
+ 10,
2674
+ 50
2675
+ );
2618
2676
  if (parent && "appendChild" in parent) {
2619
- parent.appendChild(node);
2677
+ if (insertIndex !== void 0 && "insertChild" in parent) {
2678
+ parent.insertChild(insertIndex, node);
2679
+ } else {
2680
+ parent.appendChild(node);
2681
+ }
2620
2682
  return;
2621
2683
  }
2684
+ console.warn(`Parent ${parentId} not found after retries, appending to page`);
2622
2685
  }
2623
2686
  figma.currentPage.appendChild(node);
2624
2687
  });
@@ -2730,6 +2793,37 @@
2730
2793
  b: parseInt(clean.slice(4, 6), 16) / 255
2731
2794
  };
2732
2795
  }
2796
+ function parsestring(color) {
2797
+ const varMatch = color.match(/^(?:var:|[$])(.+)$/);
2798
+ if (varMatch) {
2799
+ return { variable: varMatch[1] };
2800
+ }
2801
+ return { hex: color };
2802
+ }
2803
+ function getHexColor(color) {
2804
+ const parsed = parsestring(color);
2805
+ return parsed.hex || "#000000";
2806
+ }
2807
+ function createSolidPaint(color) {
2808
+ return __async(this, null, function* () {
2809
+ const parsed = parsestring(color);
2810
+ if (parsed.hex) {
2811
+ return { type: "SOLID", color: hexToRgb(parsed.hex) };
2812
+ }
2813
+ const variables = yield figma.variables.getLocalVariablesAsync("COLOR");
2814
+ const variable = variables.find((v) => v.name === parsed.variable);
2815
+ if (!variable) {
2816
+ console.warn(`Variable "${parsed.variable}" not found, using black`);
2817
+ return { type: "SOLID", color: { r: 0, g: 0, b: 0 } };
2818
+ }
2819
+ const paint = {
2820
+ type: "SOLID",
2821
+ color: { r: 0, g: 0, b: 0 }
2822
+ // Will be overridden by variable
2823
+ };
2824
+ return figma.variables.setBoundVariableForPaint(paint, "color", variable);
2825
+ });
2826
+ }
2733
2827
  function hexToRgba(hex) {
2734
2828
  const clean = expandHex(hex);
2735
2829
  const hasAlpha = clean.length === 8;