@softize/opus 15.2.2 → 16.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 +51 -18
- package/bin/lib/check.mjs +98 -15
- package/bin/lib/copy.mjs +811 -314
- package/bin/lib/gen-manifest.mjs +24 -23
- package/bin/lib/gen-runner.mjs +188 -148
- package/docs/adr/0010-page-header-owns-page-chrome.md +3 -4
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -3
- package/docs/adr/0012-modal-header-only-names-the-surface.md +45 -0
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +92 -0
- package/docs/code-style.md +24 -19
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +43 -26
- package/src/core/presentation.ts +512 -0
- package/src/presentation/index.ts +1 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +26 -9
- package/src/ui/components/patterns/confirm.tsx +34 -29
- package/src/ui/components/patterns/form-dialog.tsx +20 -8
- package/src/ui/components/patterns/list.tsx +26 -9
- package/src/ui/components/patterns/page.tsx +99 -139
- package/src/ui/components/patterns/presentation.tsx +316 -0
- package/src/ui/components/patterns/sidebar.tsx +3 -3
- package/src/ui/components/patterns/trigger.tsx +38 -17
- package/src/ui/components/primitives/button-group.tsx +53 -43
- package/src/ui/components/primitives/command.tsx +30 -72
- package/src/ui/components/primitives/dialog.tsx +23 -89
- package/src/ui/components/primitives/drawer.tsx +8 -34
- package/src/ui/docs/content/action-form-dialog.md +12 -10
- package/src/ui/docs/content/action-list-dialog.md +22 -17
- package/src/ui/docs/content/button.md +52 -35
- package/src/ui/docs/content/communication.md +26 -26
- package/src/ui/docs/content/dialog.md +173 -154
- package/src/ui/docs/content/drawer.md +12 -11
- package/src/ui/docs/content/page.md +72 -91
- package/src/ui/docs/content/presentation.md +158 -0
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/meta.ts +8 -2
- package/src/ui/react.tsx +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,15 +7,45 @@ 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
|
+
## 16.0.0 — 2026-09-11
|
|
11
|
+
|
|
12
|
+
Opus 16 introduz `Presentation` como artefato público declarativo, disponível também pelo subpath
|
|
13
|
+
`@softize/opus/presentation`, e sua composição React portátil entre Page, Dialog e Drawer. A
|
|
14
|
+
`PresentationInvocation` guarda o identificador do recurso, a surface, input estritamente JSON e a
|
|
15
|
+
pilha de frames usada por `back`; `openPresentation`, `backPresentation`, `closePresentation` e
|
|
16
|
+
`applyPresentationEffects` oferecem transições puras para o renderer. O efeito `navigate` declara
|
|
17
|
+
input por bindings e escolhe entre `push` e `replace`.
|
|
18
|
+
|
|
19
|
+
`PresentationInspector` oferece, sob demanda, um snapshot único da definição, invocação, bindings
|
|
20
|
+
resolvidos e diagnósticos. Chaves que indicam senha, segredo, token, cookie, autorização ou chave de
|
|
21
|
+
API são mascaradas antes de o JSON aparecer ou ser copiado. A Lens continua expondo somente a
|
|
22
|
+
definição estática projetada no manifest.
|
|
23
|
+
|
|
24
|
+
`PageFooter` entra na composição explícita de Page. `PageHeader` organiza retorno ou breadcrumb,
|
|
25
|
+
título e ações na mesma linha, acompanhando a anatomia usada pela Presentation; `PageIntro` também
|
|
26
|
+
aceita a navegação própria do recurso quando existe um `PageShell`. `PageBack` vira um controle
|
|
27
|
+
somente com ícone; o nome do destino continua no nome acessível e no tooltip. `ButtonGroup` ganha a
|
|
28
|
+
distribuição `equal` para ações compactas de mesma largura. A Sidebar recolhida passa a `4rem`, com
|
|
29
|
+
itens de `2.5rem` e respiro de `0.75rem`.
|
|
30
|
+
|
|
31
|
+
**Breaking:** cabeçalhos de Dialog e Drawer passam a nomear apenas a superfície. Remova
|
|
32
|
+
`DialogDescription` e `DrawerDescription` e leve explicações para o body ou para o `help` do campo
|
|
33
|
+
correspondente. Nos wrappers `ActionFormDialog` e `ActionListDialog`, remova `description` pelo
|
|
34
|
+
mesmo motivo. Nas APIs imperativas `alert`, `confirm`, `prompt` e `choose`, e em
|
|
35
|
+
`ActionTrigger.confirm`, renomeie `description` para `body`. `PageHeader.variant` e o tipo
|
|
36
|
+
`PageHeaderVariant` deixam de existir; use `PageShell` para a barra persistente e `PageIntro` para
|
|
37
|
+
a introdução do conteúdo.
|
|
38
|
+
|
|
39
|
+
|
|
10
40
|
## 15.2.2 — 2026-09-11
|
|
11
41
|
|
|
12
42
|
O acesso descritivo de Produtos de Dados usa `permissionContexts` no lugar do nome genérico
|
|
13
|
-
`contexts`. A qualificação evita confundir
|
|
43
|
+
`contexts`. A qualificação evita confundir contexto do framework, contexto de tela, domínio de
|
|
14
44
|
dados e vocabulário de autorização nas projeções do manifest e da Lens.
|
|
15
45
|
|
|
16
46
|
**Migração recomendada:** em declarações `defineDataProduct`, renomeie `access.contexts` para
|
|
17
47
|
`access.permissionContexts` e regenere o manifest. O nome anterior continua aceito e projetado em
|
|
18
|
-
toda a série 15.x como alias
|
|
48
|
+
toda a série 15.x como alias obsoleto; consumidores novos devem ler somente o nome qualificado.
|
|
19
49
|
|
|
20
50
|
## 15.2.1 — 2026-09-11
|
|
21
51
|
|
|
@@ -214,7 +244,7 @@ resizable e slider passam a ejetados no `registry.lock.json`.
|
|
|
214
244
|
Descontinuados, com aviso no editor e sem mudança de comportamento: as variantes legadas
|
|
215
245
|
(`default`, `secondary`, `destructive`, `success`, `warning`, `info`), `confirm()`, `ConfirmHost`,
|
|
216
246
|
`FieldSpec.hint` (use `help`), `ShellNav*` (use `SidebarNav`) e `TbdlibProvider`, renomeado para
|
|
217
|
-
`OpusProvider`. `'danger'` deixa de ser aceito como
|
|
247
|
+
`OpusProvider`. `'danger'` deixa de ser aceito como _variante_ legada de Badge e Alert no tipo (era
|
|
218
248
|
um contexto disfarçado): declare `context="danger"`. O botão de confirmar do `ActionTrigger` passa a
|
|
219
249
|
ser sempre `solid` — `danger` quando a action é `destructive`, senão o `context` do gatilho — e não
|
|
220
250
|
herda mais o `variant` do gatilho; a permissão negada (`authorization`/`authentication`) entra na
|
|
@@ -454,6 +484,7 @@ Os patterns existentes passam a reutilizar os primitives canônicos: `ActionForm
|
|
|
454
484
|
`FieldGroup`, descrições e erros associados ao controle; `ActionList` usa a moldura de `Table` e
|
|
455
485
|
os estados seguros de `DataState`, preservando a tentativa de recuperação; e a confirmação de uma
|
|
456
486
|
`ActionTrigger` destrutiva mantém o tratamento destrutivo mesmo quando o gatilho visual é discreto.
|
|
487
|
+
|
|
457
488
|
## 12.5.5 — 2026-08-25
|
|
458
489
|
|
|
459
490
|
Só documentação: leva ao pacote a correção de nota que a 12.5.4 recebeu no repositório depois de
|
|
@@ -800,6 +831,7 @@ todas as mensagens que apenas têm comportamento `sticky` disponível.
|
|
|
800
831
|
fronteira memoizada separada do estado do composer, e cada bloco Markdown reaproveita o
|
|
801
832
|
resultado enquanto seu conteúdo não muda. Conversas longas mantêm o custo de digitação
|
|
802
833
|
constante; durante streaming, somente a mensagem alterada volta a passar pelo parser.
|
|
834
|
+
|
|
803
835
|
## 9.0.3 — 2026-08-12
|
|
804
836
|
|
|
805
837
|
**O pre-push ignora worktrees internos do Maestro.** Projetos sob `.maestro/` pertencem
|
|
@@ -1033,11 +1065,11 @@ de backend de pé — nem, pior, falar com prod por um proxy esquecido no vite.c
|
|
|
1033
1065
|
`vite --mode design`, o vite.config traz `opusDesign({ devBackend: true })` (dev normal
|
|
1034
1066
|
serve o handler real via /api; design roteia pro mock) e o domínio-exemplo mostra o
|
|
1035
1067
|
split canônico: `defineContract` com `mockHandler` alimentado pelo schema (`fakeMany`)
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1068
|
+
- `bindAction` com o handler in-memory — o App consome o CONTRATO via `useListAction`
|
|
1069
|
+
(fim do "chama o handler direto, sem servidor ainda"), e o teste cobre o bound E o
|
|
1070
|
+
contrato puro em design. O plugin também marca `@softize/opus` como `ssr.noExternal`:
|
|
1071
|
+
o pacote embarca TS source e, externalizado, o node recusa TS sob node_modules no
|
|
1072
|
+
load SSR do entry.
|
|
1041
1073
|
- **`options: { kind: 'dictionary', ref }` agora resolve.** O `TbdlibProvider` ganha
|
|
1042
1074
|
`dicts` (`Record<ref, DictLike>` — o `DictType` do `t.dict` encaixa direto) e a UI
|
|
1043
1075
|
resolve a origem declarada no contrato: no `ActionList`, filtro e chips mostram os
|
|
@@ -1292,7 +1324,7 @@ todos. Elemento novo (table, figure, dl) entra sozinho.
|
|
|
1292
1324
|
|
|
1293
1325
|
**O respiro fantasma nas mensagens.** As bolhas do `Chat` tentavam zerar a margem do
|
|
1294
1326
|
primeiro e do último bloco com `[&>*:first-child]:mt-0`, mas o primeiro filho da bolha é o
|
|
1295
|
-
|
|
1327
|
+
_wrapper_ do `prose` — que não tem margem; quem tem é o `<p>` um nível abaixo. A
|
|
1296
1328
|
compensação nunca surtiu efeito, e cada mensagem carregava ~16px invisíveis em cima e
|
|
1297
1329
|
embaixo. Agora mora dentro do `Markdown`, onde alcança o bloco certo.
|
|
1298
1330
|
|
|
@@ -1644,7 +1676,7 @@ ver a nota de migração do t.json()).
|
|
|
1644
1676
|
- **`readOnlyContextPool` (`@softize/opus/data/readonly-pool`).** SQL escrito por LLM
|
|
1645
1677
|
com as QUATRO defesas dos ADRs 0003/0007 — todas no BANCO, nenhuma em regex: pool com
|
|
1646
1678
|
teto (`max` do pool e/ou `maxConcurrent` com fila própria), `BEGIN TRANSACTION READ
|
|
1647
|
-
|
|
1679
|
+
ONLY`, `SET LOCAL ROLE` por transação (alcance do contexto; roles/grants
|
|
1648
1680
|
default-fechado são migração sua) e protocolo estendido (o SQL roda sempre com array
|
|
1649
1681
|
de valores — multi-sentença é recusada pelo protocolo). Saindo, `DISCARD ALL` devolve
|
|
1650
1682
|
a conexão limpa; limpeza que falha DESTRÓI a conexão (nunca volta suja ao pool).
|
|
@@ -1676,7 +1708,7 @@ novos, todos aditivos).
|
|
|
1676
1708
|
mas roda qualquer um (o hook usa name/kind e lê `invalidates`) — disparar um
|
|
1677
1709
|
FormContract fora de form (modal composto que submete `role.update`) exigia
|
|
1678
1710
|
`as unknown as SimpleContract<...>`. Agora o parâmetro é `ActionContract<TInput,
|
|
1679
|
-
|
|
1711
|
+
TData>`; o cast morre. Nenhuma chamada existente muda.
|
|
1680
1712
|
- **`useActionFormContext()` (novo export, `ui/react`).** A válvula de escape pra
|
|
1681
1713
|
CONTROLE CUSTOM no modo composição do `<ActionForm>`: campo com UI própria (a grade
|
|
1682
1714
|
de permissões que forçou abandonar o form inteiro) participa do form do contrato —
|
|
@@ -1765,9 +1797,9 @@ o guard agora moram na base, então qualquer consumidor de IA do Opus nasce prot
|
|
|
1765
1797
|
breaking** (tipos/campos/exports novos, aditivos).
|
|
1766
1798
|
|
|
1767
1799
|
- **Contrato de pergunta (novos tipos, `@softize/opus`).** `AskQuestion` (enunciado + `header`
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1800
|
+
- `options` + `multiSelect`), `AskOption` e `AskAnswer` (rótulos escolhidos + texto livre) —
|
|
1801
|
+
a forma única que todo mundo fala. Espelha o AskUserQuestion do Claude Code, mas servida por
|
|
1802
|
+
um round-trip REAL.
|
|
1771
1803
|
- **`AiRunOptions.onAsk` / `BoundAiRunOptions.onAsk` (novo hook).** O round-trip de "perguntar
|
|
1772
1804
|
à pessoa": com ele, o driver INTERCEPTA a tool reservada `ask_user` (injetada automaticamente
|
|
1773
1805
|
no catálogo) e chama o handler, devolvendo as respostas ao modelo. Passa direto por `ctx.ai`
|
|
@@ -1859,8 +1891,8 @@ Correções da 2.35.0 (que saiu antes de passar pelo review). **Prefira esta à
|
|
|
1859
1891
|
|
|
1860
1892
|
## 2.35.0 — 2026-07-21
|
|
1861
1893
|
|
|
1862
|
-
Sidebar recolhível no `AppShell` e o **rem base passa a 15px**.
|
|
1863
|
-
que a 2.35.1 conserta — use a 2.35.1.)
|
|
1894
|
+
Sidebar recolhível no `AppShell` e o **rem base passa a 15px**. _(Publicada com os defeitos
|
|
1895
|
+
que a 2.35.1 conserta — use a 2.35.1.)_
|
|
1864
1896
|
|
|
1865
1897
|
- **Base do rem em 15px. Visual: GLOBAL — e o delta depende de onde você está.** Todo `rem`
|
|
1866
1898
|
deriva daqui, então tipografia, espaçamento e tamanho de controle mudam em toda tela no
|
|
@@ -1875,9 +1907,10 @@ que a 2.35.1 conserta — use a 2.35.1.)*
|
|
|
1875
1907
|
reincidência). Quem quiser a escala densa fixa `html { font-size: 14px }` no próprio entry.
|
|
1876
1908
|
**Consumidor que já sobrescrevia pra 15px pode remover o override** — vira redundante, não
|
|
1877
1909
|
conflitante (mesmo valor), então dá pra limpar sem pressa.
|
|
1878
|
-
<br
|
|
1910
|
+
<br>_Correção de registro:_ a entrada da 2.31.5 saiu com dois erros (chamava 15px de "a
|
|
1879
1911
|
escala anterior" e dizia "encolhe ~6,7%"), ambos por supor uma baseline de 15px que nunca
|
|
1880
1912
|
existiu. Corrigidos lá, com nota.
|
|
1913
|
+
|
|
1881
1914
|
- **`AppShell` ganha `collapsible` (opt-in).** Sem a prop, nada muda. Com ela, o shell
|
|
1882
1915
|
controla a **largura** e publica o estado; **o que some é decisão do conteúdo**, porque o
|
|
1883
1916
|
`sidebarNav` é do consumidor. Em vez de exigir estado no app, o aside expõe
|
|
@@ -1946,7 +1979,7 @@ para cá.**
|
|
|
1946
1979
|
empilhava uma entrada. Era o caso pt-BR inteiro.
|
|
1947
1980
|
- Some a última cópia à mão do mecanismo, que tinha sobrado dentro do próprio
|
|
1948
1981
|
`registry.tsx` (o redirect de `/docs/ui` pra `/ui` — agora `navigate('/ui', { replace:
|
|
1949
|
-
|
|
1982
|
+
true })`, porque redirect não merece entrada no histórico).
|
|
1950
1983
|
|
|
1951
1984
|
## 2.34.0 — 2026-07-21
|
|
1952
1985
|
|
package/bin/lib/check.mjs
CHANGED
|
@@ -107,9 +107,10 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
107
107
|
["PageHeader", new Set(["Page"])],
|
|
108
108
|
["PageIntro", new Set(["Page"])],
|
|
109
109
|
["PageBody", new Set(["Page"])],
|
|
110
|
+
["PageFooter", new Set(["Page"])],
|
|
110
111
|
["PageTitle", new Set(["PageHeader", "PageIntro"])],
|
|
111
|
-
["PageBack", new Set(["PageHeader"])],
|
|
112
|
-
["PageNavigation", new Set(["PageHeader"])],
|
|
112
|
+
["PageBack", new Set(["PageHeader", "PageIntro"])],
|
|
113
|
+
["PageNavigation", new Set(["PageHeader", "PageIntro"])],
|
|
113
114
|
["PageDescription", new Set(["PageHeader", "PageIntro"])],
|
|
114
115
|
["PageActions", new Set(["PageHeader", "PageIntro"])],
|
|
115
116
|
["ContentHeader", new Set(["Content"])],
|
|
@@ -169,11 +170,14 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
169
170
|
header: "PageHeader",
|
|
170
171
|
alternateHeader: "PageIntro",
|
|
171
172
|
body: "PageBody",
|
|
173
|
+
footer: "PageFooter",
|
|
172
174
|
shorthand: new Set(["title", "description", "actions"]),
|
|
173
175
|
// O contador saiu do título da página (ADR 0009). Tirar `count` do shorthand não proíbe
|
|
174
176
|
// nada sozinho — só apaga o marcador que fazia o gate enxergar aquele Page —, então a
|
|
175
177
|
// prop removida é cobrada aqui, nas duas formas de composição.
|
|
176
|
-
removed: new Map([
|
|
178
|
+
removed: new Map([
|
|
179
|
+
["count", "o total pertence ao conteúdo que o explica"],
|
|
180
|
+
]),
|
|
177
181
|
},
|
|
178
182
|
],
|
|
179
183
|
[
|
|
@@ -191,6 +195,7 @@ const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
|
191
195
|
"PageHeader",
|
|
192
196
|
"PageIntro",
|
|
193
197
|
"PageBody",
|
|
198
|
+
"PageFooter",
|
|
194
199
|
"PageTitle",
|
|
195
200
|
"PageBack",
|
|
196
201
|
"PageNavigation",
|
|
@@ -207,6 +212,44 @@ const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
|
207
212
|
"AlertActions",
|
|
208
213
|
]);
|
|
209
214
|
|
|
215
|
+
const UI_REMOVED_PROPS = new Map([
|
|
216
|
+
[
|
|
217
|
+
"PageHeader",
|
|
218
|
+
new Map([
|
|
219
|
+
["variant", "use `PageShell` para a barra persistente da página"],
|
|
220
|
+
]),
|
|
221
|
+
],
|
|
222
|
+
[
|
|
223
|
+
"ActionFormDialog",
|
|
224
|
+
new Map([
|
|
225
|
+
[
|
|
226
|
+
"description",
|
|
227
|
+
"use `intro` somente para contexto relevante no início do corpo",
|
|
228
|
+
],
|
|
229
|
+
]),
|
|
230
|
+
],
|
|
231
|
+
[
|
|
232
|
+
"ActionListDialog",
|
|
233
|
+
new Map([
|
|
234
|
+
[
|
|
235
|
+
"description",
|
|
236
|
+
"use `intro` somente para contexto relevante no início do corpo",
|
|
237
|
+
],
|
|
238
|
+
]),
|
|
239
|
+
],
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
const UI_REMOVED_COMPONENTS = new Map([
|
|
243
|
+
[
|
|
244
|
+
"DialogDescription",
|
|
245
|
+
"mova contexto relevante para o início de `DialogBody`",
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
"DrawerDescription",
|
|
249
|
+
"mova contexto relevante para o início de `DrawerBody`",
|
|
250
|
+
],
|
|
251
|
+
]);
|
|
252
|
+
|
|
210
253
|
const UI_STRUCTURAL_CONTAINERS = new Set(["PageShell"]);
|
|
211
254
|
|
|
212
255
|
const UI_STRUCTURAL_HEADERS = new Map([
|
|
@@ -228,7 +271,13 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
228
271
|
"PageIntro",
|
|
229
272
|
{
|
|
230
273
|
title: "PageTitle",
|
|
231
|
-
optional: new Set([
|
|
274
|
+
optional: new Set([
|
|
275
|
+
"PageBack",
|
|
276
|
+
"PageNavigation",
|
|
277
|
+
"PageDescription",
|
|
278
|
+
"PageActions",
|
|
279
|
+
]),
|
|
280
|
+
exclusive: [new Set(["PageBack", "PageNavigation"])],
|
|
232
281
|
shorthand: new Set(),
|
|
233
282
|
},
|
|
234
283
|
],
|
|
@@ -295,14 +344,7 @@ const UI_LEGACY_VARIANTS = new Map([
|
|
|
295
344
|
],
|
|
296
345
|
[
|
|
297
346
|
"Alert",
|
|
298
|
-
new Set([
|
|
299
|
-
"default",
|
|
300
|
-
"destructive",
|
|
301
|
-
"info",
|
|
302
|
-
"success",
|
|
303
|
-
"warning",
|
|
304
|
-
"danger",
|
|
305
|
-
]),
|
|
347
|
+
new Set(["default", "destructive", "info", "success", "warning", "danger"]),
|
|
306
348
|
],
|
|
307
349
|
[
|
|
308
350
|
"Dot",
|
|
@@ -969,6 +1011,16 @@ export function checkUiStructure(file, text) {
|
|
|
969
1011
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
970
1012
|
const local = jsxLocalName(node);
|
|
971
1013
|
const component = local === null ? null : imports.get(local);
|
|
1014
|
+
const removedComponent =
|
|
1015
|
+
component === null ? undefined : UI_REMOVED_COMPONENTS.get(component);
|
|
1016
|
+
if (component !== null && removedComponent !== undefined) {
|
|
1017
|
+
add(
|
|
1018
|
+
node,
|
|
1019
|
+
component,
|
|
1020
|
+
`${component} não faz mais parte da API pública: ${removedComponent}.`,
|
|
1021
|
+
"ui-structure-removed-component",
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
972
1024
|
const parents =
|
|
973
1025
|
component === null ? undefined : UI_STRUCTURAL_PARENTS.get(component);
|
|
974
1026
|
if (parents !== undefined) {
|
|
@@ -990,6 +1042,29 @@ export function checkUiStructure(file, text) {
|
|
|
990
1042
|
}
|
|
991
1043
|
}
|
|
992
1044
|
|
|
1045
|
+
const removedProps =
|
|
1046
|
+
component === null ? undefined : UI_REMOVED_PROPS.get(component);
|
|
1047
|
+
if (removedProps !== undefined) {
|
|
1048
|
+
const opening = jsxOpening(node);
|
|
1049
|
+
const attributes = new Set(
|
|
1050
|
+
opening.attributes.properties.flatMap((attribute) =>
|
|
1051
|
+
ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name)
|
|
1052
|
+
? [attribute.name.text]
|
|
1053
|
+
: [],
|
|
1054
|
+
),
|
|
1055
|
+
);
|
|
1056
|
+
for (const [attribute, hint] of removedProps) {
|
|
1057
|
+
if (attributes.has(attribute)) {
|
|
1058
|
+
add(
|
|
1059
|
+
node,
|
|
1060
|
+
component,
|
|
1061
|
+
`${component} não aceita mais a propriedade \`${attribute}\`: ${hint}.`,
|
|
1062
|
+
"ui-structure-removed-prop",
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
993
1068
|
const header =
|
|
994
1069
|
component === null ? undefined : UI_STRUCTURAL_HEADERS.get(component);
|
|
995
1070
|
if (header !== undefined) {
|
|
@@ -1090,7 +1165,8 @@ export function checkUiStructure(file, text) {
|
|
|
1090
1165
|
).filter(Boolean);
|
|
1091
1166
|
const structural =
|
|
1092
1167
|
acceptedHeaders.some((header) => children.names.includes(header)) ||
|
|
1093
|
-
children.names.includes(root.body)
|
|
1168
|
+
children.names.includes(root.body) ||
|
|
1169
|
+
(root.footer !== undefined && children.names.includes(root.footer));
|
|
1094
1170
|
// A prop removida saiu do `shorthand`, então sozinha ela não marca o nó como forma curta:
|
|
1095
1171
|
// ele cai no ramo explícito, que cobra header e body — uma segunda mensagem inteiramente
|
|
1096
1172
|
// induzida pela prop. Sem filhos estruturais essa cobrança dispara de qualquer jeito, e
|
|
@@ -1113,19 +1189,26 @@ export function checkUiStructure(file, text) {
|
|
|
1113
1189
|
const bodies = children.names.filter(
|
|
1114
1190
|
(name) => name === root.body,
|
|
1115
1191
|
).length;
|
|
1192
|
+
const footers = children.names.filter(
|
|
1193
|
+
(name) => name === root.footer,
|
|
1194
|
+
).length;
|
|
1116
1195
|
const unexpected = children.names.some(
|
|
1117
|
-
(name) =>
|
|
1196
|
+
(name) =>
|
|
1197
|
+
!acceptedHeaders.includes(name) &&
|
|
1198
|
+
name !== root.body &&
|
|
1199
|
+
name !== root.footer,
|
|
1118
1200
|
);
|
|
1119
1201
|
if (
|
|
1120
1202
|
headers !== 1 ||
|
|
1121
1203
|
bodies !== 1 ||
|
|
1204
|
+
footers > 1 ||
|
|
1122
1205
|
unexpected ||
|
|
1123
1206
|
children.hasOpaque
|
|
1124
1207
|
) {
|
|
1125
1208
|
add(
|
|
1126
1209
|
node,
|
|
1127
1210
|
component,
|
|
1128
|
-
`${component} explícito exige exatamente um ${acceptedHeaders.join(" ou ")} e um ${root.
|
|
1211
|
+
`${component} explícito exige exatamente um ${acceptedHeaders.join(" ou ")}, um ${root.body} e no máximo um ${root.footer ?? "footer"} como filhos diretos.`,
|
|
1129
1212
|
);
|
|
1130
1213
|
}
|
|
1131
1214
|
}
|