meno-core 1.0.19 → 1.0.21

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 (50) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/lib/client/core/ComponentBuilder.test.ts +68 -56
  3. package/lib/client/core/ComponentBuilder.ts +6 -4
  4. package/lib/client/core/builders/embedBuilder.ts +10 -1
  5. package/lib/client/core/builders/index.ts +6 -2
  6. package/lib/client/core/builders/{cmsListBuilder.ts → listBuilder.ts} +227 -95
  7. package/lib/client/responsiveStyleResolver.test.ts +12 -12
  8. package/lib/client/responsiveStyleResolver.ts +19 -7
  9. package/lib/client/routing/Router.tsx +35 -7
  10. package/lib/client/templateEngine.test.ts +126 -0
  11. package/lib/client/templateEngine.ts +53 -13
  12. package/lib/server/jsonLoader.test.ts +4 -1
  13. package/lib/server/jsonLoader.ts +64 -15
  14. package/lib/server/services/configService.ts +68 -13
  15. package/lib/server/ssr/attributeBuilder.ts +8 -0
  16. package/lib/server/ssr/index.ts +1 -1
  17. package/lib/server/ssr/ssrRenderer.ts +245 -111
  18. package/lib/server/ssrRenderer.test.ts +197 -3
  19. package/lib/server/validateStyleCoverage.ts +14 -17
  20. package/lib/shared/breakpoints.test.ts +210 -23
  21. package/lib/shared/breakpoints.ts +124 -17
  22. package/lib/shared/constants.test.ts +1 -1
  23. package/lib/shared/constants.ts +5 -1
  24. package/lib/shared/cssGeneration.test.ts +17 -0
  25. package/lib/shared/cssGeneration.ts +49 -12
  26. package/lib/shared/index.ts +3 -0
  27. package/lib/shared/itemTemplateUtils.test.ts +44 -2
  28. package/lib/shared/itemTemplateUtils.ts +15 -2
  29. package/lib/shared/nodeUtils.ts +23 -4
  30. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +2 -2
  31. package/lib/shared/registry/nodeTypes/ListNodeType.ts +186 -0
  32. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -0
  33. package/lib/shared/registry/nodeTypes/index.ts +6 -5
  34. package/lib/shared/responsiveScaling.test.ts +87 -0
  35. package/lib/shared/responsiveScaling.ts +33 -29
  36. package/lib/shared/responsiveStyleUtils.test.ts +7 -7
  37. package/lib/shared/responsiveStyleUtils.ts +22 -16
  38. package/lib/shared/styleNodeUtils.ts +5 -5
  39. package/lib/shared/styleValueRegistry.ts +60 -5
  40. package/lib/shared/tree/PathBuilder.ts +3 -3
  41. package/lib/shared/treePathUtils.ts +7 -5
  42. package/lib/shared/types/cms.ts +4 -57
  43. package/lib/shared/types/components.ts +45 -4
  44. package/lib/shared/types/index.ts +13 -0
  45. package/lib/shared/utilityClassConfig.ts +14 -0
  46. package/lib/shared/utilityClassMapper.ts +43 -2
  47. package/lib/shared/validation/propValidator.ts +9 -1
  48. package/lib/shared/validation/schemas.ts +60 -14
  49. package/package.json +1 -1
  50. package/lib/shared/registry/nodeTypes/CMSListNodeType.ts +0 -109
@@ -1214,8 +1214,9 @@ describe("SSR Renderer - CMSList rendering", () => {
1214
1214
 
1215
1215
  const pageData: JSONPage = {
1216
1216
  root: {
1217
- type: "cms-list",
1218
- collection: "blog-posts",
1217
+ type: "list",
1218
+ sourceType: "collection",
1219
+ source: "blog-posts",
1219
1220
  children: []
1220
1221
  } as unknown as ComponentNode
1221
1222
  };
@@ -1224,7 +1225,7 @@ describe("SSR Renderer - CMSList rendering", () => {
1224
1225
  const result = await renderPageSSR(pageData, {}, '/', '');
1225
1226
 
1226
1227
  expect(result.html).toBe('');
1227
- expect(consoleSpy).toHaveBeenCalledWith('CMSList requires CMS service');
1228
+ expect(consoleSpy).toHaveBeenCalledWith('List with sourceType "collection" requires CMS service');
1228
1229
 
1229
1230
  consoleSpy.mockRestore();
1230
1231
  });
@@ -1918,3 +1919,196 @@ describe("SSR Renderer - Interactive Styles", () => {
1918
1919
  });
1919
1920
  });
