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.
- package/bin/cli.js +226 -485
- package/package.json +1 -1
- package/src/draply-features.js +435 -741
- package/src/overlay.js +3 -15
package/src/draply-features.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Draply
|
|
3
|
-
* -
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
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
|
-
//
|
|
19
|
+
// STYLES
|
|
21
20
|
// ══════════════════════════════════════════
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
/* ── FEATURE TABS
|
|
25
|
-
.ps-
|
|
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:
|
|
28
|
+
margin-bottom: 8px;
|
|
32
29
|
}
|
|
33
|
-
.ps-
|
|
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:
|
|
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-
|
|
48
|
-
.ps-
|
|
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
|
-
/* ──
|
|
54
|
-
#
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
126
|
-
|
|
59
|
+
/* ── DIFF PANEL ──────────────────────────── */
|
|
60
|
+
.ps-dp {
|
|
61
|
+
display: none;
|
|
62
|
+
flex-direction: column;
|
|
127
63
|
gap: 6px;
|
|
128
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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:
|
|
178
|
-
|
|
179
|
-
font-size:
|
|
180
|
-
color: #aaaacc;
|
|
181
|
-
pointer-events: none;
|
|
75
|
+
border-radius: 5px;
|
|
76
|
+
padding: 6px 8px;
|
|
77
|
+
font-size: 9px;
|
|
182
78
|
}
|
|
183
|
-
.ps-
|
|
184
|
-
.ps-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
#
|
|
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:
|
|
191
|
-
|
|
192
|
-
max-height: 250px;
|
|
97
|
+
gap: 6px;
|
|
98
|
+
max-height: 200px;
|
|
193
99
|
overflow-y: auto;
|
|
194
100
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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-
|
|
225
|
-
background: #
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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-
|
|
240
|
-
.ps-
|
|
241
|
-
.ps-
|
|
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-
|
|
125
|
+
.ps-cp {
|
|
244
126
|
background: linear-gradient(135deg, #7fff6e22, #7fff6e11);
|
|
245
|
-
border: 1px solid #7fff6e44;
|
|
246
|
-
|
|
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:
|
|
139
|
+
padding: 8px 12px;
|
|
249
140
|
font-size: 10px;
|
|
250
141
|
cursor: pointer;
|
|
251
142
|
font-family: 'Space Mono', monospace;
|
|
252
|
-
transition: all .
|
|
143
|
+
transition: all .2s;
|
|
253
144
|
text-align: center;
|
|
254
|
-
margin-top:
|
|
145
|
+
margin-top: 6px;
|
|
146
|
+
width: 100%;
|
|
255
147
|
}
|
|
256
|
-
.ps-
|
|
257
|
-
background: linear-gradient(135deg, #
|
|
258
|
-
border-color: #
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
/* ──
|
|
269
|
-
|
|
166
|
+
/* ── SETTINGS PANEL ──────────────────────── */
|
|
167
|
+
.ps-settings {
|
|
270
168
|
display: none;
|
|
271
169
|
flex-direction: column;
|
|
272
|
-
gap:
|
|
170
|
+
gap: 6px;
|
|
273
171
|
padding: 8px 0;
|
|
274
172
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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-
|
|
285
|
-
color: #
|
|
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:
|
|
184
|
+
margin-bottom: 2px;
|
|
290
185
|
}
|
|
291
|
-
.ps-
|
|
186
|
+
.ps-settings-input {
|
|
187
|
+
flex: 1;
|
|
188
|
+
background: #131313;
|
|
189
|
+
border: 1px solid #1e1e3a;
|
|
292
190
|
color: #ccccee;
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
306
|
-
line-height: 1.5;
|
|
195
|
+
outline: none;
|
|
307
196
|
}
|
|
308
|
-
.ps-
|
|
309
|
-
|
|
310
|
-
|
|
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-
|
|
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-
|
|
213
|
+
.ps-settings-status.ok { color: #7fff6e; }
|
|
214
|
+
.ps-gear {
|
|
215
|
+
background: none;
|
|
216
|
+
border: none;
|
|
319
217
|
color: #555577;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
padding:
|
|
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(
|
|
225
|
+
document.head.appendChild(s);
|
|
335
226
|
|
|
336
227
|
// ══════════════════════════════════════════
|
|
337
|
-
//
|
|
228
|
+
// BUILD UI
|
|
338
229
|
// ══════════════════════════════════════════
|
|
339
230
|
const sidebar = document.getElementById('__ps_sidebar__');
|
|
340
231
|
if (!sidebar) return;
|
|
341
232
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
<button class="ps-
|
|
348
|
-
<button class="ps-
|
|
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
|
|
353
|
-
<div
|
|
354
|
-
<div
|
|
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
|
|
364
|
-
<div id="
|
|
365
|
-
<div class="ps-
|
|
366
|
-
<button class="ps-
|
|
367
|
-
<button class="ps-
|
|
368
|
-
<button class="ps-
|
|
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
|
-
<
|
|
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
|
-
<!--
|
|
379
|
-
<div id="
|
|
380
|
-
<div
|
|
381
|
-
|
|
382
|
-
<
|
|
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
|
|
385
|
-
<
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
412
|
-
} else {
|
|
413
|
-
sidebar.appendChild(featuresDiv);
|
|
414
|
-
}
|
|
275
|
+
if (foot) sidebar.insertBefore(feat, foot);
|
|
276
|
+
else sidebar.appendChild(feat);
|
|
415
277
|
|
|
416
278
|
// ══════════════════════════════════════════
|
|
417
|
-
//
|
|
279
|
+
// TAB SWITCHING
|
|
418
280
|
// ══════════════════════════════════════════
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
tab.
|
|
426
|
-
|
|
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
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
if (
|
|
434
|
-
|
|
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
|
-
//
|
|
310
|
+
// API KEY SETTINGS
|
|
440
311
|
// ══════════════════════════════════════════
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
455
|
-
|
|
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
|
-
|
|
355
|
+
showToast('No changes to apply');
|
|
460
356
|
return;
|
|
461
357
|
}
|
|
462
358
|
|
|
463
|
-
// Collect
|
|
464
|
-
const
|
|
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
|
|
469
|
-
const
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
428
|
+
// DIFF PANEL
|
|
533
429
|
// ══════════════════════════════════════════
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
'
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return
|
|
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
|
-
|
|
448
|
+
const changes = [];
|
|
449
|
+
rows.forEach(btn => {
|
|
598
450
|
const row = btn.closest('div[style]');
|
|
599
451
|
if (!row) return;
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
if (
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
|
518
|
+
return map;
|
|
635
519
|
}
|
|
636
520
|
|
|
637
|
-
function
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
|
538
|
+
return o.trim();
|
|
679
539
|
}
|
|
680
540
|
|
|
681
|
-
function
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
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
|
|
551
|
+
return o.trim();
|
|
696
552
|
}
|
|
697
553
|
|
|
698
|
-
function
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
573
|
+
o += `/* ${s} */\nclass="${cls.join(' ')}"\n\n`;
|
|
709
574
|
});
|
|
710
|
-
return
|
|
575
|
+
return o.trim();
|
|
711
576
|
}
|
|
712
577
|
|
|
713
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
//
|
|
594
|
+
// HELPERS
|
|
885
595
|
// ══════════════════════════════════════════
|
|
886
596
|
function showToast(msg) {
|
|
887
|
-
const
|
|
888
|
-
if (
|
|
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
|
})();
|