import type * as React from 'react'; import type { PassThroughProps } from '../utils/passThrough.js'; import type { PortalProps } from '../utils/portal.js'; import type { PrimitivePropsWithRef } from '../utils/primitive.js'; import type { FocusOutsideEvent, InteractOutsideEvent } from '../utils/useDismiss.js'; /** The panel's top-left corner, in CSS pixels, measured from the top-left of its `boundary`. */ export interface FloatingPanelPosition { x: number; y: number; } /** The panel's box, in CSS pixels. */ export interface FloatingPanelSize { width: number; height: number; } /** Panel window state: `default` uses position/size, `maximized` fills the boundary, `minimized` collapses to header height; only `default` is draggable/resizable. */ export type FloatingPanelStage = 'default' | 'minimized' | 'maximized'; /** Which edge or corner a {@link FloatingPanelResizeTrigger} pulls. */ export type FloatingPanelResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'; /** @internal Gesture type for release announcements: `'move'` (header drag) or `'resize'` (edge/corner pull). */ export type FloatingPanelGesture = 'move' | 'resize'; /** Shared state distributed from {@link FloatingPanel} to its parts via context. @internal */ export interface FloatingPanelContextValue { triggerRef: React.RefObject; contentRef: React.RefObject; contentId: string; titleId: string; /** Whether title is mounted; Content uses this to withhold `aria-labelledby`. */ hasTitle: boolean; setHasTitle: (present: boolean) => void; open: boolean; onOpenChange: (open: boolean) => void; position: FloatingPanelPosition; size: FloatingPanelSize; minSize: FloatingPanelSize; maxSize: FloatingPanelSize; /** Measured boundary box (null before first measurement). ResizeTrigger uses it for `aria-valuemax`. */ bounds: FloatingPanelSize | null; /** The boundary's top-left corner in the viewport, added to `position` when the content places itself on screen; the origin when the viewport is the boundary. */ origin: FloatingPanelPosition; /** Whether an explicit boundary element is measured, so `maximized` fills it rather than the viewport. */ bounded: boolean; stage: FloatingPanelStage; onStageChange: (stage: FloatingPanelStage) => void; moveBy: (dx: number, dy: number) => void; resizeBy: (direction: FloatingPanelResizeDirection, dx: number, dy: number) => void; announceRelease: (gesture: FloatingPanelGesture) => void; draggable: boolean; resizable: boolean; disabled: boolean; step: number; dragging: boolean; setDragging: (dragging: boolean) => void; resizing: boolean; setResizing: (resizing: boolean) => void; contentPresent: boolean; setContentPresent: (present: boolean) => void; } /** * Props for the {@link FloatingPanel} recipe. Pass-through attributes (`id`, * `className`, `style`, `data-*`, `aria-*` — see {@link PassThroughProps}) land * on the derived `FloatingPanelContent` surface. */ export interface FloatingPanelProps extends PassThroughProps { /** * The panel's body, rendered inside the derived `FloatingPanelContent` below the derived header. * @default undefined — an empty panel body. */ children?: React.ReactNode | undefined; /** * The panel's heading, rendered as the derived title inside the derived header. * @default undefined — no title renders. */ title?: string | undefined; /** * Window controls (stage triggers); each supplies its own visible `label`. * @default [] */ stages?: ReadonlyArray<{ /** The window state this control switches the panel to. */ stage: FloatingPanelStage; /** The control's visible text, and its accessible name. */ label: string; }> | undefined; /** * Whether the derived header renders a close button. * @default true */ dismissible?: boolean | undefined; /** * Whether the panel is open, for controlled usage. Pair with `onOpenChange`. * @default undefined — uncontrolled, tracks its own open state internally. */ open?: boolean | undefined; /** * Whether the panel is initially open, for uncontrolled usage. * @default false — the panel is closed until the trigger is pressed. */ defaultOpen?: boolean | undefined; /** * Fires with the new open state whenever the panel opens or closes, controlled or not. * @default undefined — no-op if omitted. */ onOpenChange?: ((open: boolean) => void) | undefined; /** * Fires once the open/close animation settles, the settled counterpart to `onOpenChange`. * @default undefined */ onOpenChangeComplete?: ((open: boolean) => void) | undefined; /** * The panel's top-left corner, for controlled usage; pair with `onPositionChange` or it refuses to move. * @default undefined — uncontrolled, tracks its own position internally. */ position?: FloatingPanelPosition | undefined; /** * Where the panel starts, for uncontrolled usage. * @default { x: 0, y: 0 } — the top-left corner of the boundary. */ defaultPosition?: FloatingPanelPosition | undefined; /** * Fires with the clamped position on every drag frame and arrow-key nudge, not once per gesture. * @default undefined — no-op if omitted. */ onPositionChange?: ((position: FloatingPanelPosition) => void) | undefined; /** * Fires once when a drag/arrow-key gesture settles, and after resize too since west/north grips relocate the origin. * @default undefined */ onPositionCommit?: ((position: FloatingPanelPosition) => void) | undefined; /** * The panel's box, for controlled usage; pair with `onSizeChange` or it refuses to resize. * @default undefined — uncontrolled, tracks its own size internally. */ size?: FloatingPanelSize | undefined; /** * The panel's starting box, for uncontrolled usage. * @default { width: 320, height: 240 } */ defaultSize?: FloatingPanelSize | undefined; /** * Fires with the clamped size on every resize frame and arrow-key nudge, not once per gesture. * @default undefined — no-op if omitted. */ onSizeChange?: ((size: FloatingPanelSize) => void) | undefined; /** * Fires once when a resize gesture settles; a move alone never fires it. * @default undefined */ onSizeCommit?: ((size: FloatingPanelSize) => void) | undefined; /** * Smallest box the panel can be resized to; every resize is clamped to it. * @default { width: 160, height: 120 } */ minSize?: FloatingPanelSize | undefined; /** * Largest box the panel can be resized to. * @default { width: Infinity, height: Infinity } — only the boundary limits growth. */ maxSize?: FloatingPanelSize | undefined; /** * Element the panel may not be dragged or resized out of; position is measured from its top-left corner. * @default undefined — the viewport constrains the panel instead. */ boundary?: React.RefObject | undefined; /** * The panel's window state, for controlled usage. Pair with `onStageChange`. * @default undefined — uncontrolled, tracks its own stage internally. */ stage?: FloatingPanelStage | undefined; /** * The panel's starting window state, for uncontrolled usage. * @default 'default' — the panel uses its own position and size. */ defaultStage?: FloatingPanelStage | undefined; /** * Fires with the new stage whenever a {@link FloatingPanelStageTrigger} is pressed. * @default undefined — no-op if omitted. */ onStageChange?: ((stage: FloatingPanelStage) => void) | undefined; /** * Whether the header can move the panel; `false` drops its `move` cursor. * @default true */ draggable?: boolean | undefined; /** * Whether resize triggers do anything; `false` makes every {@link FloatingPanelResizeTrigger} inert and non-focusable. * @default true */ resizable?: boolean | undefined; /** * How far one arrow key moves or resizes the panel, in pixels; Shift+arrow uses ten times this. * @default 10 */ step?: number | undefined; /** * Freezes the panel's geometry: drag, resize and stage changes are refused and their triggers leave the tab order. * @default false */ disabled?: boolean | undefined; /** * Words the polite live-region message announced once per drag or resize, on release; return `''` to announce nothing. * @default undefined — the built-in English wording ("Panel moved to 40, 25"). */ getGestureAnnouncement?: ((gesture: FloatingPanelGesture, detail: FloatingPanelGeometry) => string) | undefined; /** * DOM node the derived Portal renders the overlay into, so it inherits that subtree's theme and stacking context (e.g. a `[data-skdx-theme]` wrapper). * @default undefined — `document.body`. */ portalContainer?: Element | DocumentFragment | null | undefined; } /** The panel's geometry at the moment a gesture ended, handed to {@link FloatingPanelProps.getGestureAnnouncement}. */ export interface FloatingPanelGeometry { /** Top-left corner, in CSS pixels, rounded. */ position: FloatingPanelPosition; /** Width and height, in CSS pixels, rounded. */ size: FloatingPanelSize; } /** Props for {@link FloatingPanelTrigger}, the button that opens the panel. */ export interface FloatingPanelTriggerProps extends PrimitivePropsWithRef<'button'> { } /** Props for {@link FloatingPanelPortal}, which teleports the panel to `document.body`. */ export interface FloatingPanelPortalProps extends PortalProps { /** * Render the portal even while the panel is closed, instead of unmounting it. * @default false */ forceMount?: boolean | undefined; } /** Props for {@link FloatingPanelContent}, the movable surface itself. */ export interface FloatingPanelContentProps extends PrimitivePropsWithRef<'div'> { /** * Keep this panel mounted even while closed, for animation libraries that drive the exit themselves. * @default false */ forceMount?: boolean | undefined; /** * Fires when Escape is pressed while the panel is open. Call `event.preventDefault()` to stop it from closing. * @default undefined — no-op if omitted. */ onEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined; /** * Fires on pointer-down outside the panel and its trigger. Call `event.preventDefault()` to stop it from closing. * @default undefined — no-op if omitted. */ onPointerDownOutside?: ((event: PointerEvent) => void) | undefined; /** * Fires when focus moves outside the panel and trigger; non-modal panels don't auto-close on it. * @default undefined */ onFocusOutside?: ((event: FocusOutsideEvent) => void) | undefined; /** * Fires for pointer-down or focus outside, before specific handlers. One `preventDefault()` vetoes either. * @default undefined */ onInteractOutside?: ((event: InteractOutsideEvent) => void) | undefined; } /** Props for {@link FloatingPanelRoot}: {@link FloatingPanelProps} minus the derived-anatomy-only props. */ export type FloatingPanelRootProps = Omit; /** Props for {@link FloatingPanelHeader}, the drag handle. */ export interface FloatingPanelHeaderProps extends PrimitivePropsWithRef<'div'> { } /** Props for {@link FloatingPanelTitle}, the panel's accessible name. */ export interface FloatingPanelTitleProps extends PrimitivePropsWithRef<'h2'> { } /** Props for {@link FloatingPanelCloseTrigger}, a button that closes the panel. */ export interface FloatingPanelCloseTriggerProps extends PrimitivePropsWithRef<'button'> { } /** Props for {@link FloatingPanelResizeTrigger}, one edge or corner grip. */ export interface FloatingPanelResizeTriggerProps extends PrimitivePropsWithRef<'div'> { /** Which edge or corner this grip pulls; corners move two edges at once. */ direction: FloatingPanelResizeDirection; } /** Props for {@link FloatingPanelStageTrigger}, a minimize/maximize/restore button. */ export interface FloatingPanelStageTriggerProps extends PrimitivePropsWithRef<'button'> { /** The stage this button switches to; pressing it while already in that stage restores `'default'`. */ stage: FloatingPanelStage; }