|
2 | 2 | * SpotlightShellChrome |
3 | 3 | * |
4 | 4 | * Low-level chrome for SpotlightShell: simple panel + optional portal + |
5 | | - * backdrop + viewport-centered positioning + footer slot beneath the panel. |
| 5 | + * backdrop + content-area-centered positioning + footer slot beneath the |
| 6 | + * panel. Horizontal centering excludes the docked layout sidebar and the |
| 7 | + * focused-chat workstation trail, so the spotlight sits over the visible |
| 8 | + * content rather than the full viewport. |
6 | 9 | * |
7 | 10 | * This is a direct merge of the previous SelectorContainer + SpotlightPortal |
8 | 11 | * layer. Only consumed by SpotlightShell; palettes never see this component. |
9 | 12 | */ |
10 | 13 | import { useAtomValue } from "jotai"; |
11 | | -import React, { useEffect, useRef } from "react"; |
| 14 | +import React, { useEffect, useMemo, useRef } from "react"; |
12 | 15 | import { createPortal } from "react-dom"; |
| 16 | +import { useLocation } from "react-router-dom"; |
13 | 17 |
|
| 18 | +import { getSidebarId } from "@src/config/sidebarRegistry"; |
14 | 19 | import { CODEMIRROR_STYLE_NONCE } from "@src/features/CodeMirror/config/nonce"; |
15 | 20 | import { useOverlayLayer } from "@src/store/ui/overlayLayerAtom"; |
| 21 | +import { |
| 22 | + sidebarCollapsedAtom, |
| 23 | + sidebarWidthAtom, |
| 24 | +} from "@src/store/ui/sidebarAtom"; |
16 | 25 | import { spotlightPlacementAtom } from "@src/store/ui/uiAtom"; |
17 | 26 |
|
18 | 27 | import { SPOTLIGHT_CONFIG } from "../constants"; |
@@ -43,6 +52,30 @@ export const SpotlightShellChrome: React.FC<SpotlightShellChromeProps> = ({ |
43 | 52 | }) => { |
44 | 53 | const inputHostRef = useRef<HTMLDivElement | null>(null); |
45 | 54 | const spotlightPlacement = useAtomValue(spotlightPlacementAtom); |
| 55 | + const location = useLocation(); |
| 56 | + const sidebarWidth = useAtomValue(sidebarWidthAtom); |
| 57 | + const sidebarCollapsed = useAtomValue(sidebarCollapsedAtom); |
| 58 | + |
| 59 | + // Routes without a docked layout sidebar (and collapsed sidebars) reserve |
| 60 | + // no horizontal space, so they contribute no centering inset. |
| 61 | + const sidebarInset = |
| 62 | + getSidebarId(location.pathname) !== null && !sidebarCollapsed |
| 63 | + ? sidebarWidth |
| 64 | + : 0; |
| 65 | + |
| 66 | + // The focused-chat workstation trail (live rail or launchpad placeholder) |
| 67 | + // reserves width at the right edge of the content area. Its visibility |
| 68 | + // mixes chat-focus state, tab type, a container query, and rail-local |
| 69 | + // collapse state that no atom exposes, so the rendered track is measured |
| 70 | + // instead of mirrored. Measured once per open — the backdrop prevents the |
| 71 | + // trail from changing while the spotlight is up. |
| 72 | + const trailInset = useMemo(() => { |
| 73 | + if (!isOpen || !asPortal) return 0; |
| 74 | + const track = document.querySelector<HTMLElement>( |
| 75 | + "[data-workstation-trail-track]" |
| 76 | + ); |
| 77 | + return track ? track.getBoundingClientRect().width : 0; |
| 78 | + }, [isOpen, asPortal]); |
46 | 79 |
|
47 | 80 | useOverlayLayer(isOpen && asPortal); |
48 | 81 |
|
@@ -147,13 +180,13 @@ export const SpotlightShellChrome: React.FC<SpotlightShellChromeProps> = ({ |
147 | 180 | spotlightPlacement === "center" |
148 | 181 | ? "50%" |
149 | 182 | : SPOTLIGHT_CONFIG.topOffset, |
150 | | - left: "50%", |
| 183 | + left: `calc(50% + ${(sidebarInset - trailInset) / 2}px)`, |
151 | 184 | transform: |
152 | 185 | spotlightPlacement === "center" |
153 | 186 | ? "translate(-50%, -50%)" |
154 | 187 | : "translateX(-50%)", |
155 | 188 | zIndex: SPOTLIGHT_CONFIG.containerZIndex, |
156 | | - width: `min(${width}px, calc(100vw - 160px))`, |
| 189 | + width: `min(${width}px, calc(100vw - ${sidebarInset + trailInset}px - 160px))`, |
157 | 190 | }} |
158 | 191 | onClick={(event) => event.stopPropagation()} |
159 | 192 | > |
|
0 commit comments