aetherx-ui-inspector 1.1.0 → 1.2.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/package.json +1 -1
- package/src/core.js +131 -5
- package/src/editor.js +102 -20
- package/src/feedback.js +9 -9
- package/src/inspector.js +5 -2
- package/src/ui.js +67 -25
package/package.json
CHANGED
package/src/core.js
CHANGED
|
@@ -23,6 +23,46 @@ export function init() {
|
|
|
23
23
|
let selectedOriginals = null
|
|
24
24
|
let textEditCleanup = () => {}
|
|
25
25
|
let isCollapsed = false
|
|
26
|
+
let activeTab = 'element'
|
|
27
|
+
|
|
28
|
+
// ── Class stylesheet (for Class tab mode) ─────────────────────────────────
|
|
29
|
+
let classRules = {}
|
|
30
|
+
let classStyleEl = null
|
|
31
|
+
|
|
32
|
+
function getClassStyle() {
|
|
33
|
+
if (!classStyleEl) {
|
|
34
|
+
classStyleEl = document.createElement('style')
|
|
35
|
+
classStyleEl.id = '__dev_inspector_class_rules__'
|
|
36
|
+
document.head.appendChild(classStyleEl)
|
|
37
|
+
}
|
|
38
|
+
return classStyleEl
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setClassRule(cls, prop, val) {
|
|
42
|
+
if (!classRules[cls]) classRules[cls] = {}
|
|
43
|
+
classRules[cls][prop] = val
|
|
44
|
+
rebuildClassSheet()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function removeClassRule(cls, prop) {
|
|
48
|
+
if (!classRules[cls]) return
|
|
49
|
+
delete classRules[cls][prop]
|
|
50
|
+
if (!Object.keys(classRules[cls]).length) delete classRules[cls]
|
|
51
|
+
rebuildClassSheet()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rebuildClassSheet() {
|
|
55
|
+
getClassStyle().textContent = Object.entries(classRules)
|
|
56
|
+
.map(([cls, props]) => {
|
|
57
|
+
const rules = Object.entries(props).map(([p, v]) => ` ${p}: ${v} !important;`).join('\n')
|
|
58
|
+
return `.${cls} {\n${rules}\n}`
|
|
59
|
+
}).join('\n')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getSelectedClass(el) {
|
|
63
|
+
if (!el) return null
|
|
64
|
+
return [...el.classList].find(c => !c.startsWith('di-') && !c.startsWith('__dev')) || null
|
|
65
|
+
}
|
|
26
66
|
|
|
27
67
|
// ── Body margin sync (panel pushes page instead of covering it) ────────────
|
|
28
68
|
document.body.style.transition = 'margin-right 0.22s cubic-bezier(.4,0,.2,1)'
|
|
@@ -36,12 +76,24 @@ export function init() {
|
|
|
36
76
|
document.body.style.marginRight = width + 'px'
|
|
37
77
|
}
|
|
38
78
|
|
|
79
|
+
// Animates overlay position updates over `ms` to cover CSS transition duration
|
|
80
|
+
function animateOverlayUpdate(ms = 260) {
|
|
81
|
+
if (!selectedEl) return
|
|
82
|
+
const end = performance.now() + ms
|
|
83
|
+
const tick = () => {
|
|
84
|
+
if (selectedEl) { updateOverlay(overlay, selectedEl); updateBoxModel(bm, selectedEl) }
|
|
85
|
+
if (performance.now() < end) requestAnimationFrame(tick)
|
|
86
|
+
}
|
|
87
|
+
requestAnimationFrame(tick)
|
|
88
|
+
}
|
|
89
|
+
|
|
39
90
|
// ── State helpers ──────────────────────────────────────────────────────────
|
|
40
91
|
|
|
41
92
|
function openPanel() {
|
|
42
93
|
panelOpen = true
|
|
43
94
|
panel.classList.add('open')
|
|
44
95
|
syncBodyMargin()
|
|
96
|
+
animateOverlayUpdate()
|
|
45
97
|
}
|
|
46
98
|
|
|
47
99
|
function closePanel() {
|
|
@@ -49,6 +101,7 @@ export function init() {
|
|
|
49
101
|
panel.classList.remove('open')
|
|
50
102
|
selectedEl = null
|
|
51
103
|
selectedOriginals = null
|
|
104
|
+
activeTab = 'element'
|
|
52
105
|
overlay.style.display = 'none'
|
|
53
106
|
hoverOverlay.style.display = 'none'
|
|
54
107
|
hideSpacing(spacingContainer)
|
|
@@ -89,6 +142,7 @@ export function init() {
|
|
|
89
142
|
|
|
90
143
|
function selectElement(el) {
|
|
91
144
|
textEditCleanup()
|
|
145
|
+
activeTab = 'element'
|
|
92
146
|
hoverOverlay.style.display = 'none'
|
|
93
147
|
hideSpacing(spacingContainer)
|
|
94
148
|
|
|
@@ -123,6 +177,7 @@ export function init() {
|
|
|
123
177
|
if (!el) {
|
|
124
178
|
bar.style.display = 'none'
|
|
125
179
|
label.textContent = ''
|
|
180
|
+
updateTabRow(null)
|
|
126
181
|
return
|
|
127
182
|
}
|
|
128
183
|
const tag = el.tagName.toLowerCase()
|
|
@@ -134,6 +189,21 @@ export function init() {
|
|
|
134
189
|
.join('')
|
|
135
190
|
label.textContent = `${tag}${id}${cls}`
|
|
136
191
|
bar.style.display = 'flex'
|
|
192
|
+
updateTabRow(el)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function updateTabRow(el) {
|
|
196
|
+
const tabRow = document.getElementById('di-tab-row')
|
|
197
|
+
const classTab = document.getElementById('di-tab-class')
|
|
198
|
+
const classLabel = document.getElementById('di-tab-class-label')
|
|
199
|
+
if (!tabRow) return
|
|
200
|
+
if (!el) { tabRow.style.display = 'none'; return }
|
|
201
|
+
tabRow.style.display = 'flex'
|
|
202
|
+
const cls = getSelectedClass(el)
|
|
203
|
+
if (classLabel) classLabel.textContent = cls ? `.${cls}` : 'No class'
|
|
204
|
+
if (classTab) classTab.disabled = !cls
|
|
205
|
+
document.getElementById('di-tab-element')?.classList.toggle('active', activeTab === 'element')
|
|
206
|
+
classTab?.classList.toggle('active', activeTab === 'class')
|
|
137
207
|
}
|
|
138
208
|
|
|
139
209
|
function renderPanel(el) {
|
|
@@ -156,6 +226,15 @@ export function init() {
|
|
|
156
226
|
if (footer) footer.style.display = 'flex'
|
|
157
227
|
updateFeedbackCount()
|
|
158
228
|
|
|
229
|
+
const cls = getSelectedClass(el)
|
|
230
|
+
const isClassMode = activeTab === 'class' && !!cls
|
|
231
|
+
const applyStyle = isClassMode
|
|
232
|
+
? (prop, val) => setClassRule(cls, prop, val)
|
|
233
|
+
: (prop, val) => el.style.setProperty(prop, val)
|
|
234
|
+
const clearStyle = isClassMode
|
|
235
|
+
? (prop) => removeClassRule(cls, prop)
|
|
236
|
+
: (prop) => el.style.removeProperty(prop)
|
|
237
|
+
|
|
159
238
|
textEditCleanup = renderEditPanel(
|
|
160
239
|
content,
|
|
161
240
|
el,
|
|
@@ -171,7 +250,8 @@ export function init() {
|
|
|
171
250
|
tracker.remove(el, prop)
|
|
172
251
|
updateFeedbackCount()
|
|
173
252
|
if (selectedEl) { updateOverlay(overlay, selectedEl); updateBoxModel(bm, selectedEl) }
|
|
174
|
-
}
|
|
253
|
+
},
|
|
254
|
+
{ applyStyle, clearStyle }
|
|
175
255
|
)
|
|
176
256
|
}
|
|
177
257
|
|
|
@@ -251,11 +331,27 @@ export function init() {
|
|
|
251
331
|
}
|
|
252
332
|
})
|
|
253
333
|
|
|
334
|
+
// ── Mode tabs ──────────────────────────────────────────────────────────────
|
|
335
|
+
document.getElementById('di-tab-element')?.addEventListener('click', () => {
|
|
336
|
+
if (activeTab === 'element') return
|
|
337
|
+
activeTab = 'element'
|
|
338
|
+
updateTabRow(selectedEl)
|
|
339
|
+
if (selectedEl) renderPanel(selectedEl)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
document.getElementById('di-tab-class')?.addEventListener('click', () => {
|
|
343
|
+
if (activeTab === 'class' || !getSelectedClass(selectedEl)) return
|
|
344
|
+
activeTab = 'class'
|
|
345
|
+
updateTabRow(selectedEl)
|
|
346
|
+
if (selectedEl) renderPanel(selectedEl)
|
|
347
|
+
})
|
|
348
|
+
|
|
254
349
|
// ── Collapse toggle ────────────────────────────────────────────────────────
|
|
255
350
|
document.getElementById('di-collapse-btn')?.addEventListener('click', () => {
|
|
256
351
|
isCollapsed = !isCollapsed
|
|
257
352
|
panel.classList.toggle('collapsed', isCollapsed)
|
|
258
353
|
syncBodyMargin()
|
|
354
|
+
animateOverlayUpdate()
|
|
259
355
|
})
|
|
260
356
|
|
|
261
357
|
// ── Resize handle (drag left edge to resize panel width) ───────────────────
|
|
@@ -281,6 +377,7 @@ export function init() {
|
|
|
281
377
|
const newWidth = Math.min(580, Math.max(220, resizeStartWidth + dx))
|
|
282
378
|
panel.style.width = newWidth + 'px'
|
|
283
379
|
syncBodyMargin()
|
|
380
|
+
if (selectedEl) { updateOverlay(overlay, selectedEl); updateBoxModel(bm, selectedEl) }
|
|
284
381
|
})
|
|
285
382
|
|
|
286
383
|
document.addEventListener('mouseup', () => {
|
|
@@ -357,23 +454,35 @@ export function init() {
|
|
|
357
454
|
renderTooltip(tooltip, styles, e.clientX, e.clientY)
|
|
358
455
|
})
|
|
359
456
|
|
|
360
|
-
// ── Hover while selected: spacing
|
|
457
|
+
// ── Hover while selected: spacing + tooltip on self ──────────────────────
|
|
361
458
|
document.addEventListener('mousemove', (e) => {
|
|
362
459
|
if (inspecting || !selectedEl) {
|
|
363
460
|
if (hoverOverlay.style.display !== 'none') {
|
|
364
461
|
hoverOverlay.style.display = 'none'
|
|
365
462
|
hideSpacing(spacingContainer)
|
|
366
463
|
}
|
|
464
|
+
tooltip.style.display = 'none'
|
|
367
465
|
return
|
|
368
466
|
}
|
|
369
467
|
const el = e.target
|
|
370
|
-
if (isInspectorElement(el)
|
|
468
|
+
if (isInspectorElement(el)) {
|
|
371
469
|
hoverOverlay.style.display = 'none'
|
|
372
470
|
hideSpacing(spacingContainer)
|
|
471
|
+
tooltip.style.display = 'none'
|
|
373
472
|
return
|
|
374
473
|
}
|
|
474
|
+
if (el === selectedEl) {
|
|
475
|
+
// Hovering the selected element itself — show its tooltip, no spacing lines
|
|
476
|
+
hoverOverlay.style.display = 'none'
|
|
477
|
+
hideSpacing(spacingContainer)
|
|
478
|
+
const styles = getElementStyles(el)
|
|
479
|
+
renderTooltip(tooltip, styles, e.clientX, e.clientY)
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
// Hovering a different element — show spacing measurements, hide tooltip
|
|
375
483
|
updateHoverOverlay(hoverOverlay, el)
|
|
376
484
|
updateSpacing(spacingContainer, selectedEl, el)
|
|
485
|
+
tooltip.style.display = 'none'
|
|
377
486
|
})
|
|
378
487
|
|
|
379
488
|
// ── Click: select element (works whenever panel is open OR inspect is active)
|
|
@@ -386,12 +495,25 @@ export function init() {
|
|
|
386
495
|
selectElement(el)
|
|
387
496
|
}, true)
|
|
388
497
|
|
|
389
|
-
// ──
|
|
498
|
+
// ── Keyboard shortcuts ─────────────────────────────────────────────────────
|
|
390
499
|
document.addEventListener('keydown', (e) => {
|
|
500
|
+
// Skip if user is typing inside an input / textarea / contenteditable
|
|
501
|
+
const tag = document.activeElement?.tagName.toLowerCase()
|
|
502
|
+
const isTyping = ['input', 'textarea', 'select'].includes(tag)
|
|
503
|
+
|| document.activeElement?.isContentEditable
|
|
504
|
+
|
|
391
505
|
if (e.key === 'Escape') {
|
|
392
506
|
if (inspecting) stopInspect()
|
|
393
507
|
else if (panelOpen) closePanel()
|
|
394
508
|
}
|
|
509
|
+
|
|
510
|
+
// i — toggle inspect mode while panel is open
|
|
511
|
+
if ((e.key === 'i' || e.key === 'I') && !isTyping && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
512
|
+
if (panelOpen) {
|
|
513
|
+
if (inspecting) stopInspect()
|
|
514
|
+
else startInspect()
|
|
515
|
+
}
|
|
516
|
+
}
|
|
395
517
|
})
|
|
396
518
|
|
|
397
519
|
// ── Fix 1: scroll — update overlay using fresh getBoundingClientRect ────────
|
|
@@ -406,9 +528,10 @@ export function init() {
|
|
|
406
528
|
tooltip.style.display = 'none'
|
|
407
529
|
hideBoxModel(bm)
|
|
408
530
|
}
|
|
409
|
-
// Clear hover overlay and
|
|
531
|
+
// Clear hover overlay, spacing, and tooltip on scroll (positions are stale)
|
|
410
532
|
hoverOverlay.style.display = 'none'
|
|
411
533
|
hideSpacing(spacingContainer)
|
|
534
|
+
tooltip.style.display = 'none'
|
|
412
535
|
}, true)
|
|
413
536
|
|
|
414
537
|
return {
|
|
@@ -421,6 +544,9 @@ export function init() {
|
|
|
421
544
|
bm.remove()
|
|
422
545
|
hoverOverlay.remove()
|
|
423
546
|
spacingContainer.remove()
|
|
547
|
+
classStyleEl?.remove()
|
|
548
|
+
classStyleEl = null
|
|
549
|
+
classRules = {}
|
|
424
550
|
document.getElementById('__dev_inspector_styles__')?.remove()
|
|
425
551
|
document.body.classList.remove('di-inspect-mode')
|
|
426
552
|
document.body.style.marginRight = ''
|
package/src/editor.js
CHANGED
|
@@ -3,11 +3,14 @@ import { getElementStyles } from './inspector.js'
|
|
|
3
3
|
|
|
4
4
|
const ALL_PROPS = [
|
|
5
5
|
'color', 'background-color', 'font-size', 'font-weight', 'line-height',
|
|
6
|
-
'
|
|
6
|
+
'font-style', 'text-align', 'text-decoration', 'text-transform', 'letter-spacing',
|
|
7
|
+
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
7
8
|
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
9
|
+
'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height',
|
|
8
10
|
'border-radius', 'border-width', 'border-style', 'border-color',
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
+
'position', 'top', 'right', 'bottom', 'left', 'z-index',
|
|
12
|
+
'display', 'flex-direction', 'justify-content', 'align-items', 'gap', 'overflow',
|
|
13
|
+
'opacity', 'box-shadow', 'object-fit',
|
|
11
14
|
]
|
|
12
15
|
|
|
13
16
|
export function captureOriginals(el) {
|
|
@@ -23,7 +26,9 @@ export function captureOriginals(el) {
|
|
|
23
26
|
return map
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
export function renderEditPanel(panelContent, el, originals, onChange, onReset) {
|
|
29
|
+
export function renderEditPanel(panelContent, el, originals, onChange, onReset, { applyStyle, clearStyle } = {}) {
|
|
30
|
+
const _apply = applyStyle || ((p, v) => el.style.setProperty(p, v))
|
|
31
|
+
const _clear = clearStyle || ((p) => el.style.removeProperty(p))
|
|
27
32
|
const styles = getElementStyles(el)
|
|
28
33
|
const { typography, background, spacing, border, layout, dimensions, opacity } = styles
|
|
29
34
|
const cs = window.getComputedStyle(el)
|
|
@@ -86,6 +91,20 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
86
91
|
${resetBtn(prop)}
|
|
87
92
|
</div>`
|
|
88
93
|
|
|
94
|
+
// Plain text input — for values like "none", "auto", "100%", "0 4px 8px rgba(...)"
|
|
95
|
+
const textField = (label, prop, value) => {
|
|
96
|
+
const orig = originals[prop] || ''
|
|
97
|
+
return `
|
|
98
|
+
<div class="di-field">
|
|
99
|
+
<label>${label}</label>
|
|
100
|
+
<div class="di-field-control">
|
|
101
|
+
<input class="di-input" data-prop="${prop}" data-original="${escAttr(orig)}"
|
|
102
|
+
value="${escAttr(value || '')}" placeholder="auto" />
|
|
103
|
+
</div>
|
|
104
|
+
${resetBtn(prop)}
|
|
105
|
+
</div>`
|
|
106
|
+
}
|
|
107
|
+
|
|
89
108
|
// Spacing grid cell with mini stepper
|
|
90
109
|
const spacingCell = (prop, val, label) => {
|
|
91
110
|
const orig = originals[prop] || ''
|
|
@@ -135,8 +154,14 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
135
154
|
</div>
|
|
136
155
|
</div>` : ''
|
|
137
156
|
|
|
157
|
+
// ── Element type flags ────────────────────────────────────────────────────
|
|
158
|
+
const isImg = el.tagName === 'IMG'
|
|
159
|
+
const isSvg = el instanceof SVGElement
|
|
160
|
+
const isTextEl = ['p','h1','h2','h3','h4','h5','h6','span','a','li','td','th',
|
|
161
|
+
'label','button','em','strong','small','code','pre','blockquote',
|
|
162
|
+
].includes(el.tagName.toLowerCase())
|
|
163
|
+
|
|
138
164
|
// ── Image section (Fix 1) ─────────────────────────────────────────────────
|
|
139
|
-
const isImg = el.tagName === 'IMG'
|
|
140
165
|
const imgSection = isImg ? `
|
|
141
166
|
${section('Image')}
|
|
142
167
|
<div style="padding:8px 16px 12px;display:flex;flex-direction:column;gap:8px;">
|
|
@@ -153,6 +178,10 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
153
178
|
${section('Size')}
|
|
154
179
|
${numberField('width', 'width', dimensions.width, 'px')}
|
|
155
180
|
${numberField('height', 'height', dimensions.height, 'px')}
|
|
181
|
+
${textField('min-width', 'min-width', cs.minWidth)}
|
|
182
|
+
${textField('max-width', 'max-width', cs.maxWidth)}
|
|
183
|
+
${textField('min-height', 'min-height', cs.minHeight)}
|
|
184
|
+
${textField('max-height', 'max-height', cs.maxHeight)}
|
|
156
185
|
`
|
|
157
186
|
|
|
158
187
|
// ── Typography ────────────────────────────────────────────────────────────
|
|
@@ -160,12 +189,19 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
160
189
|
const typoSection = `
|
|
161
190
|
${section('Typography')}
|
|
162
191
|
${numberField('font-size', 'font-size', typography.fontSize, 'px')}
|
|
192
|
+
${selectField('font-style', 'font-style', cs.fontStyle,
|
|
193
|
+
['normal','italic','oblique'])}
|
|
163
194
|
${selectField('font-weight', 'font-weight', typography.fontWeight,
|
|
164
195
|
['100','200','300','400','500','600','700','800','900'])}
|
|
165
196
|
${numberField('line-height', 'line-height', typography.lineHeight, 'px')}
|
|
166
197
|
${colorField('color', 'color', typography.colorHex, origColorHex)}
|
|
167
198
|
${selectField('text-align', 'text-align', typography.textAlign,
|
|
168
199
|
['left','center','right','justify'])}
|
|
200
|
+
${selectField('decoration', 'text-decoration', cs.textDecorationLine,
|
|
201
|
+
['none','underline','line-through','overline'])}
|
|
202
|
+
${selectField('transform', 'text-transform', cs.textTransform,
|
|
203
|
+
['none','uppercase','lowercase','capitalize'])}
|
|
204
|
+
${textField('letter-spacing', 'letter-spacing', cs.letterSpacing)}
|
|
169
205
|
`
|
|
170
206
|
|
|
171
207
|
// ── Background ────────────────────────────────────────────────────────────
|
|
@@ -210,7 +246,7 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
210
246
|
const layoutSection = `
|
|
211
247
|
${section('Layout')}
|
|
212
248
|
${selectField('display', 'display', layout.display,
|
|
213
|
-
['block','flex','grid','inline-flex','inline-block','none'])}
|
|
249
|
+
['block','flex','grid','inline-flex','inline-block','inline','none'])}
|
|
214
250
|
${isFlex ? `
|
|
215
251
|
${selectField('direction', 'flex-direction', layout.flexDirection,
|
|
216
252
|
['row','row-reverse','column','column-reverse'])}
|
|
@@ -220,30 +256,72 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
220
256
|
['flex-start','flex-end','center','stretch','baseline'])}
|
|
221
257
|
` : ''}
|
|
222
258
|
${(isFlex || isGrid) ? numberField('gap', 'gap', layout.gap, 'px') : ''}
|
|
259
|
+
${selectField('overflow', 'overflow', cs.overflow,
|
|
260
|
+
['visible','hidden','auto','scroll','clip'])}
|
|
261
|
+
`
|
|
262
|
+
|
|
263
|
+
// ── Position ──────────────────────────────────────────────────────────────
|
|
264
|
+
const isPositioned = cs.position !== 'static'
|
|
265
|
+
const posSection = `
|
|
266
|
+
${section('Position')}
|
|
267
|
+
${selectField('position', 'position', cs.position,
|
|
268
|
+
['static','relative','absolute','fixed','sticky'])}
|
|
269
|
+
${isPositioned ? `
|
|
270
|
+
${textField('top', 'top', cs.top)}
|
|
271
|
+
${textField('right', 'right', cs.right)}
|
|
272
|
+
${textField('bottom', 'bottom', cs.bottom)}
|
|
273
|
+
${textField('left', 'left', cs.left)}
|
|
274
|
+
` : ''}
|
|
275
|
+
${textField('z-index', 'z-index', cs.zIndex === 'auto' ? 'auto' : cs.zIndex)}
|
|
223
276
|
`
|
|
224
277
|
|
|
225
278
|
// ── Misc ──────────────────────────────────────────────────────────────────
|
|
226
279
|
const miscSection = `
|
|
227
280
|
${section('Misc')}
|
|
228
281
|
${numberField('opacity', 'opacity', opacity)}
|
|
282
|
+
${textField('box-shadow', 'box-shadow', cs.boxShadow === 'none' ? '' : cs.boxShadow)}
|
|
283
|
+
${isImg ? selectField('object-fit', 'object-fit', cs.objectFit,
|
|
284
|
+
['fill','contain','cover','none','scale-down']) : ''}
|
|
285
|
+
`
|
|
286
|
+
|
|
287
|
+
// SVGs: show color only instead of full typography
|
|
288
|
+
const origColorHexForSvg = colorToHex(originals['color'])
|
|
289
|
+
const svgColorSection = `
|
|
290
|
+
${section('Color')}
|
|
291
|
+
${colorField('color', 'color', typography.colorHex, origColorHexForSvg)}
|
|
229
292
|
`
|
|
230
293
|
|
|
231
|
-
|
|
294
|
+
// Typography visibility: hidden for images, color-only for SVGs, full for everything else
|
|
295
|
+
const typoRendered = isImg ? '' : isSvg ? svgColorSection : typoSection
|
|
296
|
+
|
|
297
|
+
// Section order: text elements get typography first for quick access
|
|
298
|
+
panelContent.innerHTML = isTextEl ? `
|
|
299
|
+
${textSection}
|
|
300
|
+
${typoRendered}
|
|
301
|
+
${bgSection}
|
|
302
|
+
${sizeSection}
|
|
303
|
+
${spacingSection}
|
|
304
|
+
${borderSection}
|
|
305
|
+
${layoutSection}
|
|
306
|
+
${posSection}
|
|
307
|
+
${miscSection}
|
|
308
|
+
` : `
|
|
232
309
|
${imgSection}
|
|
233
310
|
${textSection}
|
|
234
311
|
${sizeSection}
|
|
235
|
-
${
|
|
312
|
+
${typoRendered}
|
|
236
313
|
${bgSection}
|
|
237
314
|
${spacingSection}
|
|
238
315
|
${borderSection}
|
|
239
316
|
${layoutSection}
|
|
317
|
+
${posSection}
|
|
240
318
|
${miscSection}
|
|
241
319
|
`
|
|
242
320
|
|
|
243
321
|
// ── Wire: regular inputs ───────────────────────────────────────────────────
|
|
244
322
|
panelContent.querySelectorAll('.di-input[data-prop]').forEach(input => {
|
|
245
|
-
input.addEventListener('input', () => applyChange(input, el, onChange, originals))
|
|
246
|
-
input.addEventListener('change', () => applyChange(input, el, onChange, originals))
|
|
323
|
+
input.addEventListener('input', () => applyChange(input, el, onChange, originals, _apply))
|
|
324
|
+
input.addEventListener('change', () => applyChange(input, el, onChange, originals, _apply))
|
|
247
325
|
})
|
|
248
326
|
|
|
249
327
|
// ── Wire: color pickers ───────────────────────────────────────────────────
|
|
@@ -255,7 +333,7 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
255
333
|
picker.addEventListener('input', () => {
|
|
256
334
|
if (textBox) textBox.value = picker.value
|
|
257
335
|
if (swatch) swatch.style.background = picker.value
|
|
258
|
-
applyColorChange(picker.dataset.prop, picker.value, el, onChange, originals)
|
|
336
|
+
applyColorChange(picker.dataset.prop, picker.value, el, onChange, originals, _apply)
|
|
259
337
|
markResetBtn(picker.dataset.prop, picker.value, picker.dataset.original)
|
|
260
338
|
})
|
|
261
339
|
|
|
@@ -265,7 +343,7 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
265
343
|
if (/^#[0-9a-fA-F]{3,6}$/.test(val)) {
|
|
266
344
|
picker.value = val
|
|
267
345
|
swatch.style.background = val
|
|
268
|
-
applyColorChange(picker.dataset.prop, val, el, onChange, originals)
|
|
346
|
+
applyColorChange(picker.dataset.prop, val, el, onChange, originals, _apply)
|
|
269
347
|
markResetBtn(textBox.dataset.prop, val, textBox.dataset.original)
|
|
270
348
|
}
|
|
271
349
|
})
|
|
@@ -281,11 +359,11 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
281
359
|
const delta = parseFloat(btn.dataset.step)
|
|
282
360
|
const current = parseFloat(input.value) || 0
|
|
283
361
|
input.value = round(current + delta)
|
|
284
|
-
applyChange(input, el, onChange, originals)
|
|
362
|
+
applyChange(input, el, onChange, originals, _apply)
|
|
285
363
|
})
|
|
286
364
|
})
|
|
287
365
|
|
|
288
|
-
// ── Wire: arrow keys on numeric inputs
|
|
366
|
+
// ── Wire: arrow keys on numeric inputs ────────────────────────────────────
|
|
289
367
|
panelContent.querySelectorAll('.di-number-input').forEach(input => {
|
|
290
368
|
input.addEventListener('keydown', (e) => {
|
|
291
369
|
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
|
|
@@ -294,7 +372,7 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
294
372
|
const delta = e.key === 'ArrowUp' ? step : -step
|
|
295
373
|
const current = parseFloat(input.value) || 0
|
|
296
374
|
input.value = round(current + delta)
|
|
297
|
-
applyChange(input, el, onChange, originals)
|
|
375
|
+
applyChange(input, el, onChange, originals, _apply)
|
|
298
376
|
})
|
|
299
377
|
})
|
|
300
378
|
|
|
@@ -302,7 +380,7 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
302
380
|
panelContent.querySelectorAll('.di-reset-btn[data-reset-prop]').forEach(btn => {
|
|
303
381
|
btn.addEventListener('click', () => {
|
|
304
382
|
const prop = btn.dataset.resetProp
|
|
305
|
-
|
|
383
|
+
_clear(prop)
|
|
306
384
|
const restored = window.getComputedStyle(el).getPropertyValue(prop).trim()
|
|
307
385
|
|
|
308
386
|
const input = panelContent.querySelector(`.di-input[data-prop="${prop}"]`)
|
|
@@ -382,7 +460,7 @@ export function renderEditPanel(panelContent, el, originals, onChange, onReset)
|
|
|
382
460
|
|
|
383
461
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
384
462
|
|
|
385
|
-
function applyChange(input, el, onChange, originals) {
|
|
463
|
+
function applyChange(input, el, onChange, originals, applyStyleFn) {
|
|
386
464
|
const prop = input.dataset.prop
|
|
387
465
|
const unit = input.dataset.unit || ''
|
|
388
466
|
const rawVal = input.value.trim()
|
|
@@ -400,14 +478,14 @@ function applyChange(input, el, onChange, originals) {
|
|
|
400
478
|
}
|
|
401
479
|
|
|
402
480
|
const oldValue = el.style.getPropertyValue(prop) || window.getComputedStyle(el).getPropertyValue(prop)
|
|
403
|
-
|
|
481
|
+
applyStyleFn(prop, value)
|
|
404
482
|
onChange(el, prop, oldValue, value)
|
|
405
483
|
markResetBtn(prop, value, originals[prop])
|
|
406
484
|
}
|
|
407
485
|
|
|
408
|
-
function applyColorChange(prop, value, el, onChange, originals) {
|
|
486
|
+
function applyColorChange(prop, value, el, onChange, originals, applyStyleFn) {
|
|
409
487
|
const oldValue = el.style.getPropertyValue(prop) || window.getComputedStyle(el).getPropertyValue(prop)
|
|
410
|
-
|
|
488
|
+
applyStyleFn(prop, value)
|
|
411
489
|
onChange(el, prop, colorToHex(oldValue) || oldValue, value)
|
|
412
490
|
}
|
|
413
491
|
|
|
@@ -492,6 +570,10 @@ function escHtml(str) {
|
|
|
492
570
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
493
571
|
}
|
|
494
572
|
|
|
573
|
+
function escAttr(str) {
|
|
574
|
+
return String(str || '').replace(/"/g, '"').replace(/</g, '<')
|
|
575
|
+
}
|
|
576
|
+
|
|
495
577
|
function camelize(str) {
|
|
496
578
|
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
497
579
|
}
|
package/src/feedback.js
CHANGED
|
@@ -61,22 +61,22 @@ export function createChangeTracker() {
|
|
|
61
61
|
grouped[selector].push({ property, from, to })
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
const lines = [
|
|
64
|
+
const lines = [
|
|
65
|
+
'## UI Feedback Summary',
|
|
66
|
+
'',
|
|
67
|
+
`> ${changes.length} change${changes.length === 1 ? '' : 's'} · ${new Date().toLocaleString()}`,
|
|
68
|
+
'',
|
|
69
|
+
]
|
|
65
70
|
|
|
66
71
|
Object.entries(grouped).forEach(([selector, props]) => {
|
|
67
|
-
lines.push(
|
|
72
|
+
lines.push(`### \`${selector}\``)
|
|
68
73
|
props.forEach(({ property, from, to }) => {
|
|
69
|
-
|
|
70
|
-
const toDisplay = to || '(none)'
|
|
71
|
-
lines.push(` ${property}: ${fromDisplay} → ${toDisplay}`)
|
|
74
|
+
lines.push(`- **${property}**: \`${from || '(none)'}\` → \`${to || '(none)'}\``)
|
|
72
75
|
})
|
|
73
76
|
lines.push('')
|
|
74
77
|
})
|
|
75
78
|
|
|
76
|
-
lines.
|
|
77
|
-
lines.push(`Captured: ${new Date().toLocaleString()}`)
|
|
78
|
-
|
|
79
|
-
return lines.join('\n')
|
|
79
|
+
return lines.join('\n').trimEnd()
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function remove(el, property) {
|
package/src/inspector.js
CHANGED
|
@@ -41,6 +41,7 @@ export function getElementStyles(el) {
|
|
|
41
41
|
return {
|
|
42
42
|
label,
|
|
43
43
|
tag,
|
|
44
|
+
isSvg: el instanceof SVGElement,
|
|
44
45
|
textContent,
|
|
45
46
|
size: {
|
|
46
47
|
width: Math.round(rect.width) + 'px',
|
|
@@ -196,7 +197,7 @@ export function updateBoxModel(bm, el) {
|
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
export function renderTooltip(tooltip, styles, x, y) {
|
|
199
|
-
const { label, size, typography, background, spacing, border, layout, textContent } = styles
|
|
200
|
+
const { label, size, typography, background, spacing, border, layout, textContent, isSvg } = styles
|
|
200
201
|
|
|
201
202
|
const colorDot = (hex) => hex
|
|
202
203
|
? `<span class="di-swatch" style="background:${hex};"></span>`
|
|
@@ -205,6 +206,7 @@ export function renderTooltip(tooltip, styles, x, y) {
|
|
|
205
206
|
const row = (key, val) =>
|
|
206
207
|
`<div class="di-row"><span class="di-key">${key}</span><span class="di-val">${val}</span></div>`
|
|
207
208
|
|
|
209
|
+
const isImg = styles.tag === 'img'
|
|
208
210
|
const isText = ['p','h1','h2','h3','h4','h5','h6','span','a','li','td','th','label','button'].includes(styles.tag)
|
|
209
211
|
const isFlex = layout.display === 'flex'
|
|
210
212
|
const isGrid = layout.display === 'grid'
|
|
@@ -217,7 +219,8 @@ export function renderTooltip(tooltip, styles, x, y) {
|
|
|
217
219
|
|
|
218
220
|
html += row('size', `${size.width} × ${size.height}`)
|
|
219
221
|
|
|
220
|
-
|
|
222
|
+
// Typography: skip for images and SVGs
|
|
223
|
+
if (!isImg && !isSvg && (isText || typography.fontSize !== '0px')) {
|
|
221
224
|
html += `<div class="di-section">Typography</div>`
|
|
222
225
|
html += row('font', typography.fontFamily)
|
|
223
226
|
html += row('size', typography.fontSize)
|
package/src/ui.js
CHANGED
|
@@ -128,6 +128,7 @@ export function injectStyles() {
|
|
|
128
128
|
min-width: 40px;
|
|
129
129
|
}
|
|
130
130
|
#__dev_inspector_panel__.collapsed .di-element-bar,
|
|
131
|
+
#__dev_inspector_panel__.collapsed .di-tab-row,
|
|
131
132
|
#__dev_inspector_panel__.collapsed #di-panel-content,
|
|
132
133
|
#__dev_inspector_panel__.collapsed .di-panel-footer,
|
|
133
134
|
#__dev_inspector_panel__.collapsed .di-panel-header h3 {
|
|
@@ -702,6 +703,32 @@ export function injectStyles() {
|
|
|
702
703
|
line-height: 1.6;
|
|
703
704
|
}
|
|
704
705
|
|
|
706
|
+
/* ── Mode tabs (Element / Class) ── */
|
|
707
|
+
.di-tab-row {
|
|
708
|
+
display: flex;
|
|
709
|
+
padding: 6px 10px;
|
|
710
|
+
gap: 4px;
|
|
711
|
+
border-bottom: 1px solid #27272a;
|
|
712
|
+
flex-shrink: 0;
|
|
713
|
+
background: #18181b;
|
|
714
|
+
}
|
|
715
|
+
.di-tab {
|
|
716
|
+
padding: 4px 10px;
|
|
717
|
+
border-radius: 6px;
|
|
718
|
+
border: 1px solid transparent;
|
|
719
|
+
background: transparent;
|
|
720
|
+
color: #71717a;
|
|
721
|
+
font-size: 11px;
|
|
722
|
+
font-weight: 500;
|
|
723
|
+
cursor: pointer;
|
|
724
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
725
|
+
transition: color 0.12s, background 0.12s, border-color 0.12s;
|
|
726
|
+
white-space: nowrap;
|
|
727
|
+
}
|
|
728
|
+
.di-tab:hover:not(:disabled) { color: #e4e4e7; background: #27272a; }
|
|
729
|
+
.di-tab.active { background: #27272a; border-color: #6366f1; color: #818cf8; }
|
|
730
|
+
.di-tab:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
731
|
+
|
|
705
732
|
/* ── Inspect cursor mode ── */
|
|
706
733
|
body.di-inspect-mode * { cursor: crosshair !important; }
|
|
707
734
|
`
|
|
@@ -757,6 +784,10 @@ export function createPanel() {
|
|
|
757
784
|
<span class="di-element-label" id="di-element-label"></span>
|
|
758
785
|
<button class="di-reinspect-btn" id="di-reinspect-btn">↖ Pick</button>
|
|
759
786
|
</div>
|
|
787
|
+
<div class="di-tab-row" id="di-tab-row" style="display:none;">
|
|
788
|
+
<button class="di-tab active" id="di-tab-element">Element</button>
|
|
789
|
+
<button class="di-tab" id="di-tab-class"><span id="di-tab-class-label">.class</span></button>
|
|
790
|
+
</div>
|
|
760
791
|
<div id="di-panel-content">
|
|
761
792
|
<div class="di-panel-empty">
|
|
762
793
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -813,13 +844,9 @@ export function updateOverlay(overlay, el) {
|
|
|
813
844
|
.filter(c => !c.startsWith('di-') && !c.startsWith('__dev'))
|
|
814
845
|
.slice(0, 2).map(c => `.${c}`).join('')
|
|
815
846
|
badge.textContent = `${tag}${id}${cls}`
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
} else {
|
|
820
|
-
badge.style.top = '-22px'
|
|
821
|
-
badge.style.borderRadius = '4px 4px 0 0'
|
|
822
|
-
}
|
|
847
|
+
// Selected badge always sits above the element (static position)
|
|
848
|
+
badge.style.top = '-22px'
|
|
849
|
+
badge.style.borderRadius = '4px 4px 0 0'
|
|
823
850
|
}
|
|
824
851
|
}
|
|
825
852
|
|
|
@@ -850,13 +877,9 @@ export function updateHoverOverlay(overlay, el) {
|
|
|
850
877
|
.filter(c => !c.startsWith('di-') && !c.startsWith('__dev'))
|
|
851
878
|
.slice(0, 2).map(c => `.${c}`).join('')
|
|
852
879
|
badge.textContent = `${tag}${id}${cls}`
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
} else {
|
|
857
|
-
badge.style.top = '-22px'
|
|
858
|
-
badge.style.borderRadius = '4px 4px 0 0'
|
|
859
|
-
}
|
|
880
|
+
// Hover badge always sits below the highlighted element
|
|
881
|
+
badge.style.top = 'calc(100% + 2px)'
|
|
882
|
+
badge.style.borderRadius = '0 0 4px 4px'
|
|
860
883
|
}
|
|
861
884
|
}
|
|
862
885
|
|
|
@@ -882,28 +905,47 @@ export function updateSpacing(container, selEl, hovEl) {
|
|
|
882
905
|
const vOverlap = sR.bottom > hR.top && hR.bottom > sR.top
|
|
883
906
|
const lines = []
|
|
884
907
|
|
|
885
|
-
if (!hOverlap) {
|
|
908
|
+
if (!hOverlap && !vOverlap) {
|
|
909
|
+
// ── Diagonal: L-shaped measurement anchored at the nearest corner ─────────
|
|
910
|
+
const isBelowV = hR.top >= sR.bottom
|
|
911
|
+
const isRightH = hR.left >= sR.right
|
|
912
|
+
|
|
913
|
+
const vGap = Math.round(isBelowV ? hR.top - sR.bottom : sR.top - hR.bottom)
|
|
914
|
+
const hGap = Math.round(isRightH ? hR.left - sR.right : sR.left - hR.right)
|
|
915
|
+
|
|
916
|
+
// Corner of the L: sR's nearest horizontal edge × hR's nearest vertical edge
|
|
917
|
+
const cornerX = isRightH ? sR.right : sR.left
|
|
918
|
+
const cornerY = isBelowV ? hR.top : hR.bottom
|
|
919
|
+
|
|
920
|
+
// Vertical segment: from sR's bottom/top down/up to cornerY
|
|
921
|
+
const vY1 = isBelowV ? sR.bottom : cornerY
|
|
922
|
+
const vY2 = isBelowV ? cornerY : sR.top
|
|
923
|
+
if (vGap > 0) lines.push({ x: cornerX - 0.5, y: vY1, w: 1, h: vY2 - vY1, label: vGap + 'px' })
|
|
924
|
+
|
|
925
|
+
// Horizontal segment: from cornerX across to hR's nearest edge, at cornerY
|
|
926
|
+
const hX1 = isRightH ? cornerX : hR.right
|
|
927
|
+
const hX2 = isRightH ? hR.left : cornerX
|
|
928
|
+
if (hGap > 0) lines.push({ x: hX1, y: cornerY - 0.5, w: hX2 - hX1, h: 1, label: hGap + 'px' })
|
|
929
|
+
|
|
930
|
+
} else if (!hOverlap) {
|
|
931
|
+
// ── Side by side (vertical overlap): single horizontal gap line ───────────
|
|
886
932
|
const isRight = hR.left > sR.right
|
|
887
933
|
const x1 = isRight ? sR.right : hR.right
|
|
888
|
-
const x2 = isRight ? hR.left
|
|
934
|
+
const x2 = isRight ? hR.left : sR.left
|
|
889
935
|
const gap = Math.round(x2 - x1)
|
|
890
936
|
if (gap > 0) {
|
|
891
|
-
const y =
|
|
892
|
-
? (Math.max(sR.top, hR.top) + Math.min(sR.bottom, hR.bottom)) / 2
|
|
893
|
-
: (sR.top + sR.bottom) / 2
|
|
937
|
+
const y = (Math.max(sR.top, hR.top) + Math.min(sR.bottom, hR.bottom)) / 2
|
|
894
938
|
lines.push({ x: x1, y: y - 0.5, w: x2 - x1, h: 1, label: gap + 'px' })
|
|
895
939
|
}
|
|
896
|
-
}
|
|
897
940
|
|
|
898
|
-
if (!vOverlap) {
|
|
941
|
+
} else if (!vOverlap) {
|
|
942
|
+
// ── Stacked (horizontal overlap): single vertical gap line ────────────────
|
|
899
943
|
const isBelow = hR.top > sR.bottom
|
|
900
944
|
const y1 = isBelow ? sR.bottom : hR.bottom
|
|
901
|
-
const y2 = isBelow ? hR.top
|
|
945
|
+
const y2 = isBelow ? hR.top : sR.top
|
|
902
946
|
const gap = Math.round(y2 - y1)
|
|
903
947
|
if (gap > 0) {
|
|
904
|
-
const x =
|
|
905
|
-
? (Math.max(sR.left, hR.left) + Math.min(sR.right, hR.right)) / 2
|
|
906
|
-
: (sR.left + sR.right) / 2
|
|
948
|
+
const x = (Math.max(sR.left, hR.left) + Math.min(sR.right, hR.right)) / 2
|
|
907
949
|
lines.push({ x: x - 0.5, y: y1, w: 1, h: y2 - y1, label: gap + 'px' })
|
|
908
950
|
}
|
|
909
951
|
}
|