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/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 TOKENS ── */
8
+ /* ── GHOSTTY DESIGN SYSTEM (High-Performance Native Terminal Aesthetic) ── */
9
9
  :root {
10
- --bg: #09090f;
11
- --surface-1: #0f1117;
12
- --surface-2: #141720;
13
- --surface-3: #1a1f2e;
14
- --surface-4: #222844;
15
- --border-1: rgba(255,255,255,0.05);
16
- --border-2: rgba(255,255,255,0.10);
17
- --border-3: rgba(255,255,255,0.16);
18
- --text-1: #f0f2f8;
19
- --text-2: #8b95b0;
20
- --text-3: #4e5873;
21
- --accent: #6366f1;
22
- --accent-2: #06b6d4;
23
- --green: #22d3a0;
24
- --amber: #f59e0b;
25
- --rose: #f43f5e;
26
- --violet: #a78bfa;
27
- --radius-sm: 6px;
28
- --radius-md: 10px;
29
- --radius-lg: 14px;
30
- --transition: 0.18s cubic-bezier(.4,0,.2,1);
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: -apple-system, BlinkMacSystemFont, 'Inter', 'SF Pro Text', sans-serif;
49
+ font-family: var(--font-ui);
38
50
  background: var(--bg);
39
- color: var(--text-1);
51
+ color: var(--text-primary);
40
52
  font-size: 11px;
41
- line-height: 1.5;
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: -60px;
64
+ top: -80px; left: -50px;
52
65
  width: 260px; height: 260px;
53
- background: radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%);
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: -40px;
61
- width: 200px; height: 200px;
62
- background: radial-gradient(circle, rgba(6,182,212,0.06) 0%, transparent 70%);
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: 10px;
74
- gap: 7px;
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-1);
83
- border: 1px solid var(--border-1);
95
+ background: var(--surface-base);
96
+ border: 1px solid var(--border-subtle);
84
97
  border-radius: var(--radius-md);
85
- padding: 8px 10px;
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
- .header::before {
91
- content: '';
92
- position: absolute;
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: 9px; }
163
+ .brand { display: flex; align-items: center; gap: 7px; min-width: 0; }
99
164
 
100
165
  .brand-logo {
101
- width: 26px; height: 26px;
102
- border-radius: 7px;
103
- background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
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(99,102,241,0.3), 0 4px 12px rgba(79,70,229,0.35);
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: 6px;
115
- background: var(--surface-2);
116
- border: 1px solid var(--border-1);
117
- border-radius: 100px;
118
- padding: 4px 10px 4px 7px;
119
- font-size: 10px; font-weight: 600;
120
- color: var(--text-2); letter-spacing: 0.1px;
121
- transition: border-color var(--transition), background var(--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.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); }
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: 7px; height: 7px;
241
+ width: 6px; height: 6px;
128
242
  border-radius: 50%; flex-shrink: 0;
