@pedroab0/skills 0.0.0-stage → 1.0.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/README.md +173 -2
- package/bin/cli.js +748 -0
- package/figma-extractor/README.md +170 -0
- package/figma-extractor/SKILL.md +199 -0
- package/figma-extractor/demo/desktop_home.json +2434 -0
- package/figma-extractor/demo/dt_money_desktop_home.png +0 -0
- package/figma-extractor/demo/dt_money_preview.png +0 -0
- package/figma-extractor/demo/dt_money_selection.json +472 -0
- package/figma-extractor/demo/index.html +188 -0
- package/figma-extractor/demo/style.css +360 -0
- package/figma-extractor/figma-plugin/README.md +70 -0
- package/figma-extractor/figma-plugin/code.js +407 -0
- package/figma-extractor/figma-plugin/manifest.json +10 -0
- package/figma-extractor/figma-plugin/ui.html +470 -0
- package/figma-extractor/scripts/fetch_figma.js +1314 -0
- package/figma-extractor/scripts/test_fetch_figma.js +601 -0
- package/figma-node-builder/README.md +107 -0
- package/figma-node-builder/SKILL.md +113 -0
- package/figma-node-builder/scripts/inspect_spec.js +514 -0
- package/figma-node-builder/scripts/test_inspect_spec.js +148 -0
- package/figma-to-code/README.md +160 -0
- package/figma-to-code/SKILL.md +211 -0
- package/figma-to-code/scripts/fetch_figma.js +1251 -0
- package/figma-to-code/scripts/test_fetch_figma.js +581 -0
- package/package.json +39 -4
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Figma Node JSON Extractor</title>
|
|
6
|
+
<style>
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
background: #1e1e1e;
|
|
15
|
+
color: #e0e0e0;
|
|
16
|
+
padding: 16px;
|
|
17
|
+
font-size: 13px;
|
|
18
|
+
line-height: 1.4;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
height: 100vh;
|
|
22
|
+
overflow-y: auto;
|
|
23
|
+
}
|
|
24
|
+
.header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
border-bottom: 1px solid #333;
|
|
29
|
+
padding-bottom: 12px;
|
|
30
|
+
margin-bottom: 14px;
|
|
31
|
+
}
|
|
32
|
+
.badge {
|
|
33
|
+
background: #0d9488;
|
|
34
|
+
color: #ffffff;
|
|
35
|
+
font-size: 10px;
|
|
36
|
+
font-weight: 700;
|
|
37
|
+
padding: 2px 7px;
|
|
38
|
+
border-radius: 10px;
|
|
39
|
+
text-transform: uppercase;
|
|
40
|
+
letter-spacing: 0.5px;
|
|
41
|
+
}
|
|
42
|
+
.title {
|
|
43
|
+
font-size: 14px;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
color: #fff;
|
|
46
|
+
}
|
|
47
|
+
.empty-state {
|
|
48
|
+
flex: 1;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: center;
|
|
53
|
+
text-align: center;
|
|
54
|
+
color: #888;
|
|
55
|
+
padding: 40px 20px;
|
|
56
|
+
}
|
|
57
|
+
.empty-icon {
|
|
58
|
+
font-size: 32px;
|
|
59
|
+
margin-bottom: 10px;
|
|
60
|
+
}
|
|
61
|
+
.content {
|
|
62
|
+
display: none;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 12px;
|
|
65
|
+
flex: 1;
|
|
66
|
+
min-height: 0;
|
|
67
|
+
}
|
|
68
|
+
.card {
|
|
69
|
+
background: #252526;
|
|
70
|
+
border: 1px solid #383838;
|
|
71
|
+
border-radius: 8px;
|
|
72
|
+
padding: 12px;
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
flex: 1;
|
|
76
|
+
min-height: 0;
|
|
77
|
+
}
|
|
78
|
+
.content.no-preview .card {
|
|
79
|
+
flex: initial;
|
|
80
|
+
}
|
|
81
|
+
.node-header {
|
|
82
|
+
display: flex;
|
|
83
|
+
justify-content: space-between;
|
|
84
|
+
align-items: center;
|
|
85
|
+
margin-bottom: 8px;
|
|
86
|
+
}
|
|
87
|
+
.node-name {
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
font-size: 13px;
|
|
90
|
+
color: #fff;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
text-overflow: ellipsis;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
max-width: 260px;
|
|
95
|
+
}
|
|
96
|
+
.node-type {
|
|
97
|
+
font-size: 11px;
|
|
98
|
+
background: #333;
|
|
99
|
+
color: #aaa;
|
|
100
|
+
padding: 2px 6px;
|
|
101
|
+
border-radius: 4px;
|
|
102
|
+
}
|
|
103
|
+
.metrics {
|
|
104
|
+
display: flex;
|
|
105
|
+
gap: 16px;
|
|
106
|
+
font-size: 11px;
|
|
107
|
+
color: #999;
|
|
108
|
+
}
|
|
109
|
+
.metric strong {
|
|
110
|
+
color: #ddd;
|
|
111
|
+
}
|
|
112
|
+
.preview-box {
|
|
113
|
+
margin-top: 10px;
|
|
114
|
+
background: #181818;
|
|
115
|
+
border: 1px solid #2a2a2a;
|
|
116
|
+
border-radius: 6px;
|
|
117
|
+
padding: 10px;
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
flex: 1;
|
|
122
|
+
min-height: 180px;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
}
|
|
125
|
+
.preview-box img {
|
|
126
|
+
max-width: 100%;
|
|
127
|
+
max-height: 100%;
|
|
128
|
+
width: auto;
|
|
129
|
+
height: auto;
|
|
130
|
+
object-fit: contain;
|
|
131
|
+
border-radius: 4px;
|
|
132
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
|
133
|
+
}
|
|
134
|
+
.actions {
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
gap: 8px;
|
|
138
|
+
}
|
|
139
|
+
button {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
justify-content: center;
|
|
143
|
+
gap: 8px;
|
|
144
|
+
width: 100%;
|
|
145
|
+
padding: 9px 14px;
|
|
146
|
+
border-radius: 6px;
|
|
147
|
+
font-size: 12px;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
border: none;
|
|
151
|
+
transition: all 0.15s ease;
|
|
152
|
+
}
|
|
153
|
+
.btn-primary {
|
|
154
|
+
background: #0d9488;
|
|
155
|
+
color: #fff;
|
|
156
|
+
}
|
|
157
|
+
.btn-primary:hover {
|
|
158
|
+
background: #0f766e;
|
|
159
|
+
}
|
|
160
|
+
.btn-secondary {
|
|
161
|
+
background: #333338;
|
|
162
|
+
color: #eee;
|
|
163
|
+
border: 1px solid #444;
|
|
164
|
+
}
|
|
165
|
+
.btn-secondary:hover {
|
|
166
|
+
background: #3e3e46;
|
|
167
|
+
}
|
|
168
|
+
.btn-row {
|
|
169
|
+
display: flex;
|
|
170
|
+
gap: 8px;
|
|
171
|
+
}
|
|
172
|
+
.btn-row button {
|
|
173
|
+
flex: 1;
|
|
174
|
+
min-width: 0;
|
|
175
|
+
padding: 9px 6px;
|
|
176
|
+
font-size: 11px;
|
|
177
|
+
}
|
|
178
|
+
.hint {
|
|
179
|
+
margin-top: 0;
|
|
180
|
+
background: #182220;
|
|
181
|
+
border: 1px solid #115e59;
|
|
182
|
+
color: #a7f3d0;
|
|
183
|
+
padding: 10px;
|
|
184
|
+
border-radius: 6px;
|
|
185
|
+
font-size: 11px;
|
|
186
|
+
line-height: 1.4;
|
|
187
|
+
}
|
|
188
|
+
.hint code {
|
|
189
|
+
background: #0f3632;
|
|
190
|
+
padding: 2px 4px;
|
|
191
|
+
border-radius: 3px;
|
|
192
|
+
font-family: monospace;
|
|
193
|
+
color: #5eead4;
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
196
|
+
</head>
|
|
197
|
+
<body>
|
|
198
|
+
<div class="header">
|
|
199
|
+
<div class="title">Figma Node JSON Extractor</div>
|
|
200
|
+
<div class="badge">Developer Handoff</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div id="empty-state" class="empty-state">
|
|
204
|
+
<div class="empty-icon">📐</div>
|
|
205
|
+
<p><strong>Select a frame or component</strong></p>
|
|
206
|
+
<p style="font-size: 11px; margin-top: 4px;">Click any element on canvas to inspect and export its Node JSON.</p>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div id="content" class="content">
|
|
210
|
+
<div class="card">
|
|
211
|
+
<div class="node-header">
|
|
212
|
+
<span id="node-name" class="node-name">Frame Name</span>
|
|
213
|
+
<span id="node-type" class="node-type">FRAME</span>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="metrics">
|
|
216
|
+
<div class="metric">Bounds: <strong id="node-bounds">0 × 0 px</strong></div>
|
|
217
|
+
<div class="metric">AST Size: <strong id="node-size">0 KB</strong></div>
|
|
218
|
+
</div>
|
|
219
|
+
<div id="preview-wrapper" class="preview-box">
|
|
220
|
+
<img id="preview-img" src="" alt="Component Preview">
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div class="actions">
|
|
225
|
+
<button id="btn-copy" class="btn-primary">
|
|
226
|
+
<span>📋 Copy Node JSON (Clipboard)</span>
|
|
227
|
+
</button>
|
|
228
|
+
|
|
229
|
+
<div class="btn-row">
|
|
230
|
+
<button id="btn-download-json" class="btn-secondary" title="Download JSON AST spec">
|
|
231
|
+
<span>💾 .json</span>
|
|
232
|
+
</button>
|
|
233
|
+
<button id="btn-download-png" class="btn-secondary" title="Download PNG preview image">
|
|
234
|
+
<span>🖼 .png</span>
|
|
235
|
+
</button>
|
|
236
|
+
<button id="btn-download-both" class="btn-secondary" title="Download both spec & preview in a .zip archive">
|
|
237
|
+
<span>📦 Both (.zip)</span>
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div class="hint">
|
|
243
|
+
<strong>✨ Fast Local Handoff</strong>: Save files into <code>./.specs/</code>, then tell your agent:<br>
|
|
244
|
+
<code>Build component from .specs/<span id="hint-name">card</span>.json</code>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<script>
|
|
249
|
+
let currentData = null;
|
|
250
|
+
let currentImageBlob = null;
|
|
251
|
+
let currentImageBytes = null;
|
|
252
|
+
|
|
253
|
+
function triggerDownload(blob, filename) {
|
|
254
|
+
const url = URL.createObjectURL(blob);
|
|
255
|
+
const a = document.createElement('a');
|
|
256
|
+
a.href = url;
|
|
257
|
+
a.download = filename;
|
|
258
|
+
document.body.appendChild(a);
|
|
259
|
+
a.click();
|
|
260
|
+
document.body.removeChild(a);
|
|
261
|
+
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function flashButton(btn, text) {
|
|
265
|
+
const original = btn.innerHTML;
|
|
266
|
+
btn.innerHTML = `<span>${text}</span>`;
|
|
267
|
+
btn.style.borderColor = '#10b981';
|
|
268
|
+
setTimeout(() => {
|
|
269
|
+
btn.innerHTML = original;
|
|
270
|
+
btn.style.borderColor = '';
|
|
271
|
+
}, 1500);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Pure vanilla zero-dependency ZIP archive creator (Store format)
|
|
275
|
+
function createZipBlob(files) {
|
|
276
|
+
const crcTable = new Uint32Array(256);
|
|
277
|
+
for (let i = 0; i < 256; i++) {
|
|
278
|
+
let c = i;
|
|
279
|
+
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
|
280
|
+
crcTable[i] = c >>> 0;
|
|
281
|
+
}
|
|
282
|
+
function crc32(bytes) {
|
|
283
|
+
let crc = 0 ^ (-1);
|
|
284
|
+
for (let i = 0; i < bytes.length; i++) crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
|
|
285
|
+
return (crc ^ (-1)) >>> 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const parts = [];
|
|
289
|
+
const cdParts = [];
|
|
290
|
+
let offset = 0;
|
|
291
|
+
const encoder = new TextEncoder();
|
|
292
|
+
|
|
293
|
+
for (const f of files) {
|
|
294
|
+
const nameBytes = encoder.encode(f.name);
|
|
295
|
+
const dataBytes = f.data;
|
|
296
|
+
const crc = crc32(dataBytes);
|
|
297
|
+
const size = dataBytes.length;
|
|
298
|
+
|
|
299
|
+
const lh = new Uint8Array(30 + nameBytes.length);
|
|
300
|
+
const lv = new DataView(lh.buffer);
|
|
301
|
+
lv.setUint32(0, 0x04034b50, true);
|
|
302
|
+
lv.setUint16(4, 10, true);
|
|
303
|
+
lv.setUint16(6, 0x0800, true);
|
|
304
|
+
lv.setUint32(14, crc, true);
|
|
305
|
+
lv.setUint32(18, size, true);
|
|
306
|
+
lv.setUint32(22, size, true);
|
|
307
|
+
lv.setUint16(26, nameBytes.length, true);
|
|
308
|
+
lh.set(nameBytes, 30);
|
|
309
|
+
|
|
310
|
+
parts.push(lh, dataBytes);
|
|
311
|
+
|
|
312
|
+
const cd = new Uint8Array(46 + nameBytes.length);
|
|
313
|
+
const cv = new DataView(cd.buffer);
|
|
314
|
+
cv.setUint32(0, 0x02014b50, true);
|
|
315
|
+
cv.setUint16(4, 20, true);
|
|
316
|
+
cv.setUint16(6, 10, true);
|
|
317
|
+
cv.setUint16(8, 0x0800, true);
|
|
318
|
+
cv.setUint32(16, crc, true);
|
|
319
|
+
cv.setUint32(20, size, true);
|
|
320
|
+
cv.setUint32(24, size, true);
|
|
321
|
+
cv.setUint16(28, nameBytes.length, true);
|
|
322
|
+
cv.setUint32(42, offset, true);
|
|
323
|
+
cd.set(nameBytes, 46);
|
|
324
|
+
|
|
325
|
+
cdParts.push(cd);
|
|
326
|
+
offset += lh.length + size;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const cdOffset = offset;
|
|
330
|
+
let cdSize = 0;
|
|
331
|
+
for (const c of cdParts) cdSize += c.length;
|
|
332
|
+
|
|
333
|
+
const eocd = new Uint8Array(22);
|
|
334
|
+
const ev = new DataView(eocd.buffer);
|
|
335
|
+
ev.setUint32(0, 0x06054b50, true);
|
|
336
|
+
ev.setUint16(8, files.length, true);
|
|
337
|
+
ev.setUint16(10, files.length, true);
|
|
338
|
+
ev.setUint32(12, cdSize, true);
|
|
339
|
+
ev.setUint32(16, cdOffset, true);
|
|
340
|
+
|
|
341
|
+
return new Blob([...parts, ...cdParts, eocd], { type: 'application/zip' });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
window.onmessage = (event) => {
|
|
345
|
+
const msg = event.data.pluginMessage;
|
|
346
|
+
if (!msg) return;
|
|
347
|
+
|
|
348
|
+
const emptyEl = document.getElementById('empty-state');
|
|
349
|
+
const contentEl = document.getElementById('content');
|
|
350
|
+
|
|
351
|
+
if (msg.type === 'NO_SELECTION') {
|
|
352
|
+
emptyEl.style.display = 'flex';
|
|
353
|
+
contentEl.style.display = 'none';
|
|
354
|
+
currentData = null;
|
|
355
|
+
currentImageBlob = null;
|
|
356
|
+
currentImageBytes = null;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (msg.type === 'SPEC_EXPORTED') {
|
|
361
|
+
currentData = msg;
|
|
362
|
+
emptyEl.style.display = 'none';
|
|
363
|
+
contentEl.style.display = 'flex';
|
|
364
|
+
|
|
365
|
+
document.getElementById('node-name').textContent = msg.originalName || msg.name || 'Component';
|
|
366
|
+
document.getElementById('node-type').textContent = msg.nodeType || 'FRAME';
|
|
367
|
+
const w = (msg.bounds && typeof msg.bounds.width === 'number') ? msg.bounds.width : 0;
|
|
368
|
+
const h = (msg.bounds && typeof msg.bounds.height === 'number') ? msg.bounds.height : 0;
|
|
369
|
+
document.getElementById('node-bounds').textContent = `${w} × ${h} px`;
|
|
370
|
+
document.getElementById('hint-name').textContent = msg.name || 'component';
|
|
371
|
+
|
|
372
|
+
const jsonStr = JSON.stringify(msg.spec || {}, null, 2);
|
|
373
|
+
const sizeKb = (new Blob([jsonStr]).size / 1024).toFixed(1);
|
|
374
|
+
document.getElementById('node-size').textContent = `${sizeKb} KB`;
|
|
375
|
+
|
|
376
|
+
const previewImg = document.getElementById('preview-img');
|
|
377
|
+
const previewWrap = document.getElementById('preview-wrapper');
|
|
378
|
+
const btnPng = document.getElementById('btn-download-png');
|
|
379
|
+
|
|
380
|
+
// Extract image bytes from TypedArray or base64 fallback
|
|
381
|
+
let imageBytes = null;
|
|
382
|
+
if (msg.imageBytes instanceof Uint8Array) {
|
|
383
|
+
imageBytes = msg.imageBytes;
|
|
384
|
+
} else if (msg.imageBytes && msg.imageBytes.buffer instanceof ArrayBuffer) {
|
|
385
|
+
imageBytes = new Uint8Array(msg.imageBytes.buffer, msg.imageBytes.byteOffset || 0, msg.imageBytes.byteLength || msg.imageBytes.length);
|
|
386
|
+
} else if (msg.imageBase64) {
|
|
387
|
+
try {
|
|
388
|
+
const binary = atob(msg.imageBase64);
|
|
389
|
+
imageBytes = new Uint8Array(binary.length);
|
|
390
|
+
for (let i = 0; i < binary.length; i++) imageBytes[i] = binary.charCodeAt(i);
|
|
391
|
+
} catch (_) {}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (imageBytes && imageBytes.length > 0) {
|
|
395
|
+
currentImageBytes = imageBytes;
|
|
396
|
+
currentImageBlob = new Blob([imageBytes], { type: 'image/png' });
|
|
397
|
+
if (msg.imageBase64) {
|
|
398
|
+
previewImg.src = `data:image/png;base64,${msg.imageBase64}`;
|
|
399
|
+
} else {
|
|
400
|
+
previewImg.src = URL.createObjectURL(currentImageBlob);
|
|
401
|
+
}
|
|
402
|
+
previewWrap.style.display = 'flex';
|
|
403
|
+
contentEl.classList.remove('no-preview');
|
|
404
|
+
btnPng.disabled = false;
|
|
405
|
+
btnPng.style.opacity = '1';
|
|
406
|
+
} else {
|
|
407
|
+
currentImageBytes = null;
|
|
408
|
+
currentImageBlob = null;
|
|
409
|
+
previewWrap.style.display = 'none';
|
|
410
|
+
contentEl.classList.add('no-preview');
|
|
411
|
+
btnPng.disabled = true;
|
|
412
|
+
btnPng.style.opacity = '0.5';
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Copy to clipboard
|
|
418
|
+
document.getElementById('btn-copy').onclick = async () => {
|
|
419
|
+
if (!currentData) return;
|
|
420
|
+
const jsonStr = JSON.stringify(currentData.spec, null, 2);
|
|
421
|
+
try {
|
|
422
|
+
await navigator.clipboard.writeText(jsonStr);
|
|
423
|
+
const btn = document.getElementById('btn-copy');
|
|
424
|
+
const originalText = btn.innerHTML;
|
|
425
|
+
btn.innerHTML = '<span>✔ Copied Node JSON!</span>';
|
|
426
|
+
btn.style.background = '#16a34a';
|
|
427
|
+
parent.postMessage({ pluginMessage: { type: 'NOTIFY', message: `Copied "${currentData.name}.json" to clipboard!` } }, '*');
|
|
428
|
+
setTimeout(() => {
|
|
429
|
+
btn.innerHTML = originalText;
|
|
430
|
+
btn.style.background = '';
|
|
431
|
+
}, 2000);
|
|
432
|
+
} catch (err) {
|
|
433
|
+
console.error('Clipboard failed', err);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// Download JSON
|
|
438
|
+
document.getElementById('btn-download-json').onclick = () => {
|
|
439
|
+
if (!currentData) return;
|
|
440
|
+
const jsonStr = JSON.stringify(currentData.spec, null, 2);
|
|
441
|
+
const blob = new Blob([jsonStr], { type: 'application/json' });
|
|
442
|
+
triggerDownload(blob, `${currentData.name}.json`);
|
|
443
|
+
flashButton(document.getElementById('btn-download-json'), '✔ Saved .json');
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Download PNG
|
|
447
|
+
document.getElementById('btn-download-png').onclick = () => {
|
|
448
|
+
if (!currentData || !currentImageBlob) return;
|
|
449
|
+
triggerDownload(currentImageBlob, `${currentData.name}.png`);
|
|
450
|
+
flashButton(document.getElementById('btn-download-png'), '✔ Saved .png');
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Download Both (.specs.zip)
|
|
454
|
+
document.getElementById('btn-download-both').onclick = () => {
|
|
455
|
+
if (!currentData) return;
|
|
456
|
+
const jsonStr = JSON.stringify(currentData.spec, null, 2);
|
|
457
|
+
const enc = new TextEncoder();
|
|
458
|
+
const files = [
|
|
459
|
+
{ name: `${currentData.name}.json`, data: enc.encode(jsonStr) }
|
|
460
|
+
];
|
|
461
|
+
if (currentImageBytes) {
|
|
462
|
+
files.push({ name: `${currentData.name}.png`, data: currentImageBytes });
|
|
463
|
+
}
|
|
464
|
+
const zipBlob = createZipBlob(files);
|
|
465
|
+
triggerDownload(zipBlob, `${currentData.name}.specs.zip`);
|
|
466
|
+
flashButton(document.getElementById('btn-download-both'), '✔ Saved .zip');
|
|
467
|
+
};
|
|
468
|
+
</script>
|
|
469
|
+
</body>
|
|
470
|
+
</html>
|