@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.
- package/LICENSE +21 -0
- package/dist/chunk-243QYRUF.js +2145 -0
- package/dist/chunk-243QYRUF.js.map +1 -0
- package/dist/chunk-3LLRNCPX.js +111 -0
- package/dist/chunk-3LLRNCPX.js.map +1 -0
- package/dist/chunk-EIYNNS77.js +48 -0
- package/dist/chunk-EIYNNS77.js.map +1 -0
- package/dist/chunk-JNBFJ34I.js +326 -0
- package/dist/chunk-JNBFJ34I.js.map +1 -0
- package/dist/chunk-R3X7UBHU.js +228 -0
- package/dist/chunk-R3X7UBHU.js.map +1 -0
- package/dist/chunk-SH4KPIYH.js +393 -0
- package/dist/chunk-SH4KPIYH.js.map +1 -0
- package/dist/chunk-V4VQB57N.js +56 -0
- package/dist/chunk-V4VQB57N.js.map +1 -0
- package/dist/chunk-X34IA4LR.js +319 -0
- package/dist/chunk-X34IA4LR.js.map +1 -0
- package/dist/chunk-YF65VYRY.js +493 -0
- package/dist/chunk-YF65VYRY.js.map +1 -0
- package/dist/codes/index.d.ts +40 -0
- package/dist/codes/index.js +23 -0
- package/dist/codes/index.js.map +1 -0
- package/dist/compiled-types/index.d.ts +5 -0
- package/dist/compiled-types/index.js +1 -0
- package/dist/compiled-types/index.js.map +1 -0
- package/dist/generate/index.d.ts +47 -0
- package/dist/generate/index.js +11 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/governance-BOa3KyiJ.d.ts +3279 -0
- package/dist/graph/index.d.ts +66 -0
- package/dist/graph/index.js +17 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +11388 -0
- package/dist/index.js +8711 -0
- package/dist/index.js.map +1 -0
- package/dist/package-identity.d.ts +42 -0
- package/dist/package-identity.js +23 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/preview/index.d.ts +484 -0
- package/dist/preview/index.js +1129 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/react-types.d.ts +22 -0
- package/dist/react-types.js +1 -0
- package/dist/react-types.js.map +1 -0
- package/dist/registry.d.ts +2367 -0
- package/dist/registry.js +62 -0
- package/dist/registry.js.map +1 -0
- package/dist/schemas/index.d.ts +2584 -0
- package/dist/schemas/index.js +55 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/severity.d.ts +18 -0
- package/dist/severity.js +27 -0
- package/dist/severity.js.map +1 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +63 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/topology/index.d.ts +48 -0
- package/dist/topology/index.js +7 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/types-xJ2xyp_G.d.ts +119 -0
- package/package.json +120 -0
- package/src/__tests__/contract-parser.test.ts +614 -0
- package/src/__tests__/dtcg-generators.test.ts +439 -0
- package/src/__tests__/dtcg-parser.test.ts +884 -0
- package/src/__tests__/preview-runtime.test.tsx +111 -0
- package/src/__tests__/schema.test.ts +31 -0
- package/src/__tests__/severity.test.ts +34 -0
- package/src/__tests__/style-comparison.test.ts +662 -0
- package/src/__tests__/tokens-compat.test.ts +51 -0
- package/src/agent-format.test.ts +105 -0
- package/src/agent-format.ts +141 -0
- package/src/bundle.ts +149 -0
- package/src/codes/__tests__/codes.test.ts +73 -0
- package/src/codes/codes.ts +463 -0
- package/src/codes/index.ts +7 -0
- package/src/codes/source-finding-bridge.ts +113 -0
- package/src/compiled-types/index.ts +293 -0
- package/src/component-contract.ts +231 -0
- package/src/component-discovery.ts +80 -0
- package/src/composition.test.ts +262 -0
- package/src/composition.ts +318 -0
- package/src/config-paths.test.ts +78 -0
- package/src/config-paths.ts +47 -0
- package/src/config.ts +18 -0
- package/src/conform-prove.test.ts +274 -0
- package/src/conform-prove.ts +326 -0
- package/src/conform.ts +160 -0
- package/src/constants.ts +114 -0
- package/src/context.ts +2 -0
- package/src/contract/fixture.ts +110 -0
- package/src/contract/hash.test.ts +86 -0
- package/src/contract/hash.ts +195 -0
- package/src/contract/index.ts +43 -0
- package/src/contract/preimage.test.ts +254 -0
- package/src/contract/preimage.ts +365 -0
- package/src/contract/stamp.test.ts +100 -0
- package/src/contract/stamp.ts +105 -0
- package/src/contract-parser.ts +141 -0
- package/src/defineFragment.test.ts +257 -0
- package/src/defineFragment.ts +293 -0
- package/src/dtcg-generators.ts +324 -0
- package/src/dtcg-parser.ts +1 -0
- package/src/dtcg.ts +237 -0
- package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
- package/src/facts/builders.ts +835 -0
- package/src/facts/compile.ts +422 -0
- package/src/facts/fact-index.ts +394 -0
- package/src/facts/facts.test.ts +494 -0
- package/src/facts/ids.ts +125 -0
- package/src/facts/index.ts +119 -0
- package/src/facts/types.ts +614 -0
- package/src/figma.ts +263 -0
- package/src/fragment-types.ts +214 -0
- package/src/generate/generate.test.ts +594 -0
- package/src/generate/index.ts +438 -0
- package/src/governance-integrity.test.ts +186 -0
- package/src/governance-integrity.ts +282 -0
- package/src/governance.test.ts +300 -0
- package/src/governance.ts +605 -0
- package/src/governance.type-test.ts +75 -0
- package/src/graph/engine.ts +446 -0
- package/src/graph/graph.test.ts +746 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/serialization.ts +55 -0
- package/src/graph/types.ts +178 -0
- package/src/index.ts +891 -0
- package/src/local-canonical.ts +102 -0
- package/src/mcp-snapshot.ts +222 -0
- package/src/package-identity-match.ts +33 -0
- package/src/package-identity.generated.ts +233 -0
- package/src/package-identity.test.ts +99 -0
- package/src/package-identity.ts +157 -0
- package/src/performance-presets.ts +142 -0
- package/src/preview/comparison-artifact.test.ts +228 -0
- package/src/preview/comparison-artifact.ts +181 -0
- package/src/preview/comparison-context.ts +93 -0
- package/src/preview/comparison-evidence.ts +112 -0
- package/src/preview/comparison-format.ts +21 -0
- package/src/preview/comparison-presentation.ts +187 -0
- package/src/preview/comparison-references.test.ts +430 -0
- package/src/preview/comparison-references.ts +72 -0
- package/src/preview/comparison-score.ts +59 -0
- package/src/preview/comparison-styles.ts +197 -0
- package/src/preview/comparison.test.ts +559 -0
- package/src/preview/comparison.ts +488 -0
- package/src/preview/index.ts +10 -0
- package/src/preview/types.ts +309 -0
- package/src/preview/validation.test.ts +290 -0
- package/src/preview/validation.ts +356 -0
- package/src/preview-runtime.tsx +144 -0
- package/src/raw-html-canonical.test.ts +41 -0
- package/src/raw-html-canonical.ts +179 -0
- package/src/react-types.ts +21 -0
- package/src/registry-install-plan.ts +344 -0
- package/src/registry.test.ts +245 -0
- package/src/registry.ts +369 -0
- package/src/rules/a11y-required-accessible-name.ts +93 -0
- package/src/rules/color-math.test.ts +72 -0
- package/src/rules/color-math.ts +192 -0
- package/src/rules/components-forbidden-prop-value.ts +108 -0
- package/src/rules/components-prefer-library.ts +943 -0
- package/src/rules/components-unknown-prop.ts +94 -0
- package/src/rules/composition-pattern-schema.ts +125 -0
- package/src/rules/composition-pattern.test.ts +320 -0
- package/src/rules/composition-pattern.ts +271 -0
- package/src/rules/emit-gate.test.ts +42 -0
- package/src/rules/emit-gate.ts +60 -0
- package/src/rules/finding.ts +54 -0
- package/src/rules/index.ts +235 -0
- package/src/rules/jsx-preferred-component.ts +68 -0
- package/src/rules/jsx-preferred-import-path.ts +67 -0
- package/src/rules/presets.ts +57 -0
- package/src/rules/props-invalid-value.ts +115 -0
- package/src/rules/rules.test.ts +3636 -0
- package/src/rules/spacing-resolution.ts +184 -0
- package/src/rules/styles-no-raw-color.ts +329 -0
- package/src/rules/styles-no-raw-dimensions.ts +357 -0
- package/src/rules/styles-no-raw-spacing.ts +236 -0
- package/src/rules/styles-no-raw-typography.ts +237 -0
- package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
- package/src/rules/tailwind-arbitrary-color.ts +93 -0
- package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
- package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
- package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
- package/src/rules/tailwind-forbidden-palette.ts +107 -0
- package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
- package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.ts +82 -0
- package/src/rules/tailwind-rule-utils.ts +106 -0
- package/src/rules/tailwind-unknown-class.test.ts +157 -0
- package/src/rules/tailwind-unknown-class.ts +121 -0
- package/src/rules/taxonomy.test.ts +238 -0
- package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
- package/src/rules/tiers.ts +111 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
- package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
- package/src/rules/tokens-require-dual-fallback.ts +78 -0
- package/src/rules/types.ts +27 -0
- package/src/rules/utils.test.ts +110 -0
- package/src/rules/utils.ts +599 -0
- package/src/schema.ts +427 -0
- package/src/schemas/index.ts +275 -0
- package/src/schemas/schemas.test.ts +124 -0
- package/src/severity.ts +54 -0
- package/src/storyAdapter.test.ts +571 -0
- package/src/storyAdapter.ts +761 -0
- package/src/storyFilters.test.ts +350 -0
- package/src/storyFilters.ts +253 -0
- package/src/storybook-csf.ts +11 -0
- package/src/style-comparison.ts +423 -0
- package/src/test-utils.ts +68 -0
- package/src/token-parser.ts +2 -0
- package/src/token-types.ts +287 -0
- package/src/tokens/categories.ts +131 -0
- package/src/tokens/color.ts +193 -0
- package/src/tokens/design-token-parser.test.ts +159 -0
- package/src/tokens/design-token-parser.ts +402 -0
- package/src/tokens/dtcg.ts +428 -0
- package/src/tokens/index.ts +130 -0
- package/src/tokens/lookup.ts +19 -0
- package/src/tokens/scss.ts +171 -0
- package/src/tokens/tailwind-v4.ts +187 -0
- package/src/tokens/types.ts +43 -0
- package/src/topology/index.ts +9 -0
- package/src/topology/resolve-area.test.ts +174 -0
- package/src/topology/resolve-area.ts +215 -0
- 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
|
+
}
|