draply-dev 1.0.0 → 1.1.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.
@@ -0,0 +1,915 @@
1
+ /**
2
+ * Draply Advanced Features
3
+ * - Visual Diff Panel
4
+ * - Code Generation (Export)
5
+ * - Source Map Integration
6
+ */
7
+ (function () {
8
+ if (window.__draply_features__) return;
9
+ window.__draply_features__ = true;
10
+
11
+ // Wait for overlay to initialize
12
+ const waitForOverlay = setInterval(() => {
13
+ if (!window.__pixelshift__) return;
14
+ clearInterval(waitForOverlay);
15
+ initFeatures();
16
+ }, 100);
17
+
18
+ function initFeatures() {
19
+ // ══════════════════════════════════════════
20
+ // INJECT ADDITIONAL STYLES
21
+ // ══════════════════════════════════════════
22
+ const featureStyles = document.createElement('style');
23
+ featureStyles.textContent = `
24
+ /* ── FEATURE TABS ──────────────────────────────── */
25
+ .ps-feature-tabs {
26
+ display: flex;
27
+ gap: 0;
28
+ margin: 0 -12px;
29
+ padding: 0 12px;
30
+ border-bottom: 1px solid #1e1e3a;
31
+ margin-bottom: 10px;
32
+ }
33
+ .ps-feature-tab {
34
+ flex: 1;
35
+ background: none;
36
+ border: none;
37
+ color: #555577;
38
+ font-family: 'Space Mono', monospace;
39
+ font-size: 9px;
40
+ padding: 8px 4px;
41
+ cursor: pointer;
42
+ border-bottom: 2px solid transparent;
43
+ transition: all .2s;
44
+ text-transform: uppercase;
45
+ letter-spacing: 0.5px;
46
+ }
47
+ .ps-feature-tab:hover { color: #aaaacc; }
48
+ .ps-feature-tab.active {
49
+ color: #7fff6e;
50
+ border-bottom-color: #7fff6e;
51
+ }
52
+
53
+ /* ── DIFF PANEL ────────────────────────────────── */
54
+ #__ps_diff_panel__ {
55
+ display: none;
56
+ flex-direction: column;
57
+ gap: 8px;
58
+ padding: 8px 0;
59
+ }
60
+ #__ps_diff_panel__.v { display: flex; }
61
+
62
+ .ps-diff-empty {
63
+ color: #555577;
64
+ font-size: 10px;
65
+ text-align: center;
66
+ padding: 20px 0;
67
+ }
68
+
69
+ .ps-diff-item {
70
+ background: #0d0d1a;
71
+ border: 1px solid #1e1e3a;
72
+ border-radius: 6px;
73
+ padding: 8px 10px;
74
+ font-size: 9px;
75
+ }
76
+ .ps-diff-item:hover {
77
+ border-color: #2a2a5a;
78
+ }
79
+ .ps-diff-selector {
80
+ color: #7fff6e;
81
+ font-weight: 600;
82
+ margin-bottom: 6px;
83
+ white-space: nowrap;
84
+ overflow: hidden;
85
+ text-overflow: ellipsis;
86
+ }
87
+ .ps-diff-row {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 6px;
91
+ margin: 3px 0;
92
+ font-size: 9px;
93
+ }
94
+ .ps-diff-prop {
95
+ color: #8888aa;
96
+ min-width: 80px;
97
+ flex-shrink: 0;
98
+ }
99
+ .ps-diff-old {
100
+ color: #ff6b6b;
101
+ text-decoration: line-through;
102
+ opacity: 0.7;
103
+ flex: 1;
104
+ overflow: hidden;
105
+ text-overflow: ellipsis;
106
+ white-space: nowrap;
107
+ }
108
+ .ps-diff-arrow {
109
+ color: #555577;
110
+ flex-shrink: 0;
111
+ }
112
+ .ps-diff-new {
113
+ color: #7fff6e;
114
+ flex: 1;
115
+ overflow: hidden;
116
+ text-overflow: ellipsis;
117
+ white-space: nowrap;
118
+ }
119
+
120
+ .ps-diff-actions {
121
+ display: flex;
122
+ gap: 6px;
123
+ margin-top: 6px;
124
+ }
125
+ .ps-diff-btn {
126
+ background: none;
127
+ border: 1px solid #2a2a44;
128
+ color: #8888aa;
129
+ border-radius: 4px;
130
+ padding: 3px 8px;
131
+ font-size: 8px;
132
+ cursor: pointer;
133
+ font-family: 'Space Mono', monospace;
134
+ transition: all .15s;
135
+ }
136
+ .ps-diff-btn:hover {
137
+ border-color: #7fff6e;
138
+ color: #7fff6e;
139
+ }
140
+ .ps-diff-btn.revert:hover {
141
+ border-color: #ff6b6b;
142
+ color: #ff6b6b;
143
+ }
144
+
145
+ /* ── SPLIT VIEW OVERLAY ───────────────────────── */
146
+ #__ps_splitview__ {
147
+ display: none;
148
+ position: fixed;
149
+ top: 0; left: 0;
150
+ width: 100%; height: 100%;
151
+ z-index: 999998;
152
+ pointer-events: none;
153
+ }
154
+ #__ps_splitview__.v { display: block; }
155
+ #__ps_splitview__::after {
156
+ content: '';
157
+ position: absolute;
158
+ top: 0;
159
+ left: 50%;
160
+ width: 3px;
161
+ height: 100%;
162
+ background: #7fff6e;
163
+ transform: translateX(-50%);
164
+ box-shadow: 0 0 12px rgba(127,255,110,0.4);
165
+ }
166
+ .ps-split-label {
167
+ position: absolute;
168
+ top: 8px;
169
+ padding: 4px 12px;
170
+ background: rgba(10,10,26,0.9);
171
+ border: 1px solid #1e1e3a;
172
+ border-radius: 4px;
173
+ font-family: 'Space Mono', monospace;
174
+ font-size: 10px;
175
+ color: #aaaacc;
176
+ pointer-events: none;
177
+ }
178
+ .ps-split-label.before { left: 12px; }
179
+ .ps-split-label.after { right: 12px; }
180
+
181
+ /* ── EXPORT PANEL ──────────────────────────────── */
182
+ #__ps_export_panel__ {
183
+ display: none;
184
+ flex-direction: column;
185
+ gap: 8px;
186
+ padding: 8px 0;
187
+ }
188
+ #__ps_export_panel__.v { display: flex; }
189
+
190
+ .ps-export-format-tabs {
191
+ display: flex;
192
+ flex-wrap: wrap;
193
+ gap: 4px;
194
+ margin-bottom: 6px;
195
+ }
196
+ .ps-export-format-btn {
197
+ background: #0d0d1a;
198
+ border: 1px solid #1e1e3a;
199
+ color: #8888aa;
200
+ border-radius: 4px;
201
+ padding: 4px 8px;
202
+ font-size: 8px;
203
+ cursor: pointer;
204
+ font-family: 'Space Mono', monospace;
205
+ transition: all .15s;
206
+ }
207
+ .ps-export-format-btn:hover { border-color: #555577; }
208
+ .ps-export-format-btn.active {
209
+ border-color: #7fff6e;
210
+ color: #7fff6e;
211
+ background: rgba(127,255,110,0.05);
212
+ }
213
+
214
+ .ps-export-code {
215
+ background: #0d0d1a;
216
+ border: 1px solid #1e1e3a;
217
+ border-radius: 6px;
218
+ padding: 10px;
219
+ font-family: 'Space Mono', monospace;
220
+ font-size: 9px;
221
+ color: #ccccee;
222
+ white-space: pre-wrap;
223
+ word-break: break-all;
224
+ max-height: 300px;
225
+ overflow-y: auto;
226
+ line-height: 1.6;
227
+ position: relative;
228
+ }
229
+ .ps-export-code::-webkit-scrollbar { width: 4px; }
230
+ .ps-export-code::-webkit-scrollbar-track { background: transparent; }
231
+ .ps-export-code::-webkit-scrollbar-thumb { background: #2a2a44; border-radius: 2px; }
232
+
233
+ .ps-export-copy {
234
+ background: linear-gradient(135deg, #7fff6e22, #7fff6e11);
235
+ border: 1px solid #7fff6e44;
236
+ color: #7fff6e;
237
+ border-radius: 5px;
238
+ padding: 6px 16px;
239
+ font-size: 10px;
240
+ cursor: pointer;
241
+ font-family: 'Space Mono', monospace;
242
+ transition: all .15s;
243
+ text-align: center;
244
+ margin-top: 4px;
245
+ }
246
+ .ps-export-copy:hover {
247
+ background: linear-gradient(135deg, #7fff6e33, #7fff6e22);
248
+ border-color: #7fff6e;
249
+ }
250
+
251
+ .ps-export-empty {
252
+ color: #555577;
253
+ font-size: 10px;
254
+ text-align: center;
255
+ padding: 20px 0;
256
+ }
257
+
258
+ /* ── SOURCE PANEL ──────────────────────────────── */
259
+ #__ps_source_panel__ {
260
+ display: none;
261
+ flex-direction: column;
262
+ gap: 8px;
263
+ padding: 8px 0;
264
+ }
265
+ #__ps_source_panel__.v { display: flex; }
266
+
267
+ .ps-source-info {
268
+ background: #0d0d1a;
269
+ border: 1px solid #1e1e3a;
270
+ border-radius: 6px;
271
+ padding: 10px;
272
+ font-size: 9px;
273
+ }
274
+ .ps-source-label {
275
+ color: #555577;
276
+ font-size: 8px;
277
+ text-transform: uppercase;
278
+ letter-spacing: 0.5px;
279
+ margin-bottom: 4px;
280
+ }
281
+ .ps-source-value {
282
+ color: #ccccee;
283
+ font-size: 10px;
284
+ word-break: break-all;
285
+ }
286
+ .ps-source-value.highlight {
287
+ color: #7fff6e;
288
+ }
289
+ .ps-source-hint {
290
+ background: rgba(127,255,110,0.05);
291
+ border: 1px solid #7fff6e33;
292
+ border-radius: 6px;
293
+ padding: 8px 10px;
294
+ font-size: 9px;
295
+ color: #aaddaa;
296
+ line-height: 1.5;
297
+ }
298
+ .ps-source-hint-icon {
299
+ color: #7fff6e;
300
+ margin-right: 4px;
301
+ }
302
+ .ps-source-loading {
303
+ color: #555577;
304
+ font-size: 10px;
305
+ text-align: center;
306
+ padding: 20px 0;
307
+ }
308
+ .ps-source-empty {
309
+ color: #555577;
310
+ font-size: 10px;
311
+ text-align: center;
312
+ padding: 20px 0;
313
+ }
314
+
315
+ /* ── FEATURE PANEL CONTAINER ───────────────────── */
316
+ #__ps_features__ {
317
+ display: none;
318
+ flex-direction: column;
319
+ padding: 0 12px 12px;
320
+ }
321
+ #__ps_features__.v { display: flex; }
322
+ `;
323
+ document.head.appendChild(featureStyles);
324
+
325
+ // ══════════════════════════════════════════
326
+ // FIND SIDEBAR & INJECT UI
327
+ // ══════════════════════════════════════════
328
+ const sidebar = document.getElementById('__ps_sidebar__');
329
+ if (!sidebar) return;
330
+
331
+ // Create feature panel container
332
+ const featuresDiv = document.createElement('div');
333
+ featuresDiv.id = '__ps_features__';
334
+ featuresDiv.innerHTML = `
335
+ <div class="ps-feature-tabs">
336
+ <button class="ps-feature-tab active" data-ftab="diff">📊 Diff</button>
337
+ <button class="ps-feature-tab" data-ftab="export">📦 Export</button>
338
+ <button class="ps-feature-tab" data-ftab="source">🔍 Source</button>
339
+ </div>
340
+
341
+ <!-- DIFF PANEL -->
342
+ <div id="__ps_diff_panel__" class="v">
343
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
344
+ <span style="color:#8888aa;font-size:9px;">Changes</span>
345
+ <button id="__ps_split_toggle__" class="ps-diff-btn" style="font-size:8px;">⊞ Split View</button>
346
+ </div>
347
+ <div id="__ps_diff_list__">
348
+ <div class="ps-diff-empty">No changes yet. Edit elements to see diffs.</div>
349
+ </div>
350
+ </div>
351
+
352
+ <!-- EXPORT PANEL -->
353
+ <div id="__ps_export_panel__">
354
+ <div class="ps-export-format-tabs">
355
+ <button class="ps-export-format-btn active" data-format="css">CSS</button>
356
+ <button class="ps-export-format-btn" data-format="jsx">JSX Inline</button>
357
+ <button class="ps-export-format-btn" data-format="tailwind">Tailwind</button>
358
+ <button class="ps-export-format-btn" data-format="modules">CSS Modules</button>
359
+ <button class="ps-export-format-btn" data-format="styled">styled-components</button>
360
+ </div>
361
+ <div id="__ps_export_code__" class="ps-export-code">
362
+ <span class="ps-export-empty">No changes to export.</span>
363
+ </div>
364
+ <button id="__ps_export_copy__" class="ps-export-copy">📋 Copy to Clipboard</button>
365
+ </div>
366
+
367
+ <!-- SOURCE PANEL -->
368
+ <div id="__ps_source_panel__">
369
+ <div id="__ps_source_project__" class="ps-source-info">
370
+ <div class="ps-source-label">Project Framework</div>
371
+ <div class="ps-source-value ps-source-loading">Detecting...</div>
372
+ </div>
373
+ <div id="__ps_source_element__" class="ps-source-info" style="display:none;">
374
+ <div class="ps-source-label">Selected Element</div>
375
+ <div id="__ps_source_el_info__" class="ps-source-value"></div>
376
+ </div>
377
+ <div id="__ps_source_file__" class="ps-source-info" style="display:none;">
378
+ <div class="ps-source-label">Source File</div>
379
+ <div id="__ps_source_file_info__" class="ps-source-value highlight"></div>
380
+ </div>
381
+ <div id="__ps_source_hint__" class="ps-source-hint" style="display:none;">
382
+ <span class="ps-source-hint-icon">💡</span>
383
+ <span id="__ps_source_hint_text__"></span>
384
+ </div>
385
+ </div>
386
+ `;
387
+
388
+ // Split view overlay
389
+ const splitView = document.createElement('div');
390
+ splitView.id = '__ps_splitview__';
391
+ splitView.innerHTML = `
392
+ <div class="ps-split-label before">BEFORE</div>
393
+ <div class="ps-split-label after">AFTER</div>
394
+ `;
395
+ document.body.appendChild(splitView);
396
+
397
+ // Insert features panel into sidebar (before unsaved section)
398
+ const unsavedSection = sidebar.querySelector('[id*="uns"]') || sidebar.lastElementChild;
399
+ if (unsavedSection) {
400
+ sidebar.insertBefore(featuresDiv, unsavedSection);
401
+ } else {
402
+ sidebar.appendChild(featuresDiv);
403
+ }
404
+
405
+ // ══════════════════════════════════════════
406
+ // FEATURE TABS
407
+ // ══════════════════════════════════════════
408
+ const ftabs = featuresDiv.querySelectorAll('.ps-feature-tab');
409
+ const diffPanel = document.getElementById('__ps_diff_panel__');
410
+ const exportPanel = document.getElementById('__ps_export_panel__');
411
+ const sourcePanel = document.getElementById('__ps_source_panel__');
412
+
413
+ ftabs.forEach(tab => {
414
+ tab.addEventListener('click', () => {
415
+ ftabs.forEach(t => t.classList.remove('active'));
416
+ tab.classList.add('active');
417
+ const which = tab.dataset.ftab;
418
+ diffPanel.classList.toggle('v', which === 'diff');
419
+ exportPanel.classList.toggle('v', which === 'export');
420
+ sourcePanel.classList.toggle('v', which === 'source');
421
+ if (which === 'diff') refreshDiffPanel();
422
+ if (which === 'export') refreshExportPanel();
423
+ if (which === 'source') refreshSourcePanel();
424
+ });
425
+ });
426
+
427
+ // Show features panel when sidebar opens
428
+ const observer = new MutationObserver(() => {
429
+ const sidebarVisible = sidebar.classList.contains('open') || sidebar.style.display !== 'none';
430
+ if (sidebarVisible) {
431
+ featuresDiv.classList.add('v');
432
+ } else {
433
+ featuresDiv.classList.remove('v');
434
+ }
435
+ });
436
+ observer.observe(sidebar, { attributes: true, attributeFilter: ['class', 'style'] });
437
+ // Also check periodically
438
+ setInterval(() => {
439
+ const sidebarEl = document.getElementById('__ps_sidebar__');
440
+ if (sidebarEl) {
441
+ const isVis = sidebarEl.offsetWidth > 0;
442
+ featuresDiv.classList.toggle('v', isVis);
443
+ }
444
+ }, 500);
445
+
446
+ // ══════════════════════════════════════════
447
+ // VISUAL DIFF
448
+ // ══════════════════════════════════════════
449
+ const diffList = document.getElementById('__ps_diff_list__');
450
+
451
+ // Hook into history changes by polling
452
+ let lastHistoryLength = 0;
453
+ setInterval(() => {
454
+ // Access overlay's history via DOM observation
455
+ const historyRows = document.querySelectorAll('#__ps__ [data-hid]');
456
+ if (historyRows.length !== lastHistoryLength) {
457
+ lastHistoryLength = historyRows.length;
458
+ refreshDiffPanel();
459
+ }
460
+ }, 300);
461
+
462
+ function refreshDiffPanel() {
463
+ // Read changes from the unsaved list entries
464
+ const historyRows = document.querySelectorAll('#__ps__ [data-hid]');
465
+
466
+ if (historyRows.length === 0) {
467
+ diffList.innerHTML = '<div class="ps-diff-empty">No changes yet. Edit elements to see diffs.</div>';
468
+ return;
469
+ }
470
+
471
+ // Collect change data from the DOM
472
+ const changes = [];
473
+ historyRows.forEach(btn => {
474
+ const row = btn.closest('div[style]');
475
+ if (!row) return;
476
+ const selectorDiv = row.querySelector('div[style*="color:#7fff6e"]');
477
+ const propsDiv = row.querySelector('div[style*="color:#555577"]');
478
+ if (selectorDiv && propsDiv) {
479
+ changes.push({
480
+ hid: btn.dataset.hid,
481
+ selector: selectorDiv.textContent,
482
+ propsStr: propsDiv.textContent
483
+ });
484
+ }
485
+ });
486
+
487
+ diffList.innerHTML = '';
488
+ changes.forEach(ch => {
489
+ const item = document.createElement('div');
490
+ item.className = 'ps-diff-item';
491
+
492
+ // Parse props string
493
+ const propPairs = ch.propsStr.split(',').map(p => p.trim()).filter(Boolean);
494
+ let rowsHtml = '';
495
+ propPairs.forEach(pair => {
496
+ const colonIdx = pair.indexOf(':');
497
+ if (colonIdx < 0) return;
498
+ const prop = pair.substring(0, colonIdx).trim();
499
+ const val = pair.substring(colonIdx + 1).trim();
500
+ rowsHtml += `
501
+ <div class="ps-diff-row">
502
+ <span class="ps-diff-prop">${prop}</span>
503
+ <span class="ps-diff-arrow">→</span>
504
+ <span class="ps-diff-new">${val}</span>
505
+ </div>
506
+ `;
507
+ });
508
+
509
+ item.innerHTML = `
510
+ <div class="ps-diff-selector">${ch.selector}</div>
511
+ ${rowsHtml}
512
+ <div class="ps-diff-actions">
513
+ <button class="ps-diff-btn revert" data-hid="${ch.hid}">↩ Revert</button>
514
+ </div>
515
+ `;
516
+
517
+ // Revert button
518
+ item.querySelector('.revert').addEventListener('click', () => {
519
+ const origBtn = document.querySelector(`#__ps__ [data-hid="${ch.hid}"]`);
520
+ if (origBtn) origBtn.click();
521
+ setTimeout(refreshDiffPanel, 100);
522
+ });
523
+
524
+ diffList.appendChild(item);
525
+ });
526
+ }
527
+
528
+ // Split view toggle
529
+ const splitToggle = document.getElementById('__ps_split_toggle__');
530
+ let splitActive = false;
531
+ splitToggle.addEventListener('click', () => {
532
+ splitActive = !splitActive;
533
+ splitView.classList.toggle('v', splitActive);
534
+ splitToggle.textContent = splitActive ? '⊟ Close Split' : '⊞ Split View';
535
+ splitToggle.style.borderColor = splitActive ? '#7fff6e' : '';
536
+ splitToggle.style.color = splitActive ? '#7fff6e' : '';
537
+ });
538
+
539
+ // ══════════════════════════════════════════
540
+ // CODE GENERATION
541
+ // ══════════════════════════════════════════
542
+ const exportCode = document.getElementById('__ps_export_code__');
543
+ const exportCopy = document.getElementById('__ps_export_copy__');
544
+ const formatBtns = featuresDiv.querySelectorAll('.ps-export-format-btn');
545
+ let currentFormat = 'css';
546
+
547
+ // CSS to Tailwind mapping (basic properties)
548
+ const cssToTailwind = {
549
+ 'background-color': (v) => {
550
+ if (v === 'transparent') return 'bg-transparent';
551
+ return `bg-[${v}]`;
552
+ },
553
+ 'color': (v) => `text-[${v}]`,
554
+ 'font-size': (v) => {
555
+ const px = parseInt(v);
556
+ const map = { 12: 'text-xs', 14: 'text-sm', 16: 'text-base', 18: 'text-lg', 20: 'text-xl', 24: 'text-2xl', 30: 'text-3xl', 36: 'text-4xl' };
557
+ return map[px] || `text-[${v}]`;
558
+ },
559
+ 'font-weight': (v) => {
560
+ const map = { '400': 'font-normal', '500': 'font-medium', '600': 'font-semibold', '700': 'font-bold' };
561
+ return map[v] || `font-[${v}]`;
562
+ },
563
+ 'font-style': (v) => v === 'italic' ? 'italic' : 'not-italic',
564
+ 'text-decoration': (v) => {
565
+ if (v.includes('underline')) return 'underline';
566
+ if (v.includes('line-through')) return 'line-through';
567
+ return 'no-underline';
568
+ },
569
+ 'text-transform': (v) => {
570
+ const map = { 'uppercase': 'uppercase', 'lowercase': 'lowercase', 'capitalize': 'capitalize', 'none': 'normal-case' };
571
+ return map[v] || '';
572
+ },
573
+ 'line-height': (v) => `leading-[${v}]`,
574
+ 'letter-spacing': (v) => `tracking-[${v}]`,
575
+ 'width': (v) => `w-[${v}]`,
576
+ 'height': (v) => `h-[${v}]`,
577
+ 'left': (v) => `left-[${v}]`,
578
+ 'top': (v) => `top-[${v}]`,
579
+ 'border': (v) => {
580
+ if (v === 'none') return 'border-0';
581
+ return `border-[${v}]`;
582
+ },
583
+ 'border-color': (v) => `border-[${v}]`,
584
+ 'z-index': (v) => `z-[${v}]`,
585
+ };
586
+
587
+ function camelCase(str) {
588
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
589
+ }
590
+
591
+ formatBtns.forEach(btn => {
592
+ btn.addEventListener('click', () => {
593
+ formatBtns.forEach(b => b.classList.remove('active'));
594
+ btn.classList.add('active');
595
+ currentFormat = btn.dataset.format;
596
+ refreshExportPanel();
597
+ });
598
+ });
599
+
600
+ function getChangesData() {
601
+ // Collect changes from history rows
602
+ const historyRows = document.querySelectorAll('#__ps__ [data-hid]');
603
+ const changesMap = new Map();
604
+
605
+ historyRows.forEach(btn => {
606
+ const row = btn.closest('div[style]');
607
+ if (!row) return;
608
+ const selectorDiv = row.querySelector('div[style*="color:#7fff6e"]');
609
+ const propsDiv = row.querySelector('div[style*="color:#555577"]');
610
+ if (!selectorDiv || !propsDiv) return;
611
+
612
+ const selector = selectorDiv.textContent.trim();
613
+ const propsStr = propsDiv.textContent.trim();
614
+
615
+ if (!changesMap.has(selector)) {
616
+ changesMap.set(selector, {});
617
+ }
618
+
619
+ const props = changesMap.get(selector);
620
+ propsStr.split(',').forEach(pair => {
621
+ const colonIdx = pair.indexOf(':');
622
+ if (colonIdx < 0) return;
623
+ const prop = pair.substring(0, colonIdx).trim();
624
+ const val = pair.substring(colonIdx + 1).trim();
625
+ props[prop] = val;
626
+ });
627
+ });
628
+
629
+ return changesMap;
630
+ }
631
+
632
+ function generateCSS(changesMap) {
633
+ if (changesMap.size === 0) return '/* No changes to export */';
634
+ let output = '/* Generated by Draply */\n\n';
635
+ changesMap.forEach((props, selector) => {
636
+ output += `${selector} {\n`;
637
+ Object.entries(props).forEach(([prop, val]) => {
638
+ output += ` ${prop}: ${val};\n`;
639
+ });
640
+ output += '}\n\n';
641
+ });
642
+ return output.trim();
643
+ }
644
+
645
+ function generateJSX(changesMap) {
646
+ if (changesMap.size === 0) return '// No changes to export';
647
+ let output = '// Generated by Draply — JSX Inline Styles\n\n';
648
+ changesMap.forEach((props, selector) => {
649
+ output += `// ${selector}\n`;
650
+ output += 'const style = {\n';
651
+ Object.entries(props).forEach(([prop, val]) => {
652
+ const camel = camelCase(prop);
653
+ // Numbers for certain props
654
+ if (['fontSize', 'lineHeight', 'letterSpacing', 'width', 'height', 'left', 'top', 'zIndex'].includes(camel) && val.endsWith('px')) {
655
+ if (camel === 'zIndex') {
656
+ output += ` ${camel}: ${parseInt(val)},\n`;
657
+ } else {
658
+ output += ` ${camel}: '${val}',\n`;
659
+ }
660
+ } else {
661
+ output += ` ${camel}: '${val}',\n`;
662
+ }
663
+ });
664
+ output += '};\n\n';
665
+ });
666
+ return output.trim();
667
+ }
668
+
669
+ function generateTailwind(changesMap) {
670
+ if (changesMap.size === 0) return '<!-- No changes to export -->';
671
+ let output = '<!-- Generated by Draply — Tailwind Classes -->\n\n';
672
+ changesMap.forEach((props, selector) => {
673
+ const classes = [];
674
+ Object.entries(props).forEach(([prop, val]) => {
675
+ const mapper = cssToTailwind[prop];
676
+ if (mapper) {
677
+ const cls = mapper(val);
678
+ if (cls) classes.push(cls);
679
+ } else {
680
+ classes.push(`[${prop}:${val}]`);
681
+ }
682
+ });
683
+ output += `/* ${selector} */\n`;
684
+ output += `class="${classes.join(' ')}"\n\n`;
685
+ });
686
+ return output.trim();
687
+ }
688
+
689
+ function generateModules(changesMap) {
690
+ if (changesMap.size === 0) return '/* No changes to export */';
691
+ let output = '/* Generated by Draply — CSS Modules */\n\n';
692
+ let counter = 0;
693
+ changesMap.forEach((props, selector) => {
694
+ const className = selector.replace(/[^a-zA-Z0-9]/g, '').substring(0, 20) || `element${counter++}`;
695
+ output += `.${className} {\n`;
696
+ Object.entries(props).forEach(([prop, val]) => {
697
+ output += ` ${prop}: ${val};\n`;
698
+ });
699
+ output += '}\n\n';
700
+ output += `/* Usage: import styles from './module.module.css'; */\n`;
701
+ output += `/* <div className={styles.${className}}> */\n\n`;
702
+ });
703
+ return output.trim();
704
+ }
705
+
706
+ function generateStyled(changesMap) {
707
+ if (changesMap.size === 0) return '// No changes to export';
708
+ let output = "// Generated by Draply — styled-components\nimport styled from 'styled-components';\n\n";
709
+ let counter = 0;
710
+ changesMap.forEach((props, selector) => {
711
+ const name = 'Styled' + (selector.replace(/[^a-zA-Z0-9]/g, '').substring(0, 15) || `Element${counter++}`);
712
+ output += `const ${name} = styled.div\`\n`;
713
+ Object.entries(props).forEach(([prop, val]) => {
714
+ output += ` ${prop}: ${val};\n`;
715
+ });
716
+ output += '`;\n\n';
717
+ });
718
+ return output.trim();
719
+ }
720
+
721
+ function refreshExportPanel() {
722
+ const changesMap = getChangesData();
723
+
724
+ let code = '';
725
+ switch (currentFormat) {
726
+ case 'css': code = generateCSS(changesMap); break;
727
+ case 'jsx': code = generateJSX(changesMap); break;
728
+ case 'tailwind': code = generateTailwind(changesMap); break;
729
+ case 'modules': code = generateModules(changesMap); break;
730
+ case 'styled': code = generateStyled(changesMap); break;
731
+ }
732
+
733
+ exportCode.textContent = code;
734
+ }
735
+
736
+ exportCopy.addEventListener('click', async () => {
737
+ try {
738
+ await navigator.clipboard.writeText(exportCode.textContent);
739
+ showToast('📋 Copied to clipboard!');
740
+ exportCopy.textContent = '✓ Copied!';
741
+ setTimeout(() => { exportCopy.textContent = '📋 Copy to Clipboard'; }, 1500);
742
+ } catch {
743
+ // Fallback
744
+ const ta = document.createElement('textarea');
745
+ ta.value = exportCode.textContent;
746
+ document.body.appendChild(ta);
747
+ ta.select();
748
+ document.execCommand('copy');
749
+ document.body.removeChild(ta);
750
+ showToast('📋 Copied to clipboard!');
751
+ exportCopy.textContent = '✓ Copied!';
752
+ setTimeout(() => { exportCopy.textContent = '📋 Copy to Clipboard'; }, 1500);
753
+ }
754
+ });
755
+
756
+ // ══════════════════════════════════════════
757
+ // SOURCE MAP INTEGRATION
758
+ // ══════════════════════════════════════════
759
+ let projectInfo = null;
760
+
761
+ async function detectProject() {
762
+ try {
763
+ const resp = await fetch('/draply-project-info');
764
+ if (resp.ok) {
765
+ projectInfo = await resp.json();
766
+ updateProjectUI();
767
+ }
768
+ } catch {
769
+ projectInfo = { framework: 'unknown', cssStrategy: 'external' };
770
+ updateProjectUI();
771
+ }
772
+ }
773
+
774
+ function updateProjectUI() {
775
+ const projEl = document.querySelector('#__ps_source_project__ .ps-source-value');
776
+ if (!projEl || !projectInfo) return;
777
+
778
+ const fwNames = {
779
+ 'react': '⚛️ React',
780
+ 'next': '▲ Next.js',
781
+ 'vue': '💚 Vue.js',
782
+ 'nuxt': '💚 Nuxt',
783
+ 'angular': '🅰️ Angular',
784
+ 'svelte': '🔶 Svelte',
785
+ 'vite': '⚡ Vite',
786
+ 'unknown': '📁 Static/Unknown'
787
+ };
788
+
789
+ const cssNames = {
790
+ 'tailwind': 'Tailwind CSS',
791
+ 'css-modules': 'CSS Modules',
792
+ 'styled-components': 'styled-components',
793
+ 'emotion': 'Emotion',
794
+ 'sass': 'SASS/SCSS',
795
+ 'external': 'External CSS',
796
+ 'unknown': 'Unknown'
797
+ };
798
+
799
+ projEl.classList.remove('ps-source-loading');
800
+ projEl.classList.add('highlight');
801
+ projEl.textContent = `${fwNames[projectInfo.framework] || projectInfo.framework}`;
802
+
803
+ if (projectInfo.cssStrategy && projectInfo.cssStrategy !== 'unknown') {
804
+ projEl.textContent += ` + ${cssNames[projectInfo.cssStrategy] || projectInfo.cssStrategy}`;
805
+ }
806
+ }
807
+
808
+ function refreshSourcePanel() {
809
+ if (!projectInfo) detectProject();
810
+
811
+ // Check for selected element
812
+ const selectedEl = document.querySelector('.__ps__');
813
+ const elInfo = document.getElementById('__ps_source_el_info__');
814
+ const elSection = document.getElementById('__ps_source_element__');
815
+ const fileSection = document.getElementById('__ps_source_file__');
816
+ const fileInfo = document.getElementById('__ps_source_file_info__');
817
+ const hintSection = document.getElementById('__ps_source_hint__');
818
+ const hintText = document.getElementById('__ps_source_hint_text__');
819
+
820
+ if (!selectedEl) {
821
+ if (elSection) elSection.style.display = 'none';
822
+ if (fileSection) fileSection.style.display = 'none';
823
+ if (hintSection) hintSection.style.display = 'none';
824
+ return;
825
+ }
826
+
827
+ if (elSection) {
828
+ elSection.style.display = '';
829
+ const tag = selectedEl.tagName.toLowerCase();
830
+ const cls = [...selectedEl.classList].filter(c => !c.startsWith('__')).join('.');
831
+ const id = selectedEl.id ? `#${selectedEl.id}` : '';
832
+ elInfo.textContent = `<${tag}${id}${cls ? '.' + cls : ''}>`;
833
+ }
834
+
835
+ // Try to find source via data attributes
836
+ const sourceFile = selectedEl.dataset?.source ||
837
+ selectedEl.getAttribute('data-source') ||
838
+ findReactSource(selectedEl);
839
+
840
+ if (sourceFile && fileSection) {
841
+ fileSection.style.display = '';
842
+ fileInfo.textContent = sourceFile;
843
+ } else if (fileSection) {
844
+ fileSection.style.display = 'none';
845
+ }
846
+
847
+ // Generate insertion hint
848
+ if (hintSection && hintText && projectInfo) {
849
+ hintSection.style.display = '';
850
+ const tag = selectedEl.tagName.toLowerCase();
851
+ const cls = [...selectedEl.classList].filter(c => !c.startsWith('__'))[0];
852
+
853
+ if (projectInfo.framework === 'react' || projectInfo.framework === 'next') {
854
+ if (projectInfo.cssStrategy === 'tailwind') {
855
+ hintText.textContent = `Add Tailwind classes directly to the ${cls ? '.' + cls : tag} JSX element's className prop.`;
856
+ } else if (projectInfo.cssStrategy === 'css-modules') {
857
+ hintText.textContent = `Add styles to the corresponding .module.css file for ${cls ? '.' + cls : tag}.`;
858
+ } else if (projectInfo.cssStrategy === 'styled-components') {
859
+ hintText.textContent = `Modify the styled() definition for ${cls ? '.' + cls : tag} in the component file.`;
860
+ } else {
861
+ hintText.textContent = `Add the CSS rules for ${cls ? '.' + cls : tag} to your stylesheet or use inline styles in JSX.`;
862
+ }
863
+ } else if (projectInfo.framework === 'vue' || projectInfo.framework === 'nuxt') {
864
+ hintText.textContent = `Add styles in the <style scoped> section of the ${cls ? '.' + cls : tag} component.`;
865
+ } else {
866
+ hintText.textContent = `Add the generated CSS to your stylesheet targeting ${cls ? '.' + cls : tag}.`;
867
+ }
868
+ }
869
+ }
870
+
871
+ function findReactSource(el) {
872
+ // Try to find React fiber for source info
873
+ const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'));
874
+ if (!fiberKey) return null;
875
+
876
+ try {
877
+ let fiber = el[fiberKey];
878
+ while (fiber) {
879
+ if (fiber._debugSource) {
880
+ return `${fiber._debugSource.fileName}:${fiber._debugSource.lineNumber}`;
881
+ }
882
+ if (fiber._debugOwner && fiber._debugOwner._debugSource) {
883
+ return `${fiber._debugOwner._debugSource.fileName}:${fiber._debugOwner._debugSource.lineNumber}`;
884
+ }
885
+ fiber = fiber.return;
886
+ }
887
+ } catch { /* ignore */ }
888
+ return null;
889
+ }
890
+
891
+ // ══════════════════════════════════════════
892
+ // UTILS
893
+ // ══════════════════════════════════════════
894
+ function showToast(msg) {
895
+ const tst = document.getElementById('__tst__');
896
+ if (tst) {
897
+ tst.textContent = msg;
898
+ tst.classList.add('v');
899
+ clearTimeout(tst._t);
900
+ tst._t = setTimeout(() => tst.classList.remove('v'), 2800);
901
+ }
902
+ }
903
+
904
+ // Auto-detect project on load
905
+ setTimeout(detectProject, 500);
906
+
907
+ // Listen for element selection changes
908
+ setInterval(() => {
909
+ const activeTab = featuresDiv.querySelector('.ps-feature-tab.active');
910
+ if (activeTab && activeTab.dataset.ftab === 'source') {
911
+ refreshSourcePanel();
912
+ }
913
+ }, 1000);
914
+ }
915
+ })();