@softize/opus 19.0.0 → 20.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +239 -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/0020-icon-tiles-are-explicitly-composed.md +8 -0
- package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -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 +109 -18
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/list.tsx +91 -92
- 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/surface-header.tsx +58 -25
- 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 +1 -1
- 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 +42 -11
- package/src/ui/components/primitives/drawer.tsx +10 -2
- 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 +141 -58
- 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 +119 -27
- 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 +38 -15
- 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 +32 -32
- package/src/ui/react.tsx +1 -0
- package/src/vite/design.ts +16 -4
|
@@ -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. |
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
## Notificação temporária
|
|
2
2
|
|
|
3
|
-
Use `toast` para informar o resultado temporário de uma ação sem interromper o fluxo.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Use `toast` para informar o resultado temporário de uma ação sem interromper o fluxo. A notificação
|
|
4
|
+
some sozinha, então não use toast para algo que a pessoa precisa ler ou resolver depois; nesse caso,
|
|
5
|
+
mantenha a mensagem na página com [`Alert`](/ui/alert).
|
|
6
|
+
|
|
7
|
+
Cada tipo usa o ícone correspondente do Opus. Escreva o título como rótulo, sem ponto final, e a
|
|
8
|
+
descrição como uma frase. A moldura do ícone permanece quadrada e alinhada à primeira linha mesmo
|
|
9
|
+
quando a descrição ocupa várias linhas. Corpo e padding seguem os mesmos `0.875rem` e `0.75rem` do
|
|
10
|
+
`Alert`.
|
|
8
11
|
|
|
9
12
|
```tsx preview
|
|
10
13
|
<Button variant="outline" onClick={() => toast.success('Workspace criado')}>Sucesso</Button>
|
|
11
14
|
<Button
|
|
12
15
|
variant="outline"
|
|
13
|
-
onClick={() => toast.error('Falha no push', { description: 'O
|
|
16
|
+
onClick={() => toast.error('Falha no push', { description: 'O repositório remoto não respondeu. Tente de novo.' })}
|
|
14
17
|
>
|
|
15
18
|
Erro
|
|
16
19
|
</Button>
|
|
@@ -52,7 +55,7 @@ oferecer uma ação.
|
|
|
52
55
|
variant="outline"
|
|
53
56
|
onClick={() =>
|
|
54
57
|
toast.promise(new Promise((resolve) => setTimeout(resolve, 1800)), {
|
|
55
|
-
loading: 'Publicando
|
|
58
|
+
loading: 'Publicando agentes…',
|
|
56
59
|
success: 'Agentes publicados',
|
|
57
60
|
error: 'Falha ao publicar',
|
|
58
61
|
})
|
|
@@ -62,6 +65,28 @@ oferecer uma ação.
|
|
|
62
65
|
</Button>
|
|
63
66
|
```
|
|
64
67
|
|
|
68
|
+
## Carregamento e mensagens neutras
|
|
69
|
+
|
|
70
|
+
`toast.message`, como `toast`, exibe uma notificação sem ícone de tipo. `toast.loading` mostra o
|
|
71
|
+
indicador de progresso e não expira sozinho: guarde o identificador que ele retorna e passe-o em
|
|
72
|
+
`id` para trocar a mesma notificação pelo resultado. `toast.dismiss(id)` fecha uma notificação; sem argumento, fecha todas.
|
|
73
|
+
|
|
74
|
+
```tsx preview
|
|
75
|
+
<Button
|
|
76
|
+
variant="outline"
|
|
77
|
+
onClick={() => {
|
|
78
|
+
const id = toast.loading('Sincronizando workspace…')
|
|
79
|
+
setTimeout(() => toast.success('Workspace sincronizado', { id }), 1800)
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
Carregamento
|
|
83
|
+
</Button>
|
|
84
|
+
<Button variant="outline" onClick={() => toast.message('Rascunho salvo neste navegador')}>
|
|
85
|
+
Neutra
|
|
86
|
+
</Button>
|
|
87
|
+
<Button variant="ghost" onClick={() => toast.dismiss()}>Fechar todas</Button>
|
|
88
|
+
```
|
|
89
|
+
|
|
65
90
|
## Toaster na raiz
|
|
66
91
|
|
|
67
92
|
Monte um único `Toaster` na raiz do aplicativo. O tema vem da propriedade `theme`, cujo padrão é
|
|
@@ -80,11 +105,27 @@ Monte um único `Toaster` na raiz do aplicativo. O tema vem da propriedade `them
|
|
|
80
105
|
| `theme` | `'light' \| 'dark' \| 'system'` | `'system'` | Define o tema das notificações; sem `next-themes`, a escolha pertence ao aplicativo. |
|
|
81
106
|
| `position` | `'bottom-right' \| 'top-center' \| …` | `'bottom-right'` | Região da tela onde as notificações aparecem. |
|
|
82
107
|
|
|
108
|
+
## Funções de toast
|
|
109
|
+
|
|
110
|
+
As funções de notificação (`toast` e suas variantes de contexto, `message` e `loading`) recebem o
|
|
111
|
+
título e as opções e retornam o identificador da notificação. `toast.promise` e `toast.dismiss` têm
|
|
112
|
+
assinaturas próprias, indicadas na tabela.
|
|
113
|
+
|
|
114
|
+
| Função | Descrição |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `toast(title, options?)` | Notificação padrão, sem ícone de tipo. |
|
|
117
|
+
| `toast.success`, `toast.info`, `toast.warning`, `toast.error` | Notificação com o ícone e a cor do contexto correspondente. |
|
|
118
|
+
| `toast.message(title, options?)` | Equivale a `toast(title, options?)`: notificação sem ícone de tipo. |
|
|
119
|
+
| `toast.loading(title, options?)` | Notificação com indicador de progresso que não expira sozinha. |
|
|
120
|
+
| `toast.promise(promise, { loading, success, error })` | Acompanha a promessa e atualiza a mesma notificação. Não aceita `actions`. |
|
|
121
|
+
| `toast.dismiss(id?)` | Fecha a notificação indicada ou, sem argumento, todas. |
|
|
122
|
+
|
|
83
123
|
## Opções de toast
|
|
84
124
|
|
|
85
125
|
| Opção | Tipo | Padrão | Descrição |
|
|
86
126
|
|---|---|---|---|
|
|
87
127
|
| `description` | `React.ReactNode` | | Complemento exibido abaixo do título. |
|
|
128
|
+
| `id` | `string \| number` | gerado | Identificador da notificação. Passe o de uma notificação existente para substituí-la, como depois de `toast.loading`. |
|
|
88
129
|
| `actions` | `ToastAction[]` | | Coleção ordenada de ações; a última recebe destaque primário por padrão. |
|
|
89
130
|
| `duration` | `number` | `4000` | Tempo de permanência da notificação, em milissegundos. O `Toaster` pode mudar o padrão, e `toast.loading` não expira sozinho. |
|
|
90
131
|
|
|
@@ -50,7 +50,7 @@ borda. `size` ajusta a altura.
|
|
|
50
50
|
|
|
51
51
|
## Desabilitado
|
|
52
52
|
|
|
53
|
-
disabled esmaece e bloqueia o clique
|
|
53
|
+
`disabled` esmaece o toggle e bloqueia o clique. O estado pressionado continua visível.
|
|
54
54
|
|
|
55
55
|
```tsx preview
|
|
56
56
|
<Toggle disabled aria-label="Negrito">
|
|
@@ -66,10 +66,10 @@ disabled esmaece e bloqueia o clique — o estado pressed permanece visível.
|
|
|
66
66
|
|
|
67
67
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
68
68
|
|---|---|---|---|
|
|
69
|
-
| `pressed` | `boolean` | |
|
|
69
|
+
| `pressed` | `boolean` | | Estado no modo controlado. Use com `onPressedChange`. |
|
|
70
70
|
| `onPressedChange` | `(pressed: boolean) => void` | | Chamado a cada alternância, com o novo estado. |
|
|
71
71
|
| `defaultPressed` | `boolean` | `false` | Estado inicial no modo não controlado. |
|
|
72
|
-
| `variant` | `'default' \| 'outline'` | `'default'` | default não tem borda
|
|
72
|
+
| `variant` | `'default' \| 'outline'` | `'default'` | `default` não tem borda e só ganha fundo quando ativo; `outline` mantém a borda. |
|
|
73
73
|
| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Altura na escala única dos controles (2 · 2.25 · 2.5rem) — sm para toolbar densa, lg para alvo mais confortável. |
|
|
74
74
|
| `shape` | `'default' \| 'pill'` | `'default'` | Geometria do controle; `pill` arredonda as extremidades. |
|
|
75
75
|
| `disabled` | `boolean` | `false` | Esmaece e bloqueia o clique, preservando o estado visual. |
|
|
@@ -4,9 +4,10 @@ title: Tokens e tema
|
|
|
4
4
|
|
|
5
5
|
# Tokens e tema
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Use esta página para escolher a cor, a borda, a sombra, o texto ou o raio certo em uma tela e para
|
|
8
|
+
saber quais variáveis sobrescrever ao aplicar a marca do projeto. Os tokens ficam em `theme.css`:
|
|
9
|
+
cada variável guarda a cor inteira e é mapeada para as classes do Tailwind por `@theme inline`. Os
|
|
10
|
+
exemplos leem as variáveis ao vivo; troque o tema do app e a página acompanha.
|
|
10
11
|
|
|
11
12
|
## Superfícies e intenções
|
|
12
13
|
|
|
@@ -210,24 +211,20 @@ render(
|
|
|
210
211
|
|
|
211
212
|
## Divergências declaradas vs shadcn
|
|
212
213
|
|
|
213
|
-
>
|
|
214
|
-
>
|
|
214
|
+
> Qualquer diferença em relação ao shadcn que não esteja declarada aqui (e no `theme.css`) é uma
|
|
215
|
+
> divergência a sincronizar, conforme a skill `build-opus-ui`.
|
|
215
216
|
|
|
216
|
-
1. Formato HSL nos tokens (o upstream migrou para oklch)
|
|
217
|
-
|
|
218
|
-
2. Elevação no
|
|
219
|
-
|
|
217
|
+
1. Formato HSL nos tokens (o upstream migrou para oklch), escolhido pela legibilidade; os valores
|
|
218
|
+
acompanham o upstream, só o formato difere.
|
|
219
|
+
2. Elevação no tema escuro: `--card` e `--popover` ficam acima de `--background` (`10%` contra `3.9%`
|
|
220
|
+
de luminosidade), para que dialog e card se destaquem de fato da página.
|
|
220
221
|
3. Radius mapeado (`xs` a `2xl`) preservando o sentido pré-v4 dos utilitários `rounded-*`. Esse
|
|
221
222
|
trecho derivado permanece monotônico quando a base muda; aplicações que também usam `3xl`
|
|
222
223
|
precisam revisar a transição para esse degrau nativo ao customizar `--radius`.
|
|
223
|
-
|
|
224
224
|
4. Escala `shadow-*` recomposta em duas camadas claras e difusas — preserva os degraus do
|
|
225
|
-
Tailwind e troca somente o peso visual.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
5. Bordas mais leves no light: `--border`/`--input` em 92.5% (upstream 89.8%) — par do canvas
|
|
225
|
+
Tailwind e troca somente o peso visual. Componentes e consumidores usam diretamente a escala.
|
|
226
|
+
5. Bordas mais leves no tema claro: `--border`/`--input` em `92.5%` (upstream `89.8%`) — par do canvas
|
|
229
227
|
quase-branco do body; superfícies elevadas usam essa mesma borda real.
|
|
230
|
-
|
|
231
228
|
6. Radius base em `0.75rem` — superfícies principais ganham cantos mais presentes sem aproximar
|
|
232
229
|
controles compactos de modais. A decisão detalhada acompanha o pacote em
|
|
233
230
|
`docs/radius-scale.md`, com os defaults, o comportamento de superfícies aninhadas e as
|
|
@@ -235,14 +232,6 @@ render(
|
|
|
235
232
|
|
|
236
233
|
## Identidade por app
|
|
237
234
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
/* index.css do app — depois do import do tema. */
|
|
242
|
-
@import '@softize/opus/ui/theme.css';
|
|
243
|
-
|
|
244
|
-
:root {
|
|
245
|
-
--primary: hsl(262 83% 58%); /* A cor da marca do projeto. */
|
|
246
|
-
--ring: hsl(262 83% 58%);
|
|
247
|
-
}
|
|
248
|
-
```
|
|
235
|
+
O tema do Opus é a base, e cada app sobrescreve as variáveis para ter a própria identidade sem
|
|
236
|
+
copiar componentes. O passo a passo, com o exemplo de CSS, está em
|
|
237
|
+
[Customização](/ui/customization).
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Use `Tooltip` para informação curta e complementar. Como a dica não está disponível em todas as
|
|
4
4
|
formas de interação, ela não pode conter informação essencial. Botões somente com ícone continuam
|
|
5
|
-
precisando de `aria-label`.
|
|
5
|
+
precisando de `aria-label`. Para conteúdo que a pessoa precisa ler ou usar, como um formulário
|
|
6
|
+
curto, use [`Popover`](/ui/popover).
|
|
6
7
|
|
|
7
8
|
```tsx preview
|
|
8
9
|
<Tooltip>
|
|
@@ -62,7 +63,22 @@ default nos formulários.
|
|
|
62
63
|
|
|
63
64
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
64
65
|
|---|---|---|---|
|
|
65
|
-
| `delayDuration` | `number` | `0` | Atraso antes da exibição, compartilhado pelos tooltips do aplicativo. |
|
|
66
|
+
| `delayDuration` | `number` | `0` | Atraso antes da exibição, em milissegundos, compartilhado pelos tooltips do aplicativo. |
|
|
67
|
+
|
|
68
|
+
## Propriedades de Tooltip
|
|
69
|
+
|
|
70
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| `open` | `boolean` | | Estado da dica no modo controlado. |
|
|
73
|
+
| `onOpenChange` | `(open: boolean) => void` | | Chamado quando a dica abre ou fecha. |
|
|
74
|
+
| `defaultOpen` | `boolean` | `false` | Estado inicial no modo não controlado. |
|
|
75
|
+
| `delayDuration` | `number` | valor do `TooltipProvider` | Atraso antes da exibição desta dica, em milissegundos; substitui o do provider. |
|
|
76
|
+
|
|
77
|
+
## Propriedades de TooltipTrigger
|
|
78
|
+
|
|
79
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| `asChild` | `boolean` | `false` | Faz do filho (por exemplo, um `Button`) o gatilho, em vez de renderizar outro `<button>`. |
|
|
66
82
|
|
|
67
83
|
## Propriedades de TooltipContent
|
|
68
84
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
## Truncar somente quando necessário
|
|
2
2
|
|
|
3
3
|
Use `Truncate` para limitar um texto a uma linha e mostrar a dica somente quando houver transbordo.
|
|
4
|
-
O componente mede novamente o conteúdo quando o tamanho muda. Se o texto couber, não cria
|
|
4
|
+
O componente mede novamente o conteúdo quando o tamanho muda. Se o texto couber, não cria
|
|
5
|
+
[tooltip](/ui/tooltip).
|
|
5
6
|
|
|
6
7
|
```tsx preview
|
|
7
8
|
<div className="w-48 rounded-md border p-2">
|
|
@@ -34,8 +35,20 @@ escondido.
|
|
|
34
35
|
Uma composição que precisa do esmaecimento sem o tooltip aplica a utilitária `truncate-fade`,
|
|
35
36
|
que cobre o último `1.5rem` da caixa, e decide quando ligá-la com o hook `useOverflowing` — a
|
|
36
37
|
mesma medição que este componente usa. A máscara pressupõe corte real: em texto que cabe
|
|
37
|
-
inteiro, ela esmaeceria o fim de uma palavra visível. É assim que o rótulo do `SidebarItem
|
|
38
|
-
funciona.
|
|
38
|
+
inteiro, ela esmaeceria o fim de uma palavra visível. É assim que o rótulo do `SidebarItem`, da
|
|
39
|
+
[`Sidebar`](/ui/sidebar), funciona.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
function useOverflowing<T extends HTMLElement>(content?: React.ReactNode): {
|
|
43
|
+
ref: React.RefObject<T | null>
|
|
44
|
+
overflowing: boolean
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Ligue `ref` ao elemento de uma linha que pode ser cortado. `overflowing` fica `true` quando o
|
|
49
|
+
conteúdo é mais largo que a caixa. A medida é refeita quando a caixa muda de tamanho e quando
|
|
50
|
+
`content` muda; passe nele o texto exibido, porque a troca de texto sem mudança de tamanho não é
|
|
51
|
+
percebida de outra forma.
|
|
39
52
|
|
|
40
53
|
```tsx
|
|
41
54
|
const { ref, overflowing } = useOverflowing<HTMLSpanElement>(label)
|
|
@@ -58,9 +71,26 @@ quando o texto completo ainda não oferecer contexto suficiente.
|
|
|
58
71
|
</div>
|
|
59
72
|
```
|
|
60
73
|
|
|
61
|
-
Requer `TooltipProvider` na raiz
|
|
62
|
-
|
|
63
|
-
`fade`, esmaece o fim da linha em vez das reticências.
|
|
74
|
+
Requer `TooltipProvider` na raiz; o projeto criado por `opus create` já o monta. A largura vem do
|
|
75
|
+
contêiner ou de `className` (`max-w-*`): o `span` é `block` e corta em reticências (`truncate`);
|
|
76
|
+
com `fade`, esmaece o fim da linha em vez das reticências.
|
|
77
|
+
|
|
78
|
+
## Acesso pelo teclado
|
|
79
|
+
|
|
80
|
+
A dica abre quando o ponteiro passa sobre o texto ou quando o texto recebe o foco. Com o texto
|
|
81
|
+
cortado, `Truncate` entra na ordem de tabulação e mostra o anel de foco dos controles, então quem
|
|
82
|
+
navega pelo teclado alcança a dica com Tab. Sem corte, o texto fica fora da ordem de tabulação,
|
|
83
|
+
porque não há nada escondido para revelar.
|
|
84
|
+
|
|
85
|
+
Dentro de um link ou botão, o próprio controle já recebe o foco. Passe `tabIndex={-1}` ao
|
|
86
|
+
`Truncate` para não criar uma segunda parada de tabulação no mesmo item. Nesse caso, a dica deixa de
|
|
87
|
+
abrir pelo teclado: dê ao controle o nome completo, com `aria-label`, ou mostre o texto completo em
|
|
88
|
+
outro lugar visível.
|
|
89
|
+
|
|
90
|
+
Leitores de tela leem o texto completo do elemento, mesmo cortado na tela; o conteúdo próprio
|
|
91
|
+
passado em `tooltip` só é anunciado enquanto a dica está aberta. Quando o texto completo for
|
|
92
|
+
necessário para a tarefa, mostre-o também em um lugar que não dependa da dica, como o detalhe do
|
|
93
|
+
registro.
|
|
64
94
|
|
|
65
95
|
## Propriedades de Truncate
|
|
66
96
|
|