@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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,105 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 24.0.1 — 2026-10-01
|
|
11
|
+
|
|
12
|
+
Em `DialogHeader` e `DrawerHeader` com navegação e título, os dois passam a formar um grupo com
|
|
13
|
+
intervalo de `0.5rem`. Antes, o voltar ou o recolher ficava a `1rem` do título, a mesma distância
|
|
14
|
+
que separa o título das ações. As ações e o botão de fechar continuam fora do grupo, separados pelo
|
|
15
|
+
intervalo da família, e a mídia mantém a distância que já tinha até o título (ADR 0049). A API não
|
|
16
|
+
muda. Nessa combinação, a navegação passa a ficar dentro de `dialog-header-context` ou
|
|
17
|
+
`drawer-header-context`; seletores que exigiam um filho direto de `dialog-header-row` ou
|
|
18
|
+
`drawer-header-row` precisam usar o descendente, e seletores pelo `data-slot` da navegação
|
|
19
|
+
continuam válidos.
|
|
20
|
+
|
|
21
|
+
A documentação ganha a página **UI > Design > Composições**, que mostra como montar `Page`,
|
|
22
|
+
`Dialog`, `Drawer` e `Surface` com cabeçalho, corpo e rodapé, onde cada ação fica e quando o título
|
|
23
|
+
vai no cabeçalho ou na intro. A mesma tarefa aparece em cada superfície; `Dialog` e `Drawer`
|
|
24
|
+
aparecem nos layouts `sectioned` e `continuous`, e um exemplo de `Pane` mostra a `Surface` completa
|
|
25
|
+
dentro de um `Split`. As páginas de `Page`, `Dialog`, `Drawer` e `Pane` apontam para a nova
|
|
26
|
+
referência no ponto em que a escolha aparece.
|
|
27
|
+
|
|
28
|
+
## 24.0.0 — 2026-10-01
|
|
29
|
+
|
|
30
|
+
`ToggleGroup` passa a compartilhar a gramática estrutural de `ButtonGroup`: `mode` aceita
|
|
31
|
+
`connected`, `spaced`, `divided` e o novo `segmented`; `distribution` aceita `content` e `equal`;
|
|
32
|
+
`orientation`, `shape` e `framed` permanecem eixos independentes. A aparência segmentada mantém
|
|
33
|
+
escolhas curtas numa superfície compartilhada e destaca o controle ativo dentro dela. Filtros
|
|
34
|
+
`select` com `presentation: "toggle"` passam a usar esse modo; estado, URL, defaults e input da
|
|
35
|
+
action não mudam.
|
|
36
|
+
|
|
37
|
+
O tema passa a distinguir canvas, superfícies elevadas, controles e estados neutros com tokens
|
|
38
|
+
opacos por papel. No tema claro, o canvas quase branco sustenta cards e popovers brancos; inputs,
|
|
39
|
+
hover, seleção e pressão deixam de depender de alpha sobre a superfície ancestral. Os componentes
|
|
40
|
+
usam os mesmos papéis nos dois temas, enquanto cada tema calibra somente os valores.
|
|
41
|
+
|
|
42
|
+
A escala neutra passa a ser derivada de quatro eixos de aparência por modo — intensidade de cor,
|
|
43
|
+
contraste, brilho das superfícies e contraste entre camadas —, cada um de 0 a 100, lidos de
|
|
44
|
+
`--appearance-light-*` e `--appearance-dark-*` (ADR 0044). Com os eixos no preset, o tema é
|
|
45
|
+
exatamente o calibrado; quem não personaliza não precisa mudar nada. Os limites impedem que um
|
|
46
|
+
ajuste reduza a separação entre os papéis ou o contraste do texto sobre as superfícies em repouso
|
|
47
|
+
abaixo do preset, e a intensidade tinge somente os neutros, no matiz de `--neutral-hue`. O novo
|
|
48
|
+
módulo sem React `@softize/opus/ui/appearance`, também reexportado por `@softize/opus/ui` e
|
|
49
|
+
`@softize/opus/ui/react`, publica tipos, presets e o helper que converte overrides parciais em
|
|
50
|
+
custom properties. Reset é a remoção dos overrides pela aplicação; o Opus não publica componente
|
|
51
|
+
de ajuste nem guarda a preferência. Um app que redeclara diretamente um token neutro continua
|
|
52
|
+
funcionando, mas esse token deixa de acompanhar os eixos.
|
|
53
|
+
|
|
54
|
+
No preset, seleção persistente também se afasta mais do hover transitório nos dois temas. Navegação
|
|
55
|
+
ativa permanece reconhecível mesmo sem depender de texto em negrito, enquanto pressão conserva um
|
|
56
|
+
degrau próprio além da seleção.
|
|
57
|
+
|
|
58
|
+
`ToggleGroup mode="segmented"` usa `bg-card` no item ativo, e `Tabs framed` aproxima a linha dos
|
|
59
|
+
triggers com inset próprio. `PageHeader`, `DialogHeader` e `DrawerHeader` aceitam navegação e ações
|
|
60
|
+
sem título; `SurfaceHeader` aceita ações sem título. O título pode permanecer no conteúdo de modais
|
|
61
|
+
por meio das novas `DialogIntro` e `DrawerIntro`. `DetailGroup` ganha `elevation` e passa a usar
|
|
62
|
+
`raised` por padrão, inclusive em `plain`; declare `elevation="none"` quando o agrupamento tiver de
|
|
63
|
+
permanecer no mesmo plano do pai.
|
|
64
|
+
|
|
65
|
+
### Breaking
|
|
66
|
+
|
|
67
|
+
`ToggleGroupItem` foi removido. Use `Toggle` como filho de `ToggleGroup` e informe `value`; fora do
|
|
68
|
+
grupo, `Toggle` continua usando `pressed`, `defaultPressed` e `onPressedChange`. `spacing` também foi
|
|
69
|
+
removido: use `mode="connected"` no lugar de zero e `mode="spaced"` no lugar de valores positivos.
|
|
70
|
+
Não há aliases de compatibilidade porque o único consumidor é migrado atomicamente.
|
|
71
|
+
|
|
72
|
+
`Content*` e `Card*` foram removidos da API pública. Use `Surface` e seus slots
|
|
73
|
+
`SurfaceHeader`, `SurfaceMedia`, `SurfaceTitle`, `SurfaceDescription`, `SurfaceMeta`,
|
|
74
|
+
`SurfaceActions`, `SurfaceBody` e `SurfaceFooter`. A raiz agora é `div` por padrão; declare
|
|
75
|
+
`as="section"`, `as="article"` ou `as="fieldset"` quando o documento exigir essa semântica.
|
|
76
|
+
`framed` acrescenta borda, `elevation="raised"` acrescenta o fundo elevado e
|
|
77
|
+
`layout="sectioned"` cria as faixas com divisores. `frameVariant` não existe mais.
|
|
78
|
+
|
|
79
|
+
`SurfaceMedia` reserva posição e não cria `IconTile`: componha o tile explicitamente quando o
|
|
80
|
+
ícone precisar de moldura. O recuo estrutural de superfícies limitadas passa de `1.25rem` para
|
|
81
|
+
`1rem`, títulos internos usam `text-sm` e conteúdos que já controlam o próprio recuo declaram
|
|
82
|
+
`flush` em `SurfaceBody`. Essas mudanças são intencionais e não têm aliases de compatibilidade.
|
|
83
|
+
|
|
84
|
+
`DialogMedia` e `DrawerMedia` seguem o mesmo contrato sem decoração de
|
|
85
|
+
`SurfaceMedia`. As propriedades `size`, `context`, `variant` e `asChild` foram removidas desses
|
|
86
|
+
slots: passe o glifo, a imagem ou a marca diretamente; quando precisar de moldura, componha
|
|
87
|
+
`IconTile` com essas propriedades. `Dialog mode="alert"` também deixa de aumentar ou emoldurar a
|
|
88
|
+
mídia automaticamente.
|
|
89
|
+
|
|
90
|
+
Bodies estruturais passam a governar o ritmo entre seus filhos diretos com intervalo de `1.5rem`.
|
|
91
|
+
O layout do pai decide o inset; no seccionado, Surface, Dialog e Drawer usam `1rem` lateral e
|
|
92
|
+
`1.5rem` vertical. `DialogBody` e `DrawerBody` ganham `flush`, como `SurfaceBody`.
|
|
93
|
+
Migração: remova `space-*`, `gap-*` e padding de `className` no body; use um wrapper para relações
|
|
94
|
+
internas e `flush` somente quando o filho controlar o próprio inset. `opus check` cobra a regra.
|
|
95
|
+
|
|
96
|
+
`Pane` passa a ser somente a unidade de dimensionamento de `Split`. As propriedades `inset` e
|
|
97
|
+
`layout`, além de `PaneHeader`, `PaneBody`, `PaneContent`, `PaneFooter`, `PaneMedia`, `PaneTitle`,
|
|
98
|
+
`PaneDescription` e `PaneActions`, foram removidas sem aliases. Migração: componha uma `Surface`
|
|
99
|
+
dentro do pane quando precisar de anatomia visual; para navegação ou conteúdo especializado, deixe
|
|
100
|
+
o próprio filho declarar organização e rolagem.
|
|
101
|
+
|
|
102
|
+
### Novidades
|
|
103
|
+
|
|
104
|
+
`DialogIntro` e `DrawerIntro` formalizam o título do conteúdo quando o header do modal é reservado
|
|
105
|
+
a navegação, ações globais e fechamento. Cada intro exige o título da família e aceita
|
|
106
|
+
`DialogIntroActions` ou `DrawerIntroActions`; no formato compacto, título e ações continuam no
|
|
107
|
+
header. O body permanece dono do ritmo e da rolagem (ADR 0048).
|
|
108
|
+
|
|
10
109
|
## 23.0.0 — 2026-09-29
|
|
11
110
|
|
|
12
111
|
Esta major conclui a separação entre contrato e dados de design, simplifica a hierarquia de
|
package/bin/cli.mjs
CHANGED
|
@@ -433,6 +433,8 @@ Regras da UI (@softize/opus/ui):
|
|
|
433
433
|
ui-structure-split — filho direto de Split que não é Pane (elemento HTML, texto,
|
|
434
434
|
outro componente Opus ou componente do próprio arquivo); o
|
|
435
435
|
Split lança erro ao renderizá-lo
|
|
436
|
+
ui-structure-body-rhythm — padding, gap ou space aplicado diretamente a um Body;
|
|
437
|
+
use o ritmo nativo, flush ou um wrapper filho
|
|
436
438
|
ui-semantic-api — API semântica: \`context\` + \`variant\`; reprova aliases de
|
|
437
439
|
migração e \`tone\` em novos dicionários
|
|
438
440
|
removed-ui-token — token público removido (ring-edge, shadow-card…) com a
|
package/bin/lib/check.mjs
CHANGED
|
@@ -128,40 +128,34 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
128
128
|
["PageBack", new Set(["PageHeader"])],
|
|
129
129
|
["PageNavigation", new Set(["PageHeader"])],
|
|
130
130
|
["PageActions", new Set(["PageHeader"])],
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
["
|
|
134
|
-
["
|
|
135
|
-
["
|
|
136
|
-
["
|
|
137
|
-
["
|
|
138
|
-
["
|
|
139
|
-
["
|
|
140
|
-
["
|
|
141
|
-
["CardTitle", new Set(["CardHeader"])],
|
|
142
|
-
["CardDescription", new Set(["CardHeader"])],
|
|
143
|
-
["CardAction", new Set(["CardHeader"])],
|
|
131
|
+
// As regiões de Surface não são estritas: com `asChild`, elas ficam dentro do elemento que
|
|
132
|
+
// recebe a superfície (ADR 0045). Os slots do header são filhos diretos dele.
|
|
133
|
+
["SurfaceHeader", new Set(["Surface"])],
|
|
134
|
+
["SurfaceBody", new Set(["Surface"])],
|
|
135
|
+
["SurfaceFooter", new Set(["Surface"])],
|
|
136
|
+
["SurfaceMedia", new Set(["SurfaceHeader"])],
|
|
137
|
+
["SurfaceTitle", new Set(["SurfaceHeader"])],
|
|
138
|
+
["SurfaceDescription", new Set(["SurfaceHeader"])],
|
|
139
|
+
["SurfaceMeta", new Set(["SurfaceHeader"])],
|
|
140
|
+
["SurfaceActions", new Set(["SurfaceHeader"])],
|
|
144
141
|
["DialogHeader", new Set(["DialogContent"])],
|
|
145
142
|
["DialogBody", new Set(["DialogContent"])],
|
|
146
143
|
["DialogFooter", new Set(["DialogContent"])],
|
|
144
|
+
["DialogIntro", new Set(["DialogBody"])],
|
|
145
|
+
["DialogIntroActions", new Set(["DialogIntro"])],
|
|
147
146
|
["DialogNavigation", new Set(["DialogHeader"])],
|
|
148
147
|
["DialogMedia", new Set(["DialogHeader"])],
|
|
149
|
-
["DialogTitle", new Set(["DialogHeader"])],
|
|
148
|
+
["DialogTitle", new Set(["DialogHeader", "DialogIntro"])],
|
|
150
149
|
["DialogActions", new Set(["DialogHeader"])],
|
|
151
150
|
["DrawerHeader", new Set(["DrawerContent"])],
|
|
152
151
|
["DrawerBody", new Set(["DrawerContent"])],
|
|
153
152
|
["DrawerFooter", new Set(["DrawerContent"])],
|
|
153
|
+
["DrawerIntro", new Set(["DrawerBody"])],
|
|
154
|
+
["DrawerIntroActions", new Set(["DrawerIntro"])],
|
|
154
155
|
["DrawerNavigation", new Set(["DrawerHeader"])],
|
|
155
156
|
["DrawerMedia", new Set(["DrawerHeader"])],
|
|
156
|
-
["DrawerTitle", new Set(["DrawerHeader"])],
|
|
157
|
+
["DrawerTitle", new Set(["DrawerHeader", "DrawerIntro"])],
|
|
157
158
|
["DrawerActions", new Set(["DrawerHeader"])],
|
|
158
|
-
["PaneHeader", new Set(["Pane", "Sidebar"])],
|
|
159
|
-
["PaneBody", new Set(["Pane", "Sidebar"])],
|
|
160
|
-
["PaneFooter", new Set(["Pane", "Sidebar"])],
|
|
161
|
-
["PaneMedia", new Set(["PaneHeader"])],
|
|
162
|
-
["PaneTitle", new Set(["PaneHeader"])],
|
|
163
|
-
["PaneDescription", new Set(["PaneHeader"])],
|
|
164
|
-
["PaneActions", new Set(["PaneHeader"])],
|
|
165
159
|
["AlertMedia", new Set(["Alert"])],
|
|
166
160
|
["AlertHeader", new Set(["Alert"])],
|
|
167
161
|
["AlertActions", new Set(["Alert"])],
|
|
@@ -205,12 +199,15 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
205
199
|
},
|
|
206
200
|
],
|
|
207
201
|
[
|
|
208
|
-
"
|
|
202
|
+
"Surface",
|
|
209
203
|
{
|
|
210
|
-
header: "
|
|
211
|
-
body: "
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
header: "SurfaceHeader",
|
|
205
|
+
body: "SurfaceBody",
|
|
206
|
+
footer: "SurfaceFooter",
|
|
207
|
+
// Cada região é opcional, a forma curta convive com SurfaceBody e SurfaceFooter, e sem
|
|
208
|
+
// região nenhuma a Surface é uma caixa de filhos livres (ADR 0045).
|
|
209
|
+
flexible: true,
|
|
210
|
+
shorthand: new Set(["title", "media", "description", "count", "actions"]),
|
|
214
211
|
},
|
|
215
212
|
],
|
|
216
213
|
]);
|
|
@@ -223,18 +220,61 @@ const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
|
223
220
|
"PageBack",
|
|
224
221
|
"PageNavigation",
|
|
225
222
|
"PageActions",
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"
|
|
230
|
-
"
|
|
231
|
-
"ContentActions",
|
|
223
|
+
"SurfaceMedia",
|
|
224
|
+
"SurfaceTitle",
|
|
225
|
+
"SurfaceDescription",
|
|
226
|
+
"SurfaceMeta",
|
|
227
|
+
"SurfaceActions",
|
|
232
228
|
"AlertMedia",
|
|
233
229
|
"AlertHeader",
|
|
234
230
|
"AlertActions",
|
|
235
231
|
]);
|
|
236
232
|
|
|
233
|
+
// O body público é a única fonte de inset e ritmo estrutural (ADR 0046). Classes de
|
|
234
|
+
// ocupação, overflow, alinhamento e tipografia continuam livres; relações locais usam
|
|
235
|
+
// um wrapper filho e full bleed usa a prop `flush` das famílias que a oferecem.
|
|
236
|
+
const UI_STRUCTURAL_BODIES = new Set([
|
|
237
|
+
"SurfaceBody",
|
|
238
|
+
"DialogBody",
|
|
239
|
+
"DrawerBody",
|
|
240
|
+
"PageBody",
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
const UI_BODY_RHYTHM_CLASS =
|
|
244
|
+
/(?:^|:)(?:!?-?)(?:space-[xy]|gap(?:-[xy])?|p|px|py|pt|pr|pb|pl|ps|pe)-/;
|
|
245
|
+
|
|
246
|
+
const UI_FLUSH_BODIES = new Set(["SurfaceBody", "DialogBody", "DrawerBody"]);
|
|
247
|
+
|
|
237
248
|
const UI_REMOVED_PROPS = new Map([
|
|
249
|
+
[
|
|
250
|
+
"Pane",
|
|
251
|
+
new Map([
|
|
252
|
+
["inset", "mova o recuo para o conteúdo ou para uma `Surface` filha"],
|
|
253
|
+
["layout", "use `Surface` para anatomia visual contínua ou seccionada"],
|
|
254
|
+
]),
|
|
255
|
+
],
|
|
256
|
+
[
|
|
257
|
+
"Surface",
|
|
258
|
+
new Map([["meta", "use `count` para o total ao lado do título"]]),
|
|
259
|
+
],
|
|
260
|
+
[
|
|
261
|
+
"DialogMedia",
|
|
262
|
+
new Map([
|
|
263
|
+
["size", "dimensione o nó filho"],
|
|
264
|
+
["context", "declare o contexto no `IconTile` filho"],
|
|
265
|
+
["variant", "declare a variante no `IconTile` filho"],
|
|
266
|
+
["asChild", "passe o nó diretamente como filho"],
|
|
267
|
+
]),
|
|
268
|
+
],
|
|
269
|
+
[
|
|
270
|
+
"DrawerMedia",
|
|
271
|
+
new Map([
|
|
272
|
+
["size", "dimensione o nó filho"],
|
|
273
|
+
["context", "declare o contexto no `IconTile` filho"],
|
|
274
|
+
["variant", "declare a variante no `IconTile` filho"],
|
|
275
|
+
["asChild", "passe o nó diretamente como filho"],
|
|
276
|
+
]),
|
|
277
|
+
],
|
|
238
278
|
[
|
|
239
279
|
"PageHeader",
|
|
240
280
|
new Map([
|
|
@@ -262,6 +302,14 @@ const UI_REMOVED_PROPS = new Map([
|
|
|
262
302
|
]);
|
|
263
303
|
|
|
264
304
|
const UI_REMOVED_COMPONENTS = new Map([
|
|
305
|
+
["PaneHeader", "use `SurfaceHeader` dentro de uma `Surface`"],
|
|
306
|
+
["PaneBody", "use `SurfaceBody` ou um contêiner próprio do conteúdo"],
|
|
307
|
+
["PaneContent", "use `SurfaceBody` ou um contêiner próprio do conteúdo"],
|
|
308
|
+
["PaneFooter", "use `SurfaceFooter` dentro de uma `Surface`"],
|
|
309
|
+
["PaneMedia", "use `SurfaceMedia` dentro de `SurfaceHeader`"],
|
|
310
|
+
["PaneTitle", "use `SurfaceTitle` dentro de `SurfaceHeader`"],
|
|
311
|
+
["PaneDescription", "use `SurfaceDescription` dentro de `SurfaceHeader`"],
|
|
312
|
+
["PaneActions", "use `SurfaceActions` dentro de `SurfaceHeader`"],
|
|
265
313
|
["PageIntro", "use `PageHeader` como a única introdução estrutural"],
|
|
266
314
|
["PageDescription", "mova contexto relevante para o início de `PageBody`"],
|
|
267
315
|
[
|
|
@@ -272,11 +320,54 @@ const UI_REMOVED_COMPONENTS = new Map([
|
|
|
272
320
|
"DrawerDescription",
|
|
273
321
|
"mova contexto relevante para o início de `DrawerBody`",
|
|
274
322
|
],
|
|
323
|
+
// Content e Card deram lugar a Surface (ADR 0045).
|
|
324
|
+
[
|
|
325
|
+
"Content",
|
|
326
|
+
'use `Surface as="section"`, ou `as="fieldset"` para um grupo de campos',
|
|
327
|
+
],
|
|
328
|
+
["ContentHeader", "use `SurfaceHeader`"],
|
|
329
|
+
[
|
|
330
|
+
"ContentMedia",
|
|
331
|
+
"use `SurfaceMedia` e componha `IconTile` quando quiser a moldura",
|
|
332
|
+
],
|
|
333
|
+
["ContentTitle", "use `SurfaceTitle`"],
|
|
334
|
+
["ContentDescription", "use `SurfaceDescription`"],
|
|
335
|
+
["ContentMeta", "use `SurfaceMeta`"],
|
|
336
|
+
["ContentActions", "use `SurfaceActions`"],
|
|
337
|
+
["ContentBody", "use `SurfaceBody`"],
|
|
338
|
+
["Card", 'use `Surface framed elevation="raised"`'],
|
|
339
|
+
["CardHeader", "use `SurfaceHeader`"],
|
|
340
|
+
[
|
|
341
|
+
"CardMedia",
|
|
342
|
+
"use `SurfaceMedia` e componha `IconTile` quando quiser a moldura",
|
|
343
|
+
],
|
|
344
|
+
["CardTitle", "use `SurfaceTitle` e declare `level` em `Surface`"],
|
|
345
|
+
["CardDescription", "use `SurfaceDescription`"],
|
|
346
|
+
["CardAction", "use `SurfaceActions`"],
|
|
347
|
+
["CardBody", "use `SurfaceBody`"],
|
|
348
|
+
["CardContent", "use `SurfaceBody`"],
|
|
349
|
+
["CardFooter", "use `SurfaceFooter`"],
|
|
275
350
|
]);
|
|
276
351
|
|
|
277
352
|
const UI_STRUCTURAL_CONTAINERS = new Set(["PageShell"]);
|
|
278
353
|
|
|
279
354
|
const UI_STRUCTURAL_HEADERS = new Map([
|
|
355
|
+
[
|
|
356
|
+
"DialogIntro",
|
|
357
|
+
{
|
|
358
|
+
title: "DialogTitle",
|
|
359
|
+
optional: new Set(["DialogIntroActions"]),
|
|
360
|
+
shorthand: new Set(),
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
[
|
|
364
|
+
"DrawerIntro",
|
|
365
|
+
{
|
|
366
|
+
title: "DrawerTitle",
|
|
367
|
+
optional: new Set(["DrawerIntroActions"]),
|
|
368
|
+
shorthand: new Set(),
|
|
369
|
+
},
|
|
370
|
+
],
|
|
280
371
|
[
|
|
281
372
|
"PageHeader",
|
|
282
373
|
{
|
|
@@ -288,15 +379,19 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
288
379
|
},
|
|
289
380
|
],
|
|
290
381
|
[
|
|
291
|
-
"
|
|
382
|
+
"SurfaceHeader",
|
|
292
383
|
{
|
|
293
|
-
title: "
|
|
384
|
+
title: "SurfaceTitle",
|
|
294
385
|
optional: new Set([
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
"
|
|
386
|
+
"SurfaceMedia",
|
|
387
|
+
"SurfaceDescription",
|
|
388
|
+
"SurfaceMeta",
|
|
389
|
+
"SurfaceActions",
|
|
298
390
|
]),
|
|
299
|
-
shorthand: new Set(
|
|
391
|
+
shorthand: new Set(),
|
|
392
|
+
// Slots condicionais (`{cond && <SurfaceActions />}`) são composição legítima; o runtime
|
|
393
|
+
// valida o que de fato foi renderizado.
|
|
394
|
+
allowOpaque: true,
|
|
300
395
|
},
|
|
301
396
|
],
|
|
302
397
|
[
|
|
@@ -1085,6 +1180,57 @@ function directStructuralChildren(node, imports) {
|
|
|
1085
1180
|
return { names, hasOpaque };
|
|
1086
1181
|
}
|
|
1087
1182
|
|
|
1183
|
+
function countImportedJsxDescendants(node, imports, component) {
|
|
1184
|
+
const boundary =
|
|
1185
|
+
component === "DialogTitle"
|
|
1186
|
+
? "DialogContent"
|
|
1187
|
+
: component === "DrawerTitle"
|
|
1188
|
+
? "DrawerContent"
|
|
1189
|
+
: undefined;
|
|
1190
|
+
const count = (child, root = false) => {
|
|
1191
|
+
if (
|
|
1192
|
+
!root &&
|
|
1193
|
+
(ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child))
|
|
1194
|
+
) {
|
|
1195
|
+
const local = jsxLocalName(child);
|
|
1196
|
+
const imported = local === null ? undefined : imports.get(local);
|
|
1197
|
+
if (imported === boundary) return 0;
|
|
1198
|
+
if (imported === component) return 1;
|
|
1199
|
+
}
|
|
1200
|
+
if (ts.isConditionalExpression(child)) {
|
|
1201
|
+
return (
|
|
1202
|
+
count(child.condition) +
|
|
1203
|
+
Math.max(count(child.whenTrue), count(child.whenFalse))
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
let total = 0;
|
|
1207
|
+
ts.forEachChild(child, (descendant) => {
|
|
1208
|
+
total += count(descendant);
|
|
1209
|
+
});
|
|
1210
|
+
return total;
|
|
1211
|
+
};
|
|
1212
|
+
return count(node, true);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Diz se o JSX literal prova um filho livre ao lado das regiões: texto, elemento HTML ou outro
|
|
1217
|
+
* componente do Opus. Expressões e componentes de outros módulos ficam de fora, porque só o
|
|
1218
|
+
* runtime sabe o que eles renderizam.
|
|
1219
|
+
*/
|
|
1220
|
+
function hasLiteralFreeChild(node, imports, regionNames) {
|
|
1221
|
+
if (!ts.isJsxElement(node)) return false;
|
|
1222
|
+
return node.children.some((child) => {
|
|
1223
|
+
if (ts.isJsxText(child)) return child.text.trim() !== "";
|
|
1224
|
+
if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child))
|
|
1225
|
+
return false;
|
|
1226
|
+
const local = jsxLocalName(child);
|
|
1227
|
+
if (local === null) return false;
|
|
1228
|
+
const imported = imports.get(local);
|
|
1229
|
+
if (imported !== undefined) return !regionNames.includes(imported);
|
|
1230
|
+
return /^[a-z]/.test(local);
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1088
1234
|
/**
|
|
1089
1235
|
* Componentes declarados no próprio arquivo (função, classe ou arrow/function atribuída).
|
|
1090
1236
|
* Um deles nunca é o `Pane` do Opus, então o `Split` recusaria o elemento mesmo que o
|
|
@@ -1270,7 +1416,7 @@ function countPageHeadingSources(page, imports) {
|
|
|
1270
1416
|
const local = jsxLocalName(node);
|
|
1271
1417
|
if (
|
|
1272
1418
|
local !== null &&
|
|
1273
|
-
((imports.get(local) === "
|
|
1419
|
+
((imports.get(local) === "Surface" &&
|
|
1274
1420
|
jsxNumericAttribute(node, "level") === 1) ||
|
|
1275
1421
|
imports.get(local) === "PageState")
|
|
1276
1422
|
) {
|
|
@@ -1410,6 +1556,36 @@ export function checkUiStructure(file, text) {
|
|
|
1410
1556
|
}
|
|
1411
1557
|
}
|
|
1412
1558
|
|
|
1559
|
+
if (component !== null && UI_STRUCTURAL_BODIES.has(component)) {
|
|
1560
|
+
const overridden = jsxStringAttributeValues(
|
|
1561
|
+
jsxOpening(node),
|
|
1562
|
+
"className",
|
|
1563
|
+
)
|
|
1564
|
+
.flatMap((value) => value.trim().split(/\s+/))
|
|
1565
|
+
.filter((token) => UI_BODY_RHYTHM_CLASS.test(token));
|
|
1566
|
+
if (overridden.length > 0) {
|
|
1567
|
+
add(
|
|
1568
|
+
node,
|
|
1569
|
+
component,
|
|
1570
|
+
`${component} já governa inset e ritmo: remova ${[...new Set(overridden)].map((token) => `\`${token}\``).join(", ")}; ${UI_FLUSH_BODIES.has(component) ? "use `flush` para full bleed ou " : "use "}um wrapper filho para relações internas.`,
|
|
1571
|
+
"ui-structure-body-rhythm",
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
if (component === "DialogContent" || component === "DrawerContent") {
|
|
1577
|
+
const title =
|
|
1578
|
+
component === "DialogContent" ? "DialogTitle" : "DrawerTitle";
|
|
1579
|
+
if (countImportedJsxDescendants(node, imports, title) > 1) {
|
|
1580
|
+
add(
|
|
1581
|
+
node,
|
|
1582
|
+
component,
|
|
1583
|
+
`${component} aceita um único ${title}: mantenha o título no header ou na intro, nunca nos dois.`,
|
|
1584
|
+
"ui-structure-duplicate-title",
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1413
1589
|
const header =
|
|
1414
1590
|
component === null ? undefined : UI_STRUCTURAL_HEADERS.get(component);
|
|
1415
1591
|
if (header !== undefined) {
|
|
@@ -1514,7 +1690,30 @@ export function checkUiStructure(file, text) {
|
|
|
1514
1690
|
// some aqui.
|
|
1515
1691
|
// Com filhos estruturais, o que as regras de forma acham é defeito independente: misturar
|
|
1516
1692
|
// shorthand com slots, ou um filho inesperado, não some quando a prop sai.
|
|
1517
|
-
if (
|
|
1693
|
+
if (root.flexible) {
|
|
1694
|
+
const regionNames = [root.header, root.body, root.footer];
|
|
1695
|
+
const regions = children.names.filter((name) =>
|
|
1696
|
+
regionNames.includes(name),
|
|
1697
|
+
);
|
|
1698
|
+
if (shorthand && regions.includes(root.header)) {
|
|
1699
|
+
add(
|
|
1700
|
+
node,
|
|
1701
|
+
component,
|
|
1702
|
+
`${component} não permite misturar a forma curta com ${root.header} explícito.`,
|
|
1703
|
+
"ui-structure-mode",
|
|
1704
|
+
);
|
|
1705
|
+
} else if (
|
|
1706
|
+
regions.length > 0 &&
|
|
1707
|
+
(new Set(regions).size !== regions.length ||
|
|
1708
|
+
hasLiteralFreeChild(node, imports, regionNames))
|
|
1709
|
+
) {
|
|
1710
|
+
add(
|
|
1711
|
+
node,
|
|
1712
|
+
component,
|
|
1713
|
+
`${component} com slots estruturais aceita no máximo um ${root.header}, um ${root.body} e um ${root.footer} como filhos diretos e não os mistura com filhos livres: envolva o conteúdo em ${root.body}.`,
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
} else if (removedProp && !structural) {
|
|
1518
1717
|
// Nada a acrescentar; os filhos seguem sendo visitados no fim de `visit`.
|
|
1519
1718
|
} else if (shorthand && structural) {
|
|
1520
1719
|
add(
|
|
@@ -1573,7 +1772,7 @@ export function checkUiStructure(file, text) {
|
|
|
1573
1772
|
add(
|
|
1574
1773
|
node,
|
|
1575
1774
|
component,
|
|
1576
|
-
"Page explícito exige exatamente um heading principal em PageHeader,
|
|
1775
|
+
"Page explícito exige exatamente um heading principal em PageHeader, Surface level={1} ou PageState dentro de PageBody.",
|
|
1577
1776
|
);
|
|
1578
1777
|
}
|
|
1579
1778
|
}
|
package/bin/lib/copy.mjs
CHANGED
|
@@ -94,12 +94,8 @@ const JSX_CHILD_ROLES = new Map([
|
|
|
94
94
|
['BreadcrumbLink', 'breadcrumb'],
|
|
95
95
|
['BreadcrumbPage', 'breadcrumb'],
|
|
96
96
|
['Button', 'button'],
|
|
97
|
-
['CardDescription', 'description'],
|
|
98
|
-
['CardTitle', 'title'],
|
|
99
97
|
['CommandEmpty', 'empty-state'],
|
|
100
98
|
['CommandItem', 'menu-item'],
|
|
101
|
-
['ContentDescription', 'description'],
|
|
102
|
-
['ContentTitle', 'title'],
|
|
103
99
|
['DialogDescription', 'dialog-body'],
|
|
104
100
|
['DialogTitle', 'title'],
|
|
105
101
|
['DrawerDescription', 'dialog-body'],
|
|
@@ -124,8 +120,8 @@ const JSX_CHILD_ROLES = new Map([
|
|
|
124
120
|
// comando. Classificado como `button`, a política universal cobraria verbo de ação.
|
|
125
121
|
['PageBack', 'breadcrumb'],
|
|
126
122
|
['PageTitle', 'title'],
|
|
127
|
-
['
|
|
128
|
-
['
|
|
123
|
+
['SurfaceDescription', 'description'],
|
|
124
|
+
['SurfaceTitle', 'title'],
|
|
129
125
|
['TableCaption', 'description'],
|
|
130
126
|
['TableHead', 'heading'],
|
|
131
127
|
['TabsListItem', 'tab'],
|
|
@@ -222,20 +218,6 @@ const JSX_PROP_ROLES = new Map([
|
|
|
222
218
|
]),
|
|
223
219
|
],
|
|
224
220
|
['CommandInput', new Map([['placeholder', 'placeholder']])],
|
|
225
|
-
[
|
|
226
|
-
'Content',
|
|
227
|
-
new Map([
|
|
228
|
-
['title', 'title'],
|
|
229
|
-
['description', 'description'],
|
|
230
|
-
]),
|
|
231
|
-
],
|
|
232
|
-
[
|
|
233
|
-
'ContentHeader',
|
|
234
|
-
new Map([
|
|
235
|
-
['title', 'title'],
|
|
236
|
-
['description', 'description'],
|
|
237
|
-
]),
|
|
238
|
-
],
|
|
239
221
|
[
|
|
240
222
|
'DataState',
|
|
241
223
|
new Map([
|
|
@@ -290,6 +272,13 @@ const JSX_PROP_ROLES = new Map([
|
|
|
290
272
|
['emptyText', 'empty-state'],
|
|
291
273
|
]),
|
|
292
274
|
],
|
|
275
|
+
[
|
|
276
|
+
'Surface',
|
|
277
|
+
new Map([
|
|
278
|
+
['title', 'title'],
|
|
279
|
+
['description', 'description'],
|
|
280
|
+
]),
|
|
281
|
+
],
|
|
293
282
|
['Textarea', new Map([['placeholder', 'placeholder']])],
|
|
294
283
|
])
|
|
295
284
|
|
|
@@ -363,20 +352,6 @@ const JSX_PROP_CLASS = new Map([
|
|
|
363
352
|
]),
|
|
364
353
|
],
|
|
365
354
|
['CommandInput', new Map([['placeholder', 'className']])],
|
|
366
|
-
[
|
|
367
|
-
'Content',
|
|
368
|
-
new Map([
|
|
369
|
-
['title', 'className'],
|
|
370
|
-
['description', 'className'],
|
|
371
|
-
]),
|
|
372
|
-
],
|
|
373
|
-
[
|
|
374
|
-
'ContentHeader',
|
|
375
|
-
new Map([
|
|
376
|
-
['title', 'className'],
|
|
377
|
-
['description', 'className'],
|
|
378
|
-
]),
|
|
379
|
-
],
|
|
380
355
|
['Input', new Map([['placeholder', 'className']])],
|
|
381
356
|
['InputGroupInput', new Map([['placeholder', 'className']])],
|
|
382
357
|
['InputGroupTextarea', new Map([['placeholder', 'className']])],
|
|
@@ -394,6 +369,13 @@ const JSX_PROP_CLASS = new Map([
|
|
|
394
369
|
]),
|
|
395
370
|
],
|
|
396
371
|
['Select', new Map([['placeholder', 'className']])],
|
|
372
|
+
[
|
|
373
|
+
'Surface',
|
|
374
|
+
new Map([
|
|
375
|
+
['title', 'className'],
|
|
376
|
+
['description', 'className'],
|
|
377
|
+
]),
|
|
378
|
+
],
|
|
397
379
|
['Textarea', new Map([['placeholder', 'className']])],
|
|
398
380
|
])
|
|
399
381
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# ADR 0023 — Superfícies compartilham uma anatomia e dois layouts
|
|
2
2
|
|
|
3
|
+
> **Atualização em 2026-10-01:** a ADR 0046 torna o body responsável pelo ritmo entre seus filhos
|
|
4
|
+
> diretos e dá ao conteúdo um respiro vertical maior que o das faixas; “regiões simétricas” abaixo
|
|
5
|
+
> descreve a implementação original. A ADR 0047 também remove `Pane` desta família: ele passa a ser
|
|
6
|
+
> somente a unidade de layout de `Split`, enquanto `Surface`, `Dialog` e `Drawer` preservam a
|
|
7
|
+
> anatomia.
|
|
8
|
+
|
|
3
9
|
- **Status:** aceita.
|
|
4
10
|
- **Data:** 2026-09-23.
|
|
5
11
|
- **Complementa:** ADRs 0005, 0014, 0018, 0019 e 0021.
|
|
@@ -22,9 +22,9 @@ preserva `select` como apresentação padrão.
|
|
|
22
22
|
|
|
23
23
|
`toggle` aplica-se a filtros `type: "select"` de escolha única e usa as mesmas opções, valor de
|
|
24
24
|
repouso, regra de vazio, dependências e atualização de estado do controle padrão. A apresentação
|
|
25
|
-
mostra todas as opções em um `ToggleGroup
|
|
26
|
-
|
|
27
|
-
catálogo, o grupo apresenta ícone e rótulo juntos.
|
|
25
|
+
mostra todas as opções em um `ToggleGroup`; a composição visual vigente é definida pela ADR 0042.
|
|
26
|
+
O controle não permite desmarcar a opção ativa quando o filtro não aceita vazio. Quando a opção vem
|
|
27
|
+
de um dicionário e sua entrada declara um ícone do catálogo, o grupo apresenta ícone e rótulo juntos.
|
|
28
28
|
|
|
29
29
|
O tipo do filtro continua descrevendo o valor e sua operação. A apresentação escolhe somente o
|
|
30
30
|
controle usado para editá-lo.
|
|
@@ -21,9 +21,8 @@ A anatomia pública nomeia a posição de cada parte: `TabsListItem` pertence a
|
|
|
21
21
|
`TabsPanel` identifica o painel associado. Os nomes anteriores, `TabsTrigger` e `TabsContent`, não
|
|
22
22
|
são mantidos como aliases porque há um único consumidor e a migração é atômica.
|
|
23
23
|
|
|
24
|
-
Escolhas únicas ou múltiplas que alteram um valor no contexto atual usam `ToggleGroup`.
|
|
25
|
-
|
|
26
|
-
a borda pertence aos itens e as arestas internas duplicadas são removidas pela primitiva.
|
|
24
|
+
Escolhas únicas ou múltiplas que alteram um valor no contexto atual usam `ToggleGroup`. A composição
|
|
25
|
+
visual vigente do grupo é definida pela ADR 0042.
|
|
27
26
|
|
|
28
27
|
## Consequências
|
|
29
28
|
|
|
@@ -31,7 +30,7 @@ a borda pertence aos itens e as arestas internas duplicadas são removidas pela
|
|
|
31
30
|
- Filtros e escolhas de formulário não usam semântica de tabs para obter uma aparência compacta.
|
|
32
31
|
- Consumidores removem `variant="line"`; não há substituição para a antiga superfície preenchida.
|
|
33
32
|
- Consumidores renomeiam `TabsTrigger` para `TabsListItem` e `TabsContent` para `TabsPanel`.
|
|
34
|
-
- A documentação de ToggleGroup explicita
|
|
33
|
+
- A documentação de ToggleGroup explicita seus modos de composição.
|
|
35
34
|
|
|
36
35
|
## Alternativas descartadas
|
|
37
36
|
|
|
@@ -56,5 +55,5 @@ da anatomia exposta ao consumidor. `ListItem` e `Panel` deixam a composição le
|
|
|
56
55
|
- testes da primitiva confirmam a linha-base, o indicador ativo e a distribuição;
|
|
57
56
|
- testes do `ActionFilterBar` confirmam que `presentation: "toggle"` usa seleção exclusiva sem
|
|
58
57
|
alterar o estado transportado para a action;
|
|
59
|
-
- a documentação de Tabs não oferece apresentação preenchida e a de ToggleGroup demonstra
|
|
60
|
-
|
|
58
|
+
- a documentação de Tabs não oferece apresentação preenchida e a de ToggleGroup demonstra seus
|
|
59
|
+
modos próprios de escolha.
|