meno-core 1.0.40 → 1.0.41

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 (56) hide show
  1. package/bin/cli.ts +33 -0
  2. package/build-astro.ts +3 -27
  3. package/dist/bin/cli.js +30 -2
  4. package/dist/bin/cli.js.map +2 -2
  5. package/dist/build-static.js +7 -7
  6. package/dist/chunks/{chunk-3NOZVNM4.js → chunk-EQOSDQS2.js} +4 -4
  7. package/dist/chunks/{chunk-V4SVSX3X.js → chunk-IBR2F4IL.js} +4 -5
  8. package/dist/chunks/{chunk-V4SVSX3X.js.map → chunk-IBR2F4IL.js.map} +2 -2
  9. package/dist/chunks/{chunk-OJ5SROQN.js → chunk-IGVQF5GY.js} +8 -6
  10. package/dist/chunks/chunk-IGVQF5GY.js.map +7 -0
  11. package/dist/chunks/{chunk-Z7SAOCDG.js → chunk-LBWIHPN7.js} +5 -2
  12. package/dist/chunks/{chunk-Z7SAOCDG.js.map → chunk-LBWIHPN7.js.map} +2 -2
  13. package/dist/chunks/{chunk-A6KWUEA6.js → chunk-MKB2J6AD.js} +9 -1
  14. package/dist/chunks/chunk-MKB2J6AD.js.map +7 -0
  15. package/dist/chunks/{chunk-LOJLO2EY.js → chunk-S2HXJTAF.js} +1 -1
  16. package/dist/chunks/chunk-S2HXJTAF.js.map +7 -0
  17. package/dist/chunks/{chunk-GKICS7CF.js → chunk-SK3TLNUP.js} +120 -107
  18. package/dist/chunks/chunk-SK3TLNUP.js.map +7 -0
  19. package/dist/chunks/{chunk-MOCRENNU.js → chunk-SNUROC7E.js} +6 -6
  20. package/dist/chunks/{configService-TXBNUBBL.js → configService-MICL4S2L.js} +2 -2
  21. package/dist/chunks/{constants-L75FR445.js → constants-ZEU4TZCA.js} +2 -2
  22. package/dist/entries/server-router.js +7 -7
  23. package/dist/lib/client/index.js +9 -4
  24. package/dist/lib/client/index.js.map +2 -2
  25. package/dist/lib/server/index.js +76 -2966
  26. package/dist/lib/server/index.js.map +4 -4
  27. package/dist/lib/shared/index.js +3 -3
  28. package/dist/lib/test-utils/index.js +1 -1
  29. package/lib/client/core/ComponentBuilder.ts +1 -1
  30. package/lib/client/routing/Router.tsx +6 -0
  31. package/lib/client/templateEngine.test.ts +178 -0
  32. package/lib/client/templateEngine.ts +1 -2
  33. package/lib/server/astro/cmsPageEmitter.ts +3 -0
  34. package/lib/server/astro/componentEmitter.ts +60 -12
  35. package/lib/server/astro/nodeToAstro.test.ts +1101 -0
  36. package/lib/server/astro/nodeToAstro.ts +43 -4
  37. package/lib/server/astro/pageEmitter.ts +4 -0
  38. package/lib/server/index.ts +18 -4
  39. package/lib/server/services/configService.ts +12 -0
  40. package/lib/server/ssr/htmlGenerator.ts +0 -5
  41. package/lib/server/ssr/imageMetadata.ts +3 -3
  42. package/lib/server/ssr/ssrRenderer.ts +48 -19
  43. package/lib/shared/constants.ts +2 -0
  44. package/lib/shared/types/components.ts +8 -4
  45. package/lib/shared/validation/propValidator.ts +2 -1
  46. package/lib/shared/validation/schemas.ts +3 -1
  47. package/package.json +1 -1
  48. package/templates/index-router.html +0 -5
  49. package/dist/chunks/chunk-A6KWUEA6.js.map +0 -7
  50. package/dist/chunks/chunk-GKICS7CF.js.map +0 -7
  51. package/dist/chunks/chunk-LOJLO2EY.js.map +0 -7
  52. package/dist/chunks/chunk-OJ5SROQN.js.map +0 -7
  53. /package/dist/chunks/{chunk-3NOZVNM4.js.map → chunk-EQOSDQS2.js.map} +0 -0
  54. /package/dist/chunks/{chunk-MOCRENNU.js.map → chunk-SNUROC7E.js.map} +0 -0
  55. /package/dist/chunks/{configService-TXBNUBBL.js.map → configService-MICL4S2L.js.map} +0 -0
  56. /package/dist/chunks/{constants-L75FR445.js.map → constants-ZEU4TZCA.js.map} +0 -0
