astro-tractstack 2.0.0-rc.45 → 2.0.0-rc.47

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": "astro-tractstack",
3
- "version": "2.0.0-rc.45",
3
+ "version": "2.0.0-rc.47",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -189,7 +189,7 @@ export default function SearchModal({
189
189
  {selectedTerms.map((term, index) => (
190
190
  <div
191
191
  key={index}
192
- className="flex items-center gap-1 rounded-full border border-blue-200 bg-blue-100 px-3 py-1 text-sm font-medium text-blue-800"
192
+ className="flex items-center gap-1 rounded-full border border-blue-200 bg-blue-100 px-3 py-1 text-sm font-bold text-blue-800"
193
193
  >
194
194
  <span>{term}</span>
195
195
  <button
@@ -205,26 +205,25 @@ export default function SearchModal({
205
205
  )}
206
206
 
207
207
  {!showResults && (
208
- <div className="relative">
209
- <div className="relative w-full border-none bg-transparent px-6 py-2 text-xl">
210
- {/* Background layer with completion text */}
211
- {showCompletion && (
212
- <div className="pointer-events-none absolute inset-0 px-6 py-2 text-xl text-gray-400">
213
- {bestCompletion}
214
- </div>
215
- )}
216
- {/* Foreground input */}
217
- <input
218
- ref={inputRef}
219
- type="text"
220
- value={query}
221
- onChange={handleInputChange}
222
- onKeyDown={handleKeyDown}
223
- placeholder="Search content..."
224
- className="text-mydarkgrey relative z-10 w-full border-none bg-transparent text-xl placeholder-gray-500 outline-none"
225
- style={{ background: 'transparent' }}
226
- />
227
- </div>
208
+ <div className="relative w-full px-6 py-2">
209
+ {showCompletion && (
210
+ <div className="pointer-events-none absolute left-0 top-0 flex h-full w-full items-center px-6 py-2 text-xl text-gray-400">
211
+ <span style={{ visibility: 'hidden' }}>{query}</span>
212
+ {bestCompletion
213
+ .slice(query.length)
214
+ .replace(/ /g, '\u00A0')}
215
+ </div>
216
+ )}
217
+ <input
218
+ ref={inputRef}
219
+ type="text"
220
+ value={query}
221
+ onChange={handleInputChange}
222
+ onKeyDown={handleKeyDown}
223
+ placeholder="Search content..."
224
+ className="text-mydarkgrey relative z-10 w-full border-none bg-transparent text-xl placeholder-gray-500 outline-none"
225
+ style={{ background: 'transparent', padding: '0' }}
226
+ />
228
227
  </div>
229
228
  )}
230
229
  <button
@@ -286,7 +285,7 @@ export default function SearchModal({
286
285
  {/* Suggestion Pills */}
287
286
  {showSuggestions && (
288
287
  <div className="w-full p-6">
289
- <p className="text-mydarkgrey mb-4 text-sm font-medium">
288
+ <p className="text-mydarkgrey mb-4 text-sm font-bold">
290
289
  Suggestions ({suggestions.length})
291
290
  </p>
292
291
  <div className="flex flex-wrap gap-2">
@@ -294,7 +293,7 @@ export default function SearchModal({
294
293
  <button
295
294
  key={index}
296
295
  onClick={() => handleSuggestionClick(suggestion)}
297
- className={`inline-flex items-center rounded-full border px-3 py-1.5 text-sm font-medium transition-all hover:shadow-md ${getTypeColor(suggestion.type)}`}
296
+ className={`inline-flex items-center rounded-full border px-3 py-1.5 text-sm font-bold transition-all hover:shadow-md ${getTypeColor(suggestion.type)}`}
298
297
  >
299
298
  <span>{suggestion.term}</span>
300
299
  </button>
@@ -342,7 +341,6 @@ export default function SearchModal({
342
341
  <SearchResults
343
342
  results={searchResults}
344
343
  contentMap={contentMap}
345
- onResultClick={handleClose}
346
344
  />
347
345
  )}
348
346
  </div>
@@ -14,7 +14,6 @@ const VERBOSE = false;
14
14
  interface SearchResultsProps {
15
15
  results: CategorizedResults;
16
16
  contentMap: FullContentMapItem[];
17
- onResultClick: () => void;
18
17
  }
19
18
 
20
19
  interface ResultItem {
@@ -36,7 +35,6 @@ const ITEMS_PER_PAGE = 10;
36
35
  export default function SearchResults({
37
36
  results,
38
37
  contentMap,
39
- onResultClick,
40
38
  }: SearchResultsProps) {
41
39
  const [currentPage, setCurrentPage] = useState(1);
42
40
 
@@ -238,7 +236,7 @@ export default function SearchResults({
238
236
  key={item.id}
239
237
  className="rounded-lg border border-gray-200 p-4 transition-colors hover:bg-gray-100"
240
238
  >
241
- <a href={item.url} onClick={onResultClick} className="group block">
239
+ <a href={item.url} className="group block">
242
240
  <div className="flex flex-col md:flex-row md:items-start md:gap-4">
243
241
  {/* Mobile: Full width image with overlay badge */}
244
242
  <div
@@ -23,7 +23,7 @@ interface UseSearchReturn {
23
23
  clearAll: () => void;
24
24
  }
25
25
 
26
- const DEBOUNCE_MS = 100;
26
+ const DEBOUNCE_MS = 150;
27
27
  const BACKEND_THROTTLE_MS = 1200;
28
28
 
29
29
  export function useSearch(): UseSearchReturn {
@@ -39,10 +39,11 @@ export function useSearch(): UseSearchReturn {
39
39
  const [isRetrieving, setIsRetrieving] = useState(false);
40
40
  const [retrieveError, setRetrieveError] = useState<string | null>(null);
41
41
 
42
- const debounceRef = useRef<NodeJS.Timeout>();
43
- const lastSearchTimeRef = useRef<number>(0);
42
+ // --- REVISED STATE FOR SEARCH LOGIC ---
43
+ const searchTimerRef = useRef<NodeJS.Timeout>();
44
+ const lastExecutionTimeRef = useRef<number>(0);
44
45
  const pendingQueryRef = useRef<string | null>(null);
45
- const throttleTimeoutRef = useRef<NodeJS.Timeout>();
46
+ const inflightQueryRef = useRef<string | null>(null);
46
47
  const api = useMemo(() => new TractStackAPI(), []);
47
48
 
48
49
  const performDiscovery = useCallback(
@@ -52,26 +53,37 @@ export function useSearch(): UseSearchReturn {
52
53
  return;
53
54
  }
54
55
 
56
+ if (inflightQueryRef.current === query.trim()) {
57
+ return;
58
+ }
59
+
60
+ inflightQueryRef.current = query.trim();
55
61
  setIsDiscovering(true);
56
62
  setDiscoverError(null);
57
- lastSearchTimeRef.current = Date.now();
58
63
 
59
64
  try {
60
65
  const response = await api.discover(query.trim());
61
66
 
62
- if (response.success && response.data) {
63
- setSuggestions(response.data.suggestions);
64
- } else {
65
- setDiscoverError(response.error || 'Discovery failed');
66
- setSuggestions([]);
67
+ if (inflightQueryRef.current === query.trim()) {
68
+ if (response.success && response.data) {
69
+ setSuggestions(response.data.suggestions);
70
+ } else {
71
+ setDiscoverError(response.error || 'Discovery failed');
72
+ setSuggestions([]);
73
+ }
67
74
  }
68
75
  } catch (err) {
69
- setDiscoverError(
70
- err instanceof Error ? err.message : 'Discovery failed'
71
- );
72
- setSuggestions([]);
76
+ if (inflightQueryRef.current === query.trim()) {
77
+ setDiscoverError(
78
+ err instanceof Error ? err.message : 'Discovery failed'
79
+ );
80
+ setSuggestions([]);
81
+ }
73
82
  } finally {
74
- setIsDiscovering(false);
83
+ if (inflightQueryRef.current === query.trim()) {
84
+ inflightQueryRef.current = null;
85
+ setIsDiscovering(false);
86
+ }
75
87
  }
76
88
  },
77
89
  [api]
@@ -103,24 +115,11 @@ export function useSearch(): UseSearchReturn {
103
115
  [api]
104
116
  );
105
117
 
106
- const executePendingSearch = useCallback(() => {
107
- if (pendingQueryRef.current) {
108
- const queryToExecute = pendingQueryRef.current;
109
- pendingQueryRef.current = null;
110
- performDiscovery(queryToExecute);
111
- }
112
- }, [performDiscovery]);
113
-
114
118
  const discoverTerms = useCallback(
115
119
  (query: string) => {
116
- // Clear existing debounce timer
117
- if (debounceRef.current) {
118
- clearTimeout(debounceRef.current);
119
- }
120
-
121
- // Clear existing throttle timer
122
- if (throttleTimeoutRef.current) {
123
- clearTimeout(throttleTimeoutRef.current);
120
+ // Clear any existing timer.
121
+ if (searchTimerRef.current) {
122
+ clearTimeout(searchTimerRef.current);
124
123
  }
125
124
 
126
125
  // Clear results when starting new discovery
@@ -133,31 +132,37 @@ export function useSearch(): UseSearchReturn {
133
132
  setDiscoverError(null);
134
133
  setIsDiscovering(false);
135
134
  pendingQueryRef.current = null;
135
+ inflightQueryRef.current = null;
136
136
  return;
137
137
  }
138
138
 
139
- // Always store the latest query
139
+ // Always store the latest query for the next execution
140
140
  pendingQueryRef.current = query;
141
141
 
142
- // Debounce first - wait for user to stop typing
143
- debounceRef.current = setTimeout(() => {
144
- const now = Date.now();
145
- const timeSinceLastSearch = now - lastSearchTimeRef.current;
142
+ const now = Date.now();
143
+ const timeSinceLastSearch = now - lastExecutionTimeRef.current;
146
144
 
147
- // If enough time has passed since last search, execute immediately
148
- if (timeSinceLastSearch >= BACKEND_THROTTLE_MS) {
149
- executePendingSearch();
150
- } else {
151
- // Need to wait for throttle - schedule execution
152
- const remainingTime = BACKEND_THROTTLE_MS - timeSinceLastSearch;
153
- throttleTimeoutRef.current = setTimeout(
154
- executePendingSearch,
155
- remainingTime
156
- );
145
+ // Start with the basic debounce delay
146
+ let delay = DEBOUNCE_MS;
147
+
148
+ // If we are inside the throttle window, we must wait longer
149
+ if (timeSinceLastSearch < BACKEND_THROTTLE_MS) {
150
+ const remainingThrottle = BACKEND_THROTTLE_MS - timeSinceLastSearch;
151
+ delay = Math.max(delay, remainingThrottle);
152
+ }
153
+
154
+ searchTimerRef.current = setTimeout(() => {
155
+ // Double check there's a query to run
156
+ if (pendingQueryRef.current !== null) {
157
+ const queryToExecute = pendingQueryRef.current;
158
+
159
+ // Update execution time as soon as the search is initiated
160
+ lastExecutionTimeRef.current = Date.now();
161
+ performDiscovery(queryToExecute);
157
162
  }
158
- }, DEBOUNCE_MS);
163
+ }, delay);
159
164
  },
160
- [executePendingSearch]
165
+ [performDiscovery]
161
166
  );
162
167
 
163
168
  const selectSuggestion = useCallback(
@@ -191,12 +196,9 @@ export function useSearch(): UseSearchReturn {
191
196
  );
192
197
 
193
198
  const clearAll = useCallback(() => {
194
- // Clear all timeouts
195
- if (debounceRef.current) {
196
- clearTimeout(debounceRef.current);
197
- }
198
- if (throttleTimeoutRef.current) {
199
- clearTimeout(throttleTimeoutRef.current);
199
+ // Clear the main search timer
200
+ if (searchTimerRef.current) {
201
+ clearTimeout(searchTimerRef.current);
200
202
  }
201
203
 
202
204
  // Reset all state
@@ -207,6 +209,8 @@ export function useSearch(): UseSearchReturn {
207
209
  setIsRetrieving(false);
208
210
  setRetrieveError(null);
209
211
  pendingQueryRef.current = null;
212
+ inflightQueryRef.current = null;
213
+ lastExecutionTimeRef.current = 0; // Reset throttle timer
210
214
  }, []);
211
215
 
212
216
  return {
@@ -273,8 +273,8 @@ const enableBunny = import.meta.env.PUBLIC_ENABLE_BUNNY === 'true';
273
273
  }
274
274
  <div
275
275
  id="loading-indicator"
276
- class="bg-myorange fixed left-0 top-0 z-50 h-1 w-full scale-x-0 transform transition-transform duration-300 ease-out"
277
- style="opacity: 0.5; filter: blur(0.5px);"
276
+ class="bg-myorange fixed left-0 top-0 h-1 w-full scale-x-0 transform transition-transform duration-300 ease-out"
277
+ style="opacity: 0.5; filter: blur(0.5px); z-index: 10070;"
278
278
  >
279
279
  </div>
280
280