@softize/opus 21.0.1 → 22.1.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 (62) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/bin/cli.mjs +9 -1
  3. package/bin/lib/check.mjs +37 -29
  4. package/docs/adr/0004-page-content-state-is-composed.md +3 -2
  5. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +3 -0
  6. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +23 -14
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -0
  8. package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +7 -4
  9. package/docs/adr/0016-list-collection-header-belongs-to-content.md +13 -8
  10. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +27 -26
  11. package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +1 -0
  12. package/docs/adr/0029-page-header-is-the-single-page-introduction.md +71 -0
  13. package/docs/adr/0030-framed-content-can-use-sectioned-anatomy.md +89 -0
  14. package/docs/adr/0031-detail-dividers-own-the-tabular-label-treatment.md +61 -0
  15. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +59 -0
  16. package/docs/adr/0033-drawer-uses-an-inset-structural-frame.md +72 -0
  17. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +60 -0
  18. package/docs/adr/0035-button-group-separates-frame-relationship-and-distribution.md +63 -0
  19. package/docs/adr/0036-tabs-declares-trigger-distribution.md +58 -0
  20. package/docs/adr/0037-design-data-lives-outside-the-contract.md +117 -0
  21. package/docs/radius-scale.md +5 -2
  22. package/package.json +5 -1
  23. package/registry/skills/build-opus-ui/SKILL.md +9 -6
  24. package/registry/skills/build-opus-ui/references/evaluations.md +5 -2
  25. package/registry/skills/build-opus-ui/references/ui-patterns.md +14 -16
  26. package/registry/skills/create-opus-action/references/contract-and-binding.md +5 -3
  27. package/registry/skills/create-opus-action/scripts/scaffold.mjs +0 -4
  28. package/registry/skills/test-opus-action/SKILL.md +2 -2
  29. package/registry/skills/test-opus-action/references/harness.md +2 -2
  30. package/registry/templates/app/src/App.tsx +1 -1
  31. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +2 -19
  32. package/registry/templates/app/src/domains/tasks/actions/list.ts +3 -5
  33. package/registry/templates/app/vite.config.ts +3 -2
  34. package/src/core/domain.ts +2 -2
  35. package/src/core/types.ts +6 -20
  36. package/src/design/index.ts +179 -0
  37. package/src/testing/fake.ts +2 -2
  38. package/src/testing/index.ts +7 -3
  39. package/src/ui/components/patterns/content-header.tsx +51 -13
  40. package/src/ui/components/patterns/list.tsx +51 -33
  41. package/src/ui/components/patterns/page.tsx +139 -251
  42. package/src/ui/components/patterns/presentation.tsx +2 -6
  43. package/src/ui/components/primitives/button-group.tsx +80 -8
  44. package/src/ui/components/primitives/button.tsx +85 -60
  45. package/src/ui/components/primitives/detail.tsx +63 -57
  46. package/src/ui/components/primitives/dialog.tsx +8 -11
  47. package/src/ui/components/primitives/drawer.tsx +16 -12
  48. package/src/ui/components/primitives/tabs.tsx +123 -54
  49. package/src/ui/docs/content/actions.md +49 -43
  50. package/src/ui/docs/content/button.md +55 -29
  51. package/src/ui/docs/content/content.md +61 -36
  52. package/src/ui/docs/content/detail.md +28 -19
  53. package/src/ui/docs/content/drawer.md +53 -47
  54. package/src/ui/docs/content/input.md +51 -35
  55. package/src/ui/docs/content/page.md +84 -95
  56. package/src/ui/docs/content/pane.md +83 -63
  57. package/src/ui/docs/content/presentation.md +20 -20
  58. package/src/ui/docs/content/tabs.md +107 -36
  59. package/src/ui/docs/content/testing.md +9 -25
  60. package/src/ui/meta.ts +5 -5
  61. package/src/ui/react.tsx +0 -1
  62. package/src/vite/design.ts +133 -23
