From 3aeab3d84293fe08e2dd882859550b5fba61af28 Mon Sep 17 00:00:00 2001 From: Scott Rhamy Date: Wed, 24 Dec 2025 15:58:05 -0500 Subject: [PATCH] flexsearch - added wiring for flexsearch - searchIndex.ts is fake data, waiting for you to scape via tools - supports persistent recent searches (5) saved to localstorage using PersistedState from runed. - Escape closes search - prior Ctrl-K on windows focuses URL bar (I tried at work) rather than focusing on search, now fixed. - This should allow you to run with it pretty quickly. - sections would be nice (Components, Examples, Getting Started, etc) - some icons too --- docs/package.json | 1 + docs/src/routes/docs/+layout.svelte | 53 +------ docs/src/routes/docs/search/Search.svelte | 156 +++++++++++++++++++++ docs/src/routes/docs/search/search.ts | 66 +++++++++ docs/src/routes/docs/search/searchIndex.ts | 62 ++++++++ pnpm-lock.yaml | 8 ++ 6 files changed, 296 insertions(+), 50 deletions(-) create mode 100644 docs/src/routes/docs/search/Search.svelte create mode 100644 docs/src/routes/docs/search/search.ts create mode 100644 docs/src/routes/docs/search/searchIndex.ts diff --git a/docs/package.json b/docs/package.json index fa46f5af2..18ea80d25 100644 --- a/docs/package.json +++ b/docs/package.json @@ -83,6 +83,7 @@ "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.13.1", + "flexsearch": "^0.8.212", "globals": "^16.5.0", "layerchart": "workspace:*", "mdsx": "^0.0.7", diff --git a/docs/src/routes/docs/+layout.svelte b/docs/src/routes/docs/+layout.svelte index d82010ea3..c5a65d101 100644 --- a/docs/src/routes/docs/+layout.svelte +++ b/docs/src/routes/docs/+layout.svelte @@ -19,6 +19,7 @@ import { page } from '$app/state'; import { examples } from '$lib/context.js'; import DocsMenu from '$lib/components/DocsMenu.svelte'; + import Search from './search/Search.svelte'; import favicon from '$lib/assets/favicon.svg'; import LucideAlignLeft from '~icons/lucide/align-left'; @@ -48,15 +49,6 @@ }; examples.set(examplesContext); - let searchQuery = $state(''); - - function handleSearch() { - goto(`/docs/search?q=${searchQuery}`); - searchQuery = ''; - } - - let searchInput = $state(); - // let pageContent = $derived(page.data.content.docs[page.params.slug] ?? {}); let showDrawer = $state(false); let showSidebar = $state(true); @@ -112,38 +104,7 @@ LayerChart -
- -
+
@@ -204,7 +165,7 @@ icon: CustomBluesky } ]} - on:change={(e) => { + on:change={(e: CustomEvent) => { window.open(e.detail.value, '_blank'); }} class="inline-block md:hidden" @@ -359,11 +320,3 @@
{/if}
- - { - if (e[env.getModifierKey()] && e.key === 'k') { - searchInput?.focus(); - } - }} -/> diff --git a/docs/src/routes/docs/search/Search.svelte b/docs/src/routes/docs/search/Search.svelte new file mode 100644 index 000000000..c6cd23ffc --- /dev/null +++ b/docs/src/routes/docs/search/Search.svelte @@ -0,0 +1,156 @@ + + +
+ (isSearching = true)} + onclick={() => (isSearching = true)} + oninput={() => { + if (searchQuery.trim()) { + results = searchPostsIndex(searchQuery); + } else { + results = []; + } + }} + onblur={() => { + isSearching = false; + searchQuery = ''; + results = []; + }} + classes={{ + root: 'hidden sm:block px-2', + container: 'hover:border-surface-content/20' + }} + > + {#snippet prepend()} + + {/snippet} + {#snippet append()} +
+ + K +
+ {/snippet} +
+
+{#if isSearching && (priorQueries.current.length > 0 || searchQuery.trim())} +
+
+ {#if priorQueries.current.length > 0} +
+ +

Recent Searches

+ + (priorQueries.current = [])} + /> + +
+
    + {#each priorQueries.current as priorQuery} +
  • + +
  • + {/each} +
+ {/if} + {#if priorQueries.current.length > 0 && results.length > 0} +
+ {/if} + {#if results.length > 0} + + {/if} +
+
+{/if} + + { + if (e[env.getModifierKey()] && e.key === 'k') { + e.preventDefault(); + searchInput?.focus(); + isSearching = true; + } else if (e.key === 'Escape' && isSearching) { + isSearching = false; + searchQuery = ''; + results = []; + searchInput?.blur(); + } + }} +/> diff --git a/docs/src/routes/docs/search/search.ts b/docs/src/routes/docs/search/search.ts new file mode 100644 index 000000000..73594ac70 --- /dev/null +++ b/docs/src/routes/docs/search/search.ts @@ -0,0 +1,66 @@ +import FlexSearch from 'flexsearch'; + +export type Post = { + content: string; + slug: string; + title: string; +}; + +export type Result = { + content: string[]; + slug: string; + title: string; +}; + +let postsIndex: InstanceType; +let posts: Post[]; + +export function createPostsIndex(data: Post[]) { + postsIndex = new FlexSearch.Index({ tokenize: 'forward' }); + + data.forEach((post, i) => { + const item = `${post.title} ${post.content}`; + postsIndex.add(i, item); + }); + + posts = data; +} + +export function searchPostsIndex(searchTerm: string) { + const match = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const results = postsIndex.search(match) as number[]; + + return results + .map((index: number) => posts[index]) + .map(({ slug, title, content }: { slug: string; title: string; content: string }) => { + return { + slug, + title: replaceTextWithMarker(title, match), + content: getMatches(content, match) + }; + }); +} + +function replaceTextWithMarker(text: string, match: string) { + const regex = new RegExp(match, 'gi'); + return text.replaceAll(regex, (match) => `${match}`); +} + +function getMatches(text: string, searchTerm: string, limit = 1) { + const regex = new RegExp(searchTerm, 'gi'); + const indexes = []; + let matches = 0; + let match; + + while ((match = regex.exec(text)) !== null && matches < limit) { + indexes.push(match.index); + matches++; + } + + return indexes.map((index) => { + const start = index - 20; + const end = index + 80; + const excerpt = text.substring(start, end).trim(); + return `...${replaceTextWithMarker(excerpt, searchTerm)}...`; + }); +} diff --git a/docs/src/routes/docs/search/searchIndex.ts b/docs/src/routes/docs/search/searchIndex.ts new file mode 100644 index 000000000..4fc4934b4 --- /dev/null +++ b/docs/src/routes/docs/search/searchIndex.ts @@ -0,0 +1,62 @@ +export const searchIndex = [ + { + title: 'The Joy of Painting', + slug: 'the-joy-of-painting', + content: + 'Imagine you are painting a beautiful sunset over a calm lake. Each stroke of your brush brings another layer of rich color onto the canvas. The sky is a mix of orange, red, and purple hues, gently blending together. The water reflects the colors of the sky, adding depth to the scene. Take your time to add details, like the silhouette of trees on the shore. Remember, every stroke counts. Your painting is a reflection of your emotions and thoughts. Keep adding layers until you are satisfied with the final result.' + }, + { + title: 'The Power of Nature', + slug: 'the-power-of-nature', + content: + 'Imagine you are painting a dense forest. The trees are tall and majestic, their leaves shimmering in the sunlight. The ground beneath them is covered in a carpet of green, broken up by patches of brightly colored wildflowers. Add depth to your painting by including a winding path that leads deeper into the forest. Remember, every tree and flower adds to the beauty of nature. Your painting is a celebration of life and the natural world around us.' + }, + { + title: 'The Beauty of Abstract Art', + slug: 'the-beauty-of-abstract-art', + content: + 'Imagine you are painting a piece of abstract art. Start with a blank canvas. Then, slowly begin to add shapes and colors. There are no rules here. You can use any color that makes you happy. Add circles, squares, triangles, and lines. Mix different shades and tones. The goal is not to replicate reality, but to express yourself through color and shape. Your painting is a unique creation that comes from your imagination.' + }, + { + title: 'The Calm of Water', + slug: 'the-calm-of-water', + content: + 'Imagine you are painting a peaceful river. The water flows smoothly, its surface reflecting the surrounding landscape. Trees grow alongside the riverbank, their branches reaching out towards the water. Birds fly overhead, their wings spread wide. Add details like fish swimming in the water, or a small boat floating downstream. Your painting captures the serene beauty of nature and the calmness it brings.' + }, + { + title: 'The Energy of Landscapes', + slug: 'the-energy-of-landscapes', + content: + 'Imagine you are painting a dynamic landscape. Mountains rise high into the sky, their peaks covered in snow. A river cuts through the mountains, its flow powerful and relentless. Trees cling to the sides of the mountains, their roots reaching out towards the soil. Clouds float overhead, their shapes constantly changing. Your painting captures the raw energy of nature and the power of its elements.' + }, + { + title: 'The Warmth of Autumn', + slug: 'the-warmth-of-autumn', + content: + 'Imagine you are painting an autumn scene. Leaves change color, transitioning from green to yellow, orange, and red. They fall gently from the trees, covering the ground in a thick layer. A warm sun hangs low in the sky, casting long shadows across the landscape. Your painting captures the beauty and tranquility of autumn, a time of year that is filled with warmth and coziness.' + }, + { + title: 'The Serenity of Winter', + slug: 'the-serenity-of-winter', + content: + 'Imagine you are painting a winter scene. Snow covers the ground, creating a pristine white canvas. Trees stand bare, their branches stark against the white background. Ice forms on a nearby body of water, creating a shiny mirror. Your painting captures the quiet beauty and serenity of winter, a time of year that offers a peaceful respite from the busy summer months.' + }, + { + title: 'The Elegance of Spring', + slug: 'the-elegance-of-spring', + content: + 'Imagine you are painting a spring scene. Flowers bloom in full force, their petals a riot of colors. Blossoms cover the trees, their fragrance filling the air. Birds sing, their melodious songs echoing through the landscape. Your painting captures the elegance and freshness of spring, a time of renewal and growth.' + }, + { + title: 'The Mystery of Summer', + slug: 'the-mystery-of-summer', + content: + 'Imagine you are painting a summer scene. The sun is hot, the sky is blue, and the air is still. A gentle breeze rustles the leaves of the trees. Butterflies flutter from flower to flower, their wings a blur of color. Your painting captures the mystery and heat of summer, a time of year that is filled with life and activity.' + }, + { + title: 'The Magic of Fall', + slug: 'the-magic-of-fall', + content: + 'Imagine you are painting a fall scene. Leaves change color, their hues ranging from green to gold, orange, and red. They fall gently from the trees, forming a carpet on the ground. A cool breeze blows, carrying the scent of decaying leaves. Your painting captures the magic and beauty of fall, a transitional season that marks the end of summer and the beginning of winter.' + } +]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7c08b8bf..c190e604b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,6 +234,9 @@ importers: eslint-plugin-svelte: specifier: ^3.13.1 version: 3.13.1(eslint@9.39.2(jiti@2.6.1))(svelte@5.46.0) + flexsearch: + specifier: ^0.8.212 + version: 0.8.212 globals: specifier: ^16.5.0 version: 16.5.0 @@ -3433,6 +3436,9 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flexsearch@0.8.212: + resolution: {integrity: sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -8272,6 +8278,8 @@ snapshots: flatted@3.3.3: {} + flexsearch@0.8.212: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11