@softize/opus 15.0.0 → 15.0.1

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 CHANGED
@@ -7,6 +7,38 @@ 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
+ ## 15.0.1 — 2026-09-09
11
+
12
+ Correções sobre a 15.0.0, publicada horas antes, todas vindas da revisão dela. Nenhuma novidade de
13
+ API: nada entrou, saiu ou mudou de nome.
14
+
15
+ As ações de uma linha do `ActionList` só entram no `ButtonGroup` a partir de duas. Com uma só, o
16
+ grupo não é materializado: um `role="group"` sem nome por linha enchia a árvore de acessibilidade
17
+ sem informar nada, e o espaçamento não tinha o que espaçar. Fragmento devolvido pelo consumidor
18
+ conta como as ações que carrega, não como um filho só.
19
+
20
+ **Migração:** quem escreveu seletor contra o `[data-slot="button-group"]` da linha na 15.0.0
21
+ precisa mirar a célula. Com uma ação só, não há mais grupo.
22
+
23
+ A região introdutória do `PageHeader` — `PageBack` ou `PageNavigation` — passa a carregar
24
+ `data-slot="page-navigation"` também na apresentação em barra. Antes o marcador existia só no
25
+ header padrão, então um seletor de teste ou de CSS que funcionasse num não funcionava no outro.
26
+
27
+ `Page` deixa de emitir `min-h-0` e `min-h-full` ao mesmo tempo quando a barra encontra um estado
28
+ integral. O resultado dependia da ordem do CSS gerado; agora o estado integral declara a altura
29
+ que precisa e a barra só contém a rolagem quando não há estado.
30
+
31
+ `opus check` para de acrescentar um segundo diagnóstico sobre a forma de composição quando o
32
+ problema é uma propriedade removida. A prop já diz o que corrigir; a mensagem seguinte era
33
+ verdadeira e enganosa ao mesmo tempo.
34
+
35
+ A ADR 0004 ganha, no adendo, o motivo que sustenta a alternativa descartada — antes ela se apoiava
36
+ numa citação que não sustentava a conclusão — e a segunda consequência da decisão: `PageHeader`
37
+ desiste antes de validar os filhos, então um cabeçalho inválido só lança quando a página chega em
38
+ `ready` — o `opus check` continua pegando isso estaticamente. A guidance de UI e a doc de `Page`
39
+ passam a avisar que o retorno some junto com o cabeçalho e que a saída, nesse caso, é o `action` do
40
+ próprio `PageState`.
41
+
10
42
  ## 15.0.0 — 2026-09-09
11
43
 
12
44
  `PageHeader` passa a oferecer `variant="bar"`, uma apresentação compacta da mesma região de título,
package/bin/lib/check.mjs CHANGED
@@ -950,8 +950,10 @@ export function checkUiStructure(file, text) {
950
950
  : [],
951
951
  ),
952
952
  );
