@usefragments/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-243QYRUF.js +2145 -0
  3. package/dist/chunk-243QYRUF.js.map +1 -0
  4. package/dist/chunk-3LLRNCPX.js +111 -0
  5. package/dist/chunk-3LLRNCPX.js.map +1 -0
  6. package/dist/chunk-EIYNNS77.js +48 -0
  7. package/dist/chunk-EIYNNS77.js.map +1 -0
  8. package/dist/chunk-JNBFJ34I.js +326 -0
  9. package/dist/chunk-JNBFJ34I.js.map +1 -0
  10. package/dist/chunk-R3X7UBHU.js +228 -0
  11. package/dist/chunk-R3X7UBHU.js.map +1 -0
  12. package/dist/chunk-SH4KPIYH.js +393 -0
  13. package/dist/chunk-SH4KPIYH.js.map +1 -0
  14. package/dist/chunk-V4VQB57N.js +56 -0
  15. package/dist/chunk-V4VQB57N.js.map +1 -0
  16. package/dist/chunk-X34IA4LR.js +319 -0
  17. package/dist/chunk-X34IA4LR.js.map +1 -0
  18. package/dist/chunk-YF65VYRY.js +493 -0
  19. package/dist/chunk-YF65VYRY.js.map +1 -0
  20. package/dist/codes/index.d.ts +40 -0
  21. package/dist/codes/index.js +23 -0
  22. package/dist/codes/index.js.map +1 -0
  23. package/dist/compiled-types/index.d.ts +5 -0
  24. package/dist/compiled-types/index.js +1 -0
  25. package/dist/compiled-types/index.js.map +1 -0
  26. package/dist/generate/index.d.ts +47 -0
  27. package/dist/generate/index.js +11 -0
  28. package/dist/generate/index.js.map +1 -0
  29. package/dist/governance-BOa3KyiJ.d.ts +3279 -0
  30. package/dist/graph/index.d.ts +66 -0
  31. package/dist/graph/index.js +17 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index.d.ts +11388 -0
  34. package/dist/index.js +8711 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/package-identity.d.ts +42 -0
  37. package/dist/package-identity.js +23 -0
  38. package/dist/package-identity.js.map +1 -0
  39. package/dist/preview/index.d.ts +484 -0
  40. package/dist/preview/index.js +1129 -0
  41. package/dist/preview/index.js.map +1 -0
  42. package/dist/react-types.d.ts +22 -0
  43. package/dist/react-types.js +1 -0
  44. package/dist/react-types.js.map +1 -0
  45. package/dist/registry.d.ts +2367 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/schemas/index.d.ts +2584 -0
  49. package/dist/schemas/index.js +55 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/severity.d.ts +18 -0
  52. package/dist/severity.js +27 -0
  53. package/dist/severity.js.map +1 -0
  54. package/dist/test-utils.d.ts +14 -0
  55. package/dist/test-utils.js +63 -0
  56. package/dist/test-utils.js.map +1 -0
  57. package/dist/topology/index.d.ts +48 -0
  58. package/dist/topology/index.js +7 -0
  59. package/dist/topology/index.js.map +1 -0
  60. package/dist/types-xJ2xyp_G.d.ts +119 -0
  61. package/package.json +120 -0
  62. package/src/__tests__/contract-parser.test.ts +614 -0
  63. package/src/__tests__/dtcg-generators.test.ts +439 -0
  64. package/src/__tests__/dtcg-parser.test.ts +884 -0
  65. package/src/__tests__/preview-runtime.test.tsx +111 -0
  66. package/src/__tests__/schema.test.ts +31 -0
  67. package/src/__tests__/severity.test.ts +34 -0
  68. package/src/__tests__/style-comparison.test.ts +662 -0
  69. package/src/__tests__/tokens-compat.test.ts +51 -0
  70. package/src/agent-format.test.ts +105 -0
  71. package/src/agent-format.ts +141 -0
  72. package/src/bundle.ts +149 -0
  73. package/src/codes/__tests__/codes.test.ts +73 -0
  74. package/src/codes/codes.ts +463 -0
  75. package/src/codes/index.ts +7 -0
  76. package/src/codes/source-finding-bridge.ts +113 -0
  77. package/src/compiled-types/index.ts +293 -0
  78. package/src/component-contract.ts +231 -0
  79. package/src/component-discovery.ts +80 -0
  80. package/src/composition.test.ts +262 -0
  81. package/src/composition.ts +318 -0
  82. package/src/config-paths.test.ts +78 -0
  83. package/src/config-paths.ts +47 -0
  84. package/src/config.ts +18 -0
  85. package/src/conform-prove.test.ts +274 -0
  86. package/src/conform-prove.ts +326 -0
  87. package/src/conform.ts +160 -0
  88. package/src/constants.ts +114 -0
  89. package/src/context.ts +2 -0
  90. package/src/contract/fixture.ts +110 -0
  91. package/src/contract/hash.test.ts +86 -0
  92. package/src/contract/hash.ts +195 -0
  93. package/src/contract/index.ts +43 -0
  94. package/src/contract/preimage.test.ts +254 -0
  95. package/src/contract/preimage.ts +365 -0
  96. package/src/contract/stamp.test.ts +100 -0
  97. package/src/contract/stamp.ts +105 -0
  98. package/src/contract-parser.ts +141 -0
  99. package/src/defineFragment.test.ts +257 -0
  100. package/src/defineFragment.ts +293 -0
  101. package/src/dtcg-generators.ts +324 -0
  102. package/src/dtcg-parser.ts +1 -0
  103. package/src/dtcg.ts +237 -0
  104. package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
  105. package/src/facts/builders.ts +835 -0
  106. package/src/facts/compile.ts +422 -0
  107. package/src/facts/fact-index.ts +394 -0
  108. package/src/facts/facts.test.ts +494 -0
  109. package/src/facts/ids.ts +125 -0
  110. package/src/facts/index.ts +119 -0
  111. package/src/facts/types.ts +614 -0
  112. package/src/figma.ts +263 -0
  113. package/src/fragment-types.ts +214 -0
  114. package/src/generate/generate.test.ts +594 -0
  115. package/src/generate/index.ts +438 -0
  116. package/src/governance-integrity.test.ts +186 -0
  117. package/src/governance-integrity.ts +282 -0
  118. package/src/governance.test.ts +300 -0
  119. package/src/governance.ts +605 -0
  120. package/src/governance.type-test.ts +75 -0
  121. package/src/graph/engine.ts +446 -0
  122. package/src/graph/graph.test.ts +746 -0
  123. package/src/graph/index.ts +32 -0
  124. package/src/graph/serialization.ts +55 -0
  125. package/src/graph/types.ts +178 -0
  126. package/src/index.ts +891 -0
  127. package/src/local-canonical.ts +102 -0
  128. package/src/mcp-snapshot.ts +222 -0
  129. package/src/package-identity-match.ts +33 -0
  130. package/src/package-identity.generated.ts +233 -0
  131. package/src/package-identity.test.ts +99 -0
  132. package/src/package-identity.ts +157 -0
  133. package/src/performance-presets.ts +142 -0
  134. package/src/preview/comparison-artifact.test.ts +228 -0
  135. package/src/preview/comparison-artifact.ts +181 -0
  136. package/src/preview/comparison-context.ts +93 -0
  137. package/src/preview/comparison-evidence.ts +112 -0
  138. package/src/preview/comparison-format.ts +21 -0
  139. package/src/preview/comparison-presentation.ts +187 -0
  140. package/src/preview/comparison-references.test.ts +430 -0
  141. package/src/preview/comparison-references.ts +72 -0
  142. package/src/preview/comparison-score.ts +59 -0
  143. package/src/preview/comparison-styles.ts +197 -0
  144. package/src/preview/comparison.test.ts +559 -0
  145. package/src/preview/comparison.ts +488 -0
  146. package/src/preview/index.ts +10 -0
  147. package/src/preview/types.ts +309 -0
  148. package/src/preview/validation.test.ts +290 -0
  149. package/src/preview/validation.ts +356 -0
  150. package/src/preview-runtime.tsx +144 -0
  151. package/src/raw-html-canonical.test.ts +41 -0
  152. package/src/raw-html-canonical.ts +179 -0
  153. package/src/react-types.ts +21 -0
  154. package/src/registry-install-plan.ts +344 -0
  155. package/src/registry.test.ts +245 -0
  156. package/src/registry.ts +369 -0
  157. package/src/rules/a11y-required-accessible-name.ts +93 -0
  158. package/src/rules/color-math.test.ts +72 -0
  159. package/src/rules/color-math.ts +192 -0
  160. package/src/rules/components-forbidden-prop-value.ts +108 -0
  161. package/src/rules/components-prefer-library.ts +943 -0
  162. package/src/rules/components-unknown-prop.ts +94 -0
  163. package/src/rules/composition-pattern-schema.ts +125 -0
  164. package/src/rules/composition-pattern.test.ts +320 -0
  165. package/src/rules/composition-pattern.ts +271 -0
  166. package/src/rules/emit-gate.test.ts +42 -0
  167. package/src/rules/emit-gate.ts +60 -0
  168. package/src/rules/finding.ts +54 -0
  169. package/src/rules/index.ts +235 -0
  170. package/src/rules/jsx-preferred-component.ts +68 -0
  171. package/src/rules/jsx-preferred-import-path.ts +67 -0
  172. package/src/rules/presets.ts +57 -0
  173. package/src/rules/props-invalid-value.ts +115 -0
  174. package/src/rules/rules.test.ts +3636 -0
  175. package/src/rules/spacing-resolution.ts +184 -0
  176. package/src/rules/styles-no-raw-color.ts +329 -0
  177. package/src/rules/styles-no-raw-dimensions.ts +357 -0
  178. package/src/rules/styles-no-raw-spacing.ts +236 -0
  179. package/src/rules/styles-no-raw-typography.ts +237 -0
  180. package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
  181. package/src/rules/tailwind-arbitrary-color.ts +93 -0
  182. package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
  183. package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
  184. package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
  185. package/src/rules/tailwind-forbidden-palette.ts +107 -0
  186. package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
  187. package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
  188. package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
  189. package/src/rules/tailwind-raw-color-via-token.ts +82 -0
  190. package/src/rules/tailwind-rule-utils.ts +106 -0
  191. package/src/rules/tailwind-unknown-class.test.ts +157 -0
  192. package/src/rules/tailwind-unknown-class.ts +121 -0
  193. package/src/rules/taxonomy.test.ts +238 -0
  194. package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
  195. package/src/rules/tiers.ts +111 -0
  196. package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
  197. package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
  198. package/src/rules/tokens-require-dual-fallback.ts +78 -0
  199. package/src/rules/types.ts +27 -0
  200. package/src/rules/utils.test.ts +110 -0
  201. package/src/rules/utils.ts +599 -0
  202. package/src/schema.ts +427 -0
  203. package/src/schemas/index.ts +275 -0
  204. package/src/schemas/schemas.test.ts +124 -0
  205. package/src/severity.ts +54 -0
  206. package/src/storyAdapter.test.ts +571 -0
  207. package/src/storyAdapter.ts +761 -0
  208. package/src/storyFilters.test.ts +350 -0
  209. package/src/storyFilters.ts +253 -0
  210. package/src/storybook-csf.ts +11 -0
  211. package/src/style-comparison.ts +423 -0
  212. package/src/test-utils.ts +68 -0
  213. package/src/token-parser.ts +2 -0
  214. package/src/token-types.ts +287 -0
  215. package/src/tokens/categories.ts +131 -0
  216. package/src/tokens/color.ts +193 -0
  217. package/src/tokens/design-token-parser.test.ts +159 -0
  218. package/src/tokens/design-token-parser.ts +402 -0
  219. package/src/tokens/dtcg.ts +428 -0
  220. package/src/tokens/index.ts +130 -0
  221. package/src/tokens/lookup.ts +19 -0
  222. package/src/tokens/scss.ts +171 -0
  223. package/src/tokens/tailwind-v4.ts +187 -0
  224. package/src/tokens/types.ts +43 -0
  225. package/src/topology/index.ts +9 -0
  226. package/src/topology/resolve-area.test.ts +174 -0
  227. package/src/topology/resolve-area.ts +215 -0
  228. package/src/types.ts +1057 -0
