From 3f0ea4ec8283c8804e242fbabf98109a93727652 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 23 Mar 2026 09:47:10 +0000 Subject: [PATCH] Extract search input enhancement presentation owner --- .../subsystems/frontend-primitives.md | 9 +++++ .../shared/SearchInputEnhancements.tsx | 36 ++++++++++--------- .../SharedPrimitives.guardrails.test.ts | 22 ++++++++++++ .../shared/__tests__/SearchInput.test.tsx | 24 +++++++++++++ .../shared/searchInputEnhancementsModel.ts | 28 +++++++++++++++ .../frontendResourceTypeBoundaries.test.ts | 19 ++++++++++ 6 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 frontend-modern/src/components/shared/searchInputEnhancementsModel.ts diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 614e3c076..fe606bcd4 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -367,6 +367,15 @@ and `frontend-modern/src/components/shared/searchInputModel.ts` owns the shared search-input contract plus shortcut-hint and trailing-control policy. Future search-input work should extend those owners instead of pushing type-to-search or enhancement wiring back into the shared shell. +The search-input enhancement surfaces now follow that same owner split. +`frontend-modern/src/components/shared/SearchInputEnhancements.tsx` stays the +render shell, `frontend-modern/src/components/shared/useSearchInputEnhancements.ts` +owns search-history persistence, menu-open lifecycle, blur commit policy, and +tips/history interaction runtime, and +`frontend-modern/src/components/shared/searchInputEnhancementsModel.ts` owns +history-toggle copy plus history-menu button and row class policy. Future +search-input-enhancement work should extend those owners instead of pushing +history copy or menu presentation policy back into the shell. The shared search tips popover now follows that same owner split. `frontend-modern/src/components/shared/SearchTipsPopover.tsx` stays the render shell, `frontend-modern/src/components/shared/useSearchTipsPopoverState.ts` diff --git a/frontend-modern/src/components/shared/SearchInputEnhancements.tsx b/frontend-modern/src/components/shared/SearchInputEnhancements.tsx index d7d7edf49..71c7a766e 100644 --- a/frontend-modern/src/components/shared/SearchInputEnhancements.tsx +++ b/frontend-modern/src/components/shared/SearchInputEnhancements.tsx @@ -1,5 +1,16 @@ import { Component, For, Show } from 'solid-js'; import { SearchTipsPopover } from '@/components/shared/SearchTipsPopover'; +import { + SEARCH_HISTORY_CLEAR_LABEL, + SEARCH_HISTORY_EMPTY_STATE_CLASS, + SEARCH_HISTORY_ENTRY_BUTTON_CLASS, + SEARCH_HISTORY_MENU_CLASS, + SEARCH_HISTORY_ROW_CLASS, + getSearchHistoryClearButtonClass, + getSearchHistoryDeleteButtonClass, + getSearchHistoryToggleButtonClass, + getSearchHistoryToggleTitle, +} from '@/components/shared/searchInputEnhancementsModel'; import type { SearchInputEnhancementsState, SearchTipsConfig, @@ -16,19 +27,12 @@ export const SearchInputTrailingControls: Component 0 ? 'Show recent searches' : 'No recent searches yet' - } + title={getSearchHistoryToggleTitle(props.state.searchHistory().length)} > @@ -59,22 +63,22 @@ export const SearchInputHistoryDropdown: Component
0} fallback={ -
{props.state.emptyHistoryMessage()}
+
{props.state.emptyHistoryMessage()}
} >
{(entry) => ( -
+
diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index ca3524a60..786997143 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -37,6 +37,8 @@ import whatsNewModalModelSource from '@/components/shared/whatsNewModalModel.ts? import searchFieldSource from '@/components/shared/SearchField.tsx?raw'; import searchFieldModelSource from '@/components/shared/searchFieldModel.ts?raw'; import searchInputSource from '@/components/shared/SearchInput.tsx?raw'; +import searchInputEnhancementsSource from '@/components/shared/SearchInputEnhancements.tsx?raw'; +import searchInputEnhancementsModelSource from '@/components/shared/searchInputEnhancementsModel.ts?raw'; import searchInputModelSource from '@/components/shared/searchInputModel.ts?raw'; import scrollToTopButtonSource from '@/components/shared/ScrollToTopButton.tsx?raw'; import scrollToTopButtonModelSource from '@/components/shared/scrollToTopButtonModel.ts?raw'; @@ -80,6 +82,7 @@ import pulseDataGridStateSource from '@/components/shared/usePulseDataGridState. import whatsNewModalStateSource from '@/components/shared/useWhatsNewModalState.ts?raw'; import searchFieldStateSource from '@/components/shared/useSearchFieldState.ts?raw'; import searchInputStateSource from '@/components/shared/useSearchInputState.ts?raw'; +import searchInputEnhancementsStateSource from '@/components/shared/useSearchInputEnhancements.ts?raw'; import scrollToTopButtonStateSource from '@/components/shared/useScrollToTopButtonState.ts?raw'; import statusBadgeStateSource from '@/components/shared/useStatusBadgeState.ts?raw'; import toggleStateSource from '@/components/shared/useToggleState.ts?raw'; @@ -739,6 +742,25 @@ describe('shared primitive guardrails', () => { expect(searchInputModelSource).toContain('getSearchInputShortcutHint'); expect(searchInputModelSource).toContain('shouldSearchInputShowTrailingControls'); expect(searchInputModelSource).toContain('export interface SearchInputProps'); + + expect(searchInputEnhancementsSource).toContain('getSearchHistoryToggleButtonClass'); + expect(searchInputEnhancementsSource).toContain('getSearchHistoryToggleTitle'); + expect(searchInputEnhancementsSource).toContain('SEARCH_HISTORY_CLEAR_LABEL'); + expect(searchInputEnhancementsSource).not.toContain('Show recent searches'); + expect(searchInputEnhancementsSource).not.toContain('No recent searches yet'); + expect(searchInputEnhancementsSource).not.toContain('Clear history'); + expect(searchInputEnhancementsSource).not.toContain('hover:bg-blue-50'); + + expect(searchInputEnhancementsStateSource).toContain('createSearchHistoryManager'); + expect(searchInputEnhancementsStateSource).not.toContain('Show recent searches'); + expect(searchInputEnhancementsStateSource).toContain( + "options.history?.emptyMessage ?? 'Searches you run will appear here.'", + ); + + expect(searchInputEnhancementsModelSource).toContain('getSearchHistoryToggleButtonClass'); + expect(searchInputEnhancementsModelSource).toContain('getSearchHistoryToggleTitle'); + expect(searchInputEnhancementsModelSource).toContain('SEARCH_HISTORY_CLEAR_LABEL'); + expect(searchInputEnhancementsModelSource).toContain('SEARCH_HISTORY_MENU_CLASS'); }); it('keeps search tips popover on shell, runtime, and model owners', () => { diff --git a/frontend-modern/src/components/shared/__tests__/SearchInput.test.tsx b/frontend-modern/src/components/shared/__tests__/SearchInput.test.tsx index 4ebe7041a..265aaeae3 100644 --- a/frontend-modern/src/components/shared/__tests__/SearchInput.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/SearchInput.test.tsx @@ -5,6 +5,9 @@ import { CollapsibleSearchInput } from '@/components/shared/CollapsibleSearchInp import { SearchInput } from '@/components/shared/SearchInput'; import searchInputSource from '@/components/shared/SearchInput.tsx?raw'; import searchInputModelSource from '@/components/shared/searchInputModel.ts?raw'; +import searchInputEnhancementsSource from '@/components/shared/SearchInputEnhancements.tsx?raw'; +import searchInputEnhancementsModelSource from '@/components/shared/searchInputEnhancementsModel.ts?raw'; +import searchInputEnhancementsStateSource from '@/components/shared/useSearchInputEnhancements.ts?raw'; import searchInputStateSource from '@/components/shared/useSearchInputState.ts?raw'; import collapsibleSearchInputSource from '@/components/shared/CollapsibleSearchInput.tsx?raw'; import collapsibleSearchInputModelSource from '@/components/shared/collapsibleSearchInputModel.ts?raw'; @@ -58,6 +61,27 @@ describe('SearchInput', () => { expect(searchInputModelSource).toContain('getSearchInputShortcutHint'); expect(searchInputModelSource).toContain('shouldSearchInputShowTrailingControls'); expect(searchInputModelSource).toContain('export interface SearchInputProps'); + + expect(searchInputEnhancementsSource).toContain('getSearchHistoryToggleButtonClass'); + expect(searchInputEnhancementsSource).toContain('getSearchHistoryToggleTitle'); + expect(searchInputEnhancementsSource).toContain('SEARCH_HISTORY_CLEAR_LABEL'); + expect(searchInputEnhancementsSource).not.toContain('Show recent searches'); + expect(searchInputEnhancementsSource).not.toContain('No recent searches yet'); + expect(searchInputEnhancementsSource).not.toContain('Clear history'); + expect(searchInputEnhancementsSource).not.toContain('hover:bg-blue-50'); + + expect(searchInputStateSource).toContain('useSearchInputEnhancements'); + + expect(searchInputEnhancementsStateSource).toContain('createSearchHistoryManager'); + expect(searchInputEnhancementsStateSource).toContain( + "options.history?.emptyMessage ?? 'Searches you run will appear here.'", + ); + expect(searchInputEnhancementsStateSource).not.toContain('Show recent searches'); + + expect(searchInputEnhancementsModelSource).toContain('getSearchHistoryToggleButtonClass'); + expect(searchInputEnhancementsModelSource).toContain('getSearchHistoryToggleTitle'); + expect(searchInputEnhancementsModelSource).toContain('SEARCH_HISTORY_CLEAR_LABEL'); + expect(searchInputEnhancementsModelSource).toContain('SEARCH_HISTORY_MENU_CLASS'); }); it('keeps collapsible search input on shell, runtime, and model owners', () => { diff --git a/frontend-modern/src/components/shared/searchInputEnhancementsModel.ts b/frontend-modern/src/components/shared/searchInputEnhancementsModel.ts new file mode 100644 index 000000000..8c988bb0a --- /dev/null +++ b/frontend-modern/src/components/shared/searchInputEnhancementsModel.ts @@ -0,0 +1,28 @@ +export const SEARCH_HISTORY_MENU_CLASS = + 'absolute left-0 right-0 top-full z-50 mt-2 w-full overflow-hidden rounded-md border border-border bg-surface text-sm shadow-sm'; +export const SEARCH_HISTORY_EMPTY_STATE_CLASS = 'px-3 py-2 text-xs text-muted'; +export const SEARCH_HISTORY_ROW_CLASS = + 'flex items-center justify-between px-2 py-1.5 hover:bg-blue-50 dark:hover:bg-blue-900'; +export const SEARCH_HISTORY_ENTRY_BUTTON_CLASS = + 'flex-1 truncate pr-2 text-left text-sm text-base-content transition-colors hover:text-blue-600 focus:outline-none dark:hover:text-blue-300'; +export const SEARCH_HISTORY_CLEAR_LABEL = 'Clear history'; + +export function getSearchHistoryToggleButtonClass(isOpen: boolean): string { + return `flex h-7 w-7 items-center justify-center rounded-md transition-colors ${ + isOpen + ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400' + : 'text-muted hover:bg-surface-hover hover:text-base-content' + }`; +} + +export function getSearchHistoryToggleTitle(historyCount: number): string { + return historyCount > 0 ? 'Show recent searches' : 'No recent searches yet'; +} + +export function getSearchHistoryDeleteButtonClass(): string { + return 'ml-1 flex h-6 w-6 items-center justify-center rounded text-slate-400 transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none'; +} + +export function getSearchHistoryClearButtonClass(): string { + return 'flex w-full items-center justify-center gap-2 border-t border-border px-3 py-2 text-xs font-medium text-muted transition-colors hover:bg-surface-hover hover:text-base-content'; +} diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 588e8d516..bb84a53e9 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -43,6 +43,8 @@ import whatsNewModalModelSource from '@/components/shared/whatsNewModalModel.ts? import searchFieldSource from '@/components/shared/SearchField.tsx?raw'; import searchFieldModelSource from '@/components/shared/searchFieldModel.ts?raw'; import searchInputSource from '@/components/shared/SearchInput.tsx?raw'; +import searchInputEnhancementsSource from '@/components/shared/SearchInputEnhancements.tsx?raw'; +import searchInputEnhancementsModelSource from '@/components/shared/searchInputEnhancementsModel.ts?raw'; import searchInputModelSource from '@/components/shared/searchInputModel.ts?raw'; import scrollToTopButtonSource from '@/components/shared/ScrollToTopButton.tsx?raw'; import scrollToTopButtonModelSource from '@/components/shared/scrollToTopButtonModel.ts?raw'; @@ -82,6 +84,7 @@ import pulseDataGridStateSource from '@/components/shared/usePulseDataGridState. import whatsNewModalStateSource from '@/components/shared/useWhatsNewModalState.ts?raw'; import searchFieldStateSource from '@/components/shared/useSearchFieldState.ts?raw'; import searchInputStateSource from '@/components/shared/useSearchInputState.ts?raw'; +import searchInputEnhancementsStateSource from '@/components/shared/useSearchInputEnhancements.ts?raw'; import scrollToTopButtonStateSource from '@/components/shared/useScrollToTopButtonState.ts?raw'; import statusBadgeStateSource from '@/components/shared/useStatusBadgeState.ts?raw'; import toggleStateSource from '@/components/shared/useToggleState.ts?raw'; @@ -2726,6 +2729,22 @@ describe('frontend resource type boundaries', () => { expect(searchInputStateSource).toContain('getSearchInputShortcutHint'); expect(searchInputModelSource).toContain('getSearchInputShortcutHint'); expect(searchInputModelSource).toContain('shouldSearchInputShowTrailingControls'); + expect(searchInputEnhancementsSource).toContain('getSearchHistoryToggleButtonClass'); + expect(searchInputEnhancementsSource).toContain('getSearchHistoryToggleTitle'); + expect(searchInputEnhancementsSource).toContain('SEARCH_HISTORY_CLEAR_LABEL'); + expect(searchInputEnhancementsSource).not.toContain('Show recent searches'); + expect(searchInputEnhancementsSource).not.toContain('No recent searches yet'); + expect(searchInputEnhancementsSource).not.toContain('Clear history'); + expect(searchInputEnhancementsSource).not.toContain('hover:bg-blue-50'); + expect(searchInputEnhancementsStateSource).toContain('createSearchHistoryManager'); + expect(searchInputEnhancementsStateSource).not.toContain('Show recent searches'); + expect(searchInputEnhancementsStateSource).toContain( + "options.history?.emptyMessage ?? 'Searches you run will appear here.'", + ); + expect(searchInputEnhancementsModelSource).toContain('getSearchHistoryToggleButtonClass'); + expect(searchInputEnhancementsModelSource).toContain('getSearchHistoryToggleTitle'); + expect(searchInputEnhancementsModelSource).toContain('SEARCH_HISTORY_CLEAR_LABEL'); + expect(searchInputEnhancementsModelSource).toContain('SEARCH_HISTORY_MENU_CLASS'); expect(searchTipsPopoverSource).toContain('useSearchTipsPopoverState'); expect(searchTipsPopoverSource).toContain('getSearchTipsPopoverTriggerClass'); expect(searchTipsPopoverSource).not.toContain('createSignal');