mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-11 16:07:15 +02:00
feat: kind links
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -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" && (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">Kind</div>
|
||||
<div className="bg-muted p-2 rounded font-mono text-xs">
|
||||
{(data as any).kind}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">Kind</div>
|
||||
<div className="bg-muted p-3 rounded text-xs">
|
||||
<KindBadge
|
||||
kind={(data as any).kind}
|
||||
variant="full"
|
||||
iconClassname="size-3 text-muted-foreground"
|
||||
clickable
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-2">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Identifier
|
||||
</div>
|
||||
<div className="bg-muted p-2 rounded font-mono text-xs truncate">
|
||||
{(data as any).identifier}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground">Identifier</div>
|
||||
<div className="bg-muted p-3 rounded font-mono text-xs break-all">
|
||||
{(data as any).identifier}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
|
||||
@@ -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 (
|
||||
<div className={cn(style, className)}>
|
||||
<div
|
||||
className={cn(style, interactiveStyle, className)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span>Kind {kind}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -50,11 +64,22 @@ export function KindBadge({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(style, className)}
|
||||
title={`${kindInfo.description} (NIP-${kindInfo.nip})`}
|
||||
className={cn(style, interactiveStyle, className)}
|
||||
title={`${kindInfo.description} (NIP-${kindInfo.nip})${clickable ? " - Click to view" : ""}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{showIcon && Icon && <Icon className={cn("size-4", iconClassname)} />}
|
||||
{showName && <span>{kindInfo.name}</span>}
|
||||
{showName && (
|
||||
<span
|
||||
className={
|
||||
clickable
|
||||
? "cursor-crosshair hover:underline decoration-dotted"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{kindInfo.name}
|
||||
</span>
|
||||
)}
|
||||
{showKindNumber && <span>({kind})</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ export function NipRenderer({ nipId, className = "" }: NipRendererProps) {
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{kinds.map((kind) => (
|
||||
<KindBadge key={kind} kind={kind} variant="full" />
|
||||
<KindBadge key={kind} kind={kind} variant="full" clickable />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -9,11 +9,7 @@ export const VisuallyHidden = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.HTMLAttributes<HTMLSpanElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn("sr-only", className)}
|
||||
{...props}
|
||||
/>
|
||||
<span ref={ref} className={cn("sr-only", className)} {...props} />
|
||||
));
|
||||
|
||||
VisuallyHidden.displayName = "VisuallyHidden";
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ export interface ParsedReqCommand {
|
||||
function parseCommaSeparated<T>(
|
||||
value: string,
|
||||
parser: (v: string) => T | null,
|
||||
target: Set<T>
|
||||
target: Set<T>,
|
||||
): 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 {
|
||||
|
||||
@@ -157,7 +157,8 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
},
|
||||
{
|
||||
flag: "-e <id>",
|
||||
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 <npub|hex|nip05>",
|
||||
@@ -166,11 +167,13 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
},
|
||||
{
|
||||
flag: "-t <hashtag>",
|
||||
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 <identifier>",
|
||||
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 <time>",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import path from 'path';
|
||||
import { defineConfig } from "vitest/config";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
environment: "node",
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user