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