handoff-app 2.0.0-9 → 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 +813 -406
- 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/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 +26 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +1 -1
- 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/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +2 -1
- package/dist/transformers/preview/component/css.js +8 -5
- 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/utils/build.d.ts +5 -0
- package/dist/transformers/utils/build.js +21 -4
- package/dist/types/config.d.ts +129 -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/Preview.tsx +5 -2
- 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/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 +25 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +1 -1
- 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/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +9 -4
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/utils/build.ts +16 -3
- package/src/types/config.ts +142 -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,80 +3,436 @@
|
|
|
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
|
|
129
|
+
```
|
|
130
|
+
|
|
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.
|
|
183
|
+
|
|
184
|
+
To add Handoff in the recommended subdirectory layout:
|
|
185
|
+
|
|
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
|
+
});
|
|
67
269
|
```
|
|
68
270
|
|
|
69
|
-
|
|
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
|
|
70
421
|
|
|
71
|
-
Every documented UI entry is a catalog item.
|
|
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.
|
|
422
|
+
Every documented UI entry is a catalog item. An item declares one of these:
|
|
74
423
|
|
|
75
|
-
|
|
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.
|
|
76
427
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
80
436
|
|
|
81
437
|
```tsx
|
|
82
438
|
// components/example/Component.tsx
|
|
@@ -112,88 +468,23 @@ export const Default = {
|
|
|
112
468
|
} satisfies ComponentPreview;
|
|
113
469
|
```
|
|
114
470
|
|
|
115
|
-
The export name is the preview name.
|
|
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.
|
|
116
475
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
documenting the implementation rather than a wrapper. Name a declaration that holds JSX
|
|
120
|
-
`example.handoff.tsx`.
|
|
476
|
+
**Handlebars.** The implementation is the path to the template:
|
|
477
|
+
`implementation: './Badge.hbs'`, from `handoff-app/handlebars`.
|
|
121
478
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
},
|
|
128
|
-
} satisfies ComponentPreview;
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Handlebars
|
|
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.
|
|
132
484
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
// components/badge/badge.handoff.ts
|
|
138
|
-
import { defineCatalogItem, type Preview } from 'handoff-app/handlebars';
|
|
139
|
-
|
|
140
|
-
const item = defineCatalogItem({
|
|
141
|
-
id: 'badge',
|
|
142
|
-
name: 'Badge',
|
|
143
|
-
implementation: './Badge.hbs',
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
export default item;
|
|
147
|
-
|
|
148
|
-
type BadgePreview = Preview<typeof item>;
|
|
149
|
-
|
|
150
|
-
export const Primary = {
|
|
151
|
-
args: { variant: 'primary', children: 'New' },
|
|
152
|
-
} satisfies BadgePreview;
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### CSF
|
|
156
|
-
|
|
157
|
-
CSF is a source format. A story file uses the renderer from the module that declares it. A React item references an existing CSF file with `fromCSF`.
|
|
158
|
-
The CSF file stays unchanged and defines its previews through named exports. Each story's `args` merge with and override `meta.args`.
|
|
159
|
-
Handoff preserves story names, `argTypes`, and `render` functions.
|
|
160
|
-
It finds the React component through `meta.component`, then documents and publishes it with the story file.
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
// components/card/card.handoff.ts
|
|
164
|
-
import { defineCatalogItem, fromCSF } from 'handoff-app/react';
|
|
165
|
-
|
|
166
|
-
export default defineCatalogItem({
|
|
167
|
-
id: 'card',
|
|
168
|
-
name: 'Card',
|
|
169
|
-
implementation: fromCSF('./Card.stories.tsx'),
|
|
170
|
-
});
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
`Meta` and `StoryObj` are exported from `handoff-app/react`, so a story file type-checks without a
|
|
174
|
-
Storybook dependency. A project that already has Storybook keeps using its own types.
|
|
175
|
-
|
|
176
|
-
```tsx
|
|
177
|
-
// components/card/Card.stories.tsx
|
|
178
|
-
import { type Meta, type StoryObj } from 'handoff-app/react';
|
|
179
|
-
import Card from './Card';
|
|
180
|
-
|
|
181
|
-
const meta = { component: Card } satisfies Meta<typeof Card>;
|
|
182
|
-
export default meta;
|
|
183
|
-
|
|
184
|
-
export const Primary: StoryObj<typeof meta> = {
|
|
185
|
-
args: { title: 'Example' },
|
|
186
|
-
};
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
A CSF preview is rendered on the server and is not hydrated. A direct React implementation is
|
|
190
|
-
hydrated in the browser.
|
|
191
|
-
|
|
192
|
-
### Compositions
|
|
193
|
-
|
|
194
|
-
A composition references other items by stable id. A named preview can be selected and its arguments
|
|
195
|
-
can be overridden by each reference. A composition needs no renderer, so it is declared with
|
|
196
|
-
`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`:
|
|
197
488
|
|
|
198
489
|
```ts
|
|
199
490
|
// patterns/example/example.handoff.ts
|
|
@@ -211,233 +502,353 @@ export default defineCatalogItem({
|
|
|
211
502
|
});
|
|
212
503
|
```
|
|
213
504
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
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.
|
|
220
510
|
|
|
221
|
-
|
|
222
|
-
|
|
511
|
+
Terms such as atom, element, and block are optional classification metadata on
|
|
512
|
+
`type` and `categories`.
|
|
223
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)
|
|
224
517
|
|
|
225
518
|
### Registration
|
|
226
519
|
|
|
227
|
-
|
|
228
|
-
|
|
520
|
+
`catalog.include` in the config registers the catalog directories. Each path is
|
|
521
|
+
one of these:
|
|
229
522
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
import { defineConfig } from 'handoff-app';
|
|
523
|
+
- An item directory.
|
|
524
|
+
- A collection directory. Each subdirectory is an item.
|
|
233
525
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
runtime: {
|
|
239
|
-
workspace: {
|
|
240
|
-
declarationFormat: 'ts',
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
});
|
|
526
|
+
```ts
|
|
527
|
+
catalog: {
|
|
528
|
+
include: ['components', 'patterns'],
|
|
529
|
+
},
|
|
244
530
|
```
|
|
245
531
|
|
|
246
|
-
|
|
532
|
+
`npx handoff-app make:component <name>` creates a Handlebars item and adds its
|
|
533
|
+
directory to `catalog.include` when necessary.
|
|
247
534
|
|
|
248
|
-
|
|
249
|
-
for breaking changes, migration steps, and upgrade verification.
|
|
535
|
+
### Pages
|
|
250
536
|
|
|
251
|
-
Custom documentation pages are Markdown files under `pages/`.
|
|
252
|
-
|
|
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.
|
|
253
542
|
|
|
254
|
-
|
|
543
|
+
### Figma foundations
|
|
255
544
|
|
|
256
|
-
Figma integration is optional.
|
|
257
|
-
`init`,
|
|
258
|
-
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.
|
|
259
547
|
|
|
260
|
-
|
|
261
|
-
be added or updated in `.env`:
|
|
548
|
+
To add or change the Figma source, set these values in `.env`:
|
|
262
549
|
|
|
263
|
-
```
|
|
550
|
+
```dotenv
|
|
264
551
|
HANDOFF_FIGMA_PROJECT_ID=figma-file-id
|
|
265
552
|
HANDOFF_DEV_ACCESS_TOKEN=figma-personal-access-token
|
|
266
553
|
```
|
|
267
554
|
|
|
268
|
-
The
|
|
269
|
-
|
|
555
|
+
The personal access token must have the `file_content:read` and
|
|
556
|
+
`library_content:read` scopes. Fetch the library:
|
|
270
557
|
|
|
271
558
|
```bash
|
|
272
559
|
npm run fetch
|
|
273
560
|
```
|
|
274
561
|
|
|
275
|
-
|
|
276
|
-
|
|
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.
|
|
574
|
+
|
|
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:
|
|
277
578
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
```
|
|
283
587
|
|
|
284
|
-
##
|
|
588
|
+
## Running the workspace
|
|
285
589
|
|
|
286
|
-
|
|
287
|
-
workspace files. For day-to-day authoring, the documentation server and file
|
|
288
|
-
watchers are started with:
|
|
590
|
+
Start the documentation server and the Handoff file watchers:
|
|
289
591
|
|
|
290
592
|
```bash
|
|
291
593
|
npm run start
|
|
292
594
|
```
|
|
293
595
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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.
|
|
297
599
|
|
|
298
|
-
|
|
600
|
+
Open http://localhost:3000 and make sure that:
|
|
299
601
|
|
|
300
|
-
-
|
|
301
|
-
-
|
|
302
|
-
- component
|
|
303
|
-
-
|
|
304
|
-
- 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.
|
|
305
607
|
|
|
306
|
-
##
|
|
608
|
+
## Building
|
|
307
609
|
|
|
308
|
-
|
|
610
|
+
**Static site.** Build the documentation as a static site:
|
|
309
611
|
|
|
310
612
|
```bash
|
|
311
613
|
npm run build
|
|
312
614
|
```
|
|
313
615
|
|
|
314
|
-
|
|
315
|
-
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.
|
|
316
619
|
|
|
317
|
-
|
|
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:
|
|
318
626
|
|
|
319
627
|
```bash
|
|
320
|
-
npm run build -- --
|
|
628
|
+
npm run build -- --force
|
|
321
629
|
```
|
|
322
630
|
|
|
323
|
-
|
|
324
|
-
command. The database-backed Next.js bundle is written to `out/registry`, and
|
|
325
|
-
workspace source is not compiled or served. A Vercel Build Output bundle can be
|
|
326
|
-
produced for either target by adding `--package vercel`.
|
|
631
|
+
**Registry server.** Build a standalone registry application:
|
|
327
632
|
|
|
328
|
-
|
|
633
|
+
```bash
|
|
634
|
+
npm run build -- --target registry
|
|
635
|
+
```
|
|
329
636
|
|
|
330
|
-
|
|
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.
|
|
331
640
|
|
|
332
|
-
|
|
333
|
-
|
|
641
|
+
**Vercel.** Add `--package vercel` to either target to make a Vercel Build
|
|
642
|
+
Output bundle in `.vercel/output`:
|
|
334
643
|
|
|
335
644
|
```bash
|
|
336
|
-
|
|
337
|
-
npm run db:migrate
|
|
645
|
+
npm run build -- --target registry --package vercel
|
|
338
646
|
```
|
|
339
647
|
|
|
340
|
-
|
|
341
|
-
server startup, and browser installer.
|
|
648
|
+
## Registry
|
|
342
649
|
|
|
343
|
-
|
|
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.
|
|
344
653
|
|
|
345
|
-
|
|
654
|
+
### Deploy a registry
|
|
346
655
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
HOSTNAME=0.0.0.0
|
|
350
|
-
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public"
|
|
351
|
-
AUTH_SECRET="replace-with-at-least-32-random-characters"
|
|
352
|
-
AUTH_URL="http://localhost:4000"
|
|
353
|
-
```
|
|
656
|
+
1. Apply the database migrations from the workspace or a CI checkout, where the
|
|
657
|
+
project config is available:
|
|
354
658
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
659
|
+
```bash
|
|
660
|
+
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public" \
|
|
661
|
+
npm run db:migrate
|
|
662
|
+
```
|
|
359
663
|
|
|
360
|
-
|
|
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.
|
|
361
667
|
|
|
362
|
-
|
|
363
|
-
|
|
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.
|
|
364
671
|
|
|
365
|
-
|
|
672
|
+
3. Build the registry:
|
|
366
673
|
|
|
367
|
-
|
|
674
|
+
```bash
|
|
675
|
+
npm run build -- --target registry
|
|
676
|
+
```
|
|
368
677
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
```
|
|
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
|
+
```
|
|
372
687
|
|
|
373
|
-
|
|
374
|
-
|
|
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.
|
|
375
691
|
|
|
376
|
-
|
|
692
|
+
5. Open http://localhost:4000/install and create the first administrator. The
|
|
693
|
+
installer examines the deployment, but it never runs migrations.
|
|
377
694
|
|
|
378
|
-
|
|
695
|
+
CAUTION: Complete the installation immediately after the deployment. The first
|
|
696
|
+
visitor can claim a registry that is not installed.
|
|
697
|
+
|
|
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:
|
|
379
705
|
|
|
380
706
|
```ts
|
|
381
707
|
runtime: {
|
|
382
708
|
registry: {
|
|
383
|
-
databaseUrl: fromEnv('DATABASE_URL'),
|
|
384
709
|
database: {
|
|
710
|
+
url: fromEnv('DATABASE_URL'),
|
|
385
711
|
driver: 'neon',
|
|
386
712
|
},
|
|
387
713
|
},
|
|
388
714
|
},
|
|
389
715
|
```
|
|
390
716
|
|
|
391
|
-
|
|
392
|
-
|
|
717
|
+
Both drivers use the same schema and the same `db:migrate` step.
|
|
718
|
+
|
|
719
|
+
### Asset storage
|
|
393
720
|
|
|
394
|
-
|
|
721
|
+
By default, the registry stores asset blobs in PostgreSQL. Each blob can be at
|
|
722
|
+
most 4 MB. `assetStorage.maxInlineBytes` changes this limit.
|
|
395
723
|
|
|
396
|
-
|
|
397
|
-
|
|
724
|
+
A [custom server module](#custom-server-modules) can store blobs in a
|
|
725
|
+
different service:
|
|
398
726
|
|
|
399
727
|
```ts
|
|
400
728
|
runtime: {
|
|
401
729
|
registry: {
|
|
402
730
|
assetStorage: {
|
|
403
|
-
|
|
731
|
+
provider: 'custom',
|
|
732
|
+
module: './server/storage/s3.mjs',
|
|
733
|
+
options: { bucket: 'handoff-assets', secretKey: fromEnv('S3_SECRET_KEY') },
|
|
404
734
|
},
|
|
405
735
|
},
|
|
406
736
|
},
|
|
407
737
|
```
|
|
408
738
|
|
|
409
|
-
The
|
|
410
|
-
|
|
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 |
|
|
411
756
|
|
|
412
|
-
|
|
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.
|
|
413
760
|
|
|
414
|
-
The
|
|
415
|
-
|
|
416
|
-
|
|
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.
|
|
417
766
|
|
|
418
|
-
|
|
419
|
-
|
|
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.
|
|
420
770
|
|
|
421
|
-
|
|
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.
|
|
422
777
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
listed and revoked under Account → Access tokens in the registry.
|
|
778
|
+
> **Recipes:** [S3-compatible storage](RECIPES.md#s3-compatible-storage) ·
|
|
779
|
+
> [Vercel Blob](RECIPES.md#vercel-blob)
|
|
426
780
|
|
|
427
|
-
|
|
781
|
+
### Email
|
|
428
782
|
|
|
429
|
-
|
|
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') },
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
```
|
|
798
|
+
|
|
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)
|
|
813
|
+
|
|
814
|
+
### Custom server modules
|
|
815
|
+
|
|
816
|
+
Asset storage, email, and AI connections can name a custom server module. Each
|
|
817
|
+
module must obey these rules:
|
|
818
|
+
|
|
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.
|
|
826
|
+
|
|
827
|
+
The factory gets `options` with each `fromEnv()` value resolved at request
|
|
828
|
+
time.
|
|
829
|
+
|
|
830
|
+
## Connecting a workspace to a registry
|
|
831
|
+
|
|
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.
|
|
837
|
+
|
|
838
|
+
### Device login
|
|
839
|
+
|
|
840
|
+
Start the authorization from the workspace:
|
|
430
841
|
|
|
431
842
|
```bash
|
|
432
843
|
npm run login -- --url http://localhost:4000
|
|
433
844
|
```
|
|
434
845
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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.
|
|
438
849
|
|
|
439
|
-
|
|
440
|
-
several registries
|
|
850
|
+
The CLI saves one credential for each profile. Thus a workspace can stay signed
|
|
851
|
+
in to several registries:
|
|
441
852
|
|
|
442
853
|
```bash
|
|
443
854
|
npm run login -- --profile staging --url https://staging.example.com
|
|
@@ -446,44 +857,55 @@ npm run publish -- all --profile staging
|
|
|
446
857
|
|
|
447
858
|
A login profile name is free. It needs no `handoff.config.<name>.*` file, and
|
|
448
859
|
`login` is the only command that accepts a new name. A profile without its own
|
|
449
|
-
login
|
|
450
|
-
|
|
860
|
+
login uses the default login. Thus one `npm run login` is enough for a
|
|
861
|
+
workspace with one registry.
|
|
451
862
|
|
|
452
|
-
`npm run logout` revokes and removes the login of the selected profile.
|
|
453
|
-
`--all`
|
|
863
|
+
`npm run logout` revokes and removes the login of the selected profile.
|
|
864
|
+
`--all` removes every saved login.
|
|
454
865
|
|
|
455
|
-
|
|
866
|
+
### Access token
|
|
456
867
|
|
|
457
|
-
|
|
458
|
-
write access
|
|
459
|
-
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`:
|
|
460
871
|
|
|
461
872
|
```dotenv
|
|
462
873
|
HANDOFF_REGISTRY_URL=http://localhost:4000
|
|
463
874
|
HANDOFF_REGISTRY_ACCESS_TOKEN=hnd_...
|
|
464
875
|
```
|
|
465
876
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
`.env
|
|
469
|
-
|
|
470
|
-
variables instead of a file.
|
|
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.
|
|
471
881
|
|
|
472
|
-
Environment values
|
|
473
|
-
|
|
474
|
-
for the registry URL
|
|
475
|
-
registry than the login of the selected profile, publish and checkout
|
|
476
|
-
which login
|
|
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.
|
|
477
887
|
|
|
478
|
-
|
|
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:
|
|
479
890
|
|
|
480
|
-
|
|
891
|
+
```ts
|
|
892
|
+
runtime: {
|
|
893
|
+
registryConnection: {
|
|
894
|
+
url: 'https://registry.example.com',
|
|
895
|
+
accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### Publishing
|
|
901
|
+
|
|
902
|
+
Publish every kind of content in dependency order:
|
|
481
903
|
|
|
482
904
|
```bash
|
|
483
905
|
npm run publish -- all
|
|
484
906
|
```
|
|
485
907
|
|
|
486
|
-
|
|
908
|
+
Publish one kind:
|
|
487
909
|
|
|
488
910
|
```bash
|
|
489
911
|
npm run publish -- catalog
|
|
@@ -492,201 +914,186 @@ npm run publish -- tokens
|
|
|
492
914
|
npm run publish -- assets
|
|
493
915
|
```
|
|
494
916
|
|
|
495
|
-
`catalog` covers every catalog item.
|
|
496
|
-
|
|
497
|
-
|
|
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.
|
|
498
920
|
|
|
499
|
-
|
|
500
|
-
|
|
921
|
+
`tokens` and `assets` run the Figma pipeline before the upload. Thus the
|
|
922
|
+
[Figma credentials](#figma-foundations) must be available.
|
|
501
923
|
|
|
502
|
-
|
|
924
|
+
Add one or more ids to publish only those entities:
|
|
503
925
|
|
|
504
926
|
```bash
|
|
505
927
|
npm run publish -- catalog item-id another-id
|
|
506
928
|
```
|
|
507
929
|
|
|
508
|
-
`--dry-run`
|
|
509
|
-
it needs
|
|
510
|
-
|
|
511
|
-
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:
|
|
512
935
|
|
|
513
936
|
```bash
|
|
514
937
|
npm run publish -- all --dry-run
|
|
515
938
|
npm run publish -- catalog --no-build
|
|
516
939
|
```
|
|
517
940
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
and writes nothing.
|
|
941
|
+
After a publish, reload the registry to see the published items and
|
|
942
|
+
foundations.
|
|
521
943
|
|
|
522
|
-
|
|
523
|
-
visible. Published database records are read by registry pages; the local
|
|
524
|
-
workspace is never read directly.
|
|
944
|
+
### Checkout
|
|
525
945
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
[Workspace authorization](#4-workspace-authorization). A connection block in
|
|
529
|
-
`handoff.config.ts` is needed only to pin the URL in the repository, or to read
|
|
530
|
-
the values from differently named variables (`fromEnv` is imported from
|
|
531
|
-
`handoff-app`):
|
|
946
|
+
`checkout` writes registry content into the workspace. It takes the same
|
|
947
|
+
`all`, kind, id, and `--dry-run` forms as `publish`:
|
|
532
948
|
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
|
|
538
|
-
},
|
|
539
|
-
},
|
|
949
|
+
```bash
|
|
950
|
+
npm run checkout -- all
|
|
951
|
+
npm run checkout -- catalog item-id
|
|
952
|
+
npm run checkout -- all --dry-run
|
|
540
953
|
```
|
|
541
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
|
+
|
|
542
962
|
## MCP
|
|
543
963
|
|
|
544
|
-
The documentation app serves a Model Context Protocol endpoint at `/api/mcp
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
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.
|
|
549
968
|
|
|
550
969
|
| Target | Endpoint | Credential |
|
|
551
970
|
| --- | --- | --- |
|
|
552
|
-
| Workspace (`dev` / `start`) | `http://localhost:3000/api/mcp/` |
|
|
553
|
-
| Registry (standalone or Vercel) | `<registry url>/api/mcp/` |
|
|
554
|
-
| 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.
|
|
555
985
|
|
|
556
|
-
|
|
557
|
-
|
|
986
|
+
> **Recipes:** [Connect a client by hand](RECIPES.md#connect-a-client-by-hand) ·
|
|
987
|
+
> [Turn off MCP](RECIPES.md#turn-off-mcp)
|
|
558
988
|
|
|
559
|
-
|
|
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:
|
|
560
998
|
|
|
561
999
|
```ts
|
|
562
1000
|
runtime: {
|
|
563
|
-
|
|
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
|
+
},
|
|
564
1013
|
},
|
|
565
1014
|
```
|
|
566
1015
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
drops the MCP SDK from the packaged bundle, and shows no connect affordance at all
|
|
570
|
-
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.
|
|
571
1018
|
|
|
572
|
-
|
|
573
|
-
for Claude Code, Cursor and VS Code behind the plug icon in the header. By hand:
|
|
1019
|
+
Each connection uses one of these credentials:
|
|
574
1020
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
"type": "http",
|
|
580
|
-
"url": "https://registry.example.com/api/mcp/",
|
|
581
|
-
"headers": { "Authorization": "Bearer hnd_..." }
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
```
|
|
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.
|
|
586
1025
|
|
|
587
|
-
|
|
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.
|
|
588
1030
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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`.
|
|
593
1037
|
|
|
594
|
-
|
|
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.
|
|
595
1041
|
|
|
596
|
-
|
|
597
|
-
| --- | --- |
|
|
598
|
-
| `HANDOFF_PROFILE` | Config profile merged onto the base config |
|
|
599
|
-
| `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
|
|
600
|
-
| `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
|
|
601
|
-
| `HANDOFF_REGISTRY_URL` | Connected workspace registry URL |
|
|
602
|
-
| `HANDOFF_REGISTRY_ACCESS_TOKEN` | Registry access token used by a connected workspace |
|
|
603
|
-
| `HANDOFF_SYNC_SECRET` | Optional deployment-wide registry credential |
|
|
604
|
-
| `DATABASE_URL` | Registry PostgreSQL connection string |
|
|
605
|
-
| `AUTH_SECRET` | Registry session-signing secret, at least 32 characters |
|
|
606
|
-
| `AUTH_URL` | Canonical public registry URL |
|
|
607
|
-
| `PORT` | Standalone registry server port |
|
|
608
|
-
| `HOSTNAME` | Standalone registry bind hostname |
|
|
609
|
-
| `BLOB_READ_WRITE_TOKEN` | Credential for the Vercel Blob asset adapter |
|
|
610
|
-
| `HANDOFF_OUTPUT_DIR` | Override the fetched output directory |
|
|
611
|
-
| `HANDOFF_SITES_DIR` | Override the build output directory |
|
|
612
|
-
| `HANDOFF_APP_PORT` | Workspace documentation server port |
|
|
613
|
-
| `HANDOFF_WEBSOCKET_PORT` | Workspace live-reload server port |
|
|
614
|
-
|
|
615
|
-
Registry assets are stored in PostgreSQL by default with a 4 MB per-blob limit.
|
|
616
|
-
Vercel Blob or a custom adapter can be selected through
|
|
617
|
-
`runtime.registry.assetStorage`. The `pg` or `neon` connection driver can be
|
|
618
|
-
selected through `runtime.registry.database.driver`; PostgreSQL is used by
|
|
619
|
-
both.
|
|
620
|
-
|
|
621
|
-
## Profiles
|
|
622
|
-
|
|
623
|
-
A profile is a sidecar config file that merges onto the base config. It holds
|
|
624
|
-
only what changes between environments, so a project keeps one shared config
|
|
625
|
-
instead of a second complete copy of it.
|
|
626
|
-
|
|
627
|
-
- The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. It is the
|
|
628
|
-
only config file loaded when no profile is selected.
|
|
629
|
-
- A profile file is `handoff.config.<profile>.*`, with the same four
|
|
630
|
-
extensions.
|
|
631
|
-
- Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
|
|
632
|
-
`--profile` takes precedence over `HANDOFF_PROFILE`.
|
|
633
|
-
- Profile names are not predefined. A name can contain lowercase letters,
|
|
634
|
-
numbers, and hyphens.
|
|
635
|
-
- A selected profile must exist. If no `handoff.config.<name>.*` file is found,
|
|
636
|
-
the command stops with an error.
|
|
637
|
-
- `login`, `logout`, `publish`, and `checkout` also accept a profile that has
|
|
638
|
-
only a saved registry login, because there the profile selects the registry.
|
|
639
|
-
Every other command still needs the config file.
|
|
640
|
-
- Layers resolve as defaults, base config, profile, then programmatic config,
|
|
641
|
-
with the merge rules described under [Configuration](#configuration).
|
|
642
|
-
- A selected profile also reads `.env.<profile>` on top of `.env`, when that
|
|
643
|
-
file exists. Both are read from the directory the command runs in, and a
|
|
644
|
-
variable already set in the environment beats both files.
|
|
1042
|
+
### Reader keys
|
|
645
1043
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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`.
|
|
649
1053
|
|
|
650
|
-
|
|
651
|
-
profile does not need to set `runtime.mode`. This makes a profile a good place
|
|
652
|
-
for the registry build settings: the database driver, the database
|
|
653
|
-
environment-variable name, the asset storage, and whether MCP is enabled. A
|
|
654
|
-
hosting provider or a CI job selects the profile through `HANDOFF_PROFILE`.
|
|
1054
|
+
### Changing connections without a rebuild
|
|
655
1055
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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.
|
|
659
1060
|
|
|
660
|
-
|
|
661
|
-
|
|
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.
|
|
662
1063
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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)
|
|
666
1068
|
|
|
667
1069
|
## CLI reference
|
|
668
1070
|
|
|
669
1071
|
| Command | Description |
|
|
670
1072
|
| --- | --- |
|
|
671
|
-
| `npx handoff-app init` |
|
|
672
|
-
| `npm run fetch` |
|
|
673
|
-
| `npm run start` |
|
|
674
|
-
| `npm run dev` |
|
|
675
|
-
| `npm run build -- [--target static\|registry]` |
|
|
676
|
-
| `npm run db:migrate` |
|
|
677
|
-
| `npm run
|
|
678
|
-
| `npm run
|
|
679
|
-
| `npm run
|
|
680
|
-
| `npm run
|
|
681
|
-
| `
|
|
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 |
|
|
682
1088
|
|
|
683
1089
|
Every command except `init` accepts `-c, --config`, `--profile`, `-d, --debug`,
|
|
684
|
-
and `-f, --force`. `publish` and `checkout`
|
|
1090
|
+
and `-f, --force`. `publish` and `checkout` also accept `--dry-run`, and
|
|
685
1091
|
`publish` accepts `--no-build`.
|
|
686
1092
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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.
|
|
690
1097
|
|
|
691
1098
|
## Maintainers
|
|
692
1099
|
|
|
@@ -706,4 +1113,4 @@ Code of Conduct.
|
|
|
706
1113
|
|
|
707
1114
|
## License
|
|
708
1115
|
|
|
709
|
-
[MIT](
|
|
1116
|
+
[MIT](LICENSE.md) ©Convertiv
|