mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
feat(desktop): add element picker state machine with shadow-dom overlay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5083adc9f9
commit
31359a9cd2
29
desktop/src/preview-agent/picker.test.ts
Normal file
29
desktop/src/preview-agent/picker.test.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { describe, expect, it, beforeEach, vi } from 'vitest'
|
||||
import { createPicker } from './picker'
|
||||
|
||||
beforeEach(() => { document.body.innerHTML = `<main><section><h1 id="t">A</h1></section></main>` })
|
||||
|
||||
describe('createPicker', () => {
|
||||
it('selects an element and exposes current selection', () => {
|
||||
const onSelect = vi.fn()
|
||||
const picker = createPicker({ onSelect })
|
||||
picker.enter()
|
||||
picker.hover(document.getElementById('t')!)
|
||||
picker.select()
|
||||
expect(picker.current()?.id).toBe('t')
|
||||
expect(onSelect).toHaveBeenCalledWith(document.getElementById('t'))
|
||||
})
|
||||
it('climb/descend move the current selection', () => {
|
||||
const picker = createPicker({ onSelect: vi.fn() })
|
||||
picker.enter(); picker.hover(document.getElementById('t')!); picker.select()
|
||||
picker.climb()
|
||||
expect(picker.current()?.tagName.toLowerCase()).toBe('section')
|
||||
picker.descend()
|
||||
expect(picker.current()?.id).toBe('t')
|
||||
})
|
||||
it('exit clears overlay and selection', () => {
|
||||
const picker = createPicker({ onSelect: vi.fn() })
|
||||
picker.enter(); picker.hover(document.getElementById('t')!); picker.select(); picker.exit()
|
||||
expect(picker.current()).toBeNull()
|
||||
})
|
||||
})
|
||||
41
desktop/src/preview-agent/picker.ts
Normal file
41
desktop/src/preview-agent/picker.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { climb as climbEl, descend as descendEl } from './treeNav'
|
||||
|
||||
type PickerDeps = { onSelect: (el: Element) => void }
|
||||
|
||||
export function createPicker(deps: PickerDeps) {
|
||||
let active = false
|
||||
let current: Element | null = null
|
||||
let hovered: Element | null = null
|
||||
|
||||
// Shadow DOM 浮层宿主:避免被页面 CSS 影响、抗 CSP 内联限制
|
||||
const host = document.createElement('div')
|
||||
host.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483647'
|
||||
const shadow = host.attachShadow({ mode: 'open' })
|
||||
const box = document.createElement('div')
|
||||
box.style.cssText = 'position:fixed;border:2px solid #2f7bff;background:rgba(47,123,255,.12);pointer-events:none'
|
||||
box.hidden = true
|
||||
shadow.appendChild(box)
|
||||
|
||||
const draw = (el: Element | null) => {
|
||||
if (!el) { box.hidden = true; return }
|
||||
const r = el.getBoundingClientRect()
|
||||
box.hidden = false
|
||||
box.style.left = `${r.left}px`; box.style.top = `${r.top}px`
|
||||
box.style.width = `${r.width}px`; box.style.height = `${r.height}px`
|
||||
}
|
||||
|
||||
const setCurrent = (el: Element) => { current = el; draw(el); deps.onSelect(el) }
|
||||
|
||||
return {
|
||||
enter() {
|
||||
active = true; current = null; hovered = null
|
||||
if (!host.isConnected) document.documentElement.appendChild(host)
|
||||
},
|
||||
hover(el: Element) { if (!active || current) return; hovered = el; draw(el) },
|
||||
select() { if (hovered) setCurrent(hovered) },
|
||||
climb() { if (current) { const p = climbEl(current); if (p) { current = p; draw(p) } } },
|
||||
descend() { if (current) { const c = descendEl(current); if (c) { current = c; draw(c) } } },
|
||||
current() { return current },
|
||||
exit() { active = false; current = null; hovered = null; box.hidden = true; host.remove() },
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user