@umituz/web-design-system 1.1.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -3
- package/src/global.d.ts +20 -0
- package/src/infrastructure/error/ErrorDisplay.tsx +6 -12
- package/src/infrastructure/error/SuspenseWrapper.tsx +15 -10
- package/src/infrastructure/performance/useMemoryOptimization.ts +1 -1
- package/src/infrastructure/utils/cn.util.ts +7 -7
- package/src/infrastructure/utils/index.ts +1 -1
- package/src/presentation/atoms/Input.tsx +1 -1
- package/src/presentation/atoms/Slider.tsx +1 -1
- package/src/presentation/atoms/Text.tsx +1 -1
- package/src/presentation/atoms/Tooltip.tsx +2 -2
- package/src/presentation/hooks/index.ts +2 -1
- package/src/presentation/hooks/useClickOutside.ts +1 -1
- package/src/presentation/molecules/CheckboxGroup.tsx +1 -1
- package/src/presentation/molecules/FormField.tsx +2 -2
- package/src/presentation/molecules/InputGroup.tsx +1 -1
- package/src/presentation/molecules/RadioGroup.tsx +1 -1
- package/src/presentation/organisms/Alert.tsx +1 -1
- package/src/presentation/organisms/Card.tsx +1 -1
- package/src/presentation/organisms/Footer.tsx +99 -0
- package/src/presentation/organisms/Navbar.tsx +1 -1
- package/src/presentation/organisms/Table.tsx +1 -1
- package/src/presentation/organisms/index.ts +3 -0
- package/src/presentation/templates/Form.tsx +2 -2
- package/src/presentation/templates/List.tsx +1 -1
- package/src/presentation/templates/PageHeader.tsx +78 -0
- package/src/presentation/templates/PageLayout.tsx +38 -0
- package/src/presentation/templates/ProjectSkeleton.tsx +35 -0
- package/src/presentation/templates/Section.tsx +1 -1
- package/src/presentation/templates/index.ts +9 -0
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-design-system",
|
|
3
|
-
"version": "1.1
|
|
4
|
-
"
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Web Design System - Atomic Design components (Atoms, Molecules, Organisms, Templates) for React applications",
|
|
5
6
|
"main": "./src/index.ts",
|
|
6
7
|
"types": "./src/index.ts",
|
|
7
8
|
"sideEffects": false,
|
|
@@ -52,13 +53,17 @@
|
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
55
|
"react": ">=18.0.0",
|
|
55
|
-
"react-dom": ">=18.0.0"
|
|
56
|
+
"react-dom": ">=18.0.0",
|
|
57
|
+
"clsx": ">=2.0.0",
|
|
58
|
+
"tailwind-merge": ">=2.0.0"
|
|
56
59
|
},
|
|
57
60
|
"devDependencies": {
|
|
58
61
|
"@types/react": "^18.0.0",
|
|
59
62
|
"@types/react-dom": "^18.0.0",
|
|
63
|
+
"clsx": "^2.1.1",
|
|
60
64
|
"react": "^18.0.0",
|
|
61
65
|
"react-dom": "^18.0.0",
|
|
66
|
+
"tailwind-merge": "^3.5.0",
|
|
62
67
|
"typescript": "~5.9.2"
|
|
63
68
|
},
|
|
64
69
|
"publishConfig": {
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Type Declarations
|
|
3
|
+
* @description Global type definitions for the design system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface ImportMetaEnv {
|
|
8
|
+
readonly DEV: boolean;
|
|
9
|
+
readonly MODE: string;
|
|
10
|
+
readonly BASE_URL: string;
|
|
11
|
+
readonly PROD: boolean;
|
|
12
|
+
readonly SSR: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ImportMeta {
|
|
16
|
+
readonly env: ImportMetaEnv;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export {};
|
|
@@ -55,13 +55,9 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|
|
55
55
|
const errorMessage = error instanceof Error ? error.message : (error || 'Unknown error');
|
|
56
56
|
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
57
57
|
|
|
58
|
-
const containerStyle: React.CSSProperties = {
|
|
59
|
-
className
|
|
60
|
-
};
|
|
61
|
-
|
|
62
58
|
if (variant === 'minimal') {
|
|
63
59
|
return (
|
|
64
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '14px', color: config.color
|
|
60
|
+
<div className={className} style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '14px', color: config.color }}>
|
|
65
61
|
<span>{config.icon}</span>
|
|
66
62
|
<span>{errorMessage}</span>
|
|
67
63
|
</div>
|
|
@@ -70,12 +66,11 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|
|
70
66
|
|
|
71
67
|
if (variant === 'inline') {
|
|
72
68
|
return (
|
|
73
|
-
<div style={{
|
|
69
|
+
<div className={className} style={{
|
|
74
70
|
padding: '12px 16px',
|
|
75
71
|
background: config.bgColor,
|
|
76
72
|
border: `1px solid ${config.borderColor}`,
|
|
77
|
-
borderRadius: '6px'
|
|
78
|
-
...containerStyle
|
|
73
|
+
borderRadius: '6px'
|
|
79
74
|
}}>
|
|
80
75
|
<span style={{ marginRight: '8px' }}>{config.icon}</span>
|
|
81
76
|
<span style={{ fontWeight: 500 }}>{title}:</span> {errorMessage}
|
|
@@ -85,12 +80,11 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|
|
85
80
|
|
|
86
81
|
if (variant === 'card') {
|
|
87
82
|
return (
|
|
88
|
-
<div style={{
|
|
83
|
+
<div className={className} style={{
|
|
89
84
|
background: 'white',
|
|
90
85
|
borderRadius: '8px',
|
|
91
86
|
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
|
92
|
-
border: 'none'
|
|
93
|
-
...containerStyle
|
|
87
|
+
border: 'none'
|
|
94
88
|
}}>
|
|
95
89
|
<div style={{ padding: '24px' }}>
|
|
96
90
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
|
@@ -209,7 +203,7 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
|
|
|
209
203
|
|
|
210
204
|
// Default variant
|
|
211
205
|
return (
|
|
212
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px'
|
|
206
|
+
<div className={className} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}>
|
|
213
207
|
<div style={{
|
|
214
208
|
width: '100%',
|
|
215
209
|
maxWidth: '448px',
|
|
@@ -8,6 +8,7 @@ export interface SuspenseWrapperProps {
|
|
|
8
8
|
loadingText?: string;
|
|
9
9
|
variant?: 'default' | 'minimal' | 'card';
|
|
10
10
|
className?: string;
|
|
11
|
+
style?: React.CSSProperties;
|
|
11
12
|
showErrorBoundary?: boolean;
|
|
12
13
|
errorBoundaryLevel?: 'page' | 'component' | 'feature';
|
|
13
14
|
}
|
|
@@ -41,6 +42,7 @@ export const SuspenseWrapper: React.FC<SuspenseWrapperProps> = ({
|
|
|
41
42
|
loadingText = 'Loading...',
|
|
42
43
|
variant = 'default',
|
|
43
44
|
className = '',
|
|
45
|
+
style,
|
|
44
46
|
showErrorBoundary = true,
|
|
45
47
|
errorBoundaryLevel = 'component'
|
|
46
48
|
}) => {
|
|
@@ -57,18 +59,21 @@ export const SuspenseWrapper: React.FC<SuspenseWrapperProps> = ({
|
|
|
57
59
|
</Suspense>
|
|
58
60
|
);
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
const content = showErrorBoundary ? (
|
|
63
|
+
<ErrorBoundary
|
|
64
|
+
level={errorBoundaryLevel}
|
|
65
|
+
fallback={errorFallback}
|
|
66
|
+
>
|
|
67
|
+
{suspenseContent}
|
|
68
|
+
</ErrorBoundary>
|
|
69
|
+
) : suspenseContent;
|
|
70
|
+
|
|
71
|
+
// Wrap in div if className or style is provided
|
|
72
|
+
if (className || style) {
|
|
73
|
+
return <div className={className} style={style}>{content}</div>;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
return
|
|
76
|
+
return content;
|
|
72
77
|
};
|
|
73
78
|
|
|
74
79
|
// Specialized Suspense wrappers
|
|
@@ -249,7 +249,7 @@ export const useMemoryOptimization = (config: MemoryOptimizationConfig = {}) =>
|
|
|
249
249
|
};
|
|
250
250
|
|
|
251
251
|
export const useMemoryLeakDetector = (componentName?: string) => {
|
|
252
|
-
const mountTime = useRef<number>(
|
|
252
|
+
const mountTime = useRef<number>(Date.now());
|
|
253
253
|
const [renderCount, setRenderCount] = useState<number>(0);
|
|
254
254
|
const [lifespan, setLifespan] = useState<number>(0);
|
|
255
255
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* cn Utility
|
|
3
|
-
* @description Conditional className utility
|
|
3
|
+
* @description Conditional className utility using clsx + tailwind-merge for proper Tailwind class merging
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
7
|
+
import { twMerge } from 'tailwind-merge';
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.join(' ');
|
|
9
|
+
export type { ClassValue };
|
|
10
|
+
|
|
11
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
12
|
+
return twMerge(clsx(inputs));
|
|
13
13
|
}
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type InputHTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, SizeVariant } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface InputProps extends InputHTMLAttributes<HTMLInputElement>, BaseProps {
|
|
10
|
+
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, BaseProps {
|
|
11
11
|
error?: boolean;
|
|
12
12
|
size?: Extract<SizeVariant, 'sm' | 'md' | 'lg'>;
|
|
13
13
|
}
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type InputHTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, BaseProps {
|
|
10
|
+
export interface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'value'>, BaseProps {
|
|
11
11
|
min?: number;
|
|
12
12
|
max?: number;
|
|
13
13
|
step?: number;
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type HTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export type TextElement = 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
10
|
+
export type TextElement = 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label';
|
|
11
11
|
export type TextVariant = 'body' | 'heading' | 'label' | 'caption';
|
|
12
12
|
export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type HTMLAttributes, type ReactNode, useState, useRef, useE
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface TooltipProps extends HTMLAttributes<HTMLDivElement>, BaseProps
|
|
10
|
+
export interface TooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'content'>, BaseProps {
|
|
11
11
|
content: ReactNode;
|
|
12
12
|
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
13
13
|
delay?: number;
|
|
@@ -23,7 +23,7 @@ const placementStyles: Record<'top' | 'bottom' | 'left' | 'right', string> = {
|
|
|
23
23
|
export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
|
|
24
24
|
({ className, children, content, placement = 'top', delay = 200, ...props }, ref) => {
|
|
25
25
|
const [isOpen, setIsOpen] = useState(false);
|
|
26
|
-
const timeoutRef = useRef<
|
|
26
|
+
const timeoutRef = useRef<number>();
|
|
27
27
|
|
|
28
28
|
const handleMouseEnter = () => {
|
|
29
29
|
timeoutRef.current = setTimeout(() => {
|
|
@@ -15,11 +15,12 @@ export { useLocalStorage } from './useLocalStorage';
|
|
|
15
15
|
export { useClickOutside } from './useClickOutside';
|
|
16
16
|
|
|
17
17
|
export { useKeyboard, useEscape } from './useKeyboard';
|
|
18
|
-
export type { KeyboardKey, KeyboardModifier, KeyboardOptions
|
|
18
|
+
export type { KeyboardKey, KeyboardModifier, KeyboardOptions } from './useKeyboard';
|
|
19
19
|
|
|
20
20
|
export { useDebounce } from './useDebounce';
|
|
21
21
|
|
|
22
22
|
export { useClipboard } from './useClipboard';
|
|
23
|
+
export type { UseClipboardReturn } from './useClipboard';
|
|
23
24
|
|
|
24
25
|
export { useToggle } from './useToggle';
|
|
25
26
|
|
|
@@ -14,7 +14,7 @@ export function useClickOutside<T extends HTMLElement>(
|
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
if (!enabled) return;
|
|
16
16
|
|
|
17
|
-
const handleClick = (event:
|
|
17
|
+
const handleClick = (event: Event) => {
|
|
18
18
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
19
19
|
callback();
|
|
20
20
|
}
|
|
@@ -15,7 +15,7 @@ export interface CheckboxOption {
|
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface CheckboxGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
18
|
+
export interface CheckboxGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>, BaseProps {
|
|
19
19
|
options: CheckboxOption[];
|
|
20
20
|
value?: string[];
|
|
21
21
|
onChange?: (values: string[]) => void;
|
|
@@ -27,10 +27,10 @@ export const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
|
|
|
27
27
|
return (
|
|
28
28
|
<div className={cn('space-y-1.5', className)} {...props}>
|
|
29
29
|
{label && (
|
|
30
|
-
<
|
|
30
|
+
<label htmlFor={fieldId} className="text-sm font-medium">
|
|
31
31
|
{label}
|
|
32
32
|
{required && <span className="text-destructive ml-1">*</span>}
|
|
33
|
-
</
|
|
33
|
+
</label>
|
|
34
34
|
)}
|
|
35
35
|
|
|
36
36
|
<Input
|
|
@@ -8,7 +8,7 @@ import { cn } from '../../infrastructure/utils';
|
|
|
8
8
|
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
9
|
import { Input } from '../atoms/Input';
|
|
10
10
|
|
|
11
|
-
export interface InputGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps
|
|
11
|
+
export interface InputGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
12
12
|
leftElement?: ReactNode;
|
|
13
13
|
rightElement?: ReactNode;
|
|
14
14
|
}
|
|
@@ -15,7 +15,7 @@ export interface RadioOption {
|
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface RadioGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
18
|
+
export interface RadioGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>, BaseProps {
|
|
19
19
|
name: string;
|
|
20
20
|
options: RadioOption[];
|
|
21
21
|
value?: string;
|
|
@@ -10,7 +10,7 @@ import { Icon } from '../atoms/Icon';
|
|
|
10
10
|
|
|
11
11
|
export type AlertVariant = Extract<ColorVariant, 'success' | 'warning' | 'destructive'> | 'info';
|
|
12
12
|
|
|
13
|
-
export interface AlertProps extends HTMLAttributes<HTMLDivElement>, BaseProps
|
|
13
|
+
export interface AlertProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
14
14
|
variant?: AlertVariant;
|
|
15
15
|
showIcon?: boolean;
|
|
16
16
|
}
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type HTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface CardProps extends HTMLAttributes<HTMLDivElement>, BaseProps
|
|
10
|
+
export interface CardProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
11
11
|
variant?: 'default' | 'outlined' | 'elevated';
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer Organism Component
|
|
3
|
+
* @description Footer with brand info, links, and social icons
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface FooterProps extends HTMLAttributes<HTMLElement>, BaseProps {
|
|
11
|
+
brand?: {
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
};
|
|
15
|
+
sections?: Array<{
|
|
16
|
+
title: string;
|
|
17
|
+
links: Array<{ label: string; href: string }>;
|
|
18
|
+
}>;
|
|
19
|
+
social?: Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
href: string;
|
|
22
|
+
icon: ReactNode;
|
|
23
|
+
}>;
|
|
24
|
+
copyright?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Footer = forwardRef<HTMLElement, FooterProps>(
|
|
28
|
+
({ className, brand, sections, social, copyright = '© 2026 UmitUZ. All rights reserved.', ...props }, ref) => {
|
|
29
|
+
return (
|
|
30
|
+
<footer
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={cn('bg-bg-primary border-t border-border mt-20 transition-theme', className)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
<div className="max-w-7xl mx-auto px-4 py-12">
|
|
36
|
+
<div className="grid md:grid-cols-3 gap-8">
|
|
37
|
+
{/* Brand */}
|
|
38
|
+
{brand && (
|
|
39
|
+
<div>
|
|
40
|
+
<h3 className="text-xl font-bold text-text-primary mb-3">{brand.name}</h3>
|
|
41
|
+
{brand.description && (
|
|
42
|
+
<p className="text-text-secondary text-sm">{brand.description}</p>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{/* Sections */}
|
|
48
|
+
{sections?.map((section, index) => (
|
|
49
|
+
<div key={index}>
|
|
50
|
+
<h4 className="font-semibold text-text-primary mb-3">{section.title}</h4>
|
|
51
|
+
<ul className="space-y-2 text-sm">
|
|
52
|
+
{section.links.map((link, linkIndex) => (
|
|
53
|
+
<li key={linkIndex}>
|
|
54
|
+
<a
|
|
55
|
+
href={link.href}
|
|
56
|
+
className="text-text-secondary hover:text-primary-light transition-colors"
|
|
57
|
+
>
|
|
58
|
+
{link.label}
|
|
59
|
+
</a>
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
|
|
66
|
+
{/* Social */}
|
|
67
|
+
{social && (
|
|
68
|
+
<div>
|
|
69
|
+
<h4 className="font-semibold text-text-primary mb-3">Connect</h4>
|
|
70
|
+
<div className="flex gap-4">
|
|
71
|
+
{social.map((item, index) => (
|
|
72
|
+
<a
|
|
73
|
+
key={index}
|
|
74
|
+
href={item.href}
|
|
75
|
+
target="_blank"
|
|
76
|
+
rel="noopener noreferrer"
|
|
77
|
+
className="text-text-secondary hover:text-primary-light transition-colors"
|
|
78
|
+
>
|
|
79
|
+
{item.icon}
|
|
80
|
+
</a>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Copyright */}
|
|
88
|
+
{copyright && (
|
|
89
|
+
<div className="border-t border-border mt-8 pt-8 text-center text-sm text-text-secondary">
|
|
90
|
+
<p>{copyright}</p>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
</footer>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
Footer.displayName = 'Footer';
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type HTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface NavbarProps extends HTMLAttributes<HTMLElement>, BaseProps
|
|
10
|
+
export interface NavbarProps extends HTMLAttributes<HTMLElement>, BaseProps {
|
|
11
11
|
variant?: 'default' | 'sticky' | 'fixed';
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type HTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface TableProps extends HTMLAttributes<HTMLTableElement>, BaseProps
|
|
10
|
+
export interface TableProps extends HTMLAttributes<HTMLTableElement>, BaseProps {}
|
|
11
11
|
|
|
12
12
|
export const Table = forwardRef<HTMLTableElement, TableProps>(
|
|
13
13
|
({ className, children, ...props }, ref) => {
|
|
@@ -36,3 +36,6 @@ export type { AccordionProps, AccordionItem } from './Accordion';
|
|
|
36
36
|
|
|
37
37
|
export { Breadcrumbs } from './Breadcrumb';
|
|
38
38
|
export type { BreadcrumbsProps, BreadcrumbItem } from './Breadcrumb';
|
|
39
|
+
|
|
40
|
+
export { Footer } from './Footer';
|
|
41
|
+
export type { FooterProps } from './Footer';
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* @description Complete form structure
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { forwardRef, type FormHTMLAttributes } from 'react';
|
|
6
|
+
import { forwardRef, type FormHTMLAttributes, type HTMLAttributes } from 'react';
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface FormProps extends FormHTMLAttributes<HTMLFormElement>, BaseProps
|
|
10
|
+
export interface FormProps extends FormHTMLAttributes<HTMLFormElement>, BaseProps {
|
|
11
11
|
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type HTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface ListProps extends HTMLAttributes<HTMLUListElement>, BaseProps
|
|
10
|
+
export interface ListProps extends HTMLAttributes<HTMLUListElement>, BaseProps {
|
|
11
11
|
variant?: 'default' | 'bordered' | 'spaced';
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageHeader Template Component
|
|
3
|
+
* @description Centralized page header with consistent heading styling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
+
import { type ReactNode } from 'react';
|
|
8
|
+
import { cn } from '../../infrastructure/utils';
|
|
9
|
+
import type { BaseProps } from '../../domain/types';
|
|
10
|
+
|
|
11
|
+
export type TextAlign = 'left' | 'center' | 'right';
|
|
12
|
+
export type HeaderSize = 'small' | 'medium' | 'large';
|
|
13
|
+
|
|
14
|
+
export interface PageHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'>, BaseProps {
|
|
15
|
+
title: ReactNode;
|
|
16
|
+
subtitle?: string;
|
|
17
|
+
align?: TextAlign;
|
|
18
|
+
size?: HeaderSize;
|
|
19
|
+
actions?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sizeClasses: Record<HeaderSize, { title: string; subtitle: string }> = {
|
|
23
|
+
small: {
|
|
24
|
+
title: 'text-2xl md:text-3xl font-bold',
|
|
25
|
+
subtitle: 'text-base md:text-lg',
|
|
26
|
+
},
|
|
27
|
+
medium: {
|
|
28
|
+
title: 'text-3xl md:text-4xl font-bold',
|
|
29
|
+
subtitle: 'text-lg md:text-xl',
|
|
30
|
+
},
|
|
31
|
+
large: {
|
|
32
|
+
title: 'text-4xl md:text-5xl lg:text-6xl font-bold',
|
|
33
|
+
subtitle: 'text-lg md:text-xl',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const alignClasses: Record<TextAlign, string> = {
|
|
38
|
+
left: 'text-left',
|
|
39
|
+
center: 'text-center',
|
|
40
|
+
right: 'text-right',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getAlignFlexClass = (align: TextAlign, hasActions: boolean): string => {
|
|
44
|
+
if (!hasActions) return '';
|
|
45
|
+
return align === 'center'
|
|
46
|
+
? 'flex justify-center'
|
|
47
|
+
: align === 'right'
|
|
48
|
+
? 'flex justify-end'
|
|
49
|
+
: 'flex justify-start';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
|
53
|
+
({ title, subtitle, align = 'center', size = 'large', actions, className, ...props }, ref) => {
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
ref={ref}
|
|
57
|
+
className={cn('mb-6 md:mb-12', alignClasses[align], className)}
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
<h1 className={cn(sizeClasses[size].title, 'text-text-primary mb-4')}>
|
|
61
|
+
{title}
|
|
62
|
+
</h1>
|
|
63
|
+
{subtitle && (
|
|
64
|
+
<p className={cn(sizeClasses[size].subtitle, 'text-text-secondary')}>
|
|
65
|
+
{subtitle}
|
|
66
|
+
</p>
|
|
67
|
+
)}
|
|
68
|
+
{actions && (
|
|
69
|
+
<div className={cn('mt-6 md:mt-8', getAlignFlexClass(align, true))}>
|
|
70
|
+
{actions}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
PageHeader.displayName = 'PageHeader';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageLayout Template Component
|
|
3
|
+
* @description Centralized page layout with consistent padding, max-width, and theme integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export type MaxWidth = '4xl' | '7xl' | 'full';
|
|
11
|
+
|
|
12
|
+
export interface PageLayoutProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
13
|
+
maxWidth?: MaxWidth;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const maxWidthClasses: Record<MaxWidth, string> = {
|
|
17
|
+
'4xl': 'max-w-4xl',
|
|
18
|
+
'7xl': 'max-w-7xl',
|
|
19
|
+
'full': 'max-w-full',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const PageLayout = forwardRef<HTMLDivElement, PageLayoutProps>(
|
|
23
|
+
({ children, maxWidth = '7xl', className, ...props }, ref) => {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
ref={ref}
|
|
27
|
+
className="min-h-screen bg-surface-gradient py-8 md:py-12 px-4 transition-theme"
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
<div className={cn(maxWidthClasses[maxWidth], 'mx-auto', className)}>
|
|
31
|
+
{children}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
PageLayout.displayName = 'PageLayout';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectSkeleton Template Component
|
|
3
|
+
* @description Loading skeleton for project cards
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
+
import { cn } from '../../infrastructure/utils';
|
|
8
|
+
import type { BaseProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface ProjectSkeletonProps extends HTMLAttributes<HTMLDivElement>, BaseProps {}
|
|
11
|
+
|
|
12
|
+
export const ProjectSkeleton = forwardRef<HTMLDivElement, ProjectSkeletonProps>(
|
|
13
|
+
({ className, ...props }, ref) => {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn('bg-bg-card rounded-xl overflow-hidden border border-border transition-theme', className)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<div className="h-28 bg-bg-tertiary animate-pulse" />
|
|
21
|
+
<div className="p-5">
|
|
22
|
+
<div className="flex gap-2 mb-3">
|
|
23
|
+
<div className="w-16 h-6 bg-bg-tertiary rounded-full animate-pulse" />
|
|
24
|
+
<div className="w-20 h-6 bg-bg-tertiary rounded-full animate-pulse" />
|
|
25
|
+
</div>
|
|
26
|
+
<div className="w-3/4 h-5 bg-bg-tertiary rounded mb-2 animate-pulse" />
|
|
27
|
+
<div className="w-full h-4 bg-bg-tertiary rounded mb-4 animate-pulse" />
|
|
28
|
+
<div className="w-1/2 h-4 bg-bg-tertiary rounded animate-pulse" />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
ProjectSkeleton.displayName = 'ProjectSkeleton';
|
|
@@ -7,7 +7,7 @@ import { forwardRef, type HTMLAttributes } from 'react';
|
|
|
7
7
|
import { cn } from '../../infrastructure/utils';
|
|
8
8
|
import type { BaseProps, ChildrenProps } from '../../domain/types';
|
|
9
9
|
|
|
10
|
-
export interface SectionProps extends HTMLAttributes<HTMLElement>, BaseProps
|
|
10
|
+
export interface SectionProps extends HTMLAttributes<HTMLElement>, BaseProps {
|
|
11
11
|
title?: string;
|
|
12
12
|
description?: string;
|
|
13
13
|
}
|
|
@@ -12,3 +12,12 @@ export type { ListProps } from './List';
|
|
|
12
12
|
|
|
13
13
|
export { Section } from './Section';
|
|
14
14
|
export type { SectionProps } from './Section';
|
|
15
|
+
|
|
16
|
+
export { PageLayout } from './PageLayout';
|
|
17
|
+
export type { PageLayoutProps, MaxWidth } from './PageLayout';
|
|
18
|
+
|
|
19
|
+
export { PageHeader } from './PageHeader';
|
|
20
|
+
export type { PageHeaderProps, TextAlign, HeaderSize } from './PageHeader';
|
|
21
|
+
|
|
22
|
+
export { ProjectSkeleton } from './ProjectSkeleton';
|
|
23
|
+
export type { ProjectSkeletonProps } from './ProjectSkeleton';
|