with-figma 0.1.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/bin/with-figma.js +72 -0
- package/figma-plugin/dist/.gitkeep +0 -0
- package/figma-plugin/dist/code.js +178 -0
- package/figma-plugin/manifest.json +13 -0
- package/figma-plugin/ui.html +322 -0
- package/mcp-server/dist/.gitkeep +0 -0
- package/mcp-server/dist/index.js +30381 -0
- package/package.json +32 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const command = process.argv[2];
|
|
6
|
+
|
|
7
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
8
|
+
|
|
9
|
+
// ─── init: configure MCP settings in current project ──────────────
|
|
10
|
+
if (command === "init") {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const serverBin = "with-figma";
|
|
13
|
+
|
|
14
|
+
// .vscode/mcp.json
|
|
15
|
+
const vscodeDir = path.join(cwd, ".vscode");
|
|
16
|
+
const mcpJsonPath = path.join(vscodeDir, "mcp.json");
|
|
17
|
+
if (!fs.existsSync(vscodeDir)) fs.mkdirSync(vscodeDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
let mcpJson = {};
|
|
20
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
21
|
+
try { mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8")); } catch {}
|
|
22
|
+
}
|
|
23
|
+
if (!mcpJson.servers) mcpJson.servers = {};
|
|
24
|
+
mcpJson.servers["with-figma"] = {
|
|
25
|
+
command: "npx",
|
|
26
|
+
args: ["-y", "with-figma", "serve"],
|
|
27
|
+
};
|
|
28
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
|
|
29
|
+
console.log("✓ .vscode/mcp.json updated");
|
|
30
|
+
|
|
31
|
+
// .claude/settings.json
|
|
32
|
+
const claudeDir = path.join(cwd, ".claude");
|
|
33
|
+
const claudePath = path.join(claudeDir, "settings.json");
|
|
34
|
+
if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
let claudeJson = {};
|
|
37
|
+
if (fs.existsSync(claudePath)) {
|
|
38
|
+
try { claudeJson = JSON.parse(fs.readFileSync(claudePath, "utf-8")); } catch {}
|
|
39
|
+
}
|
|
40
|
+
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
41
|
+
claudeJson.mcpServers["with-figma"] = {
|
|
42
|
+
command: "npx",
|
|
43
|
+
args: ["-y", "with-figma", "serve"],
|
|
44
|
+
};
|
|
45
|
+
fs.writeFileSync(claudePath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
46
|
+
console.log("✓ .claude/settings.json updated");
|
|
47
|
+
|
|
48
|
+
// Show Figma plugin path
|
|
49
|
+
const manifestPath = path.join(PKG_ROOT, "figma-plugin", "manifest.json");
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log("MCP server configured. Next steps:");
|
|
52
|
+
console.log("");
|
|
53
|
+
console.log("1. Import Figma plugin:");
|
|
54
|
+
console.log(" Figma → Plugins → Development → Import plugin from manifest");
|
|
55
|
+
console.log(" Path: " + manifestPath);
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log("2. Restart VS Code or Claude Code");
|
|
58
|
+
console.log("3. Open the plugin in Figma → select elements → chat with AI");
|
|
59
|
+
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── serve: start MCP server (default) ────────────────────────────
|
|
64
|
+
if (!command || command === "serve") {
|
|
65
|
+
require(path.join(PKG_ROOT, "mcp-server", "dist", "index.js"));
|
|
66
|
+
} else {
|
|
67
|
+
console.error(`Unknown command: ${command}`);
|
|
68
|
+
console.error("Usage:");
|
|
69
|
+
console.error(" npx with-figma init — configure MCP in current project");
|
|
70
|
+
console.error(" npx with-figma serve — start MCP server");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// figma-plugin/code.ts
|
|
4
|
+
figma.showUI(__html__, { width: 400, height: 600, themeColors: true });
|
|
5
|
+
function serializeNode(node) {
|
|
6
|
+
const base = {
|
|
7
|
+
id: node.id,
|
|
8
|
+
name: node.name,
|
|
9
|
+
type: node.type,
|
|
10
|
+
visible: node.visible
|
|
11
|
+
};
|
|
12
|
+
if ("x" in node) base.x = node.x;
|
|
13
|
+
if ("y" in node) base.y = node.y;
|
|
14
|
+
if ("width" in node) base.width = node.width;
|
|
15
|
+
if ("height" in node) base.height = node.height;
|
|
16
|
+
if ("fills" in node) base.fills = node.fills;
|
|
17
|
+
if ("strokes" in node) base.strokes = node.strokes;
|
|
18
|
+
if ("characters" in node) base.characters = node.characters;
|
|
19
|
+
if ("children" in node) {
|
|
20
|
+
base.childCount = node.children.length;
|
|
21
|
+
base.children = node.children.map((c) => ({
|
|
22
|
+
id: c.id,
|
|
23
|
+
name: c.name,
|
|
24
|
+
type: c.type
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
return base;
|
|
28
|
+
}
|
|
29
|
+
function sendSelection() {
|
|
30
|
+
const selection = figma.currentPage.selection;
|
|
31
|
+
const nodes = selection.map(serializeNode);
|
|
32
|
+
figma.ui.postMessage({
|
|
33
|
+
type: "selection-changed",
|
|
34
|
+
nodes,
|
|
35
|
+
page: {
|
|
36
|
+
id: figma.currentPage.id,
|
|
37
|
+
name: figma.currentPage.name
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
figma.on("selectionchange", sendSelection);
|
|
42
|
+
sendSelection();
|
|
43
|
+
function sendPages() {
|
|
44
|
+
const pages = figma.root.children.map((p) => ({
|
|
45
|
+
id: p.id,
|
|
46
|
+
name: p.name
|
|
47
|
+
}));
|
|
48
|
+
figma.ui.postMessage({ type: "pages-list", pages });
|
|
49
|
+
}
|
|
50
|
+
sendPages();
|
|
51
|
+
figma.ui.onmessage = async (msg) => {
|
|
52
|
+
switch (msg.type) {
|
|
53
|
+
case "get-selection":
|
|
54
|
+
sendSelection();
|
|
55
|
+
break;
|
|
56
|
+
case "get-pages":
|
|
57
|
+
sendPages();
|
|
58
|
+
break;
|
|
59
|
+
case "get-node": {
|
|
60
|
+
const nodeId = msg.nodeId;
|
|
61
|
+
const node = figma.getNodeById(nodeId);
|
|
62
|
+
if (node) {
|
|
63
|
+
figma.ui.postMessage({
|
|
64
|
+
type: "node-data",
|
|
65
|
+
requestId: msg.requestId,
|
|
66
|
+
node: serializeNode(node)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "create-frame": {
|
|
72
|
+
const frame = figma.createFrame();
|
|
73
|
+
frame.name = msg.name || "New Frame";
|
|
74
|
+
frame.resize(
|
|
75
|
+
msg.width || 375,
|
|
76
|
+
msg.height || 812
|
|
77
|
+
);
|
|
78
|
+
frame.x = msg.x || 0;
|
|
79
|
+
frame.y = msg.y || 0;
|
|
80
|
+
if (msg.fills) frame.fills = msg.fills;
|
|
81
|
+
figma.currentPage.appendChild(frame);
|
|
82
|
+
figma.ui.postMessage({
|
|
83
|
+
type: "node-created",
|
|
84
|
+
requestId: msg.requestId,
|
|
85
|
+
node: serializeNode(frame)
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "create-rectangle": {
|
|
90
|
+
const parent = msg.parentId ? figma.getNodeById(msg.parentId) : figma.currentPage;
|
|
91
|
+
const rect = figma.createRectangle();
|
|
92
|
+
rect.name = msg.name || "Rectangle";
|
|
93
|
+
rect.resize(msg.width || 100, msg.height || 100);
|
|
94
|
+
rect.x = msg.x || 0;
|
|
95
|
+
rect.y = msg.y || 0;
|
|
96
|
+
if (msg.fills) rect.fills = msg.fills;
|
|
97
|
+
if (msg.cornerRadius) rect.cornerRadius = msg.cornerRadius;
|
|
98
|
+
if (parent && "appendChild" in parent) parent.appendChild(rect);
|
|
99
|
+
figma.ui.postMessage({
|
|
100
|
+
type: "node-created",
|
|
101
|
+
requestId: msg.requestId,
|
|
102
|
+
node: serializeNode(rect)
|
|
103
|
+
});
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "create-text": {
|
|
107
|
+
const textParent = msg.parentId ? figma.getNodeById(msg.parentId) : figma.currentPage;
|
|
108
|
+
const text = figma.createText();
|
|
109
|
+
await figma.loadFontAsync({ family: "Inter", style: "Regular" });
|
|
110
|
+
text.name = msg.name || "Text";
|
|
111
|
+
text.characters = msg.characters || "Text";
|
|
112
|
+
text.fontSize = msg.fontSize || 16;
|
|
113
|
+
text.x = msg.x || 0;
|
|
114
|
+
text.y = msg.y || 0;
|
|
115
|
+
if (msg.fills) text.fills = msg.fills;
|
|
116
|
+
if (textParent && "appendChild" in textParent)
|
|
117
|
+
textParent.appendChild(text);
|
|
118
|
+
figma.ui.postMessage({
|
|
119
|
+
type: "node-created",
|
|
120
|
+
requestId: msg.requestId,
|
|
121
|
+
node: serializeNode(text)
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "modify-node": {
|
|
126
|
+
const target = figma.getNodeById(msg.nodeId);
|
|
127
|
+
if (!target) break;
|
|
128
|
+
const props = msg.properties;
|
|
129
|
+
for (const [key, value] of Object.entries(props)) {
|
|
130
|
+
if (key === "characters" && target.type === "TEXT") {
|
|
131
|
+
await figma.loadFontAsync({ family: "Inter", style: "Regular" });
|
|
132
|
+
target.characters = value;
|
|
133
|
+
} else if (key === "width" || key === "height") {
|
|
134
|
+
const w = key === "width" ? value : target.width;
|
|
135
|
+
const h = key === "height" ? value : target.height;
|
|
136
|
+
target.resize(w, h);
|
|
137
|
+
} else {
|
|
138
|
+
target[key] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
figma.ui.postMessage({
|
|
142
|
+
type: "node-modified",
|
|
143
|
+
requestId: msg.requestId,
|
|
144
|
+
node: serializeNode(target)
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "delete-node": {
|
|
149
|
+
const toDelete = figma.getNodeById(msg.nodeId);
|
|
150
|
+
if (toDelete) {
|
|
151
|
+
toDelete.remove();
|
|
152
|
+
figma.ui.postMessage({
|
|
153
|
+
type: "node-deleted",
|
|
154
|
+
requestId: msg.requestId,
|
|
155
|
+
nodeId: msg.nodeId
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case "export-node": {
|
|
161
|
+
const exportNode = figma.getNodeById(msg.nodeId);
|
|
162
|
+
if (exportNode) {
|
|
163
|
+
const bytes = await exportNode.exportAsync({
|
|
164
|
+
format: msg.format || "PNG",
|
|
165
|
+
scale: msg.scale || 2
|
|
166
|
+
});
|
|
167
|
+
figma.ui.postMessage({
|
|
168
|
+
type: "node-exported",
|
|
169
|
+
requestId: msg.requestId,
|
|
170
|
+
data: Array.from(bytes),
|
|
171
|
+
format: msg.format || "PNG"
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
})();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "With Figma AI Bridge",
|
|
3
|
+
"id": "with-figma-ai-bridge",
|
|
4
|
+
"api": "1.0.0",
|
|
5
|
+
"main": "dist/code.js",
|
|
6
|
+
"ui": "ui.html",
|
|
7
|
+
"editorType": ["figma"],
|
|
8
|
+
"networkAccess": {
|
|
9
|
+
"allowedDomains": ["http://localhost:3055", "ws://localhost:3055"],
|
|
10
|
+
"reasoning": "WebSocket connection to local MCP server"
|
|
11
|
+
},
|
|
12
|
+
"permissions": ["currentuser"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<style>
|
|
5
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
6
|
+
body {
|
|
7
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
8
|
+
font-size: 13px;
|
|
9
|
+
color: var(--figma-color-text, #333);
|
|
10
|
+
background: var(--figma-color-bg, #fff);
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
height: 100vh;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* ─── Header ─── */
|
|
17
|
+
.header {
|
|
18
|
+
padding: 12px 16px;
|
|
19
|
+
border-bottom: 1px solid var(--figma-color-border, #e5e5e5);
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 8px;
|
|
23
|
+
}
|
|
24
|
+
.status-dot {
|
|
25
|
+
width: 8px; height: 8px;
|
|
26
|
+
border-radius: 50%;
|
|
27
|
+
background: #ccc;
|
|
28
|
+
}
|
|
29
|
+
.status-dot.connected { background: #18a957; }
|
|
30
|
+
.status-dot.connecting { background: #f5a623; animation: pulse 1s infinite; }
|
|
31
|
+
@keyframes pulse { 50% { opacity: 0.5; } }
|
|
32
|
+
.header-title { font-weight: 600; flex: 1; }
|
|
33
|
+
|
|
34
|
+
/* ─── Selection Info ─── */
|
|
35
|
+
.selection-bar {
|
|
36
|
+
padding: 8px 16px;
|
|
37
|
+
background: var(--figma-color-bg-secondary, #f5f5f5);
|
|
38
|
+
border-bottom: 1px solid var(--figma-color-border, #e5e5e5);
|
|
39
|
+
font-size: 12px;
|
|
40
|
+
color: var(--figma-color-text-secondary, #666);
|
|
41
|
+
}
|
|
42
|
+
.selection-bar .node-name {
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
color: var(--figma-color-text, #333);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ─── Chat Area ─── */
|
|
48
|
+
.chat-area {
|
|
49
|
+
flex: 1;
|
|
50
|
+
overflow-y: auto;
|
|
51
|
+
padding: 16px;
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: 12px;
|
|
55
|
+
}
|
|
56
|
+
.message {
|
|
57
|
+
max-width: 85%;
|
|
58
|
+
padding: 10px 14px;
|
|
59
|
+
border-radius: 12px;
|
|
60
|
+
line-height: 1.5;
|
|
61
|
+
word-break: break-word;
|
|
62
|
+
}
|
|
63
|
+
.message.user {
|
|
64
|
+
align-self: flex-end;
|
|
65
|
+
background: #0d99ff;
|
|
66
|
+
color: #fff;
|
|
67
|
+
border-bottom-right-radius: 4px;
|
|
68
|
+
}
|
|
69
|
+
.message.assistant {
|
|
70
|
+
align-self: flex-start;
|
|
71
|
+
background: var(--figma-color-bg-secondary, #f0f0f0);
|
|
72
|
+
border-bottom-left-radius: 4px;
|
|
73
|
+
}
|
|
74
|
+
.message.system {
|
|
75
|
+
align-self: center;
|
|
76
|
+
font-size: 11px;
|
|
77
|
+
color: var(--figma-color-text-tertiary, #999);
|
|
78
|
+
background: none;
|
|
79
|
+
padding: 4px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ─── Input Area ─── */
|
|
83
|
+
.input-area {
|
|
84
|
+
padding: 12px 16px;
|
|
85
|
+
border-top: 1px solid var(--figma-color-border, #e5e5e5);
|
|
86
|
+
display: flex;
|
|
87
|
+
gap: 8px;
|
|
88
|
+
}
|
|
89
|
+
.input-area textarea {
|
|
90
|
+
flex: 1;
|
|
91
|
+
border: 1px solid var(--figma-color-border, #ddd);
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
padding: 8px 12px;
|
|
94
|
+
font-size: 13px;
|
|
95
|
+
font-family: inherit;
|
|
96
|
+
resize: none;
|
|
97
|
+
outline: none;
|
|
98
|
+
background: var(--figma-color-bg, #fff);
|
|
99
|
+
color: var(--figma-color-text, #333);
|
|
100
|
+
min-height: 40px;
|
|
101
|
+
max-height: 120px;
|
|
102
|
+
}
|
|
103
|
+
.input-area textarea:focus { border-color: #0d99ff; }
|
|
104
|
+
.input-area button {
|
|
105
|
+
background: #0d99ff;
|
|
106
|
+
color: #fff;
|
|
107
|
+
border: none;
|
|
108
|
+
border-radius: 8px;
|
|
109
|
+
padding: 8px 16px;
|
|
110
|
+
font-size: 13px;
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
align-self: flex-end;
|
|
114
|
+
}
|
|
115
|
+
.input-area button:hover { background: #0b85e0; }
|
|
116
|
+
.input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
117
|
+
</style>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<div class="header">
|
|
121
|
+
<div class="status-dot" id="statusDot"></div>
|
|
122
|
+
<span class="header-title">With Figma AI</span>
|
|
123
|
+
<span id="statusText" style="font-size:11px; color:#999">Disconnected</span>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="selection-bar" id="selectionBar">
|
|
127
|
+
No selection
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="chat-area" id="chatArea">
|
|
131
|
+
<div class="message system">Select an element and describe what you want to do.</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="input-area">
|
|
135
|
+
<textarea id="input" placeholder="이 요소를 어떻게 작업할까요?" rows="1"></textarea>
|
|
136
|
+
<button id="sendBtn" disabled>Send</button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<script>
|
|
140
|
+
const WS_URL = "ws://localhost:3055";
|
|
141
|
+
|
|
142
|
+
let ws = null;
|
|
143
|
+
let currentSelection = [];
|
|
144
|
+
let currentPage = null;
|
|
145
|
+
let reconnectTimer = null;
|
|
146
|
+
|
|
147
|
+
const chatArea = document.getElementById("chatArea");
|
|
148
|
+
const input = document.getElementById("input");
|
|
149
|
+
const sendBtn = document.getElementById("sendBtn");
|
|
150
|
+
const statusDot = document.getElementById("statusDot");
|
|
151
|
+
const statusText = document.getElementById("statusText");
|
|
152
|
+
const selectionBar = document.getElementById("selectionBar");
|
|
153
|
+
|
|
154
|
+
// ─── WebSocket ────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
function connect() {
|
|
157
|
+
if (ws && ws.readyState <= 1) return;
|
|
158
|
+
setStatus("connecting");
|
|
159
|
+
|
|
160
|
+
ws = new WebSocket(WS_URL);
|
|
161
|
+
|
|
162
|
+
ws.onopen = () => {
|
|
163
|
+
setStatus("connected");
|
|
164
|
+
addMessage("system", "Connected to AI server.");
|
|
165
|
+
// Send current selection state
|
|
166
|
+
if (currentSelection.length > 0) {
|
|
167
|
+
ws.send(JSON.stringify({
|
|
168
|
+
type: "selection-update",
|
|
169
|
+
nodes: currentSelection,
|
|
170
|
+
page: currentPage,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
ws.onmessage = (event) => {
|
|
176
|
+
try {
|
|
177
|
+
const msg = JSON.parse(event.data);
|
|
178
|
+
handleServerMessage(msg);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.error("Failed to parse message:", e);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
ws.onclose = () => {
|
|
185
|
+
setStatus("disconnected");
|
|
186
|
+
scheduleReconnect();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
ws.onerror = () => {
|
|
190
|
+
ws.close();
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function scheduleReconnect() {
|
|
195
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
196
|
+
reconnectTimer = setTimeout(connect, 3000);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function setStatus(state) {
|
|
200
|
+
statusDot.className = "status-dot " + (state === "connected" ? "connected" : state === "connecting" ? "connecting" : "");
|
|
201
|
+
statusText.textContent = state === "connected" ? "Connected" : state === "connecting" ? "Connecting..." : "Disconnected";
|
|
202
|
+
sendBtn.disabled = state !== "connected";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Server message handling ──────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function handleServerMessage(msg) {
|
|
208
|
+
switch (msg.type) {
|
|
209
|
+
case "chat-response":
|
|
210
|
+
addMessage("assistant", msg.text);
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case "figma-command":
|
|
214
|
+
// Relay command to Figma plugin sandbox
|
|
215
|
+
parent.postMessage({ pluginMessage: msg.command }, "*");
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case "request-selection":
|
|
219
|
+
parent.postMessage({ pluginMessage: { type: "get-selection" } }, "*");
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
case "status":
|
|
223
|
+
addMessage("system", msg.text);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── Messages from Figma plugin sandbox ───────────────────────
|
|
229
|
+
|
|
230
|
+
window.onmessage = (event) => {
|
|
231
|
+
const msg = event.data.pluginMessage;
|
|
232
|
+
if (!msg) return;
|
|
233
|
+
|
|
234
|
+
switch (msg.type) {
|
|
235
|
+
case "selection-changed":
|
|
236
|
+
currentSelection = msg.nodes;
|
|
237
|
+
currentPage = msg.page;
|
|
238
|
+
updateSelectionUI();
|
|
239
|
+
// Forward to server
|
|
240
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
241
|
+
ws.send(JSON.stringify({
|
|
242
|
+
type: "selection-update",
|
|
243
|
+
nodes: msg.nodes,
|
|
244
|
+
page: msg.page,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
case "pages-list":
|
|
250
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
251
|
+
ws.send(JSON.stringify({ type: "pages-list", pages: msg.pages }));
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
|
|
255
|
+
// Forward all result messages to server
|
|
256
|
+
case "node-data":
|
|
257
|
+
case "node-created":
|
|
258
|
+
case "node-modified":
|
|
259
|
+
case "node-deleted":
|
|
260
|
+
case "node-exported":
|
|
261
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
262
|
+
ws.send(JSON.stringify(msg));
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// ─── UI Helpers ───────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
function updateSelectionUI() {
|
|
271
|
+
if (currentSelection.length === 0) {
|
|
272
|
+
selectionBar.innerHTML = "No selection";
|
|
273
|
+
} else if (currentSelection.length === 1) {
|
|
274
|
+
const n = currentSelection[0];
|
|
275
|
+
selectionBar.innerHTML = `<span class="node-name">${n.name}</span> (${n.type}) — ${Math.round(n.width)}×${Math.round(n.height)}`;
|
|
276
|
+
} else {
|
|
277
|
+
selectionBar.innerHTML = `<span class="node-name">${currentSelection.length} elements</span> selected`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function addMessage(role, text) {
|
|
282
|
+
const div = document.createElement("div");
|
|
283
|
+
div.className = "message " + role;
|
|
284
|
+
div.textContent = text;
|
|
285
|
+
chatArea.appendChild(div);
|
|
286
|
+
chatArea.scrollTop = chatArea.scrollHeight;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── User Input ───────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
function sendMessage() {
|
|
292
|
+
const text = input.value.trim();
|
|
293
|
+
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
294
|
+
|
|
295
|
+
addMessage("user", text);
|
|
296
|
+
ws.send(JSON.stringify({
|
|
297
|
+
type: "chat-message",
|
|
298
|
+
text,
|
|
299
|
+
selection: currentSelection,
|
|
300
|
+
page: currentPage,
|
|
301
|
+
}));
|
|
302
|
+
input.value = "";
|
|
303
|
+
input.style.height = "auto";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
sendBtn.addEventListener("click", sendMessage);
|
|
307
|
+
input.addEventListener("keydown", (e) => {
|
|
308
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
309
|
+
e.preventDefault();
|
|
310
|
+
sendMessage();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
input.addEventListener("input", () => {
|
|
314
|
+
input.style.height = "auto";
|
|
315
|
+
input.style.height = Math.min(input.scrollHeight, 120) + "px";
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Start connection
|
|
319
|
+
connect();
|
|
320
|
+
</script>
|
|
321
|
+
</body>
|
|
322
|
+
</html>
|
|
File without changes
|