@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.
- package/CHANGELOG.md +99 -0
- package/bin/cli.mjs +2 -0
- package/bin/lib/check.mjs +241 -42
- package/bin/lib/copy.mjs +16 -34
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
- package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
- package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
- package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
- package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
- package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
- package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
- package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
- package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
- package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
- package/docs/adr/0049-inline-navigation-groups-with-the-heading.md +95 -0
- package/package.json +5 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
- package/registry/templates/app/src/App.tsx +24 -12
- package/src/ui/appearance.ts +149 -0
- package/src/ui/components/patterns/action-form-card.tsx +46 -33
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
- package/src/ui/components/patterns/confirm.tsx +3 -1
- package/src/ui/components/patterns/fill-region.ts +8 -7
- package/src/ui/components/patterns/form-dialog.tsx +1 -3
- package/src/ui/components/patterns/form.tsx +8 -7
- package/src/ui/components/patterns/list.tsx +15 -19
- package/src/ui/components/patterns/page.tsx +15 -11
- package/src/ui/components/patterns/pane.tsx +9 -240
- package/src/ui/components/patterns/presentation.tsx +14 -14
- package/src/ui/components/patterns/sidebar.tsx +7 -17
- package/src/ui/components/patterns/split.tsx +111 -59
- package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +90 -27
- package/src/ui/components/patterns/structural-media.tsx +26 -0
- package/src/ui/components/patterns/surface-anatomy.ts +71 -61
- package/src/ui/components/patterns/surface-assistant.tsx +2 -4
- package/src/ui/components/patterns/surface.tsx +620 -0
- package/src/ui/components/patterns/trigger.tsx +11 -9
- package/src/ui/components/primitives/ask.tsx +155 -83
- package/src/ui/components/primitives/button.tsx +11 -9
- package/src/ui/components/primitives/checkbox.tsx +8 -8
- package/src/ui/components/primitives/command.tsx +1 -1
- package/src/ui/components/primitives/composer.tsx +47 -45
- package/src/ui/components/primitives/detail.tsx +9 -2
- package/src/ui/components/primitives/dialog.tsx +60 -26
- package/src/ui/components/primitives/drawer.tsx +58 -12
- package/src/ui/components/primitives/icon-picker.tsx +90 -73
- package/src/ui/components/primitives/input-group.tsx +35 -28
- package/src/ui/components/primitives/input-otp.tsx +16 -16
- package/src/ui/components/primitives/input.tsx +20 -10
- package/src/ui/components/primitives/item.tsx +9 -12
- package/src/ui/components/primitives/menu.tsx +44 -65
- package/src/ui/components/primitives/metric-card.tsx +75 -59
- package/src/ui/components/primitives/radio-group.tsx +9 -9
- package/src/ui/components/primitives/select.tsx +344 -209
- package/src/ui/components/primitives/switch.tsx +10 -10
- package/src/ui/components/primitives/table.tsx +29 -24
- package/src/ui/components/primitives/tabs.tsx +4 -2
- package/src/ui/components/primitives/textarea.tsx +6 -6
- package/src/ui/components/primitives/toggle-group-context.ts +24 -0
- package/src/ui/components/primitives/toggle-group.tsx +116 -74
- package/src/ui/components/primitives/toggle.tsx +82 -16
- package/src/ui/docs/DocBrowser.tsx +77 -69
- package/src/ui/docs/content/action-form-card.md +9 -9
- package/src/ui/docs/content/action-list.md +76 -70
- package/src/ui/docs/content/action-view.md +33 -18
- package/src/ui/docs/content/button.md +1 -1
- package/src/ui/docs/content/communication.md +1 -1
- package/src/ui/docs/content/compositions.md +479 -0
- package/src/ui/docs/content/customization.md +64 -11
- package/src/ui/docs/content/detail.md +8 -6
- package/src/ui/docs/content/dialog.md +105 -60
- package/src/ui/docs/content/drawer.md +68 -22
- package/src/ui/docs/content/field.md +14 -15
- package/src/ui/docs/content/icon-tile.md +14 -13
- package/src/ui/docs/content/page.md +44 -34
- package/src/ui/docs/content/pane.md +42 -290
- package/src/ui/docs/content/presentation.md +3 -3
- package/src/ui/docs/content/separator.md +6 -6
- package/src/ui/docs/content/sidebar.md +68 -266
- package/src/ui/docs/content/skeleton.md +20 -18
- package/src/ui/docs/content/split.md +14 -14
- package/src/ui/docs/content/surface.md +513 -0
- package/src/ui/docs/content/tabs.md +20 -20
- package/src/ui/docs/content/toggle.md +117 -65
- package/src/ui/docs/content/tokens.md +124 -40
- package/src/ui/docs/registry.tsx +10 -6
- package/src/ui/index.ts +30 -27
- package/src/ui/meta.ts +15 -27
- package/src/ui/react.tsx +40 -43
- package/src/ui/theme.css +311 -64
- package/src/ui/components/patterns/content-header.tsx +0 -528
- package/src/ui/components/patterns/surface-media.tsx +0 -69
- package/src/ui/components/primitives/card.tsx +0 -191
- package/src/ui/docs/content/card.md +0 -292
- 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.
|