@dannote/figma-use 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +33 -1
- package/SKILL.md +22 -1
- package/dist/cli/index.js +186 -25
- package/dist/proxy/index.js +173 -2
- package/package.json +1 -1
- package/packages/plugin/dist/main.js +51 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-01-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`defineVars` API for Figma variables** — bind colors to variables by name
|
|
15
|
+
```tsx
|
|
16
|
+
const colors = defineVars({
|
|
17
|
+
primary: { name: 'Colors/Gray/50', value: '#F8FAFC' },
|
|
18
|
+
accent: { name: 'Colors/Blue/500', value: '#3B82F6' },
|
|
19
|
+
})
|
|
20
|
+
<Frame style={{ backgroundColor: colors.primary }} />
|
|
21
|
+
```
|
|
22
|
+
- Variable binding for `backgroundColor`, `borderColor`, and text `color`
|
|
23
|
+
- Variables resolved by name at render time (no more magic IDs)
|
|
24
|
+
- `defineVars` support in stdin snippets
|
|
25
|
+
- Explicit fallback values in `defineVars` for proper color display
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Auto-layout now works correctly via `trigger-layout` post-render
|
|
30
|
+
- Nested auto-layout frames trigger recursively
|
|
31
|
+
- Variable binding encoding matches Figma's exact wire format
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Marked React render and variable bindings as **experimental** in docs
|
|
36
|
+
|
|
37
|
+
## [0.3.1] - 2026-01-18
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **Variable binding via multiplayer protocol** — bind fill colors to Figma variables without plugin API
|
|
42
|
+
- `encodePaintWithVariableBinding()` — encode Paint with color variable binding
|
|
43
|
+
- `encodeNodeChangeWithVariables()` — encode NodeChange with variable-bound paints
|
|
44
|
+
- `parseVariableId()` — parse "VariableID:sessionID:localID" strings
|
|
45
|
+
- New exports: `VariableBinding`, `encodePaintWithVariableBinding`, `encodeNodeChangeWithVariables`, `parseVariableId`
|
|
46
|
+
- `bind-fill-variable` plugin command — bind fill color to variable
|
|
47
|
+
- `bind-stroke-variable` plugin command — bind stroke color to variable
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- Message field mapping: nodeChanges is field 4, reconnectSequenceNumber is field 25
|
|
52
|
+
- Paint variable binding format now matches Figma's exact wire format
|
|
53
|
+
|
|
54
|
+
### Technical
|
|
55
|
+
|
|
56
|
+
- Discovered Figma's variable binding wire format via WebSocket traffic analysis
|
|
57
|
+
- Created capture/diff tools for binary protocol analysis (`scripts/capture.ts`, `scripts/diff-hex.ts`)
|
|
58
|
+
- 142 tests passing
|
|
59
|
+
|
|
10
60
|
## [0.3.0] - 2025-01-17
|
|
11
61
|
|
|
12
62
|
### Added
|
package/README.md
CHANGED
|
@@ -258,7 +258,9 @@ figma-use group ungroup <id>
|
|
|
258
258
|
figma-use group flatten "1:2,1:3"
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
### Render React Components
|
|
261
|
+
### Render React Components (Experimental)
|
|
262
|
+
|
|
263
|
+
> ⚠️ **Experimental**: The React render feature uses Figma's internal multiplayer protocol, which is undocumented and may change without notice. Use for prototyping and automation, not production workflows.
|
|
262
264
|
|
|
263
265
|
Render TSX/JSX components directly to Figma via WebSocket (bypasses plugin API for ~100x speed):
|
|
264
266
|
|
|
@@ -443,3 +445,33 @@ Workflow:
|
|
|
443
445
|
## License
|
|
444
446
|
|
|
445
447
|
MIT
|
|
448
|
+
|
|
449
|
+
### Variable Bindings (Experimental)
|
|
450
|
+
|
|
451
|
+
> ⚠️ **Experimental**: Variable binding uses reverse-engineered protocol. Supports `backgroundColor`, `borderColor`, and text `color`.
|
|
452
|
+
|
|
453
|
+
Bind Figma variables to colors using human-readable names:
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
// tokens.figma.ts
|
|
457
|
+
import { defineVars } from '@dannote/figma-use'
|
|
458
|
+
|
|
459
|
+
export const colors = defineVars({
|
|
460
|
+
primary: { name: 'Colors/Gray/50', value: '#F8FAFC' },
|
|
461
|
+
accent: { name: 'Colors/Blue/500', value: '#3B82F6' },
|
|
462
|
+
text: { name: 'Colors/Gray/900', value: '#0F172A' },
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
// Card.figma.tsx
|
|
466
|
+
import { colors } from './tokens.figma'
|
|
467
|
+
|
|
468
|
+
export function Card({ title }: { title: string }) {
|
|
469
|
+
return (
|
|
470
|
+
<Frame style={{ backgroundColor: colors.primary }}>
|
|
471
|
+
<Text style={{ color: colors.text }}>{title}</Text>
|
|
472
|
+
</Frame>
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
The `value` field provides a fallback color for display. Variables are bound at the protocol level — no plugin API calls needed.
|
package/SKILL.md
CHANGED
|
@@ -148,7 +148,9 @@ figma-use group create "1:2,1:3"
|
|
|
148
148
|
figma-use group ungroup <id>
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
### Render React Components
|
|
151
|
+
### Render React Components (Experimental)
|
|
152
|
+
|
|
153
|
+
> ⚠️ Uses Figma's internal multiplayer protocol — may break without notice.
|
|
152
154
|
|
|
153
155
|
Render TSX/JSX directly to Figma (~100x faster than plugin API):
|
|
154
156
|
|
|
@@ -178,6 +180,25 @@ figma-use render ./Card.figma.tsx --parent "1:23"
|
|
|
178
180
|
|
|
179
181
|
Available elements: `Frame`, `Rectangle`, `Ellipse`, `Text`, `Line`, `Star`, `Polygon`, `Vector`, `Component`, `Instance`, `Group`
|
|
180
182
|
|
|
183
|
+
#### Variable Bindings (Experimental)
|
|
184
|
+
|
|
185
|
+
Bind Figma variables to colors by name with fallback values:
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { defineVars, Frame } from '@dannote/figma-use'
|
|
189
|
+
|
|
190
|
+
const colors = defineVars({
|
|
191
|
+
primary: { name: 'Colors/Gray/50', value: '#F8FAFC' },
|
|
192
|
+
border: { name: 'Colors/Gray/500', value: '#64748B' },
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
export default () => (
|
|
196
|
+
<Frame style={{ backgroundColor: colors.primary, borderColor: colors.border }} />
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Supports: `backgroundColor`, `borderColor`, text `color`.
|
|
201
|
+
|
|
181
202
|
### Eval (Arbitrary Code)
|
|
182
203
|
|
|
183
204
|
```bash
|
package/dist/cli/index.js
CHANGED
|
@@ -18920,6 +18920,92 @@ var require_react_reconciler = __commonJS((exports, module) => {
|
|
|
18920
18920
|
}
|
|
18921
18921
|
});
|
|
18922
18922
|
|
|
18923
|
+
// packages/cli/src/render/vars.ts
|
|
18924
|
+
var exports_vars = {};
|
|
18925
|
+
__export(exports_vars, {
|
|
18926
|
+
resolveVariable: () => resolveVariable,
|
|
18927
|
+
loadVariablesIntoRegistry: () => loadVariablesIntoRegistry,
|
|
18928
|
+
isVariable: () => isVariable,
|
|
18929
|
+
isRegistryLoaded: () => isRegistryLoaded,
|
|
18930
|
+
getRegistrySize: () => getRegistrySize,
|
|
18931
|
+
figmaVar: () => figmaVar,
|
|
18932
|
+
defineVars: () => defineVars
|
|
18933
|
+
});
|
|
18934
|
+
function isVariable(value) {
|
|
18935
|
+
return typeof value === "object" && value !== null && VAR_SYMBOL in value;
|
|
18936
|
+
}
|
|
18937
|
+
function loadVariablesIntoRegistry(variables) {
|
|
18938
|
+
variableRegistry.clear();
|
|
18939
|
+
for (const v3 of variables) {
|
|
18940
|
+
const match = v3.id.match(/VariableID:(\d+):(\d+)/);
|
|
18941
|
+
if (match) {
|
|
18942
|
+
variableRegistry.set(v3.name, {
|
|
18943
|
+
id: v3.id,
|
|
18944
|
+
sessionID: parseInt(match[1], 10),
|
|
18945
|
+
localID: parseInt(match[2], 10)
|
|
18946
|
+
});
|
|
18947
|
+
}
|
|
18948
|
+
}
|
|
18949
|
+
}
|
|
18950
|
+
function resolveVariable(variable) {
|
|
18951
|
+
if (variable._resolved) {
|
|
18952
|
+
return variable._resolved;
|
|
18953
|
+
}
|
|
18954
|
+
const idMatch = variable.name.match(/^(?:VariableID:)?(\d+):(\d+)$/);
|
|
18955
|
+
if (idMatch) {
|
|
18956
|
+
const resolved2 = {
|
|
18957
|
+
id: `VariableID:${idMatch[1]}:${idMatch[2]}`,
|
|
18958
|
+
sessionID: parseInt(idMatch[1], 10),
|
|
18959
|
+
localID: parseInt(idMatch[2], 10)
|
|
18960
|
+
};
|
|
18961
|
+
variable._resolved = resolved2;
|
|
18962
|
+
return resolved2;
|
|
18963
|
+
}
|
|
18964
|
+
const resolved = variableRegistry.get(variable.name);
|
|
18965
|
+
if (!resolved) {
|
|
18966
|
+
const available = Array.from(variableRegistry.keys()).slice(0, 5).join(", ");
|
|
18967
|
+
throw new Error(`Variable "${variable.name}" not found. ` + `Available: ${available}${variableRegistry.size > 5 ? "..." : ""}. ` + `Make sure variables are loaded before render.`);
|
|
18968
|
+
}
|
|
18969
|
+
variable._resolved = resolved;
|
|
18970
|
+
return resolved;
|
|
18971
|
+
}
|
|
18972
|
+
function isRegistryLoaded() {
|
|
18973
|
+
return variableRegistry.size > 0;
|
|
18974
|
+
}
|
|
18975
|
+
function getRegistrySize() {
|
|
18976
|
+
return variableRegistry.size;
|
|
18977
|
+
}
|
|
18978
|
+
function defineVars(vars) {
|
|
18979
|
+
const result = {};
|
|
18980
|
+
for (const [key, def] of Object.entries(vars)) {
|
|
18981
|
+
if (typeof def === "string") {
|
|
18982
|
+
result[key] = {
|
|
18983
|
+
[VAR_SYMBOL]: true,
|
|
18984
|
+
name: def
|
|
18985
|
+
};
|
|
18986
|
+
} else {
|
|
18987
|
+
result[key] = {
|
|
18988
|
+
[VAR_SYMBOL]: true,
|
|
18989
|
+
name: def.name,
|
|
18990
|
+
value: def.value
|
|
18991
|
+
};
|
|
18992
|
+
}
|
|
18993
|
+
}
|
|
18994
|
+
return result;
|
|
18995
|
+
}
|
|
18996
|
+
function figmaVar(name, value) {
|
|
18997
|
+
return {
|
|
18998
|
+
[VAR_SYMBOL]: true,
|
|
18999
|
+
name,
|
|
19000
|
+
value
|
|
19001
|
+
};
|
|
19002
|
+
}
|
|
19003
|
+
var VAR_SYMBOL, variableRegistry;
|
|
19004
|
+
var init_vars = __esm(() => {
|
|
19005
|
+
VAR_SYMBOL = Symbol.for("figma.variable");
|
|
19006
|
+
variableRegistry = new Map;
|
|
19007
|
+
});
|
|
19008
|
+
|
|
18923
19009
|
// node_modules/esbuild/lib/main.js
|
|
18924
19010
|
var require_main = __commonJS((exports, module) => {
|
|
18925
19011
|
var __dirname = "/Users/dannote/Development/figma-use/node_modules/esbuild/lib", __filename = "/Users/dannote/Development/figma-use/node_modules/esbuild/lib/main.js";
|
|
@@ -27547,6 +27633,7 @@ function parseColor(color) {
|
|
|
27547
27633
|
}
|
|
27548
27634
|
|
|
27549
27635
|
// packages/cli/src/render/reconciler.ts
|
|
27636
|
+
init_vars();
|
|
27550
27637
|
function styleToNodeChange(type, props, localID, sessionID, parentGUID, position, textContent) {
|
|
27551
27638
|
const style = props.style || {};
|
|
27552
27639
|
const name = props.name || type;
|
|
@@ -27575,22 +27662,52 @@ function styleToNodeChange(type, props, localID, sessionID, parentGUID, position
|
|
|
27575
27662
|
m12: y5
|
|
27576
27663
|
};
|
|
27577
27664
|
if (style.backgroundColor) {
|
|
27578
|
-
const
|
|
27579
|
-
|
|
27580
|
-
|
|
27581
|
-
|
|
27582
|
-
|
|
27583
|
-
|
|
27584
|
-
|
|
27665
|
+
const bgColor = style.backgroundColor;
|
|
27666
|
+
if (isVariable(bgColor)) {
|
|
27667
|
+
const resolved = resolveVariable(bgColor);
|
|
27668
|
+
const fallback = bgColor.value ? parseColor(bgColor.value) : { r: 0, g: 0, b: 0, a: 1 };
|
|
27669
|
+
nodeChange.fillPaints = [{
|
|
27670
|
+
type: "SOLID",
|
|
27671
|
+
color: { r: fallback.r, g: fallback.g, b: fallback.b, a: fallback.a },
|
|
27672
|
+
opacity: 1,
|
|
27673
|
+
visible: true,
|
|
27674
|
+
colorVariableBinding: {
|
|
27675
|
+
variableID: { sessionID: resolved.sessionID, localID: resolved.localID }
|
|
27676
|
+
}
|
|
27677
|
+
}];
|
|
27678
|
+
} else {
|
|
27679
|
+
const color = parseColor(bgColor);
|
|
27680
|
+
nodeChange.fillPaints = [{
|
|
27681
|
+
type: "SOLID",
|
|
27682
|
+
color: { r: color.r, g: color.g, b: color.b, a: color.a },
|
|
27683
|
+
opacity: color.a,
|
|
27684
|
+
visible: true
|
|
27685
|
+
}];
|
|
27686
|
+
}
|
|
27585
27687
|
}
|
|
27586
27688
|
if (style.borderColor) {
|
|
27587
|
-
const
|
|
27588
|
-
|
|
27589
|
-
|
|
27590
|
-
|
|
27591
|
-
|
|
27592
|
-
|
|
27593
|
-
|
|
27689
|
+
const borderColor = style.borderColor;
|
|
27690
|
+
if (isVariable(borderColor)) {
|
|
27691
|
+
const resolved = resolveVariable(borderColor);
|
|
27692
|
+
const fallback = borderColor.value ? parseColor(borderColor.value) : { r: 0, g: 0, b: 0, a: 1 };
|
|
27693
|
+
nodeChange.strokePaints = [{
|
|
27694
|
+
type: "SOLID",
|
|
27695
|
+
color: { r: fallback.r, g: fallback.g, b: fallback.b, a: fallback.a },
|
|
27696
|
+
opacity: 1,
|
|
27697
|
+
visible: true,
|
|
27698
|
+
colorVariableBinding: {
|
|
27699
|
+
variableID: { sessionID: resolved.sessionID, localID: resolved.localID }
|
|
27700
|
+
}
|
|
27701
|
+
}];
|
|
27702
|
+
} else {
|
|
27703
|
+
const color = parseColor(borderColor);
|
|
27704
|
+
nodeChange.strokePaints = [{
|
|
27705
|
+
type: "SOLID",
|
|
27706
|
+
color: { r: color.r, g: color.g, b: color.b, a: color.a },
|
|
27707
|
+
opacity: color.a,
|
|
27708
|
+
visible: true
|
|
27709
|
+
}];
|
|
27710
|
+
}
|
|
27594
27711
|
nodeChange.strokeWeight = Number(style.borderWidth ?? 1);
|
|
27595
27712
|
}
|
|
27596
27713
|
if (style.borderRadius !== undefined) {
|
|
@@ -27614,6 +27731,8 @@ function styleToNodeChange(type, props, localID, sessionID, parentGUID, position
|
|
|
27614
27731
|
}
|
|
27615
27732
|
if (style.flexDirection) {
|
|
27616
27733
|
nodeChange.stackMode = style.flexDirection === "row" ? "HORIZONTAL" : "VERTICAL";
|
|
27734
|
+
nodeChange.stackPrimarySizing = "RESIZE_TO_FIT";
|
|
27735
|
+
nodeChange.stackCounterSizing = "RESIZE_TO_FIT";
|
|
27617
27736
|
}
|
|
27618
27737
|
if (style.gap !== undefined) {
|
|
27619
27738
|
nodeChange.stackSpacing = Number(style.gap);
|
|
@@ -27667,13 +27786,28 @@ function styleToNodeChange(type, props, localID, sessionID, parentGUID, position
|
|
|
27667
27786
|
nc.textAlignHorizontal = map[style.textAlign] || "LEFT";
|
|
27668
27787
|
}
|
|
27669
27788
|
if (style.color) {
|
|
27670
|
-
const
|
|
27671
|
-
|
|
27672
|
-
|
|
27673
|
-
|
|
27674
|
-
|
|
27675
|
-
|
|
27676
|
-
|
|
27789
|
+
const textColor = style.color;
|
|
27790
|
+
if (isVariable(textColor)) {
|
|
27791
|
+
const resolved = resolveVariable(textColor);
|
|
27792
|
+
const fallback = textColor.value ? parseColor(textColor.value) : { r: 0, g: 0, b: 0, a: 1 };
|
|
27793
|
+
nodeChange.fillPaints = [{
|
|
27794
|
+
type: "SOLID",
|
|
27795
|
+
color: { r: fallback.r, g: fallback.g, b: fallback.b, a: fallback.a },
|
|
27796
|
+
opacity: 1,
|
|
27797
|
+
visible: true,
|
|
27798
|
+
colorVariableBinding: {
|
|
27799
|
+
variableID: { sessionID: resolved.sessionID, localID: resolved.localID }
|
|
27800
|
+
}
|
|
27801
|
+
}];
|
|
27802
|
+
} else {
|
|
27803
|
+
const color = parseColor(textColor);
|
|
27804
|
+
nodeChange.fillPaints = [{
|
|
27805
|
+
type: "SOLID",
|
|
27806
|
+
color: { r: color.r, g: color.g, b: color.b, a: color.a },
|
|
27807
|
+
opacity: color.a,
|
|
27808
|
+
visible: true
|
|
27809
|
+
}];
|
|
27810
|
+
}
|
|
27677
27811
|
}
|
|
27678
27812
|
}
|
|
27679
27813
|
return nodeChange;
|
|
@@ -27866,6 +28000,7 @@ function renderToNodeChanges(element, options) {
|
|
|
27866
28000
|
};
|
|
27867
28001
|
}
|
|
27868
28002
|
// packages/cli/src/render/components.tsx
|
|
28003
|
+
init_vars();
|
|
27869
28004
|
var React = __toESM(require_react(), 1);
|
|
27870
28005
|
var c6 = (type) => (props) => React.createElement(type, props);
|
|
27871
28006
|
var Frame = c6("frame");
|
|
@@ -27921,8 +28056,27 @@ function findNodeModulesDir() {
|
|
|
27921
28056
|
var JSX_DEFINE = Object.fromEntries(INTRINSIC_ELEMENTS.map((name) => [name, JSON.stringify(name.toLowerCase())]));
|
|
27922
28057
|
function transformJsxSnippet(code) {
|
|
27923
28058
|
const snippet = code.trim();
|
|
27924
|
-
|
|
27925
|
-
|
|
28059
|
+
if (snippet.includes("import ") || snippet.includes("export ")) {
|
|
28060
|
+
return import_esbuild.transformSync(snippet, {
|
|
28061
|
+
loader: "tsx",
|
|
28062
|
+
jsx: "transform",
|
|
28063
|
+
jsxFactory: "React.createElement",
|
|
28064
|
+
jsxFragment: "React.Fragment",
|
|
28065
|
+
define: JSX_DEFINE
|
|
28066
|
+
}).code;
|
|
28067
|
+
}
|
|
28068
|
+
const jsxStart = snippet.search(/<[A-Z]/);
|
|
28069
|
+
const hasSetupCode = jsxStart > 0;
|
|
28070
|
+
const usesDefineVars = snippet.includes("defineVars");
|
|
28071
|
+
let fullCode;
|
|
28072
|
+
if (hasSetupCode) {
|
|
28073
|
+
const setupPart = snippet.slice(0, jsxStart).trim();
|
|
28074
|
+
const jsxPart = snippet.slice(jsxStart);
|
|
28075
|
+
const params = usesDefineVars ? "(React, { defineVars })" : "(React)";
|
|
28076
|
+
fullCode = `export default ${params} => { ${setupPart}; return () => (${jsxPart}); };`;
|
|
28077
|
+
} else {
|
|
28078
|
+
fullCode = `export default (React) => () => (${snippet});`;
|
|
28079
|
+
}
|
|
27926
28080
|
const result = import_esbuild.transformSync(fullCode, {
|
|
27927
28081
|
loader: "tsx",
|
|
27928
28082
|
jsx: "transform",
|
|
@@ -27973,8 +28127,9 @@ var render_default = defineCommand({
|
|
|
27973
28127
|
const module = await import(filePath);
|
|
27974
28128
|
const exportName = args.export || "default";
|
|
27975
28129
|
let Component2 = module[exportName];
|
|
27976
|
-
if (typeof Component2 === "function" && Component2.length === 1 && args.stdin) {
|
|
27977
|
-
|
|
28130
|
+
if (typeof Component2 === "function" && (Component2.length === 1 || Component2.length === 2) && args.stdin) {
|
|
28131
|
+
const { defineVars: defineVars2 } = await Promise.resolve().then(() => (init_vars(), exports_vars));
|
|
28132
|
+
Component2 = Component2(React2, { defineVars: defineVars2 });
|
|
27978
28133
|
}
|
|
27979
28134
|
if (!Component2) {
|
|
27980
28135
|
console.error(fail(`Export "${exportName}" not found`));
|
|
@@ -28003,6 +28158,12 @@ var render_default = defineCommand({
|
|
|
28003
28158
|
throw new Error(data.error);
|
|
28004
28159
|
}
|
|
28005
28160
|
};
|
|
28161
|
+
if (!isRegistryLoaded()) {
|
|
28162
|
+
try {
|
|
28163
|
+
const variables = await sendCommand("get-variables", { simple: true });
|
|
28164
|
+
loadVariablesIntoRegistry(variables);
|
|
28165
|
+
} catch {}
|
|
28166
|
+
}
|
|
28006
28167
|
const props = args.props ? JSON.parse(args.props) : {};
|
|
28007
28168
|
const element = React2.createElement(Component2, props);
|
|
28008
28169
|
const result = renderToNodeChanges(element, {
|
package/dist/proxy/index.js
CHANGED
|
@@ -10178,6 +10178,21 @@ message PaintFilterMessage {
|
|
|
10178
10178
|
float brightness = 10 [deprecated];
|
|
10179
10179
|
}
|
|
10180
10180
|
|
|
10181
|
+
// Variable binding embedded in Paint - discovered via WS sniffing 2026-01
|
|
10182
|
+
// Figma wire format: 15 01 04 01 {sessionID varint} {localID varint}
|
|
10183
|
+
// - 15 = field 21 in Paint
|
|
10184
|
+
// - 01 = always 1 (binding type for color)
|
|
10185
|
+
// - 04 = field 4
|
|
10186
|
+
// - 01 = always 1 (flag)
|
|
10187
|
+
// - then GUID as raw sessionID + localID varints (no field numbers)
|
|
10188
|
+
// NOTE: kiwi-schema cannot produce this exact format, handled manually in codec
|
|
10189
|
+
message PaintVariableBinding {
|
|
10190
|
+
uint bindingType = 1; // Always 1 for color
|
|
10191
|
+
uint flag = 2; // Placeholder
|
|
10192
|
+
uint flag2 = 3; // Placeholder
|
|
10193
|
+
GUID variableID = 4; // Variable GUID
|
|
10194
|
+
}
|
|
10195
|
+
|
|
10181
10196
|
message Paint {
|
|
10182
10197
|
PaintType type = 1;
|
|
10183
10198
|
Color color = 2;
|
|
@@ -10199,6 +10214,7 @@ message Paint {
|
|
|
10199
10214
|
Video video = 18;
|
|
10200
10215
|
uint originalImageWidth = 19;
|
|
10201
10216
|
uint originalImageHeight = 20;
|
|
10217
|
+
PaintVariableBinding variableBinding = 21; // Not in fig-kiwi. Discovered via WS sniffing 2026-01
|
|
10202
10218
|
}
|
|
10203
10219
|
|
|
10204
10220
|
message FontMetaData {
|
|
@@ -11320,6 +11336,8 @@ message Message {
|
|
|
11320
11336
|
uint reconnectSequenceNumber = 25;
|
|
11321
11337
|
string pasteBranchSourceFileKey = 26;
|
|
11322
11338
|
EditorType pasteEditorType = 27;
|
|
11339
|
+
// Field 38 (timestamp) exists but kiwi-schema limits field IDs to 28
|
|
11340
|
+
// Discovered via WS sniffing 2026-01, skipped during manual pre-processing
|
|
11323
11341
|
}
|
|
11324
11342
|
|
|
11325
11343
|
message DiffChunk {
|
|
@@ -11540,8 +11558,11 @@ var init_schema = __esm(() => {
|
|
|
11540
11558
|
var exports_codec = {};
|
|
11541
11559
|
__export(exports_codec, {
|
|
11542
11560
|
peekMessageType: () => peekMessageType,
|
|
11561
|
+
parseVariableId: () => parseVariableId,
|
|
11543
11562
|
isCodecReady: () => isCodecReady,
|
|
11544
11563
|
initCodec: () => initCodec,
|
|
11564
|
+
encodePaintWithVariableBinding: () => encodePaintWithVariableBinding,
|
|
11565
|
+
encodeNodeChangeWithVariables: () => encodeNodeChangeWithVariables,
|
|
11545
11566
|
encodeMessage: () => encodeMessage,
|
|
11546
11567
|
decompress: () => decompress,
|
|
11547
11568
|
decodeMessage: () => decodeMessage,
|
|
@@ -11569,8 +11590,36 @@ function encodeMessage(message) {
|
|
|
11569
11590
|
if (!compiledSchema) {
|
|
11570
11591
|
throw new Error("Codec not initialized. Call initCodec() first.");
|
|
11571
11592
|
}
|
|
11572
|
-
const
|
|
11573
|
-
|
|
11593
|
+
const hasVariables = message.nodeChanges?.some((nc) => nc.fillPaints?.some((p4) => p4.colorVariableBinding) || nc.strokePaints?.some((p4) => p4.colorVariableBinding));
|
|
11594
|
+
if (!hasVariables) {
|
|
11595
|
+
const encoded = compiledSchema.encodeMessage(message);
|
|
11596
|
+
return compress(encoded);
|
|
11597
|
+
}
|
|
11598
|
+
const messageWithoutNodes = { ...message, nodeChanges: [] };
|
|
11599
|
+
const baseEncoded = compiledSchema.encodeMessage(messageWithoutNodes);
|
|
11600
|
+
const baseHex = Buffer.from(baseEncoded).toString("hex");
|
|
11601
|
+
const nodeChangeBytes = [];
|
|
11602
|
+
for (const nc of message.nodeChanges || []) {
|
|
11603
|
+
const encoded = encodeNodeChangeWithVariables(nc);
|
|
11604
|
+
nodeChangeBytes.push(encoded);
|
|
11605
|
+
}
|
|
11606
|
+
const emptyArrayPattern = "0400";
|
|
11607
|
+
const emptyArrayIdx = baseHex.indexOf(emptyArrayPattern);
|
|
11608
|
+
if (emptyArrayIdx === -1) {
|
|
11609
|
+
const encoded = compiledSchema.encodeMessage(message);
|
|
11610
|
+
return compress(encoded);
|
|
11611
|
+
}
|
|
11612
|
+
const ncBytes = [4];
|
|
11613
|
+
ncBytes.push(...encodeVarint(nodeChangeBytes.length));
|
|
11614
|
+
for (const ncArr of nodeChangeBytes) {
|
|
11615
|
+
ncBytes.push(...Array.from(ncArr));
|
|
11616
|
+
}
|
|
11617
|
+
const beforeArray = baseHex.slice(0, emptyArrayIdx);
|
|
11618
|
+
const afterArray = baseHex.slice(emptyArrayIdx + 4);
|
|
11619
|
+
const ncHex = Buffer.from(ncBytes).toString("hex");
|
|
11620
|
+
const finalHex = beforeArray + ncHex + afterArray;
|
|
11621
|
+
const finalBytes = new Uint8Array(finalHex.match(/.{2}/g).map((b2) => parseInt(b2, 16)));
|
|
11622
|
+
return compress(finalBytes);
|
|
11574
11623
|
}
|
|
11575
11624
|
function decodeMessage(data) {
|
|
11576
11625
|
if (!compiledSchema) {
|
|
@@ -11644,6 +11693,114 @@ function createNodeChange(opts) {
|
|
|
11644
11693
|
}
|
|
11645
11694
|
return change;
|
|
11646
11695
|
}
|
|
11696
|
+
function encodeVarint(value) {
|
|
11697
|
+
const bytes = [];
|
|
11698
|
+
while (value > 127) {
|
|
11699
|
+
bytes.push(value & 127 | 128);
|
|
11700
|
+
value >>>= 7;
|
|
11701
|
+
}
|
|
11702
|
+
bytes.push(value);
|
|
11703
|
+
return bytes;
|
|
11704
|
+
}
|
|
11705
|
+
function encodePaintWithVariableBinding(paint, variableSessionID, variableLocalID) {
|
|
11706
|
+
if (!compiledSchema) {
|
|
11707
|
+
throw new Error("Codec not initialized. Call initCodec() first.");
|
|
11708
|
+
}
|
|
11709
|
+
const basePaint = { ...paint };
|
|
11710
|
+
delete basePaint.colorVariableBinding;
|
|
11711
|
+
const baseBytes = compiledSchema.encodePaint(basePaint);
|
|
11712
|
+
const baseArray = Array.from(baseBytes);
|
|
11713
|
+
if (baseArray[baseArray.length - 1] === 0) {
|
|
11714
|
+
baseArray.pop();
|
|
11715
|
+
}
|
|
11716
|
+
baseArray.push(21, 1);
|
|
11717
|
+
baseArray.push(4, 1);
|
|
11718
|
+
baseArray.push(...encodeVarint(variableSessionID));
|
|
11719
|
+
baseArray.push(...encodeVarint(variableLocalID));
|
|
11720
|
+
baseArray.push(0, 0, 2, 3, 3, 4);
|
|
11721
|
+
baseArray.push(0, 0);
|
|
11722
|
+
return new Uint8Array(baseArray);
|
|
11723
|
+
}
|
|
11724
|
+
function parseVariableId(variableId) {
|
|
11725
|
+
const match = variableId.match(/VariableID:(\d+):(\d+)/);
|
|
11726
|
+
if (!match)
|
|
11727
|
+
return null;
|
|
11728
|
+
return {
|
|
11729
|
+
sessionID: parseInt(match[1], 10),
|
|
11730
|
+
localID: parseInt(match[2], 10)
|
|
11731
|
+
};
|
|
11732
|
+
}
|
|
11733
|
+
function encodeNodeChangeWithVariables(nodeChange) {
|
|
11734
|
+
if (!compiledSchema) {
|
|
11735
|
+
throw new Error("Codec not initialized. Call initCodec() first.");
|
|
11736
|
+
}
|
|
11737
|
+
const hasFillBinding = nodeChange.fillPaints?.some((p4) => p4.colorVariableBinding);
|
|
11738
|
+
const hasStrokeBinding = nodeChange.strokePaints?.some((p4) => p4.colorVariableBinding);
|
|
11739
|
+
if (!hasFillBinding && !hasStrokeBinding) {
|
|
11740
|
+
return compiledSchema.encodeNodeChange(nodeChange);
|
|
11741
|
+
}
|
|
11742
|
+
const cleanNodeChange = { ...nodeChange };
|
|
11743
|
+
if (cleanNodeChange.fillPaints) {
|
|
11744
|
+
cleanNodeChange.fillPaints = cleanNodeChange.fillPaints.map((p4) => {
|
|
11745
|
+
const clean2 = { ...p4 };
|
|
11746
|
+
delete clean2.colorVariableBinding;
|
|
11747
|
+
return clean2;
|
|
11748
|
+
});
|
|
11749
|
+
}
|
|
11750
|
+
if (cleanNodeChange.strokePaints) {
|
|
11751
|
+
cleanNodeChange.strokePaints = cleanNodeChange.strokePaints.map((p4) => {
|
|
11752
|
+
const clean2 = { ...p4 };
|
|
11753
|
+
delete clean2.colorVariableBinding;
|
|
11754
|
+
return clean2;
|
|
11755
|
+
});
|
|
11756
|
+
}
|
|
11757
|
+
const baseBytes = compiledSchema.encodeNodeChange(cleanNodeChange);
|
|
11758
|
+
let hex2 = Buffer.from(baseBytes).toString("hex");
|
|
11759
|
+
if (hasFillBinding && nodeChange.fillPaints?.[0]?.colorVariableBinding) {
|
|
11760
|
+
hex2 = injectVariableBinding(hex2, "2601", nodeChange.fillPaints[0].colorVariableBinding);
|
|
11761
|
+
}
|
|
11762
|
+
if (hasStrokeBinding && nodeChange.strokePaints?.[0]?.colorVariableBinding) {
|
|
11763
|
+
hex2 = injectVariableBinding(hex2, "2701", nodeChange.strokePaints[0].colorVariableBinding);
|
|
11764
|
+
}
|
|
11765
|
+
return new Uint8Array(hex2.match(/.{2}/g).map((b2) => parseInt(b2, 16)));
|
|
11766
|
+
}
|
|
11767
|
+
function injectVariableBinding(hex2, marker, binding) {
|
|
11768
|
+
const markerIdx = hex2.indexOf(marker);
|
|
11769
|
+
if (markerIdx === -1)
|
|
11770
|
+
return hex2;
|
|
11771
|
+
const visiblePattern = "0401";
|
|
11772
|
+
let patternIdx = hex2.indexOf(visiblePattern, markerIdx);
|
|
11773
|
+
if (patternIdx === -1)
|
|
11774
|
+
return hex2;
|
|
11775
|
+
let insertPoint = patternIdx + visiblePattern.length;
|
|
11776
|
+
if (hex2.slice(insertPoint, insertPoint + 4) === "0501") {
|
|
11777
|
+
insertPoint += 4;
|
|
11778
|
+
}
|
|
11779
|
+
const varBytes = [
|
|
11780
|
+
21,
|
|
11781
|
+
1,
|
|
11782
|
+
4,
|
|
11783
|
+
1,
|
|
11784
|
+
...encodeVarint(binding.variableID.sessionID),
|
|
11785
|
+
...encodeVarint(binding.variableID.localID),
|
|
11786
|
+
0,
|
|
11787
|
+
0,
|
|
11788
|
+
2,
|
|
11789
|
+
3,
|
|
11790
|
+
3,
|
|
11791
|
+
4,
|
|
11792
|
+
0,
|
|
11793
|
+
0
|
|
11794
|
+
];
|
|
11795
|
+
const varHex = Buffer.from(varBytes).toString("hex");
|
|
11796
|
+
const beforeVar = hex2.slice(0, insertPoint);
|
|
11797
|
+
let afterIdx = insertPoint;
|
|
11798
|
+
if (hex2.slice(afterIdx, afterIdx + 2) === "00") {
|
|
11799
|
+
afterIdx += 2;
|
|
11800
|
+
}
|
|
11801
|
+
const afterVar = hex2.slice(afterIdx);
|
|
11802
|
+
return beforeVar + varHex + afterVar;
|
|
11803
|
+
}
|
|
11647
11804
|
var compiledSchema = null;
|
|
11648
11805
|
var init_codec = __esm(() => {
|
|
11649
11806
|
init_kiwi_esm();
|
|
@@ -26796,6 +26953,20 @@ new Elysia().ws("/plugin", {
|
|
|
26796
26953
|
const { client, sessionID } = await getMultiplayerConnection(fileKey);
|
|
26797
26954
|
consola.info(`render: ${nodeChanges.length} nodes to ${fileKey}`);
|
|
26798
26955
|
await client.sendNodeChangesSync(nodeChanges);
|
|
26956
|
+
if (sendToPlugin) {
|
|
26957
|
+
const rootId = `${nodeChanges[0].guid.sessionID}:${nodeChanges[0].guid.localID}`;
|
|
26958
|
+
const layoutId = crypto.randomUUID();
|
|
26959
|
+
try {
|
|
26960
|
+
await new Promise((resolve, reject) => {
|
|
26961
|
+
const timeout = setTimeout(() => {
|
|
26962
|
+
pendingRequests.delete(layoutId);
|
|
26963
|
+
reject(new Error("Layout trigger timeout"));
|
|
26964
|
+
}, 5000);
|
|
26965
|
+
pendingRequests.set(layoutId, { resolve: () => resolve(), reject, timeout });
|
|
26966
|
+
sendToPlugin(JSON.stringify({ id: layoutId, command: "trigger-layout", args: { nodeId: rootId } }));
|
|
26967
|
+
});
|
|
26968
|
+
} catch {}
|
|
26969
|
+
}
|
|
26799
26970
|
const ids = nodeChanges.map((nc) => ({
|
|
26800
26971
|
id: `${nc.guid.sessionID}:${nc.guid.localID}`,
|
|
26801
26972
|
name: nc.name
|
package/package.json
CHANGED
|
@@ -1184,10 +1184,34 @@
|
|
|
1184
1184
|
const fn = new AsyncFunction("figma", wrappedCode);
|
|
1185
1185
|
return yield fn(figma);
|
|
1186
1186
|
}
|
|
1187
|
+
// ==================== LAYOUT ====================
|
|
1188
|
+
case "trigger-layout": {
|
|
1189
|
+
const { nodeId } = args;
|
|
1190
|
+
const root = yield figma.getNodeByIdAsync(nodeId);
|
|
1191
|
+
if (!root) return null;
|
|
1192
|
+
const triggerRecursive = (node) => {
|
|
1193
|
+
if ("layoutMode" in node && node.layoutMode !== "NONE" && "resize" in node) {
|
|
1194
|
+
const w = node.width;
|
|
1195
|
+
const h = node.height;
|
|
1196
|
+
node.resize(w + 0.01, h + 0.01);
|
|
1197
|
+
node.resize(w, h);
|
|
1198
|
+
}
|
|
1199
|
+
if ("children" in node) {
|
|
1200
|
+
for (const child of node.children) {
|
|
1201
|
+
triggerRecursive(child);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
triggerRecursive(root);
|
|
1206
|
+
return { triggered: true };
|
|
1207
|
+
}
|
|
1187
1208
|
// ==================== VARIABLES ====================
|
|
1188
1209
|
case "get-variables": {
|
|
1189
|
-
const { type } = args;
|
|
1210
|
+
const { type, simple } = args;
|
|
1190
1211
|
const variables = yield figma.variables.getLocalVariablesAsync(type);
|
|
1212
|
+
if (simple) {
|
|
1213
|
+
return variables.map((v) => ({ id: v.id, name: v.name }));
|
|
1214
|
+
}
|
|
1191
1215
|
return variables.map((v) => serializeVariable(v));
|
|
1192
1216
|
}
|
|
1193
1217
|
case "get-variable": {
|
|
@@ -1234,6 +1258,32 @@
|
|
|
1234
1258
|
}
|
|
1235
1259
|
return serializeNode(node);
|
|
1236
1260
|
}
|
|
1261
|
+
case "bind-fill-variable": {
|
|
1262
|
+
const { nodeId, variableId, paintIndex = 0 } = args;
|
|
1263
|
+
const node = yield figma.getNodeByIdAsync(nodeId);
|
|
1264
|
+
if (!node) throw new Error("Node not found");
|
|
1265
|
+
if (!("fills" in node)) throw new Error("Node does not have fills");
|
|
1266
|
+
const variable = yield figma.variables.getVariableByIdAsync(variableId);
|
|
1267
|
+
if (!variable) throw new Error("Variable not found");
|
|
1268
|
+
const fills = node.fills;
|
|
1269
|
+
if (!fills[paintIndex]) throw new Error("Paint not found at index " + paintIndex);
|
|
1270
|
+
const newFill = figma.variables.setBoundVariableForPaint(fills[paintIndex], "color", variable);
|
|
1271
|
+
node.fills = [...fills.slice(0, paintIndex), newFill, ...fills.slice(paintIndex + 1)];
|
|
1272
|
+
return serializeNode(node);
|
|
1273
|
+
}
|
|
1274
|
+
case "bind-stroke-variable": {
|
|
1275
|
+
const { nodeId, variableId, paintIndex = 0 } = args;
|
|
1276
|
+
const node = yield figma.getNodeByIdAsync(nodeId);
|
|
1277
|
+
if (!node) throw new Error("Node not found");
|
|
1278
|
+
if (!("strokes" in node)) throw new Error("Node does not have strokes");
|
|
1279
|
+
const variable = yield figma.variables.getVariableByIdAsync(variableId);
|
|
1280
|
+
if (!variable) throw new Error("Variable not found");
|
|
1281
|
+
const strokes = node.strokes;
|
|
1282
|
+
if (!strokes[paintIndex]) throw new Error("Paint not found at index " + paintIndex);
|
|
1283
|
+
const newStroke = figma.variables.setBoundVariableForPaint(strokes[paintIndex], "color", variable);
|
|
1284
|
+
node.strokes = [...strokes.slice(0, paintIndex), newStroke, ...strokes.slice(paintIndex + 1)];
|
|
1285
|
+
return serializeNode(node);
|
|
1286
|
+
}
|
|
1237
1287
|
// ==================== VARIABLE COLLECTIONS ====================
|
|
1238
1288
|
case "get-variable-collections": {
|
|
1239
1289
|
const collections = yield figma.variables.getLocalVariableCollectionsAsync();
|