Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 1x 242x 242x 6x | "use client";
/**
* Search input, progress, and match navigation (SPEC.md §3.4).
*
* The `capped` notice is not decoration: a scan that stopped at 10 000 matches
* has produced an *incomplete* answer, and showing "1–10 of 10 000" as if it
* were the whole story is the kind of silent truncation STANDALONE-IDEAS calls
* out by name.
*/
import styles from "./styles/JsonViewer.module.scss";
import type { JsonSearchProgress } from "./types";
export interface SearchBarProps {
query: string;
onQueryChange: (query: string) => void;
matchCount: number;
currentMatch: number;
progress: JsonSearchProgress | null;
/**
* True from the keystroke until the scan for *this* query has finished.
*
* Not derived from `progress`: between the keystroke and the debounced start
* there is no progress object yet, and rendering "No matches" in that gap
* tells the user something false about a query that has plenty.
*/
scanning: boolean;
error: string | null;
onPrevious: () => void;
onNext: () => void;
}
export function SearchBar({
query,
onQueryChange,
matchCount,
currentMatch,
progress,
scanning,
error,
onPrevious,
onNext,
}: SearchBarProps) {
const hasMatches = matchCount > 0;
return (
<div className={styles.search}>
<input
type="search"
className={styles.input}
value={query}
placeholder="Search keys and values…"
aria-label="Search keys and values"
data-testid="json-search-input"
onChange={(event) => onQueryChange(event.target.value)}
onKeyDown={(event) => {
if (event.key !== "Enter") return;
event.preventDefault();
if (event.shiftKey) onPrevious();
else onNext();
}}
/>
{error ? (
<span className={styles.notice} data-testid="json-search-error">
{error}
</span>
) : scanning ? (
<span className={styles.progress} data-testid="json-search-scanning">
{progress ? `Scanning… ${progress.scanned.toLocaleString()}` : "Scanning…"}
</span>
) : query ? (
<span className={styles.progress} data-testid="json-search-count">
{hasMatches ? `${currentMatch + 1} of ${matchCount.toLocaleString()}` : "No matches"}
</span>
) : null}
{progress?.capped ? (
<span className={styles.notice} data-testid="json-search-capped">
{`first ${matchCount.toLocaleString()} only`}
</span>
) : null}
<button
type="button"
className={styles.button}
onClick={onPrevious}
disabled={!hasMatches}
aria-label="Previous match"
>
{"↑"}
</button>
<button
type="button"
className={styles.button}
onClick={onNext}
disabled={!hasMatches}
aria-label="Next match"
>
{"↓"}
</button>
</div>
);
}
|