draply-dev 1.4.0 → 1.4.2

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.
Files changed (3) hide show
  1. package/bin/cli.js +18 -2
  2. package/package.json +1 -1
  3. package/src/overlay.js +159 -26
package/bin/cli.js CHANGED
@@ -80,6 +80,19 @@ const server = http.createServer((req, res) => {
80
80
  });
81
81
  return;
82
82
  }
83
+ // ── Serve assets_draply ───────────────────────────────────────────────────
84
+ if (req.url.startsWith('/assets_draply/')) {
85
+ const filePath = path.join(process.cwd(), req.url.split('?')[0]);
86
+ if (fs.existsSync(filePath)) {
87
+ const ext = path.extname(filePath).toLowerCase();
88
+ const mime = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp' };
89
+ res.writeHead(200, { 'Content-Type': mime[ext] || 'application/octet-stream' });
90
+ fs.createReadStream(filePath).pipe(res);
91
+ } else {
92
+ res.writeHead(404); res.end();
93
+ }
94
+ return;
95
+ }
83
96
 
84
97
  // ── Config endpoint (get/set API key) ───────────────────────────────────────
85
98
  const configPath = path.join(process.cwd(), 'draply.config.json');
@@ -204,7 +217,8 @@ const server = http.createServer((req, res) => {
204
217
 
205
218
  items.forEach(item => {
206
219
  const propsStr = Object.entries(item.props).map(([k, v]) => ` ${k}: ${v}`).join('\n');
207
- changesBlock += `\nTarget Selector: ${item.selector}\nChanges:\n${propsStr}\n`;
220
+ changesBlock += `\nTarget Selector: ${item.selector}\nType: ${item.type}\nChanges:\n${propsStr}\n`;
221
+ if (item.type === 'create') changesBlock += `HTML to Insert: ${item.outerHTML}\n`;
208
222
  });
209
223
 
210
224
  const prompt = `You are a strict code editor applying style changes to a file.
@@ -223,8 +237,10 @@ Rules:
223
237
  - The content inside <search> must be an EXACT substring from the file snippet above (including indentation).
224
238
  - Update HTML/JSX inline styles or Tailwind classes appropriately.
225
239
  - REPLACE old style values if they exist, DO NOT duplicate keys!
226
- - If 'innerText' is provided in Changes, update the text content inside the target HTML element!
240
+ - If 'innerText' or 'innerHTML' is provided in Changes, update the text content inside the target HTML element! Use ONLY the exact text provided in the request (preserving internal HTML tags like spans for colors/styles if present). DO NOT add signatures, names, attributions, or "complete" the text based on context.
227
241
  - If 'src' is provided in Changes, update the src attribute of the target HTML element!
242
+ - If a change is marked for a new element (type: create) and you cannot find the target selector, INSERT the provided 'outerHTML' before the closing </body> tag or in an appropriate container.
243
+
228
244
 
229
245
  Example response:
230
246
  <patch>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draply-dev",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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
@@ -435,7 +435,7 @@
435
435
 
436
436
  /* ── PLACED ASSETS ────────────────────────────────── */
437
437
  .ps-asset-placed {
438
- position: fixed;
438
+ position: absolute;
439
439
  z-index: 999990;
440
440
  cursor: grab;
441
441
  user-select: none;
@@ -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;">
@@ -1164,14 +1165,14 @@
1164
1165
  // Make text directly editable
1165
1166
  if (!el.isContentEditable) {
1166
1167
  el.contentEditable = 'true';
1167
- el.dataset.origText = el.innerText || '';
1168
+ el.dataset.origText = el.innerHTML || '';
1168
1169
  el.focus();
1169
1170
 
1170
1171
  const finishEdit = () => {
1171
1172
  el.contentEditable = 'false';
1172
1173
  el.removeEventListener('blur', finishEdit);
1173
- if (el.innerText !== el.dataset.origText) {
1174
- rec(el, { innerText: el.innerText }, { innerText: el.dataset.origText });
1174
+ if (el.innerHTML !== el.dataset.origText) {
1175
+ rec(el, { innerHTML: el.innerHTML }, { innerHTML: el.dataset.origText });
1175
1176
  toast('Text updated!');
1176
1177
  }
1177
1178
  };
@@ -1335,19 +1336,27 @@
1335
1336
  e.preventDefault(); e.stopPropagation();
1336
1337
  if (!pendingAsset || !placingEl) return;
1337
1338
 
1338
- if (e.target.tagName.toLowerCase() === 'img') {
1339
- const prevSrc = e.target.getAttribute('src');
1340
- e.target.src = pendingAsset.src;
1341
- rec(e.target, { src: pendingAsset.src }, { src: prevSrc || '' });
1342
- toast('🖼️ Image replaced!');
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
+ }
1343
1355
  } else {
1344
- const cs = getComputedStyle(e.target);
1345
- const prevBg = cs.backgroundImage;
1346
- e.target.style.backgroundImage = `url('${pendingAsset.src}')`;
1347
- e.target.style.backgroundSize = 'cover';
1348
- e.target.style.backgroundPosition = 'center';
1349
- rec(e.target, { backgroundImage: `url('${pendingAsset.src}')`, backgroundSize: 'cover', backgroundPosition: 'center' }, { backgroundImage: prevBg });
1350
- toast('🖼️ Background image set!');
1356
+ // PLACE mode
1357
+ const x = e.clientX - 60;
1358
+ const y = e.clientY - 60;
1359
+ placeAsset(pendingAsset, x, y, 120, 120);
1351
1360
  }
1352
1361
  cancelPlacing();
1353
1362
  }
@@ -1365,13 +1374,127 @@
1365
1374
  document.removeEventListener('keydown', onPlacingCancel);
1366
1375
  }
1367
1376
 
1377
+ // Place asset permanently on page (absolute positioned)
1378
+ function placeAsset(asset, x, y, w, h) {
1379
+ const wrap = document.createElement('div');
1380
+ wrap.className = 'ps-asset-placed';
1381
+ const uid = 'ps-asset-' + Date.now();
1382
+ wrap.id = uid;
1383
+ wrap.style.cssText = `left:${Math.round(x)}px;top:${Math.round(y)}px;width:${w}px;height:${h}px;z-index:1;`;
1384
+ wrap.innerHTML = `<img src="${asset.src}" draggable="false" alt="${asset.name}">`;
1385
+ document.body.appendChild(wrap);
1386
+
1387
+ // Click to select this placed asset (for z-index control)
1388
+ wrap.addEventListener('click', e => {
1389
+ if (ps(e.target)) return;
1390
+ selectPlaced(wrap);
1391
+ });
1392
+
1393
+ // Make placed asset draggable
1394
+ wrap.addEventListener('mousedown', e => {
1395
+ if (ps(e.target)) return;
1396
+ e.preventDefault();
1397
+ selectPlaced(wrap);
1398
+ astDragEl = wrap;
1399
+ astDragSX = e.clientX; astDragSY = e.clientY;
1400
+ astDragOL = parseFloat(wrap.style.left) || 0;
1401
+ astDragOT = parseFloat(wrap.style.top) || 0;
1402
+ wrap.style.cursor = 'grabbing';
1403
+ state.dragging = true;
1404
+ });
1405
+
1406
+ rec(wrap, {
1407
+ src: asset.src,
1408
+ left: Math.round(x) + 'px',
1409
+ top: Math.round(y) + 'px',
1410
+ width: w + 'px',
1411
+ height: h + 'px',
1412
+ 'z-index': '1',
1413
+ }, null, true); // true = isCreate
1414
+
1415
+ toast('✦ Placed — drag to reposition');
1416
+ selectPlaced(wrap);
1417
+ return wrap;
1418
+ }
1419
+
1420
+ // Track selected placed asset for z-index controls
1421
+ let selectedPlaced = null;
1422
+ const zCtrl = document.getElementById('__ast_zctrl__');
1423
+ const zVal = document.getElementById('__z_val__');
1424
+ const zFront = document.getElementById('__z_front__');
1425
+ const zBack = document.getElementById('__z_back__');
1426
+ const zSet = document.getElementById('__z_set__');
1427
+
1428
+ function selectPlaced(wrap) {
1429
+ // Deselect previous
1430
+ if (selectedPlaced) selectedPlaced.style.outline = '';
1431
+ selectedPlaced = wrap;
1432
+ wrap.style.outline = '2px solid #7fff6e';
1433
+ zVal.value = parseInt(wrap.style.zIndex) || 1;
1434
+ zCtrl.style.display = 'block';
1435
+ }
1436
+
1437
+ zFront.onclick = () => {
1438
+ if (!selectedPlaced) return;
1439
+ // Find max z-index of all placed assets
1440
+ const max = Math.max(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
1441
+ const nz = max + 1;
1442
+ selectedPlaced.style.zIndex = nz;
1443
+ zVal.value = nz;
1444
+ rec(selectedPlaced, { 'z-index': String(nz) });
1445
+ toast('▲ Moved to front (z:' + nz + ')');
1446
+ };
1447
+
1448
+ zBack.onclick = () => {
1449
+ if (!selectedPlaced) return;
1450
+ const min = Math.min(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
1451
+ const nz = min - 1;
1452
+ selectedPlaced.style.zIndex = nz;
1453
+ zVal.value = nz;
1454
+ rec(selectedPlaced, { 'z-index': String(nz) });
1455
+ toast('▼ Moved to back (z:' + nz + ')');
1456
+ };
1457
+
1458
+ zSet.onclick = () => {
1459
+ if (!selectedPlaced) return;
1460
+ const nz = parseInt(zVal.value) || 0;
1461
+ selectedPlaced.style.zIndex = nz;
1462
+ rec(selectedPlaced, { 'z-index': String(nz) });
1463
+ toast('z-index set to ' + nz);
1464
+ };
1465
+
1466
+ zVal.onkeydown = e => { if (e.key === 'Enter') zSet.click(); };
1467
+
1468
+ // Drag placed assets
1469
+ document.addEventListener('mousemove', e => {
1470
+ if (!astDragEl || !state.dragging) return;
1471
+ const dx = e.clientX - astDragSX, dy = e.clientY - astDragSY;
1472
+ astDragEl.style.left = (astDragOL + dx) + 'px';
1473
+ astDragEl.style.top = (astDragOT + dy) + 'px';
1474
+ tip.textContent = `x:${Math.round(astDragOL + dx)} y:${Math.round(astDragOT + dy)}`;
1475
+ tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px';
1476
+ tip.classList.add('v');
1477
+ });
1478
+
1479
+ document.addEventListener('mouseup', () => {
1480
+ if (!astDragEl) return;
1481
+ tip.classList.remove('v');
1482
+ const cs = getComputedStyle(astDragEl);
1483
+ rec(astDragEl, {
1484
+ left: Math.round(parseFloat(cs.left)) + 'px',
1485
+ top: Math.round(parseFloat(cs.top)) + 'px',
1486
+ });
1487
+ astDragEl.style.cursor = 'grab';
1488
+ astDragEl = null;
1489
+ });
1490
+
1368
1491
  // ══════════════════════════════════════════
1369
1492
  // RECORD + SAVE
1370
1493
  // ══════════════════════════════════════════
1371
1494
  // Full history — every individual action
1372
1495
  const history = [];
1373
1496
 
1374
- function rec(el, props, prevPropsOverride) {
1497
+ function rec(el, props, prevPropsOverride, isCreate = false) {
1375
1498
  const selector = el.dataset.pixelshiftId ? null : gsel(el);
1376
1499
  const key = el.dataset.pixelshiftId || selector;
1377
1500
 
@@ -1401,19 +1524,22 @@
1401
1524
 
1402
1525
  // Merge into state.changes (for save/apply)
1403
1526
  const ch = {
1404
- type: el.dataset.pixelshiftId ? 'inline' : 'css',
1527
+ type: isCreate ? 'create' : (el.dataset.pixelshiftId ? 'inline' : 'css'),
1528
+ isCreate,
1405
1529
  pixelshiftId: el.dataset.pixelshiftId || null,
1406
1530
  selector,
1407
1531
  exactFile: getReactSource(el),
1408
1532
  file: el.dataset.pixelshiftFile || null,
1409
- props
1533
+ props,
1534
+ tagName: el.tagName.toLowerCase(),
1535
+ outerHTML: el.outerHTML // Send the whole element for 'create' actions
1410
1536
  };
1411
1537
  const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
1412
1538
  if (i >= 0) Object.assign(state.changes[i].props, props); else state.changes.push(ch);
1413
1539
 
1414
1540
  // Push to history
1415
1541
  const hid = Date.now() + Math.random();
1416
- history.push({ hid, el, props, prevProps, selector: key });
1542
+ history.push({ hid, el, props, prevProps, selector: key, isCreate });
1417
1543
 
1418
1544
  updateUnsUI();
1419
1545
  }
@@ -1442,11 +1568,18 @@
1442
1568
  }
1443
1569
 
1444
1570
  function revertChange(h) {
1445
- // Re-apply previous inline values (empty string = remove inline style → falls back to stylesheet)
1446
- Object.entries(h.prevProps).forEach(([prop, val]) => {
1447
- const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1448
- h.el.style[camel] = val; // '' removes inline override, restoring original
1449
- });
1571
+ if (h.isCreate) {
1572
+ h.el.remove();
1573
+ } else {
1574
+ // Re-apply previous inline values
1575
+ Object.entries(h.prevProps).forEach(([prop, val]) => {
1576
+ const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1577
+ h.el.style[camel] = val;
1578
+ });
1579
+ // Also handle text/html
1580
+ if (h.prevProps.innerHTML !== undefined) h.el.innerHTML = h.prevProps.innerHTML;
1581
+ if (h.prevProps.innerText !== undefined) h.el.innerText = h.prevProps.innerText;
1582
+ }
1450
1583
  // Remove from history
1451
1584
  const idx = history.findIndex(x => x.hid === h.hid);
1452
1585
  if (idx >= 0) history.splice(idx, 1);