@softize/opus 30.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 (55) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/bin/lib/check.mjs +109 -101
  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/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/README.md +4 -0
  10. package/package.json +1 -1
  11. package/registry/skills/build-opus-ui/SKILL.md +21 -19
  12. package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
  13. package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
  14. package/src/core/presentation.ts +938 -32
  15. package/src/core/types.ts +1 -0
  16. package/src/ui/components/patterns/form.tsx +2 -0
  17. package/src/ui/components/patterns/grouped-list.ts +2 -2
  18. package/src/ui/components/patterns/list.tsx +572 -358
  19. package/src/ui/components/patterns/page-heading-context.tsx +12 -5
  20. package/src/ui/components/patterns/page.tsx +227 -197
  21. package/src/ui/components/patterns/presentation-fields.tsx +246 -0
  22. package/src/ui/components/patterns/presentation-sections.tsx +494 -0
  23. package/src/ui/components/patterns/presentation.tsx +1142 -229
  24. package/src/ui/components/patterns/structural-header.tsx +23 -32
  25. package/src/ui/components/patterns/surface-anatomy.ts +27 -217
  26. package/src/ui/components/patterns/surface.tsx +19 -29
  27. package/src/ui/components/patterns/trigger.tsx +10 -2
  28. package/src/ui/components/primitives/close-button.tsx +11 -3
  29. package/src/ui/components/primitives/command.tsx +2 -8
  30. package/src/ui/components/primitives/detail.tsx +1 -1
  31. package/src/ui/components/primitives/dialog.tsx +19 -81
  32. package/src/ui/components/primitives/drawer.tsx +17 -79
  33. package/src/ui/components/primitives/field.tsx +1 -1
  34. package/src/ui/components/primitives/pagination.tsx +3 -2
  35. package/src/ui/components/primitives/table.tsx +1 -1
  36. package/src/ui/components/primitives/tabs.tsx +13 -2
  37. package/src/ui/docs/content/action-form.md +2 -1
  38. package/src/ui/docs/content/action-list.md +134 -68
  39. package/src/ui/docs/content/action-trigger.md +5 -0
  40. package/src/ui/docs/content/button.md +5 -6
  41. package/src/ui/docs/content/command.md +62 -50
  42. package/src/ui/docs/content/compositions.md +129 -459
  43. package/src/ui/docs/content/dialog.md +44 -112
  44. package/src/ui/docs/content/drawer.md +44 -103
  45. package/src/ui/docs/content/field.md +2 -1
  46. package/src/ui/docs/content/page.md +204 -412
  47. package/src/ui/docs/content/presentation.md +371 -23
  48. package/src/ui/docs/content/separator.md +1 -1
  49. package/src/ui/docs/content/surface.md +65 -120
  50. package/src/ui/docs/content/tokens.md +18 -4
  51. package/src/ui/drivers/context.tsx +11 -4
  52. package/src/ui/drivers/react.tsx +27 -7
  53. package/src/ui/meta.ts +5 -5
  54. package/src/ui/react.tsx +34 -8
  55. package/src/ui/theme.css +44 -33
