Exploration ยท not implemented yet
@@ -208,7 +208,7 @@ export default function ExperimentsPage() {diff --git a/.gitignore b/.gitignore
index 12f73ae..e8406ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,6 @@ playwright-report/
.agents/
.claude/
.windsurf/
+
+# Deterministic model-specific Next.js route entries generated before dev/build.
+/src/app/(generated-variant-routes)/
diff --git a/next.config.ts b/next.config.ts
index 96ee744..0c2d8ca 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -2,8 +2,45 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
allowedDevOrigins: ["127.0.0.1"],
+ poweredByHeader: false,
images: {
- unoptimized: true,
+ deviceSizes: [640, 750, 828, 1080, 1200, 1600, 1920],
+ imageSizes: [32, 48, 64, 96, 128, 256, 384],
+ minimumCacheTTL: 86_400,
+ qualities: [70, 75],
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "picsum.photos",
+ },
+ ],
+ },
+ experimental: {
+ optimizePackageImports: ["@phosphor-icons/react"],
+ },
+ async headers() {
+ return [
+ {
+ // Compare output is deterministic for the full URL and contains no user data.
+ // Cache it at Vercel's edge while keeping the browser response revalidated.
+ source: "/compare",
+ headers: [
+ {
+ key: "Vercel-CDN-Cache-Control",
+ value: "public, s-maxage=31536000, stale-while-revalidate=86400",
+ },
+ ],
+ },
+ ...["/gallery-previews/:path*", "/variants/:path*"].map((source) => ({
+ source,
+ headers: [
+ {
+ key: "Cache-Control",
+ value: "public, max-age=86400, stale-while-revalidate=604800",
+ },
+ ],
+ })),
+ ];
},
};
diff --git a/package.json b/package.json
index 70a151c..cc0b446 100644
--- a/package.json
+++ b/package.json
@@ -5,12 +5,13 @@
"scripts": {
"dev": "next dev",
"build": "next build",
- "predev": "node scripts/scope-variant-css.mjs",
- "prebuild": "node scripts/scope-variant-css.mjs",
+ "predev": "node scripts/generate-variant-routes.mjs && node scripts/scope-variant-css.mjs",
+ "prebuild": "node scripts/generate-variant-routes.mjs && node scripts/scope-variant-css.mjs",
"start": "next start",
"lint": "eslint",
"capture-previews": "node scripts/capture-previews.mjs",
"scope:variant-css": "node scripts/scope-variant-css.mjs",
+ "generate:variant-routes": "node scripts/generate-variant-routes.mjs",
"test:routes": "playwright test tests/gallery-routes.spec.ts",
"test:visual": "playwright test tests/gallery-visual-smoke.spec.ts"
},
diff --git a/scripts/generate-variant-routes.mjs b/scripts/generate-variant-routes.mjs
new file mode 100644
index 0000000..fd35676
--- /dev/null
+++ b/scripts/generate-variant-routes.mjs
@@ -0,0 +1,92 @@
+import { readdir, rm, mkdir, writeFile } from "node:fs/promises";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const variantsRoot = path.join(projectRoot, "src", "variants");
+const appRoot = path.join(projectRoot, "src", "app");
+const outputRoot = path.join(appRoot, "(generated-variant-routes)");
+const nextRoot = path.join(projectRoot, ".next");
+
+if (path.dirname(outputRoot) !== appRoot) {
+ throw new Error(`Refusing to generate routes outside the app directory: ${outputRoot}`);
+}
+
+const iterationIds = ["1", "2", "3", "4", "5"];
+
+function pageSource({ group, model, preview }) {
+ const componentName = preview ? "PreviewIterationPage" : "IterationPage";
+ return `import "./variant-tailwind.css";
+import { GalleryIterationView } from "@/components/gallery/gallery-iteration-view";
+import variantModule from "@/variants/${group}/${model}";
+
+export const dynamicParams = false;
+
+export function generateStaticParams() {
+ return ${JSON.stringify(iterationIds)}.map((iteration) => ({ iteration }));
+}
+
+export default async function ${componentName}({
+ params,
+}: {
+ params: Promise<{ iteration: string }>;
+}) {
+ const { iteration } = await params;
+ return (
+ Exploration ยท not implemented yet
Back to the{" "}
-
+
main gallery
.
diff --git a/src/app/globals.css b/src/app/globals.css
index 8d702d2..a0e2870 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,4 +1,6 @@
-@import "tailwindcss";
+@import "tailwindcss" source(none);
+@source ".";
+@source "../components";
@custom-variant dark (&:where(.dark, .dark *));
@@ -111,6 +113,10 @@ body {
.gallery-card-shell {
border-color: #d4d4d4;
+ /* Render long gallery grids incrementally instead of activating a whole
+ section in one main-thread task as it enters the viewport. */
+ content-visibility: auto;
+ contain-intrinsic-size: auto 28rem;
}
.gallery-card-shell:hover {
@@ -403,6 +409,23 @@ html {
animation: spin-slow 28s linear infinite;
}
+@keyframes gallery-toast-enter {
+ from {
+ opacity: 0;
+ translate: 0 0.35rem;
+ scale: 0.97;
+ }
+ to {
+ opacity: 1;
+ translate: 0 0;
+ scale: 1;
+ }
+}
+
+.gallery-toast-enter {
+ animation: gallery-toast-enter 160ms cubic-bezier(0.22, 1, 0.36, 1) both;
+}
+
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
diff --git a/src/app/lab-guess/page.tsx b/src/app/lab-guess/page.tsx
index 7a6faf4..de027b2 100644
--- a/src/app/lab-guess/page.tsx
+++ b/src/app/lab-guess/page.tsx
@@ -1,11 +1,12 @@
import { GalleryRankingsNav } from "@/components/gallery/gallery-rankings-nav";
import { ModelLabWordle } from "@/components/game/model-lab-wordle";
+import { galleryCatalog } from "@/lib/gallery-catalog";
export default function LabGuessPage() {
return (
<>
-