draply-dev 1.3.9 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draply-dev",
3
- "version": "1.3.9",
3
+ "version": "1.4.1",
4
4
  "description": "Visual overlay for any frontend project — move, resize, restyle live in the browser, save to CSS",
5
5
  "author": "Arman",
6
6
  "type": "commonjs",
package/src/overlay.js CHANGED
@@ -629,7 +629,8 @@
629
629
  <input type="file" id="__ast_file__" accept="image/*" style="display:none">
630
630
  <div id="__ast_list__" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px"></div>
631
631
  <div id="__ast_hint__" style="font-size:10px;color:#44446a;line-height:1.5;display:none">
632
- Click thumbnail place on page<br>then drag to position
632
+ Click to <b>place</b><br>
633
+ ✦ Shift+Click to <b>replace</b> src/bg
633
634
  </div>
634
635
  <!-- Z-INDEX CONTROLS -->
635
636
  <div id="__ast_zctrl__" style="display:none;margin-top:6px;">
@@ -642,7 +643,6 @@
642
643
  <span class="ps-lbl">z-index</span>
643
644
  <input class="ps-inp" id="__z_val__" type="number" value="1" min="-999" max="9999" style="width:70px;flex:none">
644
645
  <button class="ps-apply" id="__z_set__" style="margin:0;padding:4px 8px;flex:none">SET</button>
645
- <button class="ps-apply" id="__ast_del__" style="width:100%;margin-top:6px;background:#ff4444;color:#fff;border:none;">✕ Delete image</button>
646
646
  </div>
647
647
  </div>
648
648
  </div>
@@ -1162,6 +1162,23 @@
1162
1162
  };
1163
1163
 
1164
1164
  function populateTypo(el) {
1165
+ // Make text directly editable
1166
+ if (!el.isContentEditable) {
1167
+ el.contentEditable = 'true';
1168
+ el.dataset.origText = el.innerText || '';
1169
+ el.focus();
1170
+
1171
+ const finishEdit = () => {
1172
+ el.contentEditable = 'false';
1173
+ el.removeEventListener('blur', finishEdit);
1174
+ if (el.innerText !== el.dataset.origText) {
1175
+ rec(el, { innerText: el.innerText }, { innerText: el.dataset.origText });
1176
+ toast('Text updated!');
1177
+ }
1178
+ };
1179
+ el.addEventListener('blur', finishEdit);
1180
+ }
1181
+
1165
1182
  const cs = getComputedStyle(el);
1166
1183
  typElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList].filter(c => !c.startsWith('__'))[0] : '');
1167
1184
  typSz.value = Math.round(parseFloat(cs.fontSize)) || 16;
@@ -1209,7 +1226,10 @@
1209
1226
  'line-height': typLh.value,
1210
1227
  'letter-spacing': typLs.value + 'px',
1211
1228
  };
1212
- if (typFont.value) props['font-family'] = `'${typFont.value}', sans-serif`;
1229
+ if (typFont.value) {
1230
+ props['font-family'] = `'${typFont.value}', sans-serif`;
1231
+ props['googleFont'] = typFont.value;
1232
+ }
1213
1233
  // Snapshot BEFORE previewTypo applies styles
1214
1234
  const prevProps = {};
