Files
Enterprise-Knowledge-Base/frontend/src/utils/zoom.ts
T
wizardchen 30332cca99 fix(frontend): correct floating UI positioning under root zoom
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`.
2026-05-24 23:03:26 +08:00

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,
}
}