From 32160480e2d8d90b942c7fea9c3b4c51f5ff5224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Fri, 12 Dec 2025 22:22:28 +0100 Subject: [PATCH] feat: kind links --- src/components/CommandLauncher.tsx | 6 +--- src/components/DecodeViewer.tsx | 28 +++++++++-------- src/components/KindBadge.tsx | 33 +++++++++++++++++--- src/components/NipRenderer.tsx | 2 +- src/components/nostr/kinds/Kind3Renderer.tsx | 2 +- src/components/nostr/kinds/index.tsx | 6 +++- src/components/ui/visually-hidden.tsx | 6 +--- src/lib/req-parser.test.ts | 20 +++++++----- src/lib/req-parser.ts | 16 +++++----- src/types/man.ts | 9 ++++-- vitest.config.ts | 8 ++--- 11 files changed, 84 insertions(+), 52 deletions(-) diff --git a/src/components/CommandLauncher.tsx b/src/components/CommandLauncher.tsx index cd55220..947ee31 100644 --- a/src/components/CommandLauncher.tsx +++ b/src/components/CommandLauncher.tsx @@ -2,11 +2,7 @@ import { useEffect, useState } from "react"; import { Command } from "cmdk"; import { useGrimoire } from "@/core/state"; import { manPages } from "@/types/man"; -import { - Dialog, - DialogContent, - DialogTitle, -} from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { VisuallyHidden } from "@/components/ui/visually-hidden"; import "./command-launcher.css"; diff --git a/src/components/DecodeViewer.tsx b/src/components/DecodeViewer.tsx index 9bf1d70..a2fa191 100644 --- a/src/components/DecodeViewer.tsx +++ b/src/components/DecodeViewer.tsx @@ -10,6 +10,7 @@ import { useGrimoire } from "@/core/state"; import { useCopy } from "../hooks/useCopy"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; +import { KindBadge } from "./KindBadge"; interface DecodeViewerProps { args: string[]; @@ -216,20 +217,21 @@ export default function DecodeViewer({ args }: DecodeViewerProps) { )} {type === "naddr" && ( <> -
-
-
Kind
-
- {(data as any).kind} -
+
+
Kind
+
+
-
-
- Identifier -
-
- {(data as any).identifier} -
+
+
+
Identifier
+
+ {(data as any).identifier}
diff --git a/src/components/KindBadge.tsx b/src/components/KindBadge.tsx index 7890a9c..6e71c0d 100644 --- a/src/components/KindBadge.tsx +++ b/src/components/KindBadge.tsx @@ -1,5 +1,6 @@ import { getKindInfo } from "@/constants/kinds"; import { cn } from "@/lib/utils"; +import { useGrimoire } from "@/core/state"; interface KindBadgeProps { kind: number; @@ -9,6 +10,7 @@ interface KindBadgeProps { variant?: "default" | "compact" | "full"; className?: string; iconClassname?: string; + clickable?: boolean; } export function KindBadge({ @@ -19,11 +21,20 @@ export function KindBadge({ variant = "default", className = "", iconClassname = "text-muted-foreground", + clickable = false, }: KindBadgeProps) { + const { addWindow } = useGrimoire(); const kindInfo = getKindInfo(kind); const Icon = kindInfo?.icon; const style = "inline-flex items-center gap-2 text-foreground"; + const interactiveStyle = clickable ? "cursor-pointer" : ""; + + const handleClick = () => { + if (clickable) { + addWindow("kind", { number: String(kind) }, `Kind ${kind}`); + } + }; // Apply variant presets or use props let showIcon = propShowIcon ?? true; @@ -42,7 +53,10 @@ export function KindBadge({ if (!kindInfo) { return ( -
+
Kind {kind}
); @@ -50,11 +64,22 @@ export function KindBadge({ return (
{showIcon && Icon && } - {showName && {kindInfo.name}} + {showName && ( + + {kindInfo.name} + + )} {showKindNumber && ({kind})}
); diff --git a/src/components/NipRenderer.tsx b/src/components/NipRenderer.tsx index 0d2efc9..0ac0220 100644 --- a/src/components/NipRenderer.tsx +++ b/src/components/NipRenderer.tsx @@ -47,7 +47,7 @@ export function NipRenderer({ nipId, className = "" }: NipRendererProps) {
{kinds.map((kind) => ( - + ))}
diff --git a/src/components/nostr/kinds/Kind3Renderer.tsx b/src/components/nostr/kinds/Kind3Renderer.tsx index b97298e..9c787e5 100644 --- a/src/components/nostr/kinds/Kind3Renderer.tsx +++ b/src/components/nostr/kinds/Kind3Renderer.tsx @@ -50,7 +50,7 @@ export function Kind3DetailView({ event }: { event: any }) { const { state } = useGrimoire(); const followedPubkeys = getTagValues(event, "p").filter( - (pk) => pk.length === 64 + (pk) => pk.length === 64, ); const topics = getTagValues(event, "t"); diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index de4a313..55bd849 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -90,7 +90,11 @@ export { } from "./BaseEventRenderer"; export type { BaseEventProps } from "./BaseEventRenderer"; export { Kind1Renderer } from "./Kind1Renderer"; -export { RepostRenderer, Kind6Renderer, Kind16Renderer } from "./RepostRenderer"; +export { + RepostRenderer, + Kind6Renderer, + Kind16Renderer, +} from "./RepostRenderer"; export { Kind7Renderer } from "./Kind7Renderer"; export { Kind20Renderer } from "./Kind20Renderer"; export { Kind21Renderer } from "./Kind21Renderer"; diff --git a/src/components/ui/visually-hidden.tsx b/src/components/ui/visually-hidden.tsx index 7ffbc7d..fee1bbd 100644 --- a/src/components/ui/visually-hidden.tsx +++ b/src/components/ui/visually-hidden.tsx @@ -9,11 +9,7 @@ export const VisuallyHidden = React.forwardRef< HTMLSpanElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( - + )); VisuallyHidden.displayName = "VisuallyHidden"; diff --git a/src/lib/req-parser.test.ts b/src/lib/req-parser.test.ts index 7f6f313..86787d2 100644 --- a/src/lib/req-parser.test.ts +++ b/src/lib/req-parser.test.ts @@ -64,7 +64,10 @@ describe("parseReqCommand", () => { "-a", "user@domain.com,alice@example.com", ]); - expect(result.nip05Authors).toEqual(["user@domain.com", "alice@example.com"]); + expect(result.nip05Authors).toEqual([ + "user@domain.com", + "alice@example.com", + ]); expect(result.filter.authors).toBeUndefined(); }); @@ -76,10 +79,7 @@ describe("parseReqCommand", () => { }); it("should deduplicate NIP-05 identifiers", () => { - const result = parseReqCommand([ - "-a", - "user@domain.com,user@domain.com", - ]); + const result = parseReqCommand(["-a", "user@domain.com,user@domain.com"]); expect(result.nip05Authors).toEqual(["user@domain.com"]); }); }); @@ -120,8 +120,14 @@ describe("parseReqCommand", () => { }); it("should accumulate NIP-05 identifiers for #p tags", () => { - const result = parseReqCommand(["-p", "user@domain.com,alice@example.com"]); - expect(result.nip05PTags).toEqual(["user@domain.com", "alice@example.com"]); + const result = parseReqCommand([ + "-p", + "user@domain.com,alice@example.com", + ]); + expect(result.nip05PTags).toEqual([ + "user@domain.com", + "alice@example.com", + ]); expect(result.filter["#p"]).toBeUndefined(); }); diff --git a/src/lib/req-parser.ts b/src/lib/req-parser.ts index c1dc9d6..2478de4 100644 --- a/src/lib/req-parser.ts +++ b/src/lib/req-parser.ts @@ -22,9 +22,9 @@ export interface ParsedReqCommand { function parseCommaSeparated( value: string, parser: (v: string) => T | null, - target: Set + target: Set, ): boolean { - const values = value.split(',').map(v => v.trim()); + const values = value.split(",").map((v) => v.trim()); let addedAny = false; for (const val of values) { @@ -102,7 +102,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand { const kind = parseInt(v, 10); return isNaN(kind) ? null : kind; }, - kinds + kinds, ); i += addedAny ? 2 : 1; break; @@ -116,7 +116,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand { break; } let addedAny = false; - const values = nextArg.split(',').map(a => a.trim()); + const values = nextArg.split(",").map((a) => a.trim()); for (const authorStr of values) { if (!authorStr) continue; // Check if it's a NIP-05 identifier @@ -156,7 +156,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand { const addedAny = parseCommaSeparated( nextArg, parseNoteOrHex, - eventIds + eventIds, ); i += addedAny ? 2 : 1; break; @@ -169,7 +169,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand { break; } let addedAny = false; - const values = nextArg.split(',').map(p => p.trim()); + const values = nextArg.split(",").map((p) => p.trim()); for (const pubkeyStr of values) { if (!pubkeyStr) continue; // Check if it's a NIP-05 identifier @@ -194,7 +194,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand { const addedAny = parseCommaSeparated( nextArg, (v) => v, // hashtags are already strings - tTags + tTags, ); i += addedAny ? 2 : 1; } else { @@ -209,7 +209,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand { const addedAny = parseCommaSeparated( nextArg, (v) => v, // d-tags are already strings - dTags + dTags, ); i += addedAny ? 2 : 1; } else { diff --git a/src/types/man.ts b/src/types/man.ts index 4a6a63c..8815955 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -157,7 +157,8 @@ export const manPages: Record = { }, { flag: "-e ", - description: "Filter by referenced event ID (#e tag). Supports comma-separated values: -e id1,id2,id3", + description: + "Filter by referenced event ID (#e tag). Supports comma-separated values: -e id1,id2,id3", }, { flag: "-p ", @@ -166,11 +167,13 @@ export const manPages: Record = { }, { flag: "-t ", - description: "Filter by hashtag (#t tag). Supports comma-separated values: -t nostr,bitcoin,lightning", + description: + "Filter by hashtag (#t tag). Supports comma-separated values: -t nostr,bitcoin,lightning", }, { flag: "-d ", - description: "Filter by d-tag identifier (replaceable events). Supports comma-separated values: -d article1,article2", + description: + "Filter by d-tag identifier (replaceable events). Supports comma-separated values: -d article1,article2", }, { flag: "--since