Skip to content

Commit d70eef7

Browse files
Hugos68AdrianGonz97endigo9740
authored
NEXT Documentation site: Search (#2677)
Co-authored-by: AdrianGonz97 <[email protected]> Co-authored-by: endigo9740 <[email protected]>
1 parent 5f4a67f commit d70eef7

14 files changed

+8318
-6447
lines changed

pnpm-lock.yaml

+8,036-6,386
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sites/next.skeleton.dev/.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@ pnpm-debug.log*
2121
.DS_Store
2222

2323
# vercel
24-
.vercel
24+
.vercel
25+
26+
# pagefind
27+
**/pagefind

sites/next.skeleton.dev/astro.config.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import AutoImport from 'astro-auto-import';
1212
import mdx from '@astrojs/mdx';
1313
// Vite Plugins
1414
import skeletonPluginWatcher from 'vite-plugin-tw-plugin-watcher';
15+
import pagefind from 'vite-plugin-pagefind';
1516

1617
// https://astro.build/config
1718
export default defineConfig({
@@ -61,6 +62,9 @@ export default defineConfig({
6162
mdx(),
6263
],
6364
vite: {
64-
plugins: [skeletonPluginWatcher(path.resolve(path.join('..', '..', 'packages', 'skeleton', 'src', 'plugin')))],
65+
plugins: [
66+
skeletonPluginWatcher(path.resolve(path.join('..', '..', 'packages', 'skeleton', 'src', 'plugin'))),
67+
pagefind(),
68+
],
6569
},
6670
});

sites/next.skeleton.dev/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"dev:react": "pnpm -F @skeletonlabs/skeleton-react package:watch",
1616
"dev:astro": "astro dev",
1717
"check": "astro check",
18-
"build": "pnpm -F '@skeletonlabs/*' package && astro check && astro build",
18+
"build": "pnpm -F '@skeletonlabs/*' package && astro check && astro build && pagefind",
1919
"preview": "astro preview",
2020
"astro": "astro",
2121
"format": "prettier --write ."
@@ -51,8 +51,10 @@
5151
"@tailwindcss/typography": "^0.5.12",
5252
"@types/node": "^20.12.4",
5353
"minimatch": "^9.0.4",
54+
"pagefind": "^1.1.0",
5455
"prettier": "^3.2.5",
5556
"prettier-plugin-astro": "^0.13.0",
57+
"vite-plugin-pagefind": "^0.2.6",
5658
"vite-plugin-tw-plugin-watcher": "workspace:*"
5759
}
5860
}

sites/next.skeleton.dev/pagefind.json

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

sites/next.skeleton.dev/src/components/docs/FrameworkPicker.astro

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
import { Icon } from 'astro-icon/components';
3-
import { frameworks } from 'src/utils';
3+
import { frameworks } from 'src/stores/preferred-framework';
44
55
function getPathAndFramework(pathname: string) {
66
const parts = pathname.split('/').filter(Boolean);
@@ -38,12 +38,12 @@ const cHover =
3838
}
3939

4040
<script>
41-
import { isFramework, setPreferredFramework } from 'src/utils';
41+
import { isFramework, preferredFrameworkStore } from 'src/stores/preferred-framework';
4242

4343
for (const frameworkTab of document.querySelectorAll('[data-framework]')) {
4444
const framework = frameworkTab.getAttribute('data-framework');
4545
if (!framework) continue;
4646
if (!isFramework(framework)) continue;
47-
frameworkTab.addEventListener('click', () => setPreferredFramework(framework));
47+
frameworkTab.addEventListener('click', () => preferredFrameworkStore.set(framework));
4848
}
4949
</script>

