@softize/opus 18.5.0 → 20.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 (151) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/check.mjs +198 -10
  4. package/bin/lib/copy.mjs +29 -0
  5. package/bin/lib/mcp.mjs +1 -1
  6. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
  7. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  8. package/docs/data-products.md +15 -9
  9. package/docs/protocol.md +3 -4
  10. package/docs/releasing.md +2 -2
  11. package/package.json +2 -2
  12. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  13. package/registry/templates/app/package.json +1 -1
  14. package/src/core/runtime.ts +96 -8
  15. package/src/core/types.ts +5 -2
  16. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  17. package/src/ui/components/patterns/content-header.tsx +16 -12
  18. package/src/ui/components/patterns/fill-region.ts +150 -0
  19. package/src/ui/components/patterns/heading-level.ts +16 -0
  20. package/src/ui/components/patterns/list.tsx +103 -73
  21. package/src/ui/components/patterns/page.tsx +20 -4
  22. package/src/ui/components/patterns/pane.tsx +294 -0
  23. package/src/ui/components/patterns/presentation.tsx +81 -54
  24. package/src/ui/components/patterns/sidebar.tsx +13 -72
  25. package/src/ui/components/patterns/split.tsx +51 -38
  26. package/src/ui/components/patterns/surface-anatomy.ts +246 -0
  27. package/src/ui/components/patterns/surface-header.tsx +51 -6
  28. package/src/ui/components/patterns/surface-media.tsx +69 -0
  29. package/src/ui/components/patterns/trigger.tsx +12 -2
  30. package/src/ui/components/primitives/alert.tsx +9 -1
  31. package/src/ui/components/primitives/avatar.tsx +7 -3
  32. package/src/ui/components/primitives/card.tsx +91 -29
  33. package/src/ui/components/primitives/carousel.tsx +34 -5
  34. package/src/ui/components/primitives/checkbox.tsx +11 -3
  35. package/src/ui/components/primitives/command.tsx +14 -5
  36. package/src/ui/components/primitives/copyable.tsx +120 -10
  37. package/src/ui/components/primitives/detail.tsx +10 -13
  38. package/src/ui/components/primitives/dialog.tsx +179 -45
  39. package/src/ui/components/primitives/drawer.tsx +131 -24
  40. package/src/ui/components/primitives/empty.tsx +1 -1
  41. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  42. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  43. package/src/ui/components/primitives/input-group.tsx +9 -2
  44. package/src/ui/components/primitives/input-otp.tsx +1 -1
  45. package/src/ui/components/primitives/kbd.tsx +1 -1
  46. package/src/ui/components/primitives/label.tsx +3 -0
  47. package/src/ui/components/primitives/menu.tsx +5 -0
  48. package/src/ui/components/primitives/metric-card.tsx +98 -39
  49. package/src/ui/components/primitives/popover.tsx +1 -1
  50. package/src/ui/components/primitives/progress.tsx +9 -1
  51. package/src/ui/components/primitives/slider.tsx +62 -11
  52. package/src/ui/components/primitives/truncate.tsx +10 -1
  53. package/src/ui/docs/content/accordion.md +74 -27
  54. package/src/ui/docs/content/action-form-card.md +24 -15
  55. package/src/ui/docs/content/action-form-dialog.md +17 -7
  56. package/src/ui/docs/content/action-form.md +92 -77
  57. package/src/ui/docs/content/action-list-dialog.md +75 -38
  58. package/src/ui/docs/content/action-list.md +134 -52
  59. package/src/ui/docs/content/action-trigger.md +27 -10
  60. package/src/ui/docs/content/action-view.md +14 -14
  61. package/src/ui/docs/content/actions.md +161 -66
  62. package/src/ui/docs/content/ai.md +43 -44
  63. package/src/ui/docs/content/alert.md +83 -29
  64. package/src/ui/docs/content/ask.md +51 -26
  65. package/src/ui/docs/content/aspect-ratio.md +22 -24
  66. package/src/ui/docs/content/audit.md +23 -19
  67. package/src/ui/docs/content/auth.md +38 -19
  68. package/src/ui/docs/content/avatar.md +54 -5
  69. package/src/ui/docs/content/badge.md +29 -9
  70. package/src/ui/docs/content/breadcrumb.md +21 -8
  71. package/src/ui/docs/content/button.md +73 -43
  72. package/src/ui/docs/content/calendar.md +134 -26
  73. package/src/ui/docs/content/card.md +252 -35
  74. package/src/ui/docs/content/carousel.md +37 -6
  75. package/src/ui/docs/content/chat.md +118 -37
  76. package/src/ui/docs/content/checkbox.md +73 -7
  77. package/src/ui/docs/content/cli.md +94 -55
  78. package/src/ui/docs/content/collapsible.md +43 -19
  79. package/src/ui/docs/content/command.md +30 -8
  80. package/src/ui/docs/content/composer.md +23 -8
  81. package/src/ui/docs/content/content.md +37 -7
  82. package/src/ui/docs/content/copyable.md +39 -8
  83. package/src/ui/docs/content/customization.md +55 -46
  84. package/src/ui/docs/content/cycle.md +33 -18
  85. package/src/ui/docs/content/data-products.md +115 -0
  86. package/src/ui/docs/content/data-state.md +20 -6
  87. package/src/ui/docs/content/data.md +20 -19
  88. package/src/ui/docs/content/detail.md +12 -5
  89. package/src/ui/docs/content/dialog.md +219 -28
  90. package/src/ui/docs/content/dictionary-value.md +4 -4
  91. package/src/ui/docs/content/dock.md +54 -19
  92. package/src/ui/docs/content/dot.md +35 -5
  93. package/src/ui/docs/content/drawer.md +193 -22
  94. package/src/ui/docs/content/empty-value.md +20 -6
  95. package/src/ui/docs/content/empty.md +24 -3
  96. package/src/ui/docs/content/events.md +76 -16
  97. package/src/ui/docs/content/executions.md +9 -9
  98. package/src/ui/docs/content/field.md +21 -6
  99. package/src/ui/docs/content/getting-started.md +44 -34
  100. package/src/ui/docs/content/icon-picker.md +19 -13
  101. package/src/ui/docs/content/icon-tile.md +28 -5
  102. package/src/ui/docs/content/input-otp.md +58 -7
  103. package/src/ui/docs/content/input.md +22 -3
  104. package/src/ui/docs/content/item.md +53 -6
  105. package/src/ui/docs/content/kbd.md +5 -1
  106. package/src/ui/docs/content/label.md +13 -4
  107. package/src/ui/docs/content/log.md +5 -5
  108. package/src/ui/docs/content/markdown.md +5 -4
  109. package/src/ui/docs/content/mcp.md +49 -23
  110. package/src/ui/docs/content/menu.md +95 -10
  111. package/src/ui/docs/content/metric-card.md +68 -21
  112. package/src/ui/docs/content/observability.md +10 -9
  113. package/src/ui/docs/content/page.md +125 -11
  114. package/src/ui/docs/content/pagination.md +13 -5
  115. package/src/ui/docs/content/pane.md +300 -0
  116. package/src/ui/docs/content/popover.md +42 -5
  117. package/src/ui/docs/content/presentation.md +91 -47
  118. package/src/ui/docs/content/progress.md +27 -14
  119. package/src/ui/docs/content/queue.md +39 -21
  120. package/src/ui/docs/content/radio-group.md +26 -4
  121. package/src/ui/docs/content/router.md +29 -10
  122. package/src/ui/docs/content/runtime.md +90 -54
  123. package/src/ui/docs/content/scheduler.md +53 -21
  124. package/src/ui/docs/content/scroll-area.md +42 -35
  125. package/src/ui/docs/content/select.md +78 -33
  126. package/src/ui/docs/content/semantic-context.md +21 -2
  127. package/src/ui/docs/content/separator.md +15 -9
  128. package/src/ui/docs/content/sidebar.md +25 -45
  129. package/src/ui/docs/content/skeleton.md +13 -4
  130. package/src/ui/docs/content/slider.md +37 -9
  131. package/src/ui/docs/content/spinner.md +4 -3
  132. package/src/ui/docs/content/split.md +34 -20
  133. package/src/ui/docs/content/storage.md +8 -9
  134. package/src/ui/docs/content/switch.md +10 -7
  135. package/src/ui/docs/content/table.md +46 -8
  136. package/src/ui/docs/content/tabs.md +14 -4
  137. package/src/ui/docs/content/testing.md +76 -31
  138. package/src/ui/docs/content/textarea.md +5 -4
  139. package/src/ui/docs/content/toast.md +48 -7
  140. package/src/ui/docs/content/toggle.md +3 -3
  141. package/src/ui/docs/content/tokens.md +15 -26
  142. package/src/ui/docs/content/tooltip.md +18 -2
  143. package/src/ui/docs/content/truncate.md +36 -6
  144. package/src/ui/docs/content/ui.md +74 -22
  145. package/src/ui/docs/content/upgrading.md +45 -36
  146. package/src/ui/docs/plugin.ts +36 -2
  147. package/src/ui/docs/registry.tsx +14 -2
  148. package/src/ui/drivers/react.tsx +16 -8
  149. package/src/ui/meta.ts +38 -32
  150. package/src/ui/react.tsx +14 -0
  151. package/src/vite/design.ts +16 -4
