@softize/opus 18.0.1 → 18.1.1

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 (129) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/PROMOTED.md +4 -5
  3. package/README.md +5 -4
  4. package/bin/cli.mjs +4 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +3 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
  8. package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
  9. package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
  10. package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +54 -0
  11. package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
  12. package/docs/adr/0019-productive-surfaces-use-compact-density.md +65 -0
  13. package/docs/code-style.md +2 -2
  14. package/docs/consumer-upgrade-propagation.md +1 -1
  15. package/docs/data-products.md +5 -3
  16. package/docs/protocol.md +6 -6
  17. package/docs/relative-unit-scale.md +9 -2
  18. package/docs/releasing.md +28 -4
  19. package/package.json +1 -1
  20. package/registry/skills/build-opus-ui/references/evaluations.md +3 -1
  21. package/registry/skills/build-opus-ui/references/ui-patterns.md +20 -4
  22. package/src/auth/drivers/jwt.ts +2 -1
  23. package/src/core/runtime.ts +32 -5
  24. package/src/core/types.ts +16 -7
  25. package/src/mcp/index.ts +13 -1
  26. package/src/ui/components/patterns/action-list-dialog.tsx +2 -2
  27. package/src/ui/components/patterns/content-header.tsx +2 -2
  28. package/src/ui/components/patterns/form-dialog.tsx +7 -2
  29. package/src/ui/components/patterns/form.tsx +1 -1
  30. package/src/ui/components/patterns/list.tsx +239 -47
  31. package/src/ui/components/patterns/presentation.tsx +7 -5
  32. package/src/ui/components/patterns/sidebar.tsx +1 -1
  33. package/src/ui/components/patterns/state-surface.tsx +2 -2
  34. package/src/ui/components/patterns/surface-header.tsx +4 -4
  35. package/src/ui/components/primitives/alert.tsx +2 -2
  36. package/src/ui/components/primitives/breadcrumb.tsx +1 -1
  37. package/src/ui/components/primitives/button-group.tsx +1 -1
  38. package/src/ui/components/primitives/button.tsx +3 -3
  39. package/src/ui/components/primitives/calendar.tsx +1 -1
  40. package/src/ui/components/primitives/card.tsx +1 -1
  41. package/src/ui/components/primitives/close-button.tsx +40 -0
  42. package/src/ui/components/primitives/detail.tsx +68 -30
  43. package/src/ui/components/primitives/dialog.tsx +36 -21
  44. package/src/ui/components/primitives/drawer.tsx +26 -19
  45. package/src/ui/components/primitives/empty-value.tsx +3 -3
  46. package/src/ui/components/primitives/empty.tsx +1 -1
  47. package/src/ui/components/primitives/field.tsx +12 -12
  48. package/src/ui/components/primitives/icon-picker.tsx +1 -1
  49. package/src/ui/components/primitives/input-group.tsx +1 -1
  50. package/src/ui/components/primitives/input.tsx +2 -2
  51. package/src/ui/components/primitives/item.tsx +5 -5
  52. package/src/ui/components/primitives/pagination.tsx +4 -4
  53. package/src/ui/components/primitives/radio-group.tsx +1 -1
  54. package/src/ui/components/primitives/select.tsx +3 -3
  55. package/src/ui/components/primitives/table.tsx +26 -17
  56. package/src/ui/components/primitives/tabs.tsx +80 -23
  57. package/src/ui/components/primitives/textarea.tsx +1 -1
  58. package/src/ui/components/primitives/toggle-group.tsx +9 -2
  59. package/src/ui/docs/content/action-form-dialog.md +11 -4
  60. package/src/ui/docs/content/action-form.md +13 -3
  61. package/src/ui/docs/content/action-list-dialog.md +5 -7
  62. package/src/ui/docs/content/action-list.md +53 -5
  63. package/src/ui/docs/content/action-trigger.md +9 -5
  64. package/src/ui/docs/content/action-view.md +12 -8
  65. package/src/ui/docs/content/actions.md +36 -13
  66. package/src/ui/docs/content/ai.md +26 -7
  67. package/src/ui/docs/content/alert.md +6 -3
  68. package/src/ui/docs/content/aspect-ratio.md +2 -2
  69. package/src/ui/docs/content/auth.md +25 -10
  70. package/src/ui/docs/content/avatar.md +1 -1
  71. package/src/ui/docs/content/badge.md +2 -2
  72. package/src/ui/docs/content/breadcrumb.md +3 -2
  73. package/src/ui/docs/content/button.md +33 -8
  74. package/src/ui/docs/content/calendar.md +1 -1
  75. package/src/ui/docs/content/card.md +1 -1
  76. package/src/ui/docs/content/carousel.md +14 -3
  77. package/src/ui/docs/content/chat.md +1 -1
  78. package/src/ui/docs/content/cli.md +13 -7
  79. package/src/ui/docs/content/command.md +34 -2
  80. package/src/ui/docs/content/composer.md +1 -1
  81. package/src/ui/docs/content/content.md +5 -4
  82. package/src/ui/docs/content/customization.md +12 -2
  83. package/src/ui/docs/content/cycle.md +7 -5
  84. package/src/ui/docs/content/data-state.md +6 -5
  85. package/src/ui/docs/content/data.md +3 -3
  86. package/src/ui/docs/content/detail.md +12 -10
  87. package/src/ui/docs/content/dialog.md +14 -7
  88. package/src/ui/docs/content/dictionary-value.md +1 -1
  89. package/src/ui/docs/content/dock.md +23 -2
  90. package/src/ui/docs/content/dot.md +0 -2
  91. package/src/ui/docs/content/drawer.md +7 -4
  92. package/src/ui/docs/content/empty-value.md +4 -4
  93. package/src/ui/docs/content/empty.md +1 -4
  94. package/src/ui/docs/content/events.md +1 -1
  95. package/src/ui/docs/content/field.md +21 -12
  96. package/src/ui/docs/content/getting-started.md +4 -2
  97. package/src/ui/docs/content/icon-picker.md +2 -2
  98. package/src/ui/docs/content/input-otp.md +2 -0
  99. package/src/ui/docs/content/input.md +2 -3
  100. package/src/ui/docs/content/item.md +6 -3
  101. package/src/ui/docs/content/kbd.md +2 -1
  102. package/src/ui/docs/content/mcp.md +10 -4
  103. package/src/ui/docs/content/menu.md +27 -0
  104. package/src/ui/docs/content/page.md +20 -6
  105. package/src/ui/docs/content/pagination.md +9 -2
  106. package/src/ui/docs/content/popover.md +2 -2
  107. package/src/ui/docs/content/presentation.md +48 -47
  108. package/src/ui/docs/content/progress.md +2 -6
  109. package/src/ui/docs/content/runtime.md +8 -5
  110. package/src/ui/docs/content/scheduler.md +1 -1
  111. package/src/ui/docs/content/select.md +13 -8
  112. package/src/ui/docs/content/sidebar.md +3 -2
  113. package/src/ui/docs/content/skeleton.md +1 -1
  114. package/src/ui/docs/content/slider.md +4 -4
  115. package/src/ui/docs/content/spinner.md +3 -3
  116. package/src/ui/docs/content/tabs.md +22 -12
  117. package/src/ui/docs/content/testing.md +4 -2
  118. package/src/ui/docs/content/toast.md +5 -6
  119. package/src/ui/docs/content/toggle.md +37 -0
  120. package/src/ui/docs/content/tokens.md +45 -2
  121. package/src/ui/docs/content/tooltip.md +4 -3
  122. package/src/ui/docs/content/truncate.md +3 -2
  123. package/src/ui/docs/content/ui.md +3 -1
  124. package/src/ui/docs/content/upgrading.md +43 -13
  125. package/src/ui/docs/doc-client.tsx +1 -1
  126. package/src/ui/docs/registry.tsx +30 -5
  127. package/src/ui/meta.ts +4 -4
  128. package/src/ui/react.tsx +1 -0
  129. package/src/ui/theme.css +3 -0
