@softize/opus 19.0.0 → 20.1.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 (148) hide show
  1. package/CHANGELOG.md +239 -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/0020-icon-tiles-are-explicitly-composed.md +8 -0
  7. package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
  9. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  10. package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
  11. package/docs/data-products.md +15 -9
  12. package/docs/protocol.md +3 -4
  13. package/docs/releasing.md +2 -2
  14. package/package.json +2 -2
  15. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  16. package/registry/templates/app/package.json +1 -1
  17. package/src/core/runtime.ts +96 -8
  18. package/src/core/types.ts +5 -2
  19. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  20. package/src/ui/components/patterns/content-header.tsx +109 -18
  21. package/src/ui/components/patterns/fill-region.ts +150 -0
  22. package/src/ui/components/patterns/list.tsx +91 -92
  23. package/src/ui/components/patterns/page.tsx +20 -4
  24. package/src/ui/components/patterns/presentation.tsx +52 -37
  25. package/src/ui/components/patterns/split.tsx +44 -4
  26. package/src/ui/components/patterns/surface-anatomy.ts +35 -8
  27. package/src/ui/components/patterns/surface-header.tsx +58 -25
  28. package/src/ui/components/patterns/trigger.tsx +12 -2
  29. package/src/ui/components/primitives/alert.tsx +9 -1
  30. package/src/ui/components/primitives/avatar.tsx +7 -3
  31. package/src/ui/components/primitives/card.tsx +1 -1
  32. package/src/ui/components/primitives/carousel.tsx +34 -5
  33. package/src/ui/components/primitives/checkbox.tsx +11 -3
  34. package/src/ui/components/primitives/command.tsx +2 -1
  35. package/src/ui/components/primitives/copyable.tsx +120 -10
  36. package/src/ui/components/primitives/dialog.tsx +42 -11
  37. package/src/ui/components/primitives/drawer.tsx +10 -2
  38. package/src/ui/components/primitives/empty.tsx +1 -1
  39. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  40. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  41. package/src/ui/components/primitives/input-group.tsx +9 -2
  42. package/src/ui/components/primitives/input-otp.tsx +1 -1
  43. package/src/ui/components/primitives/kbd.tsx +1 -1
  44. package/src/ui/components/primitives/label.tsx +3 -0
  45. package/src/ui/components/primitives/menu.tsx +5 -0
  46. package/src/ui/components/primitives/popover.tsx +1 -1
  47. package/src/ui/components/primitives/progress.tsx +9 -1
  48. package/src/ui/components/primitives/slider.tsx +62 -11
  49. package/src/ui/components/primitives/truncate.tsx +10 -1
  50. package/src/ui/docs/content/accordion.md +74 -27
  51. package/src/ui/docs/content/action-form-card.md +24 -15
  52. package/src/ui/docs/content/action-form-dialog.md +17 -7
  53. package/src/ui/docs/content/action-form.md +92 -77
  54. package/src/ui/docs/content/action-list-dialog.md +75 -38
  55. package/src/ui/docs/content/action-list.md +141 -58
  56. package/src/ui/docs/content/action-trigger.md +27 -10
  57. package/src/ui/docs/content/action-view.md +14 -14
  58. package/src/ui/docs/content/actions.md +161 -66
  59. package/src/ui/docs/content/ai.md +43 -44
  60. package/src/ui/docs/content/alert.md +83 -29
  61. package/src/ui/docs/content/ask.md +51 -26
  62. package/src/ui/docs/content/aspect-ratio.md +22 -24
  63. package/src/ui/docs/content/audit.md +23 -19
  64. package/src/ui/docs/content/auth.md +38 -19
  65. package/src/ui/docs/content/avatar.md +54 -5
  66. package/src/ui/docs/content/badge.md +29 -9
  67. package/src/ui/docs/content/breadcrumb.md +21 -8
  68. package/src/ui/docs/content/button.md +73 -43
  69. package/src/ui/docs/content/calendar.md +134 -26
  70. package/src/ui/docs/content/card.md +41 -15
  71. package/src/ui/docs/content/carousel.md +37 -6
  72. package/src/ui/docs/content/chat.md +118 -37
  73. package/src/ui/docs/content/checkbox.md +73 -7
  74. package/src/ui/docs/content/cli.md +94 -55
  75. package/src/ui/docs/content/collapsible.md +43 -19
  76. package/src/ui/docs/content/command.md +30 -8
  77. package/src/ui/docs/content/composer.md +23 -8
  78. package/src/ui/docs/content/content.md +119 -27
  79. package/src/ui/docs/content/copyable.md +39 -8
  80. package/src/ui/docs/content/customization.md +55 -46
  81. package/src/ui/docs/content/cycle.md +33 -18
  82. package/src/ui/docs/content/data-products.md +115 -0
  83. package/src/ui/docs/content/data-state.md +20 -6
  84. package/src/ui/docs/content/data.md +20 -19
  85. package/src/ui/docs/content/detail.md +12 -5
  86. package/src/ui/docs/content/dialog.md +107 -24
  87. package/src/ui/docs/content/dictionary-value.md +4 -4
  88. package/src/ui/docs/content/dock.md +54 -19
  89. package/src/ui/docs/content/dot.md +35 -5
  90. package/src/ui/docs/content/drawer.md +63 -19
  91. package/src/ui/docs/content/empty-value.md +20 -6
  92. package/src/ui/docs/content/empty.md +24 -3
  93. package/src/ui/docs/content/events.md +76 -16
  94. package/src/ui/docs/content/executions.md +9 -9
  95. package/src/ui/docs/content/field.md +21 -6
  96. package/src/ui/docs/content/getting-started.md +44 -34
  97. package/src/ui/docs/content/icon-picker.md +19 -13
  98. package/src/ui/docs/content/icon-tile.md +38 -15
  99. package/src/ui/docs/content/input-otp.md +58 -7
  100. package/src/ui/docs/content/input.md +22 -3
  101. package/src/ui/docs/content/item.md +53 -6
  102. package/src/ui/docs/content/kbd.md +5 -1
  103. package/src/ui/docs/content/label.md +13 -4
  104. package/src/ui/docs/content/log.md +5 -5
  105. package/src/ui/docs/content/markdown.md +5 -4
  106. package/src/ui/docs/content/mcp.md +49 -23
  107. package/src/ui/docs/content/menu.md +95 -10
  108. package/src/ui/docs/content/metric-card.md +12 -4
  109. package/src/ui/docs/content/observability.md +10 -9
  110. package/src/ui/docs/content/page.md +118 -12
  111. package/src/ui/docs/content/pagination.md +13 -5
  112. package/src/ui/docs/content/pane.md +55 -5
  113. package/src/ui/docs/content/popover.md +42 -5
  114. package/src/ui/docs/content/presentation.md +91 -47
  115. package/src/ui/docs/content/progress.md +27 -14
  116. package/src/ui/docs/content/queue.md +39 -21
  117. package/src/ui/docs/content/radio-group.md +26 -4
  118. package/src/ui/docs/content/router.md +29 -10
  119. package/src/ui/docs/content/runtime.md +90 -54
  120. package/src/ui/docs/content/scheduler.md +53 -21
  121. package/src/ui/docs/content/scroll-area.md +42 -35
  122. package/src/ui/docs/content/select.md +78 -33
  123. package/src/ui/docs/content/semantic-context.md +21 -2
  124. package/src/ui/docs/content/separator.md +15 -9
  125. package/src/ui/docs/content/sidebar.md +3 -2
  126. package/src/ui/docs/content/skeleton.md +6 -1
  127. package/src/ui/docs/content/slider.md +37 -9
  128. package/src/ui/docs/content/spinner.md +4 -3
  129. package/src/ui/docs/content/split.md +19 -0
  130. package/src/ui/docs/content/storage.md +8 -9
  131. package/src/ui/docs/content/switch.md +10 -7
  132. package/src/ui/docs/content/table.md +46 -8
  133. package/src/ui/docs/content/tabs.md +14 -4
  134. package/src/ui/docs/content/testing.md +76 -31
  135. package/src/ui/docs/content/textarea.md +5 -4
  136. package/src/ui/docs/content/toast.md +48 -7
  137. package/src/ui/docs/content/toggle.md +3 -3
  138. package/src/ui/docs/content/tokens.md +15 -26
  139. package/src/ui/docs/content/tooltip.md +18 -2
  140. package/src/ui/docs/content/truncate.md +36 -6
  141. package/src/ui/docs/content/ui.md +74 -22
  142. package/src/ui/docs/content/upgrading.md +45 -36
  143. package/src/ui/docs/plugin.ts +36 -2
  144. package/src/ui/docs/registry.tsx +6 -0
  145. package/src/ui/drivers/react.tsx +16 -8
  146. package/src/ui/meta.ts +32 -32
  147. package/src/ui/react.tsx +1 -0
  148. package/src/vite/design.ts +16 -4
