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 <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-02-23 10:48:00 +08:00
parent 8132609ccb
commit f0558aa0d0
13 changed files with 2139 additions and 62 deletions

View File

@@ -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';
*
* <AdminThemeProvider>
* <YourComponent />
* </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 (
<MantineProvider
theme={theme}
forceColorScheme={useDark ? 'dark' : 'light'}
defaultColorScheme={useDark ? 'dark' : 'light'}
>
<div
style={{
backgroundColor: tokens.colors.bg.main,
color: tokens.colors.text.primary,
minHeight: '100vh',
transition: 'background-color 0.3s ease, color 0.3s ease',
}}
>
{children}
</div>
</MantineProvider>
);
}
export default AdminThemeProvider;

View File

@@ -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';
*
* <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 (
<Tooltip
label={isDark ? 'Mode Terang' : 'Mode Gelap'}
position={tooltipPosition}
withArrow
disabled={!showTooltip}
>
<ActionIcon
variant={variant}
size={size}
radius="xl"
onClick={toggle}
color={iconColor}
style={{
transition: 'all 0.3s ease',
transform: 'scale(1)',
'&:hover': {
transform: 'scale(1.1)',
},
}}
>
{/* Icon Sun untuk Light Mode */}
<Transition mounted={!isDark} transition="scale" duration={200}>
{(style) => (
<IconSun style={style} size={20} stroke={1.5} />
)}
</Transition>
{/* Icon Moon untuk Dark Mode */}
<Transition mounted={isDark} transition="scale" duration={200}>
{(style) => (
<IconMoon style={style} size={20} stroke={1.5} />
)}
</Transition>
</ActionIcon>
</Tooltip>
);
}
export default DarkModeToggle;

View File

