mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 15:36:53 +02:00
128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
import { useState, useRef, useEffect } from "react";
|
|
import { Search, X } from "lucide-react";
|
|
import { VALID_NIPS, NIP_TITLES } from "@/constants/nips";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { NIPBadge } from "./NIPBadge";
|
|
import { useGrimoire } from "@/core/state";
|
|
import { CenteredContent } from "./ui/CenteredContent";
|
|
|
|
/**
|
|
* NipsViewer - Documentation introspection command
|
|
* Shows all Nostr Implementation Possibilities (NIPs)
|
|
*/
|
|
export default function NipsViewer() {
|
|
const [search, setSearch] = useState("");
|
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
const { addWindow } = useGrimoire();
|
|
|
|
// Autofocus on mount
|
|
useEffect(() => {
|
|
searchInputRef.current?.focus();
|
|
}, []);
|
|
|
|
// Sort NIPs: numeric first (01-99), then hex (7D, A0, etc.)
|
|
const sortedNips = [...VALID_NIPS].sort((a, b) => {
|
|
const aIsHex = /^[A-F]/.test(a);
|
|
const bIsHex = /^[A-F]/.test(b);
|
|
|
|
// If both are hex or both are numeric, sort alphabetically
|
|
if (aIsHex === bIsHex) {
|
|
return a.localeCompare(b);
|
|
}
|
|
|
|
// Numeric before hex
|
|
return aIsHex ? 1 : -1;
|
|
});
|
|
|
|
// Filter NIPs by search term (matches NIP number or title)
|
|
const filteredNips = search
|
|
? sortedNips.filter((nipId) => {
|
|
const title = NIP_TITLES[nipId] || "";
|
|
const searchLower = search.toLowerCase();
|
|
return (
|
|
nipId.toLowerCase().includes(searchLower) ||
|
|
title.toLowerCase().includes(searchLower)
|
|
);
|
|
})
|
|
: sortedNips;
|
|
|
|
// Clear search
|
|
const handleClear = () => {
|
|
setSearch("");
|
|
searchInputRef.current?.focus();
|
|
};
|
|
|
|
// Handle keyboard shortcuts
|
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
if (e.key === "Escape") {
|
|
handleClear();
|
|
} else if (e.key === "Enter" && filteredNips.length === 1) {
|
|
// Open the single result when Enter is pressed
|
|
const nipId = filteredNips[0];
|
|
addWindow("nip", { number: nipId });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<CenteredContent>
|
|
{/* Header */}
|
|
<div>
|
|
<h1 className="text-2xl font-bold mb-2">
|
|
{search
|
|
? `Showing ${filteredNips.length} of ${sortedNips.length} NIPs`
|
|
: `Nostr Implementation Possibilities (${sortedNips.length})`}
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
Protocol specifications and extensions for the Nostr network. Click
|
|
any NIP to view its full specification document.
|
|
</p>
|
|
|
|
{/* Search Input */}
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
|
<Input
|
|
ref={searchInputRef}
|
|
type="text"
|
|
placeholder="Search NIPs by number or title..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
className="pl-9 pr-9"
|
|
/>
|
|
{search && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleClear}
|
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0 hover:bg-muted"
|
|
aria-label="Clear search"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* NIP List */}
|
|
{filteredNips.length > 0 ? (
|
|
<div className="flex flex-col gap-0">
|
|
{filteredNips.map((nipId) => (
|
|
<NIPBadge
|
|
className="border-none"
|
|
key={nipId}
|
|
showName
|
|
nipNumber={nipId}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<p className="text-lg mb-2">No NIPs match "{search}"</p>
|
|
<p className="text-sm">Try searching for a different term</p>
|
|
</div>
|
|
)}
|
|
</CenteredContent>
|
|
);
|
|
}
|