@papyrus-sdk/ui-react 0.2.19 → 0.2.20

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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // components/Topbar.tsx
2
- import { useEffect, useRef, useState } from "react";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { useViewerStore } from "@papyrus-sdk/core";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -17,6 +17,7 @@ var Topbar = ({
17
17
  showUpload = true,
18
18
  showSearch = true
19
19
  }) => {
20
+ const viewerState = useViewerStore();
20
21
  const {
21
22
  currentPage,
22
23
  pageCount,
@@ -29,13 +30,15 @@ var Topbar = ({
29
30
  toggleSidebarLeft,
30
31
  toggleSidebarRight,
31
32
  triggerScrollToPage
32
- } = useViewerStore();
33
+ } = viewerState;
34
+ const mobileTopbarVisible = viewerState.mobileTopbarVisible ?? true;
33
35
  const fileInputRef = useRef(null);
34
36
  const zoomTimerRef = useRef(null);
35
37
  const pendingZoomRef = useRef(null);
36
38
  const [pageInput, setPageInput] = useState(currentPage.toString());
37
39
  const [showPageThemes, setShowPageThemes] = useState(false);
38
40
  const [showMobileMenu, setShowMobileMenu] = useState(false);
41
+ const [isMobileViewport, setIsMobileViewport] = useState(false);
39
42
  const pageDigits = Math.max(2, String(pageCount || 1).length);
40
43
  const isDark = uiTheme === "dark";
41
44
  const canUseDOM = typeof document !== "undefined";
@@ -52,6 +55,18 @@ var Topbar = ({
52
55
  useEffect(() => {
53
56
  if (!hasMobileMenu) setShowMobileMenu(false);
54
57
  }, [hasMobileMenu]);
58
+ useEffect(() => {
59
+ if (!canUseDOM || typeof window.matchMedia !== "function") return;
60
+ const mediaQuery = window.matchMedia("(max-width: 639px)");
61
+ const updateViewport = () => setIsMobileViewport(mediaQuery.matches);
62
+ updateViewport();
63
+ if (typeof mediaQuery.addEventListener === "function") {
64
+ mediaQuery.addEventListener("change", updateViewport);
65
+ return () => mediaQuery.removeEventListener("change", updateViewport);
66
+ }
67
+ mediaQuery.addListener(updateViewport);
68
+ return () => mediaQuery.removeListener(updateViewport);
69
+ }, [canUseDOM]);
55
70
  useEffect(() => {
56
71
  if (!showMobileMenu || !canUseDOM) return;
57
72
  const previousOverflow = document.body.style.overflow;
@@ -65,6 +80,30 @@ var Topbar = ({
65
80
  window.removeEventListener("keydown", handleKeyDown);
66
81
  };
67
82
  }, [showMobileMenu, canUseDOM]);
83
+ const topbarStyle = useMemo(() => {
84
+ const mergedStyle = { ...style ?? {} };
85
+ if (!isMobileViewport) {
86
+ mergedStyle.transition = "height 180ms ease, opacity 160ms ease, padding 180ms ease, border-width 180ms ease";
87
+ return mergedStyle;
88
+ }
89
+ mergedStyle.transition = "height 180ms ease, opacity 160ms ease, padding 180ms ease, border-width 180ms ease";
90
+ mergedStyle.overflow = "hidden";
91
+ if (!mobileTopbarVisible) {
92
+ mergedStyle.height = 0;
93
+ mergedStyle.minHeight = 0;
94
+ mergedStyle.paddingTop = 0;
95
+ mergedStyle.paddingBottom = 0;
96
+ mergedStyle.borderBottomWidth = 0;
97
+ mergedStyle.opacity = 0;
98
+ mergedStyle.pointerEvents = "none";
99
+ return mergedStyle;
100
+ }
101
+ mergedStyle.height = 56;
102
+ mergedStyle.minHeight = 56;
103
+ mergedStyle.opacity = 1;
104
+ mergedStyle.pointerEvents = "auto";
105
+ return mergedStyle;
106
+ }, [isMobileViewport, mobileTopbarVisible, style]);
68
107
  const handleZoom = (delta) => {
69
108
  const baseZoom = pendingZoomRef.current ?? zoom;
70
109
  const nextZoom = Math.max(0.2, Math.min(5, baseZoom + delta));
@@ -355,7 +394,7 @@ var Topbar = ({
355
394
  {
356
395
  "data-papyrus-theme": uiTheme,
357
396
  className: `papyrus-topbar papyrus-theme relative h-14 border-b flex items-center px-3 sm:px-4 z-50 transition-colors duration-200 ${isDark ? "bg-[#1a1a1a] border-[#333] text-white" : "bg-white border-gray-200 text-gray-800"}`,
358
- style,
397
+ style: topbarStyle,
359
398
  children: [
360
399
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0 z-10", children: [
361
400
  showSidebarLeftToggle && /* @__PURE__ */ jsx(
@@ -1333,11 +1372,11 @@ var SidebarRight = ({ engine, style }) => {
1333
1372
  var SidebarRight_default = SidebarRight;
1334
1373
 
1335
1374
  // components/Viewer.tsx
1336
- import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState5 } from "react";
1375
+ import { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState5 } from "react";
1337
1376
  import { useViewerStore as useViewerStore5 } from "@papyrus-sdk/core";
1338
1377
 
1339
1378
  // components/PageRenderer.tsx
1340
- import { useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState4 } from "react";
1379
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState4 } from "react";
1341
1380
  import { useViewerStore as useViewerStore4, papyrusEvents } from "@papyrus-sdk/core";
1342
1381
  import {
1343
1382
  PapyrusEventType
@@ -1390,7 +1429,7 @@ var PageRenderer = ({
1390
1429
  "strikeout"
1391
1430
  ]);
1392
1431
  const canSelectText = activeTool === "select" || textMarkupTools.has(activeTool);
1393
- const hasSearchHits = useMemo(
1432
+ const hasSearchHits = useMemo2(
1394
1433
  () => Boolean(searchQuery?.trim()) && searchResults.some((res) => res.pageIndex === pageIndex),
1395
1434
  [searchQuery, searchResults, pageIndex]
1396
1435
  );
@@ -1411,14 +1450,14 @@ var PageRenderer = ({
1411
1450
  active = false;
1412
1451
  };
1413
1452
  }, [engine, pageIndex]);
1414
- const fitScale = useMemo(() => {
1453
+ const fitScale = useMemo2(() => {
1415
1454
  if (!availableWidth || !pageSize?.width) return 1;
1416
1455
  const targetWidth = Math.max(0, availableWidth - 48);
1417
1456
  if (!targetWidth) return 1;
1418
1457
  const rawScale = Math.min(1, targetWidth / pageSize.width);
1419
1458
  return Math.round(rawScale * SCALE_PRECISION) / SCALE_PRECISION;
1420
1459
  }, [availableWidth, pageSize]);
1421
- const displaySize = useMemo(() => {
1460
+ const displaySize = useMemo2(() => {
1422
1461
  if (!pageSize) return null;
1423
1462
  const scale = zoom * fitScale;
1424
1463
  return {
@@ -2083,7 +2122,11 @@ var MIN_ZOOM = 0.2;
2083
2122
  var MAX_ZOOM = 5;
2084
2123
  var WIDTH_SNAP_PX = 4;
2085
2124
  var WIDTH_HYSTERESIS_PX = 6;
2125
+ var MOBILE_HEADER_HIDE_DELTA_PX = 28;
2126
+ var MOBILE_HEADER_SHOW_DELTA_PX = 16;
2127
+ var MOBILE_HEADER_TOP_RESET_PX = 12;
2086
2128
  var Viewer = ({ engine, style }) => {
2129
+ const viewerState = useViewerStore5();
2087
2130
  const {
2088
2131
  pageCount,
2089
2132
  currentPage,
@@ -2096,7 +2139,8 @@ var Viewer = ({ engine, style }) => {
2096
2139
  annotationColor,
2097
2140
  setAnnotationColor,
2098
2141
  toolDockOpen
2099
- } = useViewerStore5();
2142
+ } = viewerState;
2143
+ const mobileTopbarVisible = viewerState.mobileTopbarVisible ?? true;
2100
2144
  const isDark = uiTheme === "dark";
2101
2145
  const viewerRef = useRef4(null);
2102
2146
  const colorPickerRef = useRef4(null);
@@ -2106,6 +2150,11 @@ var Viewer = ({ engine, style }) => {
2106
2150
  const jumpRef = useRef4(false);
2107
2151
  const jumpTimerRef = useRef4(null);
2108
2152
  const lastWidthRef = useRef4(null);
2153
+ const lastScrollTopRef = useRef4(0);
2154
+ const scrollDownAccumulatorRef = useRef4(0);
2155
+ const scrollUpAccumulatorRef = useRef4(0);
2156
+ const previousCurrentPageRef = useRef4(currentPage);
2157
+ const mobileTopbarVisibleRef = useRef4(mobileTopbarVisible);
2109
2158
  const pinchRef = useRef4({
2110
2159
  active: false,
2111
2160
  startDistance: 0,
@@ -2118,6 +2167,7 @@ var Viewer = ({ engine, style }) => {
2118
2167
  const [pageSizes, setPageSizes] = useState5({});
2119
2168
  const [colorPickerOpen, setColorPickerOpen] = useState5(false);
2120
2169
  const isCompact = availableWidth !== null && availableWidth < 820;
2170
+ const isMobileViewport = availableWidth !== null && availableWidth < 640;
2121
2171
  const paddingY = isCompact ? "py-10" : "py-16";
2122
2172
  const toolDockPosition = isCompact ? "bottom-4" : "bottom-8";
2123
2173
  const colorPalette = [
@@ -2130,6 +2180,13 @@ var Viewer = ({ engine, style }) => {
2130
2180
  "#8b5cf6",
2131
2181
  "#111827"
2132
2182
  ];
2183
+ const setMobileTopbarVisibility = (visible) => {
2184
+ if (mobileTopbarVisibleRef.current === visible) return;
2185
+ mobileTopbarVisibleRef.current = visible;
2186
+ setDocumentState({
2187
+ mobileTopbarVisible: visible
2188
+ });
2189
+ };
2133
2190
  useEffect4(() => {
2134
2191
  if (!colorPickerOpen) return;
2135
2192
  const handleClick = (event) => {
@@ -2144,6 +2201,9 @@ var Viewer = ({ engine, style }) => {
2144
2201
  useEffect4(() => {
2145
2202
  if (!toolDockOpen && colorPickerOpen) setColorPickerOpen(false);
2146
2203
  }, [toolDockOpen, colorPickerOpen]);
2204
+ useEffect4(() => {
2205
+ mobileTopbarVisibleRef.current = mobileTopbarVisible;
2206
+ }, [mobileTopbarVisible]);
2147
2207
  useEffect4(
2148
2208
  () => () => {
2149
2209
  if (pinchRef.current.rafId != null) {
@@ -2195,6 +2255,64 @@ var Viewer = ({ engine, style }) => {
2195
2255
  observer.disconnect();
2196
2256
  };
2197
2257
  }, []);
2258
+ useEffect4(() => {
2259
+ const root = viewerRef.current;
2260
+ if (!root) return;
2261
+ if (!isMobileViewport) {
2262
+ lastScrollTopRef.current = root.scrollTop;
2263
+ scrollDownAccumulatorRef.current = 0;
2264
+ scrollUpAccumulatorRef.current = 0;
2265
+ setMobileTopbarVisibility(true);
2266
+ return;
2267
+ }
2268
+ lastScrollTopRef.current = root.scrollTop;
2269
+ scrollDownAccumulatorRef.current = 0;
2270
+ scrollUpAccumulatorRef.current = 0;
2271
+ const handleScroll = () => {
2272
+ const nextScrollTop = root.scrollTop;
2273
+ const delta = nextScrollTop - lastScrollTopRef.current;
2274
+ lastScrollTopRef.current = nextScrollTop;
2275
+ if (Math.abs(delta) < 1) return;
2276
+ if (nextScrollTop <= MOBILE_HEADER_TOP_RESET_PX) {
2277
+ scrollDownAccumulatorRef.current = 0;
2278
+ scrollUpAccumulatorRef.current = 0;
2279
+ setMobileTopbarVisibility(true);
2280
+ return;
2281
+ }
2282
+ if (delta > 0) {
2283
+ scrollDownAccumulatorRef.current += delta;
2284
+ scrollUpAccumulatorRef.current = 0;
2285
+ } else {
2286
+ scrollUpAccumulatorRef.current += -delta;
2287
+ scrollDownAccumulatorRef.current = 0;
2288
+ }
2289
+ if (scrollDownAccumulatorRef.current >= MOBILE_HEADER_HIDE_DELTA_PX && mobileTopbarVisibleRef.current) {
2290
+ scrollDownAccumulatorRef.current = 0;
2291
+ scrollUpAccumulatorRef.current = 0;
2292
+ setMobileTopbarVisibility(false);
2293
+ return;
2294
+ }
2295
+ if (scrollUpAccumulatorRef.current >= MOBILE_HEADER_SHOW_DELTA_PX && !mobileTopbarVisibleRef.current) {
2296
+ scrollDownAccumulatorRef.current = 0;
2297
+ scrollUpAccumulatorRef.current = 0;
2298
+ setMobileTopbarVisibility(true);
2299
+ }
2300
+ };
2301
+ root.addEventListener("scroll", handleScroll, { passive: true });
2302
+ return () => {
2303
+ root.removeEventListener("scroll", handleScroll);
2304
+ };
2305
+ }, [isMobileViewport, setDocumentState]);
2306
+ useEffect4(() => {
2307
+ const previousPage = previousCurrentPageRef.current;
2308
+ previousCurrentPageRef.current = currentPage;
2309
+ if (!isMobileViewport) return;
2310
+ if (currentPage < previousPage && !mobileTopbarVisibleRef.current) {
2311
+ scrollDownAccumulatorRef.current = 0;
2312
+ scrollUpAccumulatorRef.current = 0;
2313
+ setMobileTopbarVisibility(true);
2314
+ }
2315
+ }, [currentPage, isMobileViewport, setDocumentState]);
2198
2316
  useEffect4(() => {
2199
2317
  let active = true;
2200
2318
  if (!pageCount) return;
@@ -2309,7 +2427,7 @@ var Viewer = ({ engine, style }) => {
2309
2427
  const virtualAnchor = currentPage - 1;
2310
2428
  const virtualStart = Math.max(0, virtualAnchor - virtualOverscan);
2311
2429
  const virtualEnd = Math.min(pageCount - 1, virtualAnchor + virtualOverscan);
2312
- const fallbackSize = useMemo2(() => {
2430
+ const fallbackSize = useMemo3(() => {
2313
2431
  if (basePageSize && availableWidth) {
2314
2432
  const fitScale = Math.min(
2315
2433
  1,
@@ -2326,7 +2444,7 @@ var Viewer = ({ engine, style }) => {
2326
2444
  height: Math.round(base * zoom)
2327
2445
  };
2328
2446
  }, [basePageSize, availableWidth, zoom]);
2329
- const averagePageHeight = useMemo2(() => {
2447
+ const averagePageHeight = useMemo3(() => {
2330
2448
  const heights = Object.values(pageSizes).map((size) => size.height);
2331
2449
  if (!heights.length)
2332
2450
  return availableWidth ? Math.max(680, availableWidth * 1.3) : 1100;