@softize/opus 19.0.0 → 20.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +193 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +5 -0
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +15 -3
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/list.tsx +89 -78
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/presentation.tsx +52 -37
- package/src/ui/components/patterns/split.tsx +44 -4
- package/src/ui/components/patterns/surface-anatomy.ts +35 -8
- package/src/ui/components/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +2 -1
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/dialog.tsx +41 -10
- package/src/ui/components/primitives/drawer.tsx +9 -1
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +134 -52
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +41 -15
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +31 -7
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +107 -24
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +63 -19
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +28 -5
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +12 -4
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +118 -12
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +55 -5
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +3 -2
- package/src/ui/docs/content/skeleton.md +6 -1
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +19 -0
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +31 -31
- package/src/vite/design.ts +16 -4
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Use `Popover` para apresentar conteúdo livre junto a um gatilho, sem abrir um modal.
|
|
4
4
|
`PopoverHeader` agrupa título e descrição; o restante do painel aceita formulários curtos ou
|
|
5
|
-
detalhes.
|
|
5
|
+
detalhes. Para uma lista de ações, use [`Menu`](/ui/menu); para uma dica curta ao passar o ponteiro,
|
|
6
|
+
use [`Tooltip`](/ui/tooltip).
|
|
6
7
|
|
|
7
8
|
```tsx preview
|
|
8
9
|
<Popover>
|
|
@@ -12,7 +13,7 @@ detalhes.
|
|
|
12
13
|
<PopoverContent align="start">
|
|
13
14
|
<PopoverHeader>
|
|
14
15
|
<PopoverTitle>Renomear</PopoverTitle>
|
|
15
|
-
<PopoverDescription>O
|
|
16
|
+
<PopoverDescription>O endereço da sessão continua o mesmo; só o título muda.</PopoverDescription>
|
|
16
17
|
</PopoverHeader>
|
|
17
18
|
<div className="mt-3 space-y-2">
|
|
18
19
|
<Label htmlFor="pop-titulo">Título</Label>
|
|
@@ -23,9 +24,14 @@ detalhes.
|
|
|
23
24
|
</Popover>
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
O painel mede `18rem` de largura por padrão e abre abaixo do gatilho. Clicar fora ou pressionar
|
|
28
|
+
Esc fecha o painel.
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
## Posição e largura
|
|
31
|
+
|
|
32
|
+
`side` escolhe o lado preferido, `align` alinha o painel ao gatilho e `sideOffset` define a
|
|
33
|
+
distância entre eles. Quando não há espaço no lado escolhido, o painel abre no lado oposto. Ajuste
|
|
34
|
+
a largura com `className`, como `w-56` para `14rem`.
|
|
29
35
|
|
|
30
36
|
```tsx preview
|
|
31
37
|
<Popover>
|
|
@@ -40,6 +46,12 @@ detalhes.
|
|
|
40
46
|
<PopoverDescription>Alinhado à borda direita do gatilho.</PopoverDescription>
|
|
41
47
|
</PopoverContent>
|
|
42
48
|
</Popover>
|
|
49
|
+
<Popover>
|
|
50
|
+
<PopoverTrigger asChild><Button variant="ghost">À direita</Button></PopoverTrigger>
|
|
51
|
+
<PopoverContent side="right" className="w-56">
|
|
52
|
+
<PopoverDescription>Abre ao lado direito do gatilho.</PopoverDescription>
|
|
53
|
+
</PopoverContent>
|
|
54
|
+
</Popover>
|
|
43
55
|
```
|
|
44
56
|
|
|
45
57
|
## Propriedades de Popover
|
|
@@ -47,7 +59,15 @@ detalhes.
|
|
|
47
59
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
48
60
|
|---|---|---|---|
|
|
49
61
|
| `open` | `boolean` | | Estado no modo controlado. |
|
|
50
|
-
| `onOpenChange` | `(open: boolean) => void` | |
|
|
62
|
+
| `onOpenChange` | `(open: boolean) => void` | | Chamado quando o painel abre ou fecha; atualize `open` com o valor recebido. |
|
|
63
|
+
| `defaultOpen` | `boolean` | `false` | Estado inicial no modo não controlado. |
|
|
64
|
+
| `modal` | `boolean` | `false` | Com `true`, prende o foco no painel e bloqueia a interação com o restante da página enquanto ele está aberto. |
|
|
65
|
+
|
|
66
|
+
## Propriedades de PopoverTrigger
|
|
67
|
+
|
|
68
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| `asChild` | `boolean` | `false` | Faz do filho (por exemplo, um `Button`) o gatilho, em vez de renderizar outro `<button>`. |
|
|
51
71
|
|
|
52
72
|
## Propriedades de PopoverAnchor
|
|
53
73
|
|
|
@@ -59,5 +79,22 @@ não introduzir um nó extra; sem ele, o `PopoverTrigger` é a âncora.
|
|
|
59
79
|
|
|
60
80
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
61
81
|
|---|---|---|---|
|
|
82
|
+
| `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'bottom'` | Lado preferido do painel; sem espaço, ele abre no lado oposto. |
|
|
62
83
|
| `align` | `'start' \| 'center' \| 'end'` | `'center'` | Alinhamento do painel em relação ao gatilho. |
|
|
63
84
|
| `sideOffset` | `number` | `4` | Distância entre o gatilho e o painel. |
|
|
85
|
+
| `className` | `string` | | Classes do painel. A largura padrão é `w-72` (`18rem`). |
|
|
86
|
+
|
|
87
|
+
## Propriedades de PopoverHeader
|
|
88
|
+
|
|
89
|
+
`PopoverHeader` empilha `PopoverTitle` e `PopoverDescription` com espaçamento curto. Não tem props
|
|
90
|
+
próprias; aceita as de um elemento `div`.
|
|
91
|
+
|
|
92
|
+
## Propriedades de PopoverTitle
|
|
93
|
+
|
|
94
|
+
`PopoverTitle` destaca o nome do painel com peso médio. Ele renderiza um `div`, sem nível de
|
|
95
|
+
heading. Não tem props próprias; aceita as de um elemento `div`.
|
|
96
|
+
|
|
97
|
+
## Propriedades de PopoverDescription
|
|
98
|
+
|
|
99
|
+
`PopoverDescription` apresenta o texto de apoio em cor atenuada. Não tem props próprias; aceita as
|
|
100
|
+
de um elemento `p`.
|
|
@@ -1,45 +1,11 @@
|
|
|
1
|
-
`Presentation`
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 dimensionam o footer pelo conteúdo, com `Cancelar` em
|
|
9
|
-
`ghost` e a ação principal sólida.
|
|
10
|
-
Em Page hospedada por `PageShell`, a barra concentra navegação e ações globais. Uma Presentation
|
|
11
|
-
de listagem materializa título e comandos de header no `Content` que envolve o `ActionList`; assim
|
|
12
|
-
a criação permanece próxima da coleção. Esses comandos usam o tamanho `default`: o que abre outra
|
|
13
|
-
Presentation é um `Button` na variante padrão (`solid`), e uma action `simple` vira um
|
|
14
|
-
`ActionTrigger` na variante `ghost`. Nas demais Presentations, o título abre o conteúdo em
|
|
15
|
-
`PageIntro` e os comandos da superfície seguem a mesma regra.
|
|
16
|
-
|
|
17
|
-
Use esse padrão quando uma lista puder abrir um detalhe lateral, quando a mesma edição precisar
|
|
18
|
-
funcionar em modal e em rota própria ou quando um fluxo começar compacto e crescer sem ganhar uma
|
|
19
|
-
segunda implementação.
|
|
20
|
-
|
|
21
|
-
Dentro de `PageShell`, a superfície `page` usa a anatomia real da aplicação: a Presentation projeta
|
|
22
|
-
navigation e actions globais por `PageHeader`, mantém a introdução no conteúdo quando existir e
|
|
23
|
-
compõe `PageBody` e, quando necessário, `PageFooter`. Para uma action `list`, `PageBody` recebe
|
|
24
|
-
`Content + ActionList`; o header da coleção recebe o título e os comandos declarados no placement
|
|
25
|
-
`header`. Dialog e Drawer continuam usando o header da própria superfície.
|
|
26
|
-
Os comandos da coleção ficam no extremo oposto ao título e permanecem textuais, mesmo quando a
|
|
27
|
-
action declara um ícone. A criação usa `Criar recurso`, sem ícone, em um `Button` na variante
|
|
28
|
-
padrão (`solid`) e no tamanho `default`.
|
|
29
|
-
Não envolva a Presentation em um card para simular a página; valide proporção, rolagem e ações no
|
|
30
|
-
shell que efetivamente hospeda a rota.
|
|
1
|
+
Use `Presentation` quando o mesmo recurso precisa aparecer ora como página, ora como dialog, ora
|
|
2
|
+
como drawer: uma lista que abre o detalhe ao lado, uma edição que funciona em modal e em rota
|
|
3
|
+
própria, um fluxo que começa compacto e cresce sem ganhar uma segunda implementação. A superfície
|
|
4
|
+
decide quanto espaço ocupar; o recurso mantém a mesma navegação, cabeçalho, corpo e rodapé.
|
|
31
5
|
|
|
32
|
-
O exemplo declara
|
|
6
|
+
O exemplo declara uma Presentation sobre um contrato de formulário e alterna a superfície. O
|
|
33
7
|
`DocBrowserActionProvider` simula o cliente de actions; na aplicação, esse papel é do provider real.
|
|
34
8
|
|
|
35
|
-
Quando o recurso aceita assistência contextual, declare somente o gatilho serializável em
|
|
36
|
-
`assistant`. O renderer coloca esse gatilho no cabeçalho e abre o painel à direita do body da
|
|
37
|
-
própria Page, Dialog ou Drawer. Cabeçalho e rodapé continuam ocupando toda a largura da superfície;
|
|
38
|
-
enquanto o painel está aberto, o gatilho sai do cabeçalho e o próprio painel assume sua identidade
|
|
39
|
-
visual. O dado vivo do recurso não entra na definição: a página que o carregou fornece
|
|
40
|
-
`assistant.render`, captura identidade, nome e demais dados autorizados e monta o chat. Assim,
|
|
41
|
-
contexto de conversa não vira autorização nem dado persistido no manifest.
|
|
42
|
-
|
|
43
9
|
```tsx live
|
|
44
10
|
const workspacePresentation = definePresentation({
|
|
45
11
|
schemaVersion: 1,
|
|
@@ -64,7 +30,7 @@ function Example() {
|
|
|
64
30
|
<DocBrowserActionProvider>
|
|
65
31
|
<div className="space-y-4">
|
|
66
32
|
<ButtonGroup mode="spaced">
|
|
67
|
-
{["page", "dialog", "drawer"].map((value) => (
|
|
33
|
+
{(["page", "dialog", "drawer"] as const).map((value) => (
|
|
68
34
|
<Button
|
|
69
35
|
key={value}
|
|
70
36
|
size="sm"
|
|
@@ -115,14 +81,48 @@ function Example() {
|
|
|
115
81
|
render(<Example />);
|
|
116
82
|
```
|
|
117
83
|
|
|
84
|
+
## Como cada superfície apresenta o recurso
|
|
85
|
+
|
|
86
|
+
Em Dialog e Drawer, o cabeçalho da própria superfície reúne, nesta ordem, o retorno, o título e as
|
|
87
|
+
ações. O retorno só aparece quando o modal foi aberto sobre outro Dialog ou Drawer: a página ao
|
|
88
|
+
fundo sustenta o modal, mas não é uma etapa para onde voltar. Formulários modais dimensionam o
|
|
89
|
+
rodapé pelo conteúdo, com `Cancelar` em `ghost` e a ação principal sólida.
|
|
90
|
+
|
|
91
|
+
Dentro de [`PageShell`](/ui/page), a superfície `page` usa a anatomia real da aplicação. O
|
|
92
|
+
`PageHeader` recebe só a navegação e as ações globais; o título abre o conteúdo em `PageIntro`, e o
|
|
93
|
+
corpo vai para `PageBody`, com `PageFooter` quando há ações de rodapé.
|
|
94
|
+
|
|
95
|
+
Uma Presentation de listagem exibida como página muda essa distribuição para manter a criação perto
|
|
96
|
+
da coleção. O `PageBody` recebe um [`Content`](/ui/content) com a
|
|
97
|
+
[`ActionList`](/ui/action-list), e o cabeçalho desse `Content` recebe o título e os comandos
|
|
98
|
+
declarados com `placement: "header"`, no extremo oposto ao título. Os comandos de cabeçalho usam o
|
|
99
|
+
tamanho `default` e permanecem textuais, mesmo quando a action declara um ícone: o que abre outra
|
|
100
|
+
Presentation é um `Button` sólido, e uma action `simple` vira um
|
|
101
|
+
[`ActionTrigger`](/ui/action-trigger) `ghost`. O rótulo vem do `label` da action, como `Criar workspace`.
|
|
102
|
+
|
|
103
|
+
Não envolva a Presentation em um card para simular a página. Valide proporção, rolagem e ações no
|
|
104
|
+
shell que efetivamente hospeda a rota.
|
|
105
|
+
|
|
106
|
+
## Assistência contextual
|
|
107
|
+
|
|
108
|
+
Quando o recurso aceita uma conversa sobre ele, declare em `assistant` somente o gatilho
|
|
109
|
+
serializável, como no exemplo acima. O renderer coloca o gatilho no cabeçalho e abre o painel à
|
|
110
|
+
direita do corpo da própria página, dialog ou drawer. Cabeçalho e rodapé continuam ocupando toda a
|
|
111
|
+
largura; enquanto o painel está aberto, o gatilho sai do cabeçalho e o painel assume a identidade
|
|
112
|
+
visual da assistência.
|
|
113
|
+
|
|
114
|
+
O dado vivo do recurso não entra na definição. A página que carregou o recurso fornece
|
|
115
|
+
`assistant.render`, reúne identidade, nome e demais dados autorizados e monta o chat. Assim, o
|
|
116
|
+
contexto da conversa não vira autorização nem dado persistido no manifest.
|
|
117
|
+
|
|
118
118
|
## A declaração
|
|
119
119
|
|
|
120
120
|
`definePresentation` cria o artefato serializável. O body recebe uma action `form`, `list` ou
|
|
121
|
-
`view
|
|
122
|
-
`body.submitLabel` sobrescreve o padrão
|
|
123
|
-
como “Renomear” ou “Criar nova versão”.
|
|
124
|
-
interface abrem outra Presentation. Bindings fornecem valores vindos da
|
|
125
|
-
seleção, da sessão ou do resultado anterior sem guardar callbacks no JSON.
|
|
121
|
+
`view`, ou um componente React, descrito em “Conteúdo React no body”; uma action `simple` aparece
|
|
122
|
+
como comando no cabeçalho ou no rodapé. Em formulário, `body.submitLabel` sobrescreve o padrão
|
|
123
|
+
“Salvar” quando a operação tem outro resultado observável, como “Renomear” ou “Criar nova versão”.
|
|
124
|
+
Actions que precisam de interface abrem outra Presentation. Bindings fornecem valores vindos da
|
|
125
|
+
rota, do registro, da seleção, da sessão ou do resultado anterior sem guardar callbacks no JSON.
|
|
126
126
|
|
|
127
127
|
Quando a exibição precisa sobreviver a refresh, deep link ou histórico, declare `route` na própria
|
|
128
128
|
Presentation. O `path` aceita segmentos fixos e parâmetros como `:id`; `surface` determina como a
|
|
@@ -154,6 +154,48 @@ const workspaceList = definePresentation({
|
|
|
154
154
|
});
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
## Conteúdo React no body
|
|
158
|
+
|
|
159
|
+
Uma tela que já existe em React pode adotar a casca da Presentation — rota, superfície, título,
|
|
160
|
+
navegação e ações — sem migrar o miolo para actions declarativas de uma vez. Declare
|
|
161
|
+
`body: { component }` com um identificador estável para esse conteúdo e passe o conteúdo como
|
|
162
|
+
`children` do `Presentation`:
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
const appearance = definePresentation({
|
|
166
|
+
schemaVersion: 1,
|
|
167
|
+
id: "settings.appearance",
|
|
168
|
+
title: "Aparência",
|
|
169
|
+
route: { path: "/settings/appearance", surface: "page" },
|
|
170
|
+
body: { component: "settings.appearance.content" },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
<Presentation
|
|
174
|
+
definition={appearance}
|
|
175
|
+
definitions={[appearance]}
|
|
176
|
+
actions={{}}
|
|
177
|
+
invocation={invocation}
|
|
178
|
+
>
|
|
179
|
+
<AppearanceSettings />
|
|
180
|
+
</Presentation>;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
O identificador de `component` vai para o manifest; o JSX, não. Uma Presentation com body de
|
|
184
|
+
componente exige `children`, e uma com body de action os recusa: nos dois casos, a renderização
|
|
185
|
+
lança um erro que nomeia a Presentation.
|
|
186
|
+
|
|
187
|
+
Com `heading: "shell"`, o padrão, a casca emite o título da definição. Quando o conteúdo React já
|
|
188
|
+
declara o heading principal da página, como um `Content level={1}`, use `heading: "content"`: dentro
|
|
189
|
+
de `PageShell`, a casca deixa de emitir o `PageIntro` e o documento continua com um único `h1`. Em
|
|
190
|
+
Dialog e Drawer, o título continua no cabeçalho da superfície.
|
|
191
|
+
|
|
192
|
+
Se o conteúdo precisa ocupar a altura da página e controlar a própria rolagem, como uma lista com
|
|
193
|
+
cabeçalho fixo, passe `layout="fill"` ao `Presentation`. Com o padrão `flow`, a página rola como um
|
|
194
|
+
documento. Uma [`ActionList`](/ui/action-list) filha direta do conteúdo React recebe essa altura e
|
|
195
|
+
assume `fill` sozinha.
|
|
196
|
+
|
|
197
|
+
## Registrar as declarações
|
|
198
|
+
|
|
157
199
|
Registre as declarações em `presentations` no `opus.config.ts`. `opus gen` valida as referências e
|
|
158
200
|
projeta o JSON no manifest; a Lens pode então exibir o artefato completo sem reconstruí-lo a partir
|
|
159
201
|
do JSX.
|
|
@@ -161,7 +203,7 @@ do JSX.
|
|
|
161
203
|
## A invocação
|
|
162
204
|
|
|
163
205
|
`definePresentationInvocation` cria o estado vivo de uma exibição. Ele contém somente valores JSON,
|
|
164
|
-
a
|
|
206
|
+
a superfície atual e a pilha necessária para voltar. A definição continua estática no manifest; input
|
|
165
207
|
de rota, sessão e resultado existe apenas na invocação.
|
|
166
208
|
|
|
167
209
|
```ts
|
|
@@ -194,7 +236,7 @@ no manifest e concentre a inspeção na Lens em vez de criar um launcher flutuan
|
|
|
194
236
|
| `definition` | `PresentationDefinition` | | Artefato estático que descreve o recurso. |
|
|
195
237
|
| `definitions` | `PresentationDefinition[]` | | Registry alcançável por navegação, abertura e retorno. |
|
|
196
238
|
| `actions` | `PresentationActionRegistry` | | Contratos compartilháveis referenciados pelo artefato. |
|
|
197
|
-
| `invocation` | `PresentationInvocation` | |
|
|
239
|
+
| `invocation` | `PresentationInvocation` | | Superfície, input e pilha da exibição atual. |
|
|
198
240
|
| `bindingContext` | `PresentationBindingContext` | | Rota, item, seleção, sessão e resultado disponíveis. |
|
|
199
241
|
| `assistant` | `PresentationAssistantRuntime` | | Painel contextual fornecido pela superfície hospedeira. |
|
|
200
242
|
| `onInvocationChange` | `(next: PresentationInvocation \| null) => void` | | Recebe navegação, retorno e fechamento. |
|
|
@@ -205,6 +247,8 @@ no manifest e concentre a inspeção na Lens em vez de criar um launcher flutuan
|
|
|
205
247
|
| `onOpenChange` | `(open: boolean) => void` | | Notifica abertura e fechamento da superfície modal. |
|
|
206
248
|
| `className` | `string` | | Classes adicionais da superfície. |
|
|
207
249
|
| `bodyClassName` | `string` | | Classes adicionais do body, aplicadas uma única vez. |
|
|
250
|
+
| `layout` | `'flow' \| 'fill'` | `'flow'` | Em `page`, `fill` limita a altura ao shell e entrega a rolagem ao conteúdo do body; a lista de um body `list` ocupa essa altura e rola só os resultados. Dialog e Drawer ignoram. |
|
|
251
|
+
| `children` | `ReactNode` | | Conteúdo React do body declarado com `body.component`. Obrigatório nesse caso e recusado com `body.action`. |
|
|
208
252
|
|
|
209
253
|
## Propriedades de PresentationInspector
|
|
210
254
|
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
## Progresso determinado
|
|
2
2
|
|
|
3
|
-
Use `Progress` quando a tarefa informar
|
|
4
|
-
100 e anima o preenchimento a cada mudança.
|
|
3
|
+
Use `Progress` quando a tarefa informar quanto já foi concluído. `value` aceita valores de 0 a
|
|
4
|
+
`max`, que por padrão é 100, e anima o preenchimento a cada mudança. Para contar etapas, declare
|
|
5
|
+
`max` com o total: `value={3} max={4}` preenche três quartos da barra. Quando a duração da espera
|
|
6
|
+
não é conhecida, use [`Spinner`](/ui/spinner).
|
|
7
|
+
|
|
8
|
+
A barra é exposta aos leitores de tela como `progressbar`, com o valor atual, mas o texto ao lado
|
|
9
|
+
dela não a nomeia automaticamente. Ligue o rótulo visível à barra com `aria-labelledby` ou, sem
|
|
10
|
+
rótulo visível, passe `aria-label`.
|
|
5
11
|
|
|
6
12
|
```tsx preview col
|
|
7
13
|
<div className="w-full max-w-sm space-y-2">
|
|
8
14
|
<div className="flex items-center justify-between text-sm">
|
|
9
|
-
<span>opus check</span>
|
|
15
|
+
<span id="progress-check">opus check</span>
|
|
10
16
|
<span className="text-muted-foreground">65%</span>
|
|
11
17
|
</div>
|
|
12
|
-
<Progress value={65} />
|
|
18
|
+
<Progress value={65} aria-labelledby="progress-check" />
|
|
13
19
|
</div>
|
|
14
20
|
```
|
|
15
21
|
|
|
22
|
+
Sem `value`, ou com `null`, a barra fica vazia e é exposta como progresso indeterminado.
|
|
23
|
+
|
|
16
24
|
## Controlado
|
|
17
25
|
|
|
18
26
|
Controle `value` externamente quando o progresso acompanhar uma tarefa em andamento. No exemplo,
|
|
@@ -20,15 +28,16 @@ cada clique avança uma etapa da sincronização.
|
|
|
20
28
|
|
|
21
29
|
```tsx preview col
|
|
22
30
|
const [step, setStep] = useState(40)
|
|
31
|
+
const labelId = useId()
|
|
23
32
|
const advance = (): void => setStep((prev) => Math.min(prev + 20, 100))
|
|
24
33
|
|
|
25
34
|
render(
|
|
26
35
|
<div className="w-full max-w-sm space-y-3">
|
|
27
36
|
<div className="flex items-center justify-between text-sm">
|
|
28
|
-
<span>Sincronizar Empresa X</span>
|
|
37
|
+
<span id={labelId}>Sincronizar Empresa X</span>
|
|
29
38
|
<span className="text-muted-foreground">{step}%</span>
|
|
30
39
|
</div>
|
|
31
|
-
<Progress value={step} />
|
|
40
|
+
<Progress value={step} aria-labelledby={labelId} />
|
|
32
41
|
<Button variant="outline" size="sm" onClick={advance}>
|
|
33
42
|
Avançar etapa
|
|
34
43
|
</Button>
|
|
@@ -39,18 +48,19 @@ render(
|
|
|
39
48
|
## Altura e contexto
|
|
40
49
|
|
|
41
50
|
`context` diz o que o andamento comunica — `success` para meta atingida, `warning` ou `danger`
|
|
42
|
-
para atenção — e tinge trilha e preenchimento com a família semântica.
|
|
43
|
-
|
|
51
|
+
para atenção — e tinge trilha e preenchimento com a família semântica. A cor reforça o significado,
|
|
52
|
+
mas não o substitui: mantenha o rótulo e o valor legíveis. `className` compõe sobre o padrão para
|
|
53
|
+
ajustar a altura.
|
|
44
54
|
|
|
45
55
|
```tsx preview col
|
|
46
56
|
<div className="w-full max-w-sm space-y-4">
|
|
47
57
|
<div className="space-y-1.5">
|
|
48
|
-
<span className="text-sm">Cobertura de testes</span>
|
|
49
|
-
<Progress value={92} context="success" className="h-1.5" />
|
|
58
|
+
<span id="progress-coverage" className="text-sm">Cobertura de testes</span>
|
|
59
|
+
<Progress value={92} context="success" className="h-1.5" aria-labelledby="progress-coverage" />
|
|
50
60
|
</div>
|
|
51
61
|
<div className="space-y-1.5">
|
|
52
|
-
<span className="text-sm">Skills cobertas pelo revisor</span>
|
|
53
|
-
<Progress value={45} context="warning" className="h-3" />
|
|
62
|
+
<span id="progress-review" className="text-sm">Skills cobertas pelo revisor</span>
|
|
63
|
+
<Progress value={45} context="warning" className="h-3" aria-labelledby="progress-review" />
|
|
54
64
|
</div>
|
|
55
65
|
</div>
|
|
56
66
|
```
|
|
@@ -59,6 +69,9 @@ padrão para ajustar a altura.
|
|
|
59
69
|
|
|
60
70
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
61
71
|
|---|---|---|---|
|
|
62
|
-
| `value` | `number \| null` | | O progresso de 0 a
|
|
72
|
+
| `value` | `number \| null` | | O progresso de 0 a `max`. O preenchimento anima a cada mudança; sem valor, a barra fica vazia e indeterminada. |
|
|
73
|
+
| `max` | `number` | `100` | O valor que representa a conclusão. O preenchimento é a fração `value / max`. |
|
|
63
74
|
| `context` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'primary'` | O que o andamento comunica; tinge trilha e preenchimento pela família `--context-*`. |
|
|
64
|
-
| `
|
|
75
|
+
| `aria-label` | `string` | | Nome acessível da barra quando não há rótulo visível. |
|
|
76
|
+
| `aria-labelledby` | `string` | | `id` do rótulo visível que nomeia a barra. |
|
|
77
|
+
| `className` | `string` | | Compõe sobre o padrão — ajuste a altura (`h-1.5`, `h-3`). |
|
|
@@ -5,7 +5,7 @@ title: Filas
|
|
|
5
5
|
# Filas
|
|
6
6
|
|
|
7
7
|
Use uma action em background quando a operação não puder manter a requisição aberta. O
|
|
8
|
-
`QueueAdapter` enfileira o trabalho e devolve um `JobHandle` para
|
|
8
|
+
`QueueAdapter` enfileira o trabalho e devolve um `JobHandle` para acompanhar o andamento.
|
|
9
9
|
|
|
10
10
|
## O contrato
|
|
11
11
|
|
|
@@ -21,18 +21,21 @@ interface JobHandle<T = unknown> {
|
|
|
21
21
|
jobId: string
|
|
22
22
|
action: string
|
|
23
23
|
status: 'queued' | 'running' | 'done' | 'failed' | 'cancelled'
|
|
24
|
-
progress?: { percent?: number; message?: string }
|
|
24
|
+
progress?: { percent?: number; message?: string; data?: unknown }
|
|
25
25
|
data?: T
|
|
26
26
|
error?: ActionError
|
|
27
|
+
enqueuedAt: string
|
|
28
|
+
startedAt?: string
|
|
29
|
+
finishedAt?: string
|
|
27
30
|
attempts: number
|
|
28
31
|
trace?: TraceContext
|
|
29
32
|
}
|
|
30
33
|
```
|
|
31
34
|
|
|
32
|
-
`runtime.execute()` valida a chamada, cria o `JobSpec` e inclui o trace efetivo, request ID e
|
|
33
|
-
provenance original. O adapter persiste
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
`runtime.execute()` valida a chamada, cria o `JobSpec` e inclui o trace efetivo, o request ID e a
|
|
36
|
+
provenance original. O adapter persiste esse envelope, e o handle também expõe o trace. No worker,
|
|
37
|
+
`runtime.executeJob()` revalida o envelope e executa um span filho, preservando a cadeia requisição
|
|
38
|
+
→ enfileiramento → job sem aceitar baggage ou campos extras.
|
|
36
39
|
|
|
37
40
|
## Driver
|
|
38
41
|
|
|
@@ -43,26 +46,41 @@ import { bullmqQueue } from '@softize/opus/queue/bullmq'
|
|
|
43
46
|
const queue = bullmqQueue({ queue: new Queue('opus', { connection: { host: 'localhost' } }) })
|
|
44
47
|
```
|
|
45
48
|
|
|
46
|
-
Redis
|
|
47
|
-
|
|
49
|
+
O driver usa Redis por meio do BullMQ, com durabilidade, entrega entre processos e novas tentativas.
|
|
50
|
+
`bullmq` é dependência opcional: só precisa ser instalado por quem usa o driver.
|
|
48
51
|
|
|
49
|
-
##
|
|
52
|
+
## Declarar a action em background
|
|
53
|
+
|
|
54
|
+
Configure a fila no runtime e declare `background` no binding da action:
|
|
50
55
|
|
|
51
56
|
```ts
|
|
52
57
|
const runtime = createRuntime({
|
|
53
58
|
// …server/data…
|
|
54
59
|
queue,
|
|
55
60
|
})
|
|
61
|
+
|
|
62
|
+
export const relatorioGerar = bindAction(relatorioGerarContract, {
|
|
63
|
+
background: { enabled: true, retry: { attempts: 3 }, progress: true },
|
|
64
|
+
handler: async (ctx, input, loaded, progress) => {
|
|
65
|
+
await progress?.report({ percent: 50, message: 'Consultando dados' })
|
|
66
|
+
return gerarRelatorio(input)
|
|
67
|
+
},
|
|
68
|
+
})
|
|
56
69
|
```
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
`
|
|
60
|
-
`
|
|
71
|
+
Só actions `simple` e `form` executam em background; além de `retry` e `progress`, a configuração
|
|
72
|
+
aceita `queue`, `priority` e `timeout`. Em uma action `list` ou `view`, o tipo do binding aceita
|
|
73
|
+
`background`, mas o runtime o ignora e executa a action na própria requisição. Para evitar essa
|
|
74
|
+
surpresa, o runtime avisa ao registrar a action fora de produção (`runtime.action_field_ignored`), e
|
|
75
|
+
o [`opus check`](/docs/cli) aponta a regra `ignored-by-kind`. Sem `QueueAdapter`, a chamada falha com
|
|
76
|
+
`runtime.queue_required`. Chamar a action devolve um `JobHandle` em vez do resultado. O Opus não
|
|
77
|
+
monta uma rota de acompanhamento: exponha `queue.status(jobId)` ou `queue.subscribe` por uma action
|
|
78
|
+
ou rota do próprio app até o job chegar a `done` ou `failed`.
|
|
61
79
|
|
|
62
80
|
## Worker BullMQ
|
|
63
81
|
|
|
64
|
-
O app continua dono do `Worker` e
|
|
65
|
-
`executeJob()
|
|
82
|
+
O app continua dono do `Worker` e de reconstruir a autenticação de quem enfileirou. O processor
|
|
83
|
+
chama `executeJob()`, e não `execute()`, que criaria outro job:
|
|
66
84
|
|
|
67
85
|
```ts
|
|
68
86
|
new Worker('opus', async (job) => {
|
|
@@ -76,13 +94,13 @@ new Worker('opus', async (job) => {
|
|
|
76
94
|
}, { connection })
|
|
77
95
|
```
|
|
78
96
|
|
|
79
|
-
O runtime confirma que o
|
|
80
|
-
trace e provenance vêm do envelope. O driver
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
envelope para o worker aplicar,
|
|
97
|
+
O runtime confirma que o usuário reconstruído corresponde a `spec.ctx.userId`; tenant, request,
|
|
98
|
+
trace e provenance vêm do envelope. O driver traduz `attempts`, o backoff nativo e a prioridade para
|
|
99
|
+
o BullMQ. Um backoff exponencial com multiplicador diferente de 2 ou com `maxMs` exige uma
|
|
100
|
+
`backoffStrategy` própria no worker, e o driver o recusa em vez de ignorá-lo em silêncio. `timeout`
|
|
101
|
+
permanece no envelope para o worker aplicar, porque não é uma opção de execução do `Queue.add`.
|
|
84
102
|
|
|
85
103
|
## Limites atuais
|
|
86
104
|
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
O único driver é o `bullmq`. Uma fila em memória para desenvolvimento e outros backends, como SQS e
|
|
106
|
+
pg-boss, ainda não existem.
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
## Escolha única
|
|
2
2
|
|
|
3
|
-
Use `RadioGroup` quando todas as opções mutuamente exclusivas precisarem permanecer visíveis.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use `RadioGroup` quando todas as opções mutuamente exclusivas precisarem permanecer visíveis. Com
|
|
4
|
+
muitas opções, prefira [`Select`](/ui/select); para ligar ou desligar um único item, use
|
|
5
|
+
[`Checkbox`](/ui/checkbox) ou [`Switch`](/ui/switch). Cada `RadioGroupItem` declara um `value` e
|
|
6
|
+
deve estar associado a um `Label`, escrito logo depois do item. `defaultValue` define a opção
|
|
7
|
+
inicial no modo não controlado.
|
|
6
8
|
|
|
7
9
|
```tsx preview col-start
|
|
8
10
|
<RadioGroup defaultValue="balanced">
|
|
@@ -68,6 +70,25 @@ bloquear todas as opções, aplique `disabled` ao `RadioGroup`.
|
|
|
68
70
|
</RadioGroup>
|
|
69
71
|
```
|
|
70
72
|
|
|
73
|
+
## Estado inválido
|
|
74
|
+
|
|
75
|
+
Quando a escolha for obrigatória e nenhuma opção estiver marcada, declare `aria-invalid` em cada
|
|
76
|
+
`RadioGroupItem` e explique o que falta em um texto associado ao grupo por `aria-describedby`.
|
|
77
|
+
|
|
78
|
+
```tsx preview col-start
|
|
79
|
+
<RadioGroup aria-describedby="review-mode-error">
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<RadioGroupItem id="review-auto" value="auto" aria-invalid />
|
|
82
|
+
<Label htmlFor="review-auto">Revisar automaticamente</Label>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<RadioGroupItem id="review-manual" value="manual" aria-invalid />
|
|
86
|
+
<Label htmlFor="review-manual">Revisar manualmente</Label>
|
|
87
|
+
</div>
|
|
88
|
+
</RadioGroup>
|
|
89
|
+
<FieldError id="review-mode-error">Escolha como a sessão será revisada.</FieldError>
|
|
90
|
+
```
|
|
91
|
+
|
|
71
92
|
## Propriedades de RadioGroup
|
|
72
93
|
|
|
73
94
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
@@ -82,4 +103,5 @@ bloquear todas as opções, aplique `disabled` ao `RadioGroup`.
|
|
|
82
103
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
83
104
|
|---|---|---|---|
|
|
84
105
|
| `value` | `string` | | Valor que o item atribui ao grupo quando selecionado. |
|
|
85
|
-
| `disabled` | `boolean` | `false` | Desabilita somente este item; o `Label`
|
|
106
|
+
| `disabled` | `boolean` | `false` | Desabilita somente este item; o `Label` escrito logo depois dele acompanha o estado. |
|
|
107
|
+
| `aria-invalid` | `boolean` | | Aplica a borda e o anel de erro ao item. |
|
|
@@ -19,11 +19,13 @@ function App() {
|
|
|
19
19
|
O router oferece leitura reativa da URL e `navigate`; o aplicativo decide o que renderizar. Ele não
|
|
20
20
|
inclui tabela de rotas, parâmetros tipados nem carregamento de dados.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Se a tela precisa casar padrões como `/users/:id/posts/:postId`, receber parâmetros tipados ou
|
|
23
|
+
carregar dados por rota, use uma biblioteca de rotas. Um app de back-office com uma dúzia de
|
|
24
|
+
destinos costuma dispensar esses recursos.
|
|
23
25
|
|
|
24
26
|
## As quatro peças
|
|
25
27
|
|
|
26
|
-
| | |
|
|
28
|
+
| API | Comportamento |
|
|
27
29
|
|---|---|
|
|
28
30
|
| `usePathname()` | O pathname como estado reativo. |
|
|
29
31
|
| `useSegments()` | Os segmentos, já sem os vazios: `/settings/users` → `['settings','users']`. |
|
|
@@ -32,15 +34,23 @@ A régua: se a sua tela precisa de casamento de padrão (`/users/:id/posts/:post
|
|
|
32
34
|
|
|
33
35
|
## Comportamentos preservados
|
|
34
36
|
|
|
35
|
-
**
|
|
37
|
+
**Navegar para o destino atual não cria entrada.** `navigate` resolve o destino com `new URL` e
|
|
38
|
+
compara o `href` inteiro; se a pessoa já está exatamente lá, nada acontece. Assim, clicar duas vezes
|
|
39
|
+
no mesmo item do menu não empilha entradas idênticas, e o botão Voltar continua saindo da página.
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
A comparação usa a URL resolvida, e não o texto cru, por dois motivos. O hash: estando em
|
|
42
|
+
`/a#secao`, `navigate('/a')` precisa remover a âncora, e não ser tratado como destino repetido. E a
|
|
43
|
+
codificação: `window.location` devolve `/relatórios` como `/relat%C3%B3rios`, então uma comparação
|
|
44
|
+
crua nunca casaria e cada clique empilharia uma entrada, o que afeta caminhos acentuados e
|
|
45
|
+
chamadas como ``navigate(`/reports?report=${arquivo}`)`` com nome de arquivo acentuado.
|
|
38
46
|
|
|
39
47
|
**A notificação usa um evento do navegador.** Como `pushState` não dispara `popstate`, `navigate`
|
|
40
48
|
emite o evento explicitamente. Isso mantém listeners existentes e cópias diferentes do pacote
|
|
41
49
|
sincronizados pela mesma janela.
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
**Todos os componentes leem a mesma URL.** Os hooks usam `useSyncExternalStore`. Ler
|
|
52
|
+
`window.location` dentro de `useState` ou `useEffect` permite que, na renderização concorrente, dois
|
|
53
|
+
componentes mostrem URLs diferentes no mesmo commit.
|
|
44
54
|
|
|
45
55
|
## `replace`: quando o passo não é história
|
|
46
56
|
|
|
@@ -48,14 +58,23 @@ sincronizados pela mesma janela.
|
|
|
48
58
|
navigate(`/reports?report=${file}`, { replace: true })
|
|
49
59
|
```
|
|
50
60
|
|
|
51
|
-
Use quando a mudança não
|
|
61
|
+
Use `replace` quando a mudança não deve virar um passo do botão Voltar: um rascunho que ganha id no
|
|
62
|
+
meio do fluxo, um filtro que a pessoa ajusta várias vezes seguidas. Para uma navegação entre
|
|
63
|
+
destinos, mantenha o padrão, `pushState`.
|
|
52
64
|
|
|
53
65
|
## SSR
|
|
54
66
|
|
|
55
|
-
|
|
67
|
+
No servidor, os hooks usam um valor fixo (`/` e busca vazia): renderizam sem `window` e passam a
|
|
68
|
+
refletir a URL real no primeiro render do cliente. Se a página precisa do caminho correto no HTML
|
|
69
|
+
servido, receba o caminho do framework por prop.
|
|
56
70
|
|
|
57
|
-
|
|
71
|
+
`navigate` acessa `window` diretamente e lança um erro quando chamado no servidor. Ler a URL fora
|
|
72
|
+
do navegador tem uma resposta razoável; navegar não tem, e ignorar a chamada em silêncio esconderia
|
|
73
|
+
o erro de uso.
|
|
58
74
|
|
|
59
|
-
##
|
|
75
|
+
## Validar destinos vindos de fora
|
|
60
76
|
|
|
61
|
-
`navigate` espera um caminho da própria origem
|
|
77
|
+
`navigate` espera um caminho da própria origem e não valida destinos. Quando o destino vem de fora,
|
|
78
|
+
como o `?redirect=` de um pós-login, valide-o antes de navegar: resolva com
|
|
79
|
+
`new URL(valor, origin)` e compare o `origin` do resultado, em vez de conferir apenas o prefixo do
|
|
80
|
+
texto.
|