30332cca99
The user font-size preference applies CSS `zoom` on `<html>` (see `composables/useFont.ts`). This breaks every popover that anchors itself with `getBoundingClientRect()` + `position: fixed/absolute`, because the rect is in visual pixels while CSS lengths are then multiplied by zoom again — shifting popovers toward the lower-right and miscomputing maxHeight / width / viewport-edge clamping. PR #1459 patched only `AgentSelector.vue` (and left its width/maxHeight unfixed). This PR introduces a shared helper `frontend/src/utils/zoom.ts` (`getRootZoom`, `rectToCssPx`, `cssViewportSize`) and applies it to every affected popover: - `AgentSelector.vue` — refactored to use the helper; also normalizes `width` and `maxHeight` (previously still in visual px). - `Input-field.vue` — model dropdown, mention popup (typed @ and button- triggered), and agent-mode dropdown. - `KnowledgeBaseSelector.vue` — primary anchor path and fallback path. - `UserMenu.vue` — tenant and IM floating submenus + viewport clamp. - `FAQTagTooltip.vue` — fixed tooltip positioning + edge clamp. - `AgentEditorModal.vue` — placeholder popups for system_prompt, context_template, and rewrite/fallback prompts. - `AgentStreamDisplay.vue` — `kb-float-popup` (absolute under body, also under the zoom containing block). Verified with `npm --prefix frontend run build`.
76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
/**
|
|
* Helpers for positioning floating UI (fixed/absolute popovers) under CSS
|
|
* `zoom` applied on the root `<html>` element.
|
|
*
|
|
* Why this exists:
|
|
* - `composables/useFont.ts` writes `zoom: <scale>` onto `<html>` to honor the
|
|
* user's font-size preference. That zoom multiplies everything inside.
|
|
* - `getBoundingClientRect()`, `window.innerWidth/innerHeight`, `MouseEvent.clientX/Y`
|
|
* and similar APIs return values in **visual (device-like) pixels** — already
|
|
* scaled by zoom.
|
|
* - CSS lengths written to a fixed/absolute element living inside the zoom
|
|
* context (anywhere under `<html>`) are interpreted in **CSS pixels** and
|
|
* then multiplied by zoom when rendered.
|
|
*
|
|
* To anchor a popover to a visual coordinate (e.g. the rect of a button), we
|
|
* must divide the visual measurement by the root zoom before writing it into
|
|
* CSS `left/top/right/bottom/width/height/maxHeight/...`.
|
|
*/
|
|
|
|
/**
|
|
* Read the current `zoom` value applied to `<html>`. Returns 1 when zoom is
|
|
* unset, computed as a non-finite value, or in environments without a DOM.
|
|
*/
|
|
export function getRootZoom(): number {
|
|
if (typeof document === 'undefined') return 1
|
|
const root = document.documentElement
|
|
if (!root) return 1
|
|
const raw = getComputedStyle(root).zoom
|
|
const zoom = Number.parseFloat(raw || '1')
|
|
if (!Number.isFinite(zoom) || zoom <= 0) return 1
|
|
return zoom
|
|
}
|
|
|
|
/**
|
|
* Convert a visual-pixel measurement to the CSS-pixel value that should be
|
|
* written into the style of a fixed/absolute element under the root zoom.
|
|
*
|
|
* Pass `zoom` explicitly when you already cached it for a calculation to
|
|
* avoid re-reading computed style multiple times.
|
|
*/
|
|
export function toCssPx(visualPx: number, zoom: number = getRootZoom()): number {
|
|
return visualPx / zoom
|
|
}
|
|
|
|
/**
|
|
* Normalize a `DOMRect`-like value from `getBoundingClientRect()` (visual
|
|
* pixels) into CSS pixels. Returns a plain object so callers can read/write
|
|
* freely without worrying about the immutable `DOMRect` shape.
|
|
*/
|
|
export function rectToCssPx(
|
|
rect: { top: number; left: number; right: number; bottom: number; width: number; height: number },
|
|
zoom: number = getRootZoom(),
|
|
): { top: number; left: number; right: number; bottom: number; width: number; height: number } {
|
|
return {
|
|
top: rect.top / zoom,
|
|
left: rect.left / zoom,
|
|
right: rect.right / zoom,
|
|
bottom: rect.bottom / zoom,
|
|
width: rect.width / zoom,
|
|
height: rect.height / zoom,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Viewport size in CSS pixels (i.e., the coordinate system used by CSS
|
|
* lengths under the root zoom). `window.innerWidth/innerHeight` are visual
|
|
* pixels, so divide by zoom.
|
|
*/
|
|
export function cssViewportSize(zoom: number = getRootZoom()): { width: number; height: number } {
|
|
if (typeof window === 'undefined') return { width: 0, height: 0 }
|
|
return {
|
|
width: window.innerWidth / zoom,
|
|
height: window.innerHeight / zoom,
|
|
}
|
|
}
|