@softize/opus 22.2.0 → 24.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 (115) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/bin/cli.mjs +3 -9
  3. package/bin/lib/check.mjs +242 -49
  4. package/bin/lib/copy.mjs +17 -35
  5. package/bin/lib/gen-runner.mjs +2 -0
  6. package/bin/lib/gen-stubs.mjs +3 -1
  7. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +1 -1
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  9. package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +16 -12
  10. package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +8 -6
  11. package/docs/adr/0036-tabs-declares-trigger-distribution.md +11 -8
  12. package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
  13. package/docs/adr/0038-content-level-defines-primary-presentation.md +62 -0
  14. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +62 -0
  15. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +59 -0
  16. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  17. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  18. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  19. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  20. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  21. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  22. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  23. package/docs/code-style.md +1 -1
  24. package/package.json +5 -1
  25. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  26. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  27. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  28. package/registry/skills/create-opus-action/references/contract-and-binding.md +3 -3
  29. package/registry/templates/app/src/App.tsx +24 -12
  30. package/src/core/contracts.ts +1 -1
  31. package/src/core/runtime.ts +4 -37
  32. package/src/core/types.ts +5 -23
  33. package/src/design/index.ts +14 -3
  34. package/src/testing/index.ts +6 -26
  35. package/src/ui/appearance.ts +149 -0
  36. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  37. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  38. package/src/ui/components/patterns/confirm.tsx +3 -1
  39. package/src/ui/components/patterns/fill-region.ts +8 -7
  40. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  41. package/src/ui/components/patterns/form.tsx +8 -7
  42. package/src/ui/components/patterns/list.tsx +64 -14
  43. package/src/ui/components/patterns/page.tsx +15 -11
  44. package/src/ui/components/patterns/pane.tsx +9 -240
  45. package/src/ui/components/patterns/presentation.tsx +14 -14
  46. package/src/ui/components/patterns/sidebar.tsx +7 -17
  47. package/src/ui/components/patterns/split.tsx +111 -59
  48. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
  49. package/src/ui/components/patterns/structural-media.tsx +26 -0
  50. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  51. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  52. package/src/ui/components/patterns/surface.tsx +620 -0
  53. package/src/ui/components/patterns/trigger.tsx +11 -9
  54. package/src/ui/components/primitives/ask.tsx +155 -83
  55. package/src/ui/components/primitives/button.tsx +11 -9
  56. package/src/ui/components/primitives/checkbox.tsx +8 -8
  57. package/src/ui/components/primitives/command.tsx +1 -1
  58. package/src/ui/components/primitives/composer.tsx +47 -45
  59. package/src/ui/components/primitives/detail.tsx +9 -2
  60. package/src/ui/components/primitives/dialog.tsx +60 -26
  61. package/src/ui/components/primitives/drawer.tsx +58 -12
  62. package/src/ui/components/primitives/icon-picker.tsx +93 -70
  63. package/src/ui/components/primitives/input-group.tsx +35 -28
  64. package/src/ui/components/primitives/input-otp.tsx +16 -16
  65. package/src/ui/components/primitives/input.tsx +20 -10
  66. package/src/ui/components/primitives/item.tsx +9 -12
  67. package/src/ui/components/primitives/menu.tsx +44 -65
  68. package/src/ui/components/primitives/metric-card.tsx +75 -59
  69. package/src/ui/components/primitives/radio-group.tsx +9 -9
  70. package/src/ui/components/primitives/select.tsx +344 -209
  71. package/src/ui/components/primitives/switch.tsx +10 -10
  72. package/src/ui/components/primitives/table.tsx +29 -24
  73. package/src/ui/components/primitives/tabs.tsx +58 -88
  74. package/src/ui/components/primitives/textarea.tsx +6 -6
  75. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  76. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  77. package/src/ui/components/primitives/toggle.tsx +82 -16
  78. package/src/ui/docs/DocBrowser.tsx +77 -69
  79. package/src/ui/docs/content/action-form-card.md +9 -9
  80. package/src/ui/docs/content/action-list.md +85 -70
  81. package/src/ui/docs/content/action-view.md +33 -18
  82. package/src/ui/docs/content/actions.md +6 -7
  83. package/src/ui/docs/content/button.md +1 -1
  84. package/src/ui/docs/content/communication.md +1 -1
  85. package/src/ui/docs/content/customization.md +64 -11
  86. package/src/ui/docs/content/detail.md +8 -6
  87. package/src/ui/docs/content/dialog.md +100 -58
  88. package/src/ui/docs/content/drawer.md +63 -19
  89. package/src/ui/docs/content/field.md +14 -15
  90. package/src/ui/docs/content/icon-tile.md +14 -13
  91. package/src/ui/docs/content/page.md +34 -26
  92. package/src/ui/docs/content/pane.md +41 -290
  93. package/src/ui/docs/content/presentation.md +3 -3
  94. package/src/ui/docs/content/semantic-context.md +0 -1
  95. package/src/ui/docs/content/separator.md +6 -6
  96. package/src/ui/docs/content/sidebar.md +68 -266
  97. package/src/ui/docs/content/skeleton.md +20 -18
  98. package/src/ui/docs/content/split.md +14 -14
  99. package/src/ui/docs/content/surface.md +513 -0
  100. package/src/ui/docs/content/tabs.md +82 -83
  101. package/src/ui/docs/content/testing.md +2 -3
  102. package/src/ui/docs/content/toggle.md +120 -63
  103. package/src/ui/docs/content/tokens.md +124 -40
  104. package/src/ui/docs/registry.tsx +4 -6
  105. package/src/ui/drivers/react.tsx +4 -3
  106. package/src/ui/index.ts +30 -27
  107. package/src/ui/meta.ts +15 -27
  108. package/src/ui/react.tsx +42 -46
  109. package/src/ui/theme.css +311 -64
  110. package/src/vite/design.ts +23 -21
  111. package/src/ui/components/patterns/content-header.tsx +0 -532
  112. package/src/ui/components/patterns/surface-media.tsx +0 -69
  113. package/src/ui/components/primitives/card.tsx +0 -191
  114. package/src/ui/docs/content/card.md +0 -292
  115. package/src/ui/docs/content/content.md +0 -276
