@stll/folio-core 0.1.3 → 0.3.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 (85) hide show
  1. package/dist/ai-edits/clean-text.d.ts +21 -1
  2. package/dist/ai-edits/clean-text.js +69 -1
  3. package/dist/ai-edits/headless.d.ts +49 -22
  4. package/dist/ai-edits/headless.js +101 -77
  5. package/dist/ai-edits/index.d.ts +3 -1
  6. package/dist/ai-edits/index.js +3 -1
  7. package/dist/ai-edits/read.d.ts +43 -0
  8. package/dist/ai-edits/read.js +91 -0
  9. package/dist/docx/index.d.ts +2 -0
  10. package/dist/docx/index.js +2 -0
  11. package/dist/docx/parser.js +1 -1
  12. package/dist/docx/wrapTypes.d.ts +8 -1
  13. package/dist/docx/wrapTypes.js +0 -8
  14. package/dist/i18n/messages/catalogs.gen.d.ts +9899 -0
  15. package/dist/i18n/messages/catalogs.gen.js +9831 -0
  16. package/dist/i18n/messages/messages.gen.d.ts +585 -0
  17. package/dist/i18n/messages/messages.gen.js +0 -0
  18. package/dist/i18n/messages.d.ts +13 -0
  19. package/dist/i18n/messages.js +39 -0
  20. package/dist/layout-bridge/cellSelectionHighlight.d.ts +18 -0
  21. package/dist/layout-bridge/cellSelectionHighlight.js +36 -0
  22. package/dist/layout-bridge/headerFooterLayout.d.ts +50 -0
  23. package/dist/layout-bridge/headerFooterLayout.js +216 -0
  24. package/dist/layout-bridge/measuring/measureBlocksPipeline.d.ts +33 -0
  25. package/dist/layout-bridge/measuring/measureBlocksPipeline.js +0 -0
  26. package/dist/layout-bridge/sectionColumns.d.ts +11 -0
  27. package/dist/layout-bridge/sectionColumns.js +21 -0
  28. package/dist/layout-bridge/tableInsertHover.d.ts +42 -0
  29. package/dist/layout-bridge/tableInsertHover.js +102 -0
  30. package/dist/layout-engine/index.d.ts +2 -1
  31. package/dist/layout-engine/index.js +2 -1
  32. package/dist/layout-engine/measure/measureParagraph.js +40 -5
  33. package/dist/layout-engine/pmPageIndex.d.ts +24 -0
  34. package/dist/layout-engine/pmPageIndex.js +37 -0
  35. package/dist/layout-painter/imageLayout.d.ts +101 -0
  36. package/dist/layout-painter/imageLayout.js +161 -0
  37. package/dist/layout-painter/renderPage.d.ts +16 -1
  38. package/dist/layout-painter/renderPage.js +28 -1
  39. package/dist/layout-painter/renderParagraph.js +1 -1
  40. package/dist/layout-painter/sdtBoundary.d.ts +24 -1
  41. package/dist/layout-painter/sdtBoundary.js +40 -1
  42. package/dist/managers/AutoSaveManager.d.ts +89 -0
  43. package/dist/managers/AutoSaveManager.js +279 -0
  44. package/dist/managers/ClipboardManager.d.ts +31 -0
  45. package/dist/managers/ClipboardManager.js +147 -0
  46. package/dist/paged-layout/rangeProjection.d.ts +23 -1
  47. package/dist/paged-layout/rangeProjection.js +34 -1
  48. package/dist/prosemirror/cellDragSelection.d.ts +34 -0
  49. package/dist/prosemirror/cellDragSelection.js +69 -0
  50. package/dist/prosemirror/commands/sectionBreak.d.ts +15 -0
  51. package/dist/prosemirror/commands/sectionBreak.js +54 -0
  52. package/dist/prosemirror/commentIdAllocator.d.ts +32 -0
  53. package/dist/prosemirror/commentIdAllocator.js +35 -0
  54. package/dist/prosemirror/commentOps.d.ts +35 -0
  55. package/dist/prosemirror/commentOps.js +104 -0
  56. package/dist/prosemirror/conversion/toProseDoc.js +29 -6
  57. package/dist/prosemirror/extensions/index.d.ts +3 -0
  58. package/dist/prosemirror/extensions/index.js +3 -0
  59. package/dist/prosemirror/imageCommit.d.ts +42 -0
  60. package/dist/prosemirror/imageCommit.js +118 -0
  61. package/dist/prosemirror/paraText.d.ts +26 -0
  62. package/dist/prosemirror/paraText.js +73 -0
  63. package/dist/prosemirror/plugins/templateDirectives.d.ts +21 -1
  64. package/dist/prosemirror/plugins/templateDirectives.js +40 -1
  65. package/dist/prosemirror/queries.d.ts +46 -0
  66. package/dist/prosemirror/queries.js +74 -0
  67. package/dist/prosemirror/styles/styleResolver.d.ts +43 -1
  68. package/dist/prosemirror/styles/styleResolver.js +34 -0
  69. package/dist/prosemirror/tableResize.d.ts +49 -0
  70. package/dist/prosemirror/tableResize.js +162 -0
  71. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +106 -0
  72. package/dist/prosemirror/utils/extractTrackedChanges.js +379 -0
  73. package/dist/prosemirror/utils/visualLineNavigation.d.ts +27 -0
  74. package/dist/prosemirror/utils/visualLineNavigation.js +217 -0
  75. package/dist/server.d.ts +2 -1
  76. package/dist/style-engine/index.d.ts +2 -2
  77. package/dist/style-engine/styleEngine.d.ts +14 -1
  78. package/dist/style-engine/styleEngine.js +15 -0
  79. package/dist/utils/colorResolver.d.ts +8 -1
  80. package/dist/utils/colorResolver.js +12 -1
  81. package/dist/utils/findVerticalScrollParent.d.ts +20 -0
  82. package/dist/utils/findVerticalScrollParent.js +30 -0
  83. package/dist/utils/fontResolver.d.ts +22 -3
  84. package/dist/utils/fontResolver.js +216 -31
  85. package/package.json +10 -1
