@umituz/web-design-system 6.14.0 → 6.16.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": "6.14.0",
3
+ "version": "6.16.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Web Design System - Tailwind v4 authored, precompiled CSS shipped. Atomic Design components (Atoms, Molecules, Organisms, Templates) for React applications.",
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import { ConditionPips } from '../presentation/molecules/ConditionPips';
4
+
5
+ describe('ConditionPips', () => {
6
+ it('renders score pips with default max of 5', () => {
7
+ const { container } = render(<ConditionPips score={3} />);
8
+ const root = container.querySelector('.cond-pips');
9
+ expect(root).toBeDefined();
10
+ expect(root?.getAttribute('aria-label')).toBe('3/5');
11
+
12
+ const pips = container.querySelectorAll('.pip');
13
+ expect(pips.length).toBe(5);
14
+
15
+ const onPips = container.querySelectorAll('.pip.on');
16
+ expect(onPips.length).toBe(3);
17
+ });
18
+
19
+ it('handles custom max and zero or null scores', () => {
20
+ const { container: c1 } = render(<ConditionPips score={null} max={4} />);
21
+ const root1 = c1.querySelector('.cond-pips');
22
+ expect(root1?.getAttribute('aria-label')).toBe('0/4');
23
+ expect(c1.querySelectorAll('.pip').length).toBe(4);
24
+ expect(c1.querySelectorAll('.pip.on').length).toBe(0);
25
+
26
+ const { container: c2 } = render(<ConditionPips score={7} max={5} ariaLabel="Excellent (5/5)" />);
27
+ const root2 = c2.querySelector('.cond-pips');
28
+ expect(root2?.getAttribute('aria-label')).toBe('Excellent (5/5)');
29
+ expect(c2.querySelectorAll('.pip.on').length).toBe(5);
30
+ });
31
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router';
4
+ import { EntityRow } from '../presentation/molecules/EntityRow';
5
+ import { PlaceholderPanel } from '../presentation/molecules/PlaceholderPanel';
6
+
7
+ describe('EntityRow', () => {
8
+ it('renders title, icon, meta and aside correctly', () => {
9
+ render(
10
+ <MemoryRouter>
11
+ <ul>
12
+ <EntityRow
13
+ icon={<span data-testid="icon">ICON</span>}
14
+ title="Comprehensive Auto"
15
+ meta={['Renewal date: 2026-12-01', 'Policy #A-9928']}
16
+ badge={<span data-testid="badge">Active</span>}
17
+ aside={<span data-testid="aside">$120/mo</span>}
18
+ />
19
+ </ul>
20
+ </MemoryRouter>
21
+ );
22
+
23
+ expect(screen.getByText('Comprehensive Auto')).toBeDefined();
24
+ expect(screen.getByTestId('icon')).toBeDefined();
25
+ expect(screen.getByText('Renewal date: 2026-12-01')).toBeDefined();
26
+ expect(screen.getByText('Policy #A-9928')).toBeDefined();
27
+ expect(screen.getByTestId('badge')).toBeDefined();
28
+ expect(screen.getByTestId('aside')).toBeDefined();
29
+ });
30
+
31
+ it('renders as a link when to prop is provided', () => {
32
+ render(
33
+ <MemoryRouter>
34
+ <ul>
35
+ <EntityRow
36
+ icon={<span>ICON</span>}
37
+ title="Homeowners Plus"
38
+ to="/policies/123"
39
+ highlight={true}
40
+ />
41
+ </ul>
42
+ </MemoryRouter>
43
+ );
44
+
45
+ const link = screen.getByRole('link');
46
+ expect(link.getAttribute('href')).toBe('/policies/123');
47
+ expect(link.className).toContain('highlight');
48
+ expect(screen.getByRole('img', { name: 'Unread' })).toBeDefined();
49
+ });
50
+ });
51
+
52
+ describe('PlaceholderPanel', () => {
53
+ it('renders step, title, description and capabilities', () => {
54
+ render(
55
+ <PlaceholderPanel
56
+ step="Step 4"
57
+ title="Automated Claims"
58
+ description="Submit photos and get instant estimates."
59
+ capabilities={['Direct damage upload', 'Fast adjuster review']}
60
+ >
61
+ <div data-testid="extra-child">Extra content</div>
62
+ </PlaceholderPanel>
63
+ );
64
+
65
+ expect(screen.getByText('Roadmap · Step 4')).toBeDefined();
66
+ expect(screen.getByText('Automated Claims')).toBeDefined();
67
+ expect(screen.getByText('Submit photos and get instant estimates.')).toBeDefined();
68
+ expect(screen.getByText('Direct damage upload')).toBeDefined();
69
+ expect(screen.getByText('Fast adjuster review')).toBeDefined();
70
+ expect(screen.getByTestId('extra-child')).toBeDefined();
71
+ });
72
+ });
@@ -14,6 +14,8 @@ import {
14
14
  editorialTheme,
15
15
  oceanTheme,
16
16
  defaultTheme,
17
+ forestTheme,
18
+ fieldTheme,
17
19
  } from '../infrastructure/themes';
18
20
  import type { ThemeConfig } from '../domain/types';
19
21
 
@@ -30,6 +32,8 @@ describe('Theme System and Provider', () => {
30
32
  expect(getTheme('midnight').name).toBe('midnight');
31
33
  expect(getTheme('editorial').name).toBe('editorial');
32
34
  expect(getTheme('ocean').name).toBe('ocean');
35
+ expect(getTheme('forest').name).toBe('forest');
36
+ expect(getTheme('field').name).toBe('field');
33
37
  expect(getTheme('default').name).toBe('default');
34
38
  expect(getTheme('non-existent').name).toBe('default');
35
39
  });
@@ -114,6 +118,11 @@ describe('Theme System and Provider', () => {
114
118
 
115
119
  const defaultCss = themeToCssVariables(defaultTheme);
116
120
  expect(defaultCss).toContain('--color-primary: #2563eb;');
121
+
122
+ const forestCss = themeToCssVariables(forestTheme);
123
+ expect(forestCss).toContain('--color-primary: #14705f;');
124
+ expect(forestCss).toContain('--color-background: #faf9f7;');
125
+ expect(forestCss).toContain('--color-brand-600: #14705f;');
117
126
  });
118
127
 
119
128
  it('applies theme variables to document head', () => {
@@ -164,4 +173,14 @@ describe('Theme System and Provider', () => {
164
173
  expect(screen.getByTestId('theme-mode').textContent).toBe('dark');
165
174
  expect(document.documentElement.classList.contains('dark')).toBe(true);
166
175
  });
176
+
177
+ it('generates field theme CSS custom variables and dark mode overrides', () => {
178
+ const css = themeToCssVariables(fieldTheme);
179
+ expect(css).toContain('--paper: #f4f3ee;');
180
+ expect(css).toContain('--primary-500: #357344;');
181
+ expect(css).toContain('--font-display: \'Fraunces\', Georgia, serif;');
182
+ expect(css).toContain('.dark, [data-theme="dark"]');
183
+ expect(css).toContain('--paper: #10140f;');
184
+ expect(css).toContain('--primary-500: #6aa878;');
185
+ });
167
186
  });
@@ -53,4 +53,5 @@ export interface ThemeConfig {
53
53
  typography?: ThemeTypography;
54
54
  radii?: ThemeRadii;
55
55
  customVars?: Record<string, string>;
56
+ darkCustomVars?: Record<string, string>;
56
57
  }
package/src/index.ts CHANGED
@@ -147,4 +147,10 @@ export { MarkdownText } from './presentation/molecules/MarkdownText';
147
147
  export type { MarkdownTextProps } from './presentation/molecules/MarkdownText';
148
148
  export { FilterSelect } from './presentation/molecules/FilterSelect';
149
149
  export type { FilterSelectProps, FilterSelectOption } from './presentation/molecules/FilterSelect';
150
+ export { EntityRow } from './presentation/molecules/EntityRow';
151
+ export type { EntityRowProps } from './presentation/molecules/EntityRow';
152
+ export { PlaceholderPanel } from './presentation/molecules/PlaceholderPanel';
153
+ export type { PlaceholderPanelProps } from './presentation/molecules/PlaceholderPanel';
154
+ export { ConditionPips } from './presentation/molecules/ConditionPips';
155
+ export type { ConditionPipsProps } from './presentation/molecules/ConditionPips';
150
156
 
@@ -108,12 +108,24 @@ export function themeToCssVariables(theme: ThemeConfig): string {
108
108
 
109
109
  let css = `:root {\n ${rootLines.join('\n ')}\n}\n`;
110
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`;
111
+ const hasDarkColors = Boolean(theme.darkColors && Object.keys(theme.darkColors).length > 0);
112
+ const hasDarkVars = Boolean(theme.darkCustomVars && Object.keys(theme.darkCustomVars).length > 0);
113
+
114
+ if (hasDarkColors || hasDarkVars) {
115
+ const darkLines: string[] = [];
116
+ if (hasDarkColors) {
117
+ const darkMerged: ThemeColors = {
118
+ ...theme.colors,
119
+ ...theme.darkColors,
120
+ };
121
+ darkLines.push(colorPropsToCss(darkMerged));
122
+ }
123
+ if (hasDarkVars && theme.darkCustomVars) {
124
+ for (const [k, v] of Object.entries(theme.darkCustomVars)) {
125
+ darkLines.push(`--${k}: ${v};`);
126
+ }
127
+ }
128
+ css += `\n.dark, [data-theme="dark"] {\n ${darkLines.join('\n ')}\n}\n`;
117
129
  }
118
130
 
119
131
  return css;
@@ -0,0 +1,230 @@
1
+ import type { ThemeConfig } from '../../domain/types';
2
+
3
+ export const fieldTheme: ThemeConfig = {
4
+ name: 'field',
5
+ mode: 'light',
6
+ colors: {
7
+ background: '#f4f3ee',
8
+ foreground: '#1a231c',
9
+ card: '#ffffff',
10
+ cardForeground: '#212c23',
11
+ popover: '#ffffff',
12
+ popoverForeground: '#212c23',
13
+ primary: '#357344',
14
+ primaryForeground: '#ffffff',
15
+ primarySoft: '#dcead9',
16
+ secondary: '#f1f4ef',
17
+ secondaryForeground: '#2b382d',
18
+ muted: '#edebe3',
19
+ mutedForeground: '#5a655b',
20
+ accent: '#f5e8d3',
21
+ accentForeground: '#c9862f',
22
+ border: '#e2dfd3',
23
+ input: '#cfcaba',
24
+ destructive: '#a53f2a',
25
+ destructiveForeground: '#ffffff',
26
+ ring: 'rgba(53, 115, 68, 0.45)',
27
+ success: '#2f6b40',
28
+ successForeground: '#ffffff',
29
+ warning: '#96690f',
30
+ warningForeground: '#ffffff',
31
+ info: '#3e5f80',
32
+ infoForeground: '#ffffff',
33
+ },
34
+ darkColors: {
35
+ background: '#10140f',
36
+ foreground: '#edf0e7',
37
+ card: '#191f18',
38
+ cardForeground: '#dce1d2',
39
+ popover: '#191f18',
40
+ popoverForeground: '#dce1d2',
41
+ primary: '#6aa878',
42
+ primaryForeground: '#10140f',
43
+ secondary: '#1f261e',
44
+ secondaryForeground: '#c3c9b6',
45
+ muted: '#151a14',
46
+ mutedForeground: '#828b79',
47
+ accent: 'rgba(217, 165, 79, 0.15)',
48
+ accentForeground: '#cf9a48',
49
+ border: '#262e24',
50
+ input: '#333d30',
51
+ destructive: '#d07a63',
52
+ destructiveForeground: '#10140f',
53
+ ring: 'rgba(122, 183, 136, 0.45)',
54
+ success: '#7cb989',
55
+ successForeground: '#10140f',
56
+ warning: '#d3a04e',
57
+ warningForeground: '#10140f',
58
+ info: '#8fb0d4',
59
+ infoForeground: '#10140f',
60
+ },
61
+ typography: {
62
+ fontSans: "'Archivo', 'Segoe UI', system-ui, sans-serif",
63
+ fontDisplay: "'Fraunces', Georgia, serif",
64
+ fontMono: "'IBM Plex Mono', 'SFMono-Regular', Menlo, monospace",
65
+ },
66
+ radii: {
67
+ xs: '2px',
68
+ sm: '4px',
69
+ md: '6px',
70
+ lg: '9px',
71
+ xl: '999px',
72
+ },
73
+ customVars: {
74
+ paper: '#f4f3ee',
75
+ 'paper-deep': '#edebe3',
76
+ surface: '#ffffff',
77
+ 'surface-sunken': '#faf9f5',
78
+ 'surface-tint': '#f1f4ef',
79
+ 'ink-900': '#1a231c',
80
+ 'ink-800': '#212c23',
81
+ 'ink-700': '#2b382d',
82
+ 'ink-600': '#414d42',
83
+ 'ink-500': '#5a655b',
84
+ 'ink-400': '#79827a',
85
+ 'ink-300': '#9aa29a',
86
+ 'ink-200': '#c3c8bd',
87
+ 'ink-100': '#dfe2d8',
88
+ 'chrome-900': '#131f17',
89
+ 'chrome-800': '#16251b',
90
+ 'chrome-700': '#1c2e21',
91
+ 'chrome-600': '#233a2a',
92
+ 'chrome-500': '#2c4734',
93
+ 'chrome-line': '#2a3f30',
94
+ 'chrome-text': '#c5cfc4',
95
+ 'chrome-text-dim': '#7f9484',
96
+ 'chrome-active': '#d8e6d4',
97
+ 'primary-700': '#24502f',
98
+ 'primary-600': '#2c6139',
99
+ 'primary-500': '#357344',
100
+ 'primary-100': '#dcead9',
101
+ 'primary-050': '#edf4ea',
102
+ 'accent-600': '#b06e22',
103
+ 'accent-500': '#c9862f',
104
+ 'accent-100': '#f5e8d3',
105
+ 'status-success': '#2f6b40',
106
+ 'status-success-bg': '#e2efdf',
107
+ 'status-warning': '#96690f',
108
+ 'status-warning-bg': '#f6ecd4',
109
+ 'status-danger': '#a53f2a',
110
+ 'status-danger-bg': '#f7e2db',
111
+ 'status-info': '#3e5f80',
112
+ 'status-info-bg': '#e0eaf2',
113
+ 'status-neutral': '#55605a',
114
+ 'status-neutral-bg': '#e8eae4',
115
+ 'status-critical': '#8f1f14',
116
+ 'danger-600': '#a53f2a',
117
+ 'warning-600': '#96690f',
118
+ line: '#e2dfd3',
119
+ 'line-strong': '#cfcaba',
120
+ 'line-chrome': '#2a3f30',
121
+ 'font-ui': "'Archivo', 'Segoe UI', system-ui, sans-serif",
122
+ 'font-display': "'Fraunces', Georgia, serif",
123
+ 'font-mono': "'IBM Plex Mono', 'SFMono-Regular', Menlo, monospace",
124
+ 'text-xs': '11px',
125
+ 'text-sm': '12.5px',
126
+ 'text-base': '13.5px',
127
+ 'text-md': '15px',
128
+ 'text-lg': '17px',
129
+ 'text-xl': '21px',
130
+ 'text-2xl': '26px',
131
+ 'sp-1': '4px',
132
+ 'sp-2': '8px',
133
+ 'sp-3': '12px',
134
+ 'sp-4': '16px',
135
+ 'sp-5': '20px',
136
+ 'sp-6': '24px',
137
+ 'sp-7': '32px',
138
+ 'sp-8': '40px',
139
+ 'sp-9': '56px',
140
+ 'r-sm': '4px',
141
+ 'r-md': '6px',
142
+ 'r-lg': '9px',
143
+ 'r-pill': '999px',
144
+ 'shadow-1': '0 1px 2px rgba(26, 35, 28, 0.05)',
145
+ 'shadow-2': '0 2px 6px rgba(26, 35, 28, 0.07), 0 1px 2px rgba(26, 35, 28, 0.05)',
146
+ 'shadow-3': '0 12px 32px -8px rgba(26, 35, 28, 0.18), 0 2px 6px rgba(26, 35, 28, 0.06)',
147
+ 'shadow-overlay': '0 24px 64px -16px rgba(19, 31, 23, 0.35)',
148
+ 'sidebar-w': '244px',
149
+ 'sidebar-w-collapsed': '64px',
150
+ 'topbar-h': '54px',
151
+ 'page-max': '1560px',
152
+ 'chart-1': '#357344',
153
+ 'chart-2': '#b06e22',
154
+ 'chart-3': '#3e5f80',
155
+ 'chart-4': '#7a6ba8',
156
+ 'chart-5': '#8a5a3b',
157
+ 'chart-6': '#5e7d5a',
158
+ 'chart-faint': '#e5e9df',
159
+ 'tint-hover': 'rgba(26, 35, 28, 0.06)',
160
+ 'tint-press': 'rgba(26, 35, 28, 0.09)',
161
+ 'topbar-glass': 'rgba(244, 243, 238, 0.86)',
162
+ 'dur-1': '110ms',
163
+ 'dur-2': '180ms',
164
+ 'dur-3': '280ms',
165
+ ease: 'cubic-bezier(0.32, 0.72, 0, 1)',
166
+ },
167
+ darkCustomVars: {
168
+ paper: '#10140f',
169
+ 'paper-deep': '#151a14',
170
+ surface: '#191f18',
171
+ 'surface-sunken': '#141a13',
172
+ 'surface-tint': '#1f261e',
173
+ 'ink-900': '#edf0e7',
174
+ 'ink-800': '#dce1d2',
175
+ 'ink-700': '#c3c9b6',
176
+ 'ink-600': '#a2aa96',
177
+ 'ink-500': '#828b79',
178
+ 'ink-400': '#646d5d',
179
+ 'ink-300': '#4b5346',
180
+ 'ink-200': '#333c30',
181
+ 'ink-100': '#232b21',
182
+ 'chrome-900': '#0c130d',
183
+ 'chrome-800': '#0f1810',
184
+ 'chrome-700': '#132016',
185
+ 'chrome-600': '#182718',
186
+ 'chrome-500': '#1d2f1e',
187
+ 'chrome-line': '#1c2a1d',
188
+ 'chrome-text': '#b9c6b6',
189
+ 'chrome-text-dim': '#71836f',
190
+ 'chrome-active': '#cfe2ca',
191
+ 'primary-700': '#8fc79c',
192
+ 'primary-600': '#79b788',
193
+ 'primary-500': '#6aa878',
194
+ 'primary-100': 'rgba(121, 183, 136, 0.16)',
195
+ 'primary-050': 'rgba(121, 183, 136, 0.09)',
196
+ 'accent-600': '#d9a54f',
197
+ 'accent-500': '#cf9a48',
198
+ 'accent-100': 'rgba(217, 165, 79, 0.15)',
199
+ 'status-success': '#7cb989',
200
+ 'status-success-bg': 'rgba(124, 185, 137, 0.14)',
201
+ 'status-warning': '#d3a04e',
202
+ 'status-warning-bg': 'rgba(211, 160, 78, 0.14)',
203
+ 'status-danger': '#d07a63',
204
+ 'status-danger-bg': 'rgba(208, 122, 99, 0.15)',
205
+ 'status-info': '#8fb0d4',
206
+ 'status-info-bg': 'rgba(143, 176, 212, 0.14)',
207
+ 'status-neutral': '#a3a898',
208
+ 'status-neutral-bg': 'rgba(163, 168, 152, 0.13)',
209
+ 'status-critical': '#e08a73',
210
+ 'danger-600': '#d07a63',
211
+ 'warning-600': '#d3a04e',
212
+ line: '#262e24',
213
+ 'line-strong': '#333d30',
214
+ 'line-chrome': '#1c2a1d',
215
+ 'shadow-1': '0 1px 2px rgba(0, 0, 0, 0.35)',
216
+ 'shadow-2': '0 2px 6px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3)',
217
+ 'shadow-3': '0 12px 32px -8px rgba(0, 0, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.35)',
218
+ 'shadow-overlay': '0 24px 64px -16px rgba(0, 0, 0, 0.7)',
219
+ 'chart-1': '#79b788',
220
+ 'chart-2': '#d9a54f',
221
+ 'chart-3': '#8fb0d4',
222
+ 'chart-4': '#a79bd4',
223
+ 'chart-5': '#c78e6b',
224
+ 'chart-6': '#93b389',
225
+ 'chart-faint': '#232b21',
226
+ 'tint-hover': 'rgba(237, 240, 231, 0.06)',
227
+ 'tint-press': 'rgba(237, 240, 231, 0.09)',
228
+ 'topbar-glass': 'rgba(16, 20, 15, 0.86)',
229
+ },
230
+ };
@@ -0,0 +1,145 @@
1
+ import type { ThemeConfig } from '../../domain/types';
2
+
3
+ export const forestTheme: ThemeConfig = {
4
+ name: 'forest',
5
+ mode: 'light',
6
+ colors: {
7
+ background: '#faf9f7',
8
+ foreground: '#101c19',
9
+ card: '#ffffff',
10
+ cardForeground: '#1a2723',
11
+ popover: '#ffffff',
12
+ popoverForeground: '#1a2723',
13
+ primary: '#14705f',
14
+ primaryForeground: '#ffffff',
15
+ primarySoft: '#eef5f2',
16
+ secondary: '#f3f2ee',
17
+ secondaryForeground: '#33413d',
18
+ muted: '#f3f2ee',
19
+ mutedForeground: '#5c6b66',
20
+ accent: '#f4ead4',
21
+ accentForeground: '#9a7328',
22
+ border: '#e7e5df',
23
+ input: '#d5d2ca',
24
+ destructive: '#a32d22',
25
+ destructiveForeground: '#ffffff',
26
+ ring: 'rgba(20, 112, 95, 0.45)',
27
+ success: '#1d6b46',
28
+ successForeground: '#ffffff',
29
+ warning: '#8a5a00',
30
+ warningForeground: '#ffffff',
31
+ info: '#1c5a80',
32
+ infoForeground: '#ffffff',
33
+ },
34
+ darkColors: {
35
+ background: '#0c1916',
36
+ foreground: '#e6ece9',
37
+ card: '#10221e',
38
+ cardForeground: '#e6ece9',
39
+ popover: '#10221e',
40
+ popoverForeground: '#e6ece9',
41
+ primary: '#4f9c86',
42
+ primaryForeground: '#0c1916',
43
+ secondary: '#16332d',
44
+ secondaryForeground: '#a3ada9',
45
+ muted: '#16332d',
46
+ mutedForeground: '#a3ada9',
47
+ accent: '#16332d',
48
+ accentForeground: '#e6ece9',
49
+ border: '#1c423a',
50
+ input: '#245248',
51
+ destructive: '#c2665c',
52
+ destructiveForeground: '#2a1210',
53
+ ring: 'rgba(79, 156, 134, 0.45)',
54
+ success: '#5ea184',
55
+ successForeground: '#0e1f17',
56
+ warning: '#c98f4a',
57
+ warningForeground: '#221607',
58
+ info: '#7f92cc',
59
+ infoForeground: '#0e1424',
60
+ },
61
+ typography: {
62
+ fontSans: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
63
+ fontDisplay: "'Fraunces', Georgia, 'Times New Roman', serif",
64
+ },
65
+ radii: {
66
+ sm: '6px',
67
+ md: '10px',
68
+ lg: '16px',
69
+ xl: '999px',
70
+ },
71
+ customVars: {
72
+ 'color-brand-50': '#eef5f2',
73
+ 'color-brand-100': '#d9eae3',
74
+ 'color-brand-200': '#b3d5c8',
75
+ 'color-brand-400': '#4f9c86',
76
+ 'color-brand-500': '#2c8268',
77
+ 'color-brand-600': '#14705f',
78
+ 'color-brand-700': '#11574b',
79
+ 'color-brand-800': '#0f463d',
80
+ 'color-brand-900': '#0c3b33',
81
+ 'color-ink-950': '#101c19',
82
+ 'color-ink-900': '#1a2723',
83
+ 'color-ink-700': '#33413d',
84
+ 'color-ink-500': '#5c6b66',
85
+ 'color-ink-400': '#7d8a85',
86
+ 'color-ink-300': '#a3ada9',
87
+ 'color-paper': '#faf9f7',
88
+ 'color-surface': '#ffffff',
89
+ 'color-surface-subtle': '#f3f2ee',
90
+ 'color-line': '#e7e5df',
91
+ 'color-line-strong': '#d5d2ca',
92
+ 'color-inverse': '#0f2e28',
93
+ 'color-scrim': 'rgba(16, 28, 25, 0.4)',
94
+ 'color-hairline-inverse': 'rgba(238, 245, 242, 0.14)',
95
+ 'color-accent-600': '#9a7328',
96
+ 'color-accent-100': '#f4ead4',
97
+ 'color-success-700': '#1d6b46',
98
+ 'color-success-100': '#e0f0e6',
99
+ 'color-warning-700': '#8a5a00',
100
+ 'color-warning-100': '#f9eed3',
101
+ 'color-danger-700': '#a32d22',
102
+ 'color-danger-100': '#f9e3df',
103
+ 'color-info-700': '#1c5a80',
104
+ 'color-info-100': '#e1eef6',
105
+ 'space-1': '0.25rem',
106
+ 'space-2': '0.5rem',
107
+ 'space-3': '0.75rem',
108
+ 'space-4': '1rem',
109
+ 'space-5': '1.5rem',
110
+ 'space-6': '2rem',
111
+ 'space-8': '3rem',
112
+ 'space-10': '4rem',
113
+ 'space-12': '5rem',
114
+ 'space-16': '7rem',
115
+ 'radius-pill': '999px',
116
+ 'shadow-sm': '0 1px 2px rgba(16, 28, 25, 0.05)',
117
+ 'shadow-md': '0 10px 30px rgba(16, 28, 25, 0.08)',
118
+ 'container-max': '72rem',
119
+ 'container-narrow': '48rem',
120
+ 'header-height': '4rem',
121
+ 'duration-fast': '140ms',
122
+ 'duration-slow': '240ms',
123
+ 'ease-out': 'cubic-bezier(0.2, 0.7, 0.3, 1)',
124
+ 'focus-ring': '0 0 0 2px #faf9f7, 0 0 0 4px #2c8268',
125
+ 'text-xs': '0.75rem',
126
+ 'text-sm': '0.875rem',
127
+ 'text-base': '1rem',
128
+ 'text-lg': '1.125rem',
129
+ 'text-xl': '1.375rem',
130
+ 'text-2xl': '1.75rem',
131
+ 'text-3xl': '2.125rem',
132
+ 'text-4xl': '2.75rem',
133
+ 'text-hero': 'clamp(2.625rem, 6vw, 4.25rem)',
134
+ 'leading-tight': '1.15',
135
+ 'leading-snug': '1.35',
136
+ 'leading-normal': '1.55',
137
+ 'leading-relaxed': '1.7',
138
+ 'tracking-tight': '-0.02em',
139
+ 'tracking-display': '-0.025em',
140
+ 'tracking-wide': '0.08em',
141
+ 'weight-regular': '400',
142
+ 'weight-medium': '500',
143
+ 'weight-semibold': '600',
144
+ },
145
+ };
@@ -2,6 +2,8 @@ export { defaultTheme } from './default.theme';
2
2
  export { oceanTheme } from './ocean.theme';
3
3
  export { midnightTheme } from './midnight.theme';
4
4
  export { editorialTheme } from './editorial.theme';
5
+ export { forestTheme } from './forest.theme';
6
+ export { fieldTheme } from './field.theme';
5
7
  export { createTheme } from './create-theme';
6
8
  export type { ThemeOverrides } from './create-theme';
7
9
  export { registerTheme, getTheme, hasTheme, listThemes } from './registry';
@@ -3,12 +3,16 @@ import { defaultTheme } from './default.theme';
3
3
  import { oceanTheme } from './ocean.theme';
4
4
  import { midnightTheme } from './midnight.theme';
5
5
  import { editorialTheme } from './editorial.theme';
6
+ import { forestTheme } from './forest.theme';
7
+ import { fieldTheme } from './field.theme';
6
8
 
7
9
  const themeRegistry = new Map<string, ThemeConfig>([
8
10
  ['default', defaultTheme],
9
11
  ['ocean', oceanTheme],
10
12
  ['midnight', midnightTheme],
11
13
  ['editorial', editorialTheme],
14
+ ['forest', forestTheme],
15
+ ['field', fieldTheme],
12
16
  ]);
13
17
 
14
18
  export function registerTheme(theme: ThemeConfig): void {
@@ -0,0 +1,37 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ import { cn } from '../../infrastructure/utils';
3
+
4
+ export interface ConditionPipsProps extends HTMLAttributes<HTMLSpanElement> {
5
+ score: number | null | undefined;
6
+ max?: number;
7
+ className?: string;
8
+ ariaLabel?: string;
9
+ }
10
+
11
+ export function ConditionPips({
12
+ score,
13
+ max = 5,
14
+ className,
15
+ ariaLabel,
16
+ ...props
17
+ }: ConditionPipsProps) {
18
+ const value = Math.max(0, Math.min(score ?? 0, max));
19
+ const label = ariaLabel ?? `${value}/${max}`;
20
+
21
+ return (
22
+ <span
23
+ className={cn('cond-pips', className)}
24
+ role="img"
25
+ aria-label={label}
26
+ {...props}
27
+ >
28
+ {Array.from({ length: max }, (_, i) => (
29
+ <span
30
+ key={i}
31
+ className={cn('pip', i < value && 'on')}
32
+ aria-hidden="true"
33
+ />
34
+ ))}
35
+ </span>
36
+ );
37
+ }