@softize/opus 27.1.0 → 28.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 +55 -0
- package/bin/lib/check.mjs +139 -30
- package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +161 -0
- package/docs/adr/README.md +2 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -15
- package/src/ui/components/patterns/list.tsx +62 -53
- package/src/ui/components/patterns/page.tsx +146 -38
- package/src/ui/components/patterns/presentation.tsx +44 -59
- package/src/ui/components/patterns/structural-header.tsx +42 -6
- package/src/ui/components/patterns/surface-anatomy.ts +52 -22
- package/src/ui/components/patterns/surface.tsx +2 -3
- package/src/ui/components/primitives/control.ts +2 -2
- package/src/ui/components/primitives/dialog.tsx +7 -2
- package/src/ui/components/primitives/drawer.tsx +7 -2
- package/src/ui/components/primitives/icon-picker.tsx +271 -34
- package/src/ui/components/primitives/pagination.tsx +5 -4
- package/src/ui/components/primitives/tabs.tsx +15 -2
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/docs/content/action-list.md +11 -2
- package/src/ui/docs/content/button.md +7 -7
- package/src/ui/docs/content/compositions.md +35 -23
- package/src/ui/docs/content/dialog.md +18 -6
- package/src/ui/docs/content/drawer.md +17 -5
- package/src/ui/docs/content/icon-picker.md +37 -19
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/page.md +169 -69
- package/src/ui/docs/content/pagination.md +7 -8
- package/src/ui/docs/content/presentation.md +4 -3
- package/src/ui/docs/content/surface.md +8 -5
- package/src/ui/docs/content/tabs.md +12 -4
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,61 @@ 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
|
+
## 28.0.0 — 2026-10-13
|
|
11
|
+
|
|
12
|
+
### Breaking
|
|
13
|
+
|
|
14
|
+
Page passa a usar `surfaceLayout="sectioned"` por padrão; `continuous` remove os divisores.
|
|
15
|
+
`className` de Page configura a moldura externa e o de PageBody, o viewport. Migre ajustes
|
|
16
|
+
internos de largura e alinhamento para `contentClassName`, disponível nos dois.
|
|
17
|
+
PageShell preserva rolagem de contingência para fallbacks; wrappers de rota devem propagar
|
|
18
|
+
altura para manter o header da Page fixo. Presentation fora do shell mantém largura total.
|
|
19
|
+
TabsPanel com `forceMount` preserva a montagem, mas oculta o painel inativo.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
A composição e a escala das superfícies mudam. Migre os consumidores conforme a ADR 0060 e remova os recuos locais substituídos pelo body.
|
|
23
|
+
|
|
24
|
+
DrawerTitle e DialogTitle na intro acompanham a escala do PageTitle; o header compacto
|
|
25
|
+
preserva seu título menor.
|
|
26
|
+
|
|
27
|
+
Surface, Page, Dialog, Drawer e Tabs compartilham ritmo vertical de 1rem. Superfícies delimitadas
|
|
28
|
+
fecham com padding inferior; corpos sem moldura ficam alinhados ao hospedeiro. Tabs framed
|
|
29
|
+
fornece os recuos do painel. Remova overrides locais que reconstruíam esses espaçamentos.
|
|
30
|
+
|
|
31
|
+
IconCatalogProvider permite catálogo assíncrono e renderização de assets sob demanda no IconPicker,
|
|
32
|
+
com busca no índice completo, grade de oito colunas com scroll infinito e tentativa após erro.
|
|
33
|
+
|
|
34
|
+
IconPicker apresenta uma grade sem nomes visíveis, com nomes acessíveis e busca por nomes e sinônimos
|
|
35
|
+
em português. O scroll do popover não se propaga ao modal. `searchTerms` personaliza a busca
|
|
36
|
+
sem modificar identificadores salvos. A paleta inclui Carro, pesquisável também por veículo e automóvel.
|
|
37
|
+
|
|
38
|
+
Toggle `outline` volta a distinguir o preenchimento ativo da borda, com fundo semitransparente
|
|
39
|
+
e preservação do formato conectado.
|
|
40
|
+
|
|
41
|
+
ActionList oferece `viewControls` para controles específicos da visualização ativa,
|
|
42
|
+
imediatamente após seu seletor. As configurações da lista aparecem antes dos seletores.
|
|
43
|
+
|
|
44
|
+
### Escala dos controles e composição
|
|
45
|
+
|
|
46
|
+
Corrigida a altura de `icon-sm`: 2rem, igual a `sm`, preservando o glifo de 0,875rem.
|
|
47
|
+
PaginationLink respeita o tamanho textual declarado; números, setas e elipse do pager
|
|
48
|
+
de ActionList usam 2rem. Remova compensações locais que fixavam esses controles em 1,75rem.
|
|
49
|
+
|
|
50
|
+
Page, Dialog, Drawer e Surface delimitada compartilham header com padding de 1rem e
|
|
51
|
+
linha mínima de 2rem. Ações de header usam `sm`/`icon-sm`, inclusive criação de coleções
|
|
52
|
+
e comandos de Presentation. Rodapés mantêm a escala anterior. Consumidores devem remover
|
|
53
|
+
compensações de altura/padding e declarar a escala compacta nos botões das barras.
|
|
54
|
+
|
|
55
|
+
Presentation em página passa a delegar padding e rolagem ao PageBody, evitando conteúdo
|
|
56
|
+
recortado e recuos duplicados. Ações de headers compactos sem navegação permanecem no
|
|
57
|
+
extremo oposto ao título.
|
|
58
|
+
|
|
59
|
+
Page oferece `PageIntro` e `PageIntroActions`, seguindo a separação entre controles da superfície
|
|
60
|
+
e conteúdo já disponível em Dialog e Drawer. A forma curta passa a colocar título e orientação
|
|
61
|
+
na intro, mantendo ações no header. Headers sem controles não reservam espaço. A forma explícita
|
|
62
|
+
compacta continua válida. A mudança visual exige migração coordenada dos consumidores (ADR 0060).
|
|
63
|
+
Page mantém header e footer fora da rolagem; o body rola em flow e repassa a altura à lista em fill.
|
|
64
|
+
|
|
10
65
|
## 27.1.0 — 2026-10-13
|
|
11
66
|
|
|
12
67
|
Portais documentais podem oferecer navegação recolhível em telas estreitas com
|
package/bin/lib/check.mjs
CHANGED
|
@@ -93,7 +93,11 @@ const KINDS = new Set(["simple", "form", "list", "view"]);
|
|
|
93
93
|
const ASSISTANT_RESOURCE_RE = /^[a-z][a-zA-Z0-9]*$/;
|
|
94
94
|
const ASSISTANT_SKILL_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
95
95
|
const ASSISTANT_CONTEXT_MIN = 1000;
|
|
96
|
-
const ASSISTANT_RESERVED_SEGMENTS = new Set([
|
|
96
|
+
const ASSISTANT_RESERVED_SEGMENTS = new Set([
|
|
97
|
+
"__proto__",
|
|
98
|
+
"constructor",
|
|
99
|
+
"prototype",
|
|
100
|
+
]);
|
|
97
101
|
const ASSISTANT_CONTEXT_MAX = 16000;
|
|
98
102
|
const ASSISTANT_REQUIRED = {
|
|
99
103
|
label: "`label`, o nome do assistente para pessoas",
|
|
@@ -150,9 +154,11 @@ const UI_SURFACE_PAIRS = new Map([
|
|
|
150
154
|
const UI_STRUCTURAL_PARENTS = new Map([
|
|
151
155
|
["PageHeader", new Set(["Page"])],
|
|
152
156
|
["PageBody", new Set(["Page"])],
|
|
157
|
+
["PageIntro", new Set(["PageBody"])],
|
|
158
|
+
["PageIntroActions", new Set(["PageIntro"])],
|
|
153
159
|
["PageFooter", new Set(["Page"])],
|
|
154
|
-
["PageTitle", new Set(["PageHeader"])],
|
|
155
|
-
["PageDescription", new Set(["PageHeader"])],
|
|
160
|
+
["PageTitle", new Set(["PageHeader", "PageIntro"])],
|
|
161
|
+
["PageDescription", new Set(["PageHeader", "PageIntro"])],
|
|
156
162
|
["PageBack", new Set(["PageHeader"])],
|
|
157
163
|
["PageNavigation", new Set(["PageHeader"])],
|
|
158
164
|
["PageActions", new Set(["PageHeader"])],
|
|
@@ -311,7 +317,6 @@ const UI_REMOVED_PROPS = new Map([
|
|
|
311
317
|
["variant", "o cabeçalho da página tem uma única apresentação rolável"],
|
|
312
318
|
]),
|
|
313
319
|
],
|
|
314
|
-
|
|
315
320
|
]);
|
|
316
321
|
|
|
317
322
|
const UI_REMOVED_COMPONENTS = new Map([
|
|
@@ -323,7 +328,6 @@ const UI_REMOVED_COMPONENTS = new Map([
|
|
|
323
328
|
["PaneTitle", "use `SurfaceTitle` dentro de `SurfaceHeader`"],
|
|
324
329
|
["PaneDescription", "use `SurfaceDescription` dentro de `SurfaceHeader`"],
|
|
325
330
|
["PaneActions", "use `SurfaceActions` dentro de `SurfaceHeader`"],
|
|
326
|
-
["PageIntro", "use `PageHeader` como a única introdução estrutural"],
|
|
327
331
|
// Content e Card deram lugar a Surface (ADR 0045).
|
|
328
332
|
[
|
|
329
333
|
"Content",
|
|
@@ -356,6 +360,14 @@ const UI_REMOVED_COMPONENTS = new Map([
|
|
|
356
360
|
const UI_STRUCTURAL_CONTAINERS = new Set(["PageShell"]);
|
|
357
361
|
|
|
358
362
|
const UI_STRUCTURAL_HEADERS = new Map([
|
|
363
|
+
[
|
|
364
|
+
"PageIntro",
|
|
365
|
+
{
|
|
366
|
+
title: "PageTitle",
|
|
367
|
+
optional: new Set(["PageIntroActions", "PageDescription"]),
|
|
368
|
+
shorthand: new Set(),
|
|
369
|
+
},
|
|
370
|
+
],
|
|
359
371
|
[
|
|
360
372
|
"DialogIntro",
|
|
361
373
|
{
|
|
@@ -376,7 +388,12 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
376
388
|
"PageHeader",
|
|
377
389
|
{
|
|
378
390
|
title: "PageTitle",
|
|
379
|
-
optional: new Set([
|
|
391
|
+
optional: new Set([
|
|
392
|
+
"PageBack",
|
|
393
|
+
"PageNavigation",
|
|
394
|
+
"PageActions",
|
|
395
|
+
"PageDescription",
|
|
396
|
+
]),
|
|
380
397
|
exclusive: [new Set(["PageBack", "PageNavigation"])],
|
|
381
398
|
shorthand: new Set(),
|
|
382
399
|
titleRequired: false,
|
|
@@ -568,7 +585,8 @@ export function parseRetention(fileName, sourceText) {
|
|
|
568
585
|
if (item === undefined) return undefined;
|
|
569
586
|
if (!ts.isPropertyAssignment(item)) return null;
|
|
570
587
|
const init = item.initializer;
|
|
571
|
-
return ts.isStringLiteral(init) ||
|
|
588
|
+
return ts.isStringLiteral(init) ||
|
|
589
|
+
ts.isNoSubstitutionTemplateLiteral(init)
|
|
572
590
|
? init.text
|
|
573
591
|
: null;
|
|
574
592
|
};
|
|
@@ -797,7 +815,9 @@ function checkRetention(retention) {
|
|
|
797
815
|
if (batch !== null && (!Number.isInteger(batch) || batch < 1))
|
|
798
816
|
at("`batchSize` precisa ser um inteiro positivo.");
|
|
799
817
|
else if (batch !== null && batch > MAX_PURGE_BATCH_SIZE)
|
|
800
|
-
at(
|
|
818
|
+
at(
|
|
819
|
+
`\`batchSize\` aceita no máximo ${MAX_PURGE_BATCH_SIZE} linhas por lote.`,
|
|
820
|
+
);
|
|
801
821
|
const inDomain =
|
|
802
822
|
entity.inlineInDomain ||
|
|
803
823
|
(entity.ident !== null && entityRefs.has(entity.ident));
|
|
@@ -813,7 +833,9 @@ function checkRetention(retention) {
|
|
|
813
833
|
for (const purge of parsed.purges) {
|
|
814
834
|
const subject = `bindPurge(${purge.entityRef ?? "?"})`;
|
|
815
835
|
const entity =
|
|
816
|
-
purge.entityRef === null
|
|
836
|
+
purge.entityRef === null
|
|
837
|
+
? undefined
|
|
838
|
+
: entitiesByIdent.get(purge.entityRef);
|
|
817
839
|
if (
|
|
818
840
|
entity !== undefined &&
|
|
819
841
|
entity !== "ambiguous" &&
|
|
@@ -982,15 +1004,20 @@ export function parseActions(fileName, sourceText) {
|
|
|
982
1004
|
/** `ai` literal → "enabled" | "disabled"; ausente → null; não literal → "unknown". */
|
|
983
1005
|
function aiState(obj) {
|
|
984
1006
|
const p = prop(obj, "ai");
|
|
985
|
-
if (!p)
|
|
1007
|
+
if (!p)
|
|
1008
|
+
return obj.properties.some((pr) => pr.name?.getText(sf) === "ai")
|
|
1009
|
+
? "unknown"
|
|
1010
|
+
: null;
|
|
986
1011
|
const value = p.initializer;
|
|
987
1012
|
if (value.kind === ts.SyntaxKind.TrueKeyword) return "enabled";
|
|
988
1013
|
if (value.kind === ts.SyntaxKind.FalseKeyword) return "disabled";
|
|
989
1014
|
if (ts.isObjectLiteralExpression(value)) {
|
|
990
1015
|
const enabled = prop(value, "enabled");
|
|
991
1016
|
if (!enabled) return "enabled";
|
|
992
|
-
if (enabled.initializer.kind === ts.SyntaxKind.FalseKeyword)
|
|
993
|
-
|
|
1017
|
+
if (enabled.initializer.kind === ts.SyntaxKind.FalseKeyword)
|
|
1018
|
+
return "disabled";
|
|
1019
|
+
if (enabled.initializer.kind === ts.SyntaxKind.TrueKeyword)
|
|
1020
|
+
return "enabled";
|
|
994
1021
|
}
|
|
995
1022
|
return "unknown";
|
|
996
1023
|
}
|
|
@@ -1007,14 +1034,17 @@ export function parseActions(fileName, sourceText) {
|
|
|
1007
1034
|
if (!found) return { present: objKeys(value).includes(key), text: null };
|
|
1008
1035
|
return {
|
|
1009
1036
|
present: true,
|
|
1010
|
-
text: ts.isStringLiteralLike(found.initializer)
|
|
1037
|
+
text: ts.isStringLiteralLike(found.initializer)
|
|
1038
|
+
? found.initializer.text
|
|
1039
|
+
: null,
|
|
1011
1040
|
};
|
|
1012
1041
|
};
|
|
1013
1042
|
const limit = prop(value, "maxContextChars");
|
|
1014
1043
|
let maxContextChars = null;
|
|
1015
1044
|
if (limit) {
|
|
1016
1045
|
const node = limit.initializer;
|
|
1017
|
-
if (ts.isNumericLiteral(node))
|
|
1046
|
+
if (ts.isNumericLiteral(node))
|
|
1047
|
+
maxContextChars = Number(node.text.replace(/_/g, ""));
|
|
1018
1048
|
else if (
|
|
1019
1049
|
ts.isPrefixUnaryExpression(node) &&
|
|
1020
1050
|
node.operator === ts.SyntaxKind.MinusToken &&
|
|
@@ -1025,7 +1055,9 @@ export function parseActions(fileName, sourceText) {
|
|
|
1025
1055
|
const contextNode = prop(value, "context");
|
|
1026
1056
|
const contextPaths =
|
|
1027
1057
|
contextNode && ts.isArrayLiteralExpression(contextNode.initializer)
|
|
1028
|
-
? contextNode.initializer.elements
|
|
1058
|
+
? contextNode.initializer.elements
|
|
1059
|
+
.filter(ts.isStringLiteralLike)
|
|
1060
|
+
.map((element) => element.text)
|
|
1029
1061
|
: [];
|
|
1030
1062
|
return {
|
|
1031
1063
|
readable: true,
|
|
@@ -1110,17 +1142,27 @@ function assistantMessages(a) {
|
|
|
1110
1142
|
if (!assistant.readable) return out;
|
|
1111
1143
|
const { resource, skill } = assistant;
|
|
1112
1144
|
if (!resource.present)
|
|
1113
|
-
out.push(
|
|
1145
|
+
out.push(
|
|
1146
|
+
"O assistente precisa declarar `resource`, o tipo de recurso de que a action é dona.",
|
|
1147
|
+
);
|
|
1114
1148
|
else if (resource.text !== null && !ASSISTANT_RESOURCE_RE.test(resource.text))
|
|
1115
|
-
out.push(
|
|
1149
|
+
out.push(
|
|
1150
|
+
`\`resource: "${resource.text}"\` precisa estar em camelCase, como \`customer\` ou \`aftersaleOpportunity\`.`,
|
|
1151
|
+
);
|
|
1116
1152
|
if (!skill.present)
|
|
1117
|
-
out.push(
|
|
1153
|
+
out.push(
|
|
1154
|
+
"O assistente precisa declarar `skill`, a chave do procedimento que a IA segue.",
|
|
1155
|
+
);
|
|
1118
1156
|
else if (skill.text !== null && !ASSISTANT_SKILL_RE.test(skill.text))
|
|
1119
|
-
out.push(
|
|
1157
|
+
out.push(
|
|
1158
|
+
`\`skill: "${skill.text}"\` precisa estar em minúsculas com hífen, como \`review-customer\`.`,
|
|
1159
|
+
);
|
|
1120
1160
|
const reserved = [assistant.title.text, ...assistant.contextPaths].filter(
|
|
1121
1161
|
(path) =>
|
|
1122
1162
|
typeof path === "string" &&
|
|
1123
|
-
path
|
|
1163
|
+
path
|
|
1164
|
+
.split(".")
|
|
1165
|
+
.some((segment) => ASSISTANT_RESERVED_SEGMENTS.has(segment)),
|
|
1124
1166
|
);
|
|
1125
1167
|
if (reserved.length > 0)
|
|
1126
1168
|
out.push(
|
|
@@ -1129,12 +1171,16 @@ function assistantMessages(a) {
|
|
|
1129
1171
|
const limit = assistant.maxContextChars;
|
|
1130
1172
|
if (
|
|
1131
1173
|
limit !== null &&
|
|
1132
|
-
(!Number.isInteger(limit) ||
|
|
1174
|
+
(!Number.isInteger(limit) ||
|
|
1175
|
+
limit < ASSISTANT_CONTEXT_MIN ||
|
|
1176
|
+
limit > ASSISTANT_CONTEXT_MAX)
|
|
1133
1177
|
)
|
|
1134
1178
|
out.push(
|
|
1135
1179
|
`\`maxContextChars: ${limit}\` precisa ser um inteiro de 1.000 a 16.000; sem ele, o teto é de 4.000 caracteres.`,
|
|
1136
1180
|
);
|
|
1137
|
-
const missing = Object.keys(ASSISTANT_REQUIRED).filter(
|
|
1181
|
+
const missing = Object.keys(ASSISTANT_REQUIRED).filter(
|
|
1182
|
+
(key) => !assistant.keys.includes(key),
|
|
1183
|
+
);
|
|
1138
1184
|
if (missing.length > 0)
|
|
1139
1185
|
out.push(
|
|
1140
1186
|
`O assistente precisa declarar ${missing.map((key) => ASSISTANT_REQUIRED[key]).join("; ")}.`,
|
|
@@ -1402,7 +1448,9 @@ export function checkProject(sources) {
|
|
|
1402
1448
|
|
|
1403
1449
|
for (const [resource, owners] of assistantResources) {
|
|
1404
1450
|
if (owners.length < 2) continue;
|
|
1405
|
-
const names = owners.map(
|
|
1451
|
+
const names = owners.map(
|
|
1452
|
+
({ item }) => item.name ?? item.ident ?? "(sem name)",
|
|
1453
|
+
);
|
|
1406
1454
|
for (const { item, file } of owners) {
|
|
1407
1455
|
findings.push({
|
|
1408
1456
|
rule: "assistant-declaration",
|
|
@@ -1707,6 +1755,22 @@ function directStructuralChildren(node, imports) {
|
|
|
1707
1755
|
for (const child of node.children) {
|
|
1708
1756
|
if (ts.isJsxText(child) && child.text.trim() === "") continue;
|
|
1709
1757
|
if (ts.isJsxExpression(child) && child.expression === undefined) continue;
|
|
1758
|
+
if (
|
|
1759
|
+
ts.isJsxExpression(child) &&
|
|
1760
|
+
child.expression &&
|
|
1761
|
+
ts.isConditionalExpression(child.expression)
|
|
1762
|
+
) {
|
|
1763
|
+
const branches = [child.expression.whenTrue, child.expression.whenFalse];
|
|
1764
|
+
const slots = branches.map((branch) =>
|
|
1765
|
+
ts.isJsxElement(branch) || ts.isJsxSelfClosingElement(branch)
|
|
1766
|
+
? imports.get(jsxLocalName(branch))
|
|
1767
|
+
: undefined,
|
|
1768
|
+
);
|
|
1769
|
+
if (slots[0] !== undefined && slots[0] === slots[1]) {
|
|
1770
|
+
names.push(slots[0]);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1710
1774
|
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
|
|
1711
1775
|
const local = jsxLocalName(child);
|
|
1712
1776
|
const imported = local === null ? null : imports.get(local);
|
|
@@ -1957,6 +2021,7 @@ function countPageHeadingSources(page, imports) {
|
|
|
1957
2021
|
local !== null &&
|
|
1958
2022
|
((imports.get(local) === "Surface" &&
|
|
1959
2023
|
jsxNumericAttribute(node, "level") === 1) ||
|
|
2024
|
+
imports.get(local) === "PageIntro" ||
|
|
1960
2025
|
imports.get(local) === "PageState")
|
|
1961
2026
|
) {
|
|
1962
2027
|
if (imports.get(local) === "PageState") states += 1;
|
|
@@ -2095,11 +2160,55 @@ export function checkUiStructure(file, text) {
|
|
|
2095
2160
|
}
|
|
2096
2161
|
}
|
|
2097
2162
|
|
|
2098
|
-
if (
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2163
|
+
if (["PageBody", "DialogBody", "DrawerBody"].includes(component)) {
|
|
2164
|
+
const intro = component.replace("Body", "Intro");
|
|
2165
|
+
const visible = (children) =>
|
|
2166
|
+
children.flatMap((child) => {
|
|
2167
|
+
if (ts.isJsxText(child)) return child.text.trim() ? [child] : [];
|
|
2168
|
+
if (ts.isJsxFragment(child)) return visible(child.children);
|
|
2169
|
+
if (
|
|
2170
|
+
ts.isJsxExpression(child) &&
|
|
2171
|
+
(!child.expression ||
|
|
2172
|
+
[
|
|
2173
|
+
ts.SyntaxKind.NullKeyword,
|
|
2174
|
+
ts.SyntaxKind.FalseKeyword,
|
|
2175
|
+
ts.SyntaxKind.TrueKeyword,
|
|
2176
|
+
].includes(child.expression.kind))
|
|
2177
|
+
)
|
|
2178
|
+
return [];
|
|
2179
|
+
return [child];
|
|
2180
|
+
});
|
|
2181
|
+
const children = ts.isJsxElement(node) ? visible(node.children) : [];
|
|
2182
|
+
const indexes = children.flatMap((child, index) =>
|
|
2183
|
+
(ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) &&
|
|
2184
|
+
imports.get(jsxLocalName(child)) === intro
|
|
2185
|
+
? [index]
|
|
2186
|
+
: [],
|
|
2187
|
+
);
|
|
2188
|
+
if (indexes.length > 1 || (indexes.length === 1 && indexes[0] !== 0)) {
|
|
2189
|
+
add(
|
|
2190
|
+
node,
|
|
2191
|
+
component,
|
|
2192
|
+
`${intro} deve ser o primeiro e único bloco introdutório de ${component}.`,
|
|
2193
|
+
"ui-structure-intro-order",
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
if (
|
|
2199
|
+
component !== null &&
|
|
2200
|
+
(UI_STRUCTURAL_BODIES.has(component) || component === "Page")
|
|
2201
|
+
) {
|
|
2202
|
+
const attributes =
|
|
2203
|
+
component === "Page"
|
|
2204
|
+
? ["contentClassName"]
|
|
2205
|
+
: component === "PageBody"
|
|
2206
|
+
? ["className", "contentClassName"]
|
|
2207
|
+
: ["className"];
|
|
2208
|
+
const overridden = attributes
|
|
2209
|
+
.flatMap((attribute) =>
|
|
2210
|
+
jsxStringAttributeValues(jsxOpening(node), attribute),
|
|
2211
|
+
)
|
|
2103
2212
|
.flatMap((value) => value.trim().split(/\s+/))
|
|
2104
2213
|
.filter((token) => UI_BODY_RHYTHM_CLASS.test(token));
|
|
2105
2214
|
if (overridden.length > 0) {
|
|
@@ -2686,8 +2795,8 @@ export async function scanDir(rootDir) {
|
|
|
2686
2795
|
for (const file of files) {
|
|
2687
2796
|
const text = readProjectFile(rootDir, path.relative(rootDir, file)).content;
|
|
2688
2797
|
if (
|
|
2689
|
-
![...ACTION_MARKERS, ...DATA_PRODUCT_MARKERS, ...RETENTION_MARKERS].some(
|
|
2690
|
-
text.includes(m),
|
|
2798
|
+
![...ACTION_MARKERS, ...DATA_PRODUCT_MARKERS, ...RETENTION_MARKERS].some(
|
|
2799
|
+
(m) => text.includes(m),
|
|
2691
2800
|
)
|
|
2692
2801
|
)
|
|
2693
2802
|
continue;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# ADR 0015 — O tamanho da ação acompanha a densidade da interação
|
|
2
2
|
|
|
3
|
+
> Parcialmente substituída pela [ADR 0060](0060-surface-composition-separates-chrome-and-intro.md), que define a composição e a escala atuais dos headers.
|
|
4
|
+
|
|
3
5
|
- **Status:** aceita.
|
|
4
6
|
- **Data:** 2026-09-12.
|
|
5
7
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# ADR 0029 — PageHeader é a única introdução estrutural da página
|
|
2
2
|
|
|
3
|
-
-
|
|
3
|
+
> Parcialmente substituída pela [ADR 0060](0060-surface-composition-separates-chrome-and-intro.md), que define a composição e a escala atuais dos headers.
|
|
4
|
+
|
|
5
|
+
- **Status:** substituída parcialmente pela ADR 0060 na versão 28.0.0 da composição com intro.
|
|
4
6
|
- **Data:** 2026-09-28.
|
|
5
7
|
- **Substitui:** ADR 0011, quanto à barra persistente e à separação entre `PageHeader` e
|
|
6
8
|
`PageIntro`.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# ADR 0060 — A composição separa controles da superfície e introdução do conteúdo
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-10.
|
|
5
|
+
- **Substitui parcialmente:** ADR 0029, quanto à proibição de PageIntro.
|
|
6
|
+
- **Complementa:** ADRs 0023 e 0048.
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
Page perdeu a intro para evitar barras vazias e identificação redundante. Depois, Dialog e Drawer
|
|
11
|
+
passaram a oferecer uma intro para separar os controles da superfície do conteúdo. A mesma página
|
|
12
|
+
editorial passou a mudar o lugar do título e das ações quando aberta em outro hospedeiro.
|
|
13
|
+
|
|
14
|
+
## Decisão
|
|
15
|
+
|
|
16
|
+
Page, Dialog e Drawer compartilham duas composições, declaradas pelos slots, sem prop redundante:
|
|
17
|
+
|
|
18
|
+
- compacta: o título e a orientação ficam no Header;
|
|
19
|
+
- com intro: o Header recebe navegação e ações da superfície; a Intro é o primeiro bloco do Body
|
|
20
|
+
e recebe o único título, orientação opcional e ações locais por IntroActions.
|
|
21
|
+
|
|
22
|
+
Header sem controles não reserva espaço. Navegação não é repetida na intro. O fechamento continua
|
|
23
|
+
sendo responsabilidade do modal; Page não ganha um botão de fechar implícito. A posição da intro
|
|
24
|
+
não muda o significado de layout: continuous/sectioned controla regiões e flow/fill controla altura.
|
|
25
|
+
|
|
26
|
+
Page ganha PageIntro e PageIntroActions. A forma curta passa a colocar title e description na intro,
|
|
27
|
+
com actions no Header. A forma explícita preserva a composição compacta para situações deliberadas.
|
|
28
|
+
PageShell continua fornecendo contexto sem criar outra barra. Estados integrais continuam substituindo
|
|
29
|
+
a introdução e o conteúdo, com um único heading acessível. O teto centralizado de 80rem é preservado.
|
|
30
|
+
|
|
31
|
+
Surface mantém o cabeçalho do conteúdo, inclusive ações locais: não possui navegação ou fechamento
|
|
32
|
+
que justifique uma segunda região. Pane e Split continuam sendo apenas layout. Alert mantém sua
|
|
33
|
+
composição especializada. Patterns de formulário, coleção e Presentation precisam respeitar a mesma
|
|
34
|
+
semântica ao montar os componentes, sem reconstruir linhas no consumidor.
|
|
35
|
+
|
|
36
|
+
## Verificação e migração
|
|
37
|
+
|
|
38
|
+
O checker valida família, posição da intro, unicidade do título e ações no slot correspondente.
|
|
39
|
+
Testes renderizados cobrem as duas composições, estados integrais, ausência de barra vazia e contexto
|
|
40
|
+
do shell. Consumidores de Page explícita migram título/descrição para PageIntro quando suas ações
|
|
41
|
+
pertencem à superfície; cabeçalhos de coleções em Surface mantêm criação junto à coleção.
|
|
42
|
+
|
|
43
|
+
A migração inclui SDK, exemplos, documentação e aplicação. Publicação e adoção definitiva só ocorrem
|
|
44
|
+
após os gates e a revisão independente do conjunto. A versão 28.0.0 consolida a migração.
|
|
45
|
+
|
|
46
|
+
## Alternativas
|
|
47
|
+
|
|
48
|
+
Uma prop compact duplicaria informação já expressa pelos slots, abrindo combinações contraditórias.
|
|
49
|
+
Manter Page diferente preservaria compatibilidade, mas perpetuaria a diferença entre hospedeiros.
|
|
50
|
+
Uma barra obrigatória recuperaria o espaço vazio e a identificação repetida que motivaram a ADR 0029.
|
|
51
|
+
|
|
52
|
+
## Rolagem
|
|
53
|
+
|
|
54
|
+
Page ocupa a altura disponível do hospedeiro, com header e footer fora da região rolável.
|
|
55
|
+
PageBody é o viewport vertical; a intro rola com o conteúdo. PageShell conserva a rolagem horizontal do canvas mínimo e permite rolagem vertical de
|
|
56
|
+
contingência para rotas sem Page ou wrappers sem altura definida. Em layout fill, quando uma ActionList assume a rolagem, o body repassa
|
|
57
|
+
a altura e não cria uma segunda rolagem vertical. Sem folha fill, o próprio body continua rolando.
|
|
58
|
+
A composição compacta preserva a mesma responsabilidade: o título permanece no header estável.
|
|
59
|
+
A fixação resulta da divisão de altura, sem position fixed ou sticky no consumidor.
|
|
60
|
+
|
|
61
|
+
O header ocupa toda a largura disponível. O limite de 80rem e o recuo pertencem ao conteúdo
|
|
62
|
+
interno do Body; o viewport ocupa a largura da página para manter sua barra de rolagem na extremidade.
|
|
63
|
+
|
|
64
|
+
Quando presente, o header da Page usa borda inferior em toda a largura, como o Drawer.
|
|
65
|
+
|
|
66
|
+
## Divisão visual da Page
|
|
67
|
+
|
|
68
|
+
Page expõe `surfaceLayout="sectioned" | "continuous"`, com padrão `sectioned`.
|
|
69
|
+
O nome separa a anatomia visual de `layout="flow" | "fill"`, preservado para altura.
|
|
70
|
+
As regiões usam a tabela compartilhada de anatomia e seus divisores por adjacência.
|
|
71
|
+
A largura total do header e do viewport, e o limite interno do conteúdo, valem nos dois modos.
|
|
72
|
+
|
|
73
|
+
O header da Page acompanha o recuo do Drawer: 1rem no modo sectioned; no modo
|
|
74
|
+
continuous, 1rem com recuo inferior de 0,75rem, ou 1rem quando for a única região.
|
|
75
|
+
|
|
76
|
+
Após validação visual da barra com breadcrumb, o header da Page passa a usar 1rem nas
|
|
77
|
+
laterais e 0,75rem na vertical nos dois modos. Esse ajuste substitui a equivalência de
|
|
78
|
+
padding com o Drawer descrita acima; os divisores continuam compartilhados.
|
|
79
|
+
|
|
80
|
+
A avaliação seguinte restaurou o padding do Drawer no header da Page: 1rem no modo
|
|
81
|
+
sectioned e o ritmo correspondente no continuous. O experimento de redução vertical foi descartado.
|
|
82
|
+
|
|
83
|
+
## Altura estável da linha do header
|
|
84
|
+
|
|
85
|
+
A linha interna reserva 2rem em Page, Dialog, Drawer e Surface, mesmo sem ações.
|
|
86
|
+
No header da Page, botões textuais usam `sm` (2rem), e os de ícone, `icon-sm` (2rem).
|
|
87
|
+
Os consumidores declaram o tamanho; PageBack e comandos de Presentation em página já usam
|
|
88
|
+
essa escala. Coleções e headers de modais usam a mesma escala; rodapés conservam default. É uma altura mínima, permitindo crescimento do
|
|
89
|
+
conteúdo. Padding e intro permanecem independentes; headers ausentes não reservam espaço.
|
|
90
|
+
|
|
91
|
+
Presentation em página delega padding e rolagem ao PageBody, tanto dentro quanto fora
|
|
92
|
+
de PageShell. Não intercala um viewport próprio; PresentationBody conserva somente a
|
|
93
|
+
propagação de altura quando houver conteúdo fill. Headers compactos distribuem título
|
|
94
|
+
e ações na linha interna, inclusive sem navegação.
|
|
95
|
+
|
|
96
|
+
## Revisão: geometria comum da família
|
|
97
|
+
|
|
98
|
+
A escala compacta exclusiva de Page mantinha headers de Drawer maiores com a mesma
|
|
99
|
+
composição. A família inteira passa a compartilhar padding de 1rem e linha mínima
|
|
100
|
+
de 2rem: Page, Dialog, Drawer e Surface delimitada, também quando hospedada em Pane.
|
|
101
|
+
Vale nos layouts continuous e sectioned; o divisor continua sendo uma decisão do layout.
|
|
102
|
+
Isso substitui o ritmo variável de padding inferior dos headers descrito anteriormente.
|
|
103
|
+
|
|
104
|
+
Botões de header usam `sm` para texto e `icon-sm` para ícones, incluindo comandos de
|
|
105
|
+
Presentation e criação de coleções. Rodapés preservam a escala anterior. A seção
|
|
106
|
+
Surface sem delimitação não ganha padding externo, e legendas recortadas continuam
|
|
107
|
+
com sua geometria própria. O mínimo de linha e a escala das ações permanecem comuns.
|
|
108
|
+
|
|
109
|
+
As medidas têm uma fonte compartilhada, comparada em testes entre as quatro famílias.
|
|
110
|
+
Os tamanhos são declarados pelos consumidores: não há CSS de redimensionamento nem
|
|
111
|
+
contexto herdável que altere botões de portais, corpos ou rodapés.
|
|
112
|
+
|
|
113
|
+
## Correção da escala compacta
|
|
114
|
+
|
|
115
|
+
`icon-sm` passa de 1,75rem para 2rem, correspondendo à altura de `sm`. O desenho
|
|
116
|
+
do ícone continua em 0,875rem. O mínimo de 2rem da linha protege headers sem ações;
|
|
117
|
+
não existe mais diferença de altura entre os controles compactos textuais e quadrados.
|
|
118
|
+
Paginação usa os mesmos tokens, com largura flexível para números longos.
|
|
119
|
+
|
|
120
|
+
## Ritmo vertical compartilhado — 2026-10-13
|
|
121
|
+
|
|
122
|
+
A validação em Conhecimento encontrou intervalos de 0,5rem, 0,75rem e 1,25rem para a mesma
|
|
123
|
+
relação entre cabeçalho e conteúdo. O padrão passa a ser 1rem entre regiões contínuas e
|
|
124
|
+
entre blocos diretos de um body, inclusive TabsPanel. O componente hospedeiro fornece o
|
|
125
|
+
ritmo; DetailGroup e Separator não recebem margem externa própria.
|
|
126
|
+
|
|
127
|
+
Surface sem delimitação e Tabs sem moldura não adicionam padding inferior. Surface delimitada
|
|
128
|
+
por framed ou elevation, Dialog e Drawer reservam 1rem até a borda. No layout sectioned,
|
|
129
|
+
cada região reserva 1rem entre seu conteúdo e a divisória. No continuous, a região anterior
|
|
130
|
+
fornece o intervalo, evitando somar dois paddings. A ausência de header preserva o recuo
|
|
131
|
+
superior de uma superfície delimitada. Fieldset com legend conserva 1rem após a legenda.
|
|
132
|
+
|
|
133
|
+
Page preserva largura de leitura e recuos laterais. Seu body não reserva padding inferior
|
|
134
|
+
quando encerra a página; quando há footer sectioned, reserva 1rem até a divisória. A barra
|
|
135
|
+
continua fora da rolagem. O modo flush continua removendo recuos, mas conserva o ritmo
|
|
136
|
+
entre blocos: tabelas e listas que ocupam a superfície controlam seu próprio interior.
|
|
137
|
+
|
|
138
|
+
Tabs mantém o espaçamento interno dos botões. O intervalo até o painel é 1rem; com framed,
|
|
139
|
+
o painel recebe recuos laterais e inferior de 1rem. Abas verticais mantêm o intervalo entre
|
|
140
|
+
as colunas e a moldura recebe o recuo externo sem duplicá-lo no painel.
|
|
141
|
+
|
|
142
|
+
Manter medidas locais dependeria de disciplina em cada tela; impor margem a cada bloco
|
|
143
|
+
somaria intervalos no aninhamento. As duas alternativas foram descartadas. A migração remove
|
|
144
|
+
overrides de ritmo e padding nos slots estruturais, preservando layouts internos deliberados.
|
|
145
|
+
Testes em navegador medem intervalos, fechamento inferior, aninhamento, presença/ausência de
|
|
146
|
+
header e footer, flush e troca de abas. O ritmo altera o default visual; a composição de Page exige a migração documentada na versão major.
|
|
147
|
+
|
|
148
|
+
Uma composição pode manter `gap-0` quando Tabs hospeda um DrawerBody que já fornece o
|
|
149
|
+
intervalo superior e controla a rolagem, como o painel de nós de Fluxos. Isso evita dois
|
|
150
|
+
intervalos para a mesma relação; não altera a medida de 1rem percebida pelo conteúdo.
|
|
151
|
+
|
|
152
|
+
## Escala do título na intro — 2026-10-13
|
|
153
|
+
|
|
154
|
+
PageTitle, DrawerTitle e DialogTitle usam a mesma escala na intro: 1,875rem, com peso
|
|
155
|
+
semibold e tracking compacto. Drawer e Dialog mantêm a escala menor quando o título
|
|
156
|
+
está no header compacto. A hierarquia HTML continua independente do tamanho visual.
|
|
157
|
+
Metadados de data e autoria permanecem na descrição; um badge de tipo pode acompanhar
|
|
158
|
+
o título em uma composição inline com quebra, sem criar um novo slot estrutural.
|
|
159
|
+
|
|
160
|
+
`contentClassName` em Page e PageBody configura largura e alinhamento internos; `className`
|
|
161
|
+
controla a moldura e o viewport, respectivamente. Presentation fora do shell preserva largura total.
|
package/docs/adr/README.md
CHANGED
|
@@ -12,3 +12,5 @@ Uma ADR preserva seu contexto histórico mesmo quando é substituída. O comport
|
|
|
12
12
|
estar documentado na superfície correspondente, sem exigir a reconstrução dessa sequência.
|
|
13
13
|
|
|
14
14
|
- [ADR 0059 — Subheadings orientam sem carregar regras de negócio](0059-subheadings-orient-without-carrying-business-rules.md).
|
|
15
|
+
|
|
16
|
+
- [ADR 0060 — Composição de superfícies com cabeçalho e intro](0060-surface-composition-separates-chrome-and-intro.md).
|
package/package.json
CHANGED
|
@@ -43,17 +43,17 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
43
43
|
7. Tratar página, filtro, seleção e modal importante como estado navegável por URL quando
|
|
44
44
|
o produto precisa de deep link, back/forward ou refresh. Em `Presentation`, declarar `route`
|
|
45
45
|
no artefato e deixar o adaptador apenas conectar a spec ao router da aplicação.
|
|
46
|
-
8. Compor
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
8. Compor Page, Dialog e Drawer pela mesma gramática: no formato compacto, título e orientação
|
|
47
|
+
ficam no Header; com Intro, navegação e ações da superfície ficam no Header e a Intro abre o
|
|
48
|
+
Body com título, orientação e ações locais em IntroActions. Declarar a escolha pelos slots,
|
|
49
|
+
sem prop redundante. Usar PageIntro na Page explícita; a forma curta de Page já coloca title e
|
|
50
|
+
description na intro e actions no header. Não misturar shorthand com regiões explícitas.
|
|
51
|
+
Manter exatamente um heading principal, inclusive quando Surface level={1} nomeia uma coleção
|
|
52
|
+
ou PageState substitui a página. PageShell fornece contexto sem criar barra paralela; header
|
|
53
|
+
sem controles não reserva espaço. Preservar o teto de 80rem e separar composição de ocupação
|
|
54
|
+
de altura (flow/fill). Surface mantém o cabeçalho do conteúdo; Pane distribui espaço e recebe
|
|
55
|
+
uma superfície quando precisar de anatomia visual. Não criar Intro nem controles de navegação
|
|
56
|
+
artificiais para Surface ou Pane.
|
|
57
57
|
Em `Surface framed`, usar `layout="sectioned"` e `SurfaceBody flush` quando o filho direto já
|
|
58
58
|
controlar o recuo do body, como `DetailGroup` com divisores, lista ou tabela; não reconstruir
|
|
59
59
|
divisor, padding ou recorte por classes no consumidor. Em grupos de formulário,
|
|
@@ -74,3 +74,13 @@ complementar do termo. Esperado: `description` orienta a finalidade, `hint` mant
|
|
|
74
74
|
de deixar vazio e `help` oferece somente o detalhe opcional. `intro` continua no corpo quando a
|
|
75
75
|
regra afeta vários campos. Reprovar regra obrigatória somente em subheading ou tooltip, repetição
|
|
76
76
|
da label, duplicação entre hint/help e perda de `aria-describedby` quando aparece um erro.
|
|
77
|
+
|
|
78
|
+
## Composição de superfícies
|
|
79
|
+
|
|
80
|
+
- Gatilho positivo: “Monte o detalhe de um recurso com menu da página separado do título.”
|
|
81
|
+
- Gatilho negativo: “Explique por que a ADR antiga juntava título e ações”, sem pedido de UI.
|
|
82
|
+
- Execução: montar o mesmo detalhe em Page, Dialog e Drawer com header de navegação/ações e intro
|
|
83
|
+
abrindo o body; montar também a versão compacta sem intro. Provar título único, ações locais em
|
|
84
|
+
IntroActions, nenhum header vazio na Page e transição de loading para conteúdo sem título duplicado.
|
|
85
|
+
- Reprovar intro depois de texto, título simultâneo em header e intro, ações da superfície no título
|
|
86
|
+
ou adaptação de Pane para que ele volte a possuir anatomia visual.
|