@softize/opus 12.11.0 → 13.0.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bin/lib/check.mjs +2 -7
  3. package/bin/lib/copy.mjs +1 -5
  4. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +93 -10
  5. package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
  6. package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
  7. package/docs/radius-scale.md +1 -1
  8. package/package.json +1 -1
  9. package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
  10. package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
  11. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
  12. package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
  13. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
  14. package/src/ui/components/patterns/confirm.tsx +140 -40
  15. package/src/ui/components/patterns/list.tsx +35 -40
  16. package/src/ui/components/patterns/page-state.tsx +2 -2
  17. package/src/ui/components/patterns/sidebar.tsx +26 -26
  18. package/src/ui/components/patterns/trigger.tsx +25 -22
  19. package/src/ui/components/primitives/alert.tsx +3 -3
  20. package/src/ui/components/primitives/dialog.tsx +196 -39
  21. package/src/ui/components/primitives/drawer.tsx +8 -5
  22. package/src/ui/components/primitives/empty.tsx +3 -3
  23. package/src/ui/components/primitives/item.tsx +3 -3
  24. package/src/ui/components/primitives/sonner.tsx +187 -8
  25. package/src/ui/docs/DocBrowser.tsx +102 -23
  26. package/src/ui/docs/content/accordion.md +22 -16
  27. package/src/ui/docs/content/action-form-card.md +8 -8
  28. package/src/ui/docs/content/action-form-dialog.md +9 -9
  29. package/src/ui/docs/content/action-form.md +28 -34
  30. package/src/ui/docs/content/action-list-dialog.md +11 -6
  31. package/src/ui/docs/content/action-list.md +64 -39
  32. package/src/ui/docs/content/action-trigger.md +21 -14
  33. package/src/ui/docs/content/action-view.md +8 -8
  34. package/src/ui/docs/content/actions.md +9 -9
  35. package/src/ui/docs/content/ai.md +3 -3
  36. package/src/ui/docs/content/alert.md +14 -12
  37. package/src/ui/docs/content/aspect-ratio.md +4 -4
  38. package/src/ui/docs/content/audit.md +2 -2
  39. package/src/ui/docs/content/auth.md +3 -3
  40. package/src/ui/docs/content/avatar.md +34 -14
  41. package/src/ui/docs/content/badge.md +3 -3
  42. package/src/ui/docs/content/breadcrumb.md +13 -8
  43. package/src/ui/docs/content/button.md +81 -6
  44. package/src/ui/docs/content/calendar.md +5 -5
  45. package/src/ui/docs/content/card.md +1 -1
  46. package/src/ui/docs/content/carousel.md +16 -11
  47. package/src/ui/docs/content/chat.md +3 -3
  48. package/src/ui/docs/content/checkbox.md +7 -7
  49. package/src/ui/docs/content/cli.md +5 -5
  50. package/src/ui/docs/content/collapsible.md +8 -8
  51. package/src/ui/docs/content/command.md +16 -8
  52. package/src/ui/docs/content/composer.md +2 -2
  53. package/src/ui/docs/content/content.md +2 -2
  54. package/src/ui/docs/content/copyable.md +4 -3
  55. package/src/ui/docs/content/customization.md +5 -5
  56. package/src/ui/docs/content/cycle.md +3 -3
  57. package/src/ui/docs/content/data-state.md +11 -12
  58. package/src/ui/docs/content/data.md +26 -33
  59. package/src/ui/docs/content/detail.md +3 -3
  60. package/src/ui/docs/content/dialog.md +339 -31
  61. package/src/ui/docs/content/dictionary-value.md +8 -8
  62. package/src/ui/docs/content/dock.md +3 -3
  63. package/src/ui/docs/content/drawer.md +27 -14
  64. package/src/ui/docs/content/empty-value.md +2 -2
  65. package/src/ui/docs/content/empty.md +19 -12
  66. package/src/ui/docs/content/events.md +4 -4
  67. package/src/ui/docs/content/field.md +34 -12
  68. package/src/ui/docs/content/getting-started.md +1 -1
  69. package/src/ui/docs/content/icon-picker.md +8 -4
  70. package/src/ui/docs/content/input-otp.md +20 -12
  71. package/src/ui/docs/content/input.md +121 -9
  72. package/src/ui/docs/content/item.md +27 -13
  73. package/src/ui/docs/content/kbd.md +19 -11
  74. package/src/ui/docs/content/label.md +5 -3
  75. package/src/ui/docs/content/log.md +4 -4
  76. package/src/ui/docs/content/markdown.md +7 -6
  77. package/src/ui/docs/content/mcp.md +13 -15
  78. package/src/ui/docs/content/menu.md +34 -16
  79. package/src/ui/docs/content/observability.md +2 -2
  80. package/src/ui/docs/content/page.md +51 -6
  81. package/src/ui/docs/content/pagination.md +22 -17
  82. package/src/ui/docs/content/popover.md +16 -8
  83. package/src/ui/docs/content/progress.md +7 -5
  84. package/src/ui/docs/content/queue.md +5 -5
  85. package/src/ui/docs/content/radio-group.md +20 -12
  86. package/src/ui/docs/content/router.md +11 -6
  87. package/src/ui/docs/content/scheduler.md +4 -5
  88. package/src/ui/docs/content/scroll-area.md +12 -7
  89. package/src/ui/docs/content/select.md +42 -29
  90. package/src/ui/docs/content/separator.md +5 -5
  91. package/src/ui/docs/content/sidebar.md +323 -54
  92. package/src/ui/docs/content/skeleton.md +3 -2
  93. package/src/ui/docs/content/slider.md +8 -7
  94. package/src/ui/docs/content/spinner.md +8 -8
  95. package/src/ui/docs/content/split.md +8 -5
  96. package/src/ui/docs/content/storage.md +6 -8
  97. package/src/ui/docs/content/switch.md +8 -7
  98. package/src/ui/docs/content/table.md +13 -3
  99. package/src/ui/docs/content/tabs.md +28 -14
  100. package/src/ui/docs/content/testing.md +9 -11
  101. package/src/ui/docs/content/textarea.md +5 -4
  102. package/src/ui/docs/content/toast.md +47 -13
  103. package/src/ui/docs/content/toggle.md +75 -7
  104. package/src/ui/docs/content/tokens.md +3 -3
  105. package/src/ui/docs/content/tooltip.md +19 -11
  106. package/src/ui/docs/content/truncate.md +7 -8
  107. package/src/ui/docs/content/ui.md +10 -9
  108. package/src/ui/docs/content/upgrading.md +7 -8
  109. package/src/ui/docs/registry.tsx +20 -37
  110. package/src/ui/meta.ts +64 -94
  111. package/src/ui/react.tsx +15 -16
  112. package/src/ui/theme.css +50 -0
  113. package/src/ui/components/primitives/alert-dialog.tsx +0 -192
  114. package/src/ui/docs/content/alert-dialog.md +0 -73
  115. package/src/ui/docs/content/button-group.md +0 -71
  116. package/src/ui/docs/content/confirm.md +0 -120
  117. package/src/ui/docs/content/input-group.md +0 -79
  118. package/src/ui/docs/content/page-state.md +0 -45
  119. package/src/ui/docs/content/toggle-group.md +0 -81
