@o/pipeline

Operate’s design system

Installation

Note

  • @o/pipeline is currently only available for use within the operate repo.
  1. Add the package to your app or package in your package.json file, installing any necessary peerDependencies:

     "dependencies": {
       "@o/pipeline": "workspace:*",
     }
  2. Add the following to your project’s .gitignore:

    Note

    This should be taken care of already in the monorepo’s .gitignore.

    /public/.pipeline
  3. Add a prepare script in your project’s package.json:

    {
      "scripts": {
        "prepare": "pipeline prepare"
      }
    }

    This script will automatically copy @o/pipeline’s static assets to your project under /public/.pipeline.

  4. Ensure /public/.pipeline are aggressively cached

    {
      "headers": [
        {
          "source": "/.pipeline/(.*)",
          "headers": [
            {
              "key": "Cache-Control",
              "value": "public, max-age=31536000, immutable"
            }
          ]
        }
      ]
    }
  5. Follow TailwindCSS’ official installation steps

  6. Inside your main .css entry point (i.e. where you’re using tailwindcss) import the Pipeline theme and declare as an external source:

       @import "tailwindcss";
    ++ @import "@o/pipeline/theme.css";
    ++ @source "../node_modules/@o/pipeline";
  7. In your root layout, import your main .css entry point (in this example, globals.css) file, and wrap your app with the PipelineProvider

    For example, in a Next.js app:

    import { PipelineProvider } from "@o/pipeline/provider";
    import "../styles/globals.css";
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <PipelineProvider>
          <html lang="en">
            <body>{children}</body>
          </html>
        </PipelineProvider>
      );
    }
  8. Unless overriding fontFamily.sans, initialize the fonts.

    Note

    Font assets are available under src/assets/fonts

    For example, in your Next.js root layout.tsx:

       // layout.tsx
       import { PipelineProvider } from "@o/pipeline/provider";
    ++ import localFont from "next/font/local";
    
    ++ 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 (
           <PipelineProvider>
    --        <html lang="en">
    ++        <html lang="en" className={muoto.variable}>
               <body>{children}</body>
             </html>
           </PipelineProvider>
         );
       }
  9. Set up a framework-appropriate theme provider to handle switching between .light and .dark classes, as well as ensuring theme-color matches the computed style of properties.canvas (from @o/pipeline/styles/colors) on change.

    Ideally, this should respect the user’s system preferences by default, and allow for manual overrides.

  10. Add the Toaster component to your app, ensuring the theme is specified based on your theme provider

    e.g. For Next.js, you may need to create a separate component that references the useTheme hook from your theme provider

    // components/ThemedToaster.tsx
    "use client";
    
    import { Toaster } from "@o/pipeline/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 { PipelineProvider } from "@o/pipeline/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 (
           <PipelineProvider>
             <html lang="en" className={muoto.variable}>
               <body>{children}</body>
             </html>
    ++       <ThemedToaster />
           </PipelineProvider>
         );
       }

Architecture

Colors

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:

  1. It’s browser-only
  2. No type safety
  3. No way to map over all color values (e.g. for something like a “color picker”).

To mitigate this, Pipeline stores its color palette in src/styles/colors.ts

Upon pnpm i, this gets compiled into dist/styles/colors.css, allowing for full compatibility with Tailwind (including IntelliSense)

Additionally, Pipeline surfaces these colors at @o/pipeline/styles/colors, allowing them to be consumed as:

  1. Raw root (light) and dark color values

    import { root, dark } from "@o/pipeline/styles/colors";
    
    const color = root.gray[500]; // "oklch(…)"
    const darkColor = dark.gray[500]; // "oklch(…)"
  2. CSS variables

    import { vars } from "@o/pipeline/styles/colors";
    
    const color = vars.gray[500]; // "var(--color-gray-500)"
  3. CSS properties

    import { properties } from "@o/pipeline/styles/colors";
    
    const color = properties.gray[500]; // "--color-gray-500"

Editing Colors

To rebuild the colors.css file after making changes, run pnpm build (or pnpm i)

Assets

All assets should be suffixed 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:

  1. Installation instructions
  2. Use examples (or “stories”)
  3. 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/docs/story";
import { Component } from "@o/pipeline/components/component";

export function Base() {
  return (
    <Story>
      <Component />
    </Story>
  );
}

Stories can be imported and rendered 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 />