@axair-systems/ui
v0.2.0
Published
Axair Systems design system — tokens and components
Readme
@axair-systems/ui
Design system for Axair Systems — brand tokens and React components shared across all products (ADR #26).
Published to public npm — install with a plain npm install, no authentication or token required. Component baseline is shadcn/ui (ADR D2); tokens are the single source of truth.
Contents
| File / export | Purpose |
|---|---|
| tokens.css | :root CSS variables — primitives and semantic tokens. Framework-agnostic. |
| theme.css | tokens.css + @theme inline block that wires Tailwind v4 utilities to the tokens. |
| tailwind-preset.js | Tailwind v3 preset (legacy; use theme.css for v4). |
| Package main (dist/) | React components: Button, Card, Input, Label, Badge. |
Installation
The package lives on the public npm registry. No .npmrc, token, or PAT is needed — install it like any other dependency:
npm install @axair-systems/uiPeer dependencies
Install these in the consuming app if they aren't already present:
npm install react react-dom # required
npm install tailwindcss # required only if you use the components / Tailwind utilitiesreact/react-dom(^18or^19) are required by the components.tailwindcss(^3.4or^4) is an optional peer — needed to render the components and their utility classes, but not for token-only (tokens.css) consumption. npm won't warn if you omit it.
CI / other repos consuming this package
Because the package is public, no auth step is required. A plain install works in any CI runner:
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ciUsage
Tailwind v4 (Next.js, Vite, etc.)
1 — Import the theme in your global CSS:
/* app/globals.css (or src/index.css) */
@import "tailwindcss";
@import "@axair-systems/ui/theme.css";That's the only config step for Tailwind v4. theme.css loads all token variables and wires every Tailwind utility (bg-primary, text-muted-foreground, rounded-lg, …) to them via @theme inline.
2 — Load Montserrat with next/font (Next.js):
// app/layout.tsx
import { Montserrat, IBM_Plex_Mono } from 'next/font/google';
const montserrat = Montserrat({
subsets: ['latin'],
variable: '--font-sans',
display: 'swap',
});
const ibmMono = IBM_Plex_Mono({
subsets: ['latin'],
weight: ['400', '500', '600'],
variable: '--font-mono',
display: 'swap',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${montserrat.variable} ${ibmMono.variable}`}>
<body className="bg-background text-foreground font-sans">{children}</body>
</html>
);
}The variable option injects --font-sans / --font-mono into the HTML element, overriding the Google Fonts fallback already in tokens.css.
3 — Use components:
import { Button, Badge, Card, CardHeader, CardTitle, CardContent, Input, Label } from '@axair-systems/ui';
export default function Page() {
return (
<Card>
<CardHeader>
<Badge variant="accent">Active</Badge>
<CardTitle>Mission Status</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-1.5">
<Label htmlFor="callsign">Callsign</Label>
<Input id="callsign" placeholder="AX-0042" />
</div>
<Button>Deploy</Button>
</CardContent>
</Card>
);
}Tailwind v3 (legacy consumers)
Use the preset instead of theme.css:
// tailwind.config.js
const preset = require('@axair-systems/ui/tailwind-preset');
module.exports = {
presets: [preset],
content: ['./src/**/*.{ts,tsx}'],
};Import only tokens.css in your global CSS (not theme.css):
@import '@axair-systems/ui/tokens.css';
@tailwind base;
@tailwind components;
@tailwind utilities;Token-only (no Tailwind)
@import '@axair-systems/ui/tokens.css';All semantic tokens (--primary, --background, etc.) are then available as CSS variables anywhere in your stylesheet.
Components
All components accept a className prop for one-off overrides via Tailwind classes.
Button
import { Button } from '@axair-systems/ui';
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="accent">Accent</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon">…</Button>
<Button asChild><a href="/dashboard">Dashboard</a></Button>Badge
import { Badge } from '@axair-systems/ui';
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="accent">Accent</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="muted">Muted</Badge>
<Badge variant="destructive">Destructive</Badge>Card
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@axair-systems/ui';
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Supporting text</CardDescription>
</CardHeader>
<CardContent>…</CardContent>
<CardFooter>
<Button size="sm">Action</Button>
</CardFooter>
</Card>Input + Label
import { Input, Label } from '@axair-systems/ui';
<div className="space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="[email protected]" />
</div>Token reference
The rule: no raw hex in components
Components reference semantic tokens only. The two-tier system means changing a primitive updates every component that uses it.
Brand primitives (--ax-*) → Semantic tokens (--primary, --background, …) → ComponentsWrong:
<div className="bg-[#153d5d]" style={{ color: '#fff7ea' }}>Correct:
<div className="bg-primary text-primary-foreground">Audit for violations:
grep -rn '#[0-9a-fA-F]\{3,6\}' src app componentsSemantic tokens quick reference
| Token | Default value | Tailwind |
|---|---|---|
| --background | #f3e6cf (sand) | bg-background |
| --foreground | #3b3b38 (dark) | text-foreground |
| --primary | #153d5d (navy) | bg-primary |
| --primary-foreground | #fff7ea (cream) | text-primary-foreground |
| --secondary | #c4d4e0 (blue) | bg-secondary |
| --muted | #b9a58a (grey) | bg-muted |
| --muted-foreground | #878787 | text-muted-foreground |
| --accent | #b0812c (gold) | bg-accent |
| --destructive | #b3261e | bg-destructive |
| --border | #b9a58a | border-border |
| --ring | #153d5d | (focus ring) |
| --font-sans | "Montserrat", … | font-sans |
| --font-mono | "IBM Plex Mono", … | font-mono |
| --radius | 0.5rem | rounded-lg / rounded-md / rounded-sm |
Full table and instructions for adding new tokens: see USAGE.md.
Demo site
A local Vite app that shows all tokens, the type scale, every component variant, and a sample Fleet Overview screen.
# From the repo root — builds the package then starts the demo
npm run demoOpens at http://localhost:5174 (or next available port).
To run the demo independently after an initial npm run demo:
cd demo
npm run devDevelopment
# Install dependencies
npm install
# Build the package (outputs to dist/)
npm run build
# Watch mode during development
npm run dev
# Type check without building
npm run typecheckPublishing
The package is published to the public npm registry automatically when a version tag is pushed.
# 1. Bump the version (updates package.json and creates a git tag)
npm version patch # 0.1.0 → 0.1.1
npm version minor # 0.1.0 → 0.2.0
npm version major # 0.1.0 → 1.0.0
# 2. Push the commit and tag — the GitHub Action does the rest
git push --follow-tagsThe publish.yml workflow runs typecheck → build → npm publish using the repository's NPM_TOKEN secret (a publish-only npm automation token). Do not publish manually unless asked.
Repo layout
design-system/
├── src/
│ ├── components/ui/ # Button, Card, Input, Label, Badge
│ ├── lib/utils.ts # cn() helper (clsx + tailwind-merge)
│ └── index.ts # public exports
├── demo/ # Vite + React demo site
│ └── src/App.tsx # Colors / Typography / Components / Sample Screen
├── tokens.css # :root brand primitives + semantic tokens
├── theme.css # tokens.css + @theme inline (Tailwind v4)
├── tailwind-preset.js # Tailwind v3 preset
├── tsup.config.ts # Library build config
├── USAGE.md # Token table, rules, adding tokens
└── .github/workflows/
└── publish.yml # public npm publish on v* tag