Files
desa-darmasaba/src/components/admin/UnifiedSurface.tsx
2026-02-25 21:18:26 +08:00

258 lines
6.0 KiB
TypeScript

'use client';
import { useDarkMode } from '@/state/darkModeStore';
import { themeTokens } from '@/utils/themeTokens';
import { Box, BoxProps, Divider, DividerProps, Paper } 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;
}
};
const getShadow = () => {
if (shadow === 'none') return 'none';
return tokens.shadows[shadow];
};
return (
<Paper
withBorder={withBorder}
bg={tokens.colors.bg.card}
p={getPadding()}
radius={tokens.radius.lg} // 12-16px sesuai spec
shadow={getShadow()}
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;