@particle-academy/fancy-cms-ui 0.7.1 → 0.9.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,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) {
@@ -13,6 +13,12 @@ function hasNodeMap(value) {
13
13
  const nodes = value.nodes;
14
14
  return typeof nodes === "object" && nodes !== null && !Array.isArray(nodes);
15
15
  }
16
+ function withNodeDefaults(node) {
17
+ const props = typeof node.props === "object" && node.props !== null ? node.props : {};
18
+ const style = typeof node.style === "object" && node.style !== null ? node.style : { base: {} };
19
+ const base = typeof style.base === "object" && style.base !== null ? style.base : {};
20
+ return props === node.props && style === node.style && base === style.base ? node : { ...node, props, style: { ...style, base } };
21
+ }
16
22
  var DEFAULT_BREAKPOINTS = {
17
23
  base: 0,
18
24
  md: 768,
@@ -61,6 +67,17 @@ function bindingSources(doc) {
61
67
  }
62
68
  return [...seen.values()];
63
69
  }
70
+
71
+ // src/document/own.ts
72
+ var has = Object.prototype.hasOwnProperty;
73
+ function own(map, key) {
74
+ return map != null && has.call(map, key) ? map[key] : void 0;
75
+ }
76
+ function setOwn(map, key, value) {
77
+ Object.defineProperty(map, key, { value, writable: true, enumerable: true, configurable: true });
78
+ }
79
+
80
+ // src/document/reduce.ts
64
81
  function childrenOf(doc, parent) {
65
82
  return fancyDocCommons.childrenOf(doc, parent);
66
83
  }
@@ -76,7 +93,7 @@ function descendantIds(doc, id) {
76
93
  return fancyDocCommons.descendantsOf(doc, id);
77
94
  }
78
95
  function patchNode(doc, id, patch) {
79
- const node = doc.nodes[id];
96
+ const node = own(doc.nodes, id);
80
97
  return {
81
98
  ...doc,
82
99
  nodes: { ...doc.nodes, [id]: { ...node, ...patch } },
@@ -90,8 +107,8 @@ function reduce(doc, op, opts = {}) {
90
107
  };
91
108
  switch (op.t) {
92
109
  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");
110
+ if (own(doc.nodes, op.node.id)) return invalid("node id already exists");
111
+ if (op.node.parent !== null && !own(doc.nodes, op.node.parent)) return invalid("parent not found");
95
112
  return {
96
113
  ...doc,
97
114
  nodes: { ...doc.nodes, [op.node.id]: op.node },
@@ -99,11 +116,11 @@ function reduce(doc, op, opts = {}) {
99
116
  };
100
117
  }
101
118
  case "remove_node": {
102
- if (!doc.nodes[op.id]) return invalid("node not found");
119
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
103
120
  const dead = /* @__PURE__ */ new Set([op.id, ...descendantIds(doc, op.id)]);
104
121
  const nodes = {};
105
122
  for (const id of Object.keys(doc.nodes)) {
106
- if (!dead.has(id)) nodes[id] = doc.nodes[id];
123
+ if (!dead.has(id)) setOwn(nodes, id, own(doc.nodes, id));
107
124
  }
108
125
  return {
109
126
  ...doc,
@@ -112,10 +129,10 @@ function reduce(doc, op, opts = {}) {
112
129
  };
113
130
  }
114
131
  case "move_node": {
115
- const node = doc.nodes[op.id];
132
+ const node = own(doc.nodes, op.id);
116
133
  if (!node) return invalid("node not found");
117
134
  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");
135
+ if (op.parent !== null && !own(doc.nodes, op.parent)) return invalid("new parent not found");
119
136
  if (op.parent !== null && descendantIds(doc, op.id).includes(op.parent)) {
120
137
  return invalid("move would create a cycle");
121
138
  }
@@ -126,34 +143,35 @@ function reduce(doc, op, opts = {}) {
126
143
  };
127
144
  }
128
145
  case "set_node_props": {
129
- if (!doc.nodes[op.id]) return invalid("node not found");
146
+ const node = own(doc.nodes, op.id);
147
+ if (!node) return invalid("node not found");
130
148
  return patchNode(doc, op.id, {
131
- props: { ...doc.nodes[op.id].props, ...op.patch }
149
+ props: { ...node.props, ...op.patch }
132
150
  });
133
151
  }
134
152
  case "set_layout": {
135
- if (!doc.nodes[op.id]) return invalid("node not found");
153
+ if (!own(doc.nodes, op.id)) return invalid("node not found");
136
154
  return patchNode(doc, op.id, { layout: op.layout });
137
155
  }
138
156
  case "set_style": {
139
- const node = doc.nodes[op.id];
157
+ const node = own(doc.nodes, op.id);
140
158
  if (!node) return invalid("node not found");
141
- const prev = node.style[op.breakpoint] ?? {};
159
+ const prev = own(node.style, op.breakpoint) ?? {};
142
160
  return patchNode(doc, op.id, {
143
161
  style: { ...node.style, [op.breakpoint]: { ...prev, ...op.patch } }
144
162
  });
145
163
  }
146
164
  case "set_constraints": {
147
- const node = doc.nodes[op.id];
165
+ const node = own(doc.nodes, op.id);
148
166
  if (!node) return invalid("node not found");
149
167
  const base = node.constraints ?? { base: { width: "hug", height: "hug" } };
150
- const prev = base[op.breakpoint] ?? {};
168
+ const prev = own(base, op.breakpoint) ?? {};
151
169
  return patchNode(doc, op.id, {
152
170
  constraints: { ...base, [op.breakpoint]: { ...prev, ...op.patch } }
153
171
  });
154
172
  }
155
173
  case "set_animation": {
156
- const node = doc.nodes[op.id];
174
+ const node = own(doc.nodes, op.id);
157
175
  if (!node) return invalid("node not found");
158
176
  const list = node.animations ?? [];
159
177
  const idx = list.findIndex((a) => a.id === op.anim.id);
@@ -162,14 +180,14 @@ function reduce(doc, op, opts = {}) {
162
180
  });
163
181
  }
164
182
  case "remove_animation": {
165
- const node = doc.nodes[op.id];
183
+ const node = own(doc.nodes, op.id);
166
184
  if (!node) return invalid("node not found");
167
185
  return patchNode(doc, op.id, {
168
186
  animations: (node.animations ?? []).filter((a) => a.id !== op.animId)
169
187
  });
170
188
  }
171
189
  case "set_action": {
172
- const node = doc.nodes[op.id];
190
+ const node = own(doc.nodes, op.id);
173
191
  if (!node) return invalid("node not found");
174
192
  const list = node.actions ?? [];
175
193
  let actions;
@@ -185,13 +203,13 @@ function reduce(doc, op, opts = {}) {
185
203
  case "reorder_roots": {
186
204
  const currentRoots = childrenOf(doc, null);
187
205
  const current = new Set(currentRoots.map((n) => n.id));
188
- const same = op.order.length === currentRoots.length && op.order.every((id) => current.has(id));
206
+ const same = op.order.length === currentRoots.length && new Set(op.order).size === op.order.length && op.order.every((id) => current.has(id));
189
207
  if (!same) return invalid("reorder must be a permutation of the current roots");
190
208
  const nodes = { ...doc.nodes };
191
209
  let prev = null;
192
210
  for (const id of op.order) {
193
211
  prev = fancyDocCommons.fractionalKey(prev, null);
194
- nodes[id] = { ...nodes[id], order: prev };
212
+ setOwn(nodes, id, { ...own(nodes, id), order: prev });
195
213
  }
196
214
  return { ...doc, nodes, seq: doc.seq + 1 };
197
215
  }
@@ -207,17 +225,17 @@ function invert(doc, op) {
207
225
  case "insert_node":
208
226
  return [{ t: "remove_node", id: op.node.id }];
209
227
  case "remove_node": {
210
- const node = doc.nodes[op.id];
228
+ const node = own(doc.nodes, op.id);
211
229
  if (!node) return [];
212
230
  const ids = [op.id, ...descendantIds(doc, op.id)];
213
- const inserts = ids.map((id) => ({ t: "insert_node", node: doc.nodes[id] }));
231
+ const inserts = ids.map((id) => ({ t: "insert_node", node: own(doc.nodes, id) }));
214
232
  if (node.parent === null) {
215
233
  inserts.push({ t: "reorder_roots", order: childrenOf(doc, null).map((n) => n.id) });
216
234
  }
217
235
  return inserts;
218
236
  }
219
237
  case "move_node": {
220
- const node = doc.nodes[op.id];
238
+ const node = own(doc.nodes, op.id);
221
239
  if (!node) return [];
222
240
  const back = [{ t: "move_node", id: op.id, parent: node.parent, order: node.order }];
223
241
  if (node.parent === null) {
@@ -226,19 +244,19 @@ function invert(doc, op) {
226
244
  return back;
227
245
  }
228
246
  case "set_node_props": {
229
- const node = doc.nodes[op.id];
247
+ const node = own(doc.nodes, op.id);
230
248
  if (!node) return [];
231
249
  const prev = {};
232
250
  for (const k of Object.keys(op.patch)) prev[k] = node.props[k];
233
251
  return [{ t: "set_node_props", id: op.id, patch: prev }];
234
252
  }
235
253
  case "set_layout": {
236
- const node = doc.nodes[op.id];
254
+ const node = own(doc.nodes, op.id);
237
255
  if (!node) return [];
238
256
  return [{ t: "set_layout", id: op.id, layout: node.layout }];
239
257
  }
240
258
  case "set_style": {
241
- const node = doc.nodes[op.id];
259
+ const node = own(doc.nodes, op.id);
242
260
  if (!node) return [];
243
261
  const prevBp = node.style[op.breakpoint] ?? {};
244
262
  const prev = {};
@@ -276,7 +294,7 @@ function migrateDocVerbose(input2) {
276
294
  const ordered = [];
277
295
  const seen = /* @__PURE__ */ new Set();
278
296
  for (const id of sections) {
279
- if (!normalized.nodes[id]) {
297
+ if (!own(normalized.nodes, id)) {
280
298
  danglingIds.push(id);
281
299
  continue;
282
300
  }
@@ -290,7 +308,7 @@ function migrateDocVerbose(input2) {
290
308
  let prev = null;
291
309
  for (const id of finalOrder) {
292
310
  const key = fancyDocCommons.fractionalKey(prev, null);
293
- nodes[id] = { ...nodes[id], order: key };
311
+ setOwn(nodes, id, { ...own(nodes, id), order: key });
294
312
  prev = key;
295
313
  }
296
314
  const { sections: _dropped, ...rest } = legacy;
@@ -309,15 +327,58 @@ function needsMigration(input2) {
309
327
  }
310
328
 
311
329
  // src/render/css.ts
312
- var selectorFor = (id) => `[data-cms="${id}"]`;
330
+ function cssString(value) {
331
+ let out = "";
332
+ for (const ch of value) {
333
+ const code = ch.codePointAt(0);
334
+ if (ch === "\\" || ch === '"') out += `\\${ch}`;
335
+ else if (code < 32 || code === 127 || ch === "<") out += `\\${code.toString(16)} `;
336
+ else out += ch;
337
+ }
338
+ return out;
339
+ }
340
+ var selectorFor = (id) => `[data-cms="${cssString(id)}"]`;
341
+ var BASE_CSS = [
342
+ "/* fancy-cms base */",
343
+ ":where([data-cms]) {\n box-sizing: border-box;\n}",
344
+ ":where(h1, h2, h3, h4, h5, h6, p, ul, ol)[data-cms] {\n margin: 0;\n}",
345
+ ":where(ul, ol)[data-cms] {\n padding-left: 1.25em;\n}",
346
+ ":where(a[data-cms]) {\n color: inherit;\n}",
347
+ ":where([data-cms] > img) {\n display: block;\n height: 100%;\n object-fit: cover;\n width: 100%;\n}",
348
+ ':where([data-cms] > img[data-cms-fit="contain"]) {\n object-fit: contain;\n}',
349
+ ':where([data-cms] > img[data-cms-fit="fill"]) {\n object-fit: fill;\n}',
350
+ ':where([data-cms] > img[data-cms-fit="none"]) {\n object-fit: none;\n}',
351
+ ':where([data-cms] > img[data-cms-fit="scale-down"]) {\n object-fit: scale-down;\n}',
352
+ ":where([data-cms-button]) {\n align-items: center;\n border: 1px solid var(--cms-color-primary, #7c3aed);\n border-radius: 8px;\n cursor: pointer;\n display: inline-flex;\n font-weight: 600;\n gap: 6px;\n justify-content: center;\n line-height: 1.2;\n padding: 10px 18px;\n text-decoration: none;\n}",
353
+ ':where([data-cms-button="primary"]) {\n background: var(--cms-color-primary, #7c3aed);\n color: #ffffff;\n}',
354
+ ':where([data-cms-button="outline"]) {\n background: transparent;\n color: var(--cms-color-primary, #7c3aed);\n}',
355
+ ':where([data-cms-button="ghost"]) {\n background: transparent;\n border-color: currentColor;\n color: inherit;\n}',
356
+ ":where([data-cms-callout]) {\n background: #3b82f614;\n border-left: 3px solid #3b82f6;\n border-radius: 10px;\n padding: 12px 14px;\n}",
357
+ ':where([data-cms-callout="success"]) {\n background: #10b98114;\n border-left-color: #10b981;\n}',
358
+ ':where([data-cms-callout="warning"]) {\n background: #f59e0b14;\n border-left-color: #f59e0b;\n}',
359
+ ':where([data-cms-callout="danger"]) {\n background: #ef444414;\n border-left-color: #ef4444;\n}',
360
+ ":where([data-cms] > hr) {\n border: none;\n border-top: 1px solid #e2e8f0;\n margin: 0;\n}",
361
+ ":where([data-cms] > pre) {\n background: #0b1220;\n border-radius: 10px;\n color: #e2e8f0;\n font-family: ui-monospace, monospace;\n font-size: 13px;\n margin: 0;\n overflow-x: auto;\n padding: 12px 14px;\n}"
362
+ ].join("\n\n");
363
+ var UNITS = /* @__PURE__ */ new Set(["px", "%", "vw", "vh", "rem", "fr"]);
313
364
  function lengthToCss(len) {
314
- return `${len.value}${len.unit}`;
365
+ if (typeof len !== "object" || len === null) return null;
366
+ const { value, unit } = len;
367
+ if (typeof value !== "number" || !Number.isFinite(value) || typeof unit !== "string" || !UNITS.has(unit)) return null;
368
+ return `${value}${unit}`;
315
369
  }
316
370
  function sizeToCss(size) {
317
371
  if (size === "fill") return "100%";
318
372
  if (size === "hug") return "auto";
319
373
  return lengthToCss(size);
320
374
  }
375
+ function numberToCss(value) {
376
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
377
+ }
378
+ function putLen(d, props, len) {
379
+ const css = lengthToCss(len);
380
+ if (css !== null) for (const p of props) d[p] = css;
381
+ }
321
382
  var ALIGN = {
322
383
  start: "flex-start",
323
384
  center: "center",
@@ -331,51 +392,150 @@ var JUSTIFY = {
331
392
  between: "space-between",
332
393
  around: "space-around"
333
394
  };
395
+ function safeCssValue(value) {
396
+ if (/[{}<]/.test(value)) return null;
397
+ let depth = 0;
398
+ let quote = null;
399
+ for (const ch of value) {
400
+ if (quote) {
401
+ if (ch === quote) quote = null;
402
+ } else if (ch === '"' || ch === "'") {
403
+ quote = ch;
404
+ } else if (ch === "(") {
405
+ depth++;
406
+ } else if (ch === ")") {
407
+ depth = Math.max(0, depth - 1);
408
+ } else if (ch === ";" && depth === 0) {
409
+ return null;
410
+ }
411
+ }
412
+ return value;
413
+ }
414
+ function put(d, prop, value) {
415
+ if (value === void 0) return;
416
+ const safe = safeCssValue(value);
417
+ if (safe !== null) d[prop] = safe;
418
+ }
419
+ var OVERFLOW = /* @__PURE__ */ new Set(["visible", "hidden", "auto"]);
420
+ var TEXT_TRANSFORM = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
421
+ var BACKGROUND_SIZE = /* @__PURE__ */ new Set(["cover", "contain", "auto"]);
422
+ var TEXT_ALIGN = /* @__PURE__ */ new Set(["left", "center", "right", "justify"]);
423
+ var DIRECTIONS = /* @__PURE__ */ new Set(["row", "column"]);
424
+ function keyword(map, value) {
425
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(map, value) ? map[value] : null;
426
+ }
427
+ function inSet(set, value) {
428
+ return typeof value === "string" && set.has(value) ? value : null;
429
+ }
334
430
  function styleDecls(style) {
335
431
  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;
432
+ putLen(d, ["padding"], style.padding);
433
+ putLen(d, ["padding-left", "padding-right"], style.paddingX);
434
+ putLen(d, ["padding-top", "padding-bottom"], style.paddingY);
435
+ putLen(d, ["margin"], style.margin);
436
+ if (style.marginX === "auto") d["margin-left"] = d["margin-right"] = "auto";
437
+ else putLen(d, ["margin-left", "margin-right"], style.marginX);
438
+ putLen(d, ["margin-top", "margin-bottom"], style.marginY);
439
+ putLen(d, ["border-radius"], style.radius);
440
+ putLen(d, ["max-width"], style.maxWidth);
441
+ putLen(d, ["min-height"], style.minHeight);
442
+ put(d, "aspect-ratio", typeof style.aspectRatio === "string" ? style.aspectRatio : void 0);
443
+ if (style.sticky === true) {
444
+ d["position"] = "sticky";
445
+ d["top"] = "0";
446
+ }
447
+ if (typeof style.zIndex === "number" && Number.isFinite(style.zIndex)) d["z-index"] = String(Math.trunc(style.zIndex));
448
+ const overflow = inSet(OVERFLOW, style.overflow);
449
+ if (overflow) d["overflow"] = overflow;
450
+ put(d, "background", typeof style.background === "string" ? style.background : void 0);
451
+ const backgroundSize = inSet(BACKGROUND_SIZE, style.backgroundSize);
452
+ if (backgroundSize) d["background-size"] = backgroundSize;
453
+ put(d, "background-position", typeof style.backgroundPosition === "string" ? style.backgroundPosition : void 0);
454
+ put(d, "color", typeof style.color === "string" ? style.color : void 0);
455
+ const opacity = numberToCss(style.opacity);
456
+ if (opacity !== null) d["opacity"] = opacity;
457
+ put(d, "font-family", typeof style.fontFamily === "string" ? style.fontFamily : void 0);
458
+ putLen(d, ["font-size"], style.fontSize);
459
+ const fontWeight = numberToCss(style.fontWeight);
460
+ if (fontWeight !== null) d["font-weight"] = fontWeight;
461
+ const lineHeight = numberToCss(style.lineHeight);
462
+ if (lineHeight !== null) d["line-height"] = lineHeight;
463
+ const textAlign = inSet(TEXT_ALIGN, style.textAlign);
464
+ if (textAlign) d["text-align"] = textAlign;
465
+ putLen(d, ["letter-spacing"], style.letterSpacing);
466
+ const textTransform = inSet(TEXT_TRANSFORM, style.textTransform);
467
+ if (textTransform) d["text-transform"] = textTransform;
468
+ put(d, "border", typeof style.border === "string" ? style.border : void 0);
469
+ put(d, "box-shadow", typeof style.boxShadow === "string" ? style.boxShadow : void 0);
470
+ put(d, "transform", typeof style.transform === "string" ? style.transform : void 0);
471
+ put(d, "filter", typeof style.filter === "string" ? style.filter : void 0);
472
+ putLen(d, ["gap"], style.gap);
473
+ const align = keyword(ALIGN, style.align);
474
+ if (align) d["align-items"] = align;
475
+ const justify = keyword(JUSTIFY, style.justify);
476
+ if (justify) d["justify-content"] = justify;
355
477
  return d;
356
478
  }
357
- function layoutDecls(node) {
479
+ function shownDisplay(node) {
480
+ if (node.layout === "stack") return "flex";
481
+ if (node.layout === "grid") return "grid";
482
+ return "block";
483
+ }
484
+ function gridColumns(style) {
485
+ const min = lengthToCss(style.columnMin);
486
+ if (min !== null) return `repeat(auto-fit, minmax(min(${min}, 100%), 1fr))`;
487
+ if (typeof style.columns === "number" && Number.isFinite(style.columns)) return `repeat(${Math.max(1, Math.trunc(style.columns))}, 1fr)`;
488
+ return void 0;
489
+ }
490
+ function containerDecls(node, style, isBase) {
358
491
  const d = {};
359
492
  if (node.layout === "stack") {
360
- d["display"] = "flex";
361
- d["flex-direction"] = node.style.base.direction ?? "column";
493
+ if (isBase) d["display"] = "flex";
494
+ const direction = inSet(DIRECTIONS, style.direction);
495
+ if (isBase || direction) d["flex-direction"] = direction ?? "column";
496
+ if (style.wrap !== void 0) d["flex-wrap"] = style.wrap ? "wrap" : "nowrap";
362
497
  } 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") {
498
+ if (isBase) d["display"] = "grid";
499
+ const cols = gridColumns(style);
500
+ if (isBase || cols !== void 0) d["grid-template-columns"] = cols ?? "repeat(1, 1fr)";
501
+ } else if (node.layout === "free" && isBase) {
366
502
  d["position"] = "relative";
367
503
  }
368
504
  return d;
369
505
  }
506
+ function visibilityDecls(node, style, isBase) {
507
+ if (style.hidden === true) return { display: "none" };
508
+ if (style.hidden === false && !isBase) return { display: shownDisplay(node) };
509
+ return {};
510
+ }
511
+ function themeDecls(theme) {
512
+ const d = {};
513
+ const walk = (value, path) => {
514
+ if (typeof value === "string" || typeof value === "number" && Number.isFinite(value)) {
515
+ const key = path.map((p) => p.toLowerCase().replace(/[^a-z0-9-]/g, "")).filter(Boolean).join("-");
516
+ if (key) put(d, `--cms-${key}`, String(value));
517
+ return;
518
+ }
519
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
520
+ for (const [k, v] of Object.entries(value)) walk(v, [...path, k]);
521
+ }
522
+ };
523
+ if (typeof theme === "object" && theme !== null && !Array.isArray(theme)) {
524
+ for (const [k, v] of Object.entries(theme)) {
525
+ if (k !== "name") walk(v, [k]);
526
+ }
527
+ }
528
+ return d;
529
+ }
370
530
  function constraintDecls(c, parentFree) {
371
531
  const d = {};
372
532
  const transforms = [];
373
533
  if (parentFree) {
374
534
  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);
535
+ putLen(d, ["left"], c.left);
536
+ putLen(d, ["right"], c.right);
537
+ putLen(d, ["top"], c.top);
538
+ putLen(d, ["bottom"], c.bottom);
379
539
  if (c.centerX) {
380
540
  d["left"] = "50%";
381
541
  transforms.push("translateX(-50%)");
@@ -386,13 +546,15 @@ function constraintDecls(c, parentFree) {
386
546
  }
387
547
  if (transforms.length) d["transform"] = transforms.join(" ");
388
548
  }
389
- if (c.width) d["width"] = sizeToCss(c.width);
390
- if (c.height) d["height"] = sizeToCss(c.height);
549
+ const width = sizeToCss(c.width);
550
+ if (width !== null) d["width"] = width;
551
+ const height = sizeToCss(c.height);
552
+ if (height !== null) d["height"] = height;
391
553
  return d;
392
554
  }
393
555
  function isParentFree(doc, node) {
394
556
  if (node.parent === null) return false;
395
- const parent = doc.nodes[node.parent];
557
+ const parent = own(doc.nodes, node.parent);
396
558
  return !parent || parent.layout === void 0 || parent.layout === "free";
397
559
  }
398
560
  function serializeRule(selector, decls, indent) {
@@ -407,24 +569,31 @@ function emitDocCss(doc) {
407
569
  if (!hasNodeMap(doc)) return "";
408
570
  const breakpointPx = DEFAULT_BREAKPOINTS;
409
571
  const blocks = [];
572
+ const themeRule = serializeRule(":root", themeDecls(doc.theme), "");
573
+ if (themeRule) blocks.push(themeRule);
410
574
  for (const id of Object.keys(doc.nodes).sort()) {
411
- const node = doc.nodes[id];
575
+ const node = withNodeDefaults(doc.nodes[id]);
412
576
  const selector = selectorFor(id);
413
577
  const parentFree = isParentFree(doc, node);
414
578
  const base = {
415
- ...layoutDecls(node),
579
+ ...containerDecls(node, node.style.base, true),
416
580
  ...node.constraints ? constraintDecls(node.constraints.base, parentFree) : {},
417
- ...styleDecls(node.style.base)
581
+ ...styleDecls(node.style.base),
582
+ ...visibilityDecls(node, node.style.base, true)
418
583
  };
419
584
  const baseRule = serializeRule(selector, base, "");
420
585
  if (baseRule) blocks.push(baseRule);
421
586
  for (const bp of doc.breakpoints) {
422
587
  if (bp === "base") continue;
423
- const px = breakpointPx[bp];
424
- if (px === void 0) continue;
588
+ const px = own(breakpointPx, bp);
589
+ if (typeof px !== "number") continue;
590
+ const bpStyle = own(node.style, bp);
591
+ const bpConstraints = node.constraints ? own(node.constraints, bp) : void 0;
425
592
  const decls = {
426
- ...node.constraints?.[bp] ? constraintDecls(node.constraints[bp], parentFree) : {},
427
- ...node.style[bp] ? styleDecls(node.style[bp]) : {}
593
+ ...bpStyle ? containerDecls(node, bpStyle, false) : {},
594
+ ...bpConstraints ? constraintDecls(bpConstraints, parentFree) : {},
595
+ ...bpStyle ? styleDecls(bpStyle) : {},
596
+ ...bpStyle ? visibilityDecls(node, bpStyle, false) : {}
428
597
  };
429
598
  const inner = serializeRule(selector, decls, " ");
430
599
  if (inner) blocks.push(`@media (min-width: ${px}px) {
@@ -432,7 +601,8 @@ ${inner}
432
601
  }`);
433
602
  }
434
603
  }
435
- return blocks.join("\n\n") + (blocks.length ? "\n" : "");
604
+ if (blocks.length === 0 && Object.keys(doc.nodes).length === 0) return "";
605
+ return [BASE_CSS, ...blocks].join("\n\n") + "\n";
436
606
  }
437
607
  function getPath(data, path) {
438
608
  return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], data);
@@ -442,6 +612,55 @@ function resolveValue(v, data) {
442
612
  if (isBinding(v)) return getPath(data, v.$bind);
443
613
  return v;
444
614
  }
615
+ var LANDMARK_TAGS = ["section", "header", "nav", "main", "footer", "article", "aside", "div"];
616
+ var LANDMARKS = new Set(LANDMARK_TAGS);
617
+ var CONTAINERS = /* @__PURE__ */ new Set(["stack", "grid", "frame", "shape", "card"]);
618
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
619
+ function safeHref(raw) {
620
+ const href = raw.trim();
621
+ if (!href) return null;
622
+ const probe = href.replace(/[\u0000- \u007f]+/g, "");
623
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(probe);
624
+ if (scheme && !SAFE_SCHEMES.has(scheme[1].toLowerCase())) return null;
625
+ return href;
626
+ }
627
+ var ANCHOR_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
628
+ function anchorFor(node, text) {
629
+ const anchor = text(node.props.anchor);
630
+ return ANCHOR_PATTERN.test(anchor) ? anchor : null;
631
+ }
632
+ function elementFor(node, text) {
633
+ const as = text(node.props.as);
634
+ switch (node.type) {
635
+ case "heading": {
636
+ const level = Number(text(node.props.level) || 2);
637
+ return { tag: Number.isInteger(level) && level >= 1 && level <= 6 ? `h${level}` : "h2" };
638
+ }
639
+ case "text":
640
+ return { tag: "p" };
641
+ case "section":
642
+ return { tag: LANDMARKS.has(as) ? as : "section" };
643
+ case "list":
644
+ return { tag: text(node.props.ordered) === "true" ? "ol" : "ul" };
645
+ case "button":
646
+ case "link": {
647
+ const href = safeHref(text(node.props.href));
648
+ const attrs = {};
649
+ if (href !== null) {
650
+ attrs.href = href;
651
+ if (/^https?:\/\//i.test(href)) attrs.rel = "noopener noreferrer";
652
+ }
653
+ if (node.type === "button") attrs["data-cms-button"] = BUTTON_VARIANTS.has(text(node.props.variant)) ? text(node.props.variant) : "primary";
654
+ return { tag: href !== null ? "a" : node.type === "link" ? "span" : "div", attrs };
655
+ }
656
+ case "callout": {
657
+ const variant = text(node.props.variant);
658
+ return { tag: "div", attrs: { "data-cms-callout": CALLOUT_VARIANTS.has(variant) ? variant : "info" } };
659
+ }
660
+ default:
661
+ return { tag: CONTAINERS.has(node.type) && LANDMARKS.has(as) ? as : "div" };
662
+ }
663
+ }
445
664
  var defaultRegistry = {
446
665
  section: ({ children }) => children,
447
666
  stack: ({ children }) => children,
@@ -454,47 +673,38 @@ var defaultRegistry = {
454
673
  // Rich text — inline-formatted HTML (gradient spans, links, <code>, …) that
455
674
  // plain `text` can't express. The author owns the markup (it's their content).
456
675
  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" }),
458
- divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
459
- callout: ({ node, text }) => {
460
- const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
461
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 10, padding: "12px 14px", borderRadius: 10, background: `${tone}14`, borderLeft: `3px solid ${tone}` }, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 13 }, children: text(node.props.content) || "Callout" }) });
462
- },
463
- code: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("pre", { style: { margin: 0, padding: "12px 14px", borderRadius: 10, background: "#0b1220", color: "#e2e8f0", fontFamily: "ui-monospace, monospace", fontSize: 13, overflowX: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: text(node.props.content) }) }),
464
- button: ({ node, text }) => {
465
- const label2 = text(node.props.label) || text(node.props.content) || "Button";
466
- const variant = text(node.props.variant) || "primary";
467
- return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label2 });
676
+ // The [data-cms] element is already the <a> (see elementFor), so a link renders its label only.
677
+ link: ({ node, text }) => text(node.props.content) || "link",
678
+ list: ({ node, resolve }) => {
679
+ const items = resolve(node.props.items);
680
+ return Array.isArray(items) ? items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: item === null || item === void 0 ? "" : String(item) }, i)) : null;
468
681
  },
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
- )
682
+ // Looks come from the base stylesheet (render/css.ts BASE_CSS), never from
683
+ // inline style objects, so the PHP renderer emits the same markup.
684
+ divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", {}),
685
+ callout: ({ node, text }) => text(node.props.content) || "Callout",
686
+ code: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("pre", { children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: text(node.props.content) }) }),
687
+ button: ({ node, text }) => text(node.props.label) || text(node.props.content) || "Button",
688
+ image: ({ node, text }) => {
689
+ const fit = text(node.props.fit);
690
+ return /* @__PURE__ */ jsxRuntime.jsx(
691
+ "img",
692
+ {
693
+ src: text(node.props.src) || void 0,
694
+ alt: text(node.props.alt),
695
+ loading: text(node.props.loading) === "eager" ? "eager" : "lazy",
696
+ "data-cms-fit": IMAGE_FITS.has(fit) && fit !== "cover" ? fit : void 0
697
+ }
698
+ );
699
+ }
477
700
  };
478
- function buttonStyle(variant) {
479
- const base = {
480
- display: "inline-flex",
481
- alignItems: "center",
482
- gap: 6,
483
- padding: "9px 16px",
484
- borderRadius: 8,
485
- fontWeight: 600,
486
- fontSize: 14,
487
- lineHeight: 1,
488
- cursor: "pointer",
489
- userSelect: "none"
490
- };
491
- if (variant === "ghost") return { ...base, background: "transparent", color: "inherit", border: "1px solid currentColor" };
492
- if (variant === "outline") return { ...base, background: "transparent", color: "#7c3aed", border: "1px solid #7c3aed" };
493
- return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
494
- }
701
+ var IMAGE_FITS = /* @__PURE__ */ new Set(["cover", "contain", "fill", "none", "scale-down"]);
702
+ var BUTTON_VARIANTS = /* @__PURE__ */ new Set(["primary", "outline", "ghost"]);
703
+ var CALLOUT_VARIANTS = /* @__PURE__ */ new Set(["info", "success", "warning", "danger"]);
495
704
  function RenderNode({ doc, id, registry = defaultRegistry, data }) {
496
- const node = doc.nodes[id];
497
- if (!node) return null;
705
+ const stored = own(doc.nodes, id);
706
+ if (!stored) return null;
707
+ const node = withNodeDefaults(stored);
498
708
  if (node.type === "repeater") {
499
709
  const arr = resolveValue(node.props.items, data);
500
710
  const items = Array.isArray(arr) ? arr : [];
@@ -509,7 +719,19 @@ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
509
719
  };
510
720
  const renderer = registry[node.type];
511
721
  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 });
722
+ const { tag, attrs } = elementFor(node, text);
723
+ const anchor = anchorFor(node, text);
724
+ return react.createElement(
725
+ tag,
726
+ {
727
+ "data-cms": node.id,
728
+ "data-cms-island": node.island ? node.type : void 0,
729
+ className: node.className,
730
+ ...anchor ? { id: anchor } : {},
731
+ ...attrs
732
+ },
733
+ inner
734
+ );
513
735
  }
