@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.
- package/CHANGELOG.md +239 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +8 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +109 -18
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/list.tsx +91 -92
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/presentation.tsx +52 -37
- package/src/ui/components/patterns/split.tsx +44 -4
- package/src/ui/components/patterns/surface-anatomy.ts +35 -8
- package/src/ui/components/patterns/surface-header.tsx +58 -25
- package/src/ui/components/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/card.tsx +1 -1
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -1
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/dialog.tsx +42 -11
- package/src/ui/components/primitives/drawer.tsx +10 -2
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +141 -58
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +41 -15
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +119 -27
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +107 -24
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +38 -15
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +12 -4
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +118 -12
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +55 -5
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +3 -2
- package/src/ui/docs/content/skeleton.md +6 -1
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +19 -0
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +32 -32
- package/src/ui/react.tsx +1 -0
- 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
|
-
["
|
|
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
|
-
["
|
|
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 ` `), 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 ` ` 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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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.
|