Working with Canvas
Adding components, the source-only contract, contributing
Canvas is the Olympus design system, 105 React components used by Athena, Hera, and Site. Canvas has its own dedicated docs site with a live playground: olympusoss.github.io/canvas.
This page is for engineers editing Canvas itself.
Atomic-design tiers
Components live under one of:
atoms/, indivisible primitives.molecules/, combinations of atoms.organisms/, complete sections.charts/, data visualisation wrappers.
When adding a component, choose the tier deliberately. If unsure, see ADR 0026, Atomic design in Canvas.
Source-only contract
Canvas ships TypeScript source (main: ./src/index.ts), not a built bundle. The consumer's build pipeline (Next.js, Vite) transpiles it. See ADR 0012, Source-only NPM packages.
Consequences for component authors:
- Only use syntax the consumers support. Bun + Next.js (Webpack) is the lower bound. Avoid bleeding-edge TS features not supported there.
- No build artifacts to commit. No
dist/, nolib/. - Tree-shaking matters. Don't add side-effects (
import "./styles.css"at module level). All side-effects must be opt-in via explicit imports.
Adding a component
cd canvas
# Pick the tier
mkdir src/components/atoms/badge-pulse
touch src/components/atoms/badge-pulse/{index.tsx,index.test.tsx,playground.tsx}index.tsx:
import { cn } from "@/lib/cn";
export interface BadgePulseProps {
variant?: "info" | "warn" | "danger";
children: React.ReactNode;
}
export function BadgePulse({ variant = "info", children }: BadgePulseProps) {
return (
<span className={cn(
"inline-flex items-center px-2 py-1 rounded-full text-xs",
variant === "warn" && "bg-yellow-100 text-yellow-800",
variant === "danger" && "bg-red-100 text-red-800",
variant === "info" && "bg-blue-100 text-blue-800",
)}>
<span className="mr-1 w-2 h-2 rounded-full bg-current animate-pulse" />
{children}
</span>
);
}index.test.tsx:
import { render } from "@testing-library/react";
import { BadgePulse } from "./index";
test("renders danger variant", () => {
const { container } = render(<BadgePulse variant="danger">Live</BadgePulse>);
expect(container.firstChild).toHaveClass("bg-red-100");
});playground.tsx, used by Canvas's own docs site:
import { BadgePulse } from "./index";
export function Playground() {
return (
<div className="space-x-4">
<BadgePulse variant="info">Info</BadgePulse>
<BadgePulse variant="warn">Warn</BadgePulse>
<BadgePulse variant="danger">Danger</BadgePulse>
</div>
);
}Add the export to src/index.ts:
export { BadgePulse } from "./components/atoms/badge-pulse";Tailwind setup
Canvas uses Tailwind v4. Components use Tailwind classes directly; no CSS-in-JS.
Tokens (colors, spacing) live in src/styles/tokens.css. Consumers import this:
import "@olympusoss/canvas/styles/tokens.css";Adding a new token: edit tokens.css, then expose via Tailwind's theme.extend (in consumer's tailwind.config.js or via Tailwind v4's CSS-first config).
Versioning
Canvas follows strict semver. Adding a new export: minor bump. Removing an export or breaking a prop signature: major bump.
Add a changeset:
bun changesetLinking for live development
When you want changes in Canvas to immediately appear in Athena (or Hera, Site) running locally:
cd Olympus/canvas
bun link
cd ../athena
bun link @olympusoss/canvasNow Athena's import { Button } from "@olympusoss/canvas" resolves to your local canvas/src/. Edit, see changes, no rebuild.
To unlink:
cd Olympus/athena
bun unlink @olympusoss/canvas
bun install # re-pull the published version