handoff-app 2.0.0-9 → 2.0.1
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/AGENTS.md +90 -1
- package/{Changelog.md → CHANGELOG.md} +105 -1
- package/CLAUDE.md +92 -1
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +818 -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/cli.js +1 -1
- 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/errors.js +4 -0
- package/dist/registry/publish/index.js +2 -0
- package/dist/registry/publish/package.d.ts +6 -0
- package/dist/registry/publish/package.js +29 -1
- 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 +9 -0
- package/dist/transformers/utils/build.js +27 -5
- 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/cli.ts +1 -1
- 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/errors.ts +4 -0
- package/src/registry/publish/index.ts +3 -1
- package/src/registry/publish/package.ts +33 -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 +22 -4
- 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/src/app/lib/mcp/shape.ts
DELETED
|
@@ -1,596 +0,0 @@
|
|
|
1
|
-
import { buildArtifactUrl } from '@handoff/artifacts/url';
|
|
2
|
-
import { HOME_PAGE_ID } from '@handoff/registry/content-kinds';
|
|
3
|
-
import type { TokenArtifactResource } from '@handoff/store';
|
|
4
|
-
import type { SlotMetadata } from '@handoff/transformers/preview/component';
|
|
5
|
-
import type { ComponentListObject, OptionalPreviewRender, TransformComponentTokensResult } from '@handoff/transformers/preview/types';
|
|
6
|
-
import type { PageDetail } from '../docs-api/records';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Agent-facing projections of the store records.
|
|
10
|
-
*
|
|
11
|
-
* The docs read API returns records verbatim because the browser app needs every field. An MCP
|
|
12
|
-
* client pays for each field in context, so everything here is a deliberate narrowing: build config,
|
|
13
|
-
* validation state, absolute paths and Figma sync ids are dropped, and the rest is reshaped into the
|
|
14
|
-
* flatter arrays an agent can act on. Pure functions only, no I/O.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** The generated-code fields an agent can ask for; the build artifact carries them under these names. */
|
|
18
|
-
export const CODE_FIELDS = ['code', 'html', 'css', 'sass', 'js'] as const;
|
|
19
|
-
export type CodeField = (typeof CODE_FIELDS)[number];
|
|
20
|
-
|
|
21
|
-
/** Accept internal page routes without URL normalization hiding traversal segments. */
|
|
22
|
-
export const pageIdFromUrl = (url: string): string | null => {
|
|
23
|
-
if (!url.startsWith('/') || url.startsWith('//') || /[?#\\\u0000-\u001f\u007f]/.test(url)) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
if (url === '/') return HOME_PAGE_ID;
|
|
27
|
-
const id = url.slice(1).replace(/\/$/, '');
|
|
28
|
-
for (const segment of id.split('/')) {
|
|
29
|
-
try {
|
|
30
|
-
const decoded = decodeURIComponent(segment);
|
|
31
|
-
if (!decoded || decoded === '.' || decoded === '..' || /[/\\\u0000-\u001f\u007f]/.test(decoded)) return null;
|
|
32
|
-
} catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return id;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const toPageResult = (page: PageDetail) => ({
|
|
40
|
-
id: page.id,
|
|
41
|
-
url: page.path,
|
|
42
|
-
title: page.title,
|
|
43
|
-
description: page.description,
|
|
44
|
-
group: page.group,
|
|
45
|
-
external: page.external,
|
|
46
|
-
content: page.content,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* One row of a component search result: identity and classification, and nothing else.
|
|
51
|
-
*
|
|
52
|
-
* Search answers "which component do I want", so a row carries only what tells one component from
|
|
53
|
-
* another. Previews, properties and code belong to handoff_get_component.
|
|
54
|
-
*/
|
|
55
|
-
export interface ComponentSummary {
|
|
56
|
-
id: string;
|
|
57
|
-
title: string;
|
|
58
|
-
description?: string;
|
|
59
|
-
group?: string;
|
|
60
|
-
type?: string;
|
|
61
|
-
categories?: string[];
|
|
62
|
-
tags?: string[];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const toComponentSummary = (record: ComponentListObject): ComponentSummary => ({
|
|
66
|
-
id: record.id,
|
|
67
|
-
title: record.title,
|
|
68
|
-
description: record.description || undefined,
|
|
69
|
-
group: record.group || undefined,
|
|
70
|
-
type: record.type || undefined,
|
|
71
|
-
categories: record.categories?.length ? record.categories : undefined,
|
|
72
|
-
tags: record.tags?.length ? record.tags : undefined,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
/** Case-insensitive substring test that treats a missing haystack as no match. */
|
|
76
|
-
const contains = (haystack: string | undefined, needle: string): boolean => (haystack ?? '').toLowerCase().includes(needle);
|
|
77
|
-
|
|
78
|
-
/** Case-insensitive equality, used by the `group`/`category`/`tag` filters. */
|
|
79
|
-
const equals = (value: string | undefined, other: string): boolean => (value ?? '').toLowerCase() === other;
|
|
80
|
-
|
|
81
|
-
export interface ComponentSearchFilters {
|
|
82
|
-
query?: string;
|
|
83
|
-
group?: string;
|
|
84
|
-
category?: string;
|
|
85
|
-
tag?: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Whether a record satisfies every supplied filter. `query` is a substring across id, title, group,
|
|
90
|
-
* categories and tags; the others are exact case-insensitive matches, so an agent can pivot straight
|
|
91
|
-
* off a value it just read out of a search result.
|
|
92
|
-
*/
|
|
93
|
-
export const matchesComponent = (record: ComponentListObject, filters: ComponentSearchFilters): boolean => {
|
|
94
|
-
const query = filters.query?.trim().toLowerCase();
|
|
95
|
-
const group = filters.group?.trim().toLowerCase();
|
|
96
|
-
const category = filters.category?.trim().toLowerCase();
|
|
97
|
-
const tag = filters.tag?.trim().toLowerCase();
|
|
98
|
-
|
|
99
|
-
if (query) {
|
|
100
|
-
const hit =
|
|
101
|
-
contains(record.id, query) ||
|
|
102
|
-
contains(record.title, query) ||
|
|
103
|
-
contains(record.group, query) ||
|
|
104
|
-
(record.categories ?? []).some((value) => contains(value, query)) ||
|
|
105
|
-
(record.tags ?? []).some((value) => contains(value, query));
|
|
106
|
-
if (!hit) return false;
|
|
107
|
-
}
|
|
108
|
-
if (group && !equals(record.group, group)) return false;
|
|
109
|
-
if (category && !(record.categories ?? []).some((value) => equals(value, category))) return false;
|
|
110
|
-
if (tag && !(record.tags ?? []).some((value) => equals(value, tag))) return false;
|
|
111
|
-
return true;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/** A component property as an agent needs it: what to pass, of what type, and whether it is required. */
|
|
115
|
-
export interface ComponentProperty {
|
|
116
|
-
name: string;
|
|
117
|
-
description?: string;
|
|
118
|
-
/** The prop's type: its real signature when docgen resolved one, else the coarse slot type. */
|
|
119
|
-
type: string;
|
|
120
|
-
default?: unknown;
|
|
121
|
-
required?: boolean;
|
|
122
|
-
/** Element type, for an array property. */
|
|
123
|
-
items?: string;
|
|
124
|
-
/** Nested shape, for an object or array-of-object property. */
|
|
125
|
-
properties?: Record<string, ComponentProperty>;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Flatten one `SlotMetadata`.
|
|
130
|
-
*
|
|
131
|
-
* `type` prefers `docgenType` because `SlotType` mostly falls back to `text` once docgen has run: it
|
|
132
|
-
* reports a boolean prop, an enum and an array alike, which is enough to make an agent pass the
|
|
133
|
-
* wrong thing. `docgenType` is the real signature (`boolean | undefined`, `"sm" | "lg" | undefined`,
|
|
134
|
-
* `SelectOption[]`), and it covers values no preview happens to demonstrate. `SlotType` is the
|
|
135
|
-
* fallback for a component with no docgen, such as handlebars or CSF.
|
|
136
|
-
*
|
|
137
|
-
* The rest of the docgen output is dropped: `deepType` is a whole type AST, and `typeRefs`,
|
|
138
|
-
* `annotations` and `warnings` give an agent nothing to act on.
|
|
139
|
-
*/
|
|
140
|
-
export const toProperty = (key: string, slot: SlotMetadata): ComponentProperty => ({
|
|
141
|
-
name: slot.name || key,
|
|
142
|
-
description: slot.description || undefined,
|
|
143
|
-
type: slot.docgenType || slot.type,
|
|
144
|
-
default: slot.default ?? undefined,
|
|
145
|
-
required: slot.rules?.required || undefined,
|
|
146
|
-
items: slot.items?.type,
|
|
147
|
-
properties: toProperties(slot.properties ?? slot.items?.properties),
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
export const toProperties = (slots: { [key: string]: SlotMetadata } | undefined): Record<string, ComponentProperty> | undefined => {
|
|
151
|
-
if (!slots || Object.keys(slots).length === 0) return undefined;
|
|
152
|
-
return Object.fromEntries(Object.entries(slots).map(([key, slot]) => [key, toProperty(key, slot)]));
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
export interface ComponentPreview {
|
|
156
|
-
id: string;
|
|
157
|
-
title: string;
|
|
158
|
-
values: Record<string, unknown>;
|
|
159
|
-
usage?: string;
|
|
160
|
-
/** Canonical URL of the rendered preview artifact. */
|
|
161
|
-
url: string;
|
|
162
|
-
/** Markup this preview renders to, when the caller asked for `html`. */
|
|
163
|
-
html?: string;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* The rendered markup inside a preview document's `<body>`.
|
|
168
|
-
*
|
|
169
|
-
* An emitted preview is a whole HTML page: doctype, stylesheet links, the hydration script. None of
|
|
170
|
-
* that is the component, and all of it is noise in an agent's context. `trimPreview` narrows the
|
|
171
|
-
* same way, but its parser lives in a module that also pulls in prettier. These documents are
|
|
172
|
-
* generator-written with exactly one `<body>`, so a pattern is enough here.
|
|
173
|
-
*/
|
|
174
|
-
const previewBody = (document: string): string => {
|
|
175
|
-
const match = /<body[^>]*>([\s\S]*)<\/body>/i.exec(document);
|
|
176
|
-
return (match ? match[1] : document).trim();
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Previews with their rendered-artifact URL filled in, and their markup when `documents` carries it.
|
|
181
|
-
* The builder writes the record's own `url` empty, so it is derived here the way the docs UI does.
|
|
182
|
-
*/
|
|
183
|
-
export const toPreviews = (
|
|
184
|
-
id: string,
|
|
185
|
-
previews: { [key: string]: OptionalPreviewRender } | undefined,
|
|
186
|
-
basePath: string,
|
|
187
|
-
documents: Record<string, string> = {}
|
|
188
|
-
): ComponentPreview[] =>
|
|
189
|
-
Object.entries(previews ?? {}).map(([previewId, preview]) => ({
|
|
190
|
-
id: previewId,
|
|
191
|
-
title: preview.title,
|
|
192
|
-
values: preview.values ?? {},
|
|
193
|
-
usage: preview.usage || undefined,
|
|
194
|
-
url: buildArtifactUrl(`component/${id}-${previewId}.html`, basePath),
|
|
195
|
-
html: documents[previewId] ? previewBody(documents[previewId]) : undefined,
|
|
196
|
-
}));
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Property names that are content slots by convention. Only consulted for a component with no type
|
|
200
|
-
* information at all, such as a handlebars template; a declared type always wins.
|
|
201
|
-
*/
|
|
202
|
-
const CONTENT_PROPERTIES = new Set([
|
|
203
|
-
'children', 'label', 'title', 'text', 'content', 'description',
|
|
204
|
-
'placeholder', 'alt', 'src', 'href', 'caption', 'subtitle',
|
|
205
|
-
]);
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Whether a property's type is a closed set of choices, which is what makes it a variant axis.
|
|
209
|
-
*
|
|
210
|
-
* True for a boolean, for `SlotType`'s `boolean`/`enum`, and for a union of literals
|
|
211
|
-
* (`"sm" | "lg" | undefined`). False for anything open-ended: `string`, `ReactNode`, `SelectOption[]`.
|
|
212
|
-
* `undefined`/`null` members are ignored so an optional prop is judged on its real choices.
|
|
213
|
-
*/
|
|
214
|
-
const isClosedSet = (type: string | undefined): boolean => {
|
|
215
|
-
if (!type) return false;
|
|
216
|
-
const parts = type
|
|
217
|
-
.split('|')
|
|
218
|
-
.map((part) => part.trim())
|
|
219
|
-
.filter((part) => part && part !== 'undefined' && part !== 'null');
|
|
220
|
-
if (parts.length === 0) return false;
|
|
221
|
-
if (parts.length === 1 && (parts[0] === 'boolean' || parts[0] === 'enum')) return true;
|
|
222
|
-
return parts.every((part) => /^(['"]).*\1$/.test(part) || /^-?\d+(\.\d+)?$/.test(part) || part === 'true' || part === 'false');
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* The component's variant axes: the properties that select between visual states, and the values the
|
|
227
|
-
* previews demonstrate for each.
|
|
228
|
-
*
|
|
229
|
-
* A property qualifies on its *type*, not its values. A closed set of choices (an enum, a boolean)
|
|
230
|
-
* is an axis, anything open-ended is content. That is what keeps a button's `children` out of the
|
|
231
|
-
* list: `"Primary Action"` is a label, not a variant, however much it looks like one next to
|
|
232
|
-
* `primary`.
|
|
233
|
-
*
|
|
234
|
-
* With no type information at all, as with a handlebars component, booleans are still axes and
|
|
235
|
-
* strings count unless the property is a conventional content slot. That rule is weaker, so it is
|
|
236
|
-
* only reached when there is nothing better to go on.
|
|
237
|
-
*
|
|
238
|
-
* The values here are the ones the previews show, so each is something you can go and look at.
|
|
239
|
-
* `properties[].type` carries the full declared set, which is usually wider.
|
|
240
|
-
*/
|
|
241
|
-
export const deriveVariants = (
|
|
242
|
-
previews: { [key: string]: OptionalPreviewRender } | undefined,
|
|
243
|
-
properties: Record<string, ComponentProperty> | undefined
|
|
244
|
-
): Record<string, (string | number | boolean)[]> => {
|
|
245
|
-
const axes: Record<string, (string | number | boolean)[]> = {};
|
|
246
|
-
for (const preview of Object.values(previews ?? {})) {
|
|
247
|
-
for (const [property, value] of Object.entries(preview.values ?? {})) {
|
|
248
|
-
const declared = properties?.[property];
|
|
249
|
-
const isAxis = declared
|
|
250
|
-
? isClosedSet(declared.type)
|
|
251
|
-
: typeof value === 'boolean' || (typeof value === 'string' && !CONTENT_PROPERTIES.has(property.toLowerCase()));
|
|
252
|
-
if (!isAxis) continue;
|
|
253
|
-
if (typeof value !== 'string' && typeof value !== 'boolean' && typeof value !== 'number') continue;
|
|
254
|
-
if (value === '') continue;
|
|
255
|
-
const values = (axes[property] ??= []);
|
|
256
|
-
if (!values.includes(value)) values.push(value);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return axes;
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
export interface ComponentResult {
|
|
263
|
-
id: string;
|
|
264
|
-
title: string;
|
|
265
|
-
description?: string;
|
|
266
|
-
group?: string;
|
|
267
|
-
type?: string;
|
|
268
|
-
renderer?: string;
|
|
269
|
-
/** Source format the implementation is written in, when the renderer alone does not say. */
|
|
270
|
-
sourceFormat?: string;
|
|
271
|
-
categories?: string[];
|
|
272
|
-
tags?: string[];
|
|
273
|
-
properties?: Record<string, ComponentProperty>;
|
|
274
|
-
previews: ComponentPreview[];
|
|
275
|
-
variants: Record<string, (string | number | boolean)[]>;
|
|
276
|
-
usage: { general?: string; shouldDo?: string[]; shouldNotDo?: string[] };
|
|
277
|
-
/** The component's source, narrowed by the caller's `include`. Absent when it is not built. */
|
|
278
|
-
code?: Partial<Record<Exclude<CodeField, 'html'>, string>>;
|
|
279
|
-
/** This component's token set, when its declaration names the Figma component it maps to. */
|
|
280
|
-
tokens?: { set: string };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* The component's own source, per the caller's `include`.
|
|
285
|
-
*
|
|
286
|
-
* `html` is left out even though the artifact has a top-level field by that name. That field holds
|
|
287
|
-
* the rendered markup of one preview picked by the renderer (the first for handlebars, the last for
|
|
288
|
-
* react and CSF), and the pick can land on an internal pattern preview that is stripped from
|
|
289
|
-
* `previews` before the artifact is written. So it is markup for a state the response does not
|
|
290
|
-
* describe, with no way for an agent to tell which. Rendered markup is per preview and returned
|
|
291
|
-
* there, read from each preview's own artifact.
|
|
292
|
-
*/
|
|
293
|
-
const pickCode = (
|
|
294
|
-
artifact: TransformComponentTokensResult,
|
|
295
|
-
include: readonly CodeField[]
|
|
296
|
-
): Partial<Record<Exclude<CodeField, 'html'>, string>> => {
|
|
297
|
-
const code: Partial<Record<Exclude<CodeField, 'html'>, string>> = {};
|
|
298
|
-
for (const field of include) {
|
|
299
|
-
if (field === 'html') continue;
|
|
300
|
-
const value = artifact?.[field];
|
|
301
|
-
if (typeof value === 'string' && value) code[field] = value;
|
|
302
|
-
}
|
|
303
|
-
return code;
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Merge a component record with its build artifact into one agent-facing result.
|
|
308
|
-
*
|
|
309
|
-
* `artifact` is the `component/{id}.json` build output, and is `null` for a component that is
|
|
310
|
-
* declared but not built. That still has usable metadata, so it comes back without `code` rather
|
|
311
|
-
* than as an error. The compiled `sharedStyles` blob is left out: it is the bulk of the artifact and
|
|
312
|
-
* is shared across every component, so it says nothing about this one. So are `validations`,
|
|
313
|
-
* `docgen`, `page`, `options` and the Figma sync fields.
|
|
314
|
-
*/
|
|
315
|
-
export const toComponentResult = (
|
|
316
|
-
record: ComponentListObject,
|
|
317
|
-
artifact: TransformComponentTokensResult | null,
|
|
318
|
-
include: readonly CodeField[],
|
|
319
|
-
basePath: string,
|
|
320
|
-
tokenSetIds: readonly string[] = [],
|
|
321
|
-
previewDocuments: Record<string, string> = {}
|
|
322
|
-
): ComponentResult => {
|
|
323
|
-
const previews = artifact?.previews ?? record.previews;
|
|
324
|
-
const code = artifact ? pickCode(artifact, include) : undefined;
|
|
325
|
-
const properties = toProperties(artifact?.properties ?? record.properties);
|
|
326
|
-
const usagePreviews = Object.values(previews ?? {}).map((preview) => preview.usage);
|
|
327
|
-
// The artifact's `usage` is one preview's snippet repeated at the top level. Only surface it when
|
|
328
|
-
// it is not already sitting in `previews`, so it does not read as component-level guidance.
|
|
329
|
-
const general = artifact?.usage && !usagePreviews.includes(artifact.usage) ? artifact.usage : undefined;
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
id: record.id,
|
|
333
|
-
title: record.title,
|
|
334
|
-
description: record.description || undefined,
|
|
335
|
-
group: record.group || undefined,
|
|
336
|
-
type: record.type || undefined,
|
|
337
|
-
renderer: artifact?.renderer ?? record.renderer,
|
|
338
|
-
sourceFormat: artifact?.sourceFormat ?? record.sourceFormat,
|
|
339
|
-
categories: record.categories?.length ? record.categories : undefined,
|
|
340
|
-
tags: record.tags?.length ? record.tags : undefined,
|
|
341
|
-
properties,
|
|
342
|
-
previews: toPreviews(record.id, previews, basePath, previewDocuments),
|
|
343
|
-
variants: deriveVariants(previews, properties),
|
|
344
|
-
usage: {
|
|
345
|
-
general,
|
|
346
|
-
shouldDo: record.should_do?.length ? record.should_do : undefined,
|
|
347
|
-
shouldNotDo: record.should_not_do?.length ? record.should_not_do : undefined,
|
|
348
|
-
},
|
|
349
|
-
code: code && Object.keys(code).length > 0 ? code : undefined,
|
|
350
|
-
tokens: componentTokenSet(record, tokenSetIds),
|
|
351
|
-
};
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* The component's token set, when one can be named with certainty.
|
|
356
|
-
*
|
|
357
|
-
* A token set is keyed by the *Figma* component name, which need not match the component's own id:
|
|
358
|
-
* `button` is backed by `component/buttons` here, and a code-only component has no set at all. The
|
|
359
|
-
* declaration's `figmaComponentId` is the join, and without it there is nothing to match on, so
|
|
360
|
-
* nothing is claimed. Guessing `component/{id}` would quietly point `button` at a set that does not
|
|
361
|
-
* exist.
|
|
362
|
-
*
|
|
363
|
-
* Returned as `{ set }` rather than a bare string so the value reads as a set id to pass to
|
|
364
|
-
* `handoff_get_tokens`, not as the tokens themselves.
|
|
365
|
-
*/
|
|
366
|
-
const componentTokenSet = (record: ComponentListObject, tokenSetIds: readonly string[]): { set: string } | undefined => {
|
|
367
|
-
const figmaId = record.figmaComponentId?.trim();
|
|
368
|
-
if (!figmaId) {
|
|
369
|
-
return undefined;
|
|
370
|
-
}
|
|
371
|
-
const set = `component/${figmaId}`;
|
|
372
|
-
return tokenSetIds.includes(set) ? { set } : undefined;
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Drop the Figma node id from every token record.
|
|
377
|
-
*
|
|
378
|
-
* Colors, typography, effects and component instances each carry an `id` that is a Figma node
|
|
379
|
-
* reference, which an agent cannot use. Everything else (`sass`, `reference`, `machineName`,
|
|
380
|
-
* `values`, `parts`, `variantProperties`) survives, so nothing that affects generated code is lost.
|
|
381
|
-
*/
|
|
382
|
-
export const stripFigmaIds = (record: unknown): unknown => {
|
|
383
|
-
if (Array.isArray(record)) return record.map(stripFigmaIds);
|
|
384
|
-
if (record && typeof record === 'object') {
|
|
385
|
-
const { id: _id, ...rest } = record as Record<string, unknown>;
|
|
386
|
-
return Object.fromEntries(Object.entries(rest).map(([key, value]) => [key, stripFigmaIds(value)]));
|
|
387
|
-
}
|
|
388
|
-
return record;
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
/** The generated formats available for a token set, deduped and in artifact order. */
|
|
392
|
-
export const availableFormats = (artifacts: TokenArtifactResource[]): string[] =>
|
|
393
|
-
Array.from(new Set(artifacts.map((artifact) => artifact.format)));
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Foundation tokens, reshaped into what an agent needs to write code.
|
|
397
|
-
*
|
|
398
|
-
* Neither the stored record nor the generated stylesheet is usable on its own. The record says a type
|
|
399
|
-
* style is called `Heading 1` but nothing in it yields the variable to emit: its `reference` is
|
|
400
|
-
* `typography--heading-1` while the real variables are `--typography-heading-1-font-size` and
|
|
401
|
-
* friends, and it carries raw Figma fields (`textAutoResize`, three competing `lineHeight*`
|
|
402
|
-
* encodings) that never become CSS. The stylesheet has the right names and resolved values but has
|
|
403
|
-
* flattened away the human name and the grouping.
|
|
404
|
-
*
|
|
405
|
-
* So the two are joined. Names and values come from the generated CSS, token identity from the
|
|
406
|
-
* generated `types` list, and `name`/`group` from the record. Nothing is reconstructed from a naming
|
|
407
|
-
* rule of our own, since the rule differs per set and typography would come out wrong.
|
|
408
|
-
*
|
|
409
|
-
* Foundations only. Component sets are multi-axis (part x state x theme x property) and their
|
|
410
|
-
* `types` artifact lists axes rather than token names, so there is no honest join to make.
|
|
411
|
-
*/
|
|
412
|
-
|
|
413
|
-
/** One foundation token: a single value, or a bundle of properties under a shared variable prefix. */
|
|
414
|
-
export interface FoundationToken {
|
|
415
|
-
/** Human-facing name from the record, when it could be matched unambiguously. */
|
|
416
|
-
name?: string;
|
|
417
|
-
/** Record group (e.g. `primary`, `shadow`), when set. */
|
|
418
|
-
group?: string;
|
|
419
|
-
/** The CSS custom property, for a single-value token. */
|
|
420
|
-
css?: string;
|
|
421
|
-
/** Resolved value, for a single-value token. */
|
|
422
|
-
value?: string;
|
|
423
|
-
/** Shared variable prefix, for a bundled token: each property is `{cssPrefix}-{property}`. */
|
|
424
|
-
cssPrefix?: string;
|
|
425
|
-
/** Resolved values keyed by CSS property, for a bundled token. */
|
|
426
|
-
properties?: Record<string, string>;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/** Parse `--name: value;` declarations out of a generated stylesheet. */
|
|
430
|
-
export const cssVariables = (content: string): Record<string, string> => {
|
|
431
|
-
const variables: Record<string, string> = {};
|
|
432
|
-
for (const line of content.split('\n')) {
|
|
433
|
-
const match = /^\s*--([\w-]+)\s*:\s*(.+?);\s*$/.exec(line);
|
|
434
|
-
if (match) {
|
|
435
|
-
variables[match[1]] = match[2].trim();
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return variables;
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Token names from a generated `types` artifact, read out of its quoted string list (`$color-names`,
|
|
443
|
-
* `$type-sizes`, `$effects`). Without it there is no telling whether `--color-primary-blue-darker`
|
|
444
|
-
* is a `darker` property of `primary-blue` or a token in its own right. It is the latter, and only
|
|
445
|
-
* this list says so.
|
|
446
|
-
*/
|
|
447
|
-
export const tokenNamesFromTypes = (content: string): string[] =>
|
|
448
|
-
// `[^"]*` not `[^"]+`: the list can carry an empty entry (`$color-groups` ends with `""`), and a
|
|
449
|
-
// pattern that cannot match it pairs that entry's closing quote with the next entry's opening one,
|
|
450
|
-
// knocking every later name out of alignment. Empty names are dropped after matching, not during.
|
|
451
|
-
Array.from(new Set(Array.from(content.matchAll(/"([^"]*)"/g), (match) => match[1]))).filter(Boolean);
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Assign a CSS variable to the longest token name that matches, returning the leftover as a property.
|
|
455
|
-
* Longest first is what keeps `primary-blue-darker` from being read as `primary-blue` + `darker`.
|
|
456
|
-
*/
|
|
457
|
-
const claimVariable = (variable: string, namesByLength: string[]): { token: string; property?: string } | null => {
|
|
458
|
-
for (const token of namesByLength) {
|
|
459
|
-
if (variable === token || variable.endsWith(`-${token}`)) {
|
|
460
|
-
return { token };
|
|
461
|
-
}
|
|
462
|
-
const at = variable.indexOf(`-${token}-`);
|
|
463
|
-
if (at >= 0) {
|
|
464
|
-
return { token, property: variable.slice(at + token.length + 2) };
|
|
465
|
-
}
|
|
466
|
-
if (variable.startsWith(`${token}-`)) {
|
|
467
|
-
return { token, property: variable.slice(token.length + 1) };
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return null;
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
/** A foundation record, as far as the join cares. */
|
|
474
|
-
type FoundationRecord = { name?: string; group?: string; reference?: string; machineName?: string; machine_name?: string };
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* The record describing one token. Matched on `reference`, which ends with the token name across all
|
|
478
|
-
* three foundation kinds. More than one candidate means it is ambiguous, so nothing is attached: a
|
|
479
|
-
* token without its label beats a token with the wrong one.
|
|
480
|
-
*/
|
|
481
|
-
const recordFor = (token: string, records: FoundationRecord[]): FoundationRecord | null => {
|
|
482
|
-
const matches = records.filter(
|
|
483
|
-
(record) =>
|
|
484
|
-
record?.reference === token ||
|
|
485
|
-
record?.reference?.endsWith(`-${token}`) ||
|
|
486
|
-
record?.machineName === token ||
|
|
487
|
-
record?.machine_name === token
|
|
488
|
-
);
|
|
489
|
-
return matches.length === 1 ? matches[0] : null;
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Reshape a foundation set. Returns `null` when the inputs are missing (no CSS or `types` artifact,
|
|
494
|
-
* or nothing parsed out of them) so the caller can fall back to the stylesheet or the record.
|
|
495
|
-
*/
|
|
496
|
-
export const synthesizeFoundationTokens = (
|
|
497
|
-
record: unknown,
|
|
498
|
-
artifacts: TokenArtifactResource[]
|
|
499
|
-
): Record<string, FoundationToken> | null => {
|
|
500
|
-
const css = artifacts.find((artifact) => artifact.format === 'css');
|
|
501
|
-
const types = artifacts.find((artifact) => artifact.format === 'types');
|
|
502
|
-
if (!css || !types) {
|
|
503
|
-
return null;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const variables = cssVariables(css.content);
|
|
507
|
-
const namesByLength = tokenNamesFromTypes(types.content).sort((a, b) => b.length - a.length);
|
|
508
|
-
if (Object.keys(variables).length === 0 || namesByLength.length === 0) {
|
|
509
|
-
return null;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const records: FoundationRecord[] = Array.isArray(record) ? record : [];
|
|
513
|
-
const tokens: Record<string, FoundationToken> = {};
|
|
514
|
-
|
|
515
|
-
for (const [variable, value] of Object.entries(variables)) {
|
|
516
|
-
const claim = claimVariable(variable, namesByLength);
|
|
517
|
-
if (!claim) {
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
const entry = (tokens[claim.token] ??= {});
|
|
521
|
-
if (!entry.name) {
|
|
522
|
-
const matched = recordFor(claim.token, records);
|
|
523
|
-
if (matched?.name) entry.name = matched.name;
|
|
524
|
-
if (matched?.group) entry.group = matched.group;
|
|
525
|
-
}
|
|
526
|
-
if (claim.property) {
|
|
527
|
-
entry.cssPrefix = `--${variable.slice(0, variable.length - claim.property.length - 1)}`;
|
|
528
|
-
(entry.properties ??= {})[claim.property] = value;
|
|
529
|
-
} else {
|
|
530
|
-
entry.css = `--${variable}`;
|
|
531
|
-
entry.value = value;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return Object.keys(tokens).length > 0 ? tokens : null;
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
/** One variant of a component: the axis values it applies to, and the variables it sets. */
|
|
539
|
-
export interface ComponentTokenVariant {
|
|
540
|
-
/** Axis values, e.g. `{ state: 'disabled', theme: 'dark' }`. Empty for a component with no variants. */
|
|
541
|
-
variant: Record<string, string>;
|
|
542
|
-
/** Full CSS custom property names to resolved values. */
|
|
543
|
-
variables: Record<string, string>;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Component tokens, grouped by the variant they belong to.
|
|
548
|
-
*
|
|
549
|
-
* A component set is keyed by part x variant x property and the variable name runs them together
|
|
550
|
-
* (`--select-additional-disabled-dark-border-color`) with no boundary that can be split on safely.
|
|
551
|
-
* The generated stylesheet already labels each group, though: `getComponentCommentBlock` emits
|
|
552
|
-
* `/* Select, state: disabled, theme: dark *\/` ahead of every block, built from the instance's
|
|
553
|
-
* `variantProperties`. So the grouping is read off the generator's own output and variables are kept
|
|
554
|
-
* whole rather than split by a guessed rule.
|
|
555
|
-
*
|
|
556
|
-
* No axes are advertised and nothing is resolved per brand or scheme. The variants already exist in
|
|
557
|
-
* the build output; this only groups them.
|
|
558
|
-
*/
|
|
559
|
-
export const synthesizeComponentTokens = (artifacts: TokenArtifactResource[]): ComponentTokenVariant[] | null => {
|
|
560
|
-
const css = artifacts.find((artifact) => artifact.format === 'css');
|
|
561
|
-
if (!css) {
|
|
562
|
-
return null;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const variants: ComponentTokenVariant[] = [];
|
|
566
|
-
let current: ComponentTokenVariant | null = null;
|
|
567
|
-
|
|
568
|
-
for (const line of css.content.split('\n')) {
|
|
569
|
-
const comment = /^\s*\/\*\s*(.+?)\s*\*\/\s*$/.exec(line);
|
|
570
|
-
if (comment) {
|
|
571
|
-
// The first comma-separated part is the component name; the rest are `key: value` axis pairs.
|
|
572
|
-
const variant: Record<string, string> = {};
|
|
573
|
-
for (const part of comment[1].split(',').slice(1)) {
|
|
574
|
-
const [key, ...rest] = part.split(':');
|
|
575
|
-
if (rest.length > 0) {
|
|
576
|
-
variant[key.trim().toLowerCase()] = rest.join(':').trim();
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
current = { variant, variables: {} };
|
|
580
|
-
variants.push(current);
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
const declaration = /^\s*--([\w-]+)\s*:\s*(.+?);\s*$/.exec(line);
|
|
584
|
-
if (declaration) {
|
|
585
|
-
// A component with no variants emits no header, so open an unlabelled group for it.
|
|
586
|
-
if (!current) {
|
|
587
|
-
current = { variant: {}, variables: {} };
|
|
588
|
-
variants.push(current);
|
|
589
|
-
}
|
|
590
|
-
current.variables[`--${declaration[1]}`] = declaration[2].trim();
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const populated = variants.filter((entry) => Object.keys(entry.variables).length > 0);
|
|
595
|
-
return populated.length > 0 ? populated : null;
|
|
596
|
-
};
|