Refine search tips popovers

This commit is contained in:
rcourtman
2025-10-01 16:57:43 +00:00
parent 03f823868d
commit fe01b72541
4 changed files with 110 additions and 47 deletions
@@ -46,7 +46,7 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
placeholder="Search backups or node:nodename"
value={props.search()}
onInput={(e) => props.setSearch(e.currentTarget.value)}
class="w-full pl-9 pr-9 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
class="w-full pl-9 pr-16 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all"
title="Search backups by name or filter by node"
@@ -64,19 +64,23 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<div class="absolute inset-y-0 right-3 flex items-center">
<SearchTipsPopover
popoverId="backups-search-help"
intro="Quick examples"
tips={[
{ code: 'media', description: 'Backups with "media" in the name' },
{ code: 'node:pve1', description: 'Show backups from a specific node' },
{ code: 'vm-104', description: 'Locate backups for VM 104' },
]}
footerHighlight="node:pve1 vm-104"
footerText="Mix terms to focus on the exact backups you need."
triggerVariant="icon"
buttonLabel="Search tips"
openOnHover
/>
</div>
</div>
<SearchTipsPopover
class="flex-shrink-0"
popoverId="backups-search-help"
intro="Quick examples"
tips={[
{ code: 'media', description: 'Backups with "media" in the name' },
{ code: 'node:pve1', description: 'Show backups from a specific node' },
{ code: 'vm-104', description: 'Locate backups for VM 104' },
]}
footerHighlight="node:pve1 vm-104"
footerText="Mix terms to focus on the exact backups you need."
/>
</div>
{/* Filters */}
@@ -30,7 +30,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
placeholder="Search or filter..."
value={props.search()}
onInput={(e) => props.setSearch(e.currentTarget.value)}
class={`w-full pl-9 pr-9 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
class={`w-full pl-9 pr-16 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all`}
/>
@@ -50,7 +50,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
<Show when={props.search()}>
<button
type="button"
class="absolute right-3 top-2 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
class="absolute right-9 top-1/2 -translate-y-1/2 transform text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
onClick={() => props.setSearch('')}
aria-label="Clear search"
title="Clear search"
@@ -65,22 +65,25 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
</svg>
</button>
</Show>
<div class="absolute inset-y-0 right-3 flex items-center">
<SearchTipsPopover
popoverId="dashboard-search-help"
intro="Combine filters to narrow results"
tips={[
{ code: 'media', description: 'Guests with "media" in the name' },
{ code: 'cpu>80', description: 'Highlight guests using more than 80% CPU' },
{ code: 'memory<20', description: 'Find guests under 20% memory usage' },
{ code: 'tags:prod', description: 'Filter by tag' },
{ code: 'node:pve1', description: 'Show guests on a specific node' },
]}
footerHighlight="node:pve1 cpu>60"
footerText="Stack filters to get laser-focused results."
triggerVariant="icon"
buttonLabel="Search tips"
openOnHover
/>
</div>
</div>
<SearchTipsPopover
class="flex-shrink-0"
popoverId="dashboard-search-help"
intro="Combine filters to narrow results"
tips={[
{ code: 'media', description: 'Guests with "media" in the name' },
{ code: 'cpu>80', description: 'Highlight guests using more than 80% CPU' },
{ code: 'memory<20', description: 'Find guests under 20% memory usage' },
{ code: 'tags:prod', description: 'Filter by tag' },
{ code: 'node:pve1', description: 'Show guests on a specific node' },
]}
footerHighlight="node:pve1 cpu>60"
footerText="Stack filters to get laser-focused results."
/>
</div>
{/* Filters */}
@@ -38,7 +38,7 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
placeholder="Search storage or node:nodename"
value={props.search()}
onInput={(e) => props.setSearch(e.currentTarget.value)}
class="w-full pl-9 pr-9 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
class="w-full pl-9 pr-16 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all"
title="Search storage by name or filter by node"
@@ -56,19 +56,23 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<div class="absolute inset-y-0 right-3 flex items-center">
<SearchTipsPopover
popoverId="storage-search-help"
intro="Quick examples"
tips={[
{ code: 'local', description: 'Storage with "local" in the name' },
{ code: 'node:pve1', description: 'Show storage on a specific node' },
{ code: 'nfs', description: 'Find NFS storage' },
]}
footerHighlight="node:pve1 nfs"
footerText="Combine filters to zero in on exactly what you need."
triggerVariant="icon"
buttonLabel="Search tips"
openOnHover
/>
</div>
</div>
<SearchTipsPopover
class="flex-shrink-0"
popoverId="storage-search-help"
intro="Quick examples"
tips={[
{ code: 'local', description: 'Storage with "local" in the name' },
{ code: 'node:pve1', description: 'Show storage on a specific node' },
{ code: 'nfs', description: 'Find NFS storage' },
]}
footerHighlight="node:pve1 nfs"
footerText="Combine filters to zero in on exactly what you need."
/>
</div>
{/* Filters */}
@@ -15,12 +15,15 @@ interface SearchTipsPopoverProps {
popoverId?: string;
align?: 'left' | 'right';
class?: string;
triggerVariant?: 'button' | 'link' | 'icon';
openOnHover?: boolean;
}
export const SearchTipsPopover: Component<SearchTipsPopoverProps> = (props) => {
const [open, setOpen] = createSignal(false);
let popoverRef: HTMLDivElement | undefined;
let triggerRef: HTMLButtonElement | undefined;
let pointerInside = false;
const close = () => setOpen(false);
@@ -57,17 +60,66 @@ export const SearchTipsPopover: Component<SearchTipsPopoverProps> = (props) => {
const popoverPositionClass = props.align === 'left' ? 'left-0' : 'right-0';
const popoverId = props.popoverId ?? 'search-tips-popover';
const triggerVariant = props.triggerVariant ?? 'button';
const openOnHover = props.openOnHover ?? false;
const buttonLabel = props.buttonLabel ?? 'Search tips';
const triggerBaseClasses =
'text-xs font-medium focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:ring-offset-1 focus:ring-offset-white dark:focus:ring-blue-400/40 dark:focus:ring-offset-gray-900';
const triggerClasses =
triggerVariant === 'button'
? `rounded-md border border-gray-200 px-2.5 py-1 text-gray-600 transition-colors hover:bg-gray-100 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700 ${triggerBaseClasses}`
: triggerVariant === 'link'
? `rounded px-1 py-0.5 text-gray-500 underline decoration-dotted underline-offset-4 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 ${triggerBaseClasses}`
: `flex h-5 w-5 items-center justify-center rounded-full text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-100 ${triggerBaseClasses}`;
const handleMouseEnter = () => {
pointerInside = true;
setOpen(true);
};
const handleMouseLeave = () => {
pointerInside = false;
setOpen(false);
};
return (
<div class={`relative ${props.class ?? ''}`}>
<div
class={`relative ${props.class ?? ''}`}
onMouseEnter={openOnHover ? handleMouseEnter : undefined}
onMouseLeave={openOnHover ? handleMouseLeave : undefined}
>
<button
ref={(el) => (triggerRef = el)}
type="button"
class="rounded-md border border-gray-200 px-2.5 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:ring-offset-1 focus:ring-offset-white dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:ring-blue-400/40 dark:focus:ring-offset-gray-900"
onClick={() => setOpen((value) => !value)}
class={triggerClasses}
onClick={openOnHover ? () => setOpen(true) : () => setOpen((value) => !value)}
onFocus={() => setOpen(true)}
onBlur={() => {
if (!pointerInside) {
setOpen(false);
}
}}
aria-expanded={open()}
aria-controls={popoverId}
aria-label={buttonLabel}
>
{props.buttonLabel ?? 'Search tips'}
{triggerVariant === 'icon' ? (
<>
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span class="sr-only">{buttonLabel}</span>
</>
) : (
buttonLabel
)}
</button>
<Show when={open()}>