mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-08 22:47:02 +02:00
feat(kinds): add search box to KINDS command (#206)
Add a search input to KindsViewer matching the style from NipsViewer. Users can now filter kinds by number, name, or description. Supports autofocus on mount, clear button, and Escape to clear. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { getKindInfo } from "@/constants/kinds";
|
||||
import { kindRenderers } from "./nostr/kinds";
|
||||
import { NIPBadge } from "./NIPBadge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CenteredContent } from "./ui/CenteredContent";
|
||||
|
||||
// Dynamically derive supported kinds from renderer registry
|
||||
@@ -11,62 +15,135 @@ const SUPPORTED_KINDS = Object.keys(kindRenderers).map(Number);
|
||||
* Shows all event kinds with rich rendering support
|
||||
*/
|
||||
export default function KindsViewer() {
|
||||
const [search, setSearch] = useState("");
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Autofocus on mount
|
||||
useEffect(() => {
|
||||
searchInputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Sort kinds in ascending order
|
||||
const sortedKinds = [...SUPPORTED_KINDS].sort((a, b) => a - b);
|
||||
|
||||
// Filter kinds by search term (matches kind number or name)
|
||||
const filteredKinds = search
|
||||
? sortedKinds.filter((kind) => {
|
||||
const kindInfo = getKindInfo(kind);
|
||||
const name = kindInfo?.name || "";
|
||||
const description = kindInfo?.description || "";
|
||||
const searchLower = search.toLowerCase();
|
||||
return (
|
||||
kind.toString().includes(search) ||
|
||||
name.toLowerCase().includes(searchLower) ||
|
||||
description.toLowerCase().includes(searchLower)
|
||||
);
|
||||
})
|
||||
: sortedKinds;
|
||||
|
||||
// Clear search
|
||||
const handleClear = () => {
|
||||
setSearch("");
|
||||
searchInputRef.current?.focus();
|
||||
};
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Escape") {
|
||||
handleClear();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-2">
|
||||
Supported Event Kinds ({sortedKinds.length})
|
||||
{search
|
||||
? `Showing ${filteredKinds.length} of ${sortedKinds.length} Kinds`
|
||||
: `Supported Event Kinds (${sortedKinds.length})`}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Event kinds with rich rendering support in Grimoire. Default kinds
|
||||
display raw content only.
|
||||
</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 kinds by number or name..."
|
||||
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>
|
||||
|
||||
{/* Kind List */}
|
||||
<div className="border border-border divide-y divide-border">
|
||||
{sortedKinds.map((kind) => {
|
||||
const kindInfo = getKindInfo(kind);
|
||||
const Icon = kindInfo?.icon;
|
||||
{filteredKinds.length > 0 ? (
|
||||
<div className="border border-border divide-y divide-border">
|
||||
{filteredKinds.map((kind) => {
|
||||
const kindInfo = getKindInfo(kind);
|
||||
const Icon = kindInfo?.icon;
|
||||
|
||||
return (
|
||||
<div key={kind} className="p-4 hover:bg-muted/30 transition-colors">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Icon */}
|
||||
<div className="w-10 h-10 bg-accent/20 rounded flex items-center justify-center flex-shrink-0">
|
||||
{Icon ? (
|
||||
<Icon className="w-5 h-5 text-accent" />
|
||||
) : (
|
||||
<span className="text-xs font-mono text-muted-foreground">
|
||||
{kind}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<code className="text-sm font-mono font-semibold">
|
||||
{kind}
|
||||
</code>
|
||||
<span className="text-sm font-semibold">
|
||||
{kindInfo?.name || `Kind ${kind}`}
|
||||
</span>
|
||||
return (
|
||||
<div
|
||||
key={kind}
|
||||
className="p-4 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Icon */}
|
||||
<div className="w-10 h-10 bg-accent/20 rounded flex items-center justify-center flex-shrink-0">
|
||||
{Icon ? (
|
||||
<Icon className="w-5 h-5 text-accent" />
|
||||
) : (
|
||||
<span className="text-xs font-mono text-muted-foreground">
|
||||
{kind}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<code className="text-sm font-mono font-semibold">
|
||||
{kind}
|
||||
</code>
|
||||
<span className="text-sm font-semibold">
|
||||
{kindInfo?.name || `Kind ${kind}`}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
{kindInfo?.description || "No description available"}
|
||||
</p>
|
||||
{kindInfo?.nip && <NIPBadge nipNumber={kindInfo.nip} />}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
{kindInfo?.description || "No description available"}
|
||||
</p>
|
||||
{kindInfo?.nip && <NIPBadge nipNumber={kindInfo.nip} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<p className="text-lg mb-2">No kinds match "{search}"</p>
|
||||
<p className="text-sm">Try searching for a different term</p>
|
||||
</div>
|
||||
)}
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user