@opendocsdev/cli 0.2.2 → 0.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendocsdev/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -30,28 +30,23 @@ const shouldTrack = backend && siteId && analyticsEnabled;
30
30
  is:inline
31
31
  >
32
32
  (function() {
33
- // Get configuration from script tag
34
- const script = document.currentScript;
35
- const backend = script.getAttribute('data-backend');
36
- const siteId = script.getAttribute('data-site-id');
37
- const pathFromProp = script.getAttribute('data-path');
33
+ var script = document.currentScript;
34
+ var backend = script.getAttribute('data-backend');
35
+ var siteId = script.getAttribute('data-site-id');
38
36
 
39
- // Use provided path or current pathname
40
- const path = pathFromProp || window.location.pathname;
37
+ function trackPageview() {
38
+ fetch(backend + '/api/analytics/pv', {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({
42
+ siteId: siteId,
43
+ path: window.location.pathname,
44
+ }),
45
+ }).catch(function() {});
46
+ }
41
47
 
42
- // Send pageview to backend (short path to avoid ad blockers)
43
- fetch(`${backend}/api/analytics/pv`, {
44
- method: 'POST',
45
- headers: {
46
- 'Content-Type': 'application/json',
47
- },
48
- body: JSON.stringify({
49
- siteId: siteId,
50
- path: path,
51
- }),
52
- }).catch(function(err) {
53
- console.debug('[opendocs] Analytics error:', err);
54
- });
48
+ trackPageview();
49
+ document.addEventListener('astro:after-swap', trackPageview);
55
50
  })();
56
51
  </script>
57
52
  )}
