mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 03:04:03 +00:00
Refine search tips popovers
This commit is contained in:
@@ -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()}>
|
||||
|
||||
Reference in New Issue
Block a user