@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.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { B as Binding, P as PageDoc, m as NodeId, N as Node, J as Json, q as StyleProps, L as LayoutMode } from './registry-CUNaBqTs.cjs';
2
- export { A as Action, a as ActionEvent, b as ActionHandler, c as Anim, d as AnimDriver, e as Bound, f as Breakpoint, C as Constraints, D as DEFAULT_BREAKPOINTS, g as DataContext, E as ElementContext, h as ElementRegistry, i as ElementRenderer, K as Keyframe, j as LegacyPageDoc, k as Length, l as LengthUnit, M as MigrateResult, n as PageMeta, o as PerBreakpoint, S as ScrollMode, p as SizeMode, T as ThemeTokens, r as defaultRegistry, s as emptyDoc, t as getPath, u as hasNodeMap, v as isBinding, w as migrateDoc, x as migrateDocVerbose, y as needsMigration, z as resolveValue } from './registry-CUNaBqTs.cjs';
3
- import { P as PageOp } from './useEditor-wdlYQMHg.cjs';
4
- export { A as Actor, 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, O as OpEnvelope, h as PageOpType, i as editorReduce, j as initEditor, u as useEditor } from './useEditor-wdlYQMHg.cjs';
1
+ import { B as Binding, P as PageDoc, m as NodeId, N as Node, J as Json, q as StyleProps, L as LayoutMode } from './registry-F2TAMjhx.cjs';
2
+ export { A as Action, a as ActionEvent, b as ActionHandler, c as Anim, d as AnimDriver, e as Bound, f as Breakpoint, C as Constraints, D as DEFAULT_BREAKPOINTS, g as DataContext, E as ElementContext, h as ElementRegistry, i as ElementRenderer, K as Keyframe, j as LegacyPageDoc, k as Length, l as LengthUnit, M as MigrateResult, n as PageMeta, o as PerBreakpoint, S as ScrollMode, p as SizeMode, T as ThemeTokens, r as defaultRegistry, s as emptyDoc, t as getPath, u as hasNodeMap, v as isBinding, w as migrateDoc, x as migrateDocVerbose, y as needsMigration, z as resolveValue, F as withNodeDefaults } from './registry-F2TAMjhx.cjs';
3
+ import { P as PageOp } from './useEditor-B2kVZJA0.cjs';
4
+ export { A as Actor, 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, O as OpEnvelope, h as PageOpType, i as editorReduce, j as initEditor, u as useEditor } from './useEditor-B2kVZJA0.cjs';
5
5
  export { fractionalKey, fractionalKey as keyBetween } from '@particle-academy/fancy-doc-commons';
6
6
  export { CmsPage, CmsPageProps, CmsRegion, CmsRegionProps, RenderNode, RenderNodeProps } from './react.cjs';
7
7
  import 'react';
@@ -120,7 +120,7 @@ declare function emitDocCss(doc: PageDoc): string;
120
120
  */
121
121
  /** Node types that accept children, so an insert lands *inside* them. */
122
122
  declare const CONTAINER_TYPES: Set<string>;
