mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-09 14:19:02 +02:00
feat: syntax highlighting
This commit is contained in:
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