@softize/opus 23.0.0 → 24.0.1

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 (97) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/bin/cli.mjs +2 -0
  3. package/bin/lib/check.mjs +241 -42
  4. package/bin/lib/copy.mjs +16 -34
  5. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  6. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
  7. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
  8. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  9. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  10. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  11. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  12. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  13. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  14. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  15. package/docs/adr/0049-inline-navigation-groups-with-the-heading.md +95 -0
  16. package/package.json +5 -1
  17. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  18. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  19. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  20. package/registry/templates/app/src/App.tsx +24 -12
  21. package/src/ui/appearance.ts +149 -0
  22. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  23. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  24. package/src/ui/components/patterns/confirm.tsx +3 -1
  25. package/src/ui/components/patterns/fill-region.ts +8 -7
  26. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  27. package/src/ui/components/patterns/form.tsx +8 -7
  28. package/src/ui/components/patterns/list.tsx +15 -19
  29. package/src/ui/components/patterns/page.tsx +15 -11
  30. package/src/ui/components/patterns/pane.tsx +9 -240
  31. package/src/ui/components/patterns/presentation.tsx +14 -14
  32. package/src/ui/components/patterns/sidebar.tsx +7 -17
  33. package/src/ui/components/patterns/split.tsx +111 -59
  34. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +90 -27
  35. package/src/ui/components/patterns/structural-media.tsx +26 -0
  36. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  37. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  38. package/src/ui/components/patterns/surface.tsx +620 -0
  39. package/src/ui/components/patterns/trigger.tsx +11 -9
  40. package/src/ui/components/primitives/ask.tsx +155 -83
  41. package/src/ui/components/primitives/button.tsx +11 -9
  42. package/src/ui/components/primitives/checkbox.tsx +8 -8
  43. package/src/ui/components/primitives/command.tsx +1 -1
  44. package/src/ui/components/primitives/composer.tsx +47 -45
  45. package/src/ui/components/primitives/detail.tsx +9 -2
  46. package/src/ui/components/primitives/dialog.tsx +60 -26
  47. package/src/ui/components/primitives/drawer.tsx +58 -12
  48. package/src/ui/components/primitives/icon-picker.tsx +90 -73
  49. package/src/ui/components/primitives/input-group.tsx +35 -28
  50. package/src/ui/components/primitives/input-otp.tsx +16 -16
  51. package/src/ui/components/primitives/input.tsx +20 -10
  52. package/src/ui/components/primitives/item.tsx +9 -12
  53. package/src/ui/components/primitives/menu.tsx +44 -65
  54. package/src/ui/components/primitives/metric-card.tsx +75 -59
  55. package/src/ui/components/primitives/radio-group.tsx +9 -9
  56. package/src/ui/components/primitives/select.tsx +344 -209
  57. package/src/ui/components/primitives/switch.tsx +10 -10
  58. package/src/ui/components/primitives/table.tsx +29 -24
  59. package/src/ui/components/primitives/tabs.tsx +4 -2
  60. package/src/ui/components/primitives/textarea.tsx +6 -6
  61. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  62. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  63. package/src/ui/components/primitives/toggle.tsx +82 -16
  64. package/src/ui/docs/DocBrowser.tsx +77 -69
  65. package/src/ui/docs/content/action-form-card.md +9 -9
  66. package/src/ui/docs/content/action-list.md +76 -70
  67. package/src/ui/docs/content/action-view.md +33 -18
  68. package/src/ui/docs/content/button.md +1 -1
  69. package/src/ui/docs/content/communication.md +1 -1
  70. package/src/ui/docs/content/compositions.md +479 -0
  71. package/src/ui/docs/content/customization.md +64 -11
  72. package/src/ui/docs/content/detail.md +8 -6
  73. package/src/ui/docs/content/dialog.md +105 -60
  74. package/src/ui/docs/content/drawer.md +68 -22
  75. package/src/ui/docs/content/field.md +14 -15
  76. package/src/ui/docs/content/icon-tile.md +14 -13
  77. package/src/ui/docs/content/page.md +44 -34
  78. package/src/ui/docs/content/pane.md +42 -290
  79. package/src/ui/docs/content/presentation.md +3 -3
  80. package/src/ui/docs/content/separator.md +6 -6
  81. package/src/ui/docs/content/sidebar.md +68 -266
  82. package/src/ui/docs/content/skeleton.md +20 -18
  83. package/src/ui/docs/content/split.md +14 -14
  84. package/src/ui/docs/content/surface.md +513 -0
  85. package/src/ui/docs/content/tabs.md +20 -20
  86. package/src/ui/docs/content/toggle.md +117 -65
  87. package/src/ui/docs/content/tokens.md +124 -40
  88. package/src/ui/docs/registry.tsx +10 -6
  89. package/src/ui/index.ts +30 -27
  90. package/src/ui/meta.ts +15 -27
  91. package/src/ui/react.tsx +40 -43
  92. package/src/ui/theme.css +311 -64
  93. package/src/ui/components/patterns/content-header.tsx +0 -528
  94. package/src/ui/components/patterns/surface-media.tsx +0 -69
  95. package/src/ui/components/primitives/card.tsx +0 -191
  96. package/src/ui/docs/content/card.md +0 -292
  97. package/src/ui/docs/content/content.md +0 -275