129
- transition: background var(--transition), box-shadow var(--transition);
243
+ transition: all var(--transition);
130
244
  }
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); }
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 blink {
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
- /* STATS ROW */
141
- .stats-row {
142
- display: grid;
143
- grid-template-columns: repeat(4, 1fr);
144
- gap: 5px;
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
- .stats-subrow {
148
- display: grid;
149
- grid-template-columns: 1fr 1fr auto;
150
- gap: 5px;
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
- .stat-card {
155
- background: var(--surface-1);
156
- border: 1px solid var(--border-1);
157
- border-radius: var(--radius-sm);
158
- padding: 6px 7px;
159
- display: flex; flex-direction: column; gap: 2px;
160
- transition: border-color var(--transition);
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
- .stat-label {
165
- font-size: 8px; font-weight: 700;
166
- letter-spacing: 0.7px; text-transform: uppercase;
167
- color: var(--text-3);
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
- .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;
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
- .stat-unit {
176
- font-size: 8.5px; font-weight: 600; color: var(--text-3);
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
- .port-card {
185
- background: var(--surface-1);
186
- border: 1px solid var(--border-1);
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
- padding: 6px 9px;
189
- display: flex; flex-direction: column; gap: 2px;
190
- cursor: pointer;
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
- transition: border-color var(--transition), background var(--transition);
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
- .port-card:hover { border-color: rgba(6,182,212,0.35); background: rgba(6,182,212,0.04); }
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
- /* TERMINAL */
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-1);
203
- border: 1px solid var(--border-1);
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: 6px 10px;
211
- background: var(--surface-2);
212
- border-bottom: 1px solid var(--border-1);
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-title {
217
- display: flex; align-items: center; gap: 6px;
218
- font-size: 9px; font-weight: 700;
219
- letter-spacing: 0.8px; text-transform: uppercase;
220
- color: var(--text-3);
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-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); opacity: 0.5; }
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: 9px; font-weight: 600;
226
- color: var(--text-3);
227
- background: var(--surface-3);
228
- border: 1px solid var(--border-1);
229
- border-radius: 100px;
230
- padding: 1px 7px;
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 10px;
236
- font-family: 'SF Mono', 'JetBrains Mono', 'Fira Code', monospace;
237
- font-size: 10.5px; line-height: 1.6;
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-2);
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-2); border-radius: 2px; }
485
+ .terminal-body::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 2px; }
244
486
 
245
487
  .log-line {
246
- display: flex; align-items: baseline; gap: 8px;
247
- padding: 1px 3px; border-radius: 3px;
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.02); }
250
- .log-ts { font-size: 9px; color: var(--text-3); flex-shrink: 0; letter-spacing: -0.2px; }
251
- .log-msg { flex: 1; word-break: break-all; }
252
- .log-line .ok { color: var(--green); }
253
- .log-line .err { color: var(--rose); }
254
- .log-line .info { color: var(--accent-2); }
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: 6px; color: var(--text-3);
506
+ height: 100%; gap: 5px; color: var(--text-muted);
260
507
  }
261
- .terminal-empty-icon { font-size: 20px; opacity: 0.4; }
262
- .terminal-empty-text { font-size: 10px; opacity: 0.5; }
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: 5px; padding: 7px 11px;
516
+ gap: 6px; padding: 6px 11px;
270
517
  border-radius: var(--radius-sm);
271
- font-size: 10.5px; font-weight: 600;
518
+ font-size: 10px; font-weight: 600;
519
+ font-family: var(--font-ui);
272
520
  cursor: pointer;
273
- border: 1px solid var(--border-2);
274
- background: var(--surface-2);
275
- color: var(--text-2);
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-3); border-color: var(--border-3); color: var(--text-1); }
281
- .btn:active { transform: scale(0.96); }
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, #4f46e5 0%, #06b6d4 100%);
288
- border-color: transparent; color: #fff; font-weight: 700;
289
- box-shadow: 0 1px 0 rgba(255,255,255,0.1) inset, 0 4px 16px rgba(79,70,229,0.3);
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
- opacity: 0.9; border-color: transparent; color: #fff;
293
- box-shadow: 0 1px 0 rgba(255,255,255,0.12) inset, 0 6px 20px rgba(79,70,229,0.4);
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-live {
297
- flex: 1;
298
- background: rgba(34,211,160,0.08);
299
- border-color: rgba(34,211,160,0.25);
300
- color: var(--green);
301
- }
302
- .btn-live:hover { background: rgba(34,211,160,0.12); border-color: rgba(34,211,160,0.4); color: var(--green); }
303
-
304
- /* RESIZE GRIP */
305
- .resize-grip {
306
- position: fixed; right: 0; bottom: 0;
307
- width: 24px; height: 24px;
308
- cursor: nwse-resize; opacity: 0.35;
309
- color: var(--text-2);
310
- transition: opacity var(--transition), color var(--transition);
311
- z-index: 1000;
312
- display: flex; align-items: flex-end; justify-content: flex-end;
313
- padding: 4px;
314
- touch-action: none;
315
- }
316
- .resize-grip:hover { opacity: 1; color: var(--accent-2); }
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
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
327
- <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
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
- <span class="brand-name">Figma MCP</span>
332
- <span class="brand-file" id="session-display">READY</span>
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="status-pill" id="status-pill">
337
- <div class="dot yellow" id="dot"></div>
338
- <span id="status-text">Connecting</span>
339
- <span id="mode-badge" style="display:none;color:var(--text-3)">—</span>
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
- <div class="stats-row">
344
- <div class="stat-card writes" title="Write Operations">
345
- <span class="stat-label">Writes</span>
346
- <span class="stat-value" id="write-count">0</span>
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="stat-card reads" title="Read Operations">
349
- <span class="stat-label">Reads</span>
350
- <span class="stat-value" id="read-count">0</span>
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="stat-card speed" title="Last Request Execution Time / Speed">
353
- <span class="stat-label">Speed</span>
354
- <span class="stat-value"><span id="speed-display">0</span><span class="stat-unit">ms</span></span>
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="stat-card memory" title="Server Memory Usage (RAM RSS)">
357
- <span class="stat-label">RAM</span>
358
- <span class="stat-value"><span id="ram-display">3.2</span><span class="stat-unit">MB</span></span>
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-title">
365
- <div class="t-dot"></div>
366
- Stream Console
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">Waiting for events…</div>
826
+ <div class="terminal-empty-text">Listening on MCP bridge…</div>
374
827
  </div>
