From 0cbb4d6f34c68d74233e82d1fbfe6fc6f6856fac Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Wed, 9 Sep 2026 08:50:47 -0700 Subject: [PATCH] feat(design-system): ship the interactive catalog and generated UI foundation Signed-off-by: Arjun Mahanti Co-authored-by: Codex --- .github/workflows/design-system.yml | 8 + desktop-next/.gitignore | 3 + desktop-next/DESIGN.md | 2 + desktop-next/README.md | 45 ++ desktop-next/TESTING.md | 31 ++ desktop-next/package.json | 16 +- desktop-next/playwright.config.ts | 20 + desktop-next/scripts/check-contrast.mjs | 13 + desktop-next/scripts/check-type.mjs | 5 +- .../src/app/routes/design/components.tsx | 5 + .../src/app/routes/design/compositions.tsx | 5 + .../src/app/routes/design/open-source.tsx | 5 + .../design-system/catalog/catalog.css | 53 ++ .../design-system/catalog/display.tsx | 257 ++++++++++ .../features/design-system/catalog/forms.tsx | 332 +++++++++++++ .../design-system/catalog/navigation.tsx | 239 +++++++++ .../design-system/catalog/overlays.tsx | 307 ++++++++++++ .../design-system/catalog/registry.ts | 456 ++++++++++++++++++ .../design-system/ui/ComponentsPage.tsx | 106 ++++ .../design-system/ui/CompositionsPage.tsx | 254 ++++++++++ .../design-system/ui/DesignSystemLayout.tsx | 9 +- .../design-system/ui/FoundationRoles.tsx | 44 ++ .../features/design-system/ui/MotionPage.tsx | 22 +- .../design-system/ui/OpenSourcePage.tsx | 121 +++++ .../design-system/ui/OverviewPage.tsx | 164 ++++--- .../features/design-system/ui/RadiusPage.tsx | 13 +- .../features/design-system/ui/SpacingPage.tsx | 19 +- .../design-system/ui/TypographyPage.tsx | 7 +- desktop-next/tests/catalog.spec.ts | 282 +++++++++++ desktop-next/vite.config.ts | 11 + packages/design-tokens/build.mjs | 2 +- packages/design-tokens/src/foundations.css | 7 +- packages/design-tokens/src/registry.ts | 13 +- packages/design-tokens/src/tokens.css | 2 + packages/ui/README.md | 18 + packages/ui/package.json | 3 +- .../ui/src/generated/GeneratedResponse.tsx | 157 ++++++ packages/ui/src/generated/schema.ts | 161 +++++++ packages/ui/src/index.ts | 10 + packages/ui/src/primitives/toast.tsx | 1 + packages/ui/src/styles/controls.css | 6 +- packages/ui/src/styles/layout.css | 6 + packages/ui/src/styles/overlays.css | 7 + packages/ui/test/generated.test.mjs | 92 ++++ packages/ui/test/renderer.test.mjs | 43 ++ packages/ui/vite.config.ts | 10 + pnpm-lock.yaml | 19 + 47 files changed, 3291 insertions(+), 120 deletions(-) create mode 100644 desktop-next/README.md create mode 100644 desktop-next/TESTING.md create mode 100644 desktop-next/playwright.config.ts create mode 100644 desktop-next/src/app/routes/design/components.tsx create mode 100644 desktop-next/src/app/routes/design/compositions.tsx create mode 100644 desktop-next/src/app/routes/design/open-source.tsx create mode 100644 desktop-next/src/features/design-system/catalog/catalog.css create mode 100644 desktop-next/src/features/design-system/catalog/display.tsx create mode 100644 desktop-next/src/features/design-system/catalog/forms.tsx create mode 100644 desktop-next/src/features/design-system/catalog/navigation.tsx create mode 100644 desktop-next/src/features/design-system/catalog/overlays.tsx create mode 100644 desktop-next/src/features/design-system/catalog/registry.ts create mode 100644 desktop-next/src/features/design-system/ui/ComponentsPage.tsx create mode 100644 desktop-next/src/features/design-system/ui/CompositionsPage.tsx create mode 100644 desktop-next/src/features/design-system/ui/FoundationRoles.tsx create mode 100644 desktop-next/src/features/design-system/ui/OpenSourcePage.tsx create mode 100644 desktop-next/tests/catalog.spec.ts create mode 100644 packages/ui/src/generated/GeneratedResponse.tsx create mode 100644 packages/ui/src/generated/schema.ts create mode 100644 packages/ui/test/generated.test.mjs create mode 100644 packages/ui/test/renderer.test.mjs diff --git a/.github/workflows/design-system.yml b/.github/workflows/design-system.yml index 95c821be54..5639f41fe2 100644 --- a/.github/workflows/design-system.yml +++ b/.github/workflows/design-system.yml @@ -17,3 +17,11 @@ jobs: - run: pnpm install --filter buzz-desktop-next... --frozen-lockfile - run: pnpm --filter buzz-desktop-next check - run: pnpm --filter buzz-desktop-next build + - run: pnpm exec biome check packages/ui packages/design-tokens + - run: pnpm --filter buzz-desktop-next exec playwright install --with-deps chromium + - run: pnpm --filter buzz-desktop-next exec playwright test + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: buzz-design-system + path: desktop-next/dist + retention-days: 14 diff --git a/desktop-next/.gitignore b/desktop-next/.gitignore index 56f1058162..e8f0186e18 100644 --- a/desktop-next/.gitignore +++ b/desktop-next/.gitignore @@ -3,3 +3,6 @@ dist *.log .DS_Store src/app/routeTree.gen.ts + +test-results +playwright-report diff --git a/desktop-next/DESIGN.md b/desktop-next/DESIGN.md index 1a46d64a2c..e4316ad0d6 100644 --- a/desktop-next/DESIGN.md +++ b/desktop-next/DESIGN.md @@ -183,3 +183,5 @@ Every addition lands in `src/shared/tokens/registry.ts` in the same change that ## Open-source component foundation The shared component library is built proactively for the new client. Public Base UI provides behavior; Buzz owns styling, tokens, and named variants. Use Inter, JetBrains Mono, and Lucide. See OPEN_SOURCE.md for the adaptation contract. + +Editable and checkable control boundaries use the proposed `border-interactive` role, measured at 3:1 against neutral surfaces. This is an intentional exception to the quiet separator ramp; it identifies an affordance rather than separating content. diff --git a/desktop-next/README.md b/desktop-next/README.md new file mode 100644 index 0000000000..32df028d97 --- /dev/null +++ b/desktop-next/README.md @@ -0,0 +1,45 @@ +# Buzz design system + +The next Buzz web client starts with a living design system. `/design` is the +visual overview; `/design/components` contains 54 searchable live examples; +`/design/compositions` shows product patterns and generated responses. + +```sh +. ./bin/activate-hermit +pnpm install --frozen-lockfile +pnpm --filter buzz-desktop-next dev +``` + +Open `http://localhost:1430/design`. Use another port when needed: +`pnpm --filter buzz-desktop-next dev --port 5188`. + +## Packages and portability + +- `packages/design-tokens`: semantic registry, CSS roles, typography, geometry, + and motion. Color and type pages render from this source. +- `packages/ui`: React components, public behavior primitives, portable CSS, + generated-response schema and renderer, and third-party notices. +- `desktop-next`: documentation and examples, consuming those packages. + +`pnpm --filter buzz-desktop-next build` creates a self-contained static site in +`desktop-next/dist`, including local fonts and license notices. Serve at the host +root with an SPA fallback to `index.html` for `/design/*` routes. No authentication, +Blockcell, Storybook, private package registry, relay, or Tauri runtime is required. +The Design system GitHub workflow uploads this site as a build artifact; it does +not publish a website or an npm package automatically. + +## Checks + +```sh +pnpm --filter buzz-desktop-next check +pnpm --filter buzz-desktop-next test:e2e +``` + +The first command checks formatting, typography roles, contrast, token output, +compiled utility mappings, schema bounds, and renderer action safety. The second +builds the production app and exercises it in Chromium. Install the browser once +with `pnpm --filter buzz-desktop-next exec playwright install chromium` if needed. +See `TESTING.md` for coverage and screenshot paths. + +Read `DESIGN.md` for visual rules and `OPEN_SOURCE.md` for the adaptation contract. +The existing desktop and mobile clients are not migrated by this foundation. diff --git a/desktop-next/TESTING.md b/desktop-next/TESTING.md new file mode 100644 index 0000000000..b527fd9bb1 --- /dev/null +++ b/desktop-next/TESTING.md @@ -0,0 +1,31 @@ +# Design-system validation + +Use the repository Hermit environment. The catalog runs independently of Tauri, +so it needs no mock native bridge or live relay. + +- `pnpm check` checks formatting, both source layers' typography, both modes' + APCA text pairings, essential-control boundary contrast, generated CSS freshness, + compiled Tailwind role mappings, and generated-response validation/rendering. +- `pnpm test:e2e` builds fresh production output, starts a dedicated preview server + on 5189, and runs Chromium. It refuses to reuse an existing server. +- `pnpm exec playwright test` reuses an already-built `dist` for a focused rerun. + Rebuild after any source change. `pnpm exec playwright test --grep ` narrows + a regression without replacing the full final pass. + +Browser coverage binds the actual production catalog: all 54 examples, axe ARIA +checks in both modes, no external runtime requests, preserved loading labels, +checkbox and switch keyboard activation, dialog focus trap and return, selection, +command dispatch, form validation, multiline input, tabs, manual carousel, +keyboard resizing, filtering recovery, generated-response actions/streaming/error +recovery, reduced motion, and overflow at 390/768/1440px. + +Axe's WCAG text-color rule is disabled because DESIGN.md deliberately uses APCA; +`check:contrast` is the required independent contrast gate. Automated accessibility +checks supplement manual keyboard and visual inspection; they do not certify every +screen-reader/browser combination. + +Screenshots are generated under `test-results/`. Every explicit capture uses the +repository's `waitForAnimations` helper. Overview light/dark and composed examples +are different states; compare hashes before including images in a PR. Use the root +`scripts/post-screenshots.sh` workflow for PR image hosting. Do not commit reports, +traces, or screenshots to the source branch. diff --git a/desktop-next/package.json b/desktop-next/package.json index 220447a251..488bc3cfd8 100644 --- a/desktop-next/package.json +++ b/desktop-next/package.json @@ -15,19 +15,22 @@ "check:contrast": "node scripts/check-contrast.mjs", "check:tokens": "node ../packages/design-tokens/build.mjs --check", "tokens:build": "node ../packages/design-tokens/build.mjs", - "test": "pnpm --filter @buzz/design-tokens test" + "test": "pnpm --filter @buzz/design-tokens test && pnpm --filter @buzz/ui test", + "test:e2e": "pnpm build && playwright test" }, "dependencies": { + "@buzz/design-tokens": "workspace:*", + "@buzz/ui": "workspace:*", "@fontsource-variable/inter": "^5.2.8", "@fontsource/jetbrains-mono": "^5.3.0", "@tanstack/react-router": "^1.168.10", + "lucide-react": "^1.0.0", "react": "^19.1.0", - "react-dom": "^19.1.0", - "@buzz/design-tokens": "workspace:*", - "@buzz/ui": "workspace:*", - "lucide-react": "^1.0.0" + "react-dom": "^19.1.0" }, "devDependencies": { + "@axe-core/playwright": "^4.13.0", + "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4.3.0", "@tanstack/router-plugin": "^1.167.12", "@types/react": "^19.1.8", @@ -35,7 +38,6 @@ "@vitejs/plugin-react": "^6.0.0", "tailwindcss": "^4.3.0", "typescript": "~6.0.0", - "vite": "^8.0.0", - "@playwright/test": "^1.58.2" + "vite": "^8.0.0" } } diff --git a/desktop-next/playwright.config.ts b/desktop-next/playwright.config.ts new file mode 100644 index 0000000000..80b451280e --- /dev/null +++ b/desktop-next/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig, devices } from "@playwright/test"; +export default defineConfig({ + testDir: "./tests", + fullyParallel: true, + workers: 2, + timeout: 30000, + reporter: [["list"], ["html", { open: "never" }]], + use: { + baseURL: "http://127.0.0.1:5189", + trace: "retain-on-failure", + screenshot: "only-on-failure", + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], + webServer: { + command: "pnpm exec vite preview --host 127.0.0.1 --port 5189 --strictPort", + url: "http://127.0.0.1:5189/design", + reuseExistingServer: false, + timeout: 30000, + }, +}); diff --git a/desktop-next/scripts/check-contrast.mjs b/desktop-next/scripts/check-contrast.mjs index 7390715e47..bc503fa3d2 100644 --- a/desktop-next/scripts/check-contrast.mjs +++ b/desktop-next/scripts/check-contrast.mjs @@ -198,3 +198,16 @@ console.log("✓ Contrast: every text role clears its APCA target in both modes" for (const [role, why] of EXCEPTIONS) { console.log(` (exception) ${role} — ${why.split(";")[0]}`); } + +// Essential control boundaries use non-text contrast, independently of the APCA text contract. +for (const [mode, map] of Object.entries(modes)) { + for (const surface of SURFACES) { + const border = resolve(map, "--border-interactive"); + const fill = resolve(map, surface); + if (!border || !fill || wcagRatio(border, fill) < 3) { + console.error(`Control boundary fails 3:1 in ${mode} on ${surface}`); + process.exit(1); + } + } +} +console.log("✓ Essential control boundaries clear 3:1 in both modes"); diff --git a/desktop-next/scripts/check-type.mjs b/desktop-next/scripts/check-type.mjs index 3833679f1f..68d02b0fa4 100644 --- a/desktop-next/scripts/check-type.mjs +++ b/desktop-next/scripts/check-type.mjs @@ -83,7 +83,10 @@ function walk(dir) { const failures = []; -for (const file of walk(SRC)) { +for (const file of [ + ...walk(SRC), + ...walk(new URL("../../packages/ui/src", import.meta.url).pathname), +]) { const source = readFileSync(file, "utf8"); // The typography ramp itself is the one place literals are legal — it is // layer 1, where values live by design. diff --git a/desktop-next/src/app/routes/design/components.tsx b/desktop-next/src/app/routes/design/components.tsx new file mode 100644 index 0000000000..6e76afec32 --- /dev/null +++ b/desktop-next/src/app/routes/design/components.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ComponentsPage } from "@/features/design-system/ui/ComponentsPage"; +export const Route = createFileRoute("/design/components")({ + component: ComponentsPage, +}); diff --git a/desktop-next/src/app/routes/design/compositions.tsx b/desktop-next/src/app/routes/design/compositions.tsx new file mode 100644 index 0000000000..c16554b6dc --- /dev/null +++ b/desktop-next/src/app/routes/design/compositions.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { CompositionsPage } from "@/features/design-system/ui/CompositionsPage"; +export const Route = createFileRoute("/design/compositions")({ + component: CompositionsPage, +}); diff --git a/desktop-next/src/app/routes/design/open-source.tsx b/desktop-next/src/app/routes/design/open-source.tsx new file mode 100644 index 0000000000..c9b07a0405 --- /dev/null +++ b/desktop-next/src/app/routes/design/open-source.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { OpenSourcePage } from "@/features/design-system/ui/OpenSourcePage"; +export const Route = createFileRoute("/design/open-source")({ + component: OpenSourcePage, +}); diff --git a/desktop-next/src/features/design-system/catalog/catalog.css b/desktop-next/src/features/design-system/catalog/catalog.css new file mode 100644 index 0000000000..159b1a6c28 --- /dev/null +++ b/desktop-next/src/features/design-system/catalog/catalog.css @@ -0,0 +1,53 @@ +.catalog-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + column-gap: var(--space-page-inset); + row-gap: 3rem; +} +.catalog-entry { + display: flex; + flex-direction: column; + min-width: 0; + gap: var(--space-panel-inset); + scroll-margin-top: 2rem; +} +.catalog-preview { + padding-block: var(--space-panel-inset); + min-height: 10rem; + display: flex; + align-items: center; +} +.catalog-preview > * { + max-width: 100%; + width: 100%; +} +.catalog-source { + margin-top: auto; + border-top: 1px solid var(--border-tertiary); + padding-top: var(--space-control-inset); +} +.catalog-source summary { + cursor: pointer; + width: fit-content; +} +.catalog-source pre { + overflow: auto; + max-height: 24rem; + margin-top: var(--space-control-inset); + padding: var(--space-control-inset); + background: var(--bg-inset); + border-radius: var(--radius-control); + tab-size: 2; +} +.catalog-source summary:focus-visible { + outline: 2px solid var(--ring-focus); + outline-offset: 4px; +} +@media (max-width: 760px) { + .catalog-grid { + grid-template-columns: minmax(0, 1fr); + } + .catalog-preview { + min-height: auto; + } +} diff --git a/desktop-next/src/features/design-system/catalog/display.tsx b/desktop-next/src/features/design-system/catalog/display.tsx new file mode 100644 index 0000000000..e1bf2f1cc7 --- /dev/null +++ b/desktop-next/src/features/design-system/catalog/display.tsx @@ -0,0 +1,257 @@ +import { Link } from "@tanstack/react-router"; +import { + Alert, + AspectRatio, + Avatar, + Badge, + BarChart, + Button, + Card, + Carousel, + EmptyState, + Meter, + Progress, + Resizable, + ScrollArea, + Separator, + Skeleton, + Spinner, + Table, +} from "@buzz/ui"; +import { Check, Layers, Sparkles } from "lucide-react"; +export function CardDemo() { + return ( + +
+
+

