From 7e46ddfe18af8b94c08b63b2a488dc9bbcbd2250 Mon Sep 17 00:00:00 2001 From: Tyler Hogarth <5163494+tylerhogarth@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:11:33 +0200 Subject: [PATCH 1/4] Replace /stack with the Prisma Stack landing page Rebuild the /stack route as a Prisma Stack marketing page: hero, an interactive "stack simplified" flow diagram (Compute, Postgres, Bun, TypeScript with SVG connectors and a cycling frontend framework), a vertical framework carousel, a Bun API grid, a Prisma Postgres data section with highlighted code, and a CTA. Built with Tailwind + @prisma/eclipse (Button, Card, Action, CodeBlock) and semantic design tokens so it themes in light and dark. Diagram and marquee geometry live in a scoped CSS module; product marks use Font Awesome plus inline Prisma and Bun marks tinted to currentColor. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/site/src/app/stack/bun-mark.tsx | 17 + .../site/src/app/stack/framework-carousel.tsx | 47 ++ apps/site/src/app/stack/page.tsx | 274 ++++++++--- apps/site/src/app/stack/prisma-mark.tsx | 8 + apps/site/src/app/stack/reveal.tsx | 41 ++ apps/site/src/app/stack/stack-data.ts | 137 +++--- apps/site/src/app/stack/stack-diagram.tsx | 303 ++++++++++++ apps/site/src/app/stack/stack.module.css | 449 ++++++++++++++++++ 8 files changed, 1136 insertions(+), 140 deletions(-) create mode 100644 apps/site/src/app/stack/bun-mark.tsx create mode 100644 apps/site/src/app/stack/framework-carousel.tsx create mode 100644 apps/site/src/app/stack/prisma-mark.tsx create mode 100644 apps/site/src/app/stack/reveal.tsx create mode 100644 apps/site/src/app/stack/stack-diagram.tsx create mode 100644 apps/site/src/app/stack/stack.module.css diff --git a/apps/site/src/app/stack/bun-mark.tsx b/apps/site/src/app/stack/bun-mark.tsx new file mode 100644 index 0000000000..37c0b71e41 --- /dev/null +++ b/apps/site/src/app/stack/bun-mark.tsx @@ -0,0 +1,17 @@ +/** + * The Bun mark, drawn as a single-colour silhouette (with eyes + smile cut out + * via even-odd) in currentColor so it matches the Prisma mark's treatment. + */ +export function BunMark({ className }: { className?: string }) { + const silhouette = + "M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75Z"; + const leftEye = "M20.19,33.29a5.51,5.51 0 1,0 11.02,0a5.51,5.51 0 1,0 -11.02,0Z"; + const rightEye = "M44.96,33.29a5.51,5.51 0 1,0 11.02,0a5.51,5.51 0 1,0 -11.02,0Z"; + const mouth = + "M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z"; + return ( + + + + ); +} diff --git a/apps/site/src/app/stack/framework-carousel.tsx b/apps/site/src/app/stack/framework-carousel.tsx new file mode 100644 index 0000000000..124e4fb532 --- /dev/null +++ b/apps/site/src/app/stack/framework-carousel.tsx @@ -0,0 +1,47 @@ +import { Action, Card } from "@prisma/eclipse"; +import { frameworks } from "./stack-data"; +import styles from "./stack.module.css"; + +/** + * Vertical stack: a scrolling marquee of the frameworks Prisma Compute runs, + * with a Prisma Compute card beneath it. Both are full-width so the pairing + * reads cleanly on mobile. The compute icon matches the diagram node. + */ +export function FrameworkCarousel() { + return ( +
+ +
+
+ {[...frameworks, ...frameworks].map((f, i) => ( +
+
+ {f.mono} +
+
{f.name}
+
+ ))} +
+
+
+ +
+ +
+ + + + + +
+ + Prisma Compute + + + Same runtime, same database, same deploy for any framework. + +
+
+
+ ); +} diff --git a/apps/site/src/app/stack/page.tsx b/apps/site/src/app/stack/page.tsx index e0a8540e1e..5f198a98b8 100644 --- a/apps/site/src/app/stack/page.tsx +++ b/apps/site/src/app/stack/page.tsx @@ -1,18 +1,17 @@ import { createPageMetadata } from "@/lib/page-metadata"; -import { Card } from "@prisma/eclipse"; -import { Layers } from "lucide-react"; -import Image from "next/image"; -import Link from "next/link"; -import { cn } from "@/lib/cn"; -import { - stackCategories, - type StackCategory, - type StackLinkItem, -} from "./stack-data"; +import { prisma_highlighter } from "@/lib/shiki_prisma"; +import { Action, Button, Card, CodeBlock } from "@prisma/eclipse"; +import type { ReactNode } from "react"; +import { FrameworkCarousel } from "./framework-carousel"; +import { PrismaMark } from "./prisma-mark"; +import { Reveal } from "./reveal"; +import { StackDiagram } from "./stack-diagram"; +import { bunApis, postgresExtensions } from "./stack-data"; +import styles from "./stack.module.css"; -const title = "Prisma in your stack | Prisma"; +const title = "Prisma Stack: a complete TypeScript foundation | Prisma"; const description = - "Prisma is a Node.js and TypeScript ORM that integrates easily with popular databases, and frameworks."; + "Prisma Compute, Prisma Postgres, Bun and TypeScript. A complete, opinionated stack with room for your frontend."; export const metadata = createPageMetadata({ title, @@ -21,60 +20,221 @@ export const metadata = createPageMetadata({ ogImage: "/og/og-stack.png", }); +const ORM_CODE = `const posts = await prisma.post.findMany({ + where: { published: true }, + include: { author: true }, // fully typed +})`; + +const EXT_CODE = `-- semantic search, no extra service +CREATE EXTENSION vector; + +SELECT id, content +FROM docs +ORDER BY embedding <-> $query +LIMIT 5;`; + +async function Code({ code, lang }: { code: string; lang: "typescript" | "sql" }) { + const html = await (await prisma_highlighter()).codeToHtml(code, { lang, theme: "prisma-dark" }); + return ( +
+ +
+ +
+ ); +} + +function SectionHead({ + eyebrow, + title, + children, +}: { + eyebrow?: string; + title: string; + children?: ReactNode; +}) { + return ( + + {eyebrow ? {eyebrow} : null} +

{title}

+ {children ?

{children}

: null} +
+ ); +} + export default function StackPage() { return ( -
-
-
-
-
-
- - Prisma Stack -
-

- Build with -
your favorite tools -

-
+
+ {/* ===== HERO ===== */} +
+
+
+
+

+ Prisma Stack +

+

+ Stop configuring. Start shipping. +

+

+ Most apps have the same recurring problems. Prisma Stack solves them with the fewest + dependencies running on one platform. Great performance without learning and configuring + multiple frameworks or providers. +

-
-
- {stackCategories.map((category: StackCategory) => ( -
-

- {category.title} -

-

- {category.description} -

-
- {category.items.map((item: StackLinkItem) => ( - - - {item.name} - - {item.name} +
+ {/* ===== THE STACK, SIMPLIFIED + DIAGRAM ===== */} +
+
+ + + You can still deploy your app or database wherever you'd like. Use any dependency + you're familiar with. Prisma Stack still works. + +
+
+ + {/* ===== FRONTEND CAROUSEL ===== */} +
+
+ + Prisma Compute runs every major frontend framework. Same database, same runtime, same + deploy. Your call on the view layer. + + + + +
+
+ + {/* ===== BUN ===== */} +
+
+ Bun simplifies your dependencies. +
+ {bunApis.map((b) => ( + + + + {b.api} + +

{b.role}

+
+
+ ))} +
+
+
+ + {/* ===== DATA ===== */} +
+
+ + A real Postgres database with a type-safe ORM on top and the extensions you actually + use, one line away. + +
+ + +
+ + + +

Prisma ORM

+
+

+ Model your schema once. Get autocompletion, compile-time safety and migrations + for free. +

+
    +
  • + + Type-safe queries, end to end +
  • +
  • + + Declarative schema and versioned migrations +
  • +
  • + + Agent guardrails against unsafe writes +
  • +
+ +
+
+ + +
+ + + +

+ Postgres extensions +

+
+

+ Opinionated defaults you can swap. Turn on vector search, full-text and cron + without leaving Postgres. +

+
+ {postgresExtensions.map((ext) => ( + + {ext} - - - ))} + ))} +
+ +
+
+
+
+
+ + {/* ===== FINAL CTA ===== */} +
+
+
+
+

+ Stop configuring. Start shipping. +

+

+ The database, the runtime and the compute layer are decided and connected. You + write features. +

+ +

+ + bun create prisma-stack@latest + +

- ))} +
-
+
); } diff --git a/apps/site/src/app/stack/prisma-mark.tsx b/apps/site/src/app/stack/prisma-mark.tsx new file mode 100644 index 0000000000..3e85a65827 --- /dev/null +++ b/apps/site/src/app/stack/prisma-mark.tsx @@ -0,0 +1,8 @@ +/** The Prisma prism mark, drawn in currentColor so it inherits its tile's colour. */ +export function PrismaMark({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/apps/site/src/app/stack/reveal.tsx b/apps/site/src/app/stack/reveal.tsx new file mode 100644 index 0000000000..0ecbbd0119 --- /dev/null +++ b/apps/site/src/app/stack/reveal.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useEffect, useRef, type ReactNode } from "react"; +import styles from "./stack.module.css"; + +/** Fades + lifts its children into view the first time they intersect. */ +export function Reveal({ + children, + className, + as: Tag = "div", +}: { + children: ReactNode; + className?: string; + as?: "div" | "section"; +}) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const io = new IntersectionObserver( + (entries) => { + entries.forEach((e) => { + if (e.isIntersecting) { + e.target.classList.add(styles.in); + io.unobserve(e.target); + } + }); + }, + { threshold: 0.12 }, + ); + io.observe(el); + return () => io.disconnect(); + }, []); + + return ( + + {children} + + ); +} diff --git a/apps/site/src/app/stack/stack-data.ts b/apps/site/src/app/stack/stack-data.ts index 0c993cf1eb..9544fc334d 100644 --- a/apps/site/src/app/stack/stack-data.ts +++ b/apps/site/src/app/stack/stack-data.ts @@ -1,89 +1,60 @@ -export type StackLinkItem = { - id: string; +export type Framework = { name: string; - href: string; - icon: string; - /** Black/monochrome marks: invert in dark mode so they stay visible on dark UI */ - invertInDark?: boolean; + /** Short monogram shown in the badge tile */ + mono: string; + /** Accent color for the badge glyph */ + color: string; + /** Badge tile background */ + bg: string; }; -export type StackCategory = { - id: string; - title: string; - description: string; - items: StackLinkItem[]; -}; - -/** - * Twelve stack routes only — grouped like the legacy /stack page: - * Languages → Databases → Frameworks. - */ -export const stackCategories: StackCategory[] = [ - { - id: "languages", - title: "Languages", - description: "Prisma can be used in any Node.js or TypeScript backend application.", - items: [ - { - id: "typescript", - name: "TypeScript", - href: "/typescript", - icon: "/icons/technologies/ts.svg", - }, - ], - }, - { - id: "databases", - title: "Databases", - description: "Prisma works seamlessly across most popular databases and service providers.", - items: [ - { - id: "mongodb", - name: "MongoDB", - href: "/mongodb", - icon: "/icons/technologies/mongo.svg", - }, - { - id: "cockroachdb", - name: "CockroachDB", - href: "/cockroachdb", - icon: "/icons/technologies/cockroach.svg", - }, - { - id: "planetscale", - name: "PlanetScale", - href: "/planetscale", - icon: "/icons/technologies/planetscale.svg", - invertInDark: true, - }, - ], - }, +/** Frameworks that Prisma Compute runs, cycled through the diagram + marquee. */ +export const frameworks: Framework[] = [ { - id: "frameworks", - title: "Frameworks", - description: - "Here is a non-exhaustive list of libraries and frameworks you can use with Prisma.", - items: [ - { id: "nextjs", name: "Next.js", href: "/nextjs", icon: "/icons/technologies/nextjs.svg" }, - { id: "nestjs", name: "NestJS", href: "/nestjs", icon: "/icons/technologies/nestjs.svg" }, - { id: "react", name: "React", href: "/react", icon: "/icons/technologies/react.svg" }, - { - id: "express", - name: "Express", - href: "/express", - icon: "/icons/technologies/express.svg", - invertInDark: true, - }, - { id: "hapi", name: "Hapi", href: "/hapi", icon: "/icons/technologies/hapi.svg" }, - { id: "graphql", name: "GraphQL", href: "/graphql", icon: "/icons/technologies/graphql.svg" }, - { - id: "fastify", - name: "Fastify", - href: "/fastify", - icon: "/icons/technologies/fastify.svg", - invertInDark: true, - }, - { id: "apollo", name: "Apollo", href: "/apollo", icon: "/icons/technologies/apollo.svg" }, - ], + name: "Next.js", + mono: "N", + color: "var(--color-foreground-neutral)", + bg: "var(--color-background-neutral-weak)", }, + { name: "React Router", mono: "RR", color: "#f44250", bg: "rgba(244,66,80,0.14)" }, + { name: "TanStack Start", mono: "TS", color: "#ff6b35", bg: "rgba(255,107,53,0.14)" }, + { name: "Astro", mono: "A", color: "#ff5d01", bg: "rgba(255,93,1,0.14)" }, + { name: "SvelteKit", mono: "S", color: "#ff3e00", bg: "rgba(255,62,0,0.14)" }, + { name: "Nuxt", mono: "V", color: "#00dc82", bg: "rgba(0,220,130,0.14)" }, + { name: "SolidStart", mono: "So", color: "#4c86c6", bg: "rgba(70,130,200,0.18)" }, + { name: "Qwik City", mono: "Q", color: "#18b6f6", bg: "rgba(24,182,246,0.14)" }, + { name: "Remix", mono: "Rx", color: "#3992ff", bg: "rgba(57,146,255,0.16)" }, + { name: "Vite + React", mono: "Vt", color: "#bd34fe", bg: "rgba(189,52,254,0.16)" }, +]; + +export type BunApi = { + api: string; + role: string; +}; + +/** Bun capabilities that ship with Prisma Compute. */ +export const bunApis: BunApi[] = [ + { api: "Bun.serve()", role: "HTTP + WebSocket server, built in." }, + { api: "Bun.image", role: "Resize, convert and optimise images." }, + { api: "bun test", role: "Jest-compatible test runner." }, + { api: "Bun.password", role: "Argon2 / bcrypt hashing." }, + { api: "bun install", role: "Package manager, fast lockfile." }, + { api: "Bun bundler", role: "Bundle and transpile, no config." }, + { api: "Bun.$``", role: "Shell scripting in TypeScript." }, + { api: "Bun.s3", role: "S3-compatible object storage client." }, + { api: "Bun.redis", role: "Built-in Redis / cache client." }, + { api: "Bun.file", role: "Fast file reads, writes and streams." }, + { api: ".env (native)", role: "Auto-loads environment files." }, + { api: "TS + JSX", role: "Runs TypeScript directly, no build." }, +]; + +/** Postgres extensions surfaced in the data section. */ +export const postgresExtensions: string[] = [ + "pgvector", + "pg_search", + "Vector type", + "pg_cron", + "pg_trgm", + "pg_stat_statements", + "uuid-ossp", ]; diff --git a/apps/site/src/app/stack/stack-diagram.tsx b/apps/site/src/app/stack/stack-diagram.tsx new file mode 100644 index 0000000000..0286c2c081 --- /dev/null +++ b/apps/site/src/app/stack/stack-diagram.tsx @@ -0,0 +1,303 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react"; +import { Button } from "@prisma/eclipse"; +import { BunMark } from "./bun-mark"; +import { PrismaMark } from "./prisma-mark"; +import styles from "./stack.module.css"; +import { frameworks } from "./stack-data"; + +const accentPPG = { "--accent": "var(--color-foreground-ppg)" } as CSSProperties; +const accentViolet = { "--accent": "var(--color-foreground-violet)" } as CSSProperties; + +/** Orthogonal connectors drawn between diagram nodes. */ +const FLOW_LINKS: [string, string][] = [ + ["n-frontend", "n-compute"], + ["n-backend", "n-compute"], + ["n-orm", "n-postgres"], + ["n-compute", "n-fimage"], + ["n-compute", "n-ffile"], + ["n-compute", "n-fredis"], + ["n-compute", "n-fmore"], + ["n-postgres", "n-pgvector"], + ["n-postgres", "n-pgsearch"], + ["n-postgres", "n-pgstat"], + ["n-postgres", "n-pgmore"], +]; + +function Leaf({ id, name, role }: { id: string; name: string; role: string }) { + return ( +
+
+ {name} {role} +
+
+ ); +} + +export function StackDiagram() { + const rootRef = useRef(null); + const flowRef = useRef(null); + const svgRef = useRef(null); + const badgeRef = useRef(null); + const [idx, setIdx] = useState(0); + const [swapping, setSwapping] = useState(false); + + const fw = frameworks[idx]; + + const drawFlow = useCallback(() => { + const flow = flowRef.current; + const svg = svgRef.current; + const root = rootRef.current; + if (!flow || !svg || !root) return; + + // measure in the diagram's natural (unscaled) coordinate space + flow.style.transform = "none"; + flow.style.marginBottom = ""; + const W = flow.offsetWidth; + const H = flow.offsetHeight; + + // offset geometry (relative to #flow) is immune to the scale transform + // applied below, unlike getBoundingClientRect + const rel = (id: string) => { + const el = root.querySelector(`[data-node="${id}"]`); + if (!el) return null; + let x = 0; + let y = 0; + let n: HTMLElement | null = el; + while (n && n !== flow) { + x += n.offsetLeft; + y += n.offsetTop; + n = n.offsetParent as HTMLElement | null; + } + const w = el.offsetWidth; + const h = el.offsetHeight; + return { left: x, right: x + w, top: y, bottom: y + h, cx: x + w / 2, cy: y + h / 2 }; + }; + const elbow = (a: string, b: string) => { + const s = rel(a); + const t = rel(b); + if (!s || !t) return ""; + const mx = (s.right + t.left) / 2; + return `M ${s.right} ${s.cy} H ${mx} V ${t.cy} H ${t.left}`; + }; + + let paths = FLOW_LINKS.map((l) => { + const d = elbow(l[0], l[1]); + return d + ? `` + : ""; + }).join(""); + + // dashed sub-ms latency link between Compute and Postgres + const c = rel("n-compute"); + const p = rel("n-postgres"); + if (c && p) { + const lx = c.cx; + paths += ``; + const badge = badgeRef.current; + if (badge) { + badge.style.left = `${lx}px`; + badge.style.top = `${(c.bottom + p.top) / 2}px`; + } + } + + svg.setAttribute("width", String(W)); + svg.setAttribute("height", String(H)); + svg.setAttribute("viewBox", `0 0 ${W} ${H}`); + svg.innerHTML = + `` + + paths; + + // scale the diagram down to fit its container so it never needs + // horizontal scrolling on narrower viewports + const scroller = flow.parentElement; + if (!scroller) return; + const cs = getComputedStyle(scroller); + const avail = scroller.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight); + const scale = Math.min(1, avail / W); + if (scale < 1) { + const tx = (avail - W * scale) / 2; + flow.style.transformOrigin = "top left"; + flow.style.transform = `translateX(${tx}px) scale(${scale})`; + flow.style.marginBottom = `${-(1 - scale) * H}px`; + } + }, []); + + // cycle the frontend framework in the header + diagram node + useEffect(() => { + const interval = setInterval(() => { + setSwapping(true); + setTimeout(() => { + setIdx((i) => (i + 1) % frameworks.length); + setSwapping(false); + }, 240); + }, 2000); + return () => clearInterval(interval); + }, []); + + // redraw connectors after the frontend node width changes + useEffect(() => { + drawFlow(); + }, [idx, drawFlow]); + + // draw on mount, on resize, and once fonts are ready + useEffect(() => { + drawFlow(); + window.addEventListener("resize", drawFlow); + if (document.fonts?.ready) document.fonts.ready.then(drawFlow); + const t1 = setTimeout(drawFlow, 300); + const t2 = setTimeout(drawFlow, 900); + return () => { + window.removeEventListener("resize", drawFlow); + clearTimeout(t1); + clearTimeout(t2); + }; + }, [drawFlow]); + + return ( +
+

+ The stack simplified: + + + Prisma + + Bun + Postgres + + + + + {fw.name} + +

+ +
+ +

+ + bun create prisma-stack@latest + {" "} + and you have compute, a database, and a runtime. +

+
+ +
+
+ {/* Column 1: everything you write, grouped under TypeScript */} +
+
+
+
Frontend
+
+ {fw.mono} + {fw.name} +
+ Fully open +
+
+
Backend
+
+ + + + Bun +
+
test · build
+
+
+
+
+
Data access
+
+ + + + Prisma ORM +
+
type-safety · query · migrations
+
+
+
TypeScript
+
+ + {/* Column 2: managed platform */} +
+
+ + + + Prisma Compute +
+
+ Bun runtime +
+
+
+
+ + + + Prisma Postgres +
+
+ Managed Postgres +
+
+ + {/* Column 3: Compute's Bun features + Postgres extensions */} +
+ + + +
+
...
+
+
+
+ + + +
+
...
+
+
+ + +
+ sub-ms latency +
+
+
+
+ ); +} diff --git a/apps/site/src/app/stack/stack.module.css b/apps/site/src/app/stack/stack.module.css new file mode 100644 index 0000000000..4c9233e939 --- /dev/null +++ b/apps/site/src/app/stack/stack.module.css @@ -0,0 +1,449 @@ +/* + * Prisma Stack — flow-diagram + marquee geometry only. + * + * The page chrome (hero, sections, cards, code, CTA) is built with Tailwind + * and @prisma/eclipse components, matching the compute/postgres pages. This + * module holds only what those primitives can't express: the subgrid diagram, + * its SVG connectors, the "stack simplified" bracket, and the framework + * marquee. All colours come from semantic design tokens so it themes in both + * modes. Class names are referenced from JSX via styles["kebab-name"]. + */ + +.page { + --panel-from: var(--color-background-default); + --panel-to: var(--color-background-ppg); + --border: var(--color-stroke-neutral); + --border-strong: var(--color-stroke-neutral-strong); + --border-ppg: var(--color-stroke-ppg); + --line: var(--color-stroke-neutral-strong); + --ink: var(--color-foreground-neutral); + --ink-dim: var(--color-foreground-neutral-weak); + --ink-faint: var(--color-foreground-neutral-weaker); + --teal: var(--color-foreground-ppg); + --teal-bright: var(--color-foreground-ppg-weak); + --violet: var(--color-foreground-violet); + --font-display: var(--font-mona-sans), "Mona Sans VF", "Inter", sans-serif; + + position: relative; + width: 100%; + /* the diagram is 820px wide before it scales to fit; clip so it can never + widen the page and cause horizontal overflow on narrow viewports */ + overflow-x: clip; +} +.content { + position: relative; + z-index: 1; +} + +/* ===== "stack simplified" one-liner + include bracket ===== */ +.stack-simplified { + text-align: center; + max-width: 940px; + margin: 0 auto; + font-weight: 800; + font-family: var(--font-display); +} +.stack-simplified .ss-eq { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: baseline; + column-gap: 0.28em; + margin-top: 10px; + text-align: left; +} +.stack-simplified .ss-leftcol { + grid-column: 1; + justify-self: end; + display: flex; + flex-direction: column; + align-items: flex-start; +} +.stack-simplified .ss-brand { + white-space: nowrap; + background: linear-gradient(100deg, var(--teal-bright), var(--color-foreground-orm)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +.stack-simplified .ss-plus { + grid-column: 2; + white-space: nowrap; + color: var(--ink-faint); +} +.stack-simplified .ss-fw { + grid-column: 3; + justify-self: start; + min-width: 0; + white-space: nowrap; + color: var(--ink); + transition: opacity 0.35s ease; +} +.stack-simplified.swapping .ss-fw { + opacity: 0; +} +/* the include bracket: one vertical rail spanning the two branch centres, + each branch a horizontal stub that meets the rail with no gap */ +.stack-simplified .ss-includes { + position: relative; + width: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 4px 0 0 0.3em; + font-size: 0.68em; + font-weight: 600; +} +/* vertical rail descends from just below "Prisma" down to the last branch */ +.stack-simplified .ss-includes::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0.9em; + width: 2px; + background: var(--line); +} +.stack-simplified .ss-inc { + position: relative; + padding-left: 1.05em; + line-height: 1.8; + white-space: nowrap; + color: var(--ink-dim); +} +.stack-simplified .ss-inc::before { + content: ""; + position: absolute; + left: 0; + top: 0.9em; + transform: translateY(-50%); + width: 0.8em; + height: 2px; + background: var(--line); +} + +/* ===== flow diagram ===== */ +.flow-scroll { + overflow: hidden; + padding: 12px 6px 52px; +} +.flow { + position: relative; + display: grid; + grid-template-columns: auto max-content max-content; + grid-template-areas: + "tsg compute feat" + "tsg postgres ext"; + align-items: center; + column-gap: 70px; + row-gap: 48px; + min-width: 820px; + width: max-content; + margin: 0 auto; +} +.ts-group { + grid-area: tsg; + position: relative; + display: grid; + grid-template-rows: subgrid; + align-items: center; + padding: 0 16px; +} +/* container box; a pseudo-element so its height is independent of the subgrid */ +.ts-group::before { + content: ""; + position: absolute; + inset: -6px 0 -34px 0; + border-radius: 20px; + background: color-mix(in srgb, var(--teal) 6%, transparent); + border: 1px solid color-mix(in srgb, var(--teal) 26%, transparent); + z-index: -1; +} +.ts-sub { + display: flex; + flex-direction: column; + gap: 18px; +} +.ts-label { + position: absolute; + left: 0; + right: 0; + bottom: -26px; + z-index: 1; + text-align: center; + font-family: var(--font-display); + font-weight: 800; + letter-spacing: 0.04em; + font-size: 13px; + color: var(--teal); +} +.n-compute { + grid-area: compute; + justify-self: start; +} +.n-postgres { + grid-area: postgres; + justify-self: start; +} +.feat-stack { + grid-area: feat; + display: flex; + flex-direction: column; + gap: 20px; +} +.ext-stack { + grid-area: ext; + display: flex; + flex-direction: column; + gap: 20px; +} +.flow-svg { + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + overflow: visible; +} +.flow-path { + fill: none; + stroke: var(--line); + stroke-width: 1.5; +} +.flow-path.dashed { + stroke: var(--teal); + stroke-dasharray: 5 5; +} +.flow-badge { + position: absolute; + z-index: 5; + transform: translate(-50%, -50%); + display: inline-flex; + align-items: center; + gap: 7px; + background: var(--color-background-default); + border: 1px solid color-mix(in srgb, var(--teal) 45%, transparent); + color: var(--teal); + font-size: 11px; + font-weight: 700; + padding: 5px 10px; + border-radius: 999px; + white-space: nowrap; + box-shadow: var(--shadow-box-low); +} + +/* ===== node ===== */ +.node { + position: relative; + z-index: 1; + border-radius: 12px; + border: 1px solid var(--border); + background: linear-gradient(180deg, var(--panel-from) 0%, var(--panel-to) 240%); + padding: 12px 15px; + min-width: 168px; + white-space: nowrap; + transition: + transform 0.18s, + border-color 0.18s, + box-shadow 0.18s; +} +.node:hover { + transform: translateY(-2px); + border-color: var(--border-ppg); + box-shadow: var(--shadow-box); +} +.node::before { + content: ""; + position: absolute; + left: 0; + top: 10px; + bottom: 10px; + width: 3px; + border-radius: 3px; + background: var(--accent, var(--teal)); +} +.node-eyebrow { + font-size: 10px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--ink-faint); + font-weight: 700; +} +.node-title { + font-family: var(--font-display); + font-weight: 800; + font-size: 15px; + display: flex; + align-items: center; + gap: 9px; + color: var(--ink); +} +.node-sub { + font-size: 11.5px; + color: var(--ink-dim); + margin-top: 3px; + display: flex; + align-items: center; + gap: 6px; +} +/* Action-style icon tile (mirrors eclipse Action color="ppg") */ +.node-icon { + width: 26px; + height: 26px; + border-radius: 6px; + display: grid; + place-items: center; + flex: 0 0 auto; + font-size: 12px; + background: var(--color-background-ppg); + color: var(--color-foreground-ppg); + border: 1px solid color-mix(in srgb, var(--accent, var(--teal)) 30%, transparent); +} +.node.root .node-icon { + width: 30px; + height: 30px; + font-size: 14px; +} +/* framework monogram tile (violet "open" layer) */ +.mono-badge { + width: 26px; + height: 26px; + border-radius: 6px; + display: grid; + place-items: center; + font-family: var(--font-display); + font-weight: 900; + font-size: 12px; + flex: 0 0 auto; + background: color-mix(in srgb, var(--violet) 16%, transparent); + color: var(--violet); + border: 1px solid color-mix(in srgb, var(--violet) 34%, transparent); +} +.pill { + display: inline-block; + margin-top: 8px; + font-size: 9.5px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 3px 8px; + border-radius: 6px; +} +.pill.open { + color: var(--violet); + background: color-mix(in srgb, var(--violet) 14%, transparent); + border: 1px solid color-mix(in srgb, var(--violet) 30%, transparent); +} +.node.leaf { + min-width: 150px; + padding: 10px 14px; + background: color-mix(in srgb, var(--ink) 3%, transparent); +} +.node.leaf .node-title { + font-size: 13px; + font-weight: 600; + font-family: inherit; +} +.node.leaf::before { + top: 8px; + bottom: 8px; +} +.node.leaf.dots-leaf { + min-width: 60px; + text-align: center; +} +.node.leaf.dots-leaf .node-title { + justify-content: center; + color: var(--ink-faint); + font-weight: 800; + letter-spacing: 3px; +} +.node.root { + min-width: 208px; + border-color: color-mix(in srgb, var(--teal) 40%, transparent); +} +.node.root .node-title { + font-size: 16px; +} +.n-frontend .node-title { + transition: opacity 0.35s ease; +} +.n-frontend.swapping .node-title { + opacity: 0; +} +.leaf-dim { + color: var(--ink-faint); + font-weight: 400; +} + +/* ===== framework marquee (vertical) ===== */ +.fw-marquee { + position: relative; + width: 100%; + height: 232px; + overflow: hidden; + -webkit-mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent); + mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent); +} +.fw-track { + animation: fwscroll 22s linear infinite; + will-change: transform; +} +@keyframes fwscroll { + from { + transform: translateY(-50%); + } + to { + transform: translateY(0); + } +} +.fw-row { + display: flex; + align-items: center; + gap: 14px; + height: 66px; + margin-bottom: 14px; + padding: 0 20px; + border: 1px solid var(--border); + border-radius: 12px; + background: linear-gradient(180deg, var(--panel-from) 0%, var(--panel-to) 260%); +} +.fw-logo-sm { + width: 42px; + height: 42px; + border-radius: 10px; + display: grid; + place-items: center; + font-family: var(--font-display); + font-weight: 900; + font-size: 16px; + border: 1px solid var(--border-strong); + flex: 0 0 auto; +} +.fw-row-name { + font-family: var(--font-display); + font-weight: 800; + font-size: 17px; + color: var(--ink); +} + +/* ===== scroll reveal ===== */ +.reveal { + opacity: 0; + transform: translateY(24px); + transition: + opacity 0.7s ease, + transform 0.7s ease; +} +.reveal.in { + opacity: 1; + transform: none; +} +@media (prefers-reduced-motion: reduce) { + .reveal { + opacity: 1; + transform: none; + transition: none; + } + .fw-track { + animation: none; + } +} From 534506cac9345066bba09ec130f8f45c14fd5d21 Mon Sep 17 00:00:00 2001 From: Tyler Hogarth <5163494+tylerhogarth@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:23:48 +0200 Subject: [PATCH 2/4] Reuse shared Marquee for the framework carousel Drop the bespoke .fw-* marquee CSS and fwscroll keyframe in favour of the existing @/components/marquee (two-copy loop, marquee-up keyframes, fade mask). Row styling moves to Tailwind. Trims the CSS module to the diagram/bracket geometry that has no eclipse/Tailwind equivalent. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../site/src/app/stack/framework-carousel.tsx | 37 +++++++++---- apps/site/src/app/stack/stack.module.css | 55 ------------------- 2 files changed, 25 insertions(+), 67 deletions(-) diff --git a/apps/site/src/app/stack/framework-carousel.tsx b/apps/site/src/app/stack/framework-carousel.tsx index 124e4fb532..74d6d99f06 100644 --- a/apps/site/src/app/stack/framework-carousel.tsx +++ b/apps/site/src/app/stack/framework-carousel.tsx @@ -1,6 +1,7 @@ +import { Marquee } from "@/components/marquee"; import { Action, Card } from "@prisma/eclipse"; +import type { CSSProperties } from "react"; import { frameworks } from "./stack-data"; -import styles from "./stack.module.css"; /** * Vertical stack: a scrolling marquee of the frameworks Prisma Compute runs, @@ -11,18 +12,30 @@ export function FrameworkCarousel() { return (
-
-
- {[...frameworks, ...frameworks].map((f, i) => ( -
-
- {f.mono} -
-
{f.name}
+ + {frameworks.map((f) => ( +
+
+ {f.mono}
- ))} -
- +
+ {f.name} +
+ + ))} +
diff --git a/apps/site/src/app/stack/stack.module.css b/apps/site/src/app/stack/stack.module.css index 4c9233e939..5dfb0872ed 100644 --- a/apps/site/src/app/stack/stack.module.css +++ b/apps/site/src/app/stack/stack.module.css @@ -13,7 +13,6 @@ --panel-from: var(--color-background-default); --panel-to: var(--color-background-ppg); --border: var(--color-stroke-neutral); - --border-strong: var(--color-stroke-neutral-strong); --border-ppg: var(--color-stroke-ppg); --line: var(--color-stroke-neutral-strong); --ink: var(--color-foreground-neutral); @@ -374,57 +373,6 @@ font-weight: 400; } -/* ===== framework marquee (vertical) ===== */ -.fw-marquee { - position: relative; - width: 100%; - height: 232px; - overflow: hidden; - -webkit-mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent); - mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent); -} -.fw-track { - animation: fwscroll 22s linear infinite; - will-change: transform; -} -@keyframes fwscroll { - from { - transform: translateY(-50%); - } - to { - transform: translateY(0); - } -} -.fw-row { - display: flex; - align-items: center; - gap: 14px; - height: 66px; - margin-bottom: 14px; - padding: 0 20px; - border: 1px solid var(--border); - border-radius: 12px; - background: linear-gradient(180deg, var(--panel-from) 0%, var(--panel-to) 260%); -} -.fw-logo-sm { - width: 42px; - height: 42px; - border-radius: 10px; - display: grid; - place-items: center; - font-family: var(--font-display); - font-weight: 900; - font-size: 16px; - border: 1px solid var(--border-strong); - flex: 0 0 auto; -} -.fw-row-name { - font-family: var(--font-display); - font-weight: 800; - font-size: 17px; - color: var(--ink); -} - /* ===== scroll reveal ===== */ .reveal { opacity: 0; @@ -443,7 +391,4 @@ transform: none; transition: none; } - .fw-track { - animation: none; - } } From 664bc31411808bcc9fa07bee920d7ec5a3d4b6f7 Mon Sep 17 00:00:00 2001 From: Tyler Hogarth <5163494+tylerhogarth@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:28:40 +0200 Subject: [PATCH 3/4] Address PR review: timer cleanup, resize debounce, a11y, reduced motion - Track and clear the framework-swap setTimeout on unmount - rAF-batch the resize handler so drag-resize doesn't thrash layout - aria-hidden the runtime-built connector SVG - Drop the unused polymorphic `as` prop and `ref as never` cast in Reveal - Extend prefers-reduced-motion to the diagram's swap + hover transitions Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/site/src/app/stack/reveal.tsx | 14 +++----------- apps/site/src/app/stack/stack-diagram.tsx | 21 ++++++++++++++++----- apps/site/src/app/stack/stack.module.css | 9 +++++++++ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apps/site/src/app/stack/reveal.tsx b/apps/site/src/app/stack/reveal.tsx index 0ecbbd0119..20aec40ac9 100644 --- a/apps/site/src/app/stack/reveal.tsx +++ b/apps/site/src/app/stack/reveal.tsx @@ -4,15 +4,7 @@ import { useEffect, useRef, type ReactNode } from "react"; import styles from "./stack.module.css"; /** Fades + lifts its children into view the first time they intersect. */ -export function Reveal({ - children, - className, - as: Tag = "div", -}: { - children: ReactNode; - className?: string; - as?: "div" | "section"; -}) { +export function Reveal({ children, className }: { children: ReactNode; className?: string }) { const ref = useRef(null); useEffect(() => { @@ -34,8 +26,8 @@ export function Reveal({ }, []); return ( - +
{children} - +
); } diff --git a/apps/site/src/app/stack/stack-diagram.tsx b/apps/site/src/app/stack/stack-diagram.tsx index 0286c2c081..f82c309204 100644 --- a/apps/site/src/app/stack/stack-diagram.tsx +++ b/apps/site/src/app/stack/stack-diagram.tsx @@ -126,14 +126,18 @@ export function StackDiagram() { // cycle the frontend framework in the header + diagram node useEffect(() => { + let swapTimeout: ReturnType; const interval = setInterval(() => { setSwapping(true); - setTimeout(() => { + swapTimeout = setTimeout(() => { setIdx((i) => (i + 1) % frameworks.length); setSwapping(false); }, 240); }, 2000); - return () => clearInterval(interval); + return () => { + clearInterval(interval); + clearTimeout(swapTimeout); + }; }, []); // redraw connectors after the frontend node width changes @@ -143,13 +147,20 @@ export function StackDiagram() { // draw on mount, on resize, and once fonts are ready useEffect(() => { + // rAF-batch resize so a drag-resize doesn't thrash layout on every event + let raf = 0; + const onResize = () => { + cancelAnimationFrame(raf); + raf = requestAnimationFrame(drawFlow); + }; drawFlow(); - window.addEventListener("resize", drawFlow); + window.addEventListener("resize", onResize); if (document.fonts?.ready) document.fonts.ready.then(drawFlow); const t1 = setTimeout(drawFlow, 300); const t2 = setTimeout(drawFlow, 900); return () => { - window.removeEventListener("resize", drawFlow); + window.removeEventListener("resize", onResize); + cancelAnimationFrame(raf); clearTimeout(t1); clearTimeout(t2); }; @@ -292,7 +303,7 @@ export function StackDiagram() {
- +
sub-ms latency
diff --git a/apps/site/src/app/stack/stack.module.css b/apps/site/src/app/stack/stack.module.css index 5dfb0872ed..8cc20f34eb 100644 --- a/apps/site/src/app/stack/stack.module.css +++ b/apps/site/src/app/stack/stack.module.css @@ -391,4 +391,13 @@ transform: none; transition: none; } + /* the diagram auto-cycles frameworks, so silence its swap + hover motion too */ + .stack-simplified .ss-fw, + .n-frontend .node-title, + .node { + transition: none; + } + .node:hover { + transform: none; + } } From 342bc36ff779a78314cb9d096788694d27fd9f5e Mon Sep 17 00:00:00 2001 From: Tyler Hogarth <5163494+tylerhogarth@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:15:37 +0200 Subject: [PATCH 4/4] Update install command to npx create-prisma Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/site/src/app/stack/page.tsx | 4 +--- apps/site/src/app/stack/stack-diagram.tsx | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/site/src/app/stack/page.tsx b/apps/site/src/app/stack/page.tsx index 5f198a98b8..6f092787bb 100644 --- a/apps/site/src/app/stack/page.tsx +++ b/apps/site/src/app/stack/page.tsx @@ -227,9 +227,7 @@ export default function StackPage() {

- - bun create prisma-stack@latest - + npx create-prisma

diff --git a/apps/site/src/app/stack/stack-diagram.tsx b/apps/site/src/app/stack/stack-diagram.tsx index f82c309204..ddc621cc19 100644 --- a/apps/site/src/app/stack/stack-diagram.tsx +++ b/apps/site/src/app/stack/stack-diagram.tsx @@ -7,8 +7,12 @@ import { PrismaMark } from "./prisma-mark"; import styles from "./stack.module.css"; import { frameworks } from "./stack-data"; -const accentPPG = { "--accent": "var(--color-foreground-ppg)" } as CSSProperties; -const accentViolet = { "--accent": "var(--color-foreground-violet)" } as CSSProperties; +const accentPPG = { + "--accent": "var(--color-foreground-ppg)", +} as CSSProperties; +const accentViolet = { + "--accent": "var(--color-foreground-violet)", +} as CSSProperties; /** Orthogonal connectors drawn between diagram nodes. */ const FLOW_LINKS: [string, string][] = [ @@ -72,7 +76,14 @@ export function StackDiagram() { } const w = el.offsetWidth; const h = el.offsetHeight; - return { left: x, right: x + w, top: y, bottom: y + h, cx: x + w / 2, cy: y + h / 2 }; + return { + left: x, + right: x + w, + top: y, + bottom: y + h, + cx: x + w / 2, + cy: y + h / 2, + }; }; const elbow = (a: string, b: string) => { const s = rel(a); @@ -195,10 +206,8 @@ export function StackDiagram() {

- - bun create prisma-stack@latest - {" "} - and you have compute, a database, and a runtime. + npx create-prisma and you + have compute, a database, and a runtime.