mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 06:57:07 +02:00
feat: NIPS command
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { getNIPInfo } from "../lib/nip-icons";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { isNipDeprecated } from "@/constants/nips";
|
||||
|
||||
export interface NIPBadgeProps {
|
||||
nipNumber: string;
|
||||
@@ -23,6 +24,7 @@ export function NIPBadge({
|
||||
const name = nipInfo?.name || `NIP-${nipNumber}`;
|
||||
const description =
|
||||
nipInfo?.description || `Nostr Implementation Possibility ${nipNumber}`;
|
||||
const isDeprecated = isNipDeprecated(nipNumber);
|
||||
|
||||
const openNIP = () => {
|
||||
const paddedNum = nipNumber.toString().padStart(2, "0");
|
||||
@@ -36,8 +38,10 @@ export function NIPBadge({
|
||||
return (
|
||||
<button
|
||||
onClick={openNIP}
|
||||
className={`flex items-center gap-2 border bg-card px-2.5 py-1.5 text-sm hover:underline hover:decoration-dotted cursor-crosshair ${className}`}
|
||||
title={description}
|
||||
className={`flex items-center gap-2 border bg-card px-2.5 py-1.5 text-sm hover:underline hover:decoration-dotted cursor-crosshair ${
|
||||
isDeprecated ? "opacity-50" : ""
|
||||
} ${className}`}
|
||||
title={isDeprecated ? `${description} (DEPRECATED)` : description}
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{`${showNIPPrefix ? "NIP-" : ""}${nipNumber}`}
|
||||
|
||||
129
src/components/NipsViewer.tsx
Normal file
129
src/components/NipsViewer.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
* 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];
|
||||
const title = NIP_TITLES[nipId] || `NIP-${nipId}`;
|
||||
addWindow("nip", { number: nipId }, `NIP-${nipId}: ${title}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-y-auto p-6">
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
{/* 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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import DecodeViewer from "./DecodeViewer";
|
||||
import { RelayViewer } from "./RelayViewer";
|
||||
import KindRenderer from "./KindRenderer";
|
||||
import KindsViewer from "./KindsViewer";
|
||||
import NipsViewer from "./NipsViewer";
|
||||
import { DebugViewer } from "./DebugViewer";
|
||||
import ConnViewer from "./ConnViewer";
|
||||
|
||||
@@ -98,6 +99,9 @@ export function WindowRenderer({ window, onClose }: WindowRendererProps) {
|
||||
case "kinds":
|
||||
content = <KindsViewer />;
|
||||
break;
|
||||
case "nips":
|
||||
content = <NipsViewer />;
|
||||
break;
|
||||
case "man":
|
||||
content = <ManPage cmd={window.props.cmd} />;
|
||||
break;
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
* List of valid NIPs from https://github.com/nostr-protocol/nips
|
||||
* Includes both numeric (01-99) and hexadecimal (7D, A0, etc.) identifiers
|
||||
*/
|
||||
/**
|
||||
* Deprecated NIPs that are no longer recommended for use
|
||||
*/
|
||||
export const DEPRECATED_NIPS = ["04", "08", "26", "96"] as const;
|
||||
|
||||
export const VALID_NIPS = [
|
||||
// Numeric NIPs
|
||||
"01",
|
||||
@@ -201,3 +206,7 @@ export function getNipUrl(nipId: string): string {
|
||||
export function getNipTitle(nipId: string): string {
|
||||
return NIP_TITLES[nipId] || `NIP-${nipId}`;
|
||||
}
|
||||
|
||||
export function isNipDeprecated(nipId: string): boolean {
|
||||
return DEPRECATED_NIPS.includes(nipId as any);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
Compass,
|
||||
Gamepad2,
|
||||
type LucideIcon,
|
||||
Signature,
|
||||
} from "lucide-react";
|
||||
|
||||
export interface NIPInfo {
|
||||
@@ -54,51 +55,57 @@ export interface NIPInfo {
|
||||
|
||||
export const NIP_METADATA: Record<number | string, NIPInfo> = {
|
||||
// Core Protocol
|
||||
1: {
|
||||
"01": {
|
||||
number: 1,
|
||||
name: "Basic Protocol",
|
||||
description: "Basic protocol flow description",
|
||||
icon: FileText,
|
||||
},
|
||||
2: {
|
||||
"02": {
|
||||
number: 2,
|
||||
name: "Follow List",
|
||||
description: "Contact list and petnames",
|
||||
icon: Users,
|
||||
},
|
||||
4: {
|
||||
"03": {
|
||||
number: 3,
|
||||
name: "OpenTimestamps Attestations for Events",
|
||||
description: "A proof of any event",
|
||||
icon: Signature,
|
||||
},
|
||||
"04": {
|
||||
number: 4,
|
||||
name: "Encrypted DMs",
|
||||
description: "Encrypted direct messages",
|
||||
icon: Mail,
|
||||
deprecated: true,
|
||||
},
|
||||
5: {
|
||||
"05": {
|
||||
number: 5,
|
||||
name: "Mapping Nostr keys to DNS",
|
||||
description: "Mapping Nostr keys to DNS-based internet identifiers",
|
||||
icon: Globe,
|
||||
},
|
||||
6: {
|
||||
"06": {
|
||||
number: 6,
|
||||
name: "Key Derivation",
|
||||
description: "Basic key derivation from mnemonic seed phrase",
|
||||
icon: Key,
|
||||
},
|
||||
7: {
|
||||
"07": {
|
||||
number: 7,
|
||||
name: "window.nostr",
|
||||
description: "window.nostr capability for web browsers",
|
||||
icon: Globe,
|
||||
},
|
||||
8: {
|
||||
"08": {
|
||||
number: 8,
|
||||
name: "Mentions",
|
||||
description: "Handling mentions",
|
||||
icon: Tag,
|
||||
deprecated: true,
|
||||
},
|
||||
9: {
|
||||
"09": {
|
||||
number: 9,
|
||||
name: "Event Deletion",
|
||||
description: "Event deletion",
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { GlobalRelayState } from "./relay-state";
|
||||
|
||||
export type AppId =
|
||||
| "nip"
|
||||
//| "nips"
|
||||
| "nips"
|
||||
| "kind"
|
||||
| "kinds"
|
||||
| "man"
|
||||
|
||||
@@ -100,6 +100,18 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
category: "System",
|
||||
defaultProps: {},
|
||||
},
|
||||
nips: {
|
||||
name: "nips",
|
||||
section: "1",
|
||||
synopsis: "nips",
|
||||
description:
|
||||
"Display all Nostr Implementation Possibilities (NIPs). Shows NIP numbers and titles, with links to view each specification document.",
|
||||
examples: ["nips View all NIPs"],
|
||||
seeAlso: ["nip", "kinds", "man"],
|
||||
appId: "nips",
|
||||
category: "Documentation",
|
||||
defaultProps: {},
|
||||
},
|
||||
// debug: {
|
||||
// name: "debug",
|
||||
// section: "1",
|
||||
|
||||
Reference in New Issue
Block a user