@yurikilian/lex4 0.1.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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/dist/__tests__/setup.d.ts +1 -0
  3. package/dist/__tests__/setup.d.ts.map +1 -0
  4. package/dist/components/DocumentView.d.ts +10 -0
  5. package/dist/components/DocumentView.d.ts.map +1 -0
  6. package/dist/components/EditorSidebar.d.ts +12 -0
  7. package/dist/components/EditorSidebar.d.ts.map +1 -0
  8. package/dist/components/HeaderFooterActions.d.ts +16 -0
  9. package/dist/components/HeaderFooterActions.d.ts.map +1 -0
  10. package/dist/components/HeaderFooterToggle.d.ts +8 -0
  11. package/dist/components/HeaderFooterToggle.d.ts.map +1 -0
  12. package/dist/components/HistorySidebar.d.ts +3 -0
  13. package/dist/components/HistorySidebar.d.ts.map +1 -0
  14. package/dist/components/Lex4Editor.d.ts +10 -0
  15. package/dist/components/Lex4Editor.d.ts.map +1 -0
  16. package/dist/components/PageBody.d.ts +26 -0
  17. package/dist/components/PageBody.d.ts.map +1 -0
  18. package/dist/components/PageFooter.d.ts +18 -0
  19. package/dist/components/PageFooter.d.ts.map +1 -0
  20. package/dist/components/PageHeader.d.ts +18 -0
  21. package/dist/components/PageHeader.d.ts.map +1 -0
  22. package/dist/components/PageView.d.ts +21 -0
  23. package/dist/components/PageView.d.ts.map +1 -0
  24. package/dist/components/Toolbar.d.ts +3 -0
  25. package/dist/components/Toolbar.d.ts.map +1 -0
  26. package/dist/components/index.d.ts +12 -0
  27. package/dist/components/index.d.ts.map +1 -0
  28. package/dist/constants/dimensions.d.ts +25 -0
  29. package/dist/constants/dimensions.d.ts.map +1 -0
  30. package/dist/constants/index.d.ts +3 -0
  31. package/dist/constants/index.d.ts.map +1 -0
  32. package/dist/constants/page-layout.d.ts +11 -0
  33. package/dist/constants/page-layout.d.ts.map +1 -0
  34. package/dist/context/document-context.d.ts +108 -0
  35. package/dist/context/document-context.d.ts.map +1 -0
  36. package/dist/context/document-provider.d.ts +10 -0
  37. package/dist/context/document-provider.d.ts.map +1 -0
  38. package/dist/context/document-reducer.d.ts +8 -0
  39. package/dist/context/document-reducer.d.ts.map +1 -0
  40. package/dist/context/index.d.ts +5 -0
  41. package/dist/context/index.d.ts.map +1 -0
  42. package/dist/engine/index.d.ts +7 -0
  43. package/dist/engine/index.d.ts.map +1 -0
  44. package/dist/engine/measure.d.ts +15 -0
  45. package/dist/engine/measure.d.ts.map +1 -0
  46. package/dist/engine/overflow.d.ts +28 -0
  47. package/dist/engine/overflow.d.ts.map +1 -0
  48. package/dist/engine/paginate.d.ts +12 -0
  49. package/dist/engine/paginate.d.ts.map +1 -0
  50. package/dist/engine/reflow.d.ts +22 -0
  51. package/dist/engine/reflow.d.ts.map +1 -0
  52. package/dist/hooks/index.d.ts +4 -0
  53. package/dist/hooks/index.d.ts.map +1 -0
  54. package/dist/hooks/use-header-footer.d.ts +10 -0
  55. package/dist/hooks/use-header-footer.d.ts.map +1 -0
  56. package/dist/hooks/use-overflow-detection.d.ts +11 -0
  57. package/dist/hooks/use-overflow-detection.d.ts.map +1 -0
  58. package/dist/hooks/use-pagination.d.ts +22 -0
  59. package/dist/hooks/use-pagination.d.ts.map +1 -0
  60. package/dist/index.d.ts +9 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/lex4-editor.cjs +3774 -0
  63. package/dist/lex4-editor.cjs.map +1 -0
  64. package/dist/lex4-editor.js +3774 -0
  65. package/dist/lex4-editor.js.map +1 -0
  66. package/dist/lexical/commands/format-commands.d.ts +14 -0
  67. package/dist/lexical/commands/format-commands.d.ts.map +1 -0
  68. package/dist/lexical/commands/index.d.ts +4 -0
  69. package/dist/lexical/commands/index.d.ts.map +1 -0
  70. package/dist/lexical/commands/list-commands.d.ts +25 -0
  71. package/dist/lexical/commands/list-commands.d.ts.map +1 -0
  72. package/dist/lexical/editor-setup.d.ts +10 -0
  73. package/dist/lexical/editor-setup.d.ts.map +1 -0
  74. package/dist/lexical/index.d.ts +8 -0
  75. package/dist/lexical/index.d.ts.map +1 -0
  76. package/dist/lexical/plugins/active-editor-plugin.d.ts +16 -0
  77. package/dist/lexical/plugins/active-editor-plugin.d.ts.map +1 -0
  78. package/dist/lexical/plugins/font-plugin.d.ts +19 -0
  79. package/dist/lexical/plugins/font-plugin.d.ts.map +1 -0
  80. package/dist/lexical/plugins/height-limit-plugin.d.ts +15 -0
  81. package/dist/lexical/plugins/height-limit-plugin.d.ts.map +1 -0
  82. package/dist/lexical/plugins/history-capture-plugin.d.ts +8 -0
  83. package/dist/lexical/plugins/history-capture-plugin.d.ts.map +1 -0
  84. package/dist/lexical/plugins/index.d.ts +9 -0
  85. package/dist/lexical/plugins/index.d.ts.map +1 -0
  86. package/dist/lexical/plugins/overflow-plugin.d.ts +22 -0
  87. package/dist/lexical/plugins/overflow-plugin.d.ts.map +1 -0
  88. package/dist/lexical/plugins/page-boundary-plugin.d.ts +9 -0
  89. package/dist/lexical/plugins/page-boundary-plugin.d.ts.map +1 -0
  90. package/dist/lexical/plugins/paragraph-indent-plugin.d.ts +7 -0
  91. package/dist/lexical/plugins/paragraph-indent-plugin.d.ts.map +1 -0
  92. package/dist/lexical/plugins/paste-plugin.d.ts +9 -0
  93. package/dist/lexical/plugins/paste-plugin.d.ts.map +1 -0
  94. package/dist/lexical/plugins/tab-indent-plugin.d.ts +9 -0
  95. package/dist/lexical/plugins/tab-indent-plugin.d.ts.map +1 -0
  96. package/dist/lexical/theme.d.ts +7 -0
  97. package/dist/lexical/theme.d.ts.map +1 -0
  98. package/dist/style.css +1065 -0
  99. package/dist/types/document.d.ts +40 -0
  100. package/dist/types/document.d.ts.map +1 -0
  101. package/dist/types/editor-props.d.ts +19 -0
  102. package/dist/types/editor-props.d.ts.map +1 -0
  103. package/dist/types/history.d.ts +43 -0
  104. package/dist/types/history.d.ts.map +1 -0
  105. package/dist/types/index.d.ts +5 -0
  106. package/dist/types/index.d.ts.map +1 -0
  107. package/dist/types/page.d.ts +26 -0
  108. package/dist/types/page.d.ts.map +1 -0
  109. package/dist/utils/debug.d.ts +41 -0
  110. package/dist/utils/debug.d.ts.map +1 -0
  111. package/dist/utils/editor-state-utils.d.ts +48 -0
  112. package/dist/utils/editor-state-utils.d.ts.map +1 -0
  113. package/dist/utils/history-manager.d.ts +26 -0
  114. package/dist/utils/history-manager.d.ts.map +1 -0
  115. package/dist/utils/index.d.ts +3 -0
  116. package/dist/utils/index.d.ts.map +1 -0
  117. package/package.json +80 -0