123
- type AddKind = "text" | "heading" | "button" | "link" | "image" | "box" | "stack" | "grid" | "card" | "callout" | "divider" | "code" | "richtext" | "repeater";
123
+ type AddKind = "text" | "heading" | "button" | "link" | "image" | "box" | "stack" | "grid" | "card" | "callout" | "divider" | "code" | "richtext" | "repeater" | "list";
124
124
  declare const ADD_DEFAULTS: Record<AddKind, {
125
125
  type: string;
126
126
  props: Record<string, Json>;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { B as Binding, P as PageDoc, m as NodeId, N as Node, J as Json, q as StyleProps, L as LayoutMode } from './registry-CUNaBqTs.js';
2
- export { A as Action, a as ActionEvent, b as ActionHandler, c as Anim, d as AnimDriver, e as Bound, f as Breakpoint, C as Constraints, D as DEFAULT_BREAKPOINTS, g as DataContext, E as ElementContext, h as ElementRegistry, i as ElementRenderer, K as Keyframe, j as LegacyPageDoc, k as Length, l as LengthUnit, M as MigrateResult, n as PageMeta, o as PerBreakpoint, S as ScrollMode, p as SizeMode, T as ThemeTokens, r as defaultRegistry, s as emptyDoc, t as getPath, u as hasNodeMap, v as isBinding, w as migrateDoc, x as migrateDocVerbose, y as needsMigration, z as resolveValue } from './registry-CUNaBqTs.js';
3
- import { P as PageOp } from './useEditor-aPH5falI.js';
4
- export { A as Actor, 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, O as OpEnvelope, h as PageOpType, i as editorReduce, j as initEditor, u as useEditor } from './useEditor-aPH5falI.js';
1
+ import { B as Binding, P as PageDoc, m as NodeId, N as Node, J as Json, q as StyleProps, L as LayoutMode } from './registry-F2TAMjhx.js';
2
+ export { A as Action, a as ActionEvent, b as ActionHandler, c as Anim, d as AnimDriver, e as Bound, f as Breakpoint, C as Constraints, D as DEFAULT_BREAKPOINTS, g as DataContext, E as ElementContext, h as ElementRegistry, i as ElementRenderer, K as Keyframe, j as LegacyPageDoc, k as Length, l as LengthUnit, M as MigrateResult, n as PageMeta, o as PerBreakpoint, S as ScrollMode, p as SizeMode, T as ThemeTokens, r as defaultRegistry, s as emptyDoc, t as getPath, u as hasNodeMap, v as isBinding, w as migrateDoc, x as migrateDocVerbose, y as needsMigration, z as resolveValue, F as withNodeDefaults } from './registry-F2TAMjhx.js';
3
+ import { P as PageOp } from './useEditor-BQSictH3.js';
4
+ export { A as Actor, 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, O as OpEnvelope, h as PageOpType, i as editorReduce, j as initEditor, u as useEditor } from './useEditor-BQSictH3.js';
5
5
  export { fractionalKey, fractionalKey as keyBetween } from '@particle-academy/fancy-doc-commons';
6
6
  export { CmsPage, CmsPageProps, CmsRegion, CmsRegionProps, RenderNode, RenderNodeProps } from './react.js';
7
7
  import 'react';
@@ -120,7 +120,7 @@ declare function emitDocCss(doc: PageDoc): string;
120
120
  */
121
121
  /** Node types that accept children, so an insert lands *inside* them. */
122
122
  declare const CONTAINER_TYPES: Set<string>;
123
- type AddKind = "text" | "heading" | "button" | "link" | "image" | "box" | "stack" | "grid" | "card" | "callout" | "divider" | "code" | "richtext" | "repeater";
123
+ type AddKind = "text" | "heading" | "button" | "link" | "image" | "box" | "stack" | "grid" | "card" | "callout" | "divider" | "code" | "richtext" | "repeater" | "list";
124
124
  declare const ADD_DEFAULTS: Record<AddKind, {
125
125
  type: string;
126
126
  props: Record<string, Json>;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- export { CmsRegion } from './chunk-R7HDS4VG.js';
2
- export { ADD_DEFAULTS, ADD_MENU, CONTAINER_TYPES, Canvas, Editor, Inspector, LayersPanel, buildInsertOp, editorReduce, initEditor, resolveInsertParent, useEditor } from './chunk-SYRU2X45.js';
3
- import { isBinding } from './chunk-LY6ID2J2.js';
4
- export { CmsPage, DEFAULT_BREAKPOINTS, RenderNode, childrenOf, defaultRegistry, descendantIds, emitDocCss, emptyDoc, getPath, hasNodeMap, invert, isBinding, lastRootId, migrateDoc, migrateDocVerbose, needsMigration, reduce, reduceAll, resolveValue, rootIds } from './chunk-LY6ID2J2.js';
1
+ export { CmsRegion } from './chunk-TJL26EAX.js';
2
+ export { ADD_DEFAULTS, ADD_MENU, CONTAINER_TYPES, Canvas, Editor, Inspector, LayersPanel, buildInsertOp, editorReduce, initEditor, resolveInsertParent, useEditor } from './chunk-BVMSHUQF.js';
3
+ import { isBinding } from './chunk-IRG5IKKO.js';
4
+ export { CmsPage, DEFAULT_BREAKPOINTS, RenderNode, childrenOf, defaultRegistry, descendantIds, emitDocCss, emptyDoc, getPath, hasNodeMap, invert, isBinding, lastRootId, migrateDoc, migrateDocVerbose, needsMigration, reduce, reduceAll, resolveValue, rootIds, withNodeDefaults } from './chunk-IRG5IKKO.js';
5
5
  export { fractionalKey, fractionalKey as keyBetween } from '@particle-academy/fancy-doc-commons';
6
6
 
7
7
  // src/document/bindings.ts
package/dist/react.cjs CHANGED
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ var react = require('react');
3
4
  var fancyDocCommons = require('@particle-academy/fancy-doc-commons');
4
5
  var jsxRuntime = require('react/jsx-runtime');
5
- var react = require('react');
6
6
 
7
- // src/document/reduce.ts
7
+ // src/react/RenderNode.tsx
8
8
 
9
9
  // src/document/types.ts
10
10
  function isBinding(v) {
@@ -15,6 +15,12 @@ function hasNodeMap(value) {
15
15
  const nodes = value.nodes;
16
16
  return typeof nodes === "object" && nodes !== null && !Array.isArray(nodes);
17
17
  }
18
+ function withNodeDefaults(node) {
19
+ const props = typeof node.props === "object" && node.props !== null ? node.props : {};
20
+ const style = typeof node.style === "object" && node.style !== null ? node.style : { base: {} };
21
+ const base = typeof style.base === "object" && style.base !== null ? style.base : {};
22
+ return props === node.props && style === node.style && base === style.base ? node : { ...node, props, style: { ...style, base } };
23
+ }
18
24
  var DEFAULT_BREAKPOINTS = {
19
25
  base: 0,
20
26
  md: 768,
@@ -31,6 +37,15 @@ function emptyDoc(id, slug = "/") {
31
37
  };
32
38
  }
33
39
 
40
+ // src/document/own.ts
41
+ var has = Object.prototype.hasOwnProperty;
42
+ function own(map, key) {
43
+ return map != null && has.call(map, key) ? map[key] : void 0;
44
+ }
45
+ function setOwn(map, key, value) {
46
+ Object.defineProperty(map, key, { value, writable: true, enumerable: true, configurable: true });
47
+ }
48
+
34
49
  // src/document/reduce.ts
35
50
  function childrenOf(doc, parent) {
36
51
  return fancyDocCommons.childrenOf(doc, parent);
@@ -47,6 +62,55 @@ function resolveValue(v, data) {
47
62
  if (isBinding(v)) return getPath(data, v.$bind);
48
63
  return v;
49
64
  }
65
+ var LANDMARK_TAGS = ["section", "header", "nav", "main", "footer", "article", "aside", "div"];
66
+ var LANDMARKS = new Set(LANDMARK_TAGS);
67
+ var CONTAINERS = /* @__PURE__ */ new Set(["stack", "grid", "frame", "shape", "card"]);
68
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
69
+ function safeHref(raw) {
70
+ const href = raw.trim();
71
+ if (!href) return null;
72
+ const probe = href.replace(/[\u0000- \u007f]+/g, "");
73
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(probe);
74
+ if (scheme && !SAFE_SCHEMES.has(scheme[1].toLowerCase())) return null;
75
+ return href;
76
+ }
77
+ var ANCHOR_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
78
+ function anchorFor(node, text) {
79
+ const anchor = text(node.props.anchor);
80
+ return ANCHOR_PATTERN.test(anchor) ? anchor : null;
81
+ }
82
+ function elementFor(node, text) {
83
+ const as = text(node.props.as);
84
+ switch (node.type) {
85
+ case "heading": {
86
+ const level = Number(text(node.props.level) || 2);
87
+ return { tag: Number.isInteger(level) && level >= 1 && level <= 6 ? `h${level}` : "h2" };
88
+ }
89
+ case "text":
90
+ return { tag: "p" };
91
+ case "section":
92
+ return { tag: LANDMARKS.has(as) ? as : "section" };
93
+ case "list":
94
+ return { tag: text(node.props.ordered) === "true" ? "ol" : "ul" };
95
+ case "button":
96
+ case "link": {
97
+ const href = safeHref(text(node.props.href));
98
+ const attrs = {};
99
+ if (href !== null) {
100
+ attrs.href = href;
101
+ if (/^https?:\/\//i.test(href)) attrs.rel = "noopener noreferrer";
102
+ }
103
+ if (node.type === "button") attrs["data-cms-button"] = BUTTON_VARIANTS.has(text(node.props.variant)) ? text(node.props.variant) : "primary";
104
+ return { tag: href !== null ? "a" : node.type === "link" ? "span" : "div", attrs };
105
+ }
106
+ case "callout": {
107
+ const variant = text(node.props.variant);
108
+ return { tag: "div", attrs: { "data-cms-callout": CALLOUT_VARIANTS.has(variant) ? variant : "info" } };
109
+ }
110
+ default:
111
+ return { tag: CONTAINERS.has(node.type) && LANDMARKS.has(as) ? as : "div" };
112
+ }
113
+ }
50
114
  var defaultRegistry = {
51
115
  section: ({ children }) => children,
52
116
  stack: ({ children }) => children,
@@ -59,47 +123,38 @@ var defaultRegistry = {
59
123
  // Rich text — inline-formatted HTML (gradient spans, links, <code>, …) that
60
124
  // plain `text` can't express. The author owns the markup (it's their content).
61
125
  richtext: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: text(node.props.html) } }),
62
- link: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: text(node.props.href) || "#", style: { color: "inherit", textDecoration: "underline" }, children: text(node.props.content) || "link" }),
63
- divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: "1px solid #e2e8f0", margin: 0 } }),
64
- callout: ({ node, text }) => {
65
- const tone = { info: "#3b82f6", success: "#10b981", warning: "#f59e0b", danger: "#ef4444" }[text(node.props.variant)] ?? "#3b82f6";
66
- 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" }) });
126
+ // The [data-cms] element is already the <a> (see elementFor), so a link renders its label only.
127
+ link: ({ node, text }) => text(node.props.content) || "link",
128
+ list: ({ node, resolve }) => {
129
+ const items = resolve(node.props.items);
130
+ return Array.isArray(items) ? items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: item === null || item === void 0 ? "" : String(item) }, i)) : null;
67
131
  },
