@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,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
|