From f0558aa0d0aeee81b5444990070748476e1922cf Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 23 Feb 2026 10:48:00 +0800 Subject: [PATCH] feat: implement dark mode support for admin layout and components - Add dark mode toggle component in admin header - Integrate dark mode store across admin layout and child components - Update header, judulList, and judulListTab components with theme tokens - Add unified typography components for consistent theming - Implement smooth transitions for dark/light mode switching - Add mounted state to prevent hydration mismatches - Style navbar with dark mode aware colors and hover states - Update button styles with gradient effects for both themes Co-authored-by: Qwen-Coder --- darkMode.md | 169 ++++++ src/app/admin/(dashboard)/_com/header.tsx | 25 +- src/app/admin/(dashboard)/_com/judulList.tsx | 20 +- .../admin/(dashboard)/_com/judulListTab.tsx | 44 +- src/app/admin/layout.tsx | 190 ++++-- src/components/admin/AdminThemeProvider.tsx | 119 ++++ src/components/admin/DarkModeToggle.tsx | 78 +++ .../admin/README_UNIFIED_STYLING.md | 546 ++++++++++++++++++ src/components/admin/UnifiedSurface.tsx | 252 ++++++++ src/components/admin/UnifiedTypography.tsx | 268 +++++++++ src/state/darkModeStore.ts | 76 +++ src/styles/dark-mode-table.css | 31 + src/utils/themeTokens.ts | 383 ++++++++++++ 13 files changed, 2139 insertions(+), 62 deletions(-) create mode 100644 darkMode.md create mode 100644 src/components/admin/AdminThemeProvider.tsx create mode 100644 src/components/admin/DarkModeToggle.tsx create mode 100644 src/components/admin/README_UNIFIED_STYLING.md create mode 100644 src/components/admin/UnifiedSurface.tsx create mode 100644 src/components/admin/UnifiedTypography.tsx create mode 100644 src/state/darkModeStore.ts create mode 100644 src/styles/dark-mode-table.css create mode 100644 src/utils/themeTokens.ts diff --git a/darkMode.md b/darkMode.md new file mode 100644 index 00000000..713ac5db --- /dev/null +++ b/darkMode.md @@ -0,0 +1,169 @@ +# 🌙 Dark Mode Design Specification +## Admin Darmasaba – Dashboard & CMS + +Dokumen ini mendefinisikan standar **Dark Mode UI** agar: +- nyaman di mata +- konsisten +- tidak flat +- tetap profesional untuk aplikasi pemerintahan + +--- + +## 🎨 Color Palette (Dark Mode) + +### Background Layers +| Layer | Token | Warna | Fungsi | +|------|------|------|------| +| Base | `--bg-base` | `#0B1220` | Background utama aplikasi | +| App | `--bg-app` | `#0F172A` | Area kerja utama | +| Card | `--bg-card` | `#162235` | Card / container | +| Surface | `--bg-surface` | `#1E2A3D` | Table header, tab, input | + +--- + +### Border & Divider +| Token | Warna | Catatan | +|-----|------|--------| +| `--border-default` | `#2A3A52` | Border utama | +| `--border-soft` | `#22314A` | Divider halus | + +> ❗ Hindari border terlalu tipis (`opacity < 20%`) + +--- + +### Text Colors +| Jenis | Token | Warna | +|-----|------|------| +| Primary | `--text-primary` | `#E5E7EB` | +| Secondary | `--text-secondary` | `#9CA3AF` | +| Muted | `--text-muted` | `#6B7280` | +| Inverse | `--text-inverse` | `#020617` | + +--- + +### Accent & Action +| Fungsi | Warna | +|------|------| +| Primary Action | `#3B82F6` | +| Hover | `#2563EB` | +| Active | `#1D4ED8` | +| Link | `#60A5FA` | + +--- + +### Status Colors +| Status | Warna | +|------|------| +| Success | `#22C55E` | +| Warning | `#FACC15` | +| Error | `#EF4444` | +| Info | `#38BDF8` | + +--- + +## 🧱 Layout Rules + +### Sidebar +- Background: `--bg-app` +- Active menu: + - Background: `rgba(59,130,246,0.15)` + - Text: Primary + - Indicator: kiri (2–3px accent bar) +- Hover: + - Background: `rgba(255,255,255,0.04)` + +--- + +### Header / Topbar +- Background: `linear-gradient(#0F172A → #0B1220)` +- Border bawah wajib (`--border-soft`) +- Icon: + - Default: muted + - Hover: primary + +--- + +## 📦 Card & Section + +### Card +- Background: `--bg-card` +- Border: `--border-default` +- Radius: 12–16px +- Jangan pakai shadow hitam + +### Section Header +- Font weight lebih besar +- Text: primary +- Spacing jelas dari konten + +--- + +## 📊 Table (Dark Mode Friendly) + +### Table Header +- Background: `--bg-surface` +- Text: secondary +- Font weight: medium + +### Table Row +- Default: transparent +- Hover: + - Background: `rgba(255,255,255,0.03)` +- Divider antar row wajib terlihat + +### Link di Table +- Warna link **lebih terang dari text** +- Hover underline + +--- + +## 🔘 Button Rules + +### Primary Button +- Background: Primary Action +- Text: Inverse +- Hover: darker shade + +### Secondary Button +- Background: transparent +- Border: `--border-default` +- Text: primary + +### Icon Button +- Default: muted +- Hover: primary + bg soft + +--- + +## 🧭 Tab Navigation + +- Inactive: + - Text: muted +- Active: + - Background: `rgba(59,130,246,0.15)` + - Text: primary + - Icon ikut berubah + +--- + +## 🌗 Dark vs Light Mode Rule +- Layout, spacing, typography **HARUS SAMA** +- Yang boleh beda: + - warna + - border intensity + - background layer + +> ❌ Jangan ganti struktur UI antara dark & light + +--- + +## ✅ Dark Mode Checklist +- [ ] Kontras teks terbaca +- [ ] Active state jelas +- [ ] Hover terasa hidup +- [ ] Tidak flat +- [ ] Tidak silau + +--- + +Dokumen ini adalah **single source of truth** untuk Dark Mode. \ No newline at end of file diff --git a/src/app/admin/(dashboard)/_com/header.tsx b/src/app/admin/(dashboard)/_com/header.tsx index 39735f4d..0b7c345b 100644 --- a/src/app/admin/(dashboard)/_com/header.tsx +++ b/src/app/admin/(dashboard)/_com/header.tsx @@ -1,7 +1,11 @@ +'use client'; + import React from 'react'; -import { Grid, GridCol, Paper, TextInput, Title } from '@mantine/core'; +import { Grid, GridCol, Paper, TextInput } from '@mantine/core'; import { IconSearch } from '@tabler/icons-react'; -import colors from '@/con/colors'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { UnifiedTitle } from '@/components/admin/UnifiedTypography'; type HeaderSearchProps = { title: string; @@ -18,13 +22,16 @@ const HeaderSearch = ({ value, onChange, }: HeaderSearchProps) => { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + return ( - {title} + {title} - + diff --git a/src/app/admin/(dashboard)/_com/judulList.tsx b/src/app/admin/(dashboard)/_com/judulList.tsx index 4eaa5731..7f376d11 100644 --- a/src/app/admin/(dashboard)/_com/judulList.tsx +++ b/src/app/admin/(dashboard)/_com/judulList.tsx @@ -1,12 +1,16 @@ 'use client' -import colors from '@/con/colors'; -import { Grid, GridCol, Button, Text } from '@mantine/core'; +import { Grid, GridCol, Button } from '@mantine/core'; import { IconCircleDashedPlus } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import React from 'react'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { UnifiedText } from '@/components/admin/UnifiedTypography'; const JudulList = ({ title = "", href = "#" }) => { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); const router = useRouter(); const handleNavigate = () => { @@ -16,10 +20,18 @@ const JudulList = ({ title = "", href = "#" }) => { return ( - {title} + {title} - diff --git a/src/app/admin/(dashboard)/_com/judulListTab.tsx b/src/app/admin/(dashboard)/_com/judulListTab.tsx index 21037671..dda1fe69 100644 --- a/src/app/admin/(dashboard)/_com/judulListTab.tsx +++ b/src/app/admin/(dashboard)/_com/judulListTab.tsx @@ -1,9 +1,11 @@ 'use client' -import colors from '@/con/colors'; -import { Grid, GridCol, Button, Text, Paper, TextInput } from '@mantine/core'; +import { Grid, GridCol, Button, Paper, TextInput } from '@mantine/core'; import { IconCircleDashedPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import React from 'react'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { UnifiedText } from '@/components/admin/UnifiedTypography'; type JudulListTabProps = { title: string; @@ -14,17 +16,16 @@ type JudulListTabProps = { onChange?: (e: React.ChangeEvent) => void; } - - - const JudulListTab = ({ title = "", href = "#", placeholder = "pencarian", searchIcon = , value, - onChange + onChange }: JudulListTabProps) => { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); const router = useRouter(); const handleNavigate = () => { @@ -34,10 +35,17 @@ const JudulListTab = ({ return ( - {title} + + {title} + - + - diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 55687b41..6700ea9b 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,7 +1,9 @@ 'use client' -import colors from "@/con/colors"; import { authStore } from "@/store/authStore"; +import { useDarkMode } from "@/state/darkModeStore"; +import { themeTokens, getActiveStateStyles } from "@/utils/themeTokens"; +import { DarkModeToggle } from "@/components/admin/DarkModeToggle"; import { ActionIcon, AppShell, @@ -33,13 +35,21 @@ import { useEffect, useState } from "react"; import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar"; export default function Layout({ children }: { children: React.ReactNode }) { - const [opened, { toggle, close }] = useDisclosure(); // ✅ Tambahkan 'close' + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const [mounted, setMounted] = useState(false); + const [opened, { toggle, close }] = useDisclosure(); const [loading, setLoading] = useState(true); const [isLoggingOut, setIsLoggingOut] = useState(false); const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); const router = useRouter(); const segments = useSelectedLayoutSegments().map((s) => _.lowerCase(s)); - + + // Ensure component is mounted on client side + useEffect(() => { + setMounted(true); + }, []); useEffect(() => { const fetchUser = async () => { @@ -74,7 +84,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { }); const currentPath = window.location.pathname; - + if (currentPath === '/admin') { const expectedPath = getRedirectPath(Number(data.user.roleId)); console.log('🔄 Redirecting from /admin to:', expectedPath); @@ -112,11 +122,11 @@ export default function Layout({ children }: { children: React.ReactNode }) { } }; - if (loading) { + if (loading || !mounted) { return ( -
+
@@ -132,7 +142,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { try { setIsLoggingOut(true); - const response = await fetch('/api/auth/logout', { + const response = await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); @@ -158,10 +168,9 @@ export default function Layout({ children }: { children: React.ReactNode }) { } }; - // ✅ Handler untuk menutup mobile menu saat navigasi const handleNavClick = (path: string) => { router.push(path); - close(); // Tutup mobile menu + close(); }; return ( @@ -178,11 +187,16 @@ export default function Layout({ children }: { children: React.ReactNode }) { }} padding="md" > + {/* + HEADER / TOPBAR + Spec: Background gradient, border bawah wajib + */} - + Admin Darmasaba + {/* Dark Mode Toggle */} + + {!desktopOpened && ( - + )} - + - router.push("/darmasaba")} color={colors["blue-button"]} radius="xl" size="lg" variant="gradient" gradient={{ from: colors["blue-button"], to: "#228be6" }}> + router.push("/darmasaba")} + color={mounted ? tokens.colors.primary : '#3B82F6'} + radius="xl" + size="lg" + variant="gradient" + gradient={mounted ? tokens.colors.gradient : { from: '#3B82F6', to: '#60A5FA' }} + > Logo Darmasaba - + @@ -229,47 +262,104 @@ export default function Layout({ children }: { children: React.ReactNode }) { - + {/* + SIDEBAR / NAVBAR + Spec: Background --bg-app, active state dengan accent bar + */} + {currentNav.map((v, k) => { const isParentActive = segments.includes(_.lowerCase(v.name)); return ( - {v.name}} - style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} - styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} - variant="light" + + {v.name} + + } + style={{ + borderRadius: rem(10), + marginBottom: rem(4), + transition: "background 150ms ease", + ...(mounted && isParentActive && !isDark && { + borderLeft: `3px solid ${tokens.colors.primary}`, + }), + }} + styles={{ + root: { + '&:hover': { + backgroundColor: mounted && isDark ? '#1E293B' : tokens.colors.bg.hover, + }, + ...(mounted && isParentActive && isDark && { + backgroundColor: 'rgba(59,130,246,0.25)', + borderLeft: `3px solid ${tokens.colors.primary}`, + }), + } + }} + variant="light" active={isParentActive} > {v.children.map((child, key) => { const isChildActive = segments.includes(_.lowerCase(child.name)); return ( - { e.preventDefault(); handleNavClick(child.path); }} href={child.path} - c={isChildActive ? colors["blue-button"] : "gray"} - label={{child.name}} - styles={{ - root: { - borderRadius: rem(8), - marginBottom: rem(2), - transition: 'background 150ms ease', - padding: '6px 12px', - '&:hover': { - backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' - }, - ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) - } - }} - active={isChildActive} + c={mounted && isChildActive ? tokens.colors.primary : mounted && isDark ? '#E5E7EB' : tokens.colors.text.secondary} + label={ + + {child.name} + + } + styles={{ + root: { + borderRadius: rem(8), + marginBottom: rem(2), + transition: 'background 150ms ease', + padding: '6px 12px', + '&:hover': { + backgroundColor: mounted && isDark ? 'rgba(255, 255, 255, 0.05)' : tokens.colors.bg.hover, + }, + ...(mounted && isChildActive && isDark && { + backgroundColor: 'rgba(59,130,246,0.15)', + borderLeft: `2px solid ${tokens.colors.primary}`, + }), + ...(mounted && isChildActive && !isDark && { + backgroundColor: tokens.colors.bg.hover, + }), + } + }} + active={isChildActive} + variant="subtle" component={Link} /> ); @@ -282,7 +372,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { - + @@ -290,7 +380,17 @@ export default function Layout({ children }: { children: React.ReactNode }) { - + {/* + MAIN CONTENT + Spec: Background --bg-base + */} + {children} diff --git a/src/components/admin/AdminThemeProvider.tsx b/src/components/admin/AdminThemeProvider.tsx new file mode 100644 index 00000000..0a2a5c76 --- /dev/null +++ b/src/components/admin/AdminThemeProvider.tsx @@ -0,0 +1,119 @@ +'use client'; + +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { MantineProvider, createTheme } from '@mantine/core'; +import '@mantine/core/styles.css'; +import '@/styles/dark-mode-table.css'; +import React from 'react'; + +/** + * Admin Theme Provider + * + * Wrapper untuk MantineProvider dengan custom theme + * Mendukung dark mode otomatis + * + * Usage: + * import { AdminThemeProvider } from '@/components/admin/AdminThemeProvider'; + * + * + * + * + */ + +interface AdminThemeProviderProps { + children: React.ReactNode; + forceTheme?: 'light' | 'dark'; +} + +export function AdminThemeProvider({ children, forceTheme }: AdminThemeProviderProps) { + const { isDark } = useDarkMode(); + + // Use forced theme if provided, otherwise use store + const useDark = forceTheme ? forceTheme === 'dark' : isDark; + const tokens = themeTokens(useDark); + + const theme = createTheme({ + colors: { + primary: [ + tokens.colors.primaryLight, + tokens.colors.primaryLight, + tokens.colors.primary, + tokens.colors.primary, + tokens.colors.primary, + tokens.colors.primary, + tokens.colors.primaryDark, + tokens.colors.primaryDark, + tokens.colors.primaryDark, + tokens.colors.primaryDark, + ], + }, + primaryColor: 'primary', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + fontFamilyMonospace: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace', + + // Override default colors based on mode + white: tokens.colors.text.inverse, + black: tokens.colors.text.primary, + + // CSS variables for table hover + activeClassName: useDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.02)', + + // Component defaults + components: { + Paper: { + defaultProps: { + bg: tokens.colors.bg.card, + radius: 'md', + shadow: 'sm', + }, + }, + Button: { + defaultProps: { + radius: 'md', + }, + }, + TextInput: { + defaultProps: { + radius: 'md', + }, + }, + Select: { + defaultProps: { + radius: 'md', + }, + }, + Modal: { + defaultProps: { + radius: 'lg', + }, + }, + Table: { + defaultProps: { + highlightOnHover: true, + }, + }, + }, + }); + + return ( + +
+ {children} +
+
+ ); +} + +export default AdminThemeProvider; diff --git a/src/components/admin/DarkModeToggle.tsx b/src/components/admin/DarkModeToggle.tsx new file mode 100644 index 00000000..9ef35ecb --- /dev/null +++ b/src/components/admin/DarkModeToggle.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { ActionIcon, Tooltip, Transition } from '@mantine/core'; +import { IconMoon, IconSun } from '@tabler/icons-react'; + +/** + * Dark Mode Toggle Button + * + * Component untuk toggle dark/light mode + * + * Usage: + * import { DarkModeToggle } from '@/components/admin/DarkModeToggle'; + * + * + */ + +interface DarkModeToggleProps { + variant?: 'light' | 'filled' | 'outline' | 'subtle'; + size?: 'sm' | 'md' | 'lg'; + color?: string; + showTooltip?: boolean; + tooltipPosition?: 'top' | 'bottom' | 'left' | 'right'; +} + +export function DarkModeToggle({ + variant = 'light', + size = 'lg', + color, + showTooltip = true, + tooltipPosition = 'bottom', +}: DarkModeToggleProps) { + const { isDark, toggle } = useDarkMode(); + const tokens = themeTokens(isDark); + + const iconColor = color || tokens.colors.primary; + + return ( + + + {/* Icon Sun untuk Light Mode */} + + {(style) => ( + + )} + + + {/* Icon Moon untuk Dark Mode */} + + {(style) => ( + + )} + + + + ); +} + +export default DarkModeToggle; diff --git a/src/components/admin/README_UNIFIED_STYLING.md b/src/components/admin/README_UNIFIED_STYLING.md new file mode 100644 index 00000000..3e8375f1 --- /dev/null +++ b/src/components/admin/README_UNIFIED_STYLING.md @@ -0,0 +1,546 @@ +# 🎨 Unified Styling System - Admin Dashboard + +Sistem styling terpusat untuk admin dashboard Darmasaba dengan dukungan **dark mode**. + +**Berdasarkan spesifikasi:** `darkMode.md` + +--- + +## 📋 Daftar Isi + +- [Konsep Utama](#konsep-utama) +- [Dark Mode Palette](#dark-mode-palette) +- [Struktur File](#struktur-file) +- [Cara Menggunakan](#cara-menggunakan) +- [Mengedit Style](#mengedit-style) +- [Dark Mode Toggle](#dark-mode-toggle) +- [Contoh Penggunaan](#contoh-penggunaan) + +--- + +## 🎯 Konsep Utama + +**Satu File Edit = Semua Halaman Terupdate** + +Sebelumnya: +- ❌ Style tersebar di 493 file `.tsx` +- ❌ Hardcode warna di setiap komponen +- ❌ Tidak ada konsistensi +- ❌ Sulit maintain + +Sekarang: +- ✅ Edit di **1 file** = semua halaman update +- ✅ Component reusable +- ✅ Konsisten di seluruh aplikasi +- ✅ Dark mode otomatis sesuai spesifikasi `darkMode.md` + +--- + +## 🌙 Dark Mode Palette + +### Background Layers (Dark Mode) +| Layer | Token | Warna | Fungsi | +|------|------|------|------| +| Base | `bg.base` | `#0B1220` | Background utama aplikasi | +| App | `bg.app` | `#0F172A` | Area sidebar | +| Card | `bg.card` | `#162235` | Card / container | +| Surface | `bg.surface` | `#1E2A3D` | Table header, tab, input | + +### Text Colors (Dark Mode) +| Jenis | Token | Warna | +|-----|------|------| +| Primary | `text.primary` | `#E5E7EB` | +| Secondary | `text.secondary` | `#9CA3AF` | +| Muted | `text.muted` | `#6B7280` | + +### Accent & Actions (Dark Mode) +| Fungsi | Warna | +|------|------| +| Primary Action | `#3B82F6` | +| Hover | `#2563EB` | +| Active | `#1D4ED8` | +| Link | `#60A5FA` | + +### Borders (Dark Mode) +| Token | Warna | +|-----|------| +| `border.default` | `#2A3A52` | +| `border.soft` | `#22314A` | + +> **Catatan:** Light mode menggunakan palette original yang lebih terang + +--- + +## 📁 Struktur File + +``` +src/ +├── utils/ +│ └── themeTokens.ts # 📦 PUSAT SEMUA STYLE (edit di sini!) +├── state/ +│ └── darkModeStore.ts # 🌙 State management dark mode +├── components/admin/ +│ ├── DarkModeToggle.tsx # 🌓 Toggle button +│ ├── AdminThemeProvider.tsx # 🎨 Theme provider wrapper +│ ├── UnifiedTypography.tsx # 📝 Text components (Title, Text) +│ ├── UnifiedSurface.tsx # 📦 Card, Paper components +│ └── README_UNIFIED_STYLING.md # 📖 Dokumentasi ini +├── app/admin/ +│ ├── layout.tsx # ✅ Sudah diupdate dengan dark mode +│ └── (dashboard)/ +│ └── _com/ +│ ├── header.tsx # ✅ Sudah diupdate +│ ├── judulList.tsx # ✅ Sudah diupdate +│ └── judulListTab.tsx # ✅ Sudah diupdate +└── darkMode.md # 📐 Spesifikasi lengkap dark mode +``` + +--- + +## 🚀 Cara Menggunakan + +### 1. **Untuk Developer: Edit Style Global** + +Edit file: `src/utils/themeTokens.ts` + +```typescript +export const themeTokens = (isDark: boolean = false): ThemeTokens => { + const darkColors = { + bgBase: '#0B1220', // ← Edit warna dark mode di sini + bgCard: '#162235', + textPrimary: '#E5E7EB', + primaryAction: '#3B82F6', + // ... dan lainnya + }; + + return { + colors: { + primary: current.primaryAction, + bg: { + base: current.bgBase, + card: current.bgCard, + // ... + }, + // ... + }, + }; +}; +``` + +### 2. **Menggunakan Components di Halaman** + +#### A. Typography Components + +```tsx +import { UnifiedTitle, UnifiedText } from '@/components/admin/UnifiedTypography'; + +// Heading - otomatis dark mode +Judul Halaman +Sub Judul +Section Title +Card Title + +// Text dengan color semantic +Teks primary +Teks secondary +Teks muted +Link text +Brand color + +// Dengan weight +Teks bold +Teks medium +``` + +#### B. Surface Components + +```tsx +import UnifiedCard, { UnifiedDivider } from '@/components/admin/UnifiedSurface'; + +// Card sederhana - border dan warna otomatis dark mode + +

