@papyrus-sdk/ui-react 0.2.19 → 0.2.21

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(
@@ -1063,10 +1102,15 @@ var SidebarRight = ({ engine, style }) => {
1063
1102
  setDocumentState,
1064
1103
  triggerScrollToPage,
1065
1104
  annotations,
1066
- accentColor
1105
+ accentColor,
1106
+ updateAnnotation,
1107
+ addAnnotationReply,
1108
+ setSelectedAnnotation
1067
1109
  } = useViewerStore3();
1068
1110
  const [query, setQuery] = useState3("");
1069
1111
  const [isSearching, setIsSearching] = useState3(false);
1112
+ const [contentDrafts, setContentDrafts] = useState3({});
1113
+ const [replyDrafts, setReplyDrafts] = useState3({});
1070
1114
  const searchService = new SearchService(engine);
1071
1115
  const isDark = uiTheme === "dark";
1072
1116
  const accentSoft = withAlpha2(accentColor, 0.12);
@@ -1082,6 +1126,41 @@ var SidebarRight = ({ engine, style }) => {
1082
1126
  setSearch(query, results);
1083
1127
  setIsSearching(false);
1084
1128
  };
1129
+ const jumpToAnnotation = (annotation) => {
1130
+ const page = annotation.pageIndex + 1;
1131
+ engine.goToPage(page);
1132
+ setDocumentState({ currentPage: page });
1133
+ setSelectedAnnotation(annotation.id);
1134
+ triggerScrollToPage(annotation.pageIndex);
1135
+ };
1136
+ const getContentDraft = (annotation) => {
1137
+ if (Object.prototype.hasOwnProperty.call(contentDrafts, annotation.id)) {
1138
+ return contentDrafts[annotation.id];
1139
+ }
1140
+ return annotation.content ?? "";
1141
+ };
1142
+ const updateContentDraft = (annotationId, nextValue) => {
1143
+ setContentDrafts((prev) => ({ ...prev, [annotationId]: nextValue }));
1144
+ };
1145
+ const submitContent = (annotation) => {
1146
+ const nextContent = getContentDraft(annotation).trim();
1147
+ const currentContent = (annotation.content ?? "").trim();
1148
+ if (nextContent === currentContent) return;
1149
+ updateAnnotation(annotation.id, {
1150
+ content: nextContent,
1151
+ updatedAt: Date.now()
1152
+ });
1153
+ };
1154
+ const getReplyDraft = (annotationId) => replyDrafts[annotationId] ?? "";
1155
+ const updateReplyDraft = (annotationId, nextValue) => {
1156
+ setReplyDrafts((prev) => ({ ...prev, [annotationId]: nextValue }));
1157
+ };
1158
+ const submitReply = (annotationId) => {
1159
+ const nextReply = getReplyDraft(annotationId).trim();
1160
+ if (!nextReply) return;
1161
+ addAnnotationReply(annotationId, nextReply);
1162
+ setReplyDrafts((prev) => ({ ...prev, [annotationId]: "" }));
1163
+ };
1085
1164
  if (!sidebarRightOpen) return null;
1086
1165
  return /* @__PURE__ */ jsxs3(
1087
1166
  "div",
@@ -1283,48 +1362,152 @@ var SidebarRight = ({ engine, style }) => {
1283
1362
  }
1284
1363
  ) }),
1285
1364
  /* @__PURE__ */ jsx3("p", { className: "text-[10px] font-bold text-gray-400 uppercase tracking-widest", children: "Sem anota\xE7\xF5es" })
