@algenium/blocks 1.7.0-rc.3 → 1.7.0-rc.5

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/dist/index.cjs CHANGED
@@ -8205,6 +8205,68 @@ function PdfPageCanvas({
8205
8205
  }
8206
8206
  );
8207
8207
  }
8208
+ var PAGE_INPUT_CLASS = "h-8 w-14 rounded-md border px-2 text-sm tabular-nums shadow-sm border-neutral-300 bg-white text-neutral-950 caret-neutral-950 selection:bg-neutral-200 selection:text-neutral-950 dark:border-neutral-600 dark:bg-neutral-950 dark:text-neutral-50 dark:caret-neutral-50 dark:selection:bg-neutral-700 dark:selection:text-neutral-50";
8209
+ function touchPairDistance(touches) {
8210
+ if (touches.length < 2) return 0;
8211
+ const a = touches.item(0);
8212
+ const b = touches.item(1);
8213
+ return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
8214
+ }
8215
+ function PdfThumbnailStrip({
8216
+ page,
8217
+ pageNumber,
8218
+ active,
8219
+ onPick,
8220
+ label
8221
+ }) {
8222
+ const canvasRef = React2.useRef(null);
8223
+ React2.useEffect(() => {
8224
+ const canvas = canvasRef.current;
8225
+ if (!canvas) return;
8226
+ const viewport = page.getViewport({ scale: 1 });
8227
+ const thumbMax = 176;
8228
+ const s = thumbMax / Math.max(viewport.width, viewport.height);
8229
+ const vp = page.getViewport({ scale: s });
8230
+ const ctx = canvas.getContext("2d");
8231
+ if (!ctx) return;
8232
+ canvas.width = vp.width;
8233
+ canvas.height = vp.height;
8234
+ const task = page.render({
8235
+ canvasContext: ctx,
8236
+ viewport: vp,
8237
+ canvas
8238
+ });
8239
+ task.promise.catch(() => {
8240
+ });
8241
+ return () => {
8242
+ task.cancel();
8243
+ };
8244
+ }, [page]);
8245
+ return /* @__PURE__ */ jsxRuntime.jsxs(
8246
+ "button",
8247
+ {
8248
+ type: "button",
8249
+ onClick: () => onPick(pageNumber),
8250
+ className: cn(
8251
+ "flex w-full flex-col items-center gap-1 rounded-md p-2 transition-colors hover:bg-white/5",
8252
+ active ? "bg-white/10" : ""
8253
+ ),
8254
+ "aria-label": label,
8255
+ "aria-current": active ? "page" : void 0,
8256
+ children: [
8257
+ /* @__PURE__ */ jsxRuntime.jsx(
8258
+ "canvas",
8259
+ {
8260
+ ref: canvasRef,
8261
+ className: "block max-w-full overflow-hidden rounded-sm bg-black",
8262
+ "aria-hidden": true
8263
+ }
8264
+ ),
8265
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] tabular-nums text-neutral-400", children: pageNumber })
8266
+ ]
8267
+ }
8268
+ );
8269
+ }
8208
8270
  function PdfViewer({
8209
8271
  src,
8210
8272
  workerSrc,
@@ -8215,6 +8277,9 @@ function PdfViewer({
8215
8277
  maxScale = 4,
8216
8278
  scaleStep = 0.2,
8217
8279
  enableKeyboardShortcuts = true,
8280
+ enablePinchZoom = true,
8281
+ enableThumbnailSidebar = true,
8282
+ thumbnailSidebarDefaultOpen = true,
8218
8283
  labels,
8219
8284
  onLoad,
8220
8285
  onError,
@@ -8232,6 +8297,7 @@ function PdfViewer({
8232
8297
  const errorLabelFallback = mergedLabels.error ?? "Failed to load PDF";
8233
8298
  const toolbarId = React2.useId();
8234
8299
  const pageInputId = `${toolbarId}-page`;
8300
+ const thumbnailNavId = `${toolbarId}-thumbnails`;
8235
8301
  const scrollRef = React2.useRef(null);
8236
8302
  const observerRef = React2.useRef(null);
8237
8303
  const lastReportedPage = React2.useRef(0);
@@ -8245,6 +8311,9 @@ function PdfViewer({
8245
8311
  const [scale, setScale] = React2.useState(initialScale);
8246
8312
  const [ratios, setRatios] = React2.useState({});
8247
8313
  const [pageInput, setPageInput] = React2.useState(String(initialPage));
8314
+ const [thumbnailSidebarOpen, setThumbnailSidebarOpen] = React2.useState(
8315
+ thumbnailSidebarDefaultOpen
8316
+ );
8248
8317
  const baselineScaleRef = React2.useRef(initialScale);
8249
8318
  React2.useEffect(() => {
8250
8319
  baselineScaleRef.current = initialScale;
@@ -8255,6 +8324,9 @@ function PdfViewer({
8255
8324
  const zoomOutLabel = mergedLabels.zoomOut ?? "Zoom out";
8256
8325
  const resetZoomLabel = mergedLabels.resetZoom ?? "Reset zoom";
8257
8326
  const fitWidthLabel = mergedLabels.fitToWidth ?? "Fit to width";
8327
+ const thumbnailSidebarLabel = mergedLabels.thumbnailSidebar ?? "Page thumbnails";
8328
+ const toggleThumbLabel = mergedLabels.toggleThumbnailSidebar ?? "Toggle page thumbnails";
8329
+ const pageThumbLabel = mergedLabels.pageThumbnail ?? ((n) => `Page ${n}, show in document`);
8258
8330
  const pageCount = layouts.length;
8259
8331
  const currentVisiblePage = React2.useMemo(() => {
8260
8332
  let bestPage = 1;
@@ -8414,6 +8486,54 @@ function PdfViewer({
8414
8486
  React2.useEffect(() => {
8415
8487
  setScale(clampScaleValue(initialScale));
8416
8488
  }, [initialScale, clampScaleValue]);
8489
+ React2.useEffect(() => {
8490
+ const el = scrollRef.current;
8491
+ if (!el || !enablePinchZoom) return;
8492
+ let pinchDist = 0;
8493
+ const onWheel = (e) => {
8494
+ if (!e.ctrlKey) return;
8495
+ e.preventDefault();
8496
+ const delta = -e.deltaY;
8497
+ const factor = Math.exp(delta * 2e-3);
8498
+ setScale((prev) => {
8499
+ const c = clampScaleValue(prev * factor);
8500
+ onScaleChange?.(c);
8501
+ return c;
8502
+ });
8503
+ };
8504
+ const onTouchStart = (e) => {
8505
+ if (e.touches.length === 2) {
8506
+ pinchDist = touchPairDistance(e.touches);
8507
+ }
8508
+ };
8509
+ const onTouchMove = (e) => {
8510
+ if (e.touches.length !== 2 || pinchDist <= 0) return;
8511
+ e.preventDefault();
8512
+ const d = touchPairDistance(e.touches);
8513
+ const ratio = d / pinchDist;
8514
+ pinchDist = d;
8515
+ setScale((prev) => {
8516
+ const c = clampScaleValue(prev * ratio);
8517
+ onScaleChange?.(c);
8518
+ return c;
8519
+ });
8520
+ };
8521
+ const endPinch = (e) => {
8522
+ if (e.touches.length < 2) pinchDist = 0;
8523
+ };
8524
+ el.addEventListener("wheel", onWheel, { passive: false });
8525
+ el.addEventListener("touchstart", onTouchStart, { passive: true });
8526
+ el.addEventListener("touchmove", onTouchMove, { passive: false });
8527
+ el.addEventListener("touchend", endPinch);
8528
+ el.addEventListener("touchcancel", endPinch);
8529
+ return () => {
8530
+ el.removeEventListener("wheel", onWheel);
8531
+ el.removeEventListener("touchstart", onTouchStart);
8532
+ el.removeEventListener("touchmove", onTouchMove);
8533
+ el.removeEventListener("touchend", endPinch);
8534
+ el.removeEventListener("touchcancel", endPinch);
8535
+ };
8536
+ }, [enablePinchZoom, clampScaleValue, onScaleChange]);
8417
8537
  React2.useEffect(() => {
8418
8538
  if (layouts.length === 0) return;
8419
8539
  const frame = requestAnimationFrame(() => {
@@ -8519,6 +8639,7 @@ function PdfViewer({
8519
8639
  resetZoomAct
8520
8640
  ]
8521
8641
  );
8642
+ const thumbnailsRailEligible = enableThumbnailSidebar && !busy && pagesMap !== null && pageCount > 0;
8522
8643
  if (loadError) {
8523
8644
  return /* @__PURE__ */ jsxRuntime.jsx(
8524
8645
  "div",
@@ -8548,6 +8669,20 @@ function PdfViewer({
8548
8669
  toolbarClassName
8549
8670
  ),
8550
8671
  children: [
8672
+ thumbnailsRailEligible ? /* @__PURE__ */ jsxRuntime.jsx(
8673
+ Button,
8674
+ {
8675
+ type: "button",
8676
+ variant: "outline",
8677
+ size: "icon-sm",
8678
+ className: "hidden md:inline-flex",
8679
+ onClick: () => setThumbnailSidebarOpen((v) => !v),
8680
+ "aria-label": toggleThumbLabel,
8681
+ "aria-expanded": thumbnailSidebarOpen,
8682
+ "aria-controls": thumbnailNavId,
8683
+ children: thumbnailSidebarOpen ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftOpen, { className: "size-4", "aria-hidden": true }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelRightOpen, { className: "size-4", "aria-hidden": true })
8684
+ }
8685
+ ) : null,
8551
8686
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground text-xs tabular-nums sm:text-sm", children: busy || pagesMap === null ? "\u2014" : pageOfFormatter(currentVisiblePage, pageCount) }),
8552
8687
  /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: pageInputId, className: "sr-only", children: resolvedGoToLabel }),
8553
8688
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -8558,7 +8693,7 @@ function PdfViewer({
8558
8693
  min: 1,
8559
8694
  max: Math.max(pageCount, 1),
8560
8695
  disabled: busy || pageCount === 0,
8561
- className: "h-8 w-14 rounded-md border border-input bg-background px-2 text-sm tabular-nums",
8696
+ className: PAGE_INPUT_CLASS,
8562
8697
  "aria-label": resolvedGoToLabel,
8563
8698
  value: pageInput,
8564
8699
  onChange: (ev) => setPageInput(ev.target.value),
@@ -8622,32 +8757,56 @@ function PdfViewer({
8622
8757
  ]
8623
8758
  }
8624
8759
  ),
8625
- /* @__PURE__ */ jsxRuntime.jsx(
8626
- "div",
8627
- {
8628
- ref: scrollRef,
8629
- tabIndex: 0,
8630
- onKeyDown: handleKeyDown,
8631
- className: "relative min-h-[240px] flex-1 overflow-auto outline-none focus-visible:ring-2 focus-visible:ring-ring",
8632
- "aria-busy": busy,
8633
- "aria-label": busy ? loadingLabel : "PDF pages",
8634
- children: busy || pagesMap === null ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground p-6 text-sm", children: loadingLabel }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-center gap-4 pb-8 pt-2", children: layouts.map((layout) => {
8635
- const pg = pagesMap.get(layout.pageNumber);
8636
- const visible = (ratios[layout.pageNumber] ?? 0) >= 1e-3;
8637
- return pg ? /* @__PURE__ */ jsxRuntime.jsx(
8638
- PdfPageCanvas,
8639
- {
8640
- page: pg,
8641
- layout,
8642
- scale,
8643
- visible,
8644
- pageClassName
8645
- },
8646
- layout.pageNumber
8647
- ) : null;
8648
- }) })
8649
- }
8650
- )
8760
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [
8761
+ thumbnailsRailEligible && thumbnailSidebarOpen ? /* @__PURE__ */ jsxRuntime.jsx(
8762
+ "nav",
8763
+ {
8764
+ id: thumbnailNavId,
8765
+ className: "hidden w-[220px] shrink-0 flex-col overflow-y-auto border-r border-neutral-900 bg-black py-3 pl-3 pr-2 md:flex",
8766
+ "aria-label": thumbnailSidebarLabel,
8767
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2", children: layouts.map((layout) => {
8768
+ const pg = pagesMap.get(layout.pageNumber);
8769
+ return pg ? /* @__PURE__ */ jsxRuntime.jsx(
8770
+ PdfThumbnailStrip,
8771
+ {
8772
+ page: pg,
8773
+ pageNumber: layout.pageNumber,
8774
+ active: layout.pageNumber === currentVisiblePage,
8775
+ onPick: scrollToPage,
8776
+ label: pageThumbLabel(layout.pageNumber)
8777
+ },
8778
+ `thumb-${layout.pageNumber}`
8779
+ ) : null;
8780
+ }) })
8781
+ }
8782
+ ) : null,
8783
+ /* @__PURE__ */ jsxRuntime.jsx(
8784
+ "div",
8785
+ {
8786
+ ref: scrollRef,
8787
+ tabIndex: 0,
8788
+ onKeyDown: handleKeyDown,
8789
+ className: "relative min-h-[240px] min-w-0 flex-1 overflow-auto outline-none focus-visible:ring-2 focus-visible:ring-ring",
8790
+ "aria-busy": busy,
8791
+ "aria-label": busy ? loadingLabel : "PDF pages",
8792
+ children: busy || pagesMap === null ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground p-6 text-sm", children: loadingLabel }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-center gap-4 pb-8 pt-2", children: layouts.map((layout) => {
8793
+ const pg = pagesMap.get(layout.pageNumber);
8794
+ const visible = (ratios[layout.pageNumber] ?? 0) >= 1e-3;
8795
+ return pg ? /* @__PURE__ */ jsxRuntime.jsx(
8796
+ PdfPageCanvas,
8797
+ {
8798
+ page: pg,
8799
+ layout,
8800
+ scale,
8801
+ visible,
8802
+ pageClassName
8803
+ },
8804
+ layout.pageNumber
8805
+ ) : null;
8806
+ }) })
8807
+ }
8808
+ )
8809
+ ] })
8651
8810
  ]
8652
8811
  }
8653
8812
  );