@umituz/web-design-system 6.10.0 → 6.12.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.
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { render, screen, act } from '@testing-library/react';
3
+ import {
4
+ DesignSystemProvider,
5
+ useDesignSystem,
6
+ } from '../presentation/providers/DesignSystemProvider';
7
+ import {
8
+ getTheme,
9
+ registerTheme,
10
+ applyTheme,
11
+ themeToCssVariables,
12
+ accessiaTheme,
13
+ } from '../infrastructure/themes';
14
+ import type { ThemeConfig } from '../domain/types';
15
+
16
+ describe('Theme System and Provider', () => {
17
+ beforeEach(() => {
18
+ localStorage.clear();
19
+ document.documentElement.className = '';
20
+ document.documentElement.removeAttribute('data-theme');
21
+ const existing = document.getElementById('umituz-theme-vars');
22
+ if (existing) existing.remove();
23
+ });
24
+
25
+ it('retrieves default and built-in themes by name', () => {
26
+ expect(getTheme('accessia').name).toBe('accessia');
27
+ expect(getTheme('hangora').name).toBe('hangora');
28
+ expect(getTheme('default').name).toBe('default');
29
+ expect(getTheme('non-existent').name).toBe('default');
30
+ });
31
+
32
+ it('registers and retrieves custom themes', () => {
33
+ const custom: ThemeConfig = {
34
+ name: 'custom-corp',
35
+ colors: {
36
+ background: '#111111',
37
+ foreground: '#eeeeee',
38
+ card: '#222222',
39
+ cardForeground: '#eeeeee',
40
+ popover: '#222222',
41
+ popoverForeground: '#eeeeee',
42
+ primary: '#ff0055',
43
+ primaryForeground: '#ffffff',
44
+ secondary: '#333333',
45
+ secondaryForeground: '#ffffff',
46
+ muted: '#333333',
47
+ mutedForeground: '#888888',
48
+ accent: '#444444',
49
+ accentForeground: '#ffffff',
50
+ border: '#555555',
51
+ input: '#555555',
52
+ destructive: '#ff0000',
53
+ destructiveForeground: '#ffffff',
54
+ },
55
+ };
56
+
57
+ registerTheme(custom);
58
+ expect(getTheme('custom-corp').colors.primary).toBe('#ff0055');
59
+ });
60
+
61
+ it('converts theme config to CSS variables string', () => {
62
+ const css = themeToCssVariables(accessiaTheme);
63
+ expect(css).toContain('--color-primary: #4e8cff;');
64
+ expect(css).toContain('--app-primary: #4e8cff;');
65
+ expect(css).toContain('--color-background: #060a12;');
66
+ expect(css).toContain('--bg-0: #060a12;');
67
+ expect(css).toContain('--font-sans:');
68
+ });
69
+
70
+ it('applies theme variables to document head', () => {
71
+ applyTheme(accessiaTheme);
72
+ const styleEl = document.getElementById('umituz-theme-vars');
73
+ expect(styleEl).toBeTruthy();
74
+ expect(styleEl?.textContent).toContain('--color-primary: #4e8cff;');
75
+ });
76
+
77
+ it('DesignSystemProvider provides theme and allows mode toggle', () => {
78
+ function Consumer() {
79
+ const { theme, mode, toggleMode, setMode } = useDesignSystem();
80
+ return (
81
+ <div>
82
+ <span data-testid="theme-name">{theme.name}</span>
83
+ <span data-testid="theme-mode">{mode}</span>
84
+ <button data-testid="toggle-btn" onClick={toggleMode}>
85
+ Toggle
86
+ </button>
87
+ <button data-testid="set-dark" onClick={() => setMode('dark')}>
88
+ Dark
89
+ </button>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ render(
95
+ <DesignSystemProvider theme="accessia" mode="dark">
96
+ <Consumer />
97
+ </DesignSystemProvider>
98
+ );
99
+
100
+ expect(screen.getByTestId('theme-name').textContent).toBe('accessia');
101
+ expect(screen.getByTestId('theme-mode').textContent).toBe('dark');
102
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
103
+
104
+ act(() => {
105
+ screen.getByTestId('toggle-btn').click();
106
+ });
107
+
108
+ expect(screen.getByTestId('theme-mode').textContent).toBe('light');
109
+ expect(document.documentElement.classList.contains('dark')).toBe(false);
110
+
111
+ act(() => {
112
+ screen.getByTestId('set-dark').click();
113
+ });
114
+
115
+ expect(screen.getByTestId('theme-mode').textContent).toBe('dark');
116
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
117
+ });
118
+ });
@@ -18,3 +18,12 @@ export type {
18
18
  BreakpointState,
19
19
  UseBreakpointReturn,
20
20
  } from './breakpoint.types';
21
+
22
+ export type {
23
+ ThemeMode,
24
+ ThemeColors,
25
+ ThemeTypography,
26
+ ThemeRadii,
27
+ ThemeConfig,
28
+ } from './theme.types';
29
+
@@ -0,0 +1,56 @@
1
+ export type ThemeMode = 'light' | 'dark' | 'system';
2
+
3
+ export interface ThemeColors {
4
+ background: string;
5
+ foreground: string;
6
+ card: string;
7
+ cardForeground: string;
8
+ popover: string;
9
+ popoverForeground: string;
10
+ primary: string;
11
+ primaryForeground: string;
12
+ secondary: string;
13
+ secondaryForeground: string;
14
+ muted: string;
15
+ mutedForeground: string;
16
+ accent: string;
17
+ accentForeground: string;
18
+ border: string;
19
+ input: string;
20
+ destructive: string;
21
+ destructiveForeground: string;
22
+ ring?: string;
23
+ success?: string;
24
+ successForeground?: string;
25
+ warning?: string;
26
+ warningForeground?: string;
27
+ info?: string;
28
+ infoForeground?: string;
29
+ violet?: string;
30
+ violetForeground?: string;
31
+ primarySoft?: string;
32
+ }
33
+
34
+ export interface ThemeTypography {
35
+ fontSans?: string;
36
+ fontMono?: string;
37
+ fontDisplay?: string;
38
+ }
39
+
40
+ export interface ThemeRadii {
41
+ xs?: string;
42
+ sm?: string;
43
+ md?: string;
44
+ lg?: string;
45
+ xl?: string;
46
+ }
47
+
48
+ export interface ThemeConfig {
49
+ name: string;
50
+ mode?: 'light' | 'dark';
51
+ colors: ThemeColors;
52
+ darkColors?: Partial<ThemeColors>;
53
+ typography?: ThemeTypography;
54
+ radii?: ThemeRadii;
55
+ customVars?: Record<string, string>;
56
+ }
package/src/index.ts CHANGED
@@ -1,19 +1,23 @@
1
- // Core Tokens, Types, Constants, Hooks & Utils
2
1
  export * from './domain/tokens';
3
2
  export * from './domain/types';
4
3
  export * from './infrastructure/utils';
5
4
  export * from './infrastructure/constants';
5
+ export * from './infrastructure/themes';
6
6
  export * from './presentation/hooks';
7
-
8
- // Templates
9
7
  export * from './presentation/templates';
10
-
11
- // Organisms
12
8
  export * from './presentation/organisms';
9
+ export * from './presentation/providers';
10
+
13
11
 
14
- // Atoms
15
12
  export type { ButtonProps } from './presentation/atoms/Button';
16
13
  export { Button, buttonVariants } from './presentation/atoms/Button';
14
+ export type { StackProps, StackGap, StackAlign, StackJustify } from './presentation/atoms/Stack';
15
+ export { Stack } from './presentation/atoms/Stack';
16
+ export type { RowProps, RowGap, RowAlign, RowJustify } from './presentation/atoms/Row';
17
+ export { Row } from './presentation/atoms/Row';
18
+ export type { GridProps, GridCols, GridColsNumber, ResponsiveGridCols, GridGap } from './presentation/atoms/Grid';
19
+ export { Grid } from './presentation/atoms/Grid';
20
+
17
21
  export type { BadgeProps, BadgeVariant } from './presentation/atoms/Badge';
18
22
  export { Badge } from './presentation/atoms/Badge';
19
23
  export type { InputProps } from './presentation/atoms/Input';
@@ -74,7 +78,6 @@ export type { CopyButtonProps } from './presentation/atoms/CopyButton';
74
78
  export { StatusDot } from './presentation/atoms/StatusDot';
75
79
  export type { StatusDotProps, StatusType } from './presentation/atoms/StatusDot';
76
80
 
77
- // Molecules
78
81
  export type { FormFieldProps } from './presentation/molecules/FormField';
79
82
  export { FormField } from './presentation/molecules/FormField';
80
83
  export type { SearchBoxProps } from './presentation/molecules/SearchBox';
@@ -0,0 +1,79 @@
1
+ import type { ThemeConfig } from '../../domain/types';
2
+
3
+ export const accessiaTheme: ThemeConfig = {
4
+ name: 'accessia',
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,149 @@
1
+ import type { ThemeConfig, ThemeColors } from '../../domain/types';
2
+ import { getTheme } from './registry';
3
+
4
+ function colorPropsToCss(colors: ThemeColors): string {
5
+ const lines: string[] = [];
6
+ const entries: [keyof ThemeColors, string][] = [
7
+ ['background', '--color-background'],
8
+ ['foreground', '--color-foreground'],
9
+ ['card', '--color-card'],
10
+ ['cardForeground', '--color-card-foreground'],
11
+ ['popover', '--color-popover'],
12
+ ['popoverForeground', '--color-popover-foreground'],
13
+ ['primary', '--color-primary'],
14
+ ['primaryForeground', '--color-primary-foreground'],
15
+ ['secondary', '--color-secondary'],
16
+ ['secondaryForeground', '--color-secondary-foreground'],
17
+ ['muted', '--color-muted'],
18
+ ['mutedForeground', '--color-muted-foreground'],
19
+ ['accent', '--color-accent'],
20
+ ['accentForeground', '--color-accent-foreground'],
21
+ ['border', '--color-border'],
22
+ ['input', '--color-input'],
23
+ ['destructive', '--color-destructive'],
24
+ ['destructiveForeground', '--color-destructive-foreground'],
25
+ ];
26
+
27
+ for (const [key, cssVar] of entries) {
28
+ const val = colors[key];
29
+ if (val) {
30
+ lines.push(`${cssVar}: ${val};`);
31
+ const appVar = cssVar.replace('--color-', '--app-');
32
+ lines.push(`${appVar}: ${val};`);
33
+ }
34
+ }
35
+
36
+ if (colors.ring) {
37
+ lines.push(`--color-ring: ${colors.ring};`);
38
+ lines.push(`--app-ring: ${colors.ring};`);
39
+ }
40
+ if (colors.success) {
41
+ lines.push(`--color-success: ${colors.success};`);
42
+ lines.push(`--app-success: ${colors.success};`);
43
+ }
44
+ if (colors.successForeground) {
45
+ lines.push(`--color-success-foreground: ${colors.successForeground};`);
46
+ lines.push(`--app-success-foreground: ${colors.successForeground};`);
47
+ }
48
+ if (colors.warning) {
49
+ lines.push(`--color-warning: ${colors.warning};`);
50
+ lines.push(`--app-warning: ${colors.warning};`);
51
+ }
52
+ if (colors.warningForeground) {
53
+ lines.push(`--color-warning-foreground: ${colors.warningForeground};`);
54
+ lines.push(`--app-warning-foreground: ${colors.warningForeground};`);
55
+ }
56
+ if (colors.info) {
57
+ lines.push(`--color-info: ${colors.info};`);
58
+ lines.push(`--app-info: ${colors.info};`);
59
+ }
60
+ if (colors.infoForeground) {
61
+ lines.push(`--color-info-foreground: ${colors.infoForeground};`);
62
+ lines.push(`--app-info-foreground: ${colors.infoForeground};`);
63
+ }
64
+ if (colors.violet) {
65
+ lines.push(`--color-violet: ${colors.violet};`);
66
+ lines.push(`--app-violet: ${colors.violet};`);
67
+ }
68
+ if (colors.violetForeground) {
69
+ lines.push(`--color-violet-foreground: ${colors.violetForeground};`);
70
+ lines.push(`--app-violet-foreground: ${colors.violetForeground};`);
71
+ }
72
+ if (colors.primarySoft) {
73
+ lines.push(`--color-primary-soft: ${colors.primarySoft};`);
74
+ lines.push(`--app-primary-soft: ${colors.primarySoft};`);
75
+ }
76
+
77
+ return lines.join('\n ');
78
+ }
79
+
80
+ export function themeToCssVariables(theme: ThemeConfig): string {
81
+ const rootLines: string[] = [colorPropsToCss(theme.colors)];
82
+
83
+ if (theme.typography?.fontSans) {
84
+ rootLines.push(`--font-sans: ${theme.typography.fontSans};`);
85
+ rootLines.push(`--app-font-sans: ${theme.typography.fontSans};`);
86
+ }
87
+ if (theme.typography?.fontMono) {
88
+ rootLines.push(`--font-mono: ${theme.typography.fontMono};`);
89
+ rootLines.push(`--app-font-mono: ${theme.typography.fontMono};`);
90
+ }
91
+ if (theme.typography?.fontDisplay) {
92
+ rootLines.push(`--font-display: ${theme.typography.fontDisplay};`);
93
+ }
94
+
95
+ if (theme.radii) {
96
+ if (theme.radii.xs) rootLines.push(`--radius-xs: ${theme.radii.xs};`);
97
+ if (theme.radii.sm) rootLines.push(`--radius-sm: ${theme.radii.sm};`);
98
+ if (theme.radii.md) rootLines.push(`--radius-md: ${theme.radii.md};`);
99
+ if (theme.radii.lg) rootLines.push(`--radius-lg: ${theme.radii.lg};`);
100
+ if (theme.radii.xl) rootLines.push(`--radius-xl: ${theme.radii.xl};`);
101
+ }
102
+
103
+ if (theme.customVars) {
104
+ for (const [k, v] of Object.entries(theme.customVars)) {
105
+ rootLines.push(`--${k}: ${v};`);
106
+ }
107
+ }
108
+
109
+ let css = `:root {\n ${rootLines.join('\n ')}\n}\n`;
110
+
111
+ if (theme.darkColors && Object.keys(theme.darkColors).length > 0) {
112
+ const darkMerged: ThemeColors = {
113
+ ...theme.colors,
114
+ ...theme.darkColors,
115
+ };
116
+ css += `\n.dark, [data-theme="dark"] {\n ${colorPropsToCss(darkMerged)}\n}\n`;
117
+ }
118
+
119
+ return css;
120
+ }
121
+
122
+ export function applyTheme(
123
+ themeOrName: string | ThemeConfig,
124
+ targetDoc?: Document
125
+ ): ThemeConfig {
126
+ const theme = typeof themeOrName === 'string' ? getTheme(themeOrName) : themeOrName;
127
+ const doc = targetDoc ?? (typeof document !== 'undefined' ? document : undefined);
128
+ if (!doc) return theme;
129
+
130
+ const styleId = 'umituz-theme-vars';
131
+ let styleEl = doc.getElementById(styleId) as HTMLStyleElement | null;
132
+ if (!styleEl) {
133
+ styleEl = doc.createElement('style');
134
+ styleEl.id = styleId;
135
+ doc.head.appendChild(styleEl);
136
+ }
137
+
138
+ styleEl.textContent = themeToCssVariables(theme);
139
+
140
+ if (theme.mode === 'dark') {
141
+ doc.documentElement.classList.add('dark');
142
+ doc.documentElement.setAttribute('data-theme', 'dark');
143
+ } else if (theme.mode === 'light') {
144
+ doc.documentElement.classList.remove('dark');
145
+ doc.documentElement.setAttribute('data-theme', 'light');
146
+ }
147
+
148
+ return theme;
149
+ }
@@ -0,0 +1,73 @@
1
+ import type { ThemeConfig } from '../../domain/types';
2
+
3
+ export const defaultTheme: ThemeConfig = {
4
+ name: 'default',
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: '#2563eb',
14
+ primaryForeground: '#ffffff',
15
+ primarySoft: 'rgba(37, 99, 235, 0.1)',
16
+ secondary: '#8b5cf6',
17
+ secondaryForeground: '#ffffff',
18
+ muted: '#f1f5f9',
19
+ mutedForeground: '#64748b',
20
+ accent: '#f1f5f9',
21
+ accentForeground: '#0f172a',
22
+ border: '#e2e8f0',
23
+ input: '#e2e8f0',
24
+ destructive: '#dc2626',
25
+ destructiveForeground: '#ffffff',
26
+ ring: 'rgba(37, 99, 235, 0.45)',
27
+ success: '#16a34a',
28
+ successForeground: '#ffffff',
29
+ warning: '#d97706',
30
+ warningForeground: '#ffffff',
31
+ info: '#2563eb',
32
+ infoForeground: '#ffffff',
33
+ violet: '#7c3aed',
34
+ violetForeground: '#ffffff',
35
+ },
36
+ darkColors: {
37
+ background: '#0b1120',
38
+ foreground: '#f1f5f9',
39
+ card: '#111827',
40
+ cardForeground: '#f1f5f9',
41
+ popover: '#111827',
42
+ popoverForeground: '#f1f5f9',
43
+ primary: '#3b82f6',
44
+ primaryForeground: '#ffffff',
45
+ secondary: '#a78bfa',
46
+ secondaryForeground: '#ffffff',
47
+ muted: '#1e293b',
48
+ mutedForeground: '#94a3b8',
49
+ accent: '#1e293b',
50
+ accentForeground: '#f1f5f9',
51
+ border: '#1f2a3c',
52
+ input: '#1f2a3c',
53
+ destructive: '#ef4444',
54
+ destructiveForeground: '#ffffff',
55
+ ring: 'rgba(59, 130, 246, 0.5)',
56
+ success: '#22c55e',
57
+ successForeground: '#ffffff',
58
+ warning: '#f59e0b',
59
+ warningForeground: '#ffffff',
60
+ info: '#3b82f6',
61
+ infoForeground: '#ffffff',
62
+ },
63
+ typography: {
64
+ fontSans: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
65
+ fontMono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
66
+ },
67
+ radii: {
68
+ sm: '6px',
69
+ md: '8px',
70
+ lg: '12px',
71
+ xl: '16px',
72
+ },
73
+ };
@@ -0,0 +1,65 @@
1
+ import type { ThemeConfig } from '../../domain/types';
2
+
3
+ export const hangoraTheme: ThemeConfig = {
4
+ name: 'hangora',
5
+ mode: 'light',
6
+ colors: {
7
+ background: '#f4f1e9',
8
+ foreground: '#23252b',
9
+ card: '#fdfcf8',
10
+ cardForeground: '#23252b',
11
+ popover: '#fdfcf8',
12
+ popoverForeground: '#23252b',
13
+ primary: '#33406b',
14
+ primaryForeground: '#ffffff',
15
+ primarySoft: 'rgba(51, 64, 107, 0.12)',
16
+ secondary: '#efeadc',
17
+ secondaryForeground: '#5a5d66',
18
+ muted: '#efeadc',
19
+ mutedForeground: '#5a5d66',
20
+ accent: '#efeadc',
21
+ accentForeground: '#23252b',
22
+ border: '#e5e1d4',
23
+ input: '#d4cfbf',
24
+ destructive: '#a6483f',
25
+ destructiveForeground: '#ffffff',
26
+ ring: 'rgba(51, 64, 107, 0.4)',
27
+ success: '#3b7a5e',
28
+ successForeground: '#ffffff',
29
+ warning: '#a26b2f',
30
+ warningForeground: '#ffffff',
31
+ info: '#46618f',
32
+ infoForeground: '#ffffff',
33
+ },
34
+ darkColors: {
35
+ background: '#14161f',
36
+ foreground: '#eceadf',
37
+ card: '#1b1e2a',
38
+ cardForeground: '#eceadf',
39
+ popover: '#1b1e2a',
40
+ popoverForeground: '#eceadf',
41
+ primary: '#7f92cc',
42
+ primaryForeground: '#14161f',
43
+ secondary: '#212533',
44
+ secondaryForeground: '#a8abba',
45
+ muted: '#212533',
46
+ mutedForeground: '#a8abba',
47
+ accent: '#212533',
48
+ accentForeground: '#eceadf',
49
+ border: '#2e3344',
50
+ input: '#3d4358',
51
+ destructive: '#c2665c',
52
+ destructiveForeground: '#2a1210',
53
+ ring: 'rgba(143, 160, 214, 0.4)',
54
+ success: '#5ea184',
55
+ successForeground: '#0e1f17',
56
+ warning: '#c98f4a',
57
+ warningForeground: '#221607',
58
+ info: '#7f92cc',
59
+ infoForeground: '#0e1424',
60
+ },
61
+ typography: {
62
+ fontSans: '"Hanken Grotesk", "Noto Sans", -apple-system, "Segoe UI", sans-serif',
63
+ fontDisplay: '"Newsreader", "Iowan Old Style", Georgia, serif',
64
+ },
65
+ };
@@ -0,0 +1,5 @@
1
+ export { accessiaTheme } from './accessia.theme';
2
+ export { hangoraTheme } from './hangora.theme';
3
+ export { defaultTheme } from './default.theme';
4
+ export { registerTheme, getTheme, hasTheme, listThemes } from './registry';
5
+ export { themeToCssVariables, applyTheme } from './apply-theme';
@@ -0,0 +1,26 @@
1
+ import type { ThemeConfig } from '../../domain/types';
2
+ import { accessiaTheme } from './accessia.theme';
3
+ import { hangoraTheme } from './hangora.theme';
4
+ import { defaultTheme } from './default.theme';
5
+
6
+ const themeRegistry = new Map<string, ThemeConfig>([
7
+ ['default', defaultTheme],
8
+ ['accessia', accessiaTheme],
9
+ ['hangora', hangoraTheme],
10
+ ]);
11
+
12
+ export function registerTheme(theme: ThemeConfig): void {
13
+ themeRegistry.set(theme.name, theme);
14
+ }
15
+
16
+ export function getTheme(name: string): ThemeConfig {
17
+ return themeRegistry.get(name) ?? defaultTheme;
18
+ }
19
+
20
+ export function hasTheme(name: string): boolean {
21
+ return themeRegistry.has(name);
22
+ }
23
+
24
+ export function listThemes(): string[] {
25
+ return Array.from(themeRegistry.keys());
26
+ }
@@ -5,6 +5,8 @@ export interface DateFormatOptions {
5
5
  order?: 'dmy' | 'mdy' | 'ymd';
6
6
  separator?: string;
7
7
  invalidOutput?: string;
8
+ weekday?: 'short' | 'long' | 'none';
9
+ month?: 'short' | 'long';
8
10
  }
9
11
 
10
12
  export interface DateTimeFormatOptions extends DateFormatOptions {
@@ -34,6 +36,19 @@ const cachedFormatter = (locale: string, options: Intl.DateTimeFormatOptions): I
34
36
  return formatter;
35
37
  };
36
38
 
39
+ const resolveOptions = (style: Exclude<DateStyle, 'numeric'>, options: DateFormatOptions): Intl.DateTimeFormatOptions => {
40
+ const resolved: Intl.DateTimeFormatOptions = { ...PRESET_OPTIONS[style] };
41
+ if (options.weekday === 'none') {
42
+ delete resolved.weekday;
43
+ } else if (options.weekday !== undefined) {
44
+ resolved.weekday = options.weekday;
45
+ }
46
+ if (options.month !== undefined) {
47
+ resolved.month = options.month;
48
+ }
49
+ return resolved;
50
+ };
51
+
37
52
  export const parseDateInput = (input: string | Date): Date | null => {
38
53
  if (input instanceof Date) {
39
54
  return Number.isNaN(input.getTime()) ? null : input;
@@ -85,7 +100,7 @@ export const formatDate = (
85
100
  if (style === 'numeric') {
86
101
  return formatNumeric(date, locale, options.order ?? 'dmy', options.separator ?? DEFAULT_SEPARATOR);
87
102
  }
88
- return cachedFormatter(locale, PRESET_OPTIONS[style]).format(date);
103
+ return cachedFormatter(locale, resolveOptions(style, options)).format(date);
89
104
  };
90
105
 
91
106
  export const formatDateTime = (