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.
Files changed (231) hide show
  1. package/bin/cli.ts +281 -0
  2. package/build-static.ts +298 -0
  3. package/bunfig.toml +39 -0
  4. package/entries/client-router.tsx +111 -0
  5. package/entries/server-router.tsx +71 -0
  6. package/lib/client/ClientInitializer.test.ts +9 -0
  7. package/lib/client/ClientInitializer.test.ts.skip +92 -0
  8. package/lib/client/ClientInitializer.ts +60 -0
  9. package/lib/client/ErrorBoundary.test.tsx +595 -0
  10. package/lib/client/ErrorBoundary.tsx +230 -0
  11. package/lib/client/componentRegistry.test.ts +165 -0
  12. package/lib/client/componentRegistry.ts +18 -0
  13. package/lib/client/contexts/ThemeContext.tsx +73 -0
  14. package/lib/client/core/ComponentBuilder.test.ts +677 -0
  15. package/lib/client/core/ComponentBuilder.ts +660 -0
  16. package/lib/client/core/ComponentRenderer.test.tsx +176 -0
  17. package/lib/client/core/ComponentRenderer.tsx +83 -0
  18. package/lib/client/core/cmsTemplateProcessor.ts +129 -0
  19. package/lib/client/elementRegistry.ts +81 -0
  20. package/lib/client/hmr/HMRManager.tsx +179 -0
  21. package/lib/client/hmr/index.ts +5 -0
  22. package/lib/client/hmrWebSocket.test.ts +9 -0
  23. package/lib/client/hmrWebSocket.ts +250 -0
  24. package/lib/client/hooks/useColorVariables.test.ts +166 -0
  25. package/lib/client/hooks/useColorVariables.ts +249 -0
  26. package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
  27. package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
  28. package/lib/client/hydration/HydrationUtils.test.ts +154 -0
  29. package/lib/client/hydration/HydrationUtils.ts +35 -0
  30. package/lib/client/i18nConfigService.test.ts +74 -0
  31. package/lib/client/i18nConfigService.ts +78 -0
  32. package/lib/client/index.ts +56 -0
  33. package/lib/client/navigation.test.ts +441 -0
  34. package/lib/client/navigation.ts +23 -0
  35. package/lib/client/responsiveStyleResolver.test.ts +491 -0
  36. package/lib/client/responsiveStyleResolver.ts +184 -0
  37. package/lib/client/routing/RouteLoader.test.ts +635 -0
  38. package/lib/client/routing/RouteLoader.ts +347 -0
  39. package/lib/client/routing/Router.tsx +382 -0
  40. package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
  41. package/lib/client/scripts/ScriptExecutor.ts +171 -0
  42. package/lib/client/scripts/formHandler.ts +103 -0
  43. package/lib/client/styleProcessor.test.ts +126 -0
  44. package/lib/client/styleProcessor.ts +92 -0
  45. package/lib/client/styles/StyleInjector.test.ts +354 -0
  46. package/lib/client/styles/StyleInjector.ts +154 -0
  47. package/lib/client/templateEngine.test.ts +660 -0
  48. package/lib/client/templateEngine.ts +667 -0
  49. package/lib/client/theme.test.ts +173 -0
  50. package/lib/client/theme.ts +159 -0
  51. package/lib/client/utils/toast.ts +46 -0
  52. package/lib/server/createServer.ts +170 -0
  53. package/lib/server/cssGenerator.test.ts +172 -0
  54. package/lib/server/cssGenerator.ts +58 -0
  55. package/lib/server/fileWatcher.ts +134 -0
  56. package/lib/server/index.ts +55 -0
  57. package/lib/server/jsonLoader.test.ts +103 -0
  58. package/lib/server/jsonLoader.ts +350 -0
  59. package/lib/server/middleware/cors.test.ts +177 -0
  60. package/lib/server/middleware/cors.ts +69 -0
  61. package/lib/server/middleware/errorHandler.test.ts +208 -0
  62. package/lib/server/middleware/errorHandler.ts +63 -0
  63. package/lib/server/middleware/index.ts +9 -0
  64. package/lib/server/middleware/logger.test.ts +233 -0
  65. package/lib/server/middleware/logger.ts +99 -0
  66. package/lib/server/pageCache.test.ts +167 -0
  67. package/lib/server/pageCache.ts +97 -0
  68. package/lib/server/projectContext.ts +51 -0
  69. package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
  70. package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
  71. package/lib/server/providers/fileSystemPageProvider.ts +83 -0
  72. package/lib/server/routes/api/cms.test.ts +177 -0
  73. package/lib/server/routes/api/cms.ts +82 -0
  74. package/lib/server/routes/api/colors.ts +59 -0
  75. package/lib/server/routes/api/components.ts +70 -0
  76. package/lib/server/routes/api/config.test.ts +9 -0
  77. package/lib/server/routes/api/config.ts +28 -0
  78. package/lib/server/routes/api/core-routes.ts +182 -0
  79. package/lib/server/routes/api/functions.ts +170 -0
  80. package/lib/server/routes/api/index.ts +69 -0
  81. package/lib/server/routes/api/pages.ts +95 -0
  82. package/lib/server/routes/api/shared.test.ts +81 -0
  83. package/lib/server/routes/api/shared.ts +31 -0
  84. package/lib/server/routes/editor.test.ts +9 -0
  85. package/lib/server/routes/index.ts +104 -0
  86. package/lib/server/routes/pages.ts +161 -0
  87. package/lib/server/routes/static.ts +107 -0
  88. package/lib/server/services/ColorService.ts +193 -0
  89. package/lib/server/services/cmsService.test.ts +388 -0
  90. package/lib/server/services/cmsService.ts +296 -0
  91. package/lib/server/services/componentService.test.ts +276 -0
  92. package/lib/server/services/componentService.ts +346 -0
  93. package/lib/server/services/configService.ts +156 -0
  94. package/lib/server/services/fileWatcherService.ts +67 -0
  95. package/lib/server/services/index.ts +10 -0
  96. package/lib/server/services/pageService.test.ts +258 -0
  97. package/lib/server/services/pageService.ts +240 -0
  98. package/lib/server/ssrRenderer.test.ts +1005 -0
  99. package/lib/server/ssrRenderer.ts +878 -0
  100. package/lib/server/utilityClassGenerator.ts +11 -0
  101. package/lib/server/utils/index.ts +5 -0
  102. package/lib/server/utils/jsonLineMapper.test.ts +100 -0
  103. package/lib/server/utils/jsonLineMapper.ts +166 -0
  104. package/lib/server/validateStyleCoverage.test.ts +9 -0
  105. package/lib/server/validateStyleCoverage.ts +167 -0
  106. package/lib/server/websocketManager.test.ts +9 -0
  107. package/lib/server/websocketManager.ts +95 -0
  108. package/lib/shared/attributeNodeUtils.test.ts +152 -0
  109. package/lib/shared/attributeNodeUtils.ts +50 -0
  110. package/lib/shared/breakpoints.test.ts +166 -0
  111. package/lib/shared/breakpoints.ts +65 -0
  112. package/lib/shared/colorProperties.test.ts +111 -0
  113. package/lib/shared/colorProperties.ts +40 -0
  114. package/lib/shared/colorVariableUtils.test.ts +319 -0
  115. package/lib/shared/colorVariableUtils.ts +97 -0
  116. package/lib/shared/constants.test.ts +175 -0
  117. package/lib/shared/constants.ts +116 -0
  118. package/lib/shared/cssGeneration.ts +481 -0
  119. package/lib/shared/cssProperties.test.ts +252 -0
  120. package/lib/shared/cssProperties.ts +338 -0
  121. package/lib/shared/elementUtils.test.ts +245 -0
  122. package/lib/shared/elementUtils.ts +90 -0
  123. package/lib/shared/fontLoader.ts +97 -0
  124. package/lib/shared/i18n.test.ts +313 -0
  125. package/lib/shared/i18n.ts +286 -0
  126. package/lib/shared/index.ts +50 -0
  127. package/lib/shared/interfaces/contentProvider.test.ts +9 -0
  128. package/lib/shared/interfaces/contentProvider.ts +121 -0
  129. package/lib/shared/nodeUtils.test.ts +320 -0
  130. package/lib/shared/nodeUtils.ts +220 -0
  131. package/lib/shared/pathArrayUtils.test.ts +315 -0
  132. package/lib/shared/pathArrayUtils.ts +17 -0
  133. package/lib/shared/pathUtils.test.ts +260 -0
  134. package/lib/shared/pathUtils.ts +244 -0
  135. package/lib/shared/paths/Path.test.ts +74 -0
  136. package/lib/shared/paths/Path.ts +23 -0
  137. package/lib/shared/paths/PathConverter.test.ts +232 -0
  138. package/lib/shared/paths/PathConverter.ts +141 -0
  139. package/lib/shared/paths/PathUtils.ts +290 -0
  140. package/lib/shared/paths/PathValidator.test.ts +193 -0
  141. package/lib/shared/paths/PathValidator.ts +53 -0
  142. package/lib/shared/paths/index.ts +48 -0
  143. package/lib/shared/propResolver.test.ts +639 -0
  144. package/lib/shared/propResolver.ts +124 -0
  145. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
  146. package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
  147. package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
  148. package/lib/shared/registry/ClientRegistry.test.ts +26 -0
  149. package/lib/shared/registry/ClientRegistry.ts +15 -0
  150. package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
  151. package/lib/shared/registry/ComponentRegistry.ts +100 -0
  152. package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
  153. package/lib/shared/registry/NodeTypeManager.ts +94 -0
  154. package/lib/shared/registry/RegistryManager.test.ts +58 -0
  155. package/lib/shared/registry/RegistryManager.ts +60 -0
  156. package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
  157. package/lib/shared/registry/SSRRegistry.test.ts +26 -0
  158. package/lib/shared/registry/SSRRegistry.ts +15 -0
  159. package/lib/shared/registry/createNodeType.ts +175 -0
  160. package/lib/shared/registry/defineNodeType.ts +73 -0
  161. package/lib/shared/registry/fieldPresets.ts +109 -0
  162. package/lib/shared/registry/index.ts +50 -0
  163. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
  164. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
  165. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
  166. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
  167. package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
  168. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
  169. package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
  170. package/lib/shared/registry/nodeTypes/index.ts +75 -0
  171. package/lib/shared/responsiveScaling.test.ts +268 -0
  172. package/lib/shared/responsiveScaling.ts +194 -0
  173. package/lib/shared/responsiveStyleUtils.test.ts +300 -0
  174. package/lib/shared/responsiveStyleUtils.ts +139 -0
  175. package/lib/shared/slugTranslator.test.ts +325 -0
  176. package/lib/shared/slugTranslator.ts +177 -0
  177. package/lib/shared/styleNodeUtils.test.ts +132 -0
  178. package/lib/shared/styleNodeUtils.ts +102 -0
  179. package/lib/shared/styleUtils.test.ts +238 -0
  180. package/lib/shared/styleUtils.ts +63 -0
  181. package/lib/shared/themeDefaults.test.ts +113 -0
  182. package/lib/shared/themeDefaults.ts +103 -0
  183. package/lib/shared/tree/PathBuilder.ts +383 -0
  184. package/lib/shared/treePathUtils.test.ts +539 -0
  185. package/lib/shared/treePathUtils.ts +339 -0
  186. package/lib/shared/types/api.ts +58 -0
  187. package/lib/shared/types/cms.ts +95 -0
  188. package/lib/shared/types/colors.ts +45 -0
  189. package/lib/shared/types/components.ts +121 -0
  190. package/lib/shared/types/errors.test.ts +103 -0
  191. package/lib/shared/types/errors.ts +69 -0
  192. package/lib/shared/types/index.ts +96 -0
  193. package/lib/shared/types/nodes.ts +20 -0
  194. package/lib/shared/types/rendering.ts +61 -0
  195. package/lib/shared/types/styles.ts +38 -0
  196. package/lib/shared/types.ts +11 -0
  197. package/lib/shared/utilityClassConfig.ts +287 -0
  198. package/lib/shared/utilityClassMapper.test.ts +140 -0
  199. package/lib/shared/utilityClassMapper.ts +229 -0
  200. package/lib/shared/utils/fileUtils.test.ts +99 -0
  201. package/lib/shared/utils/fileUtils.ts +56 -0
  202. package/lib/shared/utils.test.ts +261 -0
  203. package/lib/shared/utils.ts +84 -0
  204. package/lib/shared/validation/index.ts +7 -0
  205. package/lib/shared/validation/propValidator.test.ts +178 -0
  206. package/lib/shared/validation/propValidator.ts +238 -0
  207. package/lib/shared/validation/schemas.test.ts +177 -0
  208. package/lib/shared/validation/schemas.ts +401 -0
  209. package/lib/shared/validation/validators.test.ts +109 -0
  210. package/lib/shared/validation/validators.ts +304 -0
  211. package/lib/test-utils/dom-setup.ts +55 -0
  212. package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
  213. package/lib/test-utils/factories/DomMockFactory.ts +487 -0
  214. package/lib/test-utils/factories/EventMockFactory.ts +244 -0
  215. package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
  216. package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
  217. package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
  218. package/lib/test-utils/factories/index.ts +11 -0
  219. package/lib/test-utils/fixtures.ts +134 -0
  220. package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
  221. package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
  222. package/lib/test-utils/helpers/index.ts +6 -0
  223. package/lib/test-utils/helpers.test.ts +73 -0
  224. package/lib/test-utils/helpers.ts +90 -0
  225. package/lib/test-utils/index.ts +17 -0
  226. package/lib/test-utils/mockFactories.ts +92 -0
  227. package/lib/test-utils/mocks.ts +341 -0
  228. package/package.json +38 -0
  229. package/templates/index-router.html +34 -0
  230. package/tsconfig.json +14 -0
  231. 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
+