@softize/opus 27.1.0 → 29.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 +113 -0
- package/bin/cli.mjs +68 -3
- package/bin/lib/check.mjs +139 -30
- package/bin/lib/copy.mjs +505 -67
- package/bin/lib/materialize.mjs +99 -1
- package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
- package/docs/adr/0028-schedules-run-actions-as-the-system-by-action-opt-in.md +1 -1
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
- package/docs/adr/0037-design-data-lives-outside-the-contract.md +1 -1
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +161 -0
- package/docs/adr/0061-claude-md-imports-agents-md.md +41 -0
- package/docs/adr/README.md +4 -0
- package/docs/code-style.md +47 -8
- package/docs/protocol.md +1 -1
- package/docs/releasing.md +7 -0
- package/package.json +1 -1
- package/registry/instructions/opus.md +3 -1
- package/registry/skills/build-opus-ui/SKILL.md +16 -14
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -15
- package/registry/skills/maintain-opus-docs/SKILL.md +2 -1
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +9 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +8 -0
- package/registry/templates/app/pnpm-workspace.yaml +1 -1
- package/src/core/system-actor.ts +1 -1
- package/src/ui/components/patterns/list.tsx +63 -54
- 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 +12 -3
- package/src/ui/docs/content/actions.md +1 -1
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/button.md +7 -7
- package/src/ui/docs/content/cli.md +6 -0
- package/src/ui/docs/content/communication.md +10 -8
- 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/events.md +13 -13
- package/src/ui/docs/content/icon-picker.md +37 -19
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/installation.md +4 -0
- package/src/ui/docs/content/log.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/queue.md +2 -2
- package/src/ui/docs/content/router.md +1 -1
- package/src/ui/docs/content/scheduler.md +15 -15
- package/src/ui/docs/content/surface.md +8 -5
- package/src/ui/docs/content/tabs.md +12 -4
- package/src/ui/docs/content/testing.md +4 -4
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +9 -1
- package/src/vite/design.ts +1 -1
package/bin/lib/materialize.mjs
CHANGED
|
@@ -27,6 +27,7 @@ const REGISTRY = join(PACKAGE_ROOT, 'registry')
|
|
|
27
27
|
const BLOCK_SOURCE = 'registry/instructions/opus.md'
|
|
28
28
|
const BLOCK_END = '<!-- softize-managed:end @softize/opus -->'
|
|
29
29
|
const MANAGED_ROOTS = ['.agents/skills', '.claude/skills', '.claude/hooks', '.github/workflows', '.githooks/pre-push.d']
|
|
30
|
+
const CLAUDE_IMPORT = '@AGENTS.md'
|
|
30
31
|
|
|
31
32
|
const normalizedLineEndings = (content) => content.replace(/\r\n?/gu, '\n')
|
|
32
33
|
const hash = (content) => createHash('sha256').update(normalizedLineEndings(content)).digest('hex')
|
|
@@ -533,6 +534,103 @@ function reconcileInstructions(root, file, mode, errors, changes) {
|
|
|
533
534
|
}
|
|
534
535
|
}
|
|
535
536
|
|
|
537
|
+
// Blocos gerenciados de qualquer pacote. Um marcador de início ilegível não identifica dono e
|
|
538
|
+
// conta como texto do projeto. Mesmo critério da Base (@softize/base, ADR 0006).
|
|
539
|
+
function managedBlocks(content) {
|
|
540
|
+
const blocks = []
|
|
541
|
+
let cursor = 0
|
|
542
|
+
for (const match of content.matchAll(/<!-- softize-managed:start (\{[^\n]+\}) -->/g)) {
|
|
543
|
+
let meta
|
|
544
|
+
try {
|
|
545
|
+
meta = JSON.parse(match[1])
|
|
546
|
+
} catch {
|
|
547
|
+
continue
|
|
548
|
+
}
|
|
549
|
+
if (typeof meta?.package !== 'string') continue
|
|
550
|
+
const openEnd = match.index + match[0].length
|
|
551
|
+
const endMarker = `<!-- softize-managed:end ${meta.package} -->`
|
|
552
|
+
const endAt = content.indexOf(endMarker, openEnd)
|
|
553
|
+
if (match.index < cursor || endAt === -1 || content.slice(openEnd, endAt).includes('softize-managed:start')) return null
|
|
554
|
+
const end = endAt + endMarker.length
|
|
555
|
+
blocks.push({ start: match.index, end, package: meta.package, text: content.slice(match.index, end) })
|
|
556
|
+
cursor = end
|
|
557
|
+
}
|
|
558
|
+
return blocks
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Texto do projeto fora dos blocos gerenciados e da linha de import, normalizado para comparar o
|
|
562
|
+
// CLAUDE.md legado com o AGENTS.md sem depender de fim de linha ou de linhas em branco.
|
|
563
|
+
function freeInstructions(content, blocks) {
|
|
564
|
+
let rest = ''
|
|
565
|
+
let cursor = 0
|
|
566
|
+
for (const block of blocks) {
|
|
567
|
+
rest += content.slice(cursor, block.start)
|
|
568
|
+
cursor = block.end
|
|
569
|
+
}
|
|
570
|
+
rest += content.slice(cursor)
|
|
571
|
+
return normalizedLineEndings(rest)
|
|
572
|
+
.split('\n')
|
|
573
|
+
.map((line) => line.trimEnd())
|
|
574
|
+
.filter((line) => line !== CLAUDE_IMPORT)
|
|
575
|
+
.join('\n')
|
|
576
|
+
.replace(/\n{3,}/gu, '\n\n')
|
|
577
|
+
.trim()
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// O CLAUDE.md só importa o AGENTS.md, que concentra as instruções para qualquer agente. O Opus
|
|
581
|
+
// remove dele apenas o próprio bloco; blocos de outros pacotes continuam depois do import até que
|
|
582
|
+
// o pacote dono os remova, para que versões antigas e novas convivam sem reescrever o arquivo uma
|
|
583
|
+
// da outra. Texto do projeto que só existe no CLAUDE.md é preservado e reprovado.
|
|
584
|
+
function reconcileClaudeImport(root, mode, errors, changes) {
|
|
585
|
+
const destination = 'CLAUDE.md'
|
|
586
|
+
const pending =
|
|
587
|
+
`${destination}: deve conter a linha ${CLAUDE_IMPORT}, que importa o AGENTS.md, seguida apenas dos ` +
|
|
588
|
+
'blocos de outros pacotes, quando houver; rode opus setup.'
|
|
589
|
+
const currentFile = readMaterializedFile(root, destination, errors)
|
|
590
|
+
if (currentFile === null) return
|
|
591
|
+
const current = currentFile.exists ? currentFile.content : ''
|
|
592
|
+
const own = findBlock(current)
|
|
593
|
+
const blocks = managedBlocks(current)
|
|
594
|
+
if (own?.malformed || blocks === null) {
|
|
595
|
+
errors.push(`${destination}: bloco gerenciado malformado.`)
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
if (own !== null && (own.owner?.package !== PACKAGE_NAME || hash(own.body) !== own.owner.sha256)) {
|
|
599
|
+
errors.push(`${destination}: bloco Opus foi editado ou perdeu sua identificação de origem; preservado.`)
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const free = freeInstructions(current, blocks)
|
|
604
|
+
if (free !== '') {
|
|
605
|
+
const agentsFile = readMaterializedFile(root, 'AGENTS.md', errors)
|
|
606
|
+
if (agentsFile === null) return
|
|
607
|
+
const agents = agentsFile.exists ? agentsFile.content : ''
|
|
608
|
+
if (free !== freeInstructions(agents, managedBlocks(agents) ?? [])) {
|
|
609
|
+
errors.push(
|
|
610
|
+
`${destination}: tem instruções que não estão no AGENTS.md; preservado. O CLAUDE.md passa a conter só ` +
|
|
611
|
+
`${CLAUDE_IMPORT}, então mova essas instruções para o AGENTS.md e rode opus setup.`,
|
|
612
|
+
)
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const foreign = blocks.filter((block) => block.package !== PACKAGE_NAME).map((block) => normalizedLineEndings(block.text))
|
|
618
|
+
const expected = `${[CLAUDE_IMPORT, ...foreign].join('\n\n')}\n`
|
|
619
|
+
if (currentFile.exists && managedTextEquivalent(current, expected)) return
|
|
620
|
+
if (mode === 'check') errors.push(pending)
|
|
621
|
+
else if (
|
|
622
|
+
replaceMaterializedFile(
|
|
623
|
+
root,
|
|
624
|
+
destination,
|
|
625
|
+
currentFile.exists ? withProjectLineEndings(expected, current) : expected,
|
|
626
|
+
currentFile,
|
|
627
|
+
errors,
|
|
628
|
+
)
|
|
629
|
+
) {
|
|
630
|
+
changes.push(`${currentFile.exists ? 'atualizado' : 'criado'} ${destination} como import do AGENTS.md`)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
536
634
|
function isOpusHook(hook) { return typeof hook?.command === 'string' && hook.command.includes('.claude/hooks/opus-check-on-stop.mjs') }
|
|
537
635
|
|
|
538
636
|
function isPlainObject(value) {
|
|
@@ -792,7 +890,7 @@ export function materializeOpus(root, mode = 'setup', options = {}) {
|
|
|
792
890
|
removeObsolete(target, expected, mode, errors, changes)
|
|
793
891
|
detectLegacyArtifacts(target, errors)
|
|
794
892
|
reconcileInstructions(target, 'AGENTS.md', mode, errors, changes)
|
|
795
|
-
|
|
893
|
+
reconcileClaudeImport(target, mode, errors, changes)
|
|
796
894
|
reconcileSettings(target, mode, errors, changes)
|
|
797
895
|
configureGit(target, mode, errors, changes)
|
|
798
896
|
return { ok: errors.length === 0, errors, changes, expected: [...expected.keys()] }
|
|
@@ -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
|
|
|
@@ -45,7 +45,7 @@ HTTP sem usuário; o acesso do sistema é opt-in por action e negado por padrão
|
|
|
45
45
|
`{ kind: 'schedule', scheduleName }` — ou `background` com essa provenance original. O helper
|
|
46
46
|
`isSystemActor(ctx)`, exportado pelo core, responde se o ator é o sistema, para que um
|
|
47
47
|
`authorize` possa aceitá-lo de forma explícita: `(ctx) => isSystemActor(ctx) ||
|
|
48
|
-
ctx.can('
|
|
48
|
+
ctx.can('ledger:close')`. O helper não lê a provenance, que qualquer código servidor pode
|
|
49
49
|
preencher: ele consulta um registro interno dos contextos que o próprio runtime criou para o
|
|
50
50
|
sistema. `requires` continua declarativo: o sistema não detém permissões, e o manifest mostra
|
|
51
51
|
`automatable` ao lado de `permission`.
|
|
@@ -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`.
|
|
@@ -54,7 +54,7 @@ Forças em tensão:
|
|
|
54
54
|
- uma action ligada a um handler real (`bindAction`) mantém esse handler, como hoje.
|
|
55
55
|
3. **Cenários são registrados fora do contrato, pelo nome da action.** O plugin ganha a opção
|
|
56
56
|
`scenarios`, com um ou mais globs. Os módulos casados exportam cenários criados por
|
|
57
|
-
`defineDesignScenario(
|
|
57
|
+
`defineDesignScenario(contract, response)`, e a resposta pode ser um valor ou uma função tipada
|
|
58
58
|
pela entrada e pela saída do contrato. O cenário vence a resposta padrão. Um cenário para uma
|
|
59
59
|
action inexistente é erro visível no preview, não um silêncio.
|
|
60
60
|
4. **O Opus não decide onde os cenários ficam.** A opção não tem default dentro do código da
|
|
@@ -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.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ADR 0061 — O `CLAUDE.md` importa o `AGENTS.md`
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-15.
|
|
5
|
+
- **Acompanha:** ADR 0006 do `@softize/base`, que define o mesmo formato.
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
O `opus setup` escrevia o bloco de instruções do Opus no `AGENTS.md`, lido pelo Codex, e no
|
|
10
|
+
`CLAUDE.md`, lido pelo Claude. A Base fazia o mesmo com o bloco dela. Fora dos blocos, cada projeto
|
|
11
|
+
mantinha à mão as próprias regras nos dois arquivos, e as duas cópias divergiam: na GrandBrasil,
|
|
12
|
+
quatro convenções existiram só no `AGENTS.md` por doze dias, e as sessões do Claude trabalharam sem
|
|
13
|
+
elas.
|
|
14
|
+
|
|
15
|
+
A documentação do Claude Code recomenda, para quem usa as duas ferramentas, um `CLAUDE.md` com a
|
|
16
|
+
linha `@AGENTS.md`. A Base adotou esse formato na ADR 0006. Como o `CLAUDE.md` recebe blocos dos
|
|
17
|
+
dois pacotes, e cada projeto adota as versões deles em momentos diferentes, o Opus precisa seguir
|
|
18
|
+
a mesma regra para que um `setup` não desfaça o do outro.
|
|
19
|
+
|
|
20
|
+
## Decisão
|
|
21
|
+
|
|
22
|
+
O `AGENTS.md` é o único arquivo com instruções. O `opus setup` mantém nele o bloco do Opus e grava
|
|
23
|
+
o `CLAUDE.md` como a linha `@AGENTS.md`, sem marcador.
|
|
24
|
+
|
|
25
|
+
Ao reconciliar o `CLAUDE.md`, o Opus remove só o próprio bloco, quando íntegro. Blocos de outros
|
|
26
|
+
pacotes ficam depois da linha do import, separados por uma linha em branco, até que o pacote dono
|
|
27
|
+
os remova. O texto do projeto fora dos blocos e do import é comparado com o do `AGENTS.md`, com fim
|
|
28
|
+
de linha, espaços no fim das linhas e linhas em branco seguidas normalizados: se for igual, é
|
|
29
|
+
cópia e sai; se for diferente, `setup` e `opus check` preservam o arquivo e pedem para mover as
|
|
30
|
+
instruções para o `AGENTS.md`. Os critérios de bloco, de linha do import e de normalização são os
|
|
31
|
+
da Base, e as mensagens seguem as dela, trocando o comando por `opus setup`.
|
|
32
|
+
|
|
33
|
+
## Consequências
|
|
34
|
+
|
|
35
|
+
- Projeto com instruções só no `CLAUDE.md` precisa movê-las para o `AGENTS.md` antes de o `setup`
|
|
36
|
+
concluir. Por isso a mudança sai numa versão major.
|
|
37
|
+
- Enquanto um projeto usa uma Base anterior à ADR 0006, o `CLAUDE.md` fica com o import e o bloco
|
|
38
|
+
da Base. O conteúdo da Base aparece duas vezes para o Claude, pelo import e pelo bloco, até a
|
|
39
|
+
adoção da Base nova; o arquivo não oscila entre os dois `setup`.
|
|
40
|
+
- Um bloco do Opus editado à mão ou um bloco gerenciado malformado no `CLAUDE.md` são preservados e
|
|
41
|
+
reprovados, como antes.
|
package/docs/adr/README.md
CHANGED
|
@@ -12,3 +12,7 @@ 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).
|
|
17
|
+
|
|
18
|
+
- [ADR 0061 — O `CLAUDE.md` importa o `AGENTS.md`](0061-claude-md-imports-agents-md.md).
|
package/docs/code-style.md
CHANGED
|
@@ -48,8 +48,12 @@ dinâmica, importada sem fallback ou composta de forma opaca reprova a extraçã
|
|
|
48
48
|
lacunas conhecidas visíveis em vez de produzir um inventário verde e incompleto.
|
|
49
49
|
|
|
50
50
|
Transformações visuais não observáveis e exceções ficam explícitas em `base.json`, usando um
|
|
51
|
-
seletor estável e revisável. `source`, `
|
|
52
|
-
|
|
51
|
+
seletor estável e revisável. `source`, `role` e `text` selecionam o texto pelo conteúdo: a
|
|
52
|
+
declaração vale para toda ocorrência desse texto com esse papel no arquivo e continua válida
|
|
53
|
+
quando linhas entram ou saem acima dela. Um seletor que não encontra o texto reprova a extração,
|
|
54
|
+
porque o texto mudou ou saiu do arquivo. O formato anterior, com `line` no lugar de `text` (ou
|
|
55
|
+
junto dele), ainda é aceito e seleciona exatamente uma entrada naquela linha; `opus copy
|
|
56
|
+
--reanchor` converte esses seletores para o formato por conteúdo. Metadata
|
|
53
57
|
pode complementar a análise, mas nunca sobrescrever uma transformação já observada no JSX —
|
|
54
58
|
`transform: "none"` não mascara `className="uppercase"`. O Opus valida o shape `COPYnnn`; a
|
|
55
59
|
Base instalada é dona do catálogo, dos kinds aceitos e do fundamento de cada regra:
|
|
@@ -63,16 +67,16 @@ Base instalada é dona do catálogo, dos kinds aceitos e do fundamento de cada r
|
|
|
63
67
|
"transforms": [
|
|
64
68
|
{
|
|
65
69
|
"source": "src/account.ts",
|
|
66
|
-
"line": 12,
|
|
67
70
|
"role": "button",
|
|
71
|
+
"text": "Confirmar",
|
|
68
72
|
"transform": "uppercase"
|
|
69
73
|
}
|
|
70
74
|
],
|
|
71
75
|
"exemptions": [
|
|
72
76
|
{
|
|
73
77
|
"source": "src/account.ts",
|
|
74
|
-
"line": 18,
|
|
75
78
|
"role": "label",
|
|
79
|
+
"text": "ABC-123",
|
|
76
80
|
"ruleId": "COPY001",
|
|
77
81
|
"kind": "external-requirement",
|
|
78
82
|
"reason": "Código regulatório exibido na grafia oficial.",
|
|
@@ -145,21 +149,56 @@ exigiria adivinhar espaços continuam reprovando.
|
|
|
145
149
|
|
|
146
150
|
Quando o diagnóstico corresponde a dados reais da aplicação ou a uma mensagem interpolada,
|
|
147
151
|
`copy.dynamic` permite classificar somente aquela superfície sem excluir o arquivo. Cada item
|
|
148
|
-
seleciona
|
|
149
|
-
`
|
|
152
|
+
seleciona o diagnóstico por `source`, `field` e `expression` — o trecho de código que o
|
|
153
|
+
extrator não conseguiu resolver; a mensagem do `opus copy` traz os dois valores prontos para
|
|
154
|
+
copiar —,
|
|
155
|
+
registra `reason` e `reference` auditáveis e usa um destes contratos:
|
|
150
156
|
|
|
151
157
|
- `runtime-data` reconhece nomes, contagens, status ou opções fornecidos em runtime. Nenhuma
|
|
152
158
|
entrada editorial é fabricada;
|
|
153
159
|
- `message-template` exige `role` e `text`. O texto-modelo entra no inventário e passa pela
|
|
154
160
|
política da Base, mas não substitui nem altera o conteúdo renderizado.
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
No trecho, espaços e quebras de linha viram um espaço só e a vírgula final antes de `)`, `]` ou
|
|
163
|
+
`}` é ignorada, para a âncora sobreviver à reformatação; trechos com mais de 200 caracteres são
|
|
164
|
+
cortados e terminam em `…`. A declaração vale para toda ocorrência do mesmo trecho no mesmo
|
|
165
|
+
campo do arquivo e não depende da linha em que ele está.
|
|
166
|
+
|
|
167
|
+
As declarações aparecem em `dynamicSurfaces` no inventário, com as linhas atuais de cada
|
|
168
|
+
ocorrência. Se o código mudar e o seletor deixar de encontrar o trecho, `opus copy` falha e lista,
|
|
169
|
+
numa execução, todas as declarações quebradas, cada uma com o arquivo, o campo e os trechos desse
|
|
170
|
+
campo que ainda não têm declaração. Assim, a classificação permanece
|
|
158
171
|
estreita e revisável, sem transformar metadata em uma exclusão silenciosa. O mecanismo aceita
|
|
159
172
|
somente diagnósticos de conteúdo; incerteza estrutural, `className`, `style`, transformação ou
|
|
160
173
|
modo de renderização continua bloqueante. Superfície vazia também não pode ser reclassificada
|
|
161
174
|
como dado de runtime.
|
|
162
175
|
|
|
176
|
+
Declarações escritas antes da versão 29.0.0 usam `line` no lugar de `expression` (ou de `text`,
|
|
177
|
+
nas exceções e transformações) e quebram sempre que alguém edita o arquivo acima delas. Elas
|
|
178
|
+
continuam aceitas, selecionam exatamente um alvo naquela linha e são aplicadas antes das
|
|
179
|
+
declarações por conteúdo, para que uma declaração pontual não seja coberta por uma geral; o
|
|
180
|
+
`opus copy` avisa quantas restam. Para convertê-las, rode `opus copy --reanchor`. O comando
|
|
181
|
+
agrupa as declarações por arquivo e campo (em `copy.dynamic`) ou por arquivo, papel e texto
|
|
182
|
+
(nas exceções e transformações), grava o `base.json` e regenera o inventário:
|
|
183
|
+
|
|
184
|
+
- um grupo só é convertido quando tem exatamente uma declaração para cada alvo. Assim a âncora
|
|
185
|
+
por conteúdo cobre os mesmos alvos que as âncoras por linha cobriam. Se o mesmo texto aparece
|
|
186
|
+
mais vezes do que foi declarado, ou se há um diagnóstico do campo sem declaração, o grupo
|
|
187
|
+
continua por linha e aparece no relatório;
|
|
188
|
+
- dentro do grupo, declarações e alvos são pareados em ordem de linha. Quando o arquivo mudou e
|
|
189
|
+
as âncoras quebraram, o comando lista cada declaração que mudou de linha para revisão no diff;
|
|
190
|
+
- declarações idênticas que passariam a selecionar o mesmo conteúdo viram uma só;
|
|
191
|
+
- duas declarações com motivos diferentes para o mesmo conteúdo continuam por linha, já
|
|
192
|
+
corrigida, porque um seletor por conteúdo cobriria as duas com um único motivo. Para
|
|
193
|
+
convertê-las, unifique o motivo ou mantenha a âncora por linha;
|
|
194
|
+
- os seletores gravados precisam passar na extração do `opus copy`; o que não dá para ancorar com
|
|
195
|
+
segurança fica como está e aparece no relatório.
|
|
196
|
+
|
|
197
|
+
O `--reanchor` não avalia a política de copy da Base. Depois dele, rode `opus copy --check` e
|
|
198
|
+
`base copy check`. Na revisão de uma declaração por conteúdo, vale olhar `dynamicSurfaces[].lines`
|
|
199
|
+
no diff do inventário: uma lista que cresce indica que um trecho genérico, como `title` ou
|
|
200
|
+
`count`, passou a cobrir uma ocorrência nova, que pode ter outra origem.
|
|
201
|
+
|
|
163
202
|
Um `opus copy --check` verde **não significa que toda a copy do produto foi coberta**. Ficam
|
|
164
203
|
fora, deliberadamente:
|
|
165
204
|
|
package/docs/protocol.md
CHANGED
|
@@ -679,7 +679,7 @@ O acesso do sistema é **opt-in por action** e negado por padrão. Sem usuário,
|
|
|
679
679
|
`isSystemActor(ctx)` responde se o ator é o sistema. Ele consulta um registro interno dos contextos que o runtime criou, e não a provenance. Como o sistema não detém permissões, um `authorize` que delega a `ctx.can` o aceita de forma explícita, numa closure — a DSL em string não expressa isso, e `ctx.provenance` não serve como teste de sistema, porque o chamador de `runtime.execute` pode preenchê-la:
|
|
680
680
|
|
|
681
681
|
```ts
|
|
682
|
-
authorize: (ctx) => isSystemActor(ctx) || ctx.can('
|
|
682
|
+
authorize: (ctx) => isSystemActor(ctx) || ctx.can('ledger:close')
|
|
683
683
|
```
|
|
684
684
|
|
|
685
685
|
`requires` continua declarativo; o manifest projeta `automatable` ao lado de `permission`. O audit registra `actor: { id: null, type: 'system' }`. Ver ADR 0028.
|
package/docs/releasing.md
CHANGED
|
@@ -18,6 +18,13 @@ confere essa entrada só DEPOIS do bump, então esquecê-la aborta a release com
|
|
|
18
18
|
alterado — e a tentativa seguinte para na guarda de árvore limpa. Para recuperar, desfaça o bump
|
|
19
19
|
(`git checkout -- package.json`), escreva a entrada, commite e rode de novo.
|
|
20
20
|
|
|
21
|
+
Uma entrada que já está na `origin/main` não muda mais: quem consome o pacote lê o changelog como
|
|
22
|
+
guia de migração, e em 10/09/2026 uma formatação avulsa reescreveu entradas publicadas e mudou o
|
|
23
|
+
sentido de uma delas. Para corrigir uma informação publicada, registre a correção numa entrada
|
|
24
|
+
nova, na próxima versão. A entrada da versão em preparo, enquanto não chega à `main`, pode ser
|
|
25
|
+
ajustada livremente. O pre-push (`.githooks/pre-push.d/01-changelog`) e o `pnpm release` reprovam
|
|
26
|
+
a alteração de uma seção publicada; para conferir antes, rode `pnpm changelog:check` na raiz.
|
|
27
|
+
|
|
21
28
|
Os comandos rodam em `packages/opus`; o `package.json` da raiz não tem `release`.
|
|
22
29
|
|
|
23
30
|
```bash
|
package/package.json
CHANGED
|
@@ -24,7 +24,9 @@ artefatos de agentes fica em `base.json`; cada app Opus mantém seu marcador `op
|
|
|
24
24
|
e operá-los por `opus seed`; não criar comandos de seed paralelos nem reset implícito.
|
|
25
25
|
- Regenerar o inventário com `opus copy` quando mudar copy em contrato ou componente Opus
|
|
26
26
|
mapeado; o Opus classifica os papéis semânticos e a política universal de `@softize/base`
|
|
27
|
-
valida o conteúdo.
|
|
27
|
+
valida o conteúdo. O inventário também guarda o hash de todo arquivo JS/TS do escopo, mesmo sem
|
|
28
|
+
copy, como os scripts em `scripts/*.mjs`; alterar qualquer um deles também exige `opus copy`,
|
|
29
|
+
e o pre-push reprova o inventário desatualizado.
|
|
28
30
|
- Rodar `opus check`, `opus copy --check` e os gates do projeto depois de alterar actions,
|
|
29
31
|
bindings ou versão do SDK.
|
|
30
32
|
- Quando a default branch tiver adotado uma versão diferente da worktree, reconciliar a
|
|
@@ -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,
|
|
@@ -89,10 +89,12 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
89
89
|
Rodar `opus check`, testes focados, typecheck e build da superfície. Se a interface alterou
|
|
90
90
|
copy em contrato ou componente Opus mapeado, regenerar com `opus copy` e executar
|
|
91
91
|
`base copy check`. Quando uma superfície mapeada recebe dados da aplicação, usar
|
|
92
|
-
`copy.dynamic` com
|
|
93
|
-
editorial; `message-template` com `role` e
|
|
92
|
+
`copy.dynamic` com `field` e `expression` copiados do diagnóstico, motivo e referência:
|
|
93
|
+
`runtime-data` para valores sem autoria editorial; `message-template` com `role` e
|
|
94
|
+
texto-modelo para mensagens interpoladas. Não usar `line` em declaração nova. Não excluir
|
|
94
95
|
o arquivo inteiro nem contornar o diagnóstico com `aria-label` paralelo. Revisar cada declaração
|
|
95
|
-
dinâmica como fronteira de cobertura, mesmo com os gates verdes
|
|
96
|
+
dinâmica como fronteira de cobertura, mesmo com os gates verdes, inclusive quando
|
|
97
|
+
`dynamicSurfaces[].lines` cresce no diff do inventário.
|
|
96
98
|
Inspecionar visualmente a rota real e validar navegação por URL quando aplicável.
|
|
97
99
|
|
|
98
100
|
## Limites
|
|
@@ -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.
|