375
828
  </div>
376
829
  </section>
377
830
 
378
- <footer class="footer">
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>
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
- var logEl = document.getElementById("log");
432
- var logEmptyEl = document.getElementById("log-empty");
433
- var dotEl = document.getElementById("dot");
434
- var statusEl = document.getElementById("status-text");
435
- var modeEl = document.getElementById("mode-badge");
436
- var logCountEl = document.getElementById("log-count");
437
- var sessionDispEl = document.getElementById("session-display");
438
- var launchBtn = document.getElementById("launch-btn");
439
- var launchBtnLabel = document.getElementById("launch-btn-label");
440
- var statusPill = document.getElementById("status-pill");
441
- var speedEl = document.getElementById("speed-display");
442
- var ramEl = document.getElementById("ram-display");
443
- var reindexBtn = document.getElementById("reindex-btn");
444
- var reindexBtnLabel = document.getElementById("reindex-btn-label");
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 (reindexBtnLabel) reindexBtnLabel.textContent = "Indexing…";
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, mode) {
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") statusPill.classList.add("connected");
484
- if (state === "red") statusPill.classList.add("offline");
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 launchServer() {
499
- if (wsConnected) {
500
- log("Server is already running and connected!", "ok");
501
- return;
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,9,15,0.96)",
515
- backdropFilter: "blur(4px)",
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: "18px", gap: "10px",
520
- fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', sans-serif"
1172
+ padding: "16px", gap: "10px",
1173
+ fontFamily: "var(--font-ui)"
521
1174
  });
522
1175
 
523
1176
  overlay.innerHTML = [
524
- '<div style="width:44px;height:44px;border-radius:12px;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">',
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:14px;font-weight:700;color:#f0f2f8;letter-spacing:-0.2px">Start Figma MCP Server</div>',
528
- '<div style="font-size:10.5px;color:#8b95b0;text-align:center;max-width:230px;line-height:1.6">Run one of these commands in your terminal:</div>',
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:#0f1117;border:1px solid rgba(255,255,255,0.07);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:4px">',
531
- '<div style="font-size:8.5px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#4e5873">Option A · Run once</div>',
532
- '<div style="font-family:\'SF Mono\',monospace;font-size:12px;font-weight:600;color:#38bdf8;user-select:text">npx figma-mcp</div>',
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:#0f1117;border:1px solid rgba(34,211,160,0.15);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:4px">',
536
- '<div style="font-size:8.5px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#4e5873">Option B · Auto-start on login ✦ Recommended</div>',
537
- '<div style="font-family:\'SF Mono\',monospace;font-size:12px;font-weight:600;color:#22d3a0;user-select:text">npx figma-mcp --install-service</div>',
538
- '<div style="font-size:9px;color:#4e5873">Sets up LaunchAgent / systemd / Task Scheduler</div>',
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:9.5px;color:#4e5873">',
542
- '<div style="width:5px;height:5px;border-radius:50%;background:#f59e0b;animation:blink 1.4s ease-in-out infinite"></div>',
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:7px 20px;border-radius:8px;border:1px solid rgba(255,255,255,0.1);',
548
- 'background:rgba(255,255,255,0.05);color:#8b95b0;font-size:10.5px;font-weight:600;cursor:pointer;',
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 + " events";
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
- // Trim log buffer: remove oldest entries in one batch DOM op to avoid
597
- // triggering O(N) reflows from individual removeChild calls.
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 events";
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">Waiting for events…</div>';
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", "ready");
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", "ready");
1327
+ setStatus("green", "Connected");
664
1328
  connectWs();
665
1329
  return;
666
- } catch(_) { /* no server yet */ }
1330
+ } catch(_) {}
667
1331
 
