From e492fe845c9745e55ea5f84db5c30adf825d4735 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 3 Sep 2026 03:44:12 -0500 Subject: [PATCH 01/11] feat(specimens): give the shell the chat ritual-tech finish Carry the OpenCoven Chat visual language onto the specimen browser so the two products read as cut from one material: near-black surfaces separated by depth rather than borders alone, violet depth under the shell, cyan reserved for circuitry and focus, and a hexagonal field held at texture strength. The shared token set already made this mostly a matter of arrangement -- the dark `--presence` is byte-identical to chat's `--accent-violet`. Cyan is the one hue that had to be introduced, and it resolves per scheme: against near-black it earns its keep, on the light paper surface it goes weak, so light falls back to the semantic presence and ring tokens that already own accent duty there. Nothing here reaches into packages/ui tokens or a public component's own surface; the design doc holds package and registry output fixed. Also adopts the in-flight CLI / React API tab split left behind in the working tree by a session that has since exited, and updates the contract that still described the previous single Install tab. Replace the blanket `gradient(` ban in verify-contracts with the intent it was standing in for. That ban was a blunt proxy added while recovering a broken shell, and the approved layout-parity design and plan both now specify gradient treatments in this exact file, so it had begun failing the repository's own accepted direction. Three precise contracts replace it: decorative layers stay out of public component surfaces, they yield to prefers-contrast and forced-colors, and the chrome never animates perpetually. Each was mutation-tested to confirm it fails when violated. Verification: pnpm check exit 0 (49 architecture contracts, 11 package tests, build, deploy and package export checks); 9/9 visual-review scenarios captured passing across dark, light, compact, mobile, 200% text and both routes; a 3x detail probe confirms the circuitry trace paints real cyan and that zero animations run on load. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 3 + apps/specimens/src/app.tsx | 103 ++++++------ apps/specimens/src/specimens.css | 263 ++++++++++++++++++++++++++++++- scripts/serve-dist.mjs | 44 ++++++ scripts/verify-contracts.mjs | 49 +++++- scripts/visual-review.mjs | 151 ++++++++++++------ scripts/zoom-probe.mjs | 84 ++++++++++ 7 files changed, 581 insertions(+), 116 deletions(-) create mode 100644 scripts/serve-dist.mjs create mode 100644 scripts/zoom-probe.mjs diff --git a/.gitignore b/.gitignore index f695edc..3bc8b71 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ apps/specimens/dist/ packages/ui/dist/ artifacts/mobile-quality/ artifacts/visual-review/ +.superpowers/ +artifacts/visual-baseline/ +artifacts/visual-luxe/ diff --git a/apps/specimens/src/app.tsx b/apps/specimens/src/app.tsx index 84da0f2..0763718 100644 --- a/apps/specimens/src/app.tsx +++ b/apps/specimens/src/app.tsx @@ -106,65 +106,64 @@ function SpecimenCard({ Preview - Install - Usage + CLI + React API
{specimen.preview}
- -
-
-
- CLI - shadcn registry -
-
-                
-                  pnpm{" "}
-                  dlx{" "}
-                  shadcn@latest{" "}
-                  add{" "}
-                  {registryUrl}
-                
-              
-
-
-
- TypeScript - package API -
-
-                
-                  import{" "}
-                  {"{ "}
-                  {exportName}
-                  {" }"}{" "}
-                  from{" "}
-                  
-                    "{packagePath}"
-                  
-                  ;
-                
-              
-
-
-

- States - {specimen.states} + +

+
+ shadcn CLI + registry install +
+
+              
+                pnpm{" "}
+                dlx{" "}
+                shadcn@latest{" "}
+                add{" "}
+                {registryUrl}
+              
+            
+
+

+ Installs the source-owned component and its declared dependencies + through the published shadcn registry.

- + +
+
+ TypeScript + React package API +
+
+              
+                import{" "}
+                {"{ "}
+                {exportName}
+                {" }"}{" "}
+                from{" "}
+                "{packagePath}"
+                ;
+              
+            
+

- 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. + The React package API is the SDK surface for these UI components; + there is no separate OpenCoven UI SDK layer. +

+

+ States + {specimen.states}

