@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.js CHANGED
@@ -46,6 +46,7 @@ var Topbar = ({
46
46
  showUpload = true,
47
47
  showSearch = true
48
48
  }) => {
49
+ const viewerState = (0, import_core.useViewerStore)();
49
50
  const {
50
51
  currentPage,
51
52
  pageCount,
@@ -58,13 +59,15 @@ var Topbar = ({
58
59
  toggleSidebarLeft,
59
60
  toggleSidebarRight,
60
61
  triggerScrollToPage
61
- } = (0, import_core.useViewerStore)();
62
+ } = viewerState;
63
+ const mobileTopbarVisible = viewerState.mobileTopbarVisible ?? true;
62
64
  const fileInputRef = (0, import_react.useRef)(null);
63
65
  const zoomTimerRef = (0, import_react.useRef)(null);
64
66
  const pendingZoomRef = (0, import_react.useRef)(null);
65
67
  const [pageInput, setPageInput] = (0, import_react.useState)(currentPage.toString());
66
68
  const [showPageThemes, setShowPageThemes] = (0, import_react.useState)(false);
67
69
  const [showMobileMenu, setShowMobileMenu] = (0, import_react.useState)(false);
70
+ const [isMobileViewport, setIsMobileViewport] = (0, import_react.useState)(false);
68
71
  const pageDigits = Math.max(2, String(pageCount || 1).length);
69
72
  const isDark = uiTheme === "dark";
70
73
  const canUseDOM = typeof document !== "undefined";
@@ -81,6 +84,18 @@ var Topbar = ({
81
84
  (0, import_react.useEffect)(() => {
82
85
  if (!hasMobileMenu) setShowMobileMenu(false);
83
86
  }, [hasMobileMenu]);
87
+ (0, import_react.useEffect)(() => {
88
+ if (!canUseDOM || typeof window.matchMedia !== "function") return;
89
+ const mediaQuery = window.matchMedia("(max-width: 639px)");
90
+ const updateViewport = () => setIsMobileViewport(mediaQuery.matches);
91
+ updateViewport();
92
+ if (typeof mediaQuery.addEventListener === "function") {
93
+ mediaQuery.addEventListener("change", updateViewport);
94
+ return () => mediaQuery.removeEventListener("change", updateViewport);
95
+ }
96
+ mediaQuery.addListener(updateViewport);
97
+ return () => mediaQuery.removeListener(updateViewport);
98
+ }, [canUseDOM]);
84
99
  (0, import_react.useEffect)(() => {
85
100
  if (!showMobileMenu || !canUseDOM) return;
86
101
  const previousOverflow = document.body.style.overflow;
@@ -94,6 +109,30 @@ var Topbar = ({
94
109
  window.removeEventListener("keydown", handleKeyDown);
95
110
  };
96
111
  }, [showMobileMenu, canUseDOM]);
112
+ const topbarStyle = (0, import_react.useMemo)(() => {
113
+ const mergedStyle = { ...style ?? {} };
114
+ if (!isMobileViewport) {
115
+ mergedStyle.transition = "height 180ms ease, opacity 160ms ease, padding 180ms ease, border-width 180ms ease";
116
+ return mergedStyle;
117
+ }
118
+ mergedStyle.transition = "height 180ms ease, opacity 160ms ease, padding 180ms ease, border-width 180ms ease";
119
+ mergedStyle.overflow = "hidden";
120
+ if (!mobileTopbarVisible) {
121
+ mergedStyle.height = 0;
122
+ mergedStyle.minHeight = 0;
123
+ mergedStyle.paddingTop = 0;
124
+ mergedStyle.paddingBottom = 0;
125
+ mergedStyle.borderBottomWidth = 0;
126
+ mergedStyle.opacity = 0;
127
+ mergedStyle.pointerEvents = "none";
128
+ return mergedStyle;
129
+ }
130
+ mergedStyle.height = 56;
131
+ mergedStyle.minHeight = 56;
132
+ mergedStyle.opacity = 1;
133
+ mergedStyle.pointerEvents = "auto";
134
+ return mergedStyle;
135
+ }, [isMobileViewport, mobileTopbarVisible, style]);
97
136
  const handleZoom = (delta) => {
98
137
  const baseZoom = pendingZoomRef.current ?? zoom;
99
138
  const nextZoom = Math.max(0.2, Math.min(5, baseZoom + delta));
@@ -384,7 +423,7 @@ var Topbar = ({
384
423
  {
385
424
  "data-papyrus-theme": uiTheme,
386
425
  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"}`,
387
- style,
426
+ style: topbarStyle,
388
427
  children: [
389
428
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2 min-w-0 z-10", children: [
390
429
  showSidebarLeftToggle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -1092,10 +1131,15 @@ var SidebarRight = ({ engine, style }) => {
1092
1131
  setDocumentState,
1093
1132
  triggerScrollToPage,
1094
1133
  annotations,
1095
- accentColor
1134
+ accentColor,
1135
+ updateAnnotation,
1136
+ addAnnotationReply,
1137
+ setSelectedAnnotation
1096
1138
  } = (0, import_core3.useViewerStore)();
1097
1139
  const [query, setQuery] = (0, import_react3.useState)("");
1098
1140
  const [isSearching, setIsSearching] = (0, import_react3.useState)(false);
1141
+ const [contentDrafts, setContentDrafts] = (0, import_react3.useState)({});
1142
+ const [replyDrafts, setReplyDrafts] = (0, import_react3.useState)({});
1099
1143
  const searchService = new import_core3.SearchService(engine);
1100
1144
  const isDark = uiTheme === "dark";
1101
1145
  const accentSoft = withAlpha2(accentColor, 0.12);
@@ -1111,6 +1155,41 @@ var SidebarRight = ({ engine, style }) => {
1111
1155
  setSearch(query, results);
1112
1156
  setIsSearching(false);
1113
1157
  };
1158
+ const jumpToAnnotation = (annotation) => {
1159
+ const page = annotation.pageIndex + 1;
1160
+ engine.goToPage(page);
1161
+ setDocumentState({ currentPage: page });
1162
+ setSelectedAnnotation(annotation.id);
1163
+ triggerScrollToPage(annotation.pageIndex);
1164
+ };
1165
+ const getContentDraft = (annotation) => {
1166
+ if (Object.prototype.hasOwnProperty.call(contentDrafts, annotation.id)) {
1167
+ return contentDrafts[annotation.id];
1168
+ }
1169
+ return annotation.content ?? "";
1170
+ };
1171
+ const updateContentDraft = (annotationId, nextValue) => {
1172
+ setContentDrafts((prev) => ({ ...prev, [annotationId]: nextValue }));
1173
+ };
1174
+ const submitContent = (annotation) => {
1175
+ const nextContent = getContentDraft(annotation).trim();
1176
+ const currentContent = (annotation.content ?? "").trim();
1177
+ if (nextContent === currentContent) return;
1178
+ updateAnnotation(annotation.id, {
1179
+ content: nextContent,
1180
+ updatedAt: Date.now()
1181
+ });
1182
+ };
1183
+ const getReplyDraft = (annotationId) => replyDrafts[annotationId] ?? "";
1184
+ const updateReplyDraft = (annotationId, nextValue) => {
1185
+ setReplyDrafts((prev) => ({ ...prev, [annotationId]: nextValue }));
1186
+ };
1187
+ const submitReply = (annotationId) => {
1188
+ const nextReply = getReplyDraft(annotationId).trim();
1189
+ if (!nextReply) return;
1190
+ addAnnotationReply(annotationId, nextReply);
1191
+ setReplyDrafts((prev) => ({ ...prev, [annotationId]: "" }));
1192
+ };
1114
1193
  if (!sidebarRightOpen) return null;
1115
1194
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1116
1195
  "div",
@@ -1312,48 +1391,152 @@ var SidebarRight = ({ engine, style }) => {
1312
1391
  }
1313
1392
  ) }),
1314
1393
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-[10px] font-bold text-gray-400 uppercase tracking-widest", children: "Sem anota\xE7\xF5es" })
1315
- ] }) : annotations.map((ann) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1316
- "div",
1317
- {
1318
- 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"}`,
1319
- children: [
1320
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center justify-between mb-3", children: [
1321
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center space-x-2", children: [
1394
+ ] }) : annotations.slice().sort(
1395
+ (a, b) => (b.updatedAt ?? b.createdAt) - (a.updatedAt ?? a.createdAt)
1396
+ ).map((ann) => {
1397
+ const isCommentThread = ann.type === "comment" || ann.type === "text";
1398
+ const replies = ann.replies ?? [];
1399
+ const contentDraft = getContentDraft(ann);
1400
+ const replyDraft = getReplyDraft(ann.id);
1401
+ const hasExistingContent = Boolean((ann.content ?? "").trim());
1402
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1403
+ "div",
1404
+ {
1405
+ className: "rounded-xl border p-4 transition-colors",
1406
+ style: {
1407
+ background: "var(--papyrus-surface-2-resolved, var(--papyrus-surface-2, #1f2937))",
1408
+ borderColor: "var(--papyrus-border-resolved, var(--papyrus-border, #374151))",
1409
+ color: "var(--papyrus-text-resolved, var(--papyrus-text, #e5e7eb))"
1410
+ },
1411
+ children: [
1412
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mb-3 flex items-center justify-between", children: [
1413
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center space-x-2", children: [
1414
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1415
+ "div",
1416
+ {
1417
+ className: "h-2.5 w-2.5 rounded-full",
1418
+ style: { backgroundColor: ann.color }
1419
+ }
1420
+ ),
1421
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1422
+ "span",
1423
+ {
1424
+ className: "text-[10px] font-black uppercase tracking-wide",
1425
+ style: { color: accentColor },
1426
+ children: [
1427
+ "P",
1428
+ ann.pageIndex + 1
1429
+ ]
1430
+ }
1431
+ ),
1432
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[10px] font-semibold opacity-70 uppercase tracking-wide", children: ann.type })
1433
+ ] }),
1322
1434
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1323
- "div",
1435
+ "button",
1436
+ {
1437
+ type: "button",
1438
+ className: "rounded-md border px-2 py-1 text-[10px] font-semibold uppercase tracking-wide",
1439
+ onClick: () => jumpToAnnotation(ann),
1440
+ style: {
1441
+ borderColor: accentColor,
1442
+ color: accentColor
1443
+ },
1444
+ children: "Ir para pagina"
1445
+ }
1446
+ )
1447
+ ] }),
1448
+ isCommentThread ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-2", children: [
1449
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1450
+ "textarea",
1324
1451
  {
1325
- className: "w-2.5 h-2.5 rounded-full",
1326
- style: { backgroundColor: ann.color }
1452
+ className: "w-full resize-none rounded-md border p-2 text-xs focus:outline-none",
1453
+ style: {
1454
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #111827))",
1455
+ borderColor: "var(--papyrus-border-resolved, var(--papyrus-border, #374151))",
1456
+ color: "var(--papyrus-text-resolved, var(--papyrus-text, #e5e7eb))"
1457
+ },
1458
+ rows: 3,
1459
+ placeholder: "Escreva seu comentario...",
1460
+ value: contentDraft,
1461
+ onChange: (event) => updateContentDraft(ann.id, event.target.value),
1462
+ onKeyDown: (event) => {
1463
+ if (event.key === "Enter" && !event.shiftKey) {
1464
+ event.preventDefault();
1465
+ submitContent(ann);
1466
+ }
1467
+ }
1327
1468
  }
