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.
- package/.claude/settings.local.json +7 -0
- package/lib/client/core/ComponentBuilder.test.ts +68 -56
- package/lib/client/core/ComponentBuilder.ts +6 -4
- package/lib/client/core/builders/embedBuilder.ts +10 -1
- package/lib/client/core/builders/index.ts +6 -2
- package/lib/client/core/builders/{cmsListBuilder.ts → listBuilder.ts} +227 -95
- package/lib/client/responsiveStyleResolver.test.ts +12 -12
- package/lib/client/responsiveStyleResolver.ts +19 -7
- package/lib/client/routing/Router.tsx +35 -7
- package/lib/client/templateEngine.test.ts +126 -0
- package/lib/client/templateEngine.ts +53 -13
- package/lib/server/jsonLoader.test.ts +4 -1
- package/lib/server/jsonLoader.ts +64 -15
- package/lib/server/services/configService.ts +68 -13
- package/lib/server/ssr/attributeBuilder.ts +8 -0
- package/lib/server/ssr/index.ts +1 -1
- package/lib/server/ssr/ssrRenderer.ts +245 -111
- package/lib/server/ssrRenderer.test.ts +197 -3
- package/lib/server/validateStyleCoverage.ts +14 -17
- package/lib/shared/breakpoints.test.ts +210 -23
- package/lib/shared/breakpoints.ts +124 -17
- package/lib/shared/constants.test.ts +1 -1
- package/lib/shared/constants.ts +5 -1
- package/lib/shared/cssGeneration.test.ts +17 -0
- package/lib/shared/cssGeneration.ts +49 -12
- package/lib/shared/index.ts +3 -0
- package/lib/shared/itemTemplateUtils.test.ts +44 -2
- package/lib/shared/itemTemplateUtils.ts +15 -2
- package/lib/shared/nodeUtils.ts +23 -4
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +2 -2
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +186 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -0
- package/lib/shared/registry/nodeTypes/index.ts +6 -5
- package/lib/shared/responsiveScaling.test.ts +87 -0
- package/lib/shared/responsiveScaling.ts +33 -29
- package/lib/shared/responsiveStyleUtils.test.ts +7 -7
- package/lib/shared/responsiveStyleUtils.ts +22 -16
- package/lib/shared/styleNodeUtils.ts +5 -5
- package/lib/shared/styleValueRegistry.ts +60 -5
- package/lib/shared/tree/PathBuilder.ts +3 -3
- package/lib/shared/treePathUtils.ts +7 -5
- package/lib/shared/types/cms.ts +4 -57
- package/lib/shared/types/components.ts +45 -4
- package/lib/shared/types/index.ts +13 -0
- package/lib/shared/utilityClassConfig.ts +14 -0
- package/lib/shared/utilityClassMapper.ts +43 -2
- package/lib/shared/validation/propValidator.ts +9 -1
- package/lib/shared/validation/schemas.ts +60 -14
- package/package.json +1 -1
- package/lib/shared/registry/nodeTypes/CMSListNodeType.ts +0 -109
|
@@ -4,17 +4,94 @@
|
|
|
4
4
|
* Supports dynamic breakpoints from project.config.json
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Extended breakpoint entry with separate CSS threshold and editor preview width
|
|
9
|
+
* - breakpoint: The CSS media query threshold (max-width value)
|
|
10
|
+
* - previewPoint: The width used in editor preview mode (defaults to breakpoint if not specified)
|
|
11
|
+
* - label: Optional display label for UI (e.g., "Tablet Landscape", "Phone")
|
|
12
|
+
*/
|
|
13
|
+
export interface BreakpointEntry {
|
|
14
|
+
breakpoint: number;
|
|
15
|
+
previewPoint: number;
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Input format for breakpoint config - supports both legacy (number) and new (object) format
|
|
21
|
+
* Legacy: { tablet: 1024 }
|
|
22
|
+
* New: { tablet: { breakpoint: 1024, previewPoint: 768 } }
|
|
23
|
+
*/
|
|
24
|
+
export type BreakpointConfigInput = Record<string, number | BreakpointEntry>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalized breakpoint config - always uses object format
|
|
28
|
+
*/
|
|
29
|
+
export type BreakpointConfig = Record<string, BreakpointEntry>;
|
|
11
30
|
|
|
12
|
-
|
|
13
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Legacy format for backward compatibility (simple number values)
|
|
33
|
+
*/
|
|
34
|
+
export type LegacyBreakpointConfig = Record<string, number>;
|
|
14
35
|
|
|
15
36
|
// BreakpointName is now a string to support dynamic breakpoints
|
|
16
37
|
export type BreakpointName = string;
|
|
17
38
|
|
|
39
|
+
export const DEFAULT_BREAKPOINTS: BreakpointConfig = {
|
|
40
|
+
tablet: { breakpoint: 1024, previewPoint: 768 },
|
|
41
|
+
mobile: { breakpoint: 540, previewPoint: 375 },
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Normalize a breakpoint config input to the full object format
|
|
46
|
+
* Converts legacy number format to object format
|
|
47
|
+
* If previewPoint is not specified, defaults to breakpoint value
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeBreakpointConfig(
|
|
50
|
+
input: BreakpointConfigInput | LegacyBreakpointConfig
|
|
51
|
+
): BreakpointConfig {
|
|
52
|
+
const result: BreakpointConfig = {};
|
|
53
|
+
|
|
54
|
+
for (const [name, value] of Object.entries(input)) {
|
|
55
|
+
if (typeof value === 'number') {
|
|
56
|
+
// Legacy format: number -> { breakpoint: value, previewPoint: value }
|
|
57
|
+
result[name] = { breakpoint: value, previewPoint: value };
|
|
58
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
59
|
+
// New format: ensure both values exist, preserve label if present
|
|
60
|
+
result[name] = {
|
|
61
|
+
breakpoint: value.breakpoint,
|
|
62
|
+
previewPoint: value.previewPoint ?? value.breakpoint,
|
|
63
|
+
...(value.label !== undefined && { label: value.label }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get just the breakpoint values for CSS generation (media queries)
|
|
73
|
+
* Returns Record<string, number> with breakpoint values
|
|
74
|
+
*/
|
|
75
|
+
export function getBreakpointValues(config: BreakpointConfig): LegacyBreakpointConfig {
|
|
76
|
+
const result: LegacyBreakpointConfig = {};
|
|
77
|
+
for (const [name, entry] of Object.entries(config)) {
|
|
78
|
+
result[name] = entry.breakpoint;
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get just the preview point values for editor preview
|
|
85
|
+
* Returns Record<string, number> with previewPoint values
|
|
86
|
+
*/
|
|
87
|
+
export function getPreviewPointValues(config: BreakpointConfig): LegacyBreakpointConfig {
|
|
88
|
+
const result: LegacyBreakpointConfig = {};
|
|
89
|
+
for (const [name, entry] of Object.entries(config)) {
|
|
90
|
+
result[name] = entry.previewPoint;
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
18
95
|
/**
|
|
19
96
|
* Get all breakpoint names from the breakpoint configuration
|
|
20
97
|
* Always includes 'base' plus all keys from the config
|
|
@@ -25,17 +102,17 @@ export function getAllBreakpointNames(
|
|
|
25
102
|
): BreakpointName[] {
|
|
26
103
|
// Base is always included first
|
|
27
104
|
const names: BreakpointName[] = ['base'];
|
|
28
|
-
|
|
29
|
-
// Get all breakpoint names from config and sort by value (descending)
|
|
105
|
+
|
|
106
|
+
// Get all breakpoint names from config and sort by breakpoint value (descending)
|
|
30
107
|
// This ensures proper order: largest viewport first
|
|
31
108
|
const breakpointEntries = Object.entries(breakpoints);
|
|
32
|
-
breakpointEntries.sort((a, b) => b[1] - a[1]); // Sort descending by value
|
|
33
|
-
|
|
109
|
+
breakpointEntries.sort((a, b) => b[1].breakpoint - a[1].breakpoint); // Sort descending by breakpoint value
|
|
110
|
+
|
|
34
111
|
// Add breakpoint names in sorted order
|
|
35
112
|
for (const [name] of breakpointEntries) {
|
|
36
113
|
names.push(name);
|
|
37
114
|
}
|
|
38
|
-
|
|
115
|
+
|
|
39
116
|
return names;
|
|
40
117
|
}
|
|
41
118
|
|
|
@@ -43,23 +120,53 @@ export function getAllBreakpointNames(
|
|
|
43
120
|
* Get active breakpoint name based on viewport width
|
|
44
121
|
* Returns the smallest breakpoint that the viewport width is less than or equal to
|
|
45
122
|
* If viewport is larger than all breakpoints, returns 'base'
|
|
123
|
+
* Uses .breakpoint value for comparison (CSS threshold)
|
|
46
124
|
*/
|
|
47
125
|
export function getBreakpointName(
|
|
48
126
|
viewportWidth: number,
|
|
49
127
|
breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS
|
|
50
128
|
): BreakpointName {
|
|
51
|
-
// Sort breakpoints by value (ascending) to find the smallest one that matches
|
|
129
|
+
// Sort breakpoints by breakpoint value (ascending) to find the smallest one that matches
|
|
52
130
|
const breakpointEntries = Object.entries(breakpoints);
|
|
53
|
-
breakpointEntries.sort((a, b) => a[1] - b[1]); // Sort ascending by value
|
|
54
|
-
|
|
131
|
+
breakpointEntries.sort((a, b) => a[1].breakpoint - b[1].breakpoint); // Sort ascending by breakpoint value
|
|
132
|
+
|
|
55
133
|
// Find the smallest breakpoint that viewport width is <= to
|
|
56
|
-
for (const [name,
|
|
57
|
-
if (viewportWidth <=
|
|
134
|
+
for (const [name, entry] of breakpointEntries) {
|
|
135
|
+
if (viewportWidth <= entry.breakpoint) {
|
|
58
136
|
return name;
|
|
59
137
|
}
|
|
60
138
|
}
|
|
61
|
-
|
|
139
|
+
|
|
62
140
|
// If viewport is larger than all breakpoints, return 'base'
|
|
63
141
|
return 'base';
|
|
64
142
|
}
|
|
65
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Get display label for a breakpoint
|
|
146
|
+
* Priority:
|
|
147
|
+
* 1. Label from config (if set)
|
|
148
|
+
* 2. "Desktop" for 'base'
|
|
149
|
+
* 3. Auto-capitalized name (camelCase -> Title Case)
|
|
150
|
+
*/
|
|
151
|
+
export function getBreakpointLabel(
|
|
152
|
+
name: BreakpointName,
|
|
153
|
+
breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS
|
|
154
|
+
): string {
|
|
155
|
+
// Base always returns "Desktop"
|
|
156
|
+
if (name === 'base') {
|
|
157
|
+
return 'Desktop';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check for custom label in config
|
|
161
|
+
const entry = breakpoints[name];
|
|
162
|
+
if (entry?.label) {
|
|
163
|
+
return entry.label;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Auto-format: camelCase -> Title Case
|
|
167
|
+
// e.g., "tabletLandscape" -> "Tablet Landscape"
|
|
168
|
+
return name
|
|
169
|
+
.replace(/([A-Z])/g, ' $1')
|
|
170
|
+
.replace(/^./, (str) => str.toUpperCase())
|
|
171
|
+
.trim();
|
|
172
|
+
}
|
|
@@ -130,7 +130,7 @@ describe('constants', () => {
|
|
|
130
130
|
expect(NODE_TYPE.EMBED).toBe('embed');
|
|
131
131
|
expect(NODE_TYPE.LINK).toBe('link');
|
|
132
132
|
expect(NODE_TYPE.LOCALE_LIST).toBe('locale-list');
|
|
133
|
-
expect(NODE_TYPE.
|
|
133
|
+
expect(NODE_TYPE.LIST).toBe('list');
|
|
134
134
|
});
|
|
135
135
|
});
|
|
136
136
|
|
package/lib/shared/constants.ts
CHANGED
|
@@ -35,6 +35,8 @@ export const API_ROUTES = {
|
|
|
35
35
|
SAVE_COLORS: '/api/save-colors', // Save colors config
|
|
36
36
|
// Page deletion
|
|
37
37
|
DELETE_PAGE: '/api/delete-page', // Delete a page
|
|
38
|
+
// Component preview
|
|
39
|
+
COMPONENT_PREVIEW: '/api/component-preview', // Render component preview HTML
|
|
38
40
|
} as const;
|
|
39
41
|
|
|
40
42
|
export const HMR_ROUTE = '/hmr';
|
|
@@ -92,6 +94,8 @@ export const IFRAME_MESSAGE_TYPES = {
|
|
|
92
94
|
REDO_REQUEST: 'REDO_REQUEST',
|
|
93
95
|
TOGGLE_INTERACTIVITY_EDITOR: 'TOGGLE_INTERACTIVITY_EDITOR',
|
|
94
96
|
SET_BREAKPOINT: 'SET_BREAKPOINT',
|
|
97
|
+
PAGE_DATA_PREVIEW: 'PAGE_DATA_PREVIEW', // Editor → Iframe for component hover preview
|
|
98
|
+
PAGE_DATA_PREVIEW_REVERT: 'PAGE_DATA_PREVIEW_REVERT', // Editor → Iframe to revert preview
|
|
95
99
|
} as const;
|
|
96
100
|
|
|
97
101
|
// Component node type constants
|
|
@@ -102,7 +106,7 @@ export const NODE_TYPE = {
|
|
|
102
106
|
EMBED: 'embed',
|
|
103
107
|
LINK: 'link',
|
|
104
108
|
LOCALE_LIST: 'locale-list',
|
|
105
|
-
|
|
109
|
+
LIST: 'list',
|
|
106
110
|
} as const;
|
|
107
111
|
|
|
108
112
|
export type NodeType = typeof NODE_TYPE[keyof typeof NODE_TYPE];
|
|
@@ -40,6 +40,14 @@ describe('extractUtilityClassesFromHTML', () => {
|
|
|
40
40
|
const classes = extractUtilityClassesFromHTML(html);
|
|
41
41
|
expect(classes.has('bgi-linear-gradient(#0000,-#0f1442-94%)')).toBe(true);
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
test('extracts l- (left) classes correctly without treating as responsive prefix', () => {
|
|
45
|
+
const html = '<div class="l-10px l-auto l-0"></div>';
|
|
46
|
+
const classes = extractUtilityClassesFromHTML(html);
|
|
47
|
+
expect(classes.has('l-10px')).toBe(true);
|
|
48
|
+
expect(classes.has('l-auto')).toBe(true);
|
|
49
|
+
expect(classes.has('l-0')).toBe(true);
|
|
50
|
+
});
|
|
43
51
|
});
|
|
44
52
|
|
|
45
53
|
describe('generateUtilityCSS end-to-end', () => {
|
|
@@ -56,6 +64,15 @@ describe('generateUtilityCSS end-to-end', () => {
|
|
|
56
64
|
const css = generateUtilityCSS(classes);
|
|
57
65
|
expect(css).toContain('background-image: linear-gradient(#000, #fff)');
|
|
58
66
|
});
|
|
67
|
+
|
|
68
|
+
test('generates CSS for l- (left) classes from HTML', () => {
|
|
69
|
+
const html = '<div class="l-10px l-auto l-0"></div>';
|
|
70
|
+
const classes = extractUtilityClassesFromHTML(html);
|
|
71
|
+
const css = generateUtilityCSS(classes);
|
|
72
|
+
expect(css).toContain('left: 10px');
|
|
73
|
+
expect(css).toContain('left: auto');
|
|
74
|
+
expect(css).toContain('left: 0');
|
|
75
|
+
});
|
|
59
76
|
});
|
|
60
77
|
|
|
61
78
|
describe('cssGeneration', () => {
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { prefixToCSSProperty, propertyMap } from './utilityClassConfig';
|
|
8
|
-
import { getStyleValue } from './styleValueRegistry';
|
|
9
|
-
import type { BreakpointConfig } from './breakpoints';
|
|
10
|
-
import { DEFAULT_BREAKPOINTS } from './breakpoints';
|
|
8
|
+
import { getStyleValue, getDynamicStyle, isDynamicClass } from './styleValueRegistry';
|
|
9
|
+
import type { BreakpointConfig, LegacyBreakpointConfig } from './breakpoints';
|
|
10
|
+
import { DEFAULT_BREAKPOINTS, getBreakpointValues } from './breakpoints';
|
|
11
11
|
import type { ResponsiveScales } from './responsiveScaling';
|
|
12
12
|
import { scalePropertyValue } from './responsiveScaling';
|
|
13
13
|
import type { InteractiveStyles, StyleObject, ResponsiveStyleObject, StyleValue } from './types/styles';
|
|
@@ -67,6 +67,13 @@ const utilityClassRules: Record<string, string> = {
|
|
|
67
67
|
'us-text': 'user-select: text;',
|
|
68
68
|
'us-all': 'user-select: all;',
|
|
69
69
|
|
|
70
|
+
// White space
|
|
71
|
+
'whs-normal': 'white-space: normal;',
|
|
72
|
+
'whs-nowrap': 'white-space: nowrap;',
|
|
73
|
+
'whs-pre': 'white-space: pre;',
|
|
74
|
+
'whs-pre-wrap': 'white-space: pre-wrap;',
|
|
75
|
+
'whs-pre-line': 'white-space: pre-line;',
|
|
76
|
+
|
|
70
77
|
// Shadow presets
|
|
71
78
|
'sh-0': 'box-shadow: none;',
|
|
72
79
|
'sh-1': 'box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);',
|
|
@@ -132,11 +139,25 @@ export function generateRuleForClass(className: string): string | null {
|
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
if (!prefix || !classValue)
|
|
142
|
+
if (!prefix || !classValue) {
|
|
143
|
+
// Check dynamic registry for classes with unknown prefixes
|
|
144
|
+
const dynamicStyle = getDynamicStyle(className);
|
|
145
|
+
if (dynamicStyle) {
|
|
146
|
+
return `${dynamicStyle.property}: ${dynamicStyle.value};`;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
136
150
|
|
|
137
151
|
// Look up the CSS property from prefix
|
|
138
152
|
const cssProp = prefixToCSSProperty[prefix];
|
|
139
|
-
if (!cssProp)
|
|
153
|
+
if (!cssProp) {
|
|
154
|
+
// Check dynamic registry for classes with unknown prefixes
|
|
155
|
+
const dynamicStyle = getDynamicStyle(className);
|
|
156
|
+
if (dynamicStyle) {
|
|
157
|
+
return `${dynamicStyle.property}: ${dynamicStyle.value};`;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
140
161
|
|
|
141
162
|
// Handle border-side classes (bt-, bb-, bl-, border-r-) with special syntax FIRST
|
|
142
163
|
// Generate ONLY width and style, NOT color - allows bc- to control color independently
|
|
@@ -314,13 +335,16 @@ export function generateUtilityCSS(
|
|
|
314
335
|
const baseClasses = new Set<string>();
|
|
315
336
|
const autoResponsiveClasses = new Set<string>(); // Classes that should get auto-scaling
|
|
316
337
|
|
|
338
|
+
// Extract breakpoint values for CSS media queries
|
|
339
|
+
const breakpointValues = getBreakpointValues(breakpoints);
|
|
340
|
+
|
|
317
341
|
// Create a map for responsive breakpoint classes
|
|
318
342
|
// Map from prefix (e.g., 't', 'm') to the class name and breakpoint info
|
|
319
343
|
type BreakpointClassMap = Record<string, { classes: Set<string>; breakpointName: string; value: number }>;
|
|
320
344
|
const responsiveClasses: BreakpointClassMap = {};
|
|
321
345
|
|
|
322
346
|
// Initialize responsive class sets for each breakpoint
|
|
323
|
-
for (const [breakpointName, breakpointValue] of Object.entries(
|
|
347
|
+
for (const [breakpointName, breakpointValue] of Object.entries(breakpointValues)) {
|
|
324
348
|
// Generate prefix from breakpoint name, avoiding conflicts with property prefixes
|
|
325
349
|
// For 'mobile', use 'mob' to avoid conflict with 'margin' (m-), etc.
|
|
326
350
|
let prefix = breakpointName.charAt(0).toLowerCase();
|
|
@@ -436,9 +460,10 @@ export function generateUtilityCSS(
|
|
|
436
460
|
const escapedClassName = escapeCSSClassName(className);
|
|
437
461
|
|
|
438
462
|
// Generate scaled rules for each breakpoint
|
|
439
|
-
for (const [breakpointName, breakpointValue] of Object.entries(
|
|
440
|
-
|
|
441
|
-
|
|
463
|
+
for (const [breakpointName, breakpointValue] of Object.entries(breakpointValues)) {
|
|
464
|
+
// Use the actual breakpoint name to look up the scale
|
|
465
|
+
// This allows custom breakpoints like 'small', 'large', etc. to have their own scales
|
|
466
|
+
const scale = scaleConfig[breakpointName];
|
|
442
467
|
if (!scale) continue;
|
|
443
468
|
|
|
444
469
|
const scaledValue = scalePropertyValue(propValue.value, baseRef, scale);
|
|
@@ -530,6 +555,8 @@ export function extractUtilityClassesFromHTML(html: string): Set<string> {
|
|
|
530
555
|
'o-h', 'o-a', 'o-s', 'o-v',
|
|
531
556
|
// Cursor
|
|
532
557
|
'cursor-pointer', 'cursor-default',
|
|
558
|
+
// White space
|
|
559
|
+
'whs-normal', 'whs-nowrap', 'whs-pre', 'whs-pre-wrap', 'whs-pre-line',
|
|
533
560
|
]);
|
|
534
561
|
|
|
535
562
|
while ((match = classRegex.exec(html)) !== null) {
|
|
@@ -560,9 +587,10 @@ export function extractUtilityClassesFromHTML(html: string): Set<string> {
|
|
|
560
587
|
else if (className.length > 2 && className.charAt(1) === '-' && className.match(/^[a-z]-/)) {
|
|
561
588
|
const firstChar = className.charAt(0);
|
|
562
589
|
// Only treat as responsive prefix if it looks like a breakpoint indicator
|
|
563
|
-
// Common breakpoint prefixes: t (tablet), s (small),
|
|
590
|
+
// Common breakpoint prefixes: t (tablet), s (small), x (extra), u (ultra)
|
|
564
591
|
// NOTE: We exclude 'm' because it conflicts with margin prefix - use 'mob' instead
|
|
565
|
-
|
|
592
|
+
// NOTE: We exclude 'l' because it conflicts with left property prefix - use 'lg' instead
|
|
593
|
+
if (['t', 's', 'x', 'u'].includes(firstChar)) {
|
|
566
594
|
classToCheck = className.substring(2); // Remove responsive prefix
|
|
567
595
|
hasResponsivePrefix = true;
|
|
568
596
|
}
|
|
@@ -594,6 +622,12 @@ export function extractUtilityClassesFromHTML(html: string): Set<string> {
|
|
|
594
622
|
}
|
|
595
623
|
}
|
|
596
624
|
}
|
|
625
|
+
|
|
626
|
+
// Check if it's a dynamic class (registered in the dynamic registry)
|
|
627
|
+
// This handles classes for properties not in propertyMap
|
|
628
|
+
if (!classes.has(className) && isDynamicClass(className)) {
|
|
629
|
+
classes.add(className);
|
|
630
|
+
}
|
|
597
631
|
}
|
|
598
632
|
}
|
|
599
633
|
|
|
@@ -673,6 +707,9 @@ export function generateInteractiveCSS(
|
|
|
673
707
|
): string {
|
|
674
708
|
const css: string[] = [];
|
|
675
709
|
|
|
710
|
+
// Extract breakpoint values for CSS media queries
|
|
711
|
+
const breakpointValues = getBreakpointValues(breakpoints);
|
|
712
|
+
|
|
676
713
|
for (const rule of interactiveStyles) {
|
|
677
714
|
const { prefix, postfix, style } = rule;
|
|
678
715
|
|
|
@@ -692,7 +729,7 @@ export function generateInteractiveCSS(
|
|
|
692
729
|
}
|
|
693
730
|
|
|
694
731
|
// Breakpoint styles (sorted by value descending)
|
|
695
|
-
const sortedBreakpoints = Object.entries(
|
|
732
|
+
const sortedBreakpoints = Object.entries(breakpointValues).sort(
|
|
696
733
|
([, a], [, b]) => b - a
|
|
697
734
|
);
|
|
698
735
|
|
package/lib/shared/index.ts
CHANGED
|
@@ -298,10 +298,52 @@ describe('itemTemplateUtils', () => {
|
|
|
298
298
|
});
|
|
299
299
|
});
|
|
300
300
|
|
|
301
|
-
it('should
|
|
301
|
+
it('should process arrays with template strings', () => {
|
|
302
302
|
const props = { items: ['{{item.title}}', '{{item.url}}'] };
|
|
303
303
|
const result = processItemPropsTemplate(props, baseContext);
|
|
304
|
-
expect(result).toEqual({ items: ['
|
|
304
|
+
expect(result).toEqual({ items: ['Test Title', '/test-url'] });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should process nested arrays', () => {
|
|
308
|
+
const props = {
|
|
309
|
+
outer: [
|
|
310
|
+
['{{item.title}}', 'static'],
|
|
311
|
+
['{{item.url}}'],
|
|
312
|
+
]
|
|
313
|
+
};
|
|
314
|
+
const result = processItemPropsTemplate(props, baseContext);
|
|
315
|
+
expect(result).toEqual({
|
|
316
|
+
outer: [
|
|
317
|
+
['Test Title', 'static'],
|
|
318
|
+
['/test-url'],
|
|
319
|
+
]
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should process arrays containing objects with templates', () => {
|
|
324
|
+
const props = {
|
|
325
|
+
items: [
|
|
326
|
+
{ label: '{{item.title}}', href: '{{item.url}}' },
|
|
327
|
+
{ label: 'Static', href: '/static' },
|
|
328
|
+
]
|
|
329
|
+
};
|
|
330
|
+
const result = processItemPropsTemplate(props, baseContext);
|
|
331
|
+
expect(result).toEqual({
|
|
332
|
+
items: [
|
|
333
|
+
{ label: 'Test Title', href: '/test-url' },
|
|
334
|
+
{ label: 'Static', href: '/static' },
|
|
335
|
+
]
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should handle mixed array content', () => {
|
|
340
|
+
const props = {
|
|
341
|
+
tags: ['{{item.title}}', 42, true, null, { nested: '{{item.url}}' }]
|
|
342
|
+
};
|
|
343
|
+
const result = processItemPropsTemplate(props, baseContext);
|
|
344
|
+
expect(result).toEqual({
|
|
345
|
+
tags: ['Test Title', 42, true, null, { nested: '/test-url' }]
|
|
346
|
+
});
|
|
305
347
|
});
|
|
306
348
|
|
|
307
349
|
it('should handle mixed content', () => {
|
|
@@ -217,8 +217,21 @@ export function processItemPropsTemplate(
|
|
|
217
217
|
for (const [key, value] of Object.entries(props)) {
|
|
218
218
|
if (typeof value === 'string' && hasItemTemplates(value)) {
|
|
219
219
|
result[key] = processItemTemplate(value, ctx, resolveValue);
|
|
220
|
-
} else if (
|
|
221
|
-
//
|
|
220
|
+
} else if (Array.isArray(value)) {
|
|
221
|
+
// Process arrays - recursively handle each element
|
|
222
|
+
result[key] = value.map(item => {
|
|
223
|
+
if (typeof item === 'string' && hasItemTemplates(item)) {
|
|
224
|
+
return processItemTemplate(item, ctx, resolveValue);
|
|
225
|
+
} else if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
226
|
+
return processItemPropsTemplate(item as Record<string, unknown>, ctx, resolveValue);
|
|
227
|
+
} else if (Array.isArray(item)) {
|
|
228
|
+
// Handle nested arrays recursively
|
|
229
|
+
return processItemPropsTemplate({ _arr: item }, ctx, resolveValue)._arr;
|
|
230
|
+
}
|
|
231
|
+
return item;
|
|
232
|
+
});
|
|
233
|
+
} else if (value && typeof value === 'object') {
|
|
234
|
+
// Recursively process nested objects (not arrays or null)
|
|
222
235
|
result[key] = processItemPropsTemplate(value as Record<string, unknown>, ctx, resolveValue);
|
|
223
236
|
} else {
|
|
224
237
|
result[key] = value;
|
package/lib/shared/nodeUtils.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* Provides helper functions for working with ComponentNode types
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { ComponentNode, ComponentInstanceNode, HtmlNode, SlotMarker, EmbedNode, LinkNode, LocaleListNode,
|
|
6
|
+
import type { ComponentNode, ComponentInstanceNode, HtmlNode, SlotMarker, EmbedNode, LinkNode, LocaleListNode, ComponentDefinition, StructuredComponentDefinition } from './types';
|
|
7
|
+
import type { ListNode } from './registry/nodeTypes/ListNodeType';
|
|
8
|
+
|
|
9
|
+
// For backward compatibility during migration
|
|
10
|
+
type CMSListNode = ListNode;
|
|
7
11
|
import { NODE_TYPE } from './constants';
|
|
8
12
|
|
|
9
13
|
/**
|
|
@@ -122,9 +126,24 @@ export function isLocaleListNode(node: ComponentNode | null | undefined): node i
|
|
|
122
126
|
|
|
123
127
|
/**
|
|
124
128
|
* Type guard to check if a node is a CMS list node
|
|
129
|
+
* @deprecated Use isListNode() and check sourceType === 'collection' instead.
|
|
130
|
+
* Kept for backward compatibility during migration.
|
|
131
|
+
*/
|
|
132
|
+
export function isCMSListNode(node: unknown): node is ListNode {
|
|
133
|
+
if (node === null || typeof node !== 'object') return false;
|
|
134
|
+
const n = node as Record<string, unknown>;
|
|
135
|
+
// Support both old cms-list type and new list with sourceType: 'collection'
|
|
136
|
+
return n.type === 'cms-list' || (n.type === NODE_TYPE.LIST && n.sourceType === 'collection');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Type guard to check if a node is a list node (unified type)
|
|
125
141
|
*/
|
|
126
|
-
export function
|
|
127
|
-
|
|
142
|
+
export function isListNode(node: unknown): node is ListNode {
|
|
143
|
+
if (node === null || typeof node !== 'object') return false;
|
|
144
|
+
const n = node as Record<string, unknown>;
|
|
145
|
+
// Support both old cms-list type (for migration) and new list type
|
|
146
|
+
return n.type === NODE_TYPE.LIST || n.type === 'cms-list';
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
/**
|
|
@@ -133,7 +152,7 @@ export function isCMSListNode(node: unknown): node is CMSListNode {
|
|
|
133
152
|
export function isValidNodeType(type: string): type is typeof NODE_TYPE[keyof typeof NODE_TYPE] {
|
|
134
153
|
return type === NODE_TYPE.NODE || type === NODE_TYPE.COMPONENT || type === NODE_TYPE.SLOT ||
|
|
135
154
|
type === NODE_TYPE.EMBED || type === NODE_TYPE.LINK || type === NODE_TYPE.LOCALE_LIST ||
|
|
136
|
-
type === NODE_TYPE.
|
|
155
|
+
type === NODE_TYPE.LIST || type === 'cms-list'; // 'cms-list' supported for migration
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
/**
|
|
@@ -42,7 +42,7 @@ describe('BaseNodeTypeRegistry', () => {
|
|
|
42
42
|
describe('registerAll', () => {
|
|
43
43
|
it('should register multiple node type definitions', () => {
|
|
44
44
|
registry.registerAll(builtInNodeTypes);
|
|
45
|
-
expect(registry.size).toBe(7);
|
|
45
|
+
expect(registry.size).toBe(7); // 7 node types after cms-list merged into list
|
|
46
46
|
expect(registry.has(NODE_TYPE.NODE)).toBe(true);
|
|
47
47
|
expect(registry.has(NODE_TYPE.COMPONENT)).toBe(true);
|
|
48
48
|
expect(registry.has(NODE_TYPE.EMBED)).toBe(true);
|
|
@@ -185,6 +185,6 @@ describe('globalNodeTypeManager', () => {
|
|
|
185
185
|
registerBuiltInNodeTypes();
|
|
186
186
|
expect(globalNodeTypeManager.has(NODE_TYPE.NODE)).toBe(true);
|
|
187
187
|
expect(globalNodeTypeManager.has(NODE_TYPE.EMBED)).toBe(true);
|
|
188
|
-
expect(globalNodeTypeManager.getAll().length).toBe(7);
|
|
188
|
+
expect(globalNodeTypeManager.getAll().length).toBe(7); // 7 node types after cms-list merged into list
|
|
189
189
|
});
|
|
190
190
|
});
|