Shared React component library for CellarNode dashboards. Ships UI primitives, design tokens, chat components, offer components, a globe visualisation, and a Kanban drag-and-drop system — all typed, tree-shakeable, and tested via Storybook.
Contents
- Architecture
- Install
- Peer Dependencies
- Exports
- Theme & Design Tokens
- Development
- Writing Stories
- Make Commands
- Storybook MCP (AI Integration)
- Publishing
Architecture
src/
├── components/ UI primitives (Button, Card, Dialog, Table, Sidebar…)
├── chat/ Chat & negotiation UI (ChatShell, ChatMessageList, ConnectionBanner)
├── offer/ Offer workflow components (BeverageDetails, OrgBanner…)
├── globe/ Interactive 3-D globe visualisation (react-globe.gl)
├── hooks/ Shared hooks (useIsMobile, useToast, useKanbanDnd)
├── lib/ Utilities (cn, semantic tones, currencies, kanban transitions)
└── theme/
└── tokens.css Canonical Tailwind v4 design tokens
Primitive layers
| Layer | Package | Used for |
|---|---|---|
| Radix UI | @radix-ui/* | Dialog, Dropdown, Select, Tabs, Toast… |
| Headless UI | @headlessui/react | Dropdown (Catalyst), Fieldset, Combobox |
| shadcn/ui | built on Radix | Chat & offer internal variants |
Install
pnpm add @cellarnode/ui
Note: This package is public on npmjs.org (
publishConfig.access: "public", CEL-1983); installs need no registry auth. The repo.npmrcstill routes the@cellarnodescope to registry.npmjs.org and reads${NPM_TOKEN}if one is set.
Peer Dependencies
Install the peers your app uses:
# Always required
pnpm add react react-dom
# Optional — only needed when importing the specific sub-path
pnpm add motion # animations
pnpm add @dnd-kit/react @dnd-kit/dom # ./kanban-dnd
pnpm add @tanstack/react-query # chat hooks
pnpm add react-globe.gl three topojson-client world-atlas # ./globe
pnpm add @cellarnode/beverage-utils i18n-iso-countries # ./offer
Exports
| Import path | Contents |
|---|---|
@cellarnode/ui | All UI primitives, hooks, and utilities |
@cellarnode/ui/theme/tokens.css | Tailwind v4 design token CSS |
@cellarnode/ui/chat | ChatShell, ChatMessageList, ConnectionBanner, MessengerLayout, chat hooks |
@cellarnode/ui/offer | BeverageDetails, OrgBanner, OfferStatusBadge… |
@cellarnode/ui/kanban-dnd | KanbanDndBoard component |
@cellarnode/ui/kanban-dnd-hooks | useDraggableCard, useDroppableColumn |
@cellarnode/ui/globe | GlobeView component |
@cellarnode/ui/animated-list | AnimatedList component |
Theme & Design Tokens
Import the canonical token sheet before Tailwind in your app's root CSS:
@import "@cellarnode/ui/theme/tokens.css";
@import "tailwindcss";
Tokens expose semantic CSS variables (--color-background, --color-primary, --color-brand-secondary, etc.) that are automatically mapped into Tailwind utility classes.
Light/dark mode is toggled by adding the dark class to <html>.
Development
Prerequisites: Node 20+, pnpm 9+, Playwright Chromium (for browser tests).
# Install dependencies
pnpm install
# Install Playwright browser
pnpm playwright install chromium
# Start Storybook dev server
make storybook # → http://localhost:6006
# Type-check
make typecheck
# Lint
make lint
# Run tests (Storybook browser tests, headless)
make test
# Full build
make build
Writing Stories
Every component must have a .stories.tsx file co-located next to it.
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "./button";
const meta = {
title: "Components/Button",
component: Button,
tags: ["autodocs"],
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { children: "Click me" },
};
export const Variants: Story = {
render: () => (
<div className="flex gap-3">
<Button color="brand">Brand</Button>
<Button outline>Outline</Button>
<Button plain>Plain</Button>
</div>
),
};
Conventions
tags: ["autodocs"]→ auto-generated prop docs- Include a
Defaultstory + at least one variant story - Use
argTypesfor enum-like props (color, size, variant) - Check the A11y addon panel — fix all violations before merging
- Toggle light / dark via the Themes toolbar
- Write
playfunctions for interaction testing when behaviour is complex
Make Commands
make build Full build: lint + typecheck + compile
make clean Remove dist/
make compile Compile TypeScript to dist/
make lint Run ESLint
make typecheck TypeScript type checking (no emit)
make test Run Storybook/Vitest browser tests (headless Chromium)
make test-watch Run Vitest in watch mode
make storybook Start Storybook dev server on :6006
make build-storybook Build static Storybook to storybook-static/
make publish Manual fallback ONLY — build and publish current version to npm (not the normal path)
make release-patch Bump patch version, commit, and git tag (publishes after main's CI is green)
make release-minor Bump minor version, commit, and git tag (publishes after main's CI is green)
make release-major Bump major version, commit, and git tag (publishes after main's CI is green)
Storybook MCP (AI Integration)
When Storybook is running (make storybook), the @storybook/addon-mcp addon exposes an MCP server at http://localhost:6006/mcp.
Register it with Claude Code once:
claude mcp add storybook-mcp --transport http http://localhost:6006/mcp --scope project
Available MCP tools:
| Tool | Description |
|---|---|
get_ui_building_instructions | Guidelines for creating components and stories |
get_story_urls | Direct browser links to every story |
Component manifest (for AI context): http://localhost:6006/manifests/components.html
Publishing
Packages are published to npm (public access — publishConfig.access: "public", see CEL-1983) via npm Trusted Publishing (OIDC) —
.github/workflows/publish.yml publishes automatically once the push-to-main CI run for the tip of main
is green AND package.json's version is not on npm yet. Nobody runs npm publish by hand; there is no
long-lived npm token in the publish job. Releases are cut locally via Make, then landed through a normal PR:
make release-patch # 0.2.1 → 0.2.2
make release-minor # 0.2.1 → 0.3.0
make release-major # 0.2.1 → 1.0.0
Each release target: bumps version in package.json, rolls CHANGELOG.md, builds, commits, and
creates a git tag ui-vX.Y.Z — it does not publish. Push the branch, open a PR, and merge it.
Publishing then follows the green CI run on main's tip. make publish remains as an explicit manual
fallback (needs a local npm login with publish rights); it is not the normal path.
How the gated publish behaves (CEL-2530):
- Wait for CI.
Publish to npmstarts when theCIworkflow of a push tomaincompletes with success, so expect the CI wall clock (1.5-8 h on the shared runner pool) before the package appears. Watch the Publish to npm run, not the merge. - Merge the release PR LAST and pause other merges to
mainuntil the publish appears: only the tip ofmainpublishes, so a later merge makes the older commit's run skip ("main has moved on") and the newer commit's own green CI decides. If CI on the tip is red or cancelled, nothing is published: fix forward or re-run CI on the tip. [skip ci]tips never publish (no CI run, nothing to wait for).gitHeadin the published package may not equal theui-vX.Y.Ztag commit when a later green tip published the version.- Recovery. Re-run the failed CI job on
main's tip (the publish follows the new green result), or use the escape hatch: Actions > Publish to npm > Run workflow frommain, typing the exactpackage.jsonversion inconfirm. The real controls on that run: it must start frommain, the person who starts it must be on the actor allowlist inpublish.yml(a JSON list in the jobif; add a maintainer in a reviewed PR), the typedconfirmmust match, the commit must still be the tip ofmain(otherwise the run fails and you start it again), and the same "version not on npm yet" check applies. There is no environment or required-reviewers gate (not available on a private repo of this plan); keep npm's trusted-publisher "Environment" field blank.