@@ -0,0 +1,3774 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const lexical = require("lexical");
6
+ const LexicalComposerContext = require("@lexical/react/LexicalComposerContext");
7
+ const list = require("@lexical/list");
8
+ const LexicalComposer = require("@lexical/react/LexicalComposer");
9
+ const LexicalRichTextPlugin = require("@lexical/react/LexicalRichTextPlugin");
10
+ const LexicalContentEditable = require("@lexical/react/LexicalContentEditable");
11
+ const LexicalListPlugin = require("@lexical/react/LexicalListPlugin");
12
+ const LexicalErrorBoundary = require("@lexical/react/LexicalErrorBoundary");
13
+ const LexicalOnChangePlugin = require("@lexical/react/LexicalOnChangePlugin");
14
+ const richText = require("@lexical/rich-text");
15
+ function createEmptyPage(id) {
16
+ return {
17
+ id: id ?? crypto.randomUUID(),
18
+ bodyState: null,
19
+ headerState: null,
20
+ footerState: null,
21
+ headerHeight: 0,
22
+ footerHeight: 0,
23
+ bodySyncVersion: 0,
24
+ headerSyncVersion: 0,
25
+ footerSyncVersion: 0
26
+ };
27
+ }
28
+ function createPageFromTemplate(template) {
29
+ return {
30
+ ...createEmptyPage(),
31
+ headerState: (template == null ? void 0 : template.headerState) ?? null,
32
+ footerState: (template == null ? void 0 : template.footerState) ?? null,
33
+ headerHeight: (template == null ? void 0 : template.headerHeight) ?? 0,
34
+ footerHeight: (template == null ? void 0 : template.footerHeight) ?? 0
35
+ };
36
+ }
37
+ function createEmptyDocument() {
38
+ return {
39
+ pages: [createEmptyPage()],
40
+ headerFooterEnabled: false,
41
+ pageCounterMode: "none",
42
+ defaultHeaderState: null,
43
+ defaultFooterState: null,
44
+ defaultHeaderHeight: 0,
45
+ defaultFooterHeight: 0
46
+ };
47
+ }
48
+ const DocumentContext = React.createContext(null);
49
+ function useDocument() {
50
+ const ctx = React.useContext(DocumentContext);
51
+ if (!ctx) {
52
+ throw new Error("useDocument must be used within a DocumentProvider");
53
+ }
54
+ return ctx;
55
+ }
56
+ const A4_WIDTH_MM = 210;
57
+ const A4_HEIGHT_MM = 297;
58
+ const PX_PER_MM = 96 / 25.4;
59
+ const A4_WIDTH_PX = Math.round(A4_WIDTH_MM * PX_PER_MM);
60
+ const A4_HEIGHT_PX = Math.round(A4_HEIGHT_MM * PX_PER_MM);
61
+ const PAGE_MARGIN_PX = 40;
62
+ const MAX_HEADER_RATIO = 0.2;
63
+ const MAX_FOOTER_RATIO = 0.2;
64
+ const MAX_HEADER_HEIGHT_PX = Math.round(A4_HEIGHT_PX * MAX_HEADER_RATIO);
65
+ const MAX_FOOTER_HEIGHT_PX = Math.round(A4_HEIGHT_PX * MAX_FOOTER_RATIO);
66
+ const CHANNEL_COLORS = {
67
+ reducer: "#e91e63",
68
+ overflow: "#ff5722",
69
+ registry: "#9c27b0",
70
+ page: "#2196f3",
71
+ toolbar: "#4caf50",
72
+ header: "#ff9800",
73
+ footer: "#795548",
74
+ focus: "#607d8b"
75
+ };
76
+ function getFlag() {
77
+ if (typeof window === "undefined") return null;
78
+ const win = window.__LEX4_DEBUG__;
79
+ if (win !== void 0 && win !== null) return win;
80
+ if (typeof localStorage !== "undefined") {
81
+ return localStorage.getItem("lex4:debug");
82
+ }
83
+ return null;
84
+ }
85
+ function isEnabled(channel) {
86
+ const flag = getFlag();
87
+ if (!flag) return false;
88
+ if (flag === true || flag === "*" || flag === "true") return true;
89
+ if (typeof flag === "string") {
90
+ return flag.split(",").some((s) => s.trim() === channel);
91
+ }
92
+ return false;
93
+ }
94
+ function debug(channel, message, ...data) {
95
+ if (!isEnabled(channel)) return;
96
+ const color = CHANNEL_COLORS[channel];
97
+ const prefix = `%c[lex4:${channel}]`;
98
+ const style = `color: ${color}; font-weight: bold;`;
99
+ if (data.length > 0) {
100
+ console.log(prefix, style, message, ...data);
101
+ } else {
102
+ console.log(prefix, style, message);
103
+ }
104
+ }
105
+ function debugWarn(channel, message, ...data) {
106
+ if (!isEnabled(channel)) return;
107
+ const color = CHANNEL_COLORS[channel];
108
+ const prefix = `%c[lex4:${channel}]`;
109
+ const style = `color: ${color}; font-weight: bold;`;
110
+ if (data.length > 0) {
111
+ console.warn(prefix, style, message, ...data);
112
+ } else {
113
+ console.warn(prefix, style, message);
114
+ }
115
+ }
116
+ function shortId(id) {
117
+ return id.substring(0, 8);
118
+ }
119
+ function serializedStateChanged(current, next) {
120
+ return JSON.stringify(current) !== JSON.stringify(next);
121
+ }
122
+ function withExternalSyncVersions(currentDocument, nextDocument) {
123
+ const currentPages = new Map(currentDocument.pages.map((page) => [page.id, page]));
124
+ return {
125
+ ...nextDocument,
126
+ pages: nextDocument.pages.map((nextPage) => {
127
+ const currentPage = currentPages.get(nextPage.id);
128
+ if (!currentPage) {
129
+ return nextPage;
130
+ }
131
+ const bodyChanged = serializedStateChanged(currentPage.bodyState, nextPage.bodyState);
132
+ const headerChanged = currentPage.headerHeight !== nextPage.headerHeight || serializedStateChanged(currentPage.headerState, nextPage.headerState);
133
+ const footerChanged = currentPage.footerHeight !== nextPage.footerHeight || serializedStateChanged(currentPage.footerState, nextPage.footerState);
134
+ return {
135
+ ...nextPage,
136
+ bodySyncVersion: bodyChanged ? currentPage.bodySyncVersion + 1 : currentPage.bodySyncVersion,
137
+ headerSyncVersion: headerChanged ? currentPage.headerSyncVersion + 1 : currentPage.headerSyncVersion,
138
+ footerSyncVersion: footerChanged ? currentPage.footerSyncVersion + 1 : currentPage.footerSyncVersion
139
+ };
140
+ })
141
+ };
142
+ }
143
+ function documentReducer(state, action) {
144
+ var _a, _b, _c, _d, _e, _f, _g;
145
+ switch (action.type) {
146
+ case "ADD_PAGE": {
147
+ const newPage = action.page ?? createEmptyPage();
148
+ const pages = [...state.pages];
149
+ const insertAt = action.afterIndex !== void 0 ? action.afterIndex + 1 : pages.length;
150
+ pages.splice(insertAt, 0, newPage);
151
+ const hasBody = !!((_d = (_c = (_b = (_a = action.page) == null ? void 0 : _a.bodyState) == null ? void 0 : _b.root) == null ? void 0 : _c.children) == null ? void 0 : _d.length);
152
+ debug("reducer", `ADD_PAGE id=${shortId(newPage.id)} at=${insertAt} withBody=${hasBody} totalPages=${pages.length}`);
153
+ return { ...state, pages };
154
+ }
155
+ case "REMOVE_PAGE": {
156
+ if (state.pages.length <= 1) {
157
+ debug("reducer", `REMOVE_PAGE id=${shortId(action.pageId)} — skipped (last page)`);
158
+ return state;
159
+ }
160
+ debug("reducer", `REMOVE_PAGE id=${shortId(action.pageId)} remaining=${state.pages.length - 1}`);
161
+ return {
162
+ ...state,
163
+ pages: state.pages.filter((p) => p.id !== action.pageId)
164
+ };
165
+ }
166
+ case "UPDATE_PAGE_BODY": {
167
+ const childCount = ((_g = (_f = (_e = action.bodyState) == null ? void 0 : _e.root) == null ? void 0 : _f.children) == null ? void 0 : _g.length) ?? 0;
168
+ debug("reducer", `UPDATE_PAGE_BODY id=${shortId(action.pageId)} children=${childCount}`);
169
+ return {
170
+ ...state,
171
+ pages: state.pages.map(
172
+ (p) => p.id === action.pageId ? { ...p, bodyState: action.bodyState } : p
173
+ )
174
+ };
175
+ }
176
+ case "UPDATE_PAGE_HEADER": {
177
+ debug("reducer", `UPDATE_PAGE_HEADER id=${shortId(action.pageId)}`);
178
+ return {
179
+ ...state,
180
+ pages: state.pages.map(
181
+ (p) => p.id === action.pageId ? { ...p, headerState: action.headerState } : p
182
+ )
183
+ };
184
+ }
185
+ case "UPDATE_PAGE_HEADER_CONTENT": {
186
+ const clamped = Math.min(action.height, MAX_HEADER_HEIGHT_PX);
187
+ debug("reducer", `UPDATE_PAGE_HEADER_CONTENT id=${shortId(action.pageId)} height=${clamped}`);
188
+ return {
189
+ ...state,
190
+ pages: state.pages.map(
191
+ (p) => p.id === action.pageId ? { ...p, headerState: action.headerState, headerHeight: clamped } : p
192
+ )
193
+ };
194
+ }
195
+ case "UPDATE_PAGE_FOOTER": {
196
+ debug("reducer", `UPDATE_PAGE_FOOTER id=${shortId(action.pageId)}`);
197
+ return {
198
+ ...state,
199
+ pages: state.pages.map(
200
+ (p) => p.id === action.pageId ? { ...p, footerState: action.footerState } : p
201
+ )
202
+ };
203
+ }
204
+ case "UPDATE_PAGE_FOOTER_CONTENT": {
205
+ const clamped = Math.min(action.height, MAX_FOOTER_HEIGHT_PX);
206
+ debug("reducer", `UPDATE_PAGE_FOOTER_CONTENT id=${shortId(action.pageId)} height=${clamped}`);
207
+ return {
208
+ ...state,
209
+ pages: state.pages.map(
210
+ (p) => p.id === action.pageId ? { ...p, footerState: action.footerState, footerHeight: clamped } : p
211
+ )
212
+ };
213
+ }
214
+ case "SET_HEADER_FOOTER_ENABLED": {
215
+ debug("reducer", `SET_HEADER_FOOTER_ENABLED enabled=${action.enabled}`);
216
+ return { ...state, headerFooterEnabled: action.enabled };
217
+ }
218
+ case "SET_PAGE_COUNTER_MODE": {
219
+ debug("reducer", `SET_PAGE_COUNTER_MODE mode=${action.mode}`);
220
+ return { ...state, pageCounterMode: action.mode };
221
+ }
222
+ case "COPY_HEADER_TO_ALL": {
223
+ debug("reducer", `COPY_HEADER_TO_ALL from=${shortId(action.sourcePageId)}`);
224
+ const source = state.pages.find((p) => p.id === action.sourcePageId);
225
+ if (!source) return state;
226
+ return {
227
+ ...state,
228
+ defaultHeaderState: source.headerState,
229
+ defaultHeaderHeight: source.headerHeight,
230
+ pages: state.pages.map((p) => ({
231
+ ...p,
232
+ headerState: source.headerState,
233
+ headerHeight: source.headerHeight,
234
+ headerSyncVersion: p.headerSyncVersion + 1
235
+ }))
236
+ };
237
+ }
238
+ case "COPY_FOOTER_TO_ALL": {
239
+ debug("reducer", `COPY_FOOTER_TO_ALL from=${shortId(action.sourcePageId)}`);
240
+ const source = state.pages.find((p) => p.id === action.sourcePageId);
241
+ if (!source) return state;
242
+ return {
243
+ ...state,
244
+ defaultFooterState: source.footerState,
245
+ defaultFooterHeight: source.footerHeight,
246
+ pages: state.pages.map((p) => ({
247
+ ...p,
248
+ footerState: source.footerState,
249
+ footerHeight: source.footerHeight,
250
+ footerSyncVersion: p.footerSyncVersion + 1
251
+ }))
252
+ };
253
+ }
254
+ case "CLEAR_HEADER": {
255
+ debug("reducer", `CLEAR_HEADER id=${shortId(action.pageId)}`);
256
+ return {
257
+ ...state,
258
+ pages: state.pages.map(
259
+ (p) => p.id === action.pageId ? { ...p, headerState: null, headerHeight: 0, headerSyncVersion: p.headerSyncVersion + 1 } : p
260
+ )
261
+ };
262
+ }
263
+ case "CLEAR_FOOTER": {
264
+ debug("reducer", `CLEAR_FOOTER id=${shortId(action.pageId)}`);
265
+ return {
266
+ ...state,
267
+ pages: state.pages.map(
268
+ (p) => p.id === action.pageId ? { ...p, footerState: null, footerHeight: 0, footerSyncVersion: p.footerSyncVersion + 1 } : p
269
+ )
270
+ };
271
+ }
272
+ case "CLEAR_ALL_HEADERS": {
273
+ debug("reducer", "CLEAR_ALL_HEADERS");
274
+ return {
275
+ ...state,
276
+ defaultHeaderState: null,
277
+ defaultHeaderHeight: 0,
278
+ pages: state.pages.map((p) => ({
279
+ ...p,
280
+ headerState: null,
281
+ headerHeight: 0,
282
+ headerSyncVersion: p.headerSyncVersion + 1
283
+ }))
284
+ };
285
+ }
286
+ case "CLEAR_ALL_FOOTERS": {
287
+ debug("reducer", "CLEAR_ALL_FOOTERS");
288
+ return {
289
+ ...state,
290
+ defaultFooterState: null,
291
+ defaultFooterHeight: 0,
292
+ pages: state.pages.map((p) => ({
293
+ ...p,
294
+ footerState: null,
295
+ footerHeight: 0,
296
+ footerSyncVersion: p.footerSyncVersion + 1
297
+ }))
298
+ };
299
+ }
300
+ case "CLEAR_DOCUMENT_CONTENT": {
301
+ debug("reducer", "CLEAR_DOCUMENT_CONTENT");
302
+ const firstPage = state.pages[0];
303
+ return {
304
+ ...state,
305
+ pages: [{
306
+ ...createEmptyPage(firstPage == null ? void 0 : firstPage.id),
307
+ headerState: (firstPage == null ? void 0 : firstPage.headerState) ?? null,
308
+ footerState: (firstPage == null ? void 0 : firstPage.footerState) ?? null,
309
+ headerHeight: (firstPage == null ? void 0 : firstPage.headerHeight) ?? 0,
310
+ footerHeight: (firstPage == null ? void 0 : firstPage.footerHeight) ?? 0,
311
+ bodySyncVersion: ((firstPage == null ? void 0 : firstPage.bodySyncVersion) ?? 0) + 1,
312
+ headerSyncVersion: (firstPage == null ? void 0 : firstPage.headerSyncVersion) ?? 0,
313
+ footerSyncVersion: (firstPage == null ? void 0 : firstPage.footerSyncVersion) ?? 0
314
+ }]
315
+ };
316
+ }
317
+ case "SET_HEADER_HEIGHT": {
318
+ const clamped = Math.min(action.height, MAX_HEADER_HEIGHT_PX);
319
+ debug("header", `SET_HEADER_HEIGHT id=${shortId(action.pageId)} raw=${action.height} clamped=${clamped}`);
320
+ return {
321
+ ...state,
322
+ pages: state.pages.map(
323
+ (p) => p.id === action.pageId ? { ...p, headerHeight: clamped } : p
324
+ )
325
+ };
326
+ }
327
+ case "SET_FOOTER_HEIGHT": {
328
+ const clamped = Math.min(action.height, MAX_FOOTER_HEIGHT_PX);
329
+ debug("footer", `SET_FOOTER_HEIGHT id=${shortId(action.pageId)} raw=${action.height} clamped=${clamped}`);
330
+ return {
331
+ ...state,
332
+ pages: state.pages.map(
333
+ (p) => p.id === action.pageId ? { ...p, footerHeight: clamped } : p
334
+ )
335
+ };
336
+ }
337
+ case "SET_DOCUMENT": {
338
+ debug("reducer", `SET_DOCUMENT pages=${action.document.pages.length}`);
339
+ return withExternalSyncVersions(state, action.document);
340
+ }
341
+ default:
342
+ return state;
343
+ }
344
+ }
345
+ const MAX_HISTORY_ACTIONS = 100;
346
+ function cloneSnapshot(document2) {
347
+ return structuredClone(document2);
348
+ }
349
+ function createHistoryEntry(document2, descriptor, undoSnapshot, caretPosition, caretSelection, undoCaretPosition, undoCaretSelection) {
350
+ return {
351
+ ...descriptor,
352
+ id: crypto.randomUUID(),
353
+ snapshot: cloneSnapshot(document2),
354
+ undoSnapshot: cloneSnapshot(undoSnapshot),
355
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
356
+ caretPosition,
357
+ caretSelection,
358
+ undoCaretPosition,
359
+ undoCaretSelection
360
+ };
361
+ }
362
+ function createHistoryState(document2, baseCaretPosition = null) {
363
+ return {
364
+ baseSnapshot: cloneSnapshot(document2),
365
+ baseCaretPosition,
366
+ entries: [],
367
+ cursor: 0
368
+ };
369
+ }
370
+ function getHistorySnapshot(history, cursor = history.cursor) {
371
+ if (cursor <= 0) {
372
+ return cloneSnapshot(history.baseSnapshot);
373
+ }
374
+ return cloneSnapshot(history.entries[cursor - 1].snapshot);
375
+ }
376
+ function clearHistoryState(document2, baseCaretPosition = null) {
377
+ return createHistoryState(document2, baseCaretPosition);
378
+ }
379
+ function recordHistoryEntry(history, nextDocument, descriptor, undoSnapshot, caretPosition, caretSelection, undoCaretPosition, undoCaretSelection) {
380
+ const entries = history.entries.slice(0, history.cursor);
381
+ entries.push(createHistoryEntry(
382
+ nextDocument,
383
+ descriptor,
384
+ undoSnapshot,
385
+ caretPosition,
386
+ caretSelection,
387
+ undoCaretPosition,
388
+ undoCaretSelection
389
+ ));
390
+ let baseSnapshot = cloneSnapshot(history.baseSnapshot);
391
+ let baseCaretPosition = history.baseCaretPosition;
392
+ let trimmedEntries = entries;
393
+ if (trimmedEntries.length > MAX_HISTORY_ACTIONS) {
394
+ baseSnapshot = cloneSnapshot(trimmedEntries[1].undoSnapshot);
395
+ baseCaretPosition = trimmedEntries[1].undoCaretPosition;
396
+ trimmedEntries = trimmedEntries.slice(1);
397
+ }
398
+ return {
399
+ baseSnapshot,
400
+ baseCaretPosition,
401
+ entries: trimmedEntries,
402
+ cursor: trimmedEntries.length
403
+ };
404
+ }
405
+ function undoHistory(history) {
406
+ if (history.cursor === 0) {
407
+ return null;
408
+ }
409
+ const cursor = history.cursor - 1;
410
+ const nextHistory = {
411
+ ...history,
412
+ cursor
413
+ };
414
+ return {
415
+ history: nextHistory,
416
+ document: cloneSnapshot(history.entries[history.cursor - 1].undoSnapshot),
417
+ caretPosition: history.entries[history.cursor - 1].undoCaretPosition,
418
+ caretSelection: history.entries[history.cursor - 1].undoCaretSelection
419
+ };
420
+ }
421
+ function redoHistory(history) {
422
+ var _a, _b;
423
+ if (history.cursor >= history.entries.length) {
424
+ return null;
425
+ }
426
+ const cursor = history.cursor + 1;
427
+ const nextHistory = {
428
+ ...history,
429
+ cursor
430
+ };
431
+ return {
432
+ history: nextHistory,
433
+ document: getHistorySnapshot(nextHistory),
434
+ caretPosition: ((_a = nextHistory.entries[cursor - 1]) == null ? void 0 : _a.caretPosition) ?? nextHistory.baseCaretPosition,
435
+ caretSelection: ((_b = nextHistory.entries[cursor - 1]) == null ? void 0 : _b.caretSelection) ?? null
436
+ };
437
+ }
438
+ function jumpToHistoryEntry(history, entryIndex) {
439
+ var _a, _b;
440
+ if (entryIndex < 0 || entryIndex >= history.entries.length) {
441
+ return null;
442
+ }
443
+ const nextHistory = {
444
+ ...history,
445
+ cursor: entryIndex + 1
446
+ };
447
+ return {
448
+ history: nextHistory,
449
+ document: getHistorySnapshot(nextHistory),
450
+ caretPosition: ((_a = nextHistory.entries[nextHistory.cursor - 1]) == null ? void 0 : _a.caretPosition) ?? nextHistory.baseCaretPosition,
451
+ caretSelection: ((_b = nextHistory.entries[nextHistory.cursor - 1]) == null ? void 0 : _b.caretSelection) ?? null
452
+ };
453
+ }
454
+ const HISTORY_RESTORE_SUPPRESSION_MS = 100;
455
+ const HISTORY_BATCH_FLUSH_MS = 16;
456
+ function cloneDocumentSnapshot(document2) {
457
+ return structuredClone(document2);
458
+ }
459
+ function captureUndoSnapshot(document2, activeEditor, caretPosition) {
460
+ const snapshot = cloneDocumentSnapshot(document2);
461
+ if (!activeEditor || !caretPosition) {
462
+ return snapshot;
463
+ }
464
+ const page = snapshot.pages.find((candidate) => candidate.id === caretPosition.pageId);
465
+ if (!page) {
466
+ return snapshot;
467
+ }
468
+ const editorState = activeEditor.getEditorState().toJSON();
469
+ if (caretPosition.region === "body") {
470
+ page.bodyState = editorState;
471
+ } else if (caretPosition.region === "header") {
472
+ page.headerState = editorState;
473
+ } else {
474
+ page.footerState = editorState;
475
+ }
476
+ return snapshot;
477
+ }
478
+ function captureCaretSelection(editor) {
479
+ if (!editor) {
480
+ return null;
481
+ }
482
+ const rootElement = editor.getRootElement();
483
+ const domSelection = window.getSelection();
484
+ const selectionInRoot = (node) => !!node && !!rootElement && (node === rootElement || rootElement.contains(node));
485
+ const getTextOffset = (node, offset) => {
486
+ if (!rootElement) {
487
+ return 0;
488
+ }
489
+ const range = document.createRange();
490
+ range.setStart(rootElement, 0);
491
+ range.setEnd(node, offset);
492
+ return range.toString().length;
493
+ };
494
+ let caretSelection = null;
495
+ editor.getEditorState().read(() => {
496
+ const selection = lexical.$createRangeSelectionFromDom(window.getSelection(), editor) ?? lexical.$getSelection();
497
+ if (!lexical.$isRangeSelection(selection)) {
498
+ return;
499
+ }
500
+ caretSelection = {
501
+ anchor: {
502
+ key: selection.anchor.key,
503
+ offset: selection.anchor.offset,
504
+ type: selection.anchor.type
505
+ },
506
+ focus: {
507
+ key: selection.focus.key,
508
+ offset: selection.focus.offset,
509
+ type: selection.focus.type
510
+ },
511
+ anchorTextOffset: domSelection && selectionInRoot(domSelection.anchorNode) ? getTextOffset(domSelection.anchorNode, domSelection.anchorOffset) : 0,
512
+ focusTextOffset: domSelection && selectionInRoot(domSelection.focusNode) ? getTextOffset(domSelection.focusNode, domSelection.focusOffset) : 0,
513
+ format: selection.format,
514
+ style: selection.style
515
+ };
516
+ });
517
+ return caretSelection;
518
+ }
519
+ function getPageNumber(document2, pageId) {
520
+ if (!pageId) {
521
+ return null;
522
+ }
523
+ const index = document2.pages.findIndex((page) => page.id === pageId);
524
+ return index >= 0 ? index + 1 : null;
525
+ }
526
+ function describeAction(action, document2) {
527
+ switch (action.type) {
528
+ case "UPDATE_PAGE_BODY": {
529
+ const pageNumber = getPageNumber(document2, action.pageId);
530
+ return {
531
+ label: pageNumber ? `Edited body - Page ${pageNumber}` : "Edited body",
532
+ source: "body",
533
+ pageId: action.pageId,
534
+ region: "body"
535
+ };
536
+ }
537
+ case "UPDATE_PAGE_HEADER":
538
+ case "UPDATE_PAGE_HEADER_CONTENT": {
539
+ const pageNumber = getPageNumber(document2, action.pageId);
540
+ return {
541
+ label: pageNumber ? `Edited header - Page ${pageNumber}` : "Edited header",
542
+ source: "header",
543
+ pageId: action.pageId,
544
+ region: "header"
545
+ };
546
+ }
547
+ case "UPDATE_PAGE_FOOTER":
548
+ case "UPDATE_PAGE_FOOTER_CONTENT": {
549
+ const pageNumber = getPageNumber(document2, action.pageId);
550
+ return {
551
+ label: pageNumber ? `Edited footer - Page ${pageNumber}` : "Edited footer",
552
+ source: "footer",
553
+ pageId: action.pageId,
554
+ region: "footer"
555
+ };
556
+ }
557
+ case "SET_HEADER_FOOTER_ENABLED":
558
+ return {
559
+ label: action.enabled ? "Enabled headers and footers" : "Disabled headers and footers",
560
+ source: "document",
561
+ region: "document"
562
+ };
563
+ case "SET_PAGE_COUNTER_MODE":
564
+ return {
565
+ label: `Page counter set to ${action.mode}`,
566
+ source: "document",
567
+ region: "document"
568
+ };
569
+ case "COPY_HEADER_TO_ALL":
570
+ return {
571
+ label: "Copied header to all pages",
572
+ source: "toolbar",
573
+ pageId: action.sourcePageId,
574
+ region: "header"
575
+ };
576
+ case "COPY_FOOTER_TO_ALL":
577
+ return {
578
+ label: "Copied footer to all pages",
579
+ source: "toolbar",
580
+ pageId: action.sourcePageId,
581
+ region: "footer"
582
+ };
583
+ case "CLEAR_HEADER": {
584
+ const pageNumber = getPageNumber(document2, action.pageId);
585
+ return {
586
+ label: pageNumber ? `Cleared header - Page ${pageNumber}` : "Cleared header",
587
+ source: "toolbar",
588
+ pageId: action.pageId,
589
+ region: "header"
590
+ };
591
+ }
592
+ case "CLEAR_FOOTER": {
593
+ const pageNumber = getPageNumber(document2, action.pageId);
594
+ return {
595
+ label: pageNumber ? `Cleared footer - Page ${pageNumber}` : "Cleared footer",
596
+ source: "toolbar",
597
+ pageId: action.pageId,
598
+ region: "footer"
599
+ };
600
+ }
601
+ case "CLEAR_ALL_HEADERS":
602
+ return {
603
+ label: "Cleared all headers",
604
+ source: "toolbar",
605
+ region: "header"
606
+ };
607
+ case "CLEAR_ALL_FOOTERS":
608
+ return {
609
+ label: "Cleared all footers",
610
+ source: "toolbar",
611
+ region: "footer"
612
+ };
613
+ case "CLEAR_DOCUMENT_CONTENT":
614
+ return {
615
+ label: "Cleared document body",
616
+ source: "document",
617
+ region: "document"
618
+ };
619
+ case "SET_HEADER_HEIGHT": {
620
+ const pageNumber = getPageNumber(document2, action.pageId);
621
+ return {
622
+ label: pageNumber ? `Resized header - Page ${pageNumber}` : "Resized header",
623
+ source: "header",
624
+ pageId: action.pageId,
625
+ region: "header"
626
+ };
627
+ }
628
+ case "SET_FOOTER_HEIGHT": {
629
+ const pageNumber = getPageNumber(document2, action.pageId);
630
+ return {
631
+ label: pageNumber ? `Resized footer - Page ${pageNumber}` : "Resized footer",
632
+ source: "footer",
633
+ pageId: action.pageId,
634
+ region: "footer"
635
+ };
636
+ }
637
+ case "ADD_PAGE":
638
+ return {
639
+ label: "Added page",
640
+ source: "overflow",
641
+ region: "document"
642
+ };
643
+ case "REMOVE_PAGE":
644
+ return {
645
+ label: "Removed page",
646
+ source: "overflow",
647
+ region: "document"
648
+ };
649
+ case "SET_DOCUMENT":
650
+ return {
651
+ label: "Document reflow",
652
+ source: "overflow",
653
+ region: "document"
654
+ };
655
+ default:
656
+ return {
657
+ label: "Updated document",
658
+ source: "document",
659
+ region: "document"
660
+ };
661
+ }
662
+ }
663
+ function documentsAreEqual(current, next) {
664
+ return JSON.stringify(current) === JSON.stringify(next);
665
+ }
666
+ const DocumentProvider = ({
667
+ initialDocument,
668
+ onDocumentChange,
669
+ children
670
+ }) => {
671
+ var _a, _b;
672
+ const initialSnapshot = initialDocument ?? createEmptyDocument();
673
+ const [document2, baseDispatch] = React.useReducer(documentReducer, initialSnapshot);
674
+ const [historyState, setHistoryState] = React.useState(() => createHistoryState(initialSnapshot));
675
+ const documentRef = React.useRef(document2);
676
+ const historyStateRef = React.useRef(historyState);
677
+ const [activePageId, setActivePageIdRaw] = React.useState(
678
+ ((_a = initialSnapshot.pages[0]) == null ? void 0 : _a.id) ?? null
679
+ );
680
+ const activePageIdRef = React.useRef(((_b = initialSnapshot.pages[0]) == null ? void 0 : _b.id) ?? null);
681
+ const [globalSelectionActive, setGlobalSelectionActive] = React.useState(false);
682
+ const [historySidebarOpen, setHistorySidebarOpen] = React.useState(true);
683
+ const [focusAtEndVersion, setFocusAtEndVersion] = React.useState(0);
684
+ const activeEditorRef = React.useRef(null);
685
+ const activeCaretPositionRef = React.useRef(null);
686
+ const pendingCaretPositionRef = React.useRef(null);
687
+ const pendingCaretSelectionRef = React.useRef(null);
688
+ const pendingFocusAtEndRef = React.useRef(null);
689
+ const [, forceUpdate] = React.useState(0);
690
+ const historySuppressedRef = React.useRef(false);
691
+ const historyReleaseTimerRef = React.useRef(null);
692
+ const historyFlushTimerRef = React.useRef(null);
693
+ const pendingHistoryActionRef = React.useRef(null);
694
+ const pendingUndoSnapshotRef = React.useRef(null);
695
+ const pendingUndoCaretPositionRef = React.useRef(null);
696
+ const pendingUndoCaretSelectionRef = React.useRef(null);
697
+ const historyBatchRef = React.useRef(null);
698
+ const editorMapRef = React.useRef(/* @__PURE__ */ new Map());
699
+ const editorRegistry = React.useMemo(() => ({
700
+ register: (pageId, editor) => {
701
+ editorMapRef.current.set(pageId, editor);
702
+ },
703
+ unregister: (pageId) => {
704
+ editorMapRef.current.delete(pageId);
705
+ },
706
+ get: (pageId) => editorMapRef.current.get(pageId),
707
+ all: () => Array.from(editorMapRef.current.values())
708
+ }), []);
709
+ const setActivePageId = React.useCallback((id) => {
710
+ activePageIdRef.current = id;
711
+ setActivePageIdRaw(id);
712
+ }, []);
713
+ const setActiveEditor = React.useCallback((editor, caretPosition = null) => {
714
+ activeEditorRef.current = editor;
715
+ activeCaretPositionRef.current = caretPosition;
716
+ forceUpdate((n) => n + 1);
717
+ }, []);
718
+ const focusEditorAtEnd = React.useCallback((editor, caretPosition) => {
719
+ requestAnimationFrame(() => {
720
+ setActivePageId(caretPosition.pageId);
721
+ setActiveEditor(editor, caretPosition);
722
+ editor.focus(() => {
723
+ editor.update(() => {
724
+ lexical.$getRoot().selectEnd();
725
+ });
726
+ });
727
+ });
728
+ }, [setActiveEditor, setActivePageId]);
729
+ const consumePendingCaretPosition = React.useCallback((caretPosition) => {
730
+ const pendingCaretPosition = pendingCaretPositionRef.current;
731
+ if (!pendingCaretPosition || pendingCaretPosition.pageId !== caretPosition.pageId || pendingCaretPosition.region !== caretPosition.region) {
732
+ return void 0;
733
+ }
734
+ pendingCaretPositionRef.current = null;
735
+ const pendingCaretSelection = pendingCaretSelectionRef.current;
736
+ pendingCaretSelectionRef.current = null;
737
+ return pendingCaretSelection;
738
+ }, []);
739
+ const consumePendingFocusAtEnd = React.useCallback((caretPosition) => {
740
+ const pendingFocusAtEnd = pendingFocusAtEndRef.current;
741
+ if (!pendingFocusAtEnd || pendingFocusAtEnd.pageId !== caretPosition.pageId || pendingFocusAtEnd.region !== caretPosition.region) {
742
+ return false;
743
+ }
744
+ pendingFocusAtEndRef.current = null;
745
+ return true;
746
+ }, []);
747
+ const requestFocusAtEnd = React.useCallback((caretPosition) => {
748
+ pendingCaretPositionRef.current = null;
749
+ pendingCaretSelectionRef.current = null;
750
+ if (caretPosition.region === "body") {
751
+ const editor = editorRegistry.get(caretPosition.pageId);
752
+ if (editor) {
753
+ pendingFocusAtEndRef.current = null;
754
+ focusEditorAtEnd(editor, caretPosition);
755
+ return;
756
+ }
757
+ }
758
+ pendingFocusAtEndRef.current = caretPosition;
759
+ setFocusAtEndVersion((version) => version + 1);
760
+ }, [editorRegistry, focusEditorAtEnd]);
761
+ const flushHistoryBatch = React.useCallback(() => {
762
+ if (historyFlushTimerRef.current) {
763
+ clearTimeout(historyFlushTimerRef.current);
764
+ historyFlushTimerRef.current = null;
765
+ }
766
+ const batch = historyBatchRef.current;
767
+ historyBatchRef.current = null;
768
+ pendingHistoryActionRef.current = null;
769
+ pendingUndoSnapshotRef.current = null;
770
+ pendingUndoCaretPositionRef.current = null;
771
+ pendingUndoCaretSelectionRef.current = null;
772
+ if (!batch || historySuppressedRef.current || !batch.hasMutation || !batch.nextDocument) {
773
+ return;
774
+ }
775
+ const undoSnapshot = batch.undoSnapshot ?? batch.nextDocument;
776
+ setHistoryState((previousHistory) => {
777
+ const nextHistory = recordHistoryEntry(
778
+ previousHistory,
779
+ batch.nextDocument,
780
+ batch.descriptor,
781
+ undoSnapshot,
782
+ activeCaretPositionRef.current,
783
+ captureCaretSelection(activeEditorRef.current),
784
+ batch.undoCaretPosition,
785
+ batch.undoCaretSelection
786
+ );
787
+ historyStateRef.current = nextHistory;
788
+ return nextHistory;
789
+ });
790
+ }, []);
791
+ const scheduleHistoryBatchFlush = React.useCallback(() => {
792
+ if (historyFlushTimerRef.current) {
793
+ clearTimeout(historyFlushTimerRef.current);
794
+ }
795
+ historyFlushTimerRef.current = setTimeout(() => {
796
+ flushHistoryBatch();
797
+ }, HISTORY_BATCH_FLUSH_MS);
798
+ }, [flushHistoryBatch]);
799
+ const queueHistoryAction = React.useCallback((action) => {
800
+ pendingHistoryActionRef.current = action;
801
+ pendingUndoSnapshotRef.current = action ? captureUndoSnapshot(
802
+ documentRef.current,
803
+ activeEditorRef.current,
804
+ activeCaretPositionRef.current
805
+ ) : null;
806
+ pendingUndoCaretPositionRef.current = action ? activeCaretPositionRef.current : null;
807
+ pendingUndoCaretSelectionRef.current = action ? captureCaretSelection(activeEditorRef.current) : null;
808
+ }, []);
809
+ const runHistoryAction = React.useCallback((action, callback) => {
810
+ if (historyBatchRef.current) {
811
+ callback();
812
+ return;
813
+ }
814
+ historyBatchRef.current = {
815
+ descriptor: action,
816
+ nextDocument: null,
817
+ undoSnapshot: captureUndoSnapshot(
818
+ documentRef.current,
819
+ activeEditorRef.current,
820
+ activeCaretPositionRef.current
821
+ ),
822
+ undoCaretPosition: activeCaretPositionRef.current,
823
+ undoCaretSelection: captureCaretSelection(activeEditorRef.current),
824
+ hasMutation: false
825
+ };
826
+ pendingHistoryActionRef.current = action;
827
+ pendingUndoSnapshotRef.current = null;
828
+ pendingUndoCaretPositionRef.current = null;
829
+ pendingUndoCaretSelectionRef.current = null;
830
+ try {
831
+ callback();
832
+ } finally {
833
+ scheduleHistoryBatchFlush();
834
+ }
835
+ }, [scheduleHistoryBatchFlush]);
836
+ const suppressHistoryTemporarily = React.useCallback(() => {
837
+ historySuppressedRef.current = true;
838
+ pendingHistoryActionRef.current = null;
839
+ pendingUndoSnapshotRef.current = null;
840
+ pendingUndoCaretPositionRef.current = null;
841
+ pendingUndoCaretSelectionRef.current = null;
842
+ historyBatchRef.current = null;
843
+ if (historyFlushTimerRef.current) {
844
+ clearTimeout(historyFlushTimerRef.current);
845
+ historyFlushTimerRef.current = null;
846
+ }
847
+ if (historyReleaseTimerRef.current) {
848
+ clearTimeout(historyReleaseTimerRef.current);
849
+ }
850
+ historyReleaseTimerRef.current = setTimeout(() => {
851
+ historySuppressedRef.current = false;
852
+ historyReleaseTimerRef.current = null;
853
+ }, HISTORY_RESTORE_SUPPRESSION_MS);
854
+ }, []);
855
+ const dispatch = React.useCallback((action) => {
856
+ const currentDocument = documentRef.current;
857
+ const nextDocument = documentReducer(currentDocument, action);
858
+ const changed = !documentsAreEqual(currentDocument, nextDocument);
859
+ const suppressPassiveBodyHistory = action.type === "UPDATE_PAGE_BODY" && action.pageId !== activePageIdRef.current && !historyBatchRef.current && !pendingHistoryActionRef.current;
860
+ documentRef.current = nextDocument;
861
+ if (changed && !historySuppressedRef.current && !suppressPassiveBodyHistory) {
862
+ if (historyBatchRef.current) {
863
+ historyBatchRef.current.nextDocument = cloneDocumentSnapshot(nextDocument);
864
+ historyBatchRef.current.hasMutation = true;
865
+ scheduleHistoryBatchFlush();
866
+ } else if (pendingHistoryActionRef.current) {
867
+ historyBatchRef.current = {
868
+ descriptor: pendingHistoryActionRef.current,
869
+ nextDocument: cloneDocumentSnapshot(nextDocument),
870
+ undoSnapshot: pendingUndoSnapshotRef.current,
871
+ undoCaretPosition: pendingUndoCaretPositionRef.current,
872
+ undoCaretSelection: pendingUndoCaretSelectionRef.current,
873
+ hasMutation: true
874
+ };
875
+ scheduleHistoryBatchFlush();
876
+ } else {
877
+ const descriptor = describeAction(action, currentDocument);
878
+ setHistoryState((previousHistory) => {
879
+ const nextHistory = recordHistoryEntry(
880
+ previousHistory,
881
+ nextDocument,
882
+ descriptor,
883
+ captureUndoSnapshot(
884
+ currentDocument,
885
+ activeEditorRef.current,
886
+ activeCaretPositionRef.current
887
+ ),
888
+ activeCaretPositionRef.current,
889
+ captureCaretSelection(activeEditorRef.current),
890
+ activeCaretPositionRef.current,
891
+ captureCaretSelection(activeEditorRef.current)
892
+ );
893
+ historyStateRef.current = nextHistory;
894
+ return nextHistory;
895
+ });
896
+ }
897
+ } else {
898
+ pendingHistoryActionRef.current = null;
899
+ pendingUndoSnapshotRef.current = null;
900
+ pendingUndoCaretPositionRef.current = null;
901
+ pendingUndoCaretSelectionRef.current = null;
902
+ }
903
+ baseDispatch(action);
904
+ }, [scheduleHistoryBatchFlush]);
905
+ const restoreDocument = React.useCallback((snapshot, caretPosition, caretSelection) => {
906
+ suppressHistoryTemporarily();
907
+ pendingCaretPositionRef.current = caretPosition;
908
+ pendingCaretSelectionRef.current = caretSelection;
909
+ activeEditorRef.current = null;
910
+ activeCaretPositionRef.current = caretPosition;
911
+ forceUpdate((n) => n + 1);
912
+ documentRef.current = cloneDocumentSnapshot(snapshot);
913
+ baseDispatch({ type: "SET_DOCUMENT", document: cloneDocumentSnapshot(snapshot) });
914
+ }, [suppressHistoryTemporarily]);
915
+ const undo = React.useCallback(() => {
916
+ flushHistoryBatch();
917
+ const result = undoHistory(historyStateRef.current);
918
+ if (!result) {
919
+ return;
920
+ }
921
+ historyStateRef.current = result.history;
922
+ setHistoryState(result.history);
923
+ restoreDocument(result.document, result.caretPosition, result.caretSelection);
924
+ }, [flushHistoryBatch, restoreDocument]);
925
+ const redo = React.useCallback(() => {
926
+ flushHistoryBatch();
927
+ const result = redoHistory(historyStateRef.current);
928
+ if (!result) {
929
+ return;
930
+ }
931
+ historyStateRef.current = result.history;
932
+ setHistoryState(result.history);
933
+ restoreDocument(result.document, result.caretPosition, result.caretSelection);
934
+ }, [flushHistoryBatch, restoreDocument]);
935
+ const clearHistory = React.useCallback(() => {
936
+ flushHistoryBatch();
937
+ const nextHistory = clearHistoryState(documentRef.current, activeCaretPositionRef.current);
938
+ historyStateRef.current = nextHistory;
939
+ setHistoryState(nextHistory);
940
+ }, [flushHistoryBatch]);
941
+ const handleJumpToHistoryEntry = React.useCallback((entryIndex) => {
942
+ flushHistoryBatch();
943
+ const result = jumpToHistoryEntry(historyStateRef.current, entryIndex);
944
+ if (!result) {
945
+ return;
946
+ }
947
+ historyStateRef.current = result.history;
948
+ setHistoryState(result.history);
949
+ restoreDocument(result.document, result.caretPosition, result.caretSelection);
950
+ }, [flushHistoryBatch, restoreDocument]);
951
+ React.useEffect(() => {
952
+ documentRef.current = document2;
953
+ onDocumentChange == null ? void 0 : onDocumentChange(document2);
954
+ }, [document2, onDocumentChange]);
955
+ React.useEffect(() => {
956
+ historyStateRef.current = historyState;
957
+ }, [historyState]);
958
+ React.useEffect(() => {
959
+ var _a2;
960
+ const firstPageId = ((_a2 = document2.pages[0]) == null ? void 0 : _a2.id) ?? null;
961
+ const activePageStillExists = activePageId !== null && document2.pages.some((page) => page.id === activePageId);
962
+ if (!activePageStillExists) {
963
+ activePageIdRef.current = firstPageId;
964
+ setActivePageIdRaw(firstPageId);
965
+ activeEditorRef.current = null;
966
+ activeCaretPositionRef.current = null;
967
+ forceUpdate((n) => n + 1);
968
+ }
969
+ }, [activePageId, document2.pages]);
970
+ React.useEffect(
971
+ () => () => {
972
+ if (historyFlushTimerRef.current) {
973
+ clearTimeout(historyFlushTimerRef.current);
974
+ }
975
+ if (historyReleaseTimerRef.current) {
976
+ clearTimeout(historyReleaseTimerRef.current);
977
+ }
978
+ },
979
+ []
980
+ );
981
+ return /* @__PURE__ */ jsxRuntime.jsx(DocumentContext.Provider, { value: {
982
+ document: document2,
983
+ dispatch,
984
+ activePageId,
985
+ setActivePageId,
986
+ activeEditor: activeEditorRef.current,
987
+ consumePendingCaretPosition,
988
+ consumePendingFocusAtEnd,
989
+ requestFocusAtEnd,
990
+ focusAtEndVersion,
991
+ setActiveEditor,
992
+ globalSelectionActive,
993
+ setGlobalSelectionActive,
994
+ historyEntries: historyState.entries,
995
+ historyCursor: historyState.cursor,
996
+ canUndo: historyState.cursor > 0,
997
+ canRedo: historyState.cursor < historyState.entries.length,
998
+ queueHistoryAction,
999
+ runHistoryAction,
1000
+ jumpToHistoryEntry: handleJumpToHistoryEntry,
1001
+ clearHistory,
1002
+ historySidebarOpen,
1003
+ setHistorySidebarOpen,
1004
+ undo,
1005
+ redo,
1006
+ editorRegistry
1007
+ }, children });
1008
+ };
1009
+ /**
1010
+ * @license lucide-react v1.8.0 - ISC
1011
+ *
1012
+ * This source code is licensed under the ISC license.
1013
+ * See the LICENSE file in the root directory of this source tree.
1014
+ */
1015
+ const mergeClasses = (...classes) => classes.filter((className, index, array) => {
1016
+ return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
1017
+ }).join(" ").trim();
1018
+ /**
1019
+ * @license lucide-react v1.8.0 - ISC
1020
+ *
1021
+ * This source code is licensed under the ISC license.
1022
+ * See the LICENSE file in the root directory of this source tree.
1023
+ */
1024
+ const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
1025
+ /**
1026
+ * @license lucide-react v1.8.0 - ISC
1027
+ *
1028
+ * This source code is licensed under the ISC license.
1029
+ * See the LICENSE file in the root directory of this source tree.
1030
+ */
1031
+ const toCamelCase = (string) => string.replace(
1032
+ /^([A-Z])|[\s-_]+(\w)/g,
1033
+ (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase()
1034
+ );
1035
+ /**
1036
+ * @license lucide-react v1.8.0 - ISC
1037
+ *
1038
+ * This source code is licensed under the ISC license.
1039
+ * See the LICENSE file in the root directory of this source tree.
1040
+ */
1041
+ const toPascalCase = (string) => {
1042
+ const camelCase = toCamelCase(string);
1043
+ return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
1044
+ };
1045
+ /**
1046
+ * @license lucide-react v1.8.0 - ISC
1047
+ *
1048
+ * This source code is licensed under the ISC license.
1049
+ * See the LICENSE file in the root directory of this source tree.
1050
+ */
1051
+ var defaultAttributes = {
1052
+ xmlns: "http://www.w3.org/2000/svg",
1053
+ width: 24,
1054
+ height: 24,
1055
+ viewBox: "0 0 24 24",
1056
+ fill: "none",
1057
+ stroke: "currentColor",
1058
+ strokeWidth: 2,
1059
+ strokeLinecap: "round",
1060
+ strokeLinejoin: "round"
1061
+ };
1062
+ /**
1063
+ * @license lucide-react v1.8.0 - ISC
1064
+ *
1065
+ * This source code is licensed under the ISC license.
1066
+ * See the LICENSE file in the root directory of this source tree.
1067
+ */
1068
+ const hasA11yProp = (props) => {
1069
+ for (const prop in props) {
1070
+ if (prop.startsWith("aria-") || prop === "role" || prop === "title") {
1071
+ return true;
1072
+ }
1073
+ }
1074
+ return false;
1075
+ };
1076
+ const LucideContext = React.createContext({});
1077
+ const useLucideContext = () => React.useContext(LucideContext);
1078
+ const Icon = React.forwardRef(
1079
+ ({ color, size, strokeWidth, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => {
1080
+ const {
1081
+ size: contextSize = 24,
1082
+ strokeWidth: contextStrokeWidth = 2,
1083
+ absoluteStrokeWidth: contextAbsoluteStrokeWidth = false,
1084
+ color: contextColor = "currentColor",
1085
+ className: contextClass = ""
1086
+ } = useLucideContext() ?? {};
1087
+ const calculatedStrokeWidth = absoluteStrokeWidth ?? contextAbsoluteStrokeWidth ? Number(strokeWidth ?? contextStrokeWidth) * 24 / Number(size ?? contextSize) : strokeWidth ?? contextStrokeWidth;
1088
+ return React.createElement(
1089
+ "svg",
1090
+ {
1091
+ ref,
1092
+ ...defaultAttributes,
1093
+ width: size ?? contextSize ?? defaultAttributes.width,
1094
+ height: size ?? contextSize ?? defaultAttributes.height,
1095
+ stroke: color ?? contextColor,
1096
+ strokeWidth: calculatedStrokeWidth,
1097
+ className: mergeClasses("lucide", contextClass, className),
1098
+ ...!children && !hasA11yProp(rest) && { "aria-hidden": "true" },
1099
+ ...rest
1100
+ },
1101
+ [
1102
+ ...iconNode.map(([tag, attrs]) => React.createElement(tag, attrs)),
1103
+ ...Array.isArray(children) ? children : [children]
1104
+ ]
1105
+ );
1106
+ }
1107
+ );
1108
+ /**
1109
+ * @license lucide-react v1.8.0 - ISC
1110
+ *
1111
+ * This source code is licensed under the ISC license.
1112
+ * See the LICENSE file in the root directory of this source tree.
1113
+ */
1114
+ const createLucideIcon = (iconName, iconNode) => {
1115
+ const Component = React.forwardRef(
1116
+ ({ className, ...props }, ref) => React.createElement(Icon, {
1117
+ ref,
1118
+ iconNode,
1119
+ className: mergeClasses(
1120
+ `lucide-${toKebabCase(toPascalCase(iconName))}`,
1121
+ `lucide-${iconName}`,
1122
+ className
1123
+ ),
1124
+ ...props
1125
+ })
1126
+ );
1127
+ Component.displayName = toPascalCase(iconName);
1128
+ return Component;
1129
+ };
1130
+ /**
1131
+ * @license lucide-react v1.8.0 - ISC
1132
+ *
1133
+ * This source code is licensed under the ISC license.
1134
+ * See the LICENSE file in the root directory of this source tree.
1135
+ */
1136
+ const __iconNode$j = [
1137
+ [
1138
+ "path",
1139
+ { d: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8", key: "mg9rjx" }
1140
+ ]
1141
+ ];
1142
+ const Bold = createLucideIcon("bold", __iconNode$j);
1143
+ /**
1144
+ * @license lucide-react v1.8.0 - ISC
1145
+ *
1146
+ * This source code is licensed under the ISC license.
1147
+ * See the LICENSE file in the root directory of this source tree.
1148
+ */
1149
+ const __iconNode$i = [
1150
+ ["line", { x1: "15", x2: "15", y1: "12", y2: "18", key: "1p7wdc" }],
1151
+ ["line", { x1: "12", x2: "18", y1: "15", y2: "15", key: "1nscbv" }],
1152
+ ["rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2", key: "17jyea" }],
1153
+ ["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2", key: "zix9uf" }]
1154
+ ];
1155
+ const CopyPlus = createLucideIcon("copy-plus", __iconNode$i);
1156
+ /**
1157
+ * @license lucide-react v1.8.0 - ISC
1158
+ *
1159
+ * This source code is licensed under the ISC license.
1160
+ * See the LICENSE file in the root directory of this source tree.
1161
+ */
1162
+ const __iconNode$h = [
1163
+ [
1164
+ "path",
1165
+ {
1166
+ d: "M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21",
1167
+ key: "g5wo59"
1168
+ }
1169
+ ],
1170
+ ["path", { d: "m5.082 11.09 8.828 8.828", key: "1wx5vj" }]
1171
+ ];
1172
+ const Eraser = createLucideIcon("eraser", __iconNode$h);
1173
+ /**
1174
+ * @license lucide-react v1.8.0 - ISC
1175
+ *
1176
+ * This source code is licensed under the ISC license.
1177
+ * See the LICENSE file in the root directory of this source tree.
1178
+ */
1179
+ const __iconNode$g = [
1180
+ ["line", { x1: "4", x2: "20", y1: "9", y2: "9", key: "4lhtct" }],
1181
+ ["line", { x1: "4", x2: "20", y1: "15", y2: "15", key: "vyu0kd" }],
1182
+ ["line", { x1: "10", x2: "8", y1: "3", y2: "21", key: "1ggp8o" }],
1183
+ ["line", { x1: "16", x2: "14", y1: "3", y2: "21", key: "weycgp" }]
1184
+ ];
1185
+ const Hash = createLucideIcon("hash", __iconNode$g);
1186
+ /**
1187
+ * @license lucide-react v1.8.0 - ISC
1188
+ *
1189
+ * This source code is licensed under the ISC license.
1190
+ * See the LICENSE file in the root directory of this source tree.
1191
+ */
1192
+ const __iconNode$f = [
1193
+ ["line", { x1: "19", x2: "10", y1: "4", y2: "4", key: "15jd3p" }],
1194
+ ["line", { x1: "14", x2: "5", y1: "20", y2: "20", key: "bu0au3" }],
1195
+ ["line", { x1: "15", x2: "9", y1: "4", y2: "20", key: "uljnxc" }]
1196
+ ];
1197
+ const Italic = createLucideIcon("italic", __iconNode$f);
1198
+ /**
1199
+ * @license lucide-react v1.8.0 - ISC
1200
+ *
1201
+ * This source code is licensed under the ISC license.
1202
+ * See the LICENSE file in the root directory of this source tree.
1203
+ */
1204
+ const __iconNode$e = [
1205
+ ["path", { d: "M21 5H11", key: "us1j55" }],
1206
+ ["path", { d: "M21 12H11", key: "wd7e0v" }],
1207
+ ["path", { d: "M21 19H11", key: "saa85w" }],
1208
+ ["path", { d: "m7 8-4 4 4 4", key: "o5hrat" }]
1209
+ ];
1210
+ const ListIndentDecrease = createLucideIcon("list-indent-decrease", __iconNode$e);
1211
+ /**
1212
+ * @license lucide-react v1.8.0 - ISC
1213
+ *
1214
+ * This source code is licensed under the ISC license.
1215
+ * See the LICENSE file in the root directory of this source tree.
1216
+ */
1217
+ const __iconNode$d = [
1218
+ ["path", { d: "M21 5H11", key: "us1j55" }],
1219
+ ["path", { d: "M21 12H11", key: "wd7e0v" }],
1220
+ ["path", { d: "M21 19H11", key: "saa85w" }],
1221
+ ["path", { d: "m3 8 4 4-4 4", key: "1a3j6y" }]
1222
+ ];
1223
+ const ListIndentIncrease = createLucideIcon("list-indent-increase", __iconNode$d);
1224
+ /**
1225
+ * @license lucide-react v1.8.0 - ISC
1226
+ *
1227
+ * This source code is licensed under the ISC license.
1228
+ * See the LICENSE file in the root directory of this source tree.
1229
+ */
1230
+ const __iconNode$c = [
1231
+ ["path", { d: "M11 5h10", key: "1cz7ny" }],
1232
+ ["path", { d: "M11 12h10", key: "1438ji" }],
1233
+ ["path", { d: "M11 19h10", key: "11t30w" }],
1234
+ ["path", { d: "M4 4h1v5", key: "10yrso" }],
1235
+ ["path", { d: "M4 9h2", key: "r1h2o0" }],
1236
+ ["path", { d: "M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02", key: "xtkcd5" }]
1237
+ ];
1238
+ const ListOrdered = createLucideIcon("list-ordered", __iconNode$c);
1239
+ /**
1240
+ * @license lucide-react v1.8.0 - ISC
1241
+ *
1242
+ * This source code is licensed under the ISC license.
1243
+ * See the LICENSE file in the root directory of this source tree.
1244
+ */
1245
+ const __iconNode$b = [
1246
+ ["path", { d: "M3 5h.01", key: "18ugdj" }],
1247
+ ["path", { d: "M3 12h.01", key: "nlz23k" }],
1248
+ ["path", { d: "M3 19h.01", key: "noohij" }],
1249
+ ["path", { d: "M8 5h13", key: "1pao27" }],
1250
+ ["path", { d: "M8 12h13", key: "1za7za" }],
1251
+ ["path", { d: "M8 19h13", key: "m83p4d" }]
1252
+ ];
1253
+ const List = createLucideIcon("list", __iconNode$b);
1254
+ /**
1255
+ * @license lucide-react v1.8.0 - ISC
1256
+ *
1257
+ * This source code is licensed under the ISC license.
1258
+ * See the LICENSE file in the root directory of this source tree.
1259
+ */
1260
+ const __iconNode$a = [
1261
+ ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }],
1262
+ ["path", { d: "M15 3v18", key: "14nvp0" }]
1263
+ ];
1264
+ const PanelRight = createLucideIcon("panel-right", __iconNode$a);
1265
+ /**
1266
+ * @license lucide-react v1.8.0 - ISC
1267
+ *
1268
+ * This source code is licensed under the ISC license.
1269
+ * See the LICENSE file in the root directory of this source tree.
1270
+ */
1271
+ const __iconNode$9 = [
1272
+ ["path", { d: "m15 14 5-5-5-5", key: "12vg1m" }],
1273
+ ["path", { d: "M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13", key: "6uklza" }]
1274
+ ];
1275
+ const Redo2 = createLucideIcon("redo-2", __iconNode$9);
1276
+ /**
1277
+ * @license lucide-react v1.8.0 - ISC
1278
+ *
1279
+ * This source code is licensed under the ISC license.
1280
+ * See the LICENSE file in the root directory of this source tree.
1281
+ */
1282
+ const __iconNode$8 = [
1283
+ ["path", { d: "M16 4H9a3 3 0 0 0-2.83 4", key: "43sutm" }],
1284
+ ["path", { d: "M14 12a4 4 0 0 1 0 8H6", key: "nlfj13" }],
1285
+ ["line", { x1: "4", x2: "20", y1: "12", y2: "12", key: "1e0a9i" }]
1286
+ ];
1287
+ const Strikethrough = createLucideIcon("strikethrough", __iconNode$8);
1288
+ /**
1289
+ * @license lucide-react v1.8.0 - ISC
1290
+ *
1291
+ * This source code is licensed under the ISC license.
1292
+ * See the LICENSE file in the root directory of this source tree.
1293
+ */
1294
+ const __iconNode$7 = [
1295
+ ["path", { d: "M21 5H3", key: "1fi0y6" }],
1296
+ ["path", { d: "M17 12H7", key: "16if0g" }],
1297
+ ["path", { d: "M19 19H5", key: "vjpgq2" }]
1298
+ ];
1299
+ const TextAlignCenter = createLucideIcon("text-align-center", __iconNode$7);
1300
+ /**
1301
+ * @license lucide-react v1.8.0 - ISC
1302
+ *
1303
+ * This source code is licensed under the ISC license.
1304
+ * See the LICENSE file in the root directory of this source tree.
1305
+ */
1306
+ const __iconNode$6 = [
1307
+ ["path", { d: "M21 5H3", key: "1fi0y6" }],
1308
+ ["path", { d: "M21 12H9", key: "dn1m92" }],
1309
+ ["path", { d: "M21 19H7", key: "4cu937" }]
1310
+ ];
1311
+ const TextAlignEnd = createLucideIcon("text-align-end", __iconNode$6);
1312
+ /**
1313
+ * @license lucide-react v1.8.0 - ISC
1314
+ *
1315
+ * This source code is licensed under the ISC license.
1316
+ * See the LICENSE file in the root directory of this source tree.
1317
+ */
1318
+ const __iconNode$5 = [
1319
+ ["path", { d: "M3 5h18", key: "1u36vt" }],
1320
+ ["path", { d: "M3 12h18", key: "1i2n21" }],
1321
+ ["path", { d: "M3 19h18", key: "awlh7x" }]
1322
+ ];
1323
+ const TextAlignJustify = createLucideIcon("text-align-justify", __iconNode$5);
1324
+ /**
1325
+ * @license lucide-react v1.8.0 - ISC
1326
+ *
1327
+ * This source code is licensed under the ISC license.
1328
+ * See the LICENSE file in the root directory of this source tree.
1329
+ */
1330
+ const __iconNode$4 = [
1331
+ ["path", { d: "M21 5H3", key: "1fi0y6" }],
1332
+ ["path", { d: "M15 12H3", key: "6jk70r" }],
1333
+ ["path", { d: "M17 19H3", key: "z6ezky" }]
1334
+ ];
1335
+ const TextAlignStart = createLucideIcon("text-align-start", __iconNode$4);
1336
+ /**
1337
+ * @license lucide-react v1.8.0 - ISC
1338
+ *
1339
+ * This source code is licensed under the ISC license.
1340
+ * See the LICENSE file in the root directory of this source tree.
1341
+ */
1342
+ const __iconNode$3 = [
1343
+ ["path", { d: "M10 11v6", key: "nco0om" }],
1344
+ ["path", { d: "M14 11v6", key: "outv1u" }],
1345
+ ["path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6", key: "miytrc" }],
1346
+ ["path", { d: "M3 6h18", key: "d0wm0j" }],
1347
+ ["path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2", key: "e791ji" }]
1348
+ ];
1349
+ const Trash2 = createLucideIcon("trash-2", __iconNode$3);
1350
+ /**
1351
+ * @license lucide-react v1.8.0 - ISC
1352
+ *
1353
+ * This source code is licensed under the ISC license.
1354
+ * See the LICENSE file in the root directory of this source tree.
1355
+ */
1356
+ const __iconNode$2 = [
1357
+ ["path", { d: "M6 4v6a6 6 0 0 0 12 0V4", key: "9kb039" }],
1358
+ ["line", { x1: "4", x2: "20", y1: "20", y2: "20", key: "nun2al" }]
1359
+ ];
1360
+ const Underline = createLucideIcon("underline", __iconNode$2);
1361
+ /**
1362
+ * @license lucide-react v1.8.0 - ISC
1363
+ *
1364
+ * This source code is licensed under the ISC license.
1365
+ * See the LICENSE file in the root directory of this source tree.
1366
+ */
1367
+ const __iconNode$1 = [
1368
+ ["path", { d: "M9 14 4 9l5-5", key: "102s5s" }],
1369
+ ["path", { d: "M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11", key: "f3b9sd" }]
1370
+ ];
1371
+ const Undo2 = createLucideIcon("undo-2", __iconNode$1);
1372
+ /**
1373
+ * @license lucide-react v1.8.0 - ISC
1374
+ *
1375
+ * This source code is licensed under the ISC license.
1376
+ * See the LICENSE file in the root directory of this source tree.
1377
+ */
1378
+ const __iconNode = [
1379
+ ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
1380
+ ["path", { d: "m6 6 12 12", key: "d8bk6v" }]
1381
+ ];
1382
+ const X = createLucideIcon("x", __iconNode);
1383
+ const EditorSidebar = ({
1384
+ title,
1385
+ subtitle,
1386
+ open,
1387
+ onClose,
1388
+ headerActions,
1389
+ children
1390
+ }) => {
1391
+ if (!open) {
1392
+ return null;
1393
+ }
1394
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1395
+ "aside",
1396
+ {
1397
+ className: "flex h-full w-[320px] shrink-0 flex-col border-l border-gray-200 bg-white",
1398
+ "data-testid": "editor-sidebar",
1399
+ children: [
1400
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between border-b border-gray-200 px-3 py-2.5", children: [
1401
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1402
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xs font-semibold text-gray-900", children: title }),
1403
+ subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: subtitle })
1404
+ ] }),
1405
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
1406
+ headerActions,
1407
+ onClose && /* @__PURE__ */ jsxRuntime.jsx(
1408
+ "button",
1409
+ {
1410
+ type: "button",
1411
+ className: "flex h-6 w-6 items-center justify-center rounded text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600",
1412
+ "data-testid": "close-editor-sidebar",
1413
+ onClick: onClose,
1414
+ "aria-label": "Close sidebar",
1415
+ children: /* @__PURE__ */ jsxRuntime.jsx(X, { size: 14 })
1416
+ }
1417
+ )
1418
+ ] })
1419
+ ] }),
1420
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-auto", children })
1421
+ ]
1422
+ }
1423
+ );
1424
+ };
1425
+ function formatTimestamp(timestamp) {
1426
+ const date = new Date(timestamp);
1427
+ return date.toLocaleTimeString([], {
1428
+ hour: "2-digit",
1429
+ minute: "2-digit",
1430
+ second: "2-digit"
1431
+ });
1432
+ }
1433
+ const HistorySidebar = () => {
1434
+ const {
1435
+ clearHistory,
1436
+ historyEntries,
1437
+ historyCursor,
1438
+ historySidebarOpen,
1439
+ jumpToHistoryEntry: jumpToHistoryEntry2,
1440
+ setHistorySidebarOpen
1441
+ } = useDocument();
1442
+ const visibleEntries = historyEntries.slice().reverse();
1443
+ const headerActions = /* @__PURE__ */ jsxRuntime.jsx(
1444
+ "button",
1445
+ {
1446
+ type: "button",
1447
+ title: "Clear History",
1448
+ "aria-label": "Clear History",
1449
+ onMouseDown: (e) => e.preventDefault(),
1450
+ onClick: () => clearHistory("manual"),
1451
+ className: "flex h-6 w-6 items-center justify-center rounded text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600",
1452
+ "data-testid": "clear-history",
1453
+ children: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { size: 13 })
1454
+ }
1455
+ );
1456
+ return /* @__PURE__ */ jsxRuntime.jsx(
1457
+ EditorSidebar,
1458
+ {
1459
+ title: "History",
1460
+ subtitle: "Word-style session history (last 100 actions)",
1461
+ open: historySidebarOpen,
1462
+ onClose: () => setHistorySidebarOpen(false),
1463
+ headerActions,
1464
+ children: visibleEntries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1465
+ "div",
1466
+ {
1467
+ className: "px-3 py-4 text-xs text-gray-500",
1468
+ "data-testid": "history-empty",
1469
+ children: "No history yet."
1470
+ }
1471
+ ) : /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "divide-y divide-gray-100", "data-testid": "history-entry-list", children: visibleEntries.map((entry, reversedIndex) => {
1472
+ const actualIndex = historyEntries.length - reversedIndex - 1;
1473
+ const isCurrent = actualIndex === historyCursor - 1;
1474
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
1475
+ "button",
1476
+ {
1477
+ type: "button",
1478
+ className: `w-full px-3 py-2 text-left transition-colors ${isCurrent ? "bg-blue-50" : "bg-white hover:bg-gray-50"}`,
1479
+ "data-testid": `history-entry-${actualIndex}`,
1480
+ "data-history-current": isCurrent ? "true" : "false",
1481
+ onClick: () => jumpToHistoryEntry2(actualIndex),
1482
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-2", children: [
1483
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
1484
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: `text-xs ${isCurrent ? "font-semibold text-blue-700" : "text-gray-900"}`, children: entry.label }),
1485
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-0.5 text-xs text-gray-400", children: entry.source })
1486
+ ] }),
1487
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0 text-xs text-gray-400", children: formatTimestamp(entry.timestamp) })
1488
+ ] })
1489
+ }
1490
+ ) }, entry.id);
1491
+ }) })
1492
+ }
1493
+ );
1494
+ };
1495
+ const HeaderFooterToggle = ({
1496
+ enabled,
1497
+ onToggle
1498
+ }) => {
1499
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1500
+ "label",
1501
+ {
1502
+ className: "flex items-center gap-1.5 cursor-pointer select-none",
1503
+ "data-testid": "header-footer-toggle",
1504
+ children: [
1505
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-gray-600", children: "Headers & Footers" }),
1506
+ /* @__PURE__ */ jsxRuntime.jsx(
1507
+ "button",
1508
+ {
1509
+ type: "button",
1510
+ role: "switch",
1511
+ "aria-checked": enabled,
1512
+ onMouseDown: (e) => e.preventDefault(),
1513
+ onClick: () => onToggle(!enabled),
1514
+ className: `
1515
+ relative inline-flex h-4 w-7 items-center rounded-full
1516
+ transition-colors duration-200
1517
+ ${enabled ? "bg-blue-500" : "bg-gray-300"}
1518
+ `,
1519
+ "data-testid": "header-footer-switch",
1520
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1521
+ "span",
1522
+ {
1523
+ className: `
1524
+ inline-block h-3 w-3 rounded-full bg-white shadow-sm
1525
+ transition-transform duration-200
1526
+ ${enabled ? "translate-x-3.5" : "translate-x-0.5"}
1527
+ `
1528
+ }
1529
+ )
1530
+ }
1531
+ )
1532
+ ]
1533
+ }
1534
+ );
1535
+ };
1536
+ const HeaderFooterActions = ({
1537
+ activePageId,
1538
+ pageCounterMode,
1539
+ onPageCounterModeChange,
1540
+ onCopyHeaderToAll,
1541
+ onCopyFooterToAll,
1542
+ onClearHeader,
1543
+ onClearFooter,
1544
+ onClearAllHeaders,
1545
+ onClearAllFooters
1546
+ }) => {
1547
+ const hasActivePage = activePageId !== null;
1548
+ const handlePageCounterModeChange = (event) => {
1549
+ const nextMode = event.target.value;
1550
+ if (nextMode === "none" || nextMode === "header" || nextMode === "footer" || nextMode === "both") {
1551
+ onPageCounterModeChange(nextMode);
1552
+ }
1553
+ };
1554
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1555
+ "div",
1556
+ {
1557
+ className: "flex flex-wrap items-center gap-2 text-xs",
1558
+ "data-testid": "header-footer-actions",
1559
+ children: [
1560
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-gray-500", htmlFor: "page-counter-mode", children: [
1561
+ /* @__PURE__ */ jsxRuntime.jsx(Hash, { size: 13, className: "text-gray-400" }),
1562
+ /* @__PURE__ */ jsxRuntime.jsxs(
1563
+ "select",
1564
+ {
1565
+ id: "page-counter-mode",
1566
+ className: "h-6 rounded border border-gray-200 bg-white px-1.5 text-xs text-gray-600\n focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400",
1567
+ "data-testid": "page-counter-mode",
1568
+ value: pageCounterMode,
1569
+ onChange: handlePageCounterModeChange,
1570
+ children: [
1571
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "none", children: "None" }),
1572
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "header", children: "Header" }),
1573
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "footer", children: "Footer" }),
1574
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "both", children: "Both" })
1575
+ ]
1576
+ }
1577
+ )
1578
+ ] }),
1579
+ /* @__PURE__ */ jsxRuntime.jsx(ActionDivider, {}),
1580
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 font-medium", children: "Header" }),
1581
+ /* @__PURE__ */ jsxRuntime.jsx(
1582
+ ActionIconButton,
1583
+ {
1584
+ title: "Copy header to all pages",
1585
+ icon: /* @__PURE__ */ jsxRuntime.jsx(CopyPlus, { size: 13 }),
1586
+ onClick: onCopyHeaderToAll,
1587
+ disabled: !hasActivePage,
1588
+ testId: "copy-header-all"
1589
+ }
1590
+ ),
1591
+ /* @__PURE__ */ jsxRuntime.jsx(
1592
+ ActionIconButton,
1593
+ {
1594
+ title: "Clear header on this page",
1595
+ icon: /* @__PURE__ */ jsxRuntime.jsx(Eraser, { size: 13 }),
1596
+ onClick: onClearHeader,
1597
+ disabled: !hasActivePage,
1598
+ testId: "clear-header"
1599
+ }
1600
+ ),
1601
+ /* @__PURE__ */ jsxRuntime.jsx(
1602
+ ActionIconButton,
1603
+ {
1604
+ title: "Clear all headers",
1605
+ icon: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { size: 13 }),
1606
+ onClick: onClearAllHeaders,
1607
+ testId: "clear-all-headers"
1608
+ }
1609
+ ),
1610
+ /* @__PURE__ */ jsxRuntime.jsx(ActionDivider, {}),
1611
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 font-medium", children: "Footer" }),
1612
+ /* @__PURE__ */ jsxRuntime.jsx(
1613
+ ActionIconButton,
1614
+ {
1615
+ title: "Copy footer to all pages",
1616
+ icon: /* @__PURE__ */ jsxRuntime.jsx(CopyPlus, { size: 13 }),
1617
+ onClick: onCopyFooterToAll,
1618
+ disabled: !hasActivePage,
1619
+ testId: "copy-footer-all"
1620
+ }
1621
+ ),
1622
+ /* @__PURE__ */ jsxRuntime.jsx(
1623
+ ActionIconButton,
1624
+ {
1625
+ title: "Clear footer on this page",
1626
+ icon: /* @__PURE__ */ jsxRuntime.jsx(Eraser, { size: 13 }),
1627
+ onClick: onClearFooter,
1628
+ disabled: !hasActivePage,
1629
+ testId: "clear-footer"
1630
+ }
1631
+ ),
1632
+ /* @__PURE__ */ jsxRuntime.jsx(
1633
+ ActionIconButton,
1634
+ {
1635
+ title: "Clear all footers",
1636
+ icon: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { size: 13 }),
1637
+ onClick: onClearAllFooters,
1638
+ testId: "clear-all-footers"
1639
+ }
1640
+ )
1641
+ ]
1642
+ }
1643
+ );
1644
+ };
1645
+ const ActionIconButton = ({ title, icon, onClick, disabled, testId }) => /* @__PURE__ */ jsxRuntime.jsx(
1646
+ "button",
1647
+ {
1648
+ type: "button",
1649
+ title,
1650
+ "aria-label": title,
1651
+ onMouseDown: (e) => e.preventDefault(),
1652
+ onClick,
1653
+ disabled,
1654
+ className: "flex h-6 w-6 items-center justify-center rounded text-gray-500 transition-colors\n hover:bg-gray-200/60 hover:text-gray-700\n disabled:cursor-not-allowed disabled:text-gray-300",
1655
+ "data-testid": testId,
1656
+ children: icon
1657
+ }
1658
+ );
1659
+ const ActionDivider = () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-0.5 h-4 w-px bg-gray-300/60" });
1660
+ const SUPPORTED_FONTS = [
1661
+ "Times New Roman",
1662
+ "Arial",
1663
+ "Calibri",
1664
+ "Georgia",
1665
+ "Courier New"
1666
+ ];
1667
+ function applyFontFamily(editor, fontFamily) {
1668
+ editor.update(() => {
1669
+ const selection = lexical.$getSelection();
1670
+ if (!lexical.$isRangeSelection(selection)) return;
1671
+ const nodes = selection.getNodes();
1672
+ for (const node of nodes) {
1673
+ if (lexical.$isTextNode(node)) {
1674
+ node.setStyle(`font-family: ${fontFamily}`);
1675
+ }
1676
+ }
1677
+ });
1678
+ }
1679
+ function toggleFormat(editor, format) {
1680
+ editor.dispatchCommand(lexical.FORMAT_TEXT_COMMAND, format);
1681
+ }
1682
+ function toggleBold(editor) {
1683
+ toggleFormat(editor, "bold");
1684
+ }
1685
+ function toggleItalic(editor) {
1686
+ toggleFormat(editor, "italic");
1687
+ }
1688
+ function toggleUnderline(editor) {
1689
+ toggleFormat(editor, "underline");
1690
+ }
1691
+ function toggleStrikethrough(editor) {
1692
+ toggleFormat(editor, "strikethrough");
1693
+ }
1694
+ function setAlignment(editor, alignment) {
1695
+ editor.dispatchCommand(lexical.FORMAT_ELEMENT_COMMAND, alignment);
1696
+ }
1697
+ function insertList(editor, type) {
1698
+ if (type === "number") {
1699
+ editor.dispatchCommand(list.INSERT_ORDERED_LIST_COMMAND, void 0);
1700
+ } else {
1701
+ editor.dispatchCommand(list.INSERT_UNORDERED_LIST_COMMAND, void 0);
1702
+ }
1703
+ }
1704
+ function indentContent(editor) {
1705
+ editor.dispatchCommand(lexical.INDENT_CONTENT_COMMAND, void 0);
1706
+ }
1707
+ function outdentContent(editor) {
1708
+ editor.dispatchCommand(lexical.OUTDENT_CONTENT_COMMAND, void 0);
1709
+ }
1710
+ const Toolbar = () => {
1711
+ const {
1712
+ document: document2,
1713
+ dispatch,
1714
+ activePageId,
1715
+ activeEditor,
1716
+ canRedo,
1717
+ canUndo,
1718
+ editorRegistry,
1719
+ globalSelectionActive,
1720
+ historySidebarOpen,
1721
+ redo,
1722
+ runHistoryAction,
1723
+ setHistorySidebarOpen,
1724
+ undo
1725
+ } = useDocument();
1726
+ const withBodySelection = React.useCallback(
1727
+ (editor, action) => {
1728
+ editor.update(() => {
1729
+ lexical.$selectAll();
1730
+ });
1731
+ action(editor);
1732
+ },
1733
+ []
1734
+ );
1735
+ const applyToBodyEditors = React.useCallback(
1736
+ (action) => {
1737
+ const targetEditors = globalSelectionActive ? editorRegistry.all() : activeEditor ? [activeEditor] : [];
1738
+ targetEditors.forEach((editor) => {
1739
+ if (globalSelectionActive) {
1740
+ withBodySelection(editor, action);
1741
+ } else {
1742
+ action(editor);
1743
+ }
1744
+ });
1745
+ },
1746
+ [activeEditor, editorRegistry, globalSelectionActive, withBodySelection]
1747
+ );
1748
+ const runToolbarAction = React.useCallback(
1749
+ (label, callback) => {
1750
+ runHistoryAction(
1751
+ {
1752
+ label,
1753
+ source: "toolbar",
1754
+ region: "document"
1755
+ },
1756
+ callback
1757
+ );
1758
+ },
1759
+ [runHistoryAction]
1760
+ );
1761
+ const handleToggle = (enabled) => {
1762
+ runToolbarAction(
1763
+ enabled ? "Enabled headers and footers" : "Disabled headers and footers",
1764
+ () => {
1765
+ dispatch({ type: "SET_HEADER_FOOTER_ENABLED", enabled });
1766
+ }
1767
+ );
1768
+ };
1769
+ const handleCopyHeaderToAll = () => {
1770
+ if (activePageId) {
1771
+ runToolbarAction("Copied header to all pages", () => {
1772
+ dispatch({ type: "COPY_HEADER_TO_ALL", sourcePageId: activePageId });
1773
+ });
1774
+ }
1775
+ };
1776
+ const handleCopyFooterToAll = () => {
1777
+ if (activePageId) {
1778
+ runToolbarAction("Copied footer to all pages", () => {
1779
+ dispatch({ type: "COPY_FOOTER_TO_ALL", sourcePageId: activePageId });
1780
+ });
1781
+ }
1782
+ };
1783
+ const handleClearHeader = () => {
1784
+ if (activePageId) {
1785
+ runToolbarAction("Cleared header", () => {
1786
+ dispatch({ type: "CLEAR_HEADER", pageId: activePageId });
1787
+ });
1788
+ }
1789
+ };
1790
+ const handleClearFooter = () => {
1791
+ if (activePageId) {
1792
+ runToolbarAction("Cleared footer", () => {
1793
+ dispatch({ type: "CLEAR_FOOTER", pageId: activePageId });
1794
+ });
1795
+ }
1796
+ };
1797
+ const handleClearAllHeaders = () => runToolbarAction("Cleared all headers", () => {
1798
+ dispatch({ type: "CLEAR_ALL_HEADERS" });
1799
+ });
1800
+ const handleClearAllFooters = () => runToolbarAction("Cleared all footers", () => {
1801
+ dispatch({ type: "CLEAR_ALL_FOOTERS" });
1802
+ });
1803
+ const handlePageCounterModeChange = React.useCallback((mode) => {
1804
+ runToolbarAction(`Page counter set to ${mode}`, () => {
1805
+ dispatch({ type: "SET_PAGE_COUNTER_MODE", mode });
1806
+ });
1807
+ }, [dispatch, runToolbarAction]);
1808
+ const handleBold = React.useCallback(() => {
1809
+ debug("toolbar", `bold (globalSelection=${globalSelectionActive}, editors=${editorRegistry.all().length}, hasEditor=${!!activeEditor})`);
1810
+ runToolbarAction("Bold applied", () => {
1811
+ applyToBodyEditors(toggleBold);
1812
+ });
1813
+ }, [activeEditor, applyToBodyEditors, editorRegistry, globalSelectionActive, runToolbarAction]);
1814
+ const handleItalic = React.useCallback(() => {
1815
+ debug("toolbar", `italic (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
1816
+ runToolbarAction("Italic applied", () => {
1817
+ applyToBodyEditors(toggleItalic);
1818
+ });
1819
+ }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction]);
1820
+ const handleUnderline = React.useCallback(() => {
1821
+ debug("toolbar", `underline (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
1822
+ runToolbarAction("Underline applied", () => {
1823
+ applyToBodyEditors(toggleUnderline);
1824
+ });
1825
+ }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction]);
1826
+ const handleStrikethrough = React.useCallback(() => {
1827
+ debug("toolbar", `strikethrough (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
1828
+ runToolbarAction("Strikethrough applied", () => {
1829
+ applyToBodyEditors(toggleStrikethrough);
1830
+ });
1831
+ }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction]);
1832
+ const handleAlignLeft = React.useCallback(() => {
1833
+ runToolbarAction("Aligned left", () => {
1834
+ applyToBodyEditors((editor) => setAlignment(editor, "left"));
1835
+ });
1836
+ }, [applyToBodyEditors, runToolbarAction]);
1837
+ const handleAlignCenter = React.useCallback(() => {
1838
+ runToolbarAction("Aligned center", () => {
1839
+ applyToBodyEditors((editor) => setAlignment(editor, "center"));
1840
+ });
1841
+ }, [applyToBodyEditors, runToolbarAction]);
1842
+ const handleAlignRight = React.useCallback(() => {
1843
+ runToolbarAction("Aligned right", () => {
1844
+ applyToBodyEditors((editor) => setAlignment(editor, "right"));
1845
+ });
1846
+ }, [applyToBodyEditors, runToolbarAction]);
1847
+ const handleAlignJustify = React.useCallback(() => {
1848
+ runToolbarAction("Justified text", () => {
1849
+ applyToBodyEditors((editor) => setAlignment(editor, "justify"));
1850
+ });
1851
+ }, [applyToBodyEditors, runToolbarAction]);
1852
+ const handleListNumber = React.useCallback(() => {
1853
+ runToolbarAction("Inserted numbered list", () => {
1854
+ applyToBodyEditors((editor) => insertList(editor, "number"));
1855
+ });
1856
+ }, [applyToBodyEditors, runToolbarAction]);
1857
+ const handleListBullet = React.useCallback(() => {
1858
+ runToolbarAction("Inserted bullet list", () => {
1859
+ applyToBodyEditors((editor) => insertList(editor, "bullet"));
1860
+ });
1861
+ }, [applyToBodyEditors, runToolbarAction]);
1862
+ const handleIndent = React.useCallback(() => {
1863
+ runToolbarAction("Indented content", () => {
1864
+ applyToBodyEditors(indentContent);
1865
+ });
1866
+ }, [applyToBodyEditors, runToolbarAction]);
1867
+ const handleOutdent = React.useCallback(() => {
1868
+ runToolbarAction("Outdented content", () => {
1869
+ applyToBodyEditors(outdentContent);
1870
+ });
1871
+ }, [applyToBodyEditors, runToolbarAction]);
1872
+ const handleFontChange = React.useCallback(
1873
+ (e) => {
1874
+ runToolbarAction(`Font changed to ${e.target.value}`, () => {
1875
+ applyToBodyEditors((editor) => applyFontFamily(editor, e.target.value));
1876
+ });
1877
+ },
1878
+ [applyToBodyEditors, runToolbarAction]
1879
+ );
1880
+ return /* @__PURE__ */ jsxRuntime.jsx(
1881
+ "div",
1882
+ {
1883
+ className: "lex4-toolbar sticky top-0 z-10 bg-white border-b border-gray-200",
1884
+ "data-testid": "toolbar",
1885
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 px-2 py-1.5", children: [
1886
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "history-controls", children: [
1887
+ /* @__PURE__ */ jsxRuntime.jsx(
1888
+ ToolbarIconButton,
1889
+ {
1890
+ title: "Undo",
1891
+ testId: "btn-undo",
1892
+ disabled: !canUndo,
1893
+ onClick: undo,
1894
+ children: /* @__PURE__ */ jsxRuntime.jsx(Undo2, { size: 15 })
1895
+ }
1896
+ ),
1897
+ /* @__PURE__ */ jsxRuntime.jsx(
1898
+ ToolbarIconButton,
1899
+ {
1900
+ title: "Redo",
1901
+ testId: "btn-redo",
1902
+ disabled: !canRedo,
1903
+ onClick: redo,
1904
+ children: /* @__PURE__ */ jsxRuntime.jsx(Redo2, { size: 15 })
1905
+ }
1906
+ )
1907
+ ] }),
1908
+ /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
1909
+ /* @__PURE__ */ jsxRuntime.jsx(
1910
+ "select",
1911
+ {
1912
+ className: "h-7 rounded border border-gray-200 bg-white px-2 text-xs text-gray-700\n focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400",
1913
+ "data-testid": "font-selector",
1914
+ defaultValue: "Arial",
1915
+ onChange: handleFontChange,
1916
+ children: SUPPORTED_FONTS.map((font) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: font, style: { fontFamily: font }, children: font }, font))
1917
+ }
1918
+ ),
1919
+ /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
1920
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "format-group", children: [
1921
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Bold (Ctrl+B)", testId: "btn-bold", onClick: handleBold, children: /* @__PURE__ */ jsxRuntime.jsx(Bold, { size: 15 }) }),
1922
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Italic (Ctrl+I)", testId: "btn-italic", onClick: handleItalic, children: /* @__PURE__ */ jsxRuntime.jsx(Italic, { size: 15 }) }),
1923
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Underline (Ctrl+U)", testId: "btn-underline", onClick: handleUnderline, children: /* @__PURE__ */ jsxRuntime.jsx(Underline, { size: 15 }) }),
1924
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Strikethrough", testId: "btn-strike", onClick: handleStrikethrough, children: /* @__PURE__ */ jsxRuntime.jsx(Strikethrough, { size: 15 }) })
1925
+ ] }),
1926
+ /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
1927
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "align-group", children: [
1928
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Align Left", testId: "btn-align-left", onClick: handleAlignLeft, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignStart, { size: 15 }) }),
1929
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Align Center", testId: "btn-align-center", onClick: handleAlignCenter, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignCenter, { size: 15 }) }),
1930
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Align Right", testId: "btn-align-right", onClick: handleAlignRight, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignEnd, { size: 15 }) }),
1931
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Justify", testId: "btn-align-justify", onClick: handleAlignJustify, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignJustify, { size: 15 }) })
1932
+ ] }),
1933
+ /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
1934
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "list-group", children: [
1935
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Numbered List", testId: "btn-list-number", onClick: handleListNumber, children: /* @__PURE__ */ jsxRuntime.jsx(ListOrdered, { size: 15 }) }),
1936
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Bullet List", testId: "btn-list-bullet", onClick: handleListBullet, children: /* @__PURE__ */ jsxRuntime.jsx(List, { size: 15 }) }),
1937
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Indent", testId: "btn-indent", onClick: handleIndent, children: /* @__PURE__ */ jsxRuntime.jsx(ListIndentIncrease, { size: 15 }) }),
1938
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: "Outdent", testId: "btn-outdent", onClick: handleOutdent, children: /* @__PURE__ */ jsxRuntime.jsx(ListIndentDecrease, { size: 15 }) })
1939
+ ] }),
1940
+ /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
1941
+ /* @__PURE__ */ jsxRuntime.jsx(
1942
+ HeaderFooterToggle,
1943
+ {
1944
+ enabled: document2.headerFooterEnabled,
1945
+ onToggle: handleToggle
1946
+ }
1947
+ ),
1948
+ document2.headerFooterEnabled && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1949
+ /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
1950
+ /* @__PURE__ */ jsxRuntime.jsx(
1951
+ HeaderFooterActions,
1952
+ {
1953
+ activePageId,
1954
+ pageCounterMode: document2.pageCounterMode,
1955
+ onPageCounterModeChange: handlePageCounterModeChange,
1956
+ onCopyHeaderToAll: handleCopyHeaderToAll,
1957
+ onCopyFooterToAll: handleCopyFooterToAll,
1958
+ onClearHeader: handleClearHeader,
1959
+ onClearFooter: handleClearFooter,
1960
+ onClearAllHeaders: handleClearAllHeaders,
1961
+ onClearAllFooters: handleClearAllFooters
1962
+ }
1963
+ )
1964
+ ] }),
1965
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-auto flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx(
1966
+ ToolbarIconButton,
1967
+ {
1968
+ title: historySidebarOpen ? "Close History" : "Open History",
1969
+ testId: "toggle-history-sidebar",
1970
+ active: historySidebarOpen,
1971
+ onClick: () => setHistorySidebarOpen(!historySidebarOpen),
1972
+ children: /* @__PURE__ */ jsxRuntime.jsx(PanelRight, { size: 15 })
1973
+ }
1974
+ ) })
1975
+ ] })
1976
+ }
1977
+ );
1978
+ };
1979
+ const ToolbarIconButton = ({
1980
+ title,
1981
+ testId,
1982
+ disabled = false,
1983
+ active = false,
1984
+ onClick,
1985
+ children
1986
+ }) => /* @__PURE__ */ jsxRuntime.jsx(
1987
+ "button",
1988
+ {
1989
+ type: "button",
1990
+ title,
1991
+ "aria-label": title,
1992
+ disabled,
1993
+ onMouseDown: (e) => e.preventDefault(),
1994
+ onClick,
1995
+ className: `
1996
+ flex h-7 w-7 items-center justify-center rounded transition-colors
1997
+ ${disabled ? "cursor-not-allowed text-gray-300" : active ? "bg-blue-50 text-blue-600" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}
1998
+ `,
1999
+ "data-testid": testId,
2000
+ children
2001
+ }
2002
+ );
2003
+ const Divider = () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-0.5 h-5 w-px bg-gray-200" });
2004
+ function computeBodyHeight(headerHeight, footerHeight) {
2005
+ const verticalMargins = PAGE_MARGIN_PX * 2;
2006
+ return A4_HEIGHT_PX - headerHeight - footerHeight - verticalMargins;
2007
+ }
2008
+ function getTopLevelNodes(state) {
2009
+ if (!state) return [];
2010
+ return state.root.children ?? [];
2011
+ }
2012
+ function createEditorStateFromNodes(nodes) {
2013
+ return {
2014
+ root: {
2015
+ children: nodes,
2016
+ direction: null,
2017
+ format: "",
2018
+ indent: 0,
2019
+ type: "root",
2020
+ version: 1
2021
+ }
2022
+ };
2023
+ }
2024
+ function splitEditorState(state, splitIndex) {
2025
+ const nodes = getTopLevelNodes(state);
2026
+ if (splitIndex <= 0) {
2027
+ return [null, state];
2028
+ }
2029
+ if (splitIndex >= nodes.length) {
2030
+ return [state, null];
2031
+ }
2032
+ const before = nodes.slice(0, splitIndex);
2033
+ const after = nodes.slice(splitIndex);
2034
+ return [
2035
+ before.length > 0 ? createEditorStateFromNodes(before) : null,
2036
+ after.length > 0 ? createEditorStateFromNodes(after) : null
2037
+ ];
2038
+ }
2039
+ function mergeEditorStates(stateA, stateB) {
2040
+ const nodesA = getTopLevelNodes(stateA);
2041
+ const nodesB = getTopLevelNodes(stateB);
2042
+ const all = [...nodesA, ...nodesB];
2043
+ if (all.length === 0) return null;
2044
+ return createEditorStateFromNodes(all);
2045
+ }
2046
+ function usePagination(document2, dispatch) {
2047
+ const reflowingRef = React.useRef(false);
2048
+ const pendingReflowRef = React.useRef(false);
2049
+ const estimateNodesFitting = React.useCallback(
2050
+ (state, bodyHeight) => {
2051
+ const nodes = getTopLevelNodes(state);
2052
+ if (nodes.length === 0) return 0;
2053
+ const estimatedLineHeight = 24;
2054
+ let usedHeight = 0;
2055
+ let fitCount = 0;
2056
+ for (const _node of nodes) {
2057
+ usedHeight += estimatedLineHeight;
2058
+ if (usedHeight > bodyHeight) break;
2059
+ fitCount++;
2060
+ }
2061
+ return Math.max(1, fitCount);
2062
+ },
2063
+ []
2064
+ );
2065
+ const handlePageOverflow = React.useCallback(
2066
+ (pageIndex) => {
2067
+ if (reflowingRef.current) {
2068
+ pendingReflowRef.current = true;
2069
+ return;
2070
+ }
2071
+ reflowingRef.current = true;
2072
+ try {
2073
+ const pages = document2.pages;
2074
+ const page = pages[pageIndex];
2075
+ if (!page) return;
2076
+ const headerH = document2.headerFooterEnabled ? page.headerHeight : 0;
2077
+ const footerH = document2.headerFooterEnabled ? page.footerHeight : 0;
2078
+ const bodyHeight = computeBodyHeight(headerH, footerH);
2079
+ const nodes = getTopLevelNodes(page.bodyState);
2080
+ if (nodes.length <= 1) return;
2081
+ const fitCount = estimateNodesFitting(page.bodyState, bodyHeight);
2082
+ if (fitCount >= nodes.length) return;
2083
+ const [keepState, overflowState] = splitEditorState(page.bodyState, fitCount);
2084
+ if (!overflowState) return;
2085
+ const nextPage = pages[pageIndex + 1];
2086
+ if (nextPage) {
2087
+ const mergedBody = mergeEditorStates(overflowState, nextPage.bodyState);
2088
+ dispatch({
2089
+ type: "SET_DOCUMENT",
2090
+ document: {
2091
+ ...document2,
2092
+ pages: pages.map((p, i) => {
2093
+ if (i === pageIndex) return { ...p, bodyState: keepState };
2094
+ if (i === pageIndex + 1) return { ...p, bodyState: mergedBody };
2095
+ return p;
2096
+ })
2097
+ }
2098
+ });
2099
+ } else {
2100
+ const newPage = {
2101
+ ...createPageFromTemplate({
2102
+ headerState: document2.defaultHeaderState,
2103
+ footerState: document2.defaultFooterState,
2104
+ headerHeight: document2.defaultHeaderHeight,
2105
+ footerHeight: document2.defaultFooterHeight
2106
+ }),
2107
+ bodyState: overflowState
2108
+ };
2109
+ dispatch({
2110
+ type: "SET_DOCUMENT",
2111
+ document: {
2112
+ ...document2,
2113
+ pages: [
2114
+ ...pages.map(
2115
+ (p, i) => i === pageIndex ? { ...p, bodyState: keepState } : p
2116
+ ),
2117
+ newPage
2118
+ ]
2119
+ }
2120
+ });
2121
+ }
2122
+ } finally {
2123
+ reflowingRef.current = false;
2124
+ if (pendingReflowRef.current) {
2125
+ pendingReflowRef.current = false;
2126
+ setTimeout(() => handlePageOverflow(pageIndex), 0);
2127
+ }
2128
+ }
2129
+ },
2130
+ [document2, dispatch, estimateNodesFitting]
2131
+ );
2132
+ const handlePageUnderflow = React.useCallback(
2133
+ (pageIndex) => {
2134
+ if (reflowingRef.current) return;
2135
+ const pages = document2.pages;
2136
+ const page = pages[pageIndex];
2137
+ const nextPage = pages[pageIndex + 1];
2138
+ if (!page || !nextPage) return;
2139
+ const nextNodes = getTopLevelNodes(nextPage.bodyState);
2140
+ if (nextNodes.length === 0) {
2141
+ if (pages.length > 1) {
2142
+ dispatch({
2143
+ type: "SET_DOCUMENT",
2144
+ document: {
2145
+ ...document2,
2146
+ pages: pages.filter((_, i) => i !== pageIndex + 1)
2147
+ }
2148
+ });
2149
+ }
2150
+ return;
2151
+ }
2152
+ const headerH = document2.headerFooterEnabled ? page.headerHeight : 0;
2153
+ const footerH = document2.headerFooterEnabled ? page.footerHeight : 0;
2154
+ const bodyHeight = computeBodyHeight(headerH, footerH);
2155
+ const currentNodes = getTopLevelNodes(page.bodyState);
2156
+ const estimatedCurrentHeight = currentNodes.length * 24;
2157
+ const remainingSpace = bodyHeight - estimatedCurrentHeight;
2158
+ if (remainingSpace > 24) {
2159
+ const [pulled, remaining] = [nextNodes.slice(0, 1), nextNodes.slice(1)];
2160
+ const updatedCurrent = mergeEditorStates(
2161
+ page.bodyState,
2162
+ createEditorStateFromNodes(pulled)
2163
+ );
2164
+ const updatedNext = remaining.length > 0 ? createEditorStateFromNodes(remaining) : null;
2165
+ const newPages = pages.map((p, i) => {
2166
+ if (i === pageIndex) return { ...p, bodyState: updatedCurrent };
2167
+ if (i === pageIndex + 1) return { ...p, bodyState: updatedNext };
2168
+ return p;
2169
+ });
2170
+ while (newPages.length > 1) {
2171
+ const last = newPages[newPages.length - 1];
2172
+ if (getTopLevelNodes(last.bodyState).length === 0) {
2173
+ newPages.pop();
2174
+ } else {
2175
+ break;
2176
+ }
2177
+ }
2178
+ dispatch({
2179
+ type: "SET_DOCUMENT",
2180
+ document: { ...document2, pages: newPages }
2181
+ });
2182
+ }
2183
+ },
2184
+ [document2, dispatch]
2185
+ );
2186
+ const reflowAll = React.useCallback(() => {
2187
+ if (reflowingRef.current) return;
2188
+ reflowingRef.current = true;
2189
+ try {
2190
+ const pages = document2.pages;
2191
+ const allNodes = pages.flatMap((p) => getTopLevelNodes(p.bodyState));
2192
+ if (allNodes.length === 0) {
2193
+ if (pages.length !== 1) {
2194
+ dispatch({
2195
+ type: "SET_DOCUMENT",
2196
+ document: {
2197
+ ...document2,
2198
+ pages: [pages[0] ?? createEmptyPage()]
2199
+ }
2200
+ });
2201
+ }
2202
+ return;
2203
+ }
2204
+ const newPages = [];
2205
+ let remainingNodes = [...allNodes];
2206
+ let pageIdx = 0;
2207
+ while (remainingNodes.length > 0) {
2208
+ const existingPage = pages[pageIdx];
2209
+ const headerH = document2.headerFooterEnabled ? (existingPage == null ? void 0 : existingPage.headerHeight) ?? 0 : 0;
2210
+ const footerH = document2.headerFooterEnabled ? (existingPage == null ? void 0 : existingPage.footerHeight) ?? 0 : 0;
2211
+ const bodyHeight = computeBodyHeight(headerH, footerH);
2212
+ const estimatedLineHeight = 24;
2213
+ const maxNodes = Math.max(1, Math.floor(bodyHeight / estimatedLineHeight));
2214
+ const pageNodes = remainingNodes.slice(0, maxNodes);
2215
+ remainingNodes = remainingNodes.slice(maxNodes);
2216
+ const basePage = existingPage ?? createPageFromTemplate({
2217
+ headerState: document2.defaultHeaderState,
2218
+ footerState: document2.defaultFooterState,
2219
+ headerHeight: document2.defaultHeaderHeight,
2220
+ footerHeight: document2.defaultFooterHeight
2221
+ });
2222
+ newPages.push({
2223
+ ...basePage,
2224
+ bodyState: createEditorStateFromNodes(pageNodes)
2225
+ });
2226
+ pageIdx++;
2227
+ }
2228
+ dispatch({
2229
+ type: "SET_DOCUMENT",
2230
+ document: { ...document2, pages: newPages }
2231
+ });
2232
+ } finally {
2233
+ reflowingRef.current = false;
2234
+ }
2235
+ }, [document2, dispatch]);
2236
+ return {
2237
+ handlePageOverflow,
2238
+ handlePageUnderflow,
2239
+ reflowAll
2240
+ };
2241
+ }
2242
+ const lexicalTheme = {
2243
+ root: "lex4-root outline-none",
2244
+ paragraph: "lex4-paragraph mb-1",
2245
+ heading: {
2246
+ h1: "text-3xl font-bold mb-2",
2247
+ h2: "text-2xl font-bold mb-2",
2248
+ h3: "text-xl font-semibold mb-1",
2249
+ h4: "text-lg font-semibold mb-1",
2250
+ h5: "text-base font-medium mb-1"
2251
+ },
2252
+ text: {
2253
+ bold: "font-bold",
2254
+ italic: "italic",
2255
+ underline: "underline",
2256
+ strikethrough: "line-through",
2257
+ underlineStrikethrough: "underline line-through"
2258
+ },
2259
+ list: {
2260
+ nested: {
2261
+ listitem: "list-none"
2262
+ },
2263
+ ol: "list-decimal ml-6",
2264
+ ul: "list-disc ml-6",
2265
+ listitem: "lex4-listitem",
2266
+ listitemChecked: "lex4-listitem-checked",
2267
+ listitemUnchecked: "lex4-listitem-unchecked"
2268
+ },
2269
+ quote: "border-l-4 border-gray-300 pl-4 italic text-gray-600"
2270
+ };
2271
+ function createEditorConfig(mode, pageId) {
2272
+ const namespace = pageId ? `lex4-${mode}-${pageId}` : `lex4-${mode}`;
2273
+ return {
2274
+ namespace,
2275
+ theme: lexicalTheme,
2276
+ nodes: [richText.HeadingNode, richText.QuoteNode, list.ListNode, list.ListItemNode],
2277
+ onError: (error) => {
2278
+ console.error(`[Lex4 ${mode} editor error]`, error);
2279
+ }
2280
+ };
2281
+ }
2282
+ const TabIndentPlugin = () => {
2283
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
2284
+ const handleTab = React.useCallback(
2285
+ (event) => {
2286
+ event.preventDefault();
2287
+ if (event.shiftKey) {
2288
+ outdentContent(editor);
2289
+ } else {
2290
+ indentContent(editor);
2291
+ }
2292
+ return true;
2293
+ },
2294
+ [editor]
2295
+ );
2296
+ React.useEffect(() => {
2297
+ return editor.registerCommand(lexical.KEY_TAB_COMMAND, handleTab, lexical.COMMAND_PRIORITY_LOW);
2298
+ }, [editor, handleTab]);
2299
+ return null;
2300
+ };
2301
+ const FIRST_LINE_INDENT_PX = 40;
2302
+ function visitElements(node, callback) {
2303
+ callback(node);
2304
+ for (const child of node.getChildren()) {
2305
+ if (lexical.$isElementNode(child)) {
2306
+ visitElements(child, callback);
2307
+ }
2308
+ }
2309
+ }
2310
+ const ParagraphIndentPlugin = () => {
2311
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
2312
+ React.useEffect(() => {
2313
+ const syncParagraphIndents = () => editor.getEditorState().read(() => {
2314
+ visitElements(lexical.$getRoot(), (node) => {
2315
+ if (!lexical.$isParagraphNode(node)) {
2316
+ return;
2317
+ }
2318
+ const element = editor.getElementByKey(node.getKey());
2319
+ if (!(element instanceof HTMLElement)) {
2320
+ return;
2321
+ }
2322
+ const indent = node.getIndent();
2323
+ element.style.paddingInlineStart = "0px";
2324
+ element.style.textIndent = indent > 0 ? `${indent * FIRST_LINE_INDENT_PX}px` : "";
2325
+ });
2326
+ });
2327
+ syncParagraphIndents();
2328
+ return editor.registerUpdateListener(() => {
2329
+ syncParagraphIndents();
2330
+ });
2331
+ }, [editor]);
2332
+ return null;
2333
+ };
2334
+ const PastePlugin = () => {
2335
+ return null;
2336
+ };
2337
+ function resolveTextOffset(root, targetOffset) {
2338
+ var _a, _b;
2339
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
2340
+ let remaining = targetOffset;
2341
+ let currentNode = walker.nextNode();
2342
+ let lastTextNode = null;
2343
+ while (currentNode) {
2344
+ const textLength = ((_a = currentNode.textContent) == null ? void 0 : _a.length) ?? 0;
2345
+ if (remaining <= textLength) {
2346
+ return { node: currentNode, offset: remaining };
2347
+ }
2348
+ remaining -= textLength;
2349
+ lastTextNode = currentNode;
2350
+ currentNode = walker.nextNode();
2351
+ }
2352
+ if (lastTextNode) {
2353
+ return {
2354
+ node: lastTextNode,
2355
+ offset: ((_b = lastTextNode.textContent) == null ? void 0 : _b.length) ?? 0
2356
+ };
2357
+ }
2358
+ return {
2359
+ node: root,
2360
+ offset: root.childNodes.length
2361
+ };
2362
+ }
2363
+ const ActiveEditorPlugin = ({
2364
+ pageId,
2365
+ region,
2366
+ onFocus
2367
+ }) => {
2368
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
2369
+ const {
2370
+ consumePendingCaretPosition,
2371
+ consumePendingFocusAtEnd,
2372
+ focusAtEndVersion,
2373
+ setActiveEditor,
2374
+ setActivePageId
2375
+ } = useDocument();
2376
+ React.useEffect(() => {
2377
+ const caretPosition = { pageId, region };
2378
+ const selectionToRestore = consumePendingCaretPosition(caretPosition);
2379
+ if (selectionToRestore === void 0) {
2380
+ return;
2381
+ }
2382
+ requestAnimationFrame(() => {
2383
+ setActivePageId(pageId);
2384
+ setActiveEditor(editor, caretPosition);
2385
+ editor.focus();
2386
+ if (!selectionToRestore) {
2387
+ return;
2388
+ }
2389
+ requestAnimationFrame(() => {
2390
+ const rootElement = editor.getRootElement();
2391
+ const domSelection = window.getSelection();
2392
+ if (!rootElement || !domSelection) {
2393
+ return;
2394
+ }
2395
+ const anchorPoint = resolveTextOffset(rootElement, selectionToRestore.anchorTextOffset);
2396
+ const focusPoint = resolveTextOffset(rootElement, selectionToRestore.focusTextOffset);
2397
+ const range = document.createRange();
2398
+ range.setStart(anchorPoint.node, anchorPoint.offset);
2399
+ range.setEnd(focusPoint.node, focusPoint.offset);
2400
+ domSelection.removeAllRanges();
2401
+ domSelection.addRange(range);
2402
+ editor.update(() => {
2403
+ const nextSelection = lexical.$createRangeSelectionFromDom(window.getSelection(), editor);
2404
+ if (nextSelection) {
2405
+ lexical.$setSelection(nextSelection);
2406
+ }
2407
+ });
2408
+ });
2409
+ });
2410
+ }, [consumePendingCaretPosition, editor, pageId, region, setActiveEditor, setActivePageId]);
2411
+ React.useEffect(() => {
2412
+ const caretPosition = { pageId, region };
2413
+ if (!consumePendingFocusAtEnd(caretPosition)) {
2414
+ return;
2415
+ }
2416
+ requestAnimationFrame(() => {
2417
+ setActivePageId(pageId);
2418
+ setActiveEditor(editor, caretPosition);
2419
+ editor.focus(() => {
2420
+ editor.update(() => {
2421
+ lexical.$getRoot().selectEnd();
2422
+ });
2423
+ });
2424
+ });
2425
+ }, [
2426
+ consumePendingFocusAtEnd,
2427
+ editor,
2428
+ focusAtEndVersion,
2429
+ pageId,
2430
+ region,
2431
+ setActiveEditor,
2432
+ setActivePageId
2433
+ ]);
2434
+ React.useEffect(() => {
2435
+ return editor.registerCommand(
2436
+ lexical.FOCUS_COMMAND,
2437
+ () => {
2438
+ debug("focus", `editor focused (ns=${editor.getKey()})`);
2439
+ setActivePageId(pageId);
2440
+ setActiveEditor(editor, { pageId, region });
2441
+ onFocus == null ? void 0 : onFocus(editor);
2442
+ return false;
2443
+ },
2444
+ lexical.COMMAND_PRIORITY_LOW
2445
+ );
2446
+ }, [editor, onFocus, pageId, region, setActiveEditor, setActivePageId]);
2447
+ return null;
2448
+ };
2449
+ function serializeNodeTree(node) {
2450
+ const json = node.exportJSON();
2451
+ if (lexical.$isElementNode(node)) {
2452
+ json.children = node.getChildren().map(serializeNodeTree);
2453
+ }
2454
+ return json;
2455
+ }
2456
+ const OVERFLOW_DEBOUNCE_MS = 300;
2457
+ const OverflowPlugin = ({
2458
+ onOverflow
2459
+ }) => {
2460
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
2461
+ const processingRef = React.useRef(false);
2462
+ const onOverflowRef = React.useRef(onOverflow);
2463
+ onOverflowRef.current = onOverflow;
2464
+ React.useEffect(() => {
2465
+ let observer = null;
2466
+ let unregisterUpdateListener = null;
2467
+ let debounceTimer = null;
2468
+ const checkOverflow = (rootElement, container, cause) => {
2469
+ if (processingRef.current) return;
2470
+ const availableHeight = container.clientHeight;
2471
+ if (availableHeight <= 0) return;
2472
+ const contentHeight = rootElement.scrollHeight;
2473
+ if (contentHeight <= availableHeight + 2) return;
2474
+ const children = Array.from(rootElement.children);
2475
+ if (children.length === 0) return;
2476
+ if (children.length <= 1) {
2477
+ debug("overflow", `single block overflows (content=${contentHeight}px > available=${availableHeight}px) — cannot split`);
2478
+ return;
2479
+ }
2480
+ processingRef.current = true;
2481
+ debug("overflow", `OVERFLOW detected: content=${contentHeight}px available=${availableHeight}px children=${children.length}`);
2482
+ let splitIndex = children.length;
2483
+ for (let i = 0; i < children.length; i++) {
2484
+ const child = children[i];
2485
+ const childBottom = child.offsetTop + child.offsetHeight;
2486
+ if (childBottom > availableHeight && i > 0) {
2487
+ splitIndex = i;
2488
+ debug("overflow", `split at index ${i} (childBottom=${childBottom}px > ${availableHeight}px)`);
2489
+ break;
2490
+ }
2491
+ }
2492
+ if (splitIndex >= children.length) {
2493
+ debugWarn("overflow", "no valid split point found — all children fit individually");
2494
+ processingRef.current = false;
2495
+ return;
2496
+ }
2497
+ editor.update(
2498
+ () => {
2499
+ const root = lexical.$getRoot();
2500
+ const allChildren = root.getChildren();
2501
+ if (splitIndex >= allChildren.length) {
2502
+ debugWarn("overflow", `splitIndex=${splitIndex} >= Lexical children=${allChildren.length} — mismatch`);
2503
+ processingRef.current = false;
2504
+ return;
2505
+ }
2506
+ const overflowNodes = [];
2507
+ const toRemove = allChildren.slice(splitIndex);
2508
+ for (const node of toRemove) {
2509
+ overflowNodes.push(serializeNodeTree(node));
2510
+ }
2511
+ for (const node of toRemove) {
2512
+ node.remove();
2513
+ }
2514
+ debug("overflow", `extracted ${overflowNodes.length} overflow nodes, kept ${splitIndex} nodes on current page`);
2515
+ const overflowState = {
2516
+ root: {
2517
+ children: overflowNodes,
2518
+ direction: null,
2519
+ format: "",
2520
+ indent: 0,
2521
+ type: "root",
2522
+ version: 1
2523
+ }
2524
+ };
2525
+ setTimeout(() => {
2526
+ onOverflowRef.current(overflowState, cause);
2527
+ processingRef.current = false;
2528
+ }, 0);
2529
+ },
2530
+ { tag: "overflow-split" }
2531
+ );
2532
+ };
2533
+ const setupObservers = (rootElement) => {
2534
+ if (observer) observer.disconnect();
2535
+ if (unregisterUpdateListener) unregisterUpdateListener();
2536
+ if (debounceTimer) clearTimeout(debounceTimer);
2537
+ const container = rootElement.closest(".lex4-page-body");
2538
+ if (!container) {
2539
+ debugWarn("overflow", "no .lex4-page-body container found");
2540
+ return;
2541
+ }
2542
+ debug("overflow", `observers attached (ns=${editor.getKey()})`);
2543
+ const immediateCheck = () => {
2544
+ requestAnimationFrame(() => checkOverflow(rootElement, container, "content"));
2545
+ };
2546
+ const pasteCheck = () => {
2547
+ requestAnimationFrame(() => checkOverflow(rootElement, container, "paste"));
2548
+ };
2549
+ const debouncedCheck = () => {
2550
+ if (debounceTimer) clearTimeout(debounceTimer);
2551
+ debounceTimer = setTimeout(immediateCheck, OVERFLOW_DEBOUNCE_MS);
2552
+ };
2553
+ observer = new ResizeObserver(debouncedCheck);
2554
+ observer.observe(rootElement);
2555
+ observer.observe(container);
2556
+ unregisterUpdateListener = editor.registerUpdateListener(({ tags }) => {
2557
+ if (tags.has("overflow-split")) return;
2558
+ if (tags.has("paste")) {
2559
+ debug("overflow", `paste check (tags=${Array.from(tags).join(",")})`);
2560
+ pasteCheck();
2561
+ } else if (tags.has("collaboration") || tags.has("historic")) {
2562
+ debug("overflow", `immediate check (tags=${Array.from(tags).join(",")})`);
2563
+ immediateCheck();
2564
+ } else {
2565
+ debouncedCheck();
2566
+ }
2567
+ });
2568
+ immediateCheck();
2569
+ };
2570
+ const unregisterRootListener = editor.registerRootListener(
2571
+ (rootElement) => {
2572
+ if (rootElement) {
2573
+ debug("overflow", `rootListener: root available (ns=${editor.getKey()})`);
2574
+ setupObservers(rootElement);
2575
+ } else {
2576
+ debug("overflow", `rootListener: root detached (ns=${editor.getKey()})`);
2577
+ if (observer) observer.disconnect();
2578
+ if (unregisterUpdateListener) unregisterUpdateListener();
2579
+ if (debounceTimer) clearTimeout(debounceTimer);
2580
+ observer = null;
2581
+ unregisterUpdateListener = null;
2582
+ debounceTimer = null;
2583
+ }
2584
+ }
2585
+ );
2586
+ return () => {
2587
+ unregisterRootListener();
2588
+ if (observer) observer.disconnect();
2589
+ if (unregisterUpdateListener) unregisterUpdateListener();
2590
+ if (debounceTimer) clearTimeout(debounceTimer);
2591
+ };
2592
+ }, [editor]);
2593
+ return null;
2594
+ };
2595
+ function createPageLabel(region, pageNumber) {
2596
+ if (region === "body") {
2597
+ return pageNumber ? `Page ${pageNumber}` : "Body";
2598
+ }
2599
+ const regionLabel = region.charAt(0).toUpperCase() + region.slice(1);
2600
+ return pageNumber ? `${regionLabel} Page ${pageNumber}` : regionLabel;
2601
+ }
2602
+ const HistoryCapturePlugin = ({ pageId, region }) => {
2603
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
2604
+ const { document: document2, queueHistoryAction } = useDocument();
2605
+ const context = React.useMemo(() => {
2606
+ const pageNumber = document2.pages.findIndex((page) => page.id === pageId);
2607
+ return {
2608
+ pageNumber: pageNumber >= 0 ? pageNumber + 1 : null,
2609
+ source: region
2610
+ };
2611
+ }, [document2.pages, pageId, region]);
2612
+ React.useEffect(() => {
2613
+ const buildDescriptor = (prefix) => ({
2614
+ label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
2615
+ source: context.source,
2616
+ pageId,
2617
+ region
2618
+ });
2619
+ return editor.registerCommand(
2620
+ lexical.CONTROLLED_TEXT_INSERTION_COMMAND,
2621
+ () => {
2622
+ queueHistoryAction(buildDescriptor("Typed text"));
2623
+ return false;
2624
+ },
2625
+ lexical.COMMAND_PRIORITY_LOW
2626
+ );
2627
+ }, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
2628
+ React.useEffect(() => {
2629
+ const buildDescriptor = (prefix) => ({
2630
+ label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
2631
+ source: context.source,
2632
+ pageId,
2633
+ region
2634
+ });
2635
+ return editor.registerCommand(
2636
+ lexical.KEY_DOWN_COMMAND,
2637
+ (event) => {
2638
+ if (event.metaKey || event.ctrlKey || event.altKey) {
2639
+ return false;
2640
+ }
2641
+ if (event.key.length !== 1) {
2642
+ return false;
2643
+ }
2644
+ queueHistoryAction(buildDescriptor("Typed text"));
2645
+ return false;
2646
+ },
2647
+ lexical.COMMAND_PRIORITY_LOW
2648
+ );
2649
+ }, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
2650
+ React.useEffect(() => {
2651
+ const buildDescriptor = (prefix) => ({
2652
+ label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
2653
+ source: context.source,
2654
+ pageId,
2655
+ region
2656
+ });
2657
+ return editor.registerCommand(
2658
+ lexical.PASTE_COMMAND,
2659
+ (event) => {
2660
+ var _a;
2661
+ const text = ((_a = event.clipboardData) == null ? void 0 : _a.getData("text/plain")) ?? "";
2662
+ if (text.trim().length === 0) {
2663
+ return false;
2664
+ }
2665
+ queueHistoryAction(buildDescriptor("Pasted content"));
2666
+ return false;
2667
+ },
2668
+ lexical.COMMAND_PRIORITY_LOW
2669
+ );
2670
+ }, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
2671
+ React.useEffect(() => {
2672
+ const buildDescriptor = (prefix) => ({
2673
+ label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
2674
+ source: context.source,
2675
+ pageId,
2676
+ region
2677
+ });
2678
+ return editor.registerCommand(
2679
+ lexical.KEY_ENTER_COMMAND,
2680
+ () => {
2681
+ queueHistoryAction(buildDescriptor("Inserted line break"));
2682
+ return false;
2683
+ },
2684
+ lexical.COMMAND_PRIORITY_LOW
2685
+ );
2686
+ }, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
2687
+ React.useEffect(() => {
2688
+ const buildDescriptor = (prefix) => ({
2689
+ label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
2690
+ source: context.source,
2691
+ pageId,
2692
+ region
2693
+ });
2694
+ return editor.registerCommand(
2695
+ lexical.KEY_BACKSPACE_COMMAND,
2696
+ () => {
2697
+ queueHistoryAction(buildDescriptor("Deleted backward"));
2698
+ return false;
2699
+ },
2700
+ lexical.COMMAND_PRIORITY_LOW
2701
+ );
2702
+ }, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
2703
+ React.useEffect(() => {
2704
+ const buildDescriptor = (prefix) => ({
2705
+ label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
2706
+ source: context.source,
2707
+ pageId,
2708
+ region
2709
+ });
2710
+ return editor.registerCommand(
2711
+ lexical.KEY_DELETE_COMMAND,
2712
+ () => {
2713
+ queueHistoryAction(buildDescriptor("Deleted forward"));
2714
+ return false;
2715
+ },
2716
+ lexical.COMMAND_PRIORITY_LOW
2717
+ );
2718
+ }, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
2719
+ React.useEffect(() => {
2720
+ const buildDescriptor = (prefix) => ({
2721
+ label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
2722
+ source: context.source,
2723
+ pageId,
2724
+ region
2725
+ });
2726
+ return editor.registerCommand(
2727
+ lexical.FORMAT_TEXT_COMMAND,
2728
+ () => {
2729
+ queueHistoryAction(buildDescriptor("Formatted text"));
2730
+ return false;
2731
+ },
2732
+ lexical.COMMAND_PRIORITY_LOW
2733
+ );
2734
+ }, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
2735
+ React.useEffect(() => {
2736
+ const buildDescriptor = (prefix) => ({
2737
+ label: `${prefix} - ${createPageLabel(region, context.pageNumber)}`,
2738
+ source: context.source,
2739
+ pageId,
2740
+ region
2741
+ });
2742
+ return editor.registerCommand(
2743
+ lexical.FORMAT_ELEMENT_COMMAND,
2744
+ () => {
2745
+ queueHistoryAction(buildDescriptor("Formatted paragraph"));
2746
+ return false;
2747
+ },
2748
+ lexical.COMMAND_PRIORITY_LOW
2749
+ );
2750
+ }, [context.pageNumber, context.source, editor, pageId, queueHistoryAction, region]);
2751
+ return null;
2752
+ };
2753
+ function getCollapsedTextPosition(rootElement) {
2754
+ const domSelection = window.getSelection();
2755
+ if (!rootElement || !domSelection || domSelection.rangeCount === 0 || !domSelection.isCollapsed) {
2756
+ return null;
2757
+ }
2758
+ const range = domSelection.getRangeAt(0);
2759
+ if (!rootElement.contains(range.startContainer)) {
2760
+ return null;
2761
+ }
2762
+ const offsetRange = document.createRange();
2763
+ offsetRange.setStart(rootElement, 0);
2764
+ offsetRange.setEnd(range.startContainer, range.startOffset);
2765
+ const contentRange = document.createRange();
2766
+ contentRange.selectNodeContents(rootElement);
2767
+ return {
2768
+ textOffset: offsetRange.toString().length,
2769
+ totalTextLength: contentRange.toString().length
2770
+ };
2771
+ }
2772
+ const PageBoundaryPlugin = ({
2773
+ onBackspaceAtStart,
2774
+ onDeleteAtEnd,
2775
+ onMoveToPreviousPage,
2776
+ onMoveToNextPage
2777
+ }) => {
2778
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
2779
+ React.useEffect(() => editor.registerCommand(
2780
+ lexical.KEY_BACKSPACE_COMMAND,
2781
+ () => {
2782
+ const position = getCollapsedTextPosition(editor.getRootElement());
2783
+ if (!position || position.textOffset !== 0) {
2784
+ return false;
2785
+ }
2786
+ onBackspaceAtStart();
2787
+ return true;
2788
+ },
2789
+ lexical.COMMAND_PRIORITY_HIGH
2790
+ ), [editor, onBackspaceAtStart]);
2791
+ React.useEffect(() => editor.registerCommand(
2792
+ lexical.KEY_DELETE_COMMAND,
2793
+ () => {
2794
+ const position = getCollapsedTextPosition(editor.getRootElement());
2795
+ if (!position || position.textOffset !== position.totalTextLength) {
2796
+ return false;
2797
+ }
2798
+ onDeleteAtEnd();
2799
+ return true;
2800
+ },
2801
+ lexical.COMMAND_PRIORITY_HIGH
2802
+ ), [editor, onDeleteAtEnd]);
2803
+ React.useEffect(() => editor.registerCommand(
2804
+ lexical.KEY_DOWN_COMMAND,
2805
+ (event) => {
2806
+ if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) {
2807
+ return false;
2808
+ }
2809
+ const position = getCollapsedTextPosition(editor.getRootElement());
2810
+ if (!position) {
2811
+ return false;
2812
+ }
2813
+ if ((event.key === "ArrowLeft" || event.key === "ArrowUp") && position.textOffset === 0) {
2814
+ event.preventDefault();
2815
+ onMoveToPreviousPage();
2816
+ return true;
2817
+ }
2818
+ if ((event.key === "ArrowRight" || event.key === "ArrowDown") && position.textOffset === position.totalTextLength) {
2819
+ event.preventDefault();
2820
+ onMoveToNextPage();
2821
+ return true;
2822
+ }
2823
+ return false;
2824
+ },
2825
+ lexical.COMMAND_PRIORITY_HIGH
2826
+ ), [editor, onMoveToNextPage, onMoveToPreviousPage]);
2827
+ return null;
2828
+ };
2829
+ const EditorRegistryPlugin = ({ pageId }) => {
2830
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
2831
+ const { editorRegistry } = useDocument();
2832
+ React.useEffect(() => {
2833
+ editorRegistry.register(pageId, editor);
2834
+ debug("registry", `registered editor for page ${shortId(pageId)}`);
2835
+ return () => {
2836
+ editorRegistry.unregister(pageId);
2837
+ debug("registry", `unregistered editor for page ${shortId(pageId)}`);
2838
+ };
2839
+ }, [editor, pageId, editorRegistry]);
2840
+ return null;
2841
+ };
2842
+ const PageBody = ({
2843
+ pageId,
2844
+ initialBodyState,
2845
+ onBodyChange,
2846
+ onOverflow,
2847
+ onFocus,
2848
+ onBackspaceAtStart,
2849
+ onDeleteAtEnd,
2850
+ onMoveToPreviousPage,
2851
+ onMoveToNextPage,
2852
+ readOnly = false
2853
+ }) => {
2854
+ const config = React.useMemo(
2855
+ () => {
2856
+ var _a, _b;
2857
+ const baseConfig = {
2858
+ ...createEditorConfig("body", pageId),
2859
+ editable: !readOnly
2860
+ };
2861
+ if (initialBodyState) {
2862
+ debug("page", `PageBody ${shortId(pageId)}: initializing with ${((_b = (_a = initialBodyState.root) == null ? void 0 : _a.children) == null ? void 0 : _b.length) ?? 0} children`);
2863
+ return {
2864
+ ...baseConfig,
2865
+ editorState: JSON.stringify(initialBodyState)
2866
+ };
2867
+ }
2868
+ debug("page", `PageBody ${shortId(pageId)}: initializing empty`);
2869
+ return baseConfig;
2870
+ },
2871
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally only use initialBodyState at mount time
2872
+ [pageId, readOnly]
2873
+ );
2874
+ const handleChange = React.useCallback(
2875
+ (editorState) => {
2876
+ const serialized = editorState.toJSON();
2877
+ onBodyChange == null ? void 0 : onBodyChange(serialized);
2878
+ },
2879
+ [onBodyChange]
2880
+ );
2881
+ const handleOverflow = React.useCallback(
2882
+ (overflowContent, cause) => {
2883
+ var _a, _b;
2884
+ debug("page", `PageBody ${shortId(pageId)}: overflow callback fired with ${((_b = (_a = overflowContent.root) == null ? void 0 : _a.children) == null ? void 0 : _b.length) ?? 0} children`);
2885
+ onOverflow == null ? void 0 : onOverflow(overflowContent, cause);
2886
+ },
2887
+ [onOverflow, pageId]
2888
+ );
2889
+ return /* @__PURE__ */ jsxRuntime.jsx(
2890
+ "div",
2891
+ {
2892
+ className: "lex4-page-body flex-1 min-h-0 relative",
2893
+ style: { overflow: "hidden" },
2894
+ "data-testid": `page-body-${pageId}`,
2895
+ onFocus,
2896
+ children: /* @__PURE__ */ jsxRuntime.jsxs(LexicalComposer.LexicalComposer, { initialConfig: config, children: [
2897
+ /* @__PURE__ */ jsxRuntime.jsx(
2898
+ LexicalRichTextPlugin.RichTextPlugin,
2899
+ {
2900
+ contentEditable: /* @__PURE__ */ jsxRuntime.jsx(
2901
+ LexicalContentEditable.ContentEditable,
2902
+ {
2903
+ className: "outline-none h-full p-0",
2904
+ style: { overflow: "visible" }
2905
+ }
2906
+ ),
2907
+ placeholder: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-0 left-0 text-gray-400 pointer-events-none select-none", children: "Start typing..." }),
2908
+ ErrorBoundary: LexicalErrorBoundary.LexicalErrorBoundary
2909
+ }
2910
+ ),
2911
+ /* @__PURE__ */ jsxRuntime.jsx(LexicalListPlugin.ListPlugin, {}),
2912
+ /* @__PURE__ */ jsxRuntime.jsx(ParagraphIndentPlugin, {}),
2913
+ /* @__PURE__ */ jsxRuntime.jsx(TabIndentPlugin, {}),
2914
+ /* @__PURE__ */ jsxRuntime.jsx(PastePlugin, {}),
2915
+ /* @__PURE__ */ jsxRuntime.jsx(EditorRegistryPlugin, { pageId }),
2916
+ /* @__PURE__ */ jsxRuntime.jsx(ActiveEditorPlugin, { pageId, region: "body" }),
2917
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx(HistoryCapturePlugin, { pageId, region: "body" }),
2918
+ !readOnly && onBackspaceAtStart && onDeleteAtEnd && onMoveToPreviousPage && onMoveToNextPage && /* @__PURE__ */ jsxRuntime.jsx(
2919
+ PageBoundaryPlugin,
2920
+ {
2921
+ onBackspaceAtStart,
2922
+ onDeleteAtEnd,
2923
+ onMoveToPreviousPage,
2924
+ onMoveToNextPage
2925
+ }
2926
+ ),
2927
+ /* @__PURE__ */ jsxRuntime.jsx(OverflowPlugin, { onOverflow: handleOverflow }),
2928
+ /* @__PURE__ */ jsxRuntime.jsx(LexicalOnChangePlugin.OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true })
2929
+ ] })
2930
+ }
2931
+ );
2932
+ };
2933
+ function HeightLimitPlugin({ maxHeight, channel }) {
2934
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
2935
+ const lastGoodStateRef = React.useRef(null);
2936
+ const isRevertingRef = React.useRef(false);
2937
+ React.useEffect(() => {
2938
+ lastGoodStateRef.current = JSON.stringify(editor.getEditorState().toJSON());
2939
+ const removeEnterCommand = editor.registerCommand(
2940
+ lexical.KEY_ENTER_COMMAND,
2941
+ () => {
2942
+ const root = editor.getRootElement();
2943
+ if (root && root.scrollHeight >= maxHeight) {
2944
+ debug(channel, `blocked Enter — scrollHeight ${root.scrollHeight}px >= max ${maxHeight}px`);
2945
+ return true;
2946
+ }
2947
+ return false;
2948
+ },
2949
+ lexical.COMMAND_PRIORITY_CRITICAL
2950
+ );
2951
+ const removePasteCommand = editor.registerCommand(
2952
+ lexical.PASTE_COMMAND,
2953
+ () => {
2954
+ const root = editor.getRootElement();
2955
+ if (root && root.scrollHeight >= maxHeight) {
2956
+ debug(channel, `blocked paste — scrollHeight ${root.scrollHeight}px >= max ${maxHeight}px`);
2957
+ return true;
2958
+ }
2959
+ return false;
2960
+ },
2961
+ lexical.COMMAND_PRIORITY_CRITICAL
2962
+ );
2963
+ const removeUpdateListener = editor.registerUpdateListener(({ editorState, prevEditorState }) => {
2964
+ if (isRevertingRef.current) return;
2965
+ const root = editor.getRootElement();
2966
+ if (!root) return;
2967
+ requestAnimationFrame(() => {
2968
+ if (root.scrollHeight > maxHeight) {
2969
+ debug(channel, `reverting — scrollHeight ${root.scrollHeight}px > max ${maxHeight}px`);
2970
+ const restoreState = JSON.stringify(prevEditorState.toJSON()) || lastGoodStateRef.current;
2971
+ if (!restoreState) {
2972
+ return;
2973
+ }
2974
+ isRevertingRef.current = true;
2975
+ editor.setEditorState(editor.parseEditorState(restoreState));
2976
+ isRevertingRef.current = false;
2977
+ } else {
2978
+ lastGoodStateRef.current = JSON.stringify(editorState.toJSON());
2979
+ }
2980
+ });
2981
+ });
2982
+ return () => {
2983
+ removeEnterCommand();
2984
+ removePasteCommand();
2985
+ removeUpdateListener();
2986
+ };
2987
+ }, [editor, maxHeight, channel]);
2988
+ return null;
2989
+ }
2990
+ const PageHeader = ({
2991
+ pageId,
2992
+ initialHeaderState,
2993
+ pageCounterLabel,
2994
+ onHeaderChange
2995
+ }) => {
2996
+ const hasPageCounter = !!pageCounterLabel;
2997
+ const config = React.useMemo(
2998
+ () => {
2999
+ const baseConfig = createEditorConfig("header", pageId);
3000
+ if (initialHeaderState) {
3001
+ return { ...baseConfig, editorState: JSON.stringify(initialHeaderState) };
3002
+ }
3003
+ return baseConfig;
3004
+ },
3005
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- syncVersion forces remount via key, only used at init
3006
+ [pageId]
3007
+ );
3008
+ const contentRef = React.useRef(null);
3009
+ const handleChange = React.useCallback(
3010
+ (editorState) => {
3011
+ requestAnimationFrame(() => {
3012
+ const el = contentRef.current;
3013
+ if (el) {
3014
+ if (el.scrollHeight > MAX_HEADER_HEIGHT_PX) {
3015
+ debug(
3016
+ "header",
3017
+ `page ${shortId(pageId)}: skipped external sync at ${el.scrollHeight}px > max ${MAX_HEADER_HEIGHT_PX}px`
3018
+ );
3019
+ return;
3020
+ }
3021
+ const height = Math.min(el.scrollHeight, MAX_HEADER_HEIGHT_PX);
3022
+ debug("header", `page ${shortId(pageId)}: height=${height}px (scrollH=${el.scrollHeight})`);
3023
+ onHeaderChange == null ? void 0 : onHeaderChange(editorState.toJSON(), height);
3024
+ }
3025
+ });
3026
+ },
3027
+ [onHeaderChange, pageId]
3028
+ );
3029
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3030
+ "div",
3031
+ {
3032
+ className: "lex4-page-header border-b border-dashed border-gray-200 relative flex-shrink-0",
3033
+ style: { maxHeight: MAX_HEADER_HEIGHT_PX, overflow: "clip" },
3034
+ "data-testid": `page-header-${pageId}`,
3035
+ children: [
3036
+ /* @__PURE__ */ jsxRuntime.jsxs(LexicalComposer.LexicalComposer, { initialConfig: config, children: [
3037
+ /* @__PURE__ */ jsxRuntime.jsx(
3038
+ LexicalRichTextPlugin.RichTextPlugin,
3039
+ {
3040
+ contentEditable: /* @__PURE__ */ jsxRuntime.jsx(
3041
+ LexicalContentEditable.ContentEditable,
3042
+ {
3043
+ ref: contentRef,
3044
+ className: `outline-none p-2 text-sm text-gray-600 min-h-[24px] ${hasPageCounter ? "pr-24" : ""}`
3045
+ }
3046
+ ),
3047
+ placeholder: /* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute top-0 left-0 text-gray-400 pointer-events-none select-none p-2 text-sm ${hasPageCounter ? "pr-24" : ""}`, children: "Header" }),
3048
+ ErrorBoundary: LexicalErrorBoundary.LexicalErrorBoundary
3049
+ }
3050
+ ),
3051
+ /* @__PURE__ */ jsxRuntime.jsx(ActiveEditorPlugin, { pageId, region: "header" }),
3052
+ /* @__PURE__ */ jsxRuntime.jsx(HistoryCapturePlugin, { pageId, region: "header" }),
3053
+ /* @__PURE__ */ jsxRuntime.jsx(HeightLimitPlugin, { maxHeight: MAX_HEADER_HEIGHT_PX, channel: "header" }),
3054
+ /* @__PURE__ */ jsxRuntime.jsx(LexicalOnChangePlugin.OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true })
3055
+ ] }),
3056
+ pageCounterLabel && /* @__PURE__ */ jsxRuntime.jsx(
3057
+ "div",
3058
+ {
3059
+ className: "pointer-events-none absolute right-2 top-2 select-none text-xs text-gray-500",
3060
+ "data-testid": `page-counter-header-${pageId}`,
3061
+ children: pageCounterLabel
3062
+ }
3063
+ )
3064
+ ]
3065
+ }
3066
+ );
3067
+ };
3068
+ const PageFooter = ({
3069
+ pageId,
3070
+ initialFooterState,
3071
+ pageCounterLabel,
3072
+ onFooterChange
3073
+ }) => {
3074
+ const hasPageCounter = !!pageCounterLabel;
3075
+ const config = React.useMemo(
3076
+ () => {
3077
+ const baseConfig = createEditorConfig("footer", pageId);
3078
+ if (initialFooterState) {
3079
+ return { ...baseConfig, editorState: JSON.stringify(initialFooterState) };
3080
+ }
3081
+ return baseConfig;
3082
+ },
3083
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- syncVersion forces remount via key, only used at init
3084
+ [pageId]
3085
+ );
3086
+ const contentRef = React.useRef(null);
3087
+ const handleChange = React.useCallback(
3088
+ (editorState) => {
3089
+ requestAnimationFrame(() => {
3090
+ const el = contentRef.current;
3091
+ if (el) {
3092
+ if (el.scrollHeight > MAX_FOOTER_HEIGHT_PX) {
3093
+ debug(
3094
+ "footer",
3095
+ `page ${shortId(pageId)}: skipped external sync at ${el.scrollHeight}px > max ${MAX_FOOTER_HEIGHT_PX}px`
3096
+ );
3097
+ return;
3098
+ }
3099
+ const height = Math.min(el.scrollHeight, MAX_FOOTER_HEIGHT_PX);
3100
+ debug("footer", `page ${shortId(pageId)}: height=${height}px (scrollH=${el.scrollHeight})`);
3101
+ onFooterChange == null ? void 0 : onFooterChange(editorState.toJSON(), height);
3102
+ }
3103
+ });
3104
+ },
3105
+ [onFooterChange, pageId]
3106
+ );
3107
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3108
+ "div",
3109
+ {
3110
+ className: "lex4-page-footer border-t border-dashed border-gray-200 relative flex-shrink-0",
3111
+ style: { maxHeight: MAX_FOOTER_HEIGHT_PX, overflow: "clip" },
3112
+ "data-testid": `page-footer-${pageId}`,
3113
+ children: [
3114
+ /* @__PURE__ */ jsxRuntime.jsxs(LexicalComposer.LexicalComposer, { initialConfig: config, children: [
3115
+ /* @__PURE__ */ jsxRuntime.jsx(
3116
+ LexicalRichTextPlugin.RichTextPlugin,
3117
+ {
3118
+ contentEditable: /* @__PURE__ */ jsxRuntime.jsx(
3119
+ LexicalContentEditable.ContentEditable,
3120
+ {
3121
+ ref: contentRef,
3122
+ className: `outline-none p-2 text-sm text-gray-600 min-h-[24px] ${hasPageCounter ? "pr-24" : ""}`
3123
+ }
3124
+ ),
3125
+ placeholder: /* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute top-0 left-0 text-gray-400 pointer-events-none select-none p-2 text-sm ${hasPageCounter ? "pr-24" : ""}`, children: "Footer" }),
3126
+ ErrorBoundary: LexicalErrorBoundary.LexicalErrorBoundary
3127
+ }
3128
+ ),
3129
+ /* @__PURE__ */ jsxRuntime.jsx(ActiveEditorPlugin, { pageId, region: "footer" }),
3130
+ /* @__PURE__ */ jsxRuntime.jsx(HistoryCapturePlugin, { pageId, region: "footer" }),
3131
+ /* @__PURE__ */ jsxRuntime.jsx(HeightLimitPlugin, { maxHeight: MAX_FOOTER_HEIGHT_PX, channel: "footer" }),
3132
+ /* @__PURE__ */ jsxRuntime.jsx(LexicalOnChangePlugin.OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true })
3133
+ ] }),
3134
+ pageCounterLabel && /* @__PURE__ */ jsxRuntime.jsx(
3135
+ "div",
3136
+ {
3137
+ className: "pointer-events-none absolute right-2 top-2 select-none text-xs text-gray-500",
3138
+ "data-testid": `page-counter-footer-${pageId}`,
3139
+ children: pageCounterLabel
3140
+ }
3141
+ )
3142
+ ]
3143
+ }
3144
+ );
3145
+ };
3146
+ const PageView = React.memo(({
3147
+ pageId,
3148
+ pageIndex,
3149
+ onOverflow,
3150
+ onBackspaceAtStart,
3151
+ onDeleteAtEnd,
3152
+ onMoveToPreviousPage,
3153
+ onMoveToNextPage
3154
+ }) => {
3155
+ const { document: document2, dispatch, setActivePageId } = useDocument();
3156
+ const page = document2.pages.find((p) => p.id === pageId);
3157
+ const showHeaderFooter = document2.headerFooterEnabled;
3158
+ const pageCounterMode = document2.pageCounterMode;
3159
+ const pageCounterLabel = `Page ${pageIndex + 1} of ${document2.pages.length}`;
3160
+ if (!page) return null;
3161
+ const handleBodyChange = React.useCallback(
3162
+ (bodyState) => {
3163
+ dispatch({ type: "UPDATE_PAGE_BODY", pageId, bodyState });
3164
+ },
3165
+ [dispatch, pageId]
3166
+ );
3167
+ const handleHeaderChange = React.useCallback(
3168
+ (headerState, height) => {
3169
+ dispatch({ type: "UPDATE_PAGE_HEADER_CONTENT", pageId, headerState, height });
3170
+ },
3171
+ [dispatch, pageId]
3172
+ );
3173
+ const handleFooterChange = React.useCallback(
3174
+ (footerState, height) => {
3175
+ dispatch({ type: "UPDATE_PAGE_FOOTER_CONTENT", pageId, footerState, height });
3176
+ },
3177
+ [dispatch, pageId]
3178
+ );
3179
+ const handleFocus = React.useCallback(() => {
3180
+ setActivePageId(pageId);
3181
+ }, [setActivePageId, pageId]);
3182
+ const handleOverflow = React.useCallback(
3183
+ (overflowContent, cause) => {
3184
+ onOverflow == null ? void 0 : onOverflow(overflowContent, cause);
3185
+ },
3186
+ [onOverflow]
3187
+ );
3188
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3189
+ "div",
3190
+ {
3191
+ className: "lex4-page bg-white shadow-sm flex flex-col",
3192
+ style: {
3193
+ width: A4_WIDTH_PX,
3194
+ height: A4_HEIGHT_PX,
3195
+ padding: PAGE_MARGIN_PX
3196
+ },
3197
+ "data-testid": `page-${pageIndex}`,
3198
+ "data-page-id": pageId,
3199
+ children: [
3200
+ showHeaderFooter && /* @__PURE__ */ jsxRuntime.jsx(
3201
+ PageHeader,
3202
+ {
3203
+ pageId,
3204
+ initialHeaderState: page.headerState,
3205
+ pageCounterLabel: pageCounterMode === "header" || pageCounterMode === "both" ? pageCounterLabel : void 0,
3206
+ onHeaderChange: handleHeaderChange
3207
+ },
3208
+ `header-${page.headerSyncVersion}`
3209
+ ),
3210
+ /* @__PURE__ */ jsxRuntime.jsx(
3211
+ PageBody,
3212
+ {
3213
+ pageId,
3214
+ initialBodyState: page.bodyState,
3215
+ onBodyChange: handleBodyChange,
3216
+ onOverflow: handleOverflow,
3217
+ onFocus: handleFocus,
3218
+ onBackspaceAtStart: () => onBackspaceAtStart == null ? void 0 : onBackspaceAtStart(pageIndex, pageId),
3219
+ onDeleteAtEnd: () => onDeleteAtEnd == null ? void 0 : onDeleteAtEnd(pageIndex, pageId),
3220
+ onMoveToPreviousPage: () => onMoveToPreviousPage == null ? void 0 : onMoveToPreviousPage(pageIndex),
3221
+ onMoveToNextPage: () => onMoveToNextPage == null ? void 0 : onMoveToNextPage(pageIndex)
3222
+ },
3223
+ `body-${page.bodySyncVersion}`
3224
+ ),
3225
+ showHeaderFooter && /* @__PURE__ */ jsxRuntime.jsx(
3226
+ PageFooter,
3227
+ {
3228
+ pageId,
3229
+ initialFooterState: page.footerState,
3230
+ pageCounterLabel: pageCounterMode === "footer" || pageCounterMode === "both" ? pageCounterLabel : void 0,
3231
+ onFooterChange: handleFooterChange
3232
+ },
3233
+ `footer-${page.footerSyncVersion}`
3234
+ )
3235
+ ]
3236
+ }
3237
+ );
3238
+ });
3239
+ PageView.displayName = "PageView";
3240
+ const DocumentView = () => {
3241
+ const {
3242
+ document: document2,
3243
+ dispatch,
3244
+ editorRegistry,
3245
+ requestFocusAtEnd,
3246
+ runHistoryAction,
3247
+ setActiveEditor,
3248
+ setActivePageId
3249
+ } = useDocument();
3250
+ const { handlePageUnderflow, reflowAll } = usePagination(document2, dispatch);
3251
+ const previousBodyHeightsRef = React.useRef(null);
3252
+ const pasteOverflowSequenceRef = React.useRef(false);
3253
+ const pasteOverflowReleaseTimerRef = React.useRef(null);
3254
+ const defaultPageTemplate = React.useMemo(
3255
+ () => ({
3256
+ headerState: document2.defaultHeaderState,
3257
+ footerState: document2.defaultFooterState,
3258
+ headerHeight: document2.defaultHeaderHeight,
3259
+ footerHeight: document2.defaultFooterHeight
3260
+ }),
3261
+ [
3262
+ document2.defaultFooterHeight,
3263
+ document2.defaultFooterState,
3264
+ document2.defaultHeaderHeight,
3265
+ document2.defaultHeaderState
3266
+ ]
3267
+ );
3268
+ const bodyHeights = React.useMemo(
3269
+ () => document2.pages.map((page) => computeBodyHeight(
3270
+ document2.headerFooterEnabled ? page.headerHeight : 0,
3271
+ document2.headerFooterEnabled ? page.footerHeight : 0
3272
+ )),
3273
+ [document2.headerFooterEnabled, document2.pages]
3274
+ );
3275
+ React.useEffect(() => {
3276
+ const previousBodyHeights = previousBodyHeightsRef.current;
3277
+ previousBodyHeightsRef.current = bodyHeights;
3278
+ if (!previousBodyHeights) {
3279
+ return;
3280
+ }
3281
+ const bodySpaceExpanded = bodyHeights.some((height, index) => {
3282
+ const previousHeight = previousBodyHeights[index];
3283
+ return previousHeight !== void 0 && height > previousHeight;
3284
+ });
3285
+ if (bodySpaceExpanded) {
3286
+ debug("page", "body space expanded — running full reflow");
3287
+ reflowAll();
3288
+ }
3289
+ }, [bodyHeights, reflowAll]);
3290
+ React.useEffect(
3291
+ () => () => {
3292
+ if (pasteOverflowReleaseTimerRef.current) {
3293
+ clearTimeout(pasteOverflowReleaseTimerRef.current);
3294
+ }
3295
+ },
3296
+ []
3297
+ );
3298
+ const focusBodyEditor = React.useCallback(
3299
+ (pageId, boundary) => {
3300
+ let attempts = 0;
3301
+ const focusWhenReady = () => {
3302
+ const editor = editorRegistry.get(pageId);
3303
+ if (!editor) {
3304
+ if (attempts < 4) {
3305
+ attempts += 1;
3306
+ requestAnimationFrame(focusWhenReady);
3307
+ }
3308
+ return;
3309
+ }
3310
+ const caretPosition = { pageId, region: "body" };
3311
+ setActivePageId(pageId);
3312
+ setActiveEditor(editor, caretPosition);
3313
+ editor.focus(() => {
3314
+ editor.update(() => {
3315
+ if (boundary === "start") {
3316
+ lexical.$getRoot().selectStart();
3317
+ } else {
3318
+ lexical.$getRoot().selectEnd();
3319
+ }
3320
+ });
3321
+ });
3322
+ };
3323
+ requestAnimationFrame(focusWhenReady);
3324
+ },
3325
+ [editorRegistry, setActiveEditor, setActivePageId]
3326
+ );
3327
+ const handlePageOverflow = React.useCallback(
3328
+ (pageIndex, overflowContent, cause) => {
3329
+ var _a, _b, _c, _d;
3330
+ const nextPageIndex = pageIndex + 1;
3331
+ const overflowChildCount = ((_b = (_a = overflowContent.root) == null ? void 0 : _a.children) == null ? void 0 : _b.length) ?? 0;
3332
+ debug("page", `handlePageOverflow: pageIndex=${pageIndex} overflowChildren=${overflowChildCount} totalPages=${document2.pages.length}`);
3333
+ if (cause === "paste") {
3334
+ pasteOverflowSequenceRef.current = true;
3335
+ }
3336
+ if (pasteOverflowSequenceRef.current) {
3337
+ if (pasteOverflowReleaseTimerRef.current) {
3338
+ clearTimeout(pasteOverflowReleaseTimerRef.current);
3339
+ }
3340
+ pasteOverflowReleaseTimerRef.current = setTimeout(() => {
3341
+ pasteOverflowSequenceRef.current = false;
3342
+ pasteOverflowReleaseTimerRef.current = null;
3343
+ }, 800);
3344
+ }
3345
+ if (nextPageIndex < document2.pages.length) {
3346
+ const nextPage = document2.pages[nextPageIndex];
3347
+ const nextEditor = editorRegistry.get(nextPage.id);
3348
+ if (nextEditor) {
3349
+ const currentState = nextEditor.getEditorState().toJSON();
3350
+ const existingChildren = ((_c = currentState.root) == null ? void 0 : _c.children) ?? [];
3351
+ const overflowChildren = ((_d = overflowContent.root) == null ? void 0 : _d.children) ?? [];
3352
+ debug("page", `prepending ${overflowChildren.length} nodes to existing page ${shortId(nextPage.id)} (had ${existingChildren.length} children)`);
3353
+ const mergedState = {
3354
+ root: {
3355
+ ...currentState.root,
3356
+ children: [...overflowChildren, ...existingChildren]
3357
+ }
3358
+ };
3359
+ const newEditorState = nextEditor.parseEditorState(JSON.stringify(mergedState));
3360
+ nextEditor.setEditorState(newEditorState);
3361
+ if (pasteOverflowSequenceRef.current) {
3362
+ requestFocusAtEnd({ pageId: nextPage.id, region: "body" });
3363
+ }
3364
+ } else {
3365
+ debug("page", `editor not found in registry for page ${shortId(nextPage.id)} — falling back to ADD_PAGE`);
3366
+ const newPage = createPageFromTemplate(defaultPageTemplate);
3367
+ newPage.bodyState = overflowContent;
3368
+ runHistoryAction(
3369
+ {
3370
+ label: "Overflow created new page",
3371
+ source: "overflow",
3372
+ region: "document"
3373
+ },
3374
+ () => {
3375
+ dispatch({ type: "ADD_PAGE", afterIndex: pageIndex, page: newPage });
3376
+ }
3377
+ );
3378
+ if (pasteOverflowSequenceRef.current) {
3379
+ requestFocusAtEnd({ pageId: newPage.id, region: "body" });
3380
+ }
3381
+ }
3382
+ } else {
3383
+ const newPage = createPageFromTemplate(defaultPageTemplate);
3384
+ newPage.bodyState = overflowContent;
3385
+ debug("page", `creating new page ${shortId(newPage.id)} with ${overflowChildCount} overflow children`);
3386
+ runHistoryAction(
3387
+ {
3388
+ label: "Overflow created new page",
3389
+ source: "overflow",
3390
+ region: "document"
3391
+ },
3392
+ () => {
3393
+ dispatch({ type: "ADD_PAGE", page: newPage });
3394
+ }
3395
+ );
3396
+ if (pasteOverflowSequenceRef.current) {
3397
+ requestFocusAtEnd({ pageId: newPage.id, region: "body" });
3398
+ }
3399
+ }
3400
+ },
3401
+ [defaultPageTemplate, document2.pages, dispatch, editorRegistry, requestFocusAtEnd, runHistoryAction]
3402
+ );
3403
+ const handleBackspaceAtPageStart = React.useCallback(
3404
+ (pageIndex, pageId) => {
3405
+ if (pageIndex <= 0) {
3406
+ return;
3407
+ }
3408
+ const previousPage = document2.pages[pageIndex - 1];
3409
+ if (!previousPage) {
3410
+ return;
3411
+ }
3412
+ runHistoryAction(
3413
+ {
3414
+ label: `Deleted backward - Page ${pageIndex + 1}`,
3415
+ source: "body",
3416
+ pageId,
3417
+ region: "body"
3418
+ },
3419
+ () => {
3420
+ handlePageUnderflow(pageIndex - 1);
3421
+ }
3422
+ );
3423
+ focusBodyEditor(previousPage.id, "end");
3424
+ },
3425
+ [document2.pages, focusBodyEditor, handlePageUnderflow, runHistoryAction]
3426
+ );
3427
+ const handleDeleteAtPageEnd = React.useCallback(
3428
+ (pageIndex, pageId) => {
3429
+ const currentPage = document2.pages[pageIndex];
3430
+ const nextPage = document2.pages[pageIndex + 1];
3431
+ if (!currentPage || !nextPage) {
3432
+ return;
3433
+ }
3434
+ runHistoryAction(
3435
+ {
3436
+ label: `Deleted forward - Page ${pageIndex + 1}`,
3437
+ source: "body",
3438
+ pageId,
3439
+ region: "body"
3440
+ },
3441
+ () => {
3442
+ handlePageUnderflow(pageIndex);
3443
+ }
3444
+ );
3445
+ focusBodyEditor(currentPage.id, "end");
3446
+ },
3447
+ [document2.pages, focusBodyEditor, handlePageUnderflow, runHistoryAction]
3448
+ );
3449
+ const handleMoveToPreviousPage = React.useCallback(
3450
+ (pageIndex) => {
3451
+ if (pageIndex <= 0) {
3452
+ return;
3453
+ }
3454
+ const previousPage = document2.pages[pageIndex - 1];
3455
+ if (!previousPage) {
3456
+ return;
3457
+ }
3458
+ focusBodyEditor(previousPage.id, "end");
3459
+ },
3460
+ [document2.pages, focusBodyEditor]
3461
+ );
3462
+ const handleMoveToNextPage = React.useCallback(
3463
+ (pageIndex) => {
3464
+ const nextPage = document2.pages[pageIndex + 1];
3465
+ if (!nextPage) {
3466
+ return;
3467
+ }
3468
+ focusBodyEditor(nextPage.id, "start");
3469
+ },
3470
+ [document2.pages, focusBodyEditor]
3471
+ );
3472
+ return /* @__PURE__ */ jsxRuntime.jsx(
3473
+ "div",
3474
+ {
3475
+ className: "lex4-document-view flex flex-col items-center gap-8 py-8 min-h-full",
3476
+ "data-testid": "document-view",
3477
+ tabIndex: -1,
3478
+ children: document2.pages.map((page, index) => /* @__PURE__ */ jsxRuntime.jsx(
3479
+ PageView,
3480
+ {
3481
+ pageId: page.id,
3482
+ pageIndex: index,
3483
+ onOverflow: (content, cause) => handlePageOverflow(index, content, cause),
3484
+ onBackspaceAtStart: handleBackspaceAtPageStart,
3485
+ onDeleteAtEnd: handleDeleteAtPageEnd,
3486
+ onMoveToPreviousPage: handleMoveToPreviousPage,
3487
+ onMoveToNextPage: handleMoveToNextPage
3488
+ },
3489
+ page.id
3490
+ ))
3491
+ }
3492
+ );
3493
+ };
3494
+ function selectEntireDocument(rootElement, selectionBuffer) {
3495
+ if (!rootElement || !selectionBuffer) {
3496
+ return;
3497
+ }
3498
+ const bodyRoots = Array.from(
3499
+ rootElement.querySelectorAll('[data-testid^="page-body-"] [data-lexical-editor="true"]')
3500
+ );
3501
+ if (bodyRoots.length === 0) {
3502
+ return;
3503
+ }
3504
+ selectionBuffer.value = bodyRoots.map((bodyRoot) => bodyRoot.innerText.trim()).filter(Boolean).join("\n");
3505
+ selectionBuffer.focus();
3506
+ selectionBuffer.select();
3507
+ }
3508
+ const GLOBAL_SELECTION_BACKGROUND = "rgb(191, 219, 254)";
3509
+ const GLOBAL_SELECTION_FOREGROUND = "rgb(30, 64, 175)";
3510
+ function isFormFieldTarget(target) {
3511
+ const element = target;
3512
+ const tagName = element == null ? void 0 : element.tagName;
3513
+ return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
3514
+ }
3515
+ const EditorChrome = ({
3516
+ captureHistoryShortcutsOnWindow,
3517
+ className
3518
+ }) => {
3519
+ const {
3520
+ document: document2,
3521
+ dispatch,
3522
+ globalSelectionActive,
3523
+ setGlobalSelectionActive,
3524
+ undo,
3525
+ redo
3526
+ } = useDocument();
3527
+ const rootRef = React.useRef(null);
3528
+ const selectionBufferRef = React.useRef(null);
3529
+ const clearGlobalSelection = React.useCallback(() => {
3530
+ var _a;
3531
+ setGlobalSelectionActive(false);
3532
+ (_a = selectionBufferRef.current) == null ? void 0 : _a.blur();
3533
+ }, [setGlobalSelectionActive]);
3534
+ const handleHistoryShortcut = React.useCallback((event) => {
3535
+ var _a, _b;
3536
+ const key = event.key.toLowerCase();
3537
+ if (key === "z") {
3538
+ event.preventDefault();
3539
+ (_a = event.stopPropagation) == null ? void 0 : _a.call(event);
3540
+ clearGlobalSelection();
3541
+ if (event.shiftKey) {
3542
+ redo();
3543
+ } else {
3544
+ undo();
3545
+ }
3546
+ return true;
3547
+ }
3548
+ if (key === "y") {
3549
+ event.preventDefault();
3550
+ (_b = event.stopPropagation) == null ? void 0 : _b.call(event);
3551
+ clearGlobalSelection();
3552
+ redo();
3553
+ return true;
3554
+ }
3555
+ return false;
3556
+ }, [clearGlobalSelection, redo, undo]);
3557
+ const handleKeyDownCapture = React.useCallback((event) => {
3558
+ const target = event.target;
3559
+ const isGlobalSelectionBufferTarget = target === selectionBufferRef.current;
3560
+ if (!isGlobalSelectionBufferTarget && isFormFieldTarget(target)) {
3561
+ return;
3562
+ }
3563
+ const key = event.key.toLowerCase();
3564
+ if (isGlobalSelectionBufferTarget && (key === "backspace" || key === "delete")) {
3565
+ event.preventDefault();
3566
+ event.stopPropagation();
3567
+ dispatch({ type: "CLEAR_DOCUMENT_CONTENT" });
3568
+ clearGlobalSelection();
3569
+ return;
3570
+ }
3571
+ const hasModifier = event.metaKey || event.ctrlKey;
3572
+ if (!hasModifier) {
3573
+ return;
3574
+ }
3575
+ if (key === "a") {
3576
+ event.preventDefault();
3577
+ event.stopPropagation();
3578
+ requestAnimationFrame(() => {
3579
+ const activeElement = window.document.activeElement;
3580
+ if (activeElement instanceof HTMLElement && activeElement.isContentEditable) {
3581
+ activeElement.blur();
3582
+ }
3583
+ selectEntireDocument(rootRef.current, selectionBufferRef.current);
3584
+ setGlobalSelectionActive(true);
3585
+ });
3586
+ return;
3587
+ }
3588
+ if (handleHistoryShortcut(event)) {
3589
+ return;
3590
+ }
3591
+ }, [
3592
+ dispatch,
3593
+ handleHistoryShortcut,
3594
+ setGlobalSelectionActive
3595
+ ]);
3596
+ const handleMouseDownCapture = React.useCallback((event) => {
3597
+ if (!globalSelectionActive) {
3598
+ return;
3599
+ }
3600
+ const target = event.target;
3601
+ const clickedToolbar = target == null ? void 0 : target.closest('[data-testid="toolbar"]');
3602
+ if (event.target !== selectionBufferRef.current && !clickedToolbar) {
3603
+ clearGlobalSelection();
3604
+ }
3605
+ }, [clearGlobalSelection, globalSelectionActive]);
3606
+ React.useEffect(() => {
3607
+ var _a;
3608
+ const editableRoots = ((_a = rootRef.current) == null ? void 0 : _a.querySelectorAll(
3609
+ '[data-testid^="page-body-"] [data-lexical-editor="true"]'
3610
+ )) ?? [];
3611
+ editableRoots.forEach((editableRoot) => {
3612
+ editableRoot.style.backgroundColor = globalSelectionActive ? GLOBAL_SELECTION_BACKGROUND : "";
3613
+ editableRoot.style.color = globalSelectionActive ? GLOBAL_SELECTION_FOREGROUND : "";
3614
+ editableRoot.style.caretColor = globalSelectionActive ? "transparent" : "";
3615
+ });
3616
+ }, [globalSelectionActive, document2.pages.length]);
3617
+ React.useEffect(() => {
3618
+ if (!captureHistoryShortcutsOnWindow) {
3619
+ return;
3620
+ }
3621
+ const handleWindowKeyDown = (event) => {
3622
+ const hasModifier = event.metaKey || event.ctrlKey;
3623
+ if (!hasModifier) {
3624
+ return;
3625
+ }
3626
+ handleHistoryShortcut(event);
3627
+ };
3628
+ window.addEventListener("keydown", handleWindowKeyDown, { capture: true });
3629
+ const handleWindowBeforeInput = (event) => {
3630
+ if (event.inputType === "historyUndo") {
3631
+ event.preventDefault();
3632
+ clearGlobalSelection();
3633
+ undo();
3634
+ } else if (event.inputType === "historyRedo") {
3635
+ event.preventDefault();
3636
+ clearGlobalSelection();
3637
+ redo();
3638
+ }
3639
+ };
3640
+ window.addEventListener("beforeinput", handleWindowBeforeInput, { capture: true });
3641
+ return () => {
3642
+ window.removeEventListener("keydown", handleWindowKeyDown, { capture: true });
3643
+ window.removeEventListener("beforeinput", handleWindowBeforeInput, { capture: true });
3644
+ };
3645
+ }, [captureHistoryShortcutsOnWindow, clearGlobalSelection, handleHistoryShortcut, redo, undo]);
3646
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3647
+ "div",
3648
+ {
3649
+ ref: rootRef,
3650
+ className: `lex4-editor flex flex-col h-full ${className ?? ""}`,
3651
+ "data-testid": "lex4-editor",
3652
+ "data-global-selection-active": globalSelectionActive ? "true" : "false",
3653
+ onKeyDownCapture: handleKeyDownCapture,
3654
+ onMouseDownCapture: handleMouseDownCapture,
3655
+ children: [
3656
+ /* @__PURE__ */ jsxRuntime.jsx(
3657
+ "textarea",
3658
+ {
3659
+ ref: selectionBufferRef,
3660
+ "aria-hidden": "true",
3661
+ "data-testid": "global-selection-buffer",
3662
+ readOnly: true,
3663
+ tabIndex: -1,
3664
+ className: "pointer-events-none fixed -left-[9999px] top-0 h-0 w-0 opacity-0"
3665
+ }
3666
+ ),
3667
+ /* @__PURE__ */ jsxRuntime.jsx(Toolbar, {}),
3668
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden bg-gray-200", children: [
3669
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1 overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsx(DocumentView, {}) }),
3670
+ /* @__PURE__ */ jsxRuntime.jsx(HistorySidebar, {})
3671
+ ] })
3672
+ ]
3673
+ }
3674
+ );
3675
+ };
3676
+ const Lex4Editor = ({
3677
+ captureHistoryShortcutsOnWindow = true,
3678
+ initialDocument,
3679
+ onDocumentChange,
3680
+ className
3681
+ }) => {
3682
+ return /* @__PURE__ */ jsxRuntime.jsx(
3683
+ DocumentProvider,
3684
+ {
3685
+ initialDocument,
3686
+ onDocumentChange,
3687
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3688
+ EditorChrome,
3689
+ {
3690
+ captureHistoryShortcutsOnWindow,
3691
+ className
3692
+ }
3693
+ )
3694
+ }
3695
+ );
3696
+ };
3697
+ function useOverflowDetection(bodyHeight, onOverflow, debounceMs = 100) {
3698
+ const timerRef = React.useRef(null);
3699
+ const observerRef = React.useRef(null);
3700
+ const check = React.useCallback(
3701
+ (el) => {
3702
+ if (!onOverflow) return;
3703
+ if (el.scrollHeight > bodyHeight + 2) {
3704
+ onOverflow();
3705
+ }
3706
+ },
3707
+ [bodyHeight, onOverflow]
3708
+ );
3709
+ const attachRef = React.useCallback(
3710
+ (el) => {
3711
+ if (observerRef.current) {
3712
+ observerRef.current.disconnect();
3713
+ observerRef.current = null;
3714
+ }
3715
+ if (!el || !onOverflow) return;
3716
+ observerRef.current = new ResizeObserver(() => {
3717
+ if (timerRef.current) clearTimeout(timerRef.current);
3718
+ timerRef.current = setTimeout(() => check(el), debounceMs);
3719
+ });
3720
+ observerRef.current.observe(el);
3721
+ },
3722
+ [check, debounceMs, onOverflow]
3723
+ );
3724
+ React.useEffect(() => {
3725
+ return () => {
3726
+ if (timerRef.current) clearTimeout(timerRef.current);
3727
+ if (observerRef.current) observerRef.current.disconnect();
3728
+ };
3729
+ }, []);
3730
+ return { attachRef };
3731
+ }
3732
+ function useHeaderFooter(maxHeight, onHeightChange) {
3733
+ const observerRef = React.useRef(null);
3734
+ const lastHeightRef = React.useRef(0);
3735
+ const attachRef = React.useCallback(
3736
+ (el) => {
3737
+ if (observerRef.current) {
3738
+ observerRef.current.disconnect();
3739
+ observerRef.current = null;
3740
+ }
3741
+ if (!el) return;
3742
+ const measure = () => {
3743
+ const height = Math.min(el.scrollHeight, maxHeight);
3744
+ if (height !== lastHeightRef.current) {
3745
+ lastHeightRef.current = height;
3746
+ onHeightChange(height);
3747
+ }
3748
+ };
3749
+ observerRef.current = new ResizeObserver(measure);
3750
+ observerRef.current.observe(el);
3751
+ measure();
3752
+ },
3753
+ [maxHeight, onHeightChange]
3754
+ );
3755
+ React.useEffect(() => {
3756
+ return () => {
3757
+ if (observerRef.current) observerRef.current.disconnect();
3758
+ };
3759
+ }, []);
3760
+ return { attachRef };
3761
+ }
3762
+ exports.A4_HEIGHT_MM = A4_HEIGHT_MM;
3763
+ exports.A4_HEIGHT_PX = A4_HEIGHT_PX;
3764
+ exports.A4_WIDTH_MM = A4_WIDTH_MM;
3765
+ exports.A4_WIDTH_PX = A4_WIDTH_PX;
3766
+ exports.Lex4Editor = Lex4Editor;
3767
+ exports.MAX_FOOTER_HEIGHT_PX = MAX_FOOTER_HEIGHT_PX;
3768
+ exports.MAX_HEADER_HEIGHT_PX = MAX_HEADER_HEIGHT_PX;
3769
+ exports.createEmptyDocument = createEmptyDocument;
3770
+ exports.createEmptyPage = createEmptyPage;
3771
+ exports.useHeaderFooter = useHeaderFooter;
3772
+ exports.useOverflowDetection = useOverflowDetection;
3773
+ exports.usePagination = usePagination;
3774
+ //# sourceMappingURL=lex4-editor.cjs.map