aetherx-ui-inspector 1.1.0 → 1.2.1
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 +168 -11
- 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,53 @@ export function init() {
|
|
|
36
76
|
document.body.style.marginRight = width + 'px'
|
|
37
77
|
}
|
|
38
78
|
|
|
79
|
+
// Hides hover overlay and resets selected badge back to its default top position
|
|
80
|
+
function hideHoverOverlay() {
|
|
81
|
+
hoverOverlay.style.display = 'none'
|
|
82
|
+
const selBadge = overlay.querySelector('.di-overlay-badge')
|
|
83
|
+
if (selBadge) {
|
|
84
|
+
selBadge.style.top = '-22px'
|
|
85
|
+
selBadge.style.borderRadius = '4px 4px 0 0'
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Repositions both badges so they never collide when elements are above/below each other
|
|
90
|
+
function repositionBadges(hovEl) {
|
|
91
|
+
if (!selectedEl || !hovEl) return
|
|
92
|
+
const sR = selectedEl.getBoundingClientRect()
|
|
93
|
+
const hR = hovEl.getBoundingClientRect()
|
|
94
|
+
const hovAbove = (hR.top + hR.bottom) / 2 < (sR.top + sR.bottom) / 2
|
|
95
|
+
|
|
96
|
+
const hovBadge = hoverOverlay.querySelector('.di-overlay-badge')
|
|
97
|
+
if (hovBadge) {
|
|
98
|
+
hovBadge.style.top = hovAbove ? '-22px' : 'calc(100% + 2px)'
|
|
99
|
+
hovBadge.style.borderRadius = hovAbove ? '4px 4px 0 0' : '0 0 4px 4px'
|
|
100
|
+
}
|
|
101
|
+
const selBadge = overlay.querySelector('.di-overlay-badge')
|
|
102
|
+
if (selBadge) {
|
|
103
|
+
selBadge.style.top = hovAbove ? 'calc(100% + 2px)' : '-22px'
|
|
104
|
+
selBadge.style.borderRadius = hovAbove ? '0 0 4px 4px' : '4px 4px 0 0'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Animates overlay position updates over `ms` to cover CSS transition duration
|
|
109
|
+
function animateOverlayUpdate(ms = 260) {
|
|
110
|
+
if (!selectedEl) return
|
|
111
|
+
const end = performance.now() + ms
|
|
112
|
+
const tick = () => {
|
|
113
|
+
if (selectedEl) { updateOverlay(overlay, selectedEl); updateBoxModel(bm, selectedEl) }
|
|
114
|
+
if (performance.now() < end) requestAnimationFrame(tick)
|
|
115
|
+
}
|
|
116
|
+
requestAnimationFrame(tick)
|
|
117
|
+
}
|
|
118
|
+
|
|
39
119
|
// ── State helpers ──────────────────────────────────────────────────────────
|
|
40
120
|
|
|
41
121
|
function openPanel() {
|
|
42
122
|
panelOpen = true
|
|
43
123
|
panel.classList.add('open')
|
|
44
124
|
syncBodyMargin()
|
|
125
|
+
animateOverlayUpdate()
|
|
45
126
|
}
|
|
46
127
|
|
|
47
128
|
function closePanel() {
|
|
@@ -49,8 +130,9 @@ export function init() {
|
|
|
49
130
|
panel.classList.remove('open')
|
|
50
131
|
selectedEl = null
|
|
51
132
|
selectedOriginals = null
|
|
133
|
+
activeTab = 'element'
|
|
52
134
|
overlay.style.display = 'none'
|
|
53
|
-
|
|
135
|
+
hideHoverOverlay()
|
|
54
136
|
hideSpacing(spacingContainer)
|
|
55
137
|
setElementBar(null)
|
|
56
138
|
stopInspect(true)
|
|
@@ -61,7 +143,7 @@ export function init() {
|
|
|
61
143
|
inspecting = true
|
|
62
144
|
cog.classList.add('active')
|
|
63
145
|
document.body.classList.add('di-inspect-mode')
|
|
64
|
-
|
|
146
|
+
hideHoverOverlay()
|
|
65
147
|
hideSpacing(spacingContainer)
|
|
66
148
|
// Mark re-inspect button as active if panel is open
|
|
67
149
|
const reinspectBtn = document.getElementById('di-reinspect-btn')
|
|
@@ -89,7 +171,8 @@ export function init() {
|
|
|
89
171
|
|
|
90
172
|
function selectElement(el) {
|
|
91
173
|
textEditCleanup()
|
|
92
|
-
|
|
174
|
+
activeTab = 'element'
|
|
175
|
+
hideHoverOverlay()
|
|
93
176
|
hideSpacing(spacingContainer)
|
|
94
177
|
|
|
95
178
|
// Stop inspect mode without hiding the selected-element overlay
|
|
@@ -123,6 +206,7 @@ export function init() {
|
|
|
123
206
|
if (!el) {
|
|
124
207
|
bar.style.display = 'none'
|
|
125
208
|
label.textContent = ''
|
|
209
|
+
updateTabRow(null)
|
|
126
210
|
return
|
|
127
211
|
}
|
|
128
212
|
const tag = el.tagName.toLowerCase()
|
|
@@ -134,6 +218,21 @@ export function init() {
|
|
|
134
218
|
.join('')
|
|
135
219
|
label.textContent = `${tag}${id}${cls}`
|
|
136
220
|
bar.style.display = 'flex'
|
|
221
|
+
updateTabRow(el)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function updateTabRow(el) {
|
|
225
|
+
const tabRow = document.getElementById('di-tab-row')
|
|
226
|
+
const classTab = document.getElementById('di-tab-class')
|
|
227
|
+
const classLabel = document.getElementById('di-tab-class-label')
|
|
228
|
+
if (!tabRow) return
|
|
229
|
+
if (!el) { tabRow.style.display = 'none'; return }
|
|
230
|
+
tabRow.style.display = 'flex'
|
|
231
|
+
const cls = getSelectedClass(el)
|
|
232
|
+
if (classLabel) classLabel.textContent = cls ? `.${cls}` : 'No class'
|
|
233
|
+
if (classTab) classTab.disabled = !cls
|
|
234
|
+
document.getElementById('di-tab-element')?.classList.toggle('active', activeTab === 'element')
|
|
235
|
+
classTab?.classList.toggle('active', activeTab === 'class')
|
|
137
236
|
}
|
|
138
237
|
|
|
139
238
|
function renderPanel(el) {
|
|
@@ -156,6 +255,15 @@ export function init() {
|
|
|
156
255
|
if (footer) footer.style.display = 'flex'
|
|
157
256
|
updateFeedbackCount()
|
|
158
257
|
|
|
258
|
+
const cls = getSelectedClass(el)
|
|
259
|
+
const isClassMode = activeTab === 'class' && !!cls
|
|
260
|
+
const applyStyle = isClassMode
|
|
261
|
+
? (prop, val) => setClassRule(cls, prop, val)
|
|
262
|
+
: (prop, val) => el.style.setProperty(prop, val)
|
|
263
|
+
const clearStyle = isClassMode
|
|
264
|
+
? (prop) => removeClassRule(cls, prop)
|
|
265
|
+
: (prop) => el.style.removeProperty(prop)
|
|
266
|
+
|
|
159
267
|
textEditCleanup = renderEditPanel(
|
|
160
268
|
content,
|
|
161
269
|
el,
|
|
@@ -171,7 +279,8 @@ export function init() {
|
|
|
171
279
|
tracker.remove(el, prop)
|
|
172
280
|
updateFeedbackCount()
|
|
173
281
|
if (selectedEl) { updateOverlay(overlay, selectedEl); updateBoxModel(bm, selectedEl) }
|
|
174
|
-
}
|
|
282
|
+
},
|
|
283
|
+
{ applyStyle, clearStyle }
|
|
175
284
|
)
|
|
176
285
|
}
|
|
177
286
|
|
|
@@ -251,11 +360,27 @@ export function init() {
|
|
|
251
360
|
}
|
|
252
361
|
})
|
|
253
362
|
|
|
363
|
+
// ── Mode tabs ──────────────────────────────────────────────────────────────
|
|
364
|
+
document.getElementById('di-tab-element')?.addEventListener('click', () => {
|
|
365
|
+
if (activeTab === 'element') return
|
|
366
|
+
activeTab = 'element'
|
|
367
|
+
updateTabRow(selectedEl)
|
|
368
|
+
if (selectedEl) renderPanel(selectedEl)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
document.getElementById('di-tab-class')?.addEventListener('click', () => {
|
|
372
|
+
if (activeTab === 'class' || !getSelectedClass(selectedEl)) return
|
|
373
|
+
activeTab = 'class'
|
|
374
|
+
updateTabRow(selectedEl)
|
|
375
|
+
if (selectedEl) renderPanel(selectedEl)
|
|
376
|
+
})
|
|
377
|
+
|
|
254
378
|
// ── Collapse toggle ────────────────────────────────────────────────────────
|
|
255
379
|
document.getElementById('di-collapse-btn')?.addEventListener('click', () => {
|
|
256
380
|
isCollapsed = !isCollapsed
|
|
257
381
|
panel.classList.toggle('collapsed', isCollapsed)
|
|
258
382
|
syncBodyMargin()
|
|
383
|
+
animateOverlayUpdate()
|
|
259
384
|
})
|
|
260
385
|
|
|
261
386
|
// ── Resize handle (drag left edge to resize panel width) ───────────────────
|
|
@@ -281,6 +406,7 @@ export function init() {
|
|
|
281
406
|
const newWidth = Math.min(580, Math.max(220, resizeStartWidth + dx))
|
|
282
407
|
panel.style.width = newWidth + 'px'
|
|
283
408
|
syncBodyMargin()
|
|
409
|
+
if (selectedEl) { updateOverlay(overlay, selectedEl); updateBoxModel(bm, selectedEl) }
|
|
284
410
|
})
|
|
285
411
|
|
|
286
412
|
document.addEventListener('mouseup', () => {
|
|
@@ -357,23 +483,37 @@ export function init() {
|
|
|
357
483
|
renderTooltip(tooltip, styles, e.clientX, e.clientY)
|
|
358
484
|
})
|
|
359
485
|
|
|
360
|
-
// ── Hover while selected: spacing
|
|
486
|
+
// ── Hover while selected: spacing + tooltip on self ──────────────────────
|
|
361
487
|
document.addEventListener('mousemove', (e) => {
|
|
362
488
|
if (inspecting || !selectedEl) {
|
|
363
489
|
if (hoverOverlay.style.display !== 'none') {
|
|
364
|
-
|
|
490
|
+
hideHoverOverlay()
|
|
365
491
|
hideSpacing(spacingContainer)
|
|
366
492
|
}
|
|
493
|
+
// Don't touch the tooltip here — inspect-mode handler owns it when inspecting
|
|
494
|
+
if (!inspecting) tooltip.style.display = 'none'
|
|
367
495
|
return
|
|
368
496
|
}
|
|
369
497
|
const el = e.target
|
|
370
|
-
if (isInspectorElement(el)
|
|
371
|
-
|
|
498
|
+
if (isInspectorElement(el)) {
|
|
499
|
+
hideHoverOverlay()
|
|
500
|
+
hideSpacing(spacingContainer)
|
|
501
|
+
tooltip.style.display = 'none'
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
if (el === selectedEl) {
|
|
505
|
+
// Hovering the selected element itself — show its tooltip, no spacing lines
|
|
506
|
+
hideHoverOverlay()
|
|
372
507
|
hideSpacing(spacingContainer)
|
|
508
|
+
const styles = getElementStyles(el)
|
|
509
|
+
renderTooltip(tooltip, styles, e.clientX, e.clientY)
|
|
373
510
|
return
|
|
374
511
|
}
|
|
512
|
+
// Hovering a different element — show spacing measurements, reposition badges
|
|
375
513
|
updateHoverOverlay(hoverOverlay, el)
|
|
376
514
|
updateSpacing(spacingContainer, selectedEl, el)
|
|
515
|
+
repositionBadges(el)
|
|
516
|
+
tooltip.style.display = 'none'
|
|
377
517
|
})
|
|
378
518
|
|
|
379
519
|
// ── Click: select element (works whenever panel is open OR inspect is active)
|
|
@@ -386,12 +526,25 @@ export function init() {
|
|
|
386
526
|
selectElement(el)
|
|
387
527
|
}, true)
|
|
388
528
|
|
|
389
|
-
// ──
|
|
529
|
+
// ── Keyboard shortcuts ─────────────────────────────────────────────────────
|
|
390
530
|
document.addEventListener('keydown', (e) => {
|
|
531
|
+
// Skip if user is typing inside an input / textarea / contenteditable
|
|
532
|
+
const tag = document.activeElement?.tagName.toLowerCase()
|
|
533
|
+
const isTyping = ['input', 'textarea', 'select'].includes(tag)
|
|
534
|
+
|| document.activeElement?.isContentEditable
|
|
535
|
+
|
|
391
536
|
if (e.key === 'Escape') {
|
|
392
537
|
if (inspecting) stopInspect()
|
|
393
538
|
else if (panelOpen) closePanel()
|
|
394
539
|
}
|
|
540
|
+
|
|
541
|
+
// i — toggle inspect mode while panel is open
|
|
542
|
+
if ((e.key === 'i' || e.key === 'I') && !isTyping && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
543
|
+
if (panelOpen) {
|
|
544
|
+
if (inspecting) stopInspect()
|
|
545
|
+
else startInspect()
|
|
546
|
+
}
|
|
547
|
+
}
|
|
395
548
|
})
|
|
396
549
|
|
|
397
550
|
// ── Fix 1: scroll — update overlay using fresh getBoundingClientRect ────────
|
|
@@ -406,9 +559,10 @@ export function init() {
|
|
|
406
559
|
tooltip.style.display = 'none'
|
|
407
560
|
hideBoxModel(bm)
|
|
408
561
|
}
|
|
409
|
-
// Clear hover overlay and
|
|
410
|
-
|
|
562
|
+
// Clear hover overlay, spacing, and tooltip on scroll (positions are stale)
|
|
563
|
+
hideHoverOverlay()
|
|
411
564
|
hideSpacing(spacingContainer)
|
|
565
|
+
tooltip.style.display = 'none'
|
|
412
566
|
}, true)
|
|
413
567
|
|
|
414
568
|
return {
|
|
@@ -421,6 +575,9 @@ export function init() {
|
|
|
421
575
|
bm.remove()
|
|
422
576
|
hoverOverlay.remove()
|
|
423
577
|
spacingContainer.remove()
|
|
578
|
+
classStyleEl?.remove()
|
|
579
|
+
classStyleEl = null
|
|
580
|
+
classRules = {}
|
|
424
581
|
document.getElementById('__dev_inspector_styles__')?.remove()
|
|
425
582
|
document.body.classList.remove('di-inspect-mode')
|
|
426
583
|
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
|
}
|