@softize/opus 26.0.0 → 26.2.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 +39 -0
- package/bin/lib/check.mjs +9 -32
- package/bin/lib/copy.mjs +7 -4
- package/docs/adr/0014-structural-headers-do-not-carry-description.md +1 -1
- package/docs/adr/0018-modal-header-only-names-the-surface.md +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +4 -0
- package/docs/adr/0057-doc-browser-permite-busca-no-chrome-do-consumidor.md +27 -0
- package/docs/adr/0059-subheadings-orient-without-carrying-business-rules.md +51 -0
- package/docs/adr/README.md +2 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/references/evaluations.md +8 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +14 -4
- package/src/core/types.ts +3 -2
- package/src/ui/components/patterns/action-list-dialog.tsx +14 -7
- package/src/ui/components/patterns/form-dialog.tsx +18 -7
- package/src/ui/components/patterns/form.tsx +36 -8
- package/src/ui/components/patterns/page.tsx +22 -0
- package/src/ui/components/patterns/sidebar.tsx +17 -3
- package/src/ui/components/patterns/surface-anatomy.ts +1 -1
- package/src/ui/components/primitives/dialog.tsx +28 -1
- package/src/ui/components/primitives/drawer.tsx +30 -2
- package/src/ui/docs/DocBrowser.tsx +128 -81
- package/src/ui/docs/content/action-form-card.md +1 -1
- package/src/ui/docs/content/action-form-dialog.md +8 -0
- package/src/ui/docs/content/action-form.md +18 -5
- package/src/ui/docs/content/action-list-dialog.md +8 -0
- package/src/ui/docs/content/dialog.md +24 -2
- package/src/ui/docs/content/documentation-portal.md +45 -2
- package/src/ui/docs/content/drawer.md +24 -2
- package/src/ui/docs/content/page.md +23 -1
- package/src/ui/docs/content/sidebar.md +23 -0
- package/src/ui/docs/content/surface.md +8 -2
- package/src/ui/docs/types.ts +2 -0
- package/src/ui/lib/modal-layers.ts +27 -3
- package/src/ui/react.tsx +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@ 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
|
+
## 26.2.0 — 2026-10-09
|
|
11
|
+
|
|
12
|
+
Portais podem organizar capítulos em subgrupos e abrir ferramentas relacionadas sem substituir
|
|
13
|
+
a página em leitura.
|
|
14
|
+
|
|
15
|
+
- `DocGroup.groups` declara subgrupos opcionais. Navegação, busca, URLs diretas e sequência
|
|
16
|
+
editorial percorrem todos os níveis, com as páginas próprias antes dos subgrupos. A busca
|
|
17
|
+
preserva os ancestrais dos resultados; no modo inicialmente recolhido, restaura o estado
|
|
18
|
+
manual ao limpar. No modo expandido, os ancestrais da página ativa são reabertos. Ramos
|
|
19
|
+
vazios não aparecem, sem sugerir outro termo quando não há consulta.
|
|
20
|
+
- `DocBrowser.sidebarLinks` preserva a navegação nativa para URLs HTTP(S) absolutas.
|
|
21
|
+
- `SidebarItem` e `DocBrowser.sidebarLinks` aceitam `newTab`: o link abre em outra aba com
|
|
22
|
+
`noopener noreferrer` e informa a abertura no nome acessível. No portal, também exibe
|
|
23
|
+
um ícone de nova aba. A opção não intercepta a navegação nem chama `onClick`.
|
|
24
|
+
|
|
25
|
+
As novas propriedades são opcionais. Consumidores sem subgrupos ou `newTab` mantêm seus
|
|
26
|
+
contratos; não há migração obrigatória nesta versão.
|
|
27
|
+
|
|
28
|
+
## 26.1.0 — 2026-10-09
|
|
29
|
+
|
|
30
|
+
Subheadings descrevem a finalidade da superfície; orientações necessárias ficam visíveis no corpo
|
|
31
|
+
ou junto ao campo (ADR 0059).
|
|
32
|
+
|
|
33
|
+
- `Page` aceita `description`. `PageDescription`, `DialogDescription` e `DrawerDescription`
|
|
34
|
+
compõem o cabeçalho ou a introdução declarada da superfície.
|
|
35
|
+
- `ActionFormDialog` e `ActionListDialog` recebem `description` para o cabeçalho. `intro` continua
|
|
36
|
+
no corpo, para regras e consequências da tarefa; ambos participam da descrição acessível.
|
|
37
|
+
- `FieldSpec.hint` declara orientação visível abaixo do controle. `ActionFormField` associa hint
|
|
38
|
+
e erro por `aria-describedby`; `help` continua sendo a ajuda opcional junto à label.
|
|
39
|
+
- Os textos dos campos resolvem `I18nRef` pelo texto padrão também nos renderizadores próprios.
|
|
40
|
+
O ícone de ajuda participa do Tab e abre por foco, sem tomar o foco inicial de outro controle
|
|
41
|
+
em Dialog e Drawer.
|
|
42
|
+
- O inventário de copy, o validador estrutural, a skill de UI e os exemplos da documentação
|
|
43
|
+
reconhecem esses papéis sem proibir subheadings de maneira indiscriminada.
|
|
44
|
+
|
|
45
|
+
**O que fazer:** rode `opus setup` e `opus copy`; revise a posição das orientações existentes.
|
|
46
|
+
Renderizadores próprios de campos devem apresentar `hint` e associá-lo ao controle, preservando
|
|
47
|
+
os erros. Nenhum texto migra automaticamente de `help` para `hint`.
|
|
48
|
+
|
|
10
49
|
## 26.0.0 — 2026-10-09
|
|
11
50
|
|
|
12
51
|
A conversa com a IA pode levar imagens (ADR 0058). Uma mensagem passa a aceitar texto e imagem
|
package/bin/lib/check.mjs
CHANGED
|
@@ -152,6 +152,7 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
152
152
|
["PageBody", new Set(["Page"])],
|
|
153
153
|
["PageFooter", new Set(["Page"])],
|
|
154
154
|
["PageTitle", new Set(["PageHeader"])],
|
|
155
|
+
["PageDescription", new Set(["PageHeader"])],
|
|
155
156
|
["PageBack", new Set(["PageHeader"])],
|
|
156
157
|
["PageNavigation", new Set(["PageHeader"])],
|
|
157
158
|
["PageActions", new Set(["PageHeader"])],
|
|
@@ -173,6 +174,7 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
173
174
|
["DialogNavigation", new Set(["DialogHeader"])],
|
|
174
175
|
["DialogMedia", new Set(["DialogHeader"])],
|
|
175
176
|
["DialogTitle", new Set(["DialogHeader", "DialogIntro"])],
|
|
177
|
+
["DialogDescription", new Set(["DialogHeader", "DialogIntro"])],
|
|
176
178
|
["DialogActions", new Set(["DialogHeader"])],
|
|
177
179
|
["DrawerHeader", new Set(["DrawerContent"])],
|
|
178
180
|
["DrawerBody", new Set(["DrawerContent"])],
|
|
@@ -182,6 +184,7 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
182
184
|
["DrawerNavigation", new Set(["DrawerHeader"])],
|
|
183
185
|
["DrawerMedia", new Set(["DrawerHeader"])],
|
|
184
186
|
["DrawerTitle", new Set(["DrawerHeader", "DrawerIntro"])],
|
|
187
|
+
["DrawerDescription", new Set(["DrawerHeader", "DrawerIntro"])],
|
|
185
188
|
["DrawerActions", new Set(["DrawerHeader"])],
|
|
186
189
|
["AlertMedia", new Set(["Alert"])],
|
|
187
190
|
["AlertHeader", new Set(["Alert"])],
|
|
@@ -215,13 +218,12 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
215
218
|
body: "PageBody",
|
|
216
219
|
footer: "PageFooter",
|
|
217
220
|
headerRequired: false,
|
|
218
|
-
shorthand: new Set(["title", "actions"]),
|
|
221
|
+
shorthand: new Set(["title", "description", "actions"]),
|
|
219
222
|
// O contador saiu do título da página (ADR 0009). Tirar `count` do shorthand não proíbe
|
|
220
223
|
// nada sozinho — só apaga o marcador que fazia o gate enxergar aquele Page —, então a
|
|
221
224
|
// prop removida é cobrada aqui, nas duas formas de composição.
|
|
222
225
|
removed: new Map([
|
|
223
226
|
["count", "o total pertence ao conteúdo que o explica"],
|
|
224
|
-
["description", "mova contexto relevante para o início do body"],
|
|
225
227
|
]),
|
|
226
228
|
},
|
|
227
229
|
],
|
|
@@ -244,6 +246,7 @@ const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
|
244
246
|
"PageBody",
|
|
245
247
|
"PageFooter",
|
|
246
248
|
"PageTitle",
|
|
249
|
+
"PageDescription",
|
|
247
250
|
"PageBack",
|
|
248
251
|
"PageNavigation",
|
|
249
252
|
"PageActions",
|
|
@@ -308,24 +311,7 @@ const UI_REMOVED_PROPS = new Map([
|
|
|
308
311
|
["variant", "o cabeçalho da página tem uma única apresentação rolável"],
|
|
309
312
|
]),
|
|
310
313
|
],
|
|
311
|
-
|
|
312
|
-
"ActionFormDialog",
|
|
313
|
-
new Map([
|
|
314
|
-
[
|
|
315
|
-
"description",
|
|
316
|
-
"use `intro` somente para contexto relevante no início do corpo",
|
|
317
|
-
],
|
|
318
|
-
]),
|
|
319
|
-
],
|
|
320
|
-
[
|
|
321
|
-
"ActionListDialog",
|
|
322
|
-
new Map([
|
|
323
|
-
[
|
|
324
|
-
"description",
|
|
325
|
-
"use `intro` somente para contexto relevante no início do corpo",
|
|
326
|
-
],
|
|
327
|
-
]),
|
|
328
|
-
],
|
|
314
|
+
|
|
329
315
|
]);
|
|
330
316
|
|
|
331
317
|
const UI_REMOVED_COMPONENTS = new Map([
|
|
@@ -338,15 +324,6 @@ const UI_REMOVED_COMPONENTS = new Map([
|
|
|
338
324
|
["PaneDescription", "use `SurfaceDescription` dentro de `SurfaceHeader`"],
|
|
339
325
|
["PaneActions", "use `SurfaceActions` dentro de `SurfaceHeader`"],
|
|
340
326
|
["PageIntro", "use `PageHeader` como a única introdução estrutural"],
|
|
341
|
-
["PageDescription", "mova contexto relevante para o início de `PageBody`"],
|
|
342
|
-
[
|
|
343
|
-
"DialogDescription",
|
|
344
|
-
"mova contexto relevante para o início de `DialogBody`",
|
|
345
|
-
],
|
|
346
|
-
[
|
|
347
|
-
"DrawerDescription",
|
|
348
|
-
"mova contexto relevante para o início de `DrawerBody`",
|
|
349
|
-
],
|
|
350
327
|
// Content e Card deram lugar a Surface (ADR 0045).
|
|
351
328
|
[
|
|
352
329
|
"Content",
|
|
@@ -383,7 +360,7 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
383
360
|
"DialogIntro",
|
|
384
361
|
{
|
|
385
362
|
title: "DialogTitle",
|
|
386
|
-
optional: new Set(["DialogIntroActions"]),
|
|
363
|
+
optional: new Set(["DialogIntroActions", "DialogDescription"]),
|
|
387
364
|
shorthand: new Set(),
|
|
388
365
|
},
|
|
389
366
|
],
|
|
@@ -391,7 +368,7 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
391
368
|
"DrawerIntro",
|
|
392
369
|
{
|
|
393
370
|
title: "DrawerTitle",
|
|
394
|
-
optional: new Set(["DrawerIntroActions"]),
|
|
371
|
+
optional: new Set(["DrawerIntroActions", "DrawerDescription"]),
|
|
395
372
|
shorthand: new Set(),
|
|
396
373
|
},
|
|
397
374
|
],
|
|
@@ -399,7 +376,7 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
399
376
|
"PageHeader",
|
|
400
377
|
{
|
|
401
378
|
title: "PageTitle",
|
|
402
|
-
optional: new Set(["PageBack", "PageNavigation", "PageActions"]),
|
|
379
|
+
optional: new Set(["PageBack", "PageNavigation", "PageActions", "PageDescription"]),
|
|
403
380
|
exclusive: [new Set(["PageBack", "PageNavigation"])],
|
|
404
381
|
shorthand: new Set(),
|
|
405
382
|
titleRequired: false,
|
package/bin/lib/copy.mjs
CHANGED
|
@@ -99,9 +99,9 @@ const JSX_CHILD_ROLES = new Map([
|
|
|
99
99
|
['Button', 'button'],
|
|
100
100
|
['CommandEmpty', 'empty-state'],
|
|
101
101
|
['CommandItem', 'menu-item'],
|
|
102
|
-
['DialogDescription', '
|
|
102
|
+
['DialogDescription', 'description'],
|
|
103
103
|
['DialogTitle', 'title'],
|
|
104
|
-
['DrawerDescription', '
|
|
104
|
+
['DrawerDescription', 'description'],
|
|
105
105
|
['DrawerTitle', 'title'],
|
|
106
106
|
['EmptyDescription', 'empty-state'],
|
|
107
107
|
['EmptyTitle', 'title'],
|
|
@@ -123,6 +123,7 @@ const JSX_CHILD_ROLES = new Map([
|
|
|
123
123
|
// comando. Classificado como `button`, a política universal cobraria verbo de ação.
|
|
124
124
|
['PageBack', 'breadcrumb'],
|
|
125
125
|
['PageTitle', 'title'],
|
|
126
|
+
['PageDescription', 'description'],
|
|
126
127
|
['SurfaceDescription', 'description'],
|
|
127
128
|
['SurfaceTitle', 'title'],
|
|
128
129
|
['TableCaption', 'description'],
|
|
@@ -182,7 +183,7 @@ const JSX_PROP_ROLES = new Map([
|
|
|
182
183
|
'ActionFormDialog',
|
|
183
184
|
new Map([
|
|
184
185
|
['title', 'title'],
|
|
185
|
-
['description', '
|
|
186
|
+
['description', 'description'],
|
|
186
187
|
['intro', 'dialog-body'],
|
|
187
188
|
['submitLabel', 'button'],
|
|
188
189
|
['cancelLabel', 'button'],
|
|
@@ -202,7 +203,7 @@ const JSX_PROP_ROLES = new Map([
|
|
|
202
203
|
'ActionListDialog',
|
|
203
204
|
new Map([
|
|
204
205
|
['title', 'title'],
|
|
205
|
-
['description', '
|
|
206
|
+
['description', 'description'],
|
|
206
207
|
['intro', 'dialog-body'],
|
|
207
208
|
['emptyMessage', 'empty-state'],
|
|
208
209
|
['errorMessage', 'error'],
|
|
@@ -255,6 +256,7 @@ const JSX_PROP_ROLES = new Map([
|
|
|
255
256
|
'Page',
|
|
256
257
|
new Map([
|
|
257
258
|
['title', 'title'],
|
|
259
|
+
['description', 'description'],
|
|
258
260
|
]),
|
|
259
261
|
],
|
|
260
262
|
[
|
|
@@ -1503,6 +1505,7 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1503
1505
|
addProperty(object, 'label', 'label', prefix)
|
|
1504
1506
|
addProperty(object, 'placeholder', 'placeholder', prefix)
|
|
1505
1507
|
addProperty(object, 'help', 'helper-text', prefix)
|
|
1508
|
+
addProperty(object, 'hint', 'helper-text', prefix)
|
|
1506
1509
|
nestedObject(object, 'options', optionItems, prefix)
|
|
1507
1510
|
}
|
|
1508
1511
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Publicada originalmente com o número 0012, que colidia com a ADR de Produtos de Dados do mesmo
|
|
4
4
|
> dia. Renumerada em 2026-09-16; o conteúdo não mudou.
|
|
5
5
|
|
|
6
|
-
- **Status:**
|
|
6
|
+
- **Status:** parcialmente substituída pela ADR 0059 (subheading opcional no cabeçalho).
|
|
7
7
|
- **Data:** 2026-09-11.
|
|
8
8
|
|
|
9
9
|
## Contexto
|
|
@@ -158,6 +158,10 @@ anteriores já os delimitaram:
|
|
|
158
158
|
| Body | sim | sim | sim | sim | sim | sim |
|
|
159
159
|
| Footer | sim | não | sim | sim | sim | sim |
|
|
160
160
|
|
|
161
|
+
> **Atualização (2026-10-09):** a ADR 0059 substitui a restrição de descrição abaixo. Page, Dialog
|
|
162
|
+
> e Drawer passam a aceitar subheading opcional de orientação; regras necessárias ficam no corpo
|
|
163
|
+
> ou junto dos controles. A tabela registra a decisão original desta ADR.
|
|
164
|
+
|
|
161
165
|
As ADRs 0009, 0014 e 0018 não são reabertas: Page, Dialog e Drawer continuam sem descrição no
|
|
162
166
|
header, e Page continua sem contador. `Dialog`, `Drawer` e `Pane` ganham o slot de ações que hoje
|
|
163
167
|
não têm — a ausência obrigava a injetar nós soltos ao lado do título, e o `Studio` a manter três
|
|
@@ -39,6 +39,19 @@ de ADRs ou versões.
|
|
|
39
39
|
|
|
40
40
|
### Hierarquia e leitura
|
|
41
41
|
|
|
42
|
+
> **Emenda — 2026-10-08, protótipo local:** `DocGroup.groups` permite subgrupos
|
|
43
|
+
> opcionais. Um assunto como Integrações pode pertencer a Domínios e ainda reunir
|
|
44
|
+
> páginas por sistema. A navegação, a busca, a seleção direta e a sequência de
|
|
45
|
+
> leitura percorrem os mesmos grupos aninhados. Páginas próprias aparecem antes
|
|
46
|
+
> dos subgrupos; ramos sem resultados somem da busca, sem perder a abertura manual.
|
|
47
|
+
> O contrato anterior continua válido quando `groups` é omitido. Uma árvore paralela
|
|
48
|
+
> no host e rótulos artificiais para simular profundidade foram descartados por
|
|
49
|
+
> duplicar comportamento ou perder a hierarquia. Testes de links diretos, busca,
|
|
50
|
+
> grupos vazios, rótulos repetidos e continuidade verificam a extensão.
|
|
51
|
+
|
|
52
|
+
> **Consolidação — 2026-10-09:** a extensão de subgrupos compõe o candidato da versão
|
|
53
|
+
> 26.2.0, sobre a base 26.1.0. Publicação e adoção permanecem etapas separadas.
|
|
54
|
+
|
|
42
55
|
`sidebarSize` configura a largura inicial usando o `PaneSize` do `Split`. A omissão preserva
|
|
43
56
|
`14rem`. `sidebarInitiallyCollapsed` permite começar com seções e grupos nomeados recolhidos,
|
|
44
57
|
inclusive quando a pessoa abre uma página por URL direta. A busca revela os resultados
|
|
@@ -52,6 +65,16 @@ Esses destinos usam `SidebarItem.href`, não uma cópia do estilo da linha. A ex
|
|
|
52
65
|
botão quando `href` é omitido, o foco nos dois modos e a abertura nativa em outra aba. Quando há
|
|
53
66
|
`onClick`, somente a navegação simples é interceptada; o host mantém a decisão de roteamento.
|
|
54
67
|
|
|
68
|
+
> **Emenda — 2026-10-09:** destinos HTTP(S) absolutos em `sidebarLinks` usam a navegação
|
|
69
|
+
> nativa, pois podem apontar para outra aplicação. `newTab` é opcional em `SidebarItem`
|
|
70
|
+
> e `sidebarLinks`: com `href`, declara `target="_blank"`, `rel="noopener noreferrer"`
|
|
71
|
+
> e a abertura no nome acessível, sem chamar `onClick`. Sem `href`, não muda o botão.
|
|
72
|
+
> O portal acrescenta o ícone pelo slot `badge` do item canônico. Interceptar esses
|
|
73
|
+
> destinos no roteador ou copiar a linha no host foi descartado: o primeiro impede
|
|
74
|
+
> a navegação entre aplicações e o segundo duplica foco, acessibilidade e composição.
|
|
75
|
+
> Testes verificam links internos e absolutos, modificadores, nome acessível, ícone
|
|
76
|
+
> nos dois modos iniciais do portal e comportamento do item aberto ou recolhido.
|
|
77
|
+
|
|
55
78
|
O modo opcional `editorial` acrescenta contexto, sumário dos headings realmente renderizados e
|
|
56
79
|
links de continuidade segundo a ordem declarada. `additionalPages` preserva URLs de páginas fora
|
|
57
80
|
da árvore e da sequência. O renderer de exemplos aceita `codeCollapsible={false}` quando ocultar
|
|
@@ -143,6 +166,10 @@ e separa a consulta documental da consulta local.
|
|
|
143
166
|
|
|
144
167
|
- Testes de `DocBrowser`: busca local e externa, vazio, largura configurável, lateral opcional,
|
|
145
168
|
persistência de grupos, expansão temporária, conteúdo do host, redirects e leitura editorial.
|
|
169
|
+
- Regressões da extensão: espaçamento dos grupos sem rótulo em diferentes níveis, acervo vazio
|
|
170
|
+
sem consulta, modificadores em links absolutos e ícone de nova aba nos dois modos iniciais.
|
|
171
|
+
- Testes de `SidebarItem`: nova aba com modificadores, botão habilitado sem `href`, estado
|
|
172
|
+
desabilitado e preservação do badge textual no nome acessível do item aberto.
|
|
146
173
|
- Testes de `DocHeader`: slots, atributos nativos e composição com ou sem busca e navegação.
|
|
147
174
|
- Testes de `DocCatalog`: busca local controlada, independência do topo, ausência de controles de
|
|
148
175
|
coleção, texto integral das versões, incompatibilidades e abertura nativa em outra aba.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ADR 0059 — Subheadings orientam sem carregar regras de negócio
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita para implementação local; publicação pendente.
|
|
4
|
+
- **Data:** 2026-10-09.
|
|
5
|
+
- **Substitui:** ADR 0014.
|
|
6
|
+
- **Substitui parcialmente:** ADRs 0018 e 0023, quanto à proibição de descrição no cabeçalho.
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
A proibição de descrições nos cabeçalhos evitou texto de preenchimento, mas também deslocou
|
|
11
|
+
orientações úteis para o corpo. A distinção necessária é semântica: uma frase que apresenta a
|
|
12
|
+
finalidade da superfície pode ser ignorada sem comprometer a tarefa; uma regra ou consequência
|
|
13
|
+
necessária à decisão precisa estar visível no ponto em que será usada.
|
|
14
|
+
|
|
15
|
+
## Decisão
|
|
16
|
+
|
|
17
|
+
Page, Surface (inclusive os usos antes atendidos por Content e Card), Dialog e Drawer aceitam um
|
|
18
|
+
subheading opcional sob o título. A API usa `description` e os slots `PageDescription`,
|
|
19
|
+
`SurfaceDescription`, `DialogDescription` e `DrawerDescription`. Não há texto padrão nem obrigação
|
|
20
|
+
de preencher esse espaço. Frases que apenas repetem o título são omitidas.
|
|
21
|
+
|
|
22
|
+
“Escolha onde a pasta ‘Produtos’ e suas subpastas aparecem.” orienta a tarefa e pode ocupar esse
|
|
23
|
+
slot. “O motivo define se a observação é obrigatória e qual pendência será criada.” comunica regras:
|
|
24
|
+
as condições precisam acompanhar os respectivos campos, ações ou conteúdo. O tamanho da frase não
|
|
25
|
+
altera essa classificação. A pergunta de revisão é: ao pular o subheading, a pessoa ainda consegue
|
|
26
|
+
decidir e concluir corretamente?
|
|
27
|
+
|
|
28
|
+
`ActionFormDialog` e `ActionListDialog` separam `description`, orientação no cabeçalho, de `intro`,
|
|
29
|
+
contexto necessário no corpo. Quando ambos existem, a descrição acessível relaciona ambos os IDs.
|
|
30
|
+
Nas composições diretas, o consumidor mantém controle explícito de `aria-describedby`.
|
|
31
|
+
A API imperativa de decisões permanece com `body`: confirmações devem apresentar seus efeitos no
|
|
32
|
+
conteúdo da decisão. Fieldsets mantêm somente nome e mídia na legenda.
|
|
33
|
+
|
|
34
|
+
## Alternativas e consequências
|
|
35
|
+
|
|
36
|
+
Manter a proibição exigiria recriar subheadings no consumidor. Mover toda descrição para o
|
|
37
|
+
cabeçalho esconderia regras em uma região visual secundária. A escolha preserva os textos
|
|
38
|
+
necessários no corpo e permite orientação breve com a anatomia compartilhada.
|
|
39
|
+
|
|
40
|
+
As APIs são aditivas; `intro` existente não muda de lugar. A classificação semântica exige revisão
|
|
41
|
+
contextual de copy e não pode ser provada por uma busca de palavras ou limite de caracteres.
|
|
42
|
+
`FieldSpec.hint` mantém visível a orientação necessária junto ao controle; `help` oferece detalhe
|
|
43
|
+
complementar no ícone. Ambos podem coexistir sem repetir texto. Hint e erro participam da descrição
|
|
44
|
+
acessível do controle. A escolha depende da necessidade para decidir, não do tamanho da frase.
|
|
45
|
+
A Base mantém o princípio de comunicação; o Opus mantém componentes, documentação e extração.
|
|
46
|
+
|
|
47
|
+
## Verificação
|
|
48
|
+
|
|
49
|
+
Testar composição, omissão, coexistência com `intro`, associação acessível e extração de copy.
|
|
50
|
+
O checker deve aceitar os novos slots em seus pais e continuar rejeitando anatomias inválidas.
|
|
51
|
+
Validar no consumidor os exemplos de organização de pastas e configuração de motivos de perda.
|
package/docs/adr/README.md
CHANGED
|
@@ -10,3 +10,5 @@ entender por que uma restrição existe ou como ela evoluiu.
|
|
|
10
10
|
|
|
11
11
|
Uma ADR preserva seu contexto histórico mesmo quando é substituída. O comportamento vigente deve
|
|
12
12
|
estar documentado na superfície correspondente, sem exigir a reconstrução dessa sequência.
|
|
13
|
+
|
|
14
|
+
- [ADR 0059 — Subheadings orientam sem carregar regras de negócio](0059-subheadings-orient-without-carrying-business-rules.md).
|
package/package.json
CHANGED
|
@@ -66,3 +66,11 @@
|
|
|
66
66
|
sobreporem.
|
|
67
67
|
- Reprova: dimensionar select inline de filtro pelo conteúdo ou pela label (`min-w-*` solto,
|
|
68
68
|
largura calculada), em vez da largura fixa do pattern.
|
|
69
|
+
|
|
70
|
+
## Orientação, hint e help
|
|
71
|
+
|
|
72
|
+
Construir um formulário com orientação breve no cabeçalho, regra de seleção vazia e explicação
|
|
73
|
+
complementar do termo. Esperado: `description` orienta a finalidade, `hint` mantém visível o efeito
|
|
74
|
+
de deixar vazio e `help` oferece somente o detalhe opcional. `intro` continua no corpo quando a
|
|
75
|
+
regra afeta vários campos. Reprovar regra obrigatória somente em subheading ou tooltip, repetição
|
|
76
|
+
da label, duplicação entre hint/help e perda de `aria-describedby` quando aparece um erro.
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
- `Page` fornece o `<main>` e o container centralizado com teto padrão de `80rem`. Sua forma
|
|
11
11
|
explícita exige `PageBody`, exatamente um heading principal e aceita `PageHeader` e `PageFooter`.
|
|
12
12
|
Declare esse heading com `PageTitle` no header, `Surface level={1}` ou um `PageState` ativo no
|
|
13
|
-
body. `PageHeader` é a única introdução estrutural e reúne navegação, título
|
|
14
|
-
|
|
13
|
+
body. `PageHeader` é a única introdução estrutural e reúne navegação, título, `PageDescription`
|
|
14
|
+
opcional e ações. `title`, `description` e `actions` no próprio `Page` abreviam o caso direto.
|
|
15
15
|
Não misturar as duas formas. Alterar `className` apenas quando a superfície tiver uma necessidade
|
|
16
16
|
real de largura; não reconstruir esse container em cada rota.
|
|
17
|
-
- `PageHeader` declara navegação contextual, título e ações. Em uma subpágina simples, `PageBack`
|
|
17
|
+
- `PageHeader` declara navegação contextual, título, subheading opcional e ações. Em uma subpágina simples, `PageBack`
|
|
18
18
|
recebe o destino pai explícito e aparece acima da linha do título como controle somente com
|
|
19
19
|
ícone. Para mais de um ancestral relevante, use
|
|
20
20
|
`Breadcrumb` dentro de `PageNavigation`. Não combine retorno e breadcrumb nem crie um chrome
|
|
@@ -92,7 +92,9 @@ SurfaceTitle, SurfaceDescription, SurfaceMeta, SurfaceActions) + SurfaceBody + S
|
|
|
92
92
|
cancelamento a `outline`.
|
|
93
93
|
- Cabeçalhos de `Dialog` e `Drawer` nomeiam a superfície com o título e organizam suas ações. O close
|
|
94
94
|
padrão é o `CloseButton` (`neutral`, `subtle`, pílula) no final do header, sem posição flutuante
|
|
95
|
-
alternativa.
|
|
95
|
+
alternativa. `PageDescription`, `SurfaceDescription`, `DialogDescription` e `DrawerDescription`
|
|
96
|
+
orientam brevemente sobre a finalidade; wrappers usam `description`. Omitir repetição do título.
|
|
97
|
+
Pular esse subheading não pode comprometer a decisão. Consequência, restrição ou instrução que
|
|
96
98
|
realmente mude a tarefa entra no início de `DialogBody`/`DrawerBody`, como texto ou `Alert`;
|
|
97
99
|
wrappers usam `intro` e a API imperativa usa `body`. Relacione texto conciso por
|
|
98
100
|
`aria-describedby` somente quando ele também precisar descrever a superfície para tecnologias
|
|
@@ -219,3 +221,11 @@ cells={{
|
|
|
219
221
|
// Risco comportamental usado como nome visual.
|
|
220
222
|
<Button variant="destructive">Excluir</Button>
|
|
221
223
|
```
|
|
224
|
+
|
|
225
|
+
## Orientação de campo
|
|
226
|
+
|
|
227
|
+
Usar `FieldSpec.hint` para condição, formato ou consequência necessários à escolha, sempre visível
|
|
228
|
+
abaixo do controle. Reservar `FieldSpec.help` ao ícone com detalhe complementar sob demanda.
|
|
229
|
+
Omitir repetição da label; combinar ambos somente com funções distintas. Regra de vários campos
|
|
230
|
+
pertence ao corpo ou ao grupo. Em composição manual, `FieldDescription` e erro entram juntos no
|
|
231
|
+
`aria-describedby` do controle.
|
package/src/core/types.ts
CHANGED
|
@@ -618,8 +618,9 @@ export type FieldWidget = BuiltInFieldWidget | (string & { readonly __opusCustom
|
|
|
618
618
|
export interface FieldSpec {
|
|
619
619
|
label: I18nRef
|
|
620
620
|
placeholder?: I18nRef
|
|
621
|
-
/**
|
|
622
|
-
|
|
621
|
+
/** Orientação necessária para preencher ou prever o efeito da escolha, sempre visível. */
|
|
622
|
+
hint?: I18nRef
|
|
623
|
+
/** Explicação complementar sob demanda junto à label; não recebe regras indispensáveis. */
|
|
623
624
|
help?: I18nRef
|
|
624
625
|
|
|
625
626
|
default?: unknown
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
DialogContent,
|
|
20
20
|
DialogHeader,
|
|
21
21
|
DialogTitle,
|
|
22
|
+
DialogDescription,
|
|
22
23
|
} from '../primitives/dialog.tsx'
|
|
23
24
|
import { ActionList } from './list.tsx'
|
|
24
25
|
import type { ListAction } from '../../../core/index.ts'
|
|
@@ -34,6 +35,8 @@ export interface ActionListDialogProps<
|
|
|
34
35
|
open: boolean
|
|
35
36
|
onOpenChange: (open: boolean) => void
|
|
36
37
|
title: string
|
|
38
|
+
/** Orientação opcional sobre a finalidade; regras e consequências pertencem a intro ou aos campos. */
|
|
39
|
+
description?: ReactNode
|
|
37
40
|
/** Conteúdo relevante apresentado antes da lista, como texto ou Alert. */
|
|
38
41
|
intro?: ReactNode
|
|
39
42
|
/** Nota à esquerda da linha de ações (contexto curto). O total já mora no rodapé. */
|
|
@@ -63,6 +66,7 @@ export function ActionListDialog<
|
|
|
63
66
|
open,
|
|
64
67
|
onOpenChange,
|
|
65
68
|
title,
|
|
69
|
+
description,
|
|
66
70
|
intro,
|
|
67
71
|
note,
|
|
68
72
|
actions,
|
|
@@ -75,19 +79,22 @@ export function ActionListDialog<
|
|
|
75
79
|
children,
|
|
76
80
|
}: ActionListDialogProps<TItem, TInput>): React.ReactElement {
|
|
77
81
|
const introId = useId()
|
|
82
|
+
const descriptionId = useId()
|
|
83
|
+
const describedBy =
|
|
84
|
+
[description == null ? null : descriptionId, intro == null ? null : introId]
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.join(' ') || undefined
|
|
78
87
|
return (
|
|
79
88
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
80
|
-
<DialogContent
|
|
81
|
-
className={cn('max-w-3xl', className)}
|
|
82
|
-
aria-describedby={intro === undefined ? undefined : introId}
|
|
83
|
-
>
|
|
89
|
+
<DialogContent className={cn('max-w-3xl', className)} aria-describedby={describedBy}>
|
|
84
90
|
<DialogHeader>
|
|
85
91
|
<DialogTitle>{title}</DialogTitle>
|
|
92
|
+
{description != null && (
|
|
93
|
+
<DialogDescription id={descriptionId}>{description}</DialogDescription>
|
|
94
|
+
)}
|
|
86
95
|
</DialogHeader>
|
|
87
96
|
<DialogBody>
|
|
88
|
-
{intro
|
|
89
|
-
<div id={introId}>{intro}</div>
|
|
90
|
-
)}
|
|
97
|
+
{intro != null && <div id={introId}>{intro}</div>}
|
|
91
98
|
{(note !== undefined || actions !== undefined) && (
|
|
92
99
|
<div
|
|
93
100
|
data-slot="action-list-dialog-toolbar"
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
DialogFooter,
|
|
17
17
|
DialogHeader,
|
|
18
18
|
DialogTitle,
|
|
19
|
+
DialogDescription,
|
|
19
20
|
} from "../primitives/dialog.tsx";
|
|
20
21
|
import { ActionForm, type ActionFormProps } from "./form.tsx";
|
|
21
22
|
import { ButtonGroup } from "../primitives/button-group.tsx";
|
|
@@ -27,6 +28,8 @@ export interface ActionFormDialogProps<
|
|
|
27
28
|
open: boolean;
|
|
28
29
|
onOpenChange: (open: boolean) => void;
|
|
29
30
|
title: string;
|
|
31
|
+
/** Orientação opcional sobre a finalidade; regras e consequências pertencem a intro ou aos campos. */
|
|
32
|
+
description?: ReactNode;
|
|
30
33
|
/** Conteúdo relevante apresentado antes dos campos, como texto ou Alert. */
|
|
31
34
|
intro?: ReactNode;
|
|
32
35
|
/** Distribui as ações pela largura disponível somente quando as decisões pedem peso equivalente. */
|
|
@@ -40,6 +43,7 @@ export function ActionFormDialog<
|
|
|
40
43
|
open,
|
|
41
44
|
onOpenChange,
|
|
42
45
|
title,
|
|
46
|
+
description,
|
|
43
47
|
intro,
|
|
44
48
|
onSuccess,
|
|
45
49
|
onCancel,
|
|
@@ -48,15 +52,23 @@ export function ActionFormDialog<
|
|
|
48
52
|
...rest
|
|
49
53
|
}: ActionFormDialogProps<TInput, TData>) {
|
|
50
54
|
const introId = useId();
|
|
55
|
+
const descriptionId = useId();
|
|
56
|
+
const describedBy =
|
|
57
|
+
[description == null ? null : descriptionId, intro == null ? null : introId]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join(" ") || undefined;
|
|
51
60
|
return (
|
|
52
61
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
53
62
|
{/* Header e rodapé FIXOS, só o CORPO (campos) scrolla — o "X" não rola junto e
|
|
54
63
|
forms longos não estouram a viewport. O content já é coluna flex com teto. */}
|
|
55
|
-
<DialogContent
|
|
56
|
-
aria-describedby={intro === undefined ? undefined : introId}
|
|
57
|
-
>
|
|
64
|
+
<DialogContent aria-describedby={describedBy}>
|
|
58
65
|
<DialogHeader>
|
|
59
66
|
<DialogTitle>{title}</DialogTitle>
|
|
67
|
+
{description != null && (
|
|
68
|
+
<DialogDescription id={descriptionId}>
|
|
69
|
+
{description}
|
|
70
|
+
</DialogDescription>
|
|
71
|
+
)}
|
|
60
72
|
</DialogHeader>
|
|
61
73
|
<ActionForm
|
|
62
74
|
{...rest}
|
|
@@ -65,9 +77,7 @@ export function ActionFormDialog<
|
|
|
65
77
|
// faixa da casa — fonte única). Os botões seguem dentro do <form> do ActionForm.
|
|
66
78
|
body={(fields) => (
|
|
67
79
|
<DialogBody>
|
|
68
|
-
{intro
|
|
69
|
-
<div id={introId}>{intro}</div>
|
|
70
|
-
)}
|
|
80
|
+
{intro != null && <div id={introId}>{intro}</div>}
|
|
71
81
|
{fields}
|
|
72
82
|
</DialogBody>
|
|
73
83
|
)}
|
|
@@ -84,7 +94,8 @@ export function ActionFormDialog<
|
|
|
84
94
|
}}
|
|
85
95
|
onCancel={onCancel ?? (() => onOpenChange(false))}
|
|
86
96
|
cancelVariant={
|
|
87
|
-
cancelVariant ??
|
|
97
|
+
cancelVariant ??
|
|
98
|
+
(footerDistribution === "equal" ? "outline" : "ghost")
|
|
88
99
|
}
|
|
89
100
|
/>
|
|
90
101
|
</DialogContent>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Dois modos, um motor:
|
|
5
5
|
* - AUTO (sem children): monta TODOS os campos na ordem do contrato — zero JSX de campo.
|
|
6
6
|
* - COMPOSIÇÃO (com children): você diagrama; <ActionFormField name /> coloca cada campo
|
|
7
|
-
* (label/help/widget/erro/asterisco derivados do contrato) onde quiser. Condicional é
|
|
7
|
+
* (label/hint/help/widget/erro/asterisco derivados do contrato) onde quiser. Condicional é
|
|
8
8
|
* JSX ({cond && <ActionFormField/>}); opções de runtime entram por prop no campo. O
|
|
9
9
|
* FieldGroup canônico permanece nos dois modos; grids e seções entram como filhos explícitos.
|
|
10
10
|
*
|
|
@@ -54,6 +54,7 @@ import { ToggleGroup } from "../primitives/toggle-group.tsx";
|
|
|
54
54
|
import {
|
|
55
55
|
Field,
|
|
56
56
|
FieldError,
|
|
57
|
+
FieldDescription,
|
|
57
58
|
FieldGroup,
|
|
58
59
|
FieldLabel,
|
|
59
60
|
} from "../primitives/field.tsx";
|
|
@@ -137,7 +138,9 @@ function inferFieldKind(schema: ZodTypeAny): FieldKind {
|
|
|
137
138
|
interface FieldSpec {
|
|
138
139
|
label?: string;
|
|
139
140
|
placeholder?: string;
|
|
140
|
-
/**
|
|
141
|
+
/** Orientação necessária sempre visível abaixo do controle. */
|
|
142
|
+
hint?: string;
|
|
143
|
+
/** Detalhe complementar no ícone junto à label, disponível por hover ou foco. */
|
|
141
144
|
help?: string;
|
|
142
145
|
/** Override explícito do tipo de campo, quando o auto-detect do Zod não basta.
|
|
143
146
|
* Honra 'textarea' (string sem max), 'code' (textarea monoespaçada, ex.: SKILL.md),
|
|
@@ -206,9 +209,8 @@ function dictMetaOptions(schema: ZodTypeAny): SelectOption[] | undefined {
|
|
|
206
209
|
* lugar (`<FieldLabel>Nome<LabelHelp help="…" /></FieldLabel>`) e o `opus copy` inventarie
|
|
207
210
|
* o texto de `help` como helper-text.
|
|
208
211
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* (não foca o input / não toggla).
|
|
212
|
+
* O botão participa do Tab e abre por foco ou hover. Dialog e Drawer preservam o foco
|
|
213
|
+
* inicial no controle, sem remover a ajuda da navegação por teclado. Clicar não ativa a label.
|
|
212
214
|
*/
|
|
213
215
|
export function LabelHelp({
|
|
214
216
|
help,
|
|
@@ -222,7 +224,8 @@ export function LabelHelp({
|
|
|
222
224
|
<TooltipTrigger asChild>
|
|
223
225
|
<button
|
|
224
226
|
type="button"
|
|
225
|
-
|
|
227
|
+
data-slot="label-help"
|
|
228
|
+
tabIndex={0}
|
|
226
229
|
aria-label="Ajuda"
|
|
227
230
|
onClick={(e) => {
|
|
228
231
|
// Clicar o ícone não ativa o label (não foca/toggla o campo).
|
|
@@ -344,7 +347,8 @@ export function ActionFormField({
|
|
|
344
347
|
const errorMessage =
|
|
345
348
|
typeof fieldError?.message === "string" ? fieldError.message : undefined;
|
|
346
349
|
const errorId = errorMessage !== undefined ? `${name}-error` : undefined;
|
|
347
|
-
const
|
|
350
|
+
const hintId = spec.hint?.trim() ? `${name}-hint` : undefined;
|
|
351
|
+
const describedBy = [hintId, errorId].filter(Boolean).join(" ") || undefined;
|
|
348
352
|
useEffect(() => {
|
|
349
353
|
if (errorMessage === undefined) return;
|
|
350
354
|
const control = document.getElementById(name);
|
|
@@ -430,6 +434,9 @@ export function ActionFormField({
|
|
|
430
434
|
</span>
|
|
431
435
|
<LabelHelp help={spec.help} />
|
|
432
436
|
</FieldLabel>
|
|
437
|
+
{hintId !== undefined && (
|
|
438
|
+
<FieldDescription id={hintId}>{spec.hint}</FieldDescription>
|
|
439
|
+
)}
|
|
433
440
|
{errorMessage !== undefined && (
|
|
434
441
|
<FieldError id={errorId}>{errorMessage}</FieldError>
|
|
435
442
|
)}
|
|
@@ -683,6 +690,9 @@ export function ActionFormField({
|
|
|
683
690
|
/>
|
|
684
691
|
)}
|
|
685
692
|
|
|
693
|
+
{hintId !== undefined && (
|
|
694
|
+
<FieldDescription id={hintId}>{spec.hint}</FieldDescription>
|
|
695
|
+
)}
|
|
686
696
|
{errorMessage !== undefined && (
|
|
687
697
|
<FieldError id={errorId}>{errorMessage}</FieldError>
|
|
688
698
|
)}
|
|
@@ -774,7 +784,25 @@ export function ActionForm<TInput extends Record<string, unknown>, TData>({
|
|
|
774
784
|
string,
|
|
775
785
|
ZodTypeAny | undefined
|
|
776
786
|
>;
|
|
777
|
-
|
|
787
|
+
// O contexto entrega texto resolvido também aos renderizadores de campo do consumidor.
|
|
788
|
+
const fields: Record<string, FieldSpec> = Object.fromEntries(
|
|
789
|
+
Object.entries(action.fields ?? {}).map(([name, declared]) => {
|
|
790
|
+
const spec = declared as FieldSpec;
|
|
791
|
+
return [
|
|
792
|
+
name,
|
|
793
|
+
{
|
|
794
|
+
...spec,
|
|
795
|
+
label: msgText(spec.label, name),
|
|
796
|
+
placeholder:
|
|
797
|
+
spec.placeholder === undefined
|
|
798
|
+
? undefined
|
|
799
|
+
: msgText(spec.placeholder, ""),
|
|
800
|
+
hint: spec.hint === undefined ? undefined : msgText(spec.hint, ""),
|
|
801
|
+
help: spec.help === undefined ? undefined : msgText(spec.help, ""),
|
|
802
|
+
},
|
|
803
|
+
];
|
|
804
|
+
}),
|
|
805
|
+
);
|
|
778
806
|
|
|
779
807
|
// Corpo e rodapé como SLOTS: por padrão renderizam inline; o ActionFormDialog injeta
|
|
780
808
|
// DialogBody/DialogFooter (fonte única do scroll e da faixa). Os botões ficam DENTRO do
|