mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-25 04:33:03 +00:00
Extract search input enhancement presentation owner
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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<SearchInputTrailingControlsP
|
||||
<button
|
||||
ref={props.state.setHistoryToggleRef}
|
||||
type="button"
|
||||
class={`flex h-7 w-7 items-center justify-center rounded-md transition-colors
|
||||
${
|
||||
props.state.isHistoryOpen()
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||
: 'text-muted hover:bg-surface-hover hover:text-base-content'
|
||||
}`}
|
||||
class={getSearchHistoryToggleButtonClass(props.state.isHistoryOpen())}
|
||||
onClick={props.state.toggleHistory}
|
||||
onMouseDown={props.state.onClearMouseDown}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={props.state.isHistoryOpen()}
|
||||
title={
|
||||
props.state.searchHistory().length > 0 ? 'Show recent searches' : 'No recent searches yet'
|
||||
}
|
||||
title={getSearchHistoryToggleTitle(props.state.searchHistory().length)}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
||||
@@ -59,22 +63,22 @@ export const SearchInputHistoryDropdown: Component<SearchInputHistoryDropdownPro
|
||||
<Show when={props.state.hasHistory() && props.state.isHistoryOpen()}>
|
||||
<div
|
||||
ref={props.state.setHistoryMenuRef}
|
||||
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"
|
||||
class={SEARCH_HISTORY_MENU_CLASS}
|
||||
role="listbox"
|
||||
>
|
||||
<Show
|
||||
when={props.state.searchHistory().length > 0}
|
||||
fallback={
|
||||
<div class="px-3 py-2 text-xs text-muted">{props.state.emptyHistoryMessage()}</div>
|
||||
<div class={SEARCH_HISTORY_EMPTY_STATE_CLASS}>{props.state.emptyHistoryMessage()}</div>
|
||||
}
|
||||
>
|
||||
<div class="max-h-52 overflow-y-auto py-1">
|
||||
<For each={props.state.searchHistory()}>
|
||||
{(entry) => (
|
||||
<div class="flex items-center justify-between px-2 py-1.5 hover:bg-blue-50 dark:hover:bg-blue-900">
|
||||
<div class={SEARCH_HISTORY_ROW_CLASS}>
|
||||
<button
|
||||
type="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"
|
||||
class={SEARCH_HISTORY_ENTRY_BUTTON_CLASS}
|
||||
onClick={() => props.state.selectHistoryEntry(entry)}
|
||||
onMouseDown={props.state.onClearMouseDown}
|
||||
>
|
||||
@@ -82,7 +86,7 @@ export const SearchInputHistoryDropdown: Component<SearchInputHistoryDropdownPro
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="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"
|
||||
class={getSearchHistoryDeleteButtonClass()}
|
||||
title="Remove from history"
|
||||
onClick={() => props.state.deleteHistoryEntry(entry)}
|
||||
onMouseDown={props.state.onClearMouseDown}
|
||||
@@ -102,7 +106,7 @@ export const SearchInputHistoryDropdown: Component<SearchInputHistoryDropdownPro
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="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"
|
||||
class={getSearchHistoryClearButtonClass()}
|
||||
onClick={props.state.clearHistory}
|
||||
onMouseDown={props.state.onClearMouseDown}
|
||||
>
|
||||
@@ -114,7 +118,7 @@ export const SearchInputHistoryDropdown: Component<SearchInputHistoryDropdownPro
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3m-9 0h12"
|
||||
/>
|
||||
</svg>
|
||||
Clear history
|
||||
{SEARCH_HISTORY_CLEAR_LABEL}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user