@dannote/figma-use 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -1
- package/README.md +34 -3
- package/SKILL.md +72 -0
- package/bin/figma-use.js +1 -4
- package/dist/cli/index.js +22348 -20440
- package/dist/proxy/index.js +25 -25
- package/package.json +7 -3
- package/packages/cli/src/render/component-set.tsx +30 -21
- package/packages/cli/src/render/components.tsx +40 -16
- package/packages/cli/src/render/index.ts +14 -14
- package/packages/cli/src/render/reconciler.ts +222 -190
- package/packages/cli/src/render/vars.ts +23 -22
- package/packages/plugin/dist/main.js +168 -55
- package/packages/plugin/dist/ui.html +23 -18
- package/packages/plugin/dist/ui.js +44 -43
package/dist/proxy/index.js
CHANGED
|
@@ -180370,23 +180370,27 @@ function createNodeChange(opts) {
|
|
|
180370
180370
|
};
|
|
180371
180371
|
if (opts.fill) {
|
|
180372
180372
|
const color = typeof opts.fill === "string" ? parseColor(opts.fill) : opts.fill;
|
|
180373
|
-
change.fillPaints = [
|
|
180374
|
-
|
|
180375
|
-
|
|
180376
|
-
|
|
180377
|
-
|
|
180378
|
-
|
|
180379
|
-
|
|
180373
|
+
change.fillPaints = [
|
|
180374
|
+
{
|
|
180375
|
+
type: "SOLID",
|
|
180376
|
+
color,
|
|
180377
|
+
opacity: 1,
|
|
180378
|
+
visible: true,
|
|
180379
|
+
blendMode: "NORMAL"
|
|
180380
|
+
}
|
|
180381
|
+
];
|
|
180380
180382
|
}
|
|
180381
180383
|
if (opts.stroke) {
|
|
180382
180384
|
const color = typeof opts.stroke === "string" ? parseColor(opts.stroke) : opts.stroke;
|
|
180383
|
-
change.strokePaints = [
|
|
180384
|
-
|
|
180385
|
-
|
|
180386
|
-
|
|
180387
|
-
|
|
180388
|
-
|
|
180389
|
-
|
|
180385
|
+
change.strokePaints = [
|
|
180386
|
+
{
|
|
180387
|
+
type: "SOLID",
|
|
180388
|
+
color,
|
|
180389
|
+
opacity: 1,
|
|
180390
|
+
visible: true,
|
|
180391
|
+
blendMode: "NORMAL"
|
|
180392
|
+
}
|
|
180393
|
+
];
|
|
180390
180394
|
change.strokeWeight = opts.strokeWeight ?? 1;
|
|
180391
180395
|
}
|
|
180392
180396
|
if (opts.cornerRadius !== undefined) {
|
|
@@ -209229,25 +209233,21 @@ var PORT = Number(process.env.PORT) || 38451;
|
|
|
209229
209233
|
var MCP_VERSION = "2024-11-05";
|
|
209230
209234
|
var TIMEOUT_LIGHT = 1e4;
|
|
209231
209235
|
var TIMEOUT_HEAVY = 120000;
|
|
209232
|
-
var HEAVY_COMMANDS = new Set([
|
|
209233
|
-
"export-node",
|
|
209234
|
-
"screenshot",
|
|
209235
|
-
"export-selection",
|
|
209236
|
-
"eval"
|
|
209237
|
-
]);
|
|
209236
|
+
var HEAVY_COMMANDS = new Set(["export-node", "screenshot", "export-selection", "eval"]);
|
|
209238
209237
|
var pendingRequests = new Map;
|
|
209239
209238
|
var sendToPlugin = null;
|
|
209240
|
-
async function executeCommand(command, args) {
|
|
209239
|
+
async function executeCommand(command, args, timeoutMs) {
|
|
209241
209240
|
if (!sendToPlugin) {
|
|
209242
209241
|
throw new Error("Plugin not connected");
|
|
209243
209242
|
}
|
|
209244
209243
|
const id = crypto.randomUUID();
|
|
209245
209244
|
const defaultTimeout = HEAVY_COMMANDS.has(command) ? TIMEOUT_HEAVY : TIMEOUT_LIGHT;
|
|
209245
|
+
const timeoutDuration = timeoutMs ?? defaultTimeout;
|
|
209246
209246
|
const result = await new Promise((resolve, reject) => {
|
|
209247
209247
|
const timeout = setTimeout(() => {
|
|
209248
209248
|
pendingRequests.delete(id);
|
|
209249
209249
|
reject(new Error("Request timeout"));
|
|
209250
|
-
},
|
|
209250
|
+
}, timeoutDuration);
|
|
209251
209251
|
pendingRequests.set(id, { resolve, reject, timeout });
|
|
209252
209252
|
sendToPlugin(JSON.stringify({ id, command, args }));
|
|
209253
209253
|
});
|
|
@@ -209379,10 +209379,10 @@ new Elysia().ws("/plugin", {
|
|
|
209379
209379
|
}
|
|
209380
209380
|
}
|
|
209381
209381
|
}).post("/command", async ({ body }) => {
|
|
209382
|
-
const { command, args } = body;
|
|
209382
|
+
const { command, args, timeout } = body;
|
|
209383
209383
|
consola.info(`${command}`, args || "");
|
|
209384
209384
|
try {
|
|
209385
|
-
const result = await executeCommand(command, args);
|
|
209385
|
+
const result = await executeCommand(command, args, timeout);
|
|
209386
209386
|
return { result };
|
|
209387
209387
|
} catch (e5) {
|
|
209388
209388
|
consola.error(`${command} failed:`, e5 instanceof Error ? e5.message : e5);
|
|
@@ -209430,7 +209430,7 @@ new Elysia().ws("/plugin", {
|
|
|
209430
209430
|
const { client, sessionID } = await getMultiplayerConnection(fileKey);
|
|
209431
209431
|
consola.info(`render: ${nodeChanges.length} nodes to ${fileKey}`);
|
|
209432
209432
|
try {
|
|
209433
|
-
await client.sendNodeChangesSync(nodeChanges);
|
|
209433
|
+
await client.sendNodeChangesSync(nodeChanges, 15000);
|
|
209434
209434
|
} catch (codecError) {
|
|
209435
209435
|
const msg = codecError instanceof Error ? codecError.message : String(codecError);
|
|
209436
209436
|
if (msg.includes("Invalid value") && msg.includes("for enum")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dannote/figma-use",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Control Figma from the command line. Full read/write access for AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"build:plugin": "cd packages/plugin && bun run build",
|
|
19
19
|
"dev": "bun run packages/proxy/src/index.ts",
|
|
20
20
|
"test": "cd packages/cli && bun test",
|
|
21
|
+
"lint": "oxlint packages/",
|
|
22
|
+
"format": "oxfmt --write packages/",
|
|
21
23
|
"prepublishOnly": "bun run build"
|
|
22
24
|
},
|
|
23
25
|
"files": [
|
|
@@ -51,12 +53,14 @@
|
|
|
51
53
|
"dependencies": {
|
|
52
54
|
"citty": "^0.1.6",
|
|
53
55
|
"consola": "^3.4.2",
|
|
54
|
-
"elysia": "^1.2.25"
|
|
55
|
-
"kiwi-schema": "^0.5.0"
|
|
56
|
+
"elysia": "^1.2.25"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@types/bun": "^1.3.6",
|
|
59
60
|
"esbuild": "^0.25.4",
|
|
61
|
+
"kiwi-schema": "^0.5.0",
|
|
62
|
+
"oxfmt": "^0.24.0",
|
|
63
|
+
"oxlint": "^1.39.0",
|
|
60
64
|
"react": "19",
|
|
61
65
|
"typescript": "^5.8.3"
|
|
62
66
|
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ComponentSet (variants) support
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* ## How ComponentSet works in Figma's multiplayer protocol
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* ComponentSet is NOT a separate node type - it's a FRAME with special fields:
|
|
7
7
|
* - type = FRAME (4)
|
|
8
8
|
* - isStateGroup = true (field 225)
|
|
9
9
|
* - stateGroupPropertyValueOrders (field 238) = [{property: "variant", values: ["A", "B"]}]
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
11
|
* Children are SYMBOL (Component) nodes with names like "variant=Primary, size=Large".
|
|
12
12
|
* Figma auto-generates componentPropertyDefinitions from these names.
|
|
13
|
-
*
|
|
13
|
+
*
|
|
14
14
|
* ## Why Instances are created via Plugin API
|
|
15
|
-
*
|
|
15
|
+
*
|
|
16
16
|
* IMPORTANT: Instance nodes with symbolData.symbolID CANNOT be created via multiplayer
|
|
17
17
|
* when linking to components in the same batch. Figma reassigns GUIDs on receive,
|
|
18
18
|
* breaking the symbolID references.
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
20
|
* Example: We send SYMBOL with localID=100, Instance with symbolData.symbolID=100.
|
|
21
21
|
* Figma receives and assigns new IDs: SYMBOL becomes 200, but Instance still
|
|
22
22
|
* references 100 → broken link.
|
|
23
|
-
*
|
|
23
|
+
*
|
|
24
24
|
* This works for defineComponent() because Component and first Instance are adjacent
|
|
25
25
|
* in the node tree, but fails for ComponentSet where variant components are created
|
|
26
26
|
* first, then instances are created as siblings of the ComponentSet.
|
|
27
|
-
*
|
|
27
|
+
*
|
|
28
28
|
* Solution: Create the ComponentSet and variant Components via multiplayer (fast),
|
|
29
29
|
* then create Instances via Plugin API in trigger-layout (correct linking).
|
|
30
30
|
* The pendingComponentSetInstances array passes instance specs to the plugin.
|
|
31
|
-
*
|
|
31
|
+
*
|
|
32
32
|
* Discovered through protocol sniffing - see scripts/sniff-ws.ts
|
|
33
33
|
*/
|
|
34
34
|
|
|
@@ -56,7 +56,7 @@ export function getComponentSetRegistry() {
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Define a component with variants (ComponentSet)
|
|
59
|
-
*
|
|
59
|
+
*
|
|
60
60
|
* @example
|
|
61
61
|
* const Button = defineComponentSet('Button', {
|
|
62
62
|
* variant: ['Primary', 'Secondary'] as const,
|
|
@@ -66,7 +66,7 @@ export function getComponentSetRegistry() {
|
|
|
66
66
|
* <Text>{variant}</Text>
|
|
67
67
|
* </Frame>
|
|
68
68
|
* ))
|
|
69
|
-
*
|
|
69
|
+
*
|
|
70
70
|
* <Button variant="Primary" size="Large" />
|
|
71
71
|
*/
|
|
72
72
|
export function defineComponentSet<V extends VariantDef>(
|
|
@@ -75,19 +75,26 @@ export function defineComponentSet<V extends VariantDef>(
|
|
|
75
75
|
render: (props: VariantProps<V>) => React.ReactElement
|
|
76
76
|
): React.FC<Partial<VariantProps<V>> & { style?: Record<string, unknown> }> {
|
|
77
77
|
const sym = Symbol(name)
|
|
78
|
-
componentSetRegistry.set(sym, {
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
componentSetRegistry.set(sym, {
|
|
79
|
+
name,
|
|
80
|
+
variants,
|
|
81
|
+
render,
|
|
82
|
+
symbol: sym
|
|
83
|
+
} as ComponentSetDef<VariantDef>)
|
|
84
|
+
|
|
85
|
+
const VariantInstance: React.FC<
|
|
86
|
+
Partial<VariantProps<V>> & { style?: Record<string, unknown> }
|
|
87
|
+
> = (props) => {
|
|
81
88
|
const { style, ...variantProps } = props
|
|
82
89
|
return React.createElement('__component_set_instance__', {
|
|
83
90
|
__componentSetSymbol: sym,
|
|
84
91
|
__componentSetName: name,
|
|
85
92
|
__variantProps: variantProps,
|
|
86
|
-
style
|
|
93
|
+
style
|
|
87
94
|
})
|
|
88
95
|
}
|
|
89
96
|
VariantInstance.displayName = name
|
|
90
|
-
|
|
97
|
+
|
|
91
98
|
return VariantInstance
|
|
92
99
|
}
|
|
93
100
|
|
|
@@ -99,21 +106,21 @@ export function generateVariantCombinations<V extends VariantDef>(
|
|
|
99
106
|
): Array<VariantProps<V>> {
|
|
100
107
|
const keys = Object.keys(variants) as (keyof V)[]
|
|
101
108
|
if (keys.length === 0) return [{}] as Array<VariantProps<V>>
|
|
102
|
-
|
|
109
|
+
|
|
103
110
|
const result: Array<VariantProps<V>> = []
|
|
104
|
-
|
|
111
|
+
|
|
105
112
|
function combine(index: number, current: Partial<VariantProps<V>>) {
|
|
106
113
|
if (index === keys.length) {
|
|
107
114
|
result.push(current as VariantProps<V>)
|
|
108
115
|
return
|
|
109
116
|
}
|
|
110
|
-
|
|
117
|
+
|
|
111
118
|
const key = keys[index]
|
|
112
119
|
for (const value of variants[key]) {
|
|
113
120
|
combine(index + 1, { ...current, [key]: value })
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
|
-
|
|
123
|
+
|
|
117
124
|
combine(0, {})
|
|
118
125
|
return result
|
|
119
126
|
}
|
|
@@ -130,7 +137,9 @@ export function buildVariantName(props: Record<string, string>): string {
|
|
|
130
137
|
/**
|
|
131
138
|
* Build stateGroupPropertyValueOrders for ComponentSet
|
|
132
139
|
*/
|
|
133
|
-
export function buildStateGroupPropertyValueOrders(
|
|
140
|
+
export function buildStateGroupPropertyValueOrders(
|
|
141
|
+
variants: VariantDef
|
|
142
|
+
): Array<{ property: string; values: string[] }> {
|
|
134
143
|
return Object.entries(variants).map(([property, values]) => ({
|
|
135
144
|
property,
|
|
136
145
|
values: [...values]
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Figma-like React components
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* API inspired by react-figma but outputs JSON instead of Figma nodes
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as React from 'react'
|
|
8
8
|
|
|
9
9
|
// Re-export defineVars for use in .figma.tsx files
|
|
10
|
-
export {
|
|
11
|
-
defineVars,
|
|
12
|
-
figmaVar,
|
|
13
|
-
isVariable,
|
|
10
|
+
export {
|
|
11
|
+
defineVars,
|
|
12
|
+
figmaVar,
|
|
13
|
+
isVariable,
|
|
14
14
|
loadVariablesIntoRegistry,
|
|
15
15
|
isRegistryLoaded,
|
|
16
|
-
type FigmaVariable
|
|
16
|
+
type FigmaVariable
|
|
17
17
|
} from './vars.ts'
|
|
18
18
|
|
|
19
19
|
// Component registry - tracks defined components and their instances
|
|
@@ -35,14 +35,14 @@ export function getComponentRegistry() {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Define a reusable Figma component
|
|
38
|
-
*
|
|
38
|
+
*
|
|
39
39
|
* @example
|
|
40
40
|
* const Button = defineComponent('Button',
|
|
41
41
|
* <Frame style={{ padding: 12, backgroundColor: "#3B82F6" }}>
|
|
42
42
|
* <Text style={{ color: "#FFF" }}>Click</Text>
|
|
43
43
|
* </Frame>
|
|
44
44
|
* )
|
|
45
|
-
*
|
|
45
|
+
*
|
|
46
46
|
* export default () => (
|
|
47
47
|
* <Frame>
|
|
48
48
|
* <Button />
|
|
@@ -56,17 +56,17 @@ export function defineComponent<P extends BaseProps = BaseProps>(
|
|
|
56
56
|
): React.FC<P> {
|
|
57
57
|
const sym = Symbol(name)
|
|
58
58
|
componentRegistry.set(sym, { name, element })
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
// Return a component that renders as a special marker
|
|
61
61
|
const ComponentInstance: React.FC<P> = (props) => {
|
|
62
62
|
return React.createElement('__component_instance__', {
|
|
63
63
|
__componentSymbol: sym,
|
|
64
64
|
__componentName: name,
|
|
65
|
-
...props
|
|
65
|
+
...props
|
|
66
66
|
})
|
|
67
67
|
}
|
|
68
68
|
ComponentInstance.displayName = name
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
return ComponentInstance
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -99,7 +99,18 @@ interface Style {
|
|
|
99
99
|
interface TextStyle extends Style {
|
|
100
100
|
fontSize?: number
|
|
101
101
|
fontFamily?: string
|
|
102
|
-
fontWeight?:
|
|
102
|
+
fontWeight?:
|
|
103
|
+
| 'normal'
|
|
104
|
+
| 'bold'
|
|
105
|
+
| '100'
|
|
106
|
+
| '200'
|
|
107
|
+
| '300'
|
|
108
|
+
| '400'
|
|
109
|
+
| '500'
|
|
110
|
+
| '600'
|
|
111
|
+
| '700'
|
|
112
|
+
| '800'
|
|
113
|
+
| '900'
|
|
103
114
|
fontStyle?: 'normal' | 'italic'
|
|
104
115
|
color?: string
|
|
105
116
|
textAlign?: 'left' | 'center' | 'right'
|
|
@@ -130,8 +141,10 @@ interface InstanceProps extends BaseProps {
|
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
// Component factory - creates intrinsic element wrapper
|
|
133
|
-
const c =
|
|
134
|
-
(
|
|
144
|
+
const c =
|
|
145
|
+
<T extends BaseProps>(type: string): React.FC<T> =>
|
|
146
|
+
(props) =>
|
|
147
|
+
React.createElement(type, props)
|
|
135
148
|
|
|
136
149
|
// Components
|
|
137
150
|
export const Frame = c<BaseProps>('frame')
|
|
@@ -150,6 +163,17 @@ export const View = Frame
|
|
|
150
163
|
|
|
151
164
|
// All component names for JSX transform
|
|
152
165
|
export const INTRINSIC_ELEMENTS = [
|
|
153
|
-
'Frame',
|
|
154
|
-
'
|
|
166
|
+
'Frame',
|
|
167
|
+
'Rectangle',
|
|
168
|
+
'Ellipse',
|
|
169
|
+
'Text',
|
|
170
|
+
'Line',
|
|
171
|
+
'Star',
|
|
172
|
+
'Polygon',
|
|
173
|
+
'Vector',
|
|
174
|
+
'Component',
|
|
175
|
+
'Instance',
|
|
176
|
+
'Group',
|
|
177
|
+
'Page',
|
|
178
|
+
'View'
|
|
155
179
|
] as const
|
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
* React → Figma Renderer
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export {
|
|
6
|
-
renderToNodeChanges,
|
|
5
|
+
export {
|
|
6
|
+
renderToNodeChanges,
|
|
7
7
|
resetRenderedComponents,
|
|
8
8
|
getPendingComponentSetInstances,
|
|
9
9
|
clearPendingComponentSetInstances,
|
|
10
|
-
type RenderOptions,
|
|
10
|
+
type RenderOptions,
|
|
11
11
|
type RenderResult,
|
|
12
|
-
type PendingComponentSetInstance
|
|
12
|
+
type PendingComponentSetInstance
|
|
13
13
|
} from './reconciler.ts'
|
|
14
|
-
export {
|
|
15
|
-
Frame,
|
|
16
|
-
Rectangle,
|
|
17
|
-
Ellipse,
|
|
18
|
-
Text,
|
|
19
|
-
Line,
|
|
20
|
-
Star,
|
|
21
|
-
Polygon,
|
|
14
|
+
export {
|
|
15
|
+
Frame,
|
|
16
|
+
Rectangle,
|
|
17
|
+
Ellipse,
|
|
18
|
+
Text,
|
|
19
|
+
Line,
|
|
20
|
+
Star,
|
|
21
|
+
Polygon,
|
|
22
22
|
Vector,
|
|
23
23
|
Component,
|
|
24
24
|
Instance,
|
|
@@ -36,12 +36,12 @@ export {
|
|
|
36
36
|
// Component definitions
|
|
37
37
|
defineComponent,
|
|
38
38
|
resetComponentRegistry,
|
|
39
|
-
getComponentRegistry
|
|
39
|
+
getComponentRegistry
|
|
40
40
|
} from './components.tsx'
|
|
41
41
|
|
|
42
42
|
export {
|
|
43
43
|
// ComponentSet (variants)
|
|
44
44
|
defineComponentSet,
|
|
45
45
|
resetComponentSetRegistry,
|
|
46
|
-
getComponentSetRegistry
|
|
46
|
+
getComponentSetRegistry
|
|
47
47
|
} from './component-set.tsx'
|