@softize/opus 13.0.0 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/cli-shared.mjs +131 -0
  6. package/bin/lib/copy.mjs +276 -6
  7. package/bin/lib/db.mjs +16 -74
  8. package/bin/lib/gen-openapi.mjs +3 -3
  9. package/bin/lib/gen-runner.mjs +1 -1
  10. package/bin/lib/gen.mjs +14 -69
  11. package/bin/lib/mcp.mjs +3 -1
  12. package/bin/lib/seed.mjs +5 -62
  13. package/docs/code-style.md +4 -1
  14. package/docs/ownership-vs-shadcn-lock.md +2 -3
  15. package/docs/protocol.md +7 -7
  16. package/docs/releasing.md +8 -2
  17. package/package.json +7 -3
  18. package/registry/instructions/opus.md +3 -3
  19. package/registry/templates/app/package.json +1 -1
  20. package/registry/templates/app/src/App.tsx +11 -6
  21. package/registry/templates/app/src/main.tsx +4 -4
  22. package/src/audit/drivers/console.ts +1 -0
  23. package/src/auth/drivers/better-auth.ts +1 -0
  24. package/src/auth/drivers/jwt.ts +1 -0
  25. package/src/cache/drivers/memory.ts +1 -0
  26. package/src/client/drivers/fetch.ts +2 -1
  27. package/src/core/actions.ts +6 -1
  28. package/src/core/audit.ts +9 -3
  29. package/src/core/contracts.ts +7 -0
  30. package/src/core/domain.ts +1 -1
  31. package/src/core/errors.ts +18 -15
  32. package/src/core/index.ts +4 -2
  33. package/src/core/package-version.ts +26 -0
  34. package/src/core/reactions.ts +1 -1
  35. package/src/core/runtime.ts +33 -23
  36. package/src/core/schedules.ts +1 -1
  37. package/src/core/types.ts +5 -6
  38. package/src/dsl/eval.ts +2 -2
  39. package/src/dsl/kysely.ts +2 -2
  40. package/src/dsl/loads.ts +1 -1
  41. package/src/dsl/parser.ts +5 -5
  42. package/src/events/drivers/mitt.ts +1 -0
  43. package/src/mcp/index.ts +2 -1
  44. package/src/observability/drivers/opentelemetry.ts +1 -0
  45. package/src/queue/drivers/bullmq.ts +3 -3
  46. package/src/scheduler/drivers/node-cron.ts +3 -2
  47. package/src/scheduler/every.ts +7 -7
  48. package/src/schema/openapi.ts +3 -3
  49. package/src/seed/index.ts +29 -0
  50. package/src/server/drivers/fastify.ts +5 -2
  51. package/src/server/drivers/node.ts +9 -6
  52. package/src/server/index.ts +3 -1
  53. package/src/storage/drivers/fs.ts +1 -0
  54. package/src/testing/index.ts +3 -3
  55. package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
  56. package/src/ui/components/patterns/confirm.tsx +2 -31
  57. package/src/ui/components/patterns/content-header.tsx +44 -137
  58. package/src/ui/components/patterns/data-state.tsx +42 -68
  59. package/src/ui/components/patterns/dock.tsx +20 -3
  60. package/src/ui/components/patterns/form.tsx +26 -22
  61. package/src/ui/components/patterns/list.tsx +18 -12
  62. package/src/ui/components/patterns/page-state.tsx +39 -51
  63. package/src/ui/components/patterns/page.tsx +37 -54
  64. package/src/ui/components/patterns/sidebar.tsx +17 -6
  65. package/src/ui/components/patterns/state-surface.tsx +148 -0
  66. package/src/ui/components/patterns/surface-header.tsx +119 -0
  67. package/src/ui/components/patterns/trigger.tsx +21 -25
  68. package/src/ui/components/patterns/view.tsx +29 -22
  69. package/src/ui/components/primitives/alert.tsx +1 -27
  70. package/src/ui/components/primitives/ask.tsx +3 -3
  71. package/src/ui/components/primitives/avatar.tsx +15 -5
  72. package/src/ui/components/primitives/badge.tsx +5 -41
  73. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  74. package/src/ui/components/primitives/button.tsx +39 -30
  75. package/src/ui/components/primitives/calendar.tsx +28 -2
  76. package/src/ui/components/primitives/carousel.tsx +3 -3
  77. package/src/ui/components/primitives/chat.tsx +1 -1
  78. package/src/ui/components/primitives/checkbox.tsx +1 -1
  79. package/src/ui/components/primitives/command.tsx +2 -2
  80. package/src/ui/components/primitives/control.ts +72 -0
  81. package/src/ui/components/primitives/copyable.tsx +1 -1
  82. package/src/ui/components/primitives/dialog.tsx +12 -7
  83. package/src/ui/components/primitives/dot.tsx +1 -25
  84. package/src/ui/components/primitives/drawer.tsx +10 -3
  85. package/src/ui/components/primitives/field.tsx +3 -3
  86. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  87. package/src/ui/components/primitives/input-group.tsx +12 -9
  88. package/src/ui/components/primitives/input-otp.tsx +1 -1
  89. package/src/ui/components/primitives/input.tsx +2 -2
  90. package/src/ui/components/primitives/item.tsx +3 -1
  91. package/src/ui/components/primitives/menu.tsx +1 -7
  92. package/src/ui/components/primitives/pagination.tsx +16 -8
  93. package/src/ui/components/primitives/progress.tsx +32 -3
  94. package/src/ui/components/primitives/radio-group.tsx +1 -1
  95. package/src/ui/components/primitives/resizable.tsx +3 -1
  96. package/src/ui/components/primitives/select.tsx +6 -6
  97. package/src/ui/components/primitives/slider.tsx +5 -1
  98. package/src/ui/components/primitives/sonner.tsx +3 -0
  99. package/src/ui/components/primitives/spinner.tsx +13 -16
  100. package/src/ui/components/primitives/switch.tsx +5 -1
  101. package/src/ui/components/primitives/tabs.tsx +6 -3
  102. package/src/ui/components/primitives/textarea.tsx +1 -1
  103. package/src/ui/components/primitives/toggle.tsx +9 -4
  104. package/src/ui/components/primitives/tooltip.tsx +1 -0
  105. package/src/ui/docs/changelog.tsx +1 -1
  106. package/src/ui/docs/content/action-form.md +36 -3
  107. package/src/ui/docs/content/action-list-dialog.md +2 -2
  108. package/src/ui/docs/content/action-list.md +13 -2
  109. package/src/ui/docs/content/action-trigger.md +12 -5
  110. package/src/ui/docs/content/action-view.md +11 -3
  111. package/src/ui/docs/content/ask.md +11 -0
  112. package/src/ui/docs/content/avatar.md +7 -3
  113. package/src/ui/docs/content/button.md +30 -14
  114. package/src/ui/docs/content/calendar.md +13 -0
  115. package/src/ui/docs/content/card.md +26 -0
  116. package/src/ui/docs/content/chat.md +20 -0
  117. package/src/ui/docs/content/cli.md +71 -19
  118. package/src/ui/docs/content/communication.md +36 -0
  119. package/src/ui/docs/content/composer.md +15 -0
  120. package/src/ui/docs/content/content.md +17 -1
  121. package/src/ui/docs/content/copyable.md +8 -0
  122. package/src/ui/docs/content/data-state.md +17 -13
  123. package/src/ui/docs/content/detail.md +19 -1
  124. package/src/ui/docs/content/dialog.md +1 -4
  125. package/src/ui/docs/content/dictionary-value.md +9 -2
  126. package/src/ui/docs/content/dock.md +8 -0
  127. package/src/ui/docs/content/dot.md +8 -0
  128. package/src/ui/docs/content/empty.md +2 -2
  129. package/src/ui/docs/content/getting-started.md +2 -2
  130. package/src/ui/docs/content/icon-picker.md +11 -0
  131. package/src/ui/docs/content/input.md +1 -1
  132. package/src/ui/docs/content/item.md +1 -1
  133. package/src/ui/docs/content/label.md +7 -0
  134. package/src/ui/docs/content/menu.md +6 -0
  135. package/src/ui/docs/content/metric-card.md +13 -0
  136. package/src/ui/docs/content/page.md +20 -4
  137. package/src/ui/docs/content/pagination.md +11 -9
  138. package/src/ui/docs/content/popover.md +6 -0
  139. package/src/ui/docs/content/progress.md +8 -11
  140. package/src/ui/docs/content/select.md +5 -5
  141. package/src/ui/docs/content/semantic-context.md +5 -4
  142. package/src/ui/docs/content/sidebar.md +13 -47
  143. package/src/ui/docs/content/skeleton.md +6 -0
  144. package/src/ui/docs/content/spinner.md +9 -6
  145. package/src/ui/docs/content/split.md +21 -0
  146. package/src/ui/docs/content/switch.md +1 -1
  147. package/src/ui/docs/content/tabs.md +1 -1
  148. package/src/ui/docs/content/textarea.md +7 -0
  149. package/src/ui/docs/content/toggle.md +1 -1
  150. package/src/ui/docs/content/tokens.md +4 -4
  151. package/src/ui/docs/content/truncate.md +8 -0
  152. package/src/ui/docs/content/ui.md +14 -0
  153. package/src/ui/docs/doc-client.tsx +5 -5
  154. package/src/ui/docs/doc.tsx +26 -14
  155. package/src/ui/docs/registry.tsx +5 -5
  156. package/src/ui/docs/standalone.tsx +2 -2
  157. package/src/ui/drivers/react.tsx +12 -12
  158. package/src/ui/lib/action-errors.ts +45 -0
  159. package/src/ui/lib/zod-pt-br.ts +31 -4
  160. package/src/ui/meta.ts +8 -8
  161. package/src/ui/react.tsx +10 -16
  162. package/src/ui/theme.css +10 -8
  163. package/src/vite/design.ts +6 -18
  164. package/src/ui/components/patterns/shell-nav.tsx +0 -147
