SPB Git

spb/svgarden Public

SVGarden — searchable bank of 74 self-contained SVG+CSS animation snippets (svgarden.dev)

HTML 79.2% Astro 10.3% JavaScript 6% CSS 3.7% Shell 0.8%
2.3 KB · 73 lines astro
Raw Blame History
1---2/**3 * ============================================================4 * SVGarden — https://www.svgarden.dev5 * Author : Simon-Pierre Boucher6 * Contact: contact@spboucher.ai7 * File   : src/components/SearchBar.astro8 * Desc   : Instant client-side search — Fuse.js lazy-loaded on first focus9 * ============================================================10 */11---1213<div class="sg-search">14  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">15    <circle cx="11" cy="11" r="7" />16    <path d="m20 20-3.5-3.5" />17  </svg>18  <input19    type="search"20    id="sg-search-input"21    placeholder="Search animations… (title, tag, technique)"22    autocomplete="off"23    aria-label="Search animations"24  />25  <kbd aria-hidden="true">/</kbd>26</div>2728<script>29  import type Fuse from 'fuse.js';3031  const input = document.getElementById('sg-search-input') as HTMLInputElement;32  let fuse: Fuse<{ slug: string }> | null = null;33  let loading: Promise<void> | null = null;3435  // Lazy-load Fuse.js + the index only when the visitor actually searches.36  const ensureIndex = () =>37    (loading ??= Promise.all([38      import('fuse.js'),39      fetch('/search-index.json').then((r) => r.json()),40    ]).then(([{ default: FuseCtor }, docs]) => {41      fuse = new FuseCtor(docs, {42        keys: [43          { name: 'title', weight: 3 },44          { name: 'tags', weight: 2 },45          { name: 'techniques', weight: 2 },46          { name: 'category', weight: 1 },47          { name: 'desc', weight: 1 },48        ],49        threshold: 0.35,50        ignoreLocation: true,51      });52    }));5354  const emit = (slugs: string[] | null) =>55    document.dispatchEvent(new CustomEvent('sg:search', { detail: { slugs } }));5657  input.addEventListener('focus', () => void ensureIndex(), { once: true });58  input.addEventListener('input', async () => {59    const q = input.value.trim();60    if (!q) return emit(null);61    await ensureIndex();62    emit(fuse!.search(q).map((r) => r.item.slug));63  });6465  // `/` focuses search from anywhere (unless already typing in a field)66  document.addEventListener('keydown', (e) => {67    if (e.key === '/' && !(e.target instanceof HTMLInputElement) && !(e.target instanceof HTMLTextAreaElement)) {68      e.preventDefault();69      input.focus();70    }71  });72</script>73