@softize/opus 25.7.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 +95 -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/0058-mensagem-da-ia-aceita-imagem-e-o-chat-recebe-anexos.md +201 -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/ai/drivers/anthropic.ts +49 -9
- package/src/ai/index.ts +5 -2
- package/src/core/ai-message.ts +20 -0
- package/src/core/index.ts +5 -0
- package/src/core/types.ts +28 -4
- 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/chat.tsx +179 -22
- package/src/ui/components/primitives/composer.tsx +397 -7
- 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/ai.md +46 -0
- package/src/ui/docs/content/chat.md +97 -2
- package/src/ui/docs/content/composer.md +144 -1
- 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/lib/prepare-image.ts +386 -0
- package/src/ui/meta.ts +2 -2
- package/src/ui/react.tsx +21 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,101 @@ 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
|
+
|
|
31
|
+
## 26.0.0 — 2026-10-09
|
|
32
|
+
|
|
33
|
+
A conversa com a IA pode levar imagens (ADR 0058). Uma mensagem passa a aceitar texto e imagem
|
|
34
|
+
juntos, o driver Anthropic envia as imagens ao modelo, e o `Composer` e o `Chat` recebem imagens
|
|
35
|
+
coladas, soltas ou escolhidas pelo botão **Anexar imagem**. O pacote não guarda imagem: enviar,
|
|
36
|
+
guardar, expirar e mostrar de novo são do aplicativo, que mantém o estado dos anexos.
|
|
37
|
+
|
|
38
|
+
- **Mensagem com imagem.** `AiMessage.content` aceita `string | AiContentPart[]`, em que cada parte é
|
|
39
|
+
`{ type: 'text', text }` ou `{ type: 'image', mediaType, data }`, com PNG, JPEG ou WebP em base64.
|
|
40
|
+
Os tipos `AiContentPart` e `AiImageMediaType` saem de `@softize/opus` e de `@softize/opus/ai`.
|
|
41
|
+
- **`aiMessageText(message)`.** Devolve o texto de uma mensagem: o próprio `content` quando é texto,
|
|
42
|
+
ou as partes de texto unidas por uma linha em branco, sem as imagens. Sai de `@softize/opus` e de
|
|
43
|
+
`@softize/opus/ai`.
|
|
44
|
+
- **Driver Anthropic.** `run` e `runStream` convertem cada parte no bloco da API (`text`, ou `image`
|
|
45
|
+
com `source.type: 'base64'`). Uma parte desconhecida, uma lista de partes vazia, uma parte de texto
|
|
46
|
+
vazia, uma imagem fora dos três formatos ou uma imagem numa mensagem `assistant` fazem a chamada
|
|
47
|
+
lançar erro antes de chegar ao provedor, dizendo qual mensagem e qual parte.
|
|
48
|
+
`complete` e `extract` continuam recebendo texto.
|
|
49
|
+
- **`Composer` com imagens.** A propriedade `attachments` liga o botão **Anexar imagem**, a faixa
|
|
50
|
+
acima do campo (miniatura, nome, estado `uploading`, `ready` ou `failed` e remover), colar e
|
|
51
|
+
soltar. A triagem aceita PNG, JPEG e WebP, até 4 por mensagem (`max` muda o limite, e `accept`,
|
|
52
|
+
tipado como `readonly AiImageMediaType[]`, escolhe um subconjunto dos três, sem curinga), e
|
|
53
|
+
explica a recusa acima do campo, ou a entrega a `onReject`. O envio passa com texto ou com uma
|
|
54
|
+
imagem pronta; espera enquanto alguma imagem está em envio e fica bloqueado com uma imagem em
|
|
55
|
+
falha, até ela ser removida ou reenviada. Uma região viva anuncia a imagem que fica pronta ou
|
|
56
|
+
falha, e o foco volta ao campo quando o botão de anexar fica desabilitado pelo limite. Colar
|
|
57
|
+
texto continua colando texto; a imagem colada sem texto recebe "Imagem colada 1.png", com um
|
|
58
|
+
número que não se repete no mesmo composer. O soltar que o composer consome não sobe para os
|
|
59
|
+
contêineres do aplicativo.
|
|
60
|
+
- **`Chat` com imagens.** No modo controlado, `composerAttachments` liga as imagens no composer e o
|
|
61
|
+
soltar sobre a conversa inteira; `onSend(text, attachments)` recebe as imagens prontas, e a
|
|
62
|
+
mensagem pode ter só imagem. Sem `composerAttachments`, `onSend` continua sendo chamado só com o
|
|
63
|
+
texto. O item `user` do transcript aceita `attachments` (`ChatAttachment`,
|
|
64
|
+
com `id`, `name`, `mediaType`, dimensões, `status: 'available' | 'expired'` e o `url` dado pelo
|
|
65
|
+
aplicativo): a fala mostra as miniaturas acima do texto, com o nome como texto alternativo, e a
|
|
66
|
+
imagem expirada como um cartão com o nome e "Imagem expirada". O modo autogerenciado (`send`) não
|
|
67
|
+
aceita imagem. Com `composerAttachments`, o arraste de arquivos sobre o Chat é consumido nele
|
|
68
|
+
(`stopPropagation` em `dragenter`, `dragover`, `dragleave` e `drop`) e não chega a um contêiner do
|
|
69
|
+
aplicativo com o próprio soltar; sem ele, o arraste segue como antes.
|
|
70
|
+
- **`prepareImage(file, options)`**, de `@softize/opus/ui/react`, prepara a imagem no navegador antes
|
|
71
|
+
do envio: lê só o cabeçalho para conferir o formato pelos bytes e as dimensões, recusa GIF, SVG,
|
|
72
|
+
imagem animada e, antes de decodificar, a imagem acima de `maxPixels` (padrão 40 megapixels);
|
|
73
|
+
reduz o lado maior a `maxEdge` (padrão 1568) respeitando a orientação EXIF
|
|
74
|
+
(`imageOrientation: 'from-image'`) e codifica de novo, descartando metadados. PNG continua PNG quando cabe em
|
|
75
|
+
`maxBytes` (padrão 3 MB); o resto vira JPEG com qualidade 0,85. Devolve o arquivo com tipo,
|
|
76
|
+
dimensões e tamanho, ou a recusa com o motivo e a mensagem em pt-BR.
|
|
77
|
+
|
|
78
|
+
Sem as propriedades novas, o `Composer` e o `Chat` se comportam como antes.
|
|
79
|
+
|
|
80
|
+
### Breaking
|
|
81
|
+
|
|
82
|
+
**`AiMessage.content` passa a ser `string | AiContentPart[]`.** Código que lê `content` como texto
|
|
83
|
+
deixa de compilar: `message.content.length`, `message.content.trim()`, uma função que recebe
|
|
84
|
+
`string`, um driver de IA próprio que repassa `content` ao provedor. Migração: leia o texto com
|
|
85
|
+
`aiMessageText(message)` e, num driver próprio, converta cada parte para o formato do provedor e
|
|
86
|
+
recuse com erro a parte que ele não conhece.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { aiMessageText } from '@softize/opus/ai'
|
|
90
|
+
|
|
91
|
+
const title = aiMessageText(message).slice(0, 80) // antes: message.content.slice(0, 80)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
O compilador não aponta o texto montado por interpolação (`${message.content}`) nem o que passa por
|
|
95
|
+
`as`: com uma mensagem com imagem, esses pontos produzem "[object Object]" sem erro. Procure-os à
|
|
96
|
+
parte (`grep -rn '\${[^}]*content' src`) e troque por `aiMessageText`. Um `ChatMessage` do `Chat`
|
|
97
|
+
autogerenciado continua com `content: string` e segue atribuível a `AiMessage`.
|
|
98
|
+
|
|
99
|
+
**`Chat.onSend` ganha o segundo parâmetro opcional `attachments?: ComposerAttachment[]`.** Em tempo de
|
|
100
|
+
execução nada muda sem `composerAttachments`: a chamada continua `onSend(text)`. Um handler que já
|
|
101
|
+
declarava um segundo parâmetro opcional de outro tipo, como `(text, options?) => …`, deixa de
|
|
102
|
+
compilar ao ser passado direto. Migração: envolva-o (`onSend={(text) => runTurn(text)}`) ou, ao ligar
|
|
103
|
+
as imagens, receba `attachments` explicitamente.
|
|
104
|
+
|
|
10
105
|
## 25.7.0 — 2026-10-07
|
|
11
106
|
|
|
12
107
|
Portais de documentação podem manter o mesmo mapa lateral em guias, ADRs e changelog. A superfície
|
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,201 @@
|
|
|
1
|
+
# ADR 0058 — A mensagem da IA aceita imagem, e o Chat recebe anexos que o app guarda
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-09.
|
|
5
|
+
- **Complementa:** a regra de crescimento por reincidência do `AiAdapter` (`core/types.ts`, nota do
|
|
6
|
+
contrato) e da entrada `@softize/opus/ai`; ADR 0050 (o `Markdown` não carrega imagem externa).
|
|
7
|
+
- **Origem:** ADR 0122 da plataforma Grand Brasil, "A imagem anexada à conversa com a IA fica
|
|
8
|
+
guardada com a conversa", que pede ao pacote o corte 1 do plano dela.
|
|
9
|
+
|
|
10
|
+
## Contexto
|
|
11
|
+
|
|
12
|
+
A plataforma Grand Brasil decidiu que a pessoa pode colar, soltar ou escolher uma imagem na conversa
|
|
13
|
+
com a IA, e que a IA a vê no mesmo turno. O caso que motivou a decisão é pedir ao assistente de
|
|
14
|
+
Relatórios que reproduza um relatório a partir de um print. É o primeiro caso real de entrada que não
|
|
15
|
+
é texto, e a superfície de IA do pacote cresce por reincidência: este é o caso que a cobra.
|
|
16
|
+
|
|
17
|
+
Hoje o pacote só conhece texto em três pontos:
|
|
18
|
+
|
|
19
|
+
- **O tipo da mensagem.** `AiMessage` é `{ role, content: string }`. `run` e `runStream` recebem
|
|
20
|
+
`string | AiMessage[]`.
|
|
21
|
+
- **O driver Anthropic.** Ele monta as mensagens com `content: m.content`, sem conferir a forma. Uma
|
|
22
|
+
lista de blocos no formato da Anthropic passaria por acaso, forçada com um `as`, e qualquer outro
|
|
23
|
+
provedor do consumidor receberia uma forma que não conhece.
|
|
24
|
+
- **A interface.** O `Composer` só habilita o envio com texto, e o `Chat` guarda `content: string` em
|
|
25
|
+
cada item. O modo controlado recebe o texto em `onSend(text)`.
|
|
26
|
+
|
|
27
|
+
A guarda da imagem não é do pacote. Na Grand Brasil, o motor de IA guarda os bytes junto com a
|
|
28
|
+
conversa, com retenção, governança e uma rota própria de envio. Outro consumidor pode guardar em
|
|
29
|
+
outro lugar, ou não guardar. O que se repete em qualquer consumidor é a forma de levar a imagem ao
|
|
30
|
+
modelo, a caixa de escrever com anexos e a preparação da imagem no navegador.
|
|
31
|
+
|
|
32
|
+
## Decisão
|
|
33
|
+
|
|
34
|
+
### 1. Conteúdo neutro na mensagem
|
|
35
|
+
|
|
36
|
+
O núcleo ganha um tipo de conteúdo que não tem a forma de nenhum provedor:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
type AiImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp'
|
|
40
|
+
|
|
41
|
+
type AiContentPart =
|
|
42
|
+
| { type: 'text'; text: string }
|
|
43
|
+
| { type: 'image'; mediaType: AiImageMediaType; data: string } // base64, sem o prefixo data:
|
|
44
|
+
|
|
45
|
+
interface AiMessage {
|
|
46
|
+
role: 'user' | 'assistant'
|
|
47
|
+
content: string | AiContentPart[]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- Os formatos são PNG, JPEG e WebP, os três que os provedores conhecidos aceitam. GIF fica de fora.
|
|
52
|
+
- A parte de imagem não leva nome. O rótulo que a pessoa e o modelo usam ("Imagem 1: print.png") é
|
|
53
|
+
uma parte de texto que o consumidor monta antes da imagem, como a Grand Brasil faz.
|
|
54
|
+
- `content: string` continua válido. Quem só manda texto não muda nada na escrita.
|
|
55
|
+
|
|
56
|
+
### 2. `aiMessageText` lê o texto de qualquer mensagem
|
|
57
|
+
|
|
58
|
+
`aiMessageText(message)` devolve o texto de uma mensagem: o próprio `content` quando ele é texto, ou
|
|
59
|
+
as partes de texto unidas por uma linha em branco. As imagens não entram. Serve para quem precisa só
|
|
60
|
+
do texto, como título de conversa, contagem de caracteres ou um bloco de contexto montado por
|
|
61
|
+
interpolação. Sai de `@softize/opus` e de `@softize/opus/ai`.
|
|
62
|
+
|
|
63
|
+
### 3. O driver Anthropic converte cada parte
|
|
64
|
+
|
|
65
|
+
- A parte `text` vira o bloco `text`, e a parte `image` vira o bloco `image` com
|
|
66
|
+
`source: { type: 'base64', media_type, data }`, em `run` e em `runStream`.
|
|
67
|
+
- Uma parte de tipo desconhecido, uma lista de partes vazia, uma parte de texto vazia, uma imagem num
|
|
68
|
+
formato fora da lista ou uma imagem numa mensagem `assistant` fazem o driver lançar erro antes de
|
|
69
|
+
qualquer chamada, dizendo qual mensagem e qual parte. Nunca há descarte silencioso.
|
|
70
|
+
- `complete` e `extract` continuam recebendo texto.
|
|
71
|
+
|
|
72
|
+
### 4. A guarda é do consumidor
|
|
73
|
+
|
|
74
|
+
O pacote não guarda imagem, não envia bytes a um servidor e não gera endereço. O `StorageAdapter`
|
|
75
|
+
existente não entra nesta decisão. O consumidor escolhe o transporte (a Grand Brasil manda a imagem
|
|
76
|
+
por uma rota própria, antes do turno) e entrega ao `Chat` o estado dos anexos e o endereço de cada
|
|
77
|
+
imagem. Assim, retenção, expiração, governança e dado pessoal ficam com quem guarda.
|
|
78
|
+
|
|
79
|
+
### 5. Composer e Chat recebem anexos controlados pelo app
|
|
80
|
+
|
|
81
|
+
- O `Composer` ganha a propriedade `attachments`, e o `Chat`, a `composerAttachments`, com a mesma
|
|
82
|
+
forma: os anexos pendentes (`id`, nome, estado `uploading`, `ready` ou `failed`, miniatura e motivo
|
|
83
|
+
da falha), `onAddFiles`, `onRemove`, o limite por mensagem (padrão 4), os formatos aceitos
|
|
84
|
+
(`readonly AiImageMediaType[]`, padrão PNG, JPEG e WebP, sem curinga: um formato que a mensagem
|
|
85
|
+
não leva não pode ser aceito na faixa) e `onReject` opcional. Sem essa propriedade, não há faixa, botão, colar nem soltar
|
|
86
|
+
de arquivos: o composer é o de sempre.
|
|
87
|
+
- Com a propriedade, o composer mostra a faixa de anexos acima do texto, com miniatura, nome, estado
|
|
88
|
+
e o botão de remover, e o botão **Anexar imagem** na barra. Colar uma imagem no campo, soltá-la
|
|
89
|
+
sobre a conversa ou escolhê-la pelo botão entregam os arquivos aceitos a `onAddFiles`. A imagem
|
|
90
|
+
colada recebe um nome gerado ("Imagem colada 1.png"), com um contador da instância que não volta
|
|
91
|
+
atrás, para que dois anexos da mesma conversa não tenham o mesmo nome. Colar texto continua colando texto, mesmo
|
|
92
|
+
quando a área de transferência também traz uma imagem, como acontece ao copiar células de uma
|
|
93
|
+
planilha.
|
|
94
|
+
- A triagem recusa o que não está na lista de tipos e o que passaria do limite. Sem `onReject`, o
|
|
95
|
+
composer explica a recusa logo acima do campo; com `onReject`, a explicação é do app.
|
|
96
|
+
- Uma região viva anuncia a imagem que fica pronta ou falha, porque a mudança na faixa é só visual.
|
|
97
|
+
Quando o botão de anexar fica desabilitado por atingir o limite, o foco volta ao campo.
|
|
98
|
+
- O arraste de arquivos que o `Chat` (ou o `Composer` sozinho) consome não sobe para os contêineres
|
|
99
|
+
do app, que podem ter o próprio soltar, como o envio de arquivos a um acervo.
|
|
100
|
+
- O envio passa com texto ou com pelo menos um anexo pronto. Ele espera os anexos em envio e não
|
|
101
|
+
passa com um anexo em falha: a pessoa remove o anexo, ou o app tenta de novo, para que nada do que
|
|
102
|
+
está na faixa fique de fora sem aviso.
|
|
103
|
+
- O modo controlado recebe `onSend(text, attachments)`, com os anexos prontos, quando há
|
|
104
|
+
`composerAttachments`. Sem eles, a chamada continua `onSend(text)`: um handler que já tinha um
|
|
105
|
+
segundo parâmetro opcional, com outro significado, não recebe uma lista inesperada. Limpar a faixa
|
|
106
|
+
depois do envio é do app, que é o dono do estado. Retornar `false` devolve o texto ao campo, e os
|
|
107
|
+
anexos continuam na faixa.
|
|
108
|
+
- O item `user` do transcript ganha `attachments`, com `id`, nome, tipo, dimensões, estado
|
|
109
|
+
(`available` ou `expired`) e o endereço da imagem fornecido pelo app. A fala da pessoa mostra as
|
|
110
|
+
miniaturas acima do texto, com o nome como texto alternativo. O anexo expirado aparece como um
|
|
111
|
+
cartão com o nome e "Imagem expirada". Uma fala só com imagem não mostra balão de texto vazio.
|
|
112
|
+
- As miniaturas são um componente próprio, e não `![]()` no Markdown da mensagem: a fala da pessoa
|
|
113
|
+
continua sendo o que ela escreveu. O endereço vem do app, que deve servi-lo pela própria origem ou
|
|
114
|
+
como `data:`, como a ADR 0050 já recomenda.
|
|
115
|
+
- O modo autogerenciado do `Chat` (`send(messages)`) não ganha anexo. Ele não tem transporte para a
|
|
116
|
+
imagem, e `ChatMessage` continua só com texto. Se um consumidor precisar, uma nova decisão define
|
|
117
|
+
como.
|
|
118
|
+
|
|
119
|
+
### 6. Preparação da imagem no navegador
|
|
120
|
+
|
|
121
|
+
`prepareImage(file, options)` prepara a imagem antes do envio, com os tetos que o app passa:
|
|
122
|
+
|
|
123
|
+
- lê só o cabeçalho do arquivo, sem carregá-lo inteiro, para conferir o formato pelos primeiros
|
|
124
|
+
bytes, não pela extensão nem pelo tipo declarado, e recusa o que não está em `accept` (padrão PNG,
|
|
125
|
+
JPEG e WebP);
|
|
126
|
+
- recusa PNG e WebP animados, que o modelo leria só pelo primeiro quadro;
|
|
127
|
+
- lê largura e altura do cabeçalho e recusa, antes de decodificar, a imagem acima de `maxPixels`
|
|
128
|
+
(padrão 40 megapixels): decodificá-la ocuparia centenas de megabytes de memória do navegador;
|
|
129
|
+
- reduz para que o lado maior não passe de `maxEdge` (padrão 1568 px), mantendo a proporção e a
|
|
130
|
+
orientação gravada pela câmera (EXIF), e codifica de novo sempre, o que descarta metadados como a
|
|
131
|
+
localização;
|
|
132
|
+
- mantém PNG como PNG quando cabe em `maxBytes` (padrão 3 MB) e converte o resto para JPEG com
|
|
133
|
+
qualidade 0,85, sobre fundo branco; o que continua acima do teto é recusado;
|
|
134
|
+
- devolve o arquivo preparado com tipo, dimensões e tamanho, ou a recusa com o motivo e uma mensagem
|
|
135
|
+
em pt-BR para a pessoa.
|
|
136
|
+
|
|
137
|
+
Os padrões repetem os tetos da ADR 0122 da Grand Brasil, mas o consumidor deve passar os seus: o
|
|
138
|
+
utilitário não decide teto de produto.
|
|
139
|
+
|
|
140
|
+
## Por que é major
|
|
141
|
+
|
|
142
|
+
`AiMessage.content` passa de `string` para `string | AiContentPart[]`. Quem lê `content` como texto
|
|
143
|
+
deixa de compilar, e é de propósito: o compilador aponta os consumidores e os drivers que usam o
|
|
144
|
+
conteúdo como texto, em vez de deixar a imagem sumir em silêncio. A migração é ler o texto com
|
|
145
|
+
`aiMessageText(message)`.
|
|
146
|
+
|
|
147
|
+
O compilador não aponta o texto montado por interpolação (`${message.content}`) nem o que passa por um
|
|
148
|
+
`as`. Com uma lista de partes, esses pontos produziriam "[object Object]" sem erro. Dentro do pacote
|
|
149
|
+
não há nenhum; nos consumidores, a migração precisa procurá-los à parte e cobri-los com teste.
|
|
150
|
+
|
|
151
|
+
As mudanças de `Composer` e `Chat` preservam o comportamento de quem não usa anexo: sem as
|
|
152
|
+
propriedades novas, a interface é a mesma, e `onSend` continua sendo chamado só com o texto. O tipo de
|
|
153
|
+
`onSend` ganha o segundo parâmetro opcional; um handler que já declarava um segundo parâmetro
|
|
154
|
+
opcional de outro tipo deixa de compilar e precisa ser envolvido, como `(text) => run(text)`.
|
|
155
|
+
|
|
156
|
+
## Consequências
|
|
157
|
+
|
|
158
|
+
- Um driver de IA do consumidor que repassava `content` sem conferir passa a receber partes neutras
|
|
159
|
+
quando o consumidor manda imagem. Cada driver converte as partes, ou recusa com erro claro.
|
|
160
|
+
- O pacote cresce com a faixa de anexos e a preparação da imagem, usadas por um consumidor só por
|
|
161
|
+
enquanto. Ficam no pacote porque o `Chat` e o `Composer` são dele, e cada consumidor reconstruiria
|
|
162
|
+
a sua por cima de `composerActions`.
|
|
163
|
+
- A redução no navegador limita tamanho e custo, mas não garante que o provedor não reduza de novo:
|
|
164
|
+
no nível padrão do Claude, uma imagem retangular de 1568 px ainda passa por redução na API.
|
|
165
|
+
- O tipo e o estado de cada anexo continuam como texto no pacote. A plataforma que adota declara o
|
|
166
|
+
próprio vocabulário (na Grand Brasil, um `t.dict` no protocolo da IA).
|
|
167
|
+
|
|
168
|
+
## Alternativas descartadas
|
|
169
|
+
|
|
170
|
+
- **Campo opcional só para imagens em `AiMessage`.** Não quebraria ninguém, mas um driver que não o
|
|
171
|
+
conhece descartaria a imagem sem aviso, e o compilador não apontaria nada.
|
|
172
|
+
- **Partes no formato da Anthropic.** Funcionaria só com esse driver. Os demais provedores, que a
|
|
173
|
+
Grand Brasil também usa, receberiam uma forma que não conhecem.
|
|
174
|
+
- **Nome na parte de imagem.** A API da Anthropic não tem onde pôr o nome, e o rótulo em texto antes
|
|
175
|
+
da imagem já permite que a pessoa e o modelo falem de "imagem 2".
|
|
176
|
+
- **O `Composer` envia e guarda a imagem.** Prenderia o pacote a um transporte e a uma guarda que
|
|
177
|
+
são do consumidor, com retenção e governança próprias.
|
|
178
|
+
- **Anexo no modo autogerenciado.** Exigiria levar os bytes no histórico enviado a `send`, que é o
|
|
179
|
+
que a Grand Brasil recusou para o corpo do chat. Fica para quando um consumidor trouxer o caso.
|
|
180
|
+
- **Aceitar a imagem animada e mandar só o primeiro quadro.** A pessoa acharia que o modelo viu a
|
|
181
|
+
animação; recusar com o motivo é mais honesto.
|
|
182
|
+
|
|
183
|
+
## Verificação
|
|
184
|
+
|
|
185
|
+
- Teste do driver: uma mensagem com texto e imagem chega ao client falso como bloco `text` e bloco
|
|
186
|
+
`image` com `source.type: 'base64'`, no `run` e no `runStream`; uma parte desconhecida, uma lista
|
|
187
|
+
vazia, uma parte de texto vazia, uma imagem num formato fora da lista e uma imagem em mensagem
|
|
188
|
+
`assistant` lançam erro sem chamar o client.
|
|
189
|
+
- Teste de `aiMessageText` com texto puro e com partes.
|
|
190
|
+
- Testes do `Composer` e do `Chat`: colar, soltar e escolher entregam o arquivo a `onAddFiles`;
|
|
191
|
+
colar texto continua colando; o envio passa só com imagem pronta e não passa com imagem em envio
|
|
192
|
+
ou em falha; tipo e limite são recusados com o motivo; o `Chat` mostra as miniaturas e o cartão de
|
|
193
|
+
imagem expirada, entrega `onSend(text, attachments)` e, sem `composerAttachments` ou no modo
|
|
194
|
+
autogerenciado, não mostra a faixa; o nome da imagem colada não se repete depois de remover uma;
|
|
195
|
+
a região viva anuncia pronta e falha; o foco volta ao campo no limite; o soltar consumido não
|
|
196
|
+
chega ao contêiner do app.
|
|
197
|
+
- Testes de `prepareImage` com canvas simulado e em navegador real: uma imagem de 4000 px sai com
|
|
198
|
+
lado maior de 1568 px, o PNG que cabe continua PNG, o que passa vira JPEG, e GIF, SVG e imagem
|
|
199
|
+
animada são recusados com o motivo; as dimensões vêm do cabeçalho de PNG, JPEG e WebP (com e sem
|
|
200
|
+
perdas e estendido), sem ler o arquivo inteiro, e a imagem acima do teto de pixels é recusada sem
|
|
201
|
+
decodificar; um JPEG com EXIF de rotação sai em pé.
|
|
@@ -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.
|