1920
1921
  });
1922
+
1923
+ describe("SSR Renderer - List node rendering", () => {
1924
+ test("should render children for each item in list prop", async () => {
1925
+ // Component with list prop definition
1926
+ const components: Record<string, ComponentDefinition> = {
1927
+ ItemsList: {
1928
+ component: {
1929
+ interface: {
1930
+ items: {
1931
+ type: 'list',
1932
+ itemSchema: {
1933
+ title: { type: 'string' }
1934
+ },
1935
+ default: []
1936
+ }
1937
+ },
1938
+ structure: {
1939
+ type: "list",
1940
+ source: "items",
1941
+ children: [
1942
+ { type: "node", tag: "div", children: ["{{item.title}}"] }
1943
+ ]
1944
+ } as any
1945
+ }
1946
+ }
1947
+ };
1948
+
1949
+ const pageData: JSONPage = {
1950
+ root: {
1951
+ type: "component",
1952
+ component: "ItemsList",
1953
+ props: {
1954
+ items: [
1955
+ { title: "First Item" },
1956
+ { title: "Second Item" },
1957
+ { title: "Third Item" }
1958
+ ]
1959
+ }
1960
+ } as any
1961
+ };
1962
+
1963
+ const result = await renderPageSSR(pageData, components, '/', '');
1964
+
1965
+ expect(result.html).toContain('First Item');
1966
+ expect(result.html).toContain('Second Item');
1967
+ expect(result.html).toContain('Third Item');
1968
+ });
1969
+
1970
+ test("should provide item context variables (itemIndex, itemFirst, itemLast)", async () => {
1971
+ const components: Record<string, ComponentDefinition> = {
1972
+ TagsList: {
1973
+ component: {
1974
+ interface: {
1975
+ tags: {
1976
+ type: 'list',
1977
+ itemSchema: {
1978
+ name: { type: 'string' }
1979
+ },
1980
+ default: []
1981
+ }
1982
+ },
1983
+ structure: {
1984
+ type: "list",
1985
+ source: "tags",
1986
+ itemAs: "tag",
1987
+ children: [
1988
+ { type: "node", tag: "span", attributes: { 'data-index': '{{tagIndex}}', 'data-first': '{{tagFirst}}', 'data-last': '{{tagLast}}' }, children: ["{{tag.name}}"] }
1989
+ ]
1990
+ } as any
1991
+ }
1992
+ }
1993
+ };
1994
+
1995
+ const pageData: JSONPage = {
1996
+ root: {
1997
+ type: "component",
1998
+ component: "TagsList",
1999
+ props: {
2000
+ tags: [
2001
+ { name: "Tag1" },
2002
+ { name: "Tag2" }
2003
+ ]
2004
+ }
2005
+ } as any
2006
+ };
2007
+
2008
+ const result = await renderPageSSR(pageData, components, '/', '');
2009
+
2010
+ expect(result.html).toContain('data-index="0"');
2011
+ expect(result.html).toContain('data-index="1"');
2012
+ expect(result.html).toContain('data-first="true"');
2013
+ expect(result.html).toContain('data-last="true"');
2014
+ expect(result.html).toContain('Tag1');
2015
+ expect(result.html).toContain('Tag2');
2016
+ });
2017
+
2018
+ test("should render empty when list prop is empty", async () => {
2019
+ const components: Record<string, ComponentDefinition> = {
2020
+ EmptyList: {
2021
+ component: {
2022
+ interface: {
2023
+ items: {
2024
+ type: 'list',
2025
+ itemSchema: {
2026
+ title: { type: 'string' }
2027
+ },
2028
+ default: []
2029
+ }
2030
+ },
2031
+ structure: {
2032
+ type: "list",
2033
+ source: "items",
2034
+ children: [
2035
+ { type: "node", tag: "div", children: ["{{item.title}}"] }
2036
+ ]
2037
+ } as any
2038
+ }
2039
+ }
2040
+ };
2041
+
2042
+ const pageData: JSONPage = {
2043
+ root: {
2044
+ type: "component",
2045
+ component: "EmptyList",
2046
+ props: {
2047
+ items: []
2048
+ }
2049
+ } as any
2050
+ };
2051
+
2052
+ const result = await renderPageSSR(pageData, components, '/', '');
2053
+
2054
+ // Should not render any list items
2055
+ expect(result.html).not.toContain('item.');
2056
+ });
2057
+
2058
+ test("should support nested lists", async () => {
2059
+ const components: Record<string, ComponentDefinition> = {
2060
+ CategoryList: {
2061
+ component: {
2062
+ interface: {
2063
+ categories: {
2064
+ type: 'list',
2065
+ itemSchema: {
2066
+ name: { type: 'string' }
2067
+ },
2068
+ default: []
2069
+ }
2070
+ },
2071
+ structure: {
2072
+ type: "list",
2073
+ source: "categories",
2074
+ itemAs: "category",
2075
+ children: [
2076
+ { type: "node", tag: "div", children: [
2077
+ "{{category.name}}",
2078
+ {
2079
+ type: "list",
2080
+ source: "{{category.items}}",
2081
+ itemAs: "subItem",
2082
+ children: [
2083
+ { type: "node", tag: "span", children: ["{{subItem.label}}"] }
2084
+ ]
2085
+ }
2086
+ ] }
2087
+ ]
2088
+ } as any
2089
+ }
2090
+ }
2091
+ };
2092
+
2093
+ const pageData: JSONPage = {
2094
+ root: {
2095
+ type: "component",
2096
+ component: "CategoryList",
2097
+ props: {
2098
+ categories: [
2099
+ { name: "Category A", items: [{ label: "Item A1" }, { label: "Item A2" }] },
2100
+ { name: "Category B", items: [{ label: "Item B1" }] }
2101
+ ]
2102
+ }
2103
+ } as any
2104
+ };
2105
+
2106
+ const result = await renderPageSSR(pageData, components, '/', '');
2107
+
2108
+ expect(result.html).toContain('Category A');
2109
+ expect(result.html).toContain('Category B');
2110
+ expect(result.html).toContain('Item A1');
2111
+ expect(result.html).toContain('Item A2');
2112
+ expect(result.html).toContain('Item B1');
2113
+ });
2114
+ });
@@ -18,6 +18,10 @@ const missingStylesMap = new Map<string, MissingStyleReport>();
18
18
 
