@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
|
@@ -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,13 @@ 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";
|
|
22
|
+
|
|
23
|
+
/** Metadados contextuais, separados da orientação textual e da linha de ações. */
|
|
24
|
+
export const structuralMetaClasses =
|
|
25
|
+
"mt-3 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted-foreground [overflow-wrap:anywhere] [&>*]:max-w-full";
|
|
20
26
|
|
|
21
27
|
export type StructuralHeaderVariant = "page" | "section";
|
|
22
28
|
|
|
@@ -26,10 +32,10 @@ export const structuralHeaderClasses: Record<
|
|
|
26
32
|
{ title: string; description: string; count: string; actions: string }
|
|
27
33
|
> = {
|
|
28
34
|
page: {
|
|
29
|
-
title:
|
|
35
|
+
title: structuralTitleClasses,
|
|
30
36
|
description: "mt-1 truncate text-sm text-muted-foreground",
|
|
31
37
|
count: "font-mono text-base text-muted-foreground/60",
|
|
32
|
-
actions: "flex w-auto shrink-0 items-center gap-
|
|
38
|
+
actions: "flex w-auto shrink-0 items-center gap-2",
|
|
33
39
|
},
|
|
34
40
|
section: {
|
|
35
41
|
title: "text-sm font-semibold",
|
|
@@ -46,6 +52,7 @@ interface StructuralHeaderSlots {
|
|
|
46
52
|
title: ElementType;
|
|
47
53
|
count?: ElementType;
|
|
48
54
|
description?: ElementType;
|
|
55
|
+
meta?: ElementType;
|
|
49
56
|
actions?: ElementType;
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -68,6 +75,8 @@ export interface StructuralHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
68
75
|
titleRequired?: boolean;
|
|
69
76
|
/** Classes da linha `inline` — as famílias com anatomia própria trazem a sua (ADR 0023). */
|
|
70
77
|
rowClassName?: string;
|
|
78
|
+
/** Lista de abas sob o título; o Tabs ancestral governa seleção e painéis. */
|
|
79
|
+
tabs?: ReactNode;
|
|
71
80
|
/** Nó interno acrescentado ao fim da linha, depois das ações (o botão de fechar). */
|
|
72
81
|
trailing?: ReactNode;
|
|
73
82
|
/**
|
|
@@ -103,12 +112,18 @@ export function StructuralHeader({
|
|
|
103
112
|
titleRequired = true,
|
|
104
113
|
rowClassName,
|
|
105
114
|
trailing,
|
|
115
|
+
tabs,
|
|
106
116
|
allowFreeContent = false,
|
|
107
117
|
invalidMessage,
|
|
108
118
|
className,
|
|
109
119
|
children,
|
|
110
120
|
...props
|
|
111
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
|
+
}
|
|
112
127
|
const Wrapper = Root === "legend" ? "span" : "div";
|
|
113
128
|
const nodes = flatten(children).filter(
|
|
114
129
|
(node) => typeof node !== "string" || node.trim() !== "",
|
|
@@ -127,6 +142,7 @@ export function StructuralHeader({
|
|
|
127
142
|
const counts = slots.count === undefined ? [] : of(slots.count);
|
|
128
143
|
const descriptions =
|
|
129
144
|
slots.description === undefined ? [] : of(slots.description);
|
|
145
|
+
const metas = slots.meta === undefined ? [] : of(slots.meta);
|
|
130
146
|
const actionSlots = slots.actions === undefined ? [] : of(slots.actions);
|
|
131
147
|
const slotTypes = [
|
|
132
148
|
...leadingTypes,
|
|
@@ -134,6 +150,7 @@ export function StructuralHeader({
|
|
|
134
150
|
slots.title,
|
|
135
151
|
slots.count,
|
|
136
152
|
slots.description,
|
|
153
|
+
slots.meta,
|
|
137
154
|
slots.actions,
|
|
138
155
|
].filter((type): type is ElementType => type !== undefined);
|
|
139
156
|
const free = allowFreeContent
|
|
@@ -151,6 +168,7 @@ export function StructuralHeader({
|
|
|
151
168
|
titles.length +
|
|
152
169
|
counts.length +
|
|
153
170
|
descriptions.length +
|
|
171
|
+
metas.length +
|
|
154
172
|
actionSlots.length;
|
|
155
173
|
|
|
156
174
|
if (
|
|
@@ -159,6 +177,7 @@ export function StructuralHeader({
|
|
|
159
177
|
medias.length > 1 ||
|
|
160
178
|
counts.length > 1 ||
|
|
161
179
|
descriptions.length > 1 ||
|
|
180
|
+
metas.length > 1 ||
|
|
162
181
|
actionSlots.length > 1 ||
|
|
163
182
|
recognized !== nodes.length
|
|
164
183
|
) {
|
|
@@ -170,6 +189,7 @@ export function StructuralHeader({
|
|
|
170
189
|
...leadingTypes,
|
|
171
190
|
slots.media,
|
|
172
191
|
slots.description,
|
|
192
|
+
slots.meta,
|
|
173
193
|
slots.count,
|
|
174
194
|
slots.actions,
|
|
175
195
|
]
|
|
@@ -193,6 +213,10 @@ export function StructuralHeader({
|
|
|
193
213
|
);
|
|
194
214
|
}
|
|
195
215
|
|
|
216
|
+
if (metas.length > 0 && titles.length === 0) {
|
|
217
|
+
throw new Error(`${name} exige título para exibir metadados.`);
|
|
218
|
+
}
|
|
219
|
+
|
|
196
220
|
const titleLine = titles.length + counts.length > 0 && (
|
|
197
221
|
<Wrapper className="flex items-baseline gap-2">
|
|
198
222
|
{titles}
|
|
@@ -267,8 +291,7 @@ export function StructuralHeader({
|
|
|
267
291
|
{inlineContext === null &&
|
|
268
292
|
leadingPlacement === "inline" &&
|
|
269
293
|
leading.length > 0 && (
|
|
270
|
-
//
|
|
271
|
-
// 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.
|
|
272
295
|
<Wrapper
|
|
273
296
|
data-slot={`${slot}-navigation`}
|
|
274
297
|
className={cn("flex min-w-0 items-center", !heading && "flex-1")}
|
|
@@ -342,11 +365,20 @@ export function StructuralHeader({
|
|
|
342
365
|
</>
|
|
343
366
|
);
|
|
344
367
|
|
|
368
|
+
const withMeta = (
|
|
369
|
+
<>
|
|
370
|
+
{layout}
|
|
371
|
+
{metas}
|
|
372
|
+
</>
|
|
373
|
+
);
|
|
374
|
+
|
|
345
375
|
return (
|
|
346
376
|
<Root
|
|
347
377
|
data-slot={`${slot}-header`}
|
|
348
378
|
className={cn(
|
|
349
|
-
|
|
379
|
+
!hasTabs &&
|
|
380
|
+
metas.length === 0 &&
|
|
381
|
+
contentClassName === undefined &&
|
|
350
382
|
leadingPlacement !== "inline" &&
|
|
351
383
|
leading.length === 0 &&
|
|
352
384
|
actionSlots.length > 0 &&
|
|
@@ -355,44 +387,25 @@ export function StructuralHeader({
|
|
|
355
387
|
heading ? "justify-between" : "justify-end",
|
|
356
388
|
),
|
|
357
389
|
className,
|
|
390
|
+
hasTabs && "block border-b pb-0",
|
|
358
391
|
)}
|
|
359
392
|
{...props}
|
|
360
393
|
>
|
|
361
394
|
{contentClassName === undefined ? (
|
|
362
|
-
|
|
395
|
+
withMeta
|
|
363
396
|
) : (
|
|
364
397
|
<Wrapper
|
|
365
398
|
data-slot={`${slot}-header-content`}
|
|
366
399
|
className={contentClassName}
|
|
367
400
|
>
|
|
368
|
-
{
|
|
401
|
+
{withMeta}
|
|
369
402
|
</Wrapper>
|
|
370
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
|
+
)}
|
|
371
409
|
</Root>
|
|
372
410
|
);
|
|
373
411
|
}
|
|
374
|
-
|
|
375
|
-
/** A intro visível abre o corpo; wrappers opacos são verificados quando materializam seus slots. */
|
|
376
|
-
export function validateSurfaceIntro(
|
|
377
|
-
children: ReactNode,
|
|
378
|
-
intro: ElementType,
|
|
379
|
-
name: string,
|
|
380
|
-
): void {
|
|
381
|
-
const flatten = (nodes: ReactNode): ReactNode[] =>
|
|
382
|
-
Children.toArray(nodes).flatMap((node) =>
|
|
383
|
-
isValidElement(node) && node.type === Fragment
|
|
384
|
-
? flatten((node.props as { children?: ReactNode }).children)
|
|
385
|
-
: [node],
|
|
386
|
-
);
|
|
387
|
-
const nodes = flatten(children).filter(
|
|
388
|
-
(node) => typeof node !== "string" || node.trim() !== "",
|
|
389
|
-
);
|
|
390
|
-
const indexes = nodes.flatMap((node, index) =>
|
|
391
|
-
isValidElement(node) && node.type === intro ? [index] : [],
|
|
392
|
-
);
|
|
393
|
-
if (indexes.length > 1 || (indexes.length === 1 && indexes[0] !== 0)) {
|
|
394
|
-
throw new Error(
|
|
395
|
-
`${name} aceita uma única intro como primeiro bloco do corpo.`,
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
@@ -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
|
-
}
|