@stll/folio-core 0.1.3 → 0.2.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 (75) hide show
  1. package/dist/docx/index.d.ts +2 -0
  2. package/dist/docx/index.js +2 -0
  3. package/dist/docx/wrapTypes.d.ts +8 -1
  4. package/dist/docx/wrapTypes.js +0 -8
  5. package/dist/i18n/messages/catalogs.gen.d.ts +2776 -0
  6. package/dist/i18n/messages/catalogs.gen.js +2742 -0
  7. package/dist/i18n/messages/messages.gen.d.ts +166 -0
  8. package/dist/i18n/messages/messages.gen.js +0 -0
  9. package/dist/i18n/messages.d.ts +13 -0
  10. package/dist/i18n/messages.js +39 -0
  11. package/dist/layout-bridge/cellSelectionHighlight.d.ts +18 -0
  12. package/dist/layout-bridge/cellSelectionHighlight.js +36 -0
  13. package/dist/layout-bridge/headerFooterLayout.d.ts +50 -0
  14. package/dist/layout-bridge/headerFooterLayout.js +216 -0
  15. package/dist/layout-bridge/measuring/measureBlocksPipeline.d.ts +33 -0
  16. package/dist/layout-bridge/measuring/measureBlocksPipeline.js +0 -0
  17. package/dist/layout-bridge/sectionColumns.d.ts +11 -0
  18. package/dist/layout-bridge/sectionColumns.js +21 -0
  19. package/dist/layout-bridge/tableInsertHover.d.ts +42 -0
  20. package/dist/layout-bridge/tableInsertHover.js +102 -0
  21. package/dist/layout-engine/index.d.ts +2 -1
  22. package/dist/layout-engine/index.js +2 -1
  23. package/dist/layout-engine/measure/measureParagraph.js +40 -5
  24. package/dist/layout-engine/pmPageIndex.d.ts +24 -0
  25. package/dist/layout-engine/pmPageIndex.js +37 -0
  26. package/dist/layout-painter/imageLayout.d.ts +101 -0
  27. package/dist/layout-painter/imageLayout.js +161 -0
  28. package/dist/layout-painter/renderPage.d.ts +16 -1
  29. package/dist/layout-painter/renderPage.js +28 -1
  30. package/dist/layout-painter/renderParagraph.js +1 -1
  31. package/dist/layout-painter/sdtBoundary.d.ts +24 -1
  32. package/dist/layout-painter/sdtBoundary.js +40 -1
  33. package/dist/managers/AutoSaveManager.d.ts +89 -0
  34. package/dist/managers/AutoSaveManager.js +279 -0
  35. package/dist/managers/ClipboardManager.d.ts +31 -0
  36. package/dist/managers/ClipboardManager.js +147 -0
  37. package/dist/paged-layout/rangeProjection.d.ts +23 -1
  38. package/dist/paged-layout/rangeProjection.js +34 -1
  39. package/dist/prosemirror/cellDragSelection.d.ts +34 -0
  40. package/dist/prosemirror/cellDragSelection.js +69 -0
  41. package/dist/prosemirror/commands/sectionBreak.d.ts +15 -0
  42. package/dist/prosemirror/commands/sectionBreak.js +54 -0
  43. package/dist/prosemirror/commentIdAllocator.d.ts +32 -0
  44. package/dist/prosemirror/commentIdAllocator.js +35 -0
  45. package/dist/prosemirror/commentOps.d.ts +35 -0
  46. package/dist/prosemirror/commentOps.js +104 -0
  47. package/dist/prosemirror/conversion/toProseDoc.js +29 -6
  48. package/dist/prosemirror/extensions/index.d.ts +3 -0
  49. package/dist/prosemirror/extensions/index.js +3 -0
  50. package/dist/prosemirror/imageCommit.d.ts +42 -0
  51. package/dist/prosemirror/imageCommit.js +118 -0
  52. package/dist/prosemirror/paraText.d.ts +26 -0
  53. package/dist/prosemirror/paraText.js +73 -0
  54. package/dist/prosemirror/plugins/templateDirectives.d.ts +21 -1
  55. package/dist/prosemirror/plugins/templateDirectives.js +40 -1
  56. package/dist/prosemirror/queries.d.ts +46 -0
  57. package/dist/prosemirror/queries.js +74 -0
  58. package/dist/prosemirror/styles/styleResolver.d.ts +43 -1
  59. package/dist/prosemirror/styles/styleResolver.js +34 -0
  60. package/dist/prosemirror/tableResize.d.ts +49 -0
  61. package/dist/prosemirror/tableResize.js +162 -0
  62. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +106 -0
  63. package/dist/prosemirror/utils/extractTrackedChanges.js +379 -0
  64. package/dist/prosemirror/utils/visualLineNavigation.d.ts +27 -0
  65. package/dist/prosemirror/utils/visualLineNavigation.js +217 -0
  66. package/dist/style-engine/index.d.ts +2 -2
  67. package/dist/style-engine/styleEngine.d.ts +14 -1
  68. package/dist/style-engine/styleEngine.js +15 -0
  69. package/dist/utils/colorResolver.d.ts +8 -1
  70. package/dist/utils/colorResolver.js +12 -1
  71. package/dist/utils/findVerticalScrollParent.d.ts +20 -0
  72. package/dist/utils/findVerticalScrollParent.js +30 -0
  73. package/dist/utils/fontResolver.d.ts +22 -3
  74. package/dist/utils/fontResolver.js +216 -31
  75. package/package.json +9 -1