1328
1469
  ),
1329
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1330
- "span",
1470
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1471
+ "button",
1331
1472
  {
1332
- className: "text-[10px] font-black",
1333
- style: { color: accentColor },
1334
- children: [
1335
- "P",
1336
- ann.pageIndex + 1
1337
- ]
1473
+ type: "button",
1474
+ className: "rounded-md px-3 py-1.5 text-[11px] font-semibold text-white",
1475
+ style: { backgroundColor: accentColor },
1476
+ onClick: () => submitContent(ann),
1477
+ children: hasExistingContent ? "Atualizar comentario" : "Enviar comentario"
1478
+ }
1479
+ ) })
1480
+ ] }) : null,
1481
+ replies.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mt-3 space-y-2", children: replies.map((reply) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1482
+ "div",
1483
+ {
1484
+ className: "rounded-md border p-2",
1485
+ style: {
1486
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #111827))",
1487
+ borderColor: "var(--papyrus-border-resolved, var(--papyrus-border, #374151))"
1488
+ },
1489
+ children: [
1490
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs leading-relaxed", children: reply.content }),
1491
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mt-1 text-[10px] opacity-70", children: new Date(reply.createdAt).toLocaleTimeString(
1492
+ [],
1493
+ {
1494
+ hour: "2-digit",
1495
+ minute: "2-digit"
1496
+ }
1497
+ ) })
1498
+ ]
1499
+ },
1500
+ reply.id
1501
+ )) }) : null,
1502
+ isCommentThread ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mt-3 flex items-center gap-2", children: [
1503
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1504
+ "input",
1505
+ {
1506
+ type: "text",
1507
+ className: "flex-1 rounded-md border px-2 py-1.5 text-xs focus:outline-none",
1508
+ style: {
1509
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #111827))",
1510
+ borderColor: "var(--papyrus-border-resolved, var(--papyrus-border, #374151))",
1511
+ color: "var(--papyrus-text-resolved, var(--papyrus-text, #e5e7eb))"
1512
+ },
1513
+ value: replyDraft,
1514
+ placeholder: "Responder...",
1515
+ onChange: (event) => updateReplyDraft(ann.id, event.target.value),
1516
+ onKeyDown: (event) => {
1517
+ if (event.key === "Enter") {
1518
+ event.preventDefault();
1519
+ submitReply(ann.id);
1520
+ }
1521
+ }
1522
+ }
1523
+ ),
1524
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1525
+ "button",
1526
+ {
1527
+ type: "button",
1528
+ className: "rounded-md px-3 py-1.5 text-[11px] font-semibold text-white",
1529
+ style: { backgroundColor: accentColor },
1530
+ onClick: () => submitReply(ann.id),
1531
+ children: "Responder"
1338
1532
  }
