@softize/opus 17.0.0 → 17.2.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 +20 -0
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +10 -12
- package/docs/adr/0013-presentation-is-a-portable-action-oriented-artifact.md +17 -11
- package/docs/adr/0015-action-size-follows-interaction-density.md +64 -0
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/SKILL.md +4 -3
- package/registry/skills/build-opus-ui/references/evaluations.md +3 -2
- package/registry/skills/build-opus-ui/references/ui-patterns.md +11 -9
- package/src/ui/components/patterns/form.tsx +7 -1
- package/src/ui/components/patterns/page.tsx +34 -36
- package/src/ui/components/patterns/presentation.tsx +35 -7
- package/src/ui/docs/content/button.md +8 -2
- package/src/ui/docs/content/page.md +16 -13
- package/src/ui/docs/content/presentation.md +22 -7
- package/src/ui/meta.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ 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
|
+
## 17.2.0 — 2026-09-12
|
|
11
|
+
|
|
12
|
+
Páginas hospedadas por `PageShell` passam a apresentar o título no início do conteúdo, em
|
|
13
|
+
`PageIntro`, e reservam a barra persistente para navegação e ações. O shorthand de `Page` aplica
|
|
14
|
+
essa anatomia automaticamente, evitando repetir a página atual ao lado do breadcrumb.
|
|
15
|
+
|
|
16
|
+
Na composição explícita dentro de `PageShell`, substitua `PageHeader` por `PageIntro`. Fora do
|
|
17
|
+
shell, `PageHeader` continua sendo o cabeçalho convencional da página.
|
|
18
|
+
|
|
19
|
+
## 17.1.0 — 2026-09-12
|
|
20
|
+
|
|
21
|
+
Formulários de `Presentation` em Dialog e Drawer passam a oferecer `Cancelar` ao lado da ação
|
|
22
|
+
principal. O retorno no header modal aparece somente quando a surface anterior também é um Dialog
|
|
23
|
+
ou Drawer; a Page mantida ao fundo não cria uma etapa de navegação.
|
|
24
|
+
|
|
25
|
+
Em Page hospedada por `PageShell`, a `Presentation` mantém navegação e ações na barra e apresenta o
|
|
26
|
+
título no `PageIntro`. Ações que abrem outra Presentation usam o botão default para assumir a
|
|
27
|
+
hierarquia primária do fluxo. Ações textuais de header e footer usam o tamanho `default`; `sm` fica
|
|
28
|
+
reservado às operações densas de toolbar, seção e coleção.
|
|
29
|
+
|
|
10
30
|
## 17.0.0 — 2026-09-11
|
|
11
31
|
|
|
12
32
|
A inspeção de `Presentation` passa a pertencer exclusivamente à Lens, que lê as definições estáticas
|
|
@@ -21,14 +21,13 @@ renderiza a barra, recebe a navegação conhecida pelo shell e oferece o alvo ca
|
|
|
21
21
|
declaradas pela `Page` descendente. Essa barra existe somente por `PageShell`; `PageHeader` não
|
|
22
22
|
possui variante visual para reproduzi-la dentro da página.
|
|
23
23
|
|
|
24
|
-
Quando uma `Page` está dentro de `PageShell`, `
|
|
25
|
-
`PageTitle
|
|
26
|
-
|
|
27
|
-
`
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
título e ações mesmo quando o conteúdo carrega, falha ou está vazio. Uma ação que depende do
|
|
24
|
+
Quando uma `Page` está dentro de `PageShell`, `PageIntro` é seu header estrutural no conteúdo.
|
|
25
|
+
`PageTitle` permanece nessa região, enquanto `PageNavigation`, `PageBack` e `PageActions` são
|
|
26
|
+
projetados nos alvos da barra. Na forma explícita, a página usa `PageIntro` e `PageBody`, sem
|
|
27
|
+
`PageHeader`. Fora de `PageShell`, `PageHeader` continua dentro do container da página.
|
|
28
|
+
|
|
29
|
+
`PageState` oculta o título de `PageIntro`. A barra do `PageShell` permanece visível com navegação
|
|
30
|
+
e ações mesmo quando o conteúdo carrega, falha ou está vazio. Uma ação que depende do
|
|
32
31
|
conteúdo deve se omitir por estado na própria rota.
|
|
33
32
|
|
|
34
33
|
`PageShell` não substitui o shell completo da aplicação, não inclui sidebar e não cria navegação.
|
|
@@ -38,10 +37,9 @@ Ele apenas coordena a barra e a área em que uma única `Page` é renderizada.
|
|
|
38
37
|
|
|
39
38
|
- shell e rota podem continuar conhecendo partes diferentes da página sem criar componentes locais
|
|
40
39
|
de chrome;
|
|
41
|
-
- título e ações têm
|
|
40
|
+
- o título inicia o conteúdo e as ações têm destino oficial na barra;
|
|
42
41
|
- estados integrais preservam a barra e escondem somente a introdução do conteúdo;
|
|
43
|
-
- a composição
|
|
44
|
-
visual muda;
|
|
42
|
+
- a composição explícita usa `PageIntro` dentro do shell e `PageHeader` fora dele;
|
|
45
43
|
- `PageActionsTarget` continua disponível apenas para workspaces imersivos que não usam
|
|
46
44
|
`PageShell`.
|
|
47
45
|
|
|
@@ -64,7 +62,7 @@ estado do conteúdo. A moldura do shell não deve oscilar com a consulta da rota
|
|
|
64
62
|
|
|
65
63
|
## Verificação
|
|
66
64
|
|
|
67
|
-
- testes cobrem a projeção de navegação
|
|
65
|
+
- testes cobrem o título no início do conteúdo, a projeção de navegação e ações, a permanência da barra e a ocultação de `PageIntro` em estados
|
|
68
66
|
integrais;
|
|
69
67
|
- a documentação demonstra a composição de `PageShell`, `PageIntro` e `Page` sem CSS externo;
|
|
70
68
|
- consumidores removem barras paralelas e `PageActionsTarget` ao adotar o novo contrato.
|
|
@@ -19,11 +19,12 @@ capacidades em uma apresentação verificável.
|
|
|
19
19
|
`Presentation` é o artefato declarativo que descreve um recurso independentemente da superfície em
|
|
20
20
|
que aparece. `Surface` escolhe `page`, `dialog` ou `drawer` no momento da renderização.
|
|
21
21
|
|
|
22
|
-
A anatomia portátil é `Header + Body + Footer
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
A anatomia portátil é `Header + Body + Footer?`, mas cada surface materializa seu header de acordo
|
|
23
|
+
com o espaço disponível. Dialog e Drawer organizam horizontalmente
|
|
24
|
+
`Navigation? + Title + Actions? + Close` e mantêm as ações de conclusão no footer. Em Page
|
|
25
|
+
hospedada por `PageShell`, a barra persistente reúne navegação e ações; o título inicia o conteúdo
|
|
26
|
+
em `PageIntro`, sem repetir a página atual ao lado do breadcrumb. Fora do shell, a Page materializa
|
|
27
|
+
seu header completo no próprio container.
|
|
27
28
|
|
|
28
29
|
Uma action assume um de dois papéis:
|
|
29
30
|
|
|
@@ -48,12 +49,15 @@ estática, publicável no manifest e inspecionável pela Lens. A invocação é
|
|
|
48
49
|
uma execução: identifica a Presentation, escolhe a surface, carrega somente input JSON e mantém a
|
|
49
50
|
pilha de frames necessária para voltar. Ela não é publicada no manifest.
|
|
50
51
|
|
|
51
|
-
`back` restaura o último frame da pilha.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
`back` restaura o último frame da pilha. Em Dialog ou Drawer, ele só é apresentado quando o frame
|
|
53
|
+
anterior também é modal; a Page mantida ao fundo não cria uma etapa intermediária. `close` encerra
|
|
54
|
+
apenas dialog ou drawer e é inválido para page. Formulários modais oferecem `Cancelar` como ação de
|
|
55
|
+
abandono no footer, ao lado da conclusão. `navigate` abre outra invocação por push ou substitui a
|
|
56
|
+
atual; sincronizar esse estado com URL é responsabilidade do adaptador da aplicação quando refresh,
|
|
57
|
+
deep link ou histórico forem necessários. O inspetor de desenvolvimento reúne definição,
|
|
58
|
+
invocação, bindings resolvidos e diagnósticos, mascarando chaves sensíveis antes de expor o JSON. O
|
|
59
|
+
inventário estático e seu JSON ficam disponíveis na Lens; a aplicação não adiciona um launcher
|
|
60
|
+
flutuante ao shell.
|
|
57
61
|
|
|
58
62
|
## Consequências
|
|
59
63
|
|
|
@@ -90,6 +94,8 @@ Foi descartada porque compatibilidade exige delegar cada kind ao pattern canôni
|
|
|
90
94
|
- o manifest projeta Presentations sem callbacks nem dados server-only;
|
|
91
95
|
- a lens lista e expõe o JSON integral de cada Presentation;
|
|
92
96
|
- testes de invocação cobrem push, replace, back, close e rejeição de input não JSON;
|
|
97
|
+
- testes do renderer cobrem a ausência de retorno entre Page e modal, o retorno entre modais e as
|
|
98
|
+
ações Cancelar/concluir do formulário;
|
|
93
99
|
- o inspetor mascara segredo, token, cookie, autorização, senha e chave de API;
|
|
94
100
|
- a Lens lista as definições registradas e abre o JSON integral sem depender da árvore React;
|
|
95
101
|
- uma aplicação consumidora usa o pacote local por symlink durante a migração página por página.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# ADR 0015 — O tamanho da ação acompanha a densidade da interação
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-09-12.
|
|
5
|
+
|
|
6
|
+
## Contexto
|
|
7
|
+
|
|
8
|
+
O uso de `default` e `sm` passou a variar entre headers de Page, footers de Dialog e Drawer,
|
|
9
|
+
toolbars e ações de listagem. Como tamanho também era usado para sugerir importância, duas decisões
|
|
10
|
+
equivalentes podiam ganhar alturas diferentes, enquanto ações repetitivas ocupavam o mesmo espaço
|
|
11
|
+
de uma conclusão da superfície.
|
|
12
|
+
|
|
13
|
+
O Opus já separa contexto semântico, variante visual e tamanho. Misturar esses eixos impede que a
|
|
14
|
+
mesma regra funcione quando uma ação muda de secundária para primária ou quando um recurso troca de
|
|
15
|
+
Page para Dialog ou Drawer.
|
|
16
|
+
|
|
17
|
+
## Decisão
|
|
18
|
+
|
|
19
|
+
O tamanho comunica densidade e alcance da interação; `context` e `variant` comunicam hierarquia e
|
|
20
|
+
risco.
|
|
21
|
+
|
|
22
|
+
- ações textuais que decidem a superfície usam `default`: header de Page e footer de Page, Dialog
|
|
23
|
+
ou Drawer;
|
|
24
|
+
- ações operacionais em toolbar, header de seção ou coleção densa usam `sm`;
|
|
25
|
+
- ações internas de linha, célula ou campo usam `xs` ou `icon-xs`;
|
|
26
|
+
- ações somente com ícone que pertencem ao chrome da superfície, como voltar e fechar, usam
|
|
27
|
+
`icon-sm`;
|
|
28
|
+
- `lg` fica reservado a chamadas que deliberadamente precisam de uma área de toque maior, não a
|
|
29
|
+
uma ação primária comum.
|
|
30
|
+
|
|
31
|
+
Patterns que conhecem a região aplicam o tamanho. Componentes estruturais genéricos não clonam nem
|
|
32
|
+
reescrevem filhos arbitrários: em composição manual, o consumidor segue a mesma matriz. Alterar a
|
|
33
|
+
ênfase de uma ação não altera seu tamanho dentro da região.
|
|
34
|
+
|
|
35
|
+
## Consequências
|
|
36
|
+
|
|
37
|
+
- Page, Dialog e Drawer mantêm proporção equivalente nas decisões principais;
|
|
38
|
+
- toolbars e grids preservam densidade sem reduzir ações de conclusão;
|
|
39
|
+
- `primary`, `neutral` e `danger` podem mudar sem provocar salto de altura;
|
|
40
|
+
- exemplos e skills deixam de recomendar `sm` por todo cabeçalho compacto.
|
|
41
|
+
|
|
42
|
+
## Alternativas consideradas
|
|
43
|
+
|
|
44
|
+
### Usar `sm` em toda barra ou header
|
|
45
|
+
|
|
46
|
+
Preservaria a menor altura possível, mas trataria uma decisão da página como operação repetitiva e
|
|
47
|
+
criaria diferença em relação ao footer modal do mesmo recurso.
|
|
48
|
+
|
|
49
|
+
### Fazer a ação primária sempre maior
|
|
50
|
+
|
|
51
|
+
Reforçaria hierarquia, mas misturaria importância com densidade. A variante `solid` e o contexto
|
|
52
|
+
`primary` já comunicam essa relação sem deslocar o layout.
|
|
53
|
+
|
|
54
|
+
### Fazer `ButtonGroup` redimensionar qualquer filho
|
|
55
|
+
|
|
56
|
+
Centralizaria parte da aparência, mas introduziria comportamento implícito e frágil para filhos que
|
|
57
|
+
não são `Button`. A região ou o pattern conhece melhor o tamanho adequado.
|
|
58
|
+
|
|
59
|
+
## Verificação
|
|
60
|
+
|
|
61
|
+
- testes da `Presentation` conferem `default` em ações textuais do header e do footer;
|
|
62
|
+
- testes de `ActionList` continuam cobrindo `sm` em toolbar e `icon-xs` nas ações de linha;
|
|
63
|
+
- a documentação de Button e Page apresenta a mesma matriz;
|
|
64
|
+
- revisão de consumidor trata divergência como exceção explícita, não como novo default.
|
package/package.json
CHANGED
|
@@ -40,10 +40,11 @@ existentes, mantendo navegação observável e componentes reutilizáveis sem la
|
|
|
40
40
|
esses defaults na tela.
|
|
41
41
|
7. Tratar página, filtro, seleção e modal importante como estado navegável por URL quando
|
|
42
42
|
o produto precisa de deep link, back/forward ou refresh.
|
|
43
|
-
8. Compor superfícies pela gramática estrutural do catálogo: `Page` contém
|
|
44
|
-
`PageBody`;
|
|
43
|
+
8. Compor superfícies pela gramática estrutural do catálogo: fora de `PageShell`, `Page` contém
|
|
44
|
+
`PageHeader` e `PageBody`; dentro dele, contém `PageIntro` e `PageBody`; `Content` contém
|
|
45
|
+
`ContentHeader` e `ContentBody`; Card, Drawer e Pane usam seus
|
|
45
46
|
respectivos `*Body`. Para o caso direto, usar a sintaxe abreviada de `Page` (`title`,
|
|
46
|
-
`
|
|
47
|
+
`actions`) ou de `Content` (`title`, `description`, `actions` e `count`); não misturá-la com o
|
|
47
48
|
header explícito. O título da página não carrega contador. Ajustar o
|
|
48
49
|
nível do heading pela hierarquia semântica, não pelo destaque visual. O `Page` mantém seu teto
|
|
49
50
|
centralizado padrão de `80rem`.
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
- Dispara: “Monte a tela de edição usando a form action do Opus.”
|
|
4
4
|
- Não dispara: “Ajuste o CSS de um e-mail estático.”
|
|
5
5
|
- Execução: implementar uma lista com modal roteável e provar loading, erro, vazio e back.
|
|
6
|
-
- Execução estrutural: montar uma página de relatório com `Page > PageHeader + PageBody`,
|
|
7
|
-
`
|
|
6
|
+
- Execução estrutural: montar uma página de relatório isolada com `Page > PageHeader + PageBody`,
|
|
7
|
+
outra em `PageShell` com `Page > PageIntro + PageBody`, uma seção `Content > ContentHeader +
|
|
8
|
+
ContentBody`, `ActionFilterBar` separado do renderer, `ItemGroup` para
|
|
8
9
|
uma coleção secundária e um `ActionFormDialog`; provar teto padrão de `80rem`, hierarquia por
|
|
9
10
|
`level`, vazio estrutural sólido, `Empty` apenas para criação ou vínculo, números sem alinhamento
|
|
10
11
|
inferido e cancelamento `ghost` no modal.
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
- Campos, labels, mensagens e invalidações pertencem ao contrato quando são parte da
|
|
5
5
|
operação, não a uma tela isolada.
|
|
6
6
|
- URL representa estado que precisa sobreviver a refresh, deep link ou histórico.
|
|
7
|
-
- `Page` fornece o `<main>` e o container centralizado com teto padrão de `80rem`.
|
|
8
|
-
explícita é `Page > PageHeader (PageBack? | PageNavigation?, PageTitle,
|
|
7
|
+
- `Page` fornece o `<main>` e o container centralizado com teto padrão de `80rem`. Fora de
|
|
8
|
+
`PageShell`, sua forma explícita é `Page > PageHeader (PageBack? | PageNavigation?, PageTitle,
|
|
9
9
|
PageActions?) + PageBody`; `title` e `actions` no próprio `Page` são a abreviação
|
|
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
|
-
- `PageIntro` é
|
|
14
|
-
o
|
|
13
|
+
- `PageIntro` é a região estrutural do título quando a página está em `PageShell`; fora dele, pode
|
|
14
|
+
substituir `PageHeader` quando o conteúdo precisar começar por uma introdução.
|
|
15
15
|
- `PageHeader` é a região de cabeçalho dentro de uma `Page` isolada e organiza navegação, título e
|
|
16
16
|
ações na mesma linha. Em uma subpágina simples, `PageBack` recebe o destino pai explícito e aparece
|
|
17
17
|
antes do título como controle somente com ícone. Para mais de um ancestral relevante, use
|
|
@@ -20,11 +20,13 @@ PageActions?) + PageBody`; `title` e `actions` no próprio `Page` são a abrevia
|
|
|
20
20
|
possuam chrome próprio.
|
|
21
21
|
- Quando shell e rota conhecem partes diferentes da mesma página, use `PageShell` ao redor da rota.
|
|
22
22
|
O shell é o único responsável pela barra: fornece `navigation`; a `Page` descendente continua
|
|
23
|
-
declarando `title` e `actions`. O Opus mantém a barra de `3rem
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`
|
|
23
|
+
declarando `title` e `actions`. O Opus mantém a barra de `3rem`, projeta ações nela e inicia o
|
|
24
|
+
conteúdo com o título em `PageIntro`.
|
|
25
|
+
Ações com texto na barra usam o tamanho `default`, como as ações de footer de Dialog e Drawer;
|
|
26
|
+
ações somente com ícone de chrome usam `icon-sm`. O tamanho `sm` fica para ações operacionais em
|
|
27
|
+
toolbar, seção ou coleção densa, e `xs`/`icon-xs` para ações internas de linha ou célula. Contexto
|
|
28
|
+
e variante resolvem a hierarquia visual sem alterar essa medida. Na forma explícita dentro do
|
|
29
|
+
shell, use `Page > PageIntro (PageNavigation?, PageTitle, PageActions?) + PageBody`. Não monte
|
|
28
30
|
`PaneHeader`, portal ou seletor global para reconstruir essa composição.
|
|
29
31
|
- `PageState` substitui todo o conteúdo principal quando a página carrega, falha ou está vazia. Na
|
|
30
32
|
forma curta ele pode ser escrito como filho direto de `Page`, que cria o `PageBody`; na forma
|
|
@@ -719,7 +719,13 @@ export function ActionForm<TInput extends Record<string, unknown>, TData>({
|
|
|
719
719
|
{wrapFooter(
|
|
720
720
|
<>
|
|
721
721
|
{onCancel !== undefined && (
|
|
722
|
-
<Button
|
|
722
|
+
<Button
|
|
723
|
+
type="button"
|
|
724
|
+
form={formId}
|
|
725
|
+
variant="ghost"
|
|
726
|
+
disabled={disabled || isLoading}
|
|
727
|
+
onClick={onCancel}
|
|
728
|
+
>
|
|
723
729
|
{cancelLabel}
|
|
724
730
|
</Button>
|
|
725
731
|
)}
|
|
@@ -34,7 +34,6 @@ const PageHeaderContext = createContext<PageHeadingRegion | null>(null);
|
|
|
34
34
|
const PageIntegralStateContext = createContext(false);
|
|
35
35
|
const PageActionsTargetContext = createContext<HTMLElement | null>(null);
|
|
36
36
|
const PageNavigationTargetContext = createContext<HTMLElement | null>(null);
|
|
37
|
-
const PageTitleTargetContext = createContext<HTMLElement | null>(null);
|
|
38
37
|
const PageShellContext = createContext(false);
|
|
39
38
|
const pageBarClassName =
|
|
40
39
|
"flex h-12 shrink-0 items-center border-b border-border bg-background text-foreground";
|
|
@@ -124,7 +123,6 @@ export function PageShell({
|
|
|
124
123
|
);
|
|
125
124
|
const [navigationTarget, setNavigationTarget] =
|
|
126
125
|
useState<HTMLDivElement | null>(null);
|
|
127
|
-
const [titleTarget, setTitleTarget] = useState<HTMLDivElement | null>(null);
|
|
128
126
|
|
|
129
127
|
return (
|
|
130
128
|
<div
|
|
@@ -152,20 +150,18 @@ export function PageShell({
|
|
|
152
150
|
<PageShellNavigationSlot targetRef={setNavigationTarget}>
|
|
153
151
|
{navigation}
|
|
154
152
|
</PageShellNavigationSlot>
|
|
155
|
-
<PageShellTitleSlot
|
|
153
|
+
<PageShellTitleSlot />
|
|
156
154
|
<PageShellActionsSlot targetRef={setActionsTarget}>
|
|
157
155
|
{actions}
|
|
158
156
|
</PageShellActionsSlot>
|
|
159
157
|
</SurfaceHeader>
|
|
160
158
|
<PageShellContext.Provider value>
|
|
161
159
|
<PageNavigationTargetContext.Provider value={navigationTarget}>
|
|
162
|
-
<
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
</PageActionsTarget>
|
|
168
|
-
</PageTitleTargetContext.Provider>
|
|
160
|
+
<PageActionsTarget target={actionsTarget}>
|
|
161
|
+
<div data-slot="page-shell-content" className="min-h-0 flex-1">
|
|
162
|
+
{children}
|
|
163
|
+
</div>
|
|
164
|
+
</PageActionsTarget>
|
|
169
165
|
</PageNavigationTargetContext.Provider>
|
|
170
166
|
</PageShellContext.Provider>
|
|
171
167
|
</div>
|
|
@@ -254,10 +250,16 @@ export function Page({
|
|
|
254
250
|
}, []);
|
|
255
251
|
if (
|
|
256
252
|
shorthand &&
|
|
257
|
-
nodes.some(
|
|
253
|
+
nodes.some(
|
|
254
|
+
(node) =>
|
|
255
|
+
isValidElement(node) &&
|
|
256
|
+
[PageHeader, PageIntro, PageBody, PageFooter].includes(
|
|
257
|
+
node.type as typeof PageHeader,
|
|
258
|
+
),
|
|
259
|
+
)
|
|
258
260
|
) {
|
|
259
261
|
throw new Error(
|
|
260
|
-
"Page não permite misturar propriedades de shorthand com
|
|
262
|
+
"Page não permite misturar propriedades de shorthand com composição explícita.",
|
|
261
263
|
);
|
|
262
264
|
}
|
|
263
265
|
if (!shorthand) {
|
|
@@ -277,9 +279,9 @@ export function Page({
|
|
|
277
279
|
headers.length + bodies.length + footers.length === nodes.length;
|
|
278
280
|
const validShellComposition =
|
|
279
281
|
insideShell &&
|
|
280
|
-
headers.length ===
|
|
282
|
+
headers.length === 0 &&
|
|
281
283
|
bodies.length === 1 &&
|
|
282
|
-
intros.length
|
|
284
|
+
intros.length === 1 &&
|
|
283
285
|
footers.length <= 1 &&
|
|
284
286
|
headers.length + intros.length + bodies.length + footers.length ===
|
|
285
287
|
nodes.length;
|
|
@@ -290,7 +292,7 @@ export function Page({
|
|
|
290
292
|
) {
|
|
291
293
|
throw new Error(
|
|
292
294
|
insideShell
|
|
293
|
-
? "Page explícito dentro de PageShell exige um
|
|
295
|
+
? "Page explícito dentro de PageShell exige um PageIntro, um PageBody e aceita no máximo um PageFooter como filhos diretos."
|
|
294
296
|
: "Page explícito exige um PageHeader ou PageIntro, um PageBody e no máximo um PageFooter como filhos diretos.",
|
|
295
297
|
);
|
|
296
298
|
}
|
|
@@ -298,10 +300,10 @@ export function Page({
|
|
|
298
300
|
const content =
|
|
299
301
|
shorthand && insideShell ? (
|
|
300
302
|
<>
|
|
301
|
-
<
|
|
303
|
+
<PageIntro>
|
|
302
304
|
<PageTitle>{title}</PageTitle>
|
|
303
305
|
{actions !== undefined && <PageActions>{actions}</PageActions>}
|
|
304
|
-
</
|
|
306
|
+
</PageIntro>
|
|
305
307
|
<PageBody>{children}</PageBody>
|
|
306
308
|
</>
|
|
307
309
|
) : shorthand ? (
|
|
@@ -346,7 +348,7 @@ export function Page({
|
|
|
346
348
|
);
|
|
347
349
|
}
|
|
348
350
|
|
|
349
|
-
/**
|
|
351
|
+
/** Título estrutural no conteúdo de PageShell ou introdução opcional de uma Page isolada. */
|
|
350
352
|
export function PageIntro({
|
|
351
353
|
className,
|
|
352
354
|
children,
|
|
@@ -358,12 +360,16 @@ export function PageIntro({
|
|
|
358
360
|
requireParent(page !== null, "PageIntro", "Page");
|
|
359
361
|
if (integralState) {
|
|
360
362
|
if (!insideShell) return <></>;
|
|
361
|
-
const
|
|
362
|
-
(node) =>
|
|
363
|
+
const persistentSlots = Children.toArray(children).filter(
|
|
364
|
+
(node) =>
|
|
365
|
+
isValidElement(node) &&
|
|
366
|
+
[PageBack, PageNavigation, PageActions].includes(
|
|
367
|
+
node.type as typeof PageBack,
|
|
368
|
+
),
|
|
363
369
|
);
|
|
364
370
|
return (
|
|
365
371
|
<PageHeaderContext.Provider value="intro">
|
|
366
|
-
{
|
|
372
|
+
{persistentSlots}
|
|
367
373
|
</PageHeaderContext.Provider>
|
|
368
374
|
);
|
|
369
375
|
}
|
|
@@ -374,9 +380,11 @@ export function PageIntro({
|
|
|
374
380
|
name="PageIntro"
|
|
375
381
|
slot="page-intro"
|
|
376
382
|
slots={{
|
|
383
|
+
leading: [PageBack, PageNavigation],
|
|
377
384
|
title: PageTitle,
|
|
378
385
|
actions: PageActions,
|
|
379
386
|
}}
|
|
387
|
+
leadingPlacement="inline"
|
|
380
388
|
className={className}
|
|
381
389
|
{...props}
|
|
382
390
|
>
|
|
@@ -431,10 +439,8 @@ export function PageTitle({
|
|
|
431
439
|
...props
|
|
432
440
|
}: HTMLAttributes<HTMLHeadingElement>): ReactElement {
|
|
433
441
|
const variant = useContext(PageHeaderContext);
|
|
434
|
-
const insideShell = useContext(PageShellContext);
|
|
435
|
-
const target = useContext(PageTitleTargetContext);
|
|
436
442
|
requireParent(variant !== null, "PageTitle", "PageHeader ou PageIntro");
|
|
437
|
-
|
|
443
|
+
return (
|
|
438
444
|
<h1
|
|
439
445
|
data-slot="page-title"
|
|
440
446
|
className={cn(
|
|
@@ -446,10 +452,6 @@ export function PageTitle({
|
|
|
446
452
|
{...props}
|
|
447
453
|
/>
|
|
448
454
|
);
|
|
449
|
-
if (insideShell && variant === "header") {
|
|
450
|
-
return target === null ? <></> : createPortal(title, target);
|
|
451
|
-
}
|
|
452
|
-
return title;
|
|
453
455
|
}
|
|
454
456
|
|
|
455
457
|
export function PageActions({
|
|
@@ -495,11 +497,7 @@ export function PageNavigation({
|
|
|
495
497
|
const variant = useContext(PageHeaderContext);
|
|
496
498
|
const insideShell = useContext(PageShellContext);
|
|
497
499
|
const target = useContext(PageNavigationTargetContext);
|
|
498
|
-
requireParent(
|
|
499
|
-
variant !== null,
|
|
500
|
-
"PageNavigation",
|
|
501
|
-
"PageHeader",
|
|
502
|
-
);
|
|
500
|
+
requireParent(variant !== null, "PageNavigation", "PageHeader ou PageIntro");
|
|
503
501
|
const navigation = (
|
|
504
502
|
<div
|
|
505
503
|
data-slot="page-navigation-content"
|
|
@@ -507,7 +505,7 @@ export function PageNavigation({
|
|
|
507
505
|
{...props}
|
|
508
506
|
/>
|
|
509
507
|
);
|
|
510
|
-
if (insideShell
|
|
508
|
+
if (insideShell) {
|
|
511
509
|
return target === null ? <></> : createPortal(navigation, target);
|
|
512
510
|
}
|
|
513
511
|
return navigation;
|
|
@@ -522,7 +520,7 @@ export function PageBack({
|
|
|
522
520
|
const variant = useContext(PageHeaderContext);
|
|
523
521
|
const insideShell = useContext(PageShellContext);
|
|
524
522
|
const target = useContext(PageNavigationTargetContext);
|
|
525
|
-
requireParent(variant !== null, "PageBack", "PageHeader");
|
|
523
|
+
requireParent(variant !== null, "PageBack", "PageHeader ou PageIntro");
|
|
526
524
|
const destination =
|
|
527
525
|
typeof children === "string" ? children : "a página anterior";
|
|
528
526
|
const accessibleLabel = ariaLabel ?? `Voltar para ${destination}`;
|
|
@@ -540,7 +538,7 @@ export function PageBack({
|
|
|
540
538
|
</a>
|
|
541
539
|
</Button>
|
|
542
540
|
);
|
|
543
|
-
if (insideShell
|
|
541
|
+
if (insideShell) {
|
|
544
542
|
return target === null ? <></> : createPortal(back, target);
|
|
545
543
|
}
|
|
546
544
|
return back;
|
|
@@ -29,7 +29,11 @@ import type {
|
|
|
29
29
|
ViewContract,
|
|
30
30
|
} from "../../../core/contracts.ts";
|
|
31
31
|
import { cn } from "../../lib/cn.ts";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
Button,
|
|
34
|
+
buttonVariants,
|
|
35
|
+
type ButtonSize,
|
|
36
|
+
} from "../primitives/button.tsx";
|
|
33
37
|
import { ButtonGroup } from "../primitives/button-group.tsx";
|
|
34
38
|
import { Copyable } from "../primitives/copyable.tsx";
|
|
35
39
|
import { DetailField, DetailGroup } from "../primitives/detail.tsx";
|
|
@@ -55,6 +59,7 @@ import {
|
|
|
55
59
|
PageBody,
|
|
56
60
|
PageFooter,
|
|
57
61
|
PageHeader,
|
|
62
|
+
PageIntro,
|
|
58
63
|
PageNavigation,
|
|
59
64
|
PageTitle,
|
|
60
65
|
useInsidePageShell,
|
|
@@ -143,7 +148,7 @@ function PresentationFrame({
|
|
|
143
148
|
if (insidePageShell) {
|
|
144
149
|
return (
|
|
145
150
|
<Page className={cn("flex min-h-full flex-col", className)}>
|
|
146
|
-
<
|
|
151
|
+
<PageIntro>
|
|
147
152
|
{navigation === undefined ? null : (
|
|
148
153
|
<PageNavigation>{navigation}</PageNavigation>
|
|
149
154
|
)}
|
|
@@ -153,7 +158,7 @@ function PresentationFrame({
|
|
|
153
158
|
<PresentationActions>{headerActions}</PresentationActions>
|
|
154
159
|
</PageActions>
|
|
155
160
|
)}
|
|
156
|
-
</
|
|
161
|
+
</PageIntro>
|
|
157
162
|
<PageBody className="min-h-0 flex-1 overflow-y-auto">{body}</PageBody>
|
|
158
163
|
{hasFooter ? <PageFooter>{renderFooterActions()}</PageFooter> : null}
|
|
159
164
|
</Page>
|
|
@@ -298,6 +303,7 @@ function PresentationCommandTrigger({
|
|
|
298
303
|
command,
|
|
299
304
|
action,
|
|
300
305
|
input,
|
|
306
|
+
size,
|
|
301
307
|
disabled,
|
|
302
308
|
onLoadingChange,
|
|
303
309
|
onSuccess,
|
|
@@ -305,6 +311,7 @@ function PresentationCommandTrigger({
|
|
|
305
311
|
command: PresentationCommand;
|
|
306
312
|
action: SimpleContract<Record<string, unknown>, unknown>;
|
|
307
313
|
input: Record<string, unknown>;
|
|
314
|
+
size: ButtonSize;
|
|
308
315
|
disabled: boolean;
|
|
309
316
|
onLoadingChange: (command: PresentationCommand, loading: boolean) => void;
|
|
310
317
|
onSuccess: (data: unknown) => void;
|
|
@@ -318,6 +325,7 @@ function PresentationCommandTrigger({
|
|
|
318
325
|
action={action}
|
|
319
326
|
input={input}
|
|
320
327
|
variant="ghost"
|
|
328
|
+
size={size}
|
|
321
329
|
disabled={disabled}
|
|
322
330
|
onLoadingChange={reportLoading}
|
|
323
331
|
onSuccess={onSuccess}
|
|
@@ -374,6 +382,11 @@ export function Presentation({
|
|
|
374
382
|
[context, invocation, onInvocationChange, onRefresh],
|
|
375
383
|
);
|
|
376
384
|
|
|
385
|
+
const closeSurface = useCallback((): void => {
|
|
386
|
+
onOpenChange?.(false);
|
|
387
|
+
applyEffects([{ effect: "close" }]);
|
|
388
|
+
}, [applyEffects, onOpenChange]);
|
|
389
|
+
|
|
377
390
|
const reportLoading = useCallback(
|
|
378
391
|
(command: PresentationCommand, loading: boolean): void => {
|
|
379
392
|
setLoadingCommands((current) => {
|
|
@@ -429,6 +442,10 @@ export function Presentation({
|
|
|
429
442
|
string,
|
|
430
443
|
unknown
|
|
431
444
|
>;
|
|
445
|
+
// Header e footer são regiões de decisão da superfície, não toolbars operacionais.
|
|
446
|
+
// Seus comandos textuais usam a linha padrão; listas e grids continuam donos da
|
|
447
|
+
// densidade compacta de suas próprias ações.
|
|
448
|
+
const size: ButtonSize = "default";
|
|
432
449
|
if (action.kind === "simple") {
|
|
433
450
|
return (
|
|
434
451
|
<PresentationCommandTrigger
|
|
@@ -436,6 +453,7 @@ export function Presentation({
|
|
|
436
453
|
command={command}
|
|
437
454
|
action={action as SimpleContract<Record<string, unknown>, unknown>}
|
|
438
455
|
input={input}
|
|
456
|
+
size={size}
|
|
439
457
|
disabled={isBlocked(command)}
|
|
440
458
|
onLoadingChange={reportLoading}
|
|
441
459
|
onSuccess={(data) => applyEffects(command.onSuccess, data)}
|
|
@@ -450,7 +468,7 @@ export function Presentation({
|
|
|
450
468
|
return (
|
|
451
469
|
<Button
|
|
452
470
|
key={`${command.placement}:${command.action}`}
|
|
453
|
-
|
|
471
|
+
size={size}
|
|
454
472
|
disabled={isBlocked(command)}
|
|
455
473
|
onClick={() =>
|
|
456
474
|
openTarget(
|
|
@@ -480,6 +498,11 @@ export function Presentation({
|
|
|
480
498
|
action={bodyAction as FormContract<Record<string, unknown>, unknown>}
|
|
481
499
|
defaultValues={bodyInput}
|
|
482
500
|
submitLabel={definition.body.submitLabel}
|
|
501
|
+
onCancel={
|
|
502
|
+
invocation.surface === "page"
|
|
503
|
+
? undefined
|
|
504
|
+
: closeSurface
|
|
505
|
+
}
|
|
483
506
|
onSuccess={(data) => applyEffects(definition.body.onSuccess, data)}
|
|
484
507
|
disabled={hasSurfaceBlock}
|
|
485
508
|
onLoadingChange={setBodyLoading}
|
|
@@ -544,8 +567,12 @@ export function Presentation({
|
|
|
544
567
|
const footerActions = definition.actions
|
|
545
568
|
.filter((command) => command.placement === "footer")
|
|
546
569
|
.map(renderCommand);
|
|
570
|
+
const parentSurface = invocation.stack.at(-1)?.surface;
|
|
571
|
+
const canGoBack =
|
|
572
|
+
parentSurface !== undefined &&
|
|
573
|
+
(invocation.surface === "page" || parentSurface !== "page");
|
|
547
574
|
const navigation =
|
|
548
|
-
|
|
575
|
+
!canGoBack ? undefined : (
|
|
549
576
|
<Button
|
|
550
577
|
size="icon-sm"
|
|
551
578
|
variant="ghost"
|
|
@@ -567,10 +594,11 @@ export function Presentation({
|
|
|
567
594
|
footerActions={footerActions.length === 0 ? undefined : footerActions}
|
|
568
595
|
open={open}
|
|
569
596
|
onOpenChange={(nextOpen) => {
|
|
570
|
-
onOpenChange?.(nextOpen);
|
|
571
597
|
if (!nextOpen && invocation.surface !== "page") {
|
|
572
|
-
|
|
598
|
+
closeSurface();
|
|
599
|
+
return;
|
|
573
600
|
}
|
|
601
|
+
onOpenChange?.(nextOpen);
|
|
574
602
|
}}
|
|
575
603
|
className={className}
|
|
576
604
|
bodyClassName={bodyClassNameProp}
|
|
@@ -24,14 +24,20 @@ há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em
|
|
|
24
24
|
| Nome | Medida | Uso |
|
|
25
25
|
| --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
26
|
| `xs` | 1.5rem | Ação dentro de um campo (`InputGroupButton`). |
|
|
27
|
-
| `sm` | 2rem |
|
|
28
|
-
| `default` | 2.25rem |
|
|
27
|
+
| `sm` | 2rem | Ação operacional em toolbar, seção ou coleção densa, ao lado de `Select` e `Tabs` `sm`. |
|
|
28
|
+
| `default` | 2.25rem | Decisão da superfície em header de Page ou footer de Dialog/Drawer; é também a linha padrão de `Input`. |
|
|
29
29
|
| `lg` | 2.5rem | Chamada principal com mais área de toque. |
|
|
30
30
|
| `icon-xs` | 1.5rem | Ação só de ícone dentro de um campo ou de uma linha densa; é o quadrado de `ActionTrigger` com `icon`. |
|
|
31
31
|
| `icon-sm` | 1.75rem | Ação só de ícone em composições compactas que precisam de mais presença; também é o quadrado das setas do pager de `ActionList`. |
|
|
32
32
|
| `icon` | 2.25rem | Ação só de ícone na linha padrão. |
|
|
33
33
|
| `icon-lg` | 2.5rem | Ação só de ícone ao lado de um `lg`. |
|
|
34
34
|
|
|
35
|
+
Escolha o tamanho pela região, não pela importância visual: `variant` e `context` resolvem a
|
|
36
|
+
hierarquia da ação. Headers de Page e footers de Dialog/Drawer usam `default`; ações operacionais
|
|
37
|
+
de seção, toolbar e coleção usam `sm`; ações dentro de linha ou célula usam `xs` ou `icon-xs`.
|
|
38
|
+
Controles de chrome, como voltar e fechar, usam `icon-sm`. Assim a mesma decisão mantém a mesma
|
|
39
|
+
altura mesmo quando uma superfície troca uma ação secundária por uma primária.
|
|
40
|
+
|
|
35
41
|
```tsx preview
|
|
36
42
|
<Button size="xs">Mínimo</Button>
|
|
37
43
|
<Button size="sm">Pequeno</Button>
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Use `Page` para manter título, ações, estado e conteúdo na mesma anatomia. Sozinha, ela apresenta
|
|
4
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
|
|
5
|
+
com `PageShell`: o shell fornece a navegação, a página projeta as ações na barra e o título inicia
|
|
6
|
+
o conteúdo.
|
|
6
7
|
|
|
7
8
|
`PageHeader` mantém navegação, título e ações na mesma linha. O container é centralizado e ocupa a
|
|
8
9
|
largura disponível até `80rem` (`max-w-7xl`). Use `className` somente quando a composição pedir
|
|
9
10
|
outro teto ou largura total.
|
|
10
11
|
|
|
11
12
|
A forma curta é o padrão para páginas comuns. Fora de `PageShell`, ela cria `PageHeader` e
|
|
12
|
-
`PageBody`. Dentro dele, cria `
|
|
13
|
-
|
|
13
|
+
`PageBody`. Dentro dele, cria `PageIntro` e `PageBody`: o título fica na introdução e as ações são
|
|
14
|
+
projetadas na barra. A forma explícita segue a mesma anatomia, sem duplicar o título ao lado do
|
|
15
|
+
breadcrumb.
|
|
14
16
|
Contadores e outros indicadores pertencem ao
|
|
15
17
|
conteúdo que os explica.
|
|
16
18
|
|
|
@@ -64,8 +66,8 @@ posição introdutória sem transformar a trilha em ação.
|
|
|
64
66
|
## Barra persistente do shell
|
|
65
67
|
|
|
66
68
|
Use `PageShell` quando o shell conhece o breadcrumb e a rota conhece título e ações. Não crie um
|
|
67
|
-
`PaneHeader` paralelo nem esconda slots de `Page` com CSS. O Opus
|
|
68
|
-
`PageActions` na barra.
|
|
69
|
+
`PaneHeader` paralelo nem esconda slots de `Page` com CSS. O Opus mantém o `PageTitle` no início do
|
|
70
|
+
conteúdo e projeta somente navegação contextual e `PageActions` na barra.
|
|
69
71
|
|
|
70
72
|
```tsx preview col
|
|
71
73
|
<PageShell
|
|
@@ -82,7 +84,7 @@ Use `PageShell` quando o shell conhece o breadcrumb e a rota conhece título e a
|
|
|
82
84
|
<Page
|
|
83
85
|
title="Clientes"
|
|
84
86
|
actions={
|
|
85
|
-
<Button
|
|
87
|
+
<Button>
|
|
86
88
|
<Plus /> Novo cliente
|
|
87
89
|
</Button>
|
|
88
90
|
}
|
|
@@ -95,17 +97,17 @@ Use `PageShell` quando o shell conhece o breadcrumb e a rota conhece título e a
|
|
|
95
97
|
A barra permanece visível durante `PageState`; uma introdução opcional desaparece. Se uma ação não
|
|
96
98
|
puder ser executada sem o conteúdo, a própria rota deve omiti-la naquele estado.
|
|
97
99
|
|
|
98
|
-
Na composição explícita dentro do shell, use `
|
|
100
|
+
Na composição explícita dentro do shell, use `PageIntro` e `PageBody`:
|
|
99
101
|
|
|
100
102
|
```tsx preview col
|
|
101
103
|
<PageShell navigation={<Breadcrumb>...</Breadcrumb>}>
|
|
102
104
|
<Page>
|
|
103
|
-
<
|
|
105
|
+
<PageIntro>
|
|
104
106
|
<PageTitle>Clientes</PageTitle>
|
|
105
107
|
<PageActions>
|
|
106
|
-
<Button
|
|
108
|
+
<Button>Novo cliente</Button>
|
|
107
109
|
</PageActions>
|
|
108
|
-
</
|
|
110
|
+
</PageIntro>
|
|
109
111
|
<PageBody>Conteúdo da listagem.</PageBody>
|
|
110
112
|
</Page>
|
|
111
113
|
</PageShell>
|
|
@@ -136,8 +138,9 @@ render(
|
|
|
136
138
|
);
|
|
137
139
|
```
|
|
138
140
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
Fora de `PageShell`, use `PageIntro` somente quando o conteúdo precisar de uma introdução em vez do
|
|
142
|
+
header convencional. Dentro do shell, ele é a região estrutural do título e não deve ser combinado
|
|
143
|
+
com `PageHeader`.
|
|
141
144
|
|
|
142
145
|
## Estados integrais
|
|
143
146
|
|
|
@@ -208,7 +211,7 @@ estado na própria seção com `DataState`, `ActionView`, `ActionList` ou `Alert
|
|
|
208
211
|
|
|
209
212
|
| Propriedade | Tipo | Descrição |
|
|
210
213
|
| ----------- | ----------------------------- | ------------------------------------------------------------- |
|
|
211
|
-
| `children` | `ReactNode` |
|
|
214
|
+
| `children` | `ReactNode` | Título e ações da página dentro de `PageShell`, ou introdução de uma página isolada. |
|
|
212
215
|
| `className` | `string` | Classes adicionais da região introdutória. |
|
|
213
216
|
| demais | Atributos de `HTMLDivElement` | Atributos nativos repassados à região introdutória. |
|
|
214
217
|
|
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
um drawer. A superfície decide quanto espaço ocupar; o recurso continua com a mesma navegação,
|
|
3
3
|
cabeçalho, body e rodapé.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Dialog e Drawer mantêm no cabeçalho a ordem horizontal de navegação ou retorno, título e ações.
|
|
6
|
+
O retorno aparece em uma surface modal somente
|
|
7
|
+
quando ela foi aberta sobre outro Dialog ou Drawer; a Page ao fundo sustenta o modal, mas não cria
|
|
8
|
+
uma etapa de navegação. Formulários modais encerram o footer com `Cancelar` e a ação principal.
|
|
9
|
+
Em Page hospedada por `PageShell`, a barra concentra navegação e ações, enquanto o título abre o
|
|
10
|
+
conteúdo em um `PageIntro`. Assim o breadcrumb não disputa espaço nem repete o título na mesma linha.
|
|
11
|
+
Ações textuais de header e footer usam o tamanho `default`, pois pertencem à decisão da superfície;
|
|
12
|
+
o tamanho `sm` permanece reservado às operações densas de toolbar, seção e coleção. Contexto e
|
|
13
|
+
variante continuam definindo hierarquia visual sem alterar essa medida.
|
|
9
14
|
|
|
10
15
|
Use esse padrão quando uma lista puder abrir um detalhe lateral, quando a mesma edição precisar
|
|
11
16
|
funcionar em modal e em rota própria ou quando um fluxo começar compacto e crescer sem ganhar uma
|
|
12
17
|
segunda implementação.
|
|
13
18
|
|
|
14
19
|
Dentro de `PageShell`, a superfície `page` usa a anatomia real da aplicação: a Presentation projeta
|
|
15
|
-
navigation
|
|
20
|
+
navigation e actions no header da barra, apresenta o título em `PageIntro` e compõe `PageBody` e,
|
|
21
|
+
quando necessário, `PageFooter`.
|
|
16
22
|
Fora do shell, ela materializa o `PageHeader` completo para exemplos e superfícies independentes.
|
|
17
23
|
Não envolva a Presentation em um card para simular a página; valide proporção, rolagem e ações no
|
|
18
24
|
shell que efetivamente hospeda a rota.
|
|
@@ -35,6 +41,7 @@ const workspacePresentation = definePresentation({
|
|
|
35
41
|
});
|
|
36
42
|
|
|
37
43
|
function Example() {
|
|
44
|
+
const [open, setOpen] = React.useState(true);
|
|
38
45
|
const [invocation, setInvocation] = React.useState(() =>
|
|
39
46
|
definePresentationInvocation({
|
|
40
47
|
schemaVersion: 1,
|
|
@@ -52,7 +59,10 @@ function Example() {
|
|
|
52
59
|
key={value}
|
|
53
60
|
size="sm"
|
|
54
61
|
variant={invocation.surface === value ? "solid" : "outline"}
|
|
55
|
-
onClick={() =>
|
|
62
|
+
onClick={() => {
|
|
63
|
+
setInvocation({ ...invocation, surface: value });
|
|
64
|
+
setOpen(true);
|
|
65
|
+
}}
|
|
56
66
|
>
|
|
57
67
|
{value}
|
|
58
68
|
</Button>
|
|
@@ -64,7 +74,12 @@ function Example() {
|
|
|
64
74
|
definitions={[workspacePresentation]}
|
|
65
75
|
actions={{ [workspaceUpdate.name]: workspaceUpdate }}
|
|
66
76
|
invocation={invocation}
|
|
67
|
-
|
|
77
|
+
open={invocation.surface === "page" || open}
|
|
78
|
+
onOpenChange={setOpen}
|
|
79
|
+
onInvocationChange={(next) => {
|
|
80
|
+
if (next) setInvocation(next);
|
|
81
|
+
else setOpen(false);
|
|
82
|
+
}}
|
|
68
83
|
/>
|
|
69
84
|
</div>
|
|
70
85
|
);
|
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 e ações. Quando shell e rota conhecem partes diferentes da página, PageShell mantém a barra de 3rem
|
|
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 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; o título inicia o conteúdo em PageIntro. Fora do shell, a forma explícita usa PageHeader para o cabeçalho completo. PageActionsTarget fica restrito a workspaces imersivos sem PageShell. PageState oculta somente PageIntro; informações importantes entram no corpo como Alert ou texto. Para uma região disponível à criação ou vínculo, use Empty.",
|
|
404
404
|
},
|
|
405
405
|
presentation: {
|
|
406
406
|
name: "presentation",
|