Skip to content

Commit 0720e4a

Browse files
authored
Added search to docs (#102)
* Added search * fix? * output * Test with vercel config * done? * test with vercel.json * stuff * Overhaul, modal stuffs * search * Removed debounce for shits and giggles * Better init * Use `focus-trap` for correctly managing focus * Moved drawer to global dialog system with types * Fixed broken search * Updated plugin dep * Updated vite-plugin-pagefind * Dep update * Fixed pnpm check issue, updated vite-plugin-svelte * update deps * fixed portal * Updated styles for SearchDialog.
1 parent 7d0aaff commit 0720e4a

20 files changed

+582
-237
lines changed

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ node_modules
88
.env.*
99
!.env.example
1010
/coverage
11+
**/pagefind
1112

1213
# Ignore files for PNPM, NPM and YARN
1314
pnpm-lock.yaml
1415
package-lock.json
1516
yarn.lock
17+

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ node_modules
1010
vite.config.js.timestamp-*
1111
vite.config.ts.timestamp-*
1212
/coverage
13+
**/pagefind

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ yarn.lock
55
/coverage
66
**/*.mdx
77
**/*.md
8+
**/pagefind

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"dev": "vite dev",
77
"preview": "vite preview",
88
"build": "pnpm build:docs && pnpm build:package",
9-
"build:docs": "svelte-kit sync && vite build",
9+
"build:docs": "svelte-kit sync && vite build && pagefind",
1010
"build:package": "svelte-kit sync && svelte-package && publint",
1111
"ci:publish": "pnpm build:package && changeset publish",
1212
"test": "vitest run --coverage",
@@ -39,7 +39,7 @@
3939
"@sveltejs/adapter-static": "^3.0.1",
4040
"@sveltejs/kit": "^2.0.0",
4141
"@sveltejs/package": "^2.0.0",
42-
"@sveltejs/vite-plugin-svelte": "4.0.0-next.1",
42+
"@sveltejs/vite-plugin-svelte": "4.0.0-next.2",
4343
"@testing-library/jest-dom": "^6.4.2",
4444
"@testing-library/svelte": "^5.1.0",
4545
"@types/eslint": "^8.56.0",
@@ -51,8 +51,10 @@
5151
"eslint": "^8.56.0",
5252
"eslint-config-prettier": "^9.1.0",
5353
"eslint-plugin-svelte": "^2.36.0-next.4",
54+
"focus-trap": "^7.5.4",
5455
"jsdom": "^24.0.0",
5556
"lucide-svelte": "^0.373.0",
57+
"pagefind": "^1.1.0",
5658
"postcss": "^8.4.32",
5759
"postcss-load-config": "^5.0.2",
5860
"prettier": "^3.1.1",
@@ -65,6 +67,7 @@
6567
"tslib": "^2.4.1",
6668
"typescript": "^5.0.0",
6769
"vite": "^5.0.11",
70+
"vite-plugin-pagefind": "^0.2.8",
6871
"vitest": "^1.2.0"
6972
},
7073
"svelte": "./dist/index.js",

pagefind.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"site": "build",
3+
"exclude_selectors": [".expressive-code"],
4+
"vite_plugin_pagefind": {
5+
"assets_dir": "static",
6+
"build_command": "pnpm build:docs",
7+
"dev_strategy": "lazy"
8+
}
9+
}

pnpm-lock.yaml