1286
- ] }) : annotations.map((ann) => /* @__PURE__ */ jsxs3(
1287
- "div",
1288
- {
1289
- className: `p-4 rounded-xl border group transition-all cursor-pointer ${isDark ? "bg-[#222] border-[#333] hover:border-[#444]" : "bg-white border-gray-100 shadow-sm hover:shadow-md"}`,
1290
- children: [
1291
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between mb-3", children: [
1292
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center space-x-2", children: [
1365
+ ] }) : annotations.slice().sort(
1366
+ (a, b) => (b.updatedAt ?? b.createdAt) - (a.updatedAt ?? a.createdAt)
1367
+ ).map((ann) => {
1368
+ const isCommentThread = ann.type === "comment" || ann.type === "text";
1369
+ const replies = ann.replies ?? [];
1370
+ const contentDraft = getContentDraft(ann);
1371
+ const replyDraft = getReplyDraft(ann.id);
1372
+ const hasExistingContent = Boolean((ann.content ?? "").trim());
1373
+ return /* @__PURE__ */ jsxs3(
1374
+ "div",
1375
+ {
1376
+ className: "rounded-xl border p-4 transition-colors",
1377
+ style: {
1378
+ background: "var(--papyrus-surface-2-resolved, var(--papyrus-surface-2, #1f2937))",
1379
+ borderColor: "var(--papyrus-border-resolved, var(--papyrus-border, #374151))",
1380
+ color: "var(--papyrus-text-resolved, var(--papyrus-text, #e5e7eb))"
1381
+ },
1382
+ children: [
1383
+ /* @__PURE__ */ jsxs3("div", { className: "mb-3 flex items-center justify-between", children: [
1384
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center space-x-2", children: [
1385
+ /* @__PURE__ */ jsx3(
1386
+ "div",
1387
+ {
1388
+ className: "h-2.5 w-2.5 rounded-full",
1389
+ style: { backgroundColor: ann.color }
1390
+ }
1391
+ ),
1392
+ /* @__PURE__ */ jsxs3(
1393
+ "span",
1394
+ {
1395
+ className: "text-[10px] font-black uppercase tracking-wide",
1396
+ style: { color: accentColor },
1397
+ children: [
1398
+ "P",
1399
+ ann.pageIndex + 1
1400
+ ]
1401
+ }
1402
+ ),
1403
+ /* @__PURE__ */ jsx3("span", { className: "text-[10px] font-semibold opacity-70 uppercase tracking-wide", children: ann.type })
1404
+ ] }),
1293
1405
  /* @__PURE__ */ jsx3(
1294
- "div",
1406
+ "button",
1407
+ {
1408
+ type: "button",
1409
+ className: "rounded-md border px-2 py-1 text-[10px] font-semibold uppercase tracking-wide",
1410
+ onClick: () => jumpToAnnotation(ann),
1411
+ style: {
1412
+ borderColor: accentColor,
1413
+ color: accentColor
1414
+ },
1415
+ children: "Ir para pagina"
1416
+ }
1417
+ )
1418
+ ] }),
1419
+ isCommentThread ? /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
1420
+ /* @__PURE__ */ jsx3(
1421
+ "textarea",
1295
1422
  {
1296
- className: "w-2.5 h-2.5 rounded-full",
1297
- style: { backgroundColor: ann.color }
1423
+ className: "w-full resize-none rounded-md border p-2 text-xs focus:outline-none",
1424
+ style: {
1425
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #111827))",
1426
+ borderColor: "var(--papyrus-border-resolved, var(--papyrus-border, #374151))",
1427
+ color: "var(--papyrus-text-resolved, var(--papyrus-text, #e5e7eb))"
1428
+ },
1429
+ rows: 3,
1430
+ placeholder: "Escreva seu comentario...",
1431
+ value: contentDraft,
1432
+ onChange: (event) => updateContentDraft(ann.id, event.target.value),
1433
+ onKeyDown: (event) => {
1434
+ if (event.key === "Enter" && !event.shiftKey) {
1435
+ event.preventDefault();
1436
+ submitContent(ann);
1437
+ }
1438
+ }
1298
1439
  }
1299
1440
  ),
1300
- /* @__PURE__ */ jsxs3(
1301
- "span",
1441
+ /* @__PURE__ */ jsx3("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx3(
1442
+ "button",
1302
1443
  {
1303
- className: "text-[10px] font-black",
1304
- style: { color: accentColor },
1305
- children: [
1306
- "P",
1307
- ann.pageIndex + 1
1308
- ]
1444
+ type: "button",
1445
+ className: "rounded-md px-3 py-1.5 text-[11px] font-semibold text-white",
1446
+ style: { backgroundColor: accentColor },
1447
+ onClick: () => submitContent(ann),
1448
+ children: hasExistingContent ? "Atualizar comentario" : "Enviar comentario"
1449
+ }
1450
+ ) })
1451
+ ] }) : null,
1452
+ replies.length > 0 ? /* @__PURE__ */ jsx3("div", { className: "mt-3 space-y-2", children: replies.map((reply) => /* @__PURE__ */ jsxs3(
1453
+ "div",
1454
+ {
1455
+ className: "rounded-md border p-2",
1456
+ style: {
1457
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #111827))",
1458
+ borderColor: "var(--papyrus-border-resolved, var(--papyrus-border, #374151))"
1459
+ },
1460
+ children: [
1461
+ /* @__PURE__ */ jsx3("p", { className: "text-xs leading-relaxed", children: reply.content }),
1462
+ /* @__PURE__ */ jsx3("p", { className: "mt-1 text-[10px] opacity-70", children: new Date(reply.createdAt).toLocaleTimeString(
1463
+ [],
1464
+ {
1465
+ hour: "2-digit",
1466
+ minute: "2-digit"
1467
+ }
1468
+ ) })
1469
+ ]
1470
+ },
1471
+ reply.id
1472
+ )) }) : null,
1473
+ isCommentThread ? /* @__PURE__ */ jsxs3("div", { className: "mt-3 flex items-center gap-2", children: [
1474
+ /* @__PURE__ */ jsx3(
1475
+ "input",
1476
+ {
1477
+ type: "text",
1478
+ className: "flex-1 rounded-md border px-2 py-1.5 text-xs focus:outline-none",
1479
+ style: {
1480
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #111827))",
1481
+ borderColor: "var(--papyrus-border-resolved, var(--papyrus-border, #374151))",
1482
+ color: "var(--papyrus-text-resolved, var(--papyrus-text, #e5e7eb))"
1483
+ },
1484
+ value: replyDraft,
1485
+ placeholder: "Responder...",
1486
+ onChange: (event) => updateReplyDraft(ann.id, event.target.value),
1487
+ onKeyDown: (event) => {
1488
+ if (event.key === "Enter") {
1489
+ event.preventDefault();
1490
+ submitReply(ann.id);
1491
+ }
1492
+ }
1493
+ }
1494
+ ),
1495
+ /* @__PURE__ */ jsx3(
1496
+ "button",
1497
+ {
1498
+ type: "button",
1499
+ className: "rounded-md px-3 py-1.5 text-[11px] font-semibold text-white",
1500
+ style: { backgroundColor: accentColor },
1501
+ onClick: () => submitReply(ann.id),
1502
+ children: "Responder"
1309
1503
  }
1310
1504
  )
1311
- ] }),
1312
- /* @__PURE__ */ jsx3("span", { className: "text-[9px] text-gray-400 font-bold", children: new Date(ann.createdAt).toLocaleTimeString([], {
1313
- hour: "2-digit",
1314
- minute: "2-digit"
1315
- }) })
1316
- ] }),
1317
- /* @__PURE__ */ jsx3(
1318
- "p",
1319
- {
1320
- className: `text-[11px] font-bold uppercase tracking-tight ${isDark ? "text-gray-200" : "text-gray-700"}`,
1321
- children: ann.type
1322
- }
1323
- )
1324
- ]
1325
- },
1326
- ann.id
1327
- ))
1505
+ ] }) : null
1506
+ ]
1507
+ },
1508
+ ann.id
1509
+ );
1510
+ })
1328
1511
  ] }) })
