@softize/opus 13.0.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 (138) hide show
  1. package/CHANGELOG.md +47 -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/db.mjs +16 -74
  7. package/bin/lib/gen-openapi.mjs +3 -3
  8. package/bin/lib/gen-runner.mjs +1 -1
  9. package/bin/lib/gen.mjs +14 -69
  10. package/bin/lib/mcp.mjs +3 -1
  11. package/bin/lib/seed.mjs +5 -62
  12. package/docs/ownership-vs-shadcn-lock.md +2 -3
  13. package/docs/protocol.md +7 -7
  14. package/docs/releasing.md +8 -2
  15. package/package.json +7 -3
  16. package/registry/templates/app/package.json +1 -1
  17. package/registry/templates/app/src/main.tsx +4 -4
  18. package/src/audit/drivers/console.ts +1 -0
  19. package/src/auth/drivers/better-auth.ts +1 -0
  20. package/src/auth/drivers/jwt.ts +1 -0
  21. package/src/cache/drivers/memory.ts +1 -0
  22. package/src/client/drivers/fetch.ts +2 -1
  23. package/src/core/actions.ts +6 -1
  24. package/src/core/audit.ts +9 -3
  25. package/src/core/contracts.ts +7 -0
  26. package/src/core/domain.ts +1 -1
  27. package/src/core/errors.ts +18 -15
  28. package/src/core/index.ts +4 -2
  29. package/src/core/package-version.ts +26 -0
  30. package/src/core/reactions.ts +1 -1
  31. package/src/core/runtime.ts +33 -23
  32. package/src/core/schedules.ts +1 -1
  33. package/src/core/types.ts +2 -2
  34. package/src/dsl/eval.ts +2 -2
  35. package/src/dsl/kysely.ts +2 -2
  36. package/src/dsl/loads.ts +1 -1
  37. package/src/dsl/parser.ts +5 -5
  38. package/src/events/drivers/mitt.ts +1 -0
  39. package/src/mcp/index.ts +2 -1
  40. package/src/observability/drivers/opentelemetry.ts +1 -0
  41. package/src/queue/drivers/bullmq.ts +3 -3
  42. package/src/scheduler/drivers/node-cron.ts +3 -2
  43. package/src/scheduler/every.ts +7 -7
  44. package/src/schema/openapi.ts +3 -3
  45. package/src/seed/index.ts +29 -0
  46. package/src/server/drivers/fastify.ts +5 -2
  47. package/src/server/drivers/node.ts +9 -6
  48. package/src/server/index.ts +3 -1
  49. package/src/storage/drivers/fs.ts +1 -0
  50. package/src/testing/index.ts +3 -3
  51. package/src/ui/components/patterns/confirm.tsx +2 -2
  52. package/src/ui/components/patterns/content-header.tsx +7 -1
  53. package/src/ui/components/patterns/data-state.tsx +1 -1
  54. package/src/ui/components/patterns/dock.tsx +20 -3
  55. package/src/ui/components/patterns/form.tsx +12 -8
  56. package/src/ui/components/patterns/list.tsx +4 -4
  57. package/src/ui/components/patterns/page.tsx +19 -1
  58. package/src/ui/components/patterns/shell-nav.tsx +10 -3
  59. package/src/ui/components/patterns/sidebar.tsx +17 -6
  60. package/src/ui/components/patterns/trigger.tsx +14 -16
  61. package/src/ui/components/patterns/view.tsx +26 -17
  62. package/src/ui/components/primitives/alert.tsx +11 -5
  63. package/src/ui/components/primitives/ask.tsx +3 -3
  64. package/src/ui/components/primitives/badge.tsx +11 -6
  65. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  66. package/src/ui/components/primitives/button.tsx +16 -3
  67. package/src/ui/components/primitives/calendar.tsx +28 -2
  68. package/src/ui/components/primitives/carousel.tsx +3 -3
  69. package/src/ui/components/primitives/chat.tsx +1 -1
  70. package/src/ui/components/primitives/checkbox.tsx +1 -1
  71. package/src/ui/components/primitives/command.tsx +2 -2
  72. package/src/ui/components/primitives/control.ts +12 -0
  73. package/src/ui/components/primitives/copyable.tsx +1 -1
  74. package/src/ui/components/primitives/dialog.tsx +12 -7
  75. package/src/ui/components/primitives/dot.tsx +5 -0
  76. package/src/ui/components/primitives/drawer.tsx +10 -3
  77. package/src/ui/components/primitives/field.tsx +3 -3
  78. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  79. package/src/ui/components/primitives/input-group.tsx +1 -1
  80. package/src/ui/components/primitives/input-otp.tsx +1 -1
  81. package/src/ui/components/primitives/input.tsx +2 -2
  82. package/src/ui/components/primitives/progress.tsx +32 -3
  83. package/src/ui/components/primitives/radio-group.tsx +1 -1
  84. package/src/ui/components/primitives/resizable.tsx +3 -1
  85. package/src/ui/components/primitives/select.tsx +5 -5
  86. package/src/ui/components/primitives/slider.tsx +5 -1
  87. package/src/ui/components/primitives/sonner.tsx +3 -0
  88. package/src/ui/components/primitives/switch.tsx +1 -0
  89. package/src/ui/components/primitives/tabs.tsx +1 -0
  90. package/src/ui/components/primitives/textarea.tsx +1 -1
  91. package/src/ui/components/primitives/toggle.tsx +1 -1
  92. package/src/ui/components/primitives/tooltip.tsx +1 -0
  93. package/src/ui/docs/changelog.tsx +1 -1
  94. package/src/ui/docs/content/action-form.md +10 -3
  95. package/src/ui/docs/content/action-list.md +10 -1
  96. package/src/ui/docs/content/action-trigger.md +8 -1
  97. package/src/ui/docs/content/action-view.md +9 -2
  98. package/src/ui/docs/content/ask.md +11 -0
  99. package/src/ui/docs/content/calendar.md +13 -0
  100. package/src/ui/docs/content/card.md +26 -0
  101. package/src/ui/docs/content/chat.md +20 -0
  102. package/src/ui/docs/content/cli.md +71 -19
  103. package/src/ui/docs/content/composer.md +15 -0
  104. package/src/ui/docs/content/content.md +15 -0
  105. package/src/ui/docs/content/copyable.md +8 -0
  106. package/src/ui/docs/content/detail.md +19 -1
  107. package/src/ui/docs/content/dictionary-value.md +9 -2
  108. package/src/ui/docs/content/dock.md +8 -0
  109. package/src/ui/docs/content/dot.md +8 -0
  110. package/src/ui/docs/content/empty.md +2 -2
  111. package/src/ui/docs/content/getting-started.md +2 -2
  112. package/src/ui/docs/content/icon-picker.md +11 -0
  113. package/src/ui/docs/content/label.md +7 -0
  114. package/src/ui/docs/content/menu.md +6 -0
  115. package/src/ui/docs/content/metric-card.md +13 -0
  116. package/src/ui/docs/content/page.md +8 -0
  117. package/src/ui/docs/content/popover.md +6 -0
  118. package/src/ui/docs/content/progress.md +8 -11
  119. package/src/ui/docs/content/select.md +5 -5
  120. package/src/ui/docs/content/semantic-context.md +2 -2
  121. package/src/ui/docs/content/sidebar.md +14 -8
  122. package/src/ui/docs/content/skeleton.md +6 -0
  123. package/src/ui/docs/content/split.md +21 -0
  124. package/src/ui/docs/content/textarea.md +7 -0
  125. package/src/ui/docs/content/tokens.md +4 -4
  126. package/src/ui/docs/content/truncate.md +8 -0
  127. package/src/ui/docs/content/ui.md +14 -0
  128. package/src/ui/docs/doc-client.tsx +5 -5
  129. package/src/ui/docs/doc.tsx +26 -14
  130. package/src/ui/docs/registry.tsx +5 -5
  131. package/src/ui/docs/standalone.tsx +2 -2
  132. package/src/ui/drivers/react.tsx +17 -12
  133. package/src/ui/lib/action-errors.ts +45 -0
  134. package/src/ui/lib/zod-pt-br.ts +31 -4
  135. package/src/ui/meta.ts +3 -3
  136. package/src/ui/react.tsx +2 -0
  137. package/src/ui/theme.css +10 -8
  138. package/src/vite/design.ts +6 -18
