@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.
Files changed (129) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/PROMOTED.md +4 -5
  3. package/README.md +5 -4
  4. package/bin/cli.mjs +4 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +3 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
  8. package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
  9. package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
  10. package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +54 -0
  11. package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
  12. package/docs/adr/0019-productive-surfaces-use-compact-density.md +65 -0
  13. package/docs/code-style.md +2 -2
  14. package/docs/consumer-upgrade-propagation.md +1 -1
  15. package/docs/data-products.md +5 -3
  16. package/docs/protocol.md +6 -6
  17. package/docs/relative-unit-scale.md +9 -2
  18. package/docs/releasing.md +28 -4
  19. package/package.json +1 -1
  20. package/registry/skills/build-opus-ui/references/evaluations.md +3 -1
  21. package/registry/skills/build-opus-ui/references/ui-patterns.md +20 -4
  22. package/src/auth/drivers/jwt.ts +2 -1
  23. package/src/core/runtime.ts +32 -5
  24. package/src/core/types.ts +16 -7
  25. package/src/mcp/index.ts +13 -1
  26. package/src/ui/components/patterns/action-list-dialog.tsx +2 -2
  27. package/src/ui/components/patterns/content-header.tsx +2 -2
  28. package/src/ui/components/patterns/form-dialog.tsx +7 -2
  29. package/src/ui/components/patterns/form.tsx +1 -1
  30. package/src/ui/components/patterns/list.tsx +239 -47
  31. package/src/ui/components/patterns/presentation.tsx +7 -5
  32. package/src/ui/components/patterns/sidebar.tsx +1 -1
  33. package/src/ui/components/patterns/state-surface.tsx +2 -2
  34. package/src/ui/components/patterns/surface-header.tsx +4 -4
  35. package/src/ui/components/primitives/alert.tsx +2 -2
  36. package/src/ui/components/primitives/breadcrumb.tsx +1 -1
  37. package/src/ui/components/primitives/button-group.tsx +1 -1
  38. package/src/ui/components/primitives/button.tsx +3 -3
  39. package/src/ui/components/primitives/calendar.tsx +1 -1
  40. package/src/ui/components/primitives/card.tsx +1 -1
  41. package/src/ui/components/primitives/close-button.tsx +40 -0
  42. package/src/ui/components/primitives/detail.tsx +68 -30
  43. package/src/ui/components/primitives/dialog.tsx +36 -21
  44. package/src/ui/components/primitives/drawer.tsx +26 -19
  45. package/src/ui/components/primitives/empty-value.tsx +3 -3
  46. package/src/ui/components/primitives/empty.tsx +1 -1
  47. package/src/ui/components/primitives/field.tsx +12 -12
  48. package/src/ui/components/primitives/icon-picker.tsx +1 -1
  49. package/src/ui/components/primitives/input-group.tsx +1 -1
  50. package/src/ui/components/primitives/input.tsx +2 -2
  51. package/src/ui/components/primitives/item.tsx +5 -5
  52. package/src/ui/components/primitives/pagination.tsx +4 -4
  53. package/src/ui/components/primitives/radio-group.tsx +1 -1
  54. package/src/ui/components/primitives/select.tsx +3 -3
  55. package/src/ui/components/primitives/table.tsx +26 -17
  56. package/src/ui/components/primitives/tabs.tsx +80 -23
  57. package/src/ui/components/primitives/textarea.tsx +1 -1
  58. package/src/ui/components/primitives/toggle-group.tsx +9 -2
  59. package/src/ui/docs/content/action-form-dialog.md +11 -4
  60. package/src/ui/docs/content/action-form.md +13 -3
  61. package/src/ui/docs/content/action-list-dialog.md +5 -7
  62. package/src/ui/docs/content/action-list.md +53 -5
  63. package/src/ui/docs/content/action-trigger.md +9 -5
  64. package/src/ui/docs/content/action-view.md +12 -8
  65. package/src/ui/docs/content/actions.md +36 -13
  66. package/src/ui/docs/content/ai.md +26 -7
  67. package/src/ui/docs/content/alert.md +6 -3
  68. package/src/ui/docs/content/aspect-ratio.md +2 -2
  69. package/src/ui/docs/content/auth.md +25 -10
  70. package/src/ui/docs/content/avatar.md +1 -1
  71. package/src/ui/docs/content/badge.md +2 -2
  72. package/src/ui/docs/content/breadcrumb.md +3 -2
  73. package/src/ui/docs/content/button.md +33 -8
  74. package/src/ui/docs/content/calendar.md +1 -1
  75. package/src/ui/docs/content/card.md +1 -1
  76. package/src/ui/docs/content/carousel.md +14 -3
  77. package/src/ui/docs/content/chat.md +1 -1
  78. package/src/ui/docs/content/cli.md +13 -7
  79. package/src/ui/docs/content/command.md +34 -2
  80. package/src/ui/docs/content/composer.md +1 -1
  81. package/src/ui/docs/content/content.md +5 -4
  82. package/src/ui/docs/content/customization.md +12 -2
  83. package/src/ui/docs/content/cycle.md +7 -5
  84. package/src/ui/docs/content/data-state.md +6 -5
  85. package/src/ui/docs/content/data.md +3 -3
  86. package/src/ui/docs/content/detail.md +12 -10
  87. package/src/ui/docs/content/dialog.md +14 -7
  88. package/src/ui/docs/content/dictionary-value.md +1 -1
  89. package/src/ui/docs/content/dock.md +23 -2
  90. package/src/ui/docs/content/dot.md +0 -2
  91. package/src/ui/docs/content/drawer.md +7 -4
  92. package/src/ui/docs/content/empty-value.md +4 -4
  93. package/src/ui/docs/content/empty.md +1 -4
  94. package/src/ui/docs/content/events.md +1 -1
  95. package/src/ui/docs/content/field.md +21 -12
  96. package/src/ui/docs/content/getting-started.md +4 -2
  97. package/src/ui/docs/content/icon-picker.md +2 -2
  98. package/src/ui/docs/content/input-otp.md +2 -0
  99. package/src/ui/docs/content/input.md +2 -3
  100. package/src/ui/docs/content/item.md +6 -3
  101. package/src/ui/docs/content/kbd.md +2 -1
  102. package/src/ui/docs/content/mcp.md +10 -4
  103. package/src/ui/docs/content/menu.md +27 -0
  104. package/src/ui/docs/content/page.md +20 -6
  105. package/src/ui/docs/content/pagination.md +9 -2
  106. package/src/ui/docs/content/popover.md +2 -2
  107. package/src/ui/docs/content/presentation.md +48 -47
  108. package/src/ui/docs/content/progress.md +2 -6
  109. package/src/ui/docs/content/runtime.md +8 -5
  110. package/src/ui/docs/content/scheduler.md +1 -1
  111. package/src/ui/docs/content/select.md +13 -8
  112. package/src/ui/docs/content/sidebar.md +3 -2
  113. package/src/ui/docs/content/skeleton.md +1 -1
  114. package/src/ui/docs/content/slider.md +4 -4
  115. package/src/ui/docs/content/spinner.md +3 -3
  116. package/src/ui/docs/content/tabs.md +22 -12
  117. package/src/ui/docs/content/testing.md +4 -2
  118. package/src/ui/docs/content/toast.md +5 -6
  119. package/src/ui/docs/content/toggle.md +37 -0
  120. package/src/ui/docs/content/tokens.md +45 -2
  121. package/src/ui/docs/content/tooltip.md +4 -3
  122. package/src/ui/docs/content/truncate.md +3 -2
  123. package/src/ui/docs/content/ui.md +3 -1
  124. package/src/ui/docs/content/upgrading.md +43 -13
  125. package/src/ui/docs/doc-client.tsx +1 -1
  126. package/src/ui/docs/registry.tsx +30 -5
  127. package/src/ui/meta.ts +4 -4
  128. package/src/ui/react.tsx +1 -0
  129. 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' // o mesmo contrato do bloco acima
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: EventEntity.zod(),
91
- mockHandler: () => fake(EventEntity.zod()), // 1 evento fake
95
+ output: eventRowSchema,
96
+ mockHandler: () => fake(eventRowSchema), // 1 evento fake
92
97
  })
