Workspace

Directory Conventions

The standard folder layout inside a Handoff workspace, component declaration formats, and patterns observed across real client workspaces.

Top-level layout

my-design-system/
  handoff.config.ts          # or .js / .cjs / .json
  pages/                     # markdown docs pages, mirrors the site nav
    guides/colors.md
  design-system/
    tokens/                  # hand-authored + migrated DTCG input
      primitive/color.tokens.json
      semantic/typography.tokens.json
      brands/default.tokens.json
    dist/                    # generated, safe to gitignore, rebuilt by tokens:build
      css/tokens.css
      scss/_tokens.scss
      tailwind/theme.css
      dtcg/tokens.resolved.json
    manifest.json             # source/brand/count metadata for the compiled bundle
  icons/catalog.json
  logos/logo-set.json
  components/                # or atoms/blocks/elements, see entries.components below
    button/
      button.handoff.ts
      Button.tsx
  patterns/
  .handoff/                  # local CLI/sync state, gitignored, see below

design-system/dist/ and .handoff/ are generated/local, never hand-edit or commit build output; tokens:build and the push/pull commands regenerate them. Everything else (handoff.config.*, design-system/tokens/, pages/, component/pattern source, icons/, logos/) is hand-authored and belongs in git.

Component and pattern declarations

Where Handoff looks for components/patterns is controlled by entries.components / entries.patterns in handoff.config.*, an array of directory paths to scan. Each component gets one declaration file (.handoff.ts, .js, or .handoff.json) describing its metadata, entry files, and preview args. A declaration picks one renderer:

RendererHelperUse case
reactdefineReactComponent(Component, config)Native React/TSX components — Handoff SSR-renders them for the preview.
handlebarsdefineHandlebarsComponent(config)Handlebars templates (.hbs) — the classic Bootstrap/Handlebars stack.
csfdefineCsfComponent(config)Storybook Component Story Format: a meta + named StoryObject exports, same shape as a .stories.tsx file.
button.handoff.ts
import { defineReactComponent } from 'handoff-app';
import Button, { ButtonProps } from './Button';

export default defineReactComponent(Button, {
  id: 'button',
  name: 'Button',
  group: 'Actions',
  entries: { component: './Button.tsx', scss: './styles.scss' },
  previews: {
    default: { title: 'Default', args: { label: 'Click me' } },
  },
  properties: {
    label: { name: 'Label', type: 'text', default: 'Click me' },
  },
});

A fourth helper, defineComponent(config), takes a fully generic declaration (no default renderer) for advanced cases; definePattern(config) declares a saved composition of component blocks. Which renderer a new project should default to is driven by its stack profile (bootstrap-handlebars, react-tailwind, react-scss), see MCP → Stack guides.

Per-client patterns observed

A cross-client review of active workspaces (8x8, SS&C, Resolvet, Cynosure) surfaced real variation worth calling out, useful as a checklist when setting up a new one:

  • Set stackProfile explicitly. Only one of four active workspaces set it; the rest relied on default heuristics, fine to start, fragile once a project has strong conventions to enforce.
  • Pick one templating engine per project (React or Handlebars) and stick to it. The one legitimate hybrid seen in practice used React for atom-level components imported from a separate component-library package, and Handlebars for page-level blocks, two layers that never share a rendering context. Don't mix renderers within the same layer.
  • Always use the three-tier token structure: primitive/ (raw values) → semantic/ (role-mapped aliases) → brands/ (one file per brand, referencing primitive/semantic). Projects missing brands/ fall back to localStyles from the raw Figma snapshot for the Colors/Effects/Typography pages, which is read-only and can't be grouped. Single-brand projects should still ship brands/default.tokens.json for a consistent data path.
  • Keep source images under public/images/ (Figma-exported page images under public/images/figma-export/[page-name]/) so the CLI's image resolver finds them without extra config, and reference them via property bindings (e.g. a background_image prop) rather than hardcoded URLs, hardcoded paths don't get rewritten to the registry's asset store on push.
  • Install handoff-app in the workspace's own package.json, not at a monorepo root, it keeps the workspace self-contained and avoids version drift between the CLI and whatever else the root installs.
  • Prefer handoff/ at the monorepo root (one level below the repo root) over deep nesting: it keeps relative-path config (brands.entries, SCSS loadPaths) short and stable.
  • Ship both icons/catalog.json and logos/logo-set.json: without them the Icons/Logos nav items exist but render empty.

See Token pipeline for how design-system/tokens/ becomes design-system/dist/, and .handoff/ runtime state for what's under .handoff/.

On this page