@softize/opus 12.11.0 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/check.mjs +2 -7
  6. package/bin/lib/cli-shared.mjs +131 -0
  7. package/bin/lib/copy.mjs +1 -5
  8. package/bin/lib/db.mjs +16 -74
  9. package/bin/lib/gen-openapi.mjs +3 -3
  10. package/bin/lib/gen-runner.mjs +1 -1
  11. package/bin/lib/gen.mjs +14 -69
  12. package/bin/lib/mcp.mjs +3 -1
  13. package/bin/lib/seed.mjs +5 -62
  14. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +93 -10
  15. package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
  16. package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
  17. package/docs/ownership-vs-shadcn-lock.md +2 -3
  18. package/docs/protocol.md +7 -7
  19. package/docs/radius-scale.md +1 -1
  20. package/docs/releasing.md +8 -2
  21. package/package.json +7 -3
  22. package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
  23. package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
  24. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
  25. package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
  26. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
  27. package/registry/templates/app/package.json +1 -1
  28. package/registry/templates/app/src/main.tsx +4 -4
  29. package/src/audit/drivers/console.ts +1 -0
  30. package/src/auth/drivers/better-auth.ts +1 -0
  31. package/src/auth/drivers/jwt.ts +1 -0
  32. package/src/cache/drivers/memory.ts +1 -0
  33. package/src/client/drivers/fetch.ts +2 -1
  34. package/src/core/actions.ts +6 -1
  35. package/src/core/audit.ts +9 -3
  36. package/src/core/contracts.ts +7 -0
  37. package/src/core/domain.ts +1 -1
  38. package/src/core/errors.ts +18 -15
  39. package/src/core/index.ts +4 -2
  40. package/src/core/package-version.ts +26 -0
  41. package/src/core/reactions.ts +1 -1
  42. package/src/core/runtime.ts +33 -23
  43. package/src/core/schedules.ts +1 -1
  44. package/src/core/types.ts +2 -2
  45. package/src/dsl/eval.ts +2 -2
  46. package/src/dsl/kysely.ts +2 -2
  47. package/src/dsl/loads.ts +1 -1
  48. package/src/dsl/parser.ts +5 -5
  49. package/src/events/drivers/mitt.ts +1 -0
  50. package/src/mcp/index.ts +2 -1
  51. package/src/observability/drivers/opentelemetry.ts +1 -0
  52. package/src/queue/drivers/bullmq.ts +3 -3
  53. package/src/scheduler/drivers/node-cron.ts +3 -2
  54. package/src/scheduler/every.ts +7 -7
  55. package/src/schema/openapi.ts +3 -3
  56. package/src/seed/index.ts +29 -0
  57. package/src/server/drivers/fastify.ts +5 -2
  58. package/src/server/drivers/node.ts +9 -6
  59. package/src/server/index.ts +3 -1
  60. package/src/storage/drivers/fs.ts +1 -0
  61. package/src/testing/index.ts +3 -3
  62. package/src/ui/components/patterns/confirm.tsx +142 -42
  63. package/src/ui/components/patterns/content-header.tsx +7 -1
  64. package/src/ui/components/patterns/data-state.tsx +1 -1
  65. package/src/ui/components/patterns/dock.tsx +20 -3
  66. package/src/ui/components/patterns/form.tsx +12 -8
  67. package/src/ui/components/patterns/list.tsx +36 -41
  68. package/src/ui/components/patterns/page-state.tsx +2 -2
  69. package/src/ui/components/patterns/page.tsx +19 -1
  70. package/src/ui/components/patterns/shell-nav.tsx +10 -3
  71. package/src/ui/components/patterns/sidebar.tsx +43 -32
  72. package/src/ui/components/patterns/trigger.tsx +39 -38
  73. package/src/ui/components/patterns/view.tsx +26 -17
  74. package/src/ui/components/primitives/alert.tsx +14 -8
  75. package/src/ui/components/primitives/ask.tsx +3 -3
  76. package/src/ui/components/primitives/badge.tsx +11 -6
  77. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  78. package/src/ui/components/primitives/button.tsx +16 -3
  79. package/src/ui/components/primitives/calendar.tsx +28 -2
  80. package/src/ui/components/primitives/carousel.tsx +3 -3
  81. package/src/ui/components/primitives/chat.tsx +1 -1
  82. package/src/ui/components/primitives/checkbox.tsx +1 -1
  83. package/src/ui/components/primitives/command.tsx +2 -2
  84. package/src/ui/components/primitives/control.ts +12 -0
  85. package/src/ui/components/primitives/copyable.tsx +1 -1
  86. package/src/ui/components/primitives/dialog.tsx +202 -40
  87. package/src/ui/components/primitives/dot.tsx +5 -0
  88. package/src/ui/components/primitives/drawer.tsx +18 -8
  89. package/src/ui/components/primitives/empty.tsx +3 -3
  90. package/src/ui/components/primitives/field.tsx +3 -3
  91. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  92. package/src/ui/components/primitives/input-group.tsx +1 -1
  93. package/src/ui/components/primitives/input-otp.tsx +1 -1
  94. package/src/ui/components/primitives/input.tsx +2 -2
  95. package/src/ui/components/primitives/item.tsx +3 -3
  96. package/src/ui/components/primitives/progress.tsx +32 -3
  97. package/src/ui/components/primitives/radio-group.tsx +1 -1
  98. package/src/ui/components/primitives/resizable.tsx +3 -1
  99. package/src/ui/components/primitives/select.tsx +5 -5
  100. package/src/ui/components/primitives/slider.tsx +5 -1
  101. package/src/ui/components/primitives/sonner.tsx +190 -8
  102. package/src/ui/components/primitives/switch.tsx +1 -0
  103. package/src/ui/components/primitives/tabs.tsx +1 -0
  104. package/src/ui/components/primitives/textarea.tsx +1 -1
  105. package/src/ui/components/primitives/toggle.tsx +1 -1
  106. package/src/ui/components/primitives/tooltip.tsx +1 -0
  107. package/src/ui/docs/DocBrowser.tsx +102 -23
  108. package/src/ui/docs/changelog.tsx +1 -1
  109. package/src/ui/docs/content/accordion.md +22 -16
  110. package/src/ui/docs/content/action-form-card.md +8 -8
  111. package/src/ui/docs/content/action-form-dialog.md +9 -9
  112. package/src/ui/docs/content/action-form.md +37 -36
  113. package/src/ui/docs/content/action-list-dialog.md +11 -6
  114. package/src/ui/docs/content/action-list.md +73 -39
  115. package/src/ui/docs/content/action-trigger.md +29 -15
  116. package/src/ui/docs/content/action-view.md +17 -10
  117. package/src/ui/docs/content/actions.md +9 -9
  118. package/src/ui/docs/content/ai.md +3 -3
  119. package/src/ui/docs/content/alert.md +14 -12
  120. package/src/ui/docs/content/ask.md +11 -0
  121. package/src/ui/docs/content/aspect-ratio.md +4 -4
  122. package/src/ui/docs/content/audit.md +2 -2
  123. package/src/ui/docs/content/auth.md +3 -3
  124. package/src/ui/docs/content/avatar.md +34 -14
  125. package/src/ui/docs/content/badge.md +3 -3
  126. package/src/ui/docs/content/breadcrumb.md +13 -8
  127. package/src/ui/docs/content/button.md +81 -6
  128. package/src/ui/docs/content/calendar.md +18 -5
  129. package/src/ui/docs/content/card.md +27 -1
  130. package/src/ui/docs/content/carousel.md +16 -11
  131. package/src/ui/docs/content/chat.md +23 -3
  132. package/src/ui/docs/content/checkbox.md +7 -7
  133. package/src/ui/docs/content/cli.md +74 -22
  134. package/src/ui/docs/content/collapsible.md +8 -8
  135. package/src/ui/docs/content/command.md +16 -8
  136. package/src/ui/docs/content/composer.md +17 -2
  137. package/src/ui/docs/content/content.md +17 -2
  138. package/src/ui/docs/content/copyable.md +12 -3
  139. package/src/ui/docs/content/customization.md +5 -5
  140. package/src/ui/docs/content/cycle.md +3 -3
  141. package/src/ui/docs/content/data-state.md +11 -12
  142. package/src/ui/docs/content/data.md +26 -33
  143. package/src/ui/docs/content/detail.md +22 -4
  144. package/src/ui/docs/content/dialog.md +339 -31
  145. package/src/ui/docs/content/dictionary-value.md +17 -10
  146. package/src/ui/docs/content/dock.md +11 -3
  147. package/src/ui/docs/content/dot.md +8 -0
  148. package/src/ui/docs/content/drawer.md +27 -14
  149. package/src/ui/docs/content/empty-value.md +2 -2
  150. package/src/ui/docs/content/empty.md +19 -12
  151. package/src/ui/docs/content/events.md +4 -4
  152. package/src/ui/docs/content/field.md +34 -12
  153. package/src/ui/docs/content/getting-started.md +3 -3
  154. package/src/ui/docs/content/icon-picker.md +19 -4
  155. package/src/ui/docs/content/input-otp.md +20 -12
  156. package/src/ui/docs/content/input.md +121 -9
  157. package/src/ui/docs/content/item.md +27 -13
  158. package/src/ui/docs/content/kbd.md +19 -11
  159. package/src/ui/docs/content/label.md +12 -3
  160. package/src/ui/docs/content/log.md +4 -4
  161. package/src/ui/docs/content/markdown.md +7 -6
  162. package/src/ui/docs/content/mcp.md +13 -15
  163. package/src/ui/docs/content/menu.md +40 -16
  164. package/src/ui/docs/content/metric-card.md +13 -0
  165. package/src/ui/docs/content/observability.md +2 -2
  166. package/src/ui/docs/content/page.md +59 -6
  167. package/src/ui/docs/content/pagination.md +22 -17
  168. package/src/ui/docs/content/popover.md +22 -8
  169. package/src/ui/docs/content/progress.md +15 -16
  170. package/src/ui/docs/content/queue.md +5 -5
  171. package/src/ui/docs/content/radio-group.md +20 -12
  172. package/src/ui/docs/content/router.md +11 -6
  173. package/src/ui/docs/content/scheduler.md +4 -5
  174. package/src/ui/docs/content/scroll-area.md +12 -7
  175. package/src/ui/docs/content/select.md +47 -34
  176. package/src/ui/docs/content/semantic-context.md +2 -2
  177. package/src/ui/docs/content/separator.md +5 -5
  178. package/src/ui/docs/content/sidebar.md +329 -54
  179. package/src/ui/docs/content/skeleton.md +9 -2
  180. package/src/ui/docs/content/slider.md +8 -7
  181. package/src/ui/docs/content/spinner.md +8 -8
  182. package/src/ui/docs/content/split.md +29 -5
  183. package/src/ui/docs/content/storage.md +6 -8
  184. package/src/ui/docs/content/switch.md +8 -7
  185. package/src/ui/docs/content/table.md +13 -3
  186. package/src/ui/docs/content/tabs.md +28 -14
  187. package/src/ui/docs/content/testing.md +9 -11
  188. package/src/ui/docs/content/textarea.md +12 -4
  189. package/src/ui/docs/content/toast.md +47 -13
  190. package/src/ui/docs/content/toggle.md +75 -7
  191. package/src/ui/docs/content/tokens.md +7 -7
  192. package/src/ui/docs/content/tooltip.md +19 -11
  193. package/src/ui/docs/content/truncate.md +15 -8
  194. package/src/ui/docs/content/ui.md +24 -9
  195. package/src/ui/docs/content/upgrading.md +7 -8
  196. package/src/ui/docs/doc-client.tsx +5 -5
  197. package/src/ui/docs/doc.tsx +26 -14
  198. package/src/ui/docs/registry.tsx +25 -42
  199. package/src/ui/docs/standalone.tsx +2 -2
  200. package/src/ui/drivers/react.tsx +17 -12
  201. package/src/ui/lib/action-errors.ts +45 -0
  202. package/src/ui/lib/zod-pt-br.ts +31 -4
  203. package/src/ui/meta.ts +65 -95
  204. package/src/ui/react.tsx +17 -16
  205. package/src/ui/theme.css +60 -8
  206. package/src/vite/design.ts +6 -18
  207. package/src/ui/components/primitives/alert-dialog.tsx +0 -192
  208. package/src/ui/docs/content/alert-dialog.md +0 -73
  209. package/src/ui/docs/content/button-group.md +0 -71
  210. package/src/ui/docs/content/confirm.md +0 -120
  211. package/src/ui/docs/content/input-group.md +0 -79
  212. package/src/ui/docs/content/page-state.md +0 -45
  213. package/src/ui/docs/content/toggle-group.md +0 -81
