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.
Files changed (230) hide show
  1. package/.env.example +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +17 -3
  4. package/bin/rizzo-css.js +93 -43
  5. package/package.json +5 -3
  6. package/scaffold/astro-app/README.md +13 -2
  7. package/scaffold/astro-app/src/components/Accordion.astro +178 -0
  8. package/scaffold/astro-app/src/components/Alert.astro +131 -0
  9. package/scaffold/astro-app/src/components/Avatar.astro +59 -0
  10. package/scaffold/astro-app/src/components/Badge.astro +24 -0
  11. package/scaffold/astro-app/src/components/Breadcrumb.astro +61 -0
  12. package/scaffold/astro-app/src/components/Button.astro +3 -0
  13. package/scaffold/astro-app/src/components/Card.astro +18 -0
  14. package/scaffold/astro-app/src/components/Checkbox.astro +38 -0
  15. package/scaffold/astro-app/src/components/CodeBlock.astro +393 -0
  16. package/scaffold/astro-app/src/components/CopyToClipboard.astro +219 -0
  17. package/scaffold/astro-app/src/components/Divider.astro +37 -0
  18. package/scaffold/astro-app/src/components/Dropdown.astro +807 -0
  19. package/scaffold/astro-app/src/components/FormGroup.astro +59 -0
  20. package/scaffold/astro-app/src/components/FrameworkSwitcher.astro +72 -0
  21. package/scaffold/astro-app/src/components/Input.astro +59 -0
  22. package/scaffold/astro-app/src/components/Modal.astro +212 -0
  23. package/scaffold/astro-app/src/components/Navbar.astro +701 -0
  24. package/scaffold/astro-app/src/components/Pagination.astro +240 -0
  25. package/scaffold/astro-app/src/components/ProgressBar.astro +65 -0
  26. package/scaffold/astro-app/src/components/Radio.astro +38 -0
  27. package/scaffold/astro-app/src/components/Search.astro +1259 -0
  28. package/scaffold/astro-app/src/components/Select.astro +49 -0
  29. package/scaffold/astro-app/src/components/Settings.astro +382 -0
  30. package/scaffold/astro-app/src/components/Spinner.astro +30 -0
  31. package/scaffold/astro-app/src/components/Table.astro +181 -0
  32. package/scaffold/astro-app/src/components/Tabs.astro +223 -0
  33. package/scaffold/astro-app/src/components/Textarea.astro +58 -0
  34. package/scaffold/astro-app/src/components/ThemeSwitcher.astro +504 -0
  35. package/scaffold/astro-app/src/components/Toast.astro +30 -0
  36. package/scaffold/astro-app/src/components/Tooltip.astro +32 -0
  37. package/scaffold/astro-app/src/components/icons/Brush.astro +10 -0
  38. package/scaffold/astro-app/src/components/icons/Cake.astro +11 -0
  39. package/scaffold/astro-app/src/components/icons/Check.astro +29 -0
  40. package/scaffold/astro-app/src/components/icons/Cherry.astro +11 -0
  41. package/scaffold/astro-app/src/components/icons/ChevronDown.astro +29 -0
  42. package/scaffold/astro-app/src/components/icons/Circle.astro +29 -0
  43. package/scaffold/astro-app/src/components/icons/Close.astro +30 -0
  44. package/scaffold/astro-app/src/components/icons/Cmd.astro +26 -0
  45. package/scaffold/astro-app/src/components/icons/Copy.astro +30 -0
  46. package/scaffold/astro-app/src/components/icons/Eye.astro +30 -0
  47. package/scaffold/astro-app/src/components/icons/Filter.astro +29 -0
  48. package/scaffold/astro-app/src/components/icons/Flame.astro +28 -0
  49. package/scaffold/astro-app/src/components/icons/Flower.astro +11 -0
  50. package/scaffold/astro-app/src/components/icons/Gear.astro +30 -0
  51. package/scaffold/astro-app/src/components/icons/Heart.astro +28 -0
  52. package/scaffold/astro-app/src/components/icons/IceCream.astro +31 -0
  53. package/scaffold/astro-app/src/components/icons/Leaf.astro +29 -0
  54. package/scaffold/astro-app/src/components/icons/Lemon.astro +11 -0
  55. package/scaffold/astro-app/src/components/icons/Moon.astro +29 -0
  56. package/scaffold/astro-app/src/components/icons/Owl.astro +34 -0
  57. package/scaffold/astro-app/src/components/icons/Palette.astro +33 -0
  58. package/scaffold/astro-app/src/components/icons/Rainbow.astro +31 -0
  59. package/scaffold/astro-app/src/components/icons/Search.astro +30 -0
  60. package/scaffold/astro-app/src/components/icons/Shield.astro +28 -0
  61. package/scaffold/astro-app/src/components/icons/Snowflake.astro +34 -0
  62. package/scaffold/astro-app/src/components/icons/Sort.astro +30 -0
  63. package/scaffold/astro-app/src/components/icons/Sun.astro +29 -0
  64. package/scaffold/astro-app/src/components/icons/Sunset.astro +10 -0
  65. package/scaffold/astro-app/src/components/icons/Zap.astro +9 -0
  66. package/scaffold/astro-app/src/components/icons/devicons/Astro.astro +53 -0
  67. package/scaffold/astro-app/src/components/icons/devicons/Bash.astro +34 -0
  68. package/scaffold/astro-app/src/components/icons/devicons/Css3.astro +29 -0
  69. package/scaffold/astro-app/src/components/icons/devicons/Git.astro +24 -0
  70. package/scaffold/astro-app/src/components/icons/devicons/Html5.astro +27 -0
  71. package/scaffold/astro-app/src/components/icons/devicons/Javascript.astro +25 -0
  72. package/scaffold/astro-app/src/components/icons/devicons/Nodejs.astro +47 -0
  73. package/scaffold/astro-app/src/components/icons/devicons/Plaintext.astro +33 -0
  74. package/scaffold/astro-app/src/components/icons/devicons/React.astro +27 -0
  75. package/scaffold/astro-app/src/components/icons/devicons/Svelte.astro +25 -0
  76. package/scaffold/astro-app/src/components/icons/devicons/Vue.astro +26 -0
  77. package/scaffold/astro-app/src/config/frameworks.ts +26 -0
  78. package/scaffold/astro-app/src/config/themes.ts +54 -0
  79. package/scaffold/astro-app/src/layouts/DocsLayout.astro +204 -0
  80. package/scaffold/astro-app/src/layouts/Layout.astro +11 -2
  81. package/scaffold/astro-app/src/pages/components/accordion.astro +172 -0
  82. package/scaffold/astro-app/src/pages/components/alert.astro +250 -0
  83. package/scaffold/astro-app/src/pages/components/avatar.astro +102 -0
  84. package/scaffold/astro-app/src/pages/components/badge.astro +119 -0
  85. package/scaffold/astro-app/src/pages/components/breadcrumb.astro +124 -0
  86. package/scaffold/astro-app/src/pages/components/button.astro +74 -0
  87. package/scaffold/astro-app/src/pages/components/cards.astro +247 -0
  88. package/scaffold/astro-app/src/pages/components/copy-to-clipboard.astro +49 -0
  89. package/scaffold/astro-app/src/pages/components/divider.astro +74 -0
  90. package/scaffold/astro-app/src/pages/components/dropdown.astro +394 -0
  91. package/scaffold/astro-app/src/pages/components/forms.astro +367 -0
  92. package/scaffold/astro-app/src/pages/components/icons.astro +246 -0
  93. package/scaffold/astro-app/src/pages/components/modal.astro +152 -0
  94. package/scaffold/astro-app/src/pages/components/navbar.astro +80 -0
  95. package/scaffold/astro-app/src/pages/components/pagination.astro +126 -0
  96. package/scaffold/astro-app/src/pages/components/progress-bar.astro +94 -0
  97. package/scaffold/astro-app/src/pages/components/search.astro +155 -0
  98. package/scaffold/astro-app/src/pages/components/settings.astro +78 -0
  99. package/scaffold/astro-app/src/pages/components/spinner.astro +81 -0
  100. package/scaffold/astro-app/src/pages/components/table.astro +144 -0
  101. package/scaffold/astro-app/src/pages/components/tabs.astro +220 -0
  102. package/scaffold/astro-app/src/pages/components/theme-switcher.astro +67 -0
  103. package/scaffold/astro-app/src/pages/components/toast.astro +157 -0
  104. package/scaffold/astro-app/src/pages/components/tooltip.astro +209 -0
  105. package/scaffold/astro-app/src/pages/components.astro +290 -0
  106. package/scaffold/astro-app/src/pages/docs/accessibility.astro +9 -0
  107. package/scaffold/astro-app/src/pages/docs/colors.astro +9 -0
  108. package/scaffold/astro-app/src/pages/docs/design-system.astro +9 -0
  109. package/scaffold/astro-app/src/pages/docs/getting-started.astro +9 -0
  110. package/scaffold/astro-app/src/pages/docs/index.astro +15 -0
  111. package/scaffold/astro-app/src/pages/docs/themes/[theme].astro +14 -0
  112. package/scaffold/astro-app/src/pages/docs/theming.astro +10 -0
  113. package/scaffold/astro-app/src/pages/index.astro +5 -11
  114. package/scaffold/svelte-app/README.md +9 -2
  115. package/scaffold/svelte-app/src/app.html +1 -1
  116. package/scaffold/svelte-app/src/lib/rizzo/Accordion.svelte +128 -0
  117. package/scaffold/svelte-app/src/lib/rizzo/Alert.svelte +85 -0
  118. package/scaffold/svelte-app/src/lib/rizzo/Avatar.svelte +39 -0
  119. package/scaffold/svelte-app/src/lib/rizzo/Badge.svelte +31 -0
  120. package/scaffold/svelte-app/src/lib/rizzo/Breadcrumb.svelte +49 -0
  121. package/scaffold/svelte-app/src/lib/rizzo/Button.svelte +27 -0
  122. package/scaffold/svelte-app/src/lib/rizzo/Card.svelte +17 -0
  123. package/scaffold/svelte-app/src/lib/rizzo/Checkbox.svelte +37 -0
  124. package/scaffold/svelte-app/src/lib/rizzo/CopyToClipboard.svelte +79 -0
  125. package/scaffold/svelte-app/src/lib/rizzo/Divider.svelte +28 -0
  126. package/scaffold/svelte-app/src/lib/rizzo/Dropdown.svelte +254 -0
  127. package/scaffold/svelte-app/src/lib/rizzo/FormGroup.svelte +51 -0
  128. package/scaffold/svelte-app/src/lib/rizzo/Input.svelte +59 -0
  129. package/scaffold/svelte-app/src/lib/rizzo/Modal.svelte +157 -0
  130. package/scaffold/svelte-app/src/lib/rizzo/Pagination.svelte +93 -0
  131. package/scaffold/svelte-app/src/lib/rizzo/ProgressBar.svelte +58 -0
  132. package/scaffold/svelte-app/src/lib/rizzo/Radio.svelte +38 -0
  133. package/scaffold/svelte-app/src/lib/rizzo/Select.svelte +51 -0
  134. package/scaffold/svelte-app/src/lib/rizzo/Spinner.svelte +14 -0
  135. package/scaffold/svelte-app/src/lib/rizzo/Table.svelte +158 -0
  136. package/scaffold/svelte-app/src/lib/rizzo/Tabs.svelte +117 -0
  137. package/scaffold/svelte-app/src/lib/rizzo/Textarea.svelte +59 -0
  138. package/scaffold/svelte-app/src/lib/rizzo/ThemeSwitcher.svelte +315 -0
  139. package/scaffold/svelte-app/src/lib/rizzo/Toast.svelte +33 -0
  140. package/scaffold/svelte-app/src/lib/rizzo/Tooltip.svelte +19 -0
  141. package/scaffold/svelte-app/src/lib/rizzo/icons/Check.svelte +29 -0
  142. package/scaffold/svelte-app/src/lib/rizzo/icons/ChevronDown.svelte +29 -0
  143. package/scaffold/svelte-app/src/lib/rizzo/icons/Circle.svelte +29 -0
  144. package/scaffold/svelte-app/src/lib/rizzo/icons/Close.svelte +30 -0
  145. package/scaffold/svelte-app/src/lib/rizzo/icons/Cmd.svelte +27 -0
  146. package/scaffold/svelte-app/src/lib/rizzo/icons/Copy.svelte +30 -0
  147. package/scaffold/svelte-app/src/lib/rizzo/icons/Eye.svelte +30 -0
  148. package/scaffold/svelte-app/src/lib/rizzo/icons/Filter.svelte +29 -0
  149. package/scaffold/svelte-app/src/lib/rizzo/icons/Gear.svelte +30 -0
  150. package/scaffold/svelte-app/src/lib/rizzo/icons/IceCream.svelte +31 -0
  151. package/scaffold/svelte-app/src/lib/rizzo/icons/Moon.svelte +29 -0
  152. package/scaffold/svelte-app/src/lib/rizzo/icons/Owl.svelte +34 -0
  153. package/scaffold/svelte-app/src/lib/rizzo/icons/Palette.svelte +33 -0
  154. package/scaffold/svelte-app/src/lib/rizzo/icons/Rainbow.svelte +31 -0
  155. package/scaffold/svelte-app/src/lib/rizzo/icons/Search.svelte +30 -0
  156. package/scaffold/svelte-app/src/lib/rizzo/icons/Snowflake.svelte +34 -0
  157. package/scaffold/svelte-app/src/lib/rizzo/icons/Sort.svelte +30 -0
  158. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Astro.svelte +45 -0
  159. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Bash.svelte +28 -0
  160. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Css3.svelte +23 -0
  161. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Git.svelte +18 -0
  162. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Html5.svelte +21 -0
  163. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Javascript.svelte +19 -0
  164. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Nodejs.svelte +44 -0
  165. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Plaintext.svelte +24 -0
  166. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/React.svelte +21 -0
  167. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/SvelteIcon.svelte +19 -0
  168. package/scaffold/svelte-app/src/lib/rizzo/icons/devicons/Vue.svelte +20 -0
  169. package/scaffold/svelte-app/src/lib/rizzo/index.ts +33 -0
  170. package/scaffold/svelte-app/src/lib/rizzo-docs/CodeBlock.svelte +239 -0
  171. package/scaffold/svelte-app/src/lib/rizzo-docs/SvelteDocPage.svelte +99 -0
  172. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/AccordionDoc.svelte +53 -0
  173. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/AlertDoc.svelte +114 -0
  174. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/AvatarDoc.svelte +92 -0
  175. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/BadgeDoc.svelte +60 -0
  176. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/BreadcrumbDoc.svelte +55 -0
  177. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ButtonDoc.svelte +55 -0
  178. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/CardsDoc.svelte +173 -0
  179. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ComingSoon.svelte +12 -0
  180. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ComponentsOverview.svelte +92 -0
  181. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/CopyToClipboardDoc.svelte +26 -0
  182. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/DividerDoc.svelte +105 -0
  183. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/DropdownDoc.svelte +161 -0
  184. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/FormsDoc.svelte +375 -0
  185. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/IconsDoc.svelte +246 -0
  186. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/Index.svelte +8 -0
  187. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ModalDoc.svelte +50 -0
  188. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/NavbarDoc.svelte +79 -0
  189. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/PaginationDoc.svelte +44 -0
  190. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ProgressBarDoc.svelte +95 -0
  191. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/SearchDoc.svelte +147 -0
  192. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/SettingsDoc.svelte +158 -0
  193. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/SpinnerDoc.svelte +41 -0
  194. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/TableDoc.svelte +116 -0
  195. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/TabsDoc.svelte +152 -0
  196. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ThemeSwitcherDoc.svelte +181 -0
  197. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/Theming.svelte +6 -0
  198. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/ToastDoc.svelte +136 -0
  199. package/scaffold/svelte-app/src/lib/rizzo-docs/pages/TooltipDoc.svelte +57 -0
  200. package/scaffold/svelte-app/src/routes/+page.svelte +2 -2
  201. package/scaffold/svelte-app/src/routes/components/+page.svelte +4 -0
  202. package/scaffold/svelte-app/src/routes/components/[slug]/+page.svelte +7 -0
  203. package/scaffold/vanilla/README.md +11 -4
  204. package/scaffold/vanilla/components/accordion.html +187 -0
  205. package/scaffold/vanilla/components/alert.html +187 -0
  206. package/scaffold/vanilla/components/avatar.html +187 -0
  207. package/scaffold/vanilla/components/badge.html +187 -0
  208. package/scaffold/vanilla/components/breadcrumb.html +187 -0
  209. package/scaffold/vanilla/components/button.html +187 -0
  210. package/scaffold/vanilla/components/cards.html +187 -0
  211. package/scaffold/vanilla/components/copy-to-clipboard.html +187 -0
  212. package/scaffold/vanilla/components/divider.html +187 -0
  213. package/scaffold/vanilla/components/dropdown.html +187 -0
  214. package/scaffold/vanilla/components/forms.html +187 -0
  215. package/scaffold/vanilla/components/icons.html +187 -0
  216. package/scaffold/vanilla/components/index.html +212 -0
  217. package/scaffold/vanilla/components/modal.html +187 -0
  218. package/scaffold/vanilla/components/navbar.html +187 -0
  219. package/scaffold/vanilla/components/pagination.html +187 -0
  220. package/scaffold/vanilla/components/progress-bar.html +187 -0
  221. package/scaffold/vanilla/components/search.html +187 -0
  222. package/scaffold/vanilla/components/settings.html +187 -0
  223. package/scaffold/vanilla/components/spinner.html +187 -0
  224. package/scaffold/vanilla/components/table.html +187 -0
  225. package/scaffold/vanilla/components/tabs.html +187 -0
  226. package/scaffold/vanilla/components/theme-switcher.html +187 -0
  227. package/scaffold/vanilla/components/toast.html +187 -0
  228. package/scaffold/vanilla/components/tooltip.html +187 -0
  229. package/scaffold/vanilla/index.html +16 -6
  230. 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, '&amp;')
411
+ .replace(/</g, '&lt;')
412
+ .replace(/>/g, '&gt;')
413
+ .replace(/"/g, '&quot;');
414
+ const escapedCategory = String(result.category || '')
415
+ .replace(/&/g, '&amp;')
416
+ .replace(/</g, '&lt;')
417
+ .replace(/>/g, '&gt;')
418
+ .replace(/"/g, '&quot;');
419
+ const escapedContent = result.content
420
+ ? String(result.content)
421
+ .replace(/&/g, '&amp;')
422
+ .replace(/</g, '&lt;')
423
+ .replace(/>/g, '&gt;')
424
+ .replace(/"/g, '&quot;')
425
+ .substring(0, 150)
426
+ : '';
427
+ const escapedUrl = result.url
428
+ ? String(result.url).replace(/"/g, '&quot;')
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>