/** @jsxImportSource preact */ import {createContext} from 'preact'; import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks'; import {DarkTheme, LightTheme} from '@deck.gl/widgets'; import type {ComponentChildren, JSX} from 'preact'; /** * Internal panel identifier used by both accordion and tab containers. */ type WidgetPanelId = string; /** Light/dark theme modes used for panel-scoped overrides. */ export type WidgetPanelThemeMode = 'light' | 'dark'; /** Public theme override options for widget panels. */ export type WidgetPanelTheme = 'inherit' | 'light' | 'dark' | 'invert'; /** * Describes one entry in an accordion or tabbed container. */ export type WidgetPanel = { /** Stable id used for expansion/selection bookkeeping. */ id: WidgetPanelId; /** Visible heading text for the panel. */ title: string; /** Renderable panel body. */ content: JSX.Element; /** Optional theme override applied to this panel subtree. */ theme?: WidgetPanelTheme; /** * If true, the panel can not be interacted with and will not switch/expand. */ disabled?: boolean; /** * If true, keep the panel mounted when collapsed (for preserving internal state). */ keepMounted?: boolean; }; export type WidgetPanelRecord = Record; export type AccordeonPanelProps = { /** * Map of panel IDs to panel definitions. */ panels: WidgetPanelRecord; /** Optional identifier for the wrapper panel when embedded in another container. */ id?: string; /** Optional heading used by outer overlays when this is rendered as a direct child panel. */ title?: string; /** Optional theme override applied to this panel subtree. */ theme?: WidgetPanelTheme; }; export type TabbedPanelProps = { /** * Map of panel IDs to panel definitions. */ panels: WidgetPanelRecord; /** Optional identifier for the wrapper panel when embedded in another container. */ id?: string; /** Optional heading used by outer overlays when this is rendered as a direct child panel. */ title?: string; /** Controls whether the tab list wraps onto multiple rows or scrolls horizontally. */ tabListLayout?: 'wrap' | 'scroll'; /** Optional theme override applied to this panel subtree. */ theme?: WidgetPanelTheme; }; export type ColumnPanelProps = { /** * Map of panel IDs to panel definitions. */ panels: WidgetPanelRecord; /** Optional identifier for the wrapper panel when embedded in another container. */ id?: string; /** Optional heading used by outer overlays when this is rendered as a direct child panel. */ title?: string; /** Optional theme override applied to this panel subtree. */ theme?: WidgetPanelTheme; }; export type CustomPanelProps = { /** Stable id used for expansion/selection bookkeeping. */ id: string; /** Visible heading text for the panel. */ title: string; /** * Called after the host element mounts. * Return a cleanup callback to dispose any manual DOM work on unmount. */ onRenderHTML: (rootElement: HTMLElement) => void | (() => void); /** * If true, the panel can not be interacted with and will not switch/expand. */ disabled?: boolean; /** * If true, keep the panel mounted when collapsed (for preserving internal state). */ keepMounted?: boolean; /** Optional class name applied to the host element. */ className?: string; /** Optional theme override applied to this panel subtree. */ theme?: WidgetPanelTheme; }; export type MarkdownPanelProps = { /** Stable id used for expansion/selection bookkeeping. */ id: string; /** Visible heading text for the panel. */ title: string; /** Markdown source rendered into a small built-in safe subset. */ markdown: string; /** * If true, the panel can not be interacted with and will not switch/expand. */ disabled?: boolean; /** * If true, keep the panel mounted when collapsed (for preserving internal state). */ keepMounted?: boolean; /** Optional class name applied to the markdown content host. */ className?: string; /** Optional theme override applied to this panel subtree. */ theme?: WidgetPanelTheme; }; /** * Normalizes an object map of panels into an array in insertion order. */ function normalizePanelRecordPanels(panels: WidgetPanelRecord): WidgetPanel[] { return Object.keys(panels).map((panelId) => { const panel = panels[panelId]; return { ...panel, id: panelId }; }); } /** * Mounts an imperative HTML host for custom widget panels. */ function CustomPanelContent({ className, onRenderHTML }: Pick) { const rootElementRef = useRef(null); useLayoutEffect(() => { const rootElement = rootElementRef.current; if (!rootElement) { return undefined; } const cleanup = onRenderHTML(rootElement); return () => { if (typeof cleanup === 'function') { cleanup(); } }; }, [onRenderHTML]); return
; } /** * A wrapper panel that renders child panels in an accordion layout. */ export class AccordeonPanel implements WidgetPanel { id: string; title: string; content: JSX.Element; theme?: WidgetPanelTheme; constructor({ panels, id = 'accordeon-widgets', title = 'Panels', theme = 'inherit' }: AccordeonPanelProps) { this.id = id; this.title = title; this.theme = theme; this.content = ; } } /** * A wrapper panel that renders child panels in a tabbed layout. */ export class TabbedPanel implements WidgetPanel { id: string; title: string; content: JSX.Element; theme?: WidgetPanelTheme; constructor({ panels, id = 'tabbed-widgets', title = 'Panels', tabListLayout = 'wrap', theme = 'inherit' }: TabbedPanelProps) { this.id = id; this.title = title; this.theme = theme; this.content = ( ); } } /** * A wrapper panel that renders child panels in a vertical column. */ export class ColumnPanel implements WidgetPanel { id: string; title: string; content: JSX.Element; theme?: WidgetPanelTheme; constructor({ panels, id = 'column-widgets', title = 'Panels', theme = 'inherit' }: ColumnPanelProps) { this.id = id; this.title = title; this.theme = theme; this.content = ; } } /** * A wrapper panel that renders imperative HTML content into a managed host element. */ export class CustomPanel implements WidgetPanel { id: string; title: string; content: JSX.Element; theme?: WidgetPanelTheme; disabled?: boolean; keepMounted?: boolean; constructor({ id, title, onRenderHTML, disabled, keepMounted, className, theme = 'inherit' }: CustomPanelProps) { this.id = id; this.title = title; this.theme = theme; this.disabled = disabled; this.keepMounted = keepMounted; this.content = ; } } /** * A wrapper panel that renders a minimal built-in Markdown subset without external parsers. */ export class MarkdownPanel implements WidgetPanel { id: string; title: string; content: JSX.Element; theme?: WidgetPanelTheme; disabled?: boolean; keepMounted?: boolean; constructor({ id, title, markdown, disabled, keepMounted, className, theme = 'inherit' }: MarkdownPanelProps) { this.id = id; this.title = title; this.theme = theme; this.disabled = disabled; this.keepMounted = keepMounted; this.content = (
{renderMarkdownBlocks(markdown)}
); } } /** * Shared props for both container implementations. */ export type WidgetContainerPanelBase = { /** Optional class name applied to the outer container. */ className?: string; /** Optional set of panels to render. */ panels: ReadonlyArray; }; /** * Single-panel container props for direct modal/sidebar content rendering. */ export type WidgetPanelContainerProps = { /** Optional class name applied to the outer container. */ className?: string; /** The panel to render as raw content. */ panel: WidgetPanel; }; /** * Accordion container properties. */ export type AccordeonWidgetContainerProps = WidgetContainerPanelBase & { /** Optional uncontrolled default expanded panel ids. */ defaultExpandedPanelIds?: ReadonlyArray; /** * Controlled expanded panel ids. If supplied, callers manage expand/collapse state. */ expandedPanelIds?: ReadonlyArray; /** * Called when user intent changes expanded panel ids. */ onExpandedPanelIdsChange?: (expandedPanelIds: ReadonlyArray) => void; /** * If false, opening one panel closes all others. Defaults to true. */ allowMultipleExpanded?: boolean; }; /** * Tabs container properties. */ export type TabbedWidgetContainerProps = WidgetContainerPanelBase & { /** Optional uncontrolled default active tab id. */ defaultActivePanelId?: WidgetPanelId; /** * Controlled active tab id. If supplied, callers manage active tab state. */ activePanelId?: WidgetPanelId; /** * Called when user intent changes active tab id. */ onActivePanelIdChange?: (activePanelId: WidgetPanelId | undefined) => void; /** Controls whether the tab list wraps onto multiple rows or scrolls horizontally. */ tabListLayout?: 'wrap' | 'scroll'; }; /** * Column container properties. */ export type ColumnWidgetContainerProps = WidgetContainerPanelBase; /** A serialized form describing an accordion widget container. */ export type WidgetAccordeonContainer = { /** Container variant discriminator. */ kind: 'accordeon'; /** Accordion container props. */ props: AccordeonWidgetContainerProps; }; /** A serialized form describing a tabbed widget container. */ export type WidgetTabbedContainer = { /** Container variant discriminator. */ kind: 'tabs'; /** Tabs container props. */ props: TabbedWidgetContainerProps; }; /** A serialized form describing a single panel widget container. */ export type WidgetPanelContainer = { /** Container variant discriminator. */ kind: 'panel'; /** Single-panel props. */ props: WidgetPanelContainerProps; }; /** A serialized widget-container description consumed by modal and sidebar widgets. */ export type WidgetContainer = | WidgetAccordeonContainer | WidgetTabbedContainer | WidgetPanelContainer; /** * Builds a direct-content container for modal-style and sidebar panel shorthands. */ export function asPanelContainer(panel: WidgetPanel): WidgetPanelContainer { return { kind: 'panel', props: { panel } }; } /** * Renders an accordion-style widget panel stack. */ export function AccordeonWidgetContainer({ panels, className, defaultExpandedPanelIds = [], expandedPanelIds, onExpandedPanelIdsChange, allowMultipleExpanded = true }: AccordeonWidgetContainerProps) { const [currentExpandedPanelIds, setCurrentExpandedPanelIds] = useControlledStringListState( expandedPanelIds, defaultExpandedPanelIds ); const expandedPanelIdSet = useMemo( () => new Set(currentExpandedPanelIds), [currentExpandedPanelIds] ); const effectivePanels = useMemo(() => [...panels], [panels]); const handleTogglePanel = useCallback( (panelId: WidgetPanelId) => { const nextPanelIds = new Set(allowMultipleExpanded ? currentExpandedPanelIds : []); if (nextPanelIds.has(panelId)) { nextPanelIds.delete(panelId); } else { if (!allowMultipleExpanded) { nextPanelIds.clear(); } nextPanelIds.add(panelId); } const next = [...nextPanelIds]; setCurrentExpandedPanelIds(next); onExpandedPanelIdsChange?.(next); }, [allowMultipleExpanded, currentExpandedPanelIds, onExpandedPanelIdsChange] ); return (
{effectivePanels.map((panel) => { const isExpanded = expandedPanelIdSet.has(panel.id); const shouldRenderContent = panel.keepMounted || isExpanded; return (
{shouldRenderContent ? ( {panel.content} ) : null}
); })}
); } /** * Renders a tabbed widget panel switcher. */ export function TabbedWidgetContainer({ panels, className, defaultActivePanelId, activePanelId, onActivePanelIdChange, tabListLayout = 'wrap' }: TabbedWidgetContainerProps) { const [currentActivePanelId, setCurrentActivePanelId] = useControlledStringState( activePanelId, defaultActivePanelId ); const enabledPanels = useMemo(() => panels.filter((panel) => !panel.disabled), [panels]); const initialActivePanelId = useMemo(() => { const isActivePanelEnabled = currentActivePanelId ? enabledPanels.some((panel) => panel.id === currentActivePanelId) : false; return isActivePanelEnabled ? currentActivePanelId : enabledPanels[0]?.id; }, [currentActivePanelId, enabledPanels]); useEffect(() => { const shouldSyncActivePanel = currentActivePanelId !== initialActivePanelId; if (!shouldSyncActivePanel || !initialActivePanelId) { return; } setCurrentActivePanelId(initialActivePanelId); onActivePanelIdChange?.(initialActivePanelId); }, [currentActivePanelId, initialActivePanelId, onActivePanelIdChange, setCurrentActivePanelId]); return (
{panels.map((panel) => { const isActive = panel.id === initialActivePanelId; return ( ); })}
{panels.map((panel) => { const isActive = panel.id === initialActivePanelId; return (
{panel.content}
); })}
); } /** * Renders child panels in a simple vertical column. */ export function ColumnWidgetContainer({panels, className}: ColumnWidgetContainerProps) { const effectivePanels = useMemo(() => [...panels], [panels]); return (
{effectivePanels.map((panel, panelIndex) => (
0 ? '1px solid var(--menu-border, rgba(148, 163, 184, 0.25))' : 'none', opacity: panel.disabled ? 0.55 : 1 }} > {panel.title ?
{panel.title}
: null}
{panel.content}
))}
); } /** * Renders the requested container based on descriptor kind. */ export function WidgetContainerRenderer({container}: {container: WidgetContainer}) { if (container.kind === 'accordeon') { return ; } if (container.kind === 'panel') { return (
{container.props.panel.content}
); } return ; } const PanelThemeModeContext = createContext(undefined); /** * Returns the effective light/dark theme mode for the current panel subtree. */ export function useEffectiveWidgetPanelThemeMode(): WidgetPanelThemeMode { return useContext(PanelThemeModeContext) ?? 'light'; } /** * Applies a panel-level theme override and exposes the resolved mode to descendants. */ function WidgetPanelThemeScope({ panel, children }: { panel: WidgetPanel; children: ComponentChildren; }) { const inheritedMode = useContext(PanelThemeModeContext); const hostElementRef = useRef(null); const [rootMode, setRootMode] = useState('light'); const parentMode = inheritedMode ?? rootMode; const resolvedMode = resolveWidgetPanelThemeMode(parentMode, panel.theme); useLayoutEffect(() => { if (!inheritedMode) { const hostElement = hostElementRef.current; if (!hostElement) { return undefined; } const parentHostElement = hostElement.parentElement instanceof HTMLElement ? hostElement.parentElement : hostElement; const updateRootMode = () => { const inferredMode = inferWidgetPanelThemeMode(parentHostElement); setRootMode((previousMode) => previousMode === inferredMode ? previousMode : inferredMode ); }; updateRootMode(); const themedContainer = parentHostElement.closest('.deck-widget-container'); const mutationObserver = new MutationObserver(() => { updateRootMode(); }); mutationObserver.observe(parentHostElement, { attributes: true, attributeFilter: ['style', 'class'] }); if (themedContainer && themedContainer !== parentHostElement) { mutationObserver.observe(themedContainer, { attributes: true, attributeFilter: ['style', 'class'] }); } return () => { mutationObserver.disconnect(); }; } return undefined; }, [inheritedMode]); return (
{children}
); } /** * Resolves one local panel theme override against its parent effective mode. */ function resolveWidgetPanelThemeMode( parentMode: WidgetPanelThemeMode, theme: WidgetPanelTheme | undefined ): WidgetPanelThemeMode { if (theme === 'dark') { return 'dark'; } if (theme === 'light') { return 'light'; } if (theme === 'invert') { return parentMode === 'dark' ? 'light' : 'dark'; } return parentMode; } /** * Builds the inline CSS variable scope for one resolved panel theme mode. */ function getWidgetPanelThemeScopeStyle(mode: WidgetPanelThemeMode): JSX.CSSProperties { const themeVariables = mode === 'dark' ? DarkTheme : LightTheme; return {...themeVariables} as JSX.CSSProperties; } /** * Infers the surrounding widget theme mode from resolved CSS variables. */ function inferWidgetPanelThemeMode(hostElement: HTMLElement): WidgetPanelThemeMode { const ownerWindow = hostElement.ownerDocument.defaultView; if (!ownerWindow) { return 'light'; } const computedStyle = ownerWindow.getComputedStyle(hostElement); const menuBackground = computedStyle.getPropertyValue('--menu-background').trim(); const parsedColor = parseThemeColor(menuBackground); if (!parsedColor) { return 'light'; } return getRelativeLuminance(parsedColor) < 0.5 ? 'dark' : 'light'; } /** * Parses a CSS rgb/rgba/hex color string into numeric channels. */ function parseThemeColor(value: string): [number, number, number] | null { if (!value) { return null; } if (value.startsWith('#')) { const hex = value.slice(1); if (hex.length === 3) { return [ parseInt(hex[0] + hex[0], 16), parseInt(hex[1] + hex[1], 16), parseInt(hex[2] + hex[2], 16) ]; } if (hex.length >= 6) { return [ parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16) ]; } return null; } const channelMatches = value.match(/[\d.]+/g); if (!channelMatches || channelMatches.length < 3) { return null; } return [Number(channelMatches[0]), Number(channelMatches[1]), Number(channelMatches[2])]; } /** * Computes relative luminance for an RGB color. */ function getRelativeLuminance([red, green, blue]: [number, number, number]): number { return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255; } /** * Keeps local string-state in sync with controlled/uncontrolled props. */ function useControlledStringState( controlledValue?: WidgetPanelId, defaultValue?: WidgetPanelId ): [WidgetPanelId | undefined, (next: WidgetPanelId | undefined) => void] { const [internalValue, setInternalValue] = useState(defaultValue); const isControlled = controlledValue !== undefined; const resolvedValue = isControlled ? controlledValue : internalValue; useEffect(() => { if (!isControlled) { return; } setInternalValue(controlledValue); }, [isControlled, controlledValue]); const setValue = useCallback( (next: WidgetPanelId | undefined) => { if (!isControlled) { setInternalValue(next); } }, [isControlled] ); return [resolvedValue, setValue]; } /** * Renders a Markdown document into a safe built-in subset of block elements. */ /* eslint-disable max-statements, no-continue */ function renderMarkdownBlocks(markdown: string): JSX.Element[] { const normalizedMarkdown = markdown.replace(/\r\n?/g, '\n'); const lines = normalizedMarkdown.split('\n'); const blocks: JSX.Element[] = []; const paragraphLines: string[] = []; const unorderedListItems: string[] = []; const orderedListItems: string[] = []; let fencedCodeLines: string[] | undefined; let blockIndex = 0; const flushParagraph = () => { if (paragraphLines.length === 0) { return; } const paragraphText = paragraphLines.join(' '); const currentBlockKey = `paragraph-${blockIndex++}`; blocks.push(

{renderInlineMarkdown(paragraphText, currentBlockKey)}

); paragraphLines.length = 0; }; const flushUnorderedList = () => { if (unorderedListItems.length === 0) { return; } const currentBlockIndex = blockIndex++; blocks.push(
    {unorderedListItems.map((item, itemIndex) => (
  • {renderInlineMarkdown(item, `unordered-item-${currentBlockIndex}-${itemIndex}`)}
  • ))}
); unorderedListItems.length = 0; }; const flushOrderedList = () => { if (orderedListItems.length === 0) { return; } const currentBlockIndex = blockIndex++; blocks.push(
    {orderedListItems.map((item, itemIndex) => (
  1. {renderInlineMarkdown(item, `ordered-item-${currentBlockIndex}-${itemIndex}`)}
  2. ))}
); orderedListItems.length = 0; }; const flushFencedCodeBlock = () => { if (fencedCodeLines === undefined) { return; } blocks.push(
        {fencedCodeLines.join('\n')}
      
); fencedCodeLines = undefined; }; for (const line of lines) { const trimmedLine = line.trim(); if (fencedCodeLines !== undefined) { if (trimmedLine.startsWith('```')) { flushFencedCodeBlock(); } else { fencedCodeLines.push(line); } continue; } if (trimmedLine.startsWith('```')) { flushParagraph(); flushUnorderedList(); flushOrderedList(); fencedCodeLines = []; continue; } if (trimmedLine.length === 0) { flushParagraph(); flushUnorderedList(); flushOrderedList(); continue; } const headingMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { flushParagraph(); flushUnorderedList(); flushOrderedList(); const headingLevel = headingMatch[1].length; const headingText = headingMatch[2]; const HeadingTag = `h${headingLevel}` as keyof JSX.IntrinsicElements; const currentBlockKey = `heading-${blockIndex++}`; blocks.push( {renderInlineMarkdown(headingText, currentBlockKey)} ); continue; } const unorderedListMatch = trimmedLine.match(/^[-*]\s+(.+)$/); if (unorderedListMatch) { flushParagraph(); flushOrderedList(); unorderedListItems.push(unorderedListMatch[1]); continue; } const orderedListMatch = trimmedLine.match(/^\d+\.\s+(.+)$/); if (orderedListMatch) { flushParagraph(); flushUnorderedList(); orderedListItems.push(orderedListMatch[1]); continue; } paragraphLines.push(trimmedLine); } flushParagraph(); flushUnorderedList(); flushOrderedList(); flushFencedCodeBlock(); return blocks; } /** * Renders a minimal inline Markdown subset into safe text and inline elements. */ // eslint-disable-next-line complexity function renderInlineMarkdown(source: string, keyPrefix: string): ComponentChildren[] { const inlineTokenPattern = /(\[([^\]]+)\]\(([^)\s]+)\)|`([^`]+)`|\*\*([^*]+)\*\*|__([^_]+)__|\*([^*]+)\*|_([^_]+)_)/g; const children: ComponentChildren[] = []; let match: RegExpExecArray | null; let previousIndex = 0; let tokenIndex = 0; for (match = inlineTokenPattern.exec(source); match; match = inlineTokenPattern.exec(source)) { if (match.index > previousIndex) { children.push(source.slice(previousIndex, match.index)); } if (match[2] !== undefined && match[3] !== undefined) { children.push( {match[2]} ); } else if (match[4] !== undefined) { children.push( {match[4]} ); } else if (match[5] !== undefined || match[6] !== undefined) { children.push( {match[5] ?? match[6]} ); } else if (match[7] !== undefined || match[8] !== undefined) { children.push({match[7] ?? match[8]}); } previousIndex = match.index + match[0].length; tokenIndex += 1; } if (previousIndex < source.length) { children.push(source.slice(previousIndex)); } if (children.length === 0) { return [source]; } return children; } /* eslint-enable max-statements, no-continue */ /** * Returns the heading style for one Markdown heading level. */ function getMarkdownHeadingStyle(level: number): JSX.CSSProperties { if (level === 1) { return MARKDOWN_HEADING_1_STYLE; } if (level === 2) { return MARKDOWN_HEADING_2_STYLE; } if (level === 3) { return MARKDOWN_HEADING_3_STYLE; } return MARKDOWN_HEADING_4_TO_6_STYLE; } /** * Normalizes panel id collections for deterministic membership checks and stable state. */ function normalizePanelIds( values: ReadonlyArray | undefined ): ReadonlyArray { if (!values || values.length === 0) { return []; } const deduped: WidgetPanelId[] = []; const seen = new Set(); for (const value of values) { const trimmed = String(value); if (!seen.has(trimmed)) { seen.add(trimmed); deduped.push(trimmed); } } return deduped; } /** * Keeps local list state in sync with controlled/uncontrolled props. */ function useControlledStringListState( controlledValue?: ReadonlyArray, defaultValue?: ReadonlyArray ): [ReadonlyArray, (next: ReadonlyArray) => void] { const [internalValue, setInternalValue] = useState>(() => normalizePanelIds(controlledValue ?? defaultValue) ); const isControlled = controlledValue !== undefined; const resolvedValue = isControlled ? normalizePanelIds(controlledValue) : internalValue; useEffect(() => { if (!isControlled) { return; } setInternalValue(normalizePanelIds(controlledValue)); }, [isControlled, controlledValue]); const setValue = useCallback( (next: ReadonlyArray) => { if (!isControlled) { setInternalValue(normalizePanelIds(next)); } }, [isControlled] ); return [resolvedValue, setValue]; } /** * Shared accordion/tab layout styles. */ const ACCORDEON_CONTAINER_STYLE: JSX.CSSProperties = { display: 'flex', flexDirection: 'column', gap: '8px' }; const ACCORDION_PANEL_STYLE: JSX.CSSProperties = { border: 'var(--menu-border, 1px solid rgba(148, 163, 184, 0.35))', borderRadius: 'var(--button-corner-radius, 8px)', overflow: 'hidden' }; const ACCORDION_HEADING_STYLE: JSX.CSSProperties = { width: '100%', border: '0', margin: '0', padding: '10px 12px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', backgroundColor: 'var(--menu-background, #fff)', color: 'var(--menu-text, rgb(24, 24, 26))', fontSize: '12px', lineHeight: 1.2, fontWeight: 700 }; const ACCORDION_CONTENT_STYLE: JSX.CSSProperties = { padding: '8px 10px 10px 12px', borderTop: 'var(--menu-divider, var(--menu-border, 1px solid rgba(148, 163, 184, 0.25)))', backgroundColor: 'var(--menu-background, #fff)', color: 'var(--menu-text, rgb(24, 24, 26))' }; const TABBED_CONTAINER_STYLE: JSX.CSSProperties = { display: 'flex', flexDirection: 'column', minWidth: '220px', border: 'var(--menu-border, 1px solid rgba(148, 163, 184, 0.35))', borderRadius: 'var(--button-corner-radius, 8px)', overflow: 'hidden', backgroundColor: 'var(--menu-background, #fff)', color: 'var(--menu-text, rgb(24, 24, 26))' }; function getTabListStyle(tabListLayout: 'wrap' | 'scroll'): JSX.CSSProperties { return { display: 'flex', alignItems: 'flex-start', flexWrap: tabListLayout === 'wrap' ? 'wrap' : 'nowrap', gap: '4px', overflowX: tabListLayout === 'scroll' ? 'auto' : 'hidden', overflowY: 'hidden', background: 'var(--menu-weak-background, var(--button-background, var(--menu-background, #fff)))', padding: '4px 6px', borderBottom: 'var(--menu-divider, var(--menu-border, 1px solid rgba(148, 163, 184, 0.2)))', position: 'sticky', top: 0, zIndex: 1 }; } const TAB_BUTTON_STYLE: JSX.CSSProperties = { flex: '0 0 auto', border: '1px solid transparent', borderRadius: '4px', margin: 0, padding: '4px 8px', fontSize: '12px', fontWeight: 600, lineHeight: 1.25, cursor: 'pointer', color: 'var(--menu-text, rgb(24, 24, 26))', backgroundColor: 'rgba(255, 255, 255, 0.35)', whiteSpace: 'nowrap', transition: 'background-color 120ms ease, border-color 120ms ease, color 120ms ease' }; const TAB_PANEL_STYLE: JSX.CSSProperties = { display: 'grid', alignItems: 'start', padding: '10px', overflow: 'auto' }; const TAB_PANEL_CONTENT_STYLE: JSX.CSSProperties = { gridArea: '1 / 1', minWidth: 0 }; const COLUMN_CONTAINER_STYLE: JSX.CSSProperties = { display: 'grid', gap: '0' }; const COLUMN_PANEL_STYLE: JSX.CSSProperties = { display: 'grid', gap: '10px', padding: '12px 0' }; const COLUMN_PANEL_HEADER_STYLE: JSX.CSSProperties = { fontSize: '12px', fontWeight: 700, color: 'var(--button-text, currentColor)', padding: '0 2px' }; const COLUMN_PANEL_CONTENT_STYLE: JSX.CSSProperties = { minWidth: 0 }; const MARKDOWN_PANEL_STYLE: JSX.CSSProperties = { display: 'flex', flexDirection: 'column', gap: '12px', color: 'var(--menu-text, rgb(24, 24, 26))', fontSize: '13px', lineHeight: '1.5' }; const MARKDOWN_PARAGRAPH_STYLE: JSX.CSSProperties = { margin: '0' }; const MARKDOWN_LIST_STYLE: JSX.CSSProperties = { margin: '0', paddingLeft: '18px', display: 'flex', flexDirection: 'column', gap: '6px' }; const MARKDOWN_LIST_ITEM_STYLE: JSX.CSSProperties = { margin: '0' }; const MARKDOWN_CODE_BLOCK_STYLE: JSX.CSSProperties = { margin: '0', padding: '10px 12px', borderRadius: '8px', overflowX: 'auto', background: 'var(--menu-weak-background, var(--button-background, var(--menu-background, #fff)))', border: '1px solid var(--menu-border, rgba(148, 163, 184, 0.2))', color: 'var(--menu-text, rgb(24, 24, 26))', fontSize: '12px', lineHeight: '1.45' }; const MARKDOWN_INLINE_CODE_STYLE: JSX.CSSProperties = { padding: '1px 5px', borderRadius: '4px', background: 'var(--menu-weak-background, var(--button-background, var(--menu-background, #fff)))', border: '1px solid var(--menu-border, rgba(148, 163, 184, 0.2))', color: 'var(--menu-text, rgb(24, 24, 26))', fontSize: '12px' }; const MARKDOWN_LINK_STYLE: JSX.CSSProperties = { color: 'var(--button-text, rgb(29, 78, 216))' }; const MARKDOWN_HEADING_1_STYLE: JSX.CSSProperties = { margin: '0', fontSize: '20px', fontWeight: 700, lineHeight: '1.25' }; const MARKDOWN_HEADING_2_STYLE: JSX.CSSProperties = { margin: '0', fontSize: '17px', fontWeight: 700, lineHeight: '1.3' }; const MARKDOWN_HEADING_3_STYLE: JSX.CSSProperties = { margin: '0', fontSize: '15px', fontWeight: 700, lineHeight: '1.35' }; const MARKDOWN_HEADING_4_TO_6_STYLE: JSX.CSSProperties = { margin: '0', fontSize: '13px', fontWeight: 700, lineHeight: '1.4' };