diff --git a/src/__tests__/componentKit.test.ts b/src/__tests__/componentKit.test.ts index d61adc0..226c50b 100644 --- a/src/__tests__/componentKit.test.ts +++ b/src/__tests__/componentKit.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; import { KIT, KIT_BASE, KIT_TOKENS, kitComponent, kitPage, kitStylesheet } from "@/lib/componentKit"; /** @@ -168,3 +169,38 @@ describe("everything clickable is big enough to hit", () => { expect(faq.css).toMatch(/summary \{[^}]*padding:\s*0\.4rem/); }); }); + +describe("the page that shows the kit", () => { + const PAGE = readFileSync("src/app/ui/page.tsx", "utf8"); + const CLIENT = readFileSync("src/app/ui/UiClient.tsx", "utf8"); + + it("builds every preview from the same registry the code blocks come from", () => { + // A preview rendered from anything else could show one thing and copy another. + expect(PAGE).toContain("kitPage([c], \"dark\")"); + expect(PAGE).toContain("kitPage([c], \"light\")"); + expect(PAGE).toContain("components={KIT}"); + expect(CLIENT).toContain("srcDoc={light ? pageFor[c.id].light : pageFor[c.id].dark}"); + }); + + it("copies the component's own html and css, not a reformatted copy of them", () => { + expect(CLIENT).toContain("text={active === \"html\" ? c.html : c.css}"); + expect(CLIENT).toContain("navigator.clipboard.writeText(text)"); + }); + + it("shows each preview in a frame of its own, so this site's CSS cannot reach into it", () => { + expect(CLIENT).toContain(" { + expect(CLIENT).toContain("Phone: 390"); + expect(CLIENT).toContain("Tablet: 768"); + expect(CLIENT).toContain("lightTokens"); + }); + + it("is reachable: in the nav and in the sitemap", () => { + expect(readFileSync("src/components/Navbar.tsx", "utf8")).toContain('href: "/ui"'); + expect(readFileSync("src/app/sitemap.ts", "utf8")).toContain("${siteUrl}/ui"); + }); +}); diff --git a/src/__tests__/routeMetadata.test.ts b/src/__tests__/routeMetadata.test.ts index 2316ac1..92e98d7 100644 --- a/src/__tests__/routeMetadata.test.ts +++ b/src/__tests__/routeMetadata.test.ts @@ -13,6 +13,7 @@ import { pageMeta, SHARE_BASE, DEFAULT_SHARE_IMAGE } from "@/lib/pageMeta"; const ROUTES: [string, string][] = [ ["src/app/page.tsx", "/"], ["src/app/templates/page.tsx", "/templates"], + ["src/app/ui/page.tsx", "/ui"], ["src/app/examples/page.tsx", "/examples"], ["src/app/presets/layout.tsx", "/presets"], ["src/app/changelog/page.tsx", "/changelog"], diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index c85060f..dba7c4f 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -17,6 +17,7 @@ export default function sitemap(): MetadataRoute.Sitemap { changeFrequency: "monthly" as const, priority: 0.7, })), + { url: `${siteUrl}/ui`, lastModified: now, changeFrequency: "weekly", priority: 0.8 }, { url: `${siteUrl}/presets`, lastModified: now, changeFrequency: "weekly", priority: 0.6 }, { url: `${siteUrl}/about`, lastModified: now, changeFrequency: "monthly", priority: 0.5 }, { url: `${siteUrl}/contact`, lastModified: now, changeFrequency: "monthly", priority: 0.4 }, diff --git a/src/app/ui/UiClient.tsx b/src/app/ui/UiClient.tsx new file mode 100644 index 0000000..0fdd34e --- /dev/null +++ b/src/app/ui/UiClient.tsx @@ -0,0 +1,200 @@ +"use client"; +import { useMemo, useState } from "react"; +import { Check, Copy, Monitor, Smartphone, Tablet } from "lucide-react"; +import Navbar from "@/components/Navbar"; +import SiteFooter from "@/components/SiteFooter"; +import { Input } from "@/components/ui/input"; +import type { KitComponent } from "@/lib/componentKit"; + +/** + * The page the kit is actually used from. + * + * Each preview is an iframe of the page that block would make on its own, so what is on + * screen is what the copy button hands over, down to the stylesheet. Rendering it inline + * would let this site's CSS reach into the preview and show somebody a block that behaves + * differently once pasted. + */ + +const WIDTHS = { + Phone: 390, + Tablet: 768, + Desktop: 1180, +} as const; +type WidthName = keyof typeof WIDTHS; + +const ICONS: Record = { Phone: Smartphone, Tablet: Tablet, Desktop: Monitor }; + +function CopyButton({ text, label }: { text: string; label: string }) { + const [copied, setCopied] = useState(false); + return ( + + ); +} + +export default function UiClient({ + components, + tokens, + lightTokens, + base, + pageFor, +}: { + components: KitComponent[]; + tokens: string; + lightTokens: string; + base: string; + /** The exact document each preview shows, built on the server from the same registry. */ + pageFor: Record; +}) { + const [query, setQuery] = useState(""); + const [width, setWidth] = useState("Desktop"); + const [light, setLight] = useState(false); + const [tab, setTab] = useState>({}); + + const shown = useMemo(() => { + const needle = query.trim().toLowerCase(); + if (!needle) return components; + return components.filter( + (c) => c.name.toLowerCase().includes(needle) || c.description.toLowerCase().includes(needle) || c.id.includes(needle) + ); + }, [components, query]); + + return ( +
+ + +
+

{components.length} sections, nothing to install

+

+ Copy a section, + paste it anywhere +

+

+ Plain HTML and CSS. No React, no Tailwind, no build step. These work in a static folder, + a WordPress block or a PHP template, and they read your page's own colours where you have them. +

+
+ +
+
+ + setQuery(e.target.value)} + placeholder="Search sections" + className="lc-mono h-11 bg-card" + /> +
+ +
+
+ {(Object.keys(WIDTHS) as WidthName[]).map((name) => { + const Icon = ICONS[name]; + return ( + + ); + })} +
+ + + +
+ +

+ {shown.length} of {components.length} sections +

+
+ +
+ {shown.map((c) => { + const active = tab[c.id] ?? "html"; + const code = active === "html" ? c.html : c.css; + return ( +
+
+
+

{c.name}

+

{c.description}

+
+ +
+ +
+