@clovis500c/figma-bridge 0.0.0-stage → 1.8.0
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/LICENSE +21 -0
- package/README.md +171 -2
- package/dist/server.js +31012 -0
- package/package.json +61 -4
- package/plugin/code.js +4189 -0
- package/plugin/manifest.json +15 -0
- package/plugin/ui.html +1206 -0
package/plugin/ui.html
ADDED
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<style>
|
|
6
|
+
/* ── Tokens (light by default, dark when Figma is dark) ─────────────────── */
|
|
7
|
+
:root {
|
|
8
|
+
--bg: #ffffff;
|
|
9
|
+
--surface: #f6f6f7;
|
|
10
|
+
--surface-2: #ececef;
|
|
11
|
+
--hover: rgba(0, 0, 0, .04);
|
|
12
|
+
--border: rgba(0, 0, 0, .08);
|
|
13
|
+
--border-strong: rgba(0, 0, 0, .14);
|
|
14
|
+
--text: #111113;
|
|
15
|
+
--text-2: #5d5d66;
|
|
16
|
+
--text-3: #9a9aa3;
|
|
17
|
+
--ok: #16a34a;
|
|
18
|
+
--warn: #d97706;
|
|
19
|
+
--err: #e5484d;
|
|
20
|
+
--accent: #0d99ff;
|
|
21
|
+
--accent-hover: #0b8ae6;
|
|
22
|
+
--accent-soft: rgba(13, 153, 255, .12);
|
|
23
|
+
--on-accent: #ffffff;
|
|
24
|
+
--claude: #d97757;
|
|
25
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, .06);
|
|
26
|
+
--r: 10px;
|
|
27
|
+
--ease: cubic-bezier(.2, .8, .2, 1);
|
|
28
|
+
color-scheme: light;
|
|
29
|
+
}
|
|
30
|
+
html.figma-dark {
|
|
31
|
+
--bg: #1a1a1d;
|
|
32
|
+
--surface: #222226;
|
|
33
|
+
--surface-2: #2b2b30;
|
|
34
|
+
--hover: rgba(255, 255, 255, .045);
|
|
35
|
+
--border: rgba(255, 255, 255, .08);
|
|
36
|
+
--border-strong: rgba(255, 255, 255, .15);
|
|
37
|
+
--text: #f4f4f5;
|
|
38
|
+
--text-2: #a8a8b3;
|
|
39
|
+
--text-3: #6e6e78;
|
|
40
|
+
--ok: #34d399;
|
|
41
|
+
--warn: #fbbf24;
|
|
42
|
+
--err: #f87171;
|
|
43
|
+
--accent: #0d99ff;
|
|
44
|
+
--accent-hover: #2aa5ff;
|
|
45
|
+
--accent-soft: rgba(13, 153, 255, .16);
|
|
46
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, .35);
|
|
47
|
+
color-scheme: dark;
|
|
48
|
+
}
|
|
49
|
+
@media (prefers-color-scheme: dark) {
|
|
50
|
+
html:not(.figma-light):not(.figma-dark) {
|
|
51
|
+
--bg: #1a1a1d; --surface: #222226; --surface-2: #2b2b30; --hover: rgba(255,255,255,.045);
|
|
52
|
+
--border: rgba(255,255,255,.08); --border-strong: rgba(255,255,255,.15);
|
|
53
|
+
--text: #f4f4f5; --text-2: #a8a8b3; --text-3: #6e6e78;
|
|
54
|
+
--ok: #34d399; --warn: #fbbf24; --err: #f87171; --accent: #0d99ff; --accent-hover: #2aa5ff; --accent-soft: rgba(13,153,255,.16);
|
|
55
|
+
--shadow: 0 1px 2px rgba(0,0,0,.35);
|
|
56
|
+
color-scheme: dark;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
61
|
+
html, body { height: 100%; }
|
|
62
|
+
body {
|
|
63
|
+
background: var(--bg); color: var(--text);
|
|
64
|
+
font: 12px/1.45 "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
|
|
65
|
+
-webkit-font-smoothing: antialiased; overflow: hidden; user-select: none;
|
|
66
|
+
}
|
|
67
|
+
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
|
|
68
|
+
svg { flex: none; display: block; }
|
|
69
|
+
.mono { font-family: "JetBrains Mono", "Cascadia Mono", "Cascadia Code", Consolas, monospace; font-size: 11px; }
|
|
70
|
+
.trunc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
71
|
+
|
|
72
|
+
#app { display: flex; flex-direction: column; height: 100%; }
|
|
73
|
+
|
|
74
|
+
/* ── Top bar ────────────────────────────────────────────────────────────── */
|
|
75
|
+
.top { display: flex; align-items: center; gap: 8px; height: 44px; padding: 0 8px 0 12px; border-bottom: 1px solid var(--border); flex: none; }
|
|
76
|
+
.mark {
|
|
77
|
+
width: 22px; height: 22px; border-radius: 6px; background: var(--text); color: var(--bg);
|
|
78
|
+
display: grid; place-items: center; flex: none; position: relative;
|
|
79
|
+
}
|
|
80
|
+
.name { font-weight: 650; font-size: 12.5px; letter-spacing: -.01em; flex: 1; }
|
|
81
|
+
.pill {
|
|
82
|
+
display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 8px 0 7px; border-radius: 99px;
|
|
83
|
+
font-size: 11px; font-weight: 550; color: var(--state); background: color-mix(in srgb, var(--state) 12%, transparent);
|
|
84
|
+
transition: color .3s, background .3s;
|
|
85
|
+
}
|
|
86
|
+
.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--state); position: relative; flex: none; }
|
|
87
|
+
.dot::after { content: ""; position: absolute; inset: -3px; border-radius: 50%; border: 1.5px solid var(--state); opacity: 0; }
|
|
88
|
+
.s-connecting .dot::after, .busy .dot::after { animation: ping 1.3s cubic-bezier(0,0,.2,1) infinite; }
|
|
89
|
+
@keyframes ping { from { transform: scale(.5); opacity: .9; } to { transform: scale(1.8); opacity: 0; } }
|
|
90
|
+
.ibtn { width: 28px; height: 28px; border-radius: 7px; display: grid; place-items: center; color: var(--text-2); transition: background .15s, color .15s; }
|
|
91
|
+
.ibtn:hover { background: var(--hover); color: var(--text); }
|
|
92
|
+
.ibtn:active { background: var(--surface-2); }
|
|
93
|
+
|
|
94
|
+
.s-connected { --state: var(--ok); }
|
|
95
|
+
.s-connecting { --state: var(--warn); }
|
|
96
|
+
.s-offline { --state: var(--text-3); }
|
|
97
|
+
.s-foreign { --state: var(--err); }
|
|
98
|
+
|
|
99
|
+
/* ── Bridge visual ──────────────────────────────────────────────────────── */
|
|
100
|
+
.bridge { padding: 16px 16px 14px; flex: none; }
|
|
101
|
+
.bridge-row { display: flex; align-items: center; }
|
|
102
|
+
.tile {
|
|
103
|
+
width: 40px; height: 40px; border-radius: 12px; display: grid; place-items: center; flex: none; position: relative;
|
|
104
|
+
background: var(--surface); border: 1px solid var(--border); color: var(--text-2);
|
|
105
|
+
transition: border-color .3s, color .3s;
|
|
106
|
+
}
|
|
107
|
+
.tile.claude { color: var(--claude); }
|
|
108
|
+
.s-connected .tile { border-color: var(--border-strong); }
|
|
109
|
+
.s-connected .tile.figma { color: var(--accent); }
|
|
110
|
+
.tile.hit { animation: hit .4s var(--ease); }
|
|
111
|
+
@keyframes hit { 40% { border-color: var(--accent); } }
|
|
112
|
+
.link { flex: 1; height: 40px; position: relative; margin: 0 6px; }
|
|
113
|
+
.wire { position: absolute; left: 0; right: 0; top: 50%; height: 1.5px; margin-top: -.75px; transition: background .3s; }
|
|
114
|
+
.s-connected .wire { background: var(--border-strong); }
|
|
115
|
+
.s-connected.busy .wire { background: var(--accent); }
|
|
116
|
+
.s-connecting .wire, .s-offline .wire, .s-foreign .wire {
|
|
117
|
+
background: repeating-linear-gradient(90deg, var(--state) 0 5px, transparent 5px 10px); opacity: .6;
|
|
118
|
+
}
|
|
119
|
+
.s-connecting .wire { animation: dash .6s linear infinite; background-size: 20px 2px; }
|
|
120
|
+
@keyframes dash { to { background-position: 20px 0; } }
|
|
121
|
+
.badge {
|
|
122
|
+
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); white-space: nowrap;
|
|
123
|
+
font-size: 10px; font-weight: 600; letter-spacing: .02em; padding: 2px 7px; border-radius: 99px;
|
|
124
|
+
background: var(--bg); color: var(--state); border: 1px solid color-mix(in srgb, var(--state) 35%, var(--border));
|
|
125
|
+
}
|
|
126
|
+
.s-connected:not(.busy) .badge { display: none; }
|
|
127
|
+
.packet {
|
|
128
|
+
position: absolute; top: 50%; width: 8px; height: 8px; margin: -4px 0 0 -4px; border-radius: 50%;
|
|
129
|
+
background: var(--c); box-shadow: 0 0 0 2px var(--bg);
|
|
130
|
+
}
|
|
131
|
+
.labels { display: flex; justify-content: space-between; gap: 12px; margin-top: 9px; }
|
|
132
|
+
.lbl { display: flex; flex-direction: column; min-width: 0; max-width: 50%; }
|
|
133
|
+
.lbl.right { align-items: flex-end; text-align: right; }
|
|
134
|
+
.lbl b { font-weight: 600; font-size: 12px; }
|
|
135
|
+
.lbl span { color: var(--text-3); font-size: 11px; }
|
|
136
|
+
|
|
137
|
+
/* ── Banners (agent waiting, version mismatch) ──────────────────────────── */
|
|
138
|
+
.banner {
|
|
139
|
+
display: none; align-items: center; gap: 10px; margin: 12px 12px 0; padding: 9px 8px 9px 10px; border-radius: var(--r); flex: none;
|
|
140
|
+
background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
|
|
141
|
+
}
|
|
142
|
+
.banner .bico { width: 26px; height: 26px; border-radius: 8px; display: grid; place-items: center; flex: none; color: var(--accent); background: var(--bg); }
|
|
143
|
+
.banner .main b { display: block; font-weight: 600; }
|
|
144
|
+
.banner .main span { display: block; color: var(--text-2); }
|
|
145
|
+
.waiting #waitBar { display: flex; }
|
|
146
|
+
.banner.warn { background: color-mix(in srgb, var(--warn) 12%, transparent); border-color: color-mix(in srgb, var(--warn) 35%, transparent); }
|
|
147
|
+
.banner.warn .bico { color: var(--warn); }
|
|
148
|
+
.mismatch #verBar { display: flex; }
|
|
149
|
+
.waiting #bar { background: var(--accent-soft); }
|
|
150
|
+
|
|
151
|
+
/* ── Setup checklist ────────────────────────────────────────────────────── */
|
|
152
|
+
.setup { margin: 0 12px 12px; padding: 12px; border-radius: var(--r); background: var(--surface); border: 1px solid var(--border); display: none; flex: none; }
|
|
153
|
+
.s-offline .setup, .s-foreign .setup { display: block; }
|
|
154
|
+
.check { display: flex; gap: 10px; padding: 5px 0; }
|
|
155
|
+
.check b { display: block; font-weight: 600; }
|
|
156
|
+
.check span { color: var(--text-2); }
|
|
157
|
+
.cico { width: 18px; height: 18px; border-radius: 50%; flex: none; display: grid; place-items: center; margin-top: 1px; }
|
|
158
|
+
.check.done .cico { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok); }
|
|
159
|
+
.check.wait .cico { border: 1.5px dashed var(--text-3); }
|
|
160
|
+
.check.fail .cico { background: color-mix(in srgb, var(--err) 18%, transparent); color: var(--err); }
|
|
161
|
+
.s-foreign #chkServer, .s-offline #chkPort { display: none; }
|
|
162
|
+
.cmd {
|
|
163
|
+
display: flex; align-items: center; gap: 6px; margin-top: 6px; padding: 4px 4px 4px 8px; border-radius: 7px;
|
|
164
|
+
background: var(--bg); border: 1px solid var(--border); user-select: text;
|
|
165
|
+
}
|
|
166
|
+
.cmd code { flex: 1; }
|
|
167
|
+
.setup-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }
|
|
168
|
+
.btn {
|
|
169
|
+
height: 28px; padding: 0 12px; border-radius: 6px; font-weight: 600; color: var(--on-accent); background: var(--accent);
|
|
170
|
+
transition: background .15s, transform .06s;
|
|
171
|
+
}
|
|
172
|
+
.btn:hover { background: var(--accent-hover); }
|
|
173
|
+
.btn:active { transform: translateY(1px); }
|
|
174
|
+
.btn.quiet { background: var(--surface-2); color: var(--text); box-shadow: inset 0 0 0 1px var(--border); }
|
|
175
|
+
.mini {
|
|
176
|
+
height: 22px; padding: 0 8px; border-radius: 6px; font-size: 11px; font-weight: 600; color: var(--text);
|
|
177
|
+
background: var(--bg); box-shadow: inset 0 0 0 1px var(--border-strong); transition: color .15s, box-shadow .15s;
|
|
178
|
+
}
|
|
179
|
+
.mini:hover { color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
|
|
180
|
+
.countdown { color: var(--text-3); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
181
|
+
|
|
182
|
+
/* ── Metrics ────────────────────────────────────────────────────────────── */
|
|
183
|
+
.metrics { display: flex; margin: 0 12px; border: 1px solid var(--border); border-radius: var(--r); flex: none; overflow: hidden; }
|
|
184
|
+
.metric { flex: 1; padding: 8px 10px; min-width: 0; position: relative; }
|
|
185
|
+
.metric + .metric { border-left: 1px solid var(--border); }
|
|
186
|
+
.metric .v { font-size: 15px; font-weight: 650; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
|
|
187
|
+
.metric .k { color: var(--text-3); font-size: 10.5px; }
|
|
188
|
+
.metric.bad .v { color: var(--err); }
|
|
189
|
+
.metric.wide { flex: 1.4; }
|
|
190
|
+
.s-offline.idle .metrics, .s-foreign.idle .metrics { display: none; }
|
|
191
|
+
#spark { position: absolute; right: 8px; top: 10px; width: 46px; height: 18px; }
|
|
192
|
+
|
|
193
|
+
/* ── Activity ───────────────────────────────────────────────────────────── */
|
|
194
|
+
.act-head { display: flex; align-items: center; gap: 8px; padding: 14px 8px 6px 14px; flex: none; }
|
|
195
|
+
.act-head h2 { font-size: 11px; font-weight: 650; color: var(--text-2); letter-spacing: .04em; text-transform: uppercase; flex: 1; }
|
|
196
|
+
.seg { display: inline-flex; padding: 2px; border-radius: 8px; background: var(--surface); box-shadow: inset 0 0 0 1px var(--border); }
|
|
197
|
+
.seg button { height: 20px; padding: 0 8px; border-radius: 6px; font-size: 11px; font-weight: 550; color: var(--text-2); display: inline-flex; align-items: center; gap: 5px; }
|
|
198
|
+
.seg button.on { background: var(--bg); color: var(--text); box-shadow: var(--shadow), inset 0 0 0 1px var(--border); }
|
|
199
|
+
.count { font-size: 10px; min-width: 15px; height: 15px; padding: 0 4px; border-radius: 99px; background: var(--err); color: #fff; display: none; place-items: center; }
|
|
200
|
+
.count.show { display: inline-grid; }
|
|
201
|
+
|
|
202
|
+
#log { list-style: none; flex: 1; overflow-y: auto; padding: 0 6px 8px; }
|
|
203
|
+
#log::-webkit-scrollbar { width: 10px; }
|
|
204
|
+
#log::-webkit-scrollbar-thumb { background: var(--surface-2); border-radius: 10px; border: 3px solid var(--bg); }
|
|
205
|
+
.row { display: flex; gap: 10px; padding: 7px 8px; border-radius: 9px; cursor: pointer; transition: background .12s; }
|
|
206
|
+
.row:hover { background: var(--hover); }
|
|
207
|
+
.row.open { background: var(--surface); box-shadow: inset 0 0 0 1px var(--border); }
|
|
208
|
+
.row.new { animation: rowin .25s var(--ease); }
|
|
209
|
+
@keyframes rowin { from { opacity: 0; transform: translateY(-5px); } }
|
|
210
|
+
.ic {
|
|
211
|
+
width: 26px; height: 26px; border-radius: 8px; flex: none; display: grid; place-items: center; position: relative;
|
|
212
|
+
color: var(--text-2); background: var(--surface); box-shadow: inset 0 0 0 1px var(--border);
|
|
213
|
+
}
|
|
214
|
+
.row.err .ic { color: var(--err); background: color-mix(in srgb, var(--err) 10%, transparent); box-shadow: none; }
|
|
215
|
+
.row.run .ic::after {
|
|
216
|
+
content: ""; position: absolute; inset: -3px; border-radius: 10px; border: 2px solid transparent;
|
|
217
|
+
border-top-color: var(--accent); animation: spin .8s linear infinite;
|
|
218
|
+
}
|
|
219
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
220
|
+
.main { flex: 1; min-width: 0; }
|
|
221
|
+
.l1 { display: flex; align-items: baseline; gap: 6px; }
|
|
222
|
+
.l1 .m { font-weight: 600; }
|
|
223
|
+
.l1 .t { color: var(--text-3); font-size: 10.5px; }
|
|
224
|
+
.l2 { color: var(--text-2); margin-top: 1px; }
|
|
225
|
+
.row.err .l2 { color: var(--err); }
|
|
226
|
+
.side { display: flex; align-items: center; gap: 4px; flex: none; align-self: flex-start; height: 18px; }
|
|
227
|
+
.dur { color: var(--text-3); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
228
|
+
.row.slow .dur { color: var(--warn); }
|
|
229
|
+
.row .quick { display: none; }
|
|
230
|
+
.row:hover .quick, .row.run .quick { display: inline-block; }
|
|
231
|
+
.row:hover .dur { display: none; }
|
|
232
|
+
.mini.danger:hover { color: var(--err); box-shadow: inset 0 0 0 1px var(--err); }
|
|
233
|
+
.row.cancelled .l2 { color: var(--text-3); }
|
|
234
|
+
.row.open.cancelled .l2 { display: none; }
|
|
235
|
+
.detail { display: none; margin-top: 8px; }
|
|
236
|
+
.row.open .detail { display: block; }
|
|
237
|
+
.row.open .l2 { white-space: normal; word-break: break-word; }
|
|
238
|
+
.row.open.err .l2 { display: none; }
|
|
239
|
+
.errbox {
|
|
240
|
+
padding: 7px 9px; border-radius: 7px; white-space: pre-wrap; word-break: break-word; user-select: text; cursor: text;
|
|
241
|
+
color: var(--err); background: color-mix(in srgb, var(--err) 9%, transparent);
|
|
242
|
+
}
|
|
243
|
+
.facts { display: flex; flex-wrap: wrap; gap: 4px 12px; color: var(--text-3); font-size: 11px; margin-top: 7px; }
|
|
244
|
+
.facts b { color: var(--text-2); font-weight: 550; }
|
|
245
|
+
.detail-actions { display: flex; gap: 6px; margin-top: 8px; }
|
|
246
|
+
|
|
247
|
+
.empty { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 6px; padding: 28px 24px; color: var(--text-3); }
|
|
248
|
+
.empty .art { width: 44px; height: 44px; border-radius: 13px; display: grid; place-items: center; background: var(--surface); border: 1px solid var(--border); color: var(--text-3); margin-bottom: 4px; }
|
|
249
|
+
.empty b { color: var(--text); font-size: 12.5px; font-weight: 600; }
|
|
250
|
+
.quote { margin-top: 6px; padding: 5px 10px; border-radius: 6px; background: var(--surface); box-shadow: inset 0 0 0 1px var(--border); color: var(--text-2); font-weight: 500; }
|
|
251
|
+
|
|
252
|
+
/* ── Footer + resize grip ───────────────────────────────────────────────── */
|
|
253
|
+
.foot {
|
|
254
|
+
display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 22px 0 12px; flex: none;
|
|
255
|
+
border-top: 1px solid var(--border); color: var(--text-3); font-size: 10.5px;
|
|
256
|
+
}
|
|
257
|
+
.foot .sep { width: 3px; height: 3px; border-radius: 50%; background: var(--text-3); opacity: .6; }
|
|
258
|
+
.foot .grow { flex: 1; }
|
|
259
|
+
#grip {
|
|
260
|
+
position: fixed; right: 0; bottom: 0; width: 18px; height: 18px; cursor: nwse-resize; z-index: 20;
|
|
261
|
+
display: grid; place-items: center; color: var(--text-3);
|
|
262
|
+
}
|
|
263
|
+
#grip:hover { color: var(--text); }
|
|
264
|
+
.compact #grip { display: none; }
|
|
265
|
+
|
|
266
|
+
/* ── Compact bar ────────────────────────────────────────────────────────── */
|
|
267
|
+
#bar { display: none; align-items: center; gap: 10px; height: 44px; padding: 0 6px 0 12px; }
|
|
268
|
+
.compact #app { display: none; }
|
|
269
|
+
.compact #bar { display: flex; }
|
|
270
|
+
#bar .mark .dot { position: absolute; right: -3px; bottom: -3px; width: 8px; height: 8px; box-shadow: 0 0 0 2px var(--bg); }
|
|
271
|
+
.bar-text { flex: 1; min-width: 0; line-height: 1.3; }
|
|
272
|
+
.bar-text b { display: block; font-size: 12px; font-weight: 600; }
|
|
273
|
+
.bar-text span { display: block; color: var(--text-3); font-size: 11px; }
|
|
274
|
+
|
|
275
|
+
/* ── Settings sheet ─────────────────────────────────────────────────────── */
|
|
276
|
+
.scrim { position: fixed; inset: 0; background: rgba(0,0,0,.28); opacity: 0; pointer-events: none; transition: opacity .2s; z-index: 30; }
|
|
277
|
+
.sheet {
|
|
278
|
+
position: fixed; top: 0; right: 0; bottom: 0; width: min(300px, 88%); background: var(--bg); z-index: 31;
|
|
279
|
+
border-left: 1px solid var(--border); box-shadow: -12px 0 30px -12px rgba(0,0,0,.35);
|
|
280
|
+
display: flex; flex-direction: column; transform: translateX(100%); transition: transform .25s var(--ease);
|
|
281
|
+
}
|
|
282
|
+
.settings-open .scrim { opacity: 1; pointer-events: auto; }
|
|
283
|
+
.settings-open .sheet { transform: none; }
|
|
284
|
+
.sheet-head { display: flex; align-items: center; height: 44px; padding: 0 8px 0 14px; border-bottom: 1px solid var(--border); }
|
|
285
|
+
.sheet-head b { flex: 1; font-size: 12.5px; }
|
|
286
|
+
.sheet-body { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 18px; }
|
|
287
|
+
.group h3 { font-size: 10.5px; font-weight: 650; color: var(--text-3); text-transform: uppercase; letter-spacing: .05em; margin-bottom: 8px; }
|
|
288
|
+
label { display: block; font-weight: 550; margin-bottom: 5px; }
|
|
289
|
+
input {
|
|
290
|
+
width: 100%; height: 30px; padding: 0 10px; border-radius: 8px; border: 0; outline: none; font: inherit;
|
|
291
|
+
background: var(--surface); color: var(--text); box-shadow: inset 0 0 0 1px var(--border);
|
|
292
|
+
}
|
|
293
|
+
input:focus { box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 3px var(--accent-soft); }
|
|
294
|
+
.hint { color: var(--text-3); font-size: 11px; margin-top: 5px; }
|
|
295
|
+
.kv { display: flex; justify-content: space-between; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--border); color: var(--text-2); }
|
|
296
|
+
.kv:last-child { border: 0; }
|
|
297
|
+
.kv .mono { color: var(--text); }
|
|
298
|
+
.sheet-foot { padding: 12px 14px; border-top: 1px solid var(--border); display: flex; gap: 8px; justify-content: flex-end; }
|
|
299
|
+
|
|
300
|
+
/* ── Toast ──────────────────────────────────────────────────────────────── */
|
|
301
|
+
#toast {
|
|
302
|
+
position: fixed; left: 50%; bottom: 38px; transform: translate(-50%, 8px); opacity: 0; pointer-events: none; z-index: 40;
|
|
303
|
+
padding: 6px 12px; border-radius: 8px; background: var(--text); color: var(--bg); font-weight: 550;
|
|
304
|
+
transition: opacity .18s, transform .2s var(--ease); box-shadow: var(--shadow);
|
|
305
|
+
}
|
|
306
|
+
#toast.show { opacity: 1; transform: translate(-50%, 0); }
|
|
307
|
+
</style>
|
|
308
|
+
</head>
|
|
309
|
+
<body class="s-connecting">
|
|
310
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
311
|
+
<defs>
|
|
312
|
+
<symbol id="i-mark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
|
313
|
+
<circle cx="5" cy="15.5" r="2.4"/><circle cx="19" cy="15.5" r="2.4"/><path d="M5 13c1.6-6 12.4-6 14 0"/>
|
|
314
|
+
</symbol>
|
|
315
|
+
<symbol id="i-spark" viewBox="0 0 24 24" fill="currentColor">
|
|
316
|
+
<path d="M12 2.5c.5 0 .9.4 1 .9l.6 4.3 3-3.1a1 1 0 0 1 1.6 1.2l-2 3.8 4.3-.6a1 1 0 0 1 .4 2l-4.2 1.3 3.8 2.1a1 1 0 0 1-.8 1.9l-4.1-1.6.9 4.2a1 1 0 0 1-1.9.6L12.6 16l-1.7 3.9a1 1 0 0 1-1.9-.6l.9-4.2-4.1 1.6a1 1 0 0 1-.8-1.9l3.8-2.1L4.6 11.4a1 1 0 0 1 .4-2l4.3.6-2-3.8a1 1 0 0 1 1.6-1.2l3 3.1.6-4.3c.1-.5.5-.9 1-.9z"/>
|
|
317
|
+
</symbol>
|
|
318
|
+
<symbol id="i-frame" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
319
|
+
<path d="M8 3v18M16 3v18M3 8h18M3 16h18"/>
|
|
320
|
+
</symbol>
|
|
321
|
+
<symbol id="i-script" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 8 4.5 12l4 4M15.5 8l4 4-4 4M13 5.5 11 18.5"/></symbol>
|
|
322
|
+
<symbol id="i-shot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8V5.5A1.5 1.5 0 0 1 5.5 4H8M16 4h2.5A1.5 1.5 0 0 1 20 5.5V8M20 16v2.5a1.5 1.5 0 0 1-1.5 1.5H16M8 20H5.5A1.5 1.5 0 0 1 4 18.5V16"/><circle cx="12" cy="12" r="3"/></symbol>
|
|
323
|
+
<symbol id="i-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><circle cx="9" cy="10" r="1.8"/><path d="m20 16-4.5-4.5L7 19.5"/></symbol>
|
|
324
|
+
<symbol id="i-pen" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19.5 19.5 12l2 2-7.5 7.5z"/><path d="m18 13.5-1.4-7L3 3l3.5 13.6 7 1.4z"/><path d="m3 3 7.2 7.2"/><circle cx="11.5" cy="11.5" r="1.8"/></symbol>
|
|
325
|
+
<symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3.5 8.5 4.5-8.5 4.5L3.5 8z"/><path d="m3.5 12.5 8.5 4.5 8.5-4.5"/><path d="m3.5 16.5 8.5 4.5 8.5-4.5" opacity=".5"/></symbol>
|
|
326
|
+
<symbol id="i-type" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V5h11v2M9.5 5v14M7.5 19h4M14 13v-1.5h7V13M17.5 11.5V19M16 19h3"/></symbol>
|
|
327
|
+
<symbol id="i-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4l2.5-6 5 12 2.5-6h4"/></symbol>
|
|
328
|
+
<symbol id="i-build" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3.5" width="17" height="17" rx="2.5"/><path d="M3.5 9h17M9 9v11.5"/></symbol>
|
|
329
|
+
<symbol id="i-tree" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h6M8 12h8M8 19h8M6 5v14h2M6 12h2"/></symbol>
|
|
330
|
+
<symbol id="i-swatch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="4"/><circle cx="16" cy="8" r="4"/><circle cx="12" cy="16" r="4"/></symbol>
|
|
331
|
+
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5 19.5 6v5.5c0 4.5-3.2 7.7-7.5 9-4.3-1.3-7.5-4.5-7.5-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
|
332
|
+
<symbol id="i-braces" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 4H7a2 2 0 0 0-2 2v3.5a2 2 0 0 1-2 2 2 2 0 0 1 2 2V18a2 2 0 0 0 2 2h1M16 4h1a2 2 0 0 1 2 2v3.5a2 2 0 0 0 2 2 2 2 0 0 0-2 2V18a2 2 0 0 1-2 2h-1"/></symbol>
|
|
333
|
+
<symbol id="i-history" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 1 0 2.5-6L3.5 8.5"/><path d="M3.5 4v4.5H8M12 8v4.5l3 2"/></symbol>
|
|
334
|
+
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6.5"/><path d="m20 20-4.4-4.4"/></symbol>
|
|
335
|
+
<symbol id="i-diff" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5" width="10" height="14" rx="2"/><path d="M17 5h1.5A2 2 0 0 1 20.5 7v10a2 2 0 0 1-2 2H17" stroke-dasharray="2 2.5"/><path d="M8.5 9.5v5M6 12h5"/></symbol>
|
|
336
|
+
<symbol id="i-pointer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 3.5 13.5 6-5.8 1.9-2 5.9z"/><path d="m13 13 5.5 5.5"/></symbol>
|
|
337
|
+
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M10.2 8.8v6.4l5-3.2z"/></symbol>
|
|
338
|
+
<symbol id="i-note" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4.5h14a1.5 1.5 0 0 1 1.5 1.5v9.5a1.5 1.5 0 0 1-1.5 1.5H12l-4.5 3.5V17H5A1.5 1.5 0 0 1 3.5 15.5V6A1.5 1.5 0 0 1 5 4.5z"/><path d="M8 9h8M8 12.5h5"/></symbol>
|
|
339
|
+
<symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4 21 19.5H3z"/><path d="M12 10v4.5M12 17.3v.2"/></symbol>
|
|
340
|
+
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m5.5 12.5 4 4 9-9"/></symbol>
|
|
341
|
+
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"><path d="M7 7l10 10M17 7 7 17"/></symbol>
|
|
342
|
+
</defs>
|
|
343
|
+
</svg>
|
|
344
|
+
|
|
345
|
+
<div id="app">
|
|
346
|
+
<header class="top">
|
|
347
|
+
<div class="mark"><svg width="14" height="14"><use href="#i-mark"/></svg></div>
|
|
348
|
+
<div class="name">Figma Bridge</div>
|
|
349
|
+
<span class="pill"><span class="dot"></span><span id="pillText">Connecting</span></span>
|
|
350
|
+
<button class="ibtn" id="btnSettings" title="Settings">
|
|
351
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/></svg>
|
|
352
|
+
</button>
|
|
353
|
+
<button class="ibtn" id="btnCompact" title="Collapse to a bar">
|
|
354
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>
|
|
355
|
+
</button>
|
|
356
|
+
</header>
|
|
357
|
+
|
|
358
|
+
<section class="banner warn" id="verBar">
|
|
359
|
+
<span class="bico"><svg width="14" height="14"><use href="#i-alert"/></svg></span>
|
|
360
|
+
<div class="main"><b>Version mismatch</b><span id="verMsg">Reopen the plugin or update.</span></div>
|
|
361
|
+
</section>
|
|
362
|
+
|
|
363
|
+
<section class="banner" id="waitBar">
|
|
364
|
+
<span class="bico"><svg width="14" height="14"><use href="#i-pointer"/></svg></span>
|
|
365
|
+
<div class="main"><b>Your agent is waiting</b><span id="waitMsg">Select one or more layers</span></div>
|
|
366
|
+
<span class="countdown" id="waitLeft"></span>
|
|
367
|
+
<button class="mini" id="btnWaitCancel">Cancel</button>
|
|
368
|
+
</section>
|
|
369
|
+
|
|
370
|
+
<section class="bridge">
|
|
371
|
+
<div class="bridge-row">
|
|
372
|
+
<div class="tile claude" id="tileAgent"><svg width="20" height="20"><use href="#i-spark"/></svg></div>
|
|
373
|
+
<div class="link"><div class="wire"></div><div id="packets"></div><span class="badge" id="badge">connecting</span></div>
|
|
374
|
+
<div class="tile figma" id="tileFigma"><svg width="18" height="18"><use href="#i-frame"/></svg></div>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="labels">
|
|
377
|
+
<div class="lbl"><b>AI agent</b><span class="trunc" id="agentSub">MCP · localhost:3055</span></div>
|
|
378
|
+
<div class="lbl right"><b class="trunc" id="fileName">This file</b><span class="trunc" id="pageName">—</span></div>
|
|
379
|
+
</div>
|
|
380
|
+
</section>
|
|
381
|
+
|
|
382
|
+
<section class="setup">
|
|
383
|
+
<div class="check done">
|
|
384
|
+
<span class="cico"><svg width="10" height="10"><use href="#i-check"/></svg></span>
|
|
385
|
+
<div class="trunc"><b>Plugin is running</b><span class="trunc" id="chkFile">in this file</span></div>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="check wait" id="chkServer">
|
|
388
|
+
<span class="cico"></span>
|
|
389
|
+
<div><b>Start your AI client</b><span>Claude Code, Codex, Antigravity, Cursor… The bridge starts with it (MCP server <b style="display:inline">FigmaBridge</b>). This panel connects by itself.</span></div>
|
|
390
|
+
</div>
|
|
391
|
+
<div class="check fail" id="chkPort">
|
|
392
|
+
<span class="cico"><svg width="10" height="10"><use href="#i-x"/></svg></span>
|
|
393
|
+
<div style="flex:1;min-width:0"><b>Port 3055 is used by another app</b><span>Often an old TalkToFigma socket. Close it and the bridge reconnects.</span>
|
|
394
|
+
<div class="cmd"><code class="mono trunc">Get-NetTCPConnection -LocalPort 3055</code><button class="mini" id="btnCopyCmd">Copy</button></div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
<div class="setup-actions"><button class="btn" id="btnRetry">Retry now</button><span class="countdown" id="countdown"></span></div>
|
|
398
|
+
</section>
|
|
399
|
+
|
|
400
|
+
<section class="metrics">
|
|
401
|
+
<div class="metric"><div class="v" id="mCount">0</div><div class="k">Commands</div></div>
|
|
402
|
+
<div class="metric" id="mErrCell"><div class="v" id="mErr">0</div><div class="k">Errors</div></div>
|
|
403
|
+
<div class="metric wide"><div class="v" id="mAvg">—</div><div class="k">Avg. time</div><svg id="spark" viewBox="0 0 46 18" preserveAspectRatio="none"></svg></div>
|
|
404
|
+
</section>
|
|
405
|
+
|
|
406
|
+
<div class="act-head">
|
|
407
|
+
<h2>Activity</h2>
|
|
408
|
+
<div class="seg" id="filter"><button data-f="all" class="on">All</button><button data-f="err">Errors<span class="count" id="errCount"></span></button></div>
|
|
409
|
+
<button class="ibtn" id="btnClear" title="Clear activity">
|
|
410
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M9 7V4.5h6V7M6.5 7l1 12.5h9l1-12.5"/></svg>
|
|
411
|
+
</button>
|
|
412
|
+
</div>
|
|
413
|
+
<ul id="log"></ul>
|
|
414
|
+
|
|
415
|
+
<footer class="foot">
|
|
416
|
+
<span id="footChannel">default</span><span class="sep"></span><span>localhost:3055</span>
|
|
417
|
+
<span class="grow"></span><span id="footVer">v—</span>
|
|
418
|
+
</footer>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<div id="bar">
|
|
422
|
+
<div class="mark"><svg width="14" height="14"><use href="#i-mark"/></svg><span class="dot"></span></div>
|
|
423
|
+
<div class="bar-text trunc"><b class="trunc" id="barTitle">Connecting…</b><span class="trunc" id="barSub">—</span></div>
|
|
424
|
+
<button class="ibtn" id="btnExpand" title="Expand">
|
|
425
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4h5v5M9 20H4v-5M20 4l-6 6M4 20l6-6"/></svg>
|
|
426
|
+
</button>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<div id="grip" title="Drag to resize">
|
|
430
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M9 3 3 9M9 6.5 6.5 9"/></svg>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<div class="scrim" id="scrim"></div>
|
|
434
|
+
<aside class="sheet" id="sheet">
|
|
435
|
+
<div class="sheet-head"><b>Settings</b>
|
|
436
|
+
<button class="ibtn" id="btnCloseSettings" title="Close"><svg width="14" height="14"><use href="#i-x"/></svg></button>
|
|
437
|
+
</div>
|
|
438
|
+
<div class="sheet-body">
|
|
439
|
+
<div class="group">
|
|
440
|
+
<h3>Connection</h3>
|
|
441
|
+
<label for="inChannel">Channel</label>
|
|
442
|
+
<input id="inChannel" spellcheck="false" autocomplete="off" />
|
|
443
|
+
<div class="hint">Keep “default” unless the MCP server sets FIGMA_BRIDGE_CHANNEL.</div>
|
|
444
|
+
</div>
|
|
445
|
+
<div class="group">
|
|
446
|
+
<h3>Window</h3>
|
|
447
|
+
<button class="btn quiet" id="btnResetSize">Reset window size</button>
|
|
448
|
+
<div class="hint">Drag the bottom-right corner to resize. The size is remembered.</div>
|
|
449
|
+
</div>
|
|
450
|
+
<div class="group">
|
|
451
|
+
<h3>About</h3>
|
|
452
|
+
<div class="kv"><span>Server</span><span class="mono">ws://localhost:3055</span></div>
|
|
453
|
+
<div class="kv"><span>Session</span><span class="mono" id="sessId">—</span></div>
|
|
454
|
+
<div class="kv"><span>Version</span><span class="mono" id="verText">—</span></div>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
<div class="sheet-foot"><button class="btn quiet" id="btnCancel">Cancel</button><button class="btn" id="btnSave">Save</button></div>
|
|
458
|
+
</aside>
|
|
459
|
+
|
|
460
|
+
<div id="toast"></div>
|
|
461
|
+
|
|
462
|
+
<script>
|
|
463
|
+
"use strict";
|
|
464
|
+
const WS_URL = "ws://localhost:3055";
|
|
465
|
+
const CHUNK = 1 << 20;
|
|
466
|
+
const MAX_LOG = 50;
|
|
467
|
+
const HANDSHAKE_MS = 3000;
|
|
468
|
+
const SLOW_MS = 3000;
|
|
469
|
+
const METHODS = {
|
|
470
|
+
run_script: { label: "Script", color: "#8b5cf6", icon: "i-script" },
|
|
471
|
+
build: { label: "Build", color: "#0d99ff", icon: "i-build" },
|
|
472
|
+
describe: { label: "Describe", color: "#64748b", icon: "i-tree" },
|
|
473
|
+
find: { label: "Find", color: "#64748b", icon: "i-search" },
|
|
474
|
+
get_design_system: { label: "Design system", color: "#64748b", icon: "i-swatch" },
|
|
475
|
+
design_tokens: { label: "Design tokens", color: "#0d99ff", icon: "i-swatch" },
|
|
476
|
+
export_tokens: { label: "Export tokens", color: "#64748b", icon: "i-swatch" },
|
|
477
|
+
audit: { label: "Audit", color: "#16a34a", icon: "i-shield" },
|
|
478
|
+
get_css: { label: "CSS", color: "#64748b", icon: "i-braces" },
|
|
479
|
+
checkpoint: { label: "Checkpoint", color: "#64748b", icon: "i-history" },
|
|
480
|
+
screenshot: { label: "Screenshot", color: "#0ea5e9", icon: "i-shot" },
|
|
481
|
+
compare_images: { label: "Compare", color: "#0ea5e9", icon: "i-diff" },
|
|
482
|
+
place_image: { label: "Image", color: "#10b981", icon: "i-image" },
|
|
483
|
+
import_svg: { label: "SVG", color: "#f59e0b", icon: "i-pen" },
|
|
484
|
+
get_context: { label: "Context", color: "#64748b", icon: "i-layers" },
|
|
485
|
+
list_fonts: { label: "Fonts", color: "#ec4899", icon: "i-type" },
|
|
486
|
+
wait_for_selection: { label: "Wait for selection", color: "#0d99ff", icon: "i-pointer" },
|
|
487
|
+
prototype: { label: "Prototype", color: "#8b5cf6", icon: "i-play" },
|
|
488
|
+
annotate: { label: "Annotate", color: "#f59e0b", icon: "i-note" },
|
|
489
|
+
export_tree: { label: "Export code", color: "#64748b", icon: "i-braces" },
|
|
490
|
+
ping: { label: "Ping", color: "#64748b", icon: "i-pulse" },
|
|
491
|
+
};
|
|
492
|
+
const methodOf = (m) => METHODS[m] || { label: m, color: "#64748b", icon: "i-pulse" };
|
|
493
|
+
|
|
494
|
+
const $ = (id) => document.getElementById(id);
|
|
495
|
+
const toPlugin = (msg) => parent.postMessage({ pluginMessage: msg }, "*");
|
|
496
|
+
|
|
497
|
+
let version = "";
|
|
498
|
+
let session = null;
|
|
499
|
+
let settings = { channel: "default", compact: false };
|
|
500
|
+
let ws = null;
|
|
501
|
+
let state = "connecting";
|
|
502
|
+
let everConnected = false;
|
|
503
|
+
let retryDelay = 400;
|
|
504
|
+
let retryAt = 0;
|
|
505
|
+
let retryTimer = 0;
|
|
506
|
+
let handshakeTimer = 0;
|
|
507
|
+
let filter = "all";
|
|
508
|
+
const waits = new Map();
|
|
509
|
+
let openId = null;
|
|
510
|
+
const entries = [];
|
|
511
|
+
const running = new Map();
|
|
512
|
+
// Requests answered with CANCELLED: the plugin's late reply must not reach the agent twice.
|
|
513
|
+
const cancelled = new Set();
|
|
514
|
+
let serverVersion = "";
|
|
515
|
+
const stats = { count: 0, errors: 0, totalMs: 0 };
|
|
516
|
+
|
|
517
|
+
// ─── Framing (mirror of src/bridge.ts) ──────────────────────────────────────
|
|
518
|
+
const partials = new Map();
|
|
519
|
+
function encode(msg) {
|
|
520
|
+
const text = JSON.stringify(msg);
|
|
521
|
+
if (text.length <= CHUNK) return [text];
|
|
522
|
+
const cid = crypto.randomUUID();
|
|
523
|
+
const n = Math.ceil(text.length / CHUNK);
|
|
524
|
+
const frames = [];
|
|
525
|
+
for (let i = 0; i < n; i++) frames.push(JSON.stringify({ t: "chunk", cid, i, n, d: text.slice(i * CHUNK, (i + 1) * CHUNK) }));
|
|
526
|
+
return frames;
|
|
527
|
+
}
|
|
528
|
+
function decode(raw) {
|
|
529
|
+
const msg = JSON.parse(raw);
|
|
530
|
+
if (msg.t !== "chunk") return msg;
|
|
531
|
+
let p = partials.get(msg.cid);
|
|
532
|
+
if (!p) partials.set(msg.cid, (p = { parts: new Array(msg.n), got: 0 }));
|
|
533
|
+
if (p.parts[msg.i] === undefined) { p.parts[msg.i] = msg.d; p.got++; }
|
|
534
|
+
if (p.got < msg.n) return null;
|
|
535
|
+
partials.delete(msg.cid);
|
|
536
|
+
return JSON.parse(p.parts.join(""));
|
|
537
|
+
}
|
|
538
|
+
function send(msg) {
|
|
539
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
540
|
+
for (const f of encode(msg)) ws.send(f);
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ─── Binary helpers ─────────────────────────────────────────────────────────
|
|
545
|
+
function bytesToB64(bytes) {
|
|
546
|
+
let s = "";
|
|
547
|
+
for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
|
|
548
|
+
return btoa(s);
|
|
549
|
+
}
|
|
550
|
+
function b64ToBytes(b64) {
|
|
551
|
+
const s = atob(b64);
|
|
552
|
+
const out = new Uint8Array(s.length);
|
|
553
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
// Converts WEBP and oversized images to PNG/JPEG within Figma's 4096 px limit.
|
|
557
|
+
async function transcode(bytes, mime, maxDim) {
|
|
558
|
+
const bmp = await createImageBitmap(new Blob([bytes], { type: mime }));
|
|
559
|
+
const scale = Math.min(1, maxDim / Math.max(bmp.width, bmp.height));
|
|
560
|
+
const canvas = document.createElement("canvas");
|
|
561
|
+
canvas.width = Math.max(1, Math.round(bmp.width * scale));
|
|
562
|
+
canvas.height = Math.max(1, Math.round(bmp.height * scale));
|
|
563
|
+
const ctx = canvas.getContext("2d");
|
|
564
|
+
ctx.imageSmoothingQuality = "high";
|
|
565
|
+
ctx.drawImage(bmp, 0, 0, canvas.width, canvas.height);
|
|
566
|
+
const type = mime === "image/jpeg" ? "image/jpeg" : "image/png";
|
|
567
|
+
const blob = await new Promise((r) => canvas.toBlob(r, type, 0.92));
|
|
568
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ─── Image comparison (compare tool) ────────────────────────────────────────
|
|
572
|
+
// Runs here because the plugin main thread has no canvas.
|
|
573
|
+
const VIEW_MAX = 1568;
|
|
574
|
+
|
|
575
|
+
function canvasOf(w, h) {
|
|
576
|
+
const c = document.createElement("canvas");
|
|
577
|
+
c.width = w;
|
|
578
|
+
c.height = h;
|
|
579
|
+
return c;
|
|
580
|
+
}
|
|
581
|
+
async function pngB64(canvas) {
|
|
582
|
+
const blob = await new Promise((r) => canvas.toBlob(r, "image/png"));
|
|
583
|
+
return bytesToB64(new Uint8Array(await blob.arrayBuffer()));
|
|
584
|
+
}
|
|
585
|
+
// Pixels drawn on white at w×h, so transparent areas compare as white.
|
|
586
|
+
function pixelsOf(bmp, w, h) {
|
|
587
|
+
const ctx = canvasOf(w, h).getContext("2d", { willReadFrequently: true });
|
|
588
|
+
ctx.fillStyle = "#fff";
|
|
589
|
+
ctx.fillRect(0, 0, w, h);
|
|
590
|
+
ctx.imageSmoothingQuality = "high";
|
|
591
|
+
ctx.drawImage(bmp, 0, 0, w, h);
|
|
592
|
+
return ctx.getImageData(0, 0, w, h);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function compareImages(p) {
|
|
596
|
+
const actual = await createImageBitmap(new Blob([b64ToBytes(p.actual)], { type: "image/png" }));
|
|
597
|
+
const ref = await createImageBitmap(new Blob([b64ToBytes(p.reference)], { type: p.referenceMime || "image/png" }));
|
|
598
|
+
const w = actual.width, h = actual.height;
|
|
599
|
+
const a = pixelsOf(actual, w, h).data;
|
|
600
|
+
const refImg = pixelsOf(ref, w, h);
|
|
601
|
+
const b = refImg.data;
|
|
602
|
+
const threshold = typeof p.threshold === "number" ? p.threshold : 0.1;
|
|
603
|
+
const cell = Math.max(4, Math.round(Math.max(w, h) / 160));
|
|
604
|
+
const cols = Math.ceil(w / cell), rows = Math.ceil(h / cell);
|
|
605
|
+
const cellDiff = new Uint32Array(cols * rows);
|
|
606
|
+
const heat = new ImageData(w, h);
|
|
607
|
+
const hd = heat.data;
|
|
608
|
+
let diffCount = 0;
|
|
609
|
+
for (let i = 0, px = 0; i < a.length; i += 4, px++) {
|
|
610
|
+
const dr = a[i] - b[i], dg = a[i + 1] - b[i + 1], db = a[i + 2] - b[i + 2];
|
|
611
|
+
const d = Math.sqrt(0.299 * dr * dr + 0.587 * dg * dg + 0.114 * db * db) / 255;
|
|
612
|
+
if (d > threshold) {
|
|
613
|
+
diffCount++;
|
|
614
|
+
const x = px % w, y = (px - x) / w;
|
|
615
|
+
cellDiff[Math.floor(y / cell) * cols + Math.floor(x / cell)]++;
|
|
616
|
+
const k = Math.min(1, 0.5 + d);
|
|
617
|
+
hd[i] = 255;
|
|
618
|
+
hd[i + 1] = hd[i + 2] = Math.round(90 * (1 - k));
|
|
619
|
+
} else {
|
|
620
|
+
// The design itself, faded, gives context around the red areas.
|
|
621
|
+
const luma = 0.299 * a[i] + 0.587 * a[i + 1] + 0.114 * a[i + 2];
|
|
622
|
+
hd[i] = hd[i + 1] = hd[i + 2] = Math.round(255 - (255 - luma) * 0.22);
|
|
623
|
+
}
|
|
624
|
+
hd[i + 3] = 255;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Regions: connected groups of differing cells (gaps of one cell are bridged).
|
|
628
|
+
const minCell = Math.max(1, Math.round(cell * cell * 0.02));
|
|
629
|
+
const seen = new Uint8Array(cols * rows);
|
|
630
|
+
const regions = [];
|
|
631
|
+
for (let start = 0; start < cellDiff.length; start++) {
|
|
632
|
+
if (seen[start] || cellDiff[start] < minCell) continue;
|
|
633
|
+
const stack = [start];
|
|
634
|
+
seen[start] = 1;
|
|
635
|
+
let x0 = cols, y0 = rows, x1 = 0, y1 = 0, count = 0;
|
|
636
|
+
while (stack.length) {
|
|
637
|
+
const c = stack.pop();
|
|
638
|
+
const cx = c % cols, cy = (c - cx) / cols;
|
|
639
|
+
x0 = Math.min(x0, cx); y0 = Math.min(y0, cy); x1 = Math.max(x1, cx); y1 = Math.max(y1, cy);
|
|
640
|
+
count += cellDiff[c];
|
|
641
|
+
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) {
|
|
642
|
+
const nx = cx + dx, ny = cy + dy;
|
|
643
|
+
if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
|
|
644
|
+
const n = ny * cols + nx;
|
|
645
|
+
if (!seen[n] && cellDiff[n] >= minCell) { seen[n] = 1; stack.push(n); }
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const rx = x0 * cell, ry = y0 * cell;
|
|
649
|
+
const rw = Math.min(w, (x1 + 1) * cell) - rx, rh = Math.min(h, (y1 + 1) * cell) - ry;
|
|
650
|
+
regions.push({ x: rx, y: ry, width: rw, height: rh, pixels: count, mismatch: count / (rw * rh) });
|
|
651
|
+
}
|
|
652
|
+
regions.sort((r1, r2) => r2.pixels - r1.pixels);
|
|
653
|
+
|
|
654
|
+
const heatCanvas = canvasOf(w, h);
|
|
655
|
+
const hctx = heatCanvas.getContext("2d");
|
|
656
|
+
hctx.putImageData(heat, 0, 0);
|
|
657
|
+
hctx.strokeStyle = "#e5484d";
|
|
658
|
+
hctx.lineWidth = Math.max(1, Math.round(Math.max(w, h) / 600));
|
|
659
|
+
for (const r of regions.slice(0, 12)) hctx.strokeRect(r.x + 0.5, r.y + 0.5, r.width - 1, r.height - 1);
|
|
660
|
+
|
|
661
|
+
const out = {
|
|
662
|
+
width: w,
|
|
663
|
+
height: h,
|
|
664
|
+
mismatch: diffCount / (w * h),
|
|
665
|
+
regions: regions.slice(0, 12),
|
|
666
|
+
totalRegions: regions.length,
|
|
667
|
+
reference: { width: ref.width, height: ref.height },
|
|
668
|
+
heatmap: await pngB64(heatCanvas),
|
|
669
|
+
};
|
|
670
|
+
if (p.sideBySide) {
|
|
671
|
+
// Reference | Figma, with a label bar, downscaled to what a model can read.
|
|
672
|
+
const gap = 16, bar = 28;
|
|
673
|
+
const k = Math.min(1, VIEW_MAX / (2 * w + gap), (VIEW_MAX - bar) / h);
|
|
674
|
+
const pw = Math.max(1, Math.round(w * k)), ph = Math.max(1, Math.round(h * k));
|
|
675
|
+
const c = canvasOf(2 * pw + gap, ph + bar);
|
|
676
|
+
const ctx = c.getContext("2d");
|
|
677
|
+
ctx.fillStyle = "#f3f3f5";
|
|
678
|
+
ctx.fillRect(0, 0, c.width, c.height);
|
|
679
|
+
ctx.fillStyle = "#111113";
|
|
680
|
+
ctx.font = "600 13px Inter, system-ui, sans-serif";
|
|
681
|
+
ctx.textBaseline = "middle";
|
|
682
|
+
ctx.fillText("Reference", 4, bar / 2);
|
|
683
|
+
ctx.fillText("Figma", pw + gap + 4, bar / 2);
|
|
684
|
+
ctx.imageSmoothingQuality = "high";
|
|
685
|
+
const refCanvas = canvasOf(w, h);
|
|
686
|
+
refCanvas.getContext("2d").putImageData(refImg, 0, 0);
|
|
687
|
+
ctx.drawImage(refCanvas, 0, bar, pw, ph);
|
|
688
|
+
ctx.fillStyle = "#fff";
|
|
689
|
+
ctx.fillRect(pw + gap, bar, pw, ph);
|
|
690
|
+
ctx.drawImage(actual, pw + gap, bar, pw, ph);
|
|
691
|
+
out.sideBySide = await pngB64(c);
|
|
692
|
+
}
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Commands answered by this iframe instead of the plugin main thread.
|
|
697
|
+
const UI_HANDLERS = { compare_images: compareImages };
|
|
698
|
+
|
|
699
|
+
// ─── Connection ─────────────────────────────────────────────────────────────
|
|
700
|
+
function connect() {
|
|
701
|
+
clearTimeout(retryTimer);
|
|
702
|
+
clearTimeout(handshakeTimer);
|
|
703
|
+
if (ws) { ws.onclose = null; try { ws.close(); } catch (e) {} ws = null; }
|
|
704
|
+
if (state !== "offline" && state !== "foreign") setState("connecting");
|
|
705
|
+
let sock;
|
|
706
|
+
try { sock = new WebSocket(WS_URL); } catch (e) { return scheduleRetry("offline"); }
|
|
707
|
+
ws = sock;
|
|
708
|
+
sock.onopen = () => {
|
|
709
|
+
send({ t: "hello", role: "plugin", channel: settings.channel, session, version });
|
|
710
|
+
handshakeTimer = setTimeout(() => { sock.onclose = null; sock.close(); ws = null; scheduleRetry("foreign"); }, HANDSHAKE_MS);
|
|
711
|
+
};
|
|
712
|
+
sock.onmessage = (ev) => {
|
|
713
|
+
let msg;
|
|
714
|
+
try { msg = decode(ev.data); } catch (e) { return; }
|
|
715
|
+
if (!msg) return;
|
|
716
|
+
if (msg.t === "welcome") {
|
|
717
|
+
clearTimeout(handshakeTimer);
|
|
718
|
+
if (msg.bridge !== "figma-bridge") { sock.onclose = null; sock.close(); ws = null; return scheduleRetry("foreign"); }
|
|
719
|
+
retryDelay = 400;
|
|
720
|
+
serverVersion = msg.version || "";
|
|
721
|
+
renderVersion();
|
|
722
|
+
setState("connected");
|
|
723
|
+
if (!everConnected) { everConnected = true; toPlugin({ t: "notify", text: "✦ Figma Bridge connected to your AI" }); }
|
|
724
|
+
} else if (msg.t === "req") {
|
|
725
|
+
onRequest(msg);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
sock.onclose = () => { if (ws === sock) { ws = null; scheduleRetry("offline"); } };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function scheduleRetry(nextState) {
|
|
732
|
+
clearTimeout(retryTimer);
|
|
733
|
+
setState(nextState);
|
|
734
|
+
const delay = nextState === "foreign" ? 4000 : retryDelay;
|
|
735
|
+
retryDelay = Math.min(Math.round(retryDelay * 1.6), 5000);
|
|
736
|
+
retryAt = Date.now() + delay;
|
|
737
|
+
retryTimer = setTimeout(connect, delay);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
setInterval(() => { if (ws && ws.readyState === WebSocket.OPEN) ws.send('{"t":"ping"}'); }, 20000);
|
|
741
|
+
setInterval(() => {
|
|
742
|
+
if (waits.size) renderWait();
|
|
743
|
+
if (state !== "offline" && state !== "foreign") return;
|
|
744
|
+
const s = Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));
|
|
745
|
+
$("countdown").textContent = s > 0 ? "Retrying in " + s + " s" : "Trying…";
|
|
746
|
+
}, 250);
|
|
747
|
+
setInterval(() => { if (entries.length) renderLog(); }, 30000);
|
|
748
|
+
window.addEventListener("focus", () => { if (state === "offline") { retryDelay = 400; connect(); } });
|
|
749
|
+
|
|
750
|
+
// ─── Requests ───────────────────────────────────────────────────────────────
|
|
751
|
+
function summarize(msg) {
|
|
752
|
+
const p = msg.params || {};
|
|
753
|
+
switch (msg.method) {
|
|
754
|
+
case "run_script": {
|
|
755
|
+
const line = String(p.code || "").split("\n").map((l) => l.trim()).find((l) => l && !l.startsWith("//"));
|
|
756
|
+
return line || "(empty script)";
|
|
757
|
+
}
|
|
758
|
+
case "screenshot": return (p.nodeId ? "Node " + p.nodeId : "Selection") + (p.scale ? " · " + p.scale + "×" : "") + (p.format === "JPG" ? " · JPG" : "");
|
|
759
|
+
case "place_image": return p.name || (p.nodeId ? "Fill on " + p.nodeId : "New image layer");
|
|
760
|
+
case "import_svg": return p.name ? p.name.replace(/^icon\//, "Icon · ") : (p.svg ? (Math.round(p.svg.length / 102.4) / 10) + " KB of SVG" : "SVG");
|
|
761
|
+
case "compare_images": return "Design vs reference" + (p.sideBySide ? " · side by side" : "");
|
|
762
|
+
case "list_fonts": return p.filter ? "Matching “" + p.filter + "”" : "All fonts";
|
|
763
|
+
case "get_context": return "File, pages and selection";
|
|
764
|
+
case "prototype": {
|
|
765
|
+
const parts = [];
|
|
766
|
+
if (p.links && p.links.length) parts.push(p.links.length + " link" + (p.links.length === 1 ? "" : "s"));
|
|
767
|
+
if (p.flows && p.flows.length) parts.push(p.flows.length + " flow" + (p.flows.length === 1 ? "" : "s"));
|
|
768
|
+
if (p.clear && p.clear.length) parts.push("clear " + p.clear.length);
|
|
769
|
+
return parts.join(" · ") || "List interactions";
|
|
770
|
+
}
|
|
771
|
+
case "annotate": {
|
|
772
|
+
const action = p.action || (p.label || p.properties ? "add" : "list");
|
|
773
|
+
if (action === "add") return (p.label ? "“" + p.label.split("\n")[0] + "”" : (p.properties || []).join(", ")) + (p.nodeId ? " on " + p.nodeId : "");
|
|
774
|
+
return action.charAt(0).toUpperCase() + action.slice(1) + (p.nodeId ? " · " + p.nodeId : "");
|
|
775
|
+
}
|
|
776
|
+
case "export_tree": return p.nodeId ? "Node " + p.nodeId : "Selection";
|
|
777
|
+
case "wait_for_selection": return p.message || "Select layers";
|
|
778
|
+
case "build": {
|
|
779
|
+
let count = 0;
|
|
780
|
+
const walk = (n) => { if (!n || typeof n !== "object") return; if (Array.isArray(n)) return n.forEach(walk); count++; if (Array.isArray(n.children)) n.children.forEach(walk); if (Array.isArray(n.variants)) n.variants.forEach(walk); };
|
|
781
|
+
walk(p.spec);
|
|
782
|
+
const root = Array.isArray(p.spec) ? p.spec[0] : p.spec;
|
|
783
|
+
return (p.label ? p.label + " · " : "") + ((root && root.name) || "Layout") + " · " + count + " layer" + (count === 1 ? "" : "s");
|
|
784
|
+
}
|
|
785
|
+
case "find": {
|
|
786
|
+
const parts = [];
|
|
787
|
+
if (p.name) parts.push("name “" + p.name + "”");
|
|
788
|
+
if (p.text) parts.push("text “" + p.text + "”");
|
|
789
|
+
if (p.type) parts.push([].concat(p.type).join("|").toLowerCase());
|
|
790
|
+
if (p.style) parts.push("style “" + p.style + "”");
|
|
791
|
+
if (p.component) parts.push("instances of “" + p.component + "”");
|
|
792
|
+
return parts.join(" · ") || "Layers";
|
|
793
|
+
}
|
|
794
|
+
case "describe": return (p.nodeId ? "Node " + p.nodeId : "Selection") + (p.depth !== undefined ? " · depth " + p.depth : "");
|
|
795
|
+
case "get_design_system": return p.include ? p.include.join(", ") : "Styles, variables and components";
|
|
796
|
+
case "design_tokens": {
|
|
797
|
+
const vars = (p.collections || []).reduce((n, c) => n + (c.variables || []).length, 0);
|
|
798
|
+
const st = p.styles || {};
|
|
799
|
+
const styles = (st.colors || []).length + (st.text || []).length + (st.effects || []).length;
|
|
800
|
+
return vars + " variable" + (vars === 1 ? "" : "s") + " · " + styles + " style" + (styles === 1 ? "" : "s");
|
|
801
|
+
}
|
|
802
|
+
case "export_tokens": return p.collections && p.collections.length ? p.collections.join(", ") : "Variables and styles";
|
|
803
|
+
case "audit": return (p.fix || p.fixes ? "Fix · " : p.scope === "design-system" ? "Design system health · " : "") + (p.nodeId ? "Node " + p.nodeId : p.scope === "design-system" && !p.fix ? "Whole file" : "Selection");
|
|
804
|
+
case "get_css": return p.nodeId ? "Node " + p.nodeId : "Selection";
|
|
805
|
+
case "checkpoint": return p.action.charAt(0).toUpperCase() + p.action.slice(1) + (p.label ? " · " + p.label : p.id ? " · " + p.id : "");
|
|
806
|
+
default: return "";
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// The layer a command produced or targeted, so the log can offer "Show".
|
|
811
|
+
function resultNode(method, result) {
|
|
812
|
+
if (!result) return null;
|
|
813
|
+
if (typeof result.nodeId === "string") return result.nodeId;
|
|
814
|
+
if (typeof result.rootId === "string") return result.rootId;
|
|
815
|
+
if (method !== "run_script") return null;
|
|
816
|
+
const r = result.result;
|
|
817
|
+
if (r && typeof r.id === "string" && /^[\dI;:]+$/.test(r.id)) return r.id;
|
|
818
|
+
if (typeof r === "string" && /^\d+:\d+$/.test(r)) return r;
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function onRequest(msg) {
|
|
823
|
+
const entry = { id: msg.id, method: msg.method, sum: summarize(msg), start: performance.now(), at: Date.now(), status: "run", isNew: true };
|
|
824
|
+
running.set(msg.id, entry);
|
|
825
|
+
entries.unshift(entry);
|
|
826
|
+
if (entries.length > MAX_LOG) entries.length = MAX_LOG;
|
|
827
|
+
packet("out");
|
|
828
|
+
render();
|
|
829
|
+
let params = msg.params || {};
|
|
830
|
+
if (UI_HANDLERS[msg.method]) {
|
|
831
|
+
try {
|
|
832
|
+
onResult({ t: "res", id: msg.id, ok: true, result: await UI_HANDLERS[msg.method](params) });
|
|
833
|
+
} catch (e) {
|
|
834
|
+
onResult({ t: "res", id: msg.id, ok: false, code: (e && e.code) || "UI_ERROR", error: String((e && e.message) || e) });
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
if (params.images) {
|
|
840
|
+
const bytes = {};
|
|
841
|
+
for (const key of Object.keys(params.images)) {
|
|
842
|
+
const img = params.images[key];
|
|
843
|
+
let b = b64ToBytes(img.b64);
|
|
844
|
+
if (img.transcode) b = await transcode(b, img.mime, img.transcode.maxDim);
|
|
845
|
+
bytes[key] = b;
|
|
846
|
+
}
|
|
847
|
+
params = Object.assign({}, params, { imageBytes: bytes });
|
|
848
|
+
delete params.images;
|
|
849
|
+
}
|
|
850
|
+
if (params.bytesB64) {
|
|
851
|
+
let bytes = b64ToBytes(params.bytesB64);
|
|
852
|
+
if (params.transcode) bytes = await transcode(bytes, params.mime, params.transcode.maxDim);
|
|
853
|
+
params = Object.assign({}, params, { bytes });
|
|
854
|
+
delete params.bytesB64;
|
|
855
|
+
}
|
|
856
|
+
} catch (e) {
|
|
857
|
+
return onResult({ t: "res", id: msg.id, ok: false, code: "BAD_IMAGE", error: "Could not decode the image: " + (e && e.message || e) });
|
|
858
|
+
}
|
|
859
|
+
toPlugin({ t: "req", id: msg.id, method: msg.method, params, timeoutMs: msg.timeoutMs });
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function onResult(res) {
|
|
863
|
+
if (cancelled.has(res.id)) {
|
|
864
|
+
cancelled.delete(res.id);
|
|
865
|
+
const late = entries.find((e) => e.id === res.id);
|
|
866
|
+
if (late) { late.lateNote = res.ok ? "Finished in the background after the cancel" : ""; renderLog(); }
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const entry = running.get(res.id);
|
|
870
|
+
if (entry) {
|
|
871
|
+
running.delete(res.id);
|
|
872
|
+
entry.ms = Math.round(performance.now() - entry.start);
|
|
873
|
+
entry.status = res.ok ? "ok" : res.code === "CANCELLED" ? "cancelled" : "err";
|
|
874
|
+
entry.error = res.ok ? "" : (res.line ? "Line " + res.line + ": " : "") + res.error;
|
|
875
|
+
entry.code = res.code;
|
|
876
|
+
entry.nodeId = res.ok ? resultNode(entry.method, res.result) : null;
|
|
877
|
+
stats.count++;
|
|
878
|
+
stats.totalMs += entry.ms;
|
|
879
|
+
if (!res.ok && res.code !== "CANCELLED") stats.errors++;
|
|
880
|
+
packet(res.ok ? "ok" : "err");
|
|
881
|
+
render();
|
|
882
|
+
}
|
|
883
|
+
if (res.ok && res.result && res.result.bytes instanceof Uint8Array) {
|
|
884
|
+
res.result.bytesB64 = bytesToB64(res.result.bytes);
|
|
885
|
+
delete res.result.bytes;
|
|
886
|
+
}
|
|
887
|
+
send(res);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// A dot travels along the bridge for each request and each answer.
|
|
891
|
+
function packet(kind) {
|
|
892
|
+
const host = $("packets");
|
|
893
|
+
if (!host.animate) return;
|
|
894
|
+
const dot = document.createElement("span");
|
|
895
|
+
dot.className = "packet";
|
|
896
|
+
const css = getComputedStyle(document.documentElement);
|
|
897
|
+
dot.style.setProperty("--c", kind === "out" ? css.getPropertyValue("--accent") : kind === "ok" ? css.getPropertyValue("--ok") : css.getPropertyValue("--err"));
|
|
898
|
+
host.appendChild(dot);
|
|
899
|
+
const frames = kind === "out" ? [{ left: "0%", opacity: 0 }, { opacity: 1, offset: .15 }, { left: "100%", opacity: 1 }] : [{ left: "100%", opacity: 0 }, { opacity: 1, offset: .15 }, { left: "0%", opacity: 1 }];
|
|
900
|
+
dot.animate(frames, { duration: 520, easing: "cubic-bezier(.4,0,.2,1)" }).onfinish = () => {
|
|
901
|
+
dot.remove();
|
|
902
|
+
const tile = $(kind === "out" ? "tileFigma" : "tileAgent");
|
|
903
|
+
tile.classList.remove("hit");
|
|
904
|
+
void tile.offsetWidth;
|
|
905
|
+
tile.classList.add("hit");
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Answers the agent right away; the plugin stops long commands (build) at their next step,
|
|
910
|
+
// but a running script cannot be interrupted and finishes in the background.
|
|
911
|
+
function cancelRequest(e) {
|
|
912
|
+
if (!running.has(e.id)) return;
|
|
913
|
+
onResult({ t: "res", id: e.id, ok: false, code: "CANCELLED", error: "Cancelled by the user in Figma. A running script may still finish in the background: check the file before retrying." });
|
|
914
|
+
cancelled.add(e.id);
|
|
915
|
+
toPlugin({ t: "cancel", id: e.id });
|
|
916
|
+
toast("Cancelled");
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Updates the row text in place: re-rendering the log would replace the Cancel button mid-click.
|
|
920
|
+
function onProgress(msg) {
|
|
921
|
+
const entry = running.get(msg.id);
|
|
922
|
+
if (!entry) return;
|
|
923
|
+
entry.progress = msg.text;
|
|
924
|
+
const l2 = document.querySelector('.row[data-id="' + msg.id + '"] .l2');
|
|
925
|
+
if (l2) l2.textContent = entry.progress + " · " + entry.sum;
|
|
926
|
+
renderBar();
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function renderVersion() {
|
|
930
|
+
const mismatch = !!(serverVersion && version && serverVersion !== version);
|
|
931
|
+
document.body.classList.toggle("mismatch", mismatch);
|
|
932
|
+
if (mismatch) $("verMsg").textContent = "Plugin v" + version + ", server v" + serverVersion + ": reopen the plugin or update.";
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// ─── Rendering ──────────────────────────────────────────────────────────────
|
|
936
|
+
const PILL = { connecting: "Connecting", connected: "Live", offline: "Offline", foreign: "Port busy" };
|
|
937
|
+
const BADGE = { connecting: "connecting", connected: "working", offline: "waiting for agent", foreign: "port 3055 busy" };
|
|
938
|
+
|
|
939
|
+
function setState(next) {
|
|
940
|
+
state = next;
|
|
941
|
+
document.body.classList.remove("s-connecting", "s-connected", "s-offline", "s-foreign");
|
|
942
|
+
document.body.classList.add("s-" + next);
|
|
943
|
+
$("pillText").textContent = PILL[next];
|
|
944
|
+
$("badge").textContent = BADGE[next];
|
|
945
|
+
renderBar();
|
|
946
|
+
if (!entries.length) renderLog();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function esc(s) {
|
|
950
|
+
return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
951
|
+
}
|
|
952
|
+
function ago(t) {
|
|
953
|
+
const s = Math.round((Date.now() - t) / 1000);
|
|
954
|
+
if (s < 45) return "just now";
|
|
955
|
+
if (s < 3600) return Math.round(s / 60) + " min ago";
|
|
956
|
+
return new Date(t).toTimeString().slice(0, 5);
|
|
957
|
+
}
|
|
958
|
+
const fmtMs = (ms) => (ms >= 1000 ? (ms / 1000).toFixed(ms >= 10000 ? 0 : 1) + " s" : ms + " ms");
|
|
959
|
+
const icon = (id, size) => '<svg width="' + size + '" height="' + size + '"><use href="#' + id + '"/></svg>';
|
|
960
|
+
|
|
961
|
+
function render() {
|
|
962
|
+
document.body.classList.toggle("busy", running.size > 0);
|
|
963
|
+
document.body.classList.toggle("idle", stats.count === 0);
|
|
964
|
+
$("mCount").textContent = stats.count;
|
|
965
|
+
$("mErr").textContent = stats.errors;
|
|
966
|
+
$("mErrCell").classList.toggle("bad", stats.errors > 0);
|
|
967
|
+
$("mAvg").textContent = stats.count ? fmtMs(Math.round(stats.totalMs / stats.count)) : "—";
|
|
968
|
+
const errCount = entries.filter((e) => e.status === "err").length;
|
|
969
|
+
$("errCount").textContent = errCount;
|
|
970
|
+
$("errCount").classList.toggle("show", errCount > 0);
|
|
971
|
+
renderSpark();
|
|
972
|
+
renderLog();
|
|
973
|
+
renderBar();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function renderSpark() {
|
|
977
|
+
const done = entries.filter((e) => e.status !== "run").slice(0, 16).reverse();
|
|
978
|
+
const svg = $("spark");
|
|
979
|
+
if (done.length < 2) { svg.innerHTML = ""; return; }
|
|
980
|
+
// Square-root scale so one slow command doesn't flatten the others.
|
|
981
|
+
const w = 46, h = 18, max = Math.sqrt(Math.max(...done.map((e) => e.ms), 1));
|
|
982
|
+
const bw = w / done.length;
|
|
983
|
+
svg.innerHTML = done.map((e, i) => {
|
|
984
|
+
const bh = Math.max(2, (Math.sqrt(e.ms) / max) * h);
|
|
985
|
+
const color = e.status === "err" ? "var(--err)" : i === done.length - 1 ? "var(--accent)" : "var(--text-3)";
|
|
986
|
+
return '<rect x="' + (i * bw + 0.6).toFixed(2) + '" y="' + (h - bh).toFixed(2) + '" width="' + Math.max(1, bw - 1.2).toFixed(2) + '" height="' + bh.toFixed(2) + '" rx="1" fill="' + color + '" opacity=".75"/>';
|
|
987
|
+
}).join("");
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function renderLog() {
|
|
991
|
+
const log = $("log");
|
|
992
|
+
const list = filter === "err" ? entries.filter((e) => e.status === "err") : entries;
|
|
993
|
+
if (!list.length) {
|
|
994
|
+
log.innerHTML = filter === "err"
|
|
995
|
+
? '<li class="empty"><div class="art">' + icon("i-check", 18) + "</div><b>No errors</b>Every command worked.</li>"
|
|
996
|
+
: state === "connected"
|
|
997
|
+
? '<li class="empty"><div class="art">' + icon("i-spark", 20) + '</div><b>Ready when you are</b>Ask your agent to design something in this file.<span class="quote">“Build a pricing card with 3 plans”</span></li>'
|
|
998
|
+
: '<li class="empty"><div class="art">' + icon("i-pulse", 18) + "</div><b>No activity yet</b>Commands from your agent will show up here.</li>";
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
log.innerHTML = list.map((e) => {
|
|
1002
|
+
const m = methodOf(e.method);
|
|
1003
|
+
const cls = "row " + e.status + (e.id === openId ? " open" : "") + (e.isNew ? " new" : "") + (e.ms > SLOW_MS ? " slow" : "");
|
|
1004
|
+
e.isNew = false;
|
|
1005
|
+
const quick = e.status === "run"
|
|
1006
|
+
? '<button class="mini quick danger" data-act="cancel">Cancel</button>'
|
|
1007
|
+
: e.nodeId ? '<button class="mini quick" data-act="show">Show</button>' : e.status === "err" ? '<button class="mini quick" data-act="copy">Copy</button>' : "";
|
|
1008
|
+
const facts = '<div class="facts"><span>Took <b>' + (e.status === "run" ? "…" : fmtMs(e.ms)) + "</b></span>"
|
|
1009
|
+
+ "<span>At <b>" + new Date(e.at).toTimeString().slice(0, 8) + "</b></span>"
|
|
1010
|
+
+ (e.nodeId ? '<span>Layer <b class="mono">' + esc(e.nodeId) + "</b></span>" : "")
|
|
1011
|
+
+ (e.code && e.status !== "ok" && e.status !== "run" ? '<span>Code <b class="mono">' + esc(e.code) + "</b></span>" : "")
|
|
1012
|
+
+ (e.lateNote ? "<span>" + esc(e.lateNote) + "</span>" : "") + "</div>";
|
|
1013
|
+
const actions = (e.status === "run" ? '<button class="mini danger" data-act="cancel">Cancel</button>' : "")
|
|
1014
|
+
+ (e.nodeId ? '<button class="mini" data-act="show">Show in canvas</button>' : "")
|
|
1015
|
+
+ (e.status !== "ok" && e.status !== "run" ? '<button class="mini" data-act="copy">Copy error</button>' : "")
|
|
1016
|
+
+ '<button class="mini" data-act="copysum">Copy command</button>';
|
|
1017
|
+
return '<li class="' + cls + '" data-id="' + e.id + '">'
|
|
1018
|
+
+ '<div class="ic" style="--c:' + m.color + '">' + icon(m.icon, 14) + "</div>"
|
|
1019
|
+
+ '<div class="main"><div class="l1"><span class="m">' + esc(m.label) + '</span><span class="t">' + (e.status === "run" ? "running…" : ago(e.at)) + "</span></div>"
|
|
1020
|
+
+ '<div class="l2 mono trunc">' + esc(e.status !== "ok" && e.status !== "run" ? e.error.split("\n")[0] : e.progress && e.status === "run" ? e.progress + " · " + e.sum : e.sum) + "</div>"
|
|
1021
|
+
+ '<div class="detail">' + (e.status !== "ok" && e.status !== "run" ? '<div class="errbox mono">' + esc(e.error) + "</div>" : "") + facts
|
|
1022
|
+
+ '<div class="detail-actions">' + actions + "</div></div></div>"
|
|
1023
|
+
+ '<div class="side"><span class="dur">' + (e.status === "run" ? "" : fmtMs(e.ms)) + "</span>" + quick + "</div></li>";
|
|
1024
|
+
}).join("");
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function renderWait() {
|
|
1028
|
+
const w = [...waits.values()].pop();
|
|
1029
|
+
document.body.classList.toggle("waiting", !!w);
|
|
1030
|
+
if (w) {
|
|
1031
|
+
$("waitMsg").textContent = w.message;
|
|
1032
|
+
const s = Math.max(0, Math.ceil((w.until - Date.now()) / 1000));
|
|
1033
|
+
$("waitLeft").textContent = s + " s";
|
|
1034
|
+
}
|
|
1035
|
+
renderBar();
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function renderBar() {
|
|
1039
|
+
const w = [...waits.values()].pop();
|
|
1040
|
+
if (w) {
|
|
1041
|
+
$("barTitle").textContent = "Your agent is waiting";
|
|
1042
|
+
$("barSub").textContent = w.message;
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
$("barTitle").textContent = state === "connected" ? (session ? session.fileName : "Connected") : PILL[state] + (state === "offline" ? ": waiting for your agent" : "");
|
|
1046
|
+
const last = entries[0];
|
|
1047
|
+
$("barSub").textContent = last
|
|
1048
|
+
? methodOf(last.method).label + " · " + (last.status === "run" ? last.progress || "running…" : last.status === "ok" ? fmtMs(last.ms) : last.code === "CANCELLED" ? "cancelled" : "error")
|
|
1049
|
+
: state === "connected" ? "Ready" : "Keep this plugin open";
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function renderSession() {
|
|
1053
|
+
$("fileName").textContent = session ? session.fileName : "This file";
|
|
1054
|
+
$("pageName").textContent = session ? session.page : "—";
|
|
1055
|
+
$("chkFile").textContent = session ? "in “" + session.fileName + "”" : "in this file";
|
|
1056
|
+
$("sessId").textContent = session ? session.id.slice(0, 8) : "—";
|
|
1057
|
+
$("verText").textContent = version || "—";
|
|
1058
|
+
$("footVer").textContent = "v" + (version || "—");
|
|
1059
|
+
$("footChannel").textContent = settings.channel === "default" ? "default channel" : "channel " + settings.channel;
|
|
1060
|
+
renderBar();
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// ─── Interactions ───────────────────────────────────────────────────────────
|
|
1064
|
+
let toastTimer = 0;
|
|
1065
|
+
function toast(text) {
|
|
1066
|
+
const el = $("toast");
|
|
1067
|
+
el.textContent = text;
|
|
1068
|
+
el.classList.add("show");
|
|
1069
|
+
clearTimeout(toastTimer);
|
|
1070
|
+
toastTimer = setTimeout(() => el.classList.remove("show"), 1400);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function copy(text) {
|
|
1074
|
+
const ta = document.createElement("textarea");
|
|
1075
|
+
ta.value = text;
|
|
1076
|
+
ta.style.cssText = "position:fixed;opacity:0";
|
|
1077
|
+
document.body.appendChild(ta);
|
|
1078
|
+
ta.select();
|
|
1079
|
+
let ok = false;
|
|
1080
|
+
try { ok = document.execCommand("copy"); } catch (e) {}
|
|
1081
|
+
ta.remove();
|
|
1082
|
+
toast(ok ? "Copied" : "Copy failed");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
$("log").addEventListener("click", (ev) => {
|
|
1086
|
+
const li = ev.target.closest(".row");
|
|
1087
|
+
if (!li) return;
|
|
1088
|
+
const e = entries.find((x) => x.id === li.dataset.id);
|
|
1089
|
+
if (!e) return;
|
|
1090
|
+
const act = ev.target.closest("[data-act]");
|
|
1091
|
+
if (act) {
|
|
1092
|
+
if (act.dataset.act === "cancel") cancelRequest(e);
|
|
1093
|
+
else if (act.dataset.act === "show") { toPlugin({ t: "focus", nodeId: e.nodeId }); toast("Selected in canvas"); }
|
|
1094
|
+
else if (act.dataset.act === "copy") copy(e.error);
|
|
1095
|
+
else if (act.dataset.act === "copysum") copy(e.sum);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (window.getSelection().toString()) return;
|
|
1099
|
+
openId = openId === e.id ? null : e.id;
|
|
1100
|
+
renderLog();
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
$("filter").addEventListener("click", (ev) => {
|
|
1104
|
+
const b = ev.target.closest("button[data-f]");
|
|
1105
|
+
if (!b) return;
|
|
1106
|
+
filter = b.dataset.f;
|
|
1107
|
+
for (const x of $("filter").children) x.classList.toggle("on", x === b);
|
|
1108
|
+
renderLog();
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
$("btnWaitCancel").onclick = () => {
|
|
1112
|
+
const w = [...waits.values()].pop();
|
|
1113
|
+
if (w) toPlugin({ t: "cancelWait", id: w.id });
|
|
1114
|
+
};
|
|
1115
|
+
$("btnClear").onclick = () => { entries.length = 0; openId = null; render(); };
|
|
1116
|
+
$("btnRetry").onclick = () => { retryDelay = 400; connect(); };
|
|
1117
|
+
$("btnCopyCmd").onclick = () => copy("Get-NetTCPConnection -LocalPort 3055");
|
|
1118
|
+
$("btnCompact").onclick = () => setCompact(true);
|
|
1119
|
+
$("btnExpand").onclick = () => setCompact(false);
|
|
1120
|
+
|
|
1121
|
+
function openSettings(on) {
|
|
1122
|
+
if (on) $("inChannel").value = settings.channel;
|
|
1123
|
+
document.body.classList.toggle("settings-open", on);
|
|
1124
|
+
if (on) setTimeout(() => $("inChannel").focus(), 120);
|
|
1125
|
+
}
|
|
1126
|
+
$("btnSettings").onclick = () => openSettings(true);
|
|
1127
|
+
$("btnCloseSettings").onclick = $("btnCancel").onclick = $("scrim").onclick = () => openSettings(false);
|
|
1128
|
+
$("btnResetSize").onclick = () => { toPlugin({ t: "resetSize" }); toast("Window size reset"); };
|
|
1129
|
+
$("btnSave").onclick = () => {
|
|
1130
|
+
const next = $("inChannel").value.trim() || "default";
|
|
1131
|
+
openSettings(false);
|
|
1132
|
+
if (next === settings.channel) return;
|
|
1133
|
+
settings.channel = next;
|
|
1134
|
+
toPlugin({ t: "set", key: "channel", value: next });
|
|
1135
|
+
renderSession();
|
|
1136
|
+
retryDelay = 400;
|
|
1137
|
+
connect();
|
|
1138
|
+
toast("Reconnecting on “" + next + "”");
|
|
1139
|
+
};
|
|
1140
|
+
$("inChannel").addEventListener("keydown", (ev) => { if (ev.key === "Enter") $("btnSave").click(); });
|
|
1141
|
+
document.addEventListener("keydown", (ev) => { if (ev.key === "Escape") openSettings(false); });
|
|
1142
|
+
|
|
1143
|
+
function setCompact(on) {
|
|
1144
|
+
settings.compact = on;
|
|
1145
|
+
document.body.classList.toggle("compact", on);
|
|
1146
|
+
toPlugin({ t: "compact", value: on });
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Resize from the corner grip: Figma windows can only be resized by the plugin itself.
|
|
1150
|
+
$("grip").addEventListener("pointerdown", (ev) => {
|
|
1151
|
+
ev.preventDefault();
|
|
1152
|
+
const grip = $("grip");
|
|
1153
|
+
grip.setPointerCapture(ev.pointerId);
|
|
1154
|
+
const startX = ev.screenX, startY = ev.screenY, startW = window.innerWidth, startH = window.innerHeight;
|
|
1155
|
+
let size = null;
|
|
1156
|
+
let frame = 0;
|
|
1157
|
+
const move = (e) => {
|
|
1158
|
+
size = { width: startW + e.screenX - startX, height: startH + e.screenY - startY };
|
|
1159
|
+
if (!frame) frame = requestAnimationFrame(() => { frame = 0; toPlugin({ t: "resize", width: size.width, height: size.height }); });
|
|
1160
|
+
};
|
|
1161
|
+
const up = () => {
|
|
1162
|
+
grip.removeEventListener("pointermove", move);
|
|
1163
|
+
grip.removeEventListener("pointerup", up);
|
|
1164
|
+
grip.removeEventListener("pointercancel", up);
|
|
1165
|
+
if (size) toPlugin({ t: "resize", width: size.width, height: size.height, save: true });
|
|
1166
|
+
};
|
|
1167
|
+
grip.addEventListener("pointermove", move);
|
|
1168
|
+
grip.addEventListener("pointerup", up);
|
|
1169
|
+
grip.addEventListener("pointercancel", up);
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// ─── Plugin messages ────────────────────────────────────────────────────────
|
|
1173
|
+
window.onmessage = (ev) => {
|
|
1174
|
+
const msg = ev.data && ev.data.pluginMessage;
|
|
1175
|
+
if (!msg) return;
|
|
1176
|
+
if (msg.t === "init") {
|
|
1177
|
+
version = msg.version;
|
|
1178
|
+
session = msg.session;
|
|
1179
|
+
settings = msg.settings;
|
|
1180
|
+
document.body.classList.toggle("compact", !!settings.compact);
|
|
1181
|
+
renderSession();
|
|
1182
|
+
render();
|
|
1183
|
+
connect();
|
|
1184
|
+
} else if (msg.t === "info") {
|
|
1185
|
+
session = msg.session;
|
|
1186
|
+
renderSession();
|
|
1187
|
+
send({ t: "info", fileName: session.fileName, page: session.page });
|
|
1188
|
+
} else if (msg.t === "res") {
|
|
1189
|
+
onResult(msg);
|
|
1190
|
+
} else if (msg.t === "progress") {
|
|
1191
|
+
onProgress(msg);
|
|
1192
|
+
} else if (msg.t === "wait") {
|
|
1193
|
+
waits.set(msg.id, msg);
|
|
1194
|
+
renderWait();
|
|
1195
|
+
} else if (msg.t === "waitDone") {
|
|
1196
|
+
waits.delete(msg.id);
|
|
1197
|
+
renderWait();
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
setState("connecting");
|
|
1202
|
+
render();
|
|
1203
|
+
toPlugin({ t: "ready" });
|
|
1204
|
+
</script>
|
|
1205
|
+
</body>
|
|
1206
|
+
</html>
|