Skip to content

rogueoak/canopy

Repository files navigation

Canopy β€” the rogueoak design system

An earthy, tree-themed design system for rogueoak β€” built on Radix Β· shadcn Β· Tailwind v4 Β· TypeScript.

CI Storybook License: MIT

TypeScript Tailwind v4 Radix UI Style Dictionary


Note

🚧 Status: early development. Canopy is being built in the open, foundation-first. The first packages are published to npm under the @rogueoak scope; APIs below marked (planned) don't exist yet β€” they describe where we're headed. Follow the roadmap for what's live.

What is Canopy?

Canopy is the design system that defines the look, feel, and building blocks of rogueoak β€” its products and its website. It ships as consumable npm packages so any rogueoak app can build interfaces from the same earthy, considered foundation.

The whole system is organised like a tree, foundation β†’ composite, and every layer is named for a part of one. rogueoak is the forest, canopy is the system, and the layers below grow from shared roots.

The Canopy model

Atomic design, renamed by tree anatomy:

Atomic layer Canopy name What lives here
Design tokens Roots 🌱 primitive + semantic tokens β€” colour, type, spacing, radii, elevation, motion. Everything draws nourishment from here.
Atoms Seeds the smallest components β€” Button, Input, Label, Badge (icons ship separately, see below)
Molecules Twigs small compositions β€” FormField, SearchBar, Card
Organisms Branches larger assemblies β€” NavBar, DataTable, Dialog
Templates (later) Boughs page scaffolds and layout patterns
The whole system Canopy the published library + the Storybook showcase

Components only ever consume Roots semantic tokens (color-surface, text-primary, radius-control) β€” never raw palette values. Light and dark are a property of the token layer: semantic tokens remap per theme, so a component is themed without knowing it.

Icons are atom-tier too, but ship as their own package β€” @rogueoak/icons, a curated, tree-shakeable set re-exported from react-icons (Lucide glyphs + the popular social marks). They sit apart from @rogueoak/canopy because their dependency footprint (react-icons, no tokens) and release cadence differ; they render in currentColor, so they theme through whatever text colour they inherit.

Tokens & theming

Roots is a token source of truth, not hand-written CSS. Tokens are authored once as DTCG JSON and compiled β€” via Style Dictionary β€” into the outputs each consumer needs:

  • CSS custom properties (tokens.css) for runtime theming
  • A typed TypeScript export (tokens) for programmatic access
  • A Tailwind v4 @theme preset (tailwind-preset.css) so utilities map straight onto tokens

The system is two-tier, so theming is a remap of one layer and never touches components:

  • Primitive ramps β€” the raw palette, 50…950: moss (brand), bark, stone (neutrals), amber (accent), and desaturated functional ramps success / warning / danger / info. Muted & natural; moss/olive brand. Primitives are never used by components directly.
  • Semantic tokens β€” theme roles that reference primitives: surfaces (color-bg, color-surface, color-muted), text (color-text, color-text-muted, color-text-subtle), lines (color-border, color-ring), roles (color-primary + -foreground, secondary, accent), interaction states (-hover/-active, plus a color-disabled + -foreground convention) and status (success/warning/danger/info). Components consume only these. Each role has a light and a dark value β€” see Theming.

Alongside colour: typography (Figtree sans + Geist Mono, type scale text-xs…6xl, weights, leading, tracking), spacing (4px base), radii, elevation (shadow-*), and motion (durations + easings). Token names flatten onto Tailwind v4 @theme namespaces so utilities generate directly: color-*β†’bg-*/text-*, radius-mdβ†’rounded-md, text-lgβ†’text-lg, font-sansβ†’font-sans, shadow-mdβ†’shadow-md, and spacing utilities (p-4, gap-2) derive from a single --spacing base.

Fonts are self-hosted (no CDN). Roots ships the family names; consumers install the open-licensed @fontsource packages and import them once:

pnpm add @fontsource-variable/figtree @fontsource-variable/geist-mono
/* in your global stylesheet, alongside the Roots imports */
@import '@fontsource-variable/figtree';
@import '@fontsource-variable/geist-mono';
@import '@rogueoak/roots/tokens.css';
@import '@rogueoak/roots/tailwind-preset.css';

This pipeline is deliberately built to grow: a native (Swift) target can be added later as just another output platform, without rewriting a single token. (native target: planned)

Theming (light & dark)

The theme is a property of the token layer, not your components. tokens.css declares the light theme on :root and a .dark block that re-points only the semantic vars at different primitive ramp steps (primitives are the shared, theme-agnostic palette and are not repeated). Because every utility (bg-primary, text-default, …) and var(--color-*) read resolves through those runtime vars, toggling a single class re-themes the whole UI with zero per-component code. Light is the default; add dark to a root element to flip:

// the one-line mechanism β€” toggle the class on <html> (or any container)
document.documentElement.classList.toggle('dark');

Optional β€” respect the OS preference on first paint (before your app hydrates):

<script>
  // bootstrap: honour a saved choice, else the OS setting
  const saved = localStorage.getItem('theme');
  const dark = saved ? saved === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
  document.documentElement.classList.toggle('dark', dark);
</script>

