@ai-matrx/office 0.8.49 → 0.8.51

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 CHANGED
@@ -1,10 +1,30 @@
1
1
  # Changelog
2
2
 
3
- ## 0.8.49
3
+ ## 0.8.51
4
4
 
5
5
  UNVERIFIED: this release has no Unreleased entry and the source comparison could not be made (the caller supplied no source comparison). Whether source changed is unknown; consumer action is unknown.
6
6
  Check the package's gitHead on npm against this version's gitHead: `git diff <previous gitHead> <this gitHead> -- apps/shared/office/src`.
7
7
 
8
+ ## 0.8.50
9
+
10
+ SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
11
+ so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
12
+ Diff: `git diff 90e2be3bbc 973cc3e9a3 -- apps/shared/office/src`
13
+
14
+ Changed shipped source files (2):
15
+ - `apps/shared/office/src/pdf/left-panel.tsx`
16
+ - `apps/shared/office/src/pdf/ports.ts`
17
+
18
+ ## 0.8.49
19
+
20
+ SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
21
+ so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
22
+ Diff: `git diff 184cfc8959 90e2be3bbc -- apps/shared/office/src`
23
+
24
+ Changed shipped source files (2):
25
+ - `apps/shared/office/src/pdf/PdfWorkspace.tsx`
26
+ - `apps/shared/office/src/pdf/ports.ts`
27
+
8
28
  ## 0.8.48
9
29
 
10
30
  Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 5da59424ae 184cfc8959 -- apps/shared/office/src`
@@ -19,6 +39,7 @@ has no shipped-source changes outside tests, fixtures and docs). No consumer act
19
39
 
20
40
  ## 0.8.46
21
41
 
42
+ - `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.
22
43
  - 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.
23
44
  - 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.
24
45
  - 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.
@@ -21,35 +21,140 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var model_exports = {};
22
22
  __export(model_exports, {
23
23
  HISTORY_LIMIT: () => HISTORY_LIMIT,
24
+ MARKUP_KIND_LABEL: () => MARKUP_KIND_LABEL,
25
+ MARKUP_SIZES: () => MARKUP_SIZES,
26
+ MARKUP_STYLE_FIELDS: () => MARKUP_STYLE_FIELDS,
27
+ MARKUP_SWATCHES: () => MARKUP_SWATCHES,
24
28
  PDF_COMING_SOON: () => PDF_COMING_SOON,
29
+ PDF_MARKUP_COMING_SOON: () => PDF_MARKUP_COMING_SOON,
30
+ PDF_MARKUP_SHORTCUTS: () => PDF_MARKUP_SHORTCUTS,
31
+ PDF_MARKUP_TOOLS: () => PDF_MARKUP_TOOLS,
25
32
  PDF_SHORTCUTS: () => PDF_SHORTCUTS,
26
33
  PDF_TOOLS: () => PDF_TOOLS,
34
+ TIMELINE_LIMIT: () => TIMELINE_LIMIT,
27
35
  canRedo: () => canRedo,
28
36
  canUndo: () => canUndo,
29
37
  comingSoonIdOf: () => comingSoonIdOf,
30
38
  compilePageOps: () => compilePageOps,
31
39
  deletePages: () => deletePages,
40
+ dropOrigin: () => dropOrigin,
41
+ emptyTimeline: () => emptyTimeline,
32
42
  formatKeys: () => formatKeys,
33
43
  freshDecisions: () => freshDecisions,
34
44
  isDirty: () => isDirty,
35
45
  keptOrder: () => keptOrder,
46
+ markupComingSoonId: () => markupComingSoonId,
47
+ markupRedoneElsewhere: () => markupRedoneElsewhere,
48
+ markupUndoneElsewhere: () => markupUndoneElsewhere,
49
+ matchMarkupKey: () => matchMarkupKey,
36
50
  matchShortcut: () => matchShortcut,
51
+ nextRedo: () => nextRedo,
52
+ nextUndo: () => nextUndo,
53
+ pdfMarkupTool: () => pdfMarkupTool,
37
54
  pdfTool: () => pdfTool,
38
55
  pushStep: () => pushStep,
56
+ recordStep: () => recordStep,
39
57
  redo: () => redo,
40
58
  redoLabel: () => redoLabel,
41
59
  reorderPages: () => reorderPages,
42
60
  restorePages: () => restorePages,
43
61
  startHistory: () => startHistory,
62
+ stepBack: () => stepBack,
63
+ stepForward: () => stepForward,
44
64
  turnPages: () => turnPages,
45
65
  undo: () => undo,
46
66
  undoLabel: () => undoLabel
47
67
  });
48
68
  module.exports = __toCommonJS(model_exports);
49
69
 