953
+ let removedProp = false;
953
954
  for (const [attribute, hint] of root.removed ?? []) {
954
955
  if (attributes.has(attribute)) {
956
+ removedProp = true;
955
957
  add(
956
958
  node,
957
959
  component,
@@ -967,7 +969,15 @@ export function checkUiStructure(file, text) {
967
969
  const structural =
968
970
  children.names.includes(root.header) ||
969
971
  children.names.includes(root.body);
970
- if (shorthand && structural) {
972
+ // A prop removida saiu do `shorthand`, então sozinha ela não marca o nó como forma curta:
973
+ // ele cai no ramo explícito, que cobra header e body — uma segunda mensagem inteiramente
974
+ // induzida pela prop. Sem filhos estruturais essa cobrança dispara de qualquer jeito, e
975
+ // some aqui.
976
+ // Com filhos estruturais, o que as regras de forma acham é defeito independente: misturar
977
+ // shorthand com slots, ou um filho inesperado, não some quando a prop sai.
978
+ if (removedProp && !structural) {
979
+ // Nada a acrescentar; os filhos seguem sendo visitados no fim de `visit`.
980
+ } else if (shorthand && structural) {
971
981
  add(
972
982
  node,
973
983
  component,
@@ -90,10 +90,17 @@ superfície de estado carrega `role="heading"` com `aria-level={1}`.
90
90
 
91
91
  **Alternativa descartada.** Ocultar apenas título, descrição e ações, preservando a região
92
92
  introdutória. Foi descartada para esta versão porque partiria o cabeçalho em duas regras de
93
- visibilidade e deixaria a barra materializada para um botão, contrariando "um header sem
94
- conteúdo útil não é materializado" da ADR 0010.
93
+ visibilidade uma para o retorno, outra para o resto —, e um cabeçalho que aparece pela metade
94
+ é mais difícil de prever do que um que some inteiro. Não é uma decisão confortável: ver
95
+ "O que se perde".
95
96
 
96
97
  **O que se perde, e é conhecido.** Some junto o `PageBack`, então uma subpágina em erro fica sem o
97
98
  retorno in-page para o pai — justamente quando a pessoa mais precisa sair. Enquanto esta ADR não
98
99
  for revista, uma subpágina que dependa desse retorno oferece a saída pelo `action` do próprio
99
100
  `PageState`. Reavaliar se o custo aparecer em uso real.
101
+
102
+ Uma segunda consequência é da mesma decisão: como `PageHeader` desiste antes de validar seus
103
+ filhos, um cabeçalho estruturalmente inválido — dois `PageBack`, ou sem `PageTitle` — deixa de
104
+ lançar enquanto a página está em estado integral, e só lança quando ela chega em `ready`. O
105
+ `opus check` continua pegando isso estaticamente, então o erro não passa despercebido até
106
+ produção; o que muda é o momento em que aparece no desenvolvimento.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "15.0.0",
3
+ "version": "15.0.1",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,12 +19,13 @@
19
19
  `PageActionsTarget` fica reservado a workspaces imersivos que já possuam chrome próprio.
20
20
  - `PageState` substitui todo o conteúdo principal quando a página carrega, falha ou está vazia. Na
21
21
  forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
22
- explícita, fica sozinho dentro de `PageBody`. Nos três estados ativos, o cabeçalho some, o estado
23
- ocupa a área disponível e seu título assume o heading principal, inclusive quando um componente
24
- intermediário renderiza o estado. O erro mantém `role="alert"`, usa a mesma composição central e
25
- sem moldura dos demais estados e apresenta a recuperação como botão `outline` textual. Estados de
26
- seção ou coleção continuam em `DataState`, `ActionView`, `ActionList` ou `Alert`; não elevar uma
27
- falha parcial a estado da página.
22
+ explícita, fica sozinho dentro de `PageBody`. Nos três estados ativos, o cabeçalho some inteiro
23
+ incluindo o `PageBack` —, o estado ocupa a área disponível e seu título assume o heading
24
+ principal, inclusive quando um componente intermediário renderiza o estado. Uma subpágina que
25
+ dependa do retorno ao pai oferece essa saída pelo `action` do próprio `PageState`. O erro mantém
26
+ `role="alert"`, usa a mesma composição central e sem moldura dos demais estados e apresenta a
27
+ recuperação como botão `outline` textual. Estados de seção ou coleção continuam em `DataState`,
28
+ `ActionView`, `ActionList` ou `Alert`; não elevar uma falha parcial a estado da página.
28
29
  - `Content` delimita uma seção e segue a mesma anatomia: `Content > ContentHeader (ContentTitle,
29
30
  ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader` nunca fica
30
31
  solto. `title`, `description`, `count` e `actions` no `Content` são a abreviação para o caso
@@ -20,6 +20,9 @@
20
20
  */
21
21
 
22
22
  import {
23
+ Children,
24
+ Fragment,
25
+ isValidElement,
23
26
  useEffect,
24
27
  useLayoutEffect,
25
28
  useMemo,
@@ -1142,6 +1145,31 @@ export function ActionFilterBar({
1142
1145
  // ActionList
1143
1146
  // =============================================================================
1144
1147
 
1148
+ /**
1149
+ * Ações de uma linha da tabela. Com duas ou mais, agrupa e espaça; com uma só, não
1150
+ * materializa o `ButtonGroup` — um `role="group"` sem nome por linha enche a árvore de
1151
+ * acessibilidade sem informar nada, e `mode="spaced"` não teria o que espaçar.
1152
+ */
1153
+ function RowActions({ children }: { children: ReactNode }) {
1154
+ // O consumidor costuma devolver as ações dentro de um fragment, que conta como UM filho —
1155
+ // e às vezes dentro de fragments aninhados. Expandir recursivamente, e contar só elementos,
1156
+ // evita tanto tratar duas ações como uma quanto deixar um espaço em branco virar ação.
1157
+ const expand = (node: ReactNode): ReactNode[] =>
1158
+ Children.toArray(node).flatMap((child) =>
1159
+ isValidElement(child) && child.type === Fragment
1160
+ ? expand((child.props as { children?: ReactNode }).children)
1161
+ : [child],
1162
+ );
1163
+ const alone = expand(children).filter(isValidElement).length < 2;
1164
+ return alone ? (
1165
+ <div className="flex justify-end">{children}</div>
1166
+ ) : (
1167
+ <ButtonGroup mode="spaced" className="ml-auto">
1168
+ {children}
1169
+ </ButtonGroup>
1170
+ );
1171
+ }
1172
+
1145
1173
  export function ActionList<TInput, TItem>({
1146
1174
  action,
1147
1175
  input,
@@ -1634,9 +1662,7 @@ export function ActionList<TInput, TItem>({
1634
1662
  className="w-px whitespace-nowrap text-right"
1635
1663
  onClick={(e) => e.stopPropagation()}
1636
1664
  >
1637
- <ButtonGroup mode="spaced" className="ml-auto">
1638
- {rowActions(item)}
1639
- </ButtonGroup>
1665
+ <RowActions>{rowActions(item)}</RowActions>
1640
1666
  </TableCell>
1641
1667
  )}
1642
1668
  </TableRow>
@@ -169,8 +169,11 @@ export function Page({
169
169
  data-slot="page"
170
170
  className={cn(
171
171
  "min-w-0 flex-1",
172
- headerVariant === "bar" && "flex min-h-0 flex-col",
173
- integralState && "flex min-h-full flex-col",
172
+ (headerVariant === "bar" || integralState) && "flex flex-col",
173
+ // A barra contém a rolagem (`min-h-0`); o estado integral ocupa a altura
174
+ // disponível (`min-h-full`). Emitir os dois juntos deixava o resultado por
175
+ // conta da ordem do CSS gerado — no estado integral, quem manda é ele.
176
+ integralState ? "min-h-full" : headerVariant === "bar" && "min-h-0",
174
177
  )}
175
178
  {...props}
176
179
  >
@@ -146,9 +146,17 @@ export function SurfaceHeader({
146
146
  {descriptions}
147
147
  </div>
148
148
  );
149
+ // A região introdutória carrega o mesmo `data-slot` nas duas apresentações: seletor de teste
150
+ // ou de CSS que funcione no header padrão precisa funcionar na barra.
149
151
  const row = (
150
152
  <>
151
- {leadingPlacement === "inline" && leading}
153
+ {leadingPlacement === "inline" && leading.length > 0 && (
154
+ // `min-w-0` aqui é o que deixa a trilha do PageNavigation ceder e truncar; sem ele o
155
+ // wrapper assume o min-content do breadcrumb e a compressão toda cai sobre o título.
156
+ <div data-slot={`${slot}-navigation`} className="flex min-w-0 items-center">
157
+ {leading}
158
+ </div>
159
+ )}
152
160
  {heading}
153
161
  {actionSlots}
154
162
  </>
@@ -302,4 +302,4 @@ chamar a action.
302
302
  | `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
303
303
  | `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
304
304
  | `toolbarActions` | `ReactNode` | | Ações do consumidor no fim da barra, separadas do grupo de controles. |
305
- | `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita), agrupadas automaticamente com intervalo compacto; cliques ali não disparam o `onRowClick`. |
305
+ | `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita). A partir de duas, entram num grupo com intervalo compacto; uma só fica solta, sem grupo. Cliques ali não disparam o `onRowClick`. |
@@ -123,10 +123,11 @@ render(
123
123
  Use `PageState` quando carregamento, falha ou ausência substituírem todo o conteúdo principal. Na
124
124
  forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
125
125
  explícita, coloque-o sozinho dentro de `PageBody`. Enquanto `status` for `loading`, `error` ou
126
- `empty`, `Page` oculta o cabeçalho e o estado ocupa a altura disponível. O título do estado assume o
127
- heading principal. Esse registro também funciona quando um componente intermediário decide qual
128
- `PageState` renderizar. Em `ready`, o cabeçalho e o conteúdo voltam à composição normal. O cabeçalho some inteiro, incluindo o `PageBack`: uma subpágina que dependa desse retorno
129
- oferece a saída pelo `action` do próprio `PageState` (ADR 0004, adendo).
126
+ `empty`, `Page` oculta o cabeçalho inteiro — incluindo o `PageBack` — e o estado ocupa a altura
127
+ disponível. Uma subpágina que dependa desse retorno oferece a saída pelo `action` do próprio
128
+ `PageState`. O título do estado assume o heading principal. Esse registro também funciona quando um
129
+ componente intermediário decide qual `PageState` renderizar. Em `ready`, o cabeçalho e o conteúdo
130
+ voltam à composição normal.
130
131
 
131
132
  ```tsx preview col
132
133
  <Page title="Relatório">
@@ -244,7 +245,7 @@ estado na própria seção com `DataState`, `ActionView`, `ActionList` ou `Alert
244
245
  | `title` | `ReactNode` | Texto seguro por estado | Situação reconhecível pela pessoa. |
245
246
  | `description` | `ReactNode` | | Impacto ou próximo passo aplicável. |
246
247
  | `icon` | `ReactNode` | Alerta no erro | Ícone decorativo do estado. |
247
- | `action` | `ReactNode` | | Seleção ou criação aplicável ao estado. |
248
+ | `action` | `ReactNode` | | Seleção, criação ou saída aplicável ao estado — inclusive o retorno ao pai, já que o cabeçalho está oculto. |
248
249
  | `emptyMessage` | `string` | `'Nada por aqui'` | Título do vazio quando `title` não é informado. |
249
250
  | `errorMessage` | `string` | `'Não foi possível carregar esta página'` | Título do erro quando `title` não é informado. |
250
251
  | `onRetry` | `() => void \| Promise<void>` | | Recuperação do erro: acrescenta um botão `outline` textual ao lado de `action`. |