@@ -102,6 +102,8 @@ export interface AstroEmitContext {
102
102
  slugMappings?: SlugMap[];
103
103
  /** I18n default locale (for slug translation) */
104
104
  i18nDefaultLocale?: string;
105
+ /** Image format: 'webp' uses plain <img>, 'avif' uses <picture> with AVIF+WebP sources */
106
+ imageFormat?: 'webp' | 'avif';
105
107
  }
106
108
 
107
109
  // ---------------------------------------------------------------------------
@@ -773,8 +775,8 @@ function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
773
775
  const ifExpr = emitIfOpen(node, ctx);
774
776
  const ifClose = emitIfClose(node, ctx);
775
777
 
776
- // If we have AVIF srcset, render as <picture> with split classes
777
- if (metadata?.avifSrcset) {
778
+ // If we have AVIF srcset and format allows, render as <picture> with split classes
779
+ if (metadata?.avifSrcset && ctx.imageFormat !== 'webp') {
778
780
  // Extract static classes from classExpr for splitting
779
781
  const classMatch = classExpr.match(/class="([^"]*)"/);
780
782
  const classListMatch = classExpr.match(/class:list={\[(.+)\]}/);
@@ -998,6 +1000,31 @@ function emitEmbedNode(node: EmbedNode, ctx: AstroEmitContext): string {
998
1000
  }
999
1001
  }
1000
1002
 
1003
+ // Handle template expressions in HTML string (e.g., "{{icon}}")
1004
+ if (typeof node.html === 'string' && hasTemplates(node.html) && ctx.isComponentDef) {
1005
+ const fullMatch = node.html.match(/^\{\{(.+)\}\}$/);
1006
+ if (fullMatch) {
1007
+ let propRef = fullMatch[1].trim();
1008
+ if (ctx.listItemBinding) propRef = rewriteItemVar(propRef, ctx.listItemBinding);
1009
+ if (ctx.listIndexVar) propRef = replaceItemMetaVars(propRef, ctx.listIndexVar, ctx.listSourceVar);
1010
+
1011
+ let tplClassExpr = classExpr;
1012
+ if (!classExpr.includes('oem')) {
1013
+ if (classExpr) {
1014
+ tplClassExpr = classExpr.replace(/class="/, 'class="oem ').replace(/class:list={\['/, "class:list={['oem ");
1015
+ } else {
1016
+ tplClassExpr = ' class="oem"';
1017
+ }
1018
+ }
1019
+
1020
+ return (
1021
+ `${ifExpr}${ind(ctx)}<div${tplClassExpr}${attrs}>\n` +
1022
+ `${ind(ctx)} <Fragment set:html={${propRef}} />\n` +
1023
+ `${ind(ctx)}</div>\n${emitIfClose(node, ctx)}`
1024
+ );
1025
+ }
1026
+ }
1027
+
1001
1028
  const htmlStr = typeof node.html === 'string' ? node.html : '';
1002
1029
  const escapedHtml = escapeTemplateLiteral(htmlStr);
1003
1030
 
@@ -1156,7 +1183,7 @@ function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
1156
1183
 
1157
1184
  const sizesValue = DEFAULT_SIZES;
1158
1185
 
