@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/bin/lib/check.mjs +178 -94
  3. package/bin/lib/copy.mjs +2970 -1801
  4. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
  5. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
  6. package/docs/adr/0045-surface-replaces-content-and-card.md +3 -0
  7. package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  8. package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
  9. package/docs/adr/0063-metadados-das-superficies.md +44 -0
  10. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  11. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  12. package/docs/adr/README.md +8 -0
  13. package/package.json +1 -1
  14. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  15. package/registry/skills/build-opus-ui/references/evaluations.md +21 -10
  16. package/registry/skills/build-opus-ui/references/ui-patterns.md +33 -28
  17. package/src/core/presentation.ts +938 -32
  18. package/src/core/types.ts +2 -1
  19. package/src/ui/components/patterns/form.tsx +2 -0
  20. package/src/ui/components/patterns/grouped-list.ts +2 -2
  21. package/src/ui/components/patterns/list.tsx +588 -365
  22. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  23. package/src/ui/components/patterns/page.tsx +238 -181
  24. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  25. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  26. package/src/ui/components/patterns/presentation.tsx +1142 -229
  27. package/src/ui/components/patterns/structural-header.tsx +47 -34
  28. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  29. package/src/ui/components/patterns/surface.tsx +55 -37
  30. package/src/ui/components/patterns/trigger.tsx +10 -2
  31. package/src/ui/components/primitives/close-button.tsx +11 -3
  32. package/src/ui/components/primitives/command.tsx +2 -8
  33. package/src/ui/components/primitives/detail.tsx +1 -1
  34. package/src/ui/components/primitives/dialog.tsx +59 -89
  35. package/src/ui/components/primitives/drawer.tsx +52 -87
  36. package/src/ui/components/primitives/field.tsx +1 -1
  37. package/src/ui/components/primitives/pagination.tsx +3 -2
  38. package/src/ui/components/primitives/table.tsx +3 -3
  39. package/src/ui/components/primitives/tabs.tsx +13 -2
  40. package/src/ui/docs/content/action-form.md +2 -1
  41. package/src/ui/docs/content/action-list.md +146 -73
  42. package/src/ui/docs/content/action-trigger.md +5 -0
  43. package/src/ui/docs/content/button.md +5 -6
  44. package/src/ui/docs/content/command.md +62 -50
  45. package/src/ui/docs/content/compositions.md +129 -459
  46. package/src/ui/docs/content/dialog.md +72 -101
  47. package/src/ui/docs/content/drawer.md +69 -92
  48. package/src/ui/docs/content/field.md +2 -1
  49. package/src/ui/docs/content/page.md +207 -386
  50. package/src/ui/docs/content/presentation.md +371 -23
  51. package/src/ui/docs/content/separator.md +1 -1
  52. package/src/ui/docs/content/surface.md +94 -116
  53. package/src/ui/docs/content/table.md +7 -0
  54. package/src/ui/docs/content/tokens.md +18 -4
  55. package/src/ui/drivers/context.tsx +11 -4
  56. package/src/ui/drivers/react.tsx +27 -7
  57. package/src/ui/meta.ts +5 -5
  58. package/src/ui/react.tsx +38 -8
  59. package/src/ui/theme.css +44 -33
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Cabeçalho estrutural (interno): a anatomia que Page, Surface, Dialog, Drawer e Pane
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
- * em larguras amplas e empilha abaixo do contexto nas estreitas. Cada família escolhe quais slots
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: "text-3xl font-semibold tracking-tight",
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-4",
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
- // `min-w-0` aqui é o que deixa a trilha do PageNavigation ceder e truncar; sem ele o
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
- contentClassName === undefined &&
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
- layout
395
+ withMeta
363
396
  ) : (
364
397
  <Wrapper
365
398
  data-slot={`${slot}-header-content`}
366
399
  className={contentClassName}
367
400
  >
368
- {layout}
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
- /** Recuo e ritmo por layout. O recuo lateral é comum às três regiões. */
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
- 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
- },
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
- regions: {
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
- regions: {
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
- regions: {
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
- /** Classes da região, já com o divisor que o layout deriva da vizinhança. */
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
- { flush = false }: { flush?: boolean } = {},
60
+ {
61
+ flush = false,
62
+ bounded = true,
63
+ }: { flush?: boolean; bounded?: boolean } = {},
187
64
  ): string {
188
- const anatomy = surfaceAnatomy[family];
189
- const base =
190
- region === "body" && flush
191
- ? structuralBodyRhythm
192
- : anatomy.regions[layout][region];
193
- const regionClasses =
194
- region === "body" && !flush ? `${base} ${structuralBodyRhythm}` : base;
195
- if (layout !== "sectioned") return regionClasses;
196
- if (region === "header") return `${regionClasses} ${headerDivider}`;
197
- if (region === "footer")
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
- /** Escala visual do título — o nível do heading vem da composição, nunca daqui. */
205
- export function surfaceTitleClasses(
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} ${surfaceAnatomy[family].headerMinHeight}`;
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
- }