19
19
  /**
20
20
  * Check if a style property and value can be converted to a utility class
21
+ *
22
+ * All CSS properties now generate utility classes:
23
+ * - Known properties (in propertyMap) use standard prefixes
24
+ * - Unknown properties generate dynamic classes with auto-prefixes
21
25
  */
22
26
  function canGenerateClass(property: string, value: unknown): boolean {
23
27
  // Skip internal component properties that shouldn't be styles
@@ -30,11 +34,6 @@ function canGenerateClass(property: string, value: unknown): boolean {
30
34
  return true; // Skip reporting these - they're not CSS styles
31
35
  }
32
36
 
33
- // Skip if property is not in our mapping
34
- if (!propertyMap[property]) {
35
- return false;
36
- }
37
-
38
37
  // Skip if value is null, undefined, or empty - these don't need utility classes
39
38
  if (value === null || value === undefined || value === '') {
40
39
  return true; // Skip reporting - nothing to convert
@@ -46,17 +45,15 @@ function canGenerateClass(property: string, value: unknown): boolean {
46
45
  return false;
47
46
  }
48
47
 
49
- // Ignore complex values that shouldn't be utility classes
50
- // But allow CSS var() functions since these can be converted to utility classes
48
+ // Object values cannot be converted to utility classes
51
49
  if (typeof value === 'object') {
52
50
  return false;
53
51
  }
54
52
 
55
- // All string values are converted by the mapper - it handles:
56
- // - CSS var() calls: "1px solid var(--border)" → "b-border"
57
- // - Space-separated values: "0 4px 12px rgba(...)" "bsh-0-4px-..."
58
- // - Functions: "blur(8px)" "flt-bl8"
59
- // - Everything else via fallback: any value → "prefix-value"
53
+ // All CSS properties (including unknown ones) now generate utility classes:
54
+ // - Known properties use standard prefixes from propertyMap
55
+ // - Unknown properties generate dynamic classes registered in the dynamic registry
56
+ // - CSS var() calls, space-separated values, and functions are all handled
60
57
  return true;
61
58
  }
62
59
 
@@ -115,10 +112,10 @@ export function printMissingStyleWarnings(verbose = false): void {
115
112
 
116
113
  const warnings: string[] = [];
117
114
  warnings.push(
118
- '\n⚠️ WARNING: Found styles without utility class mappings during build\n'
115
+ '\n⚠️ WARNING: Found styles that cannot be converted to utility classes\n'
119
116
  );
120
117
  warnings.push(
121
- 'These styles use complex values and remain as inline/component styles:\n'
118
+ 'These styles use object values which cannot be serialized to class names:\n'
122
119
  );
123
120
 
124
121
  // Sort by frequency
@@ -144,9 +141,9 @@ export function printMissingStyleWarnings(verbose = false): void {
144
141
  }
145
142
 
146
143
  warnings.push('\n💡 Note:');
147
- warnings.push(' • Simple functions (blur, translateY, scale, rotate, repeat) are converted automatically');
148
- warnings.push(' • Complex values (gradients, rgba, calc with expressions) remain as inline styles');
149
- warnings.push(' • These styles work correctly - this is just informational\n');
144
+ warnings.push(' • All string/number values are now converted to utility classes');
145
+ warnings.push(' • Object values (non-primitive) cannot be converted and will remain as-is');
146
+ warnings.push(' • This warning only appears for object-type style values\n');
150
147
 
151
148
  console.warn(warnings.join('\n'));
152
149
  }
@@ -1,6 +1,14 @@
1
1
  import { describe, test, expect } from 'bun:test';
2
- import { getAllBreakpointNames, getBreakpointName, DEFAULT_BREAKPOINTS } from './breakpoints';
3
- import type { BreakpointConfig } from './breakpoints';
2
+ import {
3
+ getAllBreakpointNames,
4
+ getBreakpointName,
5
+ DEFAULT_BREAKPOINTS,
6
+ normalizeBreakpointConfig,
7
+ getBreakpointValues,
8
+ getPreviewPointValues,
9
+ getBreakpointLabel,
10
+ } from './breakpoints';
11
+ import type { BreakpointConfig, BreakpointConfigInput } from './breakpoints';
4
12
 
5
13
  /**
6
14
  * breakpoints Tests
@@ -9,9 +17,118 @@ import type { BreakpointConfig } from './breakpoints';
9
17
 
10
18
  describe('breakpoints', () => {
11
19
  describe('DEFAULT_BREAKPOINTS', () => {
12
- test('should have tablet and mobile breakpoints', () => {
13
- expect(DEFAULT_BREAKPOINTS.tablet).toBe(1024);
14
- expect(DEFAULT_BREAKPOINTS.mobile).toBe(540);
20
+ test('should have tablet and mobile breakpoints with new format', () => {
21
+ expect(DEFAULT_BREAKPOINTS.tablet.breakpoint).toBe(1024);
22
+ expect(DEFAULT_BREAKPOINTS.tablet.previewPoint).toBe(768);
23
+ expect(DEFAULT_BREAKPOINTS.mobile.breakpoint).toBe(540);
24
+ expect(DEFAULT_BREAKPOINTS.mobile.previewPoint).toBe(375);
25
+ });
26
+ });
27
+
28
+ describe('normalizeBreakpointConfig', () => {
29
+ test('should convert legacy number format to object format', () => {
30
+ const input: BreakpointConfigInput = {
31
+ tablet: 1024,
32
+ mobile: 540,
33
+ };
34
+ const result = normalizeBreakpointConfig(input);
35
+
36
+ expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 1024 });
37
+ expect(result.mobile).toEqual({ breakpoint: 540, previewPoint: 540 });
38
+ });
39
+
40
+ test('should preserve new object format', () => {
41
+ const input: BreakpointConfigInput = {
42
+ tablet: { breakpoint: 1024, previewPoint: 768 },
43
+ mobile: { breakpoint: 540, previewPoint: 375 },
44
+ };
45
+ const result = normalizeBreakpointConfig(input);
46
+
47
+ expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 768 });
48
+ expect(result.mobile).toEqual({ breakpoint: 540, previewPoint: 375 });
49
+ });
50
+
51
+ test('should handle mixed format', () => {
52
+ const input: BreakpointConfigInput = {
53
+ tablet: { breakpoint: 1024, previewPoint: 768 },
54
+ mobile: 540, // legacy
55
+ };
56
+ const result = normalizeBreakpointConfig(input);
57
+
58
+ expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 768 });
59
+ expect(result.mobile).toEqual({ breakpoint: 540, previewPoint: 540 });
60
+ });
61
+
62
+ test('should default previewPoint to breakpoint if not specified', () => {
63
+ const input: BreakpointConfigInput = {
64
+ tablet: { breakpoint: 1024 } as any, // Missing previewPoint
65
+ };
66
+ const result = normalizeBreakpointConfig(input);
67
+
68
+ expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 1024 });
69
+ });
70
+
71
+ test('should handle empty input', () => {
72
+ const result = normalizeBreakpointConfig({});
73
+ expect(result).toEqual({});
74
+ });
75
+
76
+ test('should preserve label when present', () => {
77
+ const input: BreakpointConfigInput = {
78
+ tablet: { breakpoint: 1024, previewPoint: 768, label: 'Tablet Landscape' },
79
+ mobile: { breakpoint: 540, previewPoint: 375, label: 'Phone' },
80
+ };
81
+ const result = normalizeBreakpointConfig(input);
82
+
83
+ expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 768, label: 'Tablet Landscape' });
84
+ expect(result.mobile).toEqual({ breakpoint: 540, previewPoint: 375, label: 'Phone' });
85
+ });
86
+
87
+ test('should not include label when not present', () => {
88
+ const input: BreakpointConfigInput = {
89
+ tablet: { breakpoint: 1024, previewPoint: 768 },
90
+ };
91
+ const result = normalizeBreakpointConfig(input);
92
+
93
+ expect(result.tablet).toEqual({ breakpoint: 1024, previewPoint: 768 });
94
+ expect(result.tablet).not.toHaveProperty('label');
95
+ });
96
+
97
+ test('should handle mixed entries with and without labels', () => {
98
+ const input: BreakpointConfigInput = {
99
+ tabletLandscape: { breakpoint: 1024, previewPoint: 900, label: 'Tablet Landscape' },
100
+ tabletPortrait: { breakpoint: 768, previewPoint: 700 }, // no label
101
+ mobile: 540, // legacy format
102
+ };
103
+ const result = normalizeBreakpointConfig(input);
104
+
105
+ expect(result.tabletLandscape.label).toBe('Tablet Landscape');
106
+ expect(result.tabletPortrait).not.toHaveProperty('label');
107
+ expect(result.mobile).not.toHaveProperty('label');
108
+ });
109
+ });
110
+
111
+ describe('getBreakpointValues', () => {
112
+ test('should extract breakpoint values', () => {
113
+ const config: BreakpointConfig = {
114
+ tablet: { breakpoint: 1024, previewPoint: 768 },
115
+ mobile: { breakpoint: 540, previewPoint: 375 },
116
+ };
117
+ const result = getBreakpointValues(config);
118
+
119
+ expect(result).toEqual({ tablet: 1024, mobile: 540 });
120
+ });
121
+ });
122
+
123
+ describe('getPreviewPointValues', () => {
124
+ test('should extract preview point values', () => {
125
+ const config: BreakpointConfig = {
126
+ tablet: { breakpoint: 1024, previewPoint: 768 },
127
+ mobile: { breakpoint: 540, previewPoint: 375 },
128
+ };
129
+ const result = getPreviewPointValues(config);
130
+
131
+ expect(result).toEqual({ tablet: 768, mobile: 375 });
15
132
  });
16
133
  });
17
134
 
@@ -39,9 +156,9 @@ describe('breakpoints', () => {
39
156
 
40
157
  test('should handle custom breakpoints', () => {
41
158
  const customBreakpoints: BreakpointConfig = {
42
- large: 1440,
43
- medium: 960,
44
- small: 480,
159
+ large: { breakpoint: 1440, previewPoint: 1200 },
160
+ medium: { breakpoint: 960, previewPoint: 800 },
161
+ small: { breakpoint: 480, previewPoint: 375 },
45
162
  };
46
163
  const names = getAllBreakpointNames(customBreakpoints);
47
164
  expect(names).toEqual(['base', 'large', 'medium', 'small']);
@@ -49,11 +166,11 @@ describe('breakpoints', () => {
49
166
 
50
167
  test('should sort custom breakpoints correctly', () => {
51
168
  const customBreakpoints: BreakpointConfig = {
52
- xs: 320,
53
- xl: 1920,
54
- md: 768,
55
- sm: 480,
56
- lg: 1280,
169
+ xs: { breakpoint: 320, previewPoint: 320 },
170
+ xl: { breakpoint: 1920, previewPoint: 1920 },
171
+ md: { breakpoint: 768, previewPoint: 768 },
172
+ sm: { breakpoint: 480, previewPoint: 480 },
173
+ lg: { breakpoint: 1280, previewPoint: 1280 },
57
174
  };
58
175
  const names = getAllBreakpointNames(customBreakpoints);
59
176
  expect(names).toEqual(['base', 'xl', 'lg', 'md', 'sm', 'xs']);
@@ -65,15 +182,15 @@ describe('breakpoints', () => {
65
182
  });
66
183
 
67
184
  test('should handle single breakpoint', () => {
68
- const names = getAllBreakpointNames({ mobile: 640 });
185
+ const names = getAllBreakpointNames({ mobile: { breakpoint: 640, previewPoint: 375 } });
69
186
  expect(names).toEqual(['base', 'mobile']);
70
187
  });
71
188
 
72
189
  test('should handle breakpoints with same values', () => {
73
190
  const customBreakpoints: BreakpointConfig = {
74
- a: 1024,
75
- b: 1024,
76
- c: 540,
191
+ a: { breakpoint: 1024, previewPoint: 1024 },
192
+ b: { breakpoint: 1024, previewPoint: 768 },
193
+ c: { breakpoint: 540, previewPoint: 375 },
77
194
  };
78
195
  const names = getAllBreakpointNames(customBreakpoints);
79
196
  expect(names).toContain('base');
@@ -115,9 +232,9 @@ describe('breakpoints', () => {
115
232
 
116
233
  test('should work with custom breakpoints', () => {
117
234
  const customBreakpoints: BreakpointConfig = {
118
- large: 1440,
119
- medium: 960,
120
- small: 480,
235
+ large: { breakpoint: 1440, previewPoint: 1200 },
236
+ medium: { breakpoint: 960, previewPoint: 800 },
237
+ small: { breakpoint: 480, previewPoint: 375 },
121
238
  };
122
239
 
123
240
  expect(getBreakpointName(1920, customBreakpoints)).toBe('base');
@@ -150,9 +267,9 @@ describe('breakpoints', () => {
150
267
 
151
268
  test('should find correct breakpoint when multiple are similar', () => {
152
269
  const customBreakpoints: BreakpointConfig = {
153
- a: 1000,
154
- b: 999,
155
- c: 500,
270
+ a: { breakpoint: 1000, previewPoint: 1000 },
271
+ b: { breakpoint: 999, previewPoint: 999 },
272
+ c: { breakpoint: 500, previewPoint: 500 },
156
273
  };
157
274
 
158
275
  expect(getBreakpointName(1001, customBreakpoints)).toBe('base');
@@ -162,5 +279,75 @@ describe('breakpoints', () => {
162
279
  expect(getBreakpointName(500, customBreakpoints)).toBe('c');
163
280
  expect(getBreakpointName(499, customBreakpoints)).toBe('c');
164
281
  });
282
+
283
+ test('should use breakpoint value not previewPoint for determination', () => {
284
+ const customBreakpoints: BreakpointConfig = {
285
+ tablet: { breakpoint: 1024, previewPoint: 768 },
286
+ mobile: { breakpoint: 540, previewPoint: 375 },
287
+ };
288
+
289
+ // Should use breakpoint (1024) not previewPoint (768) for tablet threshold
290
+ expect(getBreakpointName(1024, customBreakpoints)).toBe('tablet');
291
+ expect(getBreakpointName(768, customBreakpoints)).toBe('tablet');
292
+ expect(getBreakpointName(1025, customBreakpoints)).toBe('base');
293
+
294
+ // Should use breakpoint (540) not previewPoint (375) for mobile threshold
295
+ expect(getBreakpointName(540, customBreakpoints)).toBe('mobile');
296
+ expect(getBreakpointName(375, customBreakpoints)).toBe('mobile');
297
+ expect(getBreakpointName(541, customBreakpoints)).toBe('tablet');
298
+ });
299
+ });
300
+
301
+ describe('getBreakpointLabel', () => {
302
+ test('should return "Desktop" for base', () => {
303
+ expect(getBreakpointLabel('base')).toBe('Desktop');
304
+ expect(getBreakpointLabel('base', DEFAULT_BREAKPOINTS)).toBe('Desktop');
305
+ });
306
+
307
+ test('should return custom label when set in config', () => {
308
+ const customBreakpoints: BreakpointConfig = {
309
+ tablet: { breakpoint: 1024, previewPoint: 768, label: 'Tablet Landscape' },
310
+ mobile: { breakpoint: 540, previewPoint: 375, label: 'Phone' },
311
+ };
312
+
313
+ expect(getBreakpointLabel('tablet', customBreakpoints)).toBe('Tablet Landscape');
314
+ expect(getBreakpointLabel('mobile', customBreakpoints)).toBe('Phone');
315
+ });
316
+
317
+ test('should auto-capitalize simple names when no label set', () => {
318
+ expect(getBreakpointLabel('tablet')).toBe('Tablet');
319
+ expect(getBreakpointLabel('mobile')).toBe('Mobile');
320
+ });
321
+
322
+ test('should convert camelCase to Title Case when no label set', () => {
323
+ const customBreakpoints: BreakpointConfig = {
324
+ tabletLandscape: { breakpoint: 1024, previewPoint: 900 },
325
+ tabletPortrait: { breakpoint: 768, previewPoint: 700 },
326
+ smallPhone: { breakpoint: 320, previewPoint: 320 },
327
+ };
328
+
329
+ expect(getBreakpointLabel('tabletLandscape', customBreakpoints)).toBe('Tablet Landscape');
330
+ expect(getBreakpointLabel('tabletPortrait', customBreakpoints)).toBe('Tablet Portrait');
331
+ expect(getBreakpointLabel('smallPhone', customBreakpoints)).toBe('Small Phone');
332
+ });
333
+
334
+ test('should handle breakpoint not in config', () => {
335
+ // If breakpoint doesn't exist in config, should still format the name
336
+ expect(getBreakpointLabel('unknownBreakpoint', DEFAULT_BREAKPOINTS)).toBe('Unknown Breakpoint');
337
+ });
338
+
339
+ test('should prefer label over auto-formatting', () => {
340
+ const customBreakpoints: BreakpointConfig = {
341
+ tabletLandscape: { breakpoint: 1024, previewPoint: 900, label: 'iPad Pro' },
342
+ };
343
+
344
+ // Should use the label, not auto-format "tabletLandscape"
345
+ expect(getBreakpointLabel('tabletLandscape', customBreakpoints)).toBe('iPad Pro');
346
+ });
347
+
348
+ test('should handle empty breakpoints config', () => {
349
+ expect(getBreakpointLabel('tablet', {})).toBe('Tablet');
350
+ expect(getBreakpointLabel('customBreakpoint', {})).toBe('Custom Breakpoint');
351
+ });
165
352
  });
166
353
  });