@softize/opus 18.4.0 → 19.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.
- package/CHANGELOG.md +94 -0
- package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +46 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +65 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +293 -0
- package/package.json +1 -1
- package/src/ui/components/patterns/content-header.tsx +169 -50
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +76 -27
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +29 -17
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +8 -35
- package/src/ui/components/patterns/surface-anatomy.ts +219 -0
- package/src/ui/components/patterns/surface-header.tsx +51 -6
- package/src/ui/components/patterns/surface-media.tsx +69 -0
- package/src/ui/components/primitives/card.tsx +91 -29
- package/src/ui/components/primitives/command.tsx +12 -4
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +138 -35
- package/src/ui/components/primitives/drawer.tsx +122 -23
- package/src/ui/components/primitives/empty.tsx +2 -1
- package/src/ui/components/primitives/field.tsx +38 -16
- package/src/ui/components/primitives/icon-tile.tsx +91 -0
- package/src/ui/components/primitives/item.tsx +2 -1
- package/src/ui/components/primitives/metric-card.tsx +98 -39
- package/src/ui/components/primitives/table.tsx +1 -1
- package/src/ui/docs/content/action-list.md +61 -52
- package/src/ui/docs/content/card.md +221 -30
- package/src/ui/docs/content/content.md +90 -8
- package/src/ui/docs/content/dialog.md +115 -7
- package/src/ui/docs/content/drawer.md +136 -9
- package/src/ui/docs/content/field.md +78 -30
- package/src/ui/docs/content/icon-tile.md +47 -0
- package/src/ui/docs/content/metric-card.md +57 -18
- package/src/ui/docs/content/page.md +8 -0
- package/src/ui/docs/content/pane.md +250 -0
- package/src/ui/docs/content/sidebar.md +22 -43
- package/src/ui/docs/content/skeleton.md +7 -3
- package/src/ui/docs/content/split.md +16 -21
- package/src/ui/docs/registry.tsx +14 -2
- package/src/ui/meta.ts +17 -5
- package/src/ui/react.tsx +31 -0
|
@@ -54,9 +54,7 @@ function DetailGroup({
|
|
|
54
54
|
columnClasses[columns],
|
|
55
55
|
variant === 'plain' && !dividers && 'gap-x-6 gap-y-4',
|
|
56
56
|
dividers && 'gap-px overflow-hidden',
|
|
57
|
-
(variant === 'framed' || dividers) &&
|
|
58
|
-
!splitHorizontalField &&
|
|
59
|
-
'[&>[data-slot=detail-field]]:p-3',
|
|
57
|
+
(variant === 'framed' || dividers) && !splitHorizontalField && '[&>[data-slot=detail-field]]:p-3',
|
|
60
58
|
variant === 'framed' &&
|
|
61
59
|
'overflow-hidden rounded-lg border border-border bg-transparent text-foreground',
|
|
62
60
|
// Default da coluna de rótulos como CLASSE, antes do `className`: assim uma classe do
|
|
@@ -74,10 +72,7 @@ function DetailGroup({
|
|
|
74
72
|
)
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
export interface DetailFieldProps extends Omit<
|
|
78
|
-
React.ComponentProps<'div'>,
|
|
79
|
-
'children'
|
|
80
|
-
> {
|
|
75
|
+
export interface DetailFieldProps extends Omit<React.ComponentProps<'div'>, 'children'> {
|
|
81
76
|
label: React.ReactNode
|
|
82
77
|
/** `null`, `undefined`, string vazia ou só com espaços renderizam a ausência
|
|
83
78
|
* como travessão; “Não informado” ou `empty` preservam o significado para
|
|
@@ -99,11 +94,13 @@ function DetailField({
|
|
|
99
94
|
...props
|
|
100
95
|
}: DetailFieldProps): React.ReactElement {
|
|
101
96
|
const { splitHorizontalField } = React.useContext(DetailGroupLayoutContext)
|
|
102
|
-
const content = !isAbsentValue(value)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
const content = !isAbsentValue(value) ? (
|
|
98
|
+
value
|
|
99
|
+
) : empty === undefined || typeof empty === 'string' ? (
|
|
100
|
+
<EmptyValue compact {...(empty !== undefined ? { label: empty } : {})} />
|
|
101
|
+
) : (
|
|
102
|
+
empty
|
|
103
|
+
)
|
|
107
104
|
return (
|
|
108
105
|
<div
|
|
109
106
|
data-slot="detail-field"
|
|
@@ -131,7 +128,7 @@ function DetailField({
|
|
|
131
128
|
<dt
|
|
132
129
|
data-slot="detail-field-label"
|
|
133
130
|
className={cn(
|
|
134
|
-
'flex items-center gap-
|
|
131
|
+
'flex items-center gap-1.5 text-sm leading-snug font-medium text-muted-foreground',
|
|
135
132
|
splitHorizontalField && 'bg-muted/30 p-3 dark:bg-input/30',
|
|
136
133
|
)}
|
|
137
134
|
>
|
|
@@ -2,6 +2,21 @@ 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
4
|
import { cn } from "../../lib/cn.ts";
|
|
5
|
+
import {
|
|
6
|
+
SurfaceLayoutProvider,
|
|
7
|
+
surfaceActionsClasses,
|
|
8
|
+
surfaceHeaderRowClasses,
|
|
9
|
+
surfaceRegionClasses,
|
|
10
|
+
surfaceTitleClasses,
|
|
11
|
+
useSurfaceLayout,
|
|
12
|
+
type SurfaceLayout,
|
|
13
|
+
} from "../patterns/surface-anatomy.ts";
|
|
14
|
+
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
15
|
+
import { SurfaceHeader } from "../patterns/surface-header.tsx";
|
|
16
|
+
import {
|
|
17
|
+
SurfaceMedia,
|
|
18
|
+
type SurfaceMediaProps,
|
|
19
|
+
} from "../patterns/surface-media.tsx";
|
|
5
20
|
import {
|
|
6
21
|
CloseButton,
|
|
7
22
|
type CloseButtonSize,
|
|
@@ -175,6 +190,7 @@ export function DialogContent({
|
|
|
175
190
|
showCloseButton,
|
|
176
191
|
closeDisabled = false,
|
|
177
192
|
closeSize = "sm",
|
|
193
|
+
layout = "sectioned",
|
|
178
194
|
"aria-describedby": ariaDescribedBy,
|
|
179
195
|
...props
|
|
180
196
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
@@ -184,6 +200,8 @@ export function DialogContent({
|
|
|
184
200
|
closeDisabled?: boolean;
|
|
185
201
|
/** Tamanho da ação circular de fechar no cabeçalho. */
|
|
186
202
|
closeSize?: CloseButtonSize;
|
|
203
|
+
/** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
204
|
+
layout?: SurfaceLayout;
|
|
187
205
|
}) {
|
|
188
206
|
const { mode } = useDialogContext();
|
|
189
207
|
const showClose = showCloseButton ?? mode !== "alert";
|
|
@@ -221,6 +239,7 @@ export function DialogContent({
|
|
|
221
239
|
<DialogPrimitive.Content
|
|
222
240
|
data-slot="dialog-content"
|
|
223
241
|
data-mode="default"
|
|
242
|
+
data-layout={layout}
|
|
224
243
|
className={cn(
|
|
225
244
|
contentClasses,
|
|
226
245
|
"flex max-h-[85vh] max-w-lg flex-col",
|
|
@@ -229,11 +248,13 @@ export function DialogContent({
|
|
|
229
248
|
aria-describedby={ariaDescribedBy}
|
|
230
249
|
{...props}
|
|
231
250
|
>
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
251
|
+
<SurfaceLayoutProvider value={layout}>
|
|
252
|
+
<DialogContentContext.Provider
|
|
253
|
+
value={{ showCloseButton: showClose, closeDisabled, closeSize }}
|
|
254
|
+
>
|
|
255
|
+
{children}
|
|
256
|
+
</DialogContentContext.Provider>
|
|
257
|
+
</SurfaceLayoutProvider>
|
|
237
258
|
</DialogPrimitive.Content>
|
|
238
259
|
</DialogPortal>
|
|
239
260
|
);
|
|
@@ -245,40 +266,99 @@ export function DialogHeader({
|
|
|
245
266
|
...props
|
|
246
267
|
}: React.ComponentProps<"div">) {
|
|
247
268
|
const { mode } = useDialogContext();
|
|
269
|
+
const layout = useSurfaceLayout("dialog");
|
|
248
270
|
const { showCloseButton, closeDisabled, closeSize } =
|
|
249
271
|
React.useContext(DialogContentContext);
|
|
272
|
+
|
|
273
|
+
// O modo de alerta é a exceção declarada da ADR 0023: a resposta obrigatória empilha
|
|
274
|
+
// mídia, título e corpo centralizados, e o padding mora na raiz do conteúdo.
|
|
275
|
+
if (mode === "alert") {
|
|
276
|
+
return (
|
|
277
|
+
<div
|
|
278
|
+
data-slot="dialog-header"
|
|
279
|
+
className={cn(
|
|
280
|
+
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=dialog-media]:grid-rows-[auto_auto_1fr]",
|
|
281
|
+
className,
|
|
282
|
+
)}
|
|
283
|
+
{...props}
|
|
284
|
+
>
|
|
285
|
+
{children}
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
250
290
|
return (
|
|
251
|
-
<
|
|
252
|
-
|
|
291
|
+
<SurfaceHeader
|
|
292
|
+
name="DialogHeader"
|
|
293
|
+
slot="dialog"
|
|
294
|
+
slots={{
|
|
295
|
+
leading: DialogNavigation,
|
|
296
|
+
media: DialogMedia,
|
|
297
|
+
title: DialogTitle,
|
|
298
|
+
actions: DialogActions,
|
|
299
|
+
}}
|
|
300
|
+
leadingPlacement="inline"
|
|
301
|
+
titleRequired={false}
|
|
302
|
+
rowClassName={surfaceHeaderRowClasses("dialog")}
|
|
253
303
|
className={cn(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
: "flex shrink-0 items-center gap-4 border-b p-4 text-left [&:has(+[data-slot=dialog-footer])]:border-b-0",
|
|
304
|
+
"text-left",
|
|
305
|
+
surfaceRegionClasses("dialog", layout, "header"),
|
|
257
306
|
className,
|
|
258
307
|
)}
|
|
308
|
+
trailing={
|
|
309
|
+
showCloseButton ? (
|
|
310
|
+
<DialogCloseButton disabled={closeDisabled} size={closeSize} />
|
|
311
|
+
) : undefined
|
|
312
|
+
}
|
|
259
313
|
{...props}
|
|
260
314
|
>
|
|
261
315
|
{children}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
316
|
+
</SurfaceHeader>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Região introdutória do header — o voltar de um fluxo modal encadeado. */
|
|
321
|
+
export function DialogNavigation({ className, ...props }: React.ComponentProps<"div">) {
|
|
322
|
+
return (
|
|
323
|
+
<div
|
|
324
|
+
data-slot="dialog-navigation-content"
|
|
325
|
+
className={cn("flex min-w-0 shrink-0 items-center", className)}
|
|
326
|
+
{...props}
|
|
327
|
+
/>
|
|
266
328
|
);
|
|
267
329
|
}
|
|
268
330
|
|
|
269
|
-
|
|
331
|
+
/** Ações da tela no header, antes do botão de fechar. */
|
|
332
|
+
export function DialogActions({
|
|
270
333
|
className,
|
|
271
334
|
...props
|
|
272
335
|
}: React.ComponentProps<"div">) {
|
|
273
336
|
return (
|
|
274
337
|
<div
|
|
275
|
-
data-slot="dialog-
|
|
338
|
+
data-slot="dialog-actions"
|
|
339
|
+
className={cn(surfaceActionsClasses, className)}
|
|
340
|
+
{...props}
|
|
341
|
+
/>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function DialogMedia({ className, ...props }: SurfaceMediaProps) {
|
|
346
|
+
const { mode } = useDialogContext();
|
|
347
|
+
// No alerta a mídia é o elemento de reconhecimento da resposta e ocupa a moldura
|
|
348
|
+
// maior — exceção declarada da ADR 0023, coberta por teste. Fora dele, a mesma
|
|
349
|
+
// moldura das demais famílias.
|
|
350
|
+
return mode === "alert" ? (
|
|
351
|
+
<SurfaceMedia
|
|
352
|
+
slot="dialog-media"
|
|
353
|
+
size="lg"
|
|
276
354
|
className={cn(
|
|
277
|
-
'mb-2
|
|
355
|
+
'mb-2 size-16 *:[svg:not([class*="size-"])]:size-8',
|
|
278
356
|
className,
|
|
279
357
|
)}
|
|
280
358
|
{...props}
|
|
281
359
|
/>
|
|
360
|
+
) : (
|
|
361
|
+
<SurfaceMedia slot="dialog-media" {...props} className={className} />
|
|
282
362
|
);
|
|
283
363
|
}
|
|
284
364
|
|
|
@@ -288,11 +368,17 @@ export function DialogBody({
|
|
|
288
368
|
...props
|
|
289
369
|
}: React.ComponentProps<"div">) {
|
|
290
370
|
const { mode } = useDialogContext();
|
|
371
|
+
const layout = useSurfaceLayout("dialog");
|
|
291
372
|
return (
|
|
292
373
|
<div
|
|
293
374
|
data-slot="dialog-body"
|
|
294
375
|
className={cn(
|
|
295
|
-
mode === "alert"
|
|
376
|
+
mode === "alert"
|
|
377
|
+
? "min-w-0"
|
|
378
|
+
: cn(
|
|
379
|
+
"min-h-0 flex-1 overflow-y-auto",
|
|
380
|
+
surfaceRegionClasses("dialog", layout, "body"),
|
|
381
|
+
),
|
|
296
382
|
className,
|
|
297
383
|
)}
|
|
298
384
|
style={
|
|
@@ -310,13 +396,17 @@ export function DialogFooter({
|
|
|
310
396
|
...props
|
|
311
397
|
}: React.ComponentProps<"div">) {
|
|
312
398
|
const { mode } = useDialogContext();
|
|
399
|
+
const layout = useSurfaceLayout("dialog");
|
|
313
400
|
return (
|
|
314
401
|
<div
|
|
315
402
|
data-slot="dialog-footer"
|
|
316
403
|
className={cn(
|
|
317
404
|
mode === "alert"
|
|
318
405
|
? "w-full"
|
|
319
|
-
:
|
|
406
|
+
: cn(
|
|
407
|
+
"flex shrink-0 flex-row justify-end gap-2",
|
|
408
|
+
surfaceRegionClasses("dialog", layout, "footer"),
|
|
409
|
+
),
|
|
320
410
|
className,
|
|
321
411
|
)}
|
|
322
412
|
{...props}
|
|
@@ -326,23 +416,36 @@ export function DialogFooter({
|
|
|
326
416
|
|
|
327
417
|
export function DialogTitle({
|
|
328
418
|
className,
|
|
419
|
+
level,
|
|
420
|
+
children,
|
|
329
421
|
...props
|
|
330
|
-
}: React.ComponentProps<typeof DialogPrimitive.Title>
|
|
422
|
+
}: React.ComponentProps<typeof DialogPrimitive.Title> & {
|
|
423
|
+
/** Hierarquia do documento. Sem ele vale o `h2` que a primitiva emite. */
|
|
424
|
+
level?: HeadingLevel;
|
|
425
|
+
}) {
|
|
331
426
|
const { mode } = useDialogContext();
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
427
|
+
const layout = useSurfaceLayout("dialog");
|
|
428
|
+
const Title = mode === "alert" ? AlertDialogPrimitive.Title : DialogPrimitive.Title;
|
|
429
|
+
const classes = cn(
|
|
430
|
+
mode === "alert" ? undefined : "min-w-0 truncate",
|
|
431
|
+
surfaceTitleClasses("dialog", layout),
|
|
432
|
+
className,
|
|
433
|
+
);
|
|
434
|
+
// O nível troca a TAG do mesmo nó, via `asChild` — nunca aninha um heading dentro
|
|
435
|
+
// de outro, e a escala continua vindo do layout (ADR 0023).
|
|
436
|
+
if (level === undefined) {
|
|
437
|
+
return (
|
|
438
|
+
<Title data-slot="dialog-title" className={classes} {...props}>
|
|
439
|
+
{children}
|
|
440
|
+
</Title>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const Heading = headingTags[level];
|
|
444
|
+
return (
|
|
445
|
+
<Title asChild {...props}>
|
|
446
|
+
<Heading data-slot="dialog-title" className={classes}>
|
|
447
|
+
{children}
|
|
448
|
+
</Heading>
|
|
449
|
+
</Title>
|
|
347
450
|
);
|
|
348
451
|
}
|
|
@@ -2,6 +2,21 @@ import * as React from "react";
|
|
|
2
2
|
import { Dialog as DrawerPrimitive } from "radix-ui";
|
|
3
3
|
|
|
4
4
|
import { cn } from "../../lib/cn.ts";
|
|
5
|
+
import {
|
|
6
|
+
SurfaceLayoutProvider,
|
|
7
|
+
surfaceActionsClasses,
|
|
8
|
+
surfaceHeaderRowClasses,
|
|
9
|
+
surfaceRegionClasses,
|
|
10
|
+
surfaceTitleClasses,
|
|
11
|
+
useSurfaceLayout,
|
|
12
|
+
type SurfaceLayout,
|
|
13
|
+
} from "../patterns/surface-anatomy.ts";
|
|
14
|
+
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
15
|
+
import { SurfaceHeader } from "../patterns/surface-header.tsx";
|
|
16
|
+
import {
|
|
17
|
+
SurfaceMedia,
|
|
18
|
+
type SurfaceMediaProps,
|
|
19
|
+
} from "../patterns/surface-media.tsx";
|
|
5
20
|
import {
|
|
6
21
|
CloseButton,
|
|
7
22
|
type CloseButtonSize,
|
|
@@ -78,6 +93,7 @@ function DrawerContent({
|
|
|
78
93
|
showCloseButton = true,
|
|
79
94
|
closeDisabled = false,
|
|
80
95
|
closeSize = "sm",
|
|
96
|
+
layout = "sectioned",
|
|
81
97
|
"aria-describedby": ariaDescribedBy,
|
|
82
98
|
...props
|
|
83
99
|
}: React.ComponentProps<typeof DrawerPrimitive.Content> & {
|
|
@@ -85,12 +101,15 @@ function DrawerContent({
|
|
|
85
101
|
showCloseButton?: boolean;
|
|
86
102
|
closeDisabled?: boolean;
|
|
87
103
|
closeSize?: CloseButtonSize;
|
|
104
|
+
/** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
105
|
+
layout?: SurfaceLayout;
|
|
88
106
|
}) {
|
|
89
107
|
return (
|
|
90
108
|
<DrawerPortal>
|
|
91
109
|
<DrawerOverlay />
|
|
92
110
|
<DrawerPrimitive.Content
|
|
93
111
|
data-slot="drawer-content"
|
|
112
|
+
data-layout={layout}
|
|
94
113
|
className={cn(
|
|
95
114
|
"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",
|
|
96
115
|
side === "right" &&
|
|
@@ -106,11 +125,13 @@ function DrawerContent({
|
|
|
106
125
|
aria-describedby={ariaDescribedBy}
|
|
107
126
|
{...props}
|
|
108
127
|
>
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
<SurfaceLayoutProvider value={layout}>
|
|
129
|
+
<DrawerContentContext.Provider
|
|
130
|
+
value={{ showCloseButton, closeDisabled, closeSize }}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</DrawerContentContext.Provider>
|
|
134
|
+
</SurfaceLayoutProvider>
|
|
114
135
|
</DrawerPrimitive.Content>
|
|
115
136
|
</DrawerPortal>
|
|
116
137
|
);
|
|
@@ -121,27 +142,76 @@ function DrawerHeader({
|
|
|
121
142
|
children,
|
|
122
143
|
...props
|
|
123
144
|
}: React.ComponentProps<"div">) {
|
|
145
|
+
const layout = useSurfaceLayout("drawer");
|
|
124
146
|
const { showCloseButton, closeDisabled, closeSize } =
|
|
125
147
|
React.useContext(DrawerContentContext);
|
|
126
148
|
return (
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
149
|
+
<SurfaceHeader
|
|
150
|
+
name="DrawerHeader"
|
|
151
|
+
slot="drawer"
|
|
152
|
+
slots={{
|
|
153
|
+
leading: DrawerNavigation,
|
|
154
|
+
media: DrawerMedia,
|
|
155
|
+
title: DrawerTitle,
|
|
156
|
+
actions: DrawerActions,
|
|
157
|
+
}}
|
|
158
|
+
leadingPlacement="inline"
|
|
159
|
+
titleRequired={false}
|
|
160
|
+
rowClassName={surfaceHeaderRowClasses("drawer")}
|
|
161
|
+
className={cn(
|
|
162
|
+
"text-left",
|
|
163
|
+
surfaceRegionClasses("drawer", layout, "header"),
|
|
164
|
+
className,
|
|
165
|
+
)}
|
|
166
|
+
trailing={
|
|
167
|
+
showCloseButton ? (
|
|
168
|
+
<DrawerCloseButton disabled={closeDisabled} size={closeSize} />
|
|
169
|
+
) : undefined
|
|
170
|
+
}
|
|
130
171
|
{...props}
|
|
131
172
|
>
|
|
132
173
|
{children}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
174
|
+
</SurfaceHeader>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Moldura de ícone ou imagem que abre a linha do header. */
|
|
179
|
+
function DrawerMedia(props: SurfaceMediaProps) {
|
|
180
|
+
return <SurfaceMedia slot="drawer-media" {...props} />;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Região introdutória do header — o voltar de um fluxo modal encadeado. */
|
|
184
|
+
function DrawerNavigation({ className, ...props }: React.ComponentProps<"div">) {
|
|
185
|
+
return (
|
|
186
|
+
<div
|
|
187
|
+
data-slot="drawer-navigation-content"
|
|
188
|
+
className={cn("flex min-w-0 shrink-0 items-center", className)}
|
|
189
|
+
{...props}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Ações da tela no header, antes do botão de fechar. */
|
|
195
|
+
function DrawerActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
196
|
+
return (
|
|
197
|
+
<div
|
|
198
|
+
data-slot="drawer-actions"
|
|
199
|
+
className={cn(surfaceActionsClasses, className)}
|
|
200
|
+
{...props}
|
|
201
|
+
/>
|
|
137
202
|
);
|
|
138
203
|
}
|
|
139
204
|
|
|
140
205
|
function DrawerBody({ className, style, ...props }: React.ComponentProps<"div">) {
|
|
206
|
+
const layout = useSurfaceLayout("drawer");
|
|
141
207
|
return (
|
|
142
208
|
<div
|
|
143
209
|
data-slot="drawer-body"
|
|
144
|
-
className={cn(
|
|
210
|
+
className={cn(
|
|
211
|
+
"min-h-0 flex-1 overflow-y-auto",
|
|
212
|
+
surfaceRegionClasses("drawer", layout, "body"),
|
|
213
|
+
className,
|
|
214
|
+
)}
|
|
145
215
|
style={{ "--opus-inline-gutter": "1rem", ...style } as React.CSSProperties}
|
|
146
216
|
{...props}
|
|
147
217
|
/>
|
|
@@ -149,11 +219,16 @@ function DrawerBody({ className, style, ...props }: React.ComponentProps<"div">)
|
|
|
149
219
|
}
|
|
150
220
|
|
|
151
221
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
222
|
+
const layout = useSurfaceLayout("drawer");
|
|
152
223
|
return (
|
|
153
224
|
<div
|
|
154
225
|
data-slot="drawer-footer"
|
|
155
226
|
className={cn(
|
|
156
|
-
|
|
227
|
+
// `mt-auto` é a divergência declarada do Drawer: o conteúdo lateral quase nunca
|
|
228
|
+
// preenche a altura da janela, e sem isso o rodapé subiria atrás do corpo. No
|
|
229
|
+
// Dialog a altura é do conteúdo, então ele não precisa (ADR 0023).
|
|
230
|
+
"mt-auto flex shrink-0 flex-row justify-end gap-2",
|
|
231
|
+
surfaceRegionClasses("drawer", layout, "footer"),
|
|
157
232
|
className,
|
|
158
233
|
)}
|
|
159
234
|
{...props}
|
|
@@ -163,21 +238,45 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
163
238
|
|
|
164
239
|
function DrawerTitle({
|
|
165
240
|
className,
|
|
241
|
+
level,
|
|
242
|
+
children,
|
|
166
243
|
...props
|
|
167
|
-
}: React.ComponentProps<typeof DrawerPrimitive.Title>
|
|
244
|
+
}: React.ComponentProps<typeof DrawerPrimitive.Title> & {
|
|
245
|
+
/** Hierarquia do documento. Sem ele vale o `h2` que a primitiva emite. */
|
|
246
|
+
level?: HeadingLevel;
|
|
247
|
+
}) {
|
|
248
|
+
const layout = useSurfaceLayout("drawer");
|
|
249
|
+
const classes = cn(
|
|
250
|
+
"min-w-0 truncate",
|
|
251
|
+
surfaceTitleClasses("drawer", layout),
|
|
252
|
+
className,
|
|
253
|
+
);
|
|
254
|
+
// O nível troca a TAG do mesmo nó, via `asChild` — a escala continua vindo do layout.
|
|
255
|
+
if (level === undefined) {
|
|
256
|
+
return (
|
|
257
|
+
<DrawerPrimitive.Title
|
|
258
|
+
data-slot="drawer-title"
|
|
259
|
+
className={classes}
|
|
260
|
+
{...props}
|
|
261
|
+
>
|
|
262
|
+
{children}
|
|
263
|
+
</DrawerPrimitive.Title>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const Heading = headingTags[level];
|
|
168
267
|
return (
|
|
169
|
-
<DrawerPrimitive.Title
|
|
170
|
-
data-slot="drawer-title"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)}
|
|
175
|
-
{...props}
|
|
176
|
-
/>
|
|
268
|
+
<DrawerPrimitive.Title asChild {...props}>
|
|
269
|
+
<Heading data-slot="drawer-title" className={classes}>
|
|
270
|
+
{children}
|
|
271
|
+
</Heading>
|
|
272
|
+
</DrawerPrimitive.Title>
|
|
177
273
|
);
|
|
178
274
|
}
|
|
179
275
|
|
|
180
276
|
export {
|
|
277
|
+
DrawerActions,
|
|
278
|
+
DrawerMedia,
|
|
279
|
+
DrawerNavigation,
|
|
181
280
|
Drawer,
|
|
182
281
|
DrawerTrigger,
|
|
183
282
|
DrawerClose,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
2
2
|
|
|
3
3
|
import { cn } from '../../lib/cn.ts'
|
|
4
|
+
import { iconTileVariants } from './icon-tile.tsx'
|
|
4
5
|
|
|
5
6
|
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
7
|
return (
|
|
@@ -34,7 +35,7 @@ const emptyMediaVariants = cva(
|
|
|
34
35
|
variants: {
|
|
35
36
|
variant: {
|
|
36
37
|
default: "bg-transparent",
|
|
37
|
-
icon:
|
|
38
|
+
icon: iconTileVariants({ size: "lg", variant: "subtle" }),
|
|
38
39
|
},
|
|
39
40
|
},
|
|
40
41
|
defaultVariants: {
|
|
@@ -1,21 +1,41 @@
|
|
|
1
|
-
import { useMemo } from "react"
|
|
1
|
+
import { createContext, useContext, useMemo } from "react"
|
|
2
2
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
5
|
import { Label } from './label.tsx'
|
|
6
6
|
import { Separator } from './separator.tsx'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
export type FieldSetVariant = "plain" | "framed"
|
|
9
|
+
|
|
10
|
+
export interface FieldSetProps extends React.ComponentProps<"fieldset"> {
|
|
11
|
+
/** `framed` envolve o conjunto com a moldura canônica e integra a legenda à borda. */
|
|
12
|
+
variant?: FieldSetVariant
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const FieldSetVariantContext = createContext<FieldSetVariant>("plain")
|
|
16
|
+
|
|
17
|
+
function FieldSet({
|
|
18
|
+
className,
|
|
19
|
+
children,
|
|
20
|
+
variant = "plain",
|
|
21
|
+
...props
|
|
22
|
+
}: FieldSetProps) {
|
|
9
23
|
return (
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
<FieldSetVariantContext.Provider value={variant}>
|
|
25
|
+
<fieldset
|
|
26
|
+
data-slot="field-set"
|
|
27
|
+
data-variant={variant}
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex flex-col gap-6",
|
|
30
|
+
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
31
|
+
variant === "framed" && "rounded-lg border border-border px-4 pb-4",
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</fieldset>
|
|
38
|
+
</FieldSetVariantContext.Provider>
|
|
19
39
|
)
|
|
20
40
|
}
|
|
21
41
|
|
|
@@ -29,6 +49,8 @@ function FieldLegend({
|
|
|
29
49
|
variant?: "legend" | "label" | "section"
|
|
30
50
|
icon?: React.ReactNode
|
|
31
51
|
}) {
|
|
52
|
+
const fieldSetVariant = useContext(FieldSetVariantContext)
|
|
53
|
+
|
|
32
54
|
return (
|
|
33
55
|
<legend
|
|
34
56
|
data-slot="field-legend"
|
|
@@ -38,7 +60,10 @@ function FieldLegend({
|
|
|
38
60
|
icon !== undefined && "flex items-center gap-2",
|
|
39
61
|
"data-[variant=legend]:text-base",
|
|
40
62
|
"data-[variant=label]:text-sm",
|
|
41
|
-
|
|
63
|
+
fieldSetVariant === "plain" &&
|
|
64
|
+
"data-[variant=section]:w-full data-[variant=section]:border-b data-[variant=section]:border-border/70 data-[variant=section]:pb-2",
|
|
65
|
+
"data-[variant=section]:text-sm data-[variant=section]:font-semibold",
|
|
66
|
+
fieldSetVariant === "framed" && "-ml-2 w-auto px-2",
|
|
42
67
|
className
|
|
43
68
|
)}
|
|
44
69
|
{...props}
|
|
@@ -47,10 +72,7 @@ function FieldLegend({
|
|
|
47
72
|
<span
|
|
48
73
|
data-slot="field-legend-icon"
|
|
49
74
|
aria-hidden="true"
|
|
50
|
-
className=
|
|
51
|
-
"inline-flex shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4",
|
|
52
|
-
variant === "section" && "size-7 rounded-md bg-muted",
|
|
53
|
-
)}
|
|
75
|
+
className="inline-flex shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4"
|
|
54
76
|
>
|
|
55
77
|
{icon}
|
|
56
78
|
</span>
|