feat: NIPS command

This commit is contained in:
Alejandro Gómez
2025-12-14 15:14:14 +01:00
parent b8d05e40c7
commit 6f17415340
7 changed files with 176 additions and 11 deletions

View File

@@ -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}`}

View 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>
);
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -3,7 +3,7 @@ import type { GlobalRelayState } from "./relay-state";
export type AppId =
| "nip"
//| "nips"
| "nips"
| "kind"
| "kinds"
| "man"

View File

@@ -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",