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/*.
| Layer | Directory | What lives there |
|---|---|---|
| Generated | src/shadcn/ | Raw CLI output. Do not hand-edit; a regeneration overwrites it. |
| Public | src/components/ | The exported API. Mostly re-exports; a real wrapper only where we add behaviour. |
| Composed | src/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-themeon<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 asshared-components/stylesand do not redefine it. asChilddoes not exist on Base UI. The equivalent isrender, which takes an element:<DropdownMenuItem render={<Link href="…" />}>. Note thatButtonwithrenderstampsrole="button"on whatever it renders, so a link styled as a button usesButtonLink, notButton.GroupLabelmust sit inside aGroup. 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