seemore 1.5.0 → 1.5.1
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,4 +1,4 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { useNavigate } from 'react-router';
|
|
3
3
|
import { useDocsSearch } from 'fumadocs-core/search/client';
|
|
4
4
|
import {
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
SearchDialogContent,
|
|
8
8
|
SearchDialogHeader,
|
|
9
9
|
SearchDialogIcon,
|
|
10
|
-
SearchDialogInput,
|
|
11
10
|
SearchDialogList,
|
|
12
11
|
SearchDialogOverlay,
|
|
13
12
|
} from 'fumadocs-ui/components/dialog/search';
|
|
@@ -44,14 +43,35 @@ export function SearchDialog(props: SharedProps) {
|
|
|
44
43
|
const { search, setSearch, query } = useDocsSearch({ client });
|
|
45
44
|
const results = query.data === 'empty' || query.data === undefined ? [] : query.data;
|
|
46
45
|
|
|
46
|
+
/**
|
|
47
|
+
* A long index is parsed on the first query, so the wait is seconds rather than
|
|
48
|
+
* milliseconds — long enough that the list's "No results found" reads as an answer.
|
|
49
|
+
*
|
|
50
|
+
* `query.isLoading` alone is not the whole wait: fumadocs debounces the input before it
|
|
51
|
+
* flips, so a keystroke's worth of that wrong answer shows first. A query is pending from
|
|
52
|
+
* the keystroke until the search settles.
|
|
53
|
+
*/
|
|
54
|
+
const [pending, setPending] = useState(false);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
setPending(search !== '');
|
|
57
|
+
}, [search]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!query.isLoading) setPending(false);
|
|
60
|
+
}, [query.isLoading]);
|
|
61
|
+
|
|
47
62
|
// `search.suggest`: complete the last word inline from the best result's title.
|
|
48
63
|
const completion = useMemo(() => {
|
|
49
64
|
if (!feature('search.suggest') || search === '' || results.length === 0) return '';
|
|
50
|
-
|
|
65
|
+
// A result's content arrives with the matched span wrapped in `<mark>` — and the match
|
|
66
|
+
// is the query itself, so leaving the markup in place is a prefix test that can never
|
|
67
|
+
// pass.
|
|
68
|
+
const title = (results[0]?.content ?? '').replaceAll('<mark>', '').replaceAll('</mark>', '');
|
|
51
69
|
return title.toLowerCase().startsWith(search.toLowerCase()) ? title.slice(search.length) : '';
|
|
52
70
|
}, [results, search]);
|
|
53
71
|
|
|
54
|
-
|
|
72
|
+
// An empty box has nothing to answer: `null` collapses the list, where an empty array
|
|
73
|
+
// would answer "No results found".
|
|
74
|
+
const items = search === '' ? null : results.map((result) => ({ ...result, external: false }));
|
|
55
75
|
|
|
56
76
|
return (
|
|
57
77
|
<Dialog
|
|
@@ -71,7 +91,15 @@ export function SearchDialog(props: SharedProps) {
|
|
|
71
91
|
<SearchDialogContent>
|
|
72
92
|
<SearchDialogHeader>
|
|
73
93
|
<SearchDialogIcon />
|
|
74
|
-
|
|
94
|
+
{/*
|
|
95
|
+
Our own input rather than fumadocs' `SearchDialogInput`: that one overwrites
|
|
96
|
+
`placeholder` with its own translated "Search" after spreading props, so the
|
|
97
|
+
wording here would never reach the box. The state is already ours.
|
|
98
|
+
*/}
|
|
99
|
+
<input
|
|
100
|
+
className="seemore-search-input"
|
|
101
|
+
value={search}
|
|
102
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
75
103
|
placeholder="Search documentation…"
|
|
76
104
|
onKeyDown={(event) => {
|
|
77
105
|
if (event.key !== 'ArrowRight' || completion === '') return;
|
|
@@ -87,13 +115,20 @@ export function SearchDialog(props: SharedProps) {
|
|
|
87
115
|
<span>{completion}</span>
|
|
88
116
|
</p>
|
|
89
117
|
)}
|
|
90
|
-
{query.error
|
|
91
|
-
<SearchDialogList items={items} />
|
|
92
|
-
) : (
|
|
118
|
+
{query.error !== undefined ? (
|
|
93
119
|
// A search box that silently finds nothing is worse than one that says why.
|
|
94
120
|
<p className="seemore-search-error" role="alert">
|
|
95
121
|
{query.error.message}
|
|
96
122
|
</p>
|
|
123
|
+
) : pending && (items?.length ?? 0) === 0 ? (
|
|
124
|
+
// Only with nothing to show: results already on screen stay put while the next
|
|
125
|
+
// query runs, under the input icon's own pulse.
|
|
126
|
+
<div className="seemore-search-loading" role="status">
|
|
127
|
+
<span className="seemore-search-spinner" aria-hidden="true" />
|
|
128
|
+
Searching…
|
|
129
|
+
</div>
|
|
130
|
+
) : (
|
|
131
|
+
<SearchDialogList items={items} />
|
|
97
132
|
)}
|
|
98
133
|
</SearchDialogContent>
|
|
99
134
|
</Dialog>
|
|
@@ -68,6 +68,18 @@
|
|
|
68
68
|
@apply opacity-60;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
.seemore-search-input {
|
|
72
|
+
@apply w-0 flex-1 bg-transparent text-lg placeholder:text-fd-muted-foreground focus-visible:outline-none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.seemore-search-loading {
|
|
76
|
+
@apply flex items-center justify-center gap-2 px-4 py-6 text-sm text-fd-muted-foreground;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.seemore-search-spinner {
|
|
80
|
+
@apply h-4 w-4 animate-spin rounded-full border-2 border-fd-muted-foreground/30 border-t-fd-muted-foreground;
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
.seemore-theme-toggle {
|
|
72
84
|
@apply rounded-lg border border-fd-border p-2;
|
|
73
85
|
}
|