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/README.md CHANGED
@@ -98,13 +98,19 @@ Core context provider. Use directly for custom setups. When not using `npx apost
98
98
  ```tsx
99
99
  import "apostil/styles.css";
100
100
 
101
- <ApostilProvider pageId="my-page" storage={customAdapter}>
101
+ <ApostilProvider pageId="my-page" storage={customAdapter} brandColor="#2563eb">
102
102
  {children}
103
103
  <CommentOverlay />
104
104
  <CommentToggle />
105
105
  </ApostilProvider>
106
106
  ```
107
107
 
108
+ | Prop | Type | Default | Description |
109
+ |------|------|---------|-------------|
110
+ | `pageId` | `string` | required | Current page identifier |
111
+ | `storage` | `ApostilStorage` | REST `/api/apostil` | Storage adapter |
112
+ | `brandColor` | `string` | `"#171717"` | Accent color for buttons, tabs, and UI elements |
113
+
108
114
  ### `<CommentOverlay>`
109
115
 
110
116
  Captures clicks in comment mode. Auto-detects z-index to sit above popovers and modals.
package/dist/index.cjs CHANGED
@@ -150,6 +150,7 @@ var ApostilContext = (0, import_react.createContext)(null);
150
150
  function ApostilProvider({
151
151
  pageId,
152
152
  storage,
153
+ brandColor = "#171717",
153
154
  children
154
155
  }) {
155
156
  const adapter = storage ?? defaultAdapter;
@@ -167,6 +168,7 @@ function ApostilProvider({
167
168
  (0, import_react.useEffect)(() => {
168
169
  pageIdRef.current = pageId;
169
170
  setLoaded(false);
171
+ hadThreadsRef.current = false;
170
172
  debug.log("loading threads for pageId:", pageId);
171
173
  adapter.load(pageId).then((t) => {
172
174
  if (pageIdRef.current === pageId) {
@@ -176,10 +178,17 @@ function ApostilProvider({
176
178
  }
177
179
  });
178
180
  }, [pageId]);
181
+ const hadThreadsRef = (0, import_react.useRef)(false);
179
182
  (0, import_react.useEffect)(() => {
180
- if (loaded && pageIdRef.current === pageId && threads.length > 0) {
183
+ if (!loaded || pageIdRef.current !== pageId) return;
184
+ if (threads.length > 0) {
185
+ hadThreadsRef.current = true;
181
186
  debug.log("saving", threads.length, "threads for pageId:", pageId);
182
187
  adapter.save(pageId, threads);
188
+ } else if (hadThreadsRef.current) {
189
+ debug.log("saving empty threads for pageId:", pageId);
190
+ adapter.save(pageId, threads);
191
+ hadThreadsRef.current = false;
183
192
  }
184
193
  }, [threads, pageId, loaded]);
185
194
  const setUser = (0, import_react.useCallback)((name) => {
@@ -241,6 +250,7 @@ function ApostilProvider({
241
250
  commentMode,
242
251
  activeThreadId,
243
252
  sidebarOpen,
253
+ brandColor,
244
254
  setCommentMode,
245
255
  setActiveThreadId,
246
256
  setSidebarOpen,
@@ -474,7 +484,11 @@ function OverlayPin({
474
484
  (0, import_react2.useEffect)(() => {
475
485
  updatePos();
476
486
  window.addEventListener("resize", updatePos);
477
- return () => window.removeEventListener("resize", updatePos);
487
+ document.addEventListener("scroll", updatePos, true);
488
+ return () => {
489
+ window.removeEventListener("resize", updatePos);
490
+ document.removeEventListener("scroll", updatePos, true);
491
+ };
478
492
  }, [updatePos]);
479
493
  if (!pos) return null;
480
494
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -507,6 +521,7 @@ function CommentPin({
507
521
  index,
508
522
  overlayRef
509
523
  }) {
524
+ if (thread.resolved) return null;
510
525
  if (thread.targetId) {
511
526
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TargetedPin, { thread, index }, `targeted-${thread.id}`);
512
527
  }
@@ -586,11 +601,15 @@ function CommentComposer({
586
601
  placeholder = "Add a comment...",
587
602
  autoFocus = false
588
603
  }) {
604
+ const { brandColor } = useApostil();
589
605
  const [value, setValue] = (0, import_react3.useState)("");
590
606
  const inputRef = (0, import_react3.useRef)(null);
591
607
  (0, import_react3.useEffect)(() => {
592
608
  if (autoFocus) {
593
- inputRef.current?.focus();
609
+ const timer = setTimeout(() => {
610
+ inputRef.current?.focus();
611
+ }, 50);
612
+ return () => clearTimeout(timer);
594
613
  }
595
614
  }, [autoFocus]);
596
615
  const handleSubmit = () => {
@@ -614,7 +633,7 @@ function CommentComposer({
614
633
  },
615
634
  placeholder,
616
635
  rows: 1,
617
- 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]"
636
+ 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]"
618
637
  }
619
638
  ),
620
639
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -622,7 +641,8 @@ function CommentComposer({
622
641
  {
623
642
  onClick: handleSubmit,
624
643
  disabled: !value.trim(),
625
- 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",
644
+ className: "flex items-center justify-center w-9 h-9 rounded-lg\n text-white disabled:opacity-30\n transition-colors shrink-0",
645
+ style: { backgroundColor: brandColor },
626
646
  children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Send, { className: "w-3.5 h-3.5" })
627
647
  }
628
648
  )
@@ -649,9 +669,17 @@ function ApostilThreadPopover({
649
669
  const ref = (0, import_react4.useRef)(null);
650
670
  const isOpen = activeThreadId === thread.id;
651
671
  const [pos, setPos] = (0, import_react4.useState)(null);
672
+ const [flip, setFlip] = (0, import_react4.useState)({ x: false, y: false });
652
673
  const updatePos = (0, import_react4.useCallback)(() => {
653
674
  setPos(resolvePosition(thread, overlayRef.current));
654
675
  }, [thread, overlayRef]);
676
+ (0, import_react4.useEffect)(() => {
677
+ if (!isOpen || !pos || !ref.current) return;
678
+ const rect = ref.current.getBoundingClientRect();
679
+ const flipX = rect.right > window.innerWidth;
680
+ const flipY = rect.bottom > window.innerHeight;
681
+ setFlip({ x: flipX, y: flipY });
682
+ }, [isOpen, pos]);
655
683
  (0, import_react4.useEffect)(() => {
656
684
  if (!isOpen) return;
657
685
  updatePos();
@@ -680,8 +708,14 @@ function ApostilThreadPopover({
680
708
  "div",
681
709
  {
682
710
  ref,
683
- className: "absolute z-[70] ml-5 -mt-3",
684
- style: { left: pos.left, top: pos.top },
711
+ className: "absolute z-[70]",
712
+ style: {
713
+ left: pos.left,
714
+ top: pos.top,
715
+ marginLeft: flip.x ? -340 : 20,
716
+ marginTop: flip.y ? -12 : -12,
717
+ ...flip.y ? { transform: "translateY(-100%)" } : {}
718
+ },
685
719
  onClick: (e) => e.stopPropagation(),
686
720
  children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "w-80 bg-white rounded-xl shadow-2xl border border-neutral-200 overflow-hidden", children: [
687
721
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center justify-between px-4 py-2.5 border-b border-neutral-100 bg-neutral-50", children: [
@@ -747,7 +781,7 @@ function ApostilThreadPopover({
747
781
  var import_react5 = require("react");
748
782
  var import_jsx_runtime6 = require("react/jsx-runtime");
749
783
  function UserPrompt() {
750
- const { user, setUser, commentMode } = useApostil();
784
+ const { user, setUser, commentMode, brandColor } = useApostil();
751
785
  const [name, setName] = (0, import_react5.useState)("");
752
786
  if (user || !commentMode) return null;
753
787
  const handleSubmit = () => {
@@ -783,7 +817,8 @@ function UserPrompt() {
783
817
  {
784
818
  onClick: handleSubmit,
785
819
  disabled: !name.trim(),
786
- 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",
820
+ className: "w-full py-2 rounded-lg text-white text-sm font-medium\n disabled:opacity-30 transition-colors",
821
+ style: { backgroundColor: brandColor },
787
822
  children: "Continue"
788
823
  }
789
824
  )
@@ -934,18 +969,26 @@ function findCommentTarget(el, boundary) {
934
969
  };
935
970
  }
936
971
  function CommentOverlay() {
937
- const { threads, commentMode, setCommentMode, user, addThread, setActiveThreadId } = useApostil();
972
+ const { threads, commentMode, setCommentMode, user, addThread, activeThreadId, setActiveThreadId, brandColor } = useApostil();
938
973
  const overlayRef = (0, import_react6.useRef)(null);
939
974
  const [pendingPin, setPendingPin] = (0, import_react6.useState)(null);
940
975
  const [pendingPixel, setPendingPixel] = (0, import_react6.useState)(null);
976
+ const pendingRef = (0, import_react6.useRef)(null);
977
+ const [pendingFlip, setPendingFlip] = (0, import_react6.useState)({ x: false, y: false });
941
978
  const handleClick = (0, import_react6.useCallback)(
942
979
  (e) => {
943
980
  if (!commentMode || !overlayRef.current) return;
944
981
  const overlayRect = overlayRef.current.getBoundingClientRect();
945
982
  const overlay = overlayRef.current;
946
- overlay.style.pointerEvents = "none";
947
- const elementBelow = document.elementFromPoint(e.clientX, e.clientY);
948
- overlay.style.pointerEvents = "";
983
+ const elements = document.elementsFromPoint(e.clientX, e.clientY);
984
+ let elementBelow = null;
985
+ for (const el of elements) {
986
+ if (el === overlay || overlay.contains(el)) continue;
987
+ if (el instanceof HTMLElement) {
988
+ elementBelow = el;
989
+ break;
990
+ }
991
+ }
949
992
  debug.log(" click at", { clientX: e.clientX, clientY: e.clientY });
950
993
  debug.log(" element below overlay:", elementBelow);
951
994
  if (elementBelow) {
@@ -1014,6 +1057,16 @@ function CommentOverlay() {
1014
1057
  setPendingPixel(null);
1015
1058
  setCommentMode(false);
1016
1059
  }, [setCommentMode]);
1060
+ (0, import_react6.useEffect)(() => {
1061
+ if (!pendingPixel || !overlayRef.current) return;
1062
+ const overlayRect = overlayRef.current.getBoundingClientRect();
1063
+ const clickX = overlayRect.left + pendingPixel.left;
1064
+ const clickY = overlayRect.top + pendingPixel.top;
1065
+ setPendingFlip({
1066
+ x: clickX + 308 > window.innerWidth,
1067
+ y: clickY + 200 > window.innerHeight
1068
+ });
1069
+ }, [pendingPixel, overlayRef]);
1017
1070
  (0, import_react6.useEffect)(() => {
1018
1071
  const hash = window.location.hash;
1019
1072
  console.log("[apostil] hash check:", hash, "threads:", threads.length, threads.map((t) => t.id));
@@ -1039,6 +1092,8 @@ function CommentOverlay() {
1039
1092
  setPendingPin(null);
1040
1093
  setPendingPixel(null);
1041
1094
  setCommentMode(false);
1095
+ } else if (activeThreadId) {
1096
+ setActiveThreadId(null);
1042
1097
  } else if (commentMode) {
1043
1098
  setCommentMode(false);
1044
1099
  }
@@ -1060,11 +1115,8 @@ function CommentOverlay() {
1060
1115
  };
1061
1116
  document.addEventListener("keydown", handler);
1062
1117
  return () => document.removeEventListener("keydown", handler);
1063
- }, [commentMode, pendingPin, setCommentMode, setActiveThreadId]);
1064
- const sortedThreads = [...threads].sort((a, b) => {
1065
- if (a.resolved !== b.resolved) return a.resolved ? 1 : -1;
1066
- return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
1067
- });
1118
+ }, [commentMode, pendingPin, activeThreadId, setCommentMode, setActiveThreadId]);
1119
+ const visibleThreads = threads.filter((t) => !t.resolved).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
1068
1120
  const showingUserPrompt = commentMode && !user;
1069
1121
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1070
1122
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
@@ -1075,7 +1127,7 @@ function CommentOverlay() {
1075
1127
  style: { zIndex: commentMode ? getHighestZIndex() + 10 : 55 },
1076
1128
  onMouseDown: handleClick,
1077
1129
  children: [
1078
- sortedThreads.map((thread, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "pointer-events-auto", children: [
1130
+ visibleThreads.map((thread, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "pointer-events-auto", children: [
1079
1131
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CommentPin, { thread, index: i, overlayRef }),
1080
1132
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ApostilThreadPopover, { thread, overlayRef })
1081
1133
  ] }, thread.id)),
@@ -1098,35 +1150,46 @@ function CommentOverlay() {
1098
1150
  children: "+"
1099
1151
  }
1100
1152
  ),
1101
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "absolute ml-5 -mt-3 w-72", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "bg-white rounded-xl shadow-2xl border border-neutral-200 p-3", children: [
1102
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2 mb-2", children: [
1103
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-xs text-neutral-500", children: "New comment" }),
1104
- pendingPin.targetLabel && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium", children: pendingPin.targetLabel })
1105
- ] }),
1106
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1107
- CommentComposer,
1108
- {
1109
- onSubmit: handleNewComment,
1110
- placeholder: "What's on your mind?",
1111
- autoFocus: true
1112
- }
1113
- ),
1114
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1115
- "button",
1116
- {
1117
- onClick: cancelPending,
1118
- className: "mt-2 text-xs text-neutral-400 hover:text-neutral-600 transition-colors",
1119
- children: "Cancel"
1120
- }
1121
- )
1122
- ] }) })
1153
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1154
+ "div",
1155
+ {
1156
+ ref: pendingRef,
1157
+ className: "absolute w-72",
1158
+ style: {
1159
+ marginLeft: pendingFlip.x ? -308 : 20,
1160
+ marginTop: pendingFlip.y ? void 0 : -12,
1161
+ ...pendingFlip.y ? { bottom: 0 } : {}
1162
+ },
1163
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "bg-white rounded-xl shadow-2xl border border-neutral-200 p-3", children: [
1164
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2 mb-2", children: [
1165
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-xs text-neutral-500", children: "New comment" }),
1166
+ pendingPin.targetLabel && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium", children: pendingPin.targetLabel })
1167
+ ] }),
1168
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1169
+ CommentComposer,
1170
+ {
1171
+ onSubmit: handleNewComment,
1172
+ placeholder: "What's on your mind?",
1173
+ autoFocus: true
1174
+ }
1175
+ )
1176
+ ] })
1177
+ }
1178
+ )
1123
1179
  ]
1124
1180
  }
1125
1181
  )
1126
1182
  ]
1127
1183
  }
1128
1184
  ),
1129
- commentMode && !pendingPin && !showingUserPrompt && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "absolute bottom-6 left-1/2 -translate-x-1/2 z-[60] pointer-events-none", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("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" }) }),
1185
+ commentMode && !pendingPin && !showingUserPrompt && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "absolute bottom-6 left-1/2 -translate-x-1/2 z-[60] pointer-events-none", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1186
+ "div",
1187
+ {
1188
+ className: "text-white text-sm px-4 py-2 rounded-full backdrop-blur-sm",
1189
+ style: { backgroundColor: `color-mix(in oklab, ${brandColor} 80%, transparent)` },
1190
+ children: "Click anywhere to add a comment"
1191
+ }
1192
+ ) }),
1130
1193
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(UserPrompt, {})
1131
1194
  ] });
1132
1195
  }
@@ -1140,7 +1203,8 @@ function CommentToggle() {
1140
1203
  sidebarOpen,
1141
1204
  setSidebarOpen,
1142
1205
  unresolvedCount,
1143
- setActiveThreadId
1206
+ setActiveThreadId,
1207
+ brandColor
1144
1208
  } = useApostil();
1145
1209
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "absolute bottom-5 right-5 z-[65] flex flex-col gap-2 items-end", children: [
1146
1210
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
@@ -1149,7 +1213,8 @@ function CommentToggle() {
1149
1213
  onClick: () => setSidebarOpen(!sidebarOpen),
1150
1214
  className: `flex items-center justify-center w-10 h-10 rounded-full shadow-lg
