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 belowdesign-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:
| Renderer | Helper | Use case |
|---|---|---|
react | defineReactComponent(Component, config) | Native React/TSX components — Handoff SSR-renders them for the preview. |
handlebars | defineHandlebarsComponent(config) | Handlebars templates (.hbs) — the classic Bootstrap/Handlebars stack. |
csf | defineCsfComponent(config) | Storybook Component Story Format: a meta + named StoryObject exports, same shape as a .stories.tsx file. |
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
stackProfileexplicitly. 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 missingbrands/fall back tolocalStylesfrom the raw Figma snapshot for the Colors/Effects/Typography pages, which is read-only and can't be grouped. Single-brand projects should still shipbrands/default.tokens.jsonfor a consistent data path. - Keep source images under
public/images/(Figma-exported page images underpublic/images/figma-export/[page-name]/) so the CLI's image resolver finds them without extra config, and reference them via property bindings (e.g. abackground_imageprop) rather than hardcoded URLs, hardcoded paths don't get rewritten to the registry's asset store on push. - Install
handoff-appin the workspace's ownpackage.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, SCSSloadPaths) short and stable. - Ship both
icons/catalog.jsonandlogos/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/.