@particle-academy/fancy-cms-ui 0.8.0 → 0.10.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/CHANGELOG.md +84 -0
- package/README.md +46 -0
- package/dist/{chunk-IOMIYI45.js → chunk-G7ULXTKI.js} +267 -34
- package/dist/chunk-G7ULXTKI.js.map +1 -0
- package/dist/{chunk-SJYKJ6SG.js → chunk-IRG5IKKO.js} +63 -45
- package/dist/chunk-IRG5IKKO.js.map +1 -0
- package/dist/{chunk-V6QIOJI7.js → chunk-TJL26EAX.js} +3 -3
- package/dist/{chunk-V6QIOJI7.js.map → chunk-TJL26EAX.js.map} +1 -1
- package/dist/editor.cjs +368 -117
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.d.cts +2 -2
- package/dist/editor.d.ts +2 -2
- package/dist/editor.js +3 -3
- package/dist/index.cjs +324 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/react.cjs +60 -42
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{registry-B4FaQDip.d.cts → registry-F2TAMjhx.d.cts} +7 -1
- package/dist/{registry-B4FaQDip.d.ts → registry-F2TAMjhx.d.ts} +7 -1
- package/dist/{useEditor-DZDD9qpS.d.ts → useEditor-fIQEaKzZ.d.cts} +55 -3
- package/dist/{useEditor-CIRm2oi-.d.cts → useEditor-wGEqxpJT.d.ts} +55 -3
- package/package.json +2 -2
- package/dist/chunk-IOMIYI45.js.map +0 -1
- package/dist/chunk-SJYKJ6SG.js.map +0 -1
package/dist/editor.cjs
CHANGED
|
@@ -16,6 +16,12 @@ function hasNodeMap(value) {
|
|
|
16
16
|
const nodes = value.nodes;
|
|
17
17
|
return typeof nodes === "object" && nodes !== null && !Array.isArray(nodes);
|
|
18
18
|
}
|
|
19
|
+
function withNodeDefaults(node) {
|
|
20
|
+
const props = typeof node.props === "object" && node.props !== null ? node.props : {};
|
|
21
|
+
const style = typeof node.style === "object" && node.style !== null ? node.style : { base: {} };
|
|
22
|
+
const base = typeof style.base === "object" && style.base !== null ? style.base : {};
|
|
23
|
+
return props === node.props && style === node.style && base === style.base ? node : { ...node, props, style: { ...style, base } };
|
|
24
|
+
}
|
|
19
25
|
var DEFAULT_BREAKPOINTS = {
|
|
20
26
|
base: 0,
|
|
21
27
|
md: 768,
|
|
@@ -41,51 +47,7 @@ function setOwn(map, key, value) {
|
|
|
41
47
|
Object.defineProperty(map, key, { value, writable: true, enumerable: true, configurable: true });
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
// src/document/
|
|
45
|
-
function normalizeDoc(input3) {
|
|
46
|
-
if (hasNodeMap(input3)) return input3;
|
|
47
|
-
const id = typeof input3 === "object" && input3 !== null && !Array.isArray(input3) && typeof input3.id === "string" ? input3.id : "malformed";
|
|
48
|
-
return emptyDoc(id);
|
|
49
|
-
}
|
|
50
|
-
function migrateDocVerbose(input3) {
|
|
51
|
-
const normalized = normalizeDoc(input3);
|
|
52
|
-
const legacy = normalized;
|
|
53
|
-
const sections = legacy.sections;
|
|
54
|
-
if (!Array.isArray(sections)) {
|
|
55
|
-
return { doc: normalized, migrated: false, danglingIds: [], unlistedRootIds: [] };
|
|
56
|
-
}
|
|
57
|
-
const danglingIds = [];
|
|
58
|
-
const ordered = [];
|
|
59
|
-
const seen = /* @__PURE__ */ new Set();
|
|
60
|
-
for (const id of sections) {
|
|
61
|
-
if (!own(normalized.nodes, id)) {
|
|
62
|
-
danglingIds.push(id);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (seen.has(id)) continue;
|
|
66
|
-
seen.add(id);
|
|
67
|
-
ordered.push(id);
|
|
68
|
-
}
|
|
69
|
-
const unlistedRootIds = Object.values(normalized.nodes).filter((n) => n.parent === null && !seen.has(n.id)).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0).map((n) => n.id);
|
|
70
|
-
const finalOrder = [...ordered, ...unlistedRootIds];
|
|
71
|
-
const nodes = { ...normalized.nodes };
|
|
72
|
-
let prev = null;
|
|
73
|
-
for (const id of finalOrder) {
|
|
74
|
-
const key = fancyDocCommons.fractionalKey(prev, null);
|
|
75
|
-
setOwn(nodes, id, { ...own(nodes, id), order: key });
|
|
76
|
-
prev = key;
|
|
77
|
-
}
|
|
78
|
-
const { sections: _dropped, ...rest } = legacy;
|
|
79
|
-
return {
|
|
80
|
-
doc: { ...rest, nodes },
|
|
81
|
-
migrated: true,
|
|
82
|
-
danglingIds,
|
|
83
|
-
unlistedRootIds
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function migrateDoc(input3) {
|
|
87
|
-
return migrateDocVerbose(input3).doc;
|
|
88
|
-
}
|
|
50
|
+
// src/document/reduce.ts
|
|
89
51
|
function childrenOf(doc, parent) {
|
|
90
52
|
return fancyDocCommons.childrenOf(doc, parent);
|
|
91
53
|
}
|
|
@@ -225,6 +187,50 @@ function reduce(doc, op, opts = {}) {
|
|
|
225
187
|
return invalid(`unknown op "${op.t}"`);
|
|
226
188
|
}
|
|
227
189
|
}
|
|
190
|
+
function normalizeDoc(input3) {
|
|
191
|
+
if (hasNodeMap(input3)) return input3;
|
|
192
|
+
const id = typeof input3 === "object" && input3 !== null && !Array.isArray(input3) && typeof input3.id === "string" ? input3.id : "malformed";
|
|
193
|
+
return emptyDoc(id);
|
|
194
|
+
}
|
|
195
|
+
function migrateDocVerbose(input3) {
|
|
196
|
+
const normalized = normalizeDoc(input3);
|
|
197
|
+
const legacy = normalized;
|
|
198
|
+
const sections = legacy.sections;
|
|
199
|
+
if (!Array.isArray(sections)) {
|
|
200
|
+
return { doc: normalized, migrated: false, danglingIds: [], unlistedRootIds: [] };
|
|
201
|
+
}
|
|
202
|
+
const danglingIds = [];
|
|
203
|
+
const ordered = [];
|
|
204
|
+
const seen = /* @__PURE__ */ new Set();
|
|
205
|
+
for (const id of sections) {
|
|
206
|
+
if (!own(normalized.nodes, id)) {
|
|
207
|
+
danglingIds.push(id);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (seen.has(id)) continue;
|
|
211
|
+
seen.add(id);
|
|
212
|
+
ordered.push(id);
|
|
213
|
+
}
|
|
214
|
+
const unlistedRootIds = Object.values(normalized.nodes).filter((n) => n.parent === null && !seen.has(n.id)).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0).map((n) => n.id);
|
|
215
|
+
const finalOrder = [...ordered, ...unlistedRootIds];
|
|
216
|
+
const nodes = { ...normalized.nodes };
|
|
217
|
+
let prev = null;
|
|
218
|
+
for (const id of finalOrder) {
|
|
219
|
+
const key = fancyDocCommons.fractionalKey(prev, null);
|
|
220
|
+
setOwn(nodes, id, { ...own(nodes, id), order: key });
|
|
221
|
+
prev = key;
|
|
222
|
+
}
|
|
223
|
+
const { sections: _dropped, ...rest } = legacy;
|
|
224
|
+
return {
|
|
225
|
+
doc: { ...rest, nodes },
|
|
226
|
+
migrated: true,
|
|
227
|
+
danglingIds,
|
|
228
|
+
unlistedRootIds
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function migrateDoc(input3) {
|
|
232
|
+
return migrateDocVerbose(input3).doc;
|
|
233
|
+
}
|
|
228
234
|
function initEditor(doc) {
|
|
229
235
|
return { doc: migrateDoc(doc), past: [], future: [], selection: null };
|
|
230
236
|
}
|
|
@@ -311,6 +317,28 @@ function cssString(value) {
|
|
|
311
317
|
return out;
|
|
312
318
|
}
|
|
313
319
|
var selectorFor = (id) => `[data-cms="${cssString(id)}"]`;
|
|
320
|
+
var BASE_CSS = [
|
|
321
|
+
"/* fancy-cms base */",
|
|
322
|
+
":where([data-cms]) {\n box-sizing: border-box;\n}",
|
|
323
|
+
":where(h1, h2, h3, h4, h5, h6, p, ul, ol)[data-cms] {\n margin: 0;\n}",
|
|
324
|
+
":where(ul, ol)[data-cms] {\n padding-left: 1.25em;\n}",
|
|
325
|
+
":where(a[data-cms]) {\n color: inherit;\n}",
|
|
326
|
+
":where([data-cms] > img) {\n display: block;\n height: 100%;\n object-fit: cover;\n width: 100%;\n}",
|
|
327
|
+
':where([data-cms] > img[data-cms-fit="contain"]) {\n object-fit: contain;\n}',
|
|
328
|
+
':where([data-cms] > img[data-cms-fit="fill"]) {\n object-fit: fill;\n}',
|
|
329
|
+
':where([data-cms] > img[data-cms-fit="none"]) {\n object-fit: none;\n}',
|
|
330
|
+
':where([data-cms] > img[data-cms-fit="scale-down"]) {\n object-fit: scale-down;\n}',
|
|
331
|
+
":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}",
|
|
332
|
+
':where([data-cms-button="primary"]) {\n background: var(--cms-color-primary, #7c3aed);\n color: #ffffff;\n}',
|
|
333
|
+
':where([data-cms-button="outline"]) {\n background: transparent;\n color: var(--cms-color-primary, #7c3aed);\n}',
|
|
334
|
+
':where([data-cms-button="ghost"]) {\n background: transparent;\n border-color: currentColor;\n color: inherit;\n}',
|
|
335
|
+
":where([data-cms-callout]) {\n background: #3b82f614;\n border-left: 3px solid #3b82f6;\n border-radius: 10px;\n padding: 12px 14px;\n}",
|
|
336
|
+
':where([data-cms-callout="success"]) {\n background: #10b98114;\n border-left-color: #10b981;\n}',
|
|
337
|
+
':where([data-cms-callout="warning"]) {\n background: #f59e0b14;\n border-left-color: #f59e0b;\n}',
|
|
338
|
+
':where([data-cms-callout="danger"]) {\n background: #ef444414;\n border-left-color: #ef4444;\n}',
|
|
339
|
+
":where([data-cms] > hr) {\n border: none;\n border-top: 1px solid #e2e8f0;\n margin: 0;\n}",
|
|
340
|
+
":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}"
|
|
341
|
+
].join("\n\n");
|
|
314
342
|
var UNITS = /* @__PURE__ */ new Set(["px", "%", "vw", "vh", "rem", "fr"]);
|
|
315
343
|
function lengthToCss(len2) {
|
|
316
344
|
if (typeof len2 !== "object" || len2 === null) return null;
|
|
@@ -523,7 +551,7 @@ function emitDocCss(doc) {
|
|
|
523
551
|
const themeRule = serializeRule(":root", themeDecls(doc.theme), "");
|
|
524
552
|
if (themeRule) blocks.push(themeRule);
|
|
525
553
|
for (const id of Object.keys(doc.nodes).sort()) {
|
|
526
|
-
const node = doc.nodes[id];
|
|
554
|
+
const node = withNodeDefaults(doc.nodes[id]);
|
|
527
555
|
const selector = selectorFor(id);
|
|
528
556
|
const parentFree = isParentFree(doc, node);
|
|
529
557
|
const base = {
|
|
@@ -536,12 +564,13 @@ function emitDocCss(doc) {
|
|
|
536
564
|
if (baseRule) blocks.push(baseRule);
|
|
537
565
|
for (const bp of doc.breakpoints) {
|
|
538
566
|
if (bp === "base") continue;
|
|
539
|
-
const px = breakpointPx
|
|
540
|
-
if (px
|
|
541
|
-
const bpStyle = node.style
|
|
567
|
+
const px = own(breakpointPx, bp);
|
|
568
|
+
if (typeof px !== "number") continue;
|
|
569
|
+
const bpStyle = own(node.style, bp);
|
|
570
|
+
const bpConstraints = node.constraints ? own(node.constraints, bp) : void 0;
|
|
542
571
|
const decls = {
|
|
543
572
|
...bpStyle ? containerDecls(node, bpStyle, false) : {},
|
|
544
|
-
...
|
|
573
|
+
...bpConstraints ? constraintDecls(bpConstraints, parentFree) : {},
|
|
545
574
|
...bpStyle ? styleDecls(bpStyle) : {},
|
|
546
575
|
...bpStyle ? visibilityDecls(node, bpStyle, false) : {}
|
|
547
576
|
};
|
|
@@ -551,7 +580,8 @@ ${inner}
|
|
|
551
580
|
}`);
|
|
552
581
|
}
|
|
553
582
|
}
|
|
554
|
-
|
|
583
|
+
if (blocks.length === 0 && Object.keys(doc.nodes).length === 0) return "";
|
|
584
|
+
return [BASE_CSS, ...blocks].join("\n\n") + "\n";
|
|
555
585
|
}
|
|
556
586
|
function getPath(data, path) {
|
|
557
587
|
return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], data);
|
|
@@ -594,10 +624,17 @@ function elementFor(node, text) {
|
|
|
594
624
|
case "button":
|
|
595
625
|
case "link": {
|
|
596
626
|
const href = safeHref(text(node.props.href));
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
627
|
+
const attrs = {};
|
|
628
|
+
if (href !== null) {
|
|
629
|
+
attrs.href = href;
|
|
630
|
+
if (/^https?:\/\//i.test(href)) attrs.rel = "noopener noreferrer";
|
|
631
|
+
}
|
|
632
|
+
if (node.type === "button") attrs["data-cms-button"] = BUTTON_VARIANTS.has(text(node.props.variant)) ? text(node.props.variant) : "primary";
|
|
633
|
+
return { tag: href !== null ? "a" : node.type === "link" ? "span" : "div", attrs };
|
|
634
|
+
}
|
|
635
|
+
case "callout": {
|
|
636
|
+
const variant = text(node.props.variant);
|
|
637
|
+
return { tag: "div", attrs: { "data-cms-callout": CALLOUT_VARIANTS.has(variant) ? variant : "info" } };
|
|
601
638
|
}
|
|
602
639
|
default:
|
|
603
640
|
return { tag: CONTAINERS.has(node.type) && LANDMARKS.has(as) ? as : "div" };
|
|
@@ -621,51 +658,32 @@ var defaultRegistry = {
|
|
|
621
658
|
const items = resolve(node.props.items);
|
|
622
659
|
return Array.isArray(items) ? items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: item === null || item === void 0 ? "" : String(item) }, i)) : null;
|
|
623
660
|
},
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
},
|
|
629
|
-
|
|
630
|
-
button: ({ node, text }) => {
|
|
631
|
-
const label2 = text(node.props.label) || text(node.props.content) || "Button";
|
|
632
|
-
const variant = text(node.props.variant) || "primary";
|
|
633
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cms-button": variant, style: buttonStyle(variant), children: label2 });
|
|
634
|
-
},
|
|
661
|
+
// Looks come from the base stylesheet (render/css.ts BASE_CSS), never from
|
|
662
|
+
// inline style objects, so the PHP renderer emits the same markup.
|
|
663
|
+
divider: () => /* @__PURE__ */ jsxRuntime.jsx("hr", {}),
|
|
664
|
+
callout: ({ node, text }) => text(node.props.content) || "Callout",
|
|
665
|
+
code: ({ node, text }) => /* @__PURE__ */ jsxRuntime.jsx("pre", { children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: text(node.props.content) }) }),
|
|
666
|
+
button: ({ node, text }) => text(node.props.label) || text(node.props.content) || "Button",
|
|
635
667
|
image: ({ node, text }) => {
|
|
636
668
|
const fit = text(node.props.fit);
|
|
637
669
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
638
670
|
"img",
|
|
639
671
|
{
|
|
640
|
-
src: text(node.props.src),
|
|
672
|
+
src: text(node.props.src) || void 0,
|
|
641
673
|
alt: text(node.props.alt),
|
|
642
674
|
loading: text(node.props.loading) === "eager" ? "eager" : "lazy",
|
|
643
|
-
|
|
675
|
+
"data-cms-fit": IMAGE_FITS.has(fit) && fit !== "cover" ? fit : void 0
|
|
644
676
|
}
|
|
645
677
|
);
|
|
646
678
|
}
|
|
647
679
|
};
|
|
648
680
|
var IMAGE_FITS = /* @__PURE__ */ new Set(["cover", "contain", "fill", "none", "scale-down"]);
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
display: "inline-flex",
|
|
652
|
-
alignItems: "center",
|
|
653
|
-
gap: 6,
|
|
654
|
-
padding: "9px 16px",
|
|
655
|
-
borderRadius: 8,
|
|
656
|
-
fontWeight: 600,
|
|
657
|
-
fontSize: 14,
|
|
658
|
-
lineHeight: 1,
|
|
659
|
-
cursor: "pointer",
|
|
660
|
-
userSelect: "none"
|
|
661
|
-
};
|
|
662
|
-
if (variant === "ghost") return { ...base, background: "transparent", color: "inherit", border: "1px solid currentColor" };
|
|
663
|
-
if (variant === "outline") return { ...base, background: "transparent", color: "#7c3aed", border: "1px solid #7c3aed" };
|
|
664
|
-
return { ...base, background: "#7c3aed", color: "#fff", border: "1px solid #7c3aed" };
|
|
665
|
-
}
|
|
681
|
+
var BUTTON_VARIANTS = /* @__PURE__ */ new Set(["primary", "outline", "ghost"]);
|
|
682
|
+
var CALLOUT_VARIANTS = /* @__PURE__ */ new Set(["info", "success", "warning", "danger"]);
|
|
666
683
|
function RenderNode({ doc, id, registry = defaultRegistry, data }) {
|
|
667
|
-
const
|
|
668
|
-
if (!
|
|
684
|
+
const stored = own(doc.nodes, id);
|
|
685
|
+
if (!stored) return null;
|
|
686
|
+
const node = withNodeDefaults(stored);
|
|
669
687
|
if (node.type === "repeater") {
|
|
670
688
|
const arr = resolveValue(node.props.items, data);
|
|
671
689
|
const items = Array.isArray(arr) ? arr : [];
|
|
@@ -882,16 +900,42 @@ var input = {
|
|
|
882
900
|
font: "inherit",
|
|
883
901
|
fontSize: 13
|
|
884
902
|
};
|
|
885
|
-
function Inspector({ doc, selection, apply }) {
|
|
903
|
+
function Inspector({ doc, selection, apply, drafts }) {
|
|
886
904
|
const uid = react.useId();
|
|
887
905
|
const fieldId = (key) => `${uid}-${key}`;
|
|
888
906
|
const node = selection ? doc.nodes[selection] : null;
|
|
889
907
|
if (!node) {
|
|
890
908
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 16, color: "var(--fcms-muted)", fontFamily: "system-ui, sans-serif", fontSize: 13 }, children: "Select an element to edit." });
|
|
891
909
|
}
|
|
892
|
-
const
|
|
893
|
-
const
|
|
894
|
-
|
|
910
|
+
const nodeId = node.id;
|
|
911
|
+
const bind = (name, read, op, numeric = false) => {
|
|
912
|
+
const key = JSON.stringify([nodeId, name]);
|
|
913
|
+
const field2 = {
|
|
914
|
+
key,
|
|
915
|
+
label: `${nodeId} / ${name}`,
|
|
916
|
+
op,
|
|
917
|
+
read: (next) => {
|
|
918
|
+
const item = next.nodes[nodeId];
|
|
919
|
+
return item ? read(item) : void 0;
|
|
920
|
+
},
|
|
921
|
+
canonical: numeric ? (raw) => String(Number(raw) || 0) : (raw) => raw
|
|
922
|
+
};
|
|
923
|
+
const accepted = read(node);
|
|
924
|
+
return {
|
|
925
|
+
value: drafts ? drafts.value(key, accepted) : accepted,
|
|
926
|
+
onChange: (event) => {
|
|
927
|
+
if (drafts) drafts.change(field2, event.target.value);
|
|
928
|
+
else apply(op(event.target.value));
|
|
929
|
+
},
|
|
930
|
+
onFocus: () => drafts?.focus(key),
|
|
931
|
+
onBlur: () => drafts?.blur(key),
|
|
932
|
+
onCompositionStart: () => drafts?.composition(key, true),
|
|
933
|
+
onCompositionEnd: () => drafts?.composition(key, false)
|
|
934
|
+
};
|
|
935
|
+
};
|
|
936
|
+
const prop = (key) => bind(key, (item) => typeof item.props[key] === "string" ? item.props[key] : "", (raw) => ({ t: "set_node_props", id: nodeId, patch: { [key]: raw } }));
|
|
937
|
+
const style = (key) => bind(key, (item) => item.style.base[key] ?? "", (raw) => ({ t: "set_style", id: nodeId, breakpoint: "base", patch: { [key]: raw } }));
|
|
938
|
+
const length = (key) => bind(key, (item) => item.style.base[key]?.value?.toString() ?? "", (raw) => ({ t: "set_style", id: nodeId, breakpoint: "base", patch: { [key]: { value: Number(raw) || 0, unit: "px" } } }), true);
|
|
895
939
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { overflow: "auto", padding: 16, fontFamily: "system-ui, sans-serif", height: "100%", boxSizing: "border-box" }, children: [
|
|
896
940
|
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "var(--fcms-fg)" }, children: node.type }),
|
|
897
941
|
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: "var(--fcms-muted)", marginBottom: 8 }, children: node.id }),
|
|
@@ -903,8 +947,7 @@ function Inspector({ doc, selection, apply }) {
|
|
|
903
947
|
id: fieldId("content"),
|
|
904
948
|
"data-cms-field": "content",
|
|
905
949
|
style: { ...input, minHeight: 64, resize: "vertical" },
|
|
906
|
-
|
|
907
|
-
onChange: (e) => setProp("content", e.target.value)
|
|
950
|
+
...prop("content")
|
|
908
951
|
}
|
|
909
952
|
)
|
|
910
953
|
] }) : null,
|
|
@@ -916,8 +959,7 @@ function Inspector({ doc, selection, apply }) {
|
|
|
916
959
|
id: fieldId("src"),
|
|
917
960
|
"data-cms-field": "src",
|
|
918
961
|
style: input,
|
|
919
|
-
|
|
920
|
-
onChange: (e) => setProp("src", e.target.value)
|
|
962
|
+
...prop("src")
|
|
921
963
|
}
|
|
922
964
|
)
|
|
923
965
|
] }) : null,
|
|
@@ -929,8 +971,7 @@ function Inspector({ doc, selection, apply }) {
|
|
|
929
971
|
"data-cms-field": "background",
|
|
930
972
|
style: input,
|
|
931
973
|
placeholder: "#ffffff | gradient | url(\u2026)",
|
|
932
|
-
|
|
933
|
-
onChange: (e) => setStyle({ background: e.target.value })
|
|
974
|
+
...style("background")
|
|
934
975
|
}
|
|
935
976
|
),
|
|
936
977
|
/* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("color"), children: "Text color" }),
|
|
@@ -941,8 +982,7 @@ function Inspector({ doc, selection, apply }) {
|
|
|
941
982
|
"data-cms-field": "color",
|
|
942
983
|
style: input,
|
|
943
984
|
placeholder: "#0f172a",
|
|
944
|
-
|
|
945
|
-
onChange: (e) => setStyle({ color: e.target.value })
|
|
985
|
+
...style("color")
|
|
946
986
|
}
|
|
947
987
|
),
|
|
948
988
|
/* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("fontSize"), children: "Font size (px)" }),
|
|
@@ -953,8 +993,7 @@ function Inspector({ doc, selection, apply }) {
|
|
|
953
993
|
"data-cms-field": "fontSize",
|
|
954
994
|
type: "number",
|
|
955
995
|
style: input,
|
|
956
|
-
|
|
957
|
-
onChange: (e) => setStyle({ fontSize: { value: Number(e.target.value) || 0, unit: "px" } })
|
|
996
|
+
...length("fontSize")
|
|
958
997
|
}
|
|
959
998
|
),
|
|
960
999
|
/* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("padding"), children: "Padding (px)" }),
|
|
@@ -965,8 +1004,7 @@ function Inspector({ doc, selection, apply }) {
|
|
|
965
1004
|
"data-cms-field": "padding",
|
|
966
1005
|
type: "number",
|
|
967
1006
|
style: input,
|
|
968
|
-
|
|
969
|
-
onChange: (e) => setStyle({ padding: { value: Number(e.target.value) || 0, unit: "px" } })
|
|
1007
|
+
...length("padding")
|
|
970
1008
|
}
|
|
971
1009
|
),
|
|
972
1010
|
/* @__PURE__ */ jsxRuntime.jsx("label", { style: label, htmlFor: fieldId("opacity"), children: "Opacity" }),
|
|
@@ -980,12 +1018,171 @@ function Inspector({ doc, selection, apply }) {
|
|
|
980
1018
|
min: "0",
|
|
981
1019
|
max: "1",
|
|
982
1020
|
style: input,
|
|
983
|
-
|
|
984
|
-
onChange: (e) => setStyle({ opacity: Number(e.target.value) })
|
|
1021
|
+
...bind("opacity", (item) => item.style.base.opacity?.toString() ?? "", (raw) => ({ t: "set_style", id: nodeId, breakpoint: "base", patch: { opacity: Number(raw) } }), true)
|
|
985
1022
|
}
|
|
986
1023
|
)
|
|
987
1024
|
] });
|
|
988
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
// src/editor/InspectorDrafts.ts
|
|
1028
|
+
var InspectorDrafts = class {
|
|
1029
|
+
constructor(doc) {
|
|
1030
|
+
this.doc = doc;
|
|
1031
|
+
this.drafts = /* @__PURE__ */ new Map();
|
|
1032
|
+
this.composing = /* @__PURE__ */ new Set();
|
|
1033
|
+
this.focused = null;
|
|
1034
|
+
this.flight = null;
|
|
1035
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1036
|
+
this.version = 0;
|
|
1037
|
+
this.disabled = false;
|
|
1038
|
+
this.emit = () => false;
|
|
1039
|
+
this.subscribe = (listener) => {
|
|
1040
|
+
this.listeners.add(listener);
|
|
1041
|
+
return () => {
|
|
1042
|
+
this.listeners.delete(listener);
|
|
1043
|
+
};
|
|
1044
|
+
};
|
|
1045
|
+
this.snapshot = () => this.version;
|
|
1046
|
+
}
|
|
1047
|
+
get busy() {
|
|
1048
|
+
return this.flight !== null || [...this.drafts.values()].some((draft) => draft.dirty || draft.conflict !== null);
|
|
1049
|
+
}
|
|
1050
|
+
get conflicts() {
|
|
1051
|
+
return [...this.drafts.values()].filter((draft) => draft.conflict !== null).map((draft) => ({
|
|
1052
|
+
key: draft.field.key,
|
|
1053
|
+
label: draft.field.label,
|
|
1054
|
+
message: draft.conflict,
|
|
1055
|
+
missing: draft.field.read(this.doc) === void 0
|
|
1056
|
+
}));
|
|
1057
|
+
}
|
|
1058
|
+
value(key, accepted) {
|
|
1059
|
+
return this.drafts.get(key)?.raw ?? accepted;
|
|
1060
|
+
}
|
|
1061
|
+
configure(doc, disabled, emit) {
|
|
1062
|
+
const settled = this.disabled && !disabled;
|
|
1063
|
+
this.doc = doc;
|
|
1064
|
+
this.disabled = disabled;
|
|
1065
|
+
this.emit = emit;
|
|
1066
|
+
let changed = false;
|
|
1067
|
+
for (const [key, draft] of this.drafts) {
|
|
1068
|
+
const accepted = draft.field.read(doc);
|
|
1069
|
+
if (this.flight?.draft === draft && accepted === this.flight.expected) {
|
|
1070
|
+
this.clearFlight();
|
|
1071
|
+
draft.base = accepted;
|
|
1072
|
+
draft.dirty = draft.field.canonical(draft.raw) !== accepted;
|
|
1073
|
+
if (!draft.dirty && !draft.focused) this.drafts.delete(key);
|
|
1074
|
+
changed = true;
|
|
1075
|
+
} else if (accepted !== draft.base) {
|
|
1076
|
+
this.conflict(draft, accepted === void 0 ? "The edited element was removed. Your draft is retained." : "The server changed this field. Your draft is retained.");
|
|
1077
|
+
draft.base = accepted;
|
|
1078
|
+
changed = true;
|
|
1079
|
+
} else if (settled && this.flight?.draft === draft) {
|
|
1080
|
+
this.conflict(draft, "The save was not accepted. Your draft is retained.");
|
|
1081
|
+
changed = true;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (changed) this.notify();
|
|
1085
|
+
this.arm();
|
|
1086
|
+
}
|
|
1087
|
+
focus(key) {
|
|
1088
|
+
this.focused = key;
|
|
1089
|
+
const draft = this.drafts.get(key);
|
|
1090
|
+
if (draft) draft.focused = true;
|
|
1091
|
+
}
|
|
1092
|
+
change(field2, raw) {
|
|
1093
|
+
let draft = this.drafts.get(field2.key);
|
|
1094
|
+
if (!draft) {
|
|
1095
|
+
draft = { field: field2, raw, base: field2.read(this.doc), focused: this.focused === field2.key, dirty: true, conflict: null, readyAt: Date.now() + 250 };
|
|
1096
|
+
this.drafts.set(field2.key, draft);
|
|
1097
|
+
} else {
|
|
1098
|
+
draft.field = field2;
|
|
1099
|
+
draft.raw = raw;
|
|
1100
|
+
draft.readyAt = Date.now() + 250;
|
|
1101
|
+
}
|
|
1102
|
+
draft.dirty = field2.canonical(raw) !== draft.base;
|
|
1103
|
+
this.notify();
|
|
1104
|
+
this.arm();
|
|
1105
|
+
}
|
|
1106
|
+
blur(key) {
|
|
1107
|
+
if (this.focused === key) this.focused = null;
|
|
1108
|
+
const draft = this.drafts.get(key);
|
|
1109
|
+
if (draft) {
|
|
1110
|
+
draft.focused = false;
|
|
1111
|
+
draft.readyAt = Date.now();
|
|
1112
|
+
if (!draft.dirty && !draft.conflict && this.flight?.draft !== draft) this.drafts.delete(key);
|
|
1113
|
+
this.notify();
|
|
1114
|
+
this.pump();
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
composition(key, active) {
|
|
1118
|
+
if (active) this.composing.add(key);
|
|
1119
|
+
else this.composing.delete(key);
|
|
1120
|
+
const draft = this.drafts.get(key);
|
|
1121
|
+
if (draft) draft.readyAt = Date.now() + 250;
|
|
1122
|
+
this.arm();
|
|
1123
|
+
}
|
|
1124
|
+
resolve(key, keep) {
|
|
1125
|
+
if (this.disabled) return;
|
|
1126
|
+
const draft = this.drafts.get(key);
|
|
1127
|
+
if (!draft) return;
|
|
1128
|
+
if (this.flight?.draft === draft) this.clearFlight();
|
|
1129
|
+
if (!keep) this.drafts.delete(key);
|
|
1130
|
+
else {
|
|
1131
|
+
const accepted = draft.field.read(this.doc);
|
|
1132
|
+
if (accepted === void 0) return;
|
|
1133
|
+
draft.base = accepted;
|
|
1134
|
+
draft.conflict = null;
|
|
1135
|
+
draft.dirty = draft.field.canonical(draft.raw) !== accepted;
|
|
1136
|
+
draft.readyAt = Date.now();
|
|
1137
|
+
if (!draft.dirty && !draft.focused) this.drafts.delete(key);
|
|
1138
|
+
}
|
|
1139
|
+
this.notify();
|
|
1140
|
+
this.pump();
|
|
1141
|
+
}
|
|
1142
|
+
dispose() {
|
|
1143
|
+
clearTimeout(this.timer);
|
|
1144
|
+
clearTimeout(this.ackTimer);
|
|
1145
|
+
}
|
|
1146
|
+
notify() {
|
|
1147
|
+
this.version++;
|
|
1148
|
+
for (const listener of this.listeners) listener();
|
|
1149
|
+
}
|
|
1150
|
+
clearFlight() {
|
|
1151
|
+
this.flight = null;
|
|
1152
|
+
clearTimeout(this.ackTimer);
|
|
1153
|
+
this.ackTimer = void 0;
|
|
1154
|
+
}
|
|
1155
|
+
conflict(draft, message) {
|
|
1156
|
+
draft.conflict = message;
|
|
1157
|
+
if (this.flight?.draft === draft) this.clearFlight();
|
|
1158
|
+
}
|
|
1159
|
+
arm() {
|
|
1160
|
+
clearTimeout(this.timer);
|
|
1161
|
+
this.timer = void 0;
|
|
1162
|
+
if (this.disabled || this.flight || this.conflicts.length > 0) return;
|
|
1163
|
+
const ready = [...this.drafts.values()].filter((draft) => draft.dirty && !this.composing.has(draft.field.key));
|
|
1164
|
+
if (ready.length) this.timer = setTimeout(() => this.pump(), Math.max(0, Math.min(...ready.map((draft) => draft.readyAt)) - Date.now()));
|
|
1165
|
+
}
|
|
1166
|
+
pump() {
|
|
1167
|
+
if (this.disabled || this.flight || this.conflicts.length > 0) return;
|
|
1168
|
+
const draft = [...this.drafts.values()].find((item) => item.dirty && item.readyAt <= Date.now() && !this.composing.has(item.field.key));
|
|
1169
|
+
if (!draft) {
|
|
1170
|
+
this.arm();
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
this.flight = { draft, expected: draft.field.canonical(draft.raw) };
|
|
1174
|
+
this.ackTimer = setTimeout(() => {
|
|
1175
|
+
this.conflict(draft, "The save has not been acknowledged. Your draft is retained.");
|
|
1176
|
+
this.notify();
|
|
1177
|
+
}, 1e4);
|
|
1178
|
+
try {
|
|
1179
|
+
if (!this.emit(draft.field.op(draft.raw))) this.conflict(draft, "This edit could not be submitted. Your draft is retained.");
|
|
1180
|
+
} catch {
|
|
1181
|
+
this.conflict(draft, "This edit could not be submitted. Your draft is retained.");
|
|
1182
|
+
}
|
|
1183
|
+
this.notify();
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
989
1186
|
var CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
990
1187
|
"section",
|
|
991
1188
|
"frame",
|
|
@@ -1059,18 +1256,59 @@ function buildInsertOp(doc, kind, selectedId, targetId) {
|
|
|
1059
1256
|
}
|
|
1060
1257
|
};
|
|
1061
1258
|
}
|
|
1062
|
-
function Editor({ defaultValue, onChange, registry, data }) {
|
|
1259
|
+
function Editor({ value, defaultValue, onChange, onOp, disabled = false, registry, data }) {
|
|
1063
1260
|
const initial = react.useMemo(() => defaultValue ?? emptyDoc("untitled"), [defaultValue]);
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1261
|
+
const local = useEditor(initial);
|
|
1262
|
+
const controlled = value !== void 0;
|
|
1263
|
+
const localDoc = useLatestRef(local.state.doc);
|
|
1264
|
+
const lastNotified = react.useRef(local.state.doc);
|
|
1265
|
+
const onOpRef = useLatestRef(onOp);
|
|
1266
|
+
const [drafts] = react.useState(() => new InspectorDrafts(value ?? initial));
|
|
1267
|
+
react.useSyncExternalStore(drafts.subscribe, drafts.snapshot, drafts.snapshot);
|
|
1066
1268
|
const onChangeRef = useLatestRef(onChange);
|
|
1067
1269
|
react.useEffect(() => {
|
|
1068
|
-
if (
|
|
1069
|
-
|
|
1070
|
-
|
|
1270
|
+
if (controlled || lastNotified.current === local.state.doc) return;
|
|
1271
|
+
lastNotified.current = local.state.doc;
|
|
1272
|
+
onChangeRef.current?.(local.state.doc);
|
|
1273
|
+
}, [controlled, local.state.doc, onChangeRef]);
|
|
1274
|
+
const apply = (op) => {
|
|
1275
|
+
if (disabled) return false;
|
|
1276
|
+
const source = value ?? localDoc.current;
|
|
1277
|
+
const next = reduce(source, op);
|
|
1278
|
+
if (next === source) return false;
|
|
1279
|
+
if (!controlled) {
|
|
1280
|
+
localDoc.current = next;
|
|
1281
|
+
local.apply(op);
|
|
1071
1282
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1283
|
+
onOpRef.current?.(op);
|
|
1284
|
+
if (controlled) onChangeRef.current?.(next);
|
|
1285
|
+
return true;
|
|
1286
|
+
};
|
|
1287
|
+
react.useEffect(() => {
|
|
1288
|
+
if (controlled) drafts.configure(value, disabled, apply);
|
|
1289
|
+
});
|
|
1290
|
+
react.useEffect(() => () => drafts.dispose(), [drafts]);
|
|
1291
|
+
const structuralDisabled = disabled || controlled && drafts.busy;
|
|
1292
|
+
const ed = {
|
|
1293
|
+
...local,
|
|
1294
|
+
state: { ...local.state, doc: value ?? local.state.doc },
|
|
1295
|
+
apply: (op) => {
|
|
1296
|
+
if (!structuralDisabled) apply(op);
|
|
1297
|
+
},
|
|
1298
|
+
// Snapshot history belongs to the local document. Replaying it into a
|
|
1299
|
+
// server-owned value could overwrite another writer's accepted work.
|
|
1300
|
+
undo: () => {
|
|
1301
|
+
if (!controlled && !disabled) local.undo();
|
|
1302
|
+
},
|
|
1303
|
+
redo: () => {
|
|
1304
|
+
if (!controlled && !disabled) local.redo();
|
|
1305
|
+
},
|
|
1306
|
+
replace: (next) => {
|
|
1307
|
+
if (!controlled && !disabled) local.replace(next);
|
|
1308
|
+
},
|
|
1309
|
+
canUndo: !controlled && !disabled && local.canUndo,
|
|
1310
|
+
canRedo: !controlled && !disabled && local.canRedo
|
|
1311
|
+
};
|
|
1074
1312
|
const shell = {
|
|
1075
1313
|
display: "grid",
|
|
1076
1314
|
gridTemplateColumns: "220px 1fr 300px",
|
|
@@ -1082,9 +1320,9 @@ function Editor({ defaultValue, onChange, registry, data }) {
|
|
|
1082
1320
|
borderRadius: 12,
|
|
1083
1321
|
overflow: "hidden"
|
|
1084
1322
|
};
|
|
1085
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fancy-cms-editor", style: shell, children: [
|
|
1323
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fancy-cms-editor", style: shell, "aria-busy": structuralDisabled || void 0, children: [
|
|
1086
1324
|
/* @__PURE__ */ jsxRuntime.jsx("style", { children: CHROME_CSS }),
|
|
1087
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { borderRight: "1px solid var(--fcms-border)", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
|
|
1325
|
+
/* @__PURE__ */ jsxRuntime.jsx("fieldset", { disabled: structuralDisabled, "aria-label": "Page structure controls", style: { display: "contents", border: 0, padding: 0, margin: 0 }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { borderRight: "1px solid var(--fcms-border)", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
|
|
1088
1326
|
/* @__PURE__ */ jsxRuntime.jsx(Toolbar, { ed }),
|
|
1089
1327
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1090
1328
|
LayersPanel,
|
|
@@ -1095,9 +1333,22 @@ function Editor({ defaultValue, onChange, registry, data }) {
|
|
|
1095
1333
|
onMove: (id, parent, order) => ed.apply({ t: "move_node", id, parent, order })
|
|
1096
1334
|
}
|
|
1097
1335
|
)
|
|
1098
|
-
] }),
|
|
1336
|
+
] }) }),
|
|
1099
1337
|
/* @__PURE__ */ jsxRuntime.jsx(Canvas, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select, apply: ed.apply, registry, data }),
|
|
1100
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1338
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { borderLeft: "1px solid var(--fcms-border)", minHeight: 0, overflow: "auto" }, children: [
|
|
1339
|
+
controlled && drafts.conflicts.map((conflict) => /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "alert", "data-cms-draft-conflict": conflict.key, style: { padding: 12, fontFamily: "system-ui,sans-serif", fontSize: 13 }, children: [
|
|
1340
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
1341
|
+
conflict.label,
|
|
1342
|
+
": ",
|
|
1343
|
+
conflict.message
|
|
1344
|
+
] }),
|
|
1345
|
+
/* @__PURE__ */ jsxRuntime.jsx("pre", { "data-cms-retained-draft": "", style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere" }, children: drafts.value(conflict.key, "") }),
|
|
1346
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled, onClick: () => drafts.resolve(conflict.key, false), children: "Use server value" }),
|
|
1347
|
+
" ",
|
|
1348
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled: disabled || conflict.missing, onClick: () => drafts.resolve(conflict.key, true), children: "Keep draft" })
|
|
1349
|
+
] }, conflict.key)),
|
|
1350
|
+
/* @__PURE__ */ jsxRuntime.jsx("fieldset", { disabled: !controlled && disabled, "aria-label": "Page property controls", style: { border: 0, padding: 0, margin: 0, minWidth: 0, height: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsx(Inspector, { doc: ed.state.doc, selection: ed.state.selection, apply: ed.apply, drafts: controlled ? drafts : void 0 }) })
|
|
1351
|
+
] })
|
|
1101
1352
|
] });
|
|
1102
1353
|
}
|
|
1103
1354
|
function Toolbar({ ed }) {
|