@softize/opus 15.0.1 → 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 +15 -0
- package/bin/lib/check.mjs +42 -9
- package/docs/adr/0004-page-content-state-is-composed.md +3 -0
- 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 +12 -2
- package/src/ui/components/patterns/page.tsx +200 -18
- package/src/ui/components/patterns/surface-header.tsx +13 -4
- package/src/ui/docs/content/page.md +99 -26
- package/src/ui/meta.ts +1 -1
- package/src/ui/react.tsx +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ 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
|
+
|
|
10
25
|
## 15.0.1 — 2026-09-09
|
|
11
26
|
|
|
12
27
|
Correções sobre a 15.0.0, publicada horas antes, todas vindas da revisão dela. Nenhuma novidade de
|
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 = [];
|
|
@@ -966,8 +992,15 @@ export function checkUiStructure(file, text) {
|
|
|
966
992
|
attributes.has(attribute),
|
|
967
993
|
);
|
|
968
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);
|
|
969
1002
|
const structural =
|
|
970
|
-
children.names.includes(
|
|
1003
|
+
acceptedHeaders.some((header) => children.names.includes(header)) ||
|
|
971
1004
|
children.names.includes(root.body);
|
|
972
1005
|
// A prop removida saiu do `shorthand`, então sozinha ela não marca o nó como forma curta:
|
|
973
1006
|
// ele cai no ramo explícito, que cobra header e body — uma segunda mensagem inteiramente
|
|
@@ -981,18 +1014,18 @@ export function checkUiStructure(file, text) {
|
|
|
981
1014
|
add(
|
|
982
1015
|
node,
|
|
983
1016
|
component,
|
|
984
|
-
`${component} não permite misturar propriedades de shorthand com ${
|
|
1017
|
+
`${component} não permite misturar propriedades de shorthand com ${acceptedHeaders.join("/")}/${root.body}.`,
|
|
985
1018
|
"ui-structure-mode",
|
|
986
1019
|
);
|
|
987
1020
|
} else if (!shorthand) {
|
|
988
|
-
const headers = children.names.filter(
|
|
989
|
-
(name)
|
|
1021
|
+
const headers = children.names.filter((name) =>
|
|
1022
|
+
acceptedHeaders.includes(name),
|
|
990
1023
|
).length;
|
|
991
1024
|
const bodies = children.names.filter(
|
|
992
1025
|
(name) => name === root.body,
|
|
993
1026
|
).length;
|
|
994
1027
|
const unexpected = children.names.some(
|
|
995
|
-
(name) => name
|
|
1028
|
+
(name) => !acceptedHeaders.includes(name) && name !== root.body,
|
|
996
1029
|
);
|
|
997
1030
|
if (
|
|
998
1031
|
headers !== 1 ||
|
|
@@ -1003,7 +1036,7 @@ export function checkUiStructure(file, text) {
|
|
|
1003
1036
|
add(
|
|
1004
1037
|
node,
|
|
1005
1038
|
component,
|
|
1006
|
-
`${component} explícito exige exatamente um ${
|
|
1039
|
+
`${component} explícito exige exatamente um ${acceptedHeaders.join(" ou ")} e um ${root.body} como filhos diretos.`,
|
|
1007
1040
|
);
|
|
1008
1041
|
}
|
|
1009
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
|
|
|
@@ -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,10 +20,17 @@
|
|
|
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
|
-
incluindo o `PageBack
|
|
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
|
|
24
34
|
principal, inclusive quando um componente intermediário renderiza o estado. Uma subpágina que
|
|
25
35
|
dependa do retorno ao pai oferece essa saída pelo `action` do próprio `PageState`. O erro mantém
|
|
26
36
|
`role="alert"`, usa a mesma composição central e sem moldura dos demais estados e apresenta a
|
|
@@ -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,6 +298,7 @@ export function Page({
|
|
|
169
298
|
data-slot="page"
|
|
170
299
|
className={cn(
|
|
171
300
|
"min-w-0 flex-1",
|
|
301
|
+
insideShell && "min-h-full",
|
|
172
302
|
(headerVariant === "bar" || integralState) && "flex flex-col",
|
|
173
303
|
// A barra contém a rolagem (`min-h-0`); o estado integral ocupa a altura
|
|
174
304
|
// disponível (`min-h-full`). Emitir os dois juntos deixava o resultado por
|
|
@@ -198,6 +328,47 @@ export function Page({
|
|
|
198
328
|
);
|
|
199
329
|
}
|
|
200
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
|
+
|
|
201
372
|
/** A anatomia é a de `SurfaceHeader`, a mesma de `ContentHeader`; só os slots mudam de nome. */
|
|
202
373
|
export interface PageHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
203
374
|
/** `default` fica no container; `bar` cria uma faixa compacta no topo da Page. */
|
|
@@ -228,12 +399,15 @@ export function PageHeader({
|
|
|
228
399
|
leadingPlacement={variant === "bar" ? "inline" : "above"}
|
|
229
400
|
contentClassName={
|
|
230
401
|
variant === "bar"
|
|
231
|
-
? cn(
|
|
402
|
+
? cn(
|
|
403
|
+
pageBarContentClassName,
|
|
404
|
+
"mx-auto max-w-7xl px-8",
|
|
405
|
+
page?.containerClassName,
|
|
406
|
+
)
|
|
232
407
|
: undefined
|
|
233
408
|
}
|
|
234
409
|
className={cn(
|
|
235
|
-
variant === "bar" &&
|
|
236
|
-
"shrink-0 border-b border-border bg-background text-foreground",
|
|
410
|
+
variant === "bar" && pageBarClassName,
|
|
237
411
|
className,
|
|
238
412
|
)}
|
|
239
413
|
{...props}
|
|
@@ -249,14 +423,16 @@ export function PageTitle({
|
|
|
249
423
|
...props
|
|
250
424
|
}: HTMLAttributes<HTMLHeadingElement>): ReactElement {
|
|
251
425
|
const variant = useContext(PageHeaderContext);
|
|
252
|
-
requireParent(variant !== null, "PageTitle", "PageHeader");
|
|
426
|
+
requireParent(variant !== null, "PageTitle", "PageHeader ou PageIntro");
|
|
253
427
|
return (
|
|
254
428
|
<h1
|
|
255
429
|
data-slot="page-title"
|
|
256
430
|
className={cn(
|
|
257
431
|
variant === "bar"
|
|
258
432
|
? "truncate text-sm font-semibold"
|
|
259
|
-
:
|
|
433
|
+
: variant === "intro"
|
|
434
|
+
? "text-3xl font-semibold tracking-tight"
|
|
435
|
+
: surfaceHeaderClasses.page.title,
|
|
260
436
|
className,
|
|
261
437
|
)}
|
|
262
438
|
{...props}
|
|
@@ -269,7 +445,11 @@ export function PageDescription({
|
|
|
269
445
|
...props
|
|
270
446
|
}: HTMLAttributes<HTMLParagraphElement>): ReactElement {
|
|
271
447
|
const variant = useContext(PageHeaderContext);
|
|
272
|
-
requireParent(
|
|
448
|
+
requireParent(
|
|
449
|
+
variant !== null,
|
|
450
|
+
"PageDescription",
|
|
451
|
+
"PageHeader ou PageIntro",
|
|
452
|
+
);
|
|
273
453
|
return (
|
|
274
454
|
<p
|
|
275
455
|
data-slot="page-description"
|
|
@@ -289,13 +469,14 @@ export function PageActions({
|
|
|
289
469
|
...props
|
|
290
470
|
}: HTMLAttributes<HTMLDivElement>): ReactElement {
|
|
291
471
|
const variant = useContext(PageHeaderContext);
|
|
292
|
-
requireParent(variant !== null, "PageActions", "PageHeader");
|
|
472
|
+
requireParent(variant !== null, "PageActions", "PageHeader ou PageIntro");
|
|
293
473
|
const target = useContext(PageActionsTargetContext);
|
|
474
|
+
const insideShell = useContext(PageShellContext);
|
|
294
475
|
const actions = (
|
|
295
476
|
<div
|
|
296
477
|
data-slot="page-actions"
|
|
297
478
|
className={cn(
|
|
298
|
-
variant === "bar"
|
|
479
|
+
variant === "bar" || insideShell
|
|
299
480
|
? "flex shrink-0 items-center gap-2"
|
|
300
481
|
: surfaceHeaderClasses.page.actions,
|
|
301
482
|
className,
|
|
@@ -303,6 +484,7 @@ export function PageActions({
|
|
|
303
484
|
{...props}
|
|
304
485
|
/>
|
|
305
486
|
);
|
|
487
|
+
if (insideShell && target === null) return <></>;
|
|
306
488
|
return target === null ? actions : createPortal(actions, target);
|
|
307
489
|
}
|
|
308
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")}
|
|
@@ -153,7 +156,13 @@ export function SurfaceHeader({
|
|
|
153
156
|
{leadingPlacement === "inline" && leading.length > 0 && (
|
|
154
157
|
// `min-w-0` aqui é o que deixa a trilha do PageNavigation ceder e truncar; sem ele o
|
|
155
158
|
// wrapper assume o min-content do breadcrumb e a compressão toda cai sobre o título.
|
|
156
|
-
<div
|
|
159
|
+
<div
|
|
160
|
+
data-slot={`${slot}-navigation`}
|
|
161
|
+
className={cn(
|
|
162
|
+
"flex min-w-0 items-center",
|
|
163
|
+
!heading && "flex-1",
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
157
166
|
{leading}
|
|
158
167
|
</div>
|
|
159
168
|
)}
|
|
@@ -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,16 +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 inteiro — incluindo
|
|
127
|
-
disponível.
|
|
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
|
|
128
182
|
`PageState`. O título do estado assume o heading principal. Esse registro também funciona quando um
|
|
129
|
-
componente intermediário decide qual `PageState` renderizar. Em `ready`, o cabeçalho
|
|
130
|
-
voltam à composição normal.
|
|
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.
|
|
131
185
|
|
|
132
186
|
```tsx preview col
|
|
133
187
|
<Page title="Relatório">
|
|
@@ -172,6 +226,25 @@ estado na própria seção com `DataState`, `ActionView`, `ActionList` ou `Alert
|
|
|
172
226
|
| `className` | `string` | `max-w-7xl` | Classes do container para substituir o teto padrão de `80rem`. |
|
|
173
227
|
| `children` | `ReactNode` | | O body da página — espaçamento e diagramação são seus. |
|
|
174
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
|
+
|
|
175
248
|
## Propriedades de PageHeader
|
|
176
249
|
|
|
177
250
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -202,7 +275,7 @@ estado na própria seção com `DataState`, `ActionView`, `ActionList` ou `Alert
|
|
|
202
275
|
|
|
203
276
|
| Propriedade | Tipo | Descrição |
|
|
204
277
|
| ----------- | --------------------------------- | ------------------------------------------------------ |
|
|
205
|
-
| `children` | `ReactNode` | Título principal `h1`; compacto na
|
|
278
|
+
| `children` | `ReactNode` | Título principal `h1`; compacto na barra e ampliado em `PageIntro`. |
|
|
206
279
|
| `className` | `string` | Classes adicionais do título. |
|
|
207
280
|
| demais | Atributos de `HTMLHeadingElement` | Atributos nativos repassados ao heading. |
|
|
208
281
|
|
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,
|