/** @jsxImportSource preact */ import {Widget} from '@deck.gl/core'; import {render} from 'preact'; import type {WidgetPlacement, WidgetProps} from '@deck.gl/core'; import type {ComponentChild, JSX} from 'preact'; export type ToolbarWidgetActionItem = { kind: 'action'; id: string; label: string; icon?: ComponentChild; title?: string; disabled?: boolean; active?: boolean; onClick?: () => void; }; export type ToolbarWidgetToggleOption = { id: string; label: string; icon?: ComponentChild; title?: string; disabled?: boolean; }; export type ToolbarWidgetToggleGroupItem = { kind: 'toggle-group'; id: string; label?: string; title?: string; disabled?: boolean; selectedId?: string | null; options: ToolbarWidgetToggleOption[]; onSelect?: (optionId: string) => void; }; export type ToolbarWidgetBadgeItem = { kind: 'badge'; id: string; label: string; title?: string; }; export type ToolbarWidgetItem = | ToolbarWidgetActionItem | ToolbarWidgetToggleGroupItem | ToolbarWidgetBadgeItem; export type ToolbarWidgetProps = WidgetProps & { placement?: WidgetPlacement; items?: ToolbarWidgetItem[]; }; const ROOT_STYLE: Partial = { position: 'absolute', display: 'flex', pointerEvents: 'auto', userSelect: 'none', zIndex: '99' }; const TOOLBAR_STYLE: JSX.CSSProperties = { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap', padding: '8px 10px', borderRadius: '999px', background: 'rgba(36, 40, 41, 0.88)', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.25)' }; const ITEM_GROUP_STYLE: JSX.CSSProperties = { display: 'flex', alignItems: 'center', gap: '6px', minWidth: '0' }; const BUTTON_STYLE: JSX.CSSProperties = { appearance: 'none', border: 'none', borderRadius: '999px', background: 'transparent', color: '#f0f0f0', minHeight: '30px', padding: '0 10px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '6px', cursor: 'pointer', fontSize: '11px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontWeight: '500', transition: 'background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease', whiteSpace: 'nowrap' }; const ACTIVE_BUTTON_STYLE: JSX.CSSProperties = { background: '#0071e3', color: '#ffffff', boxShadow: '0 0 0 2px rgba(255, 255, 255, 0.35)' }; const DISABLED_BUTTON_STYLE: JSX.CSSProperties = { opacity: 0.45, cursor: 'default' }; const GROUP_LABEL_STYLE: JSX.CSSProperties = { color: 'rgba(255, 255, 255, 0.65)', fontSize: '11px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', whiteSpace: 'nowrap' }; const BADGE_STYLE: JSX.CSSProperties = { padding: '0 8px', minHeight: '28px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: '999px', background: 'rgba(255, 255, 255, 0.12)', color: 'rgba(255, 255, 255, 0.88)', fontSize: '11px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', whiteSpace: 'nowrap' }; function stopToolbarEventPropagation(event: Event) { event.stopPropagation(); if ( typeof (event as {stopImmediatePropagation?: () => void}).stopImmediatePropagation === 'function' ) { (event as {stopImmediatePropagation?: () => void}).stopImmediatePropagation?.(); } } function ToolbarWidgetView({items}: {items: ToolbarWidgetItem[]}) { return (
{items.map((item) => renderToolbarItem(item))}
); } function renderToolbarItem(item: ToolbarWidgetItem): JSX.Element { if (item.kind === 'badge') { return (
{item.label}
); } if (item.kind === 'action') { const disabled = item.disabled ?? false; const buttonStyle: JSX.CSSProperties = { ...BUTTON_STYLE, ...(item.active ? ACTIVE_BUTTON_STYLE : {}), ...(disabled ? DISABLED_BUTTON_STYLE : {}) }; return ( ); } const groupDisabled = item.disabled ?? false; return (
{item.label ? {item.label} : null} {item.options.map((option) => { const disabled = groupDisabled || option.disabled; const active = item.selectedId === option.id; const buttonStyle: JSX.CSSProperties = { ...BUTTON_STYLE, ...(active ? ACTIVE_BUTTON_STYLE : {}), ...(disabled ? DISABLED_BUTTON_STYLE : {}) }; return ( ); })}
); } export class ToolbarWidget extends Widget { static defaultProps: Required = { ...Widget.defaultProps, id: 'toolbar-widget', placement: 'top-right', items: [] }; className = 'deck-widget-toolbar'; placement: WidgetPlacement = ToolbarWidget.defaultProps.placement; #rootElement: HTMLElement | null = null; constructor(props: ToolbarWidgetProps = {}) { super({...ToolbarWidget.defaultProps, ...props}); this.setProps(this.props); } override setProps(props: Partial): void { if (props.placement !== undefined) { this.placement = props.placement; } super.setProps(props); this.#render(); } override onRemove(): void { if (this.#rootElement) { render(null, this.#rootElement); } } override onRenderHTML(rootElement: HTMLElement): void { this.#rootElement = rootElement; const className = ['deck-widget', this.className, this.props.className] .filter(Boolean) .join(' '); rootElement.className = className; Object.assign(rootElement.style, ROOT_STYLE, this.props.style ?? {}); this.#render(); } #render() { if (!this.#rootElement) { return; } render(, this.#rootElement); } }