1215
1235
  Object.keys(props).forEach(p => {
@@ -1248,12 +1268,25 @@
1248
1268
  [...files].forEach(file => {
1249
1269
  if (!file.type.startsWith('image/')) return;
1250
1270
  const reader = new FileReader();
1251
- reader.onload = ev => {
1252
- const asset = { id: Date.now() + Math.random(), name: file.name, src: ev.target.result };
1253
- astStore.push(asset);
1254
- addThumb(asset);
1255
- astHint.style.display = 'block';
1256
- toast('🖼️ ' + file.name + ' loaded');
1271
+ reader.onload = async ev => {
1272
+ try {
1273
+ const res = await fetch('/draply-upload', {
1274
+ method: 'POST',
1275
+ body: JSON.stringify({ name: file.name, base64: ev.target.result })
1276
+ });
1277
+ const data = await res.json();
1278
+ if (data.ok) {
1279
+ const asset = { id: Date.now() + Math.random(), name: file.name, src: data.url };
1280
+ astStore.push(asset);
1281
+ addThumb(asset);
1282
+ astHint.style.display = 'block';
1283
+ toast('🖼️ ' + file.name + ' loaded');
1284
+ } else {
1285
+ toast('⚠ Upload failed');
1286
+ }
1287
+ } catch(e) {
1288
+ toast('⚠ Upload failed');
1289
+ }
1257
1290
  };
1258
1291
  reader.readAsDataURL(file);
1259
1292
  });
@@ -1303,10 +1336,28 @@
1303
1336
  e.preventDefault(); e.stopPropagation();
1304
1337
  if (!pendingAsset || !placingEl) return;
1305
1338
 
1306
- // Drop asset at click position
1307
- const x = e.clientX - 60;
1308
- const y = e.clientY - 60;
1309
- placeAsset(pendingAsset, x, y, 120, 120);
1339
+ if (e.shiftKey) {
1340
+ // REPLACE mode
1341
+ if (e.target.tagName.toLowerCase() === 'img') {
1342
+ const prevSrc = e.target.getAttribute('src');
1343
+ e.target.src = pendingAsset.src;
1344
+ rec(e.target, { src: pendingAsset.src }, { src: prevSrc || '' });
1345
+ toast('🖼️ Image source replaced!');
1346
+ } else {
1347
+ const cs = getComputedStyle(e.target);
1348
+ const prevBg = cs.backgroundImage;
1349
+ e.target.style.backgroundImage = `url('${pendingAsset.src}')`;
1350
+ e.target.style.backgroundSize = 'cover';
1351
+ e.target.style.backgroundPosition = 'center';
1352
+ rec(e.target, { backgroundImage: `url('${pendingAsset.src}')`, backgroundSize: 'cover', backgroundPosition: 'center' }, { backgroundImage: prevBg });
1353
+ toast('🖼️ Background image set!');
1354
+ }
1355
+ } else {
1356
+ // PLACE mode
1357
+ const x = e.clientX - 60;
1358
+ const y = e.clientY - 60;
1359
+ placeAsset(pendingAsset, x, y, 120, 120);
1360
+ }
1310
1361
  cancelPlacing();
1311
1362
  }
1312
1363
 
@@ -1353,7 +1404,7 @@
1353
1404
  });
1354
1405
 
1355
1406
  rec(wrap, {
1356
- src: asset.name,
1407
+ src: asset.src,
1357
1408
  left: Math.round(x) + 'px',
1358
1409
  top: Math.round(y) + 'px',
1359
1410
  width: w + 'px',
@@ -1414,20 +1465,9 @@
1414
1465
 
1415
1466
  zVal.onkeydown = e => { if (e.key === 'Enter') zSet.click(); };
1416
1467
 
1417
- const astDel = document.getElementById('__ast_del__');
1418
- astDel.onclick = () => {
1419
- if (!selectedPlaced) return;
1420
- selectedPlaced.remove();
1421
- selectedPlaced = null;
1422
- zCtrl.style.display = 'none';
1423
- toast('🗑 Image deleted');
1424
- };
1425
1468
  // Drag placed assets
1426
1469
  document.addEventListener('mousemove', e => {
1427
1470
  if (!astDragEl || !state.dragging) return;
1428
- if (state.resizing || hdl.classList.contains('v') && document.activeElement !== hdl) {
1429
- // only handle if we initiated from an asset
1430
- }
1431
1471
  const dx = e.clientX - astDragSX, dy = e.clientY - astDragSY;
1432
1472
  astDragEl.style.left = (astDragOL + dx) + 'px';
1433
1473
  astDragEl.style.top = (astDragOT + dy) + 'px';
@@ -1446,7 +1486,6 @@
1446
1486
  });
1447
1487
  astDragEl.style.cursor = 'grab';
1448
1488
  astDragEl = null;
1449
- // Don't clear state.dragging here — the move tool mouseup handler does it
1450
1489
  });
