@particle-academy/fancy-cms-ui 0.9.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 CHANGED
@@ -14,6 +14,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.10.0] - 2026-10-23
18
+
19
+ ### Added
20
+
21
+ - `Editor` accepts controlled `value`, native-operation `onOp` and `disabled`
22
+ props for server-owned shared documents. Local edits propose changes; only
23
+ the next host value changes the controlled document. Incoming values preserve
24
+ the selected handle and never echo through either callback. Local snapshot
25
+ Undo/Redo is disabled in controlled mode to protect other writers' work.
26
+ - Inspector fields keep a local draft, so typing survives server latency,
27
+ including while `disabled` pauses structural edits and submissions. Drafts
28
+ commit after 250 ms idle or on blur, one acknowledgement at a time, and
29
+ wait for IME composition to finish.
30
+ - When another writer changes the field being drafted, a save is rejected or
31
+ never acknowledged, or the node is removed, the draft is kept and the
32
+ inspector offers **Use server value** / **Keep draft**. Text is never
33
+ silently lost or overwritten.
34
+ - The host contract (compare-and-set, serialization, pending, unmount) is in
35
+ the README. Nothing changes for an uncontrolled `Editor`.
36
+
37
+ ### Fixed
38
+
39
+ - An uncontrolled `Editor` mounted under React StrictMode no longer emits an
40
+ initial `onChange` before any edit.
41
+
42
+ ### Security
43
+
44
+ - **vitest 4.1.12 in development** (was 4.1.11), clearing GHSA-qxg6-89mg-rqv8
45
+ and GHSA-xp26-3454-mjgp in `@vitest/mocker`. Test tooling only: nothing in
46
+ the published package changes, so **nothing to do** as a consumer.
47
+
17
48
  ## [0.9.0] - 2026-10-23
18
49
 
19
50
  Released together with `particle-academy/fancy-cms` 0.5.0, whose PHP renderer
package/README.md CHANGED
@@ -72,6 +72,52 @@ Lean subpath entries also exist: `@particle-academy/fancy-cms-ui/react`
72
72
  the inline `EditablePage` — the one component that additionally requires the
73
73
  optional `@particle-academy/react-fancy` peer).
74
74
 
75
+ ### A server-owned document
76
+
77
+ Use `value` when a server or another writer owns the document. The canvas shows
78
+ accepted host state. Inspector fields keep local drafts so typing continues
79
+ without reverting while a save is pending. A field proposes a native `PageOp`
80
+ through `onOp` and a reduced document through `onChange` after 250 ms without
81
+ typing, or on blur. IME composition finishes before submission. Choose one
82
+ callback to submit writes; the other can observe proposals. Incoming values do
83
+ not echo themselves through these callbacks and preserve the selected handle.
84
+
85
+ ```tsx
86
+ import { Editor, type PageDoc, type PageOp } from "@particle-academy/fancy-cms-ui";
87
+
88
+ function SharedAuthor({ doc, pending, submit }: {
89
+ doc: PageDoc;
90
+ pending: boolean;
91
+ submit: (op: PageOp) => void;
92
+ }) {
93
+ return <Editor value={doc} onOp={submit} disabled={pending} />;
94
+ }
95
+ ```
96
+
97
+ The host validates and authorizes each operation, pins it to the current server
98
+ revision, and supplies accepted state through `value`. Keep `pending` true while
99
+ a write is being accepted, and clear it after publishing the accepted or
100
+ rejected server state. `disabled` blocks structural edits and draft submission;
101
+ controlled inspector fields remain editable. Additional typing stays buffered
102
+ until the prior field value is acknowledged. Inspector proposals are serialized
103
+ even if the host omits `disabled`; the host must still serialize and revision-pin
104
+ all writes. An operation-only host can omit `onChange`.
105
+
106
+ Incoming state updates other fields immediately. If it changes a drafted field,
107
+ the draft stays visible with a conflict. Choose **Use server value** to discard
108
+ it or **Keep draft** to submit it against the current state. The editor never
109
+ silently retries a conflict. Clearing `pending` without accepting the field,
110
+ or receiving no acknowledgement for 10 seconds, also retains the draft and
111
+ surfaces a decision. Node removal retains copyable draft text without recreating
112
+ the node. Keep this editor mounted while drafts or writes are outstanding;
113
+ navigation/unmount is owned by the host.
114
+
115
+ Controlled editors disable local snapshot Undo/Redo, since restoring an old
116
+ snapshot could overwrite another writer. The `defaultValue` mode retains local
117
+ history and reports document changes through `onChange`. Choose one ownership
118
+ mode for an editor's lifetime. In uncontrolled mode, `disabled` blocks property
119
+ edits as well.
120
+
75
121
  ### Styling
