@umituz/web-design-system 2.7.2 → 2.8.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-design-system",
3
- "version": "2.7.2",
3
+ "version": "2.8.0",
4
4
  "private": false,
5
5
  "description": "Web Design System - Atomic Design components (Atoms, Molecules, Organisms, Templates) for React applications",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Responsive Design Tokens
3
+ * @description Centralized mapping of size variants to Tailwind spacing scale values
4
+ *
5
+ * Scale values align with Tailwind's default spacing scale:
6
+ * 3=12px, 4=16px, 5=20px, 6=24px, 8=32px, 10=40px, 12=48px, 16=64px, 20=80px
7
+ *
8
+ * All responsive calculations are centralized here - components should NOT
9
+ * define their own size styles or do any spacing calculations.
10
+ */
11
+
12
+ import type { SizeVariant, Breakpoint } from '../types';
13
+
14
+ /**
15
+ * Spacing scale values for padding, margin, and gap
16
+ * Format: Tailwind spacing unit numbers (as strings for easier concatenation)
17
+ */
18
+ export type SpacingScale = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '12' | '14' | '16' | '20' | '24';
19
+
20
+ /**
21
+ * Responsive spacing mapping by size variant and breakpoint
22
+ */
23
+ export const RESPONSIVE_SPACING: Record<
24
+ SizeVariant,
25
+ Partial<Record<Breakpoint, SpacingScale>>
26
+ > = {
27
+ xs: {
28
+ xs: '2',
29
+ sm: '3',
30
+ md: '3',
31
+ lg: '4',
32
+ xl: '4',
33
+ '2xl': '5',
34
+ },
35
+ sm: {
36
+ xs: '3',
37
+ sm: '4',
38
+ md: '4',
39
+ lg: '5',
40
+ xl: '6',
41
+ '2xl': '8',
42
+ },
43
+ md: {
44
+ xs: '4',
45
+ sm: '5',
46
+ md: '5',
47
+ lg: '6',
48
+ xl: '8',
49
+ '2xl': '10',
50
+ },
51
+ lg: {
52
+ xs: '5',
53
+ sm: '6',
54
+ md: '6',
55
+ lg: '8',
56
+ xl: '10',
57
+ '2xl': '12',
58
+ },
59
+ xl: {
60
+ xs: '6',
61
+ sm: '8',
62
+ md: '8',
63
+ lg: '10',
64
+ xl: '12',
65
+ '2xl': '16',
66
+ },
67
+ } as const;
68
+
69
+ /**
70
+ * Icon size scale values (height/width)
71
+ */
72
+ export const RESPONSIVE_ICON_SIZE: Record<
73
+ SizeVariant,
74
+ Partial<Record<Breakpoint, SpacingScale>>
75
+ > = {
76
+ xs: {
77
+ xs: '3',
78
+ sm: '4',
79
+ md: '5',
80
+ lg: '5',
81
+ xl: '6',
82
+ '2xl': '7',
83
+ },
84
+ sm: {
85
+ xs: '4',
86
+ sm: '5',
87
+ md: '6',
88
+ lg: '6',
89
+ xl: '8',
90
+ '2xl': '8',
91
+ },
92
+ md: {
93
+ xs: '5',
94
+ sm: '6',
95
+ md: '7',
96
+ lg: '8',
97
+ xl: '9',
98
+ '2xl': '10',
99
+ },
100
+ lg: {
101
+ xs: '6',
102
+ sm: '7',
103
+ md: '8',
104
+ lg: '9',
105
+ xl: '10',
106
+ '2xl': '12',
107
+ },
108
+ xl: {
109
+ xs: '8',
110
+ sm: '9',
111
+ md: '10',
112
+ lg: '12',
113
+ xl: '14',
114
+ '2xl': '16',
115
+ },
116
+ } as const;
117
+
118
+ /**
119
+ * Container/icon wrapper size (for rounded containers that hold icons)
120
+ */
121
+ export const RESPONSIVE_CONTAINER_SIZE: Record<
122
+ SizeVariant,
123
+ Partial<Record<Breakpoint, SpacingScale>>
124
+ > = {
125
+ xs: {
126
+ xs: '6',
127
+ sm: '8',
128
+ md: '8',
129
+ lg: '10',
130
+ xl: '10',
131
+ '2xl': '12',
132
+ },
133
+ sm: {
134
+ xs: '8',
135
+ sm: '10',
136
+ md: '10',
137
+ lg: '12',
138
+ xl: '12',
139
+ '2xl': '14',
140
+ },
141
+ md: {
142
+ xs: '10',
143
+ sm: '12',
144
+ md: '12',
145
+ lg: '14',
146
+ xl: '14',
147
+ '2xl': '16',
148
+ },
149
+ lg: {
150
+ xs: '12',
151
+ sm: '14',
152
+ md: '14',
153
+ lg: '16',
154
+ xl: '16',
155
+ '2xl': '20',
156
+ },
157
+ xl: {
158
+ xs: '14',
159
+ sm: '16',
160
+ md: '16',
161
+ lg: '20',
162
+ xl: '20',
163
+ '2xl': '24',
164
+ },
165
+ } as const;
166
+
167
+ /**
168
+ * Text size scale (Tailwind text utilities)
169
+ */
170
+ export const RESPONSIVE_TEXT_SIZE: Record<
171
+ SizeVariant,
172
+ Partial<Record<'mobile' | 'tablet', string>>
173
+ > = {
174
+ xs: {
175
+ mobile: 'text-xs',
176
+ tablet: 'sm:text-xs',
177
+ },
178
+ sm: {
179
+ mobile: 'text-xs',
180
+ tablet: 'sm:text-sm',
181
+ },
182
+ md: {
183
+ mobile: 'text-sm',
184
+ tablet: 'sm:text-base',
185
+ },
186
+ lg: {
187
+ mobile: 'text-base',
188
+ tablet: 'sm:text-lg',
189
+ },
190
+ xl: {
191
+ mobile: 'text-lg',
192
+ tablet: 'sm:text-xl',
193
+ },
194
+ } as const;
195
+
196
+ /**
197
+ * Gap/Spacing between elements (space-y-*, space-x-*, gap-*)
198
+ */
199
+ export const RESPONSIVE_GAP: Record<
200
+ SizeVariant,
201
+ Partial<Record<Breakpoint, SpacingScale>>
202
+ > = {
203
+ xs: {
204
+ xs: '1',
205
+ sm: '2',
206
+ md: '2',
207
+ lg: '3',
208
+ xl: '3',
209
+ '2xl': '4',
210
+ },
211
+ sm: {
212
+ xs: '2',
213
+ sm: '3',
214
+ md: '3',
215
+ lg: '4',
216
+ xl: '4',
217
+ '2xl': '5',
218
+ },
219
+ md: {
220
+ xs: '3',
221
+ sm: '4',
222
+ md: '4',
223
+ lg: '5',
224
+ xl: '5',
225
+ '2xl': '6',
226
+ },
227
+ lg: {
228
+ xs: '4',
229
+ sm: '5',
230
+ md: '5',
231
+ lg: '6',
232
+ xl: '6',
233
+ '2xl': '8',
234
+ },
235
+ xl: {
236
+ xs: '5',
237
+ sm: '6',
238
+ md: '6',
239
+ lg: '8',
240
+ xl: '8',
241
+ '2xl': '10',
242
+ },
243
+ } as const;
@@ -8,3 +8,13 @@ export {
8
8
  cn,
9
9
  } from './cn.util';
