From c8fda55d8ea35a152846534389dc739f0768e923 Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Thu, 28 May 2026 15:06:05 -0700 Subject: [PATCH] Remove contextual tour spotlight Co-authored-by: Orca --- src/renderer/src/assets/main.css | 61 ------------- .../ContextualTourOverlay.test.tsx | 2 - .../ContextualTourOverlay.tsx | 90 +------------------ .../ContextualTourOverlaySurface.tsx | 90 +------------------ .../use-workspace-kanban-outside-dismiss.ts | 1 - 5 files changed, 4 insertions(+), 240 deletions(-) diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index e3186fb69e6..fc93e49c0e6 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -2562,64 +2562,3 @@ html.onboarding-tour-start-transition::view-transition-new(root) { justify-content: flex-end; gap: 6px; } - -/* Why: contextual-tour spotlight is a full-cover scrim with one SVG-mask - hole at the target rect, so attention reads from the single undimmed area - rather than from extra chrome painted over the target. - The mask uses the target's own border-radius so curved buttons don't - leave un-dimmed corners between their curve and the cutout edge. */ -.contextual-tour-spotlight { - pointer-events: none; - inset: 0; - animation: contextual-tour-spotlight-fade-in 200ms ease-out; -} - -.contextual-tour-spotlight-svg { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - pointer-events: none; -} - -/* Why: tint toward black so the scrim reads as a dim regardless of - theme — tinting toward --background would be invisible in light theme - (white over white). */ -.contextual-tour-spotlight-fill { - fill: rgb(0 0 0 / 0.22); -} - -/* Why: inside Dialog/Sheet content the target element is lifted above this - scrim. A plain scrim avoids drawing a fake rectangular cutout around grouped - controls such as the Add Project source choices. */ -.contextual-tour-hosted-scrim { - pointer-events: none; - inset: 0; - background: rgb(0 0 0 / 0.14); - animation: contextual-tour-spotlight-fade-in 200ms ease-out; -} - -/* Why: tours can appear inside existing modal surfaces. In that case the - modal backdrop is secondary context, not the primary attention device. */ -:root[data-contextual-tour-active='true'] [data-slot='dialog-overlay'], -:root[data-contextual-tour-active='true'] [data-slot='sheet-overlay'] { - background-color: rgb(0 0 0 / 0.28); -} - -@keyframes contextual-tour-spotlight-fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@media (prefers-reduced-motion: reduce) { - .contextual-tour-spotlight { - animation: none; - } - .contextual-tour-hosted-scrim { - animation: none; - } -} diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx index 960e94a4fe2..526dbbbed89 100644 --- a/src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx +++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx @@ -54,8 +54,6 @@ function renderSurface( activeTourId: 'tasks', renderState, panelRef: { current: null } as RefObject, - spotlightRect: { left: 4, top: 14, width: 112, height: 72, radius: 8 }, - spotlightHostRect: null, panelPosition: { left: 130, top: 20, '--contextual-tour-arrow-offset': '40px' }, panelPlacement: 'right', panelHost: renderState.panelHost, diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx index f466af4d6a1..b174fc2942b 100644 --- a/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx +++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx @@ -30,14 +30,10 @@ import { ContextualTourOverlaySurface, getContextualTourFocusableElements, handleContextualTourOverlayKeyDown, - type ActiveTourRenderState, - type SpotlightRect + type ActiveTourRenderState } from './ContextualTourOverlaySurface' const PANEL_FALLBACK_SIZE = { width: 304, height: 172 } -// Why: padding around the spotlight cutout so a tight ring of the -// underlying surface still reads as part of the highlighted target. -const SPOTLIGHT_PADDING = 6 export function ContextualTourOverlay(): JSX.Element | null { const activeTourId = useAppStore((s) => s.activeContextualTourId) @@ -287,52 +283,6 @@ export function ContextualTourOverlay(): JSX.Element | null { } }, [activeTourId]) - useEffect(() => { - if (!activeTourId || typeof document === 'undefined') { - return - } - // Why: a tour over a Dialog/Sheet already has a modal backdrop; keep that - // backdrop lighter so the tour has one clear focus instead of a blackout. - document.documentElement.dataset.contextualTourActive = 'true' - return () => { - delete document.documentElement.dataset.contextualTourActive - } - }, [activeTourId]) - - useEffect(() => { - const panelHost = renderState?.panelHost - if (!activeTourId || !panelHost) { - return - } - // Why: Radix dialog/sheet content owns focus and z-index; hosting controls - // there avoids outside-focus dismissal and keeps them above the backdrop. - // The hosted spotlight portals into the host so it dims only the panel - // contents around the target, not the rest of the app. - const previousZIndex = panelHost.style.zIndex - const targetElement = - renderState?.targetElement instanceof HTMLElement ? renderState.targetElement : null - const previousPosition = targetElement?.style.position - const previousZ = targetElement?.style.zIndex - panelHost.style.zIndex = '80' - if (targetElement) { - // Why: lift the target above the host-portaled scrim quadrants so the - // cutout reads as un-dimmed even though the scrim shares its stacking - // context. Setting position only when unset preserves any existing - // layout behavior. - if (!targetElement.style.position) { - targetElement.style.position = 'relative' - } - targetElement.style.zIndex = '1' - } - return () => { - panelHost.style.zIndex = previousZIndex - if (targetElement) { - targetElement.style.position = previousPosition ?? '' - targetElement.style.zIndex = previousZ ?? '' - } - } - }, [activeTourId, renderState]) - if (!activeTourId || !renderState) { return null } @@ -360,31 +310,12 @@ export function ContextualTourOverlay(): JSX.Element | null { top: cssPosition.top, '--contextual-tour-arrow-offset': `${cssPosition.arrowOffset}px` } - const targetRadius = getContextualTourTargetRadius(renderState.targetElement) - const spotlightRect: SpotlightRect = { - left: renderState.rect.left - SPOTLIGHT_PADDING, - top: renderState.rect.top - SPOTLIGHT_PADDING, - width: renderState.rect.width + SPOTLIGHT_PADDING * 2, - height: renderState.rect.height + SPOTLIGHT_PADDING * 2, - radius: targetRadius + SPOTLIGHT_PADDING - } - const spotlightHostRect: SpotlightRect | null = panelHostRect - ? { - left: spotlightRect.left - panelHostRect.left, - top: spotlightRect.top - panelHostRect.top, - width: spotlightRect.width, - height: spotlightRect.height, - radius: spotlightRect.radius - } - : null return ( ) } - -function getContextualTourTargetRadius(element: Element): number { - if (typeof window === 'undefined' || !(element instanceof HTMLElement)) { - return 0 - } - // Why: read all four corners and take the max so the cutout never has a - // smaller radius than the target. Asymmetric radii are uncommon in our - // chrome but worth handling. - const style = window.getComputedStyle(element) - const radii = [ - style.borderTopLeftRadius, - style.borderTopRightRadius, - style.borderBottomRightRadius, - style.borderBottomLeftRadius - ] - .map((value) => parseFloat(value)) - .filter((value) => Number.isFinite(value)) - return radii.length > 0 ? Math.max(...radii) : 0 -} diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx index 0ffa03a20ba..eeb6eace749 100644 --- a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx +++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx @@ -30,8 +30,6 @@ type ContextualTourOverlaySurfaceProps = { activeTourId: ContextualTourId renderState: ActiveTourRenderState panelRef: RefObject - spotlightRect: SpotlightRect - spotlightHostRect: SpotlightRect | null panelPosition: PanelPositionStyle panelPlacement: ContextualTourPanelPlacement | null panelHost: HTMLElement | null @@ -41,14 +39,6 @@ type ContextualTourOverlaySurfaceProps = { onOverlayKeyDownCapture: (event: KeyboardEvent) => void } -export type SpotlightRect = { - top: number - left: number - width: number - height: number - radius: number -} - if (typeof window !== 'undefined') { const guardedWindow = window as Window & { __orcaContextualTourGlobalKeyGuardInstalled?: boolean @@ -68,8 +58,6 @@ export function ContextualTourOverlaySurface({ activeTourId, renderState, panelRef, - spotlightRect, - spotlightHostRect, panelPosition, panelPlacement, panelHost, @@ -144,96 +132,24 @@ export function ContextualTourOverlaySurface({ ) - const spotlight = panelHost ? ( - spotlightHostRect ? ( - - ) : null - ) : ( - - ) - return (
- {panelHost ? null : spotlight}
- {panelHost - ? createPortal( - <> - {spotlight} - {panel} - , - panelHost - ) - : panel} + {panelHost ? createPortal(panel, panelHost) : panel}
) } -function ContextualTourSpotlight({ - rect, - hosted -}: { - rect: SpotlightRect - hosted: boolean -}): JSX.Element { - if (hosted) { - return ( -