Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 3 additions & 50 deletions docs/src/routes/docs/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -48,15 +49,6 @@
};
examples.set(examplesContext);

let searchQuery = $state('');

function handleSearch() {
goto(`/docs/search?q=${searchQuery}`);
searchQuery = '';
}

let searchInput = $state<HTMLInputElement>();

// let pageContent = $derived(page.data.content.docs[page.params.slug] ?? {});
let showDrawer = $state(false);
let showSidebar = $state(true);
Expand Down Expand Up @@ -112,38 +104,7 @@

<a href="/" class="text-xl font-bold lg:w-60">LayerChart</a>

<div class="flex grow justify-end sm:justify-center lg:justify-start">
<!-- TODO: Add search functionality -->
<Button icon={LucideSearch} class="inline-block sm:hidden" />
<TextField
placeholder="Search"
bind:value={searchQuery}
on:keydown={(e) => {
if (e.key === 'Enter') {
handleSearch();
}
}}
bind:inputEl={searchInput}
classes={{
root: 'hidden sm:block px-2',
container: 'hover:border-surface-content/20'
}}
>
{#snippet prepend()}
<LucideSearch class="text-surface-content/50 mr-4" />
{/snippet}
{#snippet append()}
<div class="flex items-center gap-1">
<Kbd
command={env.isMac()}
control={!env.isMac()}
class="size-4 items-center justify-center text-xs"
/>
<Kbd class="size-4 items-center justify-center text-xs">K</Kbd>
</div>
{/snippet}
</TextField>
</div>
<Search />

<div class="flex items-center gap-2">
<div class="flex items-center border-r pr-2">
Expand Down Expand Up @@ -204,7 +165,7 @@
icon: CustomBluesky
}
]}
on:change={(e) => {
on:change={(e: CustomEvent) => {
window.open(e.detail.value, '_blank');
}}
class="inline-block md:hidden"
Expand Down Expand Up @@ -359,11 +320,3 @@
</div>
{/if}
</div>

<svelte:window
onkeydown={(e) => {
if (e[env.getModifierKey()] && e.key === 'k') {
searchInput?.focus();
}
}}
/>
156 changes: 156 additions & 0 deletions docs/src/routes/docs/search/Search.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<script lang="ts">
import { env } from '@layerstack/utils';
import { PersistedState } from 'runed';
import { searchIndex } from './searchIndex';
import { createPostsIndex, searchPostsIndex, type Result } from './search';
import { Kbd, TextField, Tooltip } from 'svelte-ux';
import LucideSearch from '~icons/lucide/search';
import LucideX from '~icons/lucide/x';

const MAX_PRIOR_QUERIES = 5;

let priorQueries = new PersistedState<{ query: string; count: number }[]>('prior-queries', []);
let searchQuery = $state('');
let isSearching = $state(false);
let searchInput = $state<HTMLInputElement>();
let results: Result[] = $state([]);

// Initialize search index immediately
createPostsIndex(searchIndex);

function addPriorQuery(query: string, count: number) {
// Remove duplicate if exists
priorQueries.current = priorQueries.current.filter((q) => q.query !== query);
// Add to the beginning
priorQueries.current.unshift({ query, count });
// Keep only the first 5 items
priorQueries.current = priorQueries.current.slice(0, MAX_PRIOR_QUERIES);
}
</script>

<div class="flex grow justify-end sm:justify-center lg:justify-start">
<TextField
placeholder="Search"
bind:value={searchQuery}
bind:inputEl={searchInput}
onfocus={() => (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()}
<LucideSearch class="text-surface-content/50 mr-4" />
{/snippet}
{#snippet append()}
<div class="flex items-center gap-1">
<Kbd
command={env.isMac()}
control={!env.isMac()}
class="size-4 items-center justify-center text-xs"
/>
<Kbd class="size-4 items-center justify-center text-xs">K</Kbd>
</div>
{/snippet}
</TextField>
</div>
{#if isSearching && (priorQueries.current.length > 0 || searchQuery.trim())}
<div
class="fixed z-9999 p-4 inset-0 flex items-center justify-center min-h-dvh rounder-xl shadow-xl"
>
<div
class="max-h-[48vh] w-full max-w-2xl p-6 bg-[hsl(220_10%_14%)] overflow-y-auto [scrollbar-width:thin] rounded-lg shadow-lg"
>
{#if priorQueries.current.length > 0}
<div class="relative flex items-center">
<LucideSearch class="text-surface-content/50 mr-2" />
<h2 class="text-lg font-bold">Recent Searches</h2>
<Tooltip title="Clear recent searches" placement="top" offset={8}>
<LucideX
class="text-surface-content/50 ml-2 hover:text-primary-500"
onclick={() => (priorQueries.current = [])}
/>
</Tooltip>
</div>
<ul>
{#each priorQueries.current as priorQuery}
<li class="not-last:py-1/2">
<button
type="button"
class="w-full text-left cursor-pointer hover:border-primary-500/50 rounded-md border border-transparent p-2 transition-colors duration-200"
onclick={() => {
searchQuery = priorQuery.query;
results = searchPostsIndex(searchQuery);
searchInput?.focus();
}}
>
<div class="flex items-center justify-between">
<span>{priorQuery.query}</span>
<span
class="inline-flex items-center justify-center bg-primary-500 text-sm font-semibold px-2.5 py-0.5 rounded-full w-10"
>
{priorQuery.count}
</span>
</div>
</button>
</li>
{/each}
</ul>
{/if}
{#if priorQueries.current.length > 0 && results.length > 0}
<div class="flex-1 h-1 bg-white/20 my-4"></div>
{/if}
{#if results.length > 0}
<ul class="grid gap-3 p-0 m-0 list-none">
{#each results as result}
<li
class="not-last:py-0.5 hover:border-primary-500/50 rounded-md p-2 border-transparent border transition-border duration-200"
>
<a
href="/{result.slug}"
class="block text-xl text-surface-content/80 no-underline"
onclick={() => {
addPriorQuery(searchQuery, results.length);
searchInput?.focus();
}}
>
<p class="text-lg font-bold">
{@html result.title}
</p>
<p>{@html result.content}</p>
</a>
</li>
{/each}
</ul>
{/if}
</div>
</div>
{/if}

<svelte:window
onkeydown={(e) => {
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();
}
}}
/>
66 changes: 66 additions & 0 deletions docs/src/routes/docs/search/search.ts
Original file line number Diff line number Diff line change
@@ -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<typeof FlexSearch.Index>;
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) => `<mark class="rounded">${match}</mark>`);
}

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)}...`;
});
}
62 changes: 62 additions & 0 deletions docs/src/routes/docs/search/searchIndex.ts
Original file line number Diff line number Diff line change
@@ -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.'
}
];
Loading
Loading