@softize/opus 13.1.0 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/bin/cli.mjs +2 -0
  3. package/bin/lib/check.mjs +33 -5
  4. package/bin/lib/cli-shared.mjs +30 -1
  5. package/bin/lib/copy.mjs +279 -6
  6. package/bin/lib/db.mjs +2 -0
  7. package/docs/adr/0004-page-content-state-is-composed.md +39 -5
  8. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +9 -3
  9. package/docs/adr/0009-page-title-does-not-carry-a-counter.md +57 -0
  10. package/docs/adr/0010-page-header-owns-page-chrome.md +73 -0
  11. package/docs/code-style.md +4 -1
  12. package/docs/data-layer.md +9 -0
  13. package/package.json +1 -1
  14. package/registry/instructions/opus.md +3 -3
  15. package/registry/skills/build-opus-ui/SKILL.md +3 -2
  16. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
  17. package/registry/templates/app/src/App.tsx +11 -6
  18. package/src/core/types.ts +3 -4
  19. package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
  20. package/src/ui/components/patterns/confirm.tsx +2 -31
  21. package/src/ui/components/patterns/content-header.tsx +42 -141
  22. package/src/ui/components/patterns/data-state.tsx +42 -68
  23. package/src/ui/components/patterns/form.tsx +15 -15
  24. package/src/ui/components/patterns/list.tsx +55 -34
  25. package/src/ui/components/patterns/page-state.tsx +81 -51
  26. package/src/ui/components/patterns/page.tsx +228 -97
  27. package/src/ui/components/patterns/state-surface.tsx +262 -0
  28. package/src/ui/components/patterns/surface-header.tsx +204 -0
  29. package/src/ui/components/patterns/trigger.tsx +9 -10
  30. package/src/ui/components/patterns/view.tsx +14 -16
  31. package/src/ui/components/primitives/alert.tsx +1 -33
  32. package/src/ui/components/primitives/avatar.tsx +15 -5
  33. package/src/ui/components/primitives/badge.tsx +2 -43
  34. package/src/ui/components/primitives/button-group.tsx +34 -8
  35. package/src/ui/components/primitives/button.tsx +31 -35
  36. package/src/ui/components/primitives/control.ts +69 -0
  37. package/src/ui/components/primitives/dot.tsx +1 -30
  38. package/src/ui/components/primitives/input-group.tsx +11 -8
  39. package/src/ui/components/primitives/item.tsx +3 -1
  40. package/src/ui/components/primitives/menu.tsx +1 -7
  41. package/src/ui/components/primitives/pagination.tsx +16 -8
  42. package/src/ui/components/primitives/select.tsx +2 -2
  43. package/src/ui/components/primitives/spinner.tsx +13 -16
  44. package/src/ui/components/primitives/switch.tsx +4 -1
  45. package/src/ui/components/primitives/tabs.tsx +5 -3
  46. package/src/ui/components/primitives/toggle.tsx +9 -4
  47. package/src/ui/docs/content/action-form.md +26 -0
  48. package/src/ui/docs/content/action-list-dialog.md +2 -2
  49. package/src/ui/docs/content/action-list.md +35 -2
  50. package/src/ui/docs/content/action-trigger.md +5 -4
  51. package/src/ui/docs/content/action-view.md +3 -2
  52. package/src/ui/docs/content/alert.md +16 -4
  53. package/src/ui/docs/content/avatar.md +7 -3
  54. package/src/ui/docs/content/button.md +48 -17
  55. package/src/ui/docs/content/communication.md +36 -0
  56. package/src/ui/docs/content/content.md +5 -4
  57. package/src/ui/docs/content/data-state.md +17 -13
  58. package/src/ui/docs/content/dialog.md +1 -4
  59. package/src/ui/docs/content/input.md +1 -1
  60. package/src/ui/docs/content/item.md +1 -1
  61. package/src/ui/docs/content/page.md +160 -37
  62. package/src/ui/docs/content/pagination.md +11 -9
  63. package/src/ui/docs/content/semantic-context.md +3 -2
  64. package/src/ui/docs/content/sidebar.md +2 -42
  65. package/src/ui/docs/content/spinner.md +9 -6
  66. package/src/ui/docs/content/switch.md +1 -1
  67. package/src/ui/docs/content/tabs.md +1 -1
  68. package/src/ui/docs/content/toggle.md +1 -1
  69. package/src/ui/drivers/react.tsx +1 -6
  70. package/src/ui/meta.ts +8 -8
  71. package/src/ui/react.tsx +16 -18
  72. package/src/ui/components/patterns/shell-nav.tsx +0 -154