68
- 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) }) }),
69
- button: ({ node, text }) => {
70
- const label = text(node.props.label) || text(node.props.content) || "Button";
71
- const variant = text(node.props.variant) || "primary";
72
- return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label });
73
- },
74
- image: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx(
75
- "img",
76
- {
77
- src: text(node.props.src),
78
- alt: text(node.props.alt),
79
- style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
80
- }
81
- )
132
+ // Looks come from the base stylesheet (render/css.ts BASE_CSS), never from
133
+ // inline style objects, so the PHP renderer emits the same markup.
134
+ divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", {}),
135
+ callout: ({ node, text }) => text(node.props.content) || "Callout",
136
+ code: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("pre", { children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: text(node.props.content) }) }),
137
+ button: ({ node, text }) => text(node.props.label) || text(node.props.content) || "Button",
138
+ image: ({ node, text }) => {
139
+ const fit = text(node.props.fit);
140
+ return /* @__PURE__ */ jsxRuntime.jsx(
141
+ "img",
142
+ {
143
+ src: text(node.props.src) || void 0,
144
+ alt: text(node.props.alt),
145
+ loading: text(node.props.loading) === "eager" ? "eager" : "lazy",
146
+ "data-cms-fit": IMAGE_FITS.has(fit) && fit !== "cover" ? fit : void 0
147
+ }
148
+ );
149
+ }
82
150
  };
