meno-core 1.0.48 → 1.0.49

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 (74) hide show
  1. package/dist/build-static.js +7 -7
  2. package/dist/chunks/{chunk-UUA5LEWF.js → chunk-6IVUG7FY.js} +138 -7
  3. package/dist/chunks/chunk-6IVUG7FY.js.map +7 -0
  4. package/dist/chunks/{chunk-B2RTLDXY.js → chunk-AZQYF6KE.js} +132 -1
  5. package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
  6. package/dist/chunks/{chunk-NKUV77SR.js → chunk-CHD5UCFF.js} +21 -9
  7. package/dist/chunks/{chunk-NKUV77SR.js.map → chunk-CHD5UCFF.js.map} +2 -2
  8. package/dist/chunks/{chunk-TPQ7APVQ.js → chunk-EQYDSPBB.js} +418 -62
  9. package/dist/chunks/chunk-EQYDSPBB.js.map +7 -0
  10. package/dist/chunks/{chunk-RQSTH2BS.js → chunk-H4JSCDNW.js} +2 -2
  11. package/dist/chunks/{chunk-EK4KESLU.js → chunk-J23ZX5AP.js} +8 -2
  12. package/dist/chunks/{chunk-EK4KESLU.js.map → chunk-J23ZX5AP.js.map} +2 -2
  13. package/dist/chunks/{chunk-D5E3OKSL.js → chunk-JER5NQVM.js} +5 -5
  14. package/dist/chunks/{chunk-BJRKEPMP.js → chunk-KPU2XHOS.js} +5 -2
  15. package/dist/chunks/{chunk-BJRKEPMP.js.map → chunk-KPU2XHOS.js.map} +2 -2
  16. package/dist/chunks/{chunk-NP76N4HQ.js → chunk-LKAGAQ3M.js} +2 -2
  17. package/dist/chunks/{chunk-3FHJUHAS.js → chunk-S2CX6HFM.js} +260 -25
  18. package/dist/chunks/chunk-S2CX6HFM.js.map +7 -0
  19. package/dist/chunks/{configService-IGJEC3MC.js → configService-CCA6AIDI.js} +3 -3
  20. package/dist/entries/server-router.js +9 -9
  21. package/dist/entries/server-router.js.map +2 -2
  22. package/dist/lib/client/index.js +54 -20
  23. package/dist/lib/client/index.js.map +3 -3
  24. package/dist/lib/server/index.js +9 -9
  25. package/dist/lib/shared/index.js +46 -10
  26. package/dist/lib/shared/index.js.map +3 -3
  27. package/entries/server-router.tsx +6 -2
  28. package/lib/client/core/ComponentBuilder.ts +8 -1
  29. package/lib/client/core/builders/embedBuilder.ts +15 -2
  30. package/lib/client/core/builders/linkNodeBuilder.ts +15 -2
  31. package/lib/client/core/builders/localeListBuilder.ts +17 -6
  32. package/lib/client/styles/StyleInjector.ts +3 -2
  33. package/lib/client/theme.ts +4 -4
  34. package/lib/server/cssGenerator.test.ts +64 -1
  35. package/lib/server/cssGenerator.ts +48 -9
  36. package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
  37. package/lib/server/providers/fileSystemCMSProvider.ts +200 -11
  38. package/lib/server/routes/index.ts +1 -1
  39. package/lib/server/routes/pages.ts +23 -1
  40. package/lib/server/services/cmsService.test.ts +246 -0
  41. package/lib/server/services/cmsService.ts +122 -5
  42. package/lib/server/services/configService.ts +5 -0
  43. package/lib/server/ssr/attributeBuilder.ts +41 -0
  44. package/lib/server/ssr/htmlGenerator.test.ts +113 -0
  45. package/lib/server/ssr/htmlGenerator.ts +51 -4
  46. package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
  47. package/lib/server/ssr/ssrRenderer.test.ts +306 -0
  48. package/lib/server/ssr/ssrRenderer.ts +182 -44
  49. package/lib/shared/cssGeneration.test.ts +267 -1
  50. package/lib/shared/cssGeneration.ts +240 -18
  51. package/lib/shared/cssProperties.test.ts +247 -1
  52. package/lib/shared/cssProperties.ts +196 -6
  53. package/lib/shared/interfaces/contentProvider.ts +39 -6
  54. package/lib/shared/pathSecurity.ts +16 -0
  55. package/lib/shared/responsiveScaling.test.ts +143 -0
  56. package/lib/shared/responsiveScaling.ts +253 -2
  57. package/lib/shared/themeDefaults.test.ts +3 -3
  58. package/lib/shared/themeDefaults.ts +3 -3
  59. package/lib/shared/types/cms.ts +28 -3
  60. package/lib/shared/types/index.ts +1 -0
  61. package/lib/shared/utilityClassConfig.ts +3 -0
  62. package/lib/shared/utilityClassMapper.test.ts +123 -0
  63. package/lib/shared/utilityClassMapper.ts +179 -8
  64. package/lib/shared/validation/schemas.ts +15 -1
  65. package/lib/shared/validation/validators.ts +26 -1
  66. package/package.json +1 -1
  67. package/dist/chunks/chunk-3FHJUHAS.js.map +0 -7
  68. package/dist/chunks/chunk-B2RTLDXY.js.map +0 -7
  69. package/dist/chunks/chunk-TPQ7APVQ.js.map +0 -7
  70. package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
  71. /package/dist/chunks/{chunk-RQSTH2BS.js.map → chunk-H4JSCDNW.js.map} +0 -0
  72. /package/dist/chunks/{chunk-D5E3OKSL.js.map → chunk-JER5NQVM.js.map} +0 -0
  73. /package/dist/chunks/{chunk-NP76N4HQ.js.map → chunk-LKAGAQ3M.js.map} +0 -0
  74. /package/dist/chunks/{configService-IGJEC3MC.js.map → configService-CCA6AIDI.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  configService
3
- } from "./chunk-BJRKEPMP.js";
3
+ } from "./chunk-KPU2XHOS.js";
4
4
  import {
5
5
  projectPaths,
6
6
  resolveProjectPath,
@@ -13,10 +13,12 @@ import {
13
13
  writeFile
14
14
  } from "./chunk-WQFG7PAH.js";
15
15
  import {
16
+ CMS_DRAFT_SUFFIX,
17
+ isReservedDraftFilename,
16
18
  isSafePathSegment,
17
19
  isValidIdentifier,
18
20
  resolvePaletteColor
19
- } from "./chunk-EK4KESLU.js";
21
+ } from "./chunk-J23ZX5AP.js";
20
22
  import {
21
23
  extractAttributesFromNode,
22
24
  isHtmlMapping,
@@ -24,7 +26,7 @@ import {
24
26
  processStructure,
25
27
  resolveHtmlMapping,
26
28
  skipEmptyTemplateAttributes
27
- } from "./chunk-NP76N4HQ.js";
29
+ } from "./chunk-LKAGAQ3M.js";
28
30
  import {
29
31
  DEFAULT_PREFETCH_CONFIG,
30
32
  SSRRegistry,
@@ -61,20 +63,26 @@ import {
61
63
  resolveTemplateRawValue,
62
64
  responsiveStylesToClasses,
63
65
  singularize,
66
+ validateCMSDraftItem,
64
67
  validateCMSItem,
65
68
  validateComponentDefinition
66
- } from "./chunk-3FHJUHAS.js";
69
+ } from "./chunk-S2CX6HFM.js";
67
70
  import {
68
71
  DEFAULT_BREAKPOINTS,
72
+ DEFAULT_FLUID_RANGE,
69
73
  DEFAULT_I18N_CONFIG,
74
+ DEFAULT_SITE_MARGIN,
75
+ buildFluidPropertyValue,
70
76
  buildLocalizedPath,
77
+ buildSiteMarginClamp,
71
78
  extractLocaleFromPath,
79
+ getSmallestBreakpointName,
72
80
  isI18nValue,
73
81
  migrateI18nConfig,
74
82
  normalizeBreakpointConfig,
75
83
  resolveI18nValue,
76
84
  scalePropertyValue
77
- } from "./chunk-B2RTLDXY.js";
85
+ } from "./chunk-AZQYF6KE.js";
78
86
  import {
79
87
  isTiptapDocument,
80
88
  tiptapToHtml
@@ -717,6 +725,7 @@ var CMSService = class {
717
725
  schemaCache = /* @__PURE__ */ new Map();
718
726
  routePatterns = [];
719
727
  provider;
728
+ previewMode;
720
729
  /** Item cache with TTL-based expiration */
721
730
  itemsCache = /* @__PURE__ */ new Map();
722
731
  /** Cache TTL in milliseconds (5 seconds) */
@@ -724,9 +733,11 @@ var CMSService = class {
724
733
  /**
725
734
  * Creates a new CMSService instance
726
735
  * @param provider - Optional CMSProvider for loading data (enables DI for testing)
736
+ * @param options - Service-level flags (preview mode for dev server)
727
737
  */
728
- constructor(provider) {
738
+ constructor(provider, options = {}) {
729
739
  this.provider = provider;
740
+ this.previewMode = options.previewMode === true;
730
741
  }
731
742
  /**
732
743
  * Set the CMS provider
@@ -737,9 +748,14 @@ var CMSService = class {
737
748
  this.provider = provider;
738
749
  }
739
750
  /**
740
- * Get items with caching
741
- * Returns cached items if available and not expired, otherwise fetches fresh data
742
- * Rich-text fields are automatically preprocessed to HTML for template interpolation
751
+ * Get items with caching, used by SSR-facing read methods.
752
+ *
753
+ * In preview mode (dev server) drafts are merged over published — drafts
754
+ * win on a per-`_filename` basis and draft-only items are included — so
755
+ * the editor preview reflects unpublished edits. Returns published-only
756
+ * otherwise. Rich-text fields are preprocessed to HTML for template
757
+ * interpolation either way.
758
+ *
743
759
  * @param collection - Collection ID to fetch items for
744
760
  * @returns Array of CMSItems with rich-text fields converted to HTML markers
745
761
  */
@@ -749,7 +765,20 @@ var CMSService = class {
749
765
  if (cached && now - cached.timestamp < this.ITEMS_CACHE_TTL) {
750
766
  return cached.items;
751
767
  }
752
- const rawItems = await this.provider.getItems(collection);
768
+ let rawItems = await this.provider.getItems(collection);
769
+ if (this.previewMode) {
770
+ const drafts = await this.provider.getAllDrafts(collection);
771
+ if (drafts.length > 0) {
772
+ const byFilename = /* @__PURE__ */ new Map();
773
+ for (const item of rawItems) {
774
+ if (item._filename) byFilename.set(item._filename, item);
775
+ }
776
+ for (const draft of drafts) {
777
+ if (draft._filename) byFilename.set(draft._filename, draft);
778
+ }
779
+ rawItems = Array.from(byFilename.values());
780
+ }
781
+ }
753
782
  const items = this.preprocessRichTextFields(collection, rawItems);
754
783
  this.itemsCache.set(collection, { items, timestamp: now });
755
784
  return items;
@@ -987,6 +1016,71 @@ var CMSService = class {
987
1016
  * Only clears items cache and provider cache before re-initializing.
988
1017
  * Schema/route caches are swapped atomically inside initialize().
989
1018
  */
1019
+ // ----------------------------------------------------------------------
1020
+ // Draft-version methods (Studio-only — never used by SSR / static export)
1021
+ // ----------------------------------------------------------------------
1022
+ /**
1023
+ * Load both published and draft versions of an item, used by the editor.
1024
+ * Returns `{}` when neither version exists.
1025
+ */
1026
+ async getItemVersions(collection, filename) {
1027
+ if (!this.provider) return {};
1028
+ const [published, draft] = await Promise.all([
1029
+ this.provider.getItemByFilename(collection, filename),
1030
+ this.provider.getDraft(collection, filename)
1031
+ ]);
1032
+ const result = {};
1033
+ if (published) result.published = published;
1034
+ if (draft) result.draft = draft;
1035
+ return result;
1036
+ }
1037
+ /**
1038
+ * List all items in a collection for the Studio item list:
1039
+ * - Published items annotated with `_hasDraft: true` when a draft sibling exists.
1040
+ * - Draft-only items (no published file yet) returned with `_isDraft: true`.
1041
+ */
1042
+ async listItemsWithDraftFlag(collection) {
1043
+ if (!this.provider) return [];
1044
+ const [published, drafts] = await Promise.all([
1045
+ this.provider.getItems(collection),
1046
+ this.provider.getAllDrafts(collection)
1047
+ ]);
1048
+ const draftFilenames = new Set(drafts.map((d) => d._filename).filter(Boolean));
1049
+ const annotatedPublished = published.map((item) => {
1050
+ if (item._filename && draftFilenames.has(item._filename)) {
1051
+ return { ...item, _hasDraft: true };
1052
+ }
1053
+ return item;
1054
+ });
1055
+ const publishedFilenames = new Set(published.map((i) => i._filename).filter(Boolean));
1056
+ const draftOnly = drafts.filter((d) => d._filename && !publishedFilenames.has(d._filename));
1057
+ return [...annotatedPublished, ...draftOnly];
1058
+ }
1059
+ /** Pass-through to provider; no caching (drafts are read fresh in the editor). */
1060
+ async getDraft(collection, filename) {
1061
+ if (!this.provider) return null;
1062
+ return this.provider.getDraft(collection, filename);
1063
+ }
1064
+ async hasDraft(collection, filename) {
1065
+ if (!this.provider) return false;
1066
+ return this.provider.hasDraft(collection, filename);
1067
+ }
1068
+ async saveDraft(collection, item) {
1069
+ if (!this.provider) throw new Error("CMS provider not configured");
1070
+ await this.provider.saveDraft(collection, item);
1071
+ this.itemsCache.delete(collection);
1072
+ }
1073
+ async discardDraft(collection, filename) {
1074
+ if (!this.provider) throw new Error("CMS provider not configured");
1075
+ await this.provider.discardDraft(collection, filename);
1076
+ this.itemsCache.delete(collection);
1077
+ }
1078
+ async publishDraft(collection, filename) {
1079
+ if (!this.provider) throw new Error("CMS provider not configured");
1080
+ const item = await this.provider.publishDraft(collection, filename);
1081
+ this.itemsCache.delete(collection);
1082
+ return item;
1083
+ }
990
1084
  async refreshSchemas() {
991
1085
  if (!this.provider) {
992
1086
  return;
@@ -1295,20 +1389,45 @@ ${cssVars.join("\n")}
1295
1389
  return cssBlocks.join("\n\n");
1296
1390
  }
1297
1391
  function generateVariablesCSS(config, breakpoints, responsiveScales) {
1298
- if (!config.variables || config.variables.length === 0) {
1392
+ const cssBlocks = [];
1393
+ const fluidActive = responsiveScales?.enabled === true && responsiveScales?.mode === "fluid";
1394
+ const fluidRange = responsiveScales?.fluidRange ?? DEFAULT_FLUID_RANGE;
1395
+ const fluidBaseRef = responsiveScales?.baseReference || 16;
1396
+ const smallestBp = fluidActive ? getSmallestBreakpointName(breakpoints) : null;
1397
+ const baseVars = (config.variables ?? []).map((v) => {
1398
+ if (fluidActive && smallestBp && v.type !== "none") {
1399
+ const categoryScales = responsiveScales?.[v.type];
1400
+ const scale = categoryScales?.[smallestBp];
1401
+ if (scale != null && scale !== 1) {
1402
+ const fluid = buildFluidPropertyValue(
1403
+ v.value,
1404
+ scale,
1405
+ fluidRange.min,
1406
+ fluidRange.max,
1407
+ fluidBaseRef
1408
+ );
1409
+ if (fluid) return ` ${v.cssVar}: ${fluid};`;
1410
+ }
1411
+ }
1412
+ return ` ${v.cssVar}: ${v.value};`;
1413
+ });
1414
+ if (fluidActive) {
1415
+ const siteMargin = responsiveScales?.siteMargin ?? DEFAULT_SITE_MARGIN;
1416
+ baseVars.push(` --site-margin: ${buildSiteMarginClamp(siteMargin, fluidRange)};`);
1417
+ }
1418
+ if (baseVars.length === 0) {
1299
1419
  return "";
1300
1420
  }
1301
- const cssBlocks = [];
1302
- const baseVars = config.variables.map((v) => ` ${v.cssVar}: ${v.value};`);
1303
1421
  cssBlocks.push(`:root {
1304
1422
  ${baseVars.join("\n")}
1305
1423
  }`);
1306
- if (breakpoints && responsiveScales?.enabled) {
1424
+ const userVariables = config.variables ?? [];
1425
+ if (breakpoints && responsiveScales?.enabled && !fluidActive && userVariables.length > 0) {
1307
1426
  const baseRef = responsiveScales.baseReference || 16;
1308
1427
  const sortedBreakpoints = Object.entries(breakpoints).sort((a, b) => b[1].breakpoint - a[1].breakpoint);
1309
1428
  for (const [bpName, bpEntry] of sortedBreakpoints) {
1310
1429
  const scaledVars = [];
1311
- for (const variable of config.variables) {
1430
+ for (const variable of userVariables) {
1312
1431
  if (variable.scales && variable.scales[bpName]) {
1313
1432
  const overrideValue = variable.scales[bpName];
1314
1433
  if (overrideValue !== variable.value) {
@@ -1389,6 +1508,27 @@ function buildAttributes(props, exclude = []) {
1389
1508
  }
1390
1509
  return attrs.length > 0 ? " " + attrs.join(" ") : "";
1391
1510
  }
1511
+ function buildEditorAttrs(opts) {
1512
+ const { elementPath } = opts;
1513
+ if (!elementPath) return "";
1514
+ const parts = [`data-element-path="${escapeHtml(elementPath.join(","))}"`];
1515
+ if (opts.cmsItemIndexPath && opts.cmsItemIndexPath.length > 0) {
1516
+ parts.push(`data-cms-item-index="${escapeHtml(opts.cmsItemIndexPath.join("."))}"`);
1517
+ if (opts.cmsListPaths && opts.cmsListPaths.length === opts.cmsItemIndexPath.length) {
1518
+ const ctx = JSON.stringify({ itemIndexPath: opts.cmsItemIndexPath, listPaths: opts.cmsListPaths });
1519
+ parts.push(`data-cms-context="${escapeHtml(ctx)}"`);
1520
+ }
1521
+ }
1522
+ if (opts.isCMSListContainer) parts.push(`data-cms-list="true"`);
1523
+ if (opts.isComponentRoot) parts.push(`data-component-root="true"`);
1524
+ if (opts.parentComponentName) {
1525
+ parts.push(`data-parent-component="${escapeHtml(opts.parentComponentName)}"`);
1526
+ }
1527
+ if (opts.componentContext && !opts.isSlotContent) {
1528
+ parts.push(`data-component-context="${escapeHtml(opts.componentContext)}"`);
1529
+ }
1530
+ return " " + parts.join(" ");
1531
+ }
1392
1532
  function styleToString(style) {
1393
1533
  if (!style || Object.keys(style).length === 0) return "";
1394
1534
  const declarations = [];
@@ -1901,6 +2041,12 @@ function getDOMPurify() {
1901
2041
  function getTemplateContext(ctx) {
1902
2042
  return ctx.templateContext || null;
1903
2043
  }
2044
+ function buildListResolutionScope(ctx) {
2045
+ const tplCtx = ctx.templateContext;
2046
+ const props = ctx.componentResolvedProps;
2047
+ if (!tplCtx && !props) return void 0;
2048
+ return { ...props ?? {}, ...tplCtx ?? {} };
2049
+ }
1904
2050
  function getI18nResolver(ctx) {
1905
2051
  return createI18nResolver(ctx.locale, ctx.i18nConfig);
1906
2052
  }
@@ -1939,7 +2085,11 @@ function processStyleToClasses(style, ctx) {
1939
2085
  getI18nResolver(ctx)
1940
2086
  );
1941
2087
  }
1942
- return responsiveStylesToClasses(processedStyle);
2088
+ const fluidActive = ctx.responsiveScales?.enabled === true && ctx.responsiveScales?.mode === "fluid";
2089
+ return responsiveStylesToClasses(
2090
+ processedStyle,
2091
+ { fluidActive, responsiveScales: ctx.responsiveScales }
2092
+ );
1943
2093
  }
1944
2094
  function evaluateIfCondition(node, ctx) {
1945
2095
  const ifValue = hasIf(node) ? node.if : void 0;
@@ -1971,31 +2121,31 @@ function evaluateIfCondition(node, ctx) {
1971
2121
  }
1972
2122
  return true;
1973
2123
  }
1974
- function resolveFilterValue(value, templateContext) {
1975
- if (!templateContext || typeof value !== "string" || !value.startsWith("{{") || !value.endsWith("}}")) {
2124
+ function resolveFilterValue(value, scope) {
2125
+ if (!scope || typeof value !== "string" || !value.startsWith("{{") || !value.endsWith("}}")) {
1976
2126
  return value;
1977
2127
  }
1978
2128
  const path2 = value.slice(2, -2).trim();
1979
- const resolved = getNestedValue(templateContext, path2);
2129
+ const resolved = getNestedValue(scope, path2);
1980
2130
  return resolved !== void 0 ? resolved : value;
1981
2131
  }
1982
- function resolveFilterTemplates(filter, templateContext) {
1983
- if (!filter || !templateContext) return filter;
2132
+ function resolveFilterTemplates(filter, scope) {
2133
+ if (!filter || !scope) return filter;
1984
2134
  if (Array.isArray(filter)) {
1985
2135
  return filter.map((cond) => ({
1986
2136
  ...cond,
1987
- value: resolveFilterValue(cond.value, templateContext)
2137
+ value: resolveFilterValue(cond.value, scope)
1988
2138
  }));
1989
2139
  }
1990
2140
  if ("field" in filter && "value" in filter) {
1991
2141
  return {
1992
2142
  ...filter,
1993
- value: resolveFilterValue(filter.value, templateContext)
2143
+ value: resolveFilterValue(filter.value, scope)
1994
2144
  };
1995
2145
  }
1996
2146
  const resolved = {};
1997
2147
  for (const [key, value] of Object.entries(filter)) {
1998
- resolved[key] = resolveFilterValue(value, templateContext);
2148
+ resolved[key] = resolveFilterValue(value, scope);
1999
2149
  }
2000
2150
  return resolved;
2001
2151
  }
@@ -2032,7 +2182,7 @@ async function expandRichTextComponents(html, ctx) {
2032
2182
  return resolved.join("");
2033
2183
  }
2034
2184
  var ssrComponentRegistry = new SSRRegistry();
2035
- async function buildComponentHTML(node, globalComponents = {}, pageComponents = {}, locale, i18nConfig, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild) {
2185
+ async function buildComponentHTML(node, globalComponents = {}, pageComponents = {}, locale, i18nConfig, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild, injectEditorAttrs) {
2036
2186
  const interactiveStylesMap = /* @__PURE__ */ new Map();
2037
2187
  const preloadImages = [];
2038
2188
  const neededCollections = /* @__PURE__ */ new Set();
@@ -2067,7 +2217,9 @@ async function buildComponentHTML(node, globalComponents = {}, pageComponents =
2067
2217
  // Collect SSR fallback HTML for complex nodes
2068
2218
  processedRawHtmlCollector,
2069
2219
  // Collect raw→processed HTML for Astro exporter
2070
- imageFormat: configService.getImageFormat()
2220
+ imageFormat: configService.getImageFormat(),
2221
+ injectEditorAttrs,
2222
+ responsiveScales: configService.getResponsiveScales()
2071
2223
  };
2072
2224
  const html = await renderNode(node, ctx);
2073
2225
  return { html, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector };
@@ -2098,7 +2250,7 @@ async function renderNestedListPlaceholder(node, ctx) {
2098
2250
  };
2099
2251
  const templateContent = node.children ? await renderChildrenAsync(node.children, childTemplateCtx) : "";
2100
2252
  const configJson = escapeHtml(JSON.stringify(config));
2101
- return `<div data-cms-list-nested="true" data-collection="${escapeHtml(sourceStr)}" data-cms-config="${configJson}"><template data-nested-template>${templateContent}</template></div>`;
2253
+ return `<div data-cms-list-nested="true" data-collection="${escapeHtml(sourceStr)}" data-cms-config="${configJson}"${editorAttrs(ctx, { isCMSListContainer: true })}><template data-nested-template>${templateContent}</template></div>`;
2102
2254
  }
2103
2255
  async function processList(node, ctx) {
2104
2256
  const nodeType = node.type;
@@ -2157,7 +2309,9 @@ async function processList(node, ctx) {
2157
2309
  );
2158
2310
  const itemCtx = {
2159
2311
  ...ctx,
2160
- templateContext
2312
+ templateContext,
2313
+ cmsItemIndexPath: [...ctx.cmsItemIndexPath ?? [], i],
2314
+ cmsListPaths: [...ctx.cmsListPaths ?? [], ctx.elementPath ?? []]
2161
2315
  };
2162
2316
  const childrenHtml = await renderChildrenAsync(node.children || [], itemCtx);
2163
2317
  renderedItems.push(childrenHtml);
@@ -2201,7 +2355,8 @@ async function getCollectionItems(node, source, ctx) {
2201
2355
  resolvedIds = Array.isArray(value) ? value.map((v) => String(v)) : String(value);
2202
2356
  }
2203
2357
  } else {
2204
- const parentContext = ctx.templateContext || { _type: "template" };
2358
+ const mergedScope = buildListResolutionScope(ctx) ?? {};
2359
+ const parentContext = { _type: "template", ...mergedScope };
2205
2360
  resolvedIds = resolveItemsTemplate(node.items, parentContext);
2206
2361
  }
2207
2362
  if (!resolvedIds) {
@@ -2219,7 +2374,7 @@ async function getCollectionItems(node, source, ctx) {
2219
2374
  } else {
2220
2375
  const query = {
2221
2376
  collection: source,
2222
- filter: resolveFilterTemplates(node.filter, ctx.templateContext),
2377
+ filter: resolveFilterTemplates(node.filter, buildListResolutionScope(ctx)),
2223
2378
  sort: node.sort,
2224
2379
  limit: node.limit,
2225
2380
  offset: node.offset,
@@ -2274,11 +2429,13 @@ function buildNodeElementClass(ctx, label, isSlotContent2) {
2274
2429
  const useComponentContext = !isSlotContent2 && Boolean(ctx.componentContext);
2275
2430
  const effectiveFileType = useComponentContext ? "component" : "page";
2276
2431
  const effectiveFileName = useComponentContext ? ctx.componentContext : pagePath ? pagePath.replace(/^\//, "").replace(/\//g, "_") || "index" : "page";
2432
+ const rawPath = ctx.elementPath || [];
2433
+ const path2 = useComponentContext && ctx.componentRootPath ? rawPath.slice(ctx.componentRootPath.length) : rawPath;
2277
2434
  const elementClassCtx = {
2278
2435
  fileType: effectiveFileType,
2279
2436
  fileName: effectiveFileName || "page",
2280
2437
  label,
2281
- path: ctx.elementPath || []
2438
+ path: path2
2282
2439
  };
2283
2440
  return generateElementClassName(elementClassCtx);
2284
2441
  }
@@ -2298,6 +2455,26 @@ function buildCssVariableStyleAttr(cssVariables) {
2298
2455
  const styleString = Object.entries(cssVariables).map(([k, v]) => `${k}: ${v}`).join("; ");
2299
2456
  return ` style="${escapeHtml(styleString)}"`;
2300
2457
  }
2458
+ function arraysEqual(a, b) {
2459
+ if (!a || !b || a.length !== b.length) return false;
2460
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
2461
+ return true;
2462
+ }
2463
+ function editorAttrs(ctx, opts = {}) {
2464
+ if (!ctx.injectEditorAttrs) return "";
2465
+ const isComponentRoot = !!ctx.componentContext && !opts.isSlotContent && arraysEqual(ctx.elementPath, ctx.componentRootPath);
2466
+ const effectiveParent = opts.isSlotContent ? ctx.parentComponentName : isComponentRoot ? ctx.parentComponentName : ctx.componentContext ?? ctx.parentComponentName;
2467
+ return buildEditorAttrs({
2468
+ elementPath: ctx.elementPath,
2469
+ cmsItemIndexPath: ctx.cmsItemIndexPath,
2470
+ cmsListPaths: ctx.cmsListPaths,
2471
+ componentContext: ctx.componentContext,
2472
+ parentComponentName: effectiveParent,
2473
+ isComponentRoot,
2474
+ isSlotContent: opts.isSlotContent,
2475
+ isCMSListContainer: opts.isCMSListContainer
2476
+ });
2477
+ }
2301
2478
  async function renderNode(node, ctx) {
2302
2479
  const { breakpoints, viewportWidth, locale, i18nConfig, slugMappings, pagePath } = ctx;
2303
2480
  if (node === null || node === void 0) return "";
@@ -2406,7 +2583,7 @@ async function renderNode(node, ctx) {
2406
2583
  delete nodeAttributes2.class;
2407
2584
  const attrs2 = buildAttributes(nodeAttributes2);
2408
2585
  const classAttr2 = classNames.length > 0 ? ` class="${escapeHtml(classNames.filter(Boolean).join(" "))}"` : "";
2409
- return `<span${classAttr2}${embedStyleAttr}${attrs2}>${optimizedHtml}</span>`;
2586
+ return `<span${classAttr2}${embedStyleAttr}${attrs2}${editorAttrs(ctx, { isSlotContent: isSlotContent(node) })}>${optimizedHtml}</span>`;
2410
2587
  }
2411
2588
  const attrClassName2 = nodeAttributes2.className || nodeAttributes2.class || "";
2412
2589
  if (attrClassName2) {
@@ -2416,7 +2593,7 @@ async function renderNode(node, ctx) {
2416
2593
  delete nodeAttributes2.class;
2417
2594
  const attrs = buildAttributes(nodeAttributes2);
2418
2595
  const classAttr = classNames.length > 0 ? ` class="${escapeHtml(classNames.filter(Boolean).join(" "))}"` : "";
2419
- return `<span${classAttr}${attrs}>${optimizedHtml}</span>`;
2596
+ return `<span${classAttr}${attrs}${editorAttrs(ctx, { isSlotContent: isSlotContent(node) })}>${optimizedHtml}</span>`;
2420
2597
  }
2421
2598
  if (isLinkNode(node)) {
2422
2599
  let href = typeof node.href === "string" ? node.href : "#";
@@ -2477,7 +2654,7 @@ async function renderNode(node, ctx) {
2477
2654
  const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
2478
2655
  return renderNode(child, { ...ctx, elementPath: childPath });
2479
2656
  }))).join("") : await renderNode(children, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
2480
- return `<a href="${escapeHtml(String(href))}"${classAttr}${olinkStyleAttr}${attrs}>${childrenHTML}</a>`;
2657
+ return `<a href="${escapeHtml(String(href))}"${classAttr}${olinkStyleAttr}${attrs}${editorAttrs(ctx, { isSlotContent: isSlotContent(node) })}>${childrenHTML}</a>`;
2481
2658
  }
2482
2659
  if (isLocaleListNode(node)) {
2483
2660
  return renderLocaleList(node, ctx);
@@ -2651,8 +2828,10 @@ async function renderComponent(componentName, propsWithStyleAndAttrs, children,
2651
2828
  }
2652
2829
  return await renderNode(processedStructure, {
2653
2830
  ...ctx,
2831
+ // The previously-active component (if any) becomes the parent for editor attrs
2832
+ parentComponentName: ctx.componentContext,
2654
2833
  componentContext: componentName,
2655
- elementPath: [0],
2834
+ componentRootPath: ctx.elementPath,
2656
2835
  componentResolvedProps: resolvedProps
2657
2836
  });
2658
2837
  } catch (error) {
@@ -2672,7 +2851,7 @@ async function renderLinkNode(propsWithStyleAndAttrs, children, ctx) {
2672
2851
  const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
2673
2852
  return renderNode(child, { ...ctx, elementPath: childPath });
2674
2853
  }))).join("") : await renderNode(children, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
2675
- return `<a href="${escapeHtml(String(href))}"${linkClassAttr}${attrs}>${childrenHTML}</a>`;
2854
+ return `<a href="${escapeHtml(String(href))}"${linkClassAttr}${attrs}${editorAttrs(ctx)}>${childrenHTML}</a>`;
2676
2855
  }
2677
2856
  async function renderHtmlElement(tag, propsWithStyleAndAttrs, children, ctx) {
2678
2857
  let classValue = propsWithStyleAndAttrs.className ? String(propsWithStyleAndAttrs.className) : "";
@@ -2694,18 +2873,32 @@ async function renderHtmlElement(tag, propsWithStyleAndAttrs, children, ctx) {
2694
2873
  const imageProps = ["src", "alt", "loading", "width", "height", "sizes", "srcset", "fetchpriority"];
2695
2874
  const excludeProps = tag.toLowerCase() === "img" ? ["style", "className", ...imageProps] : ["style", "className"];
2696
2875
  const attrs = buildAttributes(propsWithStyleAndAttrs, excludeProps);
2697
- const childrenHTML = Array.isArray(children) ? (await Promise.all(children.map((child, index) => {
2698
- const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
2699
- return renderNode(child, { ...ctx, elementPath: childPath });
2700
- }))).join("") : await renderNode(children, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
2876
+ const isRawTextElement = tag.toLowerCase() === "style" || tag.toLowerCase() === "script";
2877
+ let childrenHTML;
2878
+ if (isRawTextElement) {
2879
+ const flatten = (node) => {
2880
+ if (node == null) return "";
2881
+ if (typeof node === "string") return node;
2882
+ if (typeof node === "number") return String(node);
2883
+ if (Array.isArray(node)) return node.map(flatten).join("");
2884
+ return "";
2885
+ };
2886
+ childrenHTML = flatten(children);
2887
+ } else {
2888
+ childrenHTML = Array.isArray(children) ? (await Promise.all(children.map((child, index) => {
2889
+ const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
2890
+ return renderNode(child, { ...ctx, elementPath: childPath });
2891
+ }))).join("") : await renderNode(children, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
2892
+ }
2893
+ const ea = editorAttrs(ctx);
2701
2894
  const voidElements = ["img", "input", "br", "hr", "meta", "link", "area", "base", "col", "embed", "source", "track", "wbr"];
2702
2895
  if (voidElements.includes(tag.toLowerCase())) {
2703
2896
  if (tag.toLowerCase() === "img") {
2704
- return renderImageElement(propsWithStyleAndAttrs, classAttr, styleAttr, attrs, ctx);
2897
+ return renderImageElement(propsWithStyleAndAttrs, classAttr, styleAttr, attrs + ea, ctx);
2705
2898
  }
2706
- return `<${tag}${classAttr}${styleAttr}${attrs} />`;
2899
+ return `<${tag}${classAttr}${styleAttr}${attrs}${ea} />`;
2707
2900
  }
2708
- return `<${tag}${classAttr}${styleAttr}${attrs}>${childrenHTML}</${tag}>`;
2901
+ return `<${tag}${classAttr}${styleAttr}${attrs}${ea}>${childrenHTML}</${tag}>`;
2709
2902
  }
2710
2903
  function renderImageElement(propsWithStyleAndAttrs, classAttr, styleAttr, attrs, ctx) {
2711
2904
  const imgProps = propsWithStyleAndAttrs;
@@ -2793,9 +2986,13 @@ function renderLocaleList(node, ctx) {
2793
2986
  localeIconMap.set(localeConfig.code, localeConfig.icon);
2794
2987
  }
2795
2988
  }
2989
+ const localeStyleOpts = {
2990
+ fluidActive: ctx.responsiveScales?.enabled === true && ctx.responsiveScales?.mode === "fluid",
2991
+ responsiveScales: ctx.responsiveScales
2992
+ };
2796
2993
  let containerClasses = [];
2797
2994
  if (nodeStyle) {
2798
- containerClasses = responsiveStylesToClasses(nodeStyle);
2995
+ containerClasses = responsiveStylesToClasses(nodeStyle, localeStyleOpts);
2799
2996
  }
2800
2997
  const localeListInteractiveStyles = node.interactiveStyles;
2801
2998
  const localeListGenerateElementClass = node.generateElementClass;
@@ -2812,21 +3009,21 @@ function renderLocaleList(node, ctx) {
2812
3009
  const containerClassAttr = containerClasses.length > 0 ? ` class="${escapeHtml(containerClasses.join(" "))}"` : "";
2813
3010
  let itemClasses = [];
2814
3011
  if (node.itemStyle) {
2815
- itemClasses = responsiveStylesToClasses(node.itemStyle);
3012
+ itemClasses = responsiveStylesToClasses(node.itemStyle, localeStyleOpts);
2816
3013
  }
2817
3014
  const itemClassAttr = itemClasses.length > 0 ? ` class="${escapeHtml(itemClasses.join(" "))}"` : "";
2818
3015
  let activeItemClasses = [];
2819
3016
  if (node.activeItemStyle) {
2820
- activeItemClasses = responsiveStylesToClasses(node.activeItemStyle);
3017
+ activeItemClasses = responsiveStylesToClasses(node.activeItemStyle, localeStyleOpts);
2821
3018
  }
2822
3019
  let separatorClasses = [];
2823
3020
  if (node.separatorStyle) {
2824
- separatorClasses = responsiveStylesToClasses(node.separatorStyle);
3021
+ separatorClasses = responsiveStylesToClasses(node.separatorStyle, localeStyleOpts);
2825
3022
  }
2826
3023
  const separatorClassAttr = separatorClasses.length > 0 ? ` class="${escapeHtml(separatorClasses.join(" "))}"` : "";
2827
3024
  let flagClasses = [];
2828
3025
  if (node.flagStyle) {
2829
- flagClasses = responsiveStylesToClasses(node.flagStyle);
3026
+ flagClasses = responsiveStylesToClasses(node.flagStyle, localeStyleOpts);
2830
3027
  }
2831
3028
  const flagClassAttr = flagClasses.length > 0 ? ` class="${escapeHtml(flagClasses.join(" "))}"` : "";
2832
3029
  const currentItemClasses = [...itemClasses, ...activeItemClasses];
@@ -2861,7 +3058,7 @@ function renderLocaleList(node, ctx) {
2861
3058
  const linksHTML = showSeparator ? links.join(`<span${separatorClassAttr}></span>`) : links.join("");
2862
3059
  const nodeAttributes = extractAttributesFromNode(node);
2863
3060
  const attrsStr = buildAttributes(nodeAttributes);
2864
- const localeListResult = `<div data-locale-list="true"${containerClassAttr}${localeListStyleAttr}${attrsStr}>${linksHTML}</div>`;
3061
+ const localeListResult = `<div data-locale-list="true"${containerClassAttr}${localeListStyleAttr}${attrsStr}${editorAttrs(ctx)}>${linksHTML}</div>`;
2865
3062
  if (ctx.ssrFallbackCollector && ctx.elementPath) {
2866
3063
  ctx.ssrFallbackCollector.set(ctx.elementPath.join("."), localeListResult);
2867
3064
  }
@@ -2869,7 +3066,7 @@ function renderLocaleList(node, ctx) {
2869
3066
  }
2870
3067
  return '<div data-locale-list="true"></div>';
2871
3068
  }
2872
- async function renderPageSSR(pageData, globalComponents = {}, pagePath = "/", baseUrl = "", locale, i18nConfig, slugMappings, cmsContext, cmsService, isProductionBuild) {
3069
+ async function renderPageSSR(pageData, globalComponents = {}, pagePath = "/", baseUrl = "", locale, i18nConfig, slugMappings, cmsContext, cmsService, isProductionBuild, injectEditorAttrs) {
2873
3070
  const rootNode = pageData?.root || void 0;
2874
3071
  if (!rootNode) {
2875
3072
  throw new Error("Page data must have a root node");
@@ -2893,7 +3090,7 @@ async function renderPageSSR(pageData, globalComponents = {}, pagePath = "/", ba
2893
3090
  }
2894
3091
  }
2895
3092
  const pageComponents = pageData?.components || {};
2896
- const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector } = rootNode ? await buildComponentHTML(rootNode, globalComponents, pageComponents, effectiveLocale, config, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild) : { html: "", interactiveStylesMap: /* @__PURE__ */ new Map(), preloadImages: [], neededCollections: /* @__PURE__ */ new Set(), ssrFallbackCollector: /* @__PURE__ */ new Map(), processedRawHtmlCollector: /* @__PURE__ */ new Map() };
3093
+ const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector } = rootNode ? await buildComponentHTML(rootNode, globalComponents, pageComponents, effectiveLocale, config, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild, injectEditorAttrs) : { html: "", interactiveStylesMap: /* @__PURE__ */ new Map(), preloadImages: [], neededCollections: /* @__PURE__ */ new Set(), ssrFallbackCollector: /* @__PURE__ */ new Map(), processedRawHtmlCollector: /* @__PURE__ */ new Map() };
2897
3094
  const javascript = await collectComponentJavaScript(globalComponents, pageComponents);
2898
3095
  const componentCSS = collectComponentCSS(globalComponents, pageComponents);
2899
3096
  const fullUrl = baseUrl ? `${baseUrl}${pagePath}` : pagePath;
@@ -3217,7 +3414,7 @@ function generateImagePreloadTags(preloadImages) {
3217
3414
  }
3218
3415
  function minifyCSS(code) {
3219
3416
  if (!code.trim()) return code;
3220
- return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s*([{};:,>~+])\s*/g, "$1").replace(/\s+/g, " ").replace(/\{\s+/g, "{").replace(/\s+\}/g, "}").replace(/;}/g, "}").trim();
3417
+ return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s*([{};:,>~])\s*/g, "$1").replace(/\s+/g, " ").replace(/\{\s+/g, "{").replace(/\s+\}/g, "}").replace(/;}/g, "}").trim();
3221
3418
  }
3222
3419
  async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePath = "/", baseUrl = "", useBuiltBundle = false, locale, slugMappings, cmsContext, cmsService, externalScriptPath) {
3223
3420
  let options;
@@ -3254,11 +3451,12 @@ async function generateSSRHTML(pageDataOrOptions, globalComponents = {}, pagePat
3254
3451
  pageCustomCode,
3255
3452
  clientDataCollections,
3256
3453
  injectLiveReload = false,
3454
+ injectEditorAttrs = false,
3257
3455
  isEditor = false,
3258
3456
  isProductionBuild = false,
3259
3457
  serverPort
3260
3458
  } = options;
3261
- const rendered = await renderPageSSR(pageData, components, path2, base, loc, void 0, slugs, cms, cmsServ, isProductionBuild);
3459
+ const rendered = await renderPageSSR(pageData, components, path2, base, loc, void 0, slugs, cms, cmsServ, isProductionBuild, injectEditorAttrs);
3262
3460
  let finalClientDataCollections = clientDataCollections;
3263
3461
  if (rendered.neededCollections.size > 0 && cmsServ) {
3264
3462
  finalClientDataCollections = clientDataCollections ? new Map(clientDataCollections) : /* @__PURE__ */ new Map();
@@ -3440,7 +3638,7 @@ picture {
3440
3638
  const scriptPreloadTag = extScriptPath ? `<link rel="preload" href="${extScriptPath}" as="script">` : "";
3441
3639
  const imagePreloadTags = generateImagePreloadTags(rendered.preloadImages);
3442
3640
  const wsUrl = serverPort ? `'ws://localhost:${serverPort}/hmr'` : `location.origin.replace('http','ws')+'/hmr'`;
3443
- const liveReloadScript = injectLiveReload ? `<script>(function(){var ws,timer,gen=0;function connect(){ws=new WebSocket(${wsUrl});ws.onmessage=function(e){var d=JSON.parse(e.data);if(d.type==='hmr:libraries-update'){location.reload()}else if(d.type==='hmr:update'||d.type==='hmr:cms-update'||d.type==='hmr:colors-update'||d.type==='hmr:variables-update')hotReload()};ws.onclose=function(){clearTimeout(timer);timer=setTimeout(connect,1000)}}function hotReload(){var g=++gen;var sx=window.scrollX,sy=window.scrollY;fetch(location.href,{cache:'no-store'}).then(function(r){return r.text()}).then(function(html){if(g!==gen)return;var p=new DOMParser();var d=p.parseFromString(html,'text/html');var or=document.getElementById('root');var nr=d.getElementById('root');if(or&&nr)or.innerHTML=nr.innerHTML;var os=document.getElementById('meno-styles');var ns=d.getElementById('meno-styles');if(os&&ns){os.parentNode.replaceChild(ns.cloneNode(true),os)}var nh=d.documentElement;if(nh){document.documentElement.setAttribute('lang',nh.getAttribute('lang')||'en');document.documentElement.setAttribute('theme',nh.getAttribute('theme')||'light')}document.querySelectorAll('script[id^="meno-cms-"]').forEach(function(s){s.remove()});d.querySelectorAll('script[id^="meno-cms-"]').forEach(function(s){var c=document.createElement('script');c.type=s.type;c.id=s.id;c.textContent=s.textContent;document.head.appendChild(c)});window.__menoHotReload=true;document.querySelectorAll('body > script[src^="/libraries/"]').forEach(function(o){o.remove()});d.querySelectorAll('body > script[src^="/libraries/"]').forEach(function(n){var ls=document.createElement('script');ls.src=n.getAttribute('src')+(n.getAttribute('src').indexOf('?')>-1?'&':'?')+'_r='+Date.now();document.body.appendChild(ls)});var oscr=document.querySelector('script[src^="/_scripts/"]');var nscr=d.querySelector('script[src^="/_scripts/"]');if(nscr){var src=nscr.getAttribute('src');if(oscr)oscr.remove();var s=document.createElement('script');s.src=src+(src.indexOf('?')>-1?'&':'?')+'_r='+Date.now();s.onload=function(){document.dispatchEvent(new Event('DOMContentLoaded'));window.scrollTo(sx,sy)};s.onerror=function(){window.scrollTo(sx,sy)};document.body.appendChild(s)}else{if(oscr)oscr.remove();document.dispatchEvent(new Event('DOMContentLoaded'));window.scrollTo(sx,sy)}}).catch(function(){location.reload()})}connect()})()</script>` : "";
3641
+ const liveReloadScript = injectLiveReload ? `<script>(function(){var ws,timer,gen=0,lastSrvRoot=null;function strip(s){return s?s.replace(/[?&]_r=\\d+/,''):''}function classList(el){return (el.getAttribute('class')||'').split(/\\s+/).filter(Boolean)}function syncEl(cur,srv,old){var cc=classList(cur),sc=classList(srv),oc=old?new Set(classList(old)):new Set();var rt=cc.filter(function(c){return !oc.has(c)});var seen=new Set(),fin=[];sc.concat(rt).forEach(function(c){if(!seen.has(c)){seen.add(c);fin.push(c)}});var fs=fin.join(' ');if((cur.getAttribute('class')||'')!==fs){if(fs)cur.setAttribute('class',fs);else cur.removeAttribute('class')}for(var i=0;i<srv.attributes.length;i++){var a=srv.attributes[i];if(a.name==='class')continue;if(cur.getAttribute(a.name)!==a.value)cur.setAttribute(a.name,a.value)}if(old){for(var i=0;i<old.attributes.length;i++){var a=old.attributes[i];if(a.name==='class')continue;if(!srv.hasAttribute(a.name)&&cur.hasAttribute(a.name))cur.removeAttribute(a.name)}}}function syncText(cur,srv){var cc=cur.childNodes,sc=srv.childNodes;for(var i=0;i<sc.length;i++){var s=sc[i],c=cc[i];if(s.nodeType===3&&c&&c.nodeType===3){if(c.textContent!==s.textContent)c.textContent=s.textContent}}}function smartUpdate(curR,srvR,oldR){var ce=curR.querySelectorAll('[data-element-path]'),se=srvR.querySelectorAll('[data-element-path]');if(ce.length!==se.length){if(curR.innerHTML!==srvR.innerHTML)curR.innerHTML=srvR.innerHTML;return}var sbp={};for(var i=0;i<se.length;i++)sbp[se[i].getAttribute('data-element-path')]=se[i];var obp={};if(oldR){var oe=oldR.querySelectorAll('[data-element-path]');for(var i=0;i<oe.length;i++)obp[oe[i].getAttribute('data-element-path')]=oe[i]}for(var i=0;i<ce.length;i++){var c=ce[i],p=c.getAttribute('data-element-path'),s=sbp[p];if(!s){if(curR.innerHTML!==srvR.innerHTML)curR.innerHTML=srvR.innerHTML;return}syncEl(c,s,obp[p]);syncText(c,s)}syncText(curR,srvR)}function connect(){ws=new WebSocket(${wsUrl});ws.onmessage=function(e){var d=JSON.parse(e.data);if(d.type==='hmr:libraries-update'){location.reload()}else if(d.type==='hmr:update'||d.type==='hmr:cms-update'||d.type==='hmr:colors-update'||d.type==='hmr:variables-update')hotReload()};ws.onclose=function(){clearTimeout(timer);timer=setTimeout(connect,1000)}}function hotReload(){var g=++gen;var sx=window.scrollX,sy=window.scrollY;fetch(location.href,{cache:'no-store'}).then(function(r){return r.text()}).then(function(html){if(g!==gen)return;var p=new DOMParser();var d=p.parseFromString(html,'text/html');var or=document.getElementById('root'),nr=d.getElementById('root');if(or&&nr)smartUpdate(or,nr,lastSrvRoot);if(nr)lastSrvRoot=nr.cloneNode(true);var os=document.getElementById('meno-styles'),ns=d.getElementById('meno-styles');if(os&&ns&&os.textContent!==ns.textContent)os.parentNode.replaceChild(ns.cloneNode(true),os);var nh=d.documentElement;if(nh){var nl=nh.getAttribute('lang')||'en',nt=nh.getAttribute('theme')||'light';if(document.documentElement.getAttribute('lang')!==nl)document.documentElement.setAttribute('lang',nl);if(document.documentElement.getAttribute('theme')!==nt)document.documentElement.setAttribute('theme',nt)}var ocms=document.querySelectorAll('script[id^="meno-cms-"]'),ncms=d.querySelectorAll('script[id^="meno-cms-"]');var ock=JSON.stringify(Array.prototype.map.call(ocms,function(s){return [s.id,s.textContent]}));var nck=JSON.stringify(Array.prototype.map.call(ncms,function(s){return [s.id,s.textContent]}));if(ock!==nck){ocms.forEach(function(s){s.remove()});ncms.forEach(function(s){var c=document.createElement('script');c.type=s.type;c.id=s.id;c.textContent=s.textContent;document.head.appendChild(c)})}window.__menoHotReload=true;var olib=document.querySelectorAll('body > script[src^="/libraries/"]'),nlib=d.querySelectorAll('body > script[src^="/libraries/"]');var olk=JSON.stringify(Array.prototype.map.call(olib,function(s){return strip(s.getAttribute('src'))}).sort());var nlk=JSON.stringify(Array.prototype.map.call(nlib,function(s){return strip(s.getAttribute('src'))}).sort());if(olk!==nlk){olib.forEach(function(o){o.remove()});nlib.forEach(function(n){var src=n.getAttribute('src');var ls=document.createElement('script');ls.src=src+(src.indexOf('?')>-1?'&':'?')+'_r='+Date.now();document.body.appendChild(ls)})}var oscr=document.querySelector('script[src^="/_scripts/"]'),nscr=d.querySelector('script[src^="/_scripts/"]');var oss=oscr?strip(oscr.getAttribute('src')):'',nss=nscr?strip(nscr.getAttribute('src')):'';if(oss===nss){window.scrollTo(sx,sy)}else{if(oscr)oscr.remove();if(nscr){var src=nscr.getAttribute('src');var s=document.createElement('script');s.src=src+(src.indexOf('?')>-1?'&':'?')+'_r='+Date.now();s.onload=function(){document.dispatchEvent(new Event('DOMContentLoaded'));window.scrollTo(sx,sy)};s.onerror=function(){window.scrollTo(sx,sy)};document.body.appendChild(s)}else{document.dispatchEvent(new Event('DOMContentLoaded'));window.scrollTo(sx,sy)}}}).catch(function(){location.reload()})}var iR=document.getElementById('root');if(iR)lastSrvRoot=iR.cloneNode(true);connect()})()</script>` : "";
3444
3642
  const scrollHandlerScript = injectLiveReload ? `<script>(function(){window.addEventListener('message',function(e){if(e.data.type==='GET_SCROLL_POSITION'){window.parent.postMessage({type:'SCROLL_POSITION_RESPONSE',scrollX:window.scrollX,scrollY:window.scrollY},'*')}else if(e.data.type==='SET_SCROLL_POSITION'){window.scrollTo(e.data.scrollX,e.data.scrollY)}})})()</script>` : "";
3445
3643
  const styleContent = useBundled ? finalCSS : `
3446
3644
  ${combinedCSS.split("\n").join("\n ")}
@@ -5622,6 +5820,7 @@ autoInit();
5622
5820
  // lib/server/providers/fileSystemCMSProvider.ts
5623
5821
  import { existsSync as existsSync2, readdirSync as readdirSync2, mkdirSync } from "fs";
5624
5822
  import { join as join2 } from "path";
5823
+ var DRAFT_FILE_SUFFIX = `${CMS_DRAFT_SUFFIX}.json`;
5625
5824
  async function loadJSONFile2(filePath) {
5626
5825
  try {
5627
5826
  if (await fileExists(filePath)) {
@@ -5633,12 +5832,18 @@ async function loadJSONFile2(filePath) {
5633
5832
  return null;
5634
5833
  }
5635
5834
  }
5636
- function normalizeItem(content, filename) {
5637
- return {
5835
+ function normalizeItem(content, filename, isDraft = false) {
5836
+ const base = {
5638
5837
  ...content,
5639
5838
  _slug: filename,
5640
5839
  _filename: filename
5641
5840
  };
5841
+ if (isDraft) base._isDraft = true;
5842
+ return base;
5843
+ }
5844
+ function stripTransient(item) {
5845
+ const { _slug, _isDraft, _hasDraft, _url, ...rest } = item;
5846
+ return rest;
5642
5847
  }
5643
5848
  var FileSystemCMSProvider = class {
5644
5849
  constructor(templatesDir, cmsDir) {
@@ -5656,13 +5861,16 @@ var FileSystemCMSProvider = class {
5656
5861
  }
5657
5862
  }
5658
5863
  /**
5659
- * Validate filename to prevent path traversal attacks
5864
+ * Validate filename to prevent path traversal attacks and reserved-suffix collisions.
5660
5865
  * @throws Error if filename is invalid
5661
5866
  */
5662
5867
  validateFilename(filename) {
5663
5868
  if (!isSafePathSegment(filename)) {
5664
5869
  throw new Error(`Invalid filename: "${filename}". Filenames cannot contain path separators or traversal sequences.`);
5665
5870
  }
5871
+ if (isReservedDraftFilename(filename)) {
5872
+ throw new Error(`Invalid filename: "${filename}". The "${CMS_DRAFT_SUFFIX}" suffix is reserved for draft files.`);
5873
+ }
5666
5874
  }
5667
5875
  /**
5668
5876
  * Load all CMS schemas from page files with source: 'cms' in templates/
@@ -5709,7 +5917,7 @@ var FileSystemCMSProvider = class {
5709
5917
  return [];
5710
5918
  }
5711
5919
  const files = readdirSync2(collectionDir);
5712
- const jsonFiles = files.filter((f) => f.endsWith(".json"));
5920
+ const jsonFiles = files.filter((f) => f.endsWith(".json") && !f.endsWith(DRAFT_FILE_SUFFIX));
5713
5921
  const results = await Promise.all(
5714
5922
  jsonFiles.map(async (file) => {
5715
5923
  const filePath = join2(collectionDir, file);
@@ -5793,22 +6001,170 @@ var FileSystemCMSProvider = class {
5793
6001
  if (!existsSync2(collectionDir)) {
5794
6002
  mkdirSync(collectionDir, { recursive: true });
5795
6003
  }
5796
- const { _slug, ...itemData } = item;
6004
+ const itemData = stripTransient(item);
5797
6005
  const filePath = join2(collectionDir, `${filename}.json`);
5798
6006
  await writeFile2(filePath, JSON.stringify(itemData, null, 2), "utf-8");
5799
6007
  }
5800
6008
  /**
5801
- * Delete item by filename (or slug for backward compat)
6009
+ * Delete item by filename. Removes the published file AND any draft sibling.
5802
6010
  */
5803
6011
  async deleteItem(collection, filename) {
5804
6012
  this.validateCollection(collection);
5805
6013
  this.validateFilename(filename);
5806
6014
  const { unlink } = await import("fs/promises");
5807
- const filePath = join2(this.cmsDir, collection, `${filename}.json`);
6015
+ const publishedPath = join2(this.cmsDir, collection, `${filename}.json`);
6016
+ if (existsSync2(publishedPath)) {
6017
+ await unlink(publishedPath);
6018
+ }
6019
+ const draftPath = this.draftPath(collection, filename);
6020
+ if (existsSync2(draftPath)) {
6021
+ await unlink(draftPath);
6022
+ }
6023
+ }
6024
+ // ---- Draft helpers ----------------------------------------------------
6025
+ draftPath(collection, filename) {
6026
+ return join2(this.cmsDir, collection, `${filename}${DRAFT_FILE_SUFFIX}`);
6027
+ }
6028
+ /**
6029
+ * Get the draft version of an item, or null if no draft file exists.
6030
+ * Drafts skip strict validation — they may be partial / WIP.
6031
+ */
6032
+ async getDraft(collection, filename) {
6033
+ this.validateCollection(collection);
6034
+ this.validateFilename(filename);
6035
+ const filePath = this.draftPath(collection, filename);
6036
+ const content = await loadJSONFile2(filePath);
6037
+ if (!content || typeof content !== "object") {
6038
+ return null;
6039
+ }
6040
+ return normalizeItem(
6041
+ content,
6042
+ filename,
6043
+ /*isDraft*/
6044
+ true
6045
+ );
6046
+ }
6047
+ /**
6048
+ * List all drafts in a collection. Used by the Studio item list to mark
6049
+ * items that have an outstanding draft sibling (or are draft-only).
6050
+ */
6051
+ async getAllDrafts(collection) {
6052
+ this.validateCollection(collection);
6053
+ const collectionDir = join2(this.cmsDir, collection);
6054
+ if (!existsSync2(collectionDir)) return [];
6055
+ const files = readdirSync2(collectionDir).filter((f) => f.endsWith(DRAFT_FILE_SUFFIX));
6056
+ const results = await Promise.all(
6057
+ files.map(async (file) => {
6058
+ const filePath = join2(collectionDir, file);
6059
+ const content = await loadJSONFile2(filePath);
6060
+ return { file, content };
6061
+ })
6062
+ );
6063
+ const drafts = [];
6064
+ for (const { file, content } of results) {
6065
+ if (content && typeof content === "object") {
6066
+ const filename = file.slice(0, -DRAFT_FILE_SUFFIX.length);
6067
+ drafts.push(normalizeItem(
6068
+ content,
6069
+ filename,
6070
+ /*isDraft*/
6071
+ true
6072
+ ));
6073
+ }
6074
+ }
6075
+ return drafts;
6076
+ }
6077
+ async hasDraft(collection, filename) {
6078
+ this.validateCollection(collection);
6079
+ this.validateFilename(filename);
6080
+ return existsSync2(this.draftPath(collection, filename));
6081
+ }
6082
+ /**
6083
+ * Save the draft version of an item. Loose validation — drafts may have
6084
+ * missing required fields or partial data. Strict validation only runs at
6085
+ * publish time. The item's `_filename` determines the target file.
6086
+ */
6087
+ async saveDraft(collection, item) {
6088
+ this.validateCollection(collection);
6089
+ const { writeFile: writeFile2 } = await import("fs/promises");
6090
+ const schemas = await this.getAllSchemas();
6091
+ const schemaInfo = schemas.get(collection);
6092
+ if (!schemaInfo) {
6093
+ throw new Error(`Unknown collection: ${collection}`);
6094
+ }
6095
+ let filename;
6096
+ if (item._filename) {
6097
+ filename = item._filename;
6098
+ } else {
6099
+ const slugField = schemaInfo.schema.slugField;
6100
+ const slugValue = item[slugField];
6101
+ filename = typeof slugValue === "string" ? slugValue : String(slugValue);
6102
+ }
6103
+ if (!filename || filename === "[object Object]") {
6104
+ throw new Error("Missing _filename field. Drafts must have _filename set on creation.");
6105
+ }
6106
+ this.validateFilename(filename);
6107
+ const collectionDir = join2(this.cmsDir, collection);
6108
+ if (!existsSync2(collectionDir)) {
6109
+ mkdirSync(collectionDir, { recursive: true });
6110
+ }
6111
+ const itemData = stripTransient(item);
6112
+ const validation = validateCMSDraftItem(itemData);
6113
+ if (!validation.valid) {
6114
+ const messages = validation.errors.map((e) => `${e.path}: ${e.message}`).join(", ");
6115
+ throw new Error(`Invalid draft: ${messages}`);
6116
+ }
6117
+ const filePath = this.draftPath(collection, filename);
6118
+ await writeFile2(filePath, JSON.stringify(itemData, null, 2), "utf-8");
6119
+ }
6120
+ /**
6121
+ * Discard the draft version of an item. No-op if no draft exists.
6122
+ */
6123
+ async discardDraft(collection, filename) {
6124
+ this.validateCollection(collection);
6125
+ this.validateFilename(filename);
6126
+ const { unlink } = await import("fs/promises");
6127
+ const filePath = this.draftPath(collection, filename);
5808
6128
  if (existsSync2(filePath)) {
5809
6129
  await unlink(filePath);
5810
6130
  }
5811
6131
  }
6132
+ /**
6133
+ * Promote a draft to published. Reads `{filename}.draft.json`, writes the
6134
+ * content to `{filename}.json`, then unlinks the draft. The published write
6135
+ * happens first so a crash mid-operation leaves a valid published file plus
6136
+ * an orphan draft (recoverable via the editor's Discard button) — never a
6137
+ * gap with no published content.
6138
+ *
6139
+ * Throws if no draft exists.
6140
+ */
6141
+ async publishDraft(collection, filename) {
6142
+ this.validateCollection(collection);
6143
+ this.validateFilename(filename);
6144
+ const { writeFile: writeFile2, unlink } = await import("fs/promises");
6145
+ const draftFilePath = this.draftPath(collection, filename);
6146
+ const content = await loadJSONFile2(draftFilePath);
6147
+ if (!content || typeof content !== "object") {
6148
+ throw new Error(`No draft to publish for ${collection}/${filename}`);
6149
+ }
6150
+ const item = normalizeItem(content, filename);
6151
+ const validation = validateCMSItem(item);
6152
+ if (!validation.valid) {
6153
+ const messages = validation.errors.map((e) => `${e.path}: ${e.message}`).join(", ");
6154
+ throw new Error(`Cannot publish invalid draft: ${messages}`);
6155
+ }
6156
+ const collectionDir = join2(this.cmsDir, collection);
6157
+ if (!existsSync2(collectionDir)) {
6158
+ mkdirSync(collectionDir, { recursive: true });
6159
+ }
6160
+ const itemData = stripTransient(validation.data);
6161
+ const publishedPath = join2(collectionDir, `${filename}.json`);
6162
+ await writeFile2(publishedPath, JSON.stringify(itemData, null, 2), "utf-8");
6163
+ if (existsSync2(draftFilePath)) {
6164
+ await unlink(draftFilePath);
6165
+ }
6166
+ return normalizeItem(itemData, filename);
6167
+ }
5812
6168
  /**
5813
6169
  * Clear schema cache (useful when pages are modified)
5814
6170
  */
@@ -5932,4 +6288,4 @@ export {
5932
6288
  FileSystemCMSProvider,
5933
6289
  migrateTemplatesDirectory
5934
6290
  };
5935
- //# sourceMappingURL=chunk-TPQ7APVQ.js.map
6291
+ //# sourceMappingURL=chunk-EQYDSPBB.js.map