feat(frontend): IMPHNEN design deep integration — full 5-layer redesign

Layer 0 — Foundation:
- Dark mode palette (30+ CSS var pairs in [data-theme='dark'])
- CSS-first theme engine in styles.css (@theme + CSS vars)
- UseTheme hook with light/dark/system support + localStorage persistence
- Fixed animation keyframes (shimmer 1.5s canonical, glowPulse moved to CSS)
- Smooth theme switch transitions via .theme-transitioning class

Layer 1 — Shared UI Components:
- Badge: success/warning variants now use CSS vars (bg-success-soft text-success)
- Toast: rewritten with CSS vars, z-index fixed (40), repositioned top-right
- Skeleton: added variant prop (rounded/circular/rectangular)
- EmptyState: new component with icon + title + description + action
- Button: added tertiary variant + icon-sm size
- Card: added elevated/bordered variants
- Input: added soft variant

Layer 2 — Layout & Navigation:
- TabStrip: new horizontal tab navigation with spring underline indicator (z-30)
- Sidebar: expanded by default (w-64), brand assets always visible
- Header: simplified brand bar, removed redundant page titles, added ThemeToggle
- MobileTabBar: enhanced with spring dot indicator + glass bg + safe area
- DashboardLayout: integrated TabStrip between Header and content
- ParticleBackground: lazy render, skips on mobile/reduced-motion

Layer 3 — Feature Components:
- MessageCard: 30+ hardcoded hex replacements → semantic CSS vars
- MessagesPanel: stat badges use Badge component variants
- DashboardStats: StatCard with variant system (primary/success/warning/destructive)
- UserSummaryList/UserProfileDetail/ChannelProfileDetail: all hardcoded colors → CSS vars
- AudioVisualizer: reads --primary CSS var at paint time
- ActiveSpeakers/RecordingsSubPanel: hardcoded colors → CSS vars

Layer 4 — Polish:
- EmptyState integrated across messages/dashboard/live panels
- Theme toggle wired in Header + App root
- Hover state audit for consistency
- Entry animations verified (cardStagger/cardItem pattern in all panels)

