seemore 1.5.0 → 1.5.2
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
|
|
@@ -67,11 +87,22 @@ export function SearchDialog(props: SharedProps) {
|
|
|
67
87
|
void navigate(withHighlight(stripBase(config.base, item.url), search), { viewTransition: true });
|
|
68
88
|
}}
|
|
69
89
|
>
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
{/* The sticky header is `z-50` too, and it renders after this portal in the DOM — at
|
|
91
|
+
mobile's `top-4` it wins the tie and paints over the input row. fumadocs' own header
|
|
92
|
+
is `z-30`, so theirs never meets this; ours needs the dialog lifted above it. */}
|
|
93
|
+
<SearchDialogOverlay className="z-[60]" />
|
|
94
|
+
<SearchDialogContent className="z-[60]">
|
|
72
95
|
<SearchDialogHeader>
|
|
73
96
|
<SearchDialogIcon />
|
|
74
|
-
|
|
97
|
+
{/*
|
|
98
|
+
Our own input rather than fumadocs' `SearchDialogInput`: that one overwrites
|
|
99
|
+
`placeholder` with its own translated "Search" after spreading props, so the
|
|
100
|
+
wording here would never reach the box. The state is already ours.
|
|
101
|
+
*/}
|
|
102
|
+
<input
|
|
103
|
+
className="seemore-search-input"
|
|
104
|
+
value={search}
|
|
105
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
75
106
|
placeholder="Search documentation…"
|
|
76
107
|
onKeyDown={(event) => {
|
|
77
108
|
if (event.key !== 'ArrowRight' || completion === '') return;
|
|
@@ -87,13 +118,20 @@ export function SearchDialog(props: SharedProps) {
|
|
|
87
118
|
<span>{completion}</span>
|
|
88
119
|
</p>
|
|
89
120
|
)}
|
|
90
|
-
{query.error
|
|
91
|
-
<SearchDialogList items={items} />
|
|
92
|
-
) : (
|
|
121
|
+
{query.error !== undefined ? (
|
|
93
122
|
// A search box that silently finds nothing is worse than one that says why.
|
|
94
123
|
<p className="seemore-search-error" role="alert">
|
|
95
124
|
{query.error.message}
|
|
96
125
|
</p>
|
|
126
|
+
) : pending && (items?.length ?? 0) === 0 ? (
|
|
127
|
+
// Only with nothing to show: results already on screen stay put while the next
|
|
128
|
+
// query runs, under the input icon's own pulse.
|
|
129
|
+
<div className="seemore-search-loading" role="status">
|
|
130
|
+
<span className="seemore-search-spinner" aria-hidden="true" />
|
|
131
|
+
Searching…
|
|
132
|
+
</div>
|
|
133
|
+
) : (
|
|
134
|
+
<SearchDialogList items={items} />
|
|
97
135
|
)}
|
|
98
136
|
</SearchDialogContent>
|
|
99
137
|
</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
|
}
|
|
@@ -386,5 +398,16 @@
|
|
|
386
398
|
}
|
|
387
399
|
}
|
|
388
400
|
|
|
401
|
+
/* Unlayered, deliberately: fumadocs' `max-h-[460px]` utility lives in Tailwind's utilities
|
|
402
|
+
layer, which beats anything in `@layer components` regardless of specificity. fumadocs
|
|
403
|
+
caps the results list at 460px, but the dialog opens at `top-4` plus a ~3.5rem input row,
|
|
404
|
+
so on a short (landscape phone) viewport the list runs past the fold with its bottom
|
|
405
|
+
unreachable. Bind it to the real viewport height there instead. */
|
|
406
|
+
@media (max-height: 600px) {
|
|
407
|
+
[role='dialog'] [class*='max-h-[460px]'] {
|
|
408
|
+
max-height: calc(100dvh - 6rem);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
389
412
|
/* seemore:user-css — the stylesheet named by `css` in seemore.config.ts is inlined here, at
|
|
390
413
|
the very end, so that it wins against everything above it. */
|