1339
1533
  )
1340
- ] }),
1341
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[9px] text-gray-400 font-bold", children: new Date(ann.createdAt).toLocaleTimeString([], {
1342
- hour: "2-digit",
1343
- minute: "2-digit"
1344
- }) })
1345
- ] }),
1346
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1347
- "p",
1348
- {
1349
- className: `text-[11px] font-bold uppercase tracking-tight ${isDark ? "text-gray-200" : "text-gray-700"}`,
1350
- children: ann.type
1351
- }
1352
- )
1353
- ]
1354
- },
1355
- ann.id
1356
- ))
1534
+ ] }) : null
1535
+ ]
1536
+ },
1537
+ ann.id
1538
+ );
1539
+ })
1357
1540
  ] }) })
1358
1541
  ]
1359
1542
  }
@@ -1381,6 +1564,10 @@ var PageRenderer = ({
1381
1564
  const canvasRef = (0, import_react4.useRef)(null);
1382
1565
  const htmlLayerRef = (0, import_react4.useRef)(null);
1383
1566
  const textLayerRef = (0, import_react4.useRef)(null);
1567
+ const skipNextAnnotationSelectRef = (0, import_react4.useRef)(false);
1568
+ const skipSelectResetTimerRef = (0, import_react4.useRef)(
1569
+ null
1570
+ );
1384
1571
  const [loading, setLoading] = (0, import_react4.useState)(true);
1385
1572
  const [pageSize, setPageSize] = (0, import_react4.useState)(null);
1386
1573
  const [isDragging, setIsDragging] = (0, import_react4.useState)(false);
@@ -1398,6 +1585,8 @@ var PageRenderer = ({
1398
1585
  setDocumentState,
1399
1586
  annotations,
1400
1587
  addAnnotation,
1588
+ addAnnotationReply,
1589
+ updateAnnotation,
1401
1590
  activeTool,
1402
1591
  removeAnnotation,
1403
1592
  selectedAnnotationId,
@@ -1421,6 +1610,24 @@ var PageRenderer = ({
1421
1610
  () => Boolean(searchQuery?.trim()) && searchResults.some((res) => res.pageIndex === pageIndex),
1422
1611
  [searchQuery, searchResults, pageIndex]
1423
1612
  );
1613
+ const suppressNextAnnotationSelect = () => {
1614
+ skipNextAnnotationSelectRef.current = true;
1615
+ if (skipSelectResetTimerRef.current) {
1616
+ clearTimeout(skipSelectResetTimerRef.current);
1617
+ }
1618
+ skipSelectResetTimerRef.current = setTimeout(() => {
1619
+ skipNextAnnotationSelectRef.current = false;
1620
+ skipSelectResetTimerRef.current = null;
1621
+ }, 0);
1622
+ };
1623
+ (0, import_react4.useEffect)(
1624
+ () => () => {
1625
+ if (skipSelectResetTimerRef.current) {
1626
+ clearTimeout(skipSelectResetTimerRef.current);
1627
+ }
1628
+ },
1629
+ []
1630
+ );
1424
1631
  (0, import_react4.useEffect)(() => {
1425
1632
  let active = true;
1426
1633
  const loadSize = async () => {
@@ -1591,6 +1798,16 @@ var PageRenderer = ({
1591
1798
  textLayerVersion
1592
1799
  ]);
1593
1800
  const handleMouseDown = (e) => {
1801
+ const target = e.target;
1802
+ const clickedInsideAnnotation = Boolean(
1803
+ target?.closest("[data-papyrus-annotation-id]")
1804
+ );
1805
+ const clickedSelectionMenu = Boolean(
1806
+ target?.closest("[data-papyrus-selection-menu]")
1807
+ );
1808
+ if (!clickedInsideAnnotation && !clickedSelectionMenu) {
1809
+ setSelectedAnnotation(null);
1810
+ }
1594
1811
  setSelectionMenu(null);
1595
1812
  if (activeTool === "ink") {
1596
1813
  const rect2 = containerRef.current?.getBoundingClientRect();
@@ -1647,6 +1864,7 @@ var PageRenderer = ({
1647
1864
  x: Math.max(0, Math.min(1, p.x)),
1648
1865
  y: Math.max(0, Math.min(1, p.y))
1649
1866
  }));
1867
+ suppressNextAnnotationSelect();
1650
1868
  addAnnotation({
1651
1869
  id: Math.random().toString(36).substr(2, 9),
1652
1870
  pageIndex,
@@ -1727,6 +1945,7 @@ var PageRenderer = ({
1727
1945
  height: Math.max(...ye) - Math.min(...ys)
1728
1946
  };
1729
1947
  if (textMarkupTools.has(activeTool)) {
1948
+ suppressNextAnnotationSelect();
1730
1949
  addAnnotation({
1731
1950
  id: Math.random().toString(36).substr(2, 9),
1732
1951
  pageIndex,
@@ -1762,6 +1981,9 @@ var PageRenderer = ({
1762
1981
  if (currentRect.w > 5 && currentRect.h > 5) {
1763
1982
  const rect = containerRef.current?.getBoundingClientRect();
1764
1983
  if (rect) {
1984
+ if (activeTool !== "text" && activeTool !== "comment") {
1985
+ suppressNextAnnotationSelect();
1986
+ }
1765
1987
  addAnnotation({
1766
1988
  id: Math.random().toString(36).substr(2, 9),
1767
1989
  pageIndex,
@@ -1885,6 +2107,7 @@ var PageRenderer = ({
1885
2107
  selectionMenu && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1886
2108
  "div",
1887
2109
  {
2110
+ "data-papyrus-selection-menu": "true",
1888
2111
  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",
1889
2112
  style: { left: selectionMenu.anchor.x, top: selectionMenu.anchor.y },
1890
2113
  children: [
@@ -1897,6 +2120,7 @@ var PageRenderer = ({
1897
2120
  {
1898
2121
  className: "text-[10px] font-bold px-2 py-1 rounded-full hover:bg-gray-100",
1899
2122
  onClick: () => {
2123
+ suppressNextAnnotationSelect();
1900
2124
  addAnnotation({
1901
2125
  id: Math.random().toString(36).substr(2, 9),
1902
2126
  pageIndex,
@@ -1923,7 +2147,15 @@ var PageRenderer = ({
1923
2147
  isSelected: selectedAnnotationId === ann.id,
1924
2148
  accentColor,
1925
2149
  onDelete: () => removeAnnotation(ann.id),
1926
- onSelect: () => setSelectedAnnotation(ann.id)
2150
+ onSelect: () => {
2151
+ if (skipNextAnnotationSelectRef.current) {
2152
+ skipNextAnnotationSelectRef.current = false;
2153
+ return;
2154
+ }
2155
+ setSelectedAnnotation(ann.id);
2156
+ },
2157
+ onUpdate: (updates) => updateAnnotation(ann.id, updates),
2158
+ onAddReply: (content) => addAnnotationReply(ann.id, content)
1927
2159
  },
1928
2160
  ann.id
1929
2161
  )) })
@@ -1931,12 +2163,43 @@ var PageRenderer = ({
1931
2163
  }
1932
2164
  );
1933
2165
  };
1934
- var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
2166
+ var AnnotationItem = ({
2167
+ ann,
2168
+ isSelected,
2169
+ accentColor,
2170
+ onDelete,
2171
+ onSelect,
2172
+ onUpdate,
2173
+ onAddReply
2174
+ }) => {
1935
2175
  const isText = ann.type === "text" || ann.type === "comment";
1936
2176
  const isHighlight = ann.type === "highlight";
1937
2177
  const isMarkup = ann.type === "highlight" || ann.type === "underline" || ann.type === "squiggly" || ann.type === "strikeout";
1938
2178
  const rects = ann.rects && ann.rects.length > 0 ? ann.rects : [ann.rect];
1939
2179
  const isInk = ann.type === "ink" && ann.path && ann.path.length > 1;
2180
+ const [draftContent, setDraftContent] = (0, import_react4.useState)(ann.content ?? "");
2181
+ const [draftReply, setDraftReply] = (0, import_react4.useState)("");
2182
+ (0, import_react4.useEffect)(() => {
2183
+ setDraftContent(ann.content ?? "");
2184
+ }, [ann.id, ann.content]);
2185
+ (0, import_react4.useEffect)(() => {
2186
+ setDraftReply("");
2187
+ }, [ann.id]);
2188
+ const handleSaveContent = () => {
2189
+ const nextContent = draftContent.trim();
2190
+ const currentContent = (ann.content ?? "").trim();
2191
+ if (nextContent === currentContent) return;
2192
+ onUpdate({
2193
+ content: nextContent,
2194
+ updatedAt: Date.now()
2195
+ });
2196
+ };
2197
+ const handleReplySubmit = () => {
2198
+ const nextReply = draftReply.trim();
2199
+ if (!nextReply) return;
2200
+ onAddReply(nextReply);
2201
+ setDraftReply("");
2202
+ };
1940
2203
  const renderMarkupRects = () => {
1941
2204
  if (!isMarkup) return null;
1942
2205
  return rects.map((r, idx) => {
@@ -2041,6 +2304,7 @@ var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
2041
2304
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2042
2305
  "div",
2043
2306
  {
2307
+ "data-papyrus-annotation-id": ann.id,
2044
2308
  className: `absolute pointer-events-auto transition-all ${isSelected ? "shadow-xl z-30" : "z-10"}`,
2045
2309
  style: {
2046
2310
  left: `${ann.rect.x * 100}%`,
@@ -2059,16 +2323,151 @@ var AnnotationItem = ({ ann, isSelected, accentColor, onDelete, onSelect }) => {
2059
2323
  children: [
2060
2324
  renderMarkupRects(),
2061
2325
  renderInk(),
2062
- isText && isSelected && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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__ */ (0, import_jsx_runtime4.jsx)(
2063
- "textarea",
2326
+ isText && !isSelected && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2327
+ "button",
2064
2328
  {
2065
- className: "w-full bg-transparent border-none focus:ring-0 p-0 text-gray-800 text-xs font-medium",
2066
- placeholder: "Escreva sua nota...",
2067
- rows: 3,
2068
- defaultValue: ann.content || "",
2069
- autoFocus: true
2329
+ type: "button",
2330
+ className: "absolute -top-2 -right-2 h-6 w-6 rounded-full flex items-center justify-center shadow-lg",
2331
+ style: {
2332
+ background: "var(--papyrus-surface-2-resolved, var(--papyrus-surface-2, #1f2937))",
2333
+ border: "1px solid var(--papyrus-border-resolved, #374151)",
2334
+ color: "var(--papyrus-text-resolved, #e5e7eb)"
2335
+ },
2336
+ title: "Abrir comentario",
2337
+ "aria-label": "Abrir comentario",
2338
+ onClick: (event) => {
2339
+ event.stopPropagation();
2340
+ onSelect();
2341
+ },
2342
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2343
+ "svg",
2344
+ {
2345
+ className: "h-3.5 w-3.5",
2346
+ fill: "none",
2347
+ stroke: "currentColor",
2348
+ viewBox: "0 0 24 24",
2349
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2350
+ "path",
2351
+ {
2352
+ strokeLinecap: "round",
2353
+ strokeLinejoin: "round",
2354
+ strokeWidth: 2,
2355
+ 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"
2356
+ }
2357
+ )
2358
+ }
2359
+ )
2070
2360
  }
2071
- ) }),
2361
+ ),
2362
+ isText && isSelected && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2363
+ "div",
2364
+ {
2365
+ className: "absolute top-full mt-2 w-72 rounded-xl p-3 z-50",
2366
+ style: {
2367
+ background: "var(--papyrus-popover-resolved, var(--papyrus-popover, #ffffff))",
2368
+ border: "1px solid var(--papyrus-border-resolved, #d1d5db)",
2369
+ color: "var(--papyrus-text-resolved, #111827)",
2370
+ boxShadow: "0 20px 40px var(--papyrus-shadow-resolved, rgba(0, 0, 0, 0.3))"
2371
+ },
2372
+ onClick: (event) => event.stopPropagation(),
2373
+ children: [
2374
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "mb-2 flex items-center justify-between", children: [
2375
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-[10px] font-bold uppercase tracking-wider opacity-70", children: ann.type === "comment" ? "Comentario" : "Nota" }),
2376
+ ann.replies?.length ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "text-[10px] opacity-70", children: [
2377
+ ann.replies.length,
2378
+ " resposta",
2379
+ ann.replies.length > 1 ? "s" : ""
2380
+ ] }) : null
2381
+ ] }),
2382
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2383
+ "textarea",
2384
+ {
2385
+ className: "w-full rounded-md border p-2 text-xs font-medium resize-none focus:outline-none",
2386
+ style: {
2387
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #ffffff))",
2388
+ borderColor: "var(--papyrus-border-resolved, #d1d5db)",
2389
+ color: "var(--papyrus-text-resolved, #111827)"
2390
+ },
2391
+ placeholder: "Escreva seu comentario...",
2392
+ rows: 3,
2393
+ value: draftContent,
2394
+ onChange: (event) => setDraftContent(event.target.value),
2395
+ onKeyDown: (event) => {
2396
+ if (event.key === "Enter" && !event.shiftKey) {
2397
+ event.preventDefault();
2398
+ handleSaveContent();
2399
+ }
2400
+ },
2401
+ autoFocus: true
2402
+ }
2403
+ ),
2404
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2405
+ "button",
2406
+ {
2407
+ type: "button",
2408
+ className: "rounded-md px-3 py-1.5 text-[11px] font-semibold text-white",
2409
+ style: { backgroundColor: accentColor },
2410
+ onClick: (event) => {
2411
+ event.stopPropagation();
2412
+ handleSaveContent();
2413
+ },
2414
+ children: (ann.content ?? "").trim() ? "Atualizar" : "Enviar"
2415
+ }
2416
+ ) }),
2417
+ ann.replies && ann.replies.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-3 space-y-2", children: ann.replies.map((reply) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2418
+ "div",
2419
+ {
2420
+ className: "rounded-md border p-2",
2421
+ style: {
2422
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #ffffff))",
2423
+ borderColor: "var(--papyrus-border-resolved, #d1d5db)"
2424
+ },
2425
+ children: [
2426
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xs", children: reply.content }),
2427
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "mt-1 text-[10px] opacity-70", children: new Date(reply.createdAt).toLocaleTimeString([], {
2428
+ hour: "2-digit",
2429
+ minute: "2-digit"
2430
+ }) })
2431
+ ]
2432
+ },
2433
+ reply.id
2434
+ )) }) : null,
2435
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "mt-3 flex items-center gap-2", children: [
2436
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2437
+ "input",
2438
+ {
2439
+ type: "text",
2440
+ className: "flex-1 rounded-md border px-2 py-1.5 text-xs focus:outline-none",
2441
+ style: {
2442
+ background: "var(--papyrus-surface-resolved, var(--papyrus-surface, #ffffff))",
2443
+ borderColor: "var(--papyrus-border-resolved, #d1d5db)",
2444
+ color: "var(--papyrus-text-resolved, #111827)"
2445
+ },
2446
+ placeholder: "Responder...",
2447
+ value: draftReply,
2448
+ onChange: (event) => setDraftReply(event.target.value),
2449
+ onKeyDown: (event) => {
2450
+ if (event.key === "Enter") {
2451
+ event.preventDefault();
2452
+ handleReplySubmit();
2453
+ }
2454
+ }
2455
+ }
2456
+ ),
2457
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2458
+ "button",
2459
+ {
2460
+ type: "button",
2461
+ className: "rounded-md px-3 py-1.5 text-[11px] font-semibold text-white",
2462
+ style: { backgroundColor: accentColor },
2463
+ onClick: handleReplySubmit,
2464
+ children: "Responder"
2465
+ }
2466
+ )
2467
+ ] })
2468
+ ]
2469
+ }
2470
+ ),
2072
2471
  isSelected && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2073
2472
  "button",
2074
2473
  {
@@ -2110,7 +2509,11 @@ var MIN_ZOOM = 0.2;
2110
2509
  var MAX_ZOOM = 5;
2111
2510
  var WIDTH_SNAP_PX = 4;
2112
2511
  var WIDTH_HYSTERESIS_PX = 6;
2512
+ var MOBILE_HEADER_HIDE_DELTA_PX = 28;
2513
+ var MOBILE_HEADER_SHOW_DELTA_PX = 16;
2514
+ var MOBILE_HEADER_TOP_RESET_PX = 12;
2113
2515
  var Viewer = ({ engine, style }) => {
2516
+ const viewerState = (0, import_core5.useViewerStore)();
2114
2517
  const {
2115
2518
  pageCount,
2116
2519
  currentPage,
@@ -2123,7 +2526,8 @@ var Viewer = ({ engine, style }) => {
2123
2526
  annotationColor,
2124
2527
  setAnnotationColor,
2125
2528
  toolDockOpen
2126
- } = (0, import_core5.useViewerStore)();
2529
+ } = viewerState;
2530
+ const mobileTopbarVisible = viewerState.mobileTopbarVisible ?? true;
2127
2531
  const isDark = uiTheme === "dark";
2128
2532
  const viewerRef = (0, import_react5.useRef)(null);
2129
2533
  const colorPickerRef = (0, import_react5.useRef)(null);
@@ -2133,6 +2537,11 @@ var Viewer = ({ engine, style }) => {
2133
2537
  const jumpRef = (0, import_react5.useRef)(false);
2134
2538
  const jumpTimerRef = (0, import_react5.useRef)(null);
2135
2539
  const lastWidthRef = (0, import_react5.useRef)(null);
2540
+ const lastScrollTopRef = (0, import_react5.useRef)(0);
2541
+ const scrollDownAccumulatorRef = (0, import_react5.useRef)(0);
2542
+ const scrollUpAccumulatorRef = (0, import_react5.useRef)(0);
2543
+ const previousCurrentPageRef = (0, import_react5.useRef)(currentPage);
2544
+ const mobileTopbarVisibleRef = (0, import_react5.useRef)(mobileTopbarVisible);
2136
2545
  const pinchRef = (0, import_react5.useRef)({
2137
2546
  active: false,
2138
2547
  startDistance: 0,
@@ -2145,6 +2554,7 @@ var Viewer = ({ engine, style }) => {
2145
2554
  const [pageSizes, setPageSizes] = (0, import_react5.useState)({});
2146
2555
  const [colorPickerOpen, setColorPickerOpen] = (0, import_react5.useState)(false);
2147
2556
  const isCompact = availableWidth !== null && availableWidth < 820;
2557
+ const isMobileViewport = availableWidth !== null && availableWidth < 640;
2148
2558
  const paddingY = isCompact ? "py-10" : "py-16";
2149
2559
  const toolDockPosition = isCompact ? "bottom-4" : "bottom-8";
2150
2560
  const colorPalette = [
@@ -2157,6 +2567,13 @@ var Viewer = ({ engine, style }) => {
2157
2567
  "#8b5cf6",
2158
2568
  "#111827"
2159
2569
  ];
2570
+ const setMobileTopbarVisibility = (visible) => {
2571
+ if (mobileTopbarVisibleRef.current === visible) return;
2572
+ mobileTopbarVisibleRef.current = visible;
2573
+ setDocumentState({
2574
+ mobileTopbarVisible: visible
2575
+ });
2576
+ };
2160
2577
  (0, import_react5.useEffect)(() => {
2161
2578
  if (!colorPickerOpen) return;
2162
2579
  const handleClick = (event) => {
@@ -2171,6 +2588,9 @@ var Viewer = ({ engine, style }) => {
2171
2588
  (0, import_react5.useEffect)(() => {
2172
2589
  if (!toolDockOpen && colorPickerOpen) setColorPickerOpen(false);
2173
2590
  }, [toolDockOpen, colorPickerOpen]);
2591
+ (0, import_react5.useEffect)(() => {
2592
+ mobileTopbarVisibleRef.current = mobileTopbarVisible;
2593
+ }, [mobileTopbarVisible]);
2174
2594
  (0, import_react5.useEffect)(
2175
2595
  () => () => {
2176
2596
  if (pinchRef.current.rafId != null) {
@@ -2222,6 +2642,64 @@ var Viewer = ({ engine, style }) => {
2222
2642
  observer.disconnect();
2223
2643
  };
2224
2644
  }, []);
2645
+ (0, import_react5.useEffect)(() => {
2646
+ const root = viewerRef.current;
2647
+ if (!root) return;
2648
+ if (!isMobileViewport) {
2649
+ lastScrollTopRef.current = root.scrollTop;
2650
+ scrollDownAccumulatorRef.current = 0;
2651
+ scrollUpAccumulatorRef.current = 0;
2652
+ setMobileTopbarVisibility(true);
2653
+ return;
2654
+ }
2655
+ lastScrollTopRef.current = root.scrollTop;
2656
+ scrollDownAccumulatorRef.current = 0;
2657
+ scrollUpAccumulatorRef.current = 0;
2658
+ const handleScroll = () => {
2659
+ const nextScrollTop = root.scrollTop;
2660
+ const delta = nextScrollTop - lastScrollTopRef.current;
2661
+ lastScrollTopRef.current = nextScrollTop;
2662
+ if (Math.abs(delta) < 1) return;
2663
+ if (nextScrollTop <= MOBILE_HEADER_TOP_RESET_PX) {
2664
+ scrollDownAccumulatorRef.current = 0;
2665
+ scrollUpAccumulatorRef.current = 0;
2666
+ setMobileTopbarVisibility(true);
2667
+ return;
2668
+ }
2669
+ if (delta > 0) {
2670
+ scrollDownAccumulatorRef.current += delta;
2671
+ scrollUpAccumulatorRef.current = 0;
2672
+ } else {
2673
+ scrollUpAccumulatorRef.current += -delta;
2674
+ scrollDownAccumulatorRef.current = 0;
2675
+ }
2676
+ if (scrollDownAccumulatorRef.current >= MOBILE_HEADER_HIDE_DELTA_PX && mobileTopbarVisibleRef.current) {
2677
+ scrollDownAccumulatorRef.current = 0;
2678
+ scrollUpAccumulatorRef.current = 0;
2679
+ setMobileTopbarVisibility(false);
2680
+ return;
2681
+ }
2682
+ if (scrollUpAccumulatorRef.current >= MOBILE_HEADER_SHOW_DELTA_PX && !mobileTopbarVisibleRef.current) {
2683
+ scrollDownAccumulatorRef.current = 0;
2684
+ scrollUpAccumulatorRef.current = 0;
2685
+ setMobileTopbarVisibility(true);
2686
+ }
2687
+ };
2688
+ root.addEventListener("scroll", handleScroll, { passive: true });
2689
+ return () => {
2690
+ root.removeEventListener("scroll", handleScroll);
2691
+ };
2692
+ }, [isMobileViewport, setDocumentState]);
2693
+ (0, import_react5.useEffect)(() => {
2694
+ const previousPage = previousCurrentPageRef.current;
2695
+ previousCurrentPageRef.current = currentPage;
2696
+ if (!isMobileViewport) return;
2697
+ if (currentPage < previousPage && !mobileTopbarVisibleRef.current) {
2698
+ scrollDownAccumulatorRef.current = 0;
2699
+ scrollUpAccumulatorRef.current = 0;
2700
+ setMobileTopbarVisibility(true);
2701
+ }
2702
+ }, [currentPage, isMobileViewport, setDocumentState]);
2225
2703
  (0, import_react5.useEffect)(() => {
2226
2704
  let active = true;
2227
2705
  if (!pageCount) return;