@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 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(["title", "media", "description", "count", "actions"]),
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 `SurfaceMeta`"],
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(["DialogIntroActions", "DialogDescription"]),
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(["DrawerIntroActions", "DrawerDescription"]),
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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "29.0.0",
3
+ "version": "30.0.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 `default`, sem ícone, com
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 `default` sem ícone e com
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
- /** Coluna encolhe ao conteúdo (não expande). */
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 && "w-px whitespace-nowrap"),
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 key={col.key} className={col.className}>
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 key={col.key} className={col.className}>
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,