@@ -0,0 +1,79 @@
1
+ # ADR 0042 — ToggleGroup compartilha a gramática de controles agrupados
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-30.
5
+ - **Substitui parcialmente:** ADR 0040 quanto à composição e à anatomia pública de `ToggleGroup`.
6
+
7
+ ## Contexto
8
+
9
+ `ButtonGroup` expressa a relação entre controles com `mode`, distribui largura com `distribution` e
10
+ mantém orientação, forma e moldura como eixos independentes. `ToggleGroup` resolvia necessidades
11
+ equivalentes por outra gramática: `spacing` controlava indiretamente se os itens pareciam conectados,
12
+ e cada controle era exposto como `ToggleGroupItem`, embora o controle isolado se chamasse `Toggle`.
13
+
14
+ Essa diferença ficou mais visível ao buscar uma apresentação segmentada para filtros. Criar
15
+ `appearance="segmented"` repetiria o significado visual de `variant`; criar
16
+ `variant="segmented"` misturaria o tratamento de cada controle com a relação entre os controles.
17
+ Nenhuma opção aproveitaria a gramática que o catálogo já usa para grupos de botões.
18
+
19
+ ## Decisão
20
+
21
+ `ToggleGroup` compartilha com `ButtonGroup` os eixos de composição:
22
+
23
+ - `mode="connected" | "spaced" | "divided" | "segmented"` define a relação entre controles;
24
+ - `distribution="content" | "equal"` preserva a largura dos rótulos ou reparte a largura disponível;
25
+ - `orientation`, `shape` e `framed` mantêm seus significados estruturais;
26
+ - `variant` e `size` continuam tratando cada `Toggle` e podem ser declarados no grupo para chegar
27
+ aos controles por contexto.
28
+
29
+ `connected` substitui o `spacing={0}` implícito; `spaced` substitui valores positivos de `spacing`;
30
+ `divided` insere divisores sem incluí-los na distribuição; `segmented` cria uma superfície
31
+ compartilhada, destaca a seleção dentro dela e separa somente segmentos inativos vizinhos.
32
+ `spacing` sai do contrato.
33
+
34
+ A anatomia pública passa a usar `ToggleGroup + Toggle`, em paralelo a `ButtonGroup + Button`.
35
+ Dentro do grupo, `Toggle` exige `value` e participa da seleção coordenada; fora dele, mantém
36
+ `pressed`, `defaultPressed` e `onPressedChange`. `ToggleGroupItem` sai sem alias porque existe um
37
+ único consumidor e a migração é atômica.
38
+
39
+ Filtros `select` declarados com `presentation: "toggle"` usam `mode="segmented"`. A propriedade do
40
+ filtro continua escolhendo entre `Select` e `ToggleGroup`; estado, URL, defaults e input da action
41
+ não mudam.
42
+
43
+ ## Consequências
44
+
45
+ - grupos de ações e grupos de seleção compartilham vocabulário para relação, distribuição e forma;
46
+ - `segmented` fica no eixo que descreve a relação entre controles, sem criar um sinônimo de
47
+ `variant`;
48
+ - a mesma unidade pública, `Toggle`, funciona isolada ou sob seleção coordenada;
49
+ - a ausência de `value` em um `Toggle` dentro do grupo é recusada em runtime, pois JSX não permite
50
+ que o tipo do filho dependa estaticamente do pai;
51
+ - consumidores migram `ToggleGroupItem` para `Toggle` e `spacing` para o `mode` correspondente;
52
+ - filtros toggle mudam apenas de acabamento visual e preservam o contrato transportado.
53
+
54
+ ## Alternativas descartadas
55
+
56
+ ### `appearance="segmented"`
57
+
58
+ Separaria a composição do grupo, mas criaria um eixo visual sinônimo de `variant` sem organizar as
59
+ outras diferenças já existentes entre `ButtonGroup` e `ToggleGroup`.
60
+
61
+ ### `variant="segmented"`
62
+
63
+ Seria curto, porém faria `variant` responder ao mesmo tempo pelo tratamento do controle e pela
64
+ relação entre controles. Também deixaria `spacing` e `distribution` sem uma gramática comum.
65
+
66
+ ### Manter `ToggleGroupItem`
67
+
68
+ Preservaria a exigência estática de `value`, mas manteria uma anatomia diferente de
69
+ `ButtonGroup + Button` para conceitos equivalentes. A verificação em runtime é um custo localizado
70
+ e produz uma mensagem direta no uso inválido.
71
+
72
+ ## Verificação
73
+
74
+ - o tipo público de `ToggleGroup` oferece os quatro modos e as duas distribuições, sem `spacing`;
75
+ - `ToggleGroupItem` deixa de ser exportado e os consumidores usam `Toggle`;
76
+ - testes cobrem os modos, distribuição igual, orientação dos divisores e exigência de `value`;
77
+ - testes do `ActionFilterBar` confirmam que `presentation: "toggle"` usa `mode="segmented"` sem
78
+ alterar o valor transportado;
79
+ - documentação e metadata distinguem escolha com `ToggleGroup` de navegação com `Tabs`.
@@ -0,0 +1,73 @@
1
+ # ADR 0043 — Superfícies e interações neutras usam tokens opacos por papel
2
+
3
+ - **Status:** aceita.
4
+ - **Data:** 2026-09-30.
5
+ - **Relacionadas:** ADR 0023, quanto à anatomia das superfícies; ADR 0042, quanto aos controles
6
+ segmentados.
7
+
8
+ ## Contexto
9
+
10
+ O tema claro usava o mesmo branco em `background` e `card`, enquanto o escuro elevava cards sobre
11
+ um canvas mais escuro. Estados neutros também eram construídos com combinações como `muted/30`,
12
+ `muted/50`, `background/60`, `brightness` e correções `dark:` nos componentes.
13
+
14
+ Uma cor translúcida não representa um resultado: ela depende da superfície que estiver atrás.
15
+ Consequentemente, a mesma classe podia funcionar dentro de um card e desaparecer no canvas. Quem
16
+ compunha uma tela no tema claro não conseguia presumir a hierarquia nem o comportamento no escuro;
17
+ cada contexto precisava ser inspecionado.
18
+
19
+ ## Decisão
20
+
21
+ O tema declara uma escala neutra opaca por papel:
22
+
23
+ - `background` é o canvas da aplicação;
24
+ - `card` e `popover` são superfícies elevadas;
25
+ - `control` é a superfície de inputs e controles editáveis;
26
+ - `interaction-hover`, `interaction-pressed` e `interaction-selected` são estados de interação.
27
+
28
+ Os dois temas preservam a mesma ordem comportamental: superfície em repouso, hover, seleção e
29
+ pressão ganham contraste progressivo. Os valores podem avançar em direções luminosas diferentes,
30
+ mas o componente usa o mesmo token e a mesma classe nos dois temas. `accent` passa a apontar para
31
+ `interaction-hover`, preservando a integração com primitives externas que esperam esse nome.
32
+
33
+ Estados neutros essenciais não são obtidos com alpha sobre uma superfície desconhecida. Alpha
34
+ continua permitido para decoração, feedback semântico colorido, overlays e efeitos cuja mistura é
35
+ parte deliberada do resultado.
36
+
37
+ No tema claro, cards e popovers permanecem brancos, enquanto o canvas recebe um cinza quase branco.
38
+ No escuro, a hierarquia existente é preservada. A distinção entre superfícies não substitui borda,
39
+ foco nem texto quando esses sinais forem necessários para identificar um controle.
40
+
41
+ ## Consequências
42
+
43
+ - componentes deixam de carregar receitas `dark:` para estados neutros equivalentes;
44
+ - mover um controle entre canvas, card e modal não muda a cor resultante de seu estado;
45
+ - hover, pressed e selected deixam de disputar o significado de `muted`;
46
+ - o tema pode ser calibrado sem editar cada primitive;
47
+ - light e dark ainda exigem verificação visual, mas compartilham a mesma gramática e deixam de
48
+ depender da superfície ancestral para produzir seus estados essenciais.
49
+
50
+ ## Alternativas descartadas
51
+
52
+ ### Manter transparências e calibrar por componente
53
+
54
+ Preservaria menos tokens, mas manteria o resultado dependente do ancestral e multiplicaria exceções
55
+ por tema.
56
+
57
+ ### Tornar o card cinza sobre canvas branco
58
+
59
+ Criaria contraste, porém inverteria a relação de elevação já consolidada no escuro e daria ao card
60
+ um peso visual desnecessário. O canvas quase branco com superfícies brancas preserva a elevação sem
61
+ transformar a aplicação numa coleção de blocos cinza.
62
+
63
+ ### Usar somente `accent` para todos os estados
64
+
65
+ Reduziria o vocabulário, mas hover transitório, seleção persistente e pressão deixariam de ser
66
+ distinguíveis e voltariam a depender de modifiers locais.
67
+
68
+ ## Verificação
69
+
70
+ - o tema publica os quatro papéis opacos (`control` e os três estados de interação) nos dois temas;
71
+ - primitives neutras usam os mesmos tokens em light e dark, sem receitas locais equivalentes;
72
+ - testes cobrem a projeção dos tokens e os estados de tabela, toggle, menu e navegação lateral;
73
+ - a aplicação consumidora é compilada e verificada em páginas com canvas, card, formulário e grid.
@@ -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.