@@ -0,0 +1,133 @@
1
+ # ADR 0044 — A aparência neutra deriva de quatro eixos limitados por modo
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-30.
5
+ - **Relacionadas:** ADR 0043, quanto aos papéis opacos que esta decisão passa a derivar; ADR 0006,
6
+ quanto aos contextos semânticos, que permanecem fora da derivação.
7
+
8
+ ## Contexto
9
+
10
+ Pessoas diferentes leem melhor com aparências diferentes: mais separação entre as camadas, estados
11
+ mais marcados, um claro menos ofuscante, um escuro mais profundo ou neutros levemente tingidos pela
12
+ cor do produto. Até aqui, atender a um desses pedidos exigia que a aplicação sobrescrevesse, token
13
+ a token, a escala neutra da ADR 0043.
14
+
15
+ Sobrescrever tokens soltos tem dois problemas. O primeiro é a coerência: a escala tem ordem —
16
+ canvas, controle, superfície elevada, hover, seleção, pressão, borda — e mover um valor sem mover os
17
+ vizinhos apaga a distinção entre dois papéis. O segundo é a leitura: escurecer uma superfície
18
+ clara ou clarear uma superfície escura reduz o contraste do texto, e nada avisava a aplicação de
19
+ que o ajuste tinha passado do ponto.
20
+
21
+ ## Decisão
22
+
23
+ O tema deriva a escala neutra de quatro eixos. Cada eixo é um inteiro de 0 a 100, declarado
24
+ separadamente para o modo claro e para o modo escuro:
25
+
26
+ | Eixo | O que muda | Preset claro | Preset escuro |
27
+ | ------------------- | --------------------------------------------------------------------------------- | ------------ | ------------- |
28
+ | `colorIntensity` | Saturação dos neutros no matiz `--neutral-hue` | 0 | 0 |
29
+ | `contrast` | Distância de hover, seleção, pressão, `muted` e bordas até a superfície de origem | 0 | 0 |
30
+ | `surfaceBrightness` | Luminosidade do canvas e das superfícies | 100 | 50 |
31
+ | `layerContrast` | Distância entre canvas, controle e superfície elevada | 0 | 0 |
32
+
33
+ As entradas são custom properties por modo — `--appearance-light-*` e `--appearance-dark-*`. `:root`
34
+ declara os presets dos dois modos e usa as entradas claras; `.dark` troca para as escuras. Assim, a
35
+ aplicação pode gravar os dois conjuntos no mesmo elemento, e alternar o tema não exige reaplicar
36
+ nada.
37
+
38
+ Com os eixos no preset, a derivação reproduz exatamente os valores calibrados pela ADR 0043. Quem
39
+ não personaliza não percebe diferença.
40
+
41
+ ### Limites
42
+
43
+ Os limites de cada eixo protegem o tema calibrado. Em qualquer combinação de 0 a 100, nos dois
44
+ modos e para qualquer `--neutral-hue`:
45
+
46
+ - a separação entre papéis nunca fica abaixo da separação do preset. Por isso `contrast` e
47
+ `layerContrast` têm o preset em zero e só ampliam distâncias;
48
+ - o texto secundário (`muted-foreground`) mantém, sobre canvas, superfície elevada, controle e
49
+ `muted`, no mínimo o contraste que tem no preset. Para isso ele escurece no claro e clareia no
50
+ escuro conforme as superfícies se aproximam dele, e recebe apenas uma fração da saturação;
51
+ - o texto principal (`foreground`) mantém contraste de pelo menos 7:1 sobre superfícies em repouso
52
+ e 4,5:1 sobre estados de interação;
53
+ - toda luminosidade derivada permanece entre 0 e 100, e cada uma varia em um único sentido ao longo
54
+ de cada eixo.
55
+
56
+ No claro, a superfície elevada do preset já é branca, então `surfaceBrightness` parte de 100 e só
57
+ diminui. No escuro, o preset fica em 50: valores menores levam o canvas até o preto, e valores
58
+ maiores produzem um escuro mais suave.
59
+
60
+ ### O que a intensidade tinge
61
+
62
+ `colorIntensity` satura somente os tokens neutros: superfícies, estados, bordas, `ring` e textos
63
+ neutros. `primary`, `destructive` e os contextos `info`, `success`, `warning` e `danger` não
64
+ participam. `--neutral-hue` é identidade da aplicação, e não um eixo: a aplicação escolhe o matiz
65
+ uma vez, e a pessoa escolhe apenas o quanto dele aparece.
66
+
67
+ ### API pública
68
+
69
+ `@softize/opus/ui/appearance` é um módulo sem React, reexportado por `@softize/opus/ui` e por
70
+ `@softize/opus/ui/react`. Ele publica:
71
+
72
+ - os tipos `AppearanceMode`, `AppearanceAxis`, `AppearanceSettings` e `AppearanceOverrides`;
73
+ - `appearanceModes`, `appearanceAxes`, `APPEARANCE_MIN`, `APPEARANCE_MAX` e `appearancePresets`;
74
+ - `appearanceCustomProperty`, que devolve o nome da custom property de um eixo em um modo;
75
+ - `normalizeAppearanceValue` e `resolveAppearance`, para tratar valores lidos de persistência;
76
+ - `appearanceCustomProperties`, que converte overrides parciais no mapa de custom properties do
77
+ modo, com `null` nos eixos sem override;
78
+ - `applyAppearanceCustomProperties`, que grava esse mapa em um alvo de estilo e remove as
79
+ propriedades marcadas com `null`.
80
+
81
+ ### Reset, persistência e interface
82
+
83
+ O Opus não guarda estado de aparência e não publica componente de ajuste. Voltar ao padrão é
84
+ remover os overrides: sem a propriedade, o cascade devolve o preset do tema ou o valor que o CSS
85
+ da aplicação tiver declarado. Onde persistir a preferência e como apresentar os controles são
86
+ decisões da aplicação consumidora.
87
+
88
+ ## Consequências
89
+
90
+ - a aplicação personaliza a escala neutra por quatro números por modo, sem redeclarar tokens;
91
+ - os papéis da ADR 0043 continuam opacos e com a mesma ordem em qualquer ajuste;
92
+ - uma aplicação que redeclara diretamente um token neutro, como `--background`, continua
93
+ funcionando, mas esse token deixa de acompanhar os eixos;
94
+ - `muted-foreground` deixa de ser um literal e passa a acompanhar as superfícies; no preset, o
95
+ valor é o mesmo de antes;
96
+ - o preset em zero impede reduzir contraste abaixo do tema calibrado. Um pedido real por uma
97
+ aparência mais plana exige recalibrar o tema, e não ampliar a faixa do eixo.
98
+
99
+ ## Alternativas descartadas
100
+
101
+ ### Presets no meio da faixa, com redução permitida
102
+
103
+ Deixaria cada controle simétrico, mas metade da faixa produziria separações menores que as do tema
104
+ calibrado: camadas que se fundem e estados que deixam de se distinguir. O ajuste passaria a poder
105
+ piorar a interface que ele existe para adaptar.
106
+
107
+ ### Um único conjunto de eixos para os dois modos
108
+
109
+ Reduziria as entradas à metade, mas os modos têm presets e direções diferentes: o brilho máximo do
110
+ claro é o ponto de partida, e o do escuro é um extremo. A preferência de uma pessoa no claro também
111
+ não prediz a do escuro.
112
+
113
+ ### Tingir todos os tokens, inclusive primary e contextos
114
+
115
+ Misturaria preferência pessoal com identidade e significado. A cor da marca e os contextos
116
+ semânticos precisam permanecer reconhecíveis em qualquer ajuste.
117
+
118
+ ### Publicar o componente de ajuste e o reset no Opus
119
+
120
+ Obrigaria o Opus a decidir persistência, escopo da preferência e texto de interface, que variam por
121
+ produto. O helper cobre a parte comum — nomes, limites e conversão —, e a aplicação compõe a tela
122
+ com os primitives existentes.
123
+
124
+ ## Verificação
125
+
126
+ - `tests/ui/appearance-tuning.test.ts` avalia as declarações de `theme.css` e cobre: paridade do
127
+ preset com os valores calibrados nos dois modos, igualdade entre os presets do módulo e os do
128
+ tema, monotonicidade de cada luminosidade em cada eixo e os limites de separação e de contraste
129
+ de texto em toda a grade de eixos e em vários matizes;
130
+ - `tests/ui/neutral-state-tokens.test.ts` garante que os tokens neutros derivados permanecem
131
+ opacos nos dois temas;
132
+ - a aplicação consumidora verifica visualmente os extremos de cada eixo em páginas com canvas,
133
+ card, formulário e grid, nos dois modos.
@@ -0,0 +1,230 @@
1
+ # ADR 0045 — Surface substitui Content e Card
2
+
3
+ > **Atualização em 2026-10-01:** a ADR 0046 atribui ao body o ritmo entre filhos diretos, amplia o
4
+ > respiro vertical e estende `flush` a Dialog e Drawer; as passagens abaixo sobre recuo permanecem
5
+ > históricas quando divergirem dessa regra. A ADR 0047 remove a anatomia visual de `Pane`, portanto
6
+ > a afirmação abaixo de que ele manteria slots próprios descreve apenas o estado anterior.
7
+
8
+ - **Status:** aceita.
9
+ - **Data:** 2026-09-30.
10
+ - **Sucede:** ADR 0021, quanto à família que reúne estrutura e elemento semântico; ADR 0023, quanto
11
+ às seis fachadas públicas, à recusa de uma superfície genérica pública e à mídia com moldura em
12
+ `Card` e `Content`; ADR 0025, quanto a `frameVariant`; ADR 0030, quanto ao body sem recuo como
13
+ parte indivisível do layout seccionado; ADR 0020, na emenda de 2026-09-26 sobre os slots de mídia
14
+ de `Card` e `Content`.
15
+ - **Preserva:** ADR 0016 (o cabeçalho da coleção pertence à região que a nomeia), ADR 0024 (a altura
16
+ se propaga por contexto) e ADR 0038 (o nível 1 define a apresentação principal), agora sob o nome
17
+ `Surface`.
18
+ - **Não altera:** `Page`, `Dialog`, `Drawer` e `Pane`, que mantêm famílias, slots e mídia próprios.
19
+
20
+ ## Contexto
21
+
22
+ Quem monta uma tela precisa delimitar e nomear um trecho dela: uma seção da página, um grupo de
23
+ campos, um registro, um item de uma grade ou um bloco clicável. Até a série 23, essa necessidade se
24
+ dividia entre duas famílias públicas:
25
+
26
+ - `Content` entregava a região nomeada: `section` ou `fieldset`, heading com nível, contador,
27
+ `aria-labelledby`, moldura opcional e a propagação de altura até a `ActionList`;
28
+ - `Card` entregava a superfície elevada: fundo próprio, rodapé, `asChild` para um bloco clicável e
29
+ a caixa simples com filhos livres.
30
+
31
+ As duas cresceram uma em direção à outra. `Content` ganhou moldura (ADR 0021), mídia (ADR 0023,
32
+ emenda), acabamento de legenda (ADR 0025) e layout seccionado (ADR 0030). `Card` ganhou heading com
33
+ nível, layout e divisores derivados (ADR 0023). O resultado são duas famílias com dezessete exports,
34
+ que respondem à mesma pergunta com respostas diferentes:
35
+
36
+ - o mesmo desenho — moldura, header com mídia e título, body — tem dois recuos (`1rem` e `1.25rem`),
37
+ duas escalas de título e duas posições de mídia, conforme o nome do componente escolhido;
38
+ - um registro que precisa de `section` e de rodapé não cabe em nenhuma das duas: `Content` não tem
39
+ footer e `Card` não tem elemento semântico;
40
+ - a escolha entre as famílias é feita pela aparência (“tem fundo?”), e não pela semântica, o que a
41
+ ADR 0025 já havia identificado como defeito ao separar `fieldset` do acabamento de legenda;
42
+ - a mídia desenhava `IconTile` por conta própria e, para não produzir moldura dentro de moldura,
43
+ inspecionava o filho e adotava as props dele. Avatar, imagem e logotipo precisavam de `asChild`
44
+ para escapar de uma moldura que não pediram.
45
+
46
+ A ADR 0023 descartou um `<Surface>` público porque ele “apagaria a semântica de cada família”. A
47
+ evidência posterior mostrou o oposto para este par: a semântica de `Content` e `Card` já não vinha
48
+ do nome, e sim de `as`, `level`, `framed` e da presença de fundo. Para `Dialog`, `Drawer`, `Pane` e
49
+ `Page`, o argumento continua válido — o nome carrega comportamento (foco, portal, redimensionamento,
50
+ chrome de página) — e esta decisão não os alcança.
51
+
52
+ As forças em tensão:
53
+
54
+ - **Uma gramática × dois contratos preservados.** A região de página e a entidade elevada precisam
55
+ continuar possíveis, com acessibilidade e ocupação de altura intactas.
56
+ - **Eixos independentes × pacotes validados.** Moldura, fundo e anatomia são decisões separadas, mas
57
+ recuo, ritmo e divisores continuam sendo pacotes que o consumidor não remonta por classes.
58
+ - **Migração atômica × compatibilidade.** Manter aliases prolongaria a dupla resposta que motiva a
59
+ decisão.
60
+
61
+ ## Decisão
62
+
63
+ ### 1. Uma família pública
64
+
65
+ `Surface` substitui integralmente `Content` e `Card`. A família é:
66
+
67
+ `Surface`, `SurfaceHeader`, `SurfaceBody`, `SurfaceFooter`, `SurfaceTitle`, `SurfaceDescription`,
68
+ `SurfaceMedia`, `SurfaceMeta` e `SurfaceActions`.
69
+
70
+ `Content*`, `Card*`, seus tipos, arquivos, metadata e páginas de documentação deixam de existir, sem
71
+ aliases e sem período de depreciação. `MetricCard` e `ActionFormCard` permanecem como patterns
72
+ especializados e compõem `Surface` internamente.
73
+
74
+ A infraestrutura interna que `Page`, `Dialog`, `Drawer` e `Pane` compartilham muda de nome para
75
+ liberar o vocabulário público: `StructuralHeader` e `StructuralMedia`. Ela continua fora da API.
76
+
77
+ ### 2. A semântica vem do elemento, não do nome
78
+
79
+ `as` escolhe o elemento: `div` (default), `section`, `article` ou `fieldset`. `asChild` entrega a
80
+ superfície ao filho — um `button` ou um link — e é mutuamente exclusivo com `as`.
81
+
82
+ - `section` e `article` com header recebem `aria-labelledby` apontando para o título, salvo quando
83
+ `aria-label` é informado.
84
+ - `fieldset` transforma o header em `legend` nativa. A legenda aceita somente mídia e título;
85
+ contador, descrição e ações não nomeiam o grupo de campos. A mídia da legenda é decorativa.
86
+ - `div` e `asChild` não recebem nome acessível implícito: um `div` não é uma região.
87
+
88
+ `level` (1 a 6, default 2) pertence à raiz e escolhe a tag do título. O nível 1 mantém a
89
+ apresentação principal da página (ADR 0038); os demais usam a escala de seção. Nenhum layout altera
90
+ a escala do título.
91
+
92
+ ### 3. Três eixos visuais independentes
93
+
94
+ | Propriedade | Valores | Default | Decide |
95
+ | ----------- | ------------------------- | ------------ | ------------------------------------------------------------ |
96
+ | `framed` | `boolean` | `false` | Borda, raio (`rounded-xl`) e recorte do conteúdo seccionado. |
97
+ | `elevation` | `none`, `raised` | `none` | Fundo próprio e altura própria. |
98
+ | `layout` | `continuous`, `sectioned` | `continuous` | Ritmo contínuo ou faixas com divisores derivados. |
99
+
100
+ `elevation="raised"` pinta o par `bg-card text-card-foreground` e declara
101
+ `--surface-background: var(--card)`. `elevation="none"` não pinta fundo, não usa alpha e não
102
+ redeclara a variável: a superfície mostra o fundo de quem a hospeda, e `--surface-background`
103
+ continua valendo o da superfície elevada, do `Dialog` ou do `Drawer` mais próximo, com `--background`
104
+ como fallback na página. Um descendente que precise de uma cor opaca igual ao fundo visível lê
105
+ `var(--surface-background, var(--background))` em qualquer nível de aninhamento.
106
+
107
+ Uma superfície é **delimitada** quando tem moldura ou elevação. Dessa condição derivam o recuo e o
108
+ ritmo, que o consumidor não escreve:
109
+
110
+ - **Sem delimitação**, as regiões não têm recuo: a superfície flui alinhada ao hospedeiro, com
111
+ `0.75rem` entre header e body (`1.5rem` no nível 1).
112
+ - **Delimitada e contínua**, header, body e footer compartilham `1rem` de recuo lateral e `0.75rem`
113
+ de intervalo; a última região fecha com o mesmo respiro do topo.
114
+ - **Delimitada e seccionada**, header e footer são faixas, o body recua `1rem` e os divisores nascem
115
+ da vizinhança entre as regiões (ADR 0023, §4). `layout="sectioned"` exige uma superfície
116
+ delimitada.
117
+
118
+ Existe uma medida só. O recuo de `1.25rem` e o título um degrau maior do antigo `Card` contínuo
119
+ saem: eram diferenças atribuídas ao nome do componente.
120
+
121
+ `elevation="raised"` também encerra a cadeia de ocupação de altura (ADR 0024): uma superfície
122
+ elevada decide a própria altura. Sem elevação, `Surface` e `SurfaceBody` repassam a altura de
123
+ `Page layout="fill"` até a `ActionList`, como `Content` fazia.
124
+
125
+ ### 4. Três formas de compor
126
+
127
+ 1. **Caixa simples.** Sem título e sem slots estruturais, os filhos são livres e a raiz não aplica
128
+ recuo nem ritmo; o consumidor declara o que a caixa precisa.
129
+ 2. **Forma curta.** `title`, `description`, `count`, `actions` e `media` declaram o header. Os filhos
130
+ viram o body; quando a superfície precisa de rodapé ou de um body com propriedades, os filhos são
131
+ `SurfaceBody` e `SurfaceFooter` explícitos.
132
+ 3. **Composição explícita.** `SurfaceHeader`, `SurfaceBody` e `SurfaceFooter` como filhos diretos,
133
+ no máximo um de cada. Cada região é opcional.
134
+
135
+ A presença de um slot estrutural direto ativa a validação: slots estruturais não se misturam com
136
+ filhos livres, não se repetem, e a forma curta não convive com `SurfaceHeader`. `SurfaceHeader`
137
+ exige um `SurfaceTitle` e aceita no máximo um de cada slot restante. As formas curta e explícita
138
+ produzem os mesmos elementos e `data-slot`.
139
+
140
+ `SurfaceFooter` vale para qualquer superfície, inclusive uma região de página sem moldura.
141
+
142
+ `SurfaceBody` aceita `flush` para entregar a largura inteira a um filho que já controla o próprio
143
+ recuo, como `DetailGroup` com divisores, lista ou tabela. É a única propriedade de recuo da família
144
+ e existe porque `className` não remove o respiro condicional da última região.
145
+
146
+ ### 5. Moldura de fieldset
147
+
148
+ `frameVariant` deixa de existir. Em um `fieldset` com moldura e layout contínuo, a `legend` nativa
149
+ ocupa a borda, como o navegador já faz; não há acabamento equivalente para `section`. No layout
150
+ seccionado, a legenda é a faixa do header, dentro da moldura.
151
+
152
+ ### 6. Mídia é um slot, não uma moldura
153
+
154
+ `SurfaceMedia` é somente estrutura e layout: reserva a posição que abre a linha do header e
155
+ centraliza o conteúdo. Não cria `IconTile`, não aceita `size`, `context` ou `variant` e não
156
+ inspeciona o filho. A moldura é composição explícita — `SurfaceMedia` contendo `IconTile` —, e
157
+ `Avatar`, `img` ou um glifo entram diretamente. Isso devolve aos slots de mídia desta família a
158
+ regra original da ADR 0020.
159
+
160
+ ## Consequências
161
+
162
+ - A escolha passa a ser feita por pergunta: qual elemento, tem moldura, tem fundo, qual anatomia.
163
+ Combinações antes impossíveis, como `article` elevado com rodapé ou região de página com footer,
164
+ tornam-se diretas.
165
+ - Dezessete exports públicos dão lugar a nove; duas páginas de documentação, a uma.
166
+ - Toda tela que usa `Content` ou `Card` precisa migrar na mesma versão. O `opus check` reprova os
167
+ nomes removidos com a indicação do substituto.
168
+ - `Content` tinha `section` como default; `Surface` tem `div`. A migração de uma região nomeada
169
+ declara `as="section"` para preservar a região e o `aria-labelledby`.
170
+ - Cards contínuos ficam `0.25rem` mais compactos e com título em `text-sm`; regiões seccionadas
171
+ ganham recuo no body e passam a declarar `flush` quando o filho controla o recuo.
172
+ - Regiões `section` que usavam `frameVariant="legend"` perdem o header sobre a borda e passam à
173
+ moldura contínua ou seccionada.
174
+ - Mídias que dependiam da moldura automática passam a compor `IconTile`.
175
+ - A exigência de exatamente um header e um body de `Content` é relaxada: cada região é opcional.
176
+ - A decisão é de difícil reversão depois que os consumidores migrarem; por isso a release só ocorre
177
+ após a migração do consumidor, os gates e a revisão do candidato.
178
+
179
+ ## Alternativas consideradas
180
+
181
+ ### Manter Content e Card
182
+
183
+ Preserva todos os consumidores, mas mantém duas respostas para o mesmo desenho e a escolha por
184
+ aparência. Cada capacidade nova continuaria sendo implementada duas vezes ou faltando em um dos
185
+ lados, como footer em `Content` e elemento semântico em `Card`.
186
+
187
+ ### Surface como fundação e Content/Card como aliases
188
+
189
+ Reduziria o custo imediato da migração. Foi descartada porque os aliases perpetuariam dois
190
+ vocabulários na documentação, nas skills e no lint, e porque os defaults diferentes de cada alias
191
+ esconderiam justamente os eixos que a decisão torna explícitos.
192
+
193
+ ### Unificar apenas a implementação interna
194
+
195
+ É o que a ADR 0023 fez. Removeu a duplicação de classes, mas não a duplicação de decisão: o
196
+ consumidor continuou escolhendo entre dois nomes para a mesma estrutura.
197
+
198
+ ### Deixar `elevation="none"` transparente por classe ou alpha
199
+
200
+ Um `bg-transparent` ou um fundo com alpha faria a cor final depender de cada ancestral
201
+ intermediário, o que a ADR 0043 removeu do tema. Não pintar nada e manter a variável herdada
202
+ produz o mesmo resultado visual sem criar um token dependente do contexto.
203
+
204
+ ### Manter a moldura automática na mídia
205
+
206
+ Pouparia a composição de `IconTile` no caso do ícone. Foi descartada porque obrigava o slot a
207
+ inspecionar o filho, a repetir as props visuais de outro componente e a oferecer `asChild` como
208
+ saída para todo conteúdo que não fosse um glifo.
209
+
210
+ ### Derivar o body sem recuo do layout seccionado
211
+
212
+ Era a regra da ADR 0030 para `Content`, e o oposto da regra da ADR 0023 para `Card`. Escolher uma
213
+ delas por default implícito quebraria a outra metade dos usos; `flush` declara a intenção no slot
214
+ a que ela pertence.
215
+
216
+ ## Verificação
217
+
218
+ - Testes de `Surface` cobrem as três formas de composição, a equivalência entre forma curta e
219
+ explícita, `as` e `asChild`, `aria-labelledby`, nível do heading, `fieldset` e `legend`, os eixos
220
+ `framed`, `elevation` e `layout`, footer, `flush`, mídia sem moldura implícita e as combinações
221
+ inválidas.
222
+ - A suíte transversal de anatomia percorre `Surface`, `Dialog`, `Drawer` e `Pane` e mantém os
223
+ invariantes de recuo lateral, divisores derivados e escala do título.
224
+ - Os testes de ocupação de altura confirmam a cadeia `Page › PageBody › Surface › SurfaceBody ›
225
+ ActionList` e a interrupção por `elevation="raised"`.
226
+ - Um teste de API pública reprova qualquer export `Card*` ou `Content*` — exceto `MetricCard` e
227
+ `ActionFormCard` — e a exportação da infraestrutura interna.
228
+ - `opus check` reprova os componentes removidos, slots fora do pai e a mistura de formas.
229
+ - A documentação tem uma página canônica, `surface`; os endereços `card` e `content` redirecionam
230
+ para ela.
@@ -0,0 +1,103 @@
1
+ # ADR 0046 — O body estrutural governa o ritmo do conteúdo
2
+
3
+ > **Atualização em 2026-10-01:** a ADR 0047 remove `PaneBody` da API. O ritmo definido aqui vale
4
+ > para `SurfaceBody`, `DialogBody`, `DrawerBody` e `PageBody`.
5
+
6
+ - **Status:** aceita.
7
+ - **Data:** 2026-10-01.
8
+ - **Complementa:** ADR 0023, quanto à anatomia compartilhada das superfícies, e ADR 0045, quanto
9
+ ao recuo derivado de `Surface`.
10
+ - **Altera:** a liberdade anterior de sobrescrever recuo e ritmo diretamente em `*Body` por
11
+ `className`.
12
+
13
+ ## Contexto
14
+
15
+ `SurfaceBody`, `DialogBody`, `DrawerBody`, `PaneBody` e `PageBody` definiam a região, mas deixavam o
16
+ intervalo entre seus filhos para cada consumidor. Na prática, telas equivalentes passaram a usar
17
+ `space-y-3`, `space-y-4`, `space-y-5`, `space-y-6`, `gap-4` ou nenhum ritmo. O mesmo título inserido
18
+ no início de um drawer tinha `1rem` acima, herdado do body, e `1.5rem` abaixo, escrito pela tela.
19
+
20
+ O defeito não é uma margem isolada: o pai já conhece se a superfície é contínua ou seccionada,
21
+ delimitada ou fluida, mas essa informação parava antes de decidir o ritmo do conteúdo. Corrigir
22
+ cada tela mantém várias fontes de verdade e permite que a divergência reapareça.
23
+
24
+ Ao mesmo tempo, nem todo descendente deve usar o ritmo estrutural. Uma grade, uma linha horizontal
25
+ ou um grupo compacto tem relações internas próprias. O body precisa governar seus filhos diretos
26
+ sem impedir que um desses filhos organize o próprio interior.
27
+
28
+ ## Decisão
29
+
30
+ ### 1. O body é uma pilha estrutural
31
+
32
+ Todo body público compõe seus filhos diretos em coluna com intervalo de `1.5rem`. Esse é o ritmo
33
+ entre blocos da superfície: intro, resumo, ações, navegação e conteúdo. Um bloco que precise de
34
+ outro ritmo o declara num wrapper interno; ele não transforma o body inteiro em grid, linha ou
35
+ pilha compacta.
36
+
37
+ `PageBody` recebe o mesmo ritmo. O inset da página continua na raiz `Page`, pois `flow` e `fill`
38
+ descrevem ocupação de altura, não a anatomia `continuous`/`sectioned`.
39
+
40
+ ### 2. O pai decide o inset
41
+
42
+ `Surface`, `DialogContent` e `DrawerContent` entregam seu layout por contexto. O body deriva
43
+ dele o recuo:
44
+
45
+ - no layout `sectioned` de Surface, Dialog e Drawer, o corpo usa `1rem` nas laterais e `1.5rem` em
46
+ cima e embaixo;
47
+ - no layout `continuous`, o intervalo entre regiões adjacentes soma `1.5rem`; quando o body abre a
48
+ superfície, seu respiro superior também é `1.5rem`.
49
+
50
+ Header e footer permanecem faixas compactas. A simetria relevante deixa de ser “todas as regiões
51
+ têm o mesmo padding vertical” e passa a ser “o conteúdo tem o mesmo respiro antes, entre e depois
52
+ dos seus blocos”. O alinhamento lateral entre regiões continua invariável.
53
+
54
+ ### 3. Full bleed é explícito
55
+
56
+ `flush` remove o inset do body sem remover seu papel estrutural. `SurfaceBody` preserva a API já
57
+ existente; Dialog e Drawer passam a oferecer a mesma saída. É o caso de listas, tabelas ou
58
+ navegações que controlam o próprio recuo.
59
+
60
+ ### 4. Classes não reescrevem a anatomia
61
+
62
+ `opus check` reprova classes estáticas de padding, `gap-*` e `space-*` aplicadas diretamente aos
63
+ bodies públicos. A correção é uma destas:
64
+
65
+ - remover a classe e aceitar o ritmo estrutural;
66
+ - usar `flush` quando o filho controla o inset inteiro;
67
+ - mover grid, linha ou ritmo compacto para um wrapper filho.
68
+
69
+ Classes de ocupação, overflow, alinhamento e tipografia continuam permitidas. O diagnóstico é
70
+ restrito aos componentes importados da API pública do Opus, sem atribuir a regra a homônimos locais.
71
+
72
+ ## Consequências
73
+
74
+ - Introduções e blocos irmãos mantêm o mesmo ritmo em páginas, superfícies, dialogs e drawers.
75
+ - Alterar a densidade estrutural volta a ter uma única fonte na anatomia do Opus.
76
+ - Alguns wrappers internos passam a ser necessários, mas agora expressam uma relação local real.
77
+ - Consumidores precisam remover overrides existentes no mesmo upgrade; a mudança é incompatível e
78
+ integra a major 24 ainda não publicada.
79
+ - `flush` deixa de ser uma particularidade de Surface e vira o escape comum para full bleed.
80
+
81
+ ## Alternativas descartadas
82
+
83
+ ### Corrigir somente o intro do drawer
84
+
85
+ Resolveria o exemplo observado, mas manteria os demais bodies com ritmos divergentes e faria a
86
+ próxima composição repetir a decisão.
87
+
88
+ ### Criar uma prop `gap` no body
89
+
90
+ Transformaria uma invariância estrutural em escolha de cada tela. Grupos compactos já podem usar
91
+ um wrapper próprio, sem ampliar a API.
92
+
93
+ ### Proibir qualquer `className` no body
94
+
95
+ Impediria necessidades legítimas de altura, overflow, alinhamento e tipografia. O lint se limita
96
+ às classes que reescrevem inset e ritmo.
97
+
98
+ ## Verificação
99
+
100
+ - testes transversais comparam o ritmo e o inset dos bodies por família e layout;
101
+ - testes de `flush` confirmam a remoção do inset sem remover o gap estrutural;
102
+ - testes do `opus check` cobrem padding, gap e space, além de classes permitidas;
103
+ - o único consumidor remove overrides diretos e seus testes verificam as composições migradas.
@@ -0,0 +1,71 @@
1
+ # ADR 0047 — Pane é somente uma unidade de layout do Split
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-10-01.
5
+ - **Substitui:** a anatomia visual de Pane definida pela ADR 0023.
6
+ - **Complementa:** ADR 0045, quanto ao uso de `Surface` para regiões visuais.
7
+
8
+ ## Contexto
9
+
10
+ `Pane` nasceu como filho dimensionável de `Split`, mas passou a acumular uma segunda função:
11
+ oferecia header, body, footer, título, mídia, ações, divisores, inset e ritmo visual. Essa anatomia
12
+ duplicava `Surface` e fazia a aparência de uma coluna depender do componente que deveria apenas
13
+ distribuir espaço.
14
+
15
+ A duplicação ficou visível quando uma mesma região precisou funcionar em página, drawer e split.
16
+ Alterar o ritmo de todos os bodies incluiu `PaneBody`, embora a decisão vigente já fosse deixar a
17
+ superfície visual a cargo de `Surface` ou de um pattern especializado, como `Sidebar`.
18
+
19
+ ## Decisão
20
+
21
+ `Pane` representa exclusivamente uma unidade espacial aceita por `Split`. Sua API pública mantém:
22
+
23
+ - `id`, para identidade e persistência do layout;
24
+ - `initialSize`, `minSize` e `maxSize`, para dimensionamento;
25
+ - `grow`, para ocupar o espaço remanescente;
26
+ - `className` e `children`, para o contêiner de layout.
27
+
28
+ `Pane` não aplica inset, fundo, borda, raio, divisor, orientação interna, rolagem nem anatomia de
29
+ conteúdo. As propriedades `inset` e `layout` são removidas.
30
+
31
+ Os slots `PaneHeader`, `PaneBody`, `PaneContent`, `PaneFooter`, `PaneMedia`, `PaneTitle`,
32
+ `PaneDescription` e `PaneActions` também são removidos, sem aliases de compatibilidade. O único
33
+ consumidor é migrado atomicamente:
34
+
35
+ - regiões visuais usam `Surface` e seus slots;
36
+ - barras laterais podem usar a anatomia própria de `Sidebar` ou conteúdo nativo;
37
+ - rolagem e organização interna pertencem ao filho inserido no pane.
38
+
39
+ ## Consequências
40
+
41
+ - `Pane` e `Split` passam a ter uma única responsabilidade observável: distribuir espaço.
42
+ - `Surface` volta a ser a fonte única da anatomia visual compartilhada.
43
+ - Uma região que precise de rolagem deve declará-la no seu conteúdo ou na superfície interna.
44
+ - Consumidores antigos deixam de compilar até substituir os slots e remover `inset`/`layout`.
45
+ - O checker orienta a migração de símbolos e propriedades removidos.
46
+
47
+ ## Alternativas descartadas
48
+
49
+ ### Manter apenas PaneBody
50
+
51
+ Ainda deixaria `Pane` responsável por rolagem e ritmo, criando uma anatomia parcial concorrente com
52
+ `SurfaceBody` e com o conteúdo especializado.
53
+
54
+ ### Manter os slots como aliases de Surface
55
+
56
+ Preservaria dois vocabulários para a mesma estrutura e permitiria que novas telas voltassem a
57
+ escolher a família errada. Como há um único consumidor migrado junto, a compatibilidade não traz
58
+ benefício proporcional.
59
+
60
+ ### Incorporar a superfície visual ao Split
61
+
62
+ Misturaria distribuição espacial com apresentação e impediria panes deliberadamente invisíveis.
63
+
64
+ ## Verificação
65
+
66
+ - a API pública exporta apenas `Pane`, `PaneProps` e `PaneSize` dessa família;
67
+ - `Pane` não contém tokens de padding, borda, fundo, raio, overflow ou anatomia flex interna;
68
+ - `Split` continua aceitando somente `Pane` como filho e preserva dimensionamento e resize;
69
+ - `opus check` reprova símbolos antigos e as propriedades removidas;
70
+ - busca nos consumidores não encontra nenhum slot `Pane*`, `inset` ou `layout` aplicado a `Pane`;
71
+ - testes renderizados cobrem split fixo, redimensionável e superfícies migradas.
@@ -0,0 +1,34 @@
1
+ # ADR 0048 — A intro do modal separa conteúdo e chrome
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-10-01.
5
+ - **Complementa:** ADR 0018, quanto à separação entre nome e contexto; ADR 0023, quanto à anatomia
6
+ de Dialog e Drawer; ADR 0046, quanto ao ritmo governado pelo body.
7
+
8
+ ## Contexto
9
+
10
+ Dialog e Drawer podem ter uma barra persistente com navegação, ação global e fechamento. Nessa
11
+ composição, colocar o título do recurso na barra mistura chrome e conteúdo; colocá-lo solto no body
12
+ preserva o desenho, mas viola a anatomia pública e não nomeia a relação entre título e ações locais.
13
+
14
+ ## Decisão
15
+
16
+ `DialogIntro` e `DrawerIntro` são o primeiro bloco opcional de seus respectivos bodies. Cada intro
17
+ exige o título da família e aceita `DialogIntroActions` ou `DrawerIntroActions`. O título usa a
18
+ escala principal da composição; o body continua responsável por inset, ritmo e rolagem.
19
+
20
+ As ações do header continuam sendo chrome da superfície. As ações da intro se relacionam ao título
21
+ e ao conteúdo. No formato compacto, título e ações permanecem no header e a intro é omitida.
22
+
23
+ ## Consequências
24
+
25
+ - o mesmo conteúdo funciona em layout contínuo ou seccionado sem título órfão;
26
+ - o fechamento e a navegação podem permanecer estáveis enquanto a intro rola com o conteúdo;
27
+ - o checker exige intro dentro do body, título dentro de header ou intro e ações no slot correto;
28
+ - exemplos e consumidores deixam de reconstruir a linha com `div` e classes locais.
29
+
30
+ ## Verificação
31
+
32
+ - testes renderizados cobrem os dois modais e a escala do título;
33
+ - `opus check` aceita as composições canônicas e reprova intro órfã ou sem título;
34
+ - a documentação mostra as formas com intro e compacta.
@@ -115,7 +115,7 @@ O extrator cobre três superfícies, sem heurística de nome:
115
115
  reprovam em vez de serem executadas e o dicionário mantém um snapshot imutável das entradas;