package/CHANGELOG.md CHANGED
@@ -7,6 +7,122 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
7
7
  `opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
8
8
  mudança cobra do seu código.
9
9
 
10
+ ## 31.0.0 — 2026-10-20
11
+
12
+ - `PageBack` aceita `onBack` e `disabled` para retorno controlado por uma pilha. `href` continua disponível para destinos navegáveis; `PageBackProps` é uma união que impede combinar as duas formas.
13
+
14
+ - Apresentações embutidas preservam ações de cabeçalho, rodapé e envio de formulário. `successMessage` pertence somente a comandos de execução direta.
15
+
16
+ - `ActionList` omite o rodapé quando o total é zero, removendo a paginação sem resultados e o divisor adicional junto da moldura vazia.
17
+
18
+ - A Page aberta passa a separar cabeçalho e conteúdo por `1,5rem`. As demais regiões e as superfícies com moldura preservam o espaçamento atual.
19
+
20
+ - `FieldDescription`, usado pelos hints dos formulários, passa a usar `text-xs` (`0,75rem`). Rótulos, controles e mensagens de erro preservam seus tamanhos.
21
+
22
+ - `PageActions` passa a compor `ButtonGroup` no modo espaçado. Voltar e ações da página compartilham o grupo, preservando a ordem e adotando o espaçamento padrão de `0,25rem`.
23
+
24
+ - O título da Page aberta passa de `1,5rem` para `1,875rem`. Títulos de Surface, Drawer e Dialog mantêm a escala atual.
25
+
26
+ - As superfícies, bordas e estados neutros do dark recebem uma redução proporcional de 5% na luminosidade HSL sobre a calibração de referência. Texto, cores semânticas e tema claro permanecem iguais.
27
+
28
+ - O dark usa conteúdo `#171717`, sidebar interna `#1c1c1c` e borda `#333333`, após a redução de luminosidade. `sidebar-global` acompanha o canvas nos dois temas, dando continuidade visual à navegação geral sem transparência.
29
+
30
+ - O token `sidebar` oferece fundo opaco para navegação interna: `#fcfcfc` no light e o degrau de `card` no dark. No light, hover, seleção e pressão usam `#f4f4f4`, `#f0f0f0` e `#e8e8e8`. A navegação global usa `sidebar-global`, que acompanha o canvas.
31
+
32
+ - O tema claro usa branco puro (`#fff`) no fundo geral, nos controles, cards e popovers. Sidebar, estados neutros e bordas preservam os tons calibrados.
33
+
34
+ - A escala neutra tem mais contraste de base em fundos discretos, estados de interação e bordas no tema claro. No escuro, hover se distingue de `muted` e as bordas ficam mais definidas. Os tokens neutros preservam os papéis de superfície e interação; a calibração padrão diferencia os temas claro e escuro.
35
+
36
+ - DetailField vertical usa o mesmo espaço de `0,375rem` entre rótulo e valor que Field usa entre rótulo e controle.
37
+
38
+ - Cabeçalhos de tabela preservam `0,5rem` de padding vertical ao quebrar texto em mais de uma linha. A altura acompanha o conteúdo, mantendo o cabeçalho fixo durante a rolagem de ActionList.
39
+
40
+ - O rodapé de ActionList usa padding vertical de `0,75rem`. Pagination destaca a página atual com fundo neutro suave e peso semibold, sem borda; os demais controles permanecem ghost.
41
+
42
+ - Dialog e Drawer usam `xs` (`1,5rem`) no botão de fechar, com o tratamento neutro `subtle` e seus estados de interação, preservando a escala normal das demais ações do cabeçalho.
43
+
44
+ - ActionList uniformiza em `1rem` o espaçamento entre toolbar e resultados em tabelas, cards e listas de itens.
45
+
46
+ - Consolida Presentation com conteúdo de linha e toolbar tipados, regiões fill, abas embedded,
47
+ fontes externas isoladas por adaptador e seletores compostos de rota. Preserva o observador
48
+ durante refetch e aplica largura ampla ao Drawer; comandos admitem mensagem de sucesso.
49
+
50
+ - Presentation admite campos especializados vinculados ao contrato e resumos que recebem o input efetivo de ActionList; validação e autorização permanecem nos patterns existentes.
51
+
52
+ `listStateToInput` compartilha com os hospedeiros a materialização do recorte usado pelo ActionList,
53
+ preservando filtros e paginação ao carregar detalhes roteados.
54
+
55
+ Presentation permite detalhe local de linha por adaptador tipado (`body.inspect`), filtros iniciais
56
+ (`body.filterDefaults`) e opções de lookup com rótulos compostos e opções estáticas anteriores.
57
+ Lookups exigem actions com `paginate: false`, garantindo opções completas.
58
+ O renderer verifica a permissão da consulta de lookup antes de executá-la. ActionForm aceita
59
+ `widget: 'password'` para cadastro e redefinição de senha. Comandos de linha usam o rótulo do
60
+ dicionário do contrato quando o identificador apresentado é um código.
61
+
62
+ Presentation aceita arquivo autoral `presentations/*.json`, validado pelo schema público e
63
+ rastreado por `opus copy`. A extração preserva o caminho e as linhas do JSON.
64
+
65
+ **Breaking:** o renderer agora considera também `requires` da action, sem depender da repetição
66
+ no layout. Forneça `can` ao host para as permissões explícitas; sem esse callback os controles e
67
+ corpos protegidos não montam. `requires` da apresentação admite alternativas em array e continua
68
+ cumulativo com o contrato. `canAccessPresentation` permite verificar rotas antes de buscar dados.
69
+
70
+ Presentation amplia as seções declarativas para controles específicos, listas de actions,
71
+ metadados e ações de cabeçalho, controles à direita dos itens e painel lateral. `page.layout`
72
+ transporta a escolha de rolagem no JSON. Abas podem referenciar seções declaradas, além de actions.
73
+ Os adaptadores runtime preservam handlers e dados; a declaração determina a anatomia.
74
+
75
+ Page usa uma única aparência aberta: padding de 2,5rem, título de 1,875rem e intervalo de
76
+ 1,5rem após o cabeçalho e 1rem entre as demais regiões, sem divisores. `flow` rola a página inteira; `fill` preserva o header
77
+ e a altura disponível às listas. Drawer e Dialog mantêm suas superfícies delimitadas.
78
+
79
+ Page aceita width narrow/default/wide, também declarado em Presentation.page.width. O limite
80
+ se aplica à coluna inteira. Sem a opção, a página ocupa a largura disponível.
81
+
82
+ Presentation permite declarar colunas de texto e switch, lookups de formulário, defaults de
83
+ registro, destinos condicionais de linha e confirmações. Abas podem referenciar outras
84
+ Presentations; `requires` recebe um resolvedor de permissões da aplicação, sem substituir a
85
+ autorização das actions. Views aceitam texto e HTML isolado; rotas de diálogos declaram pai.
86
+
87
+ Presentation aceita `body.tabs`, com seleção local ou controlada, referências a conteúdos e
88
+ ícones registrados e rotas declaradas para o roteador consumidor. Somente a aba ativa monta.
89
+
90
+ Presentation aceita comandos por linha e adaptadores nomeados de células. Comandos de linha
91
+ reutilizam confirmação e invalidação do contrato; ícones vêm do registro runtime do consumidor.
92
+
93
+ Presentation ganha composição declarativa por `body.sections`.
94
+ Seções de itens e listas externas reutilizam os patterns existentes; extensões runtime são
95
+ registradas por nome. O inventário de copy cobre os textos da nova declaração.
96
+
97
+ ActionList ganha apresentação nativa de itens, título junto das ações e fonte externa com
98
+ cache isolado. Itens usam moldura conectada e exibem paginação somente quando necessária.
99
+
100
+ Headers de Page, Drawer, Dialog e Surface aceitam abas integradas. TabsPanel repassa a
101
+ altura disponível às listas. Ações da coleção ficam em ActionList.toolbarActions; Presentation
102
+ aceita placement toolbar e label contextual com nome acessível específico.
103
+
104
+ Cabeçalhos usam controles default/icon e linha mínima de 2,25rem. PageBack ocupa a
105
+ primeira ação, por ícone com tooltip e destino nomeado.
106
+
107
+ O inventário de copy valida o aria-label dinâmico de botões de ícone sem exigir um
108
+ segundo texto nos filhos decorativos. O nome acessível continua exigindo declaração.
109
+
110
+ ### Breaking
111
+
112
+ Removidos surfaceLayout da Page, layout visual de Surface/DialogContent/DrawerContent,
113
+ PageShell.navigation, PageNavigation e as famílias PageIntro/DialogIntro/DrawerIntro
114
+ (incluindo IntroActions). Mova título, descrição, metadados e ações gerais para o Header.
115
+ Divisores são automáticos nas superfícies delimitadas; Surface sem moldura mantém o
116
+ alinhamento do hospedeiro e ritmo de 1rem, sem recuo ou divisor de região.
117
+ Surface nível 1 deixa de substituir o PageHeader dentro da Page; use níveis subordinados
118
+ no body. O nível 1 continua disponível em superfícies fora da Page.
119
+ Page.layout flow/fill permanece como controle de ocupação.
120
+
121
+ Presentation com heading de conteúdo requer uma Page completa. Não combine PageFooter
122
+ próprio com comandos de footer declarados na Presentation. Ações da coleção usam
123
+ ActionList.toolbarActions. PageBack e o retorno da Presentation
124
+ requerem TooltipProvider no aplicativo.
125
+
10
126
  ## 30.0.0 — 2026-10-16
