@softize/opus 12.11.0 → 13.1.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 (213) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/check.mjs +2 -7
  6. package/bin/lib/cli-shared.mjs +131 -0
  7. package/bin/lib/copy.mjs +1 -5
  8. package/bin/lib/db.mjs +16 -74
  9. package/bin/lib/gen-openapi.mjs +3 -3
  10. package/bin/lib/gen-runner.mjs +1 -1
  11. package/bin/lib/gen.mjs +14 -69
  12. package/bin/lib/mcp.mjs +3 -1
  13. package/bin/lib/seed.mjs +5 -62
  14. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +93 -10
  15. package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
  16. package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
  17. package/docs/ownership-vs-shadcn-lock.md +2 -3
  18. package/docs/protocol.md +7 -7
  19. package/docs/radius-scale.md +1 -1
  20. package/docs/releasing.md +8 -2
  21. package/package.json +7 -3
  22. package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
  23. package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
  24. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
  25. package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
  26. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
  27. package/registry/templates/app/package.json +1 -1
  28. package/registry/templates/app/src/main.tsx +4 -4
  29. package/src/audit/drivers/console.ts +1 -0
  30. package/src/auth/drivers/better-auth.ts +1 -0
  31. package/src/auth/drivers/jwt.ts +1 -0
  32. package/src/cache/drivers/memory.ts +1 -0
  33. package/src/client/drivers/fetch.ts +2 -1
  34. package/src/core/actions.ts +6 -1
  35. package/src/core/audit.ts +9 -3
  36. package/src/core/contracts.ts +7 -0
  37. package/src/core/domain.ts +1 -1
  38. package/src/core/errors.ts +18 -15
  39. package/src/core/index.ts +4 -2
  40. package/src/core/package-version.ts +26 -0
  41. package/src/core/reactions.ts +1 -1
  42. package/src/core/runtime.ts +33 -23
  43. package/src/core/schedules.ts +1 -1
  44. package/src/core/types.ts +2 -2
  45. package/src/dsl/eval.ts +2 -2
  46. package/src/dsl/kysely.ts +2 -2
  47. package/src/dsl/loads.ts +1 -1
  48. package/src/dsl/parser.ts +5 -5
  49. package/src/events/drivers/mitt.ts +1 -0
  50. package/src/mcp/index.ts +2 -1
  51. package/src/observability/drivers/opentelemetry.ts +1 -0
  52. package/src/queue/drivers/bullmq.ts +3 -3
  53. package/src/scheduler/drivers/node-cron.ts +3 -2
  54. package/src/scheduler/every.ts +7 -7
  55. package/src/schema/openapi.ts +3 -3
  56. package/src/seed/index.ts +29 -0
  57. package/src/server/drivers/fastify.ts +5 -2
  58. package/src/server/drivers/node.ts +9 -6
  59. package/src/server/index.ts +3 -1
  60. package/src/storage/drivers/fs.ts +1 -0
  61. package/src/testing/index.ts +3 -3
  62. package/src/ui/components/patterns/confirm.tsx +142 -42
  63. package/src/ui/components/patterns/content-header.tsx +7 -1
  64. package/src/ui/components/patterns/data-state.tsx +1 -1
  65. package/src/ui/components/patterns/dock.tsx +20 -3
  66. package/src/ui/components/patterns/form.tsx +12 -8
  67. package/src/ui/components/patterns/list.tsx +36 -41
  68. package/src/ui/components/patterns/page-state.tsx +2 -2
  69. package/src/ui/components/patterns/page.tsx +19 -1
  70. package/src/ui/components/patterns/shell-nav.tsx +10 -3
  71. package/src/ui/components/patterns/sidebar.tsx +43 -32
  72. package/src/ui/components/patterns/trigger.tsx +39 -38
  73. package/src/ui/components/patterns/view.tsx +26 -17
  74. package/src/ui/components/primitives/alert.tsx +14 -8
  75. package/src/ui/components/primitives/ask.tsx +3 -3
  76. package/src/ui/components/primitives/badge.tsx +11 -6
  77. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  78. package/src/ui/components/primitives/button.tsx +16 -3
  79. package/src/ui/components/primitives/calendar.tsx +28 -2
  80. package/src/ui/components/primitives/carousel.tsx +3 -3
  81. package/src/ui/components/primitives/chat.tsx +1 -1
  82. package/src/ui/components/primitives/checkbox.tsx +1 -1
  83. package/src/ui/components/primitives/command.tsx +2 -2
  84. package/src/ui/components/primitives/control.ts +12 -0
  85. package/src/ui/components/primitives/copyable.tsx +1 -1
  86. package/src/ui/components/primitives/dialog.tsx +202 -40
  87. package/src/ui/components/primitives/dot.tsx +5 -0
  88. package/src/ui/components/primitives/drawer.tsx +18 -8
  89. package/src/ui/components/primitives/empty.tsx +3 -3
  90. package/src/ui/components/primitives/field.tsx +3 -3
  91. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  92. package/src/ui/components/primitives/input-group.tsx +1 -1
  93. package/src/ui/components/primitives/input-otp.tsx +1 -1
  94. package/src/ui/components/primitives/input.tsx +2 -2
  95. package/src/ui/components/primitives/item.tsx +3 -3
  96. package/src/ui/components/primitives/progress.tsx +32 -3
  97. package/src/ui/components/primitives/radio-group.tsx +1 -1
  98. package/src/ui/components/primitives/resizable.tsx +3 -1
  99. package/src/ui/components/primitives/select.tsx +5 -5
  100. package/src/ui/components/primitives/slider.tsx +5 -1
  101. package/src/ui/components/primitives/sonner.tsx +190 -8
  102. package/src/ui/components/primitives/switch.tsx +1 -0
  103. package/src/ui/components/primitives/tabs.tsx +1 -0
  104. package/src/ui/components/primitives/textarea.tsx +1 -1
  105. package/src/ui/components/primitives/toggle.tsx +1 -1
  106. package/src/ui/components/primitives/tooltip.tsx +1 -0
  107. package/src/ui/docs/DocBrowser.tsx +102 -23
  108. package/src/ui/docs/changelog.tsx +1 -1
  109. package/src/ui/docs/content/accordion.md +22 -16
  110. package/src/ui/docs/content/action-form-card.md +8 -8
  111. package/src/ui/docs/content/action-form-dialog.md +9 -9
  112. package/src/ui/docs/content/action-form.md +37 -36
  113. package/src/ui/docs/content/action-list-dialog.md +11 -6
  114. package/src/ui/docs/content/action-list.md +73 -39
  115. package/src/ui/docs/content/action-trigger.md +29 -15
  116. package/src/ui/docs/content/action-view.md +17 -10
  117. package/src/ui/docs/content/actions.md +9 -9
  118. package/src/ui/docs/content/ai.md +3 -3
  119. package/src/ui/docs/content/alert.md +14 -12
  120. package/src/ui/docs/content/ask.md +11 -0
  121. package/src/ui/docs/content/aspect-ratio.md +4 -4
  122. package/src/ui/docs/content/audit.md +2 -2
  123. package/src/ui/docs/content/auth.md +3 -3
  124. package/src/ui/docs/content/avatar.md +34 -14
  125. package/src/ui/docs/content/badge.md +3 -3
  126. package/src/ui/docs/content/breadcrumb.md +13 -8
  127. package/src/ui/docs/content/button.md +81 -6
  128. package/src/ui/docs/content/calendar.md +18 -5
  129. package/src/ui/docs/content/card.md +27 -1
  130. package/src/ui/docs/content/carousel.md +16 -11
  131. package/src/ui/docs/content/chat.md +23 -3
  132. package/src/ui/docs/content/checkbox.md +7 -7
  133. package/src/ui/docs/content/cli.md +74 -22
  134. package/src/ui/docs/content/collapsible.md +8 -8
  135. package/src/ui/docs/content/command.md +16 -8
  136. package/src/ui/docs/content/composer.md +17 -2
  137. package/src/ui/docs/content/content.md +17 -2
  138. package/src/ui/docs/content/copyable.md +12 -3
  139. package/src/ui/docs/content/customization.md +5 -5
  140. package/src/ui/docs/content/cycle.md +3 -3
  141. package/src/ui/docs/content/data-state.md +11 -12
  142. package/src/ui/docs/content/data.md +26 -33
  143. package/src/ui/docs/content/detail.md +22 -4
  144. package/src/ui/docs/content/dialog.md +339 -31
  145. package/src/ui/docs/content/dictionary-value.md +17 -10
  146. package/src/ui/docs/content/dock.md +11 -3
  147. package/src/ui/docs/content/dot.md +8 -0
  148. package/src/ui/docs/content/drawer.md +27 -14
  149. package/src/ui/docs/content/empty-value.md +2 -2
  150. package/src/ui/docs/content/empty.md +19 -12
  151. package/src/ui/docs/content/events.md +4 -4
  152. package/src/ui/docs/content/field.md +34 -12
  153. package/src/ui/docs/content/getting-started.md +3 -3
  154. package/src/ui/docs/content/icon-picker.md +19 -4
  155. package/src/ui/docs/content/input-otp.md +20 -12
  156. package/src/ui/docs/content/input.md +121 -9
  157. package/src/ui/docs/content/item.md +27 -13
  158. package/src/ui/docs/content/kbd.md +19 -11
  159. package/src/ui/docs/content/label.md +12 -3
  160. package/src/ui/docs/content/log.md +4 -4
  161. package/src/ui/docs/content/markdown.md +7 -6
  162. package/src/ui/docs/content/mcp.md +13 -15
  163. package/src/ui/docs/content/menu.md +40 -16
  164. package/src/ui/docs/content/metric-card.md +13 -0
  165. package/src/ui/docs/content/observability.md +2 -2
  166. package/src/ui/docs/content/page.md +59 -6
  167. package/src/ui/docs/content/pagination.md +22 -17
  168. package/src/ui/docs/content/popover.md +22 -8
  169. package/src/ui/docs/content/progress.md +15 -16
  170. package/src/ui/docs/content/queue.md +5 -5
  171. package/src/ui/docs/content/radio-group.md +20 -12
  172. package/src/ui/docs/content/router.md +11 -6
  173. package/src/ui/docs/content/scheduler.md +4 -5
  174. package/src/ui/docs/content/scroll-area.md +12 -7
  175. package/src/ui/docs/content/select.md +47 -34
  176. package/src/ui/docs/content/semantic-context.md +2 -2
  177. package/src/ui/docs/content/separator.md +5 -5
  178. package/src/ui/docs/content/sidebar.md +329 -54
  179. package/src/ui/docs/content/skeleton.md +9 -2
  180. package/src/ui/docs/content/slider.md +8 -7
  181. package/src/ui/docs/content/spinner.md +8 -8
  182. package/src/ui/docs/content/split.md +29 -5
  183. package/src/ui/docs/content/storage.md +6 -8
  184. package/src/ui/docs/content/switch.md +8 -7
  185. package/src/ui/docs/content/table.md +13 -3
  186. package/src/ui/docs/content/tabs.md +28 -14
  187. package/src/ui/docs/content/testing.md +9 -11
  188. package/src/ui/docs/content/textarea.md +12 -4
  189. package/src/ui/docs/content/toast.md +47 -13
  190. package/src/ui/docs/content/toggle.md +75 -7
  191. package/src/ui/docs/content/tokens.md +7 -7
  192. package/src/ui/docs/content/tooltip.md +19 -11
  193. package/src/ui/docs/content/truncate.md +15 -8
  194. package/src/ui/docs/content/ui.md +24 -9
  195. package/src/ui/docs/content/upgrading.md +7 -8
  196. package/src/ui/docs/doc-client.tsx +5 -5
  197. package/src/ui/docs/doc.tsx +26 -14
  198. package/src/ui/docs/registry.tsx +25 -42
  199. package/src/ui/docs/standalone.tsx +2 -2
  200. package/src/ui/drivers/react.tsx +17 -12
  201. package/src/ui/lib/action-errors.ts +45 -0
  202. package/src/ui/lib/zod-pt-br.ts +31 -4
  203. package/src/ui/meta.ts +65 -95
  204. package/src/ui/react.tsx +17 -16
  205. package/src/ui/theme.css +60 -8
  206. package/src/vite/design.ts +6 -18
  207. package/src/ui/components/primitives/alert-dialog.tsx +0 -192
  208. package/src/ui/docs/content/alert-dialog.md +0 -73
  209. package/src/ui/docs/content/button-group.md +0 -71
  210. package/src/ui/docs/content/confirm.md +0 -120
  211. package/src/ui/docs/content/input-group.md +0 -79
  212. package/src/ui/docs/content/page-state.md +0 -45
  213. package/src/ui/docs/content/toggle-group.md +0 -81
