@softize/opus 14.0.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 +63 -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 +3 -0
- 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/data-layer.md +9 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
- package/src/ui/components/patterns/list.tsx +41 -26
- package/src/ui/components/patterns/page-state.tsx +48 -6
- package/src/ui/components/patterns/page.tsx +221 -55
- package/src/ui/components/patterns/state-surface.tsx +137 -23
- package/src/ui/components/patterns/surface-header.tsx +102 -17
- package/src/ui/components/patterns/trigger.tsx +7 -6
- package/src/ui/components/primitives/button-group.tsx +34 -8
- package/src/ui/components/primitives/control.ts +12 -3
- package/src/ui/docs/content/action-list-dialog.md +1 -1
- package/src/ui/docs/content/action-list.md +34 -3
- package/src/ui/docs/content/action-trigger.md +4 -3
- package/src/ui/docs/content/alert.md +16 -4
- package/src/ui/docs/content/button.md +20 -5
- package/src/ui/docs/content/content.md +3 -3
- package/src/ui/docs/content/data-state.md +4 -4
- package/src/ui/docs/content/page.md +159 -44
- package/src/ui/meta.ts +6 -6
- package/src/ui/react.tsx +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,69 @@ 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.0 — 2026-09-09
|
|
11
|
+
|
|
12
|
+
`PageHeader` passa a oferecer `variant="bar"`, uma apresentação compacta da mesma região de título,
|
|
13
|
+
descrição e ações. O novo `PageBack` recebe o destino pai explícito: no header padrão fica acima do
|
|
14
|
+
título com rótulo; na barra vira icon-only com tooltip e nome acessível. Páginas comuns deixam de
|
|
15
|
+
precisar de um chrome paralelo, enquanto `PageActionsTarget` permanece para workspaces imersivos que
|
|
16
|
+
já tenham uma faixa própria. Para hierarquias com mais de um ancestral, `PageNavigation` recebe o
|
|
17
|
+
`Breadcrumb` na mesma posição introdutória e não pode ser combinado com `PageBack`. Ações em barra
|
|
18
|
+
usam a escala compacta: `sm` com texto e `icon-sm` quando exibem somente um ícone.
|
|
19
|
+
|
|
20
|
+
`PageState` passa a centralizar carregamento, falha e vazio sem moldura e a ocupar a altura
|
|
21
|
+
disponível. `Page` reconhece o estado mesmo quando um componente intermediário o renderiza, oculta
|
|
22
|
+
o cabeçalho inteiro nos três estados e o restaura em `ready`. A falha mantém `role="alert"`, deixa
|
|
23
|
+
de usar o visual de `Alert` e apresenta a recuperação como botão `outline` textual. A moldura tracejada continua disponível no `Empty` usado diretamente para
|
|
24
|
+
representar uma região disponível à criação ou vínculo.
|
|
25
|
+
|
|
26
|
+
O CLI carrega o `.env` ao lado do `opus.config.ts` antes de importar o config, em `db`, `seed` e
|
|
27
|
+
`gen`. O config do consumer costuma abrir o banco no topo do módulo, lendo `process.env` na hora
|
|
28
|
+
do import: sem isso, um projeto que guarda a URL no `.env` migrava o banco DEFAULT do config
|
|
29
|
+
enquanto o runtime usava outro, e o schema aplicado sumia do banco que a aplicação abre. Variável
|
|
30
|
+
já definida no ambiente vence o arquivo (a semântica do `--env-file` do Node), então
|
|
31
|
+
`DATABASE_URL=... opus db migrate` e a injeção do container seguem mandando; ausência de `.env`
|
|
32
|
+
é normal e não é erro. Um projeto que já carregava o `.env` por conta própria no `opus.config.ts`
|
|
33
|
+
continua correto: as duas cargas são idempotentes e a primeira vence.
|
|
34
|
+
|
|
35
|
+
**Breaking — API removida.** `Page` deixa de aceitar `count` e o barrel deixa de exportar
|
|
36
|
+
`PageMeta` (ADR 0009). O cabeçalho da página reconhece somente `PageTitle`, `PageDescription`,
|
|
37
|
+
`PageActions` e a região introdutória; um total vai para o conteúdo que o explica — a listagem, a
|
|
38
|
+
métrica ou uma seção com `Content`, onde `ContentMeta` e `Content.count` continuam disponíveis.
|
|
39
|
+
`PageBack` passa a exigir `href`: sem destino, o elemento não era tabulável nem tinha nome
|
|
40
|
+
acessível. `PageNavigation` e `PageBack` não podem ser combinados na mesma página: escolha o
|
|
41
|
+
retorno de um ancestral só ou a trilha completa.
|
|
42
|
+
|
|
43
|
+
**Breaking — comportamento e DOM.** `Page` oculta o `PageHeader` inteiro enquanto o `PageState`
|
|
44
|
+
estiver em carregamento, erro ou vazio, sem opt-out. Teste que procura o título da página durante a
|
|
45
|
+
carga passa a falhar; o nível semântico segue disponível no `role="heading"` da própria superfície
|
|
46
|
+
de estado. Some junto a região introdutória, então uma subpágina em erro fica sem o `PageBack` para
|
|
47
|
+
o pai; quem depende desse retorno oferece a saída pelo `action` do próprio `PageState`. O motivo, a
|
|
48
|
+
alternativa descartada e esse custo estão no adendo da ADR 0004. O erro e o vazio do `PageState`
|
|
49
|
+
trocam de anatomia: sai `[data-slot="alert"]` e `[data-slot="alert-actions"]`, entram `error-state`
|
|
50
|
+
e `state-actions`, e o vazio passa de `data-frame="region"` para `bare`. As ações de linha do
|
|
51
|
+
`ActionList` passam a vir dentro de um `ButtonGroup`, o que acrescenta um `role="group"` por linha;
|
|
52
|
+
as ações da barra (Filtros, Recarregar, Exibição) passaram de `outline` para `ghost`, e o que o
|
|
53
|
+
consumidor entrega em `ActionFilterBar.actions` agora fica depois do grupo de controles auxiliares,
|
|
54
|
+
no fim da barra. A recuperação do aviso inline de erro virou ação só de ícone, com o rótulo no nome
|
|
55
|
+
acessível e no tooltip em vez do texto na tela. Quem afirma texto, variante ou slot nesses pontos
|
|
56
|
+
precisa revisar as asserções.
|
|
57
|
+
|
|
58
|
+
**Breaking — ícones voltam à escala.** `controlGlyph` passa a devolver seletores literais. Montada
|
|
59
|
+
em runtime, a classe saía certa no DOM, mas o scanner do Tailwind não a via e o CSS nunca era
|
|
60
|
+
gerado: na prática, todo ícone de `Button` sem `size-*` próprio vinha no tamanho padrão do lucide.
|
|
61
|
+
Agora todos passam a medir o degrau da escala, então ícones encolhem visivelmente no consumidor. O
|
|
62
|
+
`ActionTrigger` só de ícone usa `icon-xs` (1.5rem) por padrão, o degrau da ação que mora dentro de
|
|
63
|
+
uma linha densa, e passa a respeitar um `size` declarado quando a composição pede mais presença —
|
|
64
|
+
`icon-sm` na barra, por exemplo. Atenção ao inverso: um `size` que antes era ignorado no modo
|
|
65
|
+
ícone passa a valer, então `size="lg"` esquecido ao lado de `icon` agora produz um botão de altura
|
|
66
|
+
de texto com um ícone só.
|
|
67
|
+
|
|
68
|
+
Do lado aditivo, `ButtonGroup` ganha `mode`: `connected`, o comportamento de sempre com as bordas
|
|
69
|
+
coladas, e `spaced`, que só agrupa e espaça — é o que as ações de linha e os controles auxiliares
|
|
70
|
+
da barra do `ActionList` passam a usar. `ActionList` ganha `toolbarActions` para o que o consumidor
|
|
71
|
+
acrescenta no fim da barra de filtros.
|
|
72
|
+
|
|
10
73
|
## 14.0.0 — 2026-09-09
|
|
11
74
|
|
|
12
75
|
Estados vazio, carregando e erro passam a uma composição só. `DataState` compõe `Empty` (moldura
|
package/bin/cli.mjs
CHANGED
|
@@ -328,6 +328,8 @@ Flags
|
|
|
328
328
|
--json (introspect, seed) Saída estruturada em JSON.
|
|
329
329
|
--monorepo (create) Cria a raiz do workspace em vez de um app.
|
|
330
330
|
--config <path> (gen, db, seed) Caminho do opus.config.ts. Default: ./opus.config.ts.
|
|
331
|
+
O .env ao lado desse arquivo é carregado antes do config; variável
|
|
332
|
+
já definida no ambiente vence o arquivo.
|
|
331
333
|
--output <path> (gen) Pasta de saída. Default: a do config, ou ./.gen.
|
|
332
334
|
--profile <nome> (seed) Perfil do dataset. Default: o defaultProfile do seed.
|
|
333
335
|
--scope <nome> (seed) Escopo explícito dos dados. Alternativa: OPUS_SEED_SCOPE.
|
package/bin/lib/check.mjs
CHANGED
|
@@ -107,8 +107,9 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
107
107
|
["PageHeader", new Set(["Page"])],
|
|
108
108
|
["PageBody", new Set(["Page"])],
|
|
109
109
|
["PageTitle", new Set(["PageHeader"])],
|
|
110
|
+
["PageBack", new Set(["PageHeader"])],
|
|
111
|
+
["PageNavigation", new Set(["PageHeader"])],
|
|
110
112
|
["PageDescription", new Set(["PageHeader"])],
|
|
111
|
-
["PageMeta", new Set(["PageHeader"])],
|
|
112
113
|
["PageActions", new Set(["PageHeader"])],
|
|
113
114
|
["ContentHeader", new Set(["Content"])],
|
|
114
115
|
["ContentBody", new Set(["Content"])],
|
|
@@ -166,7 +167,11 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
166
167
|
{
|
|
167
168
|
header: "PageHeader",
|
|
168
169
|
body: "PageBody",
|
|
169
|
-
shorthand: new Set(["title", "description", "
|
|
170
|
+
shorthand: new Set(["title", "description", "actions"]),
|
|
171
|
+
// O contador saiu do título da página (ADR 0009). Tirar `count` do shorthand não proíbe
|
|
172
|
+
// nada sozinho — só apaga o marcador que fazia o gate enxergar aquele Page —, então a
|
|
173
|
+
// prop removida é cobrada aqui, nas duas formas de composição.
|
|
174
|
+
removed: new Map([["count", "o total pertence ao conteúdo que o explica"]]),
|
|
170
175
|
},
|
|
171
176
|
],
|
|
172
177
|
[
|
|
@@ -174,7 +179,8 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
174
179
|
{
|
|
175
180
|
header: "ContentHeader",
|
|
176
181
|
body: "ContentBody",
|
|
177
|
-
shorthand: new Set(["title", "description", "
|
|
182
|
+
shorthand: new Set(["title", "description", "count", "actions"]),
|
|
183
|
+
removed: new Map([["meta", "use `count`"]]),
|
|
178
184
|
},
|
|
179
185
|
],
|
|
180
186
|
]);
|
|
@@ -183,8 +189,9 @@ const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
|
183
189
|
"PageHeader",
|
|
184
190
|
"PageBody",
|
|
185
191
|
"PageTitle",
|
|
192
|
+
"PageBack",
|
|
193
|
+
"PageNavigation",
|
|
186
194
|
"PageDescription",
|
|
187
|
-
"PageMeta",
|
|
188
195
|
"PageActions",
|
|
189
196
|
"ContentHeader",
|
|
190
197
|
"ContentBody",
|
|
@@ -202,7 +209,13 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
202
209
|
"PageHeader",
|
|
203
210
|
{
|
|
204
211
|
title: "PageTitle",
|
|
205
|
-
optional: new Set([
|
|
212
|
+
optional: new Set([
|
|
213
|
+
"PageBack",
|
|
214
|
+
"PageNavigation",
|
|
215
|
+
"PageDescription",
|
|
216
|
+
"PageActions",
|
|
217
|
+
]),
|
|
218
|
+
exclusive: [new Set(["PageBack", "PageNavigation"])],
|
|
206
219
|
shorthand: new Set(),
|
|
207
220
|
},
|
|
208
221
|
],
|
|
@@ -898,6 +911,10 @@ export function checkUiStructure(file, text) {
|
|
|
898
911
|
(name) =>
|
|
899
912
|
children.names.filter((child) => child === name).length > 1,
|
|
900
913
|
);
|
|
914
|
+
const combinedExclusive = (header.exclusive ?? []).some(
|
|
915
|
+
(group) =>
|
|
916
|
+
children.names.filter((name) => group.has(name)).length > 1,
|
|
917
|
+
);
|
|
901
918
|
const titleValid =
|
|
902
919
|
header.titleRequired === false ? titles <= 1 : titles === 1;
|
|
903
920
|
const recognizedContent =
|
|
@@ -910,6 +927,7 @@ export function checkUiStructure(file, text) {
|
|
|
910
927
|
!(header.allowOpaque && children.hasOpaque)) ||
|
|
911
928
|
unexpected ||
|
|
912
929
|
duplicatedOptional ||
|
|
930
|
+
combinedExclusive ||
|
|
913
931
|
(children.hasOpaque && !header.allowOpaque)
|
|
914
932
|
) {
|
|
915
933
|
add(
|
|
@@ -932,6 +950,16 @@ export function checkUiStructure(file, text) {
|
|
|
932
950
|
: [],
|
|
933
951
|
),
|
|
934
952
|
);
|
|
953
|
+
for (const [attribute, hint] of root.removed ?? []) {
|
|
954
|
+
if (attributes.has(attribute)) {
|
|
955
|
+
add(
|
|
956
|
+
node,
|
|
957
|
+
component,
|
|
958
|
+
`${component} não aceita mais a propriedade \`${attribute}\`: ${hint}.`,
|
|
959
|
+
"ui-structure-removed-prop",
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
935
963
|
const shorthand = [...root.shorthand].some((attribute) =>
|
|
936
964
|
attributes.has(attribute),
|
|
937
965
|
);
|
package/bin/lib/cli-shared.mjs
CHANGED
|
@@ -55,10 +55,39 @@ export async function fileExists(file) {
|
|
|
55
55
|
// Config do consumer
|
|
56
56
|
// =============================================================================
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Carrega o `.env` ao lado do `opus.config.ts` no processo do CLI, para que ele chegue
|
|
60
|
+
* aos runners (que herdam o ambiente) ANTES de o config do consumer ser importado.
|
|
61
|
+
*
|
|
62
|
+
* O config costuma abrir a conexão do banco no topo do módulo, lendo `process.env` na
|
|
63
|
+
* hora do import: sem isto, `db` e `seed` caem na conexão default e operam um banco
|
|
64
|
+
* diferente do que o runtime do projeto usa. Ausência do arquivo é normal (container e
|
|
65
|
+
* CI injetam por ambiente) e não é erro; qualquer outra falha de leitura sobe.
|
|
66
|
+
*
|
|
67
|
+
* Variável já definida no ambiente VENCE o arquivo (semântica do `--env-file` do Node),
|
|
68
|
+
* então `DATABASE_URL=... opus db migrate` e a injeção do container continuam mandando.
|
|
69
|
+
*
|
|
70
|
+
* Devolve o caminho carregado, ou `null` quando não havia `.env`.
|
|
71
|
+
*/
|
|
72
|
+
export function loadProjectEnv(configPath) {
|
|
73
|
+
const envFile = path.join(path.dirname(configPath), '.env')
|
|
74
|
+
try {
|
|
75
|
+
process.loadEnvFile(envFile)
|
|
76
|
+
return envFile
|
|
77
|
+
} catch (cause) {
|
|
78
|
+
if (cause.code === 'ENOENT') return null
|
|
79
|
+
throw cause
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
58
83
|
/**
|
|
59
84
|
* Resolve o `opus.config.ts` a partir de `flags.config` (default: `./opus.config.ts`),
|
|
60
85
|
* contido no projeto. Lança `Error` com uma mensagem única quando o arquivo não existe;
|
|
61
86
|
* cada comando decide o canal (texto ou JSON) e o código de saída.
|
|
87
|
+
*
|
|
88
|
+
* Resolver o config é também o momento em que o projeto passa a ser conhecido, então o
|
|
89
|
+
* `.env` dele é carregado aqui (ver `loadProjectEnv`) — em um lugar só, para que nenhum
|
|
90
|
+
* comando que importe o config do consumer possa esquecer.
|
|
62
91
|
*/
|
|
63
92
|
export function resolveConfig(flags, cwd = process.cwd()) {
|
|
64
93
|
const root = canonicalProjectDirectory(cwd)
|
|
@@ -69,7 +98,7 @@ export function resolveConfig(flags, cwd = process.cwd()) {
|
|
|
69
98
|
'Passe o caminho com --config <path> ou crie o arquivo na raiz do projeto.',
|
|
70
99
|
)
|
|
71
100
|
}
|
|
72
|
-
return { cwd: root, configPath: config.path }
|
|
101
|
+
return { cwd: root, configPath: config.path, envFile: loadProjectEnv(config.path) }
|
|
73
102
|
}
|
|
74
103
|
|
|
75
104
|
// =============================================================================
|
package/bin/lib/copy.mjs
CHANGED
|
@@ -103,6 +103,9 @@ const JSX_CHILD_ROLES = new Map([
|
|
|
103
103
|
['PopoverDescription', 'description'],
|
|
104
104
|
['PopoverTitle', 'title'],
|
|
105
105
|
['PageDescription', 'description'],
|
|
106
|
+
// O texto do PageBack é o NOME do destino ('Clientes'), como um item de trilha — não um
|
|
107
|
+
// comando. Classificado como `button`, a política universal cobraria verbo de ação.
|
|
108
|
+
['PageBack', 'breadcrumb'],
|
|
106
109
|
['PageTitle', 'title'],
|
|
107
110
|
['TableCaption', 'description'],
|
|
108
111
|
['TableHead', 'heading'],
|
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/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
|
@@ -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`.
|