apostil 0.1.5 → 0.1.6

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,
@@ -586,11 +596,14 @@ function CommentComposer({
586
596
  placeholder = "Add a comment...",
587
597
  autoFocus = false
588
598
  }) {
599
+ const { brandColor } = useApostil();
589
600
  const [value, setValue] = (0, import_react3.useState)("");
590
601
  const inputRef = (0, import_react3.useRef)(null);
591
602
  (0, import_react3.useEffect)(() => {
592
603
  if (autoFocus) {
593
- inputRef.current?.focus();
604
+ requestAnimationFrame(() => {
605
+ inputRef.current?.focus();
606
+ });
594
607
  }
595
608
  }, [autoFocus]);
596
609
  const handleSubmit = () => {
@@ -622,7 +635,8 @@ function CommentComposer({
622
635
  {
623
636
  onClick: handleSubmit,
624
637
  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",
638
+ className: "flex items-center justify-center w-8 h-8 rounded-lg\n text-white disabled:opacity-30\n transition-colors shrink-0",
639
+ style: { backgroundColor: brandColor },
626
640
  children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Send, { className: "w-3.5 h-3.5" })
627
641
  }
628
642
  )
@@ -649,9 +663,17 @@ function ApostilThreadPopover({
649
663
  const ref = (0, import_react4.useRef)(null);
650
664
  const isOpen = activeThreadId === thread.id;
651
665
  const [pos, setPos] = (0, import_react4.useState)(null);
666
+ const [flip, setFlip] = (0, import_react4.useState)({ x: false, y: false });
652
667
  const updatePos = (0, import_react4.useCallback)(() => {
653
668
  setPos(resolvePosition(thread, overlayRef.current));
654
669
  }, [thread, overlayRef]);
670
+ (0, import_react4.useEffect)(() => {
671
+ if (!isOpen || !pos || !ref.current) return;
672
+ const rect = ref.current.getBoundingClientRect();
673
+ const flipX = rect.right > window.innerWidth;
674
+ const flipY = rect.bottom > window.innerHeight;
675
+ setFlip({ x: flipX, y: flipY });
676
+ }, [isOpen, pos]);
655
677
  (0, import_react4.useEffect)(() => {
656
678
  if (!isOpen) return;
657
679
  updatePos();
@@ -680,8 +702,14 @@ function ApostilThreadPopover({
680
702
  "div",
681
703
  {
682
704
  ref,
683
- className: "absolute z-[70] ml-5 -mt-3",
684
- style: { left: pos.left, top: pos.top },
705
+ className: "absolute z-[70]",
706
+ style: {
707
+ left: pos.left,
708
+ top: pos.top,
709
+ marginLeft: flip.x ? -340 : 20,
710
+ marginTop: flip.y ? -12 : -12,
711
+ ...flip.y ? { transform: "translateY(-100%)" } : {}
712
+ },
685
713
  onClick: (e) => e.stopPropagation(),
686
714
  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
715
  /* @__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 +775,7 @@ function ApostilThreadPopover({
747
775
  var import_react5 = require("react");
748
776
  var import_jsx_runtime6 = require("react/jsx-runtime");
749
777
  function UserPrompt() {
750
- const { user, setUser, commentMode } = useApostil();
778
+ const { user, setUser, commentMode, brandColor } = useApostil();
751
779
  const [name, setName] = (0, import_react5.useState)("");
752
780
  if (user || !commentMode) return null;
753
781
  const handleSubmit = () => {
@@ -783,7 +811,8 @@ function UserPrompt() {
783
811
  {
784
812
  onClick: handleSubmit,
785
813
  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",
814
+ className: "w-full py-2 rounded-lg text-white text-sm font-medium\n disabled:opacity-30 transition-colors",
815
+ style: { backgroundColor: brandColor },
787
816
  children: "Continue"
788
817
  }
789
818
  )
@@ -934,10 +963,12 @@ function findCommentTarget(el, boundary) {
934
963
  };
935
964
  }
936
965
  function CommentOverlay() {
937
- const { threads, commentMode, setCommentMode, user, addThread, setActiveThreadId } = useApostil();
966
+ const { threads, commentMode, setCommentMode, user, addThread, setActiveThreadId, brandColor } = useApostil();
938
967
  const overlayRef = (0, import_react6.useRef)(null);
939
968
  const [pendingPin, setPendingPin] = (0, import_react6.useState)(null);
940
969
  const [pendingPixel, setPendingPixel] = (0, import_react6.useState)(null);
970
+ const pendingRef = (0, import_react6.useRef)(null);
971
+ const [pendingFlip, setPendingFlip] = (0, import_react6.useState)({ x: false, y: false });
941
972
  const handleClick = (0, import_react6.useCallback)(
942
973
  (e) => {
943
974
  if (!commentMode || !overlayRef.current) return;
@@ -1014,6 +1045,17 @@ function CommentOverlay() {
1014
1045
  setPendingPixel(null);
1015
1046
  setCommentMode(false);
1016
1047
  }, [setCommentMode]);
1048
+ (0, import_react6.useEffect)(() => {
1049
+ if (!pendingPin || !pendingRef.current) return;
1050
+ requestAnimationFrame(() => {
1051
+ if (!pendingRef.current) return;
1052
+ const rect = pendingRef.current.getBoundingClientRect();
1053
+ setPendingFlip({
1054
+ x: rect.right > window.innerWidth,
1055
+ y: rect.bottom > window.innerHeight
1056
+ });
1057
+ });
1058
+ }, [pendingPin]);
1017
1059
  (0, import_react6.useEffect)(() => {
1018
1060
  const hash = window.location.hash;
1019
1061
  console.log("[apostil] hash check:", hash, "threads:", threads.length, threads.map((t) => t.id));
@@ -1098,35 +1140,46 @@ function CommentOverlay() {
1098
1140
  children: "+"
1099
1141
  }
1100
1142
  ),
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
- ] }) })
1143
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1144
+ "div",
1145
+ {
1146
+ ref: pendingRef,
1147
+ className: "absolute w-72",
1148
+ style: {
1149
+ marginLeft: pendingFlip.x ? -308 : 20,
1150
+ marginTop: pendingFlip.y ? void 0 : -12,
1151
+ ...pendingFlip.y ? { bottom: 0 } : {}
1152
+ },
1153
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "bg-white rounded-xl shadow-2xl border border-neutral-200 p-3", children: [
1154
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2 mb-2", children: [
1155
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-xs text-neutral-500", children: "New comment" }),
1156
+ 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 })
1157
+ ] }),
1158
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1159
+ CommentComposer,
1160
+ {
1161
+ onSubmit: handleNewComment,
1162
+ placeholder: "What's on your mind?",
1163
+ autoFocus: true
1164
+ }
1165
+ )
1166
+ ] })
1167
+ }
1168
+ )
1123
1169
  ]
1124
1170
  }
1125
1171
  )
1126
1172
  ]
1127
1173
  }
1128
1174
  ),
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" }) }),
1175
+ 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)(
1176
+ "div",
1177
+ {
1178
+ className: "text-white text-sm px-4 py-2 rounded-full backdrop-blur-sm",
1179
+ style: { backgroundColor: `color-mix(in oklab, ${brandColor} 80%, transparent)` },
1180
+ children: "Click anywhere to add a comment"
1181
+ }
1182
+ ) }),
1130
1183
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(UserPrompt, {})
1131
1184
  ] });
1132
1185
  }
@@ -1140,7 +1193,8 @@ function CommentToggle() {
1140
1193
  sidebarOpen,
1141
1194
  setSidebarOpen,
1142
1195
  unresolvedCount,
1143
- setActiveThreadId
1196
+ setActiveThreadId,
1197
+ brandColor
1144
1198
  } = useApostil();
1145
1199
  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
1200
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
@@ -1149,7 +1203,8 @@ function CommentToggle() {
1149
1203
  onClick: () => setSidebarOpen(!sidebarOpen),
1150
1204
  className: `flex items-center justify-center w-10 h-10 rounded-full shadow-lg
1151
1205
  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"}`,
1206
+ ${sidebarOpen ? "text-white" : "bg-white text-neutral-600 hover:bg-neutral-50 border border-neutral-200"}`,
1207
+ style: sidebarOpen ? { backgroundColor: brandColor } : void 0,
1153
1208
  title: "Toggle comment list",
1154
1209
  children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(List, { className: "w-4 h-4" })
1155
1210
  }
@@ -1167,7 +1222,8 @@ function CommentToggle() {
1167
1222
  },
1168
1223
  className: `relative flex items-center justify-center w-12 h-12 rounded-full shadow-lg
1169
1224
  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"}`,
1225
+ ${commentMode ? "text-white ring-2 ring-neutral-400" : "bg-white text-neutral-700 hover:bg-neutral-50 border border-neutral-200"}`,
1226
+ style: commentMode ? { backgroundColor: brandColor } : void 0,
1171
1227
  title: commentMode ? "Exit comment mode" : "Add comment",
1172
1228
  children: [
1173
1229
  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 +1256,8 @@ function CommentSidebar() {
1200
1256
  sidebarOpen,
1201
1257
  setSidebarOpen,
1202
1258
  setActiveThreadId,
1203
- resolveThread
1259
+ resolveThread,
1260
+ brandColor
1204
1261
  } = useApostil();
1205
1262
  const [tab, setTab] = (0, import_react7.useState)("page");
1206
1263
  const [allPages, setAllPages] = (0, import_react7.useState)([]);
@@ -1244,7 +1301,8 @@ function CommentSidebar() {
1244
1301
  "button",
1245
1302
  {
1246
1303
  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"}`,
1304
+ 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"}`,
1305
+ style: tab === "page" ? { color: brandColor, borderColor: brandColor } : void 0,
1248
1306
  children: [
1249
1307
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FileText, { className: "w-3 h-3" }),
1250
1308
  "This Page",
@@ -1256,7 +1314,8 @@ function CommentSidebar() {
1256
1314
  "button",
1257
1315
  {
1258
1316
  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"}`,
1317
+ 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"}`,
1318
+ style: tab === "all" ? { color: brandColor, borderColor: brandColor } : void 0,
1260
1319
  children: [
1261
1320
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Globe, { className: "w-3 h-3" }),
1262
1321
  "All Pages"