@softize/opus 23.0.0 → 24.0.1
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 +99 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +241 -42
- package/bin/lib/copy.mjs +16 -34
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/docs/adr/0049-inline-navigation-groups-with-the-heading.md +95 -0
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +15 -19
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +90 -27
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +90 -73
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +4 -2
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +76 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/compositions.md +479 -0
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +105 -60
- package/src/ui/docs/content/drawer.md +68 -22
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +44 -34
- package/src/ui/docs/content/pane.md +42 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +20 -20
- package/src/ui/docs/content/toggle.md +117 -65
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +10 -6
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +40 -43
- package/src/ui/theme.css +311 -64
- package/src/ui/components/patterns/content-header.tsx +0 -528
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- package/src/ui/docs/content/content.md +0 -275
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cabeçalho
|
|
2
|
+
* Cabeçalho estrutural (interno): a anatomia que Page, Surface, Dialog, Drawer e Pane
|
|
3
|
+
* compartilham. O nome público `SurfaceHeader` pertence à família Surface (ADR 0045).
|
|
3
4
|
*
|
|
4
5
|
* Um título, metadata opcional e uma região de ações que vai para o extremo oposto
|
|
5
6
|
* em larguras amplas e empilha abaixo do contexto nas estreitas. Cada família escolhe quais slots
|
|
@@ -17,11 +18,11 @@ import {
|
|
|
17
18
|
} from "react";
|
|
18
19
|
import { cn } from "../../lib/cn.ts";
|
|
19
20
|
|
|
20
|
-
export type
|
|
21
|
+
export type StructuralHeaderVariant = "page" | "section";
|
|
21
22
|
|
|
22
|
-
/** As classes por slot, por hierarquia visual — PageTitle e
|
|
23
|
-
export const
|
|
24
|
-
|
|
23
|
+
/** As classes por slot, por hierarquia visual — PageTitle e SurfaceTitle no nível 1 são o mesmo. */
|
|
24
|
+
export const structuralHeaderClasses: Record<
|
|
25
|
+
StructuralHeaderVariant,
|
|
25
26
|
{ title: string; description: string; count: string; actions: string }
|
|
26
27
|
> = {
|
|
27
28
|
page: {
|
|
@@ -38,7 +39,7 @@ export const surfaceHeaderClasses: Record<
|
|
|
38
39
|
},
|
|
39
40
|
};
|
|
40
41
|
|
|
41
|
-
interface
|
|
42
|
+
interface StructuralHeaderSlots {
|
|
42
43
|
leading?: ElementType | readonly ElementType[];
|
|
43
44
|
/** Ícone ou imagem que abre a linha, antes do heading (ADR 0023). */
|
|
44
45
|
media?: ElementType;
|
|
@@ -48,15 +49,15 @@ interface SurfaceHeaderSlots {
|
|
|
48
49
|
actions?: ElementType;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
export interface
|
|
52
|
+
export interface StructuralHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
52
53
|
/** Elemento estrutural; `legend` preserva a mesma anatomia com wrappers phrasing. */
|
|
53
54
|
as?: "div" | "legend";
|
|
54
|
-
/** Nome do componente público, para as mensagens de erro (`PageHeader`, `
|
|
55
|
+
/** Nome do componente público, para as mensagens de erro (`PageHeader`, `SurfaceHeader`). */
|
|
55
56
|
name: string;
|
|
56
57
|
/** Prefixo dos `data-slot` (`page` → `page-header`, `page-heading`). */
|
|
57
58
|
slot: string;
|
|
58
59
|
/** Os componentes de slot que este cabeçalho reconhece. */
|
|
59
|
-
slots:
|
|
60
|
+
slots: StructuralHeaderSlots;
|
|
60
61
|
/** Posição de um slot introdutório opcional, antes da linha ou dentro dela. */
|
|
61
62
|
leadingPlacement?: "above" | "inline";
|
|
62
63
|
/** Mantém a mídia na linha geral ou a agrupa com o heading. */
|
|
@@ -72,7 +73,7 @@ export interface SurfaceHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
72
73
|
/**
|
|
73
74
|
* Aceita conteúdo livre além dos slots. Vale para a superfície genérica — um pane
|
|
74
75
|
* hospeda marca, busca ou chrome que não são título nem ação. As famílias que sempre
|
|
75
|
-
* NOMEIAM algo (
|
|
76
|
+
* NOMEIAM algo (Surface, Dialog, Drawer, Page) continuam estritas.
|
|
76
77
|
*/
|
|
77
78
|
allowFreeContent?: boolean;
|
|
78
79
|
/**
|
|
@@ -91,7 +92,7 @@ function flatten(children: ReactNode): ReactNode[] {
|
|
|
91
92
|
);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
export function
|
|
95
|
+
export function StructuralHeader({
|
|
95
96
|
as: Root = "div",
|
|
96
97
|
name,
|
|
97
98
|
slot,
|
|
@@ -107,7 +108,7 @@ export function SurfaceHeader({
|
|
|
107
108
|
className,
|
|
108
109
|
children,
|
|
109
110
|
...props
|
|
110
|
-
}:
|
|
111
|
+
}: StructuralHeaderProps): ReactElement {
|
|
111
112
|
const Wrapper = Root === "legend" ? "span" : "div";
|
|
112
113
|
const nodes = flatten(children);
|
|
113
114
|
const of = (type: ElementType) =>
|
|
@@ -124,8 +125,7 @@ export function SurfaceHeader({
|
|
|
124
125
|
const counts = slots.count === undefined ? [] : of(slots.count);
|
|
125
126
|
const descriptions =
|
|
126
127
|
slots.description === undefined ? [] : of(slots.description);
|
|
127
|
-
const actionSlots =
|
|
128
|
-
slots.actions === undefined ? [] : of(slots.actions);
|
|
128
|
+
const actionSlots = slots.actions === undefined ? [] : of(slots.actions);
|
|
129
129
|
const slotTypes = [
|
|
130
130
|
...leadingTypes,
|
|
131
131
|
slots.media,
|
|
@@ -137,7 +137,9 @@ export function SurfaceHeader({
|
|
|
137
137
|
const free = allowFreeContent
|
|
138
138
|
? nodes.filter(
|
|
139
139
|
(node) =>
|
|
140
|
-
!(
|
|
140
|
+
!(
|
|
141
|
+
isValidElement(node) && slotTypes.includes(node.type as ElementType)
|
|
142
|
+
),
|
|
141
143
|
)
|
|
142
144
|
: [];
|
|
143
145
|
const recognized =
|
|
@@ -228,30 +230,72 @@ export function SurfaceHeader({
|
|
|
228
230
|
);
|
|
229
231
|
// A região introdutória carrega o mesmo `data-slot` nas duas apresentações: seletor de teste
|
|
230
232
|
// ou de CSS que funcione no header padrão precisa funcionar na barra.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
233
|
+
// Navegação e título formam um contexto com intervalo de 0,5rem (ADR 0049). O intervalo da
|
|
234
|
+
// linha continua separando esse contexto das ações, e a mídia mantém, até o título, o mesmo
|
|
235
|
+
// intervalo que teria sem navegação.
|
|
236
|
+
const rowMedias = mediaPlacement === "row" ? medias : [];
|
|
237
|
+
const inlineContext =
|
|
238
|
+
leadingPlacement === "inline" && leading.length > 0 && heading ? (
|
|
239
|
+
<Wrapper
|
|
240
|
+
data-slot={`${slot}-header-context`}
|
|
241
|
+
className="flex min-w-0 flex-1 items-center gap-2"
|
|
242
|
+
>
|
|
236
243
|
<Wrapper
|
|
237
244
|
data-slot={`${slot}-navigation`}
|
|
238
|
-
className=
|
|
245
|
+
className="flex min-w-0 items-center"
|
|
239
246
|
>
|
|
240
247
|
{leading}
|
|
241
248
|
</Wrapper>
|
|
249
|
+
{rowMedias.length > 0 ? (
|
|
250
|
+
<Wrapper
|
|
251
|
+
data-slot={`${slot}-header-identity`}
|
|
252
|
+
className={cn("flex min-w-0 items-center gap-3", rowClassName)}
|
|
253
|
+
>
|
|
254
|
+
{rowMedias}
|
|
255
|
+
{heading}
|
|
256
|
+
</Wrapper>
|
|
257
|
+
) : (
|
|
258
|
+
heading
|
|
259
|
+
)}
|
|
260
|
+
</Wrapper>
|
|
261
|
+
) : null;
|
|
262
|
+
const row = (
|
|
263
|
+
<>
|
|
264
|
+
{inlineContext}
|
|
265
|
+
{inlineContext === null &&
|
|
266
|
+
leadingPlacement === "inline" &&
|
|
267
|
+
leading.length > 0 && (
|
|
268
|
+
// `min-w-0` aqui é o que deixa a trilha do PageNavigation ceder e truncar; sem ele o
|
|
269
|
+
// wrapper assume o min-content do breadcrumb e a compressão toda cai sobre o título.
|
|
270
|
+
<Wrapper
|
|
271
|
+
data-slot={`${slot}-navigation`}
|
|
272
|
+
className={cn("flex min-w-0 items-center", !heading && "flex-1")}
|
|
273
|
+
>
|
|
274
|
+
{leading}
|
|
275
|
+
</Wrapper>
|
|
276
|
+
)}
|
|
277
|
+
{inlineContext === null && mediaPlacement === "row" && medias}
|
|
278
|
+
{inlineContext === null && heading}
|
|
279
|
+
{!heading && leading.length === 0 && (
|
|
280
|
+
<Wrapper
|
|
281
|
+
data-slot={`${slot}-header-spacer`}
|
|
282
|
+
className="min-w-0 flex-1"
|
|
283
|
+
aria-hidden="true"
|
|
284
|
+
/>
|
|
242
285
|
)}
|
|
243
|
-
{mediaPlacement === "row" && medias}
|
|
244
|
-
{heading}
|
|
245
286
|
{actionSlots}
|
|
246
287
|
{trailing}
|
|
247
288
|
</>
|
|
248
289
|
);
|
|
249
290
|
const layout =
|
|
250
291
|
leadingPlacement === "inline" ? (
|
|
251
|
-
<Wrapper
|
|
292
|
+
<Wrapper
|
|
293
|
+
data-slot={`${slot}-header-row`}
|
|
294
|
+
className={cn("flex min-w-0 items-center gap-3", rowClassName)}
|
|
295
|
+
>
|
|
252
296
|
{row}
|
|
253
297
|
</Wrapper>
|
|
254
|
-
) : leading.length > 0 ? (
|
|
298
|
+
) : leading.length > 0 && heading ? (
|
|
255
299
|
<>
|
|
256
300
|
<Wrapper data-slot={`${slot}-navigation`} className="mb-4">
|
|
257
301
|
{leading}
|
|
@@ -267,6 +311,19 @@ export function SurfaceHeader({
|
|
|
267
311
|
{actionSlots}
|
|
268
312
|
</Wrapper>
|
|
269
313
|
</>
|
|
314
|
+
) : leading.length > 0 ? (
|
|
315
|
+
<Wrapper
|
|
316
|
+
data-slot={`${slot}-header-row`}
|
|
317
|
+
className="flex min-w-0 items-center gap-4"
|
|
318
|
+
>
|
|
319
|
+
<Wrapper
|
|
320
|
+
data-slot={`${slot}-navigation`}
|
|
321
|
+
className="min-w-0 flex-1"
|
|
322
|
+
>
|
|
323
|
+
{leading}
|
|
324
|
+
</Wrapper>
|
|
325
|
+
{actionSlots}
|
|
326
|
+
</Wrapper>
|
|
270
327
|
) : (
|
|
271
328
|
<>
|
|
272
329
|
{heading}
|
|
@@ -282,7 +339,10 @@ export function SurfaceHeader({
|
|
|
282
339
|
leadingPlacement !== "inline" &&
|
|
283
340
|
leading.length === 0 &&
|
|
284
341
|
actionSlots.length > 0 &&
|
|
285
|
-
|
|
342
|
+
cn(
|
|
343
|
+
"flex flex-row items-end gap-4",
|
|
344
|
+
heading ? "justify-between" : "justify-end",
|
|
345
|
+
),
|
|
286
346
|
className,
|
|
287
347
|
)}
|
|
288
348
|
{...props}
|
|
@@ -290,7 +350,10 @@ export function SurfaceHeader({
|
|
|
290
350
|
{contentClassName === undefined ? (
|
|
291
351
|
layout
|
|
292
352
|
) : (
|
|
293
|
-
<Wrapper
|
|
353
|
+
<Wrapper
|
|
354
|
+
data-slot={`${slot}-header-content`}
|
|
355
|
+
className={contentClassName}
|
|
356
|
+
>
|
|
294
357
|
{layout}
|
|
295
358
|
</Wrapper>
|
|
296
359
|
)}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot de mídia estrutural (interno): abre a linha do header de Dialog, Drawer e Pane
|
|
3
|
+
* sem decidir a apresentação do conteúdo. A moldura, quando necessária, é composta
|
|
4
|
+
* explicitamente com `IconTile` pelo consumidor.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import { cn } from "../../lib/cn.ts";
|
|
8
|
+
|
|
9
|
+
export interface StructuralMediaProps extends React.ComponentProps<"div"> {}
|
|
10
|
+
|
|
11
|
+
export function StructuralMedia({
|
|
12
|
+
slot,
|
|
13
|
+
className,
|
|
14
|
+
...props
|
|
15
|
+
}: StructuralMediaProps & { slot: string }): ReactElement {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
data-slot={slot}
|
|
19
|
+
className={cn(
|
|
20
|
+
"flex shrink-0 items-center justify-center [&>svg:not([class*='size-'])]:size-4",
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Anatomia de superfície (interno): as medidas que
|
|
3
|
-
* compartilham —
|
|
2
|
+
* Anatomia de superfície (interno): as medidas que Surface, Dialog e Drawer
|
|
3
|
+
* compartilham — ADRs 0023, 0045 e 0047.
|
|
4
4
|
*
|
|
5
5
|
* Aqui mora o que antes era escrito seis vezes: recuo de cada região, ritmo vertical,
|
|
6
6
|
* escala do título, posição das ações e a regra dos divisores. As famílias públicas
|
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
*
|
|
11
11
|
* - `continuous` — sem divisores. O ritmo separa: o header se aproxima do corpo e a
|
|
12
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. É
|
|
13
|
+
* o mesmo respiro do topo. É a superfície de leitura.
|
|
14
14
|
* - `sectioned` — regiões simétricas, com divisor entre as que forem adjacentes. É o
|
|
15
15
|
* modal, o drawer e o painel.
|
|
16
16
|
*
|
|
17
17
|
* O recuo lateral é o mesmo nas três regiões da mesma superfície; é isso que a suíte
|
|
18
|
-
* transversal verifica.
|
|
19
|
-
* é mais folgado que um painel de operação.
|
|
18
|
+
* transversal verifica.
|
|
20
19
|
*/
|
|
21
20
|
|
|
22
21
|
import {
|
|
@@ -28,7 +27,7 @@ import {
|
|
|
28
27
|
} from "react";
|
|
29
28
|
import { FillBoundary } from "./fill-region.ts";
|
|
30
29
|
|
|
31
|
-
export type SurfaceFamily = "
|
|
30
|
+
export type SurfaceFamily = "surface" | "dialog" | "drawer";
|
|
32
31
|
export type SurfaceLayout = "continuous" | "sectioned";
|
|
33
32
|
export type SurfaceRegion = "header" | "body" | "footer";
|
|
34
33
|
|
|
@@ -70,44 +69,56 @@ interface FamilyAnatomy {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
/**
|
|
73
|
-
* O recuo seccionado de
|
|
74
|
-
*
|
|
75
|
-
*
|
|
72
|
+
* O recuo seccionado de Dialog e Drawer: 1rem, a densidade das superfícies de operação.
|
|
73
|
+
* A Surface usa o mesmo recuo lateral, para conviver com um drawer sem degrau visível, e
|
|
74
|
+
* faixas de header e footer mais baixas.
|
|
76
75
|
*/
|
|
77
76
|
const sectionedRegions: RegionAnatomy = {
|
|
78
77
|
header: "p-4",
|
|
79
|
-
body: "
|
|
78
|
+
body: "px-4 py-6",
|
|
80
79
|
footer: "p-4",
|
|
81
80
|
};
|
|
82
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Ritmo entre blocos diretos do conteúdo; relações internas pertencem a um wrapper filho.
|
|
84
|
+
* Os blocos não encolhem: o body é quem rola quando a família tem altura limitada.
|
|
85
|
+
*/
|
|
86
|
+
const structuralBodyRhythm = "flex flex-col gap-6 *:shrink-0";
|
|
87
|
+
|
|
83
88
|
export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
84
|
-
|
|
89
|
+
// Medidas da Surface delimitada (com moldura ou elevação). Sem delimitação, as regiões
|
|
90
|
+
// não recebem recuo: a superfície flui alinhada ao hospedeiro (ADR 0045).
|
|
91
|
+
surface: {
|
|
85
92
|
regions: {
|
|
86
|
-
// O
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// do que em cima.
|
|
93
|
+
// O espaço entre regiões soma o ritmo estrutural. Quando o header fica sozinho, ele
|
|
94
|
+
// reassume o respiro compacto da faixa; quando o body abre ou fecha a superfície,
|
|
95
|
+
// recebe o respiro inteiro do conteúdo.
|
|
90
96
|
continuous: {
|
|
91
|
-
header: "px-
|
|
92
|
-
body: "px-
|
|
93
|
-
footer: "px-
|
|
97
|
+
header: "px-4 pt-4 pb-6 [&:last-child]:pb-4",
|
|
98
|
+
body: "px-4 pt-0 pb-6 [&:first-child]:pt-6",
|
|
99
|
+
footer: "px-4 pt-0 pb-4 [&:first-child]:pt-4",
|
|
100
|
+
},
|
|
101
|
+
sectioned: {
|
|
102
|
+
header: "px-4 py-3",
|
|
103
|
+
body: "px-4 py-6",
|
|
104
|
+
footer: "px-4 py-3",
|
|
94
105
|
},
|
|
95
|
-
sectioned: sectionedRegions,
|
|
96
106
|
},
|
|
107
|
+
// Uma escala só: o nível 1 troca a apresentação inteira (ADR 0038), nunca o layout.
|
|
97
108
|
title: {
|
|
98
|
-
continuous: "text-
|
|
99
|
-
sectioned: "text-sm font-semibold
|
|
109
|
+
continuous: "text-sm font-semibold",
|
|
110
|
+
sectioned: "text-sm font-semibold",
|
|
100
111
|
},
|
|
101
|
-
description: "text-
|
|
102
|
-
headerGap: "gap-
|
|
112
|
+
description: "mt-0.5 text-xs text-muted-foreground",
|
|
113
|
+
headerGap: "gap-2",
|
|
103
114
|
footerBand: "",
|
|
104
115
|
},
|
|
105
116
|
dialog: {
|
|
106
117
|
regions: {
|
|
107
118
|
continuous: {
|
|
108
|
-
header: "p-4 pb-
|
|
109
|
-
body: "
|
|
110
|
-
footer: "p-4 pt-
|
|
119
|
+
header: "p-4 pb-3 [&:last-child]:pb-4",
|
|
120
|
+
body: "px-4 py-3 [&:last-child]:pb-6 [&:first-child]:pt-6",
|
|
121
|
+
footer: "p-4 pt-3",
|
|
111
122
|
},
|
|
112
123
|
sectioned: sectionedRegions,
|
|
113
124
|
},
|
|
@@ -122,9 +133,9 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
122
133
|
drawer: {
|
|
123
134
|
regions: {
|
|
124
135
|
continuous: {
|
|
125
|
-
header: "p-4 pb-
|
|
126
|
-
body: "
|
|
127
|
-
footer: "p-4 pt-
|
|
136
|
+
header: "p-4 pb-3 [&:last-child]:pb-4",
|
|
137
|
+
body: "px-4 py-3 [&:last-child]:pb-6 [&:first-child]:pt-6",
|
|
138
|
+
footer: "p-4 pt-3",
|
|
128
139
|
},
|
|
129
140
|
sectioned: sectionedRegions,
|
|
130
141
|
},
|
|
@@ -136,29 +147,6 @@ export const surfaceAnatomy: Record<SurfaceFamily, FamilyAnatomy> = {
|
|
|
136
147
|
headerGap: "gap-4",
|
|
137
148
|
footerBand: "bg-muted/20 dark:bg-input/20",
|
|
138
149
|
},
|
|
139
|
-
pane: {
|
|
140
|
-
regions: {
|
|
141
|
-
continuous: {
|
|
142
|
-
header: "flex shrink-0 items-center px-3 pt-3 pb-2 [&:last-child]:pb-3",
|
|
143
|
-
body: "p-3 [&:first-child]:pt-3",
|
|
144
|
-
footer: "px-3 pt-2 pb-3",
|
|
145
|
-
},
|
|
146
|
-
sectioned: {
|
|
147
|
-
// A faixa tem a altura da barra do shell; `flex items-center` centraliza a linha
|
|
148
|
-
// dentro dela, e `shrink-0` impede que um corpo longo comprima a faixa.
|
|
149
|
-
header: "flex h-[3.75rem] shrink-0 items-center px-3",
|
|
150
|
-
body: "p-3",
|
|
151
|
-
footer: "px-3 py-2",
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
title: {
|
|
155
|
-
continuous: "text-sm font-semibold leading-none",
|
|
156
|
-
sectioned: "text-sm font-semibold leading-none",
|
|
157
|
-
},
|
|
158
|
-
description: "text-xs text-muted-foreground",
|
|
159
|
-
headerGap: "gap-2",
|
|
160
|
-
footerBand: "",
|
|
161
|
-
},
|
|
162
150
|
};
|
|
163
151
|
|
|
164
152
|
/** Classes da região, já com o divisor que o layout deriva da vizinhança. */
|
|
@@ -166,14 +154,22 @@ export function surfaceRegionClasses(
|
|
|
166
154
|
family: SurfaceFamily,
|
|
167
155
|
layout: SurfaceLayout,
|
|
168
156
|
region: SurfaceRegion,
|
|
157
|
+
{ flush = false }: { flush?: boolean } = {},
|
|
169
158
|
): string {
|
|
170
159
|
const anatomy = surfaceAnatomy[family];
|
|
171
|
-
const base =
|
|
172
|
-
|
|
173
|
-
|
|
160
|
+
const base =
|
|
161
|
+
region === "body" && flush
|
|
162
|
+
? structuralBodyRhythm
|
|
163
|
+
: anatomy.regions[layout][region];
|
|
164
|
+
const regionClasses =
|
|
165
|
+
region === "body" && !flush ? `${base} ${structuralBodyRhythm}` : base;
|
|
166
|
+
if (layout !== "sectioned") return regionClasses;
|
|
167
|
+
if (region === "header") return `${regionClasses} ${headerDivider}`;
|
|
174
168
|
if (region === "footer")
|
|
175
|
-
return [
|
|
176
|
-
|
|
169
|
+
return [regionClasses, footerDivider, anatomy.footerBand]
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
.join(" ");
|
|
172
|
+
return regionClasses;
|
|
177
173
|
}
|
|
178
174
|
|
|
179
175
|
/** Escala visual do título — o nível do heading vem da composição, nunca daqui. */
|
|
@@ -191,8 +187,7 @@ export function surfaceDescriptionClasses(family: SurfaceFamily): string {
|
|
|
191
187
|
/**
|
|
192
188
|
* A linha do header: mídia, heading que absorve a folga, ações no extremo oposto.
|
|
193
189
|
*
|
|
194
|
-
* `flex-1`
|
|
195
|
-
* barra —, a linha vira um item que se mede pelo conteúdo e as ações param no meio.
|
|
190
|
+
* `flex-1` mantém o heading como região elástica e as ações no extremo oposto.
|
|
196
191
|
*/
|
|
197
192
|
export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
|
|
198
193
|
return `flex min-w-0 flex-1 items-center ${surfaceAnatomy[family].headerGap}`;
|
|
@@ -201,6 +196,22 @@ export function surfaceHeaderRowClasses(family: SurfaceFamily): string {
|
|
|
201
196
|
/** Ações do header e do footer: fora do fluxo de compressão, na escala densa. */
|
|
202
197
|
export const surfaceActionsClasses = "flex shrink-0 items-center gap-2";
|
|
203
198
|
|
|
199
|
+
/**
|
|
200
|
+
* A `legend` de uma Surface `fieldset` delimitada (ADR 0045).
|
|
201
|
+
*
|
|
202
|
+
* - `notch`: moldura contínua. A legend nativa ocupa a borda, afastada do canto pelo
|
|
203
|
+
* mesmo recuo lateral das regiões; a região seguinte recebe o intervalo que a legend
|
|
204
|
+
* não tem como declarar.
|
|
205
|
+
* - `band`: a legend é a faixa do header dentro da superfície. Uma legend só ocupa a
|
|
206
|
+
* largura inteira fora do fluxo de borda, e a região seguinte precisa limpar o float.
|
|
207
|
+
*/
|
|
208
|
+
export const surfaceLegendClasses = {
|
|
209
|
+
notch: "ml-2 w-auto px-2",
|
|
210
|
+
afterNotch: "pt-3",
|
|
211
|
+
band: "float-left w-full",
|
|
212
|
+
afterBand: "clear-left",
|
|
213
|
+
} as const;
|
|
214
|
+
|
|
204
215
|
/**
|
|
205
216
|
* O layout é do root e chega aos slots por contexto — ADR 0023. Slot não recebe
|
|
206
217
|
* `layout`: o divisor depende de quem está do outro lado, então a decisão precisa valer
|
|
@@ -230,10 +241,9 @@ export function SurfaceLayoutProvider({
|
|
|
230
241
|
}
|
|
231
242
|
|
|
232
243
|
const familyDefaultLayout: Record<SurfaceFamily, SurfaceLayout> = {
|
|
233
|
-
|
|
244
|
+
surface: "continuous",
|
|
234
245
|
dialog: "sectioned",
|
|
235
246
|
drawer: "sectioned",
|
|
236
|
-
pane: "sectioned",
|
|
237
247
|
};
|
|
238
248
|
|
|
239
249
|
/** O layout vigente: o declarado pelo root da superfície, ou o default da família. */
|
|
@@ -45,7 +45,6 @@ export function SurfaceAssistant({
|
|
|
45
45
|
key="surface-content"
|
|
46
46
|
id="surface-content"
|
|
47
47
|
grow
|
|
48
|
-
inset="none"
|
|
49
48
|
className={cn("h-full", contentClassName)}
|
|
50
49
|
>
|
|
51
50
|
{children}
|
|
@@ -57,13 +56,12 @@ export function SurfaceAssistant({
|
|
|
57
56
|
initialSize={initialSize}
|
|
58
57
|
minSize={minSize}
|
|
59
58
|
maxSize={maxSize}
|
|
60
|
-
|
|
61
|
-
className={cn("h-full bg-background", assistantClassName)}
|
|
59
|
+
className={cn("h-full", assistantClassName)}
|
|
62
60
|
>
|
|
63
61
|
<aside
|
|
64
62
|
data-slot="surface-assistant-panel"
|
|
65
63
|
aria-label={assistantLabel}
|
|
66
|
-
className="h-full min-w-0 overflow-hidden"
|
|
64
|
+
className="h-full min-w-0 overflow-hidden bg-background"
|
|
67
65
|
>
|
|
68
66
|
{assistant}
|
|
69
67
|
</aside>
|