@@ -80,13 +80,13 @@ export function SearchModal({ isOpen, onClose, backend, siteId }: SearchModalPro
80
80
  const resultsRef = useRef<HTMLDivElement>(null);
81
81
  const pagefindRef = useRef<any>(null);
82
82
 
83
- // Track the last search for analytics (only sent on meaningful actions)
84
- const lastSearchRef = useRef<{ query: string; resultCount: number } | null>(null);
85
83
  const hasTrackedRef = useRef(false);
84
+ const lastResultCountRef = useRef(0);
85
+ const queryRef = useRef(query);
86
+ queryRef.current = query;
86
87
 
87
88
  const loadPagefind = async () => {
88
89
  try {
89
- // Use Function constructor to create a truly dynamic import that Vite won't analyze
90
90
  const importPagefind = new Function(
91
91
  'return import("/pagefind/pagefind.js")'
92
92
  ) as () => Promise<any>;
@@ -100,39 +100,28 @@ export function SearchModal({ isOpen, onClose, backend, siteId }: SearchModalPro
100
100
  }
101
101
  };
102
102
 
103
- // Track search query for analytics - only tracks once per search session
104
- const trackSearch = useCallback(() => {
103
+ // Fire-and-forget analytics using current query state
104
+ // Tracks on close or result click — captures the query text at that moment,
105
+ // including searches with 0 results (failed searches)
106
+ const trackSearch = useCallback((searchQuery: string, resultCount: number) => {
105
107
  if (!backend || !siteId) return;
106
108
  if (hasTrackedRef.current) return;
107
- if (!lastSearchRef.current) return;
108
-
109
- const { query: searchQuery, resultCount } = lastSearchRef.current;
110
109
  if (!searchQuery.trim()) return;
111
110
 
112
111
  hasTrackedRef.current = true;
113
-
114
- fetch(`${backend}/api/analytics/sq`, {
115
- method: "POST",
116
- headers: { "Content-Type": "application/json" },
117
- body: JSON.stringify({
118
- siteId,
119
- query: searchQuery,
120
- resultCount,
121
- }),
122
- }).catch(() => {
123
- // Silently fail - analytics should not break search
124
- });
112
+ const blob = new Blob(
113
+ [JSON.stringify({ siteId, query: searchQuery, resultCount })],
114
+ { type: "application/json" }
115
+ );
116
+ navigator.sendBeacon(`${backend}/api/analytics/sq`, blob);
125
117
  }, [backend, siteId]);
126
118
 
127
- // Handle close with analytics tracking
128
- const handleClose = useCallback(() => {
129
- trackSearch();
130
- onClose();
131
- }, [onClose, trackSearch]);
119
+ // Ref so cleanup effects always call the latest trackSearch without re-registering
120
+ const trackSearchRef = useRef(trackSearch);
121
+ trackSearchRef.current = trackSearch;
132
122
 
133
- // Handle result selection with analytics tracking
134
- const handleResultSelect = useCallback((url: string) => {
135
- trackSearch();
123
+ const handleResultSelect = useCallback((url: string, searchQuery: string, resultCount: number) => {
124
+ trackSearch(searchQuery, resultCount);
136
125
  window.location.href = url;
137
126
  }, [trackSearch]);
138
127
 
@@ -161,7 +150,7 @@ export function SearchModal({ isOpen, onClose, backend, siteId }: SearchModalPro
161
150
  })
162
151
  );
163
152
  dispatch({ type: "SEARCH_SUCCESS", payload: formattedResults });
164
- lastSearchRef.current = { query: searchQuery, resultCount: searchResults.results.length };
153
+ lastResultCountRef.current = searchResults.results.length;
165
154
  } catch (e) {
166
155
  console.error("Search error:", e);
167
156
  dispatch({ type: "SEARCH_ERROR", payload: "Search failed" });
@@ -175,18 +164,34 @@ export function SearchModal({ isOpen, onClose, backend, siteId }: SearchModalPro
175
164
  }
176
165
  }, [isOpen, pagefindLoaded]);
177
166
 
178
- // Reset state and focus input when modal opens
167
+ // Track analytics when modal closes, reset state when it opens
179
168
  useEffect(() => {
180
169
  if (isOpen) {
181
170
  dispatch({ type: "RESET" });
182
- lastSearchRef.current = null;
183
171
  hasTrackedRef.current = false;
184
- // Small delay to ensure modal is rendered before focusing
172
+ lastResultCountRef.current = 0;
185
173
  const timerId = setTimeout(() => inputRef.current?.focus(), 50);
186
- return () => clearTimeout(timerId);
174
+ return () => {
175
+ clearTimeout(timerId);
176
+ // Fires when isOpen flips to false — track the search session
177
+ trackSearchRef.current(queryRef.current, lastResultCountRef.current);
178
+ };
187
179
  }
188
180
  }, [isOpen]);
189
181
 
182
+ // Global Escape key handler - works regardless of focus
183
+ useEffect(() => {
184
+ if (!isOpen) return;
185
+ const handleEscape = (e: KeyboardEvent) => {
186
+ if (e.key === "Escape") {
187
+ e.preventDefault();
188
+ onClose();
189
+ }
190
+ };
191
+ document.addEventListener("keydown", handleEscape);
192
+ return () => document.removeEventListener("keydown", handleEscape);
193
+ }, [isOpen, onClose]);
194
+
190
195
  // Scroll selected item into view
191
196
  useEffect(() => {
192
197
  if (resultsRef.current && results.length > 0) {
@@ -217,13 +222,9 @@ export function SearchModal({ isOpen, onClose, backend, siteId }: SearchModalPro
217
222
  case "Enter":
218
223
  e.preventDefault();
219
224
  if (results[selectedIndex]) {
220
- handleResultSelect(results[selectedIndex].url);
225
+ handleResultSelect(results[selectedIndex].url, query, lastResultCountRef.current);
221
226
  }
222
227
  break;
223
- case "Escape":
224
- e.preventDefault();
225
- handleClose();
226
- break;
227
228
  }
228
229
  };
229
230
 
@@ -236,18 +237,11 @@ export function SearchModal({ isOpen, onClose, backend, siteId }: SearchModalPro
236
237
  className="fixed inset-0 z-50"
237
238
  role="dialog"
238
239
  aria-modal="true"
239
- onKeyDown={(e) => {
240
- // Global escape handler for clicking outside input
241
- if (e.key === "Escape") {
242
- e.preventDefault();
243
- handleClose();
244
- }
245
- }}
246
240
  >
247
241
  {/* Backdrop */}
248
242
  <div
249
243
  className="absolute inset-0 bg-black/50 backdrop-blur-sm"
250
- onClick={handleClose}
244
+ onClick={onClose}
251
245
  aria-hidden="true"
252
246
  />
253
247
 
@@ -292,7 +286,7 @@ export function SearchModal({ isOpen, onClose, backend, siteId }: SearchModalPro
292
286
  href={result.url}
293
287
  onClick={(e) => {
294
288
  e.preventDefault();
295
- handleResultSelect(result.url);
289
+ handleResultSelect(result.url, query, lastResultCountRef.current);
296
290
  }}
297
291
  className={cn(
298
292
  "block px-4 py-3 transition-colors",
@@ -283,10 +283,17 @@ export function Sidebar({
283
283
  siteName,
284
284
  navigation,
285
285
  logo,
286
- currentPath,
286
+ currentPath: initialPath,
287
287
  githubUrl,
288
288
  }: SidebarProps) {
289
- // No more isDark state or MutationObserver - CSS handles theme switching
289
+ // Track current path locally so it updates after View Transition navigations
290
+ const [currentPath, setCurrentPath] = useState(initialPath);
291
+
292
+ useEffect(() => {
293
+ const updatePath = () => setCurrentPath(window.location.pathname);
294
+ document.addEventListener("astro:after-swap", updatePath);
295
+ return () => document.removeEventListener("astro:after-swap", updatePath);
296
+ }, []);
290
297
 
291
298
  return (
292
299
  <div className="flex flex-col h-full">
@@ -10,6 +10,7 @@ import { Sidebar } from "../components/react/Sidebar";
10
10
  import { PageFooter } from "../components/react/PageFooter";
11
11
  import { PageActions } from "../components/react/PageActions";
12
12
  import { SearchProvider } from "../components/react/SearchProvider";
13
+ import { ViewTransitions } from "astro:transitions";
13
14
  import { loadConfig } from "../lib/config.js";
14
15
  import { generateColorVariants, getPageNavigation } from "../lib/utils";
15
16
  import "../styles/global.css";
@@ -117,23 +118,20 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
117
118
  })} />
118
119
  )}
119
120
 
121
+ <ViewTransitions />
120
122
  <Analytics path={currentPath} />
121
123
 
122
124
  <!-- Theme init script - prevents flash of wrong theme -->
123
125
  <script is:inline>
124
- (function () {
125
- const storedTheme = localStorage.getItem("theme");
126
- if (storedTheme === "dark") {
127
- document.documentElement.classList.add("dark");
128
- } else if (storedTheme === "light") {
129
- document.documentElement.classList.remove("dark");
130
- } else {
131
- const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
132
- if (prefersDark) {
133
- document.documentElement.classList.add("dark");
134
- }
135
- }
136
- })();
126
+ function getThemePreference() {
127
+ const stored = localStorage.getItem("theme");
128
+ if (stored === "dark" || stored === "light") return stored;
129
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
130
+ }
131
+ document.documentElement.classList.toggle("dark", getThemePreference() === "dark");
132
+ document.addEventListener("astro:before-swap", function (e) {
133
+ e.newDocument.documentElement.classList.toggle("dark", getThemePreference() === "dark");
134
+ });
137
135
  </script>
138
136
 
139
137
  <!-- Theme injection from docs.json config -->
@@ -159,6 +157,7 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
159
157
  <aside class="hidden lg:block fixed inset-y-0 left-0 z-40 w-64 border-r border-[var(--color-border)] bg-[var(--color-surface-raised)]">
160
158
  <Sidebar
161
159
  client:load
160
+ transition:persist="desktop-sidebar"
162
161
  siteName={siteName}
163
162
  navigation={navigation}
164
163
  logo={logo}
@@ -182,6 +181,7 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
182
181
  <div class="h-full overflow-y-auto">
183
182
  <Sidebar
184
183
  client:load
184
+ transition:persist="mobile-sidebar"
185
185
  siteName={siteName}
186
186
  navigation={navigation}
187
187
  logo={logo}
@@ -276,7 +276,7 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
276
276
  </div>
277
277
 
278
278
  <!-- Search modal -->
279
- <SearchProvider client:load backend={backend} siteId={siteId} />
279
+ <SearchProvider client:load transition:persist="search" backend={backend} siteId={siteId} />
280
280
 
281
281
  <!-- Mobile sidebar toggle script -->
282
282
  <script src="../scripts/mobile-sidebar.ts"></script>