@ai-matrx/office 0.8.45 → 0.8.47
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.
- package/CHANGELOG.md +7 -0
- package/dist/pdf-model.cjs +232 -0
- package/dist/pdf-model.cjs.map +1 -0
- package/dist/pdf-model.d.cts +248 -0
- package/dist/pdf-model.d.ts +248 -0
- package/dist/pdf-model.js +209 -0
- package/dist/pdf-model.js.map +1 -0
- package/dist/pdf.cjs +1127 -0
- package/dist/pdf.cjs.map +1 -0
- package/dist/pdf.d.cts +255 -0
- package/dist/pdf.d.ts +255 -0
- package/dist/pdf.js +1133 -0
- package/dist/pdf.js.map +1 -0
- package/dist/univer-collab.cjs +63 -16
- package/dist/univer-collab.cjs.map +1 -1
- package/dist/univer-collab.d.cts +42 -14
- package/dist/univer-collab.d.ts +42 -14
- package/dist/univer-collab.js +63 -16
- package/dist/univer-collab.js.map +1 -1
- package/package.json +26 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.47
|
|
4
|
+
|
|
5
|
+
- `EditorBridgeLike` / `FilterServiceLike` accept Univer's own `IEditorBridgeService` and `SheetsFilterService` types (their `Nullable<…>` returns, `IRange`): a host passing `injector.get(IEditorBridgeService)` to `editorBridgeCell`, or the filter service to `filterServiceRange`, type-checks. New `FilterRangeLike`. Consumer action: none.
|
|
6
|
+
- Sheets co-editing, an open cell editor holds less: only a remote op that writes its cell (a value, style, merge, format, rule or sort whose ranges cover it) or moves or removes it (a row/column insert, remove or move before it, its sheet's removal, a shrink past it) waits until the editor closes. A later op passes the held ones when the two commute; everything else applies live, so the person sees the others work while typing. `SheetsOtPort.busy()` no longer includes the editor hold; `touchesEditedCell` is exported. Consumer action: none.
|
|
7
|
+
- Sheets co-editing, the loser of a held cell hears of it: the commit rebased past a held op names what it overrode (`overrode`, data's `LocalEditOptions`), so the person whose value was overwritten gets the `overridden` notice ("Someone changed the same cells"). `SheetsOtAdapter.rebaseLocalOverHeld` returns `{ op, overrode }`. Consumer action: none.
|
|
8
|
+
- Added `./pdf` and `./pdf-model`: the PDF workspace chrome (Acrobat layout, tools rail, page panel, properties forms, find, session undo/redo, shortcut map, reading mode, phone sheets) with host-injected ports. Unbuilt tools announce through the Coming-soon registry.
|
|
9
|
+
|
|
3
10
|
## 0.8.45
|
|
4
11
|
|
|
5
12
|
- Sheets co-editing, typing into a cell a collaborator writes at the same moment (live attack F1, "$1,234.50" vs "$7" became 507): Univer rewrote an open cell editor's text whenever any write landed on its cell, caret back at 0, and Enter committed the splice. `univerSheetsOtPort` takes `cellEditor` (`editorBridgeCell(IEditorBridgeService)`); while it is open, a remote op touching its cell or sheet is held (`SheetsOtPort.defers`), with every op after it, and the person's commit is rebased over them when the editor closes. A held op never feeds a quiet hash. Consumer action: lend `cellEditor: editorBridgeCell(injector.get(IEditorBridgeService))`.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/pdf/model.ts
|
|
21
|
+
var model_exports = {};
|
|
22
|
+
__export(model_exports, {
|
|
23
|
+
HISTORY_LIMIT: () => HISTORY_LIMIT,
|
|
24
|
+
PDF_COMING_SOON: () => PDF_COMING_SOON,
|
|
25
|
+
PDF_SHORTCUTS: () => PDF_SHORTCUTS,
|
|
26
|
+
PDF_TOOLS: () => PDF_TOOLS,
|
|
27
|
+
canRedo: () => canRedo,
|
|
28
|
+
canUndo: () => canUndo,
|
|
29
|
+
comingSoonIdOf: () => comingSoonIdOf,
|
|
30
|
+
compilePageOps: () => compilePageOps,
|
|
31
|
+
deletePages: () => deletePages,
|
|
32
|
+
formatKeys: () => formatKeys,
|
|
33
|
+
freshDecisions: () => freshDecisions,
|
|
34
|
+
isDirty: () => isDirty,
|
|
35
|
+
keptOrder: () => keptOrder,
|
|
36
|
+
matchShortcut: () => matchShortcut,
|
|
37
|
+
pdfTool: () => pdfTool,
|
|
38
|
+
pushStep: () => pushStep,
|
|
39
|
+
redo: () => redo,
|
|
40
|
+
redoLabel: () => redoLabel,
|
|
41
|
+
reorderPages: () => reorderPages,
|
|
42
|
+
restorePages: () => restorePages,
|
|
43
|
+
startHistory: () => startHistory,
|
|
44
|
+
turnPages: () => turnPages,
|
|
45
|
+
undo: () => undo,
|
|
46
|
+
undoLabel: () => undoLabel
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(model_exports);
|
|
49
|
+
|
|
50
|
+
// src/pdf/tools.ts
|
|
51
|
+
var PDF_TOOLS = [
|
|
52
|
+
{ id: "browse", label: "Browse", key: "v", status: "ready", opens: "none", group: 0 },
|
|
53
|
+
{ id: "organize", label: "Organize pages", key: "o", status: "ready", capability: "rotate-pages,delete-pages,reorder-pages", opens: "panel", group: 0 },
|
|
54
|
+
{ id: "extract-pages", label: "Extract pages", key: "e", status: "ready", capability: "extract-pages", opens: "panel", group: 0 },
|
|
55
|
+
{ id: "split", label: "Split", key: "s", status: "ready", capability: "split", opens: "panel", group: 0 },
|
|
56
|
+
{ id: "crop", label: "Crop pages", key: "c", status: "ready", capability: "crop-pages", opens: "panel", group: 0 },
|
|
57
|
+
{ id: "redact", label: "Redact", key: "r", status: "ready", capability: "redact-pattern", opens: "panel", group: 1 },
|
|
58
|
+
{ id: "compress", label: "Compress", key: "z", status: "ready", capability: "compress", opens: "panel", group: 1 },
|
|
59
|
+
{ id: "extract-data", label: "Text and tables", key: "t", status: "ready", capability: "extract-text-remote,extract-tables", opens: "panel", group: 1 },
|
|
60
|
+
{ id: "scan", label: "Scan", status: "ready", capability: "route:/tools/scanner", opens: "page", group: 1 },
|
|
61
|
+
{ id: "sign", label: "Sign", status: "ready", capability: "route:/esign/new", opens: "page", group: 1 },
|
|
62
|
+
{ id: "edit", label: "Edit text and images", status: "soon", opens: "none", group: 2 },
|
|
63
|
+
{ id: "forms", label: "Fill forms", status: "soon", opens: "none", group: 2 },
|
|
64
|
+
{ id: "protect", label: "Protect", status: "soon", opens: "none", group: 2 },
|
|
65
|
+
{ id: "ocr", label: "Recognize text", status: "soon", opens: "none", group: 2 },
|
|
66
|
+
{ id: "convert", label: "Convert", status: "soon", opens: "none", group: 2 },
|
|
67
|
+
{ id: "compare", label: "Compare", status: "soon", opens: "none", group: 2 },
|
|
68
|
+
{ id: "stamp", label: "Stamp and number", status: "soon", opens: "none", group: 2 }
|
|
69
|
+
];
|
|
70
|
+
var pdfTool = (id) => PDF_TOOLS.find((t) => t.id === id);
|
|
71
|
+
var comingSoonIdOf = (id) => `pdf.${id}`;
|
|
72
|
+
var PROMISES = {
|
|
73
|
+
edit: "Change text and swap images right on the page.",
|
|
74
|
+
forms: "Fill in a form and keep the values in the file.",
|
|
75
|
+
protect: "Lock a PDF with a password and limit what others can do.",
|
|
76
|
+
ocr: "Turn a scanned PDF into one you can search and copy from.",
|
|
77
|
+
convert: "Turn a PDF into Word, Excel or PowerPoint, and back.",
|
|
78
|
+
compare: "Put two versions side by side and see what changed.",
|
|
79
|
+
stamp: "Add watermarks, page numbers and Bates numbers."
|
|
80
|
+
};
|
|
81
|
+
var PANEL_PROMISES = {
|
|
82
|
+
"pdf.layers": {
|
|
83
|
+
id: "pdf.layers",
|
|
84
|
+
label: "Layers",
|
|
85
|
+
owner: "pdf",
|
|
86
|
+
promise: "Show or hide the layers inside a PDF.",
|
|
87
|
+
stage: "planned",
|
|
88
|
+
surfaces: ["PDF workspace page panel"]
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var PDF_COMING_SOON = {
|
|
92
|
+
...Object.fromEntries(
|
|
93
|
+
PDF_TOOLS.filter((t) => t.status === "soon").map((t) => {
|
|
94
|
+
const id = comingSoonIdOf(t.id);
|
|
95
|
+
return [id, { id, label: t.label, owner: "pdf", promise: PROMISES[t.id] ?? "", stage: "planned", surfaces: ["PDF workspace tool rail"] }];
|
|
96
|
+
})
|
|
97
|
+
),
|
|
98
|
+
...PANEL_PROMISES
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// src/pdf/shortcuts.ts
|
|
102
|
+
var PDF_SHORTCUTS = [
|
|
103
|
+
{ action: "undo", label: "Undo", keys: "Mod+Z", section: "Edit" },
|
|
104
|
+
{ action: "redo", label: "Redo", keys: "Mod+Shift+Z", section: "Edit" },
|
|
105
|
+
{ action: "save", label: "Apply page changes", keys: "Mod+S", section: "Edit" },
|
|
106
|
+
{ action: "find", label: "Find", keys: "Mod+F", section: "Edit" },
|
|
107
|
+
{ action: "print", label: "Print", keys: "Mod+P", section: "Edit" },
|
|
108
|
+
{ action: "zoom-in", label: "Zoom in", keys: "Mod+=", section: "View" },
|
|
109
|
+
{ action: "zoom-out", label: "Zoom out", keys: "Mod+-", section: "View" },
|
|
110
|
+
{ action: "zoom-fit-width", label: "Fit width", keys: "Mod+1", section: "View" },
|
|
111
|
+
{ action: "zoom-fit-page", label: "Fit page", keys: "Mod+0", section: "View" },
|
|
112
|
+
{ action: "two-up", label: "Two pages side by side", keys: "Mod+Shift+2", section: "View" },
|
|
113
|
+
{ action: "read-mode", label: "Reading mode", keys: "Mod+Shift+H", section: "View" },
|
|
114
|
+
{ action: "toggle-pages", label: "Page thumbnails", keys: "Mod+Shift+T", section: "View" },
|
|
115
|
+
{ action: "toggle-properties", label: "Properties panel", keys: "Mod+Shift+P", section: "View" },
|
|
116
|
+
{ action: "shortcuts", label: "Keyboard shortcuts", keys: "?", section: "View" },
|
|
117
|
+
{ action: "page-next", label: "Next page", keys: "ArrowRight", section: "Pages" },
|
|
118
|
+
{ action: "page-prev", label: "Previous page", keys: "ArrowLeft", section: "Pages" },
|
|
119
|
+
{ action: "page-first", label: "First page", keys: "Home", section: "Pages" },
|
|
120
|
+
{ action: "page-last", label: "Last page", keys: "End", section: "Pages" },
|
|
121
|
+
{ action: "select-all-pages", label: "Select all pages", keys: "Mod+A", section: "Pages" },
|
|
122
|
+
{ action: "rotate-right", label: "Rotate selected pages right", keys: "Mod+]", section: "Pages" },
|
|
123
|
+
{ action: "rotate-left", label: "Rotate selected pages left", keys: "Mod+[", section: "Pages" },
|
|
124
|
+
{ action: "delete-pages", label: "Delete selected pages", keys: "Delete", section: "Pages" },
|
|
125
|
+
...PDF_TOOLS.filter((t) => t.key && t.status === "ready").map((t) => ({
|
|
126
|
+
action: `tool:${t.id}`,
|
|
127
|
+
label: t.label,
|
|
128
|
+
keys: t.key.toUpperCase(),
|
|
129
|
+
section: "Tools"
|
|
130
|
+
}))
|
|
131
|
+
];
|
|
132
|
+
function parse(keys) {
|
|
133
|
+
const parts = keys.split("+");
|
|
134
|
+
const key = parts[parts.length - 1] === "" ? "+" : parts[parts.length - 1];
|
|
135
|
+
return { mod: parts.includes("Mod"), shift: parts.includes("Shift"), key };
|
|
136
|
+
}
|
|
137
|
+
var TYPING_PASSES = /* @__PURE__ */ new Set(["find", "save", "print"]);
|
|
138
|
+
function matchShortcut(e, typing) {
|
|
139
|
+
const mod = e.ctrlKey || e.metaKey;
|
|
140
|
+
const pressed = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
141
|
+
for (const s of PDF_SHORTCUTS) {
|
|
142
|
+
const want = parse(s.keys);
|
|
143
|
+
const wantKey = want.key.length === 1 ? want.key.toLowerCase() : want.key;
|
|
144
|
+
if (want.mod !== mod || e.altKey) continue;
|
|
145
|
+
const shiftOk = want.key === "?" ? true : want.shift === e.shiftKey || want.key === "=" && e.shiftKey && pressed === "+";
|
|
146
|
+
if (!shiftOk) continue;
|
|
147
|
+
if (pressed === wantKey || want.key === "=" && pressed === "+") {
|
|
148
|
+
if (typing && !TYPING_PASSES.has(s.action)) return null;
|
|
149
|
+
return s.action;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function formatKeys(keys, mac) {
|
|
155
|
+
return keys.split("+").map((k) => k === "Mod" ? mac ? "\u2318" : "Ctrl" : k === "Shift" ? mac ? "\u21E7" : "Shift" : k === "ArrowRight" ? "\u2192" : k === "ArrowLeft" ? "\u2190" : k).join(mac ? "" : "+");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/pdf/page-decisions.ts
|
|
159
|
+
function freshDecisions(pageCount) {
|
|
160
|
+
return { order: Array.from({ length: Math.max(0, pageCount) }, (_, i) => i + 1), turns: {}, deleted: [] };
|
|
161
|
+
}
|
|
162
|
+
var norm = (t) => (t % 360 + 360) % 360;
|
|
163
|
+
function turnPages(d, pages, by) {
|
|
164
|
+
const turns = { ...d.turns };
|
|
165
|
+
for (const p of pages) {
|
|
166
|
+
const next = norm((turns[p] ?? 0) + by);
|
|
167
|
+
if (next === 0) delete turns[p];
|
|
168
|
+
else turns[p] = next;
|
|
169
|
+
}
|
|
170
|
+
return { ...d, turns };
|
|
171
|
+
}
|
|
172
|
+
function deletePages(d, pages) {
|
|
173
|
+
const set = /* @__PURE__ */ new Set([...d.deleted, ...pages.filter((p) => d.order.includes(p))]);
|
|
174
|
+
if (set.size >= d.order.length) return d;
|
|
175
|
+
return { ...d, deleted: [...set].sort((a, b) => a - b) };
|
|
176
|
+
}
|
|
177
|
+
function restorePages(d, pages) {
|
|
178
|
+
const gone = new Set(pages);
|
|
179
|
+
return { ...d, deleted: d.deleted.filter((p) => !gone.has(p)) };
|
|
180
|
+
}
|
|
181
|
+
function reorderPages(d, order) {
|
|
182
|
+
if (order.length !== d.order.length || [...order].sort((a, b) => a - b).some((p, i) => p !== i + 1)) return d;
|
|
183
|
+
return { ...d, order: [...order] };
|
|
184
|
+
}
|
|
185
|
+
function isDirty(d) {
|
|
186
|
+
const moved = d.order.some((p, i) => p !== i + 1);
|
|
187
|
+
return moved || d.deleted.length > 0 || Object.keys(d.turns).length > 0;
|
|
188
|
+
}
|
|
189
|
+
function keptOrder(d) {
|
|
190
|
+
const gone = new Set(d.deleted);
|
|
191
|
+
return d.order.filter((p) => !gone.has(p));
|
|
192
|
+
}
|
|
193
|
+
function compilePageOps(d) {
|
|
194
|
+
const ops = [];
|
|
195
|
+
const gone = new Set(d.deleted);
|
|
196
|
+
for (const rotation of [90, 180, 270]) {
|
|
197
|
+
const pages = Object.entries(d.turns).filter(([p, t]) => t === rotation && !gone.has(Number(p))).map(([p]) => Number(p)).sort((a, b) => a - b);
|
|
198
|
+
if (pages.length) ops.push({ kind: "rotate", pages, rotation });
|
|
199
|
+
}
|
|
200
|
+
if (d.deleted.length) ops.push({ kind: "delete", pages: [...d.deleted] });
|
|
201
|
+
const kept = keptOrder(d);
|
|
202
|
+
const renumber = new Map([...kept].sort((a, b) => a - b).map((p, i) => [p, i + 1]));
|
|
203
|
+
const newOrder = kept.map((p) => renumber.get(p));
|
|
204
|
+
if (newOrder.some((p, i) => p !== i + 1)) ops.push({ kind: "reorder", newOrder });
|
|
205
|
+
return ops;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/pdf/session-history.ts
|
|
209
|
+
var HISTORY_LIMIT = 100;
|
|
210
|
+
function startHistory(value) {
|
|
211
|
+
return { past: [], present: { label: "", value }, future: [] };
|
|
212
|
+
}
|
|
213
|
+
function pushStep(h, label, value, limit = HISTORY_LIMIT) {
|
|
214
|
+
if (Object.is(h.present.value, value)) return h;
|
|
215
|
+
const past = [...h.past, h.present].slice(-limit);
|
|
216
|
+
return { past, present: { label, value }, future: [] };
|
|
217
|
+
}
|
|
218
|
+
function undo(h) {
|
|
219
|
+
const prev = h.past[h.past.length - 1];
|
|
220
|
+
if (!prev) return h;
|
|
221
|
+
return { past: h.past.slice(0, -1), present: prev, future: [h.present, ...h.future] };
|
|
222
|
+
}
|
|
223
|
+
function redo(h) {
|
|
224
|
+
const next = h.future[0];
|
|
225
|
+
if (!next) return h;
|
|
226
|
+
return { past: [...h.past, h.present], present: next, future: h.future.slice(1) };
|
|
227
|
+
}
|
|
228
|
+
var canUndo = (h) => h.past.length > 0;
|
|
229
|
+
var canRedo = (h) => h.future.length > 0;
|
|
230
|
+
var undoLabel = (h) => h.present.label;
|
|
231
|
+
var redoLabel = (h) => h.future[0]?.label ?? "";
|
|
232
|
+
//# sourceMappingURL=pdf-model.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pdf/model.ts","../src/pdf/tools.ts","../src/pdf/shortcuts.ts","../src/pdf/page-decisions.ts","../src/pdf/session-history.ts"],"sourcesContent":["/** The workspace's pure model: tools, shortcuts, page decisions, the session stack and the host ports. No React. */\nexport * from \"./tools\";\nexport * from \"./shortcuts\";\nexport * from \"./page-decisions\";\nexport * from \"./session-history\";\nexport type * from \"./ports\";\n","/**\n * The PDF workspace's tool rail, as data. Pure (no React, no icons): the host registers the\n * Coming-soon rows from here (`PDF_COMING_SOON`), the shell draws the rail from it, and the tests\n * check that the two agree.\n *\n * A tool is `ready` only where a real capability stands behind it today (`capability` names the\n * server operation or app route). Anything else is `soon`: the rail shows it plainly marked, and\n * choosing it announces its registered promise through `@ai-matrx/kit/coming-soon`. There is no\n * third state, so no dead button.\n */\nimport type { ComingSoonEntry } from \"@ai-matrx/kit/coming-soon\";\n\nexport type PdfToolId =\n | \"browse\"\n | \"organize\"\n | \"extract-pages\"\n | \"split\"\n | \"crop\"\n | \"redact\"\n | \"compress\"\n | \"scan\"\n | \"sign\"\n | \"extract-data\"\n | \"edit\"\n | \"forms\"\n | \"protect\"\n | \"ocr\"\n | \"convert\"\n | \"compare\"\n | \"stamp\";\n\nexport interface PdfToolDef {\n id: PdfToolId;\n label: string;\n /** Single-key shortcut that picks the tool (shown in the tooltip and the shortcut map). */\n key?: string;\n status: \"ready\" | \"soon\";\n /** The server operation or route behind a ready tool; the wiring test reads it. */\n capability?: string;\n /** What choosing a ready tool does: show its panel, or leave for another page. */\n opens: \"panel\" | \"page\" | \"none\";\n group: 0 | 1 | 2;\n /** One short line under the panel title. */\n hint?: string;\n}\n\nexport const PDF_TOOLS: readonly PdfToolDef[] = [\n { id: \"browse\", label: \"Browse\", key: \"v\", status: \"ready\", opens: \"none\", group: 0 },\n { id: \"organize\", label: \"Organize pages\", key: \"o\", status: \"ready\", capability: \"rotate-pages,delete-pages,reorder-pages\", opens: \"panel\", group: 0 },\n { id: \"extract-pages\", label: \"Extract pages\", key: \"e\", status: \"ready\", capability: \"extract-pages\", opens: \"panel\", group: 0 },\n { id: \"split\", label: \"Split\", key: \"s\", status: \"ready\", capability: \"split\", opens: \"panel\", group: 0 },\n { id: \"crop\", label: \"Crop pages\", key: \"c\", status: \"ready\", capability: \"crop-pages\", opens: \"panel\", group: 0 },\n { id: \"redact\", label: \"Redact\", key: \"r\", status: \"ready\", capability: \"redact-pattern\", opens: \"panel\", group: 1 },\n { id: \"compress\", label: \"Compress\", key: \"z\", status: \"ready\", capability: \"compress\", opens: \"panel\", group: 1 },\n { id: \"extract-data\", label: \"Text and tables\", key: \"t\", status: \"ready\", capability: \"extract-text-remote,extract-tables\", opens: \"panel\", group: 1 },\n { id: \"scan\", label: \"Scan\", status: \"ready\", capability: \"route:/tools/scanner\", opens: \"page\", group: 1 },\n { id: \"sign\", label: \"Sign\", status: \"ready\", capability: \"route:/esign/new\", opens: \"page\", group: 1 },\n { id: \"edit\", label: \"Edit text and images\", status: \"soon\", opens: \"none\", group: 2 },\n { id: \"forms\", label: \"Fill forms\", status: \"soon\", opens: \"none\", group: 2 },\n { id: \"protect\", label: \"Protect\", status: \"soon\", opens: \"none\", group: 2 },\n { id: \"ocr\", label: \"Recognize text\", status: \"soon\", opens: \"none\", group: 2 },\n { id: \"convert\", label: \"Convert\", status: \"soon\", opens: \"none\", group: 2 },\n { id: \"compare\", label: \"Compare\", status: \"soon\", opens: \"none\", group: 2 },\n { id: \"stamp\", label: \"Stamp and number\", status: \"soon\", opens: \"none\", group: 2 },\n];\n\nexport const pdfTool = (id: string): PdfToolDef | undefined => PDF_TOOLS.find((t) => t.id === id);\n\n/** The Coming-soon id of an unbuilt tool. */\nexport const comingSoonIdOf = (id: PdfToolId): string => `pdf.${id}`;\n\nconst PROMISES: Record<string, string> = {\n edit: \"Change text and swap images right on the page.\",\n forms: \"Fill in a form and keep the values in the file.\",\n protect: \"Lock a PDF with a password and limit what others can do.\",\n ocr: \"Turn a scanned PDF into one you can search and copy from.\",\n convert: \"Turn a PDF into Word, Excel or PowerPoint, and back.\",\n compare: \"Put two versions side by side and see what changed.\",\n stamp: \"Add watermarks, page numbers and Bates numbers.\",\n};\n\nconst PANEL_PROMISES: Record<string, ComingSoonEntry> = {\n \"pdf.layers\": {\n id: \"pdf.layers\",\n label: \"Layers\",\n owner: \"pdf\",\n promise: \"Show or hide the layers inside a PDF.\",\n stage: \"planned\",\n surfaces: [\"PDF workspace page panel\"],\n },\n};\n\n/** One row per unbuilt tool, ready for `registerComingSoon` or the host's registry table. */\nexport const PDF_COMING_SOON: Record<string, ComingSoonEntry> = {\n ...Object.fromEntries(\n PDF_TOOLS.filter((t) => t.status === \"soon\").map((t): [string, ComingSoonEntry] => {\n const id = comingSoonIdOf(t.id);\n return [id, { id, label: t.label, owner: \"pdf\", promise: PROMISES[t.id] ?? \"\", stage: \"planned\", surfaces: [\"PDF workspace tool rail\"] }];\n }),\n ),\n ...PANEL_PROMISES,\n};\n","/**\n * The PDF workspace's one keyboard map. A shortcut is data: the shell mounts ONE key handler that\n * asks `matchShortcut` for the action, and the shortcuts popover lists the same rows, so the map\n * on screen and the keys that work cannot drift (the test fires every documented key).\n */\nimport { PDF_TOOLS } from \"./tools\";\n\nexport type PdfActionId =\n | \"undo\" | \"redo\" | \"find\" | \"print\" | \"save\"\n | \"zoom-in\" | \"zoom-out\" | \"zoom-fit-width\" | \"zoom-fit-page\"\n | \"page-next\" | \"page-prev\" | \"page-first\" | \"page-last\"\n | \"rotate-right\" | \"rotate-left\" | \"delete-pages\" | \"select-all-pages\"\n | \"toggle-pages\" | \"toggle-properties\" | \"read-mode\" | \"two-up\" | \"shortcuts\"\n | `tool:${string}`;\n\nexport interface PdfShortcut {\n action: PdfActionId;\n label: string;\n /** Display keys: \"Mod+Z\" shows as Cmd+Z on a Mac and Ctrl+Z elsewhere. */\n keys: string;\n section: \"Edit\" | \"View\" | \"Pages\" | \"Tools\";\n}\n\nexport const PDF_SHORTCUTS: readonly PdfShortcut[] = [\n { action: \"undo\", label: \"Undo\", keys: \"Mod+Z\", section: \"Edit\" },\n { action: \"redo\", label: \"Redo\", keys: \"Mod+Shift+Z\", section: \"Edit\" },\n { action: \"save\", label: \"Apply page changes\", keys: \"Mod+S\", section: \"Edit\" },\n { action: \"find\", label: \"Find\", keys: \"Mod+F\", section: \"Edit\" },\n { action: \"print\", label: \"Print\", keys: \"Mod+P\", section: \"Edit\" },\n { action: \"zoom-in\", label: \"Zoom in\", keys: \"Mod+=\", section: \"View\" },\n { action: \"zoom-out\", label: \"Zoom out\", keys: \"Mod+-\", section: \"View\" },\n { action: \"zoom-fit-width\", label: \"Fit width\", keys: \"Mod+1\", section: \"View\" },\n { action: \"zoom-fit-page\", label: \"Fit page\", keys: \"Mod+0\", section: \"View\" },\n { action: \"two-up\", label: \"Two pages side by side\", keys: \"Mod+Shift+2\", section: \"View\" },\n { action: \"read-mode\", label: \"Reading mode\", keys: \"Mod+Shift+H\", section: \"View\" },\n { action: \"toggle-pages\", label: \"Page thumbnails\", keys: \"Mod+Shift+T\", section: \"View\" },\n { action: \"toggle-properties\", label: \"Properties panel\", keys: \"Mod+Shift+P\", section: \"View\" },\n { action: \"shortcuts\", label: \"Keyboard shortcuts\", keys: \"?\", section: \"View\" },\n { action: \"page-next\", label: \"Next page\", keys: \"ArrowRight\", section: \"Pages\" },\n { action: \"page-prev\", label: \"Previous page\", keys: \"ArrowLeft\", section: \"Pages\" },\n { action: \"page-first\", label: \"First page\", keys: \"Home\", section: \"Pages\" },\n { action: \"page-last\", label: \"Last page\", keys: \"End\", section: \"Pages\" },\n { action: \"select-all-pages\", label: \"Select all pages\", keys: \"Mod+A\", section: \"Pages\" },\n { action: \"rotate-right\", label: \"Rotate selected pages right\", keys: \"Mod+]\", section: \"Pages\" },\n { action: \"rotate-left\", label: \"Rotate selected pages left\", keys: \"Mod+[\", section: \"Pages\" },\n { action: \"delete-pages\", label: \"Delete selected pages\", keys: \"Delete\", section: \"Pages\" },\n ...PDF_TOOLS.filter((t) => t.key && t.status === \"ready\").map((t): PdfShortcut => ({\n action: `tool:${t.id}`,\n label: t.label,\n keys: t.key!.toUpperCase(),\n section: \"Tools\",\n })),\n];\n\nexport interface KeyLike {\n key: string;\n ctrlKey: boolean;\n metaKey: boolean;\n shiftKey: boolean;\n altKey: boolean;\n}\n\nfunction parse(keys: string) {\n const parts = keys.split(\"+\");\n const key = parts[parts.length - 1] === \"\" ? \"+\" : parts[parts.length - 1]!;\n return { mod: parts.includes(\"Mod\"), shift: parts.includes(\"Shift\"), key };\n}\n\nconst TYPING_PASSES = new Set<PdfActionId>([\"find\", \"save\", \"print\"]);\n\n/** The action a key press means, or null. Never matches while `typing` (a text field has the key). */\nexport function matchShortcut(e: KeyLike, typing: boolean): PdfActionId | null {\n const mod = e.ctrlKey || e.metaKey;\n const pressed = e.key.length === 1 ? e.key.toLowerCase() : e.key;\n for (const s of PDF_SHORTCUTS) {\n const want = parse(s.keys);\n const wantKey = want.key.length === 1 ? want.key.toLowerCase() : want.key;\n if (want.mod !== mod || e.altKey) continue;\n // \"?\" is typed with Shift; every other plain key must have no Shift unless the map says so.\n const shiftOk = want.key === \"?\" ? true : want.shift === e.shiftKey || (want.key === \"=\" && e.shiftKey && pressed === \"+\");\n if (!shiftOk) continue;\n if (pressed === wantKey || (want.key === \"=\" && pressed === \"+\")) {\n // A text field keeps its own keys (typing, its own undo, select all); only these still reach the shell.\n if (typing && !TYPING_PASSES.has(s.action)) return null;\n return s.action;\n }\n }\n return null;\n}\n\n/** \"Mod+Shift+Z\" → \"⌘⇧Z\" on a Mac, \"Ctrl+Shift+Z\" elsewhere. */\nexport function formatKeys(keys: string, mac: boolean): string {\n return keys\n .split(\"+\")\n .map((k) => (k === \"Mod\" ? (mac ? \"⌘\" : \"Ctrl\") : k === \"Shift\" ? (mac ? \"⇧\" : \"Shift\") : k === \"ArrowRight\" ? \"→\" : k === \"ArrowLeft\" ? \"←\" : k))\n .join(mac ? \"\" : \"+\");\n}\n","/**\n * The page decisions of one workspace session: which pages are turned, left out, or moved. Pure.\n *\n * Decisions are PENDING: the viewer and the page grid show them at once, and \"Apply\" sends them\n * to the server as the smallest list of page operations (`compilePageOps`). Pages are named by\n * their ORIGINAL 1-based number everywhere, so a decision survives any later move.\n */\n\nexport type Turn = 0 | 90 | 180 | 270;\n\nexport interface PageDecisions {\n /** Original page numbers in the order the person wants them (deleted pages stay in the list). */\n order: number[];\n /** Extra clockwise turn per original page; absent = 0. */\n turns: Record<number, Turn>;\n /** Original page numbers left out. */\n deleted: number[];\n}\n\nexport function freshDecisions(pageCount: number): PageDecisions {\n return { order: Array.from({ length: Math.max(0, pageCount) }, (_, i) => i + 1), turns: {}, deleted: [] };\n}\n\nconst norm = (t: number): Turn => ((((t % 360) + 360) % 360) as Turn);\n\nexport function turnPages(d: PageDecisions, pages: readonly number[], by: 90 | -90 | 180): PageDecisions {\n const turns = { ...d.turns };\n for (const p of pages) {\n const next = norm((turns[p] ?? 0) + by);\n if (next === 0) delete turns[p];\n else turns[p] = next;\n }\n return { ...d, turns };\n}\n\nexport function deletePages(d: PageDecisions, pages: readonly number[]): PageDecisions {\n const set = new Set([...d.deleted, ...pages.filter((p) => d.order.includes(p))]);\n // A document with no page left is not a document: the last page cannot be deleted.\n if (set.size >= d.order.length) return d;\n return { ...d, deleted: [...set].sort((a, b) => a - b) };\n}\n\nexport function restorePages(d: PageDecisions, pages: readonly number[]): PageDecisions {\n const gone = new Set(pages);\n return { ...d, deleted: d.deleted.filter((p) => !gone.has(p)) };\n}\n\n/** The new order, as the grid reports it (a list of original page numbers). */\nexport function reorderPages(d: PageDecisions, order: readonly number[]): PageDecisions {\n if (order.length !== d.order.length || [...order].sort((a, b) => a - b).some((p, i) => p !== i + 1)) return d;\n return { ...d, order: [...order] };\n}\n\nexport function isDirty(d: PageDecisions): boolean {\n const moved = d.order.some((p, i) => p !== i + 1);\n return moved || d.deleted.length > 0 || Object.keys(d.turns).length > 0;\n}\n\n/** The pages that will remain, in their new order. */\nexport function keptOrder(d: PageDecisions): number[] {\n const gone = new Set(d.deleted);\n return d.order.filter((p) => !gone.has(p));\n}\n\nexport type PageOp =\n | { kind: \"rotate\"; pages: number[]; rotation: 90 | 180 | 270 }\n | { kind: \"delete\"; pages: number[] }\n | { kind: \"reorder\"; newOrder: number[] };\n\n/**\n * The server operations that make the document match the decisions, in the order they must run:\n * turns (original numbering), deletions (original numbering), then one reorder over what remains\n * (renumbered 1..n). Nothing is emitted for a decision that changes nothing.\n */\nexport function compilePageOps(d: PageDecisions): PageOp[] {\n const ops: PageOp[] = [];\n const gone = new Set(d.deleted);\n for (const rotation of [90, 180, 270] as const) {\n const pages = Object.entries(d.turns)\n .filter(([p, t]) => t === rotation && !gone.has(Number(p)))\n .map(([p]) => Number(p))\n .sort((a, b) => a - b);\n if (pages.length) ops.push({ kind: \"rotate\", pages, rotation });\n }\n if (d.deleted.length) ops.push({ kind: \"delete\", pages: [...d.deleted] });\n const kept = keptOrder(d);\n const renumber = new Map([...kept].sort((a, b) => a - b).map((p, i) => [p, i + 1] as const));\n const newOrder = kept.map((p) => renumber.get(p)!);\n if (newOrder.some((p, i) => p !== i + 1)) ops.push({ kind: \"reorder\", newOrder });\n return ops;\n}\n","/**\n * The session's one undo/redo stack. Pure and generic: the workspace keeps page decisions and tool\n * form values in it, each step with the label the menu shows (\"Undo Rotate 3 pages\"). A new step\n * clears what was undone; the stack holds at most `limit` steps. Saved versions of the file are the\n * durable undo: applying changes empties this stack.\n */\nexport interface History<T> {\n past: Array<{ label: string; value: T }>;\n present: { label: string; value: T };\n future: Array<{ label: string; value: T }>;\n}\n\nexport const HISTORY_LIMIT = 100;\n\nexport function startHistory<T>(value: T): History<T> {\n return { past: [], present: { label: \"\", value }, future: [] };\n}\n\nexport function pushStep<T>(h: History<T>, label: string, value: T, limit = HISTORY_LIMIT): History<T> {\n if (Object.is(h.present.value, value)) return h;\n const past = [...h.past, h.present].slice(-limit);\n return { past, present: { label, value }, future: [] };\n}\n\nexport function undo<T>(h: History<T>): History<T> {\n const prev = h.past[h.past.length - 1];\n if (!prev) return h;\n return { past: h.past.slice(0, -1), present: prev, future: [h.present, ...h.future] };\n}\n\nexport function redo<T>(h: History<T>): History<T> {\n const next = h.future[0];\n if (!next) return h;\n return { past: [...h.past, h.present], present: next, future: h.future.slice(1) };\n}\n\nexport const canUndo = (h: History<unknown>) => h.past.length > 0;\nexport const canRedo = (h: History<unknown>) => h.future.length > 0;\n/** The label of the step Undo would take back. */\nexport const undoLabel = (h: History<unknown>) => h.present.label;\nexport const redoLabel = (h: History<unknown>) => h.future[0]?.label ?? \"\";\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { ComingSoonEntry } from '@ai-matrx/kit/coming-soon';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The PDF workspace's tool rail, as data. Pure (no React, no icons): the host registers the
|
|
6
|
+
* Coming-soon rows from here (`PDF_COMING_SOON`), the shell draws the rail from it, and the tests
|
|
7
|
+
* check that the two agree.
|
|
8
|
+
*
|
|
9
|
+
* A tool is `ready` only where a real capability stands behind it today (`capability` names the
|
|
10
|
+
* server operation or app route). Anything else is `soon`: the rail shows it plainly marked, and
|
|
11
|
+
* choosing it announces its registered promise through `@ai-matrx/kit/coming-soon`. There is no
|
|
12
|
+
* third state, so no dead button.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type PdfToolId = "browse" | "organize" | "extract-pages" | "split" | "crop" | "redact" | "compress" | "scan" | "sign" | "extract-data" | "edit" | "forms" | "protect" | "ocr" | "convert" | "compare" | "stamp";
|
|
16
|
+
interface PdfToolDef {
|
|
17
|
+
id: PdfToolId;
|
|
18
|
+
label: string;
|
|
19
|
+
/** Single-key shortcut that picks the tool (shown in the tooltip and the shortcut map). */
|
|
20
|
+
key?: string;
|
|
21
|
+
status: "ready" | "soon";
|
|
22
|
+
/** The server operation or route behind a ready tool; the wiring test reads it. */
|
|
23
|
+
capability?: string;
|
|
24
|
+
/** What choosing a ready tool does: show its panel, or leave for another page. */
|
|
25
|
+
opens: "panel" | "page" | "none";
|
|
26
|
+
group: 0 | 1 | 2;
|
|
27
|
+
/** One short line under the panel title. */
|
|
28
|
+
hint?: string;
|
|
29
|
+
}
|
|
30
|
+
declare const PDF_TOOLS: readonly PdfToolDef[];
|
|
31
|
+
declare const pdfTool: (id: string) => PdfToolDef | undefined;
|
|
32
|
+
/** The Coming-soon id of an unbuilt tool. */
|
|
33
|
+
declare const comingSoonIdOf: (id: PdfToolId) => string;
|
|
34
|
+
/** One row per unbuilt tool, ready for `registerComingSoon` or the host's registry table. */
|
|
35
|
+
declare const PDF_COMING_SOON: Record<string, ComingSoonEntry>;
|
|
36
|
+
|
|
37
|
+
type PdfActionId = "undo" | "redo" | "find" | "print" | "save" | "zoom-in" | "zoom-out" | "zoom-fit-width" | "zoom-fit-page" | "page-next" | "page-prev" | "page-first" | "page-last" | "rotate-right" | "rotate-left" | "delete-pages" | "select-all-pages" | "toggle-pages" | "toggle-properties" | "read-mode" | "two-up" | "shortcuts" | `tool:${string}`;
|
|
38
|
+
interface PdfShortcut {
|
|
39
|
+
action: PdfActionId;
|
|
40
|
+
label: string;
|
|
41
|
+
/** Display keys: "Mod+Z" shows as Cmd+Z on a Mac and Ctrl+Z elsewhere. */
|
|
42
|
+
keys: string;
|
|
43
|
+
section: "Edit" | "View" | "Pages" | "Tools";
|
|
44
|
+
}
|
|
45
|
+
declare const PDF_SHORTCUTS: readonly PdfShortcut[];
|
|
46
|
+
interface KeyLike {
|
|
47
|
+
key: string;
|
|
48
|
+
ctrlKey: boolean;
|
|
49
|
+
metaKey: boolean;
|
|
50
|
+
shiftKey: boolean;
|
|
51
|
+
altKey: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** The action a key press means, or null. Never matches while `typing` (a text field has the key). */
|
|
54
|
+
declare function matchShortcut(e: KeyLike, typing: boolean): PdfActionId | null;
|
|
55
|
+
/** "Mod+Shift+Z" → "⌘⇧Z" on a Mac, "Ctrl+Shift+Z" elsewhere. */
|
|
56
|
+
declare function formatKeys(keys: string, mac: boolean): string;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The page decisions of one workspace session: which pages are turned, left out, or moved. Pure.
|
|
60
|
+
*
|
|
61
|
+
* Decisions are PENDING: the viewer and the page grid show them at once, and "Apply" sends them
|
|
62
|
+
* to the server as the smallest list of page operations (`compilePageOps`). Pages are named by
|
|
63
|
+
* their ORIGINAL 1-based number everywhere, so a decision survives any later move.
|
|
64
|
+
*/
|
|
65
|
+
type Turn = 0 | 90 | 180 | 270;
|
|
66
|
+
interface PageDecisions {
|
|
67
|
+
/** Original page numbers in the order the person wants them (deleted pages stay in the list). */
|
|
68
|
+
order: number[];
|
|
69
|
+
/** Extra clockwise turn per original page; absent = 0. */
|
|
70
|
+
turns: Record<number, Turn>;
|
|
71
|
+
/** Original page numbers left out. */
|
|
72
|
+
deleted: number[];
|
|
73
|
+
}
|
|
74
|
+
declare function freshDecisions(pageCount: number): PageDecisions;
|
|
75
|
+
declare function turnPages(d: PageDecisions, pages: readonly number[], by: 90 | -90 | 180): PageDecisions;
|
|
76
|
+
declare function deletePages(d: PageDecisions, pages: readonly number[]): PageDecisions;
|
|
77
|
+
declare function restorePages(d: PageDecisions, pages: readonly number[]): PageDecisions;
|
|
78
|
+
/** The new order, as the grid reports it (a list of original page numbers). */
|
|
79
|
+
declare function reorderPages(d: PageDecisions, order: readonly number[]): PageDecisions;
|
|
80
|
+
declare function isDirty(d: PageDecisions): boolean;
|
|
81
|
+
/** The pages that will remain, in their new order. */
|
|
82
|
+
declare function keptOrder(d: PageDecisions): number[];
|
|
83
|
+
type PageOp = {
|
|
84
|
+
kind: "rotate";
|
|
85
|
+
pages: number[];
|
|
86
|
+
rotation: 90 | 180 | 270;
|
|
87
|
+
} | {
|
|
88
|
+
kind: "delete";
|
|
89
|
+
pages: number[];
|
|
90
|
+
} | {
|
|
91
|
+
kind: "reorder";
|
|
92
|
+
newOrder: number[];
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* The server operations that make the document match the decisions, in the order they must run:
|
|
96
|
+
* turns (original numbering), deletions (original numbering), then one reorder over what remains
|
|
97
|
+
* (renumbered 1..n). Nothing is emitted for a decision that changes nothing.
|
|
98
|
+
*/
|
|
99
|
+
declare function compilePageOps(d: PageDecisions): PageOp[];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The session's one undo/redo stack. Pure and generic: the workspace keeps page decisions and tool
|
|
103
|
+
* form values in it, each step with the label the menu shows ("Undo Rotate 3 pages"). A new step
|
|
104
|
+
* clears what was undone; the stack holds at most `limit` steps. Saved versions of the file are the
|
|
105
|
+
* durable undo: applying changes empties this stack.
|
|
106
|
+
*/
|
|
107
|
+
interface History<T> {
|
|
108
|
+
past: Array<{
|
|
109
|
+
label: string;
|
|
110
|
+
value: T;
|
|
111
|
+
}>;
|
|
112
|
+
present: {
|
|
113
|
+
label: string;
|
|
114
|
+
value: T;
|
|
115
|
+
};
|
|
116
|
+
future: Array<{
|
|
117
|
+
label: string;
|
|
118
|
+
value: T;
|
|
119
|
+
}>;
|
|
120
|
+
}
|
|
121
|
+
declare const HISTORY_LIMIT = 100;
|
|
122
|
+
declare function startHistory<T>(value: T): History<T>;
|
|
123
|
+
declare function pushStep<T>(h: History<T>, label: string, value: T, limit?: number): History<T>;
|
|
124
|
+
declare function undo<T>(h: History<T>): History<T>;
|
|
125
|
+
declare function redo<T>(h: History<T>): History<T>;
|
|
126
|
+
declare const canUndo: (h: History<unknown>) => boolean;
|
|
127
|
+
declare const canRedo: (h: History<unknown>) => boolean;
|
|
128
|
+
/** The label of the step Undo would take back. */
|
|
129
|
+
declare const undoLabel: (h: History<unknown>) => string;
|
|
130
|
+
declare const redoLabel: (h: History<unknown>) => string;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* What the PDF workspace needs from the app that mounts it. The package owns the chrome (rail,
|
|
134
|
+
* panels, top bar, find, undo, shortcuts); the host owns everything that touches a server, a route
|
|
135
|
+
* or the PDF engine: it injects them as ONE object (`PdfWorkspaceHost`), the same way the Sheets and
|
|
136
|
+
* Docs hosts inject theirs. Nothing here fails silently: an operation that cannot run REJECTS with a
|
|
137
|
+
* sentence and the shell shows it; an optional port that is absent hides its control (no dead button).
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
/** What the shell hands the injected viewer. */
|
|
141
|
+
interface PdfViewerState {
|
|
142
|
+
/** 1 = the container's width (the viewer's own zoom unit). */
|
|
143
|
+
zoom: number;
|
|
144
|
+
spread: "single" | "two-up";
|
|
145
|
+
/** Pending turns per page, degrees clockwise (see `PageDecisions`). */
|
|
146
|
+
rotations: Record<number, number>;
|
|
147
|
+
/** Changes when the file changed on the server: the viewer must load it again. */
|
|
148
|
+
reloadKey: number;
|
|
149
|
+
/** The document parsed with this many pages. */
|
|
150
|
+
onDocumentLoad(pageCount: number): void;
|
|
151
|
+
/** A page was measured, in PDF points (crop and fit-page need it). */
|
|
152
|
+
onPageSize(page: number, widthPt: number, heightPt: number): void;
|
|
153
|
+
}
|
|
154
|
+
type PdfRunRequest = {
|
|
155
|
+
tool: "apply-pages";
|
|
156
|
+
ops: PageOp[];
|
|
157
|
+
} | {
|
|
158
|
+
tool: "extract-pages";
|
|
159
|
+
pages: number[];
|
|
160
|
+
} | {
|
|
161
|
+
tool: "split";
|
|
162
|
+
maxPagesPerPart: number;
|
|
163
|
+
} | {
|
|
164
|
+
tool: "crop";
|
|
165
|
+
pages: number[];
|
|
166
|
+
box: {
|
|
167
|
+
x0: number;
|
|
168
|
+
y0: number;
|
|
169
|
+
x1: number;
|
|
170
|
+
y1: number;
|
|
171
|
+
};
|
|
172
|
+
} | {
|
|
173
|
+
tool: "redact";
|
|
174
|
+
pattern: string;
|
|
175
|
+
reason: string;
|
|
176
|
+
} | {
|
|
177
|
+
tool: "compress";
|
|
178
|
+
level: 1 | 2 | 3 | 4 | 5;
|
|
179
|
+
} | {
|
|
180
|
+
tool: "extract-text";
|
|
181
|
+
} | {
|
|
182
|
+
tool: "extract-tables";
|
|
183
|
+
pages: number[];
|
|
184
|
+
};
|
|
185
|
+
type PdfRunResult =
|
|
186
|
+
/** The file itself changed (a new version): the shell reloads the viewer. */
|
|
187
|
+
{
|
|
188
|
+
kind: "replaced";
|
|
189
|
+
version?: number;
|
|
190
|
+
}
|
|
191
|
+
/** A new file was made (the original is untouched). */
|
|
192
|
+
| {
|
|
193
|
+
kind: "file";
|
|
194
|
+
fileId: string;
|
|
195
|
+
name?: string;
|
|
196
|
+
} | {
|
|
197
|
+
kind: "text";
|
|
198
|
+
text: string;
|
|
199
|
+
} | {
|
|
200
|
+
kind: "tables";
|
|
201
|
+
tables: Array<{
|
|
202
|
+
page: number;
|
|
203
|
+
rows: string[][];
|
|
204
|
+
}>;
|
|
205
|
+
};
|
|
206
|
+
interface PdfWorkspaceHost {
|
|
207
|
+
file: {
|
|
208
|
+
id: string;
|
|
209
|
+
name: string;
|
|
210
|
+
};
|
|
211
|
+
/** The injected windowed viewer (`@ai-matrx/media/viewers/pdf`, continuous layout, no toolbar). */
|
|
212
|
+
renderViewer(state: PdfViewerState): ReactNode;
|
|
213
|
+
/** One page thumbnail (the left panel). Absent: the panel numbers its pages only. */
|
|
214
|
+
renderThumbnail?(page: number, widthPx: number): ReactNode;
|
|
215
|
+
/** Run one tool on the server. Rejects with a sentence on failure. */
|
|
216
|
+
run(request: PdfRunRequest): Promise<PdfRunResult>;
|
|
217
|
+
/** Open another page of the app for the same file (Scan, Sign). */
|
|
218
|
+
openTool(tool: "scan" | "sign"): void;
|
|
219
|
+
/** Open a file the workspace made. */
|
|
220
|
+
openFile(fileId: string): void;
|
|
221
|
+
download(): void;
|
|
222
|
+
/** Absent: no Share control. */
|
|
223
|
+
share?(): void;
|
|
224
|
+
/** Rename the file. Absent: the name is read-only. */
|
|
225
|
+
rename?(name: string): Promise<void>;
|
|
226
|
+
/** Print the whole file (`pages` null) or just these. Absent: no Print control. */
|
|
227
|
+
print?(pages: number[] | null): Promise<void>;
|
|
228
|
+
/** The layers (optional content) of the document. Absent: the Layers tab announces it is not built. */
|
|
229
|
+
layers?: {
|
|
230
|
+
list(): Promise<Array<{
|
|
231
|
+
id: string;
|
|
232
|
+
name: string;
|
|
233
|
+
visible: boolean;
|
|
234
|
+
}>>;
|
|
235
|
+
set(id: string, visible: boolean): Promise<void>;
|
|
236
|
+
};
|
|
237
|
+
/** The server's redaction pattern catalog (ids are the server's contract). Absent: a free pattern field. */
|
|
238
|
+
redactPatterns?(): Promise<Array<{
|
|
239
|
+
id: string;
|
|
240
|
+
label: string;
|
|
241
|
+
}>>;
|
|
242
|
+
/** False while the device is offline: the save pill says so and edits are not queued. */
|
|
243
|
+
online?: boolean;
|
|
244
|
+
/** Show a failure to the person (the host's toast). Defaults to an inline banner. */
|
|
245
|
+
notify?(message: string, tone: "error" | "info"): void;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { HISTORY_LIMIT, type History, type KeyLike, PDF_COMING_SOON, PDF_SHORTCUTS, PDF_TOOLS, type PageDecisions, type PageOp, type PdfActionId, type PdfRunRequest, type PdfRunResult, type PdfShortcut, type PdfToolDef, type PdfToolId, type PdfViewerState, type PdfWorkspaceHost, type Turn, canRedo, canUndo, comingSoonIdOf, compilePageOps, deletePages, formatKeys, freshDecisions, isDirty, keptOrder, matchShortcut, pdfTool, pushStep, redo, redoLabel, reorderPages, restorePages, startHistory, turnPages, undo, undoLabel };
|