From 0c328c4cffb1249a0d360c5a1cf23ed03c3d472f Mon Sep 17 00:00:00 2001 From: Alex Mercado Date: Fri, 15 May 2026 20:00:46 -0500 Subject: [PATCH] =?UTF-8?q?refactor:=20rename=20overlay=20CommandPalette?= =?UTF-8?q?=20=E2=86=92=20ComboBox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing "command palette" overlay is a per-WebView generic searchable picker (currently used by hello-rock3 to pick USFM markers), not a global command-launcher. The CommandPalette name was misleading from day one — the original type comment even hinted at the conflation ("marker code like 'ft' or command name") — and it conflicted with the broader Action Palette concept proposed separately (see PR #2284 and docs/plans/2026-05-15-action-palette-proposal.md). This overlay is the cross-iframe analog of the in-tree `ComboBox` component in `platform-bible-react` — both are built on shadcn's `Command` (which wraps cmdk). cmdk explicitly positions its `Command` primitive as serving both command palettes and accessible comboboxes; aligning the overlay's name with `ComboBox` puts the right breadcrumb for extension authors. Renames: - Types: CommandPaletteItem → ComboBoxItem, CommandPaletteRequest → ComboBoxRequest - Service method: showCommandPalette → showComboBox - Component files: overlay-command-palette.* → overlay-combo-box.* (matches pbr's combo-box.component.tsx kebab-case) - Internal symbols: validateCommandPaletteRequest, OverlayCommandPalette*, localizeCommandPaletteItems, etc. - Overlay discriminator: 'commandPalette' → 'comboBox' - Data attributes: data-overlay-command-palette* → data-overlay-combo-box* - LocalizeKeys: %overlay_commandPalette_*% → %overlay_comboBox_*%, %overlay_aria_commandPaletteOpened% → ...comboBoxOpened% Public PAPI surface preserves CommandPaletteItem, CommandPaletteRequest, and showCommandPalette as @deprecated aliases so out-of-repo extensions keep working. Capitalization note: cmdk's preferred spelling for non-command-palette uses of `Command` is `Combobox` (lowercase b). pbr uses `ComboBox` (capital B); this overlay matches pbr for consistency. Harmonizing toward cmdk's spelling is out of scope here — a future refactor could rename pbr's `ComboBox` → `Combobox`, pbr's `MultiSelectComboBox` → `ComboboxMulti`, and the overlay correspondingly. Flagged for a follow-up PR. Localization values updated: - en: 'Command palette opened' → 'Combo box opened' - es: 'Paleta de comandos abierta' → 'Cuadro combinado abierto' (flag for localizer review) 167 overlay tests pass; ESLint clean; papi.d.ts regenerated. --- assets/localization/en.json | 6 +- assets/localization/es.json | 6 +- .../src/web-views/hello-rock3.web-view.tsx | 14 +-- lib/papi-dts/papi.d.ts | 101 +++++++++------- .../components/overlay-host.component.tsx | 6 +- ...x => overlay-combo-box.component.test.tsx} | 42 +++---- ...nt.tsx => overlay-combo-box.component.tsx} | 109 +++++++++--------- ...ries.tsx => overlay-combo-box.stories.tsx} | 12 +- .../overlays/overlay-validation.test.ts | 43 ++++--- .../services/overlays/overlay-validation.ts | 14 +-- .../overlay.service-host.contribution.test.ts | 2 +- .../overlays/overlay.service-host.test.ts | 52 ++++----- .../services/overlays/overlay.service-host.ts | 40 +++---- .../overlays/overlay.service-model.ts | 73 +++++++----- src/shared/services/papi-core.service.ts | 4 + 15 files changed, 270 insertions(+), 254 deletions(-) rename src/renderer/components/overlays/{overlay-command-palette.component.test.tsx => overlay-combo-box.component.test.tsx} (87%) rename src/renderer/components/overlays/{overlay-command-palette.component.tsx => overlay-combo-box.component.tsx} (75%) rename src/renderer/components/overlays/{overlay-command-palette.stories.tsx => overlay-combo-box.stories.tsx} (87%) diff --git a/assets/localization/en.json b/assets/localization/en.json index 4b5db284bd9..eca49b99669 100644 --- a/assets/localization/en.json +++ b/assets/localization/en.json @@ -274,15 +274,15 @@ "%markerMenu_noResults%": "No results found.", "%markerMenu_searchPlaceholder%": "Type a style or search.", "%menuItemName_wordList%": "Word List", - "%overlay_aria_commandPaletteOpened%": "Command palette opened", + "%overlay_aria_comboBoxOpened%": "Combo box opened", "%overlay_aria_contextMenuOpened%": "Context menu opened", "%overlay_aria_dialogOpened%": "{title} dialog opened", "%overlay_aria_popoverOpened%": "Popover opened", "%overlay_dialog_title_alert%": "Alert", "%overlay_dialog_title_confirm%": "Confirm", "%overlay_dialog_title_dialog%": "Dialog", - "%overlay_commandPalette_searchPlaceholder%": "Search...", - "%overlay_commandPalette_noResults%": "No results found", + "%overlay_comboBox_searchPlaceholder%": "Search...", + "%overlay_comboBox_noResults%": "No results found", "%product_name%": "Platform.Bible", "%product_shortName%": "Platform", "%project_full_name_missing%": "*Name Missing*", diff --git a/assets/localization/es.json b/assets/localization/es.json index 60309e84971..28bd2875548 100644 --- a/assets/localization/es.json +++ b/assets/localization/es.json @@ -395,15 +395,15 @@ "%markerMenu_noResults%": "No se encontraron resultados.", "%markerMenu_searchPlaceholder%": "Escriba un estilo o busque.", "%menuItemName_wordList%": "Lista de Palabras", - "%overlay_aria_commandPaletteOpened%": "Paleta de comandos abierta", + "%overlay_aria_comboBoxOpened%": "Cuadro combinado abierto", "%overlay_aria_contextMenuOpened%": "Menú contextual abierto", "%overlay_aria_dialogOpened%": "Diálogo {title} abierto", "%overlay_aria_popoverOpened%": "Ventana emergente abierta", "%overlay_dialog_title_alert%": "Alerta", "%overlay_dialog_title_confirm%": "Confirmar", "%overlay_dialog_title_dialog%": "Diálogo", - "%overlay_commandPalette_searchPlaceholder%": "Buscar...", - "%overlay_commandPalette_noResults%": "No se encontraron resultados", + "%overlay_comboBox_searchPlaceholder%": "Buscar...", + "%overlay_comboBox_noResults%": "No se encontraron resultados", "%product_name%": "Platform.Bible", "%product_shortName%": "Platform", "%project_full_name_missing%": "*Falta el Nombre*", diff --git a/extensions/src/hello-rock3/src/web-views/hello-rock3.web-view.tsx b/extensions/src/hello-rock3/src/web-views/hello-rock3.web-view.tsx index 15d8c01ff39..fbe8e15a24b 100644 --- a/extensions/src/hello-rock3/src/web-views/hello-rock3.web-view.tsx +++ b/extensions/src/hello-rock3/src/web-views/hello-rock3.web-view.tsx @@ -536,9 +536,9 @@ globalThis.webViewComponent = function HelloRock3({ dismissPersonPopover(); }, [dismissPersonPopover]); - // Command palette demo handler - const handleCommandPalette = useCallback(async () => { - const result = await papi.overlays.showCommandPalette( + // Combo box demo handler + const handleComboBox = useCallback(async () => { + const result = await papi.overlays.showComboBox( { items: [ { id: 'p', label: 'Paragraph (p)', description: 'Normal paragraph', group: 'Paragraphs' }, @@ -573,7 +573,7 @@ globalThis.webViewComponent = function HelloRock3({ }, globalThis.webViewId, ); - logger.debug(`Command palette selected: ${result ?? 'dismissed'}`); + logger.debug(`Combo box selected: ${result ?? 'dismissed'}`); }, []); // #endregion @@ -642,10 +642,10 @@ globalThis.webViewComponent = function HelloRock3({ )} - {/* Overlay service demo: command palette */} + {/* Overlay service demo: combo box */}
-
{/* Overlay service demo: hover to show person details popover */} diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index b81b540a19f..61ec79813b4 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -7918,7 +7918,7 @@ declare module 'renderer/components/overlays/overlay-context-menu.component' { declare module 'renderer/services/overlays/overlay.service-model' { /** * Type definitions for the overlay service, a renderer-only service that manages overlays (context - * menus, popovers, command palettes) rendered in the renderer's top-level document outside iframe + * menus, popovers, combo boxes) rendered in the renderer's top-level document outside iframe * boundaries. Extensions running in sandboxed WebView iframes cannot render UI above other content, * so this service provides a way for them to request overlays that the renderer hosts on their * behalf. @@ -8003,13 +8003,13 @@ declare module 'renderer/services/overlays/overlay.service-model' { showArrow?: boolean; } /** - * A single item in a command palette. Items are displayed in a searchable, filterable list. The - * user types to filter and selects one item. + * A single item in a combo box. Items are displayed in a searchable, filterable list. The user + * types to filter and selects one item. */ - export type CommandPaletteItem = { + export type ComboBoxItem = { /** Unique identifier returned when this item is selected */ id: string; - /** Primary display text (e.g., marker code like "ft" or command name) */ + /** Primary display text (e.g., marker code like "ft") */ label: string | LocalizeKey; /** Secondary description text displayed below the label */ description?: string | LocalizeKey; @@ -8022,12 +8022,12 @@ declare module 'renderer/services/overlays/overlay.service-model' { /** Whether the item is grayed out and non-selectable. Defaults to false. */ disabled?: boolean; }; - /** Request payload for {@link IOverlayService.showCommandPalette}. */ - export interface CommandPaletteRequest { + /** Request payload for {@link IOverlayService.showComboBox}. */ + export interface ComboBoxRequest { /** The selectable items to display */ - items: CommandPaletteItem[]; + items: ComboBoxItem[]; /** - * Anchor position in pixels relative to the requesting WebView's iframe origin. The palette is + * Anchor position in pixels relative to the requesting WebView's iframe origin. The combo box is * positioned adjacent to this point. If omitted, centers in the viewport. */ anchor?: { @@ -8036,7 +8036,7 @@ declare module 'renderer/services/overlays/overlay.service-model' { width?: number; height?: number; }; - /** Preferred side of the anchor to place the palette. Defaults to 'bottom'. */ + /** Preferred side of the anchor to place the combo box. Defaults to 'bottom'. */ side?: 'top' | 'bottom' | 'left' | 'right'; /** Placeholder text for the search input */ placeholder?: string | LocalizeKey; @@ -8044,13 +8044,21 @@ declare module 'renderer/services/overlays/overlay.service-model' { maxWidth?: number; /** Maximum height in pixels. Defaults to 400. */ maxHeight?: number; - /** Whether clicking outside dismisses the palette. Defaults to true. */ + /** Whether clicking outside dismisses the combo box. Defaults to true. */ dismissOnClickOutside?: boolean; } + /** + * @deprecated Use {@link ComboBoxItem}. The "command palette" terminology was misleading — this + * overlay is a generic per-WebView searchable picker (used today for USFM marker selection), not + * the global Action Palette concept. See `docs/plans/2026-05-15-action-palette-proposal.md`. + */ + export type CommandPaletteItem = ComboBoxItem; + /** @deprecated Use {@link ComboBoxRequest}. See {@link CommandPaletteItem} for context. */ + export type CommandPaletteRequest = ComboBoxRequest; /** * - * Service for showing overlays (context menus, popovers, command palettes) that render outside - * iframe boundaries in the renderer's top-level document. Renderer-only service. + * Service for showing overlays (context menus, popovers, combo boxes) that render outside iframe + * boundaries in the renderer's top-level document. Renderer-only service. * * Extensions in sandboxed WebView iframes cannot render UI above other content or outside their * iframe bounds. This service accepts overlay requests from WebViews, translates their @@ -8058,9 +8066,9 @@ declare module 'renderer/services/overlays/overlay.service-model' { * renderer's React tree. Each method returns a promise that resolves when the user interacts with * the overlay or it is dismissed. * - * Only one overlay of each type (context menu, popover, command palette) can be active per WebView - * at a time. Requesting a new overlay of the same type from the same WebView replaces the previous - * one and rejects its promise with a PlatformError with code ABORTED. + * Only one overlay of each type (context menu, popover, combo box) can be active per WebView at a + * time. Requesting a new overlay of the same type from the same WebView replaces the previous one + * and rejects its promise with a PlatformError with code ABORTED. */ export interface IOverlayService { /** @@ -8131,20 +8139,21 @@ declare module 'renderer/services/overlays/overlay.service-model' { */ onPopoverDismissed(overlayId: string): Promise; /** - * Shows a command palette with searchable/filterable items. Returns a promise that resolves with - * the selected item's `id`, or `undefined` if dismissed. + * Shows a combo box with searchable/filterable items. Returns a promise that resolves with the + * selected item's `id`, or `undefined` if dismissed. * * @param request The items, optional anchor position, and display options - * @param webViewId The ID of the WebView requesting the command palette + * @param webViewId The ID of the WebView requesting the combo box * @returns The selected item's ID, or `undefined` if dismissed * @throws PlatformError with code INVALID_ARGUMENT if the request is invalid - * @throws PlatformError with code ABORTED if replaced by another command palette from the same - * WebView + * @throws PlatformError with code ABORTED if replaced by another combo box from the same WebView */ - showCommandPalette( - request: CommandPaletteRequest, - webViewId: string, - ): Promise; + showComboBox(request: ComboBoxRequest, webViewId: string): Promise; + /** + * @deprecated Use {@link showComboBox}. The "command palette" terminology was misleading; see + * `docs/plans/2026-05-15-action-palette-proposal.md`. + */ + showCommandPalette(request: ComboBoxRequest, webViewId: string): Promise; } /** * Internal representation of an active overlay stored in the overlay store. Each entry holds the @@ -8156,7 +8165,7 @@ declare module 'renderer/services/overlays/overlay.service-model' { * - `'contextMenu'` — An active context menu with translated position and menu items. * - `'modalDialog'` — An active modal dialog with its type-specific options. * - `'popover'` — An active popover with mutable `content` (updatable via `updatePopover`). - * - `'commandPalette'` — An active command palette with searchable/filterable items. + * - `'comboBox'` — An active combo box with searchable/filterable items. * * UI components read entries from the overlay store to render overlays, then call `resolve` or * `reject` when the user interacts with or dismisses them. @@ -8222,15 +8231,15 @@ declare module 'renderer/services/overlays/overlay.service-model' { reject: (error: PlatformError) => void; } | { - type: 'commandPalette'; + type: 'comboBox'; /** Unique overlay identifier generated by the service */ id: string; /** The WebView that requested this overlay */ webViewId: string; /** The original request */ - request: CommandPaletteRequest; + request: ComboBoxRequest; /** Items to render */ - items: CommandPaletteItem[]; + items: ComboBoxItem[]; /** Document-relative position (translated + clamped), or undefined for centered */ position?: { x: number; @@ -8249,7 +8258,7 @@ declare module 'renderer/services/overlays/overlay.service-model' { contextMenu: string | undefined; modalDialog: unknown; popover: string | undefined; - commandPalette: string | undefined; + comboBox: string | undefined; }; } declare module 'shared/services/app.service-model' { @@ -8948,7 +8957,11 @@ declare module '@papi/core' { export type { DialogTypes } from 'renderer/components/dialogs/dialog-definition.model'; export type { UseDialogCallbackOptions } from 'renderer/hooks/papi-hooks/use-dialog-callback.hook'; export type { + ComboBoxItem, + ComboBoxRequest, + /** @deprecated Use {@link ComboBoxItem}. */ CommandPaletteItem, + /** @deprecated Use {@link ComboBoxRequest}. */ CommandPaletteRequest, IOverlayService, PopoverAction, @@ -10249,7 +10262,7 @@ declare module 'renderer/services/overlays/overlay-menu-converter' { declare module 'renderer/services/overlays/overlay-validation' { import type { OverlayContextMenuItem } from 'renderer/components/overlays/overlay-context-menu.component'; import { - CommandPaletteRequest, + ComboBoxRequest, PopoverRequest, } from 'renderer/services/overlays/overlay.service-model'; /** @@ -10275,12 +10288,12 @@ declare module 'renderer/services/overlays/overlay-validation' { */ export function validatePopoverRequest(request: PopoverRequest): void; /** - * Validates a command palette request's items, anchor, and options. + * Validates a combo box request's items, anchor, and options. * - * @param request The command palette request to validate + * @param request The combo box request to validate * @throws PlatformError with code INVALID_ARGUMENT if validation fails */ - export function validateCommandPaletteRequest(request: CommandPaletteRequest): void; + export function validateComboBoxRequest(request: ComboBoxRequest): void; } declare module 'renderer/services/overlays/overlay-coordinates' { /** @@ -10793,8 +10806,8 @@ declare module '@papi/frontend' { window: IWindowService; /** * - * Service for showing overlays (context menus, popovers, command palettes) that render outside - * iframe boundaries in the renderer's top-level document. Renderer-only service. + * Service for showing overlays (context menus, popovers, combo boxes) that render outside iframe + * boundaries in the renderer's top-level document. Renderer-only service. * * Extensions in sandboxed WebView iframes cannot render UI above other content or outside their * iframe bounds. This service accepts overlay requests from WebViews, translates their @@ -10802,9 +10815,9 @@ declare module '@papi/frontend' { * renderer's React tree. Each method returns a promise that resolves when the user interacts with * the overlay or it is dismissed. * - * Only one overlay of each type (context menu, popover, command palette) can be active per WebView - * at a time. Requesting a new overlay of the same type from the same WebView replaces the previous - * one and rejects its promise with a PlatformError with code ABORTED. + * Only one overlay of each type (context menu, popover, combo box) can be active per WebView at a + * time. Requesting a new overlay of the same type from the same WebView replaces the previous one + * and rejects its promise with a PlatformError with code ABORTED. */ overlays: IOverlayService; }; @@ -10962,8 +10975,8 @@ declare module '@papi/frontend' { export const window: IWindowService; /** * - * Service for showing overlays (context menus, popovers, command palettes) that render outside - * iframe boundaries in the renderer's top-level document. Renderer-only service. + * Service for showing overlays (context menus, popovers, combo boxes) that render outside iframe + * boundaries in the renderer's top-level document. Renderer-only service. * * Extensions in sandboxed WebView iframes cannot render UI above other content or outside their * iframe bounds. This service accepts overlay requests from WebViews, translates their @@ -10971,9 +10984,9 @@ declare module '@papi/frontend' { * renderer's React tree. Each method returns a promise that resolves when the user interacts with * the overlay or it is dismissed. * - * Only one overlay of each type (context menu, popover, command palette) can be active per WebView - * at a time. Requesting a new overlay of the same type from the same WebView replaces the previous - * one and rejects its promise with a PlatformError with code ABORTED. + * Only one overlay of each type (context menu, popover, combo box) can be active per WebView at a + * time. Requesting a new overlay of the same type from the same WebView replaces the previous one + * and rejects its promise with a PlatformError with code ABORTED. */ export const overlays: IOverlayService; export type Papi = typeof papi; diff --git a/src/renderer/components/overlay-host.component.tsx b/src/renderer/components/overlay-host.component.tsx index 603e489543d..1cf9d80eed1 100644 --- a/src/renderer/components/overlay-host.component.tsx +++ b/src/renderer/components/overlay-host.component.tsx @@ -3,7 +3,7 @@ * overlay store and renders the appropriate overlay components. */ -import { OverlayCommandPalette } from '@renderer/components/overlays/overlay-command-palette.component'; +import { OverlayComboBox } from '@renderer/components/overlays/overlay-combo-box.component'; import { OverlayContextMenu } from '@renderer/components/overlays/overlay-context-menu.component'; import { OverlayModalDialog } from '@renderer/components/overlays/overlay-modal-dialog.component'; import { OverlayPopover } from '@renderer/components/overlays/overlay-popover.component'; @@ -39,8 +39,8 @@ export function OverlayHost() { if (overlay.type === 'popover') { return ; } - if (overlay.type === 'commandPalette') { - return ; + if (overlay.type === 'comboBox') { + return ; } return undefined; })} diff --git a/src/renderer/components/overlays/overlay-command-palette.component.test.tsx b/src/renderer/components/overlays/overlay-combo-box.component.test.tsx similarity index 87% rename from src/renderer/components/overlays/overlay-command-palette.component.test.tsx rename to src/renderer/components/overlays/overlay-combo-box.component.test.tsx index d69a9b819d9..2f105888992 100644 --- a/src/renderer/components/overlays/overlay-command-palette.component.test.tsx +++ b/src/renderer/components/overlays/overlay-combo-box.component.test.tsx @@ -1,8 +1,8 @@ import { vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { OverlayCommandPalettePresentational } from './overlay-command-palette.component'; -import { CommandPaletteItem } from '../../services/overlays/overlay.service-model'; +import { OverlayComboBoxPresentational } from './overlay-combo-box.component'; +import { ComboBoxItem } from '../../services/overlays/overlay.service-model'; beforeAll(() => { // Radix Popover uses ResizeObserver internally; jsdom doesn't provide it, so we stub a no-op @@ -22,8 +22,8 @@ beforeAll(() => { Element.prototype.scrollIntoView = vi.fn(); }); -describe('OverlayCommandPalettePresentational', () => { - const sampleItems: CommandPaletteItem[] = [ +describe('OverlayComboBoxPresentational', () => { + const sampleItems: ComboBoxItem[] = [ { id: 'open', label: 'Open File' }, { id: 'save', label: 'Save File' }, { id: 'close', label: 'Close Tab' }, @@ -35,7 +35,7 @@ describe('OverlayCommandPalettePresentational', () => { const onDismiss = vi.fn(); render( - { const onDismiss = vi.fn(); render( - { const onDismiss = vi.fn(); render( - , ); - const backdrop = document.querySelector('[data-overlay-command-palette-backdrop]'); + const backdrop = document.querySelector('[data-overlay-combo-box-backdrop]'); expect(backdrop).toBeInTheDocument(); // querySelector returns Element | null; the expect above guards null, but TS can't narrow it // eslint-disable-next-line no-type-assertion/no-type-assertion @@ -90,7 +90,7 @@ describe('OverlayCommandPalettePresentational', () => { it('should display custom noResultsText', () => { render( - { it('should display custom placeholder text', () => { render( - { it('should not call onSelect when a disabled item is clicked', () => { const onSelect = vi.fn(); - const items: CommandPaletteItem[] = [ + const items: ComboBoxItem[] = [ { id: 'disabled-item', label: 'Cannot Click', disabled: true }, ]; render( - , + , ); fireEvent.click(screen.getByText('Cannot Click')); @@ -140,7 +136,7 @@ describe('OverlayCommandPalettePresentational', () => { const onDismiss = vi.fn(); render( - { describe('search filtering', () => { it('should filter visible items when typing in the search input', () => { render( - { it('should show noResultsText when search matches nothing', () => { render( - { const onSelect = vi.fn(); render( - { const onSelect = vi.fn(); render( - { describe('grouped items', () => { it('should render group headings when items have group keys', () => { - const groupedItems: CommandPaletteItem[] = [ + const groupedItems: ComboBoxItem[] = [ { id: 'open', label: 'Open File', group: 'File' }, { id: 'save', label: 'Save File', group: 'File' }, { id: 'find', label: 'Find', group: 'Edit' }, ]; render( - void; -}) { +/** Renders a single combo box item with label, description, icon, and badge */ +function PaletteItem({ item, onSelect }: { item: ComboBoxItem; onSelect: (id: string) => void }) { // Build a searchable value from label + description + badge for cmdk filtering const searchValue = [item.label, item.description, item.badge].filter(Boolean).join(' '); @@ -111,11 +112,11 @@ function GroupedItems({ items, onSelect, }: { - items: CommandPaletteItem[]; + items: ComboBoxItem[]; onSelect: (id: string) => void; }) { const grouped = useMemo(() => { - const groups = new Map(); + const groups = new Map(); items.forEach((item) => { const key = item.group ?? ''; const arr = groups.get(key); @@ -153,15 +154,15 @@ function GroupedItems({ // ── Presentational Component ── /** - * Pure presentational command palette component. Renders a searchable list of items using cmdk. + * Pure presentational combo box component. Renders a searchable list of items using cmdk. * Positioned via a Radix Popover virtual anchor when `position` is provided, or centered in the * viewport when omitted. * * This component has no dependency on the overlay store or localization hooks. Use it directly in * tests and Storybook stories. For production rendering via the overlay service, use - * {@link OverlayCommandPalette} instead — it handles LocalizeKey resolution and store lifecycle. + * {@link OverlayComboBox} instead — it handles LocalizeKey resolution and store lifecycle. */ -export function OverlayCommandPalettePresentational({ +export function OverlayComboBoxPresentational({ items, position, anchor, @@ -172,7 +173,7 @@ export function OverlayCommandPalettePresentational({ maxHeight = DEFAULT_MAX_HEIGHT, onSelect, onDismiss, -}: OverlayCommandPalettePresentationalProps) { +}: OverlayComboBoxPresentationalProps) { // React's useRef requires null as the initial value for DOM refs // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); @@ -194,11 +195,7 @@ export function OverlayCommandPalettePresentational({ ); const paletteContent = ( - + {noResultsText} @@ -213,7 +210,7 @@ export function OverlayCommandPalettePresentational({ // Backdrop handles click-to-dismiss; keyboard events (Escape) are handled by the child Command component // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
{ @@ -235,7 +232,7 @@ export function OverlayCommandPalettePresentational({
({ ...item, label: resolveValue(item.label, localizedStrings), @@ -333,30 +330,30 @@ function localizeCommandPaletteItems( // ── Store-Connected Component ── -type OverlayCommandPaletteProps = { - overlay: Extract; +type OverlayComboBoxProps = { + overlay: Extract; }; /** - * Production command palette component. Resolves LocalizeKey values in items (labels, descriptions, + * Production combo box component. Resolves LocalizeKey values in items (labels, descriptions, * badges) and placeholder/no-results text via `useLocalizedStrings`, manages overlay lifecycle, and - * delegates rendering to {@link OverlayCommandPalettePresentational}. + * delegates rendering to {@link OverlayComboBoxPresentational}. * * This is the component rendered by `OverlayHost`. Do not use it directly in tests or Storybook — - * use {@link OverlayCommandPalettePresentational} instead, which accepts plain props without - * requiring an `OverlayEntry`. + * use {@link OverlayComboBoxPresentational} instead, which accepts plain props without requiring an + * `OverlayEntry`. */ -export function OverlayCommandPalette({ overlay }: OverlayCommandPaletteProps) { +export function OverlayComboBox({ overlay }: OverlayComboBoxProps) { const hasResolved = useRef(false); const localizeKeys = useMemo( - () => collectCommandPaletteKeys(overlay.items, overlay.request.placeholder), + () => collectComboBoxKeys(overlay.items, overlay.request.placeholder), [overlay.items, overlay.request.placeholder], ); const [localizedStrings] = useLocalizedStrings(localizeKeys); const localizedItems = useMemo( - () => localizeCommandPaletteItems(overlay.items, localizedStrings), + () => localizeComboBoxItems(overlay.items, localizedStrings), [overlay.items, localizedStrings], ); @@ -388,7 +385,7 @@ export function OverlayCommandPalette({ overlay }: OverlayCommandPaletteProps) { }, [overlay]); return ( - = { - title: 'Advanced/OverlayCommandPalette', - component: OverlayCommandPalettePresentational, +const meta: Meta = { + title: 'Advanced/OverlayComboBox', + component: OverlayComboBoxPresentational, tags: ['autodocs'], parameters: { docs: { description: { component: - 'A searchable command palette overlay. Displays a filterable list of items with optional descriptions, badges, icons, and grouping.', + 'A searchable combo box overlay. Displays a filterable list of items with optional descriptions, badges, icons, and grouping.', }, }, }, @@ -21,7 +21,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const BasicItems: Story = { args: { diff --git a/src/renderer/services/overlays/overlay-validation.test.ts b/src/renderer/services/overlays/overlay-validation.test.ts index 72e957d86c0..dea23cbf311 100644 --- a/src/renderer/services/overlays/overlay-validation.test.ts +++ b/src/renderer/services/overlays/overlay-validation.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect } from 'vitest'; import { isPlatformError, INVALID_ARGUMENT } from 'platform-bible-utils'; import type { OverlayContextMenuItem } from '@renderer/components/overlays/overlay-context-menu.component'; -import { CommandPaletteRequest, PopoverRequest } from './overlay.service-model'; +import { ComboBoxRequest, PopoverRequest } from './overlay.service-model'; import { - validateCommandPaletteRequest, + validateComboBoxRequest, validateContextMenuItems, validateMenuItems, validatePopoverRequest, @@ -331,21 +331,21 @@ describe('overlay-validation', () => { }); }); - describe('validateCommandPaletteRequest', () => { - const validRequest: CommandPaletteRequest = { + describe('validateComboBoxRequest', () => { + const validRequest: ComboBoxRequest = { items: [ { id: 'ft', label: 'Footnote' }, { id: 'xt', label: 'Cross Reference' }, ], }; - it('should pass for a valid command palette request', () => { - expect(() => validateCommandPaletteRequest(validRequest)).not.toThrow(); + it('should pass for a valid combo box request', () => { + expect(() => validateComboBoxRequest(validRequest)).not.toThrow(); }); it('should throw for empty items array', () => { expectValidationError( - () => validateCommandPaletteRequest({ ...validRequest, items: [] }), + () => validateComboBoxRequest({ ...validRequest, items: [] }), 'Items array must not be empty', ); }); @@ -353,61 +353,58 @@ describe('overlay-validation', () => { it('should throw for too many items (>200)', () => { const items = Array.from({ length: 201 }, (_, i) => ({ id: `id-${i}`, label: `Item ${i}` })); expectValidationError( - () => validateCommandPaletteRequest({ ...validRequest, items }), + () => validateComboBoxRequest({ ...validRequest, items }), 'Too many items (max 200)', ); }); it('should pass for exactly 200 items', () => { const items = Array.from({ length: 200 }, (_, i) => ({ id: `id-${i}`, label: `Item ${i}` })); - expect(() => validateCommandPaletteRequest({ ...validRequest, items })).not.toThrow(); + expect(() => validateComboBoxRequest({ ...validRequest, items })).not.toThrow(); }); it('should throw for item missing id', () => { // Intentionally malformed input to test validation; must bypass TS to simulate bad runtime data // eslint-disable-next-line no-type-assertion/no-type-assertion - const request = { items: [{ label: 'No ID' }] } as unknown as CommandPaletteRequest; - expectValidationError( - () => validateCommandPaletteRequest(request), - 'Each item must have an id', - ); + const request = { items: [{ label: 'No ID' }] } as unknown as ComboBoxRequest; + expectValidationError(() => validateComboBoxRequest(request), 'Each item must have an id'); }); it('should throw for label too long', () => { const items = [{ id: 'long', label: 'a'.repeat(501) }]; expectValidationError( - () => validateCommandPaletteRequest({ ...validRequest, items }), + () => validateComboBoxRequest({ ...validRequest, items }), 'Label exceeds maximum length of 500 characters', ); }); it('should throw for invalid anchor coordinates (NaN)', () => { expectValidationError( - () => validateCommandPaletteRequest({ ...validRequest, anchor: { x: NaN, y: 100 } }), + () => validateComboBoxRequest({ ...validRequest, anchor: { x: NaN, y: 100 } }), 'Anchor must have valid x and y coordinates', ); }); it('should throw for negative maxWidth', () => { expectValidationError( - () => validateCommandPaletteRequest({ ...validRequest, maxWidth: -10 }), + () => validateComboBoxRequest({ ...validRequest, maxWidth: -10 }), 'maxWidth must be greater than 0', ); }); it('should throw for zero maxWidth', () => { - expectValidationError(() => validateCommandPaletteRequest({ ...validRequest, maxWidth: 0 })); + expectValidationError(() => validateComboBoxRequest({ ...validRequest, maxWidth: 0 })); }); it('should throw for negative maxHeight', () => { expectValidationError( - () => validateCommandPaletteRequest({ ...validRequest, maxHeight: -5 }), + () => validateComboBoxRequest({ ...validRequest, maxHeight: -5 }), 'maxHeight must be greater than 0', ); }); it('should throw for zero maxHeight', () => { - expectValidationError(() => validateCommandPaletteRequest({ ...validRequest, maxHeight: 0 })); + expectValidationError(() => validateComboBoxRequest({ ...validRequest, maxHeight: 0 })); }); it('should pass with all optional fields on items', () => { @@ -422,12 +419,12 @@ describe('overlay-validation', () => { disabled: false, }, ]; - expect(() => validateCommandPaletteRequest({ ...validRequest, items })).not.toThrow(); + expect(() => validateComboBoxRequest({ ...validRequest, items })).not.toThrow(); }); it('should pass with valid anchor coordinates', () => { expect(() => - validateCommandPaletteRequest({ + validateComboBoxRequest({ ...validRequest, anchor: { x: 100, y: 200 }, }), @@ -435,7 +432,7 @@ describe('overlay-validation', () => { }); it('should pass with no anchor (centered mode)', () => { - expect(() => validateCommandPaletteRequest(validRequest)).not.toThrow(); + expect(() => validateComboBoxRequest(validRequest)).not.toThrow(); }); }); }); diff --git a/src/renderer/services/overlays/overlay-validation.ts b/src/renderer/services/overlays/overlay-validation.ts index 8f2bd6a2aae..7490b4bcc7f 100644 --- a/src/renderer/services/overlays/overlay-validation.ts +++ b/src/renderer/services/overlays/overlay-validation.ts @@ -5,7 +5,7 @@ import { newPlatformError, INVALID_ARGUMENT } from 'platform-bible-utils'; import type { OverlayContextMenuItem } from '@renderer/components/overlays/overlay-context-menu.component'; -import { CommandPaletteRequest, PopoverRequest } from './overlay.service-model'; +import { ComboBoxRequest, PopoverRequest } from './overlay.service-model'; /** Throws a PlatformError with INVALID_ARGUMENT code */ function throwValidationError(message: string): never { @@ -112,20 +112,20 @@ export function validatePopoverRequest(request: PopoverRequest): void { } } -const MAX_COMMAND_PALETTE_ITEMS = 200; +const MAX_COMBO_BOX_ITEMS = 200; /** - * Validates a command palette request's items, anchor, and options. + * Validates a combo box request's items, anchor, and options. * - * @param request The command palette request to validate + * @param request The combo box request to validate * @throws PlatformError with code INVALID_ARGUMENT if validation fails */ -export function validateCommandPaletteRequest(request: CommandPaletteRequest): void { +export function validateComboBoxRequest(request: ComboBoxRequest): void { if (!request.items || request.items.length === 0) { throwValidationError('Items array must not be empty'); } - if (request.items.length > MAX_COMMAND_PALETTE_ITEMS) { - throwValidationError(`Too many items (max ${MAX_COMMAND_PALETTE_ITEMS})`); + if (request.items.length > MAX_COMBO_BOX_ITEMS) { + throwValidationError(`Too many items (max ${MAX_COMBO_BOX_ITEMS})`); } request.items.forEach((item) => { diff --git a/src/renderer/services/overlays/overlay.service-host.contribution.test.ts b/src/renderer/services/overlays/overlay.service-host.contribution.test.ts index 55a770dddc2..50e09a8f1fe 100644 --- a/src/renderer/services/overlays/overlay.service-host.contribution.test.ts +++ b/src/renderer/services/overlays/overlay.service-host.contribution.test.ts @@ -36,7 +36,7 @@ vi.mock('@shared/services/logger.service', () => ({ // Mock validation (no-op) vi.mock('./overlay-validation', () => ({ - validateCommandPaletteRequest: vi.fn(), + validateComboBoxRequest: vi.fn(), validateContextMenuItems: vi.fn(), validateModalDialogOptions: vi.fn(), validatePopoverRequest: vi.fn(), diff --git a/src/renderer/services/overlays/overlay.service-host.test.ts b/src/renderer/services/overlays/overlay.service-host.test.ts index 39e19161ad3..b950ef44201 100644 --- a/src/renderer/services/overlays/overlay.service-host.test.ts +++ b/src/renderer/services/overlays/overlay.service-host.test.ts @@ -7,7 +7,7 @@ import { } from 'platform-bible-utils'; import { sendCommand } from '@shared/services/command.service'; import { menuDataService } from '@shared/services/menu-data.service'; -import { CommandPaletteRequest, PopoverContent, PopoverRequest } from './overlay.service-model'; +import { ComboBoxRequest, PopoverContent, PopoverRequest } from './overlay.service-model'; import { getOverlays, getOverlayById, clearAllOverlays } from './overlay-store'; import { isWebViewVisible } from './overlay-coordinates'; @@ -16,7 +16,7 @@ const DEBOUNCE_COOLDOWN_MS = 50; // Mock dependencies vi.mock('./overlay-validation', () => ({ - validateCommandPaletteRequest: vi.fn(), + validateComboBoxRequest: vi.fn(), validateContextMenuItems: vi.fn(), validatePopoverRequest: vi.fn(), })); @@ -507,8 +507,8 @@ describe('overlay.service-host', () => { }); }); - describe('command palettes', () => { - const validRequest: CommandPaletteRequest = { + describe('combo boxes', () => { + const validRequest: ComboBoxRequest = { items: [ { id: 'ft', label: 'Footnote' }, { id: 'xt', label: 'Cross Reference' }, @@ -516,12 +516,12 @@ describe('overlay.service-host', () => { anchor: { x: 100, y: 200 }, }; - it('should create an overlay entry of type commandPalette', () => { - const promise = overlayService.showCommandPalette(validRequest, 'test-webview'); + it('should create an overlay entry of type comboBox', () => { + const promise = overlayService.showComboBox(validRequest, 'test-webview'); const overlays = getOverlays(); expect(overlays).toHaveLength(1); - expect(overlays[0].type).toBe('commandPalette'); + expect(overlays[0].type).toBe('comboBox'); // Clean up overlays[0].resolve(undefined); @@ -529,12 +529,12 @@ describe('overlay.service-host', () => { }); it('should resolve with selected item ID', async () => { - const promise = overlayService.showCommandPalette(validRequest, 'test-webview'); + const promise = overlayService.showComboBox(validRequest, 'test-webview'); const overlays = getOverlays(); - // Only commandPalette overlays exist in this test + // Only comboBox overlays exist in this test // eslint-disable-next-line no-type-assertion/no-type-assertion - const overlay = overlays[0] as Extract<(typeof overlays)[0], { type: 'commandPalette' }>; + const overlay = overlays[0] as Extract<(typeof overlays)[0], { type: 'comboBox' }>; overlay.resolve('ft'); const result = await promise; @@ -542,37 +542,37 @@ describe('overlay.service-host', () => { }); it('should resolve with undefined when dismissed', async () => { - const promise = overlayService.showCommandPalette(validRequest, 'test-webview'); + const promise = overlayService.showComboBox(validRequest, 'test-webview'); const overlays = getOverlays(); // TypeScript cannot narrow a discriminated union after getOverlays(); cast needed to access typed fields // eslint-disable-next-line no-type-assertion/no-type-assertion - const overlay = overlays[0] as Extract<(typeof overlays)[0], { type: 'commandPalette' }>; + const overlay = overlays[0] as Extract<(typeof overlays)[0], { type: 'comboBox' }>; overlay.resolve(undefined); const result = await promise; expect(result).toBeUndefined(); }); - it('should replace existing command palette from same webView', async () => { + it('should replace existing combo box from same webView', async () => { vi.useFakeTimers(); - const promise1 = overlayService.showCommandPalette(validRequest, 'test-webview'); + const promise1 = overlayService.showComboBox(validRequest, 'test-webview'); vi.advanceTimersByTime(DEBOUNCE_COOLDOWN_MS); - const request2: CommandPaletteRequest = { + const request2: ComboBoxRequest = { items: [{ id: 'p', label: 'Paragraph' }], anchor: { x: 60, y: 110 }, }; - const promise2 = overlayService.showCommandPalette(request2, 'test-webview'); + const promise2 = overlayService.showComboBox(request2, 'test-webview'); await expect(promise1).rejects.toSatisfy( (error: unknown) => isPlatformError(error) && error.code === ABORTED, ); const overlays = getOverlays(); - const palettes = overlays.filter((o) => o.type === 'commandPalette'); + const palettes = overlays.filter((o) => o.type === 'comboBox'); expect(palettes).toHaveLength(1); palettes[0].resolve(undefined); @@ -581,11 +581,9 @@ describe('overlay.service-host', () => { }); it('should reject with RESOURCE_EXHAUSTED within debounce cooldown', async () => { - const promise1 = overlayService.showCommandPalette(validRequest, 'test-webview'); + const promise1 = overlayService.showComboBox(validRequest, 'test-webview'); // Second call within 50ms should throw - await expect( - overlayService.showCommandPalette(validRequest, 'test-webview'), - ).rejects.toSatisfy( + await expect(overlayService.showComboBox(validRequest, 'test-webview')).rejects.toSatisfy( (error: unknown) => isPlatformError(error) && error.code === RESOURCE_EXHAUSTED, ); expect(getOverlays()).toHaveLength(1); @@ -595,17 +593,17 @@ describe('overlay.service-host', () => { }); it('should handle centered mode (no anchor)', () => { - const request: CommandPaletteRequest = { + const request: ComboBoxRequest = { items: [{ id: 'ft', label: 'Footnote' }], // no anchor — centered mode }; - const promise = overlayService.showCommandPalette(request, 'test-webview'); + const promise = overlayService.showComboBox(request, 'test-webview'); const overlays = getOverlays(); expect(overlays).toHaveLength(1); // TypeScript cannot narrow a discriminated union after getOverlays(); cast needed to access typed fields // eslint-disable-next-line no-type-assertion/no-type-assertion - const overlay = overlays[0] as Extract<(typeof overlays)[0], { type: 'commandPalette' }>; + const overlay = overlays[0] as Extract<(typeof overlays)[0], { type: 'comboBox' }>; expect(overlay.position).toBeUndefined(); overlay.resolve(undefined); @@ -642,14 +640,14 @@ describe('overlay.service-host', () => { vi.mocked(isWebViewVisible).mockReturnValue(true); }); - it('should reject command palette with FAILED_PRECONDITION when webView is not visible', async () => { + it('should reject combo box with FAILED_PRECONDITION when webView is not visible', async () => { vi.mocked(isWebViewVisible).mockReturnValue(false); - const request: CommandPaletteRequest = { + const request: ComboBoxRequest = { items: [{ id: 'ft', label: 'Footnote' }], }; - await expect(overlayService.showCommandPalette(request, 'hidden-webview')).rejects.toSatisfy( + await expect(overlayService.showComboBox(request, 'hidden-webview')).rejects.toSatisfy( (error: unknown) => isPlatformError(error) && error.code === FAILED_PRECONDITION, ); diff --git a/src/renderer/services/overlays/overlay.service-host.ts b/src/renderer/services/overlays/overlay.service-host.ts index d37a6ad3f7d..9adfbc4d86b 100644 --- a/src/renderer/services/overlays/overlay.service-host.ts +++ b/src/renderer/services/overlays/overlay.service-host.ts @@ -31,7 +31,7 @@ import { import type { PlatformError } from 'platform-bible-utils'; import type { ReactElement } from 'react'; import { - CommandPaletteRequest, + ComboBoxRequest, IOverlayService, OverlayEntry, PopoverContent, @@ -39,7 +39,7 @@ import { } from './overlay.service-model'; import { convertContributionToContextMenuItems } from './overlay-menu-converter'; import { - validateCommandPaletteRequest, + validateComboBoxRequest, validateContextMenuItems, validatePopoverRequest, } from './overlay-validation'; @@ -523,35 +523,33 @@ async function onPopoverDismissed(overlayId: string): Promise { - validateCommandPaletteRequest(request); + validateComboBoxRequest(request); - // Visibility check (command palettes require visible WebView) + // Visibility check (combo boxes require visible WebView) if (!isWebViewVisible(webViewId)) { throw newPlatformError('Requesting WebView is not visible', FAILED_PRECONDITION); } // Leading-edge debounce: drop rapid re-triggers within 50ms - if (!debounceCheck('commandPalette', webViewId)) { + if (!debounceCheck('comboBox', webViewId)) { throw newPlatformError('Overlay request dropped by debounce cooldown', RESOURCE_EXHAUSTED); } - // Replace any existing command palette from this webView - const existingOverlays = getOverlaysByWebView(webViewId).filter( - (o) => o.type === 'commandPalette', - ); + // Replace any existing combo box from this webView + const existingOverlays = getOverlaysByWebView(webViewId).filter((o) => o.type === 'comboBox'); existingOverlays.forEach((existing) => { rejectAndRemoveOverlay( existing.id, @@ -572,13 +570,13 @@ async function showCommandPalette( position = clampToViewport(translatedPosition, 4); } - announceLocalizedToScreenReader('%overlay_aria_commandPaletteOpened%'); + announceLocalizedToScreenReader('%overlay_aria_comboBoxOpened%'); lastOverlayCreatedAt = Date.now(); return new Promise((resolve, reject) => { addOverlay({ - type: 'commandPalette', + type: 'comboBox', id: overlayId, webViewId, request, @@ -603,7 +601,9 @@ export const overlayService: IOverlayService = { updatePopover, dismissPopover, onPopoverDismissed, - showCommandPalette, + showComboBox, + /** @deprecated Use {@link showComboBox}. */ + showCommandPalette: showComboBox, }; // ── Event Listeners for Auto-Dismiss ── @@ -622,7 +622,7 @@ function registerAutoDismissListeners(): void { // Don't dismiss overlays when scrolling inside overlay content (e.g., popover with overflow) if ( e.target instanceof Element && - e.target.closest('[data-overlay-popover], [data-overlay-command-palette]') + e.target.closest('[data-overlay-popover], [data-overlay-combo-box]') ) return; @@ -631,12 +631,12 @@ function registerAutoDismissListeners(): void { { capture: true }, ); - // Dismiss context menus, command palettes, and popovers on window blur + // Dismiss context menus, combo boxes, and popovers on window blur window.addEventListener('blur', () => { // Skip if an overlay was just created — focus shifts from panel activation can trigger blur if (Date.now() - lastOverlayCreatedAt < OVERLAY_CREATION_GRACE_MS) return; - dismissAll('contextMenu', 'commandPalette'); + dismissAll('contextMenu', 'comboBox'); // Popovers with dismissOnClickOutside: false may persist across blur const allOverlays = getOverlays(); allOverlays.forEach((overlay) => { @@ -663,7 +663,7 @@ function registerAutoDismissListeners(): void { // changes that would otherwise immediately dismiss the just-created context menu if (Date.now() - lastOverlayCreatedAt < OVERLAY_CREATION_GRACE_MS) return; - dismissAll('contextMenu', 'commandPalette', 'popover'); + dismissAll('contextMenu', 'comboBox', 'popover'); }) .catch((err) => logger.warn(`Failed to subscribe to window focus changes: ${err}`)); } diff --git a/src/renderer/services/overlays/overlay.service-model.ts b/src/renderer/services/overlays/overlay.service-model.ts index 840618c3cfe..c67229fc4dd 100644 --- a/src/renderer/services/overlays/overlay.service-model.ts +++ b/src/renderer/services/overlays/overlay.service-model.ts @@ -1,6 +1,6 @@ /** * Type definitions for the overlay service, a renderer-only service that manages overlays (context - * menus, popovers, command palettes) rendered in the renderer's top-level document outside iframe + * menus, popovers, combo boxes) rendered in the renderer's top-level document outside iframe * boundaries. Extensions running in sandboxed WebView iframes cannot render UI above other content, * so this service provides a way for them to request overlays that the renderer hosts on their * behalf. @@ -86,16 +86,16 @@ export interface PopoverRequest { showArrow?: boolean; } -// ── Command Palette Types ── +// ── Quick Pick Types ── /** - * A single item in a command palette. Items are displayed in a searchable, filterable list. The - * user types to filter and selects one item. + * A single item in a combo box. Items are displayed in a searchable, filterable list. The user + * types to filter and selects one item. */ -export type CommandPaletteItem = { +export type ComboBoxItem = { /** Unique identifier returned when this item is selected */ id: string; - /** Primary display text (e.g., marker code like "ft" or command name) */ + /** Primary display text (e.g., marker code like "ft") */ label: string | LocalizeKey; /** Secondary description text displayed below the label */ description?: string | LocalizeKey; @@ -109,16 +109,16 @@ export type CommandPaletteItem = { disabled?: boolean; }; -/** Request payload for {@link IOverlayService.showCommandPalette}. */ -export interface CommandPaletteRequest { +/** Request payload for {@link IOverlayService.showComboBox}. */ +export interface ComboBoxRequest { /** The selectable items to display */ - items: CommandPaletteItem[]; + items: ComboBoxItem[]; /** - * Anchor position in pixels relative to the requesting WebView's iframe origin. The palette is + * Anchor position in pixels relative to the requesting WebView's iframe origin. The combo box is * positioned adjacent to this point. If omitted, centers in the viewport. */ anchor?: { x: number; y: number; width?: number; height?: number }; - /** Preferred side of the anchor to place the palette. Defaults to 'bottom'. */ + /** Preferred side of the anchor to place the combo box. Defaults to 'bottom'. */ side?: 'top' | 'bottom' | 'left' | 'right'; /** Placeholder text for the search input */ placeholder?: string | LocalizeKey; @@ -126,17 +126,27 @@ export interface CommandPaletteRequest { maxWidth?: number; /** Maximum height in pixels. Defaults to 400. */ maxHeight?: number; - /** Whether clicking outside dismisses the palette. Defaults to true. */ + /** Whether clicking outside dismisses the combo box. Defaults to true. */ dismissOnClickOutside?: boolean; } +/** + * @deprecated Use {@link ComboBoxItem}. The "command palette" terminology was misleading — this + * overlay is a generic per-WebView searchable picker (used today for USFM marker selection), not + * the global Action Palette concept. See `docs/plans/2026-05-15-action-palette-proposal.md`. + */ +export type CommandPaletteItem = ComboBoxItem; + +/** @deprecated Use {@link ComboBoxRequest}. See {@link CommandPaletteItem} for context. */ +export type CommandPaletteRequest = ComboBoxRequest; + // ── Service Interface ── /** * JSDOC SOURCE overlayService * - * Service for showing overlays (context menus, popovers, command palettes) that render outside - * iframe boundaries in the renderer's top-level document. Renderer-only service. + * Service for showing overlays (context menus, popovers, combo boxes) that render outside iframe + * boundaries in the renderer's top-level document. Renderer-only service. * * Extensions in sandboxed WebView iframes cannot render UI above other content or outside their * iframe bounds. This service accepts overlay requests from WebViews, translates their @@ -144,9 +154,9 @@ export interface CommandPaletteRequest { * renderer's React tree. Each method returns a promise that resolves when the user interacts with * the overlay or it is dismissed. * - * Only one overlay of each type (context menu, popover, command palette) can be active per WebView - * at a time. Requesting a new overlay of the same type from the same WebView replaces the previous - * one and rejects its promise with a PlatformError with code ABORTED. + * Only one overlay of each type (context menu, popover, combo box) can be active per WebView at a + * time. Requesting a new overlay of the same type from the same WebView replaces the previous one + * and rejects its promise with a PlatformError with code ABORTED. */ export interface IOverlayService { /** @@ -212,20 +222,21 @@ export interface IOverlayService { */ onPopoverDismissed(overlayId: string): Promise; /** - * Shows a command palette with searchable/filterable items. Returns a promise that resolves with - * the selected item's `id`, or `undefined` if dismissed. + * Shows a combo box with searchable/filterable items. Returns a promise that resolves with the + * selected item's `id`, or `undefined` if dismissed. * * @param request The items, optional anchor position, and display options - * @param webViewId The ID of the WebView requesting the command palette + * @param webViewId The ID of the WebView requesting the combo box * @returns The selected item's ID, or `undefined` if dismissed * @throws PlatformError with code INVALID_ARGUMENT if the request is invalid - * @throws PlatformError with code ABORTED if replaced by another command palette from the same - * WebView + * @throws PlatformError with code ABORTED if replaced by another combo box from the same WebView */ - showCommandPalette( - request: CommandPaletteRequest, - webViewId: string, - ): Promise; + showComboBox(request: ComboBoxRequest, webViewId: string): Promise; + /** + * @deprecated Use {@link showComboBox}. The "command palette" terminology was misleading; see + * `docs/plans/2026-05-15-action-palette-proposal.md`. + */ + showCommandPalette(request: ComboBoxRequest, webViewId: string): Promise; } // ── Internal Overlay Store Types ── @@ -240,7 +251,7 @@ export interface IOverlayService { * - `'contextMenu'` — An active context menu with translated position and menu items. * - `'modalDialog'` — An active modal dialog with its type-specific options. * - `'popover'` — An active popover with mutable `content` (updatable via `updatePopover`). - * - `'commandPalette'` — An active command palette with searchable/filterable items. + * - `'comboBox'` — An active combo box with searchable/filterable items. * * UI components read entries from the overlay store to render overlays, then call `resolve` or * `reject` when the user interacts with or dismisses them. @@ -300,15 +311,15 @@ export type OverlayEntry = reject: (error: PlatformError) => void; } | { - type: 'commandPalette'; + type: 'comboBox'; /** Unique overlay identifier generated by the service */ id: string; /** The WebView that requested this overlay */ webViewId: string; /** The original request */ - request: CommandPaletteRequest; + request: ComboBoxRequest; /** Items to render */ - items: CommandPaletteItem[]; + items: ComboBoxItem[]; /** Document-relative position (translated + clamped), or undefined for centered */ position?: { x: number; y: number }; /** Settles the caller's promise with the selected item ID, or undefined if dismissed */ @@ -325,5 +336,5 @@ export type OverlayResolveType = { contextMenu: string | undefined; modalDialog: unknown; popover: string | undefined; - commandPalette: string | undefined; + comboBox: string | undefined; }; diff --git a/src/shared/services/papi-core.service.ts b/src/shared/services/papi-core.service.ts index 9b123fc641a..1f0880c7a23 100644 --- a/src/shared/services/papi-core.service.ts +++ b/src/shared/services/papi-core.service.ts @@ -7,7 +7,11 @@ export type { ExecutionToken } from '@node/models/execution-token.model'; export type { DialogTypes } from '@renderer/components/dialogs/dialog-definition.model'; export type { UseDialogCallbackOptions } from '@renderer/hooks/papi-hooks/use-dialog-callback.hook'; export type { + ComboBoxItem, + ComboBoxRequest, + /** @deprecated Use {@link ComboBoxItem}. */ CommandPaletteItem, + /** @deprecated Use {@link ComboBoxRequest}. */ CommandPaletteRequest, IOverlayService, PopoverAction,