package/bin/lib/db.mjs CHANGED
@@ -189,6 +189,8 @@ export function helpDb() {
189
189
 
190
190
  Flags:
191
191
  --config <path> Caminho interno ao projeto para opus.config.ts. Default: ./opus.config.ts
192
+ O .env ao lado dele é carregado antes do config, então DATABASE_URL do
193
+ projeto vale sem export manual; variável do ambiente vence o arquivo.
192
194
  --help, -h Mostra esta mensagem
193
195
 
194
196
  O opus.config.ts precisa expor, pros comandos db:
@@ -1,5 +1,10 @@
1
1
  # ADR 0004 — O estado integral do conteúdo é composto dentro de Page
2
2
 
3
+ > **Atualização (2026-09-09).** A decisão original preservava o cabeçalho da página em todos os
4
+ > estados. Ela foi invertida: `PageState` passa a ocultá-lo em `loading`, `error` e `empty`, e a
5
+ > restaurá-lo em `ready`. A lista de responsabilidades abaixo já descreve o comportamento novo; o
6
+ > adendo no fim deste arquivo registra o motivo, a alternativa descartada e o que se perde.
7
+
3
8
  ## Contexto
4
9
 
5
10
  `Page` padroniza o `<main>`, o cabeçalho e o container de uma página. Hoje, consumidores que ainda
@@ -21,15 +26,21 @@ direto de `Page`, que materializa `PageBody`; na composição explícita, `PageS
21
26
  `PageState`:
22
27
 
23
28
  - recebe um estado discriminado entre `loading`, `error`, `empty` e `ready`;
24
- - preserva o cabeçalho da página em todos os estados;
25
- - compõe `Spinner`, `Alert` e `Empty` em vez de recriar suas superfícies;
29
+ - oculta o cabeçalho em `loading`, `error` e `empty`, restaurando-o em `ready`;
30
+ - registra sua presença no `Page` mesmo quando um componente intermediário o renderiza;
31
+ - ocupa a altura disponível e usa o título visível do estado como heading principal;
32
+ - centraliza os três estados integrais com uma anatomia visual comum e sem moldura;
33
+ - preserva `role="status"` no carregamento e `role="alert"` na falha sem apresentar o erro como `Alert`;
26
34
  - aceita título, descrição, ícone e ação contextual sem exibir erro técnico;
35
+ - apresenta a recuperação integral como botão `outline` textual; o ícone isolado fica restrito a
36
+ superfícies compactas;
27
37
  - expõe `data-slot="page-state"` e `data-status` para testes e análise estrutural;
28
38
  - renderiza o conteúdo sem moldura adicional em `ready`.
29
39
 
30
40
  Estados parciais continuam pertencendo a `DataState`, `ActionView`, `ActionList` ou `Alert`, conforme
31
- a fronteira afetada. Uma coleção vazia continua dentro da estrutura da coleção; `Empty` representa
32
- uma região disponível, uma escolha pendente ou uma próxima ação.
41
+ a fronteira afetada. Uma coleção vazia continua dentro da estrutura da coleção. A moldura tracejada
42
+ de `Empty` representa uma região disponível para criar ou vincular; ela não aparece automaticamente
43
+ no vazio integral de uma página.
33
44
 
34
45
  ## Alternativas consideradas
35
46
 
@@ -51,7 +62,8 @@ telas equivalentes.
51
62
 
52
63
  ## Consequências
53
64
 
54
- - Consumidores ganham uma composição uniforme sem acoplar `Page` a hooks ou actions.
65
+ - Consumidores ganham uma composição uniforme sem acoplar `Page` a hooks ou actions e sem repetir
66
+ classes de altura.
55
67
  - A copy específica do fluxo permanece no consumidor; defaults seguros cobrem usos simples.
56
68
  - Migrações precisam distinguir estado integral de estado parcial antes de substituir a composição.
57
69
  - O gate estrutural do consumidor deve impedir novos estados integrais montados manualmente.
@@ -63,3 +75,25 @@ telas equivalentes.
63
75
  composição explícita.
64
76
  - Consumidores verificam `data-slot="page-state"` nos estados integrais e mantêm testes próprios para
65
77
  recuperação e distinção entre erro e vazio.
78
+
79
+ ## Adendo (2026-09-09) — o cabeçalho é ocultado nos estados integrais
80
+
81
+ **Contexto.** A decisão original preservava o cabeçalho em `loading`, `error` e `empty`. Na
82
+ prática, uma página em carregamento mostrava título e ações de um conteúdo que ainda não existia, e
83
+ uma página em erro oferecia ações sobre um conteúdo que falhou. A ADR 0010, ao trazer a
84
+ apresentação em barra, tornou isso mais visível: a faixa ficava de pé, com ações inertes, sobre uma
85
+ superfície de estado que ocupa a tela inteira.
86
+
87
+ **Decisão.** `Page` oculta o `PageHeader` enquanto o `PageState` estiver em `loading`, `error` ou
88
+ `empty`, e o restaura em `ready`. Não há opt-out. O nível semântico não se perde: a própria
89
+ superfície de estado carrega `role="heading"` com `aria-level={1}`.
90
+
91
+ **Alternativa descartada.** Ocultar apenas título, descrição e ações, preservando a região
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 só para um botão, contrariando "um header sem
94
+ conteúdo útil não é materializado" da ADR 0010.
95
+
96
+ **O que se perde, e é conhecido.** Some junto o `PageBack`, então uma subpágina em erro fica sem o
97
+ retorno in-page para o pai — justamente quando a pessoa mais precisa sair. Enquanto esta ADR não
98
+ for revista, uma subpágina que dependa desse retorno oferece a saída pelo `action` do próprio
99
+ `PageState`. Reavaliar se o custo aparecer em uso real.
@@ -3,6 +3,10 @@
3
3
  - **Status:** aceita.
4
4
  - **Data:** 2026-09-04.
5
5
 
6
+ > **Atualização (2026-09-09).** Parcialmente substituída pela ADR 0009 quanto a `PageMeta` e ao
7
+ > `count` de `Page`, e complementada pela ADR 0010, que acrescenta as apresentações `default` e
8
+ > `bar` ao mesmo header. O corpo abaixo já traz a anatomia vigente.
9
+
6
10
  ## Contexto
7
11
 
8
12
  Os componentes estruturais da UI descrevem regiões equivalentes com APIs diferentes. `Dialog`
@@ -20,8 +24,9 @@ perdida para obter uma estrutura explícita.
20
24
  As superfícies mantidas pela casa adotam a gramática `Root > Header + Body + Footer`, com
21
25
  `Title`, `Description`, `Meta` e `Actions` pertencendo ao `Header` da mesma família.
22
26
 
23
- - `Page` oferece `PageHeader`, `PageTitle`, `PageDescription`, `PageMeta`, `PageActions` e
24
- `PageBody`.
27
+ - `Page` oferece `PageHeader`, `PageBack`, `PageNavigation`, `PageTitle`, `PageDescription`,
28
+ `PageActions` e `PageBody`. A ADR 0010 acrescenta as apresentações `default` e `bar` ao mesmo
29
+ header.
25
30
  - `Content` representa uma região de conteúdo semanticamente nomeada e oferece `ContentHeader`,
26
31
  `ContentTitle`, `ContentDescription`, `ContentMeta`, `ContentActions` e `ContentBody`.
27
32
  - `CardContent` passa a ter `CardBody` como nome canônico.
@@ -37,7 +42,8 @@ As superfícies mantidas pela casa adotam a gramática `Root > Header + Body + F
37
42
  `ItemActions` e `ItemFooter`. `ItemContent` permanece temporariamente como alias legado de
38
43
  corpo, mas deixa de envolver título e descrição no código novo.
39
44
 
40
- `Page` e `Content` aceitam também uma forma curta com `title`, `description`, `meta` e `actions`.
45
+ `Page` e `Content` aceitam também uma forma curta: `title`, `description` e `actions` nos dois,
46
+ mais `count` no `Content`. A ADR 0009 tirou o contador do título da página.
41
47
  Essa forma é açúcar sintático: produz a mesma árvore semântica, os mesmos estilos e os mesmos
42
48
  `data-slot` da composição explícita. Um consumidor não pode misturar as duas formas na mesma raiz.
43
49
 
@@ -0,0 +1,57 @@
1
+ # ADR 0009 — O título da página não carrega contador
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-09.
5
+ - **Substitui parcialmente:** ADR 0005, somente quanto a `PageMeta` e `count` em `Page`.
6
+
7
+ ## Contexto
8
+
9
+ `Page` permitia colocar um total imediatamente ao lado do título por `count` ou `PageMeta`. O
10
+ número competia com a identidade da página, não explicava sozinho o conjunto contado e repetia
11
+ informação que já pertence à listagem ou a uma seção do conteúdo.
12
+
13
+ `Content` também possui metadata, mas representa regiões menores em que o valor pode estar ligado
14
+ ao título local e continuar compreensível dentro da própria seção.
15
+
16
+ ## Decisão
17
+
18
+ `Page` deixa de aceitar `count` e de exportar `PageMeta`. O cabeçalho da página reconhece somente
19
+ `PageTitle`, `PageDescription` e `PageActions`. Totais e outros indicadores pertencem ao conteúdo
20
+ que os explica, como uma listagem, métrica ou seção composta com `Content`.
21
+
22
+ `ContentMeta` e o `count` de `Content` permanecem disponíveis. A anatomia compartilhada continua
23
+ sendo usada, mas cada família expõe apenas os slots coerentes com sua escala.
24
+
25
+ ## Consequências
26
+
27
+ - O título da página volta a comunicar somente a identidade da superfície.
28
+ - Consumidores removem contadores do cabeçalho em vez de deslocá-los para outra posição sem
29
+ contexto.
30
+ - Uma página que precise destacar um total o apresenta no corpo, próximo do conjunto ou da métrica
31
+ correspondente.
32
+ - A remoção de `count` e `PageMeta` é incompatível e entra somente na próxima versão major.
33
+
34
+ ## Alternativas consideradas
35
+
36
+ ### Manter o contador como opção
37
+
38
+ Preservaria compatibilidade, mas manteria uma composição visual que a aplicação já decidiu não
39
+ usar. Foi descartada porque uma opção pública continuaria incentivando o retorno do padrão.
40
+
41
+ ### Ocultar o contador apenas no tema
42
+
43
+ Evitaria a migração imediata, mas deixaria contrato, documentação e DOM sustentando uma capacidade
44
+ sem representação. Foi descartada porque esconder não remove o padrão.
45
+
46
+ ### Mover automaticamente o total para a descrição
47
+
48
+ Preservaria a informação, mas o componente não conhece o que está sendo contado nem consegue
49
+ escrever contexto correto. Foi descartada para que o consumidor escolha uma superfície semântica
50
+ quando o total for realmente necessário.
51
+
52
+ ## Verificação
53
+
54
+ - O tipo de `Page` não aceita `count` e o barrel público não exporta `PageMeta`.
55
+ - Testes de UI verificam a anatomia curta e explícita sem `page-meta`.
56
+ - Busca estrutural impede usos de `count` em `Page` nos consumidores migrados.
57
+ - Documentação e metadata públicas apresentam somente título, descrição e ações.
@@ -0,0 +1,73 @@
1
+ # ADR 0010 — PageHeader também representa o chrome compacto da página
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-09.
5
+ - **Complementa:** ADR 0005.
6
+
7
+ ## Contexto
8
+
9
+ Aplicações com sidebar passaram a montar uma barra superior separada de `PageHeader` para reunir
10
+ navegação contextual e ações. Essa separação cria dois cabeçalhos para a mesma página, distribui
11
+ título, retorno e ações entre contratos concorrentes e permite reservar uma faixa vazia quando o
12
+ shell não recebe conteúdo.
13
+
14
+ O breadcrumb absoluto também repete a localização já comunicada pela sidebar e pelo título. Em uma
15
+ subpágina simples, a necessidade real é retornar a um pai conhecido; em hierarquias mais profundas,
16
+ é apresentar os ancestrais relevantes. Nenhum dos casos exige uma segunda anatomia de cabeçalho.
17
+
18
+ ## Decisão
19
+
20
+ `PageHeader` é a única região de cabeçalho de uma `Page` e oferece duas apresentações:
21
+
22
+ - `variant="default"` organiza título, descrição e ações dentro do container da página;
23
+ - `variant="bar"` ocupa uma faixa compacta, delimitada por borda, no topo da página.
24
+
25
+ `PageBack` pertence diretamente a `PageHeader` e recebe um destino explícito. Na apresentação
26
+ padrão, aparece acima do conjunto de título e descrição como botão `ghost` com ícone e rótulo. Na
27
+ barra, aparece como controle icon-only com tooltip e nome acessível derivados do destino visível.
28
+ `PageBack` e breadcrumb não aparecem juntos: retorno simples usa `PageBack`; múltiplos ancestrais
29
+ relevantes são compostos com `Breadcrumb` dentro de `PageNavigation`, na mesma posição do
30
+ cabeçalho.
31
+
32
+ A variante altera somente a apresentação. Título, descrição, retorno e ações continuam pertencendo
33
+ semanticamente à mesma página. Um header sem conteúdo útil não é materializado para reservar
34
+ altura, e o header é ocultado durante os estados integrais do `PageState` — inversão da decisão
35
+ original da ADR 0004, argumentada no adendo dela. Para acompanhar o ritmo
36
+ compacto da barra, ações com texto usam `Button size="sm"` e ações somente com ícone usam
37
+ `Button size="icon-sm"`.
38
+
39
+ ## Consequências
40
+
41
+ - shells deixam de precisar de um `PageChrome` paralelo para páginas comuns;
42
+ - páginas de primeiro nível usam o header padrão e não ganham uma barra vazia;
43
+ - subpáginas simples mantêm o retorno junto ao título na apresentação padrão;
44
+ - recursos que precisam de uma faixa persistente podem escolher `variant="bar"` sem mover ações por
45
+ portal;
46
+ - botões da barra deixam de misturar a escala normal da página com controles compactos;
47
+ - workspaces imersivos ainda podem manter um shell próprio quando não são representados por `Page`.
48
+
49
+ ## Alternativas consideradas
50
+
51
+ ### Manter PageHeader e chrome separados
52
+
53
+ Preservaria a implementação atual dos consumidores, mas continuaria dividindo uma única região
54
+ semântica entre duas APIs e exigindo regras para decidir onde cada ação aparece.
55
+
56
+ ### Exibir breadcrumb absoluto em toda subpágina
57
+
58
+ Ofereceria uma trilha uniforme, mas repetiria a navegação global já visível e ocuparia espaço com a
59
+ página atual, que já está nomeada pelo título.
60
+
61
+ ### Usar sempre uma barra
62
+
63
+ Fixaria a posição dos controles, mas reservaria altura em páginas que não possuem navegação
64
+ contextual nem ações persistentes. A barra permanece uma escolha de apresentação, não o default.
65
+
66
+ ## Verificação
67
+
68
+ - testes de `Page` cobrem as duas variantes, a posição de `PageBack`, sua acessibilidade e o destino
69
+ explícito;
70
+ - `opus check` reconhece `PageBack` e `PageNavigation` somente como filhos diretos e mutuamente
71
+ exclusivos de `PageHeader`;
72
+ - a documentação demonstra retorno simples nas duas apresentações e separa esse caso de breadcrumb;
73
+ - consumidores removem barras paralelas à medida que migram para a anatomia de `Page`.
@@ -86,13 +86,16 @@ Base instalada é dona do catálogo, dos kinds aceitos e do fundamento de cada r
86
86
  | `messages.success`, `messages.error`, `messages.confirmation` | `success`, `error`, `message` |
87
87
  | `confirm.message` | `dialog-body` |
88
88
  | `fields.*.label`, `filters.*.label` | `label` |
89
- | `placeholder`, `hint`, `help` | `placeholder`, `helper-text` |
89
+ | `placeholder`, `help` | `placeholder`, `helper-text` |
90
90
  | opções estáticas | `menu-item` |
91
91
  | `columns[].label`, `periods[].label` | `heading`, `tab` |
92
92
  | `Select.emptyText`, `Select.searchPlaceholder` | `empty-state`, `placeholder` |
93
93
  | `Select.options[].hint/triggerLabel/group` | `label`, `label`, `heading` |
94
94
  | `ActionTrigger.confirm.*` | papel correspondente do diálogo |
95
95
  | `t.dict` — `label`, `description`, `doc` das entradas e `doc` do dicionário | `label`, `description` |
96
+ | `DataState`/`PageState`/`ActionList`/`ActionListDialog` — `emptyMessage`, `errorMessage`, `retryLabel`; `PageState`/`ActionListDialog` — `title`, `description`; `ActionView.emptyMessage` | `empty-state`, `error`, `button`, `title`, `description` |
97
+ | `dialog.alert/confirm/prompt/choose()` — `title`, `description`, `body`, `action`, `cancel`, `placeholder`, `actions[].label` | papel correspondente do diálogo |
98
+ | `TooltipContent` (children), `LabelHelp.help` | `label`, `helper-text` |
96
99
 
97
100
  ## Cobertura e significado do gate verde
98
101
 
@@ -70,6 +70,15 @@ opus db migrate # aplica o SCHEMA IDEMPOTENTE + drift-check na sequência
70
70
  opus db scaffold # gera rascunho a partir do diff (referência pro schema)
71
71
  ```
72
72
 
73
+ **De onde vem a conexão.** O config do consumer costuma abrir o banco no topo do módulo,
74
+ lendo `process.env` na hora do import. Por isso o CLI carrega o `.env` ao lado do
75
+ `opus.config.ts` ANTES de importar o config — em `db`, `seed` e `gen`, num lugar só, para
76
+ que nenhum comando possa esquecer. Variável já definida no ambiente vence o arquivo (a
77
+ semântica do `--env-file` do Node), então `DATABASE_URL=... opus db migrate` e a injeção
78
+ do container continuam mandando; ausência de `.env` é normal e não é erro. Sem isso, um
79
+ projeto que guarda a URL no `.env` migrava o banco DEFAULT do config enquanto o runtime
80
+ usava outro — o schema aplicado some do banco que a aplicação abre.
81
+
73
82
  **Schema idempotente evolutivo** (o padrão da casa, não migration versionada): UM
74
83
  script SQL (`config.schema`, ex. `src/db/schema.sql`) re-rodável — `CREATE IF NOT
75
84
  EXISTS` + guards `DO $$ IF EXISTS` cobrem nascer do zero E upgrade de prod no mesmo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "13.1.0",
3
+ "version": "15.0.0",
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",
@@ -17,9 +17,9 @@ artefatos de agentes fica em `base.json`; cada app Opus mantém seu marcador `op
17
17
  - Não duplicar schemas, tipos de transporte, validação ou fetch que o contrato já fornece.
18
18
  - Em UI semântica, declarar primeiro `context` (`neutral`, `primary`, `info`, `success`, `warning`
19
19
  ou `danger`) e usar `variant` somente para o tratamento visual (`solid`, `subtle`, `outline`,
20
- `ghost` ou `link`). Dicionários de status e estágio declaram `context`; `tone` e variantes
21
- semânticas antigas são apenas compatibilidade de migração. `destructive` permanece uma
22
- propriedade comportamental de actions e se projeta visualmente como `danger`.
20
+ `ghost` ou `link`). Dicionários de status e estágio declaram `context`; `tone` é apenas
21
+ compatibilidade de migração e as variantes semânticas antigas não existem mais. `destructive`
22
+ permanece uma propriedade comportamental de actions e se projeta visualmente como `danger`.
23
23
  - Declarar datasets persistentes com `defineSeed` + `bindSeed`, registrá-los em `opus.config.ts`
