@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.
package/dist/editor.cjs CHANGED
@@ -32,6 +32,15 @@ function emptyDoc(id, slug = "/") {
32
32
  };
33
33
  }
34
34
 
35
+ // src/document/own.ts
36
+ var has = Object.prototype.hasOwnProperty;
37
+ function own(map, key) {
38
+ return map != null && has.call(map, key) ? map[key] : void 0;
39
+ }
40
+ function setOwn(map, key, value) {
41
+ Object.defineProperty(map, key, { value, writable: true, enumerable: true, configurable: true });
42
+ }
43
+
35
44
  // src/document/migrate.ts
36
45
  function normalizeDoc(input3) {
37
46
  if (hasNodeMap(input3)) return input3;
@@ -49,7 +58,7 @@ function migrateDocVerbose(input3) {
49
58
  const ordered = [];
50
59
  const seen = /* @__PURE__ */ new Set();
51
60
  for (const id of sections) {
52
- if (!normalized.nodes[id]) {
61
+ if (!own(normalized.nodes, id)) {
53
62
  danglingIds.push(id);
54
63
  continue;
55
64
  }
@@ -63,7 +72,7 @@ function migrateDocVerbose(input3) {
63
72
  let prev = null;
64
73
  for (const id of finalOrder) {
65
74
  const key = fancyDocCommons.fractionalKey(prev, null);
66
- nodes[id] = { ...nodes[id], order: key };
75
+ setOwn(nodes, id, { ...own(nodes, id), order: key });
67
76
  prev = key;
68
77
  }
69
78
  const { sections: _dropped, ...rest } = legacy;
@@ -92,7 +101,7 @@ function descendantIds(doc, id) {
92
101
  return fancyDocCommons.descendantsOf(doc, id);
93
102
  }
94
103
  function patchNode(doc, id, patch) {
95
- const node = doc.nodes[id];
104
+ const node = own(doc.nodes, id);
96
105
  return {
97
106
  ...doc,
98
107
  nodes: { ...doc.nodes, [id]: { ...node, ...patch } },
@@ -106,8 +115,8 @@ function reduce(doc, op, opts = {}) {
106
115
  };
107
116
  switch (op.t) {
108
117
  case "insert_node": {
109
- if (doc.nodes[op.node.id]) return invalid("node id already exists");
110
- if (op.node.parent !== null && !doc.nodes[op.node.parent]) return invalid("parent not found");
118
+ if (own(doc.nodes, op.node.id)) return invalid("node id already exists");
119
+ if (op.node.parent !== null && !own(doc.nodes, op.node.parent)) return invalid("parent not found");
111
120
  return {
112
121
  ...doc,
113
122
  nodes: { ...doc.nodes, [op.node.id]: op.node },
@@ -115,11 +124,11 @@ function reduce(doc, op, opts = {}) {
115
124
  };
116
125
  }
117
126
  case "remove_node": {
118
- if (!doc.nodes[op.id]) return invalid("node not found");
127
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
119
128
  const dead = /* @__PURE__ */ new Set([op.id, ...descendantIds(doc, op.id)]);
120
129
  const nodes = {};
121
130
  for (const id of Object.keys(doc.nodes)) {
122
- if (!dead.has(id)) nodes[id] = doc.nodes[id];
131
+ if (!dead.has(id)) setOwn(nodes, id, own(doc.nodes, id));
123
132
  }
124
133
  return {
125
134
  ...doc,
@@ -128,10 +137,10 @@ function reduce(doc, op, opts = {}) {
128
137
  };
129
138
  }
130
139
  case "move_node": {
131
- const node = doc.nodes[op.id];
140
+ const node = own(doc.nodes, op.id);
132
141
  if (!node) return invalid("node not found");
133
142
  if (op.parent === op.id) return invalid("cannot parent a node to itself");
134
- if (op.parent !== null && !doc.nodes[op.parent]) return invalid("new parent not found");
143
+ if (op.parent !== null && !own(doc.nodes, op.parent)) return invalid("new parent not found");
135
144
  if (op.parent !== null && descendantIds(doc, op.id).includes(op.parent)) {
136
145
  return invalid("move would create a cycle");
137
146
  }
@@ -142,34 +151,35 @@ function reduce(doc, op, opts = {}) {
142
151
  };
143
152
  }
144
153
  case "set_node_props": {
145
- if (!doc.nodes[op.id]) return invalid("node not found");
154
+ const node = own(doc.nodes, op.id);
155
+ if (!node) return invalid("node not found");
146
156
  return patchNode(doc, op.id, {
147
- props: { ...doc.nodes[op.id].props, ...op.patch }
157
+ props: { ...node.props, ...op.patch }
148
158
  });
149
159
  }
150
160
  case "set_layout": {
151
- if (!doc.nodes[op.id]) return invalid("node not found");
161
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
152
162
  return patchNode(doc, op.id, { layout: op.layout });
153
163
  }
154
164
  case "set_style": {
155
- const node = doc.nodes[op.id];
165
+ const node = own(doc.nodes, op.id);
156
166
  if (!node) return invalid("node not found");
157
- const prev = node.style[op.breakpoint] ?? {};
167
+ const prev = own(node.style, op.breakpoint) ?? {};
158
168
  return patchNode(doc, op.id, {
159
169
  style: { ...node.style, [op.breakpoint]: { ...prev, ...op.patch } }
160
170
  });
161
171
  }
162
172
  case "set_constraints": {
163
- const node = doc.nodes[op.id];
173
+ const node = own(doc.nodes, op.id);
164
174
  if (!node) return invalid("node not found");
165
175
  const base = node.constraints ?? { base: { width: "hug", height: "hug" } };
166
- const prev = base[op.breakpoint] ?? {};
176
+ const prev = own(base, op.breakpoint) ?? {};
167
177
  return patchNode(doc, op.id, {
168
178
  constraints: { ...base, [op.breakpoint]: { ...prev, ...op.patch } }
169
179
  });
170
180
  }
171
181
  case "set_animation": {
172
- const node = doc.nodes[op.id];
182
+ const node = own(doc.nodes, op.id);
173
183
  if (!node) return invalid("node not found");
174
184
  const list = node.animations ?? [];
175
185
  const idx = list.findIndex((a) => a.id === op.anim.id);
@@ -178,14 +188,14 @@ function reduce(doc, op, opts = {}) {
178
188
  });
179
189
  }
180
190
  case "remove_animation": {
181
- const node = doc.nodes[op.id];
191
+ const node = own(doc.nodes, op.id);
182
192
  if (!node) return invalid("node not found");
183
193
  return patchNode(doc, op.id, {
184
194
  animations: (node.animations ?? []).filter((a) => a.id !== op.animId)
185
195
  });
186
196
  }
187
197
  case "set_action": {
188
- const node = doc.nodes[op.id];
198
+ const node = own(doc.nodes, op.id);
189
199
  if (!node) return invalid("node not found");
190
200
  const list = node.actions ?? [];
191
201
  let actions;
@@ -201,13 +211,13 @@ function reduce(doc, op, opts = {}) {
201
211
  case "reorder_roots": {
202
212
  const currentRoots = childrenOf(doc, null);
203
213
  const current = new Set(currentRoots.map((n) => n.id));
204
- const same = op.order.length === currentRoots.length && op.order.every((id) => current.has(id));
214
+ const same = op.order.length === currentRoots.length && new Set(op.order).size === op.order.length && op.order.every((id) => current.has(id));
205
215
  if (!same) return invalid("reorder must be a permutation of the current roots");
206
216
  const nodes = { ...doc.nodes };
207
217
  let prev = null;
208
218
  for (const id of op.order) {
209
219
  prev = fancyDocCommons.fractionalKey(prev, null);
210
- nodes[id] = { ...nodes[id], order: prev };
220
+ setOwn(nodes, id, { ...own(nodes, id), order: prev });
211
221
  }
212
222
  return { ...doc, nodes, seq: doc.seq + 1 };
213
223
  }
@@ -290,15 +300,36 @@ function useLatestRef(value) {
290
300
  }
291
301
 
292
302
  // src/render/css.ts
293
- var selectorFor = (id) => `[data-cms="${id}"]`;
303
+ function cssString(value) {
304
+ let out = "";
305
+ for (const ch of value) {
306
+ const code = ch.codePointAt(0);
307
+ if (ch === "\\" || ch === '"') out += `\\${ch}`;
308
+ else if (code < 32 || code === 127 || ch === "<") out += `\\${code.toString(16)} `;
309
+ else out += ch;
310
+ }
311
+ return out;
312
+ }
313
+ var selectorFor = (id) => `[data-cms="${cssString(id)}"]`;
314
+ var UNITS = /* @__PURE__ */ new Set(["px", "%", "vw", "vh", "rem", "fr"]);
294
315
  function lengthToCss(len2) {
295
- return `${len2.value}${len2.unit}`;
316
+ if (typeof len2 !== "object" || len2 === null) return null;
317
+ const { value, unit } = len2;
318
+ if (typeof value !== "number" || !Number.isFinite(value) || typeof unit !== "string" || !UNITS.has(unit)) return null;
319
+ return `${value}${unit}`;
296
320
  }
297
321
  function sizeToCss(size) {
298
322
  if (size === "fill") return "100%";
299
323
  if (size === "hug") return "auto";
300
324
  return lengthToCss(size);
301
325
  }
326
+ function numberToCss(value) {
327
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
328
+ }
329
+ function putLen(d, props, len2) {
330
+ const css = lengthToCss(len2);
331
+ if (css !== null) for (const p of props) d[p] = css;
332
+ }
302
333
  var ALIGN = {
303
334
  start: "flex-start",
304
335
  center: "center",
@@ -312,51 +343,150 @@ var JUSTIFY = {
312
343
  between: "space-between",
313
344
  around: "space-around"
314
345
  };
346
+ function safeCssValue(value) {
347
+ if (/[{}<]/.test(value)) return null;
348
+ let depth = 0;
349
+ let quote = null;
350
+ for (const ch of value) {
351
+ if (quote) {
352
+ if (ch === quote) quote = null;
353
+ } else if (ch === '"' || ch === "'") {
354
+ quote = ch;
355
+ } else if (ch === "(") {
356
+ depth++;
357
+ } else if (ch === ")") {
358
+ depth = Math.max(0, depth - 1);
359
+ } else if (ch === ";" && depth === 0) {
360
+ return null;
361
+ }
362
+ }
363
+ return value;
364
+ }
365
+ function put(d, prop, value) {
366
+ if (value === void 0) return;
367
+ const safe = safeCssValue(value);
368
+ if (safe !== null) d[prop] = safe;
369
+ }
370
+ var OVERFLOW = /* @__PURE__ */ new Set(["visible", "hidden", "auto"]);
371
+ var TEXT_TRANSFORM = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
372
+ var BACKGROUND_SIZE = /* @__PURE__ */ new Set(["cover", "contain", "auto"]);
373
+ var TEXT_ALIGN = /* @__PURE__ */ new Set(["left", "center", "right", "justify"]);
374
+ var DIRECTIONS = /* @__PURE__ */ new Set(["row", "column"]);
375
+ function keyword(map, value) {
376
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(map, value) ? map[value] : null;
377
+ }
378
+ function inSet(set, value) {
379
+ return typeof value === "string" && set.has(value) ? value : null;
380
+ }
315
381
  function styleDecls(style) {
316
382
  const d = {};
317
- if (style.padding) d["padding"] = lengthToCss(style.padding);
318
- if (style.margin) d["margin"] = lengthToCss(style.margin);
319
- if (style.radius) d["border-radius"] = lengthToCss(style.radius);
320
- if (style.background !== void 0) d["background"] = style.background;
321
- if (style.color !== void 0) d["color"] = style.color;
322
- if (style.opacity !== void 0) d["opacity"] = String(style.opacity);
323
- if (style.fontFamily !== void 0) d["font-family"] = style.fontFamily;
324
- if (style.fontSize) d["font-size"] = lengthToCss(style.fontSize);
325
- if (style.fontWeight !== void 0) d["font-weight"] = String(style.fontWeight);
326
- if (style.lineHeight !== void 0) d["line-height"] = String(style.lineHeight);
327
- if (style.textAlign !== void 0) d["text-align"] = style.textAlign;
328
- if (style.letterSpacing) d["letter-spacing"] = lengthToCss(style.letterSpacing);
329
- if (style.border !== void 0) d["border"] = style.border;
330
- if (style.boxShadow !== void 0) d["box-shadow"] = style.boxShadow;
331
- if (style.transform !== void 0) d["transform"] = style.transform;
332
- if (style.filter !== void 0) d["filter"] = style.filter;
333
- if (style.gap) d["gap"] = lengthToCss(style.gap);
334
- if (style.align !== void 0) d["align-items"] = ALIGN[style.align] ?? style.align;
335
- if (style.justify !== void 0) d["justify-content"] = JUSTIFY[style.justify] ?? style.justify;
383
+ putLen(d, ["padding"], style.padding);
384
+ putLen(d, ["padding-left", "padding-right"], style.paddingX);
385
+ putLen(d, ["padding-top", "padding-bottom"], style.paddingY);
386
+ putLen(d, ["margin"], style.margin);
387
+ if (style.marginX === "auto") d["margin-left"] = d["margin-right"] = "auto";
388
+ else putLen(d, ["margin-left", "margin-right"], style.marginX);
389
+ putLen(d, ["margin-top", "margin-bottom"], style.marginY);
390
+ putLen(d, ["border-radius"], style.radius);
391
+ putLen(d, ["max-width"], style.maxWidth);
392
+ putLen(d, ["min-height"], style.minHeight);
393
+ put(d, "aspect-ratio", typeof style.aspectRatio === "string" ? style.aspectRatio : void 0);
394
+ if (style.sticky === true) {
395
+ d["position"] = "sticky";
396
+ d["top"] = "0";
397
+ }
398
+ if (typeof style.zIndex === "number" && Number.isFinite(style.zIndex)) d["z-index"] = String(Math.trunc(style.zIndex));
399
+ const overflow = inSet(OVERFLOW, style.overflow);
400
+ if (overflow) d["overflow"] = overflow;
401
+ put(d, "background", typeof style.background === "string" ? style.background : void 0);
402
+ const backgroundSize = inSet(BACKGROUND_SIZE, style.backgroundSize);
403
+ if (backgroundSize) d["background-size"] = backgroundSize;
404
+ put(d, "background-position", typeof style.backgroundPosition === "string" ? style.backgroundPosition : void 0);
405
+ put(d, "color", typeof style.color === "string" ? style.color : void 0);
406
+ const opacity = numberToCss(style.opacity);
407
+ if (opacity !== null) d["opacity"] = opacity;
408
+ put(d, "font-family", typeof style.fontFamily === "string" ? style.fontFamily : void 0);
409
+ putLen(d, ["font-size"], style.fontSize);
410
+ const fontWeight = numberToCss(style.fontWeight);
411
+ if (fontWeight !== null) d["font-weight"] = fontWeight;
412
+ const lineHeight = numberToCss(style.lineHeight);
413
+ if (lineHeight !== null) d["line-height"] = lineHeight;
414
+ const textAlign = inSet(TEXT_ALIGN, style.textAlign);
415
+ if (textAlign) d["text-align"] = textAlign;
416
+ putLen(d, ["letter-spacing"], style.letterSpacing);
417
+ const textTransform = inSet(TEXT_TRANSFORM, style.textTransform);
418
+ if (textTransform) d["text-transform"] = textTransform;
419
+ put(d, "border", typeof style.border === "string" ? style.border : void 0);
420
+ put(d, "box-shadow", typeof style.boxShadow === "string" ? style.boxShadow : void 0);
421
+ put(d, "transform", typeof style.transform === "string" ? style.transform : void 0);
422
+ put(d, "filter", typeof style.filter === "string" ? style.filter : void 0);
423
+ putLen(d, ["gap"], style.gap);
424
+ const align = keyword(ALIGN, style.align);
425
+ if (align) d["align-items"] = align;
426
+ const justify = keyword(JUSTIFY, style.justify);
427
+ if (justify) d["justify-content"] = justify;
336
428
  return d;
337
429
  }
338
- function layoutDecls(node) {
430
+ function shownDisplay(node) {
431
+ if (node.layout === "stack") return "flex";
432
+ if (node.layout === "grid") return "grid";
433
+ return "block";
434
+ }
435
+ function gridColumns(style) {
436
+ const min = lengthToCss(style.columnMin);
437
+ if (min !== null) return `repeat(auto-fit, minmax(min(${min}, 100%), 1fr))`;
438
+ if (typeof style.columns === "number" && Number.isFinite(style.columns)) return `repeat(${Math.max(1, Math.trunc(style.columns))}, 1fr)`;
439
+ return void 0;
440
+ }
441
+ function containerDecls(node, style, isBase) {
339
442
  const d = {};
340
443
  if (node.layout === "stack") {
341
- d["display"] = "flex";
342
- d["flex-direction"] = node.style.base.direction ?? "column";
444
+ if (isBase) d["display"] = "flex";
445
+ const direction = inSet(DIRECTIONS, style.direction);
446
+ if (isBase || direction) d["flex-direction"] = direction ?? "column";
447
+ if (style.wrap !== void 0) d["flex-wrap"] = style.wrap ? "wrap" : "nowrap";
343
448
  } else if (node.layout === "grid") {
344
- d["display"] = "grid";
345
- d["grid-template-columns"] = `repeat(${node.style.base.columns ?? 1}, 1fr)`;
346
- } else if (node.layout === "free") {
449
+ if (isBase) d["display"] = "grid";
450
+ const cols = gridColumns(style);
451
+ if (isBase || cols !== void 0) d["grid-template-columns"] = cols ?? "repeat(1, 1fr)";
452
+ } else if (node.layout === "free" && isBase) {
347
453
  d["position"] = "relative";
348
454
  }
349
455
  return d;
350
456
  }
457
+ function visibilityDecls(node, style, isBase) {
458
+ if (style.hidden === true) return { display: "none" };
459
+ if (style.hidden === false && !isBase) return { display: shownDisplay(node) };
460
+ return {};
461
+ }
462
+ function themeDecls(theme) {
463
+ const d = {};
464
+ const walk = (value, path) => {
465
+ if (typeof value === "string" || typeof value === "number" && Number.isFinite(value)) {
466
+ const key = path.map((p) => p.toLowerCase().replace(/[^a-z0-9-]/g, "")).filter(Boolean).join("-");
467
+ if (key) put(d, `--cms-${key}`, String(value));
468
+ return;
469
+ }
470
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
471
+ for (const [k, v] of Object.entries(value)) walk(v, [...path, k]);
472
+ }
473
+ };
474
+ if (typeof theme === "object" && theme !== null && !Array.isArray(theme)) {
475
+ for (const [k, v] of Object.entries(theme)) {
476
+ if (k !== "name") walk(v, [k]);
477
+ }
478
+ }
479
+ return d;
480
+ }
351
481
  function constraintDecls(c, parentFree) {
352
482
  const d = {};
353
483
  const transforms = [];
354
484
  if (parentFree) {
355
485
  d["position"] = "absolute";
356
- if (c.left) d["left"] = lengthToCss(c.left);
357
- if (c.right) d["right"] = lengthToCss(c.right);
358
- if (c.top) d["top"] = lengthToCss(c.top);
359
- if (c.bottom) d["bottom"] = lengthToCss(c.bottom);
486
+ putLen(d, ["left"], c.left);
487
+ putLen(d, ["right"], c.right);
488
+ putLen(d, ["top"], c.top);
489
+ putLen(d, ["bottom"], c.bottom);
360
490
  if (c.centerX) {
361
491
  d["left"] = "50%";
362
492
  transforms.push("translateX(-50%)");
@@ -367,13 +497,15 @@ function constraintDecls(c, parentFree) {
367
497
  }
368
498
  if (transforms.length) d["transform"] = transforms.join(" ");
369
499
  }
370
- if (c.width) d["width"] = sizeToCss(c.width);
371
- if (c.height) d["height"] = sizeToCss(c.height);
500
+ const width = sizeToCss(c.width);
501
+ if (width !== null) d["width"] = width;
502
+ const height = sizeToCss(c.height);
503
+ if (height !== null) d["height"] = height;
372
504
  return d;
373
505
  }
374
506
  function isParentFree(doc, node) {
375
507
  if (node.parent === null) return false;
376
- const parent = doc.nodes[node.parent];
508
+ const parent = own(doc.nodes, node.parent);
377
509
  return !parent || parent.layout === void 0 || parent.layout === "free";
378
510
  }
379
511
  function serializeRule(selector, decls, indent) {
@@ -388,14 +520,17 @@ function emitDocCss(doc) {
388
520
  if (!hasNodeMap(doc)) return "";
389
521
  const breakpointPx = DEFAULT_BREAKPOINTS;
390
522
  const blocks = [];
523
+ const themeRule = serializeRule(":root", themeDecls(doc.theme), "");
524
+ if (themeRule) blocks.push(themeRule);
391
525
  for (const id of Object.keys(doc.nodes).sort()) {
392
526
  const node = doc.nodes[id];
393
527
  const selector = selectorFor(id);
394
528
  const parentFree = isParentFree(doc, node);
395
529
  const base = {
396
- ...layoutDecls(node),
530
+ ...containerDecls(node, node.style.base, true),
397
531
  ...node.constraints ? constraintDecls(node.constraints.base, parentFree) : {},
398
- ...styleDecls(node.style.base)
532
+ ...styleDecls(node.style.base),
533
+ ...visibilityDecls(node, node.style.base, true)
399
534
  };
400
535
  const baseRule = serializeRule(selector, base, "");
401
536
  if (baseRule) blocks.push(baseRule);
@@ -403,9 +538,12 @@ function emitDocCss(doc) {
403
538
  if (bp === "base") continue;
404
539
  const px = breakpointPx[bp];
405
540
  if (px === void 0) continue;
541
+ const bpStyle = node.style[bp];
406
542
  const decls = {
543
+ ...bpStyle ? containerDecls(node, bpStyle, false) : {},
407
544
  ...node.constraints?.[bp] ? constraintDecls(node.constraints[bp], parentFree) : {},
408
- ...node.style[bp] ? styleDecls(node.style[bp]) : {}
545
+ ...bpStyle ? styleDecls(bpStyle) : {},
546
+ ...bpStyle ? visibilityDecls(node, bpStyle, false) : {}
409
547
  };
410
548
  const inner = serializeRule(selector, decls, " ");
411
549
  if (inner) blocks.push(`@media (min-width: ${px}px) {
@@ -423,6 +561,48 @@ function resolveValue(v, data) {
423
561
  if (isBinding(v)) return getPath(data, v.$bind);
424
562
  return v;
425
563
  }
564
+ var LANDMARK_TAGS = ["section", "header", "nav", "main", "footer", "article", "aside", "div"];
565
+ var LANDMARKS = new Set(LANDMARK_TAGS);
566
+ var CONTAINERS = /* @__PURE__ */ new Set(["stack", "grid", "frame", "shape", "card"]);
567
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
568
+ function safeHref(raw) {
569
+ const href = raw.trim();
570
+ if (!href) return null;
571
+ const probe = href.replace(/[\u0000- \u007f]+/g, "");
572
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(probe);
573
+ if (scheme && !SAFE_SCHEMES.has(scheme[1].toLowerCase())) return null;
574
+ return href;
575
+ }
576
+ var ANCHOR_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
577
+ function anchorFor(node, text) {
578
+ const anchor = text(node.props.anchor);
579
+ return ANCHOR_PATTERN.test(anchor) ? anchor : null;
580
+ }
581
+ function elementFor(node, text) {
582
+ const as = text(node.props.as);
583
+ switch (node.type) {
584
+ case "heading": {
585
+ const level = Number(text(node.props.level) || 2);
586
+ return { tag: Number.isInteger(level) && level >= 1 && level <= 6 ? `h${level}` : "h2" };
587
+ }
588
+ case "text":
589
+ return { tag: "p" };
590
+ case "section":
591
+ return { tag: LANDMARKS.has(as) ? as : "section" };
592
+ case "list":
593
+ return { tag: text(node.props.ordered) === "true" ? "ol" : "ul" };
594
+ case "button":
595
+ case "link": {
596
+ const href = safeHref(text(node.props.href));
597
+ if (href === null) return { tag: node.type === "link" ? "span" : "div" };
598
+ const attrs = { href };
599
+ if (/^https?:\/\//i.test(href)) attrs.rel = "noopener noreferrer";
600
+ return { tag: "a", attrs };
601
+ }
602
+ default:
603
+ return { tag: CONTAINERS.has(node.type) && LANDMARKS.has(as) ? as : "div" };
604
+ }
605
+ }
426
606
  var defaultRegistry = {
427
607
  section: ({ children }) => children,
428
608
  stack: ({ children }) => children,
@@ -435,7 +615,12 @@ var defaultRegistry = {
435
615
  // Rich text — inline-formatted HTML (gradient spans, links, <code>, …) that
436
616
  // plain `text` can't express. The author owns the markup (it's their content).
437
617
  richtext: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: text(node.props.html) } }),
438
- link: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: text(node.props.href) || "#", style: { color: "inherit", textDecoration: "underline" }, children: text(node.props.content) || "link" }),
618
+ // The [data-cms] element is already the <a> (see elementFor), so a link renders its label only.
619
+ link: ({ node, text }) => text(node.props.content) || "link",
620
+ list: ({ node, resolve }) => {
621
+ const items = resolve(node.props.items);
622
+ return Array.isArray(items) ? items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: item === null || item === void 0 ? "" : String(item) }, i)) : null;
623
+ },
439
624
  divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
440
625
  callout: ({ node, text }) => {
441
626
  const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
@@ -447,15 +632,20 @@ var defaultRegistry = {
447
632
  const variant = text(node.props.variant) || "primary";
448
633
  return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label2 });
449
634
  },
450
- image: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx(
451
- "img",
452
- {
453
- src: text(node.props.src),
454
- alt: text(node.props.alt),
455
- style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
456
- }
457
- )
635
+ image: ({ node, text }) => {
636
+ const fit = text(node.props.fit);
637
+ return /* @__PURE__ */ jsxRuntime.jsx(
638
+ "img",
639
+ {
640
+ src: text(node.props.src),
641
+ alt: text(node.props.alt),
642
+ loading: text(node.props.loading) === "eager" ? "eager" : "lazy",
643
+ style: { width: "100%", height: "100%", objectFit: IMAGE_FITS.has(fit) ? fit : "cover", display: "block" }
644
+ }
645
+ );
646
+ }
458
647
  };
648
+ var IMAGE_FITS = /* @__PURE__ */ new Set(["cover", "contain", "fill", "none", "scale-down"]);
459
649
  function buttonStyle(variant) {
460
650
  const base = {
461
651
  display: "inline-flex",
@@ -474,7 +664,7 @@ function buttonStyle(variant) {
474
664
  return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
475
665
  }
476
666
  function RenderNode({ doc, id, registry = defaultRegistry, data }) {
477
- const node = doc.nodes[id];
667
+ const node = own(doc.nodes, id);
478
668
  if (!node) return null;
479
669
  if (node.type === "repeater") {
480
670
  const arr = resolveValue(node.props.items, data);
@@ -490,7 +680,19 @@ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
490
680
  };
491
681
  const renderer = registry[node.type];
492
682
  const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
493
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cms": node.id, "data-cms-island": node.island ? node.type : void 0, className: node.className, children: inner });
683
+ const { tag, attrs } = elementFor(node, text);
684
+ const anchor = anchorFor(node, text);
685
+ return react.createElement(
686
+ tag,
687
+ {
688
+ "data-cms": node.id,
689
+ "data-cms-island": node.island ? node.type : void 0,
690
+ className: node.className,
691
+ ...anchor ? { id: anchor } : {},
692
+ ...attrs
693
+ },
694
+ inner
695
+ );
494
696
  }
495
697
  function CmsPage({
496
698
  doc: input3,
@@ -808,7 +1010,8 @@ var ADD_DEFAULTS = {
808
1010
  divider: { type: "divider", props: {}, style: {} },
809
1011
  code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
810
1012
  richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
811
- repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
1013
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" },
1014
+ list: { type: "list", props: { items: ["First point", "Second point"], ordered: false }, style: {} }
812
1015
  };
813
1016
  var ADD_MENU = [
814
1017
  { kind: "heading", label: "Heading" },
@@ -824,6 +1027,7 @@ var ADD_MENU = [
824
1027
  { kind: "divider", label: "Divider" },
825
1028
  { kind: "code", label: "Code" },
826
1029
  { kind: "richtext", label: "Rich text" },
1030
+ { kind: "list", label: "List" },
827
1031
  { kind: "repeater", label: "Repeater" }
828
1032
  ];
829
1033
  function resolveInsertParent(doc, selectedId) {