@softize/opus 15.0.0 → 15.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/bin/lib/check.mjs +53 -10
- package/docs/adr/0004-page-content-state-is-composed.md +12 -2
- package/docs/adr/0010-page-header-owns-page-chrome.md +4 -0
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +70 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -6
- package/src/ui/components/patterns/list.tsx +29 -3
- package/src/ui/components/patterns/page.tsx +205 -20
- package/src/ui/components/patterns/surface-header.tsx +21 -4
- package/src/ui/docs/content/action-list.md +1 -1
- package/src/ui/docs/content/page.md +101 -27
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,53 @@ 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
|
+
## 15.1.0 — 2026-09-10
|
|
11
|
+
|
|
12
|
+
`PageShell` passa a coordenar o chrome persistente quando shell e rota conhecem partes diferentes da
|
|
13
|
+
página. O shell fornece a navegação e mantém uma barra de `3rem`; a `Page` descendente continua
|
|
14
|
+
declarando título, descrição, ações e estados. As ações aparecem na barra sem portal montado pelo
|
|
15
|
+
consumidor, enquanto título e descrição formam o novo `PageIntro` dentro do conteúdo.
|
|
16
|
+
|
|
17
|
+
Em um estado integral, `PageShell` permanece visível e somente `PageIntro` é ocultado. A composição
|
|
18
|
+
anterior de `Page`, `PageHeader` e `PageBody` não muda fora do shell. `PageActionsTarget` continua
|
|
19
|
+
disponível para workspaces imersivos que já possuem uma moldura própria.
|
|
20
|
+
|
|
21
|
+
**Migração opcional:** substitua barras paralelas e seletores que escondem slots por
|
|
22
|
+
`PageShell navigation={...}` ao redor da rota. A página interna pode continuar na forma curta; use
|
|
23
|
+
`PageIntro` e `PageBody` somente quando precisar da composição explícita.
|
|
24
|
+
|
|
25
|
+
## 15.0.1 — 2026-09-09
|
|
26
|
+
|
|
27
|
+
Correções sobre a 15.0.0, publicada horas antes, todas vindas da revisão dela. Nenhuma novidade de
|
|
28
|
+
API: nada entrou, saiu ou mudou de nome.
|
|
29
|
+
|
|
30
|
+
As ações de uma linha do `ActionList` só entram no `ButtonGroup` a partir de duas. Com uma só, o
|
|
31
|
+
grupo não é materializado: um `role="group"` sem nome por linha enchia a árvore de acessibilidade
|
|
32
|
+
sem informar nada, e o espaçamento não tinha o que espaçar. Fragmento devolvido pelo consumidor
|
|
33
|
+
conta como as ações que carrega, não como um filho só.
|
|
34
|
+
|
|
35
|
+
**Migração:** quem escreveu seletor contra o `[data-slot="button-group"]` da linha na 15.0.0
|
|
36
|
+
precisa mirar a célula. Com uma ação só, não há mais grupo.
|
|
37
|
+
|
|
38
|
+
A região introdutória do `PageHeader` — `PageBack` ou `PageNavigation` — passa a carregar
|
|
39
|
+
`data-slot="page-navigation"` também na apresentação em barra. Antes o marcador existia só no
|
|
40
|
+
header padrão, então um seletor de teste ou de CSS que funcionasse num não funcionava no outro.
|
|
41
|
+
|
|
42
|
+
`Page` deixa de emitir `min-h-0` e `min-h-full` ao mesmo tempo quando a barra encontra um estado
|
|
43
|
+
integral. O resultado dependia da ordem do CSS gerado; agora o estado integral declara a altura
|
|
44
|
+
que precisa e a barra só contém a rolagem quando não há estado.
|
|
45
|
+
|
|
46
|
+
`opus check` para de acrescentar um segundo diagnóstico sobre a forma de composição quando o
|
|
47
|
+
problema é uma propriedade removida. A prop já diz o que corrigir; a mensagem seguinte era
|
|
48
|
+
verdadeira e enganosa ao mesmo tempo.
|
|
49
|
+
|
|
50
|
+
A ADR 0004 ganha, no adendo, o motivo que sustenta a alternativa descartada — antes ela se apoiava
|
|
51
|
+
numa citação que não sustentava a conclusão — e a segunda consequência da decisão: `PageHeader`
|
|
52
|
+
desiste antes de validar os filhos, então um cabeçalho inválido só lança quando a página chega em
|
|
53
|
+
`ready` — o `opus check` continua pegando isso estaticamente. A guidance de UI e a doc de `Page`
|
|
54
|
+
passam a avisar que o retorno some junto com o cabeçalho e que a saída, nesse caso, é o `action` do
|
|
55
|
+
próprio `PageState`.
|
|
56
|
+
|
|
10
57
|
## 15.0.0 — 2026-09-09
|
|
11
58
|
|
|
12
59
|
`PageHeader` passa a oferecer `variant="bar"`, uma apresentação compacta da mesma região de título,
|
package/bin/lib/check.mjs
CHANGED
|
@@ -105,12 +105,13 @@ const UI_SURFACE_PAIRS = new Map([
|
|
|
105
105
|
// locais homônimos.
|
|
106
106
|
const UI_STRUCTURAL_PARENTS = new Map([
|
|
107
107
|
["PageHeader", new Set(["Page"])],
|
|
108
|
+
["PageIntro", new Set(["Page"])],
|
|
108
109
|
["PageBody", new Set(["Page"])],
|
|
109
|
-
["PageTitle", new Set(["PageHeader"])],
|
|
110
|
+
["PageTitle", new Set(["PageHeader", "PageIntro"])],
|
|
110
111
|
["PageBack", new Set(["PageHeader"])],
|
|
111
112
|
["PageNavigation", new Set(["PageHeader"])],
|
|
112
|
-
["PageDescription", new Set(["PageHeader"])],
|
|
113
|
-
["PageActions", new Set(["PageHeader"])],
|
|
113
|
+
["PageDescription", new Set(["PageHeader", "PageIntro"])],
|
|
114
|
+
["PageActions", new Set(["PageHeader", "PageIntro"])],
|
|
114
115
|
["ContentHeader", new Set(["Content"])],
|
|
115
116
|
["ContentBody", new Set(["Content"])],
|
|
116
117
|
["ContentTitle", new Set(["ContentHeader"])],
|
|
@@ -166,6 +167,7 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
166
167
|
"Page",
|
|
167
168
|
{
|
|
168
169
|
header: "PageHeader",
|
|
170
|
+
alternateHeader: "PageIntro",
|
|
169
171
|
body: "PageBody",
|
|
170
172
|
shorthand: new Set(["title", "description", "actions"]),
|
|
171
173
|
// O contador saiu do título da página (ADR 0009). Tirar `count` do shorthand não proíbe
|
|
@@ -187,6 +189,7 @@ const UI_STRUCTURAL_ROOTS = new Map([
|
|
|
187
189
|
|
|
188
190
|
const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
189
191
|
"PageHeader",
|
|
192
|
+
"PageIntro",
|
|
190
193
|
"PageBody",
|
|
191
194
|
"PageTitle",
|
|
192
195
|
"PageBack",
|
|
@@ -204,6 +207,8 @@ const UI_STRICT_DIRECT_COMPONENTS = new Set([
|
|
|
204
207
|
"AlertActions",
|
|
205
208
|
]);
|
|
206
209
|
|
|
210
|
+
const UI_STRUCTURAL_CONTAINERS = new Set(["PageShell"]);
|
|
211
|
+
|
|
207
212
|
const UI_STRUCTURAL_HEADERS = new Map([
|
|
208
213
|
[
|
|
209
214
|
"PageHeader",
|
|
@@ -219,6 +224,14 @@ const UI_STRUCTURAL_HEADERS = new Map([
|
|
|
219
224
|
shorthand: new Set(),
|
|
220
225
|
},
|
|
221
226
|
],
|
|
227
|
+
[
|
|
228
|
+
"PageIntro",
|
|
229
|
+
{
|
|
230
|
+
title: "PageTitle",
|
|
231
|
+
optional: new Set(["PageDescription", "PageActions"]),
|
|
232
|
+
shorthand: new Set(),
|
|
233
|
+
},
|
|
234
|
+
],
|
|
222
235
|
[
|
|
223
236
|
"ContentHeader",
|
|
224
237
|
{
|
|
@@ -793,6 +806,7 @@ function structuralJsxAncestorName(node, imports) {
|
|
|
793
806
|
if (
|
|
794
807
|
imported !== undefined &&
|
|
795
808
|
(UI_STRUCTURAL_ROOTS.has(imported) ||
|
|
809
|
+
UI_STRUCTURAL_CONTAINERS.has(imported) ||
|
|
796
810
|
UI_STRUCTURAL_PARENTS.has(imported) ||
|
|
797
811
|
[...UI_STRUCTURAL_PARENTS.values()].some((parents) =>
|
|
798
812
|
parents.has(imported),
|
|
@@ -805,6 +819,18 @@ function structuralJsxAncestorName(node, imports) {
|
|
|
805
819
|
return null;
|
|
806
820
|
}
|
|
807
821
|
|
|
822
|
+
function hasJsxAncestor(node, imports, component) {
|
|
823
|
+
let current = node.parent;
|
|
824
|
+
while (current !== undefined) {
|
|
825
|
+
if (ts.isJsxElement(current) || ts.isJsxSelfClosingElement(current)) {
|
|
826
|
+
const local = jsxLocalName(current);
|
|
827
|
+
if (local !== null && imports.get(local) === component) return true;
|
|
828
|
+
}
|
|
829
|
+
current = current.parent;
|
|
830
|
+
}
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
|
|
808
834
|
function directStructuralChildren(node, imports) {
|
|
809
835
|
if (!ts.isJsxElement(node)) return { names: [], hasOpaque: false };
|
|
810
836
|
const names = [];
|
|
@@ -950,8 +976,10 @@ export function checkUiStructure(file, text) {
|
|
|
950
976
|
: [],
|
|
951
977
|
),
|
|
952
978
|
);
|
|
979
|
+
let removedProp = false;
|
|
953
980
|
for (const [attribute, hint] of root.removed ?? []) {
|
|
954
981
|
if (attributes.has(attribute)) {
|
|
982
|
+
removedProp = true;
|
|
955
983
|
add(
|
|
956
984
|
node,
|
|
957
985
|
component,
|
|
@@ -964,25 +992,40 @@ export function checkUiStructure(file, text) {
|
|
|
964
992
|
attributes.has(attribute),
|
|
965
993
|
);
|
|
966
994
|
const children = directStructuralChildren(node, imports);
|
|
995
|
+
const insidePageShell =
|
|
996
|
+
component === "Page" && hasJsxAncestor(node, imports, "PageShell");
|
|
997
|
+
const acceptedHeaders = (
|
|
998
|
+
insidePageShell
|
|
999
|
+
? [root.alternateHeader]
|
|
1000
|
+
: [root.header, root.alternateHeader]
|
|
1001
|
+
).filter(Boolean);
|
|
967
1002
|
const structural =
|
|
968
|
-
children.names.includes(
|
|
1003
|
+
acceptedHeaders.some((header) => children.names.includes(header)) ||
|
|
969
1004
|
children.names.includes(root.body);
|
|
970
|
-
|
|
1005
|
+
// A prop removida saiu do `shorthand`, então sozinha ela não marca o nó como forma curta:
|
|
1006
|
+
// ele cai no ramo explícito, que cobra header e body — uma segunda mensagem inteiramente
|
|
1007
|
+
// induzida pela prop. Sem filhos estruturais essa cobrança dispara de qualquer jeito, e
|
|
1008
|
+
// some aqui.
|
|
1009
|
+
// Com filhos estruturais, o que as regras de forma acham é defeito independente: misturar
|
|
1010
|
+
// shorthand com slots, ou um filho inesperado, não some quando a prop sai.
|
|
1011
|
+
if (removedProp && !structural) {
|
|
1012
|
+
// Nada a acrescentar; os filhos seguem sendo visitados no fim de `visit`.
|
|
1013
|
+
} else if (shorthand && structural) {
|
|
971
1014
|
add(
|
|
972
1015
|
node,
|
|
973
1016
|
component,
|
|
974
|
-
`${component} não permite misturar propriedades de shorthand com ${
|
|
1017
|
+
`${component} não permite misturar propriedades de shorthand com ${acceptedHeaders.join("/")}/${root.body}.`,
|
|
975
1018
|
"ui-structure-mode",
|
|
976
1019
|
);
|
|
977
1020
|
} else if (!shorthand) {
|
|
978
|
-
const headers = children.names.filter(
|
|
979
|
-
(name)
|
|
1021
|
+
const headers = children.names.filter((name) =>
|
|
1022
|
+
acceptedHeaders.includes(name),
|
|
980
1023
|
).length;
|
|
981
1024
|
const bodies = children.names.filter(
|
|
982
1025
|
(name) => name === root.body,
|
|
983
1026
|
).length;
|
|
984
1027
|
const unexpected = children.names.some(
|
|
985
|
-
(name) => name
|
|
1028
|
+
(name) => !acceptedHeaders.includes(name) && name !== root.body,
|
|
986
1029
|
);
|
|
987
1030
|
if (
|
|
988
1031
|
headers !== 1 ||
|
|
@@ -993,7 +1036,7 @@ export function checkUiStructure(file, text) {
|
|
|
993
1036
|
add(
|
|
994
1037
|
node,
|
|
995
1038
|
component,
|
|
996
|
-
`${component} explícito exige exatamente um ${
|
|
1039
|
+
`${component} explícito exige exatamente um ${acceptedHeaders.join(" ou ")} e um ${root.body} como filhos diretos.`,
|
|
997
1040
|
);
|
|
998
1041
|
}
|
|
999
1042
|
}
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
> estados. Ela foi invertida: `PageState` passa a ocultá-lo em `loading`, `error` e `empty`, e a
|
|
5
5
|
> restaurá-lo em `ready`. A lista de responsabilidades abaixo já descreve o comportamento novo; o
|
|
6
6
|
> adendo no fim deste arquivo registra o motivo, a alternativa descartada e o que se perde.
|
|
7
|
+
>
|
|
8
|
+
> **Atualização (2026-09-10).** Dentro de `PageShell`, o estado integral oculta somente
|
|
9
|
+
> `PageIntro`. A barra pertence à moldura persistente e continua visível (ADR 0011).
|
|
7
10
|
|
|
8
11
|
## Contexto
|
|
9
12
|
|
|
@@ -90,10 +93,17 @@ superfície de estado carrega `role="heading"` com `aria-level={1}`.
|
|
|
90
93
|
|
|
91
94
|
**Alternativa descartada.** Ocultar apenas título, descrição e ações, preservando a região
|
|
92
95
|
introdutória. Foi descartada para esta versão porque partiria o cabeçalho em duas regras de
|
|
93
|
-
visibilidade
|
|
94
|
-
|
|
96
|
+
visibilidade — uma para o retorno, outra para o resto —, e um cabeçalho que aparece pela metade
|
|
97
|
+
é mais difícil de prever do que um que some inteiro. Não é uma decisão confortável: ver
|
|
98
|
+
"O que se perde".
|
|
95
99
|
|
|
96
100
|
**O que se perde, e é conhecido.** Some junto o `PageBack`, então uma subpágina em erro fica sem o
|
|
97
101
|
retorno in-page para o pai — justamente quando a pessoa mais precisa sair. Enquanto esta ADR não
|
|
98
102
|
for revista, uma subpágina que dependa desse retorno oferece a saída pelo `action` do próprio
|
|
99
103
|
`PageState`. Reavaliar se o custo aparecer em uso real.
|
|
104
|
+
|
|
105
|
+
Uma segunda consequência é da mesma decisão: como `PageHeader` desiste antes de validar seus
|
|
106
|
+
filhos, um cabeçalho estruturalmente inválido — dois `PageBack`, ou sem `PageTitle` — deixa de
|
|
107
|
+
lançar enquanto a página está em estado integral, e só lança quando ela chega em `ready`. O
|
|
108
|
+
`opus check` continua pegando isso estaticamente, então o erro não passa despercebido até
|
|
109
|
+
produção; o que muda é o momento em que aparece no desenvolvimento.
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
- **Data:** 2026-09-09.
|
|
5
5
|
- **Complementa:** ADR 0005.
|
|
6
6
|
|
|
7
|
+
> **Atualização (2026-09-10).** A ADR 0011 acrescenta `PageShell` para o caso em que shell e rota
|
|
8
|
+
> conhecem partes diferentes da mesma página. A barra continua sendo a apresentação compacta do
|
|
9
|
+
> cabeçalho, mas o título do conteúdo passa a `PageIntro` e as ações são coordenadas pelo Opus.
|
|
10
|
+
|
|
7
11
|
## Contexto
|
|
8
12
|
|
|
9
13
|
Aplicações com sidebar passaram a montar uma barra superior separada de `PageHeader` para reunir
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# ADR 0011 — PageShell coordena o chrome persistente da página
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-09-10.
|
|
5
|
+
- **Revisa:** ADR 0010.
|
|
6
|
+
|
|
7
|
+
## Contexto
|
|
8
|
+
|
|
9
|
+
A ADR 0010 reuniu título, navegação e ações em `PageHeader`, mas partiu do pressuposto de que uma
|
|
10
|
+
única camada conhece todos esses elementos. Em aplicações com navegação persistente, o shell conhece
|
|
11
|
+
o breadcrumb e a moldura enquanto a rota conhece o título, as ações e o estado do conteúdo.
|
|
12
|
+
|
|
13
|
+
Sem um contrato para essa divisão, consumidores criam uma barra com `PaneHeader`, projetam ações
|
|
14
|
+
manualmente e escondem partes do `PageHeader` com CSS. O resultado parece correto, mas mantém duas
|
|
15
|
+
anatomias concorrentes e faz um estado integral remover também a navegação persistente do shell.
|
|
16
|
+
|
|
17
|
+
## Decisão
|
|
18
|
+
|
|
19
|
+
`PageShell` representa a moldura persistente de uma página dentro de um shell de aplicação. Ele
|
|
20
|
+
renderiza a apresentação em barra de `PageHeader`, recebe a navegação conhecida pelo shell e oferece
|
|
21
|
+
o alvo canônico para ações declaradas pela `Page` descendente.
|
|
22
|
+
|
|
23
|
+
Quando uma `Page` está dentro de `PageShell`, sua forma curta transforma título e descrição em
|
|
24
|
+
`PageIntro`, dentro do conteúdo. `PageActions` continua declarado pela página, mas aparece na barra.
|
|
25
|
+
Na forma explícita, a página usa `PageIntro` e `PageBody`. Fora de `PageShell`, a composição anterior
|
|
26
|
+
com `PageHeader` continua válida; `PageIntro` também pode ser escolhido quando o conteúdo precisa de
|
|
27
|
+
uma introdução sem navegação própria. As duas regiões não são equivalentes: `PageHeader` reúne o
|
|
28
|
+
cabeçalho completo, enquanto `PageIntro` dá mais presença ao título dentro do conteúdo.
|
|
29
|
+
|
|
30
|
+
`PageState` oculta somente `PageIntro`. A barra do `PageShell` permanece visível porque preserva
|
|
31
|
+
navegação e ações globais mesmo quando o conteúdo carrega, falha ou está vazio. Uma ação que depende
|
|
32
|
+
do conteúdo deve se omitir por estado na própria rota.
|
|
33
|
+
|
|
34
|
+
`PageShell` não substitui o shell completo da aplicação, não inclui sidebar e não cria navegação.
|
|
35
|
+
Ele apenas coordena a barra e a área em que uma única `Page` é renderizada.
|
|
36
|
+
|
|
37
|
+
## Consequências
|
|
38
|
+
|
|
39
|
+
- shell e rota podem continuar conhecendo partes diferentes da página sem criar componentes locais
|
|
40
|
+
de chrome;
|
|
41
|
+
- título e descrição deixam de ser mascarados por seletores globais;
|
|
42
|
+
- ações de página têm um único destino oficial na barra;
|
|
43
|
+
- estados integrais preservam a barra e escondem somente a introdução do conteúdo;
|
|
44
|
+
- a composição anterior de `Page` permanece compatível fora de `PageShell`;
|
|
45
|
+
- `PageActionsTarget` continua disponível apenas para workspaces imersivos que não usam
|
|
46
|
+
`PageShell`.
|
|
47
|
+
|
|
48
|
+
## Alternativas consideradas
|
|
49
|
+
|
|
50
|
+
### Passar breadcrumb a cada rota
|
|
51
|
+
|
|
52
|
+
Manteria toda a anatomia dentro de `Page`, mas faria cada página receber e repassar contexto que já
|
|
53
|
+
pertence ao shell. Também duplicaria essa infraestrutura em seções com navegação própria.
|
|
54
|
+
|
|
55
|
+
### Tornar o portal local um padrão da aplicação
|
|
56
|
+
|
|
57
|
+
Resolveria somente o GrandBrasil e deixaria o design system sem contrato para a mesma divisão de
|
|
58
|
+
responsabilidade em outros consumidores.
|
|
59
|
+
|
|
60
|
+
### Fazer a barra desaparecer em estados integrais
|
|
61
|
+
|
|
62
|
+
Repetiria o comportamento do header embutido, mas removeria navegação persistente por causa do
|
|
63
|
+
estado do conteúdo. A moldura do shell não deve oscilar com a consulta da rota.
|
|
64
|
+
|
|
65
|
+
## Verificação
|
|
66
|
+
|
|
67
|
+
- testes cobrem a projeção das ações, a permanência da barra e a ocultação de `PageIntro` em estados
|
|
68
|
+
integrais;
|
|
69
|
+
- a documentação demonstra a composição de `PageShell`, `PageIntro` e `Page` sem CSS externo;
|
|
70
|
+
- consumidores removem barras paralelas e `PageActionsTarget` ao adotar o novo contrato.
|
package/package.json
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
para o caso direto.
|
|
11
11
|
Não misturar as duas formas. Alterar `className` apenas quando a superfície tiver uma necessidade
|
|
12
12
|
real de largura; não reconstruir esse container em cada rota.
|
|
13
|
+
- Fora de `PageShell`, a composição explícita também pode trocar `PageHeader` por `PageIntro` quando
|
|
14
|
+
o conteúdo precisar somente de título, descrição e ações, sem retorno ou breadcrumb. `PageIntro`
|
|
15
|
+
dá mais presença ao título e não é uma abreviação visual de `PageHeader`.
|
|
13
16
|
- `PageHeader` é a única região de cabeçalho da página. O default acompanha o container;
|
|
14
17
|
`variant="bar"` apresenta a mesma anatomia como faixa compacta no topo. Em uma subpágina simples,
|
|
15
18
|
`PageBack` recebe o destino pai explícito: aparece acima do título no default e como icon-only com
|
|
@@ -17,14 +20,22 @@
|
|
|
17
20
|
`PageNavigation`. Não combine retorno e breadcrumb nem crie um chrome paralelo para uma `Page`.
|
|
18
21
|
Ações com texto na barra usam `Button size="sm"`; ações somente com ícone usam `icon-sm`.
|
|
19
22
|
`PageActionsTarget` fica reservado a workspaces imersivos que já possuam chrome próprio.
|
|
23
|
+
- Quando shell e rota conhecem partes diferentes da mesma página, use `PageShell` ao redor da rota.
|
|
24
|
+
O shell fornece `navigation`; a `Page` descendente continua declarando `title`, `description` e
|
|
25
|
+
`actions`. O Opus mantém a barra de `3rem`, projeta as ações nela e apresenta título e descrição
|
|
26
|
+
como `PageIntro` no conteúdo. Na forma explícita dentro do shell, use
|
|
27
|
+
`Page > PageIntro (PageTitle, PageDescription?, PageActions?) + PageBody`. Não monte `PaneHeader`,
|
|
28
|
+
portal ou seletor global para reconstruir essa composição.
|
|
20
29
|
- `PageState` substitui todo o conteúdo principal quando a página carrega, falha ou está vazia. Na
|
|
21
30
|
forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
|
|
22
|
-
explícita, fica sozinho dentro de `PageBody`. Nos três estados ativos, o cabeçalho
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
explícita, fica sozinho dentro de `PageBody`. Nos três estados ativos, o cabeçalho da Page isolada
|
|
32
|
+
some inteiro — incluindo o `PageBack`; dentro de `PageShell`, somente `PageIntro` some e a barra
|
|
33
|
+
permanece. O estado ocupa a área disponível e seu título assume o heading
|
|
34
|
+
principal, inclusive quando um componente intermediário renderiza o estado. Uma subpágina que
|
|
35
|
+
dependa do retorno ao pai oferece essa saída pelo `action` do próprio `PageState`. O erro mantém
|
|
36
|
+
`role="alert"`, usa a mesma composição central e sem moldura dos demais estados e apresenta a
|
|
37
|
+
recuperação como botão `outline` textual. Estados de seção ou coleção continuam em `DataState`,
|
|
38
|
+
`ActionView`, `ActionList` ou `Alert`; não elevar uma falha parcial a estado da página.
|
|
28
39
|
- `Content` delimita uma seção e segue a mesma anatomia: `Content > ContentHeader (ContentTitle,
|
|
29
40
|
ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader` nunca fica
|
|
30
41
|
solto. `title`, `description`, `count` e `actions` no `Content` são a abreviação para o caso
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
|
+
Children,
|
|
24
|
+
Fragment,
|
|
25
|
+
isValidElement,
|
|
23
26
|
useEffect,
|
|
24
27
|
useLayoutEffect,
|
|
25
28
|
useMemo,
|
|
@@ -1142,6 +1145,31 @@ export function ActionFilterBar({
|
|
|
1142
1145
|
// ActionList
|
|
1143
1146
|
// =============================================================================
|
|
1144
1147
|
|
|
1148
|
+
/**
|
|
1149
|
+
* Ações de uma linha da tabela. Com duas ou mais, agrupa e espaça; com uma só, não
|
|
1150
|
+
* materializa o `ButtonGroup` — um `role="group"` sem nome por linha enche a árvore de
|
|
1151
|
+
* acessibilidade sem informar nada, e `mode="spaced"` não teria o que espaçar.
|
|
1152
|
+
*/
|
|
1153
|
+
function RowActions({ children }: { children: ReactNode }) {
|
|
1154
|
+
// O consumidor costuma devolver as ações dentro de um fragment, que conta como UM filho —
|
|
1155
|
+
// e às vezes dentro de fragments aninhados. Expandir recursivamente, e contar só elementos,
|
|
1156
|
+
// evita tanto tratar duas ações como uma quanto deixar um espaço em branco virar ação.
|
|
1157
|
+
const expand = (node: ReactNode): ReactNode[] =>
|
|
1158
|
+
Children.toArray(node).flatMap((child) =>
|
|
1159
|
+
isValidElement(child) && child.type === Fragment
|
|
1160
|
+
? expand((child.props as { children?: ReactNode }).children)
|
|
1161
|
+
: [child],
|
|
1162
|
+
);
|
|
1163
|
+
const alone = expand(children).filter(isValidElement).length < 2;
|
|
1164
|
+
return alone ? (
|
|
1165
|
+
<div className="flex justify-end">{children}</div>
|
|
1166
|
+
) : (
|
|
1167
|
+
<ButtonGroup mode="spaced" className="ml-auto">
|
|
1168
|
+
{children}
|
|
1169
|
+
</ButtonGroup>
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1145
1173
|
export function ActionList<TInput, TItem>({
|
|
1146
1174
|
action,
|
|
1147
1175
|
input,
|
|
@@ -1634,9 +1662,7 @@ export function ActionList<TInput, TItem>({
|
|
|
1634
1662
|
className="w-px whitespace-nowrap text-right"
|
|
1635
1663
|
onClick={(e) => e.stopPropagation()}
|
|
1636
1664
|
>
|
|
1637
|
-
<
|
|
1638
|
-
{rowActions(item)}
|
|
1639
|
-
</ButtonGroup>
|
|
1665
|
+
<RowActions>{rowActions(item)}</RowActions>
|
|
1640
1666
|
</TableCell>
|
|
1641
1667
|
)}
|
|
1642
1668
|
</TableRow>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* <Page /> — esqueleto composto de página do back-office.
|
|
3
3
|
*
|
|
4
|
-
* A forma curta cobre o caso comum;
|
|
5
|
-
*
|
|
4
|
+
* A forma curta cobre o caso comum; PageShell coordena a barra persistente quando shell e rota
|
|
5
|
+
* conhecem partes diferentes. Todas preservam o container centralizado com teto de 80rem.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
@@ -39,9 +39,114 @@ interface PageContextValue {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const PageContext = createContext<PageContextValue | null>(null);
|
|
42
|
-
|
|
42
|
+
type PageHeadingRegion = PageHeaderVariant | "intro";
|
|
43
|
+
|
|
44
|
+
const PageHeaderContext = createContext<PageHeadingRegion | null>(null);
|
|
43
45
|
const PageIntegralStateContext = createContext(false);
|
|
44
46
|
const PageActionsTargetContext = createContext<HTMLElement | null>(null);
|
|
47
|
+
const PageShellContext = createContext(false);
|
|
48
|
+
const pageBarClassName =
|
|
49
|
+
"flex h-12 shrink-0 items-center border-b border-border bg-background text-foreground";
|
|
50
|
+
const pageBarContentClassName = "w-full py-2";
|
|
51
|
+
|
|
52
|
+
function PageShellNavigationSlot({
|
|
53
|
+
className,
|
|
54
|
+
...props
|
|
55
|
+
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
data-slot="page-shell-navigation"
|
|
59
|
+
className={cn("min-w-0 flex-1 overflow-hidden", className)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function PageShellActionsSlot({
|
|
66
|
+
targetRef,
|
|
67
|
+
className,
|
|
68
|
+
...props
|
|
69
|
+
}: HTMLAttributes<HTMLDivElement> & {
|
|
70
|
+
targetRef: (target: HTMLDivElement | null) => void;
|
|
71
|
+
}): ReactElement {
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
ref={targetRef}
|
|
75
|
+
data-slot="page-shell-actions"
|
|
76
|
+
className={cn("flex shrink-0 items-center gap-2", className)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PageShellProps extends Omit<
|
|
83
|
+
HTMLAttributes<HTMLDivElement>,
|
|
84
|
+
"children"
|
|
85
|
+
> {
|
|
86
|
+
/** Navegação contextual conhecida pelo shell, normalmente um Breadcrumb. */
|
|
87
|
+
navigation?: ReactNode;
|
|
88
|
+
/** Ações do shell ou do recurso que antecedem as ações declaradas pela Page. */
|
|
89
|
+
actions?: ReactNode;
|
|
90
|
+
/** Uma Page descendente, diretamente ou através da rota ativa. */
|
|
91
|
+
children: ReactNode;
|
|
92
|
+
/** Classes do container interno da barra. */
|
|
93
|
+
headerClassName?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Moldura persistente que coordena navegação do shell e ações da Page ativa. */
|
|
97
|
+
export function PageShell({
|
|
98
|
+
navigation,
|
|
99
|
+
actions,
|
|
100
|
+
children,
|
|
101
|
+
className,
|
|
102
|
+
headerClassName,
|
|
103
|
+
...props
|
|
104
|
+
}: PageShellProps): ReactElement {
|
|
105
|
+
const [actionsTarget, setActionsTarget] = useState<HTMLDivElement | null>(null);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
data-slot="page-shell"
|
|
110
|
+
className={cn(
|
|
111
|
+
"relative flex h-full min-h-0 min-w-0 flex-1 flex-col",
|
|
112
|
+
className,
|
|
113
|
+
)}
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
<SurfaceHeader
|
|
117
|
+
name="PageShell"
|
|
118
|
+
slot="page"
|
|
119
|
+
slots={{
|
|
120
|
+
leading: PageShellNavigationSlot,
|
|
121
|
+
title: PageTitle,
|
|
122
|
+
description: PageDescription,
|
|
123
|
+
actions: PageShellActionsSlot,
|
|
124
|
+
}}
|
|
125
|
+
leadingPlacement="inline"
|
|
126
|
+
titleRequired={false}
|
|
127
|
+
contentClassName={cn(
|
|
128
|
+
pageBarContentClassName,
|
|
129
|
+
"px-3",
|
|
130
|
+
headerClassName,
|
|
131
|
+
)}
|
|
132
|
+
className={pageBarClassName}
|
|
133
|
+
data-variant="bar"
|
|
134
|
+
>
|
|
135
|
+
<PageShellNavigationSlot>{navigation}</PageShellNavigationSlot>
|
|
136
|
+
<PageShellActionsSlot targetRef={setActionsTarget}>
|
|
137
|
+
{actions}
|
|
138
|
+
</PageShellActionsSlot>
|
|
139
|
+
</SurfaceHeader>
|
|
140
|
+
<PageShellContext.Provider value>
|
|
141
|
+
<PageActionsTarget target={actionsTarget}>
|
|
142
|
+
<div data-slot="page-shell-content" className="min-h-0 flex-1">
|
|
143
|
+
{children}
|
|
144
|
+
</div>
|
|
145
|
+
</PageActionsTarget>
|
|
146
|
+
</PageShellContext.Provider>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
45
150
|
|
|
46
151
|
export function PageActionsTarget({
|
|
47
152
|
target,
|
|
@@ -97,6 +202,7 @@ export function Page({
|
|
|
97
202
|
children,
|
|
98
203
|
...props
|
|
99
204
|
}: PageProps): ReactElement {
|
|
205
|
+
const insideShell = useContext(PageShellContext);
|
|
100
206
|
const shorthand = title !== undefined;
|
|
101
207
|
const nodes = Children.toArray(children);
|
|
102
208
|
const headers = nodes.filter(
|
|
@@ -105,6 +211,9 @@ export function Page({
|
|
|
105
211
|
const bodies = nodes.filter(
|
|
106
212
|
(node) => isValidElement(node) && node.type === PageBody,
|
|
107
213
|
);
|
|
214
|
+
const intros = nodes.filter(
|
|
215
|
+
(node) => isValidElement(node) && node.type === PageIntro,
|
|
216
|
+
);
|
|
108
217
|
const activeStateCount = useRef(0);
|
|
109
218
|
const [integralState, setIntegralState] = useState(false);
|
|
110
219
|
const registerIntegralState = useCallback(() => {
|
|
@@ -127,13 +236,22 @@ export function Page({
|
|
|
127
236
|
);
|
|
128
237
|
}
|
|
129
238
|
if (!shorthand) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
239
|
+
const validIntroComposition =
|
|
240
|
+
intros.length === 1 &&
|
|
241
|
+
headers.length === 0 &&
|
|
242
|
+
bodies.length === 1 &&
|
|
243
|
+
intros.length + bodies.length === nodes.length;
|
|
244
|
+
const validStandaloneComposition =
|
|
245
|
+
!insideShell &&
|
|
246
|
+
headers.length === 1 &&
|
|
247
|
+
intros.length === 0 &&
|
|
248
|
+
bodies.length === 1 &&
|
|
249
|
+
headers.length + bodies.length === nodes.length;
|
|
250
|
+
if (!validIntroComposition && !validStandaloneComposition) {
|
|
135
251
|
throw new Error(
|
|
136
|
-
|
|
252
|
+
insideShell
|
|
253
|
+
? "Page explícito dentro de PageShell exige exatamente um PageIntro e um PageBody como filhos diretos."
|
|
254
|
+
: "Page explícito exige exatamente um PageHeader ou PageIntro e um PageBody como filhos diretos.",
|
|
137
255
|
);
|
|
138
256
|
}
|
|
139
257
|
}
|
|
@@ -146,7 +264,18 @@ export function Page({
|
|
|
146
264
|
containerClassName: className,
|
|
147
265
|
};
|
|
148
266
|
|
|
149
|
-
const content = shorthand ? (
|
|
267
|
+
const content = shorthand && insideShell ? (
|
|
268
|
+
<>
|
|
269
|
+
<PageIntro>
|
|
270
|
+
<PageTitle>{title}</PageTitle>
|
|
271
|
+
{description !== undefined && (
|
|
272
|
+
<PageDescription>{description}</PageDescription>
|
|
273
|
+
)}
|
|
274
|
+
{actions !== undefined && <PageActions>{actions}</PageActions>}
|
|
275
|
+
</PageIntro>
|
|
276
|
+
<PageBody>{children}</PageBody>
|
|
277
|
+
</>
|
|
278
|
+
) : shorthand ? (
|
|
150
279
|
<>
|
|
151
280
|
<PageHeader>
|
|
152
281
|
<PageTitle>{title}</PageTitle>
|
|
@@ -169,8 +298,12 @@ export function Page({
|
|
|
169
298
|
data-slot="page"
|
|
170
299
|
className={cn(
|
|
171
300
|
"min-w-0 flex-1",
|
|
172
|
-
|
|
173
|
-
integralState && "flex
|
|
301
|
+
insideShell && "min-h-full",
|
|
302
|
+
(headerVariant === "bar" || integralState) && "flex flex-col",
|
|
303
|
+
// A barra contém a rolagem (`min-h-0`); o estado integral ocupa a altura
|
|
304
|
+
// disponível (`min-h-full`). Emitir os dois juntos deixava o resultado por
|
|
305
|
+
// conta da ordem do CSS gerado — no estado integral, quem manda é ele.
|
|
306
|
+
integralState ? "min-h-full" : headerVariant === "bar" && "min-h-0",
|
|
174
307
|
)}
|
|
175
308
|
{...props}
|
|
176
309
|
>
|
|
@@ -195,6 +328,47 @@ export function Page({
|
|
|
195
328
|
);
|
|
196
329
|
}
|
|
197
330
|
|
|
331
|
+
/** Introdução opcional do conteúdo, separada da navegação persistente do shell. */
|
|
332
|
+
export function PageIntro({
|
|
333
|
+
className,
|
|
334
|
+
children,
|
|
335
|
+
...props
|
|
336
|
+
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
337
|
+
const page = useContext(PageContext);
|
|
338
|
+
const insideShell = useContext(PageShellContext);
|
|
339
|
+
const integralState = useContext(PageIntegralStateContext);
|
|
340
|
+
requireParent(page !== null, "PageIntro", "Page");
|
|
341
|
+
if (integralState) {
|
|
342
|
+
if (!insideShell) return <></>;
|
|
343
|
+
const actions = Children.toArray(children).filter(
|
|
344
|
+
(node) => isValidElement(node) && node.type === PageActions,
|
|
345
|
+
);
|
|
346
|
+
return (
|
|
347
|
+
<PageHeaderContext.Provider value="intro">
|
|
348
|
+
{actions}
|
|
349
|
+
</PageHeaderContext.Provider>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<PageHeaderContext.Provider value="intro">
|
|
355
|
+
<SurfaceHeader
|
|
356
|
+
name="PageIntro"
|
|
357
|
+
slot="page-intro"
|
|
358
|
+
slots={{
|
|
359
|
+
title: PageTitle,
|
|
360
|
+
description: PageDescription,
|
|
361
|
+
actions: PageActions,
|
|
362
|
+
}}
|
|
363
|
+
className={className}
|
|
364
|
+
{...props}
|
|
365
|
+
>
|
|
366
|
+
{children}
|
|
367
|
+
</SurfaceHeader>
|
|
368
|
+
</PageHeaderContext.Provider>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
198
372
|
/** A anatomia é a de `SurfaceHeader`, a mesma de `ContentHeader`; só os slots mudam de nome. */
|
|
199
373
|
export interface PageHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
200
374
|
/** `default` fica no container; `bar` cria uma faixa compacta no topo da Page. */
|
|
@@ -225,12 +399,15 @@ export function PageHeader({
|
|
|
225
399
|
leadingPlacement={variant === "bar" ? "inline" : "above"}
|
|
226
400
|
contentClassName={
|
|
227
401
|
variant === "bar"
|
|
228
|
-
? cn(
|
|
402
|
+
? cn(
|
|
403
|
+
pageBarContentClassName,
|
|
404
|
+
"mx-auto max-w-7xl px-8",
|
|
405
|
+
page?.containerClassName,
|
|
406
|
+
)
|
|
229
407
|
: undefined
|
|
230
408
|
}
|
|
231
409
|
className={cn(
|
|
232
|
-
variant === "bar" &&
|
|
233
|
-
"shrink-0 border-b border-border bg-background text-foreground",
|
|
410
|
+
variant === "bar" && pageBarClassName,
|
|
234
411
|
className,
|
|
235
412
|
)}
|
|
236
413
|
{...props}
|
|
@@ -246,14 +423,16 @@ export function PageTitle({
|
|
|
246
423
|
...props
|
|
247
424
|
}: HTMLAttributes<HTMLHeadingElement>): ReactElement {
|
|
248
425
|
const variant = useContext(PageHeaderContext);
|
|
249
|
-
requireParent(variant !== null, "PageTitle", "PageHeader");
|
|
426
|
+
requireParent(variant !== null, "PageTitle", "PageHeader ou PageIntro");
|
|
250
427
|
return (
|
|
251
428
|
<h1
|
|
252
429
|
data-slot="page-title"
|
|
253
430
|
className={cn(
|
|
254
431
|
variant === "bar"
|
|
255
432
|
? "truncate text-sm font-semibold"
|
|
256
|
-
:
|
|
433
|
+
: variant === "intro"
|
|
434
|
+
? "text-3xl font-semibold tracking-tight"
|
|
435
|
+
: surfaceHeaderClasses.page.title,
|
|
257
436
|
className,
|
|
258
437
|
)}
|
|
259
438
|
{...props}
|
|
@@ -266,7 +445,11 @@ export function PageDescription({
|
|
|
266
445
|
...props
|
|
267
446
|
}: HTMLAttributes<HTMLParagraphElement>): ReactElement {
|
|
268
447
|
const variant = useContext(PageHeaderContext);
|
|
269
|
-
requireParent(
|
|
448
|
+
requireParent(
|
|
449
|
+
variant !== null,
|
|
450
|
+
"PageDescription",
|
|
451
|
+
"PageHeader ou PageIntro",
|
|
452
|
+
);
|
|
270
453
|
return (
|
|
271
454
|
<p
|
|
272
455
|
data-slot="page-description"
|
|
@@ -286,13 +469,14 @@ export function PageActions({
|
|
|
286
469
|
...props
|
|
287
470
|
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
288
471
|
const variant = useContext(PageHeaderContext);
|
|
289
|
-
requireParent(variant !== null, "PageActions", "PageHeader");
|
|
472
|
+
requireParent(variant !== null, "PageActions", "PageHeader ou PageIntro");
|
|
290
473
|
const target = useContext(PageActionsTargetContext);
|
|
474
|
+
const insideShell = useContext(PageShellContext);
|
|
291
475
|
const actions = (
|
|
292
476
|
<div
|
|
293
477
|
data-slot="page-actions"
|
|
294
478
|
className={cn(
|
|
295
|
-
variant === "bar"
|
|
479
|
+
variant === "bar" || insideShell
|
|
296
480
|
? "flex shrink-0 items-center gap-2"
|
|
297
481
|
: surfaceHeaderClasses.page.actions,
|
|
298
482
|
className,
|
|
@@ -300,6 +484,7 @@ export function PageActions({
|
|
|
300
484
|
{...props}
|
|
301
485
|
/>
|
|
302
486
|
);
|
|
487
|
+
if (insideShell && target === null) return <></>;
|
|
303
488
|
return target === null ? actions : createPortal(actions, target);
|
|
304
489
|
}
|
|
305
490
|
|
|
@@ -57,6 +57,8 @@ export interface SurfaceHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
57
57
|
leadingPlacement?: "above" | "inline";
|
|
58
58
|
/** Classes de um container interno quando a moldura precisa ocupar toda a largura. */
|
|
59
59
|
contentClassName?: string;
|
|
60
|
+
/** Permite uma superfície composta apenas pelas regiões leading/actions. */
|
|
61
|
+
titleRequired?: boolean;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
/** Expande fragments de primeiro nível: o shorthand monta os slots dentro de um `<>`. */
|
|
@@ -74,6 +76,7 @@ export function SurfaceHeader({
|
|
|
74
76
|
slots,
|
|
75
77
|
leadingPlacement = "above",
|
|
76
78
|
contentClassName,
|
|
79
|
+
titleRequired = true,
|
|
77
80
|
className,
|
|
78
81
|
children,
|
|
79
82
|
...props
|
|
@@ -100,7 +103,7 @@ export function SurfaceHeader({
|
|
|
100
103
|
actionSlots.length;
|
|
101
104
|
|
|
102
105
|
if (
|
|
103
|
-
titles.length !== 1 ||
|
|
106
|
+
(titleRequired ? titles.length !== 1 : titles.length > 1) ||
|
|
104
107
|
leading.length > 1 ||
|
|
105
108
|
counts.length > 1 ||
|
|
106
109
|
descriptions.length > 1 ||
|
|
@@ -130,11 +133,11 @@ export function SurfaceHeader({
|
|
|
130
133
|
throw new Error(
|
|
131
134
|
distinctLeading.size > 1
|
|
132
135
|
? `${name} aceita ${[...distinctLeading].map(label).join(" ou ")} na região introdutória, nunca os dois na mesma superfície.`
|
|
133
|
-
: `${name} exige um ${label(slots.title)} e aceita no máximo um de cada: ${optionalSlots}.`,
|
|
136
|
+
: `${name} ${titleRequired ? `exige um ${label(slots.title)}` : `aceita no máximo um ${label(slots.title)}`} e aceita no máximo um de cada: ${optionalSlots}.`,
|
|
134
137
|
);
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
const heading = (
|
|
140
|
+
const heading = titles.length + counts.length + descriptions.length > 0 && (
|
|
138
141
|
<div
|
|
139
142
|
data-slot={`${slot}-heading`}
|
|
140
143
|
className={cn("min-w-0", leadingPlacement === "inline" && "flex-1")}
|
|
@@ -146,9 +149,23 @@ export function SurfaceHeader({
|
|
|
146
149
|
{descriptions}
|
|
147
150
|
</div>
|
|
148
151
|
);
|
|
152
|
+
// A região introdutória carrega o mesmo `data-slot` nas duas apresentações: seletor de teste
|
|
153
|
+
// ou de CSS que funcione no header padrão precisa funcionar na barra.
|
|
149
154
|
const row = (
|
|
150
155
|
<>
|
|
151
|
-
{leadingPlacement === "inline" && leading
|
|
156
|
+
{leadingPlacement === "inline" && leading.length > 0 && (
|
|
157
|
+
// `min-w-0` aqui é o que deixa a trilha do PageNavigation ceder e truncar; sem ele o
|
|
158
|
+
// wrapper assume o min-content do breadcrumb e a compressão toda cai sobre o título.
|
|
159
|
+
<div
|
|
160
|
+
data-slot={`${slot}-navigation`}
|
|
161
|
+
className={cn(
|
|
162
|
+
"flex min-w-0 items-center",
|
|
163
|
+
!heading && "flex-1",
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
{leading}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
152
169
|
{heading}
|
|
153
170
|
{actionSlots}
|
|
154
171
|
</>
|
|
@@ -302,4 +302,4 @@ chamar a action.
|
|
|
302
302
|
| `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
|
|
303
303
|
| `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
|
|
304
304
|
| `toolbarActions` | `ReactNode` | | Ações do consumidor no fim da barra, separadas do grupo de controles. |
|
|
305
|
-
| `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita),
|
|
305
|
+
| `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita). A partir de duas, entram num grupo com intervalo compacto; uma só fica solta, sem grupo. Cliques ali não disparam o `onRowClick`. |
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
## Esqueleto de página
|
|
2
2
|
|
|
3
|
-
Use `Page` para manter
|
|
4
|
-
`PageHeader`
|
|
5
|
-
|
|
3
|
+
Use `Page` para manter título, ações, estado e conteúdo na mesma anatomia. Sozinha, ela apresenta
|
|
4
|
+
`PageHeader` dentro do container. Quando a aplicação possui uma barra persistente, envolva a rota
|
|
5
|
+
com `PageShell`: o shell fornece a navegação, a página fornece as ações e o título passa a
|
|
6
|
+
`PageIntro` dentro do conteúdo.
|
|
6
7
|
|
|
7
8
|
Em larguras amplas, as ações ficam no extremo oposto e acompanham a base do título e da descrição;
|
|
8
9
|
em larguras estreitas, passam para uma linha abaixo. O container é centralizado e ocupa a largura
|
|
9
10
|
disponível até `80rem` (`max-w-7xl`). Use `className` somente quando a composição pedir outro teto
|
|
10
11
|
ou largura total.
|
|
11
12
|
|
|
12
|
-
A forma curta é o padrão para páginas comuns.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
A forma curta é o padrão para páginas comuns. Fora de `PageShell`, ela cria `PageHeader` e
|
|
14
|
+
`PageBody`. Dentro dele, cria `PageIntro` e `PageBody`, enquanto envia as ações para a barra. A forma
|
|
15
|
+
explícita permite escolher a região adequada: `PageHeader` reúne navegação e contexto; `PageIntro`
|
|
16
|
+
dá mais presença ao título dentro do conteúdo. Contadores e outros indicadores pertencem ao
|
|
17
|
+
conteúdo que os explica.
|
|
16
18
|
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
`PageShell` não inclui sidebar nem inventa breadcrumb. Ele ocupa o painel principal já delimitado e
|
|
20
|
+
mantém a barra com `3rem`, mesmo quando o conteúdo muda de estado. Tabs ficam reservados a recortes
|
|
21
|
+
da mesma superfície. Canvas e outros workspaces espaciais imersivos podem usar um shell próprio.
|
|
20
22
|
|
|
21
23
|
Estados integrais de carregamento, falha ou ausência são compostos no body com `PageState`,
|
|
22
24
|
detalhado abaixo. `Page` não recebe flags de dados: uma página pode agregar fontes independentes e
|
|
@@ -63,13 +65,63 @@ Não combine `PageBack` com breadcrumb. Use o retorno para um único pai conheci
|
|
|
63
65
|
mais de um ancestral relevante, envolva o `Breadcrumb` em `PageNavigation`; ele ocupa a mesma
|
|
64
66
|
posição introdutória sem transformar a trilha em ação.
|
|
65
67
|
|
|
66
|
-
##
|
|
68
|
+
## Barra persistente do shell
|
|
67
69
|
|
|
68
|
-
Use `
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
Use `PageShell` quando o shell conhece o breadcrumb e a rota conhece título e ações. Não crie um
|
|
71
|
+
`PaneHeader` paralelo nem esconda slots de `Page` com CSS. O Opus projeta `PageActions` na barra e
|
|
72
|
+
transforma a introdução da forma curta em `PageIntro`.
|
|
73
|
+
|
|
74
|
+
```tsx preview col
|
|
75
|
+
<PageShell
|
|
76
|
+
navigation={
|
|
77
|
+
<Breadcrumb>
|
|
78
|
+
<BreadcrumbList>
|
|
79
|
+
<BreadcrumbItem>Vendas</BreadcrumbItem>
|
|
80
|
+
<BreadcrumbSeparator />
|
|
81
|
+
<BreadcrumbPage>Clientes</BreadcrumbPage>
|
|
82
|
+
</BreadcrumbList>
|
|
83
|
+
</Breadcrumb>
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
<Page
|
|
87
|
+
title="Clientes"
|
|
88
|
+
actions={
|
|
89
|
+
<Button size="sm">
|
|
90
|
+
<Plus /> Novo cliente
|
|
91
|
+
</Button>
|
|
92
|
+
}
|
|
93
|
+
>
|
|
94
|
+
Conteúdo da listagem.
|
|
95
|
+
</Page>
|
|
96
|
+
</PageShell>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A barra permanece visível durante `PageState`; somente a introdução desaparece. Se uma ação não
|
|
100
|
+
puder ser executada sem o conteúdo, a própria rota deve omiti-la naquele estado.
|
|
101
|
+
|
|
102
|
+
Na composição explícita dentro do shell, use `PageIntro` e `PageBody`:
|
|
103
|
+
|
|
104
|
+
```tsx preview col
|
|
105
|
+
<PageShell navigation={<Breadcrumb>...</Breadcrumb>}>
|
|
106
|
+
<Page>
|
|
107
|
+
<PageIntro>
|
|
108
|
+
<PageTitle>Clientes</PageTitle>
|
|
109
|
+
<PageDescription>Cadastros disponíveis para atendimento.</PageDescription>
|
|
110
|
+
<PageActions>
|
|
111
|
+
<Button size="sm">Novo cliente</Button>
|
|
112
|
+
</PageActions>
|
|
113
|
+
</PageIntro>
|
|
114
|
+
<PageBody>Conteúdo da listagem.</PageBody>
|
|
115
|
+
</Page>
|
|
116
|
+
</PageShell>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Cabeçalho em barra da própria Page
|
|
120
|
+
|
|
121
|
+
Use `PageHeader variant="bar"` quando uma página autocontida conhecer título, retorno e ações. Essa
|
|
122
|
+
forma continua útil fora de um shell persistente. `PageBack` vira icon-only e recebe tooltip e nome
|
|
123
|
+
acessível “Voltar para {destino}”. Use `sm` em botões com texto e `icon-sm` em botões somente com
|
|
124
|
+
ícone.
|
|
73
125
|
|
|
74
126
|
```tsx preview col
|
|
75
127
|
<Page className="max-w-none">
|
|
@@ -86,11 +138,8 @@ botões que exibem somente um ícone.
|
|
|
86
138
|
</Page>
|
|
87
139
|
```
|
|
88
140
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`PageActionsTarget` continua disponível para um workspace imersivo que já possua um chrome próprio.
|
|
93
|
-
Ele projeta somente `PageActions` no elemento informado; não cria uma segunda região de cabeçalho.
|
|
141
|
+
Não aninhe essa variante em `PageShell`, que já possui a barra. `PageActionsTarget` continua
|
|
142
|
+
disponível para um workspace imersivo que tenha chrome próprio e não use `PageShell`.
|
|
94
143
|
|
|
95
144
|
## Composição explícita
|
|
96
145
|
|
|
@@ -118,15 +167,21 @@ render(
|
|
|
118
167
|
);
|
|
119
168
|
```
|
|
120
169
|
|
|
170
|
+
Use `PageIntro` no lugar de `PageHeader` quando a página precisar apenas de uma introdução no
|
|
171
|
+
conteúdo, sem navegação própria. Dentro de `PageShell`, essa é a única composição explícita válida;
|
|
172
|
+
fora dele, as duas formas são aceitas porque cumprem papéis diferentes.
|
|
173
|
+
|
|
121
174
|
## Estados integrais
|
|
122
175
|
|
|
123
176
|
Use `PageState` quando carregamento, falha ou ausência substituírem todo o conteúdo principal. Na
|
|
124
177
|
forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
|
|
125
178
|
explícita, coloque-o sozinho dentro de `PageBody`. Enquanto `status` for `loading`, `error` ou
|
|
126
|
-
`empty`, `Page` oculta o cabeçalho
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
179
|
+
`empty`, uma `Page` isolada oculta o cabeçalho inteiro — incluindo `PageBack` — e o estado ocupa a
|
|
180
|
+
altura disponível. Dentro de `PageShell`, a barra persistente permanece e somente `PageIntro` é
|
|
181
|
+
ocultado. Uma subpágina isolada que dependa do retorno oferece a saída pelo `action` do próprio
|
|
182
|
+
`PageState`. O título do estado assume o heading principal. Esse registro também funciona quando um
|
|
183
|
+
componente intermediário decide qual `PageState` renderizar. Em `ready`, o cabeçalho ou a introdução
|
|
184
|
+
e o conteúdo voltam à composição normal.
|
|
130
185
|
|
|
131
186
|
```tsx preview col
|
|
132
187
|
<Page title="Relatório">
|
|
@@ -171,6 +226,25 @@ estado na própria seção com `DataState`, `ActionView`, `ActionList` ou `Alert
|
|
|
171
226
|
| `className` | `string` | `max-w-7xl` | Classes do container para substituir o teto padrão de `80rem`. |
|
|
172
227
|
| `children` | `ReactNode` | | O body da página — espaçamento e diagramação são seus. |
|
|
173
228
|
|
|
229
|
+
## Propriedades de PageShell
|
|
230
|
+
|
|
231
|
+
| Propriedade | Tipo | Descrição |
|
|
232
|
+
| ----------------- | ----------------------------- | ---------------------------------------------------------------------- |
|
|
233
|
+
| `navigation` | `ReactNode` | Navegação contextual da barra, normalmente um `Breadcrumb`. |
|
|
234
|
+
| `actions` | `ReactNode` | Ações conhecidas pelo shell, antes das ações declaradas pela `Page`. |
|
|
235
|
+
| `children` | `ReactNode` | Rota que renderiza uma `Page` descendente. |
|
|
236
|
+
| `headerClassName` | `string` | Classes adicionais do container interno da barra. |
|
|
237
|
+
| `className` | `string` | Classes adicionais da moldura. |
|
|
238
|
+
| demais | Atributos de `HTMLDivElement` | Atributos nativos repassados à moldura. |
|
|
239
|
+
|
|
240
|
+
## Propriedades de PageIntro
|
|
241
|
+
|
|
242
|
+
| Propriedade | Tipo | Descrição |
|
|
243
|
+
| ----------- | ----------------------------- | ---------------------------------------------------------------- |
|
|
244
|
+
| `children` | `ReactNode` | Um `PageTitle` e, opcionalmente, descrição e ações da página. |
|
|
245
|
+
| `className` | `string` | Classes adicionais da região introdutória. |
|
|
246
|
+
| demais | Atributos de `HTMLDivElement` | Atributos nativos repassados à região introdutória. |
|
|
247
|
+
|
|
174
248
|
## Propriedades de PageHeader
|
|
175
249
|
|
|
176
250
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -201,7 +275,7 @@ estado na própria seção com `DataState`, `ActionView`, `ActionList` ou `Alert
|
|
|
201
275
|
|
|
202
276
|
| Propriedade | Tipo | Descrição |
|
|
203
277
|
| ----------- | --------------------------------- | ------------------------------------------------------ |
|
|
204
|
-
| `children` | `ReactNode` | Título principal `h1`; compacto na
|
|
278
|
+
| `children` | `ReactNode` | Título principal `h1`; compacto na barra e ampliado em `PageIntro`. |
|
|
205
279
|
| `className` | `string` | Classes adicionais do título. |
|
|
206
280
|
| demais | Atributos de `HTMLHeadingElement` | Atributos nativos repassados ao heading. |
|
|
207
281
|
|
|
@@ -244,7 +318,7 @@ estado na própria seção com `DataState`, `ActionView`, `ActionList` ou `Alert
|
|
|
244
318
|
| `title` | `ReactNode` | Texto seguro por estado | Situação reconhecível pela pessoa. |
|
|
245
319
|
| `description` | `ReactNode` | | Impacto ou próximo passo aplicável. |
|
|
246
320
|
| `icon` | `ReactNode` | Alerta no erro | Ícone decorativo do estado. |
|
|
247
|
-
| `action` | `ReactNode` | | Seleção ou
|
|
321
|
+
| `action` | `ReactNode` | | Seleção, criação ou saída aplicável ao estado — inclusive o retorno ao pai, já que o cabeçalho está oculto. |
|
|
248
322
|
| `emptyMessage` | `string` | `'Nada por aqui'` | Título do vazio quando `title` não é informado. |
|
|
249
323
|
| `errorMessage` | `string` | `'Não foi possível carregar esta página'` | Título do erro quando `title` não é informado. |
|
|
250
324
|
| `onRetry` | `() => void \| Promise<void>` | | Recuperação do erro: acrescenta um botão `outline` textual ao lado de `action`. |
|
package/src/ui/meta.ts
CHANGED
|
@@ -400,7 +400,7 @@ export const componentMeta = {
|
|
|
400
400
|
name: "page",
|
|
401
401
|
ancestry: "opus",
|
|
402
402
|
whenToUse:
|
|
403
|
-
"O esqueleto de página do back-office: <main> + container centralizado com teto padrão de 80rem (`max-w-7xl`). O shorthand cobre título, descrição e ações
|
|
403
|
+
"O esqueleto de página do back-office: <main> + container centralizado com teto padrão de 80rem (`max-w-7xl`). O shorthand cobre título, descrição e ações. Quando shell e rota conhecem partes diferentes da página, PageShell mantém a barra de 3rem, recebe a navegação e projeta as ações da Page; título e descrição formam PageIntro no conteúdo. Fora dele, a forma explícita escolhe PageHeader para o cabeçalho completo ou PageIntro para uma introdução sem navegação; `PageHeader variant=\"bar\"` atende uma página autocontida. PageActionsTarget fica restrito a workspaces imersivos sem PageShell. PageState oculta o header da Page isolada ou somente PageIntro dentro de PageShell. Para uma região disponível à criação ou vínculo, use Empty.",
|
|
404
404
|
},
|
|
405
405
|
router: {
|
|
406
406
|
name: "router",
|
package/src/ui/react.tsx
CHANGED
|
@@ -421,8 +421,10 @@ export type { DataStateProps } from "./components/patterns/data-state.tsx";
|
|
|
421
421
|
|
|
422
422
|
// Esqueleto de página do back-office (main + container + header título/descrição/ação).
|
|
423
423
|
export {
|
|
424
|
+
PageShell,
|
|
424
425
|
Page,
|
|
425
426
|
PageHeader,
|
|
427
|
+
PageIntro,
|
|
426
428
|
PageNavigation,
|
|
427
429
|
PageBack,
|
|
428
430
|
PageTitle,
|
|
@@ -433,6 +435,7 @@ export {
|
|
|
433
435
|
} from "./components/patterns/page.tsx";
|
|
434
436
|
export type {
|
|
435
437
|
PageProps,
|
|
438
|
+
PageShellProps,
|
|
436
439
|
PageBackProps,
|
|
437
440
|
PageHeaderProps,
|
|
438
441
|
PageHeaderVariant,
|