@@ -3,31 +3,44 @@ import { Slot } from '@radix-ui/react-slot'
3
3
 
4
4
  import type { UiContext } from '../../../core/ui-context.ts'
5
5
  import { cn } from '../../lib/cn.ts'
6
- import type { ControlShape } from './control.ts'
6
+ import {
7
+ controlBase,
8
+ controlGlyphClass,
9
+ controlHeight,
10
+ controlSquare,
11
+ focusRing,
12
+ type ControlScale,
13
+ type ControlShape,
14
+ } from './control.ts'
7
15
  import { Spinner } from './spinner.tsx'
8
16
 
9
17
  export type ButtonContext = Extract<UiContext, 'neutral' | 'primary' | 'danger'>
10
18
  export type ButtonVariant = 'solid' | 'subtle' | 'outline' | 'ghost' | 'link'
11
- export type LegacyButtonVariant = 'default' | 'secondary' | 'destructive'
12
- export type ButtonSize = 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm' | 'icon-xs'
19
+ /** A escala completa de control.ts: alturas de linha e quadrados só-ícone. */
20
+ export type ButtonSize = ControlScale
13
21
 
14
22
  export interface ButtonStyleOptions {
23
+ /** Sem `context`, `solid`/`subtle` assumem `primary`; `outline`/`ghost`/`link` assumem `neutral`. */
15
24
  context?: ButtonContext | null
16
- variant?: ButtonVariant | LegacyButtonVariant | null
25
+ /** Tratamento visual. */
26
+ variant?: ButtonVariant | null
17
27
  size?: ButtonSize | null
18
28
  shape?: ControlShape | null
19
29
  }