514
736
  function CmsPage({
515
737
  doc: input2,
@@ -911,7 +1133,8 @@ var ADD_DEFAULTS = {
911
1133
  divider: { type: "divider", props: {}, style: {} },
912
1134
  code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
913
1135
  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" }
1136
+ repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" },
1137
+ list: { type: "list", props: { items: ["First point", "Second point"], ordered: false }, style: {} }
915
1138
  };
916
1139
  var ADD_MENU = [
917
1140
  { kind: "heading", label: "Heading" },
@@ -927,6 +1150,7 @@ var ADD_MENU = [
927
1150
  { kind: "divider", label: "Divider" },
928
1151
  { kind: "code", label: "Code" },
929
1152
  { kind: "richtext", label: "Rich text" },
1153
+ { kind: "list", label: "List" },
930
1154
  { kind: "repeater", label: "Repeater" }
931
1155
  ];
932
1156
  function resolveInsertParent(doc, selectedId) {
@@ -1149,5 +1373,6 @@ exports.resolveInsertParent = resolveInsertParent;
1149
1373
  exports.resolveValue = resolveValue;
1150
1374
  exports.rootIds = rootIds;
1151
1375
  exports.useEditor = useEditor;
1376
+ exports.withNodeDefaults = withNodeDefaults;
1152
1377
  //# sourceMappingURL=index.cjs.map
1153
1378
  //# sourceMappingURL=index.cjs.map