@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +239 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/check.mjs +198 -10
  4. package/bin/lib/copy.mjs +29 -0
  5. package/bin/lib/mcp.mjs +1 -1
  6. package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +8 -0
  7. package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
  9. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  10. package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
  11. package/docs/data-products.md +15 -9
  12. package/docs/protocol.md +3 -4
  13. package/docs/releasing.md +2 -2
  14. package/package.json +2 -2
  15. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  16. package/registry/templates/app/package.json +1 -1
  17. package/src/core/runtime.ts +96 -8
  18. package/src/core/types.ts +5 -2
  19. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  20. package/src/ui/components/patterns/content-header.tsx +109 -18
  21. package/src/ui/components/patterns/fill-region.ts +150 -0
  22. package/src/ui/components/patterns/list.tsx +91 -92
  23. package/src/ui/components/patterns/page.tsx +20 -4
  24. package/src/ui/components/patterns/presentation.tsx +52 -37
  25. package/src/ui/components/patterns/split.tsx +44 -4
  26. package/src/ui/components/patterns/surface-anatomy.ts +35 -8
  27. package/src/ui/components/patterns/surface-header.tsx +58 -25
  28. package/src/ui/components/patterns/trigger.tsx +12 -2
  29. package/src/ui/components/primitives/alert.tsx +9 -1
  30. package/src/ui/components/primitives/avatar.tsx +7 -3
  31. package/src/ui/components/primitives/card.tsx +1 -1
  32. package/src/ui/components/primitives/carousel.tsx +34 -5
  33. package/src/ui/components/primitives/checkbox.tsx +11 -3
  34. package/src/ui/components/primitives/command.tsx +2 -1
  35. package/src/ui/components/primitives/copyable.tsx +120 -10
  36. package/src/ui/components/primitives/dialog.tsx +42 -11
  37. package/src/ui/components/primitives/drawer.tsx +10 -2
  38. package/src/ui/components/primitives/empty.tsx +1 -1
  39. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  40. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  41. package/src/ui/components/primitives/input-group.tsx +9 -2
  42. package/src/ui/components/primitives/input-otp.tsx +1 -1
  43. package/src/ui/components/primitives/kbd.tsx +1 -1
  44. package/src/ui/components/primitives/label.tsx +3 -0
  45. package/src/ui/components/primitives/menu.tsx +5 -0
  46. package/src/ui/components/primitives/popover.tsx +1 -1
  47. package/src/ui/components/primitives/progress.tsx +9 -1
  48. package/src/ui/components/primitives/slider.tsx +62 -11
  49. package/src/ui/components/primitives/truncate.tsx +10 -1
  50. package/src/ui/docs/content/accordion.md +74 -27
  51. package/src/ui/docs/content/action-form-card.md +24 -15
  52. package/src/ui/docs/content/action-form-dialog.md +17 -7
  53. package/src/ui/docs/content/action-form.md +92 -77
  54. package/src/ui/docs/content/action-list-dialog.md +75 -38
  55. package/src/ui/docs/content/action-list.md +141 -58
  56. package/src/ui/docs/content/action-trigger.md +27 -10
  57. package/src/ui/docs/content/action-view.md +14 -14
  58. package/src/ui/docs/content/actions.md +161 -66
  59. package/src/ui/docs/content/ai.md +43 -44
  60. package/src/ui/docs/content/alert.md +83 -29
  61. package/src/ui/docs/content/ask.md +51 -26
  62. package/src/ui/docs/content/aspect-ratio.md +22 -24
  63. package/src/ui/docs/content/audit.md +23 -19
  64. package/src/ui/docs/content/auth.md +38 -19
  65. package/src/ui/docs/content/avatar.md +54 -5
  66. package/src/ui/docs/content/badge.md +29 -9
  67. package/src/ui/docs/content/breadcrumb.md +21 -8
  68. package/src/ui/docs/content/button.md +73 -43
  69. package/src/ui/docs/content/calendar.md +134 -26
  70. package/src/ui/docs/content/card.md +41 -15
  71. package/src/ui/docs/content/carousel.md +37 -6
  72. package/src/ui/docs/content/chat.md +118 -37
  73. package/src/ui/docs/content/checkbox.md +73 -7
  74. package/src/ui/docs/content/cli.md +94 -55
  75. package/src/ui/docs/content/collapsible.md +43 -19
  76. package/src/ui/docs/content/command.md +30 -8
  77. package/src/ui/docs/content/composer.md +23 -8
  78. package/src/ui/docs/content/content.md +119 -27
  79. package/src/ui/docs/content/copyable.md +39 -8
  80. package/src/ui/docs/content/customization.md +55 -46
  81. package/src/ui/docs/content/cycle.md +33 -18
  82. package/src/ui/docs/content/data-products.md +115 -0
  83. package/src/ui/docs/content/data-state.md +20 -6
  84. package/src/ui/docs/content/data.md +20 -19
  85. package/src/ui/docs/content/detail.md +12 -5
  86. package/src/ui/docs/content/dialog.md +107 -24
  87. package/src/ui/docs/content/dictionary-value.md +4 -4
  88. package/src/ui/docs/content/dock.md +54 -19
  89. package/src/ui/docs/content/dot.md +35 -5
  90. package/src/ui/docs/content/drawer.md +63 -19
  91. package/src/ui/docs/content/empty-value.md +20 -6
  92. package/src/ui/docs/content/empty.md +24 -3
  93. package/src/ui/docs/content/events.md +76 -16
  94. package/src/ui/docs/content/executions.md +9 -9
  95. package/src/ui/docs/content/field.md +21 -6
  96. package/src/ui/docs/content/getting-started.md +44 -34
  97. package/src/ui/docs/content/icon-picker.md +19 -13
  98. package/src/ui/docs/content/icon-tile.md +38 -15
  99. package/src/ui/docs/content/input-otp.md +58 -7
  100. package/src/ui/docs/content/input.md +22 -3
  101. package/src/ui/docs/content/item.md +53 -6
  102. package/src/ui/docs/content/kbd.md +5 -1
  103. package/src/ui/docs/content/label.md +13 -4
  104. package/src/ui/docs/content/log.md +5 -5
  105. package/src/ui/docs/content/markdown.md +5 -4
  106. package/src/ui/docs/content/mcp.md +49 -23
  107. package/src/ui/docs/content/menu.md +95 -10
  108. package/src/ui/docs/content/metric-card.md +12 -4
  109. package/src/ui/docs/content/observability.md +10 -9
  110. package/src/ui/docs/content/page.md +118 -12
  111. package/src/ui/docs/content/pagination.md +13 -5
  112. package/src/ui/docs/content/pane.md +55 -5
  113. package/src/ui/docs/content/popover.md +42 -5
  114. package/src/ui/docs/content/presentation.md +91 -47
  115. package/src/ui/docs/content/progress.md +27 -14
  116. package/src/ui/docs/content/queue.md +39 -21
  117. package/src/ui/docs/content/radio-group.md +26 -4
  118. package/src/ui/docs/content/router.md +29 -10
  119. package/src/ui/docs/content/runtime.md +90 -54
  120. package/src/ui/docs/content/scheduler.md +53 -21
  121. package/src/ui/docs/content/scroll-area.md +42 -35
  122. package/src/ui/docs/content/select.md +78 -33
  123. package/src/ui/docs/content/semantic-context.md +21 -2
  124. package/src/ui/docs/content/separator.md +15 -9
  125. package/src/ui/docs/content/sidebar.md +3 -2
  126. package/src/ui/docs/content/skeleton.md +6 -1
  127. package/src/ui/docs/content/slider.md +37 -9
  128. package/src/ui/docs/content/spinner.md +4 -3
  129. package/src/ui/docs/content/split.md +19 -0
  130. package/src/ui/docs/content/storage.md +8 -9
  131. package/src/ui/docs/content/switch.md +10 -7
  132. package/src/ui/docs/content/table.md +46 -8
  133. package/src/ui/docs/content/tabs.md +14 -4
  134. package/src/ui/docs/content/testing.md +76 -31
  135. package/src/ui/docs/content/textarea.md +5 -4
  136. package/src/ui/docs/content/toast.md +48 -7
  137. package/src/ui/docs/content/toggle.md +3 -3
  138. package/src/ui/docs/content/tokens.md +15 -26
  139. package/src/ui/docs/content/tooltip.md +18 -2
  140. package/src/ui/docs/content/truncate.md +36 -6
  141. package/src/ui/docs/content/ui.md +74 -22
  142. package/src/ui/docs/content/upgrading.md +45 -36
  143. package/src/ui/docs/plugin.ts +36 -2
  144. package/src/ui/docs/registry.tsx +6 -0
  145. package/src/ui/drivers/react.tsx +16 -8
  146. package/src/ui/meta.ts +32 -32
  147. package/src/ui/react.tsx +1 -0
  148. 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. A variante padrão não possui borda externa;
