import { Box, Text } from "../../ui"; import { useEffect, useId, type ReactNode } from "react"; import type { PaneSettingField } from "../../types/plugin"; import { blendHex, colors } from "../../theme/colors"; import { SelectField, type SelectFieldHandle } from "../ui/select-field"; import { Checkbox } from "../ui/checkbox"; import { DialogFrame } from "../ui/frame"; import { isPaneSettingDisabled, summarizePaneSettingValue } from "./value"; const DESKTOP_TEXT_STYLE = { letterSpacing: 0, lineHeight: "var(--cell-h)", } as const; export function desktopText(weight?: number) { return weight ? { ...DESKTOP_TEXT_STYLE, fontWeight: weight } : DESKTOP_TEXT_STYLE; } function desktopSubtleSurface(): string { return blendHex(colors.panel, colors.bg, 0.22); } function desktopHoverSurface(): string { return blendHex(colors.bg, colors.borderFocused, 0.08); } /** Pane settings dialogs: the kit DialogFrame at the settings width. */ export function DesktopDialogSurface({ title, subtitle, dismiss, children, }: { title: string; subtitle?: string; dismiss: () => void; children: ReactNode; }) { return ( {children} ); } function DesktopValuePill({ value }: { value: string }) { return ( {value} ); } function DesktopActionPill({ disabled, label }: { disabled: boolean; label: string }) { return ( {label} ); } type FocusableNode = { contains?(node: unknown): boolean; matches?(selector: string): boolean; scrollIntoView?(options: { block: "nearest" }): void; }; type FocusedNode = { blur?(): void; closest?(selector: string): unknown; matches?(selector: string): boolean }; type SettingsDocument = { activeElement: FocusedNode | null; getElementById(id: string): FocusableNode | null }; /** * The highlighted row is where the keyboard acts. A control a click or a closed * menu left focused elsewhere in the dialog would take Enter for its own row, * so it lets go when the highlight moves away from it. Returns the DOM id the * row carries: a Box ref is cell geometry, not the element. */ function useFollowHighlight(selected: boolean): string { const rowId = `pane-setting-row-${useId()}`; useEffect(() => { if (!selected) return; const doc = (globalThis as { document?: SettingsDocument }).document; const row = doc?.getElementById(rowId); const active = doc?.activeElement; // Only a control in another row; the dialog surface keeps its focus. if (active && !row?.contains?.(active) && !active.matches?.(".gloom-dialog") && active.closest?.(".gloom-dialog")) active.blur?.(); // The pointer highlighted it: scrolling now would slide another row under it. if (row?.matches?.(":hover")) return; row?.scrollIntoView?.({ block: "nearest" }); }, [rowId, selected]); return rowId; } function DesktopSettingsRow({ field, selected, currentValue, onHover, onEdit, onSelectRef, onApply, }: { field: PaneSettingField; selected: boolean; currentValue: unknown; onHover: () => void; onEdit: () => void; onSelectRef: (fieldKey: string, element: SelectFieldHandle | null) => void; onApply: (field: PaneSettingField, value: unknown) => void; }) { const isToggle = field.type === "toggle"; const disabled = isPaneSettingDisabled(field); const rowId = useFollowHighlight(selected); const summary = summarizePaneSettingValue(field, currentValue); const control = field.type === "toggle" ? ( onApply(field, checked)} /> ) : field.type === "select" ? ( onSelectRef(field.key, element)} onChange={(value) => onApply(field, value)} /> ) : field.type === "action" ? ( ) : ( Edit ); return ( { event.stopPropagation?.(); event.preventDefault?.(); if (disabled) return; if (isToggle) onApply(field, currentValue !== true); else onEdit(); }} data-gloom-interactive={field.type === "select" || disabled ? undefined : "true"} style={{ borderRadius: 6, cursor: field.type === "select" || disabled ? "default" : "pointer", opacity: disabled ? 0.6 : 1, padding: "7px 4px 8px", transition: "background-color 100ms ease", }} > {field.label} {control} {field.description && ( {field.description} )} ); } function DesktopSettingsList({ fields, selectedIndex, settings, onHover, onEdit, onSelectRef, onApply, }: { fields: PaneSettingField[]; selectedIndex: number; settings: Record; onHover: (field: PaneSettingField, index: number) => void; onEdit: (field: PaneSettingField, index: number) => void; onSelectRef: (fieldKey: string, element: SelectFieldHandle | null) => void; onApply: (field: PaneSettingField, value: unknown, index: number) => void; }) { return ( {fields.map((field, index) => ( onHover(field, index)} onEdit={() => onEdit(field, index)} onSelectRef={onSelectRef} onApply={(targetField, value) => onApply(targetField, value, index)} /> ))} ); } export function DesktopUnavailablePaneSettingsDialog({ dismiss }: { dismiss: () => void }) { return ( This pane is no longer configurable. ); } export function DesktopPaneSettingsDialogBody({ title, dismiss, fields, selectedIndex, settings, onHover, onEdit, onSelectRef, onApply, }: { title: string; dismiss: () => void; fields: PaneSettingField[]; selectedIndex: number; settings: Record; onHover: (field: PaneSettingField, index: number) => void; onEdit: (field: PaneSettingField, index: number) => void; onSelectRef: (fieldKey: string, element: SelectFieldHandle | null) => void; onApply: (field: PaneSettingField, value: unknown, index: number) => void; }) { return ( {fields.length === 0 ? ( No settings available. ) : ( )} ); }