meno-core 1.0.45 → 1.0.46

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/build-astro.ts +214 -63
  2. package/dist/bin/cli.js +2 -2
  3. package/dist/build-static.js +7 -7
  4. package/dist/chunks/{chunk-NZTSJS5C.js → chunk-2QK6U5UK.js} +3 -2
  5. package/dist/chunks/{chunk-NZTSJS5C.js.map → chunk-2QK6U5UK.js.map} +2 -2
  6. package/dist/chunks/{chunk-BZQKEJQY.js → chunk-77ZB6353.js} +29 -18
  7. package/dist/chunks/chunk-77ZB6353.js.map +7 -0
  8. package/dist/chunks/{chunk-TVH3TC2T.js → chunk-C6U5T5S5.js} +6 -6
  9. package/dist/chunks/{chunk-5ZASE4IG.js → chunk-FED5MME6.js} +234 -11
  10. package/dist/chunks/{chunk-5ZASE4IG.js.map → chunk-FED5MME6.js.map} +3 -3
  11. package/dist/chunks/{chunk-5Z5VQRTJ.js → chunk-I7YIGZXT.js} +4 -4
  12. package/dist/chunks/{chunk-5Z5VQRTJ.js.map → chunk-I7YIGZXT.js.map} +2 -2
  13. package/dist/chunks/{chunk-OUNJ76QM.js → chunk-ORN7S4AP.js} +5 -5
  14. package/dist/chunks/{chunk-GYF3ABI3.js → chunk-UUA5LEWF.js} +3 -3
  15. package/dist/chunks/{chunk-GYF3ABI3.js.map → chunk-UUA5LEWF.js.map} +2 -2
  16. package/dist/chunks/{chunk-WQSG5WHC.js → chunk-ZTKHJQ2Z.js} +2 -2
  17. package/dist/chunks/{chunk-F7MA62WG.js → chunk-ZWYDT3QJ.js} +3 -3
  18. package/dist/chunks/{configService-6KTT6GRT.js → configService-DYCUEURL.js} +3 -3
  19. package/dist/chunks/{constants-L5IKLB6U.js → constants-GWBAD66U.js} +2 -2
  20. package/dist/entries/server-router.js +7 -7
  21. package/dist/lib/client/index.js +4 -4
  22. package/dist/lib/server/index.js +586 -142
  23. package/dist/lib/server/index.js.map +3 -3
  24. package/dist/lib/shared/index.js +7 -3
  25. package/dist/lib/shared/index.js.map +2 -2
  26. package/dist/lib/test-utils/index.js +1 -1
  27. package/lib/client/templateEngine.test.ts +64 -0
  28. package/lib/server/astro/astroEmitHelpers.ts +18 -0
  29. package/lib/server/astro/cmsPageEmitter.ts +31 -1
  30. package/lib/server/astro/componentEmitter.test.ts +59 -0
  31. package/lib/server/astro/componentEmitter.ts +43 -10
  32. package/lib/server/astro/cssCollector.ts +58 -11
  33. package/lib/server/astro/nodeToAstro.test.ts +397 -5
  34. package/lib/server/astro/nodeToAstro.ts +478 -63
  35. package/lib/server/astro/pageEmitter.ts +31 -1
  36. package/lib/server/astro/tailwindMapper.test.ts +119 -0
  37. package/lib/server/astro/tailwindMapper.ts +67 -1
  38. package/lib/server/runtime/httpServer.ts +12 -4
  39. package/lib/server/ssr/htmlGenerator.ts +1 -1
  40. package/lib/server/ssr/jsCollector.ts +2 -2
  41. package/lib/server/ssr/ssrRenderer.test.ts +32 -0
  42. package/lib/server/ssr/ssrRenderer.ts +26 -11
  43. package/lib/shared/constants.ts +1 -0
  44. package/lib/shared/cssGeneration.test.ts +109 -3
  45. package/lib/shared/cssGeneration.ts +98 -13
  46. package/lib/shared/cssNamedColors.ts +47 -0
  47. package/lib/shared/cssProperties.ts +2 -2
  48. package/lib/shared/index.ts +1 -0
  49. package/package.json +1 -1
  50. package/dist/chunks/chunk-BZQKEJQY.js.map +0 -7
  51. /package/dist/chunks/{chunk-TVH3TC2T.js.map → chunk-C6U5T5S5.js.map} +0 -0
  52. /package/dist/chunks/{chunk-OUNJ76QM.js.map → chunk-ORN7S4AP.js.map} +0 -0
  53. /package/dist/chunks/{chunk-WQSG5WHC.js.map → chunk-ZTKHJQ2Z.js.map} +0 -0
  54. /package/dist/chunks/{chunk-F7MA62WG.js.map → chunk-ZWYDT3QJ.js.map} +0 -0
  55. /package/dist/chunks/{configService-6KTT6GRT.js.map → configService-DYCUEURL.js.map} +0 -0
  56. /package/dist/chunks/{constants-L5IKLB6U.js.map → constants-GWBAD66U.js.map} +0 -0
@@ -29,6 +29,7 @@ import type {
29
29
  } from '../../shared/types/styles';
30
30
  import { responsiveStylesToTailwind, propertyToTailwind } from './tailwindMapper';
31
31
  import type { BreakpointConfig } from '../../shared/breakpoints';
32
+ import type { ResponsiveScales } from '../../shared/responsiveScaling';
32
33
  import { generateElementClassName, type ElementClassContext } from '../../shared/elementClassName';
33
34
  import { isVoidElement, hasIf, hasChildren, isSlotContent as isSlotContentNode } from '../../shared/nodeUtils';
34
35
  import { NODE_TYPE, RAW_HTML_PREFIX } from '../../shared/constants';