10
10
  export type { ClassValue } from './cn.util';
11
+
12
+ export {
13
+ getSpacing,
14
+ getIconSize,
15
+ getContainerSize,
16
+ getTextSize,
17
+ getGap,
18
+ getSpaceY,
19
+ getResponsiveStyles,
20
+ } from './responsive.utils';
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Responsive Utility Functions
3
+ * @description Centralized functions for building responsive Tailwind classes
4
+ * All responsive class construction logic lives here - components should only call these functions
5
+ */
6
+
7
+ import { cn } from './cn.util';
8
+ import type { SizeVariant, Breakpoint } from '../../domain/types';
9
+ import {
10
+ RESPONSIVE_SPACING,
11
+ RESPONSIVE_ICON_SIZE,
12
+ RESPONSIVE_CONTAINER_SIZE,
13
+ RESPONSIVE_TEXT_SIZE,
14
+ RESPONSIVE_GAP,
15
+ } from '../../domain/tokens/responsive-map.tokens';
16
+
17
+ /**
18
+ * Build responsive padding/margin/gap classes
19
+ * @param property - Tailwind spacing property ('p', 'm', 'px', 'py', 'gap', 'space-y', 'space-x')
20
+ * @param size - Size variant (sm, md, lg, xl)
21
+ * @returns Complete responsive class string
22
+ *
23
+ * @example
24
+ * getSpacing('p', 'md') // "p-4 sm:p-5 md:p-5 lg:p-6 xl:p-8 2xl:p-10"
25
+ */
26
+ export function getSpacing(
27
+ property: 'p' | 'm' | 'px' | 'py' | 'pt' | 'pb' | 'pl' | 'pr' | 'gap' | 'space-y' | 'space-x',
28
+ size: SizeVariant
29
+ ): string {
30
+ const token = RESPONSIVE_SPACING[size];
31
+
32
+ // Start with base (xs/mobile) value
33
+ const classes: string[] = [`${property}-${token.xs}`];
34
+
35
+ // Add breakpoint-specific classes
36
+ const breakpoints: Breakpoint[] = ['sm', 'md', 'lg', 'xl', '2xl'];
37
+ breakpoints.forEach((bp) => {
38
+ if (token[bp] !== token.xs) {
39
+ classes.push(`${bp}:${property}-${token[bp]}`);
40
+ }
41
+ });
42
+
43
+ return classes.join(' ');
44
+ }
45
+
46
+ /**
47
+ * Build responsive icon size classes (height + width)
48
+ * @param size - Size variant
49
+ * @returns Combined height and width classes
50
+ *
51
+ * @example
52
+ * getIconSize('md') // "h-5 w-5 sm:h-6 sm:w-6 md:h-7 md:w-7 lg:h-8 lg:w-8 xl:h-9 xl:w-9 2xl:h-10 2xl:w-10"
53
+ */
54
+ export function getIconSize(size: SizeVariant): string {
55
+ const token = RESPONSIVE_ICON_SIZE[size];
56
+ const classes: string[] = [`h-${token.xs}`, `w-${token.xs}`];
57
+
58
+ const breakpoints: Breakpoint[] = ['sm', 'md', 'lg', 'xl', '2xl'];
59
+ breakpoints.forEach((bp) => {
60
+ if (token[bp] !== token.xs) {
61
+ classes.push(`${bp}:h-${token[bp]}`, `${bp}:w-${token[bp]}`);
62
+ }
63
+ });
64
+
65
+ return classes.join(' ');
66
+ }
67
+
68
+ /**
69
+ * Build responsive container size classes (for icon wrappers)
70
+ * @param size - Size variant
71
+ * @returns Combined height, width, and flex center classes
72
+ *
73
+ * @example
74
+ * getContainerSize('md') // "w-10 h-10 sm:w-12 sm:h-12 md:w-12 md:h-12..."
75
+ */
76
+ export function getContainerSize(size: SizeVariant): string {
77
+ const token = RESPONSIVE_CONTAINER_SIZE[size];
78
+ const classes: string[] = [`w-${token.xs}`, `h-${token.xs}`];
79
+
80
+ const breakpoints: Breakpoint[] = ['sm', 'md', 'lg', 'xl', '2xl'];
81
+ breakpoints.forEach((bp) => {
82
+ if (token[bp] !== token.xs) {
83
+ classes.push(`${bp}:w-${token[bp]}`, `${bp}:h-${token[bp]}`);
84
+ }
85
+ });
86
+
87
+ return classes.join(' ');
88
+ }
89
+
90
+ /**
91
+ * Get responsive text size classes
92
+ * @param size - Size variant
93
+ * @returns Text size class string
94
+ *
95
+ * @example
96
+ * getTextSize('md') // "text-sm sm:text-base"
97
+ */
98
+ export function getTextSize(size: SizeVariant): string {
99
+ const token = RESPONSIVE_TEXT_SIZE[size];
100
+ return cn(token.mobile, token.tablet);
101
+ }
102
+
103
+ /**
104
+ * Build responsive gap/spacing classes for layout
105
+ * @param size - Size variant
106
+ * @returns Gap class string
107
+ *
108
+ * @example
109
+ * getGap('md') // "gap-3 sm:gap-4 md:gap-4 lg:gap-5 xl:gap-5 2xl:gap-6"
110
+ */
111
+ export function getGap(size: SizeVariant): string {
112
+ return getSpacing('gap', size);
113
+ }
114
+
115
+ /**
116
+ * Build responsive space-y classes for vertical stacking
117
+ * @param size - Size variant
118
+ * @returns Space-y class string
119
+ *
120
+ * @example
121
+ * getSpaceY('md') // "space-y-3 sm:space-y-4 md:space-y-4 lg:space-y-5..."
122
+ */
123
+ export function getSpaceY(size: SizeVariant): string {
124
+ return getSpacing('space-y', size);
125
+ }
126
+
127
+ /**
128
+ * Get complete responsive styles for common component patterns
129
+ * @param size - Size variant
130
+ * @param options - Which styles to include
131
+ * @returns Combined class string
132
+ *
133
+ * @example
134
+ * getResponsiveStyles('md', { padding: true, gap: true })
135
+ * // "p-4 sm:p-5 md:p-5 lg:p-6 xl:p-8 2xl:p-10 gap-3 sm:gap-4..."
136
+ */
137
+ export function getResponsiveStyles(
138
+ size: SizeVariant,
139
+ options: {
140
+ padding?: boolean;
141
+ gap?: boolean;
142
+ iconSize?: boolean;
143
+ textSize?: boolean;
144
+ containerSize?: boolean;
145
+ } = {}
146
+ ): string {
147
+ const classes: string[] = [];
148
+
149
+ if (options.padding) {
150
+ classes.push(getSpacing('p', size));
151
+ }
152
+ if (options.gap) {
153
+ classes.push(getGap(size));
154
+ }
155
+ if (options.iconSize) {
156
+ classes.push(getIconSize(size));
157
+ }
158
+ if (options.textSize) {
159
+ classes.push(getTextSize(size));
160
+ }
161
+ if (options.containerSize) {
162
+ classes.push(getContainerSize(size));
163
+ }
164
+
165
+ return classes.join(' ');
166
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * FormField Component (Molecule)
3
- * @description Label + Input combination
3
+ * @description Label + Input combination (Responsive)
4
4
  */
5
5
 
6
6
  import { forwardRef } from 'react';
7
- import { cn } from '../../infrastructure/utils';
7
+ import { cn, getSpaceY } from '../../infrastructure/utils';
8
8
  import type { BaseProps } from '../../domain/types';
9
9
  import { Input } from '../atoms/Input';
10
10
  import { Text } from '../atoms/Text';
@@ -16,20 +16,21 @@ export interface FormFieldProps extends BaseProps {
16
16
  required?: boolean;
17
17
  id?: string;
18
18
  inputProps?: React.ComponentProps<typeof Input>;
19
+ fullWidth?: boolean; // Enable responsive full width
19
20
  }
20
21
 
21
22
  export const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
22
- ({ label, error, helperText, required, id, className, inputProps, ...props }, ref) => {
23
+ ({ label, error, helperText, required, id, className, inputProps, fullWidth = true, ...props }, ref) => {
23
24
  const fieldId = id || inputProps?.id || inputProps?.name;
24
25
  const errorId = error ? `${fieldId}-error` : undefined;
25
26
  const helperId = helperText ? `${fieldId}-helper` : undefined;
26
27
 
27
28
  return (
28
- <div className={cn('space-y-1.5', className)} {...props}>
29
+ <div className={cn(getSpaceY('sm'), fullWidth && 'w-full', className)} {...props}>
29
30
  {label && (
30
- <label htmlFor={fieldId} className="text-sm font-medium">
31
+ <label htmlFor={fieldId} className="text-sm font-medium block">
31
32
  {label}
32
- {required && <span className="text-destructive ml-1">*</span>}
33
+ {required && <span className="text-destructive ml-1" aria-label="required">*</span>}
33
34
  </label>
34
35
  )}
35
36
 
@@ -21,7 +21,7 @@ const SelectTrigger = React.forwardRef<
21
21
  <SelectPrimitive.Trigger
22
22
  ref={ref}
23
23
  className={cn(
24
- 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
24
+ 'flex h-9 sm:h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
25
25
  className
26
26
  )}
27
27
  {...props}
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * EmptyState Component (Organism)
3
- * @description Component for displaying empty states with optional actions
3
+ * @description Component for displaying empty states with optional actions (Responsive)
4
4
  */
5
5
 
6
6
  import { forwardRef } from 'react';
7
- import { cn } from '../../infrastructure/utils';
7
+ import { cn, getSpacing, getGap } from '../../infrastructure/utils';
8
8
  import type { BaseProps, SizeVariant } from '../../domain/types';
9
9
 
10
10
  export interface EmptyStateProps extends BaseProps {
@@ -16,36 +16,9 @@ export interface EmptyStateProps extends BaseProps {
16
16
  onClick: () => void;
17
17
  };
18
18
  size?: Extract<SizeVariant, 'sm' | 'md' | 'lg' | 'xl'>;
19
+ centered?: boolean;
19
20
  }
20
21
 
21
- const sizeStyles = {
22
- sm: 'p-6 gap-3',
23
- md: 'p-8 gap-4',
24
- lg: 'p-12 gap-5',
25
- xl: 'p-16 gap-6',
26
- };
27
-
28
- const iconSizeStyles = {
29
- sm: 'h-8 w-8',
30
- md: 'h-12 w-12',
31
- lg: 'h-16 w-16',
32
- xl: 'h-20 w-20',
33
- };
34
-
35
- const titleSizeStyles = {
36
- sm: 'text-sm',
37
- md: 'text-base',
38
- lg: 'text-lg',
39
- xl: 'text-xl',
40
- };
41
-
42
- const descriptionSizeStyles = {
43
- sm: 'text-xs',
44
- md: 'text-sm',
45
- lg: 'text-base',
46
- xl: 'text-lg',
47
- };
48
-
49
22
  export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
50
23
  (
51
24
  {
@@ -64,17 +37,18 @@ export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
64
37
  ref={ref}
65
38
  className={cn(
66
39
  'flex flex-col items-center justify-center text-center',
67
- sizeStyles[size],
40
+ getSpacing('p', size),
41
+ getGap(size === 'xl' ? 'lg' : size),
68
42
  className
69
43
  )}
70
44
  {...props}
71
45
  >
72
46
  {Icon && (
73
- <Icon className={cn(iconSizeStyles[size], 'text-muted-foreground/50 mx-auto mb-2')} />
47
+ <Icon className={cn(size === 'sm' ? 'h-8 w-8 sm:h-10 sm:w-10' : size === 'md' ? 'h-12 w-12 sm:h-14 sm:w-14' : size === 'lg' ? 'h-16 w-16 sm:h-20 sm:w-20' : 'h-20 w-20 sm:h-24 sm:w-24', 'text-muted-foreground/50 mx-auto mb-2')} />
74
48
  )}
75
- <h3 className={cn('font-semibold text-foreground', titleSizeStyles[size])}>{title}</h3>
49
+ <h3 className={cn('font-semibold text-foreground', size === 'sm' ? 'text-sm sm:text-base' : size === 'md' ? 'text-base sm:text-lg' : size === 'lg' ? 'text-lg sm:text-xl' : 'text-xl sm:text-2xl')}>{title}</h3>
76
50
  {description && (
77
- <p className={cn('text-muted-foreground max-w-md', descriptionSizeStyles[size])}>
51
+ <p className={cn('text-muted-foreground max-w-md', size === 'sm' ? 'text-xs sm:text-sm' : size === 'md' ? 'text-sm sm:text-base' : size === 'lg' ? 'text-base sm:text-lg' : 'text-lg sm:text-xl')}>
78
52
  {description}
79
53
  </p>
80
54
  )}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Grid Component (Organism)
3
+ * @description Responsive grid system with configurable columns
4
+ */
5
+
6
+ import { cn } from '../../infrastructure/utils';
7
+ import type { BaseProps } from '../../domain/types';
8
+ import type { Breakpoint } from '../../domain/types/breakpoint.types';
9
+
10
+ export interface GridProps extends BaseProps {
11
+ children: React.ReactNode;
12
+ cols?: {
13
+ xs?: number;
14
+ sm?: number;
15
+ md?: number;
16
+ lg?: number;
17
+ xl?: number;
18
+ '2xl'?: number;
19
+ };
20
+ gap?: number | string;
21
+ className?: string;
22
+ }
23
+
24
+ /**
25
+ * Responsive grid system that adjusts columns based on breakpoint
26
+ * @param cols - Number of columns per breakpoint
27
+ * @param gap - Gap between items (spacing unit or custom value)
28
+ * @param children - Grid items
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * // Auto responsive: 1 col mobile, 2 tablet, 3 desktop
33
+ * <Grid cols={{ xs: 1, sm: 2, lg: 3 }}>
34
+ * <div>Item 1</div>
35
+ * <div>Item 2</div>
36
+ * </Grid>
37
+ *
38
+ * // Fixed 4 columns
39
+ * <Grid cols={{ lg: 4 }}>
40
+ * {items.map(item => <Card key={item.id}>{item}</Card>)}
41
+ * </Grid>
42
+ * ```
43
+ */
44
+ export const Grid = ({ children, cols = {}, gap = 4, className }: GridProps) => {
45
+ const {
46
+ xs = 1,
47
+ sm = xs,
48
+ md = sm,
49
+ lg = md,
50
+ xl = lg,
51
+ '2xl': xl2 = xl
52
+ } = cols;
53
+
54
+ const gridCols = cn(
55
+ 'grid',
56
+ `grid-cols-${xs}`, // xs always applies (mobile-first)
57
+ sm !== xs && `sm:grid-cols-${sm}`,
58
+ md !== sm && `md:grid-cols-${md}`,
59
+ lg !== md && `lg:grid-cols-${lg}`,
60
+ xl !== lg && `xl:grid-cols-${xl}`,
61
+ xl2 !== xl && `2xl:grid-cols-${xl2}`
62
+ );
63
+
64
+ const gapClass = typeof gap === 'number' ? `gap-${gap}` : gap;
65
+
66
+ return (
67
+ <div className={cn(gridCols, gapClass, className)}>
68
+ {children}
69
+ </div>
70
+ );
71
+ };
72
+
73
+ Grid.displayName = 'Grid';
74
+
75
+ /**
76
+ * Grid Item component for custom column spans
77
+ */
78
+ export interface GridItemProps extends BaseProps {
79
+ children: React.ReactNode;
80
+ span?: {
81
+ xs?: number;
82
+ sm?: number;
83
+ md?: number;
84
+ lg?: number;
85
+ xl?: number;
86
+ '2xl'?: number;
87
+ };
88
+ className?: string;
89
+ }
90
+
91
+ /**
92
+ * Grid item with custom column span per breakpoint
93
+ * @param span - Number of columns to span (1-12)
94
+ *
95
+ * @example
96
+ * ```tsx
97
+ * <Grid cols={{ lg: 4 }}>
98
+ * <GridItem span={{ lg: 2 }}>Spans 2 columns</GridItem>
99
+ * <GridItem>Regular 1 column</GridItem>
100
+ * </Grid>
101
+ * ```
102
+ */
103
+ export const GridItem = ({ children, span = {}, className }: GridItemProps) => {
104
+ const {
105
+ xs = 1,
106
+ sm = xs,
107
+ md = sm,
108
+ lg = md,
109
+ xl = lg,
110
+ '2xl': xl2 = xl
111
+ } = span;
112
+
113
+ const colSpan = cn(
114
+ 'col-span-1',
115
+ `sm:col-span-${sm}`,
116
+ `md:col-span-${md}`,
117
+ `lg:col-span-${lg}`,
118
+ `xl:col-span-${xl}`,
119
+ `2xl:col-span-${xl2}`
120
+ );
121
+
122
+ return (
123
+ <div className={cn(colSpan, className)}>
124
+ {children}
125
+ </div>
126
+ );
127
+ };
128
+
129
+ GridItem.displayName = 'GridItem';
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * LoadingState Component (Organism)
3
- * @description Component for displaying loading states
3
+ * @description Component for displaying loading states (Responsive)
4
4
  */
5
5
 
6
6
  import { forwardRef } from 'react';
7
- import { cn } from '../../infrastructure/utils';
7
+ import { cn, getSpacing, getGap } from '../../infrastructure/utils';
8
8
  import { Spinner } from '../atoms/Spinner';
9
9
  import type { BaseProps, SizeVariant } from '../../domain/types';
10
10
 
@@ -12,26 +12,9 @@ export interface LoadingStateProps extends BaseProps {
12
12
  message?: string;
13
13
  size?: Extract<SizeVariant, 'sm' | 'md' | 'lg'>;
14
14
  variant?: 'spinner' | 'skeleton' | 'dots';
15
+ centered?: boolean;
15
16
  }
16
17
 
17
- const sizeStyles = {
18
- sm: 'gap-3',
19
- md: 'gap-4',
20
- lg: 'gap-5',
21
- };
22
-
23
- const spinnerSizeStyles = {
24
- sm: 'sm' as const,
25
- md: 'md' as const,
26
- lg: 'lg' as const,
27
- };
28
-
29
- const messageSizeStyles = {
30
- sm: 'text-xs',
31
- md: 'text-sm',
32
- lg: 'text-base',
33
- };
34
-
35
18
  export const LoadingState = forwardRef<HTMLDivElement, LoadingStateProps>(
36
19
  (
37
20
  {
@@ -39,30 +22,34 @@ export const LoadingState = forwardRef<HTMLDivElement, LoadingStateProps>(
39
22
  message,
40
23
  size = 'md',
41
24
  variant = 'spinner',
25
+ centered = true,
42
26
  ...props
43
27
  },
44
28
  ref
45
29
  ) => {
30
+ const spinnerSize: 'sm' | 'md' | 'lg' = size;
46
31
  return (
47
32
  <div
48
33
  ref={ref}
49
34
  className={cn(
50
- 'flex flex-col items-center justify-center p-8',
51
- sizeStyles[size],
35
+ 'flex flex-col items-center justify-center',
36
+ getGap(size),
37
+ getSpacing('p', size),
38
+ centered && 'min-h-[200px] sm:min-h-[300px]',
52
39
  className
53
40
  )}
54
41
  {...props}
55
42
  >
56
- {variant === 'spinner' && <Spinner size={spinnerSizeStyles[size]} />}
43
+ {variant === 'spinner' && <Spinner size={spinnerSize} />}
57
44
  {variant === 'dots' && (
58
- <div className="flex gap-1">
45
+ <div className="flex gap-1 sm:gap-2">
59
46
  {[0, 1, 2].map((i) => (
60
47
  <div
61
48
  key={i}
62
49
  className={cn(
63
50
  'w-2 h-2 bg-primary rounded-full animate-bounce',
64
- size === 'sm' && 'w-1.5 h-1.5',
65
- size === 'lg' && 'w-3 h-3'
51
+ 'w-1.5 h-1.5 sm:w-2 sm:h-2',
52
+ 'lg:w-2.5 h-2.5'
66
53
  )}
67
54
  style={{ animationDelay: `${i * 0.15}s` }}
68
55
  />
@@ -70,14 +57,14 @@ export const LoadingState = forwardRef<HTMLDivElement, LoadingStateProps>(
70
57
  </div>
71
58
  )}
72
59
  {variant === 'skeleton' && (
73
- <div className="w-full space-y-3">
74
- <div className="h-4 bg-muted animate-pulse rounded" style={{ width: '40%' }} />
75
- <div className="h-3 bg-muted animate-pulse rounded" style={{ width: '70%' }} />
76
- <div className="h-3 bg-muted animate-pulse rounded" style={{ width: '60%' }} />
60
+ <div className="w-full space-y-2 sm:space-y-3">
61
+ <div className="h-3 sm:h-4 bg-muted animate-pulse rounded w-2/3 sm:w-1/2" />
62
+ <div className="h-2.5 sm:h-3 bg-muted animate-pulse rounded w-full" />
63
+ <div className="h-2.5 sm:h-3 bg-muted animate-pulse rounded w-3/4" />
77
64
  </div>
78
65
  )}
79
66
  {message && variant !== 'skeleton' && (
80
- <p className={cn('text-muted-foreground mt-2', messageSizeStyles[size])}>{message}</p>
67
+ <p className={cn('text-muted-foreground mt-2 sm:mt-3', size === 'sm' ? 'text-xs sm:text-sm' : size === 'md' ? 'text-sm sm:text-base' : 'text-base sm:text-lg', 'text-center max-w-md')}>{message}</p>
81
68
  )}
82
69
  </div>
83
70
  );
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * MetricCard Component (Organism)
3
- * @description Card component for displaying metrics and statistics with trend indicators
3
+ * @description Card component for displaying metrics and statistics with trend indicators (Responsive)
4
4
  */
5
5
 
6
6
  import { forwardRef } from 'react';
7
- import { cn } from '../../infrastructure/utils';
7
+ import { cn, getSpacing, getIconSize, getTextSize, getGap } from '../../infrastructure/utils';
8
8
  import { Card, CardContent } from './Card';
9
9
  import type { BaseProps, ColorVariant, SizeVariant } from '../../domain/types';
10
10
 
@@ -21,32 +21,9 @@ export interface MetricCardProps extends BaseProps {
21
21
  variant?: ColorVariant;
22
22
  trendValueFormatter?: (value: number) => string;
23
23
  onClick?: () => void;
24
+ responsiveLayout?: boolean; // Enable responsive layout changes
24
25
  }
25
26
 
26
- const sizeStyles = {
27
- sm: 'p-3',
28
- md: 'p-4',
29
- lg: 'p-5',
30
- };
31
-
32
- const valueSizeStyles = {
33
- sm: 'text-lg',
34
- md: 'text-2xl',
35
- lg: 'text-3xl',
36
- };
37
-
38
- const iconSizeStyles = {
39
- sm: 'h-4 w-4',
40
- md: 'h-8 w-8',
41
- lg: 'h-10 w-10',
42
- };
43
-
44
- const labelSizeStyles = {
45
- sm: 'text-xs',
46
- md: 'text-sm',
47
- lg: 'text-base',
48
- };
49
-
50
27
  const trendColors: Record<ColorVariant, { positive: string; negative: string; bg: string }> = {
51
28
  primary: { positive: 'text-primary', negative: 'text-destructive', bg: 'bg-primary' },
52
29
  secondary: { positive: 'text-secondary', negative: 'text-destructive', bg: 'bg-secondary' },
@@ -68,6 +45,7 @@ export const MetricCard = forwardRef<HTMLDivElement, MetricCardProps>(
68
45
  variant = 'primary',
69
46
  trendValueFormatter = (v) => `${v > 0 ? '+' : ''}${v}%`,
70
47
  onClick,
48
+ responsiveLayout = true,
71
49
  ...props
72
50
  },
73
51
  ref
@@ -87,34 +65,48 @@ export const MetricCard = forwardRef<HTMLDivElement, MetricCardProps>(
87
65
  onClick={onClick}
88
66
  {...props}
89
67
  >
90
- <CardContent className={cn(sizeStyles[size])}>
91
- <div className="flex items-center justify-between">
92
- <div className="flex-1">
93
- <p className={cn('text-muted-foreground font-medium', labelSizeStyles[size])}>{title}</p>
94
- <p className={cn('font-bold text-foreground', valueSizeStyles[size])}>
68
+ <CardContent className={getSpacing('p', size)}>
69
+ <div className={cn(
70
+ 'flex items-center justify-between',
71
+ getGap('md'),
72
+ responsiveLayout && 'flex-col sm:flex-row'
73
+ )}>
74
+ <div className={cn(
75
+ 'flex-1',
76
+ responsiveLayout && 'text-center sm:text-left w-full sm:w-auto'
77
+ )}>
78
+ <p className={cn('text-muted-foreground font-medium', getTextSize(size))}>{title}</p>
79
+ <p className={cn('font-bold text-foreground mt-1', size === 'sm' ? 'text-lg sm:text-xl' : size === 'md' ? 'text-2xl' : 'text-3xl sm:text-4xl')}>
95
80
  {typeof value === 'number' ? value.toLocaleString() : value}
96
81
  </p>
97
82
  {trend && (
98
- <div className="flex items-center gap-1 mt-1">
83
+ <div className={cn(
84
+ 'flex items-center gap-1 mt-1',
85
+ responsiveLayout ? 'justify-center sm:justify-start' : ''
86
+ )}>
99
87
  <span
100
88
  className={cn(
101
- 'text-xs font-medium',
89
+ 'text-xs sm:text-sm font-medium',
102
90
  isPositive ? colors.positive : colors.negative
103
91
  )}
104
92
  >
105
93
  {TrendIcon}
106
94
  </span>
107
- <p className={cn('text-xs', isPositive ? colors.positive : colors.negative)}>
95
+ <p className={cn('text-xs sm:text-sm', isPositive ? colors.positive : colors.negative)}>
108
96
  {trendValueFormatter(trend.value)}
109
97
  </p>
110
98
  {trend.label && (
111
- <p className="text-xs text-muted-foreground ml-1">{trend.label}</p>
99
+ <p className="text-xs text-muted-foreground ml-1 hidden sm:inline">{trend.label}</p>
112
100
  )}
113
101
  </div>
114
102
  )}
115
103
  </div>
116
104
  {Icon && (
117
- <Icon className={cn(iconSizeStyles[size], iconColor, 'flex-shrink-0')} />
105
+ <div className={cn(
106
+ responsiveLayout ? 'sm:self-auto' : ''
107
+ )}>
108
+ <Icon className={cn(getIconSize(size), iconColor, 'flex-shrink-0')} />
109
+ </div>
118
110
  )}
119
111
  </div>
120
112
  </CardContent>
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * QuickActionCard Component (Organism)
3
- * @description Card component for quick navigation actions with icon
3
+ * @description Card component for quick navigation actions with icon (Responsive)
4
4
  */
5
5
 
6
6
  import { forwardRef } from 'react';
7
- import { cn } from '../../infrastructure/utils';
7
+ import { cn, getSpacing, getIconSize, getContainerSize, getTextSize, getGap } from '../../infrastructure/utils';
8
8
  import type { BaseProps, SizeVariant } from '../../domain/types';
9
9
 
10
10
  export interface QuickActionCardProps extends BaseProps {
@@ -16,32 +16,9 @@ export interface QuickActionCardProps extends BaseProps {
16
16
  href?: string;
17
17
  size?: Extract<SizeVariant, 'sm' | 'md' | 'lg'>;
18
18
  target?: '_blank' | '_self' | '_parent' | '_top';
19
+ responsiveLayout?: boolean; // Enable responsive layout changes
19
20
  }
20
21
 
21
- const sizeStyles = {
22
- sm: 'p-3 gap-2',
23
- md: 'p-4 gap-3',
24
- lg: 'p-5 gap-4',
25
- };
26
-
27
- const iconSizeStyles = {
28
- sm: 'w-8 h-8',
29
- md: 'w-10 h-10',
30
- lg: 'w-12 h-12',
31
- };
32
-
33
- const iconInnerSizeStyles = {
34
- sm: 'h-4 w-4',
35
- md: 'h-5 w-5',
36
- lg: 'h-6 w-6',
37
- };
38
-
39
- const labelSizeStyles = {
40
- sm: 'text-xs',
41
- md: 'text-sm',
42
- lg: 'text-base',
43
- };
44
-
45
22
  export const QuickActionCard = forwardRef<HTMLAnchorElement | HTMLDivElement, QuickActionCardProps>(
46
23
  (
47
24
  {
@@ -54,6 +31,7 @@ export const QuickActionCard = forwardRef<HTMLAnchorElement | HTMLDivElement, Qu
54
31
  href,
55
32
  size = 'md',
56
33
  target = '_self',
34
+ responsiveLayout = true,
57
35
  ...props
58
36
  },
59
37
  ref
@@ -61,11 +39,15 @@ export const QuickActionCard = forwardRef<HTMLAnchorElement | HTMLDivElement, Qu
61
39
  const content = (
62
40
  <>
63
41
  {Icon && (
64
- <div className={cn(iconSizeStyles[size], 'rounded-lg flex items-center justify-center flex-shrink-0', iconBgColor)}>
65
- <Icon className={cn(iconInnerSizeStyles[size], iconColor)} />
42
+ <div className={cn(getContainerSize(size), 'rounded-lg flex items-center justify-center flex-shrink-0', iconBgColor)}>
43
+ <Icon className={cn(getIconSize(size), iconColor)} />
66
44
  </div>
67
45
  )}
68
- <span className={cn('font-medium text-foreground group-hover:text-primary transition-colors', labelSizeStyles[size])}>
46
+ <span className={cn(
47
+ 'font-medium text-foreground group-hover:text-primary transition-colors',
48
+ getTextSize(size),
49
+ responsiveLayout && 'text-center sm:text-left'
50
+ )}>
69
51
  {label}
70
52
  </span>
71
53
  </>
@@ -73,9 +55,10 @@ export const QuickActionCard = forwardRef<HTMLAnchorElement | HTMLDivElement, Qu
73
55
 
74
56
  const baseClasses = cn(
75
57
  'bg-card border border-border rounded-xl',
76
- 'flex items-center',
77
- 'hover:border-primary/50 transition-colors group',
78
- sizeStyles[size],
58
+ responsiveLayout ? 'flex flex-col sm:flex-row items-center justify-center sm:justify-start' : 'flex items-center',
59
+ 'hover:border-primary/50 hover:shadow-md transition-all duration-200 group',
60
+ getSpacing('p', size),
61
+ getGap(size === 'sm' ? 'sm' : size),
79
62
  className
80
63
  );
81
64
 
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * StatCard Component (Organism)
3
- * @description Enhanced metric card with progress bar and target tracking
3
+ * @description Enhanced metric card with progress bar and target tracking (Responsive)
4
4
  * Combines functionality of KPICard and StatsCard from main app
5
5
  */
6
6
 
7
7
  import { forwardRef, type ComponentType } from 'react';
8
- import { cn } from '../../infrastructure/utils';
8
+ import { cn, getSpacing, getIconSize, getContainerSize, getTextSize, getSpaceY, getGap } from '../../infrastructure/utils';
9
9
  import { Card, CardContent } from './Card';
10
10
  import { Progress } from '../atoms';
11
11
  import { ArrowUpRight, ArrowDownRight } from 'lucide-react';
@@ -28,39 +28,10 @@ export interface StatCardProps extends BaseProps {
28
28
  variant?: 'default' | 'gradient' | 'elevated';
29
29
  trendValueFormatter?: (value: number) => string;
30
30
  onClick?: () => void;
31
- changeLabel?: string; // For backward compatibility with StatsCard
31
+ changeLabel?: string;
32
+ responsiveLayout?: boolean;
32
33
  }
33
34
 
34
- const sizeStyles = {
35
- sm: 'p-3',
36
- md: 'p-4',
37
- lg: 'p-6',
38
- };
39
-
40
- const valueSizeStyles = {
41
- sm: 'text-lg',
42
- md: 'text-2xl',
43
- lg: 'text-3xl',
44
- };
45
-
46
- const iconSizeStyles = {
47
- sm: 'h-4 w-4',
48
- md: 'h-8 w-8',
49
- lg: 'h-12 w-12',
50
- };
51
-
52
- const labelSizeStyles = {
53
- sm: 'text-xs',
54
- md: 'text-sm',
55
- lg: 'text-base',
56
- };
57
-
58
- const iconBgSizeStyles = {
59
- sm: 'w-8 h-8',
60
- md: 'w-12 h-12',
61
- lg: 'w-16 h-16',
62
- };
63
-
64
35
  export const StatCard = forwardRef<HTMLDivElement, StatCardProps>(
65
36
  (
66
37
  {
@@ -76,14 +47,15 @@ export const StatCard = forwardRef<HTMLDivElement, StatCardProps>(
76
47
  trendValueFormatter = (v) => `${v > 0 ? '+' : ''}${Math.abs(v).toFixed(1)}%`,
77
48
  onClick,
78
49
  changeLabel,
50
+ responsiveLayout = true,
79
51
  ...props
80
52
  },
81
53
  ref
82
54
  ) => {
83
55
  const isPositive = trend?.value !== undefined && trend.value >= 0;
84
56
  const GrowthIcon = isPositive ? ArrowUpRight : ArrowDownRight;
85
- const trendColor = isPositive ? 'text-green-600' : 'text-red-600';
86
- const iconBgColor = isPositive ? 'text-green-500' : 'text-red-500';
57
+ const trendColor = isPositive ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400';
58
+ const iconBgColor = isPositive ? 'text-green-500 dark:text-green-400' : 'text-red-500 dark:text-red-400';
87
59
 
88
60
  const cardVariant = variant === 'elevated' ? 'elevated' : 'default';
89
61
 
@@ -93,32 +65,37 @@ export const StatCard = forwardRef<HTMLDivElement, StatCardProps>(
93
65
  variant={cardVariant}
94
66
  className={cn(
95
67
  'transition-all duration-300',
96
- variant === 'gradient' && 'bg-gradient-to-br from-white to-gray-50 hover:shadow-lg border-0 shadow-md',
68
+ variant === 'gradient' && 'bg-gradient-to-br from-white to-gray-50 dark:from-gray-800 dark:to-gray-900 hover:shadow-lg border-0 shadow-md',
97
69
  onClick && 'cursor-pointer hover:shadow-lg hover:border-primary/50',
98
70
  className
99
71
  )}
100
72
  onClick={onClick}
101
73
  {...props}
102
74
  >
103
- <CardContent className={cn(sizeStyles[size])}>
104
- <div className="flex items-center justify-between mb-4">
75
+ <CardContent className={getSpacing('p', size)}>
76
+ <div className={cn(
77
+ 'flex items-center justify-between mb-4',
78
+ getGap('md'),
79
+ 'flex-col sm:flex-row sm:mb-6'
80
+ )}>
105
81
  {Icon && (
106
82
  <div
107
83
  className={cn(
108
84
  'rounded-xl flex items-center justify-center',
109
85
  variant === 'gradient' && 'bg-gradient-to-br from-teal-500 to-cyan-500',
110
86
  variant !== 'gradient' && `bg-${iconColor.split('-')[1]}-500/10`,
111
- iconBgSizeStyles[size]
87
+ getContainerSize(size),
88
+ responsiveLayout && 'w-full sm:w-auto'
112
89
  )}
113
90
  >
114
- <Icon className={cn(iconSizeStyles[size], variant === 'gradient' ? 'text-white' : iconColor)} />
91
+ <Icon className={cn(getIconSize(size), variant === 'gradient' ? 'text-white' : iconColor)} />
115
92
  </div>
116
93
  )}
117
94
  {trend && (
118
95
  <span
119
96
  className={cn(
120
- 'text-sm font-medium px-2 py-1 rounded-full',
121
- isPositive ? 'text-green-700 bg-green-100' : 'text-red-700 bg-red-100'
97
+ 'text-xs sm:text-sm font-medium px-2 py-1 rounded-full whitespace-nowrap',
98
+ isPositive ? 'text-green-700 bg-green-100 dark:text-green-300 dark:bg-green-900/30' : 'text-red-700 bg-red-100 dark:text-red-300 dark:bg-red-900/30'
122
99
  )}
123
100
  >
124
101
  {changeLabel || trendValueFormatter(trend.value)}
@@ -126,29 +103,34 @@ export const StatCard = forwardRef<HTMLDivElement, StatCardProps>(
126
103
  )}
127
104
  </div>
128
105
 
129
- <div className="space-y-3">
130
- <div>
131
- <p className={cn('font-bold text-gray-900', valueSizeStyles[size])}>
106
+ <div className={getSpaceY('md')}>
107
+ <div className={cn(
108
+ responsiveLayout && 'text-center sm:text-left'
109
+ )}>
110
+ <p className={cn('font-bold text-gray-900 dark:text-gray-100', size === 'sm' ? 'text-lg sm:text-xl' : size === 'md' ? 'text-2xl sm:text-3xl' : 'text-3xl sm:text-4xl')}>
132
111
  {typeof value === 'number' ? value.toLocaleString() : value}
133
112
  </p>
134
- <p className={cn('text-muted-foreground', labelSizeStyles[size])}>{title}</p>
113
+ <p className={cn('text-muted-foreground', getTextSize(size))}>{title}</p>
135
114
  </div>
136
115
 
137
116
  {target && (
138
- <div className="space-y-1">
139
- <div className="flex justify-between text-xs">
117
+ <div className={getSpaceY('sm')}>
118
+ <div className="flex justify-between text-xs sm:text-sm">
140
119
  <span className="text-muted-foreground">Target: {target.value}</span>
141
- <span className="text-teal-600">{target.progress}%</span>
120
+ <span className="text-teal-600 dark:text-teal-400 font-medium">{target.progress}%</span>
142
121
  </div>
143
- <Progress value={target.progress} className="h-2" />
122
+ <Progress value={target.progress} className="h-2 sm:h-2.5" />
144
123
  </div>
145
124
  )}
146
125
 
147
126
  {trend && variant !== 'gradient' && (
148
- <div className="flex items-center mt-1">
149
- <GrowthIcon className={cn('h-3 w-3 mr-1', iconBgColor)} />
150
- <p className={cn('text-xs', trendColor)}>{trendValueFormatter(trend.value)}</p>
151
- {trend.label && <p className="text-xs text-muted-foreground ml-1">{trend.label}</p>}
127
+ <div className={cn(
128
+ 'flex items-center mt-1',
129
+ responsiveLayout && 'justify-center sm:justify-start'
130
+ )}>
131
+ <GrowthIcon className={cn('h-3 w-3 sm:h-4 sm:w-4 mr-1', iconBgColor)} />
132
+ <p className={cn('text-xs sm:text-sm', trendColor)}>{trendValueFormatter(trend.value)}</p>
133
+ {trend.label && <p className="text-xs text-muted-foreground ml-1 hidden xs:inline">{trend.label}</p>}
152
134
  </div>
153
135
  )}
154
136
  </div>
@@ -92,6 +92,9 @@ export type { StatCardProps } from './StatCard';
92
92
  export { FormModal } from './FormModal';
93
93
  export type { FormModalProps } from './FormModal';
94
94
 
95
+ export { Grid, GridItem } from './Grid';
96
+ export type { GridProps, GridItemProps } from './Grid';
97
+
95
98
  export { ConfirmDialog } from './ConfirmDialog';
96
99
  export type { ConfirmDialogProps } from './ConfirmDialog';
97
100