@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/index.cjs CHANGED
@@ -1,13 +1,18 @@
1
1
  'use strict';
2
2
 
3
3
  var fancyDocCommons = require('@particle-academy/fancy-doc-commons');
4
- var jsxRuntime = require('react/jsx-runtime');
5
4
  var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
7
  // src/document/types.ts
8
8
  function isBinding(v) {
9
9
  return typeof v === "object" && v !== null && typeof v.$bind === "string";
10
10
  }
11
+ function hasNodeMap(value) {
12
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
13
+ const nodes = value.nodes;
14
+ return typeof nodes === "object" && nodes !== null && !Array.isArray(nodes);
15
+ }
11
16
  var DEFAULT_BREAKPOINTS = {
12
17
  base: 0,
13
18
  md: 768,
@@ -56,10 +61,22 @@ function bindingSources(doc) {
56
61
  }
57
62
  return [...seen.values()];
58
63
  }
64
+
65
+ // src/document/own.ts
66
+ var has = Object.prototype.hasOwnProperty;
67
+ function own(map, key) {
68
+ return map != null && has.call(map, key) ? map[key] : void 0;
69
+ }
70
+ function setOwn(map, key, value) {
71
+ Object.defineProperty(map, key, { value, writable: true, enumerable: true, configurable: true });
72
+ }
73
+
74
+ // src/document/reduce.ts
59
75
  function childrenOf(doc, parent) {
60
76
  return fancyDocCommons.childrenOf(doc, parent);
61
77
  }
62
78
  function rootIds(doc) {
79
+ if (!hasNodeMap(doc)) return [];
63
80
  return childrenOf(doc, null).map((n) => n.id);
64
81
  }
