refactor(frontend): finish shadcn→custom primitive migration (green build)

- Remove tw-animate-css import + dead src/components/ui shadcn tree
- Convert 7 orphaned components (moderation, analysis, guild-selector,
  voice/activity-timeline, shared/empty+error) to new primitives
- Add missing moderation/view.tsx; analysis uses SearchPanel directly
- globals.css now uses new signal-driven ops-console tokens
- tsc --noEmit clean, next build green (11 routes), local smoke 200
This commit is contained in:
asepharyana
2026-08-14 10:49:44 +07:00
parent 5816e94a63
commit 5bbf75a65b
130 changed files with 4431 additions and 12978 deletions
@@ -1,69 +1,35 @@
"use client";
import { ImageIcon } from "lucide-react";
import type { AttachmentRecord } from "@/lib/types";
interface AttachmentsGridProps {
attachments: AttachmentRecord[];
onImageClick?: (index: number) => void;
}
import type { AttachmentRef } from "@/lib/types";
import { cn } from "@/lib/utils";
export function AttachmentsGrid({
attachments,
onImageClick,
}: AttachmentsGridProps) {
onOpen,
}: {
attachments: AttachmentRef[];
onOpen: (url: string) => void;
}) {
if (attachments.length === 0) return null;
const images = attachments.filter((a) => a.type?.startsWith("image/"));
const others = attachments.filter((a) => !a.type?.startsWith("image/"));
const images = attachments.filter((a) => /image/i.test(a.contentType ?? ""));
if (images.length === 0) return null;
return (
<div className="space-y-2">
{images.length > 0 && (
<div className="grid grid-cols-2 gap-2">
{images.map((att, i) => (
<div
key={att.id}
className="glass relative overflow-hidden rounded-lg group"
>
<button
type="button"
onClick={() => onImageClick?.(i)}
className="block w-full cursor-zoom-in"
aria-label={`Open ${att.filename}`}
>
<img
src={att.uploaded_url || att.discord_url}
alt={att.filename}
className="h-32 w-full object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy"
decoding="async"
/>
</button>
{images.length > 1 && (
<span className="absolute bottom-1 right-1 rounded bg-black/50 px-1.5 py-0.5 font-mono text-[10px] text-white/80">
{i + 1}/{images.length}
</span>
)}
</div>
))}
</div>
)}
{others.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{others.map((att) => (
<div
key={att.id}
className="flex items-center gap-1.5 rounded-md bg-glass-bg px-2 py-1 text-xs text-text-secondary"
>
<ImageIcon className="size-3 text-text-secondary/50" />
<span className="font-mono max-w-40 truncate">
{att.filename}
</span>
</div>
))}
</div>
)}
<div className="mt-2 grid grid-cols-2 gap-2 sm:grid-cols-3">
{images.map((a, i) => (
<button
key={`${a.url}-${i}`}
type="button"
onClick={() => onOpen(a.url)}
className="group relative aspect-video overflow-hidden rounded-[var(--radius-r-control)] border border-[var(--color-hairline)]"
>
<img
src={a.url}
alt={a.name}
loading="lazy"
className="size-full object-cover transition-transform duration-200 group-hover:scale-105"
/>
</button>
))}
</div>
);
}