package/CHANGELOG.md CHANGED
@@ -7,6 +7,281 @@ 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
+ ## 20.0.0 — 2026-09-26
11
+
12
+ Esta versão corrige componentes que falhavam em acessibilidade ou em layout, leva o `fill` da
13
+ página até a lista sozinho, torna explícitas falhas que antes passavam em silêncio, reescreve boa
14
+ parte da documentação e atualiza a Base para a 2.4.0. É major porque algumas dessas mudanças podem
15
+ interromper a aplicação, o boot ou os gates de um projeto que funcionava na 19.0.0: a seção
16
+ **Breaking** diz o que fazer em cada uma. Outros pontos podem pedir ajuste sem interromper a
17
+ aplicação; alguns só afetam testes:
18
+
19
+ - `Slider` envolvido em `role="group"` com `aria-labelledby`, como a documentação anterior
20
+ recomendava: passe o `aria-labelledby` ao próprio `Slider`;
21
+ - `Truncate` dentro de link ou botão: passe `tabIndex={-1}` para evitar uma segunda parada;
22
+ - conteúdo próprio colocado direto no `Avatar`: arredonde-o, porque o root deixou de recortar;
23
+ - testes que buscam um `Alert` informativo ou de sucesso por `getByRole('alert')`: use `status`;
24
+ - testes que buscam um `Copyable` com rótulo pelo nome “Copiar”: use o texto visível;
25
+ - `closeDisabled` em Dialog e Drawer passa a bloquear também Esc e clique fora; handlers criados só
26
+ para isso podem sair;
27
+ - testes que buscam o botão de confirmar do `ActionTrigger` pelo nome longo do gatilho: ele agora
28
+ usa o rótulo da action;
29
+ - testes que conferem o texto do erro de um hook de action fora do `OpusProvider`: a mensagem mudou;
30
+ - `ActionList` sem `layout`, filha direta de `PageBody` ou `ContentBody` em uma `Page` com
31
+ `layout="fill"`: passa a ocupar a altura; declare `layout="flow"` para manter o comportamento
32
+ anterior. Os demais filhos de cada região que repassa a altura passam a ocupar a largura dela; dê
33
+ `self-start` a um botão que deva manter a largura do conteúdo.
34
+
35
+ ### Breaking
36
+
37
+ **`Split` recusa filho que não seja `Pane`.** Antes, esse filho — e um `Pane` escrito dentro de um
38
+ fragment — sumia da tela sem aviso. Agora o fragment é expandido e qualquer outro filho interrompe a
39
+ renderização com um erro que aponta o nó. Migração: envolva o conteúdo em `<Pane>`; se o componente
40
+ que você passa ao `Split` devolve um `Pane`, escreva o `<Pane>` diretamente no `Split` e ponha o
41
+ componente dentro dele. Espaço em branco, `null` e `false` continuam ignorados, mas um `0` vindo de
42
+ `{items.length && <Pane />}` interrompe: use `items.length > 0 &&`.
43
+
44
+ **`runtime.start()` valida os schedules.** Um schedule ativo que aponta para uma action não
45
+ registrada faz o boot falhar com `runtime.schedule_action_not_found`; antes, cada disparo falhava em
46
+ silêncio. Migração: corrija o nome da action ou declare `enabled: false` no schedule. Rode o
47
+ `start()` num teste com o scheduler para descobrir antes do deploy.
48
+
49
+ **`opus check` ganha duas regras que reprovam o gate.** `ignored-by-kind` aponta `emits` ou
50
+ `background` em action `list` ou `view`, que o runtime sempre ignorou; `ui-structure-split` aponta
51
+ filho de `Split` que não seja `Pane`. Migração: remova os campos ignorados e ajuste o `Split` como
52
+ acima.
53
+
54
+ **`opus check` cobra a anatomia dos headers da 19.0.0.** A regra `ui-structure` passa a reprovar
55
+ `CardMedia`, `DialogNavigation`, `DialogActions`, `DrawerNavigation`, `DrawerMedia`, `DrawerActions`,
56
+ `PaneMedia`, `PaneTitle`, `PaneDescription` e `PaneActions` fora do header da própria família —
57
+ antes, esses slots renderizavam em qualquer lugar sem aviso. Migração: mova-os para dentro de
58
+ `CardHeader`, `DialogHeader`, `DrawerHeader` ou `PaneHeader`.
59
+
60
+ **`opus copy --check` inventaria `PaneTitle` e `PaneDescription`.** Um título ou descrição de pane
61
+ com conteúdo dinâmico, como `<PaneTitle>{name}</PaneTitle>`, passa a gerar diagnóstico e reprovar o
62
+ gate. Migração: rode `opus copy`; para texto dinâmico, use uma constante ou uma `I18nRef` com
63
+ `default`, ou declare a superfície em `copy.dynamic` com seletor, tipo, motivo e referência.
64
+
65
+ **Três tipos seguem o elemento renderizado.** `PopoverTitle` e `EmptyDescription` são tipados como
66
+ `div` e `KbdGroup` como `kbd`. O DOM não muda e raramente há algo a ajustar: só um código que
67
+ dependia do tipo `HTMLHeadingElement` ou `HTMLParagraphElement` nesses componentes.
68
+
69
+ ### Avatar
70
+
71
+ O `AvatarBadge` aparece inteiro em todos os tamanhos. O selo fica no canto da caixa do `Avatar`,
72
+ por fora do círculo, e o root recortava o próprio conteúdo com `overflow-hidden`: só um quarto do
73
+ selo ficava visível. Agora `AvatarImage` e `AvatarFallback` herdam o raio do root e fazem o próprio
74
+ recorte. A forma continua declarada no `Avatar` — circular por padrão, ou `className="rounded-lg"`
75
+ para um logo — e o anel de fundo do `AvatarGroup` a acompanha.
76
+
77
+ Se o seu projeto contornava o corte com `overflow-visible` no `Avatar` ou `rounded-full` no
78
+ `AvatarImage`, pode remover essas classes. Conteúdo próprio colocado direto no `Avatar`, fora de
79
+ `AvatarImage` e `AvatarFallback`, deixa de ser recortado: arredonde-o no próprio elemento.
80
+
81
+ ### Acessibilidade
82
+
83
+ - **`Alert`** anuncia conforme o contexto: `role="alert"` em `danger` e `warning`, que interrompem o
84
+ leitor de tela, e `role="status"` nos demais, que esperam a leitura em curso terminar. Um `role`
85
+ passado ao componente continua vencendo.
86
+ - **`Progress`** passa `value` ao progressbar, que expõe `aria-valuenow` e `data-state`
87
+ (`loading`, `complete` ou `indeterminate`), e respeita `max`: `value={3} max={4}` preenche três
88
+ quartos da barra.
89
+ - **`Slider`** nomeia os controles. `aria-label` e `aria-labelledby` passados ao `Slider` nomeiam o
90
+ thumb, que é o elemento focável; em intervalo, a nova prop `thumbLabels` dá um nome a cada
91
+ controle. Sem `thumbLabels`, o nome comum se compõe com a posição (“Preço, mínimo”); sem nome
92
+ algum, o intervalo anuncia “Mínimo” e “Máximo”. Se você envolvia o Slider
93
+ em um `role="group"` com `aria-labelledby`, passe o `aria-labelledby` ao próprio `Slider`.
94
+ - **`Copyable`** com rótulo visível passa a se chamar pelo próprio texto, e quem usa comando de voz
95
+ consegue acioná-lo dizendo o que vê. Sem filhos, continua “Copiar” e “Copiado”. A confirmação é
96
+ anunciada por leitores de tela, e o botão só confirma quando o navegador aceita a cópia: com a
97
+ permissão negada, continua em “Copiar” e nenhum erro fica sem tratamento. Se você passava
98
+ `aria-label` só para repetir o texto visível, pode removê-lo.
99
+ - **`Truncate`** com texto cortado entra na ordem de tabulação, e a dica passa a abrir pelo
100
+ teclado. Dentro de um link ou botão, passe `tabIndex={-1}` para evitar uma segunda parada no
101
+ mesmo item.
102
+ - **`Carousel`** vertical responde às setas para cima e para baixo. Setas usadas em campos ou em
103
+ controles que navegam pelas setas dentro dos slides, como Slider, RadioGroup e Tabs, não trocam
104
+ mais de slide.
105
+
106
+ ### Formulário
107
+
108
+ - **`Checkbox`** indeterminado tem aparência própria: o preenchimento de marcado com um traço.
109
+ - **`IconPicker`** mede 2.25rem, como `Input` e `Select`, e mostra a moldura de erro com
110
+ `aria-invalid`.
111
+ - **`InputOTP`** usa nas casas o mesmo fundo dos campos no tema claro.
112
+ - **`InputGroup`** esmaece os addons sozinho quando o controle está desabilitado, e o clique no
113
+ addon também foca o `InputGroupTextarea`.
114
+ - **`Label`** esmaece dentro de `<Field data-disabled="true">`.
115
+ - **`CommandEmpty`** soma a `className` do consumidor ao estilo padrão, em vez de substituí-lo.
116
+
117
+ ### Layout
118
+
119
+ `Page layout="fill"` leva a altura até a `ActionList` sozinha. `PageBody`, `Content`, `ContentBody`
120
+ e o corpo da `Presentation` repassam a altura quando contêm uma lista, e a lista, sem `layout`,
121
+ assume `fill` quando é filha direta de um deles. Seções irmãs sem lista mantêm a altura do conteúdo,
122
+ uma página `fill` sem lista não muda, e `Card`, `Dialog`, `Drawer` e `Pane` não herdam o `fill` da
123
+ página (ADR 0024). A `Presentation` de lista em `layout="fill"` passa a entregar o que prometia: a
124
+ lista ocupa a altura da página e só os resultados rolam.
125
+
126
+ Quem encadeava `flex min-h-0 flex-1 flex-col` à mão pode remover as classes; mantidas, continuam
127
+ funcionando. Com um contêiner próprio entre `ContentBody` e a lista, como um `TabsContent`, a lista
128
+ continua em `flow`: declare `layout="fill"` nela e dê ao contêiner `flex min-h-0 flex-1 flex-col`.
129
+
130
+ ### Superfícies e patterns
131
+
132
+ - **`closeDisabled`** em `Dialog` e `Drawer` bloqueia todo fechamento: além do botão, Esc e clique
133
+ ou foco fora. Em `Dialog mode="alert"`, bloqueia o Esc. Handlers `onEscapeKeyDown`,
134
+ `onPointerDownOutside` e `onInteractOutside` informados continuam rodando antes do bloqueio.
135
+ - **`ActionTrigger`**: sem `confirmLabel` no contrato, o botão de confirmar usa o `label` da action,
136
+ como “Excluir”, e não mais o `label` do gatilho, como “Excluir Unidade Centro”.
137
+ - **`Split`** mostra um `Pane` escrito dentro de um fragment (veja **Breaking**).
138
+ - **Card, Pane, Dialog e Drawer em `layout="continuous"`:** um cabeçalho sem corpo nem rodapé
139
+ fecha a superfície com o mesmo respiro que tem em cima. Nada muda quando há corpo ou rodapé.
140
+ - **`ActionFormCard`** ganha `titleLevel`, que escolhe o nível do heading do título (`h3` por
141
+ padrão) sem mudar a aparência.
142
+ - **Hooks de action** fora do `OpusProvider` lançam um erro que nomeia o hook chamado e explica
143
+ como resolver.
144
+
145
+ ### Runtime
146
+
147
+ - **Schedules:** além da validação no `start()` (veja **Breaking**), uma execução que falha passa a
148
+ ser registrada no log com o schedule, a action e o código do erro.
149
+ - **Avisos em desenvolvimento:** o runtime avisa quando uma reaction chama `ctx.emit`, que continua
150
+ sem publicar (`runtime.reaction_emit_ignored`), e quando uma action `list` ou `view` declara
151
+ `emits` ou `background`, que o runtime ignora (`runtime.action_field_ignored`). Em produção,
152
+ nada muda.
153
+
154
+ ### CLI
155
+
156
+ - **`opus check`** ganha `ignored-by-kind` e `ui-structure-split` (veja **Breaking**).
157
+ - **Skill `create-opus-action`:** o scaffold gera identificador válido para recurso em kebab-case
158
+ (`sales-order create-item` → `salesOrderCreateItem`). Rode `opus setup` para atualizar a cópia.
159
+ - **`opus check` e `opus introspect`** ignoram arquivos `*.test.*` e `*.spec.*` e as pastas `test`,
160
+ `tests`, `__tests__`, `fixtures` e `__fixtures__`: actions declaradas ali não são registradas, e o
161
+ check deixava de passar por causa delas. Uma action real guardada em uma pasta com esses nomes deixa
162
+ de ser verificada.
163
+ - **`opus check`** confere a anatomia real dos headers da 19.0.0 (veja **Breaking**).
164
+ - **`opus copy`** inventaria `PaneTitle` e `PaneDescription` (veja **Breaking**) e
165
+ `Slider.thumbLabels`.
166
+
167
+ ### Depreciações
168
+
169
+ `resolveIconTileStyle` (use `IconTile` ou `iconTileVariants`) e `MenuPortal` (`MenuContent` já usa
170
+ portal) estão depreciados e saem na próxima major.
171
+
172
+ ### Documentação
173
+
174
+ As páginas foram revisadas contra o código desta versão. Exemplos que não compilavam ou que
175
+ descreviam comportamento antigo foram corrigidos, componentes e props sem referência ganharam
176
+ tabela, e as páginas passaram a ligar umas às outras no ponto de decisão. Entre as mudanças:
177
+
178
+ - nova página Produtos de Dados, e seções sobre `defineDomain`, reactions,
179
+ `loads` e os providers necessários para consumir a UI;
180
+ - os exemplos voltam a ter o layout que o texto descreve: as classes usadas só nas páginas `.md`
181
+ passam a ser geradas no site e na rota `/__docs` do `opusDocs()`, sem mudar o CSS do seu app nem
182
+ o de produção;
183
+ - `ActionList` descreve a barra de filtros como funciona desde a 18.3.0;
184
+ - o exemplo de `opusDesign()` em `@softize/opus/vite` mostra o arranjo do template: import dinâmico
185
+ condicionado ao modo design e `vite --mode design --configLoader runner`. Um import estático do
186
+ plugin quebra o carregamento do `vite.config.ts` no Node 22+ com
187
+ `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`; se o seu config faz isso, adote o padrão do
188
+ template.
189
+
190
+ ### Base 2.4.0
191
+
192
+ O Opus passa a depender de `@softize/base ^2.4.0`, e o template do `opus create` também. A regra de
193
+ formulários da Base agora é a que o Opus já pratica: título `Criar {recurso}` ou
194
+ `Editar {recurso}`, ação `Salvar`.
195
+
196
+ ### Correções do histórico
197
+
198
+ - **19.0.0:** a entrada **Breaking** diz que nó solto dentro do header lança erro nos quatro
199
+ headers. Isso vale para `CardHeader`, `DialogHeader` e `DrawerHeader`; o `PaneHeader` aceita
200
+ conteúdo livre, como uma busca ou a marca, depois do título e da descrição. Só a repetição de um
201
+ slot lança erro.
202
+
203
+ ## 19.0.0 — 2026-09-24
204
+
205
+ Card, Dialog, Drawer e Pane passam a derivar a anatomia da mesma infraestrutura que Page e Content
206
+ já usavam. Cada superfície declara `layout="continuous" | "sectioned"` no seu componente raiz, e o
207
+ layout responde por um pacote: recuo das regiões, ritmo vertical, escala do título, posição das
208
+ ações, apresentação da mídia e divisores. O divisor passa a ser derivado da vizinhança das regiões —
209
+ um header seguido de corpo desenha a linha; um header sozinho, não.
210
+
211
+ Os headers passam a posicionar sozinhos o que antes o consumidor montava: mídia, título, descrição
212
+ (onde a família a tem), ações no extremo oposto e o botão de fechar depois delas. `Dialog`, `Drawer`
213
+ e `Pane` ganham slots de ações e de navegação; `Card` e `Pane` ganham slot de mídia; `Card` ganha
214
+ título com nível semântico.
215
+
216
+ A família `Pane` deixa de viver em dois módulos: raiz e slots agora moram juntos, com título,
217
+ mídia, ações, descrição e página própria na documentação.
218
+
219
+ `MetricCard` ganha ativação: com `onActivate`, `active` e `activationLabel`, a medida vira um
220
+ controle que aplica o recorte que ela resume — o card passa a emitir `aria-pressed` e a anunciar o
221
+ próprio nome. As três propriedades andam juntas pelo tipo; declarar `active` sem `onActivate` não
222
+ compila. A marcação de ativo é a borda na cor primária: antes era um anel desenhado para fora da
223
+ borda, que qualquer ancestral com `overflow-hidden` cortava. O anel de foco também passa a ser
224
+ interno.
225
+
226
+ Na mesma versão, a lista em `layout="fill"` troca a rolagem montada com um contêiner posicionado
227
+ por uma grade de duas linhas: os resultados ocupam a folga, o rodapé se mede pelo conteúdo, e o
228
+ contêiner da tabela abre mão da rolagem horizontal para não roubar do cabeçalho a referência do
229
+ `sticky`. O rodapé encosta na última linha e mantém a mesma altura com e sem paginador. O detalhe
230
+ aproxima o ícone do rótulo, na escala densa das superfícies produtivas.
231
+
232
+ ### Breaking
233
+
234
+ **Headers validam seus slots.** `CardHeader`, `DialogHeader`, `DrawerHeader` e `PaneHeader` aceitam
235
+ apenas os slots da família e no máximo um de cada. Nó solto dentro do header passa a lançar erro com
236
+ a lista do que é aceito. Migração: envolva ações em `CardAction`, `DialogActions`, `DrawerActions`
237
+ ou `PaneActions`; navegação em `DialogNavigation` ou `DrawerNavigation`; ícone em `CardMedia`,
238
+ `DrawerMedia` ou `PaneMedia`. Descrição dentro do header de modal não tem slot e vai para o começo
239
+ do corpo, como as ADRs 0014 e 0018 já determinavam.
240
+
241
+ **`CardTitle` virou heading.** Era um `div` sem escala tipográfica; agora emite `h3` por padrão,
242
+ aceita `level` para a hierarquia correta e declara a escala pelo layout. Migração: remova o heading
243
+ que você embutia dentro dele e o `text-*` que aplicava por fora; passe `level` quando a página pedir
244
+ outra hierarquia.
245
+
246
+ **Divisores saem do consumidor.** `<CardHeader className="border-b">` e
247
+ `<CardFooter className="border-t">` deixam de ser necessários: use `layout="sectioned"` no `Card`.
248
+ As classes continuam funcionando, mas somam uma segunda linha sobre a derivada.
249
+
250
+ **`PaneHeader` e `PaneFooter` ganharam recuo próprio** e o `Pane` passa a usar `inset="none"`
251
+ sozinho quando contém slots estruturais. Migração: remova o `inset="none"` e o padding que a tela
252
+ aplicava no header; remova também o `border-b-0`/`border-t-0` de quem desligava o divisor para
253
+ redesenhá-lo à mão.
254
+
255
+ **O rodapé do `Drawer` recebeu a faixa sutil do `Dialog`.** A ausência era divergência não declarada
256
+ entre duas superfícies com a mesma anatomia; agora as duas usam o mesmo fundo. O título do Drawer
257
+ também perdeu o `text-foreground` que só ele tinha.
258
+
259
+ **Títulos de modal passam a truncar.** `DialogTitle` e `DrawerTitle` mantêm o nome da superfície em
260
+ uma linha, com reticências quando não cabe, em vez de empurrar o corpo para baixo. O
261
+ `mode="alert"` é a exceção: ali o título é centralizado e pode ocupar duas linhas, porque a
262
+ resposta obrigatória não tem corpo para empurrar. As telas que já
263
+ escreviam `truncate` podem remover a classe. Quando o nome precisar aparecer inteiro, ele pertence
264
+ ao começo do corpo, não ao cabeçalho (ADR 0018).
265
+
266
+ **Mídia com visual próprio.** Os slots de mídia aceitam `asChild`: uma marca que já se desenha —
267
+ um orbe, um logotipo — ocupa o slot sem ganhar a moldura por cima, e continua herdando a escala.
268
+
269
+ **O rodapé do `Drawer` mantém o `mt-auto`** que o `Dialog` não tem. A diferença passa a ser
270
+ declarada: o conteúdo lateral raramente preenche a altura da janela, e sem isso o rodapé subiria
271
+ atrás do corpo.
272
+
273
+ **Divisor não se desliga por classe.** `border-b-0` no consumidor não apaga a linha derivada — ela
274
+ vem de uma variante condicional, que vence uma classe simples. Uma superfície sem divisores é uma
275
+ superfície `continuous`: troque o layout no root em vez de apagar a borda região a região.
276
+
277
+ ### Migração rápida
278
+
279
+ 1. Rode a aplicação e siga os erros dos headers: cada um diz o slot que falta.
280
+ 2. Remova `p-*`, `border-b`, `border-t`, `flex-row`, `justify-between` e `text-*` que existiam só
281
+ para corrigir o default da família.
282
+ 3. Troque `<Card>` + classes de divisor por `<Card layout="sectioned">`.
283
+ 4. Onde a tela remontava header com `div`, use os slots — eles agora posicionam sozinhos.
284
+
10
285
  ## 18.5.0 — 2026-09-22
