@adea-ai/ui 0.71.1 → 0.71.3

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 (99) hide show
  1. package/dist/components/composites/appearance-editor/appearance-editor.js +1 -1
  2. package/dist/components/composites/appearance-editor/appearance-editor.js.map +1 -1
  3. package/dist/components/composites/stat/stat.d.ts.map +1 -1
  4. package/dist/components/composites/stat/stat.js +8 -8
  5. package/dist/components/composites/stat/stat.js.map +1 -1
  6. package/dist/components/composites/update-dialog/update-dialog.d.ts +7 -0
  7. package/dist/components/composites/update-dialog/update-dialog.d.ts.map +1 -1
  8. package/dist/components/composites/update-dialog/update-dialog.js +71 -26
  9. package/dist/components/composites/update-dialog/update-dialog.js.map +1 -1
  10. package/dist/components/conversation/internal/chat-composer-shell.js +1 -1
  11. package/dist/components/conversation/internal/chat-composer-shell.js.map +1 -1
  12. package/dist/components/conversation/message-composer.d.ts.map +1 -1
  13. package/dist/components/conversation/message-composer.js +1 -1
  14. package/dist/components/conversation/message-composer.js.map +1 -1
  15. package/dist/components/ui/alert/alert.d.ts +3 -1
  16. package/dist/components/ui/alert/alert.d.ts.map +1 -1
  17. package/dist/components/ui/alert/alert.js +18 -8
  18. package/dist/components/ui/alert/alert.js.map +1 -1
  19. package/dist/components/ui/badge/badge.js +4 -4
  20. package/dist/components/ui/badge/badge.js.map +1 -1
  21. package/dist/components/ui/context-menu/context-menu.d.ts.map +1 -1
  22. package/dist/components/ui/context-menu/context-menu.js +2 -2
  23. package/dist/components/ui/context-menu/context-menu.js.map +1 -1
  24. package/dist/components/ui/dialog/dialog.d.ts +17 -1
  25. package/dist/components/ui/dialog/dialog.d.ts.map +1 -1
  26. package/dist/components/ui/dialog/dialog.js +120 -2
  27. package/dist/components/ui/dialog/dialog.js.map +1 -1
  28. package/dist/components/ui/diff-block/diff-block.js +2 -2
  29. package/dist/components/ui/diff-block/diff-block.js.map +1 -1
  30. package/dist/components/ui/dropdown-menu/dropdown-menu.d.ts +3 -2
  31. package/dist/components/ui/dropdown-menu/dropdown-menu.d.ts.map +1 -1
  32. package/dist/components/ui/dropdown-menu/dropdown-menu.js +5 -4
  33. package/dist/components/ui/dropdown-menu/dropdown-menu.js.map +1 -1
  34. package/dist/components/ui/entity-icon/entity-icon.d.ts +3 -3
  35. package/dist/components/ui/entity-icon/entity-icon.js +7 -7
  36. package/dist/components/ui/entity-icon/entity-icon.js.map +1 -1
  37. package/dist/components/ui/field/field.js +1 -1
  38. package/dist/components/ui/field/field.js.map +1 -1
  39. package/dist/components/ui/menubar/menubar.d.ts.map +1 -1
  40. package/dist/components/ui/menubar/menubar.js +2 -2
  41. package/dist/components/ui/menubar/menubar.js.map +1 -1
  42. package/dist/components/ui/modal-dialog/modal-dialog.d.ts +12 -2
  43. package/dist/components/ui/modal-dialog/modal-dialog.d.ts.map +1 -1
  44. package/dist/components/ui/modal-dialog/modal-dialog.js +44 -27
  45. package/dist/components/ui/modal-dialog/modal-dialog.js.map +1 -1
  46. package/dist/components/ui/status-chip/status-chip.js +4 -4
  47. package/dist/components/ui/status-chip/status-chip.js.map +1 -1
  48. package/dist/lib/overlay.d.ts +2 -0
  49. package/dist/lib/overlay.d.ts.map +1 -1
  50. package/dist/lib/overlay.js +3 -1
  51. package/dist/lib/overlay.js.map +1 -1
  52. package/dist/lib/themes.d.ts +7 -0
  53. package/dist/lib/themes.d.ts.map +1 -1
  54. package/dist/lib/themes.js +22 -1
  55. package/dist/lib/themes.js.map +1 -1
  56. package/dist/lib/tokens.d.ts.map +1 -1
  57. package/dist/lib/tokens.js +4 -4
  58. package/dist/lib/tokens.js.map +1 -1
  59. package/dist/r/src/components/composites/appearance-editor/appearance-editor.tsx +1 -1
  60. package/dist/r/src/components/composites/stat/stat.tsx +5 -8
  61. package/dist/r/src/components/composites/update-dialog/update-dialog.tsx +98 -26
  62. package/dist/r/src/components/conversation/internal/chat-composer-shell.tsx +1 -1
  63. package/dist/r/src/components/conversation/message-composer.tsx +3 -1
  64. package/dist/r/src/components/ui/alert/alert.tsx +20 -7
  65. package/dist/r/src/components/ui/badge/badge.tsx +4 -4
  66. package/dist/r/src/components/ui/context-menu/context-menu.tsx +2 -2
  67. package/dist/r/src/components/ui/dialog/dialog.tsx +204 -2
  68. package/dist/r/src/components/ui/diff-block/diff-block.tsx +4 -4
  69. package/dist/r/src/components/ui/dropdown-menu/dropdown-menu.tsx +5 -4
  70. package/dist/r/src/components/ui/entity-icon/entity-icon.tsx +7 -7
  71. package/dist/r/src/components/ui/field/field.tsx +1 -1
  72. package/dist/r/src/components/ui/menubar/menubar.tsx +2 -2
  73. package/dist/r/src/components/ui/modal-dialog/modal-dialog.tsx +24 -2
  74. package/dist/r/src/components/ui/status-chip/status-chip.tsx +4 -4
  75. package/dist/r/src/lib/overlay.ts +4 -0
  76. package/dist/r/src/lib/themes.ts +29 -0
  77. package/dist/r/src/lib/tokens.ts +19 -4
  78. package/dist/r/src/styles/theme.css +10 -10
  79. package/package.json +2 -2
  80. package/src/components/composites/appearance-editor/appearance-editor.tsx +1 -1
  81. package/src/components/composites/stat/stat.tsx +5 -8
  82. package/src/components/composites/update-dialog/update-dialog.tsx +98 -26
  83. package/src/components/conversation/internal/chat-composer-shell.tsx +1 -1
  84. package/src/components/conversation/message-composer.tsx +3 -1
  85. package/src/components/ui/alert/alert.tsx +20 -7
  86. package/src/components/ui/badge/badge.tsx +4 -4
  87. package/src/components/ui/context-menu/context-menu.tsx +2 -2
  88. package/src/components/ui/dialog/dialog.tsx +204 -2
  89. package/src/components/ui/diff-block/diff-block.tsx +4 -4
  90. package/src/components/ui/dropdown-menu/dropdown-menu.tsx +5 -4
  91. package/src/components/ui/entity-icon/entity-icon.tsx +7 -7
  92. package/src/components/ui/field/field.tsx +1 -1
  93. package/src/components/ui/menubar/menubar.tsx +2 -2
  94. package/src/components/ui/modal-dialog/modal-dialog.tsx +24 -2
  95. package/src/components/ui/status-chip/status-chip.tsx +4 -4
  96. package/src/lib/overlay.ts +4 -0
  97. package/src/lib/themes.ts +29 -0
  98. package/src/lib/tokens.ts +19 -4
  99. package/src/styles/theme.css +10 -10
