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.
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/components/Search.svelte +320 -0
- package/dist/components/Search.svelte.d.ts +31 -0
- package/dist/components/SearchButton.svelte +82 -0
- package/dist/components/SearchButton.svelte.d.ts +13 -0
- package/dist/components/SearchProvider.svelte +87 -0
- package/dist/components/SearchProvider.svelte.d.ts +17 -0
- package/dist/components/accordion/Accordion.svelte +150 -0
- package/dist/components/accordion/Accordion.svelte.d.ts +25 -0
- package/dist/components/accordion/AccordionCover.svelte +68 -0
- package/dist/components/accordion/AccordionCover.svelte.d.ts +13 -0
- package/dist/components/accordion/AccordionGroup.svelte +23 -0
- package/dist/components/accordion/AccordionGroup.svelte.d.ts +8 -0
- package/dist/components/accordion/accordion-url-utils.d.ts +4 -0
- package/dist/components/accordion/accordion-url-utils.js +48 -0
- package/dist/components/accordion/index.d.ts +29 -0
- package/dist/components/accordion/index.js +6 -0
- package/dist/components/badge/Badge.svelte +152 -0
- package/dist/components/badge/Badge.svelte.d.ts +23 -0
- package/dist/components/badge/index.d.ts +1 -0
- package/dist/components/badge/index.js +2 -0
- package/dist/components/callout/Callout.svelte +317 -0
- package/dist/components/callout/Callout.svelte.d.ts +17 -0
- package/dist/components/callout/Check.svelte +9 -0
- package/dist/components/callout/Check.svelte.d.ts +4 -0
- package/dist/components/callout/Danger.svelte +9 -0
- package/dist/components/callout/Danger.svelte.d.ts +4 -0
- package/dist/components/callout/Info.svelte +9 -0
- package/dist/components/callout/Info.svelte.d.ts +4 -0
- package/dist/components/callout/Note.svelte +9 -0
- package/dist/components/callout/Note.svelte.d.ts +4 -0
- package/dist/components/callout/Tip.svelte +9 -0
- package/dist/components/callout/Tip.svelte.d.ts +4 -0
- package/dist/components/callout/Warning.svelte +9 -0
- package/dist/components/callout/Warning.svelte.d.ts +4 -0
- package/dist/components/callout/index.d.ts +8 -0
- package/dist/components/callout/index.js +8 -0
- package/dist/components/card/Card.svelte +172 -0
- package/dist/components/card/Card.svelte.d.ts +21 -0
- package/dist/components/card/index.d.ts +3 -0
- package/dist/components/card/index.js +2 -0
- package/dist/components/code-block/BaseCodeBlock.svelte +249 -0
- package/dist/components/code-block/BaseCodeBlock.svelte.d.ts +10 -0
- package/dist/components/code-block/CodeBlock.svelte +185 -0
- package/dist/components/code-block/CodeBlock.svelte.d.ts +5 -0
- package/dist/components/code-block/CodeFooter.svelte +34 -0
- package/dist/components/code-block/CodeFooter.svelte.d.ts +8 -0
- package/dist/components/code-block/CodeHeader.svelte +52 -0
- package/dist/components/code-block/CodeHeader.svelte.d.ts +11 -0
- package/dist/components/code-block/CopyToClipboardButton.svelte +124 -0
- package/dist/components/code-block/CopyToClipboardButton.svelte.d.ts +4 -0
- package/dist/components/code-block/index.d.ts +10 -0
- package/dist/components/code-block/index.js +5 -0
- package/dist/components/code-block/types.d.ts +85 -0
- package/dist/components/code-block/types.js +1 -0
- package/dist/components/code-group/CodeGroup.svelte +327 -0
- package/dist/components/code-group/CodeGroup.svelte.d.ts +24 -0
- package/dist/components/code-group/CodeGroupSelect.svelte +170 -0
- package/dist/components/code-group/CodeGroupSelect.svelte.d.ts +25 -0
- package/dist/components/code-group/CodeSelectDropdown.svelte +74 -0
- package/dist/components/code-group/CodeSelectDropdown.svelte.d.ts +9 -0
- package/dist/components/code-group/LanguageDropdown.svelte +134 -0
- package/dist/components/code-group/LanguageDropdown.svelte.d.ts +11 -0
- package/dist/components/code-group/LanguageIcon.svelte +24 -0
- package/dist/components/code-group/LanguageIcon.svelte.d.ts +8 -0
- package/dist/components/code-group/context.d.ts +14 -0
- package/dist/components/code-group/context.js +2 -0
- package/dist/components/code-group/index.d.ts +8 -0
- package/dist/components/code-group/index.js +4 -0
- package/dist/components/color/Color.svelte +34 -0
- package/dist/components/color/Color.svelte.d.ts +10 -0
- package/dist/components/color/ColorItem.svelte +140 -0
- package/dist/components/color/ColorItem.svelte.d.ts +10 -0
- package/dist/components/color/ColorRow.svelte +27 -0
- package/dist/components/color/ColorRow.svelte.d.ts +8 -0
- package/dist/components/color/index.d.ts +1 -0
- package/dist/components/color/index.js +8 -0
- package/dist/components/columns/Box.svelte +15 -0
- package/dist/components/columns/Box.svelte.d.ts +7 -0
- package/dist/components/columns/Columns.svelte +31 -0
- package/dist/components/columns/Columns.svelte.d.ts +10 -0
- package/dist/components/columns/constants.d.ts +5 -0
- package/dist/components/columns/constants.js +3 -0
- package/dist/components/columns/index.d.ts +5 -0
- package/dist/components/columns/index.js +3 -0
- package/dist/components/expandable/Expandable.svelte +182 -0
- package/dist/components/expandable/Expandable.svelte.d.ts +22 -0
- package/dist/components/expandable/index.d.ts +3 -0
- package/dist/components/expandable/index.js +2 -0
- package/dist/components/frame/Frame.svelte +118 -0
- package/dist/components/frame/Frame.svelte.d.ts +15 -0
- package/dist/components/frame/index.d.ts +3 -0
- package/dist/components/frame/index.js +2 -0
- package/dist/components/icon/Icon.svelte +129 -0
- package/dist/components/icon/Icon.svelte.d.ts +18 -0
- package/dist/components/icon/index.d.ts +2 -0
- package/dist/components/icon/index.js +1 -0
- package/dist/components/index.d.ts +22 -0
- package/dist/components/index.js +22 -0
- package/dist/components/mermaid/Mermaid.svelte +181 -0
- package/dist/components/mermaid/Mermaid.svelte.d.ts +11 -0
- package/dist/components/mermaid/ZoomControls.svelte +74 -0
- package/dist/components/mermaid/ZoomControls.svelte.d.ts +12 -0
- package/dist/components/mermaid/index.d.ts +7 -0
- package/dist/components/mermaid/index.js +4 -0
- package/dist/components/mermaid/use-pan-zoom.svelte.d.ts +11 -0
- package/dist/components/mermaid/use-pan-zoom.svelte.js +41 -0
- package/dist/components/panel/Panel.svelte +27 -0
- package/dist/components/panel/Panel.svelte.d.ts +9 -0
- package/dist/components/panel/index.d.ts +3 -0
- package/dist/components/panel/index.js +2 -0
- package/dist/components/property/DeprecatedPill.svelte +16 -0
- package/dist/components/property/DeprecatedPill.svelte.d.ts +6 -0
- package/dist/components/property/InfoPill.svelte +32 -0
- package/dist/components/property/InfoPill.svelte.d.ts +10 -0
- package/dist/components/property/ParamHead.svelte +188 -0
- package/dist/components/property/ParamHead.svelte.d.ts +22 -0
- package/dist/components/property/Property.svelte +123 -0
- package/dist/components/property/Property.svelte.d.ts +24 -0
- package/dist/components/property/RequiredPill.svelte +16 -0
- package/dist/components/property/RequiredPill.svelte.d.ts +6 -0
- package/dist/components/property/index.d.ts +11 -0
- package/dist/components/property/index.js +6 -0
- package/dist/components/search/index.d.ts +7 -0
- package/dist/components/search/index.js +4 -0
- package/dist/components/search/use-search.d.ts +9 -0
- package/dist/components/search/use-search.js +6 -0
- package/dist/components/steps/Step.svelte +314 -0
- package/dist/components/steps/Step.svelte.d.ts +26 -0
- package/dist/components/steps/Steps.svelte +51 -0
- package/dist/components/steps/Steps.svelte.d.ts +11 -0
- package/dist/components/steps/constants.d.ts +4 -0
- package/dist/components/steps/constants.js +2 -0
- package/dist/components/steps/context.d.ts +10 -0
- package/dist/components/steps/context.js +2 -0
- package/dist/components/steps/index.d.ts +32 -0
- package/dist/components/steps/index.js +6 -0
- package/dist/components/tabs/Tabs.svelte +254 -0
- package/dist/components/tabs/Tabs.svelte.d.ts +16 -0
- package/dist/components/tabs/TabsItem.svelte +56 -0
- package/dist/components/tabs/TabsItem.svelte.d.ts +15 -0
- package/dist/components/tabs/context.d.ts +20 -0
- package/dist/components/tabs/context.js +2 -0
- package/dist/components/tabs/index.d.ts +27 -0
- package/dist/components/tabs/index.js +5 -0
- package/dist/components/tile/Tile.svelte +124 -0
- package/dist/components/tile/Tile.svelte.d.ts +12 -0
- package/dist/components/tile/index.d.ts +2 -0
- package/dist/components/tile/index.js +1 -0
- package/dist/components/tooltip/Tooltip.svelte +170 -0
- package/dist/components/tooltip/Tooltip.svelte.d.ts +23 -0
- package/dist/components/tooltip/index.d.ts +2 -0
- package/dist/components/tooltip/index.js +1 -0
- package/dist/components/tree/Tree.svelte +226 -0
- package/dist/components/tree/Tree.svelte.d.ts +9 -0
- package/dist/components/tree/TreeFile.svelte +44 -0
- package/dist/components/tree/TreeFile.svelte.d.ts +6 -0
- package/dist/components/tree/TreeFolder.svelte +107 -0
- package/dist/components/tree/TreeFolder.svelte.d.ts +10 -0
- package/dist/components/tree/context.d.ts +3 -0
- package/dist/components/tree/context.js +7 -0
- package/dist/components/tree/index.d.ts +10 -0
- package/dist/components/tree/index.js +9 -0
- package/dist/components/tree/tree-level-context.d.ts +8 -0
- package/dist/components/tree/tree-level-context.js +7 -0
- package/dist/components/tree/use-tree-level.d.ts +3 -0
- package/dist/components/tree/use-tree-level.js +5 -0
- package/dist/components/tree/utils.d.ts +9 -0
- package/dist/components/tree/utils.js +86 -0
- package/dist/components/update/Update.svelte +210 -0
- package/dist/components/update/Update.svelte.d.ts +34 -0
- package/dist/components/update/index.d.ts +4 -0
- package/dist/components/update/index.js +3 -0
- package/dist/components/view/View.svelte +52 -0
- package/dist/components/view/View.svelte.d.ts +14 -0
- package/dist/components/view/index.d.ts +3 -0
- package/dist/components/view/index.js +2 -0
- package/dist/constants/brands.d.ts +2 -0
- package/dist/constants/brands.js +4 -0
- package/dist/constants/index.d.ts +6 -0
- package/dist/constants/index.js +6 -0
- package/dist/constants/selectors.d.ts +125 -0
- package/dist/constants/selectors.js +125 -0
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/use-expandable-memory.svelte.d.ts +6 -0
- package/dist/hooks/use-expandable-memory.svelte.js +89 -0
- package/dist/hooks/use-expandable.svelte.d.ts +15 -0
- package/dist/hooks/use-expandable.svelte.js +53 -0
- package/dist/hooks/use-get-shiki-highlighted-html.svelte.d.ts +8 -0
- package/dist/hooks/use-get-shiki-highlighted-html.svelte.js +36 -0
- package/dist/hooks/use-has-hover.d.ts +2 -0
- package/dist/hooks/use-has-hover.js +10 -0
- package/dist/hooks/use-is-dark-theme.svelte.d.ts +3 -0
- package/dist/hooks/use-is-dark-theme.svelte.js +25 -0
- package/dist/hooks/use-isomorphic-layout-effect.d.ts +3 -0
- package/dist/hooks/use-isomorphic-layout-effect.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/styles.css +2 -0
- package/dist/utils/cn.d.ts +3 -0
- package/dist/utils/cn.js +7 -0
- package/dist/utils/copy-to-clipboard.d.ts +4 -0
- package/dist/utils/copy-to-clipboard.js +50 -0
- package/dist/utils/dedent.d.ts +1 -0
- package/dist/utils/dedent.js +125 -0
- package/dist/utils/enhance-video-props.d.ts +4 -0
- package/dist/utils/enhance-video-props.js +25 -0
- package/dist/utils/get-node-text.d.ts +2 -0
- package/dist/utils/get-node-text.js +26 -0
- package/dist/utils/icon-utils.d.ts +18 -0
- package/dist/utils/icon-utils.js +30 -0
- package/dist/utils/is-absolute-url.d.ts +4 -0
- package/dist/utils/is-absolute-url.js +11 -0
- package/dist/utils/is-remote-url.d.ts +2 -0
- package/dist/utils/is-remote-url.js +12 -0
- package/dist/utils/make-marker-ids-unique.d.ts +2 -0
- package/dist/utils/make-marker-ids-unique.js +36 -0
- package/dist/utils/shiki/code-styling.d.ts +13 -0
- package/dist/utils/shiki/code-styling.js +1 -0
- package/dist/utils/shiki/constants.d.ts +21 -0
- package/dist/utils/shiki/constants.js +116 -0
- package/dist/utils/shiki/get-code-styling.d.ts +9 -0
- package/dist/utils/shiki/get-code-styling.js +38 -0
- package/dist/utils/shiki/get-shiki-background-colors.d.ts +20 -0
- package/dist/utils/shiki/get-shiki-background-colors.js +56 -0
- package/dist/utils/shiki/index.d.ts +30 -0
- package/dist/utils/shiki/index.js +209 -0
- package/dist/utils/shiki/lib.d.ts +4 -0
- package/dist/utils/shiki/lib.js +49 -0
- package/dist/utils/shiki/shiki-themes.d.ts +2 -0
- package/dist/utils/shiki/shiki-themes.js +64 -0
- package/dist/utils/shiki/snippet-presets.d.ts +27 -0
- package/dist/utils/shiki/snippet-presets.js +44 -0
- package/dist/utils/shiki/worker-client.d.ts +9 -0
- package/dist/utils/shiki/worker-client.js +22 -0
- package/dist/utils/shiki/worker.d.ts +1 -0
- package/dist/utils/shiki/worker.js +134 -0
- package/dist/utils/slugify.d.ts +2 -0
- package/dist/utils/slugify.js +9 -0
- 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"> > </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"> > </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;
|