feat: show metric type labels in trusted assertion feed view

Add Label components to the assertion feed renderer so you can see at a
glance which metrics an assertion carries (Followers, Posts, Zaps, etc.)
instead of just numeric values. Also swap Badge → Label for the kind
indicator for visual consistency.

Replace hardcoded green/yellow/red rank colors with theme variables
(success/warning/destructive) in both feed and detail renderers so the
rank bar works correctly across all themes.

Add Label to CLAUDE.md shared components list (22 imports across the
codebase).

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB
This commit is contained in:
Claude
2026-02-20 07:50:20 +00:00
parent 8bc962bd3c
commit 1d2f27f9a3
3 changed files with 24 additions and 16 deletions

View File

@@ -343,6 +343,7 @@ This allows `applyTheme()` to switch themes at runtime.
- **Shared Components** — Use these instead of rolling your own:
- **`UserName`** (`src/components/nostr/UserName.tsx`): Always use for displaying user pubkeys. Shows display name, Grimoire member badge, supporter flame. Clicking opens profile. Accepts optional `relayHints` prop for fetching profiles from specific relays.
- **`RelayLink`** (`src/components/nostr/RelayLink.tsx`): Always use for displaying relay URLs. Shows relay favicon, insecure `ws://` warnings, read/write badges, and opens relay detail window on click. Never render raw relay URL strings.
- **`Label`** (`src/components/ui/label.tsx`): Dotted-border tag/badge for metadata labels (language, kind, status, metric type). Two sizes: `sm` (default) and `md`. Use instead of ad-hoc `<span>` tags for tag-like indicators.
- **`RichText`** (`src/components/nostr/RichText.tsx`): Universal Nostr content renderer. Parses mentions, hashtags, custom emoji, media embeds, and nostr: references. Use for any event body text — never render `event.content` as a raw string.
- **`CustomEmoji`** (`src/components/nostr/CustomEmoji.tsx`): Renders NIP-30 custom emoji images inline. Shows shortcode tooltip, handles load errors gracefully.

View File

@@ -21,10 +21,9 @@ import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
function rankColor(rank: number) {
if (rank >= 70) return { indicator: "bg-green-600", text: "text-green-600" };
if (rank >= 40)
return { indicator: "bg-yellow-600", text: "text-yellow-600" };
return { indicator: "bg-red-600", text: "text-red-600" };
if (rank >= 70) return { indicator: "bg-success", text: "text-success" };
if (rank >= 40) return { indicator: "bg-warning", text: "text-warning" };
return { indicator: "bg-destructive", text: "text-destructive" };
}
/**

View File

@@ -3,7 +3,7 @@ import {
BaseEventContainer,
ClickableEventTitle,
} from "./BaseEventRenderer";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import { UserName } from "../UserName";
import { ExternalIdentifierInline } from "../ExternalIdentifierDisplay";
import {
@@ -20,10 +20,9 @@ import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
function rankColor(rank: number) {
if (rank >= 70) return { indicator: "bg-green-600", text: "text-green-600" };
if (rank >= 40)
return { indicator: "bg-yellow-600", text: "text-yellow-600" };
return { indicator: "bg-red-600", text: "text-red-600" };
if (rank >= 70) return { indicator: "bg-success", text: "text-success" };
if (rank >= 40) return { indicator: "bg-warning", text: "text-warning" };
return { indicator: "bg-destructive", text: "text-destructive" };
}
/**
@@ -122,6 +121,11 @@ function MetricsPreview({
const tags = getAssertionTags(event);
const rankTag = tags.find((t) => t.name === "rank");
// Collect metric type labels for the tag row
const metricLabels = tags
.filter((t) => t.name !== "rank" && t.name !== "t")
.map((t) => ASSERTION_TAG_LABELS[t.name] || t.name);
let summaryMetrics: { label: string; value: string; unit?: string }[] = [];
if (event.kind === 30382) {
@@ -192,6 +196,15 @@ function MetricsPreview({
{/* Rank bar */}
{rankTag && <RankBar rank={parseInt(rankTag.value, 10)} />}
{/* Metric type labels */}
{metricLabels.length > 0 && (
<div className="flex flex-wrap gap-1">
{metricLabels.map((l) => (
<Label key={l}>{l}</Label>
))}
</div>
)}
{/* Summary metrics */}
{summaryMetrics.length > 0 && (
<div className="flex flex-wrap gap-x-4 gap-y-1">
@@ -221,14 +234,9 @@ export function TrustedAssertionRenderer({ event }: BaseEventProps) {
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
{/* Kind badge + subject as title */}
{/* Kind label + subject as title */}
<div className="flex flex-col gap-1">
<Badge
variant="outline"
className="w-fit gap-1 h-5 px-1.5 text-muted-foreground"
>
{kindLabel}
</Badge>
<Label className="w-fit">{kindLabel}</Label>
{subject && <SubjectTitle event={event} subject={subject} />}
</div>