@@ -1,10 +1,212 @@
1
1
  import { Dialog as KobalteDialog } from '@kobalte/core/dialog'
2
2
  import { X } from 'lucide-solid'
3
- import type { ComponentProps, JSX } from 'solid-js'
4
- import { Show, splitProps } from 'solid-js'
3
+ import type { Accessor, ComponentProps, JSX } from 'solid-js'
4
+ import { createRenderEffect, onCleanup, Show, splitProps } from 'solid-js'
5
5
  import { overlayMotion, overlayPositioner, overlayScrim, overlaySurface } from '../../../lib/overlay'
6
6
  import { cn } from '../../../lib/utils'
7
7
 
8
+ type FocusCycle = {
9
+ content: HTMLElement
10
+ opener: HTMLElement | null
11
+ openerEligible: boolean
12
+ closed: boolean
13
+ focusMovedOutside: boolean
14
+ closeAutoFocusScheduled: boolean
15
+ frame?: number
16
+ fallbackTimer?: number
17
+ observer?: MutationObserver
18
+ onFocusIn: (event: FocusEvent) => void
19
+ }
20
+
21
+ function canRestoreFocus(target: HTMLElement): boolean {
22
+ return (
23
+ target.isConnected &&
24
+ target !== document.body &&
25
+ target !== document.documentElement &&
26
+ !target.closest('[inert], [aria-hidden="true"]') &&
27
+ !target.matches(':disabled') &&
28
+ target.getClientRects().length > 0
29
+ )
30
+ }
31
+
32
+ function hasOtherOverlay(content: HTMLElement, opener: HTMLElement): boolean {
33
+ return [...document.querySelectorAll<HTMLElement>('[role="dialog"], [role="alertdialog"]')].some(
34
+ (overlay) =>
35
+ overlay !== content &&
36
+ !content.contains(overlay) &&
37
+ !overlay.contains(opener) &&
38
+ !overlay.closest('[aria-hidden="true"], [inert]') &&
39
+ overlay.getClientRects().length > 0
40
+ )
41
+ }
42
+
43
+ type DialogFocusRestorationOptions = {
44
+ open: Accessor<boolean>
45
+ restoreFocusRef?: Accessor<HTMLElement | undefined>
46
+ onOpenAutoFocus?: ComponentProps<typeof KobalteDialog.Content>['onOpenAutoFocus']
47
+ onCloseAutoFocus?: ComponentProps<typeof KobalteDialog.Content>['onCloseAutoFocus']
48
+ }
49
+
50
+ /**
51
+ * Adds guarded opener capture and restoration to a Kobalte dialog content.
52
+ * Composed surfaces use this internal helper; it is omitted from the public
53
+ * dialog barrel so the restoration policy stays with the dialog components.
54
+ */
55
+ export function createDialogFocusRestoration({
56
+ open,
57
+ restoreFocusRef,
58
+ onOpenAutoFocus: callerOnOpenAutoFocus,
59
+ onCloseAutoFocus: callerOnCloseAutoFocus,
60
+ }: DialogFocusRestorationOptions) {
61
+ const focusCycles = new WeakMap<HTMLElement, FocusCycle>()
62
+ let activeFocusCycle: FocusCycle | undefined
63
+ let pendingOpener: HTMLElement | null = null
64
+ let pendingOpenerEligible = false
65
+
66
+ createRenderEffect(() => {
67
+ if (!open() || typeof document === 'undefined') {
68
+ pendingOpener = null
69
+ pendingOpenerEligible = false
70
+ return
71
+ }
72
+ const active = restoreFocusRef?.() ?? document.activeElement
73
+ pendingOpener = active instanceof HTMLElement ? active : null
74
+ pendingOpenerEligible = active instanceof HTMLElement && canRestoreFocus(active)
75
+ })
76
+
77
+ const finishFocusCycle = (cycle: FocusCycle) => {
78
+ if (cycle.frame !== undefined) window.cancelAnimationFrame(cycle.frame)
79
+ if (cycle.fallbackTimer !== undefined) window.clearTimeout(cycle.fallbackTimer)
80
+ cycle.observer?.disconnect()
81
+ document.removeEventListener('focusin', cycle.onFocusIn)
82
+ if (activeFocusCycle === cycle) activeFocusCycle = undefined
83
+ }
84
+
85
+ const scheduleFocusRestore = (cycle: FocusCycle) => {
86
+ if (cycle.frame !== undefined) return
87
+ cycle.frame = window.requestAnimationFrame(() => {
88
+ cycle.frame = undefined
89
+ const opener = cycle.opener
90
+ const active = document.activeElement
91
+ const focusIsAvailable =
92
+ active === document.body ||
93
+ active === document.documentElement ||
94
+ active === cycle.content ||
95
+ (active instanceof Node && cycle.content.contains(active))
96
+ if (
97
+ activeFocusCycle !== cycle ||
98
+ !cycle.closed ||
99
+ cycle.focusMovedOutside ||
100
+ !opener ||
101
+ !opener.isConnected ||
102
+ opener.matches(':disabled') ||
103
+ opener.getClientRects().length === 0 ||
104
+ !focusIsAvailable ||
105
+ hasOtherOverlay(cycle.content, opener)
106
+ ) {
107
+ finishFocusCycle(cycle)
108
+ return
109
+ }
110
+
111
+ // Inert and aria-hidden are removed by the dialog's containment cleanup.
112
+ // Wait for that cleanup before returning focus to the original control.
113
+ if (opener.closest('[inert], [aria-hidden="true"]')) return
114
+ if (cycle.openerEligible && canRestoreFocus(opener)) {
115
+ opener.focus({ preventScroll: true })
116
+ }
117
+ finishFocusCycle(cycle)
118
+ })
119
+ }
120
+
121
+ const onOpenAutoFocus = (event: Event) => {
122
+ const element = event.currentTarget
123
+ if (element instanceof HTMLElement) {
124
+ if (activeFocusCycle) finishFocusCycle(activeFocusCycle)
125
+
126
+ const openerWasCaptured = pendingOpener !== null
127
+ const opener = pendingOpener ?? document.activeElement
128
+ const openerEligible = openerWasCaptured
129
+ ? pendingOpenerEligible
130
+ : opener instanceof HTMLElement && canRestoreFocus(opener)
131
+ pendingOpener = null
132
+ pendingOpenerEligible = false
133
+ const cycle: FocusCycle = {
134
+ content: element,
135
+ opener: opener instanceof HTMLElement ? opener : null,
136
+ openerEligible,
137
+ closed: false,
138
+ focusMovedOutside: false,
139
+ closeAutoFocusScheduled: false,
140
+ onFocusIn: (focusEvent) => {
141
+ const target = focusEvent.target
142
+ if (
143
+ !(target instanceof HTMLElement) ||
144
+ target === document.body ||
145
+ target === document.documentElement ||
146
+ target === element ||
147
+ element.contains(target) ||
148
+ target === (opener instanceof HTMLElement ? opener : null)
149
+ ) {
150
+ return
151
+ }
152
+ cycle.focusMovedOutside = true
153
+ },
154
+ }
155
+ focusCycles.set(element, cycle)
156
+ activeFocusCycle = cycle
157
+ document.addEventListener('focusin', cycle.onFocusIn)
158
+ }
159
+ callerOnOpenAutoFocus?.(event)
160
+ }
161
+
162
+ const onCloseAutoFocus = (event: Event) => {
163
+ const element = event.currentTarget
164
+ const cycle = element instanceof HTMLElement ? focusCycles.get(element) : undefined
165
+ callerOnCloseAutoFocus?.(event)
166
+
167
+ if (!cycle) return
168
+ cycle.closed = true
169
+
170
+ // A caller that prevents Kobalte's close autofocus owns the focus decision.
171
+ if (event.defaultPrevented) {
172
+ finishFocusCycle(cycle)
173
+ return
174
+ }
175
+
176
+ if (cycle.fallbackTimer !== undefined) {
177
+ window.clearTimeout(cycle.fallbackTimer)
178
+ cycle.fallbackTimer = undefined
179
+ }
180
+ cycle.closeAutoFocusScheduled = true
181
+ cycle.observer = new MutationObserver(() => scheduleFocusRestore(cycle))
182
+ cycle.observer.observe(document.body, {
183
+ attributes: true,
184
+ attributeFilter: ['aria-hidden', 'inert'],
185
+ childList: true,
186
+ subtree: true,
187
+ })
188
+ scheduleFocusRestore(cycle)
189
+ }
190
+
191
+ onCleanup(() => {
192
+ const cycle = activeFocusCycle
193
+ if (!cycle) return
194
+ cycle.closed = true
195
+
196
+ // Kobalte dispatches unmount autofocus from a timer. This fallback only
197
+ // releases the scoped focus listener if the content did not dispatch it.
198
+ queueMicrotask(() => {
199
+ if (cycle.closeAutoFocusScheduled) return
200
+ cycle.fallbackTimer = window.setTimeout(() => {
201
+ cycle.fallbackTimer = undefined
202
+ if (!cycle.closeAutoFocusScheduled) finishFocusCycle(cycle)
203
+ }, 0)
204
+ })
205
+ })
206
+
207
+ return { onOpenAutoFocus, onCloseAutoFocus }
208
+ }
209
+
8
210
  /**
9
211
  * Dialog.
10
212
  *
@@ -205,8 +205,8 @@ export function DiffBlock(props: DiffBlockProps) {
205
205
  {/* The counts are text, so the summary survives greyscale and a reader
206
206
  who cannot separate the two line tones still gets the shape of it. */}