Build something together

+

+ A quiet container for a focused group of ideas. +

+ +
+ ); +} +export function BadgeDemo() { + return ( +
+ Draft + In progress + + + Needs attention + Failed + Information +
+ ); +} +export function AlertDemo() { + return ( +
+ Your changes are visible only in this preview. + All tasks completed. + One task needs your attention. + + Unable to connect. Check your connection and try again. + +
+ ); +} +export function AvatarDemo() { + return ( +
+ {["AJ", "TB", "DS"].map((name) => ( + + {name} + + ))} + + + + +
+ ); +} +export function ProgressDemo() { + return ( + +
+ Uploading files + +
+ + + +
+ ); +} +export function MeterDemo() { + return ( + +
+ Storage used + +
+ + + +
+ ); +} +export function SkeletonDemo() { + return ( +
+ + + Loading project +
+ ); +} +export function SpinnerDemo() { + return ; +} +export function EmptyDemo() { + return ( + } + > + See an example project + + } + /> + ); +} +export function SeparatorDemo() { + return ( +
+ Project overview + + Recent activity +
+ ); +} +export function TableDemo() { + return ( + + + + + + + + + + + {[ + ["Review tokens", "Alex", "Complete"], + ["Build components", "Sam", "In progress"], + ["Write guidance", "Riley", "Draft"], + ].map(([task, owner, status]) => ( + + + + + + ))} + +
Recent tasks
TaskOwnerStatus
{task}{owner} + + {status} + +
+ ); +} +export function ChartDemo() { + return ( + + ); +} +export function AspectRatioDemo() { + return ( + + 16 : 9 + + ); +} +export function CarouselDemo() { + return ( + ( +
+

Principle {index + 1}

+

{title}

+
+ ))} + /> + ); +} +export function ScrollAreaDemo() { + return ( + + +
+
    + {Array.from({ length: 20 }, (_, i) => `Activity ${i + 1}`).map( + (label) => ( +
  1. {label}
  2. + ), + )} +
+
+
+ + + +
+ ); +} +export function ResizableDemo() { + return ( + + +
+ Project list +
+
+ + +
+ Conversation +
+
+
+ ); +} diff --git a/desktop-next/src/features/design-system/catalog/forms.tsx b/desktop-next/src/features/design-system/catalog/forms.tsx new file mode 100644 index 0000000000..e392a8137e --- /dev/null +++ b/desktop-next/src/features/design-system/catalog/forms.tsx @@ -0,0 +1,332 @@ +import { useId, useState } from "react"; +import { + ArrowRight, + Check, + ChevronDown, + Minus, + Plus, + Search, +} from "lucide-react"; +import { + Autocomplete, + Button, + Calendar, + Checkbox, + CheckboxGroup, + Combobox, + Command, + Field, + Fieldset, + Form, + IconButton, + Input, + InputGroup, + NumberField, + OTPField, + Radio, + RadioGroup, + Slider, + Switch, + Textarea, +} from "@buzz/ui"; + +export function ButtonsDemo() { + const [count, setCount] = useState(0); + return ( +
+
+ + + + + + +
+
+ + + + + setCount(count + 1)} + > + +
+

+ {count ? `${count} actions taken` : "Try Continue or Add item"} +

+
+ ); +} +export function InputDemo() { + return ( + + Project name + + Visible to everyone in the project. + + ); +} +export function TextareaDemo() { + return ( + + Project description +