aetherx-ui-inspector 1.2.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aetherx-ui-inspector",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Universal dev-only UI inspector with live editing and feedback clipboard",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/src/core.js CHANGED
@@ -1,8 +1,12 @@
1
1
  import { injectStyles, createCogButton, createOverlay, createTooltip, createPanel, createBoxModelOverlay, updateOverlay, showCopiedFlash, createHoverOverlay, updateHoverOverlay, createSpacingOverlay, hideSpacing, updateSpacing } from './ui.js'
2
- import { isInspectorElement, getElementStyles, renderTooltip, updateBoxModel, hideBoxModel } from './inspector.js'
2
+ import { isInspectorElement, getElementStyles, getVueComponent, renderTooltip, updateBoxModel, hideBoxModel } from './inspector.js'
3
3
  import { renderEditPanel, captureOriginals } from './editor.js'
4
4
  import { createChangeTracker, copyToClipboard } from './feedback.js'
5
5
 
6
+ const isLocalhost = ['localhost', '127.0.0.1', '::1'].includes(location.hostname)
7
+ || location.hostname.endsWith('.local')
8
+ || /^\d+\.\d+\.\d+\.\d+$/.test(location.hostname) // any IP (dev servers)
9
+
6
10
  export function init() {
7
11
  if (document.getElementById('__dev_inspector__')) return
8
12
 
@@ -207,6 +211,7 @@ export function init() {
207
211
  bar.style.display = 'none'
208
212
  label.textContent = ''
209
213
  updateTabRow(null)
214
+ updateVueBar(null)
210
215
  return
211
216
  }
212
217
  const tag = el.tagName.toLowerCase()
@@ -219,6 +224,7 @@ export function init() {
219
224
  label.textContent = `${tag}${id}${cls}`
220
225
  bar.style.display = 'flex'
221
226
  updateTabRow(el)
227
+ updateVueBar(el)
222
228
  }
223
229
 
224
230
  function updateTabRow(el) {
@@ -235,6 +241,32 @@ export function init() {
235
241
  classTab?.classList.toggle('active', activeTab === 'class')
236
242
  }
237
243
 
244
+ function updateVueBar(el) {
245
+ const bar = document.getElementById('di-vue-bar')
246
+ const nameEl = document.getElementById('di-vue-comp-name')
247
+ const vscodeBtn = document.getElementById('di-vscode-btn')
248
+ if (!bar) return
249
+ const comp = el ? getVueComponent(el) : null
250
+ if (!comp) { bar.style.display = 'none'; return }
251
+ bar.style.display = 'flex'
252
+ if (nameEl) nameEl.textContent = `<${comp.name}>`
253
+ if (vscodeBtn) {
254
+ // Prefer vite-plugin-vue-inspector annotation (path:line:col) for exact jump
255
+ const vInspector = el.closest?.('[data-v-inspector]')?.dataset?.vInspector
256
+ let fileTarget = null
257
+ if (vInspector) fileTarget = `vscode://file/${vInspector}`
258
+ else if (comp.file) fileTarget = `vscode://file/${comp.file}`
259
+
260
+ if (isLocalhost && fileTarget) {
261
+ vscodeBtn.style.display = 'flex'
262
+ vscodeBtn.dataset.href = fileTarget
263
+ } else {
264
+ vscodeBtn.style.display = 'none'
265
+ delete vscodeBtn.dataset.href
266
+ }
267
+ }
268
+ }
269
+
238
270
  function renderPanel(el) {
239
271
  textEditCleanup()
240
272
  const content = document.getElementById('di-panel-content')
@@ -360,6 +392,13 @@ export function init() {
360
392
  }
361
393
  })
362
394
 
395
+ // ── VS Code link ───────────────────────────────────────────────────────────
396
+ document.getElementById('di-vscode-btn')?.addEventListener('click', () => {
397
+ const btn = document.getElementById('di-vscode-btn')
398
+ const href = btn?.dataset?.href
399
+ if (href) window.open(href)
400
+ })
401
+
363
402
  // ── Mode tabs ──────────────────────────────────────────────────────────────
364
403
  document.getElementById('di-tab-element')?.addEventListener('click', () => {
365
404
  if (activeTab === 'element') return
package/src/inspector.js CHANGED
@@ -18,6 +18,20 @@ export function isInspectorElement(el) {
18
18
  return false
19
19
  }
20
20
 
21
+ export function getVueComponent(el) {
22
+ let node = el
23
+ while (node && node !== document.documentElement) {
24
+ const instance = node.__vueParentComponent
25
+ if (instance?.type && typeof instance.type === 'object') {
26
+ const name = instance.type.__name || instance.type.name || null
27
+ const file = instance.type.__file || null
28
+ if (name || file) return { name: name || 'Anonymous', file }
29
+ }
30
+ node = node.parentElement
31
+ }
32
+ return null
33
+ }
34
+
21
35
  export function getElementStyles(el) {
22
36
  const cs = window.getComputedStyle(el)
23
37
  const rect = el.getBoundingClientRect()
@@ -43,6 +57,7 @@ export function getElementStyles(el) {
43
57
  tag,
44
58
  isSvg: el instanceof SVGElement,
45
59
  textContent,
60
+ vueComponent: getVueComponent(el),
46
61
  size: {
47
62
  width: Math.round(rect.width) + 'px',
48
63
  height: Math.round(rect.height) + 'px',
@@ -197,7 +212,7 @@ export function updateBoxModel(bm, el) {
197
212
  }
198
213
 
199
214
  export function renderTooltip(tooltip, styles, x, y) {
200
- const { label, size, typography, background, spacing, border, layout, textContent, isSvg } = styles
215
+ const { label, size, typography, background, spacing, border, layout, textContent, isSvg, vueComponent } = styles
201
216
 
202
217
  const colorDot = (hex) => hex
203
218
  ? `<span class="di-swatch" style="background:${hex};"></span>`
@@ -213,6 +228,15 @@ export function renderTooltip(tooltip, styles, x, y) {
213
228
 
214
229
  let html = `<div class="di-tag">${label}</div>`
215
230
 
231
+ if (vueComponent) {
232
+ html += `<div class="di-section">Vue</div>`
233
+ html += row('component', `<span class="di-vue-name">&lt;${vueComponent.name}&gt;</span>`)
234
+ if (vueComponent.file) {
235
+ const fileName = vueComponent.file.replace(/\\/g, '/').split('/').pop()
236
+ html += row('file', fileName)
237
+ }
238
+ }
239
+
216
240
  if (textContent) {
217
241
  html += row('text', `"${textContent.length > 28 ? textContent.slice(0,28)+'…' : textContent}"`)
218
242
  }
package/src/ui.js CHANGED
@@ -99,6 +99,7 @@ export function injectStyles() {
99
99
  vertical-align: middle;
100
100
  margin-right: 4px;
101
101
  }
102
+ #__dev_inspector_tooltip__ .di-vue-name { color: #a78bfa; font-weight: 600; }
102
103
 
103
104
  /* ── Side edit panel ── */
104
105
  #__dev_inspector_panel__ {
@@ -128,6 +129,7 @@ export function injectStyles() {
128
129
  min-width: 40px;
129
130
  }
130
131
  #__dev_inspector_panel__.collapsed .di-element-bar,
132
+ #__dev_inspector_panel__.collapsed .di-vue-bar,
131
133
  #__dev_inspector_panel__.collapsed .di-tab-row,
132
134
  #__dev_inspector_panel__.collapsed #di-panel-content,
133
135
  #__dev_inspector_panel__.collapsed .di-panel-footer,
@@ -242,6 +244,47 @@ export function injectStyles() {
242
244
  .di-reinspect-btn:hover { border-color: #6366f1; color: #818cf8; background: #1e1e2e; }
243
245
  .di-reinspect-btn.active { border-color: #6366f1; color: #818cf8; background: #1e1e2e; }
244
246
 
247
+ /* ── Vue component bar ── */
248
+ .di-vue-bar {
249
+ padding: 5px 16px;
250
+ border-bottom: 1px solid #27272a;
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 8px;
254
+ flex-shrink: 0;
255
+ background: #16162a;
256
+ }
257
+ .di-vue-icon { font-size: 11px; color: #7c3aed; flex-shrink: 0; }
258
+ .di-vue-comp-name {
259
+ flex: 1;
260
+ font-family: 'SF Mono', monospace;
261
+ font-size: 11px;
262
+ color: #a78bfa;
263
+ font-weight: 600;
264
+ overflow: hidden;
265
+ text-overflow: ellipsis;
266
+ white-space: nowrap;
267
+ }
268
+ .di-vscode-btn {
269
+ flex-shrink: 0;
270
+ padding: 3px 8px;
271
+ background: #1e2433;
272
+ border: 1px solid #1e3a5f;
273
+ border-radius: 6px;
274
+ color: #5b9bd5;
275
+ font-size: 10px;
276
+ font-weight: 500;
277
+ cursor: pointer;
278
+ display: flex;
279
+ align-items: center;
280
+ gap: 4px;
281
+ transition: border-color 0.12s, color 0.12s, background 0.12s;
282
+ font-family: system-ui, sans-serif;
283
+ white-space: nowrap;
284
+ }
285
+ .di-vscode-btn:hover { border-color: #007acc; color: #fff; background: #007acc; }
286
+ .di-vscode-btn svg { flex-shrink: 0; }
287
+
245
288
  .di-panel-empty {
246
289
  flex: 1;
247
290
  display: flex;
@@ -784,6 +827,16 @@ export function createPanel() {
784
827
  <span class="di-element-label" id="di-element-label"></span>
785
828
  <button class="di-reinspect-btn" id="di-reinspect-btn">↖ Pick</button>
786
829
  </div>
830
+ <div class="di-vue-bar" id="di-vue-bar" style="display:none;">
831
+ <span class="di-vue-icon">⬡</span>
832
+ <span class="di-vue-comp-name" id="di-vue-comp-name"></span>
833
+ <button class="di-vscode-btn" id="di-vscode-btn" style="display:none;" title="Open in VS Code">
834
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor">
835
+ <path d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"/>
836
+ </svg>
837
+ Open in VS Code
838
+ </button>
839
+ </div>
787
840
  <div class="di-tab-row" id="di-tab-row" style="display:none;">
788
841
  <button class="di-tab active" id="di-tab-element">Element</button>
789
842
  <button class="di-tab" id="di-tab-class"><span id="di-tab-class-label">.class</span></button>
@@ -827,6 +880,20 @@ export function createBoxModelOverlay() {
827
880
  return container
828
881
  }
829
882
 
883
+ function getVueBadgeLabel(el) {
884
+ const instance = el.__vueParentComponent
885
+ if (instance?.type && typeof instance.type === 'object') {
886
+ const name = instance.type.__name || instance.type.name
887
+ if (name) return `<${name}>`
888
+ }
889
+ const tag = el.tagName.toLowerCase()
890
+ const id = el.id ? `#${el.id}` : ''
891
+ const cls = [...el.classList]
892
+ .filter(c => !c.startsWith('di-') && !c.startsWith('__dev'))
893
+ .slice(0, 2).map(c => `.${c}`).join('')
894
+ return `${tag}${id}${cls}`
895
+ }
896
+
830
897
  // Fix 1: use raw viewport coords (fixed positioning needs no scrollY offset)
831
898
  export function updateOverlay(overlay, el) {
832
899
  if (!el) { overlay.style.display = 'none'; return }
@@ -838,12 +905,7 @@ export function updateOverlay(overlay, el) {
838
905
  overlay.style.height = rect.height + 'px'
839
906
  const badge = overlay.querySelector('.di-overlay-badge')
840
907
  if (badge) {
841
- const tag = el.tagName.toLowerCase()
842
- const id = el.id ? `#${el.id}` : ''
843
- const cls = [...el.classList]
844
- .filter(c => !c.startsWith('di-') && !c.startsWith('__dev'))
845
- .slice(0, 2).map(c => `.${c}`).join('')
846
- badge.textContent = `${tag}${id}${cls}`
908
+ badge.textContent = getVueBadgeLabel(el)
847
909
  // Selected badge always sits above the element (static position)
848
910
  badge.style.top = '-22px'
849
911
  badge.style.borderRadius = '4px 4px 0 0'
@@ -871,12 +933,7 @@ export function updateHoverOverlay(overlay, el) {
871
933
  overlay.style.height = rect.height + 'px'
872
934
  const badge = overlay.querySelector('.di-overlay-badge')
873
935
  if (badge) {
874
- const tag = el.tagName.toLowerCase()
875
- const id = el.id ? `#${el.id}` : ''
876
- const cls = [...el.classList]
877
- .filter(c => !c.startsWith('di-') && !c.startsWith('__dev'))
878
- .slice(0, 2).map(c => `.${c}`).join('')
879
- badge.textContent = `${tag}${id}${cls}`
936
+ badge.textContent = getVueBadgeLabel(el)
880
937
  // Hover badge always sits below the highlighted element
881
938
  badge.style.top = 'calc(100% + 2px)'
882
939
  badge.style.borderRadius = '0 0 4px 4px'