@adea-ai/ui 0.71.6 → 0.72.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.
Files changed (45) hide show
  1. package/dist/components/theme/theme-picker.d.ts.map +1 -1
  2. package/dist/components/theme/theme-picker.js +2 -2
  3. package/dist/components/theme/theme-picker.js.map +1 -1
  4. package/dist/components/ui/button/button.js +1 -1
  5. package/dist/components/ui/button/button.js.map +1 -1
  6. package/dist/components/ui/checkbox/checkbox.d.ts +2 -0
  7. package/dist/components/ui/checkbox/checkbox.d.ts.map +1 -1
  8. package/dist/components/ui/checkbox/checkbox.js +52 -13
  9. package/dist/components/ui/checkbox/checkbox.js.map +1 -1
  10. package/dist/components/ui/radio-group/radio-group.d.ts +2 -1
  11. package/dist/components/ui/radio-group/radio-group.d.ts.map +1 -1
  12. package/dist/components/ui/radio-group/radio-group.js +46 -13
  13. package/dist/components/ui/radio-group/radio-group.js.map +1 -1
  14. package/dist/components/ui/switch/switch.d.ts +2 -0
  15. package/dist/components/ui/switch/switch.d.ts.map +1 -1
  16. package/dist/components/ui/switch/switch.js +51 -12
  17. package/dist/components/ui/switch/switch.js.map +1 -1
  18. package/dist/lib/themes.d.ts +3 -0
  19. package/dist/lib/themes.d.ts.map +1 -1
  20. package/dist/lib/themes.js +6 -7
  21. package/dist/lib/themes.js.map +1 -1
  22. package/dist/lib/tokens.d.ts.map +1 -1
  23. package/dist/lib/tokens.js +14 -2
  24. package/dist/lib/tokens.js.map +1 -1
  25. package/dist/r/appearance-editor.json +1 -1
  26. package/dist/r/lib.json +1 -1
  27. package/dist/r/registry.json +2 -2
  28. package/dist/r/src/components/theme/theme-picker.tsx +5 -2
  29. package/dist/r/src/components/ui/button/button.tsx +1 -1
  30. package/dist/r/src/components/ui/checkbox/checkbox.tsx +34 -9
  31. package/dist/r/src/components/ui/radio-group/radio-group.tsx +22 -5
  32. package/dist/r/src/components/ui/switch/switch.tsx +34 -9
  33. package/dist/r/src/lib/themes.ts +18 -7
  34. package/dist/r/src/lib/tokens.ts +15 -2
  35. package/dist/r/src/styles/theme.css +6 -0
  36. package/package.json +2 -2
  37. package/registry.json +2 -2
  38. package/src/components/theme/theme-picker.tsx +5 -2
  39. package/src/components/ui/button/button.tsx +1 -1
  40. package/src/components/ui/checkbox/checkbox.tsx +34 -9
  41. package/src/components/ui/radio-group/radio-group.tsx +22 -5
  42. package/src/components/ui/switch/switch.tsx +34 -9
  43. package/src/lib/themes.ts +18 -7
  44. package/src/lib/tokens.ts +15 -2
  45. package/src/styles/theme.css +6 -0
@@ -21,6 +21,8 @@ export type SwitchProps = Omit<ComponentProps<typeof KobalteSwitch>, 'children'>
21
21
  thumbClass?: string
22
22
  labelClass?: string
23
23
  descriptionClass?: string
24
+ label?: JSX.Element
25
+ description?: JSX.Element
24
26
  /**
25
27
  * Replaces the control and label. Omit it to get the default control beside
26
28
  * `label` and `description`; supply it to compose the parts yourself.
@@ -36,16 +38,30 @@ export function Switch(props: SwitchProps) {
36
38
  'labelClass',
37
39
  'descriptionClass',
38
40
  'children',
41
+ 'label',
42
+ 'description',
39
43
  'aria-label',
40
44
  'aria-labelledby',
45
+ 'aria-describedby',
46
+ 'aria-invalid',
47
+ 'aria-busy',
48
+ 'title',
41
49
  ])
42
50
 
43
51
  return (
44
- <KobalteSwitch class={cn('group/switch flex items-center gap-2.5', local.class)} {...rest}>
52
+ <KobalteSwitch
53
+ title={local.title}
54
+ class={cn('group/switch flex items-center gap-2.5', local.class)}
55
+ {...rest}
56
+ >
45
57
  {/* The name belongs on the input; see Checkbox for why. */}