@@ -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,9 +163,24 @@ 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
- /** Alias retrocompatível — `confirm()` é `dialog.confirm()`. */
183
+ /** @deprecated Alias retrocompatível — use `dialog.confirm()`. */
129
184
  export function confirm(options: ConfirmOptions): Promise<boolean> {
130
185
  return dialog.confirm(options)
131
186
  }
@@ -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,25 +285,65 @@ 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
 
248
- /** Alias retrocompatível — `<ConfirmHost />` é `<DialogHost />`. */
348
+ /** @deprecated Alias retrocompatível — monte `<DialogHost />`. */
249
349
  export const ConfirmHost = DialogHost
@@ -171,7 +171,13 @@ interface ContentHeaderComposedProps extends Omit<
171
171
  export type ContentHeaderProps =
172
172
  ContentHeaderShorthandProps | ContentHeaderComposedProps;
173
173
 
174
- /** Header estrutural de Content; não é uma região autônoma. */
174
+ /**
175
+ * Header estrutural de Content; não é uma região autônoma.
176
+ *
177
+ * Compartilha o layout (título, meta, descrição e ações) com `PageHeader` (page.tsx); os dois
178
+ * serão unificados numa próxima série — até lá, uma correção de layout em um deve ser espelhada
179
+ * no outro.
180
+ */
175
181
  export function ContentHeader({
176
182
  title,
177
183
  meta,
@@ -56,7 +56,7 @@ export function DataState({
56
56
  <tr data-slot="data-state" data-status={status}>
57
57
  <td
58
58
  colSpan={colSpan}
59
- className={cn('px-4 py-10 text-center text-sm', error ? 'text-destructive' : 'text-muted-foreground/60')}
59
+ className={cn('px-4 py-10 text-center text-sm', error ? 'text-context-danger-emphasis' : 'text-muted-foreground/60')}
60
60
  >
61
61
  {error ? (
62
62
  <div className="flex flex-col items-center gap-3">
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import * as React from 'react'
16
+ import type { UiContext } from '../../../core/ui-context.ts'
16
17
  import { cn } from '../../lib/cn.ts'
17
18
  import { Button } from '../primitives/button.tsx'
18
19
  import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip.tsx'
@@ -142,7 +143,8 @@ export function DockAction({ icon, label, pressed, disabled = false, hint, class
142
143
  <Button
143
144
  data-slot="dock-action"
144
145
  size="icon"
145
- variant={pressed === true ? 'default' : 'ghost'}
146
+ context={pressed === true ? 'primary' : 'neutral'}
147
+ variant={pressed === true ? 'solid' : 'ghost'}
146
148
  aria-label={label}
147
149
  aria-pressed={pressed}
148
150
  disabled={disabled}
@@ -162,9 +164,22 @@ const STATUS_POSITION = {
162
164
  'top-left': 'top-3 left-3',
163
165
  } as const
164
166
 
167
+ export type SurfaceStatusContext = Exclude<UiContext, 'primary'>
168
+
169
+ /** `neutral` mantém o cinza do chrome; os demais tingem borda e texto pela família semântica. */
170
+ const STATUS_CONTEXT: Record<SurfaceStatusContext, string> = {
171
+ neutral: 'border-border text-muted-foreground',
172
+ info: 'border-context-info-border text-context-info-emphasis',
173
+ success: 'border-context-success-border text-context-success-emphasis',
174
+ warning: 'border-context-warning-border text-context-warning-emphasis',
175
+ danger: 'border-context-danger-border text-context-danger-emphasis',
176
+ }
177
+
165
178
  export interface SurfaceStatusProps {
166
179
  /** Canto da superfície onde o estado se ancora. */
167
180
  position?: keyof typeof STATUS_POSITION
181
+ /** O que o estado comunica (salvo, falhou, publicando…). Sem contexto, fica neutro. */
182
+ context?: SurfaceStatusContext
168
183
  /** Ações do recurso aberto — em geral o mesmo menu que a linha dele tem na navegação. */
169
184
  actions?: React.ReactNode
170
185
  className?: string
@@ -180,14 +195,16 @@ export interface SurfaceStatusProps {
180
195
  * Mensagem de sucesso é transitória por natureza; falha permanece até o estado mudar. Quem decide
181
196
  * isso é a aplicação, que conhece o ciclo — o componente só reserva o lugar.
182
197
  */
183
- export function SurfaceStatus({ position = 'top-right', actions, className, children }: SurfaceStatusProps): React.ReactElement {
198
+ export function SurfaceStatus({ position = 'top-right', context = 'neutral', actions, className, children }: SurfaceStatusProps): React.ReactElement {
184
199
  const hasActions = actions !== undefined && actions !== null
185
200
  return (
186
201
  <div
187
202
  data-slot="surface-status"
203
+ data-context={context}
188
204
  className={cn(
189
205
  'pointer-events-none absolute z-10 flex max-w-[calc(100%-1.5rem)] flex-row items-center gap-2',
190
- 'rounded-full border border-border bg-background/95 py-1.5 pl-3 text-xs text-muted-foreground shadow-sm backdrop-blur',
206
+ 'rounded-full border bg-background/95 py-1.5 pl-3 text-xs shadow-sm backdrop-blur',
207
+ STATUS_CONTEXT[context],
191
208
  hasActions ? 'pr-1' : 'pr-3',
192
209
  STATUS_POSITION[position],
193
210
  className,
@@ -28,6 +28,7 @@ import {
28
28
  import { useDicts, useFormAction, type DictLike } from '../../drivers/react.tsx'
29
29
  import { z, type ZodTypeAny } from 'zod'
30
30
  import { cn } from '../../lib/cn.ts'
31
+ import { humanizeActionError } from '../../lib/action-errors.ts'
31
32
  import { objectSchemaShape } from '../../lib/object-schema.ts'
32
33
  import { toast } from '../primitives/sonner.tsx'
33
34
  import { Button } from '../primitives/button.tsx'
@@ -111,8 +112,9 @@ function inferFieldKind(schema: ZodTypeAny): FieldKind {
111
112
  interface FieldSpec {
112
113
  label?: string
113
114
  placeholder?: string
115
+ /** @deprecated Use `help` (ajuda junto à label) ou `description` do campo composto. */
114
116
  hint?: string
115
- /** Ajuda na label: ícone ⓘ + tooltip no hover (explicação mais longa que o hint). */
117
+ /** Ajuda na label: ícone ⓘ + tooltip no hover. */
116
118
  help?: string
117
119
  /** Override explícito do tipo de campo, quando o auto-detect do Zod não basta.
118
120
  * Honra 'textarea' (string sem max), 'code' (textarea monoespaçada, ex.: SKILL.md),
@@ -126,7 +128,7 @@ interface FieldSpec {
126
128
  * No modo COMPOSIÇÃO o condicional pode (e deve) ser JSX de quem diagrama. */
127
129
  showWhen?: (input: Record<string, unknown>) => boolean
128
130
  /** Origem declarada das opções (static/dictionary/lookup) — ver a precedência
129
- * em `ActionFormField`. `dictionary` resolve pelos dicts do TbdlibProvider. */
131
+ * em `ActionFormField`. `dictionary` resolve pelos dicts do OpusProvider. */
130
132
  options?: OptionsSpec
131
133
  }
132
134
 
@@ -208,7 +210,7 @@ function RequiredMark({ required }: { required: boolean }): React.ReactElement |
208
210
  if (!required) return null
209
211
  return (
210
212
  <>
211
- <span aria-hidden className="ml-0.5 text-destructive">
213
+ <span aria-hidden className="ml-0.5 text-context-danger-emphasis">
212
214
  *
213
215
  </span>
214
216
  <span className="sr-only"> (obrigatório)</span>
@@ -530,7 +532,7 @@ export function ActionFormField({ name, options: optionsProp, className }: Actio
530
532
  <Input
531
533
  id={`${name}-${i}-text`}
532
534
  value={item.text}
533
- placeholder={spec.placeholder ?? 'O quê (com critério)…'}
535
+ placeholder={spec.placeholder ?? 'Descreva o critério…'}
534
536
  onChange={(e) => setItems(items.map((it, j) => (j === i ? { ...it, text: e.target.value } : it)))}
535
537
  aria-invalid={errorMessage !== undefined}
536
538
  aria-describedby={describedBy}
@@ -651,7 +653,7 @@ export function ActionForm<TInput extends Record<string, unknown>, TData>({
651
653
  onSuccess?.(data)
652
654
  },
653
655
  onError: (err) => {
654
- toast.error(msgText(action.messages?.error, err.message))
656
+ toast.error(humanizeActionError(err, msgText(action.messages?.error, 'Não foi possível salvar.')))
655
657
  },
656
658
  })
657
659
 
@@ -669,13 +671,15 @@ export function ActionForm<TInput extends Record<string, unknown>, TData>({
669
671
  ))
670
672
 
671
673
  // Banner de erro do servidor (chrome do próprio form, nos dois modos). O FieldGroup
672
- // mantém o mesmo ritmo entre campos, blocos compostos e este estado.
674
+ // mantém o mesmo ritmo entre campos, blocos compostos e este estado. Só a frase de
675
+ // negócio chega aqui; código e mensagem técnica ficam no log do servidor.
673
676
  const errorBanner =
674
677
  error !== undefined && !isSuccess ? (
675
678
  <div
676
- className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive"
679
+ role="alert"
680
+ className="rounded-md border border-context-danger-border bg-context-danger-subtle p-3 text-sm text-context-danger-emphasis"
677
681
  >
678
- <strong>{error.code}</strong>: {error.message}
682
+ {humanizeActionError(error, msgText(action.messages?.error, 'Não foi possível salvar.'))}
679
683
  </div>
680
684
  ) : null
681
685
 
@@ -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,
@@ -760,7 +754,7 @@ function PeriodControl({
760
754
  <Button
761
755
  key={p.value}
762
756
  size="sm"
763
- context={period === p.value ? "neutral" : "primary"}
757
+ context={period === p.value ? "primary" : "neutral"}
764
758
  variant={period === p.value ? "solid" : "ghost"}
765
759
  className="justify-start"
766
760
  onClick={() => {
@@ -1434,6 +1428,7 @@ export function ActionList<TInput, TItem>({
1434
1428
  <PopoverTrigger asChild>
1435
1429
  <Button
1436
1430
  variant="outline"
1431
+ size="icon"
1437
1432
  className="shrink-0"
1438
1433
  aria-label="Exibição"
1439
1434
  >
@@ -1769,47 +1764,47 @@ export function ActionList<TInput, TItem>({
1769
1764
  </div>
1770
1765
  {footer}
1771
1766
  {confirming !== null && (
1772
- <AlertDialog
1767
+ <Dialog
1768
+ mode="alert"
1773
1769
  open
1774
1770
  onOpenChange={(o) => {
1775
1771
  if (!o) setConfirming(null);
1776
1772
  }}
1777
1773
  >
1778
- <AlertDialogContent>
1779
- <AlertDialogHeader>
1780
- <AlertDialogTitle>
1774
+ <DialogContent>
1775
+ <DialogHeader>
1776
+ <DialogTitle>
1781
1777
  {confirming.confirm?.title ?? confirming.label}
1782
- </AlertDialogTitle>
1778
+ </DialogTitle>
1783
1779
  {confirming.confirm?.message !== undefined && (
1784
- <AlertDialogDescription>
1780
+ <DialogDescription>
1785
1781
  {confirming.confirm.message}
1786
- </AlertDialogDescription>
1782
+ </DialogDescription>
1787
1783
  )}
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>
1784
+ </DialogHeader>
1785
+ <DialogFooter>
1786
+ <DialogClose initialFocus asChild>
1787
+ <Button variant="ghost" size="sm">
1788
+ Cancelar
1789
+ </Button>
1790
+ </DialogClose>
1791
+ <DialogClose asChild>
1792
+ <Button
1793
+ size="sm"
1794
+ context={confirming.destructive === true ? "danger" : "primary"}
1795
+ variant="solid"
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
  );
@@ -14,10 +14,26 @@ import {
14
14
  type ReactElement,
15
15
  type ReactNode,
16
16
  } from "react";
17
+ import { createPortal } from "react-dom";
17
18
  import { cn } from "../../lib/cn.ts";
18
19
 
19
20
  const PageContext = createContext(false);
20
21
  const PageHeaderContext = createContext(false);
22
+ const PageActionsTargetContext = createContext<HTMLElement | null>(null);
23
+
24
+ export function PageActionsTarget({
25
+ target,
26
+ children,
27
+ }: {
28
+ target: HTMLElement | null;
29
+ children: ReactNode;
30
+ }): ReactElement {
31
+ return (
32
+ <PageActionsTargetContext.Provider value={target}>
33
+ {children}
34
+ </PageActionsTargetContext.Provider>
35
+ );
36
+ }
21
37
 
22
38
  function requireParent(
23
39
  inside: boolean,
@@ -221,7 +237,8 @@ export function PageActions({
221
237
  ...props
222
238
  }: HTMLAttributes<HTMLDivElement>): ReactElement {
223
239
  requireParent(useContext(PageHeaderContext), "PageActions", "PageHeader");
224
- return (
240
+ const target = useContext(PageActionsTargetContext);
241
+ const actions = (
225
242
  <div
226
243
  data-slot="page-actions"
227
244
  className={cn(
@@ -231,6 +248,7 @@ export function PageActions({
231
248
  {...props}
232
249
  />
233
250
  );
251
+ return target === null ? actions : createPortal(actions, target);
234
252
  }
235
253
 
236
254
  export function PageBody({