Tollerud

Tollerud UI/Guides

Resources

Guides

Migration notes, agent skills, and contributor workflows for @tollerud/ui.

Email (@tollerud/email)

HTML email is a separate render target — table layout, inline styles, no CSS variables. It ships as its own package that shares Tollerud's tokens (inlined as literals), not the web components. Built on React Email. Never render @tollerud/ui components into email.

Primitives & templates

Primitives: EmailLayout, EmailHeader (monogram + project name large), BrandMark, EmailButton, EmailHeading, EmailText, EmailDivider, EmailFooter (Tollerud footer → tollerud.no; wordmark recolors as one in dark clients, centered fine print below). Templates: WelcomeEmail, VerifyEmail, PasswordResetEmail, ReceiptEmail — each takes optional header and an overridable copy (reword / i18n). Every primitive takes an optional style escape hatch. Render with the re-exported render.

email.tsx
import { render, WelcomeEmail, EmailLayout, EmailHeader, EmailButton, EmailText, EmailFooter } from '@tollerud/email'

// Ready-made template — opt into the branded header (monogram + project name)
const html = await render(
  <WelcomeEmail
    name="Mathias"
    productName="Graphify"
    ctaUrl={dashboardUrl}
    header={{ productName: 'Graphify' }}
    footer={{ labels: { tollerudProject: 'A Tollerud Project' }, unsubscribeUrl }}
  />,
)

// Or compose your own from primitives
const custom = await render(
  <EmailLayout preview="Your report is ready">
    <EmailHeader productName="Graphify" />
    <EmailText>Your weekly report is ready to view.</EmailText>
    <EmailButton href={reportUrl}>View report</EmailButton>
    <EmailFooter unsubscribeUrl={unsubscribeUrl} />
  </EmailLayout>,
)
// hand the HTML to your mailer (Resend, SES, Nodemailer, …)
@tollerud/email README

Migration

Breaking changes ship with CHANGELOG entries. Recent majors to know about.

v3.0.0 — ESM-only

Replace require('@tollerud/ui') with ESM imports. CJS subpaths are removed.

v2.0.0 — Peer dependencies

Install Radix, Lucide, Framer Motion, and Sonner explicitly in consumer apps.

Copied local components

Projects that vendored src/components/ui/* should delete copies and import from the package. See AGENTS.md in the repo.

AGENTS.md
Consumer styling policy

Tailwind remains available, but consumer apps should use exported components first and reserve utilities for small layout glue.

Consumer project checklist

/resources/consumer-checklist/

Self-audit styling setup before shipping. Catches copied components, missing source.css, and invalid Button/Link composition.

audit
# From your consumer app root
npx tollerud-ui-audit

# Monorepo app package
npx tollerud-ui-audit ./apps/web

# Without npx (direct script path)
node node_modules/@tollerud/ui/scripts/audit-consumer-styling.mjs

# Advisory CI — print findings but exit 0 even with errors
npx tollerud-ui-audit --warn-only

Audit error codes

Findings print as ERROR [code] or WARN [code]:

CodeLevelFix
missing-ui-deperrorAdd @tollerud/ui and peers to package.json
missing-globals-csserrorImport @tollerud/ui/globals.css in Tailwind entry
missing-source-csserrorImport @tollerud/ui/source.css (prevents production purge)
local-ui-cloneerrorDelete components/ui copies; import from @tollerud/ui
copied-ds-tokenserrorReplace vendored files using tollerud-* without package imports
hardcoded-hexerrorUse text-tollerud-yellow, bg-tollerud-noir-950 tokens
button-link-nestingerrorButton asChild + Link, or buttonVariants() on the link
ui-reexport-shimwarnImport from @tollerud/ui instead of local ui/index re-exports
local-cnwarnimport { cn } from "@tollerud/ui/utils"
generic-yellow-utilwarnPrefer Button variant="primary" or text-tollerud-yellow
no-globals-csswarnVerify Tailwind entry imports both Tollerud CSS files
no-package-jsonwarnRun audit from the consumer app package root
feature-component.tsx
// src/features/hosts/HostDeployPanel.tsx — app-specific; composes @tollerud/ui
import { Button, FormPanel, Input, Stack } from '@tollerud/ui'

export function HostDeployPanel({ onDeploy }: { onDeploy: (host: string) => void }) {
  return (
    <FormPanel
      title="Connect host"
      footer={<Button variant="primary" onClick={() => onDeploy('emma.tollerud.no')}>Connect</Button>}
    >
      <Stack gap="md">
        <Input label="Hostname" placeholder="emma.tollerud.no" />
      </Stack>
    </FormPanel>
  )
}

AI agents

Sync SKILL.md into your project so coding assistants use verified exports and gotchas.

skill-sync
# Claude Code / Cursor — copy verbatim on version bumps
mkdir -p .claude/skills/tollerud-ui
curl -fsSL https://raw.githubusercontent.com/Tollerud/ui/main/SKILL.md \
  -o .claude/skills/tollerud-ui/SKILL.md

# Current package version: v5.6.0

SKILL.md reflects actual components/index.ts exports — more reliable than stale copy-paste snippets.

Contributing

Validate before opening a PR. Component changes need registry + docs demo updates.

validate
npm run validate    # typecheck, lint, test, build, package gates, docs build

# Component checklist (see AGENTS.md):
# components/*.tsx → index.ts → registry.json → docs page demo

Prop reference

Full generated prop tables live in the repo; key components show props inline on docs pages.

Regenerate after interface changes: npm run docs:props · drift check: npm run test:props