@@ -0,0 +1,279 @@
1
+ import { Subscribable } from "./Subscribable.js";
2
+ //#region src/managers/AutoSaveManager.ts
3
+ const DEFAULT_STORAGE_KEY = "docx-editor-autosave";
4
+ const DEFAULT_INTERVAL = 3e4;
5
+ const DEFAULT_MAX_AGE = 1440 * 60 * 1e3;
6
+ const DEFAULT_DEBOUNCE_DELAY = 2e3;
7
+ const SAVE_VERSION = 1;
8
+ function isLocalStorageAvailable() {
9
+ try {
10
+ const testKey = "__docx_editor_test__";
11
+ localStorage.setItem(testKey, "test");
12
+ localStorage.removeItem(testKey);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+ function serializeForStorage(document) {
19
+ return JSON.stringify({
20
+ ...document,
21
+ originalBuffer: null
22
+ });
23
+ }
24
+ function isDocumentLike(value) {
25
+ return typeof value === "object" && value !== null && "package" in value;
26
+ }
27
+ function parseSavedData(json) {
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(json);
31
+ } catch {
32
+ return null;
33
+ }
34
+ if (typeof parsed !== "object" || parsed === null) return null;
35
+ if (!("document" in parsed) || !("savedAt" in parsed) || !("version" in parsed)) return null;
36
+ const { document, savedAt, version } = parsed;
37
+ if (typeof savedAt !== "string" || typeof version !== "number") return null;
38
+ if (!isDocumentLike(document)) return null;
39
+ return {
40
+ document,
41
+ savedAt,
42
+ version
43
+ };
44
+ }
45
+ function isStale(savedAt, maxAge) {
46
+ const savedTime = new Date(savedAt).getTime();
47
+ return Date.now() - savedTime > maxAge;
48
+ }
49
+ var AutoSaveManager = class extends Subscribable {
50
+ storageKey;
51
+ interval;
52
+ maxAge;
53
+ saveOnChange;
54
+ debounceDelay;
55
+ onSaveCallback;
56
+ onErrorCallback;
57
+ onRecoveryAvailableCallback;
58
+ storageAvailable;
59
+ currentDocument = null;
60
+ lastSavedJson = null;
61
+ intervalTimer = null;
62
+ debounceTimer = null;
63
+ status = "idle";
64
+ lastSaveTime = null;
65
+ hasRecoveryData = false;
66
+ isEnabled;
67
+ constructor(options = {}) {
68
+ super({
69
+ status: "idle",
70
+ lastSaveTime: null,
71
+ hasRecoveryData: false,
72
+ isEnabled: true
73
+ });
74
+ this.storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
75
+ this.interval = options.interval ?? DEFAULT_INTERVAL;
76
+ this.maxAge = options.maxAge ?? DEFAULT_MAX_AGE;
77
+ this.saveOnChange = options.saveOnChange ?? true;
78
+ this.debounceDelay = options.debounceDelay ?? DEFAULT_DEBOUNCE_DELAY;
79
+ this.onSaveCallback = options.onSave;
80
+ this.onErrorCallback = options.onError;
81
+ this.onRecoveryAvailableCallback = options.onRecoveryAvailable;
82
+ this.isEnabled = true;
83
+ this.storageAvailable = isLocalStorageAvailable();
84
+ this.checkRecoveryData();
85
+ }
86
+ /** Update the current document. Triggers a debounced save if enabled. */
87
+ onDocumentChanged(document) {
88
+ this.currentDocument = document;
89
+ if (this.isEnabled && this.saveOnChange && document && this.storageAvailable) this.debounceSave();
90
+ }
91
+ /** Manually trigger a save. */
92
+ save() {
93
+ if (!this.storageAvailable) {
94
+ this.onErrorCallback?.(/* @__PURE__ */ new Error("localStorage is not available"));
95
+ return false;
96
+ }
97
+ const doc = this.currentDocument;
98
+ if (!doc) return false;
99
+ this.updateStatus("saving");
100
+ try {
101
+ const serialized = serializeForStorage(doc);
102
+ if (serialized === this.lastSavedJson) {
103
+ this.updateStatus("saved");
104
+ return true;
105
+ }
106
+ this.persistToStorage(doc);
107
+ this.lastSavedJson = serialized;
108
+ const saveTime = /* @__PURE__ */ new Date();
109
+ this.lastSaveTime = saveTime;
110
+ this.updateStatus("saved");
111
+ this.onSaveCallback?.(saveTime);
112
+ return true;
113
+ } catch (error) {
114
+ this.updateStatus("error");
115
+ this.onErrorCallback?.(error instanceof Error ? error : new Error(String(error)));
116
+ return false;
117
+ }
118
+ }
119
+ /** Clear auto-saved data from storage. */
120
+ clear() {
121
+ if (!this.storageAvailable) return;
122
+ try {
123
+ localStorage.removeItem(this.storageKey);
124
+ this.hasRecoveryData = false;
125
+ this.lastSavedJson = null;
126
+ this.emitSnapshot();
127
+ } catch (error) {
128
+ this.onErrorCallback?.(error instanceof Error ? error : new Error(String(error)));
129
+ }
130
+ }
131
+ /** Get recovery data from storage. */
132
+ getRecoveryData() {
133
+ if (!this.storageAvailable) return null;
134
+ const savedJson = localStorage.getItem(this.storageKey);
135
+ if (!savedJson) return null;
136
+ const savedData = parseSavedData(savedJson);
137
+ if (!savedData) return null;
138
+ if (isStale(savedData.savedAt, this.maxAge)) {
139
+ this.clear();
140
+ return null;
141
+ }
142
+ return savedData;
143
+ }
144
+ /** Accept recovery and return the document. */
145
+ acceptRecovery() {
146
+ const data = this.getRecoveryData();
147
+ if (!data) return null;
148
+ this.hasRecoveryData = false;
149
+ this.emitSnapshot();
150
+ return data.document;
151
+ }
152
+ /** Dismiss recovery and clear saved data. */
153
+ dismissRecovery() {
154
+ this.clear();
155
+ this.hasRecoveryData = false;
156
+ this.emitSnapshot();
157
+ }
158
+ /** Enable auto-save and start the interval timer. */
159
+ enable() {
160
+ this.isEnabled = true;
161
+ this.startInterval();
162
+ this.emitSnapshot();
163
+ }
164
+ /** Disable auto-save and stop all timers. */
165
+ disable() {
166
+ this.isEnabled = false;
167
+ this.stopTimers();
168
+ this.emitSnapshot();
169
+ }
170
+ /** Start the interval timer. Call after enabling or on init. */
171
+ startInterval() {
172
+ this.stopTimers();
173
+ if (!this.isEnabled || !this.storageAvailable) return;
174
+ this.intervalTimer = setInterval(() => {
175
+ this.save();
176
+ }, this.interval);
177
+ }
178
+ /** Save synchronously on destroy (best-effort). */
179
+ destroy() {
180
+ this.stopTimers();
181
+ if (this.isEnabled && this.currentDocument && this.storageAvailable) try {
182
+ this.persistToStorage(this.currentDocument);
183
+ } catch (error) {
184
+ this.onErrorCallback?.(error instanceof Error ? error : new Error(String(error)));
185
+ }
186
+ }
187
+ checkRecoveryData() {
188
+ if (!this.storageAvailable) return;
189
+ const data = this.getRecoveryData();
190
+ if (data) {
191
+ this.hasRecoveryData = true;
192
+ this.emitSnapshot();
193
+ this.onRecoveryAvailableCallback?.(data);
194
+ }
195
+ }
196
+ persistToStorage(document) {
197
+ const dataToSave = {
198
+ document: {
199
+ ...document,
200
+ originalBuffer: null
201
+ },
202
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
203
+ version: SAVE_VERSION
204
+ };
205
+ localStorage.setItem(this.storageKey, JSON.stringify(dataToSave));
206
+ }
207
+ debounceSave() {
208
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
209
+ this.debounceTimer = setTimeout(() => {
210
+ this.save();
211
+ }, this.debounceDelay);
212
+ }
213
+ stopTimers() {
214
+ if (this.intervalTimer) {
215
+ clearInterval(this.intervalTimer);
216
+ this.intervalTimer = null;
217
+ }
218
+ if (this.debounceTimer) {
219
+ clearTimeout(this.debounceTimer);
220
+ this.debounceTimer = null;
221
+ }
222
+ }
223
+ updateStatus(status) {
224
+ this.status = status;
225
+ this.emitSnapshot();
226
+ }
227
+ emitSnapshot() {
228
+ this.setSnapshot({
229
+ status: this.status,
230
+ lastSaveTime: this.lastSaveTime,
231
+ hasRecoveryData: this.hasRecoveryData,
232
+ isEnabled: this.isEnabled
233
+ });
234
+ }
235
+ };
236
+ /** Format last save time for display. */
237
+ function formatLastSaveTime(date) {
238
+ if (!date) return "Never";
239
+ const diffMs = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
240
+ const diffSec = Math.floor(diffMs / 1e3);
241
+ const diffMin = Math.floor(diffSec / 60);
242
+ const diffHour = Math.floor(diffMin / 60);
243
+ if (diffSec < 10) return "Just now";
244
+ if (diffSec < 60) return `${diffSec} seconds ago`;
245
+ if (diffMin < 60) return `${diffMin} minute${diffMin === 1 ? "" : "s"} ago`;
246
+ if (diffHour < 24) return `${diffHour} hour${diffHour === 1 ? "" : "s"} ago`;
247
+ return date.toLocaleDateString();
248
+ }
249
+ /** Get auto-save status label. */
250
+ function getAutoSaveStatusLabel(status) {
251
+ return {
252
+ idle: "Ready",
253
+ saving: "Saving...",
254
+ saved: "Saved",
255
+ error: "Save failed"
256
+ }[status];
257
+ }
258
+ /** Get storage size used by auto-save (bytes). */
259
+ function getAutoSaveStorageSize(storageKey = DEFAULT_STORAGE_KEY) {
260
+ try {
261
+ const data = localStorage.getItem(storageKey);
262
+ if (!data) return 0;
263
+ return new Blob([data]).size;
264
+ } catch {
265
+ return 0;
266
+ }
267
+ }
268
+ /** Format storage size for display. */
269
+ function formatStorageSize(bytes) {
270
+ if (bytes < 1024) return `${bytes} B`;
271
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
272
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
273
+ }
274
+ /** Check whether auto-save is supported. */
275
+ function isAutoSaveSupported() {
276
+ return isLocalStorageAvailable();
277
+ }
278
+ //#endregion
279
+ export { AutoSaveManager, formatLastSaveTime, formatStorageSize, getAutoSaveStatusLabel, getAutoSaveStorageSize, isAutoSaveSupported };
@@ -0,0 +1,31 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+
3
+ //#region src/managers/ClipboardManager.d.ts
4
+ /** Selection data for clipboard operations. */
5
+ type ClipboardSelection = {
6
+ text: string;
7
+ runs: document_d_exports.Run[];
8
+ startParagraphIndex: number;
9
+ startRunIndex: number;
10
+ startOffset: number;
11
+ endParagraphIndex: number;
12
+ endRunIndex: number;
13
+ endOffset: number;
14
+ isMultiParagraph: boolean;
15
+ };
16
+ /**
17
+ * Convert a CSS color string (rgb/rgba/hex) to a 6-char uppercase hex string.
18
+ *
19
+ * NOTE: This parses CSS color *strings*, unlike a numeric-component
20
+ * `rgbToHex(r, g, b)` helper.
21
+ */
22
+ declare function cssColorToHex(color: string): string | null;
23
+ /** Extract formatting from an HTML element's computed styles. */
24
+ declare function extractFormattingFromElement(element: HTMLElement): document_d_exports.TextFormatting | undefined;
25
+ /** Get selected runs from the current DOM selection. */
26
+ declare function getSelectionRuns(): document_d_exports.Run[];
27
+ /** Create a ClipboardSelection from the current DOM selection. */
28
+ declare function createSelectionFromDOM(): ClipboardSelection | null;
29
+ declare const rgbToHex: typeof cssColorToHex;
30
+ //#endregion
31
+ export { ClipboardSelection, createSelectionFromDOM, cssColorToHex, extractFormattingFromElement, getSelectionRuns, rgbToHex };
@@ -0,0 +1,147 @@
1
+ //#region src/managers/ClipboardManager.ts
2
+ /**
3
+ * Convert a CSS color string (rgb/rgba/hex) to a 6-char uppercase hex string.
4
+ *
5
+ * NOTE: This parses CSS color *strings*, unlike a numeric-component
6
+ * `rgbToHex(r, g, b)` helper.
7
+ */
8
+ function cssColorToHex(color) {
9
+ if (!color || color === "transparent" || color === "inherit") return null;
10
+ if (color.startsWith("#")) return color.slice(1).toUpperCase();
11
+ const rgbMatch = /rgba?\((?<r>\d+),\s*(?<g>\d+),\s*(?<b>\d+)/u.exec(color);
12
+ if (rgbMatch?.groups) {
13
+ const { r, g, b } = rgbMatch.groups;
14
+ if (r === void 0 || g === void 0 || b === void 0) return null;
15
+ const rHex = Number.parseInt(r, 10).toString(16).padStart(2, "0");
16
+ const gHex = Number.parseInt(g, 10).toString(16).padStart(2, "0");
17
+ const bHex = Number.parseInt(b, 10).toString(16).padStart(2, "0");
18
+ return (rHex + gHex + bHex).toUpperCase();
19
+ }
20
+ return null;
21
+ }
22
+ /** Extract formatting from an HTML element's computed styles. */
23
+ function extractFormattingFromElement(element) {
24
+ const style = window.getComputedStyle(element);
25
+ const formatting = {};
26
+ if (style.fontWeight === "bold" || Number.parseInt(style.fontWeight, 10) >= 700) formatting.bold = true;
27
+ if (style.fontStyle === "italic") formatting.italic = true;
28
+ const textDecoration = style.textDecoration || style.textDecorationLine || "";
29
+ if (textDecoration.includes("underline")) formatting.underline = { style: "single" };
30
+ if (textDecoration.includes("line-through")) formatting.strike = true;
31
+ const fontSize = Number.parseFloat(style.fontSize);
32
+ if (!Number.isNaN(fontSize) && fontSize > 0) formatting.fontSize = Math.round(fontSize / 1.333 * 2);
33
+ const fontFamily = style.fontFamily.replace(/["']/gu, "").split(",")[0]?.trim();
34
+ if (fontFamily) formatting.fontFamily = { ascii: fontFamily };
35
+ const color = style.color;
36
+ if (color && color !== "rgb(0, 0, 0)") {
37
+ const hex = cssColorToHex(color);
38
+ if (hex) formatting.color = { rgb: hex };
39
+ }
40
+ const bgColor = style.backgroundColor;
41
+ if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
42
+ const hex = cssColorToHex(bgColor);
43
+ if (hex) formatting.shading = { fill: { rgb: hex } };
44
+ }
45
+ return Object.keys(formatting).length > 0 ? formatting : void 0;
46
+ }
47
+ /** Get selected text from a run element, considering partial selection. */
48
+ function getSelectedTextFromRun(runEl, range) {
49
+ const runRange = document.createRange();
50
+ runRange.selectNodeContents(runEl);
51
+ const startInRun = range.compareBoundaryPoints(Range.START_TO_START, runRange) >= 0 && range.compareBoundaryPoints(Range.START_TO_END, runRange) <= 0;
52
+ const endInRun = range.compareBoundaryPoints(Range.END_TO_START, runRange) >= 0 && range.compareBoundaryPoints(Range.END_TO_END, runRange) <= 0;
53
+ if (startInRun && endInRun) return range.toString();
54
+ if (startInRun) {
55
+ const tempRange = document.createRange();
56
+ tempRange.setStart(range.startContainer, range.startOffset);
57
+ tempRange.setEnd(runRange.endContainer, runRange.endOffset);
58
+ return tempRange.toString();
59
+ }
60
+ if (endInRun) {
61
+ const tempRange = document.createRange();
62
+ tempRange.setStart(runRange.startContainer, runRange.startOffset);
63
+ tempRange.setEnd(range.endContainer, range.endOffset);
64
+ return tempRange.toString();
65
+ }
66
+ if (range.intersectsNode(runEl)) return runEl.textContent ?? "";
67
+ return "";
68
+ }
69
+ /** Find the paragraph element containing a node. */
70
+ function findParagraphElement(node) {
71
+ let current = node;
72
+ while (current) {
73
+ if (current instanceof HTMLElement && current.hasAttribute("data-paragraph-index")) return current;
74
+ current = current.parentNode;
75
+ }
76
+ return null;
77
+ }
78
+ /** Get selected runs from the current DOM selection. */
79
+ function getSelectionRuns() {
80
+ const selection = window.getSelection();
81
+ if (!selection || selection.isCollapsed) return [];
82
+ const runs = [];
83
+ const range = selection.getRangeAt(0);
84
+ const container = range.commonAncestorContainer;
85
+ const containerElement = container instanceof HTMLElement ? container : container.parentElement;
86
+ if (!containerElement) return runs;
87
+ const runElements = containerElement.querySelectorAll(".docx-run");
88
+ for (const runEl of runElements) {
89
+ if (!(runEl instanceof HTMLElement) || !range.intersectsNode(runEl)) continue;
90
+ const text = getSelectedTextFromRun(runEl, range);
91
+ if (!text) continue;
92
+ const formatting = extractFormattingFromElement(runEl);
93
+ const run = formatting ? {
94
+ type: "run",
95
+ formatting,
96
+ content: [{
97
+ type: "text",
98
+ text
99
+ }]
100
+ } : {
101
+ type: "run",
102
+ content: [{
103
+ type: "text",
104
+ text
105
+ }]
106
+ };
107
+ runs.push(run);
108
+ }
109
+ if (runs.length === 0) {
110
+ const selectedText = selection.toString();
111
+ if (selectedText) runs.push({
112
+ type: "run",
113
+ content: [{
114
+ type: "text",
115
+ text: selectedText
116
+ }]
117
+ });
118
+ }
119
+ return runs;
120
+ }
121
+ /** Create a ClipboardSelection from the current DOM selection. */
122
+ function createSelectionFromDOM() {
123
+ const selection = window.getSelection();
124
+ if (!selection || selection.isCollapsed) return null;
125
+ const runs = getSelectionRuns();
126
+ if (runs.length === 0) return null;
127
+ const text = selection.toString();
128
+ const range = selection.getRangeAt(0);
129
+ const startPara = findParagraphElement(range.startContainer);
130
+ const endPara = findParagraphElement(range.endContainer);
131
+ const startParagraphIndex = startPara ? Number.parseInt(startPara.getAttribute("data-paragraph-index") ?? "0", 10) : 0;
132
+ const endParagraphIndex = endPara ? Number.parseInt(endPara.getAttribute("data-paragraph-index") ?? "0", 10) : 0;
133
+ return {
134
+ text,
135
+ runs,
136
+ startParagraphIndex,
137
+ startRunIndex: 0,
138
+ startOffset: range.startOffset,
139
+ endParagraphIndex,
140
+ endRunIndex: 0,
141
+ endOffset: range.endOffset,
142
+ isMultiParagraph: startParagraphIndex !== endParagraphIndex
143
+ };
144
+ }
145
+ const rgbToHex = cssColorToHex;
146
+ //#endregion
147
+ export { createSelectionFromDOM, cssColorToHex, extractFormattingFromElement, getSelectionRuns, rgbToHex };
@@ -25,6 +25,28 @@ type RangeProjectionContext = {
25
25
  * left margin is fixed, so this value holds steady as text is edited.
26
26
  */
27
27
  declare const measureContentLeft: (pagesContainer: HTMLElement, zoom: number) => number | null;
28
+ /**
29
+ * One page's body-content vertical extent in container space. `.layout-page-content`
30
+ * is inset by the page margins, so its box excludes the header/footer and the
31
+ * inter-page gap — exactly the region a gutter rail is allowed to paint over.
32
+ */
33
+ type PageContentBand = {
34
+ /** 0-based page index (from `data-page-number` minus one). */pageIndex: number; /** Top of the body content area, container space. */
35
+ top: number; /** Bottom of the body content area, container space. */
36
+ bottom: number;
37
+ };
38
+ /**
39
+ * Left-margin geometry the block-directive rails need in one measurement pass:
40
+ * the text-column left edge (rail anchor), the usable left-margin width (gutter
41
+ * budget), and each page's body-content band (so a rail can be clipped per page
42
+ * instead of running across page breaks). Measured O(pages), never per-range.
43
+ */
44
+ type DirectiveGutterGeometry = {
45
+ /** Text-column left edge, container space. Rails anchor here. */contentLeft: number; /** Usable left-margin width: `contentLeft - pageLeftEdge`. Drives the budget. */
46
+ marginWidth: number; /** Per-page body-content vertical bands, in document order. */
47
+ pageBands: PageContentBand[];
48
+ };
49
+ declare const measureDirectiveGutter: (pagesContainer: HTMLElement, zoom: number) => DirectiveGutterGeometry | null;
28
50
  /**
29
51
  * Computed text style of the painted run at a PM position. Overlays
30
52
  * that paint substituted text over the pages (template fill preview,
@@ -48,4 +70,4 @@ type ProjectedRunFont = {
48
70
  declare const collectRunFontsAtPmPositions: (pagesContainer: HTMLElement, positions: readonly number[]) => Map<number, ProjectedRunFont>;
49
71
  declare const projectRangesToRects: <T extends ProjectableRange>(ranges: readonly T[], ctx: RangeProjectionContext) => Promise<ProjectedRange<T>[]>;
50
72
  //#endregion
51
- export { ProjectableRange, ProjectedRange, ProjectedRunFont, RangeProjectionContext, collectRunFontsAtPmPositions, measureContentLeft, projectRangesToRects };
73
+ export { DirectiveGutterGeometry, PageContentBand, ProjectableRange, ProjectedRange, ProjectedRunFont, RangeProjectionContext, collectRunFontsAtPmPositions, measureContentLeft, measureDirectiveGutter, projectRangesToRects };
@@ -77,6 +77,39 @@ const measureContentLeft = (pagesContainer, zoom) => {
77
77
  }
78
78
  return Number.isFinite(min) ? (min - overlayLeft) / zoom : null;
79
79
  };
80
+ const measureDirectiveGutter = (pagesContainer, zoom) => {
81
+ if (zoom <= 0) return null;
82
+ const overlay = pagesContainer.parentElement?.querySelector("[data-testid=\"selection-overlay\"]");
83
+ const pages = pagesContainer.querySelectorAll(".layout-page");
84
+ if (!overlay || pages.length === 0) return null;
85
+ const overlayRect = overlay.getBoundingClientRect();
86
+ const pageBands = [];
87
+ let contentLeft = null;
88
+ let marginWidth = null;
89
+ for (const [domIndex, page] of pages.entries()) {
90
+ const contentEl = page.querySelector(".layout-page-content");
91
+ if (!contentEl) continue;
92
+ const pageNumberRaw = page.dataset["pageNumber"];
93
+ const pageIndex = pageNumberRaw ? Number(pageNumberRaw) - 1 : domIndex;
94
+ const contentRect = contentEl.getBoundingClientRect();
95
+ pageBands.push({
96
+ pageIndex,
97
+ top: (contentRect.top - overlayRect.top) / zoom,
98
+ bottom: (contentRect.bottom - overlayRect.top) / zoom
99
+ });
100
+ if (contentLeft === null) {
101
+ const pageRect = page.getBoundingClientRect();
102
+ contentLeft = (contentRect.left - overlayRect.left) / zoom;
103
+ marginWidth = Math.max(0, (contentRect.left - pageRect.left) / zoom);
104
+ }
105
+ }
106
+ if (contentLeft === null || marginWidth === null) return null;
107
+ return {
108
+ contentLeft,
109
+ marginWidth,
110
+ pageBands
111
+ };
112
+ };
80
113
  /**
81
114
  * Sample the painted-run text style at each position in a single walk
82
115
  * over the rendered run spans. Positions outside the rendered buffer
@@ -150,4 +183,4 @@ const projectRangesToRects = async (ranges, ctx) => {
150
183
  return result;
151
184
  };
152
185
  //#endregion
153
- export { collectRunFontsAtPmPositions, measureContentLeft, projectRangesToRects };
186
+ export { collectRunFontsAtPmPositions, measureContentLeft, measureDirectiveGutter, projectRangesToRects };
@@ -0,0 +1,34 @@
1
+ import { EditorView } from "prosemirror-view";
2
+
3
+ //#region src/prosemirror/cellDragSelection.d.ts
4
+ /**
5
+ * @deprecated No longer used. The tracker ignores pointer overflow so same-cell
6
+ * drags stay text selections. Kept only for backward-compatible imports; will be
7
+ * removed in a future major.
8
+ */
9
+ declare const CELL_SELECT_OVERFLOW_PX = 5;
10
+ /**
11
+ * Walk up from a PM position to the enclosing `tableCell`/`tableHeader` and
12
+ * return its `before(d)` position (what `CellSelection` resolves against), or
13
+ * null when the position isn't inside a table cell.
14
+ */
15
+ declare function findCellPosFromPmPos(view: EditorView, pmPos: number): number | null;
16
+ /** Dispatch a `CellSelection` spanning the two cell positions. */
17
+ declare function applyCellSelection(view: EditorView, anchorCellPos: number, headCellPos: number): boolean;
18
+ /**
19
+ * Per-gesture state machine for cell-drag selection. Create one per pointer
20
+ * handler; drive it from mousedown/mousemove/mouseup.
21
+ */
22
+ type CellDragTracker = {
23
+ /** mousedown: record the cell under the press (or null when outside a table). */begin(cellPos: number | null): void;
24
+ /**
25
+ * mousemove: returns true when it applied a `CellSelection` and the caller
26
+ * should NOT also run its text-drag selection for this move.
27
+ */
28
+ update(view: EditorView, pmPos: number, clientX: number): boolean; /** mouseup / gesture end: clear the transient drag flags. */
29
+ end(): void; /** True while a cell drag is actively producing CellSelections. */
30
+ readonly isCellDragging: boolean;
31
+ };
32
+ declare function createCellDragTracker(): CellDragTracker;
33
+ //#endregion
34
+ export { CELL_SELECT_OVERFLOW_PX, CellDragTracker, applyCellSelection, createCellDragTracker, findCellPosFromPmPos };
@@ -0,0 +1,69 @@
1
+ import { CellSelection } from "prosemirror-tables";
2
+ //#region src/prosemirror/cellDragSelection.ts
3
+ /**
4
+ * @deprecated No longer used. The tracker ignores pointer overflow so same-cell
5
+ * drags stay text selections. Kept only for backward-compatible imports; will be
6
+ * removed in a future major.
7
+ */
8
+ const CELL_SELECT_OVERFLOW_PX = 5;
9
+ /**
10
+ * Walk up from a PM position to the enclosing `tableCell`/`tableHeader` and
11
+ * return its `before(d)` position (what `CellSelection` resolves against), or
12
+ * null when the position isn't inside a table cell.
13
+ */
14
+ function findCellPosFromPmPos(view, pmPos) {
15
+ try {
16
+ const $pos = view.state.doc.resolve(pmPos);
17
+ for (let d = $pos.depth; d > 0; d--) {
18
+ const node = $pos.node(d);
19
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") return $pos.before(d);
20
+ }
21
+ } catch {}
22
+ return null;
23
+ }
24
+ /** Dispatch a `CellSelection` spanning the two cell positions. */
25
+ function applyCellSelection(view, anchorCellPos, headCellPos) {
26
+ try {
27
+ const $a = view.state.doc.resolve(anchorCellPos);
28
+ const $h = view.state.doc.resolve(headCellPos);
29
+ view.dispatch(view.state.tr.setSelection(new CellSelection($a, $h)));
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+ function createCellDragTracker() {
36
+ let anchorCellPos = null;
37
+ let cellDragging = false;
38
+ return {
39
+ begin(cellPos) {
40
+ anchorCellPos = cellPos;
41
+ cellDragging = false;
42
+ },
43
+ update(view, pmPos, _clientX) {
44
+ if (anchorCellPos === null) return false;
45
+ if (cellDragging) {
46
+ const cur = findCellPosFromPmPos(view, pmPos);
47
+ if (cur !== null) {
48
+ applyCellSelection(view, anchorCellPos, cur);
49
+ return true;
50
+ }
51
+ }
52
+ const cur = findCellPosFromPmPos(view, pmPos);
53
+ if (cur !== null && cur !== anchorCellPos) {
54
+ cellDragging = true;
55
+ applyCellSelection(view, anchorCellPos, cur);
56
+ return true;
57
+ }
58
+ return false;
59
+ },
60
+ end() {
61
+ cellDragging = false;
62
+ },
63
+ get isCellDragging() {
64
+ return cellDragging;
65
+ }
66
+ };
67
+ }
68
+ //#endregion
69
+ export { CELL_SELECT_OVERFLOW_PX, applyCellSelection, createCellDragTracker, findCellPosFromPmPos };
@@ -0,0 +1,15 @@
1
+ import { Command } from "prosemirror-state";
2
+
3
+ //#region src/prosemirror/commands/sectionBreak.d.ts
4
+ /**
5
+ * Insert a "next page" section break at the cursor — starts a new section on a
6
+ * new page.
7
+ */
8
+ declare const insertSectionBreakNextPage: Command;
9
+ /**
10
+ * Insert a "continuous" section break at the cursor — starts a new section on
11
+ * the same page.
12
+ */
13
+ declare const insertSectionBreakContinuous: Command;
14
+ //#endregion
15
+ export { insertSectionBreakContinuous, insertSectionBreakNextPage };