package/CHANGELOG.md CHANGED
@@ -7,6 +7,245 @@ 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.1.0 — 2026-09-26
11
+
12
+ `Content` ganha um slot de mídia e passa a aceitar a moldura integrada ao header também em regiões
13
+ de leitura. Quem não usa as novas props mantém a mesma aparência: `frameVariant="default"` continua
14
+ sendo o padrão, com a borda contínua em `section` e em `fieldset`. Alguns pontos podem pedir ajuste
15
+ sem interromper a aplicação:
16
+
17
+ - a `legend` de todo `Content as="fieldset"` passa a envolver o título nos wrappers do header
18
+ (`legend > span[data-slot=content-heading] > …`): um seletor como
19
+ `legend > [data-slot=content-title]` deixa de casar; use `legend [data-slot=content-title]`;
20
+ - um `ContentHeader` de fieldset com slot inválido lança outra mensagem: “ContentHeader de fieldset
21
+ aceita somente ContentMedia e ContentTitle; contador, descrição e ações ficam fora da legenda.”
22
+ Atualize testes que conferem o texto;
23
+ - com `frameVariant="legend"` dentro de uma superfície com fundo próprio, como um `Card` com `bg-*`
24
+ ou um `Popover`, declare `--surface-background` com esse fundo na superfície (veja **Cor da
25
+ superfície**);
26
+ - em `advancedFilters.sectionIcons`, um glifo solto ganha a moldura padrão, e testes que buscavam
27
+ `[data-slot="content-title-icon"]` devem buscar `[data-slot="content-media"]`.
28
+
29
+ Em detalhe:
30
+
31
+ - **Mídia no header.** `ContentHeader` aceita `ContentMedia`, e a forma curta de `Content` ganha
32
+ `media`. O slot já é a moldura de `IconTile`, com a mesma escala e posição em `section` e em
33
+ `fieldset`: passe só o glifo e escolha `size`, `context` e `variant` no slot. Se você colocava um
34
+ `IconTile` dentro de `title`, mova-o para `media`; o slot o adota e herda o que ele declara, sem
35
+ desenhar moldura dentro de moldura. `media` com `null` ou `false` não desenha nada. Em `fieldset`,
36
+ a mídia é decorativa e não entra no nome do grupo.
37
+ - **Moldura integrada em `section`.** `frameVariant="legend"` deixa de exigir `as="fieldset"`. Em
38
+ uma `section`, o título continua sendo um heading no nível declarado, e contador, descrição e
39
+ ações continuam disponíveis; nenhuma `legend` é emitida. Use `as="fieldset"` só para grupos de
40
+ campos: a `legend` aceita a mídia e o título, e contador, descrição e ações continuam fora dela
41
+ (ADR 0025).
42
+ - **Cor da superfície.** Para interromper a borda, o header pinta o título e as ações com a
43
+ variável `--surface-background`. `Card`, `Dialog` e `Drawer` a declaram com o fundo que pintam,
44
+ então a linha não reaparece atrás do título quando há um `CardBody`, um `DrawerBody` ou outro
45
+ wrapper transparente no caminho. Fora delas, vale `--background`.
46
+ - **Filtros avançados.** Os ícones de `advancedFilters.sectionIcons` passam a ocupar o
47
+ `ContentMedia` de cada seção: um glifo solto ganha a moldura padrão, em vez do ícone esmaecido sem
48
+ moldura, e um `IconTile` é adotado pelo slot com o que declara. Um ícone `null` não desenha
49
+ moldura.
50
+
51
+ A página de `Content` mostra a mídia, os dois acabamentos da moldura e a declaração de
52
+ `--surface-background`. A ADR 0025 registra que o acabamento da moldura não altera a semântica; a
53
+ ADR 0023 ganha a emenda do slot de mídia de `Content`, e as ADRs 0020 e 0021, emendas que apontam
54
+ para a 0025.
55
+
56
+ ## 20.0.0 — 2026-09-26
57
+
58
+ Esta versão corrige componentes que falhavam em acessibilidade ou em layout, leva o `fill` da
59
+ página até a lista sozinho, torna explícitas falhas que antes passavam em silêncio, reescreve boa
60
+ parte da documentação e atualiza a Base para a 2.4.0. É major porque algumas dessas mudanças podem
61
+ interromper a aplicação, o boot ou os gates de um projeto que funcionava na 19.0.0: a seção
62
+ **Breaking** diz o que fazer em cada uma. Outros pontos podem pedir ajuste sem interromper a
63
+ aplicação; alguns só afetam testes:
64
+
65
+ - `Slider` envolvido em `role="group"` com `aria-labelledby`, como a documentação anterior
66
+ recomendava: passe o `aria-labelledby` ao próprio `Slider`;
67
+ - `Truncate` dentro de link ou botão: passe `tabIndex={-1}` para evitar uma segunda parada;
68
+ - conteúdo próprio colocado direto no `Avatar`: arredonde-o, porque o root deixou de recortar;
69
+ - testes que buscam um `Alert` informativo ou de sucesso por `getByRole('alert')`: use `status`;
70
+ - testes que buscam um `Copyable` com rótulo pelo nome “Copiar”: use o texto visível;
71
+ - `closeDisabled` em Dialog e Drawer passa a bloquear também Esc e clique fora; handlers criados só
72
+ para isso podem sair;
73
+ - testes que buscam o botão de confirmar do `ActionTrigger` pelo nome longo do gatilho: ele agora
74
+ usa o rótulo da action;
75
+ - testes que conferem o texto do erro de um hook de action fora do `OpusProvider`: a mensagem mudou;
76
+ - `ActionList` sem `layout`, filha direta de `PageBody` ou `ContentBody` em uma `Page` com
77
+ `layout="fill"`: passa a ocupar a altura; declare `layout="flow"` para manter o comportamento
78
+ anterior. Os demais filhos de cada região que repassa a altura passam a ocupar a largura dela; dê
79
+ `self-start` a um botão que deva manter a largura do conteúdo.
80
+
81
+ ### Breaking
82
+
83
+ **`Split` recusa filho que não seja `Pane`.** Antes, esse filho — e um `Pane` escrito dentro de um
84
+ fragment — sumia da tela sem aviso. Agora o fragment é expandido e qualquer outro filho interrompe a
85
+ renderização com um erro que aponta o nó. Migração: envolva o conteúdo em `<Pane>`; se o componente
86
+ que você passa ao `Split` devolve um `Pane`, escreva o `<Pane>` diretamente no `Split` e ponha o
87
+ componente dentro dele. Espaço em branco, `null` e `false` continuam ignorados, mas um `0` vindo de
88
+ `{items.length && <Pane />}` interrompe: use `items.length > 0 &&`.
89
+
90
+ **`runtime.start()` valida os schedules.** Um schedule ativo que aponta para uma action não
91
+ registrada faz o boot falhar com `runtime.schedule_action_not_found`; antes, cada disparo falhava em
92
+ silêncio. Migração: corrija o nome da action ou declare `enabled: false` no schedule. Rode o
93
+ `start()` num teste com o scheduler para descobrir antes do deploy.
94
+
95
+ **`opus check` ganha duas regras que reprovam o gate.** `ignored-by-kind` aponta `emits` ou
96
+ `background` em action `list` ou `view`, que o runtime sempre ignorou; `ui-structure-split` aponta
97
+ filho de `Split` que não seja `Pane`. Migração: remova os campos ignorados e ajuste o `Split` como
98
+ acima.
99
+
100
+ **`opus check` cobra a anatomia dos headers da 19.0.0.** A regra `ui-structure` passa a reprovar
101
+ `CardMedia`, `DialogNavigation`, `DialogActions`, `DrawerNavigation`, `DrawerMedia`, `DrawerActions`,
102
+ `PaneMedia`, `PaneTitle`, `PaneDescription` e `PaneActions` fora do header da própria família —
103
+ antes, esses slots renderizavam em qualquer lugar sem aviso. Migração: mova-os para dentro de
104
+ `CardHeader`, `DialogHeader`, `DrawerHeader` ou `PaneHeader`.
105
+
106
+ **`opus copy --check` inventaria `PaneTitle` e `PaneDescription`.** Um título ou descrição de pane
107
+ com conteúdo dinâmico, como `<PaneTitle>{name}</PaneTitle>`, passa a gerar diagnóstico e reprovar o
108
+ gate. Migração: rode `opus copy`; para texto dinâmico, use uma constante ou uma `I18nRef` com
109
+ `default`, ou declare a superfície em `copy.dynamic` com seletor, tipo, motivo e referência.
110
+
111
+ **Três tipos seguem o elemento renderizado.** `PopoverTitle` e `EmptyDescription` são tipados como
112
+ `div` e `KbdGroup` como `kbd`. O DOM não muda e raramente há algo a ajustar: só um código que
113
+ dependia do tipo `HTMLHeadingElement` ou `HTMLParagraphElement` nesses componentes.
114
+
115
+ ### Avatar
116
+
117
+ O `AvatarBadge` aparece inteiro em todos os tamanhos. O selo fica no canto da caixa do `Avatar`,
118
+ por fora do círculo, e o root recortava o próprio conteúdo com `overflow-hidden`: só um quarto do
119
+ selo ficava visível. Agora `AvatarImage` e `AvatarFallback` herdam o raio do root e fazem o próprio
120
+ recorte. A forma continua declarada no `Avatar` — circular por padrão, ou `className="rounded-lg"`
121
+ para um logo — e o anel de fundo do `AvatarGroup` a acompanha.
122
+
123
+ Se o seu projeto contornava o corte com `overflow-visible` no `Avatar` ou `rounded-full` no
124
+ `AvatarImage`, pode remover essas classes. Conteúdo próprio colocado direto no `Avatar`, fora de
125
+ `AvatarImage` e `AvatarFallback`, deixa de ser recortado: arredonde-o no próprio elemento.
126
+
127
+ ### Acessibilidade
128
+
129
+ - **`Alert`** anuncia conforme o contexto: `role="alert"` em `danger` e `warning`, que interrompem o
130
+ leitor de tela, e `role="status"` nos demais, que esperam a leitura em curso terminar. Um `role`
131
+ passado ao componente continua vencendo.
132
+ - **`Progress`** passa `value` ao progressbar, que expõe `aria-valuenow` e `data-state`
133
+ (`loading`, `complete` ou `indeterminate`), e respeita `max`: `value={3} max={4}` preenche três
134
+ quartos da barra.
135
+ - **`Slider`** nomeia os controles. `aria-label` e `aria-labelledby` passados ao `Slider` nomeiam o
136
+ thumb, que é o elemento focável; em intervalo, a nova prop `thumbLabels` dá um nome a cada
137
+ controle. Sem `thumbLabels`, o nome comum se compõe com a posição (“Preço, mínimo”); sem nome
138
+ algum, o intervalo anuncia “Mínimo” e “Máximo”. Se você envolvia o Slider
139
+ em um `role="group"` com `aria-labelledby`, passe o `aria-labelledby` ao próprio `Slider`.
140
+ - **`Copyable`** com rótulo visível passa a se chamar pelo próprio texto, e quem usa comando de voz
141
+ consegue acioná-lo dizendo o que vê. Sem filhos, continua “Copiar” e “Copiado”. A confirmação é
142
+ anunciada por leitores de tela, e o botão só confirma quando o navegador aceita a cópia: com a
143
+ permissão negada, continua em “Copiar” e nenhum erro fica sem tratamento. Se você passava
144
+ `aria-label` só para repetir o texto visível, pode removê-lo.
145
+ - **`Truncate`** com texto cortado entra na ordem de tabulação, e a dica passa a abrir pelo
146
+ teclado. Dentro de um link ou botão, passe `tabIndex={-1}` para evitar uma segunda parada no
147
+ mesmo item.
148
+ - **`Carousel`** vertical responde às setas para cima e para baixo. Setas usadas em campos ou em
149
+ controles que navegam pelas setas dentro dos slides, como Slider, RadioGroup e Tabs, não trocam
150
+ mais de slide.
151
+
152
+ ### Formulário
153
+
154
+ - **`Checkbox`** indeterminado tem aparência própria: o preenchimento de marcado com um traço.
155
+ - **`IconPicker`** mede 2.25rem, como `Input` e `Select`, e mostra a moldura de erro com
156
+ `aria-invalid`.
157
+ - **`InputOTP`** usa nas casas o mesmo fundo dos campos no tema claro.
158
+ - **`InputGroup`** esmaece os addons sozinho quando o controle está desabilitado, e o clique no
159
+ addon também foca o `InputGroupTextarea`.
160
+ - **`Label`** esmaece dentro de `<Field data-disabled="true">`.
161
+ - **`CommandEmpty`** soma a `className` do consumidor ao estilo padrão, em vez de substituí-lo.
162
+
163
+ ### Layout
164
+
165
+ `Page layout="fill"` leva a altura até a `ActionList` sozinha. `PageBody`, `Content`, `ContentBody`
166
+ e o corpo da `Presentation` repassam a altura quando contêm uma lista, e a lista, sem `layout`,
167
+ assume `fill` quando é filha direta de um deles. Seções irmãs sem lista mantêm a altura do conteúdo,
168
+ uma página `fill` sem lista não muda, e `Card`, `Dialog`, `Drawer` e `Pane` não herdam o `fill` da
169
+ página (ADR 0024). A `Presentation` de lista em `layout="fill"` passa a entregar o que prometia: a
170
+ lista ocupa a altura da página e só os resultados rolam.
171
+
172
+ Quem encadeava `flex min-h-0 flex-1 flex-col` à mão pode remover as classes; mantidas, continuam
173
+ funcionando. Com um contêiner próprio entre `ContentBody` e a lista, como um `TabsContent`, a lista
174
+ continua em `flow`: declare `layout="fill"` nela e dê ao contêiner `flex min-h-0 flex-1 flex-col`.
175
+
176
+ ### Superfícies e patterns
177
+
178
+ - **`closeDisabled`** em `Dialog` e `Drawer` bloqueia todo fechamento: além do botão, Esc e clique
179
+ ou foco fora. Em `Dialog mode="alert"`, bloqueia o Esc. Handlers `onEscapeKeyDown`,
180
+ `onPointerDownOutside` e `onInteractOutside` informados continuam rodando antes do bloqueio.
181
+ - **`ActionTrigger`**: sem `confirmLabel` no contrato, o botão de confirmar usa o `label` da action,
182
+ como “Excluir”, e não mais o `label` do gatilho, como “Excluir Unidade Centro”.
183
+ - **`Split`** mostra um `Pane` escrito dentro de um fragment (veja **Breaking**).
184
+ - **Card, Pane, Dialog e Drawer em `layout="continuous"`:** um cabeçalho sem corpo nem rodapé
185
+ fecha a superfície com o mesmo respiro que tem em cima. Nada muda quando há corpo ou rodapé.
186
+ - **`ActionFormCard`** ganha `titleLevel`, que escolhe o nível do heading do título (`h3` por
187
+ padrão) sem mudar a aparência.
188
+ - **Hooks de action** fora do `OpusProvider` lançam um erro que nomeia o hook chamado e explica
189
+ como resolver.
190
+
191
+ ### Runtime
192
+
193
+ - **Schedules:** além da validação no `start()` (veja **Breaking**), uma execução que falha passa a
194
+ ser registrada no log com o schedule, a action e o código do erro.
195
+ - **Avisos em desenvolvimento:** o runtime avisa quando uma reaction chama `ctx.emit`, que continua
196
+ sem publicar (`runtime.reaction_emit_ignored`), e quando uma action `list` ou `view` declara
197
+ `emits` ou `background`, que o runtime ignora (`runtime.action_field_ignored`). Em produção,
198
+ nada muda.
199
+
200
+ ### CLI
201
+
202
+ - **`opus check`** ganha `ignored-by-kind` e `ui-structure-split` (veja **Breaking**).
203
+ - **Skill `create-opus-action`:** o scaffold gera identificador válido para recurso em kebab-case
204
+ (`sales-order create-item` → `salesOrderCreateItem`). Rode `opus setup` para atualizar a cópia.
205
+ - **`opus check` e `opus introspect`** ignoram arquivos `*.test.*` e `*.spec.*` e as pastas `test`,
206
+ `tests`, `__tests__`, `fixtures` e `__fixtures__`: actions declaradas ali não são registradas, e o
207
+ check deixava de passar por causa delas. Uma action real guardada em uma pasta com esses nomes deixa
208
+ de ser verificada.
209
+ - **`opus check`** confere a anatomia real dos headers da 19.0.0 (veja **Breaking**).
210
+ - **`opus copy`** inventaria `PaneTitle` e `PaneDescription` (veja **Breaking**) e
211
+ `Slider.thumbLabels`.
212
+
213
+ ### Depreciações
214
+
215
+ `resolveIconTileStyle` (use `IconTile` ou `iconTileVariants`) e `MenuPortal` (`MenuContent` já usa
216
+ portal) estão depreciados e saem na próxima major.
217
+
218
+ ### Documentação
219
+
220
+ As páginas foram revisadas contra o código desta versão. Exemplos que não compilavam ou que
221
+ descreviam comportamento antigo foram corrigidos, componentes e props sem referência ganharam
222
+ tabela, e as páginas passaram a ligar umas às outras no ponto de decisão. Entre as mudanças:
223
+
224
+ - nova página Produtos de Dados, e seções sobre `defineDomain`, reactions,
225
+ `loads` e os providers necessários para consumir a UI;
226
+ - os exemplos voltam a ter o layout que o texto descreve: as classes usadas só nas páginas `.md`
227
+ passam a ser geradas no site e na rota `/__docs` do `opusDocs()`, sem mudar o CSS do seu app nem
228
+ o de produção;
229
+ - `ActionList` descreve a barra de filtros como funciona desde a 18.3.0;
230
+ - o exemplo de `opusDesign()` em `@softize/opus/vite` mostra o arranjo do template: import dinâmico
231
+ condicionado ao modo design e `vite --mode design --configLoader runner`. Um import estático do
232
+ plugin quebra o carregamento do `vite.config.ts` no Node 22+ com
233
+ `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`; se o seu config faz isso, adote o padrão do
234
+ template.
235
+
236
+ ### Base 2.4.0
237
+
238
+ O Opus passa a depender de `@softize/base ^2.4.0`, e o template do `opus create` também. A regra de
239
+ formulários da Base agora é a que o Opus já pratica: título `Criar {recurso}` ou
240
+ `Editar {recurso}`, ação `Salvar`.
241
+
242
+ ### Correções do histórico
243
+
244
+ - **19.0.0:** a entrada **Breaking** diz que nó solto dentro do header lança erro nos quatro
245
+ headers. Isso vale para `CardHeader`, `DialogHeader` e `DrawerHeader`; o `PaneHeader` aceita
246
+ conteúdo livre, como uma busca ou a marca, depois do título e da descrição. Só a repetição de um
247
+ slot lança erro.
248
+
10
249
  ## 19.0.0 — 2026-09-24