70
+ // src/pdf/markup.ts
71
+ var PDF_MARKUP_TOOLS = [
72
+ { id: "highlight", label: "Highlight", key: "h", status: "ready", engineTool: "highlight" },
73
+ { id: "freetext", label: "Text box", key: "x", status: "ready", engineTool: "freetext" },
74
+ { id: "ink", label: "Draw", key: "d", status: "ready", engineTool: "ink" },
75
+ { id: "image", label: "Image", key: "i", status: "ready", engineTool: "stamp" },
76
+ { id: "signature", label: "Signature", key: "g", status: "ready", engineTool: "stamp", needs: "signaturePad" },
77
+ { id: "underline", label: "Underline and strike out", status: "soon", reason: "The PDF writer we use makes highlights only." },
78
+ { id: "shapes", label: "Shapes", status: "soon", reason: "Rectangles, ovals and arrows need their own writer." },
79
+ { id: "note", label: "Sticky note", status: "soon", reason: "Comment on a highlight, text box or drawing for now." },
80
+ { id: "links", label: "Links", status: "soon", reason: "The PDF writer we use cannot add or change links." },
81
+ { id: "edit-text", label: "Edit page text", status: "soon", reason: "Changing the page's own text needs a server edit with a before-and-after check." }
82
+ ];
83
+ var pdfMarkupTool = (id) => PDF_MARKUP_TOOLS.find((t) => t.id === id);
84
+ var MARKUP_SWATCHES = [
85
+ { value: "#ffd400", label: "Yellow" },
86
+ { value: "#5fe36b", label: "Green" },
87
+ { value: "#4cc9f0", label: "Blue" },
88
+ { value: "#ff6fb5", label: "Pink" },
89
+ { value: "#ff4d4d", label: "Red" },
90
+ { value: "#1f2937", label: "Black" }
91
+ ];
92
+ var MARKUP_STYLE_FIELDS = {
93
+ highlight: ["color"],
94
+ freetext: ["color", "size"],
95
+ ink: ["color", "opacity", "size"],
96
+ stamp: [],
97
+ signature: []
98
+ };
99
+ var MARKUP_SIZES = {
100
+ freetext: [8, 10, 12, 14, 18, 24, 36],
101
+ ink: [1, 2, 3, 5, 8, 12]
102
+ };
103
+ var MARKUP_KIND_LABEL = {
104
+ highlight: "Highlight",
105
+ freetext: "Text box",
106
+ ink: "Drawing",
107
+ stamp: "Image",
108
+ signature: "Signature"
109
+ };
110
+ var MARKUP_PROMISES = {
111
+ underline: "Underline or strike out text, with the same colors as highlights.",
112
+ shapes: "Draw rectangles, ovals, lines and arrows on a page.",
113
+ note: "Pin a sticky note anywhere on a page.",
114
+ links: "Add, change and remove links to a page or a web address.",
115
+ "edit-text": "Change the page's own text, see a before-and-after view, then save."
116
+ };
117
+ var markupComingSoonId = (id) => id === "links" ? "pdf.links" : id === "edit-text" ? "pdf.edit-text" : `pdf.markup.${id}`;
118
+ var PDF_MARKUP_COMING_SOON = Object.fromEntries(
119
+ PDF_MARKUP_TOOLS.filter((t) => t.status === "soon").map((t) => {
120
+ const id = markupComingSoonId(t.id);
121
+ return [id, { id, label: t.label, owner: "pdf", promise: MARKUP_PROMISES[t.id] ?? "", stage: "planned", surfaces: ["PDF workspace markup panel"] }];
122
+ })
123
+ );
124
+ var TIMELINE_LIMIT = 100;
125
+ var emptyTimeline = () => ({ past: [], future: [] });
126
+ function recordStep(t, origin, label) {
127
+ return { past: [...t.past, { origin, label }].slice(-TIMELINE_LIMIT), future: [] };
128
+ }
129
+ var nextUndo = (t) => t.past[t.past.length - 1] ?? null;
130
+ var nextRedo = (t) => t.future[0] ?? null;
131
+ function stepBack(t) {
132
+ const last = t.past[t.past.length - 1];
133
+ return last ? { past: t.past.slice(0, -1), future: [last, ...t.future] } : t;
134
+ }
135
+ function stepForward(t) {
136
+ const next = t.future[0];
137
+ return next ? { past: [...t.past, next], future: t.future.slice(1) } : t;
138
+ }
139
+ function markupUndoneElsewhere(t) {
140
+ for (let i = t.past.length - 1; i >= 0; i--) {
141
+ if (t.past[i].origin === "markup") return { past: [...t.past.slice(0, i), ...t.past.slice(i + 1)], future: [t.past[i], ...t.future] };
142
+ }
143
+ return t;
144
+ }
145
+ function markupRedoneElsewhere(t) {
146
+ const i = t.future.findIndex((s) => s.origin === "markup");
147
+ if (i < 0) return t;
148
+ return { past: [...t.past, t.future[i]], future: [...t.future.slice(0, i), ...t.future.slice(i + 1)] };
149
+ }
150
+ function dropOrigin(t, origin) {
151
+ return { past: t.past.filter((s) => s.origin !== origin), future: t.future.filter((s) => s.origin !== origin) };
152
+ }
153
+
50
154
  // src/pdf/tools.ts
