@softize/opus 27.1.0 → 28.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 +55 -0
- package/bin/lib/check.mjs +139 -30
- package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +161 -0
- package/docs/adr/README.md +2 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -15
- package/src/ui/components/patterns/list.tsx +62 -53
- package/src/ui/components/patterns/page.tsx +146 -38
- package/src/ui/components/patterns/presentation.tsx +44 -59
- package/src/ui/components/patterns/structural-header.tsx +42 -6
- package/src/ui/components/patterns/surface-anatomy.ts +52 -22
- package/src/ui/components/patterns/surface.tsx +2 -3
- package/src/ui/components/primitives/control.ts +2 -2
- package/src/ui/components/primitives/dialog.tsx +7 -2
- package/src/ui/components/primitives/drawer.tsx +7 -2
- package/src/ui/components/primitives/icon-picker.tsx +271 -34
- package/src/ui/components/primitives/pagination.tsx +5 -4
- package/src/ui/components/primitives/tabs.tsx +15 -2
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/docs/content/action-list.md +11 -2
- package/src/ui/docs/content/button.md +7 -7
- package/src/ui/docs/content/compositions.md +35 -23
- package/src/ui/docs/content/dialog.md +18 -6
- package/src/ui/docs/content/drawer.md +17 -5
- package/src/ui/docs/content/icon-picker.md +37 -19
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/page.md +169 -69
- package/src/ui/docs/content/pagination.md +7 -8
- package/src/ui/docs/content/presentation.md +4 -3
- package/src/ui/docs/content/surface.md +8 -5
- package/src/ui/docs/content/tabs.md +12 -4
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +9 -1
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
DialogContent,
|
|
48
48
|
DialogFooter,
|
|
49
49
|
DialogHeader,
|
|
50
|
+
DialogIntro,
|
|
50
51
|
DialogNavigation,
|
|
51
52
|
DialogTitle,
|
|
52
53
|
} from "../primitives/dialog.tsx";
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
DrawerContent,
|
|
58
59
|
DrawerFooter,
|
|
59
60
|
DrawerHeader,
|
|
61
|
+
DrawerIntro,
|
|
60
62
|
DrawerNavigation,
|
|
61
63
|
DrawerTitle,
|
|
62
64
|
} from "../primitives/drawer.tsx";
|
|
@@ -66,6 +68,7 @@ import {
|
|
|
66
68
|
PageBody,
|
|
67
69
|
PageFooter,
|
|
68
70
|
PageHeader,
|
|
71
|
+
PageIntro,
|
|
69
72
|
PageNavigation,
|
|
70
73
|
PageTitle,
|
|
71
74
|
useInsidePageShell,
|
|
@@ -114,11 +117,13 @@ const bodyClassName =
|
|
|
114
117
|
* a lista do body é filha direta dele e assume `fill` sem classe no consumidor.
|
|
115
118
|
*/
|
|
116
119
|
function PresentationBody({
|
|
120
|
+
page = false,
|
|
117
121
|
layout,
|
|
118
122
|
className,
|
|
119
123
|
children,
|
|
120
124
|
}: {
|
|
121
125
|
layout: "flow" | "fill";
|
|
126
|
+
page?: boolean;
|
|
122
127
|
className?: string;
|
|
123
128
|
children: ReactNode;
|
|
124
129
|
}): ReactElement {
|
|
@@ -128,7 +133,7 @@ function PresentationBody({
|
|
|
128
133
|
data-slot="presentation-body"
|
|
129
134
|
{...fill.attributes}
|
|
130
135
|
className={cn(
|
|
131
|
-
bodyClassName,
|
|
136
|
+
!page && bodyClassName,
|
|
132
137
|
(layout === "fill" || fill.filling) && fillRegionClassName,
|
|
133
138
|
className,
|
|
134
139
|
)}
|
|
@@ -195,14 +200,14 @@ function PresentationFrame({
|
|
|
195
200
|
</PresentationActions>
|
|
196
201
|
);
|
|
197
202
|
const body = (
|
|
198
|
-
<PresentationBody
|
|
203
|
+
<PresentationBody
|
|
204
|
+
page={surface === "page"}
|
|
205
|
+
layout={layout}
|
|
206
|
+
className={bodyClassNameProp}
|
|
207
|
+
>
|
|
199
208
|
{typeof children === "function" ? children(footerTarget) : children}
|
|
200
209
|
</PresentationBody>
|
|
201
210
|
);
|
|
202
|
-
// Viewport entre o PageBody e o conteúdo. Em `fill`, é coluna flexível para a altura
|
|
203
|
-
// chegar ao conteúdo; a rolagem continua disponível como recurso de última instância.
|
|
204
|
-
const viewportFillClassName =
|
|
205
|
-
layout === "fill" ? fillRegionClassName : undefined;
|
|
206
211
|
const withAssistant = (content: ReactElement): ReactElement =>
|
|
207
212
|
assistantLabel === undefined || assistantPanel === undefined ? (
|
|
208
213
|
content
|
|
@@ -248,18 +253,7 @@ function PresentationFrame({
|
|
|
248
253
|
)}
|
|
249
254
|
</PageHeader>
|
|
250
255
|
)}
|
|
251
|
-
<PageBody
|
|
252
|
-
{withAssistant(
|
|
253
|
-
<div
|
|
254
|
-
className={cn(
|
|
255
|
-
"h-full overflow-x-hidden overflow-y-auto",
|
|
256
|
-
viewportFillClassName,
|
|
257
|
-
)}
|
|
258
|
-
>
|
|
259
|
-
{content}
|
|
260
|
-
</div>,
|
|
261
|
-
)}
|
|
262
|
-
</PageBody>
|
|
256
|
+
<PageBody>{withAssistant(content)}</PageBody>
|
|
263
257
|
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
264
258
|
</Page>
|
|
265
259
|
);
|
|
@@ -278,7 +272,6 @@ function PresentationFrame({
|
|
|
278
272
|
{navigation === undefined ? null : (
|
|
279
273
|
<PageNavigation>{navigation}</PageNavigation>
|
|
280
274
|
)}
|
|
281
|
-
{contentOwnsHeading ? null : <PageTitle>{title}</PageTitle>}
|
|
282
275
|
{headerActions === undefined &&
|
|
283
276
|
assistantAction === undefined ? null : (
|
|
284
277
|
<PageActions>
|
|
@@ -290,36 +283,24 @@ function PresentationFrame({
|
|
|
290
283
|
)}
|
|
291
284
|
</PageHeader>
|
|
292
285
|
)}
|
|
293
|
-
<PageBody
|
|
294
|
-
{
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
layout === "fill" ? "overflow-y-hidden" : "overflow-y-auto",
|
|
299
|
-
viewportFillClassName,
|
|
300
|
-
)}
|
|
301
|
-
>
|
|
302
|
-
{body}
|
|
303
|
-
</div>,
|
|
286
|
+
<PageBody>
|
|
287
|
+
{contentOwnsHeading ? null : (
|
|
288
|
+
<PageIntro>
|
|
289
|
+
<PageTitle>{title}</PageTitle>
|
|
290
|
+
</PageIntro>
|
|
304
291
|
)}
|
|
292
|
+
{withAssistant(body)}
|
|
305
293
|
</PageBody>
|
|
306
294
|
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
307
295
|
</Page>
|
|
308
296
|
);
|
|
309
297
|
}
|
|
310
298
|
return (
|
|
311
|
-
<Page
|
|
312
|
-
layout={layout}
|
|
313
|
-
className={cn(
|
|
314
|
-
"flex min-h-full max-w-none flex-col space-y-0 px-0 py-0",
|
|
315
|
-
className,
|
|
316
|
-
)}
|
|
317
|
-
>
|
|
299
|
+
<Page layout={layout} className={className} contentClassName="max-w-none">
|
|
318
300
|
<PageHeader>
|
|
319
301
|
{navigation === undefined ? null : (
|
|
320
302
|
<PageNavigation>{navigation}</PageNavigation>
|
|
321
303
|
)}
|
|
322
|
-
<PageTitle className="text-lg leading-none">{title}</PageTitle>
|
|
323
304
|
{headerActions === undefined &&
|
|
324
305
|
assistantAction === undefined ? null : (
|
|
325
306
|
<PageActions>
|
|
@@ -330,17 +311,11 @@ function PresentationFrame({
|
|
|
330
311
|
</PageActions>
|
|
331
312
|
)}
|
|
332
313
|
</PageHeader>
|
|
333
|
-
<PageBody
|
|
334
|
-
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
viewportFillClassName,
|
|
339
|
-
)}
|
|
340
|
-
>
|
|
341
|
-
{body}
|
|
342
|
-
</div>,
|
|
343
|
-
)}
|
|
314
|
+
<PageBody>
|
|
315
|
+
<PageIntro>
|
|
316
|
+
<PageTitle>{title}</PageTitle>
|
|
317
|
+
</PageIntro>
|
|
318
|
+
{withAssistant(body)}
|
|
344
319
|
</PageBody>
|
|
345
320
|
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
346
321
|
</Page>
|
|
@@ -364,7 +339,6 @@ function PresentationFrame({
|
|
|
364
339
|
{navigation === undefined ? null : (
|
|
365
340
|
<DialogNavigation>{navigation}</DialogNavigation>
|
|
366
341
|
)}
|
|
367
|
-
<DialogTitle>{title}</DialogTitle>
|
|
368
342
|
{headerActions === undefined &&
|
|
369
343
|
assistantAction === undefined ? null : (
|
|
370
344
|
<DialogActions>
|
|
@@ -375,7 +349,14 @@ function PresentationFrame({
|
|
|
375
349
|
</DialogActions>
|
|
376
350
|
)}
|
|
377
351
|
</DialogHeader>
|
|
378
|
-
{withAssistant(
|
|
352
|
+
{withAssistant(
|
|
353
|
+
<DialogBody>
|
|
354
|
+
<DialogIntro>
|
|
355
|
+
<DialogTitle>{title}</DialogTitle>
|
|
356
|
+
</DialogIntro>
|
|
357
|
+
{body}
|
|
358
|
+
</DialogBody>,
|
|
359
|
+
)}
|
|
379
360
|
{hasFooter ? (
|
|
380
361
|
<DialogFooter>{renderFooterActions()}</DialogFooter>
|
|
381
362
|
) : null}
|
|
@@ -402,7 +383,6 @@ function PresentationFrame({
|
|
|
402
383
|
{navigation === undefined ? null : (
|
|
403
384
|
<DrawerNavigation>{navigation}</DrawerNavigation>
|
|
404
385
|
)}
|
|
405
|
-
<DrawerTitle>{title}</DrawerTitle>
|
|
406
386
|
{headerActions === undefined &&
|
|
407
387
|
assistantAction === undefined ? null : (
|
|
408
388
|
<DrawerActions>
|
|
@@ -413,7 +393,14 @@ function PresentationFrame({
|
|
|
413
393
|
</DrawerActions>
|
|
414
394
|
)}
|
|
415
395
|
</DrawerHeader>
|
|
416
|
-
{withAssistant(
|
|
396
|
+
{withAssistant(
|
|
397
|
+
<DrawerBody>
|
|
398
|
+
<DrawerIntro>
|
|
399
|
+
<DrawerTitle>{title}</DrawerTitle>
|
|
400
|
+
</DrawerIntro>
|
|
401
|
+
{body}
|
|
402
|
+
</DrawerBody>,
|
|
403
|
+
)}
|
|
417
404
|
{hasFooter ? (
|
|
418
405
|
<DrawerFooter>{renderFooterActions()}</DrawerFooter>
|
|
419
406
|
) : null}
|
|
@@ -635,9 +622,7 @@ export function Presentation({
|
|
|
635
622
|
string,
|
|
636
623
|
unknown
|
|
637
624
|
>;
|
|
638
|
-
//
|
|
639
|
-
// Seus comandos textuais usam a linha padrão; listas e grids continuam donos da
|
|
640
|
-
// densidade compacta de suas próprias ações.
|
|
625
|
+
// A região chamadora escolhe a escala; o footer conserva o botão padrão.
|
|
641
626
|
if (action.kind === "simple") {
|
|
642
627
|
return (
|
|
643
628
|
<PresentationCommandTrigger
|
|
@@ -784,7 +769,7 @@ export function Presentation({
|
|
|
784
769
|
|
|
785
770
|
const headerActions = definition.actions
|
|
786
771
|
.filter((command) => command.placement === "header")
|
|
787
|
-
.map((command) => renderCommand(command));
|
|
772
|
+
.map((command) => renderCommand(command, "sm"));
|
|
788
773
|
const footerActions = definition.actions
|
|
789
774
|
.filter((command) => command.placement === "footer")
|
|
790
775
|
.map((command) => renderCommand(command));
|
|
@@ -794,7 +779,7 @@ export function Presentation({
|
|
|
794
779
|
(invocation.surface === "page" || parentSurface !== "page");
|
|
795
780
|
const navigation = !canGoBack ? undefined : (
|
|
796
781
|
<Button
|
|
797
|
-
size="icon"
|
|
782
|
+
size="icon-sm"
|
|
798
783
|
variant="ghost"
|
|
799
784
|
disabled={surfaceBlocked}
|
|
800
785
|
aria-label="Voltar"
|
|
@@ -807,7 +792,7 @@ export function Presentation({
|
|
|
807
792
|
const assistantAction =
|
|
808
793
|
definition.assistant === undefined || assistantOpen ? undefined : (
|
|
809
794
|
<Button
|
|
810
|
-
size="icon"
|
|
795
|
+
size="icon-sm"
|
|
811
796
|
variant="ghost"
|
|
812
797
|
aria-label={definition.assistant.triggerLabel}
|
|
813
798
|
title={definition.assistant.triggerLabel}
|
|
@@ -110,7 +110,9 @@ export function StructuralHeader({
|
|
|
110
110
|
...props
|
|
111
111
|
}: StructuralHeaderProps): ReactElement {
|
|
112
112
|
const Wrapper = Root === "legend" ? "span" : "div";
|
|
113
|
-
const nodes = flatten(children)
|
|
113
|
+
const nodes = flatten(children).filter(
|
|
114
|
+
(node) => typeof node !== "string" || node.trim() !== "",
|
|
115
|
+
);
|
|
114
116
|
const of = (type: ElementType) =>
|
|
115
117
|
nodes.filter((node) => isValidElement(node) && node.type === type);
|
|
116
118
|
const leadingTypes: readonly ElementType[] =
|
|
@@ -305,6 +307,7 @@ export function StructuralHeader({
|
|
|
305
307
|
className={cn(
|
|
306
308
|
actionSlots.length > 0 &&
|
|
307
309
|
"flex flex-row items-end justify-between gap-4",
|
|
310
|
+
rowClassName,
|
|
308
311
|
)}
|
|
309
312
|
>
|
|
310
313
|
{heading}
|
|
@@ -314,16 +317,24 @@ export function StructuralHeader({
|
|
|
314
317
|
) : leading.length > 0 ? (
|
|
315
318
|
<Wrapper
|
|
316
319
|
data-slot={`${slot}-header-row`}
|
|
317
|
-
className="flex min-w-0 items-center gap-4"
|
|
320
|
+
className={cn("flex min-w-0 items-center gap-4", rowClassName)}
|
|
318
321
|
>
|
|
319
|
-
<Wrapper
|
|
320
|
-
data-slot={`${slot}-navigation`}
|
|
321
|
-
className="min-w-0 flex-1"
|
|
322
|
-
>
|
|
322
|
+
<Wrapper data-slot={`${slot}-navigation`} className="min-w-0 flex-1">
|
|
323
323
|
{leading}
|
|
324
324
|
</Wrapper>
|
|
325
325
|
{actionSlots}
|
|
326
326
|
</Wrapper>
|
|
327
|
+
) : rowClassName !== undefined ? (
|
|
328
|
+
<Wrapper
|
|
329
|
+
data-slot={`${slot}-header-row`}
|
|
330
|
+
className={cn(
|
|
331
|
+
rowClassName,
|
|
332
|
+
heading ? "justify-between" : "justify-end",
|
|
333
|
+
)}
|
|
334
|
+
>
|
|
335
|
+
{heading}
|
|
336
|
+
{actionSlots}
|
|
337
|
+
</Wrapper>
|
|
327
338
|
) : (
|
|
328
339
|
<>
|
|
329
340
|
{heading}
|
|
@@ -360,3 +371,28 @@ export function StructuralHeader({
|
|
|
360
371
|
</Root>
|
|
361
372
|
);
|
|
362
373
|
}
|
|
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
|
+
}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "react";
|
|
28
28
|
import { FillBoundary } from "./fill-region.ts";
|
|
29
29
|
|
|
30
|
-
export type SurfaceFamily = "surface" | "dialog" | "drawer";
|
|
30
|
+
export type SurfaceFamily = "surface" | "dialog" | "drawer" | "page";
|
|
31
31
|
export type SurfaceLayout = "continuous" | "sectioned";
|
|
32
32
|
export type SurfaceRegion = "header" | "body" | "footer";
|
|
33
33
|
|
|
@@ -64,18 +64,22 @@ interface FamilyAnatomy {
|
|
|
64
64
|
description: string;
|
|
65
65
|
/** Espaço entre mídia, título e ações na linha do header. */
|
|
66
66
|
headerGap: string;
|
|
67
|
+
/** Altura mínima da linha, equivalente ao controle padrão da família. */
|
|
68
|
+
headerMinHeight: string;
|
|
67
69
|
/** Faixa do rodapé no layout seccionado, quando a família declara uma. */
|
|
68
70
|
footerBand: string;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
74
|
* O recuo seccionado de Dialog e Drawer: 1rem, a densidade das superfícies de operação.
|
|
73
|
-
*
|
|
74
|
-
* faixas de header e footer mais baixas.
|
|
75
|
+
* O header é comum à família; corpos e rodapés conservam o ritmo de cada superfície.
|
|
75
76
|
*/
|
|
77
|
+
const structuralHeaderPadding = "p-4";
|
|
78
|
+
const structuralHeaderMinHeight = "min-h-8";
|
|
79
|
+
|
|
76
80
|
const sectionedRegions: RegionAnatomy = {
|
|
77
|
-
header:
|
|
78
|
-
body: "
|
|
81
|
+
header: structuralHeaderPadding,
|
|
82
|
+
body: "p-4",
|
|
79
83
|
footer: "p-4",
|
|
80
84
|
};
|
|
81
85
|
|
|
@@ -83,25 +87,47 @@ const sectionedRegions: RegionAnatomy = {
|
|
|
83
87
|
* Ritmo entre blocos diretos do conteúdo; relações internas pertencem a um wrapper filho.
|
|
84
88
|
* Os blocos não encolhem: o body é quem rola quando a família tem altura limitada.
|
|
85
89
|
*/
|
|
86
|
-
const structuralBodyRhythm = "flex flex-col gap-
|
|
90
|
+
export const structuralBodyRhythm = "flex flex-col gap-4 *:shrink-0";
|
|
91
|
+
export const structuralRegionSpacing = "space-y-4";
|
|
92
|
+
export const structuralRegionGap = "gap-4";
|
|
87
93
|
|
|
88
94
|
export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
95
|
+
page: {
|
|
96
|
+
regions: {
|
|
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
|
+
},
|
|
112
|
+
description: "text-sm text-muted-foreground",
|
|
113
|
+
headerGap: "gap-4",
|
|
114
|
+
headerMinHeight: structuralHeaderMinHeight,
|
|
115
|
+
footerBand: "bg-muted/30",
|
|
116
|
+
},
|
|
89
117
|
// Medidas da Surface delimitada (com moldura ou elevação). Sem delimitação, as regiões
|
|
90
118
|
// não recebem recuo: a superfície flui alinhada ao hospedeiro (ADR 0045).
|
|
91
119
|
surface: {
|
|
92
120
|
regions: {
|
|
93
|
-
//
|
|
94
|
-
// reassume o respiro compacto da faixa; quando o body abre ou fecha a superfície,
|
|
95
|
-
// recebe o respiro inteiro do conteúdo.
|
|
121
|
+
// No contínuo, a região anterior fornece o intervalo; a primeira abre o recuo.
|
|
96
122
|
continuous: {
|
|
97
|
-
header:
|
|
98
|
-
body: "px-4 pt-0 pb-
|
|
123
|
+
header: structuralHeaderPadding,
|
|
124
|
+
body: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
99
125
|
footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
100
126
|
},
|
|
101
127
|
sectioned: {
|
|
102
|
-
header:
|
|
103
|
-
body: "
|
|
104
|
-
footer: "
|
|
128
|
+
header: structuralHeaderPadding,
|
|
129
|
+
body: "p-4",
|
|
130
|
+
footer: "p-4",
|
|
105
131
|
},
|
|
106
132
|
},
|
|
107
133
|
// Uma escala só: o nível 1 troca a apresentação inteira (ADR 0038), nunca o layout.
|
|
@@ -111,14 +137,15 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
111
137
|
},
|
|
112
138
|
description: "mt-0.5 text-xs text-muted-foreground",
|
|
113
139
|
headerGap: "gap-2",
|
|
140
|
+
headerMinHeight: structuralHeaderMinHeight,
|
|
114
141
|
footerBand: "",
|
|
115
142
|
},
|
|
116
143
|
dialog: {
|
|
117
144
|
regions: {
|
|
118
145
|
continuous: {
|
|
119
|
-
header:
|
|
120
|
-
body: "px-4
|
|
121
|
-
footer: "
|
|
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",
|
|
122
149
|
},
|
|
123
150
|
sectioned: sectionedRegions,
|
|
124
151
|
},
|
|
@@ -128,14 +155,15 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
128
155
|
},
|
|
129
156
|
description: "text-sm text-muted-foreground",
|
|
130
157
|
headerGap: "gap-4",
|
|
158
|
+
headerMinHeight: structuralHeaderMinHeight,
|
|
131
159
|
footerBand: "bg-muted/20 dark:bg-input/20",
|
|
132
160
|
},
|
|
133
161
|
drawer: {
|
|
134
162
|
regions: {
|
|
135
163
|
continuous: {
|
|
136
|
-
header:
|
|
137
|
-
body: "px-4
|
|
138
|
-
footer: "
|
|
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",
|
|
139
167
|
},
|
|
140
168
|
sectioned: sectionedRegions,
|
|
141
169
|
},
|
|
@@ -145,6 +173,7 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
145
173
|
},
|
|
146
174
|
description: "text-sm text-muted-foreground",
|
|
147
175
|
headerGap: "gap-4",
|
|
176
|
+
headerMinHeight: structuralHeaderMinHeight,
|
|
148
177
|
footerBand: "bg-muted/20 dark:bg-input/20",
|
|
149
178
|
},
|
|
150
179
|
};
|
|
@@ -190,7 +219,7 @@ export function surfaceDescriptionClasses(family: SurfaceFamily): string {
|
|
|
190
219
|
* `flex-1` mantém o heading como região elástica e as ações no extremo oposto.
|
|
191
220
|
*/
|
|
192
221
|
export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
|
|
193
|
-
return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap}`;
|
|
222
|
+
return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap} ${surfaceAnatomy[family].headerMinHeight}`;
|
|
194
223
|
}
|
|
195
224
|
|
|
196
225
|
/** Ações do header e do footer: fora do fluxo de compressão, na escala densa. */
|
|
@@ -207,7 +236,7 @@ export const surfaceActionsClasses = "flex shrink-0 items-center gap-2";
|
|
|
207
236
|
*/
|
|
208
237
|
export const surfaceLegendClasses = {
|
|
209
238
|
notch: "ml-2 w-auto px-2",
|
|
210
|
-
afterNotch: "pt-
|
|
239
|
+
afterNotch: "pt-4",
|
|
211
240
|
band: "float-left w-full",
|
|
212
241
|
afterBand: "clear-left",
|
|
213
242
|
} as const;
|
|
@@ -241,6 +270,7 @@ export function SurfaceLayoutProvider({
|
|
|
241
270
|
}
|
|
242
271
|
|
|
243
272
|
const familyDefaultLayout: Record<SurfaceFamily, SurfaceLayout> = {
|
|
273
|
+
page: "sectioned",
|
|
244
274
|
surface: "continuous",
|
|
245
275
|
dialog: "sectioned",
|
|
246
276
|
drawer: "sectioned",
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
surfaceHeaderRowClasses,
|
|
50
50
|
surfaceLegendClasses,
|
|
51
51
|
surfaceRegionClasses,
|
|
52
|
+
structuralRegionSpacing,
|
|
52
53
|
surfaceTitleClasses,
|
|
53
54
|
type SurfaceLayout,
|
|
54
55
|
} from "./surface-anatomy.ts";
|
|
@@ -343,9 +344,7 @@ export function Surface({
|
|
|
343
344
|
framed && "has-[>[data-flush=true]]:overflow-hidden",
|
|
344
345
|
raised &&
|
|
345
346
|
"bg-card text-card-foreground [--surface-background:var(--card)]",
|
|
346
|
-
structured &&
|
|
347
|
-
!bounded &&
|
|
348
|
-
(variant === "page" ? "space-y-6" : "space-y-3"),
|
|
347
|
+
structured && !bounded && structuralRegionSpacing,
|
|
349
348
|
link && fill.filling && fillRegionClassName,
|
|
350
349
|
className,
|
|
351
350
|
)}
|
|
@@ -7,7 +7,7 @@ export type ControlShape = 'default' | 'pill'
|
|
|
7
7
|
* Cada componente aceita o subconjunto coerente com seu papel, nunca um nome com outra medida.
|
|
8
8
|
*
|
|
9
9
|
* - Altura de linha (`xs` · `sm` · `default` · `lg`): 1.5 · 2 · 2.25 · 2.5rem.
|
|
10
|
-
* - Quadrado só-ícone (`icon-xs` · `icon-sm` · `icon` · `icon-lg`): 1.5 ·
|
|
10
|
+
* - Quadrado só-ícone (`icon-xs` · `icon-sm` · `icon` · `icon-lg`): 1.5 · 2 · 2.25 · 2.5rem.
|
|
11
11
|
* `icon-xs` é o degrau da ação que mora dentro de uma linha densa. `icon-sm` atende
|
|
12
12
|
* composições compactas que ainda precisam de mais presença.
|
|
13
13
|
* - Glifo (o svg dentro do controle daquele tamanho): 0.875 · 0.875 · 1 · 1.25rem. O quadrado
|
|
@@ -27,7 +27,7 @@ export const controlHeight: Record<ControlSize, string> = {
|
|
|
27
27
|
|
|
28
28
|
export const controlSquare: Record<ControlIconSize, string> = {
|
|
29
29
|
'icon-xs': 'size-6',
|
|
30
|
-
'icon-sm': 'size-
|
|
30
|
+
'icon-sm': 'size-8',
|
|
31
31
|
icon: 'size-9',
|
|
32
32
|
'icon-lg': 'size-10',
|
|
33
33
|
}
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
} from "../patterns/surface-anatomy.ts";
|
|
15
15
|
import { FillBoundary } from "../patterns/fill-region.ts";
|
|
16
16
|
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
StructuralHeader,
|
|
19
|
+
structuralHeaderClasses,
|
|
20
|
+
validateSurfaceIntro,
|
|
21
|
+
} from "../patterns/structural-header.tsx";
|
|
18
22
|
import {
|
|
19
23
|
StructuralMedia,
|
|
20
24
|
type StructuralMediaProps,
|
|
@@ -698,6 +702,7 @@ export function DialogBody({
|
|
|
698
702
|
}: React.ComponentProps<"div"> & { flush?: boolean }) {
|
|
699
703
|
const { mode } = useDialogContext();
|
|
700
704
|
const layout = useSurfaceLayout("dialog");
|
|
705
|
+
validateSurfaceIntro(props.children, DialogIntro, "DialogBody");
|
|
701
706
|
return (
|
|
702
707
|
<div
|
|
703
708
|
data-slot="dialog-body"
|
|
@@ -761,7 +766,7 @@ export function DialogTitle({
|
|
|
761
766
|
? "min-w-0 whitespace-normal"
|
|
762
767
|
: "min-w-0 truncate",
|
|
763
768
|
intro
|
|
764
|
-
?
|
|
769
|
+
? structuralHeaderClasses.page.title
|
|
765
770
|
: surfaceTitleClasses("dialog", layout),
|
|
766
771
|
className,
|
|
767
772
|
);
|
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
type SurfaceLayout,
|
|
14
14
|
} from "../patterns/surface-anatomy.ts";
|
|
15
15
|
import { headingTags, type HeadingLevel } from "../patterns/heading-level.ts";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
StructuralHeader,
|
|
18
|
+
structuralHeaderClasses,
|
|
19
|
+
validateSurfaceIntro,
|
|
20
|
+
} from "../patterns/structural-header.tsx";
|
|
17
21
|
import {
|
|
18
22
|
StructuralMedia,
|
|
19
23
|
type StructuralMediaProps,
|
|
@@ -361,6 +365,7 @@ function DrawerBody({
|
|
|
361
365
|
...props
|
|
362
366
|
}: React.ComponentProps<"div"> & { flush?: boolean }) {
|
|
363
367
|
const layout = useSurfaceLayout("drawer");
|
|
368
|
+
validateSurfaceIntro(props.children, DrawerIntro, "DrawerBody");
|
|
364
369
|
return (
|
|
365
370
|
<div
|
|
366
371
|
data-slot="drawer-body"
|
|
@@ -408,7 +413,7 @@ function DrawerTitle({
|
|
|
408
413
|
const classes = cn(
|
|
409
414
|
intro ? "min-w-0 whitespace-normal" : "min-w-0 truncate",
|
|
410
415
|
intro
|
|
411
|
-
?
|
|
416
|
+
? structuralHeaderClasses.page.title
|
|
412
417
|
: surfaceTitleClasses("drawer", layout),
|
|
413
418
|
className,
|
|
414
419
|
);
|