@webmate-studio/builder 0.2.109 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmate-studio/builder",
3
- "version": "0.2.109",
3
+ "version": "0.2.112",
4
4
  "type": "module",
5
5
  "description": "Webmate Studio Component Builder",
6
6
  "keywords": [
@@ -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 font */
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 font-family as baseline
1223
+ // Add global body styles as baseline
1214
1224
  lines.push('');
1215
- lines.push('/* Global baseline font */');
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
- // Only process strings
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 = propSchema?.[key]?.format === 'markdown';
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 = propSchema?.[key]?.headingStartLevel || componentMetadata?.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
- const openPattern = /\{#each\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s+as\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:,\s*([a-zA-Z_][a-zA-Z0-9_]*))?\s*(?:\(([^)]+)\))?\}/g;
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
- // Parse array path: "items" or "item.subitems"
243
- let pathParts = arrayPath.split('.');
244
+ let arrayValue;
245
+ let pathValid = true;
244
246
 
245
- // If we're in a nested context and the path starts with the parent loop variable,
246
- // strip it since the context IS that variable
247
- if (depth > 0 && parentLoopVar && pathParts.length > 1 && pathParts[0] === parentLoopVar) {
248
- console.log(`${indent}[processEachLoops] Stripping parent loop var "${parentLoopVar}" from path "${arrayPath}"`);
249
- pathParts = pathParts.slice(1);
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
+ }
251
270
 
252
- let arrayValue = contextData;
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
+ }
253
288
 
254
- // Navigate through path
255
- let pathValid = true;
256
- for (const part of pathParts) {
257
- if (arrayValue === null || arrayValue === undefined) {
258
- console.warn(`[Preview] Array path not found: ${arrayPath} (stopped at ${part})`);
259
- pathValid = false;
260
- break;
289
+ arrayValue = contextData;
290
+
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,25 +319,30 @@ class TemplateProcessor {
282
319
  console.log(`${indent}[Preview] Processing loop item #${index}: ${loopVar} in ${arrayPath}`, item);
283
320
  let itemHtml = content;
284
321
 
285
- // First, recursively process any nested loops with this item as context
286
- // Pass loopVar as parentLoopVar so nested loops can strip it from paths
287
- console.log(`${indent}[DEBUG] BEFORE nested processEachLoops - HTML snippet:`, itemHtml.substring(0, 300));
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
- loopContext[arrayName] = arrayValue;
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
- // First: Replace attribute expressions with quoted values (e.g., alt={item.image.alt} → alt="value")
303
- itemHtml = itemHtml.replace(new RegExp(`(\\w+)=\\{\\s*${loopVar}\\.([a-zA-Z0-9_.]+)\\s*\\}`, 'g'), (m, attrName, path) => {
343
+ // First: Replace attribute expressions with quoted values (e.g., alt={item.image.alt} or alt="{item.image.alt}" → alt="value")
344
+ // Pattern matches both: attr={expr} and attr="{expr}"
345
+ itemHtml = itemHtml.replace(new RegExp(`(\\w+)=["']?\\{\\s*${loopVar}\\.([a-zA-Z0-9_.]+)\\s*\\}["']?`, 'g'), (m, attrName, path) => {
304
346
  // Navigate through nested path (e.g., "image.src")
305
347
  const pathParts = path.split('.');
306
348
  let value = item;
@@ -312,8 +354,13 @@ class TemplateProcessor {
312
354
  value = value[part];
313
355
  }
314
356
 
357
+ // Handle link objects (from LinkField) - extract path for preview/production
358
+ if (typeof value === 'object' && value !== null && value.type === 'page' && value.pageUuid) {
359
+ // In preview mode: use /preview/{pageUuid}
360
+ value = `/preview/${value.pageUuid}`;
361
+ }
315
362
  // Handle image objects - extract URL
316
- if (typeof value === 'object' && value !== null && (value.src || value.filename)) {
363
+ else if (typeof value === 'object' && value !== null && (value.src || value.filename)) {
317
364
  value = value.src || value.filename || '';
318
365
  }
319
366
 
@@ -343,6 +390,11 @@ class TemplateProcessor {
343
390
  value = value[part];
344
391
  }
345
392
 
393
+ // Handle link objects (from LinkField) - extract path for preview
394
+ if (typeof value === 'object' && value !== null && value.type === 'page' && value.pageUuid) {
395
+ return `/preview/${value.pageUuid}`;
396
+ }
397
+
346
398
  // Handle image objects - extract URL
347
399
  if (typeof value === 'object' && value !== null && (value.src || value.filename)) {
348
400
  return value.src || value.filename || '';
@@ -382,14 +434,8 @@ class TemplateProcessor {
382
434
  });
383
435
 
384
436
  // Process {#if} conditionals within loop (supports {:else if})
385
- // Merge loop variable with parent context for nested conditionals
386
- const evalContext = { ...contextData, [loopVar]: item };
387
- if (indexVar) {
388
- evalContext[indexVar] = index;
389
- }
390
- // Add the array itself to context
391
- evalContext[arrayName] = arrayValue;
392
- 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);
393
439
 
394
440
  // Process complex expressions within loop (e.g., {item.image.src ? "Ja" : "Nein"})
395
441
  // This handles expressions that couldn't be processed by the simple regex replacements above
@@ -408,10 +454,14 @@ class TemplateProcessor {
408
454
 
409
455
  // Evaluate complex expressions with loop context
410
456
  try {
411
- const result = this.evaluateExpressionValue(expression, evalContext);
457
+ const result = this.evaluateExpressionValue(expression, loopContext);
412
458
  if (result === undefined || result === null) {
413
459
  return '';
414
460
  }
461
+ // Handle link objects (from LinkField) - extract path for preview
462
+ if (typeof result === 'object' && result !== null && result.type === 'page' && result.pageUuid) {
463
+ return `/preview/${result.pageUuid}`;
464
+ }
415
465
  // Handle image objects
416
466
  if (typeof result === 'object' && result !== null && (result.src || result.filename)) {
417
467
  return result.src || result.filename || '';