24
24
  e operá-los por `opus seed`; não criar comandos de seed paralelos nem reset implícito.
25
25
  - Regenerar o inventário com `opus copy` quando mudar copy em contrato ou componente Opus
@@ -42,8 +42,9 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
42
42
  o produto precisa de deep link, back/forward ou refresh.
43
43
  8. Compor superfícies pela gramática estrutural do catálogo: `Page` contém `PageHeader` e
44
44
  `PageBody`; `Content` contém `ContentHeader` e `ContentBody`; Card, Drawer e Pane usam seus
45
- respectivos `*Body`. Para o caso direto, usar a sintaxe abreviada de `Page` ou `Content` com
46
- `title`, `description`, `meta` e `actions`; não misturá-la com o header explícito. Ajustar o
45
+ respectivos `*Body`. Para o caso direto, usar a sintaxe abreviada de `Page` (`title`,
46
+ `description`, `actions`) ou de `Content` (as mesmas mais `count`); não misturá-la com o
47
+ header explícito. O título da página não carrega contador. Ajustar o
47
48
  nível do heading pela hierarquia semântica, não pelo destaque visual. O `Page` mantém seu teto
48
49
  centralizado padrão de `80rem`.
49
50
  9. Manter margem e posicionamento no consumidor; componente reutilizável controla apenas
