@spark-apps/piclet 1.0.3 → 1.0.4

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.
@@ -58,21 +58,27 @@ input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;hei
58
58
  @keyframes s{to{transform:rotate(360deg)}}
59
59
  .ld span{font-size:12px;color:var(--txt3)}
60
60
 
61
- /* Log - fills available space */
62
- .log{display:none;font:11px/1.4 Consolas,monospace;background:var(--bg2);border-radius:5px;padding:8px;flex:1;min-height:60px;max-height:none;overflow-y:auto}
63
- .log.on{display:block}
61
+ /* Log with preview - 6 lines high with thumbnail */
62
+ .log-container{display:none;gap:8px;height:calc(6 * 1.4em + 16px);flex-shrink:0}
63
+ .log-container.on{display:flex}
64
+ .log{flex:1;font:10px/1.4 Consolas,monospace;background:var(--bg2);border-radius:5px;padding:8px;overflow-y:auto;min-width:0}
64
65
  .log p{margin:2px 0}
65
66
  .log .i{color:var(--txt3)}
66
67
  .log .s{color:var(--ok)}
67
68
  .log .e{color:var(--err)}
69
+ .log-preview{width:80px;height:80px;object-fit:contain;background:var(--bg2);border-radius:5px;border:1px solid var(--brd);flex-shrink:0;display:none;align-self:center}
70
+ .log-preview.on{display:block}
68
71
 
69
- /* Done state */
70
- .dn{display:none;flex:1;flex-direction:column;align-items:center;justify-content:center;gap:4px}
71
- .dn.on{display:flex}
72
- .dn h4{font-size:14px}
73
- .dn.ok h4{color:var(--ok)}
74
- .dn.err h4{color:var(--err)}
75
- .dn p{font-size:12px;color:var(--txt2);text-align:center}
72
+ /* Done modal */
73
+ #doneModal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:100;align-items:center;justify-content:center}
74
+ #doneModal.on{display:flex}
75
+ .done-modal{background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:16px 24px;text-align:center;min-width:200px}
76
+ .done-modal h4{font-size:14px;margin:0 0 4px}
77
+ .done-modal.ok h4{color:var(--ok)}
78
+ .done-modal.err h4{color:var(--err)}
79
+ .done-modal p{font-size:11px;color:var(--txt2);margin:0 0 12px;word-break:break-word}
80
+ .done-btns{display:flex;gap:8px;justify-content:center}
81
+ .done-btns .btn{padding:8px 16px;font-size:12px}
76
82
 
77
83
  /* Warning/info box */
