BridgeHandbookDraft
For agents
Menu
Stable

Repository

Where the code lives, how it follows shadcn's monorepo setup, how a component file is built and the commands you run.

This page is for changing the design system itself. To use components in an app, see Using the system.

The monorepo

One npm workspace on GitLab (bridge-design-system) holds the package and this handbook. It follows shadcn’s monorepo setup: apps in apps/, the shared components in packages/ui. The handbook imports the package directly, so every example on this site is the real component.

Repository root

bridge-design-system/
  apps/
    handbook/             this site: the docs, /llms.txt and the /mcp server
  packages/
    ui/                   @brdgapp/ui: components, hooks, lib, globals.css
  DESIGN.md               the frozen visual spec
  PRODUCT.md              who Bridge is for
  .gitlab-ci.yml          checks on every push; publishes to npm on a version tag
  eslint.config.mjs       lint rules for every workspace
  package.json            workspaces and root commands

Workspace packages, read from each package.json

PackagePathRole
@brdgapp/uipackages/uiBridge's shared components, tokens and styles. Documented at apps-bridge-design-system-handbook.vercel.app.
@bridge/handbookapps/handbookThe Bridge handbook (apps-bridge-design-system-handbook.vercel.app): human docs, llms.txt and the read-only MCP.

Bridge apps live in their own repos (Bridge OS is bridge-os/app) and install @brdgapp/ui like any other package. Nothing in packages/ui imports from an app.

Built on shadcn

Every component follows shadcn’s structure: Base UI underneath, the same file shape, a name on every part. Most were brought over from Bridge OS and rebuilt that way; Button started from shadcn itself. A brand-new component starts from shadcn’s Base UI version and is then rebuilt on Bridge’s tokens. shadcn reads components.json to know where files go and what to import, so the package carries one.

packages/ui/components.json

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "base-nova",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/styles/globals.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "iconLibrary": "lucide",
  "aliases": {
    "components": "@brdgapp/ui/components",
    "ui": "@brdgapp/ui/components",
    "lib": "@brdgapp/ui/lib",
    "hooks": "@brdgapp/ui/hooks",
    "utils": "@brdgapp/ui/lib/utils"
  }
}
FieldWhat it means for us
style: base-novaComponents come in their Base UI version, not Radix.
tailwind.cssNew styles go into the one shared src/styles/globals.css. config is empty because Tailwind 4 needs no config file.
iconLibrary: lucideIcons are Lucide, the same as everywhere in Bridge.
aliasesPoint at the package’s public paths, so a pulled file imports @brdgapp/ui/lib/utils and @brdgapp/ui/components/button, never a relative path.

In shadcn’s setup each app has its own components.json too, so running shadcn add in an app lands the file in packages/ui. This repo’s only app is the handbook, which never owns components, so components are pulled from the package itself with npm run ui-add.

Never run shadcn add inside an app such as Bridge OS. Apps only use the package; a new component is added to packages/ui, gets its handbook page, and then ships in a new version.

Inside packages/ui

The package’s exports map turns each folder into an import path. Counts are read from the files on every build.

FolderFilesHoldsImport as
components37Atoms, molecules and organisms, one component family per file.@brdgapp/ui/components/<file>
hooks2Behavior without markup: useMockQuery for loading, empty and error states.@brdgapp/ui/hooks/<file>
lib5cn, the number and date formatters, the demo controls.@brdgapp/ui/lib/<file>
styles/globals.css1Every token, custom utility and base rule, 575 lines.@brdgapp/ui/globals.css

Some files hold a family (data-table.tsx has DataTable, its skeleton and its pager, and info-hint.tsx holds three tooltips). Its page covers every part the file exports.

Anatomy of a component file

Every file follows the same shape. Not every file needs all seven steps: Badge, the smallest, shows four of them (cva, data-slot, className last, named exports), and Button shows the other three.

  1. Client directive. "use client" only when the file holds state, effects or event handlers. Button has it; Badge doesn’t.
  2. Base UI primitive. Interactive parts wrap a primitive from @base-ui/react/<part>, which brings roles, focus and keyboard behavior. Badge is a plain span, so it has none.
  3. cva variants. cva(base, { variants, defaultVariants }) holds the classes, using tokens only. The variants are exported so another file can borrow the look.
  4. data-slot. Each rendered part names itself, such as data-slot="badge". Parents style children through it, and the handbook’s anatomy drawings find parts by it.
  5. render prop. useRender with mergeProps lets a component render as another element, such as a Next Link, keeping its classes and props. A link stays a link.
  6. className last. cn(variants({ … }), className) merges the caller’s classes after the component’s, so an override always wins.
  7. Named exports at the bottom. One export { Badge, badgeVariants } block closes the file, with types in their own export type line. No default exports.

badge.tsx in full

Read from packages/ui/src/components/badge.tsx when this page builds.

import type * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@brdgapp/ui/lib/utils";

// The "Solid label" sticker from the Arch Design System: 8px, bold, uppercase. Ported from Bridge OS
// src/components/ui/badge.tsx. Rounded 4px, never a pill. For soft status and category labels use Tag.
// Two fixes over Bridge OS: solid-red fills with --danger-solid (white on --error fails AA at 4.41:1), and
// solid-dark's text uses --surface-primary so it stays readable in dark mode (white on light grey didn't).
const badgeVariants = cva(
  "inline-flex items-center rounded-[4px] px-[7px] py-1 text-[8px] leading-[8px] font-bold tracking-[1px] uppercase",
  {
    variants: {
      // Named `style` to match Bridge OS's API. It is not React's style prop: Badge takes no CSS styles.
      style: {
        outline: "border border-[var(--text-disabled)] text-[var(--text-muted)]",
        alum: "border border-[var(--honey)] text-[var(--honey)]",
        "solid-red": "bg-[var(--danger-solid)] text-white",
        "solid-dark": "bg-[var(--text-secondary)] text-[var(--surface-primary)]",
      },
    },
    defaultVariants: { style: "outline" },
  },
);