65
82
  function lastRootId(doc) {
@@ -70,7 +87,7 @@ function descendantIds(doc, id) {
70
87
  return fancyDocCommons.descendantsOf(doc, id);
71
88
  }
72
89
  function patchNode(doc, id, patch) {
73
- const node = doc.nodes[id];
90
+ const node = own(doc.nodes, id);
74
91
  return {
75
92
  ...doc,
76
93
  nodes: { ...doc.nodes, [id]: { ...node, ...patch } },
@@ -84,8 +101,8 @@ function reduce(doc, op, opts = {}) {
84
101
  };
85
102
  switch (op.t) {
86
103
  case "insert_node": {
87
- if (doc.nodes[op.node.id]) return invalid("node id already exists");
88
- if (op.node.parent !== null && !doc.nodes[op.node.parent]) return invalid("parent not found");
104
+ if (own(doc.nodes, op.node.id)) return invalid("node id already exists");
105
+ if (op.node.parent !== null && !own(doc.nodes, op.node.parent)) return invalid("parent not found");
89
106
  return {
90
107
  ...doc,
91
108
  nodes: { ...doc.nodes, [op.node.id]: op.node },
@@ -93,11 +110,11 @@ function reduce(doc, op, opts = {}) {
93
110
  };
94
111
  }
95
112
  case "remove_node": {
96
- if (!doc.nodes[op.id]) return invalid("node not found");
113
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
97
114
  const dead = /* @__PURE__ */ new Set([op.id, ...descendantIds(doc, op.id)]);
98
115
  const nodes = {};
99
116
  for (const id of Object.keys(doc.nodes)) {
100
- if (!dead.has(id)) nodes[id] = doc.nodes[id];
117
+ if (!dead.has(id)) setOwn(nodes, id, own(doc.nodes, id));
101
118
  }
102
119
  return {
103
120
  ...doc,
@@ -106,10 +123,10 @@ function reduce(doc, op, opts = {}) {
106
123
  };
107
124
  }
108
125
  case "move_node": {
109
- const node = doc.nodes[op.id];
126
+ const node = own(doc.nodes, op.id);
110
127
  if (!node) return invalid("node not found");
111
128
  if (op.parent === op.id) return invalid("cannot parent a node to itself");
112
- if (op.parent !== null && !doc.nodes[op.parent]) return invalid("new parent not found");
129
+ if (op.parent !== null && !own(doc.nodes, op.parent)) return invalid("new parent not found");
113
130
  if (op.parent !== null && descendantIds(doc, op.id).includes(op.parent)) {
114
131
  return invalid("move would create a cycle");
115
132
  }
@@ -120,34 +137,35 @@ function reduce(doc, op, opts = {}) {
120
137
  };
121
138
  }
122
139
  case "set_node_props": {
123
- if (!doc.nodes[op.id]) return invalid("node not found");
140
+ const node = own(doc.nodes, op.id);
141
+ if (!node) return invalid("node not found");
124
142
  return patchNode(doc, op.id, {
125
- props: { ...doc.nodes[op.id].props, ...op.patch }
143
+ props: { ...node.props, ...op.patch }
126
144
  });
127
145
  }
128
146
  case "set_layout": {
129
- if (!doc.nodes[op.id]) return invalid("node not found");
147
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
130
148
  return patchNode(doc, op.id, { layout: op.layout });
131
149
  }
132
150
  case "set_style": {
133
- const node = doc.nodes[op.id];
151
+ const node = own(doc.nodes, op.id);
134
152
  if (!node) return invalid("node not found");
135
- const prev = node.style[op.breakpoint] ?? {};
153
+ const prev = own(node.style, op.breakpoint) ?? {};
136
154
  return patchNode(doc, op.id, {
137
155
  style: { ...node.style, [op.breakpoint]: { ...prev, ...op.patch } }
138
156
  });
139
157
  }
140
158
  case "set_constraints": {
141
- const node = doc.nodes[op.id];
159
+ const node = own(doc.nodes, op.id);
142
160
  if (!node) return invalid("node not found");
143
161
  const base = node.constraints ?? { base: { width: "hug", height: "hug" } };
144
- const prev = base[op.breakpoint] ?? {};
162
+ const prev = own(base, op.breakpoint) ?? {};
145
163
  return patchNode(doc, op.id, {
146
164
  constraints: { ...base, [op.breakpoint]: { ...prev, ...op.patch } }
147
165
  });
148
166
  }
149
167
  case "set_animation": {
150
- const node = doc.nodes[op.id];
168
+ const node = own(doc.nodes, op.id);
151
169
  if (!node) return invalid("node not found");
152
170
  const list = node.animations ?? [];
153
171
  const idx = list.findIndex((a) => a.id === op.anim.id);
@@ -156,14 +174,14 @@ function reduce(doc, op, opts = {}) {
156
174
  });
157
175
  }
158
176
  case "remove_animation": {
159
- const node = doc.nodes[op.id];
177
+ const node = own(doc.nodes, op.id);
160
178
  if (!node) return invalid("node not found");
161
179
  return patchNode(doc, op.id, {
162
180
  animations: (node.animations ?? []).filter((a) => a.id !== op.animId)
163
181
  });
164
182
  }
165
183
  case "set_action": {
166
- const node = doc.nodes[op.id];
184
+ const node = own(doc.nodes, op.id);
167
185
  if (!node) return invalid("node not found");
168
186
  const list = node.actions ?? [];
169
187
  let actions;
@@ -179,13 +197,13 @@ function reduce(doc, op, opts = {}) {
179
197
  case "reorder_roots": {
180
198
  const currentRoots = childrenOf(doc, null);
181
199
  const current = new Set(currentRoots.map((n) => n.id));
182
- const same = op.order.length === currentRoots.length && op.order.every((id) => current.has(id));
200
+ const same = op.order.length === currentRoots.length && new Set(op.order).size === op.order.length && op.order.every((id) => current.has(id));
183
201
  if (!same) return invalid("reorder must be a permutation of the current roots");
184
202
  const nodes = { ...doc.nodes };
185
203
  let prev = null;
186
204
  for (const id of op.order) {
187
205
  prev = fancyDocCommons.fractionalKey(prev, null);
188
- nodes[id] = { ...nodes[id], order: prev };
206
+ setOwn(nodes, id, { ...own(nodes, id), order: prev });
189
207
  }
190
208
  return { ...doc, nodes, seq: doc.seq + 1 };
191
209
  }
@@ -201,17 +219,17 @@ function invert(doc, op) {
201
219
  case "insert_node":
202
220
  return [{ t: "remove_node", id: op.node.id }];
203
221
  case "remove_node": {
204
- const node = doc.nodes[op.id];
222
+ const node = own(doc.nodes, op.id);
205
223
  if (!node) return [];
206
224
  const ids = [op.id, ...descendantIds(doc, op.id)];
207
- const inserts = ids.map((id) => ({ t: "insert_node", node: doc.nodes[id] }));
225
+ const inserts = ids.map((id) => ({ t: "insert_node", node: own(doc.nodes, id) }));
208
226
  if (node.parent === null) {
209
227
  inserts.push({ t: "reorder_roots", order: childrenOf(doc, null).map((n) => n.id) });
210
228
  }
211
229
  return inserts;
212
230
  }
213
231
  case "move_node": {
214
- const node = doc.nodes[op.id];
232
+ const node = own(doc.nodes, op.id);
215
233
  if (!node) return [];
216
234
  const back = [{ t: "move_node", id: op.id, parent: node.parent, order: node.order }];
217
235
  if (node.parent === null) {
@@ -220,19 +238,19 @@ function invert(doc, op) {
220
238
  return back;
221
239
  }
222
240
  case "set_node_props": {
223
- const node = doc.nodes[op.id];
241
+ const node = own(doc.nodes, op.id);
224
242
  if (!node) return [];
225
243
  const prev = {};
226
244
  for (const k of Object.keys(op.patch)) prev[k] = node.props[k];
227
245
  return [{ t: "set_node_props", id: op.id, patch: prev }];
228
246
  }
229
247
  case "set_layout": {
230
- const node = doc.nodes[op.id];
248
+ const node = own(doc.nodes, op.id);
231
249
  if (!node) return [];
232
250
  return [{ t: "set_layout", id: op.id, layout: node.layout }];
233
251
  }
234
252
  case "set_style": {
235
- const node = doc.nodes[op.id];
253
+ const node = own(doc.nodes, op.id);
236
254
  if (!node) return [];
237
255
  const prevBp = node.style[op.breakpoint] ?? {};
238
256
  const prev = {};
@@ -254,17 +272,23 @@ function pick(obj, keys) {
254
272
  for (const k of keys) out[k] = obj[k];
255
273
  return out;
256
274
  }
275
+ function normalizeDoc(input2) {
276
+ if (hasNodeMap(input2)) return input2;
277
+ const id = typeof input2 === "object" && input2 !== null && !Array.isArray(input2) && typeof input2.id === "string" ? input2.id : "malformed";
278
+ return emptyDoc(id);
279
+ }
257
280
  function migrateDocVerbose(input2) {
258
- const legacy = input2;
281
+ const normalized = normalizeDoc(input2);
282
+ const legacy = normalized;
259
283
  const sections = legacy.sections;
260
284
  if (!Array.isArray(sections)) {
261
- return { doc: input2, migrated: false, danglingIds: [], unlistedRootIds: [] };
285
+ return { doc: normalized, migrated: false, danglingIds: [], unlistedRootIds: [] };
262
286
  }
263
287
  const danglingIds = [];
264
288
  const ordered = [];
265
289
  const seen = /* @__PURE__ */ new Set();
266
290
  for (const id of sections) {
267
- if (!input2.nodes[id]) {
291
+ if (!own(normalized.nodes, id)) {
268
292
  danglingIds.push(id);
269
293
  continue;
270
294
  }
@@ -272,13 +296,13 @@ function migrateDocVerbose(input2) {
272
296
  seen.add(id);
273
297
  ordered.push(id);
274
298
  }
275
- const unlistedRootIds = Object.values(input2.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);
299
+ 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);
276
300
  const finalOrder = [...ordered, ...unlistedRootIds];
277
- const nodes = { ...input2.nodes };
301
+ const nodes = { ...normalized.nodes };
278
302
  let prev = null;
279
303
  for (const id of finalOrder) {
280
304
  const key = fancyDocCommons.fractionalKey(prev, null);
281
- nodes[id] = { ...nodes[id], order: key };
305
+ setOwn(nodes, id, { ...own(nodes, id), order: key });
282
306
  prev = key;
283
307
  }
284
308
  const { sections: _dropped, ...rest } = legacy;
@@ -297,15 +321,36 @@ function needsMigration(input2) {
297
321
  }
298
322
 
299
323
  // src/render/css.ts
300
- var selectorFor = (id) => `[data-cms="${id}"]`;
324
+ function cssString(value) {
325
+ let out = "";
326
+ for (const ch of value) {
327
+ const code = ch.codePointAt(0);
328
+ if (ch === "\\" || ch === '"') out += `\\${ch}`;
329
+ else if (code < 32 || code === 127 || ch === "<") out += `\\${code.toString(16)} `;
330
+ else out += ch;
331
+ }
332
+ return out;
333
+ }
334
+ var selectorFor = (id) => `[data-cms="${cssString(id)}"]`;
335
+ var UNITS = /* @__PURE__ */ new Set(["px", "%", "vw", "vh", "rem", "fr"]);
301
336
  function lengthToCss(len) {
302
- return `${len.value}${len.unit}`;
337
+ if (typeof len !== "object" || len === null) return null;
338
+ const { value, unit } = len;
339
+ if (typeof value !== "number" || !Number.isFinite(value) || typeof unit !== "string" || !UNITS.has(unit)) return null;
340
+ return `${value}${unit}`;
303
341
  }
304
342
  function sizeToCss(size) {
305
343
  if (size === "fill") return "100%";
306
344
  if (size === "hug") return "auto";
307
345
  return lengthToCss(size);
308
346
  }
347
+ function numberToCss(value) {
348
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
349
+ }
350
+ function putLen(d, props, len) {
351
+ const css = lengthToCss(len);
352
+ if (css !== null) for (const p of props) d[p] = css;
353
+ }
309
354
  var ALIGN = {
310
355
  start: "flex-start",
311
356
  center: "center",
@@ -319,51 +364,150 @@ var JUSTIFY = {
319
364
  between: "space-between",
320
365
  around: "space-around"
321
366
  };
367
+ function safeCssValue(value) {
368
+ if (/[{}<]/.test(value)) return null;
369
+ let depth = 0;
370
+ let quote = null;
371
+ for (const ch of value) {
372
+ if (quote) {
373
+ if (ch === quote) quote = null;
374
+ } else if (ch === '"' || ch === "'") {
375
+ quote = ch;
376
+ } else if (ch === "(") {
377
+ depth++;
378
+ } else if (ch === ")") {
379
+ depth = Math.max(0, depth - 1);
380
+ } else if (ch === ";" && depth === 0) {
381
+ return null;
382
+ }
383
+ }
384
+ return value;
385
+ }
386
+ function put(d, prop, value) {
387
+ if (value === void 0) return;
388
+ const safe = safeCssValue(value);
389
+ if (safe !== null) d[prop] = safe;
390
+ }
391
+ var OVERFLOW = /* @__PURE__ */ new Set(["visible", "hidden", "auto"]);
392
+ var TEXT_TRANSFORM = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
393
+ var BACKGROUND_SIZE = /* @__PURE__ */ new Set(["cover", "contain", "auto"]);
394
+ var TEXT_ALIGN = /* @__PURE__ */ new Set(["left", "center", "right", "justify"]);
395
+ var DIRECTIONS = /* @__PURE__ */ new Set(["row", "column"]);
396
+ function keyword(map, value) {
397
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(map, value) ? map[value] : null;
398
+ }
399
+ function inSet(set, value) {
400
+ return typeof value === "string" && set.has(value) ? value : null;
401
+ }
322
402
  function styleDecls(style) {
323
403
  const d = {};
324
- if (style.padding) d["padding"] = lengthToCss(style.padding);
325
- if (style.margin) d["margin"] = lengthToCss(style.margin);
326
- if (style.radius) d["border-radius"] = lengthToCss(style.radius);
327
- if (style.background !== void 0) d["background"] = style.background;
328
- if (style.color !== void 0) d["color"] = style.color;
329
- if (style.opacity !== void 0) d["opacity"] = String(style.opacity);
330
- if (style.fontFamily !== void 0) d["font-family"] = style.fontFamily;
331
- if (style.fontSize) d["font-size"] = lengthToCss(style.fontSize);
332
- if (style.fontWeight !== void 0) d["font-weight"] = String(style.fontWeight);
333
- if (style.lineHeight !== void 0) d["line-height"] = String(style.lineHeight);
334
- if (style.textAlign !== void 0) d["text-align"] = style.textAlign;
335
- if (style.letterSpacing) d["letter-spacing"] = lengthToCss(style.letterSpacing);
336
- if (style.border !== void 0) d["border"] = style.border;
337
- if (style.boxShadow !== void 0) d["box-shadow"] = style.boxShadow;
338
- if (style.transform !== void 0) d["transform"] = style.transform;
339
- if (style.filter !== void 0) d["filter"] = style.filter;
340
- if (style.gap) d["gap"] = lengthToCss(style.gap);
341
- if (style.align !== void 0) d["align-items"] = ALIGN[style.align] ?? style.align;
342
- if (style.justify !== void 0) d["justify-content"] = JUSTIFY[style.justify] ?? style.justify;
404
+ putLen(d, ["padding"], style.padding);
405
+ putLen(d, ["padding-left", "padding-right"], style.paddingX);
406
+ putLen(d, ["padding-top", "padding-bottom"], style.paddingY);
407
+ putLen(d, ["margin"], style.margin);
408
+ if (style.marginX === "auto") d["margin-left"] = d["margin-right"] = "auto";
409
+ else putLen(d, ["margin-left", "margin-right"], style.marginX);
410
+ putLen(d, ["margin-top", "margin-bottom"], style.marginY);
411
+ putLen(d, ["border-radius"], style.radius);
412
+ putLen(d, ["max-width"], style.maxWidth);
413
+ putLen(d, ["min-height"], style.minHeight);
414
+ put(d, "aspect-ratio", typeof style.aspectRatio === "string" ? style.aspectRatio : void 0);
415
+ if (style.sticky === true) {
416
+ d["position"] = "sticky";
417
+ d["top"] = "0";
418
+ }
419
+ if (typeof style.zIndex === "number" && Number.isFinite(style.zIndex)) d["z-index"] = String(Math.trunc(style.zIndex));
420
+ const overflow = inSet(OVERFLOW, style.overflow);
421
+ if (overflow) d["overflow"] = overflow;
422
+ put(d, "background", typeof style.background === "string" ? style.background : void 0);
423
+ const backgroundSize = inSet(BACKGROUND_SIZE, style.backgroundSize);
424
+ if (backgroundSize) d["background-size"] = backgroundSize;
425
+ put(d, "background-position", typeof style.backgroundPosition === "string" ? style.backgroundPosition : void 0);
426
+ put(d, "color", typeof style.color === "string" ? style.color : void 0);
427
+ const opacity = numberToCss(style.opacity);
428
+ if (opacity !== null) d["opacity"] = opacity;
429
+ put(d, "font-family", typeof style.fontFamily === "string" ? style.fontFamily : void 0);
430
+ putLen(d, ["font-size"], style.fontSize);
431
+ const fontWeight = numberToCss(style.fontWeight);
432
+ if (fontWeight !== null) d["font-weight"] = fontWeight;
433
+ const lineHeight = numberToCss(style.lineHeight);
434
+ if (lineHeight !== null) d["line-height"] = lineHeight;
435
+ const textAlign = inSet(TEXT_ALIGN, style.textAlign);
436
+ if (textAlign) d["text-align"] = textAlign;
437
+ putLen(d, ["letter-spacing"], style.letterSpacing);
438
+ const textTransform = inSet(TEXT_TRANSFORM, style.textTransform);
439
+ if (textTransform) d["text-transform"] = textTransform;
440
+ put(d, "border", typeof style.border === "string" ? style.border : void 0);
441
+ put(d, "box-shadow", typeof style.boxShadow === "string" ? style.boxShadow : void 0);
442
+ put(d, "transform", typeof style.transform === "string" ? style.transform : void 0);
443
+ put(d, "filter", typeof style.filter === "string" ? style.filter : void 0);
444
+ putLen(d, ["gap"], style.gap);
445
+ const align = keyword(ALIGN, style.align);
446
+ if (align) d["align-items"] = align;
447
+ const justify = keyword(JUSTIFY, style.justify);
448
+ if (justify) d["justify-content"] = justify;
343
449
  return d;
344
450
  }
345
- function layoutDecls(node) {
451
+ function shownDisplay(node) {
452
+ if (node.layout === "stack") return "flex";
453
+ if (node.layout === "grid") return "grid";
454
+ return "block";
455
+ }
456
+ function gridColumns(style) {
457
+ const min = lengthToCss(style.columnMin);
458
+ if (min !== null) return `repeat(auto-fit, minmax(min(${min}, 100%), 1fr))`;
459
+ if (typeof style.columns === "number" && Number.isFinite(style.columns)) return `repeat(${Math.max(1, Math.trunc(style.columns))}, 1fr)`;
460
+ return void 0;
461
+ }
462
+ function containerDecls(node, style, isBase) {
346
463
  const d = {};
347
464
  if (node.layout === "stack") {
348
- d["display"] = "flex";
349
- d["flex-direction"] = node.style.base.direction ?? "column";
465
+ if (isBase) d["display"] = "flex";
466
+ const direction = inSet(DIRECTIONS, style.direction);
467
+ if (isBase || direction) d["flex-direction"] = direction ?? "column";
468
+ if (style.wrap !== void 0) d["flex-wrap"] = style.wrap ? "wrap" : "nowrap";
350
469
  } else if (node.layout === "grid") {
351
- d["display"] = "grid";
352
- d["grid-template-columns"] = `repeat(${node.style.base.columns ?? 1}, 1fr)`;
353
- } else if (node.layout === "free") {
470
+ if (isBase) d["display"] = "grid";
471
+ const cols = gridColumns(style);
472
+ if (isBase || cols !== void 0) d["grid-template-columns"] = cols ?? "repeat(1, 1fr)";
473
+ } else if (node.layout === "free" && isBase) {
354
474
  d["position"] = "relative";
355
475
  }
356
476
  return d;
357
477
  }
478
+ function visibilityDecls(node, style, isBase) {
479
+ if (style.hidden === true) return { display: "none" };
480
+ if (style.hidden === false && !isBase) return { display: shownDisplay(node) };
481
+ return {};
482
+ }
483
+ function themeDecls(theme) {
484
+ const d = {};
485
+ const walk = (value, path) => {
486
+ if (typeof value === "string" || typeof value === "number" && Number.isFinite(value)) {
487
+ const key = path.map((p) => p.toLowerCase().replace(/[^a-z0-9-]/g, "")).filter(Boolean).join("-");
488
+ if (key) put(d, `--cms-${key}`, String(value));
489
+ return;
490
+ }
491
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
492
+ for (const [k, v] of Object.entries(value)) walk(v, [...path, k]);
493
+ }
494
+ };
495
+ if (typeof theme === "object" && theme !== null && !Array.isArray(theme)) {
496
+ for (const [k, v] of Object.entries(theme)) {
497
+ if (k !== "name") walk(v, [k]);
498
+ }
499
+ }
500
+ return d;
501
+ }
358
502
  function constraintDecls(c, parentFree) {
359
503
  const d = {};
360
504
  const transforms = [];
361
505
  if (parentFree) {
362
506
  d["position"] = "absolute";
363
- if (c.left) d["left"] = lengthToCss(c.left);
364
- if (c.right) d["right"] = lengthToCss(c.right);
365
- if (c.top) d["top"] = lengthToCss(c.top);
366
- if (c.bottom) d["bottom"] = lengthToCss(c.bottom);
507
+ putLen(d, ["left"], c.left);
508
+ putLen(d, ["right"], c.right);
509
+ putLen(d, ["top"], c.top);
510
+ putLen(d, ["bottom"], c.bottom);
367
511
  if (c.centerX) {
368
512
  d["left"] = "50%";
369
513
  transforms.push("translateX(-50%)");
@@ -374,13 +518,15 @@ function constraintDecls(c, parentFree) {
374
518
  }
375
519
  if (transforms.length) d["transform"] = transforms.join(" ");
376
520
  }
377
- if (c.width) d["width"] = sizeToCss(c.width);
378
- if (c.height) d["height"] = sizeToCss(c.height);
521
+ const width = sizeToCss(c.width);
522
+ if (width !== null) d["width"] = width;
523
+ const height = sizeToCss(c.height);
524
+ if (height !== null) d["height"] = height;
379
525
  return d;
380
526
  }
381
527
  function isParentFree(doc, node) {
382
528
  if (node.parent === null) return false;
383
- const parent = doc.nodes[node.parent];
529
+ const parent = own(doc.nodes, node.parent);
384
530
  return !parent || parent.layout === void 0 || parent.layout === "free";
385
531
  }
386
532
  function serializeRule(selector, decls, indent) {
@@ -392,16 +538,20 @@ ${body}
392
538
  ${indent}}`;
393
539
  }
394
540
  function emitDocCss(doc) {
541
+ if (!hasNodeMap(doc)) return "";
395
542
  const breakpointPx = DEFAULT_BREAKPOINTS;
396
543
  const blocks = [];
544
+ const themeRule = serializeRule(":root", themeDecls(doc.theme), "");
545
+ if (themeRule) blocks.push(themeRule);
397
546
  for (const id of Object.keys(doc.nodes).sort()) {
398
547
  const node = doc.nodes[id];
399
548
  const selector = selectorFor(id);
400
549
  const parentFree = isParentFree(doc, node);
401
550
  const base = {
402
- ...layoutDecls(node),
551
+ ...containerDecls(node, node.style.base, true),
403
552
  ...node.constraints ? constraintDecls(node.constraints.base, parentFree) : {},
404
- ...styleDecls(node.style.base)
553
+ ...styleDecls(node.style.base),
554
+ ...visibilityDecls(node, node.style.base, true)
405
555
  };
406
556
  const baseRule = serializeRule(selector, base, "");
407
557
  if (baseRule) blocks.push(baseRule);
@@ -409,9 +559,12 @@ function emitDocCss(doc) {
409
559
  if (bp === "base") continue;
410
560
  const px = breakpointPx[bp];
411
561
  if (px === void 0) continue;
562
+ const bpStyle = node.style[bp];
412
563
  const decls = {
564
+ ...bpStyle ? containerDecls(node, bpStyle, false) : {},
413
565
  ...node.constraints?.[bp] ? constraintDecls(node.constraints[bp], parentFree) : {},
414
- ...node.style[bp] ? styleDecls(node.style[bp]) : {}
566
+ ...bpStyle ? styleDecls(bpStyle) : {},
567
+ ...bpStyle ? visibilityDecls(node, bpStyle, false) : {}
415
568
  };
416
569
  const inner = serializeRule(selector, decls, " ");
417
570
  if (inner) blocks.push(`@media (min-width: ${px}px) {
@@ -429,6 +582,48 @@ function resolveValue(v, data) {
429
582
  if (isBinding(v)) return getPath(data, v.$bind);
430
583
  return v;
431
584
  }
585
+ var LANDMARK_TAGS = ["section", "header", "nav", "main", "footer", "article", "aside", "div"];
586
+ var LANDMARKS = new Set(LANDMARK_TAGS);
587
+ var CONTAINERS = /* @__PURE__ */ new Set(["stack", "grid", "frame", "shape", "card"]);
588
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
589
+ function safeHref(raw) {
590
+ const href = raw.trim();
591
+ if (!href) return null;
592
+ const probe = href.replace(/[\u0000- \u007f]+/g, "");
593
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(probe);
594
+ if (scheme && !SAFE_SCHEMES.has(scheme[1].toLowerCase())) return null;
595
+ return href;
596
+ }
597
+ var ANCHOR_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
598
+ function anchorFor(node, text) {
599
+ const anchor = text(node.props.anchor);
600
+ return ANCHOR_PATTERN.test(anchor) ? anchor : null;
601
+ }
602
+ function elementFor(node, text) {
603
+ const as = text(node.props.as);
604
+ switch (node.type) {
605
+ case "heading": {
606
+ const level = Number(text(node.props.level) || 2);
607
+ return { tag: Number.isInteger(level) && level >= 1 && level <= 6 ? `h${level}` : "h2" };
608
+ }
609
+ case "text":
610
+ return { tag: "p" };
611
+ case "section":
612
+ return { tag: LANDMARKS.has(as) ? as : "section" };
613
+ case "list":
614
+ return { tag: text(node.props.ordered) === "true" ? "ol" : "ul" };
615
+ case "button":
616
+ case "link": {
617
+ const href = safeHref(text(node.props.href));
618
+ if (href === null) return { tag: node.type === "link" ? "span" : "div" };
619
+ const attrs = { href };
620
+ if (/^https?:\/\//i.test(href)) attrs.rel = "noopener noreferrer";
621
+ return { tag: "a", attrs };
622
+ }
623
+ default:
624
+ return { tag: CONTAINERS.has(node.type) && LANDMARKS.has(as) ? as : "div" };
625
+ }
626
+ }
432
627
  var defaultRegistry = {
433
628
  section: ({ children }) => children,
434
629
  stack: ({ children }) => children,
@@ -441,7 +636,12 @@ var defaultRegistry = {
441
636
  // Rich text — inline-formatted HTML (gradient spans, links, <code>, …) that
442
637
  // plain `text` can't express. The author owns the markup (it's their content).
443
638
  richtext: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: text(node.props.html) } }),
444
- link: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: text(node.props.href) || "#", style: { color: "inherit", textDecoration: "underline" }, children: text(node.props.content) || "link" }),
639
+ // The [data-cms] element is already the <a> (see elementFor), so a link renders its label only.
640
+ link: ({ node, text }) => text(node.props.content) || "link",
641
+ list: ({ node, resolve }) => {
642
+ const items = resolve(node.props.items);
643
+ return Array.isArray(items) ? items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: item === null || item === void 0 ? "" : String(item) }, i)) : null;
644
+ },
445
645
  divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
446
646
  callout: ({ node, text }) => {
447
647
  const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
@@ -453,15 +653,20 @@ var defaultRegistry = {
453
653
  const variant = text(node.props.variant) || "primary";
454
654
  return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label2 });
455
655
  },
456
- image: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx(
457
- "img",
458
- {
459
- src: text(node.props.src),
460
- alt: text(node.props.alt),
461
- style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
462
- }
463
- )
656
+ image: ({ node, text }) => {
657
+ const fit = text(node.props.fit);
658
+ return /* @__PURE__ */ jsxRuntime.jsx(
659
+ "img",
660
+ {
661
+ src: text(node.props.src),
662
+ alt: text(node.props.alt),
663
+ loading: text(node.props.loading) === "eager" ? "eager" : "lazy",
664
+ style: { width: "100%", height: "100%", objectFit: IMAGE_FITS.has(fit) ? fit : "cover", display: "block" }
665
+ }
666
+ );
667
+ }
464
668
  };
669
+ var IMAGE_FITS = /* @__PURE__ */ new Set(["cover", "contain", "fill", "none", "scale-down"]);
465
670
  function buttonStyle(variant) {
466
671
  const base = {
467
672
  display: "inline-flex",
@@ -480,7 +685,7 @@ function buttonStyle(variant) {
480
685
  return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
481
686
  }
482
687
  function RenderNode({ doc, id, registry = defaultRegistry, data }) {
483
- const node = doc.nodes[id];
688
+ const node = own(doc.nodes, id);
484
689
  if (!node) return null;
485
690
  if (node.type === "repeater") {
486
691
  const arr = resolveValue(node.props.items, data);
@@ -496,7 +701,19 @@ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
496
701
  };
497
702
  const renderer = registry[node.type];
498
703
  const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
499
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cms": node.id, "data-cms-island": node.island ? node.type : void 0, className: node.className, children: inner });
704
+ const { tag, attrs } = elementFor(node, text);
705
+ const anchor = anchorFor(node, text);
706
+ return react.createElement(
707
+ tag,
708
+ {
709
+ "data-cms": node.id,
710
+ "data-cms-island": node.island ? node.type : void 0,
711
+ className: node.className,
712
+ ...anchor ? { id: anchor } : {},
713
+ ...attrs
714
+ },
715
+ inner
716
+ );
500
717
  }
501
718
  function CmsPage({
502
719
  doc: input2,
@@ -898,7 +1115,8 @@ var ADD_DEFAULTS = {
898
1115
  divider: { type: "divider", props: {}, style: {} },
899
1116
  code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
900
1117
  richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
901
- repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
1118
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" },
1119
+ list: { type: "list", props: { items: ["First point", "Second point"], ordered: false }, style: {} }
902
1120
  };
903
1121
  var ADD_MENU = [
904
1122
  { kind: "heading", label: "Heading" },
@@ -914,6 +1132,7 @@ var ADD_MENU = [
914
1132
  { kind: "divider", label: "Divider" },
915
1133
  { kind: "code", label: "Code" },
916
1134
  { kind: "richtext", label: "Rich text" },
1135
+ { kind: "list", label: "List" },
917
1136
  { kind: "repeater", label: "Repeater" }
918
1137
  ];
919
1138
  function resolveInsertParent(doc, selectedId) {
@@ -1121,6 +1340,7 @@ exports.editorReduce = editorReduce;
1121
1340
  exports.emitDocCss = emitDocCss;
1122
1341
  exports.emptyDoc = emptyDoc;
1123
1342
  exports.getPath = getPath;
1343
+ exports.hasNodeMap = hasNodeMap;
1124
1344
  exports.initEditor = initEditor;
1125
1345
  exports.invert = invert;
1126
1346
  exports.isBinding = isBinding;