@@ -1,6 +1,29 @@
1
1
  import { SdtGroup } from "../layout-engine/types.js";
2
+ import { Node } from "prosemirror-model";
2
3
 
3
4
  //#region src/layout-painter/sdtBoundary.d.ts
4
5
  declare function applySdtDataAttrs(el: HTMLElement, sdtGroups: readonly SdtGroup[] | undefined): void;
6
+ /**
7
+ * Identities of every block-level content control (`w:sdt`) that encloses the
8
+ * given selection — used to keep a control's boundary visible while the caret
9
+ * is inside it (Word-style focus), independent of mouse hover. Both ends of a
10
+ * range are collected so a selection straddling a control still lights it up.
11
+ *
12
+ * Ported from eigenpal/docx-editor `layout-painter/sdtBoundary.ts`. Upstream
13
+ * keyed the set on `sdt@${pos}` to match per-control boundary boxes; this fork
14
+ * stamps the SDT node's PM position as `data-sdt-pm-pos` on the painted block
15
+ * elements (see `applySdtDataAttrs`), so the identity here is the stringified
16
+ * `$pos.before(d)` — which is exactly that PM position — keeping the returned
17
+ * ids in lockstep with what `applySdtFocus` hit-tests against.
18
+ */
19
+ declare function enclosingSdtGroupIds(doc: Node, from: number, to: number): Set<string>;
20
+ /**
21
+ * Toggle the `.is-focused` reveal class on every painted block whose enclosing
22
+ * content control encloses the caret. Kept separate from any hover-driven
23
+ * reveal so the two paths never clear each other. Matches the fork's
24
+ * `data-sdt-pm-pos` stamping (upstream matched `.layout-block-sdt-box`'s
25
+ * `data-sdt-group-id`, which this fork does not paint).
26
+ */
27
+ declare function applySdtFocus(container: HTMLElement, focusedIds: Set<string>): void;
5
28
  //#endregion
6
- export { applySdtDataAttrs };
29
+ export { applySdtDataAttrs, applySdtFocus, enclosingSdtGroupIds };
@@ -24,5 +24,44 @@ function applySdtDataAttrs(el, sdtGroups) {
24
24
  alias: g.alias ?? null
25
25
  })));
26
26
  }
27
+ /**
28
+ * Identities of every block-level content control (`w:sdt`) that encloses the
29
+ * given selection — used to keep a control's boundary visible while the caret
30
+ * is inside it (Word-style focus), independent of mouse hover. Both ends of a
31
+ * range are collected so a selection straddling a control still lights it up.
32
+ *
33
+ * Ported from eigenpal/docx-editor `layout-painter/sdtBoundary.ts`. Upstream
34
+ * keyed the set on `sdt@${pos}` to match per-control boundary boxes; this fork
35
+ * stamps the SDT node's PM position as `data-sdt-pm-pos` on the painted block
36
+ * elements (see `applySdtDataAttrs`), so the identity here is the stringified
37
+ * `$pos.before(d)` — which is exactly that PM position — keeping the returned
38
+ * ids in lockstep with what `applySdtFocus` hit-tests against.
39
+ */
40
+ function enclosingSdtGroupIds(doc, from, to) {
41
+ const ids = /* @__PURE__ */ new Set();
42
+ const max = doc.content.size;
43
+ const collect = (pos) => {
44
+ const $pos = doc.resolve(Math.max(0, Math.min(pos, max)));
45
+ for (let d = 1; d <= $pos.depth; d++) if ($pos.node(d).type.name === "blockSdt") ids.add(String($pos.before(d)));
46
+ };
47
+ collect(from);
48
+ if (to !== from) collect(to);
49
+ return ids;
50
+ }
51
+ /**
52
+ * Toggle the `.is-focused` reveal class on every painted block whose enclosing
53
+ * content control encloses the caret. Kept separate from any hover-driven
54
+ * reveal so the two paths never clear each other. Matches the fork's
55
+ * `data-sdt-pm-pos` stamping (upstream matched `.layout-block-sdt-box`'s
56
+ * `data-sdt-group-id`, which this fork does not paint).
57
+ */
58
+ function applySdtFocus(container, focusedIds) {
59
+ const boxes = container.querySelectorAll("[data-sdt-boundary]");
60
+ for (const box of boxes) {
61
+ const id = box.dataset["sdtPmPos"];
62
+ const on = id != null && focusedIds.has(id);
63
+ if (on !== box.classList.contains("is-focused")) box.classList.toggle("is-focused", on);
64
+ }
65
+ }
27
66
  //#endregion
