@softize/opus 29.0.0 → 30.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/bin/lib/check.mjs +86 -10
- package/docs/adr/0045-surface-replaces-content-and-card.md +3 -0
- package/docs/adr/0062-listas-contidas-e-texto-flexivel.md +68 -0
- package/docs/adr/0063-metadados-das-superficies.md +44 -0
- package/docs/adr/README.md +4 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +1 -1
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +10 -2
- package/src/core/types.ts +1 -1
- package/src/ui/components/patterns/list.tsx +18 -9
- package/src/ui/components/patterns/page.tsx +27 -0
- package/src/ui/components/patterns/structural-header.tsx +25 -3
- package/src/ui/components/patterns/surface.tsx +36 -8
- package/src/ui/components/primitives/dialog.tsx +74 -42
- package/src/ui/components/primitives/drawer.tsx +69 -42
- package/src/ui/components/primitives/table.tsx +3 -3
- package/src/ui/docs/content/action-list.md +14 -7
- package/src/ui/docs/content/dialog.md +39 -0
- package/src/ui/docs/content/drawer.md +36 -0
- package/src/ui/docs/content/page.md +30 -1
- package/src/ui/docs/content/surface.md +41 -8
- package/src/ui/docs/content/table.md +7 -0
- package/src/ui/react.tsx +4 -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
|
+
## 30.0.0 — 2026-10-16
|
|
11
|
+
|
|
12
|
+
PageMeta, DrawerMeta, DialogMeta e SurfaceMeta organizam badges, datas e autoria em
|
|
13
|
+
faixa própria sob o título e a descrição. Page e Surface também aceitam a prop `meta`.
|
|
14
|
+
|
|
15
|
+
### Breaking
|
|
16
|
+
|
|
17
|
+
**Migração:** SurfaceMeta deixa de ocupar a linha do título. Contadores explícitos usam
|
|
18
|
+
SurfaceCount; a prop `count` conserva o comportamento anterior. Metadados contextuais
|
|
19
|
+
existentes permanecem em SurfaceMeta e passam para a faixa inferior. Revise manualmente
|
|
20
|
+
os usos existentes de SurfaceMeta: os gates não distinguem um contador de metadados.
|
|
21
|
+
Descrições textuais usam SurfaceDescription. DialogMeta não se aplica ao modo de alerta.
|
|
22
|
+
|
|
23
|
+
Tabelas quebram texto entre palavras, preservando palavras curtas inteiras, e ActionList
|
|
24
|
+
mantém a rolagem dentro dos resultados,
|
|
25
|
+
sem alargar a superfície. Filtros inline podem ocupar mais de uma linha. Colunas `fit`
|
|
26
|
+
preservam os valores sem quebra; a distribuição automática considera também os cabeçalhos,
|
|
27
|
+
que podem quebrar quando falta espaço. Colunas de dados não recebem largura mínima forçada;
|
|
28
|
+
seleção, ações de linha e células contendo apenas um switch permanecem compactas. Em tabelas
|
|
29
|
+
compostas diretamente, use `whitespace-nowrap` para valores que devem permanecer inteiros.
|
|
30
|
+
|
|
10
31
|
## 29.0.0 — 2026-10-15
|
|
11
32
|
|
|
12
33
|
O `CLAUDE.md` passa a importar o `AGENTS.md`, e as declarações de copy passam a ser ancoradas pelo
|
package/bin/lib/check.mjs
CHANGED
|
@@ -159,6 +159,7 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
159
159
|
["PageFooter", new Set(["Page"])],
|
|
160
160
|
["PageTitle", new Set(["PageHeader", "PageIntro"])],
|
|
161
161
|
["PageDescription", new Set(["PageHeader", "PageIntro"])],
|
|
162
|
+
["PageMeta", new Set(["PageHeader", "PageIntro"])],
|
|
162
163
|
["PageBack", new Set(["PageHeader"])],
|
|
163
164
|
["PageNavigation", new Set(["PageHeader"])],
|
|
164
165
|
["PageActions", new Set(["PageHeader"])],
|
|
@@ -171,6 +172,7 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
171
172
|
["SurfaceTitle", new Set(["SurfaceHeader"])],
|
|
172
173
|
["SurfaceDescription", new Set(["SurfaceHeader"])],
|
|
173
174
|
["SurfaceMeta", new Set(["SurfaceHeader"])],
|
|
175
|
+
["SurfaceCount", new Set(["SurfaceHeader"])],
|
|
174
176
|
["SurfaceActions", new Set(["SurfaceHeader"])],
|
|
175
177
|
["DialogHeader", new Set(["DialogContent"])],
|
|
176
178
|
["DialogBody", new Set(["DialogContent"])],
|
|
@@ -181,6 +183,7 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
181
183
|
["DialogMedia", new Set(["DialogHeader"])],
|
|
182
184
|
["DialogTitle", new Set(["DialogHeader", "DialogIntro"])],
|
|
183
185
|
["DialogDescription", new Set(["DialogHeader", "DialogIntro"])],
|
|
186
|
+
["DialogMeta", new Set(["DialogHeader", "DialogIntro"])],
|
|
184
187
|
["DialogActions", new Set(["DialogHeader"])],
|
|
185
188
|
["DrawerHeader", new Set(["DrawerContent"])],
|
|
186
189
|
["DrawerBody", new Set(["DrawerContent"])],
|
|
@@ -191,6 +194,7 @@ const UI_STRUCTURAL_PARENTS = new Map([
|
|
|
191
194
|
["DrawerMedia", new Set(["DrawerHeader"])],
|
|
192
195
|
["DrawerTitle", new Set(["DrawerHeader", "DrawerIntro"])],
|
|
193
196
|
["DrawerDescription", new Set(["DrawerHeader", "DrawerIntro"])],
|
|
197
|
+
["DrawerMeta", new Set(["DrawerHeader", "DrawerIntro"])],
|
|
194
198
|
["DrawerActions", new Set(["DrawerHeader"])],
|
|
195
199
|
["AlertMedia", new Set(["Alert"])],
|
|
196
200
|
["AlertHeader", new Set(["Alert"])],
|
|
@@ -224,7 +228,7 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
224
228
|
body: "PageBody",
|
|
225
229
|
footer: "PageFooter",
|
|
226
230
|
headerRequired: false,
|
|
227
|
-
shorthand: new Set(["title", "description", "actions"]),
|
|
231
|
+
shorthand: new Set(["title", "description", "meta", "actions"]),
|
|
228
232
|
// O contador saiu do título da página (ADR 0009). Tirar `count` do shorthand não proíbe
|
|
229
233
|
// nada sozinho — só apaga o marcador que fazia o gate enxergar aquele Page —, então a
|
|
230
234
|
// prop removida é cobrada aqui, nas duas formas de composição.
|
|
@@ -242,7 +246,14 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
242
246
|
// Cada região é opcional, a forma curta convive com SurfaceBody e SurfaceFooter, e sem
|
|
243
247
|
// região nenhuma a Surface é uma caixa de filhos livres (ADR 0045).
|
|
244
248
|
flexible: true,
|
|
245
|
-
shorthand: new Set([
|
|
249
|
+
shorthand: new Set([
|
|
250
|
+
"title",
|
|
251
|
+
"media",
|
|
252
|
+
"description",
|
|
253
|
+
"meta",
|
|
254
|
+
"count",
|
|
255
|
+
"actions",
|
|
256
|
+
]),
|
|
246
257
|
},
|
|
247
258
|
],
|
|
248
259
|
]);
|
|
@@ -260,6 +271,10 @@ const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
|
260
271
|
"SurfaceTitle",
|
|
261
272
|
"SurfaceDescription",
|
|
262
273
|
"SurfaceMeta",
|
|
274
|
+
"SurfaceCount",
|
|
275
|
+
"PageMeta",
|
|
276
|
+
"DrawerMeta",
|
|
277
|
+
"DialogMeta",
|
|
263
278
|
"SurfaceActions",
|
|
264
279
|
"AlertMedia",
|
|
265
280
|
"AlertHeader",
|
|
@@ -289,10 +304,6 @@ const UI_REMOVED_PROPS = new Map([
|
|
|
289
304
|
["layout", "use `Surface` para anatomia visual contínua ou seccionada"],
|
|
290
305
|
]),
|
|
291
306
|
],
|
|
292
|
-
[
|
|
293
|
-
"Surface",
|
|
294
|
-
new Map([["meta", "use `count` para o total ao lado do título"]]),
|
|
295
|
-
],
|
|
296
307
|
[
|
|
297
308
|
"DialogMedia",
|
|
298
309
|
new Map([
|
|
@@ -340,7 +351,7 @@ const UI_REMOVED_COMPONENTS = new Map([
|
|
|
340
351
|
],
|
|
341
352
|
["ContentTitle", "use `SurfaceTitle`"],
|
|
342
353
|
["ContentDescription", "use `SurfaceDescription`"],
|
|
343
|
-
["ContentMeta", "use `
|
|
354
|
+
["ContentMeta", "use `SurfaceCount`"],
|
|
344
355
|
["ContentActions", "use `SurfaceActions`"],
|
|
345
356
|
["ContentBody", "use `SurfaceBody`"],
|
|
346
357
|
["Card", 'use `Surface framed elevation="raised"`'],
|
|
@@ -364,7 +375,7 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
364
375
|
"PageIntro",
|
|
365
376
|
{
|
|
366
377
|
title: "PageTitle",
|
|
367
|
-
optional: new Set(["PageIntroActions", "PageDescription"]),
|
|
378
|
+
optional: new Set(["PageIntroActions", "PageDescription", "PageMeta"]),
|
|
368
379
|
shorthand: new Set(),
|
|
369
380
|
},
|
|
370
381
|
],
|
|
@@ -372,7 +383,11 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
372
383
|
"DialogIntro",
|
|
373
384
|
{
|
|
374
385
|
title: "DialogTitle",
|
|
375
|
-
optional: new Set([
|
|
386
|
+
optional: new Set([
|
|
387
|
+
"DialogIntroActions",
|
|
388
|
+
"DialogDescription",
|
|
389
|
+
"DialogMeta",
|
|
390
|
+
]),
|
|
376
391
|
shorthand: new Set(),
|
|
377
392
|
},
|
|
378
393
|
],
|
|
@@ -380,7 +395,11 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
380
395
|
"DrawerIntro",
|
|
381
396
|
{
|
|
382
397
|
title: "DrawerTitle",
|
|
383
|
-
optional: new Set([
|
|
398
|
+
optional: new Set([
|
|
399
|
+
"DrawerIntroActions",
|
|
400
|
+
"DrawerDescription",
|
|
401
|
+
"DrawerMeta",
|
|
402
|
+
]),
|
|
384
403
|
shorthand: new Set(),
|
|
385
404
|
},
|
|
386
405
|
],
|
|
@@ -393,6 +412,7 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
393
412
|
"PageNavigation",
|
|
394
413
|
"PageActions",
|
|
395
414
|
"PageDescription",
|
|
415
|
+
"PageMeta",
|
|
396
416
|
]),
|
|
397
417
|
exclusive: [new Set(["PageBack", "PageNavigation"])],
|
|
398
418
|
shorthand: new Set(),
|
|
@@ -407,6 +427,7 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
407
427
|
"SurfaceMedia",
|
|
408
428
|
"SurfaceDescription",
|
|
409
429
|
"SurfaceMeta",
|
|
430
|
+
"SurfaceCount",
|
|
410
431
|
"SurfaceActions",
|
|
411
432
|
]),
|
|
412
433
|
shorthand: new Set(),
|
|
@@ -2116,6 +2137,30 @@ export function checkUiStructure(file, text) {
|
|
|
2116
2137
|
"ui-structure-removed-component",
|
|
2117
2138
|
);
|
|
2118
2139
|
}
|
|
2140
|
+
if (component === "DialogMeta") {
|
|
2141
|
+
let ancestor = node.parent;
|
|
2142
|
+
while (ancestor !== undefined) {
|
|
2143
|
+
if (
|
|
2144
|
+
ts.isJsxElement(ancestor) &&
|
|
2145
|
+
imports.get(jsxLocalName(ancestor)) === "Dialog"
|
|
2146
|
+
) {
|
|
2147
|
+
const modes = jsxStringAttributeValues(
|
|
2148
|
+
jsxOpening(ancestor),
|
|
2149
|
+
"mode",
|
|
2150
|
+
);
|
|
2151
|
+
if (modes.length === 1 && modes[0] === "alert") {
|
|
2152
|
+
add(
|
|
2153
|
+
node,
|
|
2154
|
+
component,
|
|
2155
|
+
'DialogMeta não está disponível em Dialog com mode="alert"; coloque as informações no corpo do alerta.',
|
|
2156
|
+
"ui-structure-meta",
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
break;
|
|
2160
|
+
}
|
|
2161
|
+
ancestor = ancestor.parent;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2119
2164
|
const parents =
|
|
2120
2165
|
component === null ? undefined : UI_STRUCTURAL_PARENTS.get(component);
|
|
2121
2166
|
if (parents !== undefined) {
|
|
@@ -2234,6 +2279,37 @@ export function checkUiStructure(file, text) {
|
|
|
2234
2279
|
}
|
|
2235
2280
|
}
|
|
2236
2281
|
|
|
2282
|
+
if (
|
|
2283
|
+
[
|
|
2284
|
+
"PageHeader",
|
|
2285
|
+
"PageIntro",
|
|
2286
|
+
"DialogHeader",
|
|
2287
|
+
"DialogIntro",
|
|
2288
|
+
"DrawerHeader",
|
|
2289
|
+
"DrawerIntro",
|
|
2290
|
+
"SurfaceHeader",
|
|
2291
|
+
].includes(component)
|
|
2292
|
+
) {
|
|
2293
|
+
const family = component.replace(/(?:Header|Intro)$/, "");
|
|
2294
|
+
const children = directStructuralChildren(node, imports);
|
|
2295
|
+
const metas = children.names.filter(
|
|
2296
|
+
(name) => name === `${family}Meta`,
|
|
2297
|
+
).length;
|
|
2298
|
+
if (
|
|
2299
|
+
metas > 1 ||
|
|
2300
|
+
(metas > 0 &&
|
|
2301
|
+
!children.names.includes(`${family}Title`) &&
|
|
2302
|
+
!children.hasOpaque)
|
|
2303
|
+
) {
|
|
2304
|
+
add(
|
|
2305
|
+
node,
|
|
2306
|
+
component,
|
|
2307
|
+
`${component} aceita um único ${family}Meta e exige ${family}Title para exibi-lo.`,
|
|
2308
|
+
"ui-structure-meta",
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2237
2313
|
const header =
|
|
2238
2314
|
component === null ? undefined : UI_STRUCTURAL_HEADERS.get(component);
|
|
2239
2315
|
if (header !== undefined) {
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
`Surface`.
|
|
18
18
|
- **Não altera:** `Page`, `Dialog`, `Drawer` e `Pane`, que mantêm famílias, slots e mídia próprios.
|
|
19
19
|
|
|
20
|
+
A ADR 0063 substitui parcialmente esta decisão: `SurfaceMeta` passa a representar a faixa
|
|
21
|
+
de metadados; o contador na linha do título passa a usar `SurfaceCount`.
|
|
22
|
+
|
|
20
23
|
## Contexto
|
|
21
24
|
|
|
22
25
|
Quem monta uma tela precisa delimitar e nomear um trecho dela: uma seção da página, um grupo de
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ADR 0062 — Listas contidas e texto flexível
|
|
2
|
+
|
|
3
|
+
Data: 2026-10-15
|
|
4
|
+
Status: Aceita
|
|
5
|
+
|
|
6
|
+
## Contexto
|
|
7
|
+
|
|
8
|
+
Descrições sem quebra faziam uma tabela de Habilidades ocupar 92,21rem dentro de
|
|
9
|
+
63,13rem disponíveis. A coluna implícita do grid de resultados do ActionList
|
|
10
|
+
crescia pelo conteúdo; o contêiner da tabela crescia junto e não oferecia sua
|
|
11
|
+
rolagem horizontal. A largura mínima da página não limita a largura dos filhos.
|
|
12
|
+
|
|
13
|
+
## Decisão
|
|
14
|
+
|
|
15
|
+
Table contém sua largura e quebra texto entre palavras por padrão. `overflow-wrap:
|
|
16
|
+
break-word` preserva a largura intrínseca das palavras, evitando que uma descrição
|
|
17
|
+
comprida faça uma coluna como “Inativo” quebrar no meio. Identificadores sem espaços
|
|
18
|
+
podem exigir rolagem interna; `wrap-anywhere` fica disponível como opção explícita.
|
|
19
|
+
Classes explícitas continuam permitindo
|
|
20
|
+
células sem quebra. ActionList limita sua coluna de resultados ao espaço
|
|
21
|
+
recebido e permite encolhimento ao longo da cadeia de flex/grid.
|
|
22
|
+
|
|
23
|
+
`fit` preserva valores compactos sem quebra, como números e datas. O cabeçalho
|
|
24
|
+
de uma coluna declarada pode quebrar entre palavras. Textos flexíveis recebem o
|
|
25
|
+
restante do espaço. Não há redução de tipografia nem ocultação automática de colunas.
|
|
26
|
+
|
|
27
|
+
A barra de filtros pode ocupar mais de uma linha, mantendo a ordem dos controles.
|
|
28
|
+
Uma tabela que ainda exceda a largura disponível rola dentro da lista: em flow,
|
|
29
|
+
no contêiner da tabela; em fill, na região de resultados que também ancora o
|
|
30
|
+
cabeçalho fixo. O rodapé e a toolbar não acompanham essa rolagem.
|
|
31
|
+
|
|
32
|
+
## Alternativas e consequências
|
|
33
|
+
|
|
34
|
+
Largura fixa e repartição igual sacrificariam espaço das colunas textuais mesmo
|
|
35
|
+
quando controles curtos não precisam dele. Ocultar o excesso perderia dados e
|
|
36
|
+
ações. Regras CSS no aplicativo duplicariam o comportamento do pacote.
|
|
37
|
+
|
|
38
|
+
A quebra pode aumentar a altura das linhas. Consumidores com células compostas
|
|
39
|
+
continuam responsáveis por permitir encolhimento dos filhos flex e por declarar
|
|
40
|
+
larguras mínimas somente quando necessárias. Comparativos com colunas mínimas
|
|
41
|
+
explícitas continuam usando rolagem horizontal.
|
|
42
|
+
|
|
43
|
+
## Verificação
|
|
44
|
+
|
|
45
|
+
Testes em Chromium medem contenção, textos longos, filtros, valores compactos e
|
|
46
|
+
rolagem nos modos flow e fill, incluindo a posição do cabeçalho após rolar.
|
|
47
|
+
Os testes estão em `tests/browser/action-list-width.test.tsx` e cobrem também
|
|
48
|
+
palavras curtas ao lado de descrições longas e tokens sem espaços.
|
|
49
|
+
|
|
50
|
+
## Revisão da distribuição entre colunas — 2026-10-15
|
|
51
|
+
|
|
52
|
+
Na validação de Consumo, quatro colunas `fit` ficavam comprimidas enquanto a coluna
|
|
53
|
+
textual recebia cerca de 70% da largura. A largura mínima forçada fazia o navegador
|
|
54
|
+
quebrar títulos mesmo com espaço disponível. Retirar essa restrição reduziu a coluna
|
|
55
|
+
textual para cerca de 38% e acomodou os cabeçalhos numa linha, no mesmo contêiner.
|
|
56
|
+
|
|
57
|
+
Colunas de dados passam a participar da distribuição automática da tabela, considerando
|
|
58
|
+
cabeçalhos e valores. `fit` continua preservando valores sem quebra, mas não impõe
|
|
59
|
+
largura mínima artificial. Cabeçalhos podem quebrar quando o espaço realmente faltar.
|
|
60
|
+
Seleção, ações de linha e células contendo apenas um switch continuam compactas.
|
|
61
|
+
O switch pode ser filho direto ou estar envolvido pelo TooltipTrigger. A regra usa a
|
|
62
|
+
estrutura do controle, sem depender do nome da coluna; células com conteúdo misto
|
|
63
|
+
continuam participando da distribuição natural.
|
|
64
|
+
|
|
65
|
+
Não se distribui largura igualmente nem se mede texto em JavaScript. O algoritmo
|
|
66
|
+
nativo considera o conteúdo e o espaço recebido; a contenção e a rolagem descritas
|
|
67
|
+
acima permanecem. Os testes de navegador cobrem várias colunas numéricas ao lado de
|
|
68
|
+
texto, tanto com espaço suficiente quanto em contêiner estreito, em flow e fill.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# ADR 0063 — Metadados das superfícies em região própria
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-15.
|
|
5
|
+
- **Complementa:** ADR 0060.
|
|
6
|
+
- **Substitui parcialmente:** ADR 0045 quanto à posição e função de SurfaceMeta.
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
Badges, autoria e datas disputam espaço com o título ou usam o subheading como container.
|
|
11
|
+
A descrição é orientação textual próxima ao heading; metadados precisam de espaçamento
|
|
12
|
+
próprio e quebra de linha sem alterar o tamanho ou o significado do título.
|
|
13
|
+
|
|
14
|
+
## Decisão
|
|
15
|
+
|
|
16
|
+
PageMeta, DrawerMeta, DialogMeta e SurfaceMeta compõem uma faixa opcional após a linha
|
|
17
|
+
que contém título, descrição e ações. Não são headings nem descrições acessíveis dos modais.
|
|
18
|
+
As três primeiras famílias aceitam Meta em Intro ou Header compacto; Surface mantém
|
|
19
|
+
Meta em SurfaceHeader, pois não possui Intro. Meta exige título na mesma composição.
|
|
20
|
+
Dialog em modo de alerta não aceita Meta; informações necessárias à resposta pertencem ao corpo.
|
|
21
|
+
|
|
22
|
+
O intervalo superior é de 0,75rem; itens usam 0,75rem na horizontal e 0,5rem entre linhas.
|
|
23
|
+
A faixa permite quebra, conserva a tipografia dos badges e não insere separadores nem
|
|
24
|
+
busca dados. Sem Meta não há espaço reservado. Uma composição aceita no máximo um Meta.
|
|
25
|
+
As formas curtas de Page e Surface aceitam a prop meta. Fieldset preserva a legend
|
|
26
|
+
restrita a título e mídia decorativa; seus metadados pertencem ao corpo.
|
|
27
|
+
|
|
28
|
+
SurfaceCount preserva o contador ao lado do título e recebe count na forma curta.
|
|
29
|
+
SurfaceMeta deixa de ocupar essa posição: consumidores de contadores explícitos migram
|
|
30
|
+
para SurfaceCount. Consumidores de contexto editorial permanecem em SurfaceMeta.
|
|
31
|
+
|
|
32
|
+
## Alternativas
|
|
33
|
+
|
|
34
|
+
Usar Description conservaria a mistura de prosa e metadados. Colocar wrappers livres no
|
|
35
|
+
Body repetiria espaçamento nos consumidores e afastaria o contexto da identidade. Manter
|
|
36
|
+
SurfaceMeta como contador faria o mesmo nome expressar funções diferentes entre famílias.
|
|
37
|
+
|
|
38
|
+
## Verificação
|
|
39
|
+
|
|
40
|
+
Testes renderizados e checker cobrem posição, unicidade, pai permitido e título obrigatório.
|
|
41
|
+
Contadores permanecem ao lado do título. Testes no navegador conferem intervalo e quebra
|
|
42
|
+
nas famílias, incluindo headers compactos com ações. As evidências pertencem aos arquivos
|
|
43
|
+
`tests/ui/surface-meta.test.tsx`, `tests/browser/surface-meta.test.tsx` e
|
|
44
|
+
`tests/check/check.test.ts`. Publicação e adoção dependem de autorização de entrega.
|
package/docs/adr/README.md
CHANGED
|
@@ -16,3 +16,7 @@ estar documentado na superfície correspondente, sem exigir a reconstrução des
|
|
|
16
16
|
- [ADR 0060 — Composição de superfícies com cabeçalho e intro](0060-surface-composition-separates-chrome-and-intro.md).
|
|
17
17
|
|
|
18
18
|
- [ADR 0061 — O `CLAUDE.md` importa o `AGENTS.md`](0061-claude-md-imports-agents-md.md).
|
|
19
|
+
|
|
20
|
+
- [ADR 0062 — Listas contidas e texto flexível](0062-listas-contidas-e-texto-flexivel.md).
|
|
21
|
+
|
|
22
|
+
- [ADR 0063 — Metadados das superfícies em região própria](0063-metadados-das-superficies.md).
|
package/package.json
CHANGED
|
@@ -65,7 +65,7 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
65
65
|
relativa do Tailwind. Reservar `px` a hairlines e compensações presas à geometria da borda,
|
|
66
66
|
com justificativa e cobertura explícitas.
|
|
67
67
|
11. Em uma página centrada em coleção, compor `Surface + ActionList`: título e criação ficam em
|
|
68
|
-
extremos opostos do `SurfaceHeader`; a criação usa botão textual `
|
|
68
|
+
extremos opostos do `SurfaceHeader`; a criação usa botão textual `sm`, sem ícone, com
|
|
69
69
|
`Criar recurso`. Busca, filtros, atualização, estados e paginação permanecem no `ActionList`.
|
|
70
70
|
Reservar `toolbarActions` a operações ligadas ao recorte atual.
|
|
71
71
|
12. Evoluir um pattern compartilhado apenas quando a recorrência e o contrato estiverem claros.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
outra em `PageShell` com navegação contextual acima de `Page > PageHeader + PageBody`, garantindo
|
|
8
8
|
exatamente um `PageTitle` no header, e uma coleção nomeada com
|
|
9
9
|
`Surface > SurfaceHeader + SurfaceBody > ActionList`. Provar que criação fica em
|
|
10
|
-
`SurfaceActions`, no extremo oposto ao título, como botão textual `
|
|
10
|
+
`SurfaceActions`, no extremo oposto ao título, como botão textual `sm` sem ícone e com
|
|
11
11
|
`Criar recurso`, enquanto uma operação dependente do recorte usa `toolbarActions`; incluir
|
|
12
12
|
`ActionFilterBar` separado do renderer, `ItemGroup` para
|
|
13
13
|
uma coleção secundária e um `ActionFormDialog`; provar teto padrão de `80rem`, hierarquia por
|
|
@@ -84,3 +84,12 @@ da label, duplicação entre hint/help e perda de `aria-describedby` quando apar
|
|
|
84
84
|
IntroActions, nenhum header vazio na Page e transição de loading para conteúdo sem título duplicado.
|
|
85
85
|
- Reprovar intro depois de texto, título simultâneo em header e intro, ações da superfície no título
|
|
86
86
|
ou adaptação de Pane para que ele volte a possuir anatomia visual.
|
|
87
|
+
|
|
88
|
+
## Metadados e contadores
|
|
89
|
+
|
|
90
|
+
- Execução: compor um detalhe com título, descrição e badges em PageMeta, DrawerMeta e DialogMeta;
|
|
91
|
+
conferir a faixa separada em Header compacto e Intro. Em Surface, conferir que SurfaceCount
|
|
92
|
+
fica na linha do título e SurfaceMeta abaixo dela. Migrar um contador antigo e uma descrição
|
|
93
|
+
antiga sem alterar suas funções; provar a equivalência das formas curtas com `meta`.
|
|
94
|
+
- Reprova: contador em SurfaceMeta, metadados em Description, dois Meta, Meta sem título,
|
|
95
|
+
Meta em fieldset ou DialogMeta em alerta.
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
recuperação como botão `outline` textual. Estados de seção ou coleção continuam em `DataState`,
|
|
37
37
|
`ActionView`, `ActionList` ou `Alert`; não elevar uma falha parcial a estado da página.
|
|
38
38
|
- `Surface` delimita uma região e segue a anatomia `Surface > SurfaceHeader (SurfaceMedia,
|
|
39
|
-
SurfaceTitle, SurfaceDescription, SurfaceMeta, SurfaceActions) + SurfaceBody + SurfaceFooter`.
|
|
40
|
-
Os slots nunca ficam soltos. `title`, `description`, `count`, `media` e `actions` são a abreviação
|
|
39
|
+
SurfaceTitle, SurfaceCount, SurfaceDescription, SurfaceMeta, SurfaceActions) + SurfaceBody + SurfaceFooter`.
|
|
40
|
+
Os slots nunca ficam soltos. `title`, `description`, `count`, `meta`, `media` e `actions` são a abreviação
|
|
41
41
|
para o caso direto e não podem ser misturados ao header explícito. Em `level={1}`, ações da
|
|
42
42
|
coleção ficam no extremo oposto ao título; a criação usa botão textual `sm`, sem ícone, com
|
|
43
43
|
`Criar recurso`.
|
|
@@ -45,6 +45,14 @@ SurfaceTitle, SurfaceDescription, SurfaceMeta, SurfaceActions) + SurfaceBody + S
|
|
|
45
45
|
hierarquia semântica do heading. `Surface framed` aceita `layout="sectioned"`; declare
|
|
46
46
|
`SurfaceBody flush` quando o filho direto já possui recuo interno, como `DetailGroup` com
|
|
47
47
|
divisores, lista ou tabela. `as="fieldset"` cria a `legend` nativa sem variante de moldura.
|
|
48
|
+
- `PageMeta`, `DialogMeta` e `DrawerMeta` recebem badges, datas e autoria em faixa própria
|
|
49
|
+
após título, descrição e ações, dentro de Header compacto ou Intro da mesma família.
|
|
50
|
+
`SurfaceMeta` ocupa essa faixa em `SurfaceHeader`; `SurfaceCount` mantém o contador na linha
|
|
51
|
+
do título. Ao migrar, conferir manualmente os antigos `SurfaceMeta`: contadores passam a
|
|
52
|
+
`SurfaceCount`, frases de orientação a `SurfaceDescription`, e metadados permanecem em Meta.
|
|
53
|
+
Cada composição aceita um Meta e exige título. Page e Surface aceitam `meta` na forma curta;
|
|
54
|
+
fieldset e Dialog em `mode="alert"` não aceitam Meta. Informações necessárias à decisão
|
|
55
|
+
permanecem no corpo ou junto à ação correspondente.
|
|
48
56
|
- Uma página centrada em coleção compõe `Surface + ActionList`. O `SurfaceHeader` recebe título e
|
|
49
57
|
criação; o `ActionList` mantém busca, filtros, atualização, resultados, estados e paginação.
|
|
50
58
|
`toolbarActions` fica reservado a operações que dependem do recorte atual.
|
package/src/core/types.ts
CHANGED
|
@@ -714,7 +714,7 @@ export interface ListColumnSpec {
|
|
|
714
714
|
/** Header clicável (asc ↔ desc). Convenção: a UI escreve `sort: '<key>:<dir>'` no
|
|
715
715
|
* input; o handler implementa o orderBy. */
|
|
716
716
|
sortable?: boolean
|
|
717
|
-
/**
|
|
717
|
+
/** Preserva valores sem quebra; a largura considera também o cabeçalho e o espaço disponível. */
|
|
718
718
|
fit?: boolean
|
|
719
719
|
/** Nasce oculta (candidata ao column picker). */
|
|
720
720
|
hidden?: boolean
|
|
@@ -1524,7 +1524,7 @@ export function ActionFilterBar({
|
|
|
1524
1524
|
return (
|
|
1525
1525
|
<TooltipProvider>
|
|
1526
1526
|
<div data-slot="action-filter-bar" className="space-y-2">
|
|
1527
|
-
<div className="flex min-w-0 items-end gap-3">
|
|
1527
|
+
<div className="flex min-w-0 flex-wrap items-end gap-3">
|
|
1528
1528
|
{periodPosition === "before" && periodControl}
|
|
1529
1529
|
{inlineSpecs.map(([name, spec]) => {
|
|
1530
1530
|
if (name === periodBasisEntry?.[0]) {
|
|
@@ -1636,7 +1636,7 @@ export function ActionFilterBar({
|
|
|
1636
1636
|
{!searchAtActionsEnd && searchBox}
|
|
1637
1637
|
<div
|
|
1638
1638
|
data-slot="action-filter-actions"
|
|
1639
|
-
className="ml-auto flex min-w-0 items-end gap-2"
|
|
1639
|
+
className="ml-auto flex min-w-0 max-w-full flex-wrap items-end gap-2"
|
|
1640
1640
|
>
|
|
1641
1641
|
{searchAtActionsEnd && searchBox}
|
|
1642
1642
|
{(modalSpecs.length > 0 ||
|
|
@@ -2074,7 +2074,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2074
2074
|
header: text(c.label, c.key),
|
|
2075
2075
|
sortable: c.sortable === true,
|
|
2076
2076
|
spec: c,
|
|
2077
|
-
className: cn(c.fit === true && "
|
|
2077
|
+
className: cn(c.fit === true && "whitespace-nowrap wrap-normal"),
|
|
2078
2078
|
cell:
|
|
2079
2079
|
cells?.[c.key] ??
|
|
2080
2080
|
((item: TItem): ReactNode => {
|
|
@@ -2534,7 +2534,13 @@ export function ActionList<TInput, TItem>({
|
|
|
2534
2534
|
</TableCell>
|
|
2535
2535
|
)}
|
|
2536
2536
|
{derived.map((col) => (
|
|
2537
|
-
<TableCell
|
|
2537
|
+
<TableCell
|
|
2538
|
+
key={col.key}
|
|
2539
|
+
className={cn(
|
|
2540
|
+
"has-[>[role=switch]:only-child]:w-0 has-[>[data-slot=tooltip-trigger]:only-child>[role=switch]:only-child]:w-0",
|
|
2541
|
+
col.className,
|
|
2542
|
+
)}
|
|
2543
|
+
>
|
|
2538
2544
|
{col.cell(item)}
|
|
2539
2545
|
</TableCell>
|
|
2540
2546
|
))}
|
|
@@ -2597,7 +2603,10 @@ export function ActionList<TInput, TItem>({
|
|
|
2597
2603
|
</TableHead>
|
|
2598
2604
|
)}
|
|
2599
2605
|
{derived.map((col) => (
|
|
2600
|
-
<TableHead
|
|
2606
|
+
<TableHead
|
|
2607
|
+
key={col.key}
|
|
2608
|
+
className={cn(col.className, col.spec && "whitespace-normal")}
|
|
2609
|
+
>
|
|
2601
2610
|
{col.sortable === true ? (
|
|
2602
2611
|
<button
|
|
2603
2612
|
type="button"
|
|
@@ -2959,8 +2968,8 @@ export function ActionList<TInput, TItem>({
|
|
|
2959
2968
|
data-layout={layout}
|
|
2960
2969
|
className={cn(
|
|
2961
2970
|
layout === "fill"
|
|
2962
|
-
? "flex h-full min-h-0 flex-1 flex-col gap-3"
|
|
2963
|
-
: "space-y-3",
|
|
2971
|
+
? "flex h-full min-h-0 min-w-0 max-w-full flex-1 flex-col gap-3"
|
|
2972
|
+
: "min-w-0 max-w-full space-y-3",
|
|
2964
2973
|
)}
|
|
2965
2974
|
>
|
|
2966
2975
|
{/* A lista é a folha da cadeia de altura: o que ela hospeda decide a própria. */}
|
|
@@ -2975,7 +2984,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2975
2984
|
tem altura própria e ela vira a altura do conteúdo. */}
|
|
2976
2985
|
<div
|
|
2977
2986
|
className={cn(
|
|
2978
|
-
"grid min-h-0 grid-rows-[1fr_auto]",
|
|
2987
|
+
"grid min-h-0 min-w-0 grid-cols-[minmax(0,1fr)] grid-rows-[1fr_auto]",
|
|
2979
2988
|
layout === "fill" && "flex-1",
|
|
2980
2989
|
)}
|
|
2981
2990
|
>
|
|
@@ -2986,7 +2995,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2986
2995
|
<div
|
|
2987
2996
|
aria-busy={busy}
|
|
2988
2997
|
className={cn(
|
|
2989
|
-
"min-h-0",
|
|
2998
|
+
"min-h-0 min-w-0",
|
|
2990
2999
|
layout === "fill" && "overflow-auto",
|
|
2991
3000
|
busy && items.length > 0 && "pointer-events-none opacity-60",
|
|
2992
3001
|
)}
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "./page-heading-context.tsx";
|
|
32
32
|
import {
|
|
33
33
|
StructuralHeader,
|
|
34
|
+
structuralMetaClasses,
|
|
34
35
|
validateSurfaceIntro,
|
|
35
36
|
structuralHeaderClasses,
|
|
36
37
|
} from "./structural-header.tsx";
|
|
@@ -165,6 +166,8 @@ interface PageShorthandProps extends PageBaseProps {
|
|
|
165
166
|
title: ReactNode;
|
|
166
167
|
/** Orientação opcional sobre a finalidade da página; regras pertencem ao corpo. */
|
|
167
168
|
description?: ReactNode;
|
|
169
|
+
/** Badges, datas e autoria sob a orientação. */
|
|
170
|
+
meta?: ReactNode;
|
|
168
171
|
actions?: ReactNode;
|
|
169
172
|
children: ReactNode;
|
|
170
173
|
}
|
|
@@ -172,6 +175,7 @@ interface PageShorthandProps extends PageBaseProps {
|
|
|
172
175
|
interface PageComposedProps extends PageBaseProps {
|
|
173
176
|
title?: never;
|
|
174
177
|
description?: never;
|
|
178
|
+
meta?: never;
|
|
175
179
|
actions?: never;
|
|
176
180
|
children: ReactNode;
|
|
177
181
|
}
|
|
@@ -246,6 +250,7 @@ function hasOpaquePageBodyContent(node: ReactNode): boolean {
|
|
|
246
250
|
export function Page({
|
|
247
251
|
title,
|
|
248
252
|
description,
|
|
253
|
+
meta,
|
|
249
254
|
actions,
|
|
250
255
|
layout = "flow",
|
|
251
256
|
surfaceLayout = "sectioned",
|
|
@@ -377,6 +382,7 @@ export function Page({
|
|
|
377
382
|
{description !== undefined && (
|
|
378
383
|
<PageDescription>{description}</PageDescription>
|
|
379
384
|
)}
|
|
385
|
+
{meta !== undefined && <PageMeta>{meta}</PageMeta>}
|
|
380
386
|
</PageIntro>
|
|
381
387
|
{children}
|
|
382
388
|
</PageBody>
|
|
@@ -490,6 +496,7 @@ export function PageHeader({
|
|
|
490
496
|
leading: [PageBack, PageNavigation],
|
|
491
497
|
title: PageTitle,
|
|
492
498
|
description: PageDescription,
|
|
499
|
+
meta: PageMeta,
|
|
493
500
|
actions: PageActions,
|
|
494
501
|
}}
|
|
495
502
|
leadingPlacement={
|
|
@@ -542,6 +549,7 @@ export function PageIntro({
|
|
|
542
549
|
slots={{
|
|
543
550
|
title: PageTitle,
|
|
544
551
|
description: PageDescription,
|
|
552
|
+
meta: PageMeta,
|
|
545
553
|
actions: PageIntroActions,
|
|
546
554
|
}}
|
|
547
555
|
leadingPlacement="inline"
|
|
@@ -555,6 +563,25 @@ export function PageIntro({
|
|
|
555
563
|
);
|
|
556
564
|
}
|
|
557
565
|
|
|
566
|
+
/** Badges, datas e autoria abaixo do título e da orientação. */
|
|
567
|
+
export function PageMeta({
|
|
568
|
+
className,
|
|
569
|
+
...props
|
|
570
|
+
}: HTMLAttributes<HTMLDivElement>) {
|
|
571
|
+
const header = useContext(PageHeaderContext);
|
|
572
|
+
const intro = useContext(PageIntroContext);
|
|
573
|
+
const inside = header || intro;
|
|
574
|
+
if (!inside)
|
|
575
|
+
throw new Error("PageMeta deve estar dentro de PageHeader ou PageIntro.");
|
|
576
|
+
return (
|
|
577
|
+
<div
|
|
578
|
+
data-slot="page-meta"
|
|
579
|
+
className={cn(structuralMetaClasses, className)}
|
|
580
|
+
{...props}
|
|
581
|
+
/>
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
558
585
|
/** Ações locais do conteúdo, distintas das ações da superfície em PageActions. */
|
|
559
586
|
export function PageIntroActions({
|
|
560
587
|
className,
|