@softize/opus 18.0.1 → 18.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/PROMOTED.md +4 -5
  3. package/README.md +5 -4
  4. package/bin/cli.mjs +4 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +3 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
  8. package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
  9. package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
  10. package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +54 -0
  11. package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
  12. package/docs/adr/0019-productive-surfaces-use-compact-density.md +65 -0
  13. package/docs/code-style.md +2 -2
  14. package/docs/consumer-upgrade-propagation.md +1 -1
  15. package/docs/data-products.md +5 -3
  16. package/docs/protocol.md +6 -6
  17. package/docs/relative-unit-scale.md +9 -2
  18. package/docs/releasing.md +28 -4
  19. package/package.json +1 -1
  20. package/registry/skills/build-opus-ui/references/evaluations.md +3 -1
  21. package/registry/skills/build-opus-ui/references/ui-patterns.md +20 -4
  22. package/src/auth/drivers/jwt.ts +2 -1
  23. package/src/core/runtime.ts +32 -5
  24. package/src/core/types.ts +16 -7
  25. package/src/mcp/index.ts +13 -1
  26. package/src/ui/components/patterns/action-list-dialog.tsx +2 -2
  27. package/src/ui/components/patterns/content-header.tsx +2 -2
  28. package/src/ui/components/patterns/form-dialog.tsx +7 -2
  29. package/src/ui/components/patterns/form.tsx +1 -1
  30. package/src/ui/components/patterns/list.tsx +239 -47
  31. package/src/ui/components/patterns/presentation.tsx +7 -5
  32. package/src/ui/components/patterns/sidebar.tsx +1 -1
  33. package/src/ui/components/patterns/state-surface.tsx +2 -2
  34. package/src/ui/components/patterns/surface-header.tsx +4 -4
  35. package/src/ui/components/primitives/alert.tsx +2 -2
  36. package/src/ui/components/primitives/breadcrumb.tsx +1 -1
  37. package/src/ui/components/primitives/button-group.tsx +1 -1
  38. package/src/ui/components/primitives/button.tsx +3 -3
  39. package/src/ui/components/primitives/calendar.tsx +1 -1
  40. package/src/ui/components/primitives/card.tsx +1 -1
  41. package/src/ui/components/primitives/close-button.tsx +40 -0
  42. package/src/ui/components/primitives/detail.tsx +68 -30
  43. package/src/ui/components/primitives/dialog.tsx +36 -21
  44. package/src/ui/components/primitives/drawer.tsx +26 -19
  45. package/src/ui/components/primitives/empty-value.tsx +3 -3
  46. package/src/ui/components/primitives/empty.tsx +1 -1
  47. package/src/ui/components/primitives/field.tsx +12 -12
  48. package/src/ui/components/primitives/icon-picker.tsx +1 -1
  49. package/src/ui/components/primitives/input-group.tsx +1 -1
  50. package/src/ui/components/primitives/input.tsx +2 -2
  51. package/src/ui/components/primitives/item.tsx +5 -5
  52. package/src/ui/components/primitives/pagination.tsx +4 -4
  53. package/src/ui/components/primitives/radio-group.tsx +1 -1
  54. package/src/ui/components/primitives/select.tsx +3 -3
  55. package/src/ui/components/primitives/table.tsx +26 -17
  56. package/src/ui/components/primitives/tabs.tsx +80 -23
  57. package/src/ui/components/primitives/textarea.tsx +1 -1
  58. package/src/ui/components/primitives/toggle-group.tsx +9 -2
  59. package/src/ui/docs/content/action-form-dialog.md +11 -4
  60. package/src/ui/docs/content/action-form.md +13 -3
  61. package/src/ui/docs/content/action-list-dialog.md +5 -7
  62. package/src/ui/docs/content/action-list.md +53 -5
  63. package/src/ui/docs/content/action-trigger.md +9 -5
  64. package/src/ui/docs/content/action-view.md +12 -8
  65. package/src/ui/docs/content/actions.md +36 -13
  66. package/src/ui/docs/content/ai.md +26 -7
  67. package/src/ui/docs/content/alert.md +6 -3
  68. package/src/ui/docs/content/aspect-ratio.md +2 -2
  69. package/src/ui/docs/content/auth.md +25 -10
  70. package/src/ui/docs/content/avatar.md +1 -1
  71. package/src/ui/docs/content/badge.md +2 -2
  72. package/src/ui/docs/content/breadcrumb.md +3 -2
  73. package/src/ui/docs/content/button.md +33 -8
  74. package/src/ui/docs/content/calendar.md +1 -1
  75. package/src/ui/docs/content/card.md +1 -1
  76. package/src/ui/docs/content/carousel.md +14 -3
  77. package/src/ui/docs/content/chat.md +1 -1
  78. package/src/ui/docs/content/cli.md +13 -7
  79. package/src/ui/docs/content/command.md +34 -2
  80. package/src/ui/docs/content/composer.md +1 -1
  81. package/src/ui/docs/content/content.md +5 -4
  82. package/src/ui/docs/content/customization.md +12 -2
  83. package/src/ui/docs/content/cycle.md +7 -5
  84. package/src/ui/docs/content/data-state.md +6 -5
  85. package/src/ui/docs/content/data.md +3 -3
  86. package/src/ui/docs/content/detail.md +12 -10
  87. package/src/ui/docs/content/dialog.md +14 -7
  88. package/src/ui/docs/content/dictionary-value.md +1 -1
  89. package/src/ui/docs/content/dock.md +23 -2
  90. package/src/ui/docs/content/dot.md +0 -2
  91. package/src/ui/docs/content/drawer.md +7 -4
  92. package/src/ui/docs/content/empty-value.md +4 -4
  93. package/src/ui/docs/content/empty.md +1 -4
  94. package/src/ui/docs/content/events.md +1 -1
  95. package/src/ui/docs/content/field.md +21 -12
  96. package/src/ui/docs/content/getting-started.md +4 -2
  97. package/src/ui/docs/content/icon-picker.md +2 -2
  98. package/src/ui/docs/content/input-otp.md +2 -0
  99. package/src/ui/docs/content/input.md +2 -3
  100. package/src/ui/docs/content/item.md +6 -3
  101. package/src/ui/docs/content/kbd.md +2 -1
  102. package/src/ui/docs/content/mcp.md +10 -4
  103. package/src/ui/docs/content/menu.md +27 -0
  104. package/src/ui/docs/content/page.md +20 -6
  105. package/src/ui/docs/content/pagination.md +9 -2
  106. package/src/ui/docs/content/popover.md +2 -2
  107. package/src/ui/docs/content/presentation.md +48 -47
  108. package/src/ui/docs/content/progress.md +2 -6
  109. package/src/ui/docs/content/runtime.md +8 -5
  110. package/src/ui/docs/content/scheduler.md +1 -1
  111. package/src/ui/docs/content/select.md +13 -8
  112. package/src/ui/docs/content/sidebar.md +3 -2
  113. package/src/ui/docs/content/skeleton.md +1 -1
  114. package/src/ui/docs/content/slider.md +4 -4
  115. package/src/ui/docs/content/spinner.md +3 -3
  116. package/src/ui/docs/content/tabs.md +22 -12
  117. package/src/ui/docs/content/testing.md +4 -2
  118. package/src/ui/docs/content/toast.md +5 -6
  119. package/src/ui/docs/content/toggle.md +37 -0
  120. package/src/ui/docs/content/tokens.md +45 -2
  121. package/src/ui/docs/content/tooltip.md +4 -3
  122. package/src/ui/docs/content/truncate.md +3 -2
  123. package/src/ui/docs/content/ui.md +3 -1
  124. package/src/ui/docs/content/upgrading.md +43 -13
  125. package/src/ui/docs/doc-client.tsx +1 -1
  126. package/src/ui/docs/registry.tsx +30 -5
  127. package/src/ui/meta.ts +4 -4
  128. package/src/ui/react.tsx +1 -0
  129. package/src/ui/theme.css +3 -0