1151
1215
  transition-all duration-200 hover:scale-105
1152
- ${sidebarOpen ? "bg-neutral-900 text-white" : "bg-white text-neutral-600 hover:bg-neutral-50 border border-neutral-200"}`,
1216
+ ${sidebarOpen ? "text-white" : "bg-white text-neutral-600 hover:bg-neutral-50 border border-neutral-200"}`,
1217
+ style: sidebarOpen ? { backgroundColor: brandColor } : void 0,
1153
1218
  title: "Toggle comment list",
1154
1219
  children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(List, { className: "w-4 h-4" })
1155
1220
  }
@@ -1167,7 +1232,8 @@ function CommentToggle() {
1167
1232
  },
1168
1233
  className: `relative flex items-center justify-center w-12 h-12 rounded-full shadow-lg
1169
1234
  transition-all duration-200 hover:scale-105
1170
- ${commentMode ? "bg-neutral-900 text-white ring-2 ring-neutral-400" : "bg-white text-neutral-700 hover:bg-neutral-50 border border-neutral-200"}`,
1235
+ ${commentMode ? "text-white ring-2 ring-neutral-400" : "bg-white text-neutral-700 hover:bg-neutral-50 border border-neutral-200"}`,
1236
+ style: commentMode ? { backgroundColor: brandColor } : void 0,
1171
1237
  title: commentMode ? "Exit comment mode" : "Add comment",
1172
1238
  children: [
1173
1239
  commentMode ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(X, { className: "w-5 h-5" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MessageSquare, { className: "w-5 h-5" }),
@@ -1200,7 +1266,8 @@ function CommentSidebar() {
1200
1266
  sidebarOpen,
1201
1267
  setSidebarOpen,
1202
1268
  setActiveThreadId,
1203
- resolveThread
1269
+ resolveThread,
1270
+ brandColor
1204
1271
  } = useApostil();
1205
1272
  const [tab, setTab] = (0, import_react7.useState)("page");
1206
1273
  const [allPages, setAllPages] = (0, import_react7.useState)([]);
@@ -1244,7 +1311,8 @@ function CommentSidebar() {
1244
1311
  "button",
1245
1312
  {
1246
1313
  onClick: () => setTab("page"),
1247
- 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"}`,
1314
+ 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"}`,
1315
+ style: tab === "page" ? { color: brandColor, borderColor: brandColor } : void 0,
1248
1316
  children: [
1249
1317
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FileText, { className: "w-3 h-3" }),
1250
1318
  "This Page",
@@ -1256,7 +1324,8 @@ function CommentSidebar() {
1256
1324
  "button",
1257
1325
  {
1258
1326
  onClick: () => setTab("all"),
1259
- 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"}`,
1327
+ 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"}`,
1328
+ style: tab === "all" ? { color: brandColor, borderColor: brandColor } : void 0,
1260
1329
  children: [
1261
1330
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Globe, { className: "w-3 h-3" }),
1262
1331
  "All Pages"