From 1bc2cfa5f8bce59daf3d50cc0c5ec5887c2c2ae9 Mon Sep 17 00:00:00 2001 From: v0 Date: Wed, 9 Sep 2026 19:07:24 +0000 Subject: [PATCH] Build Coven editorial homepage and searchable component documentation --- apps/specimens/index.html | 7 +- apps/specimens/src/app.tsx | 1128 ++-------- apps/specimens/src/catalog.ts | 115 + apps/specimens/src/code-block.tsx | 52 + apps/specimens/src/docs.tsx | 580 +++++ apps/specimens/src/examples.tsx | 518 +++++ apps/specimens/src/home.tsx | 292 +++ apps/specimens/src/lab.tsx | 186 ++ apps/specimens/src/main.tsx | 14 - apps/specimens/src/specimens-fixes.css | 323 +-- apps/specimens/src/specimens.css | 1946 +++++++++++------ apps/specimens/vite.config.ts | 1 + .../ui/src/components/ui/dropdown-menu.tsx | 5 +- packages/ui/tests/components.test.tsx | 21 + public/r/dropdown-menu.json | 2 +- scripts/verify-contracts.mjs | 223 +- scripts/verify-site.mjs | 157 ++ vercel.json | 13 + 18 files changed, 3537 insertions(+), 2046 deletions(-) create mode 100644 apps/specimens/src/catalog.ts create mode 100644 apps/specimens/src/code-block.tsx create mode 100644 apps/specimens/src/docs.tsx create mode 100644 apps/specimens/src/examples.tsx create mode 100644 apps/specimens/src/home.tsx create mode 100644 apps/specimens/src/lab.tsx create mode 100644 scripts/verify-site.mjs diff --git a/apps/specimens/index.html b/apps/specimens/index.html index 917c8d3..dd5245f 100644 --- a/apps/specimens/index.html +++ b/apps/specimens/index.html @@ -1,13 +1,14 @@ - + - OpenCoven UI — component registry + + OpenCoven UI — Built for agents. Made for humans.
diff --git a/apps/specimens/src/app.tsx b/apps/specimens/src/app.tsx index 84da0f2..afe03b6 100644 --- a/apps/specimens/src/app.tsx +++ b/apps/specimens/src/app.tsx @@ -1,944 +1,262 @@ +import { useEffect, useRef, useState } from "react"; import { - ActivityItem, - AttachmentChip, - Badge, - BudgetPill, - Button, - Card, - CardContent, - CardFooter, - CardHeader, - CompletionPalette, - Composer, - ContextMeter, - FailureSurface, - MetricDisplay, - ModeSwitch, - PlanRow, - ResourceRow, - RunRail, - SearchField, - SendControl, - SessionHeader, - Tabs, - TabsContent, - TabsList, - TabsTrigger, - ToolMix, - TooltipProvider, - TranscriptTurn, - type ComposerMode, -} from "@opencoven/ui"; -import { ArrowRight, Moon, Sparkles, Sun } from "lucide-react"; -import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; + Asterisk, + ArrowUpRight, + GitFork, + Menu, + Moon, + Search, + Sun, + X, +} from "lucide-react"; +import { Button, TooltipProvider } from "@opencoven/ui"; +import { + catalog, + componentHref, + guides, + repository, + type Density, +} from "./catalog"; +import { Home } from "./home"; +import { Docs } from "./docs"; +import { Lab } from "./lab"; -type Density = "default" | "compact"; type Scheme = "light" | "dark"; -type SpecimenGroup = "Composer" | "Run rail" | "Blocks"; - -type Specimen = { - id: string; - title: string; - group: SpecimenGroup; - primitive: string; - description: string; - states: string; - preview: ReactNode; -}; - -const groupOrder: SpecimenGroup[] = ["Composer", "Run rail", "Blocks"]; - -const groupDetails: Record< - SpecimenGroup, - { id: string; eyebrow: string; description: string } -> = { - Composer: { - id: "group-composer", - eyebrow: "Input layer", - description: "Intent, authority, attachments, and send readiness.", - }, - "Run rail": { - id: "group-run-rail", - eyebrow: "Evidence layer", - description: "Execution evidence, limits, resources, and failure states.", - }, - Blocks: { - id: "group-blocks", - eyebrow: "Complete surfaces", - description: "Public components assembled into reusable agent workflows.", - }, -}; - -function SpecimenCard({ - specimen, - index, -}: { - specimen: Specimen; - index: number; -}) { - const sourceKind = specimen.group === "Blocks" ? "blocks" : "components"; - const headingId = `${specimen.id}-title`; - const exportName = specimen.id - .split("-") - .map((part) => part[0]?.toUpperCase() + part.slice(1)) - .join(""); - const registryUrl = `https://ui.opencoven.ai/r/${specimen.id}.json`; - const packagePath = `@opencoven/ui/${sourceKind}/${specimen.id}`; - - return ( -
-
-
- - {String(index + 1).padStart(2, "0")} - - {specimen.primitive} -
-

- {specimen.title} -

-

{specimen.description}

-
- - - Preview - Install - Usage - - -
{specimen.preview}
-
- -
-
-
- CLI - shadcn registry -
-
-                
-                  pnpm{" "}
-                  dlx{" "}
-                  shadcn@latest{" "}
-                  add{" "}
-                  {registryUrl}
-                
-              
-
-
-
- TypeScript - package API -
-
-                
-                  import{" "}
-                  {"{ "}
-                  {exportName}
-                  {" }"}{" "}
-                  from{" "}
-                  
-                    "{packagePath}"
-                  
-                  ;
-                
-              
-
-
-

- States - {specimen.states} -

-
- -

- Uses semantic tokens, visible focus, non-color state cues, logical - properties, and reduced-motion-safe feedback. Compact density is an - explicit prop, never a global compression shortcut. -

-
-
-
- ); -} - -function Library({ density, query }: { density: Density; query: string }) { - const [mode, setMode] = useState("do"); - const [message, setMessage] = useState("Review the changed files"); - - const specimens = useMemo( - () => [ - { - id: "mode-switch", - title: "Mode switch", - group: "Composer", - primitive: "native", - description: - "A pressed-state control for Chat, Do, and Plan authority.", - states: "default, selected, focus, disabled", - preview: ( - - ), - }, - { - id: "send-control", - title: "Send control", - group: "Composer", - primitive: "Base UI", - description: - "The surface's one filled action, with a stable stop state.", - states: "ready, running, disabled", - preview: ( -
- - -
- ), - }, - { - id: "completion-palette", - title: "Completion palette", - group: "Composer", - primitive: "Base UI Menu", - description: - "Keyboard-ready slash commands in a collision-aware overlay.", - states: "closed, open, focused, disabled", - preview: ( - Open slash commands} - onSelect={() => undefined} - commands={[ - { - id: "plan", - label: "/plan", - description: "Draft a plan before acting", - shortcut: "↵", - }, - { - id: "handoff", - label: "/handoff", - description: "Write a continuation handoff", - }, - { - id: "research", - label: "/research", - description: "Start a bounded research mission", - }, - ]} - /> - ), - }, - { - id: "attachment-chip", - title: "Attachment chip", - group: "Composer", - primitive: "native", - description: "A file attachment with explicit operational state.", - states: "ready, uploading, failed", - preview: ( -
- - - -
- ), - }, - { - id: "metric-display", - title: "Metric display", - group: "Run rail", - primitive: "native", - description: - "Comparable run figures in stable tabular numeric typography.", - states: "neutral, success, warning, information", - preview: ( - - - - - - ), - }, - { - id: "plan-row", - title: "Plan row", - group: "Run rail", - primitive: "native", - description: - "A task step whose icon, text treatment, and label carry state.", - states: "pending, active, complete, blocked", - preview: ( - - - - - - ), - }, - { - id: "activity-item", - title: "Activity item", - group: "Run rail", - primitive: "native", - description: - "One append-only tool event using the canonical class mapping.", - states: "read, write, exec, net, running", - preview: ( -
- - - - -
- ), - }, - { - id: "resource-row", - title: "Resource row", - group: "Run rail", - primitive: "native", - description: - "A direction-aware path row with operation and diff evidence.", - states: "modified, added, deleted, renamed", - preview: ( - - - - - ), - }, - { - id: "tool-mix", - title: "Tool mix", - group: "Run rail", - primitive: "native", - description: - "A readable summary with one fixed read/exec/write/net order.", - states: "populated, empty", - preview: ( - - ), - }, - { - id: "failure-surface", - title: "Failure surface", - group: "Run rail", - primitive: "Base UI Button", - description: - "A durable failure receipt with quiet output and explicit next moves.", - states: "failed, retried", - preview: ( - undefined }]} - /> - ), - }, - { - id: "context-meter", - title: "Context meter", - group: "Run rail", - primitive: "native progress", - description: - "Window consumption with a fixed threshold and stable numbers.", - states: "normal, warning, full", - preview: ( -
- - -
- ), - }, - { - id: "budget-pill", - title: "Budget pill", - group: "Run rail", - primitive: "native", - description: - "Spend against a limit with icon, text weight, and semantic tone.", - states: "normal, warning, over", - preview: ( -
- - - -
- ), - }, - { - id: "composer", - title: "Composer block", - group: "Blocks", - primitive: "composition", - description: - "The full intent-taking surface, built only from public modules.", - states: "empty, ready, running, disabled", - preview: ( - - ), - }, - { - id: "run-rail", - title: "Run rail block", - group: "Blocks", - primitive: "composition", - description: - "Metrics, activity, context, and budget as one operational report.", - states: "populated, loading, empty, error", - preview: ( - - ), - }, - { - id: "transcript-turn", - title: "Transcript turn", - group: "Blocks", - primitive: "composition", - description: - "Familiar identity and provenance lead an editorial response.", - states: "streaming, complete, with artifacts", - preview: ( - - Reply - Copy - 1.8K tokens - - } - > -

- The component source, registry item, and specimen now share one - implementation boundary. -

-
- ), - }, - { - id: "session-header", - title: "Session header", - group: "Blocks", - primitive: "composition", - description: - "The session task, branch, execution state, and spend in one line.", - states: "pending, active, complete, blocked", - preview: ( - - ), - }, - ], - [density, message, mode], - ); - - const normalizedQuery = query.trim().toLowerCase(); - const filtered = specimens.filter((specimen) => - `${specimen.title} ${specimen.group} ${specimen.description}` - .toLowerCase() - .includes(normalizedQuery), - ); - - if (filtered.length === 0) { - return ( -
- -

No matching specimens

-

Try a component name, block, state, or operational concept.

-
- ); +function preference(key: string, fallback: string) { + try { + return localStorage.getItem(key) ?? fallback; + } catch { + return fallback; } - - let specimenIndex = 0; - - return ( -
- {groupOrder.map((group) => { - const groupedSpecimens = filtered.filter( - (specimen) => specimen.group === group, - ); - - if (groupedSpecimens.length === 0) { - return null; - } - - const detail = groupDetails[group]; - - return ( -
-
-
-

- {detail.eyebrow} -

-

{group}

-

{detail.description}

-
- - {groupedSpecimens.length}{" "} - {groupedSpecimens.length === 1 ? "specimen" : "specimens"} - -
-
- {groupedSpecimens.map((specimen) => { - const currentIndex = specimenIndex; - specimenIndex += 1; - - return ( - - ); - })} -
-
- ); - })} -
- ); -} - -function Lab({ density }: { density: Density }) { - const [view, setView] = useState("composer"); - const [mode, setMode] = useState("do"); - const [message, setMessage] = useState( - "Ask Cody to review the changed files", - ); - - const views: Record = { - composer: ( -
- -

- I found two visual regressions in the specimen shell and kept the - package boundary intact. -

-
- -
- ), - messages: ( -
- - Reply - Copy - 1.2K tokens - - } - > -

- Model selection, linked context, and send readiness remain visible - without interrupting the writing flow. -

-
- -

- The same primitives can carry a different familiar identity without - changing their authority or accessibility contract. -

-
-
- ), - context: ( - - - - - - ), - actions: ( - - - - - ), - cards: ( -
- {[ - ["Pull request", "Recover attachment ingestion", "Checks 12 / 12"], - ["Proposal", "Merge #4764 · squash", "Awaiting your tap"], - ["Attachment", "Components-preview.png", "384 KB · added by Cody"], - ["Handoff", "Vercel deployment ledger", "7 sections"], - ].map(([kind, title, meta]) => ( - - - - {kind} - - - - {title} -

{meta}

-
- - Open in reader - -
- ))} -
- ), - }; - - return ( -
- - setView(String(next))}> -
- - {Object.keys(views).map((name) => ( - - {name} - - ))} - -
- {Object.entries(views).map(([name, content]) => ( - -
{content}
-
- ))} -
-
- ); -} - -function DensityControl({ - density, - onDensityChange, -}: { - density: Density; - onDensityChange: (density: Density) => void; -}) { - return ( -
- - -
- ); } function App() { + const path = window.location.pathname.replace(/\/+$/, "") || "/"; const [scheme, setScheme] = useState(() => - localStorage.getItem("coven-ui:scheme") === "light" ? "light" : "dark", + preference("coven-ui:scheme", "dark") === "light" ? "light" : "dark", ); const [density, setDensity] = useState(() => - localStorage.getItem("coven-ui:density") === "compact" + preference("coven-ui:density", "default") === "compact" ? "compact" : "default", ); const [query, setQuery] = useState(""); + const [searchOpen, setSearchOpen] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); const searchRef = useRef(null); - const topbarRef = useRef(null); - const normalizedPath = window.location.pathname.replace(/\/+$/, "") || "/"; - const isLab = normalizedPath === "/lab"; + const searchRegion = useRef(null); + const isHome = path === "/"; + const isLab = path === "/lab"; + const searchItems = [ + ...guides, + ...catalog.map((entry) => ({ + title: entry.title, + href: componentHref(entry.name), + })), + ].filter((entry) => + entry.title.toLowerCase().includes(query.trim().toLowerCase()), + ); useEffect(() => { document.documentElement.classList.toggle("dark", scheme === "dark"); - document.documentElement.dataset.density = density; - localStorage.setItem("coven-ui:scheme", scheme); - localStorage.setItem("coven-ui:density", density); + document.documentElement.dataset.density = "default"; + try { + localStorage.setItem("coven-ui:scheme", scheme); + localStorage.setItem("coven-ui:density", density); + } catch { + /* Preferences remain session-local when browser storage is blocked. */ + } }, [density, scheme]); - useEffect(() => { - const onKeyDown = (event: KeyboardEvent) => { + const title = + catalog.find((entry) => componentHref(entry.name) === path)?.title ?? + guides.find((guide) => guide.href === path)?.title ?? + (isHome + ? "Built for agents. Made for humans." + : isLab + ? "Component lab" + : "Components"); + document.title = `${title} — OpenCoven UI`; + const onKey = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); searchRef.current?.focus(); + setSearchOpen(true); + } + if (event.key === "Escape") { + setSearchOpen(false); + setMenuOpen(false); } }; - - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, []); - - useEffect(() => { - const topbar = topbarRef.current; - if (!topbar) return; - - const updateTopbarHeight = () => { - document.documentElement.style.setProperty( - "--specimen-topbar-height", - `${Math.ceil(topbar.getBoundingClientRect().height)}px`, - ); + const onPointer = (event: PointerEvent) => { + if (!searchRegion.current?.contains(event.target as Node)) + setSearchOpen(false); }; - const observer = new ResizeObserver(updateTopbarHeight); - - updateTopbarHeight(); - observer.observe(topbar); - + window.addEventListener("keydown", onKey); + window.addEventListener("pointerdown", onPointer); return () => { - observer.disconnect(); - document.documentElement.style.removeProperty("--specimen-topbar-height"); + window.removeEventListener("keydown", onKey); + window.removeEventListener("pointerdown", onPointer); }; - }, []); + }, [path, isHome, isLab]); return ( - - Skip to specimens - -
-
- - - - OpenCoven UI - Reference lab - - - -
- {!isLab ? ( - setQuery(event.target.value)} - placeholder="Search components…" - shortcut="⌘K" - className="specimen-search" - /> - ) : null} - - -
-
-
-
- -
-
-
-
-

