@softize/opus 16.1.0 → 17.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 (33) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/lib/check.mjs +9 -13
  3. package/bin/lib/copy.mjs +29 -4
  4. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -3
  5. package/docs/adr/0009-page-title-does-not-carry-a-counter.md +5 -2
  6. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +12 -14
  7. package/docs/adr/0012-modal-header-only-names-the-surface.md +8 -4
  8. package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +20 -11
  9. package/docs/adr/0014-structural-headers-do-not-carry-description.md +35 -0
  10. package/docs/adr/0015-action-size-follows-interaction-density.md +64 -0
  11. package/package.json +2 -2
  12. package/registry/skills/build-opus-ui/references/evaluations.md +1 -1
  13. package/registry/skills/build-opus-ui/references/ui-patterns.md +19 -18
  14. package/registry/templates/app/package.json +1 -1
  15. package/src/core/presentation.ts +142 -13
  16. package/src/ui/components/patterns/form.tsx +35 -10
  17. package/src/ui/components/patterns/page.tsx +93 -48
  18. package/src/ui/components/patterns/presentation.tsx +489 -329
  19. package/src/ui/components/patterns/surface-header.tsx +4 -3
  20. package/src/ui/components/patterns/trigger.tsx +8 -1
  21. package/src/ui/components/patterns/view.tsx +2 -2
  22. package/src/ui/components/primitives/command.tsx +82 -42
  23. package/src/ui/components/primitives/dialog.tsx +180 -97
  24. package/src/ui/components/primitives/drawer.tsx +63 -22
  25. package/src/ui/docs/content/button.md +8 -2
  26. package/src/ui/docs/content/command.md +3 -1
  27. package/src/ui/docs/content/content.md +1 -1
  28. package/src/ui/docs/content/dialog.md +9 -9
  29. package/src/ui/docs/content/drawer.md +4 -4
  30. package/src/ui/docs/content/page.md +16 -31
  31. package/src/ui/docs/content/presentation.md +73 -80
  32. package/src/ui/meta.ts +2 -2
  33. package/src/ui/react.tsx +1 -8
@@ -1,80 +1,111 @@
1
- import * as React from 'react'
2
- import * as DialogPrimitive from '@radix-ui/react-dialog'
3
- import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
4
- import { XIcon } from 'lucide-react'
5
- import { cn } from '../../lib/cn.ts'
6
- import { focusRing } from './control.ts'
1
+ import * as React from "react";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
4
+ import { XIcon } from "lucide-react";
5
+ import { cn } from "../../lib/cn.ts";
6
+ import { Button } from "./button.tsx";
7
7
 
8
- export type DialogMode = 'default' | 'alert'
8
+ export type DialogMode = "default" | "alert";
9
9
 
10
10
  interface DialogContextValue {
11
- mode: DialogMode
12
- onResult?: (result: string) => void
11
+ mode: DialogMode;
12
+ onResult?: (result: string) => void;
13
13
  }
14
14
 
15
15
  const DialogContext = React.createContext<DialogContextValue>({
16
- mode: 'default',
17
- })
16
+ mode: "default",
17
+ });
18
+
19
+ interface DialogContentContextValue {
20
+ showCloseButton: boolean;
21
+ closeDisabled: boolean;
22
+ }
23
+
24
+ const DialogContentContext = React.createContext<DialogContentContextValue>({
25
+ showCloseButton: false,
26
+ closeDisabled: false,
27
+ });
18
28
 
19
29
  function useDialogContext(): DialogContextValue {
20
- return React.useContext(DialogContext)
30
+ return React.useContext(DialogContext);
21
31
  }
22
32
 
