@particle-academy/fancy-cms-ui 0.7.0 → 0.8.0

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.
@@ -1,11 +1,16 @@
1
1
  import { childrenOf as childrenOf$1, descendantsOf, fractionalKey } from '@particle-academy/fancy-doc-commons';
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
- import { useMemo, Fragment } from 'react';
3
+ import { createElement, useMemo, Fragment } from 'react';
4
4
 
5
5
  // src/document/types.ts
6
6
  function isBinding(v) {
7
7
  return typeof v === "object" && v !== null && typeof v.$bind === "string";
8
8
  }
9
+ function hasNodeMap(value) {
10
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
11
+ const nodes = value.nodes;
12
+ return typeof nodes === "object" && nodes !== null && !Array.isArray(nodes);
13
+ }
9
14
  var DEFAULT_BREAKPOINTS = {
10
15
  base: 0,
11
16
  md: 768,
@@ -21,10 +26,22 @@ function emptyDoc(id, slug = "/") {
21
26
  nodes: {}
22
27
  };
23
28
  }
29
+
30
+ // src/document/own.ts
31
+ var has = Object.prototype.hasOwnProperty;
32
+ function own(map, key) {
33
+ return map != null && has.call(map, key) ? map[key] : void 0;
34
+ }
35
+ function setOwn(map, key, value) {
36
+ Object.defineProperty(map, key, { value, writable: true, enumerable: true, configurable: true });
37
+ }
38
+
39
+ // src/document/reduce.ts
24
40
  function childrenOf(doc, parent) {
25
41
  return childrenOf$1(doc, parent);
26
42
  }
27
43
  function rootIds(doc) {
44
+ if (!hasNodeMap(doc)) return [];
28
45
  return childrenOf(doc, null).map((n) => n.id);
29
46
  }
