@softize/opus 18.0.1 → 18.1.1
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 +105 -0
- package/PROMOTED.md +4 -5
- package/README.md +5 -4
- package/bin/cli.mjs +4 -0
- package/docs/adr/0004-page-content-state-is-composed.md +3 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
- package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
- package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
- package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +54 -0
- package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
- package/docs/adr/0019-productive-surfaces-use-compact-density.md +65 -0
- package/docs/code-style.md +2 -2
- package/docs/consumer-upgrade-propagation.md +1 -1
- package/docs/data-products.md +5 -3
- package/docs/protocol.md +6 -6
- package/docs/relative-unit-scale.md +9 -2
- package/docs/releasing.md +28 -4
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/references/evaluations.md +3 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +20 -4
- package/src/auth/drivers/jwt.ts +2 -1
- package/src/core/runtime.ts +32 -5
- package/src/core/types.ts +16 -7
- package/src/mcp/index.ts +13 -1
- package/src/ui/components/patterns/action-list-dialog.tsx +2 -2
- package/src/ui/components/patterns/content-header.tsx +2 -2
- package/src/ui/components/patterns/form-dialog.tsx +7 -2
- package/src/ui/components/patterns/form.tsx +1 -1
- package/src/ui/components/patterns/list.tsx +239 -47
- package/src/ui/components/patterns/presentation.tsx +7 -5
- package/src/ui/components/patterns/sidebar.tsx +1 -1
- package/src/ui/components/patterns/state-surface.tsx +2 -2
- package/src/ui/components/patterns/surface-header.tsx +4 -4
- package/src/ui/components/primitives/alert.tsx +2 -2
- package/src/ui/components/primitives/breadcrumb.tsx +1 -1
- package/src/ui/components/primitives/button-group.tsx +1 -1
- package/src/ui/components/primitives/button.tsx +3 -3
- package/src/ui/components/primitives/calendar.tsx +1 -1
- package/src/ui/components/primitives/card.tsx +1 -1
- package/src/ui/components/primitives/close-button.tsx +40 -0
- package/src/ui/components/primitives/detail.tsx +68 -30
- package/src/ui/components/primitives/dialog.tsx +36 -21
- package/src/ui/components/primitives/drawer.tsx +26 -19
- package/src/ui/components/primitives/empty-value.tsx +3 -3
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/field.tsx +12 -12
- package/src/ui/components/primitives/icon-picker.tsx +1 -1
- package/src/ui/components/primitives/input-group.tsx +1 -1
- package/src/ui/components/primitives/input.tsx +2 -2
- package/src/ui/components/primitives/item.tsx +5 -5
- package/src/ui/components/primitives/pagination.tsx +4 -4
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/select.tsx +3 -3
- package/src/ui/components/primitives/table.tsx +26 -17
- package/src/ui/components/primitives/tabs.tsx +80 -23
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle-group.tsx +9 -2
- package/src/ui/docs/content/action-form-dialog.md +11 -4
- package/src/ui/docs/content/action-form.md +13 -3
- package/src/ui/docs/content/action-list-dialog.md +5 -7
- package/src/ui/docs/content/action-list.md +53 -5
- package/src/ui/docs/content/action-trigger.md +9 -5
- package/src/ui/docs/content/action-view.md +12 -8
- package/src/ui/docs/content/actions.md +36 -13
- package/src/ui/docs/content/ai.md +26 -7
- package/src/ui/docs/content/alert.md +6 -3
- package/src/ui/docs/content/aspect-ratio.md +2 -2
- package/src/ui/docs/content/auth.md +25 -10
- package/src/ui/docs/content/avatar.md +1 -1
- package/src/ui/docs/content/badge.md +2 -2
- package/src/ui/docs/content/breadcrumb.md +3 -2
- package/src/ui/docs/content/button.md +33 -8
- package/src/ui/docs/content/calendar.md +1 -1
- package/src/ui/docs/content/card.md +1 -1
- package/src/ui/docs/content/carousel.md +14 -3
- package/src/ui/docs/content/chat.md +1 -1
- package/src/ui/docs/content/cli.md +13 -7
- package/src/ui/docs/content/command.md +34 -2
- package/src/ui/docs/content/composer.md +1 -1
- package/src/ui/docs/content/content.md +5 -4
- package/src/ui/docs/content/customization.md +12 -2
- package/src/ui/docs/content/cycle.md +7 -5
- package/src/ui/docs/content/data-state.md +6 -5
- package/src/ui/docs/content/data.md +3 -3
- package/src/ui/docs/content/detail.md +12 -10
- package/src/ui/docs/content/dialog.md +14 -7
- package/src/ui/docs/content/dictionary-value.md +1 -1
- package/src/ui/docs/content/dock.md +23 -2
- package/src/ui/docs/content/dot.md +0 -2
- package/src/ui/docs/content/drawer.md +7 -4
- package/src/ui/docs/content/empty-value.md +4 -4
- package/src/ui/docs/content/empty.md +1 -4
- package/src/ui/docs/content/events.md +1 -1
- package/src/ui/docs/content/field.md +21 -12
- package/src/ui/docs/content/getting-started.md +4 -2
- package/src/ui/docs/content/icon-picker.md +2 -2
- package/src/ui/docs/content/input-otp.md +2 -0
- package/src/ui/docs/content/input.md +2 -3
- package/src/ui/docs/content/item.md +6 -3
- package/src/ui/docs/content/kbd.md +2 -1
- package/src/ui/docs/content/mcp.md +10 -4
- package/src/ui/docs/content/menu.md +27 -0
- package/src/ui/docs/content/page.md +20 -6
- package/src/ui/docs/content/pagination.md +9 -2
- package/src/ui/docs/content/popover.md +2 -2
- package/src/ui/docs/content/presentation.md +48 -47
- package/src/ui/docs/content/progress.md +2 -6
- package/src/ui/docs/content/runtime.md +8 -5
- package/src/ui/docs/content/scheduler.md +1 -1
- package/src/ui/docs/content/select.md +13 -8
- package/src/ui/docs/content/sidebar.md +3 -2
- package/src/ui/docs/content/skeleton.md +1 -1
- package/src/ui/docs/content/slider.md +4 -4
- package/src/ui/docs/content/spinner.md +3 -3
- package/src/ui/docs/content/tabs.md +22 -12
- package/src/ui/docs/content/testing.md +4 -2
- package/src/ui/docs/content/toast.md +5 -6
- package/src/ui/docs/content/toggle.md +37 -0
- package/src/ui/docs/content/tokens.md +45 -2
- package/src/ui/docs/content/tooltip.md +4 -3
- package/src/ui/docs/content/truncate.md +3 -2
- package/src/ui/docs/content/ui.md +3 -1
- package/src/ui/docs/content/upgrading.md +43 -13
- package/src/ui/docs/doc-client.tsx +1 -1
- package/src/ui/docs/registry.tsx +30 -5
- package/src/ui/meta.ts +4 -4
- package/src/ui/react.tsx +1 -0
- package/src/ui/theme.css +3 -0
|
@@ -37,8 +37,9 @@ export const workspaceListContract = defineContract({
|
|
|
37
37
|
|
|
38
38
|
```ts
|
|
39
39
|
// api/ — server-only. bindAction amarra o handler no contrato compartilhado.
|
|
40
|
+
import { randomUUID } from 'node:crypto'
|
|
40
41
|
import { bindAction } from '@softize/opus/core'
|
|
41
|
-
import { workspaceCreateContract } from '@app/shared' //
|
|
42
|
+
import { workspaceCreateContract } from '@app/shared' // contrato `form`, declarado no shared/ como o acima
|
|
42
43
|
|
|
43
44
|
export const workspaceCreate = bindAction(workspaceCreateContract, {
|
|
44
45
|
handler: async (ctx, input) => {
|
|
@@ -81,16 +82,20 @@ handoff pluga o handler, e o mock nem toca `ctx.db`.
|
|
|
81
82
|
Alimente com `fake`/`fakeMany` (fixtures determinísticas do próprio schema — ver [Testes](testing)):
|
|
82
83
|
|
|
83
84
|
```ts
|
|
85
|
+
import { entityRowSchema } from '@softize/opus/schema'
|
|
84
86
|
import { fake, fakeMany } from '@softize/opus/testing'
|
|
85
87
|
|
|
88
|
+
// `defineEntity` devolve o próprio config; o schema da linha vem de `entityRowSchema`.
|
|
89
|
+
const eventRowSchema = entityRowSchema(EventEntity)
|
|
90
|
+
|
|
86
91
|
export const eventGet = defineContract({
|
|
87
92
|
name: 'event.get',
|
|
88
93
|
kind: 'view',
|
|
89
94
|
input: z.object({ id: z.string().uuid() }),
|
|
90
|
-
output:
|
|
91
|
-
mockHandler: () => fake(
|
|
95
|
+
output: eventRowSchema,
|
|
96
|
+
mockHandler: () => fake(eventRowSchema), // 1 evento fake
|
|
92
97
|
})
|
|
93
|
-
// listas: `mockHandler: () => fakeMany(
|
|
98
|
+
// listas: `mockHandler: () => fakeMany(eventRowSchema, 20)`
|
|
94
99
|
|
|
95
100
|
// o handoff, depois, só pluga o real — mesmo contrato:
|
|
96
101
|
export const eventGetImpl = bindAction(eventGet, { handler: async (ctx, input) => { /* db */ } })
|
|
@@ -103,12 +108,27 @@ qualquer `server.proxy` para backend externo desligado (prefixo ex-proxy sem cob
|
|
|
103
108
|
503 em envelope — nada vaza para prod). A SPA chama `/api` normal e recebe o dado fake, isolado:
|
|
104
109
|
|
|
105
110
|
```ts
|
|
106
|
-
// vite.config.ts
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
// vite.config.ts — o plugin só carrega no modo design, por import dinâmico.
|
|
112
|
+
const DESIGN_RUN =
|
|
113
|
+
process.argv.includes('--mode=design') ||
|
|
114
|
+
process.argv.some((arg, i) => (arg === '--mode' || arg === '-m') && process.argv[i + 1] === 'design')
|
|
115
|
+
|
|
116
|
+
async function designPlugin(): Promise<PluginOption> {
|
|
117
|
+
const { opusDesign } = await import('@softize/opus/vite')
|
|
118
|
+
return opusDesign()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const designPlugins: PluginOption[] = DESIGN_RUN ? [await designPlugin()] : []
|
|
122
|
+
|
|
123
|
+
export default defineConfig({ plugins: [react(), ...designPlugins] })
|
|
124
|
+
// sobe com: vite --mode design --configLoader runner (script dev:design do template)
|
|
110
125
|
```
|
|
111
126
|
|
|
127
|
+
O import fica dinâmico e condicionado porque o Opus publica TypeScript: o carregador padrão de
|
|
128
|
+
config do Vite externaliza um import estático, e o Node 22+ recusa type stripping dentro de
|
|
129
|
+
`node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). Com `--configLoader runner`, o Vite
|
|
130
|
+
transforma o TypeScript antes de avaliar o plugin. O app criado por `opus create` já traz esse arranjo.
|
|
131
|
+
|
|
112
132
|
O `entry` (default `./opus.config.ts`) também aceita **glob** — `opusDesign({ entry:
|
|
113
133
|
'src/domains/*/contract.ts' })` — re-expandido a cada rebuild: **domínio novo entra no ar ao
|
|
114
134
|
salvar o arquivo**, sem editar entry nem reiniciar o dev server (criar o arquivo já invalida o
|
|
@@ -128,12 +148,15 @@ descartável — vira o contrato que o backend honra.
|
|
|
128
148
|
> action inteira a partir do contrato — o spec é a fonte, o pattern é o renderizador.
|
|
129
149
|
|
|
130
150
|
```tsx
|
|
131
|
-
import {
|
|
132
|
-
import { ActionForm } from '@softize/opus/ui/react'
|
|
151
|
+
import { ActionForm, useListAction } from '@softize/opus/ui/react'
|
|
133
152
|
|
|
134
|
-
//
|
|
135
|
-
const {
|
|
153
|
+
// Lista declarativa: busca ao montar e refaz quando uma action invalida `workspace.list`.
|
|
154
|
+
const { items, total, isLoading } = useListAction<WorkspaceRow>(workspaceListContract, { q })
|
|
136
155
|
|
|
137
156
|
// Form contract-driven: os campos (fields) moram NO contrato.
|
|
138
|
-
<ActionForm
|
|
157
|
+
<ActionForm action={workspaceCreateContract} onSuccess={…} />
|
|
139
158
|
```
|
|
159
|
+
|
|
160
|
+
`useListAction` exige `OpusProvider` e um `QueryClientProvider` acima da árvore. Para buscar sob demanda, como num
|
|
161
|
+
autocomplete, use `useLookupAction`: ele não busca ao montar, expõe `run(input)` e `reset()` e
|
|
162
|
+
devolve `items`, `cursor`, `total` e os estados da última execução.
|
|
@@ -5,7 +5,7 @@ title: IA generativa
|
|
|
5
5
|
# IA generativa
|
|
6
6
|
|
|
7
7
|
> **Experimental.** Superfície mínima — cresce por reincidência de caso real, não por
|
|
8
|
-
> especulação (
|
|
8
|
+
> especulação (memória longa de conversa entra quando um caso real cobrar).
|
|
9
9
|
|
|
10
10
|
Três coisas: **completar** texto, **extrair** dado estruturado, e **rodar um agente** sobre
|
|
11
11
|
as actions do app. O contrato é um adapter do core (`AiAdapter`). O diferencial do `extract`
|
|
@@ -20,7 +20,9 @@ interface AiAdapter {
|
|
|
20
20
|
complete(prompt: string, opts?: AiCompleteOptions): Promise<string>
|
|
21
21
|
extract<T>(prompt: string, schema: Schema<T>, opts?: AiCompleteOptions): Promise<T>
|
|
22
22
|
// Loop agêntico: o modelo chama tools (as actions ai:enabled) até responder em texto.
|
|
23
|
-
run(input: string | AiMessage[], opts): Promise<AiRunResult>
|
|
23
|
+
run?(input: string | AiMessage[], opts: AiRunOptions): Promise<AiRunResult>
|
|
24
|
+
// O mesmo loop emitindo ChatEvent conforme acontece (ver Streaming, abaixo).
|
|
25
|
+
runStream?(input: string | AiMessage[], opts: AiRunOptions): AsyncIterable<ChatEvent>
|
|
24
26
|
}
|
|
25
27
|
// opts: { system?, model?, maxTokens?, temperature? } — tudo tem default do driver.
|
|
26
28
|
```
|
|
@@ -71,21 +73,38 @@ o modelo escolhe a tool → o runtime executa a action **como o usuário logado*
|
|
|
71
73
|
export const buscarNotas = defineContract({
|
|
72
74
|
name: 'nota.buscar',
|
|
73
75
|
kind: 'list',
|
|
76
|
+
label: 'Buscar notas',
|
|
74
77
|
input: z.object({ cliente: z.string(), mes: z.string() }),
|
|
75
|
-
output: z.object({
|
|
78
|
+
output: z.object({ numero: z.string(), valor: z.number() }), // em `list`, o schema de cada item
|
|
76
79
|
ai: { enabled: true, description: 'Busca notas por cliente e mês.' },
|
|
77
80
|
})
|
|
78
81
|
|
|
79
82
|
// No handler — ou fora dele, via runtime.aiFor(base), para o backend de um chat:
|
|
80
|
-
const { text } = await ctx.ai!.run('
|
|
83
|
+
const { text } = await ctx.ai!.run('Quais notas a Empresa X emitiu em junho?')
|
|
81
84
|
// o modelo chamou nota.buscar sozinho, como o usuário logado, e respondeu em texto.
|
|
82
85
|
```
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
`runtime.aiTools()` projeta cada action exposta como um `AiTool`: `name` é o identificador técnico
|
|
88
|
+
usado na execução e na auditoria; `title` traz a `label` legível da action, quando existe;
|
|
89
|
+
`description` vem de `ai.description` (ou da `description` da action) e `inputSchema`, do input.
|
|
90
|
+
`metadata.dataProducts` lista os Produtos de Dados relacionados e `metadata.dataProductLabels`
|
|
91
|
+
associa cada identificador ao seu nome legível. Esses metadados servem ao host: não entram no
|
|
92
|
+
prompt e não concedem acesso.
|
|
93
|
+
|
|
94
|
+
O agente só executa nomes que `aiTools()` anunciou; uma chamada a outra action volta como erro
|
|
95
|
+
para o modelo. Actions marcadas com `ai: { destructive: true }` ou `ai: { requiresConfirmation: true }`
|
|
96
|
+
**não rodam sem aprovação**: passe `run(prompt, { confirm })` — o chat mostra o "confirmar?"; sem
|
|
97
|
+
isso, a action é recusada e o modelo avisa. O gate lê somente esses dois campos de `ai`: o
|
|
98
|
+
`confirm: { destructive: true }` da action configura a confirmação da UI e não protege a chamada
|
|
99
|
+
feita pelo agente. Fora do handler, `runtime.aiFor(base)` devolve o `ai` já ligado a um contexto —
|
|
87
100
|
o backend do chat resolve o usuário e chama `.run(historico)`.
|
|
88
101
|
|
|
102
|
+
Para o modelo perguntar algo à pessoa no meio do turno, passe `onAsk` em `run` ou `runStream`. O
|
|
103
|
+
driver Anthropic então oferece a tool reservada `ask_user`, entrega as perguntas (`AskQuestion[]`)
|
|
104
|
+
ao seu `onAsk` e devolve as respostas (`AskAnswer[]`) ao modelo. Sem `onAsk`, uma chamada a
|
|
105
|
+
`ask_user` é recusada com a orientação de perguntar em texto. Na interface, o componente
|
|
106
|
+
[Ask](/ui/ask) apresenta essas perguntas e coleta as respostas.
|
|
107
|
+
|
|
89
108
|
## Streaming — o protocolo de eventos de conversa
|
|
90
109
|
|
|
91
110
|
`runStream` é o mesmo loop agêntico do `run`, emitindo **`ChatEvent`** conforme acontece
|
|
@@ -29,6 +29,8 @@ Use a forma curta quando o aviso tiver ícone, título e uma frase. Declare `tit
|
|
|
29
29
|
|
|
30
30
|
Título é opcional — o aviso de uma linha dispensa. O contexto continua visível por superfície,
|
|
31
31
|
borda e texto; o conteúdo comunica o significado sem depender somente da cor.
|
|
32
|
+
O Alert usa o corpo produtivo e `0.75rem` de padding; não reduza cada uso localmente para obter a
|
|
33
|
+
densidade comum.
|
|
32
34
|
|
|
33
35
|
```tsx preview col
|
|
34
36
|
<Alert description="Nenhuma sessão aberta neste repositório." />
|
|
@@ -39,7 +41,7 @@ borda e texto; o conteúdo comunica o significado sem depender somente da cor.
|
|
|
39
41
|
|
|
40
42
|
Sem `icon` o alert mantém somente a coluna de texto. Com ele, a forma curta materializa
|
|
41
43
|
`AlertMedia` à esquerda e `AlertHeader` à direita. A mídia mantém uma moldura quadrada de tamanho
|
|
42
|
-
estável, mesmo quando o título
|
|
44
|
+
estável, mesmo quando a descrição ocupa mais linhas; o título fica limitado a uma linha. Texto solto como filho também
|
|
43
45
|
vale (`<Alert>Sincronizado.</Alert>`) e se torna uma descrição.
|
|
44
46
|
|
|
45
47
|
```tsx preview col
|
|
@@ -86,8 +88,9 @@ botão só com ícone, nome acessível e tooltip.
|
|
|
86
88
|
</Alert>
|
|
87
89
|
```
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
A forma curta também aceita uma ação como `children`: com `title` ou `description`, esse conteúdo
|
|
92
|
+
entra depois da mensagem, na posição de `AlertActions`. Não combine a forma curta com os slots
|
|
93
|
+
`AlertMedia`, `AlertHeader` ou `AlertActions`; essa mistura lança erro.
|
|
91
94
|
|
|
92
95
|
```tsx preview col
|
|
93
96
|
<Alert
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Vídeo e preview (16/9)
|
|
2
2
|
|
|
3
|
-
O `ratio` é a proporção entre largura e altura — 16/9 é o
|
|
3
|
+
O `ratio` é a proporção entre largura e altura — 16/9 é o formato comum de vídeo. Defina a largura no
|
|
4
4
|
contêiner (o pai): a altura o componente deriva sozinho.
|
|
5
5
|
|
|
6
6
|
```tsx preview col
|
|
@@ -32,7 +32,10 @@ import { jwtAuth } from '@softize/opus/auth/jwt'
|
|
|
32
32
|
import { betterAuthSession } from '@softize/opus/auth/better-auth'
|
|
33
33
|
|
|
34
34
|
// JWT: valida o token (HS256 por padrão) e mapeia o payload para o User.
|
|
35
|
-
const jwt = jwtAuth({
|
|
35
|
+
const jwt = jwtAuth({
|
|
36
|
+
secret: process.env.JWT_SECRET!,
|
|
37
|
+
mapUser: (payload) => ({ id: String(payload.sub), email: payload.email }),
|
|
38
|
+
})
|
|
36
39
|
|
|
37
40
|
// better-auth: valida a sessão contra um IdP better-auth remoto.
|
|
38
41
|
const idp = betterAuthSession({
|
|
@@ -42,8 +45,10 @@ const idp = betterAuthSession({
|
|
|
42
45
|
})
|
|
43
46
|
```
|
|
44
47
|
|
|
45
|
-
O `jwt` procura o token no `Authorization: Bearer
|
|
46
|
-
`
|
|
48
|
+
O `jwt` procura o token no `Authorization: Bearer` e depois no cookie `auth_token` (troque o nome
|
|
49
|
+
com `cookieName` ou a extração inteira com `tokenExtractor`). `mapUser` é obrigatório; o `can`
|
|
50
|
+
default nega tudo. O `secret` pode ser string, `Buffer` ou um resolver async, hoje chamado sem o
|
|
51
|
+
`kid` do token. O `betterAuthSession` faz fetch da
|
|
47
52
|
sessão no IdP (`timeoutMs`, default 5s) e mapeia para o `User`/tenant/can (o `can` default nega
|
|
48
53
|
tudo — plugue o seu).
|
|
49
54
|
|
|
@@ -55,15 +60,25 @@ const runtime = createRuntime({
|
|
|
55
60
|
auth: idp,
|
|
56
61
|
})
|
|
57
62
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
+
// Na action: `authorize` decide antes do handler, com o contexto já resolvido.
|
|
64
|
+
export const notaEmitir = bindAction(notaEmitirContract, {
|
|
65
|
+
authorize: (ctx) => ctx.can('nota.emitir'),
|
|
66
|
+
handler: async (ctx, input) => repo.create({ ...input, tenantId: ctx.tenantId }),
|
|
67
|
+
})
|
|
63
68
|
```
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
O runtime aplica duas barreiras antes do handler. Sem `public: true`, a action exige
|
|
71
|
+
`ctx.user`; sem usuário, responde `auth.unauthenticated`. Depois, se houver `authorize`, o
|
|
72
|
+
runtime o avalia: `false` vira `auth.forbidden` e um `ActionError` devolvido é lançado como está.
|
|
73
|
+
`authorize` aceita uma closure, como acima, ou uma expressão da DSL avaliada contra `user`,
|
|
74
|
+
`input` e os dados carregados por `loads`.
|
|
75
|
+
|
|
76
|
+
Uma action sem `authorize` fica aberta a qualquer usuário autenticado. `requires` só documenta a
|
|
77
|
+
permissão esperada: o runtime não o executa, e `opus check` acusa `requires` sem `authorize`.
|
|
78
|
+
Quando a decisão depende de dado que só o handler conhece, verifique no próprio handler com
|
|
79
|
+
`ctx.can` e lance `error({ code: 'forbidden', category: 'authorization' })`.
|
|
80
|
+
Sem adapter `auth` configurado, os servidores HTTP resolvem `ctx.user` como null e só actions
|
|
81
|
+
`public` executam.
|
|
67
82
|
|
|
68
83
|
## Limites (por enquanto)
|
|
69
84
|
|
|
@@ -57,7 +57,7 @@ para o ocioso. Tinja com className.
|
|
|
57
57
|
<AvatarFallback>
|
|
58
58
|
<Bot className="size-4" />
|
|
59
59
|
</AvatarFallback>
|
|
60
|
-
<AvatarBadge className="bg-
|
|
60
|
+
<AvatarBadge className="bg-context-success" />
|
|
61
61
|
</Avatar>
|
|
62
62
|
<Avatar>
|
|
63
63
|
<AvatarFallback>RV</AvatarFallback>
|
|
@@ -32,8 +32,8 @@ Um svg filho ganha size-3 automaticamente — bom para reforçar o estado sem cr
|
|
|
32
32
|
|
|
33
33
|
## Como link (asChild)
|
|
34
34
|
|
|
35
|
-
asChild renderiza o filho (Radix Slot) — um `<a>` com cara de badge
|
|
36
|
-
|
|
35
|
+
asChild renderiza o filho (Radix Slot) — um `<a>` com cara de badge. O Badge não aplica estado de
|
|
36
|
+
hover; acrescente-o por `className` quando o link precisar de retorno visual.
|
|
37
37
|
|
|
38
38
|
```tsx preview
|
|
39
39
|
<Badge asChild context="neutral" variant="solid">
|
|
@@ -53,8 +53,9 @@ chevron padrão por outro glifo (aqui, uma barra).
|
|
|
53
53
|
|
|
54
54
|
## Colapsado
|
|
55
55
|
|
|
56
|
-
Trilha funda demais para o espaço: BreadcrumbEllipsis
|
|
57
|
-
|
|
56
|
+
Trilha funda demais para o espaço: BreadcrumbEllipsis marca os níveis do meio omitidos e mantém só a
|
|
57
|
+
raiz e o destino. A elipse é decorativa (`aria-hidden`), não um menu: os níveis ocultos deixam de ser
|
|
58
|
+
alcançáveis pela trilha. Ofereça esse caminho por outro meio, como a navegação da seção.
|
|
58
59
|
|
|
59
60
|
```tsx preview col-start
|
|
60
61
|
<Breadcrumb>
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`context` declara a hierarquia ou o risco da ação; `variant` escolhe o tratamento visual. Use
|
|
4
4
|
`primary` para a ação principal, `neutral` para ações de apoio e `danger` quando a ação tiver uma
|
|
5
|
-
consequência perigosa.
|
|
5
|
+
consequência perigosa. A variante `outline` mantém o fundo transparente em repouso e usa uma
|
|
6
|
+
superfície apenas no hover, preservando o fundo da região onde o botão está inserido.
|
|
6
7
|
|
|
7
8
|
```tsx preview
|
|
8
9
|
<Button>Criar workspace</Button>
|
|
@@ -18,8 +19,9 @@ consequência perigosa.
|
|
|
18
19
|
`size` usa a escala única dos controles: o mesmo nome tem a mesma medida em `Button`, `Select`,
|
|
19
20
|
`Tabs`, `Toggle`, `Switch`, `Avatar`, `Spinner` e nos botões embutidos. Os tamanhos de texto dão a
|
|
20
21
|
altura da linha; os `icon-*` são quadrados para botões só de ícone, que exigem `aria-label` porque não
|
|
21
|
-
há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em `xs
|
|
22
|
-
`
|
|
22
|
+
há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em `xs`, `sm`,
|
|
23
|
+
`icon-xs`, `icon-sm` e `icon`; 1rem em `default`; 1.25rem em `lg` e `icon-lg`), a menos que o
|
|
24
|
+
ícone traga um `size-*` próprio. O quadrado `icon` mantém a área de 2.25rem com o glifo discreto.
|
|
23
25
|
|
|
24
26
|
| Nome | Medida | Uso |
|
|
25
27
|
| --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -35,8 +37,12 @@ há texto visível. O glifo dentro do controle acompanha o tamanho (0.875rem em
|
|
|
35
37
|
Escolha o tamanho pela região, não pela importância visual: `variant` e `context` resolvem a
|
|
36
38
|
hierarquia da ação. Headers de Page e footers de Dialog/Drawer usam `default`; ações operacionais
|
|
37
39
|
de seção, toolbar e coleção usam `sm`; ações dentro de linha ou célula usam `xs` ou `icon-xs`.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
Assim a mesma decisão mantém a mesma altura mesmo quando uma superfície troca uma ação secundária
|
|
41
|
+
por uma primária.
|
|
42
|
+
|
|
43
|
+
O retorno de `PageBack` usa `icon`. O fechamento de Dialog e Drawer é um `CloseButton` circular
|
|
44
|
+
(`shape="pill"`), `neutral` e `subtle`, no quadrado `icon-sm` por padrão, ou `icon-xs` com
|
|
45
|
+
`closeSize="xs"`.
|
|
40
46
|
|
|
41
47
|
```tsx preview
|
|
42
48
|
<Button size="xs">Mínimo</Button>
|
|
@@ -79,9 +85,10 @@ buttonVariants serve para o caso sem filho único.
|
|
|
79
85
|
|
|
80
86
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
81
87
|
| ----------- | ------------------------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
-
| `context` | `'neutral' \| 'primary' \| 'danger'` | `'primary'` | A hierarquia ou o risco comunicado pela ação.
|
|
88
|
+
| `context` | `'neutral' \| 'primary' \| 'danger'` | `'primary'` em `solid` e `subtle`; `'neutral'` nas demais variantes | A hierarquia ou o risco comunicado pela ação. |
|
|
83
89
|
| `variant` | `'solid' \| 'subtle' \| 'outline' \| 'ghost' \| 'link'` | `'solid'` | O tratamento visual aplicado ao contexto. |
|
|
84
90
|
| `size` | `'xs' \| 'sm' \| 'default' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon' \| 'icon-lg'` | `'default'` | A medida na escala única dos controles (tabela acima). Os `icon-*` são quadrados para botões só de ícone, com `aria-label`. |
|
|
91
|
+
| `shape` | `'default' \| 'pill'` | `'default'` | Geometria do botão; `pill` arredonda as extremidades por completo. |
|
|
85
92
|
| `asChild` | `boolean` | `false` | Renderiza como o filho (Radix Slot) em vez de `<button>` — para âncoras e afins. |
|
|
86
93
|
| `busy` | `boolean` | `false` | Ação em andamento (depois do clique): mostra Spinner + desabilita. Não é "carregando" de conteúdo (que é Spinner/Skeleton em um nível de página). |
|
|
87
94
|
| `icon` | `React.ReactNode` | | Ícone à esquerda, como nó (ex.: `icon={<Plus />}`); o glifo segue o `size`. No busy é trocado pelo Spinner — não soma. |
|
|
@@ -114,12 +121,30 @@ conectar bordas. Esse modo atende ações icon-only em barras e linhas de listag
|
|
|
114
121
|
<Button variant="ghost" size="icon" aria-label="Exibição">
|
|
115
122
|
<SlidersHorizontal />
|
|
116
123
|
</Button>
|
|
117
|
-
<Button
|
|
118
|
-
<
|
|
124
|
+
<Button variant="ghost" size="icon" aria-label="Exportar">
|
|
125
|
+
<Download />
|
|
119
126
|
</Button>
|
|
120
127
|
</ButtonGroup>
|
|
121
128
|
```
|
|
122
129
|
|
|
130
|
+
### No rodapé de Dialog e Drawer
|
|
131
|
+
|
|
132
|
+
`DialogFooter` e `DrawerFooter` cuidam somente da faixa; envolva as decisões em um
|
|
133
|
+
`ButtonGroup mode="spaced"`, que responde pelo agrupamento e pela distribuição. Por padrão, as
|
|
134
|
+
ações preservam a largura do conteúdo e `Cancelar` usa `ghost`. Use `distribution="equal"` somente
|
|
135
|
+
quando as duas decisões tiverem peso equivalente; nesse caso, `Cancelar` usa `outline`.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
<DialogFooter>
|
|
139
|
+
<ButtonGroup mode="spaced">
|
|
140
|
+
<DialogClose asChild>
|
|
141
|
+
<Button variant="ghost">Cancelar</Button>
|
|
142
|
+
</DialogClose>
|
|
143
|
+
<Button>Salvar</Button>
|
|
144
|
+
</ButtonGroup>
|
|
145
|
+
</DialogFooter>
|
|
146
|
+
```
|
|
147
|
+
|
|
123
148
|
### Ação dividida
|
|
124
149
|
|
|
125
150
|
Combine a ação principal, um separador e um botão de ícone quando o mesmo comando oferecer
|
|
@@ -67,7 +67,7 @@ captionLayout=dropdown troca o título do mês por seletores de mês e ano — b
|
|
|
67
67
|
| `locale` | `Locale` | `ptBR` | Idioma dos nomes de mês e de dia (um locale do date-fns). |
|
|
68
68
|
| `labels` | `Partial<Labels>` | rótulos em pt-BR | Rótulos acessíveis da navegação e dos seletores; mescla sobre o padrão. |
|
|
69
69
|
|
|
70
|
-
## CalendarDayButton
|
|
70
|
+
## Personalizar o dia com CalendarDayButton
|
|
71
71
|
|
|
72
72
|
Cada dia é um `CalendarDayButton` — um `Button` ghost quadrado que recebe os modificadores do
|
|
73
73
|
dia (`selected`, `range-start`, `today`…). Use `components={{ DayButton: … }}` para
|
|
@@ -72,4 +72,4 @@ Para painel com estrutura: cada slot é dono do próprio padding (como o Dialog)
|
|
|
72
72
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
73
73
|
|---|---|---|---|
|
|
74
74
|
| `asChild` | `boolean` | `false` | Renderiza o filho com a superfície do Card (ex.: um `<button>` clicável inteiro). |
|
|
75
|
-
| `className` | `string` | | Compõe sobre a superfície; os slots (`CardHeader`, `CardBody`, `
|
|
75
|
+
| `className` | `string` | | Compõe sobre a superfície; os slots (`CardHeader`, `CardBody`, `CardFooter`, `CardAction`) são donos do próprio padding. `CardContent` é alias depreciado de `CardBody`. |
|
|
@@ -79,12 +79,23 @@ orientation=vertical empilha os slides; as setas migram para cima e para baixo (
|
|
|
79
79
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
80
80
|
|---|---|---|---|
|
|
81
81
|
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Eixo do deslocamento. No modo vertical, os slides são empilhados e as setas apontam para cima e para baixo. |
|
|
82
|
-
| `opts` | `
|
|
83
|
-
| `setApi` | `(api:
|
|
84
|
-
| `plugins` | `
|
|
82
|
+
| `opts` | `EmblaOptionsType` | | Opções repassadas ao Embla, como `{ loop: true }` ou `{ align: 'start' }`. O Opus não reexporta esse tipo; para nomeá-lo sem depender de `embla-carousel`, use `React.ComponentProps<typeof Carousel>['opts']`. |
|
|
83
|
+
| `setApi` | `(api: EmblaCarouselType \| undefined) => void` | | Recebe a instância do Embla para controle externo, como navegar com `scrollTo` ou ler o slide ativo. |
|
|
84
|
+
| `plugins` | `EmblaPluginType[]` | | Plugins do Embla associados ao carrossel, como autoplay. |
|
|
85
85
|
|
|
86
86
|
## Propriedades de CarouselItem
|
|
87
87
|
|
|
88
88
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
89
89
|
|---|---|---|---|
|
|
90
90
|
| `className` | `string` | | Classes de dimensão; a base define quantos slides cabem na área visível. |
|
|
91
|
+
|
|
92
|
+
## Propriedades de CarouselPrevious e CarouselNext
|
|
93
|
+
|
|
94
|
+
Além das props abaixo, as setas aceitam as props de `Button`. O nome acessível já vem pronto:
|
|
95
|
+
“Slide anterior” e “Próximo slide”. O clique e o estado desabilitado acompanham a posição do trilho.
|
|
96
|
+
|
|
97
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| `variant` | `ButtonProps['variant']` | `'outline'` | Tratamento visual da seta. |
|
|
100
|
+
| `size` | `ButtonProps['size']` | `'icon'` | Escala do botão. A seta aplica `size-8 rounded-full` por cima da escala, então o círculo mede 2rem; use `className` para outra medida. |
|
|
101
|
+
| `className` | `string` | | Classes adicionais, como ajustar a posição fora do trilho. |
|
|
@@ -58,7 +58,7 @@ async function chatRoute(req) {
|
|
|
58
58
|
<Chat send={async (messages) => (await api.post('/chat', { messages })).reply} />
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
O
|
|
61
|
+
O agente só executa actions marcadas com `ai.enabled` e, nelas, aplica o `authorize` com o contexto do usuário; uma action sem `authorize` fica aberta a qualquer usuário autenticado. Actions com `ai.destructive` ou `ai.requiresConfirmation` são recusadas nesse exemplo, porque ele não passa `confirm`; para aprovar essas chamadas, use `run(messages, { confirm })`. Para expor as actions ao agente, marque-as com `ai: { enabled: true }` no contrato — ver o recurso **IA generativa**.
|
|
62
62
|
|
|
63
63
|
## Streaming — eventos de conversa
|
|
64
64
|
|
|
@@ -20,9 +20,11 @@ opus db check # drift entidade ↔ banco (read-only)
|
|
|
20
20
|
opus seed check # bindings, dependências, ciclos e scripts paralelos de seed
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`opus check` lê o source sem executar nada e aplica
|
|
24
|
-
(`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`)
|
|
25
|
-
(`ui-structure`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`)
|
|
23
|
+
`opus check` lê o source sem executar nada e aplica regras em três grupos: actions
|
|
24
|
+
(`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`), UI
|
|
25
|
+
(`ui-structure`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`) e Produtos de Dados
|
|
26
|
+
(`data-product-export`, `-id`, `-duplicate`, `-version`, `-interfaces`, `-interface`, `-entities`
|
|
27
|
+
e `-entity`, todas com o prefixo `data-product`). Um projeto
|
|
26
28
|
marcado com `opus.json` e ainda sem actions passa vacuamente; sem o marcador, zero actions
|
|
27
29
|
falha, porque um gate vazio não é aprovação. `opus check --help` descreve cada regra.
|
|
28
30
|
|
|
@@ -43,15 +45,19 @@ opus pre-push materialization # só a freshness dos artefatos materializados
|
|
|
43
45
|
## Seeds de desenvolvimento e teste
|
|
44
46
|
|
|
45
47
|
> Seeds registrados no `opus.config.ts` têm perfis, métricas e escopos explícitos. Listagem e gate
|
|
46
|
-
> não abrem o banco; planejamento, aplicação e verificação
|
|
48
|
+
> não abrem o banco; planejamento, aplicação e verificação só conectam com
|
|
49
|
+
> `NODE_ENV=development` ou `NODE_ENV=test`.
|
|
47
50
|
|
|
48
51
|
```bash
|
|
49
52
|
opus seed list
|
|
50
|
-
opus seed plan customers.scenarios --profile smoke --scope local
|
|
51
|
-
opus seed apply customers.scenarios --profile smoke --scope local
|
|
52
|
-
opus seed verify customers.scenarios --profile smoke --scope local
|
|
53
|
+
NODE_ENV=development opus seed plan customers.scenarios --profile smoke --scope local
|
|
54
|
+
NODE_ENV=development opus seed apply customers.scenarios --profile smoke --scope local
|
|
55
|
+
NODE_ENV=development opus seed verify customers.scenarios --profile smoke --scope local
|
|
53
56
|
```
|
|
54
57
|
|
|
58
|
+
Com qualquer outro valor de `NODE_ENV`, inclusive ausente, esses três comandos falham antes de
|
|
59
|
+
abrir a conexão. O escopo informado também precisa constar em `safety.scopes` do seed.
|
|
60
|
+
|
|
55
61
|
`--profile` escolhe o perfil (default: o `defaultProfile` do seed) e `--scope` declara o escopo
|
|
56
62
|
dos dados (a variável `OPUS_SEED_SCOPE` é a alternativa). `--json` devolve o resultado
|
|
57
63
|
estruturado, inclusive em caso de erro. `apply` converge quando repetido; não há reset ou
|
|
@@ -49,14 +49,36 @@ render(
|
|
|
49
49
|
)
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
## Propriedades de Command
|
|
53
|
+
|
|
54
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `shouldFilter` | `boolean` | `true` | Com `false`, desliga a filtragem e a ordenação automáticas; o aplicativo passa a renderizar somente os itens que correspondem à busca. |
|
|
57
|
+
| `filter` | `(value: string, search: string, keywords?: string[]) => number` | | Pontua cada item para a busca atual, de `0` (oculto) a `1` (melhor resultado). |
|
|
58
|
+
| `value` | `string` | | Item destacado no modo controlado. |
|
|
59
|
+
| `onValueChange` | `(value: string) => void` | | Chamado quando o item destacado muda. |
|
|
60
|
+
| `loop` | `boolean` | `false` | Faz as setas voltarem ao início ou ao fim da lista. |
|
|
61
|
+
| `label` | `string` | | Nome acessível do menu, sem exibição visual. |
|
|
62
|
+
| `className` | `string` | | Classes adicionais aplicadas à raiz. |
|
|
63
|
+
|
|
64
|
+
## Propriedades de CommandInput
|
|
65
|
+
|
|
66
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| `placeholder` | `string` | | Texto exibido enquanto a busca está vazia. |
|
|
69
|
+
| `value` | `string` | | Texto da busca no modo controlado. |
|
|
70
|
+
| `onValueChange` | `(search: string) => void` | | Chamado quando o texto da busca muda. |
|
|
71
|
+
| `disabled` | `boolean` | `false` | Desabilita o campo de busca. |
|
|
72
|
+
| `className` | `string` | | Classes adicionais aplicadas ao campo; o ícone de busca fica no wrapper. |
|
|
73
|
+
|
|
52
74
|
## Propriedades de CommandDialog
|
|
53
75
|
|
|
54
76
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
55
77
|
|---|---|---|---|
|
|
56
78
|
| `open` | `boolean` | | Estado do modal no modo controlado. |
|
|
57
79
|
| `onOpenChange` | `(open: boolean) => void` | | Atualiza o estado do modal; pode ser conectado ao atalho do aplicativo. |
|
|
58
|
-
| `title` | `string` | `'
|
|
59
|
-
| `description` | `string` | `'Busque um comando para executar
|
|
80
|
+
| `title` | `string` | `'Paleta de comandos'` | Título exibido no header compacto; também nomeia o diálogo para tecnologias assistivas. |
|
|
81
|
+
| `description` | `string` | `'Busque um comando para executar…'` | Descrição disponível somente para tecnologias assistivas. |
|
|
60
82
|
| `showCloseButton` | `boolean` | `true` | Exibe o botão de fechamento. |
|
|
61
83
|
|
|
62
84
|
## Propriedades de CommandItem
|
|
@@ -64,3 +86,13 @@ render(
|
|
|
64
86
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
65
87
|
|---|---|---|---|
|
|
66
88
|
| `onSelect` | `(value: string) => void` | | Chamado ao selecionar o item por clique ou teclado. |
|
|
89
|
+
| `value` | `string` | | Valor estável usado na busca e no destaque. Sem ele, o valor é inferido do texto do item. |
|
|
90
|
+
| `keywords` | `string[]` | | Termos adicionais considerados na busca. |
|
|
91
|
+
| `disabled` | `boolean` | `false` | Impede a seleção do item. |
|
|
92
|
+
|
|
93
|
+
## Propriedades de CommandGroup
|
|
94
|
+
|
|
95
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `heading` | `React.ReactNode` | | Título exibido acima dos itens do grupo. |
|
|
98
|
+
| `value` | `string` | | Identificador do grupo; obrigatório e único quando não há `heading`. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Envio de texto
|
|
2
2
|
|
|
3
|
-
A caixa de escrever da casa: textarea em uma pílula elevada (`rounded-xl` + `border` + `shadow-sm`), **Enter** envia / **Shift+Enter** quebra linha, enviar dentro. É o composer do [Chat](/
|
|
3
|
+
A caixa de escrever da casa: textarea em uma pílula elevada (`rounded-xl` + `border` + `shadow-sm`), **Enter** envia / **Shift+Enter** quebra linha, enviar dentro. É o composer do [Chat](/ui/chat) extraído — use sozinho quando há entrada de texto mas não um chat (ex.: criar uma sessão). Controlado: o dono do texto é você. Os callbacks opcionais `onHistoryPrevious` e `onHistoryNext` permitem que esse dono consuma **↑/↓**; sem eles, as setas mantêm o comportamento nativo da textarea.
|
|
4
4
|
|
|
5
5
|
```tsx preview col
|
|
6
6
|
const [text, setText] = React.useState('')
|
|
@@ -14,7 +14,7 @@ render(
|
|
|
14
14
|
actions={<Button variant="outline">Encerrar outras sessões</Button>}
|
|
15
15
|
>
|
|
16
16
|
<div className="rounded-lg border border-border p-4">
|
|
17
|
-
MacBook
|
|
17
|
+
MacBook Pro · ativo agora
|
|
18
18
|
</div>
|
|
19
19
|
</Content>,
|
|
20
20
|
);
|
|
@@ -40,7 +40,7 @@ render(
|
|
|
40
40
|
</ContentHeader>
|
|
41
41
|
<ContentBody>
|
|
42
42
|
<div className="rounded-lg border border-border p-4">
|
|
43
|
-
MacBook
|
|
43
|
+
MacBook Pro · ativo agora
|
|
44
44
|
</div>
|
|
45
45
|
</ContentBody>
|
|
46
46
|
</Content>,
|
|
@@ -57,7 +57,8 @@ Quando a região principal da página é uma coleção, `Content` nomeia e gover
|
|
|
57
57
|
`ContentActions`. Busca, filtros, atualização e operações dependentes do recorte atual permanecem na
|
|
58
58
|
toolbar da lista. Essa divisão aproxima cada comando do objeto que ele afeta sem criar uma família
|
|
59
59
|
paralela de componentes `List*`. Na variante `page`, as ações ficam no extremo oposto ao título. A
|
|
60
|
-
criação usa um
|
|
60
|
+
criação usa um `Button` textual na variante padrão (`solid`) e no tamanho `default`, sem ícone,
|
|
61
|
+
nomeado `Criar recurso`; o diálogo aberto pelo
|
|
61
62
|
gatilho repete esse título e a edição usa `Editar recurso`.
|
|
62
63
|
|
|
63
64
|
## Propriedades de Content
|
|
@@ -68,7 +69,7 @@ gatilho repete esse título e a edição usa `Editar recurso`.
|
|
|
68
69
|
| `count` | `number` | | Forma curta: total de itens ao lado do título da região. |
|
|
69
70
|
| `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. |
|
|
70
71
|
| `actions` | `ReactNode` | | Forma curta: ações sobre a região inteira; em `page`, ficam no extremo oposto ao título. |
|
|
71
|
-
| `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` |
|
|
72
|
+
| `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível semântico do heading, independente do destaque visual. |
|
|
72
73
|
| `variant` | `'page' \| 'section'` | `'section'` | Hierarquia visual: `page` reproduz o cabeçalho de `Page`; `section` é a região dentro de uma superfície. |
|
|
73
74
|
|
|
74
75
|
Na composição explícita, `ContentHeader` recebe `ContentTitle`, `ContentMeta`, `ContentDescription` e
|
|
@@ -33,6 +33,16 @@ Quando o produto precisa de outra densidade, declare essa escolha no CSS do app.
|
|
|
33
33
|
`html { font-size: 93.75%; }` conserva a proporção que uma raiz de 15 teria sobre a base usual
|
|
34
34
|
de 16, sem transformar esse valor em uma regra da biblioteca.
|
|
35
35
|
|
|
36
|
+
## Layout desktop
|
|
37
|
+
|
|
38
|
+
O Opus oferece suporte a partir de `64rem` e não muda a composição conforme a largura da viewport.
|
|
39
|
+
Classes de tamanho, como `size="sm"`, escolhem a densidade do componente; não representam
|
|
40
|
+
breakpoints. Overflow e limites contra a janela continuam protegendo o conteúdo.
|
|
41
|
+
|
|
42
|
+
Não adicione uma adaptação isolada por viewport ou container em `className`. Uma futura
|
|
43
|
+
responsividade precisa começar pelo shell e formar um contrato comum para navegação, splits,
|
|
44
|
+
tabelas e componentes.
|
|
45
|
+
|
|
36
46
|
## 2 · className em tudo
|
|
37
47
|
|
|
38
48
|
> Todo componente termina em `cn(base, className)` com tailwind-merge: o utilitário do consumidor
|
|
@@ -41,7 +51,7 @@ de 16, sem transformar esse valor em uma regra da biblioteca.
|
|
|
41
51
|
```tsx
|
|
42
52
|
<Button className="w-full">Continuar</Button>
|
|
43
53
|
<Card className="max-w-sm" />
|
|
44
|
-
<DialogContent className="
|
|
54
|
+
<DialogContent className="max-w-2xl" />
|
|
45
55
|
```
|
|
46
56
|
|
|
47
57
|
## 3 · Recomposição estrutural
|
|
@@ -106,7 +116,7 @@ const [open, setOpen] = useState(false)
|
|
|
106
116
|
|
|
107
117
|
## O limite — de propósito
|
|
108
118
|
|
|
109
|
-
> O look curado não é customizável no app: rodapé-faixa do dialog, elevação no dark, `active:
|
|
119
|
+
> O look curado não é customizável no app: rodapé-faixa do dialog, elevação no dark, `active:brightness-90`
|
|
110
120
|
> do botão, a seta do tooltip — é identidade da casa, igual em todo projeto.
|
|
111
121
|
|
|
112
122
|
```tsx preview
|