@softize/opus 12.11.0 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/check.mjs +2 -7
  6. package/bin/lib/cli-shared.mjs +131 -0
  7. package/bin/lib/copy.mjs +1 -5
  8. package/bin/lib/db.mjs +16 -74
  9. package/bin/lib/gen-openapi.mjs +3 -3
  10. package/bin/lib/gen-runner.mjs +1 -1
  11. package/bin/lib/gen.mjs +14 -69
  12. package/bin/lib/mcp.mjs +3 -1
  13. package/bin/lib/seed.mjs +5 -62
  14. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +93 -10
  15. package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
  16. package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
  17. package/docs/ownership-vs-shadcn-lock.md +2 -3
  18. package/docs/protocol.md +7 -7
  19. package/docs/radius-scale.md +1 -1
  20. package/docs/releasing.md +8 -2
  21. package/package.json +7 -3
  22. package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
  23. package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
  24. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
  25. package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
  26. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
  27. package/registry/templates/app/package.json +1 -1
  28. package/registry/templates/app/src/main.tsx +4 -4
  29. package/src/audit/drivers/console.ts +1 -0
  30. package/src/auth/drivers/better-auth.ts +1 -0
  31. package/src/auth/drivers/jwt.ts +1 -0
  32. package/src/cache/drivers/memory.ts +1 -0
  33. package/src/client/drivers/fetch.ts +2 -1
  34. package/src/core/actions.ts +6 -1
  35. package/src/core/audit.ts +9 -3
  36. package/src/core/contracts.ts +7 -0
  37. package/src/core/domain.ts +1 -1
  38. package/src/core/errors.ts +18 -15
  39. package/src/core/index.ts +4 -2
  40. package/src/core/package-version.ts +26 -0
  41. package/src/core/reactions.ts +1 -1
  42. package/src/core/runtime.ts +33 -23
  43. package/src/core/schedules.ts +1 -1
  44. package/src/core/types.ts +2 -2
  45. package/src/dsl/eval.ts +2 -2
  46. package/src/dsl/kysely.ts +2 -2
  47. package/src/dsl/loads.ts +1 -1
  48. package/src/dsl/parser.ts +5 -5
  49. package/src/events/drivers/mitt.ts +1 -0
  50. package/src/mcp/index.ts +2 -1
  51. package/src/observability/drivers/opentelemetry.ts +1 -0
  52. package/src/queue/drivers/bullmq.ts +3 -3
  53. package/src/scheduler/drivers/node-cron.ts +3 -2
  54. package/src/scheduler/every.ts +7 -7
  55. package/src/schema/openapi.ts +3 -3
  56. package/src/seed/index.ts +29 -0
  57. package/src/server/drivers/fastify.ts +5 -2
  58. package/src/server/drivers/node.ts +9 -6
  59. package/src/server/index.ts +3 -1
  60. package/src/storage/drivers/fs.ts +1 -0
  61. package/src/testing/index.ts +3 -3
  62. package/src/ui/components/patterns/confirm.tsx +142 -42
  63. package/src/ui/components/patterns/content-header.tsx +7 -1
  64. package/src/ui/components/patterns/data-state.tsx +1 -1
  65. package/src/ui/components/patterns/dock.tsx +20 -3
  66. package/src/ui/components/patterns/form.tsx +12 -8
  67. package/src/ui/components/patterns/list.tsx +36 -41
  68. package/src/ui/components/patterns/page-state.tsx +2 -2
  69. package/src/ui/components/patterns/page.tsx +19 -1
  70. package/src/ui/components/patterns/shell-nav.tsx +10 -3
  71. package/src/ui/components/patterns/sidebar.tsx +43 -32
  72. package/src/ui/components/patterns/trigger.tsx +39 -38
  73. package/src/ui/components/patterns/view.tsx +26 -17
  74. package/src/ui/components/primitives/alert.tsx +14 -8
  75. package/src/ui/components/primitives/ask.tsx +3 -3
  76. package/src/ui/components/primitives/badge.tsx +11 -6
  77. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  78. package/src/ui/components/primitives/button.tsx +16 -3
  79. package/src/ui/components/primitives/calendar.tsx +28 -2
  80. package/src/ui/components/primitives/carousel.tsx +3 -3
  81. package/src/ui/components/primitives/chat.tsx +1 -1
  82. package/src/ui/components/primitives/checkbox.tsx +1 -1
  83. package/src/ui/components/primitives/command.tsx +2 -2
  84. package/src/ui/components/primitives/control.ts +12 -0
  85. package/src/ui/components/primitives/copyable.tsx +1 -1
  86. package/src/ui/components/primitives/dialog.tsx +202 -40
  87. package/src/ui/components/primitives/dot.tsx +5 -0
  88. package/src/ui/components/primitives/drawer.tsx +18 -8
  89. package/src/ui/components/primitives/empty.tsx +3 -3
  90. package/src/ui/components/primitives/field.tsx +3 -3
  91. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  92. package/src/ui/components/primitives/input-group.tsx +1 -1
  93. package/src/ui/components/primitives/input-otp.tsx +1 -1
  94. package/src/ui/components/primitives/input.tsx +2 -2
  95. package/src/ui/components/primitives/item.tsx +3 -3
  96. package/src/ui/components/primitives/progress.tsx +32 -3
  97. package/src/ui/components/primitives/radio-group.tsx +1 -1
  98. package/src/ui/components/primitives/resizable.tsx +3 -1
  99. package/src/ui/components/primitives/select.tsx +5 -5
  100. package/src/ui/components/primitives/slider.tsx +5 -1
  101. package/src/ui/components/primitives/sonner.tsx +190 -8
  102. package/src/ui/components/primitives/switch.tsx +1 -0
  103. package/src/ui/components/primitives/tabs.tsx +1 -0
  104. package/src/ui/components/primitives/textarea.tsx +1 -1
  105. package/src/ui/components/primitives/toggle.tsx +1 -1
  106. package/src/ui/components/primitives/tooltip.tsx +1 -0
  107. package/src/ui/docs/DocBrowser.tsx +102 -23
  108. package/src/ui/docs/changelog.tsx +1 -1
  109. package/src/ui/docs/content/accordion.md +22 -16
  110. package/src/ui/docs/content/action-form-card.md +8 -8
  111. package/src/ui/docs/content/action-form-dialog.md +9 -9
  112. package/src/ui/docs/content/action-form.md +37 -36
  113. package/src/ui/docs/content/action-list-dialog.md +11 -6
  114. package/src/ui/docs/content/action-list.md +73 -39
  115. package/src/ui/docs/content/action-trigger.md +29 -15
  116. package/src/ui/docs/content/action-view.md +17 -10
  117. package/src/ui/docs/content/actions.md +9 -9
  118. package/src/ui/docs/content/ai.md +3 -3
  119. package/src/ui/docs/content/alert.md +14 -12
  120. package/src/ui/docs/content/ask.md +11 -0
  121. package/src/ui/docs/content/aspect-ratio.md +4 -4
  122. package/src/ui/docs/content/audit.md +2 -2
  123. package/src/ui/docs/content/auth.md +3 -3
  124. package/src/ui/docs/content/avatar.md +34 -14
  125. package/src/ui/docs/content/badge.md +3 -3
  126. package/src/ui/docs/content/breadcrumb.md +13 -8
  127. package/src/ui/docs/content/button.md +81 -6
  128. package/src/ui/docs/content/calendar.md +18 -5
  129. package/src/ui/docs/content/card.md +27 -1
  130. package/src/ui/docs/content/carousel.md +16 -11
  131. package/src/ui/docs/content/chat.md +23 -3
  132. package/src/ui/docs/content/checkbox.md +7 -7
  133. package/src/ui/docs/content/cli.md +74 -22
  134. package/src/ui/docs/content/collapsible.md +8 -8
  135. package/src/ui/docs/content/command.md +16 -8
  136. package/src/ui/docs/content/composer.md +17 -2
  137. package/src/ui/docs/content/content.md +17 -2
  138. package/src/ui/docs/content/copyable.md +12 -3
  139. package/src/ui/docs/content/customization.md +5 -5
  140. package/src/ui/docs/content/cycle.md +3 -3
  141. package/src/ui/docs/content/data-state.md +11 -12
  142. package/src/ui/docs/content/data.md +26 -33
  143. package/src/ui/docs/content/detail.md +22 -4
  144. package/src/ui/docs/content/dialog.md +339 -31
  145. package/src/ui/docs/content/dictionary-value.md +17 -10
  146. package/src/ui/docs/content/dock.md +11 -3
  147. package/src/ui/docs/content/dot.md +8 -0
  148. package/src/ui/docs/content/drawer.md +27 -14
  149. package/src/ui/docs/content/empty-value.md +2 -2
  150. package/src/ui/docs/content/empty.md +19 -12
  151. package/src/ui/docs/content/events.md +4 -4
  152. package/src/ui/docs/content/field.md +34 -12
  153. package/src/ui/docs/content/getting-started.md +3 -3
  154. package/src/ui/docs/content/icon-picker.md +19 -4
  155. package/src/ui/docs/content/input-otp.md +20 -12
  156. package/src/ui/docs/content/input.md +121 -9
  157. package/src/ui/docs/content/item.md +27 -13
  158. package/src/ui/docs/content/kbd.md +19 -11
  159. package/src/ui/docs/content/label.md +12 -3
  160. package/src/ui/docs/content/log.md +4 -4
  161. package/src/ui/docs/content/markdown.md +7 -6
  162. package/src/ui/docs/content/mcp.md +13 -15
  163. package/src/ui/docs/content/menu.md +40 -16
  164. package/src/ui/docs/content/metric-card.md +13 -0
  165. package/src/ui/docs/content/observability.md +2 -2
  166. package/src/ui/docs/content/page.md +59 -6
  167. package/src/ui/docs/content/pagination.md +22 -17
  168. package/src/ui/docs/content/popover.md +22 -8
  169. package/src/ui/docs/content/progress.md +15 -16
  170. package/src/ui/docs/content/queue.md +5 -5
  171. package/src/ui/docs/content/radio-group.md +20 -12
  172. package/src/ui/docs/content/router.md +11 -6
  173. package/src/ui/docs/content/scheduler.md +4 -5
  174. package/src/ui/docs/content/scroll-area.md +12 -7
  175. package/src/ui/docs/content/select.md +47 -34
  176. package/src/ui/docs/content/semantic-context.md +2 -2
  177. package/src/ui/docs/content/separator.md +5 -5
  178. package/src/ui/docs/content/sidebar.md +329 -54
  179. package/src/ui/docs/content/skeleton.md +9 -2
  180. package/src/ui/docs/content/slider.md +8 -7
  181. package/src/ui/docs/content/spinner.md +8 -8
  182. package/src/ui/docs/content/split.md +29 -5
  183. package/src/ui/docs/content/storage.md +6 -8
  184. package/src/ui/docs/content/switch.md +8 -7
  185. package/src/ui/docs/content/table.md +13 -3
  186. package/src/ui/docs/content/tabs.md +28 -14
  187. package/src/ui/docs/content/testing.md +9 -11
  188. package/src/ui/docs/content/textarea.md +12 -4
  189. package/src/ui/docs/content/toast.md +47 -13
  190. package/src/ui/docs/content/toggle.md +75 -7
  191. package/src/ui/docs/content/tokens.md +7 -7
  192. package/src/ui/docs/content/tooltip.md +19 -11
  193. package/src/ui/docs/content/truncate.md +15 -8
  194. package/src/ui/docs/content/ui.md +24 -9
  195. package/src/ui/docs/content/upgrading.md +7 -8
  196. package/src/ui/docs/doc-client.tsx +5 -5
  197. package/src/ui/docs/doc.tsx +26 -14
  198. package/src/ui/docs/registry.tsx +25 -42
  199. package/src/ui/docs/standalone.tsx +2 -2
  200. package/src/ui/drivers/react.tsx +17 -12
  201. package/src/ui/lib/action-errors.ts +45 -0
  202. package/src/ui/lib/zod-pt-br.ts +31 -4
  203. package/src/ui/meta.ts +65 -95
  204. package/src/ui/react.tsx +17 -16
  205. package/src/ui/theme.css +60 -8
  206. package/src/vite/design.ts +6 -18
  207. package/src/ui/components/primitives/alert-dialog.tsx +0 -192
  208. package/src/ui/docs/content/alert-dialog.md +0 -73
  209. package/src/ui/docs/content/button-group.md +0 -71
  210. package/src/ui/docs/content/confirm.md +0 -120
  211. package/src/ui/docs/content/input-group.md +0 -79
  212. package/src/ui/docs/content/page-state.md +0 -45
  213. package/src/ui/docs/content/toggle-group.md +0 -81
