@mp3wizard/figma-console-mcp 1.30.1 → 1.32.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/README.md +4 -2
- package/dist/cloudflare/core/accessibility-tools.js +55 -0
- package/dist/cloudflare/core/port-discovery.js +130 -9
- package/dist/cloudflare/core/tokens-tools.js +1 -1
- package/dist/cloudflare/core/write-tools.js +8 -5
- package/dist/cloudflare/index.js +3 -3
- package/dist/core/accessibility-tools.d.ts.map +1 -1
- package/dist/core/accessibility-tools.js +55 -0
- package/dist/core/accessibility-tools.js.map +1 -1
- package/dist/core/port-discovery.d.ts +30 -1
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +130 -9
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/core/tokens-tools.js +1 -1
- package/dist/core/write-tools.d.ts.map +1 -1
- package/dist/core/write-tools.js +8 -5
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts +2 -0
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +40 -10
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/README.md +54 -2
- package/figma-desktop-bridge/code.js +53 -12
- package/figma-desktop-bridge/ui.html +983 -111
- package/package.json +6 -3
|
@@ -9,28 +9,63 @@
|
|
|
9
9
|
padding: 0;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/* ===== Status + log colour tokens ===== */
|
|
13
|
+
/* Dark defaults — body[data-theme] overrides below keep light/dark correct. */
|
|
14
|
+
:root {
|
|
15
|
+
--color-connected: #44FF88;
|
|
16
|
+
--color-connected-glow: rgba(68, 255, 136, 0.5);
|
|
17
|
+
--color-waiting: #FFB700;
|
|
18
|
+
--color-error: #FF455B;
|
|
19
|
+
--color-idle: #737373;
|
|
20
|
+
--log-info: #6cf;
|
|
21
|
+
--log-success: #6f6;
|
|
22
|
+
--log-error: #ff8080;
|
|
23
|
+
--log-warn: #fc0;
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
body {
|
|
13
27
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
14
28
|
font-size: 11px;
|
|
15
29
|
background: var(--figma-color-bg, #2c2c2c);
|
|
16
30
|
color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
|
|
17
|
-
|
|
18
|
-
display: flex;
|
|
19
|
-
align-items: center;
|
|
20
|
-
justify-content: center;
|
|
21
|
-
padding: 6px 8px;
|
|
31
|
+
padding: 4px 12px;
|
|
22
32
|
user-select: none;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
|
-
.
|
|
35
|
+
/* Visible keyboard focus for all interactive elements (WCAG 2.4.7) */
|
|
36
|
+
button:focus-visible,
|
|
37
|
+
input:focus-visible {
|
|
38
|
+
outline: 2px solid var(--figma-color-bg-brand, #0d99ff);
|
|
39
|
+
outline-offset: 1px;
|
|
40
|
+
border-radius: 3px;
|
|
41
|
+
}
|
|
42
|
+
/* Hide outline when mouse-clicking but keep it for keyboard nav */
|
|
43
|
+
button:focus:not(:focus-visible),
|
|
44
|
+
input:focus:not(:focus-visible) {
|
|
45
|
+
outline: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.wrap {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 4px;
|
|
52
|
+
width: 100%;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ===== Row 1 — status + CTA + icons (always visible) ===== */
|
|
56
|
+
.row-top {
|
|
26
57
|
display: flex;
|
|
27
58
|
align-items: center;
|
|
28
59
|
gap: 6px;
|
|
29
|
-
padding: 4px 8px;
|
|
30
|
-
background: var(--figma-color-bg-secondary, #383838);
|
|
31
|
-
border: 1px solid var(--figma-color-border, #4a4a4a);
|
|
32
|
-
border-radius: 4px;
|
|
33
60
|
white-space: nowrap;
|
|
61
|
+
padding-left: 4px; /* visual offset to align with Figma title bar icon */
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.status-pill {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 5px;
|
|
68
|
+
margin-right: 3px;
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
.status-indicator {
|
|
@@ -38,21 +73,21 @@
|
|
|
38
73
|
height: 8px;
|
|
39
74
|
border-radius: 50%;
|
|
40
75
|
flex-shrink: 0;
|
|
76
|
+
background: var(--color-idle);
|
|
41
77
|
}
|
|
42
78
|
|
|
43
79
|
.status-indicator.loading {
|
|
44
|
-
background:
|
|
80
|
+
background: var(--color-waiting);
|
|
45
81
|
animation: pulse 1.5s ease-in-out infinite;
|
|
46
82
|
}
|
|
47
83
|
|
|
48
84
|
.status-indicator.active {
|
|
49
|
-
background:
|
|
50
|
-
box-shadow: 0 0 6px
|
|
51
|
-
animation: glow 2s ease-in-out infinite;
|
|
85
|
+
background: var(--color-connected);
|
|
86
|
+
box-shadow: 0 0 6px var(--color-connected-glow);
|
|
52
87
|
}
|
|
53
88
|
|
|
54
89
|
.status-indicator.error {
|
|
55
|
-
background:
|
|
90
|
+
background: var(--color-error);
|
|
56
91
|
}
|
|
57
92
|
|
|
58
93
|
@keyframes pulse {
|
|
@@ -60,158 +95,457 @@
|
|
|
60
95
|
50% { opacity: 1; }
|
|
61
96
|
}
|
|
62
97
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
#status-state {
|
|
99
|
+
font-weight: 700;
|
|
100
|
+
font-size: 10px;
|
|
101
|
+
letter-spacing: -0.2px;
|
|
102
|
+
text-transform: uppercase;
|
|
103
|
+
font-stretch: 75%;
|
|
104
|
+
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.6));
|
|
66
105
|
}
|
|
67
106
|
|
|
68
|
-
.status-
|
|
69
|
-
|
|
70
|
-
letter-spacing: 0.2px;
|
|
107
|
+
.status-indicator.active + #status-state {
|
|
108
|
+
color: var(--color-connected);
|
|
71
109
|
}
|
|
72
110
|
|
|
73
|
-
.status-
|
|
74
|
-
color: var(--
|
|
111
|
+
.status-indicator.error + #status-state {
|
|
112
|
+
color: var(--color-error);
|
|
75
113
|
}
|
|
76
114
|
|
|
77
|
-
.
|
|
115
|
+
.cta-btn {
|
|
116
|
+
background: transparent;
|
|
78
117
|
color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
|
|
79
|
-
|
|
118
|
+
border: 1px solid var(--figma-color-border, #4a4a4a);
|
|
119
|
+
border-radius: 3px;
|
|
120
|
+
font-family: inherit;
|
|
121
|
+
font-size: 10px;
|
|
122
|
+
font-weight: 500;
|
|
123
|
+
padding: 4px;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
line-height: 1.4;
|
|
80
126
|
}
|
|
81
127
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
width: 100%;
|
|
128
|
+
.cta-btn:disabled {
|
|
129
|
+
opacity: 0.6;
|
|
130
|
+
cursor: default;
|
|
86
131
|
}
|
|
87
132
|
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
flex-direction: column;
|
|
91
|
-
gap: 6px;
|
|
92
|
-
margin-top: 6px;
|
|
133
|
+
.cta-btn:hover {
|
|
134
|
+
background: var(--figma-color-bg-secondary, #383838);
|
|
93
135
|
}
|
|
94
136
|
|
|
95
|
-
.
|
|
96
|
-
|
|
137
|
+
.row-top-spacer {
|
|
138
|
+
flex: 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.icon-btn {
|
|
142
|
+
background: transparent;
|
|
143
|
+
border: 1px solid transparent;
|
|
144
|
+
border-radius: 3px;
|
|
145
|
+
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.6));
|
|
146
|
+
width: 24px;
|
|
147
|
+
height: 24px;
|
|
148
|
+
padding: 0;
|
|
149
|
+
display: inline-flex;
|
|
97
150
|
align-items: center;
|
|
98
|
-
|
|
151
|
+
justify-content: center;
|
|
99
152
|
cursor: pointer;
|
|
100
|
-
font-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
margin-top: 2px;
|
|
153
|
+
font-family: inherit;
|
|
154
|
+
font-size: 14px;
|
|
155
|
+
line-height: 1;
|
|
104
156
|
}
|
|
105
157
|
|
|
106
|
-
.
|
|
158
|
+
.icon-btn:hover {
|
|
107
159
|
color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
|
|
160
|
+
border-color: var(--figma-color-border, #4a4a4a);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Borderless icon variant — hover/active change colour only, never border.
|
|
164
|
+
Applied via .icon-btn--borderless modifier class. */
|
|
165
|
+
.icon-btn--borderless,
|
|
166
|
+
.icon-btn--borderless:hover,
|
|
167
|
+
.icon-btn--borderless.active {
|
|
168
|
+
border-color: transparent !important;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Expand [+] / [−] glyph is text, not SVG. Bump to match visual weight of SVG siblings. */
|
|
172
|
+
#expand-btn {
|
|
173
|
+
font-size: 18px;
|
|
174
|
+
font-weight: 400;
|
|
175
|
+
line-height: 1;
|
|
108
176
|
}
|
|
109
177
|
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
178
|
+
.icon-btn.active,
|
|
179
|
+
.icon-btn.active:hover {
|
|
180
|
+
color: var(--figma-color-bg-brand, #0d99ff);
|
|
181
|
+
border-color: var(--figma-color-bg-brand, #0d99ff);
|
|
114
182
|
}
|
|
115
183
|
|
|
116
|
-
.
|
|
117
|
-
|
|
184
|
+
.icon-btn svg {
|
|
185
|
+
width: 14px;
|
|
186
|
+
height: 14px;
|
|
118
187
|
}
|
|
119
188
|
|
|
120
|
-
|
|
189
|
+
/* ===== Row: cloud pairing (when cloud icon on) ===== */
|
|
190
|
+
.row {
|
|
191
|
+
display: none;
|
|
121
192
|
width: 100%;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.row.visible {
|
|
196
|
+
display: flex;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.cloud-pair {
|
|
200
|
+
flex-direction: row;
|
|
201
|
+
gap: 4px;
|
|
202
|
+
align-items: stretch;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.cloud-pair input {
|
|
206
|
+
flex: 1;
|
|
207
|
+
min-width: 0;
|
|
122
208
|
background: var(--figma-color-bg, #2c2c2c);
|
|
123
209
|
border: 1px solid var(--figma-color-border, #4a4a4a);
|
|
124
210
|
border-radius: 3px;
|
|
125
211
|
color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
|
|
126
212
|
font-family: monospace;
|
|
127
|
-
font-size:
|
|
128
|
-
padding:
|
|
213
|
+
font-size: 11px;
|
|
214
|
+
padding: 3px 5px;
|
|
129
215
|
text-transform: uppercase;
|
|
130
|
-
letter-spacing:
|
|
216
|
+
letter-spacing: 2px;
|
|
131
217
|
text-align: center;
|
|
132
218
|
box-sizing: border-box;
|
|
133
219
|
}
|
|
134
220
|
|
|
135
|
-
.cloud-
|
|
221
|
+
.cloud-pair input::placeholder {
|
|
136
222
|
text-transform: none;
|
|
137
223
|
letter-spacing: normal;
|
|
138
|
-
font-family:
|
|
224
|
+
font-family: inherit;
|
|
139
225
|
font-size: 10px;
|
|
140
226
|
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.3));
|
|
141
227
|
}
|
|
142
228
|
|
|
143
|
-
|
|
144
|
-
|
|
229
|
+
/* Connect button only; icon-btn inside the row keeps its icon-btn styling */
|
|
230
|
+
.cloud-pair button:not(.icon-btn) {
|
|
231
|
+
flex-shrink: 0;
|
|
145
232
|
background: var(--figma-color-bg-brand, #0d99ff);
|
|
146
233
|
color: #fff;
|
|
147
234
|
border: none;
|
|
148
235
|
border-radius: 3px;
|
|
236
|
+
font-family: inherit;
|
|
149
237
|
font-size: 10px;
|
|
150
238
|
font-weight: 500;
|
|
151
|
-
padding:
|
|
239
|
+
padding: 4px 10px;
|
|
152
240
|
cursor: pointer;
|
|
153
241
|
}
|
|
154
242
|
|
|
155
|
-
.cloud-
|
|
243
|
+
.cloud-pair button:not(.icon-btn):disabled {
|
|
156
244
|
opacity: 0.5;
|
|
157
245
|
cursor: default;
|
|
158
246
|
}
|
|
159
247
|
|
|
248
|
+
.cloud-help {
|
|
249
|
+
flex-direction: column;
|
|
250
|
+
gap: 4px;
|
|
251
|
+
padding: 6px 8px;
|
|
252
|
+
background: var(--figma-color-bg-secondary, #383838);
|
|
253
|
+
border-radius: 3px;
|
|
254
|
+
font-size: 10px;
|
|
255
|
+
line-height: 1.4;
|
|
256
|
+
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.7));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.cloud-help p {
|
|
260
|
+
margin: 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
body[data-theme="light"] .cloud-help {
|
|
264
|
+
background: #f0f0f0;
|
|
265
|
+
color: #555;
|
|
266
|
+
}
|
|
267
|
+
|
|
160
268
|
.cloud-status {
|
|
269
|
+
display: none;
|
|
161
270
|
font-size: 9px;
|
|
162
271
|
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.5));
|
|
163
272
|
text-align: center;
|
|
273
|
+
padding: 2px 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.cloud-status:not(:empty) {
|
|
277
|
+
display: block;
|
|
164
278
|
}
|
|
165
279
|
|
|
166
280
|
.cloud-status.connected {
|
|
167
|
-
color:
|
|
281
|
+
color: var(--color-connected);
|
|
168
282
|
}
|
|
169
283
|
|
|
170
284
|
.cloud-status.error {
|
|
171
|
-
color:
|
|
285
|
+
color: var(--color-error);
|
|
172
286
|
}
|
|
173
287
|
|
|
174
|
-
/*
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
288
|
+
/* ===== Row: sub-toolbar (when [+] on) ===== */
|
|
289
|
+
.sub-toolbar {
|
|
290
|
+
align-items: center;
|
|
291
|
+
gap: 8px;
|
|
292
|
+
flex-wrap: nowrap;
|
|
293
|
+
padding-left: 4px; /* match row-top alignment with Figma title bar icon */
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.sub-btn {
|
|
297
|
+
background: transparent;
|
|
298
|
+
border: none;
|
|
299
|
+
padding: 2px 0;
|
|
300
|
+
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.6));
|
|
301
|
+
font-family: inherit;
|
|
302
|
+
font-size: 10px;
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
display: inline-flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
gap: 3px;
|
|
307
|
+
white-space: nowrap;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.sub-btn:hover {
|
|
311
|
+
color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.sub-btn.active {
|
|
315
|
+
color: var(--figma-color-bg-brand, #0d99ff);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.sub-btn svg {
|
|
319
|
+
width: 11px;
|
|
320
|
+
height: 11px;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* ===== Row: info panel ===== */
|
|
324
|
+
.info-panel {
|
|
325
|
+
flex-direction: row;
|
|
326
|
+
align-items: center;
|
|
327
|
+
gap: 8px;
|
|
328
|
+
padding: 4px 0;
|
|
329
|
+
font-size: 10px;
|
|
330
|
+
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.7));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.info-rows {
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-direction: column;
|
|
336
|
+
gap: 2px;
|
|
337
|
+
flex: 1;
|
|
338
|
+
min-width: 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.info-row {
|
|
342
|
+
display: flex;
|
|
343
|
+
gap: 6px;
|
|
344
|
+
overflow: hidden;
|
|
345
|
+
align-items: center;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.info-row-label {
|
|
349
|
+
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.5));
|
|
350
|
+
flex-shrink: 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.info-row-value {
|
|
354
|
+
color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
|
|
355
|
+
overflow: hidden;
|
|
356
|
+
text-overflow: ellipsis;
|
|
357
|
+
white-space: nowrap;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* ===== Row: log panel ===== */
|
|
361
|
+
.log-panel {
|
|
362
|
+
flex-direction: column;
|
|
363
|
+
gap: 0;
|
|
364
|
+
border: 1px solid var(--figma-color-border, #4a4a4a);
|
|
365
|
+
border-radius: 3px;
|
|
366
|
+
overflow: hidden;
|
|
367
|
+
background: var(--figma-color-bg, #1e1e1e);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.log-header {
|
|
371
|
+
display: flex;
|
|
372
|
+
justify-content: flex-end;
|
|
373
|
+
padding: 3px 6px;
|
|
374
|
+
font-size: 9px;
|
|
375
|
+
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.5));
|
|
376
|
+
background: var(--figma-color-bg-secondary, #383838);
|
|
377
|
+
border-bottom: 1px solid var(--figma-color-border, #4a4a4a);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.log-entries {
|
|
381
|
+
max-height: 160px;
|
|
382
|
+
overflow-y: auto;
|
|
383
|
+
padding: 4px 6px;
|
|
384
|
+
font-family: 'SF Mono', 'Menlo', Consolas, monospace;
|
|
385
|
+
font-size: 10px;
|
|
386
|
+
line-height: 1.4;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.log-entries.errors-only .log-entry:not(.error) {
|
|
390
|
+
display: none;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.log-entry {
|
|
394
|
+
display: flex;
|
|
395
|
+
align-items: baseline;
|
|
396
|
+
gap: 5px;
|
|
397
|
+
color: var(--figma-color-text, rgba(255, 255, 255, 0.8));
|
|
398
|
+
line-height: 1.5;
|
|
399
|
+
}
|
|
400
|
+
.log-ts {
|
|
401
|
+
flex-shrink: 0;
|
|
402
|
+
opacity: 0.45;
|
|
403
|
+
font-size: 9px;
|
|
404
|
+
user-select: none;
|
|
405
|
+
}
|
|
406
|
+
.log-msg {
|
|
407
|
+
flex: 1;
|
|
408
|
+
min-width: 0;
|
|
409
|
+
overflow: hidden;
|
|
410
|
+
text-overflow: ellipsis;
|
|
411
|
+
white-space: nowrap;
|
|
412
|
+
}
|
|
413
|
+
.log-dur {
|
|
414
|
+
display: none;
|
|
415
|
+
}
|
|
416
|
+
.log-count {
|
|
417
|
+
flex-shrink: 0;
|
|
418
|
+
opacity: 0.5;
|
|
419
|
+
font-size: 9px;
|
|
420
|
+
min-width: 18px;
|
|
421
|
+
text-align: right;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.log-entry.info { color: var(--log-info); }
|
|
425
|
+
.log-entry.success{ color: var(--log-success); }
|
|
426
|
+
.log-entry.error { color: var(--log-error); }
|
|
427
|
+
.log-entry.warn { color: var(--log-warn); }
|
|
428
|
+
|
|
429
|
+
/* ===== Light theme ===== */
|
|
430
|
+
/* Theme manual override redefines the Figma-injected vars so all token
|
|
431
|
+
consumers cascade automatically. When no data-theme attribute is set,
|
|
432
|
+
Figma's themeColors:true (in code.js) controls the values natively. */
|
|
433
|
+
body[data-theme="light"] {
|
|
434
|
+
--figma-color-bg: #ffffff;
|
|
435
|
+
--figma-color-bg-secondary: #f5f5f5;
|
|
436
|
+
--figma-color-border: #e5e5e5;
|
|
437
|
+
--figma-color-text: #333333;
|
|
438
|
+
--figma-color-text-secondary: #777777;
|
|
439
|
+
--color-connected: #16a34a;
|
|
440
|
+
--color-connected-glow: rgba(22, 163, 74, 0.45);
|
|
441
|
+
--color-waiting: #d97706;
|
|
442
|
+
--color-error: #ef4444;
|
|
443
|
+
--color-idle: #6b7280;
|
|
444
|
+
--log-info: #00639e;
|
|
445
|
+
--log-success: #167016;
|
|
446
|
+
--log-error: #b81e2c;
|
|
447
|
+
--log-warn: #7a5c00;
|
|
448
|
+
}
|
|
449
|
+
body[data-theme="dark"] {
|
|
450
|
+
--figma-color-bg: #2c2c2c;
|
|
451
|
+
--figma-color-bg-secondary: #383838;
|
|
452
|
+
--figma-color-border: #4a4a4a;
|
|
453
|
+
--figma-color-text: rgba(255, 255, 255, 0.9);
|
|
454
|
+
--figma-color-text-secondary: rgba(255, 255, 255, 0.55);
|
|
455
|
+
--color-connected: #44FF88;
|
|
456
|
+
--color-connected-glow: rgba(68, 255, 136, 0.5);
|
|
457
|
+
--color-waiting: #FFB700;
|
|
458
|
+
--color-error: #FF455B;
|
|
459
|
+
--color-idle: #737373;
|
|
460
|
+
--log-info: #6cf;
|
|
461
|
+
--log-success: #6f6;
|
|
462
|
+
--log-error: #ff8080;
|
|
463
|
+
--log-warn: #fc0;
|
|
190
464
|
}
|
|
191
465
|
</style>
|
|
192
466
|
</head>
|
|
193
467
|
<body>
|
|
194
|
-
<div
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
<div class="status-
|
|
198
|
-
<
|
|
199
|
-
<span
|
|
468
|
+
<div class="wrap">
|
|
469
|
+
<!-- Row 1 — always visible -->
|
|
470
|
+
<div class="row-top">
|
|
471
|
+
<div class="status-pill">
|
|
472
|
+
<div class="status-indicator loading" id="status-dot" aria-hidden="true"></div>
|
|
473
|
+
<span id="status-state" role="status" aria-live="polite">Connecting</span>
|
|
200
474
|
</div>
|
|
475
|
+
<button class="cta-btn" id="cta-btn" onclick="toggleLocalConnection()">Pause</button>
|
|
476
|
+
<div class="row-top-spacer"></div>
|
|
477
|
+
<button class="icon-btn" id="cloud-icon" onclick="toggleCloudPair()" title="Cloud pairing" aria-label="Cloud pairing" aria-expanded="false">
|
|
478
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
479
|
+
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>
|
|
480
|
+
</svg>
|
|
481
|
+
</button>
|
|
482
|
+
<button class="icon-btn" id="expand-btn" onclick="toggleSubToolbar()" title="Show options" aria-label="Show options" aria-expanded="false">+</button>
|
|
201
483
|
</div>
|
|
202
484
|
|
|
203
|
-
|
|
204
|
-
|
|
485
|
+
<!-- Cloud pairing (shown when cloud icon active) -->
|
|
486
|
+
<div class="row cloud-pair" id="cloud-pair">
|
|
487
|
+
<button class="icon-btn icon-btn--borderless cloud-info-btn" id="cloud-info-btn" onclick="toggleCloudHelp()" title="About pairing codes" aria-label="About pairing codes" aria-expanded="false" aria-controls="cloud-help">
|
|
488
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
489
|
+
<circle cx="12" cy="12" r="10"/>
|
|
490
|
+
<line x1="12" y1="16" x2="12" y2="12"/>
|
|
491
|
+
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
|
492
|
+
</svg>
|
|
493
|
+
</button>
|
|
494
|
+
<input type="text" id="cloud-code" maxlength="6" placeholder="Pairing code" autocomplete="off" aria-label="Cloud pairing code" />
|
|
495
|
+
<button id="cloud-btn" onclick="cloudConnect()">Connect</button>
|
|
496
|
+
</div>
|
|
497
|
+
<div class="row cloud-help" id="cloud-help" role="region" aria-label="About pairing codes">
|
|
498
|
+
<p>Use this when Claude is on a different device from Figma.</p>
|
|
499
|
+
<p>Generate a 6-char code in Claude and paste it here.</p>
|
|
500
|
+
</div>
|
|
501
|
+
<div class="cloud-status" id="cloud-status"></div>
|
|
502
|
+
|
|
503
|
+
<!-- Sub-toolbar (shown when [+] active) -->
|
|
504
|
+
<div class="row sub-toolbar" id="sub-toolbar">
|
|
505
|
+
<button class="sub-btn" id="info-toggle" onclick="toggleInfo()" aria-expanded="false" aria-controls="info-panel">
|
|
506
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
507
|
+
<circle cx="12" cy="12" r="10"/>
|
|
508
|
+
<line x1="12" y1="16" x2="12" y2="12"/>
|
|
509
|
+
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
|
510
|
+
</svg>
|
|
511
|
+
Info
|
|
512
|
+
</button>
|
|
513
|
+
<button class="sub-btn" id="log-toggle" onclick="toggleLog()" aria-expanded="false" aria-controls="log-panel">
|
|
514
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
515
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
516
|
+
<circle cx="12" cy="12" r="3"/>
|
|
517
|
+
</svg>
|
|
518
|
+
Log
|
|
519
|
+
</button>
|
|
520
|
+
<button class="icon-btn" id="errors-toggle" onclick="toggleErrorsOnly()" title="Filter errors only" aria-label="Filter errors only" aria-pressed="false" style="display:none">
|
|
521
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
522
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
|
523
|
+
<line x1="12" y1="9" x2="12" y2="13"/>
|
|
524
|
+
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
525
|
+
</svg>
|
|
526
|
+
</button>
|
|
527
|
+
<button class="icon-btn" id="copy-log-btn" onclick="copyLogToClipboard()" title="Copy log to clipboard" aria-label="Copy log to clipboard" style="display:none">
|
|
528
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
529
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
530
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
531
|
+
</svg>
|
|
532
|
+
</button>
|
|
205
533
|
</div>
|
|
206
534
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
535
|
+
<!-- Info panel (shown when Info active) -->
|
|
536
|
+
<div class="row info-panel" id="info-panel">
|
|
537
|
+
<div class="info-rows">
|
|
538
|
+
<div class="info-row"><span class="info-row-label">File:</span><span class="info-row-value" id="info-file">—</span></div>
|
|
539
|
+
<div class="info-row"><span class="info-row-label">Page:</span><span class="info-row-value" id="info-page">—</span></div>
|
|
210
540
|
</div>
|
|
211
|
-
|
|
212
|
-
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<!-- Log panel (shown when Show log active) -->
|
|
544
|
+
<div class="row log-panel" id="log-panel">
|
|
545
|
+
<div class="log-header">
|
|
546
|
+
<span id="log-servers">0 server(s)</span>
|
|
213
547
|
</div>
|
|
214
|
-
<div class="
|
|
548
|
+
<div class="log-entries" id="log-entries"></div>
|
|
215
549
|
</div>
|
|
216
550
|
</div>
|
|
217
551
|
|
|
@@ -907,6 +1241,14 @@
|
|
|
907
1241
|
// After connecting, disconnect-triggered retries have their own limit.
|
|
908
1242
|
var initialScanAttempts = 0;
|
|
909
1243
|
var MAX_INITIAL_SCANS = 3;
|
|
1244
|
+
// Set true only when the user clicks Pause. Suppresses the background
|
|
1245
|
+
// watchdog so a deliberate pause is not undone automatically.
|
|
1246
|
+
var userPaused = false;
|
|
1247
|
+
// Watchdog cadence: how often to re-probe for an MCP server while we have
|
|
1248
|
+
// ZERO connections. Runs only during genuine downtime and stops the moment
|
|
1249
|
+
// a server connects, so the unavoidable connection-refused console noise is
|
|
1250
|
+
// bounded to periods when nothing is connected anyway.
|
|
1251
|
+
var BACKGROUND_RESCAN_MS = 12000;
|
|
910
1252
|
|
|
911
1253
|
function wsScanAndConnect() {
|
|
912
1254
|
if (isScanning) return;
|
|
@@ -959,13 +1301,21 @@
|
|
|
959
1301
|
isScanning = false;
|
|
960
1302
|
// Retry with backoff if no servers found, up to MAX_INITIAL_SCANS
|
|
961
1303
|
if (!foundAny && activeConnections.length === 0) {
|
|
962
|
-
|
|
1304
|
+
// Fast burst on load: a couple of quick retries with backoff.
|
|
1305
|
+
// Once the burst exhausts we STOP re-arming here — the background
|
|
1306
|
+
// watchdog (BACKGROUND_RESCAN_MS) takes over and keeps probing
|
|
1307
|
+
// slowly, so a server that starts AFTER the plugin still connects
|
|
1308
|
+
// without a restart. The guard also prevents watchdog-triggered
|
|
1309
|
+
// scans from incrementing the counter or logging on every cycle.
|
|
963
1310
|
if (initialScanAttempts < MAX_INITIAL_SCANS) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1311
|
+
initialScanAttempts++;
|
|
1312
|
+
if (initialScanAttempts < MAX_INITIAL_SCANS) {
|
|
1313
|
+
var delay = 3000 * initialScanAttempts; // 3s, 6s
|
|
1314
|
+
console.log('[MCP Bridge] No servers found, retry ' + initialScanAttempts + '/' + MAX_INITIAL_SCANS + ' in ' + (delay/1000) + 's');
|
|
1315
|
+
setTimeout(wsScanAndConnect, delay);
|
|
1316
|
+
} else {
|
|
1317
|
+
console.log('[MCP Bridge] No MCP server yet — watchdog will keep probing every ' + (BACKGROUND_RESCAN_MS/1000) + 's until one appears (no restart needed).');
|
|
1318
|
+
}
|
|
969
1319
|
}
|
|
970
1320
|
}
|
|
971
1321
|
}
|
|
@@ -1058,8 +1408,10 @@
|
|
|
1058
1408
|
event.reason === 'Replaced by new connection' ||
|
|
1059
1409
|
event.reason === 'Replaced by same file reconnection'
|
|
1060
1410
|
));
|
|
1061
|
-
|
|
1062
|
-
|
|
1411
|
+
// If user paused via the Pause button, also stop auto-reconnect.
|
|
1412
|
+
var wasManualPause = (event.code === 1000 && event.reason === 'Manual disconnect');
|
|
1413
|
+
if (wasReplaced || wasManualPause) {
|
|
1414
|
+
console.log('[MCP Bridge] WebSocket:' + port + ': stopped by ' + (wasManualPause ? 'user pause' : 'replacement'));
|
|
1063
1415
|
return;
|
|
1064
1416
|
}
|
|
1065
1417
|
|
|
@@ -1151,9 +1503,46 @@
|
|
|
1151
1503
|
|
|
1152
1504
|
window.__wsGetActiveConnections = function() { return activeConnections; };
|
|
1153
1505
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1506
|
+
window.__wsDisconnectAll = function() {
|
|
1507
|
+
for (var i = 0; i < activeConnections.length; i++) {
|
|
1508
|
+
try { activeConnections[i].ws.close(1000, 'Manual disconnect'); } catch (e) {}
|
|
1509
|
+
}
|
|
1510
|
+
activeConnections = [];
|
|
1511
|
+
ws = null;
|
|
1512
|
+
wsConnected = false;
|
|
1513
|
+
// Reset scan state so a future Resume gets a clean retry budget.
|
|
1514
|
+
isScanning = false;
|
|
1515
|
+
initialScanAttempts = 0;
|
|
1516
|
+
wsReconnectAttempts = 0;
|
|
1517
|
+
// Deliberate user pause — keep the watchdog quiet until Resume/Reconnect.
|
|
1518
|
+
userPaused = true;
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
window.__wsScanAndConnect = wsScanAndConnect;
|
|
1522
|
+
|
|
1523
|
+
// Manual (re)connect from the UI: clear any pause, refresh the retry budget
|
|
1524
|
+
// so the user gets the responsive fast burst again, then scan.
|
|
1525
|
+
window.__wsManualScan = function() {
|
|
1526
|
+
userPaused = false;
|
|
1527
|
+
initialScanAttempts = 0;
|
|
1528
|
+
wsScanAndConnect();
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
window.__wsIsPaused = function() { return userPaused; };
|
|
1532
|
+
window.__wsIsScanning = function() { return isScanning; };
|
|
1533
|
+
|
|
1534
|
+
// Background watchdog — the fix for "plugin opened before the MCP client
|
|
1535
|
+
// started." While we hold zero connections and the user hasn't paused,
|
|
1536
|
+
// keep probing at a slow cadence. A late-starting server is picked up
|
|
1537
|
+
// automatically; the probing stops the instant a connection succeeds.
|
|
1538
|
+
setInterval(function() {
|
|
1539
|
+
if (userPaused || isScanning) return;
|
|
1540
|
+
if (activeConnections.length > 0) return;
|
|
1541
|
+
wsScanAndConnect();
|
|
1542
|
+
}, BACKGROUND_RESCAN_MS);
|
|
1543
|
+
|
|
1544
|
+
// Initial scan on load (fast burst). Thereafter the watchdog above keeps
|
|
1545
|
+
// probing while disconnected, and disconnect-triggered retries handle drops.
|
|
1157
1546
|
wsScanAndConnect();
|
|
1158
1547
|
})();
|
|
1159
1548
|
|
|
@@ -1163,19 +1552,502 @@
|
|
|
1163
1552
|
var cloudWs = null;
|
|
1164
1553
|
var CLOUD_RELAY_HOST = 'wss://figma-console-mcp.southleft.com';
|
|
1165
1554
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1555
|
+
// ============================================================================
|
|
1556
|
+
// 3-STAGE UI TOGGLES
|
|
1557
|
+
// Stage 1: row-top (always visible)
|
|
1558
|
+
// Stage 2: sub-toolbar (revealed by [+])
|
|
1559
|
+
// Stage 3: log panel (revealed by Show log)
|
|
1560
|
+
// Cloud pairing is an independent row triggered by the cloud icon.
|
|
1561
|
+
// ============================================================================
|
|
1562
|
+
|
|
1563
|
+
// Fixed 240px width matches Figma right-side nav min.
|
|
1564
|
+
// Height grows freely with content. If the plugin extends past Figma's
|
|
1565
|
+
// visible UI, the user drags the plugin window to reveal what's clipped.
|
|
1566
|
+
var PLUGIN_WIDTH = 240;
|
|
1567
|
+
|
|
1568
|
+
function sendResize() {
|
|
1569
|
+
// Force a layout pass so measurements reflect the current visible rows.
|
|
1570
|
+
void document.body.offsetHeight;
|
|
1571
|
+
// Measure the inner content element + body padding. Avoids stale
|
|
1572
|
+
// scrollHeight values when the iframe was previously larger.
|
|
1573
|
+
var wrap = document.querySelector('.wrap');
|
|
1574
|
+
if (!wrap) return;
|
|
1575
|
+
var bs = window.getComputedStyle(document.body);
|
|
1576
|
+
var h = wrap.offsetHeight
|
|
1577
|
+
+ (parseFloat(bs.paddingTop) || 0)
|
|
1578
|
+
+ (parseFloat(bs.paddingBottom) || 0);
|
|
1579
|
+
parent.postMessage({
|
|
1580
|
+
pluginMessage: { type: 'RESIZE_UI', width: PLUGIN_WIDTH, height: Math.ceil(h) }
|
|
1581
|
+
}, '*');
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function autoResize() {
|
|
1585
|
+
sendResize(); // immediate — fires before next paint so Figma can grow upward in the same frame
|
|
1586
|
+
requestAnimationFrame(function() {
|
|
1587
|
+
sendResize();
|
|
1588
|
+
setTimeout(sendResize, 150);
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Observe the wrap (actual content), not body, so size changes from any
|
|
1593
|
+
// row toggle fire a resize whether the rows grew or shrank.
|
|
1594
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
1595
|
+
var _wrapObserve = function() {
|
|
1596
|
+
var wrap = document.querySelector('.wrap');
|
|
1597
|
+
if (wrap) { var ro = new ResizeObserver(sendResize); ro.observe(wrap); }
|
|
1598
|
+
};
|
|
1599
|
+
_wrapObserve();
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// Initial tighten — multiple attempts at different times to defeat any
|
|
1603
|
+
// residual Figma-side iframe sizing from a previous plugin state.
|
|
1604
|
+
sendResize();
|
|
1605
|
+
requestAnimationFrame(sendResize);
|
|
1606
|
+
window.addEventListener('load', function() {
|
|
1607
|
+
sendResize();
|
|
1608
|
+
setTimeout(sendResize, 50);
|
|
1609
|
+
setTimeout(sendResize, 200);
|
|
1610
|
+
setTimeout(sendResize, 600);
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
function toggleRow(id, buttonId) {
|
|
1614
|
+
var row = document.getElementById(id);
|
|
1615
|
+
var btn = buttonId ? document.getElementById(buttonId) : null;
|
|
1616
|
+
var opening = !row.classList.contains('visible');
|
|
1617
|
+
row.classList.toggle('visible', opening);
|
|
1618
|
+
if (btn) {
|
|
1619
|
+
btn.classList.toggle('active', opening);
|
|
1620
|
+
// Keep aria-expanded in sync for screen readers.
|
|
1621
|
+
if (btn.hasAttribute('aria-expanded')) {
|
|
1622
|
+
btn.setAttribute('aria-expanded', opening ? 'true' : 'false');
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
autoResize();
|
|
1626
|
+
return opening;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function closeSubToolbarIfOpen() {
|
|
1630
|
+
var sub = document.getElementById('sub-toolbar');
|
|
1631
|
+
if (sub && sub.classList.contains('visible')) toggleSubToolbar();
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function closeCloudPairIfOpen() {
|
|
1635
|
+
var cp = document.getElementById('cloud-pair');
|
|
1636
|
+
if (cp && cp.classList.contains('visible')) {
|
|
1637
|
+
toggleRow('cloud-pair', 'cloud-icon');
|
|
1638
|
+
// Close the help too if it was open
|
|
1639
|
+
var ch = document.getElementById('cloud-help');
|
|
1640
|
+
if (ch && ch.classList.contains('visible')) toggleRow('cloud-help', 'cloud-info-btn');
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function toggleCloudPair() {
|
|
1645
|
+
var opening = !document.getElementById('cloud-pair').classList.contains('visible');
|
|
1646
|
+
if (opening) closeSubToolbarIfOpen();
|
|
1647
|
+
toggleRow('cloud-pair', 'cloud-icon');
|
|
1648
|
+
if (!opening) {
|
|
1649
|
+
var s = document.getElementById('cloud-status');
|
|
1650
|
+
if (s) { s.textContent = ''; s.className = 'cloud-status'; }
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1172
1653
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
var height = isExpanding ? 130 : 50;
|
|
1176
|
-
parent.postMessage({ pluginMessage: { type: 'RESIZE_UI', width: 180, height: height } }, '*');
|
|
1654
|
+
function toggleCloudHelp() {
|
|
1655
|
+
toggleRow('cloud-help', 'cloud-info-btn');
|
|
1177
1656
|
}
|
|
1178
1657
|
|
|
1658
|
+
function toggleSubToolbar() {
|
|
1659
|
+
var opening = !document.getElementById('sub-toolbar').classList.contains('visible');
|
|
1660
|
+
if (opening) closeCloudPairIfOpen();
|
|
1661
|
+
var nowOpen = toggleRow('sub-toolbar', 'expand-btn');
|
|
1662
|
+
document.getElementById('expand-btn').textContent = nowOpen ? '−' : '+';
|
|
1663
|
+
if (!nowOpen) {
|
|
1664
|
+
// Collapse Info + Log if sub-toolbar closes
|
|
1665
|
+
var info = document.getElementById('info-panel');
|
|
1666
|
+
var log = document.getElementById('log-panel');
|
|
1667
|
+
if (info.classList.contains('visible')) toggleInfo();
|
|
1668
|
+
if (log.classList.contains('visible')) toggleLog();
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function toggleInfo() {
|
|
1673
|
+
var opening = !document.getElementById('info-panel').classList.contains('visible');
|
|
1674
|
+
if (opening && document.getElementById('log-panel').classList.contains('visible')) {
|
|
1675
|
+
// Close log first so only one panel is open at a time.
|
|
1676
|
+
toggleLog();
|
|
1677
|
+
}
|
|
1678
|
+
toggleRow('info-panel', 'info-toggle');
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function toggleLog() {
|
|
1682
|
+
var logVisible = document.getElementById('log-panel').classList.contains('visible');
|
|
1683
|
+
var opening = !logVisible;
|
|
1684
|
+
if (opening && document.getElementById('info-panel').classList.contains('visible')) {
|
|
1685
|
+
// Close info first so only one panel is open at a time.
|
|
1686
|
+
toggleRow('info-panel', 'info-toggle');
|
|
1687
|
+
}
|
|
1688
|
+
toggleRow('log-panel', 'log-toggle');
|
|
1689
|
+
// Reveal Errors + Copy buttons only when log panel is open
|
|
1690
|
+
document.getElementById('errors-toggle').style.display = opening ? '' : 'none';
|
|
1691
|
+
document.getElementById('copy-log-btn').style.display = opening ? '' : 'none';
|
|
1692
|
+
autoResize();
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function toggleErrorsOnly() {
|
|
1696
|
+
var btn = document.getElementById('errors-toggle');
|
|
1697
|
+
var entries = document.getElementById('log-entries');
|
|
1698
|
+
var on = !btn.classList.contains('active');
|
|
1699
|
+
btn.classList.toggle('active', on);
|
|
1700
|
+
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
1701
|
+
entries.classList.toggle('errors-only', on);
|
|
1702
|
+
btn.setAttribute('title', on ? 'Remove error filter' : 'Filter errors only');
|
|
1703
|
+
btn.setAttribute('aria-label', on ? 'Remove error filter' : 'Filter errors only');
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Follow Figma's theme. With themeColors:true, Figma adds a "figma-light" /
|
|
1707
|
+
// "figma-dark" class to <html> and updates it live when the user switches
|
|
1708
|
+
// themes. We mirror that onto body[data-theme] so the status/log color
|
|
1709
|
+
// tokens track Figma. Falls back to OS preference if the class is absent.
|
|
1710
|
+
function applyTheme() {
|
|
1711
|
+
var root = document.documentElement;
|
|
1712
|
+
var theme;
|
|
1713
|
+
if (root.classList.contains('figma-light')) theme = 'light';
|
|
1714
|
+
else if (root.classList.contains('figma-dark')) theme = 'dark';
|
|
1715
|
+
else theme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark';
|
|
1716
|
+
document.body.setAttribute('data-theme', theme);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
applyTheme();
|
|
1720
|
+
// React to live Figma theme switches (class changes on <html>).
|
|
1721
|
+
if (window.MutationObserver) {
|
|
1722
|
+
new MutationObserver(applyTheme).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
1723
|
+
}
|
|
1724
|
+
// Fallback path: only relevant when no Figma theme class is present.
|
|
1725
|
+
if (window.matchMedia) {
|
|
1726
|
+
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', applyTheme);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
// ============================================================================
|
|
1731
|
+
// STAGE 2 — LOG PANEL, INFO PANEL, CONNECTION, CLIPBOARD EXPORT
|
|
1732
|
+
// ============================================================================
|
|
1733
|
+
|
|
1734
|
+
var PLUGIN_VERSION = 'v0.3.0';
|
|
1735
|
+
var logHistory = [];
|
|
1736
|
+
var logEntriesEl = null;
|
|
1737
|
+
var _ctaBtn = null;
|
|
1738
|
+
|
|
1739
|
+
function ensureLogRefs() {
|
|
1740
|
+
if (!logEntriesEl) logEntriesEl = document.getElementById('log-entries');
|
|
1741
|
+
if (!_ctaBtn) _ctaBtn = document.getElementById('cta-btn');
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function escHtml(s) {
|
|
1745
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function log(message, level, ts) {
|
|
1749
|
+
level = level || 'info';
|
|
1750
|
+
ensureLogRefs();
|
|
1751
|
+
if (!logEntriesEl) return;
|
|
1752
|
+
|
|
1753
|
+
// Deduplicate: consecutive identical message+level bumps the count badge.
|
|
1754
|
+
var last = logEntriesEl.lastElementChild;
|
|
1755
|
+
if (last && last.dataset.logMsg === message && last.dataset.logLevel === level) {
|
|
1756
|
+
var n = (parseInt(last.dataset.logCount, 10) || 1) + 1;
|
|
1757
|
+
last.dataset.logCount = n;
|
|
1758
|
+
var tsEl = last.querySelector('.log-ts');
|
|
1759
|
+
var countEl = last.querySelector('.log-count');
|
|
1760
|
+
if (tsEl && ts) tsEl.textContent = ts;
|
|
1761
|
+
if (countEl) countEl.textContent = '×' + n;
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
var entry = document.createElement('div');
|
|
1766
|
+
entry.className = 'log-entry ' + level;
|
|
1767
|
+
entry.dataset.logMsg = message;
|
|
1768
|
+
entry.dataset.logLevel = level;
|
|
1769
|
+
entry.dataset.logCount = '1';
|
|
1770
|
+
entry.innerHTML =
|
|
1771
|
+
'<span class="log-ts">' + escHtml(ts || '') + '</span>' +
|
|
1772
|
+
'<span class="log-msg" title="' + escHtml(message) + '">' + escHtml(message) + '</span>' +
|
|
1773
|
+
'<span class="log-dur"></span>' +
|
|
1774
|
+
'<span class="log-count"></span>';
|
|
1775
|
+
logEntriesEl.appendChild(entry);
|
|
1776
|
+
logEntriesEl.scrollTop = logEntriesEl.scrollHeight;
|
|
1777
|
+
while (logEntriesEl.children.length > 50) {
|
|
1778
|
+
logEntriesEl.removeChild(logEntriesEl.children[0]);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function logWithHistory(message, level) {
|
|
1783
|
+
level = level || 'info';
|
|
1784
|
+
var now = new Date();
|
|
1785
|
+
var ts = ('0' + now.getHours()).slice(-2) + ':' +
|
|
1786
|
+
('0' + now.getMinutes()).slice(-2) + ':' +
|
|
1787
|
+
('0' + now.getSeconds()).slice(-2);
|
|
1788
|
+
logHistory.push({ ts: now.toISOString().replace('T', ' ').replace(/\.\d+Z/, ''), level: level, message: message });
|
|
1789
|
+
log(message, level, ts);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function copyLogToClipboard() {
|
|
1793
|
+
var lines = logHistory.map(function(e) {
|
|
1794
|
+
var prefix = e.level === 'error' ? '[!] ' : e.level === 'warn' ? '[WARN] ' : '';
|
|
1795
|
+
return e.ts + ' ' + prefix + e.message;
|
|
1796
|
+
});
|
|
1797
|
+
var text = 'Figma Desktop Bridge - Session Log\n'
|
|
1798
|
+
+ 'Exported: ' + new Date().toISOString() + '\n'
|
|
1799
|
+
+ 'Plugin: ' + PLUGIN_VERSION + '\n'
|
|
1800
|
+
+ '------------------------------------------------\n'
|
|
1801
|
+
+ lines.join('\n') + '\n';
|
|
1802
|
+
var ta = document.createElement('textarea');
|
|
1803
|
+
ta.value = text;
|
|
1804
|
+
ta.style.position = 'fixed';
|
|
1805
|
+
ta.style.opacity = '0';
|
|
1806
|
+
document.body.appendChild(ta);
|
|
1807
|
+
ta.select();
|
|
1808
|
+
var ok = false;
|
|
1809
|
+
try { ok = document.execCommand('copy'); } catch (e) {}
|
|
1810
|
+
document.body.removeChild(ta);
|
|
1811
|
+
if (ok) {
|
|
1812
|
+
logWithHistory('Copied to pasteboard (' + logHistory.length + ' entries)', 'success');
|
|
1813
|
+
} else {
|
|
1814
|
+
logWithHistory('Copy failed - clipboard not available', 'error');
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// Smart command summariser: turns raw message types and EXECUTE_CODE
|
|
1819
|
+
// payloads into human-readable log lines.
|
|
1820
|
+
var COMMAND_LABELS = {
|
|
1821
|
+
'GET_FILE_INFO': 'Get file info',
|
|
1822
|
+
'REFRESH_VARIABLES': 'Refresh variables',
|
|
1823
|
+
'GET_LOCAL_COMPONENTS': 'Get local components',
|
|
1824
|
+
'CLEAR_CONSOLE': 'Clear console',
|
|
1825
|
+
'RELOAD_UI': 'Reload UI',
|
|
1826
|
+
'GET_VARIABLES_DATA': 'Get variables data',
|
|
1827
|
+
'RESIZE_UI': null // internal, don't log
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
var FIGMA_API_PATTERNS = [
|
|
1831
|
+
{ re: /figma\.create(\w+)/, fn: function(m) { return 'Create ' + camelToWords(m[1]); } },
|
|
1832
|
+
{ re: /figma\.getNodeByIdAsync/, fn: function() { return 'Get node'; } },
|
|
1833
|
+
{ re: /figma\.setCurrentPageAsync/, fn: function() { return 'Switch page'; } },
|
|
1834
|
+
{ re: /figma\.loadFontAsync/, fn: function() { return 'Load font'; } },
|
|
1835
|
+
{ re: /figma\.loadAllPagesAsync/, fn: function() { return 'Load all pages'; } },
|
|
1836
|
+
{ re: /figma\.currentPage\.findAll/, fn: function() { return 'Find nodes'; } },
|
|
1837
|
+
{ re: /figma\.currentPage\.findOne/, fn: function() { return 'Find node'; } },
|
|
1838
|
+
{ re: /figma\.(union|subtract|intersect|flatten)\b/, fn: function(m) { return 'Boolean ' + m[1]; } },
|
|
1839
|
+
{ re: /\.exportAsync/, fn: function() { return 'Export'; } },
|
|
1840
|
+
{ re: /\.clone\(\)/, fn: function() { return 'Clone node'; } },
|
|
1841
|
+
{ re: /\.remove\(\)/, fn: function() { return 'Remove node'; } },
|
|
1842
|
+
{ re: /\.appendChild\b/, fn: function() { return 'Append child'; } },
|
|
1843
|
+
{ re: /\.insertChild\b/, fn: function() { return 'Insert child'; } },
|
|
1844
|
+
{ re: /\.resize\(/, fn: function() { return 'Resize'; } },
|
|
1845
|
+
{ re: /\.characters\s*=/, fn: function() { return 'Set text'; } },
|
|
1846
|
+
{ re: /\.fills\s*=/, fn: function() { return 'Set fills'; } },
|
|
1847
|
+
{ re: /\.strokes\s*=/, fn: function() { return 'Set strokes'; } },
|
|
1848
|
+
{ re: /\.effects\s*=/, fn: function() { return 'Set effects'; } },
|
|
1849
|
+
{ re: /combineAsVariants/, fn: function() { return 'Combine as variants'; } },
|
|
1850
|
+
{ re: /swapComponent/, fn: function() { return 'Swap component'; } }
|
|
1851
|
+
];
|
|
1852
|
+
|
|
1853
|
+
function camelToWords(s) {
|
|
1854
|
+
return s.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function truncate(s, max) {
|
|
1858
|
+
max = max || 60;
|
|
1859
|
+
if (s.length <= max) return s;
|
|
1860
|
+
return s.substring(0, max - 1) + '…';
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function summariseCommand(type, params) {
|
|
1864
|
+
if (type === 'EXECUTE_CODE' && params && params.code) {
|
|
1865
|
+
var firstLine = params.code.trim().split('\n')[0].trim();
|
|
1866
|
+
if (firstLine.indexOf('//') === 0) {
|
|
1867
|
+
return truncate(firstLine.replace(/^\/\/\s*/, '').replace(/[—–]/g, '-'));
|
|
1868
|
+
}
|
|
1869
|
+
var hits = [];
|
|
1870
|
+
for (var i = 0; i < FIGMA_API_PATTERNS.length; i++) {
|
|
1871
|
+
var m = params.code.match(FIGMA_API_PATTERNS[i].re);
|
|
1872
|
+
if (m) hits.push(FIGMA_API_PATTERNS[i].fn(m));
|
|
1873
|
+
}
|
|
1874
|
+
if (hits.length > 0) {
|
|
1875
|
+
var unique = hits.filter(function(v, i, a) { return a.indexOf(v) === i; });
|
|
1876
|
+
// Drop "Get node" when it appears alongside a more specific operation — it's just boilerplate setup
|
|
1877
|
+
var meaningful = unique.length > 1
|
|
1878
|
+
? unique.filter(function(v) { return v !== 'Get node'; })
|
|
1879
|
+
: unique;
|
|
1880
|
+
return truncate(meaningful.join(', '));
|
|
1881
|
+
}
|
|
1882
|
+
// No recognisable Figma API pattern — prefix with <Code> so engineers can spot it, then show first line for context
|
|
1883
|
+
var lines = params.code.split('\n');
|
|
1884
|
+
for (var j = 0; j < lines.length; j++) {
|
|
1885
|
+
var line = lines[j].trim();
|
|
1886
|
+
if (line && line.indexOf('//') !== 0 && line.indexOf('/*') !== 0) {
|
|
1887
|
+
return truncate('<Code> ' + line);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return '<Code>';
|
|
1891
|
+
}
|
|
1892
|
+
if (COMMAND_LABELS.hasOwnProperty(type)) return COMMAND_LABELS[type];
|
|
1893
|
+
// Fallback: turn "RENAME_VARIABLE" into "Rename variable"
|
|
1894
|
+
return type.replace(/_/g, ' ').toLowerCase().replace(/^\w/, function(c) { return c.toUpperCase(); });
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// Wrap sendPluginCommand so every sent command is logged with a human summary and duration.
|
|
1898
|
+
var _origSendPluginCommand = window.sendPluginCommand;
|
|
1899
|
+
window.sendPluginCommand = function(type, params, timeoutMs) {
|
|
1900
|
+
var summary = summariseCommand(type, params);
|
|
1901
|
+
var durEntry = null;
|
|
1902
|
+
var histIdx = -1;
|
|
1903
|
+
if (summary) {
|
|
1904
|
+
logWithHistory(summary, 'info');
|
|
1905
|
+
histIdx = logHistory.length - 1;
|
|
1906
|
+
ensureLogRefs();
|
|
1907
|
+
durEntry = logEntriesEl ? logEntriesEl.lastElementChild : null;
|
|
1908
|
+
}
|
|
1909
|
+
var t0 = Date.now();
|
|
1910
|
+
return _origSendPluginCommand(type, params, timeoutMs).then(
|
|
1911
|
+
function(result) {
|
|
1912
|
+
if (durEntry) {
|
|
1913
|
+
var d = durEntry.querySelector('.log-dur');
|
|
1914
|
+
if (d) d.textContent = (Date.now() - t0) + 'ms';
|
|
1915
|
+
if (result && result.success === false) {
|
|
1916
|
+
durEntry.className = durEntry.className.replace(/\b(info|success|warn)\b/, 'error');
|
|
1917
|
+
durEntry.dataset.logLevel = 'error';
|
|
1918
|
+
var m = durEntry.querySelector('.log-msg');
|
|
1919
|
+
if (m && m.textContent.indexOf('[!]') !== 0) m.textContent = '[!] ' + m.textContent;
|
|
1920
|
+
if (histIdx >= 0 && logHistory[histIdx]) {
|
|
1921
|
+
logHistory[histIdx].level = 'error';
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
return result;
|
|
1926
|
+
},
|
|
1927
|
+
function(err) {
|
|
1928
|
+
if (durEntry) {
|
|
1929
|
+
var d = durEntry.querySelector('.log-dur');
|
|
1930
|
+
if (d) d.textContent = (Date.now() - t0) + 'ms';
|
|
1931
|
+
durEntry.className = durEntry.className.replace(/\b(info|success|warn)\b/, 'error');
|
|
1932
|
+
durEntry.dataset.logLevel = 'error';
|
|
1933
|
+
var m = durEntry.querySelector('.log-msg');
|
|
1934
|
+
if (m && m.textContent.indexOf('[!]') !== 0) m.textContent = '[!] ' + m.textContent;
|
|
1935
|
+
if (histIdx >= 0 && logHistory[histIdx]) {
|
|
1936
|
+
logHistory[histIdx].level = 'error';
|
|
1937
|
+
logHistory[histIdx].message = '[!] ' + logHistory[histIdx].message;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
throw err;
|
|
1941
|
+
}
|
|
1942
|
+
);
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
// Populate Info panel from GET_FILE_INFO response.
|
|
1946
|
+
function updateInfoPanel() {
|
|
1947
|
+
_origSendPluginCommand('GET_FILE_INFO', {}).then(function(result) {
|
|
1948
|
+
var info = (result && result.fileInfo) || result || {};
|
|
1949
|
+
var fileEl = document.getElementById('info-file');
|
|
1950
|
+
var pageEl = document.getElementById('info-page');
|
|
1951
|
+
if (fileEl && info.fileName) fileEl.textContent = info.fileName;
|
|
1952
|
+
if (pageEl && info.currentPage) pageEl.textContent = info.currentPage;
|
|
1953
|
+
// Version is TJ's hardcoded PLUGIN_VERSION; not displayed to avoid confusion
|
|
1954
|
+
// with the npm package version. Still kept in memory for audit export.
|
|
1955
|
+
if (info.pluginVersion) {
|
|
1956
|
+
PLUGIN_VERSION = 'v' + info.pluginVersion;
|
|
1957
|
+
}
|
|
1958
|
+
}).catch(function() { /* ignore */ });
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// Update "N server(s)" count in log header.
|
|
1962
|
+
function updateServerCount() {
|
|
1963
|
+
var n = 0;
|
|
1964
|
+
if (typeof window.__wsGetActiveConnections === 'function') {
|
|
1965
|
+
n = window.__wsGetActiveConnections().length;
|
|
1966
|
+
}
|
|
1967
|
+
var el = document.getElementById('log-servers');
|
|
1968
|
+
if (el) el.textContent = n + ' server(s)';
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// Real TURN ON / TURN OFF: toggles the local WebSocket bridge.
|
|
1972
|
+
function setCtaState(state) {
|
|
1973
|
+
ensureLogRefs();
|
|
1974
|
+
if (!_ctaBtn) return;
|
|
1975
|
+
if (state === 'on') { _ctaBtn.textContent = 'Pause'; _ctaBtn.disabled = false; }
|
|
1976
|
+
else if (state === 'paused') { _ctaBtn.textContent = 'Resume'; _ctaBtn.disabled = false; }
|
|
1977
|
+
else if (state === 'reconnect'){ _ctaBtn.textContent = 'Reconnect'; _ctaBtn.disabled = false; }
|
|
1978
|
+
else if (state === 'scanning') { _ctaBtn.textContent = 'In progress'; _ctaBtn.disabled = true; }
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
function toggleLocalConnection() {
|
|
1982
|
+
ensureLogRefs();
|
|
1983
|
+
if (!_ctaBtn) return;
|
|
1984
|
+
var label = _ctaBtn.textContent;
|
|
1985
|
+
if (label === 'Pause') {
|
|
1986
|
+
if (typeof window.__wsDisconnectAll === 'function') window.__wsDisconnectAll();
|
|
1987
|
+
setCtaState('paused');
|
|
1988
|
+
updateStatus('disconnected', false, false);
|
|
1989
|
+
logWithHistory('Paused', 'warn');
|
|
1990
|
+
} else if (label === 'Resume' || label === 'Reconnect') {
|
|
1991
|
+
setCtaState('scanning');
|
|
1992
|
+
updateStatus('connecting', false, false);
|
|
1993
|
+
logWithHistory((label === 'Reconnect' ? 'Reconnecting' : 'Resuming') + ', scanning ports 9223-9232', 'info');
|
|
1994
|
+
if (typeof window.__wsManualScan === 'function') window.__wsManualScan();
|
|
1995
|
+
else if (typeof window.__wsScanAndConnect === 'function') window.__wsScanAndConnect();
|
|
1996
|
+
// Watch for connection success or scan timeout.
|
|
1997
|
+
var attempts = 0;
|
|
1998
|
+
var poller = setInterval(function() {
|
|
1999
|
+
attempts++;
|
|
2000
|
+
var n = window.__wsGetActiveConnections ? window.__wsGetActiveConnections().length : 0;
|
|
2001
|
+
if (n > 0) {
|
|
2002
|
+
clearInterval(poller);
|
|
2003
|
+
setCtaState('on');
|
|
2004
|
+
updateStatus('ready', true, false);
|
|
2005
|
+
} else if (attempts > 20) { // ~10s max
|
|
2006
|
+
clearInterval(poller);
|
|
2007
|
+
setCtaState('paused');
|
|
2008
|
+
updateStatus('error', false, true);
|
|
2009
|
+
logWithHistory('Resume failed - no MCP server found', 'error');
|
|
2010
|
+
}
|
|
2011
|
+
}, 500);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// Keep the CTA button honest about the real connection state. The status
|
|
2016
|
+
// dot is driven by Figma-side data (variables loaded); this reconciles the
|
|
2017
|
+
// button with the actual number of live MCP server connections so a
|
|
2018
|
+
// never-connected or dropped plugin shows a clickable "Reconnect" instead
|
|
2019
|
+
// of a misleading "Pause". Skips while a scan is mid-flight (button disabled
|
|
2020
|
+
// or __wsIsScanning) to avoid fighting the transient state set on click.
|
|
2021
|
+
function reconcileCta() {
|
|
2022
|
+
ensureLogRefs();
|
|
2023
|
+
if (!_ctaBtn || _ctaBtn.disabled) return;
|
|
2024
|
+
if (typeof window.__wsIsScanning === 'function' && window.__wsIsScanning()) return;
|
|
2025
|
+
var n = (typeof window.__wsGetActiveConnections === 'function') ? window.__wsGetActiveConnections().length : 0;
|
|
2026
|
+
var paused = (typeof window.__wsIsPaused === 'function') ? window.__wsIsPaused() : false;
|
|
2027
|
+
if (paused) {
|
|
2028
|
+
if (_ctaBtn.textContent !== 'Resume') setCtaState('paused');
|
|
2029
|
+
} else if (n > 0) {
|
|
2030
|
+
if (_ctaBtn.textContent !== 'Pause') setCtaState('on');
|
|
2031
|
+
} else {
|
|
2032
|
+
if (_ctaBtn.textContent !== 'Reconnect') setCtaState('reconnect');
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// Periodic refresh of server count, info panel, and CTA state (cheap, <1ms).
|
|
2037
|
+
setInterval(function() {
|
|
2038
|
+
updateServerCount();
|
|
2039
|
+
reconcileCta();
|
|
2040
|
+
// Only refresh info if the panel is visible (cheap gate).
|
|
2041
|
+
var info = document.getElementById('info-panel');
|
|
2042
|
+
if (info && info.classList.contains('visible')) updateInfoPanel();
|
|
2043
|
+
}, 2000);
|
|
2044
|
+
|
|
2045
|
+
// One-shot on first ready.
|
|
2046
|
+
setTimeout(function() { updateServerCount(); updateInfoPanel(); }, 500);
|
|
2047
|
+
|
|
2048
|
+
// Size the plugin window to fit content on first paint.
|
|
2049
|
+
requestAnimationFrame(autoResize);
|
|
2050
|
+
|
|
1179
2051
|
function resetCloudUI() {
|
|
1180
2052
|
var statusEl = document.getElementById('cloud-status');
|
|
1181
2053
|
var btn = document.getElementById('cloud-btn');
|