/** * @file canvas-shell.tsx — universal input-grammar wrapper for TSX canvases * @scope apps/studio/canvas-shell.tsx * @purpose Mounted by `DesignCanvas` for every canvas. Stacks * SelectionSetProvider + ContextMenuProvider, wires the input * router to provider actions, and renders the floating chrome * (ToolPalette, hover halo, selection halos, group bbox). * * Input grammar (V0.16+ universal — there's no opt-out flag): * * Move tool (V) * bare hover / click → passes through (native interactions work) * Cmd + hover → preview halo on deepest element under cursor * Cmd + click → replace selection with deepest element * Cmd + Shift + click → add deepest to selection (multi) * right-click → context menu * * Hand tool (H) pan-on-drag, no Space required; no selection * Comment tool (C) hover paints halo, click drops comment pin; * native interactions on artboard children fully * suppressed via capture-phase preventDefault * * keydown V / H / C / Esc → tool switch (Esc also clears selection + menu) * * Wheel / pinch / space-pan / Cmd+0/1/+/- stay with `useViewportController` * (canvas-lib.tsx). The router consumes a strict non-overlapping subset. */ import { type ReactNode, type RefObject, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { AiBanner } from './ai-banner.tsx'; import { AnnotationsLayer } from './annotations-layer.tsx'; import { ArtboardMarqueeOverlay } from './artboard-marquee.tsx'; import { type ArtboardRect, PhotoPreviewBridge, SnapGuideOverlay, useArtboardsContext, useDragStateContext, useViewportControllerContext, type ViewportControllerHandle, } from './canvas-lib.tsx'; import { buildEditSourceRecord, type EditSourceApplyFn, type EditSourceOp, type EditSourcePayload, } from './commands/edit-source-command.ts'; import { type AlignMode, alignLabel, equalSpacingLabel } from './commands/equal-spacing-command.ts'; import { buildReorderRecord, type ReorderRevertFn } from './commands/reorder-command.ts'; import { ContextMenuProvider, type ContextRegistry, type ContextTarget, type ContextTargetKind, type MenuItem, useContextMenu, } from './context-menu.tsx'; import { ContextualToolbar } from './contextual-toolbar.tsx'; import { CursorsOverlay } from './cursors-overlay.tsx'; import { buildComposeSelection, cssPath, deriveFile, hoverTargetToSelection, openCommentComposer, realClasses, resolveSelectionEl, } from './dom-selection.ts'; import { setElementDragActive } from './drag-state.ts'; import { EqualSpacingHandles } from './equal-spacing-handles.tsx'; import { ExportDialogProvider } from './export-dialog.tsx'; import { crossedDragThreshold, type HoverTarget, resolveHoverTarget, useInputRouter, } from './input-router.tsx'; import { ElementMarqueeOverlay } from './marquee-overlay.tsx'; import { MeasureOverlay } from './measure-overlay.tsx'; import { ParticipantsChrome } from './participants-chrome.tsx'; import { isReadOnlyCanvas } from './read-only-mode.ts'; import { mountCaret, placeCaretAt } from './text-caret.ts'; import { ToolPalette } from './tool-palette.tsx'; import { UndoHud } from './undo-hud.tsx'; import { AnnotationSelectionProvider, useAnnotationSelection, useAnnotationSelectionOptional, } from './use-annotation-selection.tsx'; import { AnnotationsVisibilityProvider } from './use-annotations-visibility.tsx'; import { canvasConfirm, showCanvasToast } from './use-canvas-media-drop.tsx'; import { ChromeVisibilityProvider, useChromeVisibility } from './use-chrome-visibility.tsx'; import { useCollab } from './use-collab.tsx'; import { useCursorModifiers } from './use-cursor-modifiers.tsx'; import { ElementResizeOverlay, worldZoomFor } from './use-element-resize.tsx'; import { GridTrackHandlesOverlay } from './use-grid-track-handles.tsx'; import { useKeyboardDiscipline } from './use-keyboard-discipline.tsx'; import { MaybeSelectionSetProvider, type Selection, useSelectionSet, } from './use-selection-set.tsx'; import { SpacingHandlesOverlay } from './use-spacing-handles.tsx'; import { useToolMode } from './use-tool-mode.tsx'; import { useUndoSinks, useUndoStack } from './use-undo-stack.tsx'; // ───────────────────────────────────────────────────────────────────────────── // Styles — halos render as `position: fixed` siblings of the canvas. Reading // element bounds via getBoundingClientRect (screen coords) keeps the 2 px // border thickness consistent across zoom levels — CSS `zoom` on the world // plane would otherwise scale a 2 px outline to 0.84 px at 42 % zoom (subpixel // = invisible). No per-element class stamping is used. // HUD / chrome token block. System-review 2026-05-27 (D-4) flagged that the // dev-server chrome (toolbar + minimap + halos + marquee + AI banner) used // `var(--accent, …)` which inherited the canvas DS palette — a violet StudyFi // canvas turned the floating cursor toolbar violet. The HUD owns its own // `--maude-hud-*` token family, set on `:root` of the canvas iframe document // here. Canvas DSs do NOT define these names, so HUD/chrome CSS resolves // against this block regardless of what the imported `:root { --accent: … }` // looks like. // // Two sub-families: // • `--maude-hud-accent*` — theme-agnostic brand orange-rust. Defaults match // the existing inline fallback (`#d63b1f`) so the accent never changes with // the theme. Users who want to re-theme the HUD can set these via a // `