@@ -14,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
  )}
@@ -116,14 +116,14 @@ function Item({
116
116
  }
117
117
 
118
118
  const itemMediaVariants = cva(
119
- "flex shrink-0 self-stretch items-center justify-center gap-2 [&_svg]:pointer-events-none",
119
+ "flex shrink-0 self-start items-center justify-center gap-2 [&_svg]:pointer-events-none",
120
120
  {
121
121
  variants: {
122
122
  variant: {
123
123
  default: "bg-transparent",
124
- icon: "min-h-8 w-8 rounded-sm border bg-muted [&_svg:not([class*='size-'])]:size-4",
124
+ icon: "size-8 rounded-sm border bg-muted [&_svg:not([class*='size-'])]:size-4",
125
125
  image:
126
- "min-h-10 w-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
126
+ "size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
127
127
  },
128
128
  },
129
129
  defaultVariants: { variant: "default" },
@@ -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>
@@ -6,7 +6,149 @@ import {
6
6
  OctagonXIcon,
7
7
  TriangleAlertIcon,
8
8
  } from 'lucide-react'
9
- import { Toaster as Sonner, toast, type ToasterProps } from 'sonner'
9
+ import {
10
+ Toaster as Sonner,
11
+ toast as sonnerToast,
12
+ type ExternalToast,
13
+ type ToasterProps,
14
+ } from 'sonner'
15
+ import { cn } from '../../lib/cn.ts'
16
+ import {
17
+ Button,
18
+ type ButtonContext,
19
+ type ButtonVariant,
20
+ } from './button.tsx'
21
+
22
+ type ToastId = string | number
23
+ type ToastMessage = React.ReactNode | (() => React.ReactNode)
24
+
25
+ export interface ToastAction {
26
+ key?: React.Key
27
+ label: React.ReactNode
28
+ onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
29
+ context?: ButtonContext
30
+ variant?: ButtonVariant
31
+ disabled?: boolean
32
+ }
33
+
34
+ export interface ToastOptions extends Omit<ExternalToast, 'action' | 'cancel'> {
35
+ actions?: readonly ToastAction[]
36
+ }
37
+
38
+ interface ToastApi {
39
+ (message: ToastMessage, options?: ToastOptions): ToastId
40
+ success: (message: ToastMessage, options?: ToastOptions) => ToastId
41
+ info: (message: ToastMessage, options?: ToastOptions) => ToastId
42
+ warning: (message: ToastMessage, options?: ToastOptions) => ToastId
43
+ error: (message: ToastMessage, options?: ToastOptions) => ToastId
44
+ message: (message: ToastMessage, options?: ToastOptions) => ToastId
45
+ loading: (message: ToastMessage, options?: ToastOptions) => ToastId
46
+ custom: typeof sonnerToast.custom
47
+ promise: typeof sonnerToast.promise
48
+ dismiss: typeof sonnerToast.dismiss
49
+ getHistory: typeof sonnerToast.getHistory
50
+ getToasts: typeof sonnerToast.getToasts
51
+ }
52
+
53
+ interface ToastMediaProps {
54
+ children: React.ReactNode
55
+ className: string
56
+ }
57
+
58
+ function ToastMedia({ children, className }: ToastMediaProps): React.ReactElement {
59
+ return (
60
+ <span
61
+ className={cn(
62
+ 'flex size-9 items-center justify-center rounded-md bg-current/10 [&_svg]:size-5',
63
+ className,
64
+ )}
65
+ >
66
+ {children}
67
+ </span>
68
+ )
69
+ }
70
+
71
+ interface ToastActionsProps {
72
+ actions: readonly ToastAction[]
73
+ getToastId: () => ToastId | undefined
74
+ }
75
+
76
+ function ToastActions({ actions, getToastId }: ToastActionsProps): React.ReactElement {
77
+ return (
78
+ <div data-slot="toast-actions" className="ms-auto flex shrink-0 items-center gap-2">
79
+ {actions.map((action, index) => {
80
+ const primary = index === actions.length - 1
81
+ return (
82
+ <Button
83
+ key={action.key ?? index}
84
+ type="button"
85
+ size="sm"
86
+ context={action.context ?? (primary ? 'primary' : 'neutral')}
87
+ variant={action.variant ?? (primary ? 'solid' : 'ghost')}
88
+ disabled={action.disabled}
89
+ onClick={(event) => {
90
+ action.onClick(event)
91
+ if (!event.defaultPrevented) {
92
+ const toastId = getToastId()
93
+ if (toastId !== undefined) sonnerToast.dismiss(toastId)
94
+ }
95
+ }}
96
+ >
97
+ {action.label}
98
+ </Button>
99
+ )
100
+ })}
101
+ </div>
102
+ )
103
+ }
104
+
105
+ function toSonnerOptions(
106
+ options: ToastOptions | undefined,
107
+ getToastId: () => ToastId | undefined,
108
+ ): ExternalToast | undefined {
109
+ if (options === undefined) return undefined
110
+ const { actions, ...rest } = options
111
+ if (actions === undefined) return rest
112
+ return {
113
+ ...rest,
114
+ action: actions.length > 0 ? <ToastActions actions={actions} getToastId={getToastId} /> : undefined,
115
+ }
116
+ }
117
+
118
+ type SonnerToastMethod = (message: ToastMessage, options?: ExternalToast) => ToastId
119
+
120
+ function dispatchToast(
121
+ method: SonnerToastMethod,
122
+ message: ToastMessage,
123
+ options?: ToastOptions,
124
+ ): ToastId {
125
+ let toastId: ToastId | undefined
126
+ toastId = method(message, toSonnerOptions(options, () => toastId))
127
+ return toastId
128
+ }
129
+
130
+ const toast: ToastApi = Object.assign(
131
+ (message: ToastMessage, options?: ToastOptions) => dispatchToast(sonnerToast, message, options),
132
+ {
133
+ success: (message: ToastMessage, options?: ToastOptions) =>
134
+ dispatchToast(sonnerToast.success, message, options),
135
+ info: (message: ToastMessage, options?: ToastOptions) =>
136
+ dispatchToast(sonnerToast.info, message, options),
137
+ warning: (message: ToastMessage, options?: ToastOptions) =>
138
+ dispatchToast(sonnerToast.warning, message, options),
139
+ error: (message: ToastMessage, options?: ToastOptions) =>
140
+ dispatchToast(sonnerToast.error, message, options),
141
+ message: (message: ToastMessage, options?: ToastOptions) =>
142
+ dispatchToast(sonnerToast.message, message, options),
143
+ loading: (message: ToastMessage, options?: ToastOptions) =>
144
+ dispatchToast(sonnerToast.loading, message, options),
145
+ custom: sonnerToast.custom,
146
+ promise: sonnerToast.promise,
147
+ dismiss: sonnerToast.dismiss,
148
+ getHistory: sonnerToast.getHistory,
149
+ getToasts: sonnerToast.getToasts,
150
+ },
151
+ )
10
152
 
11
153
  /**
12
154
  * Toaster temático (sonner) — monte 1x no root do app. Os toasts saem estilizados
@@ -17,18 +159,57 @@ import { Toaster as Sonner, toast, type ToasterProps } from 'sonner'
17
159
  * usamos next-themes — passe `theme` por prop se precisar forçar (default 'system').
18
160
  * Elevação dark preservada: `--normal-bg` aponta pro popover (não pro background).
19
161
  */
20
- export function Toaster({ ...props }: ToasterProps): React.ReactElement {
162
+ export function Toaster({
163
+ className,
164
+ icons,
165
+ style,
166
+ toastOptions,
167
+ ...props
168
+ }: ToasterProps): React.ReactElement {
21
169
  return (
22
170
  <Sonner
23
171
  data-slot="toaster"
24
172
  theme="system"
25
- className="toaster group"
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"
176
+ className={cn('toaster group', className)}
26
177
  icons={{
27
- success: <CircleCheckIcon className="size-4" />,
28
- info: <InfoIcon className="size-4" />,
29
- warning: <TriangleAlertIcon className="size-4" />,
30
- error: <OctagonXIcon className="size-4" />,
31
- loading: <Loader2Icon className="size-4 animate-spin" />,
178
+ success: (
179
+ <ToastMedia className="text-context-success-emphasis">
180
+ <CircleCheckIcon />
181
+ </ToastMedia>
182
+ ),
183
+ info: (
184
+ <ToastMedia className="text-context-info-emphasis">
185
+ <InfoIcon />
186
+ </ToastMedia>
187
+ ),
188
+ warning: (
189
+ <ToastMedia className="text-context-warning-emphasis">
190
+ <TriangleAlertIcon />
191
+ </ToastMedia>
192
+ ),
193
+ error: (
194
+ <ToastMedia className="text-context-danger-emphasis">
195
+ <OctagonXIcon />
196
+ </ToastMedia>
197
+ ),
198
+ loading: (
199
+ <ToastMedia className="text-context-neutral-emphasis">
200
+ <Loader2Icon className="animate-spin" />
201
+ </ToastMedia>
202
+ ),
203
+ ...icons,
204
+ }}
205
+ toastOptions={{
206
+ ...toastOptions,
207
+ classNames: {
208
+ ...toastOptions?.classNames,
209
+ toast: cn('opus-toast', toastOptions?.classNames?.toast),
210
+ icon: cn('opus-toast-icon', toastOptions?.classNames?.icon),
211
+ closeButton: cn('opus-toast-close', toastOptions?.classNames?.closeButton),
212
+ },
32
213
  }}
33
214
  style={
34
215
  {
@@ -36,6 +217,7 @@ export function Toaster({ ...props }: ToasterProps): React.ReactElement {
36
217
  '--normal-text': 'var(--popover-foreground)',
37
218
  '--normal-border': 'var(--border)',
38
219
  '--border-radius': 'var(--radius)',
220
+ ...style,
39
221
  } as React.CSSProperties
40
222
  }
41
223
  {...props}
@@ -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>
@@ -12,10 +12,16 @@
12
12
  * Nos dois, navegar emite `popstate`: host que guarda o path por conta própria acompanha.
13
13
  * `basePath` adapta o prefixo das URLs (`/docs` no Maestro, `/__docs` nos apps).
14
14
  */
15
- import { useEffect } from 'react'
15
+ import { useEffect, useState } from 'react'
16
16
  import { DOC_SECTIONS, type DocSection, type DocEntry } from './registry'
17
17
  import { Pane, Split } from '../components/patterns/split.tsx'
18
- import { PaneBody, Sidebar, SidebarNav, type SidebarNavGroup } from '../components/patterns/sidebar.tsx'
18
+ import {
19
+ PaneBody,
20
+ Sidebar,
21
+ SidebarGroupLabel,
22
+ SidebarItem,
23
+ SidebarTreeGroup,
24
+ } from '../components/patterns/sidebar.tsx'
19
25
  import { navigate, usePathname } from '../router.ts'
20
26
 
21
27
  function findPage(sections: DocSection[], slug: string): DocEntry | undefined {
@@ -42,6 +48,95 @@ export interface DocBrowserProps {
42
48
  documentTitle?: (pageTitle: string) => string
43
49
  }
44
50
 
51
+ function DocSidebarTree({
52
+ sections,
53
+ activeSlug,
54
+ onSelect,
55
+ }: {
56
+ sections: DocSection[]
57
+ activeSlug?: string
58
+ onSelect: (slug: string) => void
59
+ }): React.ReactElement {
60
+ const [closedGroups, setClosedGroups] = useState<Set<string>>(() => new Set())
61
+
62
+ useEffect(() => {
63
+ const activeGroup = sections.flatMap((section, sectionIndex) =>
64
+ section.groups.map((group, groupIndex) => ({
65
+ key: `${sectionIndex}:${groupIndex}`,
66
+ group,
67
+ })),
68
+ ).find(({ group }) => group.pages.some((page) => page.slug === activeSlug))
69
+
70
+ if (activeGroup === undefined) return
71
+ setClosedGroups((current) => {
72
+ if (!current.has(activeGroup.key)) return current
73
+ const next = new Set(current)
74
+ next.delete(activeGroup.key)
75
+ return next
76
+ })
77
+ }, [activeSlug, sections])
78
+
79
+ const toggleGroup = (key: string): void => {
80
+ setClosedGroups((current) => {
81
+ const next = new Set(current)
82
+ if (next.has(key)) next.delete(key)
83
+ else next.add(key)
84
+ return next
85
+ })
86
+ }
87
+
88
+ const renderPage = (page: DocEntry): React.ReactElement => (
89
+ <SidebarItem
90
+ key={page.slug}
91
+ label={page.title}
92
+ badge={page.badge === undefined ? undefined : (
93
+ <span className="rounded border border-border/60 px-1 font-mono text-[0.625rem] leading-tight text-muted-foreground/60">
94
+ {page.badge}
95
+ </span>
96
+ )}
97
+ active={page.slug === activeSlug}
98
+ onClick={() => onSelect(page.slug)}
99
+ />
100
+ )
101
+
102
+ return (
103
+ <nav aria-label="Navegação da documentação" className="space-y-4 p-2">
104
+ {sections.map((section, sectionIndex) => (
105
+ <div key={`${section.label}:${sectionIndex}`} className="space-y-0.5">
106
+ {section.label.trim() !== '' && (
107
+ <SidebarGroupLabel>{section.label}</SidebarGroupLabel>
108
+ )}
109
+ {section.groups.map((group, groupIndex) => {
110
+ const key = `${sectionIndex}:${groupIndex}`
111
+ if (group.pages.length === 0) return null
112
+ if ((group.label ?? '').trim() === '') {
113
+ return group.pages.map((page) => renderPage(page))
114
+ }
115
+
116
+ const expanded = !closedGroups.has(key)
117
+ return (
118
+ <div key={key} className="space-y-0.5">
119
+ <SidebarItem
120
+ label={group.label!}
121
+ icon={group.icon}
122
+ expanded={expanded}
123
+ onToggle={() => toggleGroup(key)}
124
+ onClick={() => toggleGroup(key)}
125
+ />
126
+ {expanded && (
127
+ <SidebarTreeGroup>
128
+ {group.pages.map((page) => renderPage(page))}
129
+ </SidebarTreeGroup>
130
+ )}
131
+ </div>
132
+ )
133
+ })}
134
+ </div>
135
+ ))}
136
+ </nav>
137
+ )
138
+ }
139
+
45
140
  export function DocBrowser({
46
141
  basePath = '/docs',
47
142
  path: controlledPath,
@@ -76,31 +171,15 @@ export function DocBrowser({
76
171
  if (page && documentTitle) document.title = documentTitle(page.title)
77
172
  }, [page, documentTitle])
78
173
 
79
- // Seção → grupo, e os grupos da seção → subgrupos. O tier do meio NÃO pode ser
80
- // achatado: `docSectionsFromFolder` o preenche a partir de sub-pasta (ou do
81
- // frontmatter `group:`), que é o caminho do `opusDocs({ source })`.
82
- const navGroups: SidebarNavGroup[] = sections.map((section) => ({
83
- label: section.label,
84
- subgroups: section.groups.map((g) => ({
85
- label: g.label,
86
- items: g.pages.map((p) => ({
87
- id: p.slug,
88
- label: p.title,
89
- badge: p.badge === undefined ? undefined : <span className="rounded border border-border/60 px-1 font-mono text-[0.625rem] leading-tight text-muted-foreground/60">{p.badge}</span>,
90
- })),
91
- })),
92
- }))
93
-
94
174
  return (
95
175
  <Split className="h-full">
96
- <Pane inset="none" className="w-56">
176
+ <Pane initialSize="14rem" inset="none">
97
177
  <Sidebar className="w-full">
98
178
  <PaneBody>
99
- <SidebarNav
100
- groups={navGroups}
101
- activeId={page?.slug}
102
- navLabel="Navegação da documentação"
103
- onSelect={(slug) => go(`${basePath}/${slug}`)}
179
+ <DocSidebarTree
180
+ sections={sections}
181
+ activeSlug={page?.slug}
182
+ onSelect={(nextSlug) => go(`${basePath}/${nextSlug}`)}
104
183
  />
105
184
  </PaneBody>
106
185
  </Sidebar>
@@ -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">