one 1.10.0 → 1.10.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.
Files changed (29) hide show
  1. package/devtools/devtools.mjs +14 -1
  2. package/devtools/source-inspector.mjs +669 -10
  3. package/dist/cjs/router/interceptRoutes.cjs +15 -1
  4. package/dist/cjs/router/interceptRoutes.js +14 -1
  5. package/dist/cjs/router/interceptRoutes.js.map +1 -1
  6. package/dist/cjs/router/interceptRoutes.native.js +15 -1
  7. package/dist/cjs/router/interceptRoutes.native.js.map +1 -1
  8. package/dist/cjs/vite/plugins/devtoolsPlugin.cjs +18 -5
  9. package/dist/cjs/vite/plugins/devtoolsPlugin.js +13 -4
  10. package/dist/cjs/vite/plugins/devtoolsPlugin.js.map +2 -2
  11. package/dist/cjs/vite/plugins/devtoolsPlugin.native.js +20 -6
  12. package/dist/cjs/vite/plugins/devtoolsPlugin.native.js.map +1 -1
  13. package/dist/esm/router/interceptRoutes.js +14 -1
  14. package/dist/esm/router/interceptRoutes.js.map +1 -1
  15. package/dist/esm/router/interceptRoutes.mjs +15 -1
  16. package/dist/esm/router/interceptRoutes.mjs.map +1 -1
  17. package/dist/esm/router/interceptRoutes.native.js +15 -1
  18. package/dist/esm/router/interceptRoutes.native.js.map +1 -1
  19. package/dist/esm/vite/plugins/devtoolsPlugin.js +3 -1
  20. package/dist/esm/vite/plugins/devtoolsPlugin.js.map +1 -1
  21. package/dist/esm/vite/plugins/devtoolsPlugin.mjs +3 -1
  22. package/dist/esm/vite/plugins/devtoolsPlugin.mjs.map +1 -1
  23. package/dist/esm/vite/plugins/devtoolsPlugin.native.js +5 -2
  24. package/dist/esm/vite/plugins/devtoolsPlugin.native.js.map +1 -1
  25. package/package.json +9 -9
  26. package/src/router/interceptRoutes.ts +34 -5
  27. package/src/vite/plugins/devtoolsPlugin.ts +3 -1
  28. package/types/router/interceptRoutes.d.ts.map +1 -1
  29. package/types/vite/plugins/devtoolsPlugin.d.ts.map +1 -1
@@ -11,11 +11,18 @@
11
11
  let shadow = null
12
12
  let overlay = null
13
13
  let tag = null
14
+ let pickerDialog = null
14
15
  let holdTimer = null
15
16
  const holdDelay = 800
16
17
  let otherKeyPressed = false
17
18
  const mousePos = { x: 0, y: 0 }
18
19
  let removalObserver = null
20
+ let currentElementChain = []
21
+ let recording = false
22
+ let recordEvents = []
23
+ let recordStartTime = 0
24
+ let recordFrameCount = 0
25
+ const recordThrottle = 3
19
26
 
20
27
  // set up HMR listener for cursor position from vscode
21
28
  const cursorHot = createHotContext('/__one_cursor_hmr')
@@ -62,16 +69,650 @@
62
69
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
63
70
  display: none;
64
71
  }
