handoff-app 2.0.0-8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{Changelog.md → CHANGELOG.md} +85 -1
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +816 -393
- package/RECIPES.md +975 -0
- package/UPGRADE.md +57 -9
- package/components.json +20 -20
- package/config/docs/foundations/colors/guidelines.md +0 -1
- package/config/docs/foundations/colors.md +0 -1
- package/config/docs/foundations/effects/guidelines.md +0 -1
- package/config/docs/foundations/effects/resources.md +0 -1
- package/config/docs/foundations/effects.md +0 -1
- package/config/docs/foundations/fonts.md +0 -1
- package/config/docs/foundations/grid.md +0 -1
- package/config/docs/foundations/icon.md +0 -1
- package/config/docs/foundations/icons/guidelines.md +0 -1
- package/config/docs/foundations/icons.md +0 -1
- package/config/docs/foundations/images/guidelines.md +0 -1
- package/config/docs/foundations/images/resources.md +0 -1
- package/config/docs/foundations/images.md +0 -1
- package/config/docs/foundations/logo/resources.md +0 -1
- package/config/docs/foundations/logo.md +0 -1
- package/config/docs/foundations/typography/guidelines.md +0 -1
- package/config/docs/foundations/typography/resources.md +0 -1
- package/config/docs/foundations/typography.md +0 -1
- package/config/docs/foundations.md +0 -1
- package/config/docs/guidelines.md +0 -1
- package/config/docs/index.md +0 -1
- package/config/docs/system/tokens/foundations/colors.md +0 -4
- package/config/docs/system/tokens/foundations/effects.md +0 -4
- package/config/docs/system/tokens/foundations/typography.md +0 -2
- package/config/docs/system.md +0 -2
- package/config/templates/page.md +2 -1
- package/config/templates/page.mdx +2 -1
- package/dist/ai/connections.d.ts +66 -0
- package/dist/ai/connections.js +149 -0
- package/dist/ai/define.d.ts +10 -0
- package/dist/ai/define.js +13 -0
- package/dist/ai/types.d.ts +26 -0
- package/dist/ai/types.js +10 -0
- package/dist/app-builder/build.d.ts +14 -7
- package/dist/app-builder/build.js +195 -66
- package/dist/app-builder/client-config.js +3 -2
- package/dist/app-builder/paths.d.ts +0 -12
- package/dist/app-builder/paths.js +3 -20
- package/dist/app-builder/static-export.js +1 -1
- package/dist/app-builder/vercel-output.d.ts +1 -1
- package/dist/app-builder/vercel-output.js +5 -4
- package/dist/cache/build-cache.d.ts +27 -4
- package/dist/cache/build-cache.js +114 -47
- package/dist/cache/build-inputs.d.ts +13 -0
- package/dist/cache/build-inputs.js +19 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +5 -1
- package/dist/catalog/normalize.js +3 -0
- package/dist/catalog/previews.js +5 -4
- package/dist/catalog/types.d.ts +8 -0
- package/dist/cli/create.js +1 -2
- package/dist/commands/build/index.d.ts +1 -1
- package/dist/commands/build/index.js +1 -1
- package/dist/commands/utils.d.ts +3 -3
- package/dist/config/defaults.js +7 -4
- package/dist/config/from-env.d.ts +4 -0
- package/dist/config/from-env.js +38 -1
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +0 -1
- package/dist/config/index.js +1 -4
- package/dist/config/loader.js +28 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +9 -11
- package/dist/handlebars.d.ts +10 -2
- package/dist/index.d.ts +12 -7
- package/dist/index.js +18 -22
- package/dist/nav/index.d.ts +2 -2
- package/dist/nav/registry.d.ts +3 -3
- package/dist/nav/registry.js +1 -1
- package/dist/nav/workspace.d.ts +3 -3
- package/dist/nav/workspace.js +1 -1
- package/dist/pipeline/figma.d.ts +13 -2
- package/dist/pipeline/figma.js +86 -46
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/validation.d.ts +1 -1
- package/dist/pipeline/validation.js +3 -3
- package/dist/providers.d.ts +10 -0
- package/dist/providers.js +13 -0
- package/dist/registry/asset-storage/define.d.ts +5 -5
- package/dist/registry/asset-storage/define.js +4 -4
- package/dist/registry/asset-storage/resolve.d.ts +9 -19
- package/dist/registry/asset-storage/resolve.js +33 -21
- package/dist/registry/asset-storage/types.d.ts +37 -17
- package/dist/registry/asset-storage/types.js +5 -5
- package/dist/registry/auth/ai-keys.d.ts +32 -0
- package/dist/registry/auth/ai-keys.js +69 -0
- package/dist/registry/auth/crypto.d.ts +17 -0
- package/dist/registry/auth/crypto.js +61 -1
- package/dist/registry/auth/index.d.ts +2 -1
- package/dist/registry/auth/index.js +11 -2
- package/dist/registry/checkout/index.d.ts +2 -1
- package/dist/registry/checkout/index.js +57 -127
- package/dist/registry/client.js +53 -17
- package/dist/registry/db/driver.js +3 -3
- package/dist/registry/db/schema.d.ts +261 -3
- package/dist/registry/db/schema.js +29 -4
- package/dist/registry/email/define.d.ts +10 -0
- package/dist/registry/email/define.js +13 -0
- package/dist/registry/email/providers/smtp.d.ts +10 -0
- package/dist/registry/email/providers/smtp.js +73 -0
- package/dist/registry/email/resolve.d.ts +28 -0
- package/dist/registry/email/resolve.js +74 -0
- package/dist/registry/email/types.d.ts +24 -0
- package/dist/registry/email/types.js +6 -0
- package/dist/registry/path.d.ts +2 -0
- package/dist/registry/path.js +11 -1
- package/dist/registry/publish/package.js +8 -0
- package/dist/registry/transfer.d.ts +4 -0
- package/dist/store/registry.d.ts +3 -3
- package/dist/store/registry.js +4 -4
- package/dist/transformers/plugins/ssr-render.js +40 -15
- package/dist/transformers/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +3 -1
- package/dist/transformers/preview/component/css.js +92 -51
- package/dist/transformers/preview/component/html.d.ts +3 -1
- package/dist/transformers/preview/component/html.js +6 -2
- package/dist/transformers/preview/component/javascript.d.ts +2 -1
- package/dist/transformers/preview/component/javascript.js +10 -4
- package/dist/transformers/preview/pattern/html.js +3 -1
- package/dist/transformers/preview/types.d.ts +13 -0
- package/dist/transformers/utils/build.d.ts +24 -0
- package/dist/transformers/utils/build.js +100 -1
- package/dist/transformers/utils/module.js +2 -1
- package/dist/types/config.d.ts +135 -18
- package/dist/utils/path.d.ts +1 -5
- package/dist/utils/path.js +6 -23
- package/docs/api.md +6 -4
- package/docs/cli.md +275 -54
- package/drizzle/0007_add_ai_keys.sql +11 -0
- package/drizzle/0008_add_source_dir.sql +2 -0
- package/drizzle/meta/0007_snapshot.json +2013 -0
- package/drizzle/meta/0008_snapshot.json +2025 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +19 -8
- package/src/ai/README.md +42 -0
- package/src/ai/connections.ts +189 -0
- package/src/ai/define.ts +15 -0
- package/src/ai/types.ts +30 -0
- package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
- package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
- package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
- package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
- package/src/app/components/AiAssistant/dockState.ts +86 -0
- package/src/app/components/AiAssistant/toolLabel.ts +24 -0
- package/src/app/components/AiAssistant/useAiChat.ts +147 -0
- package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
- package/src/app/components/AiAssistant/usePageContext.ts +63 -0
- package/src/app/components/Auth/AccountLayout.tsx +58 -19
- package/src/app/components/Auth/AuthShell.tsx +27 -33
- package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
- package/src/app/components/Auth/installStatus.ts +29 -0
- package/src/app/components/Component/PageSliceResolver.tsx +1 -0
- package/src/app/components/Component/Preview.tsx +383 -189
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
- package/src/app/components/Foundations/ColorGrid.tsx +13 -14
- package/src/app/components/Layout/Header.tsx +21 -15
- package/src/app/components/Layout/Main.tsx +2 -2
- package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
- package/src/app/components/Navigation/MobileNav.tsx +2 -2
- package/src/app/components/Navigation/SideNav.tsx +1 -15
- package/src/app/components/context/NavProvider.tsx +7 -3
- package/src/app/components/ui/attachment.tsx +50 -0
- package/src/app/components/ui/bubble.tsx +57 -0
- package/src/app/components/ui/button.tsx +3 -1
- package/src/app/components/ui/confirm-dialog.tsx +87 -0
- package/src/app/components/ui/dialog.tsx +7 -2
- package/src/app/components/ui/marker.tsx +56 -0
- package/src/app/components/ui/sidebar.tsx +3 -1
- package/src/app/components/util/index.ts +3 -12
- package/src/app/css/index.css +64 -0
- package/src/app/lib/ai/auth.ts +33 -0
- package/src/app/lib/ai/context.ts +99 -0
- package/src/app/lib/ai/model.ts +79 -0
- package/src/app/lib/ai/prompt.ts +36 -0
- package/src/app/lib/ai/resolve.ts +97 -0
- package/src/app/lib/ai/tools.ts +120 -0
- package/src/app/lib/api/methods.ts +13 -0
- package/src/app/lib/asset-storage.ts +35 -36
- package/src/app/lib/auth/api.ts +4 -12
- package/src/app/lib/auth/email.ts +96 -23
- package/src/app/lib/docs-api/page-rendering.ts +1 -1
- package/src/app/lib/docs-api/registry.ts +2 -3
- package/src/app/lib/docs-api/runtime-config.ts +47 -21
- package/src/app/lib/mcp/components.ts +523 -0
- package/src/app/lib/mcp/page-url.ts +24 -0
- package/src/app/lib/mcp/pages.ts +84 -0
- package/src/app/lib/mcp/result.ts +33 -0
- package/src/app/lib/mcp/server.ts +39 -318
- package/src/app/lib/mcp/tokens.ts +334 -0
- package/src/app/lib/mcp/tool.ts +19 -0
- package/src/app/lib/registry-api/asset-transfer.ts +119 -11
- package/src/app/lib/registry-api/errors.ts +2 -0
- package/src/app/lib/registry-api/handler.ts +7 -1
- package/src/app/lib/registry-api/transfer.ts +28 -6
- package/src/app/lib/server-module.ts +34 -0
- package/src/app/lib/utils.ts +20 -0
- package/src/app/next.config.mjs +26 -24
- package/src/app/pages/[...slug]/index.tsx +2 -1
- package/src/app/pages/_app.tsx +6 -2
- package/src/app/pages/_document.tsx +6 -0
- package/src/app/pages/account/ai.tsx +183 -0
- package/src/app/pages/account/index.tsx +68 -68
- package/src/app/pages/account/tokens.tsx +37 -7
- package/src/app/pages/account/users.tsx +75 -15
- package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
- package/src/app/pages/api/account/ai/keys/index.ts +20 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
- package/src/app/pages/api/admin/users/index.ts +2 -2
- package/src/app/pages/api/ai/chat.ts +115 -0
- package/src/app/pages/api/ai/connections.ts +23 -0
- package/src/app/pages/api/auth/request-reset.ts +14 -16
- package/src/app/pages/api/docs/nav.json.ts +1 -1
- package/src/app/pages/api/install.ts +3 -3
- package/src/app/pages/api/mcp.ts +6 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
- package/src/app/pages/cli/device.tsx +10 -5
- package/src/app/pages/foundations/colors.tsx +9 -8
- package/src/app/pages/foundations/index.tsx +3 -3
- package/src/app/pages/index.tsx +2 -2
- package/src/app/pages/install.tsx +7 -2
- package/src/app/pages/login.tsx +32 -27
- package/src/app/pages/reset-password.tsx +21 -20
- package/src/app/pages/system/component/[component]/index.tsx +2 -5
- package/src/app/pages/system/index.tsx +11 -11
- package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
- package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
- package/src/app-builder/README.md +2 -1
- package/src/app-builder/build.ts +218 -73
- package/src/app-builder/client-config.ts +3 -2
- package/src/app-builder/paths.ts +3 -22
- package/src/app-builder/static-export.ts +1 -1
- package/src/app-builder/vercel-output.ts +5 -4
- package/src/cache/README.md +4 -1
- package/src/cache/build-cache.ts +128 -43
- package/src/cache/build-inputs.ts +28 -0
- package/src/cache/index.ts +4 -0
- package/src/catalog/README.md +3 -0
- package/src/catalog/normalize.ts +2 -0
- package/src/catalog/previews.ts +6 -3
- package/src/catalog/types.ts +8 -0
- package/src/cli/create.ts +1 -2
- package/src/commands/build/index.ts +1 -1
- package/src/config/README.md +10 -9
- package/src/config/defaults.ts +7 -4
- package/src/config/from-env.ts +33 -0
- package/src/config/helpers.ts +1 -3
- package/src/config/index.ts +0 -3
- package/src/config/loader.ts +27 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +10 -5
- package/src/handlebars.ts +8 -2
- package/src/index.ts +33 -29
- package/src/nav/index.ts +2 -2
- package/src/nav/registry.ts +3 -3
- package/src/nav/workspace.ts +3 -3
- package/src/pipeline/README.md +1 -1
- package/src/pipeline/figma.ts +98 -49
- package/src/pipeline/styles.ts +1 -1
- package/src/pipeline/validation.ts +3 -3
- package/src/providers.ts +18 -0
- package/src/registry/asset-storage/define.ts +6 -6
- package/src/registry/asset-storage/resolve.ts +38 -37
- package/src/registry/asset-storage/types.ts +39 -17
- package/src/registry/auth/ai-keys.ts +62 -0
- package/src/registry/auth/crypto.ts +63 -1
- package/src/registry/auth/index.ts +11 -1
- package/src/registry/checkout/index.ts +60 -138
- package/src/registry/client.ts +60 -20
- package/src/registry/db/driver.ts +2 -2
- package/src/registry/db/schema.ts +49 -22
- package/src/registry/email/define.ts +15 -0
- package/src/registry/email/providers/smtp.ts +39 -0
- package/src/registry/email/resolve.ts +94 -0
- package/src/registry/email/types.ts +28 -0
- package/src/registry/path.ts +11 -0
- package/src/registry/publish/package.ts +9 -0
- package/src/registry/transfer.ts +4 -0
- package/src/store/registry.ts +7 -7
- package/src/transformers/plugins/ssr-render.ts +53 -16
- package/src/transformers/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +101 -53
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/preview/pattern/html.ts +3 -1
- package/src/transformers/preview/types.ts +14 -0
- package/src/transformers/utils/build.ts +93 -0
- package/src/transformers/utils/module.ts +4 -2
- package/src/types/config.ts +148 -20
- package/src/utils/README.md +1 -1
- package/src/utils/path.ts +6 -29
- package/templates/default/handoff.config.js.tpl +10 -2
- package/templates/default/handoff.config.ts.tpl +10 -2
- package/templates/default/handoff.config.with-examples.js.tpl +10 -2
- package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
- package/templates/default/package.json.tpl +2 -2
- package/.editorconfig +0 -19
- package/.eslintrc.json +0 -4
- package/.prettierignore +0 -0
- package/.prettierrc.json +0 -8
- package/.vscode/settings.json +0 -17
- package/dist/config/validator.d.ts +0 -9
- package/dist/config/validator.js +0 -24
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
- package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
- package/src/app/components/old/Header.tsx +0 -89
- package/src/app/lib/mcp/shape.ts +0 -596
- package/src/app/pages/assets/fonts.tsx +0 -129
- package/src/app/pages/assets/index.tsx +0 -91
- package/src/app/pages/assets/logos.tsx +0 -75
- package/src/config/validator.ts +0 -22
- package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
package/README.md
CHANGED
|
@@ -3,88 +3,446 @@
|
|
|
3
3
|
<a aria-label="NPM version" href="https://www.npmjs.com/package/handoff-app">
|
|
4
4
|
<img alt="" src="https://img.shields.io/npm/v/handoff-app?style=for-the-badge&labelColor=000000">
|
|
5
5
|
</a>
|
|
6
|
-
<a aria-label="License" href="https://github.com/convertiv/handoff-app/blob/main/
|
|
6
|
+
<a aria-label="License" href="https://github.com/convertiv/handoff-app/blob/main/LICENSE.md">
|
|
7
7
|
<img alt="" src="https://img.shields.io/npm/l/handoff-app?style=for-the-badge&labelColor=000000">
|
|
8
8
|
</a>
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
documentation and distributable artifacts.
|
|
12
|
-
or a shared
|
|
10
|
+
Handoff turns design tokens, components, and patterns into working
|
|
11
|
+
documentation and distributable artifacts. You can serve the documentation
|
|
12
|
+
from a local workspace, a static site, or a shared registry with a PostgreSQL
|
|
13
|
+
database.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
> **Recipes:** [RECIPES.md](RECIPES.md) has complete examples for project
|
|
16
|
+
> layouts, catalog items, profiles, registry hosting, asset storage, email, and
|
|
17
|
+
> the AI assistant.
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- A **connected workspace** remains in workspace mode but can publish to and
|
|
20
|
-
check out from a registry.
|
|
19
|
+
> **Upgrade from version 1.x.x:** read the
|
|
20
|
+
> [migration guide](UPGRADE.md#version-1xx-to-version-2xx) for the breaking
|
|
21
|
+
> changes and the migration steps.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
## Contents
|
|
24
|
+
|
|
25
|
+
- [How Handoff works](#how-handoff-works)
|
|
26
|
+
- [Requirements](#requirements)
|
|
27
|
+
- [Installation](#installation)
|
|
28
|
+
- [Configuration](#configuration)
|
|
29
|
+
- [Authoring](#authoring)
|
|
30
|
+
- [Running the workspace](#running-the-workspace)
|
|
31
|
+
- [Building](#building)
|
|
32
|
+
- [Registry](#registry)
|
|
33
|
+
- [Connecting a workspace to a registry](#connecting-a-workspace-to-a-registry)
|
|
34
|
+
- [MCP](#mcp)
|
|
35
|
+
- [AI assistant](#ai-assistant)
|
|
36
|
+
- [CLI reference](#cli-reference)
|
|
37
|
+
|
|
38
|
+
## How Handoff works
|
|
39
|
+
|
|
40
|
+
### Runtime modes
|
|
41
|
+
|
|
42
|
+
- **Workspace** is the default mode. Local files are the source of truth, and
|
|
43
|
+
no database is necessary. You use it to author content.
|
|
44
|
+
- **Registry** is a deployed catalog with a PostgreSQL database. It stores and
|
|
45
|
+
serves only the content that a workspace publishes to it.
|
|
46
|
+
- A **connected workspace** is a workspace that can publish to a registry and
|
|
47
|
+
check out from it. It is not a third mode.
|
|
48
|
+
|
|
49
|
+
The `runtime.mode` value in the config sets the mode. A registry build sets
|
|
50
|
+
registry mode automatically, so the base config stays in workspace mode.
|
|
51
|
+
|
|
52
|
+
### Workflow
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
Figma ── fetch ──▶ exported/ tokens, CSS, Sass, assets
|
|
56
|
+
catalog, pages ── start ──▶ local documentation at http://localhost:3000
|
|
57
|
+
── build ──▶ out/static (static site) or out/registry (registry server)
|
|
58
|
+
workspace ── publish ─▶ registry ── checkout ─▶ workspace
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The workspace is the only place that builds content. A registry never builds
|
|
62
|
+
and never reads workspace source.
|
|
63
|
+
|
|
64
|
+
### Terms
|
|
65
|
+
|
|
66
|
+
- **Working directory:** the directory that holds `handoff.config.ts`. Every
|
|
67
|
+
command runs against it.
|
|
68
|
+
- **Catalog item:** one documented UI entry. It has a declaration file, a
|
|
69
|
+
stable id, and previews.
|
|
70
|
+
- **Component:** a catalog item with an implementation (React, Handlebars, or
|
|
71
|
+
CSF).
|
|
72
|
+
- **Pattern:** a catalog item that is a composition of other catalog items.
|
|
73
|
+
- **Foundations:** the tokens and assets that `fetch` gets from Figma.
|
|
74
|
+
- **Page:** a Markdown documentation page under `pages/`.
|
|
75
|
+
- **Profile:** a config file that merges onto the base config for one
|
|
76
|
+
environment.
|
|
24
77
|
|
|
25
78
|
## Requirements
|
|
26
79
|
|
|
27
|
-
- Node.js
|
|
28
|
-
- npm
|
|
29
|
-
- A paid Figma account
|
|
30
|
-
- PostgreSQL
|
|
80
|
+
- Node.js 22 or newer. We recommend Node.js 24 LTS.
|
|
81
|
+
- npm 10 or newer.
|
|
82
|
+
- A paid Figma account, to fetch a Figma library.
|
|
83
|
+
- PostgreSQL, to run a registry.
|
|
31
84
|
|
|
32
|
-
##
|
|
85
|
+
## Installation
|
|
33
86
|
|
|
34
|
-
|
|
87
|
+
### New project
|
|
88
|
+
|
|
89
|
+
Create a project with the interactive wizard:
|
|
35
90
|
|
|
36
91
|
```bash
|
|
37
92
|
npx handoff-app init
|
|
38
93
|
cd my-handoff-project
|
|
39
94
|
```
|
|
40
95
|
|
|
41
|
-
The project name, sample content, JavaScript or
|
|
42
|
-
setup, and optional Figma credentials
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
96
|
+
The wizard asks for the project name, sample content, JavaScript or
|
|
97
|
+
TypeScript, an optional Vercel setup, and optional Figma credentials. It
|
|
98
|
+
creates the starter files and installs `handoff-app` in the project.
|
|
99
|
+
|
|
100
|
+
Use the generated npm scripts. They run the project version of `handoff-app`,
|
|
101
|
+
so local development and CI use the same version:
|
|
46
102
|
|
|
47
103
|
```bash
|
|
48
104
|
npm run fetch # optional: fetch Figma foundations
|
|
49
105
|
npm run start # local server with Handoff file watchers
|
|
50
106
|
npm run dev # local development server without Handoff file watchers
|
|
51
107
|
npm run build # static build by default
|
|
108
|
+
npm run publish # publish to a registry
|
|
109
|
+
npm run checkout # check out from a registry
|
|
110
|
+
npm run login # sign in to a registry
|
|
111
|
+
npm run logout # sign out of a registry
|
|
52
112
|
npm run db:migrate # registry database migrations
|
|
53
|
-
npm run validate
|
|
54
113
|
```
|
|
55
114
|
|
|
56
|
-
|
|
115
|
+
### Project layout
|
|
57
116
|
|
|
58
117
|
```text
|
|
59
|
-
|
|
118
|
+
<working directory>/
|
|
60
119
|
├─ handoff.config.ts
|
|
61
120
|
├─ .env
|
|
62
121
|
├─ components/
|
|
63
122
|
├─ patterns/
|
|
64
123
|
├─ pages/
|
|
65
124
|
├─ exported/ # fetched tokens and assets; commit this directory
|
|
66
|
-
|
|
125
|
+
├─ public/api/ # generated docs API; gitignored
|
|
126
|
+
├─ out/ # build output; gitignored
|
|
127
|
+
├─ .vercel/ # `--package vercel` output; gitignored
|
|
128
|
+
└─ .handoff/ # local Handoff state; gitignored
|
|
67
129
|
```
|
|
68
130
|
|
|
69
|
-
|
|
131
|
+
In a project that `init` creates, the working directory is the repository
|
|
132
|
+
root. `HANDOFF_WORKING_PATH` sets a different working directory.
|
|
133
|
+
|
|
134
|
+
### Adding Handoff to an existing application
|
|
135
|
+
|
|
136
|
+
Declarations sit next to the components that they document. Thus the catalog
|
|
137
|
+
points into the application source:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
catalog: { include: ['src/components', 'src/blocks'] },
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Two rules apply to every layout.
|
|
144
|
+
|
|
145
|
+
**`handoff-app` must be a dependency of the package that holds the
|
|
146
|
+
`*.handoff.ts` files.** A declaration imports `handoff-app/react`. Handoff
|
|
147
|
+
resolves that import from the directory of the declaration, not from the
|
|
148
|
+
directory of the config. With npm, hoisting usually hides a missing dependency.
|
|
149
|
+
With pnpm, Handoff skips the affected items with
|
|
150
|
+
`Could not resolve "handoff-app/react"`, and the build still exits with code 0.
|
|
151
|
+
In a workspace-package layout, the application package and the `handoff/`
|
|
152
|
+
package must both declare `handoff-app`.
|
|
153
|
+
|
|
154
|
+
**Two `tsconfig.json` files do different jobs:**
|
|
155
|
+
|
|
156
|
+
- The config nearest to the declarations type checks them. An editor reads
|
|
157
|
+
only the nearest config. A config in a different directory that includes
|
|
158
|
+
`src/` does not count.
|
|
159
|
+
- The config in the working directory makes the property tables. Without it,
|
|
160
|
+
Handoff shows `TypeScript config not found`, and every table is empty.
|
|
161
|
+
|
|
162
|
+
If the working directory is not the project root, you need both files.
|
|
163
|
+
|
|
164
|
+
Three layouts work. Each keeps one dependency tree, so the catalog renders the
|
|
165
|
+
real components with the same React instance. Handoff does not support a
|
|
166
|
+
separate install with its own `node_modules` outside that tree.
|
|
167
|
+
|
|
168
|
+
| Layout | Working directory | Use when |
|
|
169
|
+
| --- | --- | --- |
|
|
170
|
+
| Subdirectory (recommended) | `handoff/`, set by `HANDOFF_WORKING_PATH` | The application must not change |
|
|
171
|
+
| Project root | The project root | The bundler can move its `public/` directory (not Next.js) |
|
|
172
|
+
| Workspace package | `handoff/`, with its own `package.json` | The repository is already a workspace |
|
|
173
|
+
|
|
174
|
+
In the project-root layout, Handoff writes the generated documentation API to
|
|
175
|
+
`public/api`. Thus a bundler that serves `public/` copies it into the
|
|
176
|
+
production build of the application. Move the bundler to a different
|
|
177
|
+
directory. A Next.js application cannot move its `public` directory, so it
|
|
178
|
+
cannot use this layout.
|
|
179
|
+
|
|
180
|
+
With pnpm, `build --target registry` cannot follow the pnpm symlinks. Set
|
|
181
|
+
`nodeLinker: hoisted` in `pnpm-workspace.yaml`. The other builds work with the
|
|
182
|
+
default pnpm layout.
|
|
70
183
|
|
|
71
|
-
|
|
72
|
-
An implementation names its renderer. `defineCatalogItem` comes from the module for that renderer.
|
|
73
|
-
The declaration supplies stable identity, documentation metadata, source entries, and previews.
|
|
184
|
+
To add Handoff in the recommended subdirectory layout:
|
|
74
185
|
|
|
75
|
-
|
|
186
|
+
1. Install `handoff-app` in the application package:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npm install handoff-app
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
2. Create `handoff/handoff.config.ts`. Point the catalog at the application
|
|
193
|
+
source, and load the global stylesheet of the application into the
|
|
194
|
+
previews:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { defineConfig } from 'handoff-app';
|
|
198
|
+
|
|
199
|
+
export default defineConfig({
|
|
200
|
+
app: { title: 'Acme Design System' },
|
|
201
|
+
catalog: { include: ['../src/components'] },
|
|
202
|
+
entries: { scss: '../src/styles/main.scss' },
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Paths are relative to the working directory. `npx handoff-app eject:config`
|
|
207
|
+
writes the full default config instead.
|
|
208
|
+
|
|
209
|
+
3. Create `handoff/tsconfig.json`, so that Handoff can make the property
|
|
210
|
+
tables:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{ "extends": "../tsconfig.json", "include": ["../src", "handoff.config.ts"] }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
4. Add the scripts to the `package.json` of the application:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"scripts": {
|
|
221
|
+
"handoff:dev": "HANDOFF_WORKING_PATH=handoff handoff-app start",
|
|
222
|
+
"handoff:build": "HANDOFF_WORKING_PATH=handoff handoff-app build"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
5. Write a declaration next to a component, for example
|
|
228
|
+
`src/components/button/button.handoff.ts`. The
|
|
229
|
+
[Catalog items](#catalog-items) section shows the format.
|
|
230
|
+
|
|
231
|
+
6. Start the documentation:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
npm run handoff:dev
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
7. Add `handoff/public/api`, `handoff/out`, `handoff/.handoff`, and
|
|
238
|
+
`handoff/.vercel` to `.gitignore`. Commit `handoff/exported` if you fetch
|
|
239
|
+
from Figma.
|
|
240
|
+
|
|
241
|
+
> **Recipes:** [Subdirectory](RECIPES.md#handoff-in-a-subdirectory) ·
|
|
242
|
+
> [Project root](RECIPES.md#handoff-in-the-project-root) ·
|
|
243
|
+
> [Workspace package](RECIPES.md#handoff-as-a-workspace-package) ·
|
|
244
|
+
> [pnpm](RECIPES.md#pnpm) · [npm scripts on Windows](RECIPES.md#npm-scripts-on-windows)
|
|
245
|
+
|
|
246
|
+
## Configuration
|
|
247
|
+
|
|
248
|
+
### Config file
|
|
249
|
+
|
|
250
|
+
Handoff reads `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in that order.
|
|
251
|
+
`-c, --config <file>` loads one specific file instead. `defineConfig` gives
|
|
252
|
+
typed authoring:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
// handoff.config.ts
|
|
256
|
+
import { defineConfig, fromEnv } from 'handoff-app';
|
|
257
|
+
|
|
258
|
+
export default defineConfig({
|
|
259
|
+
app: { title: 'Acme Design System' },
|
|
260
|
+
catalog: { include: ['components', 'patterns'] },
|
|
261
|
+
runtime: {
|
|
262
|
+
mode: 'workspace',
|
|
263
|
+
workspace: { declarationFormat: 'ts' },
|
|
264
|
+
registry: {
|
|
265
|
+
database: { url: fromEnv('DATABASE_URL'), driver: 'pg' },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
```
|
|
76
270
|
|
|
77
|
-
|
|
78
|
-
|
|
271
|
+
Values merge onto the defaults. Plain objects merge recursively. Arrays,
|
|
272
|
+
scalars, `null`, and functions replace the default.
|
|
273
|
+
|
|
274
|
+
### Main settings
|
|
275
|
+
|
|
276
|
+
| Setting | Purpose |
|
|
277
|
+
| --- | --- |
|
|
278
|
+
| `app.title`, `app.client` | Name of the documentation site and of the client |
|
|
279
|
+
| `app.theme` | Theme of the documentation app. `npx handoff-app eject:theme` copies it so that you can change it. |
|
|
280
|
+
| `app.basePath` | URL path of the site when you serve it under a sub-path, for example `/design-system` |
|
|
281
|
+
| `app.breakpoints` | Preview widths. A declared block merges onto the defaults. |
|
|
282
|
+
| `app.ports` | Workspace server ports, `{ app, websocket }`. The defaults are 3000 and 3001. |
|
|
283
|
+
| `catalog.include` | Catalog directories. See [Registration](#registration). |
|
|
284
|
+
| `entries.scss`, `entries.js` | Global stylesheet and script that Handoff builds and loads into every preview |
|
|
285
|
+
| `integrations.figma` | Figma file and token. See [Figma foundations](#figma-foundations). |
|
|
286
|
+
| `exportsOutputDirectory` | Directory for fetched foundations. The default is `exported`. |
|
|
287
|
+
| `sitesOutputDirectory` | Directory for build output. The default is `out`. |
|
|
288
|
+
| `runtime.mode` | `workspace` (default) or `registry` |
|
|
289
|
+
| `runtime.workspace.declarationFormat` | Format of the declarations that `checkout` writes: `ts`, `js`, or `cjs` |
|
|
290
|
+
| `runtime.registryConnection` | Registry URL and token of a connected workspace |
|
|
291
|
+
| `runtime.registry` | Registry database, asset storage, and email. See [Registry](#registry). |
|
|
292
|
+
| `runtime.mcp` | MCP endpoint on or off. See [MCP](#mcp). |
|
|
293
|
+
| `runtime.ai` | AI assistant. See [AI assistant](#ai-assistant). |
|
|
294
|
+
| `hooks` | Build hooks, documented in [docs/api.md](docs/api.md) |
|
|
295
|
+
|
|
296
|
+
The `Config` type documents every setting. Several settings read an environment
|
|
297
|
+
variable by default. [Environment variables](#environment-variables) lists them.
|
|
298
|
+
|
|
299
|
+
### Build-time and runtime values
|
|
300
|
+
|
|
301
|
+
`fromEnv('NAME')` refers to an environment variable. It does not copy the
|
|
302
|
+
value into the config.
|
|
303
|
+
|
|
304
|
+
- A literal value is baked into the build.
|
|
305
|
+
- For a `fromEnv()` value, the build keeps only the variable name. A deployed
|
|
306
|
+
registry reads the value at request time. Thus you can change it without a
|
|
307
|
+
rebuild.
|
|
308
|
+
- Secrets must use `fromEnv()`.
|
|
309
|
+
|
|
310
|
+
Some settings change the shape of the build. These settings are baked, so a
|
|
311
|
+
change to a deployed registry needs a rebuild:
|
|
312
|
+
|
|
313
|
+
- `runtime.mode`
|
|
314
|
+
- `runtime.mcp`
|
|
315
|
+
- `runtime.ai.enabled`
|
|
316
|
+
- The database driver
|
|
317
|
+
- The asset storage provider and the email provider
|
|
318
|
+
- Every custom server module
|
|
319
|
+
|
|
320
|
+
### Profiles
|
|
321
|
+
|
|
322
|
+
A profile is a config file that merges onto the base config. It holds only what
|
|
323
|
+
changes between environments. Thus a project keeps one shared config, not a
|
|
324
|
+
second full copy.
|
|
325
|
+
|
|
326
|
+
- The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. When no
|
|
327
|
+
profile is selected, Handoff loads only this file.
|
|
328
|
+
- A profile file is `handoff.config.<profile>.*`, with the same four
|
|
329
|
+
extensions.
|
|
330
|
+
- Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
|
|
331
|
+
`--profile` has priority over `HANDOFF_PROFILE`.
|
|
332
|
+
- Profile names are not predefined. A name can contain lowercase letters,
|
|
333
|
+
numbers, and hyphens.
|
|
334
|
+
- A selected profile must exist. If no `handoff.config.<name>.*` file exists,
|
|
335
|
+
the command stops with an error.
|
|
336
|
+
- `login`, `logout`, `publish`, and `checkout` also accept a profile that has
|
|
337
|
+
only a saved registry login. For these commands, the profile selects the
|
|
338
|
+
registry. Every other command needs the config file.
|
|
339
|
+
- Layers apply in this order: defaults, base config, profile, then
|
|
340
|
+
programmatic config. The [merge rules](#config-file) apply to each layer.
|
|
341
|
+
- A selected profile also reads `.env.<profile>` on top of `.env`, if that file
|
|
342
|
+
exists. Handoff reads both files from the directory where the command runs. A
|
|
343
|
+
variable that is already in the environment has priority over both files.
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
npx handoff-app build --target registry --profile registry
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
A profile is a good place for the registry build settings: the database
|
|
350
|
+
driver, the database variable name, the asset storage, the email provider, and
|
|
351
|
+
MCP. `build --target registry` always builds a registry-mode application, so
|
|
352
|
+
the profile does not set `runtime.mode`. A hosting provider or a CI job selects
|
|
353
|
+
the profile with `HANDOFF_PROFILE`.
|
|
354
|
+
|
|
355
|
+
`defineConfig` types a profile and a base config the same way. The generated
|
|
356
|
+
`.gitignore` lists `handoff.config.local.*`. Thus `local` is the usual name for
|
|
357
|
+
a profile that stays on one machine.
|
|
358
|
+
|
|
359
|
+
Arrays replace. Thus a profile that declares `catalog.include` also controls
|
|
360
|
+
what `make:component` and `checkout` register.
|
|
361
|
+
|
|
362
|
+
Handoff reads `.env.<profile>` once at startup and does not watch it. It cannot
|
|
363
|
+
set `HANDOFF_WORKING_PATH`, because Handoff finds the working path before it
|
|
364
|
+
knows the profile.
|
|
365
|
+
|
|
366
|
+
> **Recipes:** [Local profile](RECIPES.md#local-profile) ·
|
|
367
|
+
> [Registry build profile](RECIPES.md#registry-build-profile) ·
|
|
368
|
+
> [One workspace, several registries](RECIPES.md#one-workspace-several-registries)
|
|
369
|
+
|
|
370
|
+
### Environment variables
|
|
371
|
+
|
|
372
|
+
Handoff reads two kinds of variables:
|
|
373
|
+
|
|
374
|
+
- **Fixed variables.** Handoff always reads these names.
|
|
375
|
+
- **Setting defaults.** Each one is the default `fromEnv()` name of a setting.
|
|
376
|
+
To use a different name, set the setting to `fromEnv('YOUR_NAME')`.
|
|
377
|
+
|
|
378
|
+
This section does not list the variables that you name in the config. Examples
|
|
379
|
+
are the keys of a custom asset storage provider or of an AI connection. A
|
|
380
|
+
standalone registry build writes `out/registry/README.md`, which lists every
|
|
381
|
+
variable that the build needs, with the names from your config.
|
|
382
|
+
|
|
383
|
+
**Fixed variables**
|
|
384
|
+
|
|
385
|
+
| Variable | Purpose |
|
|
386
|
+
| --- | --- |
|
|
387
|
+
| `HANDOFF_PROFILE` | Config profile merged onto the base config |
|
|
388
|
+
| `HANDOFF_WORKING_PATH` | Directory that holds `handoff.config.ts`. The default is the current directory. |
|
|
389
|
+
| `HANDOFF_LOG_LEVEL` | `debug`, `info`, `warn`, `error`, or `silent`. The default is `info`. |
|
|
390
|
+
| `HANDOFF_LOG_SCOPES` | Comma-separated log sources: `handoff`, `vite`, `next`. The default is all three. |
|
|
391
|
+
| `HANDOFF_LOGIN_NO_BROWSER` | Set to `true` to stop `login` from opening a browser, the same as `--no-browser` |
|
|
392
|
+
| `HANDOFF_CREATE_ASSETS_ZIP_FILES` | Set to `false` to skip the icon and logo zip files in `fetch` |
|
|
393
|
+
| `HANDOFF_SYNC_SECRET` | Optional deployment-wide credential with read and write access. The registry accepts it. A workspace uses it only when it has no token and no login. |
|
|
394
|
+
| `AUTH_SECRET` | Registry session-signing secret, at least 32 characters. Required by a registry. |
|
|
395
|
+
| `AUTH_URL` | Canonical public registry URL, with the base path. Required by a registry. |
|
|
396
|
+
| `PORT` | Standalone registry server port |
|
|
397
|
+
| `HOSTNAME` | Standalone registry server bind hostname |
|
|
398
|
+
| `HANDOFF_AI_KEY_SECRET` | Encrypts the AI provider keys of readers, at least 32 characters |
|
|
399
|
+
| `HANDOFF_AI_CONNECTIONS` | JSON array of AI connections, merged over the built-in list by `id` |
|
|
400
|
+
|
|
401
|
+
**Setting defaults**
|
|
402
|
+
|
|
403
|
+
| Variable | Setting | Purpose |
|
|
404
|
+
| --- | --- | --- |
|
|
405
|
+
| `HANDOFF_FIGMA_PROJECT_ID` | `integrations.figma.projectId` | Figma file ID that `fetch` uses |
|
|
406
|
+
| `HANDOFF_DEV_ACCESS_TOKEN` | `integrations.figma.accessToken` | Figma personal access token that `fetch` uses |
|
|
407
|
+
| `HANDOFF_REGISTRY_URL` | `runtime.registryConnection.url` | Registry URL of a connected workspace |
|
|
408
|
+
| `HANDOFF_REGISTRY_ACCESS_TOKEN` | `runtime.registryConnection.accessToken` | Registry access token of a connected workspace |
|
|
409
|
+
| `DATABASE_URL` | `runtime.registry.database.url` | PostgreSQL connection string. Required by a registry. |
|
|
410
|
+
| `HANDOFF_APP_PORT` | `app.ports.app` | Workspace documentation server port. The default is 3000. |
|
|
411
|
+
| `HANDOFF_WEBSOCKET_PORT` | `app.ports.websocket` | Workspace live-reload server port. The default is 3001. |
|
|
412
|
+
| `HANDOFF_OUTPUT_DIR` | `exportsOutputDirectory` | Fetched output directory. The default is `exported`. |
|
|
413
|
+
| `HANDOFF_SITES_DIR` | `sitesOutputDirectory` | Build output directory. The default is `out`. |
|
|
414
|
+
|
|
415
|
+
Keep the name `HANDOFF_OUTPUT_DIR`. The documentation app also reads this
|
|
416
|
+
variable directly.
|
|
417
|
+
|
|
418
|
+
## Authoring
|
|
419
|
+
|
|
420
|
+
### Catalog items
|
|
421
|
+
|
|
422
|
+
Every documented UI entry is a catalog item. An item declares one of these:
|
|
423
|
+
|
|
424
|
+
- An `implementation`, which names a renderer: React, Handlebars, or CSF.
|
|
425
|
+
The registry stores the item as a component.
|
|
426
|
+
- A `composition` of other items. The registry stores the item as a pattern.
|
|
427
|
+
|
|
428
|
+
An item never declares both. `defineCatalogItem` comes from the module of the
|
|
429
|
+
renderer. The declaration gives the stable id, the documentation metadata, the
|
|
430
|
+
source entries, and the previews.
|
|
431
|
+
|
|
432
|
+
**React.** Handoff finds the implementation file from its import, so you do not
|
|
433
|
+
repeat the path. Handoff builds the CSS of the item from the stylesheets that
|
|
434
|
+
the import chain loads, descendants included. Previews are named exports.
|
|
435
|
+
`Preview<typeof item>` gives them the argument type of the implementation.
|
|
79
436
|
|
|
80
437
|
```tsx
|
|
81
438
|
// components/example/Component.tsx
|
|
82
439
|
export type ComponentProps = {
|
|
83
440
|
label: string;
|
|
441
|
+
onSelect?: () => void;
|
|
84
442
|
};
|
|
85
443
|
|
|
86
|
-
export default function Component({ label }: ComponentProps) {
|
|
87
|
-
return <div>{label}</div>;
|
|
444
|
+
export default function Component({ label, onSelect }: ComponentProps) {
|
|
445
|
+
return <div onClick={onSelect}>{label}</div>;
|
|
88
446
|
}
|
|
89
447
|
```
|
|
90
448
|
|
|
@@ -110,74 +468,23 @@ export const Default = {
|
|
|
110
468
|
} satisfies ComponentPreview;
|
|
111
469
|
```
|
|
112
470
|
|
|
113
|
-
The export name is the preview name.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
The implementation is the path to the template. Arguments are `Record<string, unknown>` by default;
|
|
118
|
-
an explicit argument type is supplied through `defineCatalogItem<BadgeArgs>`.
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
// components/badge/badge.handoff.ts
|
|
122
|
-
import { defineCatalogItem, type Preview } from 'handoff-app/handlebars';
|
|
123
|
-
|
|
124
|
-
const item = defineCatalogItem({
|
|
125
|
-
id: 'badge',
|
|
126
|
-
name: 'Badge',
|
|
127
|
-
implementation: './Badge.hbs',
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
export default item;
|
|
131
|
-
|
|
132
|
-
type BadgePreview = Preview<typeof item>;
|
|
471
|
+
The export name is the preview name. `name` sets a different display title.
|
|
472
|
+
Handoff serializes the arguments. Thus a preview that needs an icon, a
|
|
473
|
+
callback, JSX children, or its own state declares a `render` function. A
|
|
474
|
+
declaration that holds JSX must use the `.handoff.tsx` extension.
|
|
133
475
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
} satisfies BadgePreview;
|
|
137
|
-
```
|
|
476
|
+
**Handlebars.** The implementation is the path to the template:
|
|
477
|
+
`implementation: './Badge.hbs'`, from `handoff-app/handlebars`.
|
|
138
478
|
|
|
139
|
-
|
|
479
|
+
**CSF.** A React item uses an existing story file with
|
|
480
|
+
`implementation: fromCSF('./Card.stories.tsx')`. The story file does not
|
|
481
|
+
change, and its named exports are the previews. The server renders a CSF
|
|
482
|
+
preview, and the browser does not hydrate it. The browser hydrates a direct
|
|
483
|
+
React implementation.
|
|
140
484
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
It finds the React component through `meta.component`, then documents and publishes it with the story file.
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
// components/card/card.handoff.ts
|
|
148
|
-
import { defineCatalogItem, fromCSF } from 'handoff-app/react';
|
|
149
|
-
|
|
150
|
-
export default defineCatalogItem({
|
|
151
|
-
id: 'card',
|
|
152
|
-
name: 'Card',
|
|
153
|
-
implementation: fromCSF('./Card.stories.tsx'),
|
|
154
|
-
});
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
`Meta` and `StoryObj` are exported from `handoff-app/react`, so a story file type-checks without a
|
|
158
|
-
Storybook dependency. A project that already has Storybook keeps using its own types.
|
|
159
|
-
|
|
160
|
-
```tsx
|
|
161
|
-
// components/card/Card.stories.tsx
|
|
162
|
-
import { type Meta, type StoryObj } from 'handoff-app/react';
|
|
163
|
-
import Card from './Card';
|
|
164
|
-
|
|
165
|
-
const meta = { component: Card } satisfies Meta<typeof Card>;
|
|
166
|
-
export default meta;
|
|
167
|
-
|
|
168
|
-
export const Primary: StoryObj<typeof meta> = {
|
|
169
|
-
args: { title: 'Example' },
|
|
170
|
-
};
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
A CSF preview is rendered on the server and is not hydrated. A direct React implementation is
|
|
174
|
-
hydrated in the browser.
|
|
175
|
-
|
|
176
|
-
### Compositions
|
|
177
|
-
|
|
178
|
-
A composition references other items by stable id. A named preview can be selected and its arguments
|
|
179
|
-
can be overridden by each reference. A composition needs no renderer, so it is declared with
|
|
180
|
-
`defineCatalogItem` from `handoff-app/pattern`.
|
|
485
|
+
**Compositions.** A composition references other items by stable id. Each
|
|
486
|
+
reference can select a named preview and override its arguments. A composition
|
|
487
|
+
needs no renderer, so it uses `defineCatalogItem` from `handoff-app/pattern`:
|
|
181
488
|
|
|
182
489
|
```ts
|
|
183
490
|
// patterns/example/example.handoff.ts
|
|
@@ -195,233 +502,353 @@ export default defineCatalogItem({
|
|
|
195
502
|
});
|
|
196
503
|
```
|
|
197
504
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
both.
|
|
505
|
+
**Ids.** The `id` addresses the item everywhere: its artifacts, its
|
|
506
|
+
documentation URL, and the `publish` and `checkout` commands. Components and
|
|
507
|
+
patterns share one namespace, so each id must be unique across both. If two
|
|
508
|
+
declarations use one id, Handoff keeps the first, skips the second, and names
|
|
509
|
+
both files in a warning. `publish` does not run until each id is unique.
|
|
204
510
|
|
|
205
|
-
|
|
206
|
-
|
|
511
|
+
Terms such as atom, element, and block are optional classification metadata on
|
|
512
|
+
`type` and `categories`.
|
|
207
513
|
|
|
514
|
+
> **Recipes:** [React preview with a render function](RECIPES.md#react-preview-with-a-render-function) ·
|
|
515
|
+
> [Handlebars item](RECIPES.md#handlebars-item) · [CSF stories](RECIPES.md#csf-stories) ·
|
|
516
|
+
> [Composition with preview overrides](RECIPES.md#composition-with-preview-overrides)
|
|
208
517
|
|
|
209
518
|
### Registration
|
|
210
519
|
|
|
211
|
-
|
|
212
|
-
|
|
520
|
+
`catalog.include` in the config registers the catalog directories. Each path is
|
|
521
|
+
one of these:
|
|
213
522
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
import { defineConfig } from 'handoff-app';
|
|
523
|
+
- An item directory.
|
|
524
|
+
- A collection directory. Each subdirectory is an item.
|
|
217
525
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
runtime: {
|
|
223
|
-
workspace: {
|
|
224
|
-
declarationFormat: 'ts',
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
});
|
|
526
|
+
```ts
|
|
527
|
+
catalog: {
|
|
528
|
+
include: ['components', 'patterns'],
|
|
529
|
+
},
|
|
228
530
|
```
|
|
229
531
|
|
|
230
|
-
|
|
532
|
+
`npx handoff-app make:component <name>` creates a Handlebars item and adds its
|
|
533
|
+
directory to `catalog.include` when necessary.
|
|
231
534
|
|
|
232
|
-
|
|
233
|
-
for breaking changes, migration steps, and upgrade verification.
|
|
535
|
+
### Pages
|
|
234
536
|
|
|
235
|
-
Custom documentation pages are Markdown files under `pages/`.
|
|
236
|
-
|
|
537
|
+
Custom documentation pages are Markdown files under `pages/`. The relative
|
|
538
|
+
path of a page is its route and its registry id.
|
|
539
|
+
`npx handoff-app make:page <name> [parent]` creates a page.
|
|
540
|
+
`npx handoff-app eject:pages` copies the default pages so that you can change
|
|
541
|
+
them.
|
|
237
542
|
|
|
238
|
-
|
|
543
|
+
### Figma foundations
|
|
239
544
|
|
|
240
|
-
Figma integration is optional.
|
|
241
|
-
`init`,
|
|
242
|
-
run directly.
|
|
545
|
+
The Figma integration is optional. If you enter Figma credentials during
|
|
546
|
+
`init`, the wizard writes them to `.env`, and `fetch` is ready to run.
|
|
243
547
|
|
|
244
|
-
|
|
245
|
-
be added or updated in `.env`:
|
|
548
|
+
To add or change the Figma source, set these values in `.env`:
|
|
246
549
|
|
|
247
|
-
```
|
|
550
|
+
```dotenv
|
|
248
551
|
HANDOFF_FIGMA_PROJECT_ID=figma-file-id
|
|
249
552
|
HANDOFF_DEV_ACCESS_TOKEN=figma-personal-access-token
|
|
250
553
|
```
|
|
251
554
|
|
|
252
|
-
The
|
|
253
|
-
|
|
555
|
+
The personal access token must have the `file_content:read` and
|
|
556
|
+
`library_content:read` scopes. Fetch the library:
|
|
254
557
|
|
|
255
558
|
```bash
|
|
256
559
|
npm run fetch
|
|
257
560
|
```
|
|
258
561
|
|
|
259
|
-
|
|
260
|
-
|
|
562
|
+
`fetch` writes tokens, CSS, Sass, and the available assets to `exported/`.
|
|
563
|
+
Commit `exported/` so that local builds and CI builds use the same inputs. To
|
|
564
|
+
make sure that the fetch worked, look for `exported/tokens.json`. An empty
|
|
565
|
+
foundation page does not prove that the fetch worked.
|
|
566
|
+
|
|
567
|
+
If a value is empty, `fetch` asks for it in the terminal. It can save the value
|
|
568
|
+
to `.env`, or to `.env.<profile>` when a profile is selected. Without a
|
|
569
|
+
terminal, for example in CI, `fetch` stops with an error that names the
|
|
570
|
+
missing variable.
|
|
571
|
+
|
|
572
|
+
After a design change, publish the Figma library again. Then run `fetch` again
|
|
573
|
+
to get the latest foundations.
|
|
261
574
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
575
|
+
These variable names are the defaults of `integrations.figma`. Set the block
|
|
576
|
+
only to use different variable names, to commit the file ID, or to change the
|
|
577
|
+
file for a profile. The access token accepts only an environment reference:
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
integrations: {
|
|
581
|
+
figma: {
|
|
582
|
+
projectId: fromEnv('HANDOFF_FIGMA_PROJECT_ID', { default: null }),
|
|
583
|
+
accessToken: fromEnv('HANDOFF_DEV_ACCESS_TOKEN', { default: null }),
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
```
|
|
267
587
|
|
|
268
|
-
##
|
|
588
|
+
## Running the workspace
|
|
269
589
|
|
|
270
|
-
|
|
271
|
-
workspace files. For day-to-day authoring, the documentation server and file
|
|
272
|
-
watchers are started with:
|
|
590
|
+
Start the documentation server and the Handoff file watchers:
|
|
273
591
|
|
|
274
592
|
```bash
|
|
275
593
|
npm run start
|
|
276
594
|
```
|
|
277
595
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
596
|
+
While the command runs, Handoff rebuilds changed files and updates the
|
|
597
|
+
documentation. If you do not need the file watchers, run `npm run dev`
|
|
598
|
+
instead.
|
|
281
599
|
|
|
282
|
-
|
|
600
|
+
Open http://localhost:3000 and make sure that:
|
|
283
601
|
|
|
284
|
-
-
|
|
285
|
-
-
|
|
286
|
-
- component
|
|
287
|
-
-
|
|
288
|
-
- foundation pages
|
|
602
|
+
- The app shows `Workspace` as the active runtime mode.
|
|
603
|
+
- The component list shows your components.
|
|
604
|
+
- Each component page renders all of its previews.
|
|
605
|
+
- Each pattern renders the components that it references.
|
|
606
|
+
- The foundation pages show the fetched values.
|
|
289
607
|
|
|
290
|
-
##
|
|
608
|
+
## Building
|
|
291
609
|
|
|
292
|
-
|
|
610
|
+
**Static site.** Build the documentation as a static site:
|
|
293
611
|
|
|
294
612
|
```bash
|
|
295
613
|
npm run build
|
|
296
614
|
```
|
|
297
615
|
|
|
298
|
-
|
|
299
|
-
static
|
|
616
|
+
The output goes to `out/static`. A static file server or a CDN can serve it. A
|
|
617
|
+
static site has no API routes. Thus it has no MCP endpoint and no AI
|
|
618
|
+
assistant.
|
|
300
619
|
|
|
301
|
-
|
|
620
|
+
**Build cache.** Handoff does not build a component again if its files did not
|
|
621
|
+
change since the last build. The cache in `.handoff/.cache/` records the files
|
|
622
|
+
of each component and every file that its last build read, Sass partials
|
|
623
|
+
included. The lockfile covers files under `node_modules`. The cache does not
|
|
624
|
+
track assets that a stylesheet loads through `url()`. Build every component
|
|
625
|
+
again:
|
|
302
626
|
|
|
303
627
|
```bash
|
|
304
|
-
npm run build -- --
|
|
628
|
+
npm run build -- --force
|
|
305
629
|
```
|
|
306
630
|
|
|
307
|
-
|
|
308
|
-
command. The database-backed Next.js bundle is written to `out/registry`, and
|
|
309
|
-
workspace source is not compiled or served. A Vercel Build Output bundle can be
|
|
310
|
-
produced for either target by adding `--package vercel`.
|
|
631
|
+
**Registry server.** Build a standalone registry application:
|
|
311
632
|
|
|
312
|
-
|
|
633
|
+
```bash
|
|
634
|
+
npm run build -- --target registry
|
|
635
|
+
```
|
|
313
636
|
|
|
314
|
-
|
|
637
|
+
The command sets registry mode in the generated application. It writes the
|
|
638
|
+
Next.js server bundle to `out/registry`. It does not compile or serve the
|
|
639
|
+
workspace source. The [Registry](#registry) section explains how to deploy it.
|
|
315
640
|
|
|
316
|
-
|
|
317
|
-
|
|
641
|
+
**Vercel.** Add `--package vercel` to either target to make a Vercel Build
|
|
642
|
+
Output bundle in `.vercel/output`:
|
|
318
643
|
|
|
319
644
|
```bash
|
|
320
|
-
|
|
321
|
-
npm run db:migrate
|
|
645
|
+
npm run build -- --target registry --package vercel
|
|
322
646
|
```
|
|
323
647
|
|
|
324
|
-
|
|
325
|
-
server startup, and browser installer.
|
|
648
|
+
## Registry
|
|
326
649
|
|
|
327
|
-
|
|
650
|
+
A registry is a deployed catalog with a PostgreSQL database. It serves only
|
|
651
|
+
the records that a workspace publishes. It never reads the workspace
|
|
652
|
+
directly.
|
|
328
653
|
|
|
329
|
-
|
|
654
|
+
### Deploy a registry
|
|
330
655
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
HOSTNAME=0.0.0.0
|
|
334
|
-
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public"
|
|
335
|
-
AUTH_SECRET="replace-with-at-least-32-random-characters"
|
|
336
|
-
AUTH_URL="http://localhost:4000"
|
|
337
|
-
```
|
|
656
|
+
1. Apply the database migrations from the workspace or a CI checkout, where the
|
|
657
|
+
project config is available:
|
|
338
658
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
659
|
+
```bash
|
|
660
|
+
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public" \
|
|
661
|
+
npm run db:migrate
|
|
662
|
+
```
|
|
343
663
|
|
|
344
|
-
|
|
664
|
+
Migrations are a controlled release step. The build, the server start, and
|
|
665
|
+
the installer never run them. Run `db:migrate` again each time you upgrade
|
|
666
|
+
`handoff-app`, before you deploy the new registry build.
|
|
345
667
|
|
|
346
|
-
|
|
347
|
-
|
|
668
|
+
2. Select the [database driver](#database), the [asset storage](#asset-storage),
|
|
669
|
+
and the [email provider](#email). The build bakes these choices, so set them
|
|
670
|
+
before you build. The defaults work without more configuration.
|
|
348
671
|
|
|
349
|
-
|
|
672
|
+
3. Build the registry:
|
|
350
673
|
|
|
351
|
-
|
|
674
|
+
```bash
|
|
675
|
+
npm run build -- --target registry
|
|
676
|
+
```
|
|
352
677
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
```
|
|
678
|
+
4. Start `out/registry/server.js` with these environment variables:
|
|
679
|
+
|
|
680
|
+
```dotenv
|
|
681
|
+
PORT=4000
|
|
682
|
+
HOSTNAME=0.0.0.0
|
|
683
|
+
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public"
|
|
684
|
+
AUTH_SECRET="replace-with-at-least-32-random-characters"
|
|
685
|
+
AUTH_URL="http://localhost:4000"
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
`DATABASE_URL`, `AUTH_SECRET`, and `AUTH_URL` are required. The standalone
|
|
689
|
+
server also reads `PORT` and `HOSTNAME`. For Vercel, build with
|
|
690
|
+
`--package vercel` and set these variables in the deployment environment.
|
|
356
691
|
|
|
357
|
-
|
|
358
|
-
|
|
692
|
+
5. Open http://localhost:4000/install and create the first administrator. The
|
|
693
|
+
installer examines the deployment, but it never runs migrations.
|
|
359
694
|
|
|
360
|
-
|
|
695
|
+
CAUTION: Complete the installation immediately after the deployment. The first
|
|
696
|
+
visitor can claim a registry that is not installed.
|
|
361
697
|
|
|
362
|
-
|
|
698
|
+
> **Recipes:** [Local registry](RECIPES.md#local-registry) ·
|
|
699
|
+
> [Vercel and Neon](RECIPES.md#vercel-and-neon)
|
|
700
|
+
|
|
701
|
+
### Database
|
|
702
|
+
|
|
703
|
+
PostgreSQL is the supported database. `driver` selects how the registry
|
|
704
|
+
connects. The default is `pg`. Use `neon` for Neon PostgreSQL:
|
|
363
705
|
|
|
364
706
|
```ts
|
|
365
707
|
runtime: {
|
|
366
708
|
registry: {
|
|
367
|
-
databaseUrl: fromEnv('DATABASE_URL'),
|
|
368
709
|
database: {
|
|
710
|
+
url: fromEnv('DATABASE_URL'),
|
|
369
711
|
driver: 'neon',
|
|
370
712
|
},
|
|
371
713
|
},
|
|
372
714
|
},
|
|
373
715
|
```
|
|
374
716
|
|
|
375
|
-
|
|
376
|
-
same explicit database migration step is required before deployment.
|
|
717
|
+
Both drivers use the same schema and the same `db:migrate` step.
|
|
377
718
|
|
|
378
|
-
|
|
719
|
+
### Asset storage
|
|
379
720
|
|
|
380
|
-
|
|
381
|
-
|
|
721
|
+
By default, the registry stores asset blobs in PostgreSQL. Each blob can be at
|
|
722
|
+
most 4 MB. `assetStorage.maxInlineBytes` changes this limit.
|
|
723
|
+
|
|
724
|
+
A [custom server module](#custom-server-modules) can store blobs in a
|
|
725
|
+
different service:
|
|
382
726
|
|
|
383
727
|
```ts
|
|
384
728
|
runtime: {
|
|
385
729
|
registry: {
|
|
386
730
|
assetStorage: {
|
|
387
|
-
|
|
731
|
+
provider: 'custom',
|
|
732
|
+
module: './server/storage/s3.mjs',
|
|
733
|
+
options: { bucket: 'handoff-assets', secretKey: fromEnv('S3_SECRET_KEY') },
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
The module default-exports a `defineStorageProvider()` result with these
|
|
740
|
+
methods:
|
|
741
|
+
|
|
742
|
+
- `put` stores a blob and returns the `storageRef` that the registry records.
|
|
743
|
+
- `get` returns a blob by its `storageRef`, as `{ kind: 'stream', stream }`,
|
|
744
|
+
`{ kind: 'bytes', bytes }`, or `{ kind: 'redirect', url }`.
|
|
745
|
+
- `delete` removes a blob by its `storageRef`.
|
|
746
|
+
- `createUpload` is optional. It enables direct uploads.
|
|
747
|
+
|
|
748
|
+
**Upload paths.** `publish` uploads only the blobs that the registry does not
|
|
749
|
+
have. The active provider decides which of two paths each blob takes. No
|
|
750
|
+
setting changes the path.
|
|
751
|
+
|
|
752
|
+
| Path | When | Hash check | Size limit |
|
|
753
|
+
| --- | --- | --- | --- |
|
|
754
|
+
| Through the registry | The `database` provider, or a custom provider without `createUpload` | The registry | The request limit of the host, about 4.5 MB on Vercel. The `database` provider also applies `maxInlineBytes`. |
|
|
755
|
+
| Direct to storage | A custom provider with `createUpload` | Storage, through the signed URL | The limit of the storage |
|
|
756
|
+
|
|
757
|
+
For a direct upload, the registry gives the CLI a signed URL, and the CLI sends
|
|
758
|
+
the blob to storage. The registry records the blob only after `get` finds the
|
|
759
|
+
stored object.
|
|
760
|
+
|
|
761
|
+
- The machine that runs `publish` must be able to connect to the storage URL.
|
|
762
|
+
- If a direct upload fails, `publish` stops. It does not try again through the
|
|
763
|
+
registry.
|
|
764
|
+
- The signed URL must make storage reject bytes that do not match `hash`.
|
|
765
|
+
- The registry does not delete an uploaded object that it never records.
|
|
766
|
+
|
|
767
|
+
**Download paths.** The result of `get` controls how a reader receives a blob.
|
|
768
|
+
With `{ kind: 'redirect' }`, the registry sends the reader to storage. With
|
|
769
|
+
`bytes` or `stream`, the registry sends the blob itself.
|
|
770
|
+
|
|
771
|
+
**Changing the provider.** The registry records which provider stores each blob.
|
|
772
|
+
Blobs in PostgreSQL stay readable after you select a custom provider. A publish
|
|
773
|
+
does not move blobs. If you change the custom provider or its storage, copy the
|
|
774
|
+
stored objects first. The new provider must find each object by the
|
|
775
|
+
`storageRef` that the previous provider returned. After a change back to
|
|
776
|
+
`database`, the registry cannot read the blobs of the custom provider.
|
|
777
|
+
|
|
778
|
+
> **Recipes:** [S3-compatible storage](RECIPES.md#s3-compatible-storage) ·
|
|
779
|
+
> [Vercel Blob](RECIPES.md#vercel-blob)
|
|
780
|
+
|
|
781
|
+
### Email
|
|
782
|
+
|
|
783
|
+
The registry sends invitation and password-reset emails when `email.from` is
|
|
784
|
+
set. Without it, an administrator sees each invitation link one time and sends
|
|
785
|
+
it manually, and password reset is not available.
|
|
786
|
+
|
|
787
|
+
```ts
|
|
788
|
+
runtime: {
|
|
789
|
+
registry: {
|
|
790
|
+
email: {
|
|
791
|
+
from: 'Handoff <no-reply@example.com>',
|
|
792
|
+
provider: 'smtp',
|
|
793
|
+
options: { host: fromEnv('SMTP_HOST') },
|
|
388
794
|
},
|
|
389
795
|
},
|
|
390
796
|
},
|
|
391
797
|
```
|
|
392
798
|
|
|
393
|
-
|
|
394
|
-
|
|
799
|
+
`provider` selects the delivery service. It is required when you set `email`.
|
|
800
|
+
|
|
801
|
+
- `smtp` sends through an SMTP server, for example Amazon SES, SendGrid, or
|
|
802
|
+
Microsoft 365.
|
|
803
|
+
- `custom` names a [custom server module](#custom-server-modules) that
|
|
804
|
+
default-exports `defineEmailProvider()`. Use it for an HTTP API, for example
|
|
805
|
+
Resend or Postmark.
|
|
806
|
+
|
|
807
|
+
`apiKey` and `password` must use `fromEnv()`. A profile can set a different
|
|
808
|
+
sender or provider. When a profile changes the provider, it replaces
|
|
809
|
+
`options`.
|
|
810
|
+
|
|
811
|
+
> **Recipes:** [SMTP](RECIPES.md#smtp) · [Resend](RECIPES.md#resend) ·
|
|
812
|
+
> [Custom email provider](RECIPES.md#custom-email-provider)
|
|
395
813
|
|
|
396
|
-
###
|
|
814
|
+
### Custom server modules
|
|
397
815
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
migrations are never run by it.
|
|
816
|
+
Asset storage, email, and AI connections can name a custom server module. Each
|
|
817
|
+
module must obey these rules:
|
|
401
818
|
|
|
402
|
-
|
|
403
|
-
|
|
819
|
+
- The path is relative to the working directory.
|
|
820
|
+
- The file is `.js` or `.mjs`.
|
|
821
|
+
- The default export is the object, or a factory that returns it.
|
|
822
|
+
- The module imports its define helper from `handoff-app/providers`. The registry
|
|
823
|
+
build stops when a module imports `handoff-app`.
|
|
824
|
+
- The project installs the packages that the module imports. The registry
|
|
825
|
+
build copies them into the bundle.
|
|
404
826
|
|
|
405
|
-
|
|
827
|
+
The factory gets `options` with each `fromEnv()` value resolved at request
|
|
828
|
+
time.
|
|
406
829
|
|
|
407
|
-
|
|
408
|
-
are two ways to supply them. Both use the same kind of token, and both are
|
|
409
|
-
listed and revoked under Account → Access tokens in the registry.
|
|
830
|
+
## Connecting a workspace to a registry
|
|
410
831
|
|
|
411
|
-
|
|
832
|
+
A connected workspace publishes content to a registry and checks content out
|
|
833
|
+
from it. It needs a registry URL and an access token. You can supply them with
|
|
834
|
+
a [device login](#device-login) or with [environment variables](#access-token).
|
|
835
|
+
Both use the same kind of token. You can see and revoke tokens under
|
|
836
|
+
Account → Access tokens in the registry.
|
|
412
837
|
|
|
413
|
-
|
|
838
|
+
### Device login
|
|
839
|
+
|
|
840
|
+
Start the authorization from the workspace:
|
|
414
841
|
|
|
415
842
|
```bash
|
|
416
843
|
npm run login -- --url http://localhost:4000
|
|
417
844
|
```
|
|
418
845
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
846
|
+
In the browser, sign in to the registry, enter the device code, and approve the
|
|
847
|
+
CLI. The CLI saves the credential in `.handoff/cli-auth.json` for that exact
|
|
848
|
+
registry URL.
|
|
422
849
|
|
|
423
|
-
|
|
424
|
-
several registries
|
|
850
|
+
The CLI saves one credential for each profile. Thus a workspace can stay signed
|
|
851
|
+
in to several registries:
|
|
425
852
|
|
|
426
853
|
```bash
|
|
427
854
|
npm run login -- --profile staging --url https://staging.example.com
|
|
@@ -430,44 +857,55 @@ npm run publish -- all --profile staging
|
|
|
430
857
|
|
|
431
858
|
A login profile name is free. It needs no `handoff.config.<name>.*` file, and
|
|
432
859
|
`login` is the only command that accepts a new name. A profile without its own
|
|
433
|
-
login
|
|
434
|
-
|
|
860
|
+
login uses the default login. Thus one `npm run login` is enough for a
|
|
861
|
+
workspace with one registry.
|
|
435
862
|
|
|
436
|
-
`npm run logout` revokes and removes the login of the selected profile.
|
|
437
|
-
`--all`
|
|
863
|
+
`npm run logout` revokes and removes the login of the selected profile.
|
|
864
|
+
`--all` removes every saved login.
|
|
438
865
|
|
|
439
|
-
|
|
866
|
+
### Access token
|
|
440
867
|
|
|
441
|
-
|
|
442
|
-
write access
|
|
443
|
-
token and the registry URL
|
|
868
|
+
Create a token in the registry under Account → Access tokens. To publish, the
|
|
869
|
+
token needs read and write access. To check out, read access is enough. Set the
|
|
870
|
+
token and the registry URL in `.env`:
|
|
444
871
|
|
|
445
872
|
```dotenv
|
|
446
873
|
HANDOFF_REGISTRY_URL=http://localhost:4000
|
|
447
874
|
HANDOFF_REGISTRY_ACCESS_TOKEN=hnd_...
|
|
448
875
|
```
|
|
449
876
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
`.env
|
|
453
|
-
|
|
454
|
-
|
|
877
|
+
These two variables are the defaults of `runtime.registryConnection`, so no
|
|
878
|
+
config entry is necessary. A selected profile reads `.env.<profile>` on top of
|
|
879
|
+
`.env`. Thus each profile can address a different registry. In CI, the job
|
|
880
|
+
environment supplies the same two variables.
|
|
881
|
+
|
|
882
|
+
Environment values have priority over a saved device login. Thus a CI job gives
|
|
883
|
+
the same result on a computer where a developer is signed in. A token works
|
|
884
|
+
only for the registry URL that issued it. If the environment names a different
|
|
885
|
+
registry than the login of the selected profile, `publish` and `checkout`
|
|
886
|
+
report which login they skipped and why.
|
|
887
|
+
|
|
888
|
+
A config block is necessary only to keep the URL in the repository, or to read
|
|
889
|
+
the values from variables with different names:
|
|
455
890
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
registry
|
|
460
|
-
|
|
891
|
+
```ts
|
|
892
|
+
runtime: {
|
|
893
|
+
registryConnection: {
|
|
894
|
+
url: 'https://registry.example.com',
|
|
895
|
+
accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
```
|
|
461
899
|
|
|
462
|
-
###
|
|
900
|
+
### Publishing
|
|
463
901
|
|
|
464
|
-
|
|
902
|
+
Publish every kind of content in dependency order:
|
|
465
903
|
|
|
466
904
|
```bash
|
|
467
905
|
npm run publish -- all
|
|
468
906
|
```
|
|
469
907
|
|
|
470
|
-
|
|
908
|
+
Publish one kind:
|
|
471
909
|
|
|
472
910
|
```bash
|
|
473
911
|
npm run publish -- catalog
|
|
@@ -476,201 +914,186 @@ npm run publish -- tokens
|
|
|
476
914
|
npm run publish -- assets
|
|
477
915
|
```
|
|
478
916
|
|
|
479
|
-
`catalog` covers every catalog item.
|
|
480
|
-
|
|
481
|
-
|
|
917
|
+
`catalog` covers every catalog item. The declaration of each item decides if
|
|
918
|
+
the registry stores it as a component or as a pattern. Thus the command names
|
|
919
|
+
items, not component or pattern kinds.
|
|
482
920
|
|
|
483
|
-
|
|
484
|
-
|
|
921
|
+
`tokens` and `assets` run the Figma pipeline before the upload. Thus the
|
|
922
|
+
[Figma credentials](#figma-foundations) must be available.
|
|
485
923
|
|
|
486
|
-
|
|
924
|
+
Add one or more ids to publish only those entities:
|
|
487
925
|
|
|
488
926
|
```bash
|
|
489
927
|
npm run publish -- catalog item-id another-id
|
|
490
928
|
```
|
|
491
929
|
|
|
492
|
-
`--dry-run`
|
|
493
|
-
it needs
|
|
494
|
-
|
|
495
|
-
the existing output,
|
|
930
|
+
`--dry-run` lists the content that a real publish uploads. It does not connect
|
|
931
|
+
to a registry. Thus it needs no registry URL and no token. It still runs the build,
|
|
932
|
+
which updates the generated output on disk. `--no-build` skips the build and
|
|
933
|
+
publishes the existing output. Together, the two options do not change the
|
|
934
|
+
workspace:
|
|
496
935
|
|
|
497
936
|
```bash
|
|
498
937
|
npm run publish -- all --dry-run
|
|
499
938
|
npm run publish -- catalog --no-build
|
|
500
939
|
```
|
|
501
940
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
and writes nothing.
|
|
941
|
+
After a publish, reload the registry to see the published items and
|
|
942
|
+
foundations.
|
|
505
943
|
|
|
506
|
-
|
|
507
|
-
visible. Published database records are read by registry pages; the local
|
|
508
|
-
workspace is never read directly.
|
|
944
|
+
### Checkout
|
|
509
945
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
[Workspace authorization](#4-workspace-authorization). A connection block in
|
|
513
|
-
`handoff.config.ts` is needed only to pin the URL in the repository, or to read
|
|
514
|
-
the values from differently named variables (`fromEnv` is imported from
|
|
515
|
-
`handoff-app`):
|
|
946
|
+
`checkout` writes registry content into the workspace. It takes the same
|
|
947
|
+
`all`, kind, id, and `--dry-run` forms as `publish`:
|
|
516
948
|
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
|
|
522
|
-
},
|
|
523
|
-
},
|
|
949
|
+
```bash
|
|
950
|
+
npm run checkout -- all
|
|
951
|
+
npm run checkout -- catalog item-id
|
|
952
|
+
npm run checkout -- all --dry-run
|
|
524
953
|
```
|
|
525
954
|
|
|
955
|
+
A dry-run checkout reads from the registry and lists the files that a real
|
|
956
|
+
checkout creates or overwrites. It writes nothing. Checkout writes declarations in the
|
|
957
|
+
format that `runtime.workspace.declarationFormat` sets.
|
|
958
|
+
|
|
959
|
+
> **Recipes:** [One workspace, several registries](RECIPES.md#one-workspace-several-registries) ·
|
|
960
|
+
> [Publish from GitHub Actions](RECIPES.md#publish-from-github-actions)
|
|
961
|
+
|
|
526
962
|
## MCP
|
|
527
963
|
|
|
528
|
-
The documentation app serves a Model Context Protocol endpoint at `/api/mcp
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
sets `trailingSlash: true`, so `/api/mcp` answers with a 308 redirect.
|
|
964
|
+
The documentation app serves a Model Context Protocol endpoint at `/api/mcp/`.
|
|
965
|
+
Coding agents use it to find the pages, components, and tokens that exist. Thus
|
|
966
|
+
they do not invent markup and values. The endpoint uses stateless Streamable
|
|
967
|
+
HTTP over `POST`. There is nothing to start and no extra port.
|
|
533
968
|
|
|
534
969
|
| Target | Endpoint | Credential |
|
|
535
970
|
| --- | --- | --- |
|
|
536
|
-
| Workspace (`dev` / `start`) | `http://localhost:3000/api/mcp/` |
|
|
537
|
-
| Registry (standalone or Vercel) | `<registry url>/api/mcp/` |
|
|
538
|
-
| Static
|
|
971
|
+
| Workspace (`dev` / `start`) | `http://localhost:3000/api/mcp/` | None, as with `/api/docs/*` |
|
|
972
|
+
| Registry (standalone or Vercel) | `<registry url>/api/mcp/` | An access token. Read access is enough. |
|
|
973
|
+
| Static site | Not available | Not applicable |
|
|
974
|
+
|
|
975
|
+
Keep the trailing slash. The app sets `trailingSlash: true`, so `/api/mcp`
|
|
976
|
+
answers with a 308 redirect. Registry tokens come from `handoff-app login` or
|
|
977
|
+
from Account → Access tokens in the registry.
|
|
978
|
+
|
|
979
|
+
The documentation app shows its endpoint and a ready-made client config for
|
|
980
|
+
Claude Code, Cursor, and VS Code behind the plug icon in the header.
|
|
981
|
+
|
|
982
|
+
MCP is on by default. To turn it off, set `runtime.mcp: false`. The build bakes
|
|
983
|
+
this value. A build with MCP off answers 404 on the route, leaves the MCP SDK
|
|
984
|
+
out of the bundle, and shows no connect control.
|
|
539
985
|
|
|
540
|
-
|
|
541
|
-
|
|
986
|
+
> **Recipes:** [Connect a client by hand](RECIPES.md#connect-a-client-by-hand) ·
|
|
987
|
+
> [Turn off MCP](RECIPES.md#turn-off-mcp)
|
|
542
988
|
|
|
543
|
-
|
|
989
|
+
## AI assistant
|
|
990
|
+
|
|
991
|
+
The documentation app can answer questions about the design system. The
|
|
992
|
+
assistant reads through the same MCP tools. Thus each answer comes from the
|
|
993
|
+
catalog and links to the pages and components that it read. Open the assistant
|
|
994
|
+
from the search control in the header, or with `⌘K`.
|
|
995
|
+
|
|
996
|
+
The `ai` block turns on the assistant, and the deployment selects the
|
|
997
|
+
provider:
|
|
544
998
|
|
|
545
999
|
```ts
|
|
546
1000
|
runtime: {
|
|
547
|
-
|
|
1001
|
+
ai: {
|
|
1002
|
+
connections: [
|
|
1003
|
+
{
|
|
1004
|
+
id: 'gateway',
|
|
1005
|
+
label: 'Acme LiteLLM',
|
|
1006
|
+
baseUrl: 'https://llm.acme.internal/v1',
|
|
1007
|
+
apiKey: fromEnv('LITELLM_API_KEY'),
|
|
1008
|
+
models: ['gpt-4o', 'claude-sonnet-4-5'],
|
|
1009
|
+
},
|
|
1010
|
+
],
|
|
1011
|
+
defaultModel: 'gateway/claude-sonnet-4-5',
|
|
1012
|
+
},
|
|
548
1013
|
},
|
|
549
1014
|
```
|
|
550
1015
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
drops the MCP SDK from the packaged bundle, and shows no connect affordance at all
|
|
554
|
-
rather than one that fails.
|
|
1016
|
+
`ai: { enabled: false }` turns off the assistant. The build bakes this flag.
|
|
1017
|
+
A build without the assistant serves no chat route and shows no control.
|
|
555
1018
|
|
|
556
|
-
|
|
557
|
-
for Claude Code, Cursor and VS Code behind the plug icon in the header. By hand:
|
|
1019
|
+
Each connection uses one of these credentials:
|
|
558
1020
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
"type": "http",
|
|
564
|
-
"url": "https://registry.example.com/api/mcp/",
|
|
565
|
-
"headers": { "Authorization": "Bearer hnd_..." }
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
```
|
|
1021
|
+
- **Service key:** `apiKey` names a deployment key. The deployment pays for
|
|
1022
|
+
every reader.
|
|
1023
|
+
- **No credential:** the endpoint needs no key, for example a local Ollama.
|
|
1024
|
+
- **Reader key:** `credential: 'user'` asks each reader for their own key.
|
|
570
1025
|
|
|
571
|
-
|
|
1026
|
+
**The declared connections are the full surface.** A reader can add a key only
|
|
1027
|
+
for a connection that the config declares. Thus the server never calls a URL
|
|
1028
|
+
that a reader selected. Without a `credential: 'user'` connection, readers have
|
|
1029
|
+
no AI setting.
|
|
572
1030
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
1031
|
+
Each connection uses the OpenAI-compatible `/chat/completions` API. This does
|
|
1032
|
+
not limit the models. Ollama serves an OpenAI-compatible API at `/v1`. LiteLLM,
|
|
1033
|
+
OpenRouter, vLLM, LM Studio, and Azure OpenAI each give access to Anthropic,
|
|
1034
|
+
Google, and xAI models. For a provider without this API, name a
|
|
1035
|
+
[custom server module](#custom-server-modules) that default-exports
|
|
1036
|
+
`defineAiProvider()`, instead of `baseUrl`.
|
|
577
1037
|
|
|
578
|
-
|
|
1038
|
+
The gateway controls cost. LiteLLM and OpenRouter both apply budgets and rate
|
|
1039
|
+
limits for each key. The assistant limits only the number of steps for one
|
|
1040
|
+
question.
|
|
579
1041
|
|
|
580
|
-
|
|
581
|
-
| --- | --- |
|
|
582
|
-
| `HANDOFF_PROFILE` | Config profile merged onto the base config |
|
|
583
|
-
| `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
|
|
584
|
-
| `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
|
|
585
|
-
| `HANDOFF_REGISTRY_URL` | Connected workspace registry URL |
|
|
586
|
-
| `HANDOFF_REGISTRY_ACCESS_TOKEN` | Registry access token used by a connected workspace |
|
|
587
|
-
| `HANDOFF_SYNC_SECRET` | Optional deployment-wide registry credential |
|
|
588
|
-
| `DATABASE_URL` | Registry PostgreSQL connection string |
|
|
589
|
-
| `AUTH_SECRET` | Registry session-signing secret, at least 32 characters |
|
|
590
|
-
| `AUTH_URL` | Canonical public registry URL |
|
|
591
|
-
| `PORT` | Standalone registry server port |
|
|
592
|
-
| `HOSTNAME` | Standalone registry bind hostname |
|
|
593
|
-
| `BLOB_READ_WRITE_TOKEN` | Credential for the Vercel Blob asset adapter |
|
|
594
|
-
| `HANDOFF_OUTPUT_DIR` | Override the fetched output directory |
|
|
595
|
-
| `HANDOFF_SITES_DIR` | Override the build output directory |
|
|
596
|
-
| `HANDOFF_APP_PORT` | Workspace documentation server port |
|
|
597
|
-
| `HANDOFF_WEBSOCKET_PORT` | Workspace live-reload server port |
|
|
598
|
-
|
|
599
|
-
Registry assets are stored in PostgreSQL by default with a 4 MB per-blob limit.
|
|
600
|
-
Vercel Blob or a custom adapter can be selected through
|
|
601
|
-
`runtime.registry.assetStorage`. The `pg` or `neon` connection driver can be
|
|
602
|
-
selected through `runtime.registry.database.driver`; PostgreSQL is used by
|
|
603
|
-
both.
|
|
604
|
-
|
|
605
|
-
## Profiles
|
|
606
|
-
|
|
607
|
-
A profile is a sidecar config file that merges onto the base config. It holds
|
|
608
|
-
only what changes between environments, so a project keeps one shared config
|
|
609
|
-
instead of a second complete copy of it.
|
|
610
|
-
|
|
611
|
-
- The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. It is the
|
|
612
|
-
only config file loaded when no profile is selected.
|
|
613
|
-
- A profile file is `handoff.config.<profile>.*`, with the same four
|
|
614
|
-
extensions.
|
|
615
|
-
- Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
|
|
616
|
-
`--profile` takes precedence over `HANDOFF_PROFILE`.
|
|
617
|
-
- Profile names are not predefined. A name can contain lowercase letters,
|
|
618
|
-
numbers, and hyphens.
|
|
619
|
-
- A selected profile must exist. If no `handoff.config.<name>.*` file is found,
|
|
620
|
-
the command stops with an error.
|
|
621
|
-
- `login`, `logout`, `publish`, and `checkout` also accept a profile that has
|
|
622
|
-
only a saved registry login, because there the profile selects the registry.
|
|
623
|
-
Every other command still needs the config file.
|
|
624
|
-
- Layers resolve as defaults, base config, profile, then programmatic config,
|
|
625
|
-
with the merge rules described under [Configuration](#configuration).
|
|
626
|
-
- A selected profile also reads `.env.<profile>` on top of `.env`, when that
|
|
627
|
-
file exists. Both are read from the directory the command runs in, and a
|
|
628
|
-
variable already set in the environment beats both files.
|
|
1042
|
+
### Reader keys
|
|
629
1043
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1044
|
+
A reader adds a key under Account → AI providers. The registry encrypts keys
|
|
1045
|
+
with `HANDOFF_AI_KEY_SECRET`. It does not hash them, because the server must
|
|
1046
|
+
send them to the provider. Set this variable to a random value of at least 32
|
|
1047
|
+
characters. A key is write-only through the API. After a save, a read reports
|
|
1048
|
+
only that a key exists.
|
|
1049
|
+
|
|
1050
|
+
Reader keys work only in registry mode. A workspace has one user, whose config
|
|
1051
|
+
file and `.env` hold their keys. Thus a workspace supplies a key through
|
|
1052
|
+
`apiKey`.
|
|
633
1053
|
|
|
634
|
-
|
|
635
|
-
profile does not need to set `runtime.mode`. This makes a profile a good place
|
|
636
|
-
for the registry build settings: the database driver, the database
|
|
637
|
-
environment-variable name, the asset storage, and whether MCP is enabled. A
|
|
638
|
-
hosting provider or a CI job selects the profile through `HANDOFF_PROFILE`.
|
|
1054
|
+
### Changing connections without a rebuild
|
|
639
1055
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1056
|
+
The connection list is deployment data, not build shape. Thus a registry can
|
|
1057
|
+
change it without a rebuild. `HANDOFF_AI_CONNECTIONS` holds a JSON array. The
|
|
1058
|
+
registry reads it at request time and merges it over the built-in list by `id`.
|
|
1059
|
+
Each entry names its key variable with `apiKeyEnv`, not the key value.
|
|
643
1060
|
|
|
644
|
-
|
|
645
|
-
|
|
1061
|
+
A connection in this list can use a `module` only if a connection in the config
|
|
1062
|
+
names the same module. The build copies only those modules into the registry.
|
|
646
1063
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1064
|
+
> **Recipes:** [Service key through a gateway](RECIPES.md#service-key-through-a-gateway) ·
|
|
1065
|
+
> [Local Ollama](RECIPES.md#local-ollama) · [Reader keys](RECIPES.md#reader-keys) ·
|
|
1066
|
+
> [Custom AI provider](RECIPES.md#custom-ai-provider) ·
|
|
1067
|
+
> [Change connections without a rebuild](RECIPES.md#change-connections-without-a-rebuild)
|
|
650
1068
|
|
|
651
1069
|
## CLI reference
|
|
652
1070
|
|
|
653
1071
|
| Command | Description |
|
|
654
1072
|
| --- | --- |
|
|
655
|
-
| `npx handoff-app init` |
|
|
656
|
-
| `npm run fetch` |
|
|
657
|
-
| `npm run start` |
|
|
658
|
-
| `npm run dev` |
|
|
659
|
-
| `npm run build -- [--target static\|registry]` |
|
|
660
|
-
| `npm run db:migrate` |
|
|
661
|
-
| `npm run
|
|
662
|
-
| `npm run
|
|
663
|
-
| `npm run
|
|
664
|
-
| `npm run
|
|
665
|
-
| `
|
|
1073
|
+
| `npx handoff-app init` | Create a project with the interactive wizard |
|
|
1074
|
+
| `npm run fetch` | Fetch design tokens and assets from Figma |
|
|
1075
|
+
| `npm run start` | Start the workspace server and the Handoff file watchers |
|
|
1076
|
+
| `npm run dev` | Start the workspace server without the file watchers |
|
|
1077
|
+
| `npm run build -- [--target static\|registry] [--package vercel]` | Build the static site or the registry server |
|
|
1078
|
+
| `npm run db:migrate` | Apply the registry database migrations |
|
|
1079
|
+
| `npm run publish -- <kind\|all> [id...]` | Publish catalog items, pages, tokens, or assets |
|
|
1080
|
+
| `npm run checkout -- <kind\|all> [id...]` | Write published content into the workspace |
|
|
1081
|
+
| `npm run login -- [--profile <name>] --url <url>` | Authorize the CLI through the registry device flow |
|
|
1082
|
+
| `npm run logout -- [--profile <name>] [--all]` | Revoke and remove saved CLI credentials |
|
|
1083
|
+
| `npx handoff-app validate:components` | Validate the catalog items |
|
|
1084
|
+
| `npx handoff-app make:component <name>` | Create a Handlebars catalog item |
|
|
1085
|
+
| `npx handoff-app make:page <name> [parent]` | Create a documentation page |
|
|
1086
|
+
| `npx handoff-app scaffold` | Create catalog item stubs for fetched Figma components |
|
|
1087
|
+
| `npx handoff-app eject:config` | Write the default config to the working directory |
|
|
666
1088
|
|
|
667
1089
|
Every command except `init` accepts `-c, --config`, `--profile`, `-d, --debug`,
|
|
668
|
-
and `-f, --force`. `publish` and `checkout`
|
|
1090
|
+
and `-f, --force`. `publish` and `checkout` also accept `--dry-run`, and
|
|
669
1091
|
`publish` accepts `--no-build`.
|
|
670
1092
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1093
|
+
npm forwards the arguments after `--` to the CLI. To see the options of a
|
|
1094
|
+
command, add `--help` after `--`. [docs/cli.md](docs/cli.md) lists every
|
|
1095
|
+
command and option. [docs/api.md](docs/api.md) documents the JavaScript API and
|
|
1096
|
+
the build hooks.
|
|
674
1097
|
|
|
675
1098
|
## Maintainers
|
|
676
1099
|
|
|
@@ -690,4 +1113,4 @@ Code of Conduct.
|
|
|
690
1113
|
|
|
691
1114
|
## License
|
|
692
1115
|
|
|
693
|
-
[MIT](
|
|
1116
|
+
[MIT](LICENSE.md) ©Convertiv
|