figma-rust-mcp 3.0.4 → 3.2.1

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
@@ -3,1946 +3,209 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Figma MCP</title>
6
+ <title>Figma MCP Loader</title>
7
7
  <style>
8
- /* ── GHOSTTY DESIGN SYSTEM (High-Performance Native Terminal Aesthetic) ── */
9
8
  :root {
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;
9
+ --bg: #0d0e12;
10
+ --surface-base: #12141a;
11
+ --surface-card: #171922;
12
+ --border-subtle: rgba(255, 255, 255, 0.08);
13
+ --border-ghost: rgba(147, 129, 255, 0.35);
14
+ --text-primary: #f4f4f7;
21
15
  --text-secondary: #9aa1b2;
22
- --text-muted: #52596d;
23
- --text-dim: #363c4e;
24
-
25
- /* Ghostty Signature Palette */
16
+ --text-muted: #52596d;
26
17
  --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);
18
+ --ghost-mint: #34d399;
19
+ --font-ui: -apple-system, BlinkMacSystemFont, 'Inter', 'SF Pro Text', sans-serif;
20
+ --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, monospace;
43
21
  }
44
-
45
22
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
46
- html, body { width: 100%; height: 100%; overflow: hidden; }
23
+ html, body { width: 100%; height: 100%; overflow: hidden; background: var(--bg); color: var(--text-primary); font-family: var(--font-ui); font-size: 11px; }
47
24
 
48
- body {
49
- font-family: var(--font-ui);
50
- background: var(--bg);
51
- color: var(--text-primary);
52
- font-size: 11px;
53
- line-height: 1.4;
54
- -webkit-font-smoothing: antialiased;
55
- user-select: none;
25
+ .loader-container {
56
26
  display: flex;
57
27
  flex-direction: column;
58
- }
59
-
60
- /* Ghostty subtle atmospheric ambient */
61
- body::before {
62
- content: '';
63
- position: fixed;
64
- top: -80px; left: -50px;
65
- width: 260px; height: 260px;
66
- background: radial-gradient(circle, rgba(147, 129, 255, 0.07) 0%, transparent 65%);
67
- pointer-events: none;
68
- z-index: 0;
69
- }
70
- body::after {
71
- content: '';
72
- position: fixed;
73
- bottom: -60px; right: -50px;
74
- width: 220px; height: 220px;
75
- background: radial-gradient(circle, rgba(52, 211, 153, 0.05) 0%, transparent 65%);
76
- pointer-events: none;
77
- z-index: 0;
78
- }
79
-
80
- .app {
81
- position: relative;
82
- z-index: 1;
83
- display: flex;
84
- flex-direction: column;
85
- height: 100%;
86
- padding: 8px;
87
- gap: 6px;
88
- }
89
-
90
- /* ── HEADER (Ghostty macOS Native Window Chrome) ── */
91
- .header {
92
- display: flex;
93
- align-items: center;
94
- justify-content: space-between;
95
- background: var(--surface-base);
96
- border: 1px solid var(--border-subtle);
97
- border-radius: var(--radius-md);
98
- padding: 6px 8px;
99
- flex-shrink: 0;
100
- box-shadow: 0 1px 2px rgba(0,0,0,0.4);
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
28
  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;
133
- overflow: hidden;
134
- text-overflow: ellipsis;
135
- max-width: 220px;
136
- }
137
- .index-progress-label svg {
138
- animation: spin 1s linear infinite;
139
- flex-shrink: 0;
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 {
29
+ justify-content: center;
153
30
  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); } }
162
-
163
- .brand { display: flex; align-items: center; gap: 7px; min-width: 0; }
164
-
165
- .brand-logo {
166
- width: 22px; height: 22px;
167
- border-radius: var(--radius-xs);
168
- background: linear-gradient(135deg, #7c3aed 0%, #9381ff 60%, #34d399 100%);
169
- display: flex; align-items: center; justify-content: center;
170
- flex-shrink: 0;
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;
31
+ padding: 24px;
32
+ text-align: center;
33
+ position: relative;
196
34
  }
197
35
 
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);
36
+ .radar-pulse {
37
+ position: relative;
38
+ width: 54px;
39
+ height: 54px;
40
+ margin-bottom: 20px;
206
41
  display: flex;
207
42
  align-items: center;
208
43
  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
44
  }
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);
221
- }
222
-
223
- .status-pill {
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);
234
- }
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); }
239
-
240
- .dot {
241
- width: 6px; height: 6px;
242
- border-radius: 50%; flex-shrink: 0;
243
- transition: all var(--transition);
244
- }
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); }
248
-
249
- @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.3; transform: scale(0.85); } }
250
-
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;
261
- flex-shrink: 0;
262
- transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
263
- position: relative;
264
- overflow: hidden;
265
- }
266
- .selection-card::before {
267
- content: '';
45
+ .radar-ring {
268
46
  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);
47
+ width: 100%;
48
+ height: 100%;
49
+ border-radius: 50%;
50
+ border: 2px solid var(--ghost-lavender);
51
+ opacity: 0;
52
+ animation: pulse 2s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
279
53
  }
280
-
281
- .selection-main {
282
- display: flex;
283
- align-items: center;
284
- gap: 7px;
285
- min-width: 0;
286
- flex: 1;
287
- cursor: pointer;
54
+ .radar-ring:nth-child(2) { animation-delay: 0.6s; }
55
+ .radar-ring:nth-child(3) { animation-delay: 1.2s; }
56
+ .radar-core {
57
+ width: 14px;
58
+ height: 14px;
59
+ border-radius: 50%;
60
+ background: var(--ghost-lavender);
61
+ box-shadow: 0 0 16px var(--ghost-lavender);
288
62
  }
289
63
 
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;
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);
64
+ @keyframes pulse {
65
+ 0% { transform: scale(0.3); opacity: 0.9; }
66
+ 100% { transform: scale(1.6); opacity: 0; }
304
67
  }
305
68
 
306
- .selection-text { display: flex; flex-direction: column; min-width: 0; }
307
- .selection-name {
308
- font-size: 11px;
69
+ .title {
70
+ font-size: 13px;
309
71
  font-weight: 600;
72
+ letter-spacing: -0.01em;
73
+ margin-bottom: 6px;
310
74
  color: var(--text-primary);
311
- line-height: 1.2;
312
- white-space: nowrap;
313
- overflow: hidden;
314
- text-overflow: ellipsis;
315
75
  }
316
- .selection-meta {
317
- font-size: 9px;
76
+ .status-text {
77
+ font-size: 11px;
318
78
  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;
79
+ margin-bottom: 16px;
80
+ line-height: 1.5;
331
81
  }
332
-
333
- .act-btn {
82
+ .badge {
334
83
  display: inline-flex;
335
84
  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);
355
- }
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;
362
- }
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;
367
- }
368
-
369
- /* ── TELEMETRY SEGMENTED STATUSLINE ── */
370
- .telemetry-ribbon {
371
- display: grid;
372
- grid-template-columns: repeat(4, 1fr);
373
- background: var(--surface-base);
85
+ gap: 6px;
86
+ padding: 4px 10px;
87
+ background: var(--surface-card);
374
88
  border: 1px solid var(--border-subtle);
375
- border-radius: var(--radius-sm);
376
- overflow: hidden;
377
- flex-shrink: 0;
378
- }
379
-
380
- .telemetry-cell {
381
- display: flex;
382
- flex-direction: column;
383
- align-items: center;
384
- justify-content: center;
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;
89
+ border-radius: 9999px;
410
90
  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;
91
+ font-size: 10px;
92
+ color: var(--ghost-mint);
93
+ margin-bottom: 16px;
416
94
  }
417
- .telemetry-unit { font-size: 7.5px; font-weight: 600; color: var(--text-muted); }
418
-
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 ── */
425
- .terminal-wrap {
426
- flex: 1; min-height: 0;
427
- display: flex; flex-direction: column;
95
+ .hint-box {
428
96
  background: var(--surface-base);
429
97
  border: 1px solid var(--border-subtle);
430
- border-radius: var(--radius-md);
431
- overflow: hidden;
432
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
433
- }
434
-
435
- .terminal-header {
436
- display: flex; align-items: center; justify-content: space-between;
437
- padding: 5px 7px;
438
- background: rgba(255,255,255,0.02);
439
- border-bottom: 1px solid var(--border-subtle);
440
- flex-shrink: 0;
441
- }
442
-
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);
98
+ border-radius: 8px;
99
+ padding: 10px 12px;
100
+ max-width: 280px;
101
+ font-size: 10px;
452
102
  color: var(--text-muted);
