@discourser/design-system 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guidelines/Guidelines.md +88 -0
- package/guidelines/components/button.md +314 -0
- package/guidelines/components/card.md +353 -0
- package/guidelines/components/dialog.md +465 -0
- package/guidelines/components/icon-button.md +417 -0
- package/guidelines/components/input.md +499 -0
- package/guidelines/components/switch.md +457 -0
- package/guidelines/design-tokens/colors.md +187 -0
- package/guidelines/design-tokens/elevation.md +274 -0
- package/guidelines/design-tokens/spacing.md +289 -0
- package/guidelines/design-tokens/typography.md +226 -0
- package/guidelines/overview-components.md +156 -0
- package/package.json +3 -2
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# Switch
|
|
2
|
+
|
|
3
|
+
**Purpose:** Toggle control for binary on/off states following Material Design 3 patterns.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Switch } from '@discourser/design-system';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
The Switch component provides:
|
|
14
|
+
- Binary on/off toggle functionality
|
|
15
|
+
- Visual feedback for state changes
|
|
16
|
+
- Smooth animation between states
|
|
17
|
+
- Built-in label support
|
|
18
|
+
- Form integration capabilities
|
|
19
|
+
- Keyboard accessibility
|
|
20
|
+
|
|
21
|
+
## Sizes
|
|
22
|
+
|
|
23
|
+
| Size | Track Width | Track Height | Thumb Size (off) | Thumb Size (on) | Label Size |
|
|
24
|
+
|------|------------|--------------|------------------|-----------------|-----------|
|
|
25
|
+
| `sm` | 44px | 24px | 12×12px | 16×16px | bodySmall |
|
|
26
|
+
| `md` | 52px | 32px | 16×16px | 24×24px | bodyMedium |
|
|
27
|
+
|
|
28
|
+
**Note:** The thumb (circle) grows larger when the switch is in the "on" state, following M3 specifications.
|
|
29
|
+
|
|
30
|
+
## Props
|
|
31
|
+
|
|
32
|
+
| Prop | Type | Default | Description |
|
|
33
|
+
|------|------|---------|-------------|
|
|
34
|
+
| `label` | `string` | - | Label text displayed next to switch |
|
|
35
|
+
| `checked` | `boolean` | - | Controlled checked state |
|
|
36
|
+
| `defaultChecked` | `boolean` | - | Uncontrolled default checked state |
|
|
37
|
+
| `onCheckedChange` | `(details: { checked: boolean }) => void` | - | Callback when checked state changes |
|
|
38
|
+
| `disabled` | `boolean` | `false` | Disable switch interaction |
|
|
39
|
+
| `name` | `string` | - | Name attribute for form submission |
|
|
40
|
+
| `value` | `string` | - | Value attribute for form submission |
|
|
41
|
+
| `required` | `boolean` | `false` | Whether switch is required in form |
|
|
42
|
+
| `size` | `'sm' \| 'md'` | `'md'` | Switch size |
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
### Basic Usage
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Uncontrolled switch (default off)
|
|
50
|
+
<Switch label="Enable notifications" />
|
|
51
|
+
|
|
52
|
+
// Uncontrolled with default checked
|
|
53
|
+
<Switch label="Dark mode" defaultChecked />
|
|
54
|
+
|
|
55
|
+
// Controlled switch
|
|
56
|
+
const [enabled, setEnabled] = useState(false);
|
|
57
|
+
|
|
58
|
+
<Switch
|
|
59
|
+
label="Email notifications"
|
|
60
|
+
checked={enabled}
|
|
61
|
+
onCheckedChange={({ checked }) => setEnabled(checked)}
|
|
62
|
+
/>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Different Sizes
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Small switch
|
|
69
|
+
<Switch size="sm" label="Compact toggle" />
|
|
70
|
+
|
|
71
|
+
// Medium switch (default)
|
|
72
|
+
<Switch size="md" label="Standard toggle" />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Disabled State
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
<Switch label="Disabled (off)" disabled />
|
|
79
|
+
|
|
80
|
+
<Switch label="Disabled (on)" disabled defaultChecked />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Without Label
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Switch without label (ensure you provide accessibility context elsewhere)
|
|
87
|
+
<Switch />
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Common Patterns
|
|
91
|
+
|
|
92
|
+
### Settings Panel
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const [settings, setSettings] = useState({
|
|
96
|
+
notifications: true,
|
|
97
|
+
darkMode: false,
|
|
98
|
+
autoSave: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
<div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
|
|
102
|
+
<Switch
|
|
103
|
+
label="Push notifications"
|
|
104
|
+
checked={settings.notifications}
|
|
105
|
+
onCheckedChange={({ checked }) =>
|
|
106
|
+
setSettings({ ...settings, notifications: checked })
|
|
107
|
+
}
|
|
108
|
+
/>
|
|
109
|
+
<Switch
|
|
110
|
+
label="Dark mode"
|
|
111
|
+
checked={settings.darkMode}
|
|
112
|
+
onCheckedChange={({ checked }) =>
|
|
113
|
+
setSettings({ ...settings, darkMode: checked })
|
|
114
|
+
}
|
|
115
|
+
/>
|
|
116
|
+
<Switch
|
|
117
|
+
label="Auto-save"
|
|
118
|
+
checked={settings.autoSave}
|
|
119
|
+
onCheckedChange={({ checked }) =>
|
|
120
|
+
setSettings({ ...settings, autoSave: checked })
|
|
121
|
+
}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Form Integration
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
<form onSubmit={handleSubmit}>
|
|
130
|
+
<Switch
|
|
131
|
+
name="terms"
|
|
132
|
+
value="accepted"
|
|
133
|
+
label="I agree to the terms and conditions"
|
|
134
|
+
required
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
<Switch
|
|
138
|
+
name="newsletter"
|
|
139
|
+
value="subscribed"
|
|
140
|
+
label="Subscribe to newsletter (optional)"
|
|
141
|
+
/>
|
|
142
|
+
|
|
143
|
+
<Button type="submit">Submit</Button>
|
|
144
|
+
</form>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### With Description
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { css } from '@discourser/design-system/styled-system/css';
|
|
151
|
+
|
|
152
|
+
<div className={css({ display: 'flex', alignItems: 'flex-start', gap: 'sm' })}>
|
|
153
|
+
<Switch
|
|
154
|
+
checked={autoBackup}
|
|
155
|
+
onCheckedChange={({ checked }) => setAutoBackup(checked)}
|
|
156
|
+
/>
|
|
157
|
+
<div>
|
|
158
|
+
<label className={css({ textStyle: 'bodyMedium', fontWeight: 500 })}>
|
|
159
|
+
Automatic backups
|
|
160
|
+
</label>
|
|
161
|
+
<p className={css({ textStyle: 'bodySmall', color: 'onSurfaceVariant', mt: 'xxs' })}>
|
|
162
|
+
Your data will be backed up automatically every 24 hours
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Dynamic Enable/Disable
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const [featureEnabled, setFeatureEnabled] = useState(false);
|
|
172
|
+
const [advancedMode, setAdvancedMode] = useState(false);
|
|
173
|
+
|
|
174
|
+
<>
|
|
175
|
+
<Switch
|
|
176
|
+
label="Enable advanced features"
|
|
177
|
+
checked={featureEnabled}
|
|
178
|
+
onCheckedChange={({ checked }) => {
|
|
179
|
+
setFeatureEnabled(checked);
|
|
180
|
+
if (!checked) setAdvancedMode(false); // Reset dependent switch
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
|
|
184
|
+
<Switch
|
|
185
|
+
label="Advanced mode"
|
|
186
|
+
checked={advancedMode}
|
|
187
|
+
onCheckedChange={({ checked }) => setAdvancedMode(checked)}
|
|
188
|
+
disabled={!featureEnabled} // Only enabled when feature is on
|
|
189
|
+
/>
|
|
190
|
+
</>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### List of Toggleable Items
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const [items, setItems] = useState([
|
|
197
|
+
{ id: 1, name: 'Feature A', enabled: true },
|
|
198
|
+
{ id: 2, name: 'Feature B', enabled: false },
|
|
199
|
+
{ id: 3, name: 'Feature C', enabled: true },
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
const toggleItem = (id: number) => {
|
|
203
|
+
setItems(items.map(item =>
|
|
204
|
+
item.id === id ? { ...item, enabled: !item.enabled } : item
|
|
205
|
+
));
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
<div className={css({ display: 'flex', flexDirection: 'column', gap: 'sm' })}>
|
|
209
|
+
{items.map(item => (
|
|
210
|
+
<Switch
|
|
211
|
+
key={item.id}
|
|
212
|
+
label={item.name}
|
|
213
|
+
checked={item.enabled}
|
|
214
|
+
onCheckedChange={() => toggleItem(item.id)}
|
|
215
|
+
/>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## DO NOT
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// ❌ Don't use checkbox for on/off toggles (use Switch instead)
|
|
224
|
+
<input type="checkbox" /> Enable feature
|
|
225
|
+
|
|
226
|
+
// ✅ Use Switch for binary toggles
|
|
227
|
+
<Switch label="Enable feature" />
|
|
228
|
+
|
|
229
|
+
// ❌ Don't use Switch for multiple choice (use radio or checkbox)
|
|
230
|
+
<Switch label="Option A" />
|
|
231
|
+
<Switch label="Option B" />
|
|
232
|
+
<Switch label="Option C" /> // Wrong for mutually exclusive options
|
|
233
|
+
|
|
234
|
+
// ✅ Use radio buttons for mutually exclusive choices
|
|
235
|
+
<input type="radio" name="option" value="a" /> Option A
|
|
236
|
+
<input type="radio" name="option" value="b" /> Option B
|
|
237
|
+
|
|
238
|
+
// ❌ Don't use Switch for submit actions (use Button instead)
|
|
239
|
+
<Switch label="Submit form" /> // Wrong
|
|
240
|
+
|
|
241
|
+
// ✅ Use Button for actions
|
|
242
|
+
<Button type="submit">Submit</Button>
|
|
243
|
+
|
|
244
|
+
// ❌ Don't nest switches or use as navigation
|
|
245
|
+
<Switch label="Navigate to settings" onClick={() => navigate('/settings')} /> // Wrong
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Accessibility
|
|
249
|
+
|
|
250
|
+
The Switch component follows WCAG 2.1 Level AA standards:
|
|
251
|
+
|
|
252
|
+
- **Keyboard Navigation**: Toggle with Space key, focus with Tab
|
|
253
|
+
- **ARIA Attributes**: Uses role="switch", aria-checked for state
|
|
254
|
+
- **Labels**: Label associated with switch for screen readers
|
|
255
|
+
- **Focus Indicator**: Visible focus state
|
|
256
|
+
- **State Announcement**: State changes announced to screen readers
|
|
257
|
+
- **Touch Target**: Adequate size for touch interaction
|
|
258
|
+
|
|
259
|
+
### Accessibility Best Practices
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// ✅ Always provide labels for clarity
|
|
263
|
+
<Switch label="Enable notifications" />
|
|
264
|
+
|
|
265
|
+
// ✅ Use descriptive labels
|
|
266
|
+
<Switch label="Receive email updates" /> // Clear what it toggles
|
|
267
|
+
|
|
268
|
+
// ❌ Vague labels
|
|
269
|
+
<Switch label="Enable" /> // Enable what?
|
|
270
|
+
|
|
271
|
+
// ✅ Indicate state in label if needed
|
|
272
|
+
<Switch
|
|
273
|
+
label={darkMode ? 'Dark mode (on)' : 'Dark mode (off)'}
|
|
274
|
+
checked={darkMode}
|
|
275
|
+
onCheckedChange={({ checked }) => setDarkMode(checked)}
|
|
276
|
+
/>
|
|
277
|
+
|
|
278
|
+
// ✅ Provide context for switch without visible label
|
|
279
|
+
<Switch
|
|
280
|
+
aria-label="Enable push notifications for this conversation"
|
|
281
|
+
/>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## State Behaviors
|
|
285
|
+
|
|
286
|
+
| State | Visual Change | Behavior |
|
|
287
|
+
|-------|---------------|----------|
|
|
288
|
+
| **Unchecked** | Track: `surfaceContainerHighest` with `outline` border<br />Thumb: Small, `outline` color, left position | Off state |
|
|
289
|
+
| **Checked** | Track: `primary` color<br />Thumb: Larger, `onPrimary` color, right position | On state |
|
|
290
|
+
| **Hover** | Subtle visual feedback | Interactive feedback |
|
|
291
|
+
| **Focus** | Focus indicator (handled by Ark UI) | Keyboard accessibility |
|
|
292
|
+
| **Disabled** | 38% opacity, greyed out | Cannot be toggled |
|
|
293
|
+
| **Animation** | Smooth thumb transition (fast easing) | Visual confirmation of state change |
|
|
294
|
+
|
|
295
|
+
## Visual States
|
|
296
|
+
|
|
297
|
+
### Unchecked (Off)
|
|
298
|
+
- Track background: `surfaceContainerHighest`
|
|
299
|
+
- Track border: 2px `outline`
|
|
300
|
+
- Thumb: Small (16×16px for md), `outline` color
|
|
301
|
+
- Thumb position: Left
|
|
302
|
+
|
|
303
|
+
### Checked (On)
|
|
304
|
+
- Track background: `primary`
|
|
305
|
+
- Track border: 2px `primary`
|
|
306
|
+
- Thumb: Large (24×24px for md), `onPrimary` color
|
|
307
|
+
- Thumb position: Right
|
|
308
|
+
|
|
309
|
+
### Disabled
|
|
310
|
+
- Track background: `surfaceVariant`
|
|
311
|
+
- Track border: `onSurface` (12% opacity)
|
|
312
|
+
- Thumb: `onSurface` (38% opacity)
|
|
313
|
+
- Overall opacity: 38%
|
|
314
|
+
|
|
315
|
+
## Form Integration
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// React Hook Form
|
|
319
|
+
import { useForm, Controller } from 'react-hook-form';
|
|
320
|
+
|
|
321
|
+
const { control, handleSubmit } = useForm();
|
|
322
|
+
|
|
323
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
324
|
+
<Controller
|
|
325
|
+
name="notifications"
|
|
326
|
+
control={control}
|
|
327
|
+
defaultValue={false}
|
|
328
|
+
render={({ field }) => (
|
|
329
|
+
<Switch
|
|
330
|
+
label="Enable notifications"
|
|
331
|
+
checked={field.value}
|
|
332
|
+
onCheckedChange={({ checked }) => field.onChange(checked)}
|
|
333
|
+
/>
|
|
334
|
+
)}
|
|
335
|
+
/>
|
|
336
|
+
</form>
|
|
337
|
+
|
|
338
|
+
// Formik
|
|
339
|
+
import { useFormik } from 'formik';
|
|
340
|
+
|
|
341
|
+
const formik = useFormik({
|
|
342
|
+
initialValues: { darkMode: false },
|
|
343
|
+
onSubmit: values => { /* ... */ },
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
<Switch
|
|
347
|
+
label="Dark mode"
|
|
348
|
+
name="darkMode"
|
|
349
|
+
checked={formik.values.darkMode}
|
|
350
|
+
onCheckedChange={({ checked }) => formik.setFieldValue('darkMode', checked)}
|
|
351
|
+
/>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Use Cases
|
|
355
|
+
|
|
356
|
+
| Use Case | Recommendation |
|
|
357
|
+
|----------|---------------|
|
|
358
|
+
| Enable/disable feature | ✅ Perfect use case |
|
|
359
|
+
| On/off settings | ✅ Perfect use case |
|
|
360
|
+
| Binary preferences | ✅ Perfect use case |
|
|
361
|
+
| Show/hide sections | ✅ Good use case |
|
|
362
|
+
| Mutually exclusive options | ❌ Use radio buttons |
|
|
363
|
+
| Multiple selections | ❌ Use checkboxes |
|
|
364
|
+
| Trigger actions | ❌ Use buttons |
|
|
365
|
+
|
|
366
|
+
## Responsive Considerations
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// Mobile: Consider using md (default) for better touch targets
|
|
370
|
+
<Switch size="md" label="Enable feature" />
|
|
371
|
+
|
|
372
|
+
// Desktop: Can use sm for denser layouts
|
|
373
|
+
<Switch size={{ base: 'md', lg: 'sm' }} label="Enable feature" />
|
|
374
|
+
|
|
375
|
+
// Settings panel with responsive spacing
|
|
376
|
+
<div className={css({
|
|
377
|
+
display: 'flex',
|
|
378
|
+
flexDirection: 'column',
|
|
379
|
+
gap: { base: 'md', lg: 'sm' }
|
|
380
|
+
})}>
|
|
381
|
+
<Switch label="Notification 1" />
|
|
382
|
+
<Switch label="Notification 2" />
|
|
383
|
+
<Switch label="Notification 3" />
|
|
384
|
+
</div>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Testing
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { render, screen } from '@testing-library/react';
|
|
391
|
+
import userEvent from '@testing-library/user-event';
|
|
392
|
+
|
|
393
|
+
test('switch toggles checked state', async () => {
|
|
394
|
+
const handleChange = vi.fn();
|
|
395
|
+
render(
|
|
396
|
+
<Switch
|
|
397
|
+
label="Enable feature"
|
|
398
|
+
checked={false}
|
|
399
|
+
onCheckedChange={handleChange}
|
|
400
|
+
/>
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const switchControl = screen.getByRole('switch', { name: 'Enable feature' });
|
|
404
|
+
expect(switchControl).not.toBeChecked();
|
|
405
|
+
|
|
406
|
+
await userEvent.click(switchControl);
|
|
407
|
+
|
|
408
|
+
expect(handleChange).toHaveBeenCalledWith({ checked: true });
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('switch respects disabled state', async () => {
|
|
412
|
+
const handleChange = vi.fn();
|
|
413
|
+
render(
|
|
414
|
+
<Switch
|
|
415
|
+
label="Disabled switch"
|
|
416
|
+
disabled
|
|
417
|
+
onCheckedChange={handleChange}
|
|
418
|
+
/>
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const switchControl = screen.getByRole('switch', { name: 'Disabled switch' });
|
|
422
|
+
await userEvent.click(switchControl);
|
|
423
|
+
|
|
424
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('switch works with keyboard', async () => {
|
|
428
|
+
const handleChange = vi.fn();
|
|
429
|
+
render(
|
|
430
|
+
<Switch
|
|
431
|
+
label="Keyboard test"
|
|
432
|
+
checked={false}
|
|
433
|
+
onCheckedChange={handleChange}
|
|
434
|
+
/>
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const switchControl = screen.getByRole('switch', { name: 'Keyboard test' });
|
|
438
|
+
switchControl.focus();
|
|
439
|
+
|
|
440
|
+
await userEvent.keyboard(' '); // Space key
|
|
441
|
+
|
|
442
|
+
expect(handleChange).toHaveBeenCalledWith({ checked: true });
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## When to Use Switch vs Checkbox
|
|
447
|
+
|
|
448
|
+
| Feature | Switch | Checkbox |
|
|
449
|
+
|---------|--------|----------|
|
|
450
|
+
| **Purpose** | Toggle state (on/off) | Select option(s) |
|
|
451
|
+
| **Effect** | Immediate | Usually requires submit |
|
|
452
|
+
| **State** | Active/inactive | Selected/unselected |
|
|
453
|
+
| **Typical Use** | Settings, preferences | Forms, multi-select |
|
|
454
|
+
| **Example** | "Enable dark mode" | "I agree to terms" |
|
|
455
|
+
| **Visual** | Track + thumb | Box + checkmark |
|
|
456
|
+
|
|
457
|
+
**Rule of thumb**: Use Switch when the change takes effect immediately. Use Checkbox when part of a form that needs submission.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Color Tokens
|
|
2
|
+
|
|
3
|
+
The design system uses Material Design 3 semantic color tokens. **Always use semantic tokens, never raw hex values.**
|
|
4
|
+
|
|
5
|
+
## Why Semantic Colors?
|
|
6
|
+
|
|
7
|
+
Semantic colors automatically adapt to light/dark themes and follow M3 color roles. Using semantic names ensures:
|
|
8
|
+
- Automatic theme switching
|
|
9
|
+
- Consistent contrast ratios
|
|
10
|
+
- Proper color relationships
|
|
11
|
+
- Accessibility compliance
|
|
12
|
+
|
|
13
|
+
## Semantic Colors Reference
|
|
14
|
+
|
|
15
|
+
### Primary Colors
|
|
16
|
+
Used for primary actions, key UI elements, and brand identity.
|
|
17
|
+
|
|
18
|
+
| Token | Light Mode | Dark Mode | Usage |
|
|
19
|
+
|-------|-----------|-----------|-------|
|
|
20
|
+
| `primary` | #4C662B | #B1D18A | Primary buttons, active states, links |
|
|
21
|
+
| `onPrimary` | #FFFFFF | #1F3701 | Text/icons on primary color |
|
|
22
|
+
| `primaryContainer` | #CDEDA3 | #354E16 | Containers for primary content |
|
|
23
|
+
| `onPrimaryContainer` | #354E16 | #CDEDA3 | Text/icons on primary container |
|
|
24
|
+
|
|
25
|
+
### Secondary Colors
|
|
26
|
+
Used for secondary actions and less prominent UI elements.
|
|
27
|
+
|
|
28
|
+
| Token | Light Mode | Dark Mode | Usage |
|
|
29
|
+
|-------|-----------|-----------|-------|
|
|
30
|
+
| `secondary` | #586249 | #BFCBAD | Secondary buttons, less prominent actions |
|
|
31
|
+
| `onSecondary` | #FFFFFF | #2A331E | Text/icons on secondary color |
|
|
32
|
+
| `secondaryContainer` | #DCE7C8 | #404A33 | Secondary containers |
|
|
33
|
+
| `onSecondaryContainer` | #404A33 | #DCE7C8 | Text/icons on secondary container |
|
|
34
|
+
|
|
35
|
+
### Tertiary Colors
|
|
36
|
+
Used for accent colors and tertiary actions.
|
|
37
|
+
|
|
38
|
+
| Token | Light Mode | Dark Mode | Usage |
|
|
39
|
+
|-------|-----------|-----------|-------|
|
|
40
|
+
| `tertiary` | #386663 | #A0D0CB | Accent colors, tertiary actions |
|
|
41
|
+
| `onTertiary` | #FFFFFF | #003735 | Text/icons on tertiary color |
|
|
42
|
+
| `tertiaryContainer` | #BCECE7 | #1F4E4B | Tertiary containers |
|
|
43
|
+
| `onTertiaryContainer` | #1F4E4B | #BCECE7 | Text/icons on tertiary container |
|
|
44
|
+
|
|
45
|
+
### Error Colors
|
|
46
|
+
Used for error states, warnings, and destructive actions.
|
|
47
|
+
|
|
48
|
+
| Token | Light Mode | Dark Mode | Usage |
|
|
49
|
+
|-------|-----------|-----------|-------|
|
|
50
|
+
| `error` | #BA1A1A | #FFB4AB | Error text, error icons |
|
|
51
|
+
| `onError` | #FFFFFF | #690005 | Text/icons on error color |
|
|
52
|
+
| `errorContainer` | #FFDAD6 | #93000A | Error message backgrounds |
|
|
53
|
+
| `onErrorContainer` | #93000A | #FFDAD6 | Text/icons in error containers |
|
|
54
|
+
|
|
55
|
+
### Surface Colors
|
|
56
|
+
Used for backgrounds and containers at different elevations.
|
|
57
|
+
|
|
58
|
+
| Token | Light Mode | Dark Mode | Usage |
|
|
59
|
+
|-------|-----------|-----------|-------|
|
|
60
|
+
| `surface` | #F9FAEF | #12140E | Default background |
|
|
61
|
+
| `onSurface` | #1A1C16 | #E2E3D8 | Default text color |
|
|
62
|
+
| `surfaceVariant` | #E1E4D5 | #44483D | Alternate surface (subtle contrast) |
|
|
63
|
+
| `onSurfaceVariant` | #44483D | #C5C8BA | Text on variant surfaces |
|
|
64
|
+
|
|
65
|
+
### Surface Container Elevations
|
|
66
|
+
M3 uses surface tints instead of shadows for elevation. Higher elevations get lighter in light mode, darker in dark mode.
|
|
67
|
+
|
|
68
|
+
| Token | Light Mode | Dark Mode | Usage |
|
|
69
|
+
|-------|-----------|-----------|-------|
|
|
70
|
+
| `surfaceContainerLowest` | #FFFFFF | #0C0F09 | Lowest elevation (0dp) |
|
|
71
|
+
| `surfaceContainerLow` | #F3F4E9 | #1A1C16 | Low elevation (1dp) - Cards |
|
|
72
|
+
| `surfaceContainer` | #EEEFE3 | #1E201A | Default containers (3dp) |
|
|
73
|
+
| `surfaceContainerHigh` | #E8E9DE | #282B24 | High elevation (4dp) - Dialogs |
|
|
74
|
+
| `surfaceContainerHighest` | #E2E3D8 | #33362E | Highest elevation (5dp) |
|
|
75
|
+
|
|
76
|
+
### Outline Colors
|
|
77
|
+
Used for borders and dividers.
|
|
78
|
+
|
|
79
|
+
| Token | Light Mode | Dark Mode | Usage |
|
|
80
|
+
|-------|-----------|-----------|-------|
|
|
81
|
+
| `outline` | #75796C | #8F9285 | Borders, dividers |
|
|
82
|
+
| `outlineVariant` | #C5C8BA | #44483D | Subtle borders |
|
|
83
|
+
|
|
84
|
+
### Inverse Colors
|
|
85
|
+
Used for inverse color schemes (e.g., snackbars).
|
|
86
|
+
|
|
87
|
+
| Token | Light Mode | Dark Mode | Usage |
|
|
88
|
+
|-------|-----------|-----------|-------|
|
|
89
|
+
| `inverseSurface` | #2F312A | #E2E3D8 | Inverse backgrounds |
|
|
90
|
+
| `inverseOnSurface` | #F1F2E6 | #2F312A | Text on inverse surfaces |
|
|
91
|
+
| `inversePrimary` | #B1D18A | #4C662B | Primary color on inverse |
|
|
92
|
+
|
|
93
|
+
### Background Colors
|
|
94
|
+
Used for page/app backgrounds.
|
|
95
|
+
|
|
96
|
+
| Token | Light Mode | Dark Mode | Usage |
|
|
97
|
+
|-------|-----------|-----------|-------|
|
|
98
|
+
| `background` | #F9FAEF | #12140E | Page background |
|
|
99
|
+
| `onBackground` | #1A1C16 | #E2E3D8 | Text on background |
|
|
100
|
+
|
|
101
|
+
### Special Colors
|
|
102
|
+
|
|
103
|
+
| Token | Value | Usage |
|
|
104
|
+
|-------|-------|-------|
|
|
105
|
+
| `scrim` | #000000 | Overlay behind modals/dialogs |
|
|
106
|
+
| `shadow` | #000000 | Shadow color (rarely used directly) |
|
|
107
|
+
|
|
108
|
+
## Usage in Code
|
|
109
|
+
|
|
110
|
+
### Using Semantic Tokens in Components
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { css } from '@discourser/design-system/styled-system/css';
|
|
114
|
+
|
|
115
|
+
// ✅ Correct - Use semantic tokens
|
|
116
|
+
const container = css({
|
|
117
|
+
bg: 'surfaceContainerLow',
|
|
118
|
+
color: 'onSurface',
|
|
119
|
+
borderColor: 'outline'
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const primaryAction = css({
|
|
123
|
+
bg: 'primary',
|
|
124
|
+
color: 'onPrimary'
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const errorMessage = css({
|
|
128
|
+
bg: 'errorContainer',
|
|
129
|
+
color: 'onErrorContainer'
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Common Patterns
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// Card backgrounds
|
|
137
|
+
bg: 'surfaceContainerLow' // For elevated cards
|
|
138
|
+
bg: 'surface' // For filled/outlined cards
|
|
139
|
+
|
|
140
|
+
// Text colors
|
|
141
|
+
color: 'onSurface' // Primary text
|
|
142
|
+
color: 'onSurfaceVariant' // Secondary text
|
|
143
|
+
|
|
144
|
+
// Borders
|
|
145
|
+
borderColor: 'outline' // Standard borders
|
|
146
|
+
borderColor: 'outlineVariant' // Subtle borders
|
|
147
|
+
|
|
148
|
+
// Interactive states
|
|
149
|
+
bg: 'primary' // Default
|
|
150
|
+
bg: 'primaryContainer' // Hover/focus
|
|
151
|
+
color: 'onPrimary' // Text on primary
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## What NOT to Do
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// ❌ NEVER use raw hex colors
|
|
158
|
+
const wrong = css({ bg: '#4C662B' });
|
|
159
|
+
|
|
160
|
+
// ❌ NEVER use RGB values
|
|
161
|
+
const wrong = css({ bg: 'rgb(76, 102, 43)' });
|
|
162
|
+
|
|
163
|
+
// ❌ NEVER hardcode light/dark colors
|
|
164
|
+
const wrong = css({ bg: mode === 'dark' ? '#000' : '#fff' });
|
|
165
|
+
|
|
166
|
+
// ✅ ALWAYS use semantic tokens
|
|
167
|
+
const correct = css({ bg: 'surface' });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Color Combinations
|
|
171
|
+
|
|
172
|
+
Always pair colors with their corresponding "on" color for proper contrast:
|
|
173
|
+
|
|
174
|
+
| Background | Text/Icon Color | Use Case |
|
|
175
|
+
|-----------|----------------|----------|
|
|
176
|
+
| `primary` | `onPrimary` | Filled buttons |
|
|
177
|
+
| `primaryContainer` | `onPrimaryContainer` | Tonal buttons, chips |
|
|
178
|
+
| `surface` | `onSurface` | Default backgrounds |
|
|
179
|
+
| `surfaceVariant` | `onSurfaceVariant` | Alternate surfaces |
|
|
180
|
+
| `error` | `onError` | Error badges |
|
|
181
|
+
| `errorContainer` | `onErrorContainer` | Error messages |
|
|
182
|
+
|
|
183
|
+
## Accessibility
|
|
184
|
+
|
|
185
|
+
All semantic color combinations meet WCAG 2.1 Level AA contrast requirements (4.5:1 for normal text, 3:1 for large text).
|
|
186
|
+
|
|
187
|
+
**Never override these combinations** unless you verify contrast ratios yourself.
|