apostil 0.1.5 → 0.1.7

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
@@ -93,6 +93,7 @@ var ApostilContext = createContext(null);
93
93
  function ApostilProvider({
94
94
  pageId,
95
95
  storage,
96
+ brandColor = "#171717",
96
97
  children
97
98
  }) {
98
99
  const adapter = storage ?? defaultAdapter;
@@ -110,6 +111,7 @@ function ApostilProvider({
110
111
  useEffect(() => {
111
112
  pageIdRef.current = pageId;
112
113
  setLoaded(false);
114
+ hadThreadsRef.current = false;
113
115
  debug.log("loading threads for pageId:", pageId);
114
116
  adapter.load(pageId).then((t) => {
115
117
  if (pageIdRef.current === pageId) {
@@ -119,10 +121,17 @@ function ApostilProvider({
119
121
  }
120
122
  });
121
123
  }, [pageId]);
124
+ const hadThreadsRef = useRef(false);
122
125
  useEffect(() => {
123
- if (loaded && pageIdRef.current === pageId && threads.length > 0) {
126
+ if (!loaded || pageIdRef.current !== pageId) return;
127
+ if (threads.length > 0) {
128
+ hadThreadsRef.current = true;
124
129
  debug.log("saving", threads.length, "threads for pageId:", pageId);
125
130
  adapter.save(pageId, threads);
131
+ } else if (hadThreadsRef.current) {
132
+ debug.log("saving empty threads for pageId:", pageId);
133
+ adapter.save(pageId, threads);
134
+ hadThreadsRef.current = false;
126
135
  }
127
136
  }, [threads, pageId, loaded]);
128
137
  const setUser = useCallback((name) => {
@@ -184,6 +193,7 @@ function ApostilProvider({
184
193
  commentMode,
185
194
  activeThreadId,
186
195
  sidebarOpen,
196
+ brandColor,
187
197
  setCommentMode,
188
198
  setActiveThreadId,
189
199
  setSidebarOpen,
@@ -417,7 +427,11 @@ function OverlayPin({
417
427
  useEffect2(() => {
418
428
  updatePos();
419
429
  window.addEventListener("resize", updatePos);
420
- return () => window.removeEventListener("resize", updatePos);
430
+ document.addEventListener("scroll", updatePos, true);
431
+ return () => {
432
+ window.removeEventListener("resize", updatePos);
433
+ document.removeEventListener("scroll", updatePos, true);
434
+ };
421
435
  }, [updatePos]);
422
436
  if (!pos) return null;
423
437
  return /* @__PURE__ */ jsx2(
@@ -450,6 +464,7 @@ function CommentPin({
450
464
  index,
451
465
  overlayRef
452
466
  }) {
467
+ if (thread.resolved) return null;
453
468
  if (thread.targetId) {
454
469
  return /* @__PURE__ */ jsx2(TargetedPin, { thread, index }, `targeted-${thread.id}`);
455
470
  }
@@ -529,11 +544,15 @@ function CommentComposer({
529
544
  placeholder = "Add a comment...",
530
545
  autoFocus = false
531
546
  }) {
547
+ const { brandColor } = useApostil();
532
548
  const [value, setValue] = useState3("");
533
549
  const inputRef = useRef3(null);
534
550
  useEffect3(() => {
535
551
  if (autoFocus) {
536
- inputRef.current?.focus();
552
+ const timer = setTimeout(() => {
553
+ inputRef.current?.focus();
554
+ }, 50);
555
+ return () => clearTimeout(timer);
537
556
  }
538
557
  }, [autoFocus]);
539
558
  const handleSubmit = () => {
@@ -557,7 +576,7 @@ function CommentComposer({
557
576
  },
558
577
  placeholder,
559
578
  rows: 1,
560
- className: "flex-1 resize-none rounded-lg border border-neutral-200 bg-white px-3 py-2 text-sm\n placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-neutral-300\n min-h-[36px] max-h-[120px]"
579
+ className: "flex-1 resize-none rounded-lg border border-neutral-200 bg-white px-3 py-1.5 text-sm\n placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-neutral-300\n min-h-[36px] max-h-[120px]"
561
580
  }
562
581
  ),
563
582
  /* @__PURE__ */ jsx4(
@@ -565,7 +584,8 @@ function CommentComposer({
565
584
  {
566
585
  onClick: handleSubmit,
567
586
  disabled: !value.trim(),
568
- className: "flex items-center justify-center w-8 h-8 rounded-lg\n bg-neutral-900 text-white disabled:opacity-30\n hover:bg-neutral-700 transition-colors shrink-0",
587
+ className: "flex items-center justify-center w-9 h-9 rounded-lg\n text-white disabled:opacity-30\n transition-colors shrink-0",
588
+ style: { backgroundColor: brandColor },
569
589
  children: /* @__PURE__ */ jsx4(Send, { className: "w-3.5 h-3.5" })
570
590
  }
571
591
  )
@@ -592,9 +612,17 @@ function ApostilThreadPopover({
592
612
  const ref = useRef4(null);
593
613
  const isOpen = activeThreadId === thread.id;
594
614
  const [pos, setPos] = useState4(null);
615
+ const [flip, setFlip] = useState4({ x: false, y: false });
595
616
  const updatePos = useCallback3(() => {
596
617
  setPos(resolvePosition(thread, overlayRef.current));
597
618
  }, [thread, overlayRef]);
619
+ useEffect4(() => {
620
+ if (!isOpen || !pos || !ref.current) return;
621
+ const rect = ref.current.getBoundingClientRect();
622
+ const flipX = rect.right > window.innerWidth;
623
+ const flipY = rect.bottom > window.innerHeight;
624
+ setFlip({ x: flipX, y: flipY });
625
+ }, [isOpen, pos]);
598
626
  useEffect4(() => {
599
627
  if (!isOpen) return;
600
628
  updatePos();
@@ -623,8 +651,14 @@ function ApostilThreadPopover({
623
651
  "div",
624
652
  {
625
653
  ref,
626
- className: "absolute z-[70] ml-5 -mt-3",
627
- style: { left: pos.left, top: pos.top },
654
+ className: "absolute z-[70]",
655
+ style: {
656
+ left: pos.left,
657
+ top: pos.top,
658
+ marginLeft: flip.x ? -340 : 20,
659
+ marginTop: flip.y ? -12 : -12,
660
+ ...flip.y ? { transform: "translateY(-100%)" } : {}
661
+ },
628
662
  onClick: (e) => e.stopPropagation(),
629
663
  children: /* @__PURE__ */ jsxs4("div", { className: "w-80 bg-white rounded-xl shadow-2xl border border-neutral-200 overflow-hidden", children: [
630
664
  /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between px-4 py-2.5 border-b border-neutral-100 bg-neutral-50", children: [
@@ -690,7 +724,7 @@ function ApostilThreadPopover({
690
724
  import { useState as useState5 } from "react";
691
725
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
692
726
  function UserPrompt() {
693
- const { user, setUser, commentMode } = useApostil();
727
+ const { user, setUser, commentMode, brandColor } = useApostil();
694
728
  const [name, setName] = useState5("");
695
729
  if (user || !commentMode) return null;
696
730
  const handleSubmit = () => {
@@ -726,7 +760,8 @@ function UserPrompt() {
726
760
  {
727
761
  onClick: handleSubmit,
728
762
  disabled: !name.trim(),
729
- className: "w-full py-2 rounded-lg bg-neutral-900 text-white text-sm font-medium\n disabled:opacity-30 hover:bg-neutral-700 transition-colors",
763
+ className: "w-full py-2 rounded-lg text-white text-sm font-medium\n disabled:opacity-30 transition-colors",
764
+ style: { backgroundColor: brandColor },
730
765
  children: "Continue"
731
766
  }
732
767
  )
@@ -877,18 +912,26 @@ function findCommentTarget(el, boundary) {
877
912
  };
878
913
  }
879
914
  function CommentOverlay() {
880
- const { threads, commentMode, setCommentMode, user, addThread, setActiveThreadId } = useApostil();
915
+ const { threads, commentMode, setCommentMode, user, addThread, activeThreadId, setActiveThreadId, brandColor } = useApostil();
881
916
  const overlayRef = useRef5(null);
882
917
  const [pendingPin, setPendingPin] = useState6(null);
883
918
  const [pendingPixel, setPendingPixel] = useState6(null);
919
+ const pendingRef = useRef5(null);
920
+ const [pendingFlip, setPendingFlip] = useState6({ x: false, y: false });
884
921
  const handleClick = useCallback4(
885
922
  (e) => {
886
923
  if (!commentMode || !overlayRef.current) return;
887
924
  const overlayRect = overlayRef.current.getBoundingClientRect();
888
925
  const overlay = overlayRef.current;
889
- overlay.style.pointerEvents = "none";
890
- const elementBelow = document.elementFromPoint(e.clientX, e.clientY);
891
- overlay.style.pointerEvents = "";
926
+ const elements = document.elementsFromPoint(e.clientX, e.clientY);
927
+ let elementBelow = null;
928
+ for (const el of elements) {
929
+ if (el === overlay || overlay.contains(el)) continue;
930
+ if (el instanceof HTMLElement) {
931
+ elementBelow = el;
932
+ break;
933
+ }
934
+ }
892
935
  debug.log(" click at", { clientX: e.clientX, clientY: e.clientY });
893
936
  debug.log(" element below overlay:", elementBelow);
894
937
  if (elementBelow) {
@@ -957,6 +1000,16 @@ function CommentOverlay() {
957
1000
  setPendingPixel(null);
958
1001
  setCommentMode(false);
959
1002
  }, [setCommentMode]);
1003
+ useEffect5(() => {
1004
+ if (!pendingPixel || !overlayRef.current) return;
1005
+ const overlayRect = overlayRef.current.getBoundingClientRect();
1006
+ const clickX = overlayRect.left + pendingPixel.left;
1007
+ const clickY = overlayRect.top + pendingPixel.top;
1008
+ setPendingFlip({
1009
+ x: clickX + 308 > window.innerWidth,
1010
+ y: clickY + 200 > window.innerHeight
1011
+ });
1012
+ }, [pendingPixel, overlayRef]);
960
1013
  useEffect5(() => {
961
1014
  const hash = window.location.hash;
962
1015
  console.log("[apostil] hash check:", hash, "threads:", threads.length, threads.map((t) => t.id));
@@ -982,6 +1035,8 @@ function CommentOverlay() {
982
1035
  setPendingPin(null);
983
1036
  setPendingPixel(null);
984
1037
  setCommentMode(false);
1038
+ } else if (activeThreadId) {
1039
+ setActiveThreadId(null);
985
1040
  } else if (commentMode) {
986
1041
  setCommentMode(false);
987
1042
  }
@@ -1003,11 +1058,8 @@ function CommentOverlay() {
1003
1058
  };
1004
1059
  document.addEventListener("keydown", handler);
1005
1060
  return () => document.removeEventListener("keydown", handler);
1006
- }, [commentMode, pendingPin, setCommentMode, setActiveThreadId]);
1007
- const sortedThreads = [...threads].sort((a, b) => {
1008
- if (a.resolved !== b.resolved) return a.resolved ? 1 : -1;
1009
- return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
1010
- });
1061
+ }, [commentMode, pendingPin, activeThreadId, setCommentMode, setActiveThreadId]);
1062
+ const visibleThreads = threads.filter((t) => !t.resolved).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
1011
1063
  const showingUserPrompt = commentMode && !user;
1012
1064
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1013
1065
  /* @__PURE__ */ jsxs6(
@@ -1018,7 +1070,7 @@ function CommentOverlay() {
1018
1070
  style: { zIndex: commentMode ? getHighestZIndex() + 10 : 55 },
1019
1071
  onMouseDown: handleClick,
1020
1072
  children: [
1021
- sortedThreads.map((thread, i) => /* @__PURE__ */ jsxs6("div", { className: "pointer-events-auto", children: [
1073
+ visibleThreads.map((thread, i) => /* @__PURE__ */ jsxs6("div", { className: "pointer-events-auto", children: [
1022
1074
  /* @__PURE__ */ jsx7(CommentPin, { thread, index: i, overlayRef }),
1023
1075
  /* @__PURE__ */ jsx7(ApostilThreadPopover, { thread, overlayRef })
1024
1076
  ] }, thread.id)),
@@ -1041,35 +1093,46 @@ function CommentOverlay() {
1041
1093
  children: "+"
1042
1094
  }
1043
1095
  ),
1044
- /* @__PURE__ */ jsx7("div", { className: "absolute ml-5 -mt-3 w-72", children: /* @__PURE__ */ jsxs6("div", { className: "bg-white rounded-xl shadow-2xl border border-neutral-200 p-3", children: [
1045
- /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 mb-2", children: [
1046
- /* @__PURE__ */ jsx7("p", { className: "text-xs text-neutral-500", children: "New comment" }),
1047
- pendingPin.targetLabel && /* @__PURE__ */ jsx7("span", { className: "text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium", children: pendingPin.targetLabel })
1048
- ] }),
1049
- /* @__PURE__ */ jsx7(
1050
- CommentComposer,
1051
- {
1052
- onSubmit: handleNewComment,
1053
- placeholder: "What's on your mind?",
1054
- autoFocus: true
1055
- }
1056
- ),
1057
- /* @__PURE__ */ jsx7(
1058
- "button",
1059
- {
1060
- onClick: cancelPending,
1061
- className: "mt-2 text-xs text-neutral-400 hover:text-neutral-600 transition-colors",
1062
- children: "Cancel"
1063
- }
1064
- )
1065
- ] }) })
1096
+ /* @__PURE__ */ jsx7(
1097
+ "div",
1098
+ {
1099
+ ref: pendingRef,
1100
+ className: "absolute w-72",
1101
+ style: {
1102
+ marginLeft: pendingFlip.x ? -308 : 20,
1103
+ marginTop: pendingFlip.y ? void 0 : -12,
1104
+ ...pendingFlip.y ? { bottom: 0 } : {}
1105
+ },
1106
+ children: /* @__PURE__ */ jsxs6("div", { className: "bg-white rounded-xl shadow-2xl border border-neutral-200 p-3", children: [
1107
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 mb-2", children: [
1108
+ /* @__PURE__ */ jsx7("p", { className: "text-xs text-neutral-500", children: "New comment" }),
1109
+ pendingPin.targetLabel && /* @__PURE__ */ jsx7("span", { className: "text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium", children: pendingPin.targetLabel })
1110
+ ] }),
1111
+ /* @__PURE__ */ jsx7(
1112
+ CommentComposer,
1113
+ {
1114
+ onSubmit: handleNewComment,
1115
+ placeholder: "What's on your mind?",
1116
+ autoFocus: true
1117
+ }
1118
+ )
1119
+ ] })
1120
+ }
1121
+ )
1066
1122
  ]
1067
1123
  }
1068
1124
  )
1069
1125
  ]
1070
1126
  }
1071
1127
  ),
1072
- commentMode && !pendingPin && !showingUserPrompt && /* @__PURE__ */ jsx7("div", { className: "absolute bottom-6 left-1/2 -translate-x-1/2 z-[60] pointer-events-none", children: /* @__PURE__ */ jsx7("div", { className: "bg-neutral-900/80 text-white text-sm px-4 py-2 rounded-full backdrop-blur-sm", children: "Click anywhere to add a comment" }) }),
1128
+ commentMode && !pendingPin && !showingUserPrompt && /* @__PURE__ */ jsx7("div", { className: "absolute bottom-6 left-1/2 -translate-x-1/2 z-[60] pointer-events-none", children: /* @__PURE__ */ jsx7(
1129
+ "div",
1130
+ {
1131
+ className: "text-white text-sm px-4 py-2 rounded-full backdrop-blur-sm",
1132
+ style: { backgroundColor: `color-mix(in oklab, ${brandColor} 80%, transparent)` },
1133
+ children: "Click anywhere to add a comment"
1134
+ }
1135
+ ) }),
1073
1136
  /* @__PURE__ */ jsx7(UserPrompt, {})
1074
1137
  ] });
1075
1138
  }
@@ -1083,7 +1146,8 @@ function CommentToggle() {
1083
1146
  sidebarOpen,
1084
1147
  setSidebarOpen,
1085
1148
  unresolvedCount,
1086
- setActiveThreadId
1149
+ setActiveThreadId,
1150
+ brandColor
1087
1151
  } = useApostil();
1088
1152
  return /* @__PURE__ */ jsxs7("div", { className: "absolute bottom-5 right-5 z-[65] flex flex-col gap-2 items-end", children: [
1089
1153
  /* @__PURE__ */ jsx8(
@@ -1092,7 +1156,8 @@ function CommentToggle() {
1092
1156
  onClick: () => setSidebarOpen(!sidebarOpen),
1093
1157
  className: `flex items-center justify-center w-10 h-10 rounded-full shadow-lg
1094
1158
  transition-all duration-200 hover:scale-105
1095
- ${sidebarOpen ? "bg-neutral-900 text-white" : "bg-white text-neutral-600 hover:bg-neutral-50 border border-neutral-200"}`,
1159
+ ${sidebarOpen ? "text-white" : "bg-white text-neutral-600 hover:bg-neutral-50 border border-neutral-200"}`,
1160
+ style: sidebarOpen ? { backgroundColor: brandColor } : void 0,
1096
1161
  title: "Toggle comment list",
1097
1162
  children: /* @__PURE__ */ jsx8(List, { className: "w-4 h-4" })
1098
1163
  }
@@ -1110,7 +1175,8 @@ function CommentToggle() {
1110
1175
  },
1111
1176
  className: `relative flex items-center justify-center w-12 h-12 rounded-full shadow-lg
1112
1177
  transition-all duration-200 hover:scale-105
1113
- ${commentMode ? "bg-neutral-900 text-white ring-2 ring-neutral-400" : "bg-white text-neutral-700 hover:bg-neutral-50 border border-neutral-200"}`,
1178
+ ${commentMode ? "text-white ring-2 ring-neutral-400" : "bg-white text-neutral-700 hover:bg-neutral-50 border border-neutral-200"}`,
1179
+ style: commentMode ? { backgroundColor: brandColor } : void 0,
1114
1180
  title: commentMode ? "Exit comment mode" : "Add comment",
1115
1181
  children: [
1116
1182
  commentMode ? /* @__PURE__ */ jsx8(X, { className: "w-5 h-5" }) : /* @__PURE__ */ jsx8(MessageSquare, { className: "w-5 h-5" }),
@@ -1143,7 +1209,8 @@ function CommentSidebar() {
1143
1209
  sidebarOpen,
1144
1210
  setSidebarOpen,
1145
1211
  setActiveThreadId,
1146
- resolveThread
1212
+ resolveThread,
1213
+ brandColor
1147
1214
  } = useApostil();
1148
1215
  const [tab, setTab] = useState7("page");
1149
1216
  const [allPages, setAllPages] = useState7([]);
@@ -1187,7 +1254,8 @@ function CommentSidebar() {
1187
1254
  "button",
1188
1255
  {
1189
1256
  onClick: () => setTab("page"),
1190
- className: `flex-1 flex items-center justify-center gap-1.5 py-2 text-xs font-medium transition-colors ${tab === "page" ? "text-neutral-900 border-b-2 border-neutral-900" : "text-neutral-400 hover:text-neutral-600"}`,
1257
+ className: `flex-1 flex items-center justify-center gap-1.5 py-2 text-xs font-medium transition-colors ${tab === "page" ? "border-b-2" : "text-neutral-400 hover:text-neutral-600"}`,
1258
+ style: tab === "page" ? { color: brandColor, borderColor: brandColor } : void 0,
1191
1259
  children: [
1192
1260
  /* @__PURE__ */ jsx9(FileText, { className: "w-3 h-3" }),
1193
1261
  "This Page",
@@ -1199,7 +1267,8 @@ function CommentSidebar() {
1199
1267
  "button",
1200
1268
  {
1201
1269
  onClick: () => setTab("all"),
1202
- className: `flex-1 flex items-center justify-center gap-1.5 py-2 text-xs font-medium transition-colors ${tab === "all" ? "text-neutral-900 border-b-2 border-neutral-900" : "text-neutral-400 hover:text-neutral-600"}`,
1270
+ className: `flex-1 flex items-center justify-center gap-1.5 py-2 text-xs font-medium transition-colors ${tab === "all" ? "border-b-2" : "text-neutral-400 hover:text-neutral-600"}`,
1271
+ style: tab === "all" ? { color: brandColor, borderColor: brandColor } : void 0,
1203
1272
  children: [
1204
1273
  /* @__PURE__ */ jsx9(Globe, { className: "w-3 h-3" }),
1205
1274
  "All Pages"