@softize/opus 18.5.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 +275 -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 +298 -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 +16 -12
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +103 -73
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +81 -54
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +51 -38
- package/src/ui/components/patterns/surface-anatomy.ts +246 -0
- package/src/ui/components/patterns/surface-header.tsx +51 -6
- package/src/ui/components/patterns/surface-media.tsx +69 -0
- 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/card.tsx +91 -29
- 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 +14 -5
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +179 -45
- package/src/ui/components/primitives/drawer.tsx +131 -24
- 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/metric-card.tsx +98 -39
- 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 +252 -35
- 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 +37 -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 +219 -28
- 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 +193 -22
- 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 +68 -21
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +125 -11
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +300 -0
- 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 +25 -45
- package/src/ui/docs/content/skeleton.md +13 -4
- 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 +34 -20
- 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 +14 -2
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +38 -32
- package/src/ui/react.tsx +14 -0
- package/src/vite/design.ts +16 -4
|
@@ -3,52 +3,77 @@
|
|
|
3
3
|
Use `Slider` para escolher um valor dentro de uma faixa. `defaultValue` recebe um array; `[n]`
|
|
4
4
|
representa um único controle. `min`, `max` e `step` delimitam os valores disponíveis.
|
|
5
5
|
|
|
6
|
+
Quem recebe o foco e anuncia o valor é o controle arrastável, chamado de thumb. Sem rótulo
|
|
7
|
+
visível, passe `aria-label` ao `Slider`: ele repassa o nome ao thumb.
|
|
8
|
+
|
|
6
9
|
```tsx preview col
|
|
7
|
-
<Slider defaultValue={[40]} min={0} max={100} step={5} />
|
|
10
|
+
<Slider aria-label="Opacidade" defaultValue={[40]} min={0} max={100} step={5} />
|
|
8
11
|
```
|
|
9
12
|
|
|
10
13
|
## Controlado
|
|
11
14
|
|
|
12
15
|
`onValueChange` recebe o array atualizado. Para um único controle, leia `value[0]`.
|
|
13
16
|
|
|
17
|
+
Com um rótulo visível, ligue-o ao `Slider` por `aria-labelledby`. O `htmlFor` do `Label` não
|
|
18
|
+
alcança o thumb, porque ele não é um campo nativo.
|
|
19
|
+
|
|
14
20
|
```tsx preview col
|
|
15
21
|
const [parallelism, setParallelism] = useState([4])
|
|
16
22
|
|
|
17
23
|
render(
|
|
18
|
-
<div
|
|
24
|
+
<div className="grid w-full gap-3">
|
|
19
25
|
<div className="flex items-center justify-between">
|
|
20
26
|
<Label id="parallelism-label">Sessões em paralelo</Label>
|
|
21
27
|
<span className="text-sm text-muted-foreground">{parallelism[0]}</span>
|
|
22
28
|
</div>
|
|
23
|
-
<Slider
|
|
29
|
+
<Slider
|
|
30
|
+
aria-labelledby="parallelism-label"
|
|
31
|
+
value={parallelism}
|
|
32
|
+
onValueChange={setParallelism}
|
|
33
|
+
min={1}
|
|
34
|
+
max={8}
|
|
35
|
+
step={1}
|
|
36
|
+
/>
|
|
24
37
|
</div>,
|
|
25
38
|
)
|
|
26
39
|
```
|
|
27
40
|
|
|
28
41
|
## Intervalo
|
|
29
42
|
|
|
30
|
-
Dois números em `value` criam um intervalo selecionável entre dois
|
|
43
|
+
Dois números em `value` criam um intervalo selecionável entre dois thumbs, e cada um precisa de um
|
|
44
|
+
nome próprio. Passe os nomes em `thumbLabels`, na ordem dos valores. Sem `thumbLabels`, o nome comum
|
|
45
|
+
se compõe com a posição: com `aria-label`, “Custo, mínimo” e “Custo, máximo”; com
|
|
46
|
+
`aria-labelledby`, o texto do rótulo seguido de “Mínimo” ou “Máximo”. Sem nome algum, o leitor de
|
|
47
|
+
tela anuncia só “Mínimo” e “Máximo”, sem dizer do quê.
|
|
31
48
|
|
|
32
49
|
```tsx preview col
|
|
33
50
|
const [budget, setBudget] = useState([20, 60])
|
|
34
51
|
|
|
35
52
|
render(
|
|
36
|
-
<div
|
|
53
|
+
<div className="grid w-full gap-3">
|
|
37
54
|
<div className="flex items-center justify-between">
|
|
38
|
-
<Label
|
|
55
|
+
<Label>Custo estimado (US$)</Label>
|
|
39
56
|
<span className="text-sm text-muted-foreground">{budget[0]} – {budget[1]}</span>
|
|
40
57
|
</div>
|
|
41
|
-
<Slider
|
|
58
|
+
<Slider
|
|
59
|
+
thumbLabels={['Custo mínimo', 'Custo máximo']}
|
|
60
|
+
value={budget}
|
|
61
|
+
onValueChange={setBudget}
|
|
62
|
+
min={0}
|
|
63
|
+
max={100}
|
|
64
|
+
step={5}
|
|
65
|
+
/>
|
|
42
66
|
</div>,
|
|
43
67
|
)
|
|
44
68
|
```
|
|
45
69
|
|
|
46
70
|
## Desabilitado
|
|
47
71
|
|
|
48
|
-
disabled esmaece o trilho e o thumb e bloqueia o arraste
|
|
72
|
+
`disabled` esmaece o trilho e o thumb e bloqueia o arraste. O valor fica congelado no
|
|
73
|
+
`defaultValue`.
|
|
49
74
|
|
|
50
75
|
```tsx preview col
|
|
51
|
-
<Slider defaultValue={[70]} min={0} max={100} disabled />
|
|
76
|
+
<Slider aria-label="Opacidade" defaultValue={[70]} min={0} max={100} disabled />
|
|
52
77
|
```
|
|
53
78
|
|
|
54
79
|
## Propriedades de Slider
|
|
@@ -63,3 +88,6 @@ disabled esmaece o trilho e o thumb e bloqueia o arraste — o valor fica congel
|
|
|
63
88
|
| `step` | `number` | `1` | Incremento entre valores possíveis. |
|
|
64
89
|
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção do trilho — vertical exige altura no container. |
|
|
65
90
|
| `disabled` | `boolean` | `false` | Esmaece e bloqueia o arraste. |
|
|
91
|
+
| `aria-label` | `string` | | Nome do thumb quando não há rótulo visível. Em intervalo, prefira `thumbLabels`. |
|
|
92
|
+
| `aria-labelledby` | `string` | | `id` do rótulo visível que nomeia o thumb. |
|
|
93
|
+
| `thumbLabels` | `readonly string[]` | | Um nome por thumb, na ordem de `value`. Prevalece sobre `aria-label` e `aria-labelledby`. |
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Use `Spinner` quando a duração ou o progresso da espera não forem conhecidos. `size` usa os nomes da
|
|
4
4
|
escala dos controles e mede o glifo do controle homônimo: `sm` é o ícone de um `Button` `sm`, `lg` o
|
|
5
5
|
de um `lg`. O ícone é decorativo; quem nomeia a espera é o contêiner (`role="status"`, como fazem
|
|
6
|
-
`DataState` e `PageState`) ou o texto ao lado.
|
|
6
|
+
[`DataState`](/ui/data-state) e `PageState`) ou o texto ao lado.
|
|
7
7
|
|
|
8
8
|
```tsx preview
|
|
9
9
|
<Spinner size="xs" />
|
|
@@ -14,7 +14,7 @@ de um `lg`. O ícone é decorativo; quem nomeia a espera é o contêiner (`role=
|
|
|
14
14
|
|
|
15
15
|
## No botão
|
|
16
16
|
|
|
17
|
-
Durante uma ação, use `busy` no `Button` com um rótulo que descreva o andamento. O botão fica
|
|
17
|
+
Durante uma ação, use `busy` no [`Button`](/ui/button) com um rótulo que descreva o andamento. O botão fica
|
|
18
18
|
desabilitado e o spinner ocupa o lugar do ícone, no glifo do tamanho do botão.
|
|
19
19
|
|
|
20
20
|
```tsx preview
|
|
@@ -24,7 +24,8 @@ desabilitado e o spinner ocupa o lugar do ícone, no glifo do tamanho do botão.
|
|
|
24
24
|
## Em carga de conteúdo
|
|
25
25
|
|
|
26
26
|
Espera curta sem forma definida; o contêiner nomeia o estado. Para uma consulta inteira, `DataState`
|
|
27
|
-
já monta esse contêiner. Quando o conteúdo tem forma conhecida
|
|
27
|
+
já monta esse contêiner. Quando o conteúdo tem forma conhecida, como uma lista ou um card, prefira
|
|
28
|
+
[`Skeleton`](/ui/skeleton).
|
|
28
29
|
|
|
29
30
|
```tsx preview
|
|
30
31
|
<div role="status" className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
@@ -8,10 +8,10 @@ layout permanece estático.
|
|
|
8
8
|
render(
|
|
9
9
|
<div className="h-72 overflow-hidden rounded-lg border border-border">
|
|
10
10
|
<Split resizable>
|
|
11
|
-
<Pane initialSize={30} minSize={20}
|
|
12
|
-
<
|
|
11
|
+
<Pane initialSize={30} minSize={20} className="bg-muted/30">
|
|
12
|
+
<p className="text-sm">Navegação</p>
|
|
13
13
|
</Pane>
|
|
14
|
-
<Pane grow
|
|
14
|
+
<Pane grow>
|
|
15
15
|
<p className="text-sm text-muted-foreground">
|
|
16
16
|
Conteúdo. Arraste a divisória.
|
|
17
17
|
</p>
|
|
@@ -21,26 +21,44 @@ render(
|
|
|
21
21
|
);
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Escreva cada `Pane` diretamente dentro do `Split`; um fragment pode agrupá-los, como em uma
|
|
25
|
+
renderização condicional que devolve dois panes. O `Split` lê o tamanho de cada `Pane` antes de
|
|
26
|
+
renderizá-lo, então não enxerga um `Pane` devolvido por outro componente: nesse caso, escreva o
|
|
27
|
+
`Pane` no `Split` e passe o componente como filho dele. Qualquer outro filho interrompe a
|
|
28
|
+
renderização com um erro que aponta o nó recebido, em vez de sumir da tela sem aviso.
|
|
25
29
|
|
|
26
|
-
`
|
|
27
|
-
|
|
30
|
+
O [`opus check`](/docs/cli) antecipa esse erro com a regra `ui-structure-split`: ele aponta o filho
|
|
31
|
+
direto de um `Split` que o código mostra não ser `Pane`, como um elemento HTML, um texto, outro
|
|
32
|
+
componente do Opus ou um componente declarado no mesmo arquivo. Panes em fragment ou em condicional,
|
|
33
|
+
espaços e comentários são aceitos. Expressões dinâmicas, como `{children}` ou `{items.map(...)}`, e
|
|
34
|
+
componentes importados de outros módulos ficam fora do check; nesses casos, só o erro em runtime
|
|
35
|
+
revela um filho inválido.
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
O `Split` responde pela relação espacial e pelo arraste. O que existe dentro de cada área — o
|
|
38
|
+
cabeçalho, o corpo rolável, o rodapé, o recuo e a escolha entre regiões seccionadas e contínuas —
|
|
39
|
+
pertence ao [`Pane`](/ui/pane) e está documentado na página da família.
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
## Tamanho de cada pane
|
|
42
|
+
|
|
43
|
+
Cada `Pane` declara o próprio tamanho e o `Split` o aplica. Números representam porcentagens;
|
|
44
|
+
strings aceitam `%`, `rem`, `em`, `vh`, `vw` e `px`. Quando uma área precisa continuar legível em
|
|
45
|
+
telas largas, use uma unidade absoluta no tamanho inicial ou mínimo, de preferência `rem`, para
|
|
46
|
+
acompanhar a escala tipográfica configurada pela aplicação. `grow` entrega o espaço remanescente.
|
|
32
47
|
|
|
33
48
|
```tsx
|
|
34
49
|
<Split resizable>
|
|
35
|
-
<Pane grow
|
|
50
|
+
<Pane grow>
|
|
36
51
|
<Main />
|
|
37
52
|
</Pane>
|
|
38
|
-
<Pane initialSize="28rem" minSize="20rem"
|
|
53
|
+
<Pane initialSize="28rem" minSize="20rem">
|
|
39
54
|
<Inspector />
|
|
40
55
|
</Pane>
|
|
41
56
|
</Split>
|
|
42
57
|
```
|
|
43
58
|
|
|
59
|
+
`minSize` e `maxSize` valem enquanto o `Split` é redimensionável; sem `resizable` eles não limitam
|
|
60
|
+
nada.
|
|
61
|
+
|
|
44
62
|
## Persistência
|
|
45
63
|
|
|
46
64
|
Um layout pode ser restaurado sem acoplar `Split` a um mecanismo específico de armazenamento. Dê um `id` estável a cada `Pane`, leia o valor salvo para formar `defaultLayout` e persista apenas mudanças concluídas pela pessoa.
|
|
@@ -134,16 +152,9 @@ render(<Example />);
|
|
|
134
152
|
| `id` | `string` | | Identidade estável do grupo redimensionável. |
|
|
135
153
|
| `defaultLayout` | `Record<string, number>` | | Layout percentual restaurado, indexado pelos ids dos panes. |
|
|
136
154
|
| `onLayoutChanged` | `(layout, { isUserInteraction }) => void` | | Chamado ao concluir uma mudança de layout; persista onde fizer sentido. |
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
| Propriedade | Tipo | Padrão | Descrição |
|
|
141
|
-
| --------------------- | -------------------------------- | ------- | ----------------------------------------------------------------------------------------- |
|
|
142
|
-
| `id` | `string` | | Identidade estável usada pelo layout redimensionável e persistido. |
|
|
143
|
-
| `initialSize` | `number \| string` | | Tamanho inicial; número é porcentagem, string aceita `%`, `rem`, `em`, `vh`, `vw` e `px`. |
|
|
144
|
-
| `minSize` / `maxSize` | `number \| string` | | Limites quando o split é redimensionável. |
|
|
145
|
-
| `grow` | `boolean` | `false` | Ocupa o espaço remanescente no layout simples. |
|
|
146
|
-
| `inset` | `'none' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Respiro interno; `none` para chrome, navegação ou conteúdo com inset próprio. |
|
|
155
|
+
| `className` | `string` | | Ajusta o contêiner do grupo. |
|
|
156
|
+
| `data-slot` | `string` | `'split'` | Identificador semântico para testes e composição de patterns. |
|
|
157
|
+
| `children` | `ReactNode` | | Os `Pane`, diretos ou agrupados em fragment; outro filho gera erro. |
|
|
147
158
|
|
|
148
159
|
## Propriedades de SurfaceAssistant
|
|
149
160
|
|
|
@@ -156,3 +167,6 @@ render(<Example />);
|
|
|
156
167
|
| `initialSize` | `PaneSize` | `28rem` | Largura inicial do painel. |
|
|
157
168
|
| `minSize` | `PaneSize` | `20rem` | Largura mínima durante o redimensionamento. |
|
|
158
169
|
| `maxSize` | `PaneSize` | `40rem` | Largura máxima durante o redimensionamento. |
|
|
170
|
+
| `className` | `string` | | Ajusta o contêiner do split. |
|
|
171
|
+
| `contentClassName` | `string` | | Ajusta a coluna do conteúdo principal. |
|
|
172
|
+
| `assistantClassName` | `string` | | Ajusta a coluna do painel de assistência. |
|
|
@@ -4,8 +4,7 @@ title: Armazenamento
|
|
|
4
4
|
|
|
5
5
|
# Armazenamento
|
|
6
6
|
|
|
7
|
-
> **Experimental.**
|
|
8
|
-
> reincidência de caso real, não por especulação.
|
|
7
|
+
> **Experimental.** A superfície é mínima e cresce conforme casos reais a exigem.
|
|
9
8
|
|
|
10
9
|
Use `StorageAdapter` para upload, download, remoção e geração de URL de arquivos. A chave é um
|
|
11
10
|
caminho lógico, como `clientes/123/contrato.pdf`; cada driver decide onde o conteúdo é armazenado.
|
|
@@ -31,10 +30,11 @@ import { fsStorage } from '@softize/opus/storage/fs'
|
|
|
31
30
|
import { s3Storage } from '@softize/opus/storage/s3'
|
|
32
31
|
import { S3Client } from '@aws-sdk/client-s3'
|
|
33
32
|
|
|
34
|
-
//
|
|
33
|
+
// Desenvolvimento ou um único host: arquivos num diretório local; url() exige baseUrl,
|
|
34
|
+
// e o app serve esse diretório.
|
|
35
35
|
const dev = fsStorage({ root: './var/arquivos', baseUrl: 'http://localhost:5173/arquivos' })
|
|
36
36
|
|
|
37
|
-
// Produção: S3 e compatíveis (MinIO, R2…). O
|
|
37
|
+
// Produção: S3 e compatíveis (MinIO, R2…). O app cria o client com região, endpoint e credenciais.
|
|
38
38
|
const prod = s3Storage({
|
|
39
39
|
client: new S3Client({ region: 'us-east-1' }),
|
|
40
40
|
bucket: 'meu-app',
|
|
@@ -42,8 +42,8 @@ const prod = s3Storage({
|
|
|
42
42
|
})
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
`@aws-sdk/client-s3` e, para `url()`, `@aws-sdk/s3-request-presigner` são dependências opcionais,
|
|
46
|
+
carregadas só quando o driver é usado.
|
|
47
47
|
|
|
48
48
|
## No runtime
|
|
49
49
|
|
|
@@ -62,6 +62,5 @@ handler: async (ctx, input) => {
|
|
|
62
62
|
|
|
63
63
|
## Limites atuais
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
régua do Opus: comportamento vira peça na reincidência.
|
|
65
|
+
O conteúdo trafega inteiro em memória, como `Uint8Array`, sem streams; arquivos muito grandes ainda
|
|
66
|
+
não são suportados. Também não há `list` nem metadados além de `contentType`.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
## Alternar uma preferência
|
|
2
2
|
|
|
3
|
-
Use `Switch` para uma preferência booleana que entra em vigor imediatamente.
|
|
4
|
-
|
|
3
|
+
Use `Switch` para uma preferência booleana que entra em vigor imediatamente. Quando a escolha só vale
|
|
4
|
+
ao salvar um formulário, use [`Checkbox`](/ui/checkbox). Associe o controle a um [`Label`](/ui/label);
|
|
5
|
+
`defaultChecked` define o estado inicial no modo não controlado.
|
|
5
6
|
|
|
6
7
|
```tsx preview
|
|
7
8
|
<div className="flex items-center gap-2">
|
|
@@ -31,7 +32,8 @@ render(
|
|
|
31
32
|
|
|
32
33
|
## Tamanho sm
|
|
33
34
|
|
|
34
|
-
Use `size="sm"` em linhas de lista e outras composições compactas.
|
|
35
|
+
Use `size="sm"` em linhas de lista e outras composições compactas. O tamanho segue o degrau do ícone
|
|
36
|
+
na escala dos controles, e não a altura da linha: a chave `sm` acompanha o ícone de um botão `sm`.
|
|
35
37
|
|
|
36
38
|
```tsx preview col-start
|
|
37
39
|
<div className="flex items-center gap-2">
|
|
@@ -46,7 +48,8 @@ Use `size="sm"` em linhas de lista e outras composições compactas.
|
|
|
46
48
|
|
|
47
49
|
## Desabilitado
|
|
48
50
|
|
|
49
|
-
disabled esmaece e bloqueia a chave
|
|
51
|
+
`disabled` esmaece e bloqueia a chave, ligada ou desligada. O `Label` escrito logo depois da chave,
|
|
52
|
+
no mesmo contêiner, esmaece junto.
|
|
50
53
|
|
|
51
54
|
```tsx preview col-start
|
|
52
55
|
<div className="flex items-center gap-2">
|
|
@@ -63,8 +66,8 @@ disabled esmaece e bloqueia a chave — ligada ou desligada — e o rótulo em p
|
|
|
63
66
|
|
|
64
67
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
65
68
|
|---|---|---|---|
|
|
66
|
-
| `checked` | `boolean` | |
|
|
69
|
+
| `checked` | `boolean` | | Estado no modo controlado. Use com `onCheckedChange`. |
|
|
67
70
|
| `onCheckedChange` | `(checked: boolean) => void` | | Chamado a cada alternância, com o novo estado. |
|
|
68
71
|
| `defaultChecked` | `boolean` | `false` | Estado inicial no modo não controlado. |
|
|
69
|
-
| `size` | `'sm' \| 'default'` | `'default'` | Tamanho da chave, com os nomes da escala única
|
|
70
|
-
| `disabled` | `boolean` | `false` | Esmaece e bloqueia
|
|
72
|
+
| `size` | `'sm' \| 'default'` | `'default'` | Tamanho da chave, com os nomes da escala única; `sm` para linhas de lista densas. |
|
|
73
|
+
| `disabled` | `boolean` | `false` | Esmaece e bloqueia a chave; o `Label` irmão posterior esmaece junto. |
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
## Tabela de domínio
|
|
2
2
|
|
|
3
|
-
Use `Table` para dados organizados em linhas e colunas
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use `Table` para dados organizados em linhas e colunas que a tela já tem em mãos. Para uma coleção
|
|
4
|
+
pesquisável, paginada e carregada por uma action `kind: 'list'`, use [`ActionList`](/ui/action-list),
|
|
5
|
+
que monta a tabela, os filtros e os estados de carregamento, erro e vazio.
|
|
6
|
+
|
|
7
|
+
A variante padrão não possui borda externa; `TableBody` mantém somente as divisórias internas.
|
|
8
|
+
Aplique `text-right` ao cabeçalho e às células quando o domínio pedir alinhamento numérico.
|
|
9
|
+
`TableCaption` descreve a tabela abaixo do conteúdo.
|
|
6
10
|
|
|
7
11
|
```tsx preview col
|
|
8
12
|
<Table>
|
|
@@ -40,10 +44,8 @@ quando o domínio pedir alinhamento numérico. `TableCaption` descreve a tabela
|
|
|
40
44
|
|
|
41
45
|
## Tabela emoldurada
|
|
42
46
|
|
|
43
|
-
A variante `framed` aplica
|
|
44
|
-
|
|
45
|
-
então o quadro fecha limpo. O ActionList continua sendo o pattern indicado para coleções
|
|
46
|
-
pesquisáveis derivadas de actions `kind: 'list'`.
|
|
47
|
+
A variante `framed` aplica a borda externa e os cantos arredondados no contêiner da tabela e dá
|
|
48
|
+
ao cabeçalho um fundo discreto. A última linha já vem sem divisória, então o quadro fecha limpo.
|
|
47
49
|
|
|
48
50
|
```tsx preview col
|
|
49
51
|
<Table variant="framed">
|
|
@@ -105,9 +107,45 @@ adequados a essa região.
|
|
|
105
107
|
</Table>
|
|
106
108
|
```
|
|
107
109
|
|
|
110
|
+
## Contêiner e rolagem
|
|
111
|
+
|
|
112
|
+
`Table` renderiza um contêiner em volta do elemento `<table>`. É nele que ficam a moldura da
|
|
113
|
+
variante `framed` e a rolagem horizontal: quando as colunas não cabem, o contêiner rola e a página
|
|
114
|
+
mantém a largura. Por isso há duas props de classe. `className` vai para o `<table>`;
|
|
115
|
+
`containerClassName` vai para o contêiner, onde entram limites de altura, rolagem vertical ou
|
|
116
|
+
ajustes da moldura.
|
|
117
|
+
|
|
118
|
+
```tsx preview col
|
|
119
|
+
<Table variant="framed" containerClassName="max-h-40 overflow-y-auto">
|
|
120
|
+
<TableHeader>
|
|
121
|
+
<TableRow>
|
|
122
|
+
<TableHead>Agente</TableHead>
|
|
123
|
+
<TableHead className="text-right">Sessões</TableHead>
|
|
124
|
+
</TableRow>
|
|
125
|
+
</TableHeader>
|
|
126
|
+
<TableBody>
|
|
127
|
+
<TableRow><TableCell className="font-medium">developer</TableCell><TableCell className="text-right">42</TableCell></TableRow>
|
|
128
|
+
<TableRow><TableCell className="font-medium">reviewer</TableCell><TableCell className="text-right">18</TableCell></TableRow>
|
|
129
|
+
<TableRow><TableCell className="font-medium">designer</TableCell><TableCell className="text-right">7</TableCell></TableRow>
|
|
130
|
+
<TableRow><TableCell className="font-medium">planner</TableCell><TableCell className="text-right">5</TableCell></TableRow>
|
|
131
|
+
<TableRow><TableCell className="font-medium">tester</TableCell><TableCell className="text-right">3</TableCell></TableRow>
|
|
132
|
+
</TableBody>
|
|
133
|
+
</Table>
|
|
134
|
+
```
|
|
135
|
+
|
|
108
136
|
## Propriedades de Table
|
|
109
137
|
|
|
110
138
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
111
139
|
|---|---|---|---|
|
|
112
140
|
| `variant` | `'plain' \| 'framed'` | `'plain'` | Escolhe entre a estrutura sem moldura externa e o contêiner emoldurado. |
|
|
113
|
-
| `
|
|
141
|
+
| `containerClassName` | `string` | | Classes do contêiner que envolve o `<table>` e responde pela rolagem horizontal e pela moldura. |
|
|
142
|
+
| `className` | `string` | | Classes aplicadas ao elemento `<table>`. |
|
|
143
|
+
|
|
144
|
+
Os demais atributos vão para o elemento `<table>`.
|
|
145
|
+
|
|
146
|
+
## Propriedades das partes da tabela
|
|
147
|
+
|
|
148
|
+
`TableHeader`, `TableBody`, `TableFooter`, `TableRow`, `TableHead`, `TableCell` e `TableCaption`
|
|
149
|
+
não têm props próprias. Cada um aceita as props nativas do elemento que renderiza — `<thead>`,
|
|
150
|
+
`<tbody>`, `<tfoot>`, `<tr>`, `<th>`, `<td>` e `<caption>` —, como `className`, `colSpan` ou
|
|
151
|
+
`scope`. Para marcar uma linha selecionada, passe `data-state="selected"` ao `TableRow`.
|
|
@@ -82,7 +82,9 @@ Com `orientation="vertical"`, a lista forma uma coluna. O exemplo usa a variante
|
|
|
82
82
|
|
|
83
83
|
## Tamanho
|
|
84
84
|
|
|
85
|
-
`size="sm"` no `Tabs` reduz a lista de `
|
|
85
|
+
`size="sm"` no `Tabs` reduz a altura da lista de `2.25rem` para `2rem`. É o par do `sm` de `Button` e
|
|
86
|
+
`Select` para uma fileira densa, como uma toolbar ou um cabeçalho, em que o segmento não deve ficar
|
|
87
|
+
mais alto que os elementos vizinhos.
|
|
86
88
|
|
|
87
89
|
```tsx preview col
|
|
88
90
|
<Tabs defaultValue="preview" size="sm">
|
|
@@ -110,9 +112,17 @@ Com `orientation="vertical"`, a lista forma uma coluna. O exemplo usa a variante
|
|
|
110
112
|
|---|---|---|---|
|
|
111
113
|
| `variant` | `'default' \| 'line'` | `'default'` | `default` usa uma superfície preenchida; `line` cria uma linha-base horizontal de largura total e marca nela a aba ativa. |
|
|
112
114
|
|
|
113
|
-
## Propriedades de TabsTrigger
|
|
115
|
+
## Propriedades de TabsTrigger
|
|
114
116
|
|
|
115
117
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
116
118
|
|---|---|---|---|
|
|
117
|
-
| `value` | `string` | | Identificador que associa o gatilho ao
|
|
118
|
-
| `icon` | `ReactNode` | | Ícone decorativo antes do rótulo
|
|
119
|
+
| `value` | `string` | | Identificador que associa o gatilho ao `TabsContent` correspondente. |
|
|
120
|
+
| `icon` | `ReactNode` | | Ícone decorativo antes do rótulo, em `1rem`. Em uma aba sem texto, declare também `aria-label`. |
|
|
121
|
+
| `disabled` | `boolean` | `false` | Impede a seleção da aba. |
|
|
122
|
+
|
|
123
|
+
## Propriedades de TabsContent
|
|
124
|
+
|
|
125
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| `value` | `string` | | Identificador do painel, igual ao `value` do `TabsTrigger` que o abre. |
|
|
128
|
+
| `forceMount` | `true` | | Mantém o painel montado mesmo quando outra aba está ativa. |
|
|
@@ -5,31 +5,35 @@ title: Testes de action
|
|
|
5
5
|
# Testes de action
|
|
6
6
|
|
|
7
7
|
Teste cada action pela fronteira do contrato: entrada, saída ou erro, limites do schema e
|
|
8
|
-
autorização. `@softize/opus/testing` executa esse fluxo em unidade sem montar o runtime completo.
|
|
8
|
+
autorização. `@softize/opus/testing` executa esse fluxo em unidade, sem montar o runtime completo.
|
|
9
9
|
|
|
10
|
-
## Executar
|
|
10
|
+
## Executar a action em unidade
|
|
11
11
|
|
|
12
|
-
`runAction`
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
`runAction` segue a ordem do runtime: valida a entrada, exige usuário quando a action não é
|
|
13
|
+
`public`, avalia `authorize`, executa o handler e valida a saída. Em `list`, a validação da saída é
|
|
14
|
+
pulada, porque o schema descreve o item e o handler devolve a página. Falhas usam o mesmo
|
|
15
|
+
`ActionError` tipado do runtime, como `validation.invalid_input`, `auth.unauthenticated` e
|
|
16
|
+
`auth.forbidden`.
|
|
17
|
+
|
|
18
|
+
Passe a action ligada ao handler, produzida por `bindAction`. O contrato puro não tem handler e só
|
|
19
|
+
executa em modo design, descrito abaixo.
|
|
15
20
|
|
|
16
21
|
```ts
|
|
17
22
|
import { runAction } from '@softize/opus/testing'
|
|
18
|
-
import {
|
|
23
|
+
import { taskListImpl } from './list.ts'
|
|
19
24
|
|
|
20
|
-
it('lista as
|
|
21
|
-
const { output } = await runAction(
|
|
25
|
+
it('lista as tarefas com o formato do contrato', async () => {
|
|
26
|
+
const { output } = await runAction(taskListImpl, {}, { user: { id: 'u1' } })
|
|
22
27
|
expect(output.total).toBe(2)
|
|
23
28
|
})
|
|
24
29
|
|
|
25
|
-
it('
|
|
26
|
-
await expect(runAction(
|
|
30
|
+
it('exige usuário, porque a action não é pública', async () => {
|
|
31
|
+
await expect(runAction(taskListImpl, {})).rejects.toMatchObject({ code: 'auth.unauthenticated' })
|
|
27
32
|
})
|
|
28
33
|
```
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
o `loaded` como no runtime:
|
|
35
|
+
Os loaders não rodam em unidade. Para testar uma autorização que depende de dados carregados,
|
|
36
|
+
entregue o resultado pronto em `loaded`; o `authorize` o recebe como no runtime:
|
|
33
37
|
|
|
34
38
|
```ts
|
|
35
39
|
it('só a dona edita', async () => {
|
|
@@ -41,20 +45,63 @@ it('só a dona edita', async () => {
|
|
|
41
45
|
})
|
|
42
46
|
```
|
|
43
47
|
|
|
44
|
-
##
|
|
48
|
+
## Configurar o contexto
|
|
49
|
+
|
|
50
|
+
O terceiro argumento de `runAction` monta o contexto recebido pelo handler. Todas as opções são
|
|
51
|
+
opcionais.
|
|
52
|
+
|
|
53
|
+
| Opção | Efeito |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `user`, `tenantId` | Identidade do chamador. Sem `user`, actions que não são `public` falham com `auth.unauthenticated`. |
|
|
56
|
+
| `can` | Resposta de `ctx.can`. Padrão: permite tudo. |
|
|
57
|
+
| `db` | Valor entregue em `ctx.db`. |
|
|
58
|
+
| `storage`, `cache` | Adapters entregues em `ctx.storage` e `ctx.cache`; padrão `null`. |
|
|
59
|
+
| `ai` | Adapter de IA entregue em `ctx.ai`. Sem runtime, `run` e `runStream` não recebem tools. |
|
|
60
|
+
| `meta`, `trace` | Valores de `ctx.meta` e `ctx.trace`. |
|
|
61
|
+
| `loaded` | Substitui os loaders e chega ao `authorize` e ao handler. |
|
|
62
|
+
|
|
63
|
+
`requires` é declarativo também no harness: nada o executa. Um handler que chama `ctx.can` recebe a
|
|
64
|
+
resposta da opção `can`.
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
## Observar efeitos
|
|
67
|
+
|
|
68
|
+
`runAction` devolve, além de `output`, os efeitos capturados: `emitted` guarda cada `ctx.emit` e
|
|
69
|
+
`logged`, cada linha de `ctx.log`, na ordem em que ocorreram. `testContext` devolve o mesmo
|
|
70
|
+
contexto sem executar uma action.
|
|
48
71
|
|
|
49
72
|
```ts
|
|
50
73
|
const { output, emitted, logged } = await runAction(taskDone, { id: '1' }, { user })
|
|
51
74
|
expect(emitted).toEqual([{ event: 'task.done', data: { id: '1' } }])
|
|
52
75
|
```
|
|
53
76
|
|
|
77
|
+
## Testar em modo design
|
|
78
|
+
|
|
79
|
+
Com `OPUS_MODE=design`, `runAction` executa o `mockHandler` quando ele existe, como o runtime do
|
|
80
|
+
modo design. Nesse modo, o contrato puro também é testável. Fora dele, um contrato sem handler falha
|
|
81
|
+
com `testing.no_handler`.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { afterEach, it } from 'vitest'
|
|
85
|
+
import { runAction } from '@softize/opus/testing'
|
|
86
|
+
import { Task, taskList } from './list.ts'
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
// A variável vale para o processo inteiro; limpe para não afetar outros testes.
|
|
90
|
+
delete process.env['OPUS_MODE']
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('o mock devolve itens válidos para o schema', async () => {
|
|
94
|
+
process.env['OPUS_MODE'] = 'design'
|
|
95
|
+
const { output } = await runAction(taskList, {}, { user: { id: 'u1' } })
|
|
96
|
+
// `list` não valida a saída no pipeline: confira item a item.
|
|
97
|
+
for (const item of output.items) expect(Task.safeParse(item).success).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
54
101
|
## memStorage
|
|
55
102
|
|
|
56
|
-
|
|
57
|
-
|
|
103
|
+
Para um handler que grava arquivos, `memStorage` oferece um `StorageAdapter` em memória com a mesma
|
|
104
|
+
validação de chave dos drivers reais:
|
|
58
105
|
|
|
59
106
|
```ts
|
|
60
107
|
import { memStorage, runAction } from '@softize/opus/testing'
|
|
@@ -64,26 +111,24 @@ await runAction(notaAnexar, { id: '9' }, { user, storage })
|
|
|
64
111
|
expect(storage.keys()).toEqual(['notas/9.pdf'])
|
|
65
112
|
```
|
|
66
113
|
|
|
67
|
-
## fake —
|
|
114
|
+
## fake — dados gerados pelo schema
|
|
68
115
|
|
|
69
|
-
`fake(schema)`
|
|
70
|
-
schema zod
|
|
71
|
-
|
|
72
|
-
|
|
116
|
+
`fake(schema)` e `fakeMany(schema, n)` geram dados sintéticos determinísticos que satisfazem o
|
|
117
|
+
schema zod. Servem ao `mockHandler` do modo design e a fixtures de teste. A mesma `seed` produz a
|
|
118
|
+
mesma saída, sem `Math.random`. Os valores seguem as validações do zod (email, uuid, url, datetime)
|
|
119
|
+
e o nome do campo (email, id, `*At` como data, telefone, CPF/CNPJ, nome…), com conteúdo em pt-BR.
|
|
73
120
|
|
|
74
121
|
```ts
|
|
75
122
|
import { entityRowSchema } from '@softize/opus/schema'
|
|
76
123
|
import { fake, fakeMany } from '@softize/opus/testing'
|
|
77
124
|
|
|
78
|
-
const eventRowSchema = entityRowSchema(EventEntity) // schema
|
|
79
|
-
const one = fake(eventRowSchema) //
|
|
80
|
-
const many = fakeMany(eventRowSchema, 20) // 20, estáveis entre execuções
|
|
81
|
-
fake(
|
|
125
|
+
const eventRowSchema = entityRowSchema(EventEntity) // schema de uma linha da entidade
|
|
126
|
+
const one = fake(eventRowSchema) // um evento válido
|
|
127
|
+
const many = fakeMany(eventRowSchema, 20) // 20 eventos, estáveis entre execuções
|
|
128
|
+
const seeded = fake(eventRowSchema, { seed: 7 }) // outra seed, outra sequência
|
|
82
129
|
```
|
|
83
130
|
|
|
84
131
|
## Limites do teste de unidade
|
|
85
132
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`requires` é declarativo no protocolo (o runtime não o executa) — no harness idem;
|
|
89
|
-
handler que chama `ctx.can` responde ao `can` das options.
|
|
133
|
+
O harness não executa loaders reais, audit, reactions encadeadas nem o servidor. Para o fluxo
|
|
134
|
+
completo, escreva um teste de integração com `createRuntime`.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
## Texto com várias linhas
|
|
2
2
|
|
|
3
|
-
Use `Textarea` para texto livre com várias linhas
|
|
4
|
-
altura mínima
|
|
3
|
+
Use `Textarea` para texto livre com várias linhas, como uma descrição ou um contexto. O campo cresce
|
|
4
|
+
com o conteúdo a partir da altura mínima de `4rem`; para uma caixa de envio com botão integrado,
|
|
5
|
+
use [`Composer`](/ui/composer).
|
|
5
6
|
|
|
6
7
|
```tsx preview col md
|
|
7
8
|
<Textarea placeholder="Descreva o que o agente deve fazer nesta sessão." />
|
|
@@ -34,5 +35,5 @@ Associe `htmlFor` no `Label` ao `id` do campo para manter o rótulo acessível.
|
|
|
34
35
|
|
|
35
36
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
36
37
|
|---|---|---|---|
|
|
37
|
-
| `…props` | `ComponentProps<'textarea'>` | | Todos os atributos nativos (`value`, `defaultValue`, `rows`, `disabled`, `aria-invalid`…). A altura acompanha o conteúdo
|
|
38
|
-
| `className` | `string` | |
|
|
38
|
+
| `…props` | `ComponentProps<'textarea'>` | | Todos os atributos nativos (`value`, `defaultValue`, `rows`, `disabled`, `aria-invalid`…). A altura acompanha o conteúdo a partir do mínimo de `4rem`. |
|
|
39
|
+
| `className` | `string` | | Classes somadas ao estilo padrão, como uma altura máxima. |
|