@softize/opus 30.0.0 → 31.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/bin/lib/check.mjs +109 -101
  3. package/bin/lib/copy.mjs +2973 -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/0060-surface-composition-separates-chrome-and-intro.md +142 -0
  7. package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
  8. package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
  9. package/docs/adr/0066-civil-date-input-and-calendar-list.md +28 -0
  10. package/docs/adr/README.md +6 -0
  11. package/package.json +1 -1
  12. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  13. package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
  15. package/src/core/presentation.ts +938 -32
  16. package/src/core/types.ts +2 -0
  17. package/src/ui/components/patterns/form.tsx +22 -1
  18. package/src/ui/components/patterns/grouped-list.ts +2 -2
  19. package/src/ui/components/patterns/list-calendar.tsx +129 -0
  20. package/src/ui/components/patterns/list.tsx +612 -382
  21. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  22. package/src/ui/components/patterns/page.tsx +227 -197
  23. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  24. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  25. package/src/ui/components/patterns/presentation.tsx +1142 -229
  26. package/src/ui/components/patterns/structural-header.tsx +23 -32
  27. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  28. package/src/ui/components/patterns/surface.tsx +19 -29
  29. package/src/ui/components/patterns/trigger.tsx +10 -2
  30. package/src/ui/components/primitives/calendar.tsx +39 -18
  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/date-input.tsx +276 -0
  34. package/src/ui/components/primitives/detail.tsx +1 -1
  35. package/src/ui/components/primitives/dialog.tsx +19 -81
  36. package/src/ui/components/primitives/drawer.tsx +17 -79
  37. package/src/ui/components/primitives/field.tsx +1 -1
  38. package/src/ui/components/primitives/pagination.tsx +3 -2
  39. package/src/ui/components/primitives/table.tsx +1 -1
  40. package/src/ui/components/primitives/tabs.tsx +13 -2
  41. package/src/ui/docs/content/action-form.md +6 -1
  42. package/src/ui/docs/content/action-list.md +156 -68
  43. package/src/ui/docs/content/action-trigger.md +5 -0
  44. package/src/ui/docs/content/button.md +5 -6
  45. package/src/ui/docs/content/calendar-month-view.md +50 -0
  46. package/src/ui/docs/content/calendar.md +6 -1
  47. package/src/ui/docs/content/command.md +62 -50
  48. package/src/ui/docs/content/compositions.md +129 -459
  49. package/src/ui/docs/content/date-input.md +67 -0
  50. package/src/ui/docs/content/dialog.md +44 -112
  51. package/src/ui/docs/content/drawer.md +44 -103
  52. package/src/ui/docs/content/field.md +2 -1
  53. package/src/ui/docs/content/input.md +3 -0
  54. package/src/ui/docs/content/page.md +204 -412
  55. package/src/ui/docs/content/presentation.md +371 -23
  56. package/src/ui/docs/content/separator.md +1 -1
  57. package/src/ui/docs/content/surface.md +65 -120
  58. package/src/ui/docs/content/tokens.md +18 -4
  59. package/src/ui/docs/registry.tsx +38 -9
  60. package/src/ui/drivers/context.tsx +11 -4
  61. package/src/ui/drivers/react.tsx +27 -7
  62. package/src/ui/meta.ts +17 -6
  63. package/src/ui/react.tsx +41 -8
  64. 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,9 @@ import {
16
16
  type ReactElement,
17
17
  type ReactNode,
18
18
  } from "react";
19
+ import { structuralTitleClasses } from "./surface-anatomy.ts";
19
20
  import { cn } from "../../lib/cn.ts";
21
+ import { TabsHeaderContext } from "../primitives/tabs.tsx";
20
22
 
21
23
  /** Metadados contextuais, separados da orientação textual e da linha de ações. */
22
24
  export const structuralMetaClasses =
@@ -30,10 +32,10 @@ export const structuralHeaderClasses: Record<
30
32
  { title: string; description: string; count: string; actions: string }
