Skip to main content

Component Architecture & Workflow

How React components are built in packages/shared-components, and what the console is assembled from.

Technology Stack

  • UI Framework: React 19 with TypeScript
  • Primitives: shadcn/ui on Base UI, style base-nova
  • Styling: Tailwind CSS v4, configured entirely in CSS
  • Component development: Storybook 10 with Vite
  • Testing: Jest with React Testing Library
  • Tables: TanStack Table v9. Charts: Recharts. Command palette: cmdk

The three layers

Applications import from shared-components and never from @shadcn/*.

LayerDirectoryWhat lives there
Generatedsrc/shadcn/Raw CLI output. Do not hand-edit; a regeneration overwrites it.
Publicsrc/components/The exported API. Mostly re-exports; a real wrapper only where we add behaviour.
Composedsrc/composites/, src/layouts/Page-level pieces: DataTable, PageHeader, EmptyState, the three shells.

Everything is re-exported from src/index.ts, which carries "use client". The primitives use React context and the generated files do not all declare their own directive, so a server component importing any single export would otherwise pull a context call into the server graph and fail at build time.

That directive is also why the barrel is not the only entry point. A client boundary ships as one unit: every module the barrel re-exports reaches the browser on any page importing anything from it. Recharts is a third of the console's JavaScript, so the chart-backed exports sit behind shared-components/charts (src/charts.ts). A new dependency of that weight belongs in its own entry point too.

Adding a component

1. Generate it

cd packages/shared-components
yarn dlx shadcn@latest add [component-name]

The style and aliases come from components.json; the file lands in src/shadcn/ui/[component-name].tsx.

2. Expose it

Most components need nothing but a re-export in src/components/[name].tsx:

export { Card, CardContent, CardHeader, CardTitle } from "@shadcn/ui/card";

Write an actual wrapper only when the public API differs from the generated one — a prop we add, a variant we restrict, a default we set. button.tsx is the reference: it adds loading, and adds ButtonLink for a link that looks like a button.

3. Story and spec

A story per component (title: "Primitives/<Name>", tags: ["autodocs"]) with one story per variant and state, and a spec that asserts roles and states, not class names — a token change must not fail a test that had nothing to say about colour.

4. Export it

Add the module to src/index.ts — or to src/charts.ts when it pulls in a heavy dependency the whole console would otherwise carry.

Design decisions worth knowing

  • Theme is data-theme on <html>, dark by default, set before paint by a script in the web app's root layout. An attribute cannot collide with a utility class, and popups rendered through a portal inherit it.
  • Tokens live in one file, src/styles/globals.css. Apps import it as shared-components/styles and do not redefine it.
  • asChild does not exist on Base UI. The equivalent is render, which takes an element: <DropdownMenuItem render={<Link href="…" />}>. Note that Button with render stamps role="button" on whatever it renders, so a link styled as a button uses ButtonLink, not Button.
  • GroupLabel must sit inside a Group. Base UI throws otherwise, and the error is a numbered code rather than a sentence.

Commands

nx storybook shared-components # develop
nx build-storybook shared-components # published with the docs site
nx test shared-components # jest
nx lint shared-components