@ai-matrx/office 0.8.48 → 0.8.50

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,25 @@
1
1
  # Changelog
2
2
 
3
- ## 0.8.48
3
+ ## 0.8.50
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.49
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 184cfc8959 90e2be3bbc -- apps/shared/office/src`
13
+
14
+ Changed shipped source files (2):
15
+ - `apps/shared/office/src/pdf/PdfWorkspace.tsx`
16
+ - `apps/shared/office/src/pdf/ports.ts`
17
+
18
+ ## 0.8.48
19
+
20
+ Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 5da59424ae 184cfc8959 -- apps/shared/office/src`
21
+ has no shipped-source changes outside tests, fixtures and docs). No consumer action required.
22
+
8
23
  ## 0.8.47
9
24
 
10
25
  - `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.
@@ -305,6 +320,7 @@ Changed shipped source files (8):
305
320
 
306
321
  ### Unreleased
307
322
 
323
+ - `./pdf`: `PdfWorkspaceHost.tools` lets the host bring its own rail tools (`PdfHostTool`: id, label, icon, `render(context)`, optional `wide` and `followsPage`); the shell gives each a rail button, an All-tools row, tool-search hit and the right panel, and tells it the page, page count, selection, `goToPage` and `reload`. `PdfViewerState.currentPage` is passed while a `followsPage` tool is open. Consumer action: none (optional).
308
324
  - **US spelling: the British spelling of "color" no longer exists in this package (Arman, 2026-10-21).** Every label, template string, comment, identifier, CSS class/variable and test name that spelled it the British way now says color. Guard: `design-system/src/us-spelling.test.ts` scans all of `apps/` for the word. Consumer action: none for imports; if you matched on a British-spelled identifier, class or CSS variable from this package, use the `color` spelling; update the package.
309
325
  - **Sheets co-editing on the ordered log (C6).** `@ai-matrx/office/univer/collab` adds the Sheets room: `openSheetsOtRoom` (the C2 session over the C5a/C5b group transform, saves at a quiet point with a matching hash, restore through `collab_replace`, undo items rebased over every remote op), `univerSheetsOtPort` (capture: onlyLocal/fromCollab filtered, grouped by outermost command with a try/finally depth guard on both execute paths, params allowlist, completeCellContent, exact merge removals, style objects never ids, whole-style overrides restated as merges, annotateFormulas, inverses from the applied params against a pre-state copy; a barrier or unknown id is `not_shared` and rolls the seat back), `sheetsStateHash` / `canonicalSheet` (merges as a set, defined names by id; the transform fuzz now hashes through it), and `startWorkbookOtRoom` / `workbookUsesOrderedRoom` (the `udt_workbook_snapshots` store, the C1 doors, presence). A remote row/column insert, remove or move carries this seat's selection along and never leaves it past the sheet's edge (the port's `selections` option; a stale whole-row selection after a peer's column delete crashed the editor in the live run). Remote applies run with `{ fromCollab: true }` only (not `onlyLocal`): Univer's formula engine skips onlyLocal value writes, so a peer's formula showed blank on every other seat in the live run. Flip is per room exactly like Docs. Consumer action: matrx-frontend's WorkbookEditor opens it when the workbook's room exists or `collab.ot_rooms` says so (shipped with this release); every other consumer: none.
310
326
 
@@ -1,5 +1,5 @@
1
1
  import { ComingSoonEntry } from '@ai-matrx/kit/coming-soon';
2
- import { ReactNode } from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
3
 
4
4
  /**
5
5
  * The PDF workspace's tool rail, as data. Pure (no React, no icons): the host registers the
@@ -146,6 +146,8 @@ interface PdfViewerState {
146
146
  rotations: Record<number, number>;
147
147
  /** Changes when the file changed on the server: the viewer must load it again. */
148
148
  reloadKey: number;
149
+ /** The page the person is on (1-based), passed only while a host tool with `followsPage` is open; otherwise 1. */
150
+ currentPage: number;
149
151
  /** The document parsed with this many pages. */
150
152
  onDocumentLoad(pageCount: number): void;
151
153
  /** A page was measured, in PDF points (crop and fit-page need it). */
@@ -203,6 +205,38 @@ type PdfRunResult =
203
205
  rows: string[][];
204
206
  }>;
205
207
  };
208
+ /** What the shell tells a host tool's panel about the session it sits in. */
209
+ interface PdfHostToolContext {
210
+ currentPage: number;
211
+ pageCount: number;
212
+ /** Original page numbers selected in the page panel (empty when none). */
213
+ selected: number[];
214
+ goToPage(n: number): void;
215
+ /** The file changed under the same id: draw the viewer and thumbnails again. */
216
+ reload(): void;
217
+ }
218
+ /**
219
+ * A tool the host brings to the rail. The shell gives it a rail button, a place in "All tools", the
220
+ * tool search and the right panel; the host owns what is inside (its own server calls and state).
221
+ * This is how an app folds its older studios into the workspace without the package knowing them.
222
+ */
223
+ interface PdfHostTool {
224
+ /** Unique among the host's tools. */
225
+ id: string;
226
+ label: string;
227
+ /** One short line under the panel title. */
228
+ hint?: string;
229
+ icon: ComponentType<{
230
+ className?: string;
231
+ }>;
232
+ /** The viewer receives `currentPage` while this tool is open (a one-page-at-a-time canvas needs it). */
233
+ followsPage?: boolean;
234
+ /** A wide panel (28rem) instead of the standard one (18rem). */
235
+ wide?: boolean;
236
+ /** Rail group: 1 (default) sits with the document tools. */
237
+ group?: 0 | 1 | 2;
238
+ render(context: PdfHostToolContext): ReactNode;
239
+ }
206
240
  interface PdfWorkspaceHost {
207
241
  file: {
208
242
  id: string;
@@ -212,6 +246,8 @@ interface PdfWorkspaceHost {
212
246
  renderViewer(state: PdfViewerState): ReactNode;
213
247
  /** One page thumbnail (the left panel). Absent: the panel numbers its pages only. */
214
248
  renderThumbnail?(page: number, widthPx: number): ReactNode;
249
+ /** A small badge drawn in the corner of one page's thumbnail (the app's annotation count, for example). Null or absent: none. */
250
+ renderPageBadge?(page: number): ReactNode;
215
251
  /** Run one tool on the server. Rejects with a sentence on failure. */
216
252
  run(request: PdfRunRequest): Promise<PdfRunResult>;
217
253
  /** Open another page of the app for the same file (Scan, Sign). */
@@ -239,10 +275,12 @@ interface PdfWorkspaceHost {
239
275
  id: string;
240
276
  label: string;
241
277
  }>>;
278
+ /** Extra tools the host brings (see `PdfHostTool`). */
279
+ tools?: PdfHostTool[];
242
280
  /** False while the device is offline: the save pill says so and edits are not queued. */
243
281
  online?: boolean;
244
282
  /** Show a failure to the person (the host's toast). Defaults to an inline banner. */
245
283
  notify?(message: string, tone: "error" | "info"): void;
246
284
  }
247
285
 
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 };
286
+ 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 };
@@ -1,5 +1,5 @@
1
1
  import { ComingSoonEntry } from '@ai-matrx/kit/coming-soon';
2
- import { ReactNode } from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
3
 
4
4
  /**
5
5
  * The PDF workspace's tool rail, as data. Pure (no React, no icons): the host registers the
@@ -146,6 +146,8 @@ interface PdfViewerState {
146
146
  rotations: Record<number, number>;
147
147
  /** Changes when the file changed on the server: the viewer must load it again. */
148
148
  reloadKey: number;
149
+ /** The page the person is on (1-based), passed only while a host tool with `followsPage` is open; otherwise 1. */
150
+ currentPage: number;
149
151
  /** The document parsed with this many pages. */
150
152
  onDocumentLoad(pageCount: number): void;
151
153
  /** A page was measured, in PDF points (crop and fit-page need it). */
@@ -203,6 +205,38 @@ type PdfRunResult =
203
205
  rows: string[][];
204
206
  }>;
205
207
  };
208
+ /** What the shell tells a host tool's panel about the session it sits in. */
209
+ interface PdfHostToolContext {
210
+ currentPage: number;
211
+ pageCount: number;
212
+ /** Original page numbers selected in the page panel (empty when none). */
213
+ selected: number[];
214
+ goToPage(n: number): void;
215
+ /** The file changed under the same id: draw the viewer and thumbnails again. */
216
+ reload(): void;
217
+ }
218
+ /**
219
+ * A tool the host brings to the rail. The shell gives it a rail button, a place in "All tools", the
220
+ * tool search and the right panel; the host owns what is inside (its own server calls and state).
221
+ * This is how an app folds its older studios into the workspace without the package knowing them.
222
+ */
223
+ interface PdfHostTool {
224
+ /** Unique among the host's tools. */
225
+ id: string;
226
+ label: string;
227
+ /** One short line under the panel title. */
228
+ hint?: string;
229
+ icon: ComponentType<{
230
+ className?: string;
231
+ }>;
232
+ /** The viewer receives `currentPage` while this tool is open (a one-page-at-a-time canvas needs it). */
233
+ followsPage?: boolean;
234
+ /** A wide panel (28rem) instead of the standard one (18rem). */
235
+ wide?: boolean;
236
+ /** Rail group: 1 (default) sits with the document tools. */
237
+ group?: 0 | 1 | 2;
238
+ render(context: PdfHostToolContext): ReactNode;
239
+ }
206
240
  interface PdfWorkspaceHost {
207
241
  file: {
208
242
  id: string;
@@ -212,6 +246,8 @@ interface PdfWorkspaceHost {
212
246
  renderViewer(state: PdfViewerState): ReactNode;
213
247
  /** One page thumbnail (the left panel). Absent: the panel numbers its pages only. */
214
248
  renderThumbnail?(page: number, widthPx: number): ReactNode;
249
+ /** A small badge drawn in the corner of one page's thumbnail (the app's annotation count, for example). Null or absent: none. */
250
+ renderPageBadge?(page: number): ReactNode;
215
251
  /** Run one tool on the server. Rejects with a sentence on failure. */
216
252
  run(request: PdfRunRequest): Promise<PdfRunResult>;
217
253
  /** Open another page of the app for the same file (Scan, Sign). */
@@ -239,10 +275,12 @@ interface PdfWorkspaceHost {
239
275
  id: string;
240
276
  label: string;
241
277
  }>>;
278
+ /** Extra tools the host brings (see `PdfHostTool`). */
279
+ tools?: PdfHostTool[];
242
280
  /** False while the device is offline: the save pill says so and edits are not queued. */
243
281
  online?: boolean;
244
282
  /** Show a failure to the person (the host's toast). Defaults to an inline banner. */
245
283
  notify?(message: string, tone: "error" | "info"): void;
246
284
  }
247
285
 
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 };
286
+ 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 };
package/dist/pdf.cjs CHANGED
@@ -157,6 +157,7 @@ function LeftPanel(p) {
157
157
  const turn = p.decisions.turns[page] ?? 0;
158
158
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative flex h-full w-full items-center justify-center overflow-hidden bg-card", children: [
159
159
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { transform: turn ? `rotate(${turn}deg)` : void 0 }, className: "flex h-full w-full items-center justify-center", children: p.host.renderThumbnail ? p.host.renderThumbnail(page, THUMB_W) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-muted-foreground", children: page }) }),
160
+ p.host.renderPageBadge ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pointer-events-none absolute right-1 top-1 z-10", "data-pdf-page-badge": page, children: p.host.renderPageBadge(page) }) : null,
160
161
  page === p.currentPage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pointer-events-none absolute inset-0 border-2 border-primary" }) : null,
161
162
  gone.has(page) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute inset-0 flex items-center justify-center bg-background/80 text-xs font-medium text-destructive", children: "Deleted" }) : null
162
163
  ] });
@@ -585,25 +586,41 @@ function useWidth(ref) {
585
586
  }, [ref]);
586
587
  return size;
587
588
  }
588
- function ToolRows({ tool, onPick }) {
589
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: PDF_TOOLS.filter((t) => t.id !== "browse").map((t) => {
590
- const Icon = PDF_TOOL_ICONS[t.id];
591
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { role: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
589
+ function ToolRows({ tool, hostTools, onPick }) {
590
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
591
+ hostTools.map((t) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { role: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
592
592
  "button",
593
593
  {
594
594
  type: "button",
595
595
  role: "menuitem",
596
- "aria-current": t.id === tool ? "true" : void 0,
597
- className: (0, import_cn.cn)("flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", t.id === tool && "bg-accent"),
598
- onClick: () => onPick(t.id),
596
+ "aria-current": tool === `host:${t.id}` ? "true" : void 0,
597
+ className: (0, import_cn.cn)("flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", tool === `host:${t.id}` && "bg-accent"),
598
+ onClick: () => onPick(`host:${t.id}`),
599
599
  children: [
600
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Icon, { className: "size-4 shrink-0" }),
601
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "flex-1 whitespace-nowrap", children: t.label }),
602
- t.status === "soon" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-muted-foreground", children: "Coming soon" }) : null
600
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(t.icon, { className: "size-4 shrink-0" }),
601
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "flex-1 whitespace-nowrap", children: t.label })
603
602
  ]
604
603
  }
605
- ) }, t.id);
606
- }) });
604
+ ) }, `host:${t.id}`)),
605
+ PDF_TOOLS.filter((t) => t.id !== "browse").map((t) => {
606
+ const Icon = PDF_TOOL_ICONS[t.id];
607
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { role: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
608
+ "button",
609
+ {
610
+ type: "button",
611
+ role: "menuitem",
612
+ "aria-current": t.id === tool ? "true" : void 0,
613
+ className: (0, import_cn.cn)("flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", t.id === tool && "bg-accent"),
614
+ onClick: () => onPick(t.id),
615
+ children: [
616
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Icon, { className: "size-4 shrink-0" }),
617
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "flex-1 whitespace-nowrap", children: t.label }),
618
+ t.status === "soon" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-muted-foreground", children: "Coming soon" }) : null
619
+ ]
620
+ }
621
+ ) }, t.id);
622
+ })
623
+ ] });
607
624
  }
608
625
  function PdfWorkspace({ host, className }) {
609
626
  const rootRef = (0, import_react3.useRef)(null);
@@ -617,6 +634,10 @@ function PdfWorkspace({ host, className }) {
617
634
  const decisions = history.present.value;
618
635
  const dirty = isDirty(decisions);
619
636
  const [tool, setTool] = (0, import_react3.useState)("browse");
637
+ const [hostToolId, setHostToolId] = (0, import_react3.useState)(null);
638
+ const hostTools = (0, import_react3.useMemo)(() => host.tools ?? [], [host.tools]);
639
+ const hostTool = hostToolId ? hostTools.find((t) => t.id === hostToolId) ?? null : null;
640
+ const activeToolKey = hostTool ? `host:${hostTool.id}` : tool;
620
641
  const [leftOpen, setLeftOpen] = (0, import_react3.useState)(true);
621
642
  const [rightOpen, setRightOpen] = (0, import_react3.useState)(true);
622
643
  const [leftTab, setLeftTab] = (0, import_react3.useState)("pages");
@@ -769,8 +790,17 @@ function PdfWorkspace({ host, className }) {
769
790
  const discard = (0, import_react3.useCallback)(() => setHistory(startHistory(freshDecisions(pageCount))), [pageCount]);
770
791
  const chooseTool = (0, import_react3.useCallback)(
771
792
  (id) => {
793
+ if (id.startsWith("host:")) {
794
+ setHostToolId(id.slice(5));
795
+ setResult(null);
796
+ setError(null);
797
+ setRightOpen(true);
798
+ if (compact) setSheet("tools");
799
+ return;
800
+ }
772
801
  const def = pdfTool(id);
773
802
  if (!def) return;
803
+ setHostToolId(null);
774
804
  if (def.status === "soon") {
775
805
  announceSoon(comingSoonIdOf(def.id)).catch(fail);
776
806
  return;
@@ -793,9 +823,10 @@ function PdfWorkspace({ host, className }) {
793
823
  if (t.status !== "ready") continue;
794
824
  groups[t.group].push({ id: t.id, label: t.label, icon: PDF_TOOL_ICONS[t.id], ...t.key ? { shortcut: t.key.toUpperCase() } : null });
795
825
  }
826
+ for (const t of hostTools) groups[t.group ?? 1].push({ id: `host:${t.id}`, label: t.label, icon: t.icon });
796
827
  groups[2].push({ id: ALL_TOOLS, label: "All tools", icon: import_icons2.MoreHorizontalIcon });
797
828
  return groups.filter((g) => g.length);
798
- }, []);
829
+ }, [hostTools]);
799
830
  const runAction = (0, import_react3.useCallback)(
800
831
  (action) => {
801
832
  if (action.startsWith("tool:")) return chooseTool(action.slice(5));
@@ -883,7 +914,10 @@ function PdfWorkspace({ host, className }) {
883
914
  }
884
915
  );
885
916
  const iconBtn = (label, icon, onClick, opts) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls4.Button, { variant: "quiet", icon, "aria-label": label, title: label, onClick, ...opts?.disabled !== void 0 ? { disabled: opts.disabled } : null, ...opts?.pressed !== void 0 ? { pressed: opts.pressed } : null });
886
- const toolHits = toolQuery.trim() ? PDF_TOOLS.filter((t) => t.label.toLowerCase().includes(toolQuery.trim().toLowerCase())) : [];
917
+ const toolHits = toolQuery.trim() ? [
918
+ ...hostTools.map((t) => ({ id: `host:${t.id}`, label: t.label, status: "ready" })),
919
+ ...PDF_TOOLS
920
+ ].filter((t) => t.label.toLowerCase().includes(toolQuery.trim().toLowerCase())) : [];
887
921
  const nameNode = editingName && host.rename ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
888
922
  import_controls4.Field,
889
923
  {
@@ -914,7 +948,13 @@ function PdfWorkspace({ host, className }) {
914
948
  children: host.file.name
915
949
  }
916
950
  );
917
- const rightPanel = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex h-full min-h-0 flex-col", children: [
951
+ const rightPanel = hostTool ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex h-full min-h-0 flex-col", "data-pdf-host-tool": hostTool.id, children: [
952
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "shrink-0 border-b border-border px-3 py-2", children: [
953
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "text-sm font-medium", children: hostTool.label }),
954
+ hostTool.hint ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "text-xs text-muted-foreground", children: hostTool.hint }) : null
955
+ ] }),
956
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "min-h-0 flex-1 overflow-hidden", children: hostTool.render({ currentPage, pageCount, selected: selectedPages, goToPage, reload: () => setReloadKey((k) => k + 1) }) })
957
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex h-full min-h-0 flex-col", children: [
918
958
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "shrink-0 border-b border-border px-3 py-2", children: [
919
959
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "text-sm font-medium", children: toolDef.label }),
920
960
  toolDef.hint ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "text-xs text-muted-foreground", children: toolDef.hint }) : null
@@ -961,9 +1001,10 @@ function PdfWorkspace({ host, className }) {
961
1001
  onLayersSoon: () => void announceSoon("pdf.layers").catch(fail)
962
1002
  }
963
1003
  );
1004
+ const viewerPage = hostTool?.followsPage ? currentPage : 1;
964
1005
  const viewer = (0, import_react3.useMemo)(
965
- () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react3.Fragment, { children: host.renderViewer({ zoom, spread, rotations: decisions.turns, reloadKey, onDocumentLoad, onPageSize }) }, reloadKey),
966
- [host, reloadKey, zoom, spread, decisions.turns, onDocumentLoad, onPageSize]
1006
+ () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react3.Fragment, { children: host.renderViewer({ zoom, spread, rotations: decisions.turns, reloadKey, currentPage: viewerPage, onDocumentLoad, onPageSize }) }, reloadKey),
1007
+ [host, reloadKey, zoom, spread, decisions.turns, viewerPage, onDocumentLoad, onPageSize]
967
1008
  );
968
1009
  const showChrome = !read;
969
1010
  const popCard = "absolute right-2 top-11 z-30 rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-lg";
@@ -1076,15 +1117,15 @@ function PdfWorkspace({ host, className }) {
1076
1117
  iconBtn("Reading mode", /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons2.ExpandIcon, {}), () => setRead(true))
1077
1118
  ] }) : null,
1078
1119
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative flex min-h-0 flex-1", children: [
1079
- showChrome && !compact ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex w-10 shrink-0 flex-col items-center border-r border-border bg-background py-2", "data-pdf-rail": "", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls4.ToolRail, { groups: railGroups, value: pop === "all-tools" ? ALL_TOOLS : tool, onValueChange: (id) => id === ALL_TOOLS ? setPop((p) => p === "all-tools" ? null : "all-tools") : id && chooseTool(id), "aria-label": "PDF tools" }) }) : null,
1120
+ showChrome && !compact ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex w-10 shrink-0 flex-col items-center border-r border-border bg-background py-2", "data-pdf-rail": "", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls4.ToolRail, { groups: railGroups, value: pop === "all-tools" ? ALL_TOOLS : activeToolKey, onValueChange: (id) => id === ALL_TOOLS ? setPop((p) => p === "all-tools" ? null : "all-tools") : id && chooseTool(id), "aria-label": "PDF tools" }) }) : null,
1080
1121
  showChrome && !compact && leftOpen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("aside", { className: "w-56 shrink-0 border-r border-border bg-background", "aria-label": "Pages", "data-pdf-left": "", children: leftPanel }) : null,
1081
1122
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("main", { className: "relative flex min-w-0 flex-1 flex-col", children: [
1082
1123
  findOpen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute right-4 top-2 z-20", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_RenderedFindBar.RenderedFindBar, { rootRef: scrollerRef, onClose: () => setFindOpen(false), label: "Find in document", focusRequest: findFocus, compact: true }) }) : null,
1083
1124
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: scrollerRef, tabIndex: 0, "data-pdf-scroller": "", className: "min-h-0 flex-1 overflow-auto bg-muted outline-none", children: viewer }),
1084
1125
  read ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute right-3 top-3 z-20", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_controls4.Button, { variant: "outline", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons2.XIcon, {}), onClick: () => setRead(false), children: "Exit reading mode" }) }) : null
1085
1126
  ] }),
1086
- showChrome && !compact && rightOpen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("aside", { className: "w-72 shrink-0 border-l border-border bg-background", "aria-label": "Properties", "data-pdf-right": "", children: rightPanel }) : null,
1087
- pop === "all-tools" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { role: "menu", "aria-label": "All tools", className: "absolute left-12 top-2 z-30 w-72 rounded-lg border border-border bg-popover p-1 shadow-lg", "data-pdf-all-tools": "", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ToolRows, { tool, onPick: (id) => {
1127
+ showChrome && !compact && rightOpen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("aside", { className: (0, import_cn.cn)("shrink-0 border-l border-border bg-background", hostTool?.wide ? "w-[28rem]" : "w-72"), "aria-label": "Properties", "data-pdf-right": "", children: rightPanel }) : null,
1128
+ pop === "all-tools" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { role: "menu", "aria-label": "All tools", className: "absolute left-12 top-2 z-30 w-72 rounded-lg border border-border bg-popover p-1 shadow-lg", "data-pdf-all-tools": "", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ToolRows, { tool: activeToolKey, hostTools, onPick: (id) => {
1088
1129
  setPop(null);
1089
1130
  chooseTool(id);
1090
1131
  } }) }) : null,
@@ -1115,7 +1156,7 @@ function PdfWorkspace({ host, className }) {
1115
1156
  compact && sheet ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1116
1157
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", "aria-label": "Close", className: "absolute inset-0 z-30 bg-background/60", onClick: () => setSheet(null) }),
1117
1158
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute inset-x-0 bottom-0 z-40 flex max-h-[75%] flex-col rounded-t-xl border-t border-border bg-background", role: "dialog", "aria-label": sheet === "pages" ? "Pages" : "Tools", children: sheet === "pages" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "h-[60vh] min-h-0", children: leftPanel }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex min-h-0 flex-col overflow-y-auto", children: [
1118
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: "p-2", role: "menu", "aria-label": "Tools", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ToolRows, { tool, onPick: chooseTool }) }),
1159
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: "p-2", role: "menu", "aria-label": "Tools", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ToolRows, { tool: activeToolKey, hostTools, onPick: chooseTool }) }),
1119
1160
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "border-t border-border", children: rightPanel })
1120
1161
  ] }) })
1121
1162
  ] }) : null