1159
- if (metadata.avifSrcset) {
1186
+ if (metadata.avifSrcset && ctx.imageFormat !== 'webp') {
1160
1187
  const classMatch = classExpr.match(/class="([^"]*)"/);
1161
1188
  const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
1162
1189
  const { pictureClasses, imgClasses } = splitImageClasses(allClasses);
@@ -1496,6 +1523,10 @@ function emitIfOpen(node: ComponentNode, ctx: AstroEmitContext): string {
1496
1523
  // Component prop context
1497
1524
  if (ctx.isComponentDef) {
1498
1525
  const fullMatch = ifValue.match(/^\{\{(.+)\}\}$/);
1526
+ if (!fullMatch && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(ifValue)) {
1527
+ // Plain string without template syntax and not a valid identifier — always truthy, skip conditional
1528
+ return '';
1529
+ }
1499
1530
  let expr = fullMatch ? fullMatch[1].trim() : ifValue.replace(/\{\{(.+?)\}\}/g, '$1');
1500
1531
  if (ctx.listItemBinding) expr = rewriteItemVar(expr, ctx.listItemBinding);
1501
1532
  if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
@@ -1519,7 +1550,15 @@ function emitIfClose(node: ComponentNode, ctx: AstroEmitContext): string {
1519
1550
  if (typeof ifValue === 'string') {
1520
1551
  const hasCmsCondition = ctx.cmsMode && ifValue.includes('{{cms.');
1521
1552
  const hasItemCondition = ctx.listItemBinding && /\{\{/.test(ifValue);
1522
- if (hasCmsCondition || hasItemCondition || ctx.isComponentDef) {
1553
+ if (hasCmsCondition || hasItemCondition) {
1554
+ return `${ind(ctx)})}\n`;
1555
+ }
1556
+ if (ctx.isComponentDef) {
1557
+ const fullMatch = ifValue.match(/^\{\{(.+)\}\}$/);
1558
+ if (!fullMatch && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(ifValue)) {
1559
+ // Matches the skip in emitIfOpen — no open brace was emitted
1560
+ return '';
1561
+ }
1523
1562
  return `${ind(ctx)})}\n`;
1524
1563
  }
1525
1564
  }
@@ -51,6 +51,8 @@ export interface PageEmitOptions {
51
51
  currentPageSlugMap?: Record<string, string>;
52
52
  /** Slug mappings for translating internal link hrefs */
53
53
  slugMappings?: SlugMap[];
54
+ /** Image format: 'webp' uses plain <img>, 'avif' uses <picture> */
55
+ imageFormat?: 'webp' | 'avif';
54
56
  }
55
57
 
56
58
  // ---------------------------------------------------------------------------
@@ -102,6 +104,7 @@ export function emitAstroPage(options: PageEmitOptions): string {
102
104
  i18nConfig,
103
105
  currentPageSlugMap,
104
106
  slugMappings,
107
+ imageFormat,
105
108
  } = options;
106
109
 
107
110
  const breakpoints = breakpointsOpt ?? DEFAULT_BREAKPOINTS;
@@ -132,6 +135,7 @@ export function emitAstroPage(options: PageEmitOptions): string {
132
135
  astroImports: new Set<string>(),
133
136
  slugMappings,
134
137
  i18nDefaultLocale: i18nConfig?.defaultLocale,
138
+ imageFormat,
135
139
  };
136
140
 
137
141
  // Emit the template body
@@ -17,7 +17,7 @@ export { generateBuildErrorPage, type BuildError, type BuildErrorsData } from '.
17
17
  export { PageService } from './services/pageService';
18
18
  export { ComponentService, type ComponentInfo } from './services/componentService';
19
19
  export { CMSService, type ReferenceLocation } from './services/cmsService';
20
- export { configService, ConfigService } from './services/configService';
20
+ export { configService, ConfigService, type ImageFormat } from './services/configService';
21
21
  export { ColorService, colorService } from './services/ColorService';
22
22
  export { VariableService, variableService } from './services/VariableService';
23
23
  export { EnumService, enumService } from './services/EnumService';
@@ -62,9 +62,6 @@ export { migrateTemplatesDirectory } from './migrateTemplates';
62
62
  // Static build
63
63
  export { buildStaticPages } from '../../build-static';
64
64
 
65
- // Astro export
66
- export { buildAstroProject } from '../../build-astro';
67
-
68
65
  // Webflow export
69
66
  export { buildWebflowPayload } from './webflow';
70
67
  export type {
@@ -74,6 +71,23 @@ export type {
74
71
  WebflowStyleClass,
75
72
  } from './webflow';
76
73
 
74
+ // JSON loaders
75
+ export {
76
+ loadJSONFile,
77
+ loadComponentDirectory,
78
+ mapPageNameToPath,
79
+ parseJSON,
80
+ loadI18nConfig,
81
+ loadBreakpointConfig,
82
+ loadResponsiveScalesConfig,
83
+ } from './jsonLoader';
84
+
85
+ // CSS generation
86
+ export { generateThemeColorVariablesCSS, generateVariablesCSS } from './cssGenerator';
87
+
88
+ // Font utilities
89
+ export { generateFontCSS, generateFontPreloadTags } from '../shared/fontLoader';
90
+
77
91
  // Utilities
78
92
  export * from './utils';
79
93
 
@@ -28,6 +28,8 @@ export interface IconsConfig {
28
28
  /**
29
29
  * Raw project config structure from project.config.json
30
30
  */
31
+ export type ImageFormat = 'webp' | 'avif';
32
+
31
33
  interface RawProjectConfig {
32
34
  breakpoints?: BreakpointConfigInput;
33
35
  responsiveScales?: Partial<ResponsiveScales>;
@@ -36,6 +38,7 @@ interface RawProjectConfig {
36
38
  libraries?: LibrariesConfig;
37
39
  csp?: CSPConfig;
38
40
  baseComponent?: string;
41
+ imageFormat?: ImageFormat;
39
42
  }
40
43
 
41
44
  /**
@@ -253,6 +256,15 @@ export class ConfigService {
253
256
  return this.config.baseComponent;
254
257
  }
255
258
 
259
+ /**
260
+ * Get image format setting
261
+ * Returns 'webp' (default) or 'avif'
262
+ */
263
+ getImageFormat(): ImageFormat {
264
+ if (this.config?.imageFormat === 'avif') return 'avif';
265
+ return 'webp';
266
+ }
267
+
256
268
  /**
257
269
  * Get raw config value by key (for extension)
258
270
  */
@@ -396,11 +396,6 @@ button {
396
396
  cursor: pointer;
397
397
  outline: inherit;
398
398
  }
399
- img {
400
- display: block;
401
- width: 100%;
402
- height: 100%;
403
- }
404
399
  picture {
405
400
  display: block;
406
401
  }
@@ -135,7 +135,7 @@ export function extractImgSrc(imgTag: string): string | null {
135
135
  * Only rewrites images that exist in the metadata map (project images with generated variants).
136
136
  * External/unrecognized images are left unchanged.
137
137
  */
138
- export function rewriteRichTextImages(html: string, metadataMap: ImageMetadataMap): string {
138
+ export function rewriteRichTextImages(html: string, metadataMap: ImageMetadataMap, imageFormat?: 'webp' | 'avif'): string {
139
139
  return html.replace(/<img\b([^>]*)\/?>/gi, (fullMatch, innerAttrs: string) => {
140
140
  const src = extractImgSrc(fullMatch);
141
141
  if (!src) return fullMatch;
@@ -158,8 +158,8 @@ export function rewriteRichTextImages(html: string, metadataMap: ImageMetadataMa
158
158
 
159
159
  const sizes = DEFAULT_SIZES;
160
160
 
161
- // Render as <picture> with AVIF + WebP sources when AVIF is available
162
- if (metadata.avifSrcset) {
161
+ // Render as <picture> with AVIF + WebP sources when AVIF is available and format allows
162
+ if (metadata.avifSrcset && imageFormat !== 'webp') {
163
163
  return `<picture>` +
164
164
  `<source type="image/avif" srcset="${escapeHtml(metadata.avifSrcset)}" sizes="${escapeHtml(sizes)}" />` +
165
165
  `<source type="image/webp" srcset="${escapeHtml(metadata.srcset)}" sizes="${escapeHtml(sizes)}" />` +
@@ -12,6 +12,7 @@ import type { BreakpointConfig } from '../../shared/breakpoints';
12
12
  import { evaluateTemplate, processStructure, isResponsiveStyle, isHtmlMapping, resolveHtmlMapping } from '../../client/templateEngine';
13
13
  import { resolvePropsFromDefinition, isRichTextMarker, richTextMarkerToHtml } from '../../shared/propResolver';
14
14
  import { loadBreakpointConfig, loadI18nConfig } from '../jsonLoader';
15
+ import { configService } from '../services/configService';
15
16
  import type { I18nConfig } from '../../shared/types/components';
16
17
  import { extractLocaleFromPath, DEFAULT_I18N_CONFIG, resolveI18nValue, buildLocalizedPath, isI18nValue } from '../../shared/i18n';
17
18
  import { NODE_TYPE } from '../../shared/constants';
@@ -118,6 +119,8 @@ interface SSRContext {
118
119
  isProductionBuild?: boolean;
119
120
  /** Collector for SSR fallback HTML of complex nodes (list, locale-list) keyed by element path */
120
121
  ssrFallbackCollector?: Map<string, string>;
122
+ /** Image format: 'webp' uses plain <img>, 'avif' uses <picture> with AVIF+WebP sources */
123
+ imageFormat?: 'webp' | 'avif';
121
124
  }
122
125
 
123
126
  /**
@@ -408,6 +411,7 @@ export async function buildComponentHTML(
408
411
  neededCollections, // Track collections that need client-side data
409
412
  isProductionBuild,
410
413
  ssrFallbackCollector, // Collect SSR fallback HTML for complex nodes
414
+ imageFormat: configService.getImageFormat(),
411
415
  };
412
416
 
413
417
  const html = await renderNode(node, ctx);
@@ -800,7 +804,7 @@ async function renderNode(
800
804
  // Check for raw HTML marker (from rich-text fields) - don't escape
801
805
  if (text.startsWith(RAW_HTML_PREFIX)) {
802
806
  let rawHtml = text.slice(RAW_HTML_PREFIX.length);
803
- if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap);
807
+ if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap, ctx.imageFormat);
804
808
  rawHtml = await expandRichTextComponents(rawHtml, ctx);
805
809
  rawHtml = localizeRichTextLinks(rawHtml, ctx);
806
810
  return rawHtml;
@@ -820,7 +824,7 @@ async function renderNode(
820
824
  // Check for raw HTML marker (from rich-text fields) - don't escape
821
825
  if (text.startsWith(RAW_HTML_PREFIX)) {
822
826
  let rawHtml = text.slice(RAW_HTML_PREFIX.length);
823
- if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap);
827
+ if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap, ctx.imageFormat);
824
828
  rawHtml = await expandRichTextComponents(rawHtml, ctx);
825
829
  rawHtml = localizeRichTextLinks(rawHtml, ctx);
826
830
  return rawHtml;
@@ -892,7 +896,7 @@ async function renderNode(
892
896
  KEEP_CONTENT: true
893
897
  }) : htmlContent;
894
898
  const optimizedHtml = ctx.imageMetadataMap
895
- ? rewriteRichTextImages(sanitizedHtml, ctx.imageMetadataMap)
899
+ ? rewriteRichTextImages(sanitizedHtml, ctx.imageMetadataMap, ctx.imageFormat)
896
900
  : sanitizedHtml;
897
901
 
898
902
  // Extract attributes from node
@@ -1227,27 +1231,52 @@ async function renderComponent(
1227
1231
  // Merge instance style overrides, className, and attributes
1228
1232
  // processedStructure is typically an HTML node (the component's root element)
1229
1233
  // Handle both component nodes and HTML nodes
1230
- const rootNode = processedStructure as ComponentNode & { props?: Record<string, unknown> };
1234
+ const rootNode = processedStructure as ComponentNode & { props?: Record<string, unknown>; style?: any; attributes?: Record<string, string | number | boolean> };
1231
1235
  if (isComponentNode(rootNode) || isHtmlNode(rootNode)) {
1232
1236
  if (!rootNode.props) {
1233
1237
  rootNode.props = {};
1234
1238
  }
1235
1239
 
1236
- if (rootNode.props.style && typeof rootNode.props.style === 'object') {
1237
- rootNode.props.style = {
1238
- ...(rootNode.props.style as Record<string, unknown>),
1239
- ...(propsWithStyleAndAttrs.style as Record<string, unknown> || {})
1240
- };
1241
- } else if (propsWithStyleAndAttrs.style) {
1242
- rootNode.props.style = propsWithStyleAndAttrs.style;
1240
+ // For HTML root nodes, merge styles into node.style (where the renderer reads them)
1241
+ if (isHtmlNode(rootNode)) {
1242
+ if (propsWithStyleAndAttrs.style) {
1243
+ const existingStyle = rootNode.style;
1244
+ if (existingStyle && typeof existingStyle === 'object') {
1245
+ rootNode.style = {
1246
+ ...(existingStyle as Record<string, any>),
1247
+ ...(propsWithStyleAndAttrs.style as Record<string, any>)
1248
+ };
1249
+ } else {
1250
+ rootNode.style = propsWithStyleAndAttrs.style as any;
1251
+ }
1252
+ }
1253
+ } else {
1254
+ // For component root nodes, merge into props.style
1255
+ if (rootNode.props.style && typeof rootNode.props.style === 'object') {
1256
+ rootNode.props.style = {
1257
+ ...(rootNode.props.style as Record<string, unknown>),
1258
+ ...(propsWithStyleAndAttrs.style as Record<string, unknown> || {})
1259
+ };
1260
+ } else if (propsWithStyleAndAttrs.style) {
1261
+ rootNode.props.style = propsWithStyleAndAttrs.style;
1262
+ }
1243
1263
  }
1244
1264
 
1245
1265
  // Merge className from component instance (includes responsive utility classes)
1246
1266
  if (propsWithStyleAndAttrs.className) {
1247
- const existingClassName = rootNode.props.className || '';
1248
- rootNode.props.className = existingClassName
1249
- ? `${existingClassName} ${propsWithStyleAndAttrs.className}`
1250
- : propsWithStyleAndAttrs.className;
1267
+ if (isHtmlNode(rootNode)) {
1268
+ // For HTML root nodes, merge into attributes where the renderer reads them
1269
+ if (!rootNode.attributes) rootNode.attributes = {};
1270
+ const existingClass = (rootNode.attributes.class || '') as string;
1271
+ rootNode.attributes.class = existingClass
1272
+ ? `${existingClass} ${propsWithStyleAndAttrs.className}`
1273
+ : propsWithStyleAndAttrs.className as string;
1274
+ } else {
1275
+ const existingClassName = rootNode.props.className || '';
1276
+ rootNode.props.className = existingClassName
1277
+ ? `${existingClassName} ${propsWithStyleAndAttrs.className}`
1278
+ : propsWithStyleAndAttrs.className;
1279
+ }
1251
1280
  }
1252
1281
 
1253
1282
  // Merge attributes into props
@@ -1440,8 +1469,8 @@ function renderImageElement(
1440
1469
 
1441
1470
  // Collect high-priority images for preloading in head
1442
1471
  if (fetchpriority === 'high' && metadata && ctx.preloadImages) {
1443
- // Prefer AVIF, fallback to WebP
1444
- if (metadata.avifSrcset) {
1472
+ // Prefer AVIF when format allows, fallback to WebP
1473
+ if (metadata.avifSrcset && ctx.imageFormat !== 'webp') {
1445
1474
  ctx.preloadImages.push({
1446
1475
  srcset: metadata.avifSrcset,
1447
1476
  type: 'image/avif',
@@ -1472,9 +1501,9 @@ function renderImageElement(
1472
1501
  blurStyle = ` style="background-image: url(${escapeHtml(metadata.blurHash)}); background-size: cover;" onload="this.style.backgroundImage=''"`;
1473
1502
  }
1474
1503
 
1475
- // Render as <picture> element if AVIF is available
1504
+ // Render as <picture> element if AVIF is available and imageFormat is 'avif'
1476
1505
  // Layout classes go on <picture> (block container), image-specific classes on <img>
1477
- if (metadata?.avifSrcset) {
1506
+ if (metadata?.avifSrcset && ctx.imageFormat !== 'webp') {
1478
1507
  // Image-specific class prefixes that should stay on <img>
1479
1508
  const imgClassPrefixes = [
1480
1509
  'objf-', 'objp-', 'flt-', 'tm-', 'bs-',
@@ -131,6 +131,7 @@ export const IFRAME_MESSAGE_TYPES = {
131
131
  PAGE_DATA_PREVIEW_REVERT: 'PAGE_DATA_PREVIEW_REVERT', // Editor → Iframe to revert preview
132
132
  PAGE_DATA_COMMITTED: 'PAGE_DATA_COMMITTED', // Editor → Iframe for committed mutations
133
133
  COMPONENT_DEFINITION_COMMITTED: 'COMPONENT_DEFINITION_COMMITTED', // Editor → Iframe for component definition updates
134
+ CSS_VARIABLE_UPDATE: 'CSS_VARIABLE_UPDATE', // Editor → Iframe for instant CSS variable preview
134
135
  } as const;
135
136
 
136
137
  // Component node type constants
@@ -152,6 +153,7 @@ export const SPECIAL_PATHS = {
152
153
  COMPONENT_JAVASCRIPT: 'component_javascript',
153
154
  COMPONENT_CSS: 'component_css',
154
155
  COMPONENT_LIBRARIES: 'component_libraries',
156
+ COMPONENT_ACCEPTS_STYLES: 'component_acceptsStyles',
155
157
  STRUCTURE_STYLE: 'structure_style',
156
158
  } as const;
157
159
 
@@ -132,6 +132,8 @@ export interface StructuredComponentDefinition {
132
132
  defineVars?: true | string[];
133
133
  /** External JS/CSS libraries required by this component */
134
134
  libraries?: LibrariesConfig;
135
+ /** Whether instances of this component can have styles applied to the wrapper */
136
+ acceptsStyles?: boolean;
135
137
  }
136
138
 
137
139
  /**
@@ -166,13 +168,14 @@ export interface JSONPage {
166
168
  /**
167
169
  * Page data type that can be either a JSONPage or a component definition structure
168
170
  */
169
- export type PageData = JSONPage | {
170
- component: {
171
- structure?: ComponentNode;
171
+ export type PageData = JSONPage | {
172
+ component: {
173
+ structure?: ComponentNode;
172
174
  interface?: Record<string, PropDefinition>;
173
175
  javascript?: string;
174
176
  css?: string;
175
- }
177
+ acceptsStyles?: boolean;
178
+ }
176
179
  };
177
180
 
178
181
  /**
@@ -185,5 +188,6 @@ export interface PageDataWithComponent {
185
188
  javascript?: string;
186
189
  css?: string;
187
190
  libraries?: LibrariesConfig;
191
+ acceptsStyles?: boolean;
188
192
  };
189
193
  }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import type { PropDefinition } from '../types/components';
8
8
  import { isI18nValue } from '../i18n';
9
+ import { isTiptapDocument } from '../richtext/types';
9
10
 
10
11
  /**
11
12
  * Validation error with context for prop validation
@@ -158,7 +159,7 @@ function validateSingleProp(
158
159
  // Rich-text accepts both strings and i18n values (i18n values will be resolved later)
159
160
  // Coerce numbers/booleans to strings (e.g. from CMS template resolution like {{item.value}})
160
161
  coercedValue = (typeof value === 'number' || typeof value === 'boolean') ? String(value) : value;
161
- typeValid = typeof coercedValue === 'string' || isI18nValue(coercedValue);
162
+ typeValid = typeof coercedValue === 'string' || isI18nValue(coercedValue) || isTiptapDocument(coercedValue);
162
163
  break;
163
164
 
164
165
  case 'file':
@@ -379,6 +379,7 @@ export const StructuredComponentDefinitionSchema = z.object({
379
379
  css: z.string().optional(),
380
380
  category: z.string().optional(),
381
381
  libraries: LibrariesConfigSchema.optional(),
382
+ acceptsStyles: z.boolean().optional(),
382
383
  }).passthrough();
383
384
 
384
385
  /**
@@ -477,7 +478,8 @@ export const PageDataWithComponentSchema = z.object({
477
478
  interface: z.record(z.string(), PropDefinitionSchema).optional(),
478
479
  javascript: z.string().optional(),
479
480
  css: z.string().optional(),
480
- }),
481
+ acceptsStyles: z.boolean().optional(),
482
+ }).passthrough(),
481
483
  }).passthrough();
482
484
 
483
485
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meno-core",
3
- "version": "1.0.40",
3
+ "version": "1.0.41",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "meno": "./dist/bin/cli.js"
@@ -18,11 +18,6 @@
18
18
  background: none;
19
19
  border: none;
20
20
  }
21
-
22
- img {
23
- width: 100%;
24
- height: 100%;
25
- }
26
21
  </style>
27
22
  </head>
28
23
  <body>
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../lib/server/services/configService.ts"],
4
- "sourcesContent": ["/**\n * Config Service\n * Centralized configuration loading and access\n *\n * Consolidates multiple config loaders into a single service that loads\n * the project.config.json file once and exposes typed sections.\n */\n\nimport type { BreakpointConfig, BreakpointConfigInput, BreakpointEntry } from '../../shared/breakpoints';\nimport { DEFAULT_BREAKPOINTS, normalizeBreakpointConfig } from '../../shared/breakpoints';\nimport type { ResponsiveScales, BreakpointScales } from '../../shared/responsiveScaling';\nimport { DEFAULT_RESPONSIVE_SCALES } from '../../shared/responsiveScaling';\nimport type { I18nConfig } from '../../shared/types/components';\nimport type { LibrariesConfig, JSLibraryConfig, CSSLibraryConfig } from '../../shared/types/libraries';\nimport type { CSPConfig } from '../../shared/types/config';\nimport { DEFAULT_I18N_CONFIG, migrateI18nConfig } from '../../shared/i18n';\nimport { projectPaths } from '../projectContext';\nimport { readTextFile, fileExists } from '../runtime';\n\n/**\n * Icons configuration\n */\nexport interface IconsConfig {\n favicon?: string;\n appleTouchIcon?: string;\n}\n\n/**\n * Raw project config structure from project.config.json\n */\ninterface RawProjectConfig {\n breakpoints?: BreakpointConfigInput;\n responsiveScales?: Partial<ResponsiveScales>;\n i18n?: unknown;\n icons?: IconsConfig;\n libraries?: LibrariesConfig;\n csp?: CSPConfig;\n baseComponent?: string;\n}\n\n/**\n * ConfigService\n * Loads project configuration once and provides typed access to sections\n */\nexport class ConfigService {\n private config: RawProjectConfig | null = null;\n private loaded = false;\n\n /**\n * Load configuration from project.config.json\n * Safe to call multiple times - only loads once\n */\n async load(): Promise<void> {\n if (this.loaded) {\n return;\n }\n\n try {\n if (await fileExists(projectPaths.config())) {\n const content = await readTextFile(projectPaths.config());\n this.config = JSON.parse(content);\n }\n } catch {\n // Fall through to defaults\n this.config = null;\n }\n\n this.loaded = true;\n }\n\n /**\n * Check if configuration has been loaded\n */\n isLoaded(): boolean {\n return this.loaded;\n }\n\n /**\n * Reset the service (for testing)\n */\n reset(): void {\n this.config = null;\n this.loaded = false;\n }\n\n /**\n * Get breakpoint configuration\n * Returns validated and normalized breakpoints (always object format)\n * Supports both legacy format { tablet: 1024 } and new format { tablet: { breakpoint: 1024, previewPoint: 768 } }\n */\n getBreakpoints(): BreakpointConfig {\n if (!this.config?.breakpoints || typeof this.config.breakpoints !== 'object') {\n return { ...DEFAULT_BREAKPOINTS };\n }\n\n // Validate breakpoint values before normalization\n const validInput: BreakpointConfigInput = {};\n for (const [key, value] of Object.entries(this.config.breakpoints)) {\n if (typeof value === 'number' && value > 0) {\n // Legacy format: number\n validInput[key] = value;\n } else if (typeof value === 'object' && value !== null) {\n // New format: object with breakpoint and optional previewPoint\n const entry = value as BreakpointEntry;\n if (typeof entry.breakpoint === 'number' && entry.breakpoint > 0) {\n validInput[key] = {\n breakpoint: entry.breakpoint,\n previewPoint: typeof entry.previewPoint === 'number' && entry.previewPoint > 0\n ? entry.previewPoint\n : entry.breakpoint,\n };\n }\n }\n }\n\n // Return normalized breakpoints or defaults if none valid\n if (Object.keys(validInput).length === 0) {\n return { ...DEFAULT_BREAKPOINTS };\n }\n\n return normalizeBreakpointConfig(validInput);\n }\n\n /**\n * Get i18n configuration\n * Automatically migrates old string[] format to LocaleConfig[] format\n */\n getI18n(): I18nConfig {\n if (!this.config?.i18n) {\n return { ...DEFAULT_I18N_CONFIG };\n }\n\n return migrateI18nConfig(this.config.i18n);\n }\n\n /**\n * Deep merge scale categories, preserving user-defined breakpoints\n * while filling in missing values from defaults\n */\n private mergeScaleCategory(\n userScales: BreakpointScales | undefined,\n defaultScales: BreakpointScales | undefined\n ): BreakpointScales | undefined {\n if (!userScales && !defaultScales) return undefined;\n if (!userScales) return defaultScales ? { ...defaultScales } : undefined;\n if (!defaultScales) return { ...userScales };\n\n // User scales take precedence, but include defaults for breakpoints not specified\n return {\n ...defaultScales,\n ...userScales,\n };\n }\n\n /**\n * Get responsive scales configuration\n * Supports dynamic breakpoints - scales are keyed by breakpoint name\n * Deep merges scale categories to preserve user breakpoint definitions\n */\n getResponsiveScales(): ResponsiveScales {\n if (!this.config?.responsiveScales || typeof this.config.responsiveScales !== 'object') {\n return { ...DEFAULT_RESPONSIVE_SCALES };\n }\n\n const userScales = this.config.responsiveScales;\n\n return {\n enabled: userScales.enabled ?? DEFAULT_RESPONSIVE_SCALES.enabled,\n baseReference: userScales.baseReference ?? DEFAULT_RESPONSIVE_SCALES.baseReference,\n fontSize: this.mergeScaleCategory(\n userScales.fontSize as BreakpointScales | undefined,\n DEFAULT_RESPONSIVE_SCALES.fontSize\n ),\n padding: this.mergeScaleCategory(\n userScales.padding as BreakpointScales | undefined,\n DEFAULT_RESPONSIVE_SCALES.padding\n ),\n margin: this.mergeScaleCategory(\n userScales.margin as BreakpointScales | undefined,\n DEFAULT_RESPONSIVE_SCALES.margin\n ),\n gap: this.mergeScaleCategory(\n userScales.gap as BreakpointScales | undefined,\n DEFAULT_RESPONSIVE_SCALES.gap\n ),\n };\n }\n\n /**\n * Get icons configuration\n * Returns empty object if not configured\n */\n getIcons(): IconsConfig {\n if (!this.config?.icons || typeof this.config.icons !== 'object') {\n return {};\n }\n\n return this.config.icons;\n }\n\n /**\n * Get libraries configuration\n * Returns empty arrays if not configured\n * Normalizes string URLs to object format for backwards compatibility\n */\n getLibraries(): LibrariesConfig {\n if (!this.config?.libraries || typeof this.config.libraries !== 'object') {\n return { js: [], css: [] };\n }\n\n const libs = this.config.libraries;\n\n // Normalize JS libraries: support both string URLs and object format\n const normalizedJs = Array.isArray(libs.js)\n ? libs.js.map((lib) =>\n typeof lib === 'string' ? { url: lib } : lib\n ) as JSLibraryConfig[]\n : [];\n\n // Normalize CSS libraries: support both string URLs and object format\n const normalizedCss = Array.isArray(libs.css)\n ? libs.css.map((lib) =>\n typeof lib === 'string' ? { url: lib } : lib\n ) as CSSLibraryConfig[]\n : [];\n\n return {\n js: normalizedJs,\n css: normalizedCss,\n };\n }\n\n /**\n * Get CSP configuration\n * Returns empty object if not configured\n */\n getCSP(): CSPConfig {\n if (!this.config?.csp || typeof this.config.csp !== 'object') {\n return {};\n }\n\n return this.config.csp;\n }\n\n /**\n * Get base component name for new pages\n * Returns undefined if not configured\n */\n getBaseComponent(): string | undefined {\n if (!this.config?.baseComponent || typeof this.config.baseComponent !== 'string') {\n return undefined;\n }\n return this.config.baseComponent;\n }\n\n /**\n * Get raw config value by key (for extension)\n */\n getRaw<T>(key: string): T | undefined {\n if (!this.config) {\n return undefined;\n }\n return (this.config as Record<string, unknown>)[key] as T | undefined;\n }\n}\n\n/**\n * Singleton instance for global access\n * Use this for convenience, or create your own instance for testing\n */\nexport const configService = new ConfigService();\n"],
5
- "mappings": "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",
6
- "names": []
7
- }