453
- background: transparent;
454
- border: none;
455
- cursor: pointer;
456
- transition: all var(--transition);
457
- }
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
- .t-tab.tab-err.active { color: var(--ghost-rose); }
461
- .tab-badge-err {
462
- font-size: 7.5px;
463
- font-weight: 700;
464
- color: var(--ghost-rose);
465
- background: rgba(251, 113, 133, 0.18);
466
- border-radius: var(--radius-full);
467
- padding: 0 4px;
468
- margin-left: 2px;
469
- line-height: 1.3;
470
- display: inline-block;
471
- }
472
-
473
- .terminal-meta { display: flex; align-items: center; gap: 6px; }
474
-
475
- .event-badge {
476
- font-size: 8px;
477
- font-weight: 600;
478
- font-family: var(--font-mono);
479
- color: var(--text-muted);
480
- background: var(--surface-card);
481
- border: 1px solid var(--border-subtle);
482
- border-radius: var(--radius-full);
483
- padding: 1px 6px;
484
- }
485
-
486
- .terminal-body {
487
- flex: 1;
488
- padding: 6px 8px;
489
- font-family: var(--font-mono);
490
- font-size: 9.5px;
491
- line-height: 1.5;
492
- overflow-y: auto;
493
- color: var(--text-secondary);
494
- }
495
- .terminal-body::-webkit-scrollbar { width: 3px; }
496
- .terminal-body::-webkit-scrollbar-track { background: transparent; }
497
- .terminal-body::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 2px; }
498
-
499
- .log-line {
500
- display: flex;
501
- align-items: baseline;
502
- gap: 6px;
503
- padding: 1.5px 3px;
504
- border-radius: 3px;
505
- transition: background 0.1s ease;
506
- }
507
- .log-line:hover { background: rgba(255,255,255,0.03); }
508
- .log-ts { font-size: 8px; color: var(--text-muted); flex-shrink: 0; letter-spacing: -0.2px; }
509
- .log-prompt { color: var(--ghost-lavender); font-size: 9px; flex-shrink: 0; font-weight: 700; }
510
- .log-msg { flex: 1; word-break: break-all; color: var(--text-secondary); }
511
- .log-msg.ok { color: var(--ghost-mint); }
512
- .log-msg.err { color: var(--ghost-rose); }
513
- .log-msg.info { color: var(--ghost-cyan); }
514
-
515
- .terminal-empty {
516
- display: flex; flex-direction: column;
517
- align-items: center; justify-content: center;
518
- height: 100%; gap: 5px; color: var(--text-muted);
519
- }
520
- .terminal-empty-icon { font-size: 16px; opacity: 0.35; }
521
- .terminal-empty-text { font-size: 9.5px; opacity: 0.6; font-family: var(--font-mono); }
522
-
523
- /* ── FOOTER ACTIONS ── */
524
- .footer { display: flex; gap: 6px; flex-shrink: 0; }
525
-
526
- .btn {
527
- display: inline-flex; align-items: center; justify-content: center;
528
- gap: 6px; padding: 6px 11px;
529
- border-radius: var(--radius-sm);
530
- font-size: 10px; font-weight: 600;
531
- font-family: var(--font-ui);
532
- cursor: pointer;
533
- border: 1px solid var(--border-subtle);
534
- background: var(--surface-card);
535
- color: var(--text-secondary);
536
- transition: all var(--transition);
537
- letter-spacing: 0.1px;
538
- white-space: nowrap;
539
- }
540
- .btn:hover { background: var(--surface-hover); border-color: var(--border-light); color: var(--text-primary); }
541
- .btn:active { transform: scale(0.97); }
542
-
543
- .btn-icon { width: 30px; height: 30px; padding: 0; flex-shrink: 0; }
544
-
545
- .btn-primary {
546
- flex: 1;
547
- 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%);
548
- border-color: rgba(147, 129, 255, 0.45);
549
- color: #ffffff;
550
- font-weight: 600;
551
- box-shadow: 0 1px 0 rgba(255,255,255,0.2) inset, 0 3px 12px rgba(124, 58, 237, 0.28);
552
- }
553
- .btn-primary:hover {
554
- background: linear-gradient(135deg, #7c3aed 0%, #9381ff 50%, #34d399 100%);
555
- border-color: rgba(147, 129, 255, 0.7);
556
- color: #ffffff;
557
- box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset, 0 4px 16px rgba(124, 58, 237, 0.45);
558
- }
559
-
560
- .btn-primary.indexing svg { animation: spin 1s linear infinite; }
561
- @keyframes spin { 100% { transform: rotate(360deg); } }
562
-
563
- /* ── TOAST NOTIFICATION ── */
564
- .toast {
565
- position: fixed;
566
- bottom: 45px;
567
- left: 50%;
568
- transform: translateX(-50%) translateY(15px);
569
- opacity: 0;
570
- pointer-events: none;
571
- background: rgba(18, 20, 26, 0.96);
572
- border: 1px solid rgba(147, 129, 255, 0.3);
573
- backdrop-filter: blur(8px);
574
- box-shadow: 0 8px 24px rgba(0,0,0,0.6), 0 0 12px rgba(147, 129, 255, 0.2);
575
- border-radius: var(--radius-full);
576
- padding: 5px 12px;
577
- font-size: 9.5px;
578
- font-weight: 600;
579
- color: var(--text-primary);
580
- display: flex;
581
- align-items: center;
582
- gap: 6px;
583
- z-index: 100;
584
- transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
585
- }
586
- .toast.show {
587
- opacity: 1;
588
- transform: translateX(-50%) translateY(0);
589
- pointer-events: auto;
590
- }
591
-
592
- /* ── CUSTOM IN-PLUGIN MODAL DIALOG (No window.prompt dependency) ── */
593
- .modal-overlay {
594
- position: fixed;
595
- top: 0; left: 0; right: 0; bottom: 0;
596
- background: rgba(8, 9, 13, 0.75);
597
- backdrop-filter: blur(5px);
598
- display: flex;
599
- align-items: center;
600
- justify-content: center;
601
- padding: 12px;
602
- z-index: 999;
603
- opacity: 0;
604
- pointer-events: none;
605
- transition: opacity var(--transition);
606
- }
607
- .modal-overlay.show {
608
- opacity: 1;
609
- pointer-events: auto;
610
- }
611
- .modal-card {
612
- background: var(--surface-card);
613
- border: 1px solid var(--border-light);
614
- border-radius: var(--radius-md);
615
- box-shadow: 0 16px 36px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.08) inset;
616
- width: 100%;
617
- max-width: 290px;
618
- padding: 12px;
619
- display: flex;
620
- flex-direction: column;
621
- gap: 10px;
622
- transform: scale(0.95);
623
- transition: transform var(--transition);
624
- }
625
- .modal-overlay.show .modal-card {
626
- transform: scale(1);
627
- }
628
- .modal-title-row {
629
- display: flex;
630
- align-items: center;
631
- justify-content: space-between;
632
- }
633
- .modal-title {
634
- font-size: 11px;
635
- font-weight: 700;
636
- color: var(--text-primary);
637
- display: flex;
638
- align-items: center;
639
- gap: 5px;
640
- }
641
- .modal-desc {
642
- font-size: 9.5px;
643
- color: var(--text-secondary);
644
- line-height: 1.35;
103
+ line-height: 1.4;
104
+ text-align: left;
645
105
  }
