@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
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+
6
+ const repoRoot = resolve(process.argv[2] ?? process.cwd());
7
+ const contentDir = resolve(repoRoot, "packages/opus/src/ui/docs/content");
8
+ const registryPath = resolve(repoRoot, "packages/opus/src/ui/docs/registry.tsx");
9
+
10
+ if (!existsSync(contentDir) || !existsSync(registryPath)) {
11
+ console.error(
12
+ "Não encontrei packages/opus/src/ui/docs/content e registry.tsx. Passe a raiz do repositório Opus.",
13
+ );
14
+ process.exit(2);
15
+ }
16
+
17
+ const registry = readFileSync(registryPath, "utf8");
18
+ const importedFiles = new Set(
19
+ [...registry.matchAll(/\.\/content\/([^"']+\.md)\?raw/g)].map((match) => match[1]),
20
+ );
21
+ const files = readdirSync(contentDir)
22
+ .filter((file) => file.endsWith(".md"))
23
+ .sort();
24
+ const findings = [];
25
+
26
+ for (const file of files) {
27
+ const source = readFileSync(resolve(contentDir, file), "utf8");
28
+ const lines = source.split("\n");
29
+
30
+ if (!importedFiles.has(file)) {
31
+ findings.push(`${file}: página órfã; não há import no registry`);
32
+ }
33
+
34
+ let inFence = false;
35
+ let previousHeading = 0;
36
+ for (let index = 0; index < lines.length; index += 1) {
37
+ const line = lines[index];
38
+ const lineNumber = index + 1;
39
+
40
+ if (/^```/.test(line)) {
41
+ inFence = !inFence;
42
+ continue;
43
+ }
44
+ if (inFence) continue;
45
+
46
+ const heading = /^(#{1,6})\s+/.exec(line);
47
+ if (heading) {
48
+ const level = heading[1].length;
49
+ if (previousHeading > 0 && level > previousHeading + 1) {
50
+ findings.push(`${file}:${lineNumber}: heading salta de h${previousHeading} para h${level}`);
51
+ }
52
+ previousHeading = level;
53
+ }
54
+
55
+ if (/^#{2,6}\s+Props(?:\s|$)/i.test(line)) {
56
+ findings.push(`${file}:${lineNumber}: nomeie a referência como “Propriedades de Componente”`);
57
+ }
58
+ if (/^\|\s*Prop\s*\|/i.test(line) || /\|\s*Default\s*\|/.test(line)) {
59
+ findings.push(`${file}:${lineNumber}: use “Propriedade” e “Padrão” no cabeçalho da tabela`);
60
+ }
61
+ if (/\b(?:pra|pro|pros|numa)\b/i.test(line)) {
62
+ findings.push(`${file}:${lineNumber}: abreviação coloquial na prosa`);
63
+ }
64
+ }
65
+
66
+ if (inFence) findings.push(`${file}: fence de código sem fechamento`);
67
+ }
68
+
69
+ for (const importedFile of importedFiles) {
70
+ if (!files.includes(importedFile)) {
71
+ findings.push(`${importedFile}: importado pelo registry, mas o arquivo não existe`);
72
+ }
73
+ }
74
+
75
+ if (findings.length > 0) {
76
+ console.error(findings.join("\n"));
77
+ console.error(`\n${findings.length} problema(s) estrutural(is) encontrado(s).`);
78
+ process.exit(1);
79
+ }
80
+
81
+ console.log(`${files.length} páginas auditadas; nenhuma inconsistência estrutural encontrada.`);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `dialog` — o trio imperativo (alert · confirm · prompt) em UMA linha.
2
+ * `dialog` — respostas imperativas (alert · confirm · prompt · choose) em UMA linha.
3
3
  *
4
4
  * await dialog.alert({ title: 'Sessão expirada', description: 'Você será desconectado.' })
5
5
  * if (await dialog.confirm({ title: 'Excluir a sessão?', context: 'danger' })) …
@@ -7,13 +7,14 @@
7
7
  *
8
8
  * Mesma forma do `toast` (superfície imperativa + host no shell), com uma diferença: eles
9
9
  * RESPONDEM — `alert` devolve `Promise<void>`, `confirm` `Promise<boolean>`, `prompt`
10
- * `Promise<string | null>`. É o que tira as ~15 linhas de JSX que cada tela montava.
10
+ * `Promise<string | null>` e `choose` a união dos resultados declarados. É o que tira as
11
+ * ~15 linhas de JSX que cada tela montava.
11
12
  *
12
13
  * Namespace `dialog` de propósito: `window.alert/confirm/prompt` são globais do browser, e
13
14
  * um import esquecido cairia no nativo (cinza, sem tema) sem erro. `window.dialog` não
14
15
  * existe — a família fica imune ao footgun. `confirm()` segue exportado como alias.
15
16
  *
16
- * Por baixo é o <AlertDialog>: `role="alertdialog"`, não fecha no clique fora, foco preso
17
+ * Por baixo é o <Dialog mode="alert">: `role="alertdialog"`, não fecha no clique fora, foco preso
17
18
  * — a semântica que uma interrupção que EXIGE resposta pede. ESC/Cancelar resolve o "não"
18
19
  * de cada tipo (void, `false`, `null`).
19
20
  */
@@ -21,23 +22,28 @@
21
22
  import * as React from 'react'
22
23
  import type { UiContext } from '../../../core/ui-context.ts'
23
24
  import {
24
- AlertDialog,
25
- AlertDialogAction,
26
- AlertDialogCancel,
27
- AlertDialogContent,
28
- AlertDialogDescription,
29
- AlertDialogFooter,
30
- AlertDialogHeader,
31
- AlertDialogMedia,
32
- AlertDialogTitle,
33
- } from '../primitives/alert-dialog.tsx'
25
+ Dialog,
26
+ DialogBody,
27
+ DialogClose,
28
+ DialogContent,
29
+ DialogDescription,
30
+ DialogFooter,
31
+ DialogHeader,
32
+ DialogMedia,
33
+ DialogTitle,
34
+ } from '../primitives/dialog.tsx'
35
+ import {
36
+ Button,
37
+ type ButtonContext,
38
+ type ButtonVariant,
39
+ } from '../primitives/button.tsx'
34
40
  import { Input } from '../primitives/input.tsx'
35
41
 
36
42
  /** Campos comuns aos três — o corpo (`body`) é o que o texto da `description` não expressa. */
37
43
  interface DialogBase {
38
44
  title: React.ReactNode
39
45
  description?: React.ReactNode
40
- /** Ícone grande acima do título (o quadro do AlertDialogMedia). */
46
+ /** Ícone grande acima do título (o quadro do DialogMedia). */
41
47
  media?: React.ReactNode
42
48
  /** Conteúdo próprio entre a descrição e os botões (lista, detalhe destacado). */
43
49
  body?: React.ReactNode
@@ -67,10 +73,27 @@ export interface PromptOptions extends DialogBase {
67
73
  defaultValue?: string
68
74
  }
69
75
 
76
+ export interface DialogChoice<TResult extends string = string> {
77
+ /** Valor devolvido quando esta ação for escolhida. Deve ser único na coleção. */
78
+ result: TResult
79
+ label: React.ReactNode
80
+ context?: ButtonContext
81
+ variant?: ButtonVariant
82
+ initialFocus?: boolean
83
+ disabled?: boolean
84
+ }
85
+
86
+ /** `choose` — uma escolha simples entre resultados explícitos. */
87
+ export interface ChooseOptions<TResult extends string = string>
88
+ extends Omit<DialogBase, 'action'> {
89
+ actions: readonly DialogChoice<TResult>[]
90
+ }
91
+
70
92
  type Pending =
71
93
  | ({ kind: 'alert'; id: number; resolve: () => void } & AlertOptions)
72
94
  | ({ kind: 'confirm'; id: number; resolve: (ok: boolean) => void } & ConfirmOptions)
73
95
  | ({ kind: 'prompt'; id: number; resolve: (value: string | null) => void } & PromptOptions)
96
+ | ({ kind: 'choose'; id: number; resolve: (result: string | null) => void } & ChooseOptions)
74
97
 
75
98
  let nextId = 0
76
99
  let listener: ((p: Pending) => void) | null = null
@@ -94,6 +117,23 @@ function rejectAmbiguousContext(
94
117
  )
95
118
  }
96
119
 
120
+ function validateChoose<TResult extends string>(options: ChooseOptions<TResult>): Error | null {
121
+ if (options.actions.length === 0) {
122
+ return new Error('dialog.choose() exige pelo menos uma ação.')
123
+ }
124
+ const focused = options.actions.filter((action) => action.initialFocus === true)
125
+ if (focused.length !== 1) {
126
+ return new Error('dialog.choose() exige uma única ação com initialFocus.')
127
+ }
128
+ if (focused[0]?.disabled === true) {
129
+ return new Error('dialog.choose() não permite initialFocus em uma ação desabilitada.')
130
+ }
131
+ if (new Set(options.actions.map((action) => action.result)).size !== options.actions.length) {
132
+ return new Error('dialog.choose() exige resultados únicos.')
133
+ }
134
+ return null
135
+ }
136
+
97
137
  export const dialog = {
98
138
  /** Avisa e espera o "OK". `Promise<void>` — só reconhecimento, sem decisão. */
99
139
  alert(options: AlertOptions): Promise<void> {
@@ -123,6 +163,21 @@ export const dialog = {
123
163
  listener?.({ ...options, kind: 'prompt', id: nextId++, resolve })
124
164
  })
125
165
  },
166
+ /** Oferece resultados explícitos. Fechar/ESC sem escolher resolve `null`. */
167
+ choose<const TResult extends string>(options: ChooseOptions<TResult>): Promise<TResult | null> {
168
+ const invalid = validateChoose(options)
169
+ if (invalid !== null) return Promise.reject(invalid)
170
+ const err = requireHost('dialog.choose()')
171
+ if (err !== null) return Promise.reject(err)
172
+ return new Promise<TResult | null>((resolve) => {
173
+ listener?.({
174
+ ...options,
175
+ kind: 'choose',
176
+ id: nextId++,
177
+ resolve: (result) => resolve(result as TResult | null),
178
+ })
179
+ })
180
+ },
126
181
  }
127
182
 
128
183
  /** Alias retrocompatível — `confirm()` é `dialog.confirm()`. */
@@ -164,6 +219,7 @@ export function DialogHost(): React.ReactElement {
164
219
  const cancel = (p: Pending): void => {
165
220
  if (p.kind === 'alert') p.resolve()
166
221
  else if (p.kind === 'confirm') p.resolve(false)
222
+ else if (p.kind === 'prompt') p.resolve(null)
167
223
  else p.resolve(null)
168
224
  }
169
225
  const accept = (p: Pending, value: string): void => {
@@ -172,24 +228,28 @@ export function DialogHost(): React.ReactElement {
172
228
  else p.resolve(value)
173
229
  }
174
230
 
175
- const variant = current !== undefined && current.kind !== 'alert' ? current.variant : undefined
176
- const context = current !== undefined && current.kind !== 'alert'
231
+ const variant = current !== undefined && (current.kind === 'confirm' || current.kind === 'prompt')
232
+ ? current.variant
233
+ : undefined
234
+ const context = current !== undefined && (current.kind === 'confirm' || current.kind === 'prompt')
177
235
  ? current.context ?? (variant === 'destructive' ? 'danger' : 'primary')
178
236
  : 'primary'
179
237
  const defaultAction = current?.kind === 'alert' ? 'OK' : 'Confirmar'
180
238
 
181
239
  return (
182
- <AlertDialog
240
+ <Dialog
241
+ mode="alert"
183
242
  open={current !== undefined}
184
243
  onOpenChange={(open) => {
185
244
  if (!open && current !== undefined) drop(current.id, cancel)
186
245
  }}
187
246
  >
188
247
  {current !== undefined && (
189
- <AlertDialogContent
248
+ <DialogContent
190
249
  data-slot="dialog"
191
250
  data-kind={current.kind}
192
- // Prompt: o foco vai pro input, não pro Cancelar (default do AlertDialog). Via
251
+ className={current.kind === 'choose' ? 'max-w-lg' : undefined}
252
+ // Prompt: o foco vai pro input, não pro Cancelar (default do Dialog). Via
193
253
  // querySelector (não ref) — o Input é function component sem forwardRef, e isto
194
254
  // funciona igual em React 18 e 19.
195
255
  onOpenAutoFocus={
@@ -204,14 +264,14 @@ export function DialogHost(): React.ReactElement {
204
264
  : undefined
205
265
  }
206
266
  >
207
- <AlertDialogHeader>
208
- {current.media !== undefined && <AlertDialogMedia>{current.media}</AlertDialogMedia>}
209
- <AlertDialogTitle>{current.title}</AlertDialogTitle>
267
+ <DialogHeader>
268
+ {current.media !== undefined && <DialogMedia>{current.media}</DialogMedia>}
269
+ <DialogTitle>{current.title}</DialogTitle>
210
270
  {current.description !== undefined && (
211
- <AlertDialogDescription>{current.description}</AlertDialogDescription>
271
+ <DialogDescription>{current.description}</DialogDescription>
212
272
  )}
213
- </AlertDialogHeader>
214
- {current.body !== undefined && <div data-slot="dialog-body">{current.body}</div>}
273
+ </DialogHeader>
274
+ {current.body !== undefined && <DialogBody>{current.body}</DialogBody>}
215
275
  {current.kind === 'prompt' && (
216
276
  <Input
217
277
  value={text}
@@ -225,23 +285,63 @@ export function DialogHost(): React.ReactElement {
225
285
  }}
226
286
  />
227
287
  )}
228
- <AlertDialogFooter>
229
- {current.kind !== 'alert' && (
230
- <AlertDialogCancel onClick={() => drop(current.id, cancel)}>
231
- {current.cancel ?? 'Cancelar'}
232
- </AlertDialogCancel>
288
+ <DialogFooter
289
+ className={
290
+ current.kind === 'choose'
291
+ ? 'grid-cols-[repeat(auto-fit,minmax(7rem,1fr))]'
292
+ : undefined
293
+ }
294
+ >
295
+ {current.kind === 'choose' ? (
296
+ current.actions.map((action, index) => {
297
+ const primary = index === current.actions.length - 1
298
+ return (
299
+ <DialogClose
300
+ key={action.result}
301
+ initialFocus={action.initialFocus}
302
+ asChild
303
+ >
304
+ <Button
305
+ context={action.context ?? (primary ? 'primary' : 'neutral')}
306
+ variant={action.variant ?? (primary ? 'solid' : 'ghost')}
307
+ disabled={action.disabled}
308
+ onClick={() => drop(current.id, (p) => {
309
+ if (p.kind === 'choose') p.resolve(action.result)
310
+ })}
311
+ >
312
+ {action.label}
313
+ </Button>
314
+ </DialogClose>
315
+ )
316
+ })
317
+ ) : (
318
+ <>
319
+ {current.kind !== 'alert' && (
320
+ <DialogClose initialFocus asChild>
321
+ <Button
322
+ context="neutral"
323
+ variant="ghost"
324
+ onClick={() => drop(current.id, cancel)}
325
+ >
326
+ {current.cancel ?? 'Cancelar'}
327
+ </Button>
328
+ </DialogClose>
329
+ )}
330
+ <DialogClose initialFocus={current.kind === 'alert'} asChild>
331
+ <Button
332
+ context={context}
333
+ variant="solid"
334
+ onClick={() => drop(current.id, (p) => accept(p, text))}
335
+ >
336
+ {current.action ?? defaultAction}
337
+ </Button>
338
+ </DialogClose>
339
+ </>
233
340
  )}
234
- <AlertDialogAction
235
- context={context}
236
- variant="solid"
237
- onClick={() => drop(current.id, (p) => accept(p, text))}
238
- >
239
- {current.action ?? defaultAction}
240
- </AlertDialogAction>
241
- </AlertDialogFooter>
242
- </AlertDialogContent>
341
+ </DialogFooter>
342
+ </DialogContent>
243
343
  )}
244
- </AlertDialog>
344
+ </Dialog>
245
345
  )
246
346
  }
247
347
 
@@ -84,19 +84,13 @@ import {
84
84
  import {
85
85
  Dialog,
86
86
  DialogBody,
87
+ DialogClose,
87
88
  DialogContent,
89
+ DialogDescription,
88
90
  DialogFooter,
89
91
  DialogHeader,
90
92
  DialogTitle,
91
93
  } from "../primitives/dialog.tsx";
92
- import {
93
- AlertDialog,
94
- AlertDialogContent,
95
- AlertDialogDescription,
96
- AlertDialogFooter,
97
- AlertDialogHeader,
98
- AlertDialogTitle,
99
- } from "../primitives/alert-dialog.tsx";
100
94
  import {
101
95
  InputGroup,
102
96
  InputGroupAddon,
@@ -1769,47 +1763,48 @@ export function ActionList<TInput, TItem>({
1769
1763
  </div>
1770
1764
  {footer}
1771
1765
  {confirming !== null && (
1772
- <AlertDialog
1766
+ <Dialog
1767
+ mode="alert"
1773
1768
  open
1774
1769
  onOpenChange={(o) => {
1775
1770
  if (!o) setConfirming(null);
1776
1771
  }}
1777
1772
  >
1778
- <AlertDialogContent>
1779
- <AlertDialogHeader>
1780
- <AlertDialogTitle>
1773
+ <DialogContent>
1774
+ <DialogHeader>
1775
+ <DialogTitle>
1781
1776
  {confirming.confirm?.title ?? confirming.label}
1782
- </AlertDialogTitle>
1777
+ </DialogTitle>
1783
1778
  {confirming.confirm?.message !== undefined && (
1784
- <AlertDialogDescription>
1779
+ <DialogDescription>
1785
1780
  {confirming.confirm.message}
1786
- </AlertDialogDescription>
1781
+ </DialogDescription>
1787
1782
  )}
1788
- </AlertDialogHeader>
1789
- <AlertDialogFooter>
1790
- <Button
1791
- variant="ghost"
1792
- size="sm"
1793
- onClick={() => setConfirming(null)}
1794
- >
1795
- Cancelar
1796
- </Button>
1797
- <Button
1798
- size="sm"
1799
- variant={
1800
- confirming.destructive === true ? "destructive" : "default"
1801
- }
1802
- onClick={() => {
1803
- const a = confirming;
1804
- setConfirming(null);
1805
- void runBatch(a);
1806
- }}
1807
- >
1808
- {confirming.label}
1809
- </Button>
1810
- </AlertDialogFooter>
1811
- </AlertDialogContent>
1812
- </AlertDialog>
1783
+ </DialogHeader>
1784
+ <DialogFooter>
1785
+ <DialogClose initialFocus asChild>
1786
+ <Button variant="ghost" size="sm">
1787
+ Cancelar
1788
+ </Button>
1789
+ </DialogClose>
1790
+ <DialogClose asChild>
1791
+ <Button
1792
+ size="sm"
1793
+ variant={
1794
+ confirming.destructive === true ? "destructive" : "default"
1795
+ }
1796
+ onClick={() => {
1797
+ const a = confirming;
1798
+ setConfirming(null);
1799
+ void runBatch(a);
1800
+ }}
1801
+ >
1802
+ {confirming.label}
1803
+ </Button>
1804
+ </DialogClose>
1805
+ </DialogFooter>
1806
+ </DialogContent>
1807
+ </Dialog>
1813
1808
  )}
1814
1809
  </div>
1815
1810
  </TooltipProvider>
@@ -12,7 +12,7 @@ import { cn } from "../../lib/cn.ts";
12
12
  import { Alert } from "../primitives/alert.tsx";
13
13
  import {
14
14
  Empty,
15
- EmptyContent,
15
+ EmptyActions,
16
16
  EmptyDescription,
17
17
  EmptyHeader,
18
18
  EmptyMedia,
@@ -108,7 +108,7 @@ export function PageState({
108
108
  <EmptyDescription>{description}</EmptyDescription>
109
109
  )}
110
110
  </EmptyHeader>
111
- {action !== undefined && <EmptyContent>{action}</EmptyContent>}
111
+ {action !== undefined && <EmptyActions>{action}</EmptyActions>}
112
112
  </Empty>
113
113
  </div>
114
114
  );
@@ -327,24 +327,15 @@ export function SidebarItem({
327
327
 
328
328
  export interface SidebarNavGroup {
329
329
  label?: string;
330
- items?: SidebarNavItem[];
331
- subgroups?: SidebarNavSubgroup[];
330
+ items: SidebarNavItem[];
332
331
  }
333
332
 
334
333
  export type SidebarNavItem = Omit<SidebarItemProps, "active" | "onClick"> & {
335
334
  id: string;
336
335
  };
337
336
 
338
- export interface SidebarNavSubgroup {
339
- label?: string;
340
- items: SidebarNavItem[];
341
- }
342
-
343
337
  const titled = (label?: string): boolean => (label ?? "").trim() !== "";
344
- const subFilled = (subgroup: SidebarNavSubgroup): boolean =>
345
- subgroup.items.length > 0;
346
- const groupFilled = (group: SidebarNavGroup): boolean =>
347
- (group.items?.length ?? 0) > 0 || (group.subgroups?.some(subFilled) ?? false);
338
+ const groupFilled = (group: SidebarNavGroup): boolean => group.items.length > 0;
348
339
 
349
340
  /** Rótulo discreto de um grupo de navegação, compartilhado por listas e árvores. */
350
341
  export function SidebarGroupLabel({
@@ -367,6 +358,29 @@ export function SidebarGroupLabel({
367
358
  );
368
359
  }
369
360
 
361
+ export interface SidebarTreeGroupProps {
362
+ className?: string;
363
+ children: ReactNode;
364
+ }
365
+
366
+ /** Agrupa descendentes de um nó e desenha a guia visual da hierarquia. */
367
+ export function SidebarTreeGroup({
368
+ className,
369
+ children,
370
+ }: SidebarTreeGroupProps): React.ReactElement {
371
+ return (
372
+ <div
373
+ data-slot="sidebar-tree-group"
374
+ className={cn(
375
+ "ml-[1.0625rem] space-y-0.5 border-l border-border pl-1.5",
376
+ className,
377
+ )}
378
+ >
379
+ {children}
380
+ </div>
381
+ );
382
+ }
383
+
370
384
  /** Navegação controlada para Sidebar. Header/footer continuam slots explícitos do pai. */
371
385
  export function SidebarNav({
372
386
  groups,
@@ -406,21 +420,7 @@ export function SidebarNav({
406
420
  {!collapsed && titled(group.label) && (
407
421
  <SidebarGroupLabel>{group.label}</SidebarGroupLabel>
408
422
  )}
409
- {group.items?.map((item) => renderItem(item))}
410
- {group.subgroups?.map((subgroup, si) =>
411
- !subFilled(subgroup) ? null : (
412
- <div key={si} className="space-y-0.5">
413
- {!collapsed && titled(subgroup.label) && (
414
- <div className="pb-0.5 pl-4 pr-2.5 pt-1.5 text-[0.6875rem] font-medium text-muted-foreground/50">
415
- {subgroup.label}
416
- </div>
417
- )}
418
- {subgroup.items.map((item) =>
419
- renderItem(item, titled(subgroup.label)),
420
- )}
421
- </div>
422
- ),
423
- )}
423
+ {group.items.map((item) => renderItem(item))}
424
424
  </div>
425
425
  ),
426
426
  )}
@@ -4,7 +4,7 @@
4
4
  * Use pra mutações sem form: assign, escalate, close, archive, excluir.
5
5
  * - Loading state automático (disabled + "..." enquanto roda).
6
6
  * - Toast em sucesso/erro; cache invalidation via action.invalidates.
7
- * - Confirmação opcional via <AlertDialog> (compacto, não fecha no clique fora) antes de disparar.
7
+ * - Confirmação opcional via <Dialog> (compacto, não fecha no clique fora) antes de disparar.
8
8
  * - `icon` faz o botão virar icon-only com tooltip: é a ação que mora NO item (linha,
9
9
  * card). Absorveu o antigo DeleteButton, que era este componente com uma lixeira.
10
10
  * Emite `data-action="<action.name>"` na raiz (selector E2E).
@@ -19,13 +19,14 @@ import { cn } from '../../lib/cn.ts'
19
19
  import { Button, type ButtonProps } from '../primitives/button.tsx'
20
20
  import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip.tsx'
21
21
  import {
22
- AlertDialog,
23
- AlertDialogContent,
24
- AlertDialogDescription,
25
- AlertDialogFooter,
26
- AlertDialogHeader,
27
- AlertDialogTitle,
28
- } from '../primitives/alert-dialog.tsx'
22
+ Dialog,
23
+ DialogClose,
24
+ DialogContent,
25
+ DialogDescription,
26
+ DialogFooter,
27
+ DialogHeader,
28
+ DialogTitle,
29
+ } from '../primitives/dialog.tsx'
29
30
 
30
31
  /** action.messages.* pode ser string ou I18nRef ({ key, default }). Resolve pra texto. */
31
32
  function msgText(m: unknown, fallback: string): string {
@@ -174,28 +175,30 @@ export function ActionTrigger<TInput, TData>({
174
175
  <>
175
176
  {renderButton(() => setOpen(true))}
176
177
 
177
- <AlertDialog open={open} onOpenChange={setOpen}>
178
- <AlertDialogContent data-action={action.name}>
179
- <AlertDialogHeader>
180
- <AlertDialogTitle>{confirm.title}</AlertDialogTitle>
178
+ <Dialog mode="alert" open={open} onOpenChange={setOpen}>
179
+ <DialogContent data-action={action.name}>
180
+ <DialogHeader>
181
+ <DialogTitle>{confirm.title}</DialogTitle>
181
182
  {(itemLabel !== undefined || confirm.description !== undefined) && (
182
- <AlertDialogDescription>
183
+ <DialogDescription>
183
184
  {itemLabel !== undefined && <span className="font-medium text-foreground">“{itemLabel}”</span>}
184
185
  {itemLabel !== undefined && confirm.description !== undefined ? ' — ' : ''}
185
186
  {confirm.description}
186
- </AlertDialogDescription>
187
+ </DialogDescription>
187
188
  )}
188
- </AlertDialogHeader>
189
- <AlertDialogFooter>
190
- <Button context="neutral" variant="ghost" onClick={() => setOpen(false)} disabled={isLoading}>
191
- {confirm.cancelLabel ?? 'Cancelar'}
192
- </Button>
189
+ </DialogHeader>
190
+ <DialogFooter>
191
+ <DialogClose initialFocus asChild>
192
+ <Button context="neutral" variant="ghost" disabled={isLoading}>
193
+ {confirm.cancelLabel ?? 'Cancelar'}
194
+ </Button>
195
+ </DialogClose>
193
196
  <Button context={confirmContext} variant={confirmVariant} busy={isLoading} onClick={fire}>
194
197
  {confirm.actionLabel ?? buttonLabel}
195
198
  </Button>
196
- </AlertDialogFooter>
197
- </AlertDialogContent>
198
- </AlertDialog>
199
+ </DialogFooter>
200
+ </DialogContent>
201
+ </Dialog>
199
202
  </>
200
203
  )
201
204
  }
@@ -76,7 +76,7 @@ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
76
76
  export function alertVariants(options: AlertStyleOptions = {}): string {
77
77
  const resolved = resolveAlertStyle(options);
78
78
  return cn(
79
- "relative grid w-full grid-cols-[0_minmax(0,1fr)] items-stretch gap-x-0 gap-y-1 rounded-lg border px-4 py-3 text-sm has-[>[data-slot=alert-media]]:grid-cols-[2.25rem_minmax(0,1fr)] has-[>[data-slot=alert-media]]:gap-x-3",
79
+ "relative grid w-full grid-cols-[0_minmax(0,1fr)_auto] items-stretch gap-x-0 gap-y-1 rounded-lg border p-4 text-sm has-[>[data-slot=alert-media]]:grid-cols-[3rem_minmax(0,1fr)_auto]",
80
80
  resolved.variant === "outline"
81
81
  ? semanticOutlineClasses[resolved.context]
82
82
  : semanticSubtleClasses[resolved.context],
@@ -92,7 +92,7 @@ function alertMediaClasses(context: AlertContext): string {
92
92
  danger: "text-context-danger-emphasis",
93
93
  };
94
94
  return cn(
95
- "col-start-1 row-start-1 flex min-h-9 w-9 self-stretch items-center justify-center rounded-md bg-current/10 [&_svg]:size-5",
95
+ "col-start-1 row-start-1 flex size-9 self-start items-center justify-center rounded-md bg-current/10 [&_svg]:size-5",
96
96
  textClasses[context],
97
97
  );
98
98
  }
@@ -325,7 +325,7 @@ export function AlertActions({
325
325
  <div
326
326
  data-slot="alert-actions"
327
327
  className={cn(
328
- "col-start-2 row-start-2 mt-1 flex items-center gap-2",
328
+ "col-start-3 row-start-1 ms-3 flex self-center items-center gap-2",
329
329
  className,
330
330
  )}
331
331
  {...props}