@softize/opus 29.0.0 → 31.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 +137 -0
- package/bin/lib/check.mjs +178 -94
- package/bin/lib/copy.mjs +2970 -1801
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
- package/docs/adr/0045-surface-replaces-content-and-card.md +3 -0
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
- package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
- package/docs/adr/0063-metadados-das-superficies.md +44 -0
- package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
- package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
- package/docs/adr/README.md +8 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +21 -19
- package/registry/skills/build-opus-ui/references/evaluations.md +21 -10
- package/registry/skills/build-opus-ui/references/ui-patterns.md +33 -28
- package/src/core/presentation.ts +938 -32
- package/src/core/types.ts +2 -1
- package/src/ui/components/patterns/form.tsx +2 -0
- package/src/ui/components/patterns/grouped-list.ts +2 -2
- package/src/ui/components/patterns/list.tsx +588 -365
- package/src/ui/components/patterns/page-heading-context.tsx +12 -5
- package/src/ui/components/patterns/page.tsx +238 -181
- package/src/ui/components/patterns/presentation-fields.tsx +246 -0
- package/src/ui/components/patterns/presentation-sections.tsx +494 -0
- package/src/ui/components/patterns/presentation.tsx +1142 -229
- package/src/ui/components/patterns/structural-header.tsx +47 -34
- package/src/ui/components/patterns/surface-anatomy.ts +27 -217
- package/src/ui/components/patterns/surface.tsx +55 -37
- package/src/ui/components/patterns/trigger.tsx +10 -2
- package/src/ui/components/primitives/close-button.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -8
- package/src/ui/components/primitives/detail.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +59 -89
- package/src/ui/components/primitives/drawer.tsx +52 -87
- package/src/ui/components/primitives/field.tsx +1 -1
- package/src/ui/components/primitives/pagination.tsx +3 -2
- package/src/ui/components/primitives/table.tsx +3 -3
- package/src/ui/components/primitives/tabs.tsx +13 -2
- package/src/ui/docs/content/action-form.md +2 -1
- package/src/ui/docs/content/action-list.md +146 -73
- package/src/ui/docs/content/action-trigger.md +5 -0
- package/src/ui/docs/content/button.md +5 -6
- package/src/ui/docs/content/command.md +62 -50
- package/src/ui/docs/content/compositions.md +129 -459
- package/src/ui/docs/content/dialog.md +72 -101
- package/src/ui/docs/content/drawer.md +69 -92
- package/src/ui/docs/content/field.md +2 -1
- package/src/ui/docs/content/page.md +207 -386
- package/src/ui/docs/content/presentation.md +371 -23
- package/src/ui/docs/content/separator.md +1 -1
- package/src/ui/docs/content/surface.md +94 -116
- package/src/ui/docs/content/table.md +7 -0
- package/src/ui/docs/content/tokens.md +18 -4
- package/src/ui/drivers/context.tsx +11 -4
- package/src/ui/drivers/react.tsx +27 -7
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +38 -8
- package/src/ui/theme.css +44 -33
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*
|
|
8
8
|
* framed borda, raio e recorte;
|
|
9
9
|
* elevation fundo próprio (`raised`) ou o fundo de quem hospeda (`none`);
|
|
10
|
-
* layout ritmo contínuo ou faixas com divisores derivados.
|
|
11
10
|
*
|
|
12
11
|
* Três formas de compor produzem a mesma anatomia:
|
|
13
12
|
*
|
|
@@ -42,6 +41,7 @@ import { headingTags, type HeadingLevel } from "./heading-level.ts";
|
|
|
42
41
|
import { usePageHeadingPresence } from "./page-heading-context.tsx";
|
|
43
42
|
import {
|
|
44
43
|
StructuralHeader,
|
|
44
|
+
structuralMetaClasses,
|
|
45
45
|
structuralHeaderClasses,
|
|
46
46
|
} from "./structural-header.tsx";
|
|
47
47
|
import {
|
|
@@ -51,7 +51,6 @@ import {
|
|
|
51
51
|
surfaceRegionClasses,
|
|
52
52
|
structuralRegionSpacing,
|
|
53
53
|
surfaceTitleClasses,
|
|
54
|
-
type SurfaceLayout,
|
|
55
54
|
} from "./surface-anatomy.ts";
|
|
56
55
|
|
|
57
56
|
export type SurfaceElement = "div" | "section" | "article" | "fieldset";
|
|
@@ -63,7 +62,6 @@ interface SurfaceContextValue {
|
|
|
63
62
|
/** Tem moldura ou elevação: as regiões carregam o recuo. */
|
|
64
63
|
bounded: boolean;
|
|
65
64
|
framed: boolean;
|
|
66
|
-
layout: SurfaceLayout;
|
|
67
65
|
level: HeadingLevel;
|
|
68
66
|
titleId: string;
|
|
69
67
|
variant: "page" | "section";
|
|
@@ -83,11 +81,9 @@ function requiredContext<T>(
|
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
/** Como a `legend` de um fieldset delimitado se relaciona com a moldura. */
|
|
86
|
-
function legendPlacement(
|
|
87
|
-
surface: SurfaceContextValue,
|
|
88
|
-
): "notch" | "band" | null {
|
|
84
|
+
function legendPlacement(surface: SurfaceContextValue): "band" | null {
|
|
89
85
|
if (surface.element !== "fieldset" || !surface.bounded) return null;
|
|
90
|
-
return
|
|
86
|
+
return "band";
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
interface SurfaceBaseProps {
|
|
@@ -98,8 +94,6 @@ interface SurfaceBaseProps {
|
|
|
98
94
|
* o fundo de quem hospeda.
|
|
99
95
|
*/
|
|
100
96
|
elevation?: SurfaceElevation;
|
|
101
|
-
/** Anatomia da superfície inteira: `continuous` (default) ou `sectioned`. */
|
|
102
|
-
layout?: SurfaceLayout;
|
|
103
97
|
className?: string;
|
|
104
98
|
children?: ReactNode;
|
|
105
99
|
}
|
|
@@ -113,6 +107,8 @@ interface SurfaceHeaderShorthandProps {
|
|
|
113
107
|
count?: number;
|
|
114
108
|
/** Forma curta: frase de apoio sob o título. */
|
|
115
109
|
description?: ReactNode;
|
|
110
|
+
/** Forma curta: badges, datas e autoria sob a orientação. */
|
|
111
|
+
meta?: ReactNode;
|
|
116
112
|
/** Forma curta: ações sobre a superfície inteira, no extremo oposto ao título. */
|
|
117
113
|
actions?: ReactNode;
|
|
118
114
|
}
|
|
@@ -124,6 +120,7 @@ interface SurfaceLegendShorthandProps {
|
|
|
124
120
|
media?: ReactNode;
|
|
125
121
|
count?: never;
|
|
126
122
|
description?: never;
|
|
123
|
+
meta?: never;
|
|
127
124
|
actions?: never;
|
|
128
125
|
}
|
|
129
126
|
|
|
@@ -132,6 +129,7 @@ interface SurfaceNoShorthandProps {
|
|
|
132
129
|
media?: never;
|
|
133
130
|
count?: never;
|
|
134
131
|
description?: never;
|
|
132
|
+
meta?: never;
|
|
135
133
|
actions?: never;
|
|
136
134
|
}
|
|
137
135
|
|
|
@@ -198,12 +196,12 @@ export function Surface({
|
|
|
198
196
|
asChild = false,
|
|
199
197
|
framed = false,
|
|
200
198
|
elevation = "none",
|
|
201
|
-
layout = "continuous",
|
|
202
199
|
level = 2,
|
|
203
200
|
title,
|
|
204
201
|
media,
|
|
205
202
|
count,
|
|
206
203
|
description,
|
|
204
|
+
meta,
|
|
207
205
|
actions,
|
|
208
206
|
className,
|
|
209
207
|
children,
|
|
@@ -218,11 +216,6 @@ export function Surface({
|
|
|
218
216
|
const element: SurfaceElement | null = asChild ? null : (as ?? "div");
|
|
219
217
|
const raised = elevation === "raised";
|
|
220
218
|
const bounded = framed || raised;
|
|
221
|
-
if (layout === "sectioned" && !bounded) {
|
|
222
|
-
throw new Error(
|
|
223
|
-
'Surface layout="sectioned" exige framed ou elevation="raised".',
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
219
|
|
|
227
220
|
const shorthand = title !== undefined;
|
|
228
221
|
if (
|
|
@@ -230,10 +223,11 @@ export function Surface({
|
|
|
230
223
|
(hasMedia(media) ||
|
|
231
224
|
count !== undefined ||
|
|
232
225
|
description !== undefined ||
|
|
226
|
+
meta !== undefined ||
|
|
233
227
|
actions !== undefined)
|
|
234
228
|
) {
|
|
235
229
|
throw new Error(
|
|
236
|
-
"Surface exige title para usar media, count, description ou actions.",
|
|
230
|
+
"Surface exige title para usar media, count, description, meta ou actions.",
|
|
237
231
|
);
|
|
238
232
|
}
|
|
239
233
|
if (asChild && shorthand) {
|
|
@@ -292,7 +286,6 @@ export function Surface({
|
|
|
292
286
|
element,
|
|
293
287
|
bounded,
|
|
294
288
|
framed,
|
|
295
|
-
layout,
|
|
296
289
|
level,
|
|
297
290
|
titleId: resolvedTitleId,
|
|
298
291
|
variant,
|
|
@@ -303,10 +296,11 @@ export function Surface({
|
|
|
303
296
|
<SurfaceHeader>
|
|
304
297
|
{hasMedia(media) && <SurfaceMedia>{media}</SurfaceMedia>}
|
|
305
298
|
<SurfaceTitle>{title}</SurfaceTitle>
|
|
306
|
-
{count !== undefined && <
|
|
299
|
+
{count !== undefined && <SurfaceCount>{count}</SurfaceCount>}
|
|
307
300
|
{description !== undefined && (
|
|
308
301
|
<SurfaceDescription>{description}</SurfaceDescription>
|
|
309
302
|
)}
|
|
303
|
+
{meta !== undefined && <SurfaceMeta>{meta}</SurfaceMeta>}
|
|
310
304
|
{actions !== undefined && <SurfaceActions>{actions}</SurfaceActions>}
|
|
311
305
|
</SurfaceHeader>
|
|
312
306
|
{regions > 0
|
|
@@ -334,13 +328,12 @@ export function Surface({
|
|
|
334
328
|
data-as={element ?? undefined}
|
|
335
329
|
data-framed={framed}
|
|
336
330
|
data-elevation={elevation}
|
|
337
|
-
data-layout={layout}
|
|
338
331
|
{...(link ? fill.attributes : {})}
|
|
339
332
|
aria-labelledby={named ? resolvedTitleId : undefined}
|
|
340
333
|
className={cn(
|
|
341
334
|
framed && "rounded-xl border border-border",
|
|
342
335
|
// O divisor e o conteúdo sem recuo encostam na borda: o raio precisa recortar.
|
|
343
|
-
framed &&
|
|
336
|
+
framed && "overflow-hidden",
|
|
344
337
|
framed && "has-[>[data-flush=true]]:overflow-hidden",
|
|
345
338
|
raised &&
|
|
346
339
|
"bg-card text-card-foreground [--surface-background:var(--card)]",
|
|
@@ -367,10 +360,16 @@ export function Surface({
|
|
|
367
360
|
);
|
|
368
361
|
}
|
|
369
362
|
|
|
370
|
-
export type SurfaceHeaderProps = Omit<
|
|
363
|
+
export type SurfaceHeaderProps = Omit<
|
|
364
|
+
HTMLAttributes<HTMLDivElement>,
|
|
365
|
+
"title"
|
|
366
|
+
> & {
|
|
367
|
+
/** TabsList abaixo do título; indisponível em fieldset. */
|
|
368
|
+
tabs?: ReactNode;
|
|
369
|
+
};
|
|
371
370
|
|
|
372
371
|
const legendInvalidMessage =
|
|
373
|
-
"SurfaceHeader de fieldset aceita somente SurfaceMedia e SurfaceTitle; contador, descrição e ações ficam fora da legenda.";
|
|
372
|
+
"SurfaceHeader de fieldset aceita somente SurfaceMedia e SurfaceTitle; contador, descrição, metadados e ações ficam fora da legenda.";
|
|
374
373
|
|
|
375
374
|
/**
|
|
376
375
|
* Header da superfície; não existe fora dela. Monta a linha sozinho — mídia, título com
|
|
@@ -401,7 +400,8 @@ export function SurfaceHeader({
|
|
|
401
400
|
: {
|
|
402
401
|
media: SurfaceMedia,
|
|
403
402
|
title: SurfaceTitle,
|
|
404
|
-
count:
|
|
403
|
+
count: SurfaceCount,
|
|
404
|
+
meta: SurfaceMeta,
|
|
405
405
|
description: SurfaceDescription,
|
|
406
406
|
actions: SurfaceActions,
|
|
407
407
|
}
|
|
@@ -411,9 +411,9 @@ export function SurfaceHeader({
|
|
|
411
411
|
rowClassName={surfaceHeaderRowClasses("surface")}
|
|
412
412
|
{...(legend ? { invalidMessage: legendInvalidMessage } : {})}
|
|
413
413
|
className={cn(
|
|
414
|
-
surface
|
|
415
|
-
|
|
416
|
-
|
|
414
|
+
surfaceRegionClasses("surface", "header", {
|
|
415
|
+
bounded: surface.bounded,
|
|
416
|
+
}),
|
|
417
417
|
placement !== null && surfaceLegendClasses[placement],
|
|
418
418
|
className,
|
|
419
419
|
)}
|
|
@@ -470,7 +470,7 @@ export function SurfaceTitle({
|
|
|
470
470
|
);
|
|
471
471
|
const legend = surface.element === "fieldset";
|
|
472
472
|
const Heading = legend ? "span" : headingTags[surface.level];
|
|
473
|
-
usePageHeadingPresence(!legend && surface.level === 1);
|
|
473
|
+
usePageHeadingPresence(!legend && surface.level === 1, "surface");
|
|
474
474
|
return (
|
|
475
475
|
<Heading
|
|
476
476
|
id={id ?? surface.titleId}
|
|
@@ -478,7 +478,7 @@ export function SurfaceTitle({
|
|
|
478
478
|
className={cn(
|
|
479
479
|
surface.variant === "page"
|
|
480
480
|
? structuralHeaderClasses.page.title
|
|
481
|
-
: surfaceTitleClasses("surface"
|
|
481
|
+
: surfaceTitleClasses("surface"),
|
|
482
482
|
className,
|
|
483
483
|
)}
|
|
484
484
|
{...props}
|
|
@@ -509,25 +509,44 @@ export function SurfaceDescription({
|
|
|
509
509
|
);
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
/**
|
|
513
|
-
export function
|
|
512
|
+
/** Contador ao lado do título — o slot do `count` da forma curta. */
|
|
513
|
+
export function SurfaceCount({
|
|
514
514
|
className,
|
|
515
515
|
...props
|
|
516
516
|
}: HTMLAttributes<HTMLSpanElement>): ReactElement {
|
|
517
517
|
const surface = requiredContext(
|
|
518
518
|
useContext(SurfaceHeaderContext),
|
|
519
|
-
"
|
|
519
|
+
"SurfaceCount",
|
|
520
520
|
"SurfaceHeader",
|
|
521
521
|
);
|
|
522
522
|
return (
|
|
523
523
|
<span
|
|
524
|
-
data-slot="surface-
|
|
524
|
+
data-slot="surface-count"
|
|
525
525
|
className={cn(structuralHeaderClasses[surface.variant].count, className)}
|
|
526
526
|
{...props}
|
|
527
527
|
/>
|
|
528
528
|
);
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
+
/** Badges, datas e autoria sob o título e a orientação da superfície. */
|
|
532
|
+
export function SurfaceMeta({
|
|
533
|
+
className,
|
|
534
|
+
...props
|
|
535
|
+
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
536
|
+
requiredContext(
|
|
537
|
+
useContext(SurfaceHeaderContext),
|
|
538
|
+
"SurfaceMeta",
|
|
539
|
+
"SurfaceHeader",
|
|
540
|
+
);
|
|
541
|
+
return (
|
|
542
|
+
<div
|
|
543
|
+
data-slot="surface-meta"
|
|
544
|
+
className={cn(structuralMetaClasses, className)}
|
|
545
|
+
{...props}
|
|
546
|
+
/>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
531
550
|
/** Ações do header — a família as posiciona no extremo oposto ao título. */
|
|
532
551
|
export function SurfaceActions({
|
|
533
552
|
className,
|
|
@@ -577,10 +596,10 @@ export function SurfaceBody({
|
|
|
577
596
|
data-flush={flush ? "true" : undefined}
|
|
578
597
|
{...fill.attributes}
|
|
579
598
|
className={cn(
|
|
580
|
-
surfaceRegionClasses("surface",
|
|
581
|
-
|
|
599
|
+
surfaceRegionClasses("surface", "body", {
|
|
600
|
+
bounded: surface.bounded,
|
|
601
|
+
flush,
|
|
582
602
|
}),
|
|
583
|
-
placement === "notch" && surfaceLegendClasses.afterNotch,
|
|
584
603
|
placement === "band" && surfaceLegendClasses.afterBand,
|
|
585
604
|
fill.filling && fillRegionClassName,
|
|
586
605
|
className,
|
|
@@ -608,8 +627,7 @@ export function SurfaceFooter({
|
|
|
608
627
|
data-slot="surface-footer"
|
|
609
628
|
className={cn(
|
|
610
629
|
"flex items-center justify-end gap-2",
|
|
611
|
-
surface.bounded
|
|
612
|
-
surfaceRegionClasses("surface", surface.layout, "footer"),
|
|
630
|
+
surfaceRegionClasses("surface", "footer", { bounded: surface.bounded }),
|
|
613
631
|
placement === "band" && surfaceLegendClasses.afterBand,
|
|
614
632
|
className,
|
|
615
633
|
)}
|
|
@@ -52,6 +52,8 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
52
52
|
input: TInput;
|
|
53
53
|
/** Texto do botão. Default usa action.label. */
|
|
54
54
|
label?: string;
|
|
55
|
+
/** Nome acessível completo quando o texto visível é abreviado. */
|
|
56
|
+
"aria-label"?: string;
|
|
55
57
|
/** Contexto visual. Por padrão, ações destrutivas usam `danger`. */
|
|
56
58
|
context?: ButtonProps["context"];
|
|
57
59
|
/** Tratamento visual do Button. */
|
|
@@ -70,6 +72,8 @@ export interface ActionTriggerProps<TInput, TData> {
|
|
|
70
72
|
actionLabel?: string;
|
|
71
73
|
cancelLabel?: string;
|
|
72
74
|
};
|
|
75
|
+
/** Mensagem de sucesso desta ocorrência; substitui a mensagem do contrato. */
|
|
76
|
+
successMessage?: string;
|
|
73
77
|
/** Callback adicional pós-sucesso (cache já foi invalidado). */
|
|
74
78
|
onSuccess?: (data: TData) => void;
|
|
75
79
|
/** Notifica o renderer que coordena bloqueio entre várias actions. */
|
|
@@ -87,12 +91,14 @@ export function ActionTrigger<TInput, TData>({
|
|
|
87
91
|
action,
|
|
88
92
|
input,
|
|
89
93
|
label,
|
|
94
|
+
"aria-label": ariaLabel,
|
|
90
95
|
context,
|
|
91
96
|
variant,
|
|
92
97
|
size,
|
|
93
98
|
disabled = false,
|
|
94
99
|
confirm: confirmProp,
|
|
95
100
|
onSuccess,
|
|
101
|
+
successMessage,
|
|
96
102
|
onLoadingChange,
|
|
97
103
|
icon,
|
|
98
104
|
itemLabel,
|
|
@@ -102,7 +108,9 @@ export function ActionTrigger<TInput, TData>({
|
|
|
102
108
|
const [open, setOpen] = useState(false);
|
|
103
109
|
const { trigger, isLoading } = useTriggerAction(action, {
|
|
104
110
|
onSuccess: (data) => {
|
|
105
|
-
toast.success(
|
|
111
|
+
toast.success(
|
|
112
|
+
successMessage ?? msgText(action.messages?.success, "Concluído"),
|
|
113
|
+
);
|
|
106
114
|
onSuccess?.(data);
|
|
107
115
|
setOpen(false);
|
|
108
116
|
},
|
|
@@ -184,7 +192,7 @@ export function ActionTrigger<TInput, TData>({
|
|
|
184
192
|
size={size ?? (icon !== undefined ? "icon-xs" : "default")}
|
|
185
193
|
busy={isLoading}
|
|
186
194
|
disabled={disabled}
|
|
187
|
-
aria-label={icon !== undefined ? buttonLabel : undefined}
|
|
195
|
+
aria-label={ariaLabel ?? (icon !== undefined ? buttonLabel : undefined)}
|
|
188
196
|
onClick={(e) => {
|
|
189
197
|
e.stopPropagation();
|
|
190
198
|
onClick();
|
|
@@ -3,22 +3,30 @@ import { XIcon } from "lucide-react";
|
|
|
3
3
|
|
|
4
4
|
import { Button, type ButtonProps } from "./button.tsx";
|
|
5
5
|
|
|
6
|
-
export type CloseButtonSize = "xs" | "sm";
|
|
6
|
+
export type CloseButtonSize = "xs" | "sm" | "default";
|
|
7
7
|
|
|
8
8
|
const buttonSizeByCloseSize = {
|
|
9
|
+
default: "icon",
|
|
9
10
|
xs: "icon-xs",
|
|
10
11
|
sm: "icon-sm",
|
|
11
12
|
} as const;
|
|
12
13
|
|
|
13
14
|
export interface CloseButtonProps extends Omit<
|
|
14
15
|
ButtonProps,
|
|
15
|
-
|
|
16
|
+
| "busy"
|
|
17
|
+
| "children"
|
|
18
|
+
| "context"
|
|
19
|
+
| "icon"
|
|
20
|
+
| "shape"
|
|
21
|
+
| "size"
|
|
22
|
+
| "type"
|
|
23
|
+
| "variant"
|
|
16
24
|
> {
|
|
17
25
|
size?: CloseButtonSize;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export function CloseButton({
|
|
21
|
-
size = "
|
|
29
|
+
size = "xs",
|
|
22
30
|
disabled = false,
|
|
23
31
|
"aria-label": ariaLabel = "Fechar",
|
|
24
32
|
...props
|
|
@@ -38,9 +38,6 @@ function CommandDialog({
|
|
|
38
38
|
return (
|
|
39
39
|
<Dialog {...props}>
|
|
40
40
|
<DialogContent
|
|
41
|
-
// A paleta é uma superfície contínua: a lista já separa os grupos, e uma faixa
|
|
42
|
-
// sob o cabeçalho competiria com ela.
|
|
43
|
-
layout="continuous"
|
|
44
41
|
className={cn("overflow-hidden p-0", className)}
|
|
45
42
|
showCloseButton={showCloseButton}
|
|
46
43
|
aria-describedby={descriptionId}
|
|
@@ -50,11 +47,8 @@ function CommandDialog({
|
|
|
50
47
|
<p id={descriptionId} className="sr-only">
|
|
51
48
|
{description}
|
|
52
49
|
</p>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
superfície do SDK que diverge nos dois, e por isso está escrito aqui. */}
|
|
56
|
-
<DialogHeader className="px-3 py-2">
|
|
57
|
-
<DialogTitle className="text-sm">{title}</DialogTitle>
|
|
50
|
+
<DialogHeader>
|
|
51
|
+
<DialogTitle>{title}</DialogTitle>
|
|
58
52
|
</DialogHeader>
|
|
59
53
|
<Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
60
54
|
{children}
|
|
@@ -122,7 +122,7 @@ function DetailField({
|
|
|
122
122
|
!dividedHorizontalField &&
|
|
123
123
|
"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",
|
|
124
124
|
dividedHorizontalField && "items-stretch gap-x-0",
|
|
125
|
-
"group-data-[orientation=vertical]/detail-group:gap-y-1",
|
|
125
|
+
"group-data-[orientation=vertical]/detail-group:gap-y-1.5",
|
|
126
126
|
"group-data-[dividers=true]/detail-group:outline group-data-[dividers=true]/detail-group:outline-1 group-data-[dividers=true]/detail-group:outline-border",
|
|
127
127
|
"group-data-[variant=framed]/detail-group:bg-transparent",
|
|
128
128
|
className,
|
|
@@ -3,21 +3,17 @@ 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
5
|
import {
|
|
6
|
-
SurfaceLayoutProvider,
|
|
7
6
|
surfaceActionsClasses,
|
|
8
7
|
surfaceDescriptionClasses,
|
|
9
8
|
surfaceHeaderRowClasses,
|
|
10
9
|
surfaceRegionClasses,
|
|
11
10
|
surfaceTitleClasses,
|
|
12
|
-
useSurfaceLayout,
|
|
13
|
-
type SurfaceLayout,
|
|
14
11
|
} from "../patterns/surface-anatomy.ts";
|
|
15
12
|
import { FillBoundary } from "../patterns/fill-region.ts";
|
|
16
13
|
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
17
14
|
import {
|
|
18
15
|
StructuralHeader,
|
|
19
|
-
|
|
20
|
-
validateSurfaceIntro,
|
|
16
|
+
structuralMetaClasses,
|
|
21
17
|
} from "../patterns/structural-header.tsx";
|
|
22
18
|
import {
|
|
23
19
|
StructuralMedia,
|
|
@@ -64,10 +60,10 @@ interface DialogContentContextValue {
|
|
|
64
60
|
const DialogContentContext = React.createContext<DialogContentContextValue>({
|
|
65
61
|
showCloseButton: false,
|
|
66
62
|
closeDisabled: false,
|
|
67
|
-
closeSize: "
|
|
63
|
+
closeSize: "xs",
|
|
68
64
|
});
|
|
69
65
|
|
|
70
|
-
const
|
|
66
|
+
const DialogMetaContext = React.createContext(false);
|
|
71
67
|
|
|
72
68
|
/**
|
|
73
69
|
* Compõe o handler de dispensa do consumidor com o bloqueio de `closeDisabled`: o handler dele
|
|
@@ -415,8 +411,7 @@ export function DialogContent({
|
|
|
415
411
|
children,
|
|
416
412
|
showCloseButton,
|
|
417
413
|
closeDisabled = false,
|
|
418
|
-
closeSize = "
|
|
419
|
-
layout = "sectioned",
|
|
414
|
+
closeSize = "xs",
|
|
420
415
|
"aria-describedby": ariaDescribedBy,
|
|
421
416
|
onEscapeKeyDown,
|
|
422
417
|
onPointerDownOutside,
|
|
@@ -436,8 +431,6 @@ export function DialogContent({
|
|
|
436
431
|
closeDisabled?: boolean;
|
|
437
432
|
/** Tamanho da ação circular de fechar no cabeçalho. */
|
|
438
433
|
closeSize?: CloseButtonSize;
|
|
439
|
-
/** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
440
|
-
layout?: SurfaceLayout;
|
|
441
434
|
}) {
|
|
442
435
|
const { mode, open, modal } = useDialogContext();
|
|
443
436
|
const layer = useLayerPortal();
|
|
@@ -527,7 +520,6 @@ export function DialogContent({
|
|
|
527
520
|
ref={composedRef}
|
|
528
521
|
data-slot="dialog-content"
|
|
529
522
|
data-mode="default"
|
|
530
|
-
data-layout={layout}
|
|
531
523
|
{...{ [LAYER_MODAL_ATTRIBUTE]: internal || undefined }}
|
|
532
524
|
className={cn(
|
|
533
525
|
contentClasses,
|
|
@@ -548,13 +540,13 @@ export function DialogContent({
|
|
|
548
540
|
{...props}
|
|
549
541
|
>
|
|
550
542
|
<LayerPortalProvider value={nestedPortal}>
|
|
551
|
-
<
|
|
543
|
+
<FillBoundary>
|
|
552
544
|
<DialogContentContext.Provider
|
|
553
545
|
value={{ showCloseButton: showClose, closeDisabled, closeSize }}
|
|
554
546
|
>
|
|
555
547
|
{children}
|
|
556
548
|
</DialogContentContext.Provider>
|
|
557
|
-
</
|
|
549
|
+
</FillBoundary>
|
|
558
550
|
</LayerPortalProvider>
|
|
559
551
|
</DialogPrimitive.Content>
|
|
560
552
|
</DialogPortal>
|
|
@@ -564,16 +556,21 @@ export function DialogContent({
|
|
|
564
556
|
export function DialogHeader({
|
|
565
557
|
className,
|
|
566
558
|
children,
|
|
559
|
+
tabs,
|
|
567
560
|
...props
|
|
568
|
-
}: React.ComponentProps<"div">
|
|
561
|
+
}: React.ComponentProps<"div"> & {
|
|
562
|
+
tabs?: React.ReactNode;
|
|
563
|
+
}) {
|
|
569
564
|
const { mode } = useDialogContext();
|
|
570
|
-
const layout = useSurfaceLayout("dialog");
|
|
571
565
|
const { showCloseButton, closeDisabled, closeSize } =
|
|
572
566
|
React.useContext(DialogContentContext);
|
|
573
567
|
|
|
574
568
|
// O modo de alerta é a exceção declarada da ADR 0023: a resposta obrigatória empilha
|
|
575
569
|
// mídia, título e corpo centralizados, e o padding mora na raiz do conteúdo.
|
|
576
570
|
if (mode === "alert") {
|
|
571
|
+
if (tabs !== undefined && tabs !== null && tabs !== false) {
|
|
572
|
+
throw new Error("DialogHeader de alerta não aceita abas.");
|
|
573
|
+
}
|
|
577
574
|
return (
|
|
578
575
|
<div
|
|
579
576
|
data-slot="dialog-header"
|
|
@@ -589,33 +586,37 @@ export function DialogHeader({
|
|
|
589
586
|
}
|
|
590
587
|
|
|
591
588
|
return (
|
|
592
|
-
<
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
className
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
589
|
+
<DialogMetaContext.Provider value>
|
|
590
|
+
<StructuralHeader
|
|
591
|
+
tabs={tabs}
|
|
592
|
+
name="DialogHeader"
|
|
593
|
+
slot="dialog"
|
|
594
|
+
slots={{
|
|
595
|
+
leading: DialogNavigation,
|
|
596
|
+
media: DialogMedia,
|
|
597
|
+
title: DialogTitle,
|
|
598
|
+
description: DialogDescription,
|
|
599
|
+
meta: DialogMeta,
|
|
600
|
+
actions: DialogActions,
|
|
601
|
+
}}
|
|
602
|
+
leadingPlacement="inline"
|
|
603
|
+
titleRequired
|
|
604
|
+
rowClassName={surfaceHeaderRowClasses("dialog")}
|
|
605
|
+
className={cn(
|
|
606
|
+
"text-left",
|
|
607
|
+
surfaceRegionClasses("dialog", "header"),
|
|
608
|
+
className,
|
|
609
|
+
)}
|
|
610
|
+
trailing={
|
|
611
|
+
showCloseButton ? (
|
|
612
|
+
<DialogCloseButton disabled={closeDisabled} size={closeSize} />
|
|
613
|
+
) : undefined
|
|
614
|
+
}
|
|
615
|
+
{...props}
|
|
616
|
+
>
|
|
617
|
+
{children}
|
|
618
|
+
</StructuralHeader>
|
|
619
|
+
</DialogMetaContext.Provider>
|
|
619
620
|
);
|
|
620
621
|
}
|
|
621
622
|
|
|
@@ -647,42 +648,22 @@ export function DialogActions({
|
|
|
647
648
|
);
|
|
648
649
|
}
|
|
649
650
|
|
|
650
|
-
/**
|
|
651
|
-
export function
|
|
652
|
-
className,
|
|
653
|
-
children,
|
|
654
|
-
...props
|
|
655
|
-
}: React.ComponentProps<"div">) {
|
|
656
|
-
return (
|
|
657
|
-
<DialogIntroContext.Provider value>
|
|
658
|
-
<StructuralHeader
|
|
659
|
-
name="DialogIntro"
|
|
660
|
-
slot="dialog-intro"
|
|
661
|
-
slots={{
|
|
662
|
-
title: DialogTitle,
|
|
663
|
-
description: DialogDescription,
|
|
664
|
-
actions: DialogIntroActions,
|
|
665
|
-
}}
|
|
666
|
-
leadingPlacement="inline"
|
|
667
|
-
titleRequired
|
|
668
|
-
className={cn("text-left", className)}
|
|
669
|
-
{...props}
|
|
670
|
-
>
|
|
671
|
-
{children}
|
|
672
|
-
</StructuralHeader>
|
|
673
|
-
</DialogIntroContext.Provider>
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/** Ações relacionadas ao título do conteúdo, separadas das ações de chrome do header. */
|
|
678
|
-
export function DialogIntroActions({
|
|
651
|
+
/** Badges, datas e autoria abaixo do título e da orientação. */
|
|
652
|
+
export function DialogMeta({
|
|
679
653
|
className,
|
|
680
654
|
...props
|
|
681
655
|
}: React.ComponentProps<"div">) {
|
|
656
|
+
const { mode } = useDialogContext();
|
|
657
|
+
const inside = React.useContext(DialogMetaContext);
|
|
658
|
+
if (mode === "alert")
|
|
659
|
+
throw new Error(
|
|
660
|
+
'DialogMeta não está disponível em Dialog com mode="alert"; coloque as informações no corpo do alerta.',
|
|
661
|
+
);
|
|
662
|
+
if (!inside) throw new Error("DialogMeta deve estar dentro de DialogHeader.");
|
|
682
663
|
return (
|
|
683
664
|
<div
|
|
684
|
-
data-slot="dialog-
|
|
685
|
-
className={cn(
|
|
665
|
+
data-slot="dialog-meta"
|
|
666
|
+
className={cn(structuralMetaClasses, className)}
|
|
686
667
|
{...props}
|
|
687
668
|
/>
|
|
688
669
|
);
|
|
@@ -701,8 +682,6 @@ export function DialogBody({
|
|
|
701
682
|
...props
|
|
702
683
|
}: React.ComponentProps<"div"> & { flush?: boolean }) {
|
|
703
684
|
const { mode } = useDialogContext();
|
|
704
|
-
const layout = useSurfaceLayout("dialog");
|
|
705
|
-
validateSurfaceIntro(props.children, DialogIntro, "DialogBody");
|
|
706
685
|
return (
|
|
707
686
|
<div
|
|
708
687
|
data-slot="dialog-body"
|
|
@@ -711,7 +690,7 @@ export function DialogBody({
|
|
|
711
690
|
? "flex min-w-0 flex-col gap-6"
|
|
712
691
|
: cn(
|
|
713
692
|
"min-h-0 flex-1 overflow-y-auto",
|
|
714
|
-
surfaceRegionClasses("dialog",
|
|
693
|
+
surfaceRegionClasses("dialog", "body", { flush }),
|
|
715
694
|
),
|
|
716
695
|
className,
|
|
717
696
|
)}
|
|
@@ -727,7 +706,6 @@ export function DialogFooter({
|
|
|
727
706
|
...props
|
|
728
707
|
}: React.ComponentProps<"div">) {
|
|
729
708
|
const { mode } = useDialogContext();
|
|
730
|
-
const layout = useSurfaceLayout("dialog");
|
|
731
709
|
return (
|
|
732
710
|
<div
|
|
733
711
|
data-slot="dialog-footer"
|
|
@@ -736,7 +714,7 @@ export function DialogFooter({
|
|
|
736
714
|
? "w-full"
|
|
737
715
|
: cn(
|
|
738
716
|
"flex shrink-0 flex-row justify-end gap-2",
|
|
739
|
-
surfaceRegionClasses("dialog",
|
|
717
|
+
surfaceRegionClasses("dialog", "footer"),
|
|
740
718
|
),
|
|
741
719
|
className,
|
|
742
720
|
)}
|
|
@@ -755,19 +733,11 @@ export function DialogTitle({
|
|
|
755
733
|
level?: HeadingLevel;
|
|
756
734
|
}) {
|
|
757
735
|
const { mode } = useDialogContext();
|
|
758
|
-
const intro = React.useContext(DialogIntroContext);
|
|
759
|
-
const layout = useSurfaceLayout("dialog");
|
|
760
736
|
const Title =
|
|
761
737
|
mode === "alert" ? AlertDialogPrimitive.Title : DialogPrimitive.Title;
|
|
762
738
|
const classes = cn(
|
|
763
|
-
mode === "alert"
|
|
764
|
-
|
|
765
|
-
: intro
|
|
766
|
-
? "min-w-0 whitespace-normal"
|
|
767
|
-
: "min-w-0 truncate",
|
|
768
|
-
intro
|
|
769
|
-
? structuralHeaderClasses.page.title
|
|
770
|
-
: surfaceTitleClasses("dialog", layout),
|
|
739
|
+
mode === "alert" ? undefined : "min-w-0 truncate",
|
|
740
|
+
surfaceTitleClasses("dialog"),
|
|
771
741
|
className,
|
|
772
742
|
);
|
|
773
743
|
// O nível troca a TAG do mesmo nó, via `asChild` — nunca aninha um heading dentro
|