@softize/opus 27.0.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/bin/lib/check.mjs +139 -30
  3. package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
  4. package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +14 -8
  5. package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
  6. package/docs/adr/0057-doc-browser-permite-busca-no-chrome-do-consumidor.md +39 -5
  7. package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +161 -0
  8. package/docs/adr/README.md +2 -0
  9. package/package.json +1 -1
  10. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  11. package/registry/skills/build-opus-ui/references/evaluations.md +10 -0
  12. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -15
  13. package/src/ui/components/patterns/list.tsx +62 -53
  14. package/src/ui/components/patterns/page.tsx +146 -38
  15. package/src/ui/components/patterns/presentation.tsx +44 -59
  16. package/src/ui/components/patterns/structural-header.tsx +42 -6
  17. package/src/ui/components/patterns/surface-anatomy.ts +52 -22
  18. package/src/ui/components/patterns/surface.tsx +2 -3
  19. package/src/ui/components/primitives/control.ts +2 -2
  20. package/src/ui/components/primitives/dialog.tsx +7 -2
  21. package/src/ui/components/primitives/drawer.tsx +7 -2
  22. package/src/ui/components/primitives/icon-picker.tsx +271 -34
  23. package/src/ui/components/primitives/pagination.tsx +5 -4
  24. package/src/ui/components/primitives/tabs.tsx +15 -2
  25. package/src/ui/components/primitives/toggle.tsx +1 -1
  26. package/src/ui/docs/DocBrowser.tsx +122 -12
  27. package/src/ui/docs/content/action-list.md +11 -2
  28. package/src/ui/docs/content/button.md +7 -7
  29. package/src/ui/docs/content/compositions.md +35 -23
  30. package/src/ui/docs/content/dialog.md +18 -6
  31. package/src/ui/docs/content/documentation-portal.md +40 -1
  32. package/src/ui/docs/content/drawer.md +17 -5
  33. package/src/ui/docs/content/icon-picker.md +37 -19
  34. package/src/ui/docs/content/input.md +1 -1
  35. package/src/ui/docs/content/page.md +169 -69
  36. package/src/ui/docs/content/pagination.md +7 -8
  37. package/src/ui/docs/content/presentation.md +4 -3
  38. package/src/ui/docs/content/surface.md +8 -5
  39. package/src/ui/docs/content/tabs.md +12 -4
  40. package/src/ui/docs/types.ts +2 -0
  41. package/src/ui/meta.ts +1 -1
  42. package/src/ui/react.tsx +9 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,79 @@ 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
+
65
+ ## 27.1.0 — 2026-10-13
66
+
67
+ Portais documentais podem oferecer navegação recolhível em telas estreitas com
68
+ `DocBrowser.collapsibleNavigation`. O menu ocupa a área disponível, preserva os ramos
69
+ abertos e mostra os resultados da busca; selecionar um destino interno ou pressionar
70
+ Escape retorna à leitura. A composição desktop e consumidores sem a opção não mudam.
71
+
72
+ **Ao adotar:** remova as regras CSS móveis do host que limitavam `Split` e `Pane` a
73
+ uma faixa curta. Preserve a instância de `DocBrowser` entre áreas, sem uma `key`
74
+ que remonte o navegador a cada troca; isso mantém a busca, os ramos abertos e o foco.
75
+ Mapas distintos usam `basePath` distintos para preservar a abertura dos ramos de cada
76
+ área. Uma consulta já preenchida na montagem não abre o menu; editar a busca revela
77
+ os resultados.
78
+
79
+ `DocSection.readingSequence: false` mantém uma seção na navegação e na busca, mas a retira
80
+ dos links de leitura anterior/próxima. Isso permite consultar registros históricos sem
81
+ apresentá-los como continuidade de guias vigentes.
82
+
10
83
  ## 27.0.0 — 2026-10-13
11
84
 
12
85
  O aplicativo passa a saber, na recusa de um anexo, o que entrou na mesma ação, e o glob do runtime
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(["__proto__", "constructor", "prototype"]);
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(["PageBack", "PageNavigation", "PageActions", "PageDescription"]),
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) || ts.isNoSubstitutionTemplateLiteral(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(`\`batchSize\` aceita no máximo ${MAX_PURGE_BATCH_SIZE} linhas por lote.`);
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 ? undefined : entitiesByIdent.get(purge.entityRef);
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) return obj.properties.some((pr) => pr.name?.getText(sf) === "ai") ? "unknown" : null;
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) return "disabled";
993
- if (enabled.initializer.kind === ts.SyntaxKind.TrueKeyword) return "enabled";
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) ? found.initializer.text : null,
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)) maxContextChars = Number(node.text.replace(/_/g, ""));
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.filter(ts.isStringLiteralLike).map((element) => element.text)
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("O assistente precisa declarar `resource`, o tipo de recurso de que a action é dona.");
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(`\`resource: "${resource.text}"\` precisa estar em camelCase, como \`customer\` ou \`aftersaleOpportunity\`.`);
1149
+ out.push(
1150
+ `\`resource: "${resource.text}"\` precisa estar em camelCase, como \`customer\` ou \`aftersaleOpportunity\`.`,
1151
+ );
1116
1152
  if (!skill.present)