116
116
  3. texto estático em filhos e props de uma allowlist de componentes importados diretamente
117
117
  de `@softize/opus/ui*` — por exemplo, `Button`, `DialogTitle`, `DialogBody`,
118
- `FieldLabel`, `TabsTrigger`, `Page`, `Input` e `DataState`. Alias e namespace de import
118
+ `FieldLabel`, `TabsListItem`, `Page`, `Input` e `DataState`. Alias e namespace de import
119
119
  continuam rastreáveis. `uppercase`, variantes como `sm:hover:uppercase`, modificadores
120
120
  `!uppercase`/`uppercase!` e classes em descendentes são projetados na superfície que
121
121
  realmente herda o CSS. Props específicas também respeitam a implementação: por exemplo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "22.2.0",
3
+ "version": "24.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",
@@ -205,6 +205,10 @@
205
205
  "types": "./src/ui/meta.ts",
206
206
  "default": "./src/ui/meta.ts"
207
207
  },
208
+ "./ui/appearance": {
209
+ "types": "./src/ui/appearance.ts",
210
+ "default": "./src/ui/appearance.ts"
211
+ },
208
212
  "./ui/docs": {
209
213
  "types": "./src/ui/docs/index.ts",
210
214
  "default": "./src/ui/docs/index.ts"
@@ -46,26 +46,26 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
46
46
  8. Compor superfícies pela gramática estrutural do catálogo: `PageHeader` é a única introdução
47
47
  estrutural e reúne navegação contextual opcional, título e ações; `PageBody` contém o conteúdo.
48
48
  Na composição explícita, declare exatamente um heading principal com `PageTitle` no header,
49
- `Content level={1}` ou um `PageState` ativo no body. `PageShell` pode fornecer navegação e ações
50
- conhecidas pela aplicação, mas não cria uma barra visual. `Content` contém `ContentHeader` e `ContentBody`; Card,
51
- Drawer e Pane usam seus
52
- respectivos `*Body`. Para o caso direto, usar a sintaxe abreviada de `Page` (`title`,
53
- `actions`) ou de `Content` (`title`, `description`, `actions` e `count`); não misturá-la com o
49
+ `Surface level={1}` ou um `PageState` ativo no body. `PageShell` pode fornecer navegação e ações
50
+ conhecidas pela aplicação, mas não cria uma barra visual. `Surface` contém `SurfaceHeader` e
51
+ `SurfaceBody`; Drawer e Pane usam seus respectivos `*Body`. Para o caso direto, usar a sintaxe
52
+ abreviada de `Page` (`title`, `actions`) ou de `Surface` (`title`, `description`, `actions` e
53
+ `count`); não misturá-la com o
54
54
  header explícito. O título da página não carrega contador. Ajustar o
55
55
  nível do heading pela hierarquia semântica, não pelo destaque visual. O `Page` mantém seu teto
56
56
  centralizado padrão de `80rem`.
57
- Em `Content framed`, usar `layout="sectioned"` quando o filho direto já controlar o recuo do
58
- body, como `DetailGroup` com divisores, lista ou tabela; não reconstruir divisor, padding ou
59
- recorte por classes no consumidor. `frameVariant="legend"` continua sendo um acabamento da
60
- borda contínua e não combina com o layout seccionado.
57
+ Em `Surface framed`, usar `layout="sectioned"` e `SurfaceBody flush` quando o filho direto já
58
+ controlar o recuo do body, como `DetailGroup` com divisores, lista ou tabela; não reconstruir
59
+ divisor, padding ou recorte por classes no consumidor. Em grupos de formulário,
60
+ `as="fieldset"` transforma o header em `legend` nativa sem variante visual paralela.
61
61
  9. Manter margem e posicionamento no consumidor; componente reutilizável controla apenas
62
62
  seu interior.
63
63
  10. Não definir a fonte raiz em uma biblioteca ou componente. O navegador e a aplicação são
64
64
  responsáveis por `font-size` em `html`; medidas escaláveis da UI usam `rem` ou a escala
65
65
  relativa do Tailwind. Reservar `px` a hairlines e compensações presas à geometria da borda,
66
66
  com justificativa e cobertura explícitas.
67
- 11. Em uma página centrada em coleção, compor `Content + ActionList`: título e criação ficam em
68
- extremos opostos do `ContentHeader`; a criação usa botão textual `default`, sem ícone, com
67
+ 11. Em uma página centrada em coleção, compor `Surface + ActionList`: título e criação ficam em
68
+ extremos opostos do `SurfaceHeader`; a criação usa botão textual `default`, sem ícone, com
69
69
  `Criar recurso`. Busca, filtros, atualização, estados e paginação permanecem no `ActionList`.
70
70
  Reservar `toolbarActions` a operações ligadas ao recorte atual.
71
71
  12. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.