@softize/opus 21.0.1 → 22.1.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 +71 -0
- package/bin/cli.mjs +9 -1
- package/bin/lib/check.mjs +37 -29
- 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/adr/0037-design-data-lives-outside-the-contract.md +117 -0
- package/docs/radius-scale.md +5 -2
- package/package.json +5 -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/registry/skills/create-opus-action/references/contract-and-binding.md +5 -3
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +0 -4
- package/registry/skills/test-opus-action/SKILL.md +2 -2
- package/registry/skills/test-opus-action/references/harness.md +2 -2
- package/registry/templates/app/src/App.tsx +1 -1
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +2 -19
- package/registry/templates/app/src/domains/tasks/actions/list.ts +3 -5
- package/registry/templates/app/vite.config.ts +3 -2
- package/src/core/domain.ts +2 -2
- package/src/core/types.ts +6 -20
- package/src/design/index.ts +179 -0
- package/src/testing/fake.ts +2 -2
- package/src/testing/index.ts +7 -3
- 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/actions.md +49 -43
- 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/docs/content/testing.md +9 -25
- package/src/ui/meta.ts +5 -5
- package/src/ui/react.tsx +0 -1
- package/src/vite/design.ts +133 -23
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,77 @@ 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.1.0 — 2026-09-28
|
|
11
|
+
|
|
12
|
+
Os dados do modo design saem do contrato (ADR 0037). No modo design, toda action passa a responder
|
|
13
|
+
sem que o contrato declare nada: a resposta é gerada pelo `fake` a partir do `output`, a mesma
|
|
14
|
+
requisição recebe a mesma resposta, uma `list` devolve três páginas do tamanho pedido em `limit` (20
|
|
15
|
+
por padrão) pela paginação declarada, e a saída repete o `id` da entrada quando o schema aceita.
|
|
16
|
+
Quando o design precisa de coerência, um cenário escrito com `defineDesignScenario`, do novo
|
|
17
|
+
`@softize/opus/design`, responde no lugar da resposta gerada. O plugin encontra os cenários pela
|
|
18
|
+
opção `scenarios` de `opusDesign()`, com caminhos ou globs; um cenário de action inexistente, ou
|
|
19
|
+
dois cenários para a mesma action, impedem o runtime de design de subir, e o erro lista os nomes.
|
|
20
|
+
|
|
21
|
+
`mockHandler` fica obsoleto. Ele ainda responde no modo design, abaixo do cenário e acima da
|
|
22
|
+
resposta gerada, e sai na 23.0.0. Os tipos o marcam com `@deprecated`. O `opus check` passa a
|
|
23
|
+
contar, numa linha de aviso que não reprova, quantas actions ainda o declaram. A sonda
|
|
24
|
+
`/__opus/design` ganha `scenarios` e `generated`; `mocked` e `unmocked` mantêm o formato anterior.
|
|
25
|
+
|
|
26
|
+
O template de `opus create` e as skills `create-opus-action` e `test-opus-action` deixam de ensinar
|
|
27
|
+
`mockHandler`.
|
|
28
|
+
|
|
29
|
+
**O que fazer:** nada é obrigatório nesta versão. Para migrar, apague os `mockHandler` dos
|
|
30
|
+
contratos; se a resposta de algum importar para o design, transforme-a em cenário e aponte a opção
|
|
31
|
+
`scenarios` do plugin para o lugar onde os cenários ficam. Testes que usavam `runAction` com
|
|
32
|
+
`OPUS_MODE=design` para exercitar o mock devem testar a action ligada ao handler.
|
|
33
|
+
|
|
34
|
+
## 22.0.0 — 2026-09-28
|
|
35
|
+
|
|
36
|
+
As superfícies passam a usar uma introdução única e ganham composições explícitas para conteúdo
|
|
37
|
+
seccionado, abas emolduradas e grupos de ações divididos. A mudança elimina wrappers e classes que
|
|
38
|
+
os consumidores repetiam para montar essas formas, além de alinhar Drawer e Content ao raio
|
|
39
|
+
estrutural vigente. É major porque remove `PageIntro`, muda a posição visual do cabeçalho nas páginas
|
|
40
|
+
com `PageShell` e altera o acabamento padrão de Drawer, Tabs `line` e detalhes horizontais com
|
|
41
|
+
divisórias.
|
|
42
|
+
|
|
43
|
+
### Breaking
|
|
44
|
+
|
|
45
|
+
**`PageIntro` foi removido.** `PageHeader` é a única introdução da página e reúne, nessa ordem,
|
|
46
|
+
`PageNavigation`, `PageTitle` e `PageActions`. `PageShell` deixou de renderizar uma barra persistente:
|
|
47
|
+
ele fornece navegação e ações contextuais ao header da página ativa. Migração: mova o título e o
|
|
48
|
+
breadcrumb de `PageIntro` para `PageHeader`; remova imports de `PageIntro` e classes que compensavam
|
|
49
|
+
a barra superior. `PageShell.headerClassName` também foi removida porque não existe mais uma barra
|
|
50
|
+
própria para estilizar; remova a propriedade e, quando necessário, estilize o `PageHeader` da página.
|
|
51
|
+
Um header sem conteúdo renderizável não cria região vazia.
|
|
52
|
+
|
|
53
|
+
**Drawer usa moldura recuada.** `DrawerContent` passa a ter borda completa, `rounded-xl`, recorte e
|
|
54
|
+
`0.25rem` de respiro em relação à viewport. Migração: remova classes locais de borda, raio, recorte e
|
|
55
|
+
inset que reconstruíam essa moldura; preserve apenas dimensões deliberadas, como `max-w-*`.
|
|
56
|
+
|
|
57
|
+
**`TabsList variant="line"` mudou de composição.** A linha-base respeita a largura recebida, a aba
|
|
58
|
+
usa uma área interna da altura de Button com fundo somente no hover e o indicador ativo usa o
|
|
59
|
+
contexto primário. `DialogBody` e `DrawerBody` não publicam mais `--opus-inline-gutter`. Migração:
|
|
60
|
+
remova classes locais que estendiam a linha, simulavam o botão interno ou compensavam o gutter.
|
|
61
|
+
|
|
62
|
+
**`DetailGroup` associa o tratamento tabular a `dividers`.** Em orientação horizontal, divisórias
|
|
63
|
+
passam a aplicar padding por célula, fundo sutil nos rótulos e largura padrão de `8.5rem`, com ou sem
|
|
64
|
+
moldura externa. `variant="framed"` sem divisórias deixa de alterar o fundo dos rótulos. Migração:
|
|
65
|
+
declare `dividers` quando quiser a grade tabular; não use `framed` para escolher o tratamento interno.
|
|
66
|
+
|
|
67
|
+
### Superfícies e controles
|
|
68
|
+
|
|
69
|
+
- `Content` aceita `layout="continuous" | "sectioned"`. O layout seccionado exige `framed`, separa
|
|
70
|
+
o header com divisor e deixa o body sem recuo para tabelas, listas e grades que possuem células.
|
|
71
|
+
Molduras de Content usam `rounded-xl`, inclusive nos acabamentos `default` e `legend`.
|
|
72
|
+
- `Tabs` aceita `framed`, que aplica borda, raio e recorte sem criar fundo próprio, e
|
|
73
|
+
`distribution="content" | "equal"`, que preserva a largura dos rótulos ou reparte igualmente a
|
|
74
|
+
largura disponível entre os gatilhos.
|
|
75
|
+
- `ButtonGroup` aceita `framed` e `mode="divided"`; o modo insere divisores orientados entre os
|
|
76
|
+
controles sem incluí-los no cálculo de `distribution="equal"`.
|
|
77
|
+
- `Button` aceita `context="warning"` em todas as variantes visuais.
|
|
78
|
+
- O filtro composto de período do `ActionList` apresenta período antes do tipo de data, usa divisor
|
|
79
|
+
estrutural e aplica o foco ao grupo quando qualquer controle interno está aberto.
|
|
80
|
+
|
|
10
81
|
## 21.0.1 — 2026-09-26
|
|
11
82
|
|
|
12
83
|
**Correção.** O `status()` do driver BullMQ podia devolver `status: 'done'` com `data: null` para um
|
package/bin/cli.mjs
CHANGED
|
@@ -40,7 +40,15 @@ async function cmdCheck(dir) {
|
|
|
40
40
|
for (const error of materialization.errors) log('error', `✗ opus check: ${error}`)
|
|
41
41
|
process.exit(1)
|
|
42
42
|
}
|
|
43
|
-
const { files, actions, contracts, findings, hasMarker } = await scanDir(root)
|
|
43
|
+
const { files, actions, contracts, deprecatedMockHandlers, findings, hasMarker } = await scanDir(root)
|
|
44
|
+
// Aviso de migração, não violação: uma linha só, para não poluir o gate a cada execução.
|
|
45
|
+
if (deprecatedMockHandlers > 0) {
|
|
46
|
+
log(
|
|
47
|
+
'warn',
|
|
48
|
+
`⚠ opus check: ${deprecatedMockHandlers} action(s) ainda declaram \`mockHandler\`, que sai na próxima major. ` +
|
|
49
|
+
'No modo design, a action sem mock já responde com dado gerado; use `defineDesignScenario` quando o design precisar de um cenário (ADR 0037).',
|
|
50
|
+
)
|
|
51
|
+
}
|
|
44
52
|
|
|
45
53
|
if (actions === 0 && findings.length === 0) {
|
|
46
54
|
const verdict = emptyScanVerdict({ hasMarker, contracts })
|
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
|
{
|
|
@@ -653,6 +642,8 @@ export function checkProject(sources) {
|
|
|
653
642
|
const productDeclarations = [];
|
|
654
643
|
const entityNames = new Set();
|
|
655
644
|
const actionNames = new Set();
|
|
645
|
+
// `mockHandler` obsoleto (ADR 0037): contado, não reprovado — a minor ainda o aceita.
|
|
646
|
+
let deprecatedMockHandlers = 0;
|
|
656
647
|
|
|
657
648
|
for (const { file, text } of sources) {
|
|
658
649
|
const declarations = parseDataProducts(file, text);
|
|
@@ -677,6 +668,7 @@ export function checkProject(sources) {
|
|
|
677
668
|
if (item.name !== null) actionNames.add(item.name);
|
|
678
669
|
}
|
|
679
670
|
for (const f of lintAction(item)) findings.push({ ...f, file });
|
|
671
|
+
if (item.keys.includes("mockHandler")) deprecatedMockHandlers += 1;
|
|
680
672
|
}
|
|
681
673
|
}
|
|
682
674
|
|
|
@@ -782,6 +774,7 @@ export function checkProject(sources) {
|
|
|
782
774
|
actions,
|
|
783
775
|
contracts,
|
|
784
776
|
dataProducts: productDeclarations.length,
|
|
777
|
+
deprecatedMockHandlers,
|
|
785
778
|
findings,
|
|
786
779
|
};
|
|
787
780
|
}
|
|
@@ -1118,11 +1111,14 @@ const jsxSpaceEntities = {
|
|
|
1118
1111
|
* As demais entidades nomeadas ficam como estão: nenhuma delas vira espaço em branco.
|
|
1119
1112
|
*/
|
|
1120
1113
|
function decodeJsxSpaceEntities(raw) {
|
|
1121
|
-
return raw.replace(
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1114
|
+
return raw.replace(
|
|
1115
|
+
/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|([a-zA-Z]+));/g,
|
|
1116
|
+
(entity, dec, hex, name) => {
|
|
1117
|
+
if (name !== undefined) return jsxSpaceEntities[name] ?? entity;
|
|
1118
|
+
const code = dec !== undefined ? Number(dec) : Number.parseInt(hex, 16);
|
|
1119
|
+
return code <= 0x10ffff ? String.fromCodePoint(code) : entity;
|
|
1120
|
+
},
|
|
1121
|
+
);
|
|
1126
1122
|
}
|
|
1127
1123
|
|
|
1128
1124
|
/**
|
|
@@ -1176,7 +1172,10 @@ function invalidSplitChildren(split, imports, localComponents) {
|
|
|
1176
1172
|
return text(node.text, node);
|
|
1177
1173
|
}
|
|
1178
1174
|
if (ts.isNumericLiteral(node)) {
|
|
1179
|
-
invalid.push({
|
|
1175
|
+
invalid.push({
|
|
1176
|
+
label: `o número ${node.text}`,
|
|
1177
|
+
position: node.getStart(),
|
|
1178
|
+
});
|
|
1180
1179
|
return;
|
|
1181
1180
|
}
|
|
1182
1181
|
if (
|
|
@@ -1206,7 +1205,8 @@ function invalidSplitChildren(split, imports, localComponents) {
|
|
|
1206
1205
|
for (const nested of node.children) child(nested);
|
|
1207
1206
|
return;
|
|
1208
1207
|
}
|
|
1209
|
-
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node))
|
|
1208
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node))
|
|
1209
|
+
element(node);
|
|
1210
1210
|
};
|
|
1211
1211
|
for (const node of split.children) child(node);
|
|
1212
1212
|
return invalid;
|
|
@@ -1215,7 +1215,8 @@ function invalidSplitChildren(split, imports, localComponents) {
|
|
|
1215
1215
|
function directImportedJsxChildren(node, imports) {
|
|
1216
1216
|
if (!ts.isJsxElement(node)) return [];
|
|
1217
1217
|
return node.children.flatMap((child) => {
|
|
1218
|
-
if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child))
|
|
1218
|
+
if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child))
|
|
1219
|
+
return [];
|
|
1219
1220
|
const local = jsxLocalName(child);
|
|
1220
1221
|
const imported = local === null ? undefined : imports.get(local);
|
|
1221
1222
|
return imported === undefined ? [] : [{ node: child, component: imported }];
|
|
@@ -1224,7 +1225,8 @@ function directImportedJsxChildren(node, imports) {
|
|
|
1224
1225
|
|
|
1225
1226
|
function jsxNumericAttribute(node, name) {
|
|
1226
1227
|
const attribute = jsxAttribute(jsxOpening(node), name);
|
|
1227
|
-
if (attribute === undefined || attribute.initializer === undefined)
|
|
1228
|
+
if (attribute === undefined || attribute.initializer === undefined)
|
|
1229
|
+
return null;
|
|
1228
1230
|
if (
|
|
1229
1231
|
ts.isJsxExpression(attribute.initializer) &&
|
|
1230
1232
|
attribute.initializer.expression !== undefined &&
|
|
@@ -1243,7 +1245,7 @@ function countPageHeadingSources(page, imports) {
|
|
|
1243
1245
|
let count = 0;
|
|
1244
1246
|
let states = 0;
|
|
1245
1247
|
for (const child of directImportedJsxChildren(page, imports)) {
|
|
1246
|
-
if (child.component === "PageHeader"
|
|
1248
|
+
if (child.component === "PageHeader") {
|
|
1247
1249
|
count += directImportedJsxChildren(child.node, imports).filter(
|
|
1248
1250
|
(entry) => entry.component === "PageTitle",
|
|
1249
1251
|
).length;
|
|
@@ -1271,7 +1273,8 @@ function countPageHeadingSources(page, imports) {
|
|
|
1271
1273
|
|
|
1272
1274
|
function pageBodyHasOpaqueDescendant(page, imports) {
|
|
1273
1275
|
for (const child of directImportedJsxChildren(page, imports)) {
|
|
1274
|
-
if (child.component !== "PageBody" || !ts.isJsxElement(child.node))
|
|
1276
|
+
if (child.component !== "PageBody" || !ts.isJsxElement(child.node))
|
|
1277
|
+
continue;
|
|
1275
1278
|
let opaque = false;
|
|
1276
1279
|
const visit = (node) => {
|
|
1277
1280
|
if (opaque) return;
|
|
@@ -1323,7 +1326,11 @@ export function checkUiStructure(file, text) {
|
|
|
1323
1326
|
const local = jsxLocalName(node);
|
|
1324
1327
|
const component = local === null ? null : imports.get(local);
|
|
1325
1328
|
if (component === "Split" && ts.isJsxElement(node)) {
|
|
1326
|
-
for (const child of invalidSplitChildren(
|
|
1329
|
+
for (const child of invalidSplitChildren(
|
|
1330
|
+
node,
|
|
1331
|
+
imports,
|
|
1332
|
+
localComponents,
|
|
1333
|
+
)) {
|
|
1327
1334
|
findings.push({
|
|
1328
1335
|
rule: "ui-structure-split",
|
|
1329
1336
|
level: "error",
|
|
@@ -1552,7 +1559,7 @@ export function checkUiStructure(file, text) {
|
|
|
1552
1559
|
add(
|
|
1553
1560
|
node,
|
|
1554
1561
|
component,
|
|
1555
|
-
"Page explícito exige exatamente um heading principal em PageHeader,
|
|
1562
|
+
"Page explícito exige exatamente um heading principal em PageHeader, Content level={1} ou PageState dentro de PageBody.",
|
|
1556
1563
|
);
|
|
1557
1564
|
}
|
|
1558
1565
|
}
|
|
@@ -1918,7 +1925,7 @@ async function walkUiFiles(dir, acc = []) {
|
|
|
1918
1925
|
return acc;
|
|
1919
1926
|
}
|
|
1920
1927
|
|
|
1921
|
-
/** Escaneia um diretório → { files, actions, contracts, findings, hasMarker }.
|
|
1928
|
+
/** Escaneia um diretório → { files, actions, contracts, deprecatedMockHandlers, findings, hasMarker }.
|
|
1922
1929
|
* `hasMarker` = o diretório é um projeto opus (tem opus.json na raiz). */
|
|
1923
1930
|
export async function scanDir(rootDir) {
|
|
1924
1931
|
rootDir = canonicalProjectDirectory(rootDir);
|
|
@@ -1948,6 +1955,7 @@ export async function scanDir(rootDir) {
|
|
|
1948
1955
|
actions: checked.actions,
|
|
1949
1956
|
contracts: checked.contracts,
|
|
1950
1957
|
dataProducts: checked.dataProducts,
|
|
1958
|
+
deprecatedMockHandlers: checked.deprecatedMockHandlers,
|
|
1951
1959
|
findings: [...checked.findings, ...uiFindings],
|
|
1952
1960
|
hasMarker,
|
|
1953
1961
|
};
|
|
@@ -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
|