1117
- out.push("O assistente precisa declarar `skill`, a chave do procedimento que a IA segue.");
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(`\`skill: "${skill.text}"\` precisa estar em minúsculas com hífen, como \`review-customer\`.`);
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.split(".").some((segment) => ASSISTANT_RESERVED_SEGMENTS.has(segment)),
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) || limit < ASSISTANT_CONTEXT_MIN || limit > ASSISTANT_CONTEXT_MAX)
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((key) => !assistant.keys.includes(key));
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(({ item }) => item.name ?? item.ident ?? "(sem name)");
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 (component !== null && UI_STRUCTURAL_BODIES.has(component)) {
2099
- const overridden = jsxStringAttributeValues(
2100
- jsxOpening(node),
2101
- "className",
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((m) =>
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
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  - **Status:** aceita.
4
4
  - **Data:** 2026-09-15.
5
- - **Emenda:** 2026-10-07; portal documental delimitado pela ADR 0057.
5
+ - **Emendas:** 2026-10-07 e 2026-10-13; portal documental delimitado pela ADR 0057.
6
6
 
7
7
  ## Contexto
8
8
 
@@ -32,8 +32,9 @@ nesse caso, o cancelamento usa `outline`.
32
32
  ### Exceção: portal de documentação
33
33
 
34
34
  O portal documental tem uma jornada de leitura distinta da UI operacional das aplicações. Conforme
35
- a ADR 0057, `DocHeader`, a leitura editorial de `DocBrowser` e `DocCatalog`/`DocRecordDetail`
36
- podem adaptar cabeçalho, espaçamento e sumário à largura da tela. A exceção foi aprovada para
35
+ a ADR 0057, `DocHeader`, a leitura editorial e a navegação móvel opcional de `DocBrowser`,
36
+ além de `DocCatalog`/`DocRecordDetail`, podem adaptar cabeçalho, navegação, espaçamento e
37
+ sumário à largura da tela. A exceção foi aprovada para
37
38
  preservar a composição validada nos portais do Opus e da Grand Brasil.
38
39
 
39
40
  A exceção não torna responsivos `Split`, `Sidebar`, formulários, tabelas ou outros componentes da
@@ -41,9 +42,13 @@ UI das aplicações. O host documental continua responsável pela composição d
41
42
  estreita. Exemplos vivos usam o mesmo contrato desktop dos componentes demonstrados; o portal não
42
43
  promete que todos os exemplos sejam adequados a telas menores que `64rem`.
43
44
 
44
- O gate permite variantes de viewport somente nos três arquivos documentais identificados na ADR
45
- 0057. Arquivos novos, mesmo dentro de `docs/`, continuam sujeitos à regra até uma decisão explícita.
46
- Container queries não entram na exceção.
45
+ O gate permite variantes de viewport somente nas funções editoriais identificadas pela ADR 0057.
46
+ A consulta direta de largura via `matchMedia` tem uma exceção separada: somente os helpers
47
+ `subscribeCompactNavigation` e `compactNavigationSnapshot` de `DocBrowser.tsx` podem consultar
48
+ a constante `compactNavigationQuery`, fixada em `(max-width: 48rem)`. Isso não libera variantes
49
+ na função `DocBrowser`. Chamadas diretas com consultas dinâmicas não identificáveis são recusadas;
50
+ preferências explícitas continuam permitidas. Arquivos novos, mesmo dentro de `docs/`, seguem
51
+ a regra. Container queries não entram na exceção.
47
52
 
48
53
  ## Consequências
49
54
 
@@ -67,7 +72,8 @@ Container queries não entram na exceção.
67
72
  ## Verificação
68
73
 
69
74
  - Um teste de inventário reprova variantes de largura e container queries na UI viva.
70
- - O mesmo teste delimita a exceção documental por arquivo e comprova que ela não alcança componentes
71
- da aplicação, outros arquivos documentais ou container queries.
75
+ - O mesmo teste delimita a exceção documental por função e verifica chamadas diretas de `matchMedia`,
76
+ inclusive constantes e consultas dinâmicas. Mutações fora dos helpers, do arquivo ou do breakpoint
77
+ documentados reprovam; a exceção não alcança componentes da aplicação nem container queries.
72
78
  - Testes estruturais fixam layouts, larguras de modal e distribuição dos footers.
73
79
  - Documentação pública declara a largura mínima, o default por conteúdo e a exceção `equal`.
@@ -1,6 +1,8 @@
1
1
  # ADR 0029 — PageHeader é a única introdução estrutural da página
2
2
 
3
- - **Status:** aceita.
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`.
@@ -1,8 +1,37 @@
1
1
  # ADR 0057 — Portal documental compartilha navegação, busca e leitura
2
2
 
3
- - **Status:** aceita para implementação local; publicação pendente.
3
+ - **Status:** aceita; composição original em 25.7.0, subgrupos e abertura em nova aba em 26.2.0,
4
+ navegação recolhível e sequência de leitura em 27.1.0.
4
5
  - **Data:** 2026-10-07.
5
6
 
7
+ > **Emenda — 2026-10-13, versão 27.1.0:** a composição anterior reúne as entregas
8
+ > das versões 25.7.0 e 26.2.0. Esta extensão separa presença no mapa de participação na leitura:
9
+ > `DocSection.readingSequence: false` retira a seção dos links anterior/próxima, mas
10
+ > preserva busca, contexto e URLs. A omissão mantém a sequência existente. Excluir o
11
+ > arquivo histórico da árvore impediria sua descoberta; duplicar o mapa para calcular
12
+ > continuidade criaria duas ordens concorrentes.
13
+ >
14
+ > `DocBrowser.collapsibleNavigation` é opcional e começa desligado. Quando ligado, em
15
+ > telas de até `48rem`, oferece um botão de navegação e começa recolhido. Ao abrir,
16
+ > a árvore ocupa a área disponível no lugar da leitura, sem a faixa fixa de `14rem`.
17
+ > A seleção de um destino interno e a tecla Escape recolhem o menu; editar a busca revela
18
+ > seus resultados. A consulta preenchida na montagem não abre o menu sobre a leitura.
19
+ > A árvore permanece montada para preservar ramos abertos. Acima
20
+ > desse limite, a lateral continua visível. `Split` e `Sidebar` não mudam de contrato.
21
+ > O host preserva a instância também entre Documentação e UI: remontá-la descartaria
22
+ > o botão que recebeu o foco e o estado dos ramos. Mapas distintos usam `basePath`
23
+ > distintos; as chaves dos ramos incluem essa identidade antes da posição no mapa.
24
+ > Assim, abrir um capítulo em Documentação não abre outro na mesma posição em UI,
25
+ > e voltar restaura a abertura manual. A ordem interna de cada mapa permanece estável.
26
+ > Escape retorna
27
+ > o foco ao botão Navegação, inclusive quando acionado pela busca externa; a consulta
28
+ > permanece para permitir retomar os mesmos resultados.
29
+ > Esta composição substitui os ajustes CSS duplicados nos dois portais, estendendo
30
+ > a exceção documental à montagem do navegador, não aos exemplos ou demais telas.
31
+ > A alternativa de manter uma lateral curta preservaria a rolagem que esconde destinos
32
+ > globais. Testes de teclado, redimensionamento, busca, rotas e sequência de leitura,
33
+ > além de inspeção visual nos dois consumidores, verificam a extensão antes de publicar.
34
+
6
35
  ## Contexto
7
36
 
8
37
  Portais de documentação precisam apresentar guias, decisões e entregas sem trocar o mapa de
@@ -114,10 +143,14 @@ A composição documental é uma exceção explícita à ADR 0017: `DocHeader.ts
114
143
  (na leitura editorial) e `DocCatalog.tsx` (incluindo `DocRecordDetail`) podem usar variantes de
115
144
  viewport para organizar cabeçalho, sumário e espaçamento. A exceção fica restrita a esses arquivos;
116
145
  não libera toda a pasta `docs/` nem container queries. O gate verifica as funções de composição
117
- permitidas, sem liberar outras funções ou a montagem de `Split` no mesmo arquivo.
146
+ permitidas. A emenda de 13/10 admite a consulta de largura via `matchMedia` apenas nos helpers
147
+ `subscribeCompactNavigation` e `compactNavigationSnapshot`, em `DocBrowser.tsx`, usando a
148
+ constante `compactNavigationQuery` fixada em `(max-width: 48rem)`. Variantes de viewport
149
+ continuam proibidas na função `DocBrowser`; consultas diretas dinâmicas não identificáveis
150
+ também reprovam. Preferências explícitas de acessibilidade e tema permanecem permitidas.
118
151
 
119
152
  Essa adaptação foi aprovada para o portal de leitura, não para a UI das aplicações. O host organiza
120
- sua moldura em telas estreitas; `Split`, `Sidebar` e os exemplos vivos não ganham um contrato
153
+ sua moldura; o navegador pode oferecer sua navegação móvel opcional. `Split`, `Sidebar` e os exemplos vivos não ganham um contrato
121
154
  responsivo por estarem dentro da documentação. Manter o portal inteiramente desktop descartaria
122
155
  a composição já validada; ampliar a exceção para toda a UI alteraria uma decisão fora deste escopo.
123
156
 
@@ -159,8 +192,9 @@ e separa a consulta documental da consulta local.
159
192
  - Os dois portais compartilham cabeçalho e catálogos sem compartilhar sua taxonomia específica.
160
193
  - Guias, índices e fontes acessíveis por URL direta precisam ser filtrados antes de chegar ao
161
194
  navegador quando houver restrição de acesso. Esta composição não é uma fronteira de segurança.
162
- - A consolidação é um candidato local. A publicação e a adoção pela Grand Brasil são etapas
163
- distintas, posteriores à revisão independente.
195
+ - A composição original integra a versão 25.7.0; subgrupos e abertura em nova aba, a 26.2.0;
196
+ a navegação recolhível e a sequência de leitura, a 27.1.0. A adoção por cada portal é uma
197
+ etapa própria.
164
198
 
165
199
  ## Verificação
166
200