@@ -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
<UnifiedTitle order={1}>Judul Halaman</UnifiedTitle>
<UnifiedTitle order={2}>Sub Judul</UnifiedTitle>
<UnifiedTitle order={3}>Section Title</UnifiedTitle>
<UnifiedTitle order={4}>Card Title</UnifiedTitle>
// Text dengan color semantic
<UnifiedText size="body" color="primary">Teks primary</UnifiedText>
<UnifiedText size="body" color="secondary">Teks secondary</UnifiedText>
<UnifiedText size="body" color="muted">Teks muted</UnifiedText>
<UnifiedText size="body" color="link">Link text</UnifiedText>
<UnifiedText size="body" color="brand">Brand color</UnifiedText>
// Dengan weight
<UnifiedText weight="bold">Teks bold</UnifiedText>
<UnifiedText weight="medium">Teks medium</UnifiedText>
```
#### B. Surface Components
```tsx
import UnifiedCard, { UnifiedDivider } from '@/components/admin/UnifiedSurface';
// Card sederhana - border dan warna otomatis dark mode
<UnifiedCard>
<p>Isi card</p>
</UnifiedCard>
// Card dengan sections
<UnifiedCard>
<UnifiedCard.Header>
<UnifiedTitle order={4}>Header</UnifiedTitle>
</UnifiedCard.Header>
<UnifiedCard.Body>
<p>Body content</p>
</UnifiedCard.Body>
<UnifiedCard.Footer>
<Button>Action</Button>
</UnifiedCard.Footer>
</UnifiedCard>
// Divider dengan variant
<UnifiedDivider variant="soft" /> {/* Default */}
<UnifiedDivider variant="default" />
<UnifiedDivider variant="strong" />
```
#### C. Page Header Component
```tsx
import { UnifiedPageHeader } from '@/components/admin/UnifiedTypography';
<UnifiedPageHeader
title="Daftar Berita"
subtitle="Kelola semua berita di sini"
action={
<Button onClick={handleCreate}>
<IconPlus /> Tambah Baru
</Button>
}
/>
```
### 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 (
<div style={{
backgroundColor: tokens.colors.bg.card,
color: tokens.colors.text.primary,
padding: tokens.spacing.md,
borderRadius: tokens.radius.lg,
border: `1px solid ${tokens.colors.border.default}`,
}}>
<p style={{ fontSize: tokens.typography.body.fz }}>
Konten dengan styling konsisten
</p>
</div>
);
}
```
---
## 🌓 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 (
<div>
<p>Current mode: {isDark ? 'Dark' : 'Light'}</p>
{/* Gunakan component toggle */}
<DarkModeToggle />
{/* Atau manual */}
<button onClick={toggle}>Toggle</button>
</div>
);
}
```
### 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 (
<div>
{/* Header Halaman */}
<UnifiedPageHeader
title="Daftar Berita"
subtitle="Kelola semua berita yang diterbitkan"
action={
<Button
bg={tokens.colors.primary}
style={{
background: `linear-gradient(135deg, ${tokens.colors.primary}, ${isDark ? '#60A5FA' : '#4facfe'})`,
}}
>
+ Tambah Berita
</Button>
}
/>
{/* Card untuk Table */}
<UnifiedCard>
<Table>
<TableThead>
<TableTr>
<TableTh style={{ backgroundColor: tokens.colors.bg.surface }}>
<UnifiedText size="label" color="secondary">Judul</UnifiedText>
</TableTh>
<TableTh style={{ backgroundColor: tokens.colors.bg.surface }}>
<UnifiedText size="label" color="secondary">Kategori</UnifiedText>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.map((item) => (
<TableTr
key={item.id}
style={{
'&:hover': {
backgroundColor: tokens.colors.bg.hover,
},
}}
>
<TableTd>
<UnifiedText size="body">{item.judul}</UnifiedText>
</TableTd>
<TableTd>
<UnifiedText size="small" color="secondary">
{item.kategori}
</UnifiedText>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</UnifiedCard>
</div>
);
}
```
### 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 (
<UnifiedCard>
<UnifiedCard.Header>
<UnifiedTitle order={3} color="brand">{data.judul}</UnifiedTitle>
</UnifiedCard.Header>
<UnifiedCard.Body>
<Box mb="md">
<UnifiedText size="label" color="muted">Kategori</UnifiedText>
<UnifiedText size="body" weight="medium">{data.kategori}</UnifiedText>
</Box>
<UnifiedDivider variant="soft" />
<Box my="md">
<UnifiedText size="label" color="muted">Deskripsi</UnifiedText>
<UnifiedText size="body">{data.deskripsi}</UnifiedText>
</Box>
<UnifiedDivider variant="soft" />
<Box mt="md">
<UnifiedText size="label" color="muted">Konten</UnifiedText>
<div dangerouslySetInnerHTML={{ __html: data.content }} />
</Box>
</UnifiedCard.Body>
<UnifiedCard.Footer>
<Group justify="right">
<Button color="red">Hapus</Button>
<Button color="green">Edit</Button>
</Group>
</UnifiedCard.Footer>
</UnifiedCard>
);
}
```
---
## 🎨 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

View File

@@ -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';
*
* <UnifiedCard>
* <UnifiedCard.Header>Title</UnifiedCard.Header>
* <UnifiedCard.Body>Content</UnifiedCard.Body>
* </UnifiedCard>
*/
// ============================================================================
// 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 (
<Paper
withBorder={withBorder}
bg={tokens.colors.bg.card}
p={getPadding()}
radius={tokens.radius.lg} // 12-16px sesuai spec
style={{
borderColor: tokens.colors.border.default,
transition: hoverable
? 'transform 0.2s ease, box-shadow 0.2s ease'
: 'box-shadow 0.2s ease',
'&:hover': hoverable
? {
transform: 'translateY(-2px)',
}
: {},
...style,
}}
{...props}
>
{children}
</Paper>
);
}
// ============================================================================
// 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 (
<Box
style={{
paddingBottom: getPadding(),
borderBottom,
borderTop,
...style,
}}
>
{children}
</Box>
);
};
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 (
<Box style={{ paddingTop: getPadding(), paddingBottom: getPadding(), ...style }}>
{children}
</Box>
);
};
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 (
<Box
style={{
paddingTop: getPadding(),
borderBottom,
borderTop,
...style,
}}
>
{children}
</Box>
);
};
// ============================================================================
// 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 <Divider my={my} color={getColor()} {...props} />;
}
export default UnifiedCard;

View File

@@ -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';
*
* <UnifiedTitle order={1}>Judul Halaman</UnifiedTitle>
* <UnifiedText size="body">Konten teks</UnifiedText>
*/
// ============================================================================
// 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 (
<Title
order={order}
ta={align}
fz={{ base: responsiveFz.base, md: typo.fz }}
fw={typo.fw}
lh={typo.lh}
c={getColor()}
mb={mb}
mt={mt}
ml={ml}
mr={mr}
mx={mx}
my={my}
style={style}
>
{children}
</Title>
);
}
// ============================================================================
// 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 (
<Text.Span
ta={align}
fz={typo.fz}
fw={fw}
lh={typo.lh}
c={textColor}
lineClamp={lineClamp}
truncate={truncate}
style={style}
>
{children}
</Text.Span>
);
}
return (
<Text
ta={align}
fz={typo.fz}
fw={fw}
lh={typo.lh}
c={textColor}
lineClamp={lineClamp}
truncate={truncate}
style={style}
>
{children}
</Text>
);
}
// ============================================================================
// 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 (
<Box
mb="lg"
style={{
borderBottom: showBorder ? `1px solid ${tokens.colors.border.soft}` : 'none',
paddingBottom: showBorder ? tokens.spacing.md : 0,
...style,
}}
{...props}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: tokens.spacing.md,
}}
>
<div>
<UnifiedTitle order={3} color="primary">{title}</UnifiedTitle>
{subtitle && (
<UnifiedText size="small" color="secondary" mt="xs">
{subtitle}
</UnifiedText>
)}
</div>
{action && <div>{action}</div>}
</div>
</Box>
);
}
export default UnifiedText;