components-svelte 0.1.0

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 (242) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/dist/components/Search.svelte +320 -0
  4. package/dist/components/Search.svelte.d.ts +31 -0
  5. package/dist/components/SearchButton.svelte +82 -0
  6. package/dist/components/SearchButton.svelte.d.ts +13 -0
  7. package/dist/components/SearchProvider.svelte +87 -0
  8. package/dist/components/SearchProvider.svelte.d.ts +17 -0
  9. package/dist/components/accordion/Accordion.svelte +150 -0
  10. package/dist/components/accordion/Accordion.svelte.d.ts +25 -0
  11. package/dist/components/accordion/AccordionCover.svelte +68 -0
  12. package/dist/components/accordion/AccordionCover.svelte.d.ts +13 -0
  13. package/dist/components/accordion/AccordionGroup.svelte +23 -0
  14. package/dist/components/accordion/AccordionGroup.svelte.d.ts +8 -0
  15. package/dist/components/accordion/accordion-url-utils.d.ts +4 -0
  16. package/dist/components/accordion/accordion-url-utils.js +48 -0
  17. package/dist/components/accordion/index.d.ts +29 -0
  18. package/dist/components/accordion/index.js +6 -0
  19. package/dist/components/badge/Badge.svelte +152 -0
  20. package/dist/components/badge/Badge.svelte.d.ts +23 -0
  21. package/dist/components/badge/index.d.ts +1 -0
  22. package/dist/components/badge/index.js +2 -0
  23. package/dist/components/callout/Callout.svelte +317 -0
  24. package/dist/components/callout/Callout.svelte.d.ts +17 -0
  25. package/dist/components/callout/Check.svelte +9 -0
  26. package/dist/components/callout/Check.svelte.d.ts +4 -0
  27. package/dist/components/callout/Danger.svelte +9 -0
  28. package/dist/components/callout/Danger.svelte.d.ts +4 -0
  29. package/dist/components/callout/Info.svelte +9 -0
  30. package/dist/components/callout/Info.svelte.d.ts +4 -0
  31. package/dist/components/callout/Note.svelte +9 -0
  32. package/dist/components/callout/Note.svelte.d.ts +4 -0
  33. package/dist/components/callout/Tip.svelte +9 -0
  34. package/dist/components/callout/Tip.svelte.d.ts +4 -0
  35. package/dist/components/callout/Warning.svelte +9 -0
  36. package/dist/components/callout/Warning.svelte.d.ts +4 -0
  37. package/dist/components/callout/index.d.ts +8 -0
  38. package/dist/components/callout/index.js +8 -0
  39. package/dist/components/card/Card.svelte +172 -0
  40. package/dist/components/card/Card.svelte.d.ts +21 -0
  41. package/dist/components/card/index.d.ts +3 -0
  42. package/dist/components/card/index.js +2 -0
  43. package/dist/components/code-block/BaseCodeBlock.svelte +249 -0
  44. package/dist/components/code-block/BaseCodeBlock.svelte.d.ts +10 -0
  45. package/dist/components/code-block/CodeBlock.svelte +185 -0
  46. package/dist/components/code-block/CodeBlock.svelte.d.ts +5 -0
  47. package/dist/components/code-block/CodeFooter.svelte +34 -0
  48. package/dist/components/code-block/CodeFooter.svelte.d.ts +8 -0
  49. package/dist/components/code-block/CodeHeader.svelte +52 -0
  50. package/dist/components/code-block/CodeHeader.svelte.d.ts +11 -0
  51. package/dist/components/code-block/CopyToClipboardButton.svelte +124 -0
  52. package/dist/components/code-block/CopyToClipboardButton.svelte.d.ts +4 -0
  53. package/dist/components/code-block/index.d.ts +10 -0
  54. package/dist/components/code-block/index.js +5 -0
  55. package/dist/components/code-block/types.d.ts +85 -0
  56. package/dist/components/code-block/types.js +1 -0
  57. package/dist/components/code-group/CodeGroup.svelte +327 -0
  58. package/dist/components/code-group/CodeGroup.svelte.d.ts +24 -0
  59. package/dist/components/code-group/CodeGroupSelect.svelte +170 -0
  60. package/dist/components/code-group/CodeGroupSelect.svelte.d.ts +25 -0
  61. package/dist/components/code-group/CodeSelectDropdown.svelte +74 -0
  62. package/dist/components/code-group/CodeSelectDropdown.svelte.d.ts +9 -0
  63. package/dist/components/code-group/LanguageDropdown.svelte +134 -0
  64. package/dist/components/code-group/LanguageDropdown.svelte.d.ts +11 -0
  65. package/dist/components/code-group/LanguageIcon.svelte +24 -0
  66. package/dist/components/code-group/LanguageIcon.svelte.d.ts +8 -0
  67. package/dist/components/code-group/context.d.ts +14 -0
  68. package/dist/components/code-group/context.js +2 -0
  69. package/dist/components/code-group/index.d.ts +8 -0
  70. package/dist/components/code-group/index.js +4 -0
  71. package/dist/components/color/Color.svelte +34 -0
  72. package/dist/components/color/Color.svelte.d.ts +10 -0
  73. package/dist/components/color/ColorItem.svelte +140 -0
  74. package/dist/components/color/ColorItem.svelte.d.ts +10 -0
  75. package/dist/components/color/ColorRow.svelte +27 -0
  76. package/dist/components/color/ColorRow.svelte.d.ts +8 -0
  77. package/dist/components/color/index.d.ts +1 -0
  78. package/dist/components/color/index.js +8 -0
  79. package/dist/components/columns/Box.svelte +15 -0
  80. package/dist/components/columns/Box.svelte.d.ts +7 -0
  81. package/dist/components/columns/Columns.svelte +31 -0
  82. package/dist/components/columns/Columns.svelte.d.ts +10 -0
  83. package/dist/components/columns/constants.d.ts +5 -0
  84. package/dist/components/columns/constants.js +3 -0
  85. package/dist/components/columns/index.d.ts +5 -0
  86. package/dist/components/columns/index.js +3 -0
  87. package/dist/components/expandable/Expandable.svelte +182 -0
  88. package/dist/components/expandable/Expandable.svelte.d.ts +22 -0
  89. package/dist/components/expandable/index.d.ts +3 -0
  90. package/dist/components/expandable/index.js +2 -0
  91. package/dist/components/frame/Frame.svelte +118 -0
  92. package/dist/components/frame/Frame.svelte.d.ts +15 -0
  93. package/dist/components/frame/index.d.ts +3 -0
  94. package/dist/components/frame/index.js +2 -0
  95. package/dist/components/icon/Icon.svelte +129 -0
  96. package/dist/components/icon/Icon.svelte.d.ts +18 -0
  97. package/dist/components/icon/index.d.ts +2 -0
  98. package/dist/components/icon/index.js +1 -0
  99. package/dist/components/index.d.ts +22 -0
  100. package/dist/components/index.js +22 -0
  101. package/dist/components/mermaid/Mermaid.svelte +181 -0
  102. package/dist/components/mermaid/Mermaid.svelte.d.ts +11 -0
  103. package/dist/components/mermaid/ZoomControls.svelte +74 -0
  104. package/dist/components/mermaid/ZoomControls.svelte.d.ts +12 -0
  105. package/dist/components/mermaid/index.d.ts +7 -0
  106. package/dist/components/mermaid/index.js +4 -0
  107. package/dist/components/mermaid/use-pan-zoom.svelte.d.ts +11 -0
  108. package/dist/components/mermaid/use-pan-zoom.svelte.js +41 -0
  109. package/dist/components/panel/Panel.svelte +27 -0
  110. package/dist/components/panel/Panel.svelte.d.ts +9 -0
  111. package/dist/components/panel/index.d.ts +3 -0
  112. package/dist/components/panel/index.js +2 -0
  113. package/dist/components/property/DeprecatedPill.svelte +16 -0
  114. package/dist/components/property/DeprecatedPill.svelte.d.ts +6 -0
  115. package/dist/components/property/InfoPill.svelte +32 -0
  116. package/dist/components/property/InfoPill.svelte.d.ts +10 -0
  117. package/dist/components/property/ParamHead.svelte +188 -0
  118. package/dist/components/property/ParamHead.svelte.d.ts +22 -0
  119. package/dist/components/property/Property.svelte +123 -0
  120. package/dist/components/property/Property.svelte.d.ts +24 -0
  121. package/dist/components/property/RequiredPill.svelte +16 -0
  122. package/dist/components/property/RequiredPill.svelte.d.ts +6 -0
  123. package/dist/components/property/index.d.ts +11 -0
  124. package/dist/components/property/index.js +6 -0
  125. package/dist/components/search/index.d.ts +7 -0
  126. package/dist/components/search/index.js +4 -0
  127. package/dist/components/search/use-search.d.ts +9 -0
  128. package/dist/components/search/use-search.js +6 -0
  129. package/dist/components/steps/Step.svelte +314 -0
  130. package/dist/components/steps/Step.svelte.d.ts +26 -0
  131. package/dist/components/steps/Steps.svelte +51 -0
  132. package/dist/components/steps/Steps.svelte.d.ts +11 -0
  133. package/dist/components/steps/constants.d.ts +4 -0
  134. package/dist/components/steps/constants.js +2 -0
  135. package/dist/components/steps/context.d.ts +10 -0
  136. package/dist/components/steps/context.js +2 -0
  137. package/dist/components/steps/index.d.ts +32 -0
  138. package/dist/components/steps/index.js +6 -0
  139. package/dist/components/tabs/Tabs.svelte +254 -0
  140. package/dist/components/tabs/Tabs.svelte.d.ts +16 -0
  141. package/dist/components/tabs/TabsItem.svelte +56 -0
  142. package/dist/components/tabs/TabsItem.svelte.d.ts +15 -0
  143. package/dist/components/tabs/context.d.ts +20 -0
  144. package/dist/components/tabs/context.js +2 -0
  145. package/dist/components/tabs/index.d.ts +27 -0
  146. package/dist/components/tabs/index.js +5 -0
  147. package/dist/components/tile/Tile.svelte +124 -0
  148. package/dist/components/tile/Tile.svelte.d.ts +12 -0
  149. package/dist/components/tile/index.d.ts +2 -0
  150. package/dist/components/tile/index.js +1 -0
  151. package/dist/components/tooltip/Tooltip.svelte +170 -0
  152. package/dist/components/tooltip/Tooltip.svelte.d.ts +23 -0
  153. package/dist/components/tooltip/index.d.ts +2 -0
  154. package/dist/components/tooltip/index.js +1 -0
  155. package/dist/components/tree/Tree.svelte +226 -0
  156. package/dist/components/tree/Tree.svelte.d.ts +9 -0
  157. package/dist/components/tree/TreeFile.svelte +44 -0
  158. package/dist/components/tree/TreeFile.svelte.d.ts +6 -0
  159. package/dist/components/tree/TreeFolder.svelte +107 -0
  160. package/dist/components/tree/TreeFolder.svelte.d.ts +10 -0
  161. package/dist/components/tree/context.d.ts +3 -0
  162. package/dist/components/tree/context.js +7 -0
  163. package/dist/components/tree/index.d.ts +10 -0
  164. package/dist/components/tree/index.js +9 -0
  165. package/dist/components/tree/tree-level-context.d.ts +8 -0
  166. package/dist/components/tree/tree-level-context.js +7 -0
  167. package/dist/components/tree/use-tree-level.d.ts +3 -0
  168. package/dist/components/tree/use-tree-level.js +5 -0
  169. package/dist/components/tree/utils.d.ts +9 -0
  170. package/dist/components/tree/utils.js +86 -0
  171. package/dist/components/update/Update.svelte +210 -0
  172. package/dist/components/update/Update.svelte.d.ts +34 -0
  173. package/dist/components/update/index.d.ts +4 -0
  174. package/dist/components/update/index.js +3 -0
  175. package/dist/components/view/View.svelte +52 -0
  176. package/dist/components/view/View.svelte.d.ts +14 -0
  177. package/dist/components/view/index.d.ts +3 -0
  178. package/dist/components/view/index.js +2 -0
  179. package/dist/constants/brands.d.ts +2 -0
  180. package/dist/constants/brands.js +4 -0
  181. package/dist/constants/index.d.ts +6 -0
  182. package/dist/constants/index.js +6 -0
  183. package/dist/constants/selectors.d.ts +125 -0
  184. package/dist/constants/selectors.js +125 -0
  185. package/dist/hooks/index.d.ts +8 -0
  186. package/dist/hooks/index.js +6 -0
  187. package/dist/hooks/use-expandable-memory.svelte.d.ts +6 -0
  188. package/dist/hooks/use-expandable-memory.svelte.js +89 -0
  189. package/dist/hooks/use-expandable.svelte.d.ts +15 -0
  190. package/dist/hooks/use-expandable.svelte.js +53 -0
  191. package/dist/hooks/use-get-shiki-highlighted-html.svelte.d.ts +8 -0
  192. package/dist/hooks/use-get-shiki-highlighted-html.svelte.js +36 -0
  193. package/dist/hooks/use-has-hover.d.ts +2 -0
  194. package/dist/hooks/use-has-hover.js +10 -0
  195. package/dist/hooks/use-is-dark-theme.svelte.d.ts +3 -0
  196. package/dist/hooks/use-is-dark-theme.svelte.js +25 -0
  197. package/dist/hooks/use-isomorphic-layout-effect.d.ts +3 -0
  198. package/dist/hooks/use-isomorphic-layout-effect.js +6 -0
  199. package/dist/index.d.ts +2 -0
  200. package/dist/index.js +2 -0
  201. package/dist/styles.css +2 -0
  202. package/dist/utils/cn.d.ts +3 -0
  203. package/dist/utils/cn.js +7 -0
  204. package/dist/utils/copy-to-clipboard.d.ts +4 -0
  205. package/dist/utils/copy-to-clipboard.js +50 -0
  206. package/dist/utils/dedent.d.ts +1 -0
  207. package/dist/utils/dedent.js +125 -0
  208. package/dist/utils/enhance-video-props.d.ts +4 -0
  209. package/dist/utils/enhance-video-props.js +25 -0
  210. package/dist/utils/get-node-text.d.ts +2 -0
  211. package/dist/utils/get-node-text.js +26 -0
  212. package/dist/utils/icon-utils.d.ts +18 -0
  213. package/dist/utils/icon-utils.js +30 -0
  214. package/dist/utils/is-absolute-url.d.ts +4 -0
  215. package/dist/utils/is-absolute-url.js +11 -0
  216. package/dist/utils/is-remote-url.d.ts +2 -0
  217. package/dist/utils/is-remote-url.js +12 -0
  218. package/dist/utils/make-marker-ids-unique.d.ts +2 -0
  219. package/dist/utils/make-marker-ids-unique.js +36 -0
  220. package/dist/utils/shiki/code-styling.d.ts +13 -0
  221. package/dist/utils/shiki/code-styling.js +1 -0
  222. package/dist/utils/shiki/constants.d.ts +21 -0
  223. package/dist/utils/shiki/constants.js +116 -0
  224. package/dist/utils/shiki/get-code-styling.d.ts +9 -0
  225. package/dist/utils/shiki/get-code-styling.js +38 -0
  226. package/dist/utils/shiki/get-shiki-background-colors.d.ts +20 -0
  227. package/dist/utils/shiki/get-shiki-background-colors.js +56 -0
  228. package/dist/utils/shiki/index.d.ts +30 -0
  229. package/dist/utils/shiki/index.js +209 -0
  230. package/dist/utils/shiki/lib.d.ts +4 -0
  231. package/dist/utils/shiki/lib.js +49 -0
  232. package/dist/utils/shiki/shiki-themes.d.ts +2 -0
  233. package/dist/utils/shiki/shiki-themes.js +64 -0
  234. package/dist/utils/shiki/snippet-presets.d.ts +27 -0
  235. package/dist/utils/shiki/snippet-presets.js +44 -0
  236. package/dist/utils/shiki/worker-client.d.ts +9 -0
  237. package/dist/utils/shiki/worker-client.js +22 -0
  238. package/dist/utils/shiki/worker.d.ts +1 -0
  239. package/dist/utils/shiki/worker.js +134 -0
  240. package/dist/utils/slugify.d.ts +2 -0
  241. package/dist/utils/slugify.js +9 -0
  242. package/package.json +100 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kimenzo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # ProductClient Svelte Components