646
- .modal-input {
647
- width: 100%;
648
- background: var(--surface-base);
649
- border: 1px solid var(--border-light);
650
- border-radius: var(--radius-xs);
651
- color: var(--text-primary);
106
+ .hint-box code {
652
107
  font-family: var(--font-mono);
653
- font-size: 9.5px;
654
- padding: 6px 8px;
655
- outline: none;
656
- transition: border-color var(--transition);
657
- }
658
- .modal-input:focus {
659
- border-color: var(--ghost-lavender);
660
- box-shadow: 0 0 0 2px var(--ghost-glow);
661
- }
662
- .modal-actions {
663
- display: flex;
664
- justify-content: flex-end;
665
- gap: 6px;
666
- margin-top: 2px;
108
+ color: var(--ghost-lavender);
109
+ display: block;
110
+ margin-top: 4px;
111
+ background: rgba(0,0,0,0.3);
112
+ padding: 3px 6px;
113
+ border-radius: 4px;
667
114
  }
668
115
  </style>
669
116
  </head>
670
117
  <body>
671
-
672
- <div class="app">
673
-
674
- <!-- ── HEADER (Ghostty macOS Native Window Chrome) ── -->
675
- <header class="header">
676
- <div class="brand">
677
- <div class="brand-logo">
678
- <!-- Ghostty Ghost Mascot Icon -->
679
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
680
- <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"/>
681
- </svg>
682
- </div>
683
- <div class="brand-info">
684
- <div class="brand-title-row">
685
- <span class="brand-name">Figma MCP</span>
686
- <span class="brand-ver">v3.0.0</span>
687
- </div>
688
- <span class="brand-file" id="doc-display" title="Active Document">Ready</span>
689
- </div>
690
- </div>
691
-
692
- <div class="header-actions">
693
- <!-- Re-index Icon Button -->
694
- <button class="header-icon-btn" id="reindex-btn" onclick="triggerManualReindex()" title="Sync Index (Re-index Figma canvas to MCP)">
695
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
696
- <polygon points="12 2 2 7 12 12 22 7 12 2"/>
697
- <polyline points="2 17 12 22 22 17"/>
698
- <polyline points="2 12 12 17 22 12"/>
699
- </svg>
700
- </button>
701
-
702
- <!-- Reconnect / Probe Bridge Port Button -->
703
- <button class="header-icon-btn" id="reconnect-btn" onclick="reconnect()" title="Reconnect / Probe Bridge Port">
704
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
705
- <polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
706
- </svg>
707
- </button>
708
-
709
- <!-- Copy File / Session Link -->
710
- <button class="header-icon-btn" id="file-link-btn" onclick="copyFileLink(event)" title="🔗 Set / Copy Figma File Link">
711
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
712
- <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
713
- <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
714
- </svg>
715
- </button>
716
-
717
- <!-- Connection Status Pill -->
718
- <div class="status-pill" id="status-pill" onclick="handleStatusClick()" title="Connection Status (Click for details)">
719
- <div class="dot yellow" id="dot"></div>
720
- <span id="status-text">Connecting</span>
721
- </div>
722
- </div>
723
- </header>
724
-
725
- <!-- ── INDEXING PROGRESS HUD ── -->
726
- <div class="index-progress-wrap" id="index-progress-wrap">
727
- <div class="index-progress-header">
728
- <span class="index-progress-label" id="index-progress-label">
729
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
730
- <polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
731
- </svg>
732
- <span id="index-progress-text">Indexing canvas layers…</span>
733
- </span>
734
- <span class="index-progress-pct" id="index-progress-pct">0%</span>
735
- </div>
736
- <div class="index-progress-track">
737
- <div class="index-progress-fill" id="index-progress-fill"></div>
738
- </div>
739
- </div>
740
-
741
- <!-- ── SELECTION INSPECTOR HERO CARD ── -->
742
- <div class="selection-card" id="selection-card">
743
- <div class="selection-main" onclick="focusSelection()" title="Click to center view on selection">
744
- <div class="selection-icon-wrap" id="selection-icon">
745
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
746
- <circle cx="12" cy="12" r="9"/>
747
- </svg>
748
- </div>
749
- <div class="selection-text">
750
- <span class="selection-name" id="selection-name">No selection</span>
751
- <span class="selection-meta" id="selection-meta">Document canvas root</span>
752
- </div>
753
- </div>
754
-
755
- <div class="selection-actions" id="selection-actions" style="display:none;">
756
- <!-- Snapshot PNG button -->
757
- <button class="act-btn icon-only" id="snapshot-btn" onclick="exportSelectionImage(event)" title="📸 Copy layer PNG (2x) to clipboard">
758
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
759
- <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"/>
760
- <circle cx="12" cy="13" r="4"/>
761
- </svg>
762
- </button>
763
-
764
- <!-- Copy Link to Selection Frame button -->
765
- <button class="act-btn icon-only" id="link-btn" onclick="copySelectionLink(event)" title="🔗 Copy link to selection frame">
766
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
767
- <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
768
- <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
769
- </svg>
770
- </button>
771
-
772
- <!-- Focus / Center on Canvas button -->
773
- <button class="act-btn icon-only" onclick="focusSelection()" title="🎯 Center view on canvas">
774
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
775
- <circle cx="12" cy="12" r="10"/>
776
- <line x1="22" y1="12" x2="18" y2="12"/>
777
- <line x1="6" y1="12" x2="2" y2="12"/>
778
- <line x1="12" y1="6" x2="12" y2="2"/>
779
- <line x1="12" y1="22" x2="12" y2="18"/>
780
- </svg>
781
- </button>
782
-
783
- <!-- Copy Node ID button -->
784
- <button class="act-btn icon-only" id="copy-id-btn" onclick="copySelectionId(event)" title="📋 Copy Figma Node ID">
785
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
786
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
787
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
788
- </svg>
789
- </button>
790
-
791
- <!-- Node Type Badge button (also clickable to copy ID) -->
792
- <button class="act-btn badge-btn" id="selection-badge" onclick="copySelectionId(event)" title="📋 Click to copy Figma Node ID">
793
- FRAME
794
- </button>
795
- </div>
118
+ <div class="loader-container">
119
+ <div class="radar-pulse">
120
+ <div class="radar-ring"></div>
121
+ <div class="radar-ring"></div>
122
+ <div class="radar-ring"></div>
123
+ <div class="radar-core"></div>
796
124
  </div>
797
-
798
- <!-- ── TELEMETRY SEGMENTED RIBBON ── -->
799
- <div class="telemetry-ribbon">
800
- <div class="telemetry-cell writes" title="Write operations executed by AI">
801
- <span class="telemetry-label">Writes</span>
802
- <span class="telemetry-value" id="write-count">0</span>
803
- </div>
804
- <div class="telemetry-cell reads" title="Read / Inspection operations executed">
805
- <span class="telemetry-label">Reads</span>
806
- <span class="telemetry-value" id="read-count">0</span>
807
- </div>
808
- <div class="telemetry-cell speed" title="Execution latency of last request">
809
- <span class="telemetry-label">Latency</span>
810
- <span class="telemetry-value"><span id="speed-display">0</span><span class="telemetry-unit">ms</span></span>
811
- </div>
812
- <div class="telemetry-cell ram" title="Rust MCP Server Memory (RAM RSS)">
813
- <span class="telemetry-label">RAM</span>
814
- <span class="telemetry-value"><span id="ram-display">3.2</span><span class="telemetry-unit">MB</span></span>
815
- </div>
125
+ <div class="title">Connecting to figma-mcp</div>
126
+ <div id="status" class="status-text">Scanning local ports (38451-38455)...</div>
127
+ <div class="badge">
128
+ <span>●</span> <span id="port-label">127.0.0.1:38451</span>
816
129
  </div>
817
-
818
- <!-- ── STREAM CONSOLE TERMINAL ── -->
819
- <section class="terminal-wrap">
820
- <div class="terminal-header">
821
- <div class="terminal-tabs">
822
- <button class="t-tab active" onclick="setLogFilter('all', this)">All</button>
823
- <button class="t-tab" onclick="setLogFilter('writes', this)">Writes</button>
824
- <button class="t-tab" onclick="setLogFilter('reads', this)">Reads</button>
825
- <button class="t-tab tab-err" id="tab-errors" onclick="setLogFilter('errors', this)">Errors <span class="tab-badge-err" id="tab-err-badge" style="display:none">0</span></button>
826
- </div>
827
- <div class="terminal-meta">
828
- <span class="event-badge" id="log-count">0 ev</span>
829
- <button class="act-btn icon-only" onclick="copyLogs()" title="Copy console logs (respects active tab)" style="height:18px;width:18px;border-radius:3px;">
830
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
831
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
832
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
833
- </svg>
834
- </button>
835
- <button class="act-btn icon-only" onclick="clearLog()" title="Clear stream console" style="height:18px;width:18px;border-radius:3px;">
836
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
837
- <polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
838
- </svg>
839
- </button>
840
- </div>
841
- </div>
842
- <div class="terminal-body" id="log">
843
- <div class="terminal-empty" id="log-empty">
844
- <div class="terminal-empty-icon">◌</div>
845
- <div class="terminal-empty-text">Listening on MCP bridge…</div>
846
- </div>
847
- </div>
848
- </section>
849
-
850
- </div>
851
-
852
- <!-- File Key Input Modal Dialog -->
853
- <div class="modal-overlay" id="key-modal-overlay">
854
- <div class="modal-card">
855
- <div class="modal-title-row">
856
- <div class="modal-title">
857
- <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">
858
- <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
859
- <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
860
- </svg>
861
- Set Figma File Link
862
- </div>
863
- </div>
864
- <div class="modal-desc">
865
- Paste full Figma file URL or File Key to generate real browser links for your layers:
866
- </div>
867
- <input type="text" class="modal-input" id="file-key-input" placeholder="https://www.figma.com/design/..." spellcheck="false" autocomplete="off" />
868
- <div class="modal-actions">
869
- <button class="btn" onclick="closeKeyModal()">Cancel</button>
870
- <button class="btn btn-primary" onclick="saveFileKeyFromModal()">Save & Copy</button>
871
- </div>
130
+ <div class="hint-box">
131
+ Ensure the local Rust server is running:
132
+ <code>npx figma-rust-mcp</code>
872
133
  </div>
873
134
  </div>
874
135
 
875
- <!-- Floating Toast Component -->
876
- <div class="toast" id="toast">
877
- <span id="toast-icon">✓</span>
878
- <span id="toast-text">Action completed</span>
879
- </div>
880
-
881
- <div class="resize-grip" id="resize-grip" title="Drag to resize">
882
- <svg width="9" height="9" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
883
- <path d="M9 1L1 9M9 5L5 9"/>
884
- </svg>
885
- </div>
886
-
887
136
  <script>
888
- var currentPort = 38451;
889
- var BRIDGE = "http://localhost:" + currentPort;
890
- var POLL_MS = 300;
891
- var POLL_MAX_MS = 3000;
892
- var LONG_POLL_TIMEOUT = 12000;
893
- var SHORT_POLL_TIMEOUT = 3000;
894
- var consecutiveErrors = 0;
895
- var everConnected = false;
896
- var lastSuccessfulPollAt = Date.now();
897
- var sessionId = null;
898
- var fileName = null;
899
- var fileKey = null;
900
-
901
- var writeCount = 0, readCount = 0, errCount = 0;
902
- var totalLogEvents = 0;
903
- var polling = true;
904
- var activeLogFilter = 'all';
905
-
906
- var logEl = document.getElementById("log");
907
- var logEmptyEl = document.getElementById("log-empty");
908
- var dotEl = document.getElementById("dot");
909
- var statusEl = document.getElementById("status-text");
910
- var statusPill = document.getElementById("status-pill");
911
- var logCountEl = document.getElementById("log-count");
912
- var docDispEl = document.getElementById("doc-display");
913
- var speedEl = document.getElementById("speed-display");
914
- var ramEl = document.getElementById("ram-display");
915
- var reindexBtn = document.getElementById("reindex-btn");
916
- var reindexBtnLabel = document.getElementById("reindex-btn-label");
917
- var indexProgressWrap = document.getElementById("index-progress-wrap");
918
- var indexProgressText = document.getElementById("index-progress-text");
919
- var indexProgressPct = document.getElementById("index-progress-pct");
920
- var indexProgressFill = document.getElementById("index-progress-fill");
921
-
922
- var selCardEl = document.getElementById("selection-card");
923
- var selIconEl = document.getElementById("selection-icon");
924
- var selNameEl = document.getElementById("selection-name");
925
- var selMetaEl = document.getElementById("selection-meta");
926
- var selActionsEl = document.getElementById("selection-actions");
927
- var selBadgeEl = document.getElementById("selection-badge");
928
- var snapshotBtn = document.getElementById("snapshot-btn");
929
- var toastEl = document.getElementById("toast");
930
- var toastTextEl = document.getElementById("toast-text");
931
- var toastIconEl = document.getElementById("toast-icon");
137
+ const PORTS = [38451, 38452, 38453, 38454, 38455];
138
+ let attempts = 0;
932
139
 
933
- var currentSelectionCount = 0;
934
- var currentSelectionId = null;
935
- var currentSelectionMainId = null;
936
- var currentSelectionName = "";
937
-
938
- function showToast(msg, icon) {
939
- if (!toastEl) return;
940
- if (toastTextEl) toastTextEl.textContent = msg;
941
- if (toastIconEl) toastIconEl.textContent = icon || "✓";
942
- toastEl.classList.add("show");
943
- setTimeout(function() {
944
- toastEl.classList.remove("show");
945
- }, 2200);
946
- }
947
-
948
- function setLogFilter(filter, el) {
949
- activeLogFilter = filter;
950
- var tabs = document.querySelectorAll(".t-tab");
951
- tabs.forEach(function(t) { t.classList.remove("active"); });
952
- if (el) el.classList.add("active");
953
-
954
- var lines = logEl.querySelectorAll(".log-line");
955
- var visibleCount = 0;
956
- lines.forEach(function(line) {
957
- var match = false;
958
- if (filter === 'all') {
959
- match = true;
960
- } else if (filter === 'writes') {
961
- match = line.dataset.type === 'write';
962
- } else if (filter === 'reads') {
963
- match = line.dataset.type === 'read';
964
- } else if (filter === 'errors') {
965
- match = line.dataset.cls === 'err';
966
- }
967
- line.style.display = match ? "flex" : "none";
968
- if (match) visibleCount++;
969
- });
970
-
971
- if (logEmptyEl) {
972
- if (visibleCount === 0 && totalLogEvents > 0) {
973
- logEmptyEl.style.display = "flex";
974
- var emptyText = logEmptyEl.querySelector(".terminal-empty-text");
975
- if (emptyText) emptyText.textContent = filter === 'errors' ? "No errors logged" : "No matching logs";
976
- } else if (totalLogEvents === 0) {
977
- logEmptyEl.style.display = "flex";
978
- var emptyText = logEmptyEl.querySelector(".terminal-empty-text");
979
- if (emptyText) emptyText.textContent = "Listening on MCP bridge…";
980
- } else {
981
- logEmptyEl.style.display = "none";
982
- }
983
- }
984
- }
985
-
986
- async function fetchServerStats() {
987
- if (!everConnected) return;
140
+ async function checkPort(port) {
141
+ const controller = new AbortController();
142
+ const timeoutId = setTimeout(() => controller.abort(), 800);
988
143
  try {
989
- var res = await fetch(BRIDGE + "/health", { signal: AbortSignal.timeout(1000) });
144
+ const res = await fetch(`http://127.0.0.1:${port}/plugin/version`, {
145
+ signal: controller.signal,
146
+ cache: "no-store"
147
+ });
148
+ clearTimeout(timeoutId);
990
149
  if (res.ok) {
991
- var data = await res.json();
992
- if (data && data.stats && data.stats.memoryMb !== undefined && ramEl) {
993
- ramEl.textContent = data.stats.memoryMb.toFixed(1);
994
- }
150
+ const data = await res.json();
151
+ return { ok: true, data };
995
152
  }
996
- } catch(e) {}
997
- }
998
- setInterval(fetchServerStats, 4000);
999
-
1000
- function triggerManualReindex() {
1001
- if (!everConnected) {
1002
- log("Cannot index: Server is offline", "err");
1003
- showSetupGuide();
1004
- return;
1005
- }
1006
- if (reindexBtn) reindexBtn.classList.add("indexing");
1007
- showToast("Syncing canvas index…", "⚡");
1008
- log("⚡ Manual re-index triggered…", "info");
1009
- parent.postMessage({ pluginMessage: { type: "manual-reindex" } }, "*");
1010
- }
1011
-
1012
- function focusSelection() {
1013
- if (currentSelectionCount > 0) {
1014
- parent.postMessage({ pluginMessage: { type: "zoom-to-selection" } }, "*");
153
+ } catch (_) {
154
+ clearTimeout(timeoutId);
1015
155
  }
156
+ return { ok: false };
1016
157
  }
1017
158
 
1018
- function exportSelectionImage(event) {
1019
- if (event) event.stopPropagation();
1020
- if (currentSelectionCount === 0) return;
1021
- if (snapshotBtn) snapshotBtn.style.opacity = "0.5";
1022
- parent.postMessage({ pluginMessage: { type: "export-selection-image", scale: 2 } }, "*");
1023
- }
159
+ async function bootstrapFromPort(port) {
160
+ const statusEl = document.getElementById("status");
161
+ if (statusEl) statusEl.textContent = "Loading dynamic runtime...";
1024
162
 
1025
- function copyTextFallback(text) {
1026
163
  try {
1027
- var textarea = document.createElement("textarea");
1028
- textarea.value = text;
1029
- textarea.style.position = "fixed";
1030
- textarea.style.left = "-9999px";
1031
- textarea.style.top = "-9999px";
1032
- textarea.style.opacity = "0";
1033
- document.body.appendChild(textarea);
1034
- textarea.focus();
1035
- textarea.select();
1036
- var successful = document.execCommand("copy");
1037
- document.body.removeChild(textarea);
1038
- return successful;
164
+ // 1. Fetch main thread code
165
+ const codeRes = await fetch(`http://127.0.0.1:${port}/plugin/code.js?t=${Date.now()}`);
166
+ if (!codeRes.ok) throw new Error("Failed to load plugin code: " + codeRes.status);
167
+ const codeText = await codeRes.text();
168
+
169
+ // Send to main thread
170
+ parent.postMessage({ pluginMessage: { type: "EVAL_MAIN_CODE", code: codeText } }, "*");
171
+
172
+ // 2. Fetch full UI HTML
173
+ const uiRes = await fetch(`http://127.0.0.1:${port}/plugin/ui.html?t=${Date.now()}`);
174
+ if (!uiRes.ok) throw new Error("Failed to load plugin UI: " + uiRes.status);
175
+ const uiHtml = await uiRes.text();
176
+
177
+ // 3. Inject full UI cleanly
178
+ document.open();
179
+ document.write(uiHtml);
180
+ document.close();
1039
181
  } catch (err) {
1040
- return false;
1041
- }
1042
- }
1043
-
1044
- function openKeyModal(onSuccess) {
1045
- var modal = document.getElementById("key-modal-overlay");
1046
- var input = document.getElementById("file-key-input");
1047
- if (modal && input) {
1048
- input.value = fileKey ? ("https://www.figma.com/design/" + fileKey) : "";
1049
- modal.classList.add("show");
1050
- window.__onKeySaved = onSuccess;
1051
- setTimeout(function() { input.focus(); input.select(); }, 50);
1052
- }
1053
- }
1054
-
1055
- function closeKeyModal() {
1056
- var modal = document.getElementById("key-modal-overlay");
1057
- if (modal) modal.classList.remove("show");
1058
- window.__onKeySaved = null;
1059
- }
1060
-
1061
- function saveFileKeyFromModal() {
1062
- var input = document.getElementById("file-key-input");
1063
- var val = input ? input.value.trim() : "";
1064
- if (val) {
1065
- var match = val.match(/(?:file|design)\/([a-zA-Z0-9]+)/);
1066
- var parsedKey = match ? match[1] : val;
1067
- fileKey = parsedKey;
1068
- parent.postMessage({ pluginMessage: { type: "set-file-key", fileKey: parsedKey } }, "*");
1069
- showToast("File Key saved!", "🔗");
1070
- }
1071
- var cb = window.__onKeySaved;
1072
- closeKeyModal();
1073
- if (typeof cb === "function") {
1074
- cb(fileKey);
1075
- }
1076
- }
1077
-
1078
- function copyFileLink(event) {
1079
- if (event) {
1080
- event.stopPropagation();
1081
- event.preventDefault();
1082
- }
1083
- openKeyModal(function(savedKey) {
1084
- if (savedKey) {
1085
- showToast("File link updated", "🔗");
1086
- }
1087
- });
1088
- }
1089
-
1090
- function copySelectionLink(event) {
1091
- if (event) {
1092
- event.stopPropagation();
1093
- event.preventDefault();
1094
- }
1095
- if (!currentSelectionId) {
1096
- showToast("No selection active", "⚠️");
1097
- try { parent.postMessage({ pluginMessage: { type: "notify", message: "⚠️ No frame selected to copy link", error: true } }, "*"); } catch(e) {}
1098
- return;
1099
- }
1100
-
1101
- if (!fileKey) {
1102
- openKeyModal(function(savedKey) {
1103
- executeCopyLink(savedKey);
1104
- });
1105
- return;
1106
- }
1107
-
1108
- executeCopyLink(fileKey);
1109
- }
1110
-
1111
- function executeCopyLink(effectiveKey) {
1112
- if (!currentSelectionId) return;
1113
-
1114
- var targetId = currentSelectionMainId || currentSelectionId;
1115
- var figmaNodeId = targetId.replace(/:/g, "-");
1116
-
1117
- // Figma web link uses a sanitized slug of the file name (or untitled)
1118
- var cleanSlug = (fileName || "file")
1119
- .replace(/[\[\]\(\)\{\}]/g, "")
1120
- .trim()
1121
- .replace(/\s+/g, "-");
1122
- var encodedSlug = encodeURIComponent(cleanSlug);
1123
-
1124
- var link = "";
1125
- if (effectiveKey) {
1126
- link = "https://www.figma.com/design/" + effectiveKey + "/" + encodedSlug + "?node-id=" + figmaNodeId;
1127
- } else {
1128
- link = "https://www.figma.com/design/draft/" + encodedSlug + "?node-id=" + figmaNodeId;
1129
- }
1130
-
1131
- var copied = copyTextFallback(link);
1132
- if (!copied && navigator && navigator.clipboard && navigator.clipboard.writeText) {
1133
- navigator.clipboard.writeText(link).catch(function() {});
1134
- }
1135
-
1136
- showToast("Link: " + figmaNodeId, "🔗");
1137
- try {
1138
- parent.postMessage({
1139
- pluginMessage: {
1140
- type: "notify",
1141
- message: "🔗 Link copied to clipboard! (" + figmaNodeId + ")"
1142
- }
1143
- }, "*");
1144
- } catch(e) {}
1145
- }
1146
-
1147
- function copySelectionId(event) {
1148
- if (event) {
1149
- event.stopPropagation();
1150
- event.preventDefault();
1151
- }
1152
- if (!currentSelectionId) return;
1153
- var copied = copyTextFallback(currentSelectionId);
1154
- if (!copied && navigator && navigator.clipboard && navigator.clipboard.writeText) {
1155
- navigator.clipboard.writeText(currentSelectionId).then(function() {
1156
- showToast("Node ID: " + currentSelectionId + " copied", "📋");
1157
- try {
1158
- parent.postMessage({ pluginMessage: { type: "notify", message: "📋 Node ID: " + currentSelectionId + " copied" } }, "*");
1159
- } catch(e) {}
1160
- }).catch(function() {
1161
- prompt("Copy Node ID:", currentSelectionId);
1162
- });
1163
- } else if (copied) {
1164
- showToast("Node ID: " + currentSelectionId + " copied", "📋");
1165
- try {
1166
- parent.postMessage({ pluginMessage: { type: "notify", message: "📋 Node ID: " + currentSelectionId + " copied" } }, "*");
1167
- } catch(e) {}
1168
- } else {
1169
- prompt("Copy Node ID:", currentSelectionId);
1170
- }
1171
- }
1172
-
1173
- var READ_OPS = [
1174
- "get_selection","get_design","get_page_nodes","screenshot",
1175
- "get_styles","get_local_components","get_viewport","get_variables",
1176
- "export_svg","get_node_detail","export_image","search_nodes","scan_design",
1177
- "index_scan"
1178
- ];
1179
-
1180
- function setStatus(state, text) {
1181
- dotEl.className = "dot " + (state === "green" ? "green" : (state === "yellow" ? "yellow" : "red"));
1182
- statusEl.textContent = text;
1183
-
1184
- statusPill.className = "status-pill";
1185
- if (state === "green") statusPill.classList.add("connected");
1186
- if (state === "red") statusPill.classList.add("offline");
1187
- if (state === "yellow") statusPill.classList.add("connecting");
1188
- }
1189
-
1190
- function handleStatusClick() {
1191
- if (!wsConnected) {
1192
- showSetupGuide();
1193
- } else {
1194
- showToast("Bridge active on port " + currentPort, "⚡");
1195
- }
1196
- }
1197
-
1198
- function showSetupGuide() {
1199
- var existing = document.getElementById("setup-overlay");
1200
- if (existing) { existing.remove(); return; }
1201
-
1202
- var overlay = document.createElement("div");
1203
- overlay.id = "setup-overlay";
1204
- Object.assign(overlay.style, {
1205
- position: "fixed", inset: "0",
1206
- background: "rgba(9,10,15,0.96)",
1207
- backdropFilter: "blur(8px)",
1208
- zIndex: "999",
1209
- display: "flex", flexDirection: "column",
1210
- alignItems: "center", justifyContent: "center",
1211
- padding: "16px", gap: "10px",
1212
- fontFamily: "var(--font-ui)"
1213
- });
1214
-
1215
- overlay.innerHTML = [
1216
- '<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">',
1217
- '<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>',
1218
- '</div>',
1219
- '<div style="font-size:13px;font-weight:700;color:#f8fafc;letter-spacing:-0.2px">Start Figma MCP Server</div>',
1220
- '<div style="font-size:10px;color:#94a3b8;text-align:center;max-width:240px;line-height:1.5">Run command in your project terminal:</div>',
1221
-
1222
- '<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">',
1223
- '<div style="font-size:8px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#475569">Option A · Run once</div>',
1224
- '<div style="font-family:\'SF Mono\',monospace;font-size:11.5px;font-weight:600;color:#38bdf8;user-select:text">npx figma-mcp</div>',
1225
- '</div>',
1226
-
1227
- '<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">',
1228
- '<div style="font-size:8px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#475569">Option B · Auto-start ✦ Recommended</div>',
1229
- '<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>',
1230
- '<div style="font-size:8.5px;color:#475569">Background service for Claude / IDE</div>',
1231
- '</div>',
1232
-
1233
- '<div style="display:flex;align-items:center;gap:6px;font-size:9px;color:#475569">',
1234
- '<div style="width:5px;height:5px;border-radius:50%;background:#f59e0b;animation:pulse 1.4s ease-in-out infinite"></div>',
1235
- 'Plugin auto-connects once server is detected',
1236
- '</div>',
1237
-
1238
- '<button onclick="document.getElementById(\'setup-overlay\').remove();reconnect();" style="',
1239
- 'margin-top:4px;padding:6px 18px;border-radius:6px;border:1px solid rgba(255,255,255,0.12);',
1240
- 'background:rgba(255,255,255,0.06);color:#f8fafc;font-size:10px;font-weight:600;cursor:pointer;',
1241
- 'font-family:inherit;letter-spacing:0.1px',
1242
- '">Dismiss & Retry</button>'
1243
- ].join("");
1244
-
1245
- document.body.appendChild(overlay);
1246
-
1247
- var checks = 0;
1248
- var checker = setInterval(function() {
1249
- checks++;
1250
- fetch(BRIDGE + "/health", { cache: "no-store", signal: AbortSignal.timeout(400) })
1251
- .then(function(res) {
1252
- if (res.ok) {
1253
- clearInterval(checker);
1254
- var ov = document.getElementById("setup-overlay");
1255
- if (ov) ov.remove();
1256
- log("✓ Server detected — connecting…", "ok");
1257
- reconnect();
1258
- }
1259
- })
1260
- .catch(function() {
1261
- if (checks >= 60) clearInterval(checker);
1262
- });
1263
- }, 500);
1264
- }
1265
-
1266
- var totalErrors = 0;
1267
-
1268
- function updateErrorBadge() {
1269
- var badge = document.getElementById("tab-err-badge");
1270
- if (!badge) return;
1271
- if (totalErrors > 0) {
1272
- badge.textContent = totalErrors;
1273
- badge.style.display = "inline-block";
1274
- } else {
1275
- badge.style.display = "none";
1276
- }
1277
- }
1278
-
1279
- function copyTextToClipboard(text, successMsg) {
1280
- if (!text) return;
1281
- if (navigator.clipboard && navigator.clipboard.writeText) {
1282
- navigator.clipboard.writeText(text).then(function() {
1283
- showToast(successMsg || "Copied!", "✓");
1284
- }).catch(function() {
1285
- fallbackCopy(text, successMsg);
1286
- });
1287
- } else {
1288
- fallbackCopy(text, successMsg);
1289
- }
1290
- }
1291
-
1292
- function fallbackCopy(text, successMsg) {
1293
- try {
1294
- var ta = document.createElement("textarea");
1295
- ta.value = text;
1296
- ta.style.position = "fixed";
1297
- ta.style.left = "-9999px";
1298
- document.body.appendChild(ta);
1299
- ta.select();
1300
- document.execCommand("copy");
1301
- document.body.removeChild(ta);
1302
- showToast(successMsg || "Copied!", "✓");
1303
- } catch(e) {
1304
- showToast("Copy failed", "✗");
1305
- }
1306
- }
1307
-
1308
- function copyLogs() {
1309
- var lines = logEl.querySelectorAll(".log-line");
1310
- var visibleTexts = [];
1311
- lines.forEach(function(line) {
1312
- if (line.style.display !== "none") {
1313
- var ts = line.querySelector(".log-ts");
1314
- var msg = line.querySelector(".log-msg");
1315
- var text = (ts ? ts.textContent + " " : "") + (msg ? msg.textContent : line.textContent);
1316
- visibleTexts.push(text.trim());
1317
- }
1318
- });
1319
-
1320
- if (visibleTexts.length === 0) {
1321
- showToast(activeLogFilter === 'errors' ? "No errors to copy" : "No logs to copy", "ℹ");
1322
- return;
1323
- }
1324
-
1325
- var fullText = visibleTexts.join("\n");
1326
- var label = activeLogFilter === 'errors' ? "Errors copied (" + visibleTexts.length + ")!" : "Logs copied (" + visibleTexts.length + ")!";
1327
- copyTextToClipboard(fullText, label);
1328
- }
1329
-
1330
- function log(msg, cls, opType) {
1331
- totalLogEvents++;
1332
- if (cls === 'err') {
1333
- totalErrors++;
1334
- updateErrorBadge();
1335
- }
1336
- if (logCountEl) logCountEl.textContent = totalLogEvents + " ev";
1337
-
1338
- if (logEmptyEl) logEmptyEl.style.display = "none";
1339
-
1340
- var wasNearBottom = (logEl.scrollHeight - logEl.clientHeight - logEl.scrollTop) <= 45;
1341
-
1342
- var line = document.createElement("div");
1343
- line.className = "log-line";
1344
- if (opType) line.dataset.type = opType;
1345
- if (cls) line.dataset.cls = cls;
1346
- line.title = "Click to copy";
1347
- line.style.cursor = "pointer";
1348
-
1349
- var ts = document.createElement("span");
1350
- ts.className = "log-ts";
1351
- ts.textContent = new Date().toLocaleTimeString("en", { hour12: false });
1352
-
1353
- var prompt = document.createElement("span");
1354
- prompt.className = "log-prompt";
1355
- prompt.textContent = "❯";
1356
-
1357
- var content = document.createElement("span");
1358
- content.className = "log-msg" + (cls ? " " + cls : "");
1359
- content.textContent = msg;
1360
-
1361
- line.appendChild(ts);
1362
- line.appendChild(prompt);
1363
- line.appendChild(content);
1364
-
1365
- line.addEventListener("click", function() {
1366
- var text = (ts ? ts.textContent + " " : "") + (content ? content.textContent : line.textContent);
1367
- copyTextToClipboard(text.trim(), "Copied line!");
1368
- });
1369
-
1370
- if (activeLogFilter === 'writes' && opType !== 'write') {
1371
- line.style.display = "none";
1372
- } else if (activeLogFilter === 'reads' && opType !== 'read') {
1373
- line.style.display = "none";
1374
- } else if (activeLogFilter === 'errors' && cls !== 'err') {
1375
- line.style.display = "none";
1376
- }
1377
-
1378
- logEl.appendChild(line);
1379
-
1380
- if (wasNearBottom) {
1381
- logEl.scrollTop = logEl.scrollHeight;
1382
- }
1383
-
1384
- if (logEl.children.length > 90) {
1385
- var keep = Array.from(logEl.children).slice(-70);
1386
- logEl.replaceChildren.apply(logEl, keep);
182
+ console.error("[figma-mcp stub]", err);
183
+ if (statusEl) statusEl.textContent = "Failed to load: " + err.message + ". Retrying...";
184
+ setTimeout(findAndConnect, 1500);
1387
185
  }
1388
186
  }
1389
187
 
1390
- function clearLog() {
1391
- logEl.innerHTML = "";
1392
- totalLogEvents = 0;
1393
- totalErrors = 0;
1394
- updateErrorBadge();
1395
- if (logCountEl) logCountEl.textContent = "0 ev";
1396
- var empty = document.createElement("div");
1397
- empty.id = "log-empty";
1398
- empty.className = "terminal-empty";
1399
- empty.innerHTML = '<div class="terminal-empty-icon">◌</div><div class="terminal-empty-text">Listening on MCP bridge…</div>';
1400
- logEl.appendChild(empty);
1401
- logEmptyEl = empty;
1402
- }
1403
-
1404
- function sleep(ms) {
1405
- return new Promise(function(r) { setTimeout(r, ms); });
1406
- }
1407
-
1408
- function getBackoff() {
1409
- return Math.min(POLL_MS * Math.pow(2, Math.min(consecutiveErrors - 1, 3)), POLL_MAX_MS);
1410
- }
1411
-
1412
- var ws = null;
1413
- var wsConnected = false;
1414
- var isLongPolling = false;
1415
- var activeAbortController = null;
1416
- var isFirstPoll = true;
1417
- var retryTimer = null;
188
+ async function findAndConnect() {
189
+ attempts++;
190
+ const statusEl = document.getElementById("status");
191
+ const portEl = document.getElementById("port-label");
1418
192
 
1419
- async function initConnection() {
1420
- if (wsConnected) return;
1421
-
1422
- try {
1423
- var res = await fetch("http://localhost:" + currentPort + "/health", { signal: AbortSignal.timeout(600) });
1424
- if (res.ok) {
1425
- BRIDGE = "http://localhost:" + currentPort;
1426
- everConnected = true;
1427
- consecutiveErrors = 0;
1428
- lastSuccessfulPollAt = Date.now();
1429
- setStatus("green", "Connected");
1430
- connectWs();
193
+ for (const port of PORTS) {
194
+ if (portEl) portEl.textContent = `127.0.0.1:${port}`;
195
+ const result = await checkPort(port);
196
+ if (result.ok) {
197
+ await bootstrapFromPort(port);
1431
198
  return;
1432
199
  }
1433
- } catch(e) {}
1434
-
1435
- var scanPorts = [38451, 38452, 38453, 38454, 38455].filter(function(p) { return p !== currentPort; });
1436
- try {
1437
- var winner = await Promise.any(scanPorts.map(function(p) {
1438
- return fetch("http://localhost:" + p + "/health", { signal: AbortSignal.timeout(500) })
1439
- .then(function(r) { if (!r.ok) throw new Error("not ok"); return p; });
1440
- }));
1441
- currentPort = winner;
1442
- BRIDGE = "http://localhost:" + winner;
1443
- everConnected = true;
1444
- consecutiveErrors = 0;
1445
- lastSuccessfulPollAt = Date.now();
1446
- setStatus("green", "Connected");
1447
- connectWs();
1448
- return;
1449
- } catch(_) {}
1450
-
1451
- setStatus("yellow", "Waiting…");
1452
- if (retryTimer) clearTimeout(retryTimer);
1453
- retryTimer = setTimeout(function() {
1454
- if (!wsConnected && polling) {
1455
- initConnection();
1456
- }
1457
- }, 1500);
1458
- }
1459
-
1460
- function connectWs() {
1461
- if (ws) {
1462
- try { ws.close(); } catch(e) {}
1463
- ws = null;
1464
- }
1465
-
1466
- var wsHost = BRIDGE.replace(/^http:\/\//, "ws://");
1467
- var wsUrl = wsHost + "/ws";
1468
- var qs = [];
1469
- if (sessionId) qs.push("sessionId=" + encodeURIComponent(sessionId));
1470
- if (fileName) qs.push("fileName=" + encodeURIComponent(fileName));
1471
- if (qs.length) wsUrl += "?" + qs.join("&");
1472
-
1473
- try {
1474
- ws = new WebSocket(wsUrl);
1475
- } catch (err) {
1476
- startLongPoll();
1477
- return;
1478
- }
1479
-
1480
- var pingTimer = null;
1481
-
1482
- ws.onopen = function() {
1483
- wsConnected = true;
1484
- consecutiveErrors = 0;
1485
- everConnected = true;
1486
- if (retryTimer) clearTimeout(retryTimer);
1487
- lastSuccessfulPollAt = Date.now();
1488
- setStatus("green", "Connected");
1489
- log("Connected to Figma MCP · port " + currentPort, "ok");
1490
-
1491
- if (pingTimer) clearInterval(pingTimer);
1492
- pingTimer = setInterval(function() {
1493
- if (ws && ws.readyState === WebSocket.OPEN) {
1494
- try { ws.send(JSON.stringify({ type: "ping" })); } catch(e) {}
1495
- } else {
1496
- clearInterval(pingTimer);
1497
- }
1498
- }, 15000);
1499
- };
1500
-
1501
- ws.onmessage = function(event) {
1502
- try {
1503
- var req = JSON.parse(event.data);
1504
- if (req && req.operation) {
1505
- var isRead = READ_OPS.indexOf(req.operation) >= 0;
1506
- log("→ " + req.operation, isRead ? "info" : "ok", isRead ? "read" : "write");
1507
- try { ws.send(JSON.stringify({ type: "ack", id: req.id })); } catch(e) {}
1508
- dispatchToMain(req);
1509
- }
1510
- } catch (e) {
1511
- log("WS parse error: " + e.message, "err");
1512
- }
1513
- };
1514
-
1515
- ws.onerror = function() {
1516
- if (!everConnected) {
1517
- setStatus("yellow", "Connecting…");
1518
- }
1519
- };
1520
-
1521
- ws.onclose = function() {
1522
- if (pingTimer) clearInterval(pingTimer);
1523
- wsConnected = false;
1524
- if (polling) {
1525
- startLongPoll();
1526
- if (retryTimer) clearTimeout(retryTimer);
1527
- retryTimer = setTimeout(function() {
1528
- if (!wsConnected && polling) initConnection();
1529
- }, 2000);
1530
- }
1531
- };
1532
- }
1533
-
1534
- async function startLongPoll() {
1535
- if (isLongPolling || wsConnected) return;
1536
- isLongPolling = true;
1537
-
1538
- try {
1539
- while (polling && !wsConnected) {
1540
- var receivedThisRound = 0;
1541
- try {
1542
- var pollUrl = BRIDGE + "/poll";
1543
- var qs = [];
1544
- if (sessionId) qs.push("sessionId=" + encodeURIComponent(sessionId));
1545
- if (fileName) qs.push("fileName=" + encodeURIComponent(fileName));
1546
- if (isFirstPoll) {
1547
- qs.push("init=true");
1548
- isFirstPoll = false;
1549
- }
1550
- if (qs.length) pollUrl += "?" + qs.join("&");
1551
-
1552
- var fetchTimeout = consecutiveErrors > 0 ? SHORT_POLL_TIMEOUT : LONG_POLL_TIMEOUT;
1553
- var ctrl = new AbortController();
1554
- activeAbortController = ctrl;
1555
- var timeoutId = setTimeout(function() { ctrl.abort(); }, fetchTimeout);
1556
- var res;
1557
- try {
1558
- res = await fetch(pollUrl, { signal: ctrl.signal });
1559
- } finally {
1560
- clearTimeout(timeoutId);
1561
- if (activeAbortController === ctrl) {
1562
- activeAbortController = null;
1563
- }
1564
- }
1565
-
1566
- if (!res.ok) {
1567
- setStatus("yellow", "Busy");
1568
- consecutiveErrors++;
1569
- await sleep(getBackoff());
1570
- continue;
1571
- }
1572
-
1573
- var body = await res.json();
1574
- var reqs = body.requests || [];
1575
- receivedThisRound = reqs.length;
1576
- setStatus("green", reqs.length ? reqs.length + " cmd" : "Connected");
1577
-
1578
- consecutiveErrors = 0;
1579
- everConnected = true;
1580
- lastSuccessfulPollAt = Date.now();
1581
-
1582
- for (var i = 0; i < reqs.length; i++) {
1583
- var req = reqs[i];
1584
- var isRead = READ_OPS.indexOf(req.operation) >= 0;
1585
- log("→ " + req.operation, isRead ? "info" : "ok", isRead ? "read" : "write");
1586
- dispatchToMain(req);
1587
- }
1588
- } catch (e) {
1589
- if (!polling) break;
1590
- consecutiveErrors++;
1591
- var backoff = getBackoff();
1592
- if (consecutiveErrors <= 5) {
1593
- setStatus("yellow", "Connecting (" + consecutiveErrors + ")");
1594
- } else if (consecutiveErrors <= 10) {
1595
- setStatus("yellow", "Retrying…");
1596
- } else {
1597
- setStatus("red", "Offline");
1598
- }
1599
- await sleep(backoff);
1600
- continue;
1601
- }
1602
- if (!receivedThisRound) await sleep(POLL_MS);
1603
- }
1604
- } finally {
1605
- isLongPolling = false;
1606
- }
1607
- }
1608
-
1609
- function buildResponseBody(id, success, dataJson, error) {
1610
- return '{"id":' + JSON.stringify(id) +
1611
- ',"success":' + (success ? "true" : "false") +
1612
- ',"data":' + (dataJson === undefined || dataJson === null ? "null" : dataJson) +
1613
- ',"error":' + (error === undefined || error === null ? "null" : JSON.stringify(String(error))) +
1614
- '}';
1615
- }
1616
-
1617
- async function sendResponse(id, success, dataJson, error) {
1618
- var body = buildResponseBody(id, success, dataJson, error);
1619
-
1620
- if (ws && ws.readyState === WebSocket.OPEN) {
1621
- try {
1622
- ws.send(body);
1623
- return;
1624
- } catch(e) {}
1625
- }
1626
-
1627
- try {
1628
- await fetch(BRIDGE + "/response", {
1629
- method: "POST",
1630
- headers: { "Content-Type": "application/json" },
1631
- body: body
1632
- });
1633
- } catch (e) {
1634
- log("Response failed: " + e.message, "err");
1635
- }
1636
- }
1637
-
1638
- function dispatchToMain(req) {
1639
- parent.postMessage({ pluginMessage: req }, "*");
1640
- }
1641
-
1642
- function reconnect() {
1643
- polling = false;
1644
- isFirstPoll = true;
1645
- consecutiveErrors = 0;
1646
- setStatus("yellow", "Reconnecting…");
1647
- if (ws) {
1648
- try { ws.close(); } catch(e) {}
1649
- ws = null;
1650
- }
1651
- if (activeAbortController) {
1652
- try { activeAbortController.abort(); } catch(e) {}
1653
- }
1654
- setTimeout(function() {
1655
- polling = true;
1656
- initConnection();
1657
- }, 100);
1658
- }
1659
-
1660
- function sendActivePing() {
1661
- if (ws && ws.readyState === WebSocket.OPEN) {
1662
- try {
1663
- ws.send(JSON.stringify({ type: "ping", active: true, sessionId: sessionId, fileName: fileName }));
1664
- } catch(e) {}
1665
- }
1666
- }
1667
-
1668
- window.addEventListener("focus", sendActivePing);
1669
- document.addEventListener("pointerdown", sendActivePing);
1670
-
1671
- document.addEventListener("visibilitychange", function() {
1672
- if (document.visibilityState !== "visible") return;
1673
- sendActivePing();
1674
- if (!everConnected) return;
1675
- var idleMs = Date.now() - lastSuccessfulPollAt;
1676
- if (!wsConnected || consecutiveErrors > 0 || activeAbortController || idleMs > 20000) {
1677
- log("Focus sync (" + Math.round(idleMs / 1000) + "s)", "info");
1678
- reconnect();
1679
200
  }
1680
- });
1681
201
 
1682
- function getIconForType(type) {
1683
- if (!type) return '<rect x="3" y="3" width="18" height="18" rx="2"/>';
1684
- var t = type.toUpperCase();
1685
- if (t === "FRAME" || t === "SECTION") {
1686
- return '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M15 3v18M3 9h18M3 15h18"/>';
1687
- }
1688
- if (t === "COMPONENT" || t === "INSTANCE") {
1689
- return '<path d="M12 2L2 12l10 10 10-10L12 2z"/>';
1690
- }
1691
- if (t === "TEXT") {
1692
- 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"/>';
202
+ if (statusEl) {
203
+ statusEl.textContent = `Waiting for server (attempt #${attempts})...`;
1693
204
  }
1694
- if (t === "GROUP") {
1695
- 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"/>';
1696
- }
1697
- return '<rect x="3" y="3" width="18" height="18" rx="2"/>';
205
+ setTimeout(findAndConnect, 1000);
1698
206
  }
1699
207
 
1700
- function renderSelection(msg) {
1701
- if (!selCardEl || !selNameEl || !selMetaEl || !selBadgeEl || !selIconEl) return;
1702
-
1703
- var count = msg.count || 0;
1704
- var list = msg.selection || [];
1705
- currentSelectionCount = count;
1706
- currentSelectionId = (count === 1 && list[0]) ? list[0].id : null;
1707
- currentSelectionMainId = (count === 1 && list[0]) ? (list[0].mainId || list[0].id) : null;
1708
- currentSelectionName = (count === 1 && list[0]) ? (list[0].name || "Layer") : "";
1709
-
1710
- if (count === 0) {
1711
- selCardEl.classList.remove("has-selection");
1712
- 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>';
1713
- selNameEl.textContent = "No selection";
1714
- selMetaEl.textContent = msg.pageName ? "Page: " + msg.pageName : (fileName || "Canvas root");
1715
- if (selActionsEl) selActionsEl.style.display = "none";
1716
- selCardEl.title = "No layer selected in Figma canvas";
1717
- } else if (count === 1) {
1718
- var node = list[0];
1719
- selCardEl.classList.add("has-selection");
1720
- 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>';
1721
- selNameEl.textContent = node.name || "Untitled Node";
1722
-
1723
- var metaParts = [];
1724
- if (node.width && node.height) metaParts.push(node.width + " × " + node.height);
1725
- if (msg.pageName) metaParts.push(msg.pageName);
1726
- selMetaEl.textContent = metaParts.length ? metaParts.join(" · ") : (node.type || "Layer");
1727
-
1728
- selBadgeEl.textContent = node.type || "NODE";
1729
- if (selActionsEl) selActionsEl.style.display = "flex";
1730
- selCardEl.title = "Click to center view on: " + (node.name || "Node") + " (" + (node.type || "NODE") + " ID: " + node.id + ")";
1731
- } else {
1732
- selCardEl.classList.add("has-selection");
1733
- 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>';
1734
- var names = list.slice(0, 2).map(function(n) { return n.name; }).join(", ");
1735
- selNameEl.textContent = count + " layers selected";
1736
- selMetaEl.textContent = names + (count > 2 ? " +" + (count - 2) + " more" : "");
1737
- selBadgeEl.textContent = count + " ITEMS";
1738
- if (selActionsEl) selActionsEl.style.display = "flex";
1739
- selCardEl.title = "Click to center view on " + count + " selected layers";
1740
- }
1741
- }
1742
-
1743
- window.onmessage = async function(event) {
1744
- var msg = event.data.pluginMessage;
1745
- if (!msg) return;
1746
-
1747
- if (msg.type === "session-info") {
1748
- var prevSid = sessionId;
1749
- sessionId = msg.sessionId || null;
1750
- fileName = msg.fileName || null;
1751
- fileKey = msg.fileKey || null;
1752
- if (fileName && docDispEl) {
1753
- docDispEl.textContent = fileName;
1754
- docDispEl.title = fileName + " (" + (sessionId || "active session") + ")";
1755
- }
1756
- if (sessionId) log("Linked: " + (fileName || sessionId), "info");
1757
- if (sessionId && sessionId !== prevSid) {
1758
- reconnect();
1759
- }
1760
- return;
1761
- }
1762
-
1763
- if (msg.type === "selection-change") {
1764
- renderSelection(msg);
1765
- if (ws && ws.readyState === WebSocket.OPEN) {
1766
- try {
1767
- ws.send(JSON.stringify({
1768
- type: "selection-change",
1769
- sessionId: sessionId,
1770
- count: msg.count,
1771
- pageName: msg.pageName,
1772
- selection: msg.selection,
1773
- fullNode: msg.fullNode
1774
- }));
1775
- } catch(e) {}
1776
- }
1777
- sendActivePing();
1778
- return;
1779
- }
1780
-
1781
- if (msg.type === "delta-diff") {
1782
- if (ws && ws.readyState === WebSocket.OPEN) {
1783
- try {
1784
- ws.send(JSON.stringify({
1785
- type: "delta-diff",
1786
- sessionId: sessionId,
1787
- id: msg.id,
1788
- diff: msg.diff
1789
- }));
1790
- } catch(e) {}
1791
- }
1792
- return;
1793
- }
1794
-
1795
- if (msg.type === "selection-image-exported") {
1796
- if (snapshotBtn) snapshotBtn.style.opacity = "1";
1797
- if (msg.bytes && msg.bytes.length > 0) {
1798
- try {
1799
- var blob = new Blob([new Uint8Array(msg.bytes)], { type: "image/png" });
1800
- if (navigator && navigator.clipboard && navigator.clipboard.write) {
1801
- await navigator.clipboard.write([
1802
- new ClipboardItem({ "image/png": blob })
1803
- ]);
1804
- showToast("PNG Copied to Clipboard (2x)", "📸");
1805
- log("📸 Exported PNG for " + (msg.nodeName || "node"), "ok", "read");
1806
- } else {
1807
- showToast("Image captured", "📸");
1808
- }
1809
- } catch(err) {
1810
- showToast("Failed to copy image", "✗");
1811
- }
1812
- }
1813
- return;
1814
- }
1815
-
1816
- if (msg.type === "node-diff") {
1817
- if (ws && ws.readyState === WebSocket.OPEN) {
1818
- try { ws.send(JSON.stringify({ type: "node-diff", nodes: msg.nodes })); } catch(e) {}
1819
- }
1820
- return;
1821
- }
1822
-
1823
- if (msg.type === "index-progress") {
1824
- if (reindexBtn) reindexBtn.classList.add("indexing");
1825
- if (indexProgressWrap) indexProgressWrap.classList.add("active");
1826
- if (indexProgressText && msg.label) indexProgressText.textContent = msg.label;
1827
- if (indexProgressPct && msg.percent !== undefined) indexProgressPct.textContent = msg.percent + "%";
1828
- if (indexProgressFill && msg.percent !== undefined) indexProgressFill.style.width = Math.min(100, Math.max(0, msg.percent)) + "%";
1829
- return;
1830
- }
1831
-
1832
- if (msg.type === "index-chunk") {
1833
- if (ws && ws.readyState === WebSocket.OPEN) {
1834
- try { ws.send(JSON.stringify({ type: "index-chunk", nodes: msg.nodes })); } catch(e) {}
1835
- }
1836
- return;
1837
- }
1838
-
1839
- if (msg.type === "document-change") {
1840
- if (ws && ws.readyState === WebSocket.OPEN) {
1841
- try { ws.send(JSON.stringify({ type: "document-change" })); } catch(e) {}
1842
- }
1843
- return;
1844
- }
1845
-
1846
- if (msg.type === "index-update") {
1847
- if (reindexBtn) reindexBtn.classList.remove("indexing");
1848
- if (indexProgressPct) indexProgressPct.textContent = "100%";
1849
- if (indexProgressFill) indexProgressFill.style.width = "100%";
1850
- if (indexProgressText) indexProgressText.textContent = "Index ready";
1851
- setTimeout(function() {
1852
- if (indexProgressWrap) indexProgressWrap.classList.remove("active");
1853
- }, 1200);
1854
-
1855
- var elapsed = msg.startMs ? (Date.now() - msg.startMs) : 0;
1856
- showToast("Indexed in " + elapsed + "ms", "✓");
1857
- if (ws && ws.readyState === WebSocket.OPEN) {
1858
- try {
1859
- ws.send(JSON.stringify({
1860
- type: "index-update",
1861
- data: msg.data,
1862
- fileName: msg.fileName,
1863
- sessionId: msg.sessionId,
1864
- startMs: msg.startMs
1865
- }));
1866
- log("Indexed " + (msg.fileName || "canvas") + " (" + elapsed + "ms)", "ok", "read");
1867
- } catch(e) {}
1868
- }
1869
- return;
1870
- }
1871
-
1872
- var isRead = READ_OPS.indexOf(msg.operation) >= 0;
1873
- var durationStr = msg.durationMs !== undefined ? " (" + msg.durationMs + "ms)" : "";
1874
- if (msg.durationMs !== undefined && speedEl) {
1875
- speedEl.textContent = msg.durationMs;
1876
- }
1877
- if (msg.success) {
1878
- if (isRead) {
1879
- readCount++;
1880
- document.getElementById("read-count").textContent = readCount;
1881
- } else {
1882
- writeCount++;
1883
- document.getElementById("write-count").textContent = writeCount;
1884
- }
1885
- log("✓ " + (msg.id ? msg.id.slice(-6) : "ok") + durationStr, "ok", isRead ? "read" : "write");
1886
- } else {
1887
- errCount++;
1888
- log("✗ " + (msg.id ? msg.id.slice(-6) : "err") + durationStr + ": " + (msg.error || "fail"), "err", isRead ? "read" : "write");
1889
- }
1890
- var payloadJson = msg.dataJson !== undefined
1891
- ? msg.dataJson
1892
- : (msg.data !== undefined ? JSON.stringify(msg.data) : null);
1893
- await sendResponse(msg.id, msg.success, payloadJson, msg.error);
1894
- };
1895
-
1896
- initConnection();
1897
-
1898
- // Smooth Drag Resize (60FPS via requestAnimationFrame)
1899
- (function initResize() {
1900
- var grip = document.getElementById("resize-grip");
1901
- if (!grip) return;
1902
-
1903
- var rafId = null;
1904
- var lastW = 0, lastH = 0;
1905
-
1906
- grip.addEventListener("pointerdown", function(e) {
1907
- e.preventDefault();
1908
- grip.setPointerCapture(e.pointerId);
1909
- var startX = e.clientX;
1910
- var startY = e.clientY;
1911
- var startW = window.innerWidth;
1912
- var startH = window.innerHeight;
1913
-
1914
- function onPointerMove(ev) {
1915
- var nw = Math.max(280, Math.min(1000, Math.round(startW + (ev.clientX - startX))));
1916
- var nh = Math.max(220, Math.min(1200, Math.round(startH + (ev.clientY - startY))));
1917
- lastW = nw;
1918
- lastH = nh;
1919
-
1920
- if (!rafId) {
1921
- rafId = requestAnimationFrame(function() {
1922
- parent.postMessage({ pluginMessage: { type: "resize", width: lastW, height: lastH } }, "*");
1923
- rafId = null;
1924
- });
1925
- }
1926
- }
1927
-
1928
- function onPointerUp(ev) {
1929
- try { grip.releasePointerCapture(ev.pointerId); } catch(err) {}
1930
- window.removeEventListener("pointermove", onPointerMove);
1931
- window.removeEventListener("pointerup", onPointerUp);
1932
- if (rafId) {
1933
- cancelAnimationFrame(rafId);
1934
- rafId = null;
1935
- }
1936
- if (lastW > 0 && lastH > 0) {
1937
- parent.postMessage({ pluginMessage: { type: "resize", width: lastW, height: lastH } }, "*");
1938
- parent.postMessage({ pluginMessage: { type: "save-window-size", width: lastW, height: lastH } }, "*");
1939
- }
1940
- }
1941
-
1942
- window.addEventListener("pointermove", onPointerMove);
1943
- window.addEventListener("pointerup", onPointerUp);
1944
- });
1945
- })();
208
+ findAndConnect();
1946
209
  </script>
1947
210
  </body>
1948
211
  </html>