diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff838e8..a512d6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: version: 10.17.1 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24 cache: pnpm diff --git a/apps/specimens/src/app.tsx b/apps/specimens/src/app.tsx index 1b6d525..cc67f22 100644 --- a/apps/specimens/src/app.tsx +++ b/apps/specimens/src/app.tsx @@ -34,31 +34,63 @@ import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; type Density = "default" | "compact"; type Scheme = "light" | "dark"; +type SpecimenGroup = "Composer" | "Run rail" | "Blocks"; type Specimen = { id: string; title: string; - group: "Composer" | "Run rail" | "Blocks"; + group: SpecimenGroup; primitive: string; description: string; states: string; preview: ReactNode; }; -function SpecimenCard({ specimen }: { specimen: Specimen }) { +const groupOrder: SpecimenGroup[] = ["Composer", "Run rail", "Blocks"]; + +const groupDetails: Record = + { + Composer: { + id: "group-composer", + description: "Intent, authority, attachments, and send readiness.", + }, + "Run rail": { + id: "group-run-rail", + description: "Execution evidence, limits, resources, and failure states.", + }, + Blocks: { + id: "group-blocks", + description: "Complete surfaces assembled from the public component API.", + }, + }; + +function SpecimenCard({ + specimen, + index, +}: { + specimen: Specimen; + index: number; +}) { + const sourceKind = specimen.group === "Blocks" ? "blocks" : "components"; + const headingId = `${specimen.id}-title`; + return ( -
-
-
- - {specimen.group} +
+
+
+ + {String(index + 1).padStart(2, "0")} {specimen.primitive}
-

{specimen.title}

-

- {specimen.description} -

+

+ {specimen.title} +

+

{specimen.description}

@@ -69,24 +101,21 @@ function SpecimenCard({ specimen }: { specimen: Specimen }) {
{specimen.preview}
- -

- pnpm dlx shadcn@latest add https://ui.opencoven.ai/r/{specimen.id} - .json -

-

+ + + {`pnpm dlx shadcn@latest add https://ui.opencoven.ai/r/${specimen.id}.json`} + +

Import from{" "} - @opencoven/ui/components/{specimen.id} + @opencoven/ui/{sourceKind}/{specimen.id} .

-

- States: {specimen.states}. -

+

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. @@ -422,22 +451,69 @@ function Library({ density, query }: { density: Density; query: string }) { [density, message, mode], ); + const normalizedQuery = query.trim().toLowerCase(); const filtered = specimens.filter((specimen) => `${specimen.title} ${specimen.group} ${specimen.description}` .toLowerCase() - .includes(query.toLowerCase()), + .includes(normalizedQuery), ); + if (filtered.length === 0) { + return ( +

+ +

No matching specimens

+

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

+
+ ); + } + + let specimenIndex = 0; + return ( -
- {filtered.map((specimen) => ( - - ))} - {filtered.length === 0 ? ( -

- No components match “{query}”. -

- ) : null} +
+ {groupOrder.map((group) => { + const groupedSpecimens = filtered.filter( + (specimen) => specimen.group === group, + ); + + if (groupedSpecimens.length === 0) { + return null; + } + + const detail = groupDetails[group]; + + return ( +
+
+
+

{group}

+

{detail.description}

+
+ + {groupedSpecimens.length}{" "} + {groupedSpecimens.length === 1 ? "specimen" : "specimens"} + +
+
+ {groupedSpecimens.map((specimen) => { + const currentIndex = specimenIndex; + specimenIndex += 1; + + return ( + + ); + })} +
+
+ ); + })}
); } @@ -451,27 +527,64 @@ function Lab({ density }: { density: Density }) { const views: Record = { composer: ( - +
+ +

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

+
+ +
), messages: ( - -

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

-
+
+ + 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: ( @@ -480,6 +593,10 @@ function Lab({ density }: { density: Density }) { meta="main · src/components/chat-view.tsx · read + write" /> + ), actions: ( @@ -503,7 +620,7 @@ function Lab({ density }: { density: Density }) { ), cards: ( -
+
{[ ["Pull request", "Recover attachment ingestion", "Checks 12 / 12"], ["Proposal", "Merge #4764 · squash", "Awaiting your tap"], @@ -530,20 +647,26 @@ function Lab({ density }: { density: Density }) { }; return ( -
+
+ setView(String(next))}> - - {Object.keys(views).map((name) => ( - - {name} - - ))} - +
+ + {Object.keys(views).map((name) => ( + + {name} + + ))} + +
{Object.entries(views).map(([name, content]) => ( -
- {content} -
+
{content}
))}
@@ -551,6 +674,33 @@ function Lab({ density }: { density: Density }) { ); } +function DensityControl({ + density, + onDensityChange, +}: { + density: Density; + onDensityChange: (density: Density) => void; +}) { + return ( +
+ + +
+ ); +} + function App() { const [scheme, setScheme] = useState(() => localStorage.getItem("coven-ui:scheme") === "light" ? "light" : "dark", @@ -562,7 +712,8 @@ function App() { ); const [query, setQuery] = useState(""); const searchRef = useRef(null); - const isLab = window.location.pathname === "/lab"; + const normalizedPath = window.location.pathname.replace(/\/+$/, "") || "/"; + const isLab = normalizedPath === "/lab"; useEffect(() => { document.documentElement.classList.toggle("dark", scheme === "dark"); @@ -578,90 +729,144 @@ function App() { searchRef.current?.focus(); } }; + window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, []); return ( -
-
\n );\n}\n\nexport { SessionHeader, type SessionHeaderProps };\n", "type": "registry:block", "target": "@components/blocks/session-header.tsx" } diff --git a/public/r/tabs.json b/public/r/tabs.json index 9acc68c..cf9404c 100644 --- a/public/r/tabs.json +++ b/public/r/tabs.json @@ -14,7 +14,7 @@ "files": [ { "path": "packages/ui/src/components/ui/tabs.tsx", - "content": "\"use client\";\n\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Tabs({\n className,\n orientation = \"horizontal\",\n ...props\n}: TabsPrimitive.Root.Props) {\n return (\n \n );\n}\n\nconst tabsListVariants = cva(\n \"group/tabs-list inline-flex w-fit items-center justify-center rounded-md p-[3px] text-muted-foreground group-data-vertical/tabs:flex-col\",\n {\n variants: {\n variant: {\n default: \"bg-muted\",\n line: \"gap-1 rounded-none bg-transparent\",\n },\n density: {\n default: \"min-h-8\",\n compact: \"min-h-7\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n density: \"default\",\n },\n },\n);\n\nfunction TabsList({\n className,\n variant = \"default\",\n density = \"default\",\n ...props\n}: TabsPrimitive.List.Props & VariantProps) {\n return (\n \n );\n}\n\nfunction TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {\n return (\n \n );\n}\n\nfunction TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {\n return (\n \n );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };\n", + "content": "\"use client\";\n\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Tabs({\n className,\n orientation = \"horizontal\",\n ...props\n}: TabsPrimitive.Root.Props) {\n return (\n \n );\n}\n\nconst tabsListVariants = cva(\n \"group/tabs-list inline-flex w-fit items-center justify-center rounded-md p-[3px] text-muted-foreground data-[orientation=vertical]:flex-col\",\n {\n variants: {\n variant: {\n default: \"bg-muted\",\n line: \"gap-1 rounded-none bg-transparent\",\n },\n density: {\n default: \"min-h-8\",\n compact: \"min-h-7\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n density: \"default\",\n },\n },\n);\n\nfunction TabsList({\n className,\n variant = \"default\",\n density = \"default\",\n ...props\n}: TabsPrimitive.List.Props & VariantProps) {\n return (\n \n );\n}\n\nfunction TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {\n return (\n \n );\n}\n\nfunction TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {\n return (\n \n );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };\n", "type": "registry:ui", "target": "@ui/tabs.tsx" } diff --git a/public/r/transcript-turn.json b/public/r/transcript-turn.json index 5ef3ca2..6b8c8a9 100644 --- a/public/r/transcript-turn.json +++ b/public/r/transcript-turn.json @@ -10,7 +10,7 @@ "files": [ { "path": "packages/ui/src/blocks/transcript-turn.tsx", - "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype TranscriptTurnProps = {\n familiar: string;\n initials: string;\n role: string;\n model?: string;\n timestamp: string;\n children: React.ReactNode;\n utilities?: React.ReactNode;\n artifacts?: React.ReactNode;\n className?: string;\n};\n\nfunction TranscriptTurn({\n familiar,\n initials,\n role,\n model,\n timestamp,\n children,\n utilities,\n artifacts,\n className,\n}: TranscriptTurnProps) {\n return (\n \n
\n \n {initials}\n \n \n {familiar}\n \n {[role, model, timestamp].filter(Boolean).join(\" · \")}\n \n \n
\n
\n {children}\n
\n {artifacts}\n {utilities ? (\n \n {utilities}\n \n ) : null}\n
\n );\n}\n\nexport { TranscriptTurn, type TranscriptTurnProps };\n", + "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype TranscriptTurnProps = {\n familiar: string;\n initials: string;\n role: string;\n model?: string;\n timestamp: string;\n children: React.ReactNode;\n utilities?: React.ReactNode;\n artifacts?: React.ReactNode;\n className?: string;\n};\n\nfunction TranscriptTurn({\n familiar,\n initials,\n role,\n model,\n timestamp,\n children,\n utilities,\n artifacts,\n className,\n}: TranscriptTurnProps) {\n return (\n \n
\n \n {initials}\n \n \n {familiar}\n \n {[role, model, timestamp].filter(Boolean).join(\" · \")}\n \n \n
\n
\n {children}\n
\n {artifacts}\n {utilities ? (\n \n {utilities}\n \n ) : null}\n \n );\n}\n\nexport { TranscriptTurn, type TranscriptTurnProps };\n", "type": "registry:block", "target": "@components/blocks/transcript-turn.tsx" } diff --git a/scripts/verify-contracts.mjs b/scripts/verify-contracts.mjs index 4b19463..cd964e5 100644 --- a/scripts/verify-contracts.mjs +++ b/scripts/verify-contracts.mjs @@ -9,6 +9,8 @@ const [ packageJson, tokens, specimenCss, + specimenFixes, + specimenApp, button, tooltip, menu, @@ -17,6 +19,8 @@ const [ read("packages/ui/package.json"), read("packages/ui/src/styles/globals.css"), read("apps/specimens/src/specimens.css"), + read("apps/specimens/src/specimens-fixes.css"), + read("apps/specimens/src/app.tsx"), read("packages/ui/src/components/ui/button.tsx"), read("packages/ui/src/components/ui/tooltip.tsx"), read("packages/ui/src/components/ui/dropdown-menu.tsx"), @@ -81,9 +85,58 @@ const assertions = [ source.includes("Primitive.Positioner"), ), ], + [ + "specimen shell has stable landmarks", + specimenApp.includes('className="specimen-topbar"') && + specimenApp.includes('className="specimen-rail"') && + specimenApp.includes('id="specimen-main"') && + specimenApp.includes('className="skip-link"'), + ], + [ + "catalog restores task hierarchy", + ["group-composer", "group-run-rail", "group-blocks"].every((id) => + specimenApp.includes(id), + ), + ], + [ + "density control is explicit", + specimenApp.includes('aria-label="Display density"') && + !specimenApp.includes("nth-child(2)"), + ], [ "mobile layout covers 390px", - specimenCss.includes("@media (max-width: 24.375rem)"), + specimenFixes.includes("@media (max-width: 24.375rem)") && + specimenFixes.includes( + "grid-template-columns: repeat(5, minmax(0, 1fr))", + ), + ], + [ + "minimum viewport floor does not scale with text", + /html\s*\{[^}]*min-width:\s*320px/.test(specimenFixes), + ], + [ + "responsive rail becomes compact navigation", + specimenCss.includes("@media (max-width: 68rem)") && + specimenCss.includes( + ".specimen-shell {\n grid-template-columns: 1fr;", + ) && + specimenCss.includes("@media (max-width: 48rem)") && + specimenCss.includes( + ".specimen-rail__nav {\n grid-template-columns: repeat(3, minmax(0, 1fr));", + ), + ], + [ + "responsive grids remove intrinsic sizing floors", + specimenFixes.includes( + ".specimen-shell {\n grid-template-columns: minmax(0, 1fr);", + ) && + specimenFixes.includes( + ".catalog-group__grid {\n grid-template-columns: minmax(0, 1fr);", + ), + ], + [ + "specimen chrome avoids decorative gradients", + !specimenCss.includes("gradient("), ], [ "filled action variants are explicit",