20
30
 
21
31
  const baseClasses =
22
- 'inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 active:brightness-90 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4'
32
+ 'inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all disabled:pointer-events-none disabled:opacity-50 active:brightness-90 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 [&_svg]:pointer-events-none [&_svg]:shrink-0'
23
33
 
34
+ /** Padding e ritmo por tamanho; altura e quadrado vêm da escala compartilhada. */
24
35
  const sizeClasses: Record<ButtonSize, string> = {
25
- default: 'h-9 px-4 py-2 has-[>svg]:px-3',
26
- sm: 'h-8 gap-1.5 px-3 has-[>svg]:px-2.5',
27
- lg: 'h-10 px-6 has-[>svg]:px-4',
28
- icon: 'size-9',
29
- 'icon-sm': 'size-8',
30
- 'icon-xs': 'size-6',
36
+ xs: `${controlHeight.xs} gap-1 px-2 text-xs has-[>svg]:px-1.5`,
37
+ sm: `${controlHeight.sm} gap-1.5 px-3 has-[>svg]:px-2.5`,
38
+ default: `${controlHeight.default} px-4 py-2 has-[>svg]:px-3`,
39
+ lg: `${controlHeight.lg} px-6 has-[>svg]:px-4`,
40
+ 'icon-xs': controlSquare['icon-xs'],
41
+ 'icon-sm': controlSquare['icon-sm'],
42
+ icon: controlSquare.icon,
43
+ 'icon-lg': controlSquare['icon-lg'],
31
44
  }
32
45
 
