README
Operate’s design system
Installation
Note
@o/pipelineis currently only available for use within theoperaterepo.
-
Add the package to your app or package in your
package.jsonfile, installing any necessarypeerDependencies:"dependencies": { "@o/pipeline": "workspace:*", } -
Add the ESLint config (optional, but recommended)
Spread
@o/pipeline/eslint/configin your app’seslint.config.mjsafter your base config:import pipelineConfig from "@o/pipeline/eslint/config"; /** @type {import("eslint").Linter.Config[]} */ export default [ // ...baseConfig, ...pipelineConfig, ];This discourages use of
classNameandstyleon Pipeline components unless you add a comment explaining why. -
Add the following to your project’s
.gitignore:Note
This should be taken care of already in the monorepo’s
.gitignore./public/.pipeline -
Add a
preparescript in your project’spackage.json:{ "scripts": { "prepare": "pipeline bootstrap" } }This script copies
@o/pipeline’s static assets to your project under/public/.pipeline.Note
In the
operatemonorepo, consumer packages wrap this with Turbo for caching: see other apps for theprepare→bootstrap→bootstrap:pipelinepattern.@o/pipelineitself runsturbo run buildonprepare(generateddist/output, not thepipeline bootstrapCLI). -
Ensure
/public/.pipelineare aggressively cached{ "headers": [ { "source": "/.pipeline/(.*)", "headers": [ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } ] } ] } -
Inside your main
.cssentry point (i.e. where you’re usingtailwindcss) import the Pipeline theme and declare as an external source:@import "tailwindcss"; ++ @import "@o/pipeline/theme.css"; ++ @source "../node_modules/@o/pipeline"; -
In your root layout, import your main
.cssentry point (in this example,globals.css), then renderRootas the direct child of<body>and placeProviderinside it.Rootestablishes a full-size, isolated stacking context for popovers, tooltips, and toasts. It must be the immediate child of<body>: wrapping it in another element (or omitting it) breaks layering and iOS 26 Safari positioning.Providersupplies the tooltip, motion, and fireworks context to any Pipeline component rendered underneath it.For example, in a Next.js app:
import { Root } from "@o/pipeline/experimental/core/root"; import { Provider } from "@o/pipeline/experimental/core/provider"; import "../styles/globals.css"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body> <Root> <Provider>{children}</Provider> </Root> </body> </html> ); } -
Unless overriding
fontFamily.sans, initialize the fonts.Note
- Next.js
localFontconfig must be string literals (so unfortunately we can’t DRY this up into a shared config) - If you aren’t likely to need the italic variant, you can skip it entirely
- If your font’s aren’t visible above the fold, you can skip the
preloadoption altogether
For example, in your Next.js root
layout.tsx:// layout.tsx import { Root } from "@o/pipeline/experimental/core/root"; import { Provider } from "@o/pipeline/experimental/core/provider"; ++ import { cx } from "@o/pipeline/cva"; ++ import localFont from "next/font/local"; ++ const muoto = localFont({ ++ variable: "--font-muoto", ++ display: "swap", ++ preload: true, ++ src: [ ++ { ++ style: "normal", ++ weight: "100 900", ++ path: "../node_modules/@o/pipeline/src/fonts/ ++ 205TF-Muoto-Variable.woff2", ++ }, ++ { ++ style: "italic", ++ weight: "100 900", ++ path: "../node_modules/@o/pipeline/src/fonts/ ++ 205TF-Muoto-VariableItalic.woff2", ++ }, ++ ], ++ }); ++ const geistMono = localFont({ ++ variable: "--font-geist-mono", ++ display: "swap", ++ preload: false, ++ src: [ ++ { ++ style: "normal", ++ weight: "100 900", ++ path: "../node_modules/@o/pipeline/src/fonts/ ++ GeistMono-Variable.woff2", ++ }, ++ { ++ style: "italic", ++ weight: "100 900", ++ path: "../node_modules/@o/pipeline/src/fonts/ ++ GeistMono-VariableItalic.woff2", ++ }, ++ ], ++ }); export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( -- <html lang="en"> ++ <html lang="en" className={cx(muoto.variable, geistMono.variable)}> <body> <Root> <Provider>{children}</Provider> </Root> </body> </html> ); } - Next.js
-
Set up a framework-appropriate theme provider to handle switching between
.lightand.darkclasses, as well as ensuringtheme-colormatches the computed style oftheme.properties.colors.canvas(from@o/pipeline/theme) on change.Ideally, this should respect the user’s system preferences by default, and allow for manual overrides.
-
Add the
Toastercomponent to your app, ensuring thethemeis specified based on your theme providere.g. For Next.js, you may need to create a separate component that references the
useThemehook from your theme provider// components/ThemedToaster.tsx "use client"; import { Toaster } from "@o/pipeline/experimental/components/toast"; import { useTheme } from "next-themes"; import React from "react"; export function ThemedToaster() { const { resolvedTheme } = useTheme(); return ( <Toaster theme={ resolvedTheme as unknown as React.ComponentProps< typeof Toaster >["theme"] } /> ); }// layout.tsx import { Root } from "@o/pipeline/experimental/core/root"; import { Provider } from "@o/pipeline/experimental/core/provider"; import localFont from "next/font/local"; ++ import { ThemedToaster } from "./components/ThemedToaster"; const muoto = localFont({ src: "../node_modules/@o/pipeline/src/fonts/muoto-regular.woff2", variable: "--font-muoto", }); export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en" className={muoto.variable}> <body> <Root> <Provider> {children} ++ <ThemedToaster /> </Provider> </Root> </body> </html> ); }
Architecture
Theme
Tailwind v4 introduced a new CSS-driven approach to theming, marking their previous JS-based configuration as a “legacy” system. Additionally, Tailwind’s JS-based plugin system doesn’t (yet) provide any way to inject styles into the @theme layer.
While Tailwind do provide documentation on how to resolve theme values, it’s approach comes with the following caveats:
- It’s browser-only
- No type safety
- No way to map over all color values (e.g. for something like a “color picker”).
To mitigate this, Pipeline stores its color palette and text scale in src/theme/theme.ts: a unified map behind a createTheme({ root: { colors, text }, dark: { colors } }) call, where dark holds sparse overrides that fall back to root.
Upon pnpm i, this gets compiled into dist/styles/theme.css, allowing for full compatibility with Tailwind (including IntelliSense).
Additionally, Pipeline surfaces these tokens as a theme namespace at @o/pipeline/theme, allowing them to be consumed as:
import { theme } from "@o/pipeline/theme";
-
Raw
root(light) anddarkvalues:darkis pre-merged, so paths without an explicit dark override fall back to therootvalue (text has no dark values yet)theme.root.colors.gray[500]; // "oklch(…)" theme.dark.colors.gray[500]; // "oklch(…)" theme.dark.colors.cornflower; // ≡ theme.root.colors.cornflower (fallback) theme.root.text.base; // { fontSize: 14, lineHeight: 20 } theme.root.text.base.lineHeight; // 20 -
CSS variables
theme.vars.colors.gray[500]; // "var(--color-gray-500)" -
CSS properties
theme.properties.colors.gray[500]; // "--color-gray-500"
Editing the theme
To rebuild dist/styles/theme.css after changing theme/theme.ts, run pnpm build (or pnpm i).
Components
components/
├── experimental/
│ └── <component-name>/
├── legacy/
│ └── <component-name>/
├── <component-name>/
- Each top-level
componentsfolder specifies a stable, team QA-tested, production-ready component. - Components nested within
experimental/components/should be considered as early explorations or prototypes. These components should be used with caution. - Components nested within
legacy/components/are deprecated for gradual phase-out. Avoid using them for new features.
Important
Build component primitives on Base UI (@base-ui/react), never Radix. Radix is being removed (see OP-438): do not install or reintroduce @radix-ui/*, cmdk, or vaul. If a primitive isn’t on Base UI yet, build it there or raise it rather than reaching for Radix.
Assets
Suffix all assets with a version number to avoid caching issues.
Documentation
@o/pipeline’s documentation is surfaced at design.operate.so
We use a bespoke Astro-based site to surface each component’s README.mdx file as its own page
Each page should aim to provide a minimum of:
- Installation instructions
- Use examples (or “stories”)
- Design or implementation notes
Writing Stories
All stories should be written in the relevant src/components/component/_stories.tsx file
Each story should be wrapped in the Story component to ensure style and behavior isolation
import { Story } from "@o/pipeline/dx/story";
import { Component } from "@o/pipeline/experimental/components/component";
export function Base() {
return (
<Story>
<Component />
</Story>
);
}
Import and render stories in the component’s README.mdx file as follows…
---
title: Component
slug: components/component
status: undocumented
---
import * as Stories from "./_stories";
## Usage
<Stories.Base client:load />