package/CHANGELOG.md CHANGED
@@ -7,6 +7,77 @@ 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
+ ## 22.1.0 — 2026-09-28
11
+
12
+ Os dados do modo design saem do contrato (ADR 0037). No modo design, toda action passa a responder
13
+ sem que o contrato declare nada: a resposta é gerada pelo `fake` a partir do `output`, a mesma
14
+ requisição recebe a mesma resposta, uma `list` devolve três páginas do tamanho pedido em `limit` (20
15
+ por padrão) pela paginação declarada, e a saída repete o `id` da entrada quando o schema aceita.
16
+ Quando o design precisa de coerência, um cenário escrito com `defineDesignScenario`, do novo
17
+ `@softize/opus/design`, responde no lugar da resposta gerada. O plugin encontra os cenários pela
18
+ opção `scenarios` de `opusDesign()`, com caminhos ou globs; um cenário de action inexistente, ou
19
+ dois cenários para a mesma action, impedem o runtime de design de subir, e o erro lista os nomes.
20
+
21
+ `mockHandler` fica obsoleto. Ele ainda responde no modo design, abaixo do cenário e acima da
22
+ resposta gerada, e sai na 23.0.0. Os tipos o marcam com `@deprecated`. O `opus check` passa a
23
+ contar, numa linha de aviso que não reprova, quantas actions ainda o declaram. A sonda
24
+ `/__opus/design` ganha `scenarios` e `generated`; `mocked` e `unmocked` mantêm o formato anterior.
25
+
26
+ O template de `opus create` e as skills `create-opus-action` e `test-opus-action` deixam de ensinar
27
+ `mockHandler`.
28
+
29
+ **O que fazer:** nada é obrigatório nesta versão. Para migrar, apague os `mockHandler` dos
30
+ contratos; se a resposta de algum importar para o design, transforme-a em cenário e aponte a opção
31
+ `scenarios` do plugin para o lugar onde os cenários ficam. Testes que usavam `runAction` com
32
+ `OPUS_MODE=design` para exercitar o mock devem testar a action ligada ao handler.
33
+
34
+ ## 22.0.0 — 2026-09-28
35
+
36
+ As superfícies passam a usar uma introdução única e ganham composições explícitas para conteúdo
37
+ seccionado, abas emolduradas e grupos de ações divididos. A mudança elimina wrappers e classes que
38
+ os consumidores repetiam para montar essas formas, além de alinhar Drawer e Content ao raio
39
+ estrutural vigente. É major porque remove `PageIntro`, muda a posição visual do cabeçalho nas páginas
40
+ com `PageShell` e altera o acabamento padrão de Drawer, Tabs `line` e detalhes horizontais com
41
+ divisórias.
42
+
43
+ ### Breaking
44
+
45
+ **`PageIntro` foi removido.** `PageHeader` é a única introdução da página e reúne, nessa ordem,
46
+ `PageNavigation`, `PageTitle` e `PageActions`. `PageShell` deixou de renderizar uma barra persistente:
47
+ ele fornece navegação e ações contextuais ao header da página ativa. Migração: mova o título e o
48
+ breadcrumb de `PageIntro` para `PageHeader`; remova imports de `PageIntro` e classes que compensavam
49
+ a barra superior. `PageShell.headerClassName` também foi removida porque não existe mais uma barra
50
+ própria para estilizar; remova a propriedade e, quando necessário, estilize o `PageHeader` da página.
51
+ Um header sem conteúdo renderizável não cria região vazia.
52
+
53
+ **Drawer usa moldura recuada.** `DrawerContent` passa a ter borda completa, `rounded-xl`, recorte e
54
+ `0.25rem` de respiro em relação à viewport. Migração: remova classes locais de borda, raio, recorte e
55
+ inset que reconstruíam essa moldura; preserve apenas dimensões deliberadas, como `max-w-*`.
56
+
57
+ **`TabsList variant="line"` mudou de composição.** A linha-base respeita a largura recebida, a aba
58
+ usa uma área interna da altura de Button com fundo somente no hover e o indicador ativo usa o
59
+ contexto primário. `DialogBody` e `DrawerBody` não publicam mais `--opus-inline-gutter`. Migração:
60
+ remova classes locais que estendiam a linha, simulavam o botão interno ou compensavam o gutter.
61
+
62
+ **`DetailGroup` associa o tratamento tabular a `dividers`.** Em orientação horizontal, divisórias
63
+ passam a aplicar padding por célula, fundo sutil nos rótulos e largura padrão de `8.5rem`, com ou sem
64
+ moldura externa. `variant="framed"` sem divisórias deixa de alterar o fundo dos rótulos. Migração:
65
+ declare `dividers` quando quiser a grade tabular; não use `framed` para escolher o tratamento interno.
66
+
67
+ ### Superfícies e controles
68
+
69
+ - `Content` aceita `layout="continuous" | "sectioned"`. O layout seccionado exige `framed`, separa
70
+ o header com divisor e deixa o body sem recuo para tabelas, listas e grades que possuem células.
71
+ Molduras de Content usam `rounded-xl`, inclusive nos acabamentos `default` e `legend`.
72
+ - `Tabs` aceita `framed`, que aplica borda, raio e recorte sem criar fundo próprio, e
73
+ `distribution="content" | "equal"`, que preserva a largura dos rótulos ou reparte igualmente a
74
+ largura disponível entre os gatilhos.
75
+ - `ButtonGroup` aceita `framed` e `mode="divided"`; o modo insere divisores orientados entre os
76
+ controles sem incluí-los no cálculo de `distribution="equal"`.
77
+ - `Button` aceita `context="warning"` em todas as variantes visuais.
78
+ - O filtro composto de período do `ActionList` apresenta período antes do tipo de data, usa divisor
79
+ estrutural e aplica o foco ao grupo quando qualquer controle interno está aberto.
80
+
10
81
  ## 21.0.1 — 2026-09-26
11
82
 
12
83
  **Correção.** O `status()` do driver BullMQ podia devolver `status: 'done'` com `data: null` para um
package/bin/cli.mjs CHANGED
@@ -40,7 +40,15 @@ async function cmdCheck(dir) {
40
40
  for (const error of materialization.errors) log('error', `✗ opus check: ${error}`)
41
41
  process.exit(1)
42
42
  }
