@webmate-studio/builder 0.2.110 → 0.2.112
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/package.json +1 -1
- package/src/design-tokens.js +16 -5
- package/src/markdown.js +22 -3
- package/src/template-processor.js +72 -37
package/package.json
CHANGED
package/src/design-tokens.js
CHANGED
|
@@ -34,6 +34,7 @@ function generateSemanticColorUtilities(tokens) {
|
|
|
34
34
|
'infoLight': 'info-light',
|
|
35
35
|
|
|
36
36
|
// Base semantic colors (use prefixed names to avoid conflicts)
|
|
37
|
+
'bgBody': 'bg-body',
|
|
37
38
|
'bgBase': 'bg-default',
|
|
38
39
|
'bgElevated': 'bg-elevated',
|
|
39
40
|
'bgLifted': 'bg-lifted',
|
|
@@ -217,6 +218,7 @@ export const defaultDesignTokens = {
|
|
|
217
218
|
infoLight: '#dbeafe',
|
|
218
219
|
|
|
219
220
|
// Background Colors (Semantic)
|
|
221
|
+
bgBody: '#ffffff', // Page/body background
|
|
220
222
|
bgBase: '#ffffff',
|
|
221
223
|
bgElevated: '#f9fafb',
|
|
222
224
|
bgLifted: '#ffffff',
|
|
@@ -648,6 +650,7 @@ export function generateTailwindV4Theme(tokens) {
|
|
|
648
650
|
if (tokens.colors.infoLight) lines.push(` --color-info-light: ${tokens.colors.infoLight};`);
|
|
649
651
|
|
|
650
652
|
// Background colors (Semantic)
|
|
653
|
+
if (tokens.colors.bgBody) lines.push(` --color-bg-body: ${tokens.colors.bgBody};`);
|
|
651
654
|
if (tokens.colors.bgBase) lines.push(` --color-bg-base: ${tokens.colors.bgBase};`);
|
|
652
655
|
if (tokens.colors.bgElevated) lines.push(` --color-bg-elevated: ${tokens.colors.bgElevated};`);
|
|
653
656
|
if (tokens.colors.bgLifted) lines.push(` --color-bg-lifted: ${tokens.colors.bgLifted};`);
|
|
@@ -871,14 +874,17 @@ export function generateTailwindV4Theme(tokens) {
|
|
|
871
874
|
|
|
872
875
|
// Global styles (MUST be outside @theme block)
|
|
873
876
|
let globalStyles = `
|
|
874
|
-
/* Global baseline
|
|
877
|
+
/* Global baseline styles */
|
|
875
878
|
body {
|
|
876
879
|
font-family: var(--font-body);
|
|
880
|
+
background-color: var(--color-bg-body, var(--color-bg-base, #ffffff));
|
|
877
881
|
}`;
|
|
878
882
|
|
|
879
883
|
// Generate utility classes for text styles
|
|
884
|
+
// IMPORTANT: Wrap in @layer components so color utilities (@layer utilities) can override textColor
|
|
880
885
|
if (tokens.textStyles) {
|
|
881
886
|
globalStyles += '\n\n/* Text Style Utilities */';
|
|
887
|
+
globalStyles += '\n@layer components {';
|
|
882
888
|
for (const [styleName, style] of Object.entries(tokens.textStyles)) {
|
|
883
889
|
const kebabName = styleName
|
|
884
890
|
.replace(/([A-Z])/g, '-$1')
|
|
@@ -914,9 +920,11 @@ body {
|
|
|
914
920
|
}
|
|
915
921
|
globalStyles += `\n}`;
|
|
916
922
|
}
|
|
923
|
+
globalStyles += '\n}'; // Close @layer components for base text styles
|
|
917
924
|
|
|
918
925
|
// Generate responsive variants (sm:, md:, lg:, xl:, 2xl:) for Tailwind compatibility
|
|
919
926
|
// These allow switching to a different text style at a breakpoint: class="text-body lg:text-lead"
|
|
927
|
+
// IMPORTANT: @layer must be INSIDE @media for proper cascade behavior
|
|
920
928
|
const breakpointKeys = ['sm', 'md', 'lg', 'xl', '2xl'];
|
|
921
929
|
const breakpointValues = {
|
|
922
930
|
sm: '640px',
|
|
@@ -927,6 +935,8 @@ body {
|
|
|
927
935
|
};
|
|
928
936
|
|
|
929
937
|
for (const bp of breakpointKeys) {
|
|
938
|
+
globalStyles += `\n@media (min-width: ${breakpointValues[bp]}) {`;
|
|
939
|
+
globalStyles += `\n@layer components {`;
|
|
930
940
|
for (const [styleName, style] of Object.entries(tokens.textStyles)) {
|
|
931
941
|
const kebabName = styleName
|
|
932
942
|
.replace(/([A-Z])/g, '-$1')
|
|
@@ -937,7 +947,6 @@ body {
|
|
|
937
947
|
|
|
938
948
|
// Generate Tailwind-compatible breakpoint class: md:text-body
|
|
939
949
|
// Apply ALL properties of the text style, not just responsive values
|
|
940
|
-
globalStyles += `\n@media (min-width: ${breakpointValues[bp]}) {`;
|
|
941
950
|
globalStyles += `\n .${bp}\\:${className} {`;
|
|
942
951
|
|
|
943
952
|
// Font family
|
|
@@ -981,8 +990,9 @@ body {
|
|
|
981
990
|
}
|
|
982
991
|
|
|
983
992
|
globalStyles += `\n }`;
|
|
984
|
-
globalStyles += `\n}`;
|
|
985
993
|
}
|
|
994
|
+
globalStyles += '\n}'; // Close @layer components
|
|
995
|
+
globalStyles += '\n}'; // Close @media
|
|
986
996
|
}
|
|
987
997
|
}
|
|
988
998
|
|
|
@@ -1210,11 +1220,12 @@ export function generateCSSFromTokens(tokens) {
|
|
|
1210
1220
|
|
|
1211
1221
|
lines.push('}');
|
|
1212
1222
|
|
|
1213
|
-
// Add global body
|
|
1223
|
+
// Add global body styles as baseline
|
|
1214
1224
|
lines.push('');
|
|
1215
|
-
lines.push('/* Global baseline
|
|
1225
|
+
lines.push('/* Global baseline styles */');
|
|
1216
1226
|
lines.push('body {');
|
|
1217
1227
|
lines.push(' font-family: var(--font-body);');
|
|
1228
|
+
lines.push(' background-color: var(--color-bg-body, var(--color-bg-base, #ffffff));');
|
|
1218
1229
|
lines.push('}');
|
|
1219
1230
|
|
|
1220
1231
|
// Generate utility classes for text styles
|
package/src/markdown.js
CHANGED
|
@@ -114,6 +114,7 @@ export function markdownToHtml(markdown, options = {}) {
|
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
116
|
* Process markdown props: Convert markdown strings to HTML
|
|
117
|
+
* Supports nested arrays with markdown fields (e.g., items[].text)
|
|
117
118
|
*
|
|
118
119
|
* @param {Object} props - Component props
|
|
119
120
|
* @param {Object} propSchema - Prop schema with format info (from component.json)
|
|
@@ -126,17 +127,35 @@ export function processMarkdownProps(props, propSchema = null, componentMetadata
|
|
|
126
127
|
const processed = { ...props };
|
|
127
128
|
|
|
128
129
|
for (const [key, value] of Object.entries(processed)) {
|
|
129
|
-
|
|
130
|
+
const schema = propSchema?.[key];
|
|
131
|
+
|
|
132
|
+
// Handle arrays with nested markdown fields
|
|
133
|
+
if (Array.isArray(value) && schema?.type === 'array' && schema?.props) {
|
|
134
|
+
// Process each item in the array using the nested props schema
|
|
135
|
+
processed[key] = value.map(item => {
|
|
136
|
+
if (typeof item !== 'object' || item === null) return item;
|
|
137
|
+
return processMarkdownProps(item, schema.props, componentMetadata);
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle nested objects
|
|
143
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value) && schema?.props) {
|
|
144
|
+
processed[key] = processMarkdownProps(value, schema.props, componentMetadata);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Only process strings for markdown conversion
|
|
130
149
|
if (typeof value !== 'string' || !value) continue;
|
|
131
150
|
|
|
132
151
|
// Check if this prop is markdown (via schema)
|
|
133
|
-
const isMarkdown =
|
|
152
|
+
const isMarkdown = schema?.format === 'markdown';
|
|
134
153
|
|
|
135
154
|
if (isMarkdown) {
|
|
136
155
|
const options = {};
|
|
137
156
|
|
|
138
157
|
// Apply headingStartLevel from prop schema (preferred) or component metadata (fallback)
|
|
139
|
-
const headingStartLevel =
|
|
158
|
+
const headingStartLevel = schema?.headingStartLevel || componentMetadata?.headingStartLevel;
|
|
140
159
|
if (headingStartLevel) {
|
|
141
160
|
options.headingStartLevel = headingStartLevel;
|
|
142
161
|
}
|
|
@@ -145,6 +145,7 @@ class TemplateProcessor {
|
|
|
145
145
|
* - {#each items as item (item.id)}
|
|
146
146
|
* - {#each items as item, index (item.id)}
|
|
147
147
|
* - {#each items as item}{:else}Empty state{/each}
|
|
148
|
+
* - {#each Array(n) as _} or {#each Array(n) as _, index} (creates array of n elements)
|
|
148
149
|
*
|
|
149
150
|
* @param {string} html - HTML template with {#each} blocks
|
|
150
151
|
* @param {Object} contextData - Data context for evaluation
|
|
@@ -162,7 +163,8 @@ class TemplateProcessor {
|
|
|
162
163
|
|
|
163
164
|
// Find all {#each} opening tags with optional index parameter and key
|
|
164
165
|
// Matches: {#each items as item} or {#each items as item, index} or {#each items as item (item.id)}
|
|
165
|
-
|
|
166
|
+
// Also matches: {#each Array(n) as _} or {#each Array(expr) as _, index} for generating arrays
|
|
167
|
+
const openPattern = /\{#each\s+((?:[a-zA-Z_][a-zA-Z0-9_.]*|Array\([^)]+\)))\s+as\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:,\s*([a-zA-Z_][a-zA-Z0-9_]*))?\s*(?:\(([^)]+)\))?\}/g;
|
|
166
168
|
|
|
167
169
|
let result = html;
|
|
168
170
|
let iteration = 0;
|
|
@@ -239,27 +241,62 @@ class TemplateProcessor {
|
|
|
239
241
|
foundMatch = true;
|
|
240
242
|
console.log(`${indent}[processEachLoops] iteration=${iteration}, FOUND: ${loopVar} in ${arrayPath}`);
|
|
241
243
|
|
|
242
|
-
|
|
243
|
-
let
|
|
244
|
+
let arrayValue;
|
|
245
|
+
let pathValid = true;
|
|
244
246
|
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
// Check for Array(n) pattern - creates an array of n elements
|
|
248
|
+
const arrayConstructorMatch = arrayPath.match(/^Array\(([^)]+)\)$/);
|
|
249
|
+
if (arrayConstructorMatch) {
|
|
250
|
+
const countExpr = arrayConstructorMatch[1].trim();
|
|
251
|
+
console.log(`${indent}[processEachLoops] Array constructor pattern detected: Array(${countExpr})`);
|
|
252
|
+
|
|
253
|
+
// Evaluate the expression to get the count
|
|
254
|
+
let count = 0;
|
|
255
|
+
try {
|
|
256
|
+
// First try to parse as a simple number
|
|
257
|
+
if (/^\d+$/.test(countExpr)) {
|
|
258
|
+
count = parseInt(countExpr, 10);
|
|
259
|
+
} else {
|
|
260
|
+
// Otherwise evaluate as expression (e.g., item.rating)
|
|
261
|
+
const rawValue = this.evaluateExpressionValue(countExpr, contextData);
|
|
262
|
+
// Convert to number (handles string values like "3" from CMS)
|
|
263
|
+
count = typeof rawValue === 'string' ? parseInt(rawValue, 10) : Number(rawValue);
|
|
264
|
+
}
|
|
265
|
+
console.log(`${indent}[processEachLoops] Array count evaluated to: ${count}`);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.warn(`${indent}[processEachLoops] Failed to evaluate Array count expression "${countExpr}":`, err.message);
|
|
268
|
+
pathValid = false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Create array with 'count' undefined elements (like Array(n) in JavaScript)
|
|
272
|
+
// Use Number.isFinite to handle NaN and non-numeric values
|
|
273
|
+
if (pathValid && Number.isFinite(count) && count > 0) {
|
|
274
|
+
arrayValue = Array(count).fill(undefined);
|
|
275
|
+
} else {
|
|
276
|
+
arrayValue = [];
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
// Parse array path: "items" or "item.subitems"
|
|
280
|
+
let pathParts = arrayPath.split('.');
|
|
281
|
+
|
|
282
|
+
// If we're in a nested context and the path starts with the parent loop variable,
|
|
283
|
+
// strip it since the context IS that variable
|
|
284
|
+
if (depth > 0 && parentLoopVar && pathParts.length > 1 && pathParts[0] === parentLoopVar) {
|
|
285
|
+
console.log(`${indent}[processEachLoops] Stripping parent loop var "${parentLoopVar}" from path "${arrayPath}"`);
|
|
286
|
+
pathParts = pathParts.slice(1);
|
|
287
|
+
}
|
|
251
288
|
|
|
252
|
-
|
|
289
|
+
arrayValue = contextData;
|
|
253
290
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
291
|
+
// Navigate through path
|
|
292
|
+
for (const part of pathParts) {
|
|
293
|
+
if (arrayValue === null || arrayValue === undefined) {
|
|
294
|
+
console.warn(`[Preview] Array path not found: ${arrayPath} (stopped at ${part})`);
|
|
295
|
+
pathValid = false;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
arrayValue = arrayValue[part];
|
|
261
299
|
}
|
|
262
|
-
arrayValue = arrayValue[part];
|
|
263
300
|
}
|
|
264
301
|
|
|
265
302
|
let replacement = '';
|
|
@@ -282,21 +319,25 @@ class TemplateProcessor {
|
|
|
282
319
|
console.log(`${indent}[Preview] Processing loop item #${index}: ${loopVar} in ${arrayPath}`, item);
|
|
283
320
|
let itemHtml = content;
|
|
284
321
|
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
itemHtml = this.processEachLoops(itemHtml, item, depth + 1, loopVar);
|
|
289
|
-
console.log(`${indent}[DEBUG] AFTER nested processEachLoops - HTML snippet:`, itemHtml.substring(0, 300));
|
|
290
|
-
|
|
291
|
-
// Process class:name={loopVar.field} conditionals BEFORE replacing {loopVar.field}
|
|
292
|
-
// Use loop item as context so loop variables are available
|
|
293
|
-
const loopContext = { [loopVar]: item };
|
|
322
|
+
// Build loop context with the loop variable and index
|
|
323
|
+
// Also merge with parent context so nested expressions can access parent data
|
|
324
|
+
const loopContext = { ...contextData, [loopVar]: item };
|
|
294
325
|
if (indexVar) {
|
|
295
326
|
loopContext[indexVar] = index;
|
|
296
327
|
}
|
|
297
328
|
// Add the array itself to context (use the last part of the path as name)
|
|
298
329
|
const arrayName = arrayPath.split('.').pop();
|
|
299
|
-
|
|
330
|
+
if (arrayName && !arrayName.includes('(')) {
|
|
331
|
+
loopContext[arrayName] = arrayValue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// First, recursively process any nested loops with the merged context
|
|
335
|
+
// Pass loopVar as parentLoopVar so nested loops can strip it from paths
|
|
336
|
+
console.log(`${indent}[DEBUG] BEFORE nested processEachLoops - HTML snippet:`, itemHtml.substring(0, 300));
|
|
337
|
+
itemHtml = this.processEachLoops(itemHtml, loopContext, depth + 1, loopVar);
|
|
338
|
+
console.log(`${indent}[DEBUG] AFTER nested processEachLoops - HTML snippet:`, itemHtml.substring(0, 300));
|
|
339
|
+
|
|
340
|
+
// Process class:name={loopVar.field} conditionals BEFORE replacing {loopVar.field}
|
|
300
341
|
itemHtml = this.processClassConditionals(itemHtml, loopContext);
|
|
301
342
|
|
|
302
343
|
// First: Replace attribute expressions with quoted values (e.g., alt={item.image.alt} or alt="{item.image.alt}" → alt="value")
|
|
@@ -393,14 +434,8 @@ class TemplateProcessor {
|
|
|
393
434
|
});
|
|
394
435
|
|
|
395
436
|
// Process {#if} conditionals within loop (supports {:else if})
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
if (indexVar) {
|
|
399
|
-
evalContext[indexVar] = index;
|
|
400
|
-
}
|
|
401
|
-
// Add the array itself to context
|
|
402
|
-
evalContext[arrayName] = arrayValue;
|
|
403
|
-
itemHtml = this.processIfConditionals(itemHtml, evalContext, depth + 2);
|
|
437
|
+
// Use loopContext which already has parent context + loop variable + index
|
|
438
|
+
itemHtml = this.processIfConditionals(itemHtml, loopContext, depth + 2);
|
|
404
439
|
|
|
405
440
|
// Process complex expressions within loop (e.g., {item.image.src ? "Ja" : "Nein"})
|
|
406
441
|
// This handles expressions that couldn't be processed by the simple regex replacements above
|
|
@@ -419,7 +454,7 @@ class TemplateProcessor {
|
|
|
419
454
|
|
|
420
455
|
// Evaluate complex expressions with loop context
|
|
421
456
|
try {
|
|
422
|
-
const result = this.evaluateExpressionValue(expression,
|
|
457
|
+
const result = this.evaluateExpressionValue(expression, loopContext);
|
|
423
458
|
if (result === undefined || result === null) {
|
|
424
459
|
return '';
|
|
425
460
|
}
|