import { ComponentPropsWithoutRef, RefObject } from 'react'; import { BoxBackgroundColor } from '@viasat/beam-shared/components/box'; import { PanelDividerColor, PanelDividerColors } from '@viasat/beam-shared/components/panel'; export type { BoxBackgroundColor, PanelDividerColor }; export { PanelDividerColors }; export type PanelKind = 'inline' | 'overlay'; export type ResolvedPanelOffset = { x: string; y: string; }; export type PanelPosition = 'start' | 'end' | 'bottom'; export type PanelModalType = 'nonModal' | 'isModal' | 'alert'; export type PanelWidth = 'sm' | 'md' | 'lg' | (string & {}); export type PanelHeight = string; export type PanelPadding = 'sm' | 'md'; /** * Configuration for resizable panels. * Pass `true` to use defaults, or an object for custom constraints. */ export type PanelResizable = boolean | { /** Minimum panel size in px. Default: 165 for start/end, 100 for bottom. */ minSize?: number; /** Maximum panel size in px. Default: 50% viewport width for start/end, 90% viewport height for bottom. */ maxSize?: number; /** localStorage key for persisting the panel's last-used size across sessions. */ storageKey?: string; }; /** * The reason a Panel close was requested via user interaction. * * - `'escapeKey'` — user pressed Escape (overlay panels only) * - `'outsidePress'` — user clicked outside the panel (overlay panels only) * - `'closeButton'` — user clicked the header close button * - `'programmatic'` — close was triggered by another interaction not covered above */ export type PanelCloseReason = 'escapeKey' | 'outsidePress' | 'closeButton' | 'programmatic'; interface BasePanelProps extends ComponentPropsWithoutRef<'aside'> { /** * Specify the content of the Panel */ 'children': React.ReactNode; /** * Accessible name for the panel dialog, required when `Panel.Header.Heading` is not rendered. * * With a heading present, `aria-labelledby` points to it automatically; without one, a * `role="dialog"` needs this so screen readers can announce the Panel. * * @example * ```tsx * * ``` */ 'aria-label'?: string; /** * Controls the Panel's visibility. For inline panels, omitting this prop * renders the panel as always-visible (uncontrolled). */ 'open'?: boolean; /** * Callback when Panel open state changes */ 'onOpenChange'?: (open: boolean) => void; /** * Width of a side Panel. * * Applies only when `position` is `"start"` or `"end"`. Has no effect on * `position="bottom"` panels; use `height` for bottom panels instead. * * Use a named preset (`"sm"`, `"md"`, `"lg"`) or any valid CSS width value * such as `"480px"` or `"30vw"`. * * When omitted, the Panel uses the `"md"` width preset. * @default 'md' */ 'width'?: PanelWidth; /** * Height of a bottom Panel. * * Applies only when `position="bottom"`. Has no effect on side panels * (`position="start"` or `position="end"`); use `width` for those instead. * * Use any valid CSS height value, such as `"50dvh"` or `"480px"`. * When omitted, the Panel sizes to its content and is capped at `90dvh`. * Avoid `height="auto"` because it removes the `90dvh` viewport cap. */ 'height'?: PanelHeight; /** * Specify the horizontal padding * sm=1rem, md=1.5rem * @default 'md' */ 'padding'?: PanelPadding; /** * Specify whether to show the header close button * @default true */ 'dismissible'?: boolean; /** * Label for the close button. Used as both the button's `aria-label` * and the visible tooltip text. Provide a value that works in both contexts. * @default 'Close panel' */ 'closeButtonAriaLabel'?: string; /** * Specify whether to animate the Panel as it opens and closes * @default true */ 'isAnimated'?: boolean; /** * Specify whether to show an external divider between panel and page content * @default true */ 'divider'?: boolean; /** * Specify the background color for all panel surfaces (Header, Body, Footer). * A `backgroundColor` set on a section takes priority. */ 'backgroundColor'?: BoxBackgroundColor; /** * Specify the border color of the external divider between the panel and page content. Also sets * the default divider color for the Header and Footer; a `dividerColor` set on a * section takes priority. * @default '01' */ 'dividerColor'?: PanelDividerColor; /** * Specify where the Panel is anchored within its container * @default 'start' */ 'position'?: PanelPosition; /** * Enables user-resizing via a drag handle at the panel edge. * Pass `true` to use default min/max constraints, or an object to set custom * values in pixels. * * Provide a `storageKey` in the config object to persist the panel's last-used size across sessions. * @default false */ 'resizable'?: PanelResizable; /** * Runs before a user-initiated close and acts as a gate: return `false` (or * `Promise`) to keep the Panel open, for example when a form has unsaved changes. * * It only fires for UI-triggered closes (close button, Escape, or outside press), never * when the parent changes the `open` prop programmatically. Under `modalType="alert"`, * the `'escapeKey'` and `'outsidePress'` reasons are suppressed, so it won't fire for * those either. * * @example * ```tsx * // Drive a confirmation dialog from state; avoid window.confirm (blocking, breaks iframes). * // See the "Preserving and Guarding State" story for a full reference implementation. * { * if (!isDirty) return true; * return new Promise(resolve => { * setResolveClose(() => resolve); * setShowConfirmDialog(true); * }); * }} * /> * ``` */ 'onBeforeClose'?: (reason: PanelCloseReason) => boolean | Promise; } export interface InlinePanelProps extends BasePanelProps { /** * Render the Panel inline: it sits in document flow and pushes adjacent content aside * when open. Required to opt into the inline kind; the Panel defaults to `'overlay'`. */ kind: 'inline'; modalType?: never; offset?: never; } export interface OverlayPanelProps extends BasePanelProps { /** * Render the Panel as an overlay: portalled and floating above the page. * @default 'overlay' */ kind?: 'overlay'; /** * Modal behavior (overlay panels only). * * - `'isModal'` (default) — backdrop overlay with focus trapped; Escape and a backdrop * click close it. * - `'nonModal'` — page stays interactive; Escape closes the panel; outside clicks close * it only when `closeOnOutsidePress={true}`. * - `'alert'` — close button hidden; Escape and outside clicks are suppressed. * * @default 'isModal' */ modalType?: PanelModalType; /** * When `modalType="nonModal"`, allows the panel to close when the user * clicks outside it. Has no effect for `'isModal'` or `'alert'` panels. * @default false */ closeOnOutsidePress?: boolean; /** * Insets an overlay Panel from the viewport edge, giving it rounded corners and * removing the outer divider (regardless of the `divider` prop). * * Pass `true` for the default `1rem`, a CSS length like `"2rem"` for a uniform offset, * or a `{ x, y }` object like `{ x: "2rem", y: "0.5rem" }` to offset the axes * independently. * @default false */ offset?: boolean | string | { x: string; y: string; }; /** * Portal target for the overlay. When set, the Panel renders inside this element with * `position: absolute` instead of `position: fixed`. The container needs to: * - establish a containing block (e.g. `position: relative`) * - set an appropriate `z-index` * - use `overflow: hidden` (or `clip`), so the slide-in/out transform doesn't flash a * transient scrollbar * * Pass the resolved element, or `null` until it exists — resolution is left to the implementer. Use state * or a callback ref so the value is reactive: when it flips from `null` to the element, * the Panel re-renders into the container. A plain `useRef` whose `.current` is mutated * won't trigger that. * * Has no effect on `kind="inline"`. * * @example * ```tsx * const [container, setContainer] = useState(null); * return ( *
* *
* ); * ``` */ container?: HTMLElement | null; /** * The element that opens the Panel. Clicks on it are excluded from outside-press * dismissal, so it can toggle the Panel without the close immediately re-firing, and * focus returns to it when the Panel closes. No effect on `kind="inline"`. * * Avoid `triggerRef` when the Panel is nested inside another FloatingUI floating element. */ triggerRef?: RefObject; } export type PanelProps = InlinePanelProps | OverlayPanelProps; /** * Resolved resize configuration passed to internal Panel components. * Presence signals resizing is enabled; absence signals it is disabled. */ export type PanelResizeConfig = { /** @default 165 for start/end, 100 for bottom */ minSize: number; /** @default 50dvw for start/end, 90dvh for bottom */ maxSize: number; /** localStorage key for size persistence */ storageKey: string; }; export type { PanelHeaderProps } from './Header/Panel.Header';