mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix: move preview selection badges off small targets
Preview annotations should keep the selected element readable. The selected rectangle now carries a stronger translucent blue fill, while the numbered badge is a separate high-contrast fixed overlay placed outside the selected rect and clamped to the viewport. Constraint: No issue number was provided for this task. Constraint: Preserve the DOM-captured annotation path introduced for viewport-safe screenshots. Rejected: Put the badge inside the selected box | it obscures compact buttons and labels. Rejected: Return to canvas-side badge drawing | that reintroduces the separate coordinate system. Confidence: high Scope-risk: narrow Directive: Keep numbered badges outside compact selected targets unless visual evidence shows a better non-obscuring placement. Tested: cd desktop && bun run test --run src/preview-agent/screenshot.test.ts src/preview-agent/picker.test.ts src/preview-agent/metadata.test.ts src/preview-agent/protocol.test.ts src/preview-agent/bridge.test.ts src/preview-agent/editBubble.test.ts Tested: cd desktop && bun run lint Tested: cd desktop && bun run build:preview-agent Tested: bun run check:desktop Tested: git diff --check Not-tested: Real Tauri webview visual smoke.
This commit is contained in:
parent
c2feac8220
commit
fd040e3798
File diff suppressed because one or more lines are too long
@ -151,13 +151,15 @@ describe('captureAnnotatedRegion', () => {
|
||||
const ctx = makeMockCtx()
|
||||
html2canvasMock.mockImplementation(async () => {
|
||||
const overlay = document.querySelector('[data-preview-selection-annotation="true"]') as HTMLElement | null
|
||||
const badge = document.querySelector('[data-preview-selection-badge="true"]') as HTMLElement | null
|
||||
expect(overlay).not.toBeNull()
|
||||
expect(badge).not.toBeNull()
|
||||
expect(overlay?.style.position).toBe('fixed')
|
||||
expect(overlay?.style.left).toBe('900px')
|
||||
expect(overlay?.style.top).toBe('320px')
|
||||
expect(overlay?.style.width).toBe('86px')
|
||||
expect(overlay?.style.height).toBe('48px')
|
||||
expect(overlay?.textContent).toContain('1')
|
||||
expect(badge?.textContent).toContain('1')
|
||||
return {
|
||||
getContext: () => ctx as unknown as CanvasRenderingContext2D,
|
||||
toDataURL: () => 'data:image/png;base64,RAW',
|
||||
@ -175,7 +177,35 @@ describe('captureAnnotatedRegion', () => {
|
||||
await captureAnnotatedRegion(el, 1)
|
||||
|
||||
expect(ctx.arc).not.toHaveBeenCalled()
|
||||
expect(document.querySelector('[data-preview-selection-annotation="true"]')).toBeNull()
|
||||
expect(document.querySelector('[data-preview-selection-annotation-root="true"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('places the numbered badge outside the selected element so small buttons stay readable', async () => {
|
||||
html2canvasMock.mockImplementation(async () => {
|
||||
const overlay = document.querySelector('[data-preview-selection-annotation="true"]') as HTMLElement | null
|
||||
const badge = document.querySelector('[data-preview-selection-badge="true"]') as HTMLElement | null
|
||||
expect(overlay).not.toBeNull()
|
||||
expect(badge).not.toBeNull()
|
||||
expect(badge?.style.position).toBe('fixed')
|
||||
expect(Number.parseFloat(badge?.style.top ?? '0') + 26).toBeLessThanOrEqual(320)
|
||||
expect(badge?.style.color).toBe('white')
|
||||
expect(badge?.style.background).toBe('rgb(47, 123, 255)')
|
||||
expect(overlay?.textContent).not.toContain('1')
|
||||
return {
|
||||
getContext: () => makeMockCtx() as unknown as CanvasRenderingContext2D,
|
||||
toDataURL: () => 'data:image/png;base64,RAW',
|
||||
}
|
||||
})
|
||||
const el = document.createElement('button')
|
||||
vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
||||
left: 900, top: 320, width: 86, height: 48,
|
||||
right: 986, bottom: 368, x: 900, y: 320,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect)
|
||||
|
||||
await captureAnnotatedRegion(el, 1)
|
||||
|
||||
expect(document.querySelector('[data-preview-selection-badge="true"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns the compressed wrapper of the canvas dataURL', async () => {
|
||||
|
||||
@ -19,11 +19,39 @@ function setImportant(style: CSSStyleDeclaration, property: string, value: strin
|
||||
style.setProperty(property, value, 'important')
|
||||
}
|
||||
|
||||
const BADGE_SIZE = 26
|
||||
const BADGE_GAP = 4
|
||||
const VIEWPORT_MARGIN = 4
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(value, max))
|
||||
}
|
||||
|
||||
function computeBadgePosition(rect: DOMRect): { left: number; top: number } {
|
||||
const preferredLeft = rect.left + rect.width / 2 - BADGE_SIZE / 2
|
||||
const maxLeft = Math.max(VIEWPORT_MARGIN, window.innerWidth - BADGE_SIZE - VIEWPORT_MARGIN)
|
||||
const left = clamp(preferredLeft, VIEWPORT_MARGIN, maxLeft)
|
||||
|
||||
const topAbove = rect.top - BADGE_SIZE - BADGE_GAP
|
||||
const topBelow = rect.bottom + BADGE_GAP
|
||||
if (topAbove >= VIEWPORT_MARGIN) return { left, top: topAbove }
|
||||
if (topBelow + BADGE_SIZE <= window.innerHeight - VIEWPORT_MARGIN) return { left, top: topBelow }
|
||||
return { left, top: clamp(topAbove, VIEWPORT_MARGIN, Math.max(VIEWPORT_MARGIN, window.innerHeight - BADGE_SIZE - VIEWPORT_MARGIN)) }
|
||||
}
|
||||
|
||||
function createAnnotationOverlay(el: Element, label: number | string): HTMLElement {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const root = document.createElement('div')
|
||||
root.dataset.previewSelectionAnnotationRoot = 'true'
|
||||
root.setAttribute('aria-hidden', 'true')
|
||||
setImportant(root.style, 'position', 'fixed')
|
||||
setImportant(root.style, 'inset', '0')
|
||||
setImportant(root.style, 'overflow', 'visible')
|
||||
setImportant(root.style, 'pointer-events', 'none')
|
||||
setImportant(root.style, 'z-index', '2147483647')
|
||||
|
||||
const overlay = document.createElement('div')
|
||||
overlay.dataset.previewSelectionAnnotation = 'true'
|
||||
overlay.setAttribute('aria-hidden', 'true')
|
||||
setImportant(overlay.style, 'position', 'fixed')
|
||||
setImportant(overlay.style, 'left', `${rect.left}px`)
|
||||
setImportant(overlay.style, 'top', `${rect.top}px`)
|
||||
@ -32,31 +60,35 @@ function createAnnotationOverlay(el: Element, label: number | string): HTMLEleme
|
||||
setImportant(overlay.style, 'box-sizing', 'border-box')
|
||||
setImportant(overlay.style, 'border', '3px solid #2f7bff')
|
||||
setImportant(overlay.style, 'border-radius', '8px')
|
||||
setImportant(overlay.style, 'background', 'rgba(47, 123, 255, 0.08)')
|
||||
setImportant(overlay.style, 'background', 'rgba(47, 123, 255, 0.16)')
|
||||
setImportant(overlay.style, 'box-shadow', '0 0 0 2px rgba(255, 255, 255, 0.9)')
|
||||
setImportant(overlay.style, 'pointer-events', 'none')
|
||||
setImportant(overlay.style, 'z-index', '2147483647')
|
||||
|
||||
const badge = document.createElement('div')
|
||||
badge.dataset.previewSelectionBadge = 'true'
|
||||
badge.textContent = String(label)
|
||||
setImportant(badge.style, 'position', 'absolute')
|
||||
setImportant(badge.style, 'top', '4px')
|
||||
setImportant(badge.style, 'right', '4px')
|
||||
const badgePos = computeBadgePosition(rect)
|
||||
setImportant(badge.style, 'position', 'fixed')
|
||||
setImportant(badge.style, 'left', `${badgePos.left}px`)
|
||||
setImportant(badge.style, 'top', `${badgePos.top}px`)
|
||||
setImportant(badge.style, 'display', 'flex')
|
||||
setImportant(badge.style, 'align-items', 'center')
|
||||
setImportant(badge.style, 'justify-content', 'center')
|
||||
setImportant(badge.style, 'width', '24px')
|
||||
setImportant(badge.style, 'height', '24px')
|
||||
setImportant(badge.style, 'width', `${BADGE_SIZE}px`)
|
||||
setImportant(badge.style, 'height', `${BADGE_SIZE}px`)
|
||||
setImportant(badge.style, 'border-radius', '999px')
|
||||
setImportant(badge.style, 'background', '#2f7bff')
|
||||
setImportant(badge.style, 'color', '#ffffff')
|
||||
setImportant(badge.style, 'border', '2px solid #ffffff')
|
||||
setImportant(badge.style, 'color', 'white')
|
||||
setImportant(badge.style, 'font', '700 14px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif')
|
||||
setImportant(badge.style, 'line-height', '24px')
|
||||
setImportant(badge.style, 'box-shadow', '0 0 0 2px rgba(255, 255, 255, 0.95)')
|
||||
overlay.appendChild(badge)
|
||||
setImportant(badge.style, 'line-height', `${BADGE_SIZE}px`)
|
||||
setImportant(badge.style, 'box-sizing', 'border-box')
|
||||
setImportant(badge.style, 'box-shadow', '0 1px 8px rgba(47, 123, 255, 0.28), 0 0 0 1px rgba(47, 123, 255, 0.22)')
|
||||
|
||||
document.documentElement.appendChild(overlay)
|
||||
return overlay
|
||||
root.appendChild(overlay)
|
||||
root.appendChild(badge)
|
||||
document.documentElement.appendChild(root)
|
||||
return root
|
||||
}
|
||||
|
||||
/** Viewport screenshot with the picked element's region annotated (blue box + numbered badge). 图4 */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user