@@ -0,0 +1,1129 @@
1
+ // src/preview/comparison-context.ts
2
+ function analyzeComparisonContext(comparison, preview, variantId, component, scope) {
3
+ const expected = comparison?.state;
4
+ if (!expected) return { findings: [] };
5
+ const actual = {
6
+ variantId: preview?.variantId ?? variantId,
7
+ theme: preview?.theme,
8
+ viewport: preview?.viewport
9
+ };
10
+ const staleReasons = [];
11
+ const mismatchLabels = [];
12
+ const expectedVariantStillExists = !expected.variantId || component.variants.some((variant) => variant.id === expected.variantId);
13
+ if (scope === "variant" && expected.variantId && expectedVariantStillExists && actual.variantId && expected.variantId !== actual.variantId) {
14
+ staleReasons.push("variant");
15
+ mismatchLabels.push(`variant ${actual.variantId}`);
16
+ }
17
+ if (expected.theme && actual.theme && expected.theme !== actual.theme) {
18
+ staleReasons.push("theme");
19
+ mismatchLabels.push(`theme ${actual.theme}`);
20
+ }
21
+ if (expected.viewport && actual.viewport && !sameViewport(expected.viewport, actual.viewport)) {
22
+ staleReasons.push("viewport");
23
+ mismatchLabels.push(`viewport ${formatViewport(actual.viewport)}`);
24
+ }
25
+ const context = {
26
+ scope,
27
+ captured: expected,
28
+ active: actual,
29
+ stale: staleReasons.length > 0,
30
+ staleReasons
31
+ };
32
+ if (staleReasons.length === 0) return { context, findings: [] };
33
+ return {
34
+ context,
35
+ findings: [
36
+ {
37
+ code: "stale-comparison-signal",
38
+ severity: "warning",
39
+ message: `Stored comparison evidence was captured for ${formatComparisonState(
40
+ expected
41
+ )}; current preview uses ${mismatchLabels.join(", ")}.`
42
+ }
43
+ ]
44
+ };
45
+ }
46
+ function sameViewport(a, b) {
47
+ return a.width === b.width && a.height === b.height && a.background === b.background;
48
+ }
49
+ function formatComparisonState(state) {
50
+ return [
51
+ state.variantId ? `variant ${state.variantId}` : void 0,
52
+ state.theme ? `theme ${state.theme}` : void 0,
53
+ state.viewport ? `viewport ${formatViewport(state.viewport)}` : void 0
54
+ ].filter(Boolean).join(", ");
55
+ }
56
+ function formatViewport(viewport) {
57
+ return `${viewport.width}x${viewport.height}`;
58
+ }
59
+
60
+ // src/preview/comparison-score.ts
61
+ function scoreComparison(parts) {
62
+ const scores = [];
63
+ if (parts.image) {
64
+ scores.push({
65
+ weight: 0.5,
66
+ score: Math.max(0, 100 - parts.image.diffPercentage * 10)
67
+ });
68
+ }
69
+ if (parts.dimensions) {
70
+ const delta = Math.abs(parts.dimensions.delta.width) + Math.abs(parts.dimensions.delta.height);
71
+ scores.push({ weight: 0.15, score: Math.max(0, 100 - delta * 2) });
72
+ }
73
+ if (parts.styles) {
74
+ const matches = parts.styles.properties.filter((property) => property.match).length;
75
+ scores.push({ weight: 0.35, score: matches / parts.styles.properties.length * 100 });
76
+ }
77
+ if (scores.length === 0) return null;
78
+ const totalWeight = scores.reduce((sum, item) => sum + item.weight, 0);
79
+ const weighted = scores.reduce((sum, item) => sum + item.score * item.weight, 0);
80
+ return Math.round(weighted / totalWeight);
81
+ }
82
+ function resolveStatus(score, findings) {
83
+ if (score === null) return "incomplete";
84
+ if (findings.some((finding) => finding.severity === "error")) return "fail";
85
+ if (findings.some((finding) => finding.severity === "warning")) return "review";
86
+ if (score >= 90) return "pass";
87
+ if (score >= 65) return "review";
88
+ return "fail";
89
+ }
90
+ function summarizeStatus(status, score, findings) {
91
+ if (score === null) return "Comparison needs at least one captured signal.";
92
+ if (status === "pass") return `Preview matches the Figma reference (${score}/100).`;
93
+ const issueCount = findings.length === 1 ? "1 issue" : `${findings.length} issues`;
94
+ return `Preview needs ${status === "fail" ? "changes" : "review"} (${score}/100, ${issueCount}).`;
95
+ }
96
+
97
+ // src/preview/comparison-styles.ts
98
+ var COLOR_PROPERTIES = /* @__PURE__ */ new Set([
99
+ "background",
100
+ "backgroundColor",
101
+ "borderColor",
102
+ "color",
103
+ "fill",
104
+ "stroke"
105
+ ]);
106
+ var NUMERIC_PROPERTIES = /* @__PURE__ */ new Set([
107
+ "borderRadius",
108
+ "borderWidth",
109
+ "fontSize",
110
+ "gap",
111
+ "height",
112
+ "letterSpacing",
113
+ "lineHeight",
114
+ "margin",
115
+ "opacity",
116
+ "padding",
117
+ "width"
118
+ ]);
119
+ function compareStyles(design, preview, thresholds, findings) {
120
+ if (!design || !preview) return void 0;
121
+ const properties = [];
122
+ for (const [property, designValue] of Object.entries(design)) {
123
+ if (designValue === void 0 || designValue === "") continue;
124
+ const previewValue = preview[property] ?? "(not set)";
125
+ const match = compareStyleValue(property, designValue, previewValue, thresholds);
126
+ properties.push({ property, design: designValue, preview: previewValue, match });
127
+ if (!match) {
128
+ findings.push({
129
+ code: "style-mismatch",
130
+ severity: "warning",
131
+ message: `${property} differs between Figma and the preview.`,
132
+ property,
133
+ designValue,
134
+ previewValue
135
+ });
136
+ }
137
+ }
138
+ if (properties.length === 0) return void 0;
139
+ return {
140
+ match: properties.every((property) => property.match),
141
+ properties
142
+ };
143
+ }
144
+ function compareStyleValue(property, designValue, previewValue, thresholds) {
145
+ const design = normalizeStyleValue(designValue);
146
+ const preview = normalizeStyleValue(previewValue);
147
+ if (design === preview) return true;
148
+ if (COLOR_PROPERTIES.has(property)) {
149
+ return compareColors(design, preview, thresholds);
150
+ }
151
+ if (NUMERIC_PROPERTIES.has(property)) {
152
+ return compareNumericValues(design, preview, thresholds.numericPx);
153
+ }
154
+ return false;
155
+ }
156
+ function normalizeStyleValue(value) {
157
+ return value.trim().replace(/\s+/g, " ").replace(/^none$/, "");
158
+ }
159
+ function compareNumericValues(valueA, valueB, tolerance) {
160
+ const numberA = Number.parseFloat(valueA);
161
+ const numberB = Number.parseFloat(valueB);
162
+ if (Number.isNaN(numberA) || Number.isNaN(numberB)) return valueA === valueB;
163
+ return Math.abs(numberA - numberB) <= tolerance;
164
+ }
165
+ function compareColors(valueA, valueB, thresholds) {
166
+ const colorA = parseColor(valueA);
167
+ const colorB = parseColor(valueB);
168
+ if (!colorA || !colorB) return valueA === valueB;
169
+ return Math.abs(colorA.r - colorB.r) <= thresholds.colorChannel && Math.abs(colorA.g - colorB.g) <= thresholds.colorChannel && Math.abs(colorA.b - colorB.b) <= thresholds.colorChannel && Math.abs(colorA.a - colorB.a) <= thresholds.alpha;
170
+ }
171
+ function parseColor(value) {
172
+ const normalized = value.trim().toLowerCase();
173
+ if (normalized === "transparent") return { r: 0, g: 0, b: 0, a: 0 };
174
+ const hex = normalized.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/);
175
+ if (hex) return parseHexColor(hex[1]);
176
+ const rgb = normalized.match(/^rgba?\(([^)]+)\)$/);
177
+ if (rgb) return parseRgbColor(rgb[1]);
178
+ return null;
179
+ }
180
+ function parseHexColor(raw) {
181
+ const expanded = raw.length === 3 || raw.length === 4 ? raw.split("").map((part) => part + part).join("") : raw;
182
+ return {
183
+ r: Number.parseInt(expanded.slice(0, 2), 16),
184
+ g: Number.parseInt(expanded.slice(2, 4), 16),
185
+ b: Number.parseInt(expanded.slice(4, 6), 16),
186
+ a: expanded.length === 8 ? Number.parseInt(expanded.slice(6, 8), 16) / 255 : 1
187
+ };
188
+ }
189
+ function parseRgbColor(value) {
190
+ const channels = splitRgbChannels(value);
191
+ if (channels.length < 3 || channels.length > 4) return null;
192
+ const [rawR, rawG, rawB, rawA] = channels;
193
+ const r = parseRgbChannel(rawR);
194
+ const g = parseRgbChannel(rawG);
195
+ const b = parseRgbChannel(rawB);
196
+ const a = rawA === void 0 ? 1 : parseAlphaChannel(rawA);
197
+ if (r === null || g === null || b === null || a === null) return null;
198
+ return { r, g, b, a };
199
+ }
200
+ function splitRgbChannels(value) {
201
+ if (value.includes(",")) {
202
+ return value.split(",").map((part) => part.trim());
203
+ }
204
+ const parts = value.replace(/\s*\/\s*/g, " / ").split(/\s+/).filter(Boolean);
205
+ const slashIndex = parts.indexOf("/");
206
+ if (slashIndex === -1) return parts;
207
+ return [...parts.slice(0, slashIndex), parts[slashIndex + 1]].filter(
208
+ (part) => part !== void 0
209
+ );
210
+ }
211
+ function parseRgbChannel(value) {
212
+ if (value.endsWith("%")) {
213
+ const percentage = Number.parseFloat(value);
214
+ return Number.isFinite(percentage) ? percentage / 100 * 255 : null;
215
+ }
216
+ const channel = Number.parseFloat(value);
217
+ return Number.isFinite(channel) ? channel : null;
218
+ }
219
+ function parseAlphaChannel(value) {
220
+ if (value.endsWith("%")) {
221
+ const percentage = Number.parseFloat(value);
222
+ return Number.isFinite(percentage) ? percentage / 100 : null;
223
+ }
224
+ const alpha = Number.parseFloat(value);
225
+ return Number.isFinite(alpha) ? alpha : null;
226
+ }
227
+
228
+ // src/preview/comparison-format.ts
229
+ function formatPercent(value) {
230
+ return `${round(value)}%`;
231
+ }
232
+ function formatDelta(value) {
233
+ return value > 0 ? `+${round(value)}` : `${round(value)}`;
234
+ }
235
+ function formatSize(size) {
236
+ return `${round(size.width)} x ${round(size.height)}`;
237
+ }
238
+ function formatInteger(value) {
239
+ return Math.round(value).toLocaleString("en-US");
240
+ }
241
+ function round(value) {
242
+ return Math.round(value * 100) / 100;
243
+ }
244
+
245
+ // src/preview/comparison-evidence.ts
246
+ function normalizeImageDiffEvidence(imageDiff, findings) {
247
+ if (!isValidPercentage(imageDiff.diffPercentage)) {
248
+ findings.push({
249
+ code: "invalid-comparison-signal",
250
+ severity: "warning",
251
+ message: "Image diff percentage must be a finite number between 0 and 100."
252
+ });
253
+ return void 0;
254
+ }
255
+ return {
256
+ diffPercentage: imageDiff.diffPercentage,
257
+ changedRegions: normalizeChangedRegions(imageDiff.changedRegions, findings),
258
+ ...imageDiff.diffImageDataUrl && { diffImageDataUrl: imageDiff.diffImageDataUrl },
259
+ ...imageDiff.diffImageUrl && { diffImageUrl: imageDiff.diffImageUrl },
260
+ ...normalizePixelCounts(imageDiff, findings)
261
+ };
262
+ }
263
+ function isPositiveComparisonSize(value) {
264
+ return Number.isFinite(value.width) && Number.isFinite(value.height) && value.width > 0 && value.height > 0;
265
+ }
266
+ function normalizeChangedRegions(regions, findings) {
267
+ if (!regions) return [];
268
+ const validRegions = [];
269
+ for (const region of regions) {
270
+ if (isValidBounds(region)) {
271
+ validRegions.push(region);
272
+ continue;
273
+ }
274
+ findings.push({
275
+ code: "invalid-comparison-signal",
276
+ severity: "warning",
277
+ message: "Changed regions must use finite non-negative coordinates and positive sizes."
278
+ });
279
+ }
280
+ return validRegions;
281
+ }
282
+ function normalizePixelCounts(imageDiff, findings) {
283
+ const { diffPixelCount, totalPixels } = imageDiff;
284
+ if (diffPixelCount === void 0 && totalPixels === void 0) return {};
285
+ if (!Number.isInteger(diffPixelCount) || !Number.isInteger(totalPixels) || diffPixelCount === void 0 || totalPixels === void 0 || diffPixelCount < 0 || totalPixels <= 0 || diffPixelCount > totalPixels) {
286
+ findings.push({
287
+ code: "invalid-comparison-signal",
288
+ severity: "warning",
289
+ message: "Image diff pixel counts must be whole numbers with changed pixels less than or equal to total pixels."
290
+ });
291
+ return {};
292
+ }
293
+ return { diffPixelCount, totalPixels };
294
+ }
295
+ function isValidPercentage(value) {
296
+ return Number.isFinite(value) && value >= 0 && value <= 100;
297
+ }
298
+ function isValidBounds(value) {
299
+ return Number.isFinite(value.x) && Number.isFinite(value.y) && Number.isFinite(value.width) && Number.isFinite(value.height) && value.x >= 0 && value.y >= 0 && value.width > 0 && value.height > 0;
300
+ }
301
+
302
+ // src/preview/comparison-references.ts
303
+ function parseFigmaNodeUrl(url) {
304
+ const parsed = parseUrl(url);
305
+ if (!parsed || !isFigmaHostname(parsed.hostname)) return null;
306
+ const [, surface, fileKey] = parsed.pathname.split("/");
307
+ if (surface !== "file" && surface !== "design" || !fileKey) return null;
308
+ const nodeId = parsed.searchParams.get("node-id");
309
+ if (!nodeId) return null;
310
+ return { fileKey, nodeId: nodeId.replace(/-/g, ":") };
311
+ }
312
+ function getPreviewDesignReference(component, variantId) {
313
+ const variant = variantId ? component.variants.find((candidate) => candidate.id === variantId) : void 0;
314
+ return normalizePreviewDesignReference(variant?.design ?? component.design);
315
+ }
316
+ function buildFigmaDesignSource(reference, fallbackLabel) {
317
+ const normalized = normalizePreviewDesignReference(reference);
318
+ return {
319
+ kind: "figma",
320
+ label: normalized?.label ?? fallbackLabel,
321
+ fileKey: normalized?.fileKey,
322
+ nodeId: normalized?.nodeId,
323
+ nodeUrl: normalized?.nodeUrl,
324
+ imageUrl: normalized?.imageUrl,
325
+ size: normalized?.size
326
+ };
327
+ }
328
+ function normalizePreviewDesignReference(reference) {
329
+ if (!reference) return void 0;
330
+ if (reference.fileKey && reference.nodeId) return reference;
331
+ if (!reference.nodeUrl) return reference;
332
+ const parsed = parseFigmaNodeUrl(reference.nodeUrl);
333
+ if (!parsed) return reference;
334
+ return {
335
+ ...reference,
336
+ fileKey: reference.fileKey ?? parsed.fileKey,
337
+ nodeId: reference.nodeId ?? parsed.nodeId
338
+ };
339
+ }
340
+ function isFigmaHostname(hostname) {
341
+ return hostname === "figma.com" || hostname.endsWith(".figma.com");
342
+ }
343
+ function parseUrl(url) {
344
+ try {
345
+ return new URL(url.includes("://") ? url : `https://${url}`);
346
+ } catch {
347
+ return null;
348
+ }
349
+ }
350
+
351
+ // src/preview/comparison.ts
352
+ var DEFAULT_THRESHOLDS = {
353
+ imageDiffPercentage: 1,
354
+ dimensionPx: 1,
355
+ numericPx: 1,
356
+ colorChannel: 5,
357
+ alpha: 0.05
358
+ };
359
+ function comparePreviewToDesign(input) {
360
+ const thresholds = resolvePreviewComparisonThresholds(input.thresholds);
361
+ const findings = [...input.findings ?? []];
362
+ const image = compareImageDiff(input.imageDiff, thresholds, findings);
363
+ const dimensions = compareDimensions(input.design.size, input.preview.size, thresholds, findings);
364
+ const styles = compareStyles(input.design.styles, input.preview.styles, thresholds, findings);
365
+ const score = scoreComparison({ image, dimensions, styles });
366
+ if (score === null) {
367
+ findings.push({
368
+ code: "missing-comparison-signal",
369
+ severity: "warning",
370
+ message: "No image, dimension, or style comparison data was provided."
371
+ });
372
+ }
373
+ const status = resolveStatus(score, findings);
374
+ return {
375
+ status,
376
+ score,
377
+ summary: summarizeStatus(status, score, findings),
378
+ design: input.design,
379
+ preview: input.preview,
380
+ thresholds,
381
+ ...image && { image },
382
+ ...dimensions && { dimensions },
383
+ ...styles && { styles },
384
+ findings,
385
+ ...input.context && { context: input.context }
386
+ };
387
+ }
388
+ function resolvePreviewComparisonThresholds(thresholds) {
389
+ return {
390
+ imageDiffPercentage: readThreshold(
391
+ thresholds?.imageDiffPercentage,
392
+ DEFAULT_THRESHOLDS.imageDiffPercentage,
393
+ { min: 0, max: 100 }
394
+ ),
395
+ dimensionPx: readThreshold(thresholds?.dimensionPx, DEFAULT_THRESHOLDS.dimensionPx, {
396
+ min: 0
397
+ }),
398
+ numericPx: readThreshold(thresholds?.numericPx, DEFAULT_THRESHOLDS.numericPx, { min: 0 }),
399
+ colorChannel: readThreshold(thresholds?.colorChannel, DEFAULT_THRESHOLDS.colorChannel, {
400
+ min: 0,
401
+ max: 255
402
+ }),
403
+ alpha: readThreshold(thresholds?.alpha, DEFAULT_THRESHOLDS.alpha, { min: 0, max: 1 })
404
+ };
405
+ }
406
+ function buildPreviewComparisonReport(input) {
407
+ const variant = input.variantId ? input.component.variants.find((candidate) => candidate.id === input.variantId) : void 0;
408
+ const rawReference = input.designReference ?? variant?.design ?? input.component.design;
409
+ const reference = normalizePreviewDesignReference(rawReference);
410
+ if (!reference) return null;
411
+ const fallbackLabel = `${input.component.displayName}${variant ? ` / ${variant.name}` : ""}`;
412
+ const comparison = reference.comparison;
413
+ const comparisonContext = analyzeComparisonContext(
414
+ comparison,
415
+ input.preview,
416
+ input.variantId,
417
+ input.component,
418
+ resolveDesignReferenceScope(input, rawReference, variant)
419
+ );
420
+ const scoredComparison = comparisonContext.findings.length === 0 ? comparison : void 0;
421
+ const imageDiff = input.imageDiff ?? imageDiffFromComparison(scoredComparison);
422
+ const previewSize = scoredComparison?.previewSize ?? input.preview?.size;
423
+ return comparePreviewToDesign({
424
+ design: {
425
+ ...buildFigmaDesignSource(reference, fallbackLabel),
426
+ ...scoredComparison?.designStyles && { styles: scoredComparison.designStyles }
427
+ },
428
+ preview: {
429
+ kind: "preview",
430
+ label: input.preview?.label ?? fallbackLabel,
431
+ componentKey: input.preview?.componentKey ?? input.component.componentKey,
432
+ ...(input.preview?.variantId ?? input.variantId) && {
433
+ variantId: input.preview?.variantId ?? input.variantId
434
+ },
435
+ ...input.preview?.theme && { theme: input.preview.theme },
436
+ ...input.preview?.viewport && { viewport: input.preview.viewport },
437
+ ...previewSize && { size: previewSize },
438
+ ...(input.preview?.styles ?? scoredComparison?.previewStyles) && {
439
+ styles: input.preview?.styles ?? scoredComparison?.previewStyles
440
+ },
441
+ ...input.preview?.imageDataUrl && { imageDataUrl: input.preview.imageDataUrl },
442
+ ...(input.preview?.imageUrl ?? scoredComparison?.previewImageUrl) && {
443
+ imageUrl: input.preview?.imageUrl ?? scoredComparison?.previewImageUrl
444
+ },
445
+ ...(input.preview?.capturedAt ?? scoredComparison?.capturedAt) && {
446
+ capturedAt: input.preview?.capturedAt ?? scoredComparison?.capturedAt
447
+ }
448
+ },
449
+ ...imageDiff && { imageDiff },
450
+ ...input.thresholds && { thresholds: input.thresholds },
451
+ ...comparisonContext.findings.length > 0 && { findings: comparisonContext.findings },
452
+ ...comparisonContext.context && { context: comparisonContext.context }
453
+ });
454
+ }
455
+ function resolveDesignReferenceScope(input, reference, variant) {
456
+ if (input.designReferenceScope) return input.designReferenceScope;
457
+ return reference === variant?.design ? "variant" : "component";
458
+ }
459
+ function buildPreviewDesignComparisonEvidence(report) {
460
+ if (report.context?.stale) return null;
461
+ const state = buildStoredComparisonState(report.preview);
462
+ const previewImageUrl = report.preview.imageUrl ?? report.preview.imageDataUrl;
463
+ const diffImageUrl = report.image?.diffImageUrl ?? report.image?.diffImageDataUrl;
464
+ const designStyles = cleanStyleMap(report.design.styles);
465
+ const previewStyles = cleanStyleMap(report.preview.styles);
466
+ const previewSize = report.preview.size && isPositiveComparisonSize(report.preview.size) ? report.preview.size : report.dimensions?.preview;
467
+ const evidence = {
468
+ ...state && { state },
469
+ ...previewImageUrl && { previewImageUrl },
470
+ ...previewSize && { previewSize },
471
+ ...designStyles && { designStyles },
472
+ ...previewStyles && { previewStyles },
473
+ ...diffImageUrl && { diffImageUrl },
474
+ ...report.image && { diffPercentage: report.image.diffPercentage },
475
+ ...report.image?.diffPixelCount !== void 0 && {
476
+ diffPixelCount: report.image.diffPixelCount
477
+ },
478
+ ...report.image?.totalPixels !== void 0 && {
479
+ totalPixels: report.image.totalPixels
480
+ },
481
+ ...report.image?.changedRegions.length && {
482
+ changedRegions: report.image.changedRegions
483
+ },
484
+ ...report.preview.capturedAt && { capturedAt: report.preview.capturedAt }
485
+ };
486
+ return Object.keys(evidence).length > 0 ? evidence : null;
487
+ }
488
+ function buildPreviewDesignReferenceFromReport(report) {
489
+ const comparison = buildPreviewDesignComparisonEvidence(report);
490
+ const imageUrl = report.design.imageUrl ?? report.design.imageDataUrl;
491
+ return {
492
+ provider: "figma",
493
+ label: report.design.label,
494
+ ...report.design.nodeUrl && { nodeUrl: report.design.nodeUrl },
495
+ ...report.design.fileKey && { fileKey: report.design.fileKey },
496
+ ...report.design.nodeId && { nodeId: report.design.nodeId },
497
+ ...imageUrl && { imageUrl },
498
+ ...report.design.size && isPositiveComparisonSize(report.design.size) && {
499
+ size: report.design.size
500
+ },
501
+ ...comparison && { comparison }
502
+ };
503
+ }
504
+ function buildStoredComparisonState(preview) {
505
+ const state = {
506
+ ...preview.variantId && { variantId: preview.variantId },
507
+ ...preview.theme && { theme: preview.theme },
508
+ ...preview.viewport && { viewport: preview.viewport }
509
+ };
510
+ return Object.keys(state).length > 0 ? state : void 0;
511
+ }
512
+ function cleanStyleMap(styles) {
513
+ if (!styles) return void 0;
514
+ const entries = Object.entries(styles).filter(
515
+ (entry) => typeof entry[1] === "string"
516
+ );
517
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
518
+ }
519
+ function imageDiffFromComparison(comparison) {
520
+ if (!comparison || comparison.diffPercentage === void 0) return void 0;
521
+ return {
522
+ diffPercentage: comparison.diffPercentage,
523
+ changedRegions: comparison.changedRegions,
524
+ diffImageUrl: comparison.diffImageUrl,
525
+ diffPixelCount: comparison.diffPixelCount,
526
+ totalPixels: comparison.totalPixels
527
+ };
528
+ }
529
+ function compareImageDiff(imageDiff, thresholds, findings) {
530
+ if (!imageDiff) return void 0;
531
+ const normalized = normalizeImageDiffEvidence(imageDiff, findings);
532
+ if (!normalized) return void 0;
533
+ const match = normalized.diffPercentage <= thresholds.imageDiffPercentage;
534
+ if (!match) {
535
+ findings.push({
536
+ code: "image-diff-threshold-exceeded",
537
+ severity: "error",
538
+ message: `Image diff ${formatPercent(normalized.diffPercentage)} exceeds ${formatPercent(
539
+ thresholds.imageDiffPercentage
540
+ )}.`,
541
+ bounds: normalized.changedRegions[0]
542
+ });
543
+ }
544
+ return {
545
+ match,
546
+ diffPercentage: round(normalized.diffPercentage),
547
+ diffImageDataUrl: normalized.diffImageDataUrl,
548
+ diffImageUrl: normalized.diffImageUrl,
549
+ changedRegions: normalized.changedRegions,
550
+ diffPixelCount: normalized.diffPixelCount,
551
+ totalPixels: normalized.totalPixels
552
+ };
553
+ }
554
+ function compareDimensions(design, preview, thresholds, findings) {
555
+ if (!design || !preview) return void 0;
556
+ if (!isPositiveComparisonSize(design) || !isPositiveComparisonSize(preview)) {
557
+ findings.push({
558
+ code: "invalid-comparison-signal",
559
+ severity: "warning",
560
+ message: "Design and preview sizes must be finite positive numbers."
561
+ });
562
+ return void 0;
563
+ }
564
+ const delta = {
565
+ width: round(preview.width - design.width),
566
+ height: round(preview.height - design.height)
567
+ };
568
+ const match = Math.abs(delta.width) <= thresholds.dimensionPx && Math.abs(delta.height) <= thresholds.dimensionPx;
569
+ if (!match) {
570
+ findings.push({
571
+ code: "dimension-mismatch",
572
+ severity: "warning",
573
+ message: `Preview is ${formatDelta(delta.width)}px wide and ${formatDelta(
574
+ delta.height
575
+ )}px tall compared with the Figma frame.`
576
+ });
577
+ }
578
+ return { match, design, preview, delta };
579
+ }
580
+ function readThreshold(value, fallback, bounds) {
581
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
582
+ if (value < bounds.min) return fallback;
583
+ if (bounds.max !== void 0 && value > bounds.max) return fallback;
584
+ return value;
585
+ }
586
+
587
+ // src/preview/comparison-artifact.ts
588
+ function buildPreviewComparisonArtifactPayload(input) {
589
+ if (!input.report) return null;
590
+ const report = input.report;
591
+ const designReference = {
592
+ ...input.designReference ?? buildPreviewDesignReferenceFromReport(report)
593
+ };
594
+ const reportEvidence = buildPreviewDesignComparisonEvidence(report);
595
+ const comparisonEvidence = report.context?.stale ? void 0 : input.comparisonEvidence ? { ...input.comparisonEvidence } : designReference.comparison ? { ...designReference.comparison } : reportEvidence ? { ...reportEvidence } : void 0;
596
+ if (report.context?.stale) {
597
+ delete designReference.comparison;
598
+ }
599
+ if (input.assets?.figma) {
600
+ designReference.imageUrl = input.assets.figma;
601
+ }
602
+ if (comparisonEvidence && input.assets?.rendered) {
603
+ comparisonEvidence.previewImageUrl = input.assets.rendered;
604
+ }
605
+ if (comparisonEvidence && input.assets?.diff) {
606
+ comparisonEvidence.diffImageUrl = input.assets.diff;
607
+ }
608
+ if (comparisonEvidence) {
609
+ designReference.comparison = comparisonEvidence;
610
+ }
611
+ return {
612
+ target: buildComparisonArtifactTarget(report),
613
+ report,
614
+ designReference,
615
+ ...comparisonEvidence && { comparisonEvidence }
616
+ };
617
+ }
618
+ function applyPreviewComparisonArtifactPayload(manifest, payload) {
619
+ if (!payload.comparisonEvidence) {
620
+ return {
621
+ status: "missing-comparison-evidence",
622
+ applied: false,
623
+ manifest,
624
+ message: "Comparison payload has no reusable evidence to apply."
625
+ };
626
+ }
627
+ const component = manifest.components[payload.target.componentKey];
628
+ if (!component) {
629
+ return {
630
+ status: "component-not-found",
631
+ applied: false,
632
+ manifest,
633
+ message: `Component ${payload.target.componentKey} was not found in the manifest.`
634
+ };
635
+ }
636
+ if (payload.target.designReferenceScope === "variant") {
637
+ const variantIndex = component.variants.findIndex(
638
+ (variant) => variant.id === payload.target.variantId
639
+ );
640
+ if (variantIndex < 0) {
641
+ return {
642
+ status: "variant-not-found",
643
+ applied: false,
644
+ manifest,
645
+ message: `Variant ${payload.target.variantId ?? "(missing)"} was not found on component ${payload.target.componentKey}.`
646
+ };
647
+ }
648
+ const variants = component.variants.map(
649
+ (variant, index) => index === variantIndex ? { ...variant, design: payload.designReference } : variant
650
+ );
651
+ return {
652
+ status: "applied",
653
+ applied: true,
654
+ manifest: {
655
+ ...manifest,
656
+ components: {
657
+ ...manifest.components,
658
+ [payload.target.componentKey]: {
659
+ ...component,
660
+ variants
661
+ }
662
+ }
663
+ },
664
+ message: `Applied comparison evidence to ${payload.target.componentKey} variant ${payload.target.variantId}.`
665
+ };
666
+ }
667
+ return {
668
+ status: "applied",
669
+ applied: true,
670
+ manifest: {
671
+ ...manifest,
672
+ components: {
673
+ ...manifest.components,
674
+ [payload.target.componentKey]: {
675
+ ...component,
676
+ design: payload.designReference
677
+ }
678
+ }
679
+ },
680
+ message: `Applied comparison evidence to ${payload.target.componentKey}.`
681
+ };
682
+ }
683
+ function buildComparisonArtifactTarget(report) {
684
+ return {
685
+ componentKey: report.preview.componentKey,
686
+ ...report.preview.variantId && { variantId: report.preview.variantId },
687
+ designReferenceScope: report.context?.scope ?? (report.preview.variantId ? "variant" : "component")
688
+ };
689
+ }
690
+
691
+ // src/preview/comparison-presentation.ts
692
+ function getPreviewComparisonSignals(report) {
693
+ return [buildImageSignal(report), buildDimensionSignal(report), buildStyleSignal(report)];
694
+ }
695
+ function getPreviewComparisonRegionOverlays(report) {
696
+ const sourceSize = report.dimensions?.preview ?? report.preview.size ?? report.design.size;
697
+ if (!sourceSize || sourceSize.width <= 0 || sourceSize.height <= 0) return [];
698
+ return (report.image?.changedRegions ?? []).flatMap((region) => {
699
+ const clipped = clipRegionToSize(region, sourceSize);
700
+ if (!clipped) return [];
701
+ return {
702
+ ...region,
703
+ label: formatChangedRegionLabel(region),
704
+ leftPercent: round(clipped.x / sourceSize.width * 100),
705
+ topPercent: round(clipped.y / sourceSize.height * 100),
706
+ widthPercent: round(clipped.width / sourceSize.width * 100),
707
+ heightPercent: round(clipped.height / sourceSize.height * 100)
708
+ };
709
+ });
710
+ }
711
+ function getPreviewComparisonContextSummary(report) {
712
+ if (!report.context) return null;
713
+ const captured = report.context.captured ? formatComparisonState2(report.context.captured, {
714
+ includeVariant: report.context.scope === "variant"
715
+ }) : "unknown capture state";
716
+ const active = formatComparisonState2(report.context.active, {
717
+ includeVariant: report.context.scope === "variant"
718
+ });
719
+ if (report.context.stale) {
720
+ return {
721
+ status: "stale",
722
+ label: "Capture context",
723
+ value: "Stale evidence",
724
+ detail: `Captured ${captured}; current ${active}.`
725
+ };
726
+ }
727
+ return {
728
+ status: "current",
729
+ label: "Capture context",
730
+ value: "Current evidence",
731
+ detail: `Captured ${captured}.`
732
+ };
733
+ }
734
+ function formatPreviewComparisonFinding(finding) {
735
+ const prefix = finding.severity.toUpperCase();
736
+ const subject = finding.property ? `${finding.property}: ` : "";
737
+ return `${prefix}: ${subject}${finding.message}`;
738
+ }
739
+ function formatComparisonState2(state, options) {
740
+ const parts = [
741
+ options.includeVariant && state.variantId ? `variant ${state.variantId}` : void 0,
742
+ state.theme ? `theme ${state.theme}` : void 0,
743
+ state.viewport ? `viewport ${formatSize(state.viewport)}` : void 0
744
+ ].filter((part) => Boolean(part));
745
+ return parts.length > 0 ? parts.join(", ") : "unspecified preview state";
746
+ }
747
+ function buildImageSignal(report) {
748
+ if (!report.image) {
749
+ return {
750
+ id: "image",
751
+ label: "Image diff",
752
+ status: "missing",
753
+ value: "No capture",
754
+ detail: "Capture a Figma image and preview screenshot to compare pixels."
755
+ };
756
+ }
757
+ const changedPixels = report.image.diffPixelCount !== void 0 && report.image.totalPixels !== void 0 ? `${formatInteger(report.image.diffPixelCount)} of ${formatInteger(
758
+ report.image.totalPixels
759
+ )} pixels changed` : `${report.image.changedRegions.length} changed ${report.image.changedRegions.length === 1 ? "region" : "regions"}`;
760
+ return {
761
+ id: "image",
762
+ label: "Image diff",
763
+ status: report.image.match ? "pass" : "fail",
764
+ value: `${formatPercent(report.image.diffPercentage)} changed`,
765
+ detail: `${changedPixels}; threshold ${formatPercent(report.thresholds.imageDiffPercentage)}.`
766
+ };
767
+ }
768
+ function buildDimensionSignal(report) {
769
+ if (!report.dimensions) {
770
+ return {
771
+ id: "dimensions",
772
+ label: "Size",
773
+ status: "missing",
774
+ value: "No size",
775
+ detail: "Provide both Figma frame size and captured preview size."
776
+ };
777
+ }
778
+ return {
779
+ id: "dimensions",
780
+ label: "Size",
781
+ status: report.dimensions.match ? "pass" : "review",
782
+ value: `${formatDelta(report.dimensions.delta.width)} x ${formatDelta(
783
+ report.dimensions.delta.height
784
+ )} px`,
785
+ detail: `${formatSize(report.dimensions.design)} design, ${formatSize(
786
+ report.dimensions.preview
787
+ )} preview; tolerance +/-${report.thresholds.dimensionPx}px.`
788
+ };
789
+ }
790
+ function buildStyleSignal(report) {
791
+ if (!report.styles) {
792
+ return {
793
+ id: "styles",
794
+ label: "Styles",
795
+ status: "missing",
796
+ value: "No styles",
797
+ detail: "Provide computed Figma and preview styles to compare tokens and CSS."
798
+ };
799
+ }
800
+ const matchCount = report.styles.properties.filter((property) => property.match).length;
801
+ const firstMismatch = report.styles.properties.find((property) => !property.match);
802
+ return {
803
+ id: "styles",
804
+ label: "Styles",
805
+ status: report.styles.match ? "pass" : "review",
806
+ value: `${matchCount}/${report.styles.properties.length} matched`,
807
+ detail: firstMismatch ? `${firstMismatch.property}: ${firstMismatch.design} in Figma, ${firstMismatch.preview} in preview.` : "All checked computed styles are within tolerance."
808
+ };
809
+ }
810
+ function formatChangedRegionLabel(region) {
811
+ return `Changed region x ${region.x} y ${region.y} width ${region.width} height ${region.height}`;
812
+ }
813
+ function clipRegionToSize(region, size) {
814
+ const x = Math.max(0, region.x);
815
+ const y = Math.max(0, region.y);
816
+ const right = Math.min(size.width, region.x + region.width);
817
+ const bottom = Math.min(size.height, region.y + region.height);
818
+ if (right <= x || bottom <= y) return null;
819
+ return {
820
+ x,
821
+ y,
822
+ width: right - x,
823
+ height: bottom - y
824
+ };
825
+ }
826
+
827
+ // src/preview/validation.ts
828
+ function definePreviewArtifactManifest(manifest) {
829
+ const validation = validatePreviewArtifactManifest(manifest);
830
+ if (!validation.ok) {
831
+ throw new Error(`Invalid preview artifact manifest:
832
+ ${validation.issues.join("\n")}`);
833
+ }
834
+ return manifest;
835
+ }
836
+ function validatePreviewArtifactManifest(value) {
837
+ const issues = [];
838
+ if (!isRecord(value)) {
839
+ return { ok: false, issues: ["manifest must be an object"] };
840
+ }
841
+ requireLiteral(value, "artifactVersion", "preview-artifact/v1", issues);
842
+ validateIdentity(value.identity, issues);
843
+ validateBuilder(value.builder, issues);
844
+ validateRuntime(value.runtime, issues);
845
+ validateComponents(value.components, issues);
846
+ if (!Array.isArray(value.diagnostics)) {
847
+ issues.push("diagnostics must be an array");
848
+ }
849
+ return { ok: issues.length === 0, issues };
850
+ }
851
+ function isPreviewArtifactManifest(value) {
852
+ return validatePreviewArtifactManifest(value).ok;
853
+ }
854
+ function validateIdentity(value, issues) {
855
+ if (!isRecord(value)) {
856
+ issues.push("identity must be an object");
857
+ return;
858
+ }
859
+ requireLiteral(value, "artifactVersion", "preview-artifact/v1", issues);
860
+ for (const key of [
861
+ "artifactId",
862
+ "orgId",
863
+ "projectId",
864
+ "bindingId",
865
+ "repoFullName",
866
+ "commitSha",
867
+ "sourceRef",
868
+ "createdAt"
869
+ ]) {
870
+ requireString(value, key, issues);
871
+ }
872
+ }
873
+ function validateBuilder(value, issues) {
874
+ if (!isRecord(value)) {
875
+ issues.push("builder must be an object");
876
+ return;
877
+ }
878
+ requireString(value, "name", issues);
879
+ requireString(value, "version", issues);
880
+ if (value.typecheck !== "typescript" && value.typecheck !== "none") {
881
+ issues.push('builder.typecheck must be "typescript" or "none"');
882
+ }
883
+ if (!Array.isArray(value.sources)) {
884
+ issues.push("builder.sources must be an array");
885
+ } else {
886
+ for (const source of value.sources) {
887
+ if (!isPreviewDefinitionSource(source)) {
888
+ issues.push(`builder.sources contains unsupported source ${JSON.stringify(source)}`);
889
+ }
890
+ }
891
+ }
892
+ if (!Array.isArray(value.adapters)) {
893
+ issues.push("builder.adapters must be an array");
894
+ }
895
+ }
896
+ function validateRuntime(value, issues) {
897
+ if (!isRecord(value)) {
898
+ issues.push("runtime must be an object");
899
+ return;
900
+ }
901
+ requireLiteral(value, "framework", "react", issues);
902
+ requireLiteral(value, "moduleFormat", "esm", issues);
903
+ requireString(value, "entry", issues);
904
+ requireString(value, "assetsBaseUrl", issues);
905
+ if (!Array.isArray(value.css)) issues.push("runtime.css must be an array");
906
+ }
907
+ function validateComponents(value, issues) {
908
+ if (!isRecord(value)) {
909
+ issues.push("components must be an object");
910
+ return;
911
+ }
912
+ for (const [key, component] of Object.entries(value)) {
913
+ if (!isRecord(component)) {
914
+ issues.push(`components.${key} must be an object`);
915
+ continue;
916
+ }
917
+ requireString(component, "componentKey", issues, `components.${key}`);
918
+ requireString(component, "publicRef", issues, `components.${key}`);
919
+ requireString(component, "displayName", issues, `components.${key}`);
920
+ requireString(component, "exportName", issues, `components.${key}`);
921
+ requireString(component, "importPath", issues, `components.${key}`);
922
+ requireString(component, "definitionSource", issues, `components.${key}`);
923
+ if (!isPreviewDefinitionSource(component.definitionSource)) {
924
+ issues.push(
925
+ `components.${key}.definitionSource must be one of typescript, fragment, manual, synthesized`
926
+ );
927
+ }
928
+ if (!isRecord(component.props)) {
929
+ issues.push(`components.${key}.props must be an object`);
930
+ }
931
+ if (!Array.isArray(component.variants)) {
932
+ issues.push(`components.${key}.variants must be an array`);
933
+ } else {
934
+ component.variants.forEach((variant, index) => {
935
+ if (!isRecord(variant)) {
936
+ issues.push(`components.${key}.variants.${index} must be an object`);
937
+ return;
938
+ }
939
+ validateDesignReference(
940
+ variant.design,
941
+ issues,
942
+ `components.${key}.variants.${index}.design`
943
+ );
944
+ });
945
+ }
946
+ validateDesignReference(component.design, issues, `components.${key}.design`);
947
+ }
948
+ }
949
+ function validateDesignReference(value, issues, path) {
950
+ if (value === void 0) return;
951
+ if (!isRecord(value)) {
952
+ issues.push(`${path} must be an object`);
953
+ return;
954
+ }
955
+ if (value.provider !== "figma") {
956
+ issues.push(`${path}.provider must be "figma"`);
957
+ }
958
+ for (const key of ["label", "nodeUrl", "fileKey", "nodeId", "imageUrl"]) {
959
+ if (value[key] !== void 0 && typeof value[key] !== "string") {
960
+ issues.push(`${path}.${key} must be a string`);
961
+ }
962
+ }
963
+ validateSize(value.size, issues, `${path}.size`);
964
+ validateComparisonEvidence(value.comparison, issues, `${path}.comparison`, readSize(value.size));
965
+ }
966
+ function validateComparisonEvidence(value, issues, path, designSize) {
967
+ if (value === void 0) return;
968
+ if (!isRecord(value)) {
969
+ issues.push(`${path} must be an object`);
970
+ return;
971
+ }
972
+ for (const key of ["previewImageUrl", "diffImageUrl", "capturedAt"]) {
973
+ if (value[key] !== void 0 && typeof value[key] !== "string") {
974
+ issues.push(`${path}.${key} must be a string`);
975
+ }
976
+ }
977
+ validateSize(value.previewSize, issues, `${path}.previewSize`);
978
+ validateComparisonState(value.state, issues, `${path}.state`);
979
+ const comparisonSize = readSize(value.previewSize) ?? designSize;
980
+ validateStyleMap(value.designStyles, issues, `${path}.designStyles`);
981
+ validateStyleMap(value.previewStyles, issues, `${path}.previewStyles`);
982
+ validateFiniteNumber(value.diffPercentage, issues, `${path}.diffPercentage`);
983
+ validatePixelCounts(value, issues, path);
984
+ if (typeof value.diffPercentage === "number" && (value.diffPercentage < 0 || value.diffPercentage > 100)) {
985
+ issues.push(`${path}.diffPercentage must be between 0 and 100`);
986
+ }
987
+ if (value.changedRegions !== void 0) {
988
+ if (!Array.isArray(value.changedRegions)) {
989
+ issues.push(`${path}.changedRegions must be an array`);
990
+ } else {
991
+ value.changedRegions.forEach(
992
+ (region, index) => validateBounds(region, issues, `${path}.changedRegions.${index}`, comparisonSize)
993
+ );
994
+ }
995
+ }
996
+ }
997
+ function validateComparisonState(value, issues, path) {
998
+ if (value === void 0) return;
999
+ if (!isRecord(value)) {
1000
+ issues.push(`${path} must be an object`);
1001
+ return;
1002
+ }
1003
+ if (value.variantId !== void 0 && typeof value.variantId !== "string") {
1004
+ issues.push(`${path}.variantId must be a string`);
1005
+ }
1006
+ if (value.theme !== void 0 && typeof value.theme !== "string") {
1007
+ issues.push(`${path}.theme must be a string`);
1008
+ }
1009
+ validateSize(value.viewport, issues, `${path}.viewport`);
1010
+ }
1011
+ function validatePixelCounts(value, issues, path) {
1012
+ if (value.diffPixelCount !== void 0) {
1013
+ validateNonNegativeInteger(value.diffPixelCount, issues, `${path}.diffPixelCount`);
1014
+ }
1015
+ if (value.totalPixels !== void 0) {
1016
+ validatePositiveInteger(value.totalPixels, issues, `${path}.totalPixels`);
1017
+ }
1018
+ if (isFiniteNumber(value.diffPixelCount) && isFiniteNumber(value.totalPixels) && value.diffPixelCount > value.totalPixels) {
1019
+ issues.push(`${path}.diffPixelCount must be less than or equal to ${path}.totalPixels`);
1020
+ }
1021
+ }
1022
+ function validateSize(value, issues, path) {
1023
+ if (value === void 0) return;
1024
+ if (!isRecord(value)) {
1025
+ issues.push(`${path} must be an object`);
1026
+ return;
1027
+ }
1028
+ validatePositiveNumber(value.width, issues, `${path}.width`);
1029
+ validatePositiveNumber(value.height, issues, `${path}.height`);
1030
+ }
1031
+ function validateStyleMap(value, issues, path) {
1032
+ if (value === void 0) return;
1033
+ if (!isRecord(value)) {
1034
+ issues.push(`${path} must be an object`);
1035
+ return;
1036
+ }
1037
+ for (const [key, styleValue] of Object.entries(value)) {
1038
+ if (typeof styleValue !== "string") {
1039
+ issues.push(`${path}.${key} must be a string`);
1040
+ }
1041
+ }
1042
+ }
1043
+ function validateBounds(value, issues, path, sourceSize) {
1044
+ if (!isRecord(value)) {
1045
+ issues.push(`${path} must be an object`);
1046
+ return;
1047
+ }
1048
+ validateNonNegativeNumber(value.x, issues, `${path}.x`);
1049
+ validateNonNegativeNumber(value.y, issues, `${path}.y`);
1050
+ validatePositiveNumber(value.width, issues, `${path}.width`);
1051
+ validatePositiveNumber(value.height, issues, `${path}.height`);
1052
+ if (!sourceSize) return;
1053
+ if (isFiniteNumber(value.x) && isFiniteNumber(value.width) && value.x >= 0 && value.width > 0 && value.x + value.width > sourceSize.width) {
1054
+ issues.push(`${path} must fit within comparison source width ${sourceSize.width}`);
1055
+ }
1056
+ if (isFiniteNumber(value.y) && isFiniteNumber(value.height) && value.y >= 0 && value.height > 0 && value.y + value.height > sourceSize.height) {
1057
+ issues.push(`${path} must fit within comparison source height ${sourceSize.height}`);
1058
+ }
1059
+ }
1060
+ function isRecord(value) {
1061
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1062
+ }
1063
+ function isPreviewDefinitionSource(value) {
1064
+ return value === "typescript" || value === "fragment" || value === "manual" || value === "synthesized";
1065
+ }
1066
+ function isFiniteNumber(value) {
1067
+ return typeof value === "number" && Number.isFinite(value);
1068
+ }
1069
+ function validatePositiveNumber(value, issues, path) {
1070
+ if (!isFiniteNumber(value) || value <= 0) {
1071
+ issues.push(`${path} must be a positive number`);
1072
+ }
1073
+ }
1074
+ function validateNonNegativeNumber(value, issues, path) {
1075
+ if (!isFiniteNumber(value) || value < 0) {
1076
+ issues.push(`${path} must be a non-negative number`);
1077
+ }
1078
+ }
1079
+ function validateFiniteNumber(value, issues, path) {
1080
+ if (value !== void 0 && !isFiniteNumber(value)) {
1081
+ issues.push(`${path} must be a finite number`);
1082
+ }
1083
+ }
1084
+ function validatePositiveInteger(value, issues, path) {
1085
+ if (!Number.isInteger(value) || !isFiniteNumber(value) || value <= 0) {
1086
+ issues.push(`${path} must be a positive integer`);
1087
+ }
1088
+ }
1089
+ function validateNonNegativeInteger(value, issues, path) {
1090
+ if (!Number.isInteger(value) || !isFiniteNumber(value) || value < 0) {
1091
+ issues.push(`${path} must be a non-negative integer`);
1092
+ }
1093
+ }
1094
+ function readSize(value) {
1095
+ if (!isRecord(value)) return void 0;
1096
+ if (!isFiniteNumber(value.width) || value.width <= 0) return void 0;
1097
+ if (!isFiniteNumber(value.height) || value.height <= 0) return void 0;
1098
+ return { width: value.width, height: value.height };
1099
+ }
1100
+ function requireString(object, key, issues, path = "") {
1101
+ if (typeof object[key] !== "string" || !object[key]) {
1102
+ issues.push(`${path ? `${path}.` : ""}${key} must be a non-empty string`);
1103
+ }
1104
+ }
1105
+ function requireLiteral(object, key, expected, issues) {
1106
+ if (object[key] !== expected) {
1107
+ issues.push(`${key} must be ${JSON.stringify(expected)}`);
1108
+ }
1109
+ }
1110
+ export {
1111
+ applyPreviewComparisonArtifactPayload,
1112
+ buildFigmaDesignSource,
1113
+ buildPreviewComparisonArtifactPayload,
1114
+ buildPreviewComparisonReport,
1115
+ buildPreviewDesignComparisonEvidence,
1116
+ buildPreviewDesignReferenceFromReport,
1117
+ comparePreviewToDesign,
1118
+ definePreviewArtifactManifest,
1119
+ formatPreviewComparisonFinding,
1120
+ getPreviewComparisonContextSummary,
1121
+ getPreviewComparisonRegionOverlays,
1122
+ getPreviewComparisonSignals,
1123
+ getPreviewDesignReference,
1124
+ isPreviewArtifactManifest,
1125
+ parseFigmaNodeUrl,
1126
+ resolvePreviewComparisonThresholds,
1127
+ validatePreviewArtifactManifest
1128
+ };
1129
+ //# sourceMappingURL=index.js.map