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 committed 2026-08-14 10:49:44 +07:00
1 parent 5816e94a63
commit 5bbf75a65b
130 files changed
+4437 -12984

No files matched your search

@@ -0,0 +1,83 @@
"use client";
import { motion, useReducedMotion } from "motion/react";
import { useId } from "react";
export interface AreaPoint {
label: string;
value: number;
}
export interface AreaActivityProps {
data: AreaPoint[];
height?: number;
stroke?: string;
className?: string;
label?: string;
}
export function AreaActivity({
data,
height = 160,
stroke = "var(--color-signal)",
className,
label,
}: AreaActivityProps) {
const id = useId().replace(/:/g, "");
const reduce = useReducedMotion();
const width = 600;
if (data.length === 0)
return <div className={className} style={{ height }} />;
const max = Math.max(...data.map((d) => d.value), 1);
const stepX = width / Math.max(data.length - 1, 1);
const pts = data.map((d, i) => {
const x = i * stepX;
const y = height - (d.value / max) * (height - 10) - 5;
return [x, y] as const;
});
const line = pts
.map(
(p, i) => `${i === 0 ? "M" : "L"}${p[0].toFixed(1)},${p[1].toFixed(1)}`,
)
.join(" ");
const area = `${line} L${width},${height} L0,${height} Z`;
const pathLen = 1400;
return (
<svg
viewBox={`0 0 ${width} ${height}`}
className={className}
style={{ width: "100%", height }}
preserveAspectRatio="none"
role="img"
aria-label={label ?? "Activity chart"}
>
<defs>
<linearGradient id={`area-${id}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={stroke} stopOpacity="0.32" />
<stop offset="100%" stopColor={stroke} stopOpacity="0.02" />
</linearGradient>
</defs>
<motion.path
d={area}
fill={`url(#area-${id})`}
initial={reduce ? false : { pathLength: 0, opacity: 0.4 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 0.9, ease: [0.22, 1, 0.36, 1] }}
/>
<motion.path
d={line}
fill="none"
stroke={stroke}
strokeWidth={2}
strokeLinejoin="round"
strokeLinecap="round"
initial={reduce ? false : { pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.9, ease: [0.22, 1, 0.36, 1] }}
style={{ strokeDasharray: pathLen }}
/>
</svg>
);
}
@@ -0,0 +1,89 @@
"use client";
import { motion, useReducedMotion } from "motion/react";
import { useId } from "react";
export interface RadialGaugeProps {
/** 0..1 health ratio */
value: number;
size?: number;
label?: string;
sublabel?: string;
tone?: "signal" | "amber" | "vermilion";
}
const toneColor = {
signal: "var(--color-signal)",
amber: "var(--color-amber)",
vermilion: "var(--color-vermilion)",
};
export function RadialGauge({
value,
size = 160,
label,
sublabel,
tone = "signal",
}: RadialGaugeProps) {
const id = useId().replace(/:/g, "");
const reduce = useReducedMotion();
const stroke = 12;
const r = (size - stroke) / 2;
const c = 2 * Math.PI * r;
const pct = Math.max(0, Math.min(1, value));
const dash = c * pct;
return (
<div
className="relative inline-flex items-center justify-center"
style={{ width: size, height: size }}
>
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
className="-rotate-90"
>
<circle
cx={size / 2}
cy={size / 2}
r={r}
fill="none"
stroke="var(--color-hairline)"
strokeWidth={stroke}
/>
<motion.circle
cx={size / 2}
cy={size / 2}
r={r}
fill="none"
stroke={toneColor[tone]}
strokeWidth={stroke}
strokeLinecap="round"
strokeDasharray={c}
initial={reduce ? false : { strokeDashoffset: c }}
animate={{ strokeDashoffset: c - dash }}
transition={{ duration: 1, ease: [0.22, 1, 0.36, 1] }}
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center text-center">
<span
className="display text-2xl mono"
style={{ color: toneColor[tone] }}
>
{Math.round(pct * 100)}%
</span>
{label && (
<span className="text-[11px] font-medium text-[var(--color-ink-soft)] uppercase tracking-wide">
{label}
</span>
)}
{sublabel && (
<span className="text-[10px] text-[var(--color-ink-soft)]/70">
{sublabel}
</span>
)}
</div>
</div>
);
}
@@ -0,0 +1,56 @@
"use client";
import { cn } from "@/lib/utils";
export interface RibbonSegment {
id: string;
label: string;
value: number; // relative duration
tone?: "signal" | "amber" | "vermilion" | "neutral";
}
const toneClass = {
signal: "bg-[var(--color-signal)]",
amber: "bg-[var(--color-amber)]",
vermilion: "bg-[var(--color-vermilion)]",
neutral: "bg-[var(--color-ink-soft)]/40",
};
export interface SessionRibbonProps {
segments: RibbonSegment[];
className?: string;
height?: number;
}
export function SessionRibbon({
segments,
className,
height = 28,
}: SessionRibbonProps) {
const total = segments.reduce((s, x) => s + x.value, 0) || 1;
return (
<div
className={cn(
"flex w-full gap-0.5 overflow-hidden rounded-[var(--radius-r-control)]",
className,
)}
style={{ height }}
>
{segments.map((s) => (
<div
key={s.id}
className={cn(
"group relative flex items-center justify-center rounded-sm transition-all",
toneClass[s.tone ?? "signal"],
)}
style={{ width: `${(s.value / total) * 100}%` }}
title={`${s.label}: ${s.value}`}
>
<span className="pointer-events-none absolute inset-x-0 -top-6 hidden whitespace-nowrap rounded bg-[var(--color-ink)] px-1.5 py-0.5 text-[10px] text-[var(--color-canvas)] group-hover:block">
{s.label}
</span>
</div>
))}
</div>
);
}
@@ -0,0 +1,67 @@
"use client";
import { useId } from "react";
export interface SparklineProps {
data: number[];
width?: number;
height?: number;
stroke?: string;
className?: string;
fill?: boolean;
}
export function Sparkline({
data,
width = 120,
height = 36,
stroke = "var(--color-signal)",
className,
fill = true,
}: SparklineProps) {
const id = useId().replace(/:/g, "");
if (data.length < 2)
return <svg width={width} height={height} className={className} />;
const min = Math.min(...data);
const max = Math.max(...data);
const span = max - min || 1;
const stepX = width / (data.length - 1);
const pts = data.map((v, i) => {
const x = i * stepX;
const y = height - ((v - min) / span) * (height - 4) - 2;
return [x, y] as const;
});
const line = pts
.map(
(p, i) => `${i === 0 ? "M" : "L"}${p[0].toFixed(1)},${p[1].toFixed(1)}`,
)
.join(" ");
const area = `${line} L${width},${height} L0,${height} Z`;
return (
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
className={className}
preserveAspectRatio="none"
>
<defs>
<linearGradient id={`spark-${id}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={stroke} stopOpacity="0.28" />
<stop offset="100%" stopColor={stroke} stopOpacity="0" />
</linearGradient>
</defs>
{fill && <path d={area} fill={`url(#spark-${id})`} />}
<path
d={line}
fill="none"
stroke={stroke}
strokeWidth={1.6}
strokeLinejoin="round"
strokeLinecap="round"
/>
</svg>
);
}
@@ -0,0 +1,76 @@
"use client";
import { motion, useReducedMotion } from "motion/react";
import { useMemo } from "react";
import { cn } from "@/lib/utils";
export interface WaveformProps {
seed: string | number;
bars?: number;
height?: number;
className?: string;
tone?: "signal" | "amber" | "vermilion";
}
// deterministic pseudo-random from seed so the shape is stable per recording
function hashSeed(seed: string | number): number {
const s = String(seed);
let h = 2166136261;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
export function Waveform({
seed,
bars = 40,
height = 40,
className,
tone = "signal",
}: WaveformProps) {
const reduce = useReducedMotion();
const values = useMemo(() => {
let state = hashSeed(seed) || 1;
const out: number[] = [];
for (let i = 0; i < bars; i++) {
state = (Math.imul(state, 1103515245) + 12345) >>> 0;
const r = (state % 1000) / 1000;
// envelope: louder in the middle, quieter at edges
const env = Math.sin((i / (bars - 1)) * Math.PI);
out.push(0.18 + r * 0.82 * (0.4 + env * 0.6));
}
return out;
}, [seed, bars]);
const color = {
signal: "var(--color-signal)",
amber: "var(--color-amber)",
vermilion: "var(--color-vermilion)",
}[tone];
return (
<div
className={cn("flex items-end gap-[2px]", className)}
style={{ height }}
aria-hidden
>
{values.map((v, i) => (
<motion.span
key={i}
className="flex-1 rounded-[2px]"
style={{ background: color, height: `${Math.max(8, v * 100)}%` }}
initial={reduce ? false : { scaleY: 0.2, opacity: 0 }}
animate={{ scaleY: 1, opacity: 1 }}
whileHover={{ scaleY: 1.15 }}
transition={{
duration: 0.3,
delay: reduce ? 0 : i * 0.006,
ease: [0.22, 1, 0.36, 1],
}}
/>
))}
</div>
);
}