@@ -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
  }
@@ -1,35 +1,136 @@
1
1
  import * as React from 'react'
2
2
  import * as DialogPrimitive from '@radix-ui/react-dialog'
3
+ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
3
4
  import { XIcon } from 'lucide-react'
4
5
  import { cn } from '../../lib/cn.ts'
6
+ import { focusRing } from './control.ts'
5
7
 
6
- export function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
7
- return <DialogPrimitive.Root data-slot="dialog" {...props} />
8
+ export type DialogMode = 'default' | 'alert'
9
+
10
+ interface DialogContextValue {
11
+ mode: DialogMode
12
+ onResult?: (result: string) => void
13
+ }
14
+
15
+ const DialogContext = React.createContext<DialogContextValue>({ mode: 'default' })
16
+
17
+ function useDialogContext(): DialogContextValue {
18
+ return React.useContext(DialogContext)
19
+ }
20
+
21
+ export interface DialogProps
22
+ extends React.ComponentProps<typeof DialogPrimitive.Root> {
23
+ /** `alert` exige uma resposta explícita e deriva `role="alertdialog"`. */
24
+ mode?: DialogMode
25
+ /** Recebe o `result` do `DialogClose` acionado. */
26
+ onResult?: (result: string) => void
27
+ }
28
+
29
+ export function Dialog({
30
+ mode = 'default',
31
+ onResult,
32
+ modal,
33
+ ...props
34
+ }: DialogProps) {
35
+ const context = React.useMemo(() => ({ mode, onResult }), [mode, onResult])
36
+
37
+ return (
38
+ <DialogContext.Provider value={context}>
39
+ {mode === 'alert' ? (
40
+ <AlertDialogPrimitive.Root data-slot="dialog" {...props} />
41
+ ) : (
42
+ <DialogPrimitive.Root data-slot="dialog" modal={modal} {...props} />
43
+ )}
44
+ </DialogContext.Provider>
45
+ )
46
+ }
47
+
48
+ export function DialogTrigger({
49
+ ...props
50
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
51
+ const { mode } = useDialogContext()
52
+ return mode === 'alert' ? (
53
+ <AlertDialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
54
+ ) : (
55
+ <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
56
+ )
8
57
  }
9
58
 
10
- export function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
11
- return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
59
+ export function DialogPortal({
60
+ ...props
61
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
62
+ const { mode } = useDialogContext()
63
+ return mode === 'alert' ? (
64
+ <AlertDialogPrimitive.Portal data-slot="dialog-portal" {...props} />
65
+ ) : (
66
+ <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
67
+ )
12
68
  }
13
69
 
14
- export function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
15
- return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
70
+ export interface DialogCloseProps
71
+ extends React.ComponentProps<typeof DialogPrimitive.Close> {
72
+ /** No modo `alert`, registra este controle como destino inicial seguro do foco. */
73
+ initialFocus?: boolean
74
+ /** Resposta entregue a `Dialog.onResult` antes do fechamento. */
75
+ result?: string
16
76
  }
17
77
 
18
- export function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
19
- return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
78
+ export function DialogClose({
79
+ initialFocus = false,
80
+ result,
81
+ onClick,
82
+ ...props
83
+ }: DialogCloseProps) {
84
+ const root = useDialogContext()
85
+ const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
86
+ onClick?.(event)
87
+ if (!event.defaultPrevented && result !== undefined) root.onResult?.(result)
88
+ }
89
+
90
+ if (root.mode === 'alert') {
91
+ const ClosePrimitive = initialFocus
92
+ ? AlertDialogPrimitive.Cancel
93
+ : AlertDialogPrimitive.Action
94
+ return (
95
+ <ClosePrimitive
96
+ data-slot="dialog-close"
97
+ data-initial-focus={initialFocus || undefined}
98
+ onClick={handleClick}
99
+ {...props}
100
+ />
101
+ )
102
+ }
103
+
104
+ return (
105
+ <DialogPrimitive.Close
106
+ data-slot="dialog-close"
107
+ data-initial-focus={initialFocus || undefined}
108
+ onClick={handleClick}
109
+ {...props}
110
+ />
111
+ )
20
112
  }
21
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
+
22
118
  export function DialogOverlay({
23
119
  className,
24
120
  ...props
25
121
  }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
26
- return (
122
+ const { mode } = useDialogContext()
123
+ const overlay = cn(overlayClasses, className)
124
+ return mode === 'alert' ? (
125
+ <AlertDialogPrimitive.Overlay
126
+ data-slot="dialog-overlay"
127
+ className={overlay}
128
+ {...props}
129
+ />
130
+ ) : (
27
131
  <DialogPrimitive.Overlay
28
132
  data-slot="dialog-overlay"
29
- className={cn(
30
- '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',
31
- className,
32
- )}
133
+ className={overlay}
33
134
  {...props}
34
135
  />
35
136
  )
@@ -38,32 +139,54 @@ export function DialogOverlay({
38
139
  export function DialogContent({
39
140
  className,
40
141
  children,
41
- showCloseButton = true,
142
+ showCloseButton,
42
143
  ...props
43
144
  }: React.ComponentProps<typeof DialogPrimitive.Content> & {
44
- /** Mostra o "X" de fechar no canto. Desligue pra um modal que exige ação explícita. */
145
+ /** Mostra o X no modo padrão. O modo `alert` exige uma resposta identificável. */
45
146
  showCloseButton?: boolean
46
147
  }) {
148
+ const { mode } = useDialogContext()
149
+ const showClose = showCloseButton ?? mode !== 'alert'
150
+ const contentClasses =
151
+ 'fixed left-[50%] top-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] overflow-clip rounded-xl border border-border bg-background text-foreground shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95'
152
+
153
+ if (mode === 'alert') {
154
+ return (
155
+ <DialogPortal>
156
+ <DialogOverlay />
157
+ <AlertDialogPrimitive.Content
158
+ data-slot="dialog-content"
159
+ data-mode="alert"
160
+ className={cn(contentClasses, 'grid max-w-xs gap-4 p-6', className)}
161
+ {...props}
162
+ >
163
+ {children}
164
+ </AlertDialogPrimitive.Content>
165
+ </DialogPortal>
166
+ )
167
+ }
168
+
47
169
  return (
48
170
  <DialogPortal>
49
171
  <DialogOverlay />
50
- {/* Superfície elevada: mantemos bg-card (divergência da casa; o shadcn usa bg-background). */}
51
172
  <DialogPrimitive.Content
52
173
  data-slot="dialog-content"
174
+ data-mode="default"
53
175
  className={cn(
54
- // Superfície PURA: só a casca (bg/raio/sombra) + coluna flex com teto de
55
- // altura. O padding mora nos SLOTS (Header/Body/Footer), não aqui — assim ninguém
56
- // precisa negar margem pra escapar de um p-6 embutido.
57
- "fixed left-[50%] top-[50%] z-50 flex max-h-[85vh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] flex-col overflow-clip rounded-xl bg-card text-card-foreground shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
176
+ contentClasses,
177
+ 'flex max-h-[85vh] flex-col sm:max-w-lg',
58
178
  className,
59
179
  )}
60
180
  {...props}
61
181
  >
62
182
  {children}
63
- {showCloseButton && (
183
+ {showClose && (
64
184
  <DialogPrimitive.Close
65
185
  data-slot="dialog-close"
66
- 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
+ )}
67
190
  >
68
191
  <XIcon />
69
192
  <span className="sr-only">Fechar</span>
@@ -74,17 +197,18 @@ export function DialogContent({
74
197
  )
75
198
  }
76
199
 
77
- export function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
200
+ export function DialogHeader({
201
+ className,
202
+ ...props
203
+ }: React.ComponentProps<'div'>) {
204
+ const { mode } = useDialogContext()
78
205
  return (
79
206
  <div
80
207
  data-slot="dialog-header"
81
- // Fixo (shrink-0), com divisor embaixo (border-b). p-5: padding igual nos quatro
82
- // lados, casando com o body. O :has tira o divisor quando o header é seguido
83
- // DIRETO pelo footer (confirm sem body) — senão o border-b dele encostaria no
84
- // A compensação coincide com a soma das hairlines; o divisor fica no footer.
85
208
  className={cn(
86
- 'flex shrink-0 flex-col gap-2 border-b p-5 text-center sm:text-left',
87
- '[&:has(+[data-slot=dialog-footer])]:border-b-0',
209
+ mode === 'alert'
210
+ ? 'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=dialog-media]:grid-rows-[auto_auto_1fr]'
211
+ : 'flex shrink-0 flex-col gap-2 border-b p-5 text-center sm:text-left [&:has(+[data-slot=dialog-footer])]:border-b-0',
88
212
  className,
89
213
  )}
90
214
  {...props}
@@ -92,27 +216,51 @@ export function DialogHeader({ className, ...props }: React.ComponentProps<'div'
92
216
  )
93
217
  }
94
218
 
95
- export function DialogBody({ className, ...props }: React.ComponentProps<'div'>) {
219
+ export function DialogMedia({
220
+ className,
221
+ ...props
222
+ }: React.ComponentProps<'div'>) {
223
+ return (
224
+ <div
225
+ data-slot="dialog-media"
226
+ className={cn(
227
+ 'mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted *:[svg:not([class*="size-"])]:size-8',
228
+ className,
229
+ )}
230
+ {...props}
231
+ />
232
+ )
233
+ }
234
+
235
+ export function DialogBody({
236
+ className,
237
+ ...props
238
+ }: React.ComponentProps<'div'>) {
239
+ const { mode } = useDialogContext()
96
240
  return (
97
241
  <div
98
242
  data-slot="dialog-body"
99
- // O miolo que ROLA: cresce, scrolla e padda a si mesmo. Pra corpo edge-to-edge
100
- // (cmdk, iframe de lente) passe `className` zerando o padding.
101
- className={cn('min-h-0 flex-1 overflow-y-auto p-5', className)}
243
+ className={cn(
244
+ mode === 'alert' ? 'min-w-0' : 'min-h-0 flex-1 overflow-y-auto p-5',
245
+ className,
246
+ )}
102
247
  {...props}
103
248
  />
104
249
  )
105
250
  }
106
251
 
107
- export function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
252
+ export function DialogFooter({
253
+ className,
254
+ ...props
255
+ }: React.ComponentProps<'div'>) {
256
+ const { mode } = useDialogContext()
108
257
  return (
109
258
  <div
110
259
  data-slot="dialog-footer"
111
- // Faixa destacada (divergência da casa, ao estilo do site do shadcn): borda em cima
112
- // + fundo muted. Sangra até as bordas sozinha — o content não tem padding, então não
113
- // há -mx pra compensar; o recorte do content arredonda o canto de baixo.
114
260
  className={cn(
115
- 'flex shrink-0 flex-col-reverse gap-2 border-t bg-muted/30 px-5 py-4 sm:flex-row sm:justify-end',
261
+ mode === 'alert'
262
+ ? 'grid grid-cols-2 gap-2 [&>*:only-child]:col-span-full'
263
+ : 'flex shrink-0 flex-col-reverse gap-2 border-t bg-muted/30 px-5 py-4 sm:flex-row sm:justify-end',
116
264
  className,
117
265
  )}
118
266
  {...props}
@@ -124,7 +272,14 @@ export function DialogTitle({
124
272
  className,
125
273
  ...props
126
274
  }: React.ComponentProps<typeof DialogPrimitive.Title>) {
127
- return (
275
+ const { mode } = useDialogContext()
276
+ return mode === 'alert' ? (
277
+ <AlertDialogPrimitive.Title
278
+ data-slot="dialog-title"
279
+ className={cn('text-lg font-semibold', className)}
280
+ {...props}
281
+ />
282
+ ) : (
128
283
  <DialogPrimitive.Title
129
284
  data-slot="dialog-title"
130
285
  className={cn('text-lg font-semibold leading-none', className)}
@@ -137,7 +292,14 @@ export function DialogDescription({
137
292
  className,
138
293
  ...props
139
294
  }: React.ComponentProps<typeof DialogPrimitive.Description>) {
140
- return (
295
+ const { mode } = useDialogContext()
296
+ return mode === 'alert' ? (
297
+ <AlertDialogPrimitive.Description
298
+ data-slot="dialog-description"
299
+ className={cn('text-sm text-muted-foreground', className)}
300
+ {...props}
301
+ />
302
+ ) : (
141
303
  <DialogPrimitive.Description
142
304
  data-slot="dialog-description"
143
305
  className={cn('text-sm text-muted-foreground', className)}
@@ -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}
@@ -60,7 +62,7 @@ function DrawerContent({
60
62
  <DrawerPrimitive.Content
61
63
  data-slot="drawer-content"
62
64
  className={cn(
63
- "fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
65
+ "fixed z-50 flex flex-col bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
64
66
  side === "right" &&
65
67
  "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
66
68
  side === "left" &&
@@ -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>
@@ -89,7 +96,7 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
89
96
  return (
90
97
  <div
91
98
  data-slot="drawer-header"
92
- className={cn("flex flex-col gap-1.5 p-4", className)}
99
+ className={cn("flex shrink-0 flex-col gap-2 border-b p-5", className)}
93
100
  {...props}
94
101
  />
95
102
  );
@@ -99,7 +106,7 @@ function DrawerBody({ className, ...props }: React.ComponentProps<"div">) {
99
106
  return (
100
107
  <div
101
108
  data-slot="drawer-body"
102
- className={cn("min-h-0 flex-1 overflow-y-auto p-4", className)}
109
+ className={cn("min-h-0 flex-1 overflow-y-auto p-5", className)}
103
110
  {...props}
104
111
  />
105
112
  );
@@ -109,7 +116,10 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
109
116
  return (
110
117
  <div
111
118
  data-slot="drawer-footer"
112
- className={cn("mt-auto flex flex-col gap-2 p-4", className)}
119
+ className={cn(
120
+ "mt-auto flex shrink-0 flex-col-reverse gap-2 border-t bg-muted/30 px-5 py-4 sm:flex-row sm:justify-end",
121
+ className,
122
+ )}
113
123
  {...props}
114
124
  />
115
125
  );
@@ -122,7 +132,7 @@ function DrawerTitle({
122
132
  return (
123
133
  <DrawerPrimitive.Title
124
134
  data-slot="drawer-title"
125
- className={cn("font-semibold text-foreground", className)}
135
+ className={cn("text-lg font-semibold leading-none text-foreground", className)}
126
136
  {...props}
127
137
  />
128
138
  );
@@ -81,10 +81,10 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
81
81
  )
82
82
  }
83
83
 
84
- function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
84
+ function EmptyActions({ className, ...props }: React.ComponentProps<"div">) {
85
85
  return (
86
86
  <div
87
- data-slot="empty-content"
87
+ data-slot="empty-actions"
88
88
  className={cn(
89
89
  "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
90
90
  className
@@ -99,6 +99,6 @@ export {
99
99
  EmptyHeader,
100
100
  EmptyTitle,
101
101
  EmptyDescription,
102
- EmptyContent,
102
+ EmptyActions,
103
103
  EmptyMedia,
104
104
  }
@@ -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}