Isi card

+
+ +// Card dengan sections + + + Header + + + +

Body content

+
+ + + + +
+ +// Divider dengan variant + {/* Default */} + + +``` + +#### C. Page Header Component + +```tsx +import { UnifiedPageHeader } from '@/components/admin/UnifiedTypography'; + + + Tambah Baru + + } +/> +``` + +### 3. **Menggunakan Theme Tokens Langsung** + +```tsx +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; + +function MyComponent() { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + return ( +
+

+ Konten dengan styling konsisten +

+
+ ); +} +``` + +--- + +## 🌓 Dark Mode Toggle + +### Otomatis di Header + +Dark mode toggle sudah terintegrasi di header admin dashboard. User bisa toggle dengan klik tombol 🌙/☀️. + +### Manual Toggle + +```tsx +import { useDarkMode } from '@/state/darkModeStore'; +import { DarkModeToggle } from '@/components/admin/DarkModeToggle'; + +function MyComponent() { + const { isDark, toggle } = useDarkMode(); + + return ( +
+

Current mode: {isDark ? 'Dark' : 'Light'}

+ + {/* Gunakan component toggle */} + + + {/* Atau manual */} + +
+ ); +} +``` + +### Persistensi + +Dark mode preference disimpan di `localStorage` dengan key `darmasaba-admin-dark-mode`. +Preference akan tetap ada saat user refresh halaman atau kembali nanti. + +--- + +## 📝 Contoh Penggunaan Lengkap + +### Contoh 1: List Page dengan Table + +```tsx +'use client' +import { UnifiedPageHeader, UnifiedText } from '@/components/admin/UnifiedTypography'; +import UnifiedCard from '@/components/admin/UnifiedSurface'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { Button, Table, TableTr, TableTh, TableTd } from '@mantine/core'; + +export default function DaftarBerita() { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + return ( +
+ {/* Header Halaman */} + + + Tambah Berita + + } + /> + + {/* Card untuk Table */} + + + + + + Judul + + + Kategori + + + + + {data.map((item) => ( + + + {item.judul} + + + + {item.kategori} + + + + ))} + +
+
+
+ ); +} +``` + +### Contoh 2: Detail Page + +```tsx +import { UnifiedTitle, UnifiedText } from '@/components/admin/UnifiedTypography'; +import UnifiedCard, { UnifiedDivider } from '@/components/admin/UnifiedSurface'; + +export default function DetailBerita({ data }) { + return ( + + + {data.judul} + + + + + Kategori + {data.kategori} + + + + + + Deskripsi + {data.deskripsi} + + + + + + Konten +
+ + + + + + + + + + + ); +} +``` + +--- + +## 🎨 Mengedit Style + +### Edit Warna Dark Mode + +File: `src/utils/themeTokens.ts` + +```typescript +const darkColors = { + // Background Layers + bgBase: '#0B1220', // ← Edit di sini + bgApp: '#0F172A', + bgCard: '#162235', + bgSurface: '#1E2A3D', + + // Text + textPrimary: '#E5E7EB', // ← Edit di sini + textSecondary: '#9CA3AF', + + // Accent + primaryAction: '#3B82F6', // ← Edit primary color +}; +``` + +### Edit Warna Light Mode + +```typescript +const lightColors = { + bgBase: '#f6f9fc', + bgCard: '#ffffff', + textPrimary: '#1a1b1e', + primaryAction: baseColors['blue-button'], // Dari colors.ts +}; +``` + +### Edit Typography + +```typescript +typography: { + h1: { + fz: '2rem', // ← Edit ukuran + fw: 700, // ← Edit weight + lh: 1.2, // ← Edit line height + }, + body: { + fz: '1rem', + fw: 400, + lh: 1.5, + }, +} +``` + +### Edit Spacing & Radius + +```typescript +spacing: { + xs: '0.625rem', // 10px + sm: '1rem', // 16px + md: '1.5rem', // 24px + lg: '2rem', // 32px +} + +radius: { + sm: '0.5rem', // 8px + md: '0.75rem', // 12px + lg: '1rem', // 16px +} +``` + +--- + +## ✅ Checklist Migrasi + +Komponen yang sudah diupdate dengan dark mode: + +- ✅ `src/app/admin/layout.tsx` +- ✅ `src/app/admin/(dashboard)/_com/header.tsx` +- ✅ `src/app/admin/(dashboard)/_com/judulList.tsx` +- ✅ `src/app/admin/(dashboard)/_com/judulListTab.tsx` +- ✅ `src/components/admin/UnifiedTypography.tsx` +- ✅ `src/components/admin/UnifiedSurface.tsx` +- ✅ `src/components/admin/DarkModeToggle.tsx` +- ✅ `src/utils/themeTokens.ts` + +Komponen yang perlu diupdate (TODO): + +- [ ] Komponen di `src/app/admin/(dashboard)/desa/` +- [ ] Komponen di `src/app/admin/(dashboard)/ppid/` +- [ ] Komponen di `src/app/admin/(dashboard)/kesehatan/` +- [ ] Komponen di `src/app/admin/(dashboard)/pendidikan/` +- [ ] Komponen di `src/app/admin/(dashboard)/ekonomi/` +- [ ] Dan lain-lain... + +--- + +## 📚 Referensi + +- [Dark Mode Specification](../../../darkMode.md) - Spesifikasi lengkap dark mode +- [Mantine Theme System](https://mantine.dev/theming/theme-object/) +- [Mantine Dark Mode](https://mantine.dev/theming/dark-mode/) +- [Valtio State Management](https://github.com/pmndrs/valtio) + +--- + +## 💡 Tips + +1. **Selalu gunakan unified components** untuk konsistensi dark/light mode +2. **Edit di `themeTokens.ts`** untuk perubahan global +3. **Test dark mode** setelah perubahan style +4. **Gunakan color semantic** (`primary`, `secondary`, `muted`) bukan hex langsung +5. **Jangan hardcode shadow** di dark mode (spec: "Jangan pakai shadow hitam") +6. **Border harus terlihat** di dark mode (opacity > 20%) + +--- + +## 🆘 Troubleshooting + +### Style tidak berubah setelah edit themeTokens.ts? + +1. Clear browser cache (Cmd+Shift+R / Ctrl+Shift+R) +2. Restart dev server: `bun run dev` +3. Pastikan komponen menggunakan unified components + +### Dark mode tidak berfungsi? + +1. Cek `darkModeStore.ts` sudah diimport +2. Pastikan `useDarkMode()` hook digunakan +3. Clear localStorage: `localStorage.clear()` +4. Cek console untuk error + +### Border tidak terlihat di dark mode? + +Pastikan menggunakan `tokens.colors.border.default` atau `tokens.colors.border.soft`, bukan hardcode warna. + +### Component tidak re-render? + +1. Pastikan `'use client'` ada di file component +2. Gunakan `useSnapshot()` jika menggunakan Valtio di non-event handler +3. Cek console untuk error + +--- + +## 📐 Spesifikasi Dark Mode + +Untuk spesifikasi lengkap dark mode (layout rules, table styles, button rules, dll), lihat: +**[`darkMode.md`](../../../darkMode.md)** + +Highlights: +- ✅ Background layers berbeda (base, app, card, surface) +- ✅ Border wajib terlihat (tidak flat) +- ✅ Active state dengan accent bar (2-3px) +- ✅ Tidak pakai shadow hitam +- ✅ Hover state dengan background soft +- ✅ Text kontras terbaca + +--- + +**Last Updated:** February 20, 2026 +**Version:** 2.0.0 (Dark Mode Ready) +**Based on:** darkMode.md specification diff --git a/src/components/admin/UnifiedSurface.tsx b/src/components/admin/UnifiedSurface.tsx new file mode 100644 index 00000000..c00f911b --- /dev/null +++ b/src/components/admin/UnifiedSurface.tsx @@ -0,0 +1,252 @@ +'use client'; + +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { Paper, Box, BoxProps, Divider, DividerProps } from '@mantine/core'; +import React from 'react'; + +/** + * Unified Surface Components + * + * Komponen container/card dengan styling konsisten + * Mendukung dark mode sesuai spesifikasi darkMode.md + * + * Usage: + * import { UnifiedCard, UnifiedDivider } from '@/components/admin/UnifiedSurface'; + * + * + * Title + * Content + * + */ + +// ============================================================================ +// Unified Card Component + * ============================================================================ + +interface UnifiedCardProps extends BoxProps { + withBorder?: boolean; + shadow?: 'none' | 'sm' | 'md' | 'lg'; + padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + hoverable?: boolean; + children: React.ReactNode; +} + +export function UnifiedCard({ + withBorder = true, + shadow = 'none', // Sesuai spec: Jangan pakai shadow hitam + padding = 'md', + hoverable = false, + children, + style, + ...props +}: UnifiedCardProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + case 'xl': + return tokens.spacing.xl; + default: + return tokens.spacing.md; + } + }; + + return ( + + {children} + + ); +} + +// ============================================================================ +// Unified Card Section Components +// ============================================================================ + +interface UnifiedCardSectionProps { + children: React.ReactNode; + padding?: 'none' | 'xs' | 'sm' | 'md' | 'lg'; + border?: 'none' | 'top' | 'bottom'; + style?: React.CSSProperties; +} + +UnifiedCard.Header = function UnifiedCardHeader({ + children, + padding = 'md', + border = 'bottom', + style, +}: UnifiedCardSectionProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + default: + return tokens.spacing.md; + } + }; + + const borderBottom = border === 'bottom' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + const borderTop = border === 'top' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + + return ( + + {children} + + ); +}; + +UnifiedCard.Body = function UnifiedCardBody({ + children, + padding = 'md', + style, +}: UnifiedCardSectionProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + default: + return tokens.spacing.md; + } + }; + + return ( + + {children} + + ); +}; + +UnifiedCard.Footer = function UnifiedCardFooter({ + children, + padding = 'md', + border = 'top', + style, +}: UnifiedCardSectionProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getPadding = () => { + switch (padding) { + case 'none': + return 0; + case 'xs': + return tokens.spacing.xs; + case 'sm': + return tokens.spacing.sm; + case 'md': + return tokens.spacing.md; + case 'lg': + return tokens.spacing.lg; + default: + return tokens.spacing.md; + } + }; + + const borderBottom = border === 'bottom' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + const borderTop = border === 'top' ? `1px solid ${tokens.colors.border.soft}` : 'none'; + + return ( + + {children} + + ); +}; + +// ============================================================================ +// Unified Divider Component +// ============================================================================ + +interface UnifiedDividerProps extends DividerProps { + variant?: 'default' | 'soft' | 'strong'; +} + +export function UnifiedDivider({ + variant = 'soft', // Default soft sesuai spec + my = 'md', + ...props +}: UnifiedDividerProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getColor = () => { + switch (variant) { + case 'default': + return tokens.colors.border.default; + case 'soft': + return tokens.colors.border.soft; + case 'strong': + return tokens.colors.border.strong; + default: + return tokens.colors.border.soft; + } + }; + + return ; +} + +export default UnifiedCard; diff --git a/src/components/admin/UnifiedTypography.tsx b/src/components/admin/UnifiedTypography.tsx new file mode 100644 index 00000000..565be609 --- /dev/null +++ b/src/components/admin/UnifiedTypography.tsx @@ -0,0 +1,268 @@ +'use client'; + +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens, getResponsiveFz } from '@/utils/themeTokens'; +import { Text, Title, Box, BoxProps } from '@mantine/core'; +import React from 'react'; + +/** + * Unified Typography Components + * + * Komponen text dengan styling konsisten di seluruh aplikasi + * Mendukung dark mode sesuai spesifikasi darkMode.md + * + * Usage: + * import { UnifiedText, UnifiedTitle } from '@/components/admin/UnifiedTypography'; + * + * Judul Halaman + * Konten teks + */ + +// ============================================================================ +// Unified Title Component +// ============================================================================ + +interface UnifiedTitleProps { + order?: 1 | 2 | 3 | 4 | 5 | 6; + children: React.ReactNode; + align?: 'left' | 'center' | 'right'; + color?: 'primary' | 'secondary' | 'brand' | string; + mb?: string; + mt?: string; + ml?: string; + mr?: string; + mx?: string; + my?: string; + style?: React.CSSProperties; +} + +export function UnifiedTitle({ + order = 1, + children, + align = 'left', + color = 'primary', + mb, + mt, + ml, + mr, + mx, + my, + style, +}: UnifiedTitleProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + const responsiveFz = getResponsiveFz(isDark); + + const getTypography = () => { + switch (order) { + case 1: + return tokens.typography.h1; + case 2: + return tokens.typography.h2; + case 3: + return tokens.typography.h3; + case 4: + return tokens.typography.h4; + default: + return tokens.typography.body; + } + }; + + const typo = getTypography(); + + const getColor = () => { + if (color === 'primary') return tokens.colors.text.primary; + if (color === 'secondary') return tokens.colors.text.secondary; + if (color === 'brand') return tokens.colors.brand; + return color; + }; + + return ( + + {children} + + ); +} + +// ============================================================================ +// Unified Text Component +// ============================================================================ + +interface UnifiedTextProps { + size?: 'small' | 'body' | 'label'; + weight?: 'normal' | 'medium' | 'bold'; + children: React.ReactNode; + align?: 'left' | 'center' | 'right'; + color?: 'primary' | 'secondary' | 'tertiary' | 'muted' | 'brand' | 'link' | string; + lineClamp?: number; + truncate?: 'start' | 'end' | 'middle' | boolean; + span?: boolean; + style?: React.CSSProperties; +} + +export function UnifiedText({ + size = 'body', + weight = 'normal', + children, + align = 'left', + color = 'primary', + lineClamp, + truncate, + span = false, + style, +}: UnifiedTextProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + const getTypography = () => { + switch (size) { + case 'small': + return tokens.typography.small; + case 'label': + return tokens.typography.label; + default: + return tokens.typography.body; + } + }; + + const getWeight = () => { + switch (weight) { + case 'normal': + return 400; + case 'medium': + return 500; + case 'bold': + return 700; + default: + return 400; + } + }; + + const getColor = () => { + switch (color) { + case 'primary': + return tokens.colors.text.primary; + case 'secondary': + return tokens.colors.text.secondary; + case 'tertiary': + return tokens.colors.text.tertiary; + case 'muted': + return tokens.colors.text.muted; + case 'brand': + return tokens.colors.brand; + case 'link': + return tokens.colors.text.link; + default: + return color; + } + }; + + const typo = getTypography(); + const fw = getWeight(); + const textColor = getColor(); + + if (span) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +// ============================================================================ +// Unified Page Header Component +// +// Header standar untuk setiap halaman admin +// Sesuai spesifikasi: Section Header dengan font weight lebih besar +// ============================================================================ + +interface UnifiedPageHeaderProps extends BoxProps { + title: string; + subtitle?: string; + action?: React.ReactNode; + showBorder?: boolean; +} + +export function UnifiedPageHeader({ + title, + subtitle, + action, + showBorder = true, + style, + ...props +}: UnifiedPageHeaderProps) { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + + return ( + +
+
+ {title} + {subtitle && ( + + {subtitle} + + )} +
+ {action &&
{action}
} +
+
+ ); +} + +export default UnifiedText; diff --git a/src/state/darkModeStore.ts b/src/state/darkModeStore.ts new file mode 100644 index 00000000..25032410 --- /dev/null +++ b/src/state/darkModeStore.ts @@ -0,0 +1,76 @@ +/** + * Dark Mode State Management + * + * Menggunakan Valtio untuk global state + * Persist ke localStorage + * + * Usage: + * import { darkModeStore } from '@/state/darkModeStore'; + * + * // Toggle + * darkModeStore.toggle(); + * + * // Set explicitly + * darkModeStore.setDarkMode(true); + * + * // Get current state + * const isDark = darkModeStore.isDark; + */ + +import { proxy, useSnapshot } from 'valtio'; + +const STORAGE_KEY = 'darmasaba-admin-dark-mode'; + +// Initialize from localStorage or system preference +const getInitialDarkMode = (): boolean => { + if (typeof window === 'undefined') return false; + + const stored = localStorage.getItem(STORAGE_KEY); + if (stored !== null) { + return stored === 'true'; + } + + // Fallback to system preference + return window.matchMedia('(prefers-color-scheme: dark)').matches; +}; + +class DarkModeStore { + public isDark: boolean; + + constructor() { + this.isDark = getInitialDarkMode(); + } + + public toggle() { + this.isDark = !this.isDark; + this.persist(); + } + + public setDarkMode(value: boolean) { + this.isDark = value; + this.persist(); + } + + private persist() { + if (typeof window !== 'undefined') { + localStorage.setItem(STORAGE_KEY, String(this.isDark)); + } + } +} + +// Create proxy instance +const store = new DarkModeStore(); + +export const darkModeStore = proxy(store); + +// Hook untuk menggunakan dark mode state di React components +export const useDarkMode = () => { + const snapshot = useSnapshot(darkModeStore); + return { + isDark: snapshot.isDark, + toggle: () => darkModeStore.toggle(), + setDarkMode: (value: boolean) => darkModeStore.setDarkMode(value), + }; +}; + +export default darkModeStore; diff --git a/src/styles/dark-mode-table.css b/src/styles/dark-mode-table.css new file mode 100644 index 00000000..ecc08032 --- /dev/null +++ b/src/styles/dark-mode-table.css @@ -0,0 +1,31 @@ +/** + * Dark Mode Table Styles + * + * Override Mantine table hover styles untuk dark mode + * Agar teks putih tetap terlihat saat hover + */ + +/* Dark mode table hover */ +[data-mantine-color-scheme="dark"] { + /* Table hover */ + .mantine-Table-tr:hover { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + /* Table striped hover */ + .mantine-Table-striped .mantine-Table-tr:nth-of-type(odd):hover { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + /* Table with column borders */ + .mantine-Table-withColumnBorders .mantine-Table-tr:hover { + background-color: rgba(255, 255, 255, 0.08) !important; + } +} + +/* Light mode table hover - default Mantine behavior */ +[data-mantine-color-scheme="light"] { + .mantine-Table-tr:hover { + background-color: rgba(0, 0, 0, 0.02) !important; + } +} diff --git a/src/utils/themeTokens.ts b/src/utils/themeTokens.ts new file mode 100644 index 00000000..75c133a0 --- /dev/null +++ b/src/utils/themeTokens.ts @@ -0,0 +1,383 @@ +/** + * Unified Theme Tokens for Admin Dashboard + * + * Berdasarkan spesifikasi: darkMode.md + * + * Semua styling constants disimpan di sini untuk konsistensi + * Edit di sini = edit di seluruh aplikasi + * + * Usage: + * import { themeTokens } from '@/utils/themeTokens'; + * + * // Light mode (default) + * const tokens = themeTokens(false); + * + * // Dark mode + * const tokens = themeTokens(true); + */ + +export type ThemeTokens = { + // Colors + colors: { + primary: string; + primaryLight: string; + primaryDark: string; + gradient: { + from: string; + to: string; + }; + // Backgrounds + bg: { + base: string; + main: string; + app: string; + surface: string; + surfaceElevated: string; + header: string; + navbar: string; + card: string; + hover: string; + tableHeader: string; + tableHover: string; + }; + // Text + text: { + primary: string; + secondary: string; + tertiary: string; + muted: string; + brand: string; + inverse: string; + link: string; + }; + // Borders + border: { + default: string; + soft: string; + strong: string; + }; + // Status + success: string; + warning: string; + error: string; + info: string; + }; + + // Typography + typography: { + h1: { + fz: string; + fw: number; + lh: number; + }; + h2: { + fz: string; + fw: number; + lh: number; + }; + h3: { + fz: string; + fw: number; + lh: number; + }; + h4: { + fz: string; + fw: number; + lh: number; + }; + body: { + fz: string; + fw: number; + lh: number; + }; + small: { + fz: string; + fw: number; + lh: number; + }; + label: { + fz: string; + fw: number; + lh: number; + }; + }; + + // Spacing + spacing: { + xs: string; + sm: string; + md: string; + lg: string; + xl: string; + }; + + // Border Radius + radius: { + sm: string; + md: string; + lg: string; + xl: string; + }; + + // Shadows + shadows: { + none: string; + sm: string; + md: string; + lg: string; + }; + + // Layout + layout: { + headerHeight: number; + navbarWidth: { + base: number; + sm: number; + lg: number; + }; + }; +}; + +export const themeTokens = (isDark: boolean = false): ThemeTokens => { + // Base colors - tetap menggunakan colors.ts sebagai base untuk light mode + const baseColors = { + 'orange': '#FCAE00', + 'blue-button': '#0A4E78', + 'blue-button-1': '#E5F2FA', + 'blue-button-2': '#B8DAEF', + 'blue-button-3': '#8AC1E3', + 'blue-button-4': '#5DA9D8', + 'blue-button-5': '#2F91CC', + 'blue-button-6': '#083F61', + 'blue-button-7': '#062F49', + 'blue-button-8': '#041F32', + 'blue-button-trans': '#628EC6', + 'white-1': '#FBFBFC', + 'white-trans-1': 'rgba(255, 255, 255, 0.5)', + 'white-trans-2': 'rgba(255, 255, 255, 0.7)', + 'white-trans-3': 'rgba(255, 255, 255, 0.9)', + 'grey-1': '#F4F5F6', + 'grey-2': '#CBCACD', + 'Bg': '#D1d9e8', + 'BG-trans': '#B1C5F2', + }; + + /** + * DARK MODE PALETTE + * Berdasarkan spesifikasi: darkMode.md + */ + const darkColors = { + // Background Layers + bgBase: '#0B1220', + bgApp: '#0F172A', + bgCard: '#162235', + bgSurface: '#1E2A3D', + + // Borders + borderDefault: '#2A3A52', + borderSoft: '#22314A', + + // Text + textPrimary: '#E5E7EB', + textSecondary: '#9CA3AF', + textMuted: '#6B7280', + textInverse: '#020617', + + // Accent & Actions + primaryAction: '#3B82F6', + primaryHover: '#2563EB', + primaryActive: '#1D4ED8', + link: '#60A5FA', + + // Status + success: '#22C55E', + warning: '#FACC15', + error: '#EF4444', + info: '#38BDF8', + + // Hover states + hoverSoft: 'rgba(255,255,255,0.03)', + hoverMedium: 'rgba(255,255,255,0.04)', + activeAccent: 'rgba(59,130,246,0.15)', + }; + + /** + * LIGHT MODE PALETTE + * Original light theme + */ + const lightColors = { + bgBase: '#f6f9fc', + bgApp: '#ffffff', + bgCard: '#ffffff', + bgSurface: '#f8fafc', + borderDefault: '#e2e8f0', + borderSoft: '#e9ecef', + textPrimary: '#1a1b1e', + textSecondary: '#495057', + textMuted: '#868e96', + textInverse: '#ffffff', + primaryAction: baseColors['blue-button'], + primaryHover: '#083F61', + primaryActive: '#062F49', + link: '#2563eb', + hoverSoft: 'rgba(25, 113, 194, 0.03)', + hoverMedium: 'rgba(25, 113, 194, 0.05)', + activeAccent: 'rgba(25, 113, 194, 0.1)', + }; + + const current = isDark ? darkColors : lightColors; + + return { + colors: { + primary: current.primaryAction, + primaryLight: isDark ? current.activeAccent : baseColors['blue-button-1'], + primaryDark: current.primaryActive, + gradient: { + from: current.primaryAction, + to: isDark ? '#60A5FA' : '#228be6', + }, + bg: { + base: current.bgBase, + main: isDark ? current.bgBase : 'linear-gradient(180deg, #fdfdfd, #f6f9fc)', + app: current.bgApp, + surface: current.bgSurface, + surfaceElevated: isDark ? '#253347' : '#ffffff', + header: isDark + ? `linear-gradient(180deg, ${current.bgApp} 0%, ${current.bgBase} 100%)` + : 'linear-gradient(90deg, #ffffff, #f9fbff)', + navbar: current.bgApp, + card: current.bgCard, + hover: current.hoverMedium, + tableHeader: current.bgSurface, + tableHover: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.02)', + }, + text: { + primary: current.textPrimary, + secondary: current.textSecondary, + tertiary: current.textMuted, + muted: current.textMuted, + brand: current.primaryAction, + inverse: current.textInverse, + link: current.link, + }, + border: { + default: current.borderDefault, + soft: current.borderSoft, + strong: isDark ? '#3A4A62' : '#ced4da', + }, + success: current.success, + warning: current.warning, + error: current.error, + info: current.info, + }, + + typography: { + h1: { + fz: isDark ? '2rem' : '2.25rem', + fw: 700, + lh: 1.2, + }, + h2: { + fz: isDark ? '1.75rem' : '2rem', + fw: 700, + lh: 1.25, + }, + h3: { + fz: isDark ? '1.5rem' : '1.75rem', + fw: 700, + lh: 1.3, + }, + h4: { + fz: isDark ? '1.25rem' : '1.5rem', + fw: 600, + lh: 1.35, + }, + body: { + fz: '1rem', + fw: 400, + lh: 1.5, + }, + small: { + fz: '0.875rem', + fw: 400, + lh: 1.4, + }, + label: { + fz: '0.75rem', + fw: 600, + lh: 1.4, + }, + }, + + spacing: { + xs: '0.625rem', + sm: '1rem', + md: '1.5rem', + lg: '2rem', + xl: '2.5rem', + }, + + radius: { + sm: '0.5rem', // 8px + md: '0.75rem', // 12px + lg: '1rem', // 16px + xl: '1.25rem', // 20px + }, + + shadows: { + none: 'none', + sm: isDark ? '0 1px 3px rgba(0,0,0,0.3)' : '0 1px 3px rgba(0,0,0,0.1)', + md: isDark ? '0 4px 6px rgba(0,0,0,0.3)' : '0 4px 6px rgba(0,0,0,0.1)', + lg: isDark ? '0 10px 15px rgba(0,0,0,0.3)' : '0 10px 15px rgba(0,0,0,0.1)', + }, + + layout: { + headerHeight: 64, + navbarWidth: { + base: 260, + sm: 280, + lg: 300, + }, + }, + }; +}; + +// Export default theme instances +export const lightTheme = themeTokens(false); +export const darkTheme = themeTokens(true); + +// Helper untuk mendapatkan responsive font size +export const getResponsiveFz = (isDark: boolean = false) => ({ + base: isDark ? 'md' : 'lg', + md: isDark ? 'lg' : 'xl', +}); + +// Helper untuk mendapatkan color berdasarkan state +export const getActiveColor = (isActive: boolean, isDark: boolean = false) => + isActive ? themeTokens(isDark).colors.primary : isDark ? themeTokens(isDark).colors.text.secondary : 'gray'; + +// Helper untuk mendapatkan background hover +export const getHoverBackground = (isActive: boolean, isDark: boolean = false) => { + const tokens = themeTokens(isDark); + return isActive + ? tokens.colors.bg.hover + : tokens.colors.bg.hover; +}; + +// Helper untuk active state dengan accent bar (sidebar) +export const getActiveStateStyles = (isActive: boolean, isDark: boolean = false) => { + const tokens = themeTokens(isDark); + + if (isActive) { + return { + backgroundColor: isDark ? tokens.colors.bg.hover : 'rgba(25, 113, 194, 0.1)', + borderLeft: isDark ? `3px solid ${tokens.colors.primary}` : '3px solid #1971c2', + }; + } + + return { + '&:hover': { + backgroundColor: tokens.colors.bg.hover, + }, + }; +};