mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-05 10:11:12 +02:00
feat: syntax highlighting
This commit is contained in:
148
claudedocs/kind-1337-code-snippets.md
Normal file
148
claudedocs/kind-1337-code-snippets.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Kind 1337 Code Snippet Renderer (NIP-C0)
|
||||
|
||||
## Overview
|
||||
Added complete support for kind 1337 (Code Snippet) events from NIP-C0, with both feed and detail renderers.
|
||||
|
||||
## Files Created
|
||||
|
||||
### 1. Helper Functions (`src/lib/nip-c0-helpers.ts`)
|
||||
Tag extraction utilities using applesauce-core:
|
||||
- `getCodeLanguage()` - Programming language (l tag)
|
||||
- `getCodeName()` - Filename (name tag)
|
||||
- `getCodeExtension()` - File extension without dot (extension tag)
|
||||
- `getCodeDescription()` - Description text (description tag)
|
||||
- `getCodeRuntime()` - Runtime specification (runtime tag)
|
||||
- `getCodeLicenses()` - Array of license identifiers (license tags)
|
||||
- `getCodeDependencies()` - Array of dependencies (dep tags)
|
||||
- `getCodeRepo()` - Repository reference with type detection (URL or NIP-34 address)
|
||||
|
||||
### 2. Feed Renderer (`src/components/nostr/kinds/Kind1337Renderer.tsx`)
|
||||
Compact view showing:
|
||||
- **Clickable title** - Opens detail view, uses filename or "Code Snippet"
|
||||
- **Language badge** - Shows programming language in styled chip
|
||||
- **Description** - Truncated to 2 lines if present
|
||||
- **Code preview** - First 5 lines with line-clamp and "..." indicator
|
||||
- Wrapped in `BaseEventContainer` for consistency
|
||||
|
||||
### 3. Detail Renderer (`src/components/nostr/kinds/Kind1337DetailRenderer.tsx`)
|
||||
Full view with:
|
||||
- **Header** - Title with FileCode icon
|
||||
- **Metadata section** (before code):
|
||||
- Language and Extension
|
||||
- Description
|
||||
- Runtime (if present)
|
||||
- Licenses (if present)
|
||||
- Dependencies list (if present)
|
||||
- Repository link:
|
||||
- NIP-34 address → clickable, opens repository event
|
||||
- URL → external link with icon
|
||||
- **Code section**:
|
||||
- Full code in `<pre>` with `font-mono` styling
|
||||
- Copy button inline (absolute positioned top-right)
|
||||
- Matches JsonViewer pattern
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Kinds Registry
|
||||
Already existed in `src/constants/kinds.ts`:
|
||||
```typescript
|
||||
1337: {
|
||||
kind: 1337,
|
||||
name: "Code",
|
||||
description: "Code Snippet",
|
||||
nip: "C0",
|
||||
icon: FileCode,
|
||||
}
|
||||
```
|
||||
|
||||
### Event Title System
|
||||
Added to `src/lib/event-title.ts`:
|
||||
```typescript
|
||||
case 1337: // Code snippet
|
||||
title = getCodeName(event);
|
||||
break;
|
||||
```
|
||||
Window titles show filename or fall back to "Code Snippet"
|
||||
|
||||
### Renderer Registry
|
||||
Added to `src/components/nostr/kinds/index.tsx`:
|
||||
```typescript
|
||||
1337: Kind1337Renderer, // Code Snippet (NIP-C0)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Feed View
|
||||
- Clean, compact display
|
||||
- Language identification at a glance
|
||||
- Quick code preview without opening
|
||||
- Clickable to open full view
|
||||
|
||||
### Detail View
|
||||
- Complete metadata display
|
||||
- NIP-34 repository integration (handles both URLs and Nostr addresses)
|
||||
- One-click copy functionality
|
||||
- Clean, readable code display
|
||||
- Professional layout with metadata organized before code
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### No Syntax Highlighting (MVP)
|
||||
- Uses plain `<pre>` with `font-mono` styling (matching JsonViewer)
|
||||
- No new dependencies added
|
||||
- Can add syntax highlighting later (react-syntax-highlighter) as enhancement
|
||||
|
||||
### No Download Button
|
||||
- Simplified to just Copy functionality
|
||||
- Users can copy and save manually
|
||||
- Reduces UI complexity
|
||||
|
||||
### Metadata Before Code
|
||||
- More scannable - users see what the code is before reading it
|
||||
- Follows natural information hierarchy
|
||||
- Easier to understand context
|
||||
|
||||
### Copy Button Position
|
||||
- Inline in code section (top-right absolute)
|
||||
- Matches existing JsonViewer pattern
|
||||
- Consistent UX across app
|
||||
|
||||
## NIP-C0 Compliance
|
||||
|
||||
Supports all NIP-C0 tags:
|
||||
- ✅ `l` - Programming language
|
||||
- ✅ `name` - Filename
|
||||
- ✅ `extension` - File extension
|
||||
- ✅ `description` - Description
|
||||
- ✅ `runtime` - Runtime specification
|
||||
- ✅ `license` - License(s) (supports multiple)
|
||||
- ✅ `dep` - Dependencies (supports multiple)
|
||||
- ✅ `repo` - Repository reference (URL or NIP-34)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for Phase 2:
|
||||
1. **Syntax Highlighting** - Add react-syntax-highlighter for color-coded display
|
||||
2. **Line Numbers** - Optional line numbers for code blocks
|
||||
3. **Code Formatting** - Auto-format/prettify code
|
||||
4. **Run Functionality** - Execute supported languages (complex, low priority)
|
||||
5. **Download Button** - Add back if users request it
|
||||
6. **Diff View** - For patches or code changes
|
||||
|
||||
## Testing
|
||||
|
||||
- ✅ Type check passes
|
||||
- ✅ Integrates with existing event title system
|
||||
- ✅ Follows established component patterns
|
||||
- ✅ Uses applesauce-core helpers consistently
|
||||
- ✅ NIP-34 repository links handled correctly
|
||||
|
||||
## Usage
|
||||
|
||||
Users can now:
|
||||
1. View code snippets in feeds with preview
|
||||
2. Click to open full detail view
|
||||
3. See all metadata (language, runtime, deps, etc.)
|
||||
4. Copy code with one click
|
||||
5. Navigate to referenced repositories (NIP-34 or URLs)
|
||||
6. See proper window titles with filenames
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -32,6 +32,7 @@
|
||||
"dexie-react-hooks": "^4.2.0",
|
||||
"jotai": "^2.15.2",
|
||||
"lucide-react": "latest",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -48,6 +49,7 @@
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@react-router/dev": "^7.1.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
@@ -3841,6 +3843,13 @@
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prismjs": {
|
||||
"version": "1.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
@@ -8033,6 +8042,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/proc-log": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"dexie-react-hooks": "^4.2.0",
|
||||
"jotai": "^2.15.2",
|
||||
"lucide-react": "latest",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -56,6 +57,7 @@
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@react-router/dev": "^7.1.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
|
||||
33
src/components/CodeCopyButton.tsx
Normal file
33
src/components/CodeCopyButton.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Copy, CopyCheck } from "lucide-react";
|
||||
|
||||
interface CodeCopyButtonProps {
|
||||
onCopy: () => void;
|
||||
copied: boolean;
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable copy button for code blocks with consistent styling
|
||||
* Designed to be absolutely positioned over code containers
|
||||
*/
|
||||
export function CodeCopyButton({
|
||||
onCopy,
|
||||
copied,
|
||||
label = "Copy code",
|
||||
className = "",
|
||||
}: CodeCopyButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onCopy}
|
||||
className={`absolute top-2 right-2 p-2 bg-background/90 hover:bg-muted border border-border rounded transition-colors ${className}`.trim()}
|
||||
aria-label={label}
|
||||
>
|
||||
{copied ? (
|
||||
<CopyCheck className="size-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Copy className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -222,7 +222,7 @@ function useDynamicTitle(window: WindowInstance): WindowTitleData {
|
||||
</div>
|
||||
{getEventDisplayTitle(event, false)}
|
||||
<span> - </span>
|
||||
<UserName pubkey={event.pubkey} />
|
||||
<UserName pubkey={event.pubkey} className="text-inherit" />
|
||||
</div>
|
||||
);
|
||||
}, [appId, event]);
|
||||
|
||||
@@ -2,14 +2,15 @@ import { useState } from "react";
|
||||
import type { EventPointer, AddressPointer } from "nostr-tools/nip19";
|
||||
import { useNostrEvent } from "@/hooks/useNostrEvent";
|
||||
import { KindRenderer } from "./nostr/kinds";
|
||||
import { Kind0DetailRenderer } from "./nostr/kinds/Kind0DetailRenderer";
|
||||
import { Kind3DetailView } from "./nostr/kinds/Kind3Renderer";
|
||||
import { Kind0DetailRenderer } from "./nostr/kinds/ProfileDetailRenderer";
|
||||
import { Kind3DetailView } from "./nostr/kinds/ContactListRenderer";
|
||||
import { IssueDetailRenderer } from "./nostr/kinds/IssueDetailRenderer";
|
||||
import { PatchDetailRenderer } from "./nostr/kinds/PatchDetailRenderer";
|
||||
import { PullRequestDetailRenderer } from "./nostr/kinds/PullRequestDetailRenderer";
|
||||
import { Kind9802DetailRenderer } from "./nostr/kinds/Kind9802DetailRenderer";
|
||||
import { Kind10002DetailRenderer } from "./nostr/kinds/Kind10002DetailRenderer";
|
||||
import { Kind30023DetailRenderer } from "./nostr/kinds/Kind30023DetailRenderer";
|
||||
import { Kind1337DetailRenderer } from "./nostr/kinds/CodeSnippetDetailRenderer";
|
||||
import { Kind9802DetailRenderer } from "./nostr/kinds/HighlightDetailRenderer";
|
||||
import { Kind10002DetailRenderer } from "./nostr/kinds/RelayListDetailRenderer";
|
||||
import { Kind30023DetailRenderer } from "./nostr/kinds/ArticleDetailRenderer";
|
||||
import { RepositoryDetailRenderer } from "./nostr/kinds/RepositoryDetailRenderer";
|
||||
import { JsonViewer } from "./JsonViewer";
|
||||
import { RelayLink } from "./nostr/RelayLink";
|
||||
@@ -269,6 +270,8 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) {
|
||||
<Kind0DetailRenderer event={event} />
|
||||
) : event.kind === kinds.Contacts ? (
|
||||
<Kind3DetailView event={event} />
|
||||
) : event.kind === 1337 ? (
|
||||
<Kind1337DetailRenderer event={event} />
|
||||
) : event.kind === 1617 ? (
|
||||
<PatchDetailRenderer event={event} />
|
||||
) : event.kind === 1618 ? (
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CopyCheck, Copy } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useCopy } from "../hooks/useCopy";
|
||||
import { CodeCopyButton } from "@/components/CodeCopyButton";
|
||||
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
|
||||
|
||||
interface JsonViewerProps {
|
||||
data: any;
|
||||
@@ -31,27 +31,21 @@ export function JsonViewer({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] flex flex-col">
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] flex flex-col rounded-none">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-auto mt-2 relative">
|
||||
<pre className="text-xs font-mono bg-muted p-4 pr-10 overflow-scroll">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="link"
|
||||
onClick={handleCopy}
|
||||
aria-label="Copy JSON"
|
||||
className="absolute top-2 right-2"
|
||||
>
|
||||
{copied ? (
|
||||
<CopyCheck className="size-3.5" />
|
||||
) : (
|
||||
<Copy className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
{jsonString}
|
||||
</pre>
|
||||
<div className="flex-1 overflow-auto relative">
|
||||
<SyntaxHighlight
|
||||
code={jsonString}
|
||||
language="json"
|
||||
className="bg-muted p-4 pr-10 overflow-scroll"
|
||||
/>
|
||||
<CodeCopyButton
|
||||
onCopy={handleCopy}
|
||||
copied={copied}
|
||||
label="Copy JSON"
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -15,13 +15,11 @@ import {
|
||||
Shield,
|
||||
Filter as FilterIcon,
|
||||
Download,
|
||||
Copy,
|
||||
Clock,
|
||||
User,
|
||||
Hash,
|
||||
Search,
|
||||
Code,
|
||||
CopyCheck,
|
||||
} from "lucide-react";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { useReqTimeline } from "@/hooks/useReqTimeline";
|
||||
@@ -70,6 +68,8 @@ import {
|
||||
} from "@/lib/filter-formatters";
|
||||
import { sanitizeFilename } from "@/lib/filename-utils";
|
||||
import { useCopy } from "@/hooks/useCopy";
|
||||
import { CodeCopyButton } from "@/components/CodeCopyButton";
|
||||
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
|
||||
|
||||
// Memoized FeedEvent to prevent unnecessary re-renders during scroll
|
||||
const MemoizedFeedEvent = memo(
|
||||
@@ -639,22 +639,16 @@ function QueryDropdown({ filter, nip05Authors }: QueryDropdownProps) {
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="relative mt-2">
|
||||
<pre className="text-xs bg-muted/50 p-3 pr-10 overflow-x-auto font-mono border border-border/40 rounded">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="link"
|
||||
onClick={() => handleCopy(JSON.stringify(filter, null, 2))}
|
||||
aria-label="Copy query JSON"
|
||||
className="absolute top-2 right-2"
|
||||
>
|
||||
{copied ? (
|
||||
<CopyCheck className="size-3.5" />
|
||||
) : (
|
||||
<Copy className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
{JSON.stringify(filter, null, 2)}
|
||||
</pre>
|
||||
<SyntaxHighlight
|
||||
code={JSON.stringify(filter, null, 2)}
|
||||
language="json"
|
||||
className="bg-muted/50 p-3 pr-10 overflow-x-auto border border-border/40 rounded"
|
||||
/>
|
||||
<CodeCopyButton
|
||||
onCopy={() => handleCopy(JSON.stringify(filter, null, 2))}
|
||||
copied={copied}
|
||||
label="Copy query JSON"
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
73
src/components/SyntaxHighlight.tsx
Normal file
73
src/components/SyntaxHighlight.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import Prism from "prismjs";
|
||||
|
||||
// Core languages
|
||||
import "prismjs/components/prism-diff";
|
||||
import "prismjs/components/prism-javascript";
|
||||
import "prismjs/components/prism-typescript";
|
||||
import "prismjs/components/prism-jsx";
|
||||
import "prismjs/components/prism-tsx";
|
||||
import "prismjs/components/prism-bash";
|
||||
import "prismjs/components/prism-json";
|
||||
import "prismjs/components/prism-markdown";
|
||||
import "prismjs/components/prism-css";
|
||||
import "prismjs/components/prism-python";
|
||||
import "prismjs/components/prism-yaml";
|
||||
|
||||
interface SyntaxHighlightProps {
|
||||
code: string;
|
||||
language:
|
||||
| "diff"
|
||||
| "javascript"
|
||||
| "typescript"
|
||||
| "jsx"
|
||||
| "tsx"
|
||||
| "bash"
|
||||
| "shell"
|
||||
| "json"
|
||||
| "markdown"
|
||||
| "css"
|
||||
| "python"
|
||||
| "yaml";
|
||||
className?: string;
|
||||
showLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntax highlighting component using Prism.js
|
||||
* Matches Grimoire's dark theme using CSS custom properties
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <SyntaxHighlight code={patchContent} language="diff" />
|
||||
* ```
|
||||
*/
|
||||
export function SyntaxHighlight({
|
||||
code,
|
||||
language,
|
||||
className = "",
|
||||
showLineNumbers = false,
|
||||
}: SyntaxHighlightProps) {
|
||||
const codeRef = useRef<HTMLElement>(null);
|
||||
|
||||
// Normalize language aliases
|
||||
const normalizedLanguage = language === "shell" ? "bash" : language;
|
||||
|
||||
useEffect(() => {
|
||||
// Check for browser environment (SSR safety)
|
||||
if (typeof window === "undefined" || !codeRef.current) return;
|
||||
|
||||
// Highlight the code element
|
||||
Prism.highlightElement(codeRef.current);
|
||||
}, [code, normalizedLanguage]);
|
||||
|
||||
return (
|
||||
<pre
|
||||
className={`language-${normalizedLanguage} ${showLineNumbers ? "line-numbers" : ""} ${className}`.trim()}
|
||||
>
|
||||
<code ref={codeRef} className={`language-${normalizedLanguage}`}>
|
||||
{code}
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { UserName } from "../UserName";
|
||||
import { EmbeddedEvent } from "../EmbeddedEvent";
|
||||
import { MediaEmbed } from "../MediaEmbed";
|
||||
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
|
||||
@@ -263,12 +264,32 @@ export function Kind30023DetailRenderer({ event }: { event: NostrEvent }) {
|
||||
p: ({ ...props }) => (
|
||||
<p className="text-sm leading-relaxed mb-4" {...props} />
|
||||
),
|
||||
code: ({ ...props }: any) => (
|
||||
<code
|
||||
className="bg-muted px-0.5 py-0.5 rounded text-xs font-mono"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: ({ className, children, ...props }: any) => {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const language = match ? match[1] : null;
|
||||
const code = String(children).replace(/\n$/, "");
|
||||
|
||||
// Inline code (no language)
|
||||
if (!language) {
|
||||
return (
|
||||
<code
|
||||
className="bg-muted px-0.5 py-0.5 rounded text-xs font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
// Block code with syntax highlighting
|
||||
return (
|
||||
<SyntaxHighlight
|
||||
code={code}
|
||||
language={language as any}
|
||||
className="my-4"
|
||||
/>
|
||||
);
|
||||
},
|
||||
blockquote: ({ ...props }) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-muted pl-4 italic text-muted-foreground my-4"
|
||||
222
src/components/nostr/kinds/CodeSnippetDetailRenderer.tsx
Normal file
222
src/components/nostr/kinds/CodeSnippetDetailRenderer.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import { useMemo } from "react";
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { CodeCopyButton } from "@/components/CodeCopyButton";
|
||||
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
|
||||
import { useCopy } from "@/hooks/useCopy";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { useNostrEvent } from "@/hooks/useNostrEvent";
|
||||
import {
|
||||
getCodeLanguage,
|
||||
getCodeName,
|
||||
getCodeExtension,
|
||||
getCodeDescription,
|
||||
getCodeRuntime,
|
||||
getCodeLicenses,
|
||||
getCodeDependencies,
|
||||
getCodeRepo,
|
||||
} from "@/lib/nip-c0-helpers";
|
||||
import {
|
||||
getRepositoryName,
|
||||
getRepositoryIdentifier,
|
||||
} from "@/lib/nip34-helpers";
|
||||
import { Label } from "@/components/ui/Label";
|
||||
|
||||
interface Kind1337DetailRendererProps {
|
||||
event: NostrEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail renderer for Kind 1337 - Code Snippet (NIP-C0)
|
||||
* Full view with all metadata and complete code
|
||||
*/
|
||||
export function Kind1337DetailRenderer({ event }: Kind1337DetailRendererProps) {
|
||||
const { addWindow } = useGrimoire();
|
||||
const { copy, copied } = useCopy();
|
||||
|
||||
const name = useMemo(() => getCodeName(event), [event]);
|
||||
const language = useMemo(() => getCodeLanguage(event), [event]);
|
||||
const extension = useMemo(() => getCodeExtension(event), [event]);
|
||||
const description = useMemo(() => getCodeDescription(event), [event]);
|
||||
const runtime = useMemo(() => getCodeRuntime(event), [event]);
|
||||
const licenses = useMemo(() => getCodeLicenses(event), [event]);
|
||||
const dependencies = useMemo(() => getCodeDependencies(event), [event]);
|
||||
const repo = useMemo(() => getCodeRepo(event), [event]);
|
||||
|
||||
// Parse NIP-34 repository address if present
|
||||
const repoPointer = useMemo(() => {
|
||||
if (!repo || repo.type !== "nip34") return null;
|
||||
try {
|
||||
const [kindStr, pubkey, identifier] = repo.value.split(":");
|
||||
return {
|
||||
kind: parseInt(kindStr),
|
||||
pubkey,
|
||||
identifier,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [repo]);
|
||||
|
||||
// Fetch repository event if NIP-34 address
|
||||
const repoEvent = useNostrEvent(repoPointer || undefined);
|
||||
const repoName = repoEvent
|
||||
? getRepositoryName(repoEvent) ||
|
||||
getRepositoryIdentifier(repoEvent) ||
|
||||
"Repository"
|
||||
: repo?.type === "nip34"
|
||||
? repo.value.split(":")[2] || "Unknown Repository"
|
||||
: null;
|
||||
|
||||
const handleCopyCode = () => {
|
||||
copy(event.content);
|
||||
};
|
||||
|
||||
const handleRepoClick = () => {
|
||||
if (repoPointer) {
|
||||
addWindow("open", { pointer: repoPointer }, `Repository: ${repoName}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Normalize language to supported Prism languages
|
||||
const normalizedLanguage = useMemo(() => {
|
||||
if (!language) return null;
|
||||
const lang = language.toLowerCase();
|
||||
|
||||
// Map common language names to Prism identifiers
|
||||
const languageMap: Record<string, string> = {
|
||||
js: "javascript",
|
||||
ts: "typescript",
|
||||
py: "python",
|
||||
sh: "bash",
|
||||
shell: "bash",
|
||||
yml: "yaml",
|
||||
};
|
||||
|
||||
const mapped = languageMap[lang] || lang;
|
||||
|
||||
// Check if it's a supported language
|
||||
const supported = [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"jsx",
|
||||
"tsx",
|
||||
"bash",
|
||||
"json",
|
||||
"markdown",
|
||||
"css",
|
||||
"python",
|
||||
"yaml",
|
||||
"diff",
|
||||
];
|
||||
|
||||
return supported.includes(mapped) ? mapped : null;
|
||||
}, [language]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-6">
|
||||
{/* Header */}
|
||||
<h1 className="text-2xl font-bold">{name || "Code Snippet"}</h1>
|
||||
|
||||
{/* Description */}
|
||||
{description && <p>{description}</p>}
|
||||
|
||||
{/* Metadata Section */}
|
||||
<div className="grid grid-cols-2 gap-2 py-2 text-sm">
|
||||
{language && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Language</h3>
|
||||
<span className="font-mono">{language}</span>
|
||||
</div>
|
||||
)}
|
||||
{extension && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Extension</h3>
|
||||
<span className="font-mono">.{extension}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Runtime */}
|
||||
{runtime && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Runtime</h3>
|
||||
<span className="font-mono">{runtime}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Licenses */}
|
||||
{licenses.length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">License</h3>
|
||||
<span>{licenses.join(", ")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dependencies */}
|
||||
{dependencies.length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Dependencies</h3>
|
||||
<div className="flex gap-1 items-center flex-wrap">
|
||||
{dependencies.map((dep, idx) => (
|
||||
<Label key={idx} className="p-0.5">
|
||||
{dep}
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Repository */}
|
||||
{repo && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Repository</h3>
|
||||
{repo.type === "url" ? (
|
||||
<a
|
||||
href={repo.value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-primary hover:underline"
|
||||
>
|
||||
{repo.value}
|
||||
<ExternalLink className="size-3" />
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleRepoClick}
|
||||
className="inline-flex items-center gap-1 text-primary hover:underline cursor-crosshair"
|
||||
>
|
||||
{repoName}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Code Section */}
|
||||
<div className="relative">
|
||||
{normalizedLanguage ? (
|
||||
<>
|
||||
<SyntaxHighlight
|
||||
code={event.content}
|
||||
language={normalizedLanguage as any}
|
||||
className="bg-muted p-4 pr-10 border border-border overflow-x-auto"
|
||||
/>
|
||||
<CodeCopyButton
|
||||
onCopy={handleCopyCode}
|
||||
copied={copied}
|
||||
label="Copy code"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<pre className="text-xs font-mono bg-muted p-4 pr-10 border border-border overflow-x-auto">
|
||||
<CodeCopyButton
|
||||
onCopy={handleCopyCode}
|
||||
copied={copied}
|
||||
label="Copy code"
|
||||
/>
|
||||
<code>{event.content}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
src/components/nostr/kinds/CodeSnippetRenderer.tsx
Normal file
67
src/components/nostr/kinds/CodeSnippetRenderer.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
BaseEventContainer,
|
||||
BaseEventProps,
|
||||
ClickableEventTitle,
|
||||
} from "./BaseEventRenderer";
|
||||
import {
|
||||
getCodeLanguage,
|
||||
getCodeName,
|
||||
getCodeDescription,
|
||||
} from "@/lib/nip-c0-helpers";
|
||||
import { Label } from "@/components/ui/Label";
|
||||
|
||||
/**
|
||||
* Renderer for Kind 1337 - Code Snippet (NIP-C0)
|
||||
* Displays code snippet name, language, description, and preview in feed
|
||||
*/
|
||||
export function Kind1337Renderer({ event }: BaseEventProps) {
|
||||
const name = getCodeName(event);
|
||||
const language = getCodeLanguage(event);
|
||||
const description = getCodeDescription(event);
|
||||
|
||||
// Get first 3-5 lines for preview
|
||||
const codeLines = event.content.split("\n");
|
||||
const previewLines = codeLines.slice(0, 5);
|
||||
const hasMore = codeLines.length > 5;
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Title */}
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
windowTitle={name || "Code Snippet"}
|
||||
className="text-lg font-semibold text-foreground"
|
||||
>
|
||||
{name || "Code Snippet"}
|
||||
</ClickableEventTitle>
|
||||
|
||||
{/* Language Badge */}
|
||||
{language && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>{language}</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Code Preview */}
|
||||
<div className="relative">
|
||||
<pre className="text-xs font-mono bg-muted p-3 border border-border overflow-x-auto">
|
||||
<code className="line-clamp-5">
|
||||
{previewLines.join("\n")}
|
||||
{hasMore && "\n..."}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Tag, FolderGit2 } from "lucide-react";
|
||||
import { UserName } from "../UserName";
|
||||
import { EmbeddedEvent } from "../EmbeddedEvent";
|
||||
import { MediaEmbed } from "../MediaEmbed";
|
||||
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { useNostrEvent } from "@/hooks/useNostrEvent";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
getRepositoryName,
|
||||
getRepositoryIdentifier,
|
||||
} from "@/lib/nip34-helpers";
|
||||
import { Label } from "@/components/ui/Label";
|
||||
|
||||
/**
|
||||
* Component to render nostr: mentions inline
|
||||
@@ -214,12 +216,9 @@ export function IssueDetailRenderer({ event }: { event: NostrEvent }) {
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Tag className="size-3 text-muted-foreground" />
|
||||
{labels.map((label, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-3 py-1 border border-muted border-dotted text-muted-foreground text-xs"
|
||||
>
|
||||
<Label key={idx} size="md">
|
||||
{label}
|
||||
</span>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -282,12 +281,32 @@ export function IssueDetailRenderer({ event }: { event: NostrEvent }) {
|
||||
p: ({ ...props }) => (
|
||||
<p className="text-sm leading-relaxed mb-4" {...props} />
|
||||
),
|
||||
code: ({ ...props }: any) => (
|
||||
<code
|
||||
className="bg-muted px-0.5 py-0.5 rounded text-xs font-mono"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: ({ className, children, ...props }: any) => {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const language = match ? match[1] : null;
|
||||
const code = String(children).replace(/\n$/, "");
|
||||
|
||||
// Inline code (no language)
|
||||
if (!language) {
|
||||
return (
|
||||
<code
|
||||
className="bg-muted px-0.5 py-0.5 rounded text-xs font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
// Block code with syntax highlighting
|
||||
return (
|
||||
<SyntaxHighlight
|
||||
code={code}
|
||||
language={language as any}
|
||||
className="my-4"
|
||||
/>
|
||||
);
|
||||
},
|
||||
blockquote: ({ ...props }) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-muted pl-4 italic text-muted-foreground my-4"
|
||||
@@ -309,7 +328,7 @@ export function IssueDetailRenderer({ event }: { event: NostrEvent }) {
|
||||
hr: () => <hr className="my-4" />,
|
||||
}}
|
||||
>
|
||||
{event.content.replace(/\\n/g, '\n')}
|
||||
{event.content.replace(/\\n/g, "\n")}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
) : (
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
getRepositoryIdentifier,
|
||||
} from "@/lib/nip34-helpers";
|
||||
import { UserName } from "../UserName";
|
||||
import { Label } from "@/components/ui/Label";
|
||||
|
||||
/**
|
||||
* Renderer for Kind 1621 - Issue
|
||||
@@ -103,12 +104,7 @@ export function IssueRenderer({ event }: BaseEventProps) {
|
||||
items-center gap-1 overflow-x-scroll my-1"
|
||||
>
|
||||
{labels.map((label, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-0.5 border border-muted border-dotted text-xs text-muted-foreground"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<Label key={idx}>{label}</Label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useMemo } from "react";
|
||||
import { GitCommit, FolderGit2, User, Copy, CopyCheck } from "lucide-react";
|
||||
import { UserName } from "../UserName";
|
||||
import { CodeCopyButton } from "@/components/CodeCopyButton";
|
||||
import { useCopy } from "@/hooks/useCopy";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { useNostrEvent } from "@/hooks/useNostrEvent";
|
||||
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import {
|
||||
getPatchSubject,
|
||||
@@ -203,20 +205,16 @@ export function PatchDetailRenderer({ event }: { event: NostrEvent }) {
|
||||
<section className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">Patch</h2>
|
||||
<div className="relative">
|
||||
<pre className="overflow-x-auto text-xs font-mono bg-muted/30 p-4 whitespace-pre-wrap break-words">
|
||||
{event.content}
|
||||
</pre>
|
||||
<button
|
||||
onClick={() => copy(event.content)}
|
||||
className="absolute top-2 right-2 p-2 bg-background/90 hover:bg-muted border border-border rounded"
|
||||
aria-label="Copy patch"
|
||||
>
|
||||
{copied ? (
|
||||
<CopyCheck className="size-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Copy className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
<SyntaxHighlight
|
||||
code={event.content}
|
||||
language="diff"
|
||||
className="overflow-x-auto bg-muted/30 p-4"
|
||||
/>
|
||||
<CodeCopyButton
|
||||
onCopy={() => copy(event.content)}
|
||||
copied={copied}
|
||||
label="Copy patch"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { GitBranch, FolderGit2, Tag, Copy, CopyCheck } from "lucide-react";
|
||||
import { UserName } from "../UserName";
|
||||
import { EmbeddedEvent } from "../EmbeddedEvent";
|
||||
import { MediaEmbed } from "../MediaEmbed";
|
||||
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
|
||||
import { useCopy } from "@/hooks/useCopy";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { useNostrEvent } from "@/hooks/useNostrEvent";
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
getRepositoryName,
|
||||
getRepositoryIdentifier,
|
||||
} from "@/lib/nip34-helpers";
|
||||
import { Label } from "@/components/ui/Label";
|
||||
|
||||
/**
|
||||
* Component to render nostr: mentions inline
|
||||
@@ -227,12 +229,9 @@ export function PullRequestDetailRenderer({ event }: { event: NostrEvent }) {
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Tag className="size-3 text-muted-foreground" />
|
||||
{labels.map((label, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-3 py-1 border border-muted border-dotted text-muted-foreground text-xs"
|
||||
>
|
||||
<Label key={idx} size="md">
|
||||
{label}
|
||||
</span>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -396,12 +395,32 @@ export function PullRequestDetailRenderer({ event }: { event: NostrEvent }) {
|
||||
p: ({ ...props }) => (
|
||||
<p className="text-sm leading-relaxed mb-4" {...props} />
|
||||
),
|
||||
code: ({ ...props }: any) => (
|
||||
<code
|
||||
className="bg-muted px-0.5 py-0.5 rounded text-xs font-mono"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: ({ className, children, ...props }: any) => {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const language = match ? match[1] : null;
|
||||
const code = String(children).replace(/\n$/, "");
|
||||
|
||||
// Inline code (no language)
|
||||
if (!language) {
|
||||
return (
|
||||
<code
|
||||
className="bg-muted px-0.5 py-0.5 rounded text-xs font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
// Block code with syntax highlighting
|
||||
return (
|
||||
<SyntaxHighlight
|
||||
code={code}
|
||||
language={language as any}
|
||||
className="my-4"
|
||||
/>
|
||||
);
|
||||
},
|
||||
blockquote: ({ ...props }) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-muted pl-4 italic text-muted-foreground my-4"
|
||||
@@ -423,7 +442,7 @@ export function PullRequestDetailRenderer({ event }: { event: NostrEvent }) {
|
||||
hr: () => <hr className="my-4" />,
|
||||
}}
|
||||
>
|
||||
{event.content.replace(/\\n/g, '\n')}
|
||||
{event.content.replace(/\\n/g, "\n")}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
</>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
getRepositoryName,
|
||||
getRepositoryIdentifier,
|
||||
} from "@/lib/nip34-helpers";
|
||||
import { Label } from "@/components/ui/Label";
|
||||
|
||||
/**
|
||||
* Renderer for Kind 1618 - Pull Request
|
||||
@@ -111,12 +112,7 @@ export function PullRequestRenderer({ event }: BaseEventProps) {
|
||||
{labels.length > 0 && (
|
||||
<div className="flex items-center gap-1 overflow-x-scroll">
|
||||
{labels.map((label, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-0.5 border border-muted border-dotted text-xs text-muted-foreground"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<Label key={idx}>{label}</Label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { Kind0Renderer } from "./Kind0Renderer";
|
||||
import { Kind1Renderer } from "./Kind1Renderer";
|
||||
import { Kind3Renderer } from "./Kind3Renderer";
|
||||
import { Kind0Renderer } from "./ProfileRenderer";
|
||||
import { Kind1Renderer } from "./NoteRenderer";
|
||||
import { Kind3Renderer } from "./ContactListRenderer";
|
||||
import { RepostRenderer } from "./RepostRenderer";
|
||||
import { Kind7Renderer } from "./Kind7Renderer";
|
||||
import { Kind9Renderer } from "./Kind9Renderer";
|
||||
import { Kind20Renderer } from "./Kind20Renderer";
|
||||
import { Kind21Renderer } from "./Kind21Renderer";
|
||||
import { Kind22Renderer } from "./Kind22Renderer";
|
||||
import { Kind1063Renderer } from "./Kind1063Renderer";
|
||||
import { Kind7Renderer } from "./ReactionRenderer";
|
||||
import { Kind9Renderer } from "./ChatMessageRenderer";
|
||||
import { Kind20Renderer } from "./PictureRenderer";
|
||||
import { Kind21Renderer } from "./VideoRenderer";
|
||||
import { Kind22Renderer } from "./ShortVideoRenderer";
|
||||
import { Kind1063Renderer } from "./FileMetadataRenderer";
|
||||
import { Kind1337Renderer } from "./CodeSnippetRenderer";
|
||||
import { IssueRenderer } from "./IssueRenderer";
|
||||
import { PatchRenderer } from "./PatchRenderer";
|
||||
import { PullRequestRenderer } from "./PullRequestRenderer";
|
||||
import { Kind9735Renderer } from "./Kind9735Renderer";
|
||||
import { Kind9802Renderer } from "./Kind9802Renderer";
|
||||
import { Kind10002Renderer } from "./Kind10002Renderer";
|
||||
import { Kind30023Renderer } from "./Kind30023Renderer";
|
||||
import { Kind9735Renderer } from "./ZapReceiptRenderer";
|
||||
import { Kind9802Renderer } from "./HighlightRenderer";
|
||||
import { Kind10002Renderer } from "./RelayListRenderer";
|
||||
import { Kind30023Renderer } from "./ArticleRenderer";
|
||||
import { RepositoryRenderer } from "./RepositoryRenderer";
|
||||
import { Kind39701Renderer } from "./Kind39701Renderer";
|
||||
import { Kind39701Renderer } from "./BookmarkRenderer";
|
||||
import { GenericRelayListRenderer } from "./GenericRelayListRenderer";
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer";
|
||||
@@ -38,6 +39,7 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
|
||||
22: Kind22Renderer, // Short Video (NIP-71)
|
||||
1063: Kind1063Renderer, // File Metadata (NIP-94)
|
||||
1111: Kind1Renderer, // Post
|
||||
1337: Kind1337Renderer, // Code Snippet (NIP-C0)
|
||||
1617: PatchRenderer, // Patch (NIP-34)
|
||||
1618: PullRequestRenderer, // Pull Request (NIP-34)
|
||||
1621: IssueRenderer, // Issue (NIP-34)
|
||||
@@ -99,16 +101,16 @@ export {
|
||||
EventMenu,
|
||||
} from "./BaseEventRenderer";
|
||||
export type { BaseEventProps } from "./BaseEventRenderer";
|
||||
export { Kind1Renderer } from "./Kind1Renderer";
|
||||
export { Kind1Renderer } from "./NoteRenderer";
|
||||
export {
|
||||
RepostRenderer,
|
||||
Kind6Renderer,
|
||||
Kind16Renderer,
|
||||
} from "./RepostRenderer";
|
||||
export { Kind7Renderer } from "./Kind7Renderer";
|
||||
export { Kind9Renderer } from "./Kind9Renderer";
|
||||
export { Kind20Renderer } from "./Kind20Renderer";
|
||||
export { Kind21Renderer } from "./Kind21Renderer";
|
||||
export { Kind22Renderer } from "./Kind22Renderer";
|
||||
export { Kind1063Renderer } from "./Kind1063Renderer";
|
||||
export { Kind9735Renderer } from "./Kind9735Renderer";
|
||||
export { Kind7Renderer } from "./ReactionRenderer";
|
||||
export { Kind9Renderer } from "./ChatMessageRenderer";
|
||||
export { Kind20Renderer } from "./PictureRenderer";
|
||||
export { Kind21Renderer } from "./VideoRenderer";
|
||||
export { Kind22Renderer } from "./ShortVideoRenderer";
|
||||
export { Kind1063Renderer } from "./FileMetadataRenderer";
|
||||
export { Kind9735Renderer } from "./ZapReceiptRenderer";
|
||||
|
||||
31
src/components/ui/Label.tsx
Normal file
31
src/components/ui/Label.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface LabelProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
/**
|
||||
* Size variant for the label
|
||||
* - sm: px-2 py-0.5 (default)
|
||||
* - md: px-3 py-1
|
||||
*/
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
/**
|
||||
* Label/Badge component with dotted border styling
|
||||
* Used for tags, language indicators, and metadata labels
|
||||
*/
|
||||
export function Label({ children, className, size = "sm" }: LabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"border border-muted border-dotted text-muted-foreground text-xs",
|
||||
size === "sm" && "px-2 py-0.5",
|
||||
size === "md" && "px-3 py-1",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
/* Prism syntax highlighting theme */
|
||||
@import './styles/prism-theme.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getPatchSubject,
|
||||
getPullRequestSubject,
|
||||
} from "@/lib/nip34-helpers";
|
||||
import { getCodeName } from "@/lib/nip-c0-helpers";
|
||||
import { getKindInfo } from "@/constants/kinds";
|
||||
|
||||
/**
|
||||
@@ -35,6 +36,9 @@ export function getEventDisplayTitle(
|
||||
case 30617: // Repository
|
||||
title = getRepositoryName(event);
|
||||
break;
|
||||
case 1337: // Code snippet
|
||||
title = getCodeName(event);
|
||||
break;
|
||||
case 1621: // Issue
|
||||
title = getIssueTitle(event);
|
||||
break;
|
||||
|
||||
96
src/lib/nip-c0-helpers.ts
Normal file
96
src/lib/nip-c0-helpers.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { getTagValue } from "applesauce-core/helpers";
|
||||
|
||||
function getTagValues(event: NostrEvent, tag: string) {
|
||||
return event.tags.filter((t) => t[0] === tag).map((t) => t[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-C0 Code Snippet Helpers
|
||||
* Extract metadata from kind 1337 code snippet events
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the programming language
|
||||
* @param event - Code snippet event
|
||||
* @returns Language name (e.g., "javascript", "python")
|
||||
*/
|
||||
export function getCodeLanguage(event: NostrEvent): string | undefined {
|
||||
return getTagValue(event, "l");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the code snippet name/filename
|
||||
* @param event - Code snippet event
|
||||
* @returns Filename (e.g., "hello-world.js")
|
||||
*/
|
||||
export function getCodeName(event: NostrEvent): string | undefined {
|
||||
return getTagValue(event, "name");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file extension
|
||||
* @param event - Code snippet event
|
||||
* @returns Extension without dot (e.g., "js", "py")
|
||||
*/
|
||||
export function getCodeExtension(event: NostrEvent): string | undefined {
|
||||
return getTagValue(event, "extension");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the code description
|
||||
* @param event - Code snippet event
|
||||
* @returns Description text
|
||||
*/
|
||||
export function getCodeDescription(event: NostrEvent): string | undefined {
|
||||
return getTagValue(event, "description");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the runtime specification
|
||||
* @param event - Code snippet event
|
||||
* @returns Runtime string (e.g., "node v18.15.0", "python 3.11")
|
||||
*/
|
||||
export function getCodeRuntime(event: NostrEvent): string | undefined {
|
||||
return getTagValue(event, "runtime");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all licenses
|
||||
* @param event - Code snippet event
|
||||
* @returns Array of license identifiers (e.g., ["MIT"], ["GPL-3.0-or-later", "Apache-2.0"])
|
||||
*/
|
||||
export function getCodeLicenses(event: NostrEvent): string[] {
|
||||
return getTagValues(event, "license");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all dependencies
|
||||
* @param event - Code snippet event
|
||||
* @returns Array of dependency strings
|
||||
*/
|
||||
export function getCodeDependencies(event: NostrEvent): string[] {
|
||||
return getTagValues(event, "dep");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository reference
|
||||
* @param event - Code snippet event
|
||||
* @returns Repository info with type (url or nip34) and value
|
||||
*/
|
||||
export function getCodeRepo(
|
||||
event: NostrEvent,
|
||||
):
|
||||
| { type: "url"; value: string }
|
||||
| { type: "nip34"; value: string }
|
||||
| undefined {
|
||||
const repoTag = event.tags.find((t) => t[0] === "repo");
|
||||
if (!repoTag || !repoTag[1]) return undefined;
|
||||
|
||||
const value = repoTag[1];
|
||||
// Check if it's NIP-34 address format (30617:pubkey:dtag)
|
||||
if (value.startsWith("30617:")) {
|
||||
return { type: "nip34", value };
|
||||
}
|
||||
return { type: "url", value };
|
||||
}
|
||||
145
src/styles/prism-theme.css
Normal file
145
src/styles/prism-theme.css
Normal file
@@ -0,0 +1,145 @@
|
||||
/* Grimoire Prism Theme - Matches dark theme using CSS variables */
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: hsl(var(--foreground));
|
||||
background: none;
|
||||
text-shadow: none;
|
||||
font-family: 'Oxygen Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
tab-size: 4;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Diff-specific tokens */
|
||||
|
||||
/* Deleted lines (red) - subtle background, no strikethrough */
|
||||
.token.deleted {
|
||||
color: #ff8787;
|
||||
background: rgba(255, 59, 48, 0.1);
|
||||
display: block;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Added lines (green) - subtle background */
|
||||
.token.inserted {
|
||||
color: #69db7c;
|
||||
background: rgba(52, 199, 89, 0.1);
|
||||
display: block;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Hunk headers (@@ -1,5 +1,7 @@) - cyan/blue */
|
||||
.token.diff.coord,
|
||||
.token.coord {
|
||||
color: #66d9ef;
|
||||
background: rgba(102, 217, 239, 0.08);
|
||||
display: block;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* File headers (diff --git, ---, +++) */
|
||||
.token.diff.range,
|
||||
.token.prefix.unchanged,
|
||||
.language-diff .token.unchanged {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Prefix characters (+/-) */
|
||||
.language-diff .token.prefix {
|
||||
font-weight: 700;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* General syntax tokens */
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: hsl(var(--foreground) / 0.7);
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: hsl(var(--primary));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Line highlighting */
|
||||
pre[class*="language-"] > code {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Optional: Line numbers support */
|
||||
.line-numbers .line-numbers-rows {
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.line-numbers-rows > span:before {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
Reference in New Issue
Block a user