207
207
  <span class="shrink-0 font-mono text-2xs text-muted-foreground">
208
- <span class="text-success">+{stats().added}</span>{' '}
209
- <span class="text-destructive">−{stats().removed}</span>
208
+ <span class="text-foreground">+{stats().added}</span>{' '}
209
+ <span class="text-foreground">−{stats().removed}</span>
210
210
  </span>
211
211
  </span>
212
212
  {local.actions}
@@ -278,8 +278,8 @@ export function DiffSummary(props: { patch: string; class?: string; onSelect?: (
278
278
  >
279
279
  <span class="truncate text-foreground">{name()}</span>
280
280
  <span class="shrink-0 text-2xs">
281
- <span class="text-success">+{stats().added}</span>{' '}
282
- <span class="text-destructive">−{stats().removed}</span>
281
+ <span class="text-foreground">+{stats().added}</span>{' '}
282
+ <span class="text-foreground">−{stats().removed}</span>
283
283
  </span>
284
284
  </button>
285
285
  )
@@ -4,6 +4,7 @@ import type { ComponentProps } from 'solid-js'
4
4
  import { splitProps } from 'solid-js'
5
5
  import {
6
6
  menuContentPadding,
7
+ destructiveMenuItem,
7
8
  menuItem,
8
9
  menuLabel,
9
10
  menuSeparator,
@@ -25,8 +26,9 @@ import { cn } from '../../../lib/utils'
25
26
  * highlight, Escape to close and return focus — which is exactly what a
26
27
  * hand-rolled `div` with click handlers gets wrong.
27
28
  *
28
- * The `destructive` variant on an item is a *colour*, not a confirmation.
29
- * Pairing it with an AlertDialog is the caller's job.
29
+ * The `destructive` variant marks the row with a status-coloured edge and
30
+ * highlighted tint while keeping its label on the body foreground. Pairing it
31
+ * with an AlertDialog is the caller's job.
30
32
  */
31
33
  export function DropdownMenu(props: ComponentProps<typeof KobalteDropdownMenu>) {
32
34
  return <KobalteDropdownMenu {...props} />
@@ -83,8 +85,7 @@ export function DropdownMenuItem(
83
85
  class={cn(
84
86
  menuItem,
85
87
  {
86
- 'text-destructive data-[highlighted]:bg-destructive-subtle data-[highlighted]:text-destructive':
87
- local.variant === 'destructive',
88
+ [destructiveMenuItem]: local.variant === 'destructive',
88
89
  },
89
90
  local.class
90
91
  )}
@@ -17,9 +17,9 @@ import { cn } from '../../../lib/utils'
17
17
  * 1. **A glyph is always present.** A brand with no logo gets a monogram, not an
18
18
  * empty box. `fallback` takes the name and returns the glyph, so a caller can
19
19
  * choose initials, a domain letter, or a kind icon.
20
- * 2. **The tile owns its own contrast.** `tone` tints the background and the
21
- * glyph together, so a caller never picks a colour pair and never ships an
22
- * icon that disappears against its tile.
20
+ * 2. **The tile owns its own contrast.** `tone` tints the background while
21
+ * monogram text keeps the body foreground; vector glyphs retain the status
22
+ * colour. A caller never picks a colour pair or ships a glyph that disappears.
23
23
  * 3. **Size is on the control ladder**, not arbitrary. A tile in a rail and a
24
24
  * tile in a detail header are the same two or three sizes every other
25
25
  * interactive element uses.
@@ -51,10 +51,10 @@ export const entityIconVariants = cva(
51
51
  primary: 'border-transparent bg-primary-subtle text-primary',
52
52
  /** Bare: no fill and no edge, for a tile that already sits on a surface. */
53
53
  bare: 'border-transparent bg-transparent text-foreground',
54
- success: 'border-transparent bg-success-subtle text-success',
55
- warning: 'border-transparent bg-warning-subtle text-warning',
56
- danger: 'border-transparent bg-destructive-subtle text-destructive',
57
- info: 'border-transparent bg-info-subtle text-info',
54
+ success: 'border-transparent bg-success-subtle text-foreground [&_svg]:text-success',
55
+ warning: 'border-transparent bg-warning-subtle text-foreground [&_svg]:text-warning',
56
+ danger: 'border-transparent bg-destructive-subtle text-foreground [&_svg]:text-destructive',
57
+ info: 'border-transparent bg-info-subtle text-foreground [&_svg]:text-info',
58
58
  },
59
59
  },
60
60
  defaultVariants: { shape: 'rounded', size: 'md', tone: 'neutral' },
@@ -44,7 +44,7 @@ export function FieldDescription(props: ComponentProps<typeof KobalteTextField.D
44
44
  export function FieldError(props: ComponentProps<typeof KobalteTextField.ErrorMessage>) {
45
45
  const [local, rest] = splitProps(props, ['class'])
46
46
  return (
47
- <KobalteTextField.ErrorMessage class={cn('text-destructive text-sm', local.class)} {...rest} />
47
+ <KobalteTextField.ErrorMessage class={cn('text-foreground text-sm', local.class)} {...rest} />
48
48
  )
49
49
  }
50
50
 
@@ -4,6 +4,7 @@ import type { ComponentProps } from 'solid-js'
4
4
  import { splitProps } from 'solid-js'
5
5
  import {
6
6
  menuContentPadding,
7
+ destructiveMenuItem,
7
8
  menuItem,
8
9
  menuLabel,
9
10
  menuSeparator,
@@ -98,8 +99,7 @@ export function MenubarItem(
98
99
  class={cn(
99
100
  menuItem,
100
101
  {
101
- 'text-destructive data-[highlighted]:bg-destructive-subtle data-[highlighted]:text-destructive':
102
- local.variant === 'destructive',
102
+ [destructiveMenuItem]: local.variant === 'destructive',
103
103
  },
104
104
  local.class
105
105
  )}
@@ -1,7 +1,8 @@
1
- import type { ComponentProps, JSX } from 'solid-js'
1
+ import type { Accessor, ComponentProps, JSX } from 'solid-js'
2
2
  import { Show, createEffect, createSignal, onCleanup, splitProps } from 'solid-js'
3
3
  import { cn } from '../../../lib/utils'
4
4
  import {
5
+ createDialogFocusRestoration,
5
6
  Dialog,
6
7
  DialogContent,
7
8
  DialogDescription,
@@ -12,7 +13,7 @@ import {
12
13
  /**
13
14
  * ModalDialog.
14
15
  *
15
- * A dialog that owns three things `Dialog` leaves to its caller, each of which is a
16
+ * A dialog that owns four things `Dialog` leaves to its caller, each of which is a
16
17
  * real defect when it is missed:
17
18
  *
18
19
  * 1. **It unmounts while closed.** An always-mounted dialog that closes as a side
@@ -27,6 +28,11 @@ import {
27
28
  * from `title`, so its name does not depend on the title id registration effect.
28
29
  * Pass `aria-label` to use a different name while keeping the visible title;
29
30
  * pass `aria-labelledby` when another visible label should take precedence.
31
+ * 4. **It returns focus to the opener.** The opener is captured before the dialog
32
+ * autofocuses and restored after the owner closes, unless a close-autofocus
33
+ * handler takes control or focus has moved to another layer. For controlled
34
+ * dialogs without a composed trigger, pass `restoreFocusRef` when the opener
35
+ * cannot be inferred from the focused element.
30
36
  *
31
37
  * Use `Dialog` directly when you need its composition; use this when you want a
32
38
  * dialog that is correct by default.
@@ -50,6 +56,11 @@ export type ModalDialogProps = Omit<
50
56
  'aria-label'?: string
51
57
  /** Names the dialog from a visible element; ARIA gives it precedence over `aria-label`. */
52
58
  'aria-labelledby'?: string
59
+ /**
60
+ * Supplies the external control to focus when this controlled dialog closes.
61
+ * Defaults to the element focused immediately before the dialog opens.
62
+ */
63
+ restoreFocusRef?: Accessor<HTMLElement | undefined>
53
64
  children?: JSX.Element
54
65
  }
55
66
 
@@ -65,9 +76,18 @@ export function ModalDialog(props: ModalDialogProps) {
65
76
  'children',
66
77
  'aria-label',
67
78
  'aria-labelledby',
79
+ 'restoreFocusRef',
80
+ 'onOpenAutoFocus',
81
+ 'onCloseAutoFocus',
68
82
  ])
69
83
 
70
84
  const [content, setContent] = createSignal<HTMLElement>()
85
+ const focusRestoration = createDialogFocusRestoration({
86
+ open: () => local.open,
87
+ restoreFocusRef: local.restoreFocusRef,
88
+ onOpenAutoFocus: local.onOpenAutoFocus,
89
+ onCloseAutoFocus: local.onCloseAutoFocus,
90
+ })
71
91
 
72
92
  createEffect(() => {
73
93
  if (!local.open) return
@@ -100,6 +120,8 @@ export function ModalDialog(props: ModalDialogProps) {
100
120
  local['aria-labelledby'] ?? (local['aria-label'] !== undefined ? '' : undefined)
101
121
  }
102
122
  {...rest}
123
+ onOpenAutoFocus={focusRestoration.onOpenAutoFocus}
124
+ onCloseAutoFocus={focusRestoration.onCloseAutoFocus}
103
125
  >
104
126
  <DialogHeader>
105
127
  <DialogTitle class="flex items-center gap-2">
@@ -47,10 +47,10 @@ export const statusDotVariants = cva('size-2 shrink-0 rounded-full', {
47
47
 
48
48
  const labelTone: Record<StatusTone, string> = {
49
49
  neutral: 'text-foreground',
50
- success: 'text-success',
51
- warning: 'text-warning',
52
- danger: 'text-destructive',
53
- info: 'text-info',
50
+ success: 'text-foreground',
51
+ warning: 'text-foreground',
52
+ danger: 'text-foreground',
53
+ info: 'text-foreground',
54
54
  unknown: 'text-muted-foreground',
55
55
  }
56
56
 
@@ -59,6 +59,10 @@ export const menuItem = [
59
59
  '[&_svg:not([class*=text-])]:text-muted-foreground',
60
60
  ].join(' ')
61
61
 
62
+ /** A destructive action keeps readable text and marks its row with a status edge. */
63
+ export const destructiveMenuItem =
64
+ 'border-s-2 border-destructive ps-1.5 text-foreground data-[highlighted]:bg-destructive-subtle'
65
+
62
66
  /** The label above a group of menu rows. */
63
67
  export const menuLabel =
64
68
  'px-2 py-1.5 text-2xs font-medium tracking-wide text-muted-foreground uppercase'
@@ -40,6 +40,7 @@ import {
40
40
  getAccent,
41
41
  hasTheme,
42
42
  statusForeground,
43
+ tint,
43
44
  primaryHover,
44
45
  primarySubtleCss,
45
46
  syntaxRoles,
@@ -76,9 +77,16 @@ export type ThemeColors = {
76
77
  accentForeground: string
77
78
  destructive: string
78
79
  destructiveForeground: string
80
+ destructiveSubtle: string
79
81
  success: string
82
+ successForeground: string
83
+ successSubtle: string
80
84
  warning: string
85
+ warningForeground: string
86
+ warningSubtle: string
81
87
  info: string
88
+ infoForeground: string
89
+ infoSubtle: string
82
90
  border: string
83
91
  input: string
84
92
  ring: string
@@ -186,9 +194,16 @@ const ROLE_FOR: Readonly<Record<keyof ThemeColors, keyof AdeaThemeRecord['colors
186
194
  destructive: 'error',
187
195
  // Overwritten below from the measured pairing; nominal here so the table is total.
188
196
  destructiveForeground: 'error',
197
+ destructiveSubtle: 'error',
189
198
  success: 'success',
199
+ successForeground: 'success',
200
+ successSubtle: 'success',
190
201
  warning: 'warning',
202
+ warningForeground: 'warning',
203
+ warningSubtle: 'warning',
191
204
  info: 'info',
205
+ infoForeground: 'info',
206
+ infoSubtle: 'info',
192
207
  border: 'border',
193
208
  input: 'border',
194
209
  ring: 'accent',
@@ -208,6 +223,13 @@ function toVariant(theme: AdeaThemeRecord): ThemeVariant {
208
223
  // red it is black and on a deep red it is white, and using the body text would put
209
224
  // white on a mid-tone red at about 3:1.
210
225
  colors.destructiveForeground = statusForeground(theme, 'error')
226
+ colors.destructiveSubtle = tint(theme.colors.error, theme.colors.background)
227
+ colors.successForeground = statusForeground(theme, 'success')
228
+ colors.successSubtle = tint(theme.colors.success, theme.colors.background)
229
+ colors.warningForeground = statusForeground(theme, 'warning')
230
+ colors.warningSubtle = tint(theme.colors.warning, theme.colors.background)
231
+ colors.infoForeground = statusForeground(theme, 'info')
232
+ colors.infoSubtle = tint(theme.colors.info, theme.colors.background)
211
233
 
212
234
  const syntax = syntaxRoles(theme)
213
235
 
@@ -374,9 +396,16 @@ export function themeCssVariables(theme: ThemeVariant): Record<string, string> {
374
396
  '--accent-foreground': c.accentForeground,
375
397
  '--destructive': c.destructive,
376
398
  '--destructive-foreground': c.destructiveForeground,
399
+ '--destructive-subtle': c.destructiveSubtle,
377
400
  '--success': c.success,
401
+ '--success-foreground': c.successForeground,
402
+ '--success-subtle': c.successSubtle,
378
403
  '--warning': c.warning,
404
+ '--warning-foreground': c.warningForeground,
405
+ '--warning-subtle': c.warningSubtle,
379
406
  '--info': c.info,
407
+ '--info-foreground': c.infoForeground,
408
+ '--info-subtle': c.infoSubtle,
380
409
  '--border': c.border,
381
410
  '--input': c.input,
382
411
  '--ring': c.ring,
@@ -196,11 +196,16 @@ export const colorTokens: TokenDefinition[] = [
196
196
  name: 'destructive-subtle',
197
197
  kind: 'color',
198
198
  themed: true,
199
- description: 'Tinted destructive fill: alert banners, invalid fields.',
199
+ description: 'Opaque destructive tint against the canvas; subtle labels use foreground.',
200
200
  },
201
201
  { name: 'success', kind: 'color', themed: true, description: 'A completed or healthy state.' },
202
202
  { name: 'success-foreground', kind: 'color', themed: true, description: 'Label on `success`.' },
203
- { name: 'success-subtle', kind: 'color', themed: true, description: 'Tinted success fill.' },
203
+ {
204
+ name: 'success-subtle',
205
+ kind: 'color',
206
+ themed: true,
207
+ description: 'Opaque success tint against the canvas; subtle labels use foreground.',
208
+ },
204
209
  {
205
210
  name: 'warning',
206
211
  kind: 'color',
@@ -208,10 +213,20 @@ export const colorTokens: TokenDefinition[] = [
208
213
  description: 'Something needs attention but is not broken.',
209
214
  },
210
215
  { name: 'warning-foreground', kind: 'color', themed: true, description: 'Label on `warning`.' },
211
- { name: 'warning-subtle', kind: 'color', themed: true, description: 'Tinted warning fill.' },
216
+ {
217
+ name: 'warning-subtle',
218
+ kind: 'color',
219
+ themed: true,
220
+ description: 'Opaque warning tint against the canvas; subtle labels use foreground.',
221
+ },
212
222
  { name: 'info', kind: 'color', themed: true, description: 'Neutral information.' },
213
223
  { name: 'info-foreground', kind: 'color', themed: true, description: 'Label on `info`.' },
214
- { name: 'info-subtle', kind: 'color', themed: true, description: 'Tinted info fill.' },
224
+ {
225
+ name: 'info-subtle',
226
+ kind: 'color',
227
+ themed: true,
228
+ description: 'Opaque info tint against the canvas; subtle labels use foreground.',
229
+ },
215
230
 
216
231
  // Lines
217
232
  {
@@ -78,7 +78,7 @@
78
78
  --muted: oklch(0.898 0.0108 261.79);
79
79
  --muted-foreground: oklch(0.474 0.0245 265.01);
80
80
 
81
- /* Status. The fills are catalogue roles; the labels are measured. */
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
84
  --success: oklch(0.4311 0.1809 145.62);
@@ -87,10 +87,10 @@
87
87
  --warning-foreground: oklch(0.933 0.016 261.79);
88
88
  --info: oklch(0.4915 0.1155 202.14);
89
89
  --info-foreground: oklch(0.933 0.016 261.79);
90
- --destructive-subtle: color-mix(in oklch, var(--destructive) 16%, transparent);
91
- --success-subtle: color-mix(in oklch, var(--success) 16%, transparent);
92
- --warning-subtle: color-mix(in oklch, var(--warning) 16%, transparent);
93
- --info-subtle: color-mix(in oklch, var(--info) 16%, transparent);
90
+ --destructive-subtle: oklch(0.8683 0.0365 279.15);
91
+ --success-subtle: oklch(0.8627 0.0391 245.53);
92
+ --warning-subtle: oklch(0.8662 0.0334 286.73);
93
+ --info-subtle: oklch(0.8712 0.0299 253.44);
94
94
 
95
95
  /* Lines */
96
96
  --border: oklch(0.783 0.0144 261.79);
@@ -213,7 +213,7 @@
213
213
  --muted: oklch(0.2268 0.0288 265.37);
214
214
  --muted-foreground: oklch(0.6308 0.0224 262.91);
215
215
 
216
- /* Status. The fills are catalogue roles; the labels are measured. */
216
+ /* Status fills; solid labels and subtle surfaces come from the catalogue. */
217
217
  --destructive: oklch(0.7345 0.1626 25.78);
218
218
  --destructive-foreground: oklch(0.1918 0.0238 265.37);
219
219
  --success: oklch(0.6951 0.1809 145.62);
@@ -222,10 +222,10 @@
222
222
  --warning-foreground: oklch(0.1918 0.0238 265.37);
223
223
  --info: oklch(0.7555 0.1155 202.14);
224
224
  --info-foreground: oklch(0.1918 0.0238 265.37);
225
- --destructive-subtle: color-mix(in oklch, var(--destructive) 16%, transparent);
226
- --success-subtle: color-mix(in oklch, var(--success) 16%, transparent);
227
- --warning-subtle: color-mix(in oklch, var(--warning) 16%, transparent);
228
- --info-subtle: color-mix(in oklch, var(--info) 16%, transparent);
225
+ --destructive-subtle: oklch(0.2678 0.0432 282.23);
226
+ --success-subtle: oklch(0.2623 0.0458 248.61);
227
+ --warning-subtle: oklch(0.2657 0.0401 289.81);
228
+ --info-subtle: oklch(0.2707 0.0366 256.52);
229
229
 
230
230
  /* Lines */
231
231
  --border: oklch(0.3418 0.0214 265.37);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.71.1",
3
+ "version": "0.71.3",
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",
@@ -96,7 +96,7 @@
96
96
  "check:tree-shaking": "bun scripts/check-tree-shaking.ts",
97
97
  "theme:build": "bun scripts/build-default-theme.ts",
98
98
  "theme:check": "bun scripts/build-default-theme.ts --check",
99
- "check:packed-layout": "node scripts/check-packed-layout.mjs",
99
+ "check:packed-layout": "node --test scripts/check-packed-layout-process.test.mjs && node scripts/check-packed-layout.mjs",
100
100
  "check:packed-consumer": "bun scripts/check-packed-consumer.ts",
101
101
  "check:packed-conversation": "bun scripts/check-packed-conversation.ts",
102
102
  "check:packed-transcript": "bun scripts/check-packed-transcript.ts",
@@ -121,7 +121,7 @@ export function AppearanceEditor(props: AppearanceEditorProps) {
121
121
  onInput={(event) => props.onChange({ accent: event.currentTarget.value })}
122
122
  />
123
123
  <Show when={props.customAccentError}>
124
- <p id={errorId} role="alert" class="mt-1 text-xs text-destructive">
124
+ <p id={errorId} role="alert" class="mt-1 text-xs text-foreground">
125
125
  {props.customAccentError}
126
126
  </p>
127
127
  </Show>
@@ -59,18 +59,15 @@ export function Stat(props: StatProps) {
59
59
  </Show>
60
60
  </div>
61
61
  <Show when={local.delta}>
62
- <div
63
- class={cn('flex items-center gap-1 text-xs', {
64
- 'text-success': local.deltaTone === 'up',
65
- 'text-destructive': local.deltaTone === 'down',
66
- 'text-muted-foreground': local.deltaTone === undefined || local.deltaTone === 'neutral',
67
- })}
68
- >
62
+ <div class="text-foreground flex items-center gap-1 text-xs">
69
63
  <Show when={local.deltaTone === 'up' || local.deltaTone === 'down'}>
70
64
  <Dynamic
71
65
  component={local.deltaTone === 'down' ? ArrowDownRight : ArrowUpRight}
72
66
  aria-hidden="true"
73
- class="size-3"
67
+ class={cn('size-3', {
68
+ 'text-success': local.deltaTone === 'up',
69
+ 'text-destructive': local.deltaTone === 'down',
70
+ })}
74
71
  />
75
72
  </Show>
76
73
  <span class="tabular-nums">{local.delta}</span>