@softize/opus 30.0.0 → 31.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.
- package/CHANGELOG.md +123 -0
- package/bin/lib/check.mjs +109 -101
- package/bin/lib/copy.mjs +2973 -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/0060-surface-composition-separates-chrome-and-intro.md +142 -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/0066-civil-date-input-and-calendar-list.md +28 -0
- package/docs/adr/README.md +6 -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 +13 -11
- package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
- package/src/core/presentation.ts +938 -32
- package/src/core/types.ts +2 -0
- package/src/ui/components/patterns/form.tsx +22 -1
- package/src/ui/components/patterns/grouped-list.ts +2 -2
- package/src/ui/components/patterns/list-calendar.tsx +129 -0
- package/src/ui/components/patterns/list.tsx +612 -382
- package/src/ui/components/patterns/page-heading-context.tsx +12 -5
- package/src/ui/components/patterns/page.tsx +227 -197
- 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 +23 -32
- package/src/ui/components/patterns/surface-anatomy.ts +27 -217
- package/src/ui/components/patterns/surface.tsx +19 -29
- package/src/ui/components/patterns/trigger.tsx +10 -2
- package/src/ui/components/primitives/calendar.tsx +39 -18
- package/src/ui/components/primitives/close-button.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -8
- package/src/ui/components/primitives/date-input.tsx +276 -0
- package/src/ui/components/primitives/detail.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +19 -81
- package/src/ui/components/primitives/drawer.tsx +17 -79
- 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 +1 -1
- package/src/ui/components/primitives/tabs.tsx +13 -2
- package/src/ui/docs/content/action-form.md +6 -1
- package/src/ui/docs/content/action-list.md +156 -68
- package/src/ui/docs/content/action-trigger.md +5 -0
- package/src/ui/docs/content/button.md +5 -6
- package/src/ui/docs/content/calendar-month-view.md +50 -0
- package/src/ui/docs/content/calendar.md +6 -1
- package/src/ui/docs/content/command.md +62 -50
- package/src/ui/docs/content/compositions.md +129 -459
- package/src/ui/docs/content/date-input.md +67 -0
- package/src/ui/docs/content/dialog.md +44 -112
- package/src/ui/docs/content/drawer.md +44 -103
- package/src/ui/docs/content/field.md +2 -1
- package/src/ui/docs/content/input.md +3 -0
- package/src/ui/docs/content/page.md +204 -412
- 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 +65 -120
- package/src/ui/docs/content/tokens.md +18 -4
- package/src/ui/docs/registry.tsx +38 -9
- package/src/ui/drivers/context.tsx +11 -4
- package/src/ui/drivers/react.tsx +27 -7
- package/src/ui/meta.ts +17 -6
- package/src/ui/react.tsx +41 -8
- package/src/ui/theme.css +44 -33
|
@@ -3,22 +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
16
|
structuralMetaClasses,
|
|
20
|
-
structuralHeaderClasses,
|
|
21
|
-
validateSurfaceIntro,
|
|
22
17
|
} from "../patterns/structural-header.tsx";
|
|
23
18
|
import {
|
|
24
19
|
StructuralMedia,
|
|
@@ -65,11 +60,10 @@ interface DialogContentContextValue {
|
|
|
65
60
|
const DialogContentContext = React.createContext<DialogContentContextValue>({
|
|
66
61
|
showCloseButton: false,
|
|
67
62
|
closeDisabled: false,
|
|
68
|
-
closeSize: "
|
|
63
|
+
closeSize: "xs",
|
|
69
64
|
});
|
|
70
65
|
|
|
71
66
|
const DialogMetaContext = React.createContext(false);
|
|
72
|
-
const DialogIntroContext = React.createContext(false);
|
|
73
67
|
|
|
74
68
|
/**
|
|
75
69
|
* Compõe o handler de dispensa do consumidor com o bloqueio de `closeDisabled`: o handler dele
|
|
@@ -417,8 +411,7 @@ export function DialogContent({
|
|
|
417
411
|
children,
|
|
418
412
|
showCloseButton,
|
|
419
413
|
closeDisabled = false,
|
|
420
|
-
closeSize = "
|
|
421
|
-
layout = "sectioned",
|
|
414
|
+
closeSize = "xs",
|
|
422
415
|
"aria-describedby": ariaDescribedBy,
|
|
423
416
|
onEscapeKeyDown,
|
|
424
417
|
onPointerDownOutside,
|
|
@@ -438,8 +431,6 @@ export function DialogContent({
|
|
|
438
431
|
closeDisabled?: boolean;
|
|
439
432
|
/** Tamanho da ação circular de fechar no cabeçalho. */
|
|
440
433
|
closeSize?: CloseButtonSize;
|
|
441
|
-
/** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
442
|
-
layout?: SurfaceLayout;
|
|
443
434
|
}) {
|
|
444
435
|
const { mode, open, modal } = useDialogContext();
|
|
445
436
|
const layer = useLayerPortal();
|
|
@@ -529,7 +520,6 @@ export function DialogContent({
|
|
|
529
520
|
ref={composedRef}
|
|
530
521
|
data-slot="dialog-content"
|
|
531
522
|
data-mode="default"
|
|
532
|
-
data-layout={layout}
|
|
533
523
|
{...{ [LAYER_MODAL_ATTRIBUTE]: internal || undefined }}
|
|
534
524
|
className={cn(
|
|
535
525
|
contentClasses,
|
|
@@ -550,13 +540,13 @@ export function DialogContent({
|
|
|
550
540
|
{...props}
|
|
551
541
|
>
|
|
552
542
|
<LayerPortalProvider value={nestedPortal}>
|
|
553
|
-
<
|
|
543
|
+
<FillBoundary>
|
|
554
544
|
<DialogContentContext.Provider
|
|
555
545
|
value={{ showCloseButton: showClose, closeDisabled, closeSize }}
|
|
556
546
|
>
|
|
557
547
|
{children}
|
|
558
548
|
</DialogContentContext.Provider>
|
|
559
|
-
</
|
|
549
|
+
</FillBoundary>
|
|
560
550
|
</LayerPortalProvider>
|
|
561
551
|
</DialogPrimitive.Content>
|
|
562
552
|
</DialogPortal>
|
|
@@ -566,16 +556,21 @@ export function DialogContent({
|
|
|
566
556
|
export function DialogHeader({
|
|
567
557
|
className,
|
|
568
558
|
children,
|
|
559
|
+
tabs,
|
|
569
560
|
...props
|
|
570
|
-
}: React.ComponentProps<"div">
|
|
561
|
+
}: React.ComponentProps<"div"> & {
|
|
562
|
+
tabs?: React.ReactNode;
|
|
563
|
+
}) {
|
|
571
564
|
const { mode } = useDialogContext();
|
|
572
|
-
const layout = useSurfaceLayout("dialog");
|
|
573
565
|
const { showCloseButton, closeDisabled, closeSize } =
|
|
574
566
|
React.useContext(DialogContentContext);
|
|
575
567
|
|
|
576
568
|
// O modo de alerta é a exceção declarada da ADR 0023: a resposta obrigatória empilha
|
|
577
569
|
// mídia, título e corpo centralizados, e o padding mora na raiz do conteúdo.
|
|
578
570
|
if (mode === "alert") {
|
|
571
|
+
if (tabs !== undefined && tabs !== null && tabs !== false) {
|
|
572
|
+
throw new Error("DialogHeader de alerta não aceita abas.");
|
|
573
|
+
}
|
|
579
574
|
return (
|
|
580
575
|
<div
|
|
581
576
|
data-slot="dialog-header"
|
|
@@ -593,6 +588,7 @@ export function DialogHeader({
|
|
|
593
588
|
return (
|
|
594
589
|
<DialogMetaContext.Provider value>
|
|
595
590
|
<StructuralHeader
|
|
591
|
+
tabs={tabs}
|
|
596
592
|
name="DialogHeader"
|
|
597
593
|
slot="dialog"
|
|
598
594
|
slots={{
|
|
@@ -604,11 +600,11 @@ export function DialogHeader({
|
|
|
604
600
|
actions: DialogActions,
|
|
605
601
|
}}
|
|
606
602
|
leadingPlacement="inline"
|
|
607
|
-
titleRequired
|
|
603
|
+
titleRequired
|
|
608
604
|
rowClassName={surfaceHeaderRowClasses("dialog")}
|
|
609
605
|
className={cn(
|
|
610
606
|
"text-left",
|
|
611
|
-
surfaceRegionClasses("dialog",
|
|
607
|
+
surfaceRegionClasses("dialog", "header"),
|
|
612
608
|
className,
|
|
613
609
|
)}
|
|
614
610
|
trailing={
|
|
@@ -652,36 +648,6 @@ export function DialogActions({
|
|
|
652
648
|
);
|
|
653
649
|
}
|
|
654
650
|
|
|
655
|
-
/** Título e ações da composição principal, como primeiro bloco de DialogBody. */
|
|
656
|
-
export function DialogIntro({
|
|
657
|
-
className,
|
|
658
|
-
children,
|
|
659
|
-
...props
|
|
660
|
-
}: React.ComponentProps<"div">) {
|
|
661
|
-
return (
|
|
662
|
-
<DialogIntroContext.Provider value>
|
|
663
|
-
<DialogMetaContext.Provider value>
|
|
664
|
-
<StructuralHeader
|
|
665
|
-
name="DialogIntro"
|
|
666
|
-
slot="dialog-intro"
|
|
667
|
-
slots={{
|
|
668
|
-
title: DialogTitle,
|
|
669
|
-
description: DialogDescription,
|
|
670
|
-
meta: DialogMeta,
|
|
671
|
-
actions: DialogIntroActions,
|
|
672
|
-
}}
|
|
673
|
-
leadingPlacement="inline"
|
|
674
|
-
titleRequired
|
|
675
|
-
className={cn("text-left", className)}
|
|
676
|
-
{...props}
|
|
677
|
-
>
|
|
678
|
-
{children}
|
|
679
|
-
</StructuralHeader>
|
|
680
|
-
</DialogMetaContext.Provider>
|
|
681
|
-
</DialogIntroContext.Provider>
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
651
|
/** Badges, datas e autoria abaixo do título e da orientação. */
|
|
686
652
|
export function DialogMeta({
|
|
687
653
|
className,
|
|
@@ -693,10 +659,7 @@ export function DialogMeta({
|
|
|
693
659
|
throw new Error(
|
|
694
660
|
'DialogMeta não está disponível em Dialog com mode="alert"; coloque as informações no corpo do alerta.',
|
|
695
661
|
);
|
|
696
|
-
if (!inside)
|
|
697
|
-
throw new Error(
|
|
698
|
-
"DialogMeta deve estar dentro de DialogHeader ou DialogIntro.",
|
|
699
|
-
);
|
|
662
|
+
if (!inside) throw new Error("DialogMeta deve estar dentro de DialogHeader.");
|
|
700
663
|
return (
|
|
701
664
|
<div
|
|
702
665
|
data-slot="dialog-meta"
|
|
@@ -706,20 +669,6 @@ export function DialogMeta({
|
|
|
706
669
|
);
|
|
707
670
|
}
|
|
708
671
|
|
|
709
|
-
/** Ações relacionadas ao título do conteúdo, separadas das ações de chrome do header. */
|
|
710
|
-
export function DialogIntroActions({
|
|
711
|
-
className,
|
|
712
|
-
...props
|
|
713
|
-
}: React.ComponentProps<"div">) {
|
|
714
|
-
return (
|
|
715
|
-
<div
|
|
716
|
-
data-slot="dialog-intro-actions"
|
|
717
|
-
className={cn(surfaceActionsClasses, className)}
|
|
718
|
-
{...props}
|
|
719
|
-
/>
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
672
|
export function DialogMedia({ className, ...props }: StructuralMediaProps) {
|
|
724
673
|
return (
|
|
725
674
|
<StructuralMedia slot="dialog-media" {...props} className={className} />
|
|
@@ -733,8 +682,6 @@ export function DialogBody({
|
|
|
733
682
|
...props
|
|
734
683
|
}: React.ComponentProps<"div"> & { flush?: boolean }) {
|
|
735
684
|
const { mode } = useDialogContext();
|
|
736
|
-
const layout = useSurfaceLayout("dialog");
|
|
737
|
-
validateSurfaceIntro(props.children, DialogIntro, "DialogBody");
|
|
738
685
|
return (
|
|
739
686
|
<div
|
|
740
687
|
data-slot="dialog-body"
|
|
@@ -743,7 +690,7 @@ export function DialogBody({
|
|
|
743
690
|
? "flex min-w-0 flex-col gap-6"
|
|
744
691
|
: cn(
|
|
745
692
|
"min-h-0 flex-1 overflow-y-auto",
|
|
746
|
-
surfaceRegionClasses("dialog",
|
|
693
|
+
surfaceRegionClasses("dialog", "body", { flush }),
|
|
747
694
|
),
|
|
748
695
|
className,
|
|
749
696
|
)}
|
|
@@ -759,7 +706,6 @@ export function DialogFooter({
|
|
|
759
706
|
...props
|
|
760
707
|
}: React.ComponentProps<"div">) {
|
|
761
708
|
const { mode } = useDialogContext();
|
|
762
|
-
const layout = useSurfaceLayout("dialog");
|
|
763
709
|
return (
|
|
764
710
|
<div
|
|
765
711
|
data-slot="dialog-footer"
|
|
@@ -768,7 +714,7 @@ export function DialogFooter({
|
|
|
768
714
|
? "w-full"
|
|
769
715
|
: cn(
|
|
770
716
|
"flex shrink-0 flex-row justify-end gap-2",
|
|
771
|
-
surfaceRegionClasses("dialog",
|
|
717
|
+
surfaceRegionClasses("dialog", "footer"),
|
|
772
718
|
),
|
|
773
719
|
className,
|
|
774
720
|
)}
|
|
@@ -787,19 +733,11 @@ export function DialogTitle({
|
|
|
787
733
|
level?: HeadingLevel;
|
|
788
734
|
}) {
|
|
789
735
|
const { mode } = useDialogContext();
|
|
790
|
-
const intro = React.useContext(DialogIntroContext);
|
|
791
|
-
const layout = useSurfaceLayout("dialog");
|
|
792
736
|
const Title =
|
|
793
737
|
mode === "alert" ? AlertDialogPrimitive.Title : DialogPrimitive.Title;
|
|
794
738
|
const classes = cn(
|
|
795
|
-
mode === "alert"
|
|
796
|
-
|
|
797
|
-
: intro
|
|
798
|
-
? "min-w-0 whitespace-normal"
|
|
799
|
-
: "min-w-0 truncate",
|
|
800
|
-
intro
|
|
801
|
-
? structuralHeaderClasses.page.title
|
|
802
|
-
: surfaceTitleClasses("dialog", layout),
|
|
739
|
+
mode === "alert" ? undefined : "min-w-0 truncate",
|
|
740
|
+
surfaceTitleClasses("dialog"),
|
|
803
741
|
className,
|
|
804
742
|
);
|
|
805
743
|
// O nível troca a TAG do mesmo nó, via `asChild` — nunca aninha um heading dentro
|
|
@@ -3,21 +3,17 @@ import { Dialog as DrawerPrimitive } from "radix-ui";
|
|
|
3
3
|
|
|
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";
|
|
12
|
+
import { FillBoundary } from "../patterns/fill-region.ts";
|
|
15
13
|
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
16
14
|
import {
|
|
17
15
|
StructuralHeader,
|
|
18
16
|
structuralMetaClasses,
|
|
19
|
-
structuralHeaderClasses,
|
|
20
|
-
validateSurfaceIntro,
|
|
21
17
|
} from "../patterns/structural-header.tsx";
|
|
22
18
|
import {
|
|
23
19
|
StructuralMedia,
|
|
@@ -48,11 +44,10 @@ interface DrawerContentContextValue {
|
|
|
48
44
|
const DrawerContentContext = React.createContext<DrawerContentContextValue>({
|
|
49
45
|
showCloseButton: false,
|
|
50
46
|
closeDisabled: false,
|
|
51
|
-
closeSize: "
|
|
47
|
+
closeSize: "xs",
|
|
52
48
|
});
|
|
53
49
|
|
|
54
50
|
const DrawerMetaContext = React.createContext(false);
|
|
55
|
-
const DrawerIntroContext = React.createContext(false);
|
|
56
51
|
|
|
57
52
|
/** Estado de aberto que a pilha de modais da camada flutuante acompanha (ADR 0052). */
|
|
58
53
|
const DrawerStateContext = React.createContext({ open: false, modal: true });
|
|
@@ -150,8 +145,7 @@ function DrawerContent({
|
|
|
150
145
|
side = "right",
|
|
151
146
|
showCloseButton = true,
|
|
152
147
|
closeDisabled = false,
|
|
153
|
-
closeSize = "
|
|
154
|
-
layout = "sectioned",
|
|
148
|
+
closeSize = "xs",
|
|
155
149
|
"aria-describedby": ariaDescribedBy,
|
|
156
150
|
onEscapeKeyDown,
|
|
157
151
|
onPointerDownOutside,
|
|
@@ -170,8 +164,6 @@ function DrawerContent({
|
|
|
170
164
|
*/
|
|
171
165
|
closeDisabled?: boolean;
|
|
172
166
|
closeSize?: CloseButtonSize;
|
|
173
|
-
/** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
174
|
-
layout?: SurfaceLayout;
|
|
175
167
|
}) {
|
|
176
168
|
const { open, modal } = React.useContext(DrawerStateContext);
|
|
177
169
|
const layer = useLayerPortal();
|
|
@@ -209,7 +201,6 @@ function DrawerContent({
|
|
|
209
201
|
<DrawerPrimitive.Content
|
|
210
202
|
ref={composedRef}
|
|
211
203
|
data-slot="drawer-content"
|
|
212
|
-
data-layout={layout}
|
|
213
204
|
{...{ [LAYER_MODAL_ATTRIBUTE]: internal || undefined }}
|
|
214
205
|
className={cn(
|
|
215
206
|
"fixed z-50 flex flex-col overflow-hidden rounded-xl border bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
|
@@ -237,13 +228,13 @@ function DrawerContent({
|
|
|
237
228
|
{...props}
|
|
238
229
|
>
|
|
239
230
|
<LayerPortalProvider value={nestedPortal}>
|
|
240
|
-
<
|
|
231
|
+
<FillBoundary>
|
|
241
232
|
<DrawerContentContext.Provider
|
|
242
233
|
value={{ showCloseButton, closeDisabled, closeSize }}
|
|
243
234
|
>
|
|
244
235
|
{children}
|
|
245
236
|
</DrawerContentContext.Provider>
|
|
246
|
-
</
|
|
237
|
+
</FillBoundary>
|
|
247
238
|
</LayerPortalProvider>
|
|
248
239
|
</DrawerPrimitive.Content>
|
|
249
240
|
</DrawerPortal>
|
|
@@ -253,14 +244,17 @@ function DrawerContent({
|
|
|
253
244
|
function DrawerHeader({
|
|
254
245
|
className,
|
|
255
246
|
children,
|
|
247
|
+
tabs,
|
|
256
248
|
...props
|
|
257
|
-
}: React.ComponentProps<"div">
|
|
258
|
-
|
|
249
|
+
}: React.ComponentProps<"div"> & {
|
|
250
|
+
tabs?: React.ReactNode;
|
|
251
|
+
}) {
|
|
259
252
|
const { showCloseButton, closeDisabled, closeSize } =
|
|
260
253
|
React.useContext(DrawerContentContext);
|
|
261
254
|
return (
|
|
262
255
|
<DrawerMetaContext.Provider value>
|
|
263
256
|
<StructuralHeader
|
|
257
|
+
tabs={tabs}
|
|
264
258
|
name="DrawerHeader"
|
|
265
259
|
slot="drawer"
|
|
266
260
|
slots={{
|
|
@@ -272,11 +266,11 @@ function DrawerHeader({
|
|
|
272
266
|
actions: DrawerActions,
|
|
273
267
|
}}
|
|
274
268
|
leadingPlacement="inline"
|
|
275
|
-
titleRequired
|
|
269
|
+
titleRequired
|
|
276
270
|
rowClassName={surfaceHeaderRowClasses("drawer")}
|
|
277
271
|
className={cn(
|
|
278
272
|
"text-left",
|
|
279
|
-
surfaceRegionClasses("drawer",
|
|
273
|
+
surfaceRegionClasses("drawer", "header"),
|
|
280
274
|
className,
|
|
281
275
|
)}
|
|
282
276
|
trailing={
|
|
@@ -322,46 +316,13 @@ function DrawerActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
322
316
|
);
|
|
323
317
|
}
|
|
324
318
|
|
|
325
|
-
/** Título e ações da composição principal, como primeiro bloco de DrawerBody. */
|
|
326
|
-
function DrawerIntro({
|
|
327
|
-
className,
|
|
328
|
-
children,
|
|
329
|
-
...props
|
|
330
|
-
}: React.ComponentProps<"div">) {
|
|
331
|
-
return (
|
|
332
|
-
<DrawerIntroContext.Provider value>
|
|
333
|
-
<DrawerMetaContext.Provider value>
|
|
334
|
-
<StructuralHeader
|
|
335
|
-
name="DrawerIntro"
|
|
336
|
-
slot="drawer-intro"
|
|
337
|
-
slots={{
|
|
338
|
-
title: DrawerTitle,
|
|
339
|
-
description: DrawerDescription,
|
|
340
|
-
meta: DrawerMeta,
|
|
341
|
-
actions: DrawerIntroActions,
|
|
342
|
-
}}
|
|
343
|
-
leadingPlacement="inline"
|
|
344
|
-
titleRequired
|
|
345
|
-
className={cn("text-left", className)}
|
|
346
|
-
{...props}
|
|
347
|
-
>
|
|
348
|
-
{children}
|
|
349
|
-
</StructuralHeader>
|
|
350
|
-
</DrawerMetaContext.Provider>
|
|
351
|
-
</DrawerIntroContext.Provider>
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
319
|
/** Badges, datas e autoria abaixo do título e da orientação. */
|
|
356
320
|
export function DrawerMeta({
|
|
357
321
|
className,
|
|
358
322
|
...props
|
|
359
323
|
}: React.ComponentProps<"div">) {
|
|
360
324
|
const inside = React.useContext(DrawerMetaContext);
|
|
361
|
-
if (!inside)
|
|
362
|
-
throw new Error(
|
|
363
|
-
"DrawerMeta deve estar dentro de DrawerHeader ou DrawerIntro.",
|
|
364
|
-
);
|
|
325
|
+
if (!inside) throw new Error("DrawerMeta deve estar dentro de DrawerHeader.");
|
|
365
326
|
return (
|
|
366
327
|
<div
|
|
367
328
|
data-slot="drawer-meta"
|
|
@@ -371,34 +332,18 @@ export function DrawerMeta({
|
|
|
371
332
|
);
|
|
372
333
|
}
|
|
373
334
|
|
|
374
|
-
/** Ações relacionadas ao título do conteúdo, separadas das ações de chrome do header. */
|
|
375
|
-
function DrawerIntroActions({
|
|
376
|
-
className,
|
|
377
|
-
...props
|
|
378
|
-
}: React.ComponentProps<"div">) {
|
|
379
|
-
return (
|
|
380
|
-
<div
|
|
381
|
-
data-slot="drawer-intro-actions"
|
|
382
|
-
className={cn(surfaceActionsClasses, className)}
|
|
383
|
-
{...props}
|
|
384
|
-
/>
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
335
|
function DrawerBody({
|
|
389
336
|
flush = false,
|
|
390
337
|
className,
|
|
391
338
|
style,
|
|
392
339
|
...props
|
|
393
340
|
}: React.ComponentProps<"div"> & { flush?: boolean }) {
|
|
394
|
-
const layout = useSurfaceLayout("drawer");
|
|
395
|
-
validateSurfaceIntro(props.children, DrawerIntro, "DrawerBody");
|
|
396
341
|
return (
|
|
397
342
|
<div
|
|
398
343
|
data-slot="drawer-body"
|
|
399
344
|
className={cn(
|
|
400
345
|
"min-h-0 flex-1 overflow-y-auto",
|
|
401
|
-
surfaceRegionClasses("drawer",
|
|
346
|
+
surfaceRegionClasses("drawer", "body", { flush }),
|
|
402
347
|
className,
|
|
403
348
|
)}
|
|
404
349
|
style={style}
|
|
@@ -409,7 +354,6 @@ function DrawerBody({
|
|
|
409
354
|
}
|
|
410
355
|
|
|
411
356
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
412
|
-
const layout = useSurfaceLayout("drawer");
|
|
413
357
|
return (
|
|
414
358
|
<div
|
|
415
359
|
data-slot="drawer-footer"
|
|
@@ -418,7 +362,7 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
418
362
|
// preenche a altura da janela, e sem isso o rodapé subiria atrás do corpo. No
|
|
419
363
|
// Dialog a altura é do conteúdo, então ele não precisa (ADR 0023).
|
|
420
364
|
"mt-auto flex shrink-0 flex-row justify-end gap-2",
|
|
421
|
-
surfaceRegionClasses("drawer",
|
|
365
|
+
surfaceRegionClasses("drawer", "footer"),
|
|
422
366
|
className,
|
|
423
367
|
)}
|
|
424
368
|
{...props}
|
|
@@ -435,13 +379,9 @@ function DrawerTitle({
|
|
|
435
379
|
/** Hierarquia do documento. Sem ele vale o `h2` que a primitiva emite. */
|
|
436
380
|
level?: HeadingLevel;
|
|
437
381
|
}) {
|
|
438
|
-
const intro = React.useContext(DrawerIntroContext);
|
|
439
|
-
const layout = useSurfaceLayout("drawer");
|
|
440
382
|
const classes = cn(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
? structuralHeaderClasses.page.title
|
|
444
|
-
: surfaceTitleClasses("drawer", layout),
|
|
383
|
+
"min-w-0 truncate",
|
|
384
|
+
surfaceTitleClasses("drawer"),
|
|
445
385
|
className,
|
|
446
386
|
);
|
|
447
387
|
// O nível troca a TAG do mesmo nó, via `asChild` — a escala continua vindo do layout.
|
|
@@ -468,8 +408,6 @@ function DrawerTitle({
|
|
|
468
408
|
|
|
469
409
|
export {
|
|
470
410
|
DrawerActions,
|
|
471
|
-
DrawerIntro,
|
|
472
|
-
DrawerIntroActions,
|
|
473
411
|
DrawerMedia,
|
|
474
412
|
DrawerNavigation,
|
|
475
413
|
Drawer,
|
|
@@ -184,7 +184,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
|
184
184
|
<p
|
|
185
185
|
data-slot="field-description"
|
|
186
186
|
className={cn(
|
|
187
|
-
"text-
|
|
187
|
+
"text-xs leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
|
|
188
188
|
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
|
189
189
|
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
190
190
|
className
|
|
@@ -32,7 +32,7 @@ function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface PaginationLinkProps extends Omit<React.ComponentProps<'a'>, 'children'> {
|
|
35
|
-
/** Página atual:
|
|
35
|
+
/** Página atual: usa fundo `subtle` e ganha `aria-current="page"`. */
|
|
36
36
|
isActive?: boolean
|
|
37
37
|
/** A escala de control.ts, via Button. `default` (números: altura fixa, largura mínima
|
|
38
38
|
* quadrada que cresce com os dígitos) ou um `icon*` (setas quadradas, sem padding). */
|
|
@@ -59,7 +59,8 @@ function PaginationLink({ className, isActive, page, disabled, href, size = 'def
|
|
|
59
59
|
'data-slot': 'pagination-link',
|
|
60
60
|
'data-active': isActive,
|
|
61
61
|
className: cn(
|
|
62
|
-
buttonVariants({ variant: isActive ? '
|
|
62
|
+
buttonVariants({ context: 'neutral', variant: isActive ? 'subtle' : 'ghost', size }),
|
|
63
|
+
isActive && 'font-semibold',
|
|
63
64
|
// Número: a largura mínima é quadrada e o conteúdo manda no resto. Escala `icon*`
|
|
64
65
|
// fica quadrada de verdade (sem `has-[>svg]:px-*` do `default`).
|
|
65
66
|
!square && cn('w-auto px-2 tabular-nums', minWidth),
|
|
@@ -95,7 +95,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
|
95
95
|
<th
|
|
96
96
|
data-slot="table-head"
|
|
97
97
|
className={cn(
|
|
98
|
-
"h-10 px-3 text-left align-middle font-medium whitespace-normal text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
|
|
98
|
+
"h-10 px-3 py-2 text-left align-middle font-medium whitespace-normal text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
|
|
99
99
|
className,
|
|
100
100
|
)}
|
|
101
101
|
{...props}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
structuralRegionGap,
|
|
8
8
|
} from "../patterns/surface-anatomy.ts";
|
|
9
9
|
import { controlHeight, type ControlSize } from "./control.ts";
|
|
10
|
+
import { useFillRegion, fillRegionClassName } from "../patterns/fill-region.ts";
|
|
10
11
|
|
|
11
12
|
// Divergência da casa (ejetado): o Tabs ganhou `size` — Button e Select têm `sm`, o Tabs
|
|
12
13
|
// não tinha, e uma fileira densa ficava com as abas 0.25rem mais altas
|
|
@@ -18,6 +19,9 @@ const TabsSizeContext = React.createContext<TabsSize>("default");
|
|
|
18
19
|
const TabsDistributionContext =
|
|
19
20
|
React.createContext<TabsDistribution>("content");
|
|
20
21
|
const TabsFramedContext = React.createContext(false);
|
|
22
|
+
|
|
23
|
+
/** Uso interno: o header estrutural fornece o divisor da faixa de abas. */
|
|
24
|
+
export const TabsHeaderContext = React.createContext(false);
|
|
21
25
|
type TabsOrientation = "horizontal" | "vertical";
|
|
22
26
|
const TabsOrientationContext =
|
|
23
27
|
React.createContext<TabsOrientation>("horizontal");
|
|
@@ -70,6 +74,7 @@ function TabsList({
|
|
|
70
74
|
style,
|
|
71
75
|
...props
|
|
72
76
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
77
|
+
const inHeader = React.useContext(TabsHeaderContext);
|
|
73
78
|
// O size vem do <Tabs> (contexto) — a altura mora aqui, na list.
|
|
74
79
|
const size = React.useContext(TabsSizeContext);
|
|
75
80
|
const orientation = React.useContext(TabsOrientationContext);
|
|
@@ -101,7 +106,7 @@ function TabsList({
|
|
|
101
106
|
}}
|
|
102
107
|
{...props}
|
|
103
108
|
>
|
|
104
|
-
{hasHorizontalLine && (
|
|
109
|
+
{hasHorizontalLine && !inHeader && (
|
|
105
110
|
<span
|
|
106
111
|
aria-hidden="true"
|
|
107
112
|
data-slot="tabs-line"
|
|
@@ -196,21 +201,27 @@ function TabsListItem({
|
|
|
196
201
|
|
|
197
202
|
function TabsPanel({
|
|
198
203
|
className,
|
|
204
|
+
children,
|
|
199
205
|
...props
|
|
200
206
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
201
207
|
const framed = React.useContext(TabsFramedContext);
|
|
202
208
|
const orientation = React.useContext(TabsOrientationContext);
|
|
209
|
+
const fill = useFillRegion();
|
|
203
210
|
return (
|
|
204
211
|
<TabsPrimitive.Content
|
|
205
212
|
data-slot="tabs-panel"
|
|
213
|
+
{...fill.attributes}
|
|
206
214
|
className={cn(
|
|
207
215
|
"flex-1 outline-none data-[state=inactive]:hidden",
|
|
208
216
|
structuralBodyRhythm,
|
|
217
|
+
fill.filling && fillRegionClassName,
|
|
209
218
|
framed && orientation === "horizontal" && "px-4 pb-4",
|
|
210
219
|
className,
|
|
211
220
|
)}
|
|
212
221
|
{...props}
|
|
213
|
-
|
|
222
|
+
>
|
|
223
|
+
{fill.provide(children)}
|
|
224
|
+
</TabsPrimitive.Content>
|
|
214
225
|
);
|
|
215
226
|
}
|
|
216
227
|
|
|
@@ -122,7 +122,12 @@ referência; itens estáticos permanecem como declarados no contrato.
|
|
|
122
122
|
|
|
123
123
|
## Widgets declarativos
|
|
124
124
|
|
|
125
|
-
`FieldSpec.widget` aceita `textarea`, `code`, `lines`, `refItems`, `icon` e `toggle-group`.
|
|
125
|
+
`FieldSpec.widget` aceita `date`, `password`, `textarea`, `code`, `lines`, `refItems`, `icon` e `toggle-group`.
|
|
126
|
+
Para uma data civil, declare `widget: 'date'` e use uma string `YYYY-MM-DD` no schema e no
|
|
127
|
+
valor inicial. O formulário usa o [`DateInput`](/ui/date-input), com digitação em `DD/MM/AAAA`
|
|
128
|
+
e calendário em pt-BR. O valor válido enviado continua sendo a data civil, sem conversão para UTC.
|
|
129
|
+
Texto incompleto ou inválido chega ao schema para validação; use um schema de data civil.
|
|
130
|
+
`password` usa um input de senha com `autocomplete="new-password"`, adequado a cadastro e redefinição.
|
|
126
131
|
O literal canônico do grupo de escolhas é sempre kebab-case: `widget: 'toggle-group'`. Em enum ele
|
|
127
132
|
renderiza seleção única; em array de enum, seleção múltipla. As opções seguem a mesma precedência e
|
|
128
133
|
o mesmo `SelectOption` rico dos selects. Use esse widget quando cada opção funcionar como uma
|