4
- `TableBody` mantém somente as divisórias internas. Aplique `text-right` ao cabeçalho e às células
5
- quando o domínio pedir alinhamento numérico. `TableCaption` descreve a tabela abaixo do conteúdo.
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 no próprio contêiner a borda externa, os cantos arredondados, o
44
- scroll horizontal contido e o fundo discreto do cabeçalho. A última linha já vem sem divisória,
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
- | `className` | `string` | | Classes aplicadas ao elemento `table`. |
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 `h-9` (2.25rem) para `h-8` (2rem). É o par do `sm` de Button e Select para uma fileira densa, como uma toolbar ou um cabeçalho, em que o segmento não deve ficar mais alto que os elementos vizinhos.
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 e TabsContent
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 painel correspondente. |
118
- | `icon` | `ReactNode` | | Ícone decorativo antes do rótulo; SVGs usam `1rem`. |
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 o contrato em unidade
10
+ ## Executar a action em unidade
11
11
 
12
- `runAction` valida a entrada, aplica o gate `public`, avalia `authorize`, executa o handler e valida
13
- a saída. Falhas usam o mesmo `ActionError` tipado do runtime, como `validation.invalid_input`,
14
- `auth.unauthenticated` e `auth.forbidden`.
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 { taskList } from './list.ts'
23
+ import { taskListImpl } from './list.ts'
19
24
 
