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.Rootalso prevents iOS Safari from auto-zooming Pipeline controls below 16px. On iOS only, it addsmaximum-scale=1to the existing viewport meta tag at runtime. This preserves pinch zoom on iOS without disabling it on Android, which honors the cap. It runs in rendered HTML and on mount, so apps need only a standard viewport meta tag. Apps with a strictContent-Security-Policy(script-srcwithout'unsafe-inline') should pass their per-request nonce viaRoot’snonceprop so the inline script is allowed; without it, hydrated apps still get the fix on mount, but non-hydrated static consumers lose it.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.
PWA
@o/pipeline/pwa exports framework-independent helpers for configuration, manifests, document head data, asset descriptors, and iOS startup devices. Consumers adapt the returned plain data to their framework’s document API. The collection itself is rendered by pnpm --filter @o/pipeline pwa:build and verified by pnpm --filter @o/pipeline pwa:check, so React SSR and Sharp never enter browser metadata imports.
Each app owns its installed identity and framework manifest. The renderer hashes codepoint-sorted logical image names and encoded image bytes, writes deployment artwork under packages/pipeline/public/pwa/.generated/<hash>/, and emits the compact generated hash and measured Open Graph dimensions. Pipeline bootstrap copies that collection to consumer /.pipeline paths. Design documentation renders the full preview inventory from runtime descriptors. See the PWA documentation page for configuration and replacement instructions.
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 />