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,6 +1,6 @@
1
1
  {
2
2
  "name": "seemore",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
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
@@ -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
- <SearchDialogOverlay />
71
- <SearchDialogContent>
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
- <SearchDialogInput
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 === undefined ? (
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. */