@@ -92,7 +92,7 @@ interface SelectCustomBase extends SelectBaseProps {
92
92
  clearable?: boolean
93
93
  /** `outline`: compacto com borda; `ghost`: compacto sem borda nem fundo. */
94
94
  variant?: 'default' | 'outline' | 'ghost'
95
- /** Placeholder enquanto se digita a busca (cai pro `placeholder`). */
95
+ /** Placeholder da linha de busca dentro da lista. Default: `'Buscar…'`. */
96
96
  searchPlaceholder?: string
97
97
  emptyText?: string
98
98
  /** Ação custom no FIM do campo, dentro do controle (antes do chevron) — ex.: um botão que
@@ -161,7 +161,7 @@ function NativeSelect({
161
161
  aria-describedby={rest['aria-describedby']}
162
162
  onChange={(e) => onChange(e.target.value)}
163
163
  className={cn(
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",
164
+ "h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-muted/30 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
165
  focusRing,
166
166
  'aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40',
167
167
  icon ? 'pl-9' : '',
@@ -360,7 +360,7 @@ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.Rea
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
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),
363
+ : cn('min-h-9 border border-input bg-muted/30 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,36 +2,45 @@ import * as React from "react"
2
2
 
3
3
  import { cn } from '../../lib/cn.ts'
4
4
 
5
+ type TableVariant = 'plain' | 'framed'
6
+ const TableVariantContext = React.createContext<TableVariant>('plain')
7
+
5
8
  export interface TableProps extends React.ComponentProps<"table"> {
6
9
  /** `framed` aplica a moldura canônica de datagrid no contêiner da tabela. */
7
- variant?: 'plain' | 'framed'
10
+ variant?: TableVariant
8
11
  }
9
12
 
10
13
  function Table({ className, variant = 'plain', ...props }: TableProps) {
11
14
  return (
12
- <div
13
- data-slot="table-container"
14
- data-variant={variant}
15
- className={cn(
16
- "relative w-full overflow-x-auto",
17
- variant === 'framed' &&
18
- 'rounded-lg border border-border [&_[data-slot=table-header]]:bg-muted/20',
19
- )}
20
- >
21
- <table
22
- data-slot="table"
23
- className={cn("w-full caption-bottom text-sm", className)}
24
- {...props}
25
- />
26
- </div>
15
+ <TableVariantContext.Provider value={variant}>
16
+ <div
17
+ data-slot="table-container"
18
+ data-variant={variant}
19
+ className={cn(
20
+ "relative w-full overflow-x-auto",
21
+ variant === 'framed' && 'rounded-lg border border-border',
22
+ )}
23
+ >
24
+ <table
25
+ data-slot="table"
26
+ className={cn("w-full caption-bottom text-sm", className)}
27
+ {...props}
28
+ />
29
+ </div>
30
+ </TableVariantContext.Provider>
27
31
  )
28
32
  }