The common path needs no dark: utilities β€” semantic tokens auto-flip. For the rare explicit case, add Tailwind's dark variant once in your global CSS so dark: utilities work:

@custom-variant dark (&:where(.dark, .dark *));

Every semantic role meets WCAG AA in both themes β€” a build-time test computes the real contrast ratios for light and dark and fails the build on any regression. Interaction-state roles (color-primary-hover/-active, secondary, accent, danger-hover) and a color-disabled surface + color-disabled-foreground convention are defined with light and dark values too, ready for the first components.

Distribution

Canopy publishes under the @rogueoak npm scope as a small set of versioned packages:

Package Holds Status
@rogueoak/roots design tokens + Tailwind preset published
@rogueoak/canopy components (/seeds, /twigs, /branches) published
@rogueoak/icons curated icon set (re-exported from react-icons) published

Releases are tag-driven: pushing a bare-SemVer tag (X.Y.Z, no v prefix) publishes all three packages at that version via GitHub Actions (.github/workflows/release.yml, npm trusted publishing / OIDC).

Quick start

pnpm add @rogueoak/canopy @rogueoak/roots
import { Button } from '@rogueoak/canopy/seeds';

export function Example() {
  return <Button>Plant a seed</Button>;
}

Wiring the styles (the Tailwind-source seam)

Canopy ships className strings (Tailwind v4 utilities), not a prebuilt stylesheet β€” so your build generates and tree-shakes only the utilities you actually use, and your .dark flips canopy too. You wire this once in your global CSS: import Tailwind and the Roots preset, then add @source pointing at @rogueoak/canopy so Tailwind scans canopy's component source and emits its utilities into your build:

@import 'tailwindcss';
@import '@rogueoak/roots/tailwind-preset.css';

/* Generate canopy's component utilities by scanning its shipped code. Without this,
   canopy components render UNSTYLED β€” the utilities never get emitted. `@source` takes a
   PATH (Tailwind v4 has no bare-package resolution), RELATIVE TO THIS CSS FILE β€” adjust the
   `../` depth so it resolves to canopy in your node_modules. */
@source '../node_modules/@rogueoak/canopy';

(Add the @rogueoak/roots/tokens.css and @fontsource imports too β€” see Tokens & theming.) This is exactly how the Storybook app β€” Canopy's first consumer β€” is wired (it points @source at canopy's source by relative path). A prebuilt-CSS bundle for non-Tailwind consumers may come later.

Storybook

The component showcase β€” swatches, type specimens, and every component in light and dark β€” lives on GitHub Pages, built from Storybook and deployed by CI on every push to main: https://rogueoak.github.io/canopy/. The Foundations section is the living spec β€” colour ramps + semantic swatches, the Figtree type specimen and scale, spacing, radii, elevation, motion, a WCAG AA contrast table, and a Theme demo. Use the toolbar Light / Dark toggle β€” every story reads correctly in both themes (it flips .dark).

Development

Canopy is a pnpm + Turborepo monorepo. Requires Node 20+ and pnpm 11+ (npm install -g pnpm). The workflow:

pnpm install      # install the workspace
pnpm build        # build tokens (Style Dictionary), components (tsup), and Storybook
pnpm storybook    # run the showcase locally at http://localhost:6006
pnpm test         # run the test suite (Vitest)
pnpm lint         # lint the workspace (ESLint + Prettier)

Layout:

Path Package What it is
packages/roots @rogueoak/roots design tokens β†’ CSS vars, typed TS export, Tailwind v4 preset (Style Dictionary)
packages/canopy @rogueoak/canopy components, built to ESM + types (tsup)
apps/storybook private the Storybook showcase, deployed to GitHub Pages

Roots ships the real foundation plus light & dark theming: primitive ramps + semantic tokens (light + dark, with interaction states), type, spacing, radii, elevation, and motion. The full Seeds atom catalogue is live, built on the shared component recipe (cn(), cva variants over semantic tokens, Radix Slot for asChild). The first Twigs (molecules) are live too β€” FormField, SearchBar, and Card β€” composing those atoms on the @rogueoak/canopy/twigs subpath. The Branches (organisms) layer is filling in on the new @rogueoak/canopy/branches subpath β€” Dialog (a focus-trapping, portalled modal), TopNav (a responsive, hand-rolled-disclosure navigation bar), and SideNav (a collapsible side rail whose mobile drawer reuses the Radix Dialog primitive) β€” each composing Seeds and Twigs.

Roadmap

Built foundation-first, so there's always working software and working docs at each step:

  • Roots β€” tokens: palette, typography, spacing, radii, elevation, motion; light & dark theming
  • Seeds β€” the atoms; the full first catalogue is live
  • Twigs β€” molecules; the first compositions are live (FormField Β· SearchBar Β· Card)
  • Branches β€” organisms; the layer is open (Dialog Β· TopNav Β· SideNav are live; DataTable to come)
  • Icons β€” @rogueoak/icons, a curated tree-shakeable set (Lucide + social marks) re-exported from react-icons
  • Boughs β€” page scaffolds and layout patterns

Development follows the Spectra protocol: every change is built and tested before merge, and this README is updated as the system grows so the docs never outrun the software.

License

MIT Β© rogueoak