@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
@@ -14,9 +14,11 @@
14
14
 
15
15
  import type { ReactNode } from 'react'
16
16
  import { cn } from '../../lib/cn.ts'
17
+ import { focusRing } from '../primitives/control.ts'
17
18
  import { useSidebarCollapsed } from './sidebar.tsx'
18
19
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip.tsx'
19
20
 
21
+ /** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
20
22
  export interface ShellNavItem {
21
23
  /** Coordenada estável — volta em `onSelect` e casa com `activeId`. Único em toda a nav. */
22
24
  id: string
@@ -29,12 +31,14 @@ export interface ShellNavItem {
29
31
  disabled?: boolean
30
32
  }
31
33
 
34
+ /** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
32
35
  export interface ShellNavGroup {
33
36
  /** Cabeçalho do grupo. Ausente/vazio = itens soltos, sem rótulo. Some no recolhido. */
34
37
  label?: string
35
38
  items: ShellNavItem[]
36
39
  }
37
40
 
41
+ /** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
38
42
  export interface ShellNavProps {
39
43
  groups: ShellNavGroup[]
40
44
  /** `id` do item ativo. Ausente = nenhum destacado. */
@@ -74,7 +78,8 @@ function renderItem(item: ShellNavItem, activeId: string | undefined, onSelect:
74
78
  className={cn(
75
79
  'flex w-full items-center gap-2.5 rounded-md py-1.5 text-left text-sm transition-colors',
76
80
  'disabled:pointer-events-none disabled:opacity-40',
77
- 'outline-none focus-visible:ring-2 focus-visible:ring-ring/50',
81
+ 'outline-none',
82
+ focusRing,
78
83
  railed ? 'justify-center px-0' : 'px-2.5',
79
84
  active ? 'bg-muted font-medium text-foreground' : 'text-foreground/80 hover:bg-muted/60',
80
85
  )}
@@ -106,6 +111,7 @@ function renderGroup(group: ShellNavGroup, gi: number, activeId: string | undefi
106
111
  )
107
112
  }
108
113
 
