@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 +31 -0
- package/README.md +46 -0
- package/dist/{chunk-BVMSHUQF.js → chunk-G7ULXTKI.js} +266 -33
- package/dist/chunk-G7ULXTKI.js.map +1 -0
- package/dist/editor.cjs +308 -75
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.d.cts +1 -1
- package/dist/editor.d.ts +1 -1
- package/dist/editor.js +2 -2
- package/dist/index.cjs +263 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{useEditor-B2kVZJA0.d.cts → useEditor-fIQEaKzZ.d.cts} +54 -2
- package/dist/{useEditor-BQSictH3.d.ts → useEditor-wGEqxpJT.d.ts} +54 -2
- package/package.json +2 -2
- package/dist/chunk-BVMSHUQF.js.map +0 -1
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
|
|
254
|
-
const
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
433
|
-
const
|
|
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 (
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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__ */
|
|
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-
|
|
580
|
-
//# sourceMappingURL=chunk-
|
|
812
|
+
//# sourceMappingURL=chunk-G7ULXTKI.js.map
|
|
813
|
+
//# sourceMappingURL=chunk-G7ULXTKI.js.map
|