51
155
  var PDF_TOOLS = [
52
156
  { id: "browse", label: "Browse", key: "v", status: "ready", opens: "none", group: 0 },
157
+ { id: "markup", label: "Comment and mark up", key: "m", status: "ready", capability: "client:pdfjs-annotation-editors,save-new-version", opens: "panel", group: 0, hint: "Highlight, write, draw, sign. Saves a new version." },
53
158
  { id: "organize", label: "Organize pages", key: "o", status: "ready", capability: "rotate-pages,delete-pages,reorder-pages", opens: "panel", group: 0 },
54
159
  { id: "extract-pages", label: "Extract pages", key: "e", status: "ready", capability: "extract-pages", opens: "panel", group: 0 },
55
160
  { id: "split", label: "Split", key: "s", status: "ready", capability: "split", opens: "panel", group: 0 },
@@ -70,7 +175,7 @@ var PDF_TOOLS = [
70
175
  var pdfTool = (id) => PDF_TOOLS.find((t) => t.id === id);
71
176
  var comingSoonIdOf = (id) => `pdf.${id}`;
72
177
  var PROMISES = {
73
- edit: "Change text and swap images right on the page.",
178
+ edit: "Change the page's own text and swap images, with a before-and-after view.",
74
179
  forms: "Fill in a form and keep the values in the file.",
75
180
  protect: "Lock a PDF with a password and limit what others can do.",
76
181
  ocr: "Turn a scanned PDF into one you can search and copy from.",
@@ -95,14 +200,15 @@ var PDF_COMING_SOON = {
95
200
  return [id, { id, label: t.label, owner: "pdf", promise: PROMISES[t.id] ?? "", stage: "planned", surfaces: ["PDF workspace tool rail"] }];
96
201
  })
97
202
  ),
98
- ...PANEL_PROMISES
203
+ ...PANEL_PROMISES,
204
+ ...PDF_MARKUP_COMING_SOON
99
205
  };
100
206
 
101
207
  // src/pdf/shortcuts.ts
102
208
  var PDF_SHORTCUTS = [
103
209
  { action: "undo", label: "Undo", keys: "Mod+Z", section: "Edit" },
104
210
  { action: "redo", label: "Redo", keys: "Mod+Shift+Z", section: "Edit" },
105
- { action: "save", label: "Apply page changes", keys: "Mod+S", section: "Edit" },
211
+ { action: "save", label: "Save markup or apply page changes", keys: "Mod+S", section: "Edit" },
106
212
  { action: "find", label: "Find", keys: "Mod+F", section: "Edit" },
107
213
  { action: "print", label: "Print", keys: "Mod+P", section: "Edit" },
108
214
  { action: "zoom-in", label: "Zoom in", keys: "Mod+=", section: "View" },
@@ -129,6 +235,14 @@ var PDF_SHORTCUTS = [
129
235
  section: "Tools"
130
236
  }))
131
237
  ];
