aetherx-ui-inspector 1.0.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/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import { init } from './src/core.js'
2
+
3
+ // Auto-init: just importing this file is enough.
4
+ // Only mounts if NODE_ENV is not 'production'.
5
+ if (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') {
6
+ if (typeof window !== 'undefined') {
7
+ if (document.readyState === 'loading') {
8
+ document.addEventListener('DOMContentLoaded', () => init())
9
+ } else {
10
+ init()
11
+ }
12
+ }
13
+ }
14
+
15
+ export { init }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "aetherx-ui-inspector",
3
+ "version": "1.0.0",
4
+ "description": "Universal dev-only UI inspector with live editing and feedback clipboard",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "module": "index.js",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./react": "./react/index.jsx",
11
+ "./vue": "./vue/index.vue",
12
+ "./auto": "./index.js"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "react",
17
+ "vue",
18
+ "index.js"
19
+ ],
20
+ "keywords": [
21
+ "dev",
22
+ "inspector",
23
+ "ui",
24
+ "debug",
25
+ "design",
26
+ "feedback",
27
+ "css"
28
+ ],
29
+ "license": "MIT",
30
+ "peerDependencies": {
31
+ "react": ">=17.0.0",
32
+ "vue": ">=3.0.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "react": { "optional": true },
36
+ "vue": { "optional": true }
37
+ }
38
+ }
@@ -0,0 +1,23 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { init } from '../src/core.js'
3
+
4
+ /**
5
+ * React wrapper for dev-inspector.
6
+ *
7
+ * Usage (e.g. in app/layout.jsx or _app.jsx):
8
+ * import DevInspector from 'dev-inspector/react'
9
+ * ...
10
+ * {process.env.NODE_ENV === 'development' && <DevInspector />}
11
+ */
12
+ export default function DevInspector() {
13
+ const instanceRef = useRef(null)
14
+
15
+ useEffect(() => {
16
+ instanceRef.current = init()
17
+ return () => {
18
+ instanceRef.current?.destroy()
19
+ }
20
+ }, [])
21
+
22
+ return null
23
+ }
package/src/core.js ADDED
@@ -0,0 +1,398 @@
1
+ import { injectStyles, createCogButton, createOverlay, createTooltip, createPanel, createBoxModelOverlay, updateOverlay, showCopiedFlash } from './ui.js'
2
+ import { isInspectorElement, getElementStyles, renderTooltip, updateBoxModel, hideBoxModel } from './inspector.js'
3
+ import { renderEditPanel, captureOriginals } from './editor.js'
4
+ import { createChangeTracker, copyToClipboard } from './feedback.js'
5
+
6
+ export function init() {
7
+ if (document.getElementById('__dev_inspector__')) return
8
+
9
+ injectStyles()
10
+
11
+ const cog = createCogButton()
12
+ const overlay = createOverlay()
13
+ const tooltip = createTooltip()
14
+ const panel = createPanel()
15
+ const bm = createBoxModelOverlay()
16
+ const tracker = createChangeTracker()
17
+
18
+ let inspecting = false
19
+ let panelOpen = false
20
+ let selectedEl = null
21
+ let selectedOriginals = null
22
+ let textEditCleanup = () => {}
23
+ let isCollapsed = false
24
+
25
+ // ── Body margin sync (panel pushes page instead of covering it) ────────────
26
+ document.body.style.transition = 'margin-right 0.22s cubic-bezier(.4,0,.2,1)'
27
+
28
+ function syncBodyMargin() {
29
+ if (!panelOpen) {
30
+ document.body.style.marginRight = ''
31
+ return
32
+ }
33
+ const width = isCollapsed ? 40 : panel.offsetWidth
34
+ document.body.style.marginRight = width + 'px'
35
+ }
36
+
37
+ // ── State helpers ──────────────────────────────────────────────────────────
38
+
39
+ function openPanel() {
40
+ panelOpen = true
41
+ panel.classList.add('open')
42
+ syncBodyMargin()
43
+ }
44
+
45
+ function closePanel() {
46
+ panelOpen = false
47
+ panel.classList.remove('open')
48
+ selectedEl = null
49
+ selectedOriginals = null
50
+ overlay.style.display = 'none'
51
+ setElementBar(null)
52
+ stopInspect(true)
53
+ syncBodyMargin()
54
+ }
55
+
56
+ function startInspect() {
57
+ inspecting = true
58
+ cog.classList.add('active')
59
+ document.body.classList.add('di-inspect-mode')
60
+ // Mark re-inspect button as active if panel is open
61
+ const reinspectBtn = document.getElementById('di-reinspect-btn')
62
+ if (reinspectBtn) reinspectBtn.classList.add('active')
63
+ }
64
+
65
+ function stopInspect(clearSelected = false) {
66
+ inspecting = false
67
+ cog.classList.remove('active')
68
+ document.body.classList.remove('di-inspect-mode')
69
+ tooltip.style.display = 'none'
70
+ hideBoxModel(bm)
71
+
72
+ const reinspectBtn = document.getElementById('di-reinspect-btn')
73
+ if (reinspectBtn) reinspectBtn.classList.remove('active')
74
+
75
+ if (clearSelected || !selectedEl) {
76
+ overlay.style.display = 'none'
77
+ overlay.classList.remove('selected')
78
+ } else {
79
+ // Re-show box model for the still-selected element
80
+ updateBoxModel(bm, selectedEl)
81
+ }
82
+ }
83
+
84
+ function selectElement(el) {
85
+ textEditCleanup()
86
+
87
+ // Stop inspect mode without hiding the selected-element overlay
88
+ inspecting = false
89
+ cog.classList.remove('active')
90
+ document.body.classList.remove('di-inspect-mode')
91
+ tooltip.style.display = 'none'
92
+ const reinspectBtn = document.getElementById('di-reinspect-btn')
93
+ if (reinspectBtn) reinspectBtn.classList.remove('active')
94
+
95
+ selectedEl = el
96
+ selectedOriginals = captureOriginals(el)
97
+
98
+ // Show green border overlay + box model on selected element
99
+ overlay.classList.remove('selected')
100
+ overlay.classList.add('selected')
101
+ updateOverlay(overlay, el)
102
+ updateBoxModel(bm, el)
103
+
104
+ // Update element bar
105
+ setElementBar(el)
106
+
107
+ renderPanel(el)
108
+ openPanel()
109
+ }
110
+
111
+ function setElementBar(el) {
112
+ const bar = document.getElementById('di-element-bar')
113
+ const label = document.getElementById('di-element-label')
114
+ if (!bar || !label) return
115
+ if (!el) {
116
+ bar.style.display = 'none'
117
+ label.textContent = ''
118
+ return
119
+ }
120
+ const tag = el.tagName.toLowerCase()
121
+ const id = el.id ? `#${el.id}` : ''
122
+ const cls = [...el.classList]
123
+ .filter(c => !c.startsWith('di-') && !c.startsWith('__dev'))
124
+ .slice(0, 2)
125
+ .map(c => `.${c}`)
126
+ .join('')
127
+ label.textContent = `${tag}${id}${cls}`
128
+ bar.style.display = 'flex'
129
+ }
130
+
131
+ function renderPanel(el) {
132
+ textEditCleanup()
133
+ const content = document.getElementById('di-panel-content')
134
+ const footer = document.getElementById('di-panel-footer')
135
+
136
+ if (!el) {
137
+ content.innerHTML = `
138
+ <div class="di-panel-empty">
139
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
140
+ <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
141
+ </svg>
142
+ <p>Click any element on the page to inspect and edit it</p>
143
+ </div>`
144
+ if (footer) footer.style.display = 'none'
145
+ return
146
+ }
147
+
148
+ if (footer) footer.style.display = 'flex'
149
+ updateFeedbackCount()
150
+
151
+ textEditCleanup = renderEditPanel(
152
+ content,
153
+ el,
154
+ selectedOriginals,
155
+ // onChange
156
+ (el, prop, from, to) => {
157
+ tracker.track(el, prop, from, to)
158
+ updateFeedbackCount()
159
+ if (selectedEl) { updateOverlay(overlay, selectedEl); updateBoxModel(bm, selectedEl) }
160
+ },
161
+ // onReset
162
+ (el, prop, restoredVal) => {
163
+ tracker.remove(el, prop)
164
+ updateFeedbackCount()
165
+ if (selectedEl) { updateOverlay(overlay, selectedEl); updateBoxModel(bm, selectedEl) }
166
+ }
167
+ )
168
+ }
169
+
170
+ let changesLogOpen = false
171
+
172
+ function updateFeedbackCount() {
173
+ const count = tracker.count()
174
+ const countEl = document.getElementById('di-feedback-count')
175
+ const toggleEl = document.getElementById('di-changes-toggle')
176
+ const arrowEl = document.getElementById('di-toggle-arrow')
177
+
178
+ if (countEl) {
179
+ countEl.textContent = count > 0
180
+ ? `${count} change${count === 1 ? '' : 's'} tracked`
181
+ : 'No changes yet'
182
+ }
183
+ if (toggleEl) toggleEl.classList.toggle('has-changes', count > 0)
184
+
185
+ // Re-render log content if it's currently open
186
+ if (changesLogOpen) renderChangesLog()
187
+
188
+ // Close log automatically if all changes cleared
189
+ if (count === 0 && changesLogOpen) {
190
+ changesLogOpen = false
191
+ const log = document.getElementById('di-changes-log')
192
+ if (log) log.style.display = 'none'
193
+ if (arrowEl) arrowEl.classList.remove('open')
194
+ }
195
+ }
196
+
197
+ function renderChangesLog() {
198
+ const list = document.getElementById('di-changes-list')
199
+ if (!list) return
200
+
201
+ const changes = tracker.getAll()
202
+ if (changes.length === 0) { list.innerHTML = ''; return }
203
+
204
+ // Group by selector
205
+ const grouped = {}
206
+ changes.forEach(({ selector, property, from, to }) => {
207
+ if (!grouped[selector]) grouped[selector] = []
208
+ grouped[selector].push({ property, from, to })
209
+ })
210
+
211
+ list.innerHTML = Object.entries(grouped).map(([selector, props]) => `
212
+ <div class="di-change-group">
213
+ <div class="di-change-selector">${selector}</div>
214
+ ${props.map(({ property, from, to }) => `
215
+ <div class="di-change-row">
216
+ <span class="di-change-prop">${property}</span>
217
+ <span class="di-change-from">${from || '—'}</span>
218
+ <span class="di-change-arrow">→</span>
219
+ <span class="di-change-to">${to || '—'}</span>
220
+ </div>`).join('')}
221
+ </div>`).join('')
222
+ }
223
+
224
+ // ── Cog button ─────────────────────────────────────────────────────────────
225
+ cog.addEventListener('click', () => {
226
+ if (panelOpen) {
227
+ closePanel()
228
+ } else {
229
+ openPanel()
230
+ startInspect()
231
+ }
232
+ })
233
+
234
+ // ── Panel close button ─────────────────────────────────────────────────────
235
+ document.getElementById('di-close-panel')?.addEventListener('click', closePanel)
236
+
237
+ // ── Fix 2: Re-inspect button ───────────────────────────────────────────────
238
+ document.getElementById('di-reinspect-btn')?.addEventListener('click', () => {
239
+ if (inspecting) {
240
+ stopInspect()
241
+ } else {
242
+ startInspect()
243
+ }
244
+ })
245
+
246
+ // ── Collapse toggle ────────────────────────────────────────────────────────
247
+ document.getElementById('di-collapse-btn')?.addEventListener('click', () => {
248
+ isCollapsed = !isCollapsed
249
+ panel.classList.toggle('collapsed', isCollapsed)
250
+ syncBodyMargin()
251
+ })
252
+
253
+ // ── Resize handle (drag left edge to resize panel width) ───────────────────
254
+ const resizeHandle = document.getElementById('di-resize-handle')
255
+ if (resizeHandle) {
256
+ let isResizing = false
257
+ let resizeStartX = 0
258
+ let resizeStartWidth = 0
259
+
260
+ resizeHandle.addEventListener('mousedown', (e) => {
261
+ isResizing = true
262
+ resizeStartX = e.clientX
263
+ resizeStartWidth = panel.offsetWidth
264
+ resizeHandle.classList.add('dragging')
265
+ document.body.style.cursor = 'ew-resize'
266
+ document.body.style.userSelect = 'none'
267
+ e.preventDefault()
268
+ })
269
+
270
+ document.addEventListener('mousemove', (e) => {
271
+ if (!isResizing) return
272
+ const dx = resizeStartX - e.clientX // drag left = wider
273
+ const newWidth = Math.min(580, Math.max(220, resizeStartWidth + dx))
274
+ panel.style.width = newWidth + 'px'
275
+ syncBodyMargin()
276
+ })
277
+
278
+ document.addEventListener('mouseup', () => {
279
+ if (!isResizing) return
280
+ isResizing = false
281
+ resizeHandle.classList.remove('dragging')
282
+ document.body.style.cursor = ''
283
+ document.body.style.userSelect = ''
284
+ })
285
+ }
286
+
287
+ // ── Mouse wheel on changes log ────────────────────────────────────────────
288
+ panel.addEventListener('wheel', (e) => {
289
+ const log = document.getElementById('di-changes-log')
290
+ if (log && log.contains(e.target) && log.style.display !== 'none') {
291
+ log.scrollTop += e.deltaY
292
+ e.stopPropagation()
293
+ e.preventDefault()
294
+ return
295
+ }
296
+ }, { passive: false, capture: true })
297
+
298
+ // ── Mouse wheel scroll ────────────────────────────────────────────────────
299
+ // #di-panel-content is the direct flex child that scrolls.
300
+ // We intercept wheel on the whole panel and drive scrollTop manually so
301
+ // the event never reaches the page underneath.
302
+ panel.addEventListener('wheel', (e) => {
303
+ const content = document.getElementById('di-panel-content')
304
+ if (!content) return
305
+ content.scrollTop += e.deltaY
306
+ e.stopPropagation()
307
+ e.preventDefault()
308
+ }, { passive: false })
309
+
310
+ // ── Changes log toggle (Fix 3) ─────────────────────────────────────────────
311
+ document.getElementById('di-changes-toggle')?.addEventListener('click', () => {
312
+ if (tracker.count() === 0) return
313
+ changesLogOpen = !changesLogOpen
314
+ const log = document.getElementById('di-changes-log')
315
+ const arrow = document.getElementById('di-toggle-arrow')
316
+ if (log) log.style.display = changesLogOpen ? 'block' : 'none'
317
+ if (arrow) arrow.classList.toggle('open', changesLogOpen)
318
+ if (changesLogOpen) renderChangesLog()
319
+ })
320
+
321
+ // ── Copy feedback ──────────────────────────────────────────────────────────
322
+ document.getElementById('di-copy-feedback')?.addEventListener('click', async () => {
323
+ const text = tracker.formatAsText()
324
+ const ok = await copyToClipboard(text)
325
+ showCopiedFlash(ok ? 'Feedback copied to clipboard!' : 'Copy failed — check browser permissions')
326
+ })
327
+
328
+ // ── Clear changes ──────────────────────────────────────────────────────────
329
+ document.getElementById('di-clear-changes')?.addEventListener('click', () => {
330
+ tracker.clear()
331
+ updateFeedbackCount()
332
+ if (selectedEl) renderPanel(selectedEl)
333
+ })
334
+
335
+ // ── Mouse move: hover highlight ────────────────────────────────────────────
336
+ document.addEventListener('mousemove', (e) => {
337
+ if (!inspecting) return
338
+ const el = e.target
339
+ if (isInspectorElement(el)) {
340
+ if (!selectedEl) overlay.style.display = 'none'
341
+ tooltip.style.display = 'none'
342
+ hideBoxModel(bm)
343
+ return
344
+ }
345
+ overlay.classList.remove('selected')
346
+ updateOverlay(overlay, el)
347
+ updateBoxModel(bm, el)
348
+ const styles = getElementStyles(el)
349
+ renderTooltip(tooltip, styles, e.clientX, e.clientY)
350
+ })
351
+
352
+ // ── Click: select element ──────────────────────────────────────────────────
353
+ document.addEventListener('click', (e) => {
354
+ if (!inspecting) return
355
+ const el = e.target
356
+ if (isInspectorElement(el)) return
357
+ e.preventDefault()
358
+ e.stopPropagation()
359
+ selectElement(el)
360
+ }, true)
361
+
362
+ // ── Escape key ─────────────────────────────────────────────────────────────
363
+ document.addEventListener('keydown', (e) => {
364
+ if (e.key === 'Escape') {
365
+ if (inspecting) stopInspect()
366
+ else if (panelOpen) closePanel()
367
+ }
368
+ })
369
+
370
+ // ── Fix 1: scroll — update overlay using fresh getBoundingClientRect ────────
371
+ // Since overlay is position:fixed, getBoundingClientRect gives us exactly what we need.
372
+ document.addEventListener('scroll', () => {
373
+ if (selectedEl && panelOpen) {
374
+ updateOverlay(overlay, selectedEl)
375
+ updateBoxModel(bm, selectedEl)
376
+ }
377
+ if (inspecting) {
378
+ overlay.style.display = 'none'
379
+ tooltip.style.display = 'none'
380
+ hideBoxModel(bm)
381
+ }
382
+ }, true)
383
+
384
+ return {
385
+ destroy() {
386
+ textEditCleanup()
387
+ cog.remove()
388
+ overlay.remove()
389
+ tooltip.remove()
390
+ panel.remove()
391
+ document.getElementById('__dev_inspector_styles__')?.remove()
392
+ document.body.classList.remove('di-inspect-mode')
393
+ document.body.style.marginRight = ''
394
+ document.body.style.transition = ''
395
+ bm.remove()
396
+ }
397
+ }
398
+ }