@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.
- package/CHANGELOG.md +116 -0
- package/bin/lib/check.mjs +109 -101
- package/bin/lib/copy.mjs +2970 -1801
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +52 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +27 -2
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
- package/docs/adr/0064-action-list-com-itens-e-fontes-externas.md +47 -0
- package/docs/adr/0065-presentation-composes-settings-sections.md +295 -0
- package/docs/adr/README.md +4 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +21 -19
- package/registry/skills/build-opus-ui/references/evaluations.md +13 -11
- package/registry/skills/build-opus-ui/references/ui-patterns.md +24 -27
- package/src/core/presentation.ts +938 -32
- package/src/core/types.ts +1 -0
- package/src/ui/components/patterns/form.tsx +2 -0
- package/src/ui/components/patterns/grouped-list.ts +2 -2
- package/src/ui/components/patterns/list.tsx +572 -358
- package/src/ui/components/patterns/page-heading-context.tsx +12 -5
- package/src/ui/components/patterns/page.tsx +227 -197
- package/src/ui/components/patterns/presentation-fields.tsx +246 -0
- package/src/ui/components/patterns/presentation-sections.tsx +494 -0
- package/src/ui/components/patterns/presentation.tsx +1142 -229
- package/src/ui/components/patterns/structural-header.tsx +23 -32
- package/src/ui/components/patterns/surface-anatomy.ts +27 -217
- package/src/ui/components/patterns/surface.tsx +19 -29
- package/src/ui/components/patterns/trigger.tsx +10 -2
- package/src/ui/components/primitives/close-button.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -8
- package/src/ui/components/primitives/detail.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +19 -81
- package/src/ui/components/primitives/drawer.tsx +17 -79
- package/src/ui/components/primitives/field.tsx +1 -1
- package/src/ui/components/primitives/pagination.tsx +3 -2
- package/src/ui/components/primitives/table.tsx +1 -1
- package/src/ui/components/primitives/tabs.tsx +13 -2
- package/src/ui/docs/content/action-form.md +2 -1
- package/src/ui/docs/content/action-list.md +134 -68
- package/src/ui/docs/content/action-trigger.md +5 -0
- package/src/ui/docs/content/button.md +5 -6
- package/src/ui/docs/content/command.md +62 -50
- package/src/ui/docs/content/compositions.md +129 -459
- package/src/ui/docs/content/dialog.md +44 -112
- package/src/ui/docs/content/drawer.md +44 -103
- package/src/ui/docs/content/field.md +2 -1
- package/src/ui/docs/content/page.md +204 -412
- package/src/ui/docs/content/presentation.md +371 -23
- package/src/ui/docs/content/separator.md +1 -1
- package/src/ui/docs/content/surface.md +65 -120
- package/src/ui/docs/content/tokens.md +18 -4
- package/src/ui/drivers/context.tsx +11 -4
- package/src/ui/drivers/react.tsx +27 -7
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +34 -8
- 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"
|
|
161
|
-
["PageDescription", new Set(["PageHeader"
|
|
162
|
-
["PageMeta", new Set(["PageHeader"
|
|
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"
|
|
185
|
-
["DialogDescription", new Set(["DialogHeader"
|
|
186
|
-
["DialogMeta", new Set(["DialogHeader"
|
|
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"
|
|
196
|
-
["DrawerDescription", new Set(["DrawerHeader"
|
|
197
|
-
["DrawerMeta", new Set(["DrawerHeader"
|
|
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([
|
|
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
|
|
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
|
-
[
|
|
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:
|
|
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
|
|
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
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
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
|
|
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
|
|
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
|
|
2507
|
+
"Page explícito exige exatamente um heading principal em PageHeader ou PageState ativo dentro de PageBody.",
|
|
2500
2508
|
);
|
|
2501
2509
|
}
|
|
2502
2510
|
}
|