@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,239 @@
1
+ /**
2
+ * Component model — pure helpers for detecting and describing component
3
+ * instances from code-layer node data.
4
+ *
5
+ * Two detection strategies:
6
+ *
7
+ * 1. **Alpine / annotated inline** — the design HTML carries
8
+ * `data-agent-native-component="Name"` attributes stamped directly on the
9
+ * DOM element that acts as the component root. Optional sibling attributes
10
+ * `data-agent-native-prop-*` carry prop name/value pairs.
11
+ *
12
+ * 2. **Real-app (localhost / fusion)** — a Vite/Babel transform injects
13
+ * `data-agent-native-component` plus `data-agent-native-prop-*` at
14
+ * build time so instances are discoverable from the rendered HTML. In
15
+ * addition, the `component_index` table holds richer metadata (TS prop
16
+ * types, cva variants, Storybook stories) indexed by `index-components`.
17
+ *
18
+ * This module is **pure** — no DB, no IO. It operates on `CodeLayerNode`
19
+ * objects returned by `buildCodeLayerProjection`.
20
+ *
21
+ * See DESIGN-STUDIO-PLAN.md §6.1 for context.
22
+ */
23
+
24
+ import type { CodeLayerNode } from "./code-layer";
25
+
26
+ // ─── Component detection attributes ───────────────────────────────────────────
27
+
28
+ /** The HTML attribute that marks a DOM node as a component root. */
29
+ export const COMPONENT_NAME_ATTR = "data-agent-native-component";
30
+
31
+ /** Prefix for simple prop attributes stamped next to the component root. */
32
+ export const COMPONENT_PROP_PREFIX = "data-agent-native-prop-";
33
+
34
+ // ─── Extracted prop value ─────────────────────────────────────────────────────
35
+
36
+ export interface ComponentPropValue {
37
+ /** Prop name derived from the attribute (e.g. `"variant"` from `data-agent-native-prop-variant`). */
38
+ name: string;
39
+ /** Raw attribute value string as found in the HTML. */
40
+ value: string;
41
+ }
42
+
43
+ // ─── Component instance ───────────────────────────────────────────────────────
44
+
45
+ /**
46
+ * A component instance detected in the rendered HTML.
47
+ *
48
+ * For Alpine / annotated designs this is the authoritative shape. For
49
+ * real-app sources the `componentIndexId` ties back to the `component_index`
50
+ * row, which carries prop types, variants, and Storybook stories.
51
+ */
52
+ export interface ComponentInstance {
53
+ /**
54
+ * Stable instance id — the `data-agent-native-node-id` of the root node
55
+ * (or the node's `id` field when the node id attribute is absent).
56
+ */
57
+ instanceId: string;
58
+
59
+ /**
60
+ * Component name as declared in `data-agent-native-component`.
61
+ * E.g. `"PrimaryButton"`, `"HeroCard"`.
62
+ */
63
+ name: string;
64
+
65
+ /**
66
+ * Simple prop values extracted from `data-agent-native-prop-*` attributes.
67
+ * Present on annotated Alpine nodes and on real-app nodes instrumented by
68
+ * the Vite/Babel transform.
69
+ */
70
+ props: ComponentPropValue[];
71
+
72
+ /**
73
+ * The `x-data` Alpine expression on the node, when present. Used by the
74
+ * Alpine editor to show variant/state controls inline.
75
+ */
76
+ alpineData?: string;
77
+
78
+ /**
79
+ * CSS selector that uniquely addresses this instance on the canvas.
80
+ * Derived from the node's primary selector.
81
+ */
82
+ selector: string;
83
+
84
+ /**
85
+ * The `data-agent-native-node-id` value (same as `instanceId` for annotated
86
+ * nodes; provided separately so callers can pass it as a stable handle to
87
+ * other code-layer APIs even when `instanceId` is constructed from a
88
+ * different attribute).
89
+ */
90
+ nodeId: string;
91
+
92
+ /**
93
+ * `component_index.id` of the matching persisted component row, when
94
+ * available. Populated by `index-components` for real-app sources and used
95
+ * by `get-component-details` to load prop types and variants.
96
+ */
97
+ componentIndexId?: string;
98
+ }
99
+
100
+ // ─── Detection ────────────────────────────────────────────────────────────────
101
+
102
+ /**
103
+ * Return `true` when the node carries a `data-agent-native-component`
104
+ * annotation — i.e. it is the root of an Alpine or real-app component
105
+ * instance.
106
+ */
107
+ export function isComponentInstance(node: CodeLayerNode): boolean {
108
+ return typeof node.dataAttributes[COMPONENT_NAME_ATTR] === "string";
109
+ }
110
+
111
+ /**
112
+ * Return the component name declared on the node, or `null` when the node is
113
+ * not a component root.
114
+ */
115
+ export function componentNameFor(node: CodeLayerNode): string | null {
116
+ const raw = node.dataAttributes[COMPONENT_NAME_ATTR];
117
+ if (typeof raw !== "string" || !raw.trim()) return null;
118
+ return raw.trim();
119
+ }
120
+
121
+ /**
122
+ * Extract simple prop values from `data-agent-native-prop-*` attributes on a
123
+ * code-layer node.
124
+ *
125
+ * The attribute names are lower-cased by the HTML parser so we convert
126
+ * `data-agent-native-prop-variant` → `{ name: "variant", value: "..." }`.
127
+ */
128
+ export function extractProps(node: CodeLayerNode): ComponentPropValue[] {
129
+ const props: ComponentPropValue[] = [];
130
+ for (const [attr, value] of Object.entries(node.dataAttributes)) {
131
+ if (!attr.startsWith(COMPONENT_PROP_PREFIX)) continue;
132
+ const rawName = attr.slice(COMPONENT_PROP_PREFIX.length);
133
+ // Convert kebab-case attribute suffix to camelCase prop name.
134
+ const name = rawName.replace(/-([a-z])/g, (_, c: string) =>
135
+ c.toUpperCase(),
136
+ );
137
+ if (name) props.push({ name, value });
138
+ }
139
+ return props;
140
+ }
141
+
142
+ /**
143
+ * Build a `ComponentInstance` from a `CodeLayerNode` that carries a
144
+ * `data-agent-native-component` attribute.
145
+ *
146
+ * Returns `null` when the node is not a component root.
147
+ */
148
+ export function instanceFromNode(
149
+ node: CodeLayerNode,
150
+ componentIndexId?: string,
151
+ ): ComponentInstance | null {
152
+ const name = componentNameFor(node);
153
+ if (!name) return null;
154
+
155
+ // `x-data` is stored as a plain attribute (not a data-* attribute) so we
156
+ // look in `node.attributes` rather than `node.dataAttributes`.
157
+ const alpineDataRaw = node.attributes["x-data"];
158
+ const alpineData =
159
+ typeof alpineDataRaw === "string" ? alpineDataRaw : undefined;
160
+
161
+ return {
162
+ instanceId: node.id,
163
+ name,
164
+ props: extractProps(node),
165
+ alpineData,
166
+ selector: node.selector,
167
+ nodeId: node.id,
168
+ componentIndexId,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Scan a flat list of `CodeLayerNode` objects and return all component
174
+ * instances, preserving document order.
175
+ *
176
+ * Accepts an optional `indexMap` — a `Map<name, componentIndexId>` — which is
177
+ * populated by `index-components` after writing to `component_index` and used
178
+ * to correlate instances with their persisted metadata.
179
+ */
180
+ export function detectInstances(
181
+ nodes: CodeLayerNode[],
182
+ indexMap?: Map<string, string>,
183
+ ): ComponentInstance[] {
184
+ const instances: ComponentInstance[] = [];
185
+ for (const node of nodes) {
186
+ if (!isComponentInstance(node)) continue;
187
+ const name = componentNameFor(node);
188
+ if (!name) continue;
189
+ const componentIndexId = indexMap?.get(name);
190
+ const instance = instanceFromNode(node, componentIndexId);
191
+ if (instance) instances.push(instance);
192
+ }
193
+ return instances;
194
+ }
195
+
196
+ // ─── Unique component definitions ────────────────────────────────────────────
197
+
198
+ /**
199
+ * A distinct component definition derived by collapsing all instances of the
200
+ * same name into one entry.
201
+ */
202
+ export interface ComponentDefinition {
203
+ name: string;
204
+ /** All instance node ids for this component. */
205
+ instanceNodeIds: string[];
206
+ /** Union of all prop names seen across all instances. */
207
+ observedPropNames: string[];
208
+ }
209
+
210
+ /**
211
+ * Collapse a flat list of `ComponentInstance` objects into one
212
+ * `ComponentDefinition` per unique component name.
213
+ */
214
+ export function buildDefinitions(
215
+ instances: ComponentInstance[],
216
+ ): ComponentDefinition[] {
217
+ const map = new Map<
218
+ string,
219
+ { instanceNodeIds: string[]; propNames: Set<string> }
220
+ >();
221
+
222
+ for (const instance of instances) {
223
+ let entry = map.get(instance.name);
224
+ if (!entry) {
225
+ entry = { instanceNodeIds: [], propNames: new Set() };
226
+ map.set(instance.name, entry);
227
+ }
228
+ entry.instanceNodeIds.push(instance.nodeId);
229
+ for (const prop of instance.props) {
230
+ entry.propNames.add(prop.name);
231
+ }
232
+ }
233
+
234
+ return Array.from(map.entries()).map(([name, entry]) => ({
235
+ name,
236
+ instanceNodeIds: entry.instanceNodeIds,
237
+ observedPropNames: Array.from(entry.propNames),
238
+ }));
239
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Accessibility audit and visual-diff review types for the Design Studio
3
+ * Review panel (§6.5 + §4.3).
4
+ *
5
+ * Results are produced by `run-design-audit` over the rendered DOM and cached
6
+ * in `design_review_snapshot` rows. Fix actions are capability-gated (semantic
7
+ * code fixes are real-app only).
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Accessibility findings
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export const A11Y_FINDING_SEVERITIES = ["error", "warning", "info"] as const;
15
+
16
+ export type A11ySeverity = (typeof A11Y_FINDING_SEVERITIES)[number];
17
+
18
+ export const A11Y_FINDING_CATEGORIES = [
19
+ "contrast",
20
+ "tap-target",
21
+ "focus-visibility",
22
+ "missing-label",
23
+ "missing-alt",
24
+ "reduced-motion",
25
+ "role",
26
+ "other",
27
+ ] as const;
28
+
29
+ export type A11yFindingCategory = (typeof A11Y_FINDING_CATEGORIES)[number];
30
+
31
+ export interface A11yFinding {
32
+ /** Stable identifier for deduplication and navigation (e.g. `"contrast:node-42"`). */
33
+ id: string;
34
+ severity: A11ySeverity;
35
+ category: A11yFindingCategory;
36
+ /** Short human-readable summary (e.g. "Contrast ratio 2.1:1 — minimum is 4.5:1"). */
37
+ message: string;
38
+ /** Optional longer description or remediation guidance. */
39
+ detail?: string;
40
+ /**
41
+ * The `data-agent-native-node-id` of the offending element, when available.
42
+ * Used to navigate the canvas to the affected layer.
43
+ */
44
+ nodeId?: string;
45
+ /** CSS selector as a fallback when `nodeId` is absent. */
46
+ selector?: string;
47
+ /** WCAG success criterion reference (e.g. "1.4.3"). */
48
+ wcag?: string;
49
+ /**
50
+ * Whether a fix action is available for this finding.
51
+ * Semantic code fixes are real-app only; contrast/alt fixes may be available
52
+ * in Alpine via the deterministic write path.
53
+ */
54
+ fixAvailable: boolean;
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Inline auto-fix mapping
59
+ // ---------------------------------------------------------------------------
60
+ //
61
+ // Some a11y findings can be repaired inline against the SQL-backed HTML design
62
+ // content using the same deterministic edit primitives the visual editor uses
63
+ // (`apply-visual-edit`: style / class / textContent). Those primitives can set
64
+ // an inline style value, add/remove/replace a class token, or rewrite leaf text
65
+ // — so the fixes we can apply purely inline are the ones that reduce to one of
66
+ // those operations on a *targeted* node:
67
+ //
68
+ // - contrast / color → set an inline `color` (style edit) or swap a text
69
+ // color class, raising the foreground contrast.
70
+ // - tap-target → add a min-size utility class (e.g. `min-h-[44px]`).
71
+ // - focus-visibility → add a `focus-visible:ring-2` utility class.
72
+ //
73
+ // Fixes that require writing a *new attribute* (alt, aria-label,
74
+ // aria-labelledby) or semantic/structural code changes are NOT expressible
75
+ // through the deterministic edit engine's exported intents, so they remain
76
+ // "real-app only" and are surfaced as informational findings (no inline Fix).
77
+ // `a11yFindingToEdit` returns `null` for those.
78
+
79
+ /**
80
+ * A single deterministic edit that repairs an a11y finding inline. The shape is
81
+ * a strict subset of the `apply-visual-edit` `EditIntent` union — only the kinds
82
+ * the inline (SQL HTML) edit engine can apply without escalating: `style`,
83
+ * `class`, and `textContent`. The `apply-a11y-fix` action forwards this verbatim
84
+ * to the shared `applyVisualEdit` primitive.
85
+ */
86
+ export type A11yFixEdit =
87
+ | {
88
+ kind: "style";
89
+ target: { nodeId?: string; selector?: string };
90
+ property: string;
91
+ value: string;
92
+ }
93
+ | {
94
+ kind: "class";
95
+ target: { nodeId?: string; selector?: string };
96
+ operation: "add" | "remove" | "replace";
97
+ className?: string;
98
+ classNames?: string[];
99
+ from?: string;
100
+ to?: string;
101
+ }
102
+ | {
103
+ kind: "textContent";
104
+ target: { nodeId?: string; selector?: string };
105
+ value: string;
106
+ };
107
+
108
+ /**
109
+ * A planned inline fix for a finding: the deterministic edit to apply plus a
110
+ * short human-readable label for the UI / agent.
111
+ */
112
+ export interface A11yFixPlan {
113
+ finding: A11yFinding;
114
+ edit: A11yFixEdit;
115
+ /** Short human summary, e.g. "Raise text contrast" or "Enlarge tap target". */
116
+ label: string;
117
+ }
118
+
119
+ /**
120
+ * A high-contrast foreground color used as the default contrast remediation
121
+ * when a finding does not carry an explicit replacement color. Near-black keeps
122
+ * ≥ 4.5:1 against typical light backgrounds; the agent can refine afterward.
123
+ */
124
+ const DEFAULT_CONTRAST_COLOR = "#111827";
125
+
126
+ /** Categories whose default inline fix is a class addition, with the utility. */
127
+ const CLASS_ADD_FIX: Partial<Record<A11yFindingCategory, string>> = {
128
+ "tap-target": "min-h-[44px] min-w-[44px]",
129
+ "focus-visibility": "focus-visible:ring-2",
130
+ };
131
+
132
+ /**
133
+ * Map an {@link A11yFinding} to a deterministic inline {@link A11yFixPlan}, or
134
+ * `null` when the finding is not auto-fixable through the inline edit engine.
135
+ *
136
+ * Pure and dependency-free so both the Review panel (to decide whether to show
137
+ * a "Fix" affordance) and the `apply-a11y-fix` action (to compute the edit)
138
+ * share one source of truth.
139
+ *
140
+ * @param finding The audit finding.
141
+ * @param overrides Optional caller-supplied values — e.g. a chosen replacement
142
+ * `color` for contrast fixes — that win over the defaults.
143
+ */
144
+ export function a11yFindingToEdit(
145
+ finding: A11yFinding,
146
+ overrides?: { color?: string },
147
+ ): A11yFixPlan | null {
148
+ // A target is required for every inline edit — without a node id or selector
149
+ // there is nothing to anchor the deterministic patch to.
150
+ const target =
151
+ finding.nodeId || finding.selector
152
+ ? { nodeId: finding.nodeId, selector: finding.selector }
153
+ : null;
154
+ if (!target) return null;
155
+
156
+ if (finding.category === "contrast") {
157
+ const color = (overrides?.color ?? "").trim() || DEFAULT_CONTRAST_COLOR;
158
+ return {
159
+ finding,
160
+ label: "Raise text contrast",
161
+ edit: { kind: "style", target, property: "color", value: color },
162
+ };
163
+ }
164
+
165
+ const classToAdd = CLASS_ADD_FIX[finding.category];
166
+ if (classToAdd) {
167
+ return {
168
+ finding,
169
+ label:
170
+ finding.category === "tap-target"
171
+ ? "Enlarge tap target"
172
+ : "Add focus-visible ring",
173
+ edit: {
174
+ kind: "class",
175
+ target,
176
+ operation: "add",
177
+ classNames: classToAdd.split(/\s+/).filter(Boolean),
178
+ },
179
+ };
180
+ }
181
+
182
+ // missing-alt, missing-label, reduced-motion, role, other → require new
183
+ // attributes or semantic/structural rewrites the inline engine can't express.
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Whether a finding can be auto-fixed inline (i.e. {@link a11yFindingToEdit}
189
+ * returns a plan). Convenience wrapper for UI gating.
190
+ */
191
+ export function isA11yFindingAutoFixable(finding: A11yFinding): boolean {
192
+ return a11yFindingToEdit(finding) !== null;
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Visual diff
197
+ // ---------------------------------------------------------------------------
198
+
199
+ export const VISUAL_DIFF_CHANGE_KINDS = [
200
+ "added",
201
+ "removed",
202
+ "modified",
203
+ "moved",
204
+ ] as const;
205
+
206
+ export type VisualDiffChangeKind = (typeof VISUAL_DIFF_CHANGE_KINDS)[number];
207
+
208
+ /**
209
+ * One changed surface between two design versions.
210
+ */
211
+ export interface VisualDiffEntry {
212
+ id: string;
213
+ kind: VisualDiffChangeKind;
214
+ /**
215
+ * The `data-agent-native-node-id` of the changed element, when resolvable.
216
+ */
217
+ nodeId?: string;
218
+ /** CSS selector fallback when `nodeId` is absent. */
219
+ selector?: string;
220
+ /** Human-readable description of the change (e.g. "Background color changed"). */
221
+ description?: string;
222
+ /**
223
+ * Bounding box of the changed region in the before/after screenshot,
224
+ * expressed as fractions [0, 1] of the frame dimensions.
225
+ */
226
+ region?: {
227
+ x: number;
228
+ y: number;
229
+ width: number;
230
+ height: number;
231
+ };
232
+ /** Before screenshot crop URL or data URL, when available. */
233
+ beforeImageUrl?: string;
234
+ /** After screenshot crop URL or data URL, when available. */
235
+ afterImageUrl?: string;
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Review snapshot
240
+ // ---------------------------------------------------------------------------
241
+
242
+ export const DESIGN_REVIEW_STATUSES = [
243
+ "pending",
244
+ "running",
245
+ "done",
246
+ "error",
247
+ ] as const;
248
+
249
+ export type DesignReviewStatus = (typeof DESIGN_REVIEW_STATUSES)[number];
250
+
251
+ /**
252
+ * Cached accessibility + visual-diff results for a design, optionally scoped
253
+ * to a base/compare version pair. Stored in `design_review_snapshot` rows.
254
+ */
255
+ export interface DesignReviewSnapshot {
256
+ id: string;
257
+ designId: string;
258
+ /**
259
+ * Opaque source reference identifying the screen or file this snapshot
260
+ * covers (fileId for inline, routeId for localhost/fusion).
261
+ * `null` when the snapshot covers the entire design.
262
+ */
263
+ sourceRef: string | null;
264
+ /** The older `design_versions` id used as the diff base. `null` for a11y-only runs. */
265
+ baseVersionId: string | null;
266
+ /** The newer `design_versions` id being compared against `baseVersionId`. */
267
+ compareVersionId: string | null;
268
+ a11yFindings: A11yFinding[];
269
+ visualDiff: VisualDiffEntry[];
270
+ status: DesignReviewStatus;
271
+ /** Error message when `status` is `"error"`. */
272
+ errorMessage?: string;
273
+ createdAt: string;
274
+ updatedAt: string;
275
+ }