@umituz/web-design-system 6.11.0 → 6.13.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/dist/styles.css +1 -1
- package/package.json +3 -1
- package/src/__tests__/layout-primitives.test.tsx +85 -0
- package/src/__tests__/theme-provider.test.tsx +167 -0
- package/src/domain/types/index.ts +9 -0
- package/src/domain/types/theme.types.ts +56 -0
- package/src/index.ts +10 -7
- package/src/infrastructure/themes/apply-theme.ts +149 -0
- package/src/infrastructure/themes/create-theme.ts +49 -0
- package/src/infrastructure/themes/default.theme.ts +73 -0
- package/src/infrastructure/themes/editorial.theme.ts +65 -0
- package/src/infrastructure/themes/index.ts +8 -0
- package/src/infrastructure/themes/midnight.theme.ts +79 -0
- package/src/infrastructure/themes/ocean.theme.ts +71 -0
- package/src/infrastructure/themes/registry.ts +28 -0
- package/src/presentation/atoms/Grid.tsx +129 -0
- package/src/presentation/atoms/Row.tsx +81 -0
- package/src/presentation/atoms/Stack.tsx +78 -0
- package/src/presentation/atoms/index.ts +10 -1
- package/src/presentation/providers/DesignSystemProvider.tsx +111 -0
- package/src/presentation/providers/index.ts +5 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ThemeConfig } from '../../domain/types';
|
|
2
|
+
|
|
3
|
+
export const midnightTheme: ThemeConfig = {
|
|
4
|
+
name: 'midnight',
|
|
5
|
+
mode: 'dark',
|
|
6
|
+
colors: {
|
|
7
|
+
background: '#060a12',
|
|
8
|
+
foreground: '#e8edf8',
|
|
9
|
+
card: '#0d1422',
|
|
10
|
+
cardForeground: '#e8edf8',
|
|
11
|
+
popover: '#121b2c',
|
|
12
|
+
popoverForeground: '#e8edf8',
|
|
13
|
+
primary: '#4e8cff',
|
|
14
|
+
primaryForeground: '#ffffff',
|
|
15
|
+
primarySoft: 'rgba(78, 140, 255, 0.13)',
|
|
16
|
+
secondary: '#121b2c',
|
|
17
|
+
secondaryForeground: '#e8edf8',
|
|
18
|
+
muted: '#121b2c',
|
|
19
|
+
mutedForeground: '#9aa7c0',
|
|
20
|
+
accent: '#182338',
|
|
21
|
+
accentForeground: '#e8edf8',
|
|
22
|
+
border: '#27334a',
|
|
23
|
+
input: '#27334a',
|
|
24
|
+
destructive: '#f4585f',
|
|
25
|
+
destructiveForeground: '#ffffff',
|
|
26
|
+
ring: 'rgba(78, 140, 255, 0.45)',
|
|
27
|
+
success: '#3ddc97',
|
|
28
|
+
successForeground: '#ffffff',
|
|
29
|
+
warning: '#f5b04a',
|
|
30
|
+
warningForeground: '#060a12',
|
|
31
|
+
info: '#4e8cff',
|
|
32
|
+
infoForeground: '#ffffff',
|
|
33
|
+
},
|
|
34
|
+
typography: {
|
|
35
|
+
fontSans: "'IBM Plex Sans', -apple-system, 'Segoe UI', sans-serif",
|
|
36
|
+
fontMono: "'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace",
|
|
37
|
+
},
|
|
38
|
+
radii: {
|
|
39
|
+
xs: '3px',
|
|
40
|
+
sm: '5px',
|
|
41
|
+
md: '8px',
|
|
42
|
+
lg: '12px',
|
|
43
|
+
xl: '16px',
|
|
44
|
+
},
|
|
45
|
+
customVars: {
|
|
46
|
+
'bg-0': '#060a12',
|
|
47
|
+
'bg-1': '#090f1a',
|
|
48
|
+
'bg-2': '#0d1422',
|
|
49
|
+
'bg-3': '#121b2c',
|
|
50
|
+
'bg-4': '#182338',
|
|
51
|
+
'bg-inset': '#070c15',
|
|
52
|
+
'line-1': '#1b2536',
|
|
53
|
+
'line-2': '#27334a',
|
|
54
|
+
'line-3': '#35435f',
|
|
55
|
+
'ink-1': '#e8edf8',
|
|
56
|
+
'ink-2': '#9aa7c0',
|
|
57
|
+
'ink-3': '#5e6c88',
|
|
58
|
+
'ink-4': '#3d4a63',
|
|
59
|
+
'blue': '#4e8cff',
|
|
60
|
+
'blue-soft': 'rgba(78, 140, 255, 0.13)',
|
|
61
|
+
'blue-line': 'rgba(78, 140, 255, 0.35)',
|
|
62
|
+
'green': '#3ddc97',
|
|
63
|
+
'green-soft': 'rgba(61, 220, 151, 0.12)',
|
|
64
|
+
'green-line': 'rgba(61, 220, 151, 0.35)',
|
|
65
|
+
'amber': '#f5b04a',
|
|
66
|
+
'amber-soft': 'rgba(245, 176, 74, 0.13)',
|
|
67
|
+
'amber-line': 'rgba(245, 176, 74, 0.35)',
|
|
68
|
+
'red': '#f4585f',
|
|
69
|
+
'red-soft': 'rgba(244, 88, 95, 0.12)',
|
|
70
|
+
'red-line': 'rgba(244, 88, 95, 0.4)',
|
|
71
|
+
'idle': '#7683a0',
|
|
72
|
+
'idle-soft': 'rgba(118, 131, 160, 0.14)',
|
|
73
|
+
'shadow-1': '0 1px 2px rgba(2, 6, 14, 0.5)',
|
|
74
|
+
'shadow-2': '0 8px 28px rgba(2, 6, 14, 0.55)',
|
|
75
|
+
'shadow-3': '0 18px 60px rgba(1, 4, 10, 0.7)',
|
|
76
|
+
'sidebar-w': '250px',
|
|
77
|
+
'topbar-h': '56px',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ThemeConfig } from '../../domain/types';
|
|
2
|
+
|
|
3
|
+
export const oceanTheme: ThemeConfig = {
|
|
4
|
+
name: 'ocean',
|
|
5
|
+
mode: 'light',
|
|
6
|
+
colors: {
|
|
7
|
+
background: '#f8fafc',
|
|
8
|
+
foreground: '#0f172a',
|
|
9
|
+
card: '#ffffff',
|
|
10
|
+
cardForeground: '#0f172a',
|
|
11
|
+
popover: '#ffffff',
|
|
12
|
+
popoverForeground: '#0f172a',
|
|
13
|
+
primary: '#0284c7',
|
|
14
|
+
primaryForeground: '#ffffff',
|
|
15
|
+
primarySoft: 'rgba(2, 132, 199, 0.1)',
|
|
16
|
+
secondary: '#0ea5e9',
|
|
17
|
+
secondaryForeground: '#ffffff',
|
|
18
|
+
muted: '#f0f9ff',
|
|
19
|
+
mutedForeground: '#64748b',
|
|
20
|
+
accent: '#e0f2fe',
|
|
21
|
+
accentForeground: '#0369a1',
|
|
22
|
+
border: '#e2e8f0',
|
|
23
|
+
input: '#e2e8f0',
|
|
24
|
+
destructive: '#ef4444',
|
|
25
|
+
destructiveForeground: '#ffffff',
|
|
26
|
+
ring: 'rgba(2, 132, 199, 0.45)',
|
|
27
|
+
success: '#10b981',
|
|
28
|
+
successForeground: '#ffffff',
|
|
29
|
+
warning: '#f59e0b',
|
|
30
|
+
warningForeground: '#ffffff',
|
|
31
|
+
info: '#0284c7',
|
|
32
|
+
infoForeground: '#ffffff',
|
|
33
|
+
},
|
|
34
|
+
darkColors: {
|
|
35
|
+
background: '#08111e',
|
|
36
|
+
foreground: '#f0f9ff',
|
|
37
|
+
card: '#0c1a2e',
|
|
38
|
+
cardForeground: '#f0f9ff',
|
|
39
|
+
popover: '#0c1a2e',
|
|
40
|
+
popoverForeground: '#f0f9ff',
|
|
41
|
+
primary: '#38bdf8',
|
|
42
|
+
primaryForeground: '#08111e',
|
|
43
|
+
secondary: '#0284c7',
|
|
44
|
+
secondaryForeground: '#ffffff',
|
|
45
|
+
muted: '#13233b',
|
|
46
|
+
mutedForeground: '#94a3b8',
|
|
47
|
+
accent: '#183052',
|
|
48
|
+
accentForeground: '#7dd3fc',
|
|
49
|
+
border: '#1b3252',
|
|
50
|
+
input: '#1b3252',
|
|
51
|
+
destructive: '#f87171',
|
|
52
|
+
destructiveForeground: '#ffffff',
|
|
53
|
+
ring: 'rgba(56, 189, 248, 0.5)',
|
|
54
|
+
success: '#34d399',
|
|
55
|
+
successForeground: '#ffffff',
|
|
56
|
+
warning: '#fbbf24',
|
|
57
|
+
warningForeground: '#ffffff',
|
|
58
|
+
info: '#38bdf8',
|
|
59
|
+
infoForeground: '#ffffff',
|
|
60
|
+
},
|
|
61
|
+
typography: {
|
|
62
|
+
fontSans: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
63
|
+
fontMono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
64
|
+
},
|
|
65
|
+
radii: {
|
|
66
|
+
sm: '6px',
|
|
67
|
+
md: '8px',
|
|
68
|
+
lg: '12px',
|
|
69
|
+
xl: '16px',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ThemeConfig } from '../../domain/types';
|
|
2
|
+
import { defaultTheme } from './default.theme';
|
|
3
|
+
import { oceanTheme } from './ocean.theme';
|
|
4
|
+
import { midnightTheme } from './midnight.theme';
|
|
5
|
+
import { editorialTheme } from './editorial.theme';
|
|
6
|
+
|
|
7
|
+
const themeRegistry = new Map<string, ThemeConfig>([
|
|
8
|
+
['default', defaultTheme],
|
|
9
|
+
['ocean', oceanTheme],
|
|
10
|
+
['midnight', midnightTheme],
|
|
11
|
+
['editorial', editorialTheme],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function registerTheme(theme: ThemeConfig): void {
|
|
15
|
+
themeRegistry.set(theme.name, theme);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getTheme(name: string): ThemeConfig {
|
|
19
|
+
return themeRegistry.get(name) ?? defaultTheme;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hasTheme(name: string): boolean {
|
|
23
|
+
return themeRegistry.has(name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function listThemes(): string[] {
|
|
27
|
+
return Array.from(themeRegistry.keys());
|
|
28
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes, type ElementType } from 'react';
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cn } from '../../infrastructure/utils';
|
|
4
|
+
import type { BaseProps } from '../../domain/types';
|
|
5
|
+
|
|
6
|
+
export type GridColsNumber = 1 | 2 | 3 | 4 | 5 | 6 | 12;
|
|
7
|
+
|
|
8
|
+
export interface ResponsiveGridCols {
|
|
9
|
+
base?: GridColsNumber;
|
|
10
|
+
sm?: GridColsNumber;
|
|
11
|
+
md?: GridColsNumber;
|
|
12
|
+
lg?: GridColsNumber;
|
|
13
|
+
xl?: GridColsNumber;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type GridCols = GridColsNumber | ResponsiveGridCols;
|
|
17
|
+
export type GridGap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
18
|
+
|
|
19
|
+
export interface GridProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
20
|
+
cols?: GridCols;
|
|
21
|
+
gap?: GridGap;
|
|
22
|
+
asChild?: boolean;
|
|
23
|
+
as?: ElementType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const gapStyles: Record<GridGap, string> = {
|
|
27
|
+
none: 'gap-0',
|
|
28
|
+
xs: 'gap-1',
|
|
29
|
+
sm: 'gap-2',
|
|
30
|
+
md: 'gap-4',
|
|
31
|
+
lg: 'gap-6',
|
|
32
|
+
xl: 'gap-8',
|
|
33
|
+
'2xl': 'gap-12',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const baseColStyles: Record<GridColsNumber, string> = {
|
|
37
|
+
1: 'grid-cols-1',
|
|
38
|
+
2: 'grid-cols-2',
|
|
39
|
+
3: 'grid-cols-3',
|
|
40
|
+
4: 'grid-cols-4',
|
|
41
|
+
5: 'grid-cols-5',
|
|
42
|
+
6: 'grid-cols-6',
|
|
43
|
+
12: 'grid-cols-12',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const smColStyles: Record<GridColsNumber, string> = {
|
|
47
|
+
1: 'sm:grid-cols-1',
|
|
48
|
+
2: 'sm:grid-cols-2',
|
|
49
|
+
3: 'sm:grid-cols-3',
|
|
50
|
+
4: 'sm:grid-cols-4',
|
|
51
|
+
5: 'sm:grid-cols-5',
|
|
52
|
+
6: 'sm:grid-cols-6',
|
|
53
|
+
12: 'sm:grid-cols-12',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const mdColStyles: Record<GridColsNumber, string> = {
|
|
57
|
+
1: 'md:grid-cols-1',
|
|
58
|
+
2: 'md:grid-cols-2',
|
|
59
|
+
3: 'md:grid-cols-3',
|
|
60
|
+
4: 'md:grid-cols-4',
|
|
61
|
+
5: 'md:grid-cols-5',
|
|
62
|
+
6: 'md:grid-cols-6',
|
|
63
|
+
12: 'md:grid-cols-12',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const lgColStyles: Record<GridColsNumber, string> = {
|
|
67
|
+
1: 'lg:grid-cols-1',
|
|
68
|
+
2: 'lg:grid-cols-2',
|
|
69
|
+
3: 'lg:grid-cols-3',
|
|
70
|
+
4: 'lg:grid-cols-4',
|
|
71
|
+
5: 'lg:grid-cols-5',
|
|
72
|
+
6: 'lg:grid-cols-6',
|
|
73
|
+
12: 'lg:grid-cols-12',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const xlColStyles: Record<GridColsNumber, string> = {
|
|
77
|
+
1: 'xl:grid-cols-1',
|
|
78
|
+
2: 'xl:grid-cols-2',
|
|
79
|
+
3: 'xl:grid-cols-3',
|
|
80
|
+
4: 'xl:grid-cols-4',
|
|
81
|
+
5: 'xl:grid-cols-5',
|
|
82
|
+
6: 'xl:grid-cols-6',
|
|
83
|
+
12: 'xl:grid-cols-12',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function resolveColClasses(cols: GridCols): string {
|
|
87
|
+
if (typeof cols === 'number') {
|
|
88
|
+
return baseColStyles[cols] ?? 'grid-cols-1';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const classes: string[] = [];
|
|
92
|
+
if (cols.base) classes.push(baseColStyles[cols.base]);
|
|
93
|
+
if (cols.sm) classes.push(smColStyles[cols.sm]);
|
|
94
|
+
if (cols.md) classes.push(mdColStyles[cols.md]);
|
|
95
|
+
if (cols.lg) classes.push(lgColStyles[cols.lg]);
|
|
96
|
+
if (cols.xl) classes.push(xlColStyles[cols.xl]);
|
|
97
|
+
|
|
98
|
+
return classes.length > 0 ? classes.join(' ') : 'grid-cols-1';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const Grid = forwardRef<HTMLDivElement, GridProps>(
|
|
102
|
+
(
|
|
103
|
+
{
|
|
104
|
+
className,
|
|
105
|
+
cols = 1,
|
|
106
|
+
gap = 'md',
|
|
107
|
+
asChild = false,
|
|
108
|
+
as: Component = 'div',
|
|
109
|
+
...props
|
|
110
|
+
},
|
|
111
|
+
ref
|
|
112
|
+
) => {
|
|
113
|
+
const Comp = asChild ? Slot : Component;
|
|
114
|
+
return (
|
|
115
|
+
<Comp
|
|
116
|
+
ref={ref}
|
|
117
|
+
className={cn(
|
|
118
|
+
'grid w-full min-w-0',
|
|
119
|
+
resolveColClasses(cols),
|
|
120
|
+
gapStyles[gap],
|
|
121
|
+
className
|
|
122
|
+
)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
Grid.displayName = 'Grid';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes, type ElementType } from 'react';
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cn } from '../../infrastructure/utils';
|
|
4
|
+
import type { BaseProps } from '../../domain/types';
|
|
5
|
+
|
|
6
|
+
export type RowGap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
7
|
+
export type RowAlign = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
|
8
|
+
export type RowJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
|
9
|
+
|
|
10
|
+
export interface RowProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
11
|
+
gap?: RowGap;
|
|
12
|
+
align?: RowAlign;
|
|
13
|
+
justify?: RowJustify;
|
|
14
|
+
wrap?: boolean;
|
|
15
|
+
inline?: boolean;
|
|
16
|
+
asChild?: boolean;
|
|
17
|
+
as?: ElementType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const gapStyles: Record<RowGap, string> = {
|
|
21
|
+
none: 'gap-0',
|
|
22
|
+
xs: 'gap-1',
|
|
23
|
+
sm: 'gap-2',
|
|
24
|
+
md: 'gap-4',
|
|
25
|
+
lg: 'gap-6',
|
|
26
|
+
xl: 'gap-8',
|
|
27
|
+
'2xl': 'gap-12',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const alignStyles: Record<RowAlign, string> = {
|
|
31
|
+
start: 'items-start',
|
|
32
|
+
center: 'items-center',
|
|
33
|
+
end: 'items-end',
|
|
34
|
+
stretch: 'items-stretch',
|
|
35
|
+
baseline: 'items-baseline',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const justifyStyles: Record<RowJustify, string> = {
|
|
39
|
+
start: 'justify-start',
|
|
40
|
+
center: 'justify-center',
|
|
41
|
+
end: 'justify-end',
|
|
42
|
+
between: 'justify-between',
|
|
43
|
+
around: 'justify-around',
|
|
44
|
+
evenly: 'justify-evenly',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Row = forwardRef<HTMLDivElement, RowProps>(
|
|
48
|
+
(
|
|
49
|
+
{
|
|
50
|
+
className,
|
|
51
|
+
gap = 'sm',
|
|
52
|
+
align = 'center',
|
|
53
|
+
justify = 'start',
|
|
54
|
+
wrap = false,
|
|
55
|
+
inline = false,
|
|
56
|
+
asChild = false,
|
|
57
|
+
as: Component = 'div',
|
|
58
|
+
...props
|
|
59
|
+
},
|
|
60
|
+
ref
|
|
61
|
+
) => {
|
|
62
|
+
const Comp = asChild ? Slot : Component;
|
|
63
|
+
return (
|
|
64
|
+
<Comp
|
|
65
|
+
ref={ref}
|
|
66
|
+
className={cn(
|
|
67
|
+
inline ? 'inline-flex' : 'flex',
|
|
68
|
+
'flex-row min-w-0',
|
|
69
|
+
gapStyles[gap],
|
|
70
|
+
alignStyles[align],
|
|
71
|
+
justifyStyles[justify],
|
|
72
|
+
wrap ? 'flex-wrap' : 'flex-nowrap',
|
|
73
|
+
className
|
|
74
|
+
)}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
Row.displayName = 'Row';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes, type ElementType } from 'react';
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cn } from '../../infrastructure/utils';
|
|
4
|
+
import type { BaseProps } from '../../domain/types';
|
|
5
|
+
|
|
6
|
+
export type StackGap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
7
|
+
export type StackAlign = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
|
|
8
|
+
export type StackJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
|
|
9
|
+
|
|
10
|
+
export interface StackProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
11
|
+
gap?: StackGap;
|
|
12
|
+
align?: StackAlign;
|
|
13
|
+
justify?: StackJustify;
|
|
14
|
+
wrap?: boolean;
|
|
15
|
+
asChild?: boolean;
|
|
16
|
+
as?: ElementType;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const gapStyles: Record<StackGap, string> = {
|
|
20
|
+
none: 'gap-0',
|
|
21
|
+
xs: 'gap-1',
|
|
22
|
+
sm: 'gap-2',
|
|
23
|
+
md: 'gap-4',
|
|
24
|
+
lg: 'gap-6',
|
|
25
|
+
xl: 'gap-8',
|
|
26
|
+
'2xl': 'gap-12',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const alignStyles: Record<StackAlign, string> = {
|
|
30
|
+
start: 'items-start',
|
|
31
|
+
center: 'items-center',
|
|
32
|
+
end: 'items-end',
|
|
33
|
+
stretch: 'items-stretch',
|
|
34
|
+
baseline: 'items-baseline',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const justifyStyles: Record<StackJustify, string> = {
|
|
38
|
+
start: 'justify-start',
|
|
39
|
+
center: 'justify-center',
|
|
40
|
+
end: 'justify-end',
|
|
41
|
+
between: 'justify-between',
|
|
42
|
+
around: 'justify-around',
|
|
43
|
+
evenly: 'justify-evenly',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
47
|
+
(
|
|
48
|
+
{
|
|
49
|
+
className,
|
|
50
|
+
gap = 'md',
|
|
51
|
+
align = 'stretch',
|
|
52
|
+
justify = 'start',
|
|
53
|
+
wrap = false,
|
|
54
|
+
asChild = false,
|
|
55
|
+
as: Component = 'div',
|
|
56
|
+
...props
|
|
57
|
+
},
|
|
58
|
+
ref
|
|
59
|
+
) => {
|
|
60
|
+
const Comp = asChild ? Slot : Component;
|
|
61
|
+
return (
|
|
62
|
+
<Comp
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cn(
|
|
65
|
+
'flex flex-col min-w-0',
|
|
66
|
+
gapStyles[gap],
|
|
67
|
+
alignStyles[align],
|
|
68
|
+
justifyStyles[justify],
|
|
69
|
+
wrap && 'flex-wrap',
|
|
70
|
+
className
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
Stack.displayName = 'Stack';
|
|
@@ -85,9 +85,18 @@ export type { KbdProps } from './Kbd';
|
|
|
85
85
|
export { Money } from './Money';
|
|
86
86
|
export type { MoneyProps } from './Money';
|
|
87
87
|
|
|
88
|
-
// New Productivity Atoms
|
|
89
88
|
export { CopyButton } from './CopyButton';
|
|
90
89
|
export type { CopyButtonProps } from './CopyButton';
|
|
91
90
|
|
|
92
91
|
export { StatusDot } from './StatusDot';
|
|
93
92
|
export type { StatusDotProps, StatusType } from './StatusDot';
|
|
93
|
+
|
|
94
|
+
export { Stack } from './Stack';
|
|
95
|
+
export type { StackProps, StackGap, StackAlign, StackJustify } from './Stack';
|
|
96
|
+
|
|
97
|
+
export { Row } from './Row';
|
|
98
|
+
export type { RowProps, RowGap, RowAlign, RowJustify } from './Row';
|
|
99
|
+
|
|
100
|
+
export { Grid } from './Grid';
|
|
101
|
+
export type { GridProps, GridCols, GridColsNumber, ResponsiveGridCols, GridGap } from './Grid';
|
|
102
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import type { ThemeConfig, ThemeMode } from '../../domain/types';
|
|
10
|
+
import { applyTheme, getTheme } from '../../infrastructure/themes';
|
|
11
|
+
|
|
12
|
+
export interface DesignSystemContextValue {
|
|
13
|
+
theme: ThemeConfig;
|
|
14
|
+
mode: 'light' | 'dark';
|
|
15
|
+
setTheme: (theme: string | ThemeConfig) => void;
|
|
16
|
+
setMode: (mode: 'light' | 'dark') => void;
|
|
17
|
+
toggleMode: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DesignSystemContext = createContext<DesignSystemContextValue | null>(null);
|
|
21
|
+
|
|
22
|
+
export interface DesignSystemProviderProps {
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
theme?: string | ThemeConfig;
|
|
25
|
+
mode?: ThemeMode;
|
|
26
|
+
storageKey?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DesignSystemProvider({
|
|
30
|
+
children,
|
|
31
|
+
theme: initialTheme = 'default',
|
|
32
|
+
mode: initialMode = 'system',
|
|
33
|
+
storageKey = 'umituz-theme-mode',
|
|
34
|
+
}: DesignSystemProviderProps) {
|
|
35
|
+
const [overrideTheme, setOverrideTheme] = useState<ThemeConfig | null>(null);
|
|
36
|
+
|
|
37
|
+
const currentTheme = useMemo(() => {
|
|
38
|
+
if (overrideTheme) return overrideTheme;
|
|
39
|
+
return typeof initialTheme === 'string' ? getTheme(initialTheme) : initialTheme;
|
|
40
|
+
}, [initialTheme, overrideTheme]);
|
|
41
|
+
|
|
42
|
+
const [activeMode, setActiveMode] = useState<'light' | 'dark'>(() => {
|
|
43
|
+
if (typeof window === 'undefined') {
|
|
44
|
+
return currentTheme.mode ?? 'light';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (storageKey) {
|
|
48
|
+
const stored = localStorage.getItem(storageKey);
|
|
49
|
+
if (stored === 'light' || stored === 'dark') return stored;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (initialMode === 'dark' || initialMode === 'light') {
|
|
53
|
+
return initialMode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (currentTheme.mode) {
|
|
57
|
+
return currentTheme.mode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
61
|
+
? 'dark'
|
|
62
|
+
: 'light';
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
applyTheme(currentTheme);
|
|
67
|
+
|
|
68
|
+
const doc = document.documentElement;
|
|
69
|
+
if (activeMode === 'dark') {
|
|
70
|
+
doc.classList.add('dark');
|
|
71
|
+
doc.setAttribute('data-theme', 'dark');
|
|
72
|
+
} else {
|
|
73
|
+
doc.classList.remove('dark');
|
|
74
|
+
doc.setAttribute('data-theme', 'light');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (storageKey) {
|
|
78
|
+
localStorage.setItem(storageKey, activeMode);
|
|
79
|
+
}
|
|
80
|
+
}, [currentTheme, activeMode, storageKey]);
|
|
81
|
+
|
|
82
|
+
const value = useMemo<DesignSystemContextValue>(() => {
|
|
83
|
+
return {
|
|
84
|
+
theme: currentTheme,
|
|
85
|
+
mode: activeMode,
|
|
86
|
+
setTheme: (t: string | ThemeConfig) => {
|
|
87
|
+
setOverrideTheme(typeof t === 'string' ? getTheme(t) : t);
|
|
88
|
+
},
|
|
89
|
+
setMode: (m: 'light' | 'dark') => {
|
|
90
|
+
setActiveMode(m);
|
|
91
|
+
},
|
|
92
|
+
toggleMode: () => {
|
|
93
|
+
setActiveMode((prev) => (prev === 'dark' ? 'light' : 'dark'));
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}, [currentTheme, activeMode]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<DesignSystemContext.Provider value={value}>
|
|
100
|
+
{children}
|
|
101
|
+
</DesignSystemContext.Provider>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function useDesignSystem(): DesignSystemContextValue {
|
|
106
|
+
const ctx = useContext(DesignSystemContext);
|
|
107
|
+
if (!ctx) {
|
|
108
|
+
throw new Error('useDesignSystem must be used within a DesignSystemProvider');
|
|
109
|
+
}
|
|
110
|
+
return ctx;
|
|
111
|
+
}
|