From bf93d3aaf161d93598d24049cc6852aad072f5ed Mon Sep 17 00:00:00 2001 From: Parikshit Deshmukh Date: Thu, 27 Aug 2026 19:44:32 +0530 Subject: [PATCH 01/15] Restructure the home page around the three OpenUI products Replaces the stack diagram and the use-cases stepper with one shared ProductSection, rendered three times: Lang on the page background, and Gateway and Observability on the black Cloud band. They differ only in copy and tone, so a shared shape makes them read as three of the same thing rather than three separate pitches. The Cloud band loses its own heading, feature grid and capability ticker; the contact CTA moves to the top of it and takes the section title scale. The feature grid section keeps only its benchmark header and the compatibility band, now in that order. Adds a closing FAQ built on
rather than a JS accordion, so every answer stays in the markup for crawlers and coding agents reading the page, and it works with no JavaScript. The agent picker copies a setup prompt instead of a CLI command, so the per-agent "--agent-name" suffix is gone and the text is copied verbatim. It now sits both in the header and beside the hero command, sharing one exported AGENT_SETUP_PROMPT so the wording cannot drift. Product artwork is still pending: each band's stage renders empty at the right aspect ratio, and the brief for each image is recorded at the top of products.ts. Co-Authored-By: Claude Opus 5 --- .../AgentPicker/AgentPicker.module.css | 227 ++++++++++++++ .../components/AgentPicker/AgentPicker.tsx | 141 +++++++++ docs/app/(home)/page.module.css | 22 +- docs/app/(home)/page.tsx | 31 +- .../CloudSection/CloudSection.module.css | 26 +- .../sections/CloudSection/CloudSection.tsx | 211 +------------ .../sections/FaqSection/FaqSection.module.css | 172 +++++++++++ .../(home)/sections/FaqSection/FaqSection.tsx | 107 +++++++ .../FeatureGridSection.module.css | 8 +- .../FeatureGridSection/FeatureGridSection.tsx | 48 +-- .../sections/HeroSection/HeroSection.tsx | 35 ++- .../ProductSection/ProductSection.module.css | 288 ++++++++++++++++++ .../ProductSection/ProductSection.tsx | 144 +++++++++ .../sections/ProductSection/products.ts | 129 ++++++++ .../TweetWallSection.module.css | 3 + .../TweetWallSection/TweetWallStats.tsx | 2 +- .../UseCasesSection.module.css | 37 --- .../UseCasesSection/UseCasesSection.tsx | 22 -- docs/components/site-marketing-header.tsx | 4 + 19 files changed, 1354 insertions(+), 303 deletions(-) create mode 100644 docs/app/(home)/components/AgentPicker/AgentPicker.module.css create mode 100644 docs/app/(home)/components/AgentPicker/AgentPicker.tsx create mode 100644 docs/app/(home)/sections/FaqSection/FaqSection.module.css create mode 100644 docs/app/(home)/sections/FaqSection/FaqSection.tsx create mode 100644 docs/app/(home)/sections/ProductSection/ProductSection.module.css create mode 100644 docs/app/(home)/sections/ProductSection/ProductSection.tsx create mode 100644 docs/app/(home)/sections/ProductSection/products.ts delete mode 100644 docs/app/(home)/sections/UseCasesSection/UseCasesSection.module.css delete mode 100644 docs/app/(home)/sections/UseCasesSection/UseCasesSection.tsx diff --git a/docs/app/(home)/components/AgentPicker/AgentPicker.module.css b/docs/app/(home)/components/AgentPicker/AgentPicker.module.css new file mode 100644 index 000000000..ea480f051 --- /dev/null +++ b/docs/app/(home)/components/AgentPicker/AgentPicker.module.css @@ -0,0 +1,227 @@ +/* A row of circular agent buttons carrying the site's bevel. Only the first is + visible at rest; the rest sit under it, out of flow, so revealing them never + reflows the row around it. */ +.picker { + --agent-size: 3rem; + --agent-gap: 0.5rem; + --agent-stride: calc(var(--agent-size) + var(--agent-gap)); + + /* The bevel tokens live on the home page's theme, so they are undefined in the + site header and the buttons there would come out flat and transparent. Pinned + locally, the same way GitHubButton does it, so the circles carry the bevel + wherever they are used. */ + --home-surface-ink: #0a0a0a; + --home-surface-raised: #f7f7f8; + --home-bevel: + inset 0 2px 1px -0.5px rgba(255, 255, 255, 1), inset 0 -3px 6px -1px rgba(0, 0, 0, 0.1); + --home-bevel-on-dark: + inset 0 7px 12px -6px rgba(255, 255, 255, 0.16), inset 0 -5px 8px -2px rgba(0, 0, 0, 0.6); + + position: relative; + display: flex; + align-items: center; + gap: var(--agent-gap); + flex-shrink: 0; +} + +/* Matches the compact GitHub button in the header. */ +.pickerCompact { + --agent-size: 2.5rem; +} + +.option { + position: relative; + display: grid; + place-items: center; + flex-shrink: 0; + width: var(--agent-size); + height: var(--agent-size); + padding: 0; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: var(--openui-radius-full); + background: var(--home-surface-raised); + box-shadow: var(--home-bevel); + color: var(--openui-text-neutral-primary); + cursor: pointer; +} + +[data-theme="dark"] .option { + border-color: rgba(255, 255, 255, 0.1); + background: var(--home-surface-ink); + box-shadow: var(--home-bevel-on-dark); +} + +/* Sized as a fraction of the circle rather than in rem, so the compact variant + is an exact scale-down of the full-size one rather than its own proportions. + 20/48 and 26/48 are the full-size measurements. */ +/* Tooltip. Held back behind a delay, so sweeping the pointer across the row on + the way somewhere else never leaves a trail of labels. It goes away at once on + leaving: the delay is only on the way in. */ +.option::after { + content: attr(data-tip); + position: absolute; + top: calc(100% + 0.5rem); + left: 50%; + z-index: 3; + padding: 0.25rem 0.5rem; + /* Sits with the page rather than against it: light on light, dark on dark. The + colours are pinned rather than taken from --openui-text-neutral-primary, + which the home theme holds at near-black on both themes. */ + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 0.375rem; + background: #ffffff; + box-shadow: + 0 4px 10px -6px rgba(10, 10, 10, 0.24), + 0 1px 3px -2px rgba(10, 10, 10, 0.12); + color: #17171b; + font-family: var(--home-font-text, "Inter", sans-serif); + font-size: 0.75rem; + line-height: 1.4; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transform: translate(-50%, -0.25rem); + transition: + opacity 0.12s ease, + transform 0.12s ease; +} + +[data-theme="dark"] .option::after { + border-color: rgba(255, 255, 255, 0.1); + background: #1c1c1f; + box-shadow: + 0 4px 10px -6px rgba(0, 0, 0, 0.8), + 0 1px 3px -2px rgba(0, 0, 0, 0.6); + color: rgba(255, 255, 255, 0.92); +} + +.option:hover::after, +.option:focus-visible::after { + opacity: 1; + transform: translate(-50%, 0); + transition-delay: 0.5s; +} + +.mark { + width: calc(var(--agent-size) * 0.4167); + height: calc(var(--agent-size) * 0.4167); + object-fit: contain; + transition: transform 0.15s ease; +} + +/* Scaled rather than resized, so the mark grows without nudging the circle. */ +.option:hover .mark { + transform: scale(1.15); +} + +/* OpenAI's mark is drawn small inside its own box, so it needs a nudge to carry + the same weight as Claude's and Cursor's. */ +.markWide { + width: calc(var(--agent-size) * 0.5417); + height: calc(var(--agent-size) * 0.5417); +} + +/* Cursor's and Codex's marks are drawn in near-black and would disappear on the + dark page, so those are knocked out to white. Claude's carries its own colour, + so it is left alone on both themes. */ +[data-theme="dark"] .markInvert { + filter: brightness(0) invert(1); +} + +/* Reveal. Only where there is a pointer to hover with: without one the extras + would be unreachable, so they simply stay out. */ +@media (hover: hover) { + /* Bridges the gaps between the circles once they are out. Without it the + pointer leaves the picker's box the moment it crosses a gap, and the row + collapses before it can be reached. Left unpositioned in the stacking order + so it sits, and hit-tests, below the buttons and never swallows a click. */ + .picker::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: var(--agent-size); + height: 100%; + transition: width 0.18s cubic-bezier(0.22, 1, 0.36, 1); + } + + .pickerLeft::after { + left: auto; + right: 0; + } + + .picker:hover::after, + .picker:focus-within::after { + width: calc(2 * var(--agent-stride) + var(--agent-size)); + } + + /* The anchor stays above the two that slide out from under it, and both stay + above the bridge. */ + .picker > .option { + position: relative; + z-index: 2; + } + + .picker > .option:nth-child(2), + .picker > .option:nth-child(3) { + position: absolute; + top: 0; + z-index: 1; + opacity: 0; + pointer-events: none; + /* Swift: they start tucked under the anchor and travel out to their own + place. */ + transition: + transform 0.18s cubic-bezier(0.22, 1, 0.36, 1), + opacity 0.12s ease; + } + + .picker > .option:nth-child(2) { + left: var(--agent-stride); + transform: translateX(calc(-1 * var(--agent-stride))); + } + + .picker > .option:nth-child(3) { + left: calc(2 * var(--agent-stride)); + transform: translateX(calc(-2 * var(--agent-stride))); + } + + /* Mirrored: the row grows back towards the left instead. */ + .pickerLeft > .option:nth-child(2) { + left: auto; + right: var(--agent-stride); + transform: translateX(var(--agent-stride)); + } + + .pickerLeft > .option:nth-child(3) { + left: auto; + right: calc(2 * var(--agent-stride)); + transform: translateX(calc(2 * var(--agent-stride))); + } + + .picker:hover > .option:nth-child(2), + .picker:focus-within > .option:nth-child(2), + .picker:hover > .option:nth-child(3), + .picker:focus-within > .option:nth-child(3) { + opacity: 1; + pointer-events: auto; + transform: translateX(0); + } + + /* The outer one trails the inner one, so they cascade rather than move as a + block. */ + .picker:hover > .option:nth-child(3), + .picker:focus-within > .option:nth-child(3) { + transition-delay: 0.04s; + } +} + +@media (prefers-reduced-motion: reduce) { + .picker > .option { + transition: opacity 0.12s ease; + } + + .option:hover .mark { + transform: none; + } +} diff --git a/docs/app/(home)/components/AgentPicker/AgentPicker.tsx b/docs/app/(home)/components/AgentPicker/AgentPicker.tsx new file mode 100644 index 000000000..b97a1de38 --- /dev/null +++ b/docs/app/(home)/components/AgentPicker/AgentPicker.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { copyText } from "@/lib/copy-text"; +import { Check } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import styles from "./AgentPicker.module.css"; + +// Coding agents the create flow can install the OpenUI skill for. The first one +// is the anchor: it is always visible, and the rest slide out from under it. +// `slug` is the CLI's --agent-name value. `wideMark` is for marks drawn small in +// their own box, which need a nudge to sit at the same visual weight as the +// rest. `invertOnDark` is for marks drawn in near-black, which would disappear +// on the dark page; marks with colour of their own keep it on both themes. +/* What each agent button puts on the clipboard. A prompt to paste into an + agent, not a shell command: it is copied verbatim, with nothing appended. */ +export const AGENT_SETUP_PROMPT = + "Visit OpenUI.com/docs and set up my first Generative UI agent using the recommended OpenUI stack."; + +export const AGENTS = [ + { + id: "claude", + label: "Claude", + mark: "https://cdn.simpleicons.org/claude", + wideMark: false, + invertOnDark: false, + }, + { + id: "cursor", + label: "Cursor", + mark: "https://cdn.simpleicons.org/cursor", + wideMark: false, + invertOnDark: true, + }, + { + id: "codex", + label: "Codex", + mark: "/brand-icons/openai.svg", + wideMark: true, + invertOnDark: true, + }, +] as const; + +const COPY_FEEDBACK_MS = 1800; + +function AgentButton({ + agent, + command, + onCopy, +}: { + agent: (typeof AGENTS)[number]; + command: string; + onCopy?: (command: string) => void; +}) { + const [copied, setCopied] = useState(false); + const resetTimeoutRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (resetTimeoutRef.current) { + clearTimeout(resetTimeoutRef.current); + } + }; + }, []); + + const handleClick = async () => { + if (!(await copyText(command))) return; + onCopy?.(command); + setCopied(true); + if (resetTimeoutRef.current) { + clearTimeout(resetTimeoutRef.current); + } + resetTimeoutRef.current = setTimeout(() => setCopied(false), COPY_FEEDBACK_MS); + }; + + return ( + + ); +} + +/** + * A row of circular agent buttons. Only the first is visible at rest; hovering + * the group slides the others out from under it, to the right by default or to + * the left where the row sits against the edge of its container. + * + * Driven by CSS :hover rather than state, so the extras stay out while the + * pointer is on them (hover propagates from a descendant to its ancestor). + */ +export function AgentPicker({ + command, + className = "", + direction = "right", + compact = false, + onCopy, +}: { + command: string; + className?: string; + direction?: "left" | "right"; + compact?: boolean; + onCopy?: (command: string) => void; +}) { + return ( +
+ {AGENTS.map((agent) => ( + + ))} +
+ ); +} diff --git a/docs/app/(home)/page.module.css b/docs/app/(home)/page.module.css index d8588216b..8a0425437 100644 --- a/docs/app/(home)/page.module.css +++ b/docs/app/(home)/page.module.css @@ -7,6 +7,9 @@ } .heroShell { + /* The tagline used to close this block out; with it hidden the logo strip sat + flush against the content section below. */ + padding-bottom: clamp(3rem, 6vw, 5rem); background: var(--openui-foreground); } @@ -22,10 +25,24 @@ ); } +/* The Lang band sits straight in the shell, which has no gutters of its own, + so it gets the same inline padding the other sections apply themselves. */ +.langBand { + box-sizing: border-box; + width: 100%; + /* Mirrors CloudSection's .inner, so the Lang band lines up with the Gateway + and Observability bands below it rather than sitting 40px wider. */ + max-width: var(--home-container-max); + margin-inline: auto; + padding-inline: clamp(1.25rem, 4vw, 2.5rem); +} + +/* NB: both breakpoints below override this gap, so editing it here has no + visible effect. Change the media queries instead. */ .contentShell { display: flex; flex-direction: column; - gap: 7.5rem; + gap: 5rem; padding-block: 8rem; } @@ -35,7 +52,7 @@ @media (min-width: 768px) { .contentShell { - gap: 15rem; + gap: 5.5rem; } .contentShellTight { @@ -87,4 +104,3 @@ #000 ); } - diff --git a/docs/app/(home)/page.tsx b/docs/app/(home)/page.tsx index 52b7e82cf..eab06b313 100644 --- a/docs/app/(home)/page.tsx +++ b/docs/app/(home)/page.tsx @@ -1,15 +1,17 @@ import type { Metadata } from "next"; +import { AGENT_SETUP_PROMPT, AgentPicker } from "./components/AgentPicker/AgentPicker"; import styles from "./page.module.css"; import { CloudBanner } from "./sections/CloudBanner/CloudBanner"; import { CloudSection } from "./sections/CloudSection/CloudSection"; +import { FaqSection } from "./sections/FaqSection/FaqSection"; import { FeatureGridSection } from "./sections/FeatureGridSection/FeatureGridSection"; import { Footer } from "./sections/Footer/Footer"; -import { HeroSection, Tagline } from "./sections/HeroSection/HeroSection"; +import { HeroSection } from "./sections/HeroSection/HeroSection"; import { LogoStrip } from "./sections/LogoStrip/LogoStrip"; +import { ProductSection } from "./sections/ProductSection/ProductSection"; +import { LANG_PRODUCT } from "./sections/ProductSection/products"; import { ShiroPeek } from "./sections/ShiroPeek/ShiroPeek"; -import { StackDiagramSection } from "./sections/StackDiagramSection/StackDiagramSection"; import { TweetWallSection } from "./sections/TweetWallSection/TweetWallSection"; -import { UseCasesSection } from "./sections/UseCasesSection/UseCasesSection"; export const metadata: Metadata = { alternates: { @@ -26,26 +28,37 @@ export default function HomePage() { subtitle="Open Standard for Generative UI" showPlaygroundButton={false} showTagline={false} + commandTrailing={} /* No GitHub CTA in the hero on either breakpoint: the header carries the star count already. Dropping githubRepoUrl takes the desktop button; the mobile banner defaults on, so it is turned off here. */ showGitHubBanner={false} /> - + {/* Tagline hidden for now; restore this line to bring it back. */} - {/* The diagram opens the content band, so the page gradient starts here - rather than at the use cases below it. */} + {/* The Lang band opens the content section, so the page gradient starts + here rather than at the feature grid below it. */}
-
- - +
+ +
+ {/* The benchmark header and the compatibility band, without the feature + grid that used to sit between them. */} + +
+