31
33
  > = {
32
34
  page: {
33
- title: "text-3xl font-semibold tracking-tight",
35
+ title: structuralTitleClasses,
34
36
  description: "mt-1 truncate text-sm text-muted-foreground",
35
37
  count: "font-mono text-base text-muted-foreground/60",
36
- actions: "flex w-auto shrink-0 items-center gap-4",
38
+ actions: "flex w-auto shrink-0 items-center gap-2",
37
39
  },
38
40
  section: {
39
41
  title: "text-sm font-semibold",
@@ -73,6 +75,8 @@ export interface StructuralHeaderProps extends HTMLAttributes<HTMLDivElement> {
73
75
  titleRequired?: boolean;
74
76
  /** Classes da linha `inline` — as famílias com anatomia própria trazem a sua (ADR 0023). */
75
77
  rowClassName?: string;
78
+ /** Lista de abas sob o título; o Tabs ancestral governa seleção e painéis. */
79
+ tabs?: ReactNode;
76
80
  /** Nó interno acrescentado ao fim da linha, depois das ações (o botão de fechar). */
77
81
  trailing?: ReactNode;
78
82
  /**
@@ -108,12 +112,18 @@ export function StructuralHeader({
108
112
  titleRequired = true,
109
113
  rowClassName,
110
114
  trailing,
115
+ tabs,
111
116
  allowFreeContent = false,
112
117
  invalidMessage,
113
118
  className,
114
119
  children,
115
120
  ...props
116
121
  }: StructuralHeaderProps): ReactElement {
122
+ const hasTabs = tabs !== undefined && tabs !== null && tabs !== false;
123
+
124
+ if (hasTabs && Root === "legend") {
125
+ throw new Error("SurfaceHeader de fieldset não aceita abas.");
126
+ }
117
127
  const Wrapper = Root === "legend" ? "span" : "div";
118
128
  const nodes = flatten(children).filter(
119
129
  (node) => typeof node !== "string" || node.trim() !== "",
@@ -281,8 +291,7 @@ export function StructuralHeader({
281
291
  {inlineContext === null &&
282
292
  leadingPlacement === "inline" &&
283
293
  leading.length > 0 && (
284
- // `min-w-0` aqui é o que deixa a trilha do PageNavigation ceder e truncar; sem ele o
285
- // wrapper assume o min-content do breadcrumb e a compressão toda cai sobre o título.
294
+ // A região de navegação pode ceder espaço sem comprimir o título além do container.
286
295
  <Wrapper
287
296
  data-slot={`${slot}-navigation`}
288
297
  className={cn("flex min-w-0 items-center", !heading && "flex-1")}
@@ -367,7 +376,8 @@ export function StructuralHeader({
367
376
  <Root
368
377
  data-slot={`${slot}-header`}
369
378
  className={cn(
370
- metas.length === 0 &&
379
+ !hasTabs &&
380
+ metas.length === 0 &&
371
381
  contentClassName === undefined &&
372
382
  leadingPlacement !== "inline" &&
373
383
  leading.length === 0 &&
@@ -377,6 +387,7 @@ export function StructuralHeader({
377
387
  heading ? "justify-between" : "justify-end",
378
388
  ),
379
389
  className,
390
+ hasTabs && "block border-b pb-0",
380
391
  )}
381
392
  {...props}
382
393
  >
@@ -390,31 +401,11 @@ export function StructuralHeader({
390
401
  {withMeta}
391
402
  </Wrapper>
392
403
  )}
404
+ {hasTabs && (
405
+ <div data-slot={`${slot}-header-tabs`} className="mt-4 min-w-0">
406
+ <TabsHeaderContext.Provider value>{tabs}</TabsHeaderContext.Provider>
407
+ </div>
408
+ )}
393
409
  </Root>
394
410
  );
395
411
  }
396
-
397
- /** A intro visível abre o corpo; wrappers opacos são verificados quando materializam seus slots. */
398
- export function validateSurfaceIntro(
399
- children: ReactNode,
400
- intro: ElementType,
401
- name: string,
402
- ): void {
403
- const flatten = (nodes: ReactNode): ReactNode[] =>
404
- Children.toArray(nodes).flatMap((node) =>
405
- isValidElement(node) && node.type === Fragment
406
- ? flatten((node.props as { children?: ReactNode }).children)
407
- : [node],
408
- );
409
- const nodes = flatten(children).filter(
410
- (node) => typeof node !== "string" || node.trim() !== "",
411
- );
412
- const indexes = nodes.flatMap((node, index) =>
413
- isValidElement(node) && node.type === intro ? [index] : [],
414
- );
415
- if (indexes.length > 1 || (indexes.length === 1 && indexes[0] !== 0)) {
416
- throw new Error(
417
- `${name} aceita uma única intro como primeiro bloco do corpo.`,
418
- );
419
- }
420
- }
@@ -1,43 +1,7 @@
1
- /**
2
- * Anatomia de superfície (interno): as medidas que Surface, Dialog e Drawer
3
- * compartilham — ADRs 0023, 0045 e 0047.
4
- *
5
- * Aqui mora o que antes era escrito seis vezes: recuo de cada região, ritmo vertical,
6
- * escala do título, posição das ações e a regra dos divisores. As famílias públicas
7
- * consomem estas tabelas; nenhuma reescreve as classes.
8
- *
9
- * Dois layouts, e cada um é um pacote indivisível:
10
- *
11
- * - `continuous` — sem divisores. O ritmo separa: o header se aproxima do corpo e a
12
- * última região — seja ela o header, o body ou o footer — fecha o espaço de baixo com
13
- * o mesmo respiro do topo. É a superfície de leitura.
14
- * - `sectioned` — regiões simétricas, com divisor entre as que forem adjacentes. É o
15
- * modal, o drawer e o painel.
16
- *
17
- * O recuo lateral é o mesmo nas três regiões da mesma superfície; é isso que a suíte
18
- * transversal verifica.
19
- */
20
-
21
- import {
22
- createContext,
23
- createElement,
24
- useContext,
25
- type ReactElement,
26
- type ReactNode,
27
- } from "react";
28
- import { FillBoundary } from "./fill-region.ts";
29
-
1
+ /** Anatomia única de Page, Surface, Dialog e Drawer — ADR 0060. */
30
2
  export type SurfaceFamily = "surface" | "dialog" | "drawer" | "page";
31
- export type SurfaceLayout = "continuous" | "sectioned";
32
3
  export type SurfaceRegion = "header" | "body" | "footer";
33
4
 
34
- /**
35
- * Divisores: a região desenha a própria linha quando existe uma vizinha do outro lado.
36
- * Um header sozinho não fecha nada; um footer que é a única região também não.
37
- *
38
- * O header suprime a própria linha quando o vizinho imediato é um footer — os dois
39
- * juntos desenhariam duas bordas encostadas, uma sobre a outra.
40
- */
41
5
  const headerDivider = [
42
6
  "[&:not(:last-child)]:border-b",
43
7
  "[&:not(:last-child)]:border-border",
@@ -49,238 +13,84 @@ const headerDivider = [
49
13
  const footerDivider =
50
14
  "[&:not(:first-child)]:border-t [&:not(:first-child)]:border-border";
51
15
 
52
- interface RegionAnatomy {
53
- header: string;
54
- body: string;
55
- footer: string;
56
- }
57
-
58
16
  interface FamilyAnatomy {
59
- /** 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
- }
@@ -7,7 +7,6 @@
7
7
  *
8
8
  * framed borda, raio e recorte;
9
9
  * elevation fundo próprio (`raised`) ou o fundo de quem hospeda (`none`);
10
- * layout ritmo contínuo ou faixas com divisores derivados.
11
10
  *
12
11
  * Três formas de compor produzem a mesma anatomia:
13
12
  *
@@ -52,7 +51,6 @@ import {
52
51
  surfaceRegionClasses,
53
52
  structuralRegionSpacing,
54
53
  surfaceTitleClasses,
55
- type SurfaceLayout,
56
54
  } from "./surface-anatomy.ts";
57
55
 
58
56
  export type SurfaceElement = "div" | "section" | "article" | "fieldset";
@@ -64,7 +62,6 @@ interface SurfaceContextValue {
64
62
  /** Tem moldura ou elevação: as regiões carregam o recuo. */
65
63
  bounded: boolean;
66
64
  framed: boolean;
67
- layout: SurfaceLayout;
68
65
  level: HeadingLevel;
69
66
  titleId: string;
70
67
  variant: "page" | "section";
@@ -84,11 +81,9 @@ function requiredContext<T>(
84
81
  }
85
82
 
86
83
  /** Como a `legend` de um fieldset delimitado se relaciona com a moldura. */
87
- function legendPlacement(
88
- surface: SurfaceContextValue,
89
- ): "notch" | "band" | null {
84
+ function legendPlacement(surface: SurfaceContextValue): "band" | null {
90
85
  if (surface.element !== "fieldset" || !surface.bounded) return null;
91
- return surface.framed && surface.layout === "continuous" ? "notch" : "band";
86
+ return "band";
92
87
  }
93
88
 
94
89
  interface SurfaceBaseProps {
@@ -99,8 +94,6 @@ interface SurfaceBaseProps {
99
94
  * o fundo de quem hospeda.
100
95
  */
101
96
  elevation?: SurfaceElevation;
102
- /** Anatomia da superfície inteira: `continuous` (default) ou `sectioned`. */
103
- layout?: SurfaceLayout;
104
97
  className?: string;
105
98
  children?: ReactNode;
106
99
  }
@@ -203,7 +196,6 @@ export function Surface({
203
196
  asChild = false,
204
197
  framed = false,
205
198
  elevation = "none",
206
- layout = "continuous",
207
199
  level = 2,
208
200
  title,
209
201
  media,
@@ -224,11 +216,6 @@ export function Surface({
224
216
  const element: SurfaceElement | null = asChild ? null : (as ?? "div");
225
217
  const raised = elevation === "raised";
226
218
  const bounded = framed || raised;
227
- if (layout === "sectioned" && !bounded) {
228
- throw new Error(
229
- 'Surface layout="sectioned" exige framed ou elevation="raised".',
230
- );
231
- }
232
219
 
233
220
  const shorthand = title !== undefined;
234
221
  if (
@@ -299,7 +286,6 @@ export function Surface({
299
286
  element,
300
287
  bounded,
301
288
  framed,
302
- layout,
303
289
  level,
304
290
  titleId: resolvedTitleId,
305
291
  variant,
@@ -342,13 +328,12 @@ export function Surface({
342
328
  data-as={element ?? undefined}
343
329
  data-framed={framed}
344
330
  data-elevation={elevation}
345
- data-layout={layout}
346
331
  {...(link ? fill.attributes : {})}
347
332
  aria-labelledby={named ? resolvedTitleId : undefined}
348
333
  className={cn(
349
334
  framed && "rounded-xl border border-border",
350
335
  // O divisor e o conteúdo sem recuo encostam na borda: o raio precisa recortar.
351
- framed && layout === "sectioned" && "overflow-hidden",
336
+ framed && "overflow-hidden",
352
337
  framed && "has-[>[data-flush=true]]:overflow-hidden",
353
338
  raised &&
354
339
  "bg-card text-card-foreground [--surface-background:var(--card)]",
@@ -375,7 +360,13 @@ export function Surface({
375
360
  );
376
361
  }
377
362
 
378
- export type SurfaceHeaderProps = Omit<HTMLAttributes<HTMLDivElement>, "title">;
363
+ export type SurfaceHeaderProps = Omit<
364
+ HTMLAttributes<HTMLDivElement>,
365
+ "title"
366
+ > & {
367
+ /** TabsList abaixo do título; indisponível em fieldset. */
368
+ tabs?: ReactNode;
369
+ };
379
370
 
380
371
  const legendInvalidMessage =
381
372
  "SurfaceHeader de fieldset aceita somente SurfaceMedia e SurfaceTitle; contador, descrição, metadados e ações ficam fora da legenda.";
@@ -420,9 +411,9 @@ export function SurfaceHeader({
420
411
  rowClassName={surfaceHeaderRowClasses("surface")}
421
412
  {...(legend ? { invalidMessage: legendInvalidMessage } : {})}
422
413
  className={cn(
423
- surface.bounded &&
424
- placement !== "notch" &&
425
- surfaceRegionClasses("surface", surface.layout, "header"),
414
+ surfaceRegionClasses("surface", "header", {
415
+ bounded: surface.bounded,
416
+ }),
426
417
  placement !== null && surfaceLegendClasses[placement],
427
418
  className,
428
419
  )}
@@ -479,7 +470,7 @@ export function SurfaceTitle({
479
470
  );
480
471
  const legend = surface.element === "fieldset";
481
472
  const Heading = legend ? "span" : headingTags[surface.level];
482
- usePageHeadingPresence(!legend && surface.level === 1);
473
+ usePageHeadingPresence(!legend && surface.level === 1, "surface");
483
474
  return (
484
475
  <Heading
485
476
  id={id ?? surface.titleId}
@@ -487,7 +478,7 @@ export function SurfaceTitle({
487
478
  className={cn(
488
479
  surface.variant === "page"
489
480
  ? structuralHeaderClasses.page.title
490
- : surfaceTitleClasses("surface", surface.layout),
481
+ : surfaceTitleClasses("surface"),
491
482
  className,
492
483
  )}
493
484
  {...props}
@@ -605,10 +596,10 @@ export function SurfaceBody({
605
596
  data-flush={flush ? "true" : undefined}
606
597
  {...fill.attributes}
607
598
  className={cn(
608
- surfaceRegionClasses("surface", surface.layout, "body", {
609
- flush: !surface.bounded || flush,
599
+ surfaceRegionClasses("surface", "body", {
600
+ bounded: surface.bounded,
601
+ flush,
610
602
  }),
611
- placement === "notch" && surfaceLegendClasses.afterNotch,
612
603
  placement === "band" && surfaceLegendClasses.afterBand,
613
604
  fill.filling && fillRegionClassName,
614
605
  className,
@@ -636,8 +627,7 @@ export function SurfaceFooter({
636
627
  data-slot="surface-footer"
637
628
  className={cn(
638
629
  "flex items-center justify-end gap-2",
639
- surface.bounded &&
640
- surfaceRegionClasses("surface", surface.layout, "footer"),
630
+ surfaceRegionClasses("surface", "footer", { bounded: surface.bounded }),
641
631
  placement === "band" && surfaceLegendClasses.afterBand,
642
632
  className,
643
633
  )}
@@ -52,6 +52,8 @@ export interface ActionTriggerProps<TInput, TData> {
52
52
  input: TInput;
53
53
  /** Texto do botão. Default usa action.label. */
54
54
  label?: string;
55
+ /** Nome acessível completo quando o texto visível é abreviado. */
56
+ "aria-label"?: string;
55
57
  /** Contexto visual. Por padrão, ações destrutivas usam `danger`. */
56
58
  context?: ButtonProps["context"];
57
59
  /** Tratamento visual do Button. */
@@ -70,6 +72,8 @@ export interface ActionTriggerProps<TInput, TData> {
70
72
  actionLabel?: string;
71
73
  cancelLabel?: string;
72
74
  };
75
+ /** Mensagem de sucesso desta ocorrência; substitui a mensagem do contrato. */
76
+ successMessage?: string;
73
77
  /** Callback adicional pós-sucesso (cache já foi invalidado). */
74
78
  onSuccess?: (data: TData) => void;
75
79
  /** Notifica o renderer que coordena bloqueio entre várias actions. */
@@ -87,12 +91,14 @@ export function ActionTrigger<TInput, TData>({
87
91
  action,
88
92
  input,
89
93
  label,
94
+ "aria-label": ariaLabel,
90
95
  context,
91
96
  variant,
92
97
  size,
93
98
  disabled = false,
94
99
  confirm: confirmProp,
95
100
  onSuccess,
101
+ successMessage,
96
102
  onLoadingChange,
97
103
  icon,
98
104
  itemLabel,
@@ -102,7 +108,9 @@ export function ActionTrigger<TInput, TData>({
102
108
  const [open, setOpen] = useState(false);
103
109
  const { trigger, isLoading } = useTriggerAction(action, {
104
110
  onSuccess: (data) => {
105
- toast.success(msgText(action.messages?.success, "Concluído"));
111
+ toast.success(
112
+ successMessage ?? msgText(action.messages?.success, "Concluído"),
113
+ );
106
114
  onSuccess?.(data);
107
115
  setOpen(false);
108
116
  },
@@ -184,7 +192,7 @@ export function ActionTrigger<TInput, TData>({
184
192
  size={size ?? (icon !== undefined ? "icon-xs" : "default")}
185
193
  busy={isLoading}
186
194
  disabled={disabled}
187
- aria-label={icon !== undefined ? buttonLabel : undefined}
195
+ aria-label={ariaLabel ?? (icon !== undefined ? buttonLabel : undefined)}
188
196
  onClick={(e) => {
189
197
  e.stopPropagation();
190
198
  onClick();