sites/next.skeleton.dev/src/components/docs/Header.astro

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
22
import { Icon } from 'astro-icon/components';
3-
import { Search } from 'lucide-react';
43
import Lightswitch from '@components/docs/Lightswitch.astro';
54
import ThemeSwitch from '@components/docs/ThemeSwitch.astro';
5+
import Search from './Search.svelte';
66
77
const socialLinks = [
88
{
@@ -34,10 +34,7 @@ const socialLinks = [
3434
<!-- Middle -->
3535
<div class="flex items-center gap-2">
3636
<!-- Search -->
37-
<button type="button" class="btn preset-outlined-surface-200-800 hover:preset-tonal" onclick="onSearch()">
38-
<Search size={18} className="opacity-60" />
39-
<span class="opacity-60">Search...</span>
40-
</button>
37+
<Search client:load />
4138
</div>
4239
<!-- Right -->
4340
<div class="flex justify-end items-center gap-2">
@@ -58,9 +55,3 @@ const socialLinks = [
5855
</div>
5956
</div>
6057
</header>
61-
62-
<script is:inline>
63-
function onSearch() {
64-
window.alert('Search is not currently available. Please check back soon.');
65-
}
66-
</script>

sites/next.skeleton.dev/src/components/docs/Navigation.astro

+3-4
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,12 @@ const navigation = {
6161
</aside>
6262

6363
<script>
64-
import { getPreferredFramework } from 'src/utils';
65-
66-
const framework = getPreferredFramework();
64+
import { preferredFrameworkStore } from 'src/stores/preferred-framework';
65+
const preferredFramework = preferredFrameworkStore.get();
6766

6867
// Update all links ending with meta to use the selected framework
6968
for (const anchor of document.querySelectorAll<HTMLAnchorElement>('a')) {
7069
if (!anchor.href.endsWith('meta')) continue;
71-
anchor.setAttribute('href', anchor.href.replace('meta', framework));
70+
anchor.setAttribute('href', anchor.href.replace('meta', preferredFramework));
7271
}
7372
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
<script lang="ts">
2+
import { untrack } from 'svelte';
3+
import type { Pagefind } from 'vite-plugin-pagefind/types';
4+
// Utils
5+
import { docSearchSettingsStore } from 'src/stores/doc-search-settings';
6+
import { frameworks, isFramework, preferredFrameworkStore } from 'src/stores/preferred-framework';
7+
// Icons
8+
import IconSearch from 'lucide-svelte/icons/search';
9+
import IconFilter from 'lucide-svelte/icons/filter';
10+
import IconBook from 'lucide-svelte/icons/book';
11+
import IconHash from 'lucide-svelte/icons/hash';
12+
import IconLoader from 'lucide-svelte/icons/loader';
13+
import IconChevronRight from 'lucide-svelte/icons/chevron-right';
14+
15+
let dialog: HTMLDialogElement | null = $state(null);
16+
let pagefind: Pagefind | null = $state(null);
17+
let query = $state('');
18+
let docSearchSettings = $state(docSearchSettingsStore.get());
19+
let showFilters = $state(false);
20+
21+
$effect(() => {
22+
const unsubscribe = docSearchSettingsStore.listen((value) => {
23+
docSearchSettings = value;
24+
});
25+
26+
return unsubscribe;
27+
});
28+
29+
$effect(() => docSearchSettingsStore.set(docSearchSettings));
30+
31+
const searchPromise = $derived.by(() => {
32+
// Define deps since async context is not reactive
33+
[pagefind, query, docSearchSettings.framework];
34+
35+
return untrack(async () => {
36+
if (pagefind === null || query === '') return [];
37+
38+
const result = await pagefind.debouncedSearch(query, {}, 250);
39+
if (result === null) return [];
40+
41+
const results = await Promise.all(result.results.map((result) => result.data()));
42+
return results.filter((result) => {
43+
if (docSearchSettings.framework === 'all') {
44+
return true;
45+
}
46+
47+
const urlFramework = result.url.split('/').at(-2);
48+
49+
if (!isFramework(urlFramework)) {
50+
return true;
51+
}
52+
53+
if (docSearchSettings.framework === 'preferred') {
54+
return preferredFrameworkStore.get() === urlFramework;
55+
}
56+
57+
return urlFramework === docSearchSettings.framework;
58+
});
59+
});
60+
});
61+
62+
const openDialog = () => dialog && dialog.showModal();
63+
64+
const onClickOutside = (node: HTMLDialogElement) => {
65+
const onclick = (event: MouseEvent) => {
66+
if (event.target === null || !(event.target instanceof Element)) return;
67+
if (event.target.tagName !== 'DIALOG') return; // prevents issues with forms
68+
69+
const rect = event.target.getBoundingClientRect();
70+
71+
const clickedInDialog =
72+
rect.top <= event.clientY &&
73+
event.clientY <= rect.top + rect.height &&
74+
rect.left <= event.clientX &&
75+
event.clientX <= rect.left + rect.width;
76+
77+
if (clickedInDialog === false) {
78+
node.close();
79+
query = '';
80+
}
81+
};
82+
83+
document.addEventListener('click', onclick);
84+
85+
return {
86+
destroy: () => {
87+
document.removeEventListener('click', onclick);
88+
},
89+
};
90+
};
91+
92+
const toggleFilters = () => (showFilters = !showFilters);
93+
94+
$effect(() => {
95+
// @ts-expect-error - Pagefind will be present at runtime
96+
import('/pagefind/pagefind.js').then((module: Pagefind) => {
97+
pagefind = module;
98+
pagefind.init();
99+
});
100+
101+
return () => {
102+
if (!pagefind) return;
103+
pagefind.destroy();
104+
pagefind = null;
105+
};
106+
});
107+
108+
$effect(() => {
109+
function onKeydown(event: KeyboardEvent) {
110+
if (event.key === 'k' && (event.ctrlKey || event.metaKey)) {
111+
event.preventDefault();
112+
openDialog();
113+
}
114+
}
115+
document.addEventListener('keydown', onKeydown);
116+
return () => {
117+
document.removeEventListener('keydown', onKeydown);
118+
};
119+
});
120+
</script>
121+
122+
<!-- Trigger Button -->
123+
<button onclick={() => openDialog()} type="button" class="btn preset-outlined-surface-200-800 hover:preset-tonal">
124+
<IconSearch class="opacity-60 size-4" />
125+
<span class="opacity-60">Search...</span>
126+
</button>
127+
128+
<!-- Dialog -->
129+
<dialog
130+
class="bg-surface-50-950 text-inherit rounded-container m-0 mx-auto top-[10%] p-8 w-full max-w-[90%] md:max-w-2xl lg:max-w-4xl max-h-[75%] shadow-xl space-y-8 backdrop:bg-[rgba(var(--color-surface-900)/0.5)] backdrop:backdrop-blur-sm"
131+
bind:this={dialog}
132+
use:onClickOutside
133+
>
134+
<!-- Search Field -->
135+
<div class="input-group grid-cols-[auto_1fr_auto]">
136+
<div class="input-group-cell">
137+
<IconSearch class="opacity-60 size-4" />
138+
</div>
139+
<input placeholder="Search..." bind:value={query} />
140+
<button
141+
type="button"
142+
class="btn-icon preset-tonal scale-75 translate-y-0.5"
143+
onclick={toggleFilters}
144+
title="Show Filters"
145+
tabindex="-1"
146+
>
147+
<IconFilter class="size-6" />
148+
</button>
149+
</div>
150+
<!-- Filters -->
151+
{#if showFilters}
152+
<label class="label">
153+
<select class="select" bind:value={docSearchSettings.framework}>
154+
<option value="preferred">Preferred Framework</option>
155+
{#each frameworks as framework}
156+
<option value={framework.slug}>Only {framework.name}</option>
157+
{/each}
158+
<option value="all">All Frameworks</option>
159+
</select>
160+
</label>
161+
{/if}
162+
<!-- Results -->
163+
<article class="[&_mark]:code [&_mark]:text-inherit">
164+
{#await searchPromise}
165+
<p class="text-center py-10"><IconLoader class="size-4 inline ml-2 animate-spin" /></p>
166+
{:then results}
167+
{#if results.length === 0 && query !== ''}
168+
<p class="text-center py-10">No results found for <code class="code">{query}</code></p>
169+
{:else if results.length === 0}
170+
<p class="text-center py-10">What can we help you find?</p>
171+
{:else}
172+
<ol class="flex flex-col gap-4 space-y-4">
173+
{#each results as result}
174+
<li class="space-y-2">
175+
<!-- Page Result -->
176+
<a
177+
class="card preset-outlined-surface-100-900 hover:preset-tonal grid grid-cols-[auto_1fr_auto] gap-4 items-center p-4"
178+
href={result.url}
179+
>
180+
<span><IconBook class="size-6 opacity-60" /></span>
181+
<div class="space-y-1">
182+
<p class="type-scale-4 font-bold">{result.meta.title}</p>
183+
<p class="type-scale-1">{result.url}</p>
184+
</div>
185+
<span><IconChevronRight class="size-4 opacity-60" /></span>
186+
</a>
187+
<!-- Inner Result -->
188+
<div
189+
class="border-l border-surface-200-800 pl-4 divide-y-[1px] divide-surface-100-900 space-y-2"
190+
>
191+
{#each result.sub_results.filter((r) => r.title !== result.meta.title) as subResult}
192+
<a
193+
class="card preset-outlined-surface-100-900 hover:preset-tonal grid grid-cols-[auto_1fr_auto] gap-4 items-center p-4 space-y-1"
194+
href={subResult.url}
195+
>
196+
<span class="hidden md:block">
197+
<IconHash class="size-4 opacity-60" />
198+
</span>
199+
<div class="space-y-1 overflow-hidden">
200+
<p class="type-scale-3 font-bold">{subResult.title}</p>
201+
<p class="type-scale-1 text-surface-600-400 break-words">
202+
{@html subResult.excerpt}
203+
</p>
204+
</div>
205+
<span class="hidden md:block">
206+
<IconChevronRight class="size-4 opacity-60" />
207+
</span>
208+
</a>
209+
{/each}
210+
</div>
211+
</li>
212+
{/each}
213+
</ol>
214+
{/if}
215+
{/await}
216+
</article>
217+
</dialog>

sites/next.skeleton.dev/src/examples/tailwind/chips/ExampleSelect.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ export const Page: React.FC = () => {
1010
<div className="flex gap-2">
1111
{/* Loop through the available colors */}
1212
{color &&
13-
colors.map((c: string) => (
13+
colors.map((c) => (
1414
// On selection, set the color state, dynamically update classes
1515
<button
1616
className={`chip capitalize ${color === c ? 'preset-filled' : 'preset-tonal'}`}
1717
onClick={() => setColor(c)}
18+
key={c}
1819
>
1920
<span>{c}</span>
2021
</button>

sites/next.skeleton.dev/src/layouts/LayoutDoc.astro

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const urls = {
7272
<!-- Breadcrumbs -->
7373
<Breadcrumbs />
7474
<!-- Header -->
75-
<header class="space-y-2">
75+
<header class="space-y-2" data-pagefind-body>
7676
<h1 class="h1">{frontmatter.title ?? '(title)'}</h1>
7777
<p class="opacity-60 type-scale-4">{frontmatter.description ?? '(description)'}</p>
7878
</header>
@@ -81,7 +81,7 @@ const urls = {
8181
<!-- Framework Tabs -->
8282
<FrameworkPicker />
8383
<!-- Content -->
84-
<article class="space-y-8">
84+
<article class="space-y-8" data-pagefind-body>
8585
<slot />
8686
</article>
8787
<!-- API Reference -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { persistentMap } from '@nanostores/persistent';
2+
import type { Framework } from './preferred-framework';
3+
4+
type SearchFilters = {
5+
framework: 'preferred' | 'all' | Framework;
6+
};
7+
8+
export const docSearchSettingsStore = persistentMap<SearchFilters>('doc-search-settings', {
9+
framework: 'preferred',
10+
});

0 commit comments

Comments
 (0)