@forevka/wordcanvas 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.
@@ -0,0 +1,365 @@
1
+ let n = null;
2
+ function p(r) {
3
+ n = r;
4
+ }
5
+ function b() {
6
+ if (!n) throw new Error("WordCanvas runtime not configured (set it before importing editorApp)");
7
+ return n;
8
+ }
9
+ const a = "wordcanvas-styles", l = `
10
+ .wordcanvas-root {
11
+ display: flex; flex-direction: column; height: 100%; min-height: 0; overflow-x: hidden;
12
+ font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
13
+ color: #323130;
14
+ }
15
+
16
+ /* ===== Word-style ribbon ============================================ */
17
+ #toolbar {
18
+ flex: 0 0 auto; display: flex; flex-direction: column;
19
+ background: #f3f2f1; border-bottom: 1px solid #e1dfdd; user-select: none;
20
+ }
21
+
22
+ /* --- tab strip --- */
23
+ .rib-tabs {
24
+ display: flex; align-items: flex-end; gap: 1px; height: 32px;
25
+ padding: 4px 8px 0; background: #f3f2f1;
26
+ }
27
+ .rib-tab {
28
+ border: none; background: transparent; cursor: pointer;
29
+ font: inherit; font-size: 13px; color: #323130;
30
+ padding: 5px 11px 6px; border-radius: 4px 4px 0 0; position: relative;
31
+ }
32
+ .rib-tab:hover { background: #eceae9; }
33
+ .rib-tab.active {
34
+ background: #fff; color: #2b579a; font-weight: 600;
35
+ box-shadow: 0 -1px 2px rgba(0,0,0,0.05);
36
+ }
37
+ .rib-tab.active::after {
38
+ content: ""; position: absolute; left: 8px; right: 8px; bottom: 0;
39
+ height: 2px; background: #2b579a;
40
+ }
41
+ .rib-tab.file { background: #2b579a; color: #fff; font-weight: 600; }
42
+ .rib-tab.file:hover { background: #21457e; }
43
+
44
+ /* --- ribbon body: one panel visible at a time --- */
45
+ .rib-bodies {
46
+ background: #fff; border-top: 1px solid #e1dfdd; border-bottom: 1px solid #e1dfdd;
47
+ }
48
+ .rib-panel { display: none; align-items: stretch; min-height: 94px; padding: 0 2px; }
49
+ .rib-panel.active { display: flex; }
50
+
51
+ /* --- group: stacked controls + caption, divider on the right --- */
52
+ .rib-group {
53
+ display: flex; flex-direction: column; align-items: center;
54
+ padding: 5px 7px 3px; position: relative;
55
+ }
56
+ .rib-group + .rib-group::before {
57
+ content: ""; position: absolute; left: 0; top: 8px; bottom: 8px;
58
+ width: 1px; background: #e1dfdd;
59
+ }
60
+ .rib-controls { display: flex; align-items: center; gap: 2px; flex: 1; }
61
+ /* two stacked rows (Font, Paragraph) */
62
+ .rib-rows { display: flex; flex-direction: column; gap: 3px; flex: 1; justify-content: center; }
63
+ .rib-row { display: flex; align-items: center; gap: 2px; }
64
+ .rib-label {
65
+ font-size: 11px; color: #605e5c; padding-top: 3px; white-space: nowrap;
66
+ }
67
+
68
+ /* --- buttons --- */
69
+ #toolbar button.rib-btn {
70
+ min-width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
71
+ border: 1px solid transparent; border-radius: 4px; background: transparent; cursor: pointer;
72
+ color: #323130; font-size: 13px; padding: 0 4px; gap: 1px;
73
+ }
74
+ #toolbar button.rib-btn:hover { background: #e1dfdd; }
75
+ #toolbar button.rib-btn:active { background: #d2d0ce; }
76
+ #toolbar button.rib-btn.active { background: #cfe3fb; color: #0b57d0; border-color: #b3d3f5; }
77
+ #toolbar button.rib-btn.active:hover { background: #bcd8fa; }
78
+ #toolbar button.rib-btn.active svg { color: #0b57d0; }
79
+ #toolbar button.rib-btn svg { width: 16px; height: 16px; display: block; }
80
+ #toolbar button.rib-btn .caret { width: 8px; height: 8px; }
81
+
82
+ /* large button (Paste): icon over caption */
83
+ #toolbar button.rib-big {
84
+ flex-direction: column; height: 100%; min-width: 48px; padding: 4px 6px; gap: 2px; justify-content: center;
85
+ }
86
+ #toolbar button.rib-big svg { width: 26px; height: 26px; }
87
+ #toolbar button.rib-big .big-cap { font-size: 11px; display: flex; align-items: center; gap: 1px; }
88
+
89
+ /* swatch button: icon row + colour underline */
90
+ #toolbar button.rib-swatch { flex-direction: column; gap: 0; padding: 2px 3px; }
91
+ #toolbar button.rib-swatch .row { display: flex; align-items: center; gap: 1px; }
92
+ #toolbar button.rib-swatch .bar { width: 18px; height: 3px; margin-top: 1px; border-radius: 1px; }
93
+
94
+ /* disabled stub (feature not supported by the engine yet) */
95
+ #toolbar button.rib-btn:disabled { opacity: 0.38; cursor: default; }
96
+ #toolbar button.rib-btn:disabled:hover { background: transparent; }
97
+
98
+ /* form controls */
99
+ #toolbar select, #toolbar input[type="number"] {
100
+ height: 24px; border: 1px solid #c8c6c4; border-radius: 3px; background: #fff;
101
+ font: inherit; font-size: 13px; color: #323130;
102
+ }
103
+ #toolbar select:hover, #toolbar input[type="number"]:hover { border-color: #8a8886; }
104
+
105
+ /* styles gallery */
106
+ .rib-gallery {
107
+ display: flex; align-items: center; gap: 4px; height: 64px; padding: 0 4px;
108
+ border: 1px solid #c8c6c4; border-radius: 4px; background: #fff;
109
+ overflow-x: auto; max-width: 340px;
110
+ }
111
+ .rib-gallery::-webkit-scrollbar { height: 8px; }
112
+ .rib-gallery::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
113
+ .style-card {
114
+ flex: 0 0 auto; width: 76px; height: 50px; border: 1px solid #e1dfdd; border-radius: 2px;
115
+ background: #fff; cursor: pointer; display: flex; flex-direction: column;
116
+ align-items: center; justify-content: center; gap: 3px; padding: 2px;
117
+ }
118
+ .style-card:hover { border-color: #2b579a; }
119
+ .style-card.active { border-color: #2b579a; box-shadow: inset 0 0 0 1px #2b579a; background: #eef3fb; }
120
+ .style-card .preview { font-size: 13px; line-height: 1; color: #323130; }
121
+ .style-card .name { font-size: 10px; color: #605e5c; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 72px; }
122
+
123
+ /* work area: optional outline drawer + ruler + the scrolling page canvas */
124
+ #workarea { flex: 1 1 auto; min-height: 0; display: flex; }
125
+ #editorpane { flex: 1 1 auto; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
126
+ /* touch-action: keep one/two-finger PAN (scroll) but disable the browser's
127
+ native pinch-zoom and double-tap-zoom on the document, so our own
128
+ pinch-to-zoom handler (index.ts) owns those gestures. The toolbar/status bar
129
+ keep default touch-action so the ribbon scrolls and taps normally. */
130
+ #app { flex: 1 1 auto; min-height: 0; min-width: 0; overflow: auto; background: #e8eaed; position: relative; touch-action: pan-x pan-y; }
131
+
132
+ /* horizontal ruler (inch ticks, margin shading, draggable indent markers) */
133
+ #ruler { flex: 0 0 22px; height: 22px; position: relative; background: #e8eaed; border-bottom: 1px solid #d2d0ce; overflow: hidden; }
134
+ #ruler.hidden { display: none; }
135
+ #ruler canvas { position: absolute; inset: 0; }
136
+ #ruler .ruler-marker { position: absolute; width: 0; height: 0; cursor: ew-resize; z-index: 2; }
137
+ /* left-indent: bottom-pointing triangle sitting on the baseline */
138
+ #ruler .ruler-left { bottom: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 7px solid #5b6b8c; }
139
+ /* first-line-indent: top-pointing triangle */
140
+ #ruler .ruler-first { top: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 7px solid #5b6b8c; }
141
+
142
+ /* ===== Outline / Navigation drawer ================================== */
143
+ #outline {
144
+ flex: 0 0 264px; width: 264px; min-height: 0; overflow-y: auto;
145
+ background: #fff; border-right: 1px solid #e1dfdd; display: none;
146
+ }
147
+ #outline.open { display: block; }
148
+ #outline .outline-head {
149
+ position: sticky; top: 0; background: #fff; z-index: 1;
150
+ display: flex; align-items: center; justify-content: space-between;
151
+ padding: 9px 8px 9px 14px; border-bottom: 1px solid #e1dfdd;
152
+ font-size: 13px; font-weight: 600; color: #323130;
153
+ }
154
+ #outline .outline-head button {
155
+ border: none; background: transparent; cursor: pointer; color: #605e5c;
156
+ width: 24px; height: 24px; border-radius: 4px; font-size: 17px; line-height: 1;
157
+ }
158
+ #outline .outline-head button:hover { background: #f3f2f1; }
159
+ #outline-list { padding: 4px 0 12px; }
160
+ .outline-item {
161
+ display: block; width: 100%; box-sizing: border-box; text-align: left;
162
+ border: none; border-left: 3px solid transparent; background: transparent; cursor: pointer;
163
+ padding: 4px 12px; color: #323130; font-size: 13px; line-height: 1.35;
164
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
165
+ }
166
+ .outline-item:hover { background: #f3f2f1; }
167
+ .outline-item.active { background: #eef3fb; border-left-color: #2b579a; color: #2b579a; font-weight: 600; }
168
+ .outline-empty { padding: 14px; color: #80868b; font-size: 12px; line-height: 1.4; }
169
+
170
+ /* collapsed ribbon: keep the tab strip, hide the body */
171
+ #toolbar.collapsed .rib-bodies { display: none; }
172
+
173
+ /* ===== Status bar ================================================== */
174
+ #statusbar {
175
+ flex: 0 0 auto; height: 24px; display: flex; align-items: center; justify-content: space-between;
176
+ background: #2b579a; color: #fff; font-size: 12px; padding: 0 10px; user-select: none;
177
+ }
178
+ #statusbar .sb-left, #statusbar .sb-right { display: flex; align-items: center; gap: 16px; }
179
+ #statusbar .sb-right { gap: 8px; }
180
+ #statusbar .sb-item { white-space: nowrap; }
181
+ #statusbar .sb-sep { width: 1px; height: 14px; background: rgba(255,255,255,0.35); }
182
+ #statusbar button.sb-btn {
183
+ background: transparent; border: none; color: #fff; cursor: pointer; font-size: 15px;
184
+ width: 22px; height: 20px; border-radius: 3px; line-height: 1; padding: 0;
185
+ }
186
+ #statusbar button.sb-btn:hover { background: rgba(255,255,255,0.18); }
187
+ #statusbar input[type="range"] { width: 120px; cursor: pointer; accent-color: #fff; }
188
+ #statusbar .sb-zoom { min-width: 38px; text-align: right; }
189
+
190
+ /* ===== Popovers (palettes, menus, pickers, dialogs) ================ */
191
+ .cw-pop {
192
+ position: fixed; background: #fff; border: 1px solid #c8c6c4; border-radius: 6px;
193
+ box-shadow: 0 4px 18px rgba(0,0,0,0.18); z-index: 50; font-size: 13px; padding: 4px;
194
+ }
195
+ .cw-menu { display: flex; flex-direction: column; min-width: 168px; }
196
+ .cw-menu button {
197
+ display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
198
+ border: none; background: transparent; cursor: pointer; padding: 7px 10px; border-radius: 4px;
199
+ font: inherit; color: #323130;
200
+ }
201
+ .cw-menu button:hover { background: #f3f2f1; }
202
+ .cw-menu .check { width: 14px; color: #2b579a; }
203
+ .cw-menu .sample { color: #605e5c; }
204
+ .cw-pop-title { font-size: 11px; color: #605e5c; padding: 4px 6px 2px; }
205
+ .cw-swatches { display: grid; grid-template-columns: repeat(6, 22px); gap: 4px; padding: 4px 6px; }
206
+ .cw-swatches button { width: 22px; height: 22px; border: 1px solid rgba(0,0,0,0.18); border-radius: 3px; cursor: pointer; padding: 0; }
207
+ .cw-swatches button:hover { outline: 2px solid #2b579a; outline-offset: 1px; }
208
+ .cw-pop .pop-action {
209
+ display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: none;
210
+ background: transparent; cursor: pointer; padding: 7px 8px; border-radius: 4px; font: inherit; color: #323130;
211
+ border-top: 1px solid #edebe9; margin-top: 4px;
212
+ }
213
+ .cw-pop .pop-action:hover { background: #f3f2f1; }
214
+ .cw-grid { display: grid; gap: 2px; padding: 8px 8px 2px; }
215
+ .cw-grid .cell { width: 15px; height: 15px; border: 1px solid #c8c6c4; background: #fff; }
216
+ .cw-grid .cell.on { background: #cfe3fb; border-color: #2b579a; }
217
+ .cw-grid-label { text-align: center; font-size: 12px; color: #605e5c; padding: 4px 0 6px; }
218
+ .cw-dialog { display: flex; flex-direction: column; gap: 8px; padding: 10px; min-width: 248px; }
219
+ .cw-dialog label { font-size: 12px; color: #605e5c; display: flex; flex-direction: column; gap: 3px; }
220
+ .cw-dialog input { height: 28px; border: 1px solid #c8c6c4; border-radius: 4px; padding: 0 8px; font: inherit; font-size: 13px; }
221
+ .cw-dialog .row { display: flex; justify-content: flex-end; gap: 6px; margin-top: 2px; }
222
+ .cw-dialog button { height: 28px; border: 1px solid #c8c6c4; background: #fff; border-radius: 4px; cursor: pointer; padding: 0 12px; font: inherit; font-size: 13px; }
223
+ .cw-dialog button.primary { background: #2b579a; color: #fff; border-color: #2b579a; }
224
+ .cw-dialog button.danger { color: #a4262c; }
225
+
226
+ /* ===== Busy overlay (docx import / join / publish) ================= */
227
+ .cw-loading-overlay {
228
+ position: fixed; inset: 0; z-index: 70; background: rgba(0,0,0,0.25);
229
+ display: flex; align-items: center; justify-content: center;
230
+ }
231
+ .cw-loading-card {
232
+ display: flex; flex-direction: column; align-items: center; gap: 12px;
233
+ background: #fff; border: 1px solid #c8c6c4; border-radius: 8px;
234
+ box-shadow: 0 8px 30px rgba(0,0,0,0.25); padding: 22px 28px; min-width: 200px;
235
+ }
236
+ .cw-spinner {
237
+ width: 30px; height: 30px; border-radius: 50%;
238
+ border: 3px solid #e1dfdd; border-top-color: #2b579a;
239
+ animation: cw-spin 0.8s linear infinite;
240
+ }
241
+ @keyframes cw-spin { to { transform: rotate(360deg); } }
242
+ .cw-loading-label { font-size: 13px; color: #323130; text-align: center; }
243
+ .cw-progress { width: 180px; height: 4px; background: #e1dfdd; border-radius: 2px; overflow: hidden; }
244
+ .cw-progress-bar { height: 100%; width: 0%; background: #2b579a; transition: width 0.15s ease; }
245
+
246
+ /* floating mini-toolbar shown above a selected image */
247
+ #img-toolbar {
248
+ position: fixed; display: none; align-items: center; gap: 2px; z-index: 40;
249
+ background: #fff; border: 1px solid #c8c6c4; border-radius: 6px; padding: 3px;
250
+ box-shadow: 0 3px 12px rgba(0,0,0,0.18);
251
+ }
252
+ #img-toolbar button {
253
+ width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center;
254
+ border: 1px solid transparent; border-radius: 4px; background: transparent; cursor: pointer; color: #323130;
255
+ }
256
+ #img-toolbar button:hover { background: #e1dfdd; }
257
+ #img-toolbar button.danger:hover { background: #fde7e9; color: #a4262c; }
258
+ #img-toolbar button svg { width: 16px; height: 16px; }
259
+ #img-toolbar .sep { width: 1px; height: 18px; background: #e1dfdd; margin: 0 2px; }
260
+
261
+ /* ===== Mobile / touch responsive layer ============================== */
262
+ /* Activates on touch devices (coarse primary pointer) OR narrow screens.
263
+ Desktop is untouched outside this block. Strategy: collapse the ribbon to one
264
+ horizontally-scrollable row, hide group captions, grow touch targets to ~40px,
265
+ turn the 264px outline into an overlay drawer, and clamp floating panels to
266
+ the viewport (a bottom sheet) so they never render off-screen. */
267
+ @media (pointer: coarse), (max-width: 760px) {
268
+ /* Ribbon body: single scrollable row instead of a tall multi-group block. */
269
+ .rib-panel { min-height: 0; overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; }
270
+ .rib-label { display: none; }
271
+ .rib-group { padding: 4px 6px; }
272
+ /* Touch targets (Apple/Material guideline is ~40-48px). */
273
+ #toolbar button.rib-btn { min-width: 40px; height: 40px; }
274
+ #toolbar button.rib-btn svg { width: 18px; height: 18px; }
275
+ .rib-tab { padding: 9px 14px; font-size: 14px; }
276
+ #toolbar select, #toolbar input[type="number"] { height: 36px; font-size: 14px; }
277
+
278
+ /* Outline: overlay the page instead of stealing 264px of a ~360px screen. */
279
+ #workarea { position: relative; }
280
+ #outline { position: absolute; left: 0; top: 0; bottom: 0; height: auto; width: min(264px, 80vw); z-index: 30; box-shadow: 2px 0 16px rgba(0,0,0,0.18); }
281
+
282
+ /* Status bar: tappable zoom controls. */
283
+ #statusbar { height: 36px; }
284
+ #statusbar button.sb-btn { width: 32px; height: 30px; font-size: 17px; }
285
+ #statusbar input[type="range"] { width: 96px; }
286
+
287
+ /* Floating panels (Page Setup, Find) → bottom sheet; Activity → full-width.
288
+ !important overrides the inline position/size set in editorApp.ts. */
289
+ .cw-float-panel { left: 8px !important; right: 8px !important; top: auto !important; bottom: 8px !important; width: auto !important; max-width: none !important; max-height: 60vh; overflow: auto; }
290
+ .cw-float-drawer { width: 100% !important; }
291
+ #img-toolbar button { width: 34px; height: 34px; }
292
+
293
+ /* Image resize handles: 8px dots are unhittable with a finger — an invisible
294
+ ::before pads the touch target to ~24px without changing the visual size. */
295
+ .cw-obj-handle::before { content: ""; position: absolute; inset: -8px; }
296
+ }
297
+ `;
298
+ function s() {
299
+ if (document.getElementById(a)) return;
300
+ const r = document.createElement("style");
301
+ r.id = a, r.textContent = l, document.head.appendChild(r);
302
+ }
303
+ let i = !1;
304
+ class c {
305
+ handle = null;
306
+ ready;
307
+ handlers = /* @__PURE__ */ new Map();
308
+ constructor(e) {
309
+ if (i)
310
+ throw new Error("WordCanvas: only one instance per page is supported in this version");
311
+ i = !0, s(), this.ready = new Promise((o) => {
312
+ p({
313
+ container: e.container,
314
+ backendUrl: e.backendUrl,
315
+ // `docId` is the canonical name; fall back to the deprecated `collabId`.
316
+ collabId: e.docId ?? e.collabId,
317
+ user: e.user,
318
+ onShareLink: e.onShareLink,
319
+ onReady: (t) => {
320
+ this.handle = t, o(t);
321
+ },
322
+ onEvent: (t) => this.emit(t)
323
+ }), import("./editorApp-Bl8rfr3S.js");
324
+ });
325
+ }
326
+ /** Subscribe to a collaboration event. Returns an unsubscribe function. */
327
+ on(e, o) {
328
+ let t = this.handlers.get(e);
329
+ return t || this.handlers.set(e, t = /* @__PURE__ */ new Set()), t.add(o), () => this.off(e, o);
330
+ }
331
+ /** Unsubscribe a handler previously passed to on(). */
332
+ off(e, o) {
333
+ this.handlers.get(e)?.delete(o);
334
+ }
335
+ emit(e) {
336
+ const { type: o, ...t } = e;
337
+ for (const d of this.handlers.get(o) ?? []) d(t);
338
+ }
339
+ /** Resolves once the editor is mounted and ready. */
340
+ whenReady() {
341
+ return this.ready;
342
+ }
343
+ /** Open a .docx. When online, auto-publishes it and surfaces a share link. */
344
+ async openDocx(e) {
345
+ return (await this.ready).openDocx(e);
346
+ }
347
+ /** Publish the current document and resolve its shareable link (online only). */
348
+ async share() {
349
+ return (await this.ready).share();
350
+ }
351
+ getDocId() {
352
+ return this.handle?.getDocId() ?? null;
353
+ }
354
+ getShareLink() {
355
+ return this.handle?.getShareLink() ?? null;
356
+ }
357
+ destroy() {
358
+ this.handle?.destroy(), this.handle = null, this.handlers.clear(), i = !1;
359
+ }
360
+ }
361
+ export {
362
+ c as W,
363
+ s as e,
364
+ b as g
365
+ };
@@ -0,0 +1,4 @@
1
+ import { W as r } from "./wordcanvas-DPJsuqnm.js";
2
+ export {
3
+ r as WordCanvas
4
+ };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@forevka/wordcanvas",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Embeddable canvas-rendered, Word-compatible document editor with live collaboration and DOCX/PDF import & export. Self-contained, zero runtime dependencies.",
6
+ "license": "MIT",
7
+ "author": "Bohdan Lushchyk",
8
+ "homepage": "https://github.com/Forevka/canvas-word#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Forevka/canvas-word.git",
12
+ "directory": "frontend"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/Forevka/canvas-word/issues"
16
+ },
17
+ "keywords": [
18
+ "word",
19
+ "editor",
20
+ "docx",
21
+ "pdf",
22
+ "canvas",
23
+ "wysiwyg",
24
+ "rich-text",
25
+ "document",
26
+ "collaboration"
27
+ ],
28
+ "module": "./dist-lib/wordcanvas.js",
29
+ "types": "./types/wordcanvas.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./types/wordcanvas.d.ts",
33
+ "import": "./dist-lib/wordcanvas.js"
34
+ },
35
+ "./import": "./src/import/docx/pipeline.ts",
36
+ "./export": "./src/export/pipeline.ts",
37
+ "./export/measure": "./src/export/shared/measureHost.ts"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "exports": {
42
+ ".": {
43
+ "types": "./types/wordcanvas.d.ts",
44
+ "import": "./dist-lib/wordcanvas.js"
45
+ }
46
+ }
47
+ },
48
+ "files": ["dist-lib", "types"],
49
+ "scripts": {
50
+ "dev": "vite",
51
+ "build": "vite build",
52
+ "build:lib": "vite build --mode lib",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "prepublishOnly": "npm run typecheck && npm run build:lib"
56
+ },
57
+ "devDependencies": {
58
+ "@cw/shared": "*",
59
+ "@chenglou/pretext": "latest",
60
+ "@types/fontkit": "^2.0.9",
61
+ "@types/pdfkit": "^0.17.6",
62
+ "fflate": "^0.8.3",
63
+ "fontkit": "^2.0.4",
64
+ "pdfkit": "^0.19.0",
65
+ "playwright": "^1.60.0",
66
+ "txml": "^6.0.0",
67
+ "vite": "^6.0.0",
68
+ "vite-plugin-node-polyfills": "^0.28.0",
69
+ "vitest": "^3.0.0"
70
+ }
71
+ }
@@ -0,0 +1,70 @@
1
+ // Public type surface for the @forevka/wordcanvas (WordCanvas) package.
2
+ // Hand-written so the published types stay small, self-contained, and stable
3
+ // regardless of internal refactors — no dependency on internal workspace types.
4
+
5
+ /** Caller-supplied identity, used for change attribution and presence. */
6
+ export interface UserInfo {
7
+ id: string;
8
+ firstName: string;
9
+ lastName: string;
10
+ }
11
+
12
+ /** One other collaborator currently in the document (online mode). */
13
+ export interface Participant {
14
+ siteId: string;
15
+ user?: UserInfo;
16
+ }
17
+
18
+ export interface WordCanvasOptions {
19
+ /** Element to mount the editor into. */
20
+ container: HTMLElement;
21
+ /** Backend base URL (e.g. "https://api.example.com"). Online iff provided;
22
+ * omit for a fully offline editor (no sync, publish, or share). */
23
+ backendUrl?: string;
24
+ /** Open this document on load (online only) — e.g. the id returned by an
25
+ * upload. Canonical name; `collabId` is the deprecated alias. */
26
+ docId?: string;
27
+ /** @deprecated Use `docId`. Join an existing collaboration session on load. */
28
+ collabId?: string;
29
+ /** Caller-supplied identity (attribution + presence). The embedder owns auth. */
30
+ user?: UserInfo;
31
+ /** Override how a share link is surfaced (default: a built-in dialog). */
32
+ onShareLink?: (url: string, docId: string) => void;
33
+ }
34
+
35
+ /** Event name → payload, for `on(...)`/`off(...)`. */
36
+ export interface WordCanvasEventMap {
37
+ ready: Record<string, never>;
38
+ shared: { docId: string; url: string };
39
+ userEntered: { siteId: string; user?: UserInfo };
40
+ userLeave: { siteId: string; user?: UserInfo };
41
+ presence: { participants: Participant[] };
42
+ }
43
+
44
+ /** Handle resolved once the editor is mounted (via `whenReady()`). */
45
+ export interface EditorHandle {
46
+ /** Open a .docx (auto-publishes when online); resolves when loaded. */
47
+ openDocx(file: File | ArrayBuffer): Promise<void>;
48
+ /** Publish the current document and resolve its shareable link (online only). */
49
+ share(): Promise<string>;
50
+ getDocId(): string | null;
51
+ getShareLink(): string | null;
52
+ destroy(): void;
53
+ }
54
+
55
+ export declare class WordCanvas {
56
+ constructor(opts: WordCanvasOptions);
57
+ /** Subscribe to a collaboration event. Returns an unsubscribe function. */
58
+ on<E extends keyof WordCanvasEventMap>(event: E, handler: (data: WordCanvasEventMap[E]) => void): () => void;
59
+ /** Unsubscribe a handler previously passed to `on()`. */
60
+ off<E extends keyof WordCanvasEventMap>(event: E, handler: (data: WordCanvasEventMap[E]) => void): void;
61
+ /** Resolves once the editor is mounted and ready. */
62
+ whenReady(): Promise<EditorHandle>;
63
+ /** Open a .docx. When online, auto-publishes it and surfaces a share link. */
64
+ openDocx(file: File | ArrayBuffer): Promise<void>;
65
+ /** Publish the current document and resolve its shareable link (online only). */
66
+ share(): Promise<string>;
67
+ getDocId(): string | null;
68
+ getShareLink(): string | null;
69
+ destroy(): void;
70
+ }