@particle-academy/fancy-cms-ui 0.7.1 → 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,6 +1,6 @@
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) {
@@ -26,6 +26,17 @@ function emptyDoc(id, slug = "/") {
26
26
  nodes: {}
27
27
  };
28
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
29
40
  function childrenOf(doc, parent) {
30
41
  return childrenOf$1(doc, parent);
31
42
  }
@@ -41,7 +52,7 @@ function descendantIds(doc, id) {
41
52
  return descendantsOf(doc, id);
42
53
  }
43
54
  function patchNode(doc, id, patch) {
44
- const node = doc.nodes[id];
55
+ const node = own(doc.nodes, id);
45
56
  return {
46
57
  ...doc,
47
58
  nodes: { ...doc.nodes, [id]: { ...node, ...patch } },
@@ -55,8 +66,8 @@ function reduce(doc, op, opts = {}) {
55
66
  };
56
67
  switch (op.t) {
57
68
  case "insert_node": {
58
- if (doc.nodes[op.node.id]) return invalid("node id already exists");
59
- 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");
60
71
  return {
61
72
  ...doc,
62
73
  nodes: { ...doc.nodes, [op.node.id]: op.node },
@@ -64,11 +75,11 @@ function reduce(doc, op, opts = {}) {
64
75
  };
65
76
  }
66
77
  case "remove_node": {
67
- if (!doc.nodes[op.id]) return invalid("node not found");
78
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
68
79
  const dead = /* @__PURE__ */ new Set([op.id, ...descendantIds(doc, op.id)]);
69
80
  const nodes = {};
70
81
  for (const id of Object.keys(doc.nodes)) {
71
- if (!dead.has(id)) nodes[id] = doc.nodes[id];
82
+ if (!dead.has(id)) setOwn(nodes, id, own(doc.nodes, id));
72
83
  }
73
84
  return {
74
85
  ...doc,
@@ -77,10 +88,10 @@ function reduce(doc, op, opts = {}) {
77
88
  };
78
89
  }
79
90
  case "move_node": {
80
- const node = doc.nodes[op.id];
91
+ const node = own(doc.nodes, op.id);
81
92
  if (!node) return invalid("node not found");
82
93
  if (op.parent === op.id) return invalid("cannot parent a node to itself");
83
- 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");
84
95
  if (op.parent !== null && descendantIds(doc, op.id).includes(op.parent)) {
85
96
  return invalid("move would create a cycle");
86
97
  }
@@ -91,34 +102,35 @@ function reduce(doc, op, opts = {}) {
91
102
  };
92
103
  }
93
104
  case "set_node_props": {
94
- 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");
95
107
  return patchNode(doc, op.id, {
96
- props: { ...doc.nodes[op.id].props, ...op.patch }
108
+ props: { ...node.props, ...op.patch }
97
109
  });
98
110
  }
99
111
  case "set_layout": {
100
- if (!doc.nodes[op.id]) return invalid("node not found");
112
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
101
113
  return patchNode(doc, op.id, { layout: op.layout });
102
114
  }
103
115
  case "set_style": {
104
- const node = doc.nodes[op.id];
116
+ const node = own(doc.nodes, op.id);
105
117
  if (!node) return invalid("node not found");
106
- const prev = node.style[op.breakpoint] ?? {};
118
+ const prev = own(node.style, op.breakpoint) ?? {};
107
119
  return patchNode(doc, op.id, {
108
120
  style: { ...node.style, [op.breakpoint]: { ...prev, ...op.patch } }
109
121
  });
110
122
  }
111
123
  case "set_constraints": {
112
- const node = doc.nodes[op.id];
124
+ const node = own(doc.nodes, op.id);
113
125
  if (!node) return invalid("node not found");
114
126
  const base = node.constraints ?? { base: { width: "hug", height: "hug" } };
115
- const prev = base[op.breakpoint] ?? {};
127
+ const prev = own(base, op.breakpoint) ?? {};
116
128
  return patchNode(doc, op.id, {
117
129
  constraints: { ...base, [op.breakpoint]: { ...prev, ...op.patch } }
118
130
  });
119
131
  }
120
132
  case "set_animation": {
121
- const node = doc.nodes[op.id];
133
+ const node = own(doc.nodes, op.id);
122
134
  if (!node) return invalid("node not found");
123
135
  const list = node.animations ?? [];
124
136
  const idx = list.findIndex((a) => a.id === op.anim.id);
@@ -127,14 +139,14 @@ function reduce(doc, op, opts = {}) {
127
139
  });
128
140
  }
129
141
  case "remove_animation": {
130
- const node = doc.nodes[op.id];
142
+ const node = own(doc.nodes, op.id);
131
143
  if (!node) return invalid("node not found");
132
144
  return patchNode(doc, op.id, {
133
145
  animations: (node.animations ?? []).filter((a) => a.id !== op.animId)
134
146
  });
135
147
  }
136
148
  case "set_action": {
137
- const node = doc.nodes[op.id];
149
+ const node = own(doc.nodes, op.id);
138
150
  if (!node) return invalid("node not found");
139
151
  const list = node.actions ?? [];
140
152
  let actions;
@@ -150,13 +162,13 @@ function reduce(doc, op, opts = {}) {
150
162
  case "reorder_roots": {
151
163
  const currentRoots = childrenOf(doc, null);
152
164
  const current = new Set(currentRoots.map((n) => n.id));
153
- 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));
154
166
  if (!same) return invalid("reorder must be a permutation of the current roots");
155
167
  const nodes = { ...doc.nodes };
156
168
  let prev = null;
157
169
  for (const id of op.order) {
158
170
  prev = fractionalKey(prev, null);
159
- nodes[id] = { ...nodes[id], order: prev };
171
+ setOwn(nodes, id, { ...own(nodes, id), order: prev });
160
172
  }
161
173
  return { ...doc, nodes, seq: doc.seq + 1 };
162
174
  }
@@ -172,17 +184,17 @@ function invert(doc, op) {
172
184
  case "insert_node":
173
185
  return [{ t: "remove_node", id: op.node.id }];
174
186
  case "remove_node": {
175
- const node = doc.nodes[op.id];
187
+ const node = own(doc.nodes, op.id);
176
188
  if (!node) return [];
177
189
  const ids = [op.id, ...descendantIds(doc, op.id)];
178
- 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) }));
179
191
  if (node.parent === null) {
180
192
  inserts.push({ t: "reorder_roots", order: childrenOf(doc, null).map((n) => n.id) });
181
193
  }
182
194
  return inserts;
183
195
  }
184
196
  case "move_node": {
185
- const node = doc.nodes[op.id];
197
+ const node = own(doc.nodes, op.id);
186
198
  if (!node) return [];
187
199
  const back = [{ t: "move_node", id: op.id, parent: node.parent, order: node.order }];
188
200
  if (node.parent === null) {
@@ -191,19 +203,19 @@ function invert(doc, op) {
191
203
  return back;
192
204
  }
193
205
  case "set_node_props": {
194
- const node = doc.nodes[op.id];
206
+ const node = own(doc.nodes, op.id);
195
207
  if (!node) return [];
196
208
  const prev = {};
197
209
  for (const k of Object.keys(op.patch)) prev[k] = node.props[k];
198
210
  return [{ t: "set_node_props", id: op.id, patch: prev }];
199
211
  }
200
212
  case "set_layout": {
201
- const node = doc.nodes[op.id];
213
+ const node = own(doc.nodes, op.id);
202
214
  if (!node) return [];
203
215
  return [{ t: "set_layout", id: op.id, layout: node.layout }];
204
216
  }
205
217
  case "set_style": {
206
- const node = doc.nodes[op.id];
218
+ const node = own(doc.nodes, op.id);
207
219
  if (!node) return [];
208
220
  const prevBp = node.style[op.breakpoint] ?? {};
209
221
  const prev = {};
@@ -241,7 +253,7 @@ function migrateDocVerbose(input) {
241
253
  const ordered = [];
242
254
  const seen = /* @__PURE__ */ new Set();
243
255
  for (const id of sections) {
244
- if (!normalized.nodes[id]) {
256
+ if (!own(normalized.nodes, id)) {
245
257
  danglingIds.push(id);
246
258
  continue;
247
259
  }
@@ -255,7 +267,7 @@ function migrateDocVerbose(input) {
255
267
  let prev = null;
256
268
  for (const id of finalOrder) {
257
269
  const key = fractionalKey(prev, null);
258
- nodes[id] = { ...nodes[id], order: key };
270
+ setOwn(nodes, id, { ...own(nodes, id), order: key });
259
271
  prev = key;
260
272
  }
261
273
  const { sections: _dropped, ...rest } = legacy;
@@ -274,15 +286,36 @@ function needsMigration(input) {
274
286
  }
275
287
 
276
288
  // src/render/css.ts
277
- 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"]);
278
301
  function lengthToCss(len) {
279
- 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}`;
280
306
  }
281
307
  function sizeToCss(size) {
282
308
  if (size === "fill") return "100%";
283
309
  if (size === "hug") return "auto";
284
310
  return lengthToCss(size);
285
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
+ }
286
319
  var ALIGN = {
287
320
  start: "flex-start",
288
321
  center: "center",
@@ -296,51 +329,150 @@ var JUSTIFY = {
296
329
  between: "space-between",
297
330
  around: "space-around"
298
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
+ }
299
367
  function styleDecls(style) {
300
368
  const d = {};
301
- if (style.padding) d["padding"] = lengthToCss(style.padding);
302
- if (style.margin) d["margin"] = lengthToCss(style.margin);
303
- if (style.radius) d["border-radius"] = lengthToCss(style.radius);
304
- if (style.background !== void 0) d["background"] = style.background;
305
- if (style.color !== void 0) d["color"] = style.color;
306
- if (style.opacity !== void 0) d["opacity"] = String(style.opacity);
307
- if (style.fontFamily !== void 0) d["font-family"] = style.fontFamily;
308
- if (style.fontSize) d["font-size"] = lengthToCss(style.fontSize);
309
- if (style.fontWeight !== void 0) d["font-weight"] = String(style.fontWeight);
310
- if (style.lineHeight !== void 0) d["line-height"] = String(style.lineHeight);
311
- if (style.textAlign !== void 0) d["text-align"] = style.textAlign;
312
- if (style.letterSpacing) d["letter-spacing"] = lengthToCss(style.letterSpacing);
313
- if (style.border !== void 0) d["border"] = style.border;
314
- if (style.boxShadow !== void 0) d["box-shadow"] = style.boxShadow;
315
- if (style.transform !== void 0) d["transform"] = style.transform;
316
- if (style.filter !== void 0) d["filter"] = style.filter;
317
- if (style.gap) d["gap"] = lengthToCss(style.gap);
318
- if (style.align !== void 0) d["align-items"] = ALIGN[style.align] ?? style.align;
319
- 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;
320
414
  return d;
321
415
  }
322
- 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) {
323
428
  const d = {};
324
429
  if (node.layout === "stack") {
325
- d["display"] = "flex";
326
- 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";
327
434
  } else if (node.layout === "grid") {
328
- d["display"] = "grid";
329
- d["grid-template-columns"] = `repeat(${node.style.base.columns ?? 1}, 1fr)`;
330
- } 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) {
331
439
  d["position"] = "relative";
332
440
  }
333
441
  return d;
334
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
+ }
335
467
  function constraintDecls(c, parentFree) {
336
468
  const d = {};
337
469
  const transforms = [];
338
470
  if (parentFree) {
339
471
  d["position"] = "absolute";
340
- if (c.left) d["left"] = lengthToCss(c.left);
341
- if (c.right) d["right"] = lengthToCss(c.right);
342
- if (c.top) d["top"] = lengthToCss(c.top);
343
- 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);
344
476
  if (c.centerX) {
345
477
  d["left"] = "50%";
346
478
  transforms.push("translateX(-50%)");
@@ -351,13 +483,15 @@ function constraintDecls(c, parentFree) {
351
483
  }
352
484
  if (transforms.length) d["transform"] = transforms.join(" ");
353
485
  }
354
- if (c.width) d["width"] = sizeToCss(c.width);
355
- 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;
356
490
  return d;
357
491
  }
358
492
  function isParentFree(doc, node) {
359
493
  if (node.parent === null) return false;
360
- const parent = doc.nodes[node.parent];
494
+ const parent = own(doc.nodes, node.parent);
361
495
  return !parent || parent.layout === void 0 || parent.layout === "free";
362
496
  }
363
497
  function serializeRule(selector, decls, indent) {
@@ -372,14 +506,17 @@ function emitDocCss(doc) {
372
506
  if (!hasNodeMap(doc)) return "";
373
507
  const breakpointPx = DEFAULT_BREAKPOINTS;
374
508
  const blocks = [];
509
+ const themeRule = serializeRule(":root", themeDecls(doc.theme), "");
510
+ if (themeRule) blocks.push(themeRule);
375
511
  for (const id of Object.keys(doc.nodes).sort()) {
376
512
  const node = doc.nodes[id];
377
513
  const selector = selectorFor(id);
378
514
  const parentFree = isParentFree(doc, node);
379
515
  const base = {
380
- ...layoutDecls(node),
516
+ ...containerDecls(node, node.style.base, true),
381
517
  ...node.constraints ? constraintDecls(node.constraints.base, parentFree) : {},
382
- ...styleDecls(node.style.base)
518
+ ...styleDecls(node.style.base),
519
+ ...visibilityDecls(node, node.style.base, true)
383
520
  };
384
521
  const baseRule = serializeRule(selector, base, "");
385
522
  if (baseRule) blocks.push(baseRule);
@@ -387,9 +524,12 @@ function emitDocCss(doc) {
387
524
  if (bp === "base") continue;
388
525
  const px = breakpointPx[bp];
389
526
  if (px === void 0) continue;
527
+ const bpStyle = node.style[bp];
390
528
  const decls = {
529
+ ...bpStyle ? containerDecls(node, bpStyle, false) : {},
391
530
  ...node.constraints?.[bp] ? constraintDecls(node.constraints[bp], parentFree) : {},
392
- ...node.style[bp] ? styleDecls(node.style[bp]) : {}
531
+ ...bpStyle ? styleDecls(bpStyle) : {},
532
+ ...bpStyle ? visibilityDecls(node, bpStyle, false) : {}
393
533
  };
394
534
  const inner = serializeRule(selector, decls, " ");
395
535
  if (inner) blocks.push(`@media (min-width: ${px}px) {
@@ -407,6 +547,48 @@ function resolveValue(v, data) {
407
547
  if (isBinding(v)) return getPath(data, v.$bind);
408
548
  return v;
409
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
+ }
410
592
  var defaultRegistry = {
411
593
  section: ({ children }) => children,
412
594
  stack: ({ children }) => children,
@@ -419,7 +601,12 @@ var defaultRegistry = {
419
601
  // Rich text — inline-formatted HTML (gradient spans, links, <code>, …) that
420
602
  // plain `text` can't express. The author owns the markup (it's their content).
421
603
  richtext: ({ node, text }) => /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: text(node.props.html) } }),
422
- 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
+ },
423
610
  divider: () => /* @__PURE__ */ jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
424
611
  callout: ({ node, text }) => {
425
612
  const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
@@ -431,15 +618,20 @@ var defaultRegistry = {
431
618
  const variant = text(node.props.variant) || "primary";
432
619
  return /* @__PURE__ */ jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label });
433
620
  },
434
- image: ({ node, text }) => /* @__PURE__ */ jsx(
435
- "img",
436
- {
437
- src: text(node.props.src),
438
- alt: text(node.props.alt),
439
- style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
440
- }
441
- )
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
+ }
442
633
  };
634
+ var IMAGE_FITS = /* @__PURE__ */ new Set(["cover", "contain", "fill", "none", "scale-down"]);
443
635
  function buttonStyle(variant) {
444
636
  const base = {
445
637
  display: "inline-flex",
@@ -458,7 +650,7 @@ function buttonStyle(variant) {
458
650
  return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
459
651
  }
460
652
  function RenderNode({ doc, id, registry = defaultRegistry, data }) {
461
- const node = doc.nodes[id];
653
+ const node = own(doc.nodes, id);
462
654
  if (!node) return null;
463
655
  if (node.type === "repeater") {
464
656
  const arr = resolveValue(node.props.items, data);
@@ -474,7 +666,19 @@ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
474
666
  };
475
667
  const renderer = registry[node.type];
476
668
  const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
477
- 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
+ );
478
682
  }
479
683
  function CmsPage({
480
684
  doc: input,
@@ -490,5 +694,5 @@ function CmsPage({
490
694
  }
491
695
 
492
696
  export { CmsPage, DEFAULT_BREAKPOINTS, RenderNode, childrenOf, defaultRegistry, descendantIds, emitDocCss, emptyDoc, getPath, hasNodeMap, invert, isBinding, lastRootId, migrateDoc, migrateDocVerbose, needsMigration, reduce, reduceAll, resolveValue, rootIds };
493
- //# sourceMappingURL=chunk-LY6ID2J2.js.map
494
- //# sourceMappingURL=chunk-LY6ID2J2.js.map
697
+ //# sourceMappingURL=chunk-SJYKJ6SG.js.map
698
+ //# sourceMappingURL=chunk-SJYKJ6SG.js.map