figma-rust-mcp 2.8.5 → 2.9.6
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 +1 -1
- package/package.json +1 -1
- package/plugin/code.js +481 -103
- package/plugin/ui.html +1116 -378
- 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,780 @@
|
|
|
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;
|
|
86
99
|
flex-shrink: 0;
|
|
100
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.4);
|
|
87
101
|
position: relative;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ── INDEXING PROGRESS BAR (Embedded in Header) ── */
|
|
105
|
+
.index-progress-wrap {
|
|
106
|
+
display: none;
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
gap: 3px;
|
|
109
|
+
padding: 4px 8px 6px;
|
|
110
|
+
background: rgba(147, 129, 255, 0.08);
|
|
111
|
+
border: 1px solid rgba(147, 129, 255, 0.28);
|
|
112
|
+
border-radius: var(--radius-sm);
|
|
113
|
+
margin-top: 1px;
|
|
114
|
+
animation: fadeIn 0.2s ease-out;
|
|
115
|
+
}
|
|
116
|
+
.index-progress-wrap.active {
|
|
117
|
+
display: flex;
|
|
118
|
+
}
|
|
119
|
+
.index-progress-header {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
justify-content: space-between;
|
|
123
|
+
font-size: 9px;
|
|
124
|
+
font-weight: 600;
|
|
125
|
+
color: var(--ghost-lavender);
|
|
126
|
+
font-family: var(--font-mono);
|
|
127
|
+
}
|
|
128
|
+
.index-progress-label {
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 4px;
|
|
132
|
+
white-space: nowrap;
|
|
88
133
|
overflow: hidden;
|
|
134
|
+
text-overflow: ellipsis;
|
|
135
|
+
max-width: 220px;
|
|
89
136
|
}
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
top: 0; left: 0; right: 0;
|
|
94
|
-
height: 1px;
|
|
95
|
-
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
|
|
137
|
+
.index-progress-label svg {
|
|
138
|
+
animation: spin 1s linear infinite;
|
|
139
|
+
flex-shrink: 0;
|
|
96
140
|
}
|
|
141
|
+
.index-progress-pct {
|
|
142
|
+
color: var(--text-primary);
|
|
143
|
+
flex-shrink: 0;
|
|
144
|
+
}
|
|
145
|
+
.index-progress-track {
|
|
146
|
+
width: 100%;
|
|
147
|
+
height: 3px;
|
|
148
|
+
background: rgba(255, 255, 255, 0.08);
|
|
149
|
+
border-radius: 9999px;
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
}
|
|
152
|
+
.index-progress-fill {
|
|
153
|
+
height: 100%;
|
|
154
|
+
width: 0%;
|
|
155
|
+
background: linear-gradient(90deg, #7c3aed, #9381ff, #34d399);
|
|
156
|
+
border-radius: 9999px;
|
|
157
|
+
transition: width 0.2s ease-out;
|
|
158
|
+
box-shadow: 0 0 8px rgba(147, 129, 255, 0.6);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
|
|
97
162
|
|
|
98
|
-
.brand { display: flex; align-items: center; gap:
|
|
163
|
+
.brand { display: flex; align-items: center; gap: 7px; min-width: 0; }
|
|
99
164
|
|
|
100
165
|
.brand-logo {
|
|
101
|
-
width:
|
|
102
|
-
border-radius:
|
|
103
|
-
background: linear-gradient(135deg, #
|
|
166
|
+
width: 22px; height: 22px;
|
|
167
|
+
border-radius: var(--radius-xs);
|
|
168
|
+
background: linear-gradient(135deg, #7c3aed 0%, #9381ff 60%, #34d399 100%);
|
|
104
169
|
display: flex; align-items: center; justify-content: center;
|
|
105
170
|
flex-shrink: 0;
|
|
106
|
-
box-shadow: 0 0 0 1px rgba(
|
|
171
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.18) inset, 0 2px 8px rgba(124, 58, 237, 0.35);
|
|
172
|
+
color: #ffffff;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.brand-info { display: flex; flex-direction: column; min-width: 0; }
|
|
176
|
+
.brand-title-row { display: flex; align-items: baseline; gap: 5px; }
|
|
177
|
+
.brand-name { font-size: 11px; font-weight: 700; letter-spacing: -0.02em; color: var(--text-primary); line-height: 1; }
|
|
178
|
+
.brand-ver { font-size: 8px; font-weight: 600; color: var(--text-muted); font-family: var(--font-mono); }
|
|
179
|
+
.brand-file {
|
|
180
|
+
font-size: 9px;
|
|
181
|
+
color: var(--text-secondary);
|
|
182
|
+
font-weight: 500;
|
|
183
|
+
line-height: 1.2;
|
|
184
|
+
white-space: nowrap;
|
|
185
|
+
overflow: hidden;
|
|
186
|
+
text-overflow: ellipsis;
|
|
187
|
+
max-width: 140px;
|
|
188
|
+
margin-top: 1px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.header-actions {
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 4px;
|
|
195
|
+
flex-shrink: 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.header-icon-btn {
|
|
199
|
+
width: 22px;
|
|
200
|
+
height: 22px;
|
|
201
|
+
padding: 0;
|
|
202
|
+
border-radius: var(--radius-xs);
|
|
203
|
+
background: var(--surface-card);
|
|
204
|
+
border: 1px solid var(--border-subtle);
|
|
205
|
+
color: var(--text-secondary);
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
justify-content: center;
|
|
209
|
+
cursor: pointer;
|
|
210
|
+
transition: all var(--transition);
|
|
211
|
+
}
|
|
212
|
+
.header-icon-btn:hover {
|
|
213
|
+
background: var(--surface-hover);
|
|
214
|
+
border-color: var(--border-light);
|
|
215
|
+
color: var(--text-primary);
|
|
216
|
+
}
|
|
217
|
+
.header-icon-btn:active { transform: scale(0.94); }
|
|
218
|
+
.header-icon-btn.indexing svg {
|
|
219
|
+
animation: spin 1s linear infinite;
|
|
220
|
+
color: var(--ghost-lavender);
|
|
107
221
|
}
|
|
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
222
|
|
|
113
223
|
.status-pill {
|
|
114
|
-
display: flex; align-items: center; gap:
|
|
115
|
-
background: var(--surface-
|
|
116
|
-
border: 1px solid var(--border-
|
|
117
|
-
border-radius:
|
|
118
|
-
padding:
|
|
119
|
-
font-size:
|
|
120
|
-
color: var(--text-
|
|
121
|
-
transition:
|
|
224
|
+
display: flex; align-items: center; gap: 5px;
|
|
225
|
+
background: var(--surface-card);
|
|
226
|
+
border: 1px solid var(--border-subtle);
|
|
227
|
+
border-radius: var(--radius-full);
|
|
228
|
+
padding: 2.5px 7px 2.5px 5px;
|
|
229
|
+
font-size: 9px; font-weight: 600;
|
|
230
|
+
color: var(--text-secondary);
|
|
231
|
+
transition: all var(--transition);
|
|
232
|
+
cursor: pointer;
|
|
233
|
+
font-family: var(--font-mono);
|
|
122
234
|
}
|
|
123
|
-
.status-pill
|
|
124
|
-
.status-pill.
|
|
235
|
+
.status-pill:hover { border-color: var(--border-light); background: var(--surface-hover); color: var(--text-primary); }
|
|
236
|
+
.status-pill.connected { border-color: rgba(52,211,153,0.25); background: rgba(52,211,153,0.06); color: var(--ghost-mint); }
|
|
237
|
+
.status-pill.offline { border-color: rgba(251,113,133,0.25); background: rgba(251,113,133,0.06); color: var(--ghost-rose); }
|
|
238
|
+
.status-pill.connecting { border-color: rgba(251,191,36,0.25); background: rgba(251,191,36,0.06); color: var(--ghost-amber); }
|
|
125
239
|
|
|
126
240
|
.dot {
|
|
127
|
-
width:
|
|
241
|
+
width: 6px; height: 6px;
|
|
128
242
|
border-radius: 50%; flex-shrink: 0;
|
|
129
|
-
transition:
|
|
243
|
+
transition: all var(--transition);
|
|
130
244
|
}
|
|
131
|
-
.dot.green { background: var(--
|
|
132
|
-
.dot.yellow { background: var(--amber);
|
|
133
|
-
.dot.red { background: var(--rose);
|
|
245
|
+
.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); }
|
|
246
|
+
.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; }
|
|
247
|
+
.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); }
|
|
134
248
|
|
|
135
|
-
@keyframes
|
|
136
|
-
0%, 100% { opacity: 1; }
|
|
137
|
-
50% { opacity: 0.2; }
|
|
138
|
-
}
|
|
249
|
+
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.3; transform: scale(0.85); } }
|
|
139
250
|
|
|
140
|
-
/*
|
|
141
|
-
.
|
|
142
|
-
display:
|
|
143
|
-
|
|
144
|
-
|
|
251
|
+
/* ── SELECTION INSPECTOR (Ghostty Split-Pane Card) ── */
|
|
252
|
+
.selection-card {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
justify-content: space-between;
|
|
256
|
+
gap: 7px;
|
|
257
|
+
background: var(--surface-card);
|
|
258
|
+
border: 1px solid var(--border-subtle);
|
|
259
|
+
border-radius: var(--radius-sm);
|
|
260
|
+
padding: 6px 8px;
|
|
145
261
|
flex-shrink: 0;
|
|
262
|
+
transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
|
|
263
|
+
position: relative;
|
|
264
|
+
overflow: hidden;
|
|
146
265
|
}
|
|
147
|
-
.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
266
|
+
.selection-card::before {
|
|
267
|
+
content: '';
|
|
268
|
+
position: absolute;
|
|
269
|
+
top: 0; left: 0; right: 0; height: 1px;
|
|
270
|
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
|
|
271
|
+
}
|
|
272
|
+
.selection-card.has-selection {
|
|
273
|
+
border-color: var(--border-ghost);
|
|
274
|
+
background: linear-gradient(135deg, rgba(147, 129, 255, 0.05) 0%, rgba(52, 211, 153, 0.02) 100%), var(--surface-card);
|
|
275
|
+
}
|
|
276
|
+
.selection-card.has-selection:hover {
|
|
277
|
+
border-color: rgba(147, 129, 255, 0.55);
|
|
278
|
+
background: linear-gradient(135deg, rgba(147, 129, 255, 0.09) 0%, rgba(52, 211, 153, 0.04) 100%), var(--surface-card);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.selection-main {
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: 7px;
|
|
285
|
+
min-width: 0;
|
|
286
|
+
flex: 1;
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.selection-icon-wrap {
|
|
291
|
+
width: 22px; height: 22px;
|
|
292
|
+
border-radius: var(--radius-xs);
|
|
293
|
+
background: var(--surface-base);
|
|
294
|
+
border: 1px solid var(--border-subtle);
|
|
295
|
+
display: flex; align-items: center; justify-content: center;
|
|
151
296
|
flex-shrink: 0;
|
|
297
|
+
color: var(--text-muted);
|
|
298
|
+
transition: all var(--transition);
|
|
299
|
+
}
|
|
300
|
+
.selection-card.has-selection .selection-icon-wrap {
|
|
301
|
+
background: rgba(147, 129, 255, 0.12);
|
|
302
|
+
border-color: rgba(147, 129, 255, 0.35);
|
|
303
|
+
color: var(--ghost-lavender);
|
|
152
304
|
}
|
|
153
305
|
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
306
|
+
.selection-text { display: flex; flex-direction: column; min-width: 0; }
|
|
307
|
+
.selection-name {
|
|
308
|
+
font-size: 11px;
|
|
309
|
+
font-weight: 600;
|
|
310
|
+
color: var(--text-primary);
|
|
311
|
+
line-height: 1.2;
|
|
312
|
+
white-space: nowrap;
|
|
313
|
+
overflow: hidden;
|
|
314
|
+
text-overflow: ellipsis;
|
|
315
|
+
}
|
|
316
|
+
.selection-meta {
|
|
317
|
+
font-size: 9px;
|
|
318
|
+
color: var(--text-secondary);
|
|
319
|
+
font-family: var(--font-mono);
|
|
320
|
+
line-height: 1.2;
|
|
321
|
+
white-space: nowrap;
|
|
322
|
+
overflow: hidden;
|
|
323
|
+
text-overflow: ellipsis;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.selection-actions {
|
|
327
|
+
display: flex;
|
|
328
|
+
align-items: center;
|
|
329
|
+
gap: 4px;
|
|
330
|
+
flex-shrink: 0;
|
|
161
331
|
}
|
|
162
|
-
.stat-card:hover { border-color: var(--border-2); }
|
|
163
332
|
|
|
164
|
-
.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
333
|
+
.act-btn {
|
|
334
|
+
display: inline-flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
justify-content: center;
|
|
337
|
+
gap: 4px;
|
|
338
|
+
height: 22px;
|
|
339
|
+
padding: 0 6px;
|
|
340
|
+
border-radius: var(--radius-xs);
|
|
341
|
+
font-size: 8.5px;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
font-family: var(--font-mono);
|
|
344
|
+
background: var(--surface-base);
|
|
345
|
+
border: 1px solid var(--border-subtle);
|
|
346
|
+
color: var(--text-secondary);
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
transition: all var(--transition);
|
|
349
|
+
letter-spacing: 0.2px;
|
|
350
|
+
}
|
|
351
|
+
.act-btn:hover {
|
|
352
|
+
background: var(--surface-hover);
|
|
353
|
+
border-color: var(--border-light);
|
|
354
|
+
color: var(--text-primary);
|
|
168
355
|
}
|
|
169
|
-
.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
356
|
+
.act-btn:active { transform: scale(0.95); }
|
|
357
|
+
.act-btn.icon-only { width: 22px; padding: 0; }
|
|
358
|
+
.act-btn.badge-btn {
|
|
359
|
+
background: rgba(147, 129, 255, 0.12);
|
|
360
|
+
border-color: rgba(147, 129, 255, 0.28);
|
|
361
|
+
color: #d1c9ff;
|
|
174
362
|
}
|
|
175
|
-
.
|
|
176
|
-
|
|
363
|
+
.act-btn.badge-btn:hover {
|
|
364
|
+
background: rgba(147, 129, 255, 0.28);
|
|
365
|
+
border-color: rgba(147, 129, 255, 0.6);
|
|
366
|
+
color: #ffffff;
|
|
177
367
|
}
|
|
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
368
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
369
|
+
/* ── TELEMETRY SEGMENTED STATUSLINE ── */
|
|
370
|
+
.telemetry-ribbon {
|
|
371
|
+
display: grid;
|
|
372
|
+
grid-template-columns: repeat(4, 1fr);
|
|
373
|
+
background: var(--surface-base);
|
|
374
|
+
border: 1px solid var(--border-subtle);
|
|
187
375
|
border-radius: var(--radius-sm);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
376
|
+
overflow: hidden;
|
|
377
|
+
flex-shrink: 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.telemetry-cell {
|
|
381
|
+
display: flex;
|
|
382
|
+
flex-direction: column;
|
|
383
|
+
align-items: center;
|
|
191
384
|
justify-content: center;
|
|
192
|
-
|
|
385
|
+
padding: 4px 2px;
|
|
386
|
+
gap: 1px;
|
|
387
|
+
position: relative;
|
|
388
|
+
transition: background var(--transition);
|
|
389
|
+
}
|
|
390
|
+
.telemetry-cell:not(:last-child)::after {
|
|
391
|
+
content: '';
|
|
392
|
+
position: absolute;
|
|
393
|
+
right: 0; top: 15%; bottom: 15%;
|
|
394
|
+
width: 1px;
|
|
395
|
+
background: var(--border-subtle);
|
|
396
|
+
}
|
|
397
|
+
.telemetry-cell:hover { background: rgba(255, 255, 255, 0.02); }
|
|
398
|
+
|
|
399
|
+
.telemetry-label {
|
|
400
|
+
font-size: 7.5px;
|
|
401
|
+
font-weight: 700;
|
|
402
|
+
letter-spacing: 0.6px;
|
|
403
|
+
text-transform: uppercase;
|
|
404
|
+
color: var(--text-muted);
|
|
405
|
+
font-family: var(--font-mono);
|
|
406
|
+
}
|
|
407
|
+
.telemetry-value {
|
|
408
|
+
font-size: 13px;
|
|
409
|
+
font-weight: 700;
|
|
410
|
+
font-family: var(--font-mono);
|
|
411
|
+
line-height: 1;
|
|
412
|
+
font-variant-numeric: tabular-nums;
|
|
413
|
+
display: flex;
|
|
414
|
+
align-items: baseline;
|
|
415
|
+
gap: 1.5px;
|
|
193
416
|
}
|
|
194
|
-
.
|
|
195
|
-
.port-label { font-size: 8px; font-weight: 700; letter-spacing: 0.7px; text-transform: uppercase; color: var(--text-3); }
|
|
196
|
-
.port-value { font-size: 12px; font-weight: 700; font-family: 'SF Mono', 'Fira Code', monospace; color: var(--accent-2); line-height: 1.1; }
|
|
417
|
+
.telemetry-unit { font-size: 7.5px; font-weight: 600; color: var(--text-muted); }
|
|
197
418
|
|
|
198
|
-
|
|
419
|
+
.telemetry-cell.writes .telemetry-value { color: var(--ghost-lavender); }
|
|
420
|
+
.telemetry-cell.reads .telemetry-value { color: var(--ghost-cyan); }
|
|
421
|
+
.telemetry-cell.speed .telemetry-value { color: var(--ghost-mint); }
|
|
422
|
+
.telemetry-cell.ram .telemetry-value { color: var(--ghost-peach); }
|
|
423
|
+
|
|
424
|
+
/* ── GHOSTTY STREAM CONSOLE BUFFER ── */
|
|
199
425
|
.terminal-wrap {
|
|
200
426
|
flex: 1; min-height: 0;
|
|
201
427
|
display: flex; flex-direction: column;
|
|
202
|
-
background: var(--surface-
|
|
203
|
-
border: 1px solid var(--border-
|
|
428
|
+
background: var(--surface-base);
|
|
429
|
+
border: 1px solid var(--border-subtle);
|
|
204
430
|
border-radius: var(--radius-md);
|
|
205
431
|
overflow: hidden;
|
|
432
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
206
433
|
}
|
|
207
434
|
|
|
208
435
|
.terminal-header {
|
|
209
436
|
display: flex; align-items: center; justify-content: space-between;
|
|
210
|
-
padding:
|
|
211
|
-
background:
|
|
212
|
-
border-bottom: 1px solid var(--border-
|
|
437
|
+
padding: 5px 7px;
|
|
438
|
+
background: rgba(255,255,255,0.02);
|
|
439
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
213
440
|
flex-shrink: 0;
|
|
214
441
|
}
|
|
215
442
|
|
|
216
|
-
.terminal-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
443
|
+
.terminal-tabs { display: flex; align-items: center; gap: 3px; }
|
|
444
|
+
|
|
445
|
+
.t-tab {
|
|
446
|
+
font-size: 8.5px;
|
|
447
|
+
font-weight: 600;
|
|
448
|
+
font-family: var(--font-mono);
|
|
449
|
+
letter-spacing: 0.2px;
|
|
450
|
+
padding: 2px 7px;
|
|
451
|
+
border-radius: var(--radius-xs);
|
|
452
|
+
color: var(--text-muted);
|
|
453
|
+
background: transparent;
|
|
454
|
+
border: none;
|
|
455
|
+
cursor: pointer;
|
|
456
|
+
transition: all var(--transition);
|
|
221
457
|
}
|
|
222
|
-
.t-
|
|
458
|
+
.t-tab:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); }
|
|
459
|
+
.t-tab.active { color: var(--text-primary); background: var(--surface-card); box-shadow: 0 1px 2px rgba(0,0,0,0.3); }
|
|
460
|
+
|
|
461
|
+
.terminal-meta { display: flex; align-items: center; gap: 6px; }
|
|
223
462
|
|
|
224
463
|
.event-badge {
|
|
225
|
-
font-size:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
464
|
+
font-size: 8px;
|
|
465
|
+
font-weight: 600;
|
|
466
|
+
font-family: var(--font-mono);
|
|
467
|
+
color: var(--text-muted);
|
|
468
|
+
background: var(--surface-card);
|
|
469
|
+
border: 1px solid var(--border-subtle);
|
|
470
|
+
border-radius: var(--radius-full);
|
|
471
|
+
padding: 1px 6px;
|
|
231
472
|
}
|
|
232
473
|
|
|
233
474
|
.terminal-body {
|
|
234
475
|
flex: 1;
|
|
235
|
-
padding: 8px
|
|
236
|
-
font-family:
|
|
237
|
-
font-size:
|
|
476
|
+
padding: 6px 8px;
|
|
477
|
+
font-family: var(--font-mono);
|
|
478
|
+
font-size: 9.5px;
|
|
479
|
+
line-height: 1.5;
|
|
238
480
|
overflow-y: auto;
|
|
239
|
-
color: var(--text-
|
|
481
|
+
color: var(--text-secondary);
|
|
240
482
|
}
|
|
241
483
|
.terminal-body::-webkit-scrollbar { width: 3px; }
|
|
242
484
|
.terminal-body::-webkit-scrollbar-track { background: transparent; }
|
|
243
|
-
.terminal-body::-webkit-scrollbar-thumb { background: var(--border-
|
|
485
|
+
.terminal-body::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 2px; }
|
|
244
486
|
|
|
245
487
|
.log-line {
|
|
246
|
-
display: flex;
|
|
247
|
-
|
|
488
|
+
display: flex;
|
|
489
|
+
align-items: baseline;
|
|
490
|
+
gap: 6px;
|
|
491
|
+
padding: 1.5px 3px;
|
|
492
|
+
border-radius: 3px;
|
|
493
|
+
transition: background 0.1s ease;
|
|
248
494
|
}
|
|
249
|
-
.log-line:hover { background: rgba(255,255,255,0.
|
|
250
|
-
.log-ts { font-size:
|
|
251
|
-
.log-
|
|
252
|
-
.log-
|
|
253
|
-
.log-
|
|
254
|
-
.log-
|
|
495
|
+
.log-line:hover { background: rgba(255,255,255,0.03); }
|
|
496
|
+
.log-ts { font-size: 8px; color: var(--text-muted); flex-shrink: 0; letter-spacing: -0.2px; }
|
|
497
|
+
.log-prompt { color: var(--ghost-lavender); font-size: 9px; flex-shrink: 0; font-weight: 700; }
|
|
498
|
+
.log-msg { flex: 1; word-break: break-all; color: var(--text-secondary); }
|
|
499
|
+
.log-msg.ok { color: var(--ghost-mint); }
|
|
500
|
+
.log-msg.err { color: var(--ghost-rose); }
|
|
501
|
+
.log-msg.info { color: var(--ghost-cyan); }
|
|
255
502
|
|
|
256
503
|
.terminal-empty {
|
|
257
504
|
display: flex; flex-direction: column;
|
|
258
505
|
align-items: center; justify-content: center;
|
|
259
|
-
height: 100%; gap:
|
|
506
|
+
height: 100%; gap: 5px; color: var(--text-muted);
|
|
260
507
|
}
|
|
261
|
-
.terminal-empty-icon { font-size:
|
|
262
|
-
.terminal-empty-text { font-size:
|
|
508
|
+
.terminal-empty-icon { font-size: 16px; opacity: 0.35; }
|
|
509
|
+
.terminal-empty-text { font-size: 9.5px; opacity: 0.6; font-family: var(--font-mono); }
|
|
263
510
|
|
|
264
|
-
/* FOOTER */
|
|
511
|
+
/* ── FOOTER ACTIONS ── */
|
|
265
512
|
.footer { display: flex; gap: 6px; flex-shrink: 0; }
|
|
266
513
|
|
|
267
514
|
.btn {
|
|
268
515
|
display: inline-flex; align-items: center; justify-content: center;
|
|
269
|
-
gap:
|
|
516
|
+
gap: 6px; padding: 6px 11px;
|
|
270
517
|
border-radius: var(--radius-sm);
|
|
271
|
-
font-size:
|
|
518
|
+
font-size: 10px; font-weight: 600;
|
|
519
|
+
font-family: var(--font-ui);
|
|
272
520
|
cursor: pointer;
|
|
273
|
-
border: 1px solid var(--border-
|
|
274
|
-
background: var(--surface-
|
|
275
|
-
color: var(--text-
|
|
521
|
+
border: 1px solid var(--border-subtle);
|
|
522
|
+
background: var(--surface-card);
|
|
523
|
+
color: var(--text-secondary);
|
|
276
524
|
transition: all var(--transition);
|
|
277
525
|
letter-spacing: 0.1px;
|
|
278
526
|
white-space: nowrap;
|
|
279
527
|
}
|
|
280
|
-
.btn:hover { background: var(--surface-
|
|
281
|
-
.btn:active { transform: scale(0.
|
|
528
|
+
.btn:hover { background: var(--surface-hover); border-color: var(--border-light); color: var(--text-primary); }
|
|
529
|
+
.btn:active { transform: scale(0.97); }
|
|
282
530
|
|
|
283
531
|
.btn-icon { width: 30px; height: 30px; padding: 0; flex-shrink: 0; }
|
|
284
532
|
|
|
285
533
|
.btn-primary {
|
|
286
534
|
flex: 1;
|
|
287
|
-
background: linear-gradient(135deg,
|
|
288
|
-
border-color:
|
|
289
|
-
|
|
535
|
+
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%);
|
|
536
|
+
border-color: rgba(147, 129, 255, 0.45);
|
|
537
|
+
color: #ffffff;
|
|
538
|
+
font-weight: 600;
|
|
539
|
+
box-shadow: 0 1px 0 rgba(255,255,255,0.2) inset, 0 3px 12px rgba(124, 58, 237, 0.28);
|
|
290
540
|
}
|
|
291
541
|
.btn-primary:hover {
|
|
292
|
-
|
|
293
|
-
|
|
542
|
+
background: linear-gradient(135deg, #7c3aed 0%, #9381ff 50%, #34d399 100%);
|
|
543
|
+
border-color: rgba(147, 129, 255, 0.7);
|
|
544
|
+
color: #ffffff;
|
|
545
|
+
box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset, 0 4px 16px rgba(124, 58, 237, 0.45);
|
|
294
546
|
}
|
|
295
547
|
|
|
296
|
-
.btn-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
548
|
+
.btn-primary.indexing svg { animation: spin 1s linear infinite; }
|
|
549
|
+
@keyframes spin { 100% { transform: rotate(360deg); } }
|
|
550
|
+
|
|
551
|
+
/* ── TOAST NOTIFICATION ── */
|
|
552
|
+
.toast {
|
|
553
|
+
position: fixed;
|
|
554
|
+
bottom: 45px;
|
|
555
|
+
left: 50%;
|
|
556
|
+
transform: translateX(-50%) translateY(15px);
|
|
557
|
+
opacity: 0;
|
|
558
|
+
pointer-events: none;
|
|
559
|
+
background: rgba(18, 20, 26, 0.96);
|
|
560
|
+
border: 1px solid rgba(147, 129, 255, 0.3);
|
|
561
|
+
backdrop-filter: blur(8px);
|
|
562
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.6), 0 0 12px rgba(147, 129, 255, 0.2);
|
|
563
|
+
border-radius: var(--radius-full);
|
|
564
|
+
padding: 5px 12px;
|
|
565
|
+
font-size: 9.5px;
|
|
566
|
+
font-weight: 600;
|
|
567
|
+
color: var(--text-primary);
|
|
568
|
+
display: flex;
|
|
569
|
+
align-items: center;
|
|
570
|
+
gap: 6px;
|
|
571
|
+
z-index: 100;
|
|
572
|
+
transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
573
|
+
}
|
|
574
|
+
.toast.show {
|
|
575
|
+
opacity: 1;
|
|
576
|
+
transform: translateX(-50%) translateY(0);
|
|
577
|
+
pointer-events: auto;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* ── CUSTOM IN-PLUGIN MODAL DIALOG (No window.prompt dependency) ── */
|
|
581
|
+
.modal-overlay {
|
|
582
|
+
position: fixed;
|
|
583
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
584
|
+
background: rgba(8, 9, 13, 0.75);
|
|
585
|
+
backdrop-filter: blur(5px);
|
|
586
|
+
display: flex;
|
|
587
|
+
align-items: center;
|
|
588
|
+
justify-content: center;
|
|
589
|
+
padding: 12px;
|
|
590
|
+
z-index: 999;
|
|
591
|
+
opacity: 0;
|
|
592
|
+
pointer-events: none;
|
|
593
|
+
transition: opacity var(--transition);
|
|
594
|
+
}
|
|
595
|
+
.modal-overlay.show {
|
|
596
|
+
opacity: 1;
|
|
597
|
+
pointer-events: auto;
|
|
598
|
+
}
|
|
599
|
+
.modal-card {
|
|
600
|
+
background: var(--surface-card);
|
|
601
|
+
border: 1px solid var(--border-light);
|
|
602
|
+
border-radius: var(--radius-md);
|
|
603
|
+
box-shadow: 0 16px 36px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.08) inset;
|
|
604
|
+
width: 100%;
|
|
605
|
+
max-width: 290px;
|
|
606
|
+
padding: 12px;
|
|
607
|
+
display: flex;
|
|
608
|
+
flex-direction: column;
|
|
609
|
+
gap: 10px;
|
|
610
|
+
transform: scale(0.95);
|
|
611
|
+
transition: transform var(--transition);
|
|
612
|
+
}
|
|
613
|
+
.modal-overlay.show .modal-card {
|
|
614
|
+
transform: scale(1);
|
|
615
|
+
}
|
|
616
|
+
.modal-title-row {
|
|
617
|
+
display: flex;
|
|
618
|
+
align-items: center;
|
|
619
|
+
justify-content: space-between;
|
|
620
|
+
}
|
|
621
|
+
.modal-title {
|
|
622
|
+
font-size: 11px;
|
|
623
|
+
font-weight: 700;
|
|
624
|
+
color: var(--text-primary);
|
|
625
|
+
display: flex;
|
|
626
|
+
align-items: center;
|
|
627
|
+
gap: 5px;
|
|
628
|
+
}
|
|
629
|
+
.modal-desc {
|
|
630
|
+
font-size: 9.5px;
|
|
631
|
+
color: var(--text-secondary);
|
|
632
|
+
line-height: 1.35;
|
|
633
|
+
}
|
|
634
|
+
.modal-input {
|
|
635
|
+
width: 100%;
|
|
636
|
+
background: var(--surface-base);
|
|
637
|
+
border: 1px solid var(--border-light);
|
|
638
|
+
border-radius: var(--radius-xs);
|
|
639
|
+
color: var(--text-primary);
|
|
640
|
+
font-family: var(--font-mono);
|
|
641
|
+
font-size: 9.5px;
|
|
642
|
+
padding: 6px 8px;
|
|
643
|
+
outline: none;
|
|
644
|
+
transition: border-color var(--transition);
|
|
645
|
+
}
|
|
646
|
+
.modal-input:focus {
|
|
647
|
+
border-color: var(--ghost-lavender);
|
|
648
|
+
box-shadow: 0 0 0 2px var(--ghost-glow);
|
|
649
|
+
}
|
|
650
|
+
.modal-actions {
|
|
651
|
+
display: flex;
|
|
652
|
+
justify-content: flex-end;
|
|
653
|
+
gap: 6px;
|
|
654
|
+
margin-top: 2px;
|
|
655
|
+
}
|
|
317
656
|
</style>
|
|
318
657
|
</head>
|
|
319
658
|
<body>
|
|
320
659
|
|
|
321
660
|
<div class="app">
|
|
322
661
|
|
|
662
|
+
<!-- ── HEADER (Ghostty macOS Native Window Chrome) ── -->
|
|
323
663
|
<header class="header">
|
|
324
664
|
<div class="brand">
|
|
325
665
|
<div class="brand-logo">
|
|
326
|
-
|
|
327
|
-
|
|
666
|
+
<!-- Ghostty Ghost Mascot Icon -->
|
|
667
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
668
|
+
<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
669
|
</svg>
|
|
329
670
|
</div>
|
|
330
671
|
<div class="brand-info">
|
|
331
|
-
<
|
|
332
|
-
|
|
672
|
+
<div class="brand-title-row">
|
|
673
|
+
<span class="brand-name">Figma MCP</span>
|
|
674
|
+
<span class="brand-ver">v2.9.6</span>
|
|
675
|
+
</div>
|
|
676
|
+
<span class="brand-file" id="doc-display" title="Active Document">Ready</span>
|
|
333
677
|
</div>
|
|
334
678
|
</div>
|
|
335
679
|
|
|
336
|
-
<div class="
|
|
337
|
-
|
|
338
|
-
<
|
|
339
|
-
|
|
680
|
+
<div class="header-actions">
|
|
681
|
+
<!-- Re-index Icon Button -->
|
|
682
|
+
<button class="header-icon-btn" id="reindex-btn" onclick="triggerManualReindex()" title="Sync Index (Re-index Figma canvas to MCP)">
|
|
683
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
684
|
+
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
|
|
685
|
+
<polyline points="2 17 12 22 22 17"/>
|
|
686
|
+
<polyline points="2 12 12 17 22 12"/>
|
|
687
|
+
</svg>
|
|
688
|
+
</button>
|
|
689
|
+
|
|
690
|
+
<!-- Reconnect / Probe Bridge Port Button -->
|
|
691
|
+
<button class="header-icon-btn" id="reconnect-btn" onclick="reconnect()" title="Reconnect / Probe Bridge Port">
|
|
692
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
693
|
+
<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
694
|
+
</svg>
|
|
695
|
+
</button>
|
|
696
|
+
|
|
697
|
+
<!-- Copy File / Session Link -->
|
|
698
|
+
<button class="header-icon-btn" id="file-link-btn" onclick="copyFileLink(event)" title="🔗 Set / Copy Figma File Link">
|
|
699
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
700
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
701
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
702
|
+
</svg>
|
|
703
|
+
</button>
|
|
704
|
+
|
|
705
|
+
<!-- Connection Status Pill -->
|
|
706
|
+
<div class="status-pill" id="status-pill" onclick="handleStatusClick()" title="Connection Status (Click for details)">
|
|
707
|
+
<div class="dot yellow" id="dot"></div>
|
|
708
|
+
<span id="status-text">Connecting</span>
|
|
709
|
+
</div>
|
|
340
710
|
</div>
|
|
341
711
|
</header>
|
|
342
712
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<span class="
|
|
713
|
+
<!-- ── INDEXING PROGRESS HUD ── -->
|
|
714
|
+
<div class="index-progress-wrap" id="index-progress-wrap">
|
|
715
|
+
<div class="index-progress-header">
|
|
716
|
+
<span class="index-progress-label" id="index-progress-label">
|
|
717
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
718
|
+
<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
719
|
+
</svg>
|
|
720
|
+
<span id="index-progress-text">Indexing canvas layers…</span>
|
|
721
|
+
</span>
|
|
722
|
+
<span class="index-progress-pct" id="index-progress-pct">0%</span>
|
|
723
|
+
</div>
|
|
724
|
+
<div class="index-progress-track">
|
|
725
|
+
<div class="index-progress-fill" id="index-progress-fill"></div>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
|
|
729
|
+
<!-- ── SELECTION INSPECTOR HERO CARD ── -->
|
|
730
|
+
<div class="selection-card" id="selection-card">
|
|
731
|
+
<div class="selection-main" onclick="focusSelection()" title="Click to center view on selection">
|
|
732
|
+
<div class="selection-icon-wrap" id="selection-icon">
|
|
733
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
734
|
+
<circle cx="12" cy="12" r="9"/>
|
|
735
|
+
</svg>
|
|
736
|
+
</div>
|
|
737
|
+
<div class="selection-text">
|
|
738
|
+
<span class="selection-name" id="selection-name">No selection</span>
|
|
739
|
+
<span class="selection-meta" id="selection-meta">Document canvas root</span>
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
|
|
743
|
+
<div class="selection-actions" id="selection-actions" style="display:none;">
|
|
744
|
+
<!-- Snapshot PNG button -->
|
|
745
|
+
<button class="act-btn icon-only" id="snapshot-btn" onclick="exportSelectionImage(event)" title="📸 Copy layer PNG (2x) to clipboard">
|
|
746
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
747
|
+
<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"/>
|
|
748
|
+
<circle cx="12" cy="13" r="4"/>
|
|
749
|
+
</svg>
|
|
750
|
+
</button>
|
|
751
|
+
|
|
752
|
+
<!-- Copy Link to Selection Frame button -->
|
|
753
|
+
<button class="act-btn icon-only" id="link-btn" onclick="copySelectionLink(event)" title="🔗 Copy link to selection frame">
|
|
754
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
755
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
756
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
757
|
+
</svg>
|
|
758
|
+
</button>
|
|
759
|
+
|
|
760
|
+
<!-- Focus / Center on Canvas button -->
|
|
761
|
+
<button class="act-btn icon-only" onclick="focusSelection()" title="🎯 Center view on canvas">
|
|
762
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
763
|
+
<circle cx="12" cy="12" r="10"/>
|
|
764
|
+
<line x1="22" y1="12" x2="18" y2="12"/>
|
|
765
|
+
<line x1="6" y1="12" x2="2" y2="12"/>
|
|
766
|
+
<line x1="12" y1="6" x2="12" y2="2"/>
|
|
767
|
+
<line x1="12" y1="22" x2="12" y2="18"/>
|
|
768
|
+
</svg>
|
|
769
|
+
</button>
|
|
770
|
+
|
|
771
|
+
<!-- Copy Node ID button -->
|
|
772
|
+
<button class="act-btn icon-only" id="copy-id-btn" onclick="copySelectionId(event)" title="📋 Copy Figma Node ID">
|
|
773
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
774
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
775
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
776
|
+
</svg>
|
|
777
|
+
</button>
|
|
778
|
+
|
|
779
|
+
<!-- Node Type Badge button (also clickable to copy ID) -->
|
|
780
|
+
<button class="act-btn badge-btn" id="selection-badge" onclick="copySelectionId(event)" title="📋 Click to copy Figma Node ID">
|
|
781
|
+
FRAME
|
|
782
|
+
</button>
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
|
|
786
|
+
<!-- ── TELEMETRY SEGMENTED RIBBON ── -->
|
|
787
|
+
<div class="telemetry-ribbon">
|
|
788
|
+
<div class="telemetry-cell writes" title="Write operations executed by AI">
|
|
789
|
+
<span class="telemetry-label">Writes</span>
|
|
790
|
+
<span class="telemetry-value" id="write-count">0</span>
|
|
347
791
|
</div>
|
|
348
|
-
<div class="
|
|
349
|
-
<span class="
|
|
350
|
-
<span class="
|
|
792
|
+
<div class="telemetry-cell reads" title="Read / Inspection operations executed">
|
|
793
|
+
<span class="telemetry-label">Reads</span>
|
|
794
|
+
<span class="telemetry-value" id="read-count">0</span>
|
|
351
795
|
</div>
|
|
352
|
-
<div class="
|
|
353
|
-
<span class="
|
|
354
|
-
<span class="
|
|
796
|
+
<div class="telemetry-cell speed" title="Execution latency of last request">
|
|
797
|
+
<span class="telemetry-label">Latency</span>
|
|
798
|
+
<span class="telemetry-value"><span id="speed-display">0</span><span class="telemetry-unit">ms</span></span>
|
|
355
799
|
</div>
|
|
356
|
-
<div class="
|
|
357
|
-
<span class="
|
|
358
|
-
<span class="
|
|
800
|
+
<div class="telemetry-cell ram" title="Rust MCP Server Memory (RAM RSS)">
|
|
801
|
+
<span class="telemetry-label">RAM</span>
|
|
802
|
+
<span class="telemetry-value"><span id="ram-display">3.2</span><span class="telemetry-unit">MB</span></span>
|
|
359
803
|
</div>
|
|
360
804
|
</div>
|
|
361
805
|
|
|
806
|
+
<!-- ── STREAM CONSOLE TERMINAL ── -->
|
|
362
807
|
<section class="terminal-wrap">
|
|
363
808
|
<div class="terminal-header">
|
|
364
|
-
<div class="terminal-
|
|
365
|
-
<
|
|
366
|
-
|
|
809
|
+
<div class="terminal-tabs">
|
|
810
|
+
<button class="t-tab active" onclick="setLogFilter('all', this)">All</button>
|
|
811
|
+
<button class="t-tab" onclick="setLogFilter('writes', this)">Writes</button>
|
|
812
|
+
<button class="t-tab" onclick="setLogFilter('reads', this)">Reads</button>
|
|
813
|
+
</div>
|
|
814
|
+
<div class="terminal-meta">
|
|
815
|
+
<span class="event-badge" id="log-count">0 ev</span>
|
|
816
|
+
<button class="act-btn icon-only" onclick="clearLog()" title="Clear stream console" style="height:18px;width:18px;border-radius:3px;">
|
|
817
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
818
|
+
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
|
|
819
|
+
</svg>
|
|
820
|
+
</button>
|
|
367
821
|
</div>
|
|
368
|
-
<span class="event-badge" id="log-count">0 events</span>
|
|
369
822
|
</div>
|
|
370
823
|
<div class="terminal-body" id="log">
|
|
371
824
|
<div class="terminal-empty" id="log-empty">
|
|
372
825
|
<div class="terminal-empty-icon">◌</div>
|
|
373
|
-
<div class="terminal-empty-text">
|
|
826
|
+
<div class="terminal-empty-text">Listening on MCP bridge…</div>
|
|
374
827
|
</div>
|
|
375
828
|
</div>
|
|
376
829
|
</section>
|
|
377
830
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
<
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
</svg>
|
|
403
|
-
</button>
|
|
404
|
-
</footer>
|
|
831
|
+
</div>
|
|
832
|
+
|
|
833
|
+
<!-- File Key Input Modal Dialog -->
|
|
834
|
+
<div class="modal-overlay" id="key-modal-overlay">
|
|
835
|
+
<div class="modal-card">
|
|
836
|
+
<div class="modal-title-row">
|
|
837
|
+
<div class="modal-title">
|
|
838
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--ghost-lavender)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
839
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
840
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
841
|
+
</svg>
|
|
842
|
+
Set Figma File Link
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
<div class="modal-desc">
|
|
846
|
+
Paste full Figma file URL or File Key to generate real browser links for your layers:
|
|
847
|
+
</div>
|
|
848
|
+
<input type="text" class="modal-input" id="file-key-input" placeholder="https://www.figma.com/design/..." spellcheck="false" autocomplete="off" />
|
|
849
|
+
<div class="modal-actions">
|
|
850
|
+
<button class="btn" onclick="closeKeyModal()">Cancel</button>
|
|
851
|
+
<button class="btn btn-primary" onclick="saveFileKeyFromModal()">Save & Copy</button>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
405
855
|
|
|
856
|
+
<!-- Floating Toast Component -->
|
|
857
|
+
<div class="toast" id="toast">
|
|
858
|
+
<span id="toast-icon">✓</span>
|
|
859
|
+
<span id="toast-text">Action completed</span>
|
|
406
860
|
</div>
|
|
407
861
|
|
|
408
862
|
<div class="resize-grip" id="resize-grip" title="Drag to resize">
|
|
@@ -423,25 +877,72 @@
|
|
|
423
877
|
var lastSuccessfulPollAt = Date.now();
|
|
424
878
|
var sessionId = null;
|
|
425
879
|
var fileName = null;
|
|
880
|
+
var fileKey = null;
|
|
426
881
|
|
|
427
882
|
var writeCount = 0, readCount = 0, errCount = 0;
|
|
428
883
|
var totalLogEvents = 0;
|
|
429
884
|
var polling = true;
|
|
885
|
+
var activeLogFilter = 'all';
|
|
886
|
+
|
|
887
|
+
var logEl = document.getElementById("log");
|
|
888
|
+
var logEmptyEl = document.getElementById("log-empty");
|
|
889
|
+
var dotEl = document.getElementById("dot");
|
|
890
|
+
var statusEl = document.getElementById("status-text");
|
|
891
|
+
var statusPill = document.getElementById("status-pill");
|
|
892
|
+
var logCountEl = document.getElementById("log-count");
|
|
893
|
+
var docDispEl = document.getElementById("doc-display");
|
|
894
|
+
var speedEl = document.getElementById("speed-display");
|
|
895
|
+
var ramEl = document.getElementById("ram-display");
|
|
896
|
+
var reindexBtn = document.getElementById("reindex-btn");
|
|
897
|
+
var reindexBtnLabel = document.getElementById("reindex-btn-label");
|
|
898
|
+
var indexProgressWrap = document.getElementById("index-progress-wrap");
|
|
899
|
+
var indexProgressText = document.getElementById("index-progress-text");
|
|
900
|
+
var indexProgressPct = document.getElementById("index-progress-pct");
|
|
901
|
+
var indexProgressFill = document.getElementById("index-progress-fill");
|
|
902
|
+
|
|
903
|
+
var selCardEl = document.getElementById("selection-card");
|
|
904
|
+
var selIconEl = document.getElementById("selection-icon");
|
|
905
|
+
var selNameEl = document.getElementById("selection-name");
|
|
906
|
+
var selMetaEl = document.getElementById("selection-meta");
|
|
907
|
+
var selActionsEl = document.getElementById("selection-actions");
|
|
908
|
+
var selBadgeEl = document.getElementById("selection-badge");
|
|
909
|
+
var snapshotBtn = document.getElementById("snapshot-btn");
|
|
910
|
+
var toastEl = document.getElementById("toast");
|
|
911
|
+
var toastTextEl = document.getElementById("toast-text");
|
|
912
|
+
var toastIconEl = document.getElementById("toast-icon");
|
|
913
|
+
|
|
914
|
+
var currentSelectionCount = 0;
|
|
915
|
+
var currentSelectionId = null;
|
|
916
|
+
var currentSelectionMainId = null;
|
|
917
|
+
var currentSelectionName = "";
|
|
918
|
+
|
|
919
|
+
function showToast(msg, icon) {
|
|
920
|
+
if (!toastEl) return;
|
|
921
|
+
if (toastTextEl) toastTextEl.textContent = msg;
|
|
922
|
+
if (toastIconEl) toastIconEl.textContent = icon || "✓";
|
|
923
|
+
toastEl.classList.add("show");
|
|
924
|
+
setTimeout(function() {
|
|
925
|
+
toastEl.classList.remove("show");
|
|
926
|
+
}, 2200);
|
|
927
|
+
}
|
|
430
928
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
929
|
+
function setLogFilter(filter, el) {
|
|
930
|
+
activeLogFilter = filter;
|
|
931
|
+
var tabs = document.querySelectorAll(".t-tab");
|
|
932
|
+
tabs.forEach(function(t) { t.classList.remove("active"); });
|
|
933
|
+
if (el) el.classList.add("active");
|
|
934
|
+
|
|
935
|
+
var lines = logEl.querySelectorAll(".log-line");
|
|
936
|
+
lines.forEach(function(line) {
|
|
937
|
+
if (filter === 'all') {
|
|
938
|
+
line.style.display = "flex";
|
|
939
|
+
} else if (filter === 'writes') {
|
|
940
|
+
line.style.display = line.dataset.type === 'write' ? "flex" : "none";
|
|
941
|
+
} else if (filter === 'reads') {
|
|
942
|
+
line.style.display = line.dataset.type === 'read' ? "flex" : "none";
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
}
|
|
445
946
|
|
|
446
947
|
async function fetchServerStats() {
|
|
447
948
|
if (!everConnected) return;
|
|
@@ -460,13 +961,176 @@
|
|
|
460
961
|
function triggerManualReindex() {
|
|
461
962
|
if (!everConnected) {
|
|
462
963
|
log("Cannot index: Server is offline", "err");
|
|
964
|
+
showSetupGuide();
|
|
463
965
|
return;
|
|
464
966
|
}
|
|
465
|
-
if (
|
|
967
|
+
if (reindexBtn) reindexBtn.classList.add("indexing");
|
|
968
|
+
showToast("Syncing canvas index…", "⚡");
|
|
466
969
|
log("⚡ Manual re-index triggered…", "info");
|
|
467
970
|
parent.postMessage({ pluginMessage: { type: "manual-reindex" } }, "*");
|
|
468
971
|
}
|
|
469
972
|
|
|
973
|
+
function focusSelection() {
|
|
974
|
+
if (currentSelectionCount > 0) {
|
|
975
|
+
parent.postMessage({ pluginMessage: { type: "zoom-to-selection" } }, "*");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function exportSelectionImage(event) {
|
|
980
|
+
if (event) event.stopPropagation();
|
|
981
|
+
if (currentSelectionCount === 0) return;
|
|
982
|
+
if (snapshotBtn) snapshotBtn.style.opacity = "0.5";
|
|
983
|
+
parent.postMessage({ pluginMessage: { type: "export-selection-image", scale: 2 } }, "*");
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function copyTextFallback(text) {
|
|
987
|
+
try {
|
|
988
|
+
var textarea = document.createElement("textarea");
|
|
989
|
+
textarea.value = text;
|
|
990
|
+
textarea.style.position = "fixed";
|
|
991
|
+
textarea.style.left = "-9999px";
|
|
992
|
+
textarea.style.top = "-9999px";
|
|
993
|
+
textarea.style.opacity = "0";
|
|
994
|
+
document.body.appendChild(textarea);
|
|
995
|
+
textarea.focus();
|
|
996
|
+
textarea.select();
|
|
997
|
+
var successful = document.execCommand("copy");
|
|
998
|
+
document.body.removeChild(textarea);
|
|
999
|
+
return successful;
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function openKeyModal(onSuccess) {
|
|
1006
|
+
var modal = document.getElementById("key-modal-overlay");
|
|
1007
|
+
var input = document.getElementById("file-key-input");
|
|
1008
|
+
if (modal && input) {
|
|
1009
|
+
input.value = fileKey ? ("https://www.figma.com/design/" + fileKey) : "";
|
|
1010
|
+
modal.classList.add("show");
|
|
1011
|
+
window.__onKeySaved = onSuccess;
|
|
1012
|
+
setTimeout(function() { input.focus(); input.select(); }, 50);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function closeKeyModal() {
|
|
1017
|
+
var modal = document.getElementById("key-modal-overlay");
|
|
1018
|
+
if (modal) modal.classList.remove("show");
|
|
1019
|
+
window.__onKeySaved = null;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function saveFileKeyFromModal() {
|
|
1023
|
+
var input = document.getElementById("file-key-input");
|
|
1024
|
+
var val = input ? input.value.trim() : "";
|
|
1025
|
+
if (val) {
|
|
1026
|
+
var match = val.match(/(?:file|design)\/([a-zA-Z0-9]+)/);
|
|
1027
|
+
var parsedKey = match ? match[1] : val;
|
|
1028
|
+
fileKey = parsedKey;
|
|
1029
|
+
parent.postMessage({ pluginMessage: { type: "set-file-key", fileKey: parsedKey } }, "*");
|
|
1030
|
+
showToast("File Key saved!", "🔗");
|
|
1031
|
+
}
|
|
1032
|
+
var cb = window.__onKeySaved;
|
|
1033
|
+
closeKeyModal();
|
|
1034
|
+
if (typeof cb === "function") {
|
|
1035
|
+
cb(fileKey);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function copyFileLink(event) {
|
|
1040
|
+
if (event) {
|
|
1041
|
+
event.stopPropagation();
|
|
1042
|
+
event.preventDefault();
|
|
1043
|
+
}
|
|
1044
|
+
openKeyModal(function(savedKey) {
|
|
1045
|
+
if (savedKey) {
|
|
1046
|
+
showToast("File link updated", "🔗");
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function copySelectionLink(event) {
|
|
1052
|
+
if (event) {
|
|
1053
|
+
event.stopPropagation();
|
|
1054
|
+
event.preventDefault();
|
|
1055
|
+
}
|
|
1056
|
+
if (!currentSelectionId) {
|
|
1057
|
+
showToast("No selection active", "⚠️");
|
|
1058
|
+
try { parent.postMessage({ pluginMessage: { type: "notify", message: "⚠️ No frame selected to copy link", error: true } }, "*"); } catch(e) {}
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (!fileKey) {
|
|
1063
|
+
openKeyModal(function(savedKey) {
|
|
1064
|
+
executeCopyLink(savedKey);
|
|
1065
|
+
});
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
executeCopyLink(fileKey);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function executeCopyLink(effectiveKey) {
|
|
1073
|
+
if (!currentSelectionId) return;
|
|
1074
|
+
|
|
1075
|
+
var targetId = currentSelectionMainId || currentSelectionId;
|
|
1076
|
+
var figmaNodeId = targetId.replace(/:/g, "-");
|
|
1077
|
+
|
|
1078
|
+
// Figma web link uses a sanitized slug of the file name (or untitled)
|
|
1079
|
+
var cleanSlug = (fileName || "file")
|
|
1080
|
+
.replace(/[\[\]\(\)\{\}]/g, "")
|
|
1081
|
+
.trim()
|
|
1082
|
+
.replace(/\s+/g, "-");
|
|
1083
|
+
var encodedSlug = encodeURIComponent(cleanSlug);
|
|
1084
|
+
|
|
1085
|
+
var link = "";
|
|
1086
|
+
if (effectiveKey) {
|
|
1087
|
+
link = "https://www.figma.com/design/" + effectiveKey + "/" + encodedSlug + "?node-id=" + figmaNodeId;
|
|
1088
|
+
} else {
|
|
1089
|
+
link = "https://www.figma.com/design/draft/" + encodedSlug + "?node-id=" + figmaNodeId;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
var copied = copyTextFallback(link);
|
|
1093
|
+
if (!copied && navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
1094
|
+
navigator.clipboard.writeText(link).catch(function() {});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
showToast("Link: " + figmaNodeId, "🔗");
|
|
1098
|
+
try {
|
|
1099
|
+
parent.postMessage({
|
|
1100
|
+
pluginMessage: {
|
|
1101
|
+
type: "notify",
|
|
1102
|
+
message: "🔗 Link copied to clipboard! (" + figmaNodeId + ")"
|
|
1103
|
+
}
|
|
1104
|
+
}, "*");
|
|
1105
|
+
} catch(e) {}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function copySelectionId(event) {
|
|
1109
|
+
if (event) {
|
|
1110
|
+
event.stopPropagation();
|
|
1111
|
+
event.preventDefault();
|
|
1112
|
+
}
|
|
1113
|
+
if (!currentSelectionId) return;
|
|
1114
|
+
var copied = copyTextFallback(currentSelectionId);
|
|
1115
|
+
if (!copied && navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
1116
|
+
navigator.clipboard.writeText(currentSelectionId).then(function() {
|
|
1117
|
+
showToast("Node ID: " + currentSelectionId + " copied", "📋");
|
|
1118
|
+
try {
|
|
1119
|
+
parent.postMessage({ pluginMessage: { type: "notify", message: "📋 Node ID: " + currentSelectionId + " copied" } }, "*");
|
|
1120
|
+
} catch(e) {}
|
|
1121
|
+
}).catch(function() {
|
|
1122
|
+
prompt("Copy Node ID:", currentSelectionId);
|
|
1123
|
+
});
|
|
1124
|
+
} else if (copied) {
|
|
1125
|
+
showToast("Node ID: " + currentSelectionId + " copied", "📋");
|
|
1126
|
+
try {
|
|
1127
|
+
parent.postMessage({ pluginMessage: { type: "notify", message: "📋 Node ID: " + currentSelectionId + " copied" } }, "*");
|
|
1128
|
+
} catch(e) {}
|
|
1129
|
+
} else {
|
|
1130
|
+
prompt("Copy Node ID:", currentSelectionId);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
470
1134
|
var READ_OPS = [
|
|
471
1135
|
"get_selection","get_design","get_page_nodes","screenshot",
|
|
472
1136
|
"get_styles","get_local_components","get_viewport","get_variables",
|
|
@@ -474,33 +1138,22 @@
|
|
|
474
1138
|
"index_scan"
|
|
475
1139
|
];
|
|
476
1140
|
|
|
477
|
-
function setStatus(state, text
|
|
478
|
-
dotEl.className = "dot " + state;
|
|
1141
|
+
function setStatus(state, text) {
|
|
1142
|
+
dotEl.className = "dot " + (state === "green" ? "green" : (state === "yellow" ? "yellow" : "red"));
|
|
479
1143
|
statusEl.textContent = text;
|
|
480
|
-
if (mode && modeEl) modeEl.textContent = mode;
|
|
481
1144
|
|
|
482
1145
|
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
|
-
}
|
|
1146
|
+
if (state === "green") statusPill.classList.add("connected");
|
|
1147
|
+
if (state === "red") statusPill.classList.add("offline");
|
|
1148
|
+
if (state === "yellow") statusPill.classList.add("connecting");
|
|
496
1149
|
}
|
|
497
1150
|
|
|
498
|
-
function
|
|
499
|
-
if (wsConnected) {
|
|
500
|
-
|
|
501
|
-
|
|
1151
|
+
function handleStatusClick() {
|
|
1152
|
+
if (!wsConnected) {
|
|
1153
|
+
showSetupGuide();
|
|
1154
|
+
} else {
|
|
1155
|
+
showToast("Bridge active on port " + currentPort, "⚡");
|
|
502
1156
|
}
|
|
503
|
-
showSetupGuide();
|
|
504
1157
|
}
|
|
505
1158
|
|
|
506
1159
|
function showSetupGuide() {
|
|
@@ -511,41 +1164,41 @@
|
|
|
511
1164
|
overlay.id = "setup-overlay";
|
|
512
1165
|
Object.assign(overlay.style, {
|
|
513
1166
|
position: "fixed", inset: "0",
|
|
514
|
-
background: "rgba(9,
|
|
515
|
-
backdropFilter: "blur(
|
|
1167
|
+
background: "rgba(9,10,15,0.96)",
|
|
1168
|
+
backdropFilter: "blur(8px)",
|
|
516
1169
|
zIndex: "999",
|
|
517
1170
|
display: "flex", flexDirection: "column",
|
|
518
1171
|
alignItems: "center", justifyContent: "center",
|
|
519
|
-
padding: "
|
|
520
|
-
fontFamily: "-
|
|
1172
|
+
padding: "16px", gap: "10px",
|
|
1173
|
+
fontFamily: "var(--font-ui)"
|
|
521
1174
|
});
|
|
522
1175
|
|
|
523
1176
|
overlay.innerHTML = [
|
|
524
|
-
'<div style="width:
|
|
1177
|
+
'<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
1178
|
'<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
1179
|
'</div>',
|
|
527
|
-
'<div style="font-size:
|
|
528
|
-
'<div style="font-size:
|
|
1180
|
+
'<div style="font-size:13px;font-weight:700;color:#f8fafc;letter-spacing:-0.2px">Start Figma MCP Server</div>',
|
|
1181
|
+
'<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
1182
|
|
|
530
|
-
'<div style="width:100%;background:#
|
|
531
|
-
'<div style="font-size:
|
|
532
|
-
'<div style="font-family:\'SF Mono\',monospace;font-size:
|
|
1183
|
+
'<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">',
|
|
1184
|
+
'<div style="font-size:8px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#475569">Option A · Run once</div>',
|
|
1185
|
+
'<div style="font-family:\'SF Mono\',monospace;font-size:11.5px;font-weight:600;color:#38bdf8;user-select:text">npx figma-mcp</div>',
|
|
533
1186
|
'</div>',
|
|
534
1187
|
|
|
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:
|
|
1188
|
+
'<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">',
|
|
1189
|
+
'<div style="font-size:8px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#475569">Option B · Auto-start ✦ Recommended</div>',
|
|
1190
|
+
'<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>',
|
|
1191
|
+
'<div style="font-size:8.5px;color:#475569">Background service for Claude / IDE</div>',
|
|
539
1192
|
'</div>',
|
|
540
1193
|
|
|
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:
|
|
1194
|
+
'<div style="display:flex;align-items:center;gap:6px;font-size:9px;color:#475569">',
|
|
1195
|
+
'<div style="width:5px;height:5px;border-radius:50%;background:#f59e0b;animation:pulse 1.4s ease-in-out infinite"></div>',
|
|
543
1196
|
'Plugin auto-connects once server is detected',
|
|
544
1197
|
'</div>',
|
|
545
1198
|
|
|
546
1199
|
'<button onclick="document.getElementById(\'setup-overlay\').remove();reconnect();" style="',
|
|
547
|
-
'margin-top:4px;padding:
|
|
548
|
-
'background:rgba(255,255,255,0.
|
|
1200
|
+
'margin-top:4px;padding:6px 18px;border-radius:6px;border:1px solid rgba(255,255,255,0.12);',
|
|
1201
|
+
'background:rgba(255,255,255,0.06);color:#f8fafc;font-size:10px;font-weight:600;cursor:pointer;',
|
|
549
1202
|
'font-family:inherit;letter-spacing:0.1px',
|
|
550
1203
|
'">Dismiss & Retry</button>'
|
|
551
1204
|
].join("");
|
|
@@ -571,30 +1224,46 @@
|
|
|
571
1224
|
}, 500);
|
|
572
1225
|
}
|
|
573
1226
|
|
|
574
|
-
function log(msg, cls) {
|
|
1227
|
+
function log(msg, cls, opType) {
|
|
575
1228
|
totalLogEvents++;
|
|
576
|
-
if (logCountEl) logCountEl.textContent = totalLogEvents + "
|
|
1229
|
+
if (logCountEl) logCountEl.textContent = totalLogEvents + " ev";
|
|
577
1230
|
|
|
578
1231
|
if (logEmptyEl) logEmptyEl.style.display = "none";
|
|
579
1232
|
|
|
1233
|
+
var wasNearBottom = (logEl.scrollHeight - logEl.clientHeight - logEl.scrollTop) <= 45;
|
|
1234
|
+
|
|
580
1235
|
var line = document.createElement("div");
|
|
581
1236
|
line.className = "log-line";
|
|
1237
|
+
if (opType) line.dataset.type = opType;
|
|
582
1238
|
|
|
583
1239
|
var ts = document.createElement("span");
|
|
584
1240
|
ts.className = "log-ts";
|
|
585
1241
|
ts.textContent = new Date().toLocaleTimeString("en", { hour12: false });
|
|
586
1242
|
|
|
1243
|
+
var prompt = document.createElement("span");
|
|
1244
|
+
prompt.className = "log-prompt";
|
|
1245
|
+
prompt.textContent = "❯";
|
|
1246
|
+
|
|
587
1247
|
var content = document.createElement("span");
|
|
588
1248
|
content.className = "log-msg" + (cls ? " " + cls : "");
|
|
589
1249
|
content.textContent = msg;
|
|
590
1250
|
|
|
591
1251
|
line.appendChild(ts);
|
|
1252
|
+
line.appendChild(prompt);
|
|
592
1253
|
line.appendChild(content);
|
|
1254
|
+
|
|
1255
|
+
if (activeLogFilter === 'writes' && opType !== 'write') {
|
|
1256
|
+
line.style.display = "none";
|
|
1257
|
+
} else if (activeLogFilter === 'reads' && opType !== 'read') {
|
|
1258
|
+
line.style.display = "none";
|
|
1259
|
+
}
|
|
1260
|
+
|
|
593
1261
|
logEl.appendChild(line);
|
|
594
|
-
logEl.scrollTop = logEl.scrollHeight;
|
|
595
1262
|
|
|
596
|
-
|
|
597
|
-
|
|
1263
|
+
if (wasNearBottom) {
|
|
1264
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
598
1267
|
if (logEl.children.length > 90) {
|
|
599
1268
|
var keep = Array.from(logEl.children).slice(-70);
|
|
600
1269
|
logEl.replaceChildren.apply(logEl, keep);
|
|
@@ -604,11 +1273,11 @@
|
|
|
604
1273
|
function clearLog() {
|
|
605
1274
|
logEl.innerHTML = "";
|
|
606
1275
|
totalLogEvents = 0;
|
|
607
|
-
if (logCountEl) logCountEl.textContent = "0
|
|
1276
|
+
if (logCountEl) logCountEl.textContent = "0 ev";
|
|
608
1277
|
var empty = document.createElement("div");
|
|
609
1278
|
empty.id = "log-empty";
|
|
610
1279
|
empty.className = "terminal-empty";
|
|
611
|
-
empty.innerHTML = '<div class="terminal-empty-icon">◌</div><div class="terminal-empty-text">
|
|
1280
|
+
empty.innerHTML = '<div class="terminal-empty-icon">◌</div><div class="terminal-empty-text">Listening on MCP bridge…</div>';
|
|
612
1281
|
logEl.appendChild(empty);
|
|
613
1282
|
logEmptyEl = empty;
|
|
614
1283
|
}
|
|
@@ -626,13 +1295,11 @@
|
|
|
626
1295
|
var isLongPolling = false;
|
|
627
1296
|
var activeAbortController = null;
|
|
628
1297
|
var isFirstPoll = true;
|
|
629
|
-
|
|
630
1298
|
var retryTimer = null;
|
|
631
1299
|
|
|
632
1300
|
async function initConnection() {
|
|
633
1301
|
if (wsConnected) return;
|
|
634
1302
|
|
|
635
|
-
// 1. Instant Health Check on localhost (< 2ms)
|
|
636
1303
|
try {
|
|
637
1304
|
var res = await fetch("http://localhost:" + currentPort + "/health", { signal: AbortSignal.timeout(600) });
|
|
638
1305
|
if (res.ok) {
|
|
@@ -640,13 +1307,12 @@
|
|
|
640
1307
|
everConnected = true;
|
|
641
1308
|
consecutiveErrors = 0;
|
|
642
1309
|
lastSuccessfulPollAt = Date.now();
|
|
643
|
-
setStatus("green", "Connected"
|
|
1310
|
+
setStatus("green", "Connected");
|
|
644
1311
|
connectWs();
|
|
645
1312
|
return;
|
|
646
1313
|
}
|
|
647
1314
|
} catch(e) {}
|
|
648
1315
|
|
|
649
|
-
// 2. Parallel port scan 38451-38455 — all probes fire simultaneously
|
|
650
1316
|
var scanPorts = [38451, 38452, 38453, 38454, 38455].filter(function(p) { return p !== currentPort; });
|
|
651
1317
|
try {
|
|
652
1318
|
var winner = await Promise.any(scanPorts.map(function(p) {
|
|
@@ -655,18 +1321,15 @@
|
|
|
655
1321
|
}));
|
|
656
1322
|
currentPort = winner;
|
|
657
1323
|
BRIDGE = "http://localhost:" + winner;
|
|
658
|
-
var pDisp = document.getElementById("port-display");
|
|
659
|
-
if (pDisp) pDisp.textContent = winner;
|
|
660
1324
|
everConnected = true;
|
|
661
1325
|
consecutiveErrors = 0;
|
|
662
1326
|
lastSuccessfulPollAt = Date.now();
|
|
663
|
-
setStatus("green", "Connected"
|
|
1327
|
+
setStatus("green", "Connected");
|
|
664
1328
|
connectWs();
|
|
665
1329
|
return;
|
|
666
|
-
} catch(_) {
|
|
1330
|
+
} catch(_) {}
|
|
667
1331
|
|
|
668
|
-
|
|
669
|
-
setStatus("yellow", "Waiting for server…");
|
|
1332
|
+
setStatus("yellow", "Waiting…");
|
|
670
1333
|
if (retryTimer) clearTimeout(retryTimer);
|
|
671
1334
|
retryTimer = setTimeout(function() {
|
|
672
1335
|
if (!wsConnected && polling) {
|
|
@@ -703,8 +1366,8 @@
|
|
|
703
1366
|
everConnected = true;
|
|
704
1367
|
if (retryTimer) clearTimeout(retryTimer);
|
|
705
1368
|
lastSuccessfulPollAt = Date.now();
|
|
706
|
-
setStatus("green", "Connected"
|
|
707
|
-
log("
|
|
1369
|
+
setStatus("green", "Connected");
|
|
1370
|
+
log("Connected to Figma MCP · port " + currentPort, "ok");
|
|
708
1371
|
|
|
709
1372
|
if (pingTimer) clearInterval(pingTimer);
|
|
710
1373
|
pingTimer = setInterval(function() {
|
|
@@ -720,7 +1383,8 @@
|
|
|
720
1383
|
try {
|
|
721
1384
|
var req = JSON.parse(event.data);
|
|
722
1385
|
if (req && req.operation) {
|
|
723
|
-
|
|
1386
|
+
var isRead = READ_OPS.indexOf(req.operation) >= 0;
|
|
1387
|
+
log("→ " + req.operation, isRead ? "info" : "ok", isRead ? "read" : "write");
|
|
724
1388
|
try { ws.send(JSON.stringify({ type: "ack", id: req.id })); } catch(e) {}
|
|
725
1389
|
dispatchToMain(req);
|
|
726
1390
|
}
|
|
@@ -739,7 +1403,6 @@
|
|
|
739
1403
|
if (pingTimer) clearInterval(pingTimer);
|
|
740
1404
|
wsConnected = false;
|
|
741
1405
|
if (polling) {
|
|
742
|
-
// Attempt long-poll fallback and schedule retry
|
|
743
1406
|
startLongPoll();
|
|
744
1407
|
if (retryTimer) clearTimeout(retryTimer);
|
|
745
1408
|
retryTimer = setTimeout(function() {
|
|
@@ -791,7 +1454,7 @@
|
|
|
791
1454
|
var body = await res.json();
|
|
792
1455
|
var reqs = body.requests || [];
|
|
793
1456
|
receivedThisRound = reqs.length;
|
|
794
|
-
setStatus("green", reqs.length ? reqs.length + " cmd" : "Connected"
|
|
1457
|
+
setStatus("green", reqs.length ? reqs.length + " cmd" : "Connected");
|
|
795
1458
|
|
|
796
1459
|
consecutiveErrors = 0;
|
|
797
1460
|
everConnected = true;
|
|
@@ -799,7 +1462,8 @@
|
|
|
799
1462
|
|
|
800
1463
|
for (var i = 0; i < reqs.length; i++) {
|
|
801
1464
|
var req = reqs[i];
|
|
802
|
-
|
|
1465
|
+
var isRead = READ_OPS.indexOf(req.operation) >= 0;
|
|
1466
|
+
log("→ " + req.operation, isRead ? "info" : "ok", isRead ? "read" : "write");
|
|
803
1467
|
dispatchToMain(req);
|
|
804
1468
|
}
|
|
805
1469
|
} catch (e) {
|
|
@@ -816,9 +1480,6 @@
|
|
|
816
1480
|
await sleep(backoff);
|
|
817
1481
|
continue;
|
|
818
1482
|
}
|
|
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
1483
|
if (!receivedThisRound) await sleep(POLL_MS);
|
|
823
1484
|
}
|
|
824
1485
|
} finally {
|
|
@@ -826,9 +1487,6 @@
|
|
|
826
1487
|
}
|
|
827
1488
|
}
|
|
828
1489
|
|
|
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
1490
|
function buildResponseBody(id, success, dataJson, error) {
|
|
833
1491
|
return '{"id":' + JSON.stringify(id) +
|
|
834
1492
|
',"success":' + (success ? "true" : "false") +
|
|
@@ -847,7 +1505,6 @@
|
|
|
847
1505
|
} catch(e) {}
|
|
848
1506
|
}
|
|
849
1507
|
|
|
850
|
-
// HTTP fallback
|
|
851
1508
|
try {
|
|
852
1509
|
await fetch(BRIDGE + "/response", {
|
|
853
1510
|
method: "POST",
|
|
@@ -881,59 +1538,87 @@
|
|
|
881
1538
|
}, 100);
|
|
882
1539
|
}
|
|
883
1540
|
|
|
884
|
-
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
reconnect();
|
|
1541
|
+
function sendActivePing() {
|
|
1542
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1543
|
+
try {
|
|
1544
|
+
ws.send(JSON.stringify({ type: "ping", active: true, sessionId: sessionId, fileName: fileName }));
|
|
1545
|
+
} catch(e) {}
|
|
890
1546
|
}
|
|
891
|
-
}
|
|
1547
|
+
}
|
|
892
1548
|
|
|
893
|
-
window.addEventListener("focus",
|
|
1549
|
+
window.addEventListener("focus", sendActivePing);
|
|
1550
|
+
document.addEventListener("pointerdown", sendActivePing);
|
|
1551
|
+
|
|
1552
|
+
document.addEventListener("visibilitychange", function() {
|
|
1553
|
+
if (document.visibilityState !== "visible") return;
|
|
1554
|
+
sendActivePing();
|
|
894
1555
|
if (!everConnected) return;
|
|
895
1556
|
var idleMs = Date.now() - lastSuccessfulPollAt;
|
|
896
1557
|
if (!wsConnected || consecutiveErrors > 0 || activeAbortController || idleMs > 20000) {
|
|
897
|
-
log("
|
|
1558
|
+
log("Focus sync (" + Math.round(idleMs / 1000) + "s)", "info");
|
|
898
1559
|
reconnect();
|
|
899
1560
|
}
|
|
900
1561
|
});
|
|
901
1562
|
|
|
902
|
-
function
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
alert("Invalid port number");
|
|
908
|
-
return;
|
|
1563
|
+
function getIconForType(type) {
|
|
1564
|
+
if (!type) return '<rect x="3" y="3" width="18" height="18" rx="2"/>';
|
|
1565
|
+
var t = type.toUpperCase();
|
|
1566
|
+
if (t === "FRAME" || t === "SECTION") {
|
|
1567
|
+
return '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M15 3v18M3 9h18M3 15h18"/>';
|
|
909
1568
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1569
|
+
if (t === "COMPONENT" || t === "INSTANCE") {
|
|
1570
|
+
return '<path d="M12 2L2 12l10 10 10-10L12 2z"/>';
|
|
1571
|
+
}
|
|
1572
|
+
if (t === "TEXT") {
|
|
1573
|
+
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"/>';
|
|
1574
|
+
}
|
|
1575
|
+
if (t === "GROUP") {
|
|
1576
|
+
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"/>';
|
|
1577
|
+
}
|
|
1578
|
+
return '<rect x="3" y="3" width="18" height="18" rx="2"/>';
|
|
919
1579
|
}
|
|
920
1580
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1581
|
+
function renderSelection(msg) {
|
|
1582
|
+
if (!selCardEl || !selNameEl || !selMetaEl || !selBadgeEl || !selIconEl) return;
|
|
1583
|
+
|
|
1584
|
+
var count = msg.count || 0;
|
|
1585
|
+
var list = msg.selection || [];
|
|
1586
|
+
currentSelectionCount = count;
|
|
1587
|
+
currentSelectionId = (count === 1 && list[0]) ? list[0].id : null;
|
|
1588
|
+
currentSelectionMainId = (count === 1 && list[0]) ? (list[0].mainId || list[0].id) : null;
|
|
1589
|
+
currentSelectionName = (count === 1 && list[0]) ? (list[0].name || "Layer") : "";
|
|
1590
|
+
|
|
1591
|
+
if (count === 0) {
|
|
1592
|
+
selCardEl.classList.remove("has-selection");
|
|
1593
|
+
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>';
|
|
1594
|
+
selNameEl.textContent = "No selection";
|
|
1595
|
+
selMetaEl.textContent = msg.pageName ? "Page: " + msg.pageName : (fileName || "Canvas root");
|
|
1596
|
+
if (selActionsEl) selActionsEl.style.display = "none";
|
|
1597
|
+
selCardEl.title = "No layer selected in Figma canvas";
|
|
1598
|
+
} else if (count === 1) {
|
|
1599
|
+
var node = list[0];
|
|
1600
|
+
selCardEl.classList.add("has-selection");
|
|
1601
|
+
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>';
|
|
1602
|
+
selNameEl.textContent = node.name || "Untitled Node";
|
|
1603
|
+
|
|
1604
|
+
var metaParts = [];
|
|
1605
|
+
if (node.width && node.height) metaParts.push(node.width + " × " + node.height);
|
|
1606
|
+
if (msg.pageName) metaParts.push(msg.pageName);
|
|
1607
|
+
selMetaEl.textContent = metaParts.length ? metaParts.join(" · ") : (node.type || "Layer");
|
|
1608
|
+
|
|
1609
|
+
selBadgeEl.textContent = node.type || "NODE";
|
|
1610
|
+
if (selActionsEl) selActionsEl.style.display = "flex";
|
|
1611
|
+
selCardEl.title = "Click to center view on: " + (node.name || "Node") + " (" + (node.type || "NODE") + " ID: " + node.id + ")";
|
|
1612
|
+
} else {
|
|
1613
|
+
selCardEl.classList.add("has-selection");
|
|
1614
|
+
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>';
|
|
1615
|
+
var names = list.slice(0, 2).map(function(n) { return n.name; }).join(", ");
|
|
1616
|
+
selNameEl.textContent = count + " layers selected";
|
|
1617
|
+
selMetaEl.textContent = names + (count > 2 ? " +" + (count - 2) + " more" : "");
|
|
1618
|
+
selBadgeEl.textContent = count + " ITEMS";
|
|
1619
|
+
if (selActionsEl) selActionsEl.style.display = "flex";
|
|
1620
|
+
selCardEl.title = "Click to center view on " + count + " selected layers";
|
|
934
1621
|
}
|
|
935
|
-
log("Not found on 38451-38460 → Reset 38451", "err");
|
|
936
|
-
setPort(38451);
|
|
937
1622
|
}
|
|
938
1623
|
|
|
939
1624
|
window.onmessage = async function(event) {
|
|
@@ -944,8 +1629,10 @@
|
|
|
944
1629
|
var prevSid = sessionId;
|
|
945
1630
|
sessionId = msg.sessionId || null;
|
|
946
1631
|
fileName = msg.fileName || null;
|
|
947
|
-
|
|
948
|
-
|
|
1632
|
+
fileKey = msg.fileKey || null;
|
|
1633
|
+
if (fileName && docDispEl) {
|
|
1634
|
+
docDispEl.textContent = fileName;
|
|
1635
|
+
docDispEl.title = fileName + " (" + (sessionId || "active session") + ")";
|
|
949
1636
|
}
|
|
950
1637
|
if (sessionId) log("Linked: " + (fileName || sessionId), "info");
|
|
951
1638
|
if (sessionId && sessionId !== prevSid) {
|
|
@@ -955,14 +1642,53 @@
|
|
|
955
1642
|
}
|
|
956
1643
|
|
|
957
1644
|
if (msg.type === "selection-change") {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1645
|
+
renderSelection(msg);
|
|
1646
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1647
|
+
try {
|
|
1648
|
+
ws.send(JSON.stringify({
|
|
1649
|
+
type: "selection-change",
|
|
1650
|
+
sessionId: sessionId,
|
|
1651
|
+
count: msg.count,
|
|
1652
|
+
pageName: msg.pageName,
|
|
1653
|
+
selection: msg.selection,
|
|
1654
|
+
fullNode: msg.fullNode
|
|
1655
|
+
}));
|
|
1656
|
+
} catch(e) {}
|
|
1657
|
+
}
|
|
1658
|
+
sendActivePing();
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (msg.type === "delta-diff") {
|
|
1663
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1664
|
+
try {
|
|
1665
|
+
ws.send(JSON.stringify({
|
|
1666
|
+
type: "delta-diff",
|
|
1667
|
+
sessionId: sessionId,
|
|
1668
|
+
id: msg.id,
|
|
1669
|
+
diff: msg.diff
|
|
1670
|
+
}));
|
|
1671
|
+
} catch(e) {}
|
|
1672
|
+
}
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (msg.type === "selection-image-exported") {
|
|
1677
|
+
if (snapshotBtn) snapshotBtn.style.opacity = "1";
|
|
1678
|
+
if (msg.bytes && msg.bytes.length > 0) {
|
|
1679
|
+
try {
|
|
1680
|
+
var blob = new Blob([new Uint8Array(msg.bytes)], { type: "image/png" });
|
|
1681
|
+
if (navigator && navigator.clipboard && navigator.clipboard.write) {
|
|
1682
|
+
await navigator.clipboard.write([
|
|
1683
|
+
new ClipboardItem({ "image/png": blob })
|
|
1684
|
+
]);
|
|
1685
|
+
showToast("PNG Copied to Clipboard (2x)", "📸");
|
|
1686
|
+
log("📸 Exported PNG for " + (msg.nodeName || "node"), "ok", "read");
|
|
1687
|
+
} else {
|
|
1688
|
+
showToast("Image captured", "📸");
|
|
1689
|
+
}
|
|
1690
|
+
} catch(err) {
|
|
1691
|
+
showToast("Failed to copy image", "✗");
|
|
966
1692
|
}
|
|
967
1693
|
}
|
|
968
1694
|
return;
|
|
@@ -975,6 +1701,15 @@
|
|
|
975
1701
|
return;
|
|
976
1702
|
}
|
|
977
1703
|
|
|
1704
|
+
if (msg.type === "index-progress") {
|
|
1705
|
+
if (reindexBtn) reindexBtn.classList.add("indexing");
|
|
1706
|
+
if (indexProgressWrap) indexProgressWrap.classList.add("active");
|
|
1707
|
+
if (indexProgressText && msg.label) indexProgressText.textContent = msg.label;
|
|
1708
|
+
if (indexProgressPct && msg.percent !== undefined) indexProgressPct.textContent = msg.percent + "%";
|
|
1709
|
+
if (indexProgressFill && msg.percent !== undefined) indexProgressFill.style.width = Math.min(100, Math.max(0, msg.percent)) + "%";
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
978
1713
|
if (msg.type === "index-chunk") {
|
|
979
1714
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
980
1715
|
try { ws.send(JSON.stringify({ type: "index-chunk", nodes: msg.nodes })); } catch(e) {}
|
|
@@ -990,7 +1725,16 @@
|
|
|
990
1725
|
}
|
|
991
1726
|
|
|
992
1727
|
if (msg.type === "index-update") {
|
|
993
|
-
if (
|
|
1728
|
+
if (reindexBtn) reindexBtn.classList.remove("indexing");
|
|
1729
|
+
if (indexProgressPct) indexProgressPct.textContent = "100%";
|
|
1730
|
+
if (indexProgressFill) indexProgressFill.style.width = "100%";
|
|
1731
|
+
if (indexProgressText) indexProgressText.textContent = "Index ready";
|
|
1732
|
+
setTimeout(function() {
|
|
1733
|
+
if (indexProgressWrap) indexProgressWrap.classList.remove("active");
|
|
1734
|
+
}, 1200);
|
|
1735
|
+
|
|
1736
|
+
var elapsed = msg.startMs ? (Date.now() - msg.startMs) : 0;
|
|
1737
|
+
showToast("Indexed in " + elapsed + "ms", "✓");
|
|
994
1738
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
995
1739
|
try {
|
|
996
1740
|
ws.send(JSON.stringify({
|
|
@@ -1000,13 +1744,9 @@
|
|
|
1000
1744
|
sessionId: msg.sessionId,
|
|
1001
1745
|
startMs: msg.startMs
|
|
1002
1746
|
}));
|
|
1003
|
-
|
|
1004
|
-
log("⚡ Indexed " + (msg.fileName || "file") + " (" + (Date.now() - msg.startMs) + "ms)", "ok");
|
|
1747
|
+
log("Indexed " + (msg.fileName || "canvas") + " (" + elapsed + "ms)", "ok", "read");
|
|
1005
1748
|
} catch(e) {}
|
|
1006
1749
|
}
|
|
1007
|
-
setTimeout(function() {
|
|
1008
|
-
if (reindexBtnLabel) reindexBtnLabel.textContent = "⚡ Index";
|
|
1009
|
-
}, 3000);
|
|
1010
1750
|
return;
|
|
1011
1751
|
}
|
|
1012
1752
|
|
|
@@ -1023,11 +1763,10 @@
|
|
|
1023
1763
|
writeCount++;
|
|
1024
1764
|
document.getElementById("write-count").textContent = writeCount;
|
|
1025
1765
|
}
|
|
1026
|
-
log("✓ " + (msg.id ? msg.id.slice(-6) : "ok") + durationStr, "ok");
|
|
1766
|
+
log("✓ " + (msg.id ? msg.id.slice(-6) : "ok") + durationStr, "ok", isRead ? "read" : "write");
|
|
1027
1767
|
} else {
|
|
1028
1768
|
errCount++;
|
|
1029
|
-
|
|
1030
|
-
log("✗ " + (msg.id ? msg.id.slice(-6) : "err") + durationStr + ": " + (msg.error || "fail"), "err");
|
|
1769
|
+
log("✗ " + (msg.id ? msg.id.slice(-6) : "err") + durationStr + ": " + (msg.error || "fail"), "err", isRead ? "read" : "write");
|
|
1031
1770
|
}
|
|
1032
1771
|
var payloadJson = msg.dataJson !== undefined
|
|
1033
1772
|
? msg.dataJson
|
|
@@ -1044,7 +1783,6 @@
|
|
|
1044
1783
|
|
|
1045
1784
|
var rafId = null;
|
|
1046
1785
|
var lastW = 0, lastH = 0;
|
|
1047
|
-
var saveTimer = null;
|
|
1048
1786
|
|
|
1049
1787
|
grip.addEventListener("pointerdown", function(e) {
|
|
1050
1788
|
e.preventDefault();
|