type BadgeProps = VariantProps<typeof badgeVariants> & {
  className?: string;
  children: React.ReactNode;
};

function Badge({ style, className, children }: BadgeProps) {
  return (
    <span data-slot="badge" className={cn(badgeVariants({ style }), className)}>
      {children}
    </span>
  );
}

export { Badge, badgeVariants };
export type { BadgeProps };

The render prop in button.tsx

A native button keeps Base UI’s button behavior. Given a render element, such as a link, Button renders that element instead.

  // With `render` (for example a Next `Link`), the element keeps its own semantics: a link stays a
  // link. It owns its content, so `icon` and `loading` don't apply.
  const rendered = useRender({
    render: render as useRender.RenderProp,
    enabled: render !== undefined,
    props: mergeProps<"a">({ className: classes, "data-slot": "button" } as React.ComponentProps<"a">, props as React.ComponentProps<"a">, { children }),
  });
  if (rendered) return rendered;

Conventions

The small agreements that let files work together without reading each other.

ConventionRule
data-slotEvery rendered part sets one, in kebab case, starting with its component (button-icon, stat-box-value). 104 distinct names exist today.
File nameskebab-case, one component family per file, named after the component: copy-field.tsx holds CopyField.
SizesButtons are xs 28px, sm 32px (the default), md 40px and lg 48px. Icon buttons are sm 28px, md 32px (the default) and lg 36px. The names don’t match between the two, so pick by the height, not the name. Nothing clickable is smaller than 28px.
type="button"Base UI’s Button sets it. A hand-written <button> that doesn’t submit a form must say so.
IconsFrom lucide-react, 14, 16, 18 or 24px, aria-hidden="true" beside text. An icon on its own needs an aria-label on its button.
Extra attributesComponents pass id, aria-*, data-* and handlers to their main element through ElementProps from @brdgapp/ui/lib/utils.
ExportsNamed exports in one block at the end of the file. No file has a default export.
Imports between filesUse the public path, @brdgapp/ui/components/<file>, even inside the package. StatBox imports InfoHint that way.
cnAlways from @brdgapp/ui/lib/utils, never clsx or the bare cn package, so Bridge’s own utilities merge correctly.
TimingColor changes 150ms ease-out; switches and fades 200ms; the drawer slides in 300ms. globals.css turns motion off for people who ask for reduced motion.

The docs site

The handbook is the Next.js app in apps/handbook. Its pages render real packages/ui code.

apps/handbook

src/
  app/
    page.tsx                 the overview
    [...slug]/page.tsx       every other page, driven by the registry
    llms.txt, llms-full.txt  the index and every component's guidance, for agents
    mcp/route.ts             the read-only MCP server
  components/design/
    registry/   pages.ts (every page), rules.ts (named rules), recipe.ts,
                loaders.ts and demo-data-pages.ts (generated), adoption.ts
    content/    one folder per page: doc.tsx, demos.tsx, guidance.ts or guide.ts
    kit/        page building blocks: sections, examples, anatomy, states
    site/       sidebar, header, breadcrumbs, on this page
    agent/      the MCP tools, the code checker, the llms.txt text
    sample/     network.ts, the sample network every example uses
scripts/        design-sync, adoption, and the token, checker and MCP tests

Docs site counts

WhatCount
Pages in the registry96
Page bodies (doc.tsx)96
Demo files (demos.tsx)74
Component guidance files (guidance.ts)37
Pattern guides (guide.ts)19

Add a page by adding an entry to registry/pages.ts, then run npm run design:sync. It regenerates loaders.ts and demo-data-pages.ts and writes a stub doc.tsx, so a new entry never breaks the build.

Tooling

Run everything from the repository root. The Runs column is read from the package.json files.

CommandRunsUse it to
npm run devnext dev --port 3100Start the handbook at localhost:3100.
npm run buildnpm run design:check && npm run test && next buildCheck the registry, run the token and checker tests, then build the handbook. GitLab runs this on every push.
npm run check-typesnpm run check-types --workspaces --if-presentType-check every workspace.
npm run linteslint .Check the whole repo with ESLint.
npm run design:syncnode scripts/design-sync.tsRegenerate the generated registry files and write stubs after editing the registry.
npm run ui-add -- <name>node scripts/ui-add.mjsPull a shadcn component (Base UI version) into packages/ui and point its cn import at @brdgapp/ui/lib/utils.
npm run build:uinode scripts/build.mjsBuild the package into packages/ui/dist, the folder that goes to npm.
npm run design:adoption -w @bridge/handbook -- --app <name> --repo <path>node scripts/adoption.tsCount how much of an app’s main branch uses the package.
git tag v1.2.0 && git push origin v1.2.0GitLab CIRelease: GitLab runs every check, then publishes that version to npm.
  • A pulled component arrives in shadcn’s style. Before anyone uses it, rebuild it on Bridge tokens, add data-slot names and write its page (Component lifecycle).
  • Never edit loaders.ts or demo-data-pages.ts by hand; design:sync owns them and the build fails if they drift.