mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-15 17:28:12 +00:00
fix: search settings popup and positioning refinement (#1476)
Hey @calibrain, thought i'd come back to do a quick UI fix I noticed :) A previous PR appears to have broken the search options in any context that is not the desktop homescreen, they can clip off the page on mobile, or break the search results screen. I've tweaked this to revive the popover for search results context and allowed for cleaner wrapping on mobile, plus a little bit of UI refinement. Desktop homepage: <img width="2880" height="1800" alt="image" src="https://github.com/user-attachments/assets/a129ce56-1bea-4ede-a9c6-99777a4bc3cc" /> Mobile homepage: <img width="600" alt="image" src="https://github.com/user-attachments/assets/544e58f9-84bd-4802-97d5-22ee23c3745f" /> Desktop and mobile search results: <img width="2880" height="1800" alt="image" src="https://github.com/user-attachments/assets/9ec09e2b-fcb8-4342-a04e-c14255c373c1" />
This commit is contained in:
3 files changed
+177
-258
No files matched your search
@@ -745,6 +745,7 @@ export const Header = forwardRef<HeaderHandle, HeaderProps>(
|
||||
activeQueryTarget={activeQueryTarget}
|
||||
onQueryTargetChange={onQueryTargetChange}
|
||||
activeQueryField={activeQueryField}
|
||||
optionsLayout="popover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { InputHTMLAttributes } from 'react';
|
||||
import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import type { ComponentType, InputHTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef, useId, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useSearchMode } from '../contexts/SearchModeContext';
|
||||
import { useSearchBarAutocomplete } from '../hooks/searchBar/useSearchBarAutocomplete';
|
||||
@@ -9,7 +9,9 @@ import type { DynamicFieldOption } from '../services/api';
|
||||
import type { ContentType, MetadataSearchField, QueryTargetOption, SortOption } from '../types';
|
||||
import { SearchBarAutocompleteSession } from './SearchBarAutocompleteSession';
|
||||
import { SearchBarDynamicOptionsSession } from './SearchBarDynamicOptionsSession';
|
||||
import { Tooltip } from './shared/Tooltip';
|
||||
import { ToggleSwitch } from './shared/ToggleSwitch';
|
||||
|
||||
type SearchOptionsLayout = 'popover' | 'inline';
|
||||
|
||||
interface SearchBarProps {
|
||||
value: string | number | boolean;
|
||||
@@ -41,6 +43,7 @@ interface SearchBarProps {
|
||||
onQueryTargetChange?: (target: string) => void;
|
||||
activeQueryField?: MetadataSearchField | null;
|
||||
disabled?: boolean;
|
||||
optionsLayout: SearchOptionsLayout;
|
||||
}
|
||||
|
||||
export interface SearchBarHandle {
|
||||
@@ -51,7 +54,24 @@ const EMPTY_SORT_OPTIONS: SortOption[] = [];
|
||||
const EMPTY_AUTOCOMPLETE_OPTIONS: DynamicFieldOption[] = [];
|
||||
const EMPTY_QUERY_TARGETS: QueryTargetOption[] = [];
|
||||
|
||||
const SEARCH_CONTROLS_PANEL_ID = 'search-bar-controls-panel';
|
||||
const OPTIONS_LAYOUT_CLASSES: Record<
|
||||
SearchOptionsLayout,
|
||||
{ panel: string; group: string; options: string; option: string }
|
||||
> = {
|
||||
popover: {
|
||||
panel: 'flex flex-col divide-y divide-(--border-muted)',
|
||||
group: 'flex flex-col gap-2 py-3 first:pt-0 last:pb-0',
|
||||
options: 'grid grid-cols-2 gap-2',
|
||||
option: '',
|
||||
},
|
||||
inline: {
|
||||
panel:
|
||||
'animate-fade-in-down flex flex-col gap-4 px-1 pt-2 sm:flex-row sm:flex-wrap sm:items-start sm:gap-x-8 sm:gap-y-2',
|
||||
group: 'flex min-w-0 flex-col gap-2',
|
||||
options: 'flex flex-col gap-1 sm:flex-row sm:flex-wrap sm:gap-2',
|
||||
option: 'max-sm:border-transparent',
|
||||
},
|
||||
};
|
||||
|
||||
const BookIcon = () => (
|
||||
<svg
|
||||
@@ -117,6 +137,18 @@ const CheckIcon = ({ className = 'w-3.5 h-3.5' }: { className?: string }) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CONTENT_TYPE_OPTIONS: { type: ContentType; label: string; Icon: ComponentType }[] = [
|
||||
{ type: 'ebook', label: 'Books', Icon: BookIcon },
|
||||
{ type: 'audiobook', label: 'Audiobooks', Icon: AudiobookIcon },
|
||||
];
|
||||
|
||||
const OptionsGroupHeader = ({ label, action }: { label: string; action: ReactNode }) => (
|
||||
<div className="flex min-h-7 items-center justify-between px-1">
|
||||
<span className="text-xs font-medium opacity-60">{label}</span>
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
|
||||
const getDefaultPlaceholder = (
|
||||
contentType: ContentType,
|
||||
activeQueryTarget: QueryTargetOption | undefined,
|
||||
@@ -191,6 +223,7 @@ export const SearchBar = forwardRef<SearchBarHandle, SearchBarProps>(
|
||||
onQueryTargetChange,
|
||||
activeQueryField,
|
||||
disabled = false,
|
||||
optionsLayout,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -202,13 +235,16 @@ export const SearchBar = forwardRef<SearchBarHandle, SearchBarProps>(
|
||||
const submitLatest = useLatestCallback(() => onSubmit());
|
||||
const selectorRef = useRef<HTMLDivElement>(null);
|
||||
const hasSearchQuery = hasActiveValue(value);
|
||||
const [isSelectorOpen, setIsSelectorOpen] = useState(false);
|
||||
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
||||
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
|
||||
const selectTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
const selectPanelRef = useRef<HTMLDivElement>(null);
|
||||
const autocompletePanelRef = useRef<HTMLDivElement>(null);
|
||||
const controlsPanelRef = useRef<HTMLDivElement>(null);
|
||||
const optionsPanelId = useId();
|
||||
const isPopover = optionsLayout === 'popover';
|
||||
const layoutClasses = OPTIONS_LAYOUT_CLASSES[optionsLayout];
|
||||
const [isOptionsOpen, setIsOptionsOpen] = useState(false);
|
||||
const closeOptions = () => setIsOptionsOpen(false);
|
||||
|
||||
const hasMultipleContentTypes = !allowedContentTypes || allowedContentTypes.length !== 1;
|
||||
const showContentTypeSelector =
|
||||
@@ -233,11 +269,8 @@ export const SearchBar = forwardRef<SearchBarHandle, SearchBarProps>(
|
||||
const combinedSelectionActive = combinedMode && !isManualTarget;
|
||||
const combinedSelectorLocked = combinedModeLocked && !isManualTarget;
|
||||
const combinedToggleAvailable = !!onCombinedModeChange && !isManualTarget;
|
||||
const combinedLineColor = combinedSelectionActive
|
||||
? 'bg-emerald-500'
|
||||
: 'bg-(--border-muted) group-hover:bg-zinc-400 dark:group-hover:bg-zinc-500';
|
||||
|
||||
useDismiss(isSelectorOpen, [selectorRef, controlsPanelRef], () => setIsSelectorOpen(false));
|
||||
useDismiss(isOptionsOpen && isPopover, [selectorRef], closeOptions);
|
||||
useDismiss(isSelectOpen, [selectPanelRef, selectTriggerRef], () => setIsSelectOpen(false));
|
||||
useDismiss(isAutocompleteOpen, [autocompletePanelRef, inputRef], () =>
|
||||
setIsAutocompleteOpen(false),
|
||||
@@ -341,13 +374,11 @@ export const SearchBar = forwardRef<SearchBarHandle, SearchBarProps>(
|
||||
onCombinedModeChange?.(false);
|
||||
};
|
||||
|
||||
const handleCombinedModeSelect = () => {
|
||||
if (combinedMode) {
|
||||
onCombinedModeChange?.(false);
|
||||
} else {
|
||||
const handleCombinedModeToggle = (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
onContentTypeChange?.('ebook');
|
||||
onCombinedModeChange?.(true);
|
||||
}
|
||||
onCombinedModeChange?.(enabled);
|
||||
};
|
||||
|
||||
const handleQueryTargetSelect = (targetKey: string) => {
|
||||
@@ -360,6 +391,9 @@ export const SearchBar = forwardRef<SearchBarHandle, SearchBarProps>(
|
||||
setIsSelectOpen(shouldOpenSelect);
|
||||
setIsAutocompleteOpen(false);
|
||||
resetAutocomplete();
|
||||
if (shouldOpenSelect) {
|
||||
closeOptions();
|
||||
}
|
||||
};
|
||||
|
||||
const effectivePlaceholder = getDefaultPlaceholder(
|
||||
@@ -552,6 +586,114 @@ export const SearchBar = forwardRef<SearchBarHandle, SearchBarProps>(
|
||||
selectorIcon = <BookIcon />;
|
||||
}
|
||||
|
||||
const activeOptionClasses =
|
||||
searchMode === 'direct'
|
||||
? 'border-sky-700/70 bg-sky-700 text-white'
|
||||
: 'border-emerald-500/70 bg-emerald-600 text-white';
|
||||
const optionClasses = (isActive: boolean) =>
|
||||
`flex min-w-0 cursor-pointer items-center gap-2 rounded-xl border px-3 py-2 text-sm font-medium transition-colors ${
|
||||
isActive ? activeOptionClasses : 'hover-surface border-(--border-muted) text-(--text)'
|
||||
} ${layoutClasses.option}`;
|
||||
|
||||
const optionsButton = onAdvancedToggle ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isPopover) {
|
||||
closeOptions();
|
||||
}
|
||||
onAdvancedToggle();
|
||||
}}
|
||||
className={`-mt-1.5 -mr-1 -mb-0.5 flex cursor-pointer items-center gap-1.5 rounded-xl px-4 py-2.5 text-xs font-medium transition-colors ${
|
||||
isAdvancedActive ? activeOptionClasses : 'hover-surface text-(--text-muted)'
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"
|
||||
/>
|
||||
</svg>
|
||||
Options
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
const optionsPanel = (
|
||||
<div id={optionsPanelId} className={layoutClasses.panel}>
|
||||
{showContentTypeSelector && (
|
||||
<div className={layoutClasses.group}>
|
||||
<OptionsGroupHeader label="Content" action={optionsButton} />
|
||||
<div className={layoutClasses.options}>
|
||||
{CONTENT_TYPE_OPTIONS.map(({ type, label, Icon }) => {
|
||||
const isActive = contentType === type || combinedSelectionActive;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={type}
|
||||
onClick={() => handleContentTypeSelect(type)}
|
||||
className={optionClasses(isActive)}
|
||||
>
|
||||
<span className="flex w-4 justify-center">
|
||||
{isActive ? <CheckIcon /> : <Icon />}
|
||||
</span>
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{combinedToggleAvailable && (
|
||||
<div className="flex items-center gap-3 px-1 py-1 text-sm font-medium">
|
||||
Combined
|
||||
<ToggleSwitch
|
||||
checked={combinedSelectionActive}
|
||||
onChange={handleCombinedModeToggle}
|
||||
disabled={combinedSelectorLocked}
|
||||
color="emerald"
|
||||
ariaLabel="Combined search"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{queryTargets.length > 1 && (
|
||||
<div className={layoutClasses.group}>
|
||||
<OptionsGroupHeader
|
||||
label="Search by"
|
||||
action={showContentTypeSelector ? null : optionsButton}
|
||||
/>
|
||||
<div className={layoutClasses.options}>
|
||||
{queryTargets.map((target) => {
|
||||
const isActive = target.key === activeTarget?.key;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={target.key}
|
||||
onClick={() => handleQueryTargetSelect(target.key)}
|
||||
title={target.description || target.label}
|
||||
aria-label={target.label}
|
||||
className={optionClasses(isActive)}
|
||||
>
|
||||
{isActive && <CheckIcon />}
|
||||
<span className="block truncate">{target.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const dynamicOptionsSession = dynamicEndpoint ? (
|
||||
<SearchBarDynamicOptionsSession
|
||||
key={dynamicEndpoint}
|
||||
@@ -604,15 +746,16 @@ export const SearchBar = forwardRef<SearchBarHandle, SearchBarProps>(
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsSelectorOpen((prev) => !prev);
|
||||
setIsOptionsOpen((prev) => !prev);
|
||||
setIsSelectOpen(false);
|
||||
setIsAutocompleteOpen(false);
|
||||
}}
|
||||
className="hover-action flex cursor-pointer items-center gap-1.5 rounded-l-full pr-2 pl-5 transition-colors"
|
||||
style={{ color: 'var(--text)' }}
|
||||
aria-label={`Searching ${selectorContentTypeLabel} by ${activeTarget?.label ?? 'general'}. Click to change.`}
|
||||
aria-expanded={isSelectorOpen}
|
||||
aria-controls={SEARCH_CONTROLS_PANEL_ID}
|
||||
aria-expanded={isOptionsOpen}
|
||||
aria-controls={optionsPanelId}
|
||||
aria-haspopup={isPopover ? 'dialog' : undefined}
|
||||
>
|
||||
{selectorIcon}
|
||||
{showActiveTargetLabel && (
|
||||
@@ -621,7 +764,7 @@ export const SearchBar = forwardRef<SearchBarHandle, SearchBarProps>(
|
||||
</span>
|
||||
)}
|
||||
<svg
|
||||
className={`h-3 w-3 opacity-50 transition-transform duration-200 ${isSelectorOpen ? 'rotate-180' : ''}`}
|
||||
className={`h-3 w-3 opacity-50 transition-transform duration-200 ${isOptionsOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -639,6 +782,18 @@ export const SearchBar = forwardRef<SearchBarHandle, SearchBarProps>(
|
||||
className="absolute top-1/2 right-0 h-6 w-px -translate-y-1/2"
|
||||
style={{ background: 'var(--border-muted)' }}
|
||||
/>
|
||||
|
||||
{isPopover && isOptionsOpen && (
|
||||
<div
|
||||
className="animate-fade-in-down absolute top-full left-0 z-50 mt-2 w-[min(20rem,calc(100vw-2rem))] overflow-hidden rounded-2xl border border-(--border-muted) bg-(--bg) shadow-2xl"
|
||||
role="dialog"
|
||||
aria-label="Search context"
|
||||
>
|
||||
<div className="max-h-[min(24rem,calc(100vh-8rem))] overflow-y-auto p-3">
|
||||
{optionsPanel}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -821,245 +976,7 @@ export const SearchBar = forwardRef<SearchBarHandle, SearchBarProps>(
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showQueryTargetSelector && isSelectorOpen && (
|
||||
<div
|
||||
id={SEARCH_CONTROLS_PANEL_ID}
|
||||
className="animate-fade-in-down flex flex-wrap items-start gap-x-8 gap-y-2 px-1 pt-2"
|
||||
ref={controlsPanelRef}
|
||||
>
|
||||
{showContentTypeSelector && (
|
||||
<div className="shrink-0">
|
||||
<div className="flex items-center justify-between pb-1.5">
|
||||
<span className="text-xs font-medium tracking-wide uppercase opacity-60">
|
||||
Content
|
||||
</span>
|
||||
{onAdvancedToggle && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdvancedToggle}
|
||||
className={`-mt-1.5 -mr-1 -mb-0.5 flex cursor-pointer items-center gap-1.5 rounded-xl px-4 py-2.5 text-xs font-medium transition-colors ${
|
||||
isAdvancedActive ? 'bg-emerald-600 text-white' : 'hover-surface'
|
||||
}`}
|
||||
style={
|
||||
isAdvancedActive
|
||||
? { borderColor: 'rgb(16 185 129 / 0.7)' }
|
||||
: { color: 'var(--text-muted)' }
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"
|
||||
/>
|
||||
</svg>
|
||||
Options
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid w-fit grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleContentTypeSelect('ebook')}
|
||||
className={`flex cursor-pointer items-center gap-2 rounded-xl border px-3 py-2 text-sm font-medium transition-colors ${
|
||||
contentType === 'ebook' || combinedSelectionActive
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'hover-surface'
|
||||
}`}
|
||||
style={
|
||||
contentType === 'ebook' || combinedSelectionActive
|
||||
? { borderColor: 'rgb(16 185 129 / 0.7)' }
|
||||
: { color: 'var(--text)', borderColor: 'var(--border-muted)' }
|
||||
}
|
||||
>
|
||||
<span className="flex w-4 justify-center">
|
||||
{contentType === 'ebook' || combinedSelectionActive ? (
|
||||
<CheckIcon />
|
||||
) : (
|
||||
<BookIcon />
|
||||
)}
|
||||
</span>
|
||||
<span>Books</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleContentTypeSelect('audiobook')}
|
||||
className={`flex cursor-pointer items-center gap-2 rounded-xl border px-3 py-2 text-sm font-medium transition-colors ${
|
||||
contentType === 'audiobook' || combinedSelectionActive
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'hover-surface'
|
||||
}`}
|
||||
style={
|
||||
contentType === 'audiobook' || combinedSelectionActive
|
||||
? { borderColor: 'rgb(16 185 129 / 0.7)' }
|
||||
: { color: 'var(--text)', borderColor: 'var(--border-muted)' }
|
||||
}
|
||||
>
|
||||
<span className="flex w-4 justify-center">
|
||||
{contentType === 'audiobook' || combinedSelectionActive ? (
|
||||
<CheckIcon />
|
||||
) : (
|
||||
<AudiobookIcon />
|
||||
)}
|
||||
</span>
|
||||
<span>Audiobooks</span>
|
||||
</button>
|
||||
{combinedToggleAvailable && (
|
||||
<div className="col-span-2">
|
||||
<Tooltip
|
||||
content="Combined search"
|
||||
position="bottom"
|
||||
triggerClassName="w-full"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCombinedModeSelect}
|
||||
className="group w-full cursor-pointer"
|
||||
aria-label="Combined search"
|
||||
>
|
||||
<div className="relative flex h-7 items-end">
|
||||
<div
|
||||
className={`absolute top-1.5 bottom-[11px] left-[25%] w-px transition-colors ${combinedLineColor}`}
|
||||
/>
|
||||
<div
|
||||
className={`absolute top-1.5 right-[25%] bottom-[11px] w-px transition-colors ${combinedLineColor}`}
|
||||
/>
|
||||
<div
|
||||
className={`absolute bottom-[11px] left-[25%] h-px transition-colors ${combinedLineColor}`}
|
||||
style={{ width: 'calc(25% - 16px)' }}
|
||||
/>
|
||||
<div
|
||||
className={`absolute right-[25%] bottom-[11px] h-px transition-colors ${combinedLineColor}`}
|
||||
style={{ width: 'calc(25% - 16px)' }}
|
||||
/>
|
||||
<div
|
||||
className={`relative z-10 mx-auto rounded-full p-1 transition-colors ${
|
||||
combinedSelectionActive
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-(--bg) text-zinc-400 group-hover:bg-zinc-200 group-hover:text-zinc-600 dark:text-zinc-500 dark:group-hover:bg-zinc-700 dark:group-hover:text-zinc-300'
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{combinedSelectorLocked ? (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"
|
||||
/>
|
||||
) : (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{queryTargets.length > 1 && (
|
||||
<div className="shrink-0">
|
||||
<div className="flex items-center justify-between pb-1.5">
|
||||
<span className="text-xs font-medium tracking-wide uppercase opacity-60">
|
||||
Search By
|
||||
</span>
|
||||
{!showContentTypeSelector && onAdvancedToggle && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdvancedToggle}
|
||||
className={`-mt-1.5 -mr-1 -mb-0.5 flex cursor-pointer items-center gap-1.5 rounded-xl px-4 py-2.5 text-xs font-medium transition-colors ${
|
||||
isAdvancedActive
|
||||
? `${searchMode === 'direct' ? 'bg-sky-700' : 'bg-emerald-600'} text-white`
|
||||
: 'hover-surface'
|
||||
}`}
|
||||
style={
|
||||
isAdvancedActive
|
||||
? {
|
||||
borderColor:
|
||||
searchMode === 'direct'
|
||||
? 'rgb(3 105 161 / 0.7)'
|
||||
: 'rgb(16 185 129 / 0.7)',
|
||||
}
|
||||
: { color: 'var(--text-muted)' }
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"
|
||||
/>
|
||||
</svg>
|
||||
Options
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{queryTargets.map((target) => {
|
||||
const isActive = target.key === activeTarget?.key;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={target.key}
|
||||
onClick={() => handleQueryTargetSelect(target.key)}
|
||||
title={target.description || target.label}
|
||||
aria-label={target.label}
|
||||
className={`flex min-w-0 cursor-pointer items-center gap-2 rounded-xl border px-3 py-2 text-sm font-medium transition-colors ${
|
||||
isActive
|
||||
? `${searchMode === 'direct' ? 'bg-sky-700' : 'bg-emerald-600'} text-white`
|
||||
: 'hover-surface'
|
||||
}`}
|
||||
style={
|
||||
isActive
|
||||
? {
|
||||
borderColor:
|
||||
searchMode === 'direct'
|
||||
? 'rgb(3 105 161 / 0.7)'
|
||||
: 'rgb(16 185 129 / 0.7)',
|
||||
}
|
||||
: { color: 'var(--text)', borderColor: 'var(--border-muted)' }
|
||||
}
|
||||
>
|
||||
{isActive && <CheckIcon />}
|
||||
<span className="block truncate">{target.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showQueryTargetSelector && !isPopover && isOptionsOpen && optionsPanel}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -113,6 +113,7 @@ export const SearchSection = ({
|
||||
activeQueryTarget={activeQueryTarget}
|
||||
onQueryTargetChange={onQueryTargetChange}
|
||||
activeQueryField={activeQueryField}
|
||||
optionsLayout="inline"
|
||||
/>
|
||||
{activeQueryTarget === 'manual' && (
|
||||
<p className="px-2 text-xs opacity-50">
|
||||
|
||||
Reference in new issue
Block a user