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:
268
src/components/admin/UnifiedTypography.tsx
Normal file
268
src/components/admin/UnifiedTypography.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user