30
47
  function lastRootId(doc) {
@@ -35,7 +52,7 @@ function descendantIds(doc, id) {
35
52
  return descendantsOf(doc, id);
36
53
  }
37
54
  function patchNode(doc, id, patch) {
38
- const node = doc.nodes[id];
55
+ const node = own(doc.nodes, id);
39
56
  return {
40
57
  ...doc,
41
58
  nodes: { ...doc.nodes, [id]: { ...node, ...patch } },
@@ -49,8 +66,8 @@ function reduce(doc, op, opts = {}) {
49
66
  };
50
67
  switch (op.t) {
51
68
  case "insert_node": {
52
- if (doc.nodes[op.node.id]) return invalid("node id already exists");
53
- if (op.node.parent !== null && !doc.nodes[op.node.parent]) return invalid("parent not found");
69
+ if (own(doc.nodes, op.node.id)) return invalid("node id already exists");
70
+ if (op.node.parent !== null && !own(doc.nodes, op.node.parent)) return invalid("parent not found");
54
71
  return {
55
72
  ...doc,
56
73
  nodes: { ...doc.nodes, [op.node.id]: op.node },
@@ -58,11 +75,11 @@ function reduce(doc, op, opts = {}) {
58
75
  };
59
76
  }
60
77
  case "remove_node": {
61
- if (!doc.nodes[op.id]) return invalid("node not found");
78
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
62
79
  const dead = /* @__PURE__ */ new Set([op.id, ...descendantIds(doc, op.id)]);
63
80
  const nodes = {};
64
81
  for (const id of Object.keys(doc.nodes)) {
65
- if (!dead.has(id)) nodes[id] = doc.nodes[id];
82
+ if (!dead.has(id)) setOwn(nodes, id, own(doc.nodes, id));
66
83
  }
67
84
  return {
68
85
  ...doc,
@@ -71,10 +88,10 @@ function reduce(doc, op, opts = {}) {
71
88
  };
72
89
  }
73
90
  case "move_node": {
74
- const node = doc.nodes[op.id];
91
+ const node = own(doc.nodes, op.id);
75
92
  if (!node) return invalid("node not found");
76
93
  if (op.parent === op.id) return invalid("cannot parent a node to itself");
77
- if (op.parent !== null && !doc.nodes[op.parent]) return invalid("new parent not found");
94
+ if (op.parent !== null && !own(doc.nodes, op.parent)) return invalid("new parent not found");
78
95
  if (op.parent !== null && descendantIds(doc, op.id).includes(op.parent)) {
79
96
  return invalid("move would create a cycle");
80
97
  }
@@ -85,34 +102,35 @@ function reduce(doc, op, opts = {}) {
85
102
  };
86
103
  }
87
104
  case "set_node_props": {
88
- if (!doc.nodes[op.id]) return invalid("node not found");
105
+ const node = own(doc.nodes, op.id);
106
+ if (!node) return invalid("node not found");
89
107
  return patchNode(doc, op.id, {
90
- props: { ...doc.nodes[op.id].props, ...op.patch }
108
+ props: { ...node.props, ...op.patch }
91
109
  });
92
110
  }
93
111
  case "set_layout": {
94
- if (!doc.nodes[op.id]) return invalid("node not found");
112
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
95
113
  return patchNode(doc, op.id, { layout: op.layout });
96
114
  }
97
115
  case "set_style": {
98
- const node = doc.nodes[op.id];
116
+ const node = own(doc.nodes, op.id);
99
117
  if (!node) return invalid("node not found");
100
- const prev = node.style[op.breakpoint] ?? {};
118
+ const prev = own(node.style, op.breakpoint) ?? {};
101
119
  return patchNode(doc, op.id, {
102
120
  style: { ...node.style, [op.breakpoint]: { ...prev, ...op.patch } }
103
121
  });
104
122
  }
105
123
  case "set_constraints": {
106
- const node = doc.nodes[op.id];
124
+ const node = own(doc.nodes, op.id);
107
125
  if (!node) return invalid("node not found");
108
126
  const base = node.constraints ?? { base: { width: "hug", height: "hug" } };
109
- const prev = base[op.breakpoint] ?? {};
127
+ const prev = own(base, op.breakpoint) ?? {};
110
128
  return patchNode(doc, op.id, {
111
129
  constraints: { ...base, [op.breakpoint]: { ...prev, ...op.patch } }
112
130
  });
113
131
  }
114
132
  case "set_animation": {
115
- const node = doc.nodes[op.id];
133
+ const node = own(doc.nodes, op.id);
116
134
  if (!node) return invalid("node not found");
117
135
  const list = node.animations ?? [];
118
136
  const idx = list.findIndex((a) => a.id === op.anim.id);
@@ -121,14 +139,14 @@ function reduce(doc, op, opts = {}) {
121
139
  });
122
140
  }
123
141
  case "remove_animation": {
124
- const node = doc.nodes[op.id];
142
+ const node = own(doc.nodes, op.id);
125
143
  if (!node) return invalid("node not found");
126
144
  return patchNode(doc, op.id, {
127
145
  animations: (node.animations ?? []).filter((a) => a.id !== op.animId)
128
146
  });
129
147
  }
130
148
  case "set_action": {
131
- const node = doc.nodes[op.id];
149
+ const node = own(doc.nodes, op.id);
132
150
  if (!node) return invalid("node not found");
133
151
  const list = node.actions ?? [];
134
152
  let actions;
@@ -144,13 +162,13 @@ function reduce(doc, op, opts = {}) {
144
162
  case "reorder_roots": {
145
163
  const currentRoots = childrenOf(doc, null);
146
164
  const current = new Set(currentRoots.map((n) => n.id));
147
- const same = op.order.length === currentRoots.length && op.order.every((id) => current.has(id));
165
+ const same = op.order.length === currentRoots.length && new Set(op.order).size === op.order.length && op.order.every((id) => current.has(id));
148
166
  if (!same) return invalid("reorder must be a permutation of the current roots");
149
167
  const nodes = { ...doc.nodes };
150
168
  let prev = null;
151
169
  for (const id of op.order) {
152
170
  prev = fractionalKey(prev, null);
153
- nodes[id] = { ...nodes[id], order: prev };
171
+ setOwn(nodes, id, { ...own(nodes, id), order: prev });
154
172
  }
155
173
  return { ...doc, nodes, seq: doc.seq + 1 };
156
174
  }
@@ -166,17 +184,17 @@ function invert(doc, op) {
166
184
  case "insert_node":
167
185
  return [{ t: "remove_node", id: op.node.id }];
168
186
  case "remove_node": {
169
- const node = doc.nodes[op.id];
187
+ const node = own(doc.nodes, op.id);
170
188
  if (!node) return [];
171
189
  const ids = [op.id, ...descendantIds(doc, op.id)];
172
- const inserts = ids.map((id) => ({ t: "insert_node", node: doc.nodes[id] }));
190
+ const inserts = ids.map((id) => ({ t: "insert_node", node: own(doc.nodes, id) }));
173
191
  if (node.parent === null) {
174
192
  inserts.push({ t: "reorder_roots", order: childrenOf(doc, null).map((n) => n.id) });
175
193
  }
176
194
  return inserts;
177
195
  }
178
196
  case "move_node": {
179
- const node = doc.nodes[op.id];
197
+ const node = own(doc.nodes, op.id);
180
198
  if (!node) return [];
181
199
  const back = [{ t: "move_node", id: op.id, parent: node.parent, order: node.order }];
182
200
  if (node.parent === null) {
@@ -185,19 +203,19 @@ function invert(doc, op) {
185
203
  return back;
186
204
  }
187
205
  case "set_node_props": {
188
- const node = doc.nodes[op.id];
206
+ const node = own(doc.nodes, op.id);
189
207
  if (!node) return [];
190
208
  const prev = {};
191
209
  for (const k of Object.keys(op.patch)) prev[k] = node.props[k];
192
210
  return [{ t: "set_node_props", id: op.id, patch: prev }];
193
211
  }
194
212
  case "set_layout": {
195
- const node = doc.nodes[op.id];
213
+ const node = own(doc.nodes, op.id);
196
214
  if (!node) return [];
197
215
  return [{ t: "set_layout", id: op.id, layout: node.layout }];
198
216
  }
199
217
  case "set_style": {
200
- const node = doc.nodes[op.id];
218
+ const node = own(doc.nodes, op.id);
201
219
  if (!node) return [];
202
220
  const prevBp = node.style[op.breakpoint] ?? {};
203
221
  const prev = {};
@@ -219,17 +237,23 @@ function pick(obj, keys) {
219
237
  for (const k of keys) out[k] = obj[k];
220
238
  return out;
221
239
  }
240
+ function normalizeDoc(input) {
241
+ if (hasNodeMap(input)) return input;
242
+ const id = typeof input === "object" && input !== null && !Array.isArray(input) && typeof input.id === "string" ? input.id : "malformed";
243
+ return emptyDoc(id);
244
+ }
222
245
  function migrateDocVerbose(input) {
223
- const legacy = input;
246
+ const normalized = normalizeDoc(input);
247
+ const legacy = normalized;
224
248
  const sections = legacy.sections;
225
249
  if (!Array.isArray(sections)) {
226
- return { doc: input, migrated: false, danglingIds: [], unlistedRootIds: [] };
250
+ return { doc: normalized, migrated: false, danglingIds: [], unlistedRootIds: [] };
227
251
  }
228
252
  const danglingIds = [];
229
253
  const ordered = [];
230
254
  const seen = /* @__PURE__ */ new Set();
231
255
  for (const id of sections) {
232
- if (!input.nodes[id]) {
256
+ if (!own(normalized.nodes, id)) {
233
257
  danglingIds.push(id);
234
258
  continue;
235
259
  }
@@ -237,13 +261,13 @@ function migrateDocVerbose(input) {
237
261
  seen.add(id);
238
262
  ordered.push(id);
239
263
  }
240
- const unlistedRootIds = Object.values(input.nodes).filter((n) => n.parent === null && !seen.has(n.id)).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0).map((n) => n.id);
264
+ const unlistedRootIds = Object.values(normalized.nodes).filter((n) => n.parent === null && !seen.has(n.id)).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0).map((n) => n.id);
241
265
  const finalOrder = [...ordered, ...unlistedRootIds];
242
- const nodes = { ...input.nodes };
266
+ const nodes = { ...normalized.nodes };
243
267
  let prev = null;
244
268
  for (const id of finalOrder) {
245
269
  const key = fractionalKey(prev, null);
246
- nodes[id] = { ...nodes[id], order: key };
270
+ setOwn(nodes, id, { ...own(nodes, id), order: key });
247
271
  prev = key;
248
272
  }
249
273
  const { sections: _dropped, ...rest } = legacy;
@@ -262,15 +286,36 @@ function needsMigration(input) {
262
286
  }
263
287
 
264
288
  // src/render/css.ts
265
- var selectorFor = (id) => `[data-cms="${id}"]`;
289
+ function cssString(value) {
290
+ let out = "";
291
+ for (const ch of value) {
292
+ const code = ch.codePointAt(0);
293
+ if (ch === "\\" || ch === '"') out += `\\${ch}`;
294
+ else if (code < 32 || code === 127 || ch === "<") out += `\\${code.toString(16)} `;
295
+ else out += ch;
296
+ }
297
+ return out;
298
+ }
299
+ var selectorFor = (id) => `[data-cms="${cssString(id)}"]`;
300
+ var UNITS = /* @__PURE__ */ new Set(["px", "%", "vw", "vh", "rem", "fr"]);
266
301
  function lengthToCss(len) {
267
- return `${len.value}${len.unit}`;
302
+ if (typeof len !== "object" || len === null) return null;
303
+ const { value, unit } = len;
304
+ if (typeof value !== "number" || !Number.isFinite(value) || typeof unit !== "string" || !UNITS.has(unit)) return null;
305
+ return `${value}${unit}`;
268
306
  }
269
307
  function sizeToCss(size) {
270
308
  if (size === "fill") return "100%";
271
309
  if (size === "hug") return "auto";
272
310
  return lengthToCss(size);
273
311
  }
312
+ function numberToCss(value) {
313
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
314
+ }
315
+ function putLen(d, props, len) {
316
+ const css = lengthToCss(len);
317
+ if (css !== null) for (const p of props) d[p] = css;
318
+ }
274
319
  var ALIGN = {
275
320
  start: "flex-start",
276
321
  center: "center",
@@ -284,51 +329,150 @@ var JUSTIFY = {
284
329
  between: "space-between",
285
330
  around: "space-around"
286
331
  };
332
+ function safeCssValue(value) {
333
+ if (/[{}<]/.test(value)) return null;
334
+ let depth = 0;
335
+ let quote = null;
336
+ for (const ch of value) {
337
+ if (quote) {
338
+ if (ch === quote) quote = null;
339
+ } else if (ch === '"' || ch === "'") {
340
+ quote = ch;
341
+ } else if (ch === "(") {
342
+ depth++;
343
+ } else if (ch === ")") {
344
+ depth = Math.max(0, depth - 1);
345
+ } else if (ch === ";" && depth === 0) {
346
+ return null;
347
+ }
348
+ }
349
+ return value;
350
+ }
351
+ function put(d, prop, value) {
352
+ if (value === void 0) return;
353
+ const safe = safeCssValue(value);
354
+ if (safe !== null) d[prop] = safe;
355
+ }
356
+ var OVERFLOW = /* @__PURE__ */ new Set(["visible", "hidden", "auto"]);
357
+ var TEXT_TRANSFORM = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
358
+ var BACKGROUND_SIZE = /* @__PURE__ */ new Set(["cover", "contain", "auto"]);
359
+ var TEXT_ALIGN = /* @__PURE__ */ new Set(["left", "center", "right", "justify"]);
360
+ var DIRECTIONS = /* @__PURE__ */ new Set(["row", "column"]);
361
+ function keyword(map, value) {
362
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(map, value) ? map[value] : null;
363
+ }
364
+ function inSet(set, value) {
365
+ return typeof value === "string" && set.has(value) ? value : null;
366
+ }
287
367
  function styleDecls(style) {
288
368
  const d = {};
289
- if (style.padding) d["padding"] = lengthToCss(style.padding);
290
- if (style.margin) d["margin"] = lengthToCss(style.margin);
291
- if (style.radius) d["border-radius"] = lengthToCss(style.radius);
292
- if (style.background !== void 0) d["background"] = style.background;
293
- if (style.color !== void 0) d["color"] = style.color;
294
- if (style.opacity !== void 0) d["opacity"] = String(style.opacity);
295
- if (style.fontFamily !== void 0) d["font-family"] = style.fontFamily;
296
- if (style.fontSize) d["font-size"] = lengthToCss(style.fontSize);
297
- if (style.fontWeight !== void 0) d["font-weight"] = String(style.fontWeight);
298
- if (style.lineHeight !== void 0) d["line-height"] = String(style.lineHeight);
299
- if (style.textAlign !== void 0) d["text-align"] = style.textAlign;
300
- if (style.letterSpacing) d["letter-spacing"] = lengthToCss(style.letterSpacing);
301
- if (style.border !== void 0) d["border"] = style.border;
302
- if (style.boxShadow !== void 0) d["box-shadow"] = style.boxShadow;
303
- if (style.transform !== void 0) d["transform"] = style.transform;
304
- if (style.filter !== void 0) d["filter"] = style.filter;
305
- if (style.gap) d["gap"] = lengthToCss(style.gap);
306
- if (style.align !== void 0) d["align-items"] = ALIGN[style.align] ?? style.align;
307
- if (style.justify !== void 0) d["justify-content"] = JUSTIFY[style.justify] ?? style.justify;
369
+ putLen(d, ["padding"], style.padding);
370
+ putLen(d, ["padding-left", "padding-right"], style.paddingX);
371
+ putLen(d, ["padding-top", "padding-bottom"], style.paddingY);
372
+ putLen(d, ["margin"], style.margin);
373
+ if (style.marginX === "auto") d["margin-left"] = d["margin-right"] = "auto";
374
+ else putLen(d, ["margin-left", "margin-right"], style.marginX);
375
+ putLen(d, ["margin-top", "margin-bottom"], style.marginY);
376
+ putLen(d, ["border-radius"], style.radius);
377
+ putLen(d, ["max-width"], style.maxWidth);
378
+ putLen(d, ["min-height"], style.minHeight);
379
+ put(d, "aspect-ratio", typeof style.aspectRatio === "string" ? style.aspectRatio : void 0);
380
+ if (style.sticky === true) {
381
+ d["position"] = "sticky";
382
+ d["top"] = "0";
383
+ }
384
+ if (typeof style.zIndex === "number" && Number.isFinite(style.zIndex)) d["z-index"] = String(Math.trunc(style.zIndex));
385
+ const overflow = inSet(OVERFLOW, style.overflow);
386
+ if (overflow) d["overflow"] = overflow;
387
+ put(d, "background", typeof style.background === "string" ? style.background : void 0);
388
+ const backgroundSize = inSet(BACKGROUND_SIZE, style.backgroundSize);
389
+ if (backgroundSize) d["background-size"] = backgroundSize;
390
+ put(d, "background-position", typeof style.backgroundPosition === "string" ? style.backgroundPosition : void 0);
391
+ put(d, "color", typeof style.color === "string" ? style.color : void 0);
392
+ const opacity = numberToCss(style.opacity);
393
+ if (opacity !== null) d["opacity"] = opacity;
394
+ put(d, "font-family", typeof style.fontFamily === "string" ? style.fontFamily : void 0);
395
+ putLen(d, ["font-size"], style.fontSize);
396
+ const fontWeight = numberToCss(style.fontWeight);
397
+ if (fontWeight !== null) d["font-weight"] = fontWeight;
398
+ const lineHeight = numberToCss(style.lineHeight);
399
+ if (lineHeight !== null) d["line-height"] = lineHeight;
400
+ const textAlign = inSet(TEXT_ALIGN, style.textAlign);
401
+ if (textAlign) d["text-align"] = textAlign;
402
+ putLen(d, ["letter-spacing"], style.letterSpacing);
403
+ const textTransform = inSet(TEXT_TRANSFORM, style.textTransform);
404
+ if (textTransform) d["text-transform"] = textTransform;
405
+ put(d, "border", typeof style.border === "string" ? style.border : void 0);
406
+ put(d, "box-shadow", typeof style.boxShadow === "string" ? style.boxShadow : void 0);
407
+ put(d, "transform", typeof style.transform === "string" ? style.transform : void 0);
408
+ put(d, "filter", typeof style.filter === "string" ? style.filter : void 0);
409
+ putLen(d, ["gap"], style.gap);
410
+ const align = keyword(ALIGN, style.align);
411
+ if (align) d["align-items"] = align;
412
+ const justify = keyword(JUSTIFY, style.justify);
413
+ if (justify) d["justify-content"] = justify;
308
414
  return d;
309
415
  }
310
- function layoutDecls(node) {
416
+ function shownDisplay(node) {
417
+ if (node.layout === "stack") return "flex";
418
+ if (node.layout === "grid") return "grid";
419
+ return "block";
420
+ }
421
+ function gridColumns(style) {
422
+ const min = lengthToCss(style.columnMin);
423
+ if (min !== null) return `repeat(auto-fit, minmax(min(${min}, 100%), 1fr))`;
424
+ if (typeof style.columns === "number" && Number.isFinite(style.columns)) return `repeat(${Math.max(1, Math.trunc(style.columns))}, 1fr)`;
425
+ return void 0;
426
+ }
427
+ function containerDecls(node, style, isBase) {
311
428
  const d = {};
312
429
  if (node.layout === "stack") {
313
- d["display"] = "flex";
314
- d["flex-direction"] = node.style.base.direction ?? "column";
430
+ if (isBase) d["display"] = "flex";
431
+ const direction = inSet(DIRECTIONS, style.direction);
432
+ if (isBase || direction) d["flex-direction"] = direction ?? "column";
433
+ if (style.wrap !== void 0) d["flex-wrap"] = style.wrap ? "wrap" : "nowrap";
315
434
  } else if (node.layout === "grid") {
316
- d["display"] = "grid";
317
- d["grid-template-columns"] = `repeat(${node.style.base.columns ?? 1}, 1fr)`;
318
- } else if (node.layout === "free") {
435
+ if (isBase) d["display"] = "grid";
436
+ const cols = gridColumns(style);
437
+ if (isBase || cols !== void 0) d["grid-template-columns"] = cols ?? "repeat(1, 1fr)";
438
+ } else if (node.layout === "free" && isBase) {
319
439
  d["position"] = "relative";
320
440
  }
321
441
  return d;
322
442
  }
443
+ function visibilityDecls(node, style, isBase) {
444
+ if (style.hidden === true) return { display: "none" };
445
+ if (style.hidden === false && !isBase) return { display: shownDisplay(node) };
446
+ return {};
447
+ }
448
+ function themeDecls(theme) {
449
+ const d = {};
450
+ const walk = (value, path) => {
451
+ if (typeof value === "string" || typeof value === "number" && Number.isFinite(value)) {
452
+ const key = path.map((p) => p.toLowerCase().replace(/[^a-z0-9-]/g, "")).filter(Boolean).join("-");
453
+ if (key) put(d, `--cms-${key}`, String(value));
454
+ return;
455
+ }
456
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
457
+ for (const [k, v] of Object.entries(value)) walk(v, [...path, k]);
458
+ }
459
+ };
460
+ if (typeof theme === "object" && theme !== null && !Array.isArray(theme)) {
461
+ for (const [k, v] of Object.entries(theme)) {
462
+ if (k !== "name") walk(v, [k]);
463
+ }
464
+ }
465
+ return d;
466
+ }
323
467
  function constraintDecls(c, parentFree) {
324
468
  const d = {};
325
469
  const transforms = [];
326
470
  if (parentFree) {
327
471
  d["position"] = "absolute";
328
- if (c.left) d["left"] = lengthToCss(c.left);
329
- if (c.right) d["right"] = lengthToCss(c.right);
330
- if (c.top) d["top"] = lengthToCss(c.top);
331
- if (c.bottom) d["bottom"] = lengthToCss(c.bottom);
472
+ putLen(d, ["left"], c.left);
473
+ putLen(d, ["right"], c.right);
474
+ putLen(d, ["top"], c.top);
475
+ putLen(d, ["bottom"], c.bottom);
332
476
  if (c.centerX) {
333
477
  d["left"] = "50%";
334
478
  transforms.push("translateX(-50%)");
@@ -339,13 +483,15 @@ function constraintDecls(c, parentFree) {
339
483
  }
340
484
  if (transforms.length) d["transform"] = transforms.join(" ");
341
485
  }
342
- if (c.width) d["width"] = sizeToCss(c.width);
343
- if (c.height) d["height"] = sizeToCss(c.height);
486
+ const width = sizeToCss(c.width);
487
+ if (width !== null) d["width"] = width;
488
+ const height = sizeToCss(c.height);
489
+ if (height !== null) d["height"] = height;
344
490
  return d;
345
491
  }
346
492
  function isParentFree(doc, node) {
347
493
  if (node.parent === null) return false;
348
- const parent = doc.nodes[node.parent];
494
+ const parent = own(doc.nodes, node.parent);
349
495
  return !parent || parent.layout === void 0 || parent.layout === "free";
350
496
  }
351
497
  function serializeRule(selector, decls, indent) {
@@ -357,16 +503,20 @@ ${body}
357
503
  ${indent}}`;
358
504
  }
359
505
  function emitDocCss(doc) {
506
+ if (!hasNodeMap(doc)) return "";
360
507
  const breakpointPx = DEFAULT_BREAKPOINTS;
361
508
  const blocks = [];
509
+ const themeRule = serializeRule(":root", themeDecls(doc.theme), "");
510
+ if (themeRule) blocks.push(themeRule);
362
511
  for (const id of Object.keys(doc.nodes).sort()) {
363
512
  const node = doc.nodes[id];
364
513
  const selector = selectorFor(id);
365
514
  const parentFree = isParentFree(doc, node);
366
515
  const base = {
367
- ...layoutDecls(node),
516
+ ...containerDecls(node, node.style.base, true),
368
517
  ...node.constraints ? constraintDecls(node.constraints.base, parentFree) : {},
369
- ...styleDecls(node.style.base)
518
+ ...styleDecls(node.style.base),
519
+ ...visibilityDecls(node, node.style.base, true)
370
520
  };
371
521
  const baseRule = serializeRule(selector, base, "");
372
522
  if (baseRule) blocks.push(baseRule);
@@ -374,9 +524,12 @@ function emitDocCss(doc) {
374
524
  if (bp === "base") continue;
375
525
  const px = breakpointPx[bp];
376
526
  if (px === void 0) continue;
527
+ const bpStyle = node.style[bp];
377
528
  const decls = {
529
+ ...bpStyle ? containerDecls(node, bpStyle, false) : {},
378
530
  ...node.constraints?.[bp] ? constraintDecls(node.constraints[bp], parentFree) : {},
379
- ...node.style[bp] ? styleDecls(node.style[bp]) : {}
531
+ ...bpStyle ? styleDecls(bpStyle) : {},
532
+ ...bpStyle ? visibilityDecls(node, bpStyle, false) : {}
380
533
  };
381
534
  const inner = serializeRule(selector, decls, " ");
382
535
  if (inner) blocks.push(`@media (min-width: ${px}px) {
@@ -394,6 +547,48 @@ function resolveValue(v, data) {
394
547
  if (isBinding(v)) return getPath(data, v.$bind);
395
548
  return v;
396
549
  }
550
+ var LANDMARK_TAGS = ["section", "header", "nav", "main", "footer", "article", "aside", "div"];
551
+ var LANDMARKS = new Set(LANDMARK_TAGS);
552
+ var CONTAINERS = /* @__PURE__ */ new Set(["stack", "grid", "frame", "shape", "card"]);
553
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
554
+ function safeHref(raw) {
555
+ const href = raw.trim();
556
+ if (!href) return null;
557
+ const probe = href.replace(/[\u0000- \u007f]+/g, "");
558
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(probe);
559
+ if (scheme && !SAFE_SCHEMES.has(scheme[1].toLowerCase())) return null;
560
+ return href;
561
+ }
562
+ var ANCHOR_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
563
+ function anchorFor(node, text) {
564
+ const anchor = text(node.props.anchor);
565
+ return ANCHOR_PATTERN.test(anchor) ? anchor : null;
566
+ }
567
+ function elementFor(node, text) {
568
+ const as = text(node.props.as);
569
+ switch (node.type) {
570
+ case "heading": {
571
+ const level = Number(text(node.props.level) || 2);
572
+ return { tag: Number.isInteger(level) && level >= 1 && level <= 6 ? `h${level}` : "h2" };
573
+ }
574
+ case "text":
575
+ return { tag: "p" };
576
+ case "section":
577
+ return { tag: LANDMARKS.has(as) ? as : "section" };
578
+ case "list":
579
+ return { tag: text(node.props.ordered) === "true" ? "ol" : "ul" };
580
+ case "button":
581
+ case "link": {
582
+ const href = safeHref(text(node.props.href));
583
+ if (href === null) return { tag: node.type === "link" ? "span" : "div" };
584
+ const attrs = { href };
585
+ if (/^https?:\/\//i.test(href)) attrs.rel = "noopener noreferrer";
586
+ return { tag: "a", attrs };
587
+ }
588
+ default:
589
+ return { tag: CONTAINERS.has(node.type) && LANDMARKS.has(as) ? as : "div" };
590
+ }
591
+ }
397
592
  var defaultRegistry = {
398
593
  section: ({ children }) => children,
399
594
  stack: ({ children }) => children,
@@ -406,7 +601,12 @@ var defaultRegistry = {
406
601
  // Rich text — inline-formatted HTML (gradient spans, links, <code>, …) that
407
602
  // plain `text` can't express. The author owns the markup (it's their content).
408
603
  richtext: ({ node, text }) => /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: text(node.props.html) } }),
409
- link: ({ node, text }) => /* @__PURE__ */ jsx("a", { href: text(node.props.href) || "#", style: { color: "inherit", textDecoration: "underline" }, children: text(node.props.content) || "link" }),
604
+ // The [data-cms] element is already the <a> (see elementFor), so a link renders its label only.
605
+ link: ({ node, text }) => text(node.props.content) || "link",
606
+ list: ({ node, resolve }) => {
607
+ const items = resolve(node.props.items);
608
+ return Array.isArray(items) ? items.map((item, i) => /* @__PURE__ */ jsx("li", { children: item === null || item === void 0 ? "" : String(item) }, i)) : null;
609
+ },
410
610
  divider: () => /* @__PURE__ */ jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
411
611
  callout: ({ node, text }) => {
412
612
  const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
@@ -418,15 +618,20 @@ var defaultRegistry = {
418
618
  const variant = text(node.props.variant) || "primary";
419
619
  return /* @__PURE__ */ jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label });
420
620
  },
421
- image: ({ node, text }) => /* @__PURE__ */ jsx(
422
- "img",
423
- {
424
- src: text(node.props.src),
425
- alt: text(node.props.alt),
426
- style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
427
- }
428
- )
621
+ image: ({ node, text }) => {
622
+ const fit = text(node.props.fit);
623
+ return /* @__PURE__ */ jsx(
624
+ "img",
625
+ {
626
+ src: text(node.props.src),
627
+ alt: text(node.props.alt),
628
+ loading: text(node.props.loading) === "eager" ? "eager" : "lazy",
629
+ style: { width: "100%", height: "100%", objectFit: IMAGE_FITS.has(fit) ? fit : "cover", display: "block" }
630
+ }
631
+ );
632
+ }
429
633
  };
634
+ var IMAGE_FITS = /* @__PURE__ */ new Set(["cover", "contain", "fill", "none", "scale-down"]);
430
635
  function buttonStyle(variant) {
431
636
  const base = {
432
637
  display: "inline-flex",
@@ -445,7 +650,7 @@ function buttonStyle(variant) {
445
650
  return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
446
651
  }
447
652
  function RenderNode({ doc, id, registry = defaultRegistry, data }) {
448
- const node = doc.nodes[id];
653
+ const node = own(doc.nodes, id);
449
654
  if (!node) return null;
450
655
  if (node.type === "repeater") {
451
656
  const arr = resolveValue(node.props.items, data);
@@ -461,7 +666,19 @@ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
461
666
  };
462
667
  const renderer = registry[node.type];
463
668
  const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
464
- return /* @__PURE__ */ jsx("div", { "data-cms": node.id, "data-cms-island": node.island ? node.type : void 0, className: node.className, children: inner });
669
+ const { tag, attrs } = elementFor(node, text);
670
+ const anchor = anchorFor(node, text);
671
+ return createElement(
672
+ tag,
673
+ {
674
+ "data-cms": node.id,
675
+ "data-cms-island": node.island ? node.type : void 0,
676
+ className: node.className,
677
+ ...anchor ? { id: anchor } : {},
678
+ ...attrs
679
+ },
680
+ inner
681
+ );
465
682
  }
466
683
  function CmsPage({
467
684
  doc: input,
@@ -476,6 +693,6 @@ function CmsPage({
476
693
  ] });
477
694
  }
478
695
 
479
- export { CmsPage, DEFAULT_BREAKPOINTS, RenderNode, childrenOf, defaultRegistry, descendantIds, emitDocCss, emptyDoc, getPath, invert, isBinding, lastRootId, migrateDoc, migrateDocVerbose, needsMigration, reduce, reduceAll, resolveValue, rootIds };
480
- //# sourceMappingURL=chunk-IWQQINCF.js.map
481
- //# sourceMappingURL=chunk-IWQQINCF.js.map
696
+ export { CmsPage, DEFAULT_BREAKPOINTS, RenderNode, childrenOf, defaultRegistry, descendantIds, emitDocCss, emptyDoc, getPath, hasNodeMap, invert, isBinding, lastRootId, migrateDoc, migrateDocVerbose, needsMigration, reduce, reduceAll, resolveValue, rootIds };
697
+ //# sourceMappingURL=chunk-SJYKJ6SG.js.map
698
+ //# sourceMappingURL=chunk-SJYKJ6SG.js.map