33
46
  const solidClasses: Record<ButtonContext, string> = {
@@ -64,19 +77,17 @@ export function resolveButtonStyle({ context, variant }: ButtonStyleOptions = {}
64
77
  context: ButtonContext
65
78
  variant: ButtonVariant
66
79
  } {
67
- const legacy = variant === 'default' || variant === 'secondary' || variant === 'destructive'
68
- if (context != null && legacy) {
69
- throw new Error('Button não permite combinar context com uma variante semântica legada.')
70
- }
71
- if (variant === 'default') return { context: 'primary', variant: 'solid' }
72
- if (variant === 'secondary') return { context: 'neutral', variant: 'solid' }
73
- if (variant === 'destructive') return { context: 'danger', variant: 'solid' }
74
- return { context: context ?? 'primary', variant: variant ?? 'solid' }
80
+ const resolvedVariant: ButtonVariant = variant ?? 'solid'
81
+ // Tratamentos de superfície (solid/subtle) pedem destaque; os leves (outline/ghost/link)
82
+ // são o chrome discreto da casa e ganham cor quando o contexto é declarado.
83
+ const defaultContext: ButtonContext =
84
+ resolvedVariant === 'solid' || resolvedVariant === 'subtle' ? 'primary' : 'neutral'
85
+ return { context: context ?? defaultContext, variant: resolvedVariant }
75
86
  }
76
87
 
77
88
  /**
78
- * Classes do Button. `context` seleciona a família semântica e `variant` seleciona
79
- * o tratamento visual. `default`, `secondary` e `destructive` são aliases temporários.
89
+ * Classes do Button. `context` seleciona a família semântica, `variant` o tratamento visual e
90
+ * `size` a medida na escala compartilhada de control.ts.
80
91
  */
81
92
  export function buttonVariants(options: ButtonStyleOptions = {}): string {
82
93
  const resolved = resolveButtonStyle(options)
@@ -92,8 +103,10 @@ export function buttonVariants(options: ButtonStyleOptions = {}): string {
92
103
  : linkClasses[resolved.context]
93
104
  return cn(
94
105
  baseClasses,
106
+ focusRing,
95
107
  variantClasses,
96
108
  sizeClasses[options.size ?? 'default'],
109
+ controlGlyphClass(options.size),
97
110
  options.shape === 'pill' && 'rounded-full',
98
111
  )
99
112
  }
@@ -103,8 +116,9 @@ export interface ButtonProps extends React.ComponentProps<'button'>, ButtonStyle
103
116
  asChild?: boolean
104
117
  /** Estado de ação em andamento; mostra Spinner e desabilita o controle. */
105
118
  busy?: boolean
106
- /** Ícone à esquerda. No `busy`, o Spinner ocupa o mesmo espaço. */
107
- icon?: React.ElementType
119
+ /** Ícone à esquerda, como nó (`icon={<Plus />}`); o tamanho do glifo vem do `size`. No
120
+ * `busy`, o Spinner ocupa o mesmo espaço. */
121
+ icon?: React.ReactNode
108
122
  }
109
123
 
110
124
  export function Button({
@@ -115,14 +129,13 @@ export function Button({
115
129
  shape,
116
130
  asChild = false,
117
131
  busy = false,
118
- icon: Icon,
132
+ icon,
119
133
  disabled,
120
134
  children,
121
135
  ...props
122
136
  }: ButtonProps): React.ReactElement {
123
137
  const Comp = asChild ? Slot : 'button'
124
138
  const resolved = resolveButtonStyle({ context, variant })
125
- const iconSize = size === 'sm' ? 'size-3' : 'size-4'
126
139
  return (
127
140
  <Comp
128
141
  data-slot="button"
@@ -136,11 +149,7 @@ export function Button({
136
149
  children
137
150
  ) : (
138
151
  <>
139
- {busy ? (
140
- <Spinner size={size === 'sm' ? 'sm' : 'default'} />
141
- ) : Icon ? (
142
- <Icon className={iconSize} />
143
- ) : null}
152
+ {busy ? <Spinner size={controlBase(size)} /> : icon}
144
153
  {children}
145
154
  </>
146
155
  )}
@@ -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,74 @@
1
1
  /** Geometria compartilhada pelos controles inline da casa. */
2
2
  export type ControlShape = 'default' | 'pill'
3
+
4
+ /**
5
+ * A escala única de `size` dos controles (14.0.0). Um nome, uma medida — em qualquer
6
+ * componente que aceite `size`, `sm` é a mesma altura de linha e `icon-sm` é o mesmo quadrado.
7
+ * Cada componente aceita o subconjunto coerente com seu papel, nunca um nome com outra medida.
8
+ *
9
+ * - Altura de linha (`xs` · `sm` · `default` · `lg`): 1.5 · 2 · 2.25 · 2.5rem.
10
+ * - Quadrado só-ícone (`icon-xs` · `icon-sm` · `icon` · `icon-lg`): 1.5 · 1.75 · 2.25 · 2.5rem.
11
+ * `icon-sm` é 1.75rem de propósito: é o degrau da ação que mora dentro de uma linha densa
12
+ * (o que os consumidores forçavam com `size-7`), menor que o botão de texto `sm` ao lado.
13
+ * - Glifo (o svg dentro do controle daquele tamanho): 0.875 · 0.875 · 1 · 1.25rem. O Spinner
14
+ * usa esta medida, por isso `busy` substitui o ícone sem mexer na largura do botão.
15
+ */
16
+ export type ControlSize = 'xs' | 'sm' | 'default' | 'lg'
17
+ export type ControlIconSize = 'icon-xs' | 'icon-sm' | 'icon' | 'icon-lg'
18
+ export type ControlScale = ControlSize | ControlIconSize
19
+
20
+ export const controlHeight: Record<ControlSize, string> = {
21
+ xs: 'h-6',
22
+ sm: 'h-8',
23
+ default: 'h-9',
24
+ lg: 'h-10',
25
+ }
26
+
27
+ export const controlSquare: Record<ControlIconSize, string> = {
28
+ 'icon-xs': 'size-6',
29
+ 'icon-sm': 'size-7',
30
+ icon: 'size-9',
31
+ 'icon-lg': 'size-10',
32
+ }
33
+
34
+ export const controlGlyph: Record<ControlSize, string> = {
35
+ xs: 'size-3.5',
36
+ sm: 'size-3.5',
37
+ default: 'size-4',
38
+ lg: 'size-5',
39
+ }
40
+
41
+ /** A altura de linha por trás de um tamanho da escala (`icon-sm` → `sm`). */
42
+ export function controlBase(size: ControlScale | null | undefined): ControlSize {
43
+ switch (size) {
44
+ case 'icon-xs':
45
+ return 'xs'
46
+ case 'icon-sm':
47
+ return 'sm'
48
+ case 'icon-lg':
49
+ return 'lg'
50
+ case 'icon':
51
+ case null:
52
+ case undefined:
53
+ return 'default'
54
+ default:
55
+ return size
56
+ }
57
+ }
58
+
59
+ /** Fallback de tamanho do svg descendente, respeitando um `size-*` explícito no próprio ícone. */
60
+ export function controlGlyphClass(size: ControlScale | null | undefined): string {
61
+ return `[&_svg:not([class*=size-])]:${controlGlyph[controlBase(size)]}`
62
+ }
63
+
64
+ /**
65
+ * Anel de foco da casa: fino (0.1875rem), translúcido e encostado na borda — sem offset.
66
+ * Todo controle focável usa esta constante; espessuras próprias (`ring-2`, `ring-4`) ou
67
+ * `ring-offset-*` quebram a leitura de "foco" que o tema calibrou (ver theme.css, `--ring`).
68
+ */
69
+ export const focusRing =
70
+ 'focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50'
71
+
72
+ /** O mesmo anel, disparado por um descendente focado (grupos que envolvem um campo). */
73
+ export const focusRingWithin =
74
+ '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,17 +5,9 @@ import { cn } from '../../lib/cn.ts'
5
5
 
6
6
  export type DotContext = UiContext
7
7
  export type DotVariant = 'solid' | 'outline'
8
- export type LegacyDotVariant =
9
- | 'default'
10
- | 'secondary'
11
- | 'destructive'
12
- | 'success'
13
- | 'warning'
14
- | 'info'
15
-
16
8
  interface DotStyleOptions {
17
9
  context?: DotContext | null
18
- variant?: DotVariant | LegacyDotVariant | null
10
+ variant?: DotVariant | null
19
11
  }
20
12
 
21
13
  const solidClasses: Record<DotContext, string> = {
@@ -40,22 +32,6 @@ export function resolveDotStyle({ context, variant }: DotStyleOptions = {}): {
40
32
  context: DotContext
41
33
  variant: DotVariant
42
34
  } {
43
- const legacy =
44
- variant === 'default' ||
45
- variant === 'secondary' ||
46
- variant === 'destructive' ||
47
- variant === 'success' ||
48
- variant === 'warning' ||
49
- variant === 'info'
50
- if (context != null && legacy) {
51
- throw new Error('Dot não permite combinar context com uma variante semântica legada.')
52
- }
53
- if (variant === 'default') return { context: 'primary', variant: 'solid' }
54
- if (variant === 'secondary') return { context: 'neutral', variant: 'solid' }
55
- if (variant === 'destructive') return { context: 'danger', variant: 'solid' }
56
- if (variant === 'success' || variant === 'warning' || variant === 'info') {
57
- return { context: variant, variant: 'solid' }
58
- }
59
35
  return { context: context ?? 'neutral', variant: variant ?? 'solid' }
60
36
  }
61
37
 
@@ -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
  >
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority"
3
3
 
4
4
  import { cn } from '../../lib/cn.ts'
5
5
  import type { ControlShape } from './control.ts'
6
- import { Button } from './button.tsx'
6
+ import { Button, type ButtonProps } from './button.tsx'
7
7
  import { Input } from './input.tsx'
8
8
  import { Textarea } from './textarea.tsx'
9
9
 
@@ -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
  )}
@@ -79,16 +79,16 @@ function InputGroupAddon({
79
79
  )
80
80
  }
81
81
 
82
+ /** Só o acabamento por tamanho: altura, quadrado e glifo vêm da escala de control.ts via Button. */
82
83
  const inputGroupButtonVariants = cva(
83
84
  "flex items-center gap-2 text-sm shadow-none",
84
85
  {
85
86
  variants: {
86
87
  size: {
87
- xs: "h-6 gap-1 rounded-[calc(var(--radius)-0.3125rem)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
88
- sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
89
- "icon-xs":
90
- "size-6 rounded-[calc(var(--radius)-0.3125rem)] p-0 has-[>svg]:p-0",
91
- "icon-sm": "size-8 p-0 has-[>svg]:p-0",
88
+ xs: "rounded-[calc(var(--radius)-0.3125rem)] has-[>svg]:px-2",
89
+ sm: "rounded-md",
90
+ "icon-xs": "rounded-[calc(var(--radius)-0.3125rem)] p-0 has-[>svg]:p-0",
91
+ "icon-sm": "p-0 has-[>svg]:p-0",
92
92
  },
93
93
  },
94
94
  defaultVariants: {
@@ -97,19 +97,22 @@ const inputGroupButtonVariants = cva(
97
97
  }
98
98
  )
99
99
 
100
+ /** Subconjunto da escala que cabe dentro de um campo: `xs`, `sm`, `icon-xs` e `icon-sm`. */
101
+ type InputGroupButtonSize = Extract<NonNullable<ButtonProps['size']>, 'xs' | 'sm' | 'icon-xs' | 'icon-sm'>
102
+
100
103
  function InputGroupButton({
101
104
  className,
102
105
  type = "button",
103
106
  variant = "ghost",
104
107
  size = "xs",
105
108
  ...props
106
- }: Omit<React.ComponentProps<typeof Button>, "size"> &
107
- VariantProps<typeof inputGroupButtonVariants>) {
109
+ }: Omit<ButtonProps, 'size'> & { size?: InputGroupButtonSize }) {
108
110
  return (
109
111
  <Button
110
112
  type={type}
111
113
  data-size={size}
112
114
  variant={variant}
115
+ size={size}
113
116
  className={cn(inputGroupButtonVariants({ size }), className)}
114
117
  {...props}
115
118
  />
@@ -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
  )}
@@ -3,6 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { Slot } from "radix-ui";
4
4
 
5
5
  import { cn } from "../../lib/cn.ts";
6
+ import type { ControlSize } from "./control.ts";
6
7
  import { Separator } from "./separator.tsx";
7
8
 
8
9
  const itemGroupVariants = cva(
@@ -72,10 +73,11 @@ const itemVariants = cva(
72
73
  outline: "border-border",
73
74
  muted: "bg-muted/50",
74
75
  },
76
+ // Nomes da escala de control.ts; a linha não tem altura fixa, `sm` aperta o respiro.
75
77
  size: {
76
78
  default: "gap-4 p-4",
77
79
  sm: "gap-2.5 px-4 py-3",
78
- },
80
+ } satisfies Partial<Record<ControlSize, string>>,
79
81
  },
80
82
  defaultVariants: { variant: "default", size: "default" },
81
83
  },