@@ -5,18 +5,29 @@
5
5
  operação, não a uma tela isolada.
6
6
  - URL representa estado que precisa sobreviver a refresh, deep link ou histórico.
7
7
  - `Page` fornece o `<main>` e o container centralizado com teto padrão de `80rem`. Sua forma
8
- explícita é `Page > PageHeader (PageTitle, PageDescription, PageMeta, PageActions) + PageBody`;
9
- `title`, `description`, `meta` e `actions` no próprio `Page` são a abreviação para o caso direto.
8
+ explícita é `Page > PageHeader (PageBack? | PageNavigation?, PageTitle, PageDescription?,
9
+ PageActions?) + PageBody`; `title`, `description` e `actions` no próprio `Page` são a abreviação
10
+ para o caso direto.
10
11
  Não misturar as duas formas. Alterar `className` apenas quando a superfície tiver uma necessidade
11
12
  real de largura; não reconstruir esse container em cada rota.
13
+ - `PageHeader` é a única região de cabeçalho da página. O default acompanha o container;
14
+ `variant="bar"` apresenta a mesma anatomia como faixa compacta no topo. Em uma subpágina simples,
15
+ `PageBack` recebe o destino pai explícito: aparece acima do título no default e como icon-only com
16
+ tooltip na barra. Para mais de um ancestral relevante, use `Breadcrumb` dentro de
17
+ `PageNavigation`. Não combine retorno e breadcrumb nem crie um chrome paralelo para uma `Page`.
18
+ Ações com texto na barra usam `Button size="sm"`; ações somente com ícone usam `icon-sm`.
19
+ `PageActionsTarget` fica reservado a workspaces imersivos que já possuam chrome próprio.
12
20
  - `PageState` substitui todo o conteúdo principal quando a página carrega, falha ou está vazia. Na
