Design system

Getting started

How the Petavue front end is put together, and the rules every screen follows.

The stack

PartWhat we use
FrameworkReact 19, built with Vite 7
RoutingReact Router 7 (src/router.jsx)
DataTanStack Query for server state, zustand for client state
StylingCSS custom properties for tokens, plain CSS files with BEM class names, Tailwind 4 for page layout
Components@/ui (src/ui/components), plus the dropdown menu in @/components/shadcn/dropdown-menu
IconsPhosphor (@phosphor-icons/react)
Motionmotion/react for enter and exit, CSS transitions for hover and state
Toastssonner, mounted once in the app layout

@ is an alias for src/.

Where things live

src/
  ui/
    components/      the design system components, exported from @/ui
    tokens/          tokens.css: colour, the single source of truth
    colorography/    tokens.css: spacing, radius, borders, shadows
    typography/      typography.css: font, sizes, line heights, text classes
  components/
    shadcn/          the dropdown menu (Base UI), themed in Petavue colours
  pages/             one folder per area of the product
  mocks/             sample data for the prototype

Tokens and components are loaded once in src/main.jsx, so every page can use them without importing CSS.

Using a component

Import from @/ui. Never import from a component's own folder.

import { useState } from "react";
import { Button, TextInput } from "@/ui";

export function RenamePage({ page, onSave }) {
  const [name, setName] = useState(page.name);
  return (
    <form className="rename-page" onSubmit={(e) => { e.preventDefault(); onSave(name); }}>
      <TextInput label="Page name" value={name} onChange={(e) => setName(e.target.value)} />
      <Button type="submit" variant="primary" label="Save" />
    </form>
  );
}

Using tokens

Use the CSS variables. Never paste a hex value into new code.

.pipeline-card {
  padding: var(--spacing-16);
  border: var(--border-default) solid var(--color-grey-200);
  border-radius: var(--radius-md);
  color: var(--color-text-primary);
}

In Tailwind, the grey and primary ramps exist as utilities (text-grey-500, bg-primary-50). For everything else, use the variable in an arbitrary value: text-[var(--color-text-secondary)].

Rules every screen follows

  1. Use the component, not a lookalike. If @/ui has a Button, Dialog, Dropdown or Tooltip, use it. A <div> styled to look like a button is a bug.
  2. Tokens only. Colour, spacing, radius and shadow come from tokens. If no token fits, raise it; do not add a one-off value.
  3. Product palette only. Blue for actions and focus, greys for everything structural, green, red and orange for status. Purple is reserved for components and the Creative agent family.
  4. Two text sizes for most UI: 14px and 12px. Bigger sizes are for page and section titles. Nothing below 12px.
  5. One primary action per area. Every other action is quieter.
  6. Plain copy. Short, specific, no exclamation marks, no taglines. See Writing.
  7. Class names say what a thing is. Library code uses BEM (lib-card__title), never utility soup that hides meaning. Tailwind is fine for page layout.
  8. Change a shared component only when the change should apply everywhere. For a one-off, style around it with a scoped class.

Working with an AI coding agent

Point the agent at /llms.txt before it writes UI. Every page here is also available as Markdown: use "Copy Markdown" at the top of any page, or open /llms.mdx/docs/<page>/content.md. The rules agents follow are in For AI & LLMs.

On this page