@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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- export { C as Canvas, a as CanvasProps, E as Editor, b as EditorAction, c as EditorApi, d as EditorProps, e as EditorState, I as Inspector, f as InspectorProps, L as LayersPanel, g as LayersPanelProps, i as editorReduce, j as initEditor, u as useEditor } from './useEditor-wdlYQMHg.cjs';
1
+ export { C as Canvas, a as CanvasProps, E as Editor, b as EditorAction, c as EditorApi, d as EditorProps, e as EditorState, I as Inspector, f as InspectorProps, L as LayersPanel, g as LayersPanelProps, i as editorReduce, j as initEditor, u as useEditor } from './useEditor-CIRm2oi-.cjs';
2
2
  import { ReactNode, ReactElement } from 'react';
3
- import { P as PageDoc, h as ElementRegistry, g as DataContext, N as Node, q as StyleProps, L as LayoutMode } from './registry-CUNaBqTs.cjs';
3
+ import { P as PageDoc, h as ElementRegistry, g as DataContext, N as Node, q as StyleProps, L as LayoutMode } from './registry-B4FaQDip.cjs';
4
4
  import '@particle-academy/fancy-doc-commons';
5
5
 
6
6
  /** Animatable per-node transform (mirrors fancy-motion's NodeState, kept local to avoid a hard dep). */
package/dist/editor.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { C as Canvas, a as CanvasProps, E as Editor, b as EditorAction, c as EditorApi, d as EditorProps, e as EditorState, I as Inspector, f as InspectorProps, L as LayersPanel, g as LayersPanelProps, i as editorReduce, j as initEditor, u as useEditor } from './useEditor-aPH5falI.js';
1
+ export { C as Canvas, a as CanvasProps, E as Editor, b as EditorAction, c as EditorApi, d as EditorProps, e as EditorState, I as Inspector, f as InspectorProps, L as LayersPanel, g as LayersPanelProps, i as editorReduce, j as initEditor, u as useEditor } from './useEditor-DZDD9qpS.js';
2
2
  import { ReactNode, ReactElement } from 'react';
3
- import { P as PageDoc, h as ElementRegistry, g as DataContext, N as Node, q as StyleProps, L as LayoutMode } from './registry-CUNaBqTs.js';
3
+ import { P as PageDoc, h as ElementRegistry, g as DataContext, N as Node, q as StyleProps, L as LayoutMode } from './registry-B4FaQDip.js';
4
4
  import '@particle-academy/fancy-doc-commons';
5
5
 
6
6
  /** Animatable per-node transform (mirrors fancy-motion's NodeState, kept local to avoid a hard dep). */
