@fragments-sdk/viewer 0.2.1
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 +84 -0
- package/index.html +28 -0
- package/package.json +71 -0
- package/src/__tests__/a11y-fixes.test.ts +358 -0
- package/src/__tests__/jsx-parser.test.ts +502 -0
- package/src/__tests__/render-utils.test.ts +232 -0
- package/src/__tests__/style-utils.test.ts +404 -0
- package/src/app/index.ts +1 -0
- package/src/assets/fragments-logo.ts +4 -0
- package/src/assets/fragments_logo.png +0 -0
- package/src/components/AccessibilityPanel.tsx +1457 -0
- package/src/components/ActionCapture.tsx +172 -0
- package/src/components/ActionsPanel.tsx +332 -0
- package/src/components/AllVariantsPreview.tsx +78 -0
- package/src/components/App.tsx +604 -0
- package/src/components/BottomPanel.tsx +288 -0
- package/src/components/CodePanel.naming.test.tsx +59 -0
- package/src/components/CodePanel.tsx +118 -0
- package/src/components/CommandPalette.tsx +392 -0
- package/src/components/ComponentDocView.tsx +164 -0
- package/src/components/ComponentGraph.tsx +380 -0
- package/src/components/ComponentHeader.tsx +88 -0
- package/src/components/ContractPanel.tsx +241 -0
- package/src/components/DeviceMockup.tsx +156 -0
- package/src/components/EmptyVariantMessage.tsx +54 -0
- package/src/components/ErrorBoundary.tsx +97 -0
- package/src/components/FigmaEmbed.tsx +238 -0
- package/src/components/FragmentEditor.tsx +525 -0
- package/src/components/FragmentRenderer.tsx +61 -0
- package/src/components/HeaderSearch.tsx +24 -0
- package/src/components/HealthDashboard.tsx +441 -0
- package/src/components/HmrStatusIndicator.tsx +61 -0
- package/src/components/Icons.tsx +479 -0
- package/src/components/InteractionsPanel.tsx +757 -0
- package/src/components/IsolatedPreviewFrame.tsx +390 -0
- package/src/components/IsolatedRender.tsx +113 -0
- package/src/components/KeyboardShortcutsHelp.tsx +53 -0
- package/src/components/LandingPage.tsx +420 -0
- package/src/components/Layout.tsx +27 -0
- package/src/components/LeftSidebar.tsx +472 -0
- package/src/components/LoadErrorMessage.tsx +102 -0
- package/src/components/MultiViewportPreview.tsx +527 -0
- package/src/components/NoVariantsMessage.tsx +59 -0
- package/src/components/PanelShell.tsx +161 -0
- package/src/components/PerformancePanel.tsx +304 -0
- package/src/components/PreviewArea.tsx +254 -0
- package/src/components/PreviewAside.tsx +168 -0
- package/src/components/PreviewFrameHost.tsx +304 -0
- package/src/components/PreviewToolbar.tsx +80 -0
- package/src/components/PropsEditor.tsx +506 -0
- package/src/components/PropsTable.tsx +111 -0
- package/src/components/RelationsSection.tsx +88 -0
- package/src/components/ResizablePanel.tsx +271 -0
- package/src/components/RightSidebar.tsx +102 -0
- package/src/components/RuntimeToolsRegistrar.tsx +17 -0
- package/src/components/ScreenshotButton.tsx +90 -0
- package/src/components/ShadowPreview.tsx +204 -0
- package/src/components/Sidebar.tsx +169 -0
- package/src/components/SkeletonLoader.tsx +161 -0
- package/src/components/ThemeProvider.tsx +42 -0
- package/src/components/Toast.tsx +3 -0
- package/src/components/TokenStylePanel.tsx +699 -0
- package/src/components/TopToolbar.tsx +159 -0
- package/src/components/Untitled +1 -0
- package/src/components/UsageSection.tsx +95 -0
- package/src/components/VariantMatrix.tsx +391 -0
- package/src/components/VariantRenderer.tsx +131 -0
- package/src/components/VariantTabs.tsx +40 -0
- package/src/components/ViewerHeader.tsx +69 -0
- package/src/components/ViewerStateSync.tsx +52 -0
- package/src/components/ViewportSelector.tsx +172 -0
- package/src/components/WebMCPDevTools.tsx +503 -0
- package/src/components/WebMCPIntegration.tsx +47 -0
- package/src/components/WebMCPStatusIndicator.tsx +60 -0
- package/src/components/_future/CreatePage.tsx +835 -0
- package/src/components/viewer-utils.ts +16 -0
- package/src/composition-renderer.ts +381 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/ui.ts +166 -0
- package/src/entry.tsx +335 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useA11yCache.ts +383 -0
- package/src/hooks/useA11yService.ts +364 -0
- package/src/hooks/useActions.ts +138 -0
- package/src/hooks/useAppState.ts +147 -0
- package/src/hooks/useCompiledFragments.ts +42 -0
- package/src/hooks/useFigmaIntegration.ts +132 -0
- package/src/hooks/useHmrStatus.ts +109 -0
- package/src/hooks/useKeyboardShortcuts.ts +270 -0
- package/src/hooks/usePreviewBridge.ts +347 -0
- package/src/hooks/useScrollSpy.ts +78 -0
- package/src/hooks/useShadowStyles.ts +221 -0
- package/src/hooks/useUrlState.ts +318 -0
- package/src/hooks/useViewSettings.ts +111 -0
- package/src/intelligence/healthReport.ts +505 -0
- package/src/intelligence/styleDrift.ts +340 -0
- package/src/intelligence/usageScanner.ts +309 -0
- package/src/jsx-parser.ts +486 -0
- package/src/preview-frame-entry.tsx +25 -0
- package/src/preview-frame.html +148 -0
- package/src/render-template.html +68 -0
- package/src/render-utils.ts +311 -0
- package/src/shared/ComponentDocContent.module.scss +10 -0
- package/src/shared/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/shared/ComponentDocContent.tsx +274 -0
- package/src/shared/DocsHeaderBar.tsx +129 -0
- package/src/shared/DocsPageAsideHost.tsx +89 -0
- package/src/shared/DocsPageShell.tsx +124 -0
- package/src/shared/DocsSearchCommand.tsx +99 -0
- package/src/shared/DocsSidebarNav.tsx +66 -0
- package/src/shared/PropsTable.module.scss +68 -0
- package/src/shared/PropsTable.module.scss.d.ts +2 -0
- package/src/shared/PropsTable.tsx +76 -0
- package/src/shared/VariantPreviewCard.module.scss +114 -0
- package/src/shared/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/shared/VariantPreviewCard.tsx +137 -0
- package/src/shared/docs-data/index.ts +32 -0
- package/src/shared/docs-data/mcp-configs.ts +72 -0
- package/src/shared/docs-data/palettes.ts +75 -0
- package/src/shared/docs-data/setup-examples.ts +55 -0
- package/src/shared/docs-layout.scss +28 -0
- package/src/shared/docs-layout.scss.d.ts +2 -0
- package/src/shared/index.ts +34 -0
- package/src/shared/types.ts +53 -0
- package/src/style-utils.ts +414 -0
- package/src/styles/globals.css +278 -0
- package/src/types/a11y.ts +197 -0
- package/src/utils/a11y-fixes.ts +509 -0
- package/src/utils/actionExport.ts +372 -0
- package/src/utils/colorSchemes.ts +201 -0
- package/src/utils/contrast.ts +246 -0
- package/src/utils/detectRelationships.ts +256 -0
- package/src/webmcp/__tests__/analytics.test.ts +108 -0
- package/src/webmcp/analytics.ts +165 -0
- package/src/webmcp/index.ts +3 -0
- package/src/webmcp/posthog-bridge.ts +39 -0
- package/src/webmcp/runtime-tools.ts +152 -0
- package/src/webmcp/scan-utils.ts +135 -0
- package/src/webmcp/use-tool-analytics.ts +69 -0
- package/src/webmcp/viewer-state.ts +45 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
serializeValue,
|
|
4
|
+
serializePropsToJsx,
|
|
5
|
+
findFragmentByName,
|
|
6
|
+
getAvailableComponents,
|
|
7
|
+
generateRenderScript,
|
|
8
|
+
} from "../render-utils.js";
|
|
9
|
+
|
|
10
|
+
describe("serializeValue", () => {
|
|
11
|
+
it("serializes null", () => {
|
|
12
|
+
expect(serializeValue(null)).toBe("null");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("serializes undefined", () => {
|
|
16
|
+
expect(serializeValue(undefined)).toBe("undefined");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("serializes strings with proper escaping", () => {
|
|
20
|
+
expect(serializeValue("hello")).toBe('"hello"');
|
|
21
|
+
expect(serializeValue("hello world")).toBe('"hello world"');
|
|
22
|
+
expect(serializeValue('with "quotes"')).toBe('"with \\"quotes\\""');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("serializes numbers", () => {
|
|
26
|
+
expect(serializeValue(42)).toBe("42");
|
|
27
|
+
expect(serializeValue(3.14)).toBe("3.14");
|
|
28
|
+
expect(serializeValue(-10)).toBe("-10");
|
|
29
|
+
expect(serializeValue(0)).toBe("0");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("serializes booleans", () => {
|
|
33
|
+
expect(serializeValue(true)).toBe("true");
|
|
34
|
+
expect(serializeValue(false)).toBe("false");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("serializes arrays", () => {
|
|
38
|
+
expect(serializeValue([])).toBe("[]");
|
|
39
|
+
expect(serializeValue([1, 2, 3])).toBe("[1, 2, 3]");
|
|
40
|
+
expect(serializeValue(["a", "b"])).toBe('["a", "b"]');
|
|
41
|
+
expect(serializeValue([true, null, "x"])).toBe('[true, null, "x"]');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("serializes objects", () => {
|
|
45
|
+
expect(serializeValue({})).toBe("{}");
|
|
46
|
+
expect(serializeValue({ a: 1 })).toBe('{"a": 1}');
|
|
47
|
+
expect(serializeValue({ name: "test", value: 42 })).toBe(
|
|
48
|
+
'{"name": "test", "value": 42}'
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("serializes nested structures", () => {
|
|
53
|
+
expect(serializeValue({ items: [1, 2], nested: { deep: true } })).toBe(
|
|
54
|
+
'{"items": [1, 2], "nested": {"deep": true}}'
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns undefined for functions", () => {
|
|
59
|
+
expect(serializeValue(() => {})).toBe("undefined");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("serializePropsToJsx", () => {
|
|
64
|
+
it("serializes empty props", () => {
|
|
65
|
+
expect(serializePropsToJsx({})).toBe("");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("serializes string props as JSX attributes", () => {
|
|
69
|
+
expect(serializePropsToJsx({ variant: "primary" })).toBe(
|
|
70
|
+
'variant="primary"'
|
|
71
|
+
);
|
|
72
|
+
expect(serializePropsToJsx({ label: "Click me" })).toBe('label="Click me"');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("serializes boolean props with JSX expression syntax", () => {
|
|
76
|
+
expect(serializePropsToJsx({ disabled: true })).toBe("disabled={true}");
|
|
77
|
+
expect(serializePropsToJsx({ checked: false })).toBe("checked={false}");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("serializes number props with JSX expression syntax", () => {
|
|
81
|
+
expect(serializePropsToJsx({ count: 5 })).toBe("count={5}");
|
|
82
|
+
expect(serializePropsToJsx({ size: 24.5 })).toBe("size={24.5}");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("serializes multiple props", () => {
|
|
86
|
+
const result = serializePropsToJsx({
|
|
87
|
+
variant: "danger",
|
|
88
|
+
disabled: false,
|
|
89
|
+
size: 16,
|
|
90
|
+
});
|
|
91
|
+
expect(result).toBe('variant="danger" disabled={false} size={16}');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("filters out undefined props", () => {
|
|
95
|
+
expect(
|
|
96
|
+
serializePropsToJsx({
|
|
97
|
+
variant: "primary",
|
|
98
|
+
disabled: undefined,
|
|
99
|
+
label: "test",
|
|
100
|
+
})
|
|
101
|
+
).toBe('variant="primary" label="test"');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("serializes object props", () => {
|
|
105
|
+
expect(serializePropsToJsx({ style: { color: "red" } })).toBe(
|
|
106
|
+
'style={{"color": "red"}}'
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("serializes array props", () => {
|
|
111
|
+
expect(serializePropsToJsx({ items: [1, 2, 3] })).toBe("items={[1, 2, 3]}");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("findFragmentByName", () => {
|
|
116
|
+
const mockFragments = [
|
|
117
|
+
{ path: "Button.fragment.tsx", fragment: { meta: { name: "Button" } } },
|
|
118
|
+
{ path: "Card.fragment.tsx", fragment: { meta: { name: "Card" } } },
|
|
119
|
+
{ path: "Alert.fragment.tsx", fragment: { meta: { name: "Alert" } } },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
it("finds fragment by exact name", () => {
|
|
123
|
+
const result = findFragmentByName("Button", mockFragments);
|
|
124
|
+
expect(result).toEqual({ name: "Button", path: "Button.fragment.tsx" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("finds fragment case-insensitively", () => {
|
|
128
|
+
expect(findFragmentByName("button", mockFragments)).toEqual({
|
|
129
|
+
name: "Button",
|
|
130
|
+
path: "Button.fragment.tsx",
|
|
131
|
+
});
|
|
132
|
+
expect(findFragmentByName("BUTTON", mockFragments)).toEqual({
|
|
133
|
+
name: "Button",
|
|
134
|
+
path: "Button.fragment.tsx",
|
|
135
|
+
});
|
|
136
|
+
expect(findFragmentByName("BuTtOn", mockFragments)).toEqual({
|
|
137
|
+
name: "Button",
|
|
138
|
+
path: "Button.fragment.tsx",
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns null for non-existent component", () => {
|
|
143
|
+
expect(findFragmentByName("NonExistent", mockFragments)).toBeNull();
|
|
144
|
+
expect(findFragmentByName("", mockFragments)).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("handles empty fragments array", () => {
|
|
148
|
+
expect(findFragmentByName("Button", [])).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("getAvailableComponents", () => {
|
|
153
|
+
it("returns sorted list of component names", () => {
|
|
154
|
+
const fragments = [
|
|
155
|
+
{ fragment: { meta: { name: "Zebra" } } },
|
|
156
|
+
{ fragment: { meta: { name: "Alert" } } },
|
|
157
|
+
{ fragment: { meta: { name: "Button" } } },
|
|
158
|
+
];
|
|
159
|
+
expect(getAvailableComponents(fragments)).toEqual([
|
|
160
|
+
"Alert",
|
|
161
|
+
"Button",
|
|
162
|
+
"Zebra",
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns empty array for empty fragments", () => {
|
|
167
|
+
expect(getAvailableComponents([])).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("generateRenderScript", () => {
|
|
172
|
+
it("generates script with correct import path", () => {
|
|
173
|
+
const script = generateRenderScript("/path/to/Button.fragment.tsx", "Button", {});
|
|
174
|
+
expect(script).toContain('import("/path/to/Button.fragment.tsx")');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("generates script with empty props object when no props", () => {
|
|
178
|
+
const script = generateRenderScript("/path/to/Button.tsx", "Button", {});
|
|
179
|
+
expect(script).toContain("React.createElement(Component, {})");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("generates script with props object", () => {
|
|
183
|
+
const script = generateRenderScript("/path/to/Button.tsx", "Button", {
|
|
184
|
+
variant: "danger",
|
|
185
|
+
disabled: true,
|
|
186
|
+
});
|
|
187
|
+
expect(script).toContain(
|
|
188
|
+
'React.createElement(Component, {"variant":"danger","disabled":true})'
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("handles children prop specially", () => {
|
|
193
|
+
const script = generateRenderScript("/path/to/Button.tsx", "Button", {
|
|
194
|
+
variant: "primary",
|
|
195
|
+
children: "Click me",
|
|
196
|
+
});
|
|
197
|
+
// Children should be passed as third argument, not in props object
|
|
198
|
+
expect(script).toContain(
|
|
199
|
+
'React.createElement(Component, {"variant":"primary"}, "Click me")'
|
|
200
|
+
);
|
|
201
|
+
// Children should NOT be in the props object
|
|
202
|
+
expect(script).not.toContain('"children":"Click me"');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("handles children with other props", () => {
|
|
206
|
+
const script = generateRenderScript("/path/to/Alert.tsx", "Alert", {
|
|
207
|
+
severity: "warning",
|
|
208
|
+
children: "Warning message",
|
|
209
|
+
dismissible: true,
|
|
210
|
+
});
|
|
211
|
+
expect(script).toContain(
|
|
212
|
+
'React.createElement(Component, {"severity":"warning","dismissible":true}, "Warning message")'
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("includes React and ReactDOM imports", () => {
|
|
217
|
+
const script = generateRenderScript("/path/to/Button.tsx", "Button", {});
|
|
218
|
+
expect(script).toContain('import React from "react"');
|
|
219
|
+
expect(script).toContain('import { createRoot } from "react-dom/client"');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("includes render ready signal", () => {
|
|
223
|
+
const script = generateRenderScript("/path/to/Button.tsx", "Button", {});
|
|
224
|
+
expect(script).toContain("window.__RENDER_READY__ = true");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("includes error handling", () => {
|
|
228
|
+
const script = generateRenderScript("/path/to/Button.tsx", "Button", {});
|
|
229
|
+
expect(script).toContain("window.__RENDER_ERROR__");
|
|
230
|
+
expect(script).toContain('class="render-error"');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
compareStyles,
|
|
4
|
+
compareStyleValue,
|
|
5
|
+
normalizeStyleValue,
|
|
6
|
+
compareColors,
|
|
7
|
+
parseColor,
|
|
8
|
+
compareNumericValues,
|
|
9
|
+
} from "../style-utils.js";
|
|
10
|
+
|
|
11
|
+
describe("parseColor", () => {
|
|
12
|
+
describe("hex colors", () => {
|
|
13
|
+
it("parses 6-digit hex color", () => {
|
|
14
|
+
expect(parseColor("#ff0000")).toEqual({ r: 255, g: 0, b: 0 });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("parses lowercase hex color", () => {
|
|
18
|
+
expect(parseColor("#00ff00")).toEqual({ r: 0, g: 255, b: 0 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("parses mixed case hex color", () => {
|
|
22
|
+
expect(parseColor("#0000FF")).toEqual({ r: 0, g: 0, b: 255 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("parses hex color with values 0-255", () => {
|
|
26
|
+
expect(parseColor("#336699")).toEqual({ r: 51, g: 102, b: 153 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns null for 3-digit hex color", () => {
|
|
30
|
+
expect(parseColor("#f00")).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("rgb/rgba colors", () => {
|
|
35
|
+
it("parses rgb color", () => {
|
|
36
|
+
expect(parseColor("rgb(255, 0, 0)")).toEqual({ r: 255, g: 0, b: 0, a: 1 });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("parses rgba color with alpha", () => {
|
|
40
|
+
expect(parseColor("rgba(255, 0, 0, 0.5)")).toEqual({
|
|
41
|
+
r: 255,
|
|
42
|
+
g: 0,
|
|
43
|
+
b: 0,
|
|
44
|
+
a: 0.5,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("parses rgba color with alpha 0", () => {
|
|
49
|
+
expect(parseColor("rgba(0, 0, 0, 0)")).toEqual({
|
|
50
|
+
r: 0,
|
|
51
|
+
g: 0,
|
|
52
|
+
b: 0,
|
|
53
|
+
a: 0,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("parses rgba color with alpha 1", () => {
|
|
58
|
+
expect(parseColor("rgba(100, 150, 200, 1)")).toEqual({
|
|
59
|
+
r: 100,
|
|
60
|
+
g: 150,
|
|
61
|
+
b: 200,
|
|
62
|
+
a: 1,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("handles extra whitespace", () => {
|
|
67
|
+
expect(parseColor("rgb( 255 , 128 , 64 )")).toEqual({
|
|
68
|
+
r: 255,
|
|
69
|
+
g: 128,
|
|
70
|
+
b: 64,
|
|
71
|
+
a: 1,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("invalid colors", () => {
|
|
77
|
+
it("returns null for color keywords", () => {
|
|
78
|
+
expect(parseColor("red")).toBeNull();
|
|
79
|
+
expect(parseColor("transparent")).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns null for hsl colors", () => {
|
|
83
|
+
expect(parseColor("hsl(0, 100%, 50%)")).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns null for empty string", () => {
|
|
87
|
+
expect(parseColor("")).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("compareColors", () => {
|
|
93
|
+
it("matches identical hex colors", () => {
|
|
94
|
+
expect(compareColors("#ff0000", "#ff0000", 0)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("matches colors within tolerance", () => {
|
|
98
|
+
expect(compareColors("#ff0000", "#fe0000", 5)).toBe(true);
|
|
99
|
+
expect(compareColors("rgb(255, 0, 0)", "rgb(252, 0, 0)", 5)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("fails colors outside tolerance", () => {
|
|
103
|
+
expect(compareColors("#ff0000", "#f00000", 5)).toBe(false);
|
|
104
|
+
expect(compareColors("rgb(255, 0, 0)", "rgb(240, 0, 0)", 5)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("matches hex and rgba representations of same color", () => {
|
|
108
|
+
expect(compareColors("#ff0000", "rgba(255, 0, 0, 1)", 0)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("matches colors with similar alpha", () => {
|
|
112
|
+
expect(compareColors("rgba(255, 0, 0, 0.5)", "rgba(255, 0, 0, 0.52)", 5)).toBe(
|
|
113
|
+
true
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("fails colors with different alpha", () => {
|
|
118
|
+
expect(compareColors("rgba(255, 0, 0, 0.5)", "rgba(255, 0, 0, 0.7)", 5)).toBe(
|
|
119
|
+
false
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("falls back to string comparison for unparseable colors", () => {
|
|
124
|
+
expect(compareColors("red", "red", 0)).toBe(true);
|
|
125
|
+
expect(compareColors("red", "blue", 0)).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("compareNumericValues", () => {
|
|
130
|
+
it("matches identical values", () => {
|
|
131
|
+
expect(compareNumericValues("10px", "10px", 0)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("matches values within tolerance", () => {
|
|
135
|
+
expect(compareNumericValues("10px", "11px", 1)).toBe(true);
|
|
136
|
+
expect(compareNumericValues("10px", "9px", 1)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("fails values outside tolerance", () => {
|
|
140
|
+
expect(compareNumericValues("10px", "12px", 1)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("handles values without units", () => {
|
|
144
|
+
expect(compareNumericValues("10", "11", 1)).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("handles decimal values", () => {
|
|
148
|
+
expect(compareNumericValues("10.5px", "11px", 1)).toBe(true);
|
|
149
|
+
expect(compareNumericValues("10.5px", "12px", 1)).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("falls back to string comparison for non-numeric values", () => {
|
|
153
|
+
expect(compareNumericValues("auto", "auto", 0)).toBe(true);
|
|
154
|
+
expect(compareNumericValues("auto", "none", 0)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("normalizeStyleValue", () => {
|
|
159
|
+
it("trims whitespace", () => {
|
|
160
|
+
expect(normalizeStyleValue("any", " 10px ")).toBe("10px");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("collapses multiple spaces", () => {
|
|
164
|
+
expect(normalizeStyleValue("any", "10px 20px")).toBe("10px 20px");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('normalizes "none" boxShadow to empty string', () => {
|
|
168
|
+
expect(normalizeStyleValue("boxShadow", "none")).toBe("");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('does not normalize "none" for other properties', () => {
|
|
172
|
+
expect(normalizeStyleValue("display", "none")).toBe("none");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("normalizes transparent color to 'transparent'", () => {
|
|
176
|
+
expect(normalizeStyleValue("backgroundColor", "rgba(0, 0, 0, 0)")).toBe(
|
|
177
|
+
"transparent"
|
|
178
|
+
);
|
|
179
|
+
expect(normalizeStyleValue("borderColor", "rgba( 0 , 0 , 0 , 0 )")).toBe(
|
|
180
|
+
"transparent"
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("compareStyleValue", () => {
|
|
186
|
+
describe("exact match", () => {
|
|
187
|
+
it("matches identical values", () => {
|
|
188
|
+
expect(compareStyleValue("fontFamily", "Inter", "Inter")).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("matches after normalization", () => {
|
|
192
|
+
expect(compareStyleValue("fontFamily", " Inter ", "Inter")).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("color properties", () => {
|
|
197
|
+
it("uses color comparison for backgroundColor", () => {
|
|
198
|
+
expect(
|
|
199
|
+
compareStyleValue("backgroundColor", "#ff0000", "rgb(255, 0, 0)")
|
|
200
|
+
).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("uses color comparison for borderColor", () => {
|
|
204
|
+
expect(
|
|
205
|
+
compareStyleValue("borderColor", "#00ff00", "rgb(0, 255, 0)")
|
|
206
|
+
).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("allows tolerance in color comparison", () => {
|
|
210
|
+
expect(
|
|
211
|
+
compareStyleValue("backgroundColor", "#ff0000", "rgb(252, 0, 0)")
|
|
212
|
+
).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("numeric properties", () => {
|
|
217
|
+
it("uses numeric comparison for borderWidth", () => {
|
|
218
|
+
expect(compareStyleValue("borderWidth", "1px", "2px")).toBe(true);
|
|
219
|
+
expect(compareStyleValue("borderWidth", "1px", "3px")).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("uses numeric comparison for borderRadius", () => {
|
|
223
|
+
expect(compareStyleValue("borderRadius", "8px", "9px")).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("uses numeric comparison for fontSize", () => {
|
|
227
|
+
expect(compareStyleValue("fontSize", "16px", "17px")).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("uses numeric comparison for padding", () => {
|
|
231
|
+
expect(compareStyleValue("padding", "10px", "11px")).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("uses numeric comparison for gap", () => {
|
|
235
|
+
expect(compareStyleValue("gap", "16px", "17px")).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("other properties", () => {
|
|
240
|
+
it("uses strict comparison for fontFamily", () => {
|
|
241
|
+
expect(compareStyleValue("fontFamily", "Inter", "Arial")).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("uses strict comparison for fontWeight", () => {
|
|
245
|
+
expect(compareStyleValue("fontWeight", "400", "500")).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("uses strict comparison for textAlign", () => {
|
|
249
|
+
expect(compareStyleValue("textAlign", "left", "center")).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("compareStyles", () => {
|
|
255
|
+
it("returns match true when all styles match", () => {
|
|
256
|
+
const figmaStyles = {
|
|
257
|
+
backgroundColor: "#ff0000",
|
|
258
|
+
fontSize: "16px",
|
|
259
|
+
};
|
|
260
|
+
const renderedStyles = {
|
|
261
|
+
backgroundColor: "rgb(255, 0, 0)",
|
|
262
|
+
fontSize: "16px",
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const result = compareStyles(figmaStyles, renderedStyles);
|
|
266
|
+
expect(result.match).toBe(true);
|
|
267
|
+
expect(result.properties).toHaveLength(2);
|
|
268
|
+
expect(result.properties.every((p) => p.match)).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("returns match false when some styles differ", () => {
|
|
272
|
+
const figmaStyles = {
|
|
273
|
+
backgroundColor: "#ff0000",
|
|
274
|
+
fontSize: "16px",
|
|
275
|
+
};
|
|
276
|
+
const renderedStyles = {
|
|
277
|
+
backgroundColor: "rgb(0, 255, 0)",
|
|
278
|
+
fontSize: "16px",
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const result = compareStyles(figmaStyles, renderedStyles);
|
|
282
|
+
expect(result.match).toBe(false);
|
|
283
|
+
expect(result.properties.find((p) => p.property === "backgroundColor")?.match).toBe(
|
|
284
|
+
false
|
|
285
|
+
);
|
|
286
|
+
expect(result.properties.find((p) => p.property === "fontSize")?.match).toBe(
|
|
287
|
+
true
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("only compares properties present in figmaStyles", () => {
|
|
292
|
+
const figmaStyles = {
|
|
293
|
+
backgroundColor: "#ff0000",
|
|
294
|
+
};
|
|
295
|
+
const renderedStyles = {
|
|
296
|
+
backgroundColor: "rgb(255, 0, 0)",
|
|
297
|
+
fontSize: "16px",
|
|
298
|
+
fontFamily: "Inter",
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const result = compareStyles(figmaStyles, renderedStyles);
|
|
302
|
+
expect(result.properties).toHaveLength(1);
|
|
303
|
+
expect(result.properties[0].property).toBe("backgroundColor");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("marks missing rendered styles as not matching", () => {
|
|
307
|
+
const figmaStyles = {
|
|
308
|
+
backgroundColor: "#ff0000",
|
|
309
|
+
borderRadius: "8px",
|
|
310
|
+
};
|
|
311
|
+
const renderedStyles = {
|
|
312
|
+
backgroundColor: "rgb(255, 0, 0)",
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const result = compareStyles(figmaStyles, renderedStyles);
|
|
316
|
+
const borderRadius = result.properties.find(
|
|
317
|
+
(p) => p.property === "borderRadius"
|
|
318
|
+
);
|
|
319
|
+
expect(borderRadius?.rendered).toBe("(not set)");
|
|
320
|
+
expect(borderRadius?.match).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("returns cleaned figmaStyles object", () => {
|
|
324
|
+
const figmaStyles = {
|
|
325
|
+
backgroundColor: "#ff0000",
|
|
326
|
+
fontSize: undefined,
|
|
327
|
+
fontFamily: "Inter",
|
|
328
|
+
};
|
|
329
|
+
const renderedStyles = {
|
|
330
|
+
backgroundColor: "rgb(255, 0, 0)",
|
|
331
|
+
fontFamily: "Inter",
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const result = compareStyles(figmaStyles, renderedStyles);
|
|
335
|
+
expect(result.figmaStyles).toEqual({
|
|
336
|
+
backgroundColor: "#ff0000",
|
|
337
|
+
fontFamily: "Inter",
|
|
338
|
+
});
|
|
339
|
+
expect(result.figmaStyles.fontSize).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("preserves renderedStyles in output", () => {
|
|
343
|
+
const figmaStyles = {
|
|
344
|
+
backgroundColor: "#ff0000",
|
|
345
|
+
};
|
|
346
|
+
const renderedStyles = {
|
|
347
|
+
backgroundColor: "rgb(255, 0, 0)",
|
|
348
|
+
fontSize: "16px",
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const result = compareStyles(figmaStyles, renderedStyles);
|
|
352
|
+
expect(result.renderedStyles).toBe(renderedStyles);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("handles empty figmaStyles", () => {
|
|
356
|
+
const figmaStyles = {};
|
|
357
|
+
const renderedStyles = {
|
|
358
|
+
backgroundColor: "rgb(255, 0, 0)",
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const result = compareStyles(figmaStyles, renderedStyles);
|
|
362
|
+
expect(result.match).toBe(true);
|
|
363
|
+
expect(result.properties).toHaveLength(0);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("compares all standard properties when present", () => {
|
|
367
|
+
const figmaStyles = {
|
|
368
|
+
backgroundColor: "#ff0000",
|
|
369
|
+
borderColor: "#000000",
|
|
370
|
+
borderWidth: "1px",
|
|
371
|
+
borderRadius: "4px",
|
|
372
|
+
fontFamily: "Inter",
|
|
373
|
+
fontSize: "16px",
|
|
374
|
+
fontWeight: "400",
|
|
375
|
+
lineHeight: "24px",
|
|
376
|
+
letterSpacing: "-0.5px",
|
|
377
|
+
textAlign: "left",
|
|
378
|
+
boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.25)",
|
|
379
|
+
padding: "16px",
|
|
380
|
+
gap: "8px",
|
|
381
|
+
opacity: "0.9",
|
|
382
|
+
};
|
|
383
|
+
const renderedStyles = {
|
|
384
|
+
backgroundColor: "rgb(255, 0, 0)",
|
|
385
|
+
borderColor: "rgb(0, 0, 0)",
|
|
386
|
+
borderWidth: "1px",
|
|
387
|
+
borderRadius: "4px",
|
|
388
|
+
fontFamily: "Inter",
|
|
389
|
+
fontSize: "16px",
|
|
390
|
+
fontWeight: "400",
|
|
391
|
+
lineHeight: "24px",
|
|
392
|
+
letterSpacing: "-0.5px",
|
|
393
|
+
textAlign: "left",
|
|
394
|
+
boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.25)",
|
|
395
|
+
padding: "16px",
|
|
396
|
+
gap: "8px",
|
|
397
|
+
opacity: "0.9",
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const result = compareStyles(figmaStyles, renderedStyles);
|
|
401
|
+
expect(result.match).toBe(true);
|
|
402
|
+
expect(result.properties).toHaveLength(14);
|
|
403
|
+
});
|
|
404
|
+
});
|
package/src/app/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { App as ViewerApp } from '../components/App.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Inline base64 data URL for the Fragments logo PNG.
|
|
2
|
+
// Vite cannot resolve static asset imports from node_modules,
|
|
3
|
+
// so we embed the image directly to avoid resolution errors.
|
|
4
|
+
export const fragmentsLogo = "data:image/png;base64,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";
|
|
Binary file
|