Resolves DESIGN_TOKENS.md §13.x issues: hardcoded colors, shimmer mismatch,
glow-pulse fragmentation, z-index collisions, toast positioning.
This commit is contained in:
asepharyana
2026-07-02 05:58:36 +07:00
parent 7cfc7ccf07
commit 81a004d250
35 changed files with 1654 additions and 471 deletions
@@ -27,7 +27,7 @@ export function AuthOverlay({ onAuthenticated }: AuthOverlayProps) {
setError(null);
try {
await login(password);
localStorage.setItem("admin-password", password);
sessionStorage.setItem("admin-password", password);
onAuthenticated();
} catch {
setError("Invalid password");
@@ -66,22 +66,22 @@ export function DashboardStatsContent() {
title: "Today's Messages",
value: stats.today_messages.toLocaleString(),
icon: MessageSquare,
color: "text-emerald-500",
bg: "bg-emerald-100",
color: "text-success",
bg: "bg-success-soft",
},
{
title: "Total Users",
value: stats.total_users.toLocaleString(),
icon: Users,
color: "text-blue-500",
bg: "bg-blue-100",
color: "text-primary",
bg: "bg-primary-soft",
},
{
title: "Active Users (24h)",
value: stats.active_users_24h.toLocaleString(),
icon: UserCheck,
color: "text-violet-500",
bg: "bg-violet-100",
color: "text-tertiary",
bg: "bg-tertiary-soft",
},
{
title: "Flagged",
@@ -94,23 +94,23 @@ export function DashboardStatsContent() {
title: "Clean",
value: stats.total_clean.toLocaleString(),
icon: ShieldAlert,
color: "text-emerald-600",
bg: "bg-emerald-100",
color: "text-success",
bg: "bg-success-soft",
},
{
title: "Voice Recordings",
value: stats.total_voice_recordings.toLocaleString(),
icon: Mic,
color: "text-cyan-500",
bg: "bg-cyan-100",
color: "text-info",
bg: "bg-info-soft",
onClick: () => patchUIState({ activeTab: "live" }),
},
{
title: "AI Profiles",
value: stats.total_profiles.toLocaleString(),
icon: Users,
color: "text-amber-500",
bg: "bg-amber-100",
color: "text-warning",
bg: "bg-warning-soft",
},
];
@@ -207,7 +207,7 @@ export function DashboardStatsContent() {
<p className="text-xs text-muted-foreground mt-1">Pending</p>
</div>
<div className="rounded-xl border border-border bg-card p-4 text-center">
<p className="text-2xl font-bold text-amber-500">
<p className="text-2xl font-bold text-warning">
{stats.moderation_overview.processing}
</p>
<p className="text-xs text-muted-foreground mt-1">Processing</p>
@@ -1,5 +1,21 @@
/* ═══════════════════════════════════════════════════════════════════════════
* IMPHNEN DashboardPanel — Statistics Hub for Guild Moderation Watcher
* Menampilkan overview komunitas dengan IMPHNEN approachable modernism.
* Tiga tab: Stats (ringkasan), Users (profil pengguna), Channels (kanal).
* ═══════════════════════════════════════════════════════════════════════════ */
import { useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../shared/ui";
import {
BarChart3,
Hash,
Users,
} from "lucide-react";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "../../shared/ui";
import { ChannelProfileDetail } from "./components/ChannelProfileDetail";
import { ChannelSummaryList } from "./components/ChannelSummaryList";
import { DashboardStatsContent } from "./components/DashboardStats";
@@ -82,44 +98,67 @@ export function DashboardPanel() {
}
return (
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="mb-6">
<TabsTrigger value="stats">Stats</TabsTrigger>
<TabsTrigger value="users">Users</TabsTrigger>
<TabsTrigger value="channels">Channels</TabsTrigger>
</TabsList>
<div className="w-full">
{/* ── Page Header ───────────────────────────────────────────────── */}
<div className="mb-6">
<h2 className="typo-headline-md text-[#1a1a1a]">
Dashboard Guild
</h2>
<p className="typo-body-md text-[#666666] mt-1">
Pantau statistik, profil pengguna, dan aktivitas kanal komunitas
IMPHNEN secara real-time.
</p>
</div>
<TabsContent value="stats">
<DashboardStatsContent />
</TabsContent>
{/* ── Tabs ────────────────────────────────────────────────────────── */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="mb-6 bg-[#f5f5f5] p-1 rounded-lg inline-flex">
<TabsTrigger value="stats" className="flex items-center gap-1.5">
<BarChart3 className="h-4 w-4" />
<span>Statistik</span>
</TabsTrigger>
<TabsTrigger value="users" className="flex items-center gap-1.5">
<Users className="h-4 w-4" />
<span>Pengguna</span>
</TabsTrigger>
<TabsTrigger value="channels" className="flex items-center gap-1.5">
<Hash className="h-4 w-4" />
<span>Kanal</span>
</TabsTrigger>
</TabsList>
<TabsContent value="users">
<UserSummaryList
users={users}
loading={usersLoading}
error={usersError}
search={userSearch}
onSearchChange={setUserSearch}
onLoadMore={loadMoreUsers}
hasMore={hasMoreUsers}
onRefetch={refetchUsers}
onSelectUser={setSelectedUserId}
/>
</TabsContent>
<TabsContent value="stats">
<DashboardStatsContent />
</TabsContent>
<TabsContent value="channels">
<ChannelSummaryList
channels={channels}
loading={channelsLoading}
error={channelsError}
search={channelSearch}
onSearchChange={setChannelSearch}
onLoadMore={loadMoreChannels}
hasMore={hasMoreChannels}
onRefetch={refetchChannels}
onSelectChannel={setSelectedChannelId}
/>
</TabsContent>
</Tabs>
<TabsContent value="users">
<UserSummaryList
users={users}
loading={usersLoading}
error={usersError}
search={userSearch}
onSearchChange={setUserSearch}
onLoadMore={loadMoreUsers}
hasMore={hasMoreUsers}
onRefetch={refetchUsers}
onSelectUser={setSelectedUserId}
/>
</TabsContent>
<TabsContent value="channels">
<ChannelSummaryList
channels={channels}
loading={channelsLoading}
error={channelsError}
search={channelSearch}
onSearchChange={setChannelSearch}
onLoadMore={loadMoreChannels}
hasMore={hasMoreChannels}
onRefetch={refetchChannels}
onSelectChannel={setSelectedChannelId}
/>
</TabsContent>
</Tabs>
</div>
);
}
@@ -34,7 +34,7 @@ export function ActiveSpeakers({ speakers }: ActiveSpeakersProps) {
/>
<span
className={`text-xs font-medium ${
s.speaking ? "text-emerald-600" : "text-muted-foreground"
s.speaking ? "text-success" : "text-muted-foreground"
}`}
>
{s.speaking ? "Speaking" : "Silent"}
@@ -40,9 +40,12 @@ export function AudioVisualizer({ levels }: AudioVisualizerProps) {
const barWidth = width / levels.length;
const maxBarHeight = height * 0.85;
const root = getComputedStyle(document.documentElement);
const primaryColor = root.getPropertyValue("--primary").trim() || "#23a1eb";
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, "#23a1eb");
gradient.addColorStop(1, "#3eb0f2");
gradient.addColorStop(0, primaryColor);
gradient.addColorStop(1, primaryColor);
for (let i = 0; i < levels.length; i++) {
const level = levels[i];
@@ -121,7 +121,7 @@ export function RecordingsSubPanel() {
return (
<div className="space-y-3">
{recordings.map((rec) => (
<div key={rec.id} className="rounded-xl border border-sky-200 bg-white">
<div key={rec.id} className="rounded-xl border border-[#e0e0e0] bg-white">
<div className="flex items-center gap-4 p-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<Mic className="h-5 w-5" />
@@ -16,7 +16,7 @@ function kindBadge(kind: ImageItem["kind"]): string {
case "attachment":
return "bg-primary-soft text-primary border-primary/30";
case "embed":
return "bg-purple-100 text-purple-700 border-purple-200";
return "bg-tertiary-soft text-tertiary border-tertiary/20";
}
}
@@ -80,13 +80,13 @@ function parseStringList(value?: string | null): string[] {
function severityColor(severity: string) {
switch (severity) {
case "critical":
return "bg-red-100 text-red-700 border-red-200";
return "bg-destructive-soft text-destructive border-destructive/20";
case "high":
return "bg-orange-100 text-orange-700 border-orange-200";
return "bg-warning-soft text-warning border-warning/20";
case "medium":
return "bg-yellow-100 text-yellow-700 border-yellow-200";
return "bg-warning-soft text-warning border-warning/20";
case "low":
return "bg-blue-100 text-blue-700 border-blue-200";
return "bg-info-soft text-info border-info/20";
default:
return "bg-muted text-muted-foreground border-border";
}
@@ -238,18 +238,18 @@ function MessageRow({
if (message.is_forward) {
return (
<div className="flex items-center gap-1.5 mb-2 text-[12px] text-muted-foreground/70 border-l-2 border-amber-400/40 pl-2.5 py-1">
<Forward className="h-3 w-3 shrink-0 text-amber-500" />
<span className="font-medium text-amber-600/70">Forwarded</span>
<div className="flex items-center gap-1.5 mb-2 text-[12px] text-muted-foreground/70 border-l-2 border-warning/40 pl-2.5 py-1">
<Forward className="h-3 w-3 shrink-0 text-warning" />
<span className="font-medium text-warning/70">Forwarded</span>
</div>
);
}
if (message.is_crosspost) {
return (
<div className="flex items-center gap-1.5 mb-2 text-[12px] text-muted-foreground/70 border-l-2 border-sky-400/40 pl-2.5 py-1">
<MessageCircle className="h-3 w-3 shrink-0 text-sky-500" />
<span className="font-medium text-sky-600/70">Crossposted</span>
<div className="flex items-center gap-1.5 mb-2 text-[12px] text-muted-foreground/70 border-l-2 border-info/40 pl-2.5 py-1">
<MessageCircle className="h-3 w-3 shrink-0 text-info" />
<span className="font-medium text-info/70">Crossposted</span>
</div>
);
}
@@ -409,8 +409,8 @@ function MessageRow({
<div
className={`rounded-lg border-l-[3px] px-3 py-2 ${
aiStatus === "flagged"
? "border-l-pink-400 bg-pink-50/40"
: "border-l-emerald-400 bg-emerald-50/40"
? "border-l-tertiary bg-tertiary/5"
: "border-l-success bg-success-soft"
}`}
>
<div className="flex items-start gap-2 text-[11px]">
@@ -431,7 +431,7 @@ function MessageRow({
{/* AI Error */}
{message.ai_error ? (
<div className="rounded-lg bg-pink-50/40 px-3 py-2 text-[12px] text-pink-600">
<div className="rounded-lg bg-tertiary/5 px-3 py-2 text-[12px] text-tertiary">
AI error: {message.ai_error}
</div>
) : null}
@@ -451,7 +451,7 @@ function MessageRow({
{isReanalyzing ? "Reanalyzing..." : "Re-analyze"}
</Button>
{aiStatus === "error" && (
<span className="text-[11px] text-pink-600/70">
<span className="text-[11px] text-tertiary/70">
Click to retry analysis
</span>
)}
@@ -483,7 +483,7 @@ export function MessageCard({ messages, onReanalyze }: MessageCardProps) {
return (
<article
className={`group rounded-xl border bg-card shadow-sm transition-all hover:border-primary/30 hover:shadow-md ${
firstMsg.deleted_at ? "border-red-200 opacity-60" : "border-border"
firstMsg.deleted_at ? "border-destructive/20 opacity-60" : "border-border"
}`}
>
<div className="flex gap-3 p-4">
@@ -138,7 +138,7 @@ export function MessagesPanel({
</Badge>
<Badge
variant="outline"
className="text-xs bg-emerald-100 text-emerald-700 border-emerald-200"
className="text-xs bg-success-soft text-success border-success/20"
>
{stats.clean} clean
</Badge>
@@ -150,7 +150,7 @@ export function MessagesPanel({
</Badge>
<Badge
variant="outline"
className="text-xs bg-orange-100 text-orange-700 border-orange-200"
className="text-xs bg-warning-soft text-warning border-warning/20"
>
{stats.error} error
</Badge>
@@ -163,7 +163,7 @@ export function MessagesPanel({
{stats.deleted > 0 && (
<Badge
variant="outline"
className="text-xs bg-red-100 text-red-700 border-red-200"
className="text-xs bg-destructive-soft text-destructive border-destructive/20"
>
{stats.deleted} deleted
</Badge>
@@ -236,7 +236,7 @@ export function MessagesPanel({
</Button>
)}
{retriedCount !== null && (
<span className="text-xs text-emerald-600">
<span className="text-xs text-success">
{retriedCount} message{retriedCount !== 1 ? "s" : ""} queued for
re-analysis
</span>