@softize/opus 13.0.0 → 14.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 (164) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/cli-shared.mjs +131 -0
  6. package/bin/lib/copy.mjs +276 -6
  7. package/bin/lib/db.mjs +16 -74
  8. package/bin/lib/gen-openapi.mjs +3 -3
  9. package/bin/lib/gen-runner.mjs +1 -1
  10. package/bin/lib/gen.mjs +14 -69
  11. package/bin/lib/mcp.mjs +3 -1
  12. package/bin/lib/seed.mjs +5 -62
  13. package/docs/code-style.md +4 -1
  14. package/docs/ownership-vs-shadcn-lock.md +2 -3
  15. package/docs/protocol.md +7 -7
  16. package/docs/releasing.md +8 -2
  17. package/package.json +7 -3
  18. package/registry/instructions/opus.md +3 -3
  19. package/registry/templates/app/package.json +1 -1
  20. package/registry/templates/app/src/App.tsx +11 -6
  21. package/registry/templates/app/src/main.tsx +4 -4
  22. package/src/audit/drivers/console.ts +1 -0
  23. package/src/auth/drivers/better-auth.ts +1 -0
  24. package/src/auth/drivers/jwt.ts +1 -0
  25. package/src/cache/drivers/memory.ts +1 -0
  26. package/src/client/drivers/fetch.ts +2 -1
  27. package/src/core/actions.ts +6 -1
  28. package/src/core/audit.ts +9 -3
  29. package/src/core/contracts.ts +7 -0
  30. package/src/core/domain.ts +1 -1
  31. package/src/core/errors.ts +18 -15
  32. package/src/core/index.ts +4 -2
  33. package/src/core/package-version.ts +26 -0
  34. package/src/core/reactions.ts +1 -1
  35. package/src/core/runtime.ts +33 -23
  36. package/src/core/schedules.ts +1 -1
  37. package/src/core/types.ts +5 -6
  38. package/src/dsl/eval.ts +2 -2
  39. package/src/dsl/kysely.ts +2 -2
  40. package/src/dsl/loads.ts +1 -1
  41. package/src/dsl/parser.ts +5 -5
  42. package/src/events/drivers/mitt.ts +1 -0
  43. package/src/mcp/index.ts +2 -1
  44. package/src/observability/drivers/opentelemetry.ts +1 -0
  45. package/src/queue/drivers/bullmq.ts +3 -3
  46. package/src/scheduler/drivers/node-cron.ts +3 -2
  47. package/src/scheduler/every.ts +7 -7
  48. package/src/schema/openapi.ts +3 -3
  49. package/src/seed/index.ts +29 -0
  50. package/src/server/drivers/fastify.ts +5 -2
  51. package/src/server/drivers/node.ts +9 -6
  52. package/src/server/index.ts +3 -1
  53. package/src/storage/drivers/fs.ts +1 -0
  54. package/src/testing/index.ts +3 -3
  55. package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
  56. package/src/ui/components/patterns/confirm.tsx +2 -31
  57. package/src/ui/components/patterns/content-header.tsx +44 -137
  58. package/src/ui/components/patterns/data-state.tsx +42 -68
  59. package/src/ui/components/patterns/dock.tsx +20 -3
  60. package/src/ui/components/patterns/form.tsx +26 -22
  61. package/src/ui/components/patterns/list.tsx +18 -12
  62. package/src/ui/components/patterns/page-state.tsx +39 -51
  63. package/src/ui/components/patterns/page.tsx +37 -54
  64. package/src/ui/components/patterns/sidebar.tsx +17 -6
  65. package/src/ui/components/patterns/state-surface.tsx +148 -0
  66. package/src/ui/components/patterns/surface-header.tsx +119 -0
  67. package/src/ui/components/patterns/trigger.tsx +21 -25
  68. package/src/ui/components/patterns/view.tsx +29 -22
  69. package/src/ui/components/primitives/alert.tsx +1 -27
  70. package/src/ui/components/primitives/ask.tsx +3 -3
  71. package/src/ui/components/primitives/avatar.tsx +15 -5
  72. package/src/ui/components/primitives/badge.tsx +5 -41
  73. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  74. package/src/ui/components/primitives/button.tsx +39 -30
  75. package/src/ui/components/primitives/calendar.tsx +28 -2
  76. package/src/ui/components/primitives/carousel.tsx +3 -3
  77. package/src/ui/components/primitives/chat.tsx +1 -1
  78. package/src/ui/components/primitives/checkbox.tsx +1 -1
  79. package/src/ui/components/primitives/command.tsx +2 -2
  80. package/src/ui/components/primitives/control.ts +72 -0
  81. package/src/ui/components/primitives/copyable.tsx +1 -1
  82. package/src/ui/components/primitives/dialog.tsx +12 -7
  83. package/src/ui/components/primitives/dot.tsx +1 -25
  84. package/src/ui/components/primitives/drawer.tsx +10 -3
  85. package/src/ui/components/primitives/field.tsx +3 -3
  86. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  87. package/src/ui/components/primitives/input-group.tsx +12 -9
  88. package/src/ui/components/primitives/input-otp.tsx +1 -1
  89. package/src/ui/components/primitives/input.tsx +2 -2
  90. package/src/ui/components/primitives/item.tsx +3 -1
  91. package/src/ui/components/primitives/menu.tsx +1 -7
  92. package/src/ui/components/primitives/pagination.tsx +16 -8
  93. package/src/ui/components/primitives/progress.tsx +32 -3
  94. package/src/ui/components/primitives/radio-group.tsx +1 -1
  95. package/src/ui/components/primitives/resizable.tsx +3 -1
  96. package/src/ui/components/primitives/select.tsx +6 -6
  97. package/src/ui/components/primitives/slider.tsx +5 -1
  98. package/src/ui/components/primitives/sonner.tsx +3 -0
  99. package/src/ui/components/primitives/spinner.tsx +13 -16
  100. package/src/ui/components/primitives/switch.tsx +5 -1
  101. package/src/ui/components/primitives/tabs.tsx +6 -3
  102. package/src/ui/components/primitives/textarea.tsx +1 -1
  103. package/src/ui/components/primitives/toggle.tsx +9 -4
  104. package/src/ui/components/primitives/tooltip.tsx +1 -0
  105. package/src/ui/docs/changelog.tsx +1 -1
  106. package/src/ui/docs/content/action-form.md +36 -3
  107. package/src/ui/docs/content/action-list-dialog.md +2 -2
  108. package/src/ui/docs/content/action-list.md +13 -2
  109. package/src/ui/docs/content/action-trigger.md +12 -5
  110. package/src/ui/docs/content/action-view.md +11 -3
  111. package/src/ui/docs/content/ask.md +11 -0
  112. package/src/ui/docs/content/avatar.md +7 -3
  113. package/src/ui/docs/content/button.md +30 -14
  114. package/src/ui/docs/content/calendar.md +13 -0
  115. package/src/ui/docs/content/card.md +26 -0
  116. package/src/ui/docs/content/chat.md +20 -0
  117. package/src/ui/docs/content/cli.md +71 -19
  118. package/src/ui/docs/content/communication.md +36 -0
  119. package/src/ui/docs/content/composer.md +15 -0
  120. package/src/ui/docs/content/content.md +17 -1
  121. package/src/ui/docs/content/copyable.md +8 -0
  122. package/src/ui/docs/content/data-state.md +17 -13
  123. package/src/ui/docs/content/detail.md +19 -1
  124. package/src/ui/docs/content/dialog.md +1 -4
  125. package/src/ui/docs/content/dictionary-value.md +9 -2
  126. package/src/ui/docs/content/dock.md +8 -0
  127. package/src/ui/docs/content/dot.md +8 -0
  128. package/src/ui/docs/content/empty.md +2 -2
  129. package/src/ui/docs/content/getting-started.md +2 -2
  130. package/src/ui/docs/content/icon-picker.md +11 -0
  131. package/src/ui/docs/content/input.md +1 -1
  132. package/src/ui/docs/content/item.md +1 -1
  133. package/src/ui/docs/content/label.md +7 -0
  134. package/src/ui/docs/content/menu.md +6 -0
  135. package/src/ui/docs/content/metric-card.md +13 -0
  136. package/src/ui/docs/content/page.md +20 -4
  137. package/src/ui/docs/content/pagination.md +11 -9
  138. package/src/ui/docs/content/popover.md +6 -0
  139. package/src/ui/docs/content/progress.md +8 -11
  140. package/src/ui/docs/content/select.md +5 -5
  141. package/src/ui/docs/content/semantic-context.md +5 -4
  142. package/src/ui/docs/content/sidebar.md +13 -47
  143. package/src/ui/docs/content/skeleton.md +6 -0
  144. package/src/ui/docs/content/spinner.md +9 -6
  145. package/src/ui/docs/content/split.md +21 -0
  146. package/src/ui/docs/content/switch.md +1 -1
  147. package/src/ui/docs/content/tabs.md +1 -1
  148. package/src/ui/docs/content/textarea.md +7 -0
  149. package/src/ui/docs/content/toggle.md +1 -1
  150. package/src/ui/docs/content/tokens.md +4 -4
  151. package/src/ui/docs/content/truncate.md +8 -0
  152. package/src/ui/docs/content/ui.md +14 -0
  153. package/src/ui/docs/doc-client.tsx +5 -5
  154. package/src/ui/docs/doc.tsx +26 -14
  155. package/src/ui/docs/registry.tsx +5 -5
  156. package/src/ui/docs/standalone.tsx +2 -2
  157. package/src/ui/drivers/react.tsx +12 -12
  158. package/src/ui/lib/action-errors.ts +45 -0
  159. package/src/ui/lib/zod-pt-br.ts +31 -4
  160. package/src/ui/meta.ts +8 -8
  161. package/src/ui/react.tsx +10 -16
  162. package/src/ui/theme.css +10 -8
  163. package/src/vite/design.ts +6 -18
  164. package/src/ui/components/patterns/shell-nav.tsx +0 -147