76
122
 
77
123
  There is **no stylesheet to import**. Authored page styles are compiled by the
@@ -1,5 +1,5 @@
1
1
  import { migrateDoc, reduce, CmsPage, rootIds, childrenOf, lastRootId, emptyDoc } from './chunk-IRG5IKKO.js';
2
- import { useReducer, useCallback, useMemo, useRef, useState, useLayoutEffect, Fragment, useId, useEffect } from 'react';
2
+ import { useReducer, useCallback, useMemo, useRef, useState, useLayoutEffect, Fragment, useId, useEffect, useSyncExternalStore } from 'react';
3
3
  import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
4
4
  import { fractionalKey } from '@particle-academy/fancy-doc-commons';
5
5
 
@@ -243,16 +243,42 @@ var input = {
243
243
  font: "inherit",
244
244
  fontSize: 13
245
245
  };
246
- function Inspector({ doc, selection, apply }) {
246
+ function Inspector({ doc, selection, apply, drafts }) {
247
247
  const uid = useId();
248
248
  const fieldId = (key) => `${uid}-${key}`;
249
249
  const node = selection ? doc.nodes[selection] : null;
250
250
  if (!node) {
251
251
  return /* @__PURE__ */ jsx("div", { style: { padding: 16, color: "var(--fcms-muted)", fontFamily: "system-ui, sans-serif", fontSize: 13 }, children: "Select an element to edit." });
252
252
  }
253
- const s = node.style.base;
254
- const setStyle = (patch) => apply({ t: "set_style", id: node.id, breakpoint: "base", patch });
255
- const setProp = (key, value) => apply({ t: "set_node_props", id: node.id, patch: { [key]: value } });
253
+ const nodeId = node.id;
254
+ const bind = (name, read, op, numeric = false) => {
255
+ const key = JSON.stringify([nodeId, name]);
256
+ const field = {
257
+ key,
258
+ label: `${nodeId} / ${name}`,
259
+ op,
260
+ read: (next) => {
261
+ const item = next.nodes[nodeId];
262
+ return item ? read(item) : void 0;
263
+ },
264
+ canonical: numeric ? (raw) => String(Number(raw) || 0) : (raw) => raw
265
+ };
266
+ const accepted = read(node);
267
+ return {
268
+ value: drafts ? drafts.value(key, accepted) : accepted,
269
+ onChange: (event) => {
270
+ if (drafts) drafts.change(field, event.target.value);
271
+ else apply(op(event.target.value));
272
+ },
273
+ onFocus: () => drafts?.focus(key),
274
+ onBlur: () => drafts?.blur(key),
275
+ onCompositionStart: () => drafts?.composition(key, true),
276
+ onCompositionEnd: () => drafts?.composition(key, false)
277
+ };
278
+ };
279
+ const prop = (key) => bind(key, (item) => typeof item.props[key] === "string" ? item.props[key] : "", (raw) => ({ t: "set_node_props", id: nodeId, patch: { [key]: raw } }));
280
+ const style = (key) => bind(key, (item) => item.style.base[key] ?? "", (raw) => ({ t: "set_style", id: nodeId, breakpoint: "base", patch: { [key]: raw } }));
281
+ 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);
256
282
  return /* @__PURE__ */ jsxs("div", { style: { overflow: "auto", padding: 16, fontFamily: "system-ui, sans-serif", height: "100%", boxSizing: "border-box" }, children: [
257
283
  /* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "var(--fcms-fg)" }, children: node.type }),
258
284
  /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "var(--fcms-muted)", marginBottom: 8 }, children: node.id }),
@@ -264,8 +290,7 @@ function Inspector({ doc, selection, apply }) {
264
290
  id: fieldId("content"),
265
291
  "data-cms-field": "content",
266
292
  style: { ...input, minHeight: 64, resize: "vertical" },
267
- value: typeof node.props.content === "string" ? node.props.content : "",
268
- onChange: (e) => setProp("content", e.target.value)
293
+ ...prop("content")
269
294
  }
270
295
  )
271
296
  ] }) : null,
@@ -277,8 +302,7 @@ function Inspector({ doc, selection, apply }) {
277
302
  id: fieldId("src"),
278
303
  "data-cms-field": "src",
279
304
  style: input,
280
- value: typeof node.props.src === "string" ? node.props.src : "",
281
- onChange: (e) => setProp("src", e.target.value)
305
+ ...prop("src")
282
306
  }
283
307
  )
284
308
  ] }) : null,
@@ -290,8 +314,7 @@ function Inspector({ doc, selection, apply }) {
290
314
  "data-cms-field": "background",
291
315
  style: input,
292
316
  placeholder: "#ffffff | gradient | url(\u2026)",
293
- value: s.background ?? "",
294
- onChange: (e) => setStyle({ background: e.target.value })
317
+ ...style("background")
295
318
  }
296
319
  ),
297
320
  /* @__PURE__ */ jsx("label", { style: label, htmlFor: fieldId("color"), children: "Text color" }),
@@ -302,8 +325,7 @@ function Inspector({ doc, selection, apply }) {
302
325
  "data-cms-field": "color",
303
326
  style: input,
304
327
  placeholder: "#0f172a",
305
- value: s.color ?? "",
306
- onChange: (e) => setStyle({ color: e.target.value })
328
+ ...style("color")
307
329
  }
308
330
  ),
309
331
  /* @__PURE__ */ jsx("label", { style: label, htmlFor: fieldId("fontSize"), children: "Font size (px)" }),
@@ -314,8 +336,7 @@ function Inspector({ doc, selection, apply }) {
314
336
  "data-cms-field": "fontSize",
315
337
  type: "number",
316
338
  style: input,
317
- value: s.fontSize?.value ?? "",
318
- onChange: (e) => setStyle({ fontSize: { value: Number(e.target.value) || 0, unit: "px" } })
339
+ ...length("fontSize")
319
340
  }
320
341
  ),
321
342
  /* @__PURE__ */ jsx("label", { style: label, htmlFor: fieldId("padding"), children: "Padding (px)" }),
@@ -326,8 +347,7 @@ function Inspector({ doc, selection, apply }) {
326
347
  "data-cms-field": "padding",
327
348
  type: "number",
328
349
  style: input,
329
- value: s.padding?.value ?? "",
330
- onChange: (e) => setStyle({ padding: { value: Number(e.target.value) || 0, unit: "px" } })
350
+ ...length("padding")
331
351
  }
332
352
  ),
333
353
  /* @__PURE__ */ jsx("label", { style: label, htmlFor: fieldId("opacity"), children: "Opacity" }),
@@ -341,8 +361,7 @@ function Inspector({ doc, selection, apply }) {
341
361
  min: "0",
342
362
  max: "1",
343
363
  style: input,
344
- value: s.opacity ?? "",
345
- onChange: (e) => setStyle({ opacity: Number(e.target.value) })
364
+ ...bind("opacity", (item) => item.style.base.opacity?.toString() ?? "", (raw) => ({ t: "set_style", id: nodeId, breakpoint: "base", patch: { opacity: Number(raw) } }), true)
346
365
  }
347
366
  )