- {isLab - ? "Five working views" - : "Sixteen public building blocks"} -

-

- {isLab - ? "One agent surface, five focused views." - : "Agent UI, organized by the work it does."} -

-

- {isLab - ? "Move through composition, messages, context, actions, and cards without leaving the shared public component system." - : "Compose intent, read execution evidence, and assemble complete workflows. Every specimen maps to a public import and installable registry item."} -

+ + Components + + + Lab + + +
+
{ + if (!event.currentTarget.contains(event.relatedTarget)) + setSearchOpen(false); + }} + > +
-
-
-
{isLab ? "Views" : "Specimens"}
-
{isLab ? "05" : "16"}
-
-
-
Schemes
-
02
-
-
-
Densities
-
02
-
-
-
- {isLab ? ( - - ) : ( - - )} + + + + +
- + + {isHome ? ( + + ) : isLab ? ( + + ) : ( + + )} +
); diff --git a/apps/specimens/src/catalog.ts b/apps/specimens/src/catalog.ts new file mode 100644 index 0000000..1ffdf82 --- /dev/null +++ b/apps/specimens/src/catalog.ts @@ -0,0 +1,115 @@ +import type { ComponentType } from "react"; +import registryText from "../../../registry.json?raw"; +import exampleSource from "./examples.tsx?raw"; +import * as examples from "./examples"; + +export type Density = "default" | "compact"; +export type Group = + "Foundations" | "Composer controls" | "Run & evidence" | "Blocks"; +export const groups: Group[] = [ + "Foundations", + "Composer controls", + "Run & evidence", + "Blocks", +]; +export const repository = "https://github.com/OpenCoven/ui"; +export const registryOrigin = "https://ui.opencoven.ai/r"; +type RegistryItem = { + name: string; + title: string; + type: string; + description: string; + files: { path: string; target?: string }[]; + meta?: { examples?: string[]; primitive?: string; density?: string[] }; +}; +const registry = JSON.parse(registryText) as { items: RegistryItem[] }; +const sources = import.meta.glob("../../../packages/ui/src/**/*.tsx", { + query: "?raw", + import: "default", + eager: true, +}) as Record; +const inputs = [ + "mode-switch", + "send-control", + "completion-palette", + "attachment-chip", + "search-field", +]; + +export const catalog = registry.items + .filter((item) => + ["registry:ui", "registry:component", "registry:block"].includes(item.type), + ) + .map((item) => { + const symbol = item.name + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(""); + const exampleName = `${symbol}Example` as keyof typeof examples; + const file = item.files[0]; + if (!file || !examples[exampleName]) + throw new Error(`Missing documentation example: ${item.name}`); + const sourcePath = file.path; + const group: Group = + item.type === "registry:block" + ? "Blocks" + : sourcePath.includes("/components/ui/") + ? "Foundations" + : inputs.includes(item.name) + ? "Composer controls" + : "Run & evidence"; + const source = sources[`../../../${sourcePath}`] ?? ""; + const api = source.match(/type \w+Props = [\s\S]*?\n};/)?.[0]; + return { + ...item, + group, + symbol, + exampleName, + sourcePath, + api, + source, + Component: examples[exampleName] as ComponentType<{ density?: Density }>, + packagePath: sourcePath + .replace("packages/ui/src/", "@opencoven/ui/") + .replace(/\.tsx$/, ""), + consumerPath: (file.target ?? "") + .replace("@ui/", "@/components/ui/") + .replace("@components/", "@/components/") + .replace(/\.tsx$/, ""), + }; + }) + .sort( + (a, b) => + groups.indexOf(a.group) - groups.indexOf(b.group) || + a.title.localeCompare(b.title), + ); + +export type CatalogEntry = (typeof catalog)[number]; +export const guides = [ + { href: "/docs", title: "Introduction" }, + { href: "/docs/installation", title: "Installation" }, + { href: "/docs/theming", title: "Theming" }, +]; +export function componentHref(id: string) { + return `/docs/components/${id}`; +} +export function installCommand(id: string) { + return `pnpm dlx shadcn@latest add ${registryOrigin}/${id}.json`; +} +export function exampleCode(entry: CatalogEntry) { + const body = + exampleSource + .split(`export function ${entry.exampleName}(`)[1] + ?.split("\nexport function ")[0] + ?.trim() ?? ""; + const code = `export function ${entry.exampleName}(${body}`; + const imports = + [ + ...exampleSource.matchAll(/import \{([^}]+)\} from "@opencoven\/ui";/g), + ][0]?.[1] ?? ""; + const uiImports = imports + .split(",") + .map((name) => name.trim()) + .filter((name) => name && new RegExp(`\\b${name}\\b`).test(code)); + return `${code.includes("useState") ? 'import { useState } from "react";\n' : ""}import { ${uiImports.join(", ")} } from "@opencoven/ui";\n\n${code}`; +} diff --git a/apps/specimens/src/code-block.tsx b/apps/specimens/src/code-block.tsx new file mode 100644 index 0000000..3ffd024 --- /dev/null +++ b/apps/specimens/src/code-block.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import { Button } from "@opencoven/ui"; +import { Check, Copy, Terminal } from "lucide-react"; + +export function CodeBlock({ + code, + label = "Terminal", + compact = false, +}: { + code: string; + label?: string; + compact?: boolean; +}) { + const [status, setStatus] = useState(""); + async function copy() { + try { + await navigator.clipboard.writeText(code); + setStatus("Copied"); + } catch { + setStatus("Copy unavailable. Select the code to copy it."); + } + } + return ( +
+
+ + + +
+
+        {code}
+      
+ + {status} + +
+ ); +} diff --git a/apps/specimens/src/docs.tsx b/apps/specimens/src/docs.tsx new file mode 100644 index 0000000..3a90222 --- /dev/null +++ b/apps/specimens/src/docs.tsx @@ -0,0 +1,580 @@ +import { useState } from "react"; +import { + ArrowLeft, + ArrowRight, + ArrowUpRight, + BookOpen, + Code2, + PanelLeft, + SlidersHorizontal, +} from "lucide-react"; +import { + Badge, + Button, + EmptyState, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + buttonVariants, +} from "@opencoven/ui"; +import { + catalog, + componentHref, + exampleCode, + groups, + guides, + installCommand, + repository, + type CatalogEntry, + type Density, +} from "./catalog"; +import { CodeBlock } from "./code-block"; + +export function Docs({ + path, + query, + density, + onDensityChange, +}: { + path: string; + query: string; + density: Density; + onDensityChange: (density: Density) => void; +}) { + const [menuOpen, setMenuOpen] = useState(false); + const results = catalog.filter((entry) => + `${entry.title} ${entry.description} ${entry.group} ${entry.meta?.examples?.join(" ") ?? ""}` + .toLowerCase() + .includes(query.trim().toLowerCase()), + ); + const entry = catalog.find((item) => componentHref(item.name) === path); + const isCatalog = path === "/docs/components"; + const isGuide = guides.some((guide) => guide.href === path); + return ( +
+
+ +
+ +
+

+ Docs + / + {entry ? ( + <> + Components + / + {entry.title} + + ) : isCatalog ? ( + "Components" + ) : ( + (guides.find((guide) => guide.href === path)?.title ?? "Not found") + )} +

+ {entry ? ( + + ) : isCatalog ? ( + <> +
+ The collection +

Build with good pieces.

+

+ {catalog.length} focused components. One familiar system. Find + what your interface needs and make it your own. +

+
+ {results.length ? ( + groups.map((group) => { + const items = results.filter((item) => item.group === group); + return items.length ? ( +
+

+ {group} + {items.length} +

+
+ {items.map((item) => ( + +
+

{item.title}

+
+

{item.description}

+
+ ))} +
+
+ ) : null; + }) + ) : ( + + )} + + ) : isGuide ? ( + + ) : ( + + Browse components + + } + /> + )} +
+ +
+ ); +} + +function ComponentDoc({ + entry, + density, + onDensityChange, +}: { + entry: CatalogEntry; + density: Density; + onDensityChange: (value: Density) => void; +}) { + const index = catalog.indexOf(entry); + const previous = catalog[index - 1]; + const next = catalog[index + 1]; + const code = exampleCode(entry); + return ( + <> +
+ {entry.group} +

{entry.title}

+

{entry.description}

+ + View source + +
+
+ +
+ + Preview + Code + + {entry.source.includes('density?: "default" | "compact"') && ( + + )} +
+ +
+ +
+
+ Live component preview React · TypeScript +
+
+ + + +
+
+
+

Installation

+

+ Add the component source to your project with the shadcn CLI. Its + registry dependencies are included automatically. +

+ +

+ Using the package instead? Follow the{" "} + package setup and use{" "} + {entry.packagePath}. +

+
+
+

Usage

+

+ For a registry install using the default aliases, import from your + local source: +

+ +

+ The Code tab contains the exact interactive example above, using + package imports. Adapt those imports to your local paths when using + registry source. +

+ {entry.meta?.examples?.length ? ( +
+ Supported states + {entry.meta.examples.map((state) => ( + + {state} + + ))} +
+ ) : null} +
+
+

API reference

+

+ {entry.api + ? "The current public prop contract, taken directly from the component source." + : "This primitive forwards its underlying element or Base UI props. See the source for its composition and supported variants."} +

+ {entry.api ? ( + + ) : ( + + Explore the full API + + )} +

+ Semantic tokens carry the visual treatment. Preserve accessible + labels, explicit state cues, and visible focus when customizing your + copy. +

+
+ + + ); +} + +function Guide({ path }: { path: string }) { + if (path === "/docs/installation") + return ( +
+
+ Getting started +

A few lines. All yours.

+

+ Install source through the registry, or consume the package. Same + components. Your choice. +

+
+
+

Start with your foundation

+

+ Coven UI targets React 19.2 and Tailwind CSS 4. Interactive + primitives use Base UI. Start in a React project with shadcn + configured for Base UI and working component aliases. +

+ +

Add the Coven theme

+

+ The theme provides the semantic colors, typography, and density + variables used throughout the library. Review the generated + stylesheet, import it in your app entry, and keep only one Tailwind + entry import. +

+ + +

Add your first component

+ +

+ Registry items are copied into your project. You own them. For the + default aliases, blocks go into components/blocks, + Coven components into components, and primitives into{" "} + components/ui. Your components.json aliases determine + the exact locations. +

+
+
+

Working with the package

+

+ The repository also exposes @opencoven/ui through its + workspace package. To use that route, clone the GitHub repository + and build the workspace. This does not assume an npm release is + available. +

+ +

+ Within the workspace, add @opencoven/ui: workspace:* to + your app dependencies, then import the stylesheet and components: +

+ +

+ For projects outside this workspace, the shadcn CLI is the simplest + source installation path. +

+
+
+ ); + if (path === "/docs/theming") + return ( +
+
+ Make it yours +

+ Familiar foundations. +
+ Your own expression. +

+

+ A small semantic vocabulary makes the whole interface feel like one + considered system. +

+
+
+

Theme with meaning

+

+ Use background and foreground for the canvas, card for surfaces, and + presence for familiar identity and intentional accents. Define + tokens in your global stylesheet rather than overriding individual + components. +

+
+ {["background", "card", "foreground", "presence"].map((token) => ( +
+ + --{token} +
+ ))} +
+ +

Light and dark

+

+ Add or remove dark on the root element. Components + inherit their semantic colors without per-component dark-mode + classes. +

+ +

Density is explicit

+

+ Pass density="compact" to components that support it. + Density changes spacing, not information hierarchy. The docs preview + control lets you compare supported densities. +

+ '} + /> +

State is not decoration

+

+ Keep canonical tool classes stable: read, exec, write, and net have + distinct meanings. Statuses pair color with text and icons. Preserve + these cues, keyboard focus, and reduced-motion support when changing + your theme. +

+
+
+ ); + return ( +
+
+ Welcome to the coven +

+ Good interfaces +
+ start with good pieces. +

+

+ A collection of accessible, composable components for agent + interfaces. Built with Base UI. Distributed the shadcn way. +

+
+
+ +

+ This is your source code, not a black box. Take the components you + need, change what you want, and build something that feels like you. +

+
+
+

Less scaffolding. More making.

+

+ Coven UI brings intent, execution, and evidence into the same visual + language. Start with familiar primitives, add focused controls, then + compose complete agent surfaces. +

+ +

Four layers, one system

+ {groups.map((group) => ( +
+

{group}

+

+ { + { + Foundations: + "Buttons, inputs, menus, and the accessible primitives every product needs.", + "Composer controls": + "Express intent, choose authority, attach context, and send with confidence.", + "Run & evidence": + "Make progress, resource changes, context, and operational limits visible.", + Blocks: + "Complete composers, transcripts, session headers, and run rails.", + }[group] + } +

+
+ ))} +

Built to be understood

+

+ React 19.2, TypeScript, Tailwind CSS 4, and Base UI. Explicit props, + readable source, keyboard-ready interactions, and light and dark + themes. No model provider or backend is bundled into the components. +

+
+
+ ); +} diff --git a/apps/specimens/src/examples.tsx b/apps/specimens/src/examples.tsx new file mode 100644 index 0000000..f0af335 --- /dev/null +++ b/apps/specimens/src/examples.tsx @@ -0,0 +1,518 @@ +import { useState } from "react"; +import { + ActivityItem, + AttachmentChip, + Badge, + BudgetPill, + Button, + Card, + CardContent, + CardFooter, + CardHeader, + CompletionPalette, + Composer, + ContextMeter, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + ErrorState, + FailureSurface, + Input, + MetricDisplay, + ModeSwitch, + PlanRow, + Progress, + ResourceRow, + RunRail, + SearchField, + SendControl, + Separator, + SessionHeader, + StatusIndicator, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Textarea, + ToolClassBadge, + ToolMix, + Tooltip, + TooltipContent, + TooltipTrigger, + TranscriptTurn, +} from "@opencoven/ui"; + +export function ButtonExample() { + const [saved, setSaved] = useState(false); + return ( +
+ + + +
+ ); +} + +export function BadgeExample() { + return ( +
+ Default + In progress + Draft +
+ ); +} + +export function CardExample() { + const [saved, setSaved] = useState(false); + return ( + + + A space for your next idea. +

+ Composable surfaces. Nothing more than you need. +

+
+ + Your next project starts with a small building block. + + + + +
+ ); +} + +export function DropdownMenuExample() { + const [selected, setSelected] = useState("Choose an action"); + return ( +
+ + Open menu} /> + +
+ setSelected("Duplicate selected")}> + Duplicate + + setSelected("Archive selected")}> + Archive + +
+
+
+

{selected}

+
+ ); +} + +export function InputExample({ + density = "default", +}: { + density?: "default" | "compact"; +}) { + return ( +
+ + +
+ ); +} + +export function ProgressExample() { + const [value, setValue] = useState(40); + return ( +
+ + +
+ ); +} + +export function SeparatorExample() { + return ( +
+

Intent

+ +

Execution

+ +

Evidence

+
+ ); +} + +export function TabsExample() { + return ( + + + Overview + Activity + + + A little structure makes everything easier to find. + + + All caught up. Your next action will appear here. + + + ); +} + +export function TextareaExample({ + density = "default", +}: { + density?: "default" | "compact"; +}) { + return ( +
+ +