@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.
package/dist/editor.cjs CHANGED
@@ -6,17 +6,59 @@ var jsxRuntime = require('react/jsx-runtime');
6
6
  var reactFancy = require('@particle-academy/react-fancy');
7
7
 
8
8
  // src/editor/Editor.tsx
9
+
10
+ // src/document/types.ts
11
+ function isBinding(v) {
12
+ return typeof v === "object" && v !== null && typeof v.$bind === "string";
13
+ }
14
+ function hasNodeMap(value) {
15
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
16
+ const nodes = value.nodes;
17
+ return typeof nodes === "object" && nodes !== null && !Array.isArray(nodes);
18
+ }
19
+ var DEFAULT_BREAKPOINTS = {
20
+ base: 0,
21
+ md: 768,
22
+ lg: 1024
23
+ };
24
+ function emptyDoc(id, slug = "/") {
25
+ return {
26
+ id,
27
+ seq: 0,
28
+ meta: { title: "Untitled", slug, scrollMode: "smooth" },
29
+ theme: { name: "default" },
30
+ breakpoints: ["base", "md", "lg"],
31
+ nodes: {}
32
+ };
33
+ }
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
+
44
+ // src/document/migrate.ts
45
+ function normalizeDoc(input3) {
46
+ if (hasNodeMap(input3)) return input3;
47
+ const id = typeof input3 === "object" && input3 !== null && !Array.isArray(input3) && typeof input3.id === "string" ? input3.id : "malformed";
48
+ return emptyDoc(id);
49
+ }
9
50
  function migrateDocVerbose(input3) {
10
- const legacy = input3;
51
+ const normalized = normalizeDoc(input3);
52
+ const legacy = normalized;
11
53
  const sections = legacy.sections;
12
54
  if (!Array.isArray(sections)) {
13
- return { doc: input3, migrated: false, danglingIds: [], unlistedRootIds: [] };
55
+ return { doc: normalized, migrated: false, danglingIds: [], unlistedRootIds: [] };
14
56
  }
15
57
  const danglingIds = [];
16
58
  const ordered = [];
17
59
  const seen = /* @__PURE__ */ new Set();
18
60
  for (const id of sections) {
19
- if (!input3.nodes[id]) {
61
+ if (!own(normalized.nodes, id)) {
20
62
  danglingIds.push(id);
21
63
  continue;
22
64
  }
@@ -24,13 +66,13 @@ function migrateDocVerbose(input3) {
24
66
  seen.add(id);
25
67
  ordered.push(id);
26
68
  }
27
- const unlistedRootIds = Object.values(input3.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);
69
+ 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);
28
70
  const finalOrder = [...ordered, ...unlistedRootIds];
29
- const nodes = { ...input3.nodes };
71
+ const nodes = { ...normalized.nodes };
30
72
  let prev = null;
31
73
  for (const id of finalOrder) {
32
74
  const key = fancyDocCommons.fractionalKey(prev, null);
33
- nodes[id] = { ...nodes[id], order: key };
75
+ setOwn(nodes, id, { ...own(nodes, id), order: key });
34
76
  prev = key;
35
77
  }
36
78
  const { sections: _dropped, ...rest } = legacy;
@@ -48,6 +90,7 @@ function childrenOf(doc, parent) {
48
90
  return fancyDocCommons.childrenOf(doc, parent);
49
91
  }
50
92
  function rootIds(doc) {
93
+ if (!hasNodeMap(doc)) return [];
51
94
  return childrenOf(doc, null).map((n) => n.id);
52
95
  }
53
96
  function lastRootId(doc) {
@@ -58,7 +101,7 @@ function descendantIds(doc, id) {
58
101
  return fancyDocCommons.descendantsOf(doc, id);
59
102
  }
60
103
  function patchNode(doc, id, patch) {
61
- const node = doc.nodes[id];
104
+ const node = own(doc.nodes, id);
62
105
  return {
63
106
  ...doc,
64
107
  nodes: { ...doc.nodes, [id]: { ...node, ...patch } },
@@ -72,8 +115,8 @@ function reduce(doc, op, opts = {}) {
72
115
  };
73
116
  switch (op.t) {
74
117
  case "insert_node": {
75
- if (doc.nodes[op.node.id]) return invalid("node id already exists");
76
- 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");
77
120
  return {
78
121
  ...doc,
79
122
  nodes: { ...doc.nodes, [op.node.id]: op.node },
@@ -81,11 +124,11 @@ function reduce(doc, op, opts = {}) {
81
124
  };
82
125
  }
83
126
  case "remove_node": {
84
- if (!doc.nodes[op.id]) return invalid("node not found");
127
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
85
128
  const dead = /* @__PURE__ */ new Set([op.id, ...descendantIds(doc, op.id)]);
86
129
  const nodes = {};
87
130
  for (const id of Object.keys(doc.nodes)) {
88
- if (!dead.has(id)) nodes[id] = doc.nodes[id];
131
+ if (!dead.has(id)) setOwn(nodes, id, own(doc.nodes, id));
89
132
  }
90
133
  return {
91
134
  ...doc,
@@ -94,10 +137,10 @@ function reduce(doc, op, opts = {}) {
94
137
  };
95
138
  }
96
139
  case "move_node": {
97
- const node = doc.nodes[op.id];
140
+ const node = own(doc.nodes, op.id);
98
141
  if (!node) return invalid("node not found");
99
142
  if (op.parent === op.id) return invalid("cannot parent a node to itself");
100
- 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");
101
144
  if (op.parent !== null && descendantIds(doc, op.id).includes(op.parent)) {
102
145
  return invalid("move would create a cycle");
103
146
  }
@@ -108,34 +151,35 @@ function reduce(doc, op, opts = {}) {
108
151
  };
109
152
  }
110
153
  case "set_node_props": {
111
- 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");
112
156
  return patchNode(doc, op.id, {
113
- props: { ...doc.nodes[op.id].props, ...op.patch }
157
+ props: { ...node.props, ...op.patch }
114
158
  });
115
159
  }
116
160
  case "set_layout": {
117
- if (!doc.nodes[op.id]) return invalid("node not found");
161
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
118
162
  return patchNode(doc, op.id, { layout: op.layout });
119
163
  }
120
164
  case "set_style": {
121
- const node = doc.nodes[op.id];
165
+ const node = own(doc.nodes, op.id);
122
166
  if (!node) return invalid("node not found");
123
- const prev = node.style[op.breakpoint] ?? {};
167
+ const prev = own(node.style, op.breakpoint) ?? {};
124
168
  return patchNode(doc, op.id, {
125
169
  style: { ...node.style, [op.breakpoint]: { ...prev, ...op.patch } }
126
170
  });
127
171
  }
128
172
  case "set_constraints": {
129
- const node = doc.nodes[op.id];
173
+ const node = own(doc.nodes, op.id);
130
174
  if (!node) return invalid("node not found");
131
175
  const base = node.constraints ?? { base: { width: "hug", height: "hug" } };
132
- const prev = base[op.breakpoint] ?? {};
176
+ const prev = own(base, op.breakpoint) ?? {};
133
177
  return patchNode(doc, op.id, {
134
178
  constraints: { ...base, [op.breakpoint]: { ...prev, ...op.patch } }
135
179
  });
136
180
  }
137
181
  case "set_animation": {
138
- const node = doc.nodes[op.id];
182
+ const node = own(doc.nodes, op.id);
139
183
  if (!node) return invalid("node not found");
140
184
  const list = node.animations ?? [];
141
185
  const idx = list.findIndex((a) => a.id === op.anim.id);
@@ -144,14 +188,14 @@ function reduce(doc, op, opts = {}) {
144
188
  });
145
189
  }
146
190
  case "remove_animation": {
147
- const node = doc.nodes[op.id];
191
+ const node = own(doc.nodes, op.id);
148
192
  if (!node) return invalid("node not found");
149
193
  return patchNode(doc, op.id, {
150
194
  animations: (node.animations ?? []).filter((a) => a.id !== op.animId)
151
195
  });
152
196
  }
153
197
  case "set_action": {
154
- const node = doc.nodes[op.id];
198
+ const node = own(doc.nodes, op.id);
155
199
  if (!node) return invalid("node not found");
156
200
  const list = node.actions ?? [];
157
201
  let actions;
@@ -167,13 +211,13 @@ function reduce(doc, op, opts = {}) {
167
211
  case "reorder_roots": {
168
212
  const currentRoots = childrenOf(doc, null);
169
213
  const current = new Set(currentRoots.map((n) => n.id));
170
- 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));
171
215
  if (!same) return invalid("reorder must be a permutation of the current roots");
172
216
  const nodes = { ...doc.nodes };
173
217
  let prev = null;
174
218
  for (const id of op.order) {
175
219
  prev = fancyDocCommons.fractionalKey(prev, null);
176
- nodes[id] = { ...nodes[id], order: prev };
220
+ setOwn(nodes, id, { ...own(nodes, id), order: prev });
177
221
  }
178
222
  return { ...doc, nodes, seq: doc.seq + 1 };
179
223
  }
@@ -255,36 +299,37 @@ function useLatestRef(value) {
255
299
  return ref;
256
300
  }
257
301
 
258
- // src/document/types.ts
259
- function isBinding(v) {
260
- return typeof v === "object" && v !== null && typeof v.$bind === "string";
261
- }
262
- var DEFAULT_BREAKPOINTS = {
263
- base: 0,
264
- md: 768,
265
- lg: 1024
266
- };
267
- function emptyDoc(id, slug = "/") {
268
- return {
269
- id,
270
- seq: 0,
271
- meta: { title: "Untitled", slug, scrollMode: "smooth" },
272
- theme: { name: "default" },
273
- breakpoints: ["base", "md", "lg"],
274
- nodes: {}
275
- };
276
- }
277
-
278
302
  // src/render/css.ts
279
- 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"]);
280
315
  function lengthToCss(len2) {
281
- 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}`;
282
320
  }
283
321
  function sizeToCss(size) {
284
322
  if (size === "fill") return "100%";
285
323
  if (size === "hug") return "auto";
286
324
  return lengthToCss(size);
287
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
+ }
288
333
  var ALIGN = {
289
334
  start: "flex-start",
290
335
  center: "center",
@@ -298,51 +343,150 @@ var JUSTIFY = {
298
343
  between: "space-between",
299
344
  around: "space-around"
300
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
+ }
301
381
  function styleDecls(style) {
302
382
  const d = {};
303
- if (style.padding) d["padding"] = lengthToCss(style.padding);
304
- if (style.margin) d["margin"] = lengthToCss(style.margin);
305
- if (style.radius) d["border-radius"] = lengthToCss(style.radius);
306
- if (style.background !== void 0) d["background"] = style.background;
307
- if (style.color !== void 0) d["color"] = style.color;
308
- if (style.opacity !== void 0) d["opacity"] = String(style.opacity);
309
- if (style.fontFamily !== void 0) d["font-family"] = style.fontFamily;
310
- if (style.fontSize) d["font-size"] = lengthToCss(style.fontSize);
311
- if (style.fontWeight !== void 0) d["font-weight"] = String(style.fontWeight);
312
- if (style.lineHeight !== void 0) d["line-height"] = String(style.lineHeight);
313
- if (style.textAlign !== void 0) d["text-align"] = style.textAlign;
314
- if (style.letterSpacing) d["letter-spacing"] = lengthToCss(style.letterSpacing);
315
- if (style.border !== void 0) d["border"] = style.border;
316
- if (style.boxShadow !== void 0) d["box-shadow"] = style.boxShadow;
317
- if (style.transform !== void 0) d["transform"] = style.transform;
318
- if (style.filter !== void 0) d["filter"] = style.filter;
319
- if (style.gap) d["gap"] = lengthToCss(style.gap);
320
- if (style.align !== void 0) d["align-items"] = ALIGN[style.align] ?? style.align;
321
- 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;
322
428
  return d;
323
429
  }
324
- 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) {
325
442
  const d = {};
326
443
  if (node.layout === "stack") {
327
- d["display"] = "flex";
328
- 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";
329
448
  } else if (node.layout === "grid") {
330
- d["display"] = "grid";
331
- d["grid-template-columns"] = `repeat(${node.style.base.columns ?? 1}, 1fr)`;
332
- } 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) {
333
453
  d["position"] = "relative";
334
454
  }
335
455
  return d;
336
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
+ }
337
481
  function constraintDecls(c, parentFree) {
338
482
  const d = {};
339
483
  const transforms = [];
340
484
  if (parentFree) {
341
485
  d["position"] = "absolute";
342
- if (c.left) d["left"] = lengthToCss(c.left);
343
- if (c.right) d["right"] = lengthToCss(c.right);
344
- if (c.top) d["top"] = lengthToCss(c.top);
345
- 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);
346
490
  if (c.centerX) {
347
491
  d["left"] = "50%";
348
492
  transforms.push("translateX(-50%)");
@@ -353,13 +497,15 @@ function constraintDecls(c, parentFree) {
353
497
  }
354
498
  if (transforms.length) d["transform"] = transforms.join(" ");
355
499
  }
356
- if (c.width) d["width"] = sizeToCss(c.width);
357
- 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;
358
504
  return d;
359
505
  }
360
506
  function isParentFree(doc, node) {
361
507
  if (node.parent === null) return false;
362
- const parent = doc.nodes[node.parent];
508
+ const parent = own(doc.nodes, node.parent);
363
509
  return !parent || parent.layout === void 0 || parent.layout === "free";
364
510
  }
365
511
  function serializeRule(selector, decls, indent) {
@@ -371,16 +517,20 @@ ${body2}
371
517
  ${indent}}`;
372
518
  }
373
519
  function emitDocCss(doc) {
520
+ if (!hasNodeMap(doc)) return "";
374
521
  const breakpointPx = DEFAULT_BREAKPOINTS;
375
522
  const blocks = [];
523
+ const themeRule = serializeRule(":root", themeDecls(doc.theme), "");
524
+ if (themeRule) blocks.push(themeRule);
376
525
  for (const id of Object.keys(doc.nodes).sort()) {
377
526
  const node = doc.nodes[id];
378
527
  const selector = selectorFor(id);
379
528
  const parentFree = isParentFree(doc, node);
380
529
  const base = {
381
- ...layoutDecls(node),
530
+ ...containerDecls(node, node.style.base, true),
382
531
  ...node.constraints ? constraintDecls(node.constraints.base, parentFree) : {},
383
- ...styleDecls(node.style.base)
532
+ ...styleDecls(node.style.base),
533
+ ...visibilityDecls(node, node.style.base, true)
384
534
  };
385
535
  const baseRule = serializeRule(selector, base, "");
386
536
  if (baseRule) blocks.push(baseRule);
@@ -388,9 +538,12 @@ function emitDocCss(doc) {
388
538
  if (bp === "base") continue;
389
539
  const px = breakpointPx[bp];
390
540
  if (px === void 0) continue;
541
+ const bpStyle = node.style[bp];
391
542
  const decls = {
543
+ ...bpStyle ? containerDecls(node, bpStyle, false) : {},
392
544
  ...node.constraints?.[bp] ? constraintDecls(node.constraints[bp], parentFree) : {},
393
- ...node.style[bp] ? styleDecls(node.style[bp]) : {}
545
+ ...bpStyle ? styleDecls(bpStyle) : {},
546
+ ...bpStyle ? visibilityDecls(node, bpStyle, false) : {}
394
547
  };
395
548
  const inner = serializeRule(selector, decls, " ");
396
549
  if (inner) blocks.push(`@media (min-width: ${px}px) {
@@ -408,6 +561,48 @@ function resolveValue(v, data) {
408
561
  if (isBinding(v)) return getPath(data, v.$bind);
409
562
  return v;
410
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
+ }
411
606
  var defaultRegistry = {
412
607
  section: ({ children }) => children,
413
608
  stack: ({ children }) => children,
@@ -420,7 +615,12 @@ var defaultRegistry = {
420
615
  // Rich text — inline-formatted HTML (gradient spans, links, <code>, …) that
421
616
  // plain `text` can't express. The author owns the markup (it's their content).
422
617
  richtext: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: text(node.props.html) } }),
423
- 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
+ },
424
624
  divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
425
625
  callout: ({ node, text }) => {
426
626
  const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
@@ -432,15 +632,20 @@ var defaultRegistry = {
432
632
  const variant = text(node.props.variant) || "primary";
433
633
  return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label2 });
434
634
  },
435
- image: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx(
436
- "img",
437
- {
438
- src: text(node.props.src),
439
- alt: text(node.props.alt),
440
- style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
441
- }
442
- )
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
+ }
443
647
  };
648
+ var IMAGE_FITS = /* @__PURE__ */ new Set(["cover", "contain", "fill", "none", "scale-down"]);
444
649
  function buttonStyle(variant) {
445
650
  const base = {
446
651
  display: "inline-flex",
@@ -459,7 +664,7 @@ function buttonStyle(variant) {
459
664
  return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
460
665
  }
461
666
  function RenderNode({ doc, id, registry = defaultRegistry, data }) {
462
- const node = doc.nodes[id];
667
+ const node = own(doc.nodes, id);
463
668
  if (!node) return null;
464
669
  if (node.type === "repeater") {
465
670
  const arr = resolveValue(node.props.items, data);
@@ -475,7 +680,19 @@ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
475
680
  };
476
681
  const renderer = registry[node.type];
477
682
  const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
478
- 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
+ );
479
696
  }
480
697
  function CmsPage({
481
698
  doc: input3,
@@ -793,7 +1010,8 @@ var ADD_DEFAULTS = {
793
1010
  divider: { type: "divider", props: {}, style: {} },
794
1011
  code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
795
1012
  richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
796
- 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: {} }
797
1015
  };
798
1016
  var ADD_MENU = [
799
1017
  { kind: "heading", label: "Heading" },
@@ -809,6 +1027,7 @@ var ADD_MENU = [
809
1027
  { kind: "divider", label: "Divider" },
810
1028
  { kind: "code", label: "Code" },
811
1029
  { kind: "richtext", label: "Rich text" },
1030
+ { kind: "list", label: "List" },
812
1031
  { kind: "repeater", label: "Repeater" }
813
1032
  ];
814
1033
  function resolveInsertParent(doc, selectedId) {