# Files changed · PR \#9 · screen/col

[View on GitCafe](https://git.cafe/screen/col/pull/9/diffs)

Repository: [screen/col](https://git.cafe/screen/col)

Visibility: public

[Revert "Improve library discovery and add CI checks"](https://git.cafe/screen/col/pull/9.md)

Version: 0

Head commit: 084a95739839dd13c68b1d6b76fb3c4cfabc632a

Base commit: a763505a848ee37b328557ab2194d8fb9cadcd1d

Comparison base: a763505a848ee37b328557ab2194d8fb9cadcd1d

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## \.github/ISSUE_TEMPLATE/library-request.yml

Status: modified; +0; −7

```diff
@@ -48,13 +48,6 @@
       placeholder: React, Vue, TypeScript
     validations:
       required: true
-  - type: input
-    id: use-cases
-    attributes:
-      label: Use cases
-      placeholder: Dashboards, Rapid Prototyping
-    validations:
-      required: true
   - type: checkboxes
     id: checks
     attributes:
```

## \.github/workflows/ci.yml

Status: deleted; +0; −17

```diff
@@ -1,17 +1,0 @@
-name: Verify
-
-on:
-  push:
-  pull_request:
-
-jobs:
-  check:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-node@v4
-        with:
-          node-version: '20.9'
-          cache: npm
-      - run: npm ci
-      - run: npm run check
```

## CONTRIBUTING.md

Status: modified; +1; −1

````diff
@@ -18,7 +18,7 @@
 npm run dev
 ```
 
-Run `npm run check` before submitting changes.
+Run `npm run build` before submitting changes.
 
 ## Add a library
 
````

## PRODUCT.md

Status: modified; +2; −2

```diff
@@ -8,11 +8,11 @@
 
 ## Users
 
-Frontend developers and product designers finding UI libraries for an active project, plus contributors maintaining the directory.
+Frontend developers and product designers comparing UI libraries for an active project, plus contributors maintaining the directory.
 
 ## Product Purpose
 
-Col is a community-maintained directory for finding, filtering, sorting, and saving UI libraries without opening many disconnected sources.
+Col is a community-maintained directory for finding, filtering, and comparing UI libraries without opening many disconnected sources.
 
 ## Positioning
 
```

## README.md

Status: modified; +4; −6

````diff
@@ -31,7 +31,7 @@
 
 ## What Col does
 
-Col organizes UI libraries by category, stack, and use case. Search from the homepage, then filter, sort, and save matching libraries in the directory.
+Col organizes UI libraries by category, stack, and use case. Search from the homepage, then compare matching libraries in the directory.
 
 - Search by name, keyword, category, stack, or use case.
 - Filter libraries without leaving the directory.
@@ -57,7 +57,7 @@
 Before opening a pull request:
 
 ```bash
+npm run build
-npm run check
 ```
 
 ## Request something
@@ -74,7 +74,7 @@
 - what it provides and who it helps;
 - its supported stacks;
 - the closest Col category and use cases;
-- confirmation that it is publicly accessible.
+- confirmation that it is maintained and publicly accessible.
 
 Search [existing libraries](data/libraries.ts), [issues](https://github.com/screen-gd/Col/issues), and [pull requests](https://github.com/screen-gd/Col/pulls) first.
 
@@ -105,7 +105,7 @@
 3. Reuse the existing category, stack, and use-case values when possible.
 4. Keep the description factual and short.
 5. Confirm the URL points to the official project.
+6. Run `npm run build`.
-6. Run `npm run check`.
 7. Open a pull request using the provided template.
 
 ```ts
@@ -163,8 +163,6 @@
 app/                  Routes, layout, and global styles
 components/           Search, filters, cards, header, and shared UI
 data/libraries.ts     The curated library registry
-lib/                  Shared helpers and route metadata
-tests/                Registry and discovery checks
 public/brand/         Col brand assets
 public/hero-logos/    Library artwork used by the homepage
 .github/              Issue forms and pull request guidance
````

## app/docs/page.tsx

Status: modified; +1; −1

```diff
@@ -59,7 +59,7 @@
 
             <DocSection id="pull-requests" title="Open a pull request">
               <p>Fork the repository, create a focused branch, and keep unrelated changes out. Library additions belong in <code>data/libraries.ts</code> with a unique kebab-case slug, canonical URL, factual description, and existing taxonomy values where possible.</p>
-              <pre><code>{`npm install\nnpm run dev\nnpm run check`}</code></pre>
+              <pre><code>{`npm install\nnpm run dev\nnpm run build`}</code></pre>
               <p>Explain what changed, why it changed, and how you verified it. Include screenshots for visible interface changes.</p>
               <DocLink href="https://github.com/screen-gd/Col/blob/main/CONTRIBUTING.md">Read the contribution guide</DocLink>
             </DocSection>
```

## app/globals.css

Status: modified; +92; −9

```diff
@@ -179,10 +179,26 @@
   color: #111;
 }
 
+.gooey-surface {
+  background: #090909;
+  color: #fff;
+  box-shadow: inset 0 0 0 1px rgba(255,255,255,.14);
+}
+.hero-search-surface { box-shadow: none; }
+.gooey-input-text { color: #fff; }
+.gooey-key { border-color: rgba(255,255,255,.12); color: rgba(255,255,255,.75); }
+
+html.light .gooey-surface {
+  background: #fff;
+  color: #111;
+  box-shadow: inset 0 0 0 1px rgba(17,17,17,.16);
+}
+html.light .hero-search-surface { box-shadow: none; }
+html.light .gooey-input-text { color: #111; }
+html.light .gooey-key { border-color: rgba(17,17,17,.14); color: rgba(17,17,17,.65); }
 html.light .hero-stat { background: transparent !important; border-color: rgba(17,17,17,.14) !important; }
 
 .hero-search { border-color: rgba(255,255,255,.15); background: #090909; color: #fff; }
-.hero-search:focus-within { outline: 1px solid #fff; outline-offset: 2px; }
 .hero-search-icon { color: rgba(255,255,255,.72); transition: transform 220ms ease; }
 .hero-search:focus-within .hero-search-icon { transform: translateX(2px); }
 .hero-search-input { color: #fff; }
@@ -209,7 +225,6 @@
 }
 
 html.light .hero-search { border-color: rgba(17,17,17,.16); background: #fff; color: #111; }
-html.light .hero-search:focus-within { outline-color: #111; }
 html.light .hero-search-icon { color: rgba(17,17,17,.68); }
 html.light .hero-search-input { color: #111; }
 html.light .hero-search-hints { color: rgba(17,17,17,.48); }
@@ -387,6 +402,28 @@
   html.light .site-header-mobile-menu a:hover, html.light .site-header-mobile-menu a[aria-current="page"] { background: rgba(17,17,17,.06); }
 }
 
+html.light .gooey-filter-layer { filter: none !important; }
+html.light .gooey-orb { display: none !important; }
+
+html.light [data-slot="select-content"] {
+  border-color: rgba(17,17,17,.14);
+  background: #fff;
+  color: #111;
+  box-shadow: none;
+}
+
+html.dark [data-slot="select-content"] { box-shadow: none; }
+html.dark [data-slot="select-item"][data-highlighted],
+html.dark [data-slot="select-item"]:focus {
+  background: #191919;
+  color: #fff;
+}
+
+html.light [data-slot="select-item"][data-highlighted] {
+  background: #f0f0ed;
+  color: #111;
+}
+
 .popular-filter-button {
   border-color: rgba(255,255,255,.18);
   background: #050505;
@@ -460,9 +497,9 @@
 .coss-menu-item:focus,
 .coss-menu-item:focus-visible,
 .coss-menu-item[data-highlighted] {
-  background: #303030;
+  background: transparent;
   color: #fff;
+  outline: none;
-  outline: 1px solid #fff;
 }
 
 html.light .coss-input,
@@ -475,9 +512,9 @@
 html.light .coss-menu-item:focus,
 html.light .coss-menu-item:focus-visible,
 html.light .coss-menu-item[data-highlighted] {
-  background: #e8e8e5;
+  background: transparent;
   color: #111;
-  outline: 1px solid #111;
+  outline: none;
 }
 
 @keyframes coss-menu-in {
@@ -492,11 +529,11 @@
 
 .filter-tab { color: rgba(255,255,255,.72); }
 .filter-tab:hover { background: rgba(255,255,255,.06); color: #fff; }
+.filter-tab[data-state="active"] { border-color: rgba(255,255,255,.24); background: #fff; color: #080808; }
-.filter-tab[aria-pressed="true"] { border-color: rgba(255,255,255,.24); background: #fff; color: #080808; }
 
 html.light .filter-tab { color: rgba(17,17,17,.68); }
 html.light .filter-tab:hover { background: rgba(17,17,17,.05); color: #111; }
-html.light .filter-tab[aria-pressed="true"] { border-color: #111; background: #111; color: #fff; }
+html.light .filter-tab[data-state="active"] { border-color: #111; background: #111; color: #fff; }
 
 .library-card { border-color: rgba(255,255,255,.14); background: #050505; color: #fff; }
 .library-card:hover { border-color: rgba(255,255,255,.3); background: #090909; }
@@ -507,7 +544,7 @@
 html.light .library-card { border-color: rgba(17,17,17,.14); background: #fff; color: #111; }
 html.light .library-card:hover { border-color: rgba(17,17,17,.3); background: #fcfcfb; }
 html.light .library-muted { color: rgba(17,17,17,.7); }
-html.light .library-subtle { color: rgba(17,17,17,.65); }
+html.light .library-subtle { color: rgba(17,17,17,.52); }
 html.light .library-chip { border-color: rgba(17,17,17,.12); background: rgba(17,17,17,.045); color: rgba(17,17,17,.72); }
 
 html.light [class~="text-white"] { color: #111; }
@@ -551,6 +588,26 @@
   filter: blur(3px) brightness(.85) saturate(0);
 }
 
+.hero-background-card--mid {
+  opacity: 0.62;
+  filter: blur(1.5px) brightness(0.94) saturate(0);
+}
+
+.hero-background-card--far {
+  opacity: 0.4;
+  filter: blur(4px) brightness(0.82) saturate(0);
+}
+
+@keyframes star-border-movement-bottom {
+  0% { transform: translate(0%, 0%); opacity: 1; }
+  100% { transform: translate(-100%, 0%); opacity: 0; }
+}
+
+@keyframes star-border-movement-top {
+  0% { transform: translate(0%, 0%); opacity: 1; }
+  100% { transform: translate(100%, 0%); opacity: 0; }
+}
+
 /* Entrance */
 @keyframes fade-up {
   from {
@@ -571,7 +628,23 @@
 .delay-2 {
   animation-delay: 0.24s;
 }
+.delay-3 {
+  animation-delay: 0.36s;
+}
 
+/* Infinite marquee */
+@keyframes marquee {
+  from {
+    transform: translateX(0);
+  }
+  to {
+    transform: translateX(-50%);
+  }
+}
+.animate-marquee {
+  animation: marquee 60s linear infinite;
+}
+
 :focus-visible:not(input) {
   outline: 1px solid rgba(255, 255, 255, 0.7);
   outline-offset: 4px;
@@ -607,6 +680,16 @@
     scroll-behavior: auto !important;
   }
 }
+.marquee-mask {
+  mask-image: linear-gradient(
+    to right,
+    transparent,
+    black 12%,
+    black 88%,
+    transparent
+  );
+}
+
 @theme inline {
   --animate-rainbow: rainbow var(--speed, 2s) infinite linear;
   --color-color-5: var(--color-5);
```

## app/layout.tsx

Status: modified; +2; −2

```diff
@@ -19,7 +19,7 @@
 
 export const metadata: Metadata = {
   title: "Col — Collection of Libraries",
-  description: "Browse UI libraries by category, stack, and use case.",
+  description: "We curate all the best libraries, so you don't have to.",
   icons: { icon: "/brand/col-mark.png" },
   keywords: [
     "ui libraries",
@@ -39,7 +39,7 @@
   return (
     <html lang="en" className="dark" suppressHydrationWarning>
       <head>
-        <script dangerouslySetInnerHTML={{ __html: `let t;try{t=localStorage.getItem('col:theme')}catch{}const l=t==='light'||(t!=='dark'&&matchMedia('(prefers-color-scheme: light)').matches);document.documentElement.classList.toggle('light',l);document.documentElement.classList.toggle('dark',!l)` }} />
+        <script dangerouslySetInnerHTML={{ __html: `try{const t=localStorage.getItem('col:theme');const l=t==='light'||(!t&&matchMedia('(prefers-color-scheme: light)').matches);document.documentElement.classList.toggle('light',l);document.documentElement.classList.toggle('dark',!l)}catch{}` }} />
       </head>
       <body
         className={`${inter.variable} ${pixelifySans.variable} ${geistMono.variable} min-h-screen font-sans`}
```

## app/libraries/page.tsx

Status: modified; +3; −7

```diff
@@ -1,19 +1,15 @@
 import { DirectoryExplorer } from "@/components/DirectoryExplorer";
 import { Header } from "@/components/Header";
-import { SiteFooter } from "@/components/SiteFooter";
-import { firstQuery } from "@/lib/directory";
 
-export default async function LibrariesPage({ searchParams }: { searchParams: Promise<{ q?: string | string[] }> }) {
-  const { q } = await searchParams;
-  const initialQuery = firstQuery(q);
+export default async function LibrariesPage({ searchParams }: { searchParams: Promise<{ q?: string }> }) {
+  const { q = "" } = await searchParams;
 
   return (
     <>
       <Header />
       <main className="w-full max-w-full overflow-x-hidden">
+        <DirectoryExplorer initialQuery={q} />
-        <DirectoryExplorer initialQuery={initialQuery} />
       </main>
-      <SiteFooter />
     </>
   );
 }
```

## components/AsciiFluid.tsx

Status: added; +126; −0

```diff
@@ -1,0 +1,126 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+const RAMP = " .·:;-~=+*x#%@";
+const CELL = 16;
+
+// Coolors palette: #45A29E (deep) -> #66FCF1 (bright)
+const DEEP: [number, number, number] = [69, 162, 158];
+const BRIGHT: [number, number, number] = [102, 252, 241];
+
+/**
+ * ASCII fluid background (21st.dev style).
+ * A monospace character field with an ambient wave; the cursor injects
+ * energy that ripples and decays like fluid. Dependency-free canvas.
+ */
+export function AsciiFluid({ className = "" }: { className?: string }) {
+  const canvasRef = useRef<HTMLCanvasElement>(null);
+
+  useEffect(() => {
+    const canvas = canvasRef.current;
+    const parent = canvas?.parentElement;
+    const ctx = canvas?.getContext("2d");
+    if (!canvas || !parent || !ctx) return;
+
+    let raf = 0;
+    let cols = 0;
+    let rows = 0;
+    let field = new Float32Array(0);
+    let t = 0;
+    const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
+    const mouse = { x: -9999, y: -9999 };
+
+    const resize = () => {
+      const rect = parent.getBoundingClientRect();
+      canvas.width = rect.width * dpr;
+      canvas.height = rect.height * dpr;
+      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+      ctx.font = `${CELL * 0.85}px ui-monospace, SFMono-Regular, monospace`;
+      ctx.textAlign = "center";
+      ctx.textBaseline = "middle";
+      cols = Math.ceil(rect.width / CELL);
+      rows = Math.ceil(rect.height / CELL);
+      field = new Float32Array(cols * rows);
+    };
+
+    const onMove = (e: MouseEvent) => {
+      const rect = canvas.getBoundingClientRect();
+      mouse.x = e.clientX - rect.left;
+      mouse.y = e.clientY - rect.top;
+    };
+
+    const onLeave = () => {
+      mouse.x = -9999;
+      mouse.y = -9999;
+    };
+
+    const render = () => {
+      t += 0.02;
+      ctx.clearRect(0, 0, cols * CELL, rows * CELL);
+
+      // Inject energy around the cursor — a soft radial splat.
+      if (mouse.x > -999) {
+        const mc = mouse.x / CELL;
+        const mr = mouse.y / CELL;
+        const R = 6;
+        const y0 = Math.max(0, Math.floor(mr - R));
+        const y1 = Math.min(rows, Math.ceil(mr + R));
+        const x0 = Math.max(0, Math.floor(mc - R));
+        const x1 = Math.min(cols, Math.ceil(mc + R));
+        for (let y = y0; y < y1; y++) {
+          for (let x = x0; x < x1; x++) {
+            const d = Math.hypot(x - mc, y - mr);
+            if (d < R) {
+              const i = y * cols + x;
+              field[i] = Math.min(1, field[i] + (1 - d / R) * 0.4);
+            }
+          }
+        }
+      }
+
+      for (let y = 0; y < rows; y++) {
+        for (let x = 0; x < cols; x++) {
+          const i = y * cols + x;
+          field[i] *= 0.94; // fluid decay
+
+          // Ambient drifting wave keeps the field alive without input.
+          const ambient =
+            0.08 + 0.06 * Math.sin(x * 0.22 + t) * Math.cos(y * 0.28 - t * 0.8);
+          const v = Math.min(1, Math.max(0, ambient + field[i]));
+
+          const ch = RAMP[Math.min(RAMP.length - 1, (v * RAMP.length) | 0)];
+          if (ch === " ") continue;
+
+          const r = (DEEP[0] + (BRIGHT[0] - DEEP[0]) * v) | 0;
+          const g = (DEEP[1] + (BRIGHT[1] - DEEP[1]) * v) | 0;
+          const b = (DEEP[2] + (BRIGHT[2] - DEEP[2]) * v) | 0;
+          ctx.fillStyle = `rgba(${r},${g},${b},${(0.18 + v * 0.7).toFixed(3)})`;
+          ctx.fillText(ch, x * CELL + CELL / 2, y * CELL + CELL / 2);
+        }
+      }
+
+      raf = requestAnimationFrame(render);
+    };
+
+    resize();
+    render();
+    window.addEventListener("resize", resize);
+    window.addEventListener("mousemove", onMove);
+    document.addEventListener("mouseleave", onLeave);
+    return () => {
+      cancelAnimationFrame(raf);
+      window.removeEventListener("resize", resize);
+      window.removeEventListener("mousemove", onMove);
+      document.removeEventListener("mouseleave", onLeave);
+    };
+  }, []);
+
+  return (
+    <canvas
+      ref={canvasRef}
+      aria-hidden
+      className={`pointer-events-none absolute inset-0 h-full w-full ${className}`}
+    />
+  );
+}
```

## components/CategoryBento.tsx

Status: added; +85; −0

```diff
@@ -1,0 +1,85 @@
+"use client";
+
+import {
+  Activity,
+  ArrowUpRight,
+  BarChart3,
+  Box,
+  Cuboid,
+  ImageIcon,
+  PanelsTopLeft,
+  Palette,
+  type LucideIcon,
+} from "lucide-react";
+import { CATEGORIES, libraries, type Category } from "@/data/libraries";
+import GlareHover from "./GlareHover";
+
+const CATEGORY_ICONS: Record<Category, LucideIcon> = {
+  "Component Library": Cuboid,
+  "Animation & Motion": Activity,
+  "Templates & Blocks": PanelsTopLeft,
+  Icons: ImageIcon,
+  "Charts & Data Viz": BarChart3,
+  "3D & WebGL": Box,
+  "CSS Framework": Palette,
+};
+
+interface CategoryBentoProps {
+  active: Category | null;
+  onSelect: (category: Category | null) => void;
+}
+
+export function CategoryBento({ active, onSelect }: CategoryBentoProps) {
+  return (
+    <div className="overflow-x-auto pb-3">
+      <div className="grid min-w-[1240px] grid-cols-7 gap-4">
+        {CATEGORIES.map((category) => {
+          const matches = libraries.filter((library) => library.category === category);
+          const isActive = active === category;
+          const Icon = CATEGORY_ICONS[category];
+
+          return (
+            <GlareHover
+              key={category}
+              width="100%"
+              height="170px"
+              background={isActive ? "#111111" : "#050505"}
+              borderColor={isActive ? "rgba(255,255,255,.42)" : "rgba(255,255,255,.14)"}
+              borderRadius="12px"
+              glareColor="#ffffff"
+              glareOpacity={0.08}
+              glareSize={180}
+              className="group"
+            >
+              <button
+                type="button"
+                onClick={() => {
+                  onSelect(isActive ? null : category);
+                  document.getElementById("directory")?.scrollIntoView({
+                    behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth",
+                  });
+                }}
+                aria-pressed={isActive}
+                className="relative z-10 flex h-full w-full flex-col justify-between p-5 text-left"
+              >
+                <div className="flex items-start justify-between text-white/75">
+                  <Icon className="size-5" strokeWidth={1.5} aria-hidden />
+                  <ArrowUpRight className="size-4 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" aria-hidden />
+                </div>
+                <div>
+                  <strong className="block text-2xl tracking-[-0.04em] text-white tabular-nums">
+                    {matches.length.toString().padStart(2, "0")}
+                  </strong>
+                  <h3 className="mt-1 text-sm leading-tight font-semibold text-white">{category}</h3>
+                  <p className="mt-2 truncate text-[11px] text-white/50">
+                    {matches.slice(0, 3).map((library) => library.name).join(" · ") || "More soon"}
+                  </p>
+                </div>
+              </button>
+            </GlareHover>
+          );
+        })}
+      </div>
+    </div>
+  );
+}
```

## components/DirectoryExplorer.tsx

Status: modified; +42; −35

```diff
@@ -1,21 +1,31 @@
 "use client";
 
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
 import { Heart, SearchX } from "lucide-react";
 import {
   libraries,
   type Category,
+  type Library,
   type Stack,
   type UseCase,
 } from "@/data/libraries";
 import { Button } from "@/components/ui/button";
-import { filterLibraries, nextSaved, readSaved } from "@/lib/directory";
 import { FilterBar, FilterDropdown } from "./FilterBar";
 import { LibraryCard } from "./LibraryCard";
 
 const SAVED_LIBRARIES_KEY = "col:saved-libraries";
-const validSlugs = new Set(libraries.map(({ slug }) => slug));
 
+function matchesQuery(library: Library, tokens: string[]): boolean {
+  return tokens.every((token) => [
+    library.name,
+    library.description,
+    library.category,
+    ...library.stacks,
+    ...library.useCases,
+    ...(library.tags ?? []),
+  ].join(" ").toLowerCase().includes(token));
+}
+
 export function DirectoryExplorer({ initialQuery = "" }: { initialQuery?: string }) {
   const [query, setQuery] = useState(initialQuery);
   const [category, setCategory] = useState<Category | null>(null);
@@ -24,42 +34,41 @@
   const [saved, setSaved] = useState<Set<string>>(new Set());
   const [showSaved, setShowSaved] = useState(false);
   const [sort, setSort] = useState<"curated" | "name">("curated");
-  const searchRef = useRef<HTMLInputElement>(null);
 
   useEffect(() => {
+    try {
+      const stored: unknown = JSON.parse(localStorage.getItem(SAVED_LIBRARIES_KEY) ?? "[]");
+      if (Array.isArray(stored)) setSaved(new Set(stored.filter((slug): slug is string => typeof slug === "string")));
-    try { setSaved(readSaved(localStorage.getItem(SAVED_LIBRARIES_KEY), validSlugs)); } catch { /* Session state remains available. */ }
-    const sync = (event: StorageEvent) => {
-      if (event.key === SAVED_LIBRARIES_KEY || event.key === null) setSaved(readSaved(event.key === null ? null : event.newValue, validSlugs));
-    };
-    window.addEventListener("storage", sync);
+    } catch {
+      localStorage.removeItem(SAVED_LIBRARIES_KEY);
+    }
-    return () => window.removeEventListener("storage", sync);
   }, []);
 
+  const results = useMemo(() => {
+    const tokens = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
+    return libraries.filter((library) =>
+      (tokens.length === 0 || matchesQuery(library, tokens)) &&
+      (category === null || library.category === category) &&
+      (stacks.length === 0 || stacks.every((stack) => library.stacks.includes(stack))) &&
-  useEffect(() => {
+      (useCases.length === 0 || useCases.every((useCase) => library.useCases.includes(useCase))),
+    );
+  }, [query, category, stacks, useCases]);
-    const shortcut = (event: KeyboardEvent) => {
-      if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
-        event.preventDefault();
-        searchRef.current?.focus();
-      }
-    };
-    window.addEventListener("keydown", shortcut);
-    return () => window.removeEventListener("keydown", shortcut);
-  }, []);
 
+  const visibleResults = (showSaved ? results.filter(({ slug }) => saved.has(slug)) : results)
-  const results = useMemo(() => filterLibraries(libraries, query, category, stacks, useCases, sort), [query, category, stacks, useCases, sort]);
+    .toSorted((a, b) => sort === "name" ? a.name.localeCompare(b.name) : 0);
 
-  const visibleResults = showSaved ? results.filter(({ slug }) => saved.has(slug)) : results;
-
   const toggleSaved = (slug: string) => {
-    let stored: string | null | undefined;
-    try { stored = localStorage.getItem(SAVED_LIBRARIES_KEY); } catch { /* Use session state. */ }
-    const next = nextSaved(saved, stored, slug, validSlugs);
-    try {
-      localStorage.setItem(SAVED_LIBRARIES_KEY, JSON.stringify([...next]));
+    setSaved((current) => {
+      const next = new Set(current);
+      if (next.has(slug)) next.delete(slug);
+      else next.add(slug);
+      try {
+        localStorage.setItem(SAVED_LIBRARIES_KEY, JSON.stringify([...next]));
+      } catch {
+        // Keep the selection for this session when storage is unavailable.
+      }
+      return next;
-    } catch {
+    });
-      // Keep the selection for this session when storage is unavailable.
-    }
-    setSaved(next);
   };
 
   const clearFilters = () => {
@@ -67,7 +76,6 @@
     setCategory(null);
     setStacks([]);
     setUseCases([]);
-    searchRef.current?.focus();
   };
 
   return (
@@ -78,7 +86,7 @@
           <p className="theme-muted mt-2 text-sm">Search and filter the complete Col library directory.</p>
         </div>
         <div className="flex items-center gap-3">
-          <span role="status" aria-live="polite" className="theme-muted text-sm tabular-nums">{visibleResults.length} {visibleResults.length === 1 ? "library" : "libraries"}{visibleResults.length === 0 ? " found" : ""}</span>
+          <span className="theme-muted text-sm tabular-nums">{visibleResults.length} libraries</span>
           <FilterDropdown
             label={sort === "curated" ? "Curated order" : "Name A–Z"}
             value={sort}
@@ -94,7 +102,6 @@
 
       <FilterBar
         query={query}
-        searchRef={searchRef}
         onQueryChange={setQuery}
         activeCategory={category}
         activeStacks={stacks}
```

## components/FilterBar.tsx

Status: modified; +10; −9

```diff
@@ -1,6 +1,5 @@
 "use client";
 
-import type { RefObject } from "react";
 import { ChevronDown, RotateCcw, Search } from "lucide-react";
 import { CATEGORIES, STACKS, USE_CASES, type Category, type Stack, type UseCase } from "@/data/libraries";
 import { Button } from "@/components/ui/button";
@@ -12,6 +11,7 @@
   DropdownMenuTrigger,
 } from "@/components/ui/dropdown-menu";
 import { Input } from "@/components/ui/input";
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
 
 interface FilterDropdownProps {
   label: string;
@@ -48,7 +48,6 @@
   activeStacks: Stack[];
   activeUseCases: UseCase[];
   query: string;
-  searchRef: RefObject<HTMLInputElement | null>;
   onQueryChange: (query: string) => void;
   onCategoryChange: (category: Category | null) => void;
   onStackChange: (stack: Stack | null) => void;
@@ -56,7 +55,7 @@
   onClearAll: () => void;
 }
 
-export function FilterBar({ activeCategory, activeStacks, activeUseCases, query, searchRef, onQueryChange, onCategoryChange, onStackChange, onUseCaseChange, onClearAll }: FilterBarProps) {
+export function FilterBar({ activeCategory, activeStacks, activeUseCases, query, onQueryChange, onCategoryChange, onStackChange, onUseCaseChange, onClearAll }: FilterBarProps) {
   const hasFilters = query.trim() !== "" || activeCategory !== null || activeStacks.length > 0 || activeUseCases.length > 0;
 
   return (
@@ -64,8 +63,8 @@
       <div className="grid gap-2 lg:grid-cols-[minmax(320px,1fr)_180px_210px_auto]">
         <label className="relative block">
           <Search className="theme-muted pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" aria-hidden />
+          <Input value={query} onChange={(event) => onQueryChange(event.target.value)} placeholder="Search libraries, tags, or keywords..." aria-label="Search libraries" className="coss-input h-10 w-full pr-16 pl-10" />
+          <kbd className="theme-muted theme-border pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 rounded border px-2 py-1 text-[10px]">Ctrl K</kbd>
-          <Input ref={searchRef} value={query} onChange={(event) => onQueryChange(event.target.value)} placeholder="Search libraries, tags, or keywords..." aria-label="Search libraries" className="coss-input h-10 w-full pr-16 pl-10" />
-          <kbd className="theme-muted theme-border pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 rounded border px-2 py-1 text-[10px]">Ctrl/⌘ K</kbd>
         </label>
 
         <FilterDropdown
@@ -84,10 +83,12 @@
         {hasFilters && <Button type="button" variant="outline" onClick={onClearAll} className="coss-trigger"><RotateCcw aria-hidden /> Clear</Button>}
       </div>
 
+      <Tabs value={activeCategory ?? "all"} onValueChange={(value) => onCategoryChange(value === "all" ? null : value as Category)}>
+        <TabsList className="h-auto! w-full flex-wrap justify-start gap-1 rounded-none bg-transparent p-0">
+          <TabsTrigger value="all" className="filter-tab flex-none rounded-md px-3 py-1.5">All</TabsTrigger>
+          {CATEGORIES.map((category) => <TabsTrigger key={category} value={category} className="filter-tab flex-none rounded-md px-3 py-1.5">{category}</TabsTrigger>)}
+        </TabsList>
+      </Tabs>
-      <div role="group" aria-label="Category" className="flex flex-wrap justify-start gap-1">
-        <button type="button" aria-pressed={activeCategory === null} onClick={() => onCategoryChange(null)} className="filter-tab flex-none rounded-md border border-transparent px-3 py-1.5">All</button>
-        {CATEGORIES.map((category) => <button type="button" key={category} aria-pressed={activeCategory === category} onClick={() => onCategoryChange(category)} className="filter-tab flex-none rounded-md border border-transparent px-3 py-1.5">{category}</button>)}
-      </div>
     </div>
   );
 }
```

## components/GlareHover.tsx

Status: added; +108; −0

```diff
@@ -1,0 +1,108 @@
+import React, { useRef } from 'react';
+
+interface GlareHoverProps {
+  width?: string;
+  height?: string;
+  background?: string;
+  borderRadius?: string;
+  borderColor?: string;
+  children?: React.ReactNode;
+  glareColor?: string;
+  glareOpacity?: number;
+  glareAngle?: number;
+  glareSize?: number;
+  transitionDuration?: number;
+  playOnce?: boolean;
+  className?: string;
+  style?: React.CSSProperties;
+}
+
+const GlareHover: React.FC<GlareHoverProps> = ({
+  width = '500px',
+  height = '500px',
+  background = '#000',
+  borderRadius = '10px',
+  borderColor = '#333',
+  children,
+  glareColor = '#ffffff',
+  glareOpacity = 0.5,
+  glareAngle = -45,
+  glareSize = 250,
+  transitionDuration = 650,
+  playOnce = false,
+  className = '',
+  style = {}
+}) => {
+  const hex = glareColor.replace('#', '');
+  let rgba = glareColor;
+  if (/^[\dA-Fa-f]{6}$/.test(hex)) {
+    const r = parseInt(hex.slice(0, 2), 16);
+    const g = parseInt(hex.slice(2, 4), 16);
+    const b = parseInt(hex.slice(4, 6), 16);
+    rgba = `rgba(${r}, ${g}, ${b}, ${glareOpacity})`;
+  } else if (/^[\dA-Fa-f]{3}$/.test(hex)) {
+    const r = parseInt(hex[0] + hex[0], 16);
+    const g = parseInt(hex[1] + hex[1], 16);
+    const b = parseInt(hex[2] + hex[2], 16);
+    rgba = `rgba(${r}, ${g}, ${b}, ${glareOpacity})`;
+  }
+
+  const overlayRef = useRef<HTMLDivElement | null>(null);
+
+  const animateIn = () => {
+    const el = overlayRef.current;
+    if (!el) return;
+
+    el.style.transition = 'none';
+    el.style.backgroundPosition = '-100% -100%, 0 0';
+    el.style.transition = `${transitionDuration}ms ease`;
+    el.style.backgroundPosition = '100% 100%, 0 0';
+  };
+
+  const animateOut = () => {
+    const el = overlayRef.current;
+    if (!el) return;
+
+    if (playOnce) {
+      el.style.transition = 'none';
+      el.style.backgroundPosition = '-100% -100%, 0 0';
+    } else {
+      el.style.transition = `${transitionDuration}ms ease`;
+      el.style.backgroundPosition = '-100% -100%, 0 0';
+    }
+  };
+
+  const overlayStyle: React.CSSProperties = {
+    position: 'absolute',
+    inset: 0,
+    background: `linear-gradient(${glareAngle}deg,
+        hsla(0,0%,0%,0) 60%,
+        ${rgba} 70%,
+        hsla(0,0%,0%,0) 100%)`,
+    backgroundSize: `${glareSize}% ${glareSize}%, 100% 100%`,
+    backgroundRepeat: 'no-repeat',
+    backgroundPosition: '-100% -100%, 0 0',
+    pointerEvents: 'none'
+  };
+
+  return (
+    <div
+      className={`relative grid place-items-center overflow-hidden border cursor-pointer ${className}`}
+      style={{
+        width,
+        height,
+        background,
+        borderRadius,
+        borderColor,
+        ...style
+      }}
+      onMouseEnter={animateIn}
+      onMouseLeave={animateOut}
+    >
+      <div ref={overlayRef} style={overlayStyle} />
+      {children}
+    </div>
+  );
+};
+
+export default GlareHover;
```

## components/Header.tsx

Status: modified; +3; −6

```diff
@@ -115,7 +115,7 @@
           >
             <span ref={highlightRef} className="site-header-link-highlight" aria-hidden="true" />
             {links.map(([label, href]) => {
-              const active = !href.includes("#") && (pathname === href || pathname.startsWith(`${href}/`));
+              const active = pathname === href || pathname.startsWith(`${href}/`);
               return (
                 <a
                   key={href}
@@ -165,11 +165,8 @@
 
         {menuOpen && (
           <nav id="site-header-mobile-menu" aria-label="Mobile primary" className="site-header-mobile-menu">
+            {links.map(([label, href]) => <a key={href} href={href} aria-current={pathname === href || pathname.startsWith(`${href}/`) ? "page" : undefined}>{label}</a>)}
-            {links.map(([label, href]) => <a key={href} href={href} onClick={() => setMenuOpen(false)} aria-current={!href.includes("#") && (pathname === href || pathname.startsWith(`${href}/`)) ? "page" : undefined}>{label}</a>)}
+            <a href="/#hero-search">Search libraries</a>
-            <a href="/#hero-search" onClick={() => {
-              setMenuOpen(false);
-              if (pathname === "/") document.querySelector<HTMLInputElement>("#hero-search input")?.focus();
-            }}>Search libraries</a>
           </nav>
         )}
       </div>
```

## components/HeroLogoTrail.tsx

Status: added; +99; −0

```diff
@@ -1,0 +1,99 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+const DOMAINS = [
+  "ui.shadcn.com",
+  "reactbits.dev",
+  "21st.dev",
+  "magicui.design",
+  "ui.aceternity.com",
+  "motion.dev",
+  "www.radix-ui.com",
+  "mui.com",
+  "mantine.dev",
+  "chakra-ui.com",
+  "lucide.dev",
+  "gsap.com",
+];
+
+const favicon = (domain: string) =>
+  `https://www.google.com/s2/favicons?domain=${domain}&sz=128`;
+
+export function HeroLogoTrail() {
+  const trailRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    const trail = trailRef.current;
+    const hero = trail?.parentElement;
+    if (
+      !trail ||
+      !hero ||
+      !matchMedia(
+        "(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)",
+      ).matches
+    )
+      return;
+
+    const logos = [...trail.querySelectorAll<HTMLImageElement>("img")];
+    const safeZones = [
+      ...hero.querySelectorAll<HTMLElement>("[data-trail-safe]"),
+    ];
+    let index = 0;
+    let previousX = -100;
+    let previousY = -100;
+
+    const showLogo = (event: PointerEvent) => {
+      if (Math.hypot(event.clientX - previousX, event.clientY - previousY) < 72)
+        return;
+
+      const overContent = safeZones.some((element) => {
+        const rect = element.getBoundingClientRect();
+        return (
+          event.clientX >= rect.left - 32 &&
+          event.clientX <= rect.right + 32 &&
+          event.clientY >= rect.top - 32 &&
+          event.clientY <= rect.bottom + 32
+        );
+      });
+      if (overContent) return;
+
+      const bounds = hero.getBoundingClientRect();
+      const logo = logos[index++ % logos.length];
+      previousX = event.clientX;
+      previousY = event.clientY;
+      logo.style.left = `${event.clientX - bounds.left}px`;
+      logo.style.top = `${event.clientY - bounds.top}px`;
+      logo.animate(
+        [
+          { opacity: 0, transform: "translate(-50%, -50%) scale(.45) rotate(-8deg)" },
+          { opacity: 0.95, offset: 0.12, transform: "translate(-50%, -50%) scale(1) rotate(0deg)" },
+          { opacity: 0.85, offset: 0.68, transform: "translate(-50%, calc(-50% - 14px)) scale(.96) rotate(3deg)" },
+          { opacity: 0, transform: "translate(-50%, calc(-50% - 38px)) scale(.82) rotate(8deg)" },
+        ],
+        { duration: 1800, easing: "cubic-bezier(.16, 1, .3, 1)" },
+      );
+    };
+
+    hero.addEventListener("pointermove", showLogo);
+    return () => hero.removeEventListener("pointermove", showLogo);
+  }, []);
+
+  return (
+    <div
+      ref={trailRef}
+      aria-hidden
+      className="hero-logo-trail pointer-events-none absolute inset-0 z-[1] overflow-hidden"
+    >
+      {DOMAINS.map((domain) => (
+        // eslint-disable-next-line @next/next/no-img-element
+        <img
+          key={domain}
+          src={favicon(domain)}
+          alt=""
+          className="absolute size-12 object-contain opacity-0 drop-shadow-[0_12px_18px_rgba(0,0,0,0.65)]"
+        />
+      ))}
+    </div>
+  );
+}
```

## components/LibraryCard.tsx

Status: modified; +2; −2

```diff
@@ -48,8 +48,8 @@
 
       <CardFooter className="library-subtle theme-border flex justify-between border-t px-5 pt-3 text-[10px]">
         <div className="flex items-center gap-4">
-          <span className="flex items-center gap-1.5"><Layers3 className="size-3" aria-hidden />{library.stacks.length} {library.stacks.length === 1 ? "stack" : "stacks"}</span>
-          <span className="flex items-center gap-1.5"><Target className="size-3" aria-hidden />{library.useCases.length} {library.useCases.length === 1 ? "use" : "uses"}</span>
+          <span className="flex items-center gap-1.5"><Layers3 className="size-3" aria-hidden />{library.stacks.length} stacks</span>
+          <span className="flex items-center gap-1.5"><Target className="size-3" aria-hidden />{library.useCases.length} uses</span>
         </div>
         <Button asChild size="icon-xs" variant="ghost" className="library-subtle rounded-md hover:opacity-80">
           <a href={library.url} target="_blank" rel="noopener noreferrer" aria-label={`Open ${library.name}`}><ArrowUpRight aria-hidden /></a>
```

## components/LibraryExplorer.tsx

Status: modified; +4; −3

```diff
@@ -2,7 +2,7 @@
 
 import { useEffect, useState } from "react";
 import Image from "next/image";
-import { BookOpen, Layers3, LayoutGrid } from "lucide-react";
+import { BookOpen, GitFork, Layers3, LayoutGrid } from "lucide-react";
 import { CATEGORIES, STACKS, libraries } from "@/data/libraries";
 import { SearchBar } from "./SearchBar";
 import { Button } from "@/components/ui/button";
@@ -88,6 +88,7 @@
     { value: `${libraries.length}`, label: "Curated libraries", Icon: BookOpen },
     { value: `${CATEGORIES.length}`, label: "Categories", Icon: LayoutGrid },
     { value: `${STACKS.length}`, label: "Tech stacks", Icon: Layers3 },
+    { value: "100%", label: "Open source", Icon: GitFork },
   ];
 
   return (
@@ -112,7 +113,7 @@
             the libraries that developers love, <span className="hero-accent-text">all in one place</span>
           </h1>
           <p className="hero-description mt-5 max-w-[560px] text-base leading-[1.55] sm:text-[17px]">
+            Discover, explore, and use the best open source libraries, components, and tools in one place. Curated by developers for developers.
-            Discover UI libraries, components, and tools by stack and use case.
           </p>
           <div className="animate-fade-up delay-1 mt-8 flex w-full justify-start">
             <SearchBar
@@ -139,7 +140,7 @@
         </div>
       </div>
 
+      <div data-trail-safe className="hero-stats absolute bottom-10 left-1/2 z-10 hidden w-[min(1408px,calc(100%-3rem))] -translate-x-1/2 grid-cols-2 md:grid md:grid-cols-4">
-      <div data-trail-safe className="hero-stats absolute bottom-10 left-1/2 z-10 hidden w-[min(1408px,calc(100%-3rem))] -translate-x-1/2 grid-cols-2 md:grid md:grid-cols-3">
         {stats.map(({ value, label, Icon }, index) => (
           <div key={label} className={`hero-stat flex items-center gap-4 px-5 py-2 ${index > 0 ? "border-l" : ""}`}>
             <span className="hero-stat-icon grid size-12 shrink-0 place-items-center rounded-xl border">
```

## components/Marquee.tsx

Status: added; +22; −0

```diff
@@ -1,0 +1,22 @@
+import { libraries } from "@/data/libraries";
+
+export function Marquee() {
+  const names = libraries.map((l) => l.name);
+  // Rendered twice side-by-side so translateX(-50%) loops seamlessly.
+  const row = [...names, ...names];
+
+  return (
+    <div className="marquee-mask overflow-hidden border-y border-white/[0.06] py-5">
+      <div className="animate-marquee flex w-max items-center">
+        {row.map((name, i) => (
+          <span key={i} className="flex items-center">
+            <span className="px-6 font-mono text-xs tracking-[0.25em] whitespace-nowrap text-zinc-600 uppercase">
+              {name}
+            </span>
+            <span aria-hidden className="size-1 rounded-full bg-zinc-800" />
+          </span>
+        ))}
+      </div>
+    </div>
+  );
+}
```

## components/RevealText.tsx

Status: added; +53; −0

```diff
@@ -1,0 +1,53 @@
+"use client";
+
+import { useRef } from "react";
+import gsap from "gsap";
+import { ScrollTrigger } from "gsap/ScrollTrigger";
+import { useGSAP } from "@gsap/react";
+
+gsap.registerPlugin(ScrollTrigger);
+
+interface RevealTextProps {
+  text: string;
+  className?: string;
+}
+
+/** Words scrub from dim to fully lit as the user scrolls through them. */
+export function RevealText({ text, className = "" }: RevealTextProps) {
+  const ref = useRef<HTMLParagraphElement>(null);
+
+  useGSAP(
+    () => {
+      if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
+      const words = ref.current?.querySelectorAll("[data-word]");
+      if (!words?.length) return;
+      gsap.fromTo(
+        words,
+        { opacity: 0.12 },
+        {
+          opacity: 1,
+          stagger: 0.06,
+          ease: "none",
+          scrollTrigger: {
+            trigger: ref.current,
+            start: "top 82%",
+            end: "bottom 45%",
+            scrub: 0.6,
+          },
+        },
+      );
+    },
+    { scope: ref },
+  );
+
+  return (
+    <p ref={ref} className={className}>
+      {text.split(" ").map((word, i) => (
+        <span key={i} data-word className="inline-block">
+          {word}
+          {i < text.split(" ").length - 1 ? "\u00A0" : ""}
+        </span>
+      ))}
+    </p>
+  );
+}
```

[Next page](https://git.cafe/screen/col/pull/9/diffs?after=ImNvbXBvbmVudHMvUmV2ZWFsVGV4dC50c3gi&expectedVersion=0&format=markdown)