11
127
 
12
128
  PageMeta, DrawerMeta, DialogMeta e SurfaceMeta organizam badges, datas e autoria em
package/bin/lib/check.mjs CHANGED
@@ -154,14 +154,11 @@ const UI_SURFACE_PAIRS = new Map([
154
154
  const UI_STRUCTURAL_PARENTS = new Map([
155
155
  ["PageHeader", new Set(["Page"])],
156
156
  ["PageBody", new Set(["Page"])],
157
- ["PageIntro", new Set(["PageBody"])],
158
- ["PageIntroActions", new Set(["PageIntro"])],
159
157
  ["PageFooter", new Set(["Page"])],
160
- ["PageTitle", new Set(["PageHeader", "PageIntro"])],
161
- ["PageDescription", new Set(["PageHeader", "PageIntro"])],
162
- ["PageMeta", new Set(["PageHeader", "PageIntro"])],
163
- ["PageBack", new Set(["PageHeader"])],
164
- ["PageNavigation", new Set(["PageHeader"])],
158
+ ["PageTitle", new Set(["PageHeader"])],
159
+ ["PageDescription", new Set(["PageHeader"])],
160
+ ["PageMeta", new Set(["PageHeader"])],
161
+ ["PageBack", new Set(["PageHeader", "PageActions", "PageShell"])],
165
162
  ["PageActions", new Set(["PageHeader"])],
166
163
  // As regiões de Surface não são estritas: com `asChild`, elas ficam dentro do elemento que
167
164
  // recebe a superfície (ADR 0045). Os slots do header são filhos diretos dele.
@@ -177,24 +174,20 @@ const UI_STRUCTURAL_PARENTS = new Map([
177
174
  ["DialogHeader", new Set(["DialogContent"])],
178
175
  ["DialogBody", new Set(["DialogContent"])],
179
176
  ["DialogFooter", new Set(["DialogContent"])],
180
- ["DialogIntro", new Set(["DialogBody"])],
181
- ["DialogIntroActions", new Set(["DialogIntro"])],
182
177
  ["DialogNavigation", new Set(["DialogHeader"])],
183
178
  ["DialogMedia", new Set(["DialogHeader"])],
184
- ["DialogTitle", new Set(["DialogHeader", "DialogIntro"])],
185
- ["DialogDescription", new Set(["DialogHeader", "DialogIntro"])],
186
- ["DialogMeta", new Set(["DialogHeader", "DialogIntro"])],
179
+ ["DialogTitle", new Set(["DialogHeader"])],
180
+ ["DialogDescription", new Set(["DialogHeader"])],
181
+ ["DialogMeta", new Set(["DialogHeader"])],
187
182
  ["DialogActions", new Set(["DialogHeader"])],
188
183
  ["DrawerHeader", new Set(["DrawerContent"])],
189
184
  ["DrawerBody", new Set(["DrawerContent"])],
190
185
  ["DrawerFooter", new Set(["DrawerContent"])],
191
- ["DrawerIntro", new Set(["DrawerBody"])],
192
- ["DrawerIntroActions", new Set(["DrawerIntro"])],
193
186
  ["DrawerNavigation", new Set(["DrawerHeader"])],
194
187
  ["DrawerMedia", new Set(["DrawerHeader"])],
195
- ["DrawerTitle", new Set(["DrawerHeader", "DrawerIntro"])],
196
- ["DrawerDescription", new Set(["DrawerHeader", "DrawerIntro"])],
197
- ["DrawerMeta", new Set(["DrawerHeader", "DrawerIntro"])],
188
+ ["DrawerTitle", new Set(["DrawerHeader"])],
189
+ ["DrawerDescription", new Set(["DrawerHeader"])],
190
+ ["DrawerMeta", new Set(["DrawerHeader"])],
198
191
  ["DrawerActions", new Set(["DrawerHeader"])],
199
192
  ["AlertMedia", new Set(["Alert"])],
200
193
  ["AlertHeader", new Set(["Alert"])],
@@ -294,14 +287,45 @@ const UI_STRUCTURAL_BODIES = new Set([
294
287
  const UI_BODY_RHYTHM_CLASS =
295
288
  /(?:^|:)(?:!?-?)(?:space-[xy]|gap(?:-[xy])?|p|px|py|pt|pr|pb|pl|ps|pe)-/;
296
289
 
297
- const UI_FLUSH_BODIES = new Set(["SurfaceBody", "DialogBody", "DrawerBody"]);
290
+ const UI_FLUSH_BODIES = new Set([
291
+ "SurfaceBody",
292
+ "DialogBody",
293
+ "DrawerBody",
294
+ "PageBody",
295
+ ]);
298
296
 
299
297
  const UI_REMOVED_PROPS = new Map([
298
+ [
299
+ "Page",
300
+ new Map([
301
+ ["surfaceLayout", "os divisores são automáticos"],
302
+ ["compact", "o título pertence ao header"],
303
+ ["sectioned", "os divisores são automáticos"],
304
+ ]),
305
+ ],
306
+ ["PageShell", new Map([["navigation", "use PageBack nas ações"]])],
307
+ ...["Surface", "DialogContent", "DrawerContent"].map((component) => [
308
+ component,
309
+ new Map([
310
+ ["layout", "as regiões têm uma anatomia única"],
311
+ ["compact", "o título pertence ao header"],
312
+ ["sectioned", "os divisores são automáticos"],
313
+ ]),
314
+ ]),
315
+ ...["SurfaceHeader", "DialogHeader", "DrawerHeader"].map((component) => [
316
+ component,
317
+ new Map([
318
+ [
319
+ "tabsActions",
320
+ "mova as ações da coleção para ActionList.toolbarActions",
321
+ ],
322
+ ]),
323
+ ]),
300
324
  [
301
325
  "Pane",
302
326
  new Map([
303
327
  ["inset", "mova o recuo para o conteúdo ou para uma `Surface` filha"],
304
- ["layout", "use `Surface` para anatomia visual contínua ou seccionada"],
328
+ ["layout", "use `Surface` para a anatomia visual"],
305
329
  ]),
306
330
  ],
307
331
  [
@@ -325,12 +349,24 @@ const UI_REMOVED_PROPS = new Map([
325
349
  [
326
350
  "PageHeader",
327
351
  new Map([
328
- ["variant", "o cabeçalho da página tem uma única apresentação rolável"],
352
+ [
353
+ "tabsActions",
354
+ "mova as ações da coleção para ActionList.toolbarActions",
355
+ ],
356
+ ["variant", "o cabeçalho da página tem uma única apresentação fixa"],
329
357
  ]),
330
358
  ],
331
359
  ]);
332
360
 
333
361
  const UI_REMOVED_COMPONENTS = new Map([
362
+ ...["Page", "Dialog", "Drawer"].flatMap((family) => [
363
+ [`${family}Intro`, `mova título e metadados para ${family}Header`],
364
+ [
365
+ `${family}IntroActions`,
366
+ `use ${family}Actions ou ActionList.toolbarActions para a coleção`,
367
+ ],
368
+ ]),
369
+ ["PageNavigation", "use PageBack em PageActions"],
334
370
  ["PaneHeader", "use `SurfaceHeader` dentro de uma `Surface`"],
335
371
  ["PaneBody", "use `SurfaceBody` ou um contêiner próprio do conteúdo"],
336
372
  ["PaneContent", "use `SurfaceBody` ou um contêiner próprio do conteúdo"],
@@ -371,54 +407,35 @@ const UI_REMOVED_COMPONENTS = new Map([
371
407
  const UI_STRUCTURAL_CONTAINERS = new Set(["PageShell"]);
372
408
 
373
409
  const UI_STRUCTURAL_HEADERS = new Map([
374
- [
375
- "PageIntro",
376
- {
377
- title: "PageTitle",
378
- optional: new Set(["PageIntroActions", "PageDescription", "PageMeta"]),
379
- shorthand: new Set(),
380
- },
381
- ],
382
- [
383
- "DialogIntro",
384
- {
385
- title: "DialogTitle",
386
- optional: new Set([
387
- "DialogIntroActions",
388
- "DialogDescription",
389
- "DialogMeta",
390
- ]),
391
- shorthand: new Set(),
392
- },
393
- ],
394
- [
395
- "DrawerIntro",
396
- {
397
- title: "DrawerTitle",
398
- optional: new Set([
399
- "DrawerIntroActions",
400
- "DrawerDescription",
401
- "DrawerMeta",
402
- ]),
403
- shorthand: new Set(),
404
- },
405
- ],
406
410
  [
407
411
  "PageHeader",
408
412
  {
409
413
  title: "PageTitle",
410
414
  optional: new Set([
411
415
  "PageBack",
412
- "PageNavigation",
413
416
  "PageActions",
414
417
  "PageDescription",
415
418
  "PageMeta",
416
419
  ]),
417
- exclusive: [new Set(["PageBack", "PageNavigation"])],
418
420
  shorthand: new Set(),
419
- titleRequired: false,
421
+ titleRequired: true,
420
422
  },
421
423
  ],
424
+ ...["Dialog", "Drawer"].map((family) => [
425
+ `${family}Header`,
426
+ {
427
+ title: `${family}Title`,
428
+ optional: new Set([
429
+ `${family}Navigation`,
430
+ `${family}Media`,
431
+ `${family}Description`,
432
+ `${family}Meta`,
433
+ `${family}Actions`,
434
+ ]),
435
+ shorthand: new Set(),
436
+ allowOpaque: true,
437
+ },
438
+ ]),
422
439
  [
423
440
  "SurfaceHeader",
424
441
  {
@@ -2042,11 +2059,10 @@ function countPageHeadingSources(page, imports) {
2042
2059
  local !== null &&
2043
2060
  ((imports.get(local) === "Surface" &&
2044
2061
  jsxNumericAttribute(node, "level") === 1) ||
2045
- imports.get(local) === "PageIntro" ||
2046
2062
  imports.get(local) === "PageState")
2047
2063
  ) {
2048
2064
  if (imports.get(local) === "PageState") states += 1;
2049
- else count += 1;
2065
+ else return;
2050
2066
  }
2051
2067
  }
2052
2068
  ts.forEachChild(node, visit);
@@ -2110,6 +2126,23 @@ export function checkUiStructure(file, text) {
2110
2126
  if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
2111
2127
  const local = jsxLocalName(node);
2112
2128
  const component = local === null ? null : imports.get(local);
2129
+ if (component === "Surface" && jsxNumericAttribute(node, "level") === 1) {
2130
+ let ancestor = node.parent;
2131
+ while (ancestor) {
2132
+ if (
2133
+ ts.isJsxElement(ancestor) &&
2134
+ imports.get(jsxLocalName(ancestor)) === "Page"
2135
+ ) {
2136
+ add(
2137
+ node,
2138
+ component,
2139
+ "Surface dentro de Page é uma seção subordinada. Use PageHeader com PageTitle para o título principal.",
2140
+ );
2141
+ break;
2142
+ }
2143
+ ancestor = ancestor.parent;
2144
+ }
2145
+ }
2113
2146
  if (component === "Split" && ts.isJsxElement(node)) {
2114
2147
  for (const child of invalidSplitChildren(
2115
2148
  node,
@@ -2165,9 +2198,11 @@ export function checkUiStructure(file, text) {
2165
2198
  component === null ? undefined : UI_STRUCTURAL_PARENTS.get(component);
2166
2199
  if (parents !== undefined) {
2167
2200
  const strict = UI_STRICT_DIRECT_COMPONENTS.has(component);
2168
- const parent = strict
2169
- ? directJsxParentName(node, imports)
2170
- : structuralJsxAncestorName(node, imports);
2201
+ // PageBack também chega pelas actions de PageShell, atravessando atributo e fragmento.
2202
+ const parent =
2203
+ strict && component !== "PageBack"
2204
+ ? directJsxParentName(node, imports)
2205
+ : structuralJsxAncestorName(node, imports);
2171
2206
  // Slots das famílias históricas podem ser a raiz de um componente wrapper. Quando há
2172
2207
  // uma superfície estrutural visível no mesmo JSX, porém, ela precisa ser a família certa.
2173
2208
  if (
@@ -2205,41 +2240,6 @@ export function checkUiStructure(file, text) {
2205
2240
  }
2206
2241
  }
2207
2242
 
2208
- if (["PageBody", "DialogBody", "DrawerBody"].includes(component)) {
2209
- const intro = component.replace("Body", "Intro");
2210
- const visible = (children) =>
2211
- children.flatMap((child) => {
2212
- if (ts.isJsxText(child)) return child.text.trim() ? [child] : [];
2213
- if (ts.isJsxFragment(child)) return visible(child.children);
2214
- if (
2215
- ts.isJsxExpression(child) &&
2216
- (!child.expression ||
2217
- [
2218
- ts.SyntaxKind.NullKeyword,
2219
- ts.SyntaxKind.FalseKeyword,
2220
- ts.SyntaxKind.TrueKeyword,
2221
- ].includes(child.expression.kind))
2222
- )
2223
- return [];
2224
- return [child];
2225
- });
2226
- const children = ts.isJsxElement(node) ? visible(node.children) : [];
2227
- const indexes = children.flatMap((child, index) =>
2228
- (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) &&
2229
- imports.get(jsxLocalName(child)) === intro
2230
- ? [index]
2231
- : [],
2232
- );
2233
- if (indexes.length > 1 || (indexes.length === 1 && indexes[0] !== 0)) {
2234
- add(
2235
- node,
2236
- component,
2237
- `${intro} deve ser o primeiro e único bloco introdutório de ${component}.`,
2238
- "ui-structure-intro-order",
2239
- );
2240
- }
2241
- }
2242
-
2243
2243
  if (
2244
2244
  component !== null &&
2245
2245
  (UI_STRUCTURAL_BODIES.has(component) || component === "Page")
@@ -2273,7 +2273,7 @@ export function checkUiStructure(file, text) {
2273
2273
  add(
2274
2274
  node,
2275
2275
  component,
2276
- `${component} aceita um único ${title}: mantenha o título no header ou na intro, nunca nos dois.`,
2276
+ `${component} aceita um único ${title}: mantenha o título no header.`,
2277
2277
  "ui-structure-duplicate-title",
2278
2278
  );
2279
2279
  }
@@ -2282,11 +2282,8 @@ export function checkUiStructure(file, text) {
2282
2282
  if (
2283
2283
  [
2284
2284
  "PageHeader",
2285
- "PageIntro",
2286
2285
  "DialogHeader",
2287
- "DialogIntro",
2288
2286
  "DrawerHeader",
2289
- "DrawerIntro",
2290
2287
  "SurfaceHeader",
2291
2288
  ].includes(component)
2292
2289
  ) {
@@ -2351,7 +2348,18 @@ export function checkUiStructure(file, text) {
2351
2348
  children.names.filter((name) => group.has(name)).length > 1,
2352
2349
  );
2353
2350
  const titleValid =
2354
- header.titleRequired === false ? titles <= 1 : titles === 1;
2351
+ header.titleRequired === false
2352
+ ? titles <= 1
2353
+ : titles === 1 ||
2354
+ (titles === 0 &&
2355
+ header.allowOpaque &&
2356
+ children.hasOpaque &&
2357
+ ts.isJsxElement(node) &&
2358
+ node.children.some(
2359
+ (child) =>
2360
+ ts.isJsxExpression(child) &&
2361
+ child.expression !== undefined,
2362
+ ));
2355
2363
  const recognizedContent =
2356
2364
  titles +
2357
2365
  children.names.filter((name) => header.optional.has(name)).length;
@@ -2496,7 +2504,7 @@ export function checkUiStructure(file, text) {
2496
2504
  add(
2497
2505
  node,
2498
2506
  component,
2499
- "Page explícito exige exatamente um heading principal em PageHeader, Surface level={1} ou PageState dentro de PageBody.",
2507
+ "Page explícito exige exatamente um heading principal em PageHeader ou PageState ativo dentro de PageBody.",
2500
2508
  );
2501
2509
  }
2502
2510
  }