figma-rust-mcp 2.8.4 → 2.9.5
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 +101 -52
- package/bin/figma-mcp.js +4 -2
- package/package.json +1 -1
- package/plugin/code.js +270 -13
- package/plugin/ui.html +768 -374
- package/scripts/test-rust-mcp.sh +4 -6
package/plugin/ui.html
CHANGED
|
@@ -5,61 +5,74 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Figma MCP</title>
|
|
7
7
|
<style>
|
|
8
|
-
/* ── DESIGN
|
|
8
|
+
/* ── GHOSTTY DESIGN SYSTEM (High-Performance Native Terminal Aesthetic) ── */
|
|
9
9
|
:root {
|
|
10
|
-
--bg:
|
|
11
|
-
--surface-
|
|
12
|
-
--surface-
|
|
13
|
-
--surface-
|
|
14
|
-
--surface-
|
|
15
|
-
|
|
16
|
-
--border-
|
|
17
|
-
--border-
|
|
18
|
-
--
|
|
19
|
-
|
|
20
|
-
--text-
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
10
|
+
--bg: #0d0e12;
|
|
11
|
+
--surface-base: #12141a;
|
|
12
|
+
--surface-card: #171922;
|
|
13
|
+
--surface-hover: #1d202b;
|
|
14
|
+
--surface-active: #242837;
|
|
15
|
+
|
|
16
|
+
--border-subtle: rgba(255, 255, 255, 0.07);
|
|
17
|
+
--border-light: rgba(255, 255, 255, 0.13);
|
|
18
|
+
--border-ghost: rgba(147, 129, 255, 0.35);
|
|
19
|
+
|
|
20
|
+
--text-primary: #f4f4f7;
|
|
21
|
+
--text-secondary: #9aa1b2;
|
|
22
|
+
--text-muted: #52596d;
|
|
23
|
+
--text-dim: #363c4e;
|
|
24
|
+
|
|
25
|
+
/* Ghostty Signature Palette */
|
|
26
|
+
--ghost-lavender: #9381ff;
|
|
27
|
+
--ghost-glow: rgba(147, 129, 255, 0.22);
|
|
28
|
+
--ghost-mint: #34d399;
|
|
29
|
+
--ghost-mint-glow:rgba(52, 211, 153, 0.20);
|
|
30
|
+
--ghost-cyan: #38bdf8;
|
|
31
|
+
--ghost-peach: #ff9f68;
|
|
32
|
+
--ghost-rose: #fb7185;
|
|
33
|
+
--ghost-amber: #fbbf24;
|
|
34
|
+
|
|
35
|
+
--radius-xs: 4px;
|
|
36
|
+
--radius-sm: 6px;
|
|
37
|
+
--radius-md: 9px;
|
|
38
|
+
--radius-full: 9999px;
|
|
39
|
+
|
|
40
|
+
--font-ui: -apple-system, BlinkMacSystemFont, 'Pretendard', 'Inter', 'SF Pro Text', sans-serif;
|
|
41
|
+
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', Menlo, monospace;
|
|
42
|
+
--transition: 0.14s cubic-bezier(0.4, 0, 0.2, 1);
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
34
46
|
html, body { width: 100%; height: 100%; overflow: hidden; }
|
|
35
47
|
|
|
36
48
|
body {
|
|
37
|
-
font-family: -
|
|
49
|
+
font-family: var(--font-ui);
|
|
38
50
|
background: var(--bg);
|
|
39
|
-
color: var(--text-
|
|
51
|
+
color: var(--text-primary);
|
|
40
52
|
font-size: 11px;
|
|
41
|
-
line-height: 1.
|
|
53
|
+
line-height: 1.4;
|
|
42
54
|
-webkit-font-smoothing: antialiased;
|
|
43
55
|
user-select: none;
|
|
44
56
|
display: flex;
|
|
45
57
|
flex-direction: column;
|
|
46
58
|
}
|
|
47
59
|
|
|
60
|
+
/* Ghostty subtle atmospheric ambient */
|
|
48
61
|
body::before {
|
|
49
62
|
content: '';
|
|
50
63
|
position: fixed;
|
|
51
|
-
top: -80px; left: -
|
|
64
|
+
top: -80px; left: -50px;
|
|
52
65
|
width: 260px; height: 260px;
|
|
53
|
-
background: radial-gradient(circle, rgba(
|
|
66
|
+
background: radial-gradient(circle, rgba(147, 129, 255, 0.07) 0%, transparent 65%);
|
|
54
67
|
pointer-events: none;
|
|
55
68
|
z-index: 0;
|
|
56
69
|
}
|
|
57
70
|
body::after {
|
|
58
71
|
content: '';
|
|
59
72
|
position: fixed;
|
|
60
|
-
bottom: -60px; right: -
|
|
61
|
-
width:
|
|
62
|
-
background: radial-gradient(circle, rgba(
|
|
73
|
+
bottom: -60px; right: -50px;
|
|
74
|
+
width: 220px; height: 220px;
|
|
75
|
+
background: radial-gradient(circle, rgba(52, 211, 153, 0.05) 0%, transparent 65%);
|
|
63
76
|
pointer-events: none;
|
|
64
77
|
z-index: 0;
|
|
65
78
|
}
|
|
@@ -70,339 +83,594 @@
|
|
|
70
83
|
display: flex;
|
|
71
84
|
flex-direction: column;
|
|
72
85
|
height: 100%;
|
|
73
|
-
padding:
|
|
74
|
-
gap:
|
|
86
|
+
padding: 8px;
|
|
87
|
+
gap: 6px;
|
|
75
88
|
}
|
|
76
89
|
|
|
77
|
-
/* HEADER */
|
|
90
|
+
/* ── HEADER (Ghostty macOS Native Window Chrome) ── */
|
|
78
91
|
.header {
|
|
79
92
|
display: flex;
|
|
80
93
|
align-items: center;
|
|
81
94
|
justify-content: space-between;
|
|
82
|
-
background: var(--surface-
|
|
83
|
-
border: 1px solid var(--border-
|
|
95
|
+
background: var(--surface-base);
|
|
96
|
+
border: 1px solid var(--border-subtle);
|
|
84
97
|
border-radius: var(--radius-md);
|
|
85
|
-
padding: 8px
|
|
98
|
+
padding: 6px 8px;
|
|
99
|
+
flex-shrink: 0;
|
|
100
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.4);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.brand { display: flex; align-items: center; gap: 7px; min-width: 0; }
|
|
104
|
+
|
|
105
|
+
.brand-logo {
|
|
106
|
+
width: 22px; height: 22px;
|
|
107
|
+
border-radius: var(--radius-xs);
|
|
108
|
+
background: linear-gradient(135deg, #7c3aed 0%, #9381ff 60%, #34d399 100%);
|
|
109
|
+
display: flex; align-items: center; justify-content: center;
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.18) inset, 0 2px 8px rgba(124, 58, 237, 0.35);
|
|
112
|
+
color: #ffffff;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.brand-info { display: flex; flex-direction: column; min-width: 0; }
|
|
116
|
+
.brand-title-row { display: flex; align-items: baseline; gap: 5px; }
|
|
117
|
+
.brand-name { font-size: 11px; font-weight: 700; letter-spacing: -0.02em; color: var(--text-primary); line-height: 1; }
|
|
118
|
+
.brand-ver { font-size: 8px; font-weight: 600; color: var(--text-muted); font-family: var(--font-mono); }
|
|
119
|
+
.brand-file {
|
|
120
|
+
font-size: 9px;
|
|
121
|
+
color: var(--text-secondary);
|
|
122
|
+
font-weight: 500;
|
|
123
|
+
line-height: 1.2;
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
text-overflow: ellipsis;
|
|
127
|
+
max-width: 140px;
|
|
128
|
+
margin-top: 1px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.header-actions {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: 4px;
|
|
86
135
|
flex-shrink: 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.header-icon-btn {
|
|
139
|
+
width: 22px;
|
|
140
|
+
height: 22px;
|
|
141
|
+
padding: 0;
|
|
142
|
+
border-radius: var(--radius-xs);
|
|
143
|
+
background: var(--surface-card);
|
|
144
|
+
border: 1px solid var(--border-subtle);
|
|
145
|
+
color: var(--text-secondary);
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
justify-content: center;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
transition: all var(--transition);
|
|
151
|
+
}
|
|
152
|
+
.header-icon-btn:hover {
|
|
153
|
+
background: var(--surface-hover);
|
|
154
|
+
border-color: var(--border-light);
|
|
155
|
+
color: var(--text-primary);
|
|
156
|
+
}
|
|
157
|
+
.header-icon-btn:active { transform: scale(0.94); }
|
|
158
|
+
.header-icon-btn.indexing svg {
|
|
159
|
+
animation: spin 1s linear infinite;
|
|
160
|
+
color: var(--ghost-lavender);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.status-pill {
|
|
164
|
+
display: flex; align-items: center; gap: 5px;
|
|
165
|
+
background: var(--surface-card);
|
|
166
|
+
border: 1px solid var(--border-subtle);
|
|
167
|
+
border-radius: var(--radius-full);
|
|
168
|
+
padding: 2.5px 7px 2.5px 5px;
|
|
169
|
+
font-size: 9px; font-weight: 600;
|
|
170
|
+
color: var(--text-secondary);
|
|
171
|
+
transition: all var(--transition);
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
font-family: var(--font-mono);
|
|
174
|
+
}
|
|
175
|
+
.status-pill:hover { border-color: var(--border-light); background: var(--surface-hover); color: var(--text-primary); }
|
|
176
|
+
.status-pill.connected { border-color: rgba(52,211,153,0.25); background: rgba(52,211,153,0.06); color: var(--ghost-mint); }
|
|
177
|
+
.status-pill.offline { border-color: rgba(251,113,133,0.25); background: rgba(251,113,133,0.06); color: var(--ghost-rose); }
|
|
178
|
+
.status-pill.connecting { border-color: rgba(251,191,36,0.25); background: rgba(251,191,36,0.06); color: var(--ghost-amber); }
|
|
179
|
+
|
|
180
|
+
.dot {
|
|
181
|
+
width: 6px; height: 6px;
|
|
182
|
+
border-radius: 50%; flex-shrink: 0;
|
|
183
|
+
transition: all var(--transition);
|
|
184
|
+
}
|
|
185
|
+
.dot.green { background: var(--ghost-mint); box-shadow: 0 0 0 2px rgba(52,211,153,0.2), 0 0 8px rgba(52,211,153,0.6); }
|
|
186
|
+
.dot.yellow { background: var(--ghost-amber); box-shadow: 0 0 0 2px rgba(251,191,36,0.2), 0 0 8px rgba(251,191,36,0.5); animation: pulse 1.4s ease-in-out infinite; }
|
|
187
|
+
.dot.red { background: var(--ghost-rose); box-shadow: 0 0 0 2px rgba(251,113,133,0.2), 0 0 8px rgba(251,113,133,0.5); }
|
|
188
|
+
|
|
189
|
+
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.3; transform: scale(0.85); } }
|
|
190
|
+
|
|
191
|
+
/* ── SELECTION INSPECTOR (Ghostty Split-Pane Card) ── */
|
|
192
|
+
.selection-card {
|
|
193
|
+
display: flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
justify-content: space-between;
|
|
196
|
+
gap: 7px;
|
|
197
|
+
background: var(--surface-card);
|
|
198
|
+
border: 1px solid var(--border-subtle);
|
|
199
|
+
border-radius: var(--radius-sm);
|
|
200
|
+
padding: 6px 8px;
|
|
201
|
+
flex-shrink: 0;
|
|
202
|
+
transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
|
|
87
203
|
position: relative;
|
|
88
204
|
overflow: hidden;
|
|
89
205
|
}
|
|
90
|
-
.
|
|
206
|
+
.selection-card::before {
|
|
91
207
|
content: '';
|
|
92
208
|
position: absolute;
|
|
93
|
-
top: 0; left: 0; right: 0;
|
|
94
|
-
height: 1px;
|
|
209
|
+
top: 0; left: 0; right: 0; height: 1px;
|
|
95
210
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
|
|
96
211
|
}
|
|
212
|
+
.selection-card.has-selection {
|
|
213
|
+
border-color: var(--border-ghost);
|
|
214
|
+
background: linear-gradient(135deg, rgba(147, 129, 255, 0.05) 0%, rgba(52, 211, 153, 0.02) 100%), var(--surface-card);
|
|
215
|
+
}
|
|
216
|
+
.selection-card.has-selection:hover {
|
|
217
|
+
border-color: rgba(147, 129, 255, 0.55);
|
|
218
|
+
background: linear-gradient(135deg, rgba(147, 129, 255, 0.09) 0%, rgba(52, 211, 153, 0.04) 100%), var(--surface-card);
|
|
219
|
+
}
|
|
97
220
|
|
|
98
|
-
.
|
|
221
|
+
.selection-main {
|
|
222
|
+
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
gap: 7px;
|
|
225
|
+
min-width: 0;
|
|
226
|
+
flex: 1;
|
|
227
|
+
cursor: pointer;
|
|
228
|
+
}
|
|
99
229
|
|
|
100
|
-
.
|
|
101
|
-
width:
|
|
102
|
-
border-radius:
|
|
103
|
-
background:
|
|
230
|
+
.selection-icon-wrap {
|
|
231
|
+
width: 22px; height: 22px;
|
|
232
|
+
border-radius: var(--radius-xs);
|
|
233
|
+
background: var(--surface-base);
|
|
234
|
+
border: 1px solid var(--border-subtle);
|
|
104
235
|
display: flex; align-items: center; justify-content: center;
|
|
105
236
|
flex-shrink: 0;
|
|
106
|
-
|
|
237
|
+
color: var(--text-muted);
|
|
238
|
+
transition: all var(--transition);
|
|
239
|
+
}
|
|
240
|
+
.selection-card.has-selection .selection-icon-wrap {
|
|
241
|
+
background: rgba(147, 129, 255, 0.12);
|
|
242
|
+
border-color: rgba(147, 129, 255, 0.35);
|
|
243
|
+
color: var(--ghost-lavender);
|
|
107
244
|
}
|
|
108
|
-
.brand-logo svg { display: block; }
|
|
109
|
-
.brand-info { display: flex; flex-direction: column; gap: 1px; }
|
|
110
|
-
.brand-name { font-size: 11.5px; font-weight: 700; letter-spacing: 0.3px; color: var(--text-1); line-height: 1; }
|
|
111
|
-
.brand-file { font-size: 9px; color: var(--text-3); font-weight: 500; letter-spacing: 0.2px; line-height: 1; }
|
|
112
245
|
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
246
|
+
.selection-text { display: flex; flex-direction: column; min-width: 0; }
|
|
247
|
+
.selection-name {
|
|
248
|
+
font-size: 11px;
|
|
249
|
+
font-weight: 600;
|
|
250
|
+
color: var(--text-primary);
|
|
251
|
+
line-height: 1.2;
|
|
252
|
+
white-space: nowrap;
|
|
253
|
+
overflow: hidden;
|
|
254
|
+
text-overflow: ellipsis;
|
|
255
|
+
}
|
|
256
|
+
.selection-meta {
|
|
257
|
+
font-size: 9px;
|
|
258
|
+
color: var(--text-secondary);
|
|
259
|
+
font-family: var(--font-mono);
|
|
260
|
+
line-height: 1.2;
|
|
261
|
+
white-space: nowrap;
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
text-overflow: ellipsis;
|
|
122
264
|
}
|
|
123
|
-
.status-pill.connected { border-color: rgba(34,211,160,0.25); background: rgba(34,211,160,0.06); }
|
|
124
|
-
.status-pill.offline { border-color: rgba(244,63,94,0.2); background: rgba(244,63,94,0.05); }
|
|
125
265
|
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
266
|
+
.selection-actions {
|
|
267
|
+
display: flex;
|
|
268
|
+
align-items: center;
|
|
269
|
+
gap: 4px;
|
|
270
|
+
flex-shrink: 0;
|
|
130
271
|
}
|
|
131
|
-
.dot.green { background: var(--green); box-shadow: 0 0 0 2px rgba(34,211,160,0.2), 0 0 8px rgba(34,211,160,0.5); }
|
|
132
|
-
.dot.yellow { background: var(--amber); box-shadow: 0 0 0 2px rgba(245,158,11,0.2), 0 0 8px rgba(245,158,11,0.4); animation: blink 1.4s ease-in-out infinite; }
|
|
133
|
-
.dot.red { background: var(--rose); box-shadow: 0 0 0 2px rgba(244,63,94,0.2), 0 0 8px rgba(244,63,94,0.4); }
|
|
134
272
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
273
|
+
.act-btn {
|
|
274
|
+
display: inline-flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
justify-content: center;
|
|
277
|
+
gap: 4px;
|
|
278
|
+
height: 22px;
|
|
279
|
+
padding: 0 6px;
|
|
280
|
+
border-radius: var(--radius-xs);
|
|
281
|
+
font-size: 8.5px;
|
|
282
|
+
font-weight: 600;
|
|
283
|
+
font-family: var(--font-mono);
|
|
284
|
+
background: var(--surface-base);
|
|
285
|
+
border: 1px solid var(--border-subtle);
|
|
286
|
+
color: var(--text-secondary);
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
transition: all var(--transition);
|
|
289
|
+
letter-spacing: 0.2px;
|
|
290
|
+
}
|
|
291
|
+
.act-btn:hover {
|
|
292
|
+
background: var(--surface-hover);
|
|
293
|
+
border-color: var(--border-light);
|
|
294
|
+
color: var(--text-primary);
|
|
295
|
+
}
|
|
296
|
+
.act-btn:active { transform: scale(0.95); }
|
|
297
|
+
.act-btn.icon-only { width: 22px; padding: 0; }
|
|
298
|
+
.act-btn.badge-btn {
|
|
299
|
+
background: rgba(147, 129, 255, 0.12);
|
|
300
|
+
border-color: rgba(147, 129, 255, 0.28);
|
|
301
|
+
color: #d1c9ff;
|
|
302
|
+
}
|
|
303
|
+
.act-btn.badge-btn:hover {
|
|
304
|
+
background: rgba(147, 129, 255, 0.28);
|
|
305
|
+
border-color: rgba(147, 129, 255, 0.6);
|
|
306
|
+
color: #ffffff;
|
|
138
307
|
}
|
|
139
308
|
|
|
140
|
-
/*
|
|
141
|
-
.
|
|
309
|
+
/* ── TELEMETRY SEGMENTED STATUSLINE ── */
|
|
310
|
+
.telemetry-ribbon {
|
|
142
311
|
display: grid;
|
|
143
312
|
grid-template-columns: repeat(4, 1fr);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
display: grid;
|
|
149
|
-
grid-template-columns: 1fr 1fr auto;
|
|
150
|
-
gap: 5px;
|
|
313
|
+
background: var(--surface-base);
|
|
314
|
+
border: 1px solid var(--border-subtle);
|
|
315
|
+
border-radius: var(--radius-sm);
|
|
316
|
+
overflow: hidden;
|
|
151
317
|
flex-shrink: 0;
|
|
152
318
|
}
|
|
153
319
|
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
padding: 6px 7px;
|
|
159
|
-
display: flex; flex-direction: column; gap: 2px;
|
|
160
|
-
transition: border-color var(--transition);
|
|
161
|
-
}
|
|
162
|
-
.stat-card:hover { border-color: var(--border-2); }
|
|
163
|
-
|
|
164
|
-
.stat-label {
|
|
165
|
-
font-size: 8px; font-weight: 700;
|
|
166
|
-
letter-spacing: 0.7px; text-transform: uppercase;
|
|
167
|
-
color: var(--text-3);
|
|
168
|
-
}
|
|
169
|
-
.stat-value {
|
|
170
|
-
font-size: 15px; font-weight: 800;
|
|
171
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
172
|
-
line-height: 1.1;
|
|
173
|
-
display: flex; align-items: baseline; gap: 2px;
|
|
174
|
-
}
|
|
175
|
-
.stat-unit {
|
|
176
|
-
font-size: 8.5px; font-weight: 600; color: var(--text-3);
|
|
177
|
-
}
|
|
178
|
-
.stat-card.writes .stat-value { color: var(--violet); }
|
|
179
|
-
.stat-card.reads .stat-value { color: var(--accent-2); }
|
|
180
|
-
.stat-card.errors .stat-value { color: var(--rose); }
|
|
181
|
-
.stat-card.speed .stat-value { color: var(--green); }
|
|
182
|
-
.stat-card.memory .stat-value { color: var(--amber); }
|
|
183
|
-
|
|
184
|
-
.port-card {
|
|
185
|
-
background: var(--surface-1);
|
|
186
|
-
border: 1px solid var(--border-1);
|
|
187
|
-
border-radius: var(--radius-sm);
|
|
188
|
-
padding: 6px 9px;
|
|
189
|
-
display: flex; flex-direction: column; gap: 2px;
|
|
190
|
-
cursor: pointer;
|
|
320
|
+
.telemetry-cell {
|
|
321
|
+
display: flex;
|
|
322
|
+
flex-direction: column;
|
|
323
|
+
align-items: center;
|
|
191
324
|
justify-content: center;
|
|
192
|
-
|
|
325
|
+
padding: 4px 2px;
|
|
326
|
+
gap: 1px;
|
|
327
|
+
position: relative;
|
|
328
|
+
transition: background var(--transition);
|
|
193
329
|
}
|
|
194
|
-
.
|
|
195
|
-
|
|
196
|
-
|
|
330
|
+
.telemetry-cell:not(:last-child)::after {
|
|
331
|
+
content: '';
|
|
332
|
+
position: absolute;
|
|
333
|
+
right: 0; top: 15%; bottom: 15%;
|
|
334
|
+
width: 1px;
|
|
335
|
+
background: var(--border-subtle);
|
|
336
|
+
}
|
|
337
|
+
.telemetry-cell:hover { background: rgba(255, 255, 255, 0.02); }
|
|
338
|
+
|
|
339
|
+
.telemetry-label {
|
|
340
|
+
font-size: 7.5px;
|
|
341
|
+
font-weight: 700;
|
|
342
|
+
letter-spacing: 0.6px;
|
|
343
|
+
text-transform: uppercase;
|
|
344
|
+
color: var(--text-muted);
|
|
345
|
+
font-family: var(--font-mono);
|
|
346
|
+
}
|
|
347
|
+
.telemetry-value {
|
|
348
|
+
font-size: 13px;
|
|
349
|
+
font-weight: 700;
|
|
350
|
+
font-family: var(--font-mono);
|
|
351
|
+
line-height: 1;
|
|
352
|
+
font-variant-numeric: tabular-nums;
|
|
353
|
+
display: flex;
|
|
354
|
+
align-items: baseline;
|
|
355
|
+
gap: 1.5px;
|
|
356
|
+
}
|
|
357
|
+
.telemetry-unit { font-size: 7.5px; font-weight: 600; color: var(--text-muted); }
|
|
197
358
|
|
|
198
|
-
|
|
359
|
+
.telemetry-cell.writes .telemetry-value { color: var(--ghost-lavender); }
|
|
360
|
+
.telemetry-cell.reads .telemetry-value { color: var(--ghost-cyan); }
|
|
361
|
+
.telemetry-cell.speed .telemetry-value { color: var(--ghost-mint); }
|
|
362
|
+
.telemetry-cell.ram .telemetry-value { color: var(--ghost-peach); }
|
|
363
|
+
|
|
364
|
+
/* ── GHOSTTY STREAM CONSOLE BUFFER ── */
|
|
199
365
|
.terminal-wrap {
|
|
200
366
|
flex: 1; min-height: 0;
|
|
201
367
|
display: flex; flex-direction: column;
|
|
202
|
-
background: var(--surface-
|
|
203
|
-
border: 1px solid var(--border-
|
|
368
|
+
background: var(--surface-base);
|
|
369
|
+
border: 1px solid var(--border-subtle);
|
|
204
370
|
border-radius: var(--radius-md);
|
|
205
371
|
overflow: hidden;
|
|
372
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
206
373
|
}
|
|
207
374
|
|
|
208
375
|
.terminal-header {
|
|
209
376
|
display: flex; align-items: center; justify-content: space-between;
|
|
210
|
-
padding:
|
|
211
|
-
background:
|
|
212
|
-
border-bottom: 1px solid var(--border-
|
|
377
|
+
padding: 5px 7px;
|
|
378
|
+
background: rgba(255,255,255,0.02);
|
|
379
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
213
380
|
flex-shrink: 0;
|
|
214
381
|
}
|
|
215
382
|
|
|
216
|
-
.terminal-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
383
|
+
.terminal-tabs { display: flex; align-items: center; gap: 3px; }
|
|
384
|
+
|
|
385
|
+
.t-tab {
|
|
386
|
+
font-size: 8.5px;
|
|
387
|
+
font-weight: 600;
|
|
388
|
+
font-family: var(--font-mono);
|
|
389
|
+
letter-spacing: 0.2px;
|
|
390
|
+
padding: 2px 7px;
|
|
391
|
+
border-radius: var(--radius-xs);
|
|
392
|
+
color: var(--text-muted);
|
|
393
|
+
background: transparent;
|
|
394
|
+
border: none;
|
|
395
|
+
cursor: pointer;
|
|
396
|
+
transition: all var(--transition);
|
|
221
397
|
}
|
|
222
|
-
.t-
|
|
398
|
+
.t-tab:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); }
|
|
399
|
+
.t-tab.active { color: var(--text-primary); background: var(--surface-card); box-shadow: 0 1px 2px rgba(0,0,0,0.3); }
|
|
400
|
+
|
|
401
|
+
.terminal-meta { display: flex; align-items: center; gap: 6px; }
|
|
223
402
|
|
|
224
403
|
.event-badge {
|
|
225
|
-
font-size:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
404
|
+
font-size: 8px;
|
|
405
|
+
font-weight: 600;
|
|
406
|
+
font-family: var(--font-mono);
|
|
407
|
+
color: var(--text-muted);
|
|
408
|
+
background: var(--surface-card);
|
|
409
|
+
border: 1px solid var(--border-subtle);
|
|
410
|
+
border-radius: var(--radius-full);
|
|
411
|
+
padding: 1px 6px;
|
|
231
412
|
}
|
|
232
413
|
|
|
233
414
|
.terminal-body {
|
|
234
415
|
flex: 1;
|
|
235
|
-
padding: 8px
|
|
236
|
-
font-family:
|
|
237
|
-
font-size:
|
|
416
|
+
padding: 6px 8px;
|
|
417
|
+
font-family: var(--font-mono);
|
|
418
|
+
font-size: 9.5px;
|
|
419
|
+
line-height: 1.5;
|
|
238
420
|
overflow-y: auto;
|
|
239
|
-
color: var(--text-
|
|
421
|
+
color: var(--text-secondary);
|
|
240
422
|
}
|
|
241
423
|
.terminal-body::-webkit-scrollbar { width: 3px; }
|
|
242
424
|
.terminal-body::-webkit-scrollbar-track { background: transparent; }
|
|
243
|
-
.terminal-body::-webkit-scrollbar-thumb { background: var(--border-
|
|
425
|
+
.terminal-body::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 2px; }
|
|
244
426
|
|
|
245
427
|
.log-line {
|
|
246
|
-
display: flex;
|
|
247
|
-
|
|
428
|
+
display: flex;
|
|
429
|
+
align-items: baseline;
|
|
430
|
+
gap: 6px;
|
|
431
|
+
padding: 1.5px 3px;
|
|
432
|
+
border-radius: 3px;
|
|
433
|
+
transition: background 0.1s ease;
|
|
248
434
|
}
|
|
249
|
-
.log-line:hover { background: rgba(255,255,255,0.
|
|
250
|
-
.log-ts { font-size:
|
|
251
|
-
.log-
|
|
252
|
-
.log-
|
|
253
|
-
.log-
|
|
254
|
-
.log-
|
|
435
|
+
.log-line:hover { background: rgba(255,255,255,0.03); }
|
|
436
|
+
.log-ts { font-size: 8px; color: var(--text-muted); flex-shrink: 0; letter-spacing: -0.2px; }
|
|
437
|
+
.log-prompt { color: var(--ghost-lavender); font-size: 9px; flex-shrink: 0; font-weight: 700; }
|
|
438
|
+
.log-msg { flex: 1; word-break: break-all; color: var(--text-secondary); }
|
|
439
|
+
.log-msg.ok { color: var(--ghost-mint); }
|
|
440
|
+
.log-msg.err { color: var(--ghost-rose); }
|
|
441
|
+
.log-msg.info { color: var(--ghost-cyan); }
|
|
255
442
|
|
|
256
443
|
.terminal-empty {
|
|
257
444
|
display: flex; flex-direction: column;
|
|
258
445
|
align-items: center; justify-content: center;
|
|
259
|
-
height: 100%; gap:
|
|
446
|
+
height: 100%; gap: 5px; color: var(--text-muted);
|
|
260
447
|
}
|
|
261
|
-
.terminal-empty-icon { font-size:
|
|
262
|
-
.terminal-empty-text { font-size:
|
|
448
|
+
.terminal-empty-icon { font-size: 16px; opacity: 0.35; }
|
|
449
|
+
.terminal-empty-text { font-size: 9.5px; opacity: 0.6; font-family: var(--font-mono); }
|
|
263
450
|
|
|
264
|
-
/* FOOTER */
|
|
451
|
+
/* ── FOOTER ACTIONS ── */
|
|
265
452
|
.footer { display: flex; gap: 6px; flex-shrink: 0; }
|
|
266
453
|
|
|
267
454
|
.btn {
|
|
268
455
|
display: inline-flex; align-items: center; justify-content: center;
|
|
269
|
-
gap:
|
|
456
|
+
gap: 6px; padding: 6px 11px;
|
|
270
457
|
border-radius: var(--radius-sm);
|
|
271
|
-
font-size:
|
|
458
|
+
font-size: 10px; font-weight: 600;
|
|
459
|
+
font-family: var(--font-ui);
|
|
272
460
|
cursor: pointer;
|
|
273
|
-
border: 1px solid var(--border-
|
|
274
|
-
background: var(--surface-
|
|
275
|
-
color: var(--text-
|
|
461
|
+
border: 1px solid var(--border-subtle);
|
|
462
|
+
background: var(--surface-card);
|
|
463
|
+
color: var(--text-secondary);
|
|
276
464
|
transition: all var(--transition);
|
|
277
465
|
letter-spacing: 0.1px;
|
|
278
466
|
white-space: nowrap;
|
|
279
467
|
}
|
|
280
|
-
.btn:hover { background: var(--surface-
|
|
281
|
-
.btn:active { transform: scale(0.
|
|
468
|
+
.btn:hover { background: var(--surface-hover); border-color: var(--border-light); color: var(--text-primary); }
|
|
469
|
+
.btn:active { transform: scale(0.97); }
|
|
282
470
|
|
|
283
471
|
.btn-icon { width: 30px; height: 30px; padding: 0; flex-shrink: 0; }
|
|
284
472
|
|
|
285
473
|
.btn-primary {
|
|
286
474
|
flex: 1;
|
|
287
|
-
background: linear-gradient(135deg,
|
|
288
|
-
border-color:
|
|
289
|
-
|
|
475
|
+
background: linear-gradient(135deg, rgba(124,58,237,0.9) 0%, rgba(147,129,255,0.9) 50%, rgba(52,211,153,0.85) 100%);
|
|
476
|
+
border-color: rgba(147, 129, 255, 0.45);
|
|
477
|
+
color: #ffffff;
|
|
478
|
+
font-weight: 600;
|
|
479
|
+
box-shadow: 0 1px 0 rgba(255,255,255,0.2) inset, 0 3px 12px rgba(124, 58, 237, 0.28);
|
|
290
480
|
}
|
|
291
481
|
.btn-primary:hover {
|
|
292
|
-
|
|
293
|
-
|
|
482
|
+
background: linear-gradient(135deg, #7c3aed 0%, #9381ff 50%, #34d399 100%);
|
|
483
|
+
border-color: rgba(147, 129, 255, 0.7);
|
|
484
|
+
color: #ffffff;
|
|
485
|
+
box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset, 0 4px 16px rgba(124, 58, 237, 0.45);
|
|
294
486
|
}
|
|
295
487
|
|
|
296
|
-
.btn-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
488
|
+
.btn-primary.indexing svg { animation: spin 1s linear infinite; }
|
|
489
|
+
@keyframes spin { 100% { transform: rotate(360deg); } }
|
|
490
|
+
|
|
491
|
+
/* ── TOAST NOTIFICATION ── */
|
|
492
|
+
.toast {
|
|
493
|
+
position: fixed;
|
|
494
|
+
bottom: 45px;
|
|
495
|
+
left: 50%;
|
|
496
|
+
transform: translateX(-50%) translateY(15px);
|
|
497
|
+
opacity: 0;
|
|
498
|
+
pointer-events: none;
|
|
499
|
+
background: rgba(18, 20, 26, 0.96);
|
|
500
|
+
border: 1px solid rgba(147, 129, 255, 0.3);
|
|
501
|
+
backdrop-filter: blur(8px);
|
|
502
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.6), 0 0 12px rgba(147, 129, 255, 0.2);
|
|
503
|
+
border-radius: var(--radius-full);
|
|
504
|
+
padding: 5px 12px;
|
|
505
|
+
font-size: 9.5px;
|
|
506
|
+
font-weight: 600;
|
|
507
|
+
color: var(--text-primary);
|
|
508
|
+
display: flex;
|
|
509
|
+
align-items: center;
|
|
510
|
+
gap: 6px;
|
|
511
|
+
z-index: 100;
|
|
512
|
+
transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
513
|
+
}
|
|
514
|
+
.toast.show {
|
|
515
|
+
opacity: 1;
|
|
516
|
+
transform: translateX(-50%) translateY(0);
|
|
517
|
+
pointer-events: auto;
|
|
301
518
|
}
|
|
302
|
-
.btn-live:hover { background: rgba(34,211,160,0.12); border-color: rgba(34,211,160,0.4); color: var(--green); }
|
|
303
519
|
|
|
304
520
|
/* RESIZE GRIP */
|
|
305
521
|
.resize-grip {
|
|
306
522
|
position: fixed; right: 0; bottom: 0;
|
|
307
|
-
width:
|
|
523
|
+
width: 18px; height: 18px;
|
|
308
524
|
cursor: nwse-resize; opacity: 0.35;
|
|
309
|
-
color: var(--text-
|
|
525
|
+
color: var(--text-secondary);
|
|
310
526
|
transition: opacity var(--transition), color var(--transition);
|
|
311
527
|
z-index: 1000;
|
|
312
528
|
display: flex; align-items: flex-end; justify-content: flex-end;
|
|
313
|
-
padding:
|
|
529
|
+
padding: 3px;
|
|
314
530
|
touch-action: none;
|
|
315
531
|
}
|
|
316
|
-
.resize-grip:hover { opacity: 1; color: var(--
|
|
532
|
+
.resize-grip:hover { opacity: 1; color: var(--ghost-mint); }
|
|
317
533
|
</style>
|
|
318
534
|
</head>
|
|
319
535
|
<body>
|
|
320
536
|
|
|
321
537
|
<div class="app">
|
|
322
538
|
|
|
539
|
+
<!-- ── HEADER (Ghostty macOS Native Window Chrome) ── -->
|
|
323
540
|
<header class="header">
|
|
324
541
|
<div class="brand">
|
|
325
542
|
<div class="brand-logo">
|
|
326
|
-
|
|
327
|
-
|
|
543
|
+
<!-- Ghostty Ghost Mascot Icon -->
|
|
544
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
545
|
+
<path d="M9 10h.01M15 10h.01M12 2a8 8 0 0 0-8 8v11l3-2 3 2 2-2 2 2 3-2 3 2V10a8 8 0 0 0-8-8z"/>
|
|
328
546
|
</svg>
|
|
329
547
|
</div>
|
|
330
548
|
<div class="brand-info">
|
|
331
|
-
<
|
|
332
|
-
|
|
549
|
+
<div class="brand-title-row">
|
|
550
|
+
<span class="brand-name">Figma MCP</span>
|
|
551
|
+
<span class="brand-ver">v2.8</span>
|
|
552
|
+
</div>
|
|
553
|
+
<span class="brand-file" id="doc-display" title="Active Document">Ready</span>
|
|
333
554
|
</div>
|
|
334
555
|
</div>
|
|
335
556
|
|
|
336
|
-
<div class="
|
|
337
|
-
|
|
338
|
-
<
|
|
339
|
-
|
|
557
|
+
<div class="header-actions">
|
|
558
|
+
<!-- Re-index Icon Button -->
|
|
559
|
+
<button class="header-icon-btn" id="reindex-btn" onclick="triggerManualReindex()" title="Sync Index (Re-index Figma canvas to MCP)">
|
|
560
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
561
|
+
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
|
|
562
|
+
<polyline points="2 17 12 22 22 17"/>
|
|
563
|
+
<polyline points="2 12 12 17 22 12"/>
|
|
564
|
+
</svg>
|
|
565
|
+
</button>
|
|
566
|
+
|
|
567
|
+
<!-- Reconnect / Probe Bridge Port Button -->
|
|
568
|
+
<button class="header-icon-btn" id="reconnect-btn" onclick="reconnect()" title="Reconnect / Probe Bridge Port">
|
|
569
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
570
|
+
<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
571
|
+
</svg>
|
|
572
|
+
</button>
|
|
573
|
+
|
|
574
|
+
<!-- Connection Status Pill -->
|
|
575
|
+
<div class="status-pill" id="status-pill" onclick="handleStatusClick()" title="Connection Status (Click for details)">
|
|
576
|
+
<div class="dot yellow" id="dot"></div>
|
|
577
|
+
<span id="status-text">Connecting</span>
|
|
578
|
+
</div>
|
|
340
579
|
</div>
|
|
341
580
|
</header>
|
|
342
581
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<
|
|
582
|
+
<!-- ── SELECTION INSPECTOR HERO CARD ── -->
|
|
583
|
+
<div class="selection-card" id="selection-card">
|
|
584
|
+
<div class="selection-main" onclick="focusSelection()" title="Click to center view on selection">
|
|
585
|
+
<div class="selection-icon-wrap" id="selection-icon">
|
|
586
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
587
|
+
<circle cx="12" cy="12" r="9"/>
|
|
588
|
+
</svg>
|
|
589
|
+
</div>
|
|
590
|
+
<div class="selection-text">
|
|
591
|
+
<span class="selection-name" id="selection-name">No selection</span>
|
|
592
|
+
<span class="selection-meta" id="selection-meta">Document canvas root</span>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
<div class="selection-actions" id="selection-actions" style="display:none;">
|
|
597
|
+
<!-- Snapshot PNG button -->
|
|
598
|
+
<button class="act-btn icon-only" id="snapshot-btn" onclick="exportSelectionImage(event)" title="📸 Copy layer PNG (2x) to clipboard">
|
|
599
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
600
|
+
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
|
|
601
|
+
<circle cx="12" cy="13" r="4"/>
|
|
602
|
+
</svg>
|
|
603
|
+
</button>
|
|
604
|
+
|
|
605
|
+
<!-- Focus / Center on Canvas button -->
|
|
606
|
+
<button class="act-btn icon-only" onclick="focusSelection()" title="🎯 Center view on canvas">
|
|
607
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
608
|
+
<circle cx="12" cy="12" r="10"/>
|
|
609
|
+
<line x1="22" y1="12" x2="18" y2="12"/>
|
|
610
|
+
<line x1="6" y1="12" x2="2" y2="12"/>
|
|
611
|
+
<line x1="12" y1="6" x2="12" y2="2"/>
|
|
612
|
+
<line x1="12" y1="22" x2="12" y2="18"/>
|
|
613
|
+
</svg>
|
|
614
|
+
</button>
|
|
615
|
+
|
|
616
|
+
<!-- Copy Node ID badge -->
|
|
617
|
+
<button class="act-btn badge-btn" id="selection-badge" onclick="copySelectionId(event)" title="📋 Click to copy Figma Node ID">
|
|
618
|
+
FRAME
|
|
619
|
+
</button>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
<!-- ── TELEMETRY SEGMENTED RIBBON ── -->
|
|
624
|
+
<div class="telemetry-ribbon">
|
|
625
|
+
<div class="telemetry-cell writes" title="Write operations executed by AI">
|
|
626
|
+
<span class="telemetry-label">Writes</span>
|
|
627
|
+
<span class="telemetry-value" id="write-count">0</span>
|
|
347
628
|
</div>
|
|
348
|
-
<div class="
|
|
349
|
-
<span class="
|
|
350
|
-
<span class="
|
|
629
|
+
<div class="telemetry-cell reads" title="Read / Inspection operations executed">
|
|
630
|
+
<span class="telemetry-label">Reads</span>
|
|
631
|
+
<span class="telemetry-value" id="read-count">0</span>
|
|
351
632
|
</div>
|
|
352
|
-
<div class="
|
|
353
|
-
<span class="
|
|
354
|
-
<span class="
|
|
633
|
+
<div class="telemetry-cell speed" title="Execution latency of last request">
|
|
634
|
+
<span class="telemetry-label">Latency</span>
|
|
635
|
+
<span class="telemetry-value"><span id="speed-display">0</span><span class="telemetry-unit">ms</span></span>
|
|
355
636
|
</div>
|
|
356
|
-
<div class="
|
|
357
|
-
<span class="
|
|
358
|
-
<span class="
|
|
637
|
+
<div class="telemetry-cell ram" title="Rust MCP Server Memory (RAM RSS)">
|
|
638
|
+
<span class="telemetry-label">RAM</span>
|
|
639
|
+
<span class="telemetry-value"><span id="ram-display">3.2</span><span class="telemetry-unit">MB</span></span>
|
|
359
640
|
</div>
|
|
360
641
|
</div>
|
|
361
642
|
|
|
643
|
+
<!-- ── STREAM CONSOLE TERMINAL ── -->
|
|
362
644
|
<section class="terminal-wrap">
|
|
363
645
|
<div class="terminal-header">
|
|
364
|
-
<div class="terminal-
|
|
365
|
-
<
|
|
366
|
-
|
|
646
|
+
<div class="terminal-tabs">
|
|
647
|
+
<button class="t-tab active" onclick="setLogFilter('all', this)">All</button>
|
|
648
|
+
<button class="t-tab" onclick="setLogFilter('writes', this)">Writes</button>
|
|
649
|
+
<button class="t-tab" onclick="setLogFilter('reads', this)">Reads</button>
|
|
650
|
+
</div>
|
|
651
|
+
<div class="terminal-meta">
|
|
652
|
+
<span class="event-badge" id="log-count">0 ev</span>
|
|
653
|
+
<button class="act-btn icon-only" onclick="clearLog()" title="Clear stream console" style="height:18px;width:18px;border-radius:3px;">
|
|
654
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
655
|
+
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
|
|
656
|
+
</svg>
|
|
657
|
+
</button>
|
|
367
658
|
</div>
|
|
368
|
-
<span class="event-badge" id="log-count">0 events</span>
|
|
369
659
|
</div>
|
|
370
660
|
<div class="terminal-body" id="log">
|
|
371
661
|
<div class="terminal-empty" id="log-empty">
|
|
372
662
|
<div class="terminal-empty-icon">◌</div>
|
|
373
|
-
<div class="terminal-empty-text">
|
|
663
|
+
<div class="terminal-empty-text">Listening on MCP bridge…</div>
|
|
374
664
|
</div>
|
|
375
665
|
</div>
|
|
376
666
|
</section>
|
|
377
667
|
|
|
378
|
-
|
|
379
|
-
<button class="btn btn-icon" onclick="clearLog()" title="Clear log">
|
|
380
|
-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
381
|
-
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
|
|
382
|
-
</svg>
|
|
383
|
-
</button>
|
|
384
|
-
|
|
385
|
-
<button class="btn" id="reindex-btn" onclick="triggerManualReindex()" title="Re-index Figma document to Rust memory" style="background:var(--surface-2);border:1px solid var(--border-2);color:var(--text-2);font-size:10px;padding:0 10px;gap:5px;">
|
|
386
|
-
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
387
|
-
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/>
|
|
388
|
-
</svg>
|
|
389
|
-
<span id="reindex-btn-label">⚡ Index</span>
|
|
390
|
-
</button>
|
|
391
|
-
|
|
392
|
-
<button class="btn btn-primary" id="launch-btn" onclick="launchServer()" title="Start Figma MCP server">
|
|
393
|
-
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
394
|
-
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
|
395
|
-
</svg>
|
|
396
|
-
<span id="launch-btn-label">Launch Server</span>
|
|
397
|
-
</button>
|
|
398
|
-
|
|
399
|
-
<button class="btn btn-icon" onclick="reconnect()" title="Reconnect">
|
|
400
|
-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
401
|
-
<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
402
|
-
</svg>
|
|
403
|
-
</button>
|
|
404
|
-
</footer>
|
|
668
|
+
</div>
|
|
405
669
|
|
|
670
|
+
<!-- Floating Toast Component -->
|
|
671
|
+
<div class="toast" id="toast">
|
|
672
|
+
<span id="toast-icon">✓</span>
|
|
673
|
+
<span id="toast-text">Action completed</span>
|
|
406
674
|
</div>
|
|
407
675
|
|
|
408
676
|
<div class="resize-grip" id="resize-grip" title="Drag to resize">
|
|
@@ -427,21 +695,62 @@
|
|
|
427
695
|
var writeCount = 0, readCount = 0, errCount = 0;
|
|
428
696
|
var totalLogEvents = 0;
|
|
429
697
|
var polling = true;
|
|
698
|
+
var activeLogFilter = 'all';
|
|
699
|
+
|
|
700
|
+
var logEl = document.getElementById("log");
|
|
701
|
+
var logEmptyEl = document.getElementById("log-empty");
|
|
702
|
+
var dotEl = document.getElementById("dot");
|
|
703
|
+
var statusEl = document.getElementById("status-text");
|
|
704
|
+
var statusPill = document.getElementById("status-pill");
|
|
705
|
+
var logCountEl = document.getElementById("log-count");
|
|
706
|
+
var docDispEl = document.getElementById("doc-display");
|
|
707
|
+
var speedEl = document.getElementById("speed-display");
|
|
708
|
+
var ramEl = document.getElementById("ram-display");
|
|
709
|
+
var reindexBtn = document.getElementById("reindex-btn");
|
|
710
|
+
var reindexBtnLabel = document.getElementById("reindex-btn-label");
|
|
711
|
+
|
|
712
|
+
var selCardEl = document.getElementById("selection-card");
|
|
713
|
+
var selIconEl = document.getElementById("selection-icon");
|
|
714
|
+
var selNameEl = document.getElementById("selection-name");
|
|
715
|
+
var selMetaEl = document.getElementById("selection-meta");
|
|
716
|
+
var selActionsEl = document.getElementById("selection-actions");
|
|
717
|
+
var selBadgeEl = document.getElementById("selection-badge");
|
|
718
|
+
var snapshotBtn = document.getElementById("snapshot-btn");
|
|
719
|
+
var toastEl = document.getElementById("toast");
|
|
720
|
+
var toastTextEl = document.getElementById("toast-text");
|
|
721
|
+
var toastIconEl = document.getElementById("toast-icon");
|
|
722
|
+
|
|
723
|
+
var currentSelectionCount = 0;
|
|
724
|
+
var currentSelectionId = null;
|
|
725
|
+
var currentSelectionName = "";
|
|
726
|
+
|
|
727
|
+
function showToast(msg, icon) {
|
|
728
|
+
if (!toastEl) return;
|
|
729
|
+
if (toastTextEl) toastTextEl.textContent = msg;
|
|
730
|
+
if (toastIconEl) toastIconEl.textContent = icon || "✓";
|
|
731
|
+
toastEl.classList.add("show");
|
|
732
|
+
setTimeout(function() {
|
|
733
|
+
toastEl.classList.remove("show");
|
|
734
|
+
}, 2200);
|
|
735
|
+
}
|
|
430
736
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
737
|
+
function setLogFilter(filter, el) {
|
|
738
|
+
activeLogFilter = filter;
|
|
739
|
+
var tabs = document.querySelectorAll(".t-tab");
|
|
740
|
+
tabs.forEach(function(t) { t.classList.remove("active"); });
|
|
741
|
+
if (el) el.classList.add("active");
|
|
742
|
+
|
|
743
|
+
var lines = logEl.querySelectorAll(".log-line");
|
|
744
|
+
lines.forEach(function(line) {
|
|
745
|
+
if (filter === 'all') {
|
|
746
|
+
line.style.display = "flex";
|
|
747
|
+
} else if (filter === 'writes') {
|
|
748
|
+
line.style.display = line.dataset.type === 'write' ? "flex" : "none";
|
|
749
|
+
} else if (filter === 'reads') {
|
|
750
|
+
line.style.display = line.dataset.type === 'read' ? "flex" : "none";
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
}
|
|
445
754
|
|
|
446
755
|
async function fetchServerStats() {
|
|
447
756
|
if (!everConnected) return;
|
|
@@ -460,13 +769,42 @@
|
|
|
460
769
|
function triggerManualReindex() {
|
|
461
770
|
if (!everConnected) {
|
|
462
771
|
log("Cannot index: Server is offline", "err");
|
|
772
|
+
showSetupGuide();
|
|
463
773
|
return;
|
|
464
774
|
}
|
|
465
|
-
if (
|
|
775
|
+
if (reindexBtn) reindexBtn.classList.add("indexing");
|
|
776
|
+
showToast("Syncing canvas index…", "⚡");
|
|
466
777
|
log("⚡ Manual re-index triggered…", "info");
|
|
467
778
|
parent.postMessage({ pluginMessage: { type: "manual-reindex" } }, "*");
|
|
468
779
|
}
|
|
469
780
|
|
|
781
|
+
function focusSelection() {
|
|
782
|
+
if (currentSelectionCount > 0) {
|
|
783
|
+
parent.postMessage({ pluginMessage: { type: "zoom-to-selection" } }, "*");
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function exportSelectionImage(event) {
|
|
788
|
+
if (event) event.stopPropagation();
|
|
789
|
+
if (currentSelectionCount === 0) return;
|
|
790
|
+
if (snapshotBtn) snapshotBtn.style.opacity = "0.5";
|
|
791
|
+
parent.postMessage({ pluginMessage: { type: "export-selection-image", scale: 2 } }, "*");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function copySelectionId(event) {
|
|
795
|
+
if (event) event.stopPropagation();
|
|
796
|
+
if (!currentSelectionId) return;
|
|
797
|
+
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
798
|
+
navigator.clipboard.writeText(currentSelectionId).then(function() {
|
|
799
|
+
showToast("Node ID: " + currentSelectionId + " copied", "📋");
|
|
800
|
+
}).catch(function() {
|
|
801
|
+
prompt("Copy Node ID:", currentSelectionId);
|
|
802
|
+
});
|
|
803
|
+
} else {
|
|
804
|
+
prompt("Copy Node ID:", currentSelectionId);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
470
808
|
var READ_OPS = [
|
|
471
809
|
"get_selection","get_design","get_page_nodes","screenshot",
|
|
472
810
|
"get_styles","get_local_components","get_viewport","get_variables",
|
|
@@ -474,33 +812,22 @@
|
|
|
474
812
|
"index_scan"
|
|
475
813
|
];
|
|
476
814
|
|
|
477
|
-
function setStatus(state, text
|
|
478
|
-
dotEl.className = "dot " + state;
|
|
815
|
+
function setStatus(state, text) {
|
|
816
|
+
dotEl.className = "dot " + (state === "green" ? "green" : (state === "yellow" ? "yellow" : "red"));
|
|
479
817
|
statusEl.textContent = text;
|
|
480
|
-
if (mode && modeEl) modeEl.textContent = mode;
|
|
481
818
|
|
|
482
819
|
statusPill.className = "status-pill";
|
|
483
|
-
if (state === "green")
|
|
484
|
-
if (state === "red")
|
|
485
|
-
|
|
486
|
-
if (launchBtn && launchBtnLabel) {
|
|
487
|
-
launchBtn.className = "btn";
|
|
488
|
-
if (state === "green") {
|
|
489
|
-
launchBtn.classList.add("btn-live");
|
|
490
|
-
launchBtnLabel.textContent = "● Live";
|
|
491
|
-
} else {
|
|
492
|
-
launchBtn.classList.add("btn-primary");
|
|
493
|
-
launchBtnLabel.textContent = state === "yellow" ? "Connecting…" : "Launch Server";
|
|
494
|
-
}
|
|
495
|
-
}
|
|
820
|
+
if (state === "green") statusPill.classList.add("connected");
|
|
821
|
+
if (state === "red") statusPill.classList.add("offline");
|
|
822
|
+
if (state === "yellow") statusPill.classList.add("connecting");
|
|
496
823
|
}
|
|
497
824
|
|
|
498
|
-
function
|
|
499
|
-
if (wsConnected) {
|
|
500
|
-
|
|
501
|
-
|
|
825
|
+
function handleStatusClick() {
|
|
826
|
+
if (!wsConnected) {
|
|
827
|
+
showSetupGuide();
|
|
828
|
+
} else {
|
|
829
|
+
showToast("Bridge active on port " + currentPort, "⚡");
|
|
502
830
|
}
|
|
503
|
-
showSetupGuide();
|
|
504
831
|
}
|
|
505
832
|
|
|
506
833
|
function showSetupGuide() {
|
|
@@ -511,41 +838,41 @@
|
|
|
511
838
|
overlay.id = "setup-overlay";
|
|
512
839
|
Object.assign(overlay.style, {
|
|
513
840
|
position: "fixed", inset: "0",
|
|
514
|
-
background: "rgba(9,
|
|
515
|
-
backdropFilter: "blur(
|
|
841
|
+
background: "rgba(9,10,15,0.96)",
|
|
842
|
+
backdropFilter: "blur(8px)",
|
|
516
843
|
zIndex: "999",
|
|
517
844
|
display: "flex", flexDirection: "column",
|
|
518
845
|
alignItems: "center", justifyContent: "center",
|
|
519
|
-
padding: "
|
|
520
|
-
fontFamily: "-
|
|
846
|
+
padding: "16px", gap: "10px",
|
|
847
|
+
fontFamily: "var(--font-ui)"
|
|
521
848
|
});
|
|
522
849
|
|
|
523
850
|
overlay.innerHTML = [
|
|
524
|
-
'<div style="width:
|
|
851
|
+
'<div style="width:38px;height:38px;border-radius:9px;background:linear-gradient(135deg,#4f46e5,#06b6d4);display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 1px rgba(99,102,241,0.4),0 8px 24px rgba(79,70,229,0.4);margin-bottom:2px">',
|
|
525
852
|
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
|
|
526
853
|
'</div>',
|
|
527
|
-
'<div style="font-size:
|
|
528
|
-
'<div style="font-size:
|
|
854
|
+
'<div style="font-size:13px;font-weight:700;color:#f8fafc;letter-spacing:-0.2px">Start Figma MCP Server</div>',
|
|
855
|
+
'<div style="font-size:10px;color:#94a3b8;text-align:center;max-width:240px;line-height:1.5">Run command in your project terminal:</div>',
|
|
529
856
|
|
|
530
|
-
'<div style="width:100%;background:#
|
|
531
|
-
'<div style="font-size:
|
|
532
|
-
'<div style="font-family:\'SF Mono\',monospace;font-size:
|
|
857
|
+
'<div style="width:100%;background:#0f1118;border:1px solid rgba(255,255,255,0.08);border-radius:8px;padding:9px 11px;display:flex;flex-direction:column;gap:3px">',
|
|
858
|
+
'<div style="font-size:8px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#475569">Option A · Run once</div>',
|
|
859
|
+
'<div style="font-family:\'SF Mono\',monospace;font-size:11.5px;font-weight:600;color:#38bdf8;user-select:text">npx figma-mcp</div>',
|
|
533
860
|
'</div>',
|
|
534
861
|
|
|
535
|
-
'<div style="width:100%;background:#
|
|
536
|
-
'<div style="font-size:
|
|
537
|
-
'<div style="font-family:\'SF Mono\',monospace;font-size:
|
|
538
|
-
'<div style="font-size:
|
|
862
|
+
'<div style="width:100%;background:#0f1118;border:1px solid rgba(16,185,129,0.2);border-radius:8px;padding:9px 11px;display:flex;flex-direction:column;gap:3px">',
|
|
863
|
+
'<div style="font-size:8px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#475569">Option B · Auto-start ✦ Recommended</div>',
|
|
864
|
+
'<div style="font-family:\'SF Mono\',monospace;font-size:11.5px;font-weight:600;color:#10b981;user-select:text">npx figma-mcp --install-service</div>',
|
|
865
|
+
'<div style="font-size:8.5px;color:#475569">Background service for Claude / IDE</div>',
|
|
539
866
|
'</div>',
|
|
540
867
|
|
|
541
|
-
'<div style="display:flex;align-items:center;gap:6px;font-size:
|
|
542
|
-
'<div style="width:5px;height:5px;border-radius:50%;background:#f59e0b;animation:
|
|
868
|
+
'<div style="display:flex;align-items:center;gap:6px;font-size:9px;color:#475569">',
|
|
869
|
+
'<div style="width:5px;height:5px;border-radius:50%;background:#f59e0b;animation:pulse 1.4s ease-in-out infinite"></div>',
|
|
543
870
|
'Plugin auto-connects once server is detected',
|
|
544
871
|
'</div>',
|
|
545
872
|
|
|
546
873
|
'<button onclick="document.getElementById(\'setup-overlay\').remove();reconnect();" style="',
|
|
547
|
-
'margin-top:4px;padding:
|
|
548
|
-
'background:rgba(255,255,255,0.
|
|
874
|
+
'margin-top:4px;padding:6px 18px;border-radius:6px;border:1px solid rgba(255,255,255,0.12);',
|
|
875
|
+
'background:rgba(255,255,255,0.06);color:#f8fafc;font-size:10px;font-weight:600;cursor:pointer;',
|
|
549
876
|
'font-family:inherit;letter-spacing:0.1px',
|
|
550
877
|
'">Dismiss & Retry</button>'
|
|
551
878
|
].join("");
|
|
@@ -571,30 +898,46 @@
|
|
|
571
898
|
}, 500);
|
|
572
899
|
}
|
|
573
900
|
|
|
574
|
-
function log(msg, cls) {
|
|
901
|
+
function log(msg, cls, opType) {
|
|
575
902
|
totalLogEvents++;
|
|
576
|
-
if (logCountEl) logCountEl.textContent = totalLogEvents + "
|
|
903
|
+
if (logCountEl) logCountEl.textContent = totalLogEvents + " ev";
|
|
577
904
|
|
|
578
905
|
if (logEmptyEl) logEmptyEl.style.display = "none";
|
|
579
906
|
|
|
907
|
+
var wasNearBottom = (logEl.scrollHeight - logEl.clientHeight - logEl.scrollTop) <= 45;
|
|
908
|
+
|
|
580
909
|
var line = document.createElement("div");
|
|
581
910
|
line.className = "log-line";
|
|
911
|
+
if (opType) line.dataset.type = opType;
|
|
582
912
|
|
|
583
913
|
var ts = document.createElement("span");
|
|
584
914
|
ts.className = "log-ts";
|
|
585
915
|
ts.textContent = new Date().toLocaleTimeString("en", { hour12: false });
|
|
586
916
|
|
|
917
|
+
var prompt = document.createElement("span");
|
|
918
|
+
prompt.className = "log-prompt";
|
|
919
|
+
prompt.textContent = "❯";
|
|
920
|
+
|
|
587
921
|
var content = document.createElement("span");
|
|
588
922
|
content.className = "log-msg" + (cls ? " " + cls : "");
|
|
589
923
|
content.textContent = msg;
|
|
590
924
|
|
|
591
925
|
line.appendChild(ts);
|
|
926
|
+
line.appendChild(prompt);
|
|
592
927
|
line.appendChild(content);
|
|
928
|
+
|
|
929
|
+
if (activeLogFilter === 'writes' && opType !== 'write') {
|
|
930
|
+
line.style.display = "none";
|
|
931
|
+
} else if (activeLogFilter === 'reads' && opType !== 'read') {
|
|
932
|
+
line.style.display = "none";
|
|
933
|
+
}
|
|
934
|
+
|
|
593
935
|
logEl.appendChild(line);
|
|
594
|
-
logEl.scrollTop = logEl.scrollHeight;
|
|
595
936
|
|
|
596
|
-
|
|
597
|
-
|
|
937
|
+
if (wasNearBottom) {
|
|
938
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
939
|
+
}
|
|
940
|
+
|
|
598
941
|
if (logEl.children.length > 90) {
|
|
599
942
|
var keep = Array.from(logEl.children).slice(-70);
|
|
600
943
|
logEl.replaceChildren.apply(logEl, keep);
|
|
@@ -604,11 +947,11 @@
|
|
|
604
947
|
function clearLog() {
|
|
605
948
|
logEl.innerHTML = "";
|
|
606
949
|
totalLogEvents = 0;
|
|
607
|
-
if (logCountEl) logCountEl.textContent = "0
|
|
950
|
+
if (logCountEl) logCountEl.textContent = "0 ev";
|
|
608
951
|
var empty = document.createElement("div");
|
|
609
952
|
empty.id = "log-empty";
|
|
610
953
|
empty.className = "terminal-empty";
|
|
611
|
-
empty.innerHTML = '<div class="terminal-empty-icon">◌</div><div class="terminal-empty-text">
|
|
954
|
+
empty.innerHTML = '<div class="terminal-empty-icon">◌</div><div class="terminal-empty-text">Listening on MCP bridge…</div>';
|
|
612
955
|
logEl.appendChild(empty);
|
|
613
956
|
logEmptyEl = empty;
|
|
614
957
|
}
|
|
@@ -626,13 +969,11 @@
|
|
|
626
969
|
var isLongPolling = false;
|
|
627
970
|
var activeAbortController = null;
|
|
628
971
|
var isFirstPoll = true;
|
|
629
|
-
|
|
630
972
|
var retryTimer = null;
|
|
631
973
|
|
|
632
974
|
async function initConnection() {
|
|
633
975
|
if (wsConnected) return;
|
|
634
976
|
|
|
635
|
-
// 1. Instant Health Check on localhost (< 2ms)
|
|
636
977
|
try {
|
|
637
978
|
var res = await fetch("http://localhost:" + currentPort + "/health", { signal: AbortSignal.timeout(600) });
|
|
638
979
|
if (res.ok) {
|
|
@@ -640,13 +981,12 @@
|
|
|
640
981
|
everConnected = true;
|
|
641
982
|
consecutiveErrors = 0;
|
|
642
983
|
lastSuccessfulPollAt = Date.now();
|
|
643
|
-
setStatus("green", "Connected"
|
|
984
|
+
setStatus("green", "Connected");
|
|
644
985
|
connectWs();
|
|
645
986
|
return;
|
|
646
987
|
}
|
|
647
988
|
} catch(e) {}
|
|
648
989
|
|
|
649
|
-
// 2. Parallel port scan 38451-38455 — all probes fire simultaneously
|
|
650
990
|
var scanPorts = [38451, 38452, 38453, 38454, 38455].filter(function(p) { return p !== currentPort; });
|
|
651
991
|
try {
|
|
652
992
|
var winner = await Promise.any(scanPorts.map(function(p) {
|
|
@@ -655,18 +995,15 @@
|
|
|
655
995
|
}));
|
|
656
996
|
currentPort = winner;
|
|
657
997
|
BRIDGE = "http://localhost:" + winner;
|
|
658
|
-
var pDisp = document.getElementById("port-display");
|
|
659
|
-
if (pDisp) pDisp.textContent = winner;
|
|
660
998
|
everConnected = true;
|
|
661
999
|
consecutiveErrors = 0;
|
|
662
1000
|
lastSuccessfulPollAt = Date.now();
|
|
663
|
-
setStatus("green", "Connected"
|
|
1001
|
+
setStatus("green", "Connected");
|
|
664
1002
|
connectWs();
|
|
665
1003
|
return;
|
|
666
|
-
} catch(_) {
|
|
1004
|
+
} catch(_) {}
|
|
667
1005
|
|
|
668
|
-
|
|
669
|
-
setStatus("yellow", "Waiting for server…");
|
|
1006
|
+
setStatus("yellow", "Waiting…");
|
|
670
1007
|
if (retryTimer) clearTimeout(retryTimer);
|
|
671
1008
|
retryTimer = setTimeout(function() {
|
|
672
1009
|
if (!wsConnected && polling) {
|
|
@@ -703,8 +1040,8 @@
|
|
|
703
1040
|
everConnected = true;
|
|
704
1041
|
if (retryTimer) clearTimeout(retryTimer);
|
|
705
1042
|
lastSuccessfulPollAt = Date.now();
|
|
706
|
-
setStatus("green", "Connected"
|
|
707
|
-
log("
|
|
1043
|
+
setStatus("green", "Connected");
|
|
1044
|
+
log("Connected to Figma MCP · port " + currentPort, "ok");
|
|
708
1045
|
|
|
709
1046
|
if (pingTimer) clearInterval(pingTimer);
|
|
710
1047
|
pingTimer = setInterval(function() {
|
|
@@ -720,7 +1057,8 @@
|
|
|
720
1057
|
try {
|
|
721
1058
|
var req = JSON.parse(event.data);
|
|
722
1059
|
if (req && req.operation) {
|
|
723
|
-
|
|
1060
|
+
var isRead = READ_OPS.indexOf(req.operation) >= 0;
|
|
1061
|
+
log("→ " + req.operation, isRead ? "info" : "ok", isRead ? "read" : "write");
|
|
724
1062
|
try { ws.send(JSON.stringify({ type: "ack", id: req.id })); } catch(e) {}
|
|
725
1063
|
dispatchToMain(req);
|
|
726
1064
|
}
|
|
@@ -739,7 +1077,6 @@
|
|
|
739
1077
|
if (pingTimer) clearInterval(pingTimer);
|
|
740
1078
|
wsConnected = false;
|
|
741
1079
|
if (polling) {
|
|
742
|
-
// Attempt long-poll fallback and schedule retry
|
|
743
1080
|
startLongPoll();
|
|
744
1081
|
if (retryTimer) clearTimeout(retryTimer);
|
|
745
1082
|
retryTimer = setTimeout(function() {
|
|
@@ -791,7 +1128,7 @@
|
|
|
791
1128
|
var body = await res.json();
|
|
792
1129
|
var reqs = body.requests || [];
|
|
793
1130
|
receivedThisRound = reqs.length;
|
|
794
|
-
setStatus("green", reqs.length ? reqs.length + " cmd" : "Connected"
|
|
1131
|
+
setStatus("green", reqs.length ? reqs.length + " cmd" : "Connected");
|
|
795
1132
|
|
|
796
1133
|
consecutiveErrors = 0;
|
|
797
1134
|
everConnected = true;
|
|
@@ -799,7 +1136,8 @@
|
|
|
799
1136
|
|
|
800
1137
|
for (var i = 0; i < reqs.length; i++) {
|
|
801
1138
|
var req = reqs[i];
|
|
802
|
-
|
|
1139
|
+
var isRead = READ_OPS.indexOf(req.operation) >= 0;
|
|
1140
|
+
log("→ " + req.operation, isRead ? "info" : "ok", isRead ? "read" : "write");
|
|
803
1141
|
dispatchToMain(req);
|
|
804
1142
|
}
|
|
805
1143
|
} catch (e) {
|
|
@@ -816,9 +1154,6 @@
|
|
|
816
1154
|
await sleep(backoff);
|
|
817
1155
|
continue;
|
|
818
1156
|
}
|
|
819
|
-
// Re-poll immediately after a round that carried work: the next op in
|
|
820
|
-
// a read chain is usually already queued, and sleeping here added
|
|
821
|
-
// POLL_MS of dead time to every single round-trip.
|
|
822
1157
|
if (!receivedThisRound) await sleep(POLL_MS);
|
|
823
1158
|
}
|
|
824
1159
|
} finally {
|
|
@@ -826,9 +1161,6 @@
|
|
|
826
1161
|
}
|
|
827
1162
|
}
|
|
828
1163
|
|
|
829
|
-
// dataJson is already-serialized JSON produced by the main thread, so it is
|
|
830
|
-
// spliced into the body as-is — a multi-MB design tree is never re-encoded
|
|
831
|
-
// here. Older builds of code.js send a plain `data` object instead.
|
|
832
1164
|
function buildResponseBody(id, success, dataJson, error) {
|
|
833
1165
|
return '{"id":' + JSON.stringify(id) +
|
|
834
1166
|
',"success":' + (success ? "true" : "false") +
|
|
@@ -847,7 +1179,6 @@
|
|
|
847
1179
|
} catch(e) {}
|
|
848
1180
|
}
|
|
849
1181
|
|
|
850
|
-
// HTTP fallback
|
|
851
1182
|
try {
|
|
852
1183
|
await fetch(BRIDGE + "/response", {
|
|
853
1184
|
method: "POST",
|
|
@@ -881,59 +1212,86 @@
|
|
|
881
1212
|
}, 100);
|
|
882
1213
|
}
|
|
883
1214
|
|
|
884
|
-
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
reconnect();
|
|
1215
|
+
function sendActivePing() {
|
|
1216
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1217
|
+
try {
|
|
1218
|
+
ws.send(JSON.stringify({ type: "ping", active: true, sessionId: sessionId, fileName: fileName }));
|
|
1219
|
+
} catch(e) {}
|
|
890
1220
|
}
|
|
891
|
-
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
window.addEventListener("focus", sendActivePing);
|
|
1224
|
+
document.addEventListener("pointerdown", sendActivePing);
|
|
892
1225
|
|
|
893
|
-
|
|
1226
|
+
document.addEventListener("visibilitychange", function() {
|
|
1227
|
+
if (document.visibilityState !== "visible") return;
|
|
1228
|
+
sendActivePing();
|
|
894
1229
|
if (!everConnected) return;
|
|
895
1230
|
var idleMs = Date.now() - lastSuccessfulPollAt;
|
|
896
1231
|
if (!wsConnected || consecutiveErrors > 0 || activeAbortController || idleMs > 20000) {
|
|
897
|
-
log("
|
|
1232
|
+
log("Focus sync (" + Math.round(idleMs / 1000) + "s)", "info");
|
|
898
1233
|
reconnect();
|
|
899
1234
|
}
|
|
900
1235
|
});
|
|
901
1236
|
|
|
902
|
-
function
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
alert("Invalid port number");
|
|
908
|
-
return;
|
|
1237
|
+
function getIconForType(type) {
|
|
1238
|
+
if (!type) return '<rect x="3" y="3" width="18" height="18" rx="2"/>';
|
|
1239
|
+
var t = type.toUpperCase();
|
|
1240
|
+
if (t === "FRAME" || t === "SECTION") {
|
|
1241
|
+
return '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M15 3v18M3 9h18M3 15h18"/>';
|
|
909
1242
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1243
|
+
if (t === "COMPONENT" || t === "INSTANCE") {
|
|
1244
|
+
return '<path d="M12 2L2 12l10 10 10-10L12 2z"/>';
|
|
1245
|
+
}
|
|
1246
|
+
if (t === "TEXT") {
|
|
1247
|
+
return '<polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/>';
|
|
1248
|
+
}
|
|
1249
|
+
if (t === "GROUP") {
|
|
1250
|
+
return '<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>';
|
|
1251
|
+
}
|
|
1252
|
+
return '<rect x="3" y="3" width="18" height="18" rx="2"/>';
|
|
919
1253
|
}
|
|
920
1254
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1255
|
+
function renderSelection(msg) {
|
|
1256
|
+
if (!selCardEl || !selNameEl || !selMetaEl || !selBadgeEl || !selIconEl) return;
|
|
1257
|
+
|
|
1258
|
+
var count = msg.count || 0;
|
|
1259
|
+
var list = msg.selection || [];
|
|
1260
|
+
currentSelectionCount = count;
|
|
1261
|
+
currentSelectionId = (count === 1 && list[0]) ? list[0].id : null;
|
|
1262
|
+
currentSelectionName = (count === 1 && list[0]) ? (list[0].name || "Layer") : "";
|
|
1263
|
+
|
|
1264
|
+
if (count === 0) {
|
|
1265
|
+
selCardEl.classList.remove("has-selection");
|
|
1266
|
+
selIconEl.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/></svg>';
|
|
1267
|
+
selNameEl.textContent = "No selection";
|
|
1268
|
+
selMetaEl.textContent = msg.pageName ? "Page: " + msg.pageName : (fileName || "Canvas root");
|
|
1269
|
+
if (selActionsEl) selActionsEl.style.display = "none";
|
|
1270
|
+
selCardEl.title = "No layer selected in Figma canvas";
|
|
1271
|
+
} else if (count === 1) {
|
|
1272
|
+
var node = list[0];
|
|
1273
|
+
selCardEl.classList.add("has-selection");
|
|
1274
|
+
selIconEl.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + getIconForType(node.type) + '</svg>';
|
|
1275
|
+
selNameEl.textContent = node.name || "Untitled Node";
|
|
1276
|
+
|
|
1277
|
+
var metaParts = [];
|
|
1278
|
+
if (node.width && node.height) metaParts.push(node.width + " × " + node.height);
|
|
1279
|
+
if (msg.pageName) metaParts.push(msg.pageName);
|
|
1280
|
+
selMetaEl.textContent = metaParts.length ? metaParts.join(" · ") : (node.type || "Layer");
|
|
1281
|
+
|
|
1282
|
+
selBadgeEl.textContent = node.type || "NODE";
|
|
1283
|
+
if (selActionsEl) selActionsEl.style.display = "flex";
|
|
1284
|
+
selCardEl.title = "Click to center view on: " + (node.name || "Node") + " (" + (node.type || "NODE") + " ID: " + node.id + ")";
|
|
1285
|
+
} else {
|
|
1286
|
+
selCardEl.classList.add("has-selection");
|
|
1287
|
+
selIconEl.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>';
|
|
1288
|
+
var names = list.slice(0, 2).map(function(n) { return n.name; }).join(", ");
|
|
1289
|
+
selNameEl.textContent = count + " layers selected";
|
|
1290
|
+
selMetaEl.textContent = names + (count > 2 ? " +" + (count - 2) + " more" : "");
|
|
1291
|
+
selBadgeEl.textContent = count + " ITEMS";
|
|
1292
|
+
if (selActionsEl) selActionsEl.style.display = "flex";
|
|
1293
|
+
selCardEl.title = "Click to center view on " + count + " selected layers";
|
|
934
1294
|
}
|
|
935
|
-
log("Not found on 38451-38460 → Reset 38451", "err");
|
|
936
|
-
setPort(38451);
|
|
937
1295
|
}
|
|
938
1296
|
|
|
939
1297
|
window.onmessage = async function(event) {
|
|
@@ -944,8 +1302,9 @@
|
|
|
944
1302
|
var prevSid = sessionId;
|
|
945
1303
|
sessionId = msg.sessionId || null;
|
|
946
1304
|
fileName = msg.fileName || null;
|
|
947
|
-
if (fileName &&
|
|
948
|
-
|
|
1305
|
+
if (fileName && docDispEl) {
|
|
1306
|
+
docDispEl.textContent = fileName;
|
|
1307
|
+
docDispEl.title = fileName + " (" + (sessionId || "active session") + ")";
|
|
949
1308
|
}
|
|
950
1309
|
if (sessionId) log("Linked: " + (fileName || sessionId), "info");
|
|
951
1310
|
if (sessionId && sessionId !== prevSid) {
|
|
@@ -955,14 +1314,53 @@
|
|
|
955
1314
|
}
|
|
956
1315
|
|
|
957
1316
|
if (msg.type === "selection-change") {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1317
|
+
renderSelection(msg);
|
|
1318
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1319
|
+
try {
|
|
1320
|
+
ws.send(JSON.stringify({
|
|
1321
|
+
type: "selection-change",
|
|
1322
|
+
sessionId: sessionId,
|
|
1323
|
+
count: msg.count,
|
|
1324
|
+
pageName: msg.pageName,
|
|
1325
|
+
selection: msg.selection,
|
|
1326
|
+
fullNode: msg.fullNode
|
|
1327
|
+
}));
|
|
1328
|
+
} catch(e) {}
|
|
1329
|
+
}
|
|
1330
|
+
sendActivePing();
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (msg.type === "delta-diff") {
|
|
1335
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1336
|
+
try {
|
|
1337
|
+
ws.send(JSON.stringify({
|
|
1338
|
+
type: "delta-diff",
|
|
1339
|
+
sessionId: sessionId,
|
|
1340
|
+
id: msg.id,
|
|
1341
|
+
diff: msg.diff
|
|
1342
|
+
}));
|
|
1343
|
+
} catch(e) {}
|
|
1344
|
+
}
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (msg.type === "selection-image-exported") {
|
|
1349
|
+
if (snapshotBtn) snapshotBtn.style.opacity = "1";
|
|
1350
|
+
if (msg.bytes && msg.bytes.length > 0) {
|
|
1351
|
+
try {
|
|
1352
|
+
var blob = new Blob([new Uint8Array(msg.bytes)], { type: "image/png" });
|
|
1353
|
+
if (navigator && navigator.clipboard && navigator.clipboard.write) {
|
|
1354
|
+
await navigator.clipboard.write([
|
|
1355
|
+
new ClipboardItem({ "image/png": blob })
|
|
1356
|
+
]);
|
|
1357
|
+
showToast("PNG Copied to Clipboard (2x)", "📸");
|
|
1358
|
+
log("📸 Exported PNG for " + (msg.nodeName || "node"), "ok", "read");
|
|
1359
|
+
} else {
|
|
1360
|
+
showToast("Image captured", "📸");
|
|
1361
|
+
}
|
|
1362
|
+
} catch(err) {
|
|
1363
|
+
showToast("Failed to copy image", "✗");
|
|
966
1364
|
}
|
|
967
1365
|
}
|
|
968
1366
|
return;
|
|
@@ -990,7 +1388,9 @@
|
|
|
990
1388
|
}
|
|
991
1389
|
|
|
992
1390
|
if (msg.type === "index-update") {
|
|
993
|
-
if (
|
|
1391
|
+
if (reindexBtn) reindexBtn.classList.remove("indexing");
|
|
1392
|
+
var elapsed = msg.startMs ? (Date.now() - msg.startMs) : 0;
|
|
1393
|
+
showToast("Indexed in " + elapsed + "ms", "✓");
|
|
994
1394
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
995
1395
|
try {
|
|
996
1396
|
ws.send(JSON.stringify({
|
|
@@ -1000,13 +1400,9 @@
|
|
|
1000
1400
|
sessionId: msg.sessionId,
|
|
1001
1401
|
startMs: msg.startMs
|
|
1002
1402
|
}));
|
|
1003
|
-
|
|
1004
|
-
log("⚡ Indexed " + (msg.fileName || "file") + " (" + (Date.now() - msg.startMs) + "ms)", "ok");
|
|
1403
|
+
log("Indexed " + (msg.fileName || "canvas") + " (" + elapsed + "ms)", "ok", "read");
|
|
1005
1404
|
} catch(e) {}
|
|
1006
1405
|
}
|
|
1007
|
-
setTimeout(function() {
|
|
1008
|
-
if (reindexBtnLabel) reindexBtnLabel.textContent = "⚡ Index";
|
|
1009
|
-
}, 3000);
|
|
1010
1406
|
return;
|
|
1011
1407
|
}
|
|
1012
1408
|
|
|
@@ -1023,11 +1419,10 @@
|
|
|
1023
1419
|
writeCount++;
|
|
1024
1420
|
document.getElementById("write-count").textContent = writeCount;
|
|
1025
1421
|
}
|
|
1026
|
-
log("✓ " + (msg.id ? msg.id.slice(-6) : "ok") + durationStr, "ok");
|
|
1422
|
+
log("✓ " + (msg.id ? msg.id.slice(-6) : "ok") + durationStr, "ok", isRead ? "read" : "write");
|
|
1027
1423
|
} else {
|
|
1028
1424
|
errCount++;
|
|
1029
|
-
|
|
1030
|
-
log("✗ " + (msg.id ? msg.id.slice(-6) : "err") + durationStr + ": " + (msg.error || "fail"), "err");
|
|
1425
|
+
log("✗ " + (msg.id ? msg.id.slice(-6) : "err") + durationStr + ": " + (msg.error || "fail"), "err", isRead ? "read" : "write");
|
|
1031
1426
|
}
|
|
1032
1427
|
var payloadJson = msg.dataJson !== undefined
|
|
1033
1428
|
? msg.dataJson
|
|
@@ -1044,7 +1439,6 @@
|
|
|
1044
1439
|
|
|
1045
1440
|
var rafId = null;
|
|
1046
1441
|
var lastW = 0, lastH = 0;
|
|
1047
|
-
var saveTimer = null;
|
|
1048
1442
|
|
|
1049
1443
|
grip.addEventListener("pointerdown", function(e) {
|
|
1050
1444
|
e.preventDefault();
|