1329
1512
  ]
1330
1513
  }
@@ -1333,11 +1516,11 @@ var SidebarRight = ({ engine, style }) => {
1333
1516
  var SidebarRight_default = SidebarRight;
1334
1517
 
1335
1518
  // components/Viewer.tsx
1336
- import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState5 } from "react";
1519
+ import { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState5 } from "react";
1337
1520
  import { useViewerStore as useViewerStore5 } from "@papyrus-sdk/core";
1338
1521
 
1339
1522
  // components/PageRenderer.tsx
1340
- import { useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState4 } from "react";
1523
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState4 } from "react";
1341
1524
  import { useViewerStore as useViewerStore4, papyrusEvents } from "@papyrus-sdk/core";
1342
1525
  import {
1343
1526
  PapyrusEventType
@@ -1354,6 +1537,10 @@ var PageRenderer = ({
1354
1537
  const canvasRef = useRef3(null);
1355
1538
  const htmlLayerRef = useRef3(null);
1356
1539
  const textLayerRef = useRef3(null);
1540
+ const skipNextAnnotationSelectRef = useRef3(false);
1541
+ const skipSelectResetTimerRef = useRef3(
1542
+ null
1543
+ );
1357
1544
  const [loading, setLoading] = useState4(true);
1358
1545
  const [pageSize, setPageSize] = useState4(null);
1359
1546
  const [isDragging, setIsDragging] = useState4(false);
@@ -1371,6 +1558,8 @@ var PageRenderer = ({
1371
1558
  setDocumentState,
1372
1559
  annotations,
1373
1560
  addAnnotation,
1561
+ addAnnotationReply,
1562
+ updateAnnotation,
1374
1563
  activeTool,
1375
1564
  removeAnnotation,
1376
1565
  selectedAnnotationId,
@@ -1390,10 +1579,28 @@ var PageRenderer = ({
1390
1579
  "strikeout"
1391
1580
  ]);
1392
1581
  const canSelectText = activeTool === "select" || textMarkupTools.has(activeTool);
1393
- const hasSearchHits = useMemo(
1582
+ const hasSearchHits = useMemo2(
1394
1583
  () => Boolean(searchQuery?.trim()) && searchResults.some((res) => res.pageIndex === pageIndex),
1395
1584
  [searchQuery, searchResults, pageIndex]
1396
1585
  );
1586
+ const suppressNextAnnotationSelect = () => {
1587
+ skipNextAnnotationSelectRef.current = true;
1588
+ if (skipSelectResetTimerRef.current) {
1589
+ clearTimeout(skipSelectResetTimerRef.current);
1590
+ }
1591
+ skipSelectResetTimerRef.current = setTimeout(() => {
1592
+ skipNextAnnotationSelectRef.current = false;
1593
+ skipSelectResetTimerRef.current = null;
1594
+ }, 0);
1595
+ };
1596
+ useEffect3(
1597
+ () => () => {
1598
+ if (skipSelectResetTimerRef.current) {
1599
+ clearTimeout(skipSelectResetTimerRef.current);
1600
+ }
1601
+ },
1602
+ []
1603
+ );
1397
1604
  useEffect3(() => {
1398
1605
  let active = true;
1399
1606
  const loadSize = async () => {
@@ -1411,14 +1618,14 @@ var PageRenderer = ({
1411
1618
  active = false;
1412
1619
  };
1413
1620
  }, [engine, pageIndex]);
1414
- const fitScale = useMemo(() => {
1621
+ const fitScale = useMemo2(() => {
1415
1622
  if (!availableWidth || !pageSize?.width) return 1;
1416
1623
  const targetWidth = Math.max(0, availableWidth - 48);
1417
1624
  if (!targetWidth) return 1;
1418
1625
  const rawScale = Math.min(1, targetWidth / pageSize.width);
1419
1626
  return Math.round(rawScale * SCALE_PRECISION) / SCALE_PRECISION;
1420
1627
  }, [availableWidth, pageSize]);
1421
- const displaySize = useMemo(() => {
1628
+ const displaySize = useMemo2(() => {
1422
1629
  if (!pageSize) return null;
1423
1630
  const scale = zoom * fitScale;
1424
1631
  return {
@@ -1564,6 +1771,16 @@ var PageRenderer = ({
1564
1771
  textLayerVersion
1565
1772
  ]);
1566
1773
  const handleMouseDown = (e) => {
1774
+ const target = e.target;
1775
+ const clickedInsideAnnotation = Boolean(
1776
+ target?.closest("[data-papyrus-annotation-id]")
1777
+ );
1778
+ const clickedSelectionMenu = Boolean(
1779
+ target?.closest("[data-papyrus-selection-menu]")
1780
+ );
1781
+ if (!clickedInsideAnnotation && !clickedSelectionMenu) {
1782
+ setSelectedAnnotation(null);
1783
+ }
1567
1784
  setSelectionMenu(null);
1568
1785
  if (activeTool === "ink") {
1569
1786
  const rect2 = containerRef.current?.getBoundingClientRect();
@@ -1620,6 +1837,7 @@ var PageRenderer = ({
1620
1837
  x: Math.max(0, Math.min(1, p.x)),
1621
1838
  y: Math.max(0, Math.min(1, p.y))
1622
1839
  }));
1840
+ suppressNextAnnotationSelect();
1623
1841
  addAnnotation({
1624
1842
  id: Math.random().toString(36).substr(2, 9),
1625
1843
  pageIndex,
@@ -1700,6 +1918,7 @@ var PageRenderer = ({
1700
1918
  height: Math.max(...ye) - Math.min(...ys)
1701
1919
  };
1702
1920
  if (textMarkupTools.has(activeTool)) {
1921
+ suppressNextAnnotationSelect();
1703
1922
  addAnnotation({
1704
1923
  id: Math.random().toString(36).substr(2, 9),
1705
1924
  pageIndex,
@@ -1735,6 +1954,9 @@ var PageRenderer = ({
1735
1954
  if (currentRect.w > 5 && currentRect.h > 5) {
1736
1955
  const rect = containerRef.current?.getBoundingClientRect();
1737
1956
  if (rect) {
1957
+ if (activeTool !== "text" && activeTool !== "comment") {
1958
+ suppressNextAnnotationSelect();
1959
+ }
1738
1960
  addAnnotation({
1739
1961
  id: Math.random().toString(36).substr(2, 9),
1740
1962
  pageIndex,
@@ -1858,6 +2080,7 @@ var PageRenderer = ({
1858
2080
  selectionMenu && /* @__PURE__ */ jsx4(
1859
2081
  "div",
1860
2082
  {
2083
+ "data-papyrus-selection-menu": "true",
1861
2084
  className: "absolute z-[60] flex items-center gap-1 rounded-full border px-2 py-1 shadow-xl bg-white/95 backdrop-blur-md text-gray-700",
1862
2085
  style: { left: selectionMenu.anchor.x, top: selectionMenu.anchor.y },
1863
2086
  children: [
@@ -1870,6 +2093,7 @@ var PageRenderer = ({
1870
2093
  {
1871
2094
  className: "text-[10px] font-bold px-2 py-1 rounded-full hover:bg-gray-100",
1872
2095
  onClick: () => {
2096
+ suppressNextAnnotationSelect();
1873
2097
  addAnnotation({
1874
2098
  id: Math.random().toString(36).substr(2, 9),
1875
2099
  pageIndex,
@@ -1896,7 +2120,15 @@ var PageRenderer = ({
1896
2120
  isSelected: selectedAnnotationId === ann.id,
1897
2121
  accentColor,
1898
2122
  onDelete: () => removeAnnotation(ann.id),
1899
- onSelect: () => setSelectedAnnotation(ann.id)
2123
+ onSelect: () => {
2124
+ if (skipNextAnnotationSelectRef.current) {
2125
+ skipNextAnnotationSelectRef.current = false;
2126
+ return;
2127
+ }
2128
+ setSelectedAnnotation(ann.id);
2129
+ },
2130
+ onUpdate: (updates) => updateAnnotation(ann.id, updates),
2131
+ onAddReply: (content) => addAnnotationReply(ann.id, content)
1900
2132
  },
1901
2133
  ann.id
1902
2134
  )) })
@@ -1904,12 +2136,43 @@ var PageRenderer = ({
1904
2136
  }
1905
2137
  );
1906
2138
  };
1907
- var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
2139
+ var AnnotationItem = ({
2140
+ ann,
2141
+ isSelected,
2142
+ accentColor,
2143
+ onDelete,
2144
+ onSelect,
2145
+ onUpdate,
2146
+ onAddReply
2147
+ }) => {
1908
2148
  const isText = ann.type === "text" || ann.type === "comment";
1909
2149
  const isHighlight = ann.type === "highlight";
1910
2150
  const isMarkup = ann.type === "highlight" || ann.type === "underline" || ann.type === "squiggly" || ann.type === "strikeout";
1911
2151
  const rects = ann.rects && ann.rects.length > 0 ? ann.rects : [ann.rect];
1912
2152
  const isInk = ann.type === "ink" && ann.path && ann.path.length > 1;
2153
+ const [draftContent, setDraftContent] = useState4(ann.content ?? "");
2154
+ const [draftReply, setDraftReply] = useState4("");
2155
+ useEffect3(() => {
2156
+ setDraftContent(ann.content ?? "");
2157
+ }, [ann.id, ann.content]);
2158
+ useEffect3(() => {
2159
+ setDraftReply("");
2160
+ }, [ann.id]);
2161
+ const handleSaveContent = () => {
2162
+ const nextContent = draftContent.trim();
2163
+ const currentContent = (ann.content ?? "").trim();
2164
+ if (nextContent === currentContent) return;
2165
+ onUpdate({
2166
+ content: nextContent,
2167
+ updatedAt: Date.now()
2168
+ });
2169
+ };
2170
+ const handleReplySubmit = () => {
2171
+ const nextReply = draftReply.trim();
2172
+ if (!nextReply) return;
2173
+ onAddReply(nextReply);
2174
+ setDraftReply("");
2175
+ };
1913
2176
  const renderMarkupRects = () => {
1914
2177
  if (!isMarkup) return null;
1915
2178
  return rects.map((r, idx) => {
@@ -2014,6 +2277,7 @@ var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
2014
2277
  return /* @__PURE__ */ jsxs4(
2015
2278
  "div",
2016
2279
  {
2280
+ "data-papyrus-annotation-id": ann.id,
2017
2281
  className: `absolute pointer-events-auto transition-all ${isSelected ? "shadow-xl z-30" : "z-10"}`,
2018
2282
  style: {
2019
2283
  left: `${ann.rect.x * 100}%`,
@@ -2032,16 +2296,151 @@ var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
2032
2296
  children: [
2033
2297
  renderMarkupRects(),
2034
2298
  renderInk(),
2035
- isText && isSelected && /* @__PURE__ */ jsx4("div", { className: "absolute top-full mt-2 w-64 bg-white shadow-2xl rounded-xl p-4 border border-gray-100 z-50", children: /* @__PURE__ */ jsx4(
2036
- "textarea",
2299
+ isText && !isSelected && /* @__PURE__ */ jsx4(
2300
+ "button",
2037
2301
  {
2038
- className: "w-full bg-transparent border-none focus:ring-0 p-0 text-gray-800 text-xs font-medium",
2039
- placeholder: "Escreva sua nota...",
2040
- rows: 3,
2041
- defaultValue: ann.content || "",
2042
- autoFocus: true
2302
+ type: "button",
2303
+ className: "absolute -top-2 -right-2 h-6 w-6 rounded-full flex items-center justify-center shadow-lg",
2304
+ style: {
2305
+ background: "var(--papyrus-surface-2-resolved, var(--papyrus-surface-2, #1f2937))",
2306
+ border: "1px solid var(--papyrus-border-resolved, #374151)",
2307
+ color: "var(--papyrus-text-resolved, #e5e7eb)"
2308
+ },
2309
+ title: "Abrir comentario",
2310
+ "aria-label": "Abrir comentario",
2311
+ onClick: (event) => {
2312
+ event.stopPropagation();
2313
+ onSelect();
2314
+ },
2315
+ children: /* @__PURE__ */ jsx4(
2316
+ "svg",
2317
+ {
2318
+ className: "h-3.5 w-3.5",
2319
+ fill: "none",
2320
+ stroke: "currentColor",
2321
+ viewBox: "0 0 24 24",
2322
+ children: /* @__PURE__ */ jsx4(
2323
+ "path",
2324
+ {
2325
+ strokeLinecap: "round",
2326
+ strokeLinejoin: "round",
2327
+ strokeWidth: 2,
2328
+ d: "M8 10h.01M12 10h.01M16 10h.01M7 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-4l-4 4v-4z"
2329
+ }
2330
+ )
2331
+ }
2332
+ )
2043
2333
  }
2044
- ) }),
2334
+ ),
2335
+ isText && isSelected && /* @__PURE__ */ jsxs4(
2336
+ "div",
2337
+ {
2338
+ className: "absolute top-full mt-2 w-72 rounded-xl p-3 z-50",
2339
+ style: {
2340
+ background: "var(--papyrus-popover-resolved, var(--papyrus-popover, #ffffff))",
2341
+ border: "1px solid var(--papyrus-border-resolved, #d1d5db)",
2342
+ color: "var(--papyrus-text-resolved, #111827)",
2343
+ boxShadow: "0 20px 40px var(--papyrus-shadow-resolved, rgba(0, 0, 0, 0.3))"
2344
+ },
2345
+ onClick: (event) => event.stopPropagation(),
2346
+ children: [
2347
+ /* @__PURE__ */ jsxs4("div", { className: "mb-2 flex items-center justify-between", children: [
2348
+ /* @__PURE__ */ jsx4("span", { className: "text-[10px] font-bold uppercase tracking-wider opacity-70", children: ann.type === "comment" ? "Comentario" : "Nota" }),
2349
+ ann.replies?.length ? /* @__PURE__ */ jsxs4("span", { className: "text-[10px] opacity-70", children: [
2350
+ ann.replies.length,
2351
+ " resposta",
2352
+ ann.replies.length > 1 ? "s" : ""
2353
+ ] }) : null
2354
+ ] }),
2355
+ /* @__PURE__ */ jsx4(
2356
+ "textarea",
2357
+ {
2358
+ className: "w-full rounded-md border p-2 text-xs font-medium resize-none focus:outline-none",
2359
+ style: {
2360
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #ffffff))",
2361
+ borderColor: "var(--papyrus-border-resolved, #d1d5db)",
2362
+ color: "var(--papyrus-text-resolved, #111827)"
2363
+ },
2364
+ placeholder: "Escreva seu comentario...",
2365
+ rows: 3,
2366
+ value: draftContent,
2367
+ onChange: (event) => setDraftContent(event.target.value),
2368
+ onKeyDown: (event) => {
2369
+ if (event.key === "Enter" && !event.shiftKey) {
2370
+ event.preventDefault();
2371
+ handleSaveContent();
2372
+ }
2373
+ },
2374
+ autoFocus: true
2375
+ }
2376
+ ),
2377
+ /* @__PURE__ */ jsx4("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ jsx4(
2378
+ "button",
2379
+ {
2380
+ type: "button",
2381
+ className: "rounded-md px-3 py-1.5 text-[11px] font-semibold text-white",
2382
+ style: { backgroundColor: accentColor },
2383
+ onClick: (event) => {
2384
+ event.stopPropagation();
2385
+ handleSaveContent();
2386
+ },
2387
+ children: (ann.content ?? "").trim() ? "Atualizar" : "Enviar"
2388
+ }
2389
+ ) }),
2390
+ ann.replies && ann.replies.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "mt-3 space-y-2", children: ann.replies.map((reply) => /* @__PURE__ */ jsxs4(
2391
+ "div",
2392
+ {
2393
+ className: "rounded-md border p-2",
2394
+ style: {
2395
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #ffffff))",
2396
+ borderColor: "var(--papyrus-border-resolved, #d1d5db)"
2397
+ },
2398
+ children: [
2399
+ /* @__PURE__ */ jsx4("p", { className: "text-xs", children: reply.content }),
2400
+ /* @__PURE__ */ jsx4("p", { className: "mt-1 text-[10px] opacity-70", children: new Date(reply.createdAt).toLocaleTimeString([], {
2401
+ hour: "2-digit",
2402
+ minute: "2-digit"
2403
+ }) })
2404
+ ]
2405
+ },
2406
+ reply.id
2407
+ )) }) : null,
2408
+ /* @__PURE__ */ jsxs4("div", { className: "mt-3 flex items-center gap-2", children: [
2409
+ /* @__PURE__ */ jsx4(
2410
+ "input",
2411
+ {
2412
+ type: "text",
2413
+ className: "flex-1 rounded-md border px-2 py-1.5 text-xs focus:outline-none",
2414
+ style: {
2415
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #ffffff))",
2416
+ borderColor: "var(--papyrus-border-resolved, #d1d5db)",
2417
+ color: "var(--papyrus-text-resolved, #111827)"
2418
+ },
2419
+ placeholder: "Responder...",
2420
+ value: draftReply,
2421
+ onChange: (event) => setDraftReply(event.target.value),
2422
+ onKeyDown: (event) => {
2423
+ if (event.key === "Enter") {
2424
+ event.preventDefault();
2425
+ handleReplySubmit();
2426
+ }
2427
+ }
2428
+ }
2429
+ ),
2430
+ /* @__PURE__ */ jsx4(
2431
+ "button",
2432
+ {
2433
+ type: "button",
2434
+ className: "rounded-md px-3 py-1.5 text-[11px] font-semibold text-white",
2435
+ style: { backgroundColor: accentColor },
2436
+ onClick: handleReplySubmit,
2437
+ children: "Responder"
2438
+ }
2439
+ )
2440
+ ] })
2441
+ ]
2442
+ }
2443
+ ),
2045
2444
  isSelected && /* @__PURE__ */ jsx4(
2046
2445
  "button",
2047
2446
  {
@@ -2083,7 +2482,11 @@ var MIN_ZOOM = 0.2;
2083
2482
  var MAX_ZOOM = 5;
2084
2483
  var WIDTH_SNAP_PX = 4;
2085
2484
  var WIDTH_HYSTERESIS_PX = 6;
2485
+ var MOBILE_HEADER_HIDE_DELTA_PX = 28;
2486
+ var MOBILE_HEADER_SHOW_DELTA_PX = 16;
2487
+ var MOBILE_HEADER_TOP_RESET_PX = 12;
2086
2488
  var Viewer = ({ engine, style }) => {
2489
+ const viewerState = useViewerStore5();
2087
2490
  const {
2088
2491
  pageCount,
2089
2492
  currentPage,
@@ -2096,7 +2499,8 @@ var Viewer = ({ engine, style }) => {
2096
2499
  annotationColor,
2097
2500
  setAnnotationColor,
2098
2501
  toolDockOpen
2099
- } = useViewerStore5();
2502
+ } = viewerState;
2503
+ const mobileTopbarVisible = viewerState.mobileTopbarVisible ?? true;
2100
2504
  const isDark = uiTheme === "dark";
2101
2505
  const viewerRef = useRef4(null);
2102
2506
  const colorPickerRef = useRef4(null);
@@ -2106,6 +2510,11 @@ var Viewer = ({ engine, style }) => {
2106
2510
  const jumpRef = useRef4(false);
2107
2511
  const jumpTimerRef = useRef4(null);
2108
2512
  const lastWidthRef = useRef4(null);
2513
+ const lastScrollTopRef = useRef4(0);
2514
+ const scrollDownAccumulatorRef = useRef4(0);
2515
+ const scrollUpAccumulatorRef = useRef4(0);
2516
+ const previousCurrentPageRef = useRef4(currentPage);
2517
+ const mobileTopbarVisibleRef = useRef4(mobileTopbarVisible);
2109
2518
  const pinchRef = useRef4({
2110
2519
  active: false,
2111
2520
  startDistance: 0,
@@ -2118,6 +2527,7 @@ var Viewer = ({ engine, style }) => {
2118
2527
  const [pageSizes, setPageSizes] = useState5({});
2119
2528
  const [colorPickerOpen, setColorPickerOpen] = useState5(false);
2120
2529
  const isCompact = availableWidth !== null && availableWidth < 820;
2530
+ const isMobileViewport = availableWidth !== null && availableWidth < 640;
2121
2531
  const paddingY = isCompact ? "py-10" : "py-16";
2122
2532
  const toolDockPosition = isCompact ? "bottom-4" : "bottom-8";
2123
2533
  const colorPalette = [
@@ -2130,6 +2540,13 @@ var Viewer = ({ engine, style }) => {
2130
2540
  "#8b5cf6",
2131
2541
  "#111827"
2132
2542
  ];
2543
+ const setMobileTopbarVisibility = (visible) => {
2544
+ if (mobileTopbarVisibleRef.current === visible) return;
2545
+ mobileTopbarVisibleRef.current = visible;
2546
+ setDocumentState({
2547
+ mobileTopbarVisible: visible
2548
+ });
2549
+ };
2133
2550
  useEffect4(() => {
2134
2551
  if (!colorPickerOpen) return;
2135
2552
  const handleClick = (event) => {
@@ -2144,6 +2561,9 @@ var Viewer = ({ engine, style }) => {
2144
2561
  useEffect4(() => {
2145
2562
  if (!toolDockOpen && colorPickerOpen) setColorPickerOpen(false);
2146
2563
  }, [toolDockOpen, colorPickerOpen]);
2564
+ useEffect4(() => {
2565
+ mobileTopbarVisibleRef.current = mobileTopbarVisible;
2566
+ }, [mobileTopbarVisible]);
2147
2567
  useEffect4(
2148
2568
  () => () => {
2149
2569
  if (pinchRef.current.rafId != null) {
@@ -2195,6 +2615,64 @@ var Viewer = ({ engine, style }) => {
2195
2615
  observer.disconnect();
2196
2616
  };
2197
2617
  }, []);
2618
+ useEffect4(() => {
2619
+ const root = viewerRef.current;
2620
+ if (!root) return;
2621
+ if (!isMobileViewport) {
2622
+ lastScrollTopRef.current = root.scrollTop;
2623
+ scrollDownAccumulatorRef.current = 0;
2624
+ scrollUpAccumulatorRef.current = 0;
2625
+ setMobileTopbarVisibility(true);
2626
+ return;
2627
+ }
2628
+ lastScrollTopRef.current = root.scrollTop;
2629
+ scrollDownAccumulatorRef.current = 0;
2630
+ scrollUpAccumulatorRef.current = 0;
2631
+ const handleScroll = () => {
2632
+ const nextScrollTop = root.scrollTop;
2633
+ const delta = nextScrollTop - lastScrollTopRef.current;
2634
+ lastScrollTopRef.current = nextScrollTop;
2635
+ if (Math.abs(delta) < 1) return;
2636
+ if (nextScrollTop <= MOBILE_HEADER_TOP_RESET_PX) {
2637
+ scrollDownAccumulatorRef.current = 0;
2638
+ scrollUpAccumulatorRef.current = 0;
2639
+ setMobileTopbarVisibility(true);
2640
+ return;
2641
+ }
2642
+ if (delta > 0) {
2643
+ scrollDownAccumulatorRef.current += delta;
2644
+ scrollUpAccumulatorRef.current = 0;
2645
+ } else {
2646
+ scrollUpAccumulatorRef.current += -delta;
2647
+ scrollDownAccumulatorRef.current = 0;
2648
+ }
2649
+ if (scrollDownAccumulatorRef.current >= MOBILE_HEADER_HIDE_DELTA_PX && mobileTopbarVisibleRef.current) {
2650
+ scrollDownAccumulatorRef.current = 0;
2651
+ scrollUpAccumulatorRef.current = 0;
2652
+ setMobileTopbarVisibility(false);
2653
+ return;
2654
+ }
2655
+ if (scrollUpAccumulatorRef.current >= MOBILE_HEADER_SHOW_DELTA_PX && !mobileTopbarVisibleRef.current) {
2656
+ scrollDownAccumulatorRef.current = 0;
2657
+ scrollUpAccumulatorRef.current = 0;
2658
+ setMobileTopbarVisibility(true);
2659
+ }
2660
+ };
2661
+ root.addEventListener("scroll", handleScroll, { passive: true });
2662
+ return () => {
2663
+ root.removeEventListener("scroll", handleScroll);
2664
+ };
2665
+ }, [isMobileViewport, setDocumentState]);
2666
+ useEffect4(() => {
2667
+ const previousPage = previousCurrentPageRef.current;
2668
+ previousCurrentPageRef.current = currentPage;
2669
+ if (!isMobileViewport) return;
2670
+ if (currentPage < previousPage && !mobileTopbarVisibleRef.current) {
2671
+ scrollDownAccumulatorRef.current = 0;
2672
+ scrollUpAccumulatorRef.current = 0;
2673
+ setMobileTopbarVisibility(true);
2674
+ }
2675
+ }, [currentPage, isMobileViewport, setDocumentState]);
2198
2676
  useEffect4(() => {
2199
2677
  let active = true;
2200
2678
  if (!pageCount) return;
@@ -2309,7 +2787,7 @@ var Viewer = ({ engine, style }) => {
2309
2787
  const virtualAnchor = currentPage - 1;
2310
2788
  const virtualStart = Math.max(0, virtualAnchor - virtualOverscan);
2311
2789
  const virtualEnd = Math.min(pageCount - 1, virtualAnchor + virtualOverscan);
2312
- const fallbackSize = useMemo2(() => {
2790
+ const fallbackSize = useMemo3(() => {
2313
2791
  if (basePageSize && availableWidth) {
2314
2792
  const fitScale = Math.min(
2315
2793
  1,
@@ -2326,7 +2804,7 @@ var Viewer = ({ engine, style }) => {
2326
2804
  height: Math.round(base * zoom)
2327
2805
  };
2328
2806
  }, [basePageSize, availableWidth, zoom]);
2329
- const averagePageHeight = useMemo2(() => {
2807
+ const averagePageHeight = useMemo3(() => {
2330
2808
  const heights = Object.values(pageSizes).map((size) => size.height);
2331
2809
  if (!heights.length)
2332
2810
  return availableWidth ? Math.max(680, availableWidth * 1.3) : 1100;