93
- // listas: `mockHandler: () => fakeMany(EventEntity.zod(), 20)`
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
- import { opusDesign } from '@softize/opus/vite'
108
- export default defineConfig({ plugins: [react(), opusDesign()] })
109
- // sobe com: vite --mode design
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 { useLookupAction } from '@softize/opus/client'
132
- import { ActionForm } from '@softize/opus/ui/react'
151
+ import { ActionForm, useListAction } from '@softize/opus/ui/react'
133
152
 
134
- // O contrato mantém a lista paginada tipada sem exigir uma definição paralela.
135
- const { rows, fetchNextPage } = useLookupAction(workspaceListContract)
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 contract={workspaceCreateContract} onSuccess={…} />
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 (streaming e memória longa de conversa entram quando um caso real cobrar).
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({ total: z.number() }),
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('Quantas notas a Empresa X emitiu em junho?')
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
- O que é `destructive` ou pede `requiresConfirmation` **não roda sem aprovação**: passe
85
- `run(prompt, { confirm })` o chat mostra o "confirmar?"; sem isso, a action é recusada e o
86
- modelo avisa. Fora do handler, `runtime.aiFor(base)` devolve o `ai` ligado a um contexto
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 ou a descrição ocupam mais linhas. Texto solto como filho também
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
- Os dois modos convivem: com `title` ou `description`, `children` entra depois da mensagem e recebe a
90
- ação.
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
- ## Padrão (16/9)
1
+ ## Vídeo e preview (16/9)
2
2
 