348
367
  ] });
@@ -427,18 +446,219 @@ function useLatestRef(value) {
427
446
  });
428
447
  return ref;
429
448
  }
430
- function Editor({ defaultValue, onChange, registry, data }) {
449
+
450
+ // src/editor/InspectorDrafts.ts
451
+ var InspectorDrafts = class {
452
+ constructor(doc) {
453
+ this.doc = doc;
454
+ this.drafts = /* @__PURE__ */ new Map();
455
+ this.composing = /* @__PURE__ */ new Set();
456
+ this.focused = null;
457
+ this.flight = null;
458
+ this.listeners = /* @__PURE__ */ new Set();
459
+ this.version = 0;
460
+ this.disabled = false;
461
+ this.emit = () => false;
462
+ this.subscribe = (listener) => {
463
+ this.listeners.add(listener);
464
+ return () => {
465
+ this.listeners.delete(listener);
466
+ };
467
+ };
468
+ this.snapshot = () => this.version;
469
+ }
470
+ get busy() {
471
+ return this.flight !== null || [...this.drafts.values()].some((draft) => draft.dirty || draft.conflict !== null);
472
+ }
473
+ get conflicts() {
474
+ return [...this.drafts.values()].filter((draft) => draft.conflict !== null).map((draft) => ({
475
+ key: draft.field.key,
476
+ label: draft.field.label,
477
+ message: draft.conflict,
478
+ missing: draft.field.read(this.doc) === void 0
479
+ }));
480
+ }
481
+ value(key, accepted) {
482
+ return this.drafts.get(key)?.raw ?? accepted;
483
+ }
484
+ configure(doc, disabled, emit) {
485
+ const settled = this.disabled && !disabled;
486
+ this.doc = doc;
487
+ this.disabled = disabled;
488
+ this.emit = emit;
489
+ let changed = false;
490
+ for (const [key, draft] of this.drafts) {
491
+ const accepted = draft.field.read(doc);
492
+ if (this.flight?.draft === draft && accepted === this.flight.expected) {
493
+ this.clearFlight();
494
+ draft.base = accepted;
495
+ draft.dirty = draft.field.canonical(draft.raw) !== accepted;
496
+ if (!draft.dirty && !draft.focused) this.drafts.delete(key);
497
+ changed = true;
498
+ } else if (accepted !== draft.base) {
499
+ this.conflict(draft, accepted === void 0 ? "The edited element was removed. Your draft is retained." : "The server changed this field. Your draft is retained.");
500
+ draft.base = accepted;
501
+ changed = true;
502
+ } else if (settled && this.flight?.draft === draft) {
503
+ this.conflict(draft, "The save was not accepted. Your draft is retained.");
504
+ changed = true;
505
+ }
506
+ }
507
+ if (changed) this.notify();
508
+ this.arm();
509
+ }
510
+ focus(key) {
511
+ this.focused = key;
512
+ const draft = this.drafts.get(key);
513
+ if (draft) draft.focused = true;
514
+ }
515
+ change(field, raw) {
516
+ let draft = this.drafts.get(field.key);
517
+ if (!draft) {
518
+ draft = { field, raw, base: field.read(this.doc), focused: this.focused === field.key, dirty: true, conflict: null, readyAt: Date.now() + 250 };
519
+ this.drafts.set(field.key, draft);
520
+ } else {
521
+ draft.field = field;
522
+ draft.raw = raw;
523
+ draft.readyAt = Date.now() + 250;
524
+ }
525
+ draft.dirty = field.canonical(raw) !== draft.base;
526
+ this.notify();
527
+ this.arm();
528
+ }
529
+ blur(key) {
530
+ if (this.focused === key) this.focused = null;
531
+ const draft = this.drafts.get(key);
532
+ if (draft) {
533
+ draft.focused = false;
534
+ draft.readyAt = Date.now();
535
+ if (!draft.dirty && !draft.conflict && this.flight?.draft !== draft) this.drafts.delete(key);
536
+ this.notify();
537
+ this.pump();
538
+ }
539
+ }
540
+ composition(key, active) {
541
+ if (active) this.composing.add(key);
542
+ else this.composing.delete(key);
543
+ const draft = this.drafts.get(key);
544
+ if (draft) draft.readyAt = Date.now() + 250;
545
+ this.arm();
546
+ }
547
+ resolve(key, keep) {
548
+ if (this.disabled) return;
549
+ const draft = this.drafts.get(key);
550
+ if (!draft) return;
551
+ if (this.flight?.draft === draft) this.clearFlight();
552
+ if (!keep) this.drafts.delete(key);
553
+ else {
554
+ const accepted = draft.field.read(this.doc);
555
+ if (accepted === void 0) return;
556
+ draft.base = accepted;
557
+ draft.conflict = null;
558
+ draft.dirty = draft.field.canonical(draft.raw) !== accepted;
559
+ draft.readyAt = Date.now();
560
+ if (!draft.dirty && !draft.focused) this.drafts.delete(key);
561
+ }
562
+ this.notify();
563
+ this.pump();
564
+ }
565
+ dispose() {
566
+ clearTimeout(this.timer);
567
+ clearTimeout(this.ackTimer);
568
+ }
569
+ notify() {
570
+ this.version++;
571
+ for (const listener of this.listeners) listener();
572
+ }
573
+ clearFlight() {
574
+ this.flight = null;
575
+ clearTimeout(this.ackTimer);
576
+ this.ackTimer = void 0;
577
+ }
578
+ conflict(draft, message) {
579
+ draft.conflict = message;
580
+ if (this.flight?.draft === draft) this.clearFlight();
581
+ }
582
+ arm() {
583
+ clearTimeout(this.timer);
584
+ this.timer = void 0;
585
+ if (this.disabled || this.flight || this.conflicts.length > 0) return;
586
+ const ready = [...this.drafts.values()].filter((draft) => draft.dirty && !this.composing.has(draft.field.key));
587
+ if (ready.length) this.timer = setTimeout(() => this.pump(), Math.max(0, Math.min(...ready.map((draft) => draft.readyAt)) - Date.now()));
588
+ }
589
+ pump() {
590
+ if (this.disabled || this.flight || this.conflicts.length > 0) return;
591
+ const draft = [...this.drafts.values()].find((item) => item.dirty && item.readyAt <= Date.now() && !this.composing.has(item.field.key));
592
+ if (!draft) {
593
+ this.arm();
594
+ return;
595
+ }
596
+ this.flight = { draft, expected: draft.field.canonical(draft.raw) };
597
+ this.ackTimer = setTimeout(() => {
598
+ this.conflict(draft, "The save has not been acknowledged. Your draft is retained.");
599
+ this.notify();
600
+ }, 1e4);
601
+ try {
602
+ if (!this.emit(draft.field.op(draft.raw))) this.conflict(draft, "This edit could not be submitted. Your draft is retained.");
603
+ } catch {
604
+ this.conflict(draft, "This edit could not be submitted. Your draft is retained.");
605
+ }
606
+ this.notify();
607
+ }
608
+ };
609
+ function Editor({ value, defaultValue, onChange, onOp, disabled = false, registry, data }) {
431
610
  const initial = useMemo(() => defaultValue ?? emptyDoc("untitled"), [defaultValue]);
432
- const ed = useEditor(initial);
433
- const first = useRef(true);
611
+ const local = useEditor(initial);
612
+ const controlled = value !== void 0;
613
+ const localDoc = useLatestRef(local.state.doc);
614
+ const lastNotified = useRef(local.state.doc);
615
+ const onOpRef = useLatestRef(onOp);
616
+ const [drafts] = useState(() => new InspectorDrafts(value ?? initial));
617
+ useSyncExternalStore(drafts.subscribe, drafts.snapshot, drafts.snapshot);
434
618
  const onChangeRef = useLatestRef(onChange);
435
619
  useEffect(() => {
436
- if (first.current) {
437
- first.current = false;
438
- return;
620
+ if (controlled || lastNotified.current === local.state.doc) return;
621
+ lastNotified.current = local.state.doc;
622
+ onChangeRef.current?.(local.state.doc);
623
+ }, [controlled, local.state.doc, onChangeRef]);
624
+ const apply = (op) => {
625
+ if (disabled) return false;
626
+ const source = value ?? localDoc.current;
627
+ const next = reduce(source, op);
628
+ if (next === source) return false;
629
+ if (!controlled) {
630
+ localDoc.current = next;
631
+ local.apply(op);
439
632
  }
440
- onChangeRef.current?.(ed.state.doc);
441
- }, [ed.state.doc, onChangeRef]);
633
+ onOpRef.current?.(op);
634
+ if (controlled) onChangeRef.current?.(next);
635
+ return true;
636
+ };
637
+ useEffect(() => {
638
+ if (controlled) drafts.configure(value, disabled, apply);
639
+ });
640
+ useEffect(() => () => drafts.dispose(), [drafts]);
641
+ const structuralDisabled = disabled || controlled && drafts.busy;
642
+ const ed = {
643
+ ...local,
644
+ state: { ...local.state, doc: value ?? local.state.doc },
645
+ apply: (op) => {
646
+ if (!structuralDisabled) apply(op);
647
+ },
648
+ // Snapshot history belongs to the local document. Replaying it into a
649
+ // server-owned value could overwrite another writer's accepted work.
650
+ undo: () => {
651
+ if (!controlled && !disabled) local.undo();
652
+ },
653
+ redo: () => {
654
+ if (!controlled && !disabled) local.redo();
655
+ },
656
+ replace: (next) => {
657
+ if (!controlled && !disabled) local.replace(next);
658
+ },
659
+ canUndo: !controlled && !disabled && local.canUndo,
660
+ canRedo: !controlled && !disabled && local.canRedo
661
+ };
442
662
  const shell = {
443
663
  display: "grid",
444
664
  gridTemplateColumns: "220px 1fr 300px",
@@ -450,9 +670,9 @@ function Editor({ defaultValue, onChange, registry, data }) {
450
670
  borderRadius: 12,
451
671
  overflow: "hidden"
452
672
  };
453
- return /* @__PURE__ */ jsxs("div", { className: "fancy-cms-editor", style: shell, children: [
673
+ return /* @__PURE__ */ jsxs("div", { className: "fancy-cms-editor", style: shell, "aria-busy": structuralDisabled || void 0, children: [
454
674
  /* @__PURE__ */ jsx("style", { children: CHROME_CSS }),
455
- /* @__PURE__ */ jsxs("div", { style: { borderRight: "1px solid var(--fcms-border)", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
675
+ /* @__PURE__ */ jsx("fieldset", { disabled: structuralDisabled, "aria-label": "Page structure controls", style: { display: "contents", border: 0, padding: 0, margin: 0 }, children: /* @__PURE__ */ jsxs("div", { style: { borderRight: "1px solid var(--fcms-border)", display: "flex", flexDirection: "column", minHeight: 0 }, children: [
456
676
  /* @__PURE__ */ jsx(Toolbar, { ed }),
457
677
  /* @__PURE__ */ jsx(
458
678
  LayersPanel,
@@ -463,9 +683,22 @@ function Editor({ defaultValue, onChange, registry, data }) {
463
683
  onMove: (id, parent, order) => ed.apply({ t: "move_node", id, parent, order })
464
684
  }
465
685
  )
466
- ] }),
686
+ ] }) }),
467
687
  /* @__PURE__ */ jsx(Canvas, { doc: ed.state.doc, selection: ed.state.selection, onSelect: ed.select, apply: ed.apply, registry, data }),
468
- /* @__PURE__ */ jsx("div", { style: { borderLeft: "1px solid var(--fcms-border)", minHeight: 0 }, children: /* @__PURE__ */ jsx(Inspector, { doc: ed.state.doc, selection: ed.state.selection, apply: ed.apply }) })
688
+ /* @__PURE__ */ jsxs("div", { style: { borderLeft: "1px solid var(--fcms-border)", minHeight: 0, overflow: "auto" }, children: [
689
+ controlled && drafts.conflicts.map((conflict) => /* @__PURE__ */ jsxs("div", { role: "alert", "data-cms-draft-conflict": conflict.key, style: { padding: 12, fontFamily: "system-ui,sans-serif", fontSize: 13 }, children: [
690
+ /* @__PURE__ */ jsxs("p", { children: [
691
+ conflict.label,
692
+ ": ",
693
+ conflict.message
694
+ ] }),
695
+ /* @__PURE__ */ jsx("pre", { "data-cms-retained-draft": "", style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere" }, children: drafts.value(conflict.key, "") }),
696
+ /* @__PURE__ */ jsx("button", { type: "button", disabled, onClick: () => drafts.resolve(conflict.key, false), children: "Use server value" }),
697
+ " ",
698
+ /* @__PURE__ */ jsx("button", { type: "button", disabled: disabled || conflict.missing, onClick: () => drafts.resolve(conflict.key, true), children: "Keep draft" })
699
+ ] }, conflict.key)),
700
+ /* @__PURE__ */ jsx("fieldset", { disabled: !controlled && disabled, "aria-label": "Page property controls", style: { border: 0, padding: 0, margin: 0, minWidth: 0, height: "100%" }, children: /* @__PURE__ */ jsx(Inspector, { doc: ed.state.doc, selection: ed.state.selection, apply: ed.apply, drafts: controlled ? drafts : void 0 }) })
701
+ ] })
469
702
  ] });
470
703
  }
471
704
  function Toolbar({ ed }) {
@@ -576,5 +809,5 @@ var LIGHT_VARS = "color-scheme:light;--fcms-bg:#ffffff;--fcms-fg:#0f172a;--fcms-
576
809
  var CHROME_CSS = `.fancy-cms-editor{--fcms-accent:#8b5cf6;${LIGHT_VARS}}@media (prefers-color-scheme:dark){.fancy-cms-editor{${DARK_VARS}}}:where(.light,[data-theme="light"]) .fancy-cms-editor{${LIGHT_VARS}}:where(.dark,[data-theme="dark"]) .fancy-cms-editor{${DARK_VARS}}`;
577
810
 
578
811
  export { ADD_DEFAULTS, ADD_MENU, CONTAINER_TYPES, Canvas, Editor, Inspector, LayersPanel, buildInsertOp, editorReduce, initEditor, resolveInsertParent, useEditor, useLatestRef };
579
- //# sourceMappingURL=chunk-BVMSHUQF.js.map
580
- //# sourceMappingURL=chunk-BVMSHUQF.js.map
812
+ //# sourceMappingURL=chunk-G7ULXTKI.js.map
813
+ //# sourceMappingURL=chunk-G7ULXTKI.js.map