28
- export { applySdtDataAttrs };
67
+ export { applySdtDataAttrs, applySdtFocus, enclosingSdtGroupIds };
@@ -0,0 +1,89 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { Subscribable } from "./Subscribable.js";
3
+
4
+ //#region src/managers/AutoSaveManager.d.ts
5
+ /** Auto-save status. */
6
+ type AutoSaveStatus = "idle" | "saving" | "saved" | "error";
7
+ /** Configuration for {@link AutoSaveManager}. */
8
+ type AutoSaveManagerOptions = {
9
+ /** Storage key for localStorage (default: 'docx-editor-autosave'). */storageKey?: string; /** Save interval in milliseconds (default: 30000 - 30 seconds). */
10
+ interval?: number; /** Maximum age of auto-save before it is considered stale (default: 24 hours). */
11
+ maxAge?: number; /** Whether to save on document change with debounce (default: true). */
12
+ saveOnChange?: boolean; /** Debounce delay for saveOnChange in milliseconds (default: 2000). */
13
+ debounceDelay?: number; /** Callback when save succeeds. */
14
+ onSave?: (timestamp: Date) => void; /** Callback when save fails. */
15
+ onError?: (error: Error) => void; /** Callback when recovery data is found. */
16
+ onRecoveryAvailable?: (savedDocument: SavedDocumentData) => void;
17
+ };
18
+ /** Saved document data structure. */
19
+ type SavedDocumentData = {
20
+ /** The document. */document: document_d_exports.Document; /** When the document was saved (ISO string). */
21
+ savedAt: string; /** Version for format compatibility. */
22
+ version: number; /** Optional document identifier. */
23
+ documentId?: string;
24
+ };
25
+ /** AutoSaveManager snapshot for UI consumption. */
26
+ type AutoSaveSnapshot = {
27
+ status: AutoSaveStatus;
28
+ lastSaveTime: Date | null;
29
+ hasRecoveryData: boolean;
30
+ isEnabled: boolean;
31
+ };
32
+ declare class AutoSaveManager extends Subscribable<AutoSaveSnapshot> {
33
+ private readonly storageKey;
34
+ private readonly interval;
35
+ private readonly maxAge;
36
+ private readonly saveOnChange;
37
+ private readonly debounceDelay;
38
+ private readonly onSaveCallback;
39
+ private readonly onErrorCallback;
40
+ private readonly onRecoveryAvailableCallback;
41
+ private readonly storageAvailable;
42
+ private currentDocument;
43
+ private lastSavedJson;
44
+ private intervalTimer;
45
+ private debounceTimer;
46
+ private status;
47
+ private lastSaveTime;
48
+ private hasRecoveryData;
49
+ private isEnabled;
50
+ constructor(options?: AutoSaveManagerOptions);
51
+ /** Update the current document. Triggers a debounced save if enabled. */
52
+ onDocumentChanged(document: document_d_exports.Document | null): void;
53
+ /** Manually trigger a save. */
54
+ save(): boolean;
55
+ /** Clear auto-saved data from storage. */
56
+ clear(): void;
57
+ /** Get recovery data from storage. */
58
+ getRecoveryData(): SavedDocumentData | null;
59
+ /** Accept recovery and return the document. */
60
+ acceptRecovery(): document_d_exports.Document | null;
61
+ /** Dismiss recovery and clear saved data. */
62
+ dismissRecovery(): void;
63
+ /** Enable auto-save and start the interval timer. */
64
+ enable(): void;
65
+ /** Disable auto-save and stop all timers. */
66
+ disable(): void;
67
+ /** Start the interval timer. Call after enabling or on init. */
68
+ startInterval(): void;
69
+ /** Save synchronously on destroy (best-effort). */
70
+ destroy(): void;
71
+ private checkRecoveryData;
72
+ private persistToStorage;
73
+ private debounceSave;
74
+ private stopTimers;
75
+ private updateStatus;
76
+ private emitSnapshot;
77
+ }
78
+ /** Format last save time for display. */
79
+ declare function formatLastSaveTime(date: Date | null): string;
80
+ /** Get auto-save status label. */
81
+ declare function getAutoSaveStatusLabel(status: AutoSaveStatus): string;
82
+ /** Get storage size used by auto-save (bytes). */
83
+ declare function getAutoSaveStorageSize(storageKey?: string): number;
84
+ /** Format storage size for display. */
85
+ declare function formatStorageSize(bytes: number): string;
86
+ /** Check whether auto-save is supported. */
87
+ declare function isAutoSaveSupported(): boolean;
88
+ //#endregion
89
+ export { AutoSaveManager, AutoSaveManagerOptions, AutoSaveSnapshot, AutoSaveStatus, SavedDocumentData, formatLastSaveTime, formatStorageSize, getAutoSaveStatusLabel, getAutoSaveStorageSize, isAutoSaveSupported };
@@ -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 };