46
58
  <KobalteSwitch.Input
47
59
  aria-label={local['aria-label']}
48
60
  aria-labelledby={local['aria-labelledby']}
61
+ aria-describedby={local['aria-describedby']}
62
+ aria-invalid={local['aria-invalid']}
63
+ aria-busy={local['aria-busy']}
64
+ title={local.title}
49
65
  />
50
66
  <KobalteSwitch.Control
51
67
  class={cn(
@@ -67,14 +83,23 @@ export function Switch(props: SwitchProps) {
67
83
  )}
68
84
  />
69
85
  </KobalteSwitch.Control>
70
- {local.children ?? (
71
- <div class="grid gap-0.5">
72
- <KobalteSwitch.Label class={cn('text-sm leading-none font-medium', local.labelClass)} />
73
- <KobalteSwitch.Description
74
- class={cn('text-muted-foreground text-sm', local.descriptionClass)}
75
- />
76
- </div>
77
- )}
86
+ {local.children ??
87
+ (local.label || local.description ? (
88
+ <div class="grid gap-0.5">
89
+ {local.label ? (
90
+ <KobalteSwitch.Label class={cn('text-sm leading-none font-medium', local.labelClass)}>
91
+ {local.label}
92
+ </KobalteSwitch.Label>
93
+ ) : null}
94
+ {local.description ? (
95
+ <KobalteSwitch.Description
96
+ class={cn('text-muted-foreground text-sm', local.descriptionClass)}
97
+ >
98
+ {local.description}
99
+ </KobalteSwitch.Description>
100
+ ) : null}
101
+ </div>
102
+ ) : null)}
78
103
  </KobalteSwitch>
79
104
  )
80
105
  }