11
286
 
12
287
  `Content` passa a concentrar estrutura e apresentação de blocos com cabeçalho e corpo: aceita
package/bin/cli.mjs CHANGED
@@ -415,10 +415,15 @@ Regras das actions:
415
415
  requires-sem-authorize — \`requires\` é declarativo (o runtime não o executa);
416
416
  action com requires precisa de authorize no contrato ou no
417
417
  binding (o join contrato↔bind é cross-file)
418
+ ignored-by-kind — \`emits\` ou \`background\` em action list/view: o runtime
419
+ e o manifest só os aplicam em simple e form
418
420
 
419
421
  Regras da UI (@softize/opus/ui):
420
422
  ui-structure — anatomia pública das superfícies compostas (ADR 0005):
421
423
  filho permitido dentro do pai correto
424
+ ui-structure-split — filho direto de Split que não é Pane (elemento HTML, texto,
425
+ outro componente Opus ou componente do próprio arquivo); o
426
+ Split lança erro ao renderizá-lo
422
427
  ui-semantic-api — API semântica: \`context\` + \`variant\`; reprova aliases de
423
428
  migração e \`tone\` em novos dicionários
424
429
  removed-ui-token — token público removido (ring-edge, shadow-card…) com a
package/bin/lib/check.mjs CHANGED
@@ -12,6 +12,9 @@
12
12
  * no contrato ou no binding). O join contrato↔bind é por
13
13
  * identificador exportado, cross-file; referência ambígua
14
14
  * ou não encontrada não acusa (sem falso-positivo).
15
+ * ignored-by-kind — `emits`/`background` em action list/view: o runtime e o manifest
16
+ * só os aplicam em simple/form. No bindAction, o kind vem do
17
+ * contrato pelo mesmo join por identificador exportado.
15
18
  *
16
19
  * A checagem cross-file de registro no runtime (graph) fica pra v2 (introspecção/MCP).
17
20
  * Saída: findings + exit ≠ 0 se houver erro. Zero actions num diretório NÃO-marcado
@@ -69,6 +72,19 @@ const FIELD_GROUP = {
69
72
  const SEG = "[a-z][a-z0-9]*(-[a-z0-9]+)*";
70
73
  const ACTION_NAME_RE = new RegExp(`^${SEG}(\\.${SEG})+$`);
71
74
  const KINDS = new Set(["simple", "form", "list", "view"]);
75
+ // O runtime e o manifest só aplicam estes campos em `simple` e `form`; o tipo do binding os aceita
76
+ // em qualquer kind, então a declaração num list/view compila e não tem efeito.
77
+ const EXECUTION_ONLY_FIELDS = ["emits", "background"];
78
+ const KINDS_WITHOUT_EXECUTION_FIELDS = new Set(["list", "view"]);
79
+
80
+ /** Campos de execução declarados num kind que os ignora → mensagem do achado, ou null. */
81
+ function ignoredByKindMessage(kind, keys) {
82
+ if (!KINDS_WITHOUT_EXECUTION_FIELDS.has(kind)) return null;
83
+ const fields = EXECUTION_ONLY_FIELDS.filter((field) => keys.includes(field));
84
+ if (fields.length === 0) return null;
85
+ const list = fields.map((field) => `\`${field}\``).join(" e ");
86
+ return `Action ${kind} declara ${list}, que o runtime e o manifest só aplicam em actions simple e form: a declaração não tem efeito. Remova ${list}; se a operação precisa de fila ou de eventos declarados, use uma action simple ou form.`;
87
+ }
72
88
 
