@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cabeçalho estrutural (interno): a anatomia que Page, Surface, Dialog
|
|
2
|
+
* Cabeçalho estrutural (interno): a anatomia que Page, Surface, Dialog e Drawer
|
|
3
3
|
* compartilham. O nome público `SurfaceHeader` pertence à família Surface (ADR 0045).
|
|
4
4
|
*
|
|
5
5
|
* Um título, metadata opcional e uma região de ações que vai para o extremo oposto
|
|
6
|
-
*
|
|
6
|
+
* da linha. Cada família escolhe quais slots
|
|
7
7
|
* expõe; layout, validação e classes vivem aqui, uma vez.
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
type ReactElement,
|
|
17
17
|
type ReactNode,
|
|
18
18
|
} from "react";
|
|
19
|
+
import { structuralTitleClasses } from "./surface-anatomy.ts";
|
|
19
20
|
import { cn } from "../../lib/cn.ts";
|
|
21
|
+
import { TabsHeaderContext } from "../primitives/tabs.tsx";
|
|
20
22
|
|
|
21
23
|
/** Metadados contextuais, separados da orientação textual e da linha de ações. */
|
|
22
24
|
export const structuralMetaClasses =
|
|
@@ -30,10 +32,10 @@ export const structuralHeaderClasses: Record<
|
|
|
30
32
|
{ title: string; description: string; count: string; actions: string }
|
|
31
33
|
> = {
|
|
32
34
|
page: {
|
|
33
|
-
title:
|
|
35
|
+
title: structuralTitleClasses,
|
|
34
36
|
description: "mt-1 truncate text-sm text-muted-foreground",
|
|
35
37
|
count: "font-mono text-base text-muted-foreground/60",
|
|
36
|
-
actions: "flex w-auto shrink-0 items-center gap-
|
|
38
|
+
actions: "flex w-auto shrink-0 items-center gap-2",
|
|
37
39
|
},
|
|
38
40
|
section: {
|
|
39
41
|
title: "text-sm font-semibold",
|
|
@@ -73,6 +75,8 @@ export interface StructuralHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
73
75
|
titleRequired?: boolean;
|
|
74
76
|
/** Classes da linha `inline` — as famílias com anatomia própria trazem a sua (ADR 0023). */
|
|
75
77
|
rowClassName?: string;
|
|
78
|
+
/** Lista de abas sob o título; o Tabs ancestral governa seleção e painéis. */
|
|
79
|
+
tabs?: ReactNode;
|
|
76
80
|
/** Nó interno acrescentado ao fim da linha, depois das ações (o botão de fechar). */
|
|
77
81
|
trailing?: ReactNode;
|
|
78
82
|
/**
|
|
@@ -108,12 +112,18 @@ export function StructuralHeader({
|
|
|
108
112
|
titleRequired = true,
|
|
109
113
|
rowClassName,
|
|
110
114
|
trailing,
|
|
115
|
+
tabs,
|
|
111
116
|
allowFreeContent = false,
|
|
112
117
|
invalidMessage,
|
|
113
118
|
className,
|
|
114
119
|
children,
|
|
115
120
|
...props
|
|
116
121
|
}: StructuralHeaderProps): ReactElement {
|
|
122
|
+
const hasTabs = tabs !== undefined && tabs !== null && tabs !== false;
|
|
123
|
+
|
|
124
|
+
if (hasTabs && Root === "legend") {
|
|
125
|
+
throw new Error("SurfaceHeader de fieldset não aceita abas.");
|
|
126
|
+
}
|
|
117
127
|
const Wrapper = Root === "legend" ? "span" : "div";
|
|
118
128
|
const nodes = flatten(children).filter(
|
|
119
129
|
(node) => typeof node !== "string" || node.trim() !== "",
|
|
@@ -281,8 +291,7 @@ export function StructuralHeader({
|
|
|
281
291
|
{inlineContext === null &&
|
|
282
292
|
leadingPlacement === "inline" &&
|
|
283
293
|
leading.length > 0 && (
|
|
284
|
-
//
|
|
285
|
-
// wrapper assume o min-content do breadcrumb e a compressão toda cai sobre o título.
|
|
294
|
+
// A região de navegação pode ceder espaço sem comprimir o título além do container.
|
|
286
295
|
<Wrapper
|
|
287
296
|
data-slot={`${slot}-navigation`}
|
|
288
297
|
className={cn("flex min-w-0 items-center", !heading && "flex-1")}
|
|
@@ -367,7 +376,8 @@ export function StructuralHeader({
|
|
|
367
376
|
<Root
|
|
368
377
|
data-slot={`${slot}-header`}
|
|
369
378
|
className={cn(
|
|
370
|
-
|
|
379
|
+
!hasTabs &&
|
|
380
|
+
metas.length === 0 &&
|
|
371
381
|
contentClassName === undefined &&
|
|
372
382
|
leadingPlacement !== "inline" &&
|
|
373
383
|
leading.length === 0 &&
|
|
@@ -377,6 +387,7 @@ export function StructuralHeader({
|
|
|
377
387
|
heading ? "justify-between" : "justify-end",
|
|
378
388
|
),
|
|
379
389
|
className,
|
|
390
|
+
hasTabs && "block border-b pb-0",
|
|
380
391
|
)}
|
|
381
392
|
{...props}
|
|
382
393
|
>
|
|
@@ -390,31 +401,11 @@ export function StructuralHeader({
|
|
|
390
401
|
{withMeta}
|
|
391
402
|
</Wrapper>
|
|
392
403
|
)}
|
|
404
|
+
{hasTabs && (
|
|
405
|
+
<div data-slot={`${slot}-header-tabs`} className="mt-4 min-w-0">
|
|
406
|
+
<TabsHeaderContext.Provider value>{tabs}</TabsHeaderContext.Provider>
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
393
409
|
</Root>
|
|
394
410
|
);
|
|
395
411
|
}
|
|
396
|
-
|
|
397
|
-
/** A intro visível abre o corpo; wrappers opacos são verificados quando materializam seus slots. */
|
|
398
|
-
export function validateSurfaceIntro(
|
|
399
|
-
children: ReactNode,
|
|
400
|
-
intro: ElementType,
|
|
401
|
-
name: string,
|
|
402
|
-
): void {
|
|
403
|
-
const flatten = (nodes: ReactNode): ReactNode[] =>
|
|
404
|
-
Children.toArray(nodes).flatMap((node) =>
|
|
405
|
-
isValidElement(node) && node.type === Fragment
|
|
406
|
-
? flatten((node.props as { children?: ReactNode }).children)
|
|
407
|
-
: [node],
|
|
408
|
-
);
|
|
409
|
-
const nodes = flatten(children).filter(
|
|
410
|
-
(node) => typeof node !== "string" || node.trim() !== "",
|
|
411
|
-
);
|
|
412
|
-
const indexes = nodes.flatMap((node, index) =>
|
|
413
|
-
isValidElement(node) && node.type === intro ? [index] : [],
|
|
414
|
-
);
|
|
415
|
-
if (indexes.length > 1 || (indexes.length === 1 && indexes[0] !== 0)) {
|
|
416
|
-
throw new Error(
|
|
417
|
-
`${name} aceita uma única intro como primeiro bloco do corpo.`,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
@@ -1,43 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anatomia de superfície (interno): as medidas que Surface, Dialog e Drawer
|
|
3
|
-
* compartilham — ADRs 0023, 0045 e 0047.
|
|
4
|
-
*
|
|
5
|
-
* Aqui mora o que antes era escrito seis vezes: recuo de cada região, ritmo vertical,
|
|
6
|
-
* escala do título, posição das ações e a regra dos divisores. As famílias públicas
|
|
7
|
-
* consomem estas tabelas; nenhuma reescreve as classes.
|
|
8
|
-
*
|
|
9
|
-
* Dois layouts, e cada um é um pacote indivisível:
|
|
10
|
-
*
|
|
11
|
-
* - `continuous` — sem divisores. O ritmo separa: o header se aproxima do corpo e a
|
|
12
|
-
* última região — seja ela o header, o body ou o footer — fecha o espaço de baixo com
|
|
13
|
-
* o mesmo respiro do topo. É a superfície de leitura.
|
|
14
|
-
* - `sectioned` — regiões simétricas, com divisor entre as que forem adjacentes. É o
|
|
15
|
-
* modal, o drawer e o painel.
|
|
16
|
-
*
|
|
17
|
-
* O recuo lateral é o mesmo nas três regiões da mesma superfície; é isso que a suíte
|
|
18
|
-
* transversal verifica.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
createContext,
|
|
23
|
-
createElement,
|
|
24
|
-
useContext,
|
|
25
|
-
type ReactElement,
|
|
26
|
-
type ReactNode,
|
|
27
|
-
} from "react";
|
|
28
|
-
import { FillBoundary } from "./fill-region.ts";
|
|
29
|
-
|
|
1
|
+
/** Anatomia única de Page, Surface, Dialog e Drawer — ADR 0060. */
|
|
30
2
|
export type SurfaceFamily = "surface" | "dialog" | "drawer" | "page";
|
|
31
|
-
export type SurfaceLayout = "continuous" | "sectioned";
|
|
32
3
|
export type SurfaceRegion = "header" | "body" | "footer";
|
|
33
4
|
|
|
34
|
-
/**
|
|
35
|
-
* Divisores: a região desenha a própria linha quando existe uma vizinha do outro lado.
|
|
36
|
-
* Um header sozinho não fecha nada; um footer que é a única região também não.
|
|
37
|
-
*
|
|
38
|
-
* O header suprime a própria linha quando o vizinho imediato é um footer — os dois
|
|
39
|
-
* juntos desenhariam duas bordas encostadas, uma sobre a outra.
|
|
40
|
-
*/
|
|
41
5
|
const headerDivider = [
|
|
42
6
|
"[&:not(:last-child)]:border-b",
|
|
43
7
|
"[&:not(:last-child)]:border-border",
|
|
@@ -49,238 +13,84 @@ const headerDivider = [
|
|
|
49
13
|
const footerDivider =
|
|
50
14
|
"[&:not(:first-child)]:border-t [&:not(:first-child)]:border-border";
|
|
51
15
|
|
|
52
|
-
interface RegionAnatomy {
|
|
53
|
-
header: string;
|
|
54
|
-
body: string;
|
|
55
|
-
footer: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
16
|
interface FamilyAnatomy {
|
|
59
|
-
|
|
60
|
-
regions: Record<SurfaceLayout, RegionAnatomy>;
|
|
61
|
-
/** Escala visual do título. O nível semântico do heading é escolhido à parte. */
|
|
62
|
-
title: Record<SurfaceLayout, string>;
|
|
63
|
-
/** Descrição no header, quando a família a oferece (ADR 0059 inclui subheadings opcionais em modais). */
|
|
17
|
+
title: string;
|
|
64
18
|
description: string;
|
|
65
|
-
/** Espaço entre mídia, título e ações na linha do header. */
|
|
66
19
|
headerGap: string;
|
|
67
|
-
/** Altura mínima da linha, equivalente ao controle padrão da família. */
|
|
68
|
-
headerMinHeight: string;
|
|
69
|
-
/** Faixa do rodapé no layout seccionado, quando a família declara uma. */
|
|
70
20
|
footerBand: string;
|
|
71
21
|
}
|
|
72
22
|
|
|
73
|
-
|
|
74
|
-
* O recuo seccionado de Dialog e Drawer: 1rem, a densidade das superfícies de operação.
|
|
75
|
-
* O header é comum à família; corpos e rodapés conservam o ritmo de cada superfície.
|
|
76
|
-
*/
|
|
77
|
-
const structuralHeaderPadding = "p-4";
|
|
78
|
-
const structuralHeaderMinHeight = "min-h-8";
|
|
23
|
+
export const structuralTitleClasses = "text-lg font-semibold leading-tight";
|
|
79
24
|
|
|
80
|
-
const sectionedRegions: RegionAnatomy = {
|
|
81
|
-
header: structuralHeaderPadding,
|
|
82
|
-
body: "p-4",
|
|
83
|
-
footer: "p-4",
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Ritmo entre blocos diretos do conteúdo; relações internas pertencem a um wrapper filho.
|
|
88
|
-
* Os blocos não encolhem: o body é quem rola quando a família tem altura limitada.
|
|
89
|
-
*/
|
|
90
25
|
export const structuralBodyRhythm = "flex flex-col gap-4 *:shrink-0";
|
|
91
26
|
export const structuralRegionSpacing = "space-y-4";
|
|
92
27
|
export const structuralRegionGap = "gap-4";
|
|
93
28
|
|
|
94
29
|
export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
95
30
|
page: {
|
|
96
|
-
|
|
97
|
-
continuous: {
|
|
98
|
-
header: structuralHeaderPadding,
|
|
99
|
-
body: "px-8 pt-0 pb-0 [[data-slot=page-body]:first-child>&]:pt-4",
|
|
100
|
-
footer: "px-8 py-4",
|
|
101
|
-
},
|
|
102
|
-
sectioned: {
|
|
103
|
-
header: sectionedRegions.header,
|
|
104
|
-
body: "px-8 pt-4 pb-0 [[data-slot=page-body]:has(+[data-slot=page-footer])>&]:pb-4",
|
|
105
|
-
footer: "px-8 py-4",
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
title: {
|
|
109
|
-
continuous: "text-2xl font-semibold",
|
|
110
|
-
sectioned: "text-2xl font-semibold",
|
|
111
|
-
},
|
|
31
|
+
title: structuralTitleClasses,
|
|
112
32
|
description: "text-sm text-muted-foreground",
|
|
113
33
|
headerGap: "gap-4",
|
|
114
|
-
headerMinHeight: structuralHeaderMinHeight,
|
|
115
34
|
footerBand: "bg-muted/30",
|
|
116
35
|
},
|
|
117
|
-
// Medidas da Surface delimitada (com moldura ou elevação). Sem delimitação, as regiões
|
|
118
|
-
// não recebem recuo: a superfície flui alinhada ao hospedeiro (ADR 0045).
|
|
119
36
|
surface: {
|
|
120
|
-
|
|
121
|
-
// No contínuo, a região anterior fornece o intervalo; a primeira abre o recuo.
|
|
122
|
-
continuous: {
|
|
123
|
-
header: structuralHeaderPadding,
|
|
124
|
-
body: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
125
|
-
footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
126
|
-
},
|
|
127
|
-
sectioned: {
|
|
128
|
-
header: structuralHeaderPadding,
|
|
129
|
-
body: "p-4",
|
|
130
|
-
footer: "p-4",
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
// Uma escala só: o nível 1 troca a apresentação inteira (ADR 0038), nunca o layout.
|
|
134
|
-
title: {
|
|
135
|
-
continuous: "text-sm font-semibold",
|
|
136
|
-
sectioned: "text-sm font-semibold",
|
|
137
|
-
},
|
|
37
|
+
title: "text-sm font-semibold",
|
|
138
38
|
description: "mt-0.5 text-xs text-muted-foreground",
|
|
139
39
|
headerGap: "gap-2",
|
|
140
|
-
headerMinHeight: structuralHeaderMinHeight,
|
|
141
40
|
footerBand: "",
|
|
142
41
|
},
|
|
143
42
|
dialog: {
|
|
144
|
-
|
|
145
|
-
continuous: {
|
|
146
|
-
header: structuralHeaderPadding,
|
|
147
|
-
body: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
148
|
-
footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
149
|
-
},
|
|
150
|
-
sectioned: sectionedRegions,
|
|
151
|
-
},
|
|
152
|
-
title: {
|
|
153
|
-
continuous: "text-lg font-semibold leading-none",
|
|
154
|
-
sectioned: "text-lg font-semibold leading-none",
|
|
155
|
-
},
|
|
43
|
+
title: structuralTitleClasses,
|
|
156
44
|
description: "text-sm text-muted-foreground",
|
|
157
45
|
headerGap: "gap-4",
|
|
158
|
-
headerMinHeight: structuralHeaderMinHeight,
|
|
159
46
|
footerBand: "bg-muted/20 dark:bg-input/20",
|
|
160
47
|
},
|
|
161
48
|
drawer: {
|
|
162
|
-
|
|
163
|
-
continuous: {
|
|
164
|
-
header: structuralHeaderPadding,
|
|
165
|
-
body: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
166
|
-
footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
167
|
-
},
|
|
168
|
-
sectioned: sectionedRegions,
|
|
169
|
-
},
|
|
170
|
-
title: {
|
|
171
|
-
continuous: "text-lg font-semibold leading-none",
|
|
172
|
-
sectioned: "text-lg font-semibold leading-none",
|
|
173
|
-
},
|
|
49
|
+
title: structuralTitleClasses,
|
|
174
50
|
description: "text-sm text-muted-foreground",
|
|
175
51
|
headerGap: "gap-4",
|
|
176
|
-
headerMinHeight: structuralHeaderMinHeight,
|
|
177
52
|
footerBand: "bg-muted/20 dark:bg-input/20",
|
|
178
53
|
},
|
|
179
54
|
};
|
|
180
55
|
|
|
181
|
-
/**
|
|
56
|
+
/** Recuo da região e divisor automático quando existe uma região vizinha. */
|
|
182
57
|
export function surfaceRegionClasses(
|
|
183
58
|
family: SurfaceFamily,
|
|
184
|
-
layout: SurfaceLayout,
|
|
185
59
|
region: SurfaceRegion,
|
|
186
|
-
{
|
|
60
|
+
{
|
|
61
|
+
flush = false,
|
|
62
|
+
bounded = true,
|
|
63
|
+
}: { flush?: boolean; bounded?: boolean } = {},
|
|
187
64
|
): string {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
region === "
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return [regionClasses, footerDivider, anatomy.footerBand]
|
|
199
|
-
.filter(Boolean)
|
|
200
|
-
.join(" ");
|
|
201
|
-
return regionClasses;
|
|
65
|
+
const padding = bounded && !(region === "body" && flush) ? "p-4" : "";
|
|
66
|
+
return [
|
|
67
|
+
padding,
|
|
68
|
+
region === "body" && structuralBodyRhythm,
|
|
69
|
+
bounded && region === "header" && headerDivider,
|
|
70
|
+
bounded && region === "footer" && footerDivider,
|
|
71
|
+
region === "footer" && bounded && surfaceAnatomy[family].footerBand,
|
|
72
|
+
]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join(" ");
|
|
202
75
|
}
|
|
203
76
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
family: SurfaceFamily,
|
|
207
|
-
layout: SurfaceLayout,
|
|
208
|
-
): string {
|
|
209
|
-
return surfaceAnatomy[family].title[layout];
|
|
77
|
+
export function surfaceTitleClasses(family: SurfaceFamily): string {
|
|
78
|
+
return surfaceAnatomy[family].title;
|
|
210
79
|
}
|
|
211
80
|
|
|
212
81
|
export function surfaceDescriptionClasses(family: SurfaceFamily): string {
|
|
213
82
|
return surfaceAnatomy[family].description;
|
|
214
83
|
}
|
|
215
84
|
|
|
216
|
-
/**
|
|
217
|
-
* A linha do header: mídia, heading que absorve a folga, ações no extremo oposto.
|
|
218
|
-
*
|
|
219
|
-
* `flex-1` mantém o heading como região elástica e as ações no extremo oposto.
|
|
220
|
-
*/
|
|
85
|
+
/** Reserva a altura de um botão normal também quando o header não possui ações. */
|
|
221
86
|
export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
|
|
222
|
-
return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap}
|
|
87
|
+
return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap} min-h-9`;
|
|
223
88
|
}
|
|
224
89
|
|
|
225
|
-
/** Ações do header e do footer: fora do fluxo de compressão, na escala densa. */
|
|
226
90
|
export const surfaceActionsClasses = "flex shrink-0 items-center gap-2";
|
|
227
91
|
|
|
228
|
-
/**
|
|
229
|
-
* A `legend` de uma Surface `fieldset` delimitada (ADR 0045).
|
|
230
|
-
*
|
|
231
|
-
* - `notch`: moldura contínua. A legend nativa ocupa a borda, afastada do canto pelo
|
|
232
|
-
* mesmo recuo lateral das regiões; a região seguinte recebe o intervalo que a legend
|
|
233
|
-
* não tem como declarar.
|
|
234
|
-
* - `band`: a legend é a faixa do header dentro da superfície. Uma legend só ocupa a
|
|
235
|
-
* largura inteira fora do fluxo de borda, e a região seguinte precisa limpar o float.
|
|
236
|
-
*/
|
|
92
|
+
/** A legend delimitada ocupa uma faixa inteira; a próxima região limpa o float. */
|
|
237
93
|
export const surfaceLegendClasses = {
|
|
238
|
-
notch: "ml-2 w-auto px-2",
|
|
239
|
-
afterNotch: "pt-4",
|
|
240
94
|
band: "float-left w-full",
|
|
241
95
|
afterBand: "clear-left",
|
|
242
96
|
} as const;
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* O layout é do root e chega aos slots por contexto — ADR 0023. Slot não recebe
|
|
246
|
-
* `layout`: o divisor depende de quem está do outro lado, então a decisão precisa valer
|
|
247
|
-
* para a superfície inteira.
|
|
248
|
-
*
|
|
249
|
-
* O default por família preserva o que ela já entregava antes desta ADR.
|
|
250
|
-
*/
|
|
251
|
-
const SurfaceLayoutContext = createContext<SurfaceLayout | null>(null);
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Publica o layout da superfície para os slots. A superfície também interrompe a
|
|
255
|
-
* propagação de `layout="fill"` da página (ADR 0024): a altura do seu corpo é decisão dela,
|
|
256
|
-
* inclusive quando um portal a renderiza dentro de uma página `fill`.
|
|
257
|
-
*/
|
|
258
|
-
export function SurfaceLayoutProvider({
|
|
259
|
-
value,
|
|
260
|
-
children,
|
|
261
|
-
}: {
|
|
262
|
-
value: SurfaceLayout;
|
|
263
|
-
children?: ReactNode;
|
|
264
|
-
}): ReactElement {
|
|
265
|
-
return createElement(
|
|
266
|
-
SurfaceLayoutContext.Provider,
|
|
267
|
-
{ value },
|
|
268
|
-
createElement(FillBoundary, null, children),
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const familyDefaultLayout: Record<SurfaceFamily, SurfaceLayout> = {
|
|
273
|
-
page: "sectioned",
|
|
274
|
-
surface: "continuous",
|
|
275
|
-
dialog: "sectioned",
|
|
276
|
-
drawer: "sectioned",
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
/** O layout vigente: o declarado pelo root da superfície, ou o default da família. */
|
|
280
|
-
export function useSurfaceLayout(family: SurfaceFamily): SurfaceLayout {
|
|
281
|
-
return useContext(SurfaceLayoutContext) ?? familyDefaultLayout[family];
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export function defaultSurfaceLayout(family: SurfaceFamily): SurfaceLayout {
|
|
285
|
-
return familyDefaultLayout[family];
|
|
286
|
-
}
|
|
@@ -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
|
*
|
|
@@ -52,7 +51,6 @@ import {
|
|
|
52
51
|
surfaceRegionClasses,
|
|
53
52
|
structuralRegionSpacing,
|
|
54
53
|
surfaceTitleClasses,
|
|
55
|
-
type SurfaceLayout,
|
|
56
54
|
} from "./surface-anatomy.ts";
|
|
57
55
|
|
|
58
56
|
export type SurfaceElement = "div" | "section" | "article" | "fieldset";
|
|
@@ -64,7 +62,6 @@ interface SurfaceContextValue {
|
|
|
64
62
|
/** Tem moldura ou elevação: as regiões carregam o recuo. */
|
|
65
63
|
bounded: boolean;
|
|
66
64
|
framed: boolean;
|
|
67
|
-
layout: SurfaceLayout;
|
|
68
65
|
level: HeadingLevel;
|
|
69
66
|
titleId: string;
|
|
70
67
|
variant: "page" | "section";
|
|
@@ -84,11 +81,9 @@ function requiredContext<T>(
|
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
/** Como a `legend` de um fieldset delimitado se relaciona com a moldura. */
|
|
87
|
-
function legendPlacement(
|
|
88
|
-
surface: SurfaceContextValue,
|
|
89
|
-
): "notch" | "band" | null {
|
|
84
|
+
function legendPlacement(surface: SurfaceContextValue): "band" | null {
|
|
90
85
|
if (surface.element !== "fieldset" || !surface.bounded) return null;
|
|
91
|
-
return
|
|
86
|
+
return "band";
|
|
92
87
|
}
|
|
93
88
|
|
|
94
89
|
interface SurfaceBaseProps {
|
|
@@ -99,8 +94,6 @@ interface SurfaceBaseProps {
|
|
|
99
94
|
* o fundo de quem hospeda.
|
|
100
95
|
*/
|
|
101
96
|
elevation?: SurfaceElevation;
|
|
102
|
-
/** Anatomia da superfície inteira: `continuous` (default) ou `sectioned`. */
|
|
103
|
-
layout?: SurfaceLayout;
|
|
104
97
|
className?: string;
|
|
105
98
|
children?: ReactNode;
|
|
106
99
|
}
|
|
@@ -203,7 +196,6 @@ export function Surface({
|
|
|
203
196
|
asChild = false,
|
|
204
197
|
framed = false,
|
|
205
198
|
elevation = "none",
|
|
206
|
-
layout = "continuous",
|
|
207
199
|
level = 2,
|
|
208
200
|
title,
|
|
209
201
|
media,
|
|
@@ -224,11 +216,6 @@ export function Surface({
|
|
|
224
216
|
const element: SurfaceElement | null = asChild ? null : (as ?? "div");
|
|
225
217
|
const raised = elevation === "raised";
|
|
226
218
|
const bounded = framed || raised;
|
|
227
|
-
if (layout === "sectioned" && !bounded) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
'Surface layout="sectioned" exige framed ou elevation="raised".',
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
219
|
|
|
233
220
|
const shorthand = title !== undefined;
|
|
234
221
|
if (
|
|
@@ -299,7 +286,6 @@ export function Surface({
|
|
|
299
286
|
element,
|
|
300
287
|
bounded,
|
|
301
288
|
framed,
|
|
302
|
-
layout,
|
|
303
289
|
level,
|
|
304
290
|
titleId: resolvedTitleId,
|
|
305
291
|
variant,
|
|
@@ -342,13 +328,12 @@ export function Surface({
|
|
|
342
328
|
data-as={element ?? undefined}
|
|
343
329
|
data-framed={framed}
|
|
344
330
|
data-elevation={elevation}
|
|
345
|
-
data-layout={layout}
|
|
346
331
|
{...(link ? fill.attributes : {})}
|
|
347
332
|
aria-labelledby={named ? resolvedTitleId : undefined}
|
|
348
333
|
className={cn(
|
|
349
334
|
framed && "rounded-xl border border-border",
|
|
350
335
|
// O divisor e o conteúdo sem recuo encostam na borda: o raio precisa recortar.
|
|
351
|
-
framed &&
|
|
336
|
+
framed && "overflow-hidden",
|
|
352
337
|
framed && "has-[>[data-flush=true]]:overflow-hidden",
|
|
353
338
|
raised &&
|
|
354
339
|
"bg-card text-card-foreground [--surface-background:var(--card)]",
|
|
@@ -375,7 +360,13 @@ export function Surface({
|
|
|
375
360
|
);
|
|
376
361
|
}
|
|
377
362
|
|
|
378
|
-
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
|
+
};
|
|
379
370
|
|
|
380
371
|
const legendInvalidMessage =
|
|
381
372
|
"SurfaceHeader de fieldset aceita somente SurfaceMedia e SurfaceTitle; contador, descrição, metadados e ações ficam fora da legenda.";
|
|
@@ -420,9 +411,9 @@ export function SurfaceHeader({
|
|
|
420
411
|
rowClassName={surfaceHeaderRowClasses("surface")}
|
|
421
412
|
{...(legend ? { invalidMessage: legendInvalidMessage } : {})}
|
|
422
413
|
className={cn(
|
|
423
|
-
surface
|
|
424
|
-
|
|
425
|
-
|
|
414
|
+
surfaceRegionClasses("surface", "header", {
|
|
415
|
+
bounded: surface.bounded,
|
|
416
|
+
}),
|
|
426
417
|
placement !== null && surfaceLegendClasses[placement],
|
|
427
418
|
className,
|
|
428
419
|
)}
|
|
@@ -479,7 +470,7 @@ export function SurfaceTitle({
|
|
|
479
470
|
);
|
|
480
471
|
const legend = surface.element === "fieldset";
|
|
481
472
|
const Heading = legend ? "span" : headingTags[surface.level];
|
|
482
|
-
usePageHeadingPresence(!legend && surface.level === 1);
|
|
473
|
+
usePageHeadingPresence(!legend && surface.level === 1, "surface");
|
|
483
474
|
return (
|
|
484
475
|
<Heading
|
|
485
476
|
id={id ?? surface.titleId}
|
|
@@ -487,7 +478,7 @@ export function SurfaceTitle({
|
|
|
487
478
|
className={cn(
|
|
488
479
|
surface.variant === "page"
|
|
489
480
|
? structuralHeaderClasses.page.title
|
|
490
|
-
: surfaceTitleClasses("surface"
|
|
481
|
+
: surfaceTitleClasses("surface"),
|
|
491
482
|
className,
|
|
492
483
|
)}
|
|
493
484
|
{...props}
|
|
@@ -605,10 +596,10 @@ export function SurfaceBody({
|
|
|
605
596
|
data-flush={flush ? "true" : undefined}
|
|
606
597
|
{...fill.attributes}
|
|
607
598
|
className={cn(
|
|
608
|
-
surfaceRegionClasses("surface",
|
|
609
|
-
|
|
599
|
+
surfaceRegionClasses("surface", "body", {
|
|
600
|
+
bounded: surface.bounded,
|
|
601
|
+
flush,
|
|
610
602
|
}),
|
|
611
|
-
placement === "notch" && surfaceLegendClasses.afterNotch,
|
|
612
603
|
placement === "band" && surfaceLegendClasses.afterBand,
|
|
613
604
|
fill.filling && fillRegionClassName,
|
|
614
605
|
className,
|
|
@@ -636,8 +627,7 @@ export function SurfaceFooter({
|
|
|
636
627
|
data-slot="surface-footer"
|
|
637
628
|
className={cn(
|
|
638
629
|
"flex items-center justify-end gap-2",
|
|
639
|
-
surface.bounded
|
|
640
|
-
surfaceRegionClasses("surface", surface.layout, "footer"),
|
|
630
|
+
surfaceRegionClasses("surface", "footer", { bounded: surface.bounded }),
|
|
641
631
|
placement === "band" && surfaceLegendClasses.afterBand,
|
|
642
632
|
className,
|
|
643
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();
|