@agent-native/core 0.80.9 → 0.80.10

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.
Files changed (111) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +28 -2
  5. package/corpus/core/src/cli/skills.ts +45 -24
  6. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
  7. package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
  8. package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
  9. package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
  10. package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
  11. package/corpus/templates/design/actions/apply-design-state.ts +200 -0
  12. package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
  13. package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
  14. package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
  15. package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
  16. package/corpus/templates/design/actions/capture-design-state.ts +224 -0
  17. package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
  18. package/corpus/templates/design/actions/create-component.ts +447 -0
  19. package/corpus/templates/design/actions/create-design-branch.ts +263 -0
  20. package/corpus/templates/design/actions/create-design-state.ts +162 -0
  21. package/corpus/templates/design/actions/delete-design-state.ts +55 -0
  22. package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
  23. package/corpus/templates/design/actions/get-component-details.ts +242 -0
  24. package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
  25. package/corpus/templates/design/actions/get-design-review.ts +314 -0
  26. package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
  27. package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
  28. package/corpus/templates/design/actions/index-components.ts +258 -0
  29. package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
  30. package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
  31. package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
  32. package/corpus/templates/design/actions/list-design-states.ts +72 -0
  33. package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
  34. package/corpus/templates/design/actions/open-component-source.ts +237 -0
  35. package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
  36. package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
  37. package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
  38. package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
  39. package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
  40. package/corpus/templates/design/actions/run-design-audit.ts +421 -0
  41. package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
  42. package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
  43. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1414 -71
  44. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
  45. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
  46. package/corpus/templates/design/app/components/design/EditPanel.tsx +1391 -52
  47. package/corpus/templates/design/app/components/design/LayersPanel.tsx +4 -3
  48. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
  49. package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
  50. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1052 -98
  51. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
  52. package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
  53. package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
  54. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
  55. package/corpus/templates/design/app/components/design/index.ts +16 -0
  56. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
  57. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
  58. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
  59. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
  60. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  61. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
  62. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
  63. package/corpus/templates/design/app/i18n/zh-TW.ts +31 -0
  64. package/corpus/templates/design/app/i18n-data.ts +269 -0
  65. package/corpus/templates/design/app/pages/DesignEditor.tsx +2478 -386
  66. package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
  67. package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
  68. package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
  69. package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
  70. package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
  71. package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
  72. package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
  73. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
  74. package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
  75. package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
  76. package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
  77. package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
  78. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
  79. package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
  80. package/corpus/templates/design/e2e/helpers.ts +10 -4
  81. package/corpus/templates/design/server/db/schema.ts +123 -0
  82. package/corpus/templates/design/server/plugins/db.ts +90 -0
  83. package/corpus/templates/design/shared/builder-app.ts +297 -0
  84. package/corpus/templates/design/shared/capability-resolver.ts +123 -0
  85. package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
  86. package/corpus/templates/design/shared/code-layer.ts +1016 -71
  87. package/corpus/templates/design/shared/component-model.ts +239 -0
  88. package/corpus/templates/design/shared/design-review.ts +275 -0
  89. package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
  90. package/corpus/templates/design/shared/design-state.ts +112 -0
  91. package/corpus/templates/design/shared/design-surface-index.ts +258 -0
  92. package/corpus/templates/design/shared/motion-compiler.ts +278 -0
  93. package/corpus/templates/design/shared/motion-timeline.ts +193 -0
  94. package/corpus/templates/design/shared/responsive-classes.ts +452 -0
  95. package/corpus/templates/design/shared/shader-fill.ts +323 -0
  96. package/corpus/templates/design/shared/source-mode.ts +245 -0
  97. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
  98. package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
  99. package/dist/cli/design-connect.d.ts.map +1 -1
  100. package/dist/cli/design-connect.js +28 -2
  101. package/dist/cli/design-connect.js.map +1 -1
  102. package/dist/cli/skills.d.ts.map +1 -1
  103. package/dist/cli/skills.js +37 -21
  104. package/dist/cli/skills.js.map +1 -1
  105. package/dist/collab/routes.d.ts +1 -1
  106. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  107. package/dist/notifications/routes.d.ts +2 -2
  108. package/dist/observability/routes.d.ts +7 -7
  109. package/dist/resources/handlers.d.ts +2 -2
  110. package/dist/server/transcribe-voice.d.ts +1 -1
  111. package/package.json +1 -1
