@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +63 -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 +3 -0
  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/data-layer.md +9 -0
  12. package/package.json +1 -1
  13. package/registry/skills/build-opus-ui/SKILL.md +3 -2
  14. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
  15. package/src/ui/components/patterns/list.tsx +41 -26
  16. package/src/ui/components/patterns/page-state.tsx +48 -6
  17. package/src/ui/components/patterns/page.tsx +221 -55
  18. package/src/ui/components/patterns/state-surface.tsx +137 -23
  19. package/src/ui/components/patterns/surface-header.tsx +102 -17
  20. package/src/ui/components/patterns/trigger.tsx +7 -6
  21. package/src/ui/components/primitives/button-group.tsx +34 -8
  22. package/src/ui/components/primitives/control.ts +12 -3
  23. package/src/ui/docs/content/action-list-dialog.md +1 -1
  24. package/src/ui/docs/content/action-list.md +34 -3
  25. package/src/ui/docs/content/action-trigger.md +4 -3
  26. package/src/ui/docs/content/alert.md +16 -4
  27. package/src/ui/docs/content/button.md +20 -5
  28. package/src/ui/docs/content/content.md +3 -3
  29. package/src/ui/docs/content/data-state.md +4 -4
  30. package/src/ui/docs/content/page.md +159 -44
  31. package/src/ui/meta.ts +6 -6
  32. 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", "count", "actions"]),
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", "meta", "actions"]),
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(["PageDescription", "PageMeta", "PageActions"]),
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
  );
@@ -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
- - 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`.
@@ -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": "14.0.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",
@@ -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`.