114
+ /** @deprecated Use `SidebarNav`: mesma navegação, integrada à `Sidebar`. Mantido por compatibilidade. */
109
115
  export function ShellNav({ groups, activeId, onSelect, heading, footer, navLabel = 'Navegação', className }: ShellNavProps): React.ReactElement {
110
116
  const railed = useSidebarCollapsed()
111
117
 
@@ -127,6 +133,7 @@ export function ShellNav({ groups, activeId, onSelect, heading, footer, navLabel
127
133
  return railed ? <TooltipProvider delayDuration={200}>{body}</TooltipProvider> : body
128
134
  }
129
135
 
136
+ /** @deprecated Use `SidebarNav` (ver sidebar.md); mantido por compatibilidade. */
130
137
  export interface ShellNavHeadingProps {
131
138
  /** Título da seção. */
132
139
  title: ReactNode
@@ -135,8 +142,8 @@ export interface ShellNavHeadingProps {
135
142
  className?: string
136
143
  }
137
144
 
138
- /** Cabeçalho padrão do ShellNav: título + espaço pra um botão. É o gesto que hoje cada
139
- * nav monta à mão (o "+" do Relatórios, o título de Configurações). */
145
+ /** @deprecated Use `SidebarHeader` com `SidebarNav`. Cabeçalho do ShellNav: título + espaço
146
+ * pra um botão. Mantido por compatibilidade. */
140
147
  export function ShellNavHeading({ title, action, className }: ShellNavHeadingProps): React.ReactElement {
141
148
  return (
142
149
  <div className={cn('flex h-12 shrink-0 items-center gap-1.5 border-b border-border px-4', className)}>
@@ -7,6 +7,7 @@ import {
7
7
  import { ChevronRight } from "lucide-react";
8
8
  import { useOverflowing } from "../../lib/overflow.ts";
9
9
  import { cn } from "../../lib/cn.ts";
10
+ import { focusRing } from "../primitives/control.ts";
10
11
  import {
11
12
  Tooltip,
12
13
  TooltipContent,
@@ -177,17 +178,24 @@ export function SidebarItem({
177
178
  <button
178
179
  type="button"
179
180
  disabled={disabled}
181
+ aria-label={label}
180
182
  aria-current={active ? "page" : undefined}
181
183
  onClick={onClick}
182
184
  className={cn(
183
185
  "mx-auto grid size-9 place-items-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-40",
184
- "outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
186
+ "outline-none",
187
+ focusRing,
185
188
  active
186
189
  ? "bg-muted font-medium text-foreground"
187
190
  : "text-foreground/80 hover:bg-muted/60",
188
191
  )}
189
192
  >
190
- {icon}
193
+ <span
194
+ data-slot="sidebar-item-icon"
195
+ className="grid size-4 shrink-0 place-items-center [&_svg]:size-4"
196
+ >
197
+ {icon}
198
+ </span>
191
199
  </button>
192
200
  );
193
201
  // `className` vai no MESMO elemento nos dois modos — a linha —, senão a prop mudaria de alvo
@@ -235,8 +243,9 @@ export function SidebarItem({
235
243
  {/* Sem ícone, o chevron não se esconde: ele é o único ocupante do lugar. */}
236
244
  {icon !== undefined && (
237
245
  <span
246
+ data-slot="sidebar-item-icon"
238
247
  className={cn(
239
- "absolute inset-0 flex items-center justify-center transition-opacity",
248
+ "absolute inset-0 flex items-center justify-center transition-opacity [&_svg]:size-4",
240
249
  onToggle !== undefined &&
241
250
  "group-hover/sidebar-item:opacity-0 group-has-[[data-slot=sidebar-item-toggle]:focus-visible]/sidebar-item:opacity-0",
242
251
  )}
@@ -252,7 +261,8 @@ export function SidebarItem({
252
261
  aria-label={expanded ? `Fechar ${label}` : `Abrir ${label}`}
253
262
  aria-expanded={expanded}
254
263
  className={cn(
255
- "absolute inset-0 grid place-items-center rounded-sm text-muted-foreground transition-[color,opacity] outline-none hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring/50",
264
+ "absolute inset-0 grid place-items-center rounded-sm text-muted-foreground transition-[color,opacity] outline-none hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100",
265
+ focusRing,
256
266
  icon === undefined
257
267
  ? "pointer-events-auto opacity-100"
258
268
  : "pointer-events-none opacity-0 group-hover/sidebar-item:pointer-events-auto group-hover/sidebar-item:opacity-100",
@@ -275,7 +285,8 @@ export function SidebarItem({
275
285
  onClick={onClick}
276
286
  className={cn(
277
287
  "flex min-w-0 flex-1 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left text-sm disabled:pointer-events-none",
278
- "outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
288
+ "outline-none",
289
+ focusRing,
279
290
  disabled
280
291
  ? "text-muted-foreground/50"
281
292
  : active
@@ -327,24 +338,15 @@ export function SidebarItem({
327
338
 
328
339
  export interface SidebarNavGroup {
329
340
  label?: string;
330
- items?: SidebarNavItem[];
331
- subgroups?: SidebarNavSubgroup[];
341
+ items: SidebarNavItem[];
332
342
  }
333
343
 
334
344
  export type SidebarNavItem = Omit<SidebarItemProps, "active" | "onClick"> & {
335
345
  id: string;
336
346
  };
337
347
 
338
- export interface SidebarNavSubgroup {
339
- label?: string;
340
- items: SidebarNavItem[];
341
- }
342
-
343
348
  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);
349
+ const groupFilled = (group: SidebarNavGroup): boolean => group.items.length > 0;
348
350
 
349
351
  /** Rótulo discreto de um grupo de navegação, compartilhado por listas e árvores. */
350
352
  export function SidebarGroupLabel({
@@ -358,7 +360,30 @@ export function SidebarGroupLabel({
358
360
  <div
359
361
  data-slot="sidebar-group-label"
360
362
  className={cn(
361
- "px-2.5 pb-1 text-sm font-medium text-muted-foreground/70",
363
+ "px-2.5 py-1 text-sm font-medium text-muted-foreground/70",
364
+ className,
365
+ )}
366
+ >
367
+ {children}
368
+ </div>
369
+ );
370
+ }
371
+
372
+ export interface SidebarTreeGroupProps {
373
+ className?: string;
374
+ children: ReactNode;
375
+ }
376
+
377
+ /** Agrupa descendentes de um nó e desenha a guia visual da hierarquia. */
378
+ export function SidebarTreeGroup({
379
+ className,
380
+ children,
381
+ }: SidebarTreeGroupProps): React.ReactElement {
382
+ return (
383
+ <div
384
+ data-slot="sidebar-tree-group"
385
+ className={cn(
386
+ "ml-[1.0625rem] space-y-0.5 border-l border-border pl-1.5",
362
387
  className,
363
388
  )}
364
389
  >
@@ -406,21 +431,7 @@ export function SidebarNav({
406
431
  {!collapsed && titled(group.label) && (
407
432
  <SidebarGroupLabel>{group.label}</SidebarGroupLabel>
408
433
  )}
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
- )}
434
+ {group.items.map((item) => renderItem(item))}
424
435
  </div>
425
436
  ),
426
437
  )}
@@ -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).
@@ -16,16 +16,18 @@ import type { SimpleContract } from '../../../core/index.ts'
16
16
  import { useTriggerAction } from '../../drivers/react.tsx'
17
17
  import { toast } from '../primitives/sonner.tsx'
18
18
  import { cn } from '../../lib/cn.ts'
19
+ import { humanizeActionError } from '../../lib/action-errors.ts'
19
20
  import { Button, type ButtonProps } from '../primitives/button.tsx'
20
21
  import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip.tsx'
21
22
  import {
22
- AlertDialog,
23
- AlertDialogContent,
24
- AlertDialogDescription,
25
- AlertDialogFooter,
26
- AlertDialogHeader,
27
- AlertDialogTitle,
28
- } from '../primitives/alert-dialog.tsx'
23
+ Dialog,
24
+ DialogClose,
25
+ DialogContent,
26
+ DialogDescription,
27
+ DialogFooter,
28
+ DialogHeader,
29
+ DialogTitle,
30
+ } from '../primitives/dialog.tsx'
29
31
 
30
32
  /** action.messages.* pode ser string ou I18nRef ({ key, default }). Resolve pra texto. */
31
33
  function msgText(m: unknown, fallback: string): string {
@@ -70,16 +72,6 @@ export interface ActionTriggerProps<TInput, TData> {
70
72
  className?: string
71
73
  }
72
74
 
73
- /**
74
- * Categorias de erro cuja frase do SERVIDOR vence o rótulo do contrato: "Este agente tem 3
75
- * conversas — desabilite em vez de excluir" é acionável; "Falha ao excluir" não. É
76
- * ALLOWLIST de propósito — erro inesperado carrega texto técnico cru (uma violação de FK
77
- * viraria `violates foreign key constraint "user_unit_id_fkey"` no toast de quem só clicou
78
- * no botão), então categoria desconhecida cai no rótulo genérico, o lado seguro de errar.
79
- * `authorization` fica de fora: o runtime lança `Forbidden`, pior que a frase em pt-BR.
80
- */
81
- const BUSINESS_ERRORS = ['conflict', 'validation', 'not_found']
82
-
83
75
  export function ActionTrigger<TInput, TData>({
84
76
  action,
85
77
  input,
@@ -102,9 +94,9 @@ export function ActionTrigger<TInput, TData>({
102
94
  setOpen(false)
103
95
  },
104
96
  onError: (err) => {
105
- const detail = err.message.trim()
106
- const speakable = BUSINESS_ERRORS.includes(err.category as string) && detail !== ''
107
- toast.error(speakable ? detail : msgText(action.messages?.error, err.message))
97
+ // A frase de negócio do servidor vence o rótulo do contrato (ver lib/action-errors.ts);
98
+ // fora da allowlist, vale o rótulo nunca o texto técnico cru.
99
+ toast.error(humanizeActionError(err, msgText(action.messages?.error, 'Não foi possível concluir.')))
108
100
  },
109
101
  })
110
102
 
@@ -123,12 +115,19 @@ export function ActionTrigger<TInput, TData>({
123
115
  }
124
116
  : undefined)
125
117
  const destructive = spec?.destructive === true
126
- // Com ícone o botão é chrome do item: fica ghost, e o vermelho aparece no hover — uma
127
- // lixeira sólida vermelha em cada linha seria um campo minado visual.
118
+ /**
119
+ * Precedência de aparência:
120
+ * - Gatilho: `context` explícito vence `destructive`; sem ele, destrutivo é `danger`. Com
121
+ * ícone o botão é chrome do item: fica ghost e o vermelho aparece no hover — uma lixeira
122
+ * sólida vermelha em cada linha seria um campo minado visual.
123
+ * - Confirmar: `destructive` vence `context` (a pergunta é o momento de avisar o risco) e o
124
+ * botão é sempre `solid` — não herda o `variant` do gatilho, que descreve o item, não a
125
+ * decisão.
126
+ */
128
127
  const triggerContext = context ?? (destructive ? 'danger' : icon !== undefined ? 'neutral' : 'primary')
129
128
  const triggerVariant = variant ?? (icon !== undefined ? 'ghost' : 'solid')
130
129
  const confirmContext = destructive ? 'danger' : (context ?? 'primary')
131
- const confirmVariant = variant ?? 'solid'
130
+ const confirmVariant = 'solid'
132
131
 
133
132
  const fire = () => {
134
133
  void trigger(input)
@@ -174,28 +173,30 @@ export function ActionTrigger<TInput, TData>({
174
173
  <>
175
174
  {renderButton(() => setOpen(true))}
176
175
 
177
- <AlertDialog open={open} onOpenChange={setOpen}>
178
- <AlertDialogContent data-action={action.name}>
179
- <AlertDialogHeader>
180
- <AlertDialogTitle>{confirm.title}</AlertDialogTitle>
176
+ <Dialog mode="alert" open={open} onOpenChange={setOpen}>
177
+ <DialogContent data-action={action.name}>
178
+ <DialogHeader>
179
+ <DialogTitle>{confirm.title}</DialogTitle>
181
180
  {(itemLabel !== undefined || confirm.description !== undefined) && (
182
- <AlertDialogDescription>
181
+ <DialogDescription>
183
182
  {itemLabel !== undefined && <span className="font-medium text-foreground">“{itemLabel}”</span>}
184
183
  {itemLabel !== undefined && confirm.description !== undefined ? ' — ' : ''}
185
184
  {confirm.description}
186
- </AlertDialogDescription>
185
+ </DialogDescription>
187
186
  )}
188
- </AlertDialogHeader>
189
- <AlertDialogFooter>
190
- <Button context="neutral" variant="ghost" onClick={() => setOpen(false)} disabled={isLoading}>
191
- {confirm.cancelLabel ?? 'Cancelar'}
192
- </Button>
187
+ </DialogHeader>
188
+ <DialogFooter>
189
+ <DialogClose initialFocus asChild>
190
+ <Button context="neutral" variant="ghost" disabled={isLoading}>
191
+ {confirm.cancelLabel ?? 'Cancelar'}
192
+ </Button>
193
+ </DialogClose>
193
194
  <Button context={confirmContext} variant={confirmVariant} busy={isLoading} onClick={fire}>
194
195
  {confirm.actionLabel ?? buttonLabel}
195
196
  </Button>
196
- </AlertDialogFooter>
197
- </AlertDialogContent>
198
- </AlertDialog>
197
+ </DialogFooter>
198
+ </DialogContent>
199
+ </Dialog>
199
200
  </>
200
201
  )
201
202
  }
@@ -9,6 +9,7 @@
9
9
  import type { ReactNode } from 'react'
10
10
  import type { ViewAction } from '../../../core/index.ts'
11
11
  import { useViewAction } from '../../drivers/react.tsx'
12
+ import { isBusinessError, isRetryable } from '../../lib/action-errors.ts'
12
13
  import { Alert } from '../primitives/alert.tsx'
13
14
  import { Skeleton } from '../primitives/skeleton.tsx'
14
15
  import { Button } from '../primitives/button.tsx'
@@ -22,10 +23,13 @@ export interface ActionViewProps<TInput, TData> {
22
23
  render?: (data: TData, refetch: () => Promise<void>) => ReactNode
23
24
  /** Renderiza estado vazio (200 mas sem dado). Default: nada. */
24
25
  empty?: ReactNode
25
- /** Renderiza loading. Default: 3 skeletons stackados. */
26
- loading?: ReactNode
27
- /** Renderiza erro. Default: mensagem + botão "Tentar de novo". */
28
- error?: (err: { code: string; message: string }, retry: () => Promise<void>) => ReactNode
26
+ /** Renderiza loading. Default (ou `true`): 3 skeletons empilhados; `false` não renderiza
27
+ * nada enquanto carrega — o contêiner `data-action` permanece. */
28
+ loading?: ReactNode | boolean
29
+ /** Renderiza erro. Default: "Não foi possível carregar" + botão "Tentar de novo" (omitido em
30
+ * falta de permissão ou sessão); a frase do servidor entra como descrição só quando está na
31
+ * allowlist de lib/action-errors.ts. */
32
+ error?: (err: { code: string; message: string; category?: string }, retry: () => Promise<void>) => ReactNode
29
33
  }
30
34
 
31
35
  export function ActionView<TInput, TData>({
@@ -44,17 +48,16 @@ export function ActionView<TInput, TData>({
44
48
  }
45
49
 
46
50
  if (isLoading) {
47
- return (
48
- <div data-action={action.name}>
49
- {loading ?? (
50
- <div className="space-y-3">
51
- <Skeleton className="h-6 w-1/3" />
52
- <Skeleton className="h-4 w-2/3" />
53
- <Skeleton className="h-4 w-1/2" />
54
- </div>
55
- )}
51
+ const defaultLoading = (
52
+ <div className="space-y-3">
53
+ <Skeleton className="h-6 w-1/3" />
54
+ <Skeleton className="h-4 w-2/3" />
55
+ <Skeleton className="h-4 w-1/2" />
56
56
  </div>
57
57
  )
58
+ const loadingNode =
59
+ typeof loading === 'boolean' ? (loading ? defaultLoading : null) : (loading ?? defaultLoading)
60
+ return <div data-action={action.name}>{loadingNode}</div>
58
61
  }
59
62
 
60
63
  if (isError && err !== undefined) {
@@ -63,10 +66,16 @@ export function ActionView<TInput, TData>({
63
66
  }
64
67
  return (
65
68
  <div data-action={action.name}>
66
- <Alert context="danger" title={err.code} description={err.message}>
67
- <Button variant="outline" size="sm" onClick={() => void refetch()}>
68
- Tentar de novo
69
- </Button>
69
+ <Alert
70
+ context="danger"
71
+ title="Não foi possível carregar"
72
+ description={isBusinessError(err) ? err.message.trim() : undefined}
73
+ >
74
+ {isRetryable(err) && (
75
+ <Button variant="outline" size="sm" onClick={() => void refetch()}>
76
+ Tentar de novo
77
+ </Button>
78
+ )}
70
79
  </Alert>
71
80
  </div>
72
81
  )
@@ -8,8 +8,13 @@ import {
8
8
 
9
9
  export type AlertContext = Exclude<UiContext, "primary">;
10
10
  export type AlertVariant = "subtle" | "outline";
11
+ /**
12
+ * @deprecated Aliases de migração: `default` → `context="neutral"`, `destructive` →
13
+ * `context="danger"`; `success`, `warning` e `info` → o `context` homônimo (todos em
14
+ * `subtle`). Declare `context` + `variant`.
15
+ */
11
16
  export type LegacyAlertVariant =
12
- "default" | "destructive" | "success" | "warning" | "info" | "danger";
17
+ "default" | "destructive" | "success" | "warning" | "info";
13
18
 
14
19
  interface AlertStyleOptions {
15
20
  context?: AlertContext | null;
@@ -47,13 +52,15 @@ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
47
52
  context: AlertContext;
48
53
  variant: AlertVariant;
49
54
  } {
55
+ // `danger` não é alias: é um contexto que chegou pela prop errada. Segue como contexto.
56
+ if ((variant as string) === "danger")
57
+ return { context: "danger", variant: "subtle" };
50
58
  const legacy =
51
59
  variant === "default" ||
52
60
  variant === "destructive" ||
53
61
  variant === "success" ||
54
62
  variant === "warning" ||
55
- variant === "info" ||
56
- variant === "danger";
63
+ variant === "info";
57
64
  if (context != null && legacy) {
58
65
  throw new Error(
59
66
  "Alert não permite combinar context com uma variante semântica legada.",
@@ -65,8 +72,7 @@ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
65
72
  if (
66
73
  variant === "success" ||
67
74
  variant === "warning" ||
68
- variant === "info" ||
69
- variant === "danger"
75
+ variant === "info"
70
76
  ) {
71
77
  return { context: variant, variant: "subtle" };
72
78
  }
@@ -76,7 +82,7 @@ function resolveAlertStyle({ context, variant }: AlertStyleOptions = {}): {
76
82
  export function alertVariants(options: AlertStyleOptions = {}): string {
77
83
  const resolved = resolveAlertStyle(options);
78
84
  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",
85
+ "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
86
  resolved.variant === "outline"
81
87
  ? semanticOutlineClasses[resolved.context]
82
88
  : semanticSubtleClasses[resolved.context],
@@ -92,7 +98,7 @@ function alertMediaClasses(context: AlertContext): string {
92
98
  danger: "text-context-danger-emphasis",
93
99
  };
94
100
  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",
101
+ "col-start-1 row-start-1 flex size-9 self-start items-center justify-center rounded-md bg-current/10 [&_svg]:size-5",
96
102
  textClasses[context],
97
103
  );
98
104
  }
@@ -325,7 +331,7 @@ export function AlertActions({
325
331
  <div
326
332
  data-slot="alert-actions"
327
333
  className={cn(
328
- "col-start-2 row-start-2 mt-1 flex items-center gap-2",
334
+ "col-start-3 row-start-1 ms-3 flex self-center items-center gap-2",
329
335
  className,
330
336
  )}
331
337
  {...props}
@@ -115,7 +115,7 @@ export function Ask({
115
115
  type="button"
116
116
  size="sm"
117
117
  shape="pill"
118
- context={selected ? 'neutral' : 'primary'}
118
+ context={selected ? 'primary' : 'neutral'}
119
119
  variant={selected ? 'solid' : 'outline'}
120
120
  role={question.multiSelect ? undefined : 'radio'}
121
121
  aria-checked={question.multiSelect ? undefined : selected}
@@ -159,7 +159,7 @@ export function Ask({
159
159
  disabled={locked}
160
160
  />
161
161
  {invalid && (
162
- <p id={errorId} className="text-sm text-destructive">
162
+ <p id={errorId} className="text-sm text-context-danger-emphasis">
163
163
  Escolha uma opção ou escreva uma resposta.
164
164
  </p>
165
165
  )}
@@ -169,7 +169,7 @@ export function Ask({
169
169
  </div>
170
170
  </TooltipProvider>
171
171
  {!validCount && (
172
- <p role="alert" className="mt-3 text-sm text-destructive">
172
+ <p role="alert" className="mt-3 text-sm text-context-danger-emphasis">
173
173
  Ask aceita de 1 a 4 perguntas.
174
174
  </p>
175
175
  )}
@@ -3,6 +3,7 @@ import { Slot } from '@radix-ui/react-slot'
3
3
 
4
4
  import type { UiContext } from '../../../core/ui-context.ts'
5
5
  import { cn } from '../../lib/cn.ts'
6
+ import { focusRing } from './control.ts'
6
7
  import {
7
8
  semanticOutlineClasses,
8
9
  semanticSolidClasses,
@@ -11,6 +12,11 @@ import {
11
12
 
12
13
  export type BadgeContext = UiContext
13
14
  export type BadgeVariant = 'solid' | 'subtle' | 'outline'
15
+ /**
16
+ * @deprecated Aliases de migração: `default` → `context="primary"` solid, `secondary` →
17
+ * `context="neutral"` solid, `destructive` → `context="danger"` solid; `success`, `warning` e
18
+ * `info` → o `context` homônimo em `subtle`. Declare `context` + `variant`.
19
+ */
14
20
  export type LegacyBadgeVariant =
15
21
  | 'default'
16
22
  | 'secondary'
@@ -18,7 +24,6 @@ export type LegacyBadgeVariant =
18
24
  | 'success'
19
25
  | 'warning'
20
26
  | 'info'
21
- | 'danger'
22
27
 
23
28
  export interface BadgeStyleOptions {
24
29
  context?: BadgeContext | null
@@ -26,20 +31,21 @@ export interface BadgeStyleOptions {
26
31
  }
27
32
 
28
33
  const baseClasses =
29
- 'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&>svg]:pointer-events-none [&>svg]:size-3'
34
+ 'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&>svg]:pointer-events-none [&>svg]:size-3'
30
35
 
31
36
  export function resolveBadgeStyle({
32
37
  context,
33
38
  variant,
34
39
  }: BadgeStyleOptions = {}): { context: BadgeContext; variant: BadgeVariant } {
40
+ // `danger` não é alias: é um contexto que chegou pela prop errada. Segue como contexto.
41
+ if ((variant as string) === 'danger') return { context: 'danger', variant: 'subtle' }
35
42
  const legacy =
36
43
  variant === 'default' ||
37
44
  variant === 'secondary' ||
38
45
  variant === 'destructive' ||
39
46
  variant === 'success' ||
40
47
  variant === 'warning' ||
41
- variant === 'info' ||
42
- variant === 'danger'
48
+ variant === 'info'
43
49
  if (context != null && legacy) {
44
50
  throw new Error('Badge não permite combinar context com uma variante semântica legada.')
45
51
  }
@@ -53,7 +59,6 @@ export function resolveBadgeStyle({
53
59
  case 'success':
54
60
  case 'warning':
55
61
  case 'info':
56
- case 'danger':
57
62
  return { context: variant, variant: 'subtle' }
58
63
  default:
59
64
  return {
@@ -72,7 +77,7 @@ export function badgeVariants(options: BadgeStyleOptions = {}): string {
72
77
  : resolved.variant === 'outline'
73
78
  ? semanticOutlineClasses[resolved.context]
74
79
  : semanticSubtleClasses[resolved.context]
75
- return cn(baseClasses, classes)
80
+ return cn(baseClasses, focusRing, classes)
76
81
  }
77
82
 
78
83
  export interface BadgeProps extends React.ComponentProps<'span'>, BadgeStyleOptions {
@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
5
5
  import { cn } from '../../lib/cn.ts'
6
6
 
7
7
  function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8
- return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
8
+ return <nav aria-label="Navegação estrutural" data-slot="breadcrumb" {...props} />
9
9
  }
10
10
 
11
11
  function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
@@ -93,7 +93,7 @@ function BreadcrumbEllipsis({
93
93
  {...props}
94
94
  >
95
95
  <MoreHorizontal className="size-4" />
96
- <span className="sr-only">More</span>
96
+ <span className="sr-only">Mais</span>
97
97
  </span>
98
98
  )
99
99
  }
@@ -3,23 +3,30 @@ import { Slot } from '@radix-ui/react-slot'
3
3
 
4
4
  import type { UiContext } from '../../../core/ui-context.ts'
5
5
  import { cn } from '../../lib/cn.ts'
6
- import type { ControlShape } from './control.ts'
6
+ import { focusRing, type ControlShape } from './control.ts'
7
7
  import { Spinner } from './spinner.tsx'
8
8
 
9
9
  export type ButtonContext = Extract<UiContext, 'neutral' | 'primary' | 'danger'>
10
10
  export type ButtonVariant = 'solid' | 'subtle' | 'outline' | 'ghost' | 'link'
11
+ /**
12
+ * @deprecated Aliases de migração: `default` → `context="primary"`, `secondary` →
13
+ * `context="neutral"`, `destructive` → `context="danger"` (todos com `variant="solid"`).
14
+ * Declare `context` + `variant`.
15
+ */
11
16
  export type LegacyButtonVariant = 'default' | 'secondary' | 'destructive'
12
17
  export type ButtonSize = 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm' | 'icon-xs'
13
18
 
14
19
  export interface ButtonStyleOptions {
20
+ /** Sem `context`, `solid`/`subtle` assumem `primary`; `outline`/`ghost`/`link` assumem `neutral`. */
15
21
  context?: ButtonContext | null
22
+ /** Tratamento visual. Os valores de `LegacyButtonVariant` são aliases em retirada. */
16
23
  variant?: ButtonVariant | LegacyButtonVariant | null
17
24
  size?: ButtonSize | null
18
25
  shape?: ControlShape | null
19
26
  }
20
27
 
21
28
  const baseClasses =
22
- 'inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 active:brightness-90 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4'
29
+ 'inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all disabled:pointer-events-none disabled:opacity-50 active:brightness-90 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4'
23
30
 
24
31
  const sizeClasses: Record<ButtonSize, string> = {
25
32
  default: 'h-9 px-4 py-2 has-[>svg]:px-3',
@@ -71,7 +78,12 @@ export function resolveButtonStyle({ context, variant }: ButtonStyleOptions = {}
71
78
  if (variant === 'default') return { context: 'primary', variant: 'solid' }
72
79
  if (variant === 'secondary') return { context: 'neutral', variant: 'solid' }
73
80
  if (variant === 'destructive') return { context: 'danger', variant: 'solid' }
74
- return { context: context ?? 'primary', variant: variant ?? 'solid' }
81
+ const resolvedVariant: ButtonVariant = variant ?? 'solid'
82
+ // Tratamentos de superfície (solid/subtle) pedem destaque; os leves (outline/ghost/link)
83
+ // são o chrome discreto da casa e só ganham cor quando o contexto é declarado.
84
+ const defaultContext: ButtonContext =
85
+ resolvedVariant === 'solid' || resolvedVariant === 'subtle' ? 'primary' : 'neutral'
86
+ return { context: context ?? defaultContext, variant: resolvedVariant }
75
87
  }
76
88
 
77
89
  /**
@@ -92,6 +104,7 @@ export function buttonVariants(options: ButtonStyleOptions = {}): string {
92
104
  : linkClasses[resolved.context]
93
105
  return cn(
94
106
  baseClasses,
107
+ focusRing,
95
108
  variantClasses,
96
109
  sizeClasses[options.size ?? 'default'],
97
110
  options.shape === 'pill' && 'rounded-full',