Lines changed: 255 additions & 184 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ declare global {
66
// interface Locals {}
77
interface PageData {
88
highlighter: import('shiki').Highlighter;
9+
pagefind: import('vite-plugin-pagefind/types').Pagefind;
910
}
1011
// interface PageState {}
1112
// interface Platform {}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<script lang="ts">
2+
import { type Snippet } from 'svelte';
3+
import { createFocusTrap } from 'focus-trap';
4+
import Portal from '../Portal/Portal.svelte';
5+
import { fade, fly } from 'svelte/transition';
6+
import { cubicOut } from 'svelte/easing';
7+
import { beforeNavigate } from '$app/navigation';
8+
9+
interface Props {
10+
children: Snippet;
11+
type?: 'modal' | 'drawer';
12+
open?: boolean;
13+
}
14+
15+
let { children, type = 'modal', open = $bindable(false) }: Props = $props();
16+
17+
$effect(() => {
18+
if (open) {
19+
document.body.style.overflow = 'hidden';
20+
} else {
21+
document.body.style.overflow = '';
22+
}
23+
24+
return () => {
25+
document.body.style.overflow = '';
26+
};
27+
});
28+
29+
function focus_trap(node: HTMLElement) {
30+
const trap = createFocusTrap(node, {
31+
returnFocusOnDeactivate: true,
32+
preventScroll: true,
33+
escapeDeactivates: () => {
34+
open = false;
35+
return true;
36+
},
37+
allowOutsideClick: () => {
38+
open = false;
39+
return true;
40+
},
41+
});
42+
trap.activate();
43+
return {
44+
destroy() {
45+
trap.deactivate();
46+
},
47+
};
48+
}
49+
50+
beforeNavigate(() => {
51+
open = false;
52+
});
53+
54+
const commonClasses = 'fixed z-50';
55+
56+
const classes = $derived(
57+
{
58+
drawer: `${commonClasses} top-0 left-0 right-0 bottom-0 h-screen w-fit max-w-[500px]`,
59+
modal: `${commonClasses} top-4 md:top-[15%] left-1/2 -translate-x-1/2 w-[calc(100%-2rem)] max-w-[500px] rounded-md bg-surface-700 max-h-[75vh] max-w-[800px] overflow-auto`,
60+
}[type],
61+
);
62+
63+
const flyParams = $derived(
64+
{
65+
drawer: { x: -500, easing: cubicOut, duration: 250 },
66+
modal: { y: 50, easing: cubicOut, duration: 250 },
67+
}[type],
68+
);
69+
</script>
70+
71+
<Portal>
72+
{#if open}
73+
<div in:fade={{ duration: 250 }} class="fixed inset-0 z-50 bg-black bg-opacity-50"></div>
74+
<div in:fly={flyParams} class={classes} role="dialog" aria-modal="true" use:focus_trap>
75+
{@render children()}
76+
</div>
77+
{/if}
78+
</Portal>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<script lang="ts">
2+
import Dialog from './Dialog.svelte';
3+
import SearchIcon from 'lucide-svelte/icons/search';
4+
import LoaderIcon from 'lucide-svelte/icons/loader';
5+
import { page } from '$app/stores';
6+
7+
let open = $state(false);
8+
let query = $state('');
9+
10+
const searchPromise = $derived.by(async () => {
11+
if (query === '') {
12+
return [];
13+
}
14+
15+
// FIXME: https://github.com/sveltejs/eslint-plugin-svelte/issues/652
16+
// eslint-disable-next-line svelte/valid-compile
17+
const result = await $page.data.pagefind.debouncedSearch(query, {}, 250);
18+
19+
if (result === null) {
20+
return [];
21+
}
22+
23+
return await Promise.all(result.results.map((result) => result.data()));
24+
});
25+
26+
$effect(() => {
27+
function onKeydown(event: KeyboardEvent) {
28+
if (event.key === 'k' && (event.ctrlKey || event.metaKey)) {
29+
event.preventDefault();
30+
open = true;
31+
}
32+
}
33+
34+
document.addEventListener('keydown', onKeydown);
35+
36+
return () => {
37+
document.removeEventListener('keydown', onKeydown);
38+
};
39+
});
40+
</script>
41+
42+
<button
43+
class="flex gap-4 items-center border border-surface-600 px-4 py-1 rounded-md"
44+
onclick={() => (open = true)}
45+
>
46+
<p class="hidden md:block text-lg">Search</p>
47+
<SearchIcon size={20} />
48+
</button>
49+
50+
<Dialog bind:open>
51+
<div class="divide-y-4 divide-surface-600">
52+
<div class="relative p-4">
53+
<input
54+
class="bg-transparent text-2xl outline-none pl-10 w-full"
55+
placeholder="Search the docs..."
56+
bind:value={query}
57+
/>
58+
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" />
59+
</div>
60+
<div class="flex flex-col gap-1 p-4" tabindex="-1">
61+
{#if query === ''}
62+
<p class="text-lg text-center py-16">What can we help you find?</p>
63+
{:else}
64+
{#await searchPromise}
65+
<p class="text-lg text-center py-16">
66+
<LoaderIcon class="inline animate-spin" size={16} />
67+
</p>
68+
{:then results}
69+
{#if results.length > 0}
70+
<nav>
71+
<ul class="space-y-4">
72+
{#each results as result}
73+
<li>
74+
<a
75+
href={result.url.replace('.html', '')}
76+
class="block text-xl font-semibold hover:bg-surface-500 focus:bg-surface-500 transition p-1 rounded-md"
77+
>
78+
{result.meta.title}
79+
80+
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
81+
<p class="text-sm line-clamp-2">{@html result.excerpt}</p>
82+
</a>
83+
</li>
84+
{/each}
85+
</ul>
86+
</nav>
87+
{:else if query !== ''}
88+
<p class="text-lg text-center py-16">No results found for "{query}"</p>
89+
{/if}
90+
{/await}
91+
{/if}
92+
</div>
93+
</div>
94+
</Dialog>
95+
96+
<style lang="postcss">
97+
:global(mark) {
98+
@apply bg-rose-400 rounded-md px-0.5;
99+
}
100+
</style>

src/docs/components/Navigation/Navigation.svelte

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { page } from '$app/stores';
3-
import { drawer } from '$docs/stores.svelte';
3+
import { getDrawer } from '$docs/stores.svelte';
44
// Icons (Docs)
55
import IconGetStarted from 'lucide-svelte/icons/rocket';
66
// Icons (Examples)
@@ -53,9 +53,19 @@
5353
},
5454
];
5555
56+
const drawer = getDrawer();
57+
5658
// FIXME: Remove when Svelte 5 supports $page, see: https://github.com/sveltejs/eslint-plugin-svelte/issues/652
5759
// eslint-disable-next-line svelte/valid-compile
5860
const navActive = (href: string) => $page.route.id?.replace('/(inner)', '') == href;
61+
62+
$effect(() => {
63+
const close = () => (drawer.open = false);
64+
window.addEventListener('resize', close);
65+
return () => {
66+
window.removeEventListener('resize', close);
67+
};
68+
});
5969
</script>
6070

6171
<div
@@ -81,7 +91,7 @@
8191
href={link.href}
8292
class="grid grid-cols-[24px_1fr] items-center gap-4 rounded-tr-xl rounded-br-xl px-4 py-3 text-left hover:bg-surface-500/20"
8393
class:nav-active={navActive(link.href)}
84-
onclick={() => drawer.close()}
94+
onclick={() => (drawer.open = false)}
8595
>
8696
<svelte:component this={link.icon} size={24} />
8797
<span>{link.label}</span>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
4+
interface Props {
5+
children: Snippet;
6+
}
7+
8+
let { children }: Props = $props();
9+
</script>
10+
11+
<div data-portal-target></div>
12+
13+
<div data-body>
14+
{@render children()}
15+
</div>

src/docs/components/PageHeader/PageHeader.svelte

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
<script lang="ts">
22
import { version } from '$app/environment';
3-
import { drawer } from '$docs/stores.svelte';
43
54
// Icons
65
import IconMenu from 'lucide-svelte/icons/menu';
6+
import SearchDialog from '../Dialog/SearchDialog.svelte';
7+
import { getDrawer } from '$docs/stores.svelte.js';
8+
9+
const drawer = getDrawer();
710
</script>
811

912
<header
1013
class="sticky top-0 z-10 bg-surface-200/75 dark:bg-surface-800/75 backdrop-blur border-b border-surface-500/20"
1114
>
1215
<div class="container mx-auto flex justify-between gap-4 p-4 lg:px-32">
1316
<div class="flex items-center gap-4">
14-
<button type="button" class="inline-block lg:hidden" onclick={() => drawer.toggle()}>
17+
<button
18+
type="button"
19+
class="inline-block lg:hidden"
20+
onclick={() => (drawer.open = !drawer.open)}
21+
>
1522
<IconMenu />
1623
</button>
1724
<a href="/" class="font-bold">Floating UI Svelte</a>
1825
</div>
26+
<SearchDialog />
1927
<div class="flex items-center gap-4">
2028
<a
2129
href="https://github.com/skeletonlabs/floating-ui-svelte"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
4+
interface Props {
5+
children: Snippet;
6+
}
7+
8+
let { children }: Props = $props();
9+
10+
let element: HTMLElement | null = $state(null);
11+
12+
$effect.pre(() => {
13+
const target = document.querySelector('[data-portal-target]');
14+
if (element === null || target === null) {
15+
return;
16+
}
17+
18+
target.appendChild(element);
19+
20+
return () => {
21+
if (element === null) {
22+
return;
23+
}
24+
element.remove();
25+
};
26+
});
27+
</script>
28+
29+
<div class="contents" bind:this={element}>
30+
{@render children()}
31+
</div>

src/docs/stores.svelte.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1+
import { getContext, setContext } from 'svelte';
2+
13
// Reusable State Stores
24

35
// Navigation Drawer ---
6+
type Drawer = { open: boolean };
47

5-
function createDrawer() {
6-
let value = $state(false);
7-
return {
8-
get value() {
9-
return value;
10-
},
11-
toggle: () => (value = !value),
12-
close: () => (value = false),
13-
};
14-
}
8+
const drawerKey = Symbol('drawer');
159

16-
export const drawer = createDrawer();
10+
export const getDrawer = () => getContext<Drawer>(drawerKey);
11+
export const setDrawer = (drawer: Drawer) => setContext(drawerKey, drawer);

0 commit comments

Comments
 (0)