@softize/opus 26.0.0 → 26.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -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/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/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/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/drawer.md +24 -2
- package/src/ui/docs/content/page.md +23 -1
- package/src/ui/docs/content/surface.md +8 -2
- package/src/ui/lib/modal-layers.ts +27 -3
- package/src/ui/react.tsx +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ 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.1.0 — 2026-10-09
|
|
11
|
+
|
|
12
|
+
Subheadings descrevem a finalidade da superfície; orientações necessárias ficam visíveis no corpo
|
|
13
|
+
ou junto ao campo (ADR 0059).
|
|
14
|
+
|
|
15
|
+
- `Page` aceita `description`. `PageDescription`, `DialogDescription` e `DrawerDescription`
|
|
16
|
+
compõem o cabeçalho ou a introdução declarada da superfície.
|
|
17
|
+
- `ActionFormDialog` e `ActionListDialog` recebem `description` para o cabeçalho. `intro` continua
|
|
18
|
+
no corpo, para regras e consequências da tarefa; ambos participam da descrição acessível.
|
|
19
|
+
- `FieldSpec.hint` declara orientação visível abaixo do controle. `ActionFormField` associa hint
|
|
20
|
+
e erro por `aria-describedby`; `help` continua sendo a ajuda opcional junto à label.
|
|
21
|
+
- Os textos dos campos resolvem `I18nRef` pelo texto padrão também nos renderizadores próprios.
|
|
22
|
+
O ícone de ajuda participa do Tab e abre por foco, sem tomar o foco inicial de outro controle
|
|
23
|
+
em Dialog e Drawer.
|
|
24
|
+
- O inventário de copy, o validador estrutural, a skill de UI e os exemplos da documentação
|
|
25
|
+
reconhecem esses papéis sem proibir subheadings de maneira indiscriminada.
|
|
26
|
+
|
|
27
|
+
**O que fazer:** rode `opus setup` e `opus copy`; revise a posição das orientações existentes.
|
|
28
|
+
Renderizadores próprios de campos devem apresentar `hint` e associá-lo ao controle, preservando
|
|
29
|
+
os erros. Nenhum texto migra automaticamente de `help` para `hint`.
|
|
30
|
+
|
|
10
31
|
## 26.0.0 — 2026-10-09
|
|
11
32
|
|
|
12
33
|
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
|
|
@@ -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
|
|
@@ -147,6 +147,8 @@ interface PageBaseProps extends Omit<
|
|
|
147
147
|
|
|
148
148
|
interface PageShorthandProps extends PageBaseProps {
|
|
149
149
|
title: ReactNode;
|
|
150
|
+
/** Orientação opcional sobre a finalidade da página; regras pertencem ao corpo. */
|
|
151
|
+
description?: ReactNode;
|
|
150
152
|
actions?: ReactNode;
|
|
151
153
|
children: ReactNode;
|
|
152
154
|
}
|
|
@@ -224,6 +226,7 @@ function hasOpaquePageBodyContent(node: ReactNode): boolean {
|
|
|
224
226
|
|
|
225
227
|
export function Page({
|
|
226
228
|
title,
|
|
229
|
+
description,
|
|
227
230
|
actions,
|
|
228
231
|
layout = "flow",
|
|
229
232
|
className,
|
|
@@ -343,6 +346,9 @@ export function Page({
|
|
|
343
346
|
<>
|
|
344
347
|
<PageHeader>
|
|
345
348
|
<PageTitle>{title}</PageTitle>
|
|
349
|
+
{description !== undefined && (
|
|
350
|
+
<PageDescription>{description}</PageDescription>
|
|
351
|
+
)}
|
|
346
352
|
{actions !== undefined && <PageActions>{actions}</PageActions>}
|
|
347
353
|
</PageHeader>
|
|
348
354
|
<PageBody>{children}</PageBody>
|
|
@@ -461,6 +467,7 @@ export function PageHeader({
|
|
|
461
467
|
slots={{
|
|
462
468
|
leading: [PageBack, PageNavigation],
|
|
463
469
|
title: PageTitle,
|
|
470
|
+
description: PageDescription,
|
|
464
471
|
actions: PageActions,
|
|
465
472
|
}}
|
|
466
473
|
leadingPlacement="above"
|
|
@@ -496,6 +503,21 @@ export function PageTitle({
|
|
|
496
503
|
);
|
|
497
504
|
}
|
|
498
505
|
|
|
506
|
+
/** Orientação breve e dispensável para a decisão; não recebe regras de negócio. */
|
|
507
|
+
export function PageDescription({
|
|
508
|
+
className,
|
|
509
|
+
...props
|
|
510
|
+
}: HTMLAttributes<HTMLParagraphElement>): ReactElement {
|
|
511
|
+
requireParent(useContext(PageHeaderContext), "PageDescription", "PageHeader");
|
|
512
|
+
return (
|
|
513
|
+
<p
|
|
514
|
+
data-slot="page-description"
|
|
515
|
+
className={cn(structuralHeaderClasses.page.description, className)}
|
|
516
|
+
{...props}
|
|
517
|
+
/>
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
499
521
|
export function PageActions({
|
|
500
522
|
className,
|
|
501
523
|
...props
|
|
@@ -60,7 +60,7 @@ interface FamilyAnatomy {
|
|
|
60
60
|
regions: Record<SurfaceLayout, RegionAnatomy>;
|
|
61
61
|
/** Escala visual do título. O nível semântico do heading é escolhido à parte. */
|
|
62
62
|
title: Record<SurfaceLayout, string>;
|
|
63
|
-
/** Descrição no header, quando a família a oferece (
|
|
63
|
+
/** Descrição no header, quando a família a oferece (ADR 0059 inclui subheadings opcionais em modais). */
|
|
64
64
|
description: string;
|
|
65
65
|
/** Espaço entre mídia, título e ações na linha do header. */
|
|
66
66
|
headerGap: string;
|
|
@@ -5,6 +5,7 @@ import { cn } from "../../lib/cn.ts";
|
|
|
5
5
|
import {
|
|
6
6
|
SurfaceLayoutProvider,
|
|
7
7
|
surfaceActionsClasses,
|
|
8
|
+
surfaceDescriptionClasses,
|
|
8
9
|
surfaceHeaderRowClasses,
|
|
9
10
|
surfaceRegionClasses,
|
|
10
11
|
surfaceTitleClasses,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
exemptLayerFirst,
|
|
27
28
|
focusIsInLayer,
|
|
28
29
|
handleModalTabKey,
|
|
30
|
+
skipHelpOnAutoFocus,
|
|
29
31
|
isLayerInteraction,
|
|
30
32
|
routeEscapeToLayer,
|
|
31
33
|
useLayerPortal,
|
|
@@ -415,6 +417,7 @@ export function DialogContent({
|
|
|
415
417
|
onEscapeKeyDown,
|
|
416
418
|
onPointerDownOutside,
|
|
417
419
|
onInteractOutside,
|
|
420
|
+
onOpenAutoFocus,
|
|
418
421
|
onCloseAutoFocus,
|
|
419
422
|
onKeyDown,
|
|
420
423
|
ref,
|
|
@@ -481,6 +484,7 @@ export function DialogContent({
|
|
|
481
484
|
aria-describedby={ariaDescribedBy}
|
|
482
485
|
// O `AlertDialog` do Radix cancela sozinho o clique fora; só o Esc é composto.
|
|
483
486
|
onEscapeKeyDown={dismiss.onEscapeKeyDown}
|
|
487
|
+
onOpenAutoFocus={onOpenAutoFocus}
|
|
484
488
|
onCloseAutoFocus={dismiss.onCloseAutoFocus}
|
|
485
489
|
onKeyDown={onKeyDown}
|
|
486
490
|
{...props}
|
|
@@ -532,6 +536,10 @@ export function DialogContent({
|
|
|
532
536
|
onPointerDownOutside={dismiss.onPointerDownOutside}
|
|
533
537
|
onInteractOutside={dismiss.onInteractOutside}
|
|
534
538
|
onCloseAutoFocus={dismiss.onCloseAutoFocus}
|
|
539
|
+
onOpenAutoFocus={(event) => {
|
|
540
|
+
onOpenAutoFocus?.(event);
|
|
541
|
+
skipHelpOnAutoFocus(event);
|
|
542
|
+
}}
|
|
535
543
|
onKeyDown={handleKeyDown}
|
|
536
544
|
{...props}
|
|
537
545
|
>
|
|
@@ -584,6 +592,7 @@ export function DialogHeader({
|
|
|
584
592
|
leading: DialogNavigation,
|
|
585
593
|
media: DialogMedia,
|
|
586
594
|
title: DialogTitle,
|
|
595
|
+
description: DialogDescription,
|
|
587
596
|
actions: DialogActions,
|
|
588
597
|
}}
|
|
589
598
|
leadingPlacement="inline"
|
|
@@ -645,7 +654,11 @@ export function DialogIntro({
|
|
|
645
654
|
<StructuralHeader
|
|
646
655
|
name="DialogIntro"
|
|
647
656
|
slot="dialog-intro"
|
|
648
|
-
slots={{
|
|
657
|
+
slots={{
|
|
658
|
+
title: DialogTitle,
|
|
659
|
+
description: DialogDescription,
|
|
660
|
+
actions: DialogIntroActions,
|
|
661
|
+
}}
|
|
649
662
|
leadingPlacement="inline"
|
|
650
663
|
titleRequired
|
|
651
664
|
className={cn("text-left", className)}
|
|
@@ -770,3 +783,17 @@ export function DialogTitle({
|
|
|
770
783
|
</Title>
|
|
771
784
|
);
|
|
772
785
|
}
|
|
786
|
+
|
|
787
|
+
/** Orientação opcional sob o título; condições e consequências ficam no corpo. */
|
|
788
|
+
export function DialogDescription({
|
|
789
|
+
className,
|
|
790
|
+
...props
|
|
791
|
+
}: React.ComponentProps<"p">) {
|
|
792
|
+
return (
|
|
793
|
+
<p
|
|
794
|
+
data-slot="dialog-description"
|
|
795
|
+
className={cn("mt-1", surfaceDescriptionClasses("dialog"), className)}
|
|
796
|
+
{...props}
|
|
797
|
+
/>
|
|
798
|
+
);
|
|
799
|
+
}
|
|
@@ -5,6 +5,7 @@ import { cn } from "../../lib/cn.ts";
|
|
|
5
5
|
import {
|
|
6
6
|
SurfaceLayoutProvider,
|
|
7
7
|
surfaceActionsClasses,
|
|
8
|
+
surfaceDescriptionClasses,
|
|
8
9
|
surfaceHeaderRowClasses,
|
|
9
10
|
surfaceRegionClasses,
|
|
10
11
|
surfaceTitleClasses,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
LAYER_MODAL_ATTRIBUTE,
|
|
29
30
|
LayerPortalProvider,
|
|
30
31
|
handleModalTabKey,
|
|
32
|
+
skipHelpOnAutoFocus,
|
|
31
33
|
useLayerPortal,
|
|
32
34
|
useModalLayerEntry,
|
|
33
35
|
} from "../../lib/modal-layers.ts";
|
|
@@ -113,7 +115,9 @@ function DrawerOverlay({
|
|
|
113
115
|
return (
|
|
114
116
|
<DrawerPrimitive.Overlay
|
|
115
117
|
data-slot="drawer-overlay"
|
|
116
|
-
{...{
|
|
118
|
+
{...{
|
|
119
|
+
[LAYER_MODAL_ATTRIBUTE]: layer.container !== undefined || undefined,
|
|
120
|
+
}}
|
|
117
121
|
className={cn(overlayClasses, layer.zIndexClass, className)}
|
|
118
122
|
{...props}
|
|
119
123
|
/>
|
|
@@ -146,6 +150,7 @@ function DrawerContent({
|
|
|
146
150
|
onEscapeKeyDown,
|
|
147
151
|
onPointerDownOutside,
|
|
148
152
|
onInteractOutside,
|
|
153
|
+
onOpenAutoFocus,
|
|
149
154
|
onCloseAutoFocus,
|
|
150
155
|
onKeyDown,
|
|
151
156
|
ref,
|
|
@@ -218,6 +223,10 @@ function DrawerContent({
|
|
|
218
223
|
onPointerDownOutside={dismiss.onPointerDownOutside}
|
|
219
224
|
onInteractOutside={dismiss.onInteractOutside}
|
|
220
225
|
onCloseAutoFocus={dismiss.onCloseAutoFocus}
|
|
226
|
+
onOpenAutoFocus={(event) => {
|
|
227
|
+
onOpenAutoFocus?.(event);
|
|
228
|
+
skipHelpOnAutoFocus(event);
|
|
229
|
+
}}
|
|
221
230
|
onKeyDown={handleKeyDown}
|
|
222
231
|
{...props}
|
|
223
232
|
>
|
|
@@ -251,6 +260,7 @@ function DrawerHeader({
|
|
|
251
260
|
leading: DrawerNavigation,
|
|
252
261
|
media: DrawerMedia,
|
|
253
262
|
title: DrawerTitle,
|
|
263
|
+
description: DrawerDescription,
|
|
254
264
|
actions: DrawerActions,
|
|
255
265
|
}}
|
|
256
266
|
leadingPlacement="inline"
|
|
@@ -314,7 +324,11 @@ function DrawerIntro({
|
|
|
314
324
|
<StructuralHeader
|
|
315
325
|
name="DrawerIntro"
|
|
316
326
|
slot="drawer-intro"
|
|
317
|
-
slots={{
|
|
327
|
+
slots={{
|
|
328
|
+
title: DrawerTitle,
|
|
329
|
+
description: DrawerDescription,
|
|
330
|
+
actions: DrawerIntroActions,
|
|
331
|
+
}}
|
|
318
332
|
leadingPlacement="inline"
|
|
319
333
|
titleRequired
|
|
320
334
|
className={cn("text-left", className)}
|
|
@@ -435,3 +449,17 @@ export {
|
|
|
435
449
|
DrawerFooter,
|
|
436
450
|
DrawerTitle,
|
|
437
451
|
};
|
|
452
|
+
|
|
453
|
+
/** Orientação opcional sob o título; condições e consequências ficam no corpo. */
|
|
454
|
+
export function DrawerDescription({
|
|
455
|
+
className,
|
|
456
|
+
...props
|
|
457
|
+
}: React.ComponentProps<"p">) {
|
|
458
|
+
return (
|
|
459
|
+
<p
|
|
460
|
+
data-slot="drawer-description"
|
|
461
|
+
className={cn("mt-1", surfaceDescriptionClasses("drawer"), className)}
|
|
462
|
+
{...props}
|
|
463
|
+
/>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
@@ -28,7 +28,7 @@ a mesma.
|
|
|
28
28
|
| ------------------- | ---------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
29
|
| `title` | `string` | | Título do cabeçalho. Sem ele e sem `description`, o card começa diretamente pelo corpo. |
|
|
30
30
|
| `titleLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `3` | Nível do heading do título. Muda a hierarquia do documento, não a escala visual. |
|
|
31
|
-
| `description` | `string` | |
|
|
31
|
+
| `description` | `string` | | Orientação opcional sobre a finalidade. Regras e consequências ficam junto dos campos ou no corpo. |
|
|
32
32
|
| `cardClassName` | `string` | | Classes aplicadas à superfície do card. |
|
|
33
33
|
| `className` | `string` | | Classes aplicadas ao `<form>` interno. |
|
|
34
34
|
| Demais propriedades | Propriedades de `ActionForm` | | `action`, `defaultValues`, `fieldOptions`, `submitLabel`, `cancelLabel`, `cancelVariant`, `onCancel`, `onSuccess`, `disabled`, `onLoadingChange` e `children` mantêm o comportamento de `ActionForm`. `body` e `footer` não são aceitas: o card fornece o corpo e o rodapé. |
|
|
@@ -35,12 +35,20 @@ salvo escolha explícita em `cancelVariant`.
|
|
|
35
35
|
|
|
36
36
|
Para consultar uma coleção no mesmo formato de modal, use [`ActionListDialog`](/ui/action-list-dialog).
|
|
37
37
|
|
|
38
|
+
## Orientação e contexto
|
|
39
|
+
|
|
40
|
+
`description` apresenta a finalidade no cabeçalho, como “Escolha onde a pasta e suas subpastas
|
|
41
|
+
aparecem.”. É opcional; não repita o título. `intro` apresenta no corpo informações necessárias
|
|
42
|
+
à decisão que não pertencem a um campo específico. Regras, condições e consequências não ficam
|
|
43
|
+
somente no subheading. Quando ambos são fornecidos, ambos participam de `aria-describedby`.
|
|
44
|
+
|
|
38
45
|
## Propriedades de ActionFormDialog
|
|
39
46
|
|
|
40
47
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
41
48
|
| --------------------- | ------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
49
|
| `open / onOpenChange` | `boolean / (open: boolean) => void` | | Estado controlado do modal. `onOpenChange(false)` é chamado no sucesso e, quando não há `onCancel` próprio, ao cancelar. |
|
|
43
50
|
| `title` | `string` | | Operação e recurso exibidos no cabeçalho, como `Criar workspace` ou `Editar workspace`. |
|
|
51
|
+
| `description` | `ReactNode` | | Orientação opcional sob o título; não recebe regras de negócio. |
|
|
44
52
|
| `intro` | `ReactNode` | | Contexto relevante no início do corpo, como texto ou `Alert`. |
|
|
45
53
|
| `submitLabel` | `string` | `'Salvar'` | Resultado da ação principal. Preserve o padrão em criação e edição comuns; sobrescreva apenas quando a operação tiver outro efeito, como `Renomear` ou `Criar nova versão`. |
|
|
46
54
|
| `footerDistribution` | `'content' \| 'equal'` | `'content'` | Mantém a largura das ações pelo conteúdo ou divide a faixa igualmente. |
|
|
@@ -84,12 +84,21 @@ do mesmo peso visual. Nesse caso, o cancelamento muda para `outline`, salvo esco
|
|
|
84
84
|
`cancelVariant`. Para apresentar o formulário como uma seção delimitada da página, use
|
|
85
85
|
[`ActionFormCard`](/ui/action-form-card). As propriedades de cada wrapper estão na página dele.
|
|
86
86
|
|
|
87
|
-
##
|
|
87
|
+
## Hint visível ou ícone de help
|
|
88
88
|
|
|
89
|
-
`FieldSpec.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`FieldDescription`
|
|
89
|
+
Use `FieldSpec.hint` abaixo do controle quando a pessoa precisar da informação para preencher
|
|
90
|
+
corretamente ou prever o efeito da escolha. Exemplo: “Sem áreas selecionadas, a pasta aparece em
|
|
91
|
+
todas as áreas.” O texto permanece visível e participa do `aria-describedby` do controle, junto
|
|
92
|
+
do erro quando houver. Em uma composição própria, use `FieldDescription` com a mesma associação.
|
|
93
|
+
|
|
94
|
+
Use `FieldSpec.help` no ícone junto à label para detalhes complementares, definições e exemplos
|
|
95
|
+
opcionais. A pergunta é: sem abrir essa ajuda, a pessoa ainda consegue escolher corretamente?
|
|
96
|
+
Se não, o trecho necessário deve ser hint. Não use o comprimento da frase como critério nem
|
|
97
|
+
esconda uma condição obrigatória no tooltip. Omita texto que apenas repete a label.
|
|
98
|
+
|
|
99
|
+
Hint e help podem coexistir se tiverem funções diferentes, sem duplicar a mesma frase. Um erro
|
|
100
|
+
explica uma entrada inválida e sua correção; não substitui uma orientação necessária antes de
|
|
101
|
+
preencher. Uma regra que afeta vários campos pertence ao corpo ou ao grupo correspondente.
|
|
93
102
|
|
|
94
103
|
`LabelHelp` é esse mesmo ícone, exportado para um formulário sem contrato mostrar a ajuda no mesmo
|
|
95
104
|
lugar:
|
|
@@ -185,3 +194,7 @@ ciclo de vida que o pattern não cobre; validação e execução continuam iguai
|
|
|
185
194
|
| Propriedade | Tipo | Descrição |
|
|
186
195
|
| ----------- | -------- | ------------------------------------------------------------ |
|
|
187
196
|
| `help` | `string` | O texto do tooltip. Vazio ou ausente, o ícone não renderiza. |
|
|
197
|
+
|
|
198
|
+
O `hint` aceita texto ou `I18nRef` e permanece visível; o contexto do formulário entrega o texto
|
|
199
|
+
resolvido aos campos personalizados. O botão de `help` participa do Tab e abre por foco ou hover.
|
|
200
|
+
Dialog e Drawer evitam que a ajuda seja o primeiro foco automático quando há outro controle.
|
|
@@ -96,6 +96,13 @@ const roleOptions = roles.items.map((role) => ({ value: role.id, label: role.nam
|
|
|
96
96
|
</ActionListDialog>;
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
## Orientação e contexto
|
|
100
|
+
|
|
101
|
+
`description` apresenta a finalidade no cabeçalho, como “Escolha onde a pasta e suas subpastas
|
|
102
|
+
aparecem.”. É opcional; não repita o título. `intro` apresenta no corpo informações necessárias
|
|
103
|
+
à decisão que não pertencem a um campo específico. Regras, condições e consequências não ficam
|
|
104
|
+
somente no subheading. Quando ambos são fornecidos, ambos participam de `aria-describedby`.
|
|
105
|
+
|
|
99
106
|
## Propriedades de ActionListDialog
|
|
100
107
|
|
|
101
108
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -103,6 +110,7 @@ const roleOptions = roles.items.map((role) => ({ value: role.id, label: role.nam
|
|
|
103
110
|
| `action / input` | `ListAction / TInput` | `input`: `{}` | O contrato e o escopo base da consulta. Uma mudança em `input` refaz a consulta; as invalidações declaradas por forms e triggers também. |
|
|
104
111
|
| `open / onOpenChange` | `boolean / (open) => void` | | Estado controlado do modal. |
|
|
105
112
|
| `title` | `string` | | Nome da lista exibido no cabeçalho fixo. |
|
|
113
|
+
| `description` | `ReactNode` | | Orientação opcional sob o título; não recebe regras de negócio. |
|
|
106
114
|
| `intro` | `ReactNode` | | Contexto relevante no início do corpo, como texto ou `Alert`. |
|
|
107
115
|
| `children` | `(items, refetch) => ReactNode` | | A composição dos itens. Carregamento, erro e vazio já são tratados pelo `ActionList`. |
|
|
108
116
|
| `note` | `ReactNode` | | Contexto curto à esquerda da linha de ações. O total de itens já aparece no rodapé do `ActionList`. |
|
|
@@ -150,8 +150,21 @@ Voltar e ações do cabeçalho usam `Button` `ghost` no tamanho `icon` (2.25rem)
|
|
|
150
150
|
[`Presentation`](/ui/presentation) usa no voltar. O botão de fechar é menor: um `CloseButton` no
|
|
151
151
|
quadrado `icon-sm` (1.75rem) por padrão.
|
|
152
152
|
|
|
153
|
-
O cabeçalho
|
|
154
|
-
|
|
153
|
+
O cabeçalho aceita `DialogDescription` como subheading opcional para orientar sobre a finalidade.
|
|
154
|
+
Omita quando repetir o título. Condições, regras de negócio e consequências necessárias à decisão
|
|
155
|
+
ficam junto dos controles ou em `DialogBody`. Uma frase curta também pode ser regra: o critério é se
|
|
156
|
+
pular o texto compromete a decisão, não sua extensão.
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
<DialogHeader>
|
|
160
|
+
<DialogTitle>Organizar pasta</DialogTitle>
|
|
161
|
+
<DialogDescription>Escolha onde a pasta e suas subpastas aparecem.</DialogDescription>
|
|
162
|
+
</DialogHeader>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Na composição direta, associe o `id` do texto por `aria-describedby` em `DialogContent` quando ele
|
|
166
|
+
descrever a superfície para tecnologias assistivas. A regra sobre seleção vazia pertence ao
|
|
167
|
+
campo de áreas e permanece fora do subheading.
|
|
155
168
|
|
|
156
169
|
## Identificar a tarefa com um ícone
|
|
157
170
|
|
|
@@ -634,3 +647,12 @@ conteúdo no primeiro bloco de `DialogBody`. `DialogIntro` exige um `DialogTitle
|
|
|
634
647
|
|
|
635
648
|
No formato compacto, mantenha `DialogTitle` e `DialogActions` no próprio `DialogHeader`. Para
|
|
636
649
|
comparar as duas formas em cada `layout`, abra [Composições](/ui/compositions).
|
|
650
|
+
|
|
651
|
+
## Propriedades de DialogDescription
|
|
652
|
+
|
|
653
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
654
|
+
| --- | --- | --- | --- |
|
|
655
|
+
| `children` | `ReactNode` | | Orientação opcional sob o título em `DialogHeader` ou `DialogIntro`. |
|
|
656
|
+
| `id` | `string` | | Identificador para associação acessível explícita. |
|
|
657
|
+
| `className` | `string` | | Ajusta a apresentação do parágrafo. |
|
|
658
|
+
| `…props` | `ComponentProps<"p">` | | Atributos nativos. |
|
|
@@ -148,8 +148,21 @@ Voltar e ações do cabeçalho usam `Button` `ghost` no tamanho `icon` (2.25rem)
|
|
|
148
148
|
[`Presentation`](/ui/presentation) usa no voltar. O botão de fechar é menor: um `CloseButton` no
|
|
149
149
|
quadrado `icon-sm` (1.75rem) por padrão.
|
|
150
150
|
|
|
151
|
-
O cabeçalho
|
|
152
|
-
|
|
151
|
+
O cabeçalho aceita `DrawerDescription` como subheading opcional para orientar sobre a finalidade.
|
|
152
|
+
Omita quando repetir o título. Condições, regras de negócio e consequências necessárias à decisão
|
|
153
|
+
ficam junto dos controles ou em `DrawerBody`. Uma frase curta também pode ser regra: o critério é se
|
|
154
|
+
pular o texto compromete a decisão, não sua extensão.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
<DrawerHeader>
|
|
158
|
+
<DrawerTitle>Organizar pasta</DrawerTitle>
|
|
159
|
+
<DrawerDescription>Escolha onde a pasta e suas subpastas aparecem.</DrawerDescription>
|
|
160
|
+
</DrawerHeader>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Na composição direta, associe o `id` do texto por `aria-describedby` em `DrawerContent` quando ele
|
|
164
|
+
descrever a superfície para tecnologias assistivas. A regra sobre seleção vazia pertence ao
|
|
165
|
+
campo de áreas e permanece fora do subheading.
|
|
153
166
|
|
|
154
167
|
## Impedir o fechamento durante uma operação
|
|
155
168
|
|
|
@@ -301,3 +314,12 @@ Assim o título acompanha o conteúdo rolável enquanto o chrome permanece está
|
|
|
301
314
|
|
|
302
315
|
No formato compacto, mantenha `DrawerTitle` e `DrawerActions` no próprio `DrawerHeader`. Para
|
|
303
316
|
comparar as duas formas em cada `layout`, abra [Composições](/ui/compositions).
|
|
317
|
+
|
|
318
|
+
## Propriedades de DrawerDescription
|
|
319
|
+
|
|
320
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
321
|
+
| --- | --- | --- | --- |
|
|
322
|
+
| `children` | `ReactNode` | | Orientação opcional sob o título em `DrawerHeader` ou `DrawerIntro`. |
|
|
323
|
+
| `id` | `string` | | Identificador para associação acessível explícita. |
|
|
324
|
+
| `className` | `string` | | Ajusta a apresentação do parágrafo. |
|
|
325
|
+
| `…props` | `ComponentProps<"p">` | | Atributos nativos. |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
## Esqueleto de página
|
|
2
2
|
|
|
3
3
|
Use `Page` para manter cabeçalho, estado e conteúdo na mesma anatomia. `PageHeader` é a única
|
|
4
|
-
introdução estrutural: reúne navegação contextual opcional, título e ações. O container é
|
|
4
|
+
introdução estrutural: reúne navegação contextual opcional, título, orientação opcional e ações. O container é
|
|
5
5
|
centralizado e ocupa a largura disponível até `80rem` (`max-w-7xl`). Use `className` somente quando
|
|
6
6
|
a composição pedir outro teto ou largura total.
|
|
7
7
|
|
|
@@ -41,6 +41,19 @@ render(
|
|
|
41
41
|
);
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
## Orientar sobre a finalidade
|
|
45
|
+
|
|
46
|
+
Use `description` na forma curta ou `PageDescription` em `PageHeader` quando uma frase breve
|
|
47
|
+
ajudar a reconhecer a finalidade da página. Omita texto que só repita o título. Regras de negócio,
|
|
48
|
+
limites e consequências ficam nos campos, ações ou corpo em que são necessários. Pular o
|
|
49
|
+
subheading não deve comprometer a decisão nem a conclusão correta da tarefa.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<Page title="Conhecimento" description="Consulte os materiais de apoio da sua área.">
|
|
53
|
+
<KnowledgeList />
|
|
54
|
+
</Page>
|
|
55
|
+
```
|
|
56
|
+
|
|
44
57
|
## Retorno para a página pai
|
|
45
58
|
|
|
46
59
|
Use `PageBack` em uma subpágina simples. O destino é explícito para continuar correto após refresh ou
|
|
@@ -311,6 +324,7 @@ estado na própria seção com [`DataState`](/ui/data-state), [`ActionView`](/ui
|
|
|
311
324
|
|
|
312
325
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
313
326
|
| ----------- | -------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
327
|
+
| `description` | `ReactNode` | | Orientação opcional sob o título na forma curta. |
|
|
314
328
|
| `title` | `ReactNode` | | O h1 da página. |
|
|
315
329
|
| `actions` | `ReactNode` | | Ações contextuais no extremo oposto do cabeçalho. |
|
|
316
330
|
| `layout` | `'flow' \| 'fill'` | `'flow'` | Ocupação de altura, não anatomia: `fill` limita a página ao shell para que uma região interna role e leva essa altura até uma `ActionList` composta em `PageBody`. |
|
|
@@ -414,3 +428,11 @@ ocupa a altura.
|
|
|
414
428
|
| `retryLabel` | `string` | `'Tentar de novo'` | Texto do botão de recuperação. |
|
|
415
429
|
| `children` | `ReactNode` | | Conteúdo renderizado somente em `ready`. |
|
|
416
430
|
| `className` | `string` | | Classes da superfície de estado. |
|
|
431
|
+
|
|
432
|
+
## Propriedades de PageDescription
|
|
433
|
+
|
|
434
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
435
|
+
| --- | --- | --- | --- |
|
|
436
|
+
| `children` | `ReactNode` | | Orientação breve e opcional; filho direto de `PageHeader`. |
|
|
437
|
+
| `className` | `string` | | Ajusta a apresentação do parágrafo. |
|
|
438
|
+
| `…props` | `HTMLAttributes<HTMLParagraphElement>` | | Atributos nativos. |
|
|
@@ -131,12 +131,11 @@ render(
|
|
|
131
131
|
as="section"
|
|
132
132
|
framed
|
|
133
133
|
title="Plano atual"
|
|
134
|
-
description="Renova em 12 de maio."
|
|
135
134
|
level={3}
|
|
136
135
|
>
|
|
137
136
|
<SurfaceBody>
|
|
138
137
|
<p className="text-sm text-muted-foreground">
|
|
139
|
-
Até 10 workspaces e 50 agentes.
|
|
138
|
+
Renova em 12 de maio. Até 10 workspaces e 50 agentes.
|
|
140
139
|
</p>
|
|
141
140
|
</SurfaceBody>
|
|
142
141
|
<SurfaceFooter>
|
|
@@ -511,3 +510,10 @@ um wrapper filho declara relações locais mais compactas.
|
|
|
511
510
|
| ----------- | ----------- | ------ | ---------------------------------------------------- |
|
|
512
511
|
| `children` | `ReactNode` | | Ações persistentes, alinhadas no fim da linha. |
|
|
513
512
|
| `className` | `string` | | Ajusta o rodapé, inclusive a distribuição das ações. |
|
|
513
|
+
|
|
514
|
+
## Subheading e regras de negócio
|
|
515
|
+
|
|
516
|
+
`description` e `SurfaceDescription` apresentam brevemente a finalidade da região. A orientação
|
|
517
|
+
é opcional e não repete o título. A mesma regra vale para uma página (`level={1}`), uma seção ou
|
|
518
|
+
um card: condições, limites e consequências necessários à decisão ficam no corpo ou junto do
|
|
519
|
+
controle correspondente. Pular o subheading não deve comprometer a tarefa.
|
|
@@ -143,6 +143,21 @@ function syncHosts(): void {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/** A ajuda permanece no Tab, mas não toma o foco inicial do campo ao abrir um modal. */
|
|
147
|
+
export function skipHelpOnAutoFocus(event: Event): void {
|
|
148
|
+
if (event.defaultPrevented || !(event.target instanceof HTMLElement)) return;
|
|
149
|
+
const candidates = tabbables(event.target).filter(
|
|
150
|
+
(element) => element.tagName !== "A",
|
|
151
|
+
);
|
|
152
|
+
if (candidates[0]?.dataset.slot !== "label-help") return;
|
|
153
|
+
const control = candidates.find(
|
|
154
|
+
(element) => element.dataset.slot !== "label-help",
|
|
155
|
+
);
|
|
156
|
+
if (control === undefined) return;
|
|
157
|
+
control.focus();
|
|
158
|
+
if (document.activeElement === control) event.preventDefault();
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
/** Elementos alcançáveis pelo Tab dentro de `root`, na ordem do documento. */
|
|
147
162
|
function tabbables(root: HTMLElement): HTMLElement[] {
|
|
148
163
|
const nodes: HTMLElement[] = [];
|
|
@@ -178,7 +193,10 @@ function tabbables(root: HTMLElement): HTMLElement[] {
|
|
|
178
193
|
function focusFromKeyboard(element: HTMLElement): void {
|
|
179
194
|
element.focus();
|
|
180
195
|
// O Tab do navegador seleciona o texto do campo que recebe o foco; a passagem manual imita.
|
|
181
|
-
if (
|
|
196
|
+
if (
|
|
197
|
+
element instanceof HTMLInputElement &&
|
|
198
|
+
typeof element.select === "function"
|
|
199
|
+
) {
|
|
182
200
|
element.select();
|
|
183
201
|
}
|
|
184
202
|
}
|
|
@@ -342,7 +360,10 @@ export function registerFloatingLayer(
|
|
|
342
360
|
// podem tratar o Tab e marcá-lo com `preventDefault`.
|
|
343
361
|
const onKeyDown = (event: KeyboardEvent): void => {
|
|
344
362
|
if (!isTabKey(event) || event.defaultPrevented) return;
|
|
345
|
-
if (
|
|
363
|
+
if (
|
|
364
|
+
!layerExemptionsActive() ||
|
|
365
|
+
layerOf(document.activeElement) !== record
|
|
366
|
+
) {
|
|
346
367
|
return;
|
|
347
368
|
}
|
|
348
369
|
const candidates = tabbables(host);
|
|
@@ -361,7 +382,10 @@ export function registerFloatingLayer(
|
|
|
361
382
|
if (layerWherePersonIs() === record) escape()?.(event);
|
|
362
383
|
};
|
|
363
384
|
const observer = new MutationObserver(() => {
|
|
364
|
-
if (
|
|
385
|
+
if (
|
|
386
|
+
pointerAnchor !== null &&
|
|
387
|
+
pointerAnchor.parentNode !== pointerLayer?.host
|
|
388
|
+
) {
|
|
365
389
|
// O painel apertado saiu da camada, como quando é fechado por código e vira o FAB.
|
|
366
390
|
pointerAnchor = null;
|
|
367
391
|
pointerLayer = null;
|
package/src/ui/react.tsx
CHANGED
|
@@ -173,6 +173,7 @@ export {
|
|
|
173
173
|
DialogBody,
|
|
174
174
|
DialogFooter,
|
|
175
175
|
DialogTitle,
|
|
176
|
+
DialogDescription,
|
|
176
177
|
} from "./components/primitives/dialog.tsx";
|
|
177
178
|
export type {
|
|
178
179
|
DialogProps,
|
|
@@ -326,6 +327,7 @@ export {
|
|
|
326
327
|
DrawerNavigation,
|
|
327
328
|
DrawerBody,
|
|
328
329
|
DrawerTitle,
|
|
330
|
+
DrawerDescription,
|
|
329
331
|
DrawerTrigger,
|
|
330
332
|
} from "./components/primitives/drawer.tsx";
|
|
331
333
|
export {
|
|
@@ -509,6 +511,7 @@ export {
|
|
|
509
511
|
PageNavigation,
|
|
510
512
|
PageBack,
|
|
511
513
|
PageTitle,
|
|
514
|
+
PageDescription,
|
|
512
515
|
PageActions,
|
|
513
516
|
PageActionsTarget,
|
|
514
517
|
PageBody,
|