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