20
- it('lista as fixtures com o shape do contrato', async () => {
21
- const { output } = await runAction(taskList, {}, { user: { id: 'u1' } })
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('sem usuário, barra: a action não é pública', async () => {
26
- await expect(runAction(taskList, {})).rejects.toMatchObject({ code: 'auth.unauthenticated' })
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
- O terceiro argumento configura o contexto e substitui os loaders (que não rodam em
31
- unidade): `{ user, tenantId, can, db, storage, ai, loaded, meta }`. O `authorize` vê
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
- ## Contexto observável
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
- `runAction` (e `testContext`, para quem quer só o ctx) devolve os efeitos capturados —
47
- o teste afirma o que importa:
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
- `StorageAdapter` em memória com a mesma régua de key dos drivers reais — para handler
57
- que anexa arquivo:
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 — fixtures do schema
114
+ ## fake — dados gerados pelo schema
68
115
 
69
- `fake(schema)` / `fakeMany(schema, n)` geram dado sintético **determinístico** que satisfaz o
70
- schema zod — matéria-prima do `mockHandler` (modo design) e de fixtures de teste. Semeado
71
- (mesma `seed` → mesma saída, sem `Math.random`), com heurística por check do zod (email, uuid,
72
- url, datetime) e por nome de campo (email, id, `*At`→data, telefone, cpf/cnpj, nome…), sabor pt-BR.
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 da linha da entidade
79
- const one = fake(eventRowSchema) // 1 evento válido (safeParse passa)
80
- const many = fakeMany(eventRowSchema, 20) // 20, estáveis entre execuções
81
- fake(schema, { seed: 7 }) // seed própria
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
- De propósito — é harness de **unidade**: loaders reais (passe `loaded` pronto), audit,
87
- reactions em cadeia e o servidor. Fluxo completo é teste de integração com o runtime.
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. O campo cresce com o conteúdo a partir de sua
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 (`field-sizing-content`) a partir de `min-h-16`. |
38
- | `className` | `string` | | Compõe sobre o estilo padrão. |
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. Cada tipo usa o
4
- ícone correspondente do Opus. Escreva o título como rótulo, sem ponto final, e a descrição como uma
5
- frase. A moldura do ícone permanece quadrada e alinhada à primeira linha mesmo quando a descrição
6
- ocupa várias linhas. Corpo e padding seguem os mesmos `0.875rem` e `0.75rem` do Alert; os defaults
7
- internos do Sonner não definem a densidade da casa.
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 Multica não respondeu — tente de novo.' })}
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 no Multica…',
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 — o estado pressed permanece visível.
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` | | O estado ligado/desligado no modo controlado — parear com onPressedChange. |
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 (fundo só quando ativo); outline carrega a 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
- O tema canônico vive no Opus (`theme.css`): tokens com a cor inteira na var, mapeados para o
8
- Tailwind via `@theme inline`. Os swatches abaixo leem as vars **ao vivo** — troque o tema do app
9
- e a página acompanha.
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
- > A regra anti-drift: o que não está declarado aqui (e no `theme.css`) é drift e deve ser
214
- > sincronizado — skill `build-opus-ui`.
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) — legibilidade e ferramentas nossas; os
217
- valores acompanham o upstream, só o formato difere.
218
- 2. Elevação no dark: `--card`/`--popover` ficam ACIMA de `--background` (10% vs 3.9%) — identidade
219
- da casa; dialog e card "sobem" da página de verdade.
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. Os aliases antigos por papel foram removidos;
226
- componentes e consumidores usam diretamente a escala.
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
- > O tema do Opus é a base; cada app sobrescreve as vars para ter a própria identidade sem forkar componente.
239
-
240
- ```css
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 tooltip.
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 (o esqueleto do `opus create` já monta). Largura vem do
62
- container ou de `className` (`max-w-*`) — o span é `block` e corta em reticências (`truncate`); com
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