2
+
3
+ Svelte 5 components for documentation sites. The package provides composable documentation primitives with TypeScript types and a compiled stylesheet.
4
+
5
+ ## Requirements
6
+
7
+ - Svelte 5.57 or newer
8
+ - Node.js 20 or newer, or Bun
9
+ - Tailwind CSS 4 when extending the component styles with application utilities
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install components-svelte
15
+ ```
16
+
17
+ The package can also be installed with pnpm, yarn, or Bun.
18
+
19
+ ## Setup
20
+
21
+ Import the compiled stylesheet once from your application entrypoint or global stylesheet:
22
+
23
+ ```css
24
+ @import "components-svelte/styles.css";
25
+ ```
26
+
27
+ The stylesheet includes the component design tokens, base styles, utilities, and code-block styles. Import it before application overrides so local styles can take precedence.
28
+
29
+ ## Usage
30
+
31
+ Components are native Svelte 5 components. They are not React wrappers.
32
+
33
+ ```svelte
34
+ <script lang="ts">
35
+ import { Callout, CodeBlock } from "components-svelte";
36
+ </script>
37
+
38
+ <Callout variant="info" title="Getting started">
39
+ {#snippet children()}
40
+ <p>Install the package and import the stylesheet once.</p>
41
+ {/snippet}
42
+ </Callout>
43
+
44
+ <CodeBlock language="typescript">
45
+ {#snippet children()}const ready = true;{/snippet}
46
+ </CodeBlock>
47
+ ```
48
+
49
+ Svelte snippets are used for component content where a component accepts children or named slots:
50
+
51
+ ```svelte
52
+ <script lang="ts">
53
+ import { Card } from "components-svelte";
54
+ </script>
55
+
56
+ <Card title="ProductClient">
57
+ {#snippet children()}
58
+ <p>Documentation primitives for your product.</p>
59
+ {/snippet}
60
+ </Card>
61
+ ```
62
+
63
+ ## Component families
64
+
65
+ - Accordion
66
+ - Badge
67
+ - Callout
68
+ - Card
69
+ - Code Block
70
+ - Code Group
71
+ - Color
72
+ - Columns
73
+ - Expandable
74
+ - Frame
75
+ - Icon
76
+ - Mermaid
77
+ - Panel
78
+ - Property
79
+ - Search
80
+ - Steps
81
+ - Tabs
82
+ - Tile
83
+ - Tooltip
84
+ - Tree
85
+ - Update
86
+ - View
87
+
88
+ Compound APIs are exported from the main entrypoint, including `Accordion.Group`, `Color.Row`, `Color.Item`, `Steps.Item`, `Tabs.Item`, `Tree.File`, and `Tree.Folder`.
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ bun install
94
+ bun run check
95
+ bun run build:package
96
+ bun run storybook
97
+ ```
98
+
99
+ `bun run build:package` generates the typed component package in `dist` and the public `styles.css` export. The Vite app build remains available through `bun run build`.
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,320 @@
1
+ <script lang="ts">
2
+ import { cn } from "../utils/cn";
3
+ import { Search as SearchIcon, ChevronRight, Loader2, SearchX } from "@lucide/svelte";
4
+ import { fade, scale } from "svelte/transition";
5
+ import { tick } from "svelte";
6
+ import type { Snippet } from "svelte";
7
+
8
+ export type SearchResult = {
9
+ id: string;
10
+ header: string;
11
+ content: string;
12
+ link: string;
13
+ icon?: Snippet;
14
+ metadata?: {
15
+ breadcrumbs?: string[];
16
+ [key: string]: unknown;
17
+ };
18
+ };
19
+
20
+ export type SearchProps = {
21
+ onSearch?: (query: string) => void;
22
+ results?: SearchResult[];
23
+ isOpen: boolean;
24
+ onClose: () => void;
25
+ isLoading?: boolean;
26
+ placeholder?: string;
27
+ recentSearches?: SearchResult[];
28
+ onSelectResult?: (result: SearchResult, query: string) => void;
29
+ class?: string;
30
+ emptyState?: Snippet;
31
+ loadingState?: Snippet;
32
+ position?: "top" | "center";
33
+ paddingTop?: string;
34
+ ref?: HTMLInputElement | null;
35
+ };
36
+
37
+ let {
38
+ onSearch,
39
+ results = [],
40
+ isOpen = $bindable(false),
41
+ onClose,
42
+ isLoading = false,
43
+ placeholder = "Search...",
44
+ recentSearches = [],
45
+ onSelectResult,
46
+ class: className,
47
+ emptyState,
48
+ loadingState,
49
+ position = "top",
50
+ paddingTop = "64px",
51
+ ref = $bindable(null),
52
+ }: SearchProps = $props();
53
+
54
+ let query = $state("");
55
+ let isContentScrolled = $state(false);
56
+ let inputRef: HTMLInputElement | null = $state(null);
57
+ let activeIndex = $state(-1);
58
+
59
+ $effect(() => {
60
+ if (inputRef) ref = inputRef;
61
+ });
62
+
63
+ $effect(() => {
64
+ if (!isOpen) {
65
+ query = "";
66
+ isContentScrolled = false;
67
+ activeIndex = -1;
68
+ }
69
+ });
70
+
71
+ // Focus handling — mirrors React autoFocus, with blur on close
72
+ $effect(() => {
73
+ if (isOpen && inputRef) {
74
+ tick().then(() => {
75
+ inputRef?.focus();
76
+ });
77
+ } else if (!isOpen && inputRef) {
78
+ inputRef?.blur();
79
+ }
80
+ });
81
+
82
+ // Global Esc and click-outside via window — mirrors React Dialog closeOnEscape + backdrop click
83
+ // Also handles autofocus return — after close, blur is handled above
84
+ $effect(() => {
85
+ if (!isOpen) return;
86
+ const handleWindowKeyDown = (e: KeyboardEvent) => {
87
+ if (e.key === "Escape") {
88
+ e.preventDefault();
89
+ onClose();
90
+ }
91
+ };
92
+ window.addEventListener("keydown", handleWindowKeyDown);
93
+ return () => window.removeEventListener("keydown", handleWindowKeyDown);
94
+ });
95
+
96
+ $effect(() => {
97
+ void query;
98
+ void results.length;
99
+ void recentSearches.length;
100
+ activeIndex = -1;
101
+ });
102
+
103
+ function handleQueryChange(value: string) {
104
+ query = value;
105
+ onSearch?.(value);
106
+ }
107
+
108
+ function handleSelectOption(result: SearchResult) {
109
+ if (!result) return;
110
+ onSelectResult?.(result, query);
111
+ onClose();
112
+ }
113
+
114
+ function handleKeyDown(e: KeyboardEvent) {
115
+ const target = e.target as HTMLInputElement;
116
+ if (e.key === "ArrowDown") {
117
+ e.preventDefault();
118
+ if (allResults.length === 0) return;
119
+ activeIndex = (activeIndex + 1) % allResults.length;
120
+ return;
121
+ }
122
+ if (e.key === "ArrowUp") {
123
+ e.preventDefault();
124
+ if (allResults.length === 0) return;
125
+ activeIndex = (activeIndex - 1 + allResults.length) % allResults.length;
126
+ return;
127
+ }
128
+ if (e.key === "Enter") {
129
+ if ((e.metaKey || e.ctrlKey) && query && results.length > 0 && !isLoading) {
130
+ e.preventDefault();
131
+ const firstResult = results[0];
132
+ if (firstResult) handleSelectOption(firstResult);
133
+ return;
134
+ }
135
+ if (activeIndex >= 0 && allResults[activeIndex]) {
136
+ e.preventDefault();
137
+ handleSelectOption(allResults[activeIndex] as SearchResult);
138
+ return;
139
+ }
140
+ }
141
+ if (e.key === "Escape") {
142
+ e.preventDefault();
143
+ onClose();
144
+ return;
145
+ }
146
+ if (e.key === "Tab" && (results.length > 0 || recentSearches.length > 0)) {
147
+ e.preventDefault();
148
+ target.setSelectionRange(target.value.length, target.value.length);
149
+ }
150
+ }
151
+
152
+ function handleScroll(e: Event) {
153
+ const target = e.target as HTMLElement;
154
+ isContentScrolled = target.scrollTop > 0;
155
+ }
156
+
157
+ let showRecentSearches = $derived(!query && recentSearches.length > 0 && results.length === 0);
158
+ let showResults = $derived(!!query && results.length > 0 && !isLoading);
159
+ let showEmptyState = $derived(!!query && !isLoading && results.length === 0);
160
+ let allResults = $derived(showRecentSearches ? recentSearches : showResults ? results : []);
161
+ </script>
162
+
163
+ <!-- Mirrors React <Transition show={isOpen}><Dialog> — no extra portal, same fixed overlay/panel, React vision preserved -->
164
+ {#if isOpen}
165
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
166
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
167
+ <div class={cn("relative z-50", className)}>
168
+ <!-- Overlay — fixed, backdrop-blur, closes on click — mirrors React TransitionChild overlay -->
169
+ <div
170
+ class="fixed inset-0 bg-black/20 backdrop-blur-sm dark:bg-black/40"
171
+ onclick={onClose}
172
+ in:fade={{ duration: 100 }}
173
+ out:fade={{ duration: 50 }}
174
+ ></div>
175
+
176
+ <!-- Container — mirrors React fixed inset-0 flex justify-center -->
177
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
178
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
179
+ <div
180
+ class={cn("fixed inset-0 z-10 flex justify-center p-4", position === "top" ? "items-start" : "items-center")}
181
+ style={position === "top" ? `padding-top: ${paddingTop}` : undefined}
182
+ onclick={onClose}
183
+ >
184
+ <!-- Panel — mirrors React DialogPanel, stopPropagation so click inside doesn't close, with scale transform -->
185
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
186
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
187
+ <div
188
+ class="flex w-full max-w-[640px] flex-col overflow-hidden rounded-2xl border border-stone-200 bg-white shadow-2xl dark:border-white/10 dark:bg-stone-900"
189
+ onclick={(e) => e.stopPropagation()}
190
+ in:scale={{ duration: 100, start: 0.95, opacity: 0 }}
191
+ out:fade={{ duration: 50 }}
192
+ >
193
+ <div
194
+ class={cn("relative z-10 h-14 border-transparent border-b p-1.5 transition", isContentScrolled && "border-stone-200 dark:border-white/10")}
195
+ >
196
+ <!-- svelte-ignore a11y_autofocus -->
197
+ <input
198
+ bind:this={inputRef}
199
+ autocomplete="off"
200
+ autofocus
201
+ class={cn(
202
+ "peer h-full w-full rounded-xl bg-white pr-14 pl-11 text-stone-950 tracking-tight shadow-sm outline-none ring ring-black/5 transition placeholder:text-stone-400 focus:ring-black/90 dark:bg-stone-900 dark:text-white dark:focus:ring-white placeholder:dark:text-white/50",
203
+ "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none",
204
+ !isContentScrolled && query && "shadow-lg"
205
+ )}
206
+ oninput={(e) => handleQueryChange((e.target as HTMLInputElement).value)}
207
+ onkeydown={handleKeyDown}
208
+ placeholder={placeholder}
209
+ type="search"
210
+ value={query}
211
+ />
212
+ <SearchIcon class="absolute top-1/2 left-5 -translate-y-1/2 text-stone-950 opacity-50 peer-focus:opacity-100 dark:text-white" size={18} />
213
+ {#if query}
214
+ <div class="absolute top-1/2 right-6 flex -translate-y-1/2 items-center justify-center gap-1 rounded-md bg-stone-950/5 px-1.5 py-1.5 font-medium text-xs text-zinc-950/70 leading-[9px] dark:bg-white/5 dark:text-white/70">
215
+ ESC
216
+ </div>
217
+ {/if}
218
+ </div>
219
+
220
+ <div class="mx-1.5 max-h-[calc(100vh-10rem)] overflow-y-auto" onscroll={handleScroll} role="listbox">
221
+ {#if showRecentSearches}
222
+ <div class="flex items-center justify-between px-2.5 py-2">
223
+ <span class="truncate text-sm text-stone-500">Recent searches</span>
224
+ </div>
225
+ {#each recentSearches as result, i (result.id)}
226
+ <button
227
+ type="button"
228
+ class="w-full text-left last:mb-2"
229
+ onclick={() => handleSelectOption(result)}
230
+ onmouseenter={() => activeIndex = i}
231
+ onmouseleave={() => activeIndex = -1}
232
+ >
233
+ <div class={cn("flex w-full cursor-pointer items-start gap-2 rounded-xl border border-transparent bg-transparent px-3 py-2 text-stone-500 transition-colors focus:ring-0 focus:ring-offset-0", activeIndex === i && "bg-stone-100 dark:bg-white/5")}>
234
+ {#if result.icon}
235
+ <div class="shrink-0 self-center text-stone-700 dark:text-stone-300">
236
+ {@render result.icon()}
237
+ </div>
238
+ {/if}
239
+ <div class="flex flex-1 flex-col gap-1">
240
+ {#if result.metadata?.breadcrumbs && result.metadata.breadcrumbs.length > 0}
241
+ <div class="flex items-center gap-1 truncate text-stone-500 text-xs dark:text-stone-400">
242
+ {#each result.metadata.breadcrumbs as crumb, idx}
243
+ {#if idx > 0}<span class="text-stone-400"> &gt; </span>{/if}<span class="truncate">{crumb}</span>
244
+ {/each}
245
+ </div>
246
+ {/if}
247
+ <div class="truncate font-medium text-sm text-stone-900 dark:text-white">{result.header}</div>
248
+ {#if result.content}<div class="line-clamp-2 text-sm text-stone-600 dark:text-stone-400">{result.content}</div>{/if}
249
+ </div>
250
+ {#if activeIndex === i}<ChevronRight class="size-5 shrink-0 self-center text-stone-400 dark:text-stone-500" />{/if}
251
+ </div>
252
+ </button>
253
+ {/each}
254
+ {/if}
255
+
256
+ {#if showResults}
257
+ {#each results as result, i (result.id)}
258
+ {@const offset = showRecentSearches ? recentSearches.length : 0}
259
+ <button
260
+ type="button"
261
+ class="w-full text-left last:mb-2"
262
+ onclick={() => handleSelectOption(result)}
263
+ onmouseenter={() => activeIndex = offset + i}
264
+ onmouseleave={() => activeIndex = -1}
265
+ >
266
+ <div class={cn("flex w-full cursor-pointer items-start gap-2 rounded-xl border border-transparent bg-transparent px-3 py-2 text-stone-500 transition-colors focus:ring-0 focus:ring-offset-0", activeIndex === offset + i && "bg-stone-100 dark:bg-white/5")}>
267
+ {#if result.icon}
268
+ <div class="shrink-0 self-center text-stone-700 dark:text-stone-300">
269
+ {@render result.icon()}
270
+ </div>
271
+ {/if}
272
+ <div class="flex flex-1 flex-col gap-1">
273
+ {#if result.metadata?.breadcrumbs && result.metadata.breadcrumbs.length > 0}
274
+ <div class="flex items-center gap-1 truncate text-stone-500 text-xs dark:text-stone-400">
275
+ {#each result.metadata.breadcrumbs as crumb, idx}
276
+ {#if idx > 0}<span class="text-stone-400"> &gt; </span>{/if}<span class="truncate">{crumb}</span>
277
+ {/each}
278
+ </div>
279
+ {/if}
280
+ <div class="truncate font-medium text-sm text-stone-900 dark:text-white">{result.header}</div>
281
+ {#if result.content}<div class="line-clamp-2 text-sm text-stone-600 dark:text-stone-400">{result.content}</div>{/if}
282
+ </div>
283
+ {#if activeIndex === offset + i}<ChevronRight class="size-5 shrink-0 self-center text-stone-400 dark:text-stone-500" />{/if}
284
+ </div>
285
+ </button>
286
+ {/each}
287
+ {/if}
288
+
289
+ {#if isLoading && query}
290
+ <div class="flex flex-col items-center justify-center gap-3 px-2.5 py-12">
291
+ {#if loadingState}
292
+ {@render loadingState()}
293
+ {:else}
294
+ <Loader2 class="size-6 animate-spin text-stone-400 dark:text-stone-500" />
295
+ <div class="text-center">
296
+ <p class="font-medium text-sm text-stone-700 dark:text-stone-300">Searching...</p>
297
+ <p class="mt-1 text-stone-500 text-xs dark:text-stone-400">Finding results for "{query}"</p>
298
+ </div>
299
+ {/if}
300
+ </div>
301
+ {/if}
302
+
303
+ {#if showEmptyState}
304
+ <div class="flex flex-col items-center justify-center gap-3 px-2.5 py-12">
305
+ {#if emptyState}
306
+ {@render emptyState()}
307
+ {:else}
308
+ <SearchX class="size-6 text-stone-300 dark:text-stone-600" />
309
+ <div class="text-center">
310
+ <p class="font-medium text-sm text-stone-700 dark:text-stone-300">No results found</p>
311
+ <p class="mt-1 text-stone-500 text-xs dark:text-stone-400">Try searching with different keywords</p>
312
+ </div>
313
+ {/if}
314
+ </div>
315
+ {/if}
316
+ </div>
317
+ </div>
318
+ </div>
319
+ </div>
320
+ {/if}
@@ -0,0 +1,31 @@
1
+ import type { Snippet } from "svelte";
2
+ export type SearchResult = {
3
+ id: string;
4
+ header: string;
5
+ content: string;
6
+ link: string;
7
+ icon?: Snippet;
8
+ metadata?: {
9
+ breadcrumbs?: string[];
10
+ [key: string]: unknown;
11
+ };
12
+ };
13
+ export type SearchProps = {
14
+ onSearch?: (query: string) => void;
15
+ results?: SearchResult[];
16
+ isOpen: boolean;
17
+ onClose: () => void;
18
+ isLoading?: boolean;
19
+ placeholder?: string;
20
+ recentSearches?: SearchResult[];
21
+ onSelectResult?: (result: SearchResult, query: string) => void;
22
+ class?: string;
23
+ emptyState?: Snippet;
24
+ loadingState?: Snippet;
25
+ position?: "top" | "center";
26
+ paddingTop?: string;
27
+ ref?: HTMLInputElement | null;
28
+ };
29
+ declare const Search: import("svelte").Component<SearchProps, {}, "ref" | "isOpen">;
30
+ type Search = ReturnType<typeof Search>;
31
+ export default Search;
@@ -0,0 +1,82 @@
1
+ <script lang="ts">
2
+ import { Search } from "@lucide/svelte";
3
+ import { cn } from "../utils/cn";
4
+ import type { Snippet } from "svelte";
5
+ import type { HTMLButtonAttributes } from "svelte/elements";
6
+ import { getContext } from "svelte";
7
+
8
+ // Svelte props for SearchButton — replaces React ButtonHTMLAttributes + forwardRef
9
+ // In Svelte 5, forwardRef is replaced by bind:this with $bindable ref prop
10
+ export type SearchButtonProps = HTMLButtonAttributes & {
11
+ showShortcut?: boolean;
12
+ shortcutText?: string;
13
+ class?: string;
14
+ children?: Snippet;
15
+ // Svelte event handler — replaces React onClick prop
16
+ onclick?: (e: MouseEvent) => void;
17
+ // Svelte ref binding — replaces React forwardRef
18
+ ref?: HTMLElement | null;
19
+ };
20
+
21
+ let {
22
+ showShortcut = true,
23
+ shortcutText = "⌘K",
24
+ class: className,
25
+ children,
26
+ onclick,
27
+ ref = $bindable(null),
28
+ ...restProps
29
+ }: SearchButtonProps = $props();
30
+
31
+ // Svelte context — replaces React useSearch() / SearchContext
32
+ // When SearchProvider is ported, it will setContext with { open, close, toggle, isOpen }
33
+ type SearchContextValue = {
34
+ open: () => void;
35
+ close: () => void;
36
+ toggle: () => void;
37
+ isOpen: boolean;
38
+ };
39
+ const searchContext = getContext<SearchContextValue | null>("search-context");
40
+
41
+ // Svelte click handler — replaces React handleClick that called context?.open()
42
+ function handleClick(e: MouseEvent) {
43
+ // Svelte context open — mirrors React context?.open()
44
+ searchContext?.open();
45
+ onclick?.(e);
46
+ }
47
+ </script>
48
+
49
+ <button
50
+ bind:this={ref}
51
+ type="button"
52
+ class={cn(
53
+ "flex w-full items-center gap-2 px-3 py-2 text-left text-sm",
54
+ "bg-transparent",
55
+ "border border-stone-200 dark:border-white/10",
56
+ "rounded-xl",
57
+ "text-stone-500",
58
+ "hover:border-stone-300 dark:hover:border-white/20",
59
+ "focus:outline-none focus:ring-0 focus:ring-offset-0",
60
+ "transition-colors",
61
+ "cursor-pointer",
62
+ className
63
+ )}
64
+ onclick={handleClick}
65
+ {...restProps}
66
+ >
67
+ <Search class="shrink-0 text-stone-800 dark:text-stone-500" size={16} />
68
+ <span class="flex-1 text-stone-500 dark:text-stone-400">
69
+ {#if children}
70
+ {@render children()}
71
+ {:else}
72
+ Search...
73
+ {/if}
74
+ </span>
75
+ {#if showShortcut}
76
+ <kbd
77
+ class="hidden items-center gap-0.5 rounded px-1.5 py-0.5 font-medium font-sans text-stone-500 text-xs sm:inline-flex dark:text-stone-400"
78
+ >
79
+ {shortcutText}
80
+ </kbd>
81
+ {/if}
82
+ </button>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLButtonAttributes } from "svelte/elements";
3
+ export type SearchButtonProps = HTMLButtonAttributes & {
4
+ showShortcut?: boolean;
5
+ shortcutText?: string;
6
+ class?: string;
7
+ children?: Snippet;
8
+ onclick?: (e: MouseEvent) => void;
9
+ ref?: HTMLElement | null;
10
+ };
11
+ declare const SearchButton: import("svelte").Component<SearchButtonProps, {}, "ref">;
12
+ type SearchButton = ReturnType<typeof SearchButton>;
13
+ export default SearchButton;
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ import { setContext, onMount } from "svelte";
3
+ import Search from "./Search.svelte";
4
+ import type { SearchProps } from "./Search.svelte";
5
+ import type { Snippet } from "svelte";
6
+
7
+ // Svelte SearchContextValue — mirrors React SearchContextValue
8
+ export type SearchContextValue = {
9
+ isOpen: boolean;
10
+ open: () => void;
11
+ close: () => void;
12
+ toggle: () => void;
13
+ };
14
+
15
+ export type SearchProviderProps = {
16
+ children?: Snippet;
17
+ shortcutKey?: string;
18
+ requireModifier?: boolean;
19
+ searchProps: Omit<SearchProps, "isOpen" | "onClose">;
20
+ };
21
+
22
+ let {
23
+ children,
24
+ shortcutKey = "k",
25
+ requireModifier = true,
26
+ searchProps,
27
+ }: SearchProviderProps = $props();
28
+
29
+ // Svelte state — replaces React useState
30
+ let isOpen = $state(false);
31
+
32
+ // Svelte handlers — replaces React useCallback
33
+ function open() {
34
+ isOpen = true;
35
+ }
36
+ function close() {
37
+ isOpen = false;
38
+ }
39
+ function toggle() {
40
+ isOpen = !isOpen;
41
+ }
42
+
43
+ // Svelte context — replaces React createContext + Provider
44
+ // Use getter for isOpen so consumers see reactive value
45
+ const contextValue: SearchContextValue = {
46
+ get isOpen() {
47
+ return isOpen;
48
+ },
49
+ open,
50
+ close,
51
+ toggle,
52
+ };
53
+ setContext("search-context", contextValue);
54
+
55
+ // Svelte effect — replaces React useEffect for global shortcut
56
+ $effect(() => {
57
+ const handleKeyDown = (e: KeyboardEvent) => {
58
+ const modifierPressed = e.metaKey || e.ctrlKey;
59
+ const shouldTrigger = requireModifier
60
+ ? modifierPressed && e.key.toLowerCase() === shortcutKey.toLowerCase()
61
+ : e.key.toLowerCase() === shortcutKey.toLowerCase();
62
+
63
+ if (shouldTrigger) {
64
+ // Skip if user is typing in editable element and no modifier
65
+ if (!modifierPressed) {
66
+ const target = e.target as HTMLElement;
67
+ const isEditable =
68
+ target.tagName === "INPUT" ||
69
+ target.tagName === "TEXTAREA" ||
70
+ target.isContentEditable;
71
+ if (isEditable) return;
72
+ }
73
+ e.preventDefault();
74
+ toggle();
75
+ }
76
+ };
77
+
78
+ window.addEventListener("keydown", handleKeyDown);
79
+ return () => window.removeEventListener("keydown", handleKeyDown);
80
+ });
81
+ </script>
82
+
83
+ {#if children}
84
+ {@render children()}
85
+ {/if}
86
+ <!-- @ts-ignore - searchProps contains required Search props -->
87
+ <Search {...searchProps} bind:isOpen onClose={close} />
@@ -0,0 +1,17 @@
1
+ import type { SearchProps } from "./Search.svelte";
2
+ import type { Snippet } from "svelte";
3
+ export type SearchContextValue = {
4
+ isOpen: boolean;
5
+ open: () => void;
6
+ close: () => void;
7
+ toggle: () => void;
8
+ };
9
+ export type SearchProviderProps = {
10
+ children?: Snippet;
11
+ shortcutKey?: string;
12
+ requireModifier?: boolean;
13
+ searchProps: Omit<SearchProps, "isOpen" | "onClose">;
14
+ };
15
+ declare const SearchProvider: import("svelte").Component<SearchProviderProps, {}, "">;
16
+ type SearchProvider = ReturnType<typeof SearchProvider>;
17
+ export default SearchProvider;