/** * Desktop-shell detection for the sidebar. Shells may stamp the render URL * with `dsh-desktop-mode` / `dsh-desktop-platform` (the official Electron * shell does) and expose `window.__DSH_DESKTOP_FILE_PATH__` through a * preload. Parsed once per page and memoized (the URL never changes * mid-session); `resetDesktopEnvForTests` clears the memo for unit tests. * * GEOMETRY POLICY: this module only REPORTS shell facts — it never decides * how to adapt. The strip height comes from standard signals first (the * Window Controls Overlay API, see wco.ts), then the documented contract * parameter `dsh-desktop-titlebar-inset` (a shell may stamp the real pixels * it reserves at the top), then the user's chosen scheme (preset / custom). * The legacy win32-advanced 32px constant is gone from the core: it lives * in the opt-in shell preset (shell-presets.ts) as a fallback for shells * without the WCO API. */ export interface DesktopEnv { /** Running inside a desktop shell (any URL stamp or preload marker). */ readonly desktop: boolean /** `advanced` = frameless/custom-titlebar shell; `compatibility` = native frame. */ readonly mode: 'compatibility' | 'advanced' | null /** Shell platform stamp ('darwin' | 'win32' | …), lowercased, or null. */ readonly platform: string | null /** * Contract parameter `dsh-desktop-titlebar-inset`: pixels the shell * reserves at the top of the web content for its own chrome (0–120, * clamped; 0 when absent). Standard WCO geometry takes precedence over * this whenever the API is available. */ readonly titlebarInset: number } let cached: DesktopEnv | undefined /** Read the shell's desktop stamps (memoized per page; SSR-safe). */ export function parseDesktopEnv(): DesktopEnv { if (cached !== undefined) return cached const hasWindow = typeof window !== 'undefined' const hasPreloadMarker = hasWindow && typeof (window as { __DSH_DESKTOP_FILE_PATH__?: unknown }).__DSH_DESKTOP_FILE_PATH__ !== 'undefined' // location.search includes the leading '?', which URLSearchParams does NOT // strip (it would become part of the first key) — drop it explicitly. // SSR (no window) resolves the empty environment: nothing desktop. const params = hasWindow ? new URLSearchParams(window.location.search.replace(/^\?/, '')) : new URLSearchParams() const modeParam = params.get('dsh-desktop-mode') const mode = modeParam === 'compatibility' || modeParam === 'advanced' ? modeParam : null const platformParam = params.get('dsh-desktop-platform') const platform = platformParam !== null && platformParam !== '' ? platformParam.toLowerCase() : null const desktop = mode !== null || hasPreloadMarker cached = { desktop, mode, platform, titlebarInset: parseTitlebarInset(params.get('dsh-desktop-titlebar-inset')), } return cached } /** Clamp the contract inset parameter into 0–120 (invalid/absent → 0). */ function parseTitlebarInset(raw: string | null): number { if (raw === null) return 0 const parsed = Number(raw) if (!Number.isFinite(parsed)) return 0 return Math.min(120, Math.max(0, Math.round(parsed))) } /** Test hook: drop the memo so the next parse re-reads the URL/globals. */ export function resetDesktopEnvForTests(): void { cached = undefined } /** * The element the sidebar's panel host belongs in. * * The desktop shell stamps the upstream `#root` with * `data-dsh-desktop-content-viewport` and gives it `isolation: isolate` — a * stacking context around the whole app INCLUDING the shell's own overlay * layer (`.dshDesktopOverlay`, z-index 1000), which is where a plugin's modal * renders (the community market's, for one). A host parented to * `document.body` sits OUTSIDE that context, so the panel layer's documented * 25 outranks everything inside it and covered host modals; inside the * viewport the same 25 keeps its meaning — above the app frame (z-index auto) * and below the overlay layer (1000). * * Every other deployment (plain web, an unframed shell) keeps the body mount. * @param doc - the document to resolve against. * @returns the content viewport when the shell framed one, else the body. */ export function panelHostParent(doc: Document = document): HTMLElement { return doc.querySelector('#root[data-dsh-desktop-content-viewport]') ?? doc.body }