@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.
- package/CHANGELOG.md +137 -0
- package/bin/lib/check.mjs +178 -94
- 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/0045-surface-replaces-content-and-card.md +3 -0
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +142 -0
- package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
- package/docs/adr/0063-metadados-das-superficies.md +44 -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 +8 -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 +21 -10
- package/registry/skills/build-opus-ui/references/ui-patterns.md +33 -28
- package/src/core/presentation.ts +938 -32
- package/src/core/types.ts +2 -1
- 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 +588 -365
- package/src/ui/components/patterns/page-heading-context.tsx +12 -5
- package/src/ui/components/patterns/page.tsx +238 -181
- 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 +47 -34
- package/src/ui/components/patterns/surface-anatomy.ts +27 -217
- package/src/ui/components/patterns/surface.tsx +55 -37
- 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 +59 -89
- package/src/ui/components/primitives/drawer.tsx +52 -87
- 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 +3 -3
- 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 +146 -73
- 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 +72 -101
- package/src/ui/docs/content/drawer.md +69 -92
- package/src/ui/docs/content/field.md +2 -1
- package/src/ui/docs/content/page.md +207 -386
- 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 +94 -116
- package/src/ui/docs/content/table.md +7 -0
- 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 +38 -8
- package/src/ui/theme.css +44 -33
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,143 @@ 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
|
+
|
|
126
|
+
## 30.0.0 — 2026-10-16
|
|
127
|
+
|
|
128
|
+
PageMeta, DrawerMeta, DialogMeta e SurfaceMeta organizam badges, datas e autoria em
|
|
129
|
+
faixa própria sob o título e a descrição. Page e Surface também aceitam a prop `meta`.
|
|
130
|
+
|
|
131
|
+
### Breaking
|
|
132
|
+
|
|
133
|
+
**Migração:** SurfaceMeta deixa de ocupar a linha do título. Contadores explícitos usam
|
|
134
|
+
SurfaceCount; a prop `count` conserva o comportamento anterior. Metadados contextuais
|
|
135
|
+
existentes permanecem em SurfaceMeta e passam para a faixa inferior. Revise manualmente
|
|
136
|
+
os usos existentes de SurfaceMeta: os gates não distinguem um contador de metadados.
|
|
137
|
+
Descrições textuais usam SurfaceDescription. DialogMeta não se aplica ao modo de alerta.
|
|
138
|
+
|
|
139
|
+
Tabelas quebram texto entre palavras, preservando palavras curtas inteiras, e ActionList
|
|
140
|
+
mantém a rolagem dentro dos resultados,
|
|
141
|
+
sem alargar a superfície. Filtros inline podem ocupar mais de uma linha. Colunas `fit`
|
|
142
|
+
preservam os valores sem quebra; a distribuição automática considera também os cabeçalhos,
|
|
143
|
+
que podem quebrar quando falta espaço. Colunas de dados não recebem largura mínima forçada;
|
|
144
|
+
seleção, ações de linha e células contendo apenas um switch permanecem compactas. Em tabelas
|
|
145
|
+
compostas diretamente, use `whitespace-nowrap` para valores que devem permanecer inteiros.
|
|
146
|
+
|
|
10
147
|
## 29.0.0 — 2026-10-15
|
|
11
148
|
|
|
12
149
|
O `CLAUDE.md` passa a importar o `AGENTS.md`, e as declarações de copy passam a ser ancoradas pelo
|
package/bin/lib/check.mjs
CHANGED
|
@@ -154,13 +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
|
-
["
|
|
163
|
-
["
|
|
158
|
+
["PageTitle", new Set(["PageHeader"])],
|
|
159
|
+
["PageDescription", new Set(["PageHeader"])],
|
|
160
|
+
["PageMeta", new Set(["PageHeader"])],
|
|
161
|
+
["PageBack", new Set(["PageHeader", "PageActions", "PageShell"])],
|
|
164
162
|
["PageActions", new Set(["PageHeader"])],
|
|
165
163
|
// As regiões de Surface não são estritas: com `asChild`, elas ficam dentro do elemento que
|
|
166
164
|
// recebe a superfície (ADR 0045). Os slots do header são filhos diretos dele.
|
|
@@ -171,26 +169,25 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
171
169
|
["SurfaceTitle", new Set(["SurfaceHeader"])],
|
|
172
170
|
["SurfaceDescription", new Set(["SurfaceHeader"])],
|
|
173
171
|
["SurfaceMeta", new Set(["SurfaceHeader"])],
|
|
172
|
+
["SurfaceCount", new Set(["SurfaceHeader"])],
|
|
174
173
|
["SurfaceActions", new Set(["SurfaceHeader"])],
|
|
175
174
|
["DialogHeader", new Set(["DialogContent"])],
|
|
176
175
|
["DialogBody", new Set(["DialogContent"])],
|
|
177
176
|
["DialogFooter", new Set(["DialogContent"])],
|
|
178
|
-
["DialogIntro", new Set(["DialogBody"])],
|
|
179
|
-
["DialogIntroActions", new Set(["DialogIntro"])],
|
|
180
177
|
["DialogNavigation", new Set(["DialogHeader"])],
|
|
181
178
|
["DialogMedia", new Set(["DialogHeader"])],
|
|
182
|
-
["DialogTitle", new Set(["DialogHeader"
|
|
183
|
-
["DialogDescription", new Set(["DialogHeader"
|
|
179
|
+
["DialogTitle", new Set(["DialogHeader"])],
|
|
180
|
+
["DialogDescription", new Set(["DialogHeader"])],
|
|
181
|
+
["DialogMeta", new Set(["DialogHeader"])],
|
|
184
182
|
["DialogActions", new Set(["DialogHeader"])],
|
|
185
183
|
["DrawerHeader", new Set(["DrawerContent"])],
|
|
186
184
|
["DrawerBody", new Set(["DrawerContent"])],
|
|
187
185
|
["DrawerFooter", new Set(["DrawerContent"])],
|
|
188
|
-
["DrawerIntro", new Set(["DrawerBody"])],
|
|
189
|
-
["DrawerIntroActions", new Set(["DrawerIntro"])],
|
|
190
186
|
["DrawerNavigation", new Set(["DrawerHeader"])],
|
|
191
187
|
["DrawerMedia", new Set(["DrawerHeader"])],
|
|
192
|
-
["DrawerTitle", new Set(["DrawerHeader"
|
|
193
|
-
["DrawerDescription", new Set(["DrawerHeader"
|
|
188
|
+
["DrawerTitle", new Set(["DrawerHeader"])],
|
|
189
|
+
["DrawerDescription", new Set(["DrawerHeader"])],
|
|
190
|
+
["DrawerMeta", new Set(["DrawerHeader"])],
|
|
194
191
|
["DrawerActions", new Set(["DrawerHeader"])],
|
|
195
192
|
["AlertMedia", new Set(["Alert"])],
|
|
196
193
|
["AlertHeader", new Set(["Alert"])],
|
|
@@ -224,7 +221,7 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
224
221
|
body: "PageBody",
|
|
225
222
|
footer: "PageFooter",
|
|
226
223
|
headerRequired: false,
|
|
227
|
-
shorthand: new Set(["title", "description", "actions"]),
|
|
224
|
+
shorthand: new Set(["title", "description", "meta", "actions"]),
|
|
228
225
|
// O contador saiu do título da página (ADR 0009). Tirar `count` do shorthand não proíbe
|
|
229
226
|
// nada sozinho — só apaga o marcador que fazia o gate enxergar aquele Page —, então a
|
|
230
227
|
// prop removida é cobrada aqui, nas duas formas de composição.
|
|
@@ -242,7 +239,14 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
242
239
|
// Cada região é opcional, a forma curta convive com SurfaceBody e SurfaceFooter, e sem
|
|
243
240
|
// região nenhuma a Surface é uma caixa de filhos livres (ADR 0045).
|
|
244
241
|
flexible: true,
|
|
245
|
-
shorthand: new Set([
|
|
242
|
+
shorthand: new Set([
|
|
243
|
+
"title",
|
|
244
|
+
"media",
|
|
245
|
+
"description",
|
|
246
|
+
"meta",
|
|
247
|
+
"count",
|
|
248
|
+
"actions",
|
|
249
|
+
]),
|
|
246
250
|
},
|
|
247
251
|
],
|
|
248
252
|
]);
|
|
@@ -260,6 +264,10 @@ const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
|
260
264
|
"SurfaceTitle",
|
|
261
265
|
"SurfaceDescription",
|
|
262
266
|
"SurfaceMeta",
|
|
267
|
+
"SurfaceCount",
|
|
268
|
+
"PageMeta",
|
|
269
|
+
"DrawerMeta",
|
|
270
|
+
"DialogMeta",
|
|
263
271
|
"SurfaceActions",
|
|
264
272
|
"AlertMedia",
|
|
265
273
|
"AlertHeader",
|
|
@@ -279,19 +287,46 @@ const UI_STRUCTURAL_BODIES = new Set([
|
|
|
279
287
|
const UI_BODY_RHYTHM_CLASS =
|
|
280
288
|
/(?:^|:)(?:!?-?)(?:space-[xy]|gap(?:-[xy])?|p|px|py|pt|pr|pb|pl|ps|pe)-/;
|
|
281
289
|
|
|
282
|
-
const UI_FLUSH_BODIES = new Set([
|
|
290
|
+
const UI_FLUSH_BODIES = new Set([
|
|
291
|
+
"SurfaceBody",
|
|
292
|
+
"DialogBody",
|
|
293
|
+
"DrawerBody",
|
|
294
|
+
"PageBody",
|
|
295
|
+
]);
|
|
283
296
|
|
|
284
297
|
const UI_REMOVED_PROPS = new Map([
|
|
285
298
|
[
|
|
286
|
-
"
|
|
299
|
+
"Page",
|
|
287
300
|
new Map([
|
|
288
|
-
["
|
|
289
|
-
["
|
|
301
|
+
["surfaceLayout", "os divisores são automáticos"],
|
|
302
|
+
["compact", "o título pertence ao header"],
|
|
303
|
+
["sectioned", "os divisores são automáticos"],
|
|
290
304
|
]),
|
|
291
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
|
+
]),
|
|
292
324
|
[
|
|
293
|
-
"
|
|
294
|
-
new Map([
|
|
325
|
+
"Pane",
|
|
326
|
+
new Map([
|
|
327
|
+
["inset", "mova o recuo para o conteúdo ou para uma `Surface` filha"],
|
|
328
|
+
["layout", "use `Surface` para a anatomia visual"],
|
|
329
|
+
]),
|
|
295
330
|
],
|
|
296
331
|
[
|
|
297
332
|
"DialogMedia",
|
|
@@ -314,12 +349,24 @@ const UI_REMOVED_PROPS = new Map([
|
|
|
314
349
|
[
|
|
315
350
|
"PageHeader",
|
|
316
351
|
new Map([
|
|
317
|
-
[
|
|
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"],
|
|
318
357
|
]),
|
|
319
358
|
],
|
|
320
359
|
]);
|
|
321
360
|
|
|
322
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"],
|
|
323
370
|
["PaneHeader", "use `SurfaceHeader` dentro de uma `Surface`"],
|
|
324
371
|
["PaneBody", "use `SurfaceBody` ou um contêiner próprio do conteúdo"],
|
|
325
372
|
["PaneContent", "use `SurfaceBody` ou um contêiner próprio do conteúdo"],
|
|
@@ -340,7 +387,7 @@ const UI_REMOVED_COMPONENTS = new Map([
|
|
|
340
387
|
],
|
|
341
388
|
["ContentTitle", "use `SurfaceTitle`"],
|
|
342
389
|
["ContentDescription", "use `SurfaceDescription`"],
|
|
343
|
-
["ContentMeta", "use `
|
|
390
|
+
["ContentMeta", "use `SurfaceCount`"],
|
|
344
391
|
["ContentActions", "use `SurfaceActions`"],
|
|
345
392
|
["ContentBody", "use `SurfaceBody`"],
|
|
346
393
|
["Card", 'use `Surface framed elevation="raised"`'],
|
|
@@ -360,45 +407,35 @@ const UI_REMOVED_COMPONENTS = new Map([
|
|
|
360
407
|
const UI_STRUCTURAL_CONTAINERS = new Set(["PageShell"]);
|
|
361
408
|
|
|
362
409
|
const UI_STRUCTURAL_HEADERS = new Map([
|
|
363
|
-
[
|
|
364
|
-
"PageIntro",
|
|
365
|
-
{
|
|
366
|
-
title: "PageTitle",
|
|
367
|
-
optional: new Set(["PageIntroActions", "PageDescription"]),
|
|
368
|
-
shorthand: new Set(),
|
|
369
|
-
},
|
|
370
|
-
],
|
|
371
|
-
[
|
|
372
|
-
"DialogIntro",
|
|
373
|
-
{
|
|
374
|
-
title: "DialogTitle",
|
|
375
|
-
optional: new Set(["DialogIntroActions", "DialogDescription"]),
|
|
376
|
-
shorthand: new Set(),
|
|
377
|
-
},
|
|
378
|
-
],
|
|
379
|
-
[
|
|
380
|
-
"DrawerIntro",
|
|
381
|
-
{
|
|
382
|
-
title: "DrawerTitle",
|
|
383
|
-
optional: new Set(["DrawerIntroActions", "DrawerDescription"]),
|
|
384
|
-
shorthand: new Set(),
|
|
385
|
-
},
|
|
386
|
-
],
|
|
387
410
|
[
|
|
388
411
|
"PageHeader",
|
|
389
412
|
{
|
|
390
413
|
title: "PageTitle",
|
|
391
414
|
optional: new Set([
|
|
392
415
|
"PageBack",
|
|
393
|
-
"PageNavigation",
|
|
394
416
|
"PageActions",
|
|
395
417
|
"PageDescription",
|
|
418
|
+
"PageMeta",
|
|
396
419
|
]),
|
|
397
|
-
exclusive: [new Set(["PageBack", "PageNavigation"])],
|
|
398
420
|
shorthand: new Set(),
|
|
399
|
-
titleRequired:
|
|
421
|
+
titleRequired: true,
|
|
400
422
|
},
|
|
401
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
|
+
]),
|
|
402
439
|
[
|
|
403
440
|
"SurfaceHeader",
|
|
404
441
|
{
|
|
@@ -407,6 +444,7 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
407
444
|
"SurfaceMedia",
|
|
408
445
|
"SurfaceDescription",
|
|
409
446
|
"SurfaceMeta",
|
|
447
|
+
"SurfaceCount",
|
|
410
448
|
"SurfaceActions",
|
|
411
449
|
]),
|
|
412
450
|
shorthand: new Set(),
|
|
@@ -2021,11 +2059,10 @@ function countPageHeadingSources(page, imports) {
|
|
|
2021
2059
|
local !== null &&
|
|
2022
2060
|
((imports.get(local) === "Surface" &&
|
|
2023
2061
|
jsxNumericAttribute(node, "level") === 1) ||
|
|
2024
|
-
imports.get(local) === "PageIntro" ||
|
|
2025
2062
|
imports.get(local) === "PageState")
|
|
2026
2063
|
) {
|
|
2027
2064
|
if (imports.get(local) === "PageState") states += 1;
|
|
2028
|
-
else
|
|
2065
|
+
else return;
|
|
2029
2066
|
}
|
|
2030
2067
|
}
|
|
2031
2068
|
ts.forEachChild(node, visit);
|
|
@@ -2089,6 +2126,23 @@ export function checkUiStructure(file, text) {
|
|
|
2089
2126
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
2090
2127
|
const local = jsxLocalName(node);
|
|
2091
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
|
+
}
|
|
2092
2146
|
if (component === "Split" && ts.isJsxElement(node)) {
|
|
2093
2147
|
for (const child of invalidSplitChildren(
|
|
2094
2148
|
node,
|
|
@@ -2116,13 +2170,39 @@ export function checkUiStructure(file, text) {
|
|
|
2116
2170
|
"ui-structure-removed-component",
|
|
2117
2171
|
);
|
|
2118
2172
|
}
|
|
2173
|
+
if (component === "DialogMeta") {
|
|
2174
|
+
let ancestor = node.parent;
|
|
2175
|
+
while (ancestor !== undefined) {
|
|
2176
|
+
if (
|
|
2177
|
+
ts.isJsxElement(ancestor) &&
|
|
2178
|
+
imports.get(jsxLocalName(ancestor)) === "Dialog"
|
|
2179
|
+
) {
|
|
2180
|
+
const modes = jsxStringAttributeValues(
|
|
2181
|
+
jsxOpening(ancestor),
|
|
2182
|
+
"mode",
|
|
2183
|
+
);
|
|
2184
|
+
if (modes.length === 1 && modes[0] === "alert") {
|
|
2185
|
+
add(
|
|
2186
|
+
node,
|
|
2187
|
+
component,
|
|
2188
|
+
'DialogMeta não está disponível em Dialog com mode="alert"; coloque as informações no corpo do alerta.',
|
|
2189
|
+
"ui-structure-meta",
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
break;
|
|
2193
|
+
}
|
|
2194
|
+
ancestor = ancestor.parent;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2119
2197
|
const parents =
|
|
2120
2198
|
component === null ? undefined : UI_STRUCTURAL_PARENTS.get(component);
|
|
2121
2199
|
if (parents !== undefined) {
|
|
2122
2200
|
const strict = UI_STRICT_DIRECT_COMPONENTS.has(component);
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
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);
|
|
2126
2206
|
// Slots das famílias históricas podem ser a raiz de um componente wrapper. Quando há
|
|
2127
2207
|
// uma superfície estrutural visível no mesmo JSX, porém, ela precisa ser a família certa.
|
|
2128
2208
|
if (
|
|
@@ -2160,41 +2240,6 @@ export function checkUiStructure(file, text) {
|
|
|
2160
2240
|
}
|
|
2161
2241
|
}
|
|
2162
2242
|
|
|
2163
|
-
if (["PageBody", "DialogBody", "DrawerBody"].includes(component)) {
|
|
2164
|
-
const intro = component.replace("Body", "Intro");
|
|
2165
|
-
const visible = (children) =>
|
|
2166
|
-
children.flatMap((child) => {
|
|
2167
|
-
if (ts.isJsxText(child)) return child.text.trim() ? [child] : [];
|
|
2168
|
-
if (ts.isJsxFragment(child)) return visible(child.children);
|
|
2169
|
-
if (
|
|
2170
|
-
ts.isJsxExpression(child) &&
|
|
2171
|
-
(!child.expression ||
|
|
2172
|
-
[
|
|
2173
|
-
ts.SyntaxKind.NullKeyword,
|
|
2174
|
-
ts.SyntaxKind.FalseKeyword,
|
|
2175
|
-
ts.SyntaxKind.TrueKeyword,
|
|
2176
|
-
].includes(child.expression.kind))
|
|
2177
|
-
)
|
|
2178
|
-
return [];
|
|
2179
|
-
return [child];
|
|
2180
|
-
});
|
|
2181
|
-
const children = ts.isJsxElement(node) ? visible(node.children) : [];
|
|
2182
|
-
const indexes = children.flatMap((child, index) =>
|
|
2183
|
-
(ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) &&
|
|
2184
|
-
imports.get(jsxLocalName(child)) === intro
|
|
2185
|
-
? [index]
|
|
2186
|
-
: [],
|
|
2187
|
-
);
|
|
2188
|
-
if (indexes.length > 1 || (indexes.length === 1 && indexes[0] !== 0)) {
|
|
2189
|
-
add(
|
|
2190
|
-
node,
|
|
2191
|
-
component,
|
|
2192
|
-
`${intro} deve ser o primeiro e único bloco introdutório de ${component}.`,
|
|
2193
|
-
"ui-structure-intro-order",
|
|
2194
|
-
);
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
2243
|
if (
|
|
2199
2244
|
component !== null &&
|
|
2200
2245
|
(UI_STRUCTURAL_BODIES.has(component) || component === "Page")
|
|
@@ -2228,12 +2273,40 @@ export function checkUiStructure(file, text) {
|
|
|
2228
2273
|
add(
|
|
2229
2274
|
node,
|
|
2230
2275
|
component,
|
|
2231
|
-
`${component} aceita um único ${title}: mantenha o título no header
|
|
2276
|
+
`${component} aceita um único ${title}: mantenha o título no header.`,
|
|
2232
2277
|
"ui-structure-duplicate-title",
|
|
2233
2278
|
);
|
|
2234
2279
|
}
|
|
2235
2280
|
}
|
|
2236
2281
|
|
|
2282
|
+
if (
|
|
2283
|
+
[
|
|
2284
|
+
"PageHeader",
|
|
2285
|
+
"DialogHeader",
|
|
2286
|
+
"DrawerHeader",
|
|
2287
|
+
"SurfaceHeader",
|
|
2288
|
+
].includes(component)
|
|
2289
|
+
) {
|
|
2290
|
+
const family = component.replace(/(?:Header|Intro)$/, "");
|
|
2291
|
+
const children = directStructuralChildren(node, imports);
|
|
2292
|
+
const metas = children.names.filter(
|
|
2293
|
+
(name) => name === `${family}Meta`,
|
|
2294
|
+
).length;
|
|
2295
|
+
if (
|
|
2296
|
+
metas > 1 ||
|
|
2297
|
+
(metas > 0 &&
|
|
2298
|
+
!children.names.includes(`${family}Title`) &&
|
|
2299
|
+
!children.hasOpaque)
|
|
2300
|
+
) {
|
|
2301
|
+
add(
|
|
2302
|
+
node,
|
|
2303
|
+
component,
|
|
2304
|
+
`${component} aceita um único ${family}Meta e exige ${family}Title para exibi-lo.`,
|
|
2305
|
+
"ui-structure-meta",
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2237
2310
|
const header =
|
|
2238
2311
|
component === null ? undefined : UI_STRUCTURAL_HEADERS.get(component);
|
|
2239
2312
|
if (header !== undefined) {
|
|
@@ -2275,7 +2348,18 @@ export function checkUiStructure(file, text) {
|
|
|
2275
2348
|
children.names.filter((name) => group.has(name)).length > 1,
|
|
2276
2349
|
);
|
|
2277
2350
|
const titleValid =
|
|
2278
|
-
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
|
+
));
|
|
2279
2363
|
const recognizedContent =
|
|
2280
2364
|
titles +
|
|
2281
2365
|
children.names.filter((name) => header.optional.has(name)).length;
|
|
@@ -2420,7 +2504,7 @@ export function checkUiStructure(file, text) {
|
|
|
2420
2504
|
add(
|
|
2421
2505
|
node,
|
|
2422
2506
|
component,
|
|
2423
|
-
"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.",
|
|
2424
2508
|
);
|
|
2425
2509
|
}
|
|
2426
2510
|
}
|