@@ -8,11 +8,11 @@ export type DetailGroupOrientation = 'vertical' | 'horizontal'
8
8
  export type DetailGroupColumns = 1 | 2 | 3 | 4 | 'auto'
9
9
 
10
10
  export interface DetailGroupProps extends React.ComponentProps<'dl'> {
11
- /** `framed` aplica a superfície e a moldura canônicas ao conjunto. */
11
+ /** `framed` aplica a moldura canônica ao conjunto; na orientação horizontal, a coluna de rótulos ganha fundo sutil. */
12
12
  variant?: DetailGroupVariant
13
13
  /** Exibe hairlines somente entre os campos, sem exigir moldura externa. */
14
14
  dividers?: boolean
15
- /** Número responsivo de colunas ou distribuição automática por largura mínima. */
15
+ /** Número fixo de colunas ou distribuição automática por largura mínima. */
16
16
  columns?: DetailGroupColumns
17
17
  /** Organiza a chave sobre o valor ou ao lado dele em cada campo. */
18
18
  orientation?: DetailGroupOrientation
@@ -20,39 +20,57 @@ export interface DetailGroupProps extends React.ComponentProps<'dl'> {
20
20
 
21
21
  const columnClasses: Record<DetailGroupColumns, string> = {
22
22
  1: 'grid-cols-1',
23
- 2: 'grid-cols-1 sm:grid-cols-2',
24
- 3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
25
- 4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
23
+ 2: 'grid-cols-2',
24
+ 3: 'grid-cols-3',
25
+ 4: 'grid-cols-4',
26
26
  auto: 'grid-cols-[repeat(auto-fit,minmax(min(100%,14rem),1fr))]',
27
27
  }
28
28
 
29
+ const DetailGroupLayoutContext = React.createContext({
30
+ splitHorizontalField: false,
31
+ })
32
+
29
33
  function DetailGroup({
30
34
  className,
35
+ children,
36
+ style,
31
37
  variant = 'plain',
32
38
  dividers = false,
33
39
  columns = 1,
34
40
  orientation = 'vertical',
35
41
  ...props
36
42
  }: DetailGroupProps): React.ReactElement {
43
+ const splitHorizontalField = variant === 'framed' && orientation === 'horizontal'
44
+
37
45
  return (
38
- <dl
39
- data-slot="detail-group"
40
- data-variant={variant}
41
- data-dividers={dividers}
42
- data-orientation={orientation}
43
- className={cn(
44
- 'group/detail-group grid min-w-0 [--detail-label-width:7rem]',
45
- columnClasses[columns],
46
- variant === 'plain' && !dividers && 'gap-x-6 gap-y-4',
47
- dividers && 'gap-px overflow-hidden',
48
- (variant === 'framed' || dividers) &&
49
- '[&>[data-slot=detail-field]]:p-3',
50
- variant === 'framed' &&
51
- 'overflow-hidden rounded-lg border border-border bg-card text-card-foreground',
52
- className,
53
- )}
54
- {...props}
55
- />
46
+ <DetailGroupLayoutContext.Provider value={{ splitHorizontalField }}>
47
+ <dl
48
+ data-slot="detail-group"
49
+ data-variant={variant}
50
+ data-dividers={dividers}
51
+ data-orientation={orientation}
52
+ className={cn(
53
+ 'group/detail-group grid min-w-0 [--detail-label-width:7rem]',
54
+ columnClasses[columns],
55
+ variant === 'plain' && !dividers && 'gap-x-6 gap-y-4',
56
+ dividers && 'gap-px overflow-hidden',
57
+ (variant === 'framed' || dividers) &&
58
+ !splitHorizontalField &&
59
+ '[&>[data-slot=detail-field]]:p-3',
60
+ variant === 'framed' &&
61
+ 'overflow-hidden rounded-lg border border-border bg-transparent text-foreground',
62
+ // Default da coluna de rótulos como CLASSE, antes do `className`: assim uma classe do
63
+ // consumidor (`[--detail-label-width:9rem]`) a substitui. Em `style` inline, o default
64
+ // vencia qualquer classe e só `style` conseguia ajustar a largura.
65
+ splitHorizontalField && '[--detail-label-width:8.5rem]',
66
+ className,
67
+ )}
68
+ style={style}
69
+ {...props}
70
+ >
71
+ {children}
72
+ </dl>
73
+ </DetailGroupLayoutContext.Provider>
56
74
  )
57
75
  }
58
76
 
@@ -62,7 +80,8 @@ export interface DetailFieldProps extends Omit<
62
80
  > {
63
81
  label: React.ReactNode
64
82
  /** `null`, `undefined`, string vazia ou só com espaços renderizam a ausência
65
- * (“Não informado” ou `empty`); `0` e `false` seguem como valores. */
83
+ * como travessão; “Não informado” ou `empty` preservam o significado para
84
+ * leitura assistiva. `0` e `false` seguem como valores. */
66
85
  value: React.ReactNode
67
86
  /** Ícone decorativo que identifica o campo antes do par chave/valor. */
68
87
  icon?: React.ReactNode
@@ -76,29 +95,45 @@ function DetailField({
76
95
  value,
77
96
  icon,
78
97
  empty,
98
+ style,
79
99
  ...props
80
100
  }: DetailFieldProps): React.ReactElement {
101
+ const { splitHorizontalField } = React.useContext(DetailGroupLayoutContext)
81
102
  const content = !isAbsentValue(value)
82
103
  ? value
83
104
  : empty === undefined || typeof empty === 'string'
84
- ? <EmptyValue {...(empty !== undefined ? { label: empty } : {})} />
105
+ ? <EmptyValue compact {...(empty !== undefined ? { label: empty } : {})} />
85
106
  : empty
86
107
  return (
87
108
  <div
88
109
  data-slot="detail-field"
89
110
  className={cn(
90
111
  'grid min-w-0 items-start',
91
- 'group-data-[orientation=vertical]/detail-group:grid-cols-1 group-data-[orientation=horizontal]/detail-group:grid-cols-[minmax(0,var(--detail-label-width))_minmax(0,1fr)]',
92
- 'group-data-[orientation=vertical]/detail-group:gap-y-1 group-data-[orientation=horizontal]/detail-group:items-baseline group-data-[orientation=horizontal]/detail-group:gap-x-3',
112
+ 'group-data-[orientation=vertical]/detail-group:grid-cols-1',
113
+ !splitHorizontalField &&
114
+ 'group-data-[orientation=horizontal]/detail-group:grid-cols-[minmax(0,var(--detail-label-width))_minmax(0,1fr)] group-data-[orientation=horizontal]/detail-group:items-baseline group-data-[orientation=horizontal]/detail-group:gap-x-3',
115
+ splitHorizontalField && 'items-stretch gap-x-0',
116
+ 'group-data-[orientation=vertical]/detail-group:gap-y-1',
93
117
  'group-data-[dividers=true]/detail-group:outline group-data-[dividers=true]/detail-group:outline-1 group-data-[dividers=true]/detail-group:outline-border',
94
- 'group-data-[variant=framed]/detail-group:bg-card group-data-[variant=framed]/detail-group:text-card-foreground',
118
+ 'group-data-[variant=framed]/detail-group:bg-transparent group-data-[variant=framed]/detail-group:text-foreground',
95
119
  className,
96
120
  )}
121
+ style={
122
+ splitHorizontalField
123
+ ? {
124
+ gridTemplateColumns: 'minmax(0, var(--detail-label-width)) minmax(0, 1fr)',
125
+ ...style,
126
+ }
127
+ : style
128
+ }
97
129
  {...props}
98
130
  >
99
131
  <dt
100
132
  data-slot="detail-field-label"
101
- className="flex items-center gap-2.5 text-xs leading-snug font-medium text-muted-foreground group-data-[orientation=horizontal]/detail-group:text-sm"
133
+ className={cn(
134
+ 'flex items-center gap-2.5 text-xs leading-snug font-medium text-muted-foreground group-data-[orientation=horizontal]/detail-group:text-sm',
135
+ splitHorizontalField && 'bg-muted/30 p-3 dark:bg-input/30',
136
+ )}
102
137
  >
103
138
  {icon !== undefined && (
104
139
  <span
@@ -113,7 +148,10 @@ function DetailField({
113
148
  </dt>
114
149
  <dd
115
150
  data-slot="detail-field-value"
116
- className="min-w-0 text-sm leading-snug font-normal"
151
+ className={cn(
152
+ 'min-w-0 text-sm leading-snug font-normal',
153
+ splitHorizontalField && 'flex items-center p-3',
154
+ )}
117
155
  >
118
156
  {content}
119
157
  </dd>
@@ -1,9 +1,11 @@
1
1
  import * as React from "react";
2
2
  import * as DialogPrimitive from "@radix-ui/react-dialog";
3
3
  import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
4
- import { XIcon } from "lucide-react";
5
4
  import { cn } from "../../lib/cn.ts";
6
- import { Button } from "./button.tsx";
5
+ import {
6
+ CloseButton,
7
+ type CloseButtonSize,
8
+ } from "./close-button.tsx";
7
9
 
8
10
  export type DialogMode = "default" | "alert";
9
11
 
@@ -19,11 +21,13 @@ const DialogContext = React.createContext<DialogContextValue>({
19
21
  interface DialogContentContextValue {
20
22
  showCloseButton: boolean;
21
23
  closeDisabled: boolean;
24
+ closeSize: CloseButtonSize;
22
25
  }
23
26
 
24
27
  const DialogContentContext = React.createContext<DialogContentContextValue>({
25
28
  showCloseButton: false,
26
29
  closeDisabled: false,
30
+ closeSize: "sm",
27
31
  });
28
32
 
29
33
  function useDialogContext(): DialogContextValue {
@@ -126,9 +130,9 @@ export function DialogClose({
126
130
  );
127
131
  }
128
132
 
129
- /** Véu compartilhado por Dialog e Drawer: mesmo escurecimento, desfoque e animação. */
133
+ /** Véu compartilhado por Dialog e Drawer: mesmo escurecimento e animação. */
130
134
  export const overlayClasses =
131
- "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";
135
+ "fixed inset-0 z-50 bg-black/30 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0";
132
136
 
133
137
  export function DialogOverlay({
134
138
  className,
@@ -151,18 +155,16 @@ export function DialogOverlay({
151
155
  );
152
156
  }
153
157
 
154
- function DialogCloseButton({ disabled = false }: { disabled?: boolean }) {
158
+ function DialogCloseButton({
159
+ disabled = false,
160
+ size,
161
+ }: {
162
+ disabled?: boolean;
163
+ size: CloseButtonSize;
164
+ }) {
155
165
  return (
156
166
  <DialogPrimitive.Close data-slot="dialog-close" asChild>
157
- <Button
158
- type="button"
159
- size="icon"
160
- variant="ghost"
161
- disabled={disabled}
162
- aria-label="Fechar"
163
- >
164
- <XIcon aria-hidden />
165
- </Button>
167
+ <CloseButton size={size} disabled={disabled} />
166
168
  </DialogPrimitive.Close>
167
169
  );
168
170
  }
@@ -172,6 +174,7 @@ export function DialogContent({
172
174
  children,
173
175
  showCloseButton,
174
176
  closeDisabled = false,
177
+ closeSize = "sm",
175
178
  "aria-describedby": ariaDescribedBy,
176
179
  ...props
177
180
  }: React.ComponentProps<typeof DialogPrimitive.Content> & {
@@ -179,11 +182,13 @@ export function DialogContent({
179
182
  showCloseButton?: boolean;
180
183
  /** Mantém a ação visível, mas impede fechar enquanto a superfície está bloqueada. */
181
184
  closeDisabled?: boolean;
185
+ /** Tamanho da ação circular de fechar no cabeçalho. */
186
+ closeSize?: CloseButtonSize;
182
187
  }) {
183
188
  const { mode } = useDialogContext();
184
189
  const showClose = showCloseButton ?? mode !== "alert";
185
190
  const contentClasses =
186
- "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";
191
+ "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-transparent bg-background text-foreground shadow-xl 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 dark:border-border/70";
187
192
 
188
193
  if (mode === "alert") {
189
194
  return (
@@ -197,7 +202,11 @@ export function DialogContent({
197
202
  {...props}
198
203
  >
199
204
  <DialogContentContext.Provider
200
- value={{ showCloseButton: false, closeDisabled: true }}
205
+ value={{
206
+ showCloseButton: false,
207
+ closeDisabled: true,
208
+ closeSize,
209
+ }}
201
210
  >
202
211
  {children}
203
212
  </DialogContentContext.Provider>
@@ -214,14 +223,14 @@ export function DialogContent({
214
223
  data-mode="default"
215
224
  className={cn(
216
225
  contentClasses,
217
- "flex max-h-[85vh] flex-col sm:max-w-lg",
226
+ "flex max-h-[85vh] max-w-lg flex-col",
218
227
  className,
219
228
  )}
220
229
  aria-describedby={ariaDescribedBy}
221
230
  {...props}
222
231
  >
223
232
  <DialogContentContext.Provider
224
- value={{ showCloseButton: showClose, closeDisabled }}
233
+ value={{ showCloseButton: showClose, closeDisabled, closeSize }}
225
234
  >
226
235
  {children}
227
236
  </DialogContentContext.Provider>
@@ -236,7 +245,7 @@ export function DialogHeader({
236
245
  ...props
237
246
  }: React.ComponentProps<"div">) {
238
247
  const { mode } = useDialogContext();
239
- const { showCloseButton, closeDisabled } =
248
+ const { showCloseButton, closeDisabled, closeSize } =
240
249
  React.useContext(DialogContentContext);
241
250
  return (
242
251
  <div
@@ -251,7 +260,7 @@ export function DialogHeader({
251
260
  >
252
261
  {children}
253
262
  {mode !== "alert" && showCloseButton && (
254
- <DialogCloseButton disabled={closeDisabled} />
263
+ <DialogCloseButton disabled={closeDisabled} size={closeSize} />
255
264
  )}
256
265
  </div>
257
266
  );
@@ -275,6 +284,7 @@ export function DialogMedia({
275
284
 
276
285
  export function DialogBody({
277
286
  className,
287
+ style,
278
288
  ...props
279
289
  }: React.ComponentProps<"div">) {
280
290
  const { mode } = useDialogContext();
@@ -285,6 +295,11 @@ export function DialogBody({
285
295
  mode === "alert" ? "min-w-0" : "min-h-0 flex-1 overflow-y-auto p-4",
286
296
  className,
287
297
  )}
298
+ style={
299
+ mode === "alert"
300
+ ? style
301
+ : ({ "--opus-inline-gutter": "1rem", ...style } as React.CSSProperties)
302
+ }
288
303
  {...props}
289
304
  />
290
305
  );
@@ -301,7 +316,7 @@ export function DialogFooter({
301
316
  className={cn(
302
317
  mode === "alert"
303
318
  ? "w-full"
304
- : "flex shrink-0 flex-col-reverse gap-2 border-t p-4 sm:flex-row sm:justify-end",
319
+ : "flex shrink-0 flex-row justify-end gap-2 border-t bg-muted/20 p-4 dark:bg-input/20",
305
320
  className,
306
321
  )}
307
322
  {...props}
@@ -1,19 +1,23 @@
1
1
  import * as React from "react";
2
- import { XIcon } from "lucide-react";
3
2
  import { Dialog as DrawerPrimitive } from "radix-ui";
4
3
 
5
4
  import { cn } from "../../lib/cn.ts";
6
- import { Button } from "./button.tsx";
5
+ import {
6
+ CloseButton,
7
+ type CloseButtonSize,
8
+ } from "./close-button.tsx";
7
9
  import { overlayClasses } from "./dialog.tsx";
8
10
 
9
11
  interface DrawerContentContextValue {
10
12
  showCloseButton: boolean;
11
13
  closeDisabled: boolean;
14
+ closeSize: CloseButtonSize;
12
15
  }
13
16
 
14
17
  const DrawerContentContext = React.createContext<DrawerContentContextValue>({
15
18
  showCloseButton: false,
16
19
  closeDisabled: false,
20
+ closeSize: "sm",
17
21
  });
18
22
 
19
23
  function Drawer({
@@ -53,18 +57,16 @@ function DrawerOverlay({
53
57
  );
54
58
  }
55
59
 
56
- function DrawerCloseButton({ disabled = false }: { disabled?: boolean }) {
60
+ function DrawerCloseButton({
61
+ disabled = false,
62
+ size,
63
+ }: {
64
+ disabled?: boolean;
65
+ size: CloseButtonSize;
66
+ }) {
57
67
  return (
58
68
  <DrawerPrimitive.Close data-slot="drawer-close" asChild>
59
- <Button
60
- type="button"
61
- size="icon"
62
- variant="ghost"
63
- disabled={disabled}
64
- aria-label="Fechar"
65
- >
66
- <XIcon aria-hidden />
67
- </Button>
69
+ <CloseButton size={size} disabled={disabled} />
68
70
  </DrawerPrimitive.Close>
69
71
  );
70
72
  }
@@ -75,12 +77,14 @@ function DrawerContent({
75
77
  side = "right",
76
78
  showCloseButton = true,
77
79
  closeDisabled = false,
80
+ closeSize = "sm",
78
81
  "aria-describedby": ariaDescribedBy,
79
82
  ...props
80
83
  }: React.ComponentProps<typeof DrawerPrimitive.Content> & {
81
84
  side?: "top" | "right" | "bottom" | "left";
82
85
  showCloseButton?: boolean;
83
86
  closeDisabled?: boolean;
87
+ closeSize?: CloseButtonSize;
84
88
  }) {
85
89
  return (
86
90
  <DrawerPortal>
@@ -90,9 +94,9 @@ function DrawerContent({
90
94
  className={cn(
91
95
  "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",
92
96
  side === "right" &&
93
- "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",
97
+ "inset-y-0 right-0 h-full w-3/4 max-w-sm border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
94
98
  side === "left" &&
95
- "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
99
+ "inset-y-0 left-0 h-full w-3/4 max-w-sm border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
96
100
  side === "top" &&
97
101
  "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
98
102
  side === "bottom" &&
@@ -103,7 +107,7 @@ function DrawerContent({
103
107
  {...props}
104
108
  >
105
109
  <DrawerContentContext.Provider
106
- value={{ showCloseButton, closeDisabled }}
110
+ value={{ showCloseButton, closeDisabled, closeSize }}
107
111
  >
108
112
  {children}
109
113
  </DrawerContentContext.Provider>
@@ -117,7 +121,7 @@ function DrawerHeader({
117
121
  children,
118
122
  ...props
119
123
  }: React.ComponentProps<"div">) {
120
- const { showCloseButton, closeDisabled } =
124
+ const { showCloseButton, closeDisabled, closeSize } =
121
125
  React.useContext(DrawerContentContext);
122
126
  return (
123
127
  <div
@@ -126,16 +130,19 @@ function DrawerHeader({
126
130
  {...props}
127
131
  >
128
132
  {children}
129
- {showCloseButton && <DrawerCloseButton disabled={closeDisabled} />}
133
+ {showCloseButton && (
134
+ <DrawerCloseButton disabled={closeDisabled} size={closeSize} />
135
+ )}
130
136
  </div>
131
137
  );
132
138
  }
133
139
 
134
- function DrawerBody({ className, ...props }: React.ComponentProps<"div">) {
140
+ function DrawerBody({ className, style, ...props }: React.ComponentProps<"div">) {
135
141
  return (
136
142
  <div
137
143
  data-slot="drawer-body"
138
144
  className={cn("min-h-0 flex-1 overflow-y-auto p-4", className)}
145
+ style={{ "--opus-inline-gutter": "1rem", ...style } as React.CSSProperties}
139
146
  {...props}
140
147
  />
141
148
  );
@@ -146,7 +153,7 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
146
153
  <div
147
154
  data-slot="drawer-footer"
148
155
  className={cn(
149
- "mt-auto flex shrink-0 flex-col-reverse gap-2 border-t p-4 sm:flex-row sm:justify-end",
156
+ "mt-auto flex shrink-0 flex-row justify-end gap-2 border-t p-4",
150
157
  className,
151
158
  )}
152
159
  {...props}
@@ -19,9 +19,9 @@ export interface EmptyValueProps extends Omit<React.ComponentProps<'span'>, 'chi
19
19
  }
20
20
 
21
21
  /**
22
- * Representação padrão de valor ausente. Em texto corrido (DetailField) o rótulo aparece;
23
- * em célula compacta (`compact`, o default das colunas de ActionList) aparece o travessão
24
- * e o rótulo vai para o leitor de tela. Um domínio troca o rótulo por `label`
22
+ * Representação padrão de valor ausente. Sem `compact`, o rótulo aparece; com `compact`
23
+ * o default das colunas de ActionList e de DetailField — aparece o travessão e o rótulo
24
+ * vai para o leitor de tela. Um domínio troca o rótulo por `label`
25
25
  * (“Nunca enviado”, “Sem vencimento”, “Não se aplica”) em vez de repetir a condicional.
26
26
  */
27
27
  export function EmptyValue({
@@ -7,7 +7,7 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
7
7
  <div
8
8
  data-slot="empty"
9
9
  className={cn(
10
- "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border border-dashed border-border p-6 text-center text-balance md:p-12",
10
+ "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border border-dashed border-border p-12 text-center text-balance",
11
11
  className
12
12
  )}
13
13
  {...props}
@@ -44,7 +44,7 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
44
44
  <div
45
45
  data-slot="field-group"
46
46
  className={cn(
47
- "group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
47
+ "group/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
48
48
  className
49
49
  )}
50
50
  {...props}
@@ -52,22 +52,22 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
52
52
  )
53
53
  }
54
54
 
55
+ const horizontalFieldClasses = [
56
+ "flex-row items-center gap-3",
57
+ "[&>[data-slot=field-label]]:flex-auto",
58
+ "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
59
+ ];
60
+
55
61
  const fieldVariants = cva(
56
62
  "group/field flex w-full data-[invalid=true]:text-context-danger-emphasis",
57
63
  {
58
64
  variants: {
59
65
  orientation: {
60
66
  vertical: ["flex-col gap-1.5 [&>*]:w-full [&>.sr-only]:w-auto"],
61
- horizontal: [
62
- "flex-row items-center gap-3",
63
- "[&>[data-slot=field-label]]:flex-auto",
64
- "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
65
- ],
66
- responsive: [
67
- "flex-col gap-1.5 @md/field-group:flex-row @md/field-group:items-center @md/field-group:gap-3 [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
68
- "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
69
- "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
70
- ],
67
+ horizontal: horizontalFieldClasses,
68
+ // Compatibilidade da série 18: sem breakpoints, `responsive` resolve para o
69
+ // mesmo layout desktop da orientação horizontal.
70
+ responsive: horizontalFieldClasses,
71
71
  },
72
72
  },
73
73
  defaultVariants: {
@@ -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-context-danger-emphasis group-data-[disabled=true]/field:opacity-50",
116
+ "group/field-label peer/field-label flex w-fit gap-2 text-sm 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
@@ -142,7 +142,7 @@ export function IconPicker({
142
142
  aria-describedby={ariaDescribedBy}
143
143
  data-slot="icon-picker"
144
144
  className={cn(
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',
145
+ 'flex h-10 w-full items-center gap-2 rounded-md border border-input bg-muted/30 px-3 text-sm outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30',
146
146
  focusRing,
147
147
  className,
148
148
  )}
@@ -14,7 +14,7 @@ function InputGroup({ className, shape = 'default', ...props }: React.ComponentP
14
14
  data-shape={shape}
15
15
  role="group"
16
16
  className={cn(
17
- "group/input-group relative flex w-full items-center rounded-md border border-input transition-[color,box-shadow] outline-none data-[shape=pill]:rounded-full dark:bg-input/30",
17
+ "group/input-group relative flex w-full items-center rounded-md border border-input bg-muted/30 transition-[color,box-shadow] outline-none data-[shape=pill]:rounded-full dark:bg-input/30",
18
18
  "h-9 min-w-0 has-[>textarea]:h-auto",
19
19
 
20
20
  // Variants based on alignment.
@@ -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-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40"
17
+ "h-9 w-full min-w-0 rounded-md border border-input bg-muted/30 px-3 py-1 text-sm 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 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).
@@ -31,7 +31,7 @@ function Input({ className, type, icon, trailing, ...props }: InputProps) {
31
31
  <div
32
32
  data-slot="input-wrapper"
33
33
  className={cn(
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",
34
+ "flex h-9 w-full min-w-0 items-center gap-1.5 rounded-md border border-input bg-muted/30 px-3 text-sm transition-[color,box-shadow] outline-none 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
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",
@@ -7,7 +7,7 @@ import type { ControlSize } from "./control.ts";
7
7
  import { Separator } from "./separator.tsx";
8
8
 
9
9
  const itemGroupVariants = cva(
10
- "group/item-group @container/item-group flex flex-col",
10
+ "group/item-group flex flex-col",
11
11
  {
12
12
  variants: {
13
13
  variant: {
@@ -75,8 +75,8 @@ const itemVariants = cva(
75
75
  },
76
76
  // Nomes da escala de control.ts; a linha não tem altura fixa, `sm` aperta o respiro.
77
77
  size: {
78
- default: "gap-4 p-4",
79
- sm: "gap-2.5 px-4 py-3",
78
+ default: "gap-3 p-3",
79
+ sm: "gap-2.5 px-3 py-2",
80
80
  } satisfies Partial<Record<ControlSize, string>>,
81
81
  },
82
82
  defaultVariants: { variant: "default", size: "default" },
@@ -223,7 +223,7 @@ function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
223
223
  <div
224
224
  data-slot="item-title"
225
225
  className={cn(
226
- "flex w-fit items-center gap-2 text-sm leading-snug font-medium",
226
+ "flex w-fit items-center gap-2 leading-snug font-medium",
227
227
  className,
228
228
  )}
229
229
  {...props}
@@ -241,7 +241,7 @@ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
241
241
  <p
242
242
  data-slot="item-description"
243
243
  className={cn(
244
- "line-clamp-2 text-sm leading-normal font-normal text-balance text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
244
+ "line-clamp-2 leading-normal font-normal text-balance text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
245
245
  className,
246
246
  )}
247
247
  {...props}
@@ -104,11 +104,11 @@ function PaginationPrevious({
104
104
  <PaginationLink
105
105
  aria-label={label}
106
106
  size={size ?? (iconOnly ? 'icon' : 'default')}
107
- className={cn(!iconOnly && 'gap-1 px-2.5 sm:pl-2.5', className)}
107
+ className={cn(!iconOnly && 'gap-1 px-2.5 pl-2.5', className)}
108
108
  {...props}
109
109
  >
110
110
  <ChevronLeftIcon className={iconClassName} />
111
- {!iconOnly && <span className="hidden sm:block">{label}</span>}
111
+ {!iconOnly && <span>{label}</span>}
112
112
  </PaginationLink>
113
113
  )
114
114
  }
@@ -125,10 +125,10 @@ function PaginationNext({
125
125
  <PaginationLink
126
126
  aria-label={label}
127
127
  size={size ?? (iconOnly ? 'icon' : 'default')}
128
- className={cn(!iconOnly && 'gap-1 px-2.5 sm:pr-2.5', className)}
128
+ className={cn(!iconOnly && 'gap-1 px-2.5 pr-2.5', className)}
129
129
  {...props}
130
130
  >
131
- {!iconOnly && <span className="hidden sm:block">{label}</span>}
131
+ {!iconOnly && <span>{label}</span>}
132
132
  <ChevronRightIcon className={iconClassName} />
133
133
  </PaginationLink>
134
134
  )
@@ -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-context-danger aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:aria-invalid:ring-context-danger/40",
28
+ "peer 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}