@@ -1,35 +1,134 @@
1
1
  import * as React from 'react'
2
2
  import * as DialogPrimitive from '@radix-ui/react-dialog'
3
+ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
3
4
  import { XIcon } from 'lucide-react'
4
5
  import { cn } from '../../lib/cn.ts'
5
6
 
6
- export function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
7
- return <DialogPrimitive.Root data-slot="dialog" {...props} />
7
+ export type DialogMode = 'default' | 'alert'
8
+
9
+ interface DialogContextValue {
10
+ mode: DialogMode
11
+ onResult?: (result: string) => void
12
+ }
13
+
14
+ const DialogContext = React.createContext<DialogContextValue>({ mode: 'default' })
15
+
16
+ function useDialogContext(): DialogContextValue {
17
+ return React.useContext(DialogContext)
18
+ }
19
+
20
+ export interface DialogProps
21
+ extends React.ComponentProps<typeof DialogPrimitive.Root> {
22
+ /** `alert` exige uma resposta explícita e deriva `role="alertdialog"`. */
23
+ mode?: DialogMode
24
+ /** Recebe o `result` do `DialogClose` acionado. */
25
+ onResult?: (result: string) => void
26
+ }
27
+
28
+ export function Dialog({
29
+ mode = 'default',
30
+ onResult,
31
+ modal,
32
+ ...props
33
+ }: DialogProps) {
34
+ const context = React.useMemo(() => ({ mode, onResult }), [mode, onResult])
35
+
36
+ return (
37
+ <DialogContext.Provider value={context}>
38
+ {mode === 'alert' ? (
39
+ <AlertDialogPrimitive.Root data-slot="dialog" {...props} />
40
+ ) : (
41
+ <DialogPrimitive.Root data-slot="dialog" modal={modal} {...props} />
42
+ )}
43
+ </DialogContext.Provider>
44
+ )
45
+ }
46
+
47
+ export function DialogTrigger({
48
+ ...props
49
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
50
+ const { mode } = useDialogContext()
51
+ return mode === 'alert' ? (
52
+ <AlertDialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
53
+ ) : (
54
+ <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
55
+ )
8
56
  }
9
57
 
10
- export function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
11
- return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
58
+ export function DialogPortal({
59
+ ...props
60
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
61
+ const { mode } = useDialogContext()
62
+ return mode === 'alert' ? (
63
+ <AlertDialogPrimitive.Portal data-slot="dialog-portal" {...props} />
64
+ ) : (
65
+ <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
66
+ )
12
67
  }
13
68
 
14
- export function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
15
- return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
69
+ export interface DialogCloseProps
70
+ extends React.ComponentProps<typeof DialogPrimitive.Close> {
71
+ /** No modo `alert`, registra este controle como destino inicial seguro do foco. */
72
+ initialFocus?: boolean
73
+ /** Resposta entregue a `Dialog.onResult` antes do fechamento. */
74
+ result?: string
16
75
  }
17
76
 
18
- export function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
19
- return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
77
+ export function DialogClose({
78
+ initialFocus = false,
79
+ result,
80
+ onClick,
81
+ ...props
82
+ }: DialogCloseProps) {
83
+ const root = useDialogContext()
84
+ const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
85
+ onClick?.(event)
86
+ if (!event.defaultPrevented && result !== undefined) root.onResult?.(result)
87
+ }
88
+
89
+ if (root.mode === 'alert') {
90
+ const ClosePrimitive = initialFocus
91
+ ? AlertDialogPrimitive.Cancel
92
+ : AlertDialogPrimitive.Action
93
+ return (
94
+ <ClosePrimitive
95
+ data-slot="dialog-close"
96
+ data-initial-focus={initialFocus || undefined}
97
+ onClick={handleClick}
98
+ {...props}
99
+ />
100
+ )
101
+ }
102
+
103
+ return (
104
+ <DialogPrimitive.Close
105
+ data-slot="dialog-close"
106
+ data-initial-focus={initialFocus || undefined}
107
+ onClick={handleClick}
108
+ {...props}
109
+ />
110
+ )
20
111
  }
21
112
 
22
113
  export function DialogOverlay({
23
114
  className,
24
115
  ...props
25
116
  }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
26
- return (
117
+ const { mode } = useDialogContext()
118
+ const overlayClasses = cn(
119
+ 'fixed inset-0 z-50 bg-black/30 backdrop-blur-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
120
+ className,
121
+ )
122
+ return mode === 'alert' ? (
123
+ <AlertDialogPrimitive.Overlay
124
+ data-slot="dialog-overlay"
125
+ className={overlayClasses}
126
+ {...props}
127
+ />
128
+ ) : (
27
129
  <DialogPrimitive.Overlay
28
130
  data-slot="dialog-overlay"
29
- className={cn(
30
- 'fixed inset-0 z-50 bg-black/30 backdrop-blur-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
31
- className,
32
- )}
131
+ className={overlayClasses}
33
132
  {...props}
34
133
  />
35
134
  )
@@ -38,29 +137,48 @@ export function DialogOverlay({
38
137
  export function DialogContent({
39
138
  className,
40
139
  children,
41
- showCloseButton = true,
140
+ showCloseButton,
42
141
  ...props
43
142
  }: React.ComponentProps<typeof DialogPrimitive.Content> & {
44
- /** Mostra o "X" de fechar no canto. Desligue pra um modal que exige ação explícita. */
143
+ /** Mostra o X no modo padrão. O modo `alert` exige uma resposta identificável. */
45
144
  showCloseButton?: boolean
46
145
  }) {
146
+ const { mode } = useDialogContext()
147
+ const showClose = showCloseButton ?? mode !== 'alert'
148
+ const contentClasses =
149
+ 'fixed left-[50%] top-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] overflow-clip rounded-xl border border-border bg-background text-foreground shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95'
150
+
151
+ if (mode === 'alert') {
152
+ return (
153
+ <DialogPortal>
154
+ <DialogOverlay />
155
+ <AlertDialogPrimitive.Content
156
+ data-slot="dialog-content"
157
+ data-mode="alert"
158
+ className={cn(contentClasses, 'grid max-w-xs gap-4 p-6', className)}
159
+ {...props}
160
+ >
161
+ {children}
162
+ </AlertDialogPrimitive.Content>
163
+ </DialogPortal>
164
+ )
165
+ }
166
+
47
167
  return (
48
168
  <DialogPortal>
49
169
  <DialogOverlay />
50
- {/* Superfície elevada: mantemos bg-card (divergência da casa; o shadcn usa bg-background). */}
51
170
  <DialogPrimitive.Content
52
171
  data-slot="dialog-content"
172
+ data-mode="default"
53
173
  className={cn(
54
- // Superfície PURA: só a casca (bg/raio/sombra) + coluna flex com teto de
55
- // altura. O padding mora nos SLOTS (Header/Body/Footer), não aqui — assim ninguém
56
- // precisa negar margem pra escapar de um p-6 embutido.
57
- "fixed left-[50%] top-[50%] z-50 flex max-h-[85vh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col overflow-clip rounded-xl bg-card text-card-foreground shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
174
+ contentClasses,
175
+ 'flex max-h-[85vh] flex-col sm:max-w-lg',
58
176
  className,
59
177
  )}
60
178
  {...props}
61
179
  >
62
180
  {children}
63
- {showCloseButton && (
181
+ {showClose && (
64
182
  <DialogPrimitive.Close
65
183
  data-slot="dialog-close"
66
184
  className="absolute right-4 top-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
@@ -74,17 +192,18 @@ export function DialogContent({
74
192
  )
75
193
  }
76
194
 
77
- export function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
195
+ export function DialogHeader({
196
+ className,
197
+ ...props
198
+ }: React.ComponentProps<'div'>) {
199
+ const { mode } = useDialogContext()
78
200
  return (
79
201
  <div
80
202
  data-slot="dialog-header"
81
- // Fixo (shrink-0), com divisor embaixo (border-b). p-5: padding igual nos quatro
82
- // lados, casando com o body. O :has tira o divisor quando o header é seguido
83
- // DIRETO pelo footer (confirm sem body) — senão o border-b dele encostaria no
84
- // A compensação coincide com a soma das hairlines; o divisor fica no footer.
85
203
  className={cn(
86
- 'flex shrink-0 flex-col gap-2 border-b p-5 text-center sm:text-left',
87
- '[&:has(+[data-slot=dialog-footer])]:border-b-0',
204
+ mode === 'alert'
205
+ ? 'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=dialog-media]:grid-rows-[auto_auto_1fr]'
206
+ : 'flex shrink-0 flex-col gap-2 border-b p-5 text-center sm:text-left [&:has(+[data-slot=dialog-footer])]:border-b-0',
88
207
  className,
89
208
  )}
90
209
  {...props}
@@ -92,27 +211,51 @@ export function DialogHeader({ className, ...props }: React.ComponentProps<'div'
92
211
  )
93
212
  }
94
213
 
95
- export function DialogBody({ className, ...props }: React.ComponentProps<'div'>) {
214
+ export function DialogMedia({
215
+ className,
216
+ ...props
217
+ }: React.ComponentProps<'div'>) {
218
+ return (
219
+ <div
220
+ data-slot="dialog-media"
221
+ className={cn(
222
+ 'mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted *:[svg:not([class*="size-"])]:size-8',
223
+ className,
224
+ )}
225
+ {...props}
226
+ />
227
+ )
228
+ }
229
+
230
+ export function DialogBody({
231
+ className,
232
+ ...props
233
+ }: React.ComponentProps<'div'>) {
234
+ const { mode } = useDialogContext()
96
235
  return (
97
236
  <div
98
237
  data-slot="dialog-body"
99
- // O miolo que ROLA: cresce, scrolla e padda a si mesmo. Pra corpo edge-to-edge
100
- // (cmdk, iframe de lente) passe `className` zerando o padding.
101
- className={cn('min-h-0 flex-1 overflow-y-auto p-5', className)}
238
+ className={cn(
239
+ mode === 'alert' ? 'min-w-0' : 'min-h-0 flex-1 overflow-y-auto p-5',
240
+ className,
241
+ )}
102
242
  {...props}
103
243
  />
104
244
  )
105
245
  }
106
246
 
107
- export function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
247
+ export function DialogFooter({
248
+ className,
249
+ ...props
250
+ }: React.ComponentProps<'div'>) {
251
+ const { mode } = useDialogContext()
108
252
  return (
109
253
  <div
110
254
  data-slot="dialog-footer"
111
- // Faixa destacada (divergência da casa, ao estilo do site do shadcn): borda em cima
112
- // + fundo muted. Sangra até as bordas sozinha — o content não tem padding, então não
113
- // há -mx pra compensar; o recorte do content arredonda o canto de baixo.
114
255
  className={cn(
115
- 'flex shrink-0 flex-col-reverse gap-2 border-t bg-muted/30 px-5 py-4 sm:flex-row sm:justify-end',
256
+ mode === 'alert'
257
+ ? 'grid grid-cols-2 gap-2 [&>*:only-child]:col-span-full'
258
+ : 'flex shrink-0 flex-col-reverse gap-2 border-t bg-muted/30 px-5 py-4 sm:flex-row sm:justify-end',
116
259
  className,
117
260
  )}
118
261
  {...props}
@@ -124,7 +267,14 @@ export function DialogTitle({
124
267
  className,
125
268
  ...props
126
269
  }: React.ComponentProps<typeof DialogPrimitive.Title>) {
127
- return (
270
+ const { mode } = useDialogContext()
271
+ return mode === 'alert' ? (
272
+ <AlertDialogPrimitive.Title
273
+ data-slot="dialog-title"
274
+ className={cn('text-lg font-semibold', className)}
275
+ {...props}
276
+ />
277
+ ) : (
128
278
  <DialogPrimitive.Title
129
279
  data-slot="dialog-title"
130
280
  className={cn('text-lg font-semibold leading-none', className)}
@@ -137,7 +287,14 @@ export function DialogDescription({
137
287
  className,
138
288
  ...props
139
289
  }: React.ComponentProps<typeof DialogPrimitive.Description>) {
140
- return (
290
+ const { mode } = useDialogContext()
291
+ return mode === 'alert' ? (
292
+ <AlertDialogPrimitive.Description
293
+ data-slot="dialog-description"
294
+ className={cn('text-sm text-muted-foreground', className)}
295
+ {...props}
296
+ />
297
+ ) : (
141
298
  <DialogPrimitive.Description
142
299
  data-slot="dialog-description"
143
300
  className={cn('text-sm text-muted-foreground', className)}
@@ -60,7 +60,7 @@ function DrawerContent({
60
60
  <DrawerPrimitive.Content
61
61
  data-slot="drawer-content"
62
62
  className={cn(
63
- "fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
63
+ "fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
64
64
  side === "right" &&
65
65
  "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
66
66
  side === "left" &&
@@ -89,7 +89,7 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
89
89
  return (
90
90
  <div
91
91
  data-slot="drawer-header"
92
- className={cn("flex flex-col gap-1.5 p-4", className)}
92
+ className={cn("flex shrink-0 flex-col gap-2 border-b p-5", className)}
93
93
  {...props}
94
94
  />
95
95
  );
@@ -99,7 +99,7 @@ function DrawerBody({ className, ...props }: React.ComponentProps<"div">) {
99
99
  return (
100
100
  <div
101
101
  data-slot="drawer-body"
102
- className={cn("min-h-0 flex-1 overflow-y-auto p-4", className)}
102
+ className={cn("min-h-0 flex-1 overflow-y-auto p-5", className)}
103
103
  {...props}
104
104
  />
105
105
  );
@@ -109,7 +109,10 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
109
109
  return (
110
110
  <div
111
111
  data-slot="drawer-footer"
112
- className={cn("mt-auto flex flex-col gap-2 p-4", className)}
112
+ className={cn(
113
+ "mt-auto flex shrink-0 flex-col-reverse gap-2 border-t bg-muted/30 px-5 py-4 sm:flex-row sm:justify-end",
114
+ className,
115
+ )}
113
116
  {...props}
114
117
  />
115
118
  );
@@ -122,7 +125,7 @@ function DrawerTitle({
122
125
  return (
123
126
  <DrawerPrimitive.Title
124
127
  data-slot="drawer-title"
125
- className={cn("font-semibold text-foreground", className)}
128
+ className={cn("text-lg font-semibold leading-none text-foreground", className)}
126
129
  {...props}
127
130
  />
128
131
  );
@@ -81,10 +81,10 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
81
81
  )
82
82
  }
83
83
 
84
- function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
84
+ function EmptyActions({ className, ...props }: React.ComponentProps<"div">) {
85
85
  return (
86
86
  <div
87
- data-slot="empty-content"
87
+ data-slot="empty-actions"
88
88
  className={cn(
89
89
  "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
90
90
  className
@@ -99,6 +99,6 @@ export {
99
99
  EmptyHeader,
100
100
  EmptyTitle,
101
101
  EmptyDescription,
102
- EmptyContent,
102
+ EmptyActions,
103
103
  EmptyMedia,
104
104
  }
@@ -116,14 +116,14 @@ function Item({
116
116
  }
117
117
 
118
118
  const itemMediaVariants = cva(
119
- "flex shrink-0 self-stretch items-center justify-center gap-2 [&_svg]:pointer-events-none",
119
+ "flex shrink-0 self-start items-center justify-center gap-2 [&_svg]:pointer-events-none",
120
120
  {
121
121
  variants: {
122
122
  variant: {
123
123
  default: "bg-transparent",
124
- icon: "min-h-8 w-8 rounded-sm border bg-muted [&_svg:not([class*='size-'])]:size-4",
124
+ icon: "size-8 rounded-sm border bg-muted [&_svg:not([class*='size-'])]:size-4",
125
125
  image:
126
- "min-h-10 w-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
126
+ "size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
127
127
  },
128
128
  },
129
129
  defaultVariants: { variant: "default" },
@@ -6,7 +6,149 @@ import {
6
6
  OctagonXIcon,
7
7
  TriangleAlertIcon,
8
8
  } from 'lucide-react'
9
- import { Toaster as Sonner, toast, type ToasterProps } from 'sonner'
9
+ import {
10
+ Toaster as Sonner,
11
+ toast as sonnerToast,
12
+ type ExternalToast,
13
+ type ToasterProps,
14
+ } from 'sonner'
15
+ import { cn } from '../../lib/cn.ts'
16
+ import {
17
+ Button,
18
+ type ButtonContext,
19
+ type ButtonVariant,
20
+ } from './button.tsx'
21
+
22
+ type ToastId = string | number
23
+ type ToastMessage = React.ReactNode | (() => React.ReactNode)
24
+
25
+ export interface ToastAction {
26
+ key?: React.Key
27
+ label: React.ReactNode
28
+ onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
29
+ context?: ButtonContext
30
+ variant?: ButtonVariant
31
+ disabled?: boolean
32
+ }
33
+
34
+ export interface ToastOptions extends Omit<ExternalToast, 'action' | 'cancel'> {
35
+ actions?: readonly ToastAction[]
36
+ }
37
+
38
+ interface ToastApi {
39
+ (message: ToastMessage, options?: ToastOptions): ToastId
40
+ success: (message: ToastMessage, options?: ToastOptions) => ToastId
41
+ info: (message: ToastMessage, options?: ToastOptions) => ToastId
42
+ warning: (message: ToastMessage, options?: ToastOptions) => ToastId
43
+ error: (message: ToastMessage, options?: ToastOptions) => ToastId
44
+ message: (message: ToastMessage, options?: ToastOptions) => ToastId
45
+ loading: (message: ToastMessage, options?: ToastOptions) => ToastId
46
+ custom: typeof sonnerToast.custom
47
+ promise: typeof sonnerToast.promise
48
+ dismiss: typeof sonnerToast.dismiss
49
+ getHistory: typeof sonnerToast.getHistory
50
+ getToasts: typeof sonnerToast.getToasts
51
+ }
52
+
53
+ interface ToastMediaProps {
54
+ children: React.ReactNode
55
+ className: string
56
+ }
57
+
58
+ function ToastMedia({ children, className }: ToastMediaProps): React.ReactElement {
59
+ return (
60
+ <span
61
+ className={cn(
62
+ 'flex size-9 items-center justify-center rounded-md bg-current/10 [&_svg]:size-5',
63
+ className,
64
+ )}
65
+ >
66
+ {children}
67
+ </span>
68
+ )
69
+ }
70
+
71
+ interface ToastActionsProps {
72
+ actions: readonly ToastAction[]
73
+ getToastId: () => ToastId | undefined
74
+ }
75
+
76
+ function ToastActions({ actions, getToastId }: ToastActionsProps): React.ReactElement {
77
+ return (
78
+ <div data-slot="toast-actions" className="ms-auto flex shrink-0 items-center gap-2">
79
+ {actions.map((action, index) => {
80
+ const primary = index === actions.length - 1
81
+ return (
82
+ <Button
83
+ key={action.key ?? index}
84
+ type="button"
85
+ size="sm"
86
+ context={action.context ?? (primary ? 'primary' : 'neutral')}
87
+ variant={action.variant ?? (primary ? 'solid' : 'ghost')}
88
+ disabled={action.disabled}
89
+ onClick={(event) => {
90
+ action.onClick(event)
91
+ if (!event.defaultPrevented) {
92
+ const toastId = getToastId()
93
+ if (toastId !== undefined) sonnerToast.dismiss(toastId)
94
+ }
95
+ }}
96
+ >
97
+ {action.label}
98
+ </Button>
99
+ )
100
+ })}
101
+ </div>
102
+ )
103
+ }
104
+
105
+ function toSonnerOptions(
106
+ options: ToastOptions | undefined,
107
+ getToastId: () => ToastId | undefined,
108
+ ): ExternalToast | undefined {
109
+ if (options === undefined) return undefined
110
+ const { actions, ...rest } = options
111
+ if (actions === undefined) return rest
112
+ return {
113
+ ...rest,
114
+ action: actions.length > 0 ? <ToastActions actions={actions} getToastId={getToastId} /> : undefined,
115
+ }
116
+ }
117
+
118
+ type SonnerToastMethod = (message: ToastMessage, options?: ExternalToast) => ToastId
119
+
120
+ function dispatchToast(
121
+ method: SonnerToastMethod,
122
+ message: ToastMessage,
123
+ options?: ToastOptions,
124
+ ): ToastId {
125
+ let toastId: ToastId | undefined
126
+ toastId = method(message, toSonnerOptions(options, () => toastId))
127
+ return toastId
128
+ }
129
+
130
+ const toast: ToastApi = Object.assign(
131
+ (message: ToastMessage, options?: ToastOptions) => dispatchToast(sonnerToast, message, options),
132
+ {
133
+ success: (message: ToastMessage, options?: ToastOptions) =>
134
+ dispatchToast(sonnerToast.success, message, options),
135
+ info: (message: ToastMessage, options?: ToastOptions) =>
136
+ dispatchToast(sonnerToast.info, message, options),
137
+ warning: (message: ToastMessage, options?: ToastOptions) =>
138
+ dispatchToast(sonnerToast.warning, message, options),
139
+ error: (message: ToastMessage, options?: ToastOptions) =>
140
+ dispatchToast(sonnerToast.error, message, options),
141
+ message: (message: ToastMessage, options?: ToastOptions) =>
142
+ dispatchToast(sonnerToast.message, message, options),
143
+ loading: (message: ToastMessage, options?: ToastOptions) =>
144
+ dispatchToast(sonnerToast.loading, message, options),
145
+ custom: sonnerToast.custom,
146
+ promise: sonnerToast.promise,
147
+ dismiss: sonnerToast.dismiss,
148
+ getHistory: sonnerToast.getHistory,
149
+ getToasts: sonnerToast.getToasts,
150
+ },
151
+ )
10
152
 
11
153
  /**
12
154
  * Toaster temático (sonner) — monte 1x no root do app. Os toasts saem estilizados
@@ -17,18 +159,54 @@ import { Toaster as Sonner, toast, type ToasterProps } from 'sonner'
17
159
  * usamos next-themes — passe `theme` por prop se precisar forçar (default 'system').
18
160
  * Elevação dark preservada: `--normal-bg` aponta pro popover (não pro background).
19
161
  */
20
- export function Toaster({ ...props }: ToasterProps): React.ReactElement {
162
+ export function Toaster({
163
+ className,
164
+ icons,
165
+ style,
166
+ toastOptions,
167
+ ...props
168
+ }: ToasterProps): React.ReactElement {
21
169
  return (
22
170
  <Sonner
23
171
  data-slot="toaster"
24
172
  theme="system"
25
- className="toaster group"
173
+ className={cn('toaster group', className)}
26
174
  icons={{
27
- success: <CircleCheckIcon className="size-4" />,
28
- info: <InfoIcon className="size-4" />,
29
- warning: <TriangleAlertIcon className="size-4" />,
30
- error: <OctagonXIcon className="size-4" />,
31
- loading: <Loader2Icon className="size-4 animate-spin" />,
175
+ success: (
176
+ <ToastMedia className="text-context-success-emphasis">
177
+ <CircleCheckIcon />
178
+ </ToastMedia>
179
+ ),
180
+ info: (
181
+ <ToastMedia className="text-context-info-emphasis">
182
+ <InfoIcon />
183
+ </ToastMedia>
184
+ ),
185
+ warning: (
186
+ <ToastMedia className="text-context-warning-emphasis">
187
+ <TriangleAlertIcon />
188
+ </ToastMedia>
189
+ ),
190
+ error: (
191
+ <ToastMedia className="text-context-danger-emphasis">
192
+ <OctagonXIcon />
193
+ </ToastMedia>
194
+ ),
195
+ loading: (
196
+ <ToastMedia className="text-context-neutral-emphasis">
197
+ <Loader2Icon className="animate-spin" />
198
+ </ToastMedia>
199
+ ),
200
+ ...icons,
201
+ }}
202
+ toastOptions={{
203
+ ...toastOptions,
204
+ classNames: {
205
+ ...toastOptions?.classNames,
206
+ toast: cn('opus-toast', toastOptions?.classNames?.toast),
207
+ icon: cn('opus-toast-icon', toastOptions?.classNames?.icon),
208
+ closeButton: cn('opus-toast-close', toastOptions?.classNames?.closeButton),
209
+ },
32
210
  }}
33
211
  style={
34
212
  {
@@ -36,6 +214,7 @@ export function Toaster({ ...props }: ToasterProps): React.ReactElement {
36
214
  '--normal-text': 'var(--popover-foreground)',
37
215
  '--normal-border': 'var(--border)',
38
216
  '--border-radius': 'var(--radius)',
217
+ ...style,
39
218
  } as React.CSSProperties
40
219
  }
41
220
  {...props}