13
21
  forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
14
- explícita, fica dentro de `PageBody`. O cabeçalho permanece visível. Estados de seção ou coleção
15
- continuam em `DataState`, `ActionView`, `ActionList` ou `Alert`; não elevar uma falha parcial a
16
- estado da página.
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.
17
28
  - `Content` delimita uma seção e segue a mesma anatomia: `Content > ContentHeader (ContentTitle,
18
29
  ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader` nunca fica
19
- solto. `title`, `description`, `meta` e `actions` no `Content` são a abreviação para o caso
30
+ solto. `title`, `description`, `count` e `actions` no `Content` são a abreviação para o caso
20
31
  direto e não podem ser misturados ao header explícito. `level` preserva a hierarquia semântica
21
32
  do heading.
22
33
  - Card, Drawer e Pane nomeiam a região principal como `CardBody`, `DrawerBody` e `PaneBody`.
@@ -1,5 +1,5 @@
1
1
  import type { z } from 'zod'
2
- import { Badge, Card, useListAction } from '@softize/opus/ui/react'
2
+ import { Badge, Card, DataState, useListAction } from '@softize/opus/ui/react'
3
3
  import { Task, taskList } from './domains/tasks/actions/list.ts'
4
4
 
5
5
  type TaskItem = z.infer<typeof Task>