23
- export interface DialogProps extends React.ComponentProps<typeof DialogPrimitive.Root> {
33
+ export interface DialogProps extends React.ComponentProps<
34
+ typeof DialogPrimitive.Root
35
+ > {
24
36
  /** `alert` exige uma resposta explícita e deriva `role="alertdialog"`. */
25
- mode?: DialogMode
37
+ mode?: DialogMode;
26
38
  /** Recebe o `result` do `DialogClose` acionado. */
27
- onResult?: (result: string) => void
39
+ onResult?: (result: string) => void;
28
40
  }
29
41
 
30
- export function Dialog({ mode = 'default', onResult, modal, ...props }: DialogProps) {
31
- const context = React.useMemo(() => ({ mode, onResult }), [mode, onResult])
42
+ export function Dialog({
43
+ mode = "default",
44
+ onResult,
45
+ modal,
46
+ ...props
47
+ }: DialogProps) {
48
+ const context = React.useMemo(() => ({ mode, onResult }), [mode, onResult]);
32
49
 
33
50
  return (
34
51
  <DialogContext.Provider value={context}>
35
- {mode === 'alert' ? (
52
+ {mode === "alert" ? (
36
53
  <AlertDialogPrimitive.Root data-slot="dialog" {...props} />
37
54
  ) : (
38
55
  <DialogPrimitive.Root data-slot="dialog" modal={modal} {...props} />
39
56
  )}
40
57
  </DialogContext.Provider>
41
- )
58
+ );
42
59
  }
43
60
 
44
- export function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
45
- const { mode } = useDialogContext()
46
- return mode === 'alert' ? (
61
+ export function DialogTrigger({
62
+ ...props
63
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
64
+ const { mode } = useDialogContext();
65
+ return mode === "alert" ? (
47
66
  <AlertDialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
48
67
  ) : (
49
68
  <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
50
- )
69
+ );
51
70
  }
52
71
 
53
- export function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
54
- const { mode } = useDialogContext()
55
- return mode === 'alert' ? (
72
+ export function DialogPortal({
73
+ ...props
74
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
75
+ const { mode } = useDialogContext();
76
+ return mode === "alert" ? (
56
77
  <AlertDialogPrimitive.Portal data-slot="dialog-portal" {...props} />
57
78
  ) : (
58
79
  <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
59
- )
80
+ );
60
81
  }
61
82
 
62
- export interface DialogCloseProps extends React.ComponentProps<typeof DialogPrimitive.Close> {
83
+ export interface DialogCloseProps extends React.ComponentProps<
84
+ typeof DialogPrimitive.Close
85
+ > {
63
86
  /** No modo `alert`, registra este controle como destino inicial seguro do foco. */
64
- initialFocus?: boolean
87
+ initialFocus?: boolean;
65
88
  /** Resposta entregue a `Dialog.onResult` antes do fechamento. */
66
- result?: string
89
+ result?: string;
67
90
  }
68
91
 
69
- export function DialogClose({ initialFocus = false, result, onClick, ...props }: DialogCloseProps) {
70
- const root = useDialogContext()
92
+ export function DialogClose({
93
+ initialFocus = false,
94
+ result,
95
+ onClick,
96
+ ...props
97
+ }: DialogCloseProps) {
98
+ const root = useDialogContext();
71
99
  const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
72
- onClick?.(event)
73
- if (!event.defaultPrevented && result !== undefined) root.onResult?.(result)
74
- }
100
+ onClick?.(event);
101
+ if (!event.defaultPrevented && result !== undefined)
102
+ root.onResult?.(result);
103
+ };
75
104
 
76
- if (root.mode === 'alert') {
77
- const ClosePrimitive = initialFocus ? AlertDialogPrimitive.Cancel : AlertDialogPrimitive.Action
105
+ if (root.mode === "alert") {
106
+ const ClosePrimitive = initialFocus
107
+ ? AlertDialogPrimitive.Cancel
108
+ : AlertDialogPrimitive.Action;
78
109
  return (
79
110
  <ClosePrimitive
80
111
  data-slot="dialog-close"
@@ -82,7 +113,7 @@ export function DialogClose({ initialFocus = false, result, onClick, ...props }:
82
113
  onClick={handleClick}
83
114
  {...props}
84
115
  />
85
- )
116
+ );
86
117
  }
87
118
 
88
119
  return (
@@ -92,53 +123,85 @@ export function DialogClose({ initialFocus = false, result, onClick, ...props }:
92
123
  onClick={handleClick}
93
124
  {...props}
94
125
  />
95
- )
126
+ );
96
127
  }
97
128
 
98
129
  /** Véu compartilhado por Dialog e Drawer: mesmo escurecimento, desfoque e animação. */
99
130
  export const overlayClasses =
100
- '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'
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";
101
132
 
102
- export function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
103
- const { mode } = useDialogContext()
104
- const overlay = cn(overlayClasses, className)
105
- return mode === 'alert' ? (
106
- <AlertDialogPrimitive.Overlay data-slot="dialog-overlay" className={overlay} {...props} />
133
+ export function DialogOverlay({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
137
+ const { mode } = useDialogContext();
138
+ const overlay = cn(overlayClasses, className);
139
+ return mode === "alert" ? (
140
+ <AlertDialogPrimitive.Overlay
141
+ data-slot="dialog-overlay"
142
+ className={overlay}
143
+ {...props}
144
+ />
107
145
  ) : (
108
- <DialogPrimitive.Overlay data-slot="dialog-overlay" className={overlay} {...props} />
109
- )
146
+ <DialogPrimitive.Overlay
147
+ data-slot="dialog-overlay"
148
+ className={overlay}
149
+ {...props}
150
+ />
151
+ );
152
+ }
153
+
154
+ function DialogCloseButton({ disabled = false }: { disabled?: boolean }) {
155
+ return (
156
+ <DialogPrimitive.Close data-slot="dialog-close" asChild>
157
+ <Button
158
+ type="button"
159
+ size="icon-sm"
160
+ variant="ghost"
161
+ disabled={disabled}
162
+ aria-label="Fechar"
163
+ >
164
+ <XIcon aria-hidden />
165
+ </Button>
166
+ </DialogPrimitive.Close>
167
+ );
110
168
  }
111
169
 
112
170
  export function DialogContent({
113
171
  className,
114
172
  children,
115
173
  showCloseButton,
116
- 'aria-describedby': ariaDescribedBy,
174
+ closeDisabled = false,
175
+ "aria-describedby": ariaDescribedBy,
117
176
  ...props
118
177
  }: React.ComponentProps<typeof DialogPrimitive.Content> & {
119
- /** Mostra oX” no modo padrão. O modo `alert` exige uma resposta identificável. */
120
- showCloseButton?: boolean
178
+ /** Mostra a ação Fechar” no modo padrão. O modo `alert` exige uma resposta identificável. */
179
+ showCloseButton?: boolean;
180
+ /** Mantém a ação visível, mas impede fechar enquanto a superfície está bloqueada. */
181
+ closeDisabled?: boolean;
121
182
  }) {
122
- const { mode } = useDialogContext()
123
- const showClose = showCloseButton ?? mode !== 'alert'
183
+ const { mode } = useDialogContext();
184
+ const showClose = showCloseButton ?? mode !== "alert";
124
185
  const contentClasses =
125
- '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'
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";
126
187
 
127
- if (mode === 'alert') {
188
+ if (mode === "alert") {
128
189
  return (
129
190
  <DialogPortal>
130
191
  <DialogOverlay />
131
192
  <AlertDialogPrimitive.Content
132
193
  data-slot="dialog-content"
133
194
  data-mode="alert"
134
- className={cn(contentClasses, 'grid max-w-xs gap-4 p-6', className)}
195
+ className={cn(contentClasses, "grid max-w-xs gap-4 p-6", className)}
135
196
  aria-describedby={ariaDescribedBy}
136
197
  {...props}
137
198
  >
138
- {children}
199
+ <DialogContentContext.Provider value={{ showCloseButton: false, closeDisabled: true }}>
200
+ {children}
201
+ </DialogContentContext.Provider>
139
202
  </AlertDialogPrimitive.Content>
140
203
  </DialogPortal>
141
- )
204
+ );
142
205
  }
143
206
 
144
207
  return (
@@ -147,45 +210,50 @@ export function DialogContent({
147
210
  <DialogPrimitive.Content
148
211
  data-slot="dialog-content"
149
212
  data-mode="default"
150
- className={cn(contentClasses, 'flex max-h-[85vh] flex-col sm:max-w-lg', className)}
213
+ className={cn(
214
+ contentClasses,
215
+ "flex max-h-[85vh] flex-col sm:max-w-lg",
216
+ className,
217
+ )}
151
218
  aria-describedby={ariaDescribedBy}
152
219
  {...props}
153
220
  >
154
- {children}
155
- {showClose && (
156
- <DialogPrimitive.Close
157
- data-slot="dialog-close"
158
- className={cn(
159
- "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",
160
- focusRing,
161
- )}
162
- >
163
- <XIcon />
164
- <span className="sr-only">Fechar</span>
165
- </DialogPrimitive.Close>
166
- )}
221
+ <DialogContentContext.Provider value={{ showCloseButton: showClose, closeDisabled }}>
222
+ {children}
223
+ </DialogContentContext.Provider>
167
224
  </DialogPrimitive.Content>
168
225
  </DialogPortal>
169
- )
226
+ );
170
227
  }
171
228
 
172
- export function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
173
- const { mode } = useDialogContext()
229
+ export function DialogHeader({
230
+ className,
231
+ children,
232
+ ...props
233
+ }: React.ComponentProps<"div">) {
234
+ const { mode } = useDialogContext();
235
+ const { showCloseButton, closeDisabled } = React.useContext(DialogContentContext);
174
236
  return (
175
237
  <div
176
238
  data-slot="dialog-header"
177
239
  className={cn(
178
- mode === 'alert'
179
- ? 'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=dialog-media]:grid-rows-[auto_auto_1fr]'
180
- : 'flex shrink-0 flex-col gap-2 border-b p-5 text-center sm:text-left [&:has(+[data-slot=dialog-footer])]:border-b-0',
240
+ mode === "alert"
241
+ ? "grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=dialog-media]:grid-rows-[auto_auto_1fr]"
242
+ : "flex shrink-0 items-center gap-4 border-b p-5 text-left [&:has(+[data-slot=dialog-footer])]:border-b-0",
181
243
  className,
182
244
  )}
183
245
  {...props}
184
- />
185
- )
246
+ >
247
+ {children}
248
+ {mode !== "alert" && showCloseButton && <DialogCloseButton disabled={closeDisabled} />}
249
+ </div>
250
+ );
186
251
  }
187
252
 
188
- export function DialogMedia({ className, ...props }: React.ComponentProps<'div'>) {
253
+ export function DialogMedia({
254
+ className,
255
+ ...props
256
+ }: React.ComponentProps<"div">) {
189
257
  return (
190
258
  <div
191
259
  data-slot="dialog-media"
@@ -195,49 +263,64 @@ export function DialogMedia({ className, ...props }: React.ComponentProps<'div'>
195
263
  )}
196
264
  {...props}
197
265
  />
198
- )
266
+ );
199
267
  }
200
268
 
201
- export function DialogBody({ className, ...props }: React.ComponentProps<'div'>) {
202
- const { mode } = useDialogContext()
269
+ export function DialogBody({
270
+ className,
271
+ ...props
272
+ }: React.ComponentProps<"div">) {
273
+ const { mode } = useDialogContext();
203
274
  return (
204
275
  <div
205
276
  data-slot="dialog-body"
206
- className={cn(mode === 'alert' ? 'min-w-0' : 'min-h-0 flex-1 overflow-y-auto p-5', className)}
277
+ className={cn(
278
+ mode === "alert" ? "min-w-0" : "min-h-0 flex-1 overflow-y-auto p-5",
279
+ className,
280
+ )}
207
281
  {...props}
208
282
  />
209
- )
283
+ );
210
284
  }
211
285
 
212
- export function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
213
- const { mode } = useDialogContext()
286
+ export function DialogFooter({
287
+ className,
288
+ ...props
289
+ }: React.ComponentProps<"div">) {
290
+ const { mode } = useDialogContext();
214
291
  return (
215
292
  <div
216
293
  data-slot="dialog-footer"
217
294
  className={cn(
218
- mode === 'alert'
219
- ? 'grid grid-cols-2 gap-2 [&>*:only-child]:col-span-full'
220
- : 'flex shrink-0 flex-col-reverse gap-2 border-t bg-muted/30 px-5 py-4 sm:flex-row sm:justify-end',
295
+ mode === "alert"
296
+ ? "grid grid-cols-2 gap-2 [&>*:only-child]:col-span-full"
297
+ : "flex shrink-0 flex-col-reverse gap-2 border-t bg-muted/30 px-5 py-4 sm:flex-row sm:justify-end",
221
298
  className,
222
299
  )}
223
300
  {...props}
224
301
  />
225
- )
302
+ );
226
303
  }
227
304
 
228
- export function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
229
- const { mode } = useDialogContext()
230
- return mode === 'alert' ? (
305
+ export function DialogTitle({
306
+ className,
307
+ ...props
308
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
309
+ const { mode } = useDialogContext();
310
+ return mode === "alert" ? (
231
311
  <AlertDialogPrimitive.Title
232
312
  data-slot="dialog-title"
233
- className={cn('text-lg font-semibold', className)}
313
+ className={cn("text-lg font-semibold", className)}
234
314
  {...props}
235
315
  />
236
316
  ) : (
237
317
  <DialogPrimitive.Title
238
318
  data-slot="dialog-title"
239
- className={cn('text-lg font-semibold leading-none', className)}
319
+ className={cn(
320
+ "min-w-0 flex-1 text-lg font-semibold leading-none",
321
+ className,
322
+ )}
240
323
  {...props}
241
324
  />
242
- )
325
+ );
243
326
  }
@@ -3,22 +3,40 @@ 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";
6
+ import { Button } from "./button.tsx";
7
7
  import { overlayClasses } from "./dialog.tsx";
8
8
 
9
- function Drawer({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
9
+ interface DrawerContentContextValue {
10
+ showCloseButton: boolean;
11
+ closeDisabled: boolean;
12
+ }
13
+
14
+ const DrawerContentContext = React.createContext<DrawerContentContextValue>({
15
+ showCloseButton: false,
16
+ closeDisabled: false,
17
+ });
18
+
19
+ function Drawer({
20
+ ...props
21
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
10
22
  return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
11
23
  }
12
24
 
13
- function DrawerTrigger({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
25
+ function DrawerTrigger({
26
+ ...props
27
+ }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
14
28
  return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
15
29
  }
16
30
 
17
- function DrawerClose({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
31
+ function DrawerClose({
32
+ ...props
33
+ }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
18
34
  return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
19
35
  }
20
36
 
21
- function DrawerPortal({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
37
+ function DrawerPortal({
38
+ ...props
39
+ }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
22
40
  return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
23
41
  }
24
42
 
@@ -35,16 +53,34 @@ function DrawerOverlay({
35
53
  );
36
54
  }
37
55
 
56
+ function DrawerCloseButton({ disabled = false }: { disabled?: boolean }) {
57
+ return (
58
+ <DrawerPrimitive.Close data-slot="drawer-close" asChild>
59
+ <Button
60
+ type="button"
61
+ size="icon-sm"
62
+ variant="ghost"
63
+ disabled={disabled}
64
+ aria-label="Fechar"
65
+ >
66
+ <XIcon aria-hidden />
67
+ </Button>
68
+ </DrawerPrimitive.Close>
69
+ );
70
+ }
71
+
38
72
  function DrawerContent({
39
73
  className,
40
74
  children,
41
75
  side = "right",
42
76
  showCloseButton = true,
77
+ closeDisabled = false,
43
78
  "aria-describedby": ariaDescribedBy,
44
79
  ...props
45
80
  }: React.ComponentProps<typeof DrawerPrimitive.Content> & {
46
81
  side?: "top" | "right" | "bottom" | "left";
47
82
  showCloseButton?: boolean;
83
+ closeDisabled?: boolean;
48
84
  }) {
49
85
  return (
50
86
  <DrawerPortal>
@@ -66,30 +102,29 @@ function DrawerContent({
66
102
  aria-describedby={ariaDescribedBy}
67
103
  {...props}
68
104
  >
69
- {children}
70
- {showCloseButton && (
71
- <DrawerPrimitive.Close
72
- className={cn(
73
- "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",
74
- focusRing,
75
- )}
76
- >
77
- <XIcon className="size-4" />
78
- <span className="sr-only">Fechar</span>
79
- </DrawerPrimitive.Close>
80
- )}
105
+ <DrawerContentContext.Provider value={{ showCloseButton, closeDisabled }}>
106
+ {children}
107
+ </DrawerContentContext.Provider>
81
108
  </DrawerPrimitive.Content>
82
109
  </DrawerPortal>
83
110
  );
84
111
  }
85
112
 
86
- function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
113
+ function DrawerHeader({
114
+ className,
115
+ children,
116
+ ...props
117
+ }: React.ComponentProps<"div">) {
118
+ const { showCloseButton, closeDisabled } = React.useContext(DrawerContentContext);
87
119
  return (
88
120
  <div
89
121
  data-slot="drawer-header"
90
- className={cn("flex shrink-0 flex-col gap-2 border-b p-5", className)}
122
+ className={cn("flex shrink-0 items-center gap-4 border-b p-5", className)}
91
123
  {...props}
92
- />
124
+ >
125
+ {children}
126
+ {showCloseButton && <DrawerCloseButton disabled={closeDisabled} />}
127
+ </div>
93
128
  );
94
129
  }
95
130
 
@@ -116,11 +151,17 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
116
151
  );
117
152
  }
118
153
 
119
- function DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
154
+ function DrawerTitle({
155
+ className,
156
+ ...props
157
+ }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
120
158
  return (
121
159
  <DrawerPrimitive.Title
122
160
  data-slot="drawer-title"
123
- className={cn("text-lg font-semibold leading-none text-foreground", className)}
161
+ className={cn(
162
+ "min-w-0 flex-1 text-lg font-semibold leading-none text-foreground",
163
+ className,
164
+ )}
124
165
  {...props}
125
166
  />
126
167
  );
@@ -24,14 +24,20 @@ há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em
24
24
  | Nome | Medida | Uso |
25
25
  | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
26
26
  | `xs` | 1.5rem | Ação dentro de um campo (`InputGroupButton`). |
27
- | `sm` | 2rem | Toolbar e cabeçalho densos, ao lado de `Select` e `Tabs` `sm`. |
28
- | `default` | 2.25rem | A linha padrão, a mesma de `Input`. |
27
+ | `sm` | 2rem | Ação operacional em toolbar, seção ou coleção densa, ao lado de `Select` e `Tabs` `sm`. |
28
+ | `default` | 2.25rem | Decisão da superfície em header de Page ou footer de Dialog/Drawer; é também a linha padrão de `Input`. |
29
29
  | `lg` | 2.5rem | Chamada principal com mais área de toque. |
30
30
  | `icon-xs` | 1.5rem | Ação só de ícone dentro de um campo ou de uma linha densa; é o quadrado de `ActionTrigger` com `icon`. |
31
31
  | `icon-sm` | 1.75rem | Ação só de ícone em composições compactas que precisam de mais presença; também é o quadrado das setas do pager de `ActionList`. |
32
32
  | `icon` | 2.25rem | Ação só de ícone na linha padrão. |
33
33
  | `icon-lg` | 2.5rem | Ação só de ícone ao lado de um `lg`. |
34
34
 
35
+ Escolha o tamanho pela região, não pela importância visual: `variant` e `context` resolvem a
36
+ hierarquia da ação. Headers de Page e footers de Dialog/Drawer usam `default`; ações operacionais
37
+ de seção, toolbar e coleção usam `sm`; ações dentro de linha ou célula usam `xs` ou `icon-xs`.
38
+ Controles de chrome, como voltar e fechar, usam `icon-sm`. Assim a mesma decisão mantém a mesma
39
+ altura mesmo quando uma superfície troca uma ação secundária por uma primária.
40
+
35
41
  ```tsx preview
36
42
  <Button size="xs">Mínimo</Button>
37
43
  <Button size="sm">Pequeno</Button>
@@ -23,7 +23,9 @@ do Select pesquisável; para uma escolha em formulário, use Select.
23
23
 
24
24
  ## Palette modal (CommandDialog)
25
25
 
26
- O Command embrulhado em um Dialog, com header sr-only para a11y. O atalho ⌘K (keydown no app) só troca o open — o conteúdo é o mesmo do inline.
26
+ O Command embrulhado em um Dialog mantém um header compacto com título e close. A descrição
27
+ permanece disponível para tecnologias assistivas. O atalho ⌘K (keydown no app) só troca o open —
28
+ o conteúdo é o mesmo do inline.
27
29
 
28
30
  ```tsx preview
29
31
  const [open, setOpen] = useState(false)
@@ -23,7 +23,7 @@ render(
23
23
  Use os slots quando o header precisar de composição própria. A árvore aceita exatamente um
24
24
  `ContentHeader` e um `ContentBody` como filhos diretos. O header exige um `ContentTitle` e aceita
25
25
  uma descrição, um contador (`ContentMeta`) e uma região de ações. O contador é próprio de
26
- `Content`; `PageHeader` reconhece somente título, descrição e ações.
26
+ `Content`; `PageHeader` reconhece somente navegação, título e ações.
27
27
 
28
28
  ```tsx preview col
29
29
  render(
@@ -264,18 +264,18 @@ botão. O texto informa o efeito real, como `Excluir`, `Revogar acesso` ou `Ence
264
264
 
265
265
  ## Propriedades de DialogContent
266
266
 
267
- | Propriedade | Tipo | Padrão | Descrição |
268
- | ----------------- | ----------- | ------ | -------------------------------------------------------------------------------------------------------- |
269
- | `showCloseButton` | `boolean` | `true` | No modo padrão, exibe o botão de fechamento no canto; o modo de alerta exige uma resposta identificável. |
270
- | `className` | `string` | | Ajusta a superfície. |
271
- | `children` | `ReactNode` | | Cabeçalho, corpo, rodapé ou conteúdo próprio. |
267
+ | Propriedade | Tipo | Padrão | Descrição |
268
+ | ----------------- | ----------- | ------ | --------------------------------------------------------------------------------------------------------- |
269
+ | `showCloseButton` | `boolean` | `true` | No modo padrão, inclui a ação de fechamento no header; o modo de alerta exige uma resposta identificável. |
270
+ | `className` | `string` | | Ajusta a superfície. |
271
+ | `children` | `ReactNode` | | Cabeçalho, corpo, rodapé ou conteúdo próprio. |
272
272
 
273
273
  ## Propriedades de DialogHeader
274
274
 
275
- | Propriedade | Tipo | Padrão | Descrição |
276
- | ----------- | ----------- | ------ | ------------------------ |
277
- | `className` | `string` | | Ajusta a faixa superior. |
278
- | `children` | `ReactNode` | | Título e mídia opcional. |
275
+ | Propriedade | Tipo | Padrão | Descrição |
276
+ | ----------- | ----------- | ------ | ---------------------------------------------------------- |
277
+ | `className` | `string` | | Ajusta a faixa superior. |
278
+ | `children` | `ReactNode` | | Título e ações opcionais; o close padrão entra por último. |
279
279
 
280
280
  ## Propriedades de DialogBody
281
281
 
@@ -60,10 +60,10 @@ mesmo alinhamento horizontal entre título, conteúdo e ações.
60
60
 
61
61
  ## Propriedades de DrawerContent
62
62
 
63
- | Propriedade | Tipo | Padrão | Descrição |
64
- | ----------------- | ---------------------------------------- | --------- | -------------------------------------------------------------------------------- |
65
- | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'right'` | Borda de onde o painel aparece. |
66
- | `showCloseButton` | `boolean` | `true` | Exibe o botão de fechamento. Desative quando o painel exigir uma ação explícita. |
63
+ | Propriedade | Tipo | Padrão | Descrição |
64
+ | ----------------- | ---------------------------------------- | --------- | ------------------------------------------------------------------------------------------ |
65
+ | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'right'` | Borda de onde o painel aparece. |
66
+ | `showCloseButton` | `boolean` | `true` | Inclui a ação de fechamento no header. Desative quando o painel exigir uma ação explícita. |
67
67
 
68
68
  ## Propriedades de DrawerBody
69
69