@@ -39,6 +39,7 @@ import {
39
39
  chartSeries,
40
40
  getAccent,
41
41
  hasTheme,
42
+ shadcnDestructiveProjection,
42
43
  statusForeground,
43
44
  tint,
44
45
  primaryHover,
@@ -78,6 +79,9 @@ export type ThemeColors = {
78
79
  destructive: string
79
80
  destructiveForeground: string
80
81
  destructiveSubtle: string
82
+ /** Contrast-safe solid-action presentation, derived from the canonical error role. */
83
+ destructiveAction: string
84
+ destructiveActionForeground: string
81
85
  success: string
82
86
  successForeground: string
83
87
  successSubtle: string
@@ -170,10 +174,12 @@ export type ThemeVariant = {
170
174
  /**
171
175
  * Canonical role → this system's `ThemeColors` key.
172
176
  *
173
- * Read by {@link toVariant}. Every key of `ThemeColors` must appear, so a role added
174
- * to this system fails the type check rather than silently resolving to `undefined`.
177
+ * Read by {@link toVariant}. The two derived destructive-action presentation values
178
+ * are intentionally excluded; the shared Themes adapter owns their color math.
175
179
  */
176
- const ROLE_FOR: Readonly<Record<keyof ThemeColors, keyof AdeaThemeRecord['colors']>> = {
180
+ type ThemeRole = Exclude<keyof ThemeColors, 'destructiveAction' | 'destructiveActionForeground'>
181
+
182
+ const ROLE_FOR: Readonly<Record<ThemeRole, keyof AdeaThemeRecord['colors']>> = {
177
183
  background: 'background',
178
184
  foreground: 'text',
179
185
  card: 'surface',
@@ -216,14 +222,17 @@ const ROLE_FOR: Readonly<Record<keyof ThemeColors, keyof AdeaThemeRecord['colors
216
222
  /** Builds the design system's view of one catalogue entry. */
217
223
  function toVariant(theme: AdeaThemeRecord): ThemeVariant {
218
224
  const colors = {} as ThemeColors
219
- for (const key of Object.keys(ROLE_FOR) as (keyof ThemeColors)[]) {
225
+ for (const key of Object.keys(ROLE_FOR) as ThemeRole[]) {
220
226
  colors[key] = theme.colors[ROLE_FOR[key]]
221
227
  }
222
- // The label on a solid destructive fill is measured rather than mapped: on a bright
223
- // red it is black and on a deep red it is white, and using the body text would put
224
- // white on a mid-tone red at about 3:1.
228
+ // Keep the semantic foreground paired with the canonical error role. Filled
229
+ // destructive actions use the distinct projection below so their /90 hover state
230
+ // remains readable without changing icons, borders, or tinted status surfaces.
225
231
  colors.destructiveForeground = statusForeground(theme, 'error')
226
232
  colors.destructiveSubtle = tint(theme.colors.error, theme.colors.background)
233
+ const destructiveAction = shadcnDestructiveProjection(theme)
234
+ colors.destructiveAction = destructiveAction.fill
235
+ colors.destructiveActionForeground = destructiveAction.foreground
227
236
  colors.successForeground = statusForeground(theme, 'success')
228
237
  colors.successSubtle = tint(theme.colors.success, theme.colors.background)
229
238
  colors.warningForeground = statusForeground(theme, 'warning')
@@ -397,6 +406,8 @@ export function themeCssVariables(theme: ThemeVariant): Record<string, string> {
397
406
  '--destructive': c.destructive,
398
407
  '--destructive-foreground': c.destructiveForeground,
399
408
  '--destructive-subtle': c.destructiveSubtle,
409
+ '--destructive-action': c.destructiveAction,
410
+ '--destructive-action-foreground': c.destructiveActionForeground,
400
411
  '--success': c.success,
401
412
  '--success-foreground': c.successForeground,
402
413
  '--success-subtle': c.successSubtle,
@@ -184,13 +184,14 @@ export const colorTokens: TokenDefinition[] = [
184
184
  name: 'destructive',
185
185
  kind: 'color',
186
186
  themed: true,
187
- description: 'Irreversible or dangerous actions.',
187
+ description: 'The canonical error hue for semantic status indicators and tinted notices.',
188
188
  },
189
189
  {
190
190
  name: 'destructive-foreground',
191
191
  kind: 'color',
192
192
  themed: true,
193
- description: 'Label on `destructive`.',
193
+ description:
194
+ 'Text on the canonical error hue; solid actions use `destructive-action-foreground`.',
194
195
  },
195
196
  {
196
197
  name: 'destructive-subtle',
@@ -198,6 +199,18 @@ export const colorTokens: TokenDefinition[] = [
198
199
  themed: true,
199
200
  description: 'Opaque destructive tint against the canvas; subtle labels use foreground.',
200
201
  },
202
+ {
203
+ name: 'destructive-action',
204
+ kind: 'color',
205
+ themed: true,
206
+ description: 'Contrast-safe solid fill for destructive actions, derived from the error role.',
207
+ },
208
+ {
209
+ name: 'destructive-action-foreground',
210
+ kind: 'color',
211
+ themed: true,
212
+ description: 'Text on the destructive action fill and its 90% hover composition.',
213
+ },
201
214
  { name: 'success', kind: 'color', themed: true, description: 'A completed or healthy state.' },
202
215
  { name: 'success-foreground', kind: 'color', themed: true, description: 'Label on `success`.' },
203
216
  {
@@ -81,6 +81,8 @@
81
81
  /* Status fills; solid labels and subtle surfaces come from the catalogue. */
82
82
  --destructive: oklch(0.4705 0.1626 25.78);
83
83
  --destructive-foreground: oklch(0.933 0.016 261.79);
84
+ --destructive-action: oklch(0.4705 0.1626 25.78);
85
+ --destructive-action-foreground: oklch(0.933 0.016 261.79);
84
86
  --success: oklch(0.4311 0.1809 145.62);
85
87
  --success-foreground: oklch(0.933 0.016 261.79);
86
88
  --warning: oklch(0.4556 0.1401 79.91);
@@ -216,6 +218,8 @@
216
218
  /* Status fills; solid labels and subtle surfaces come from the catalogue. */
217
219
  --destructive: oklch(0.7345 0.1626 25.78);
218
220
  --destructive-foreground: oklch(0.1918 0.0238 265.37);
221
+ --destructive-action: oklch(0.7345 0.1626 25.78);
222
+ --destructive-action-foreground: oklch(0.1918 0.0238 265.37);
219
223
  --success: oklch(0.6951 0.1809 145.62);
220
224
  --success-foreground: oklch(0.1918 0.0238 265.37);
221
225
  --warning: oklch(0.7196 0.1401 79.91);
@@ -730,6 +734,8 @@
730
734
  --color-destructive: var(--destructive);
731
735
  --color-destructive-foreground: var(--destructive-foreground);
732
736
  --color-destructive-subtle: var(--destructive-subtle);
737
+ --color-destructive-action: var(--destructive-action);
738
+ --color-destructive-action-foreground: var(--destructive-action-foreground);
733
739
  --color-success: var(--success);
734
740
  --color-success-foreground: var(--success-foreground);
735
741
  --color-success-subtle: var(--success-subtle);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.71.6",
3
+ "version": "0.72.1",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
@@ -106,7 +106,7 @@
106
106
  "check:packed-paste-model": "node scripts/check-packed-paste-model.mjs"
107
107
  },
108
108
  "dependencies": {
109
- "@adea-ai/themes": "^0.6.1",
109
+ "@adea-ai/themes": "^0.6.2",
110
110
  "@corvu/calendar": "^0.1.2",
111
111
  "@corvu/drawer": "^0.2.4",
112
112
  "@corvu/otp-field": "^0.1.4",
package/registry.json CHANGED
@@ -1670,7 +1670,7 @@
1670
1670
  "title": "Appearance Editor",
1671
1671
  "description": "",
1672
1672
  "dependencies": [
1673
- "@adea-ai/themes@^0.6.1",
1673
+ "@adea-ai/themes@^0.6.2",
1674
1674
  "@kobalte/core@^0.13.14",
1675
1675
  "lucide-solid@^1.48.0"
1676
1676
  ],
@@ -2064,7 +2064,7 @@
2064
2064
  "title": "Class-name and token helpers",
2065
2065
  "description": "The private helpers the components import: the class-name merge, the variant recipes, the token manifest and the theme bridge. Installed automatically as a peer of any component that needs it.",
2066
2066
  "dependencies": [
2067
- "@adea-ai/themes@^0.6.1",
2067
+ "@adea-ai/themes@^0.6.2",
2068
2068
  "class-variance-authority@^0.7.1",
2069
2069
  "clsx@^2.1.1",
2070
2070
  "tailwind-merge@^3.7.0"
@@ -47,7 +47,7 @@ export function ThemeSwatch(props: ThemeSwatchProps) {
47
47
  colors().card,
48
48
  colors().primary,
49
49
  colors().accent,
50
- colors().destructive,
50
+ colors().destructiveAction,
51
51
  ]}
52
52
  >
53
53
  {(color) => (
@@ -109,7 +109,10 @@ export function ThemePreview(props: ThemePreviewProps) {
109
109
  <div class="h-1 w-16 rounded-full" style={{ 'background-color': c().mutedForeground }} />
110
110
  <div class="mt-auto flex items-center gap-1">
111
111
  <span class="h-3 w-9 rounded-sm" style={{ 'background-color': c().primary }} />
112
- <span class="h-3 w-6 rounded-sm" style={{ 'background-color': c().destructive }} />
112
+ <span
113
+ class="h-3 w-6 rounded-sm"
114
+ style={{ 'background-color': c().destructiveAction }}
115
+ />
113
116
  <span class="h-3 w-6 rounded-sm" style={{ 'background-color': c().secondary }} />
114
117
  </div>
115
118
  </div>
@@ -29,7 +29,7 @@ export const buttonVariants = cva(
29
29
  secondary: 'bg-secondary text-secondary-foreground hover:bg-surface-hover',
30
30
  /** For the action that destroys something. Never the default. */
31
31
  destructive:
32
- 'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive-subtle',
32
+ 'bg-destructive-action text-destructive-action-foreground hover:bg-destructive-action/90 focus-visible:ring-destructive-subtle',
33
33
  /** A bordered button for a surface that already has a fill. */
34
34
  outline:
35
35
  'border border-border bg-transparent text-foreground hover:bg-surface-hover hover:border-input',
@@ -21,6 +21,8 @@ export type CheckboxProps = Omit<ComponentProps<typeof KobalteCheckbox>, 'childr
21
21
  controlClass?: string
22
22
  labelClass?: string
23
23
  descriptionClass?: string
24
+ label?: JSX.Element
25
+ description?: JSX.Element
24
26
  /**
25
27
  * Replaces the control and label. Omit it to get the default control beside
26
28
  * `label` and `description`; supply it to compose the parts yourself.
@@ -35,13 +37,23 @@ export function Checkbox(props: CheckboxProps) {
35
37
  'labelClass',
36
38
  'descriptionClass',
37
39
  'children',
40
+ 'label',
41
+ 'description',
38
42
  'indeterminate',
39
43
  'aria-label',
40
44
  'aria-labelledby',
45
+ 'aria-describedby',
46
+ 'aria-invalid',
47
+ 'aria-busy',
48
+ 'title',
41
49
  ])
42
50
 
43
51
  return (
44
- <KobalteCheckbox class={cn('group/checkbox flex items-start gap-2.5', local.class)} {...rest}>
52
+ <KobalteCheckbox
53
+ title={local.title}
54
+ class={cn('group/checkbox flex items-start gap-2.5', local.class)}
55
+ {...rest}
56
+ >
45
57
  {/*
46
58
  * The name goes on the input, which is what carries the value and takes
47
59
  * focus. On the Root it would land on a plain div and name nothing — the
@@ -50,6 +62,10 @@ export function Checkbox(props: CheckboxProps) {
50
62
  <KobalteCheckbox.Input
51
63
  aria-label={local['aria-label']}
52
64
  aria-labelledby={local['aria-labelledby']}
65
+ aria-describedby={local['aria-describedby']}
66
+ aria-invalid={local['aria-invalid']}
67
+ aria-busy={local['aria-busy']}
68
+ title={local.title}
53
69
  />
54
70
  <KobalteCheckbox.Control
55
71
  class={cn(
@@ -67,14 +83,23 @@ export function Checkbox(props: CheckboxProps) {
67
83
  {local.indeterminate ? <Minus class="size-3" /> : <Check class="size-3" />}
68
84
  </KobalteCheckbox.Indicator>
69
85
  </KobalteCheckbox.Control>
70
- {local.children ?? (
71
- <div class="grid gap-0.5 leading-none">
72
- <KobalteCheckbox.Label class={cn('text-sm font-medium', local.labelClass)} />
73
- <KobalteCheckbox.Description
74
- class={cn('text-muted-foreground text-sm', local.descriptionClass)}
75
- />
76
- </div>
77
- )}
86
+ {local.children ??
87
+ (local.label || local.description ? (
88
+ <div class="grid gap-0.5">
89
+ {local.label ? (
90
+ <KobalteCheckbox.Label class={cn('text-sm font-medium', local.labelClass)}>
91
+ {local.label}
92
+ </KobalteCheckbox.Label>
93
+ ) : null}
94
+ {local.description ? (
95
+ <KobalteCheckbox.Description
96
+ class={cn('text-muted-foreground text-sm', local.descriptionClass)}
97
+ >
98
+ {local.description}
99
+ </KobalteCheckbox.Description>
100
+ ) : null}
101
+ </div>
102
+ ) : null)}
78
103
  </KobalteCheckbox>
79
104
  )
80
105
  }
@@ -25,7 +25,8 @@ export type RadioGroupItemProps = ComponentProps<typeof KobalteRadioGroup.Item>
25
25
  controlClass?: string
26
26
  /**
27
27
  * The option's visible label. A radio with no accessible name is unreachable
28
- * by a screen reader and by voice control, so supply this or a child.
28
+ * by a screen reader and by voice control, so supply this, a labelled child,
29
+ * `aria-label`, or `aria-labelledby`.
29
30
  */
30
31
  label?: string
31
32
  description?: string
@@ -38,14 +39,28 @@ export function RadioGroupItem(props: RadioGroupItemProps) {
38
39
  'label',
39
40
  'description',
40
41
  'children',
42
+ 'aria-label',
43
+ 'aria-labelledby',
44
+ 'aria-describedby',
45
+ 'aria-invalid',
46
+ 'aria-busy',
47
+ 'title',
41
48
  ])
42
49
 
43
50
  return (
44
51
  <KobalteRadioGroup.Item
52
+ title={local.title}
45
53
  class={cn('group/radio flex items-start gap-2.5', local.class)}
46
54
  {...rest}
47
55
  >
48
- <KobalteRadioGroup.ItemInput />
56
+ <KobalteRadioGroup.ItemInput
57
+ aria-label={local['aria-label']}
58
+ aria-labelledby={local['aria-labelledby']}
59
+ aria-describedby={local['aria-describedby']}
60
+ aria-invalid={local['aria-invalid']}
61
+ aria-busy={local['aria-busy']}
62
+ title={local.title}
63
+ />
49
64
  <KobalteRadioGroup.ItemControl
50
65
  class={cn(
51
66
  'border-input bg-transparent flex size-4 shrink-0 items-center justify-center rounded-full border',
@@ -63,9 +78,11 @@ export function RadioGroupItem(props: RadioGroupItemProps) {
63
78
  </KobalteRadioGroup.ItemControl>
64
79
  {local.children ?? (
65
80
  <div class="grid gap-0.5 leading-none">
66
- <KobalteRadioGroup.ItemLabel class="text-sm font-medium">
67
- {local.label}
68
- </KobalteRadioGroup.ItemLabel>
81
+ {local.label ? (
82
+ <KobalteRadioGroup.ItemLabel class="text-sm font-medium">
83
+ {local.label}
84
+ </KobalteRadioGroup.ItemLabel>
85
+ ) : null}
69
86
  {local.description ? (
70
87
  <KobalteRadioGroup.ItemDescription class="text-muted-foreground text-sm">
71
88
  {local.description}
@@ -21,6 +21,8 @@ export type SwitchProps = Omit<ComponentProps<typeof KobalteSwitch>, 'children'>
21
21
  thumbClass?: string
22
22
  labelClass?: string
23
23
  descriptionClass?: string
24
+ label?: JSX.Element
25
+ description?: JSX.Element
24
26
  /**
25
27
  * Replaces the control and label. Omit it to get the default control beside
26
28
  * `label` and `description`; supply it to compose the parts yourself.
@@ -36,16 +38,30 @@ export function Switch(props: SwitchProps) {
36
38
  'labelClass',
37
39
  'descriptionClass',
38
40
  'children',
41
+ 'label',
42
+ 'description',
39
43
  'aria-label',
40
44
  'aria-labelledby',
45
+ 'aria-describedby',
46
+ 'aria-invalid',
47
+ 'aria-busy',
48
+ 'title',
41
49
  ])
42
50
 
43
51
  return (
44
- <KobalteSwitch class={cn('group/switch flex items-center gap-2.5', local.class)} {...rest}>
52
+ <KobalteSwitch
53
+ title={local.title}
54
+ class={cn('group/switch flex items-center gap-2.5', local.class)}
55
+ {...rest}
56
+ >
45
57
  {/* The name belongs on the input; see Checkbox for why. */}
46
58
  <KobalteSwitch.Input
47
59
  aria-label={local['aria-label']}
48
60
  aria-labelledby={local['aria-labelledby']}
61
+ aria-describedby={local['aria-describedby']}
62
+ aria-invalid={local['aria-invalid']}
63
+ aria-busy={local['aria-busy']}
64
+ title={local.title}
49
65
  />
50
66
  <KobalteSwitch.Control
51
67
  class={cn(
@@ -67,14 +83,23 @@ export function Switch(props: SwitchProps) {
67
83
  )}
68
84
  />
69
85
  </KobalteSwitch.Control>
70
- {local.children ?? (
71
- <div class="grid gap-0.5">
72
- <KobalteSwitch.Label class={cn('text-sm leading-none font-medium', local.labelClass)} />
73
- <KobalteSwitch.Description
74
- class={cn('text-muted-foreground text-sm', local.descriptionClass)}
75
- />
76
- </div>
77
- )}
86
+ {local.children ??
87
+ (local.label || local.description ? (
88
+ <div class="grid gap-0.5">
89
+ {local.label ? (
90
+ <KobalteSwitch.Label class={cn('text-sm leading-none font-medium', local.labelClass)}>
91
+ {local.label}
92
+ </KobalteSwitch.Label>
93
+ ) : null}
94
+ {local.description ? (
95
+ <KobalteSwitch.Description
96
+ class={cn('text-muted-foreground text-sm', local.descriptionClass)}
97
+ >
98
+ {local.description}
99
+ </KobalteSwitch.Description>
100
+ ) : null}
101
+ </div>
102
+ ) : null)}
78
103
  </KobalteSwitch>
79
104
  )
80
105
  }
package/src/lib/themes.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  chartSeries,
40
40
  getAccent,
41
41
  hasTheme,
42
+ shadcnDestructiveProjection,
42
43
  statusForeground,
43
44
  tint,
44
45
  primaryHover,
@@ -78,6 +79,9 @@ export type ThemeColors = {
78
79
  destructive: string
79
80
  destructiveForeground: string
80
81
  destructiveSubtle: string
82
+ /** Contrast-safe solid-action presentation, derived from the canonical error role. */
83
+ destructiveAction: string
84
+ destructiveActionForeground: string
81
85
  success: string
82
86
  successForeground: string
83
87
  successSubtle: string
@@ -170,10 +174,12 @@ export type ThemeVariant = {
170
174
  /**
171
175
  * Canonical role → this system's `ThemeColors` key.
172
176
  *
173
- * Read by {@link toVariant}. Every key of `ThemeColors` must appear, so a role added
174
- * to this system fails the type check rather than silently resolving to `undefined`.
177
+ * Read by {@link toVariant}. The two derived destructive-action presentation values
178
+ * are intentionally excluded; the shared Themes adapter owns their color math.
175
179
  */
176
- const ROLE_FOR: Readonly<Record<keyof ThemeColors, keyof AdeaThemeRecord['colors']>> = {
180
+ type ThemeRole = Exclude<keyof ThemeColors, 'destructiveAction' | 'destructiveActionForeground'>
181
+
182
+ const ROLE_FOR: Readonly<Record<ThemeRole, keyof AdeaThemeRecord['colors']>> = {
177
183
  background: 'background',
178
184
  foreground: 'text',
179
185
  card: 'surface',
@@ -216,14 +222,17 @@ const ROLE_FOR: Readonly<Record<keyof ThemeColors, keyof AdeaThemeRecord['colors
216
222
  /** Builds the design system's view of one catalogue entry. */
217
223
  function toVariant(theme: AdeaThemeRecord): ThemeVariant {
218
224
  const colors = {} as ThemeColors
219
- for (const key of Object.keys(ROLE_FOR) as (keyof ThemeColors)[]) {
225
+ for (const key of Object.keys(ROLE_FOR) as ThemeRole[]) {
220
226
  colors[key] = theme.colors[ROLE_FOR[key]]
221
227
  }
222
- // The label on a solid destructive fill is measured rather than mapped: on a bright
223
- // red it is black and on a deep red it is white, and using the body text would put
224
- // white on a mid-tone red at about 3:1.
228
+ // Keep the semantic foreground paired with the canonical error role. Filled
229
+ // destructive actions use the distinct projection below so their /90 hover state
230
+ // remains readable without changing icons, borders, or tinted status surfaces.
225
231
  colors.destructiveForeground = statusForeground(theme, 'error')
226
232
  colors.destructiveSubtle = tint(theme.colors.error, theme.colors.background)
233
+ const destructiveAction = shadcnDestructiveProjection(theme)
234
+ colors.destructiveAction = destructiveAction.fill
235
+ colors.destructiveActionForeground = destructiveAction.foreground
227
236
  colors.successForeground = statusForeground(theme, 'success')
228
237
  colors.successSubtle = tint(theme.colors.success, theme.colors.background)
229
238
  colors.warningForeground = statusForeground(theme, 'warning')
@@ -397,6 +406,8 @@ export function themeCssVariables(theme: ThemeVariant): Record<string, string> {
397
406
  '--destructive': c.destructive,
398
407
  '--destructive-foreground': c.destructiveForeground,
399
408
  '--destructive-subtle': c.destructiveSubtle,
409
+ '--destructive-action': c.destructiveAction,
410
+ '--destructive-action-foreground': c.destructiveActionForeground,
400
411
  '--success': c.success,
401
412
  '--success-foreground': c.successForeground,
402
413
  '--success-subtle': c.successSubtle,
package/src/lib/tokens.ts CHANGED
@@ -184,13 +184,14 @@ export const colorTokens: TokenDefinition[] = [
184
184
  name: 'destructive',
185
185
  kind: 'color',
186
186
  themed: true,
187
- description: 'Irreversible or dangerous actions.',
187
+ description: 'The canonical error hue for semantic status indicators and tinted notices.',
188
188
  },
189
189
  {
190
190
  name: 'destructive-foreground',
191
191
  kind: 'color',
192
192
  themed: true,
193
- description: 'Label on `destructive`.',
193
+ description:
194
+ 'Text on the canonical error hue; solid actions use `destructive-action-foreground`.',
194
195
  },
195
196
  {
196
197
  name: 'destructive-subtle',
@@ -198,6 +199,18 @@ export const colorTokens: TokenDefinition[] = [
198
199
  themed: true,
199
200
  description: 'Opaque destructive tint against the canvas; subtle labels use foreground.',
200
201
  },
202
+ {
203
+ name: 'destructive-action',
204
+ kind: 'color',
205
+ themed: true,
206
+ description: 'Contrast-safe solid fill for destructive actions, derived from the error role.',
207
+ },
208
+ {
209
+ name: 'destructive-action-foreground',
210
+ kind: 'color',
211
+ themed: true,
212
+ description: 'Text on the destructive action fill and its 90% hover composition.',
213
+ },
201
214
  { name: 'success', kind: 'color', themed: true, description: 'A completed or healthy state.' },
202
215
  { name: 'success-foreground', kind: 'color', themed: true, description: 'Label on `success`.' },
203
216
  {
@@ -81,6 +81,8 @@
81
81
  /* Status fills; solid labels and subtle surfaces come from the catalogue. */
82
82
  --destructive: oklch(0.4705 0.1626 25.78);
83
83
  --destructive-foreground: oklch(0.933 0.016 261.79);
84
+ --destructive-action: oklch(0.4705 0.1626 25.78);
85
+ --destructive-action-foreground: oklch(0.933 0.016 261.79);
84
86
  --success: oklch(0.4311 0.1809 145.62);
85
87
  --success-foreground: oklch(0.933 0.016 261.79);
86
88
  --warning: oklch(0.4556 0.1401 79.91);
@@ -216,6 +218,8 @@
216
218
  /* Status fills; solid labels and subtle surfaces come from the catalogue. */
217
219
  --destructive: oklch(0.7345 0.1626 25.78);
218
220
  --destructive-foreground: oklch(0.1918 0.0238 265.37);
221
+ --destructive-action: oklch(0.7345 0.1626 25.78);
222
+ --destructive-action-foreground: oklch(0.1918 0.0238 265.37);
219
223
  --success: oklch(0.6951 0.1809 145.62);
220
224
  --success-foreground: oklch(0.1918 0.0238 265.37);
221
225
  --warning: oklch(0.7196 0.1401 79.91);
@@ -730,6 +734,8 @@
730
734
  --color-destructive: var(--destructive);
731
735
  --color-destructive-foreground: var(--destructive-foreground);
732
736
  --color-destructive-subtle: var(--destructive-subtle);
737
+ --color-destructive-action: var(--destructive-action);
738
+ --color-destructive-action-foreground: var(--destructive-action-foreground);
733
739
  --color-success: var(--success);
734
740
  --color-success-foreground: var(--success-foreground);
735
741
  --color-success-subtle: var(--success-subtle);