@@ -10,7 +10,7 @@ export function App(): React.ReactElement {
10
10
  // O front consome o CONTRATO via /api — o plugin opusDesign (vite.config.ts)
11
11
  // serve o backend no próprio dev server: dev normal executa o handler real;
12
12
  // `pnpm dev:design` responde com o mockHandler, isolado de qualquer backend.
13
- const { items, isLoading } = useListAction<TaskItem>(taskList)
13
+ const { items, isLoading, error, refetch } = useListAction<TaskItem>(taskList)
14
14
 
15
15
  return (
16
16
  <main className="flex min-h-full items-center justify-center bg-background p-6">
@@ -19,9 +19,14 @@ export function App(): React.ReactElement {
19
19
  <p className="mb-4 text-sm text-muted-foreground">
20
20
  Esqueleto criado pelo opus create. A spec vive nas declarações do domínio.
21
21
  </p>
22
- {isLoading ? (
23
- <p className="text-sm text-muted-foreground">Carregando…</p>
24
- ) : (
22
+ {/* Carregando, erro e vazio saem do DataState; a lista só cuida dos itens. */}
23
+ <DataState
24
+ loading={isLoading}
25
+ error={error ?? null}
26
+ empty={items.length === 0}
27
+ emptyMessage="Nenhuma tarefa ainda."
28
+ onRetry={() => refetch()}
29
+ >
25
30
  <ul className="space-y-2">
26
31
  {items.map((t) => (
27
32
  <li key={t.id} className="flex items-center justify-between gap-2 text-sm">
@@ -30,7 +35,7 @@ export function App(): React.ReactElement {
30
35
  </li>
31
36
  ))}
32
37
  </ul>
33
- )}
38
+ </DataState>
34
39
  </Card>
35
40
  </main>
36
41
  )
package/src/core/types.ts CHANGED
@@ -551,9 +551,8 @@ export type FieldWidget = BuiltInFieldWidget | (string & { readonly __opusCustom
551
551
  export interface FieldSpec {
552
552
  label: I18nRef
553
553
  placeholder?: I18nRef
554
- hint?: I18nRef
555
- /** Ajuda no hover/foco da label (ícone + tooltip). `hint` = texto auxiliar SOB o campo;
556
- * `help` = explicação mais longa, escondida atrás do ícone na label. */
554
+ /** Ajuda junto à label (ícone ⓘ + tooltip): critério, efeito ou limitação que a label não
555
+ * diz. Ajuda que repete a label deve ser omitida. */
557
556
  help?: I18nRef
558
557
 
559
558
  default?: unknown
@@ -629,7 +628,7 @@ export interface ListColumnSpec {
629
628
  * 'badge' (chip `outline` com o valor — legado; coluna de dicionário com `presentation`
630
629
  * declarado usa `DictionaryValue` e dispensa este tipo). */
631
630
  type?: 'text' | 'number' | 'date' | 'badge'
632
- /** Referência do dicionário registrado em `TbdlibProvider dicts` que esta coluna mostra,
631
+ /** Referência do dicionário registrado em `OpusProvider dicts` que esta coluna mostra,
633
632
  * quando o schema de saída não carrega a meta de `t.dict` (ex.: campo `z.string()`).
634
633
  * Coluna cujo campo de saída É um `t.dict().zod()` resolve sozinha, sem esta chave. */
635
634
  dictionary?: string
@@ -37,7 +37,10 @@ export interface ActionListDialogProps<TItem, TInput extends Record<string, unkn
37
37
  note?: ReactNode
38
38
  /** Ação à direita da linha — em geral o botão de criar. */
39
39
  actions?: ReactNode
40
- emptyText?: string
40
+ /** Textos dos estados, repassados ao ActionList (e dele ao DataState). */
41
+ emptyMessage?: string
42
+ errorMessage?: string
43
+ retryLabel?: string
41
44
  /** Sobrepõe o vazio derivado (items.length === 0) — ex.: form inline aberto. */
42
45
  empty?: (items: TItem[]) => boolean
43
46
  /** Carga EXTRA agregada à do fetch (ex.: a query irmã que os children precisam). */
@@ -57,7 +60,9 @@ export function ActionListDialog<TItem, TInput extends Record<string, unknown> =
57
60
  description,
58
61
  note,
59
62
  actions,
60
- emptyText,
63
+ emptyMessage,
64
+ errorMessage,
65
+ retryLabel,
61
66
  empty,
62
67
  loading,
63
68
  className,
@@ -80,7 +85,9 @@ export function ActionListDialog<TItem, TInput extends Record<string, unknown> =
80
85
  <ActionList<TInput, TItem>
81
86
  action={action as unknown as ListAction<TInput, TItem>}
82
87
  input={(input ?? {}) as TInput}
83
- {...(emptyText !== undefined ? { emptyMessage: emptyText } : {})}
88
+ {...(emptyMessage !== undefined ? { emptyMessage } : {})}
89
+ {...(errorMessage !== undefined ? { errorMessage } : {})}
90
+ {...(retryLabel !== undefined ? { retryLabel } : {})}
84
91
  {...(empty !== undefined ? { empty } : {})}
85
92
  {...(loading !== undefined ? { loading } : {})}
86
93
  >
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Namespace `dialog` de propósito: `window.alert/confirm/prompt` são globais do browser, e
14
14
  * um import esquecido cairia no nativo (cinza, sem tema) sem erro. `window.dialog` não
15
- * existe — a família fica imune ao footgun. `confirm()` segue exportado como alias.
15
+ * existe — a família fica imune ao footgun.
16
16
  *
17
17
  * Por baixo é o <Dialog mode="alert">: `role="alertdialog"`, não fecha no clique fora, foco preso
18
18
  * — a semântica que uma interrupção que EXIGE resposta pede. ESC/Cancelar resolve o "não"
@@ -59,16 +59,12 @@ export interface ConfirmOptions extends DialogBase {
59
59
  cancel?: string
60
60
  /** Contexto do botão de confirmação. */
61
61
  context?: Extract<UiContext, 'primary' | 'danger'>
62
- /** @deprecated Use `context="danger"`. */
63
- variant?: 'default' | 'destructive'
64
62
  }
65
63
 
66
64
  /** `prompt` — uma string livre. Form de verdade (campos, contrato) → ActionFormDialog. */
67
65
  export interface PromptOptions extends DialogBase {
68
66
  cancel?: string
69
67
  context?: Extract<UiContext, 'primary' | 'danger'>
70
- /** @deprecated Use `context="danger"`. */
71
- variant?: 'default' | 'destructive'
72
68
  placeholder?: string
73
69
  defaultValue?: string
74
70
  }
@@ -107,16 +103,6 @@ function requireHost(fn: string): Error | null {
107
103
  )
108
104
  }
109
105
 
110
- function rejectAmbiguousContext(
111
- fn: string,
112
- options: ConfirmOptions | PromptOptions,
113
- ): Error | null {
114
- if (options.context === undefined || options.variant === undefined) return null
115
- return new Error(
116
- `${fn} não permite combinar context com uma variante semântica legada.`,
117
- )
118
- }
119
-
120
106
  function validateChoose<TResult extends string>(options: ChooseOptions<TResult>): Error | null {
121
107
  if (options.actions.length === 0) {
122
108
  return new Error('dialog.choose() exige pelo menos uma ação.')
@@ -145,8 +131,6 @@ export const dialog = {
145
131
  },
146
132
  /** Pergunta sim/não. `Promise<boolean>` — fechar/ESC resolve `false`. */
147
133
  confirm(options: ConfirmOptions): Promise<boolean> {
148
- const ambiguous = rejectAmbiguousContext('dialog.confirm()', options)
149
- if (ambiguous !== null) return Promise.reject(ambiguous)
150
134
  const err = requireHost('dialog.confirm()')
151
135
  if (err !== null) return Promise.reject(err)
152
136
  return new Promise<boolean>((resolve) => {
@@ -155,8 +139,6 @@ export const dialog = {
155
139
  },
156
140
  /** Pede uma string. `Promise<string | null>` — fechar/ESC/Cancelar resolve `null`. */
157
141
  prompt(options: PromptOptions): Promise<string | null> {
158
- const ambiguous = rejectAmbiguousContext('dialog.prompt()', options)
159
- if (ambiguous !== null) return Promise.reject(ambiguous)
160
142
  const err = requireHost('dialog.prompt()')
161
143
  if (err !== null) return Promise.reject(err)
162
144
  return new Promise<string | null>((resolve) => {
@@ -180,11 +162,6 @@ export const dialog = {
180
162
  },
181
163
  }
182
164
 
183
- /** @deprecated Alias retrocompatível — use `dialog.confirm()`. */
184
- export function confirm(options: ConfirmOptions): Promise<boolean> {
185
- return dialog.confirm(options)
186
- }
187
-
188
165
  /**
189
166
  * O host — um por app, no shell. Enfileira: uma por vez (duas caixas empilhadas seriam
190
167
  * ambíguas sobre qual clique respondeu o quê).
@@ -228,11 +205,8 @@ export function DialogHost(): React.ReactElement {
228
205
  else p.resolve(value)
229
206
  }
230
207
 
231
- const variant = current !== undefined && (current.kind === 'confirm' || current.kind === 'prompt')
232
- ? current.variant
233
- : undefined
234
208
  const context = current !== undefined && (current.kind === 'confirm' || current.kind === 'prompt')
235
- ? current.context ?? (variant === 'destructive' ? 'danger' : 'primary')
209
+ ? current.context ?? 'primary'
236
210
  : 'primary'
237
211
  const defaultAction = current?.kind === 'alert' ? 'OK' : 'Confirmar'
238
212
 
@@ -344,6 +318,3 @@ export function DialogHost(): React.ReactElement {
344
318
  </Dialog>
345
319
  )
346
320
  }
347
-
348
- /** @deprecated Alias retrocompatível — monte `<DialogHost />`. */
349
- export const ConfirmHost = DialogHost