@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.
- package/CHANGELOG.md +97 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +33 -5
- package/bin/lib/cli-shared.mjs +30 -1
- package/bin/lib/copy.mjs +279 -6
- package/bin/lib/db.mjs +2 -0
- package/docs/adr/0004-page-content-state-is-composed.md +39 -5
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +9 -3
- package/docs/adr/0009-page-title-does-not-carry-a-counter.md +57 -0
- package/docs/adr/0010-page-header-owns-page-chrome.md +73 -0
- package/docs/code-style.md +4 -1
- package/docs/data-layer.md +9 -0
- package/package.json +1 -1
- package/registry/instructions/opus.md +3 -3
- package/registry/skills/build-opus-ui/SKILL.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
- package/registry/templates/app/src/App.tsx +11 -6
- package/src/core/types.ts +3 -4
- package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
- package/src/ui/components/patterns/confirm.tsx +2 -31
- package/src/ui/components/patterns/content-header.tsx +42 -141
- package/src/ui/components/patterns/data-state.tsx +42 -68
- package/src/ui/components/patterns/form.tsx +15 -15
- package/src/ui/components/patterns/list.tsx +55 -34
- package/src/ui/components/patterns/page-state.tsx +81 -51
- package/src/ui/components/patterns/page.tsx +228 -97
- package/src/ui/components/patterns/state-surface.tsx +262 -0
- package/src/ui/components/patterns/surface-header.tsx +204 -0
- package/src/ui/components/patterns/trigger.tsx +9 -10
- package/src/ui/components/patterns/view.tsx +14 -16
- package/src/ui/components/primitives/alert.tsx +1 -33
- package/src/ui/components/primitives/avatar.tsx +15 -5
- package/src/ui/components/primitives/badge.tsx +2 -43
- package/src/ui/components/primitives/button-group.tsx +34 -8
- package/src/ui/components/primitives/button.tsx +31 -35
- package/src/ui/components/primitives/control.ts +69 -0
- package/src/ui/components/primitives/dot.tsx +1 -30
- package/src/ui/components/primitives/input-group.tsx +11 -8
- package/src/ui/components/primitives/item.tsx +3 -1
- package/src/ui/components/primitives/menu.tsx +1 -7
- package/src/ui/components/primitives/pagination.tsx +16 -8
- package/src/ui/components/primitives/select.tsx +2 -2
- package/src/ui/components/primitives/spinner.tsx +13 -16
- package/src/ui/components/primitives/switch.tsx +4 -1
- package/src/ui/components/primitives/tabs.tsx +5 -3
- package/src/ui/components/primitives/toggle.tsx +9 -4
- package/src/ui/docs/content/action-form.md +26 -0
- package/src/ui/docs/content/action-list-dialog.md +2 -2
- package/src/ui/docs/content/action-list.md +35 -2
- package/src/ui/docs/content/action-trigger.md +5 -4
- package/src/ui/docs/content/action-view.md +3 -2
- package/src/ui/docs/content/alert.md +16 -4
- package/src/ui/docs/content/avatar.md +7 -3
- package/src/ui/docs/content/button.md +48 -17
- package/src/ui/docs/content/communication.md +36 -0
- package/src/ui/docs/content/content.md +5 -4
- package/src/ui/docs/content/data-state.md +17 -13
- package/src/ui/docs/content/dialog.md +1 -4
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/item.md +1 -1
- package/src/ui/docs/content/page.md +160 -37
- package/src/ui/docs/content/pagination.md +11 -9
- package/src/ui/docs/content/semantic-context.md +3 -2
- package/src/ui/docs/content/sidebar.md +2 -42
- package/src/ui/docs/content/spinner.md +9 -6
- package/src/ui/docs/content/switch.md +1 -1
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/toggle.md +1 -1
- package/src/ui/drivers/react.tsx +1 -6
- package/src/ui/meta.ts +8 -8
- package/src/ui/react.tsx +16 -18
- 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
|
-
-
|
|
25
|
-
-
|
|
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
|
|
32
|
-
uma região disponível
|
|
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`, `
|
|
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
|
|
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`.
|
package/docs/code-style.md
CHANGED
|
@@ -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`, `
|
|
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
|
|
package/docs/data-layer.md
CHANGED
|
@@ -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
|
@@ -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`
|
|
21
|
-
semânticas antigas
|
|
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`
|
|
46
|
-
`
|
|
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
|
|
9
|
-
`title`, `description
|
|
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`.
|
|
15
|
-
|
|
16
|
-
estado
|
|
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`, `
|
|
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
|
-
{
|
|
23
|
-
|
|
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
|
-
|
|
555
|
-
|
|
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 só 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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{...(
|
|
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.
|
|
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 ??
|
|
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
|