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,6 +1,6 @@
1
1
  {
2
2
  "name": "seemore",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Let AI write the Markdown. Let seemore show it better — zero config documentation framework.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- const title = results[0]?.content ?? '';
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
- const items = results.map((result) => ({ ...result, external: false }));
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
- <SearchDialogInput
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 === undefined ? (
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
  }