73
89
  // Tokens públicos removidos cuja ausência no CSS não quebra typecheck/build: sem uma
74
90
  // migração executável, o consumidor perde borda/sombra em silêncio. O check fica no Opus
@@ -121,23 +137,31 @@ const UI_STRUCTURAL_PARENTS = new Map([
121
137
  ["CardHeader", new Set(["Card"])],
122
138
  ["CardBody", new Set(["Card"])],
123
139
  ["CardFooter", new Set(["Card"])],
140
+ ["CardMedia", new Set(["CardHeader"])],
124
141
  ["CardTitle", new Set(["CardHeader"])],
125
142
  ["CardDescription", new Set(["CardHeader"])],
126
143
  ["CardAction", new Set(["CardHeader"])],
127
144
  ["DialogHeader", new Set(["DialogContent"])],
128
145
  ["DialogBody", new Set(["DialogContent"])],
129
146
  ["DialogFooter", new Set(["DialogContent"])],
147
+ ["DialogNavigation", new Set(["DialogHeader"])],
130
148
  ["DialogMedia", new Set(["DialogHeader"])],
131
149
  ["DialogTitle", new Set(["DialogHeader"])],
132
- ["DialogDescription", new Set(["DialogHeader"])],
150
+ ["DialogActions", new Set(["DialogHeader"])],
133
151
  ["DrawerHeader", new Set(["DrawerContent"])],
134
152
  ["DrawerBody", new Set(["DrawerContent"])],
135
153
  ["DrawerFooter", new Set(["DrawerContent"])],
154
+ ["DrawerNavigation", new Set(["DrawerHeader"])],
155
+ ["DrawerMedia", new Set(["DrawerHeader"])],
136
156
  ["DrawerTitle", new Set(["DrawerHeader"])],
137
- ["DrawerDescription", new Set(["DrawerHeader"])],
157
+ ["DrawerActions", new Set(["DrawerHeader"])],
138
158
  ["PaneHeader", new Set(["Pane", "Sidebar"])],
139
159
  ["PaneBody", new Set(["Pane", "Sidebar"])],
140
160
  ["PaneFooter", new Set(["Pane", "Sidebar"])],
161
+ ["PaneMedia", new Set(["PaneHeader"])],
162
+ ["PaneTitle", new Set(["PaneHeader"])],
163
+ ["PaneDescription", new Set(["PaneHeader"])],
164
+ ["PaneActions", new Set(["PaneHeader"])],
141
165
  ["AlertMedia", new Set(["Alert"])],
142
166
  ["AlertHeader", new Set(["Alert"])],
143
167
  ["AlertActions", new Set(["Alert"])],
@@ -589,6 +613,9 @@ export function lintAction(a) {
589
613
  // requires sem authorize: no defineAction o par mora no mesmo objeto — acusa direto.
590
614
  // No defineContract a regra é cross-file (o bind pode trazer o authorize row-level);
591
615
  // fica pro join do checkProject.
616
+ const ignored = ignoredByKindMessage(a.kind, a.keys);
617
+ if (ignored !== null) at("ignored-by-kind", ignored);
618
+
592
619
  if (
593
620
  a.form === "defineAction" &&
594
621
  a.keys.includes("requires") &&
@@ -738,6 +765,17 @@ export function checkProject(sources) {
738
765
  "`requires` do contrato é declarativo (o runtime não o executa) — sem `authorize` no contrato ou no binding, a action fica ABERTA.",
739
766
  });
740
767
  }
768
+ const ignored = ignoredByKindMessage(contract.kind, item.keys);
769
+ if (ignored !== null) {
770
+ findings.push({
771
+ rule: "ignored-by-kind",
772
+ level: "error",
773
+ action: contract.name ?? item.contractRef,
774
+ line: item.line,
775
+ file,
776
+ message: ignored,
777
+ });
778
+ }
741
779
  }
742
780
 
743
781
  return {
@@ -1040,6 +1078,140 @@ function directStructuralChildren(node, imports) {
1040
1078
  return { names, hasOpaque };
1041
1079
  }
1042
1080
 
1081
+ /**
1082
+ * Componentes declarados no próprio arquivo (função, classe ou arrow/function atribuída).
1083
+ * Um deles nunca é o `Pane` do Opus, então o `Split` recusaria o elemento mesmo que o
1084
+ * componente devolva um `Pane`.
1085
+ */
1086
+ function localComponentNames(sourceFile) {
1087
+ const names = new Set();
1088
+ const visit = (node) => {
1089
+ if (
1090
+ (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) &&
1091
+ node.name !== undefined
1092
+ ) {
1093
+ names.add(node.name.text);
1094
+ } else if (
1095
+ ts.isVariableDeclaration(node) &&
1096
+ ts.isIdentifier(node.name) &&
1097
+ node.initializer !== undefined &&
1098
+ (ts.isArrowFunction(node.initializer) ||
1099
+ ts.isFunctionExpression(node.initializer))
1100
+ ) {
1101
+ names.add(node.name.text);
1102
+ }
1103
+ ts.forEachChild(node, visit);
1104
+ };
1105
+ visit(sourceFile);
1106
+ return names;
1107
+ }
1108
+
1109
+ const jsxSpaceEntities = {
1110
+ nbsp: "\u00a0",
1111
+ ensp: "\u2002",
1112
+ emsp: "\u2003",
1113
+ thinsp: "\u2009",
1114
+ };
1115
+
1116
+ /**
1117
+ * Decodifica, no texto cru de um `JsxText`, as entidades numéricas e as nomeadas de espaço.
1118
+ * As demais entidades nomeadas ficam como estão: nenhuma delas vira espaço em branco.
1119
+ */
1120
+ function decodeJsxSpaceEntities(raw) {
1121
+ return raw.replace(/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|([a-zA-Z]+));/g, (entity, dec, hex, name) => {
1122
+ if (name !== undefined) return jsxSpaceEntities[name] ?? entity;
1123
+ const code = dec !== undefined ? Number(dec) : Number.parseInt(hex, 16);
1124
+ return code <= 0x10ffff ? String.fromCodePoint(code) : entity;
1125
+ });
1126
+ }
1127
+
1128
+ /**
1129
+ * Filhos diretos de um `<Split>` que o componente recusaria em runtime. Aceita `<Pane>`,
1130
+ * fragments com panes, espaço em branco (inclusive entidades como `&nbsp;`), comentários,
1131
+ * `null`/`undefined`/booleanos e ramos de condicional. Só aponta o que o código literal prova: elemento HTML, texto, outro componente
1132
+ * do Opus ou componente declarado no próprio arquivo. Expressões dinâmicas (`{children}`,
1133
+ * `{items.map(...)}`) e componentes de outros módulos ficam de fora, porque o valor só existe
1134
+ * em runtime.
1135
+ */
1136
+ function invalidSplitChildren(split, imports, localComponents) {
1137
+ const invalid = [];
1138
+ const text = (value, node, position) => {
1139
+ const trimmed = value.trim();
1140
+ if (trimmed === "") return;
1141
+ invalid.push({
1142
+ label: `o texto "${trimmed.length > 40 ? `${trimmed.slice(0, 40)}…` : trimmed}"`,
1143
+ position: position ?? node.getStart(),
1144
+ });
1145
+ };
1146
+ const element = (node) => {
1147
+ const tag = jsxOpening(node).tagName;
1148
+ if (!ts.isIdentifier(tag)) return;
1149
+ const name = tag.text;
1150
+ const imported = imports.get(name);
1151
+ if (imported === "Pane") return;
1152
+ const intrinsic = /^[a-z]/.test(name);
1153
+ if (imported !== undefined || intrinsic || localComponents.has(name)) {
1154
+ invalid.push({ label: `<${name}>`, position: node.getStart() });
1155
+ }
1156
+ };
1157
+ const expression = (node) => {
1158
+ if (ts.isParenthesizedExpression(node)) return expression(node.expression);
1159
+ if (ts.isConditionalExpression(node)) {
1160
+ expression(node.whenTrue);
1161
+ expression(node.whenFalse);
1162
+ return;
1163
+ }
1164
+ if (
1165
+ ts.isBinaryExpression(node) &&
1166
+ [
1167
+ ts.SyntaxKind.AmpersandAmpersandToken,
1168
+ ts.SyntaxKind.BarBarToken,
1169
+ ts.SyntaxKind.QuestionQuestionToken,
1170
+ ].includes(node.operatorToken.kind)
1171
+ ) {
1172
+ // O lado esquerdo é a condição, valor dinâmico; o direito é o que pode ser renderizado.
1173
+ return expression(node.right);
1174
+ }
1175
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
1176
+ return text(node.text, node);
1177
+ }
1178
+ if (ts.isNumericLiteral(node)) {
1179
+ invalid.push({ label: `o número ${node.text}`, position: node.getStart() });
1180
+ return;
1181
+ }
1182
+ if (
1183
+ ts.isJsxElement(node) ||
1184
+ ts.isJsxSelfClosingElement(node) ||
1185
+ ts.isJsxFragment(node)
1186
+ ) {
1187
+ child(node);
1188
+ }
1189
+ };
1190
+ const child = (node) => {
1191
+ if (ts.isJsxText(node)) {
1192
+ // O React entrega as entidades já decodificadas, e o `trim()` do Split descarta o texto
1193
+ // que sobra só com espaço, como `&nbsp;` entre dois panes.
1194
+ if (decodeJsxSpaceEntities(node.text).trim() === "") return;
1195
+ // O JsxText começa logo após o nó anterior (`pos`); a linha útil é a do primeiro
1196
+ // caractere visível.
1197
+ const offset = node.text.search(/\S/);
1198
+ if (offset >= 0) text(node.text, node, node.pos + offset);
1199
+ return;
1200
+ }
1201
+ if (ts.isJsxExpression(node)) {
1202
+ if (node.expression !== undefined) expression(node.expression);
1203
+ return;
1204
+ }
1205
+ if (ts.isJsxFragment(node)) {
1206
+ for (const nested of node.children) child(nested);
1207
+ return;
1208
+ }
1209
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) element(node);
1210
+ };
1211
+ for (const node of split.children) child(node);
1212
+ return invalid;
1213
+ }
1214
+
1043
1215
  function directImportedJsxChildren(node, imports) {
1044
1216
  if (!ts.isJsxElement(node)) return [];
1045
1217
  return node.children.flatMap((child) => {
@@ -1144,10 +1316,25 @@ export function checkUiStructure(file, text) {
1144
1316
  message,
1145
1317
  });
1146
1318
 
1319
+ const localComponents = localComponentNames(sourceFile);
1320
+
1147
1321
  const visit = (node) => {
1148
1322
  if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
1149
1323
  const local = jsxLocalName(node);
1150
1324
  const component = local === null ? null : imports.get(local);
1325
+ if (component === "Split" && ts.isJsxElement(node)) {
1326
+ for (const child of invalidSplitChildren(node, imports, localComponents)) {
1327
+ findings.push({
1328
+ rule: "ui-structure-split",
1329
+ level: "error",
1330
+ action: "Split",
1331
+ line:
1332
+ sourceFile.getLineAndCharacterOfPosition(child.position).line + 1,
1333
+ file,
1334
+ message: `Split distribui somente <Pane> como filho, e ${child.label} ficaria fora do layout: o Split interrompe a renderização com erro. Envolva o conteúdo em um <Pane>; se ele vem de um componente que devolve um <Pane>, escreva o <Pane> dentro do Split e passe o componente como filho dele.`,
1335
+ });
1336
+ }
1337
+ }
1151
1338
  const removedComponent =
1152
1339
  component === null ? undefined : UI_REMOVED_COMPONENTS.get(component);
1153
1340
  if (component !== null && removedComponent !== undefined) {
@@ -1639,6 +1826,9 @@ export function checkUiMigrations(file, text) {
1639
1826
  return findings.sort((left, right) => left.line - right.line);
1640
1827
  }
1641
1828
 
1829
+ // Dependências, builds, testes e fixtures ficam fora de todo walk. Uma action declarada num
1830
+ // teste ou fixture nunca é registrada no runtime: cobrar `export const`, ordem de campos ou
1831
+ // padrões de UI ali só gera falso positivo e esconde o que o app realmente expõe.
1642
1832
  const SKIP_DIRS = new Set([
1643
1833
  "node_modules",
1644
1834
  "dist",
@@ -1646,15 +1836,13 @@ const SKIP_DIRS = new Set([
1646
1836
  ".git",
1647
1837
  ".next",
1648
1838
  "coverage",
1649
- ]);
1650
- const UI_SKIP_DIRS = new Set([
1651
- ...SKIP_DIRS,
1652
1839
  "__tests__",
1653
1840
  "__fixtures__",
1654
1841
  "fixtures",
1655
1842
  "test",
1656
1843
  "tests",
1657
1844
  ]);
1845
+ const TEST_FILE = /\.(?:test|spec)\.[^.]+$/;
1658
1846
  const UI_EXTENSIONS = new Set([
1659
1847
  ".ts",
1660
1848
  ".tsx",
@@ -1690,7 +1878,7 @@ export async function walkTsFiles(dir, acc = []) {
1690
1878
  } else if (
1691
1879
  (e.name.endsWith(".ts") || e.name.endsWith(".tsx")) &&
1692
1880
  !e.name.endsWith(".d.ts") &&
1693
- !e.name.endsWith(".test.ts")
1881
+ !TEST_FILE.test(e.name)
1694
1882
  ) {
1695
1883
  safeProjectPath(root, relative, { mustExist: true });
1696
1884
  acc.push(full);
@@ -1707,7 +1895,7 @@ async function walkUiFiles(dir, acc = []) {
1707
1895
  const entries = readProjectDirectory(root, local).entries;
1708
1896
  for (const e of entries) {
1709
1897
  if (e.name.startsWith(".")) continue;
1710
- if (UI_SKIP_DIRS.has(e.name)) continue;
1898
+ if (SKIP_DIRS.has(e.name)) continue;
1711
1899
  const relative = path.join(local, e.name);
1712
1900
  const full = path.join(root, relative);
1713
1901
  if (e.isSymbolicLink()) {
@@ -1719,7 +1907,7 @@ async function walkUiFiles(dir, acc = []) {
1719
1907
  walk(relative);
1720
1908
  } else if (
1721
1909
  UI_EXTENSIONS.has(path.extname(e.name)) &&
1722
- !/\.(?:test|spec)\.[^.]+$/.test(e.name)
1910
+ !TEST_FILE.test(e.name)
1723
1911
  ) {
1724
1912
  safeProjectPath(root, relative, { mustExist: true });
1725
1913
  acc.push(full);
@@ -1748,8 +1936,8 @@ export async function scanDir(rootDir) {
1748
1936
  }
1749
1937
  const checked = checkProject(sources);
1750
1938
  const uiFindings = [];
1751
- // UI pode morar em src/, app/, pages/ ou na raiz. O walk cobre o projeto inteiro e
1752
- // exclui dependências, builds, testes e fixtures para não interpretar exemplos como uso.
1939
+ // UI pode morar em src/, app/, pages/ ou na raiz. O walk cobre o projeto inteiro e, como o
1940
+ // das actions, exclui dependências, builds, testes e fixtures (SKIP_DIRS, TEST_FILE).
1753
1941
  for (const file of await walkUiFiles(rootDir)) {
1754
1942
  const text = readProjectFile(rootDir, path.relative(rootDir, file)).content;
1755
1943
  uiFindings.push(...checkUiMigrations(path.relative(rootDir, file), text));