78
84
  .warn-box{display:none;font-size:10px;color:#ca8a04;background:rgba(202,138,4,0.1);padding:6px 8px;border-radius:4px;text-align:center}
@@ -162,6 +162,11 @@
162
162
  .gif-export-btn{display:none;width:100%;padding:8px 4px;font-size:10px;background:var(--acc);border:none;border-radius:4px;color:#000;cursor:pointer;font-weight:600;margin-top:6px}
163
163
  .gif-export-btn.show{display:block}
164
164
  .gif-export-btn:hover{background:var(--acc2)}
165
+ .frame-actions{display:none;gap:4px;margin-top:6px}
166
+ .frame-actions.show{display:flex}
167
+ .frame-actions button{flex:1;padding:6px 4px;font-size:9px;background:var(--bg3);border:1px solid var(--brd);border-radius:4px;color:var(--txt2);cursor:pointer}
168
+ .frame-actions button:hover{border-color:var(--acc);color:var(--acc)}
169
+ .frame-actions button.del:hover{border-color:#c44;color:#c44}
165
170
 
166
171
  /* Export modal */
167
172
  .export-modal{background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:16px 20px;min-width:280px;max-width:320px}
@@ -227,7 +232,12 @@
227
232
  <span>Frames</span>
228
233
  </div>
229
234
  <div class="frame-strip-scroll" id="frameScroll"></div>
235
+ <div class="frame-actions" id="frameActions">
236
+ <button class="del" onclick="deleteSelectedFrame()" title="Delete frame">Del</button>
237
+ <button onclick="replaceSelectedFrame()" title="Replace frame">Replace</button>
238
+ </div>
230
239
  <button class="gif-export-btn" id="gifExportBtn" onclick="showExportModal()">Export</button>
240
+ <input type="file" id="replaceInput" accept=".png,.jpg,.jpeg" style="display:none" onchange="handleReplaceFile(event)">
231
241
  </div>
232
242
 
233
243
  <!-- Left divider (for frame panel) -->
@@ -280,6 +290,12 @@
280
290
  </div>
281
291
  <label class="opt"><input type="checkbox" id="rb-trim" checked onchange="schedulePreview()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Auto-trim edges</label>
282
292
  <label class="opt"><input type="checkbox" id="rb-edges" onchange="schedulePreview()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Border only (preserve inner)</label>
293
+ <label class="opt"><input type="checkbox" id="rb-edgedetect" onchange="toggleEdgeDetect()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Feather edges (smoother cuts)</label>
294
+ <div class="tool-row" id="rb-edgeRow" style="display:none">
295
+ <label>Softness</label>
296
+ <input type="range" id="rb-edgestr" min="0" max="100" value="50" oninput="$('rb-edgestrV').textContent=this.value+'%'" onchange="schedulePreview()">
297
+ <span class="val" id="rb-edgestrV">50%</span>
298
+ </div>
283
299
  </div>
284
300
  </div>
285
301
  </div>
@@ -379,15 +395,21 @@
379
395
 
380
396
  <!-- Loading state -->
381
397
  <div class="ld" id="L"><div class="sp"></div><span id="lT">Processing...</span></div>
382
- <!-- Log -->
383
- <div class="log" id="G"></div>
384
- <!-- Done state -->
385
- <div class="dn" id="D">
398
+ <!-- Log with preview -->
399
+ <div class="log-container" id="LC">
400
+ <div class="log" id="G"></div>
401
+ <img class="log-preview" id="LP">
402
+ </div>
403
+ </div>
404
+
405
+ <!-- Done modal -->
406
+ <div class="modal-overlay" id="doneModal">
407
+ <div class="done-modal" id="D">
386
408
  <h4 id="dT"></h4>
387
409
  <p id="dM"></p>
388
- <div class="btns" style="width:100%;margin-top:8px">
410
+ <div class="done-btns">
389
411
  <button class="btn btn-g" onclick="reset()">Back</button>
390
- <button class="btn" onclick="openOutputFolder()" id="openFolderBtn" style="display:none">Open Folder</button>
412
+ <button class="btn" onclick="openOutputFolder()" id="openFolderBtn">Open Folder</button>
391
413
  <button class="btn btn-p" onclick="PicLet.close()">Done</button>
392
414
  </div>
393
415
  </div>
@@ -403,6 +425,55 @@
403
425
  </div>
404
426
  </div>
405
427
 
428
+ <!-- Simplify GIF modal -->
429
+ <div class="modal-overlay" id="simplifyModal" onclick="hideSimplifyModal(event)">
430
+ <div class="export-modal" onclick="event.stopPropagation()">
431
+ <h3>Large GIF Detected</h3>
432
+ <p style="font-size:11px;color:var(--txt2);margin-bottom:12px">
433
+ This GIF has <b id="simplifyFrameCount">0</b> frames which may be slow to process.
434
+ Would you like to simplify it by skipping frames?
435
+ </p>
436
+ <div class="export-options">
437
+ <label class="export-opt" onclick="selectSimplifyOption(1)">
438
+ <input type="radio" name="simplifyType" value="1">
439
+ <span class="radio"></span>
440
+ <div class="export-opt-content">
441
+ <div class="export-opt-title">Keep All Frames</div>
442
+ <div class="export-opt-desc">Use the original GIF as-is</div>
443
+ </div>
444
+ </label>
445
+ <label class="export-opt selected" onclick="selectSimplifyOption(2)">
446
+ <input type="radio" name="simplifyType" value="2" checked>
447
+ <span class="radio"></span>
448
+ <div class="export-opt-content">
449
+ <div class="export-opt-title">Skip Every 2nd Frame</div>
450
+ <div class="export-opt-desc" id="simplifyDesc2">Reduces to ~0 frames</div>
451
+ </div>
452
+ </label>
453
+ <label class="export-opt" onclick="selectSimplifyOption(3)">
454
+ <input type="radio" name="simplifyType" value="3">
455
+ <span class="radio"></span>
456
+ <div class="export-opt-content">
457
+ <div class="export-opt-title">Skip Every 3rd Frame</div>
458
+ <div class="export-opt-desc" id="simplifyDesc3">Reduces to ~0 frames</div>
459
+ </div>
460
+ </label>
461
+ <label class="export-opt" onclick="selectSimplifyOption(4)">
462
+ <input type="radio" name="simplifyType" value="4">
463
+ <span class="radio"></span>
464
+ <div class="export-opt-content">
465
+ <div class="export-opt-title">Skip Every 4th Frame</div>
466
+ <div class="export-opt-desc" id="simplifyDesc4">Reduces to ~0 frames</div>
467
+ </div>
468
+ </label>
469
+ </div>
470
+ <div class="export-modal-btns">
471
+ <button class="cancel" onclick="hideSimplifyModal()">Cancel</button>
472
+ <button class="confirm" onclick="confirmSimplify()">Continue</button>
473
+ </div>
474
+ </div>
475
+ </div>
476
+
406
477
  <!-- Export modal -->
407
478
  <div class="modal-overlay" id="exportModal" onclick="hideExportModal(event)">
408
479
  <div class="export-modal" onclick="event.stopPropagation()">
@@ -645,6 +716,16 @@ function toggleRatioLock() {
645
716
  schedulePreview();
646
717
  }
647
718
 
719
+ function toggleEdgeDetect() {
720
+ const enabled = $('rb-edgedetect').checked;
721
+ $('rb-edgeRow').style.display = enabled ? 'flex' : 'none';
722
+ // Edge detect and preserve inner are mutually exclusive
723
+ if (enabled) {
724
+ $('rb-edges').checked = false;
725
+ }
726
+ schedulePreview();
727
+ }
728
+
648
729
  // Get combined options for all active tools
649
730
  function getOptions() {
650
731
  const opts = { tools: Array.from(activeTools) };
@@ -653,7 +734,9 @@ function getOptions() {
653
734
  opts.removebg = {
654
735
  fuzz: +$('rb-fuzz').value,
655
736
  trim: $('rb-trim').checked,
656
- preserveInner: $('rb-edges').checked
737
+ preserveInner: $('rb-edges').checked,
738
+ edgeDetect: $('rb-edgedetect').checked,
739
+ edgeStrength: +$('rb-edgestr').value
657
740
  };
658
741
  }
659
742
 
@@ -897,11 +980,24 @@ let isPlaying = false;
897
980
  let playInterval = null;
898
981
  let playSpeed = 100; // ms per frame
899
982
 
900
- function showFrameStrip() {
983
+ function showFrameStrip(skipSimplifyCheck = false) {
984
+ // Check for large GIFs and offer to simplify
985
+ if (!skipSimplifyCheck && imageInfo.frameCount >= LARGE_GIF_THRESHOLD) {
986
+ checkForLargeGif(imageInfo.frameCount, () => {
987
+ // After simplification (or if user keeps all frames), show the strip
988
+ actuallyShowFrameStrip();
989
+ });
990
+ return;
991
+ }
992
+ actuallyShowFrameStrip();
993
+ }
994
+
995
+ function actuallyShowFrameStrip() {
901
996
  $('framePanel').classList.add('show');
902
997
  $('dividerLeft').classList.add('show');
903
998
  $('playControls').classList.add('show');
904
999
  $('gifExportBtn').classList.add('show');
1000
+ $('frameActions').classList.add('show');
905
1001
  $('frameCount').textContent = imageInfo.frameCount;
906
1002
  selectedFrameIndex = 0;
907
1003
  gifFrames = [];
@@ -914,6 +1010,7 @@ function hideFrameStrip() {
914
1010
  $('dividerLeft').classList.remove('show');
915
1011
  $('playControls').classList.remove('show');
916
1012
  $('gifExportBtn').classList.remove('show');
1013
+ $('frameActions').classList.remove('show');
917
1014
  stopPlayback();
918
1015
  gifFrames = [];
919
1016
  framePreviewCache = {};
@@ -1178,8 +1275,9 @@ async function runExport(opts, message) {
1178
1275
  $('M').classList.add('hide');
1179
1276
  $('B').classList.add('hide');
1180
1277
  $('L').classList.add('on');
1181
- $('G').classList.add('on');
1278
+ $('LC').classList.add('on');
1182
1279
  $('G').innerHTML = '';
1280
+ $('LP').classList.remove('on');
1183
1281
  $('lT').textContent = message;
1184
1282
 
1185
1283
  try {
@@ -1187,6 +1285,8 @@ async function runExport(opts, message) {
1187
1285
  if (result.logs) {
1188
1286
  result.logs.forEach(l => log('G', l.type[0], l.message));
1189
1287
  }
1288
+ // Try to show output preview
1289
+ if (result.success) await showLogPreview();
1190
1290
  showDone(result.success, result.success ? 'Done' : 'Failed', result.success ? result.output : result.error);
1191
1291
  } catch (e) {
1192
1292
  log('G', 'e', e.message);
@@ -1194,6 +1294,193 @@ async function runExport(opts, message) {
1194
1294
  }
1195
1295
  }
1196
1296
 
1297
+ // ── Frame Delete/Replace ──
1298
+
1299
+ async function deleteSelectedFrame() {
1300
+ if (!isGifFile || gifFrames.length <= 1) {
1301
+ showAlert('Cannot delete the only frame');
1302
+ return;
1303
+ }
1304
+
1305
+ stopPlayback();
1306
+
1307
+ // Show loading on the frame
1308
+ const thumb = document.querySelector(`.frame-thumb[data-index="${selectedFrameIndex}"]`);
1309
+ if (thumb) thumb.classList.add('processing');
1310
+
1311
+ try {
1312
+ const result = await postJson('/api/delete-frame', { frameIndex: selectedFrameIndex });
1313
+
1314
+ if (result.success) {
1315
+ imageInfo.frameCount = result.frameCount;
1316
+ $('frameCount').textContent = result.frameCount;
1317
+
1318
+ // Adjust selected index if needed
1319
+ if (selectedFrameIndex >= result.frameCount) {
1320
+ selectedFrameIndex = result.frameCount - 1;
1321
+ }
1322
+
1323
+ // Reload frame thumbnails
1324
+ gifFrames = [];
1325
+ framePreviewCache = {};
1326
+ loadFrameThumbnails();
1327
+
1328
+ // Show updated frame
1329
+ setTimeout(() => selectFrame(selectedFrameIndex), 100);
1330
+ } else {
1331
+ if (thumb) thumb.classList.remove('processing');
1332
+ showAlert('Failed to delete frame: ' + (result.error || 'Unknown error'));
1333
+ }
1334
+ } catch (err) {
1335
+ if (thumb) thumb.classList.remove('processing');
1336
+ showAlert('Failed to delete frame: ' + err.message);
1337
+ }
1338
+ }
1339
+
1340
+ function replaceSelectedFrame() {
1341
+ if (!isGifFile) return;
1342
+ $('replaceInput').click();
1343
+ }
1344
+
1345
+ async function handleReplaceFile(e) {
1346
+ const file = e.target.files[0];
1347
+ if (!file) return;
1348
+ e.target.value = ''; // Reset for future use
1349
+
1350
+ stopPlayback();
1351
+
1352
+ // Show loading on the frame
1353
+ const thumb = document.querySelector(`.frame-thumb[data-index="${selectedFrameIndex}"]`);
1354
+ if (thumb) thumb.classList.add('processing');
1355
+
1356
+ try {
1357
+ // Read file as base64
1358
+ const reader = new FileReader();
1359
+ reader.onload = async () => {
1360
+ const base64 = reader.result.split(',')[1]; // Remove data URL prefix
1361
+
1362
+ const result = await postJson('/api/replace-frame', {
1363
+ frameIndex: selectedFrameIndex,
1364
+ imageData: base64
1365
+ });
1366
+
1367
+ if (result.success) {
1368
+ // Reload frame thumbnails
1369
+ gifFrames = [];
1370
+ framePreviewCache = {};
1371
+ loadFrameThumbnails();
1372
+
1373
+ // Show updated frame
1374
+ setTimeout(() => selectFrame(selectedFrameIndex), 100);
1375
+ } else {
1376
+ if (thumb) thumb.classList.remove('processing');
1377
+ showAlert('Failed to replace frame: ' + (result.error || 'Unknown error'));
1378
+ }
1379
+ };
1380
+ reader.readAsDataURL(file);
1381
+ } catch (err) {
1382
+ if (thumb) thumb.classList.remove('processing');
1383
+ showAlert('Failed to replace frame: ' + err.message);
1384
+ }
1385
+ }
1386
+
1387
+ // ── GIF Simplification ──
1388
+
1389
+ const LARGE_GIF_THRESHOLD = 50; // Frames threshold for showing simplify prompt
1390
+ let selectedSimplifyOption = 2; // Default to skip every 2nd frame
1391
+ let pendingSimplifyCallback = null;
1392
+
1393
+ function checkForLargeGif(frameCount, callback) {
1394
+ if (frameCount >= LARGE_GIF_THRESHOLD) {
1395
+ showSimplifyModal(frameCount, callback);
1396
+ return true;
1397
+ }
1398
+ if (callback) callback();
1399
+ return false;
1400
+ }
1401
+
1402
+ function showSimplifyModal(frameCount, callback) {
1403
+ pendingSimplifyCallback = callback;
1404
+
1405
+ // Update modal content
1406
+ $('simplifyFrameCount').textContent = frameCount;
1407
+ $('simplifyDesc2').textContent = `Reduces to ~${Math.ceil(frameCount / 2)} frames`;
1408
+ $('simplifyDesc3').textContent = `Reduces to ~${Math.ceil(frameCount / 3)} frames`;
1409
+ $('simplifyDesc4').textContent = `Reduces to ~${Math.ceil(frameCount / 4)} frames`;
1410
+
1411
+ // Reset selection to skip every 2nd (recommended for most cases)
1412
+ selectedSimplifyOption = 2;
1413
+ document.querySelectorAll('#simplifyModal .export-opt').forEach(opt => {
1414
+ const input = opt.querySelector('input');
1415
+ opt.classList.toggle('selected', input.value === '2');
1416
+ input.checked = input.value === '2';
1417
+ });
1418
+
1419
+ $('simplifyModal').classList.add('on');
1420
+ }
1421
+
1422
+ function hideSimplifyModal(e) {
1423
+ if (!e || e.target === $('simplifyModal')) {
1424
+ $('simplifyModal').classList.remove('on');
1425
+ pendingSimplifyCallback = null;
1426
+ }
1427
+ }
1428
+
1429
+ function selectSimplifyOption(value) {
1430
+ selectedSimplifyOption = value;
1431
+ document.querySelectorAll('#simplifyModal .export-opt').forEach(opt => {
1432
+ const input = opt.querySelector('input');
1433
+ opt.classList.toggle('selected', +input.value === value);
1434
+ input.checked = +input.value === value;
1435
+ });
1436
+ }
1437
+
1438
+ async function confirmSimplify() {
1439
+ // Save callback before hiding modal (hideSimplifyModal clears it)
1440
+ const callback = pendingSimplifyCallback;
1441
+ hideSimplifyModal();
1442
+
1443
+ if (selectedSimplifyOption === 1) {
1444
+ // Keep all frames - just continue
1445
+ if (callback) callback();
1446
+ return;
1447
+ }
1448
+
1449
+ // Show loading state
1450
+ pA.innerHTML = '<div class="mini-sp"></div>';
1451
+ pI.textContent = 'Simplifying GIF...';
1452
+
1453
+ try {
1454
+ const result = await postJson('/api/simplify-gif', { skipFactor: selectedSimplifyOption });
1455
+
1456
+ if (result.success) {
1457
+ // Update image info with simplified version
1458
+ imageInfo.width = result.width;
1459
+ imageInfo.height = result.height;
1460
+ imageInfo.frameCount = result.frameCount;
1461
+
1462
+ // Update display
1463
+ $('origSize').textContent = imageInfo.width + '×' + imageInfo.height;
1464
+
1465
+ // Refresh frame strip (skip simplify check since we just simplified)
1466
+ currentFrameCount = result.frameCount;
1467
+ if (result.frameCount > 1) {
1468
+ showFrameStrip(true); // Skip simplify check
1469
+ } else {
1470
+ hideFrameStrip();
1471
+ }
1472
+
1473
+ if (callback) callback();
1474
+ } else {
1475
+ showAlert('Failed to simplify GIF: ' + (result.error || 'Unknown error'));
1476
+ showOriginal();
1477
+ }
1478
+ } catch (err) {
1479
+ showAlert('Failed to simplify GIF: ' + err.message);
1480
+ showOriginal();
1481
+ }
1482
+ }
1483
+
1197
1484
  // ── Store Assets Preset Management ──
1198
1485
 
1199
1486
  // Handle preset selection change
@@ -1373,8 +1660,9 @@ async function apply() {
1373
1660
  $('M').classList.add('hide');
1374
1661
  $('B').classList.add('hide');
1375
1662
  $('L').classList.add('on');
1376
- $('G').classList.add('on');
1663
+ $('LC').classList.add('on');
1377
1664
  $('G').innerHTML = '';
1665
+ $('LP').classList.remove('on');
1378
1666
 
1379
1667
  const toolNames = Array.from(activeTools).map(t => {
1380
1668
  const names = { removebg: 'Remove BG', scale: 'Scale', icons: 'Icons', storepack: 'Store Assets' };
@@ -1389,6 +1677,8 @@ async function apply() {
1389
1677
  result.logs.forEach(l => log('G', l.type[0], l.message));
1390
1678
  }
1391
1679
 
1680
+ // Try to show output preview
1681
+ if (result.success) await showLogPreview();
1392
1682
  showDone(result.success, result.success ? 'Done' : 'Failed', result.success ? result.output : result.error);
1393
1683
  } catch (e) {
1394
1684
  log('G', 'e', e.message);
@@ -1398,11 +1688,12 @@ async function apply() {
1398
1688
 
1399
1689
  // Reset to main view
1400
1690
  function reset() {
1401
- $('D').classList.remove('on', 'ok', 'err');
1402
- $('G').classList.remove('on');
1691
+ $('doneModal').classList.remove('on');
1692
+ $('D').classList.remove('ok', 'err');
1693
+ $('LC').classList.remove('on');
1694
+ $('LP').classList.remove('on');
1403
1695
  $('M').classList.remove('hide');
1404
1696
  $('B').classList.remove('hide');
1405
- $('openFolderBtn').style.display = 'none';
1406
1697
  lastOutputSuccess = false;
1407
1698
  }
1408
1699
 
@@ -1420,10 +1711,23 @@ function openOutputFolder() {
1420
1711
  postJson('/api/open-folder', {});
1421
1712
  }
1422
1713
 
1423
- // Show done state with optional folder button
1714
+ // Show output preview thumbnail in log area
1715
+ async function showLogPreview() {
1716
+ try {
1717
+ const result = await fetchJson('/api/output-preview');
1718
+ if (result.success && result.imageData) {
1719
+ $('LP').src = result.imageData;
1720
+ $('LP').classList.add('on');
1721
+ }
1722
+ } catch { /* ignore */ }
1723
+ }
1724
+
1725
+ // Show done modal
1424
1726
  function showDone(success, title, message) {
1425
1727
  $('L').classList.remove('on');
1426
- $('D').classList.add('on', success ? 'ok' : 'err');
1728
+ $('doneModal').classList.add('on');
1729
+ $('D').classList.add(success ? 'ok' : 'err');
1730
+ $('D').classList.remove(success ? 'err' : 'ok');
1427
1731
  $('dT').textContent = title;
1428
1732
  $('dM').textContent = message;
1429
1733
  lastOutputSuccess = success;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spark-apps/piclet",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Lightweight image tools for content creators",
5
5
  "type": "module",
6
6
  "bin": {