72
+ dialog { border: none; padding: 0; background: transparent; max-width: none; max-height: none; overflow: visible; }
73
+ dialog::backdrop { background: rgba(0,0,0,0.01); }
74
+ .picker-dialog { position: fixed; margin: 0; z-index: 2147483647; }
75
+ .picker { width: 320px; max-width: calc(100vw - 20px); background: #161616; border-radius: 8px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden; font: 12px system-ui, sans-serif; color: #ccc; }
76
+ .picker-header { display: flex; align-items: center; justify-content: flex-end; padding: 6px 10px; background: #1a1a1a; gap: 8px; border-bottom: 1px solid #252525; }
77
+ .picker-close { background: none; border: none; color: #666; cursor: pointer; padding: 2px 4px; font-size: 14px; line-height: 1; }
78
+ .picker-close:hover { color: #fff; }
79
+ .picker-shortcut { font-size: 11px; color: #888; padding: 4px 8px; cursor: pointer; border-radius: 4px; margin-left: auto; }
80
+ .picker-shortcut:hover { color: #fff; background: #252525; }
81
+ .picker-actions { display: flex; background: #1a1a1a; border-top: 1px solid #252525; border-bottom: 1px solid #252525; }
82
+ .picker-btn { flex: 1; background: none; border: none; border-right: 1px solid #252525; color: #888; padding: 8px; font: 12px system-ui, sans-serif; cursor: pointer; transition: color 0.1s; }
83
+ .picker-btn:last-child { border-right: none; }
84
+ .picker-btn:hover { color: #fff; }
85
+ .picker-btn.copied { color: #4ade80; }
86
+ .picker-btn { position: relative; }
87
+ .rec-tooltip { display: none; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: #222; border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); padding: 6px 10px; margin-bottom: 4px; font: 11px system-ui, sans-serif; color: #888; white-space: nowrap; pointer-events: none; }
88
+ .picker-btn:hover .rec-tooltip { display: block; }
89
+ .picker-btn .rec-dot { display: inline-block; width: 8px; height: 8px; background: #f55; border-radius: 50%; }
90
+ .toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(20,20,20,0.95); border-radius: 12px; padding: 24px 32px; text-align: center; z-index: 2147483647; box-shadow: 0 8px 32px rgba(0,0,0,0.5); pointer-events: none; font-family: system-ui, sans-serif; }
91
+ .toast-big { font-size: 48px; font-weight: 600; color: #f55; }
92
+ .toast-big.success { color: #4ade80; }
93
+ .toast-hint { font-size: 12px; color: #888; margin-top: 12px; }
94
+ .picker-list { max-height: 240px; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: #333 transparent; }
95
+ .picker-list::-webkit-scrollbar { width: 4px; }
96
+ .picker-list::-webkit-scrollbar-track { background: transparent; }
97
+ .picker-list::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
98
+ .picker-item { display: flex; align-items: center; padding: 8px 10px; color: #999; cursor: pointer; transition: background 0.08s; border-bottom: 1px solid #1e1e1e; gap: 8px; }
99
+ .picker-item:last-child { border-bottom: none; }
100
+ .picker-item:hover { background: #252525; color: #fff; }
101
+ .picker-item-name { font-weight: 500; color: #ddd; min-width: 60px; }
102
+ .picker-item-file { flex: 1; font-size: 10px; color: #555; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; direction: rtl; text-align: left; }
65
103
  </style>
66
104
  <div class="overlay"></div>
67
105
  <div class="tag"></div>
106
+ <dialog class="picker-dialog" id="picker-dialog">
107
+ <div class="picker" id="picker">
108
+ <div class="picker-header">
109
+ <span class="picker-shortcut">\u2325space</span>
110
+ <button class="picker-close" id="picker-close">\u00d7</button>
111
+ </div>
112
+ <div class="picker-actions" id="picker-actions">
113
+ <button class="picker-btn" data-action="open">Open</button>
114
+ <button class="picker-btn" data-action="copy">Copy</button>
115
+ <button class="picker-btn" data-action="record" id="record-btn"><span class="rec-dot"></span><span class="rec-tooltip">Tap Option to stop</span></button>
116
+ </div>
117
+ <div class="picker-list" id="picker-list"></div>
118
+ </div>
119
+ </dialog>
68
120
  `
69
121
  document.body.appendChild(host)
70
122
  overlay = shadow.querySelector('.overlay')
71
123
  tag = shadow.querySelector('.tag')
124
+ pickerDialog = shadow.getElementById('picker-dialog')
125
+ setupPicker()
72
126
  setupRemovalObserver()
73
127
  }
74
128
 
129
+ function setupPicker() {
130
+ const closeBtn = shadow.getElementById('picker-close')
131
+ const shortcut = shadow.querySelector('.picker-shortcut')
132
+ const actions = shadow.getElementById('picker-actions')
133
+ const list = shadow.getElementById('picker-list')
134
+
135
+ closeBtn.addEventListener('click', () => {
136
+ pickerDialog.close()
137
+ })
138
+
139
+ // click shortcut to open devtools menu
140
+ shortcut.addEventListener('click', () => {
141
+ pickerDialog.close()
142
+ window.dispatchEvent(new CustomEvent('one-open-devtools'))
143
+ })
144
+
145
+ // click backdrop to close
146
+ pickerDialog.addEventListener('click', (e) => {
147
+ if (e.target === pickerDialog) pickerDialog.close()
148
+ })
149
+
150
+ actions.addEventListener('click', (e) => {
151
+ const btn = e.target.closest('.picker-btn')
152
+ if (!btn) return
153
+ const action = btn.dataset.action
154
+
155
+ if (action === 'open') {
156
+ if (!currentElementChain.length) return
157
+ fetch(
158
+ '/__one/open-source?source=' +
159
+ encodeURIComponent(currentElementChain[0].source)
160
+ )
161
+ pickerDialog.close()
162
+ } else if (action === 'copy') {
163
+ if (!currentElementChain.length) return
164
+ navigator.clipboard.writeText(buildSnapshot())
165
+ flashCopied(btn)
166
+ } else if (action === 'record') {
167
+ if (recording) {
168
+ stopRecording()
169
+ } else {
170
+ startRecording()
171
+ }
172
+ }
173
+ })
174
+
175
+ list.addEventListener('click', (e) => {
176
+ const item = e.target.closest('.picker-item')
177
+ if (!item) return
178
+ const source = item.dataset.source
179
+ if (source) {
180
+ fetch('/__one/open-source?source=' + encodeURIComponent(source))
181
+ pickerDialog.close()
182
+ }
183
+ })
184
+ }
185
+
186
+ function flashCopied(btn) {
187
+ btn.classList.add('copied')
188
+ const orig = btn.textContent
189
+ btn.textContent = 'Copied!'
190
+ setTimeout(() => {
191
+ btn.classList.remove('copied')
192
+ btn.textContent = orig
193
+ }, 1000)
194
+ }
195
+
196
+ function generateSelector(el) {
197
+ const parts = []
198
+ let current = el
199
+ while (current && current !== document.body) {
200
+ if (current.id) {
201
+ parts.unshift('#' + current.id)
202
+ break
203
+ }
204
+ let selector = current.tagName.toLowerCase()
205
+ if (current.className && typeof current.className === 'string') {
206
+ const classes = current.className.trim().split(/\s+/).slice(0, 2)
207
+ if (classes.length && classes[0]) {
208
+ selector += '.' + classes.join('.')
209
+ }
210
+ }
211
+ // add nth-child if needed for specificity
212
+ const parent = current.parentElement
213
+ if (parent) {
214
+ const siblings = Array.from(parent.children).filter(
215
+ (c) => c.tagName === current.tagName
216
+ )
217
+ if (siblings.length > 1) {
218
+ const idx = siblings.indexOf(current) + 1
219
+ selector += ':nth-child(' + idx + ')'
220
+ }
221
+ }
222
+ parts.unshift(selector)
223
+ current = current.parentElement
224
+ }
225
+ return parts.join(' > ')
226
+ }
227
+
228
+ // recording functionality
229
+ let recordHandlers = {}
230
+
231
+ function getRecordSelector(el) {
232
+ if (!el || el === document.body || el === document.documentElement) return null
233
+ let current = el
234
+ let path = []
235
+ let anchor = null
236
+
237
+ while (current && current !== document.body) {
238
+ const id = current.id
239
+ const testId = current.getAttribute('data-testid')
240
+ const oneSource = current.getAttribute('data-one-source')
241
+
242
+ if (id || testId || oneSource) {
243
+ anchor = { id, testId, oneSource }
244
+ break
245
+ }
246
+
247
+ let seg = current.tagName.toLowerCase()
248
+ if (current.className && typeof current.className === 'string') {
249
+ const cls = current.className
250
+ .split(/\s+/)
251
+ .filter((c) => c && !c.startsWith('_'))[0]
252
+ if (cls) seg += '.' + cls
253
+ }
254
+ path.unshift(seg)
255
+ current = current.parentElement
256
+ }
257
+
258
+ const pathStr = path.slice(-3).join(' > ')
259
+
260
+ if (anchor) {
261
+ const ref = anchor.id
262
+ ? `#${anchor.id}`
263
+ : anchor.testId
264
+ ? `[data-testid="${anchor.testId}"]`
265
+ : `[data-one-source="${anchor.oneSource}"]`
266
+ return pathStr ? `${ref} > ${pathStr}` : ref
267
+ }
268
+
269
+ return pathStr || el.tagName.toLowerCase()
270
+ }
271
+
272
+ function getRecordElementInfo(el) {
273
+ if (!el) return null
274
+ const oneSource = el.getAttribute?.('data-one-source')
275
+ return {
276
+ selector: getRecordSelector(el),
277
+ tag: el.tagName?.toLowerCase(),
278
+ id: el.id || undefined,
279
+ testId: el.getAttribute?.('data-testid') || undefined,
280
+ source: oneSource || undefined,
281
+ text: el.textContent?.slice(0, 50).trim() || undefined,
282
+ }
283
+ }
284
+
285
+ function recordTs() {
286
+ return Date.now() - recordStartTime
287
+ }
288
+
289
+ function getMods(e) {
290
+ const m = []
291
+ if (e.ctrlKey) m.push('ctrl')
292
+ if (e.altKey) m.push('alt')
293
+ if (e.shiftKey) m.push('shift')
294
+ if (e.metaKey) m.push('cmd')
295
+ return m.length ? m : undefined
296
+ }
297
+
298
+ function showToast(big, hint, duration, isSuccess) {
299
+ if (!host) createHost()
300
+ const toast = document.createElement('div')
301
+ toast.className = 'toast'
302
+ toast.innerHTML =
303
+ '<div class="toast-big' +
304
+ (isSuccess ? ' success' : '') +
305
+ '">' +
306
+ big +
307
+ '</div>' +
308
+ (hint ? '<div class="toast-hint">' + hint + '</div>' : '')
309
+ shadow.appendChild(toast)
310
+ if (duration) setTimeout(() => toast.remove(), duration)
311
+ return toast
312
+ }
313
+
314
+ function startRecording() {
315
+ // exit inspection mode and close dialog
316
+ deactivate()
317
+ pickerDialog?.close()
318
+
319
+ recording = true
320
+ recordEvents = []
321
+ recordFrameCount = 0
322
+ recordStartTime = Date.now()
323
+
324
+ recordEvents.push({
325
+ t: 0,
326
+ type: 'start',
327
+ window: {
328
+ w: window.innerWidth,
329
+ h: window.innerHeight,
330
+ scrollX: window.scrollX,
331
+ scrollY: window.scrollY,
332
+ },
333
+ url: location.href,
334
+ title: document.title,
335
+ })
336
+
337
+ recordHandlers = {
338
+ mousemove: (e) => {
339
+ recordFrameCount++
340
+ if (recordFrameCount % recordThrottle !== 0) return
341
+ recordEvents.push({ t: recordTs(), type: 'move', x: e.clientX, y: e.clientY })
342
+ },
343
+ click: (e) => {
344
+ recordEvents.push({
345
+ t: recordTs(),
346
+ type: 'click',
347
+ x: e.clientX,
348
+ y: e.clientY,
349
+ btn: e.button,
350
+ el: getRecordElementInfo(e.target),
351
+ })
352
+ },
353
+ dblclick: (e) => {
354
+ recordEvents.push({
355
+ t: recordTs(),
356
+ type: 'dblclick',
357
+ x: e.clientX,
358
+ y: e.clientY,
359
+ el: getRecordElementInfo(e.target),
360
+ })
361
+ },
362
+ contextmenu: (e) => {
363
+ recordEvents.push({
364
+ t: recordTs(),
365
+ type: 'contextmenu',
366
+ x: e.clientX,
367
+ y: e.clientY,
368
+ el: getRecordElementInfo(e.target),
369
+ })
370
+ },
371
+ mousedown: (e) => {
372
+ recordEvents.push({
373
+ t: recordTs(),
374
+ type: 'mousedown',
375
+ x: e.clientX,
376
+ y: e.clientY,
377
+ btn: e.button,
378
+ el: getRecordElementInfo(e.target),
379
+ })
380
+ },
381
+ mouseup: (e) => {
382
+ recordEvents.push({
383
+ t: recordTs(),
384
+ type: 'mouseup',
385
+ x: e.clientX,
386
+ y: e.clientY,
387
+ btn: e.button,
388
+ })
389
+ },
390
+ keydown: (e) => {
391
+ if (e.key === 'Alt' || e.key === 'Meta') return
392
+ recordEvents.push({
393
+ t: recordTs(),
394
+ type: 'keydown',
395
+ key: e.key,
396
+ code: e.code,
397
+ mods: getMods(e),
398
+ el: getRecordElementInfo(document.activeElement),
399
+ })
400
+ },
401
+ keyup: (e) => {
402
+ if (e.key === 'Alt' || e.key === 'Meta') return
403
+ recordEvents.push({ t: recordTs(), type: 'keyup', key: e.key, code: e.code })
404
+ },
405
+ input: (e) => {
406
+ const val = e.target.value
407
+ recordEvents.push({
408
+ t: recordTs(),
409
+ type: 'input',
410
+ val: val?.slice?.(-20),
411
+ el: getRecordElementInfo(e.target),
412
+ })
413
+ },
414
+ change: (e) => {
415
+ recordEvents.push({
416
+ t: recordTs(),
417
+ type: 'change',
418
+ el: getRecordElementInfo(e.target),
419
+ })
420
+ },
421
+ focus: (e) => {
422
+ recordEvents.push({
423
+ t: recordTs(),
424
+ type: 'focus',
425
+ el: getRecordElementInfo(e.target),
426
+ })
427
+ },
428
+ blur: (e) => {
429
+ recordEvents.push({
430
+ t: recordTs(),
431
+ type: 'blur',
432
+ el: getRecordElementInfo(e.target),
433
+ })
434
+ },
435
+ scroll: () => {
436
+ recordEvents.push({
437
+ t: recordTs(),
438
+ type: 'scroll',
439
+ scrollX: window.scrollX,
440
+ scrollY: window.scrollY,
441
+ })
442
+ },
443
+ wheel: (e) => {
444
+ recordEvents.push({
445
+ t: recordTs(),
446
+ type: 'wheel',
447
+ x: e.clientX,
448
+ y: e.clientY,
449
+ dx: Math.round(e.deltaX),
450
+ dy: Math.round(e.deltaY),
451
+ })
452
+ },
453
+ resize: () => {
454
+ recordEvents.push({
455
+ t: recordTs(),
456
+ type: 'resize',
457
+ w: window.innerWidth,
458
+ h: window.innerHeight,
459
+ })
460
+ },
461
+ }
462
+
463
+ Object.entries(recordHandlers).forEach(([evt, fn]) => {
464
+ window.addEventListener(evt, fn, { capture: true, passive: true })
465
+ })
466
+
467
+ showToast('\u25cf', 'Tap Option to stop and copy', 800)
468
+ }
469
+
470
+ function buildContextLines() {
471
+ const lines = []
472
+ const projectRoot = window.__oneProjectRoot || ''
473
+ lines.push('# url: ' + location.href)
474
+ if (projectRoot) lines.push('# project: ' + projectRoot)
475
+ lines.push('# route: ' + location.pathname)
476
+ lines.push('# title: ' + document.title)
477
+ lines.push(
478
+ '# window: ' +
479
+ window.innerWidth +
480
+ 'x' +
481
+ window.innerHeight +
482
+ ' dpr:' +
483
+ devicePixelRatio
484
+ )
485
+ lines.push('# scroll: ' + window.scrollX + ',' + window.scrollY)
486
+ lines.push('# mouse: ' + mousePos.x + ',' + mousePos.y)
487
+ // color scheme
488
+ const dark = window.matchMedia('(prefers-color-scheme: dark)').matches
489
+ lines.push('# theme: ' + (dark ? 'dark' : 'light'))
490
+
491
+ if (currentElementChain.length) {
492
+ const topEl = currentElementChain[0]
493
+ const el = topEl.element
494
+ const names = currentElementChain
495
+ .slice(0, 6)
496
+ .map((e) => e.name)
497
+ .join(' < ')
498
+ const cssSelector = generateSelector(el)
499
+ const recordSelector = getRecordSelector(el)
500
+ if (names) lines.push('# components: ' + names)
501
+ if (cssSelector) lines.push('# selector: ' + cssSelector)
502
+ if (recordSelector) lines.push('# test-selector: ' + recordSelector)
503
+ // bounding box
504
+ const rect = el.getBoundingClientRect()
505
+ lines.push(
506
+ '# rect: ' +
507
+ Math.round(rect.x) +
508
+ ',' +
509
+ Math.round(rect.y) +
510
+ ' ' +
511
+ Math.round(rect.width) +
512
+ 'x' +
513
+ Math.round(rect.height)
514
+ )
515
+ // text content (innerText excludes scripts/hidden elements)
516
+ const text = (el.innerText || '').trim().replace(/\s+/g, ' ').slice(0, 80)
517
+ if (text) lines.push('# text: ' + text)
518
+ // accessibility
519
+ const role = el.getAttribute('role')
520
+ const ariaLabel = el.getAttribute('aria-label')
521
+ const testId = el.getAttribute('data-testid')
522
+ const a11y = [
523
+ role && 'role=' + role,
524
+ ariaLabel && 'label=' + ariaLabel,
525
+ testId && 'testid=' + testId,
526
+ ]
527
+ .filter(Boolean)
528
+ .join(' ')
529
+ if (a11y) lines.push('# a11y: ' + a11y)
530
+ // key computed styles
531
+ const cs = window.getComputedStyle(el)
532
+ const styles = ['display:' + cs.display, 'position:' + cs.position]
533
+ if (cs.overflow !== 'visible') styles.push('overflow:' + cs.overflow)
534
+ if (cs.zIndex !== 'auto') styles.push('z:' + cs.zIndex)
535
+ lines.push('# style: ' + styles.join(' '))
536
+ // source chain
537
+ for (const e of currentElementChain) {
538
+ lines.push('# source: ' + e.source)
539
+ }
540
+ }
541
+ return lines
542
+ }
543
+
544
+ function buildSnapshot() {
545
+ const lines = ['# Snapshot ' + new Date().toISOString()]
546
+ lines.push(...buildContextLines())
547
+ return lines.join('\n')
548
+ }
549
+
550
+ function stopRecording() {
551
+ if (!recording) return
552
+ recording = false
553
+
554
+ Object.entries(recordHandlers).forEach(([evt, fn]) => {
555
+ window.removeEventListener(evt, fn, { capture: true })
556
+ })
557
+ recordHandlers = {}
558
+
559
+ // capture final state
560
+ const finalEl = document.elementFromPoint(mousePos.x, mousePos.y)
561
+ const finalInfo = getRecordElementInfo(finalEl)
562
+
563
+ const duration = recordTs()
564
+
565
+ // build compact line-based format
566
+ const lines = ['# Recording ' + new Date().toISOString()]
567
+ lines.push(...buildContextLines())
568
+ lines.push('# duration: ' + duration + 'ms events: ' + recordEvents.length)
569
+ lines.push('')
570
+
571
+ for (const e of recordEvents) {
572
+ if (e.type === 'start') continue
573
+ let line = e.t + ' ' + e.type
574
+ if (e.x !== undefined) line += ' ' + e.x + ',' + e.y
575
+ if (e.btn !== undefined) line += ' btn:' + e.btn
576
+ if (e.dx !== undefined) line += ' d:' + e.dx + ',' + e.dy
577
+ if (e.w !== undefined) line += ' ' + e.w + 'x' + e.h
578
+ if (e.key) line += ' ' + e.key + (e.code !== e.key ? '(' + e.code + ')' : '')
579
+ if (e.mods) line += ' +' + e.mods.join('+')
580
+ if (e.val !== undefined) line += ' val:"' + e.val + '"'
581
+ if (e.scrollX !== undefined) line += ' ' + e.scrollX + ',' + e.scrollY
582
+ if (e.el) {
583
+ if (e.el.selector) line += ' [' + e.el.selector + ']'
584
+ if (e.el.source) line += ' @' + e.el.source.split('/').pop()
585
+ if (e.el.text && e.type !== 'input') line += ' "' + e.el.text.slice(0, 30) + '"'
586
+ }
587
+ lines.push(line)
588
+ }
589
+
590
+ if (finalInfo) {
591
+ lines.push('')
592
+ lines.push('# final hover: ' + (finalInfo.selector || 'none'))
593
+ if (finalInfo.source) lines.push('# final source: ' + finalInfo.source)
594
+ }
595
+
596
+ const output = lines.join('\n')
597
+ navigator.clipboard
598
+ .writeText(output)
599
+ .then(() => {
600
+ showToast('✓', 'Copied to clipboard', 1200, true)
601
+ })
602
+ .catch(() => {
603
+ console.log('[Source Inspector] Recording:\n' + output)
604
+ })
605
+
606
+ recordEvents = []
607
+ }
608
+
609
+ function getElementChain(x, y) {
610
+ const allElements = document.elementsFromPoint(x, y)
611
+ const chain = []
612
+ for (const element of allElements) {
613
+ if (element.hasAttribute('data-one-source')) {
614
+ chain.push({
615
+ element,
616
+ source: element.getAttribute('data-one-source'),
617
+ name: getComponentName(element.getAttribute('data-one-source')),
618
+ })
619
+ }
620
+ }
621
+ return chain
622
+ }
623
+
624
+ function getComponentName(source) {
625
+ const parts = source.split(':')
626
+ const filePath = parts.slice(0, -2).join(':')
627
+ const fileName = filePath.split('/').pop()
628
+ // extract component name from file name
629
+ return fileName.replace(/\.(tsx?|jsx?)$/, '')
630
+ }
631
+
632
+ function showPicker(x, y, chain) {
633
+ if (!host) createHost()
634
+ currentElementChain = chain
635
+
636
+ // build list items
637
+ const listEl = shadow.getElementById('picker-list')
638
+ listEl.innerHTML = chain
639
+ .map((el) => {
640
+ const parts = el.source.split(':')
641
+ const filePath = parts.slice(0, -2).join(':')
642
+ const line = parts[parts.length - 2]
643
+ // show just filename:line
644
+ const shortFile = filePath.split('/').pop() + ':' + line
645
+ return (
646
+ '<div class="picker-item" data-source="' +
647
+ escapeHtml(el.source) +
648
+ '">' +
649
+ '<span class="picker-item-name">' +
650
+ escapeHtml(el.name) +
651
+ '</span>' +
652
+ '<span class="picker-item-file">' +
653
+ escapeHtml(shortFile) +
654
+ '</span>' +
655
+ '</div>'
656
+ )
657
+ })
658
+ .join('')
659
+
660
+ // position picker near click, smart about viewport edges
661
+ const pickerWidth = 320
662
+ const pickerHeight = Math.min(300, 80 + chain.length * 36)
663
+ const pad = 10
664
+
665
+ let left = x + 10
666
+ let top = y + 10
667
+ let flippedUp = false
668
+
669
+ // flip to left if too close to right edge
670
+ if (left + pickerWidth > window.innerWidth - pad) {
671
+ left = x - pickerWidth - 10
672
+ }
673
+ // flip up if too close to bottom
674
+ if (top + pickerHeight > window.innerHeight - pad) {
675
+ top = y - pickerHeight - 10
676
+ flippedUp = true
677
+ }
678
+ // clamp to viewport
679
+ left = Math.max(pad, Math.min(left, window.innerWidth - pickerWidth - pad))
680
+ top = Math.max(pad, Math.min(top, window.innerHeight - pickerHeight - pad))
681
+
682
+ // reorder elements so actions row is closest to mouse
683
+ const picker = shadow.getElementById('picker')
684
+ const header = picker.querySelector('.picker-header')
685
+ const actions = picker.querySelector('.picker-actions')
686
+ const list = picker.querySelector('.picker-list')
687
+
688
+ if (flippedUp) {
689
+ // picker is above click, put actions at bottom (closest to mouse)
690
+ picker.style.flexDirection = 'column'
691
+ header.style.order = '0'
692
+ list.style.order = '1'
693
+ actions.style.order = '2'
694
+ } else {
695
+ // picker is below click, put actions at top (closest to mouse)
696
+ picker.style.flexDirection = 'column'
697
+ header.style.order = '1'
698
+ list.style.order = '2'
699
+ actions.style.order = '0'
700
+ }
701
+
702
+ pickerDialog.style.left = left + 'px'
703
+ pickerDialog.style.top = top + 'px'
704
+ pickerDialog.showModal()
705
+ }
706
+
707
+ function escapeHtml(str) {
708
+ if (!str) return ''
709
+ return String(str)
710
+ .replace(/&/g, '&amp;')
711
+ .replace(/</g, '&lt;')
712
+ .replace(/>/g, '&gt;')
713
+ .replace(/"/g, '&quot;')
714
+ }
715
+
75
716
  function setupRemovalObserver() {
76
717
  if (removalObserver) return
77
718
  removalObserver = new MutationObserver((mutations) => {
@@ -83,6 +724,7 @@
83
724
  shadow = null
84
725
  overlay = null
85
726
  tag = null
727
+ pickerDialog = null
86
728
  return
87
729
  }
88
730
  }
@@ -194,6 +836,11 @@
194
836
  activate,
195
837
  deactivate,
196
838
  isActive: () => active,
839
+ closePicker: () => pickerDialog?.close(),
840
+ startRecording: () => {
841
+ currentElementChain = [] // no element context
842
+ startRecording()
843
+ },
197
844
  }
198
845
 
199
846
  function deactivate() {
@@ -232,6 +879,11 @@
232
879
  window.addEventListener('keyup', (e) => {
233
880
  if (e.defaultPrevented) return
234
881
  if (e.key === 'Alt') {
882
+ // if recording, stop on option release
883
+ if (recording) {
884
+ stopRecording()
885
+ return
886
+ }
235
887
  deactivate()
236
888
  } else {
237
889
  // reset when other keys are released (check if no modifiers held)
@@ -262,19 +914,26 @@
262
914
  'click',
263
915
  (e) => {
264
916
  if (!active) return
265
- const allElements = document.elementsFromPoint(e.clientX, e.clientY)
266
- let el = null
267
- for (const element of allElements) {
268
- if (element.hasAttribute('data-one-source')) {
269
- el = element
270
- break
271
- }
917
+ const chain = getElementChain(e.clientX, e.clientY)
918
+ if (chain.length) {
919
+ e.preventDefault()
920
+ e.stopImmediatePropagation()
921
+ deactivate()
922
+ showPicker(e.clientX, e.clientY, chain)
272
923
  }
273
- if (el) {
924
+ },
925
+ true
926
+ )
927
+
928
+ // close picker on escape, prevent bubbling
929
+ document.addEventListener(
930
+ 'keydown',
931
+ (e) => {
932
+ if (e.key === 'Escape' && pickerDialog?.open) {
274
933
  e.preventDefault()
275
934
  e.stopPropagation()
276
- const source = el.getAttribute('data-one-source')
277
- fetch('/__one/open-source?source=' + encodeURIComponent(source))
935
+ e.stopImmediatePropagation()
936
+ pickerDialog.close()
278
937
  }
279
938
  },
280
939
  true