@@ -67,18 +67,12 @@ function MenuItem({
67
67
  className,
68
68
  inset,
69
69
  context,
70
- variant,
71
70
  ...props
72
71
  }: React.ComponentProps<typeof MenuPrimitive.Item> & {
73
72
  inset?: boolean
74
73
  context?: Extract<UiContext, 'neutral' | 'danger'>
75
- /** @deprecated Use `context="danger"`. */
76
- variant?: "default" | "destructive"
77
74
  }) {
78
- if (context !== undefined && variant !== undefined) {
79
- throw new Error('MenuItem não permite combinar context com uma variante semântica legada.')
80
- }
81
- const resolvedContext = context ?? (variant === 'destructive' ? 'danger' : 'neutral')
75
+ const resolvedContext = context ?? 'neutral'
82
76
  return (
83
77
  <MenuPrimitive.Item
84
78
  data-slot="menu-item"
@@ -3,6 +3,7 @@ import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-re
3
3
 
4
4
  import { cn } from '../../lib/cn.ts'
5
5
  import { buttonVariants, type ButtonProps } from './button.tsx'
6
+ import { controlSquare, type ControlIconSize } from './control.ts'
6
7
 
7
8
  function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
8
9
  return (
@@ -33,8 +34,8 @@ function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
33
34
  export interface PaginationLinkProps extends Omit<React.ComponentProps<'a'>, 'children'> {
34
35
  /** Página atual: vira `outline` e ganha `aria-current="page"`. */
35
36
  isActive?: boolean
36
- /** Escala do Button. `default` (números: altura fixa, largura mínima quadrada que cresce
37
- * com os dígitos) ou uma escala `icon*` (setas quadradas, sem padding horizontal). */
37
+ /** A escala de control.ts, via Button. `default` (números: altura fixa, largura mínima
38
+ * quadrada que cresce com os dígitos) ou um `icon*` (setas quadradas, sem padding). */
38
39
  size?: ButtonProps['size']
39
40
  /** Número da página: dá o nome acessível (“Página N”) e o conteúdo, quando `children` falta. */
40
41
  page?: number
@@ -83,10 +84,11 @@ function PaginationLink({ className, isActive, page, disabled, href, size = 'def
83
84
  export interface PaginationControlProps extends Omit<PaginationLinkProps, 'page' | 'isActive'> {
84
85
  /** Nome acessível e texto visível. Default: “Página anterior” / “Próxima página”. */
85
86
  label?: string
86
- /** Só a seta, quadrada (escala `icon`, `size-9`); o `label` continua como nome acessível. */
87
+ /** Só a seta, quadrada (`icon`, 2.25rem; `size="icon-sm"` para o rodapé denso); o `label`
88
+ * continua como nome acessível. */
87
89
  iconOnly?: boolean
88
- /** Classe do ícone (ex.: `size-3.5` na escala densa). Vai no próprio svg, porque o
89
- * fallback `[&_svg:not([class*='size-'])]:size-4` do Button vence qualquer seletor de fora. */
90
+ /** Classe do ícone, quando o glifo da escala não servir. Vai no próprio svg, porque o
91
+ * fallback `[&_svg:not([class*=size-])]` do Button vence qualquer seletor de fora. */
90
92
  iconClassName?: string
91
93
  }
92
94
 
@@ -132,16 +134,22 @@ function PaginationNext({
132
134
  )
133
135
  }
134
136
 
137
+ export interface PaginationEllipsisProps extends React.ComponentProps<'span'> {
138
+ /** O quadrado da escala de control.ts — o mesmo das setas ao lado. */
139
+ size?: ControlIconSize
140
+ }
141
+
135
142
  /** Decorativa: `aria-hidden`; os números ao redor já dizem que há páginas no vão. */
136
- function PaginationEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
143
+ function PaginationEllipsis({ className, size = 'icon', ...props }: PaginationEllipsisProps) {
137
144
  return (
138
145
  <span
139
146
  aria-hidden
140
147
  data-slot="pagination-ellipsis"
141
- className={cn('flex size-9 items-center justify-center', className)}
148
+ data-size={size}
149
+ className={cn('flex items-center justify-center', controlSquare[size], className)}
142
150
  {...props}
143
151
  >
144
- <MoreHorizontalIcon className="size-4" />
152
+ <MoreHorizontalIcon className={size === 'icon-xs' || size === 'icon-sm' ? 'size-3.5' : 'size-4'} />
145
153
  </span>
146
154
  )
147
155
  }
@@ -1,25 +1,54 @@
1
1
  import * as React from "react"
2
2
  import { Progress as ProgressPrimitive } from "radix-ui"
3
3
 
4
+ import type { UiContext } from '../../../core/ui-context.ts'
4
5
  import { cn } from '../../lib/cn.ts'
5
6
 
7
+ export type ProgressContext = UiContext
8
+
9
+ const trackClasses: Record<ProgressContext, string> = {
10
+ neutral: 'bg-context-neutral-subtle',
11
+ primary: 'bg-context-primary/20',
12
+ info: 'bg-context-info/20',
13
+ success: 'bg-context-success/20',
14
+ warning: 'bg-context-warning/20',
15
+ danger: 'bg-context-danger/20',
16
+ }
17
+
18
+ const indicatorClasses: Record<ProgressContext, string> = {
19
+ neutral: 'bg-context-neutral-emphasis',
20
+ primary: 'bg-context-primary',
21
+ info: 'bg-context-info',
22
+ success: 'bg-context-success',
23
+ warning: 'bg-context-warning',
24
+ danger: 'bg-context-danger',
25
+ }
26
+
27
+ export interface ProgressProps extends React.ComponentProps<typeof ProgressPrimitive.Root> {
28
+ /** O que o andamento comunica: `success` para meta atingida, `warning`/`danger` para atenção. */
29
+ context?: ProgressContext
30
+ }
31
+
6
32
  function Progress({
7
33
  className,
8
34
  value,
35
+ context = 'primary',
9
36
  ...props
10
- }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
37
+ }: ProgressProps) {
11
38
  return (
12
39
  <ProgressPrimitive.Root
13
40
  data-slot="progress"
41
+ data-context={context}
14
42
  className={cn(
15
- "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
43
+ "relative h-2 w-full overflow-hidden rounded-full",
44
+ trackClasses[context],
16
45
  className
17
46
  )}
18
47
  {...props}
19
48
  >
20
49
  <ProgressPrimitive.Indicator
21
50
  data-slot="progress-indicator"
22
- className="h-full w-full flex-1 bg-primary transition-all"
51
+ className={cn("h-full w-full flex-1 transition-all", indicatorClasses[context])}
23
52
  style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
24
53
  />
25
54
  </ProgressPrimitive.Root>
@@ -25,7 +25,7 @@ function RadioGroupItem({
25
25
  <RadioGroupPrimitive.Item
26
26
  data-slot="radio-group-item"
27
27
  className={cn(
28
- "aspect-square size-4 shrink-0 rounded-full border border-input text-primary transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
28
+ "aspect-square size-4 shrink-0 rounded-full border border-input text-primary transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:aria-invalid:ring-context-danger/40",
29
29
  className
30
30
  )}
31
31
  {...props}
@@ -2,6 +2,7 @@ import { GripVerticalIcon } from "lucide-react"
2
2
  import * as ResizablePrimitive from "react-resizable-panels"
3
3
 
4
4
  import { cn } from '../../lib/cn.ts'
5
+ import { focusRing } from './control.ts'
5
6
 
6
7
  function ResizablePanelGroup({
7
8
  className,
@@ -34,7 +35,8 @@ function ResizableHandle({
34
35
  <ResizablePrimitive.Separator
35
36
  data-slot="resizable-handle"
36
37
  className={cn(
37
- "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
38
+ "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
39
+ focusRing,
38
40
  className
39
41
  )}
40
42
  {...props}
@@ -25,7 +25,7 @@ import { Check, ChevronDownIcon, SearchIcon, X } from 'lucide-react'
25
25
  import { Command as CommandPrimitive } from 'cmdk'
26
26
 
27
27
  import { cn } from '../../lib/cn.ts'
28
- import type { ControlShape } from './control.ts'
28
+ import { focusRing, focusRingWithin, type ControlShape, type ControlSize } from './control.ts'
29
29
  import { CommandEmpty, CommandGroup, CommandItem, CommandList } from './command.tsx'
30
30
  import { Popover, PopoverAnchor, PopoverContent } from './popover.tsx'
31
31
 
@@ -60,7 +60,7 @@ interface SelectBaseProps {
60
60
  'aria-invalid'?: boolean
61
61
  'aria-describedby'?: string
62
62
  /** Altura: `default` (h-9, a do Input/Button) ou `sm` (h-8) pra toolbar densa. */
63
- size?: 'sm' | 'default'
63
+ size?: Extract<ControlSize, 'sm' | 'default'>
64
64
  /** Geometria do controle; `pill` preserva a variante visual escolhida. */
65
65
  shape?: ControlShape
66
66
  'aria-label'?: string
@@ -162,8 +162,8 @@ function NativeSelect({
162
162
  onChange={(e) => onChange(e.target.value)}
163
163
  className={cn(
164
164
  "h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent px-3 py-2 pr-9 text-sm transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1 data-[shape=pill]:rounded-full dark:bg-input/30 dark:hover:bg-input/50",
165
- 'focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50',
166
- 'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
165
+ focusRing,
166
+ 'aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40',
167
167
  icon ? 'pl-9' : '',
168
168
  )}
169
169
  >
@@ -359,8 +359,8 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
359
359
  className={cn(
360
360
  'flex items-center gap-1.5 rounded-md outline-none transition-[color,box-shadow] data-[shape=pill]:rounded-full',
361
361
  ghost
362
- ? 'min-h-9 px-3 py-1 text-muted-foreground hover:bg-muted hover:text-foreground focus-within:ring-2 focus-within:ring-ring/50 data-[size=sm]:min-h-8 data-[size=sm]:px-2'
363
- : "min-h-9 border border-input bg-transparent px-3 py-1 focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[size=sm]:min-h-8 data-[size=sm]:py-0.5 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
362
+ ? cn('min-h-9 px-3 py-1 text-muted-foreground hover:bg-muted hover:text-foreground data-[size=sm]:min-h-8 data-[size=sm]:px-2', focusRingWithin)
363
+ : cn('min-h-9 border border-input bg-transparent px-3 py-1 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[size=sm]:min-h-8 data-[size=sm]:py-0.5 dark:bg-input/30 dark:aria-invalid:ring-context-danger/40', focusRingWithin),
364
364
  variant === 'default' && 'flex-wrap',
365
365
  outline && 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-input/50',
366
366
  "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
@@ -2,6 +2,7 @@ import * as React from "react"
2
2
  import { Slider as SliderPrimitive } from "radix-ui"
3
3
 
4
4
  import { cn } from '../../lib/cn.ts'
5
+ import { focusRing } from './control.ts'
5
6
 
6
7
  function Slider({
7
8
  className,
@@ -51,7 +52,10 @@ function Slider({
51
52
  <SliderPrimitive.Thumb
52
53
  data-slot="slider-thumb"
53
54
  key={index}
54
- className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
55
+ className={cn(
56
+ "block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm transition-[color,box-shadow] hover:ring-[0.1875rem] hover:ring-ring/50 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
57
+ focusRing,
58
+ )}
55
59
  />
56
60
  ))}
57
61
  </SliderPrimitive.Root>
@@ -170,6 +170,9 @@ export function Toaster({
170
170
  <Sonner
171
171
  data-slot="toaster"
172
172
  theme="system"
173
+ // O sonner 1.x só expõe o rótulo da região; o do botão de fechar ("Close toast") vem
174
+ // com o 2.x (`closeButtonAriaLabel`).
175
+ containerAriaLabel="Notificações"
173
176
  className={cn('toaster group', className)}
174
177
  icons={{
175
178
  success: (
@@ -1,28 +1,25 @@
1
1
  import * as React from 'react'
2
2
  import { Loader2Icon } from 'lucide-react'
3
- import { cva, type VariantProps } from 'class-variance-authority'
4
3
  import { cn } from '../../lib/cn.ts'
4
+ import { controlGlyph, type ControlSize } from './control.ts'
5
5
 
6
- /** Tamanhos do spinner divergência da casa (o shadcn atual só tem `size-4` fixo).
7
- * Mantido aditivo pra cobrir botão (sm) e estados maiores (lg). */
8
- const spinnerVariants = cva('animate-spin', {
9
- variants: {
10
- size: { sm: 'size-3', default: 'size-4', lg: 'size-6' },
11
- },
12
- defaultVariants: { size: 'default' },
13
- })
14
-
15
- export interface SpinnerProps extends React.ComponentProps<'svg'>, VariantProps<typeof spinnerVariants> {}
6
+ export interface SpinnerProps extends React.ComponentProps<'svg'> {
7
+ /** O glifo do controle daquele tamanho na escala de control.ts (`sm` cabe num Button sm). */
8
+ size?: ControlSize | null
9
+ }
16
10
 
17
- /** Indicador de carregamento (Loader2Icon girando) — substitui o `<Loader2Icon className="animate-spin"/>`
18
- * inline repetido. Tamanhos sm/default/lg. Pra placeholder de conteúdo, use Skeleton. */
11
+ /**
12
+ * Indicador de carregamento (Loader2Icon girando) substitui o `<Loader2Icon
13
+ * className="animate-spin"/>` inline repetido. É decorativo: quem nomeia a espera é o contêiner
14
+ * (`role="status"` em DataState, PageState ou no seu) ou o texto ao lado; assim a superfície
15
+ * carrega um status só, sem role duplicado. Pra placeholder de conteúdo, use Skeleton.
16
+ */
19
17
  export function Spinner({ className, size, ...props }: SpinnerProps): React.ReactElement {
20
18
  return (
21
19
  <Loader2Icon
22
20
  data-slot="spinner"
23
- role="status"
24
- aria-label="Carregando"
25
- className={cn(spinnerVariants({ size }), className)}
21
+ aria-hidden="true"
22
+ className={cn('animate-spin', controlGlyph[size ?? 'default'], className)}
26
23
  {...props}
27
24
  />
28
25
  )
@@ -2,13 +2,16 @@ import * as React from "react"
2
2
  import { Switch as SwitchPrimitive } from "radix-ui"
3
3
 
4
4
  import { cn } from '../../lib/cn.ts'
5
+ import type { ControlSize } from './control.ts'
5
6
 
7
+ /** `size` usa os nomes da escala de control.ts; a chave é um trilho (não uma linha), então a
8
+ * medida é a do glifo daquele tamanho: `sm` acompanha o svg de um controle sm (0.875rem). */
6
9
  function Switch({
7
10
  className,
8
11
  size = "default",
9
12
  ...props
10
13
  }: React.ComponentProps<typeof SwitchPrimitive.Root> & {
11
- size?: "sm" | "default"
14
+ size?: Extract<ControlSize, "sm" | "default">
12
15
  }) {
13
16
  return (
14
17
  <SwitchPrimitive.Root
@@ -22,6 +25,7 @@ function Switch({
22
25
  >
23
26
  <SwitchPrimitive.Thumb
24
27
  data-slot="switch-thumb"
28
+ // px: o percurso do thumb desconta a borda (um pixel de cada lado) no calc do translate.
25
29
  className={cn(
26
30
  "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground"
27
31
  )}
@@ -3,11 +3,13 @@ import { cva, type VariantProps } from "class-variance-authority"
3
3
  import { Tabs as TabsPrimitive } from "radix-ui"
4
4
 
5
5
  import { cn } from '../../lib/cn.ts'
6
+ import { controlHeight, type ControlSize } from './control.ts'
6
7
 
7
8
  // Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
8
9
  // não tinha, e uma fileira densa ficava com o segmento 0.25rem mais alto
9
- // que os irmãos. O tamanho flui pra TabsList via contexto (a altura é dela).
10
- type TabsSize = "default" | "sm"
10
+ // que os irmãos. O tamanho flui pra TabsList via contexto (a altura é dela) e a medida é a
11
+ // da escala de control.ts.
12
+ type TabsSize = Extract<ControlSize, "default" | "sm">
11
13
  const TabsSizeContext = React.createContext<TabsSize>("default")
12
14
 
13
15
  function Tabs({
@@ -64,7 +66,7 @@ function TabsList({
64
66
  data-slot="tabs-list"
65
67
  data-variant={variant}
66
68
  data-size={size}
67
- className={cn(tabsListVariants({ variant }), size === "sm" ? "h-8" : "h-9", className)}
69
+ className={cn(tabsListVariants({ variant }), controlHeight[size], className)}
68
70
  {...props}
69
71
  />
70
72
  )
@@ -77,6 +79,7 @@ function TabsTrigger({
77
79
  return (
78
80
  <TabsPrimitive.Trigger
79
81
  data-slot="tabs-trigger"
82
+ // px: altura e indicador compensam a hairline do contêiner (um pixel físico, não escala).
80
83
  className={cn(
81
84
  "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
82
85
  "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
7
7
  <textarea
8
8
  data-slot="textarea"
9
9
  className={cn(
10
- "flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
10
+ "flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-context-danger/40",
11
11
  className
12
12
  )}
13
13
  {...props}
@@ -3,9 +3,13 @@ import { cva, type VariantProps } from "class-variance-authority"
3
3
  import { Toggle as TogglePrimitive } from "radix-ui"
4
4
 
5
5
  import { cn } from '../../lib/cn.ts'
6
+ import { controlHeight, focusRing } from './control.ts'
6
7
 
7
8
  const toggleVariants = cva(
8
- "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ [
10
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-context-danger/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
11
+ focusRing,
12
+ ],
9
13
  {
10
14
  variants: {
11
15
  variant: {
@@ -13,10 +17,11 @@ const toggleVariants = cva(
13
17
  outline:
14
18
  "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
15
19
  },
20
+ // Alturas da escala de control.ts (`sm` = o mesmo h-8 de Button e Select).
16
21
  size: {
17
- default: "h-9 min-w-9 px-2",
18
- sm: "h-8 min-w-8 px-1.5",
19
- lg: "h-10 min-w-10 px-2.5",
22
+ default: `${controlHeight.default} min-w-9 px-2`,
23
+ sm: `${controlHeight.sm} min-w-8 px-1.5`,
24
+ lg: `${controlHeight.lg} min-w-10 px-2.5`,
20
25
  },
21
26
  shape: {
22
27
  default: '',
@@ -46,6 +46,7 @@ function TooltipContent({
46
46
  {...props}
47
47
  >
48
48
  {children}
49
+ {/* px: a seta sobe dois pixels para encaixar na borda do conteúdo, independente da fonte. */}
49
50
  <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[0.125rem] bg-foreground fill-foreground" />
50
51
  </TooltipPrimitive.Content>
51
52
  </TooltipPrimitive.Portal>
@@ -54,7 +54,7 @@ export function ChangelogView({ source }: { source: string }): React.ReactElemen
54
54
  <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
55
55
  <h2 className="text-xl font-semibold tracking-tight">{v.version}</h2>
56
56
  {v.date !== undefined && <span className="text-sm text-muted-foreground">{v.date}</span>}
57
- {v.breaking !== undefined && <Badge context="danger" variant="solid">Breaking</Badge>}
57
+ {v.breaking !== undefined && <Badge context="danger" variant="solid">Incompatível</Badge>}
58
58
  </div>
59
59
  {v.body.length > 0 && (
60
60
  <div className="mt-3">
@@ -55,12 +55,19 @@ Monte os providers de consulta e execução uma vez na raiz do aplicativo:
55
55
  ```tsx
56
56
  /* main.tsx do app. */
57
57
  <QueryClientProvider client={queryClient}>
58
- <TbdlibProvider client={apiClient}>
58
+ <OpusProvider client={apiClient}>
59
59
  <App />
60
- </TbdlibProvider>
60
+ </OpusProvider>
61
61
  </QueryClientProvider>
62
62
  ```
63
63
 
64
+ ## useFormAction
65
+
66
+ `ActionForm` é uma composição sobre `useFormAction(action, { defaultValues, onSuccess, onError })`,
67
+ que devolve o `form` do react-hook-form já com o resolver Zod em pt-BR, `submit`, `isLoading`,
68
+ `error`, `isSuccess` e `reset`. Use o hook direto quando o formulário precisar de um layout ou de um
69
+ ciclo de vida que o pattern não cobre; validação e execução continuam iguais.
70
+
64
71
  ## Propriedades de ActionForm
65
72
 
66
73
  | Propriedade | Tipo | Padrão | Descrição |
@@ -81,11 +88,37 @@ Monte os providers de consulta e execução uma vez na raiz do aplicativo:
81
88
  | `options` | `SelectOption[]` | Opções por id de runtime — sobrepõe o fieldOptions do form e o z.enum. |
82
89
  | `className` | `string` | Classes do invólucro (ex.: `col-span-2` em uma grid). |
83
90
 
91
+ ## Ajuda na label
92
+
93
+ `FieldSpec.help` aparece como um ícone junto à label, com o texto num tooltip. Escreva ali o
94
+ critério, o efeito ou a limitação que a label não diz; ajuda que só repete a label deve ser omitida.
95
+ Não existe texto auxiliar sob o campo: quem precisa de uma frase permanente abaixo do controle usa
96
+ `FieldDescription` na composição própria.
97
+
98
+ `LabelHelp` é esse mesmo ícone, exportado para um formulário sem contrato mostrar a ajuda no mesmo
99
+ lugar:
100
+
101
+ ```tsx preview col md
102
+ <Field>
103
+ <FieldLabel htmlFor="staff" className="items-center gap-1.5">
104
+ <span>Staff</span>
105
+ <LabelHelp help="Tem acesso ao back-office e a todos os workspaces." />
106
+ </FieldLabel>
107
+ <Input id="staff" placeholder="Empresa X" />
108
+ </Field>
109
+ ```
110
+
111
+ ### Propriedades de LabelHelp
112
+
113
+ | Propriedade | Tipo | Descrição |
114
+ |---|---|---|
115
+ | `help` | `string` | O texto do tooltip. Vazio ou ausente, o ícone não renderiza. |
116
+
84
117
  ## Origem das opções de seleção
85
118
 
86
119
  As opções seguem esta precedência: `options` no campo, `fieldOptions` no formulário, `options` no
87
120
  `FieldSpec`, metadata de `t.dict` no schema e, por último, as chaves de `z.enum`. Uma fonte mais
88
- específica substitui as seguintes. Dicionários registrados no `TbdlibProvider` resolvem rótulos por
121
+ específica substitui as seguintes. Dicionários registrados no `OpusProvider` resolvem rótulos por
89
122
  referência; itens estáticos permanecem como declarados no contrato.
90
123
 
91
124
  ## Widgets declarativos
@@ -23,7 +23,7 @@ render(
23
23
  <Plus /> Workspace
24
24
  </Button>
25
25
  }
26
- emptyText="Nenhum workspace."
26
+ emptyMessage="Nenhum workspace."
27
27
  >
28
28
  {(workspaces) => (
29
29
  <div className="space-y-2">
@@ -69,5 +69,5 @@ corpo do modal.
69
69
  | `actions` | `ReactNode` | | Ação à direita da toolbar — em geral o botão de criar. |
70
70
  | `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado. |
71
71
  | `loading` | `boolean` | | Carga extra agregada à do fetch (query irmã). |
72
- | `emptyText / errorText` | `string` | | Textos dos estados (DataState). |
72
+ | `emptyMessage / errorMessage / retryLabel` | `string` | | Textos dos estados, repassados ao `ActionList` (e dele ao `DataState`). |
73
73
  | `className` | `string` | `sm:max-w-3xl` | Largura do DialogContent. |
@@ -97,6 +97,7 @@ periods: [
97
97
  { value: 'thisMonth', label: 'Este mês' },
98
98
  ]
99
99
  // Presets computáveis: today · yesterday · last7 · last30 · thisMonth · lastMonth.
100
+ // presetRange('last7') → { from: 'YYYY-MM-DD', to: 'YYYY-MM-DD' } — o mesmo cálculo do pattern.
100
101
  ```
101
102
 
102
103
  ## Paginação e carregamento
@@ -171,7 +172,7 @@ render(
171
172
  - **`depends: ['outroFiltro']`:** desabilita o filtro até que suas dependências tenham valor e o
172
173
  limpa quando uma delas muda.
173
174
  - **`options: { kind: 'dictionary', ref: 'sessionStatus' }`:** resolve opções e rótulos por um
174
- dicionário registrado no `TbdlibProvider`. `filterOptions` substitui essa fonte quando informado.
175
+ dicionário registrado no `OpusProvider`. `filterOptions` substitui essa fonte quando informado.
175
176
  - **`options: { kind: 'lookup', source: 'x.lookup' }`:** consulta uma action com `{ q }` e os valores
176
177
  das dependências. A resposta segue o formato `{ value, label }`.
177
178
 
@@ -244,6 +245,14 @@ render(
244
245
  | `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
245
246
  | `periods` | `{ value, label }[]` — o controle de período (presets + Personalizado com calendário); materializa em `from`/`to` no input. |
246
247
 
248
+ ## useListAction
249
+
250
+ Quando o layout não cabe no `ActionList`, `useListAction(action, input)` entrega o mesmo fetch
251
+ declarativo (`items`, `total`, `error`, `isLoading`, `isFetching`, `refetch`) sob o
252
+ `QueryClientProvider`, e `presetRange(preset)` materializa os presets de período (`today`, `last7`,
253
+ `thisMonth`…) em `{ from, to }` no formato `YYYY-MM-DD` — o mesmo cálculo que o pattern faz antes de
254
+ chamar a action.
255
+
247
256
  ## Propriedades de ActionList
248
257
 
249
258
  | Propriedade | Tipo | Padrão | Descrição |
@@ -258,6 +267,8 @@ render(
258
267
  | `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
259
268
  | `pageSize` | `number` | `50` | Itens por página padrão (vira `limit`/`page` no input; handler devolve `total`). O usuário troca no popover de exibição. |
260
269
  | `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado — para quem embala sincronizar com a URL. |
261
- | `emptyMessage` | `string` | `'Nenhum resultado.'` | O texto do estado vazio. |
270
+ | `emptyMessage` | `string` | `'Nenhum resultado.'` | Frase do estado vazio (o `Empty` do `DataState`). |
271
+ | `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; o botão de tentar de novo refaz a consulta. |
272
+ | `retryLabel` | `string` | `'Tentar de novo'` | Rótulo do botão de recuperação. |
262
273
  | `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
263
274
  | `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita) — apresentação de quem chama, como `cells`; cliques ali não disparam o `onRowClick`. |
@@ -37,9 +37,9 @@ confirm: {
37
37
 
38
38
  ## Ação somente com ícone
39
39
 
40
- Com `icon`, o botão exibe somente o ícone e usa `label`, ou `action.label`, no tooltip e no nome
41
- acessível. O clique não aciona o item clicável ao redor. `itemLabel` identifica o registro na
42
- mensagem de confirmação.
40
+ Com `icon`, o botão exibe somente o ícone no quadrado `icon-sm` da escala (1.75rem, a ação que mora
41
+ dentro de uma linha ou card) e usa `label`, ou `action.label`, no tooltip e no nome acessível. O clique
42
+ não aciona o item clicável ao redor. `itemLabel` identifica o registro na mensagem de confirmação.
43
43
 
44
44
  ```tsx
45
45
  <ActionTrigger
@@ -64,6 +64,13 @@ negócio `conflict`, `validation` e `not_found`:
64
64
  Erros inesperados mantêm a mensagem genérica do contrato, evitando expor detalhes de banco ou
65
65
  infraestrutura. O servidor registra o detalhe técnico para diagnóstico.
66
66
 
67
+ ## useTriggerAction
68
+
69
+ `ActionTrigger` é uma composição sobre `useTriggerAction(action, { onSuccess, onError })`, que
70
+ devolve `trigger(input)`, `isLoading`, `error` e `reset`, e invalida o cache declarado em
71
+ `action.invalidates`. Use o hook direto quando a execução parte de um gesto que não é um botão —
72
+ um atalho, um arrastar, um item de menu.
73
+
67
74
  ## Propriedades de ActionTrigger
68
75
 
69
76
  | Propriedade | Tipo | Padrão | Descrição |
@@ -71,9 +78,9 @@ infraestrutura. O servidor registra o detalhe técnico para diagnóstico.
71
78
  | `action` | `SimpleContract<TInput, TData>` | | A SimpleAction do Opus — label, messages e confirm vêm do contrato. |
72
79
  | `input` | `TInput` | | O que a action recebe — geralmente { id }. |
73
80
  | `label` | `string` | `action.label` | Sobrepõe o texto do botão. |
74
- | `variant / size` | `variants do Button` | `'default' (ou 'destructive' se o confirm declarar) / 'default'` | Visual do botão o destructive do ConfirmSpec escolhe sozinho. |
81
+ | `context / variant / size` | `do Button` | gatilho: `primary`/`solid`; `danger` quando a action é `destructive`; no modo ícone, `ghost` e `neutral` (ou `danger` se destrutiva) / `default` | Visual do gatilho; `context` explícito vence. O botão de confirmar é sempre `solid`: `danger` quando a action é `destructive`, senão a prop `context` do gatilho (ou `primary`). |
75
82
  | `confirm` | `{ title, description?, actionLabel?, cancelLabel? }` | | Confirmação via prop — sobrepõe o ConfirmSpec do contrato. |
76
83
  | `onSuccess` | `(data: TData) => void` | | Pós-sucesso (cache já invalidado pelo action.invalidates). |
77
84
  | `icon` | `React.ReactNode` | | Torna o botão icon-only: rótulo no tooltip e no `aria-label`, clique que não vaza para o item. |
78
85
  | `itemLabel` | `string` | | Nome do alvo na pergunta (sai entre aspas, em destaque, antes da mensagem do contrato). |
79
- | `className` | `string` | | Classes do botão (ex.: apertar o tamanho em uma linha densa). |
86
+ | `className` | `string` | | Classes do botão. O tamanho vem de `size` (escala única); com `icon` é sempre `icon-sm`. |
@@ -21,7 +21,7 @@ render(
21
21
  <div className="space-y-1.5">
22
22
  <div className="flex items-center gap-2">
23
23
  <h3 className="text-sm font-semibold">{ws.name}</h3>
24
- <Badge variant={ws.status === 'active' ? 'success' : 'warning'}>{ws.status}</Badge>
24
+ <Badge context={ws.status === 'active' ? 'success' : 'warning'}>{ws.status}</Badge>
25
25
  </div>
26
26
  <p className="text-sm text-muted-foreground">
27
27
  Cliente {ws.client} · {ws.agents} agentes vinculados.
@@ -35,6 +35,12 @@ render(
35
35
  )
36
36
  ```
37
37
 
38
+ ## useViewAction
39
+
40
+ `ActionView` é uma composição sobre `useViewAction(action, input)`, que devolve `data`, `error`,
41
+ `isLoading` e `refetch` sob o `QueryClientProvider`. Use o hook direto quando o recurso alimenta
42
+ mais de uma região da tela ou quando o carregamento precisa ser orquestrado por outro componente.
43
+
38
44
  ## Propriedades de ActionView
39
45
 
40
46
  | Propriedade | Tipo | Padrão | Descrição |
@@ -43,5 +49,7 @@ render(
43
49
  | `input` | `TInput` | | Geralmente { id } — mudou, recarrega. |
44
50
  | `children` | `(data: TData, refetch) => ReactNode` | | Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
45
51
  | `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. |
46
- | `loading / empty` | `ReactNode` | `3 skeletons / nada` | Sobrescreve os estados padrão quando o contexto pedir. |
47
- | `error` | `(err, retry) => ReactNode` | `mensagem + Tentar de novo` | Sobrescreve o estado de erro padrão. |
52
+ | `loading` | `ReactNode \| boolean` | `3 skeletons` | Sobrescreve o carregamento: um próprio, `true` para o padrão ou `false` para não renderizar nada enquanto carrega. |
53
+ | `empty` | `ReactNode` | `emptyMessage` ou nada | Sobrescreve o estado vazio (200 sem dado). |
54
+ | `emptyMessage` | `string` | | Atalho do vazio: a frase na mesma superfície de `DataState` (`Empty` com moldura sólida). |
55
+ | `error` | `(err, retry) => ReactNode` | `"Não foi possível carregar" + Tentar de novo` | Sobrescreve o estado de erro padrão. Sem ele, a frase do servidor só aparece quando é legível pela pessoa (`conflict`, `validation`, `not_found`, `authorization`, `authentication`); código técnico nunca vira título, e o botão de tentar de novo some quando o problema é de permissão ou sessão. |
@@ -29,3 +29,14 @@ render(
29
29
  ```
30
30
 
31
31
  Cada pergunta exige pelo menos uma opção válida selecionada **ou** texto livre não vazio. `busy` mostra o progresso no botão e trava os controles; `disabled` apenas trava a interação. A ordem de `answers` acompanha a ordem de `questions`, e o `header` de cada resposta é derivado de `question.header` (ou do próprio enunciado quando o header estiver vazio).
32
+
33
+ ## Propriedades de Ask
34
+
35
+ | Propriedade | Tipo | Padrão | Descrição |
36
+ |---|---|---|---|
37
+ | `questions` | `AskQuestion[]` | | Uma a quatro perguntas do contrato de elicitação; fora dessa faixa o envio é bloqueado. |
38
+ | `answers` | `AskAnswer[]` | | Respostas controladas, na mesma ordem de `questions`; entradas ausentes contam como vazias. |
39
+ | `onChange` | `(answers: AskAnswer[]) => void` | | Recebe a lista completa a cada alteração. |
40
+ | `onSubmit` | `(answers: AskAnswer[]) => void` | | Dispara somente quando toda pergunta tem uma opção ou texto livre. |
41
+ | `disabled` | `boolean` | `false` | Trava as opções e o envio. |
42
+ | `busy` | `boolean` | `false` | Envio em andamento: o botão mostra o spinner e desabilita. |