From f616ec36d9387144cba31b943256ec288af93a90 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 28 Jul 2026 07:53:35 +0700 Subject: [PATCH 01/15] feat: add design tokens, glass utilities, and ambient animations Co-Authored-By: Claude Opus 4.8 (1M context) --- services/frontend/src/app/globals.css | 334 ++++++++++---------------- 1 file changed, 121 insertions(+), 213 deletions(-) diff --git a/services/frontend/src/app/globals.css b/services/frontend/src/app/globals.css index 1bab1b2..bbf651b 100644 --- a/services/frontend/src/app/globals.css +++ b/services/frontend/src/app/globals.css @@ -5,154 +5,75 @@ @custom-variant dark (&:is(.dark *)); @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-sans); - --font-mono: var(--font-geist-mono); - --font-heading: var(--font-sans); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --color-warning: var(--warning); - --color-warning-foreground: var(--warning-foreground); - --radius-sm: calc(var(--radius) * 0.6); - --radius-md: calc(var(--radius) * 0.8); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) * 1.4); - --radius-2xl: calc(var(--radius) * 1.8); - --radius-3xl: calc(var(--radius) * 2.2); - --radius-4xl: calc(var(--radius) * 2.6); + /* Canvas — deep navy */ + --color-canvas: oklch(0.07 0.015 250); + --color-surface: oklch(0.11 0.02 245 / 0.6); + --color-surface-hover: oklch(0.15 0.02 245 / 0.7); - /* Teal-cyan accent gradient for monitoring hub look */ - --accent-gradient: linear-gradient(135deg, oklch(0.62 0.17 215), oklch(0.6 0.15 195), oklch(0.65 0.12 185)); - --accent-gradient-subtle: linear-gradient(135deg, oklch(0.62 0.17 215 / 0.15), oklch(0.65 0.12 185 / 0.05)); + /* Glass */ + --color-glass-bg: oklch(1 0 0 / 0.04); + --color-glass-border: oklch(1 0 0 / 0.08); + --glass-shadow: 0 8px 32px oklch(0 0 0 / 0.4); - /* Glass morphism */ - --glass-bg: oklch(1 0 0 / 0.05); - --glass-border: oklch(1 0 0 / 0.1); - --glass-shadow: 0 8px 32px oklch(0 0 0 / 0.3); + /* Primary — teal-cyan */ + --color-primary: oklch(0.62 0.17 215); + --color-primary-glow: oklch(0.62 0.17 215 / 0.4); + --color-primary-foreground: oklch(0.98 0 0); + --color-border: oklch(1 0 0 / 0.06); + --color-border-glow: oklch(0.62 0.17 215 / 0.3); + + /* Accents */ + --color-accent-purple: oklch(0.65 0.2 280); + --color-accent-amber: oklch(0.7 0.17 75); + --color-destructive: oklch(0.577 0.245 27.325); + --color-success: oklch(0.6 0.18 160); + + /* Text */ + --color-text-primary: oklch(0.93 0.01 245); + --color-text-secondary: oklch(0.55 0.02 245); + --color-text-mono: oklch(0.62 0.17 215); + + /* Legacy overrides for shadcn compatibility */ + --color-background: var(--color-canvas); + --color-foreground: var(--color-text-primary); + --color-card: var(--color-surface); + --color-card-foreground: var(--color-text-primary); + --color-muted: oklch(0.17 0.015 245); + --color-muted-foreground: var(--color-text-secondary); + --color-accent: var(--color-primary); + --color-accent-foreground: var(--color-primary-foreground); + + /* Radius */ + --radius-card: 16px; + --radius-panel: 12px; + --radius-control: 8px; + --radius-pill: 9999px; + --radius: 0.625rem; /* shadcn compat */ + + /* Fonts */ + --font-sans: "Inter", sans-serif; + --font-mono: "JetBrains Mono", monospace; } -:root { - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.55 0.18 240); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.95 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.65 0.15 220); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --warning: oklch(0.7 0.18 75); - --warning-foreground: oklch(0.98 0 0); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.55 0.18 240); - --chart-1: oklch(0.55 0.18 240); - --chart-2: oklch(0.55 0.15 200); - --chart-3: oklch(0.55 0.12 180); - --chart-4: oklch(0.55 0.2 260); - --chart-5: oklch(0.55 0.15 280); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.55 0.18 240); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.95 0 0); - --sidebar-accent-foreground: oklch(0.145 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.55 0.18 240); - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); -} - -.dark { - /* Deeper navy canvas — monitoring hub feel */ - --background: oklch(0.09 0.015 245); - --foreground: oklch(0.93 0.01 245); - - /* Card surface with subtle separation */ - --card: oklch(0.13 0.02 245); - --card-foreground: oklch(0.93 0.01 245); - - --popover: oklch(0.13 0.02 245); - --popover-foreground: oklch(0.93 0.01 245); - - /* Teal-cyan primary */ - --primary: oklch(0.62 0.17 215); - --primary-foreground: oklch(0.98 0 0); - - --secondary: oklch(0.2 0.015 245); - --secondary-foreground: oklch(0.93 0.01 245); - - --muted: oklch(0.17 0.015 245); - --muted-foreground: oklch(0.55 0.02 245); - - /* Electric blue-purple accent */ - --accent: oklch(0.7 0.18 260); - --accent-foreground: oklch(0.98 0 0); - - --destructive: oklch(0.6 0.22 25); - - /* Amber-gold warning (distinct from red) */ - --warning: oklch(0.7 0.17 75); - --warning-foreground: oklch(0.12 0 0); - - --border: oklch(1 0 0 / 0.06); - --input: oklch(1 0 0 / 0.1); - - --ring: oklch(0.62 0.17 215); - - /* Teal-cyan chart palette */ - --chart-1: oklch(0.62 0.17 215); - --chart-2: oklch(0.6 0.15 195); - --chart-3: oklch(0.65 0.12 185); - --chart-4: oklch(0.7 0.18 260); - --chart-5: oklch(0.55 0.15 280); - - /* Deeper sidebar with teal accent */ - --sidebar: oklch(0.075 0.01 245); - --sidebar-foreground: oklch(0.93 0.01 245); - --sidebar-primary: oklch(0.62 0.17 215); - --sidebar-primary-foreground: oklch(0.98 0 0); - --sidebar-accent: oklch(0.16 0.025 215); - --sidebar-accent-foreground: oklch(0.93 0.01 245); - --sidebar-border: oklch(1 0 0 / 0.04); - --sidebar-ring: oklch(0.62 0.17 215); - - /* Glass overrides for dark */ - --glass-bg: oklch(1 0 0 / 0.04); - --glass-border: oklch(1 0 0 / 0.08); +@layer utilities { + .glass { + background: var(--color-glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--color-glass-border); + box-shadow: var(--glass-shadow); + } + .glass-elevated { + background: var(--color-glass-bg); + backdrop-filter: blur(16px); + border: 1px solid var(--color-border-glow); + box-shadow: 0 8px 32px oklch(0 0 0 / 0.5), 0 0 20px var(--color-primary-glow); + } + .glass-intense { + background: oklch(1 0 0 / 0.08); + backdrop-filter: blur(20px); + border: 1px solid oklch(1 0 0 / 0.12); + } } @layer base { @@ -160,9 +81,12 @@ @apply border-border outline-ring/50; } body { - @apply bg-background text-foreground; - background-image: radial-gradient(circle, oklch(1 0 0 / 0.025) 1px, transparent 1px); - background-size: 24px 24px; + @apply bg-canvas text-text-primary font-sans antialiased; + background-image: + radial-gradient(circle, oklch(1 0 0 / 0.025) 1px, transparent 1px), + radial-gradient(ellipse 80% 50% at 50% -20%, oklch(0.62 0.17 215 / 0.06), transparent), + radial-gradient(ellipse 50% 40% at 80% 80%, oklch(0.65 0.2 280 / 0.04), transparent); + background-size: 24px 24px, 100% 100%, 100% 100%; } html { @apply font-sans scroll-smooth; @@ -170,12 +94,8 @@ /* Custom selection color */ ::selection { - background: oklch(0.62 0.17 215 / 0.3); - color: inherit; - } - - .dark ::selection { background: oklch(0.62 0.17 215 / 0.4); + color: inherit; } /* Scrollbar styling */ @@ -195,20 +115,58 @@ } } -/* ── Utility classes ─────────────────────────── */ +/* ── Animations ──────────────────────────── */ -/* Glass card effect */ -.glass { - background: var(--glass-bg); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--glass-border); - box-shadow: var(--glass-shadow); +@keyframes pulse-ring { + 0% { + transform: scale(0.8); + opacity: 1; + } + 100% { + transform: scale(2.5); + opacity: 0; + } } +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.animate-fade-in-up { + animation: fade-in-up 0.3s ease-out forwards; +} + +.animate-pulse-ring { + animation: pulse-ring 1.5s ease-out infinite; +} + +.animate-shimmer { + background: linear-gradient(90deg, transparent, oklch(0.62 0.17 215 / 0.08), transparent); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +/* ── Utility classes ─────────────────────── */ + /* Gradient text */ .text-gradient { - background: var(--accent-gradient); + background: linear-gradient(135deg, oklch(0.62 0.17 215), oklch(0.6 0.15 195), oklch(0.65 0.12 185)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -224,63 +182,13 @@ inset: 0; border-radius: inherit; padding: 1px; - background: var(--accent-gradient); + background: linear-gradient(135deg, oklch(0.62 0.17 215), oklch(0.6 0.15 195), oklch(0.65 0.12 185)); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask-composite: exclude; } -/* Animated background */ -@keyframes gradient-shift { - 0%, - 100% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } -} - -.animate-gradient { - background: linear-gradient( - -45deg, - oklch(0.65 0.18 240 / 0.1), - oklch(0.6 0.15 200 / 0.05), - oklch(0.12 0.02 240 / 1), - oklch(0.65 0.12 180 / 0.08) - ); - background-size: 400% 400%; - animation: gradient-shift 15s ease infinite; -} - -/* Counter animation placeholder - will be done in JS */ -@keyframes fade-in-up { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.animate-fade-in-up { - animation: fade-in-up 0.3s ease-out forwards; -} - -/* Pulse ring for live indicators */ -@keyframes pulse-ring { - 0% { - transform: scale(0.8); - opacity: 1; - } - 100% { - transform: scale(2.5); - opacity: 0; - } -} - +/* Live pulse ring (compat alias) */ .live-pulse-ring { animation: pulse-ring 1.5s ease-out infinite; } From 460acbc72b9e7d6ae7c0a3db4c18eb0c9282aabd Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 28 Jul 2026 08:02:45 +0700 Subject: [PATCH 02/15] feat: update root layout with new fonts and redirect to dashboard --- services/frontend/src/app/layout.tsx | 17 ++++++++--------- services/frontend/src/app/page.tsx | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/services/frontend/src/app/layout.tsx b/services/frontend/src/app/layout.tsx index 2620eec..de177bf 100644 --- a/services/frontend/src/app/layout.tsx +++ b/services/frontend/src/app/layout.tsx @@ -1,23 +1,22 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Inter, JetBrains_Mono } from "next/font/google"; import Script from "next/script"; import { Toaster } from "@/components/ui/sonner"; -import { TooltipProvider } from "@/components/ui/tooltip"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", +const inter = Inter({ subsets: ["latin"], + variable: "--font-inter", }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], + variable: "--font-jetbrains-mono", }); export const metadata: Metadata = { title: "Discord Automod — Moderation Dashboard", - description: "Live Discord monitoring and AI moderation dashboard", + description: "AI-powered Discord moderation and voice monitoring dashboard", }; export default function RootLayout({ @@ -28,7 +27,7 @@ export default function RootLayout({ return ( @@ -37,7 +36,7 @@ export default function RootLayout({ - {children} + {children} diff --git a/services/frontend/src/app/page.tsx b/services/frontend/src/app/page.tsx index c8618ae..c3a1c90 100644 --- a/services/frontend/src/app/page.tsx +++ b/services/frontend/src/app/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function RootPage() { - redirect("/messages"); + redirect("/dashboard"); } From 91ad054a41c0241321efcb9569462d57cf8c8c7f Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 28 Jul 2026 08:30:43 +0700 Subject: [PATCH 03/15] feat: add floating top navigation bar Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/layout/top-nav.tsx | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 services/frontend/src/components/layout/top-nav.tsx diff --git a/services/frontend/src/components/layout/top-nav.tsx b/services/frontend/src/components/layout/top-nav.tsx new file mode 100644 index 0000000..5776ca8 --- /dev/null +++ b/services/frontend/src/components/layout/top-nav.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import { Moon, Sun } from "lucide-react"; +import { useEffect, useState } from "react"; +import { navItems, isActivePath } from "@/lib/navigation"; + +export function TopNav() { + const pathname = usePathname(); + const router = useRouter(); + const [theme, setTheme] = useState<"light" | "dark">("dark"); + + useEffect(() => { + const stored = localStorage.getItem("theme") as "light" | "dark" | null; + if (stored) setTheme(stored); + }, []); + + const toggleTheme = () => { + const next = theme === "dark" ? "light" : "dark"; + setTheme(next); + localStorage.setItem("theme", next); + document.documentElement.classList.remove("light", "dark"); + document.documentElement.classList.add(next); + }; + + return ( +
+ {/* Brand */} +
+
+ D + +
+ + Discord Automod + +
+ + {/* Nav links */} + + + {/* Right side */} +
+ +
+
+ ); +} From a7cc70b5a3def78f0c3bb1f9bbf4ceb112dcf0f2 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 28 Jul 2026 08:53:54 +0700 Subject: [PATCH 04/15] feat: rewrite dashboard layout with top nav, hidden sidebar, mascot, mini-player --- .../frontend/src/app/(dashboard)/layout.tsx | 61 +++-- .../src/components/chatbot/chatbot.tsx | 227 ------------------ .../src/components/layout/app-header.tsx | 167 ------------- .../src/components/layout/app-sidebar.tsx | 101 -------- 4 files changed, 37 insertions(+), 519 deletions(-) delete mode 100644 services/frontend/src/components/chatbot/chatbot.tsx delete mode 100644 services/frontend/src/components/layout/app-header.tsx delete mode 100644 services/frontend/src/components/layout/app-sidebar.tsx diff --git a/services/frontend/src/app/(dashboard)/layout.tsx b/services/frontend/src/app/(dashboard)/layout.tsx index a1c5453..79990a6 100644 --- a/services/frontend/src/app/(dashboard)/layout.tsx +++ b/services/frontend/src/app/(dashboard)/layout.tsx @@ -2,17 +2,20 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Suspense } from "react"; - -import { Chatbot } from "@/components/chatbot/chatbot"; -import { AppHeader } from "@/components/layout/app-header"; -import { AppSidebar } from "@/components/layout/app-sidebar"; +import { TopNav } from "@/components/layout/top-nav"; import { MobileNav } from "@/components/layout/mobile-nav"; import { WsProvider } from "@/lib/ws/context"; +import { MascotProvider } from "@/components/mascot/mascot-context"; +import { MascotContainer } from "@/components/mascot/mascot-container"; +import { MiniPlayer } from "@/components/media/mini-player"; +import { MediaPlayerProvider } from "@/lib/hooks/use-media-player"; +import { HiddenSidebar } from "@/components/layout/hidden-sidebar"; +import { useState } from "react"; const queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: 15_000, + staleTime: 10_000, retry: 1, refetchOnWindowFocus: false, }, @@ -24,28 +27,38 @@ export default function DashboardLayout({ }: { children: React.ReactNode; }) { + const [guildId, setGuildId] = useState(""); + return ( -
- -
- -
- -
-
- } - > - {children} -
-
-
- -
- + + +
+ + setGuildId(g ?? "")} /> + + {/* Sub-nav space — filled per-page */} +
+
+ +
+
+ } + > + {children} +
+
+
+ + + + +
+
+
); diff --git a/services/frontend/src/components/chatbot/chatbot.tsx b/services/frontend/src/components/chatbot/chatbot.tsx deleted file mode 100644 index 5fabfd9..0000000 --- a/services/frontend/src/components/chatbot/chatbot.tsx +++ /dev/null @@ -1,227 +0,0 @@ -"use client"; - -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { - Bot, - Loader2, - MessageCircle, - Send, - Sparkles, - Trash2, - User, - X, -} from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { chatbotApi } from "@/lib/api"; -import type { ChatHistoryMessage } from "@/lib/types"; -import { cn } from "@/lib/utils"; - -export function Chatbot() { - const [open, setOpen] = useState(false); - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(""); - const scrollRef = useRef(null); - const qc = useQueryClient(); - - const { data: historyMessages = [] } = useQuery({ - queryKey: ["chatbot-history"], - queryFn: () => chatbotApi.getHistory(), - enabled: open, - }); - - useEffect(() => { - if (historyMessages.length > 0) setMessages(historyMessages); - }, [historyMessages]); - - const sendMut = useMutation({ - mutationFn: (text: string) => chatbotApi.send(text), - onSuccess: () => qc.invalidateQueries({ queryKey: ["chatbot-history"] }), - }); - - const clearMut = useMutation({ - mutationFn: () => chatbotApi.clearHistory(), - onSuccess: () => qc.setQueryData(["chatbot-history"], []), - }); - - useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, []); - - const handleClear = useCallback(() => { - clearMut.mutate(); - setMessages([]); - }, [clearMut]); - - const handleSend = useCallback(async () => { - if (!input.trim() || sendMut.isPending) return; - const text = input.trim(); - setInput(""); - - // Add optimistic user message - setMessages((prev) => [ - ...prev, - { role: "user", content: text, timestamp: new Date().toISOString() }, - ]); - - try { - const resp = await sendMut.mutateAsync(text); - setMessages((prev) => [ - ...prev, - { - role: "assistant", - content: resp.response, - timestamp: resp.timestamp, - }, - ]); - } catch { - setMessages((prev) => [ - ...prev, - { - role: "assistant", - content: "Sorry, I couldn't process that request.", - timestamp: new Date().toISOString(), - }, - ]); - } - }, [input, sendMut]); - - return ( - <> - {/* Toggle button */} - - - {/* Chat panel */} - {open && ( - - - -
- -
- Chatbot - -
- {messages.length > 0 && ( - - )} - - - - - -
- {messages.length === 0 && ( -

- Ask me anything about the server! -

- )} - {messages.map((msg) => ( -
- - - {msg.role === "user" ? ( - - ) : ( - - )} - - -
- {msg.content} -
-
- ))} - {sendMut.isPending && ( -
- - - - - -
- -
-
- )} -
-
-
- - -
{ - e.preventDefault(); - handleSend(); - }} - className="flex w-full gap-2" - > - setInput(e.target.value)} - disabled={sendMut.isPending} - className="h-8 flex-1" - /> - -
-
- - )} - - ); -} diff --git a/services/frontend/src/components/layout/app-header.tsx b/services/frontend/src/components/layout/app-header.tsx deleted file mode 100644 index 4c4e8fb..0000000 --- a/services/frontend/src/components/layout/app-header.tsx +++ /dev/null @@ -1,167 +0,0 @@ -"use client"; - -import { Moon, PanelLeft, Sun } from "lucide-react"; -import { usePathname } from "next/navigation"; -import { useEffect, useState } from "react"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { isActivePath, navItems } from "@/lib/navigation"; -import { cn } from "@/lib/utils"; -import { useWebSocket } from "@/lib/ws/context"; - -export function AppHeader() { - const pathname = usePathname(); - const { status } = useWebSocket(); - const [theme, setTheme] = useState<"light" | "dark">("dark"); - const [mobileOpen, setMobileOpen] = useState(false); - - useEffect(() => { - const stored = localStorage.getItem("theme") as "light" | "dark" | null; - if (stored) setTheme(stored); - }, []); - - const toggleTheme = () => { - const next = theme === "dark" ? "light" : "dark"; - setTheme(next); - localStorage.setItem("theme", next); - document.documentElement.classList.remove("light", "dark"); - document.documentElement.classList.add(next); - }; - - const pageTitle = - navItems - .filter((n) => isActivePath(pathname, n.matchPrefix)) - .map((n) => n.label) - .at(0) ?? "Dashboard"; - - const statusVariant = - status === "connected" - ? "default" - : status === "connecting" - ? "secondary" - : "destructive"; - - const statusLabel = - status === "connected" - ? "Connected" - : status === "connecting" - ? "Connecting" - : "Disconnected"; - - return ( - <> -
- {/* Mobile menu button */} - - -

{pageTitle}

- -
- - - - {statusLabel} - - - -
- - {/* Mobile overlay menu */} - {mobileOpen && ( -
- {/* biome-ignore lint/a11y/noStaticElementInteractions: overlay backdrop */} -
setMobileOpen(false)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") setMobileOpen(false); - }} - /> - -
- )} - - ); -} diff --git a/services/frontend/src/components/layout/app-sidebar.tsx b/services/frontend/src/components/layout/app-sidebar.tsx deleted file mode 100644 index 0306408..0000000 --- a/services/frontend/src/components/layout/app-sidebar.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client"; - -import { usePathname, useRouter } from "next/navigation"; - -import { isActivePath, navItems } from "@/lib/navigation"; -import { cn } from "@/lib/utils"; -import { useWebSocket } from "@/lib/ws/context"; - -export function AppSidebar() { - const pathname = usePathname(); - const router = useRouter(); - const { status } = useWebSocket(); - - const connectionDot = { - connected: - "bg-emerald-500 shadow-[0_0_8px] shadow-emerald-500/60 animate-pulse", - connecting: "bg-amber-400 animate-pulse", - disconnected: "bg-destructive", - error: "bg-destructive", - }[status]; - - const connectionLabel = { - connected: "Connected", - connecting: "Connecting", - disconnected: "Disconnected", - error: "Error", - }[status]; - - return ( - - ); -} From 7b517c388f5c97211c3312b1ea6baf342e1c0ff8 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 28 Jul 2026 09:07:39 +0700 Subject: [PATCH 05/15] feat: redesign mobile bottom nav with glass styling --- .../frontend/src/components/layout/mobile-nav.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/services/frontend/src/components/layout/mobile-nav.tsx b/services/frontend/src/components/layout/mobile-nav.tsx index f2aef98..e234b12 100644 --- a/services/frontend/src/components/layout/mobile-nav.tsx +++ b/services/frontend/src/components/layout/mobile-nav.tsx @@ -10,8 +10,8 @@ export function MobileNav() { const pathname = usePathname(); return ( -