238
+ var PDF_MARKUP_SHORTCUTS = PDF_MARKUP_TOOLS.filter(
239
+ (t) => t.status === "ready" && t.key
240
+ ).map((t) => ({ tool: t.id, label: t.label, keys: t.key.toUpperCase() }));
241
+ function matchMarkupKey(e, typing) {
242
+ if (typing || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.key.length !== 1) return null;
243
+ const k = e.key.toLowerCase();
244
+ return PDF_MARKUP_SHORTCUTS.find((s) => s.keys.toLowerCase() === k)?.tool ?? null;
245
+ }
132
246
  function parse(keys) {
133
247
  const parts = keys.split("+");
134
248
  const key = parts[parts.length - 1] === "" ? "+" : parts[parts.length - 1];
@@ -1 +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":[]}
1
+ {"version":3,"sources":["../src/pdf/model.ts","../src/pdf/markup.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 * from \"./markup\";\nexport type * from \"./markup-engine-port\";\nexport type * from \"./ports\";\n","/**\n * The workspace's markup model (PDF-PLAN U10). Pure: the markup tools as data (which are built,\n * which are coming and why), their keys, the palette, and the ONE session history that holds both\n * page decisions and markup steps, so Undo takes back whichever came last.\n *\n * What is built rides PDF.js's own annotation editors (highlight, text box, draw, image; a\n * signature is an image made on the platform's signature pad; a comment is the note on a piece of\n * markup). What PDF.js's writer cannot make yet is a \"Coming soon\" row with its reason, never a\n * dead button: underline and strike-out, shapes, a free-standing sticky note, links, editing the\n * page's own text.\n */\nimport type { ComingSoonEntry } from \"@ai-matrx/kit/coming-soon\";\nimport type { PdfMarkupKind, PdfMarkupStyle, PdfMarkupTool } from \"./markup-engine-port\";\n\nexport type PdfMarkupToolId = \"highlight\" | \"freetext\" | \"ink\" | \"image\" | \"signature\" | \"underline\" | \"shapes\" | \"note\" | \"links\" | \"edit-text\";\n\nexport interface PdfMarkupToolDef {\n id: PdfMarkupToolId;\n label: string;\n /** Single key, while the markup tool is open. */\n key?: string;\n status: \"ready\" | \"soon\";\n /** The PDF.js editor a ready tool turns on. */\n engineTool?: Exclude<PdfMarkupTool, \"none\">;\n /** Ready only when the host brings this port. */\n needs?: \"signaturePad\";\n /** Why a coming tool is not built (shown under its name, and in its Coming-soon promise). */\n reason?: string;\n}\n\nexport const PDF_MARKUP_TOOLS: readonly PdfMarkupToolDef[] = [\n { id: \"highlight\", label: \"Highlight\", key: \"h\", status: \"ready\", engineTool: \"highlight\" },\n { id: \"freetext\", label: \"Text box\", key: \"x\", status: \"ready\", engineTool: \"freetext\" },\n { id: \"ink\", label: \"Draw\", key: \"d\", status: \"ready\", engineTool: \"ink\" },\n { id: \"image\", label: \"Image\", key: \"i\", status: \"ready\", engineTool: \"stamp\" },\n { id: \"signature\", label: \"Signature\", key: \"g\", status: \"ready\", engineTool: \"stamp\", needs: \"signaturePad\" },\n { id: \"underline\", label: \"Underline and strike out\", status: \"soon\", reason: \"The PDF writer we use makes highlights only.\" },\n { id: \"shapes\", label: \"Shapes\", status: \"soon\", reason: \"Rectangles, ovals and arrows need their own writer.\" },\n { id: \"note\", label: \"Sticky note\", status: \"soon\", reason: \"Comment on a highlight, text box or drawing for now.\" },\n { id: \"links\", label: \"Links\", status: \"soon\", reason: \"The PDF writer we use cannot add or change links.\" },\n { id: \"edit-text\", label: \"Edit page text\", status: \"soon\", reason: \"Changing the page's own text needs a server edit with a before-and-after check.\" },\n];\n\nexport const pdfMarkupTool = (id: string): PdfMarkupToolDef | undefined => PDF_MARKUP_TOOLS.find((t) => t.id === id);\n\n/** The palette markup offers (Acrobat's comment colors). Literal colors are data: they are written into the PDF. */\nexport const MARKUP_SWATCHES: ReadonlyArray<{ value: string; label: string }> = [\n { value: \"#ffd400\", label: \"Yellow\" },\n { value: \"#5fe36b\", label: \"Green\" },\n { value: \"#4cc9f0\", label: \"Blue\" },\n { value: \"#ff6fb5\", label: \"Pink\" },\n { value: \"#ff4d4d\", label: \"Red\" },\n { value: \"#1f2937\", label: \"Black\" },\n];\n\n/** Which look controls a kind of markup has (the floating toolbar and the panel show only these). */\nexport const MARKUP_STYLE_FIELDS: Record<PdfMarkupKind, ReadonlyArray<keyof PdfMarkupStyle>> = {\n highlight: [\"color\"],\n freetext: [\"color\", \"size\"],\n ink: [\"color\", \"opacity\", \"size\"],\n stamp: [],\n signature: [],\n};\n\n/** Sizes offered: font sizes for a text box, stroke widths for a drawing (PDF points). */\nexport const MARKUP_SIZES: Record<\"freetext\" | \"ink\", readonly number[]> = {\n freetext: [8, 10, 12, 14, 18, 24, 36],\n ink: [1, 2, 3, 5, 8, 12],\n};\n\nexport const MARKUP_KIND_LABEL: Record<PdfMarkupKind, string> = {\n highlight: \"Highlight\",\n freetext: \"Text box\",\n ink: \"Drawing\",\n stamp: \"Image\",\n signature: \"Signature\",\n};\n\nconst MARKUP_PROMISES: Record<string, string> = {\n underline: \"Underline or strike out text, with the same colors as highlights.\",\n shapes: \"Draw rectangles, ovals, lines and arrows on a page.\",\n note: \"Pin a sticky note anywhere on a page.\",\n links: \"Add, change and remove links to a page or a web address.\",\n \"edit-text\": \"Change the page's own text, see a before-and-after view, then save.\",\n};\n\nexport const markupComingSoonId = (id: PdfMarkupToolId): string => (id === \"links\" ? \"pdf.links\" : id === \"edit-text\" ? \"pdf.edit-text\" : `pdf.markup.${id}`);\n\n/** One Coming-soon row per markup tool not built yet (merged into `PDF_COMING_SOON`). */\nexport const PDF_MARKUP_COMING_SOON: Record<string, ComingSoonEntry> = Object.fromEntries(\n PDF_MARKUP_TOOLS.filter((t) => t.status === \"soon\").map((t): [string, ComingSoonEntry] => {\n const id = markupComingSoonId(t.id);\n return [id, { id, label: t.label, owner: \"pdf\", promise: MARKUP_PROMISES[t.id] ?? \"\", stage: \"planned\", surfaces: [\"PDF workspace markup panel\"] }];\n }),\n);\n\n// ── the session's one history: which kind of step Undo takes back next ──────────────────────\n\nexport type StepOrigin = \"page\" | \"markup\";\nexport interface SessionTimeline {\n past: Array<{ origin: StepOrigin; label: string }>;\n future: Array<{ origin: StepOrigin; label: string }>;\n}\nexport const TIMELINE_LIMIT = 100;\nexport const emptyTimeline = (): SessionTimeline => ({ past: [], future: [] });\n\nexport function recordStep(t: SessionTimeline, origin: StepOrigin, label: string): SessionTimeline {\n return { past: [...t.past, { origin, label }].slice(-TIMELINE_LIMIT), future: [] };\n}\n/** The step Undo would take back, or null. */\nexport const nextUndo = (t: SessionTimeline) => t.past[t.past.length - 1] ?? null;\nexport const nextRedo = (t: SessionTimeline) => t.future[0] ?? null;\nexport function stepBack(t: SessionTimeline): SessionTimeline {\n const last = t.past[t.past.length - 1];\n return last ? { past: t.past.slice(0, -1), future: [last, ...t.future] } : t;\n}\nexport function stepForward(t: SessionTimeline): SessionTimeline {\n const next = t.future[0];\n return next ? { past: [...t.past, next], future: t.future.slice(1) } : t;\n}\n/** PDF.js undid a markup step on its own (its keys, focus outside the workspace): move the newest markup step back. */\nexport function markupUndoneElsewhere(t: SessionTimeline): SessionTimeline {\n for (let i = t.past.length - 1; i >= 0; i--) {\n if (t.past[i]!.origin === \"markup\") return { past: [...t.past.slice(0, i), ...t.past.slice(i + 1)], future: [t.past[i]!, ...t.future] };\n }\n return t;\n}\nexport function markupRedoneElsewhere(t: SessionTimeline): SessionTimeline {\n const i = t.future.findIndex((s) => s.origin === \"markup\");\n if (i < 0) return t;\n return { past: [...t.past, t.future[i]!], future: [...t.future.slice(0, i), ...t.future.slice(i + 1)] };\n}\n/** Saving (or discarding) one kind of change ends its steps: the file's versions are the durable undo. */\nexport function dropOrigin(t: SessionTimeline, origin: StepOrigin): SessionTimeline {\n return { past: t.past.filter((s) => s.origin !== origin), future: t.future.filter((s) => s.origin !== origin) };\n}\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\";\nimport { PDF_MARKUP_COMING_SOON } from \"./markup\";\n\nexport type PdfToolId =\n | \"browse\"\n | \"markup\"\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: \"markup\", label: \"Comment and mark up\", key: \"m\", status: \"ready\", capability: \"client:pdfjs-annotation-editors,save-new-version\", opens: \"panel\", group: 0, hint: \"Highlight, write, draw, sign. Saves a new version.\" },\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 the page's own text and swap images, with a before-and-after view.\",\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 ...PDF_MARKUP_COMING_SOON,\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\";\nimport { PDF_MARKUP_TOOLS, type PdfMarkupToolId } from \"./markup\";\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\" | \"Markup\";\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: \"Save markup or 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\n/**\n * Keys that work only while \"Comment and mark up\" is open (they pick a markup tool). Listed in the\n * shortcut map under Markup; Delete, the arrows and Escape act on the selected markup there.\n */\nexport const PDF_MARKUP_SHORTCUTS: ReadonlyArray<{ tool: PdfMarkupToolId; label: string; keys: string }> = PDF_MARKUP_TOOLS.filter(\n (t) => t.status === \"ready\" && t.key,\n).map((t) => ({ tool: t.id, label: t.label, keys: t.key!.toUpperCase() }));\n\n/** The markup tool a plain key picks while markup is open, or null. */\nexport function matchMarkupKey(e: KeyLike, typing: boolean): PdfMarkupToolId | null {\n if (typing || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.key.length !== 1) return null;\n const k = e.key.toLowerCase();\n return PDF_MARKUP_SHORTCUTS.find((s) => s.keys.toLowerCase() === k)?.tool ?? null;\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":[]}
@@ -12,7 +12,7 @@ import { ComponentType, ReactNode } from 'react';
12
12
  * third state, so no dead button.
13
13
  */
14
14
 
15
- type PdfToolId = "browse" | "organize" | "extract-pages" | "split" | "crop" | "redact" | "compress" | "scan" | "sign" | "extract-data" | "edit" | "forms" | "protect" | "ocr" | "convert" | "compare" | "stamp";
15
+ type PdfToolId = "browse" | "markup" | "organize" | "extract-pages" | "split" | "crop" | "redact" | "compress" | "scan" | "sign" | "extract-data" | "edit" | "forms" | "protect" | "ocr" | "convert" | "compare" | "stamp";
16
16
  interface PdfToolDef {
17
17
  id: PdfToolId;
18
18
  label: string;
@@ -34,15 +34,159 @@ declare const comingSoonIdOf: (id: PdfToolId) => string;
34
34
  /** One row per unbuilt tool, ready for `registerComingSoon` or the host's registry table. */
35
35
  declare const PDF_COMING_SOON: Record<string, ComingSoonEntry>;
36
36
 
37
+ /**
38
+ * The markup session the workspace drives, as the workspace needs it. The implementation is
39
+ * `@ai-matrx/media/viewers/pdf` `createPdfMarkupEngine` (PDF.js's annotation editors over the
40
+ * windowed pages); this package does not import the PDF engine, so the contract is written here
41
+ * and the host hands media's engine through `PdfViewerState.onMarkupEngine`, where TypeScript
42
+ * checks the two agree. Change one, change the other (media `viewers/pdf/markup/markup-engine.ts`).
43
+ */
44
+ type PdfMarkupTool = "none" | "highlight" | "freetext" | "ink" | "stamp";
45
+ type PdfMarkupKind = "highlight" | "freetext" | "ink" | "stamp" | "signature";
46
+ interface PdfMarkupStyle {
47
+ /** `#rrggbb`. */
48
+ color: string;
49
+ /** 0-1. */
50
+ opacity: number;
51
+ /** Font size (text box) or stroke width (draw), PDF points. */
52
+ size: number;
53
+ }
54
+ interface PdfMarkupSnapshot {
55
+ tool: PdfMarkupTool;
56
+ pending: number;
57
+ summary: string;
58
+ canUndo: boolean;
59
+ canRedo: boolean;
60
+ selection: {
61
+ kind: PdfMarkupKind;
62
+ rect: {
63
+ x: number;
64
+ y: number;
65
+ width: number;
66
+ height: number;
67
+ };
68
+ style: PdfMarkupStyle;
69
+ comment: string;
70
+ } | null;
71
+ styles: Record<Exclude<PdfMarkupTool, "none">, PdfMarkupStyle>;
72
+ textSelected: boolean;
73
+ }
74
+ type PdfMarkupHistoryEvent = {
75
+ type: "step";
76
+ label: string;
77
+ } | {
78
+ type: "undo" | "redo";
79
+ external: boolean;
80
+ };
81
+ interface PdfMarkupEngine {
82
+ getSnapshot(): PdfMarkupSnapshot;
83
+ subscribe(listener: (snapshot: PdfMarkupSnapshot) => void): () => void;
84
+ onHistory(listener: (event: PdfMarkupHistoryEvent) => void): () => void;
85
+ setTool(tool: PdfMarkupTool): Promise<void>;
86
+ setStyle(patch: Partial<PdfMarkupStyle>): void;
87
+ setComment(text: string): void;
88
+ highlightSelection(): void;
89
+ addImage(image: Blob | string, kind: "stamp" | "signature"): Promise<void>;
90
+ deleteSelected(): void;
91
+ undo(): void;
92
+ redo(): void;
93
+ setCurrentPage(pageNumber: number): void;
94
+ save(): Promise<Uint8Array>;
95
+ destroy(): void;
96
+ }
97
+
98
+ /**
99
+ * The workspace's markup model (PDF-PLAN U10). Pure: the markup tools as data (which are built,
100
+ * which are coming and why), their keys, the palette, and the ONE session history that holds both
101
+ * page decisions and markup steps, so Undo takes back whichever came last.
102
+ *
103
+ * What is built rides PDF.js's own annotation editors (highlight, text box, draw, image; a
104
+ * signature is an image made on the platform's signature pad; a comment is the note on a piece of
105
+ * markup). What PDF.js's writer cannot make yet is a "Coming soon" row with its reason, never a
106
+ * dead button: underline and strike-out, shapes, a free-standing sticky note, links, editing the
107
+ * page's own text.
108
+ */
109
+
110
+ type PdfMarkupToolId = "highlight" | "freetext" | "ink" | "image" | "signature" | "underline" | "shapes" | "note" | "links" | "edit-text";
111
+ interface PdfMarkupToolDef {
112
+ id: PdfMarkupToolId;
113
+ label: string;
114
+ /** Single key, while the markup tool is open. */
115
+ key?: string;
116
+ status: "ready" | "soon";
117
+ /** The PDF.js editor a ready tool turns on. */
118
+ engineTool?: Exclude<PdfMarkupTool, "none">;
119
+ /** Ready only when the host brings this port. */
120
+ needs?: "signaturePad";
121
+ /** Why a coming tool is not built (shown under its name, and in its Coming-soon promise). */
122
+ reason?: string;
123
+ }
124
+ declare const PDF_MARKUP_TOOLS: readonly PdfMarkupToolDef[];
125
+ declare const pdfMarkupTool: (id: string) => PdfMarkupToolDef | undefined;
126
+ /** The palette markup offers (Acrobat's comment colors). Literal colors are data: they are written into the PDF. */
127
+ declare const MARKUP_SWATCHES: ReadonlyArray<{
128
+ value: string;
129
+ label: string;
130
+ }>;
131
+ /** Which look controls a kind of markup has (the floating toolbar and the panel show only these). */
132
+ declare const MARKUP_STYLE_FIELDS: Record<PdfMarkupKind, ReadonlyArray<keyof PdfMarkupStyle>>;
133
+ /** Sizes offered: font sizes for a text box, stroke widths for a drawing (PDF points). */
134
+ declare const MARKUP_SIZES: Record<"freetext" | "ink", readonly number[]>;
135
+ declare const MARKUP_KIND_LABEL: Record<PdfMarkupKind, string>;
136
+ declare const markupComingSoonId: (id: PdfMarkupToolId) => string;
137
+ /** One Coming-soon row per markup tool not built yet (merged into `PDF_COMING_SOON`). */
138
+ declare const PDF_MARKUP_COMING_SOON: Record<string, ComingSoonEntry>;
139
+ type StepOrigin = "page" | "markup";
140
+ interface SessionTimeline {
141
+ past: Array<{
142
+ origin: StepOrigin;
143
+ label: string;
144
+ }>;
145
+ future: Array<{
146
+ origin: StepOrigin;
147
+ label: string;
148
+ }>;
149
+ }
150
+ declare const TIMELINE_LIMIT = 100;
151
+ declare const emptyTimeline: () => SessionTimeline;
152
+ declare function recordStep(t: SessionTimeline, origin: StepOrigin, label: string): SessionTimeline;
153
+ /** The step Undo would take back, or null. */
154
+ declare const nextUndo: (t: SessionTimeline) => {
155
+ origin: StepOrigin;
156
+ label: string;
157
+ } | null;
158
+ declare const nextRedo: (t: SessionTimeline) => {
159
+ origin: StepOrigin;
160
+ label: string;
161
+ } | null;
162
+ declare function stepBack(t: SessionTimeline): SessionTimeline;
163
+ declare function stepForward(t: SessionTimeline): SessionTimeline;
164
+ /** PDF.js undid a markup step on its own (its keys, focus outside the workspace): move the newest markup step back. */
165
+ declare function markupUndoneElsewhere(t: SessionTimeline): SessionTimeline;
166
+ declare function markupRedoneElsewhere(t: SessionTimeline): SessionTimeline;
167
+ /** Saving (or discarding) one kind of change ends its steps: the file's versions are the durable undo. */
168
+ declare function dropOrigin(t: SessionTimeline, origin: StepOrigin): SessionTimeline;
169
+
37
170
  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
171
  interface PdfShortcut {
39
172
  action: PdfActionId;
40
173
  label: string;
41
174
  /** Display keys: "Mod+Z" shows as Cmd+Z on a Mac and Ctrl+Z elsewhere. */
42
175
  keys: string;
43
- section: "Edit" | "View" | "Pages" | "Tools";
176
+ section: "Edit" | "View" | "Pages" | "Tools" | "Markup";
44
177
  }
45
178
  declare const PDF_SHORTCUTS: readonly PdfShortcut[];
179
+ /**
180
+ * Keys that work only while "Comment and mark up" is open (they pick a markup tool). Listed in the
181
+ * shortcut map under Markup; Delete, the arrows and Escape act on the selected markup there.
182
+ */
183
+ declare const PDF_MARKUP_SHORTCUTS: ReadonlyArray<{
184
+ tool: PdfMarkupToolId;
185
+ label: string;
186
+ keys: string;
187
+ }>;
188
+ /** The markup tool a plain key picks while markup is open, or null. */
189
+ declare function matchMarkupKey(e: KeyLike, typing: boolean): PdfMarkupToolId | null;
46
190
  interface KeyLike {
47
191
  key: string;
48
192
  ctrlKey: boolean;
@@ -152,6 +296,12 @@ interface PdfViewerState {
152
296
  onDocumentLoad(pageCount: number): void;
153
297
  /** A page was measured, in PDF points (crop and fit-page need it). */
154
298
  onPageSize(page: number, widthPt: number, heightPt: number): void;
299
+ /**
300
+ * Present only when the host can save markup (`saveMarkup`): the viewer turns on PDF.js's
301
+ * annotation editors and hands their session up here (`null` when the document closes).
302
+ * `@ai-matrx/media/viewers/pdf` takes it as `PdfDocumentViewer.onMarkupEngine`.
303
+ */
304
+ onMarkupEngine?(engine: PdfMarkupEngine | null): void;
155
305
  }
156
306
  type PdfRunRequest = {
157
307
  tool: "apply-pages";
@@ -246,6 +396,8 @@ interface PdfWorkspaceHost {
246
396
  renderViewer(state: PdfViewerState): ReactNode;
247
397
  /** One page thumbnail (the left panel). Absent: the panel numbers its pages only. */
248
398
  renderThumbnail?(page: number, widthPx: number): ReactNode;
399
+ /** A small badge drawn in the corner of one page's thumbnail (the app's annotation count, for example). Null or absent: none. */
400
+ renderPageBadge?(page: number): ReactNode;
249
401
  /** Run one tool on the server. Rejects with a sentence on failure. */
250
402
  run(request: PdfRunRequest): Promise<PdfRunResult>;
251
403
  /** Open another page of the app for the same file (Scan, Sign). */
@@ -279,6 +431,24 @@ interface PdfWorkspaceHost {
279
431
  online?: boolean;
280
432
  /** Show a failure to the person (the host's toast). Defaults to an inline banner. */
281
433
  notify?(message: string, tone: "error" | "info"): void;
434
+ /**
435
+ * Save the file's bytes with markup written in (PDF.js `saveDocument`) as a NEW VERSION of the
436
+ * same file. `summary` is "2 highlights, 1 text box". A markup-only change never re-extracts the
437
+ * text (PDF-PLAN rule R1). Absent: no markup tools.
438
+ */
439
+ saveMarkup?(bytes: Uint8Array, summary: string): Promise<PdfRunResult>;
440
+ /** The annotations already in the file (Acrobat's, Preview's, ours), for the markup panel's list. */
441
+ existingAnnotations?(): Promise<Array<{
442
+ page: number;
443
+ subtype: string;
444
+ contents: string;
445
+ author: string;
446
+ }>>;
447
+ /** The platform's one signature pad (`@ai-matrx/records-ui` SignaturePad). Absent: no Signature tool. */
448
+ signaturePad?: ComponentType<{
449
+ value: unknown;
450
+ onChange: (value: string | null) => void;
451
+ }>;
282
452
  }
283
453
 
284
- export { HISTORY_LIMIT, type History, type KeyLike, PDF_COMING_SOON, PDF_SHORTCUTS, PDF_TOOLS, type PageDecisions, type PageOp, type PdfActionId, type PdfHostTool, type PdfHostToolContext, 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 };
454
+ export { HISTORY_LIMIT, type History, type KeyLike, MARKUP_KIND_LABEL, MARKUP_SIZES, MARKUP_STYLE_FIELDS, MARKUP_SWATCHES, PDF_COMING_SOON, PDF_MARKUP_COMING_SOON, PDF_MARKUP_SHORTCUTS, PDF_MARKUP_TOOLS, PDF_SHORTCUTS, PDF_TOOLS, type PageDecisions, type PageOp, type PdfActionId, type PdfHostTool, type PdfHostToolContext, type PdfMarkupEngine, type PdfMarkupHistoryEvent, type PdfMarkupKind, type PdfMarkupSnapshot, type PdfMarkupStyle, type PdfMarkupTool, type PdfMarkupToolDef, type PdfMarkupToolId, type PdfRunRequest, type PdfRunResult, type PdfShortcut, type PdfToolDef, type PdfToolId, type PdfViewerState, type PdfWorkspaceHost, type SessionTimeline, type StepOrigin, TIMELINE_LIMIT, type Turn, canRedo, canUndo, comingSoonIdOf, compilePageOps, deletePages, dropOrigin, emptyTimeline, formatKeys, freshDecisions, isDirty, keptOrder, markupComingSoonId, markupRedoneElsewhere, markupUndoneElsewhere, matchMarkupKey, matchShortcut, nextRedo, nextUndo, pdfMarkupTool, pdfTool, pushStep, recordStep, redo, redoLabel, reorderPages, restorePages, startHistory, stepBack, stepForward, turnPages, undo, undoLabel };