43
- const { files, actions, contracts, findings, hasMarker } = await scanDir(root)
43
+ const { files, actions, contracts, deprecatedMockHandlers, findings, hasMarker } = await scanDir(root)
44
+ // Aviso de migração, não violação: uma linha só, para não poluir o gate a cada execução.
45
+ if (deprecatedMockHandlers > 0) {
46
+ log(
47
+ 'warn',
48
+ `⚠ opus check: ${deprecatedMockHandlers} action(s) ainda declaram \`mockHandler\`, que sai na próxima major. ` +
49
+ 'No modo design, a action sem mock já responde com dado gerado; use `defineDesignScenario` quando o design precisar de um cenário (ADR 0037).',
50
+ )
51
+ }
44
52
 
45
53
  if (actions === 0 && findings.length === 0) {
46
54
  const verdict = emptyScanVerdict({ hasMarker, contracts })
package/bin/lib/check.mjs CHANGED
@@ -121,13 +121,12 @@ const UI_SURFACE_PAIRS = new Map([
121
121
  // locais homônimos.
122
122
  const UI_STRUCTURAL_PARENTS = new Map([
123
123
  ["PageHeader", new Set(["Page"])],
124
- ["PageIntro", new Set(["Page"])],
125
124
  ["PageBody", new Set(["Page"])],
126
125
  ["PageFooter", new Set(["Page"])],
127
- ["PageTitle", new Set(["PageHeader", "PageIntro"])],
126
+ ["PageTitle", new Set(["PageHeader"])],
128
127
  ["PageBack", new Set(["PageHeader"])],
129
128
  ["PageNavigation", new Set(["PageHeader"])],
130
- ["PageActions", new Set(["PageHeader", "PageIntro"])],
129
+ ["PageActions", new Set(["PageHeader"])],
131
130
  ["ContentHeader", new Set(["Content"])],
132
131
  ["ContentBody", new Set(["Content"])],
133
132
  ["ContentTitle", new Set(["ContentHeader"])],
@@ -191,11 +190,9 @@ const UI_STRUCTURAL_ROOTS = new Map([
191
190
  "Page",
192
191
  {
193
192
  header: "PageHeader",
194
- alternateHeader: "PageIntro",
195
193
  body: "PageBody",
196
194
  footer: "PageFooter",
197
195
  headerRequired: false,
198
- allowBothHeaders: true,
199
196
  shorthand: new Set(["title", "actions"]),
200
197
  // O contador saiu do título da página (ADR 0009). Tirar `count` do shorthand não proíbe
201
198
  // nada sozinho — só apaga o marcador que fazia o gate enxergar aquele Page —, então a
@@ -219,7 +216,6 @@ const UI_STRUCTURAL_ROOTS = new Map([
219
216
 
220
217
  const UI_STRICT_DIRECT_COMPONENTS = new Set([
221
218
  "PageHeader",
222
- "PageIntro",
223
219
  "PageBody",
224
220
  "PageFooter",
225
221
  "PageTitle",
@@ -241,7 +237,7 @@ const UI_REMOVED_PROPS = new Map([
241
237
  [
242
238
  "PageHeader",
243
239
  new Map([
244
- ["variant", "use `PageShell` para a barra persistente da página"],
240
+ ["variant", "o cabeçalho da página tem uma única apresentação rolável"],
245
241
  ]),
246
242
  ],
247
243
  [
@@ -265,6 +261,7 @@ const UI_REMOVED_PROPS = new Map([
265
261
  ]);
266
262
 
267
263
  const UI_REMOVED_COMPONENTS = new Map([
264
+ ["PageIntro", "use `PageHeader` como a única introdução estrutural"],
268
265
  ["PageDescription", "mova contexto relevante para o início de `PageBody`"],
269
266
  [
270
267
  "DialogDescription",
@@ -289,14 +286,6 @@ const UI_STRUCTURAL_HEADERS = new Map([
289
286
  titleRequired: false,
290
287
  },
291
288
  ],
292
- [
293
- "PageIntro",
294
- {
295
- title: "PageTitle",
296
- optional: new Set(["PageActions"]),
297
- shorthand: new Set(),
298
- },
299
- ],
300
289
  [
301
290
  "ContentHeader",
302
291
  {
@@ -653,6 +642,8 @@ export function checkProject(sources) {
653
642
  const productDeclarations = [];
654
643
  const entityNames = new Set();
655
644
  const actionNames = new Set();
645
+ // `mockHandler` obsoleto (ADR 0037): contado, não reprovado — a minor ainda o aceita.
646
+ let deprecatedMockHandlers = 0;
656
647
 
657
648
  for (const { file, text } of sources) {
658
649
  const declarations = parseDataProducts(file, text);
@@ -677,6 +668,7 @@ export function checkProject(sources) {
677
668
  if (item.name !== null) actionNames.add(item.name);
678
669
  }
679
670
  for (const f of lintAction(item)) findings.push({ ...f, file });
671
+ if (item.keys.includes("mockHandler")) deprecatedMockHandlers += 1;
680
672
  }
681
673
  }
682
674
 
@@ -782,6 +774,7 @@ export function checkProject(sources) {
782
774
  actions,
783
775
  contracts,
784
776
  dataProducts: productDeclarations.length,
777
+ deprecatedMockHandlers,
785
778
  findings,
786
779
  };
787
780
  }
@@ -1118,11 +1111,14 @@ const jsxSpaceEntities = {
1118
1111
  * As demais entidades nomeadas ficam como estão: nenhuma delas vira espaço em branco.
1119
1112
  */
1120
1113
  function decodeJsxSpaceEntities(raw) {
1121
- return raw.replace(/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|([a-zA-Z]+));/g, (entity, dec, hex, name) => {
1122
- if (name !== undefined) return jsxSpaceEntities[name] ?? entity;
1123
- const code = dec !== undefined ? Number(dec) : Number.parseInt(hex, 16);
1124
- return code <= 0x10ffff ? String.fromCodePoint(code) : entity;
1125
- });
1114
+ return raw.replace(
1115
+ /&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|([a-zA-Z]+));/g,
1116
+ (entity, dec, hex, name) => {
1117
+ if (name !== undefined) return jsxSpaceEntities[name] ?? entity;
1118
+ const code = dec !== undefined ? Number(dec) : Number.parseInt(hex, 16);
1119
+ return code <= 0x10ffff ? String.fromCodePoint(code) : entity;
1120
+ },
1121
+ );
1126
1122
  }
1127
1123
 
1128
1124
  /**
@@ -1176,7 +1172,10 @@ function invalidSplitChildren(split, imports, localComponents) {
1176
1172
  return text(node.text, node);
1177
1173
  }
1178
1174
  if (ts.isNumericLiteral(node)) {
1179
- invalid.push({ label: `o número ${node.text}`, position: node.getStart() });
1175
+ invalid.push({
1176
+ label: `o número ${node.text}`,
1177
+ position: node.getStart(),
1178
+ });
1180
1179
  return;
1181
1180
  }
1182
1181
  if (
@@ -1206,7 +1205,8 @@ function invalidSplitChildren(split, imports, localComponents) {
1206
1205
  for (const nested of node.children) child(nested);
1207
1206
  return;
1208
1207
  }
1209
- if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) element(node);
1208
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node))
1209
+ element(node);
1210
1210
  };
1211
1211
  for (const node of split.children) child(node);
1212
1212
  return invalid;
@@ -1215,7 +1215,8 @@ function invalidSplitChildren(split, imports, localComponents) {
1215
1215
  function directImportedJsxChildren(node, imports) {
1216
1216
  if (!ts.isJsxElement(node)) return [];
1217
1217
  return node.children.flatMap((child) => {
1218
- if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child)) return [];
1218
+ if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child))
1219
+ return [];
1219
1220
  const local = jsxLocalName(child);
1220
1221
  const imported = local === null ? undefined : imports.get(local);
1221
1222
  return imported === undefined ? [] : [{ node: child, component: imported }];
@@ -1224,7 +1225,8 @@ function directImportedJsxChildren(node, imports) {
1224
1225
 
1225
1226
  function jsxNumericAttribute(node, name) {
1226
1227
  const attribute = jsxAttribute(jsxOpening(node), name);
1227
- if (attribute === undefined || attribute.initializer === undefined) return null;
1228
+ if (attribute === undefined || attribute.initializer === undefined)
1229
+ return null;
1228
1230
  if (
1229
1231
  ts.isJsxExpression(attribute.initializer) &&
1230
1232
  attribute.initializer.expression !== undefined &&
@@ -1243,7 +1245,7 @@ function countPageHeadingSources(page, imports) {
1243
1245
  let count = 0;
1244
1246
  let states = 0;
1245
1247
  for (const child of directImportedJsxChildren(page, imports)) {
1246
- if (child.component === "PageHeader" || child.component === "PageIntro") {
1248
+ if (child.component === "PageHeader") {
1247
1249
  count += directImportedJsxChildren(child.node, imports).filter(
1248
1250
  (entry) => entry.component === "PageTitle",
1249
1251
  ).length;
@@ -1271,7 +1273,8 @@ function countPageHeadingSources(page, imports) {
1271
1273
 
1272
1274
  function pageBodyHasOpaqueDescendant(page, imports) {
1273
1275
  for (const child of directImportedJsxChildren(page, imports)) {
1274
- if (child.component !== "PageBody" || !ts.isJsxElement(child.node)) continue;
1276
+ if (child.component !== "PageBody" || !ts.isJsxElement(child.node))
1277
+ continue;
1275
1278
  let opaque = false;
1276
1279
  const visit = (node) => {
1277
1280
  if (opaque) return;
@@ -1323,7 +1326,11 @@ export function checkUiStructure(file, text) {
1323
1326
  const local = jsxLocalName(node);
1324
1327
  const component = local === null ? null : imports.get(local);
1325
1328
  if (component === "Split" && ts.isJsxElement(node)) {
1326
- for (const child of invalidSplitChildren(node, imports, localComponents)) {
1329
+ for (const child of invalidSplitChildren(
1330
+ node,
1331
+ imports,
1332
+ localComponents,
1333
+ )) {
1327
1334
  findings.push({
1328
1335
  rule: "ui-structure-split",
1329
1336
  level: "error",
@@ -1552,7 +1559,7 @@ export function checkUiStructure(file, text) {
1552
1559
  add(
1553
1560
  node,
1554
1561
  component,
1555
- "Page explícito exige exatamente um heading principal em PageHeader, PageIntro, Content level={1} ou PageState dentro de PageBody.",
1562
+ "Page explícito exige exatamente um heading principal em PageHeader, Content level={1} ou PageState dentro de PageBody.",
1556
1563
  );
1557
1564
  }
1558
1565
  }
@@ -1918,7 +1925,7 @@ async function walkUiFiles(dir, acc = []) {
1918
1925
  return acc;
1919
1926
  }
1920
1927
 
1921
- /** Escaneia um diretório → { files, actions, contracts, findings, hasMarker }.
1928
+ /** Escaneia um diretório → { files, actions, contracts, deprecatedMockHandlers, findings, hasMarker }.
1922
1929
  * `hasMarker` = o diretório é um projeto opus (tem opus.json na raiz). */
1923
1930
  export async function scanDir(rootDir) {
1924
1931
  rootDir = canonicalProjectDirectory(rootDir);
@@ -1948,6 +1955,7 @@ export async function scanDir(rootDir) {
1948
1955
  actions: checked.actions,
1949
1956
  contracts: checked.contracts,
1950
1957
  dataProducts: checked.dataProducts,
1958
+ deprecatedMockHandlers: checked.deprecatedMockHandlers,
1951
1959
  findings: [...checked.findings, ...uiFindings],
1952
1960
  hasMarker,
1953
1961
  };
@@ -8,8 +8,9 @@
8
8
  > restaurá-lo em `ready`. A lista de responsabilidades abaixo já descreve o comportamento novo; o
9
9
  > adendo no fim deste arquivo registra o motivo, a alternativa descartada e o que se perde.
10
10
  >
11
- > **Atualização (2026-09-10).** Dentro de `PageShell`, o estado integral oculta somente
12
- > `PageIntro`. A barra pertence à moldura persistente e continua visível (ADR 0011).
11
+ > **Atualização (2026-09-10, superada em 2026-09-28).** A ADR 0011 preservava a barra do
12
+ > `PageShell` e ocultava somente `PageIntro`. A ADR 0029 removeu as duas regiões: `PageHeader` é a
13
+ > única introdução e o estado integral a substitui por inteiro, dentro ou fora de `PageShell`.
13
14
 
14
15
  ## Contexto
15
16
 
@@ -13,6 +13,9 @@
13
13
  > **Atualização (2026-09-16).** O corpo abaixo descreve a anatomia da época em que foi escrito, não a
14
14
  > vigente: a apresentação `bar` da ADR 0010 saiu com a ADR 0011, e nenhum header estrutural carrega
15
15
  > mais descrição (ADRs 0018 e 0014). A anatomia atual está na documentação de cada família.
16
+ >
17
+ > **Atualização (2026-09-28).** A ADR 0033 substitui a decisão de prender a moldura externa do
18
+ > Drawer à viewport: o painel agora usa moldura completa e recuada.
16
19
 
17
20
  ## Contexto
18
21
 
@@ -44,6 +44,13 @@ quanto sua materialização (`btn-danger`, `btn-outline-danger`), mistura hierar
44
44
  inclui `light`/`dark`, que pertencem ao tema. Copiar a API manteria a ambiguidade que queremos
45
45
  remover. Rejeitada como contrato literal e aceita como referência de vocabulário.
46
46
 
47
+ ### Representar ações de cautela como neutras, perigosas ou com classes locais
48
+
49
+ Uma ação pode pedir atenção sem ser destrutiva, como registrar uma tentativa sem sucesso. Tratá-la
50
+ como `neutral` apaga essa diferença; tratá-la como `danger` exagera o risco; aplicar tokens de
51
+ `warning` por `className` cria uma variante clandestina fora do contrato e não alcança tipos,
52
+ documentação nem componentes que reutilizam `ButtonContext`. Rejeitada.
53
+
47
54
  ### Declarar contexto e variante como eixos independentes
48
55
 
49
56
  `context` escolhe a família de tokens e responde por que existe o realce. `variant` escolhe como a
@@ -59,12 +66,7 @@ O Opus define o vocabulário canônico:
59
66
 
60
67
  ```ts
61
68
  type UiContext =
62
- | 'neutral'
63
- | 'primary'
64
- | 'info'
65
- | 'success'
66
- | 'warning'
67
- | 'danger'
69
+ "neutral" | "primary" | "info" | "success" | "warning" | "danger";
68
70
  ```
69
71
 
70
72
  - `neutral`: estado normal, inativo ou sem julgamento positivo/negativo;
@@ -86,24 +88,29 @@ domínio não se torna a ação principal da interface. A metadata canônica pas
86
88
  Para componentes semânticos, `variant` descreve somente o tratamento visual:
87
89
 
88
90
  ```ts
89
- type SemanticVariant = 'solid' | 'subtle' | 'outline' | 'ghost' | 'link'
91
+ type SemanticVariant = "solid" | "subtle" | "outline" | "ghost" | "link";
90
92
  ```
91
93
 
92
94
  Cada componente aceita apenas as variantes que consegue materializar com coerência. Os defaults
93
95
  iniciais são:
94
96
 
95
- | Componente | Contexto padrão | Variante padrão | Contextos permitidos |
96
- | --- | --- | --- | --- |
97
- | Button | `primary` | `solid` | `neutral`, `primary`, `danger` |
98
- | Badge | `neutral` | `subtle` | todos |
99
- | Alert | `neutral` | `subtle` | `neutral`, `info`, `success`, `warning`, `danger` |
100
- | Dot | `neutral` | `solid` | `neutral`, `primary`, `info`, `success`, `warning`, `danger` |
101
- | MetricCard | `neutral` | `subtle` no ícone | `neutral`, `info`, `success`, `warning`, `danger` |
97
+ | Componente | Contexto padrão | Variante padrão | Contextos permitidos |
98
+ | ---------- | --------------- | ----------------- | ------------------------------------------------------------ |
99
+ | Button | `primary` | `solid` | `neutral`, `primary`, `warning`, `danger` |
100
+ | Badge | `neutral` | `subtle` | todos |
101
+ | Alert | `neutral` | `subtle` | `neutral`, `info`, `success`, `warning`, `danger` |
102
+ | Dot | `neutral` | `solid` | `neutral`, `primary`, `info`, `success`, `warning`, `danger` |
103
+ | MetricCard | `neutral` | `subtle` no ícone | `neutral`, `info`, `success`, `warning`, `danger` |
102
104
 
103
105
  `solid`, `subtle` e `outline` podem compartilhar um contexto sem compartilhar classes. `ghost` e
104
106
  `link` ficam restritos a controles interativos. Cor e ícone continuam reforços: texto ou nome
105
107
  acessível comunica o significado.
106
108
 
109
+ `warning` em `Button` representa uma ação de cautela não destrutiva: a pessoa pode prosseguir, mas
110
+ o efeito merece atenção. `danger` permanece reservado a risco ou consequência perigosa. `info` e
111
+ `success` continuam fora do subconjunto do botão até existir uma necessidade de ação observável;
112
+ estado informativo ou resultado positivo, isoladamente, não justificam colorir um controle.
113
+
107
114
  Componentes cuja `variant` é exclusivamente estrutural ou local, como `plain | framed` e
108
115
  `default | line`, podem mantê-la. Ao criar API nova, preferir uma prop específica quando o nome do
109
116
  eixo for mais claro (`frame`, `layout`, `appearance`); não renomear primitives existentes sem ganho
@@ -161,6 +168,8 @@ for consultada.
161
168
 
162
169
  - Primitives semânticos passam a compartilhar um vocabulário e tokens contextuais.
163
170
  - Defaults podem variar por componente, mas a mesma palavra nunca muda de significado.
171
+ - Ações de cautela não destrutivas usam os tokens canônicos de `warning`, sem classes locais nem
172
+ reclassificação como `danger`.
164
173
  - A API fica mais explícita em chamadas que precisam dos dois eixos.
165
174
  - A transição aumenta temporariamente tipos, testes e normalização por causa dos aliases.
166
175
  - Temas precisam oferecer tokens de superfície, borda, texto de ênfase e sólido para cada contexto,
@@ -12,6 +12,11 @@
12
12
  > `PageHeader`, `PageIntro` e `PageFooter` como opcionais e exige exatamente um heading principal —
13
13
  > `PageTitle`, `Content level={1}` ou um `PageState` ativo. As frases abaixo que pedem `PageIntro`
14
14
  > "sem `PageHeader`" descrevem a regra da 17.2 e não valem mais.
15
+ >
16
+ > **Atualização (2026-09-27).** A padronização posterior do chrome estrutural levou a barra do
17
+ > `PageShell` dos `3rem` originais para `3.75rem`. A presença persistente da barra continua sendo a
18
+ > decisão arquitetural desta ADR; sua medida pertence ao contrato vigente do componente, registrado
19
+ > na documentação e nos testes do Opus, e não deve ser redefinida localmente pelo consumidor.
15
20
 
16
21
  ## Contexto
17
22
 
@@ -7,6 +7,9 @@
7
7
  > materializa título e comandos de header no `Content` que envolve o `ActionList`. Dialog e Drawer
8
8
  > continuam materializando esses elementos no header da superfície.
9
9
  >
10
+ > **Atualização (2026-09-28).** A ADR 0029 removeu `PageIntro` e a barra persistente de
11
+ > `PageShell`. As passagens abaixo sobre a anatomia de Page já refletem a introdução única.
12
+ >
10
13
  > Uma Presentation pode declarar uma `route` absoluta com parâmetros. Nessa forma, URL, surface e
11
14
  > origem do registro passam a integrar o artefato; o adaptador da aplicação apenas conecta a spec ao
12
15
  > router hospedeiro.
@@ -29,10 +32,10 @@ que aparece. `Surface` escolhe `page`, `dialog` ou `drawer` no momento da render
29
32
 
30
33
  A anatomia portátil é `Header + Body + Footer?`, mas cada surface materializa seu header de acordo
31
34
  com o espaço disponível. Dialog e Drawer organizam horizontalmente
32
- `Navigation? + Title + Actions? + Close` e mantêm as ações de conclusão no footer. Em Page
33
- hospedada por `PageShell`, a barra persistente reúne navegação e ações; o título inicia o conteúdo
34
- em `PageIntro`, sem repetir a página atual ao lado do breadcrumb. Fora do shell, a Page materializa
35
- seu header completo no próprio container.
35
+ `Navigation? + Title + Actions? + Close` e mantêm as ações de conclusão no footer. Em Page,
36
+ `PageHeader` reúne navegação contextual, título e ações numa única introdução rolável. Quando a
37
+ listagem fornece o heading principal por `Content level={1}`, o mesmo header pode conter somente a
38
+ navegação ou as ações fornecidas pelo `PageShell`, sem criar uma segunda região visual.
36
39
 
37
40
  Uma action assume um de dois papéis:
38
41
 
@@ -4,6 +4,10 @@
4
4
  - **Data:** 2026-09-13.
5
5
  - **Revisa:** ADRs 0011 e 0013.
6
6
 
7
+ > **Atualização (2026-09-28).** A ADR 0029 removeu `PageIntro` e a barra persistente de
8
+ > `PageShell`. As regras abaixo preservam `Content` como dono do cabeçalho da coleção e usam
9
+ > `PageHeader` como a única introdução da página.
10
+
7
11
  ## Contexto
8
12
 
9
13
  Páginas centradas em uma coleção colocavam o título no `PageIntro`, a criação na barra do
@@ -23,11 +27,12 @@ as ações da coleção, como criar um item. `ActionList` continua responsável
23
27
  atualização, visualização, resultados, estados e paginação. `toolbarActions` fica reservado a
24
28
  operações ligadas ao recorte corrente da lista, não ao comando primário de criar.
25
29
 
26
- `PageHeader` representa o chrome da página: navegação e ações globais. Dentro de `PageShell`,
27
- esses slots são projetados na barra; `PageIntro` permanece opcional para uma introdução própria da
28
- página. Uma `Page` explícita mantém exatamente um heading principal: `PageTitle` em header/intro,
29
- `Content level={1}` ou um `PageState` ativo no body. Quando `Content` fornece esse heading, a Page
30
- pode conter somente `PageBody`.
30
+ `PageHeader` é a única introdução da página e reúne navegação contextual, título e ações globais.
31
+ Dentro de `PageShell`, navegação e ações fornecidas pelo shell são compostas nesse mesmo header,
32
+ sem barra persistente. Uma `Page` explícita mantém exatamente um heading principal: `PageTitle` em
33
+ `PageHeader`, `Content level={1}` ou um `PageState` ativo no body. Quando `Content` fornece esse
34
+ heading, a Page pode conter somente `PageBody`; se `PageShell` acrescentar navegação ou ações,
35
+ `Page` materializa automaticamente o `PageHeader` que as recebe.
31
36
 
32
37
  Na `Presentation`, uma action `list` exibida como Page materializa o título e os comandos de header
33
38
  no `Content` que envolve o `ActionList`. Em Dialog e Drawer, o título e esses comandos continuam no
@@ -72,9 +77,9 @@ responsabilidades locais. Foi descartada para coleções; ações realmente glob
72
77
 
73
78
  ## Verificação
74
79
 
75
- - testes de `Page` cobrem `PageHeader + PageIntro + PageBody` e `PageBody` sem header;
80
+ - testes de `Page` cobrem `PageHeader + PageBody` e `PageBody` sem header;
76
81
  - `opus check` aceita essa gramática e continua rejeitando slots duplicados ou fora da família;
77
- - testes de `Presentation` confirmam que Page de listagem usa `Content` e mantém a barra sem a ação
78
- da coleção;
82
+ - testes de `Presentation` confirmam que Page de listagem usa `Content` e mantém a introdução sem a
83
+ ação da coleção;
79
84
  - a documentação de `ActionList` recomenda `Content` e reserva `toolbarActions` ao recorte atual;
80
85
  - consumidores validam visualmente o título e a criação junto da listagem.
@@ -3,6 +3,7 @@
3
3
  - **Status:** aceita.
4
4
  - **Data:** 2026-09-23.
5
5
  - **Complementa:** ADRs 0005, 0014, 0018, 0019 e 0021.
6
+ - **Superada em parte por:** ADR 0030, que permite anatomia seccionada em `Content` com moldura.
6
7
 
7
8
  ## Contexto
8
9
 
@@ -78,13 +79,13 @@ diferentes para coisas diferentes e permanecem assim.
78
79
 
79
80
  `continuous` e `sectioned` não são a ausência e a presença de uma borda. Cada um é um pacote:
80
81
 
81
- | Aspecto | `continuous` | `sectioned` |
82
- | --- | --- | --- |
83
- | Divisores | nenhum | entre regiões adjacentes presentes |
84
- | Ritmo vertical | header aproxima-se do corpo; o espaço separa | regiões simétricas; o divisor separa |
85
- | Escala do título | a da família | a da família, um degrau mais contida quando a família tem duas |
86
- | Fundo do footer | igual ao da superfície | sutil, quando a família declara |
87
- | Alinhamento horizontal | idêntico entre header, body e footer | idêntico entre header, body e footer |
82
+ | Aspecto | `continuous` | `sectioned` |
83
+ | ---------------------- | -------------------------------------------- | -------------------------------------------------------------- |
84
+ | Divisores | nenhum | entre regiões adjacentes presentes |
85
+ | Ritmo vertical | header aproxima-se do corpo; o espaço separa | regiões simétricas; o divisor separa |
86
+ | Escala do título | a da família | a da família, um degrau mais contida quando a família tem duas |
87
+ | Fundo do footer | igual ao da superfície | sutil, quando a família declara |
88
+ | Alinhamento horizontal | idêntico entre header, body e footer | idêntico entre header, body e footer |
88
89
 
89
90
  O alinhamento horizontal é invariante dos dois layouts: o recuo lateral de header, body e footer de
90
91
  uma mesma superfície é sempre o mesmo valor. É o que a suíte transversal verifica.
@@ -102,15 +103,15 @@ A recomendação de escrever `border-b` e `border-t` no Card sai da documentaç
102
103
  Os defaults preservam o que cada família já entrega hoje, exceto onde o comportamento atual é o
103
104
  defeito que esta decisão corrige:
104
105
 
105
- | Família | Default | Por quê |
106
- | --- | --- | --- |
107
- | `Card` | `continuous` | é o que a família entrega hoje: sem divisores, ritmo assimétrico |
108
- | `Dialog` (default) | `sectioned` | já tem divisores e faixas; o default nomeia o que existe |
109
- | `Dialog` (`mode="alert"`) | composição própria | a resposta obrigatória mantém sua anatomia centrada |
110
- | `Drawer` | `sectioned` | idem Dialog |
111
- | `Pane` | `sectioned` | header e footer já têm divisor |
112
- | `Content` | contínuo, sem opção | região semântica dentro de outra superfície |
113
- | `Page` | contínuo, sem opção | a separação do chrome pertence ao shell |
106
+ | Família | Default | Por quê |
107
+ | ------------------------- | ------------------- | ---------------------------------------------------------------- |
108
+ | `Card` | `continuous` | é o que a família entrega hoje: sem divisores, ritmo assimétrico |
109
+ | `Dialog` (default) | `sectioned` | já tem divisores e faixas; o default nomeia o que existe |
110
+ | `Dialog` (`mode="alert"`) | composição própria | a resposta obrigatória mantém sua anatomia centrada |
111
+ | `Drawer` | `sectioned` | idem Dialog |
112
+ | `Pane` | `sectioned` | header e footer já têm divisor |
113
+ | `Content` | contínuo, sem opção | região semântica dentro de outra superfície |
114
+ | `Page` | contínuo, sem opção | a separação do chrome pertence ao shell |
114
115
 
115
116
  Nenhum default muda de valor efetivo com esta ADR. O que muda é que o valor passa a ter nome,
116
117
  implementação única e teste.
@@ -127,7 +128,7 @@ A migração foi decidida sobre 374 usos reais das seis famílias na plataforma.
127
128
  `p-5`, `px-3 py-2`). O root do Card continua sem padding próprio: quem não usa slots continua
128
129
  decidindo. O padding pertence aos slots, como hoje.
129
130
  - **Os dois `PaneHeader` do produto escrevem quase a mesma linha** (`flex h-[3.75rem] items-center
130
- … p-3 … border-b-0`) em contextos diferentes. É a prova de que padding e divisor pertencem à
131
+ … p-3 … border-b-0`) em contextos diferentes. É a prova de que padding e divisor pertencem à
131
132
  primitiva.
132
133
  - **Um comentário no código do login documenta o defeito**: o `py-3` do `CardBody` "conta com um
133
134
  CardFooter, que o login não tem". O ritmo do layout contínuo precisa reagir à ausência do rodapé.
@@ -141,15 +142,15 @@ A migração foi decidida sobre 374 usos reais das seis famílias na plataforma.
141
142
  A anatomia é comum; o conjunto de slots continua sendo decisão de cada família, porque decisões
142
143
  anteriores já os delimitaram:
143
144
 
144
- | Slot | Card | Content | Page | Dialog | Drawer | Pane |
145
- | --- | --- | --- | --- | --- | --- | --- |
146
- | Media | sim | sim (`ContentMedia`) | não | sim (`DialogMedia`) | sim | sim |
147
- | Title | sim | sim | sim | sim | sim | sim |
148
- | Description | sim | sim | não (ADR 0014) | não (ADR 0018) | não (ADR 0018) | não |
149
- | Meta | não | sim (contador) | não (ADR 0009) | não | não | não |
150
- | Actions | sim | sim | sim | sim | sim | sim |
151
- | Body | sim | sim | sim | sim | sim | sim |
152
- | Footer | sim | não | sim | sim | sim | sim |
145
+ | Slot | Card | Content | Page | Dialog | Drawer | Pane |
146
+ | ----------- | ---- | -------------------- | -------------- | ------------------- | -------------- | ---- |
147
+ | Media | sim | sim (`ContentMedia`) | não | sim (`DialogMedia`) | sim | sim |
148
+ | Title | sim | sim | sim | sim | sim | sim |
149
+ | Description | sim | sim | não (ADR 0014) | não (ADR 0018) | não (ADR 0018) | não |
150
+ | Meta | não | sim (contador) | não (ADR 0009) | não | não | não |
151
+ | Actions | sim | sim | sim | sim | sim | sim |
152
+ | Body | sim | sim | sim | sim | sim | sim |
153
+ | Footer | sim | não | sim | sim | sim | sim |
153
154
 
154
155
  As ADRs 0009, 0014 e 0018 não são reabertas: Page, Dialog e Drawer continuam sem descrição no
155
156
  header, e Page continua sem contador. `Dialog`, `Drawer` e `Pane` ganham o slot de ações que hoje
@@ -3,6 +3,7 @@
3
3
  - **Status:** aceita.
4
4
  - **Data:** 2026-09-25.
5
5
  - **Complementa:** ADRs 0021 e 0023.
6
+ - **Complementada por:** ADR 0030, que separa o `layout` da região do acabamento `frameVariant`.
6
7
 
7
8
  ## Contexto
8
9