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.
- package/bin/cli.js +178 -134
- package/package.json +20 -5
- package/src/draply-features.js +915 -0
- package/src/overlay.js +912 -903
|
@@ -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
|
+
})();
|