diff --git a/.changeset/ultracite-8-formatting.md b/.changeset/ultracite-8-formatting.md new file mode 100644 index 0000000..2ce4c2d --- /dev/null +++ b/.changeset/ultracite-8-formatting.md @@ -0,0 +1,4 @@ +--- +--- + +Formatting only: apply the blank-line spacing that ultracite 8 enforces and write the Redis field pattern with `\w`. No change in behaviour, so no release. diff --git a/apps/web/app/changelog/rss.xml/route.ts b/apps/web/app/changelog/rss.xml/route.ts index 240f3e6..0fc6400 100644 --- a/apps/web/app/changelog/rss.xml/route.ts +++ b/apps/web/app/changelog/rss.xml/route.ts @@ -12,6 +12,7 @@ const escapeXml = (value: string): string => const handler = () => { const entries = getChangelogEntries(); + const items = entries .map( (entry) => ` @@ -23,6 +24,7 @@ const handler = () => { ` ) .join("\n"); + const lastBuildDate = entries[0]?.data.date.toUTCString() ?? ""; const feed = ` diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index a08b6a2..f104fb2 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -8,6 +8,7 @@ import { fonts } from "@/lib/fonts"; import { url } from "@/lib/url"; const siteName = "VecStore SDK"; + const siteDescription = "A unified vector store SDK for TypeScript. One filter language across Qdrant, pgvector, Pinecone, Supabase, Upstash Vector, Cloudflare Vectorize, and Redis, so you can switch providers without rewriting queries."; diff --git a/apps/web/app/llms.txt/route.ts b/apps/web/app/llms.txt/route.ts index 9cdf0fc..3e37531 100644 --- a/apps/web/app/llms.txt/route.ts +++ b/apps/web/app/llms.txt/route.ts @@ -5,6 +5,7 @@ export const revalidate = false; const handler = async () => { const pages = await Promise.all(source.getPages().map(getLLMText)); + return new Response(pages.join("\n\n"), { headers: { "Content-Type": "text/plain" }, }); diff --git a/apps/web/app/og/docs/[...slug]/route.ts b/apps/web/app/og/docs/[...slug]/route.ts index a97ece4..55bfc61 100644 --- a/apps/web/app/og/docs/[...slug]/route.ts +++ b/apps/web/app/og/docs/[...slug]/route.ts @@ -13,10 +13,12 @@ const backgrounds = path.join(process.cwd(), "app", "og", "docs", "[...slug]"); const readBackground = (slugs: readonly string[]) => { const [group, name] = slugs; + const provider = group === "providers" && providerIds.some((id) => id === name) ? name : undefined; + const file = provider ? `background-${provider}.png` : "background.png"; return readFile(path.join(backgrounds, file)); diff --git a/apps/web/components/brand-menu.tsx b/apps/web/components/brand-menu.tsx index 528c3f3..c6530fe 100644 --- a/apps/web/components/brand-menu.tsx +++ b/apps/web/components/brand-menu.tsx @@ -15,15 +15,19 @@ const MENU_GAP = 8; const serialize = (svg: SVGSVGElement): string => { const clone = svg.cloneNode(true); + if (!(clone instanceof SVGSVGElement)) { return ""; } + const ink = document.documentElement.classList.contains("dark") ? "#ededed" : "#171717"; + clone.removeAttribute("class"); clone.removeAttribute("aria-hidden"); clone.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + return new XMLSerializer() .serializeToString(clone) .replaceAll("currentColor", ink); @@ -44,7 +48,9 @@ const CopyTile = ({ if (status === "idle") { return; } + const timer = window.setTimeout(() => setStatus("idle"), COPIED_MS); + return () => window.clearTimeout(timer); }, [status]); @@ -53,9 +59,11 @@ const CopyTile = ({ className="text-gray-1000 hover:bg-gray-alpha-100 flex min-w-0 flex-col gap-2 rounded-md p-2 text-left text-[14px] transition-colors" onClick={async (event) => { event.stopPropagation(); + if (!svgRef.current) { return; } + try { await navigator.clipboard.writeText(serialize(svgRef.current)); setStatus("copied"); @@ -98,18 +106,23 @@ export const BrandMenu = ({ children }: { readonly children: ReactNode }) => { useEffect(() => { const anchor = anchorRef.current?.closest("a") ?? anchorRef.current; + if (!anchor) { return; } + anchor.setAttribute("aria-haspopup", "menu"); anchor.setAttribute("aria-controls", menuId); + const handle = (event: Event) => { event.preventDefault(); const rect = anchor.getBoundingClientRect(); setPosition({ left: rect.left, top: rect.bottom + MENU_GAP }); setOpen(true); }; + anchor.addEventListener("contextmenu", handle); + return () => anchor.removeEventListener("contextmenu", handle); }, [menuId]); @@ -122,11 +135,14 @@ export const BrandMenu = ({ children }: { readonly children: ReactNode }) => { if (!open) { return; } + menuRef.current?.querySelector("[role=menuitem]")?.focus(); + const close = (event: Event) => { if (event instanceof KeyboardEvent && event.key !== "Escape") { return; } + if ( event instanceof PointerEvent && event.target instanceof Node && @@ -134,11 +150,14 @@ export const BrandMenu = ({ children }: { readonly children: ReactNode }) => { ) { return; } + setOpen(false); (anchorRef.current?.closest("a") ?? anchorRef.current)?.focus(); }; + document.addEventListener("keydown", close); document.addEventListener("pointerdown", close); + return () => { document.removeEventListener("keydown", close); document.removeEventListener("pointerdown", close); @@ -148,34 +167,44 @@ export const BrandMenu = ({ children }: { readonly children: ReactNode }) => { const moveFocus = (event: KeyboardEvent) => { if (event.key === "Tab") { setOpen(false); + return; } + const items = [ ...(menuRef.current?.querySelectorAll("[role=menuitem]") ?? []), ]; + const active = document.activeElement; const index = active instanceof HTMLElement ? items.indexOf(active) : -1; + const resolveTarget = (): HTMLElement | undefined => { switch (event.key) { case "ArrowDown": { return items.at((index + 1) % items.length); } + case "ArrowUp": { return items.at((index - 1 + items.length) % items.length); } + case "End": { return items.at(-1); } + case "Home": { return items[0]; } + default: { return undefined; } } }; + const target = resolveTarget(); + if (target) { event.preventDefault(); target.focus(); diff --git a/apps/web/components/docs/sidebar-items.tsx b/apps/web/components/docs/sidebar-items.tsx index a46869c..dcee151 100644 --- a/apps/web/components/docs/sidebar-items.tsx +++ b/apps/web/components/docs/sidebar-items.tsx @@ -24,16 +24,19 @@ const containsPath = ( if (node.type === "page") { return node.url === pathname; } + if (node.type === "folder") { return ( node.index?.url === pathname || containsPath(node.children, pathname) ); } + return false; }); export const Item = ({ item }: { readonly item: PageTreeItem }) => { const pathname = usePathname(); + return ( navigator.clipboard.writeText(command) ); + const Icon = copied ? CheckIcon : CopyIcon; return ( diff --git a/apps/web/components/landing/demo.tsx b/apps/web/components/landing/demo.tsx index 44b5195..55b94e0 100644 --- a/apps/web/components/landing/demo.tsx +++ b/apps/web/components/landing/demo.tsx @@ -47,6 +47,7 @@ export const Demo = async () => { await renderCode(provider.snippet(example.filterCode)), ]) ); + return { code: Object.fromEntries(entries), expression: example.expression, @@ -56,6 +57,7 @@ export const Demo = async () => { }; }) ); + const providers = demoProviders.map(({ id, label }) => ({ id, label })); return ; diff --git a/apps/web/components/landing/hero-demo.tsx b/apps/web/components/landing/hero-demo.tsx index e6c6ca1..fd1628f 100644 --- a/apps/web/components/landing/hero-demo.tsx +++ b/apps/web/components/landing/hero-demo.tsx @@ -52,15 +52,19 @@ export const HeroDemo = ({ providers, examples }: HeroDemoProps) => { useLayoutEffect(() => { const measure = () => { const tab = tabRefs.current[exampleIndex]; + if (tab) { setIndicator({ left: tab.offsetLeft, width: tab.offsetWidth }); } }; + measure(); const observer = new ResizeObserver(measure); + if (listRef.current) { observer.observe(listRef.current); } + return () => observer.disconnect(); }, [exampleIndex]); const example = examples[exampleIndex] ?? examples[0]; @@ -77,6 +81,7 @@ export const HeroDemo = ({ providers, examples }: HeroDemoProps) => { const selectProvider = (id: string) => { const index = providers.findIndex((item) => item.id === id); + if (index !== -1) { setProviderIndex(index); } @@ -103,6 +108,7 @@ export const HeroDemo = ({ providers, examples }: HeroDemoProps) => { /> {examples.map((item, index) => { const active = index === exampleIndex; + return (