11
250
 
12
251
  Card, Dialog, Drawer e Pane passam a derivar a anatomia da mesma infraestrutura que Page e Content
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));
package/bin/lib/copy.mjs CHANGED
@@ -124,6 +124,8 @@ const JSX_CHILD_ROLES = new Map([
124
124
  // comando. Classificado como `button`, a política universal cobraria verbo de ação.
125
125
  ['PageBack', 'breadcrumb'],
126
126
  ['PageTitle', 'title'],
127
+ ['PaneDescription', 'description'],
128
+ ['PaneTitle', 'title'],
127
129
  ['TableCaption', 'description'],
128
130
  ['TableHead', 'heading'],
129
131
  ['TabsTrigger', 'tab'],
@@ -327,6 +329,10 @@ const JSX_ARRAY_PROP_ROLES = new Map([
327
329
  ],
328
330
  ])
329
331
 
332
+ // Props que recebem uma lista de textos, um por item. Cada thumb de um intervalo do Slider tem
333
+ // nome próprio, e é esse nome que o leitor de tela anuncia ao focar o controle.
334
+ const JSX_STRING_ARRAY_PROP_ROLES = new Map([['Slider', new Map([['thumbLabels', 'label']])]])
335
+
330
336
  // Prop de classe que alcança cada superfície textual. A lista acompanha a implementação
331
337
  // real dos componentes: `className` do ActionFormCard vai para o form, enquanto
332
338
  // `cardClassName` envolve o título; o menu do Select custom vive em portal e não herda a
@@ -777,6 +783,7 @@ function knownJsxAggregateConsumer(expression, checker, mode) {
777
783
  JSX_PROP_ROLES.get(component)?.has(prop) === true ||
778
784
  JSX_OBJECT_PROP_ROLES.get(component)?.has(prop) === true ||
779
785
  JSX_ARRAY_PROP_ROLES.get(component)?.has(prop) === true ||
786
+ JSX_STRING_ARRAY_PROP_ROLES.get(component)?.has(prop) === true ||
780
787
  [...(JSX_PROP_CLASS.get(component)?.values() ?? [])].includes(prop) ||
781
788
  [...(JSX_PROP_STYLE.get(component)?.values() ?? [])].includes(prop) ||
782
789
  (JSX_CHILD_ROLES.has(component) && ['className', 'style'].includes(prop))
@@ -2435,6 +2442,28 @@ export function extractCopyFromSource(file, sourceText) {
2435
2442
  }
2436
2443
  }
2437
2444
 
2445
+ for (const [name, role] of JSX_STRING_ARRAY_PROP_ROLES.get(component) ?? []) {
2446
+ const item = attribute(opening.attributes, name)
2447
+ if (item.opaque !== undefined) {
2448
+ diagnostics.push(diagnostic(file, sourceFile, item.opaque, `<${component}>.${name}`))
2449
+ continue
2450
+ }
2451
+ if (item.candidate === undefined) continue
2452
+ const raw = attributeValue(item)
2453
+ const list = raw === null ? null : resolveBinding(raw, checker)
2454
+ if (list === null || !ts.isArrayLiteralExpression(list)) {
2455
+ diagnostics.push(
2456
+ diagnostic(file, sourceFile, raw ?? opening, `<${component}>.${name}`, raw === null ? 'structure' : 'content'),
2457
+ )
2458
+ continue
2459
+ }
2460
+ for (const [index, element] of list.elements.entries()) {
2461
+ const text = ts.isSpreadElement(element) ? null : staticText(element, checker)
2462
+ if (text === null) diagnostics.push(diagnostic(file, sourceFile, element, `<${component}>.${name}[${index}]`))
2463
+ else entries.push({ source: file, line: lineOf(sourceFile, text.node), role, text: text.text })
2464
+ }
2465
+ }
2466
+
2438
2467
  const ariaLabelAttribute = attribute(opening.attributes, 'aria-label')
2439
2468
  let hasStaticAriaLabel = false
2440
2469
  if (ariaLabelAttribute.opaque !== undefined) {
package/bin/lib/mcp.mjs CHANGED
@@ -42,7 +42,7 @@ export function buildServer() {
42
42
  server.registerTool(
43
43
  'opus_check',
44
44
  {
45
- description: 'Valida as convenções das actions (régua) — defineAction e o split defineContract+bindAction: naming, kind, ordem dos campos, export, requires-sem-authorize. Retorna findings.',
45
+ description: 'Valida as convenções das actions (régua) — defineAction e o split defineContract+bindAction: naming, kind, ordem dos campos, export, requires-sem-authorize, ignored-by-kind. Retorna findings.',
46
46
  inputSchema: { dir: z.string().optional() },
47
47
  },
48
48
  async ({ dir }) => asText(await scanDir(dirOf(dir))),
@@ -24,6 +24,14 @@ a mesma base visual porque essas variantes já prometem a moldura.
24
24
  `IconTile` é somente apresentação. Quando o ícone aciona algo, deve-se usar `Button` com tamanho
25
25
  de ícone e nome acessível.
26
26
 
27
+ ### Emenda de 2026-09-26 — slots de mídia das superfícies
28
+
29
+ Os slots de mídia da anatomia de superfícies (ADR 0023), como `CardMedia` e `ContentMedia`, não são
30
+ slots genéricos: assim como `ItemMedia variant="icon"`, eles já prometem a moldura, e o consumidor
31
+ passa só o glifo. `ContentMedia` adota um `IconTile` recebido como filho em vez de desenhar uma
32
+ moldura dentro da outra (ADR 0025). Slots genéricos, como `FieldLegend.icon`, continuam como esta
33
+ ADR descreve.
34
+
27
35
  ## Consequências
28
36
 
29
37
  - A presença da moldura deixa de ser uma regra implícita da legenda.