3
- O `ratio` é a proporção entre largura e altura — 16/9 é o padrão de vídeo. Defina a largura no
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({ secret: process.env.JWT_SECRET! })
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`, em cookie ou custom (via `getToken`); o
46
- `secret` pode ser string ou um resolver async por `kid`. O `betterAuthSession` faz fetch da
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
- // No handler: o contexto já vem resolvido.
59
- handler: async (ctx, input) => {
60
- if (!(await ctx.can('nota.emitir'))) throw error({ code: 'forbidden' })
61
- return repo.create({ ...input, tenantId: ctx.tenantId })
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
- A action declara o gate no contrato (`auth: 'nota.emitir'`) e o runtime cobra antes do
66
- handler. Sem `auth` configurado, `ctx.user` é null.
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-emerald-500" />
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, com hover próprio das
36
- variantes.
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 substitui os níveis do meio (que viram um
57
- menu/popover) e mantém a raiz e o destino.
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` e `sm`, 1rem em
22
- `default`, 1.25rem em `lg`), a menos que o ícone traga um `size-*` próprio.
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
- Controles de chrome, como voltar e fechar, usam `icon`. Assim a mesma decisão mantém a mesma
39
- altura mesmo quando uma superfície troca uma ação secundária por uma primária.
40
+ 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 context="primary" size="icon" aria-label="Novo item">
118
- <Plus />
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`, `CardContent`, `CardFooter`, `CardAction`) são donos do próprio padding. |
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` | `CarouselOptions` | | Opções repassadas ao Embla, como `{ loop: true }` ou `{ align: 'start' }`. |
83
- | `setApi` | `(api: CarouselApi) => void` | | Recebe a instância para controle externo, como navegar com `scrollTo` ou ler o slide ativo. |
84
- | `plugins` | `CarouselPlugin[]` | | Plugins do Embla associados ao carrossel, como autoplay. |
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 modelofaz o que o usuário pode (`ctx.can`); o que é destrutivo pede `confirm`. Para expor as actions ao agente, marque-as com `ai: { enabled: true }` no contrato — ver o recurso **IA generativa**.
61
+ O agenteexecuta 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 nove regras: cinco sobre as actions
24
- (`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`) e quatro sobre a UI
25
- (`ui-structure`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`). Um projeto
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 são bloqueados em produção.
48
+ > não abrem o banco; planejamento, aplicação e verificação 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` | `'Comandos'` | Nome acessível do diálogo, disponível para leitura assistiva. |
59
- | `description` | `string` | `'Busque um comando para executar.'` | Descrição acessível do diálogo. |
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](/components/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.
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 Para o · ativo agora
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 Para o · ativo agora
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 botão textual `default`, sem ícone, nomeado `Criar recurso`; o diálogo aberto pelo
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` | herdado | Nível semântico do heading, independente do destaque visual. |
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="sm:max-w-2xl" />
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:scale`
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