@softize/opus 27.0.0 → 28.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 +73 -0
- package/bin/lib/check.mjs +139 -30
- package/docs/adr/0015-action-size-follows-interaction-density.md +2 -0
- package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +14 -8
- package/docs/adr/0029-page-header-is-the-single-page-introduction.md +3 -1
- package/docs/adr/0057-doc-browser-permite-busca-no-chrome-do-consumidor.md +39 -5
- package/docs/adr/0060-surface-composition-separates-chrome-and-intro.md +161 -0
- package/docs/adr/README.md +2 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +11 -11
- package/registry/skills/build-opus-ui/references/evaluations.md +10 -0
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -15
- package/src/ui/components/patterns/list.tsx +62 -53
- package/src/ui/components/patterns/page.tsx +146 -38
- package/src/ui/components/patterns/presentation.tsx +44 -59
- package/src/ui/components/patterns/structural-header.tsx +42 -6
- package/src/ui/components/patterns/surface-anatomy.ts +52 -22
- package/src/ui/components/patterns/surface.tsx +2 -3
- package/src/ui/components/primitives/control.ts +2 -2
- package/src/ui/components/primitives/dialog.tsx +7 -2
- package/src/ui/components/primitives/drawer.tsx +7 -2
- package/src/ui/components/primitives/icon-picker.tsx +271 -34
- package/src/ui/components/primitives/pagination.tsx +5 -4
- package/src/ui/components/primitives/tabs.tsx +15 -2
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/docs/DocBrowser.tsx +122 -12
- package/src/ui/docs/content/action-list.md +11 -2
- package/src/ui/docs/content/button.md +7 -7
- package/src/ui/docs/content/compositions.md +35 -23
- package/src/ui/docs/content/dialog.md +18 -6
- package/src/ui/docs/content/documentation-portal.md +40 -1
- package/src/ui/docs/content/drawer.md +17 -5
- package/src/ui/docs/content/icon-picker.md +37 -19
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/page.md +169 -69
- package/src/ui/docs/content/pagination.md +7 -8
- package/src/ui/docs/content/presentation.md +4 -3
- package/src/ui/docs/content/surface.md +8 -5
- package/src/ui/docs/content/tabs.md +12 -4
- package/src/ui/docs/types.ts +2 -0
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +9 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# ADR 0060 — A composição separa controles da superfície e introdução do conteúdo
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-10.
|
|
5
|
+
- **Substitui parcialmente:** ADR 0029, quanto à proibição de PageIntro.
|
|
6
|
+
- **Complementa:** ADRs 0023 e 0048.
|
|
7
|
+
|
|
8
|
+
## Contexto
|
|
9
|
+
|
|
10
|
+
Page perdeu a intro para evitar barras vazias e identificação redundante. Depois, Dialog e Drawer
|
|
11
|
+
passaram a oferecer uma intro para separar os controles da superfície do conteúdo. A mesma página
|
|
12
|
+
editorial passou a mudar o lugar do título e das ações quando aberta em outro hospedeiro.
|
|
13
|
+
|
|
14
|
+
## Decisão
|
|
15
|
+
|
|
16
|
+
Page, Dialog e Drawer compartilham duas composições, declaradas pelos slots, sem prop redundante:
|
|
17
|
+
|
|
18
|
+
- compacta: o título e a orientação ficam no Header;
|
|
19
|
+
- com intro: o Header recebe navegação e ações da superfície; a Intro é o primeiro bloco do Body
|
|
20
|
+
e recebe o único título, orientação opcional e ações locais por IntroActions.
|
|
21
|
+
|
|
22
|
+
Header sem controles não reserva espaço. Navegação não é repetida na intro. O fechamento continua
|
|
23
|
+
sendo responsabilidade do modal; Page não ganha um botão de fechar implícito. A posição da intro
|
|
24
|
+
não muda o significado de layout: continuous/sectioned controla regiões e flow/fill controla altura.
|
|
25
|
+
|
|
26
|
+
Page ganha PageIntro e PageIntroActions. A forma curta passa a colocar title e description na intro,
|
|
27
|
+
com actions no Header. A forma explícita preserva a composição compacta para situações deliberadas.
|
|
28
|
+
PageShell continua fornecendo contexto sem criar outra barra. Estados integrais continuam substituindo
|
|
29
|
+
a introdução e o conteúdo, com um único heading acessível. O teto centralizado de 80rem é preservado.
|
|
30
|
+
|
|
31
|
+
Surface mantém o cabeçalho do conteúdo, inclusive ações locais: não possui navegação ou fechamento
|
|
32
|
+
que justifique uma segunda região. Pane e Split continuam sendo apenas layout. Alert mantém sua
|
|
33
|
+
composição especializada. Patterns de formulário, coleção e Presentation precisam respeitar a mesma
|
|
34
|
+
semântica ao montar os componentes, sem reconstruir linhas no consumidor.
|
|
35
|
+
|
|
36
|
+
## Verificação e migração
|
|
37
|
+
|
|
38
|
+
O checker valida família, posição da intro, unicidade do título e ações no slot correspondente.
|
|
39
|
+
Testes renderizados cobrem as duas composições, estados integrais, ausência de barra vazia e contexto
|
|
40
|
+
do shell. Consumidores de Page explícita migram título/descrição para PageIntro quando suas ações
|
|
41
|
+
pertencem à superfície; cabeçalhos de coleções em Surface mantêm criação junto à coleção.
|
|
42
|
+
|
|
43
|
+
A migração inclui SDK, exemplos, documentação e aplicação. Publicação e adoção definitiva só ocorrem
|
|
44
|
+
após os gates e a revisão independente do conjunto. A versão 28.0.0 consolida a migração.
|
|
45
|
+
|
|
46
|
+
## Alternativas
|
|
47
|
+
|
|
48
|
+
Uma prop compact duplicaria informação já expressa pelos slots, abrindo combinações contraditórias.
|
|
49
|
+
Manter Page diferente preservaria compatibilidade, mas perpetuaria a diferença entre hospedeiros.
|
|
50
|
+
Uma barra obrigatória recuperaria o espaço vazio e a identificação repetida que motivaram a ADR 0029.
|
|
51
|
+
|
|
52
|
+
## Rolagem
|
|
53
|
+
|
|
54
|
+
Page ocupa a altura disponível do hospedeiro, com header e footer fora da região rolável.
|
|
55
|
+
PageBody é o viewport vertical; a intro rola com o conteúdo. PageShell conserva a rolagem horizontal do canvas mínimo e permite rolagem vertical de
|
|
56
|
+
contingência para rotas sem Page ou wrappers sem altura definida. Em layout fill, quando uma ActionList assume a rolagem, o body repassa
|
|
57
|
+
a altura e não cria uma segunda rolagem vertical. Sem folha fill, o próprio body continua rolando.
|
|
58
|
+
A composição compacta preserva a mesma responsabilidade: o título permanece no header estável.
|
|
59
|
+
A fixação resulta da divisão de altura, sem position fixed ou sticky no consumidor.
|
|
60
|
+
|
|
61
|
+
O header ocupa toda a largura disponível. O limite de 80rem e o recuo pertencem ao conteúdo
|
|
62
|
+
interno do Body; o viewport ocupa a largura da página para manter sua barra de rolagem na extremidade.
|
|
63
|
+
|
|
64
|
+
Quando presente, o header da Page usa borda inferior em toda a largura, como o Drawer.
|
|
65
|
+
|
|
66
|
+
## Divisão visual da Page
|
|
67
|
+
|
|
68
|
+
Page expõe `surfaceLayout="sectioned" | "continuous"`, com padrão `sectioned`.
|
|
69
|
+
O nome separa a anatomia visual de `layout="flow" | "fill"`, preservado para altura.
|
|
70
|
+
As regiões usam a tabela compartilhada de anatomia e seus divisores por adjacência.
|
|
71
|
+
A largura total do header e do viewport, e o limite interno do conteúdo, valem nos dois modos.
|
|
72
|
+
|
|
73
|
+
O header da Page acompanha o recuo do Drawer: 1rem no modo sectioned; no modo
|
|
74
|
+
continuous, 1rem com recuo inferior de 0,75rem, ou 1rem quando for a única região.
|
|
75
|
+
|
|
76
|
+
Após validação visual da barra com breadcrumb, o header da Page passa a usar 1rem nas
|
|
77
|
+
laterais e 0,75rem na vertical nos dois modos. Esse ajuste substitui a equivalência de
|
|
78
|
+
padding com o Drawer descrita acima; os divisores continuam compartilhados.
|
|
79
|
+
|
|
80
|
+
A avaliação seguinte restaurou o padding do Drawer no header da Page: 1rem no modo
|
|
81
|
+
sectioned e o ritmo correspondente no continuous. O experimento de redução vertical foi descartado.
|
|
82
|
+
|
|
83
|
+
## Altura estável da linha do header
|
|
84
|
+
|
|
85
|
+
A linha interna reserva 2rem em Page, Dialog, Drawer e Surface, mesmo sem ações.
|
|
86
|
+
No header da Page, botões textuais usam `sm` (2rem), e os de ícone, `icon-sm` (2rem).
|
|
87
|
+
Os consumidores declaram o tamanho; PageBack e comandos de Presentation em página já usam
|
|
88
|
+
essa escala. Coleções e headers de modais usam a mesma escala; rodapés conservam default. É uma altura mínima, permitindo crescimento do
|
|
89
|
+
conteúdo. Padding e intro permanecem independentes; headers ausentes não reservam espaço.
|
|
90
|
+
|
|
91
|
+
Presentation em página delega padding e rolagem ao PageBody, tanto dentro quanto fora
|
|
92
|
+
de PageShell. Não intercala um viewport próprio; PresentationBody conserva somente a
|
|
93
|
+
propagação de altura quando houver conteúdo fill. Headers compactos distribuem título
|
|
94
|
+
e ações na linha interna, inclusive sem navegação.
|
|
95
|
+
|
|
96
|
+
## Revisão: geometria comum da família
|
|
97
|
+
|
|
98
|
+
A escala compacta exclusiva de Page mantinha headers de Drawer maiores com a mesma
|
|
99
|
+
composição. A família inteira passa a compartilhar padding de 1rem e linha mínima
|
|
100
|
+
de 2rem: Page, Dialog, Drawer e Surface delimitada, também quando hospedada em Pane.
|
|
101
|
+
Vale nos layouts continuous e sectioned; o divisor continua sendo uma decisão do layout.
|
|
102
|
+
Isso substitui o ritmo variável de padding inferior dos headers descrito anteriormente.
|
|
103
|
+
|
|
104
|
+
Botões de header usam `sm` para texto e `icon-sm` para ícones, incluindo comandos de
|
|
105
|
+
Presentation e criação de coleções. Rodapés preservam a escala anterior. A seção
|
|
106
|
+
Surface sem delimitação não ganha padding externo, e legendas recortadas continuam
|
|
107
|
+
com sua geometria própria. O mínimo de linha e a escala das ações permanecem comuns.
|
|
108
|
+
|
|
109
|
+
As medidas têm uma fonte compartilhada, comparada em testes entre as quatro famílias.
|
|
110
|
+
Os tamanhos são declarados pelos consumidores: não há CSS de redimensionamento nem
|
|
111
|
+
contexto herdável que altere botões de portais, corpos ou rodapés.
|
|
112
|
+
|
|
113
|
+
## Correção da escala compacta
|
|
114
|
+
|
|
115
|
+
`icon-sm` passa de 1,75rem para 2rem, correspondendo à altura de `sm`. O desenho
|
|
116
|
+
do ícone continua em 0,875rem. O mínimo de 2rem da linha protege headers sem ações;
|
|
117
|
+
não existe mais diferença de altura entre os controles compactos textuais e quadrados.
|
|
118
|
+
Paginação usa os mesmos tokens, com largura flexível para números longos.
|
|
119
|
+
|
|
120
|
+
## Ritmo vertical compartilhado — 2026-10-13
|
|
121
|
+
|
|
122
|
+
A validação em Conhecimento encontrou intervalos de 0,5rem, 0,75rem e 1,25rem para a mesma
|
|
123
|
+
relação entre cabeçalho e conteúdo. O padrão passa a ser 1rem entre regiões contínuas e
|
|
124
|
+
entre blocos diretos de um body, inclusive TabsPanel. O componente hospedeiro fornece o
|
|
125
|
+
ritmo; DetailGroup e Separator não recebem margem externa própria.
|
|
126
|
+
|
|
127
|
+
Surface sem delimitação e Tabs sem moldura não adicionam padding inferior. Surface delimitada
|
|
128
|
+
por framed ou elevation, Dialog e Drawer reservam 1rem até a borda. No layout sectioned,
|
|
129
|
+
cada região reserva 1rem entre seu conteúdo e a divisória. No continuous, a região anterior
|
|
130
|
+
fornece o intervalo, evitando somar dois paddings. A ausência de header preserva o recuo
|
|
131
|
+
superior de uma superfície delimitada. Fieldset com legend conserva 1rem após a legenda.
|
|
132
|
+
|
|
133
|
+
Page preserva largura de leitura e recuos laterais. Seu body não reserva padding inferior
|
|
134
|
+
quando encerra a página; quando há footer sectioned, reserva 1rem até a divisória. A barra
|
|
135
|
+
continua fora da rolagem. O modo flush continua removendo recuos, mas conserva o ritmo
|
|
136
|
+
entre blocos: tabelas e listas que ocupam a superfície controlam seu próprio interior.
|
|
137
|
+
|
|
138
|
+
Tabs mantém o espaçamento interno dos botões. O intervalo até o painel é 1rem; com framed,
|
|
139
|
+
o painel recebe recuos laterais e inferior de 1rem. Abas verticais mantêm o intervalo entre
|
|
140
|
+
as colunas e a moldura recebe o recuo externo sem duplicá-lo no painel.
|
|
141
|
+
|
|
142
|
+
Manter medidas locais dependeria de disciplina em cada tela; impor margem a cada bloco
|
|
143
|
+
somaria intervalos no aninhamento. As duas alternativas foram descartadas. A migração remove
|
|
144
|
+
overrides de ritmo e padding nos slots estruturais, preservando layouts internos deliberados.
|
|
145
|
+
Testes em navegador medem intervalos, fechamento inferior, aninhamento, presença/ausência de
|
|
146
|
+
header e footer, flush e troca de abas. O ritmo altera o default visual; a composição de Page exige a migração documentada na versão major.
|
|
147
|
+
|
|
148
|
+
Uma composição pode manter `gap-0` quando Tabs hospeda um DrawerBody que já fornece o
|
|
149
|
+
intervalo superior e controla a rolagem, como o painel de nós de Fluxos. Isso evita dois
|
|
150
|
+
intervalos para a mesma relação; não altera a medida de 1rem percebida pelo conteúdo.
|
|
151
|
+
|
|
152
|
+
## Escala do título na intro — 2026-10-13
|
|
153
|
+
|
|
154
|
+
PageTitle, DrawerTitle e DialogTitle usam a mesma escala na intro: 1,875rem, com peso
|
|
155
|
+
semibold e tracking compacto. Drawer e Dialog mantêm a escala menor quando o título
|
|
156
|
+
está no header compacto. A hierarquia HTML continua independente do tamanho visual.
|
|
157
|
+
Metadados de data e autoria permanecem na descrição; um badge de tipo pode acompanhar
|
|
158
|
+
o título em uma composição inline com quebra, sem criar um novo slot estrutural.
|
|
159
|
+
|
|
160
|
+
`contentClassName` em Page e PageBody configura largura e alinhamento internos; `className`
|
|
161
|
+
controla a moldura e o viewport, respectivamente. Presentation fora do shell preserva largura total.
|
package/docs/adr/README.md
CHANGED
|
@@ -12,3 +12,5 @@ Uma ADR preserva seu contexto histórico mesmo quando é substituída. O comport
|
|
|
12
12
|
estar documentado na superfície correspondente, sem exigir a reconstrução dessa sequência.
|
|
13
13
|
|
|
14
14
|
- [ADR 0059 — Subheadings orientam sem carregar regras de negócio](0059-subheadings-orient-without-carrying-business-rules.md).
|
|
15
|
+
|
|
16
|
+
- [ADR 0060 — Composição de superfícies com cabeçalho e intro](0060-surface-composition-separates-chrome-and-intro.md).
|
package/package.json
CHANGED
|
@@ -43,17 +43,17 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
43
43
|
7. Tratar página, filtro, seleção e modal importante como estado navegável por URL quando
|
|
44
44
|
o produto precisa de deep link, back/forward ou refresh. Em `Presentation`, declarar `route`
|
|
45
45
|
no artefato e deixar o adaptador apenas conectar a spec ao router da aplicação.
|
|
46
|
-
8. Compor
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
8. Compor Page, Dialog e Drawer pela mesma gramática: no formato compacto, título e orientação
|
|
47
|
+
ficam no Header; com Intro, navegação e ações da superfície ficam no Header e a Intro abre o
|
|
48
|
+
Body com título, orientação e ações locais em IntroActions. Declarar a escolha pelos slots,
|
|
49
|
+
sem prop redundante. Usar PageIntro na Page explícita; a forma curta de Page já coloca title e
|
|
50
|
+
description na intro e actions no header. Não misturar shorthand com regiões explícitas.
|
|
51
|
+
Manter exatamente um heading principal, inclusive quando Surface level={1} nomeia uma coleção
|
|
52
|
+
ou PageState substitui a página. PageShell fornece contexto sem criar barra paralela; header
|
|
53
|
+
sem controles não reserva espaço. Preservar o teto de 80rem e separar composição de ocupação
|
|
54
|
+
de altura (flow/fill). Surface mantém o cabeçalho do conteúdo; Pane distribui espaço e recebe
|
|
55
|
+
uma superfície quando precisar de anatomia visual. Não criar Intro nem controles de navegação
|
|
56
|
+
artificiais para Surface ou Pane.
|
|
57
57
|
Em `Surface framed`, usar `layout="sectioned"` e `SurfaceBody flush` quando o filho direto já
|
|
58
58
|
controlar o recuo do body, como `DetailGroup` com divisores, lista ou tabela; não reconstruir
|
|
59
59
|
divisor, padding ou recorte por classes no consumidor. Em grupos de formulário,
|
|
@@ -74,3 +74,13 @@ complementar do termo. Esperado: `description` orienta a finalidade, `hint` mant
|
|
|
74
74
|
de deixar vazio e `help` oferece somente o detalhe opcional. `intro` continua no corpo quando a
|
|
75
75
|
regra afeta vários campos. Reprovar regra obrigatória somente em subheading ou tooltip, repetição
|
|
76
76
|
da label, duplicação entre hint/help e perda de `aria-describedby` quando aparece um erro.
|
|
77
|
+
|
|
78
|
+
## Composição de superfícies
|
|
79
|
+
|
|
80
|
+
- Gatilho positivo: “Monte o detalhe de um recurso com menu da página separado do título.”
|
|
81
|
+
- Gatilho negativo: “Explique por que a ADR antiga juntava título e ações”, sem pedido de UI.
|
|
82
|
+
- Execução: montar o mesmo detalhe em Page, Dialog e Drawer com header de navegação/ações e intro
|
|
83
|
+
abrindo o body; montar também a versão compacta sem intro. Provar título único, ações locais em
|
|
84
|
+
IntroActions, nenhum header vazio na Page e transição de loading para conteúdo sem título duplicado.
|
|
85
|
+
- Reprovar intro depois de texto, título simultâneo em header e intro, ações da superfície no título
|
|
86
|
+
ou adaptação de Pane para que ele volte a possuir anatomia visual.
|
|
@@ -9,24 +9,22 @@
|
|
|
9
9
|
lista; não reconstruir essa decisão com condicionais no componente da página.
|
|
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
|
-
Declare esse heading com
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Não misturar as duas formas.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
paralelo para uma `Page`. `PageActionsTarget` fica reservado a workspaces imersivos que já
|
|
22
|
-
possuam chrome próprio.
|
|
12
|
+
Declare esse heading com PageTitle na PageIntro ou no PageHeader compacto, Surface level={1}
|
|
13
|
+
ou PageState ativo. A intro abre o body; ações locais usam PageIntroActions, enquanto PageActions
|
|
14
|
+
pertence ao header. A forma curta coloca title/description na intro e actions no header.
|
|
15
|
+
Não misturar as duas formas. Para ajustar a largura interna, usar contentClassName; className pertence ao container externo.
|
|
16
|
+
- Na composição com intro, PageHeader reúne navegação contextual e ações da superfície na mesma
|
|
17
|
+
linha. PageBack recebe o destino pai explícito; para vários ancestrais relevantes, usar Breadcrumb
|
|
18
|
+
em PageNavigation. Não combinar retorno e breadcrumb, nem criar chrome paralelo. Na composição
|
|
19
|
+
compacta, título e orientação permanecem no header. PageActionsTarget fica reservado a workspaces
|
|
20
|
+
imersivos com chrome próprio.
|
|
23
21
|
- Quando shell e rota conhecem partes diferentes da mesma página, use `PageShell` ao redor da rota.
|
|
24
22
|
O shell entrega navegação e ações ao `PageHeader` descendente sem criar uma barra visual.
|
|
25
|
-
Ações com texto no cabeçalho usam
|
|
26
|
-
|
|
23
|
+
Ações com texto no cabeçalho usam `sm`, e ações somente com ícone usam `icon-sm`.
|
|
24
|
+
Ações de footer de Dialog e Drawer conservam `default`. O tamanho `sm` fica para ações operacionais em
|
|
27
25
|
toolbar, seção ou coleção densa, e `xs`/`icon-xs` para ações internas de linha ou célula. Contexto
|
|
28
26
|
e variante resolvem a hierarquia visual sem alterar essa medida. Em uma coleção, a ação de
|
|
29
|
-
criar usa botão textual `
|
|
27
|
+
criar usa botão textual `sm`, sem ícone, no extremo oposto do `SurfaceHeader`. Não monte
|
|
30
28
|
cabeçalho paralelo, portal ou seletor global para reconstruir essa composição.
|
|
31
29
|
- `PageState` substitui todo o conteúdo principal quando a página carrega, falha ou está vazia. Na
|
|
32
30
|
forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
|
|
@@ -41,7 +39,7 @@
|
|
|
41
39
|
SurfaceTitle, SurfaceDescription, SurfaceMeta, SurfaceActions) + SurfaceBody + SurfaceFooter`.
|
|
42
40
|
Os slots nunca ficam soltos. `title`, `description`, `count`, `media` e `actions` são a abreviação
|
|
43
41
|
para o caso direto e não podem ser misturados ao header explícito. Em `level={1}`, ações da
|
|
44
|
-
coleção ficam no extremo oposto ao título; a criação usa botão textual `
|
|
42
|
+
coleção ficam no extremo oposto ao título; a criação usa botão textual `sm`, sem ícone, com
|
|
45
43
|
`Criar recurso`.
|
|
46
44
|
O diálogo de criação usa `Criar recurso` e o de edição, `Editar recurso`. `level` preserva a
|
|
47
45
|
hierarquia semântica do heading. `Surface framed` aceita `layout="sectioned"`; declare
|
|
@@ -229,3 +227,7 @@ abaixo do controle. Reservar `FieldSpec.help` ao ícone com detalhe complementar
|
|
|
229
227
|
Omitir repetição da label; combinar ambos somente com funções distintas. Regra de vários campos
|
|
230
228
|
pertence ao corpo ou ao grupo. Em composição manual, `FieldDescription` e erro entram juntos no
|
|
231
229
|
`aria-describedby` do controle.
|
|
230
|
+
|
|
231
|
+
- Manter header e footer de Page fora da rolagem vertical. PageBody é o viewport em flow; em fill,
|
|
232
|
+
repassar a altura à lista quando ela assume a rolagem. Não reconstruir a fixação com sticky ou
|
|
233
|
+
fixed nem tornar PageShell uma segunda região de rolagem vertical.
|
|
@@ -362,6 +362,8 @@ export interface ActionListProps<TInput, TItem> {
|
|
|
362
362
|
layout?: "flow" | "fill";
|
|
363
363
|
/** Ações do consumidor no fim da barra de filtros, depois dos controles de exibição. */
|
|
364
364
|
toolbarActions?: ReactNode;
|
|
365
|
+
/** Controles da visualização ativa, imediatamente após seu seletor. */
|
|
366
|
+
viewControls?: ReactNode;
|
|
365
367
|
/** Tabela explícita — sobrepõe as colunas do contrato. */
|
|
366
368
|
columns?: ActionListColumn<TItem>[];
|
|
367
369
|
/** Células custom POR CIMA das colunas do contrato (chave = column.key). */
|
|
@@ -610,19 +612,18 @@ function FilterField({
|
|
|
610
612
|
);
|
|
611
613
|
const runtimeOptions =
|
|
612
614
|
lookup.onSearch !== undefined && lookup.query !== ""
|
|
613
|
-
? base.filter(
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
.
|
|
617
|
-
|
|
615
|
+
? base.filter(
|
|
616
|
+
(option) =>
|
|
617
|
+
selectedValues.has(option.value) ||
|
|
618
|
+
option.label
|
|
619
|
+
.toLocaleLowerCase()
|
|
620
|
+
.includes(lookup.query.toLocaleLowerCase()),
|
|
618
621
|
)
|
|
619
622
|
: base;
|
|
620
623
|
const selectedResolvedOptions = [...selectedValues]
|
|
621
624
|
.map((selectedValue) => resolvedOptionByValue.current.get(selectedValue))
|
|
622
625
|
.filter((option): option is SelectOption => option !== undefined);
|
|
623
|
-
const mergeUniqueOptions = (
|
|
624
|
-
...groups: SelectOption[][]
|
|
625
|
-
): SelectOption[] => {
|
|
626
|
+
const mergeUniqueOptions = (...groups: SelectOption[][]): SelectOption[] => {
|
|
626
627
|
const merged = new Map<string, SelectOption>();
|
|
627
628
|
for (const group of groups) {
|
|
628
629
|
for (const option of group) {
|
|
@@ -1781,6 +1782,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1781
1782
|
input,
|
|
1782
1783
|
layout: layoutProp,
|
|
1783
1784
|
toolbarActions,
|
|
1785
|
+
viewControls,
|
|
1784
1786
|
columns,
|
|
1785
1787
|
cells,
|
|
1786
1788
|
filterOptions,
|
|
@@ -1898,6 +1900,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1898
1900
|
filterSpecs.length > 0 ||
|
|
1899
1901
|
periods.length > 0 ||
|
|
1900
1902
|
hasSegment ||
|
|
1903
|
+
viewControls != null ||
|
|
1901
1904
|
hasTable;
|
|
1902
1905
|
|
|
1903
1906
|
// — Input efetivo: escopo base + filtros preenchidos + período (→ from/to) + q + sort —
|
|
@@ -2152,9 +2155,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2152
2155
|
return label === null ? [] : [{ value: field, label }];
|
|
2153
2156
|
});
|
|
2154
2157
|
const activeSortLabel =
|
|
2155
|
-
activeSort === null
|
|
2156
|
-
? null
|
|
2157
|
-
: sortLabelFor(activeSort.field);
|
|
2158
|
+
activeSort === null ? null : sortLabelFor(activeSort.field);
|
|
2158
2159
|
const activeSortDirection =
|
|
2159
2160
|
activeSort?.dir === "asc" ? "crescente" : "decrescente";
|
|
2160
2161
|
const sortCustomized =
|
|
@@ -2219,35 +2220,6 @@ export function ActionList<TInput, TItem>({
|
|
|
2219
2220
|
refreshing={isLoading}
|
|
2220
2221
|
controls={
|
|
2221
2222
|
<>
|
|
2222
|
-
{hasSegment && (
|
|
2223
|
-
<ToggleGroup
|
|
2224
|
-
type="single"
|
|
2225
|
-
variant="outline"
|
|
2226
|
-
data-slot="action-list-views"
|
|
2227
|
-
className="shrink-0"
|
|
2228
|
-
value={activeView}
|
|
2229
|
-
onValueChange={(key) => {
|
|
2230
|
-
// No single o Radix DESMARCA ao clicar no ativo (chega '') — o segment
|
|
2231
|
-
// de views sempre tem uma ativa, então o vazio não passa.
|
|
2232
|
-
if (key !== "")
|
|
2233
|
-
setState({
|
|
2234
|
-
...state,
|
|
2235
|
-
view: key === viewEntries[0]?.[0] ? null : key,
|
|
2236
|
-
});
|
|
2237
|
-
}}
|
|
2238
|
-
>
|
|
2239
|
-
{viewEntries.map(([key, label, icon]) => (
|
|
2240
|
-
<Tooltip key={key}>
|
|
2241
|
-
<TooltipTrigger asChild>
|
|
2242
|
-
<Toggle value={key} aria-label={label}>
|
|
2243
|
-
{icon ?? label}
|
|
2244
|
-
</Toggle>
|
|
2245
|
-
</TooltipTrigger>
|
|
2246
|
-
<TooltipContent>{label}</TooltipContent>
|
|
2247
|
-
</Tooltip>
|
|
2248
|
-
))}
|
|
2249
|
-
</ToggleGroup>
|
|
2250
|
-
)}
|
|
2251
2223
|
{/* Configuração de EXIBIÇÃO — sempre presente (em qualquer view): colunas quando a
|
|
2252
2224
|
tabela está ativa, itens por página sempre, e o que vier depois. */}
|
|
2253
2225
|
<Popover>
|
|
@@ -2292,7 +2264,11 @@ export function ActionList<TInput, TItem>({
|
|
|
2292
2264
|
<button
|
|
2293
2265
|
type="button"
|
|
2294
2266
|
onClick={() =>
|
|
2295
|
-
setState({
|
|
2267
|
+
setState({
|
|
2268
|
+
...state,
|
|
2269
|
+
sort: defaultSort,
|
|
2270
|
+
page: 1,
|
|
2271
|
+
})
|
|
2296
2272
|
}
|
|
2297
2273
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
|
2298
2274
|
>
|
|
@@ -2324,7 +2300,12 @@ export function ActionList<TInput, TItem>({
|
|
|
2324
2300
|
}
|
|
2325
2301
|
options={[
|
|
2326
2302
|
...(defaultSort === null
|
|
2327
|
-
? [
|
|
2303
|
+
? [
|
|
2304
|
+
{
|
|
2305
|
+
value: "__none__",
|
|
2306
|
+
label: "Sem ordenação",
|
|
2307
|
+
},
|
|
2308
|
+
]
|
|
2328
2309
|
: []),
|
|
2329
2310
|
...sortableFields,
|
|
2330
2311
|
]}
|
|
@@ -2468,6 +2449,36 @@ export function ActionList<TInput, TItem>({
|
|
|
2468
2449
|
</div>
|
|
2469
2450
|
</PopoverContent>
|
|
2470
2451
|
</Popover>
|
|
2452
|
+
{hasSegment && (
|
|
2453
|
+
<ToggleGroup
|
|
2454
|
+
type="single"
|
|
2455
|
+
variant="outline"
|
|
2456
|
+
data-slot="action-list-views"
|
|
2457
|
+
className="shrink-0"
|
|
2458
|
+
value={activeView}
|
|
2459
|
+
onValueChange={(key) => {
|
|
2460
|
+
// No single o Radix DESMARCA ao clicar no ativo (chega '') — o segment
|
|
2461
|
+
// de views sempre tem uma ativa, então o vazio não passa.
|
|
2462
|
+
if (key !== "")
|
|
2463
|
+
setState({
|
|
2464
|
+
...state,
|
|
2465
|
+
view: key === viewEntries[0]?.[0] ? null : key,
|
|
2466
|
+
});
|
|
2467
|
+
}}
|
|
2468
|
+
>
|
|
2469
|
+
{viewEntries.map(([key, label, icon]) => (
|
|
2470
|
+
<Tooltip key={key}>
|
|
2471
|
+
<TooltipTrigger asChild>
|
|
2472
|
+
<Toggle value={key} aria-label={label}>
|
|
2473
|
+
{icon ?? label}
|
|
2474
|
+
</Toggle>
|
|
2475
|
+
</TooltipTrigger>
|
|
2476
|
+
<TooltipContent>{label}</TooltipContent>
|
|
2477
|
+
</Tooltip>
|
|
2478
|
+
))}
|
|
2479
|
+
</ToggleGroup>
|
|
2480
|
+
)}
|
|
2481
|
+
{viewControls}
|
|
2471
2482
|
</>
|
|
2472
2483
|
}
|
|
2473
2484
|
actions={toolbarActions}
|
|
@@ -2838,10 +2849,9 @@ export function ActionList<TInput, TItem>({
|
|
|
2838
2849
|
// Encosta na última linha: a borda de cima é o fecho da tabela, não a de um
|
|
2839
2850
|
// bloco solto, então o rodapé não entra no `gap` da pilha.
|
|
2840
2851
|
// Altura: 2.5rem de piso (a mesma do `TableHead`) para a faixa não mudar de
|
|
2841
|
-
// tamanho quando o pager aparece — ele ocupa
|
|
2842
|
-
// O padding
|
|
2843
|
-
//
|
|
2844
|
-
// crescer —
|
|
2852
|
+
// tamanho quando o pager aparece — ele ocupa 2rem e cabe dentro do piso.
|
|
2853
|
+
// O padding vertical de 0.125rem acomoda o pager de 2rem e a borda dentro
|
|
2854
|
+
// do piso, mantendo a altura igual quando não há paginação.
|
|
2845
2855
|
const pages =
|
|
2846
2856
|
total !== undefined
|
|
2847
2857
|
? Math.max(1, Math.ceil(total / (state.limit ?? pageSize)))
|
|
@@ -2872,7 +2882,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2872
2882
|
total !== undefined && !isError && activeGroupBy !== null ? (
|
|
2873
2883
|
<div
|
|
2874
2884
|
data-slot="action-list-footer"
|
|
2875
|
-
className="flex min-h-10 flex-wrap items-center justify-between gap-3 border-t border-border px-3 py-
|
|
2885
|
+
className="flex min-h-10 flex-wrap items-center justify-between gap-3 border-t border-border px-3 py-0.5 text-xs text-muted-foreground"
|
|
2876
2886
|
>
|
|
2877
2887
|
{displaySummary}
|
|
2878
2888
|
<span className={cn(displaySummary === null && "ml-auto")}>
|
|
@@ -2882,7 +2892,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2882
2892
|
) : total !== undefined && !isError ? (
|
|
2883
2893
|
<div
|
|
2884
2894
|
data-slot="action-list-footer"
|
|
2885
|
-
className="grid min-h-10 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-3 border-t border-border px-3 py-
|
|
2895
|
+
className="grid min-h-10 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-3 border-t border-border px-3 py-0.5 text-xs text-muted-foreground"
|
|
2886
2896
|
>
|
|
2887
2897
|
<span className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">
|
|
2888
2898
|
<span className="whitespace-nowrap">
|
|
@@ -2893,7 +2903,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2893
2903
|
</span>
|
|
2894
2904
|
{pages > 1 && (
|
|
2895
2905
|
// A primitiva pública na escala densa do rodapé: setas no quadrado `icon-sm`,
|
|
2896
|
-
// números `
|
|
2906
|
+
// números `sm` que crescem com os dígitos (5726 e 5727 não se colam).
|
|
2897
2907
|
<Pagination className="mx-0 w-auto">
|
|
2898
2908
|
<PaginationContent className="gap-0.5">
|
|
2899
2909
|
<PaginationItem>
|
|
@@ -2915,7 +2925,8 @@ export function ActionList<TInput, TItem>({
|
|
|
2915
2925
|
<PaginationLink
|
|
2916
2926
|
page={n}
|
|
2917
2927
|
isActive={n === state.page}
|
|
2918
|
-
|
|
2928
|
+
size="sm"
|
|
2929
|
+
className="text-xs"
|
|
2919
2930
|
onClick={() => goTo(n)}
|
|
2920
2931
|
/>
|
|
2921
2932
|
</PaginationItem>
|
|
@@ -2977,9 +2988,7 @@ export function ActionList<TInput, TItem>({
|
|
|
2977
2988
|
className={cn(
|
|
2978
2989
|
"min-h-0",
|
|
2979
2990
|
layout === "fill" && "overflow-auto",
|
|
2980
|
-
busy &&
|
|
2981
|
-
items.length > 0 &&
|
|
2982
|
-
"pointer-events-none opacity-60",
|
|
2991
|
+
busy && items.length > 0 && "pointer-events-none opacity-60",
|
|
2983
2992
|
)}
|
|
2984
2993
|
>
|
|
2985
2994
|
{body}
|