@softize/opus 21.0.1 → 22.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/bin/lib/check.mjs +31 -28
- package/docs/adr/0004-page-content-state-is-composed.md +3 -2
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +3 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +23 -14
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +5 -0
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +7 -4
- package/docs/adr/0016-list-collection-header-belongs-to-content.md +13 -8
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +27 -26
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +1 -0
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +71 -0
- package/docs/adr/0030-framed-content-can-use-sectioned-anatomy.md +89 -0
- package/docs/adr/0031-detail-dividers-own-the-tabular-label-treatment.md +61 -0
- package/docs/adr/0032-tab-baseline-respects-the-container-inset.md +59 -0
- package/docs/adr/0033-drawer-uses-an-inset-structural-frame.md +72 -0
- package/docs/adr/0034-tabs-can-own-a-frame-without-owning-a-surface.md +60 -0
- package/docs/adr/0035-button-group-separates-frame-relationship-and-distribution.md +63 -0
- package/docs/adr/0036-tabs-declares-trigger-distribution.md +58 -0
- package/docs/radius-scale.md +5 -2
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +9 -6
- package/registry/skills/build-opus-ui/references/evaluations.md +5 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +14 -16
- package/src/ui/components/patterns/content-header.tsx +51 -13
- package/src/ui/components/patterns/list.tsx +51 -33
- package/src/ui/components/patterns/page.tsx +139 -251
- package/src/ui/components/patterns/presentation.tsx +2 -6
- package/src/ui/components/primitives/button-group.tsx +80 -8
- package/src/ui/components/primitives/button.tsx +85 -60
- package/src/ui/components/primitives/detail.tsx +63 -57
- package/src/ui/components/primitives/dialog.tsx +8 -11
- package/src/ui/components/primitives/drawer.tsx +16 -12
- package/src/ui/components/primitives/tabs.tsx +123 -54
- package/src/ui/docs/content/button.md +55 -29
- package/src/ui/docs/content/content.md +61 -36
- package/src/ui/docs/content/detail.md +28 -19
- package/src/ui/docs/content/drawer.md +53 -47
- package/src/ui/docs/content/input.md +51 -35
- package/src/ui/docs/content/page.md +84 -95
- package/src/ui/docs/content/pane.md +83 -63
- package/src/ui/docs/content/presentation.md +20 -20
- package/src/ui/docs/content/tabs.md +107 -36
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,53 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 22.0.0 — 2026-09-28
|
|
11
|
+
|
|
12
|
+
As superfícies passam a usar uma introdução única e ganham composições explícitas para conteúdo
|
|
13
|
+
seccionado, abas emolduradas e grupos de ações divididos. A mudança elimina wrappers e classes que
|
|
14
|
+
os consumidores repetiam para montar essas formas, além de alinhar Drawer e Content ao raio
|
|
15
|
+
estrutural vigente. É major porque remove `PageIntro`, muda a posição visual do cabeçalho nas páginas
|
|
16
|
+
com `PageShell` e altera o acabamento padrão de Drawer, Tabs `line` e detalhes horizontais com
|
|
17
|
+
divisórias.
|
|
18
|
+
|
|
19
|
+
### Breaking
|
|
20
|
+
|
|
21
|
+
**`PageIntro` foi removido.** `PageHeader` é a única introdução da página e reúne, nessa ordem,
|
|
22
|
+
`PageNavigation`, `PageTitle` e `PageActions`. `PageShell` deixou de renderizar uma barra persistente:
|
|
23
|
+
ele fornece navegação e ações contextuais ao header da página ativa. Migração: mova o título e o
|
|
24
|
+
breadcrumb de `PageIntro` para `PageHeader`; remova imports de `PageIntro` e classes que compensavam
|
|
25
|
+
a barra superior. `PageShell.headerClassName` também foi removida porque não existe mais uma barra
|
|
26
|
+
própria para estilizar; remova a propriedade e, quando necessário, estilize o `PageHeader` da página.
|
|
27
|
+
Um header sem conteúdo renderizável não cria região vazia.
|
|
28
|
+
|
|
29
|
+
**Drawer usa moldura recuada.** `DrawerContent` passa a ter borda completa, `rounded-xl`, recorte e
|
|
30
|
+
`0.25rem` de respiro em relação à viewport. Migração: remova classes locais de borda, raio, recorte e
|
|
31
|
+
inset que reconstruíam essa moldura; preserve apenas dimensões deliberadas, como `max-w-*`.
|
|
32
|
+
|
|
33
|
+
**`TabsList variant="line"` mudou de composição.** A linha-base respeita a largura recebida, a aba
|
|
34
|
+
usa uma área interna da altura de Button com fundo somente no hover e o indicador ativo usa o
|
|
35
|
+
contexto primário. `DialogBody` e `DrawerBody` não publicam mais `--opus-inline-gutter`. Migração:
|
|
36
|
+
remova classes locais que estendiam a linha, simulavam o botão interno ou compensavam o gutter.
|
|
37
|
+
|
|
38
|
+
**`DetailGroup` associa o tratamento tabular a `dividers`.** Em orientação horizontal, divisórias
|
|
39
|
+
passam a aplicar padding por célula, fundo sutil nos rótulos e largura padrão de `8.5rem`, com ou sem
|
|
40
|
+
moldura externa. `variant="framed"` sem divisórias deixa de alterar o fundo dos rótulos. Migração:
|
|
41
|
+
declare `dividers` quando quiser a grade tabular; não use `framed` para escolher o tratamento interno.
|
|
42
|
+
|
|
43
|
+
### Superfícies e controles
|
|
44
|
+
|
|
45
|
+
- `Content` aceita `layout="continuous" | "sectioned"`. O layout seccionado exige `framed`, separa
|
|
46
|
+
o header com divisor e deixa o body sem recuo para tabelas, listas e grades que possuem células.
|
|
47
|
+
Molduras de Content usam `rounded-xl`, inclusive nos acabamentos `default` e `legend`.
|
|
48
|
+
- `Tabs` aceita `framed`, que aplica borda, raio e recorte sem criar fundo próprio, e
|
|
49
|
+
`distribution="content" | "equal"`, que preserva a largura dos rótulos ou reparte igualmente a
|
|
50
|
+
largura disponível entre os gatilhos.
|
|
51
|
+
- `ButtonGroup` aceita `framed` e `mode="divided"`; o modo insere divisores orientados entre os
|
|
52
|
+
controles sem incluí-los no cálculo de `distribution="equal"`.
|
|
53
|
+
- `Button` aceita `context="warning"` em todas as variantes visuais.
|
|
54
|
+
- O filtro composto de período do `ActionList` apresenta período antes do tipo de data, usa divisor
|
|
55
|
+
estrutural e aplica o foco ao grupo quando qualquer controle interno está aberto.
|
|
56
|
+
|
|
10
57
|
## 21.0.1 — 2026-09-26
|
|
11
58
|
|
|
12
59
|
**Correção.** O `status()` do driver BullMQ podia devolver `status: 'done'` com `data: null` para um
|
package/bin/lib/check.mjs
CHANGED
|
@@ -121,13 +121,12 @@ const UI_SURFACE_PAIRS = new Map([
|
|
|
121
121
|
// locais homônimos.
|
|
122
122
|
const UI_STRUCTURAL_PARENTS = new Map([
|
|
123
123
|
["PageHeader", new Set(["Page"])],
|
|
124
|
-
["PageIntro", new Set(["Page"])],
|
|
125
124
|
["PageBody", new Set(["Page"])],
|
|
126
125
|
["PageFooter", new Set(["Page"])],
|
|
127
|
-
["PageTitle", new Set(["PageHeader"
|
|
126
|
+
["PageTitle", new Set(["PageHeader"])],
|
|
128
127
|
["PageBack", new Set(["PageHeader"])],
|
|
129
128
|
["PageNavigation", new Set(["PageHeader"])],
|
|
130
|
-
["PageActions", new Set(["PageHeader"
|
|
129
|
+
["PageActions", new Set(["PageHeader"])],
|
|
131
130
|
["ContentHeader", new Set(["Content"])],
|
|
132
131
|
["ContentBody", new Set(["Content"])],
|
|
133
132
|
["ContentTitle", new Set(["ContentHeader"])],
|
|
@@ -191,11 +190,9 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
191
190
|
"Page",
|
|
192
191
|
{
|
|
193
192
|
header: "PageHeader",
|
|
194
|
-
alternateHeader: "PageIntro",
|
|
195
193
|
body: "PageBody",
|
|
196
194
|
footer: "PageFooter",
|
|
197
195
|
headerRequired: false,
|
|
198
|
-
allowBothHeaders: true,
|
|
199
196
|
shorthand: new Set(["title", "actions"]),
|
|
200
197
|
// O contador saiu do título da página (ADR 0009). Tirar `count` do shorthand não proíbe
|
|
201
198
|
// nada sozinho — só apaga o marcador que fazia o gate enxergar aquele Page —, então a
|
|
@@ -219,7 +216,6 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
219
216
|
|
|
220
217
|
const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
221
218
|
"PageHeader",
|
|
222
|
-
"PageIntro",
|
|
223
219
|
"PageBody",
|
|
224
220
|
"PageFooter",
|
|
225
221
|
"PageTitle",
|
|
@@ -241,7 +237,7 @@ const UI_REMOVED_PROPS = new Map([
|
|
|
241
237
|
[
|
|
242
238
|
"PageHeader",
|
|
243
239
|
new Map([
|
|
244
|
-
["variant", "
|
|
240
|
+
["variant", "o cabeçalho da página tem uma única apresentação rolável"],
|
|
245
241
|
]),
|
|
246
242
|
],
|
|
247
243
|
[
|
|
@@ -265,6 +261,7 @@ const UI_REMOVED_PROPS = new Map([
|
|
|
265
261
|
]);
|
|
266
262
|
|
|
267
263
|
const UI_REMOVED_COMPONENTS = new Map([
|
|
264
|
+
["PageIntro", "use `PageHeader` como a única introdução estrutural"],
|
|
268
265
|
["PageDescription", "mova contexto relevante para o início de `PageBody`"],
|
|
269
266
|
[
|
|
270
267
|
"DialogDescription",
|
|
@@ -289,14 +286,6 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
289
286
|
titleRequired: false,
|
|
290
287
|
},
|
|
291
288
|
],
|
|
292
|
-
[
|
|
293
|
-
"PageIntro",
|
|
294
|
-
{
|
|
295
|
-
title: "PageTitle",
|
|
296
|
-
optional: new Set(["PageActions"]),
|
|
297
|
-
shorthand: new Set(),
|
|
298
|
-
},
|
|
299
|
-
],
|
|
300
289
|
[
|
|
301
290
|
"ContentHeader",
|
|
302
291
|
{
|
|
@@ -1118,11 +1107,14 @@ const jsxSpaceEntities = {
|
|
|
1118
1107
|
* As demais entidades nomeadas ficam como estão: nenhuma delas vira espaço em branco.
|
|
1119
1108
|
*/
|
|
1120
1109
|
function decodeJsxSpaceEntities(raw) {
|
|
1121
|
-
return raw.replace(
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1110
|
+
return raw.replace(
|
|
1111
|
+
/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|([a-zA-Z]+));/g,
|
|
1112
|
+
(entity, dec, hex, name) => {
|
|
1113
|
+
if (name !== undefined) return jsxSpaceEntities[name] ?? entity;
|
|
1114
|
+
const code = dec !== undefined ? Number(dec) : Number.parseInt(hex, 16);
|
|
1115
|
+
return code <= 0x10ffff ? String.fromCodePoint(code) : entity;
|
|
1116
|
+
},
|
|
1117
|
+
);
|
|
1126
1118
|
}
|
|
1127
1119
|
|
|
1128
1120
|
/**
|
|
@@ -1176,7 +1168,10 @@ function invalidSplitChildren(split, imports, localComponents) {
|
|
|
1176
1168
|
return text(node.text, node);
|
|
1177
1169
|
}
|
|
1178
1170
|
if (ts.isNumericLiteral(node)) {
|
|
1179
|
-
invalid.push({
|
|
1171
|
+
invalid.push({
|
|
1172
|
+
label: `o número ${node.text}`,
|
|
1173
|
+
position: node.getStart(),
|
|
1174
|
+
});
|
|
1180
1175
|
return;
|
|
1181
1176
|
}
|
|
1182
1177
|
if (
|
|
@@ -1206,7 +1201,8 @@ function invalidSplitChildren(split, imports, localComponents) {
|
|
|
1206
1201
|
for (const nested of node.children) child(nested);
|
|
1207
1202
|
return;
|
|
1208
1203
|
}
|
|
1209
|
-
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node))
|
|
1204
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node))
|
|
1205
|
+
element(node);
|
|
1210
1206
|
};
|
|
1211
1207
|
for (const node of split.children) child(node);
|
|
1212
1208
|
return invalid;
|
|
@@ -1215,7 +1211,8 @@ function invalidSplitChildren(split, imports, localComponents) {
|
|
|
1215
1211
|
function directImportedJsxChildren(node, imports) {
|
|
1216
1212
|
if (!ts.isJsxElement(node)) return [];
|
|
1217
1213
|
return node.children.flatMap((child) => {
|
|
1218
|
-
if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child))
|
|
1214
|
+
if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child))
|
|
1215
|
+
return [];
|
|
1219
1216
|
const local = jsxLocalName(child);
|
|
1220
1217
|
const imported = local === null ? undefined : imports.get(local);
|
|
1221
1218
|
return imported === undefined ? [] : [{ node: child, component: imported }];
|
|
@@ -1224,7 +1221,8 @@ function directImportedJsxChildren(node, imports) {
|
|
|
1224
1221
|
|
|
1225
1222
|
function jsxNumericAttribute(node, name) {
|
|
1226
1223
|
const attribute = jsxAttribute(jsxOpening(node), name);
|
|
1227
|
-
if (attribute === undefined || attribute.initializer === undefined)
|
|
1224
|
+
if (attribute === undefined || attribute.initializer === undefined)
|
|
1225
|
+
return null;
|
|
1228
1226
|
if (
|
|
1229
1227
|
ts.isJsxExpression(attribute.initializer) &&
|
|
1230
1228
|
attribute.initializer.expression !== undefined &&
|
|
@@ -1243,7 +1241,7 @@ function countPageHeadingSources(page, imports) {
|
|
|
1243
1241
|
let count = 0;
|
|
1244
1242
|
let states = 0;
|
|
1245
1243
|
for (const child of directImportedJsxChildren(page, imports)) {
|
|
1246
|
-
if (child.component === "PageHeader"
|
|
1244
|
+
if (child.component === "PageHeader") {
|
|
1247
1245
|
count += directImportedJsxChildren(child.node, imports).filter(
|
|
1248
1246
|
(entry) => entry.component === "PageTitle",
|
|
1249
1247
|
).length;
|
|
@@ -1271,7 +1269,8 @@ function countPageHeadingSources(page, imports) {
|
|
|
1271
1269
|
|
|
1272
1270
|
function pageBodyHasOpaqueDescendant(page, imports) {
|
|
1273
1271
|
for (const child of directImportedJsxChildren(page, imports)) {
|
|
1274
|
-
if (child.component !== "PageBody" || !ts.isJsxElement(child.node))
|
|
1272
|
+
if (child.component !== "PageBody" || !ts.isJsxElement(child.node))
|
|
1273
|
+
continue;
|
|
1275
1274
|
let opaque = false;
|
|
1276
1275
|
const visit = (node) => {
|
|
1277
1276
|
if (opaque) return;
|
|
@@ -1323,7 +1322,11 @@ export function checkUiStructure(file, text) {
|
|
|
1323
1322
|
const local = jsxLocalName(node);
|
|
1324
1323
|
const component = local === null ? null : imports.get(local);
|
|
1325
1324
|
if (component === "Split" && ts.isJsxElement(node)) {
|
|
1326
|
-
for (const child of invalidSplitChildren(
|
|
1325
|
+
for (const child of invalidSplitChildren(
|
|
1326
|
+
node,
|
|
1327
|
+
imports,
|
|
1328
|
+
localComponents,
|
|
1329
|
+
)) {
|
|
1327
1330
|
findings.push({
|
|
1328
1331
|
rule: "ui-structure-split",
|
|
1329
1332
|
level: "error",
|
|
@@ -1552,7 +1555,7 @@ export function checkUiStructure(file, text) {
|
|
|
1552
1555
|
add(
|
|
1553
1556
|
node,
|
|
1554
1557
|
component,
|
|
1555
|
-
"Page explícito exige exatamente um heading principal em PageHeader,
|
|
1558
|
+
"Page explícito exige exatamente um heading principal em PageHeader, Content level={1} ou PageState dentro de PageBody.",
|
|
1556
1559
|
);
|
|
1557
1560
|
}
|
|
1558
1561
|
}
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
> restaurá-lo em `ready`. A lista de responsabilidades abaixo já descreve o comportamento novo; o
|
|
9
9
|
> adendo no fim deste arquivo registra o motivo, a alternativa descartada e o que se perde.
|
|
10
10
|
>
|
|
11
|
-
> **Atualização (2026-09-10).**
|
|
12
|
-
> `PageIntro`. A
|
|
11
|
+
> **Atualização (2026-09-10, superada em 2026-09-28).** A ADR 0011 preservava a barra do
|
|
12
|
+
> `PageShell` e ocultava somente `PageIntro`. A ADR 0029 removeu as duas regiões: `PageHeader` é a
|
|
13
|
+
> única introdução e o estado integral a substitui por inteiro, dentro ou fora de `PageShell`.
|
|
13
14
|
|
|
14
15
|
## Contexto
|
|
15
16
|
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
> **Atualização (2026-09-16).** O corpo abaixo descreve a anatomia da época em que foi escrito, não a
|
|
14
14
|
> vigente: a apresentação `bar` da ADR 0010 saiu com a ADR 0011, e nenhum header estrutural carrega
|
|
15
15
|
> mais descrição (ADRs 0018 e 0014). A anatomia atual está na documentação de cada família.
|
|
16
|
+
>
|
|
17
|
+
> **Atualização (2026-09-28).** A ADR 0033 substitui a decisão de prender a moldura externa do
|
|
18
|
+
> Drawer à viewport: o painel agora usa moldura completa e recuada.
|
|
16
19
|
|
|
17
20
|
## Contexto
|
|
18
21
|
|
|
@@ -44,6 +44,13 @@ quanto sua materialização (`btn-danger`, `btn-outline-danger`), mistura hierar
|
|
|
44
44
|
inclui `light`/`dark`, que pertencem ao tema. Copiar a API manteria a ambiguidade que queremos
|
|
45
45
|
remover. Rejeitada como contrato literal e aceita como referência de vocabulário.
|
|
46
46
|
|
|
47
|
+
### Representar ações de cautela como neutras, perigosas ou com classes locais
|
|
48
|
+
|
|
49
|
+
Uma ação pode pedir atenção sem ser destrutiva, como registrar uma tentativa sem sucesso. Tratá-la
|
|
50
|
+
como `neutral` apaga essa diferença; tratá-la como `danger` exagera o risco; aplicar tokens de
|
|
51
|
+
`warning` por `className` cria uma variante clandestina fora do contrato e não alcança tipos,
|
|
52
|
+
documentação nem componentes que reutilizam `ButtonContext`. Rejeitada.
|
|
53
|
+
|
|
47
54
|
### Declarar contexto e variante como eixos independentes
|
|
48
55
|
|
|
49
56
|
`context` escolhe a família de tokens e responde por que existe o realce. `variant` escolhe como a
|
|
@@ -59,12 +66,7 @@ O Opus define o vocabulário canônico:
|
|
|
59
66
|
|
|
60
67
|
```ts
|
|
61
68
|
type UiContext =
|
|
62
|
-
|
|
|
63
|
-
| 'primary'
|
|
64
|
-
| 'info'
|
|
65
|
-
| 'success'
|
|
66
|
-
| 'warning'
|
|
67
|
-
| 'danger'
|
|
69
|
+
"neutral" | "primary" | "info" | "success" | "warning" | "danger";
|
|
68
70
|
```
|
|
69
71
|
|
|
70
72
|
- `neutral`: estado normal, inativo ou sem julgamento positivo/negativo;
|
|
@@ -86,24 +88,29 @@ domínio não se torna a ação principal da interface. A metadata canônica pas
|
|
|
86
88
|
Para componentes semânticos, `variant` descreve somente o tratamento visual:
|
|
87
89
|
|
|
88
90
|
```ts
|
|
89
|
-
type SemanticVariant =
|
|
91
|
+
type SemanticVariant = "solid" | "subtle" | "outline" | "ghost" | "link";
|
|
90
92
|
```
|
|
91
93
|
|
|
92
94
|
Cada componente aceita apenas as variantes que consegue materializar com coerência. Os defaults
|
|
93
95
|
iniciais são:
|
|
94
96
|
|
|
95
|
-
| Componente | Contexto padrão | Variante padrão
|
|
96
|
-
|
|
|
97
|
-
| Button
|
|
98
|
-
| Badge
|
|
99
|
-
| Alert
|
|
100
|
-
| Dot
|
|
101
|
-
| MetricCard | `neutral`
|
|
97
|
+
| Componente | Contexto padrão | Variante padrão | Contextos permitidos |
|
|
98
|
+
| ---------- | --------------- | ----------------- | ------------------------------------------------------------ |
|
|
99
|
+
| Button | `primary` | `solid` | `neutral`, `primary`, `warning`, `danger` |
|
|
100
|
+
| Badge | `neutral` | `subtle` | todos |
|
|
101
|
+
| Alert | `neutral` | `subtle` | `neutral`, `info`, `success`, `warning`, `danger` |
|
|
102
|
+
| Dot | `neutral` | `solid` | `neutral`, `primary`, `info`, `success`, `warning`, `danger` |
|
|
103
|
+
| MetricCard | `neutral` | `subtle` no ícone | `neutral`, `info`, `success`, `warning`, `danger` |
|
|
102
104
|
|
|
103
105
|
`solid`, `subtle` e `outline` podem compartilhar um contexto sem compartilhar classes. `ghost` e
|
|
104
106
|
`link` ficam restritos a controles interativos. Cor e ícone continuam reforços: texto ou nome
|
|
105
107
|
acessível comunica o significado.
|
|
106
108
|
|
|
109
|
+
`warning` em `Button` representa uma ação de cautela não destrutiva: a pessoa pode prosseguir, mas
|
|
110
|
+
o efeito merece atenção. `danger` permanece reservado a risco ou consequência perigosa. `info` e
|
|
111
|
+
`success` continuam fora do subconjunto do botão até existir uma necessidade de ação observável;
|
|
112
|
+
estado informativo ou resultado positivo, isoladamente, não justificam colorir um controle.
|
|
113
|
+
|
|
107
114
|
Componentes cuja `variant` é exclusivamente estrutural ou local, como `plain | framed` e
|
|
108
115
|
`default | line`, podem mantê-la. Ao criar API nova, preferir uma prop específica quando o nome do
|
|
109
116
|
eixo for mais claro (`frame`, `layout`, `appearance`); não renomear primitives existentes sem ganho
|
|
@@ -161,6 +168,8 @@ for consultada.
|
|
|
161
168
|
|
|
162
169
|
- Primitives semânticos passam a compartilhar um vocabulário e tokens contextuais.
|
|
163
170
|
- Defaults podem variar por componente, mas a mesma palavra nunca muda de significado.
|
|
171
|
+
- Ações de cautela não destrutivas usam os tokens canônicos de `warning`, sem classes locais nem
|
|
172
|
+
reclassificação como `danger`.
|
|
164
173
|
- A API fica mais explícita em chamadas que precisam dos dois eixos.
|
|
165
174
|
- A transição aumenta temporariamente tipos, testes e normalização por causa dos aliases.
|
|
166
175
|
- Temas precisam oferecer tokens de superfície, borda, texto de ênfase e sólido para cada contexto,
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
> `PageHeader`, `PageIntro` e `PageFooter` como opcionais e exige exatamente um heading principal —
|
|
13
13
|
> `PageTitle`, `Content level={1}` ou um `PageState` ativo. As frases abaixo que pedem `PageIntro`
|
|
14
14
|
> "sem `PageHeader`" descrevem a regra da 17.2 e não valem mais.
|
|
15
|
+
>
|
|
16
|
+
> **Atualização (2026-09-27).** A padronização posterior do chrome estrutural levou a barra do
|
|
17
|
+
> `PageShell` dos `3rem` originais para `3.75rem`. A presença persistente da barra continua sendo a
|
|
18
|
+
> decisão arquitetural desta ADR; sua medida pertence ao contrato vigente do componente, registrado
|
|
19
|
+
> na documentação e nos testes do Opus, e não deve ser redefinida localmente pelo consumidor.
|
|
15
20
|
|
|
16
21
|
## Contexto
|
|
17
22
|
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
> materializa título e comandos de header no `Content` que envolve o `ActionList`. Dialog e Drawer
|
|
8
8
|
> continuam materializando esses elementos no header da superfície.
|
|
9
9
|
>
|
|
10
|
+
> **Atualização (2026-09-28).** A ADR 0029 removeu `PageIntro` e a barra persistente de
|
|
11
|
+
> `PageShell`. As passagens abaixo sobre a anatomia de Page já refletem a introdução única.
|
|
12
|
+
>
|
|
10
13
|
> Uma Presentation pode declarar uma `route` absoluta com parâmetros. Nessa forma, URL, surface e
|
|
11
14
|
> origem do registro passam a integrar o artefato; o adaptador da aplicação apenas conecta a spec ao
|
|
12
15
|
> router hospedeiro.
|
|
@@ -29,10 +32,10 @@ que aparece. `Surface` escolhe `page`, `dialog` ou `drawer` no momento da render
|
|
|
29
32
|
|
|
30
33
|
A anatomia portátil é `Header + Body + Footer?`, mas cada surface materializa seu header de acordo
|
|
31
34
|
com o espaço disponível. Dialog e Drawer organizam horizontalmente
|
|
32
|
-
`Navigation? + Title + Actions? + Close` e mantêm as ações de conclusão no footer. Em Page
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
`Navigation? + Title + Actions? + Close` e mantêm as ações de conclusão no footer. Em Page,
|
|
36
|
+
`PageHeader` reúne navegação contextual, título e ações numa única introdução rolável. Quando a
|
|
37
|
+
listagem fornece o heading principal por `Content level={1}`, o mesmo header pode conter somente a
|
|
38
|
+
navegação ou as ações fornecidas pelo `PageShell`, sem criar uma segunda região visual.
|
|
36
39
|
|
|
37
40
|
Uma action assume um de dois papéis:
|
|
38
41
|
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
- **Data:** 2026-09-13.
|
|
5
5
|
- **Revisa:** ADRs 0011 e 0013.
|
|
6
6
|
|
|
7
|
+
> **Atualização (2026-09-28).** A ADR 0029 removeu `PageIntro` e a barra persistente de
|
|
8
|
+
> `PageShell`. As regras abaixo preservam `Content` como dono do cabeçalho da coleção e usam
|
|
9
|
+
> `PageHeader` como a única introdução da página.
|
|
10
|
+
|
|
7
11
|
## Contexto
|
|
8
12
|
|
|
9
13
|
Páginas centradas em uma coleção colocavam o título no `PageIntro`, a criação na barra do
|
|
@@ -23,11 +27,12 @@ as ações da coleção, como criar um item. `ActionList` continua responsável
|
|
|
23
27
|
atualização, visualização, resultados, estados e paginação. `toolbarActions` fica reservado a
|
|
24
28
|
operações ligadas ao recorte corrente da lista, não ao comando primário de criar.
|
|
25
29
|
|
|
26
|
-
`PageHeader`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`Content level={1}` ou um `PageState` ativo no body. Quando `Content` fornece esse
|
|
30
|
-
pode conter somente `PageBody
|
|
30
|
+
`PageHeader` é a única introdução da página e reúne navegação contextual, título e ações globais.
|
|
31
|
+
Dentro de `PageShell`, navegação e ações fornecidas pelo shell são compostas nesse mesmo header,
|
|
32
|
+
sem barra persistente. Uma `Page` explícita mantém exatamente um heading principal: `PageTitle` em
|
|
33
|
+
`PageHeader`, `Content level={1}` ou um `PageState` ativo no body. Quando `Content` fornece esse
|
|
34
|
+
heading, a Page pode conter somente `PageBody`; se `PageShell` acrescentar navegação ou ações,
|
|
35
|
+
`Page` materializa automaticamente o `PageHeader` que as recebe.
|
|
31
36
|
|
|
32
37
|
Na `Presentation`, uma action `list` exibida como Page materializa o título e os comandos de header
|
|
33
38
|
no `Content` que envolve o `ActionList`. Em Dialog e Drawer, o título e esses comandos continuam no
|
|
@@ -72,9 +77,9 @@ responsabilidades locais. Foi descartada para coleções; ações realmente glob
|
|
|
72
77
|
|
|
73
78
|
## Verificação
|
|
74
79
|
|
|
75
|
-
- testes de `Page` cobrem `PageHeader +
|
|
80
|
+
- testes de `Page` cobrem `PageHeader + PageBody` e `PageBody` sem header;
|
|
76
81
|
- `opus check` aceita essa gramática e continua rejeitando slots duplicados ou fora da família;
|
|
77
|
-
- testes de `Presentation` confirmam que Page de listagem usa `Content` e mantém a
|
|
78
|
-
da coleção;
|
|
82
|
+
- testes de `Presentation` confirmam que Page de listagem usa `Content` e mantém a introdução sem a
|
|
83
|
+
ação da coleção;
|
|
79
84
|
- a documentação de `ActionList` recomenda `Content` e reserva `toolbarActions` ao recorte atual;
|
|
80
85
|
- consumidores validam visualmente o título e a criação junto da listagem.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
- **Status:** aceita.
|
|
4
4
|
- **Data:** 2026-09-23.
|
|
5
5
|
- **Complementa:** ADRs 0005, 0014, 0018, 0019 e 0021.
|
|
6
|
+
- **Superada em parte por:** ADR 0030, que permite anatomia seccionada em `Content` com moldura.
|
|
6
7
|
|
|
7
8
|
## Contexto
|
|
8
9
|
|
|
@@ -78,13 +79,13 @@ diferentes para coisas diferentes e permanecem assim.
|
|
|
78
79
|
|
|
79
80
|
`continuous` e `sectioned` não são a ausência e a presença de uma borda. Cada um é um pacote:
|
|
80
81
|
|
|
81
|
-
| Aspecto
|
|
82
|
-
|
|
|
83
|
-
| Divisores
|
|
84
|
-
| Ritmo vertical
|
|
85
|
-
| Escala do título
|
|
86
|
-
| Fundo do footer
|
|
87
|
-
| Alinhamento horizontal | idêntico entre header, body e footer
|
|
82
|
+
| Aspecto | `continuous` | `sectioned` |
|
|
83
|
+
| ---------------------- | -------------------------------------------- | -------------------------------------------------------------- |
|
|
84
|
+
| Divisores | nenhum | entre regiões adjacentes presentes |
|
|
85
|
+
| Ritmo vertical | header aproxima-se do corpo; o espaço separa | regiões simétricas; o divisor separa |
|
|
86
|
+
| Escala do título | a da família | a da família, um degrau mais contida quando a família tem duas |
|
|
87
|
+
| Fundo do footer | igual ao da superfície | sutil, quando a família declara |
|
|
88
|
+
| Alinhamento horizontal | idêntico entre header, body e footer | idêntico entre header, body e footer |
|
|
88
89
|
|
|
89
90
|
O alinhamento horizontal é invariante dos dois layouts: o recuo lateral de header, body e footer de
|
|
90
91
|
uma mesma superfície é sempre o mesmo valor. É o que a suíte transversal verifica.
|
|
@@ -102,15 +103,15 @@ A recomendação de escrever `border-b` e `border-t` no Card sai da documentaç
|
|
|
102
103
|
Os defaults preservam o que cada família já entrega hoje, exceto onde o comportamento atual é o
|
|
103
104
|
defeito que esta decisão corrige:
|
|
104
105
|
|
|
105
|
-
| Família
|
|
106
|
-
|
|
|
107
|
-
| `Card`
|
|
108
|
-
| `Dialog` (default)
|
|
109
|
-
| `Dialog` (`mode="alert"`) | composição própria
|
|
110
|
-
| `Drawer`
|
|
111
|
-
| `Pane`
|
|
112
|
-
| `Content`
|
|
113
|
-
| `Page`
|
|
106
|
+
| Família | Default | Por quê |
|
|
107
|
+
| ------------------------- | ------------------- | ---------------------------------------------------------------- |
|
|
108
|
+
| `Card` | `continuous` | é o que a família entrega hoje: sem divisores, ritmo assimétrico |
|
|
109
|
+
| `Dialog` (default) | `sectioned` | já tem divisores e faixas; o default nomeia o que existe |
|
|
110
|
+
| `Dialog` (`mode="alert"`) | composição própria | a resposta obrigatória mantém sua anatomia centrada |
|
|
111
|
+
| `Drawer` | `sectioned` | idem Dialog |
|
|
112
|
+
| `Pane` | `sectioned` | header e footer já têm divisor |
|
|
113
|
+
| `Content` | contínuo, sem opção | região semântica dentro de outra superfície |
|
|
114
|
+
| `Page` | contínuo, sem opção | a separação do chrome pertence ao shell |
|
|
114
115
|
|
|
115
116
|
Nenhum default muda de valor efetivo com esta ADR. O que muda é que o valor passa a ter nome,
|
|
116
117
|
implementação única e teste.
|
|
@@ -127,7 +128,7 @@ A migração foi decidida sobre 374 usos reais das seis famílias na plataforma.
|
|
|
127
128
|
`p-5`, `px-3 py-2`). O root do Card continua sem padding próprio: quem não usa slots continua
|
|
128
129
|
decidindo. O padding pertence aos slots, como hoje.
|
|
129
130
|
- **Os dois `PaneHeader` do produto escrevem quase a mesma linha** (`flex h-[3.75rem] items-center
|
|
130
|
-
|
|
131
|
+
… p-3 … border-b-0`) em contextos diferentes. É a prova de que padding e divisor pertencem à
|
|
131
132
|
primitiva.
|
|
132
133
|
- **Um comentário no código do login documenta o defeito**: o `py-3` do `CardBody` "conta com um
|
|
133
134
|
CardFooter, que o login não tem". O ritmo do layout contínuo precisa reagir à ausência do rodapé.
|
|
@@ -141,15 +142,15 @@ A migração foi decidida sobre 374 usos reais das seis famílias na plataforma.
|
|
|
141
142
|
A anatomia é comum; o conjunto de slots continua sendo decisão de cada família, porque decisões
|
|
142
143
|
anteriores já os delimitaram:
|
|
143
144
|
|
|
144
|
-
| Slot
|
|
145
|
-
|
|
|
146
|
-
| Media
|
|
147
|
-
| Title
|
|
148
|
-
| Description | sim
|
|
149
|
-
| Meta
|
|
150
|
-
| Actions
|
|
151
|
-
| Body
|
|
152
|
-
| Footer
|
|
145
|
+
| Slot | Card | Content | Page | Dialog | Drawer | Pane |
|
|
146
|
+
| ----------- | ---- | -------------------- | -------------- | ------------------- | -------------- | ---- |
|
|
147
|
+
| Media | sim | sim (`ContentMedia`) | não | sim (`DialogMedia`) | sim | sim |
|
|
148
|
+
| Title | sim | sim | sim | sim | sim | sim |
|
|
149
|
+
| Description | sim | sim | não (ADR 0014) | não (ADR 0018) | não (ADR 0018) | não |
|
|
150
|
+
| Meta | não | sim (contador) | não (ADR 0009) | não | não | não |
|
|
151
|
+
| Actions | sim | sim | sim | sim | sim | sim |
|
|
152
|
+
| Body | sim | sim | sim | sim | sim | sim |
|
|
153
|
+
| Footer | sim | não | sim | sim | sim | sim |
|
|
153
154
|
|
|
154
155
|
As ADRs 0009, 0014 e 0018 não são reabertas: Page, Dialog e Drawer continuam sem descrição no
|
|
155
156
|
header, e Page continua sem contador. `Dialog`, `Drawer` e `Pane` ganham o slot de ações que hoje
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ADR 0029 — PageHeader é a única introdução estrutural da página
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-09-28.
|
|
5
|
+
- **Substitui:** ADR 0011, quanto à barra persistente e à separação entre `PageHeader` e
|
|
6
|
+
`PageIntro`.
|
|
7
|
+
- **Relacionada:** ADR 0016, quanto ao cabeçalho próprio de coleções.
|
|
8
|
+
|
|
9
|
+
## Contexto
|
|
10
|
+
|
|
11
|
+
`PageShell` separou a navegação e as ações globais numa barra persistente, enquanto `PageIntro`
|
|
12
|
+
manteve o título no conteúdo. A separação estabilizou a posição das ações, mas criou duas regiões
|
|
13
|
+
para identificar a mesma página. Nas rotas simples, a barra passou a repetir o item já selecionado
|
|
14
|
+
na navegação da aplicação. Nas rotas profundas, o breadcrumb ficou visualmente distante do título
|
|
15
|
+
que ele contextualiza.
|
|
16
|
+
|
|
17
|
+
Breadcrumb é uma exceção para hierarquias que a navegação persistente não revela. Ele não precisa
|
|
18
|
+
reservar uma faixa em todas as páginas, e uma página de primeiro nível não ganha contexto ao repetir
|
|
19
|
+
seu próprio nome acima do título.
|
|
20
|
+
|
|
21
|
+
## Decisão
|
|
22
|
+
|
|
23
|
+
`PageHeader` é a única introdução estrutural de `Page`:
|
|
24
|
+
|
|
25
|
+
- o header participa da rolagem normal do conteúdo;
|
|
26
|
+
- `PageNavigation` ou `PageBack`, quando presente, ocupa uma linha acima da linha principal;
|
|
27
|
+
- `PageTitle` e `PageActions` compõem a linha principal;
|
|
28
|
+
- páginas sem hierarquia adicional não renderizam nem reservam espaço para navegação;
|
|
29
|
+
- `PageIntro` deixa de fazer parte da anatomia pública;
|
|
30
|
+
- `PageShell` pode coordenar navegação e ações conhecidas pela aplicação, mas não desenha uma barra;
|
|
31
|
+
esses valores são compostos pelo `PageHeader` da página ativa;
|
|
32
|
+
- `PageState` integral continua substituindo o header e fornece o único heading do estado;
|
|
33
|
+
- uma coleção pode continuar fornecendo o heading principal por `Content level={1}` sem criar um
|
|
34
|
+
`PageHeader` redundante.
|
|
35
|
+
|
|
36
|
+
## Consequências
|
|
37
|
+
|
|
38
|
+
- breadcrumb, título e ações voltam a pertencer à mesma região visual;
|
|
39
|
+
- páginas simples começam diretamente pelo título;
|
|
40
|
+
- a altura inicial do conteúdo pode variar quando existe breadcrumb, porque a variação agora
|
|
41
|
+
comunica hierarquia real;
|
|
42
|
+
- consumidores combinam navegação, título e ações em um único `PageHeader` ao migrar da composição
|
|
43
|
+
explícita;
|
|
44
|
+
- `PageShell` deixa de garantir navegação durante estados integrais, pois o estado substitui a
|
|
45
|
+
introdução completa da página;
|
|
46
|
+
- a remoção de `PageIntro` é uma mudança incompatível e requer migração coordenada dos consumidores.
|
|
47
|
+
|
|
48
|
+
## Alternativas descartadas
|
|
49
|
+
|
|
50
|
+
### Manter a barra persistente somente quando houver breadcrumb
|
|
51
|
+
|
|
52
|
+
Evitaria a repetição em páginas simples, mas preservaria duas anatomias para o mesmo cabeçalho e
|
|
53
|
+
manteria a navegação distante do título nas páginas profundas.
|
|
54
|
+
|
|
55
|
+
### Manter `PageIntro` como região opcional abaixo de `PageHeader`
|
|
56
|
+
|
|
57
|
+
Preservaria compatibilidade, mas continuaria exigindo que cada página decidisse qual das duas
|
|
58
|
+
regiões contém o título e as ações.
|
|
59
|
+
|
|
60
|
+
### Copiar a composição no consumidor
|
|
61
|
+
|
|
62
|
+
Validaria uma tela isolada mais rapidamente, porém criaria um segundo contrato visual fora do
|
|
63
|
+
catálogo e não provaria que páginas declarativas e `Presentation` mantêm a mesma anatomia.
|
|
64
|
+
|
|
65
|
+
## Verificação
|
|
66
|
+
|
|
67
|
+
- testes de `Page` cobrem breadcrumb acima da linha de título e ações;
|
|
68
|
+
- shorthand e composição explícita produzem a mesma anatomia;
|
|
69
|
+
- `PageShell` não renderiza barra e entrega navegação e ações ao `PageHeader` descendente;
|
|
70
|
+
- `PageState` integral mantém um único heading e remove o header;
|
|
71
|
+
- testes de `Presentation` cobrem a mesma composição dentro e fora de `PageShell`.
|