rizzo-css 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +12 -0
- package/LICENSE +21 -0
- package/README.md +17 -3
- package/bin/rizzo-css.js +93 -43
- package/package.json +5 -3
- package/scaffold/astro-app/README.md +13 -2
- package/scaffold/astro-app/src/components/Accordion.astro +178 -0
- package/scaffold/astro-app/src/components/Alert.astro +131 -0
- package/scaffold/astro-app/src/components/Avatar.astro +59 -0
- package/scaffold/astro-app/src/components/Badge.astro +24 -0
- package/scaffold/astro-app/src/components/Breadcrumb.astro +61 -0
- package/scaffold/astro-app/src/components/Button.astro +3 -0
- package/scaffold/astro-app/src/components/Card.astro +18 -0
- package/scaffold/astro-app/src/components/Checkbox.astro +38 -0
- package/scaffold/astro-app/src/components/CodeBlock.astro +393 -0
- package/scaffold/astro-app/src/components/CopyToClipboard.astro +219 -0
- package/scaffold/astro-app/src/components/Divider.astro +37 -0
- package/scaffold/astro-app/src/components/Dropdown.astro +807 -0
- package/scaffold/astro-app/src/components/FormGroup.astro +59 -0
- package/scaffold/astro-app/src/components/FrameworkSwitcher.astro +72 -0
- package/scaffold/astro-app/src/components/Input.astro +59 -0
- package/scaffold/astro-app/src/components/Modal.astro +212 -0
- package/scaffold/astro-app/src/components/Navbar.astro +701 -0
- package/scaffold/astro-app/src/components/Pagination.astro +240 -0
- package/scaffold/astro-app/src/components/ProgressBar.astro +65 -0
- package/scaffold/astro-app/src/components/Radio.astro +38 -0
- package/scaffold/astro-app/src/components/Search.astro +1259 -0
- package/scaffold/astro-app/src/components/Select.astro +49 -0
- package/scaffold/astro-app/src/components/Settings.astro +382 -0
- package/scaffold/astro-app/src/components/Spinner.astro +30 -0
- package/scaffold/astro-app/src/components/Table.astro +181 -0
- package/scaffold/astro-app/src/components/Tabs.astro +223 -0
- package/scaffold/astro-app/src/components/Textarea.astro +58 -0
- package/scaffold/astro-app/src/components/ThemeSwitcher.astro +504 -0
- package/scaffold/astro-app/src/components/Toast.astro +30 -0
- package/scaffold/astro-app/src/components/Tooltip.astro +32 -0
- package/scaffold/astro-app/src/components/icons/Brush.astro +10 -0
- package/scaffold/astro-app/src/components/icons/Cake.astro +11 -0
- package/scaffold/astro-app/src/components/icons/Check.astro +29 -0
- package/scaffold/astro-app/src/components/icons/Cherry.astro +11 -0
- package/scaffold/astro-app/src/components/icons/ChevronDown.astro +29 -0
- package/scaffold/astro-app/src/components/icons/Circle.astro +29 -0
- package/scaffold/astro-app/src/components/icons/Close.astro +30 -0
- package/scaffold/astro-app/src/components/icons/Cmd.astro +26 -0
- package/scaffold/astro-app/src/components/icons/Copy.astro +30 -0
- package/scaffold/astro-app/src/components/icons/Eye.astro +30 -0
- package/scaffold/astro-app/src/components/icons/Filter.astro +29 -0
- package/scaffold/astro-app/src/components/icons/Flame.astro +28 -0
- package/scaffold/astro-app/src/components/icons/Flower.astro +11 -0
- package/scaffold/astro-app/src/components/icons/Gear.astro +30 -0
- package/scaffold/astro-app/src/components/icons/Heart.astro +28 -0
- package/scaffold/astro-app/src/components/icons/IceCream.astro +31 -0
- package/scaffold/astro-app/src/components/icons/Leaf.astro +29 -0
- package/scaffold/astro-app/src/components/icons/Lemon.astro +11 -0
- package/scaffold/astro-app/src/components/icons/Moon.astro +29 -0
- package/scaffold/astro-app/src/components/icons/Owl.astro +34 -0
- package/scaffold/astro-app/src/components/icons/Palette.astro +33 -0
- package/scaffold/astro-app/src/components/icons/Rainbow.astro +31 -0
- package/scaffold/astro-app/src/components/icons/Search.astro +30 -0
- package/scaffold/astro-app/src/components/icons/Shield.astro +28 -0
- package/scaffold/astro-app/src/components/icons/Snowflake.astro +34 -0
- package/scaffold/astro-app/src/components/icons/Sort.astro +30 -0
- package/scaffold/astro-app/src/components/icons/Sun.astro +29 -0
- package/scaffold/astro-app/src/components/icons/Sunset.astro +10 -0
- package/scaffold/astro-app/src/components/icons/Zap.astro +9 -0
- package/scaffold/astro-app/src/components/icons/devicons/Astro.astro +53 -0
- package/scaffold/astro-app/src/components/icons/devicons/Bash.astro +34 -0
- package/scaffold/astro-app/src/components/icons/devicons/Css3.astro +29 -0
- package/scaffold/astro-app/src/components/icons/devicons/Git.astro +24 -0
- package/scaffold/astro-app/src/components/icons/devicons/Html5.astro +27 -0
- package/scaffold/astro-app/src/components/icons/devicons/Javascript.astro +25 -0
- package/scaffold/astro-app/src/components/icons/devicons/Nodejs.astro +47 -0
- package/scaffold/astro-app/src/components/icons/devicons/Plaintext.astro +33 -0
- package/scaffold/astro-app/src/components/icons/devicons/React.astro +27 -0
- package/scaffold/astro-app/src/components/icons/devicons/Svelte.astro +25 -0
- package/scaffold/astro-app/src/components/icons/devicons/Vue.astro +26 -0
- package/scaffold/astro-app/src/config/frameworks.ts +26 -0
- package/scaffold/astro-app/src/config/themes.ts +54 -0
- package/scaffold/astro-app/src/layouts/DocsLayout.astro +204 -0
- package/scaffold/astro-app/src/layouts/Layout.astro +11 -2
- package/scaffold/astro-app/src/pages/components/accordion.astro +172 -0
- package/scaffold/astro-app/src/pages/components/alert.astro +250 -0
- package/scaffold/astro-app/src/pages/components/avatar.astro +102 -0
- package/scaffold/astro-app/src/pages/components/badge.astro +119 -0
- package/scaffold/astro-app/src/pages/components/breadcrumb.astro +124 -0
- package/scaffold/astro-app/src/pages/components/button.astro +74 -0
- package/scaffold/astro-app/src/pages/components/cards.astro +247 -0
- package/scaffold/astro-app/src/pages/components/copy-to-clipboard.astro +49 -0
- package/scaffold/astro-app/src/pages/components/divider.astro +74 -0
- package/scaffold/astro-app/src/pages/components/dropdown.astro +394 -0
- package/scaffold/astro-app/src/pages/components/forms.astro +367 -0
- package/scaffold/astro-app/src/pages/components/icons.astro +246 -0
- package/scaffold/astro-app/src/pages/components/modal.astro +152 -0
- package/scaffold/astro-app/src/pages/components/navbar.astro +80 -0
- package/scaffold/astro-app/src/pages/components/pagination.astro +126 -0
- package/scaffold/astro-app/src/pages/components/progress-bar.astro +94 -0
- package/scaffold/astro-app/src/pages/components/search.astro +155 -0
- package/scaffold/astro-app/src/pages/components/settings.astro +78 -0
- package/scaffold/astro-app/src/pages/components/spinner.astro +81 -0
- package/scaffold/astro-app/src/pages/components/table.astro +144 -0
- package/scaffold/astro-app/src/pages/components/tabs.astro +220 -0
- package/scaffold/astro-app/src/pages/components/theme-switcher.astro +67 -0
- package/scaffold/astro-app/src/pages/components/toast.astro +157 -0
- package/scaffold/astro-app/src/pages/components/tooltip.astro +209 -0
- package/scaffold/astro-app/src/pages/components.astro +290 -0
- package/scaffold/astro-app/src/pages/docs/accessibility.astro +9 -0
- package/scaffold/astro-app/src/pages/docs/colors.astro +9 -0
- package/scaffold/astro-app/src/pages/docs/design-system.astro +9 -0
- package/scaffold/astro-app/src/pages/docs/getting-started.astro +9 -0
- package/scaffold/astro-app/src/pages/docs/index.astro +15 -0
- package/scaffold/astro-app/src/pages/docs/themes/[theme].astro +14 -0
- package/scaffold/astro-app/src/pages/docs/theming.astro +10 -0
- package/scaffold/astro-app/src/pages/index.astro +5 -11
- package/scaffold/svelte-app/README.md +9 -2
- package/scaffold/svelte-app/src/app.html +1 -1
- package/scaffold/svelte-app/src/lib/rizzo/Accordion.svelte +128 -0
- package/scaffold/svelte-app/src/lib/rizzo/Alert.svelte +85 -0
- package/scaffold/svelte-app/src/lib/rizzo/Avatar.svelte +39 -0
- package/scaffold/svelte-app/src/lib/rizzo/Badge.svelte +31 -0
- package/scaffold/svelte-app/src/lib/rizzo/Breadcrumb.svelte +49 -0
- package/scaffold/svelte-app/src/lib/rizzo/Button.svelte +27 -0
- package/scaffold/svelte-app/src/lib/rizzo/Card.svelte +17 -0
- package/scaffold/svelte-app/src/lib/rizzo/Checkbox.svelte +37 -0
- package/scaffold/svelte-app/src/lib/rizzo/CopyToClipboard.svelte +79 -0
- package/scaffold/svelte-app/src/lib/rizzo/Divider.svelte +28 -0
- package/scaffold/svelte-app/src/lib/rizzo/Dropdown.svelte +254 -0
- package/scaffold/svelte-app/src/lib/rizzo/FormGroup.svelte +51 -0
- package/scaffold/svelte-app/src/lib/rizzo/Input.svelte +59 -0
- package/scaffold/svelte-app/src/lib/rizzo/Modal.svelte +157 -0
- package/scaffold/svelte-app/src/lib/rizzo/Pagination.svelte +93 -0
- package/scaffold/svelte-app/src/lib/rizzo/ProgressBar.svelte +58 -0
- package/scaffold/svelte-app/src/lib/rizzo/Radio.svelte +38 -0
- package/scaffold/svelte-app/src/lib/rizzo/Select.svelte +51 -0
- package/scaffold/svelte-app/src/lib/rizzo/Spinner.svelte +14 -0
- package/scaffold/svelte-app/src/lib/rizzo/Table.svelte +158 -0
- package/scaffold/svelte-app/src/lib/rizzo/Tabs.svelte +117 -0
- package/scaffold/svelte-app/src/lib/rizzo/Textarea.svelte +59 -0
- package/scaffold/svelte-app/src/lib/rizzo/ThemeSwitcher.svelte +315 -0
- package/scaffold/svelte-app/src/lib/rizzo/Toast.svelte +33 -0
- package/scaffold/svelte-app/src/lib/rizzo/Tooltip.svelte +19 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Check.svelte +29 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/ChevronDown.svelte +29 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Circle.svelte +29 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Close.svelte +30 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Cmd.svelte +27 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Copy.svelte +30 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Eye.svelte +30 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Filter.svelte +29 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Gear.svelte +30 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/IceCream.svelte +31 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Moon.svelte +29 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Owl.svelte +34 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Palette.svelte +33 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Rainbow.svelte +31 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Search.svelte +30 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Snowflake.svelte +34 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/Sort.svelte +30 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Astro.svelte +45 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Bash.svelte +28 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Css3.svelte +23 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Git.svelte +18 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Html5.svelte +21 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Javascript.svelte +19 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Nodejs.svelte +44 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Plaintext.svelte +24 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/React.svelte +21 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/SvelteIcon.svelte +19 -0
- package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Vue.svelte +20 -0
- package/scaffold/svelte-app/src/lib/rizzo/index.ts +33 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/CodeBlock.svelte +239 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/SvelteDocPage.svelte +99 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/AccordionDoc.svelte +53 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/AlertDoc.svelte +114 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/AvatarDoc.svelte +92 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/BadgeDoc.svelte +60 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/BreadcrumbDoc.svelte +55 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ButtonDoc.svelte +55 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/CardsDoc.svelte +173 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ComingSoon.svelte +12 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ComponentsOverview.svelte +92 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/CopyToClipboardDoc.svelte +26 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/DividerDoc.svelte +105 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/DropdownDoc.svelte +161 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/FormsDoc.svelte +375 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/IconsDoc.svelte +246 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/Index.svelte +8 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ModalDoc.svelte +50 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/NavbarDoc.svelte +79 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/PaginationDoc.svelte +44 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ProgressBarDoc.svelte +95 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/SearchDoc.svelte +147 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/SettingsDoc.svelte +158 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/SpinnerDoc.svelte +41 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/TableDoc.svelte +116 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/TabsDoc.svelte +152 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ThemeSwitcherDoc.svelte +181 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/Theming.svelte +6 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ToastDoc.svelte +136 -0
- package/scaffold/svelte-app/src/lib/rizzo-docs/pages/TooltipDoc.svelte +57 -0
- package/scaffold/svelte-app/src/routes/+page.svelte +2 -2
- package/scaffold/svelte-app/src/routes/components/+page.svelte +4 -0
- package/scaffold/svelte-app/src/routes/components/[slug]/+page.svelte +7 -0
- package/scaffold/vanilla/README.md +11 -4
- package/scaffold/vanilla/components/accordion.html +187 -0
- package/scaffold/vanilla/components/alert.html +187 -0
- package/scaffold/vanilla/components/avatar.html +187 -0
- package/scaffold/vanilla/components/badge.html +187 -0
- package/scaffold/vanilla/components/breadcrumb.html +187 -0
- package/scaffold/vanilla/components/button.html +187 -0
- package/scaffold/vanilla/components/cards.html +187 -0
- package/scaffold/vanilla/components/copy-to-clipboard.html +187 -0
- package/scaffold/vanilla/components/divider.html +187 -0
- package/scaffold/vanilla/components/dropdown.html +187 -0
- package/scaffold/vanilla/components/forms.html +187 -0
- package/scaffold/vanilla/components/icons.html +187 -0
- package/scaffold/vanilla/components/index.html +212 -0
- package/scaffold/vanilla/components/modal.html +187 -0
- package/scaffold/vanilla/components/navbar.html +187 -0
- package/scaffold/vanilla/components/pagination.html +187 -0
- package/scaffold/vanilla/components/progress-bar.html +187 -0
- package/scaffold/vanilla/components/search.html +187 -0
- package/scaffold/vanilla/components/settings.html +187 -0
- package/scaffold/vanilla/components/spinner.html +187 -0
- package/scaffold/vanilla/components/table.html +187 -0
- package/scaffold/vanilla/components/tabs.html +187 -0
- package/scaffold/vanilla/components/theme-switcher.html +187 -0
- package/scaffold/vanilla/components/toast.html +187 -0
- package/scaffold/vanilla/components/tooltip.html +187 -0
- package/scaffold/vanilla/index.html +16 -6
- package/scaffold/vanilla/js/main.js +4 -3
|
@@ -0,0 +1,1259 @@
|
|
|
1
|
+
---
|
|
2
|
+
import SearchIcon from './icons/Search.astro';
|
|
3
|
+
import Cmd from './icons/Cmd.astro';
|
|
4
|
+
import Close from './icons/Close.astro';
|
|
5
|
+
import { getSearchConfig } from '../config/search';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
algoliaAppId?: string;
|
|
9
|
+
algoliaApiKey?: string;
|
|
10
|
+
algoliaIndexName?: string;
|
|
11
|
+
useAlgolia?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Get default config from environment variables
|
|
15
|
+
const envConfig = getSearchConfig();
|
|
16
|
+
|
|
17
|
+
// Allow props to override environment variables
|
|
18
|
+
const {
|
|
19
|
+
algoliaAppId = envConfig.algoliaAppId,
|
|
20
|
+
algoliaApiKey = envConfig.algoliaApiKey,
|
|
21
|
+
algoliaIndexName = envConfig.algoliaIndexName,
|
|
22
|
+
useAlgolia = envConfig.useAlgolia,
|
|
23
|
+
} = Astro.props;
|
|
24
|
+
|
|
25
|
+
const searchId = `search-${Math.random().toString(36).substr(2, 9)}`;
|
|
26
|
+
const resultsId = `${searchId}-results`;
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<div class="search" data-search>
|
|
30
|
+
<div class="search__trigger-wrapper">
|
|
31
|
+
<div class="tooltip-wrapper" aria-describedby={`${searchId}-tooltip`}>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
class="search__trigger"
|
|
35
|
+
aria-label="Open search"
|
|
36
|
+
aria-expanded="false"
|
|
37
|
+
aria-controls={resultsId}
|
|
38
|
+
data-search-trigger
|
|
39
|
+
>
|
|
40
|
+
<SearchIcon width={20} height={20} class="search__icon" />
|
|
41
|
+
<span class="search__trigger-text">Search</span>
|
|
42
|
+
<kbd class="search__kbd" aria-hidden="true">
|
|
43
|
+
<span class="search__kbd-modifier" data-kbd-modifier><Cmd width={14} height={14} /></span>
|
|
44
|
+
<kbd>K</kbd>
|
|
45
|
+
</kbd>
|
|
46
|
+
</button>
|
|
47
|
+
<span class="tooltip tooltip--bottom" id={`${searchId}-tooltip`} role="tooltip" aria-hidden="true">Search</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="search__overlay" data-search-overlay aria-hidden="true">
|
|
52
|
+
<div class="search__panel" role="dialog" aria-modal="true" aria-labelledby={`${searchId}-title`} aria-hidden="true" id={resultsId} tabindex="-1" style="outline: none;">
|
|
53
|
+
<div class="search__header">
|
|
54
|
+
<h2 id={`${searchId}-title`} class="sr-only">Search documentation</h2>
|
|
55
|
+
<div class="search__input-wrapper">
|
|
56
|
+
<SearchIcon width={20} height={20} class="search__input-icon" aria-hidden="true" />
|
|
57
|
+
<input
|
|
58
|
+
type="search"
|
|
59
|
+
class="search__input"
|
|
60
|
+
id={`${searchId}-input`}
|
|
61
|
+
placeholder="Search documentation..."
|
|
62
|
+
autocomplete="off"
|
|
63
|
+
aria-label="Search documentation"
|
|
64
|
+
aria-controls={`${resultsId}-list`}
|
|
65
|
+
aria-autocomplete="list"
|
|
66
|
+
aria-activedescendant=""
|
|
67
|
+
role="searchbox"
|
|
68
|
+
tabindex="0"
|
|
69
|
+
data-search-input
|
|
70
|
+
/>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
class="search__clear"
|
|
74
|
+
aria-label="Clear search"
|
|
75
|
+
data-search-clear
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
tabindex="-1"
|
|
78
|
+
>
|
|
79
|
+
<Close width={18} height={18} />
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
class="search__close-btn"
|
|
85
|
+
aria-label="Close search"
|
|
86
|
+
data-search-close
|
|
87
|
+
>
|
|
88
|
+
<Close width={20} height={20} aria-hidden="true" />
|
|
89
|
+
<span class="search__close-text sr-only">Close</span>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="search__results" id={`${resultsId}-list`} role="listbox" aria-label="Search results" aria-live="polite" aria-atomic="true">
|
|
94
|
+
<div class="search__empty" data-search-empty hidden>
|
|
95
|
+
<p class="search__empty-text">Start typing to search documentation...</p>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="search__results-list" data-search-results role="group" aria-label="Search results list"></div>
|
|
98
|
+
<div class="search__loading" data-search-loading hidden>
|
|
99
|
+
<p class="search__loading-text" role="status" aria-live="polite">Searching...</p>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="search__no-results" data-search-no-results hidden>
|
|
102
|
+
<p class="search__no-results-text" role="status" aria-live="polite">No results found</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<script define:vars={{ useAlgolia, algoliaAppId, algoliaApiKey, algoliaIndexName }}>
|
|
110
|
+
(function initSearch() {
|
|
111
|
+
const init = () => {
|
|
112
|
+
document.querySelectorAll('[data-search]').forEach((search) => {
|
|
113
|
+
if (search.__searchInstance) return;
|
|
114
|
+
|
|
115
|
+
const trigger = search.querySelector('[data-search-trigger]');
|
|
116
|
+
const overlay = search.querySelector('[data-search-overlay]');
|
|
117
|
+
const panel = search.querySelector('.search__panel');
|
|
118
|
+
const input = search.querySelector('[data-search-input]');
|
|
119
|
+
const clearBtn = search.querySelector('[data-search-clear]');
|
|
120
|
+
const closeBtn = search.querySelector('[data-search-close]');
|
|
121
|
+
const emptyState = search.querySelector('[data-search-empty]');
|
|
122
|
+
const resultsList = search.querySelector('[data-search-results]');
|
|
123
|
+
const loadingState = search.querySelector('[data-search-loading]');
|
|
124
|
+
const noResultsState = search.querySelector('[data-search-no-results]');
|
|
125
|
+
|
|
126
|
+
if (!trigger || !overlay || !panel || !input) {
|
|
127
|
+
console.error('Search: Missing required elements');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!closeBtn) {
|
|
132
|
+
console.error('Search: Missing close button');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!emptyState || !resultsList || !loadingState || !noResultsState) {
|
|
136
|
+
console.error('Search: Missing result state elements', {
|
|
137
|
+
emptyState: !!emptyState,
|
|
138
|
+
resultsList: !!resultsList,
|
|
139
|
+
loadingState: !!loadingState,
|
|
140
|
+
noResultsState: !!noResultsState
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Extract searchId from input ID (format: searchId-input)
|
|
145
|
+
const inputId = input.id || '';
|
|
146
|
+
const searchId = inputId.replace('-input', '') || `search-${Math.random().toString(36).substr(2, 9)}`;
|
|
147
|
+
|
|
148
|
+
// Log search configuration (commented for production)
|
|
149
|
+
// console.log('🔍 Search initialized with searchId:', searchId);
|
|
150
|
+
if (useAlgolia && algoliaAppId && algoliaApiKey) {
|
|
151
|
+
// console.log('✅ Algolia search enabled');
|
|
152
|
+
// console.log(' App ID:', algoliaAppId.substring(0, 8) + '...');
|
|
153
|
+
// console.log(' Index:', algoliaIndexName);
|
|
154
|
+
} else {
|
|
155
|
+
// console.log('ℹ️ Using client-side search (Algolia not configured)');
|
|
156
|
+
if (useAlgolia) {
|
|
157
|
+
console.warn(' ⚠️ Algolia enabled but credentials missing');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let previousActiveElement = null;
|
|
162
|
+
let searchTimeout = null;
|
|
163
|
+
let currentResults = [];
|
|
164
|
+
let selectedIndex = -1;
|
|
165
|
+
let focusableElements = [];
|
|
166
|
+
let firstFocusableElement = null;
|
|
167
|
+
let lastFocusableElement = null;
|
|
168
|
+
|
|
169
|
+
// Search index (client-side fallback)
|
|
170
|
+
const searchIndex = [
|
|
171
|
+
{ title: 'Getting Started', url: '/docs/getting-started', category: 'Documentation', content: 'Installation, project structure, and quick start guide' },
|
|
172
|
+
{ title: 'Design System', url: '/docs/design-system', category: 'Documentation', content: 'Core design principles, semantic variables, spacing, typography, and utilities' },
|
|
173
|
+
{ title: 'Theming', url: '/docs/theming', category: 'Documentation', content: 'Theme system documentation and custom theme creation' },
|
|
174
|
+
{ title: 'Colors', url: '/docs/colors', category: 'Documentation', content: 'Color reference with multiple format options (OKLCH, Hex, RGB, HSL, CSS Variable)' },
|
|
175
|
+
{ title: 'Accessibility', url: '/docs/accessibility', category: 'Documentation', content: 'Accessibility guidelines and utility classes' },
|
|
176
|
+
{ title: 'Components Overview', url: '/docs/components', category: 'Components', content: 'Component library with usage examples and live demos' },
|
|
177
|
+
{ title: 'Navbar', url: '/docs/components/navbar', category: 'Components', content: 'Responsive, accessible navigation bar' },
|
|
178
|
+
{ title: 'Settings', url: '/docs/components/settings', category: 'Components', content: 'Comprehensive settings panel for theme switching and accessibility options' },
|
|
179
|
+
{ title: 'Theme Switcher', url: '/docs/components/theme-switcher', category: 'Components', content: 'Accessible theme switcher with theme icons and keyboard navigation' },
|
|
180
|
+
{ title: 'Button', url: '/docs/components/button', category: 'Components', content: 'Semantic button component with variants' },
|
|
181
|
+
{ title: 'Badge', url: '/docs/components/badge', category: 'Components', content: 'Small labels and tags for displaying status, categories, or counts with variants, sizes, and pill option' },
|
|
182
|
+
{ title: 'Forms', url: '/docs/components/forms', category: 'Components', content: 'Form components (FormGroup, Input, Textarea, Select, Checkbox, Radio)' },
|
|
183
|
+
{ title: 'Cards', url: '/docs/components/cards', category: 'Components', content: 'Flexible card component with variants, sections, and image support' },
|
|
184
|
+
{ title: 'Modal', url: '/docs/components/modal', category: 'Components', content: 'Accessible modal/dialog component with focus trapping and keyboard navigation' },
|
|
185
|
+
{ title: 'Alert', url: '/docs/components/alert', category: 'Components', content: 'Accessible alert component with variants and dismissible functionality' },
|
|
186
|
+
{ title: 'Toast', url: '/docs/components/toast', category: 'Components', content: 'Fixed position toast notifications with auto-dismiss and programmatic control' },
|
|
187
|
+
{ title: 'Search', url: '/docs/components/search', category: 'Components', content: 'Search component with Algolia integration and live filtering' },
|
|
188
|
+
{ title: 'CopyToClipboard', url: '/docs/components/copy-to-clipboard', category: 'Components', content: 'Copy to clipboard component with visual feedback' },
|
|
189
|
+
{ title: 'Icons', url: '/docs/components/icons', category: 'Components', content: 'Reusable SVG icon components using Tabler Icons and Devicons with interactive card grid and copy functionality' },
|
|
190
|
+
{ title: 'Tooltip', url: '/docs/components/tooltip', category: 'Components', content: 'Accessible tooltip component with four position options (top, bottom, left, right), keyboard support, and theme-aware styling' },
|
|
191
|
+
{ title: 'Dropdown', url: '/docs/components/dropdown', category: 'Components', content: 'Accessible dropdown menu component with keyboard navigation, nested submenus (up to 3 levels), menu items, separators, and custom click handlers' },
|
|
192
|
+
{ title: 'Tabs', url: '/docs/components/tabs', category: 'Components', content: 'Accessible tabs component with keyboard navigation, ARIA tab pattern, and three variants (default, pills, underline)' },
|
|
193
|
+
{ title: 'GitHub Dark Classic', url: '/docs/themes/github-dark-classic', category: 'Themes', content: 'Dark theme - Official GitHub dark theme for VS Code' },
|
|
194
|
+
{ title: 'Shades of Purple', url: '/docs/themes/shades-of-purple', category: 'Themes', content: 'Dark theme - Professional theme with bold purple shades' },
|
|
195
|
+
{ title: 'Hack The Box', url: '/docs/themes/hack-the-box', category: 'Themes', content: 'Dark theme - Dark blue with lime green accent, built for hackers' },
|
|
196
|
+
{ title: 'Pink Cat Boo', url: '/docs/themes/pink-cat-boo', category: 'Themes', content: 'Dark theme - Sweet and cute with rose pink accents' },
|
|
197
|
+
{ title: 'Sandstorm Classic', url: '/docs/themes/sandstorm-classic', category: 'Themes', content: 'Dark theme - Dark red-based theme for late night coding' },
|
|
198
|
+
{ title: 'Rocky Blood Orange', url: '/docs/themes/rocky-blood-orange', category: 'Themes', content: 'Dark theme - Dark theme with blood-orange accent' },
|
|
199
|
+
{ title: 'Minimal Dark Neon Yellow', url: '/docs/themes/minimal-dark-neon-yellow', category: 'Themes', content: 'Dark theme - Minimal dark with neon yellow accent' },
|
|
200
|
+
{ title: 'GitHub Light', url: '/docs/themes/github-light', category: 'Themes', content: 'Light theme - Official GitHub light theme for VS Code' },
|
|
201
|
+
{ title: 'Red Velvet Cupcake', url: '/docs/themes/red-velvet-cupcake', category: 'Themes', content: 'Light theme - Velvet-cupcake theme with red accent' },
|
|
202
|
+
{ title: 'Orangy One Light', url: '/docs/themes/orangy-one-light', category: 'Themes', content: 'Light theme - Light theme with orange accent' },
|
|
203
|
+
{ title: 'Sunflower', url: '/docs/themes/sunflower', category: 'Themes', content: 'Light theme - Yellow light theme' },
|
|
204
|
+
{ title: 'Green Breeze Light', url: '/docs/themes/green-breeze-light', category: 'Themes', content: 'Light theme - Green and blue focused with good contrast' },
|
|
205
|
+
{ title: 'Cute Pink', url: '/docs/themes/cute-pink', category: 'Themes', content: 'Light theme - Cute pink light theme for VSCode' },
|
|
206
|
+
{ title: 'Semi Light Purple', url: '/docs/themes/semi-light-purple', category: 'Themes', content: 'Light theme - Soft purple aesthetic theme by Kapil Yadav' },
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
// Client-side search function
|
|
210
|
+
const performClientSearch = (query) => {
|
|
211
|
+
if (!query || typeof query !== 'string') {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const trimmed = query.trim();
|
|
216
|
+
if (trimmed.length < 1) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const lowerQuery = trimmed.toLowerCase();
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
// Filter search index for matches
|
|
224
|
+
const filtered = searchIndex.filter((item) => {
|
|
225
|
+
if (!item || !item.title) return false;
|
|
226
|
+
|
|
227
|
+
const titleLower = item.title.toLowerCase();
|
|
228
|
+
const contentLower = item.content ? item.content.toLowerCase() : '';
|
|
229
|
+
const categoryLower = item.category ? item.category.toLowerCase() : '';
|
|
230
|
+
|
|
231
|
+
const titleMatch = titleLower.includes(lowerQuery);
|
|
232
|
+
const contentMatch = contentLower.includes(lowerQuery);
|
|
233
|
+
const categoryMatch = categoryLower.includes(lowerQuery);
|
|
234
|
+
|
|
235
|
+
return titleMatch || contentMatch || categoryMatch;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Calculate relevance and sort
|
|
239
|
+
const withRelevance = filtered.map((item) => ({
|
|
240
|
+
...item,
|
|
241
|
+
relevance: calculateRelevance(item, lowerQuery),
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
const sorted = withRelevance.sort((a, b) => b.relevance - a.relevance);
|
|
245
|
+
|
|
246
|
+
return sorted;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error('Client search error:', error);
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Calculate relevance score
|
|
254
|
+
const calculateRelevance = (item, query) => {
|
|
255
|
+
let score = 0;
|
|
256
|
+
const lowerTitle = item.title.toLowerCase();
|
|
257
|
+
const lowerContent = item.content.toLowerCase();
|
|
258
|
+
const lowerCategory = item.category.toLowerCase();
|
|
259
|
+
|
|
260
|
+
// Title matches are most important
|
|
261
|
+
if (lowerTitle.includes(query)) {
|
|
262
|
+
score += 10;
|
|
263
|
+
if (lowerTitle.startsWith(query)) score += 5;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Category matches
|
|
267
|
+
if (lowerCategory.includes(query)) {
|
|
268
|
+
score += 3;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Content matches
|
|
272
|
+
if (lowerContent.includes(query)) {
|
|
273
|
+
score += 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return score;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Algolia search function (if enabled)
|
|
280
|
+
const performAlgoliaSearch = async (query) => {
|
|
281
|
+
if (!useAlgolia || !algoliaAppId || !algoliaApiKey) {
|
|
282
|
+
// console.log('Algolia not configured, using client-side search');
|
|
283
|
+
return performClientSearch(query);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
let algoliasearch;
|
|
288
|
+
let useV5 = false;
|
|
289
|
+
|
|
290
|
+
// Try to use npm package first (v5)
|
|
291
|
+
try {
|
|
292
|
+
const algoliaModule = await import('algoliasearch');
|
|
293
|
+
algoliasearch = algoliaModule.algoliasearch || algoliaModule.default?.algoliasearch;
|
|
294
|
+
|
|
295
|
+
if (algoliasearch && typeof algoliasearch === 'function') {
|
|
296
|
+
useV5 = true;
|
|
297
|
+
// console.log('Using Algolia v5 client');
|
|
298
|
+
} else {
|
|
299
|
+
throw new Error('Algolia v5 not available');
|
|
300
|
+
}
|
|
301
|
+
} catch (npmError) {
|
|
302
|
+
console.warn('Algolia npm package not available, trying CDN fallback:', npmError.message);
|
|
303
|
+
// Fallback to CDN (v4)
|
|
304
|
+
try {
|
|
305
|
+
const algoliaModule = await import('https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js');
|
|
306
|
+
algoliasearch = algoliaModule.default || algoliaModule;
|
|
307
|
+
|
|
308
|
+
if (!algoliasearch || typeof algoliasearch !== 'function') {
|
|
309
|
+
throw new Error('Algolia client not available');
|
|
310
|
+
}
|
|
311
|
+
// console.log('Using Algolia v4 client (CDN)');
|
|
312
|
+
} catch (cdnError) {
|
|
313
|
+
console.error('Failed to load Algolia client from CDN:', cdnError.message);
|
|
314
|
+
throw new Error('Algolia client not available');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const client = algoliasearch(algoliaAppId, algoliaApiKey);
|
|
319
|
+
|
|
320
|
+
let results;
|
|
321
|
+
|
|
322
|
+
if (useV5) {
|
|
323
|
+
// Use v5 API
|
|
324
|
+
// console.log(`Searching Algolia index "${algoliaIndexName}" with query: "${query}"`);
|
|
325
|
+
const response = await client.search({
|
|
326
|
+
requests: [{
|
|
327
|
+
indexName: algoliaIndexName,
|
|
328
|
+
query: query,
|
|
329
|
+
params: {
|
|
330
|
+
hitsPerPage: 10,
|
|
331
|
+
attributesToRetrieve: ['title', 'url', 'category', 'content'],
|
|
332
|
+
attributesToSnippet: ['content:20'],
|
|
333
|
+
},
|
|
334
|
+
}],
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
results = response.results[0]?.hits || [];
|
|
338
|
+
// console.log(`Algolia returned ${results.length} results`);
|
|
339
|
+
} else {
|
|
340
|
+
// Use v4 API (fallback)
|
|
341
|
+
// console.log(`Searching Algolia index "${algoliaIndexName}" with query: "${query}"`);
|
|
342
|
+
const index = client.initIndex(algoliaIndexName);
|
|
343
|
+
const searchResponse = await index.search(query, {
|
|
344
|
+
hitsPerPage: 10,
|
|
345
|
+
attributesToRetrieve: ['title', 'url', 'category', 'content'],
|
|
346
|
+
attributesToSnippet: ['content:20'],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
results = searchResponse.hits || [];
|
|
350
|
+
// console.log(`Algolia returned ${results.length} results`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return results.map((hit) => ({
|
|
354
|
+
title: hit.title || hit.hierarchy?.lvl0 || 'Untitled',
|
|
355
|
+
url: hit.url || hit.objectID,
|
|
356
|
+
category: hit.category || 'Documentation',
|
|
357
|
+
content: hit.content || hit._snippetResult?.content?.value || hit._highlightResult?.content?.value || '',
|
|
358
|
+
}));
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.warn('Algolia search failed, falling back to client-side search:', error);
|
|
361
|
+
console.error('Algolia error details:', {
|
|
362
|
+
message: error.message,
|
|
363
|
+
stack: error.stack,
|
|
364
|
+
useAlgolia,
|
|
365
|
+
hasAppId: !!algoliaAppId,
|
|
366
|
+
hasApiKey: !!algoliaApiKey,
|
|
367
|
+
indexName: algoliaIndexName
|
|
368
|
+
});
|
|
369
|
+
return performClientSearch(query);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Render search results
|
|
374
|
+
const renderResults = (results) => {
|
|
375
|
+
// Always update current results and reset selection
|
|
376
|
+
currentResults = Array.isArray(results) ? results : [];
|
|
377
|
+
selectedIndex = -1;
|
|
378
|
+
|
|
379
|
+
// Hide all states first
|
|
380
|
+
if (emptyState) emptyState.setAttribute('hidden', 'true');
|
|
381
|
+
if (loadingState) loadingState.setAttribute('hidden', 'true');
|
|
382
|
+
if (noResultsState) noResultsState.setAttribute('hidden', 'true');
|
|
383
|
+
|
|
384
|
+
// Handle empty results
|
|
385
|
+
if (!results || !Array.isArray(results) || results.length === 0) {
|
|
386
|
+
if (resultsList) {
|
|
387
|
+
resultsList.setAttribute('hidden', 'true');
|
|
388
|
+
resultsList.classList.add('is-hidden');
|
|
389
|
+
resultsList.style.display = 'none';
|
|
390
|
+
}
|
|
391
|
+
if (noResultsState) noResultsState.removeAttribute('hidden');
|
|
392
|
+
if (input) input.setAttribute('aria-activedescendant', '');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Show results
|
|
397
|
+
if (!resultsList) {
|
|
398
|
+
console.error('Results list element not found!');
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Build HTML for results with proper escaping
|
|
403
|
+
const resultsHTML = results.map((result, index) => {
|
|
404
|
+
if (!result || !result.title) {
|
|
405
|
+
return '';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const resultId = `${searchId}-result-${index}`;
|
|
409
|
+
const escapedTitle = String(result.title || 'Untitled')
|
|
410
|
+
.replace(/&/g, '&')
|
|
411
|
+
.replace(/</g, '<')
|
|
412
|
+
.replace(/>/g, '>')
|
|
413
|
+
.replace(/"/g, '"');
|
|
414
|
+
const escapedCategory = String(result.category || '')
|
|
415
|
+
.replace(/&/g, '&')
|
|
416
|
+
.replace(/</g, '<')
|
|
417
|
+
.replace(/>/g, '>')
|
|
418
|
+
.replace(/"/g, '"');
|
|
419
|
+
const escapedContent = result.content
|
|
420
|
+
? String(result.content)
|
|
421
|
+
.replace(/&/g, '&')
|
|
422
|
+
.replace(/</g, '<')
|
|
423
|
+
.replace(/>/g, '>')
|
|
424
|
+
.replace(/"/g, '"')
|
|
425
|
+
.substring(0, 150)
|
|
426
|
+
: '';
|
|
427
|
+
const escapedUrl = result.url
|
|
428
|
+
? String(result.url).replace(/"/g, '"')
|
|
429
|
+
: '#';
|
|
430
|
+
|
|
431
|
+
return `
|
|
432
|
+
<div
|
|
433
|
+
class="search__result-item"
|
|
434
|
+
data-search-result
|
|
435
|
+
data-result-index="${index}"
|
|
436
|
+
data-url="${escapedUrl}"
|
|
437
|
+
id="${resultId}"
|
|
438
|
+
role="option"
|
|
439
|
+
aria-selected="false"
|
|
440
|
+
tabindex="-1"
|
|
441
|
+
>
|
|
442
|
+
<div class="search__result-category">${escapedCategory}</div>
|
|
443
|
+
<div class="search__result-title">${escapedTitle}</div>
|
|
444
|
+
${escapedContent ? `<div class="search__result-content">${escapedContent}</div>` : ''}
|
|
445
|
+
</div>
|
|
446
|
+
`;
|
|
447
|
+
}).filter(Boolean).join('');
|
|
448
|
+
|
|
449
|
+
// CRITICAL: Remove hidden state BEFORE setting innerHTML
|
|
450
|
+
resultsList.removeAttribute('hidden');
|
|
451
|
+
resultsList.classList.remove('is-hidden');
|
|
452
|
+
|
|
453
|
+
// Set innerHTML
|
|
454
|
+
resultsList.innerHTML = resultsHTML;
|
|
455
|
+
|
|
456
|
+
// CRITICAL: Force visibility with multiple methods
|
|
457
|
+
resultsList.removeAttribute('hidden');
|
|
458
|
+
resultsList.classList.remove('is-hidden');
|
|
459
|
+
resultsList.style.display = 'flex';
|
|
460
|
+
resultsList.style.visibility = 'visible';
|
|
461
|
+
resultsList.style.opacity = '1';
|
|
462
|
+
resultsList.style.minHeight = '200px';
|
|
463
|
+
resultsList.style.position = 'relative';
|
|
464
|
+
resultsList.style.left = '0';
|
|
465
|
+
resultsList.style.width = '100%';
|
|
466
|
+
|
|
467
|
+
// Force a reflow to ensure styles are applied
|
|
468
|
+
void resultsList.offsetHeight;
|
|
469
|
+
|
|
470
|
+
// Double-check and force removal again after reflow
|
|
471
|
+
if (resultsList.hasAttribute('hidden')) {
|
|
472
|
+
resultsList.removeAttribute('hidden');
|
|
473
|
+
}
|
|
474
|
+
if (resultsList.classList.contains('is-hidden')) {
|
|
475
|
+
resultsList.classList.remove('is-hidden');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Use requestAnimationFrame to ensure visibility after DOM updates
|
|
479
|
+
requestAnimationFrame(() => {
|
|
480
|
+
resultsList.removeAttribute('hidden');
|
|
481
|
+
resultsList.classList.remove('is-hidden');
|
|
482
|
+
resultsList.style.display = 'flex';
|
|
483
|
+
resultsList.style.visibility = 'visible';
|
|
484
|
+
resultsList.style.opacity = '1';
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Ensure parent container (search__results) is visible
|
|
488
|
+
const resultsContainer = resultsList.parentElement;
|
|
489
|
+
if (resultsContainer) {
|
|
490
|
+
// CRITICAL: Fix positioning - reset any negative left values
|
|
491
|
+
resultsContainer.style.position = 'relative';
|
|
492
|
+
resultsContainer.style.left = '0';
|
|
493
|
+
resultsContainer.style.right = 'auto';
|
|
494
|
+
resultsContainer.style.width = '100%';
|
|
495
|
+
resultsContainer.style.display = 'flex';
|
|
496
|
+
resultsContainer.style.visibility = 'visible';
|
|
497
|
+
resultsContainer.style.opacity = '1';
|
|
498
|
+
resultsContainer.style.minHeight = '300px';
|
|
499
|
+
resultsContainer.style.height = 'auto';
|
|
500
|
+
resultsContainer.removeAttribute('hidden');
|
|
501
|
+
|
|
502
|
+
// Also ensure the panel itself has proper height
|
|
503
|
+
const panel = resultsContainer.closest('.search__panel');
|
|
504
|
+
if (panel) {
|
|
505
|
+
panel.style.minHeight = '400px';
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Also fix results list positioning
|
|
510
|
+
resultsList.style.position = 'relative';
|
|
511
|
+
resultsList.style.left = '0';
|
|
512
|
+
resultsList.style.right = 'auto';
|
|
513
|
+
resultsList.style.width = '100%';
|
|
514
|
+
|
|
515
|
+
// Force each result item to be visible and add event handlers
|
|
516
|
+
const resultItems = resultsList.querySelectorAll('[data-search-result]');
|
|
517
|
+
// console.log('Found result items:', resultItems.length);
|
|
518
|
+
|
|
519
|
+
resultItems.forEach((item, index) => {
|
|
520
|
+
if (index >= results.length) return;
|
|
521
|
+
|
|
522
|
+
// Force visibility with explicit styles
|
|
523
|
+
item.style.display = 'block';
|
|
524
|
+
item.style.visibility = 'visible';
|
|
525
|
+
item.style.opacity = '1';
|
|
526
|
+
item.style.position = 'relative';
|
|
527
|
+
item.style.zIndex = String(10 + index);
|
|
528
|
+
item.style.width = '100%';
|
|
529
|
+
|
|
530
|
+
// Also ensure child elements are visible
|
|
531
|
+
const title = item.querySelector('.search__result-title');
|
|
532
|
+
const category = item.querySelector('.search__result-category');
|
|
533
|
+
const content = item.querySelector('.search__result-content');
|
|
534
|
+
|
|
535
|
+
if (title) {
|
|
536
|
+
title.style.display = 'block';
|
|
537
|
+
title.style.visibility = 'visible';
|
|
538
|
+
title.style.color = 'var(--text)';
|
|
539
|
+
title.style.fontSize = 'var(--font-size-base)';
|
|
540
|
+
title.style.fontWeight = 'var(--font-weight-medium)';
|
|
541
|
+
}
|
|
542
|
+
if (category) {
|
|
543
|
+
category.style.display = 'block';
|
|
544
|
+
category.style.visibility = 'visible';
|
|
545
|
+
category.style.color = 'var(--text-dim)';
|
|
546
|
+
category.style.fontSize = 'var(--font-size-xs)';
|
|
547
|
+
}
|
|
548
|
+
if (content) {
|
|
549
|
+
content.style.display = 'block';
|
|
550
|
+
content.style.visibility = 'visible';
|
|
551
|
+
content.style.color = 'var(--text-dim)';
|
|
552
|
+
content.style.fontSize = 'var(--font-size-sm)';
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Verify first item is visible
|
|
556
|
+
if (index === 0) {
|
|
557
|
+
const itemStyle = window.getComputedStyle(item);
|
|
558
|
+
// console.log('First item computed styles:', {
|
|
559
|
+
// display: itemStyle.display,
|
|
560
|
+
// visibility: itemStyle.visibility,
|
|
561
|
+
// opacity: itemStyle.opacity,
|
|
562
|
+
// height: itemStyle.height,
|
|
563
|
+
// width: itemStyle.width,
|
|
564
|
+
// backgroundColor: itemStyle.backgroundColor,
|
|
565
|
+
// color: itemStyle.color
|
|
566
|
+
// });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Add event handlers
|
|
570
|
+
item.addEventListener('click', (e) => {
|
|
571
|
+
e.preventDefault();
|
|
572
|
+
navigateToResult(results[index]);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
item.addEventListener('keydown', (e) => {
|
|
576
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
577
|
+
e.preventDefault();
|
|
578
|
+
navigateToResult(results[index]);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// console.log('Rendered', resultItems.length, 'result items');
|
|
584
|
+
if (resultItems.length > 0) {
|
|
585
|
+
const firstItemStyle = window.getComputedStyle(resultItems[0]);
|
|
586
|
+
// console.log('First item display:', firstItemStyle.display);
|
|
587
|
+
// console.log('First item visibility:', firstItemStyle.visibility);
|
|
588
|
+
// console.log('First item opacity:', firstItemStyle.opacity);
|
|
589
|
+
// console.log('First item color:', firstItemStyle.color);
|
|
590
|
+
// console.log('First item background:', firstItemStyle.backgroundColor);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Update focusable elements - ensure all are tabbable
|
|
594
|
+
const tabbableClearBtn = clearBtn && clearBtn.getAttribute('aria-hidden') !== 'true' ? clearBtn : null;
|
|
595
|
+
const tabbableCloseBtn = closeBtn && window.innerWidth <= 1023 ? closeBtn : null; // Close button on mobile
|
|
596
|
+
focusableElements = [
|
|
597
|
+
input,
|
|
598
|
+
...(tabbableClearBtn ? [tabbableClearBtn] : []),
|
|
599
|
+
...(tabbableCloseBtn ? [tabbableCloseBtn] : []),
|
|
600
|
+
...Array.from(resultItems)
|
|
601
|
+
].filter(Boolean);
|
|
602
|
+
firstFocusableElement = focusableElements[0];
|
|
603
|
+
lastFocusableElement = focusableElements[focusableElements.length - 1];
|
|
604
|
+
|
|
605
|
+
// Ensure all result items are tabbable
|
|
606
|
+
resultItems.forEach((item) => {
|
|
607
|
+
item.setAttribute('tabindex', '0');
|
|
608
|
+
});
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// Navigate to search result
|
|
612
|
+
const navigateToResult = (result) => {
|
|
613
|
+
// Use closeSearch directly - it will be the wrapped version once reassigned
|
|
614
|
+
closeSearch();
|
|
615
|
+
window.location.href = result.url;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// Perform search
|
|
619
|
+
const performSearch = async (query) => {
|
|
620
|
+
const trimmedQuery = query ? query.trim() : '';
|
|
621
|
+
|
|
622
|
+
// Clear previous timeout
|
|
623
|
+
if (searchTimeout) {
|
|
624
|
+
clearTimeout(searchTimeout);
|
|
625
|
+
searchTimeout = null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// If query is empty, show ALL results by default
|
|
629
|
+
if (!trimmedQuery || trimmedQuery.length < 1) {
|
|
630
|
+
// console.log('Empty query - showing all results');
|
|
631
|
+
const allResults = searchIndex.map((item, index) => ({
|
|
632
|
+
...item,
|
|
633
|
+
relevance: 100 - index, // Give all items a relevance score
|
|
634
|
+
}));
|
|
635
|
+
renderResults(allResults);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Show loading state immediately (briefly) - but don't hide results list yet
|
|
640
|
+
emptyState?.setAttribute('hidden', 'true');
|
|
641
|
+
loadingState?.removeAttribute('hidden');
|
|
642
|
+
noResultsState?.setAttribute('hidden', 'true');
|
|
643
|
+
|
|
644
|
+
// Debounce search - shorter delay for more responsive feel
|
|
645
|
+
searchTimeout = setTimeout(async () => {
|
|
646
|
+
try {
|
|
647
|
+
let results = [];
|
|
648
|
+
|
|
649
|
+
// Use Algolia if configured, otherwise fall back to client-side search
|
|
650
|
+
if (useAlgolia && algoliaAppId && algoliaApiKey) {
|
|
651
|
+
// console.log('Using Algolia search for query:', trimmedQuery);
|
|
652
|
+
results = await performAlgoliaSearch(trimmedQuery);
|
|
653
|
+
} else {
|
|
654
|
+
// console.log('Using client-side search for query:', trimmedQuery);
|
|
655
|
+
results = performClientSearch(trimmedQuery);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Debug logging (commented for production)
|
|
659
|
+
// console.log('Search results:', results);
|
|
660
|
+
// console.log('Results count:', results ? results.length : 0);
|
|
661
|
+
|
|
662
|
+
// Render results
|
|
663
|
+
if (Array.isArray(results) && results.length > 0) {
|
|
664
|
+
renderResults(results);
|
|
665
|
+
} else {
|
|
666
|
+
// Show no results state
|
|
667
|
+
renderResults([]);
|
|
668
|
+
}
|
|
669
|
+
} catch (error) {
|
|
670
|
+
console.error('Search error:', error);
|
|
671
|
+
// Fall back to client-side search on error
|
|
672
|
+
try {
|
|
673
|
+
const fallbackResults = performClientSearch(trimmedQuery);
|
|
674
|
+
renderResults(fallbackResults);
|
|
675
|
+
} catch (fallbackError) {
|
|
676
|
+
console.error('Fallback search error:', fallbackError);
|
|
677
|
+
renderResults([]);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}, useAlgolia && algoliaAppId && algoliaApiKey ? 300 : 150);
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// Trap focus within search panel - only for Tab key navigation
|
|
684
|
+
const trapFocus = (e) => {
|
|
685
|
+
if (panel.getAttribute('aria-hidden') === 'true') return;
|
|
686
|
+
if (e.key !== 'Tab') return;
|
|
687
|
+
|
|
688
|
+
// CRITICAL: If panel itself has focus, immediately move to first element
|
|
689
|
+
if (document.activeElement === panel) {
|
|
690
|
+
e.preventDefault();
|
|
691
|
+
e.stopPropagation();
|
|
692
|
+
input?.focus();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Update focusable elements list (exclude panel itself)
|
|
697
|
+
const allResultItems = Array.from(resultsList?.querySelectorAll('[data-search-result]') || []);
|
|
698
|
+
const tabbableClearBtn = clearBtn && clearBtn.getAttribute('aria-hidden') !== 'true' ? clearBtn : null;
|
|
699
|
+
const tabbableCloseBtn = closeBtn && window.innerWidth <= 1023 ? closeBtn : null; // Close button on mobile
|
|
700
|
+
const currentFocusableElements = [
|
|
701
|
+
input,
|
|
702
|
+
...(tabbableClearBtn ? [tabbableClearBtn] : []),
|
|
703
|
+
...(tabbableCloseBtn ? [tabbableCloseBtn] : []),
|
|
704
|
+
...allResultItems
|
|
705
|
+
].filter(Boolean);
|
|
706
|
+
|
|
707
|
+
// Ensure panel is never in the list
|
|
708
|
+
const filteredElements = currentFocusableElements.filter(el => el !== panel);
|
|
709
|
+
|
|
710
|
+
const firstElement = filteredElements[0];
|
|
711
|
+
const lastElement = filteredElements[filteredElements.length - 1];
|
|
712
|
+
|
|
713
|
+
if (e.shiftKey) {
|
|
714
|
+
// Shift + Tab (backward)
|
|
715
|
+
if (document.activeElement === firstElement || document.activeElement === panel) {
|
|
716
|
+
e.preventDefault();
|
|
717
|
+
lastElement?.focus();
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
// Tab (forward)
|
|
721
|
+
if (document.activeElement === lastElement || document.activeElement === panel) {
|
|
722
|
+
e.preventDefault();
|
|
723
|
+
firstElement?.focus();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
// Open search
|
|
729
|
+
let openSearch = () => {
|
|
730
|
+
previousActiveElement = document.activeElement;
|
|
731
|
+
|
|
732
|
+
// On mobile, close mobile menu if it's open
|
|
733
|
+
const isMobile = window.innerWidth <= 1023;
|
|
734
|
+
if (isMobile) {
|
|
735
|
+
const mobileMenu = document.querySelector('.navbar__menu');
|
|
736
|
+
const mobileToggle = document.querySelector('.navbar__toggle');
|
|
737
|
+
if (mobileMenu && mobileMenu.classList.contains('navbar__menu--open')) {
|
|
738
|
+
mobileMenu.classList.remove('navbar__menu--open');
|
|
739
|
+
}
|
|
740
|
+
if (mobileToggle) {
|
|
741
|
+
mobileToggle.setAttribute('aria-expanded', 'false');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Add class to navbar to hide bottom border
|
|
746
|
+
const navbar = document.querySelector('.navbar');
|
|
747
|
+
if (navbar) {
|
|
748
|
+
navbar.classList.add('navbar--search-open');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Prevent body scroll
|
|
752
|
+
document.body.style.overflow = 'hidden';
|
|
753
|
+
|
|
754
|
+
// Show overlay and panel in one step to avoid stagger
|
|
755
|
+
search.setAttribute('aria-hidden', 'false');
|
|
756
|
+
overlay?.setAttribute('aria-hidden', 'false');
|
|
757
|
+
panel.setAttribute('aria-hidden', 'false');
|
|
758
|
+
panel.setAttribute('data-open', 'true');
|
|
759
|
+
|
|
760
|
+
panel.setAttribute('tabindex', '-1');
|
|
761
|
+
panel.style.outline = 'none';
|
|
762
|
+
panel.style.pointerEvents = '';
|
|
763
|
+
|
|
764
|
+
// Show all results and update focusable elements
|
|
765
|
+
setTimeout(() => {
|
|
766
|
+
const allResults = searchIndex.map((item, index) => ({
|
|
767
|
+
...item,
|
|
768
|
+
relevance: 100 - index,
|
|
769
|
+
}));
|
|
770
|
+
renderResults(allResults);
|
|
771
|
+
|
|
772
|
+
const allResultItems = Array.from(resultsList?.querySelectorAll('[data-search-result]') || []);
|
|
773
|
+
const tabbableCloseBtn = closeBtn && window.innerWidth <= 1023 ? closeBtn : null;
|
|
774
|
+
focusableElements = [
|
|
775
|
+
input,
|
|
776
|
+
...(clearBtn && clearBtn.getAttribute('aria-hidden') !== 'true' ? [clearBtn] : []),
|
|
777
|
+
...(tabbableCloseBtn ? [tabbableCloseBtn] : []),
|
|
778
|
+
...allResultItems
|
|
779
|
+
].filter(Boolean);
|
|
780
|
+
firstFocusableElement = focusableElements[0];
|
|
781
|
+
lastFocusableElement = focusableElements[focusableElements.length - 1];
|
|
782
|
+
|
|
783
|
+
allResultItems.forEach((item) => {
|
|
784
|
+
item.setAttribute('tabindex', '0');
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
input?.focus();
|
|
788
|
+
}, 0);
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// Close search
|
|
792
|
+
let closeSearch = () => {
|
|
793
|
+
panel.removeAttribute('data-open');
|
|
794
|
+
input?.setAttribute('aria-activedescendant', '');
|
|
795
|
+
|
|
796
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
797
|
+
const animationDuration = prefersReducedMotion ? 0 : 300;
|
|
798
|
+
|
|
799
|
+
setTimeout(() => {
|
|
800
|
+
// Restore body scroll
|
|
801
|
+
document.body.style.overflow = '';
|
|
802
|
+
|
|
803
|
+
// Remove class from navbar to show bottom border again
|
|
804
|
+
const navbar = document.querySelector('.navbar');
|
|
805
|
+
if (navbar) {
|
|
806
|
+
navbar.classList.remove('navbar--search-open');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
search.setAttribute('aria-hidden', 'true');
|
|
810
|
+
overlay?.setAttribute('aria-hidden', 'true');
|
|
811
|
+
panel.setAttribute('aria-hidden', 'true');
|
|
812
|
+
input.value = '';
|
|
813
|
+
renderResults([]);
|
|
814
|
+
selectedIndex = -1;
|
|
815
|
+
|
|
816
|
+
const isMobile = window.innerWidth <= 1023;
|
|
817
|
+
if (!isMobile && previousActiveElement) {
|
|
818
|
+
// Desktop: restore focus normally
|
|
819
|
+
previousActiveElement.focus();
|
|
820
|
+
}
|
|
821
|
+
// Clear previousActiveElement on both mobile and desktop
|
|
822
|
+
previousActiveElement = null;
|
|
823
|
+
|
|
824
|
+
// Ensure search elements are truly hidden and don't block clicks on both desktop and mobile
|
|
825
|
+
if (overlay) {
|
|
826
|
+
overlay.style.display = 'none';
|
|
827
|
+
overlay.style.pointerEvents = 'none';
|
|
828
|
+
}
|
|
829
|
+
if (panel) {
|
|
830
|
+
panel.style.display = 'none';
|
|
831
|
+
panel.style.pointerEvents = 'none';
|
|
832
|
+
}
|
|
833
|
+
search.style.pointerEvents = '';
|
|
834
|
+
|
|
835
|
+
// Reset after a brief delay to ensure navigation can work
|
|
836
|
+
setTimeout(() => {
|
|
837
|
+
if (overlay) {
|
|
838
|
+
overlay.style.display = '';
|
|
839
|
+
overlay.style.pointerEvents = '';
|
|
840
|
+
}
|
|
841
|
+
if (panel) {
|
|
842
|
+
panel.style.display = '';
|
|
843
|
+
panel.style.pointerEvents = '';
|
|
844
|
+
}
|
|
845
|
+
}, isMobile ? 200 : 100);
|
|
846
|
+
}, animationDuration);
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// Clear/Close search - X button closes the search panel
|
|
850
|
+
const clearSearch = (e) => {
|
|
851
|
+
const isMobile = window.innerWidth <= 1023;
|
|
852
|
+
if (e && !isMobile) {
|
|
853
|
+
// Desktop: prevent default to avoid any navigation
|
|
854
|
+
e.preventDefault();
|
|
855
|
+
e.stopPropagation();
|
|
856
|
+
}
|
|
857
|
+
// Close the search panel
|
|
858
|
+
if (isMobile) {
|
|
859
|
+
// On mobile: close asynchronously to avoid blocking menu toggle
|
|
860
|
+
setTimeout(() => {
|
|
861
|
+
closeSearch();
|
|
862
|
+
}, 0);
|
|
863
|
+
} else {
|
|
864
|
+
closeSearch();
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
// Update active descendant
|
|
869
|
+
const updateActiveDescendant = () => {
|
|
870
|
+
if (selectedIndex >= 0 && currentResults && currentResults[selectedIndex]) {
|
|
871
|
+
const activeId = `${searchId}-result-${selectedIndex}`;
|
|
872
|
+
const activeElement = document.getElementById(activeId);
|
|
873
|
+
if (activeElement) {
|
|
874
|
+
input?.setAttribute('aria-activedescendant', activeId);
|
|
875
|
+
} else {
|
|
876
|
+
input?.setAttribute('aria-activedescendant', '');
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
input?.setAttribute('aria-activedescendant', '');
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
// Keyboard navigation for results
|
|
884
|
+
const handleResultNavigation = (e) => {
|
|
885
|
+
if (panel.getAttribute('aria-hidden') === 'true') return;
|
|
886
|
+
|
|
887
|
+
const resultItems = resultsList?.querySelectorAll('[data-search-result]');
|
|
888
|
+
if (!resultItems || resultItems.length === 0) {
|
|
889
|
+
// If no results, allow default behavior
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (e.key === 'ArrowDown') {
|
|
894
|
+
e.preventDefault();
|
|
895
|
+
selectedIndex = (selectedIndex + 1) % resultItems.length;
|
|
896
|
+
resultItems[selectedIndex].focus();
|
|
897
|
+
resultItems[selectedIndex].setAttribute('aria-selected', 'true');
|
|
898
|
+
Array.from(resultItems).forEach((item, idx) => {
|
|
899
|
+
if (idx !== selectedIndex) item.setAttribute('aria-selected', 'false');
|
|
900
|
+
});
|
|
901
|
+
updateActiveDescendant();
|
|
902
|
+
} else if (e.key === 'ArrowUp') {
|
|
903
|
+
e.preventDefault();
|
|
904
|
+
selectedIndex = selectedIndex <= 0 ? resultItems.length - 1 : selectedIndex - 1;
|
|
905
|
+
resultItems[selectedIndex].focus();
|
|
906
|
+
resultItems[selectedIndex].setAttribute('aria-selected', 'true');
|
|
907
|
+
Array.from(resultItems).forEach((item, idx) => {
|
|
908
|
+
if (idx !== selectedIndex) item.setAttribute('aria-selected', 'false');
|
|
909
|
+
});
|
|
910
|
+
updateActiveDescendant();
|
|
911
|
+
} else if (e.key === 'Home') {
|
|
912
|
+
e.preventDefault();
|
|
913
|
+
if (resultItems.length > 0) {
|
|
914
|
+
selectedIndex = 0;
|
|
915
|
+
resultItems[0].focus();
|
|
916
|
+
resultItems[0].setAttribute('aria-selected', 'true');
|
|
917
|
+
Array.from(resultItems).forEach((item, idx) => {
|
|
918
|
+
if (idx !== 0) item.setAttribute('aria-selected', 'false');
|
|
919
|
+
});
|
|
920
|
+
updateActiveDescendant();
|
|
921
|
+
}
|
|
922
|
+
} else if (e.key === 'End') {
|
|
923
|
+
e.preventDefault();
|
|
924
|
+
if (resultItems.length > 0) {
|
|
925
|
+
selectedIndex = resultItems.length - 1;
|
|
926
|
+
resultItems[selectedIndex].focus();
|
|
927
|
+
resultItems[selectedIndex].setAttribute('aria-selected', 'true');
|
|
928
|
+
Array.from(resultItems).forEach((item, idx) => {
|
|
929
|
+
if (idx !== selectedIndex) item.setAttribute('aria-selected', 'false');
|
|
930
|
+
});
|
|
931
|
+
updateActiveDescendant();
|
|
932
|
+
}
|
|
933
|
+
} else if (e.key === 'Enter' && selectedIndex >= 0 && currentResults[selectedIndex]) {
|
|
934
|
+
e.preventDefault();
|
|
935
|
+
navigateToResult(currentResults[selectedIndex]);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// Event listeners - use closeSearch/openSearch directly (will be wrapped versions after reassignment)
|
|
940
|
+
trigger?.addEventListener('click', (e) => {
|
|
941
|
+
const isMobile = window.innerWidth <= 1023;
|
|
942
|
+
const isSearchOpen = panel.getAttribute('aria-hidden') === 'false';
|
|
943
|
+
|
|
944
|
+
if (isMobile) {
|
|
945
|
+
// On mobile: check if search is actually open before toggling
|
|
946
|
+
// This prevents reopening if it's already closed
|
|
947
|
+
if (isSearchOpen) {
|
|
948
|
+
// Search is open, close it
|
|
949
|
+
closeSearch(); // Will be wrapped version after reassignment
|
|
950
|
+
} else {
|
|
951
|
+
// Search is closed, open it
|
|
952
|
+
// Don't prevent default/stop propagation to avoid blocking menu toggle
|
|
953
|
+
openSearch(); // Will be wrapped version after reassignment
|
|
954
|
+
}
|
|
955
|
+
} else {
|
|
956
|
+
// Desktop: prevent default to avoid any navigation
|
|
957
|
+
e.preventDefault();
|
|
958
|
+
e.stopPropagation();
|
|
959
|
+
|
|
960
|
+
// Always open search when button is clicked
|
|
961
|
+
if (isSearchOpen) {
|
|
962
|
+
closeSearch(); // Toggle: close if already open
|
|
963
|
+
} else {
|
|
964
|
+
openSearch(); // Will be wrapped version after reassignment
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
overlay?.addEventListener('click', (e) => {
|
|
970
|
+
// Close only when clicking the backdrop (overlay itself), not when clicking inside the panel
|
|
971
|
+
if (e.target === overlay) {
|
|
972
|
+
closeSearch(); // Will be wrapped version after reassignment
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// On desktop, also close search when clicking navbar links
|
|
977
|
+
if (window.innerWidth > 1023) {
|
|
978
|
+
document.addEventListener('click', (e) => {
|
|
979
|
+
const target = e.target;
|
|
980
|
+
// Check if click is on a navbar link
|
|
981
|
+
if (target && (target.closest('.navbar__link') || target.closest('.navbar__sublink'))) {
|
|
982
|
+
// Only close if search is actually open
|
|
983
|
+
if (panel.getAttribute('aria-hidden') === 'false') {
|
|
984
|
+
closeSearch(); // Will be wrapped version after reassignment
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}, true); // Use capture phase to catch before navigation
|
|
988
|
+
}
|
|
989
|
+
clearBtn?.addEventListener('click', clearSearch);
|
|
990
|
+
|
|
991
|
+
// Close button handler - must use wrapped version
|
|
992
|
+
// On mobile, ensure it doesn't block menu toggle
|
|
993
|
+
if (closeBtn) {
|
|
994
|
+
closeBtn.addEventListener('click', (e) => {
|
|
995
|
+
const isMobile = window.innerWidth <= 1023;
|
|
996
|
+
|
|
997
|
+
if (isMobile) {
|
|
998
|
+
// On mobile: close search asynchronously to avoid blocking menu toggle
|
|
999
|
+
// Don't prevent default or stop propagation - let the event complete
|
|
1000
|
+
setTimeout(() => {
|
|
1001
|
+
closeSearch(); // Will use wrapped version after reassignment
|
|
1002
|
+
}, 0);
|
|
1003
|
+
} else {
|
|
1004
|
+
// Desktop: prevent default to avoid any navigation
|
|
1005
|
+
e.preventDefault();
|
|
1006
|
+
e.stopPropagation();
|
|
1007
|
+
closeSearch(); // Will use wrapped version after reassignment
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Close search on Escape key (handled in input keydown)
|
|
1013
|
+
// Close search when clicking overlay (already handled above)
|
|
1014
|
+
|
|
1015
|
+
input?.addEventListener('input', (e) => {
|
|
1016
|
+
const query = e.target ? e.target.value : '';
|
|
1017
|
+
|
|
1018
|
+
// Ensure search panel is open when user types
|
|
1019
|
+
if (panel.getAttribute('aria-hidden') === 'true') {
|
|
1020
|
+
openSearch(); // Will be wrapped version after reassignment
|
|
1021
|
+
// After opening, perform search with the query
|
|
1022
|
+
setTimeout(() => performSearch(query), 150);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Show/hide clear button based on input value
|
|
1027
|
+
if (query && query.trim().length > 0) {
|
|
1028
|
+
clearBtn?.removeAttribute('aria-hidden');
|
|
1029
|
+
clearBtn?.setAttribute('tabindex', '0');
|
|
1030
|
+
} else {
|
|
1031
|
+
clearBtn?.setAttribute('aria-hidden', 'true');
|
|
1032
|
+
clearBtn?.setAttribute('tabindex', '-1');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Perform search immediately as user types
|
|
1036
|
+
performSearch(query);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
// Open search when input is focused (works on both desktop and mobile)
|
|
1040
|
+
input?.addEventListener('focus', () => {
|
|
1041
|
+
if (panel.getAttribute('aria-hidden') === 'true') {
|
|
1042
|
+
openSearch(); // Will be wrapped version after reassignment
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
input?.addEventListener('keydown', (e) => {
|
|
1047
|
+
if (e.key === 'Escape') {
|
|
1048
|
+
e.preventDefault();
|
|
1049
|
+
e.stopPropagation();
|
|
1050
|
+
closeSearch();
|
|
1051
|
+
return;
|
|
1052
|
+
} else if (e.key === 'Enter') {
|
|
1053
|
+
// If there are results and one is selected, navigate to it
|
|
1054
|
+
if (selectedIndex >= 0 && currentResults[selectedIndex]) {
|
|
1055
|
+
e.preventDefault();
|
|
1056
|
+
navigateToResult(currentResults[selectedIndex]);
|
|
1057
|
+
} else if (currentResults.length > 0) {
|
|
1058
|
+
// If there are results but none selected, navigate to first
|
|
1059
|
+
e.preventDefault();
|
|
1060
|
+
navigateToResult(currentResults[0]);
|
|
1061
|
+
}
|
|
1062
|
+
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End') {
|
|
1063
|
+
handleResultNavigation(e);
|
|
1064
|
+
} else if (e.key === 'Tab') {
|
|
1065
|
+
// Trap Tab to keep focus within panel
|
|
1066
|
+
trapFocus(e);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// Also handle Escape on the panel itself and prevent panel from receiving focus
|
|
1071
|
+
panel.addEventListener('keydown', (e) => {
|
|
1072
|
+
if (e.key === 'Escape' && panel.getAttribute('aria-hidden') === 'false') {
|
|
1073
|
+
e.preventDefault();
|
|
1074
|
+
e.stopPropagation();
|
|
1075
|
+
closeSearch();
|
|
1076
|
+
} else if (e.key === 'Tab') {
|
|
1077
|
+
// Trap Tab key to keep focus within panel content
|
|
1078
|
+
trapFocus(e);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
// AGGRESSIVELY prevent panel from receiving focus - redirect immediately
|
|
1083
|
+
const preventPanelFocus = (e) => {
|
|
1084
|
+
if (e.target === panel || document.activeElement === panel) {
|
|
1085
|
+
e.preventDefault();
|
|
1086
|
+
e.stopPropagation();
|
|
1087
|
+
// Immediately redirect focus to input
|
|
1088
|
+
requestAnimationFrame(() => {
|
|
1089
|
+
if (document.activeElement === panel) {
|
|
1090
|
+
input?.focus();
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
// Multiple event listeners to catch all focus attempts
|
|
1097
|
+
panel.addEventListener('focus', preventPanelFocus, true);
|
|
1098
|
+
panel.addEventListener('focusin', preventPanelFocus, true);
|
|
1099
|
+
|
|
1100
|
+
// Also prevent focus on mousedown/click
|
|
1101
|
+
panel.addEventListener('mousedown', (e) => {
|
|
1102
|
+
if (e.target === panel) {
|
|
1103
|
+
e.preventDefault();
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// Monitor for any focus changes and redirect if panel gets focus
|
|
1108
|
+
let focusCheckInterval = null;
|
|
1109
|
+
const focusObserver = new MutationObserver(() => {
|
|
1110
|
+
if (document.activeElement === panel && panel.getAttribute('aria-hidden') === 'false') {
|
|
1111
|
+
input?.focus();
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// Observe the panel for attribute changes that might affect focus
|
|
1116
|
+
focusObserver.observe(panel, { attributes: true, attributeFilter: ['aria-hidden'] });
|
|
1117
|
+
|
|
1118
|
+
// Also check periodically (fallback) - only when panel is open
|
|
1119
|
+
const startFocusCheck = () => {
|
|
1120
|
+
if (focusCheckInterval) clearInterval(focusCheckInterval);
|
|
1121
|
+
focusCheckInterval = setInterval(() => {
|
|
1122
|
+
if (panel.getAttribute('aria-hidden') === 'false' && document.activeElement === panel) {
|
|
1123
|
+
input?.focus();
|
|
1124
|
+
}
|
|
1125
|
+
}, 100);
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
const stopFocusCheck = () => {
|
|
1129
|
+
if (focusCheckInterval) {
|
|
1130
|
+
clearInterval(focusCheckInterval);
|
|
1131
|
+
focusCheckInterval = null;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
// Store original functions
|
|
1136
|
+
const originalOpenSearch = openSearch;
|
|
1137
|
+
const originalCloseSearch = closeSearch;
|
|
1138
|
+
|
|
1139
|
+
// Create wrapped versions that include focus checking
|
|
1140
|
+
const wrappedOpenSearch = () => {
|
|
1141
|
+
originalOpenSearch();
|
|
1142
|
+
startFocusCheck();
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const wrappedCloseSearch = () => {
|
|
1146
|
+
stopFocusCheck();
|
|
1147
|
+
originalCloseSearch();
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
// Replace the function references
|
|
1151
|
+
openSearch = wrappedOpenSearch;
|
|
1152
|
+
closeSearch = wrappedCloseSearch;
|
|
1153
|
+
|
|
1154
|
+
// Store instance on the search element for global keyboard shortcut access
|
|
1155
|
+
search.__searchInstance = {
|
|
1156
|
+
openSearch: wrappedOpenSearch,
|
|
1157
|
+
closeSearch: wrappedCloseSearch
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
// Trap focus in panel - only trap Tab key, not other keys
|
|
1161
|
+
panel.addEventListener('keydown', (e) => {
|
|
1162
|
+
if (e.key === 'Tab') {
|
|
1163
|
+
trapFocus(e);
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
// Close search when clicking outside (on overlay or document)
|
|
1168
|
+
document.addEventListener('click', (e) => {
|
|
1169
|
+
if (panel.getAttribute('aria-hidden') === 'true') return;
|
|
1170
|
+
|
|
1171
|
+
const target = e.target;
|
|
1172
|
+
// Close if clicking on overlay or outside the panel (but not on trigger)
|
|
1173
|
+
if (target === overlay || (!panel.contains(target) && target !== trigger && !trigger?.contains(target))) {
|
|
1174
|
+
closeSearch();
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// Note: Keyboard shortcut is now handled globally in the outer scope
|
|
1179
|
+
// This ensures it works even if multiple Search components exist
|
|
1180
|
+
|
|
1181
|
+
// Update trigger aria-expanded
|
|
1182
|
+
const updateTriggerState = () => {
|
|
1183
|
+
const isOpen = panel.getAttribute('aria-hidden') === 'false';
|
|
1184
|
+
trigger?.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
// Watch for panel state changes
|
|
1188
|
+
const observer = new MutationObserver(updateTriggerState);
|
|
1189
|
+
if (panel) {
|
|
1190
|
+
observer.observe(panel, {
|
|
1191
|
+
attributes: true,
|
|
1192
|
+
attributeFilter: ['aria-hidden'],
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
// Set up global keyboard shortcut handler (only once per page)
|
|
1199
|
+
// Do this AFTER all search instances are inited so __searchInstance exists
|
|
1200
|
+
if (!window.__rizzoSearchShortcutSetup) {
|
|
1201
|
+
window.__rizzoSearchShortcutSetup = true;
|
|
1202
|
+
|
|
1203
|
+
document.addEventListener('keydown', (e) => {
|
|
1204
|
+
const isModifierKey = e.ctrlKey || e.metaKey;
|
|
1205
|
+
const isKKey = e.key === 'k' || e.key === 'K';
|
|
1206
|
+
if (!isModifierKey || !isKKey) return;
|
|
1207
|
+
|
|
1208
|
+
const search = document.querySelector('[data-search]');
|
|
1209
|
+
if (!search) return;
|
|
1210
|
+
const panel = search.querySelector('.search__panel');
|
|
1211
|
+
if (!panel) return;
|
|
1212
|
+
|
|
1213
|
+
const isSearchOpen = panel.getAttribute('aria-hidden') === 'false';
|
|
1214
|
+
const target = e.target;
|
|
1215
|
+
const isOtherInput = target && !search.contains(target) && (
|
|
1216
|
+
target.tagName === 'INPUT' ||
|
|
1217
|
+
target.tagName === 'TEXTAREA' ||
|
|
1218
|
+
(target.isContentEditable === true) ||
|
|
1219
|
+
target.closest('input, textarea, [contenteditable="true"]')
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
// Handle Cmd+K / Ctrl+K: toggle search (open or close). When search is open, always handle even if focus is in search input.
|
|
1223
|
+
if (isSearchOpen || !isOtherInput) {
|
|
1224
|
+
e.preventDefault();
|
|
1225
|
+
e.stopPropagation();
|
|
1226
|
+
e.stopImmediatePropagation();
|
|
1227
|
+
|
|
1228
|
+
const searchInstance = search.__searchInstance;
|
|
1229
|
+
if (searchInstance && searchInstance.openSearch && searchInstance.closeSearch) {
|
|
1230
|
+
if (isSearchOpen) {
|
|
1231
|
+
searchInstance.closeSearch();
|
|
1232
|
+
} else {
|
|
1233
|
+
searchInstance.openSearch();
|
|
1234
|
+
}
|
|
1235
|
+
} else {
|
|
1236
|
+
const trigger = search.querySelector('[data-search-trigger]');
|
|
1237
|
+
if (trigger) {
|
|
1238
|
+
if (isSearchOpen) {
|
|
1239
|
+
const overlay = search.querySelector('[data-search-overlay]');
|
|
1240
|
+
if (overlay) overlay.click();
|
|
1241
|
+
} else {
|
|
1242
|
+
trigger.click();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
}, true); // Use capture phase to intercept before browser handlers
|
|
1249
|
+
// console.log('✅ Global keyboard shortcut handler registered (Cmd+K / Ctrl+K)');
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
if (document.readyState === 'loading') {
|
|
1254
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
1255
|
+
} else {
|
|
1256
|
+
init();
|
|
1257
|
+
}
|
|
1258
|
+
})();
|
|
1259
|
+
</script>
|