@@ -0,0 +1,258 @@
1
+ /**
2
+ * index-components — read action.
3
+ *
4
+ * Scans the design's HTML for `data-agent-native-component` annotations and
5
+ * returns the component list plus detected instances.
6
+ *
7
+ * **Inline / Alpine tier:** parses the design HTML directly using the
8
+ * code-layer projection. Writes discovered component definitions into the
9
+ * `component_index` table so subsequent reads (get-component-details) can
10
+ * resolve persisted metadata.
11
+ *
12
+ * **Real-app tier (localhost / fusion):** `indexComponents` capability is
13
+ * required. When the source does not yet advertise it, the action returns an
14
+ * empty list with a `ctaRequired: true` flag and a human-readable `ctaMessage`
15
+ * prompting the user to Connect Builder (see DESIGN-STUDIO-PLAN.md §6.1).
16
+ */
17
+
18
+ import { defineAction } from "@agent-native/core";
19
+ import { getText, hasCollabState } from "@agent-native/core/collab";
20
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
21
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
22
+ import { and, eq } from "drizzle-orm";
23
+ import { z } from "zod";
24
+
25
+ import { getDb, schema } from "../server/db/index.js";
26
+ import "../server/db/index.js"; // ensure registerShareableResource runs
27
+ import { resolveSourceCapabilities } from "../shared/capability-resolver.js";
28
+ import { buildCodeLayerProjection } from "../shared/code-layer.js";
29
+ import type { CodeLayerSource } from "../shared/code-layer.js";
30
+ import {
31
+ detectInstances,
32
+ buildDefinitions,
33
+ type ComponentDefinition,
34
+ type ComponentInstance,
35
+ } from "../shared/component-model.js";
36
+ import { hasCapability } from "../shared/design-source-capabilities.js";
37
+ import { normalizeDesignSourceType } from "../shared/source-mode.js";
38
+
39
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
40
+
41
+ async function liveContent(
42
+ fileId: string,
43
+ storedContent: string,
44
+ ): Promise<string> {
45
+ try {
46
+ if (await hasCollabState(fileId)) {
47
+ const live = await getText(fileId, "content");
48
+ if (typeof live === "string") return live;
49
+ }
50
+ } catch {
51
+ // Collab reads are best-effort; SQL content is the fallback.
52
+ }
53
+ return storedContent;
54
+ }
55
+
56
+ function componentIndexId(designId: string, name: string): string {
57
+ // Stable deterministic id — same component name on the same design always
58
+ // maps to the same row so upsert is safe.
59
+ return `ci_${designId}_${name.toLowerCase().replace(/[^a-z0-9]/g, "_")}`;
60
+ }
61
+
62
+ // ─── Action ───────────────────────────────────────────────────────────────────
63
+
64
+ export default defineAction({
65
+ description:
66
+ "Scan the design's HTML for component annotations " +
67
+ "(`data-agent-native-component`) and return the component list plus " +
68
+ "detected instances. For inline/Alpine designs, parses the HTML directly " +
69
+ "and persists the discovered components into component_index. For real-app " +
70
+ "sources (localhost / fusion), the indexComponents capability must be " +
71
+ "available; if not, returns an empty list with ctaRequired=true and a " +
72
+ "message prompting the user to Connect Builder.",
73
+ schema: z.object({
74
+ designId: z.string().describe("Design project ID to index components for"),
75
+ fileId: z
76
+ .string()
77
+ .optional()
78
+ .describe(
79
+ "Specific design file id. Defaults to the primary index.html when omitted.",
80
+ ),
81
+ }),
82
+ readOnly: false,
83
+ http: { method: "POST" },
84
+ run: async ({ designId, fileId }) => {
85
+ const db = getDb();
86
+
87
+ // ── Access check ────────────────────────────────────────────────────────
88
+ // This action writes component_index rows — require editor access.
89
+ const access = await assertAccess("design", designId, "editor");
90
+
91
+ // ── Source type + capability check ──────────────────────────────────────
92
+ let rawSourceType: unknown = "inline";
93
+ const rawData = (access.resource as { data?: unknown }).data;
94
+ if (typeof rawData === "string") {
95
+ try {
96
+ const parsed: unknown = JSON.parse(rawData);
97
+ if (
98
+ parsed !== null &&
99
+ typeof parsed === "object" &&
100
+ "sourceType" in (parsed as object)
101
+ ) {
102
+ rawSourceType = (parsed as { sourceType: unknown }).sourceType;
103
+ }
104
+ } catch {
105
+ // Stale JSON — default to inline.
106
+ }
107
+ }
108
+
109
+ const sourceType = normalizeDesignSourceType(rawSourceType) ?? "inline";
110
+ const caps = resolveSourceCapabilities(sourceType);
111
+
112
+ // Real-app sources gate on `indexComponents`. For inline designs this
113
+ // capability is `unavailable` by default (the plan marks it as a real-app
114
+ // feature for deep TS/cva parse) but the *annotation scan* still works
115
+ // from the DOM — we always run it. The flag tells callers whether the
116
+ // full real-app index is live or whether they see only annotations.
117
+ const hasFullIndex = hasCapability(caps, "indexComponents");
118
+ const ctaRequired =
119
+ sourceType !== "inline" && !hasCapability(caps, "indexComponents");
120
+
121
+ if (ctaRequired) {
122
+ return {
123
+ designId,
124
+ sourceType,
125
+ ctaRequired: true,
126
+ ctaMessage:
127
+ "Full component indexing (prop types, cva variants, Storybook " +
128
+ "stories, jump-to-source) requires a connected Builder app. " +
129
+ "Connect Builder via the Make it real CTA to unlock this feature.",
130
+ components: [] as ComponentDefinition[],
131
+ instances: [] as ComponentInstance[],
132
+ totalComponents: 0,
133
+ totalInstances: 0,
134
+ };
135
+ }
136
+
137
+ // ── Fetch the design file ────────────────────────────────────────────────
138
+ const conditions = [
139
+ accessFilter(schema.designs, schema.designShares),
140
+ eq(schema.designFiles.designId, designId),
141
+ fileId
142
+ ? eq(schema.designFiles.id, fileId)
143
+ : eq(schema.designFiles.filename, "index.html"),
144
+ ];
145
+
146
+ const [file] = await db
147
+ .select({
148
+ id: schema.designFiles.id,
149
+ designId: schema.designFiles.designId,
150
+ filename: schema.designFiles.filename,
151
+ content: schema.designFiles.content,
152
+ })
153
+ .from(schema.designFiles)
154
+ .innerJoin(
155
+ schema.designs,
156
+ eq(schema.designFiles.designId, schema.designs.id),
157
+ )
158
+ .where(and(...conditions))
159
+ .limit(1);
160
+
161
+ if (!file) throw new Error("Design HTML file not found.");
162
+
163
+ const html = await liveContent(file.id, file.content ?? "");
164
+
165
+ // ── Projection + detection ───────────────────────────────────────────────
166
+ const codeLayerSource: CodeLayerSource = {
167
+ kind: "design-file",
168
+ designId: file.designId,
169
+ fileId: file.id,
170
+ filename: file.filename,
171
+ };
172
+
173
+ const projection = buildCodeLayerProjection(html, {
174
+ source: codeLayerSource,
175
+ });
176
+
177
+ const instances = detectInstances(projection.nodes);
178
+ const definitions = buildDefinitions(instances);
179
+
180
+ // ── Persist discovered components ────────────────────────────────────────
181
+ // Write each distinct component name into component_index so that
182
+ // get-component-details can resolve metadata by node id.
183
+ const now = new Date().toISOString();
184
+ // Derive the owner from the request user, falling back to the design's
185
+ // owner. Never stamp an empty-string owner (an unowned row): require a real
186
+ // owner before writing a new component_index row.
187
+ const designOwner = (access.resource as { ownerEmail?: unknown })
188
+ .ownerEmail;
189
+ const ownerEmail =
190
+ getRequestUserEmail() ??
191
+ (typeof designOwner === "string" && designOwner ? designOwner : null);
192
+ if (!ownerEmail) throw new Error("no authenticated user");
193
+
194
+ for (const def of definitions) {
195
+ const id = componentIndexId(designId, def.name);
196
+ const existing = await db
197
+ .select({ id: schema.componentIndex.id })
198
+ .from(schema.componentIndex)
199
+ .where(eq(schema.componentIndex.id, id))
200
+ .limit(1);
201
+
202
+ if (existing.length === 0) {
203
+ await db.insert(schema.componentIndex).values({
204
+ id,
205
+ designId,
206
+ name: def.name,
207
+ runtimeSelectors: JSON.stringify(
208
+ def.instanceNodeIds.map(
209
+ (nodeId) => `[data-agent-native-node-id="${nodeId}"]`,
210
+ ),
211
+ ),
212
+ ownerEmail,
213
+ createdAt: now,
214
+ updatedAt: now,
215
+ });
216
+ } else {
217
+ await db
218
+ .update(schema.componentIndex)
219
+ .set({
220
+ runtimeSelectors: JSON.stringify(
221
+ def.instanceNodeIds.map(
222
+ (nodeId) => `[data-agent-native-node-id="${nodeId}"]`,
223
+ ),
224
+ ),
225
+ updatedAt: now,
226
+ })
227
+ .where(eq(schema.componentIndex.id, id));
228
+ }
229
+ }
230
+
231
+ // Annotate instances with their component_index id.
232
+ const indexMap = new Map(
233
+ definitions.map((def) => [
234
+ def.name,
235
+ componentIndexId(designId, def.name),
236
+ ]),
237
+ );
238
+ const annotatedInstances = instances.map((inst) => ({
239
+ ...inst,
240
+ componentIndexId: indexMap.get(inst.name),
241
+ }));
242
+
243
+ return {
244
+ designId,
245
+ sourceType,
246
+ ctaRequired: false,
247
+ hasFullIndex,
248
+ components: definitions,
249
+ instances: annotatedInstances,
250
+ totalComponents: definitions.length,
251
+ totalInstances: instances.length,
252
+ note:
253
+ sourceType === "inline"
254
+ ? "Showing annotated Alpine components from data-agent-native-component attributes. Connect Builder for full TS prop types and cva variants."
255
+ : undefined,
256
+ };
257
+ },
258
+ });
@@ -0,0 +1,273 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { extractCssVars } from "@agent-native/core/server/design-token-utils";
3
+ import { resolveAccess } from "@agent-native/core/sharing";
4
+ import { eq } from "drizzle-orm";
5
+ import { z } from "zod";
6
+
7
+ import { getDb, schema } from "../server/db/index.js";
8
+ import "../server/db/index.js"; // ensure registerShareableResource runs
9
+ import type { DesignSystemData } from "../shared/api.js";
10
+ import { resolveTweaksToCssVars } from "../shared/resolve-tweaks.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Token type classification
14
+ // ---------------------------------------------------------------------------
15
+
16
+ function classifyVar(
17
+ name: string,
18
+ value: string,
19
+ ): "color" | "typography" | "spacing" | "radius" | "shadow" | "other" {
20
+ const n = name.toLowerCase();
21
+ if (
22
+ /color|bg|background|text|border|accent|primary|secondary|surface|muted|foreground|fill|stroke/i.test(
23
+ n,
24
+ ) ||
25
+ /^(#[0-9a-fA-F]{3,8}|rgba?\(|hsla?\(|oklch\(|color\()/.test(value.trim())
26
+ ) {
27
+ return "color";
28
+ }
29
+ if (/font|size|leading|tracking|weight|heading|body|type/i.test(n)) {
30
+ return "typography";
31
+ }
32
+ if (/radius|rounded/i.test(n)) {
33
+ return "radius";
34
+ }
35
+ if (/spacing|gap|padding|margin|space/i.test(n)) {
36
+ return "spacing";
37
+ }
38
+ if (/shadow|blur|drop/i.test(n)) {
39
+ return "shadow";
40
+ }
41
+ return "other";
42
+ }
43
+
44
+ /** Derive a friendly display name from a CSS var name like `--primary-color`. */
45
+ function friendlyName(cssVar: string): string {
46
+ return cssVar
47
+ .replace(/^--/, "")
48
+ .replace(/[-_]/g, " ")
49
+ .replace(/\b\w/g, (c) => c.toUpperCase());
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Token entry shape
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export interface DesignToken {
57
+ /** Human-readable display name, e.g. "Primary Color". */
58
+ name: string;
59
+ /** CSS custom property, e.g. "--primary-color". */
60
+ cssVar: string;
61
+ /** Resolved string value, e.g. "#3B82F6" or "0.5rem". */
62
+ value: string;
63
+ /** Semantic token category. */
64
+ type: "color" | "typography" | "spacing" | "radius" | "shadow" | "other";
65
+ /** Opaque source chip label, e.g. "globals.css" or "Brand Kit". */
66
+ source: string;
67
+ /**
68
+ * True when the value comes from the design's own tweak selections (i.e.
69
+ * the user has already customised this token in the editor).
70
+ */
71
+ isTweakOverride?: boolean;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Action
76
+ // ---------------------------------------------------------------------------
77
+
78
+ export default defineAction({
79
+ description:
80
+ "Index the design tokens for a design as a friendly { name, cssVar, " +
81
+ "value, type, source } list. Parses CSS custom properties from the " +
82
+ "design's HTML :root block, the linked Brand Kit / design system, and " +
83
+ "the user's applied tweak selections. Returns tokens grouped by type " +
84
+ "(color, typography, spacing, radius, shadow, other).",
85
+ schema: z.object({
86
+ designId: z.string().describe("Design project ID"),
87
+ }),
88
+ readOnly: true,
89
+ http: { method: "GET" },
90
+ run: async ({ designId }) => {
91
+ const access = await resolveAccess("design", designId);
92
+ if (!access) {
93
+ throw new Error("Design not found");
94
+ }
95
+ const design = access.resource;
96
+ const db = getDb();
97
+
98
+ // ------------------------------------------------------------------
99
+ // 1. Parse tokens from the design's own HTML files (:root vars)
100
+ // ------------------------------------------------------------------
101
+ const files = await db
102
+ .select({
103
+ filename: schema.designFiles.filename,
104
+ content: schema.designFiles.content,
105
+ })
106
+ .from(schema.designFiles)
107
+ .where(eq(schema.designFiles.designId, designId));
108
+
109
+ /** cssVar -> { value, source } */
110
+ const rawTokens: Map<string, { value: string; source: string }> = new Map();
111
+
112
+ for (const file of files) {
113
+ const state = {
114
+ colors: {} as Record<string, string>,
115
+ cssCustomProperties: {} as Record<string, string>,
116
+ fonts: [],
117
+ spacing: {} as Record<string, string>,
118
+ borderRadius: {} as Record<string, string>,
119
+ stylingFramework: null,
120
+ rawExtracts: [],
121
+ seenFonts: new Set<string>(),
122
+ };
123
+ extractCssVars(state, file.content);
124
+ for (const [k, v] of Object.entries(state.cssCustomProperties)) {
125
+ rawTokens.set(k, { value: v, source: file.filename });
126
+ }
127
+ }
128
+
129
+ // ------------------------------------------------------------------
130
+ // 2. Overlay tokens from the linked Brand Kit / design system
131
+ // ------------------------------------------------------------------
132
+ if (design.designSystemId) {
133
+ // Design systems are their own access boundary (same pattern as
134
+ // get-design-system.ts). A user who can read the design but has no
135
+ // access to the linked design system must NOT receive its tokens.
136
+ const dsAccess = await resolveAccess(
137
+ "design-system",
138
+ design.designSystemId,
139
+ );
140
+
141
+ const [dsRow] = dsAccess
142
+ ? await db
143
+ .select({ data: schema.designSystems.data })
144
+ .from(schema.designSystems)
145
+ .where(eq(schema.designSystems.id, design.designSystemId))
146
+ .limit(1)
147
+ : [];
148
+
149
+ if (dsRow?.data) {
150
+ try {
151
+ const dsData = JSON.parse(dsRow.data) as Partial<DesignSystemData>;
152
+ // Flatten the Brand Kit's known token fields into CSS vars
153
+ const brandColors = dsData.colors ?? {};
154
+ const colorRoleMap: Record<string, string> = {
155
+ primary: "--color-primary",
156
+ secondary: "--color-secondary",
157
+ accent: "--color-accent",
158
+ background: "--color-background",
159
+ surface: "--color-surface",
160
+ text: "--color-text",
161
+ textMuted: "--color-text-muted",
162
+ };
163
+ for (const [role, cssVar] of Object.entries(colorRoleMap)) {
164
+ const v = (brandColors as Record<string, string>)[role];
165
+ if (v) rawTokens.set(cssVar, { value: v, source: "Brand Kit" });
166
+ }
167
+ // Border radius
168
+ if (dsData.borders?.radius) {
169
+ rawTokens.set("--radius", {
170
+ value: dsData.borders.radius,
171
+ source: "Brand Kit",
172
+ });
173
+ }
174
+ // Spacing
175
+ if (dsData.spacing?.elementGap) {
176
+ rawTokens.set("--spacing-element-gap", {
177
+ value: dsData.spacing.elementGap,
178
+ source: "Brand Kit",
179
+ });
180
+ }
181
+ if (dsData.spacing?.pagePadding) {
182
+ rawTokens.set("--spacing-page-padding", {
183
+ value: dsData.spacing.pagePadding,
184
+ source: "Brand Kit",
185
+ });
186
+ }
187
+ } catch {
188
+ // Malformed JSON — skip Brand Kit overlay silently.
189
+ }
190
+ }
191
+ }
192
+
193
+ // ------------------------------------------------------------------
194
+ // 3. Overlay tweak-resolved values (user customisations win)
195
+ // ------------------------------------------------------------------
196
+ let designData: Record<string, unknown> = {};
197
+ try {
198
+ designData = design.data
199
+ ? (JSON.parse(design.data) as Record<string, unknown>)
200
+ : {};
201
+ } catch {
202
+ // ignore
203
+ }
204
+ const tweaks = Array.isArray(designData.tweaks)
205
+ ? (designData.tweaks as Array<{
206
+ id: string;
207
+ cssVar?: string;
208
+ defaultValue: unknown;
209
+ }>)
210
+ : [];
211
+ const tweakSelections =
212
+ designData.tweakSelections &&
213
+ typeof designData.tweakSelections === "object" &&
214
+ !Array.isArray(designData.tweakSelections)
215
+ ? (designData.tweakSelections as Record<
216
+ string,
217
+ string | number | boolean
218
+ >)
219
+ : {};
220
+
221
+ // Cast tweaks array to the shape resolveTweaksToCssVars expects
222
+ type TweakDef = Parameters<typeof resolveTweaksToCssVars>[0][number];
223
+ const resolvedOverrides = resolveTweaksToCssVars(
224
+ tweaks as TweakDef[],
225
+ tweakSelections,
226
+ );
227
+ const tweakCssVars = new Set(
228
+ tweaks.map((t) => t.cssVar).filter(Boolean) as string[],
229
+ );
230
+ for (const [cssVar, value] of Object.entries(resolvedOverrides)) {
231
+ rawTokens.set(cssVar, { value, source: "Tweaks" });
232
+ }
233
+
234
+ // ------------------------------------------------------------------
235
+ // 4. Build friendly token list
236
+ // ------------------------------------------------------------------
237
+ const tokens: DesignToken[] = [];
238
+ for (const [cssVar, { value, source }] of rawTokens) {
239
+ tokens.push({
240
+ name: friendlyName(cssVar),
241
+ cssVar,
242
+ value,
243
+ type: classifyVar(cssVar, value),
244
+ source,
245
+ isTweakOverride: tweakCssVars.has(cssVar),
246
+ });
247
+ }
248
+
249
+ // Group by type for the panel
250
+ type TokenGroup = { type: DesignToken["type"]; tokens: DesignToken[] };
251
+ const ORDER: DesignToken["type"][] = [
252
+ "color",
253
+ "typography",
254
+ "spacing",
255
+ "radius",
256
+ "shadow",
257
+ "other",
258
+ ];
259
+ const byType = new Map<DesignToken["type"], DesignToken[]>(
260
+ ORDER.map((t) => [t, []]),
261
+ );
262
+ for (const tok of tokens) {
263
+ byType.get(tok.type)!.push(tok);
264
+ }
265
+ const groups: TokenGroup[] = [];
266
+ for (const type of ORDER) {
267
+ const toks = byType.get(type)!;
268
+ if (toks.length > 0) groups.push({ type, tokens: toks });
269
+ }
270
+
271
+ return { designId, tokenCount: tokens.length, groups, tokens };
272
+ },
273
+ });