1451
1490
 
1452
1491
  // ══════════════════════════════════════════
@@ -1469,11 +1508,26 @@
1469
1508
  });
1470
1509
  }
1471
1510
 
1472
- // Merge into state.changes (for save)
1511
+ // Extract exact file from React Fiber if available
1512
+ function getReactSource(element) {
1513
+ for (const key in element) {
1514
+ if (key.startsWith('__reactFiber$')) {
1515
+ let fiber = element[key];
1516
+ while (fiber) {
1517
+ if (fiber._debugSource && fiber._debugSource.fileName) return fiber._debugSource.fileName;
1518
+ fiber = fiber.return;
1519
+ }
1520
+ }
1521
+ }
1522
+ return null;
1523
+ }
1524
+
1525
+ // Merge into state.changes (for save/apply)
1473
1526
  const ch = {
1474
1527
  type: el.dataset.pixelshiftId ? 'inline' : 'css',
1475
1528
  pixelshiftId: el.dataset.pixelshiftId || null,
1476
1529
  selector,
1530
+ exactFile: getReactSource(el),
1477
1531
  file: el.dataset.pixelshiftFile || null,
1478
1532
  props
1479
1533
  };
@@ -1530,17 +1584,42 @@
1530
1584
  toast('↩ Reverted');
1531
1585
  }
1532
1586
 
1533
- sv.addEventListener('click', () => {
1534
- toast('⏳ Saving...');
1535
- fetch('/draply-save', {
1536
- method: 'POST',
1537
- headers: { 'Content-Type': 'application/json' },
1538
- body: JSON.stringify({ changes: state.changes })
1539
- }).then(r => r.json()).then(d => {
1540
- if (d.ok) toast('✅ Saved to CSS!');
1587
+ sv.addEventListener('click', async () => {
1588
+ // Check key config status
1589
+ let hasKey = false;
1590
+ try {
1591
+ const cfgRes = await fetch('/draply-config');
1592
+ const cfg = await cfgRes.json();
1593
+ hasKey = cfg.hasKey;
1594
+ } catch (e) {}
1595
+
1596
+ if (!hasKey) {
1597
+ const key = prompt('Draply AI Save: Enter your free Groq API key (from console.groq.com) to enable saving directly to React files:');
1598
+ if (key) {
1599
+ await fetch('/draply-config', { method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ apiKey: key.trim(), provider: 'groq' }) });
1600
+ } else {
1601
+ toast('Save aborted: AI Key required');
1602
+ return;
1603
+ }
1604
+ }
1605
+
1606
+ sv.disabled = true;
1607
+ sv.textContent = 'Applying...';
1608
+ try {
1609
+ const r = await fetch('/draply-ai-apply', {
1610
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1611
+ body: JSON.stringify({ changes: state.changes })
1612
+ });
1613
+ const d = await r.json();
1614
+ if (d.ok) toast(d.fallback ? 'Saved to draply.css (No AI Key)' : '✅ AI Applied to Source Code!');
1541
1615
  else toast('⚠ Error: ' + (d.error || 'unknown'));
1542
- }).catch(() => toast('⚠ Server unavailable'));
1543
- state.changes = []; history.length = 0; sv.disabled = true; updateUnsUI();
1616
+ } catch {
1617
+ toast('⚠ Server unreachable');
1618
+ }
1619
+
1620
+ state.changes = []; history.length = 0;
1621
+ sv.textContent = 'Save';
1622
+ updateUnsUI();
1544
1623
  });
1545
1624
 
1546
1625
  document.getElementById('__uns_clear__').onclick = () => {