83
- function buttonStyle(variant) {
84
- const base = {
85
- display: "inline-flex",
86
- alignItems: "center",
87
- gap: 6,
88
- padding: "9px 16px",
89
- borderRadius: 8,
90
- fontWeight: 600,
91
- fontSize: 14,
92
- lineHeight: 1,
93
- cursor: "pointer",
94
- userSelect: "none"
95
- };
96
- if (variant === "ghost") return { ...base, background: "transparent", color: "inherit", border: "1px solid currentColor" };
97
- if (variant === "outline") return { ...base, background: "transparent", color: "#7c3aed", border: "1px solid #7c3aed" };
98
- return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
99
- }
151
+ var IMAGE_FITS = /* @__PURE__ */ new Set(["cover", "contain", "fill", "none", "scale-down"]);
152
+ var BUTTON_VARIANTS = /* @__PURE__ */ new Set(["primary", "outline", "ghost"]);
153
+ var CALLOUT_VARIANTS = /* @__PURE__ */ new Set(["info", "success", "warning", "danger"]);
100
154
  function RenderNode({ doc, id, registry = defaultRegistry, data }) {
101
- const node = doc.nodes[id];
102
- if (!node) return null;
155
+ const stored = own(doc.nodes, id);
156
+ if (!stored) return null;
157
+ const node = withNodeDefaults(stored);
103
158
  if (node.type === "repeater") {
104
159
  const arr = resolveValue(node.props.items, data);
105
160
  const items = Array.isArray(arr) ? arr : [];
@@ -114,7 +169,19 @@ function RenderNode({ doc, id, registry = defaultRegistry, data }) {
114
169
  };
115
170
  const renderer = registry[node.type];
116
171
  const inner = renderer ? renderer({ node, doc, children: kids, data, resolve, text }) : node.island ? null : kids;
117
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cms": node.id, "data-cms-island": node.island ? node.type : void 0, className: node.className, children: inner });
172
+ const { tag, attrs } = elementFor(node, text);
173
+ const anchor = anchorFor(node, text);
174
+ return react.createElement(
175
+ tag,
176
+ {
177
+ "data-cms": node.id,
178
+ "data-cms-island": node.island ? node.type : void 0,
179
+ className: node.className,
180
+ ...anchor ? { id: anchor } : {},
181
+ ...attrs
182
+ },
183
+ inner
184
+ );
118
185
  }
119
186
  function normalizeDoc(input) {
120
187
  if (hasNodeMap(input)) return input;
@@ -132,7 +199,7 @@ function migrateDocVerbose(input) {
132
199
  const ordered = [];
133
200
  const seen = /* @__PURE__ */ new Set();
134
201
  for (const id of sections) {
135
- if (!normalized.nodes[id]) {
202
+ if (!own(normalized.nodes, id)) {
136
203
  danglingIds.push(id);
137
204
  continue;
138
205
  }
@@ -146,7 +213,7 @@ function migrateDocVerbose(input) {
146
213
  let prev = null;
147
214
  for (const id of finalOrder) {
148
215
  const key = fancyDocCommons.fractionalKey(prev, null);
149
- nodes[id] = { ...nodes[id], order: key };
216
+ setOwn(nodes, id, { ...own(nodes, id), order: key });
150
217
  prev = key;
151
218
  }
152
219
  const { sections: _dropped, ...rest } = legacy;
@@ -162,15 +229,58 @@ function migrateDoc(input) {
162
229
  }
163
230
 
164
231
  // src/render/css.ts
165
- var selectorFor = (id) => `[data-cms="${id}"]`;
232
+ function cssString(value) {
233
+ let out = "";
234
+ for (const ch of value) {
235
+ const code = ch.codePointAt(0);
236
+ if (ch === "\\" || ch === '"') out += `\\${ch}`;
237
+ else if (code < 32 || code === 127 || ch === "<") out += `\\${code.toString(16)} `;
238
+ else out += ch;
239
+ }
240
+ return out;
241
+ }
242
+ var selectorFor = (id) => `[data-cms="${cssString(id)}"]`;
243
+ var BASE_CSS = [
244
+ "/* fancy-cms base */",
245
+ ":where([data-cms]) {\n box-sizing: border-box;\n}",
246
+ ":where(h1, h2, h3, h4, h5, h6, p, ul, ol)[data-cms] {\n margin: 0;\n}",
247
+ ":where(ul, ol)[data-cms] {\n padding-left: 1.25em;\n}",
248
+ ":where(a[data-cms]) {\n color: inherit;\n}",
249
+ ":where([data-cms] > img) {\n display: block;\n height: 100%;\n object-fit: cover;\n width: 100%;\n}",
250
+ ':where([data-cms] > img[data-cms-fit="contain"]) {\n object-fit: contain;\n}',
251
+ ':where([data-cms] > img[data-cms-fit="fill"]) {\n object-fit: fill;\n}',
252
+ ':where([data-cms] > img[data-cms-fit="none"]) {\n object-fit: none;\n}',
253
+ ':where([data-cms] > img[data-cms-fit="scale-down"]) {\n object-fit: scale-down;\n}',
254
+ ":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}",
255
+ ':where([data-cms-button="primary"]) {\n background: var(--cms-color-primary, #7c3aed);\n color: #ffffff;\n}',
256
+ ':where([data-cms-button="outline"]) {\n background: transparent;\n color: var(--cms-color-primary, #7c3aed);\n}',
257
+ ':where([data-cms-button="ghost"]) {\n background: transparent;\n border-color: currentColor;\n color: inherit;\n}',
258
+ ":where([data-cms-callout]) {\n background: #3b82f614;\n border-left: 3px solid #3b82f6;\n border-radius: 10px;\n padding: 12px 14px;\n}",
259
+ ':where([data-cms-callout="success"]) {\n background: #10b98114;\n border-left-color: #10b981;\n}',
260
+ ':where([data-cms-callout="warning"]) {\n background: #f59e0b14;\n border-left-color: #f59e0b;\n}',
261
+ ':where([data-cms-callout="danger"]) {\n background: #ef444414;\n border-left-color: #ef4444;\n}',
262
+ ":where([data-cms] > hr) {\n border: none;\n border-top: 1px solid #e2e8f0;\n margin: 0;\n}",
263
+ ":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}"
264
+ ].join("\n\n");
265
+ var UNITS = /* @__PURE__ */ new Set(["px", "%", "vw", "vh", "rem", "fr"]);
166
266
  function lengthToCss(len) {
167
- return `${len.value}${len.unit}`;
267
+ if (typeof len !== "object" || len === null) return null;
268
+ const { value, unit } = len;
269
+ if (typeof value !== "number" || !Number.isFinite(value) || typeof unit !== "string" || !UNITS.has(unit)) return null;
270
+ return `${value}${unit}`;
168
271
  }
169
272
  function sizeToCss(size) {
170
273
  if (size === "fill") return "100%";
171
274
  if (size === "hug") return "auto";
172
275
  return lengthToCss(size);
173
276
  }
277
+ function numberToCss(value) {
278
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
279
+ }
280
+ function putLen(d, props, len) {
281
+ const css = lengthToCss(len);
282
+ if (css !== null) for (const p of props) d[p] = css;
283
+ }
174
284
  var ALIGN = {
175
285
  start: "flex-start",
176
286
  center: "center",
@@ -184,51 +294,150 @@ var JUSTIFY = {
184
294
  between: "space-between",
185
295
  around: "space-around"
186
296
  };
297
+ function safeCssValue(value) {
298
+ if (/[{}<]/.test(value)) return null;
299
+ let depth = 0;
300
+ let quote = null;
301
+ for (const ch of value) {
302
+ if (quote) {
303
+ if (ch === quote) quote = null;
304
+ } else if (ch === '"' || ch === "'") {
305
+ quote = ch;
306
+ } else if (ch === "(") {
307
+ depth++;
308
+ } else if (ch === ")") {
309
+ depth = Math.max(0, depth - 1);
310
+ } else if (ch === ";" && depth === 0) {
311
+ return null;
312
+ }
313
+ }
314
+ return value;
315
+ }
316
+ function put(d, prop, value) {
317
+ if (value === void 0) return;
318
+ const safe = safeCssValue(value);
319
+ if (safe !== null) d[prop] = safe;
320
+ }
321
+ var OVERFLOW = /* @__PURE__ */ new Set(["visible", "hidden", "auto"]);
322
+ var TEXT_TRANSFORM = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
323
+ var BACKGROUND_SIZE = /* @__PURE__ */ new Set(["cover", "contain", "auto"]);
324
+ var TEXT_ALIGN = /* @__PURE__ */ new Set(["left", "center", "right", "justify"]);
325
+ var DIRECTIONS = /* @__PURE__ */ new Set(["row", "column"]);
326
+ function keyword(map, value) {
327
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(map, value) ? map[value] : null;
328
+ }
329
+ function inSet(set, value) {
330
+ return typeof value === "string" && set.has(value) ? value : null;
331
+ }
187
332
  function styleDecls(style) {
188
333
  const d = {};
189
- if (style.padding) d["padding"] = lengthToCss(style.padding);
190
- if (style.margin) d["margin"] = lengthToCss(style.margin);
191
- if (style.radius) d["border-radius"] = lengthToCss(style.radius);
192
- if (style.background !== void 0) d["background"] = style.background;
193
- if (style.color !== void 0) d["color"] = style.color;
194
- if (style.opacity !== void 0) d["opacity"] = String(style.opacity);
195
- if (style.fontFamily !== void 0) d["font-family"] = style.fontFamily;
196
- if (style.fontSize) d["font-size"] = lengthToCss(style.fontSize);
197
- if (style.fontWeight !== void 0) d["font-weight"] = String(style.fontWeight);
198
- if (style.lineHeight !== void 0) d["line-height"] = String(style.lineHeight);
199
- if (style.textAlign !== void 0) d["text-align"] = style.textAlign;
200
- if (style.letterSpacing) d["letter-spacing"] = lengthToCss(style.letterSpacing);
201
- if (style.border !== void 0) d["border"] = style.border;
202
- if (style.boxShadow !== void 0) d["box-shadow"] = style.boxShadow;
203
- if (style.transform !== void 0) d["transform"] = style.transform;
204
- if (style.filter !== void 0) d["filter"] = style.filter;
205
- if (style.gap) d["gap"] = lengthToCss(style.gap);
206
- if (style.align !== void 0) d["align-items"] = ALIGN[style.align] ?? style.align;
207
- if (style.justify !== void 0) d["justify-content"] = JUSTIFY[style.justify] ?? style.justify;
334
+ putLen(d, ["padding"], style.padding);
335
+ putLen(d, ["padding-left", "padding-right"], style.paddingX);
336
+ putLen(d, ["padding-top", "padding-bottom"], style.paddingY);
337
+ putLen(d, ["margin"], style.margin);
338
+ if (style.marginX === "auto") d["margin-left"] = d["margin-right"] = "auto";
339
+ else putLen(d, ["margin-left", "margin-right"], style.marginX);
340
+ putLen(d, ["margin-top", "margin-bottom"], style.marginY);
341
+ putLen(d, ["border-radius"], style.radius);
342
+ putLen(d, ["max-width"], style.maxWidth);
343
+ putLen(d, ["min-height"], style.minHeight);
344
+ put(d, "aspect-ratio", typeof style.aspectRatio === "string" ? style.aspectRatio : void 0);
345
+ if (style.sticky === true) {
346
+ d["position"] = "sticky";
347
+ d["top"] = "0";
348
+ }
349
+ if (typeof style.zIndex === "number" && Number.isFinite(style.zIndex)) d["z-index"] = String(Math.trunc(style.zIndex));
350
+ const overflow = inSet(OVERFLOW, style.overflow);
351
+ if (overflow) d["overflow"] = overflow;
352
+ put(d, "background", typeof style.background === "string" ? style.background : void 0);
353
+ const backgroundSize = inSet(BACKGROUND_SIZE, style.backgroundSize);
354
+ if (backgroundSize) d["background-size"] = backgroundSize;
355
+ put(d, "background-position", typeof style.backgroundPosition === "string" ? style.backgroundPosition : void 0);
356
+ put(d, "color", typeof style.color === "string" ? style.color : void 0);
357
+ const opacity = numberToCss(style.opacity);
358
+ if (opacity !== null) d["opacity"] = opacity;
359
+ put(d, "font-family", typeof style.fontFamily === "string" ? style.fontFamily : void 0);
360
+ putLen(d, ["font-size"], style.fontSize);
361
+ const fontWeight = numberToCss(style.fontWeight);
362
+ if (fontWeight !== null) d["font-weight"] = fontWeight;
363
+ const lineHeight = numberToCss(style.lineHeight);
364
+ if (lineHeight !== null) d["line-height"] = lineHeight;
365
+ const textAlign = inSet(TEXT_ALIGN, style.textAlign);
366
+ if (textAlign) d["text-align"] = textAlign;
367
+ putLen(d, ["letter-spacing"], style.letterSpacing);
368
+ const textTransform = inSet(TEXT_TRANSFORM, style.textTransform);
369
+ if (textTransform) d["text-transform"] = textTransform;
370
+ put(d, "border", typeof style.border === "string" ? style.border : void 0);
371
+ put(d, "box-shadow", typeof style.boxShadow === "string" ? style.boxShadow : void 0);
372
+ put(d, "transform", typeof style.transform === "string" ? style.transform : void 0);
373
+ put(d, "filter", typeof style.filter === "string" ? style.filter : void 0);
374
+ putLen(d, ["gap"], style.gap);
375
+ const align = keyword(ALIGN, style.align);
376
+ if (align) d["align-items"] = align;
377
+ const justify = keyword(JUSTIFY, style.justify);
378
+ if (justify) d["justify-content"] = justify;
208
379
  return d;
209
380
  }
210
- function layoutDecls(node) {
381
+ function shownDisplay(node) {
382
+ if (node.layout === "stack") return "flex";
383
+ if (node.layout === "grid") return "grid";
384
+ return "block";
385
+ }
386
+ function gridColumns(style) {
387
+ const min = lengthToCss(style.columnMin);
388
+ if (min !== null) return `repeat(auto-fit, minmax(min(${min}, 100%), 1fr))`;
389
+ if (typeof style.columns === "number" && Number.isFinite(style.columns)) return `repeat(${Math.max(1, Math.trunc(style.columns))}, 1fr)`;
390
+ return void 0;
391
+ }
392
+ function containerDecls(node, style, isBase) {
211
393
  const d = {};
212
394
  if (node.layout === "stack") {
213
- d["display"] = "flex";
214
- d["flex-direction"] = node.style.base.direction ?? "column";
395
+ if (isBase) d["display"] = "flex";
396
+ const direction = inSet(DIRECTIONS, style.direction);
397
+ if (isBase || direction) d["flex-direction"] = direction ?? "column";
398
+ if (style.wrap !== void 0) d["flex-wrap"] = style.wrap ? "wrap" : "nowrap";
215
399
  } else if (node.layout === "grid") {
216
- d["display"] = "grid";
217
- d["grid-template-columns"] = `repeat(${node.style.base.columns ?? 1}, 1fr)`;
218
- } else if (node.layout === "free") {
400
+ if (isBase) d["display"] = "grid";
401
+ const cols = gridColumns(style);
402
+ if (isBase || cols !== void 0) d["grid-template-columns"] = cols ?? "repeat(1, 1fr)";
403
+ } else if (node.layout === "free" && isBase) {
219
404
  d["position"] = "relative";
220
405
  }
221
406
  return d;
222
407
  }
408
+ function visibilityDecls(node, style, isBase) {
409
+ if (style.hidden === true) return { display: "none" };
410
+ if (style.hidden === false && !isBase) return { display: shownDisplay(node) };
411
+ return {};
412
+ }
413
+ function themeDecls(theme) {
414
+ const d = {};
415
+ const walk = (value, path) => {
416
+ if (typeof value === "string" || typeof value === "number" && Number.isFinite(value)) {
417
+ const key = path.map((p) => p.toLowerCase().replace(/[^a-z0-9-]/g, "")).filter(Boolean).join("-");
418
+ if (key) put(d, `--cms-${key}`, String(value));
419
+ return;
420
+ }
421
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
422
+ for (const [k, v] of Object.entries(value)) walk(v, [...path, k]);
423
+ }
424
+ };
425
+ if (typeof theme === "object" && theme !== null && !Array.isArray(theme)) {
426
+ for (const [k, v] of Object.entries(theme)) {
427
+ if (k !== "name") walk(v, [k]);
428
+ }
429
+ }
430
+ return d;
431
+ }
223
432
  function constraintDecls(c, parentFree) {
224
433
  const d = {};
225
434
  const transforms = [];
226
435
  if (parentFree) {
227
436
  d["position"] = "absolute";
228
- if (c.left) d["left"] = lengthToCss(c.left);
229
- if (c.right) d["right"] = lengthToCss(c.right);
230
- if (c.top) d["top"] = lengthToCss(c.top);
231
- if (c.bottom) d["bottom"] = lengthToCss(c.bottom);
437
+ putLen(d, ["left"], c.left);
438
+ putLen(d, ["right"], c.right);
439
+ putLen(d, ["top"], c.top);
440
+ putLen(d, ["bottom"], c.bottom);
232
441
  if (c.centerX) {
233
442
  d["left"] = "50%";
234
443
  transforms.push("translateX(-50%)");
@@ -239,13 +448,15 @@ function constraintDecls(c, parentFree) {
239
448
  }
240
449
  if (transforms.length) d["transform"] = transforms.join(" ");
241
450
  }
242
- if (c.width) d["width"] = sizeToCss(c.width);
243
- if (c.height) d["height"] = sizeToCss(c.height);
451
+ const width = sizeToCss(c.width);
452
+ if (width !== null) d["width"] = width;
453
+ const height = sizeToCss(c.height);
454
+ if (height !== null) d["height"] = height;
244
455
  return d;
245
456
  }
246
457
  function isParentFree(doc, node) {
247
458
  if (node.parent === null) return false;
248
- const parent = doc.nodes[node.parent];
459
+ const parent = own(doc.nodes, node.parent);
249
460
  return !parent || parent.layout === void 0 || parent.layout === "free";
250
461
  }
251
462
  function serializeRule(selector, decls, indent) {
@@ -260,24 +471,31 @@ function emitDocCss(doc) {
260
471
  if (!hasNodeMap(doc)) return "";
261
472
  const breakpointPx = DEFAULT_BREAKPOINTS;
262
473
  const blocks = [];
474
+ const themeRule = serializeRule(":root", themeDecls(doc.theme), "");
475
+ if (themeRule) blocks.push(themeRule);
263
476
  for (const id of Object.keys(doc.nodes).sort()) {
264
- const node = doc.nodes[id];
477
+ const node = withNodeDefaults(doc.nodes[id]);
265
478
  const selector = selectorFor(id);
266
479
  const parentFree = isParentFree(doc, node);
267
480
  const base = {
268
- ...layoutDecls(node),
481
+ ...containerDecls(node, node.style.base, true),
269
482
  ...node.constraints ? constraintDecls(node.constraints.base, parentFree) : {},
270
- ...styleDecls(node.style.base)
483
+ ...styleDecls(node.style.base),
484
+ ...visibilityDecls(node, node.style.base, true)
271
485
  };
272
486
  const baseRule = serializeRule(selector, base, "");
273
487
  if (baseRule) blocks.push(baseRule);
274
488
  for (const bp of doc.breakpoints) {
275
489
  if (bp === "base") continue;
276
- const px = breakpointPx[bp];
277
- if (px === void 0) continue;
490
+ const px = own(breakpointPx, bp);
491
+ if (typeof px !== "number") continue;
492
+ const bpStyle = own(node.style, bp);
493
+ const bpConstraints = node.constraints ? own(node.constraints, bp) : void 0;
278
494
  const decls = {
279
- ...node.constraints?.[bp] ? constraintDecls(node.constraints[bp], parentFree) : {},
280
- ...node.style[bp] ? styleDecls(node.style[bp]) : {}
495
+ ...bpStyle ? containerDecls(node, bpStyle, false) : {},
496
+ ...bpConstraints ? constraintDecls(bpConstraints, parentFree) : {},
497
+ ...bpStyle ? styleDecls(bpStyle) : {},
498
+ ...bpStyle ? visibilityDecls(node, bpStyle, false) : {}
281
499
  };
282
500
  const inner = serializeRule(selector, decls, " ");
283
501
  if (inner) blocks.push(`@media (min-width: ${px}px) {
@@ -285,7 +503,8 @@ ${inner}
285
503
  }`);
286
504
  }
287
505
  }
288
- return blocks.join("\n\n") + (blocks.length ? "\n" : "");
506
+ if (blocks.length === 0 && Object.keys(doc.nodes).length === 0) return "";
507
+ return [BASE_CSS, ...blocks].join("\n\n") + "\n";
289
508
  }
290
509
  function CmsPage({
291
510
  doc: input,