meno-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/bin/cli.ts +281 -0
- package/build-static.ts +298 -0
- package/bunfig.toml +39 -0
- package/entries/client-router.tsx +111 -0
- package/entries/server-router.tsx +71 -0
- package/lib/client/ClientInitializer.test.ts +9 -0
- package/lib/client/ClientInitializer.test.ts.skip +92 -0
- package/lib/client/ClientInitializer.ts +60 -0
- package/lib/client/ErrorBoundary.test.tsx +595 -0
- package/lib/client/ErrorBoundary.tsx +230 -0
- package/lib/client/componentRegistry.test.ts +165 -0
- package/lib/client/componentRegistry.ts +18 -0
- package/lib/client/contexts/ThemeContext.tsx +73 -0
- package/lib/client/core/ComponentBuilder.test.ts +677 -0
- package/lib/client/core/ComponentBuilder.ts +660 -0
- package/lib/client/core/ComponentRenderer.test.tsx +176 -0
- package/lib/client/core/ComponentRenderer.tsx +83 -0
- package/lib/client/core/cmsTemplateProcessor.ts +129 -0
- package/lib/client/elementRegistry.ts +81 -0
- package/lib/client/hmr/HMRManager.tsx +179 -0
- package/lib/client/hmr/index.ts +5 -0
- package/lib/client/hmrWebSocket.test.ts +9 -0
- package/lib/client/hmrWebSocket.ts +250 -0
- package/lib/client/hooks/useColorVariables.test.ts +166 -0
- package/lib/client/hooks/useColorVariables.ts +249 -0
- package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
- package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
- package/lib/client/hydration/HydrationUtils.test.ts +154 -0
- package/lib/client/hydration/HydrationUtils.ts +35 -0
- package/lib/client/i18nConfigService.test.ts +74 -0
- package/lib/client/i18nConfigService.ts +78 -0
- package/lib/client/index.ts +56 -0
- package/lib/client/navigation.test.ts +441 -0
- package/lib/client/navigation.ts +23 -0
- package/lib/client/responsiveStyleResolver.test.ts +491 -0
- package/lib/client/responsiveStyleResolver.ts +184 -0
- package/lib/client/routing/RouteLoader.test.ts +635 -0
- package/lib/client/routing/RouteLoader.ts +347 -0
- package/lib/client/routing/Router.tsx +382 -0
- package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
- package/lib/client/scripts/ScriptExecutor.ts +171 -0
- package/lib/client/scripts/formHandler.ts +103 -0
- package/lib/client/styleProcessor.test.ts +126 -0
- package/lib/client/styleProcessor.ts +92 -0
- package/lib/client/styles/StyleInjector.test.ts +354 -0
- package/lib/client/styles/StyleInjector.ts +154 -0
- package/lib/client/templateEngine.test.ts +660 -0
- package/lib/client/templateEngine.ts +667 -0
- package/lib/client/theme.test.ts +173 -0
- package/lib/client/theme.ts +159 -0
- package/lib/client/utils/toast.ts +46 -0
- package/lib/server/createServer.ts +170 -0
- package/lib/server/cssGenerator.test.ts +172 -0
- package/lib/server/cssGenerator.ts +58 -0
- package/lib/server/fileWatcher.ts +134 -0
- package/lib/server/index.ts +55 -0
- package/lib/server/jsonLoader.test.ts +103 -0
- package/lib/server/jsonLoader.ts +350 -0
- package/lib/server/middleware/cors.test.ts +177 -0
- package/lib/server/middleware/cors.ts +69 -0
- package/lib/server/middleware/errorHandler.test.ts +208 -0
- package/lib/server/middleware/errorHandler.ts +63 -0
- package/lib/server/middleware/index.ts +9 -0
- package/lib/server/middleware/logger.test.ts +233 -0
- package/lib/server/middleware/logger.ts +99 -0
- package/lib/server/pageCache.test.ts +167 -0
- package/lib/server/pageCache.ts +97 -0
- package/lib/server/projectContext.ts +51 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
- package/lib/server/providers/fileSystemPageProvider.ts +83 -0
- package/lib/server/routes/api/cms.test.ts +177 -0
- package/lib/server/routes/api/cms.ts +82 -0
- package/lib/server/routes/api/colors.ts +59 -0
- package/lib/server/routes/api/components.ts +70 -0
- package/lib/server/routes/api/config.test.ts +9 -0
- package/lib/server/routes/api/config.ts +28 -0
- package/lib/server/routes/api/core-routes.ts +182 -0
- package/lib/server/routes/api/functions.ts +170 -0
- package/lib/server/routes/api/index.ts +69 -0
- package/lib/server/routes/api/pages.ts +95 -0
- package/lib/server/routes/api/shared.test.ts +81 -0
- package/lib/server/routes/api/shared.ts +31 -0
- package/lib/server/routes/editor.test.ts +9 -0
- package/lib/server/routes/index.ts +104 -0
- package/lib/server/routes/pages.ts +161 -0
- package/lib/server/routes/static.ts +107 -0
- package/lib/server/services/ColorService.ts +193 -0
- package/lib/server/services/cmsService.test.ts +388 -0
- package/lib/server/services/cmsService.ts +296 -0
- package/lib/server/services/componentService.test.ts +276 -0
- package/lib/server/services/componentService.ts +346 -0
- package/lib/server/services/configService.ts +156 -0
- package/lib/server/services/fileWatcherService.ts +67 -0
- package/lib/server/services/index.ts +10 -0
- package/lib/server/services/pageService.test.ts +258 -0
- package/lib/server/services/pageService.ts +240 -0
- package/lib/server/ssrRenderer.test.ts +1005 -0
- package/lib/server/ssrRenderer.ts +878 -0
- package/lib/server/utilityClassGenerator.ts +11 -0
- package/lib/server/utils/index.ts +5 -0
- package/lib/server/utils/jsonLineMapper.test.ts +100 -0
- package/lib/server/utils/jsonLineMapper.ts +166 -0
- package/lib/server/validateStyleCoverage.test.ts +9 -0
- package/lib/server/validateStyleCoverage.ts +167 -0
- package/lib/server/websocketManager.test.ts +9 -0
- package/lib/server/websocketManager.ts +95 -0
- package/lib/shared/attributeNodeUtils.test.ts +152 -0
- package/lib/shared/attributeNodeUtils.ts +50 -0
- package/lib/shared/breakpoints.test.ts +166 -0
- package/lib/shared/breakpoints.ts +65 -0
- package/lib/shared/colorProperties.test.ts +111 -0
- package/lib/shared/colorProperties.ts +40 -0
- package/lib/shared/colorVariableUtils.test.ts +319 -0
- package/lib/shared/colorVariableUtils.ts +97 -0
- package/lib/shared/constants.test.ts +175 -0
- package/lib/shared/constants.ts +116 -0
- package/lib/shared/cssGeneration.ts +481 -0
- package/lib/shared/cssProperties.test.ts +252 -0
- package/lib/shared/cssProperties.ts +338 -0
- package/lib/shared/elementUtils.test.ts +245 -0
- package/lib/shared/elementUtils.ts +90 -0
- package/lib/shared/fontLoader.ts +97 -0
- package/lib/shared/i18n.test.ts +313 -0
- package/lib/shared/i18n.ts +286 -0
- package/lib/shared/index.ts +50 -0
- package/lib/shared/interfaces/contentProvider.test.ts +9 -0
- package/lib/shared/interfaces/contentProvider.ts +121 -0
- package/lib/shared/nodeUtils.test.ts +320 -0
- package/lib/shared/nodeUtils.ts +220 -0
- package/lib/shared/pathArrayUtils.test.ts +315 -0
- package/lib/shared/pathArrayUtils.ts +17 -0
- package/lib/shared/pathUtils.test.ts +260 -0
- package/lib/shared/pathUtils.ts +244 -0
- package/lib/shared/paths/Path.test.ts +74 -0
- package/lib/shared/paths/Path.ts +23 -0
- package/lib/shared/paths/PathConverter.test.ts +232 -0
- package/lib/shared/paths/PathConverter.ts +141 -0
- package/lib/shared/paths/PathUtils.ts +290 -0
- package/lib/shared/paths/PathValidator.test.ts +193 -0
- package/lib/shared/paths/PathValidator.ts +53 -0
- package/lib/shared/paths/index.ts +48 -0
- package/lib/shared/propResolver.test.ts +639 -0
- package/lib/shared/propResolver.ts +124 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
- package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
- package/lib/shared/registry/ClientRegistry.test.ts +26 -0
- package/lib/shared/registry/ClientRegistry.ts +15 -0
- package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
- package/lib/shared/registry/ComponentRegistry.ts +100 -0
- package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
- package/lib/shared/registry/NodeTypeManager.ts +94 -0
- package/lib/shared/registry/RegistryManager.test.ts +58 -0
- package/lib/shared/registry/RegistryManager.ts +60 -0
- package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
- package/lib/shared/registry/SSRRegistry.test.ts +26 -0
- package/lib/shared/registry/SSRRegistry.ts +15 -0
- package/lib/shared/registry/createNodeType.ts +175 -0
- package/lib/shared/registry/defineNodeType.ts +73 -0
- package/lib/shared/registry/fieldPresets.ts +109 -0
- package/lib/shared/registry/index.ts +50 -0
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
- package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
- package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
- package/lib/shared/registry/nodeTypes/index.ts +75 -0
- package/lib/shared/responsiveScaling.test.ts +268 -0
- package/lib/shared/responsiveScaling.ts +194 -0
- package/lib/shared/responsiveStyleUtils.test.ts +300 -0
- package/lib/shared/responsiveStyleUtils.ts +139 -0
- package/lib/shared/slugTranslator.test.ts +325 -0
- package/lib/shared/slugTranslator.ts +177 -0
- package/lib/shared/styleNodeUtils.test.ts +132 -0
- package/lib/shared/styleNodeUtils.ts +102 -0
- package/lib/shared/styleUtils.test.ts +238 -0
- package/lib/shared/styleUtils.ts +63 -0
- package/lib/shared/themeDefaults.test.ts +113 -0
- package/lib/shared/themeDefaults.ts +103 -0
- package/lib/shared/tree/PathBuilder.ts +383 -0
- package/lib/shared/treePathUtils.test.ts +539 -0
- package/lib/shared/treePathUtils.ts +339 -0
- package/lib/shared/types/api.ts +58 -0
- package/lib/shared/types/cms.ts +95 -0
- package/lib/shared/types/colors.ts +45 -0
- package/lib/shared/types/components.ts +121 -0
- package/lib/shared/types/errors.test.ts +103 -0
- package/lib/shared/types/errors.ts +69 -0
- package/lib/shared/types/index.ts +96 -0
- package/lib/shared/types/nodes.ts +20 -0
- package/lib/shared/types/rendering.ts +61 -0
- package/lib/shared/types/styles.ts +38 -0
- package/lib/shared/types.ts +11 -0
- package/lib/shared/utilityClassConfig.ts +287 -0
- package/lib/shared/utilityClassMapper.test.ts +140 -0
- package/lib/shared/utilityClassMapper.ts +229 -0
- package/lib/shared/utils/fileUtils.test.ts +99 -0
- package/lib/shared/utils/fileUtils.ts +56 -0
- package/lib/shared/utils.test.ts +261 -0
- package/lib/shared/utils.ts +84 -0
- package/lib/shared/validation/index.ts +7 -0
- package/lib/shared/validation/propValidator.test.ts +178 -0
- package/lib/shared/validation/propValidator.ts +238 -0
- package/lib/shared/validation/schemas.test.ts +177 -0
- package/lib/shared/validation/schemas.ts +401 -0
- package/lib/shared/validation/validators.test.ts +109 -0
- package/lib/shared/validation/validators.ts +304 -0
- package/lib/test-utils/dom-setup.ts +55 -0
- package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
- package/lib/test-utils/factories/DomMockFactory.ts +487 -0
- package/lib/test-utils/factories/EventMockFactory.ts +244 -0
- package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
- package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
- package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
- package/lib/test-utils/factories/index.ts +11 -0
- package/lib/test-utils/fixtures.ts +134 -0
- package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
- package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
- package/lib/test-utils/helpers/index.ts +6 -0
- package/lib/test-utils/helpers.test.ts +73 -0
- package/lib/test-utils/helpers.ts +90 -0
- package/lib/test-utils/index.ts +17 -0
- package/lib/test-utils/mockFactories.ts +92 -0
- package/lib/test-utils/mocks.ts +341 -0
- package/package.json +38 -0
- package/templates/index-router.html +34 -0
- package/tsconfig.json +14 -0
- package/vite.config.ts +43 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
resolveResponsiveStyleSync,
|
|
4
|
+
resolveResponsiveStyle,
|
|
5
|
+
initializeBreakpoints
|
|
6
|
+
} from "./responsiveStyleResolver";
|
|
7
|
+
import { getBreakpointName, DEFAULT_BREAKPOINTS } from "../shared/breakpoints";
|
|
8
|
+
import type { ResponsiveStyleObject, StyleObject } from "../shared/types";
|
|
9
|
+
|
|
10
|
+
describe("Responsive Style Resolver - getBreakpointName", () => {
|
|
11
|
+
describe("Breakpoint detection", () => {
|
|
12
|
+
test("should return 'base' for large viewport", () => {
|
|
13
|
+
const viewportWidth = 1920;
|
|
14
|
+
const result = getBreakpointName(viewportWidth);
|
|
15
|
+
expect(result).toBe("base");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("should return 'base' for viewport wider than tablet", () => {
|
|
19
|
+
const viewportWidth = 1025;
|
|
20
|
+
const result = getBreakpointName(viewportWidth);
|
|
21
|
+
expect(result).toBe("base");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("should return 'tablet' for viewport at tablet breakpoint", () => {
|
|
25
|
+
const viewportWidth = 1024;
|
|
26
|
+
const result = getBreakpointName(viewportWidth);
|
|
27
|
+
expect(result).toBe("tablet");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("should return 'tablet' for viewport between mobile and tablet", () => {
|
|
31
|
+
const viewportWidth = 768;
|
|
32
|
+
const result = getBreakpointName(viewportWidth);
|
|
33
|
+
expect(result).toBe("tablet");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("should return 'tablet' for viewport just above mobile breakpoint", () => {
|
|
37
|
+
const viewportWidth = 541;
|
|
38
|
+
const result = getBreakpointName(viewportWidth);
|
|
39
|
+
expect(result).toBe("tablet");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("should return 'mobile' for viewport at mobile breakpoint", () => {
|
|
43
|
+
const viewportWidth = 540;
|
|
44
|
+
const result = getBreakpointName(viewportWidth);
|
|
45
|
+
expect(result).toBe("mobile");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("should return 'mobile' for viewport smaller than mobile breakpoint", () => {
|
|
49
|
+
const viewportWidth = 320;
|
|
50
|
+
const result = getBreakpointName(viewportWidth);
|
|
51
|
+
expect(result).toBe("mobile");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("should use custom breakpoints when provided", () => {
|
|
55
|
+
const viewportWidth = 800;
|
|
56
|
+
const customBreakpoints = {
|
|
57
|
+
tablet: 900,
|
|
58
|
+
mobile: 400
|
|
59
|
+
};
|
|
60
|
+
const result = getBreakpointName(viewportWidth, customBreakpoints);
|
|
61
|
+
expect(result).toBe("tablet");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("Edge cases", () => {
|
|
66
|
+
test("should handle exact breakpoint boundaries", () => {
|
|
67
|
+
const tabletWidth = DEFAULT_BREAKPOINTS.tablet;
|
|
68
|
+
const mobileWidth = DEFAULT_BREAKPOINTS.mobile;
|
|
69
|
+
|
|
70
|
+
expect(getBreakpointName(tabletWidth)).toBe("tablet");
|
|
71
|
+
expect(getBreakpointName(tabletWidth - 1)).toBe("tablet");
|
|
72
|
+
expect(getBreakpointName(tabletWidth + 1)).toBe("base");
|
|
73
|
+
|
|
74
|
+
expect(getBreakpointName(mobileWidth)).toBe("mobile");
|
|
75
|
+
expect(getBreakpointName(mobileWidth - 1)).toBe("mobile");
|
|
76
|
+
expect(getBreakpointName(mobileWidth + 1)).toBe("tablet");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("should handle zero viewport width", () => {
|
|
80
|
+
const result = getBreakpointName(0);
|
|
81
|
+
expect(result).toBe("mobile");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("should handle very large viewport width", () => {
|
|
85
|
+
const result = getBreakpointName(10000);
|
|
86
|
+
expect(result).toBe("base");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("Responsive Style Resolver - resolveResponsiveStyleSync", () => {
|
|
92
|
+
describe("Non-responsive styles", () => {
|
|
93
|
+
test("should pass through flat style objects", () => {
|
|
94
|
+
const style: StyleObject = {
|
|
95
|
+
color: "red",
|
|
96
|
+
fontSize: "16px"
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const result = resolveResponsiveStyleSync(style);
|
|
100
|
+
expect(result).toEqual(style);
|
|
101
|
+
expect(result.color).toBe("red");
|
|
102
|
+
expect(result.fontSize).toBe("16px");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("should return same object for non-responsive styles", () => {
|
|
106
|
+
const style: StyleObject = {
|
|
107
|
+
padding: "10px",
|
|
108
|
+
margin: "20px"
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const result = resolveResponsiveStyleSync(style);
|
|
112
|
+
expect(result).toBe(style); // Should return same reference for flat styles
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("Responsive styles - viewport strategy", () => {
|
|
117
|
+
test("should merge base only styles at base breakpoint", () => {
|
|
118
|
+
const style: ResponsiveStyleObject = {
|
|
119
|
+
base: {
|
|
120
|
+
color: "red",
|
|
121
|
+
fontSize: "16px"
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const viewportWidth = 1920;
|
|
125
|
+
|
|
126
|
+
const result = resolveResponsiveStyleSync(style, viewportWidth);
|
|
127
|
+
|
|
128
|
+
expect(result.color).toBe("red");
|
|
129
|
+
expect(result.fontSize).toBe("16px");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("should merge base and tablet styles at tablet breakpoint", () => {
|
|
133
|
+
const style: ResponsiveStyleObject = {
|
|
134
|
+
base: {
|
|
135
|
+
color: "red",
|
|
136
|
+
fontSize: "16px"
|
|
137
|
+
},
|
|
138
|
+
tablet: {
|
|
139
|
+
fontSize: "14px",
|
|
140
|
+
padding: "10px"
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const viewportWidth = 768;
|
|
144
|
+
|
|
145
|
+
const result = resolveResponsiveStyleSync(style, viewportWidth);
|
|
146
|
+
|
|
147
|
+
expect(result.color).toBe("red"); // From base
|
|
148
|
+
expect(result.fontSize).toBe("14px"); // Overridden by tablet
|
|
149
|
+
expect(result.padding).toBe("10px"); // From tablet
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("should merge all breakpoints at mobile breakpoint", () => {
|
|
153
|
+
const style: ResponsiveStyleObject = {
|
|
154
|
+
base: {
|
|
155
|
+
color: "red",
|
|
156
|
+
fontSize: "16px"
|
|
157
|
+
},
|
|
158
|
+
tablet: {
|
|
159
|
+
fontSize: "14px"
|
|
160
|
+
},
|
|
161
|
+
mobile: {
|
|
162
|
+
fontSize: "12px",
|
|
163
|
+
color: "blue"
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const viewportWidth = 320;
|
|
167
|
+
|
|
168
|
+
const result = resolveResponsiveStyleSync(style, viewportWidth);
|
|
169
|
+
|
|
170
|
+
expect(result.color).toBe("blue"); // Overridden by mobile
|
|
171
|
+
expect(result.fontSize).toBe("12px"); // Overridden by mobile
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("should apply tablet styles at tablet viewport", () => {
|
|
175
|
+
const style: ResponsiveStyleObject = {
|
|
176
|
+
base: {
|
|
177
|
+
padding: "20px"
|
|
178
|
+
},
|
|
179
|
+
tablet: {
|
|
180
|
+
padding: "15px"
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const viewportWidth = 1024;
|
|
184
|
+
|
|
185
|
+
const result = resolveResponsiveStyleSync(style, viewportWidth);
|
|
186
|
+
|
|
187
|
+
expect(result.padding).toBe("15px");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("should apply mobile styles at mobile viewport", () => {
|
|
191
|
+
const style: ResponsiveStyleObject = {
|
|
192
|
+
base: {
|
|
193
|
+
padding: "20px"
|
|
194
|
+
},
|
|
195
|
+
tablet: {
|
|
196
|
+
padding: "15px"
|
|
197
|
+
},
|
|
198
|
+
mobile: {
|
|
199
|
+
padding: "10px"
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const viewportWidth = 540;
|
|
203
|
+
|
|
204
|
+
const result = resolveResponsiveStyleSync(style, viewportWidth);
|
|
205
|
+
|
|
206
|
+
expect(result.padding).toBe("10px");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("Responsive styles - style object variations", () => {
|
|
211
|
+
test("should handle styles with base only", () => {
|
|
212
|
+
const style: ResponsiveStyleObject = {
|
|
213
|
+
base: {
|
|
214
|
+
color: "black",
|
|
215
|
+
backgroundColor: "white"
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const result = resolveResponsiveStyleSync(style, 1920);
|
|
220
|
+
|
|
221
|
+
expect(result.color).toBe("black");
|
|
222
|
+
expect(result.backgroundColor).toBe("white");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("should handle styles with base and tablet", () => {
|
|
226
|
+
const style: ResponsiveStyleObject = {
|
|
227
|
+
base: {
|
|
228
|
+
fontSize: "16px"
|
|
229
|
+
},
|
|
230
|
+
tablet: {
|
|
231
|
+
fontSize: "14px"
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const result = resolveResponsiveStyleSync(style, 768);
|
|
236
|
+
|
|
237
|
+
expect(result.fontSize).toBe("14px");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("should handle styles with all breakpoints", () => {
|
|
241
|
+
const style: ResponsiveStyleObject = {
|
|
242
|
+
base: {
|
|
243
|
+
fontSize: "18px",
|
|
244
|
+
color: "black"
|
|
245
|
+
},
|
|
246
|
+
tablet: {
|
|
247
|
+
fontSize: "16px"
|
|
248
|
+
},
|
|
249
|
+
mobile: {
|
|
250
|
+
fontSize: "14px",
|
|
251
|
+
color: "gray"
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const result = resolveResponsiveStyleSync(style, 320);
|
|
256
|
+
|
|
257
|
+
expect(result.fontSize).toBe("14px");
|
|
258
|
+
expect(result.color).toBe("gray");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("should handle missing breakpoint styles", () => {
|
|
262
|
+
const style: ResponsiveStyleObject = {
|
|
263
|
+
base: {
|
|
264
|
+
color: "red"
|
|
265
|
+
}
|
|
266
|
+
// No tablet or mobile
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = resolveResponsiveStyleSync(style, 320);
|
|
270
|
+
|
|
271
|
+
expect(result.color).toBe("red"); // Should still have base styles
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("should handle empty base styles", () => {
|
|
275
|
+
const style: ResponsiveStyleObject = {
|
|
276
|
+
base: {},
|
|
277
|
+
tablet: {
|
|
278
|
+
fontSize: "14px"
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const result = resolveResponsiveStyleSync(style, 768);
|
|
283
|
+
|
|
284
|
+
expect(result.fontSize).toBe("14px");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("Edge cases", () => {
|
|
289
|
+
test("should handle empty responsive style object", () => {
|
|
290
|
+
const style: ResponsiveStyleObject = {
|
|
291
|
+
base: {}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const result = resolveResponsiveStyleSync(style, 1920);
|
|
295
|
+
|
|
296
|
+
expect(Object.keys(result).length).toBe(0);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("should handle viewport width at exact tablet boundary", () => {
|
|
300
|
+
const style: ResponsiveStyleObject = {
|
|
301
|
+
base: { fontSize: "16px" },
|
|
302
|
+
tablet: { fontSize: "14px" }
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const result = resolveResponsiveStyleSync(style, DEFAULT_BREAKPOINTS.tablet);
|
|
306
|
+
|
|
307
|
+
expect(result.fontSize).toBe("14px");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("should handle viewport width at exact mobile boundary", () => {
|
|
311
|
+
const style: ResponsiveStyleObject = {
|
|
312
|
+
base: { fontSize: "16px" },
|
|
313
|
+
tablet: { fontSize: "14px" },
|
|
314
|
+
mobile: { fontSize: "12px" }
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const result = resolveResponsiveStyleSync(style, DEFAULT_BREAKPOINTS.mobile);
|
|
318
|
+
|
|
319
|
+
expect(result.fontSize).toBe("12px");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("should use default viewport width when not provided", () => {
|
|
323
|
+
const style: ResponsiveStyleObject = {
|
|
324
|
+
base: { fontSize: "16px" }
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Should use window.innerWidth or 1920 as default
|
|
328
|
+
const result = resolveResponsiveStyleSync(style);
|
|
329
|
+
|
|
330
|
+
expect(result.fontSize).toBe("16px");
|
|
331
|
+
// Note: Can't reliably test the exact default without window object in test env
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("Responsive Style Resolver - resolveResponsiveStyle (async)", () => {
|
|
337
|
+
// Mock fetch for async tests
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
// Reset global fetch mock
|
|
340
|
+
global.fetch = global.fetch || (() => Promise.reject(new Error('fetch not implemented'))) as any;
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("should resolve non-responsive styles asynchronously", async () => {
|
|
344
|
+
const style: StyleObject = {
|
|
345
|
+
color: "red"
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const result = await resolveResponsiveStyle(style);
|
|
349
|
+
|
|
350
|
+
expect(result).toEqual(style);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("should resolve responsive styles with default breakpoints on fetch error", async () => {
|
|
354
|
+
// Mock fetch to fail
|
|
355
|
+
global.fetch = () => Promise.reject(new Error('Network error')) as any;
|
|
356
|
+
|
|
357
|
+
const style: ResponsiveStyleObject = {
|
|
358
|
+
base: {
|
|
359
|
+
fontSize: "16px"
|
|
360
|
+
},
|
|
361
|
+
tablet: {
|
|
362
|
+
fontSize: "14px"
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const result = await resolveResponsiveStyle(style);
|
|
367
|
+
|
|
368
|
+
// Should use default breakpoints and resolve based on window.innerWidth or 1920
|
|
369
|
+
expect(result.fontSize).toBeDefined();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("should handle async breakpoint config loading", async () => {
|
|
373
|
+
// Mock successful fetch
|
|
374
|
+
global.fetch = () => Promise.resolve({
|
|
375
|
+
json: () => Promise.resolve({
|
|
376
|
+
breakpoints: {
|
|
377
|
+
tablet: 900,
|
|
378
|
+
mobile: 400
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
}) as any;
|
|
382
|
+
|
|
383
|
+
const style: ResponsiveStyleObject = {
|
|
384
|
+
base: { fontSize: "16px" },
|
|
385
|
+
tablet: { fontSize: "14px" }
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// First call should trigger config load
|
|
389
|
+
await resolveResponsiveStyle(style);
|
|
390
|
+
|
|
391
|
+
// Second call should use cached config
|
|
392
|
+
const result = await resolveResponsiveStyle(style);
|
|
393
|
+
|
|
394
|
+
expect(result.fontSize).toBeDefined();
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe("Responsive Style Resolver - initializeBreakpoints", () => {
|
|
399
|
+
test("should initialize breakpoint config", async () => {
|
|
400
|
+
// Mock successful fetch
|
|
401
|
+
global.fetch = () => Promise.resolve({
|
|
402
|
+
json: () => Promise.resolve({
|
|
403
|
+
breakpoints: {
|
|
404
|
+
tablet: 900,
|
|
405
|
+
mobile: 400
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
}) as any;
|
|
409
|
+
|
|
410
|
+
await initializeBreakpoints();
|
|
411
|
+
|
|
412
|
+
// If initialization succeeds, subsequent resolveResponsiveStyleSync should use cached config
|
|
413
|
+
const style: ResponsiveStyleObject = {
|
|
414
|
+
base: { fontSize: "16px" }
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const result = resolveResponsiveStyleSync(style);
|
|
418
|
+
expect(result.fontSize).toBe("16px");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("should handle initialization error gracefully", async () => {
|
|
422
|
+
// Mock fetch to fail
|
|
423
|
+
global.fetch = () => Promise.reject(new Error('Network error')) as any;
|
|
424
|
+
|
|
425
|
+
// Should not throw
|
|
426
|
+
await expect(initializeBreakpoints()).resolves.toBeUndefined();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe("Responsive Style Resolver - Real-world examples", () => {
|
|
431
|
+
test("should handle complex responsive style from index.json example", () => {
|
|
432
|
+
// Based on pages/index.json - h1 with responsive fontSize
|
|
433
|
+
const style: ResponsiveStyleObject = {
|
|
434
|
+
base: {
|
|
435
|
+
fontSize: "89px",
|
|
436
|
+
fontWeight: "600",
|
|
437
|
+
marginBottom: "16px",
|
|
438
|
+
color: "#1f2937",
|
|
439
|
+
letterSpacing: "-0.02em"
|
|
440
|
+
},
|
|
441
|
+
tablet: {
|
|
442
|
+
fontSize: "32px"
|
|
443
|
+
},
|
|
444
|
+
mobile: {}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// Test at base viewport
|
|
448
|
+
const resultBase = resolveResponsiveStyleSync(style, 1920);
|
|
449
|
+
expect(resultBase.fontSize).toBe("89px");
|
|
450
|
+
expect(resultBase.fontWeight).toBe("600");
|
|
451
|
+
|
|
452
|
+
// Test at tablet viewport
|
|
453
|
+
const resultTablet = resolveResponsiveStyleSync(style, 768);
|
|
454
|
+
expect(resultTablet.fontSize).toBe("32px");
|
|
455
|
+
expect(resultTablet.fontWeight).toBe("600"); // From base
|
|
456
|
+
|
|
457
|
+
// Test at mobile viewport
|
|
458
|
+
// Implementation note: viewport strategy applies base + active breakpoint only
|
|
459
|
+
// Tablet styles don't cascade to mobile - each breakpoint is independent
|
|
460
|
+
const resultMobile = resolveResponsiveStyleSync(style, 320);
|
|
461
|
+
expect(resultMobile.fontSize).toBe("89px"); // Base only (mobile is empty, tablet doesn't cascade)
|
|
462
|
+
expect(resultMobile.fontWeight).toBe("600"); // From base
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("should apply base + active breakpoint styles", () => {
|
|
466
|
+
// Implementation note: viewport strategy applies base + active breakpoint only
|
|
467
|
+
// Tablet styles don't cascade to mobile - each breakpoint is independent
|
|
468
|
+
const style: ResponsiveStyleObject = {
|
|
469
|
+
base: {
|
|
470
|
+
padding: "20px",
|
|
471
|
+
margin: "10px",
|
|
472
|
+
color: "black"
|
|
473
|
+
},
|
|
474
|
+
tablet: {
|
|
475
|
+
padding: "15px",
|
|
476
|
+
fontSize: "14px"
|
|
477
|
+
},
|
|
478
|
+
mobile: {
|
|
479
|
+
padding: "10px",
|
|
480
|
+
color: "gray"
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const result = resolveResponsiveStyleSync(style, 320);
|
|
485
|
+
|
|
486
|
+
expect(result.padding).toBe("10px"); // Mobile overrides
|
|
487
|
+
expect(result.margin).toBe("10px"); // From base
|
|
488
|
+
expect(result.color).toBe("gray"); // Mobile overrides
|
|
489
|
+
expect(result.fontSize).toBeUndefined(); // Tablet doesn't cascade to mobile
|
|
490
|
+
});
|
|
491
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive Style Resolver for Editor
|
|
3
|
+
* Merges responsive styles into inline styles based on current viewport width
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ResponsiveStyleObject, StyleObject } from '../shared/types';
|
|
7
|
+
import type { BreakpointConfig } from '../shared/breakpoints';
|
|
8
|
+
import { DEFAULT_BREAKPOINTS } from '../shared/breakpoints';
|
|
9
|
+
import type { ResponsiveScales } from '../shared/responsiveScaling';
|
|
10
|
+
import { DEFAULT_RESPONSIVE_SCALES } from '../shared/responsiveScaling';
|
|
11
|
+
import { isResponsiveStyle } from '../shared/styleUtils';
|
|
12
|
+
import { mergeResponsiveStyles } from '../shared/responsiveStyleUtils';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get breakpoint configuration (with caching)
|
|
16
|
+
*/
|
|
17
|
+
let breakpointConfig: BreakpointConfig | null = null;
|
|
18
|
+
let breakpointPromise: Promise<BreakpointConfig> | null = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get responsive scales configuration (with caching)
|
|
22
|
+
*/
|
|
23
|
+
let responsiveScalesConfig: ResponsiveScales | null = null;
|
|
24
|
+
let responsiveScalesPromise: Promise<ResponsiveScales> | null = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Shared config fetching promise
|
|
28
|
+
*/
|
|
29
|
+
let configPromise: Promise<void> | null = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clear the breakpoint config cache (useful when config file changes)
|
|
33
|
+
*/
|
|
34
|
+
export function clearBreakpointConfigCache(): void {
|
|
35
|
+
breakpointConfig = null;
|
|
36
|
+
breakpointPromise = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Clear the responsive scales config cache (useful when config file changes)
|
|
41
|
+
*/
|
|
42
|
+
export function clearResponsiveScalesConfigCache(): void {
|
|
43
|
+
responsiveScalesConfig = null;
|
|
44
|
+
responsiveScalesPromise = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Clear all config caches
|
|
49
|
+
*/
|
|
50
|
+
export function clearAllConfigCache(): void {
|
|
51
|
+
clearBreakpointConfigCache();
|
|
52
|
+
clearResponsiveScalesConfigCache();
|
|
53
|
+
configPromise = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get cached breakpoint config synchronously (returns null if not loaded yet)
|
|
58
|
+
*/
|
|
59
|
+
export function getCachedBreakpointConfig(): BreakpointConfig | null {
|
|
60
|
+
return breakpointConfig;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get cached responsive scales config synchronously (returns null if not loaded yet)
|
|
65
|
+
*/
|
|
66
|
+
export function getCachedResponsiveScalesConfig(): ResponsiveScales | null {
|
|
67
|
+
return responsiveScalesConfig;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Shared config fetching - loads both breakpoints and responsive scales from single API call
|
|
72
|
+
*/
|
|
73
|
+
async function loadConfig(): Promise<void> {
|
|
74
|
+
if (breakpointConfig && responsiveScalesConfig) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!configPromise) {
|
|
79
|
+
configPromise = (async () => {
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch('/api/config');
|
|
82
|
+
const config = await response.json();
|
|
83
|
+
|
|
84
|
+
// Parse breakpoints
|
|
85
|
+
if (config.breakpoints && typeof config.breakpoints === 'object') {
|
|
86
|
+
const breakpoints: BreakpointConfig = {};
|
|
87
|
+
for (const [key, value] of Object.entries(config.breakpoints)) {
|
|
88
|
+
if (typeof value === 'number' && value > 0) {
|
|
89
|
+
breakpoints[key] = value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
breakpointConfig = Object.keys(breakpoints).length > 0
|
|
93
|
+
? breakpoints
|
|
94
|
+
: { ...DEFAULT_BREAKPOINTS };
|
|
95
|
+
} else {
|
|
96
|
+
breakpointConfig = { ...DEFAULT_BREAKPOINTS };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Parse responsive scales
|
|
100
|
+
if (config.responsiveScales && typeof config.responsiveScales === 'object') {
|
|
101
|
+
responsiveScalesConfig = {
|
|
102
|
+
...DEFAULT_RESPONSIVE_SCALES,
|
|
103
|
+
...config.responsiveScales,
|
|
104
|
+
};
|
|
105
|
+
} else {
|
|
106
|
+
responsiveScalesConfig = { ...DEFAULT_RESPONSIVE_SCALES };
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
breakpointConfig = { ...DEFAULT_BREAKPOINTS };
|
|
110
|
+
responsiveScalesConfig = { ...DEFAULT_RESPONSIVE_SCALES };
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await configPromise;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function getBreakpointConfig(): Promise<BreakpointConfig> {
|
|
119
|
+
await loadConfig();
|
|
120
|
+
return breakpointConfig!;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get responsive scales configuration asynchronously
|
|
125
|
+
*/
|
|
126
|
+
export async function getResponsiveScalesConfig(): Promise<ResponsiveScales> {
|
|
127
|
+
await loadConfig();
|
|
128
|
+
return responsiveScalesConfig!;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Merge responsive styles into a single style object based on current viewport
|
|
133
|
+
* Styles cascade: base → tablet → mobile
|
|
134
|
+
*/
|
|
135
|
+
export async function resolveResponsiveStyle(
|
|
136
|
+
style: ResponsiveStyleObject | StyleObject
|
|
137
|
+
): Promise<StyleObject> {
|
|
138
|
+
if (!isResponsiveStyle(style)) {
|
|
139
|
+
return style as StyleObject;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const responsiveStyle = style as ResponsiveStyleObject;
|
|
143
|
+
const viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 1920;
|
|
144
|
+
const breakpoints = await getBreakpointConfig();
|
|
145
|
+
|
|
146
|
+
return mergeResponsiveStyles(responsiveStyle, 'viewport', viewportWidth, breakpoints);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Synchronous version that uses cached breakpoints or defaults
|
|
151
|
+
* For immediate rendering, then async update when breakpoints load
|
|
152
|
+
*/
|
|
153
|
+
export function resolveResponsiveStyleSync(
|
|
154
|
+
style: ResponsiveStyleObject | StyleObject,
|
|
155
|
+
viewportWidth: number = typeof window !== 'undefined' ? window.innerWidth : 1920
|
|
156
|
+
): StyleObject {
|
|
157
|
+
if (!isResponsiveStyle(style)) {
|
|
158
|
+
return style as StyleObject;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const responsiveStyle = style as ResponsiveStyleObject;
|
|
162
|
+
|
|
163
|
+
// Use cached breakpoints or defaults
|
|
164
|
+
const breakpoints = breakpointConfig || { ...DEFAULT_BREAKPOINTS };
|
|
165
|
+
|
|
166
|
+
return mergeResponsiveStyles(responsiveStyle, 'viewport', viewportWidth, breakpoints);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Initialize breakpoint and responsive scales config (call this early)
|
|
171
|
+
* If forceRefresh is true, clears cache and fetches fresh config
|
|
172
|
+
*/
|
|
173
|
+
export async function initializeBreakpoints(forceRefresh: boolean = false): Promise<void> {
|
|
174
|
+
if (forceRefresh) {
|
|
175
|
+
clearAllConfigCache();
|
|
176
|
+
}
|
|
177
|
+
await loadConfig();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Alias for initializeBreakpoints - initializes all config including responsive scales
|
|
182
|
+
*/
|
|
183
|
+
export const initializeConfig = initializeBreakpoints;
|
|
184
|
+
|