/** @jsxImportSource preact */ import {Widget} from '@deck.gl/core'; import {render} from 'preact'; import {asPanelContainer, WidgetContainerRenderer} from './widget-containers'; import type {WidgetContainer, WidgetPanel} from './widget-containers'; import type {WidgetPlacement, WidgetProps} from '@deck.gl/core'; import type {JSX} from 'preact'; /** Full-screen panel widget properties. */ export type FullScreenPanelWidgetProps = WidgetProps & { /** The content container to show inside the full-screen panel. */ container?: WidgetContainer; /** Optional shorthand panel. When supplied, shown directly inside the full-screen panel. */ panel?: WidgetPanel; /** Placement anchor for the full-screen panel. Defaults to deck.gl's fill placement. */ placement?: WidgetPlacement; /** Optional panel header title. */ title?: string; /** Inset from the deck overlay edge in pixels. */ marginPx?: number; }; const FULL_SCREEN_PANEL_WIDGET_CLASS = 'deck-widget-full-screen-panel'; /** * Normalizes inset margin into a practical render value. */ function normalizeMarginPx(marginPx: number): number { const clamped = Math.max(0, Math.floor(marginPx)); return Number.isFinite(clamped) ? clamped : 24; } /** * Normalizes widget-container/panel inputs into a concrete widget container. */ function asContainer(container?: WidgetContainer, panel?: WidgetPanel): WidgetContainer { if (container !== undefined) { return container; } if (panel !== undefined) { return asPanelContainer(panel); } return { kind: 'panel', props: { panel: { id: 'empty-full-screen-panel', title: '', content:
} } }; } function FullScreenPanelWidgetView({ container, title, marginPx }: { container: WidgetContainer; title?: string; marginPx: number; }) { return (
{title ?
{title}
: null}
); } /** * Prevents full-screen panel interactions from leaking into the underlying deck canvas. */ function stopFullScreenPanelEventPropagation(event: Event): void { event.stopPropagation(); } /** * A reusable deck widget that renders one container inside a large inset panel. */ export class FullScreenPanelWidget extends Widget { static defaultProps: Required = { ...Widget.defaultProps, id: 'full-screen-panel-widget', placement: 'fill', title: undefined!, marginPx: 24, panel: undefined!, container: { kind: 'panel', props: { panel: { id: 'empty-full-screen-panel', title: '', content:
} } } }; className = FULL_SCREEN_PANEL_WIDGET_CLASS; placement: WidgetPlacement = FullScreenPanelWidget.defaultProps.placement; title: string | undefined = FullScreenPanelWidget.defaultProps.title; marginPx = FullScreenPanelWidget.defaultProps.marginPx; #container: WidgetContainer = FullScreenPanelWidget.defaultProps.container; #rootElement: HTMLElement | null = null; constructor(props: Partial = {}) { super({ ...FullScreenPanelWidget.defaultProps, ...props, container: asContainer(props.container, props.panel) } as FullScreenPanelWidgetProps); this.setProps(this.props); } setProps(props: Partial): void { if (props.placement !== undefined) { this.placement = props.placement; } if ('title' in props) { this.title = props.title; } if (props.marginPx !== undefined) { this.marginPx = normalizeMarginPx(props.marginPx); } if (props.container !== undefined) { this.#container = props.container; } else if (props.panel !== undefined) { this.#container = asContainer(undefined, props.panel); } this.#render(); super.setProps(props); } onRemove(): void { if (this.#rootElement) { render(null, this.#rootElement); } } onRenderHTML(rootElement: HTMLElement): void { this.#rootElement = rootElement; const className = ['deck-widget', this.className, this.props.className] .filter(Boolean) .join(' '); rootElement.className = className; rootElement.style.position = 'absolute'; rootElement.style.inset = '0'; rootElement.style.margin = '0'; rootElement.style.pointerEvents = 'none'; rootElement.style.zIndex = '30'; this.#render(); } #render = () => { if (!this.#rootElement) { return; } render( , this.#rootElement ); }; } const FULL_SCREEN_PANEL_WIDGET_STYLE = (marginPx: number): JSX.CSSProperties => ({ position: 'absolute', inset: `${marginPx}px`, pointerEvents: 'auto', display: 'grid', gridTemplateRows: 'auto minmax(0, 1fr)', border: 'var(--menu-border, 1px solid rgba(148, 163, 184, 0.42))', borderRadius: '18px', background: 'var(--menu-background, rgba(255, 255, 255, 0.94))', backdropFilter: 'var(--menu-backdrop-filter, blur(18px))', color: 'var(--menu-text, rgb(24, 24, 26))', boxShadow: 'var(--menu-shadow, 0 28px 80px rgba(15, 23, 42, 0.24))', overflow: 'hidden' }); const FULL_SCREEN_PANEL_HEADER_STYLE: JSX.CSSProperties = { padding: '18px 22px 12px', fontSize: '24px', fontWeight: 800, lineHeight: 1.1, letterSpacing: '-0.03em', color: 'var(--button-text, currentColor)' }; const FULL_SCREEN_PANEL_CONTENT_STYLE: JSX.CSSProperties = { minHeight: 0, overflow: 'auto', padding: '0 22px 22px' };