@@ -38,6 +39,7 @@ import { isI18nValue, resolveI18nValue, DEFAULT_I18N_CONFIG, buildLocalizedPath
38
39
  import { transformCMSTemplate, isTemplateExpression, transformItemTemplate, replaceItemMetaVars, rewriteItemVar } from './templateTransformer';
39
40
  import type { SlugMap } from '../../shared/slugTranslator';
40
41
  import { buildSlugIndex, translatePath } from '../../shared/slugTranslator';
42
+ import { stripRawHtmlPrefixDeep } from './astroEmitHelpers';
41
43
 
42
44
  // ---------------------------------------------------------------------------
43
45
  // Types
@@ -64,6 +66,8 @@ export interface AstroEmitContext {
64
66
  fileName: string;
65
67
  /** Breakpoint config for responsive Tailwind classes */
66
68
  breakpoints: BreakpointConfig;
69
+ /** Responsive scales config for auto-scaling spacing/typography/sizing */
70
+ responsiveScales?: ResponsiveScales;
67
71
  /** Dynamic tag definitions collected during traversal (for frontmatter) */
68
72
  dynamicTags?: Map<string, string>;
69
73
  /** Image metadata map for responsive image generation */
@@ -102,8 +106,27 @@ export interface AstroEmitContext {
102
106
  slugMappings?: SlugMap[];
103
107
  /** I18n default locale (for slug translation) */
104
108
  i18nDefaultLocale?: string;
109
+ /**
110
+ * Raw-HTML slice → processed HTML captured during the SSR pass for this page.
111
+ * Used by the RAW_HTML_PREFIX branch to emit rich-text content that has had
112
+ * image rewriting, component expansion, and link localization applied.
113
+ */
114
+ processedRawHtml?: Map<string, string>;
105
115
  /** Image format: 'webp' uses plain <img>, 'avif' uses <picture> with AVIF+WebP sources */
106
116
  imageFormat?: 'webp' | 'avif';
117
+ /**
118
+ * Static image imports collected during emission: varName → import path
119
+ * relative to the emitting file. Drives `import foo from '...'` in
120
+ * frontmatter and unlocks astro:assets <Picture> for static local images.
121
+ *
122
+ * Note: this is a Map (ref-shared across child ctx spreads), so mutations
123
+ * inside list/dynamic inner contexts remain visible to the parent. A
124
+ * separate boolean flag would be dropped by `{ ...ctx }` shallow copies —
125
+ * the presence of any entry here implies `import { Picture } from 'astro:assets'`.
126
+ */
127
+ imageImports?: Map<string, string>;
128
+ /** File depth (relative to src/pages or src/components) for import path computation */
129
+ fileDepth?: number;
107
130
  }
108
131
 
109
132
  // ---------------------------------------------------------------------------
@@ -206,7 +229,12 @@ function escapeJSX(s: string): string {
206
229
  * Escape a string for use inside a JS template literal
207
230
  */
208
231
  function escapeTemplateLiteral(s: string): string {
209
- return s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
232
+ return s
233
+ .replace(/\\/g, '\\\\')
234
+ .replace(/`/g, '\\`')
235
+ .replace(/\$\{/g, '\\${')
236
+ .replace(/\u2028/g, '\\u2028')
237
+ .replace(/\u2029/g, '\\u2029');
210
238
  }
211
239
 
212
240
  /**
@@ -311,7 +339,7 @@ function buildClassAndStyleExpression(
311
339
  ): { classExpr: string; styleAttr: string } {
312
340
  // Static Tailwind classes from non-mapping styles
313
341
  const result = style
314
- ? responsiveStylesToTailwind(style, ctx.breakpoints)
342
+ ? responsiveStylesToTailwind(style, ctx.breakpoints, ctx.responsiveScales)
315
343
  : { classes: [], dynamicStyles: {} };
316
344
  const staticClasses = result.classes;
317
345
  const dynamicStyles = result.dynamicStyles;
@@ -439,7 +467,7 @@ function resolveTemplate(text: string, ctx: AstroEmitContext): string {
439
467
  if (ctx.listItemBinding) propName = rewriteItemVar(propName, ctx.listItemBinding);
440
468
  if (ctx.listIndexVar) propName = replaceItemMetaVars(propName, ctx.listIndexVar, ctx.listSourceVar);
441
469
  // Rich-text props contain HTML - render unescaped via set:html
442
- if (ctx.componentProps[propName]?.type === 'rich-text') {
470
+ if (ctx.componentProps[propName]?.type === 'rich-text' || ctx.componentProps[propName]?.type === 'embed') {
443
471
  return `<Fragment set:html={${propName}} />`;
444
472
  }
445
473
  return `{${propName}}`;
@@ -475,6 +503,7 @@ function buildElementClass(
475
503
  });
476
504
  }
477
505
 
506
+
478
507
  /**
479
508
  * Build HTML attributes string from node attributes
480
509
  */
@@ -555,6 +584,7 @@ function buildAttributesString(
555
584
  * Format a prop value for Astro template usage
556
585
  */
557
586
  function formatPropValue(value: unknown): string {
587
+ value = stripRawHtmlPrefixDeep(value);
558
588
  if (typeof value === 'string') return `"${escapeJSX(value)}"`;
559
589
  if (typeof value === 'number') return `{${value}}`;
560
590
  if (typeof value === 'boolean') return `{${value}}`;
@@ -591,12 +621,13 @@ export function nodeToAstro(
591
621
  if (typeof node === 'object' && !Array.isArray(node) && isI18nValue(node)) {
592
622
  const resolved = resolveI18n(node, ctx);
593
623
  if (typeof resolved === 'string') {
594
- return `${ind(ctx)}${escapeJSX(resolved)}\n`;
624
+ // Delegate to the text-node path so RAW_HTML_PREFIX gets emitted via <Fragment set:html>.
625
+ return nodeToAstro(resolved, ctx);
595
626
  }
596
627
  // In component def without locale: wrap with r() resolver
597
628
  if (ctx.isComponentDef && isI18nValue(resolved)) {
598
629
  ctx.needsI18nResolver = true;
599
- return `${ind(ctx)}{r(${JSON.stringify(resolved)})}\n`;
630
+ return `${ind(ctx)}{r(${JSON.stringify(stripRawHtmlPrefixDeep(resolved))})}\n`;
600
631
  }
601
632
  // If resolution returned a non-string (e.g. array), stringify it
602
633
  return `${ind(ctx)}${String(resolved ?? '')}\n`;
@@ -617,9 +648,12 @@ export function nodeToAstro(
617
648
  if (hasTemplates(node) && ctx.isComponentDef) {
618
649
  return `${ind(ctx)}${resolveTemplate(node, ctx)}\n`;
619
650
  }
620
- // Raw HTML marker (from rich-text fields) - render unescaped via set:html
651
+ // Raw HTML marker (from rich-text fields) - render unescaped via set:html.
652
+ // Prefer the SSR-processed HTML when available so image rewriting, component
653
+ // expansion, and link localization match what meno-core renders at runtime.
621
654
  if (node.startsWith(RAW_HTML_PREFIX)) {
622
- const rawHtml = node.slice(RAW_HTML_PREFIX.length);
655
+ const rawSlice = node.slice(RAW_HTML_PREFIX.length);
656
+ const rawHtml = ctx.processedRawHtml?.get(rawSlice) ?? rawSlice;
623
657
  return `${ind(ctx)}<Fragment set:html={\`${escapeTemplateLiteral(rawHtml)}\`} />\n`;
624
658
  }
625
659
  return `${ind(ctx)}${escapeJSX(node)}\n`;
@@ -681,6 +715,12 @@ const IMG_OPACITY_PATTERN = /^opacity-/;
681
715
  */
682
716
  const DEFAULT_SIZES = '100vw';
683
717
 
718
+ /**
719
+ * Tailwind classes applied to the inner <img> so it fills its wrapper (<picture>)
720
+ * when a responsive/blurred image wraps the element.
721
+ */
722
+ const IMG_FILL_CLASSES = ['block', 'w-full', 'h-full'];
723
+
684
724
  /**
685
725
  * Split Tailwind classes into picture (layout) and img (visual) groups.
686
726
  * Layout classes go on <picture>, image-specific classes go on <img>.
@@ -702,10 +742,283 @@ function splitImageClasses(allClasses: string[]): { pictureClasses: string[]; im
702
742
  return { pictureClasses, imgClasses };
703
743
  }
704
744
 
745
+ /**
746
+ * Extract a pixel width from a style value. Returns null for %, vw, auto,
747
+ * or any other non-px value we can't translate into a sizes hint.
748
+ */
749
+ function extractPxWidth(val: unknown): number | null {
750
+ if (typeof val === 'number' && Number.isFinite(val) && val > 0) return val;
751
+ if (typeof val !== 'string') return null;
752
+ const match = val.trim().match(/^(\d+(?:\.\d+)?)px$/);
753
+ if (!match) return null;
754
+ const n = parseFloat(match[1]);
755
+ return n > 0 ? n : null;
756
+ }
757
+
758
+ /**
759
+ * Compute a `sizes` attribute from a node's responsive style + breakpoint config.
760
+ * Produces something like `(max-width: 540px) 400px, (max-width: 1024px) 500px, 700px`
761
+ * so the browser can pick the smallest srcset variant that covers the rendered width
762
+ * at each viewport. Falls back to `100vw` when no usable px width is available.
763
+ */
764
+ function computeSizesAttribute(
765
+ style: StyleObject | ResponsiveStyleObject | undefined,
766
+ breakpoints: BreakpointConfig
767
+ ): string {
768
+ if (!style) return DEFAULT_SIZES;
769
+
770
+ const responsive = isResponsiveStyle(style as any);
771
+ const baseStyle: StyleObject | undefined = responsive
772
+ ? (style as ResponsiveStyleObject).base
773
+ : (style as StyleObject);
774
+
775
+ // Base (desktop) width — the biggest viewport
776
+ const baseWidth = baseStyle ? extractPxWidth((baseStyle as StyleObject).width) : null;
777
+ // If even the base width is unknown, nothing we can do — use 100vw.
778
+ if (baseWidth == null) return DEFAULT_SIZES;
779
+
780
+ // Walk breakpoints from smallest to largest so the emitted media queries
781
+ // are ordered from narrowest to widest, with the unconditional base last.
782
+ const bpEntries = Object.entries(breakpoints).sort(
783
+ (a, b) => a[1].breakpoint - b[1].breakpoint
784
+ );
785
+
786
+ const parts: string[] = [];
787
+ // Track the "effective" width as we walk up: if a breakpoint doesn't set its
788
+ // own width, it inherits from the next step up, and ultimately from base.
789
+ // We walk ascending, so we need to resolve by looking from smallest bp up to base.
790
+ for (const [name, entry] of bpEntries) {
791
+ let effective: number | null = null;
792
+ if (responsive) {
793
+ // Look at this bp, then larger bps, then base
794
+ const bpStyle = (style as ResponsiveStyleObject)[name];
795
+ effective = bpStyle ? extractPxWidth((bpStyle as StyleObject).width) : null;
796
+ if (effective == null) {
797
+ // Inherit from next-larger breakpoint(s)
798
+ for (const [largerName, largerEntry] of bpEntries) {
799
+ if (largerEntry.breakpoint <= entry.breakpoint) continue;
800
+ const largerStyle = (style as ResponsiveStyleObject)[largerName];
801
+ const larger = largerStyle ? extractPxWidth((largerStyle as StyleObject).width) : null;
802
+ if (larger != null) {
803
+ effective = larger;
804
+ break;
805
+ }
806
+ }
807
+ }
808
+ if (effective == null) effective = baseWidth;
809
+ } else {
810
+ effective = baseWidth;
811
+ }
812
+ parts.push(`(max-width: ${entry.breakpoint}px) ${effective}px`);
813
+ }
814
+ parts.push(`${baseWidth}px`);
815
+ return parts.join(', ');
816
+ }
817
+
818
+ /**
819
+ * Merge additional CSS declarations into an existing ` style="..."` attribute
820
+ * string. If no style attribute exists yet, builds one. The inputs are raw CSS
821
+ * already escaped for HTML attribute context.
822
+ */
823
+ function injectInlineStyle(styleAttr: string, extraCss: string): string {
824
+ if (!extraCss) return styleAttr;
825
+ if (!styleAttr) return ` style="${extraCss}"`;
826
+ return styleAttr.replace(/style="([^"]*)"/, (_, existing: string) => {
827
+ const trimmed = existing.trimEnd();
828
+ const sep = trimmed.length > 0 && !trimmed.endsWith(';') ? ';' : '';
829
+ return `style="${existing}${sep}${extraCss}"`;
830
+ });
831
+ }
832
+
705
833
  /**
706
834
  * Emit an <img> node as a <picture> element with AVIF/WebP sources when
707
835
  * image metadata is available, or as a plain <img> with srcset otherwise.
708
836
  */
837
+ // Widths used for <Picture> srcset generation. Mirrors
838
+ // RESPONSIVE_WIDTHS in imageMetadata.ts.
839
+ const PICTURE_WIDTHS = [500, 800, 1080, 1600, 2400];
840
+
841
+ /**
842
+ * Turn an image path like `/images/sub/hero-photo.jpg` into a stable JS
843
+ * identifier (`imgSubHeroPhoto`) for use as an ESM import variable.
844
+ */
845
+ function imagePathToVarName(srcPath: string): string {
846
+ const stripped = srcPath.replace(/^\/+/, '').replace(/^images\//, '').replace(/\.[^.]+$/, '');
847
+ const parts = stripped.split(/[/_\-\s.]+/).filter(Boolean);
848
+ if (parts.length === 0) return 'imgAsset';
849
+ const camel = parts
850
+ .map((p, i) => {
851
+ const clean = p.replace(/[^a-zA-Z0-9]/g, '');
852
+ if (!clean) return '';
853
+ if (i === 0) return clean.toLowerCase();
854
+ return clean[0].toUpperCase() + clean.slice(1).toLowerCase();
855
+ })
856
+ .join('');
857
+ if (!camel) return 'imgAsset';
858
+ return 'img' + camel[0].toUpperCase() + camel.slice(1);
859
+ }
860
+
861
+ /**
862
+ * Compute an ESM import path relative to the emitting file for a Meno image
863
+ * URL. For file depth N (e.g. `src/pages/en/about.astro` is depth 1), the
864
+ * path is `'../'.repeat(N + 1) + 'assets/images/<rest>'`.
865
+ */
866
+ function imageImportPath(srcPath: string, fileDepth: number): string {
867
+ const rest = srcPath.replace(/^\/+/, '').replace(/^images\//, '');
868
+ const ups = '../'.repeat(Math.max(0, fileDepth) + 1);
869
+ return `${ups}assets/images/${rest}`;
870
+ }
871
+
872
+ /**
873
+ * Decide whether an image src can be switched to <Picture>. Only plain
874
+ * literal local paths (`/images/...`) with known metadata qualify — CMS
875
+ * templates, list-item bindings, component props, and remote URLs take the
876
+ * legacy <img>/<picture> path.
877
+ */
878
+ function isStaticImageSrc(src: string | undefined, ctx: AstroEmitContext): src is string {
879
+ if (!src) return false;
880
+ if (typeof src !== 'string') return false;
881
+ if (hasTemplates(src)) return false;
882
+ if (!src.startsWith('/images/')) return false;
883
+ if (!ctx.imageMetadataMap?.has(src)) return false;
884
+ return true;
885
+ }
886
+
887
+ /**
888
+ * Register (or reuse) an ESM image import on the emit context. Returns the
889
+ * JS variable name to reference in the generated template.
890
+ */
891
+ function registerStaticImageImport(src: string, ctx: AstroEmitContext): string {
892
+ if (!ctx.imageImports) ctx.imageImports = new Map();
893
+ const depth = ctx.fileDepth ?? 0;
894
+ const importPath = imageImportPath(src, depth);
895
+
896
+ for (const [existingName, existingPath] of ctx.imageImports) {
897
+ if (existingPath === importPath) return existingName;
898
+ }
899
+
900
+ const base = imagePathToVarName(src);
901
+ let name = base;
902
+ let counter = 2;
903
+ while (ctx.imageImports.has(name)) {
904
+ name = `${base}${counter++}`;
905
+ }
906
+ ctx.imageImports.set(name, importPath);
907
+ return name;
908
+ }
909
+
910
+ /**
911
+ * Split a static `classExpr` into the outer layout classes (for <picture> /
912
+ * wrapper) and the inner image classes (for the emitted <img> inside
913
+ * <Picture>). Mirrors `splitImageClasses()` used by the legacy emitter so
914
+ * object-cover / opacity-* keep landing on the <img>. Returns null when the
915
+ * class expression is dynamic (class:list) — callers fall back to div wrap.
916
+ */
917
+ function splitStaticClassExpr(
918
+ classExpr: string
919
+ ): { outerClasses: string[]; innerClasses: string[] } | null {
920
+ if (classExpr.includes('class:list=')) return null;
921
+ const classMatch = classExpr.match(/class="([^"]*)"/);
922
+ if (!classMatch) return { outerClasses: [], innerClasses: [] };
923
+ const all = classMatch[1].split(/\s+/).filter(Boolean);
924
+ const { pictureClasses, imgClasses } = splitImageClasses(all);
925
+ return { outerClasses: pictureClasses, innerClasses: imgClasses };
926
+ }
927
+
928
+ /**
929
+ * Format a static class+style pair as an Astro `pictureAttributes={{...}}`
930
+ * prop. `classValue` comes pre-split (outer layout classes only).
931
+ */
932
+ function formatPictureAttributesProp(
933
+ classValue: string,
934
+ styleAttr: string
935
+ ): string {
936
+ const styleMatch = styleAttr.match(/style="([^"]*)"/);
937
+ const parts: string[] = [];
938
+ if (classValue) parts.push(`class: ${JSON.stringify(classValue)}`);
939
+ if (styleMatch) parts.push(`style: ${JSON.stringify(styleMatch[1])}`);
940
+ if (parts.length === 0) return '';
941
+ return ` pictureAttributes={{${parts.join(', ')}}}`;
942
+ }
943
+
944
+ /**
945
+ * Emit a static local image as an astro:assets <Picture>. Called only after
946
+ * isStaticImageSrc() has passed. Non-static paths should never reach here.
947
+ */
948
+ function emitStaticPictureImage(
949
+ src: string,
950
+ alt: string | undefined,
951
+ loading: string | undefined,
952
+ fetchpriority: string | undefined,
953
+ sizesValue: string,
954
+ classExpr: string,
955
+ styleAttr: string,
956
+ ifExpr: string,
957
+ ifClose: string,
958
+ blurHash: string | undefined,
959
+ ctx: AstroEmitContext
960
+ ): string {
961
+ const varName = registerStaticImageImport(src, ctx);
962
+
963
+ const widthsLiteral = `[${PICTURE_WIDTHS.join(', ')}]`;
964
+ const altAttr = alt !== undefined ? ` alt="${escapeJSX(String(alt))}"` : ' alt=""';
965
+ const loadingAttr = loading ? ` loading="${escapeJSX(loading)}"` : '';
966
+ const fetchpriorityAttr = fetchpriority ? ` fetchpriority="${escapeJSX(fetchpriority)}"` : '';
967
+
968
+ // Split classes so object-cover / opacity land on the <img>, not the
969
+ // outer <picture>/wrapper. splitStaticClassExpr returns null for dynamic
970
+ // class:list — callers fall back to a <div> wrapper without splitting.
971
+ const split = splitStaticClassExpr(classExpr);
972
+
973
+ if (blurHash) {
974
+ // Blur placeholder: put the blur background-image + layout classes on
975
+ // the <picture> element via pictureAttributes. This mirrors the legacy
976
+ // path where <picture> was both the blur host and layout container.
977
+ // The inner <img> gets fill + img-specific classes and clears the blur
978
+ // on load via this.parentElement (direct parent = <picture>).
979
+ const blurCss = `background-image:url(${escapeJSX(blurHash)});background-size:cover`;
980
+ const blurStyleAttr = injectInlineStyle(styleAttr, blurCss);
981
+ const onloadAttr = ` onload="this.parentElement.style.backgroundImage=''"`;
982
+
983
+ if (split) {
984
+ const outerClassValue = split.outerClasses.join(' ');
985
+ const pictureAttrs = formatPictureAttributesProp(outerClassValue, blurStyleAttr);
986
+ const innerClasses = [...split.innerClasses, ...IMG_FILL_CLASSES];
987
+ const innerClassAttr = ` class="${innerClasses.join(' ')}"`;
988
+ return `${ifExpr}${ind(ctx)}<Picture${pictureAttrs} src={${varName}}${altAttr}${innerClassAttr} formats={['avif','webp']} widths={${widthsLiteral}} sizes="${escapeJSX(sizesValue)}"${loadingAttr}${fetchpriorityAttr}${onloadAttr} />\n${ifClose}`;
989
+ }
990
+
991
+ // Dynamic class:list: fall back to a <div> wrapper since we can't
992
+ // convert class:list to a pictureAttributes JS object.
993
+ const wrapperClassExpr = classExpr;
994
+ const wrapperStyleAttr = injectInlineStyle(styleAttr, blurCss);
995
+ const fillClassAttr = ` class="${IMG_FILL_CLASSES.join(' ')}"`;
996
+ return (
997
+ `${ifExpr}${ind(ctx)}<div${wrapperClassExpr}${wrapperStyleAttr}>\n` +
998
+ `${ind(ctx)} <Picture pictureAttributes={{class: "${IMG_FILL_CLASSES.join(' ')}"}} src={${varName}}${altAttr}${fillClassAttr} formats={['avif','webp']} widths={${widthsLiteral}} sizes="${escapeJSX(sizesValue)}"${loadingAttr}${fetchpriorityAttr} onload="this.parentElement.parentElement.style.backgroundImage=''" />\n` +
999
+ `${ind(ctx)}</div>\n${ifClose}`
1000
+ );
1001
+ }
1002
+
1003
+ // Non-blur case: forward outer classes via pictureAttributes so they land
1004
+ // on the <picture>, inner img-specific classes as `class` on <Picture>.
1005
+ if (split) {
1006
+ const outerClassValue = split.outerClasses.join(' ');
1007
+ const innerClassAttr = split.innerClasses.length > 0
1008
+ ? ` class="${split.innerClasses.join(' ')}"`
1009
+ : '';
1010
+ const pictureAttrs = formatPictureAttributesProp(outerClassValue, styleAttr);
1011
+ return `${ifExpr}${ind(ctx)}<Picture${pictureAttrs} src={${varName}}${altAttr}${innerClassAttr} formats={['avif','webp']} widths={${widthsLiteral}} sizes="${escapeJSX(sizesValue)}"${loadingAttr}${fetchpriorityAttr} />\n${ifClose}`;
1012
+ }
1013
+
1014
+ // Dynamic class:list: wrap in a <div> so the dynamic expression survives.
1015
+ return (
1016
+ `${ifExpr}${ind(ctx)}<div${classExpr}${styleAttr}>\n` +
1017
+ `${ind(ctx)} <Picture src={${varName}}${altAttr} formats={['avif','webp']} widths={${widthsLiteral}} sizes="${escapeJSX(sizesValue)}"${loadingAttr}${fetchpriorityAttr} />\n` +
1018
+ `${ind(ctx)}</div>\n${ifClose}`
1019
+ );
1020
+ }
1021
+
709
1022
  function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
710
1023
  const style = node.style as StyleObject | ResponsiveStyleObject | undefined;
711
1024
 
@@ -716,6 +1029,7 @@ function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
716
1029
  node.generateElementClass
717
1030
  ) {
718
1031
  elementClass = buildElementClass(ctx, node.label);
1032
+
719
1033
  }
720
1034
 
721
1035
  const { classExpr, styleAttr } = buildClassAndStyleExpression(
@@ -744,7 +1058,26 @@ function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
744
1058
  if (height === undefined && metadata.height) height = metadata.height;
745
1059
  }
746
1060
 
747
- const sizesValue = sizes || DEFAULT_SIZES;
1061
+ // Compute real sizes from responsive style widths; user override wins.
1062
+ const sizesValue = sizes || computeSizesAttribute(style, ctx.breakpoints);
1063
+
1064
+ // Static local image → astro:assets <Picture> (ESM import, Astro-managed
1065
+ // optimization). Predicate rejects CMS/list/template/remote src.
1066
+ if (isStaticImageSrc(src, ctx)) {
1067
+ return emitStaticPictureImage(
1068
+ src,
1069
+ alt,
1070
+ loading,
1071
+ fetchpriority,
1072
+ sizesValue,
1073
+ classExpr,
1074
+ styleAttr,
1075
+ emitIfOpen(node, ctx),
1076
+ emitIfClose(node, ctx),
1077
+ metadata?.blurHash,
1078
+ ctx
1079
+ );
1080
+ }
748
1081
 
749
1082
  // Build remaining attributes (exclude image-specific ones we handle manually)
750
1083
  const imageSpecificKeys = new Set(['src', 'alt', 'loading', 'width', 'height', 'sizes', 'srcset', 'fetchpriority']);
@@ -765,56 +1098,82 @@ function emitImageNode(node: HtmlNode, ctx: AstroEmitContext): string {
765
1098
  if (width !== undefined) imgAttrs += ` width="${escapeJSX(String(width))}"`;
766
1099
  if (height !== undefined) imgAttrs += ` height="${escapeJSX(String(height))}"`;
767
1100
 
768
- // Blur placeholder style
769
- let blurStyle = '';
770
- if (metadata?.blurHash) {
771
- blurStyle = ` style="background-image: url(${escapeJSX(metadata.blurHash)}); background-size: cover;" onload="this.style.backgroundImage=''"`;
772
- }
1101
+ const hasAvif = !!(metadata?.avifSrcset && ctx.imageFormat !== 'webp');
1102
+ const hasBlur = !!metadata?.blurHash;
1103
+ // Any time we need a wrapper to host the blur placeholder or <source> tags.
1104
+ const useWrapper = hasAvif || hasBlur;
1105
+
1106
+ // Blur placeholder is painted on the wrapper element (NOT the <img>, which
1107
+ // is a replaced element and doesn't paint CSS backgrounds reliably).
1108
+ // Once the real image loads we clear the background on the wrapper so the
1109
+ // low-quality placeholder disappears.
1110
+ const blurWrapperCss = hasBlur
1111
+ ? `background-image:url(${escapeJSX(metadata!.blurHash!)});background-size:cover`
1112
+ : '';
1113
+ const blurOnload = hasBlur
1114
+ ? ` onload="this.parentElement.style.backgroundImage=''"`
1115
+ : '';
773
1116
 
774
1117
  // Conditional rendering
775
1118
  const ifExpr = emitIfOpen(node, ctx);
776
1119
  const ifClose = emitIfClose(node, ctx);
777
1120
 
778
- // If we have AVIF srcset and format allows, render as <picture> with split classes
779
- if (metadata?.avifSrcset && ctx.imageFormat !== 'webp') {
780
- // Extract static classes from classExpr for splitting
781
- const classMatch = classExpr.match(/class="([^"]*)"/);
1121
+ if (useWrapper) {
1122
+ // The inner <img> must fill the picture wrapper; otherwise it renders at
1123
+ // its intrinsic size and the wrapper's layout (w/h/position) is wasted.
1124
+ const imgFillClasses = IMG_FILL_CLASSES.slice();
1125
+
782
1126
  const classListMatch = classExpr.match(/class:list={\[(.+)\]}/);
1127
+ const classMatch = classExpr.match(/class="([^"]*)"/);
783
1128
 
1129
+ let pictureClassExpr = '';
1130
+ let imgClassAttr = '';
784
1131
  if (classListMatch) {
785
- // Dynamic class:list - put it all on the picture, img gets none
786
- // (splitting dynamic classes is too complex; layout on picture is the safer default)
1132
+ // Dynamic class:list - put it all on the picture. The inner img only
1133
+ // needs fill classes (splitting dynamic class lists isn't worth it).
1134
+ pictureClassExpr = classExpr;
1135
+ imgClassAttr = ` class="${imgFillClasses.join(' ')}"`;
1136
+ } else {
1137
+ const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
1138
+ const { pictureClasses, imgClasses } = splitImageClasses(allClasses);
1139
+ const fullImgClasses = [...imgClasses, ...imgFillClasses];
1140
+ pictureClassExpr = pictureClasses.length > 0 ? ` class="${pictureClasses.join(' ')}"` : '';
1141
+ imgClassAttr = fullImgClasses.length > 0 ? ` class="${fullImgClasses.join(' ')}"` : '';
1142
+ }
1143
+
1144
+ const wrapperStyleAttr = injectInlineStyle(styleAttr, blurWrapperCss);
1145
+
1146
+ // With AVIF: full <picture> + <source> elements
1147
+ if (hasAvif) {
787
1148
  return (
788
- `${ifExpr}${ind(ctx)}<picture${classExpr}${styleAttr}>\n` +
789
- `${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata.avifSrcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
790
- `${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata.srcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
791
- `${ind(ctx)} <img${imgAttrs}${blurStyle}${otherAttrsStr} />\n` +
1149
+ `${ifExpr}${ind(ctx)}<picture${pictureClassExpr}${wrapperStyleAttr}>\n` +
1150
+ `${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata!.avifSrcset!)}" sizes="${escapeJSX(sizesValue)}" />\n` +
1151
+ `${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata!.srcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
1152
+ `${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurOnload}${otherAttrsStr} />\n` +
792
1153
  `${ind(ctx)}</picture>\n${ifClose}`
793
1154
  );
794
1155
  }
795
1156
 
796
- const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
797
- const { pictureClasses, imgClasses } = splitImageClasses(allClasses);
798
-
799
- const pictureClassAttr = pictureClasses.length > 0 ? ` class="${pictureClasses.join(' ')}"` : '';
800
- const imgClassAttr = imgClasses.length > 0 ? ` class="${imgClasses.join(' ')}"` : '';
801
-
1157
+ // No AVIF, but we still need the wrapper for blur. Put srcset on the <img>
1158
+ // itself since we're not using <source>.
1159
+ if (metadata?.srcset) {
1160
+ imgAttrs += ` srcset="${escapeJSX(metadata.srcset)}"`;
1161
+ imgAttrs += ` sizes="${escapeJSX(sizesValue)}"`;
1162
+ }
802
1163
  return (
803
- `${ifExpr}${ind(ctx)}<picture${pictureClassAttr}${styleAttr}>\n` +
804
- `${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata.avifSrcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
805
- `${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata.srcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
806
- `${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurStyle}${otherAttrsStr} />\n` +
1164
+ `${ifExpr}${ind(ctx)}<picture${pictureClassExpr}${wrapperStyleAttr}>\n` +
1165
+ `${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurOnload}${otherAttrsStr} />\n` +
807
1166
  `${ind(ctx)}</picture>\n${ifClose}`
808
1167
  );
809
1168
  }
810
1169
 
811
- // Fallback: regular img with WebP srcset if available
1170
+ // No wrapper needed: plain <img> with optional srcset/sizes
812
1171
  if (metadata?.srcset) {
813
1172
  imgAttrs += ` srcset="${escapeJSX(metadata.srcset)}"`;
814
1173
  imgAttrs += ` sizes="${escapeJSX(sizesValue)}"`;
815
1174
  }
816
1175
 
817
- return `${ifExpr}${ind(ctx)}<img${classExpr}${styleAttr}${imgAttrs}${blurStyle}${otherAttrsStr} />\n${ifClose}`;
1176
+ return `${ifExpr}${ind(ctx)}<img${classExpr}${styleAttr}${imgAttrs}${otherAttrsStr} />\n${ifClose}`;
818
1177
  }
819
1178
 
820
1179
  function emitHtmlNode(node: HtmlNode, ctx: AstroEmitContext): string {
@@ -857,6 +1216,7 @@ function emitHtmlNode(node: HtmlNode, ctx: AstroEmitContext): string {
857
1216
  node.generateElementClass
858
1217
  ) {
859
1218
  elementClass = buildElementClass(ctx, label);
1219
+
860
1220
  }
861
1221
 
862
1222
  const { classExpr, styleAttr } = buildClassAndStyleExpression(style, node.interactiveStyles as InteractiveStyles | undefined, elementClass, ctx);
@@ -929,16 +1289,24 @@ function emitComponentInstance(node: ComponentInstanceNode, ctx: AstroEmitContex
929
1289
  } else if (ctx.isComponentDef && isI18nValue(value)) {
930
1290
  // i18n object in component def — wrap with r() resolver (resolved at runtime via Astro.currentLocale)
931
1291
  ctx.needsI18nResolver = true;
932
- propParts.push(`${key}={r(${JSON.stringify(value)})}`);
1292
+ propParts.push(`${key}={r(${JSON.stringify(stripRawHtmlPrefixDeep(value))})}`);
933
1293
  } else {
934
- propParts.push(`${key}=${formatPropValue(value)}`);
1294
+ // Rich-text / embed props contain HTML/SVG — pass as template literal
1295
+ // so the component can render via set:html without entity escaping
1296
+ const targetInterface = ctx.globalComponents[name]?.component?.interface;
1297
+ const propDef = targetInterface?.[key];
1298
+ if (typeof value === 'string' && (propDef?.type === 'rich-text' || propDef?.type === 'embed')) {
1299
+ propParts.push(`${key}={\`${escapeTemplateLiteral(stripRawHtmlPrefixDeep(value) as string)}\`}`);
1300
+ } else {
1301
+ propParts.push(`${key}=${formatPropValue(value)}`);
1302
+ }
935
1303
  }
936
1304
  }
937
1305
  }
938
1306
 
939
1307
  // Instance-level style overrides as className (Tailwind)
940
1308
  if (node.style) {
941
- const { classes: instanceClasses } = responsiveStylesToTailwind(node.style as StyleObject | ResponsiveStyleObject, ctx.breakpoints);
1309
+ const { classes: instanceClasses } = responsiveStylesToTailwind(node.style as StyleObject | ResponsiveStyleObject, ctx.breakpoints, ctx.responsiveScales);
942
1310
  if (instanceClasses.length > 0) {
943
1311
  propParts.push(`class="${instanceClasses.join(' ')}"`);
944
1312
  }
@@ -946,7 +1314,11 @@ function emitComponentInstance(node: ComponentInstanceNode, ctx: AstroEmitContex
946
1314
 
947
1315
  const propsStr = propParts.length > 0 ? ' ' + propParts.join(' ') : '';
948
1316
 
949
- const children = emitChildren(node.children, ctx);
1317
+ // Reset elementPath to [0] when entering a component instance, matching SSR's
1318
+ // renderComponent behavior. This ensures hash-based element class names for
1319
+ // slot content are identical between SSR (CSS) and Astro (HTML).
1320
+ const childCtx = { ...ctx, elementPath: [0] };
1321
+ const children = emitChildren(node.children, childCtx);
950
1322
 
951
1323
  if (!children.trim()) {
952
1324
  return `${ifExpr}${ind(ctx)}<${name}${propsStr} />\n${emitIfClose(node, ctx)}`;
@@ -983,6 +1355,7 @@ function emitEmbedNode(node: EmbedNode, ctx: AstroEmitContext): string {
983
1355
  node.generateElementClass
984
1356
  ) {
985
1357
  elementClass = buildElementClass(ctx, node.label);
1358
+
986
1359
  }
987
1360
 
988
1361
  const { classExpr, styleAttr } = buildClassAndStyleExpression(style, node.interactiveStyles as InteractiveStyles | undefined, elementClass, ctx);
@@ -1055,6 +1428,7 @@ function emitLinkNode(node: LinkNode, ctx: AstroEmitContext): string {
1055
1428
  node.generateElementClass
1056
1429
  ) {
1057
1430
  elementClass = buildElementClass(ctx, node.label);
1431
+
1058
1432
  }
1059
1433
 
1060
1434
  // Build class expression with olink base class
@@ -1151,6 +1525,7 @@ function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
1151
1525
  node.generateElementClass
1152
1526
  ) {
1153
1527
  elementClass = buildElementClass(ctx, node.label);
1528
+
1154
1529
  }
1155
1530
 
1156
1531
  const { classExpr, styleAttr } = buildClassAndStyleExpression(
@@ -1163,6 +1538,24 @@ function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
1163
1538
  const src = node.src as string | undefined;
1164
1539
  const alt = node.alt as string | undefined;
1165
1540
 
1541
+ // Static local image → astro:assets <Picture>
1542
+ if (isStaticImageSrc(src, ctx)) {
1543
+ const staticMeta = ctx.imageMetadataMap!.get(src)!;
1544
+ return emitStaticPictureImage(
1545
+ src,
1546
+ alt,
1547
+ undefined,
1548
+ undefined,
1549
+ computeSizesAttribute(style, ctx.breakpoints),
1550
+ classExpr,
1551
+ styleAttr,
1552
+ '',
1553
+ '',
1554
+ staticMeta.blurHash,
1555
+ ctx
1556
+ );
1557
+ }
1558
+
1166
1559
  let imgAttrs = '';
1167
1560
  if (src) imgAttrs += ` src="${escapeJSX(String(src))}"`;
1168
1561
  if (alt !== undefined) imgAttrs += ` alt="${escapeJSX(String(alt))}"`;
@@ -1170,32 +1563,43 @@ function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
1170
1563
  // Check for image metadata for responsive images
1171
1564
  const metadata = src ? ctx.imageMetadataMap?.get(String(src)) : undefined;
1172
1565
 
1173
- if (metadata) {
1174
- let width = metadata.width;
1175
- let height = metadata.height;
1176
- if (width !== undefined) imgAttrs += ` width="${width}"`;
1177
- if (height !== undefined) imgAttrs += ` height="${height}"`;
1178
-
1179
- let blurStyle = '';
1180
- if (metadata.blurHash) {
1181
- blurStyle = ` style="background-image: url(${escapeJSX(metadata.blurHash)}); background-size: cover;" onload="this.style.backgroundImage=''"`;
1182
- }
1566
+ if (!metadata) {
1567
+ return `${ind(ctx)}<img${classExpr}${styleAttr}${imgAttrs} />\n`;
1568
+ }
1183
1569
 
1184
- const sizesValue = DEFAULT_SIZES;
1570
+ if (metadata.width !== undefined) imgAttrs += ` width="${metadata.width}"`;
1571
+ if (metadata.height !== undefined) imgAttrs += ` height="${metadata.height}"`;
1185
1572
 
1186
- if (metadata.avifSrcset && ctx.imageFormat !== 'webp') {
1187
- const classMatch = classExpr.match(/class="([^"]*)"/);
1188
- const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
1189
- const { pictureClasses, imgClasses } = splitImageClasses(allClasses);
1573
+ const sizesValue = computeSizesAttribute(style, ctx.breakpoints);
1190
1574
 
1191
- const pictureClassAttr = pictureClasses.length > 0 ? ` class="${pictureClasses.join(' ')}"` : '';
1192
- const imgClassAttr = imgClasses.length > 0 ? ` class="${imgClasses.join(' ')}"` : '';
1575
+ const hasAvif = !!(metadata.avifSrcset && ctx.imageFormat !== 'webp');
1576
+ const hasBlur = !!metadata.blurHash;
1577
+ const useWrapper = hasAvif || hasBlur;
1193
1578
 
1579
+ const blurWrapperCss = hasBlur
1580
+ ? `background-image:url(${escapeJSX(metadata.blurHash!)});background-size:cover`
1581
+ : '';
1582
+ const blurOnload = hasBlur
1583
+ ? ` onload="this.parentElement.style.backgroundImage=''"`
1584
+ : '';
1585
+
1586
+ if (useWrapper) {
1587
+ const imgFillClasses = IMG_FILL_CLASSES.slice();
1588
+ const classMatch = classExpr.match(/class="([^"]*)"/);
1589
+ const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
1590
+ const { pictureClasses, imgClasses } = splitImageClasses(allClasses);
1591
+ const fullImgClasses = [...imgClasses, ...imgFillClasses];
1592
+
1593
+ const pictureClassAttr = pictureClasses.length > 0 ? ` class="${pictureClasses.join(' ')}"` : '';
1594
+ const imgClassAttr = fullImgClasses.length > 0 ? ` class="${fullImgClasses.join(' ')}"` : '';
1595
+ const wrapperStyleAttr = injectInlineStyle(styleAttr, blurWrapperCss);
1596
+
1597
+ if (hasAvif) {
1194
1598
  return (
1195
- `${ind(ctx)}<picture${pictureClassAttr}${styleAttr}>\n` +
1196
- `${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata.avifSrcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
1599
+ `${ind(ctx)}<picture${pictureClassAttr}${wrapperStyleAttr}>\n` +
1600
+ `${ind(ctx)} <source type="image/avif" srcset="${escapeJSX(metadata.avifSrcset!)}" sizes="${escapeJSX(sizesValue)}" />\n` +
1197
1601
  `${ind(ctx)} <source type="image/webp" srcset="${escapeJSX(metadata.srcset)}" sizes="${escapeJSX(sizesValue)}" />\n` +
1198
- `${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurStyle} />\n` +
1602
+ `${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurOnload} />\n` +
1199
1603
  `${ind(ctx)}</picture>\n`
1200
1604
  );
1201
1605
  }
@@ -1204,6 +1608,16 @@ function emitImageTypeNode(node: any, ctx: AstroEmitContext): string {
1204
1608
  imgAttrs += ` srcset="${escapeJSX(metadata.srcset)}"`;
1205
1609
  imgAttrs += ` sizes="${escapeJSX(sizesValue)}"`;
1206
1610
  }
1611
+ return (
1612
+ `${ind(ctx)}<picture${pictureClassAttr}${wrapperStyleAttr}>\n` +
1613
+ `${ind(ctx)} <img${imgClassAttr}${imgAttrs}${blurOnload} />\n` +
1614
+ `${ind(ctx)}</picture>\n`
1615
+ );
1616
+ }
1617
+
1618
+ if (metadata.srcset) {
1619
+ imgAttrs += ` srcset="${escapeJSX(metadata.srcset)}"`;
1620
+ imgAttrs += ` sizes="${escapeJSX(sizesValue)}"`;
1207
1621
  }
1208
1622
 
1209
1623
  return `${ind(ctx)}<img${classExpr}${styleAttr}${imgAttrs} />\n`;
@@ -1370,6 +1784,7 @@ function emitLocaleListNode(node: LocaleListNode, ctx: AstroEmitContext): string
1370
1784
  let elementClass: string | null = null;
1371
1785
  if ((node.interactiveStyles && (node.interactiveStyles as InteractiveStyles).length > 0) || node.generateElementClass) {
1372
1786
  elementClass = buildElementClass(ctx, node.label);
1787
+
1373
1788
  }
1374
1789
 
1375
1790
  const { classExpr: containerClassExpr, styleAttr: containerStyleAttr } = buildClassAndStyleExpression(
@@ -1381,17 +1796,17 @@ function emitLocaleListNode(node: LocaleListNode, ctx: AstroEmitContext): string
1381
1796
 
1382
1797
  // Build item classes
1383
1798
  const itemStyle = node.itemStyle as StyleObject | ResponsiveStyleObject | undefined;
1384
- const itemResult = itemStyle ? responsiveStylesToTailwind(itemStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
1799
+ const itemResult = itemStyle ? responsiveStylesToTailwind(itemStyle, ctx.breakpoints, ctx.responsiveScales) : { classes: [], dynamicStyles: {} };
1385
1800
  const itemClasses = itemResult.classes;
1386
1801
 
1387
1802
  // Build active item classes (item + active combined)
1388
1803
  const activeItemStyle = node.activeItemStyle as StyleObject | ResponsiveStyleObject | undefined;
1389
- const activeResult = activeItemStyle ? responsiveStylesToTailwind(activeItemStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
1804
+ const activeResult = activeItemStyle ? responsiveStylesToTailwind(activeItemStyle, ctx.breakpoints, ctx.responsiveScales) : { classes: [], dynamicStyles: {} };
1390
1805
  const activeItemClasses = [...itemClasses, ...activeResult.classes];
1391
1806
 
1392
1807
  // Build separator classes
1393
1808
  const separatorStyle = node.separatorStyle as StyleObject | ResponsiveStyleObject | undefined;
1394
- const sepResult = separatorStyle ? responsiveStylesToTailwind(separatorStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
1809
+ const sepResult = separatorStyle ? responsiveStylesToTailwind(separatorStyle, ctx.breakpoints, ctx.responsiveScales) : { classes: [], dynamicStyles: {} };
1395
1810
  const separatorClasses = sepResult.classes;
1396
1811
 
1397
1812
  // Build locale icon map
@@ -1404,7 +1819,7 @@ function emitLocaleListNode(node: LocaleListNode, ctx: AstroEmitContext): string
1404
1819
 
1405
1820
  // Flag classes
1406
1821
  const flagStyle = node.flagStyle as StyleObject | ResponsiveStyleObject | undefined;
1407
- const flagResult = flagStyle ? responsiveStylesToTailwind(flagStyle, ctx.breakpoints) : { classes: [], dynamicStyles: {} };
1822
+ const flagResult = flagStyle ? responsiveStylesToTailwind(flagStyle, ctx.breakpoints, ctx.responsiveScales) : { classes: [], dynamicStyles: {} };
1408
1823
  const flagClasses = flagResult.classes;
1409
1824
 
1410
1825
  // Build links