@@ -4,6 +4,7 @@ import {
4
4
  ChevronLeftIcon,
5
5
  ChevronRightIcon,
6
6
  } from "lucide-react";
7
+ import { ptBR } from "date-fns/locale";
7
8
  import {
8
9
  DayPicker,
9
10
  getDefaultClassNames,
@@ -13,12 +14,34 @@ import {
13
14
  import { cn } from "../../lib/cn.ts";
14
15
  import { Button, buttonVariants } from "./button.tsx";
15
16
 
17
+ /** Rótulos acessíveis da navegação em pt-BR — o react-day-picker só traduz nomes de mês e
18
+ * dia pelo `locale`; os aria-labels dos controles continuariam em inglês. */
19
+ const LABELS_PT_BR: NonNullable<React.ComponentProps<typeof DayPicker>["labels"]> = {
20
+ labelPrevious: () => "Mês anterior",
21
+ labelNext: () => "Próximo mês",
22
+ labelMonthDropdown: () => "Escolher o mês",
23
+ labelYearDropdown: () => "Escolher o ano",
24
+ labelDayButton: (date, modifiers, options) => {
25
+ const day = date.toLocaleDateString(options?.locale?.code ?? "pt-BR", {
26
+ weekday: "long",
27
+ day: "numeric",
28
+ month: "long",
29
+ year: "numeric",
30
+ });
31
+ const prefix = modifiers.today ? "Hoje, " : "";
32
+ const suffix = modifiers.selected ? ", selecionado" : "";
33
+ return `${prefix}${day}${suffix}`;
34
+ },
35
+ };
36
+
16
37
  function Calendar({
17
38
  className,
18
39
  classNames,
19
40
  showOutsideDays = true,
20
41
  captionLayout = "label",
21
42
  buttonVariant = "ghost",
43
+ locale = ptBR,
44
+ labels,
22
45
  formatters,
23
46
  components,
24
47
  ...props
@@ -30,6 +53,8 @@ function Calendar({
30
53
  return (
31
54
  <DayPicker
32
55
  showOutsideDays={showOutsideDays}
56
+ locale={locale}
57
+ labels={{ ...LABELS_PT_BR, ...labels }}
33
58
  className={cn(
34
59
  "group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-body]_&]:bg-transparent [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
35
60
  String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
@@ -38,8 +63,9 @@ function Calendar({
38
63
  )}
39
64
  captionLayout={captionLayout}
40
65
  formatters={{
41
- formatMonthDropdown: (date) =>
42
- date.toLocaleString("default", { month: "short" }),
66
+ // O dateLib já carrega o `locale` em vigor: o dropdown fala a mesma língua da legenda.
67
+ formatMonthDropdown: (date, dateLib) =>
68
+ dateLib?.format(date, "LLL") ?? date.toLocaleString("pt-BR", { month: "short" }),
43
69
  ...formatters,
44
70
  }}
45
71
  classNames={{
@@ -120,7 +120,7 @@ function Carousel({
120
120
  onKeyDownCapture={handleKeyDown}
121
121
  className={cn("relative", className)}
122
122
  role="region"
123
- aria-roledescription="carousel"
123
+ aria-roledescription="carrossel"
124
124
  data-slot="carousel"
125
125
  {...props}
126
126
  >
@@ -194,7 +194,7 @@ function CarouselPrevious({
194
194
  {...props}
195
195
  >
196
196
  <ArrowLeft />
197
- <span className="sr-only">Previous slide</span>
197
+ <span className="sr-only">Slide anterior</span>
198
198
  </Button>
199
199
  )
200
200
  }
@@ -224,7 +224,7 @@ function CarouselNext({
224
224
  {...props}
225
225
  >
226
226
  <ArrowRight />
227
- <span className="sr-only">Next slide</span>
227
+ <span className="sr-only">Próximo slide</span>
228
228
  </Button>
229
229
  )
230
230
  }
@@ -193,7 +193,7 @@ const ChatTranscript = React.memo(function ChatTranscript({
193
193
  return (
194
194
  <div
195
195
  key={i}
196
- className="max-w-[88%] self-start rounded-xl border border-destructive/30 bg-destructive/10 px-3.5 py-2.5 text-sm text-destructive"
196
+ className="max-w-[88%] self-start rounded-xl border border-context-danger-border bg-context-danger-subtle px-3.5 py-2.5 text-sm text-context-danger-emphasis"
197
197
  >
198
198
  {item.content}
199
199
  </div>
@@ -12,7 +12,7 @@ function Checkbox({
12
12
  <CheckboxPrimitive.Root
13
13
  data-slot="checkbox"
14
14
  className={cn(
15
- "peer size-4 shrink-0 rounded-[0.25rem] border border-input transition-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 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary",
15
+ "peer size-4 shrink-0 rounded-[0.25rem] border border-input transition-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 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-context-danger/40 dark:data-[state=checked]:bg-primary",
16
16
  className
17
17
  )}
18
18
  {...props}
@@ -28,8 +28,8 @@ function Command({
28
28
  }
29
29
 
30
30
  function CommandDialog({
31
- title = "Command Palette",
32
- description = "Search for a command to run...",
31
+ title = "Paleta de comandos",
32
+ description = "Busque um comando para executar…",
33
33
  children,
34
34
  className,
35
35
  showCloseButton = true,
@@ -1,2 +1,14 @@
1
1
  /** Geometria compartilhada pelos controles inline da casa. */
2
2
  export type ControlShape = 'default' | 'pill'
3
+
4
+ /**
5
+ * Anel de foco da casa: fino (0.1875rem), translúcido e encostado na borda — sem offset.
6
+ * Todo controle focável usa esta constante; espessuras próprias (`ring-2`, `ring-4`) ou
7
+ * `ring-offset-*` quebram a leitura de "foco" que o tema calibrou (ver theme.css, `--ring`).
8
+ */
9
+ export const focusRing =
10
+ 'focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50'
11
+
12
+ /** O mesmo anel, disparado por um descendente focado (grupos que envolvem um campo). */
13
+ export const focusRingWithin =
14
+ 'focus-within:border-ring focus-within:ring-[0.1875rem] focus-within:ring-ring/50'
@@ -44,7 +44,7 @@ export function Copyable({
44
44
  {...props}
45
45
  >
46
46
  {children}
47
- {copied ? <CheckIcon className="size-3.5 text-emerald-500" /> : <CopyIcon className="size-3.5" />}
47
+ {copied ? <CheckIcon className="size-3.5 text-context-success-emphasis" /> : <CopyIcon className="size-3.5" />}
48
48
  </button>
49
49
  )
50
50
  }
@@ -3,6 +3,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'
3
3
  import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
4
4
  import { XIcon } from 'lucide-react'
5
5
  import { cn } from '../../lib/cn.ts'
6
+ import { focusRing } from './control.ts'
6
7
 
7
8
  export type DialogMode = 'default' | 'alert'
8
9
 
@@ -110,25 +111,26 @@ export function DialogClose({
110
111
  )
111
112
  }
112
113
 
114
+ /** Véu compartilhado por Dialog e Drawer: mesmo escurecimento, desfoque e animação. */
115
+ export const overlayClasses =
116
+ 'fixed inset-0 z-50 bg-black/30 backdrop-blur-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0'
117
+
113
118
  export function DialogOverlay({
114
119
  className,
115
120
  ...props
116
121
  }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
117
122
  const { mode } = useDialogContext()
118
- const overlayClasses = cn(
119
- 'fixed inset-0 z-50 bg-black/30 backdrop-blur-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
120
- className,
121
- )
123
+ const overlay = cn(overlayClasses, className)
122
124
  return mode === 'alert' ? (
123
125
  <AlertDialogPrimitive.Overlay
124
126
  data-slot="dialog-overlay"
125
- className={overlayClasses}
127
+ className={overlay}
126
128
  {...props}
127
129
  />
128
130
  ) : (
129
131
  <DialogPrimitive.Overlay
130
132
  data-slot="dialog-overlay"
131
- className={overlayClasses}
133
+ className={overlay}
132
134
  {...props}
133
135
  />
134
136
  )
@@ -181,7 +183,10 @@ export function DialogContent({
181
183
  {showClose && (
182
184
  <DialogPrimitive.Close
183
185
  data-slot="dialog-close"
184
- className="absolute right-4 top-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
186
+ className={cn(
187
+ "absolute right-4 top-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
188
+ focusRing,
189
+ )}
185
190
  >
186
191
  <XIcon />
187
192
  <span className="sr-only">Fechar</span>
@@ -5,6 +5,11 @@ import { cn } from '../../lib/cn.ts'
5
5
 
6
6
  export type DotContext = UiContext
7
7
  export type DotVariant = 'solid' | 'outline'
8
+ /**
9
+ * @deprecated Aliases de migração: `default` → `context="primary"`, `secondary` →
10
+ * `context="neutral"`, `destructive` → `context="danger"`; `success`, `warning` e `info` → o
11
+ * `context` homônimo. Declare `context` + `variant`.
12
+ */
8
13
  export type LegacyDotVariant =
9
14
  | 'default'
10
15
  | 'secondary'
@@ -3,6 +3,8 @@ import { XIcon } from "lucide-react";
3
3
  import { Dialog as DrawerPrimitive } from "radix-ui";
4
4
 
5
5
  import { cn } from "../../lib/cn.ts";
6
+ import { focusRing } from "./control.ts";
7
+ import { overlayClasses } from "./dialog.tsx";
6
8
 
7
9
  function Drawer({
8
10
  ...props
@@ -36,7 +38,7 @@ function DrawerOverlay({
36
38
  <DrawerPrimitive.Overlay
37
39
  data-slot="drawer-overlay"
38
40
  className={cn(
39
- "fixed inset-0 z-50 bg-black/30 backdrop-blur-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
41
+ overlayClasses,
40
42
  className,
41
43
  )}
42
44
  {...props}
@@ -75,9 +77,14 @@ function DrawerContent({
75
77
  >
76
78
  {children}
77
79
  {showCloseButton && (
78
- <DrawerPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
80
+ <DrawerPrimitive.Close
81
+ className={cn(
82
+ "absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary",
83
+ focusRing,
84
+ )}
85
+ >
79
86
  <XIcon className="size-4" />
80
- <span className="sr-only">Close</span>
87
+ <span className="sr-only">Fechar</span>
81
88
  </DrawerPrimitive.Close>
82
89
  )}
83
90
  </DrawerPrimitive.Content>
@@ -53,7 +53,7 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
53
53
  }
54
54
 
55
55
  const fieldVariants = cva(
56
- "group/field flex w-full data-[invalid=true]:text-destructive",
56
+ "group/field flex w-full data-[invalid=true]:text-context-danger-emphasis",
57
57
  {
58
58
  variants: {
59
59
  orientation: {
@@ -113,7 +113,7 @@ function FieldLabel({
113
113
  <Label
114
114
  data-slot="field-label"
115
115
  className={cn(
116
- "group/field-label peer/field-label flex w-fit gap-2 text-xs leading-snug font-medium text-muted-foreground group-data-[invalid=true]/field:text-destructive group-data-[disabled=true]/field:opacity-50",
116
+ "group/field-label peer/field-label flex w-fit gap-2 text-xs leading-snug font-medium text-muted-foreground group-data-[invalid=true]/field:text-context-danger-emphasis group-data-[disabled=true]/field:opacity-50",
117
117
  "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
118
118
  "has-[>[data-slot=field]]:has-data-[state=checked]:border-primary has-[>[data-slot=field]]:has-data-[state=checked]:bg-primary/5 dark:has-[>[data-slot=field]]:has-data-[state=checked]:bg-primary/10",
119
119
  className
@@ -224,7 +224,7 @@ function FieldError({
224
224
  <div
225
225
  role="alert"
226
226
  data-slot="field-error"
227
- className={cn("text-sm font-normal text-destructive", className)}
227
+ className={cn("text-sm font-normal text-context-danger-emphasis", className)}
228
228
  {...props}
229
229
  >
230
230
  {content}
@@ -46,6 +46,7 @@ import {
46
46
  type LucideIcon,
47
47
  } from 'lucide-react'
48
48
  import { cn } from '../../lib/cn.ts'
49
+ import { focusRing } from './control.ts'
49
50
  import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './command.tsx'
50
51
  import { Popover, PopoverContent, PopoverTrigger } from './popover.tsx'
51
52
 
@@ -141,7 +142,8 @@ export function IconPicker({
141
142
  aria-describedby={ariaDescribedBy}
142
143
  data-slot="icon-picker"
143
144
  className={cn(
144
- 'flex h-10 w-full items-center gap-2 rounded-md border border-input bg-transparent px-3 text-sm outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30',
145
+ 'flex h-10 w-full items-center gap-2 rounded-md border border-input bg-transparent px-3 text-sm outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30',
146
+ focusRing,
145
147
  className,
146
148
  )}
147
149
  >
@@ -27,7 +27,7 @@ function InputGroup({ className, shape = 'default', ...props }: React.ComponentP
27
27
  "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[0.1875rem] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
28
28
 
29
29
  // Error state.
30
- "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
30
+ "has-[[data-slot][aria-invalid=true]]:border-context-danger has-[[data-slot][aria-invalid=true]]:ring-context-danger/20 dark:has-[[data-slot][aria-invalid=true]]:ring-context-danger/40",
31
31
 
32
32
  className
33
33
  )}
@@ -49,7 +49,7 @@ function InputOTPSlot({
49
49
  data-slot="input-otp-slot"
50
50
  data-active={isActive}
51
51
  className={cn(
52
- "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[0.1875rem] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-destructive/40",
52
+ "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-context-danger data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[0.1875rem] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-context-danger data-[active=true]:aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-context-danger/40",
53
53
  className
54
54
  )}
55
55
  {...props}
@@ -14,7 +14,7 @@ export interface InputProps extends React.ComponentProps<"input"> {
14
14
 
15
15
  // O <input> CRU (sem adorno): ELE é o campo — borda, foco, ref e className moram nele.
16
16
  const bareInput =
17
- "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40"
17
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40"
18
18
 
19
19
  function Input({ className, type, icon, trailing, ...props }: InputProps) {
20
20
  // Sem adorno: o <input> cru de sempre — retrocompatível (refs, forms, InputGroupInput).
@@ -33,7 +33,7 @@ function Input({ className, type, icon, trailing, ...props }: InputProps) {
33
33
  className={cn(
34
34
  "flex h-9 w-full min-w-0 items-center gap-1.5 rounded-md border border-input bg-transparent px-3 text-base transition-[color,box-shadow] outline-none md:text-sm dark:bg-input/30",
35
35
  "has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-[0.1875rem] has-[input:focus-visible]:ring-ring/50",
36
- "has-[input[aria-invalid=true]]:border-destructive has-[input[aria-invalid=true]]:ring-destructive/20 dark:has-[input[aria-invalid=true]]:ring-destructive/40",
36
+ "has-[input[aria-invalid=true]]:border-context-danger has-[input[aria-invalid=true]]:ring-context-danger/20 dark:has-[input[aria-invalid=true]]:ring-context-danger/40",
37
37
  "has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50",
38
38
  className,
39
39
  )}
@@ -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 } from './control.ts'
29
29
  import { CommandEmpty, CommandGroup, CommandItem, CommandList } from './command.tsx'
30
30
  import { Popover, PopoverAnchor, PopoverContent } from './popover.tsx'
31
31
 
@@ -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: (
@@ -22,6 +22,7 @@ function Switch({
22
22
  >
23
23
  <SwitchPrimitive.Thumb
24
24
  data-slot="switch-thumb"
25
+ // px: o percurso do thumb desconta a borda (um pixel de cada lado) no calc do translate.
25
26
  className={cn(
26
27
  "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
28
  )}
@@ -77,6 +77,7 @@ function TabsTrigger({
77
77
  return (
78
78
  <TabsPrimitive.Trigger
79
79
  data-slot="tabs-trigger"
80
+ // px: altura e indicador compensam a hairline do contêiner (um pixel físico, não escala).
80
81
  className={cn(
81
82
  "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
83
  "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}
@@ -5,7 +5,7 @@ import { Toggle as TogglePrimitive } from "radix-ui"
5
5
  import { cn } from '../../lib/cn.ts'
6
6
 
7
7
  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",
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-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",
9
9
  {
10
10
  variants: {
11
11
  variant: {
@@ -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 |
@@ -85,7 +92,7 @@ Monte os providers de consulta e execução uma vez na raiz do aplicativo:
85
92
 
86
93
  As opções seguem esta precedência: `options` no campo, `fieldOptions` no formulário, `options` no
87
94
  `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
95
+ específica substitui as seguintes. Dicionários registrados no `OpusProvider` resolvem rótulos por
89
96
  referência; itens estáticos permanecem como declarados no contrato.
90
97
 
91
98
  ## Widgets declarativos
@@ -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 |