diff --git a/apps/specimens/src/specimens.css b/apps/specimens/src/specimens.css index 19353e6..5e745f4 100644 --- a/apps/specimens/src/specimens.css +++ b/apps/specimens/src/specimens.css @@ -484,11 +484,6 @@ body { margin: 0; } -.specimen-install-grid { - display: grid; - gap: 0.75rem; -} - .specimen-code-snippet { display: grid; gap: 0.4rem; @@ -910,3 +905,261 @@ body { border: 1px solid Highlight; } } + +/* + * --------------------------------------------------------------------------- + * Ritual-tech finish + * --------------------------------------------------------------------------- + * + * Carries the OpenCoven Chat visual language (chat/src/styles.css) onto the + * specimen browser: near-black surfaces separated by depth rather than by + * borders alone, violet depth, cyan reserved for circuitry and focus, and + * restrained sacred geometry held at texture strength. + * + * Two constraints from that direction are doing as much work here as the + * palette. No terminal cosplay: no scan lines, no monospace body text. And no + * perpetual animation: atmosphere comes from depth and colour, never from + * something that moves forever behind a component being evaluated. A specimen + * browser has a stricter version of that rule than a chat app, because any + * ambient movement here would be mistaken for the component's own behaviour. + * + * These rules deliberately style the specimen shell only. The design doc holds + * public package and registry output fixed, so nothing here reaches into + * packages/ui tokens or a public component's own surface. + */ + +:root { + /* Cyan is the one hue the shared token set does not already carry. Chat + reserves it for attention and interaction so it keeps meaning something; + the same reservation applies here. */ + --specimen-cyan: #22d3ee; + --specimen-cyan-bright: #67e8f9; + + /* + * Circuitry and focus resolve per scheme rather than being cyan everywhere. + * Cyan earns its keep against near-black, but on the light paper surface it + * goes weak and slightly sickly, so light falls back to the semantic + * presence and ring tokens that already own accent duty there. + */ + --specimen-circuit: var(--presence); + --specimen-focus: var(--ring); + + /* Geometry and depth are expressed as ready-made layers so the light and + dark shells differ only in these two declarations. */ + --specimen-weave: none; + --specimen-depth: radial-gradient( + ellipse 120% 80% at 50% -20%, + color-mix(in srgb, var(--presence) 10%, transparent) 0%, + transparent 60% + ); + --specimen-panel: linear-gradient( + 160deg, + color-mix(in srgb, var(--card) 94%, var(--foreground)) 0%, + var(--card) 100% + ); + --specimen-trace: linear-gradient( + 90deg, + color-mix(in srgb, var(--presence) 55%, transparent) 0%, + color-mix(in srgb, var(--presence) 14%, transparent) 30%, + transparent 70% + ); + --specimen-elevation: 0 18px 48px + color-mix(in srgb, var(--foreground) 10%, transparent); +} + +.dark { + --specimen-circuit: var(--specimen-cyan); + --specimen-focus: var(--specimen-cyan-bright); + + /* + * The hexagonal field: two opposed 60deg rules at an alpha where the eye + * reads texture and not pattern. 46px spacing matches the chat shell so the + * two products feel cut from one material when placed side by side. + */ + --specimen-weave: + repeating-linear-gradient( + 60deg, + rgb(147 134 208 / 4.5%) 0 1px, + transparent 1px 46px + ), + repeating-linear-gradient( + -60deg, + rgb(147 134 208 / 4.5%) 0 1px, + transparent 1px 46px + ); + --specimen-depth: radial-gradient( + ellipse 130% 90% at 50% -15%, + #2c1c3f 0%, + #12101a 45%, + #07060b 100% + ); + --specimen-panel: linear-gradient(160deg, #17151f 0%, #101017 100%); + --specimen-trace: linear-gradient( + 90deg, + var(--specimen-cyan) 0%, + rgb(34 211 238 / 15%) 30%, + transparent 70% + ); + --specimen-elevation: 0 18px 48px rgb(3 2 5 / 45%); +} + +/* + * The atmosphere lives on body so it sits behind the whole shell. The main + * column and specimen shell carry no background of their own, so the depth + * reads continuously beneath the content rather than stopping at a panel edge. + * + * globals.css sets body background inside @layer base; this file is unlayered, + * so it wins without needing !important. + */ +body { + background-color: var(--background); + background-image: var(--specimen-weave), var(--specimen-depth); + background-attachment: fixed; +} + +/* + * Cyan focus, applied once and globally. :focus-visible rather than :focus so + * a mouse click does not leave a ring behind, and offset so it never sits on + * top of an element's own border. + */ +.specimen-shell :focus-visible, +.specimen-topbar :focus-visible { + outline: 2px solid var(--specimen-focus); + outline-offset: 2px; +} + +::selection { + background: color-mix(in srgb, var(--presence) 32%, transparent); + color: var(--foreground); +} + +/* The chrome turns to glass so the depth behind it stays legible. */ +.specimen-topbar { + background: color-mix(in srgb, var(--background) 72%, transparent); + backdrop-filter: blur(20px) saturate(140%); +} + +.specimen-rail { + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--card) 55%, transparent) 0%, + color-mix(in srgb, var(--card) 22%, transparent) 100% + ); + backdrop-filter: blur(12px); +} + +/* + * Specimen panels. The gradient gives the card an internal light direction, the + * top-edge trace gives it a single mark of circuitry, and the shadow does the + * separating that a border alone was doing before. + */ +.specimen-card { + position: relative; + background: var(--specimen-panel); + box-shadow: var(--specimen-elevation); +} + +.specimen-card::before { + content: ""; + position: absolute; + inset: 0 0 auto; + z-index: 1; + height: 1px; + background: var(--specimen-trace); +} + +.specimen-card:hover, +.specimen-card:focus-within { + border-color: color-mix(in srgb, var(--presence) 42%, var(--border)); + box-shadow: + var(--specimen-elevation), + 0 0 0 1px color-mix(in srgb, var(--presence) 18%, transparent); +} + +/* + * The stage is where the component itself lives, so it goes quieter than the + * card around it: a soft well, lit from above, with no texture of its own to + * compete with what is being evaluated. + */ +.specimen-stage { + position: relative; + background: + radial-gradient( + ellipse 90% 70% at 50% 0%, + color-mix(in srgb, var(--presence) 7%, transparent) 0%, + transparent 70% + ), + color-mix(in srgb, var(--muted) 38%, var(--background)); +} + +/* + * A single circuit trace leading into each eyebrow. One mark, not a motif: the + * rule appears on the section eyebrows only, never on ordinary labels. + */ +.specimen-kicker, +.catalog-group__eyebrow { + display: flex; + align-items: center; + gap: 0.55rem; +} + +.specimen-kicker::before, +.catalog-group__eyebrow::before { + content: ""; + flex: none; + width: 1.5rem; + height: 1px; + background: linear-gradient(90deg, transparent, var(--specimen-circuit)); +} + +/* The active surface reads as lit rather than merely filled. */ +.surface-switcher a[aria-current="page"] { + box-shadow: 0 0 0 1px color-mix(in srgb, var(--presence) 30%, transparent); +} + +.specimen-rail__nav a:hover { + background: color-mix(in srgb, var(--presence) 12%, transparent); +} + +/* Install commands sit in a faint cyan well, matching chat's inline code. */ +.specimen-command { + background: color-mix(in srgb, var(--specimen-circuit) 5%, var(--muted)); +} + +/* + * Increased contrast strengthens lines and drops the ambient geometry first: + * texture is the least useful pixel on the screen for someone who asked for + * more contrast. + */ +@media (prefers-contrast: more) { + body { + background-image: none; + } + + .specimen-card, + .specimen-rail, + .specimen-topbar { + background: var(--card); + backdrop-filter: none; + } + + .specimen-card::before { + display: none; + } +} + +/* + * Forced colours discard the decorative layers entirely rather than trying to + * translate them, so the system palette stays authoritative. + */ +@media (forced-colors: active) { + body { + background-image: none; + } + + .specimen-card::before, + .specimen-kicker::before, + .catalog-group__eyebrow::before { + display: none; + } +} diff --git a/scripts/serve-dist.mjs b/scripts/serve-dist.mjs new file mode 100644 index 0000000..b0ac0d8 --- /dev/null +++ b/scripts/serve-dist.mjs @@ -0,0 +1,44 @@ +import { createServer } from "node:http"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +const root = path.resolve(process.argv[2] ?? "apps/specimens/dist"); +const port = Number(process.argv[3] ?? 4321); + +const types = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".woff2": "font/woff2", + ".map": "application/json; charset=utf-8", +}; + +createServer(async (request, response) => { + const requested = decodeURIComponent( + new URL(request.url, "http://x").pathname, + ); + const candidate = path.join(root, requested); + + for (const file of [candidate, path.join(root, "index.html")]) { + if (!path.resolve(file).startsWith(root)) continue; + try { + const body = await readFile(file); + response.writeHead(200, { + "content-type": types[path.extname(file)] ?? "application/octet-stream", + "cache-control": "no-store", + }); + response.end(body); + return; + } catch { + // fall through to SPA index + } + } + + response.writeHead(404, { "content-type": "text/plain" }); + response.end("not found"); +}).listen(port, "127.0.0.1", () => { + console.log(`serving ${root} on http://127.0.0.1:${port}`); +}); diff --git a/scripts/verify-contracts.mjs b/scripts/verify-contracts.mjs index c1fb5a8..64b631e 100644 --- a/scripts/verify-contracts.mjs +++ b/scripts/verify-contracts.mjs @@ -119,13 +119,13 @@ const assertions = [ ], [ "install tab separates CLI from package API", - specimenApp.includes( - 'Install', - ) && + specimenApp.includes('CLI') && + specimenApp.includes( + 'React API', + ) && !specimenApp.includes('API') && - specimenApp.includes("CLI") && specimenApp.includes("TypeScript") && - specimenApp.includes("package API"), + specimenApp.includes("React package API"), ], [ "install snippets derive valid registry and package paths", @@ -228,9 +228,44 @@ const assertions = [ ".specimen-stats {\n grid-template-columns: repeat(3, minmax(0, 1fr));", ), ], + /* + * The original guard here was a blanket ban on `gradient(` in the specimen + * stylesheet. It was a blunt proxy, added while recovering a broken shell, + * for the real fear: chrome turning gaudy, unreadable, or distracting from + * the components on display. + * + * The approved layout-parity direction supersedes that ban. The design doc + * calls for "subtle grid or radial treatment that does not compete with the + * component", and the approved plan writes literal linear-gradient rules + * into this very file for the preview canvas. A blanket ban would now fail + * the repository's own accepted design. + * + * These three contracts replace it with the intent it was standing in for. + */ + [ + "decorative layers stay out of public component surfaces", + !/\[data-slot="[^"]+"\][^{}]*\{[^}]*gradient\(/.test(specimenCss), + ], + [ + "decorative layers yield to contrast and forced-colors preferences", + /@media \(prefers-contrast: more\)\s*\{[^@]*background-image:\s*none;/.test( + specimenCss, + ) && + /@media \(forced-colors: active\)\s*\{[^@]*background-image:\s*none;/.test( + specimenCss, + ), + ], [ - "specimen chrome avoids decorative gradients", - !specimenCss.includes("gradient("), + "specimen chrome never animates perpetually", + // Comments are stripped first so prose describing the rule cannot satisfy + // or violate it. + (() => { + const code = specimenCss.replace(/\/\*[\s\S]*?\*\//g, ""); + return ( + !code.includes("@keyframes") && + !/animation(-name)?:\s*(?!none)/.test(code) + ); + })(), ], [ "filled action variants are explicit", diff --git a/scripts/visual-review.mjs b/scripts/visual-review.mjs index 60aabd0..afd4fee 100644 --- a/scripts/visual-review.mjs +++ b/scripts/visual-review.mjs @@ -213,7 +213,7 @@ const scenarios = [ expected: "library", }, { - name: "library-install-desktop", + name: "library-cli-desktop", pathname: "/", width: 1440, height: 1000, @@ -221,15 +221,39 @@ const scenarios = [ density: "default", mobile: false, expected: "library", - revealInstall: true, - installTargetId: "mode-switch", - expectedCli: + revealCodeTab: "CLI", + codeTargetId: "mode-switch", + expectedCode: "pnpm dlx shadcn@latest add https://ui.opencoven.ai/r/mode-switch.json", - expectedImport: - 'import { ModeSwitch } from "@opencoven/ui/components/mode-switch";', + expectedSyntaxRoles: [ + "syntax-command", + "syntax-keyword", + "syntax-package", + "syntax-string", + ], }, { - name: "library-install-mobile-text-200", + name: "library-react-api-desktop", + pathname: "/", + width: 1440, + height: 1000, + scheme: "light", + density: "compact", + mobile: false, + expected: "library", + revealCodeTab: "React API", + codeTargetId: "session-header", + expectedCode: + 'import { SessionHeader } from "@opencoven/ui/blocks/session-header";', + expectedSyntaxRoles: [ + "syntax-keyword", + "syntax-punctuation", + "syntax-string", + "syntax-symbol", + ], + }, + { + name: "library-react-api-mobile-text-200", pathname: "/", width: 320, height: 900, @@ -238,12 +262,17 @@ const scenarios = [ mobile: true, expected: "library", textScale: 2, - revealInstall: true, - installTargetId: "session-header", - expectedCli: - "pnpm dlx shadcn@latest add https://ui.opencoven.ai/r/session-header.json", - expectedImport: + revealCodeTab: "React API", + codeTargetId: "session-header", + expectedCode: 'import { SessionHeader } from "@opencoven/ui/blocks/session-header";', + expectedSyntaxRoles: [ + "syntax-keyword", + "syntax-punctuation", + "syntax-string", + "syntax-symbol", + ], + expectScrollableCodeTabs: true, }, { name: "library-light-desktop", @@ -393,20 +422,28 @@ try { })()`, true, ); - if (scenario.revealInstall) { + if (scenario.revealCodeTab) { await evaluateValue( client, `(async () => { const card = document.getElementById(${JSON.stringify( - scenario.installTargetId, + scenario.codeTargetId, )}); - const installTab = card + const codeTab = card ? [...card.querySelectorAll('[role="tab"]')].find( - (tab) => tab.textContent?.trim() === "Install", + (tab) => + tab.textContent?.trim() === ${JSON.stringify( + scenario.revealCodeTab, + )}, ) : null; - if (!installTab) throw new Error("Install tab is missing"); - installTab.click(); + if (!codeTab) throw new Error("Requested code tab is missing"); + const tabList = codeTab.closest('[data-slot="tabs-list"]'); + if (${Boolean(scenario.expectScrollableCodeTabs)} && tabList) { + tabList.scrollLeft = tabList.scrollWidth; + await new Promise((resolve) => requestAnimationFrame(resolve)); + } + codeTab.click(); card.scrollIntoView({ block: "center" }); await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)), @@ -433,21 +470,22 @@ try { ".specimen-brand, .surface-switcher, .specimen-search, .density-control, .scheme-control", ), ]; - const installCard = document.getElementById(${JSON.stringify( - scenario.installTargetId ?? "mode-switch", + const codeCard = document.getElementById(${JSON.stringify( + scenario.codeTargetId ?? "mode-switch", )}); - const installGrid = installCard?.querySelector( - ".specimen-install-grid", - ); - const installSnippets = installGrid - ? [...installGrid.querySelectorAll(".specimen-code-snippet")] + const codeSnippets = codeCard + ? [...codeCard.querySelectorAll(".specimen-code-snippet")] : []; - const installCommands = installGrid - ? [...installGrid.querySelectorAll(".specimen-command")] + const codeCommands = codeCard + ? [...codeCard.querySelectorAll(".specimen-command")] : []; - const syntaxTokens = installGrid - ? [...installGrid.querySelectorAll('[class^="syntax-"]')] + const syntaxTokens = codeCard + ? [...codeCard.querySelectorAll('[class^="syntax-"]')] : []; + const codeTabList = codeCard?.querySelector('[data-slot="tabs-list"]'); + const activeCodeTab = codeTabList?.querySelector( + '[role="tab"][aria-selected="true"]', + ); const cards = [...document.querySelectorAll(".specimen-card")]; const groups = [...document.querySelectorAll(".catalog-group")]; const lab = document.querySelector(".assembled-lab"); @@ -507,6 +545,11 @@ try { (candidate) => control.label + " / " + candidate.label, ), ); + const visibleCodeSnippets = codeSnippets.filter(isVisible); + const visibleCodeCommands = codeCommands.filter(isVisible); + const visibleSyntaxTokens = syntaxTokens.filter(isVisible); + const codeTabListBounds = bounds(codeTabList); + const activeCodeTabBounds = bounds(activeCodeTab); return { pathname: location.pathname, @@ -530,27 +573,34 @@ try { tabCount: tabs.length, scheme: root.classList.contains("dark") ? "dark" : "light", density: root.dataset.density, - installVisible: isVisible(installGrid), - installSnippetCount: installSnippets.length, - installCommandTexts: installCommands.map((command) => + codeSnippetCount: visibleCodeSnippets.length, + codeCommandTexts: visibleCodeCommands.map((command) => command.textContent?.trim(), ), - maxInstallCommandOverflow: Math.max( + maxCodeCommandOverflow: Math.max( 0, - ...installCommands.map((command) => + ...visibleCodeCommands.map((command) => Math.max(0, command.scrollWidth - command.clientWidth), ), ), syntaxRoles: [ ...new Set( - syntaxTokens.flatMap((token) => + visibleSyntaxTokens.flatMap((token) => [...token.classList].filter((name) => name.startsWith("syntax-"), ), ), ), ].sort(), - syntaxTokenCount: syntaxTokens.length, + syntaxTokenCount: visibleSyntaxTokens.length, + codeTabListOverflow: codeTabList + ? Math.max(0, codeTabList.scrollWidth - codeTabList.clientWidth) + : null, + activeCodeTabFullyVisible: + codeTabListBounds && activeCodeTabBounds + ? activeCodeTabBounds.left >= codeTabListBounds.left - 1 && + activeCodeTabBounds.right <= codeTabListBounds.right + 1 + : null, topbarOverlaps, internallyClipped, }; @@ -593,24 +643,21 @@ try { ); } if ( - scenario.revealInstall && - (!layout.installVisible || - layout.installSnippetCount !== 2 || - layout.syntaxTokenCount < 8 || - layout.maxInstallCommandOverflow > 1 || - !layout.installCommandTexts.includes(scenario.expectedCli) || - !layout.installCommandTexts.includes(scenario.expectedImport) || - ![ - "syntax-command", - "syntax-keyword", - "syntax-package", - "syntax-punctuation", - "syntax-string", - "syntax-symbol", - ].every((role) => layout.syntaxRoles.includes(role))) + scenario.revealCodeTab && + (layout.codeSnippetCount !== 1 || + layout.syntaxTokenCount < 4 || + layout.maxCodeCommandOverflow > 1 || + !layout.codeCommandTexts.includes(scenario.expectedCode) || + !scenario.expectedSyntaxRoles.every((role) => + layout.syntaxRoles.includes(role), + ) || + (scenario.expectScrollableCodeTabs && + (layout.codeTabListOverflow === null || + layout.codeTabListOverflow <= 1 || + !layout.activeCodeTabFullyVisible))) ) { failures.push( - `install panel visible=${layout.installVisible} snippets=${layout.installSnippetCount} syntaxTokens=${layout.syntaxTokenCount} overflow=${layout.maxInstallCommandOverflow}px commands=${layout.installCommandTexts.join(" | ")} roles=${layout.syntaxRoles.join(",")}`, + `code panel=${scenario.revealCodeTab} snippets=${layout.codeSnippetCount} syntaxTokens=${layout.syntaxTokenCount} commandOverflow=${layout.maxCodeCommandOverflow}px tabOverflow=${layout.codeTabListOverflow}px activeTabVisible=${layout.activeCodeTabFullyVisible} commands=${layout.codeCommandTexts.join(" | ")} roles=${layout.syntaxRoles.join(",")}`, ); } if ( diff --git a/scripts/zoom-probe.mjs b/scripts/zoom-probe.mjs new file mode 100644 index 0000000..2c3cf10 --- /dev/null +++ b/scripts/zoom-probe.mjs @@ -0,0 +1,84 @@ +/* + * Detail probe for the specimen shell's ritual-tech finish. + * + * visual-review.mjs captures whole pages at 1x, which is the right scale for + * layout regressions but too coarse to confirm a 1px circuitry trace actually + * paints. This grabs one card at 3x and asserts the decorative layers resolve + * to real colours rather than to an unresolved custom property. + * + * The evaluate callbacks below run in the page, not in Node. + */ +/* global getComputedStyle, document */ +import { chromium } from "playwright"; + +const baseUrl = process.env.BASE_URL ?? "http://127.0.0.1:4321"; +const outDir = process.env.ZOOM_OUTPUT_DIR ?? "artifacts/visual-luxe"; +const executablePath = process.env.CHROME_PATH; + +const browser = await chromium.launch({ + executablePath, + args: ["--no-sandbox"], +}); +const page = await browser.newPage({ + viewport: { width: 1440, height: 1000 }, + deviceScaleFactor: 3, + colorScheme: "dark", + reducedMotion: "reduce", +}); + +await page.goto(baseUrl, { waitUntil: "load" }); +await page.waitForSelector("#specimen-main"); +await page.waitForTimeout(600); + +const card = page.locator(".specimen-card").first(); +await card.scrollIntoViewIfNeeded(); +await page.waitForTimeout(300); +await card.screenshot({ path: `${outDir}/zoom-card.png` }); + +// Prove the circuitry trace is painted, not merely declared. +const trace = await card.evaluate((node) => { + const style = getComputedStyle(node, "::before"); + return { + content: style.content, + height: style.height, + backgroundImage: style.backgroundImage.slice(0, 160), + display: style.display, + }; +}); + +const eyebrow = await page + .locator(".catalog-group__eyebrow") + .first() + .evaluate((node) => { + const style = getComputedStyle(node, "::before"); + return { + width: style.width, + backgroundImage: style.backgroundImage.slice(0, 120), + }; + }); + +const bodyLayers = await page.evaluate(() => { + const style = getComputedStyle(document.body); + return { + layerCount: style.backgroundImage.split(/,(?![^(]*\))/).length, + hasRepeating: style.backgroundImage.includes("repeating-linear-gradient"), + hasRadial: style.backgroundImage.includes("radial-gradient"), + }; +}); + +const animations = await page.evaluate(() => + document + .getAnimations() + .filter((a) => a.playState === "running") + .map((a) => a.animationName ?? "unnamed"), +); + +console.log( + JSON.stringify( + { trace, eyebrow, bodyLayers, runningAnimations: animations }, + null, + 2, + ), +); + +await browser.close(); From 8885dd3105a5a15ff95000ed644ed25e2aaa6b65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 5 Sep 2026 07:46:31 +0000 Subject: [PATCH 02/11] fix(review): address script follow-ups Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com> --- scripts/serve-dist.mjs | 2 +- scripts/verify-contracts.mjs | 2 +- scripts/zoom-probe.mjs | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/serve-dist.mjs b/scripts/serve-dist.mjs index b0ac0d8..bed66c8 100644 --- a/scripts/serve-dist.mjs +++ b/scripts/serve-dist.mjs @@ -20,7 +20,7 @@ createServer(async (request, response) => { const requested = decodeURIComponent( new URL(request.url, "http://x").pathname, ); - const candidate = path.join(root, requested); + const candidate = path.join(root, requested.slice(1)); for (const file of [candidate, path.join(root, "index.html")]) { if (!path.resolve(file).startsWith(root)) continue; diff --git a/scripts/verify-contracts.mjs b/scripts/verify-contracts.mjs index 64b631e..d2d1c3f 100644 --- a/scripts/verify-contracts.mjs +++ b/scripts/verify-contracts.mjs @@ -118,7 +118,7 @@ const assertions = [ specimenApp.includes("

{group}

"), ], [ - "install tab separates CLI from package API", + "install tabs separate CLI from React API", specimenApp.includes('CLI') && specimenApp.includes( 'React API', diff --git a/scripts/zoom-probe.mjs b/scripts/zoom-probe.mjs index 2c3cf10..48a604c 100644 --- a/scripts/zoom-probe.mjs +++ b/scripts/zoom-probe.mjs @@ -9,6 +9,7 @@ * The evaluate callbacks below run in the page, not in Node. */ /* global getComputedStyle, document */ +import { mkdir } from "node:fs/promises"; import { chromium } from "playwright"; const baseUrl = process.env.BASE_URL ?? "http://127.0.0.1:4321"; @@ -19,6 +20,7 @@ const browser = await chromium.launch({ executablePath, args: ["--no-sandbox"], }); +await mkdir(outDir, { recursive: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 3, From 3e84ca3f9028ca5a17f36a46a62b0df141cf090e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 5 Sep 2026 07:46:57 +0000 Subject: [PATCH 03/11] fix(review): harden dist path normalization Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com> --- scripts/serve-dist.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/serve-dist.mjs b/scripts/serve-dist.mjs index bed66c8..e3419c4 100644 --- a/scripts/serve-dist.mjs +++ b/scripts/serve-dist.mjs @@ -20,7 +20,7 @@ createServer(async (request, response) => { const requested = decodeURIComponent( new URL(request.url, "http://x").pathname, ); - const candidate = path.join(root, requested.slice(1)); + const candidate = path.join(root, requested.replace(/^\/+/, "")); for (const file of [candidate, path.join(root, "index.html")]) { if (!path.resolve(file).startsWith(root)) continue; From 0241ef8789e172383164e91fd9a01d714ecad1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=20Alexander=20=F0=9F=91=91?= Date: Wed, 9 Sep 2026 14:26:22 -0500 Subject: [PATCH 04/11] Ensure output directory is created before launch Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/zoom-probe.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/zoom-probe.mjs b/scripts/zoom-probe.mjs index 48a604c..3c61b38 100644 --- a/scripts/zoom-probe.mjs +++ b/scripts/zoom-probe.mjs @@ -16,6 +16,8 @@ const baseUrl = process.env.BASE_URL ?? "http://127.0.0.1:4321"; const outDir = process.env.ZOOM_OUTPUT_DIR ?? "artifacts/visual-luxe"; const executablePath = process.env.CHROME_PATH; +await mkdir(outDir, { recursive: true }); + const browser = await chromium.launch({ executablePath, args: ["--no-sandbox"], From 7ee2f604900d5fe234bd422c455e8d8177ae3e6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:31:25 +0000 Subject: [PATCH 05/11] Merge main and address review feedback Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com> --- apps/specimens/tsconfig.tsbuildinfo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/specimens/tsconfig.tsbuildinfo b/apps/specimens/tsconfig.tsbuildinfo index 24d9281..6d9a1e8 100644 --- a/apps/specimens/tsconfig.tsbuildinfo +++ b/apps/specimens/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./vite.config.ts"],"version":"6.0.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/catalog.ts","./src/code-block.tsx","./src/docs.tsx","./src/examples.tsx","./src/home.tsx","./src/lab.tsx","./src/main.tsx","./vite.config.ts"],"version":"6.0.3"} \ No newline at end of file From 509b7889923515f20a251de3a5eb585d2e3fd182 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:32:25 +0000 Subject: [PATCH 06/11] Drop accidental build artifact update Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com> --- apps/specimens/tsconfig.tsbuildinfo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/specimens/tsconfig.tsbuildinfo b/apps/specimens/tsconfig.tsbuildinfo index 6d9a1e8..24d9281 100644 --- a/apps/specimens/tsconfig.tsbuildinfo +++ b/apps/specimens/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/catalog.ts","./src/code-block.tsx","./src/docs.tsx","./src/examples.tsx","./src/home.tsx","./src/lab.tsx","./src/main.tsx","./vite.config.ts"],"version":"6.0.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./vite.config.ts"],"version":"6.0.3"} \ No newline at end of file From 5ef9c48beeb34933551ff17ab87d81b3d67d1116 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 9 Sep 2026 17:15:53 -0500 Subject: [PATCH 07/11] Revamp specimens lab and enrich blocks Refresh the specimens app with a six-scene lab, fixed-viewport navigation, live previews, and syntax-highlighted copyable source snippets, plus the OpenCoven mark favicon and updated docs. Enrich the Composer and Run Rail blocks with tool slots, keyboard send, plan/resources support, stronger visual guidance, and matching registry metadata, and add coverage for the new specimen and block behaviors. --- .DS_Store | Bin 0 -> 6148 bytes README.md | 18 +- apps/specimens/index.html | 1 + apps/specimens/package.json | 1 + apps/specimens/src/app.tsx | 596 +++++++-------------- apps/specimens/src/block-demos.tsx | 148 +++++ apps/specimens/src/code-snippet.tsx | 99 ++++ apps/specimens/src/lab.css | 516 ++++++++++++++++++ apps/specimens/src/lab.tsx | 355 ++++++++++++ apps/specimens/src/main.tsx | 1 + apps/specimens/src/specimens-fixes.css | 175 +----- apps/specimens/src/specimens.css | 236 ++++++-- packages/ui/src/blocks/composer.tsx | 62 ++- packages/ui/src/blocks/run-rail.tsx | 73 ++- packages/ui/tests/enriched-blocks.test.tsx | 99 ++++ packages/ui/tests/specimens.test.tsx | 90 ++++ pnpm-lock.yaml | 9 + public/opencoven-mark.svg | 1 + registry/blocks/registry.fragment.json | 8 +- 19 files changed, 1880 insertions(+), 608 deletions(-) create mode 100644 .DS_Store create mode 100644 apps/specimens/src/block-demos.tsx create mode 100644 apps/specimens/src/code-snippet.tsx create mode 100644 apps/specimens/src/lab.css create mode 100644 packages/ui/tests/enriched-blocks.test.tsx create mode 100644 packages/ui/tests/specimens.test.tsx create mode 100644 public/opencoven-mark.svg diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..31ae3986fbe4d958a34a9c177e6b867dfcc90b61 GIT binary patch literal 6148 zcmeHK!A`?440Xy5n~>0s93w#DJc$noQ~84N0kDn@q?Jws?)?O=aN;BQ2>yUOH^fiy z99vtfc8C)KWJ}IV96NDes=6T}H@uq+i26j-g)$~RG%dpOtPM%j!jmqpF{N|L=$3Bg zvxc`Bej@{Xc0HWh5~p@WmGc{nvLau^lrKKs+P(53O~>-@nWue`xn&0cg`~Kse=GRy+FV^{e?ODr@?m0gbW55_N z2L2iYsM##tZ9!{|0b{@zSTVrg2M=Woilt!ubf8HvPGXQww57k7u +
- - {String(index + 1).padStart(2, "0")} + + {sourceKind === "blocks" ? : } + {sourceKind === "blocks" ? "Block" : "Component"} {specimen.primitive}
@@ -111,66 +115,44 @@ function SpecimenCard({

{specimen.description}

- - - Preview +
+
+ + + Try it out +
+
{specimen.preview}
+
+ + CLI React API + States - -
{specimen.preview}
-
-
-
- shadcn CLI - registry install -
-
-              
-                pnpm{" "}
-                dlx{" "}
-                shadcn@latest{" "}
-                add{" "}
-                {registryUrl}
-              
-            
-
-

- Installs the source-owned component and its declared dependencies - through the published shadcn registry. -

+
-
-
- TypeScript - React package API -
-
-              
-                import{" "}
-                {"{ "}
-                {exportName}
-                {" }"}{" "}
-                from{" "}
-                "{packagePath}"
-                ;
-              
-            
-
-

- The React package API is the SDK surface for these UI components; - there is no separate OpenCoven UI SDK layer. -

+ +
+

- States + + {specimen.states}

@@ -181,7 +163,6 @@ function SpecimenCard({ function Library({ density, query }: { density: Density; query: string }) { const [mode, setMode] = useState("do"); - const [message, setMessage] = useState("Review the changed files"); const specimens = useMemo( () => [ @@ -417,16 +398,7 @@ function Library({ density, query }: { density: Density; query: string }) { description: "The full intent-taking surface, built only from public modules.", states: "empty, ready, running, disabled", - preview: ( - - ), + preview: , }, { id: "run-rail", @@ -436,22 +408,7 @@ function Library({ density, query }: { density: Density; query: string }) { description: "Metrics, activity, context, and budget as one operational report.", states: "populated, loading, empty, error", - preview: ( - - ), + preview: , }, { id: "transcript-turn", @@ -501,12 +458,12 @@ function Library({ density, query }: { density: Density; query: string }) { ), }, ], - [density, message, mode], + [density, mode], ); const normalizedQuery = query.trim().toLowerCase(); const filtered = specimens.filter((specimen) => - `${specimen.title} ${specimen.group} ${specimen.description}` + `${specimen.title} ${specimen.group} ${specimen.description} ${specimen.states}` .toLowerCase() .includes(normalizedQuery), ); @@ -523,8 +480,6 @@ function Library({ density, query }: { density: Density; query: string }) { ); } - let specimenIndex = 0; - return (
{groupOrder.map((group) => { @@ -554,18 +509,9 @@ function Library({ density, query }: { density: Density; query: string }) {
- {groupedSpecimens.map((specimen) => { - const currentIndex = specimenIndex; - specimenIndex += 1; - - return ( - - ); - })} + {groupedSpecimens.map((specimen) => ( + + ))}
); @@ -574,162 +520,6 @@ function Library({ density, query }: { density: Density; query: string }) { ); } -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, @@ -818,138 +608,154 @@ function App() { 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."} -

-
-
-
-
{isLab ? "Views" : "Specimens"}
-
{isLab ? "05" : "16"}
-
-
-
Schemes
-
02
-
-
-
Densities
-
02
+
+
+ +
+
+
+
+

+ {isLab + ? "The component workbench" + : "Sixteen public building blocks"} +

+

+ {isLab ? ( + <> + A little room to experiment. + + ) : ( + <> + Agent surfaces. +
+ Made tangible. + + )} +

+

+ {isLab + ? "Six scenes. Real components. One focused canvas." + : "Try the interaction, then take the source. Composable inputs, execution evidence, and complete agent workflows."} +

- -
- {isLab ? ( - - ) : ( - - )} -
-
+
+
+
{isLab ? "Views" : "Specimens"}
+
{isLab ? "06" : "16"}
+
+
+
Schemes
+
02
+
+
+
Densities
+
02
+
+
+ + {isLab ? ( + + ) : ( + + )} +
+
+
); diff --git a/apps/specimens/src/block-demos.tsx b/apps/specimens/src/block-demos.tsx new file mode 100644 index 0000000..1a767ba --- /dev/null +++ b/apps/specimens/src/block-demos.tsx @@ -0,0 +1,148 @@ +import { + Button, + CompletionPalette, + Composer, + RunRail, + type ComposerAttachment, + type ComposerMode, +} from "@opencoven/ui"; +import { Paperclip, RotateCcw, Slash } from "lucide-react"; +import { useState } from "react"; + +function ComposerDemo({ density }: { density: "default" | "compact" }) { + const [mode, setMode] = useState("do"); + const [message, setMessage] = useState( + "Review the changed files and suggest a focused fix.", + ); + const [running, setRunning] = useState(false); + const [attachments, setAttachments] = useState([ + { id: "spec", name: "spec.md", meta: "4.2 KB" }, + ]); + const [status, setStatus] = useState("Local preview. No requests are sent."); + + return ( +
+ { + setRunning(true); + setStatus("Demo run started. Use Stop to return to editing."); + }} + onStop={() => { + setRunning(false); + setStatus("Demo run stopped. Your draft is preserved."); + }} + attachments={attachments} + onRemoveAttachment={(id) => + setAttachments((items) => items.filter((item) => item.id !== id)) + } + tools={ + <> + + + Commands + + } + commands={[ + { + id: "review", + label: "/review", + description: "Review the changed files", + }, + { + id: "test", + label: "/test", + description: "Find missing test coverage", + }, + { + id: "plan", + label: "/plan", + description: "Plan a focused implementation", + }, + ]} + onSelect={(command) => { + setMessage(command.description); + if (command.id === "plan") setMode("plan"); + setStatus(`${command.label} added to your draft.`); + }} + /> + + + } + /> +

+ {status} +

+
+ ); +} + +function RunRailDemo({ density }: { density: "default" | "compact" }) { + return ( + + ); +} + +export { ComposerDemo, RunRailDemo }; diff --git a/apps/specimens/src/code-snippet.tsx b/apps/specimens/src/code-snippet.tsx new file mode 100644 index 0000000..e207cc0 --- /dev/null +++ b/apps/specimens/src/code-snippet.tsx @@ -0,0 +1,99 @@ +import { Button } from "@opencoven/ui"; +import hljs from "highlight.js/lib/core"; +import bash from "highlight.js/lib/languages/bash"; +import typescript from "highlight.js/lib/languages/typescript"; +import xml from "highlight.js/lib/languages/xml"; +import { Check, Copy, Terminal } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; + +hljs.registerLanguage("bash", bash); +hljs.registerLanguage("typescript", typescript); +hljs.registerLanguage("xml", xml); + +type CodeLanguage = "bash" | "typescript"; + +function HighlightedCode({ + code, + language = "typescript", +}: { + code: string; + language?: CodeLanguage; +}) { + const html = useMemo( + () => hljs.highlight(code, { language }).value, + [code, language], + ); + + // highlight.js escapes source text before adding its own token markup. + return ( + + ); +} + +function CodeSnippet({ + code, + language, + label, +}: { + code: string; + language: CodeLanguage; + label: string; +}) { + const [copyState, setCopyState] = useState< + "idle" | "copying" | "copied" | "failed" + >("idle"); + + useEffect(() => { + if (copyState !== "copied") return; + const timeout = window.setTimeout(() => setCopyState("idle"), 2500); + return () => window.clearTimeout(timeout); + }, [copyState]); + + async function copy() { + setCopyState("copying"); + try { + await navigator.clipboard.writeText(code); + setCopyState("copied"); + } catch { + setCopyState("failed"); + } + } + + return ( +
+
+ + + +
+
+        
+      
+ + {copyState === "failed" + ? "Could not copy. Select the code and copy it manually." + : copyState === "copied" + ? "Copied to clipboard." + : ""} + +
+ ); +} + +export { CodeSnippet, HighlightedCode }; diff --git a/apps/specimens/src/lab.css b/apps/specimens/src/lab.css new file mode 100644 index 0000000..752a578 --- /dev/null +++ b/apps/specimens/src/lab.css @@ -0,0 +1,516 @@ +.specimen-app--lab { + display: flex; + flex-direction: column; + height: 100dvh; + min-height: 0; + overflow: hidden; +} + +.specimen-app--lab .specimen-topbar { + position: relative; + flex: none; +} + +.specimen-app--lab .specimen-shell { + display: block; + flex: 1; + width: 100%; + min-height: 0; +} + +.specimen-app--lab .specimen-rail { + display: none; +} + +.specimen-app--lab .specimen-main { + height: 100%; +} + +.specimen-app--lab .specimen-main__inner { + display: grid; + width: min(100%, 90rem); + height: 100%; + min-height: 0; + grid-template-rows: auto minmax(0, 1fr); + padding: clamp(0.75rem, 2vh, 1.5rem) clamp(0.75rem, 3vw, 2.5rem); +} + +.specimen-app--lab .specimen-hero { + display: block; + margin: 0; + border: 0; + padding: 0 0 1rem; +} + +.specimen-app--lab .specimen-hero h1 { + max-width: none; + font-size: clamp(1.6rem, 3vw, 2.5rem); +} + +.specimen-app--lab .specimen-hero__copy > p:last-child, +.specimen-app--lab .specimen-stats { + display: none; +} + +.assembled-lab { + --scene-color: var(--presence); + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + border: 1px solid color-mix(in srgb, var(--scene-color) 28%, var(--border)); +} + +.assembled-lab[data-tone="information"] { + --scene-color: var(--information); +} +.assembled-lab[data-tone="success"] { + --scene-color: var(--success); +} +.assembled-lab[data-tone="warning"] { + --scene-color: var(--warning); +} + +.lab-scene-header { + display: flex; + flex: none; + align-items: center; + justify-content: space-between; + gap: 1rem; + border-block-end: 1px solid var(--border); + padding: 0.875rem 1.25rem; +} + +.lab-scene-heading, +.lab-carousel-controls { + display: flex; + min-width: 0; + align-items: center; + gap: 0.75rem; +} + +.lab-scene-icon { + display: grid; + width: 2.5rem; + height: 2.5rem; + flex: none; + place-items: center; + border: 1px solid color-mix(in srgb, var(--scene-color) 28%, var(--border)); + border-radius: var(--radius-2); + background: color-mix(in srgb, var(--scene-color) 8%, var(--card)); + color: var(--scene-color); +} + +.lab-scene-icon svg { + width: 1.125rem; + height: 1.125rem; +} +.lab-scene-heading h2 { + margin: 0; + color: var(--scene-color); + font-size: 1rem; + font-weight: 700; +} +.lab-scene-heading p { + margin: 0.2rem 0 0; + color: var(--muted-foreground); + font-size: 0.75rem; +} +.lab-carousel-controls > span { + color: var(--muted-foreground); + font-size: 0.6875rem; + white-space: nowrap; +} +.lab-carousel-controls button { + width: 2.75rem; + min-height: 2.75rem; + padding: 0; +} + +.assembled-lab .lab-carousel { + display: flex; + flex: 1; + min-height: 0; + gap: 0; +} + +.lab-carousel__viewport { + display: grid; + flex: 1; + min-width: 0; + min-height: 0; + overflow: hidden; + touch-action: pan-y pinch-zoom; +} + +.lab-scene-panel { + height: 100%; + min-height: 0; + overflow: auto; + overscroll-behavior: contain; + scrollbar-width: thin; +} + +.lab-scene-panel[hidden] { + display: none; +} +.lab-scene-panel:focus-visible { + outline-offset: -3px; +} + +.assembled-lab .assembled-lab__stage { + display: grid; + min-height: 100%; + align-content: safe center; + padding: clamp(1rem, 3vh, 2.5rem) clamp(1rem, 4vw, 4rem); + background-color: color-mix( + in srgb, + var(--scene-color) 4%, + var(--background) + ); + background-image: radial-gradient( + color-mix(in srgb, var(--scene-color) 14%, transparent) 0.75px, + transparent 0.75px + ); + background-size: 20px 20px; +} + +.lab-scene-surface { + width: min(100%, 50rem); + min-width: 0; + margin-inline: auto; +} + +.lab-scene-panel:not([hidden]) .lab-scene-surface { + animation: scene-enter var(--motion-slow) var(--motion-ease) both; +} + +.lab-scene-surface [data-slot="composer"] { + box-shadow: var(--elevation-3); +} +.lab-scene-surface .demo-status { + text-align: center; +} +.lab-scene-surface [data-slot="run-rail"] { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.25rem; +} +.lab-scene-surface [data-slot="run-rail"] > div:first-child { + grid-column: 1 / -1; +} +.lab-scene-surface [data-slot="run-rail"] > div { + min-width: 0; +} + +.assembled-lab .assembled-lab__nav { + flex: none; + overflow: visible; + border-block: 1px solid var(--border); + padding: 0.625rem; + background: var(--card); +} + +.assembled-lab .assembled-lab__tabs { + display: grid; + width: 100%; + min-width: 0; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 0.375rem; + padding: 0; + background: transparent; +} + +.assembled-lab__tabs [role="tab"] { + display: flex; + min-width: 0; + min-height: 3.5rem; + flex-direction: column; + gap: 0.375rem; + border: 1px solid transparent; + border-radius: var(--radius-2); + padding: 0.5rem; + font-size: 0.75rem; +} + +.assembled-lab__tabs [role="tab"] svg { + width: 1.125rem; + height: 1.125rem; +} +.assembled-lab__tabs [role="tab"][data-active] { + border-color: color-mix(in srgb, var(--scene-color) 35%, var(--border)); + background: color-mix(in srgb, var(--scene-color) 10%, var(--card)); + color: var(--scene-color); + box-shadow: inset 0 -2px 0 var(--scene-color); +} + +.lab-footer { + display: flex; + flex: none; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 0.625rem 1.25rem; + color: var(--muted-foreground); + font-size: 0.625rem; +} + +.lab-footer a { + display: inline-flex; + align-items: center; + gap: 0.375rem; + color: var(--scene-color); + text-decoration: none; +} +.lab-footer a:hover { + text-decoration: underline; +} +.lab-footer svg { + width: 0.875rem; + height: 0.875rem; +} +.lab-message-stack { + width: 100%; +} +.lab-message-stack [data-slot="transcript-turn"] { + padding-block: 1rem; +} +.lab-message-stack .specimen-code-snippet { + margin-block-start: 1rem; +} +.lab-context__heading { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; + margin-block-end: 1rem; + color: var(--success); +} +.lab-context__heading svg { + width: 1.25rem; + height: 1.25rem; +} +.lab-context__heading h3 { + font-size: 1rem; + font-weight: 600; +} +.lab-action-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} +.lab-action-grid button { + height: auto; + min-width: 0; + align-items: start; + justify-content: start; + padding: 1.25rem; + white-space: normal; + text-align: start; +} +.lab-action-grid button > svg { + flex: none; + margin-block-start: 0.15rem; + color: var(--warning); +} +.lab-action-grid span { + display: grid; + gap: 0.375rem; +} +.lab-action-grid small { + color: var(--muted-foreground); + font-weight: 400; + line-height: 1.5; +} +.lab-action-result { + margin-block-start: 1rem; + padding: 1.25rem; +} +.lab-action-result p { + margin-block: 0.75rem; + line-height: 1.6; +} +.lab-card-grid { + width: 100%; +} +.lab-artifact summary { + display: grid; + gap: 0.75rem; + padding: 1.25rem; + cursor: pointer; + list-style: none; +} +.lab-artifact summary::-webkit-details-marker { + display: none; +} +.lab-artifact summary [data-slot="badge"] { + justify-self: start; +} +.lab-artifact strong { + font-size: 1rem; +} +.lab-artifact small { + color: var(--muted-foreground); +} +.lab-artifact__open { + display: flex; + align-items: center; + justify-content: space-between; + color: var(--information); + font-size: 0.75rem; +} +.lab-artifact__open svg { + width: 1rem; + height: 1rem; +} +.lab-artifact details[open] .lab-artifact__open svg { + rotate: 90deg; +} +.lab-artifact details > p { + margin: 0; + border-block-start: 1px solid var(--border); + padding: 1rem 1.25rem; + color: var(--muted-foreground); + font-size: 0.8125rem; + line-height: 1.6; +} + +@keyframes scene-enter { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 68rem) { + .specimen-app--lab .specimen-topbar__inner { + flex-wrap: nowrap; + padding-block: 0.5rem; + gap: 0.5rem; + } + .specimen-app--lab .specimen-topbar__actions { + flex: 0 1 auto; + width: auto; + } + .specimen-app--lab .specimen-brand > span:last-child { + display: none; + } + .specimen-app--lab .surface-switcher { + margin-inline-start: 0; + margin-inline-end: auto; + } +} + +@media (max-width: 48rem) { + .specimen-app--lab .scheme-control span { + display: none; + } + .specimen-app--lab .specimen-hero { + padding-block-end: 0.75rem; + } + .lab-scene-header { + padding: 0.75rem; + gap: 0.5rem; + } + .lab-scene-icon { + display: none; + } + .lab-scene-heading p { + display: none; + } + .lab-carousel-controls { + gap: 0.375rem; + } + .lab-carousel-controls > span { + margin-inline-end: 0.25rem; + } + .assembled-lab .assembled-lab__stage { + padding: 1rem; + } + .lab-scene-surface [data-slot="composer"] { + padding: 1rem; + } + .lab-scene-surface [data-slot="composer"] textarea { + min-height: 5rem; + } + .lab-scene-surface [data-slot="run-rail"] { + grid-template-columns: minmax(0, 1fr); + gap: 0.875rem; + } + .assembled-lab .assembled-lab__tabs { + gap: 0.125rem; + } + .assembled-lab__tabs [role="tab"] { + min-height: 3rem; + padding: 0.5rem 0.125rem; + font-size: 0.6rem; + white-space: normal; + } + .lab-footer { + padding: 0.5rem 0.75rem; + } + .lab-footer > span { + display: none; + } + .lab-footer a { + min-height: 1.75rem; + margin-inline-start: auto; + } + .lab-action-grid { + grid-template-columns: minmax(0, 1fr); + gap: 0.625rem; + } + .lab-action-grid button { + padding: 1rem; + } + .lab-artifact summary { + padding: 1rem; + } +} + +@media (max-width: 24rem) { + .specimen-app--lab .specimen-topbar__inner { + padding-inline: 0.5rem; + gap: 0.25rem; + } + .specimen-app--lab .density-control button { + padding-inline: 0.3rem; + } + .specimen-app--lab .specimen-brand { + gap: 0; + } + .specimen-app--lab .specimen-brand__mark { + width: 1.5rem; + } + .specimen-app--lab .specimen-brand__mark > span { + width: 1.5rem; + height: 1.5rem; + } + .assembled-lab .assembled-lab__tabs { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .assembled-lab__tabs [role="tab"] { + flex-direction: row; + min-height: 2.75rem; + } +} + +@media (max-height: 650px) { + .specimen-app--lab .specimen-hero { + display: none; + } + .specimen-app--lab .specimen-main__inner { + grid-template-rows: minmax(0, 1fr); + padding-block: 0.5rem; + } + .lab-scene-header { + padding-block: 0.375rem; + } + .lab-footer { + display: none; + } +} diff --git a/apps/specimens/src/lab.tsx b/apps/specimens/src/lab.tsx index 8fc6c38..c6a8cec 100644 --- a/apps/specimens/src/lab.tsx +++ b/apps/specimens/src/lab.tsx @@ -1,19 +1,27 @@ +<<<<<<< Updated upstream import { useState } from "react"; +======= +>>>>>>> Stashed changes import { Badge, Button, Card, +<<<<<<< Updated upstream CardContent, CardFooter, CardHeader, ResourceRow, SessionHeader, +======= + ResourceRow, +>>>>>>> Stashed changes Tabs, TabsContent, TabsList, TabsTrigger, TranscriptTurn, } from "@opencoven/ui"; +<<<<<<< Updated upstream import { ComposerExample } from "./examples"; import type { Density } from "./catalog"; @@ -184,3 +192,350 @@ export function Lab({ density }: { density: Density }) { ); } +======= +import { + Activity, + ArrowLeft, + ArrowRight, + FileCode2, + Layers3, + MessageSquare, + PenLine, + Wand2, +} from "lucide-react"; +import { useRef, useState } from "react"; + +import { ComposerDemo, RunRailDemo } from "./block-demos"; +import { CodeSnippet, HighlightedCode } from "./code-snippet"; + +const scenes = [ + { + id: "composer", + title: "Composer", + subtitle: "From intent to action", + icon: PenLine, + source: "composer", + tone: "presence", + }, + { + id: "run-rail", + title: "Run rail", + subtitle: "Every step, accounted for", + icon: Activity, + source: "run-rail", + tone: "information", + }, + { + id: "messages", + title: "Messages", + subtitle: "A familiar voice, with evidence", + icon: MessageSquare, + source: "transcript-turn", + tone: "presence", + }, + { + id: "context", + title: "Context", + subtitle: "The right files, within reach", + icon: FileCode2, + source: "resource-row", + tone: "success", + }, + { + id: "actions", + title: "Actions", + subtitle: "Small controls, clear intent", + icon: Wand2, + source: "completion-palette", + tone: "warning", + }, + { + id: "cards", + title: "Cards", + subtitle: "Outcomes you can open", + icon: Layers3, + source: "session-header", + tone: "information", + }, +] as const; + +function Lab({ density }: { density: "default" | "compact" }) { + const [activeIndex, setActiveIndex] = useState(0); + const [actionStatus, setActionStatus] = useState( + "Choose an action to try its local preview.", + ); + const [attached, setAttached] = useState(false); + const [prompt, setPrompt] = useState("Review this change."); + const pointerStart = useRef<{ x: number; y: number } | null>(null); + const scene = scenes[activeIndex]!; + const SceneIcon = scene.icon; + const navigate = (offset: number) => + setActiveIndex((index) => (index + offset + scenes.length) % scenes.length); + + const views = { + composer: , + "run-rail": , + messages: ( +
+ +

+ The draft stays yours. Use {" "} + to keep it in sync with your application. +

+ ("do");\n\n' + } + /> +
+
+ ), + context: ( +
+
+
+ + + + + +

+ Context stays explicit. File operations keep their own color and + label. +

+
+ ), + actions: ( +
+
+ + +
+ + Your draft +

{prompt}

+ {attached ? ( + changes.diff · 8.1 KB + ) : null} +
+

+ {actionStatus} +

+
+ ), + cards: ( +
+ {[ + { + kind: "Pull request", + title: "Recover attachment ingestion", + meta: "12 checks passed", + detail: + "The sample change keeps attachment metadata intact across retries.", + }, + { + kind: "Proposal", + title: "A smaller composer", + meta: "Ready for your review", + detail: + "Keep the draft visible, group supporting controls, and retain one primary send action.", + }, + { + kind: "Attachment", + title: "Component preview", + meta: "384 KB · added by Cody", + detail: + "A sample image artifact associated with the current review.", + }, + { + kind: "Handoff", + title: "Deployment notes", + meta: "Source, registry, package", + detail: + "Build the package and publish the same component source through the registry.", + }, + ].map((card) => ( + +
+ + + {card.kind} + + {card.title} + {card.meta} + + Read details + +

{card.detail}

+
+
+ ))} +
+ ), + }; + + return ( +
+
+
+ + +
+

{scene.title}

+

{scene.subtitle}

+
+
+
+ + {scene.title}, scene + {String(activeIndex + 1).padStart(2, "0")} / 06 + + + +
+
+ { + const index = scenes.findIndex((item) => item.id === value); + if (index !== -1) setActiveIndex(index); + }} + className="lab-carousel" + > +
{ + pointerStart.current = null; + if ( + event.pointerType !== "touch" || + (event.target instanceof Element && + event.target.closest( + "button, input, textarea, a, summary, pre, code, [role=tab]", + )) + ) + return; + pointerStart.current = { x: event.clientX, y: event.clientY }; + }} + onPointerCancel={() => { + pointerStart.current = null; + }} + onPointerUp={(event) => { + const start = pointerStart.current; + pointerStart.current = null; + if (!start || window.getSelection()?.toString()) return; + const dx = event.clientX - start.x; + const dy = event.clientY - start.y; + if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) * 1.5) + navigate(dx < 0 ? 1 : -1); + }} + > + {scenes.map((item) => ( + +
+
{views[item.id]}
+
+
+ ))} +
+
+ + {scenes.map((item) => ( + + + ))} + +
+
+ +
+ ); +} + +export { Lab }; +>>>>>>> Stashed changes diff --git a/apps/specimens/src/main.tsx b/apps/specimens/src/main.tsx index ba2fdcb..dbd0dfe 100644 --- a/apps/specimens/src/main.tsx +++ b/apps/specimens/src/main.tsx @@ -4,6 +4,7 @@ import { createRoot } from "react-dom/client"; import "@opencoven/ui/globals.css"; import "./specimens.css"; import "./specimens-fixes.css"; +import "./lab.css"; import { App } from "./app"; const normalizedPath = window.location.pathname.replace(/\/+$/, "") || "/"; diff --git a/apps/specimens/src/specimens-fixes.css b/apps/specimens/src/specimens-fixes.css index 4894d88..fb6869d 100644 --- a/apps/specimens/src/specimens-fixes.css +++ b/apps/specimens/src/specimens-fixes.css @@ -1,10 +1,4 @@ -/* Focused regression guards layered after the specimen shell styles. */ - -/* Keep the supported viewport floor stable when users enlarge root text. */ -html { - min-width: 320px; -} - +/* Responsive guards for the catalog and its source panels. */ body:not(:has(#group-composer)) .specimen-rail a[href="#group-composer"], body:not(:has(#group-run-rail)) .specimen-rail a[href="#group-run-rail"], body:not(:has(#group-blocks)) .specimen-rail a[href="#group-blocks"] { @@ -33,22 +27,13 @@ body:not(:has(#group-blocks)) .specimen-rail a[href="#group-blocks"] { min-width: 7rem; flex: 1 1 10rem; } - - .specimen-shell { - grid-template-columns: minmax(0, 1fr); - } } @media (max-width: 48rem) { - .specimen-shell { - box-sizing: border-box; - width: 100%; - min-width: 0; - max-width: 100vw; - margin-inline: 0; - } - - .specimen-main__inner { + .specimen-shell, + .specimen-main__inner, + .specimen-rail, + .specimen-card { box-sizing: border-box; width: 100%; min-width: 0; @@ -57,44 +42,17 @@ body:not(:has(#group-blocks)) .specimen-rail a[href="#group-blocks"] { .specimen-hero, .specimen-hero__copy, - .specimen-stats { + .specimen-stats, + .specimen-stats div, + .specimen-rail > * { min-width: 0; max-width: 100%; } - .specimen-stats { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - - .specimen-stats div { - min-width: 0; - } - - .specimen-stats :where(dt, dd) { - overflow-wrap: anywhere; - } - - .specimen-rail { - box-sizing: border-box; - width: 100%; - min-width: 0; - max-width: 100%; - } - - .specimen-rail > *, + .specimen-stats :where(dt, dd), .specimen-rail__context, - .specimen-rail__nav, .specimen-rail__package { - box-sizing: border-box; - min-width: 0; - max-width: 100%; - } - - .specimen-rail__context, - .specimen-rail__package, - .specimen-rail__package code { overflow-wrap: anywhere; - word-break: break-word; } .specimen-rail__nav { @@ -104,10 +62,6 @@ body:not(:has(#group-blocks)) .specimen-rail a[href="#group-blocks"] { grid-template-columns: repeat(auto-fit, minmax(min(100%, 5.5rem), 1fr)); } - .specimen-rail__nav:has(a:only-child) { - grid-template-columns: minmax(0, 1fr); - } - .specimen-rail__nav a { min-width: 0; justify-content: center; @@ -115,93 +69,49 @@ body:not(:has(#group-blocks)) .specimen-rail a[href="#group-blocks"] { padding-inline: 0.375rem; } - .specimen-grid { - grid-template-columns: minmax(0, 1fr); - } - - .specimen-card { - box-sizing: border-box; - width: 100%; - min-width: 0; - max-width: 100%; - } - .specimen-card > [data-slot="tabs"] { width: 100%; min-width: 0; max-width: 100%; - gap: 0; } .specimen-card > [data-slot="tabs"] > [data-slot="tabs-list"] { display: grid; box-sizing: border-box; - width: 100% !important; + width: 100%; min-width: 0; max-width: 100%; min-height: 2.75rem; grid-template-columns: repeat(3, minmax(5.25rem, 1fr)); gap: 0; - margin: 0 !important; overflow-x: auto; border-block-end: 1px solid var(--border); padding: 0 0.75rem; scrollbar-width: thin; } - .specimen-card - > [data-slot="tabs"] - > [data-slot="tabs-list"] - > [data-slot="tabs-trigger"] { + .specimen-code-tabs [role="tab"] { min-width: 0; - max-width: 100%; - min-height: 2.75rem; padding: 0.625rem 0.5rem; } - .specimen-card - > [data-slot="tabs"] - > [data-slot="tabs-list"] - > [data-slot="tabs-trigger"][data-active]::after { - inset-block-end: 0 !important; + .specimen-code-tabs [role="tab"][data-active]::after { + inset-block-end: 0; } - .specimen-card - > [data-slot="tabs"] - > [data-slot="tabs-list"] - > [data-slot="tabs-trigger"]:focus-visible { + .specimen-code-tabs [role="tab"]:focus-visible { outline-offset: -3px; } - .specimen-card > [data-slot="tabs"] > [data-slot="tabs-content"] { - width: 100%; - min-width: 0; - max-width: 100%; - } - .specimen-card__header { min-height: 0; padding: 1rem; } - .specimen-card__title { - margin-block-start: 0.875rem; - } - - .specimen-card__description { - margin-block-start: 0.375rem; - line-height: 1.5; - } - .specimen-stage { - box-sizing: border-box; - width: 100%; min-width: 0; - min-height: 0; - max-width: 100%; - align-content: start; + min-height: 10rem; justify-items: stretch; - border-block-start: 0; padding: 1rem; } @@ -224,58 +134,9 @@ body:not(:has(#group-blocks)) .specimen-rail a[href="#group-blocks"] { } .specimen-documentation { - box-sizing: border-box; - width: 100%; - min-width: 0; - min-height: 0; - max-width: 100%; - padding: 1rem; - } - - .specimen-command { - max-width: 100%; - } -} - -@media (max-width: 24.375rem) { - .assembled-lab > [data-slot="tabs"], - .assembled-lab [data-slot="tabs-content"], - .assembled-lab__nav, - .assembled-lab__stage { - box-sizing: border-box; - width: 100%; - min-width: 0; - max-width: 100%; - } - - .assembled-lab__nav { - overflow-x: hidden; - } - - .assembled-lab__tabs { - display: grid; - box-sizing: border-box; - width: 100% !important; - min-width: 0; - max-width: 100%; - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - - .assembled-lab__tabs [role="tab"] { - min-width: 0; - padding-inline: 0.25rem; - font-size: 0.625rem; - white-space: normal; - } - - .assembled-lab__stage { - min-height: 0; - align-content: start; - } - - .assembled-lab__stage > * { min-width: 0; max-width: 100%; + padding: 0.75rem 1rem 1rem; } } @@ -299,10 +160,6 @@ body:not(:has(#group-blocks)) .specimen-rail a[href="#group-blocks"] { font-size: 2rem; } - .specimen-hero__copy > p:last-child { - margin-block-start: 0.625rem; - } - .catalog-group__header { gap: 0.375rem; } diff --git a/apps/specimens/src/specimens.css b/apps/specimens/src/specimens.css index 5e745f4..6ebf5d4 100644 --- a/apps/specimens/src/specimens.css +++ b/apps/specimens/src/specimens.css @@ -62,15 +62,14 @@ body { height: 2rem; flex: none; place-items: center; - border: 1px solid color-mix(in srgb, var(--presence) 42%, var(--border)); - border-radius: var(--radius-2); - background: color-mix(in srgb, var(--presence) 12%, var(--card)); - color: var(--presence); + color: var(--foreground); } -.specimen-brand__mark svg { - width: 1rem; - height: 1rem; +.specimen-brand__mark > span { + width: 1.875rem; + height: 1.875rem; + background: currentColor; + mask: url("/opencoven-mark.svg") center / contain no-repeat; } .specimen-brand > span:last-child { @@ -290,6 +289,19 @@ body { text-wrap: balance; } +.specimen-hero h1 em { + color: var(--presence); + font-style: italic; +} + +.specimen-stats div:nth-child(2) dd { + color: var(--information); +} + +.specimen-stats div:nth-child(3) dd { + color: var(--success); +} + .specimen-hero__copy > p:last-child { max-width: 68ch; margin: 0.875rem 0 0; @@ -332,9 +344,18 @@ body { } .catalog-group { + --section-color: var(--presence); scroll-margin-block-start: 0; } +#group-run-rail { + --section-color: var(--information); +} + +#group-blocks { + --section-color: var(--success); +} + .catalog-group__header { display: flex; align-items: end; @@ -345,7 +366,7 @@ body { .catalog-group__eyebrow { margin: 0; - color: var(--presence); + color: var(--section-color); font-size: 0.625rem; font-weight: 750; letter-spacing: 0.14em; @@ -359,7 +380,7 @@ body { font-weight: 700; letter-spacing: -0.025em; line-height: 1.2; - color: var(--foreground); + color: var(--section-color); text-wrap: balance; } @@ -382,10 +403,12 @@ body { .specimen-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.75rem; + gap: 1.25rem; } .specimen-card { + display: flex; + flex-direction: column; min-width: 0; overflow: hidden; scroll-margin-block-start: 0; @@ -402,15 +425,14 @@ body { .specimen-card:hover, .specimen-card:focus-within { - border-color: color-mix(in srgb, var(--presence) 34%, var(--border)); + border-color: color-mix(in srgb, var(--section-color) 48%, var(--border)); box-shadow: var(--elevation-2); translate: 0 -1px; } .specimen-card__header { min-height: 8.25rem; - border-block-end: 1px solid var(--border); - padding: 1rem; + padding: 1.25rem 1.25rem 1rem; } .specimen-card__meta { @@ -420,16 +442,26 @@ body { gap: 1rem; } -.specimen-card__index { - color: var(--muted-foreground); +.specimen-card__kind { + display: inline-flex; + align-items: center; + gap: 0.45rem; + color: var(--section-color); font-size: 0.625rem; font-weight: 700; letter-spacing: 0.12em; + text-transform: uppercase; +} + +.specimen-card__kind svg { + width: 0.875rem; + height: 0.875rem; } .specimen-card__title { margin: 0.75rem 0 0; - font-size: 1.0625rem; + color: color-mix(in srgb, var(--section-color) 55%, var(--foreground)); + font-size: 1.25rem; font-weight: 750; letter-spacing: -0.015em; } @@ -442,21 +474,80 @@ body { line-height: 1.55; } +.specimen-preview { + position: relative; + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + margin: 0 0.75rem 0.75rem; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--section-color) 24%, var(--border)); + border-radius: var(--radius-2); + background: color-mix(in srgb, var(--section-color) 5%, var(--background)); + box-shadow: inset 0 1px 0 + color-mix(in srgb, var(--section-color) 18%, transparent); +} + +.specimen-preview__label { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 0.5rem; + padding: 0.75rem 1rem 0; + color: var(--section-color); + font-size: 0.6rem; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +.specimen-preview__label > span:first-child { + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +.specimen-preview__label > span:last-child { + color: var(--muted-foreground); +} + +.specimen-preview__label svg { + width: 0.45rem; + height: 0.45rem; + fill: currentColor; +} + .specimen-stage { display: grid; - min-height: 12.5rem; + flex: 1; + min-height: 14rem; align-content: center; justify-items: center; - border-block-start: 1px solid - color-mix(in srgb, var(--border) 62%, transparent); - background: color-mix(in srgb, var(--muted) 48%, var(--background)); - padding: clamp(1rem, 2vw, 1.5rem); + padding: clamp(1rem, 2.5vw, 2rem); } .specimen-stage > * { + min-width: 0; max-width: 100%; } +.composer-demo { + width: 100%; + min-width: 0; +} + +.demo-status { + margin: 0.75rem 0 0; + color: var(--muted-foreground); + font-size: 0.6875rem; + line-height: 1.5; +} + +.specimen-stage > .composer-demo, +.specimen-stage > [data-slot="card"] { + width: 100%; +} + .specimen-stage > [data-slot="composer"], .specimen-stage > [data-slot="session-header"] { width: 100%; @@ -472,14 +563,38 @@ body { .specimen-documentation { display: grid; - min-height: 12.5rem; + min-height: 7.5rem; align-content: start; gap: 0.875rem; - padding: 1.25rem; + padding: 0.75rem 1rem 1rem; font-size: 0.8125rem; line-height: 1.6; } +.specimen-card > [data-slot="tabs"] { + gap: 0; + border-block-start: 1px solid var(--border); + background: color-mix(in srgb, var(--background) 70%, var(--card)); +} + +.specimen-code-tabs { + margin: 0; + padding: 0.375rem 0.75rem; +} + +.specimen-code-tabs [role="tab"] { + min-height: 2.75rem; + font-size: 0.6875rem; +} + +.specimen-code-tabs [role="tab"][data-active] { + color: var(--section-color); +} + +.specimen-code-tabs [role="tab"][data-active]::after { + background: var(--section-color); +} + .specimen-documentation p { margin: 0; } @@ -491,31 +606,44 @@ body { .specimen-code-snippet__header { display: flex; - align-items: baseline; + align-items: center; justify-content: space-between; gap: 0.75rem; } .specimen-code-snippet__header span { - color: var(--foreground); - font-size: 0.6875rem; + display: inline-flex; + align-items: center; + gap: 0.4rem; + color: var(--muted-foreground); + font-size: 0.6rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; } -.specimen-code-snippet__header small { - color: var(--muted-foreground); +.specimen-code-snippet__header svg { + width: 0.75rem; + height: 0.75rem; +} + +.specimen-code-snippet__header button { + min-height: 2.75rem; font-size: 0.625rem; } +.code-copy-error { + color: var(--destructive); + font-size: 0.75rem; +} + .specimen-command { display: block; margin: 0; overflow-wrap: anywhere; border: 1px solid var(--border); border-radius: var(--radius-2); - background: var(--muted); + background: var(--background); color: var(--foreground); padding: 0.75rem; font-size: 0.7rem; @@ -524,28 +652,55 @@ body { word-break: break-word; } -.syntax-command, -.syntax-symbol { +.hljs { color: var(--foreground); - font-weight: 750; + font-family: var(--font-numeric); } -.syntax-keyword { +.hljs-keyword, +.hljs-built_in, +.hljs-selector-tag, +.hljs-meta { color: var(--presence); } -.syntax-package { - color: color-mix(in srgb, var(--presence) 72%, var(--foreground)); +.hljs-string, +.hljs-regexp, +.hljs-template-tag { + color: var(--success); +} + +.hljs-title, +.hljs-name, +.hljs-attr, +.hljs-property, +.hljs-type { + color: var(--information); } -.syntax-string { - color: color-mix(in srgb, var(--information) 70%, var(--foreground)); +.hljs-number, +.hljs-literal, +.hljs-variable, +.hljs-template-variable, +.hljs-params { + color: var(--warning); } -.syntax-punctuation { +.hljs-comment, +.hljs-punctuation { color: var(--muted-foreground); } +p > .hljs { + border: 1px solid var(--border); + border-radius: var(--radius-1); + background: var(--muted); + padding: 0.1em 0.3em; + color: var(--information); + font-size: 0.8em; + overflow-wrap: anywhere; +} + .specimen-install-meta { display: grid; grid-template-columns: auto minmax(0, 1fr); @@ -561,6 +716,11 @@ body { text-transform: uppercase; } +.specimen-install-meta svg { + width: 1rem; + height: 1rem; +} + .run-spine { position: relative; } diff --git a/packages/ui/src/blocks/composer.tsx b/packages/ui/src/blocks/composer.tsx index d373773..8f23a51 100644 --- a/packages/ui/src/blocks/composer.tsx +++ b/packages/ui/src/blocks/composer.tsx @@ -1,6 +1,7 @@ "use client"; import * as React from "react"; +import { Bot, CornerDownLeft, PenLine } from "lucide-react"; import { AttachmentChip } from "@opencoven/ui/components/attachment-chip"; import { @@ -28,6 +29,7 @@ type ComposerProps = { running?: boolean; onStop?: () => void; model?: string; + tools?: React.ReactNode; density?: "default" | "compact"; className?: string; }; @@ -43,29 +45,61 @@ function Composer({ running = false, onStop, model = "GPT-5.6 Sol", + tools, density = "default", className, }: ComposerProps) { + const messageId = React.useId(); + const hintId = React.useId(); + const modeHint = { + chat: "Explore ideas without changing files.", + do: "Make changes with the context you provide.", + plan: "Map the approach before making changes.", + }[mode]; + return (
-