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
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
// Global registry mapping class names to original CSS values
|
|
8
8
|
const registry = new Map<string, string | number>();
|
|
9
9
|
|
|
10
|
+
// Registry for dynamic/fallback classes that include the CSS property name
|
|
11
|
+
// Used for properties not in the standard propertyMap
|
|
12
|
+
const dynamicRegistry = new Map<string, { property: string; value: string | number }>();
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
15
|
* Register a style value for a class name
|
|
12
16
|
* Called when generating utility class names from style objects
|
|
@@ -15,6 +19,14 @@ export function registerStyleValue(className: string, value: string | number): v
|
|
|
15
19
|
registry.set(className, value);
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Register a dynamic style (property + value) for a class name
|
|
24
|
+
* Used for properties not in propertyMap that need dynamic class generation
|
|
25
|
+
*/
|
|
26
|
+
export function registerDynamicStyle(className: string, property: string, value: string | number): void {
|
|
27
|
+
dynamicRegistry.set(className, { property, value });
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
/**
|
|
19
31
|
* Get the original style value for a class name
|
|
20
32
|
* Returns undefined if not registered (fallback to reverse-engineering)
|
|
@@ -23,20 +35,46 @@ export function getStyleValue(className: string): string | number | undefined {
|
|
|
23
35
|
return registry.get(className);
|
|
24
36
|
}
|
|
25
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Get the dynamic style (property + value) for a class name
|
|
40
|
+
* Returns undefined if not registered
|
|
41
|
+
*/
|
|
42
|
+
export function getDynamicStyle(className: string): { property: string; value: string | number } | undefined {
|
|
43
|
+
return dynamicRegistry.get(className);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a class name is registered in the dynamic registry
|
|
48
|
+
*/
|
|
49
|
+
export function isDynamicClass(className: string): boolean {
|
|
50
|
+
return dynamicRegistry.has(className);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all registered dynamic class names
|
|
55
|
+
*/
|
|
56
|
+
export function getAllDynamicClasses(): string[] {
|
|
57
|
+
return Array.from(dynamicRegistry.keys());
|
|
58
|
+
}
|
|
59
|
+
|
|
26
60
|
/**
|
|
27
61
|
* Clear all registered values
|
|
28
62
|
* Useful for testing or resetting state
|
|
29
63
|
*/
|
|
30
64
|
export function clearRegistry(): void {
|
|
31
65
|
registry.clear();
|
|
66
|
+
dynamicRegistry.clear();
|
|
32
67
|
}
|
|
33
68
|
|
|
34
69
|
/**
|
|
35
70
|
* Serialize registry for SSR embedding
|
|
36
|
-
* Returns JSON string of all entries
|
|
71
|
+
* Returns JSON string of all entries (both standard and dynamic)
|
|
37
72
|
*/
|
|
38
73
|
export function serializeRegistry(): string {
|
|
39
|
-
return JSON.stringify(
|
|
74
|
+
return JSON.stringify({
|
|
75
|
+
standard: Array.from(registry.entries()),
|
|
76
|
+
dynamic: Array.from(dynamicRegistry.entries()),
|
|
77
|
+
});
|
|
40
78
|
}
|
|
41
79
|
|
|
42
80
|
/**
|
|
@@ -44,8 +82,25 @@ export function serializeRegistry(): string {
|
|
|
44
82
|
* Call on client to restore server-side registered values
|
|
45
83
|
*/
|
|
46
84
|
export function hydrateRegistry(data: string): void {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
const parsed = JSON.parse(data);
|
|
86
|
+
|
|
87
|
+
// Handle both old format (array) and new format (object with standard/dynamic)
|
|
88
|
+
if (Array.isArray(parsed)) {
|
|
89
|
+
// Old format: just standard entries
|
|
90
|
+
for (const [key, value] of parsed as [string, string | number][]) {
|
|
91
|
+
registry.set(key, value);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// New format: { standard, dynamic }
|
|
95
|
+
if (parsed.standard) {
|
|
96
|
+
for (const [key, value] of parsed.standard as [string, string | number][]) {
|
|
97
|
+
registry.set(key, value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (parsed.dynamic) {
|
|
101
|
+
for (const [key, value] of parsed.dynamic as [string, { property: string; value: string | number }][]) {
|
|
102
|
+
dynamicRegistry.set(key, value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
50
105
|
}
|
|
51
106
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { Path } from '../pathArrayUtils';
|
|
11
11
|
import type { PageData, ComponentNode } from '../types';
|
|
12
12
|
import { getRootData } from '../treePathUtils';
|
|
13
|
-
import { isComponentNode, isSlotMarker, isLocaleListNode, isCMSListNode } from '../nodeUtils';
|
|
13
|
+
import { isComponentNode, isSlotMarker, isLocaleListNode, isCMSListNode, isListNode } from '../nodeUtils';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Node path data containing both logical and rendered paths
|
|
@@ -289,7 +289,7 @@ export function buildTreePathsWithRendered(
|
|
|
289
289
|
if (
|
|
290
290
|
typeof child === 'object' &&
|
|
291
291
|
child !== null &&
|
|
292
|
-
('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || (cumulativeInstancePath && isSlotMarker(child)))
|
|
292
|
+
('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || isListNode(child) || (cumulativeInstancePath && isSlotMarker(child)))
|
|
293
293
|
) {
|
|
294
294
|
const childPath = [...currentPath, index];
|
|
295
295
|
traverse(child as ComponentNode, childPath);
|
|
@@ -361,7 +361,7 @@ export function buildComponentTreePaths(
|
|
|
361
361
|
if (
|
|
362
362
|
typeof child === 'object' &&
|
|
363
363
|
child !== null &&
|
|
364
|
-
('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || isSlotMarker(child))
|
|
364
|
+
('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || isListNode(child) || isSlotMarker(child))
|
|
365
365
|
) {
|
|
366
366
|
const childPath = [...currentPath, index];
|
|
367
367
|
traverse(child as ComponentNode, childPath);
|
|
@@ -293,14 +293,16 @@ export function isComponentNodeParent(
|
|
|
293
293
|
// - SlotMarker: type === 'slot'
|
|
294
294
|
// - LinkNode: type === 'link'
|
|
295
295
|
// - EmbedNode: type === 'embed'
|
|
296
|
-
// -
|
|
296
|
+
// - ListNode: type === 'list' (also handles legacy 'cms-list' for migration)
|
|
297
|
+
const nodeType = (parent as any).type;
|
|
297
298
|
return 'type' in parent && (
|
|
298
299
|
'tag' in parent ||
|
|
299
300
|
'component' in parent ||
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
nodeType === 'slot' ||
|
|
302
|
+
nodeType === 'link' ||
|
|
303
|
+
nodeType === 'embed' ||
|
|
304
|
+
nodeType === 'cms-list' || // Legacy support for migration
|
|
305
|
+
nodeType === 'list'
|
|
304
306
|
);
|
|
305
307
|
}
|
|
306
308
|
|
package/lib/shared/types/cms.ts
CHANGED
|
@@ -147,64 +147,11 @@ export interface CMSListQuery {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* Context variables available in children (with named context):
|
|
154
|
-
* - {{<itemAs>.field}} - Access CMS item fields (e.g., {{post.title}})
|
|
155
|
-
* - {{<itemAs>Index}} - Zero-based index of current item (e.g., {{postIndex}})
|
|
156
|
-
* - {{<itemAs>First}} - Boolean, true if first item (e.g., {{postFirst}})
|
|
157
|
-
* - {{<itemAs>Last}} - Boolean, true if last item (e.g., {{postLast}})
|
|
158
|
-
*
|
|
159
|
-
* Legacy context (still supported):
|
|
160
|
-
* - {{item.field}}, {{itemIndex}}, {{itemFirst}}, {{itemLast}}
|
|
150
|
+
* @deprecated CMSListNode is now replaced by ListNode with sourceType: 'collection'.
|
|
151
|
+
* This type alias is kept for backward compatibility during migration.
|
|
152
|
+
* Import ListNode from '../registry/nodeTypes/ListNodeType' instead.
|
|
161
153
|
*/
|
|
162
|
-
export
|
|
163
|
-
type: 'cms-list';
|
|
164
|
-
/** Custom label displayed in structure tree */
|
|
165
|
-
label?: string;
|
|
166
|
-
/** Conditional rendering - skip node when false */
|
|
167
|
-
if?: { field: string; operator?: 'eq' | 'neq' | 'truthy' | 'falsy'; value?: unknown };
|
|
168
|
-
/** Inline styles */
|
|
169
|
-
style?: Record<string, unknown>;
|
|
170
|
-
/** Interactive CSS rules (hover, active, etc.) */
|
|
171
|
-
interactiveStyles?: Array<{
|
|
172
|
-
style: Record<string, unknown>;
|
|
173
|
-
name?: string;
|
|
174
|
-
prefix?: string;
|
|
175
|
-
postfix?: string;
|
|
176
|
-
previewProp?: string;
|
|
177
|
-
}>;
|
|
178
|
-
/** Generate element class without styles (for custom CSS) */
|
|
179
|
-
generateElementClass?: boolean;
|
|
180
|
-
/** HTML attributes */
|
|
181
|
-
attributes?: Record<string, string | number | boolean>;
|
|
182
|
-
/** Collection to query */
|
|
183
|
-
collection: string;
|
|
184
|
-
/** Variable name for item in templates (default: singular of collection) */
|
|
185
|
-
itemAs?: string;
|
|
186
|
-
/** Direct item IDs or template expression for referenced items (e.g., "{{post.authorId}}") */
|
|
187
|
-
items?: string | string[];
|
|
188
|
-
/** Filter conditions */
|
|
189
|
-
filter?: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown>;
|
|
190
|
-
/** Sort configuration */
|
|
191
|
-
sort?: CMSSortConfig | CMSSortConfig[];
|
|
192
|
-
/** Maximum number of items to return */
|
|
193
|
-
limit?: number;
|
|
194
|
-
/** Number of items to skip */
|
|
195
|
-
offset?: number;
|
|
196
|
-
/** Exclude the current CMS item from the list (useful for "related items" sections) */
|
|
197
|
-
excludeCurrentItem?: boolean;
|
|
198
|
-
/**
|
|
199
|
-
* Emit item template for dynamic client-side rendering.
|
|
200
|
-
* When true, SSR emits a `<template data-meno-item>` element with unprocessed
|
|
201
|
-
* {{item.field}} placeholders. MenoFilter uses this to render items beyond
|
|
202
|
-
* the SSR'd limit dynamically.
|
|
203
|
-
*/
|
|
204
|
-
emitTemplate?: boolean;
|
|
205
|
-
/** Children are repeated for each item */
|
|
206
|
-
children?: unknown[];
|
|
207
|
-
}
|
|
154
|
+
export type CMSListNode = import('../registry/nodeTypes/ListNodeType').ListNode;
|
|
208
155
|
|
|
209
156
|
/**
|
|
210
157
|
* Configuration for nested CMS list placeholders.
|
|
@@ -8,7 +8,7 @@ import type { ComponentNode } from './nodes';
|
|
|
8
8
|
/**
|
|
9
9
|
* Prop type definitions
|
|
10
10
|
*/
|
|
11
|
-
export type PropType = 'string' | 'select' | 'boolean' | 'number' | 'link' | 'file' | 'rich-text';
|
|
11
|
+
export type PropType = 'string' | 'select' | 'boolean' | 'number' | 'link' | 'file' | 'rich-text' | 'list';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Internationalization (i18n) value object
|
|
@@ -53,15 +53,56 @@ export interface LinkPropValue {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
56
|
+
* Base prop definition without list-specific fields
|
|
57
57
|
*/
|
|
58
|
-
export interface
|
|
59
|
-
type: PropType
|
|
58
|
+
export interface BasePropDefinition {
|
|
59
|
+
type: Exclude<PropType, 'list'>;
|
|
60
60
|
default?: string | number | boolean | I18nValue | LinkPropValue;
|
|
61
61
|
options?: readonly string[]; // Required for "select" type
|
|
62
62
|
accept?: string; // For "file" type: MIME pattern like "image/*", "video/*"
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* List item schema - defines the structure of each item in a list prop
|
|
67
|
+
* Uses the same prop types as component interfaces (except nested lists)
|
|
68
|
+
*/
|
|
69
|
+
export type ListItemSchema = Record<string, BasePropDefinition>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* List item value type
|
|
73
|
+
*/
|
|
74
|
+
export type ListItemValue = Record<string, string | number | boolean | LinkPropValue | null>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* List prop definition - for array/list data
|
|
78
|
+
*/
|
|
79
|
+
export interface ListPropDefinition {
|
|
80
|
+
type: 'list';
|
|
81
|
+
/** Schema defining the structure of each list item */
|
|
82
|
+
itemSchema: ListItemSchema;
|
|
83
|
+
/** Default value is an array of items */
|
|
84
|
+
default?: ListItemValue[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Prop definition with improved type safety
|
|
89
|
+
*/
|
|
90
|
+
export type PropDefinition = BasePropDefinition | ListPropDefinition;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Type guard to check if a prop definition is a list type
|
|
94
|
+
*/
|
|
95
|
+
export function isListPropDefinition(def: PropDefinition): def is ListPropDefinition {
|
|
96
|
+
return def.type === 'list';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Type guard to check if a prop definition is a base (non-list) type
|
|
101
|
+
*/
|
|
102
|
+
export function isBasePropDefinition(def: PropDefinition): def is BasePropDefinition {
|
|
103
|
+
return def.type !== 'list';
|
|
104
|
+
}
|
|
105
|
+
|
|
65
106
|
/**
|
|
66
107
|
* Structured component definition
|
|
67
108
|
*/
|
|
@@ -22,6 +22,10 @@ export type {
|
|
|
22
22
|
ComponentDefinition,
|
|
23
23
|
StructuredComponentDefinition,
|
|
24
24
|
PropDefinition,
|
|
25
|
+
BasePropDefinition,
|
|
26
|
+
ListPropDefinition,
|
|
27
|
+
ListItemSchema,
|
|
28
|
+
ListItemValue,
|
|
25
29
|
PropType,
|
|
26
30
|
JSONPage,
|
|
27
31
|
PageData,
|
|
@@ -29,6 +33,12 @@ export type {
|
|
|
29
33
|
I18nValue,
|
|
30
34
|
I18nConfig,
|
|
31
35
|
LocaleConfig,
|
|
36
|
+
LinkPropValue,
|
|
37
|
+
} from './components';
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
isListPropDefinition,
|
|
41
|
+
isBasePropDefinition,
|
|
32
42
|
} from './components';
|
|
33
43
|
|
|
34
44
|
// Export all style types
|
|
@@ -80,6 +90,9 @@ export type {
|
|
|
80
90
|
ReferenceLocation,
|
|
81
91
|
} from './cms';
|
|
82
92
|
|
|
93
|
+
// Export ListNode from registry (unified list node type)
|
|
94
|
+
export type { ListNode } from '../registry/nodeTypes/ListNodeType';
|
|
95
|
+
|
|
83
96
|
// Export CMS utilities
|
|
84
97
|
export { singularize } from './cms';
|
|
85
98
|
|
|
@@ -76,6 +76,7 @@ export const propertyMap: Record<string, string> = {
|
|
|
76
76
|
overflowWrap: 'ow',
|
|
77
77
|
textIndent: 'ti',
|
|
78
78
|
verticalAlign: 'va',
|
|
79
|
+
whiteSpace: 'whs',
|
|
79
80
|
|
|
80
81
|
// Lists
|
|
81
82
|
listStyle: 'lst',
|
|
@@ -213,6 +214,7 @@ export const prefixToCSSProperty: Record<string, string> = {
|
|
|
213
214
|
ow: 'overflow-wrap',
|
|
214
215
|
ti: 'text-indent',
|
|
215
216
|
va: 'vertical-align',
|
|
217
|
+
whs: 'white-space',
|
|
216
218
|
|
|
217
219
|
// Lists
|
|
218
220
|
lst: 'list-style',
|
|
@@ -343,6 +345,13 @@ export const specialValueMappings: Record<string, Record<string, string>> = {
|
|
|
343
345
|
text: 'us-text',
|
|
344
346
|
all: 'us-all',
|
|
345
347
|
},
|
|
348
|
+
whiteSpace: {
|
|
349
|
+
normal: 'whs-normal',
|
|
350
|
+
nowrap: 'whs-nowrap',
|
|
351
|
+
pre: 'whs-pre',
|
|
352
|
+
'pre-wrap': 'whs-pre-wrap',
|
|
353
|
+
'pre-line': 'whs-pre-line',
|
|
354
|
+
},
|
|
346
355
|
// Note: CSS functions (blur, translateY, scale, rotate, repeat) are now handled
|
|
347
356
|
// dynamically in utilityClassMapper.ts - no need to hardcode specific values here
|
|
348
357
|
};
|
|
@@ -378,6 +387,11 @@ export const classToStyleSpecialCases: Record<string, { prop: string; value: str
|
|
|
378
387
|
'us-auto': { prop: 'userSelect', value: 'auto' },
|
|
379
388
|
'us-text': { prop: 'userSelect', value: 'text' },
|
|
380
389
|
'us-all': { prop: 'userSelect', value: 'all' },
|
|
390
|
+
'whs-normal': { prop: 'whiteSpace', value: 'normal' },
|
|
391
|
+
'whs-nowrap': { prop: 'whiteSpace', value: 'nowrap' },
|
|
392
|
+
'whs-pre': { prop: 'whiteSpace', value: 'pre' },
|
|
393
|
+
'whs-pre-wrap': { prop: 'whiteSpace', value: 'pre-wrap' },
|
|
394
|
+
'whs-pre-line': { prop: 'whiteSpace', value: 'pre-line' },
|
|
381
395
|
// Grid template columns
|
|
382
396
|
'gtc-2': { prop: 'gridTemplateColumns', value: 'repeat(2, 1fr)' },
|
|
383
397
|
'gtc-3': { prop: 'gridTemplateColumns', value: 'repeat(3, 1fr)' },
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
gradientPresets,
|
|
14
14
|
borderPresets,
|
|
15
15
|
} from './utilityClassConfig';
|
|
16
|
-
import { registerStyleValue } from './styleValueRegistry';
|
|
16
|
+
import { registerStyleValue, registerDynamicStyle } from './styleValueRegistry';
|
|
17
17
|
|
|
18
18
|
// Function name abbreviations for shorter class names
|
|
19
19
|
const functionAbbreviations: Record<string, string> = {
|
|
@@ -40,13 +40,54 @@ function isStyleMapping(value: unknown): value is StyleMapping {
|
|
|
40
40
|
return typeof value === 'object' && value !== null && '_mapping' in value && (value as StyleMapping)._mapping === true;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Sanitize a value for use in a CSS class name
|
|
45
|
+
* Replaces special characters with safe alternatives
|
|
46
|
+
*/
|
|
47
|
+
function sanitizeClassValue(value: string): string {
|
|
48
|
+
return value
|
|
49
|
+
.replace(/\s+/g, '-') // spaces to hyphens
|
|
50
|
+
.replace(/[()]/g, '') // remove parentheses
|
|
51
|
+
.replace(/,/g, '_') // commas to underscores
|
|
52
|
+
.replace(/%/g, 'p') // percent to 'p'
|
|
53
|
+
.replace(/\./g, 'd') // dots to 'd' (for decimals)
|
|
54
|
+
.replace(/[^\w-]/g, ''); // remove other special chars
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
/**
|
|
44
58
|
* Converts a CSS property value to a utility class name
|
|
45
59
|
* Example: { prop: "padding", value: "10px" } → "p-10px"
|
|
60
|
+
*
|
|
61
|
+
* For properties not in propertyMap, generates a dynamic class using
|
|
62
|
+
* a shortened property name prefix (e.g., "textOverflow" → "txov-ellipsis")
|
|
46
63
|
*/
|
|
47
64
|
function propertyValueToClass(prop: string, value: string | number): string | null {
|
|
48
65
|
const prefix = propertyMap[prop];
|
|
49
|
-
|
|
66
|
+
|
|
67
|
+
// For properties not in propertyMap, generate a dynamic fallback class
|
|
68
|
+
if (!prefix) {
|
|
69
|
+
const stringValue = String(value);
|
|
70
|
+
// Generate a short prefix from the property name (first 2-4 chars of each word)
|
|
71
|
+
// e.g., "textOverflow" → "txov", "WebkitBackgroundClip" → "wbbg"
|
|
72
|
+
const dynamicPrefix = prop
|
|
73
|
+
.replace(/([A-Z])/g, '-$1') // camelCase to kebab
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.split('-')
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.map(word => word.slice(0, 2)) // first 2 chars of each word
|
|
78
|
+
.join('');
|
|
79
|
+
|
|
80
|
+
// Sanitize value for class name
|
|
81
|
+
const sanitizedValue = sanitizeClassValue(stringValue);
|
|
82
|
+
const className = `${dynamicPrefix}-${sanitizedValue}`;
|
|
83
|
+
|
|
84
|
+
// Convert camelCase to kebab-case for CSS property
|
|
85
|
+
const cssProperty = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
86
|
+
|
|
87
|
+
// Register in dynamic registry with property info
|
|
88
|
+
registerDynamicStyle(className, cssProperty, value);
|
|
89
|
+
return className;
|
|
90
|
+
}
|
|
50
91
|
|
|
51
92
|
const stringValue = String(value);
|
|
52
93
|
let className: string | null = null;
|
|
@@ -94,7 +94,9 @@ function validateSingleProp(
|
|
|
94
94
|
return { valid: true };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
const { type
|
|
97
|
+
const { type } = propDef;
|
|
98
|
+
// Options is only available on select type (from BasePropDefinition)
|
|
99
|
+
const options = 'options' in propDef ? propDef.options : undefined;
|
|
98
100
|
|
|
99
101
|
// Type checking and coercion
|
|
100
102
|
let coercedValue: unknown;
|
|
@@ -164,6 +166,12 @@ function validateSingleProp(
|
|
|
164
166
|
typeValid = typeof value === 'string';
|
|
165
167
|
break;
|
|
166
168
|
|
|
169
|
+
case 'list':
|
|
170
|
+
// List type accepts arrays
|
|
171
|
+
coercedValue = value;
|
|
172
|
+
typeValid = Array.isArray(value);
|
|
173
|
+
break;
|
|
174
|
+
|
|
167
175
|
default:
|
|
168
176
|
return {
|
|
169
177
|
valid: false,
|
|
@@ -13,21 +13,48 @@ import type { StyleObject, StyleMapping, LinkMapping } from '../types/styles';
|
|
|
13
13
|
import { NODE_TYPE } from '../constants';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Prop type schema - validates PropType values
|
|
16
|
+
* Prop type schema - validates PropType values (excluding list for base definition)
|
|
17
17
|
*/
|
|
18
|
-
export const
|
|
18
|
+
export const BasePropTypeSchema = z.enum(['string', 'select', 'boolean', 'number', 'link', 'file', 'rich-text']);
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
|
|
21
|
+
* Full prop type schema including list
|
|
22
|
+
*/
|
|
23
|
+
export const PropTypeSchema = z.enum(['string', 'select', 'boolean', 'number', 'link', 'file', 'rich-text', 'list']);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Base prop definition schema (for non-list props)
|
|
23
27
|
*/
|
|
24
|
-
export const
|
|
25
|
-
type:
|
|
28
|
+
export const BasePropDefinitionSchema = z.object({
|
|
29
|
+
type: BasePropTypeSchema,
|
|
26
30
|
default: z.union([z.string(), z.number(), z.boolean(), z.object({ href: z.string(), target: z.string().optional() })]).optional(),
|
|
27
31
|
options: z.array(z.string()).readonly().optional(),
|
|
28
32
|
accept: z.string().optional(), // For 'file' type: MIME pattern like "image/*", "video/*"
|
|
29
33
|
}).passthrough();
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* List item schema - defines structure of each item in a list prop
|
|
37
|
+
*/
|
|
38
|
+
export const ListItemSchemaSchema = z.record(z.string(), BasePropDefinitionSchema);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* List prop definition schema
|
|
42
|
+
*/
|
|
43
|
+
export const ListPropDefinitionSchema = z.object({
|
|
44
|
+
type: z.literal('list'),
|
|
45
|
+
itemSchema: ListItemSchemaSchema,
|
|
46
|
+
default: z.array(z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.object({ href: z.string(), target: z.string().optional() }), z.null()]))).optional(),
|
|
47
|
+
}).passthrough();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Prop definition schema (union of base and list)
|
|
51
|
+
* Validates prop definitions from component interfaces
|
|
52
|
+
*/
|
|
53
|
+
export const PropDefinitionSchema = z.union([
|
|
54
|
+
ListPropDefinitionSchema,
|
|
55
|
+
BasePropDefinitionSchema,
|
|
56
|
+
]);
|
|
57
|
+
|
|
31
58
|
/**
|
|
32
59
|
* Style mapping schema
|
|
33
60
|
*/
|
|
@@ -124,10 +151,16 @@ export const InteractiveStylesSchema = z.array(InteractiveStyleRuleSchema);
|
|
|
124
151
|
|
|
125
152
|
/**
|
|
126
153
|
* Slot marker schema
|
|
154
|
+
* default: Optional default content when instance has no children
|
|
155
|
+
* Can be an array of ComponentNodes or a string
|
|
127
156
|
*/
|
|
128
|
-
export const SlotMarkerSchema = z.object({
|
|
157
|
+
export const SlotMarkerSchema: z.ZodType<any> = z.lazy(() => z.object({
|
|
129
158
|
type: z.literal(NODE_TYPE.SLOT),
|
|
130
|
-
|
|
159
|
+
default: z.union([
|
|
160
|
+
z.array(z.union([ComponentNodeSchema, z.string()])),
|
|
161
|
+
z.string(),
|
|
162
|
+
]).optional(),
|
|
163
|
+
}).passthrough());
|
|
131
164
|
|
|
132
165
|
/**
|
|
133
166
|
* Component node schema - discriminated union (forward declaration)
|
|
@@ -226,28 +259,41 @@ export const LocaleListNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
|
|
|
226
259
|
}).passthrough());
|
|
227
260
|
|
|
228
261
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
262
|
+
* Unified List node schema (basic version for ComponentNodeSchema union)
|
|
263
|
+
* Handles both prop-based lists and CMS collection-based lists.
|
|
264
|
+
* Uses sourceType to distinguish: 'prop' (default) or 'collection'.
|
|
265
|
+
* Also supports legacy 'cms-list' type for backward compatibility.
|
|
231
266
|
*/
|
|
232
|
-
export const
|
|
233
|
-
type: z.literal(NODE_TYPE.
|
|
234
|
-
|
|
267
|
+
export const ListNodeSchemaBasic: z.ZodType<any> = z.lazy(() => z.object({
|
|
268
|
+
type: z.union([z.literal(NODE_TYPE.LIST), z.literal('cms-list')]), // Support both for migration
|
|
269
|
+
sourceType: z.enum(['prop', 'collection']).optional(), // defaults to 'prop'
|
|
270
|
+
source: z.string().optional(), // Source prop name or collection name
|
|
271
|
+
collection: z.string().optional(), // Legacy field for cms-list migration
|
|
272
|
+
tag: z.string().optional(), // Wrapper element tag, defaults to 'div'
|
|
235
273
|
label: z.string().optional(),
|
|
236
274
|
if: IfConditionSchema.optional(),
|
|
237
275
|
style: StyleValueSchema.optional(),
|
|
238
276
|
interactiveStyles: InteractiveStylesSchema.optional(),
|
|
239
277
|
generateElementClass: z.boolean().optional(),
|
|
240
278
|
itemAs: z.string().optional(),
|
|
279
|
+
// Collection-specific options
|
|
241
280
|
items: z.union([z.string(), z.array(z.string())]).optional(),
|
|
242
281
|
filter: z.unknown().optional(),
|
|
243
282
|
sort: z.unknown().optional(),
|
|
244
283
|
limit: z.number().optional(),
|
|
245
284
|
offset: z.number().optional(),
|
|
246
285
|
excludeCurrentItem: z.boolean().optional(),
|
|
286
|
+
emitTemplate: z.boolean().optional(),
|
|
247
287
|
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
248
288
|
children: z.array(z.union([ComponentNodeSchema, z.string()])).optional(),
|
|
249
289
|
}).passthrough());
|
|
250
290
|
|
|
291
|
+
/**
|
|
292
|
+
* @deprecated Use ListNodeSchemaBasic instead.
|
|
293
|
+
* Kept for backward compatibility during migration.
|
|
294
|
+
*/
|
|
295
|
+
export const CMSListNodeSchemaBasic = ListNodeSchemaBasic;
|
|
296
|
+
|
|
251
297
|
/**
|
|
252
298
|
* Component node schema - discriminated union (now defined after dependencies)
|
|
253
299
|
*/
|
|
@@ -258,7 +304,7 @@ ComponentNodeSchema = z.union([
|
|
|
258
304
|
EmbedNodeSchema,
|
|
259
305
|
LinkNodeSchema,
|
|
260
306
|
LocaleListNodeSchema,
|
|
261
|
-
|
|
307
|
+
ListNodeSchemaBasic,
|
|
262
308
|
]);
|
|
263
309
|
|
|
264
310
|
// Export ComponentNodeSchema
|