668
- // 3. If server is not yet running, schedule continuous auto-detection every 1.5s
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", "ready");
707
- log("⚡ Connected to Figma MCP · port " + currentPort, "ok");
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
- log("→ " + req.operation, READ_OPS.indexOf(req.operation) >= 0 ? "info" : "ok");
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", body.mode || "ready");
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
- log("→ " + req.operation, READ_OPS.indexOf(req.operation) >= 0 ? "info" : "ok");
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
- document.addEventListener("visibilitychange", function() {
885
- if (document.visibilityState !== "visible" || !everConnected) return;
886
- var idleMs = Date.now() - lastSuccessfulPollAt;
887
- if (!wsConnected || consecutiveErrors > 0 || activeAbortController || idleMs > 20000) {
888
- log("Focus sync (" + Math.round(idleMs / 1000) + "s)", "info");
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", function() {
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("Window focus sync", "info");
1558
+ log("Focus sync (" + Math.round(idleMs / 1000) + "s)", "info");
898
1559
  reconnect();
899
1560
  }
900
1561
  });
901
1562
 
902
- function changePort() {
903
- var input = prompt("MCP Bridge Port:", currentPort);
904
- if (!input) return;
905
- var newPort = parseInt(input, 10);
906
- if (isNaN(newPort) || newPort < 1024 || newPort > 65535) {
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
- setPort(newPort);
911
- }
912
-
913
- function setPort(port) {
914
- currentPort = port;
915
- BRIDGE = "http://localhost:" + currentPort;
916
- document.getElementById("port-display").textContent = currentPort;
917
- log("Port switched → " + currentPort, "info");
918
- reconnect();
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
- async function autoDetectPort() {
922
- setStatus("yellow", "Scanning…");
923
- log("Scanning 38451-38460…", "info");
924
- for (var p = 38451; p <= 38460; p++) {
925
- try {
926
- var res = await fetch("http://localhost:" + p + "/health", { signal: AbortSignal.timeout(400) });
927
- if (res.ok) {
928
- var data = await res.json();
929
- log("Detected port " + p, "ok");
930
- setPort(p);
931
- return;
932
- }
933
- } catch(e) {}
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
- if (fileName && sessionDispEl) {
948
- sessionDispEl.textContent = fileName.length > 16 ? fileName.slice(0, 14) + ".." : fileName;
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
- if (sessionDispEl) {
959
- if (msg.count === 0) {
960
- sessionDispEl.textContent = fileName ? (fileName.length > 16 ? fileName.slice(0, 14) + ".." : fileName) : "READY";
961
- } else if (msg.count === 1) {
962
- var nName = msg.selection[0].name;
963
- sessionDispEl.textContent = "Sel: " + (nName.length > 12 ? nName.slice(0, 10) + ".." : nName);
964
- } else {
965
- sessionDispEl.textContent = msg.count + " layers selected";
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 (reindexBtnLabel) reindexBtnLabel.textContent = "⚡ Indexed";
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
- var nodeCount = (msg.data && msg.data.pageNodes) ? msg.data.pageNodes.length : 0;
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
- document.getElementById("err-count").textContent = errCount;
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();