package/dist/editor.js CHANGED
@@ -1,6 +1,6 @@
1
- import { useEditor, useLatestRef, CONTAINER_TYPES, ADD_DEFAULTS, ADD_MENU } from './chunk-SYRU2X45.js';
2
- export { Canvas, Editor, Inspector, LayersPanel, editorReduce, initEditor, useEditor } from './chunk-SYRU2X45.js';
3
- import { isBinding, lastRootId, childrenOf, CmsPage, defaultRegistry } from './chunk-LY6ID2J2.js';
1
+ import { useEditor, useLatestRef, CONTAINER_TYPES, ADD_DEFAULTS, ADD_MENU } from './chunk-IOMIYI45.js';
2
+ export { Canvas, Editor, Inspector, LayersPanel, editorReduce, initEditor, useEditor } from './chunk-IOMIYI45.js';
3
+ import { isBinding, lastRootId, childrenOf, CmsPage, defaultRegistry } from './chunk-SJYKJ6SG.js';
4
4
  import { useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
5
5
  import { ContextMenu } from '@particle-academy/react-fancy';
6
6
  import { fractionalKey } from '@particle-academy/fancy-doc-commons';
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
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) {
@@ -61,6 +61,17 @@ function bindingSources(doc) {
61
61
  }
62
62
  return [...seen.values()];
63
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
64
75
  function childrenOf(doc, parent) {
65
76
  return fancyDocCommons.childrenOf(doc, parent);
66
77
  }
@@ -76,7 +87,7 @@ function descendantIds(doc, id) {
76
87
  return fancyDocCommons.descendantsOf(doc, id);
77
88
  }
78
89
  function patchNode(doc, id, patch) {
79
- const node = doc.nodes[id];
90
+ const node = own(doc.nodes, id);
80
91
  return {
81
92
  ...doc,
82
93
  nodes: { ...doc.nodes, [id]: { ...node, ...patch } },
@@ -90,8 +101,8 @@ function reduce(doc, op, opts = {}) {
90
101
  };
91
102
  switch (op.t) {
92
103
  case "insert_node": {
93
- if (doc.nodes[op.node.id]) return invalid("node id already exists");
94
- 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");
95
106
  return {
96
107
  ...doc,
97
108
  nodes: { ...doc.nodes, [op.node.id]: op.node },
@@ -99,11 +110,11 @@ function reduce(doc, op, opts = {}) {
99
110
  };
100
111
  }
101
112
  case "remove_node": {
102
- if (!doc.nodes[op.id]) return invalid("node not found");
113
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
103
114
  const dead = /* @__PURE__ */ new Set([op.id, ...descendantIds(doc, op.id)]);
104
115
  const nodes = {};
105
116
  for (const id of Object.keys(doc.nodes)) {
106
- if (!dead.has(id)) nodes[id] = doc.nodes[id];
117
+ if (!dead.has(id)) setOwn(nodes, id, own(doc.nodes, id));
107
118
  }
108
119
  return {
109
120
  ...doc,
@@ -112,10 +123,10 @@ function reduce(doc, op, opts = {}) {
112
123
  };
113
124
  }
114
125
  case "move_node": {
115
- const node = doc.nodes[op.id];
126
+ const node = own(doc.nodes, op.id);
116
127
  if (!node) return invalid("node not found");
117
128
  if (op.parent === op.id) return invalid("cannot parent a node to itself");
118
- 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");
119
130
  if (op.parent !== null && descendantIds(doc, op.id).includes(op.parent)) {
120
131
  return invalid("move would create a cycle");
121
132
  }
@@ -126,34 +137,35 @@ function reduce(doc, op, opts = {}) {
126
137
  };
127
138
  }
128
139
  case "set_node_props": {
129
- 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");
130
142
  return patchNode(doc, op.id, {
131
- props: { ...doc.nodes[op.id].props, ...op.patch }
143
+ props: { ...node.props, ...op.patch }
132
144
  });
133
145
  }
134
146
  case "set_layout": {
135
- if (!doc.nodes[op.id]) return invalid("node not found");
147
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
136
148
  return patchNode(doc, op.id, { layout: op.layout });
137
149
  }
138
150
  case "set_style": {
139
- const node = doc.nodes[op.id];
151
+ const node = own(doc.nodes, op.id);
140
152
  if (!node) return invalid("node not found");
141
- const prev = node.style[op.breakpoint] ?? {};
153
+ const prev = own(node.style, op.breakpoint) ?? {};
142
154
  return patchNode(doc, op.id, {
143
155
  style: { ...node.style, [op.breakpoint]: { ...prev, ...op.patch } }
144
156
  });
145
157
  }
146
158
  case "set_constraints": {
147
- const node = doc.nodes[op.id];
159
+ const node = own(doc.nodes, op.id);
148
160
  if (!node) return invalid("node not found");
149
161
  const base = node.constraints ?? { base: { width: "hug", height: "hug" } };
150
- const prev = base[op.breakpoint] ?? {};
162
+ const prev = own(base, op.breakpoint) ?? {};
151
163
  return patchNode(doc, op.id, {
152
164
  constraints: { ...base, [op.breakpoint]: { ...prev, ...op.patch } }
153
165
  });
154
166
  }
155
167
  case "set_animation": {
156
- const node = doc.nodes[op.id];
168
+ const node = own(doc.nodes, op.id);
157
169
  if (!node) return invalid("node not found");
158
170
  const list = node.animations ?? [];
159
171
  const idx = list.findIndex((a) => a.id === op.anim.id);
@@ -162,14 +174,14 @@ function reduce(doc, op, opts = {}) {
162
174
  });
163
175
  }
164
176
  case "remove_animation": {
165
- const node = doc.nodes[op.id];
177
+ const node = own(doc.nodes, op.id);
166
178
  if (!node) return invalid("node not found");
167
179
  return patchNode(doc, op.id, {
168
180
  animations: (node.animations ?? []).filter((a) => a.id !== op.animId)
169
181
  });
170
182
  }
171
183
  case "set_action": {
172
- const node = doc.nodes[op.id];
184
+ const node = own(doc.nodes, op.id);
173
185
  if (!node) return invalid("node not found");
174
186
  const list = node.actions ?? [];
175
187
  let actions;
@@ -185,13 +197,13 @@ function reduce(doc, op, opts = {}) {
185
197
  case "reorder_roots": {
186
198
  const currentRoots = childrenOf(doc, null);
187
199
  const current = new Set(currentRoots.map((n) => n.id));
188
- 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));
189
201
  if (!same) return invalid("reorder must be a permutation of the current roots");
190
202
  const nodes = { ...doc.nodes };
191
203
  let prev = null;
192
204
  for (const id of op.order) {
193
205
  prev = fancyDocCommons.fractionalKey(prev, null);
194
- nodes[id] = { ...nodes[id], order: prev };
206
+ setOwn(nodes, id, { ...own(nodes, id), order: prev });
195
207
  }
196
208
  return { ...doc, nodes, seq: doc.seq + 1 };
197
209
  }
@@ -207,17 +219,17 @@ function invert(doc, op) {
207
219
  case "insert_node":
208
220
  return [{ t: "remove_node", id: op.node.id }];
209
221
  case "remove_node": {
210
- const node = doc.nodes[op.id];
222
+ const node = own(doc.nodes, op.id);
211
223
  if (!node) return [];
212
224
  const ids = [op.id, ...descendantIds(doc, op.id)];
213
- 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) }));
214
226
  if (node.parent === null) {
215
227
  inserts.push({ t: "reorder_roots", order: childrenOf(doc, null).map((n) => n.id) });
216
228
  }
217
229
  return inserts;
218
230
  }
219
231
  case "move_node": {
220
- const node = doc.nodes[op.id];
232
+ const node = own(doc.nodes, op.id);
221
233
  if (!node) return [];
222
234
  const back = [{ t: "move_node", id: op.id, parent: node.parent, order: node.order }];
223
235
  if (node.parent === null) {
@@ -226,19 +238,19 @@ function invert(doc, op) {
226
238
  return back;
227
239
  }
228
240
  case "set_node_props": {
229
- const node = doc.nodes[op.id];
241
+ const node = own(doc.nodes, op.id);
230
242
  if (!node) return [];
231
243
  const prev = {};
232
244
  for (const k of Object.keys(op.patch)) prev[k] = node.props[k];
233
245
  return [{ t: "set_node_props", id: op.id, patch: prev }];
234
246
  }
235
247
  case "set_layout": {
236
- const node = doc.nodes[op.id];
248
+ const node = own(doc.nodes, op.id);
237
249
  if (!node) return [];
238
250
  return [{ t: "set_layout", id: op.id, layout: node.layout }];
239
251
  }
240
252
  case "set_style": {
241
- const node = doc.nodes[op.id];
253
+ const node = own(doc.nodes, op.id);
242
254
  if (!node) return [];
243
255
  const prevBp = node.style[op.breakpoint] ?? {};
244
256
  const prev = {};
@@ -276,7 +288,7 @@ function migrateDocVerbose(input2) {
276
288
  const ordered = [];
277
289
  const seen = /* @__PURE__ */ new Set();
278
290
  for (const id of sections) {
279
- if (!normalized.nodes[id]) {
291
+ if (!own(normalized.nodes, id)) {
280
292
  danglingIds.push(id);
281
293
  continue;
282
294
  }
@@ -290,7 +302,7 @@ function migrateDocVerbose(input2) {
290
302
  let prev = null;
291
303
  for (const id of finalOrder) {
292
304
  const key = fancyDocCommons.fractionalKey(prev, null);
293
- nodes[id] = { ...nodes[id], order: key };
305
+ setOwn(nodes, id, { ...own(nodes, id), order: key });
294
306
  prev = key;
295
307
  }
296
308
  const { sections: _dropped, ...rest } = legacy;
@@ -309,15 +321,36 @@ function needsMigration(input2) {
309
321
  }
310
322
 
311
323
  // src/render/css.ts
312
- 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"]);
313
336
  function lengthToCss(len) {
314
- 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}`;
315
341
  }
316
342
  function sizeToCss(size) {
317
343
  if (size === "fill") return "100%";
318
344
  if (size === "hug") return "auto";
319
345
  return lengthToCss(size);
320
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
+ }
321
354
  var ALIGN = {
322
355
  start: "flex-start",
323
356
  center: "center",
@@ -331,51 +364,150 @@ var JUSTIFY = {
331
364
  between: "space-between",
332
365
  around: "space-around"
333
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
+ }
334
402
  function styleDecls(style) {
335
403
  const d = {};
336
- if (style.padding) d["padding"] = lengthToCss(style.padding);
337
- if (style.margin) d["margin"] = lengthToCss(style.margin);
338
- if (style.radius) d["border-radius"] = lengthToCss(style.radius);
339
- if (style.background !== void 0) d["background"] = style.background;
340
- if (style.color !== void 0) d["color"] = style.color;
341
- if (style.opacity !== void 0) d["opacity"] = String(style.opacity);
342
- if (style.fontFamily !== void 0) d["font-family"] = style.fontFamily;
343
- if (style.fontSize) d["font-size"] = lengthToCss(style.fontSize);
344
- if (style.fontWeight !== void 0) d["font-weight"] = String(style.fontWeight);
345
- if (style.lineHeight !== void 0) d["line-height"] = String(style.lineHeight);
346
- if (style.textAlign !== void 0) d["text-align"] = style.textAlign;
347
- if (style.letterSpacing) d["letter-spacing"] = lengthToCss(style.letterSpacing);
348
- if (style.border !== void 0) d["border"] = style.border;
349
- if (style.boxShadow !== void 0) d["box-shadow"] = style.boxShadow;
350
- if (style.transform !== void 0) d["transform"] = style.transform;
351
- if (style.filter !== void 0) d["filter"] = style.filter;
352
- if (style.gap) d["gap"] = lengthToCss(style.gap);
353
- if (style.align !== void 0) d["align-items"] = ALIGN[style.align] ?? style.align;
354
- 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;
355
449
  return d;
356
450
  }
357
- 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) {
358
463
  const d = {};
359
464
  if (node.layout === "stack") {
360
- d["display"] = "flex";
361
- 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";
362
469
  } else if (node.layout === "grid") {
363
- d["display"] = "grid";
364
- d["grid-template-columns"] = `repeat(${node.style.base.columns ?? 1}, 1fr)`;
365
- } 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) {
366
474
  d["position"] = "relative";
367
475
  }
368
476
  return d;
369
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
+ }
370
502
  function constraintDecls(c, parentFree) {
371
503
  const d = {};
372
504
  const transforms = [];
373
505
  if (parentFree) {
374
506
  d["position"] = "absolute";
375
- if (c.left) d["left"] = lengthToCss(c.left);
376
- if (c.right) d["right"] = lengthToCss(c.right);
377
- if (c.top) d["top"] = lengthToCss(c.top);
378
- 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);
379
511
  if (c.centerX) {
380
512
  d["left"] = "50%";
381
513
  transforms.push("translateX(-50%)");
@@ -386,13 +518,15 @@ function constraintDecls(c, parentFree) {
386
518
  }
387
519
  if (transforms.length) d["transform"] = transforms.join(" ");
388
520
  }
389
- if (c.width) d["width"] = sizeToCss(c.width);
390
- 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;
391
525
  return d;
392
526
  }
393
527
  function isParentFree(doc, node) {
394
528
  if (node.parent === null) return false;
395
- const parent = doc.nodes[node.parent];
529
+ const parent = own(doc.nodes, node.parent);
396
530
  return !parent || parent.layout === void 0 || parent.layout === "free";
397
531
  }
398
532
  function serializeRule(selector, decls, indent) {
@@ -407,14 +541,17 @@ function emitDocCss(doc) {
407
541
  if (!hasNodeMap(doc)) return "";
408
542
  const breakpointPx = DEFAULT_BREAKPOINTS;
409
543
  const blocks = [];
544
+ const themeRule = serializeRule(":root", themeDecls(doc.theme), "");
545
+ if (themeRule) blocks.push(themeRule);
410
546
  for (const id of Object.keys(doc.nodes).sort()) {
411
547
  const node = doc.nodes[id];
412
548
  const selector = selectorFor(id);
413
549
  const parentFree = isParentFree(doc, node);
414
550
  const base = {
415
- ...layoutDecls(node),
551
+ ...containerDecls(node, node.style.base, true),
416
552
  ...node.constraints ? constraintDecls(node.constraints.base, parentFree) : {},
417
- ...styleDecls(node.style.base)
553
+ ...styleDecls(node.style.base),
554
+ ...visibilityDecls(node, node.style.base, true)
418
555
  };
419
556
  const baseRule = serializeRule(selector, base, "");
420
557
  if (baseRule) blocks.push(baseRule);
@@ -422,9 +559,12 @@ function emitDocCss(doc) {
422
559
  if (bp === "base") continue;
423
560
  const px = breakpointPx[bp];
424
561
  if (px === void 0) continue;
562
+ const bpStyle = node.style[bp];
425
563
  const decls = {
564
+ ...bpStyle ? containerDecls(node, bpStyle, false) : {},
426
565
  ...node.constraints?.[bp] ? constraintDecls(node.constraints[bp], parentFree) : {},
427
- ...node.style[bp] ? styleDecls(node.style[bp]) : {}
566
+ ...bpStyle ? styleDecls(bpStyle) : {},
567
+ ...bpStyle ? visibilityDecls(node, bpStyle, false) : {}
428
568
  };
429
569
  const inner = serializeRule(selector, decls, " ");
430
570
  if (inner) blocks.push(`@media (min-width: ${px}px) {
@@ -442,6 +582,48 @@ function resolveValue(v, data) {
442
582
  if (isBinding(v)) return getPath(data, v.$bind);
443
583
  return v;
444
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
+ }
445
627
  var defaultRegistry = {
446
628
  section: ({ children }) => children,
447
629
  stack: ({ children }) => children,
@@ -454,7 +636,12 @@ var defaultRegistry = {
454
636
  // Rich text — inline-formatted HTML (gradient spans, links, <code>, …) that
455
637
  // plain `text` can't express. The author owns the markup (it's their content).
456
638
  richtext: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: text(node.props.html) } }),
457
- 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
+ },
458
645
  divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
459
646
  callout: ({ node, text }) => {
460
647
  const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
@@ -466,15 +653,20 @@ var defaultRegistry = {
466
653
  const variant = text(node.props.variant) || "primary";
467
654
  return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label2 });
468
655
  },
469
- image: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx(
470
- "img",
471
- {
472
- src: text(node.props.src),
473
- alt: text(node.props.alt),
474
- style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
475
- }
476
- )
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
+ }
477
668
  };
669
+ var IMAGE_FITS = /* @__PURE__ */ new Set(["cover", "contain", "fill", "none", "scale-down"]);
478
670
  function buttonStyle(variant) {
479
671
  const base = {
480
672
  display: "inline-flex",
@@ -493,7 +685,7 @@ function buttonStyle(variant) {
493
685
  return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
494
686
  }
495
687
  function RenderNode({ doc, id, registry = defaultRegistry, data }) {
496
- const node = doc.nodes[id];
688
+ const node = own(doc.nodes, id);
497
689
  if (!node) return null;
498
690
  if (node.type === "repeater") {
499
691
  const arr = resolveValue(node.props.items, data);
@@ -509,7 +701,19 @@ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
509
701
  };
510
702
  const renderer = registry[node.type];
511
703
  const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
512
- 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
+ );
513
717
  }
514
718
  function CmsPage({
515
719
  doc: input2,
@@ -911,7 +1115,8 @@ var ADD_DEFAULTS = {
911
1115
  divider: { type: "divider", props: {}, style: {} },
912
1116
  code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
913
1117
  richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
914
- 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: {} }
915
1120
  };
916
1121
  var ADD_MENU = [
917
1122
  { kind: "heading", label: "Heading" },
@@ -927,6 +1132,7 @@ var ADD_MENU = [
927
1132
  { kind: "divider", label: "Divider" },
928
1133
  { kind: "code", label: "Code" },
929
1134
  { kind: "richtext", label: "Rich text" },
1135
+ { kind: "list", label: "List" },
930
1136
  { kind: "repeater", label: "Repeater" }
931
1137
  ];
932
1138
  function resolveInsertParent(doc, selectedId) {