29
33
 
30
34
  function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
35
+ const variant = React.useContext(TableVariantContext)
31
36
  return (
32
37
  <thead
33
38
  data-slot="table-header"
34
- className={cn("[&_tr]:border-b", className)}
39
+ className={cn(
40
+ "[&_tr]:border-b",
41
+ variant === 'framed' && 'bg-muted/30 dark:bg-input/30',
42
+ className,
43
+ )}
35
44
  {...props}
36
45
  />
37
46
  )
@@ -11,6 +11,9 @@ import { controlHeight, type ControlSize } from './control.ts'
11
11
  // da escala de control.ts.
12
12
  type TabsSize = Extract<ControlSize, "default" | "sm">
13
13
  const TabsSizeContext = React.createContext<TabsSize>("default")
14
+ type TabsOrientation = "horizontal" | "vertical"
15
+ const TabsOrientationContext = React.createContext<TabsOrientation>("horizontal")
16
+ const TabsListVariantContext = React.createContext<"default" | "line">("default")
14
17
 
15
18
  function Tabs({
16
19
  className,
@@ -19,19 +22,21 @@ function Tabs({
19
22
  ...props
20
23
  }: React.ComponentProps<typeof TabsPrimitive.Root> & { size?: TabsSize }) {
21
24
  return (
22
- <TabsSizeContext.Provider value={size}>
23
- <TabsPrimitive.Root
24
- data-slot="tabs"
25
- data-orientation={orientation}
26
- data-size={size}
27
- orientation={orientation}
28
- className={cn(
29
- "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
30
- className
31
- )}
32
- {...props}
33
- />
34
- </TabsSizeContext.Provider>
25
+ <TabsOrientationContext.Provider value={orientation}>
26
+ <TabsSizeContext.Provider value={size}>
27
+ <TabsPrimitive.Root
28
+ data-slot="tabs"
29
+ data-orientation={orientation}
30
+ data-size={size}
31
+ orientation={orientation}
32
+ className={cn(
33
+ "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ </TabsSizeContext.Provider>
39
+ </TabsOrientationContext.Provider>
35
40
  )
36
41
  }
37
42
 
@@ -44,7 +49,7 @@ const tabsListVariants = cva(
44
49
  variants: {
45
50
  variant: {
46
51
  default: "bg-muted",
47
- line: "gap-1 bg-transparent",
52
+ line: "gap-5 bg-transparent",
48
53
  },
49
54
  },
50
55
  defaultVariants: {
@@ -55,27 +60,66 @@ const tabsListVariants = cva(
55
60
 
56
61
  function TabsList({
57
62
  className,
63
+ children,
64
+ style,
58
65
  variant = "default",
59
66
  ...props
60
67
  }: React.ComponentProps<typeof TabsPrimitive.List> &
61
68
  VariantProps<typeof tabsListVariants>) {
62
69
  // O size vem do <Tabs> (contexto) — a altura mora aqui, na list.
63
70
  const size = React.useContext(TabsSizeContext)
71
+ const orientation = React.useContext(TabsOrientationContext)
72
+ const normalizedVariant = variant ?? "default"
73
+ const hasHorizontalLine = normalizedVariant === "line" && orientation === "horizontal"
64
74
  return (
65
- <TabsPrimitive.List
66
- data-slot="tabs-list"
67
- data-variant={variant}
68
- data-size={size}
69
- className={cn(tabsListVariants({ variant }), controlHeight[size], className)}
70
- {...props}
71
- />
75
+ <TabsListVariantContext.Provider value={normalizedVariant}>
76
+ <TabsPrimitive.List
77
+ data-slot="tabs-list"
78
+ data-variant={normalizedVariant}
79
+ data-size={size}
80
+ className={cn(tabsListVariants({ variant: normalizedVariant }), controlHeight[size], className)}
81
+ style={{
82
+ ...(hasHorizontalLine
83
+ ? { position: "relative", width: "100%", height: "auto", justifyContent: "flex-start" }
84
+ : {}),
85
+ ...style,
86
+ }}
87
+ {...props}
88
+ >
89
+ {hasHorizontalLine && (
90
+ <span
91
+ aria-hidden="true"
92
+ data-slot="tabs-line"
93
+ style={{
94
+ position: "absolute",
95
+ insetInline: "calc(var(--opus-inline-gutter, 0rem) * -1)",
96
+ bottom: 0,
97
+ borderBottomWidth: "0.0625rem",
98
+ borderBottomStyle: "solid",
99
+ borderBottomColor: "var(--border)",
100
+ pointerEvents: "none",
101
+ }}
102
+ />
103
+ )}
104
+ {children}
105
+ </TabsPrimitive.List>
106
+ </TabsListVariantContext.Provider>
72
107
  )
73
108
  }
74
109
 
75
110
  function TabsTrigger({
76
111
  className,
112
+ style,
113
+ icon,
114
+ children,
77
115
  ...props
78
- }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
116
+ }: React.ComponentProps<typeof TabsPrimitive.Trigger> & {
117
+ /** Ícone decorativo antes do rótulo. Em uma aba sem texto, declare também `aria-label`. */
118
+ icon?: React.ReactNode
119
+ }) {
120
+ const variant = React.useContext(TabsListVariantContext)
121
+ const orientation = React.useContext(TabsOrientationContext)
122
+ const hasHorizontalLine = variant === "line" && orientation === "horizontal"
79
123
  return (
80
124
  <TabsPrimitive.Trigger
81
125
  data-slot="tabs-trigger"
@@ -85,10 +129,23 @@ function TabsTrigger({
85
129
  "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",
86
130
  "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:text-foreground",
87
131
  "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-1px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
132
+ hasHorizontalLine && "px-0 py-3",
88
133
  className
89
134
  )}
135
+ style={{ ...(hasHorizontalLine ? { flex: "none", height: "auto" } : {}), ...style }}
90
136
  {...props}
91
- />
137
+ >
138
+ {icon !== undefined && (
139
+ <span
140
+ data-slot="tabs-trigger-icon"
141
+ aria-hidden="true"
142
+ className="flex size-4 shrink-0 items-center justify-center [&_svg]:size-4"
143
+ >
144
+ {icon}
145
+ </span>
146
+ )}
147
+ {children}
148
+ </TabsPrimitive.Trigger>
92
149
  )
93
150
  }
94
151
 
@@ -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-context-danger aria-invalid:ring-context-danger/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-context-danger/40",
10
+ "flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-muted/30 px-3 py-2 text-sm 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 dark:bg-input/30 dark:aria-invalid:ring-context-danger/40",
11
11
  className
12
12
  )}
13
13
  {...props}
@@ -18,6 +18,12 @@ const ToggleGroupContext = React.createContext<
18
18
  shape: "default",
19
19
  })
20
20
 
21
+ type ToggleGroupItemProps = React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
22
+ VariantProps<typeof toggleVariants> & {
23
+ 'data-slot'?: string
24
+ 'data-state'?: string
25
+ }
26
+
21
27
  function ToggleGroup({
22
28
  className,
23
29
  variant,
@@ -58,9 +64,10 @@ function ToggleGroupItem({
58
64
  variant,
59
65
  size,
60
66
  shape,
67
+ 'data-slot': _composedSlot,
68
+ 'data-state': _composedState,
61
69
  ...props
62
- }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
63
- VariantProps<typeof toggleVariants>) {
70
+ }: ToggleGroupItemProps) {
64
71
  const context = React.useContext(ToggleGroupContext)
65
72
 
66
73
  return (
@@ -3,8 +3,11 @@
3
3
  Use `ActionFormDialog` quando o formulário precisar interromper o fluxo atual sem levar a pessoa
4
4
  para outra página. O consumidor controla `open`; depois de uma execução bem-sucedida, o componente
5
5
  fecha o modal. O cabeçalho e o rodapé permanecem visíveis enquanto os campos podem rolar.
6
- Quando cancelamento, o rodapé usa um `ButtonGroup` dividido: `Cancelar` aparece em `outline` e
7
- a ação principal permanece `solid`, ambas no tamanho normal de uma decisão modal.
6
+ O rodapé agrupa as decisões em um `ButtonGroup` cuja largura acompanha o conteúdo: `Cancelar`
7
+ aparece em `ghost` e a ação principal permanece `solid`, ambas no tamanho normal de uma decisão
8
+ modal. `Cancelar` está sempre presente; sem `onCancel` próprio, ele fecha o modal.
9
+ Com `footerDistribution="equal"`, as duas decisões dividem a faixa e `Cancelar` passa a `outline`,
10
+ salvo escolha explícita em `cancelVariant`.
8
11
 
9
12
  ```tsx preview
10
13
  const [open, setOpen] = useState(false);
@@ -26,8 +29,12 @@ render(
26
29
 
27
30
  | Propriedade | Tipo | Padrão | Descrição |
28
31
  | --------------------- | ------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
- | `open / onOpenChange` | `boolean / (open: boolean) => void` | | Estado controlado do modal. `onOpenChange(false)` é chamado no sucesso e ao cancelar. |
32
+ | `open / onOpenChange` | `boolean / (open: boolean) => void` | | Estado controlado do modal. `onOpenChange(false)` é chamado no sucesso e, quando não há `onCancel` próprio, ao cancelar. |
30
33
  | `title` | `string` | | Nome da tarefa exibido no cabeçalho. |
31
34
  | `intro` | `ReactNode` | | Contexto relevante no início do corpo, como texto ou `Alert`. |
32
35
  | `submitLabel` | `string` | `'Salvar'` | Resultado da ação principal. Preserve o padrão em criação e edição comuns; sobrescreva apenas quando a operação tiver outro efeito, como `Renomear` ou `Criar nova versão`. |
33
- | `…ActionFormProps` | `action, defaultValues, onSuccess, fieldOptions…` | | Demais propriedades repassadas ao `ActionForm` interno. |
36
+ | `footerDistribution` | `'content' \| 'equal'` | `'content'` | Mantém a largura das ações pelo conteúdo ou divide a faixa igualmente. |
37
+ | `cancelVariant` | `'ghost' \| 'outline'` | `'ghost'`; `'outline'` com `equal` | Tratamento do `Cancelar`. O valor explícito vence o default derivado de `footerDistribution`. |
38
+ | `onCancel` | `() => void` | fecha o modal | Substitui o fechamento padrão do `Cancelar`; nesse caso, o consumidor decide quando chamar `onOpenChange(false)`. |
39
+ | `onSuccess` | `(data: TData) => void` | | Executado depois do sucesso, antes de o componente fechar o modal. |
40
+ | `…ActionFormProps` | `action, defaultValues, fieldOptions…` | | Demais propriedades repassadas ao `ActionForm` interno. `className`, `body` e `footer` não são repassadas: o wrapper as substitui para montar o corpo rolável e o footer do modal. |
@@ -34,7 +34,8 @@ invalidação continuam sob responsabilidade de `ActionForm`.
34
34
 
35
35
  ## Valores iniciais e rótulos
36
36
 
37
- `defaultValues` preenche o formulário para edição. `submitLabel` e `cancelLabel` nomeiam as ações;
37
+ `defaultValues` preenche o formulário para edição. Criação e edição comuns mantêm o rótulo padrão
38
+ `Salvar`; use `submitLabel` somente quando a operação pedir outro verbo, como `Renomear`.
38
39
  `onCancel` devolve ao consumidor a decisão de fechar um painel ou navegar para outra página.
39
40
 
40
41
  ```tsx preview col md
@@ -42,12 +43,19 @@ invalidação continuam sob responsabilidade de `ActionForm`.
42
43
  <ActionForm
43
44
  action={docWorkspaceCreate}
44
45
  defaultValues={{ name: "Empresa X", status: "active" }}
45
- submitLabel="Atualizar"
46
46
  onCancel={() => undefined}
47
47
  />
48
48
  </DocBrowserActionProvider>
49
49
  ```
50
50
 
51
+ ## Formulário modal
52
+
53
+ [`ActionFormDialog`](/ui/action-form-dialog) acrescenta a moldura, o corpo rolável e o footer ao formulário. Por padrão, as
54
+ ações ficam alinhadas no fim da faixa e preservam a largura do conteúdo; `Cancelar` usa `ghost`.
55
+ Use `footerDistribution="equal"` somente quando as duas decisões precisarem do mesmo peso visual.
56
+ Nesse caso, o cancelamento muda para `outline`, salvo escolha explícita em `cancelVariant`.
57
+ As propriedades do wrapper estão documentadas na página dele.
58
+
51
59
  ## Pré-requisitos
52
60
 
53
61
  Monte os providers de consulta e execução uma vez na raiz do aplicativo:
@@ -75,7 +83,9 @@ ciclo de vida que o pattern não cobre; validação e execução continuam iguai
75
83
  | `action` | `FormContract<TInput, TData>` | | O contrato da FormAction — dele saem campos (input Zod + fields), mensagens e invalidação de cache. |
76
84
  | `defaultValues` | `Partial<TInput>` | | Valores iniciais — o modo edição de um update/patch. |
77
85
  | `onSuccess` | `(data: TData) => void` | | Pós-sucesso (o toast e a invalidação de cache já aconteceram). |
78
- | `submitLabel / cancelLabel / cancelVariant / onCancel` | `string / string / 'ghost' \| 'outline' / () => void` | `'Salvar' / 'Cancelar' / 'ghost'` | Rodapé do form — o Cancelar só aparece com onCancel. Use `outline` quando ele dividir o footer com a ação principal. |
86
+ | `submitLabel / cancelLabel / cancelVariant / onCancel` | `string / string / 'ghost' \| 'outline' / () => void` | `'Salvar' / 'Cancelar' / 'ghost'` | Rodapé do form — o Cancelar só aparece com onCancel. Fora de um modal, mantenha `ghost`; o `ActionFormDialog` usa `outline` somente com `footerDistribution="equal"`. |
87
+ | `disabled` | `boolean` | `false` | Bloqueia campos e ações sem desmontar o formulário. |
88
+ | `onLoadingChange` | `(loading: boolean) => void` | | Informa início e fim da execução a quem coordena ações como um grupo. |
79
89
  | `fieldOptions` | `Record<string, SelectOption[]>` | | Opções de runtime por campo (ex.: ids de skills) — sobrepõe as inferidas do z.enum. |
80
90
  | `className / body / footer` | `string / (node) => node / (node) => node` | | className = classes do `<form>`. body/footer = slots: recebem os campos / os botões e escolhem o invólucro — é como o ActionFormDialog injeta DialogBody/DialogFooter (scroll + faixa). |
81
91
  | `children` | `ReactNode` | | Modo composição: diagrame com `<ActionFormField name />`. Sem children, o automático monta todos os campos na ordem do contrato. |
@@ -18,9 +18,7 @@ render(
18
18
  onOpenChange={setOpen}
19
19
  title="Workspaces"
20
20
  actions={
21
- <Button size="sm" variant="outline">
22
- <Plus /> Workspace
23
- </Button>
21
+ <Button>Criar workspace</Button>
24
22
  }
25
23
  emptyMessage="Nenhum workspace."
26
24
  >
@@ -65,14 +63,14 @@ corpo do modal.
65
63
 
66
64
  | Propriedade | Tipo | Padrão | Descrição |
67
65
  | ------------------------------------------ | ----------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------- |
68
- | `action / input` | `ListAction / TInput` | | O contrato e os filtros — mudou o input, re-busca; `invalidates` de forms/triggers refaz sozinho. |
66
+ | `action / input` | `ListAction / TInput` | `input`: `{}` | O contrato e o escopo base opcional da consulta — mudou o input, re-busca; `invalidates` de forms/triggers refaz sozinho. |
69
67
  | `open / onOpenChange` | `boolean / (open) => void` | | Controle do modal — de quem orquestra. |
70
68
  | `title` | `string` | | Nome da lista exibido no cabeçalho fixo. |
71
69
  | `intro` | `ReactNode` | | Contexto relevante no início do corpo, como texto ou `Alert`. |
72
70
  | `children` | `(items, refetch) => ReactNode` | | O layout dos itens (cards/linhas) — os estados já saíram daqui. |
73
- | `note` | `ReactNode \| (items) => ReactNode` | `"N no total"` | Nota à esquerda da toolbar o default é derivado dos itens. |
71
+ | `note` | `ReactNode` | | Contexto curto à esquerda da toolbar. O total de itens aparece no rodapé do `ActionList`. |
74
72
  | `actions` | `ReactNode` | | Ação à direita da toolbar — em geral o botão de criar. |
75
73
  | `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado. |
76
74
  | `loading` | `boolean` | | Carga extra agregada à do fetch (query irmã). |
77
- | `emptyMessage / errorMessage / retryLabel` | `string` | | Textos dos estados; `retryLabel` nomeia a ação somente com ícone e seu tooltip. |
78
- | `className` | `string` | `sm:max-w-3xl` | Largura do DialogContent. |
75
+ | `emptyMessage / errorMessage / retryLabel` | `string` | `'Nenhum resultado.'` / `'Não foi possível carregar.'` / `'Tentar de novo'` | Textos dos estados, herdados do `ActionList`; `retryLabel` nomeia a ação somente com ícone e seu tooltip. |
76
+ | `className` | `string` | `max-w-3xl` | Largura do DialogContent. |
@@ -64,6 +64,44 @@ Seletores inline mantêm largura previsível (`w-40`; lookup e múltiplo usam `w
64
64
  são truncados no controle, mas permanecem completos na lista. No modal de filtros avançados, o
65
65
  seletor ocupa toda a largura. Busca, filtros e período aparecem somente quando declarados.
66
66
 
67
+ ## Onde cada filtro aparece
68
+
69
+ Cada filtro declara `placement`. `inline`, o padrão, mantém o controle na barra; `advanced` o leva
70
+ ao modal “Filtros”; `external` mantém o filtro no estado navegável e no input, mas deixa a
71
+ apresentação para o consumidor, como cards de status acima da lista. No modal, filtros com a mesma
72
+ `section` formam um grupo, e `advancedFilters.columns` distribui os grupos em até três colunas.
73
+ Campos de data avançados usam calendário. A antiga `advanced: true` continua aceita durante a
74
+ migração e equivale a `placement: 'advanced'`.
75
+
76
+ ```tsx
77
+ // No contrato:
78
+ filters: {
79
+ status: {
80
+ label: 'Status',
81
+ type: 'select',
82
+ placement: 'inline',
83
+ options: { kind: 'dictionary', ref: 'workspace.status' },
84
+ },
85
+ client: {
86
+ label: 'Cliente',
87
+ type: 'lookup',
88
+ placement: 'advanced',
89
+ section: 'Relacionamento',
90
+ options: { kind: 'lookup', source: 'client.lookup' },
91
+ },
92
+ createdAt: { label: 'Criado em', type: 'date', placement: 'advanced', section: 'Datas' },
93
+ stage: {
94
+ label: 'Estágio',
95
+ type: 'select',
96
+ placement: 'external',
97
+ options: { kind: 'dictionary', ref: 'workspace.stage' },
98
+ },
99
+ }
100
+
101
+ // Na tela:
102
+ <ActionList action={workspaceList} input={{}} advancedFilters={{ columns: 2 }} />
103
+ ```
104
+
67
105
  ## Colunas de dicionário
68
106
 
69
107
  Quando o campo de saída usa `t.dict().zod()`, a coluna apresenta o valor com `DictionaryValue` e
@@ -88,6 +126,10 @@ Renderer customizado reutiliza `EmptyValue`.
88
126
 
89
127
  Use `cells` somente quando uma coluna precisar de apresentação própria, como link, composição ou
90
128
  ação. A chave corresponde à `key` da coluna. Valores de dicionário não precisam desse override.
129
+ O valor principal preserva o `text-sm` e o foreground da tabela; use
130
+ `text-xs text-muted-foreground` apenas em metadado subordinado a outro valor na mesma célula.
131
+ Cabeçalhos e `EmptyValue` continuam muted. O fato de uma coluna ser técnica, temporal ou menos
132
+ destacada não reduz nem atenua seu valor principal.
91
133
 
92
134
  ```tsx preview col
93
135
  render(
@@ -291,6 +333,7 @@ render(
291
333
  | `state` | `ActionFilterState` | | Estado atual da barra. |
292
334
  | `onStateChange` | `(next) => void` | | Recebe o estado completo depois de cada alteração. |
293
335
  | `filterOptions` | `Record<string, SelectOption[]>` | | Fornece opções de runtime para filtros select e lookup. |
336
+ | `advancedFilters` | `{ columns?: 1 \| 2 \| 3 }` | `{ columns: 1 }` | Define as colunas do diálogo de filtros avançados; o Opus deriva a largura correspondente. |
294
337
  | `onRefresh` | `() => Promise<void> \| void` | | Exibe a ação de recarregar e executa a consulta do consumidor. |
295
338
  | `refreshing` | `boolean` | `false` | Desabilita e anima a ação de recarregar durante a consulta. |
296
339
  | `controls` | `ReactNode` | | Controles auxiliares agrupados com recarregar em um `ButtonGroup` espaçado. |
@@ -300,11 +343,11 @@ render(
300
343
 
301
344
  | Chave | O que declara |
302
345
  | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
303
- | `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` fica fora (base do futuro column picker); `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
304
- | `filters` | `{ [nome]: { label, type, options?, multiple?, advanced?, depends?, … } }` — a toolbar. `advanced` vai para o modal; `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
346
+ | `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
347
+ | `filters` | `{ [nome]: { label, type, options?, multiple?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
305
348
  | `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Com período/filtros no contrato ela fica à direita; sendo a ÚNICA forma de recorte, abre a linha. |
306
349
  | `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
307
- | `periods` | `{ value, label }[]` — o controle de período (presets + Personalizado com calendário); materializa em `from`/`to` no input. |
350
+ | `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
308
351
 
309
352
  ## useListAction
310
353
 
@@ -318,16 +361,21 @@ chamar a action.
318
361
 
319
362
  | Propriedade | Tipo | Padrão | Descrição |
320
363
  | ----------------------- | ------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
321
- | `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item, paginate cursor). |
364
+ | `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item). A paginação envia `limit`/`page` e usa o `total` devolvido. |
322
365
  | `input` | `TInput` | | O ESCOPO BASE (ex.: { workspaceId }) — a toolbar soma por cima, nunca sobrescreve. |
323
366
  | `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
324
367
  | `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para os filtros select/lookup (chave = nome do filtro). |
368
+ | `advancedFilters` | `{ columns?: 1 \| 2 \| 3 }` | `{ columns: 1 }` | Define as colunas dos grupos de filtros avançados; o Opus deriva a largura correspondente do diálogo. |
369
+ | `views` | `Record<string, ActionListView<TItem>>` | | Visualizações alternativas `{ label, icon?, render }`; o seletor alterna entre a tabela e estas. |
370
+ | `beforeContent` | `ReactNode` | | Conteúdo entre a toolbar e o corpo, como um resumo que reage ao recorte atual. |
325
371
  | `columns` | `ActionListColumn<TItem>[]` | | Tabela EXPLÍCITA — sobrepõe as colunas do contrato (escape hatch). |
326
372
  | `children` | `(items, refetch) => ReactNode` | | Modo composição: layout livre; a toolbar segue. Tem precedência sobre columns. |
327
- | `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm?, destructive? }` — liga a multi-seleção. |
373
+ | `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm?: { title, message? }, destructive? }` — liga a multi-seleção. |
328
374
  | `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
329
375
  | `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. |
330
376
  | `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado — para quem embala sincronizar com a URL. |
377
+ | `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado, por exemplo quando um formulário inline aberto conta como conteúdo. |
378
+ | `loading` | `boolean` | | Carga extra agregada à da consulta, como uma query irmã de que os `children` dependem. |
331
379
  | `emptyMessage` | `string` | `'Nenhum resultado.'` | Frase do estado vazio (o `Empty` do `DataState`). |
332
380
  | `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; a ação de tentar de novo refaz a consulta. |
333
381
  | `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
@@ -2,7 +2,8 @@
2
2
 
3
3
  Use `ActionTrigger` para executar uma `SimpleAction` a partir de um botão. Sem confirmação, o clique
4
4
  inicia a operação, desabilita o controle durante a execução e apresenta as mensagens do contrato em
5
- um toast. O rótulo padrão vem de `action.label`.
5
+ um toast. O rótulo padrão vem de `action.label` quando ele é texto; se o label for uma referência
6
+ de tradução ou estiver ausente, o botão usa `action.name`.
6
7
 
7
8
  ```tsx preview
8
9
  <DocBrowserActionProvider>
@@ -58,7 +59,8 @@ não aciona o item clicável ao redor. `itemLabel` identifica o registro na mens
58
59
  ## Erro que a pessoa entende
59
60
 
60
61
  Quando a action falha, uma mensagem do servidor substitui o texto genérico somente nos erros de
61
- negócio `conflict`, `validation` e `not_found`:
62
+ negócio `conflict`, `validation` e `not_found`, e nos erros de acesso `authorization` e
63
+ `authentication`, cuja frase o runtime já entrega em pt-BR:
62
64
 
63
65
  > Este agente tem 3 conversas — desabilite em vez de excluir.
64
66
 
@@ -78,10 +80,12 @@ um atalho, um arrastar, um item de menu.
78
80
  |---|---|---|---|
79
81
  | `action` | `SimpleContract<TInput, TData>` | | A SimpleAction do Opus — label, messages e confirm vêm do contrato. |
80
82
  | `input` | `TInput` | | O que a action recebe — geralmente { id }. |
81
- | `label` | `string` | `action.label` | Sobrepõe o texto do botão. |
82
- | `context / variant / size` | `do Button` | gatilho: `primary`/`solid`; `danger` quando a action é `destructive`; no modo ícone, `ghost` e `neutral` (ou `danger` se destrutiva) / `icon-xs` | 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`). |
83
- | `confirm` | `{ title, description?, actionLabel?, cancelLabel? }` | | Confirmação via prop — sobrepõe o ConfirmSpec do contrato. |
83
+ | `label` | `string` | `action.label` quando é texto; senão `action.name` | Sobrepõe o texto do botão. |
84
+ | `context / variant / size` | `do Button` | gatilho: `primary`/`solid`; `danger` quando a action é `destructive`; no modo ícone, `ghost` e `neutral` (ou `danger` se destrutiva) / `default`, ou `icon-xs` com `icon` | 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`). |
85
+ | `confirm` | `{ title, body?, actionLabel?, cancelLabel? }` | | Confirmação via prop — sobrepõe o ConfirmSpec do contrato. |
86
+ | `disabled` | `boolean` | `false` | Desabilita o botão independentemente da execução em andamento. |
84
87
  | `onSuccess` | `(data: TData) => void` | | Pós-sucesso (cache já invalidado pelo action.invalidates). |
88
+ | `onLoadingChange` | `(loading: boolean) => void` | | Informa início e fim da execução a quem coordena bloqueio entre várias actions. |
85
89
  | `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. |
86
90
  | `itemLabel` | `string` | | Nome do alvo na pergunta (sai entre aspas, em destaque, antes da mensagem do contrato). |
87
91
  | `className` | `string` | | Classes do botão. O tamanho vem de `size` (escala única); com `icon`, o default é `icon-xs`. |
@@ -1,6 +1,6 @@
1
1
  ## Carregar um recurso
2
2
 
3
- Use `ActionView` para carregar um recurso por uma `ViewAction` e manter carregamento, erro e vazio no
3
+ Use `ActionView` para carregar um recurso por uma `ViewAction` e manter carregamento e erro no
4
4
  mesmo fluxo. No exemplo, alterne entre os workspaces para observar o carregamento e a recuperação de
5
5
  erro. O consumidor compõe somente o conteúdo disponível.
6
6
 
@@ -12,7 +12,7 @@ render(
12
12
  <div className="w-full space-y-3">
13
13
  <div className="flex flex-wrap gap-2">
14
14
  <Button variant="outline" size="sm" onClick={() => setId('empresa-x')}>Empresa X</Button>
15
- <Button variant="outline" size="sm" onClick={() => setId('softize-multica')}>Softize · Multica</Button>
15
+ <Button variant="outline" size="sm" onClick={() => setId('empresa-y')}>Empresa Y</Button>
16
16
  <Button variant="outline" size="sm" onClick={() => setId('sessao-fantasma')}>Inexistente (erro)</Button>
17
17
  </div>
18
18
  <div className="rounded-lg border border-border p-4">
@@ -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 context={ws.status === 'active' ? 'success' : 'warning'}>{ws.status}</Badge>
24
+ <DictionaryValue dict={docWorkspaceStatus} value={ws.status} />
25
25
  </div>
26
26
  <p className="text-sm text-muted-foreground">
27
27
  Cliente {ws.client} · {ws.agents} agentes vinculados.
@@ -35,6 +35,10 @@ render(
35
35
  )
36
36
  ```
37
37
 
38
+ O handler da view deve devolver um valor. Uma consulta que resolve `undefined` é tratada como erro
39
+ pelo react-query, então `ActionView` apresenta o estado de erro, não um vazio. Represente um recurso
40
+ ausente com o erro `not_found`, cuja frase aparece para a pessoa.
41
+
38
42
  ## useViewAction
39
43
 
40
44
  `ActionView` é uma composição sobre `useViewAction(action, input)`, que devolve `data`, `error`,
@@ -45,11 +49,11 @@ mais de uma região da tela ou quando o carregamento precisa ser orquestrado por
45
49
 
46
50
  | Propriedade | Tipo | Padrão | Descrição |
47
51
  |---|---|---|---|
48
- | `action` | `ViewAction<TInput, TData>` | | A ViewAction do Opus (kind view, 1 recurso). |
52
+ | `action` | `ViewContract<TInput, TData>` | | A ViewAction do Opus (kind view, 1 recurso). |
49
53
  | `input` | `TInput` | | Geralmente { id } — mudou, recarrega. |
50
- | `children` | `(data: TData, refetch) => ReactNode` | | Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
51
- | `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. |
54
+ | `children` | `(data: TData, refetch) => ReactNode` | | Obrigatório, salvo quando `render` é informado. Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
55
+ | `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. Sem nenhum dos dois, o componente lança erro. |
52
56
  | `loading` | `ReactNode \| boolean` | `3 skeletons` | Sobrescreve o carregamento: um nó 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). |
57
+ | `empty` | `ReactNode` | `emptyMessage` ou nada | Estado reservado a `data` indefinido. Como o react-query converte esse resultado em erro, ele não aparece na prática; devolva um valor ou o erro `not_found`. |
58
+ | `emptyMessage` | `string` | | Atalho do vazio, sujeito à mesma limitação de `empty`: a frase na mesma superfície de `DataState` (`Empty` com moldura sólida). |
55
59
  | `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. |