@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,93 @@
1
+ import type {
2
+ PreviewComponentDefinition,
3
+ PreviewDesignComparisonReference,
4
+ PreviewViewport,
5
+ } from "./types";
6
+ import type {
7
+ BuildPreviewComparisonReportInput,
8
+ PreviewComparisonContext,
9
+ PreviewComparisonContextMismatch,
10
+ PreviewComparisonFinding,
11
+ PreviewComparisonReferenceScope,
12
+ } from "./comparison";
13
+
14
+ export function analyzeComparisonContext(
15
+ comparison: PreviewDesignComparisonReference | undefined,
16
+ preview: BuildPreviewComparisonReportInput["preview"] | undefined,
17
+ variantId: string | undefined,
18
+ component: PreviewComponentDefinition,
19
+ scope: PreviewComparisonReferenceScope
20
+ ): { context?: PreviewComparisonContext; findings: PreviewComparisonFinding[] } {
21
+ const expected = comparison?.state;
22
+ if (!expected) return { findings: [] };
23
+ const actual = {
24
+ variantId: preview?.variantId ?? variantId,
25
+ theme: preview?.theme,
26
+ viewport: preview?.viewport,
27
+ };
28
+
29
+ const staleReasons: PreviewComparisonContextMismatch[] = [];
30
+ const mismatchLabels: string[] = [];
31
+
32
+ const expectedVariantStillExists =
33
+ !expected.variantId || component.variants.some((variant) => variant.id === expected.variantId);
34
+ if (
35
+ scope === "variant" &&
36
+ expected.variantId &&
37
+ expectedVariantStillExists &&
38
+ actual.variantId &&
39
+ expected.variantId !== actual.variantId
40
+ ) {
41
+ staleReasons.push("variant");
42
+ mismatchLabels.push(`variant ${actual.variantId}`);
43
+ }
44
+ if (expected.theme && actual.theme && expected.theme !== actual.theme) {
45
+ staleReasons.push("theme");
46
+ mismatchLabels.push(`theme ${actual.theme}`);
47
+ }
48
+ if (expected.viewport && actual.viewport && !sameViewport(expected.viewport, actual.viewport)) {
49
+ staleReasons.push("viewport");
50
+ mismatchLabels.push(`viewport ${formatViewport(actual.viewport)}`);
51
+ }
52
+
53
+ const context = {
54
+ scope,
55
+ captured: expected,
56
+ active: actual,
57
+ stale: staleReasons.length > 0,
58
+ staleReasons,
59
+ };
60
+
61
+ if (staleReasons.length === 0) return { context, findings: [] };
62
+
63
+ return {
64
+ context,
65
+ findings: [
66
+ {
67
+ code: "stale-comparison-signal",
68
+ severity: "warning",
69
+ message: `Stored comparison evidence was captured for ${formatComparisonState(
70
+ expected
71
+ )}; current preview uses ${mismatchLabels.join(", ")}.`,
72
+ },
73
+ ],
74
+ };
75
+ }
76
+
77
+ function sameViewport(a: PreviewViewport, b: PreviewViewport): boolean {
78
+ return a.width === b.width && a.height === b.height && a.background === b.background;
79
+ }
80
+
81
+ function formatComparisonState(state: NonNullable<PreviewDesignComparisonReference["state"]>) {
82
+ return [
83
+ state.variantId ? `variant ${state.variantId}` : undefined,
84
+ state.theme ? `theme ${state.theme}` : undefined,
85
+ state.viewport ? `viewport ${formatViewport(state.viewport)}` : undefined,
86
+ ]
87
+ .filter(Boolean)
88
+ .join(", ");
89
+ }
90
+
91
+ function formatViewport(viewport: PreviewViewport): string {
92
+ return `${viewport.width}x${viewport.height}`;
93
+ }
@@ -0,0 +1,112 @@
1
+ import type {
2
+ PreviewComparisonBounds,
3
+ PreviewComparisonFinding,
4
+ PreviewComparisonSize,
5
+ PreviewImageDiffInput,
6
+ } from "./comparison";
7
+
8
+ export interface NormalizedImageDiffEvidence {
9
+ diffPercentage: number;
10
+ changedRegions: PreviewComparisonBounds[];
11
+ diffImageDataUrl?: string;
12
+ diffImageUrl?: string;
13
+ diffPixelCount?: number;
14
+ totalPixels?: number;
15
+ }
16
+
17
+ export function normalizeImageDiffEvidence(
18
+ imageDiff: PreviewImageDiffInput,
19
+ findings: PreviewComparisonFinding[]
20
+ ): NormalizedImageDiffEvidence | undefined {
21
+ if (!isValidPercentage(imageDiff.diffPercentage)) {
22
+ findings.push({
23
+ code: "invalid-comparison-signal",
24
+ severity: "warning",
25
+ message: "Image diff percentage must be a finite number between 0 and 100.",
26
+ });
27
+ return undefined;
28
+ }
29
+
30
+ return {
31
+ diffPercentage: imageDiff.diffPercentage,
32
+ changedRegions: normalizeChangedRegions(imageDiff.changedRegions, findings),
33
+ ...(imageDiff.diffImageDataUrl && { diffImageDataUrl: imageDiff.diffImageDataUrl }),
34
+ ...(imageDiff.diffImageUrl && { diffImageUrl: imageDiff.diffImageUrl }),
35
+ ...normalizePixelCounts(imageDiff, findings),
36
+ };
37
+ }
38
+
39
+ export function isPositiveComparisonSize(value: PreviewComparisonSize): boolean {
40
+ return (
41
+ Number.isFinite(value.width) &&
42
+ Number.isFinite(value.height) &&
43
+ value.width > 0 &&
44
+ value.height > 0
45
+ );
46
+ }
47
+
48
+ function normalizeChangedRegions(
49
+ regions: PreviewComparisonBounds[] | undefined,
50
+ findings: PreviewComparisonFinding[]
51
+ ): PreviewComparisonBounds[] {
52
+ if (!regions) return [];
53
+
54
+ const validRegions: PreviewComparisonBounds[] = [];
55
+ for (const region of regions) {
56
+ if (isValidBounds(region)) {
57
+ validRegions.push(region);
58
+ continue;
59
+ }
60
+ findings.push({
61
+ code: "invalid-comparison-signal",
62
+ severity: "warning",
63
+ message: "Changed regions must use finite non-negative coordinates and positive sizes.",
64
+ });
65
+ }
66
+ return validRegions;
67
+ }
68
+
69
+ function normalizePixelCounts(
70
+ imageDiff: PreviewImageDiffInput,
71
+ findings: PreviewComparisonFinding[]
72
+ ): Pick<NormalizedImageDiffEvidence, "diffPixelCount" | "totalPixels"> {
73
+ const { diffPixelCount, totalPixels } = imageDiff;
74
+ if (diffPixelCount === undefined && totalPixels === undefined) return {};
75
+
76
+ if (
77
+ !Number.isInteger(diffPixelCount) ||
78
+ !Number.isInteger(totalPixels) ||
79
+ diffPixelCount === undefined ||
80
+ totalPixels === undefined ||
81
+ diffPixelCount < 0 ||
82
+ totalPixels <= 0 ||
83
+ diffPixelCount > totalPixels
84
+ ) {
85
+ findings.push({
86
+ code: "invalid-comparison-signal",
87
+ severity: "warning",
88
+ message:
89
+ "Image diff pixel counts must be whole numbers with changed pixels less than or equal to total pixels.",
90
+ });
91
+ return {};
92
+ }
93
+
94
+ return { diffPixelCount, totalPixels };
95
+ }
96
+
97
+ function isValidPercentage(value: number): boolean {
98
+ return Number.isFinite(value) && value >= 0 && value <= 100;
99
+ }
100
+
101
+ function isValidBounds(value: PreviewComparisonBounds): boolean {
102
+ return (
103
+ Number.isFinite(value.x) &&
104
+ Number.isFinite(value.y) &&
105
+ Number.isFinite(value.width) &&
106
+ Number.isFinite(value.height) &&
107
+ value.x >= 0 &&
108
+ value.y >= 0 &&
109
+ value.width > 0 &&
110
+ value.height > 0
111
+ );
112
+ }
@@ -0,0 +1,21 @@
1
+ import type { PreviewComparisonSize } from "./comparison";
2
+
3
+ export function formatPercent(value: number): string {
4
+ return `${round(value)}%`;
5
+ }
6
+
7
+ export function formatDelta(value: number): string {
8
+ return value > 0 ? `+${round(value)}` : `${round(value)}`;
9
+ }
10
+
11
+ export function formatSize(size: PreviewComparisonSize): string {
12
+ return `${round(size.width)} x ${round(size.height)}`;
13
+ }
14
+
15
+ export function formatInteger(value: number): string {
16
+ return Math.round(value).toLocaleString("en-US");
17
+ }
18
+
19
+ export function round(value: number): number {
20
+ return Math.round(value * 100) / 100;
21
+ }
@@ -0,0 +1,187 @@
1
+ import type {
2
+ PreviewComparisonBounds,
3
+ PreviewComparisonFinding,
4
+ PreviewComparisonContextSummary,
5
+ PreviewComparisonRegionOverlay,
6
+ PreviewComparisonReport,
7
+ PreviewComparisonSignal,
8
+ PreviewComparisonSize,
9
+ } from "./comparison";
10
+ import type { PreviewDesignComparisonState } from "./types";
11
+ import { formatDelta, formatInteger, formatPercent, formatSize, round } from "./comparison-format";
12
+
13
+ export function getPreviewComparisonSignals(
14
+ report: PreviewComparisonReport
15
+ ): PreviewComparisonSignal[] {
16
+ return [buildImageSignal(report), buildDimensionSignal(report), buildStyleSignal(report)];
17
+ }
18
+
19
+ export function getPreviewComparisonRegionOverlays(
20
+ report: PreviewComparisonReport
21
+ ): PreviewComparisonRegionOverlay[] {
22
+ const sourceSize = report.dimensions?.preview ?? report.preview.size ?? report.design.size;
23
+ if (!sourceSize || sourceSize.width <= 0 || sourceSize.height <= 0) return [];
24
+
25
+ return (report.image?.changedRegions ?? []).flatMap((region) => {
26
+ const clipped = clipRegionToSize(region, sourceSize);
27
+ if (!clipped) return [];
28
+
29
+ return {
30
+ ...region,
31
+ label: formatChangedRegionLabel(region),
32
+ leftPercent: round((clipped.x / sourceSize.width) * 100),
33
+ topPercent: round((clipped.y / sourceSize.height) * 100),
34
+ widthPercent: round((clipped.width / sourceSize.width) * 100),
35
+ heightPercent: round((clipped.height / sourceSize.height) * 100),
36
+ };
37
+ });
38
+ }
39
+
40
+ export function getPreviewComparisonContextSummary(
41
+ report: PreviewComparisonReport
42
+ ): PreviewComparisonContextSummary | null {
43
+ if (!report.context) return null;
44
+
45
+ const captured = report.context.captured
46
+ ? formatComparisonState(report.context.captured, {
47
+ includeVariant: report.context.scope === "variant",
48
+ })
49
+ : "unknown capture state";
50
+ const active = formatComparisonState(report.context.active, {
51
+ includeVariant: report.context.scope === "variant",
52
+ });
53
+
54
+ if (report.context.stale) {
55
+ return {
56
+ status: "stale",
57
+ label: "Capture context",
58
+ value: "Stale evidence",
59
+ detail: `Captured ${captured}; current ${active}.`,
60
+ };
61
+ }
62
+
63
+ return {
64
+ status: "current",
65
+ label: "Capture context",
66
+ value: "Current evidence",
67
+ detail: `Captured ${captured}.`,
68
+ };
69
+ }
70
+
71
+ export function formatPreviewComparisonFinding(finding: PreviewComparisonFinding): string {
72
+ const prefix = finding.severity.toUpperCase();
73
+ const subject = finding.property ? `${finding.property}: ` : "";
74
+ return `${prefix}: ${subject}${finding.message}`;
75
+ }
76
+
77
+ function formatComparisonState(
78
+ state: PreviewDesignComparisonState,
79
+ options: { includeVariant: boolean }
80
+ ): string {
81
+ const parts = [
82
+ options.includeVariant && state.variantId ? `variant ${state.variantId}` : undefined,
83
+ state.theme ? `theme ${state.theme}` : undefined,
84
+ state.viewport ? `viewport ${formatSize(state.viewport)}` : undefined,
85
+ ].filter((part): part is string => Boolean(part));
86
+
87
+ return parts.length > 0 ? parts.join(", ") : "unspecified preview state";
88
+ }
89
+
90
+ function buildImageSignal(report: PreviewComparisonReport): PreviewComparisonSignal {
91
+ if (!report.image) {
92
+ return {
93
+ id: "image",
94
+ label: "Image diff",
95
+ status: "missing",
96
+ value: "No capture",
97
+ detail: "Capture a Figma image and preview screenshot to compare pixels.",
98
+ };
99
+ }
100
+
101
+ const changedPixels =
102
+ report.image.diffPixelCount !== undefined && report.image.totalPixels !== undefined
103
+ ? `${formatInteger(report.image.diffPixelCount)} of ${formatInteger(
104
+ report.image.totalPixels
105
+ )} pixels changed`
106
+ : `${report.image.changedRegions.length} changed ${
107
+ report.image.changedRegions.length === 1 ? "region" : "regions"
108
+ }`;
109
+
110
+ return {
111
+ id: "image",
112
+ label: "Image diff",
113
+ status: report.image.match ? "pass" : "fail",
114
+ value: `${formatPercent(report.image.diffPercentage)} changed`,
115
+ detail: `${changedPixels}; threshold ${formatPercent(report.thresholds.imageDiffPercentage)}.`,
116
+ };
117
+ }
118
+
119
+ function buildDimensionSignal(report: PreviewComparisonReport): PreviewComparisonSignal {
120
+ if (!report.dimensions) {
121
+ return {
122
+ id: "dimensions",
123
+ label: "Size",
124
+ status: "missing",
125
+ value: "No size",
126
+ detail: "Provide both Figma frame size and captured preview size.",
127
+ };
128
+ }
129
+
130
+ return {
131
+ id: "dimensions",
132
+ label: "Size",
133
+ status: report.dimensions.match ? "pass" : "review",
134
+ value: `${formatDelta(report.dimensions.delta.width)} x ${formatDelta(
135
+ report.dimensions.delta.height
136
+ )} px`,
137
+ detail: `${formatSize(report.dimensions.design)} design, ${formatSize(
138
+ report.dimensions.preview
139
+ )} preview; tolerance +/-${report.thresholds.dimensionPx}px.`,
140
+ };
141
+ }
142
+
143
+ function buildStyleSignal(report: PreviewComparisonReport): PreviewComparisonSignal {
144
+ if (!report.styles) {
145
+ return {
146
+ id: "styles",
147
+ label: "Styles",
148
+ status: "missing",
149
+ value: "No styles",
150
+ detail: "Provide computed Figma and preview styles to compare tokens and CSS.",
151
+ };
152
+ }
153
+
154
+ const matchCount = report.styles.properties.filter((property) => property.match).length;
155
+ const firstMismatch = report.styles.properties.find((property) => !property.match);
156
+
157
+ return {
158
+ id: "styles",
159
+ label: "Styles",
160
+ status: report.styles.match ? "pass" : "review",
161
+ value: `${matchCount}/${report.styles.properties.length} matched`,
162
+ detail: firstMismatch
163
+ ? `${firstMismatch.property}: ${firstMismatch.design} in Figma, ${firstMismatch.preview} in preview.`
164
+ : "All checked computed styles are within tolerance.",
165
+ };
166
+ }
167
+
168
+ function formatChangedRegionLabel(region: PreviewComparisonBounds): string {
169
+ return `Changed region x ${region.x} y ${region.y} width ${region.width} height ${region.height}`;
170
+ }
171
+
172
+ function clipRegionToSize(
173
+ region: PreviewComparisonBounds,
174
+ size: PreviewComparisonSize
175
+ ): PreviewComparisonBounds | null {
176
+ const x = Math.max(0, region.x);
177
+ const y = Math.max(0, region.y);
178
+ const right = Math.min(size.width, region.x + region.width);
179
+ const bottom = Math.min(size.height, region.y + region.height);
180
+ if (right <= x || bottom <= y) return null;
181
+ return {
182
+ x,
183
+ y,
184
+ width: right - x,
185
+ height: bottom - y,
186
+ };
187
+ }