figma-local 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.
- package/README.md +342 -0
- package/bin/fig-start +289 -0
- package/bin/setup-alias.sh +48 -0
- package/package.json +47 -0
- package/src/blocks/dashboard-01.js +379 -0
- package/src/blocks/index.js +27 -0
- package/src/daemon.js +664 -0
- package/src/figjam-client.js +313 -0
- package/src/figma-client.js +4198 -0
- package/src/figma-patch.js +185 -0
- package/src/index.js +8543 -0
- package/src/platform.js +206 -0
- package/src/prompt-templates.js +289 -0
- package/src/read.js +243 -0
- package/src/shadcn.js +237 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FigJam CDP Client
|
|
3
|
+
*
|
|
4
|
+
* Connects directly to FigJam via Chrome DevTools Protocol,
|
|
5
|
+
* bypassing figma-use which has compatibility issues with FigJam.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import WebSocket from 'ws';
|
|
9
|
+
import { getCdpPort } from './figma-patch.js';
|
|
10
|
+
|
|
11
|
+
export class FigJamClient {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.ws = null;
|
|
14
|
+
this.contexts = [];
|
|
15
|
+
this.figmaContextId = null;
|
|
16
|
+
this.msgId = 0;
|
|
17
|
+
this.callbacks = new Map();
|
|
18
|
+
this.pageTitle = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* List all available FigJam pages
|
|
23
|
+
*/
|
|
24
|
+
static async listPages() {
|
|
25
|
+
const port = getCdpPort();
|
|
26
|
+
const response = await fetch(`http://localhost:${port}/json`);
|
|
27
|
+
const pages = await response.json();
|
|
28
|
+
return pages
|
|
29
|
+
.filter(p => p.title.includes('FigJam'))
|
|
30
|
+
.map(p => ({ title: p.title, id: p.id, url: p.url }));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Connect to a FigJam page by title (partial match)
|
|
35
|
+
*/
|
|
36
|
+
async connect(pageTitle) {
|
|
37
|
+
const port = getCdpPort();
|
|
38
|
+
const response = await fetch(`http://localhost:${port}/json`);
|
|
39
|
+
const pages = await response.json();
|
|
40
|
+
const page = pages.find(p => p.title.includes(pageTitle) && p.title.includes('FigJam'));
|
|
41
|
+
|
|
42
|
+
if (!page) {
|
|
43
|
+
const figjamPages = pages.filter(p => p.title.includes('FigJam'));
|
|
44
|
+
if (figjamPages.length > 0) {
|
|
45
|
+
throw new Error(`Page "${pageTitle}" not found. Available FigJam pages: ${figjamPages.map(p => p.title).join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
throw new Error('No FigJam pages open. Please open a FigJam file in Figma Desktop.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.pageTitle = page.title;
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
this.ws = new WebSocket(page.webSocketDebuggerUrl);
|
|
54
|
+
|
|
55
|
+
this.ws.on('open', async () => {
|
|
56
|
+
await this.send('Runtime.enable');
|
|
57
|
+
|
|
58
|
+
// Wait for contexts to be discovered
|
|
59
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
60
|
+
|
|
61
|
+
// Find figma context
|
|
62
|
+
for (const ctx of this.contexts) {
|
|
63
|
+
try {
|
|
64
|
+
const result = await this.send('Runtime.evaluate', {
|
|
65
|
+
expression: 'typeof figma !== "undefined"',
|
|
66
|
+
contextId: ctx.id,
|
|
67
|
+
returnByValue: true
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (result.result?.result?.value === true) {
|
|
71
|
+
this.figmaContextId = ctx.id;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!this.figmaContextId) {
|
|
78
|
+
reject(new Error('Could not find figma context. Try refreshing the FigJam page.'));
|
|
79
|
+
} else {
|
|
80
|
+
resolve(this);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.ws.on('message', (data) => {
|
|
85
|
+
const msg = JSON.parse(data);
|
|
86
|
+
|
|
87
|
+
if (msg.method === 'Runtime.executionContextCreated') {
|
|
88
|
+
this.contexts.push(msg.params.context);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (msg.id && this.callbacks.has(msg.id)) {
|
|
92
|
+
this.callbacks.get(msg.id)(msg);
|
|
93
|
+
this.callbacks.delete(msg.id);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.ws.on('error', reject);
|
|
98
|
+
|
|
99
|
+
setTimeout(() => reject(new Error('Connection timeout')), 10000);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
send(method, params = {}) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const id = ++this.msgId;
|
|
106
|
+
this.callbacks.set(id, resolve);
|
|
107
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Evaluate JavaScript in the FigJam context
|
|
113
|
+
*/
|
|
114
|
+
async eval(expression) {
|
|
115
|
+
if (!this.figmaContextId) {
|
|
116
|
+
throw new Error('Not connected to FigJam');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = await this.send('Runtime.evaluate', {
|
|
120
|
+
expression,
|
|
121
|
+
contextId: this.figmaContextId,
|
|
122
|
+
returnByValue: true,
|
|
123
|
+
awaitPromise: true
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (result.result?.exceptionDetails) {
|
|
127
|
+
const error = result.result.exceptionDetails;
|
|
128
|
+
throw new Error(error.exception?.description || error.text || 'Evaluation error');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return result.result?.result?.value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get current page info
|
|
136
|
+
*/
|
|
137
|
+
async getPageInfo() {
|
|
138
|
+
return await this.eval(`
|
|
139
|
+
(function() {
|
|
140
|
+
return {
|
|
141
|
+
name: figma.currentPage.name,
|
|
142
|
+
id: figma.currentPage.id,
|
|
143
|
+
childCount: figma.currentPage.children.length,
|
|
144
|
+
editorType: figma.editorType
|
|
145
|
+
};
|
|
146
|
+
})()
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* List all nodes on the current page
|
|
152
|
+
*/
|
|
153
|
+
async listNodes(limit = 50) {
|
|
154
|
+
return await this.eval(`
|
|
155
|
+
figma.currentPage.children.slice(0, ${limit}).map(function(n) {
|
|
156
|
+
return {
|
|
157
|
+
id: n.id,
|
|
158
|
+
type: n.type,
|
|
159
|
+
name: n.name || '',
|
|
160
|
+
x: Math.round(n.x),
|
|
161
|
+
y: Math.round(n.y)
|
|
162
|
+
};
|
|
163
|
+
})
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create a sticky note
|
|
169
|
+
*/
|
|
170
|
+
async createSticky(text, x = 0, y = 0, color) {
|
|
171
|
+
return await this.eval(`
|
|
172
|
+
(async function() {
|
|
173
|
+
var sticky = figma.createSticky();
|
|
174
|
+
sticky.x = ${x};
|
|
175
|
+
sticky.y = ${y};
|
|
176
|
+
${color ? `sticky.fills = [{type: 'SOLID', color: ${JSON.stringify(hexToRgb(color))}}];` : ''}
|
|
177
|
+
// Load font before setting text
|
|
178
|
+
await figma.loadFontAsync({ family: "Inter", style: "Medium" });
|
|
179
|
+
sticky.text.characters = ${JSON.stringify(text)};
|
|
180
|
+
return { id: sticky.id, x: sticky.x, y: sticky.y };
|
|
181
|
+
})()
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Create a shape with text
|
|
187
|
+
*/
|
|
188
|
+
async createShape(text, x = 0, y = 0, width = 200, height = 100, shapeType = 'ROUNDED_RECTANGLE') {
|
|
189
|
+
return await this.eval(`
|
|
190
|
+
(async function() {
|
|
191
|
+
var shape = figma.createShapeWithText();
|
|
192
|
+
shape.shapeType = ${JSON.stringify(shapeType)};
|
|
193
|
+
shape.x = ${x};
|
|
194
|
+
shape.y = ${y};
|
|
195
|
+
shape.resize(${width}, ${height});
|
|
196
|
+
if (shape.text) {
|
|
197
|
+
await figma.loadFontAsync({ family: "Inter", style: "Medium" });
|
|
198
|
+
shape.text.characters = ${JSON.stringify(text)};
|
|
199
|
+
}
|
|
200
|
+
return { id: shape.id, x: shape.x, y: shape.y };
|
|
201
|
+
})()
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create a connector between two nodes
|
|
207
|
+
*/
|
|
208
|
+
async createConnector(startNodeId, endNodeId) {
|
|
209
|
+
return await this.eval(`
|
|
210
|
+
(function() {
|
|
211
|
+
var startNode = figma.getNodeById(${JSON.stringify(startNodeId)});
|
|
212
|
+
var endNode = figma.getNodeById(${JSON.stringify(endNodeId)});
|
|
213
|
+
if (!startNode || !endNode) return { error: 'Node not found' };
|
|
214
|
+
|
|
215
|
+
var connector = figma.createConnector();
|
|
216
|
+
connector.connectorStart = { endpointNodeId: startNode.id, magnet: 'AUTO' };
|
|
217
|
+
connector.connectorEnd = { endpointNodeId: endNode.id, magnet: 'AUTO' };
|
|
218
|
+
return { id: connector.id };
|
|
219
|
+
})()
|
|
220
|
+
`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create a text node
|
|
225
|
+
*/
|
|
226
|
+
async createText(text, x = 0, y = 0, fontSize = 16) {
|
|
227
|
+
return await this.eval(`
|
|
228
|
+
(async function() {
|
|
229
|
+
var textNode = figma.createText();
|
|
230
|
+
textNode.x = ${x};
|
|
231
|
+
textNode.y = ${y};
|
|
232
|
+
await figma.loadFontAsync({ family: "Inter", style: "Medium" });
|
|
233
|
+
textNode.characters = ${JSON.stringify(text)};
|
|
234
|
+
textNode.fontSize = ${fontSize};
|
|
235
|
+
return { id: textNode.id, x: textNode.x, y: textNode.y };
|
|
236
|
+
})()
|
|
237
|
+
`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Delete a node by ID
|
|
242
|
+
*/
|
|
243
|
+
async deleteNode(nodeId) {
|
|
244
|
+
return await this.eval(`
|
|
245
|
+
(function() {
|
|
246
|
+
var node = figma.getNodeById(${JSON.stringify(nodeId)});
|
|
247
|
+
if (node) {
|
|
248
|
+
node.remove();
|
|
249
|
+
return { deleted: true };
|
|
250
|
+
}
|
|
251
|
+
return { deleted: false, error: 'Node not found' };
|
|
252
|
+
})()
|
|
253
|
+
`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Move a node
|
|
258
|
+
*/
|
|
259
|
+
async moveNode(nodeId, x, y) {
|
|
260
|
+
return await this.eval(`
|
|
261
|
+
(function() {
|
|
262
|
+
var node = figma.getNodeById(${JSON.stringify(nodeId)});
|
|
263
|
+
if (node) {
|
|
264
|
+
node.x = ${x};
|
|
265
|
+
node.y = ${y};
|
|
266
|
+
return { id: node.id, x: node.x, y: node.y };
|
|
267
|
+
}
|
|
268
|
+
return { error: 'Node not found' };
|
|
269
|
+
})()
|
|
270
|
+
`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Update text content of a node
|
|
275
|
+
*/
|
|
276
|
+
async updateText(nodeId, text) {
|
|
277
|
+
return await this.eval(`
|
|
278
|
+
(async function() {
|
|
279
|
+
var node = figma.getNodeById(${JSON.stringify(nodeId)});
|
|
280
|
+
if (!node) return { error: 'Node not found' };
|
|
281
|
+
|
|
282
|
+
await figma.loadFontAsync({ family: "Inter", style: "Medium" });
|
|
283
|
+
|
|
284
|
+
if (node.type === 'STICKY' || node.type === 'SHAPE_WITH_TEXT') {
|
|
285
|
+
node.text.characters = ${JSON.stringify(text)};
|
|
286
|
+
} else if (node.type === 'TEXT') {
|
|
287
|
+
node.characters = ${JSON.stringify(text)};
|
|
288
|
+
} else {
|
|
289
|
+
return { error: 'Node does not support text' };
|
|
290
|
+
}
|
|
291
|
+
return { id: node.id, updated: true };
|
|
292
|
+
})()
|
|
293
|
+
`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
close() {
|
|
297
|
+
if (this.ws) {
|
|
298
|
+
this.ws.close();
|
|
299
|
+
this.ws = null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function hexToRgb(hex) {
|
|
305
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
306
|
+
return result ? {
|
|
307
|
+
r: parseInt(result[1], 16) / 255,
|
|
308
|
+
g: parseInt(result[2], 16) / 255,
|
|
309
|
+
b: parseInt(result[3], 16) / 255
|
|
310
|
+
} : { r: 1, g: 0.9, b: 0.5 }; // default yellow
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export default FigJamClient;
|