@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
|
@@ -30,28 +30,23 @@ const shouldTrack = backend && siteId && analyticsEnabled;
|
|
|
30
30
|
is:inline
|
|
31
31
|
>
|
|
32
32
|
(function() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
//
|
|
104
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
//
|
|
128
|
-
const
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
172
|
+
lastResultCountRef.current = 0;
|
|
185
173
|
const timerId = setTimeout(() => inputRef.current?.focus(), 50);
|
|
186
|
-
return () =>
|
|
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={
|
|
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
|
-
//
|
|
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
|
-
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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>
|