@softize/opus 19.0.0 → 20.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +193 -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/0023-surfaces-share-one-anatomy-and-two-layouts.md +5 -0
  7. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  8. package/docs/data-products.md +15 -9
  9. package/docs/protocol.md +3 -4
  10. package/docs/releasing.md +2 -2
  11. package/package.json +2 -2
  12. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  13. package/registry/templates/app/package.json +1 -1
  14. package/src/core/runtime.ts +96 -8
  15. package/src/core/types.ts +5 -2
  16. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  17. package/src/ui/components/patterns/content-header.tsx +15 -3
  18. package/src/ui/components/patterns/fill-region.ts +150 -0
  19. package/src/ui/components/patterns/list.tsx +89 -78
  20. package/src/ui/components/patterns/page.tsx +20 -4
  21. package/src/ui/components/patterns/presentation.tsx +52 -37
  22. package/src/ui/components/patterns/split.tsx +44 -4
  23. package/src/ui/components/patterns/surface-anatomy.ts +35 -8
  24. package/src/ui/components/patterns/trigger.tsx +12 -2
  25. package/src/ui/components/primitives/alert.tsx +9 -1
  26. package/src/ui/components/primitives/avatar.tsx +7 -3
  27. package/src/ui/components/primitives/carousel.tsx +34 -5
  28. package/src/ui/components/primitives/checkbox.tsx +11 -3
  29. package/src/ui/components/primitives/command.tsx +2 -1
  30. package/src/ui/components/primitives/copyable.tsx +120 -10
  31. package/src/ui/components/primitives/dialog.tsx +41 -10
  32. package/src/ui/components/primitives/drawer.tsx +9 -1
  33. package/src/ui/components/primitives/empty.tsx +1 -1
  34. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  35. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  36. package/src/ui/components/primitives/input-group.tsx +9 -2
  37. package/src/ui/components/primitives/input-otp.tsx +1 -1
  38. package/src/ui/components/primitives/kbd.tsx +1 -1
  39. package/src/ui/components/primitives/label.tsx +3 -0
  40. package/src/ui/components/primitives/menu.tsx +5 -0
  41. package/src/ui/components/primitives/popover.tsx +1 -1
  42. package/src/ui/components/primitives/progress.tsx +9 -1
  43. package/src/ui/components/primitives/slider.tsx +62 -11
  44. package/src/ui/components/primitives/truncate.tsx +10 -1
  45. package/src/ui/docs/content/accordion.md +74 -27
  46. package/src/ui/docs/content/action-form-card.md +24 -15
  47. package/src/ui/docs/content/action-form-dialog.md +17 -7
  48. package/src/ui/docs/content/action-form.md +92 -77
  49. package/src/ui/docs/content/action-list-dialog.md +75 -38
  50. package/src/ui/docs/content/action-list.md +134 -52
  51. package/src/ui/docs/content/action-trigger.md +27 -10
  52. package/src/ui/docs/content/action-view.md +14 -14
  53. package/src/ui/docs/content/actions.md +161 -66
  54. package/src/ui/docs/content/ai.md +43 -44
  55. package/src/ui/docs/content/alert.md +83 -29
  56. package/src/ui/docs/content/ask.md +51 -26
  57. package/src/ui/docs/content/aspect-ratio.md +22 -24
  58. package/src/ui/docs/content/audit.md +23 -19
  59. package/src/ui/docs/content/auth.md +38 -19
  60. package/src/ui/docs/content/avatar.md +54 -5
  61. package/src/ui/docs/content/badge.md +29 -9
  62. package/src/ui/docs/content/breadcrumb.md +21 -8
  63. package/src/ui/docs/content/button.md +73 -43
  64. package/src/ui/docs/content/calendar.md +134 -26
  65. package/src/ui/docs/content/card.md +41 -15
  66. package/src/ui/docs/content/carousel.md +37 -6
  67. package/src/ui/docs/content/chat.md +118 -37
  68. package/src/ui/docs/content/checkbox.md +73 -7
  69. package/src/ui/docs/content/cli.md +94 -55
  70. package/src/ui/docs/content/collapsible.md +43 -19
  71. package/src/ui/docs/content/command.md +30 -8
  72. package/src/ui/docs/content/composer.md +23 -8
  73. package/src/ui/docs/content/content.md +31 -7
  74. package/src/ui/docs/content/copyable.md +39 -8
  75. package/src/ui/docs/content/customization.md +55 -46
  76. package/src/ui/docs/content/cycle.md +33 -18
  77. package/src/ui/docs/content/data-products.md +115 -0
  78. package/src/ui/docs/content/data-state.md +20 -6
  79. package/src/ui/docs/content/data.md +20 -19
  80. package/src/ui/docs/content/detail.md +12 -5
  81. package/src/ui/docs/content/dialog.md +107 -24
  82. package/src/ui/docs/content/dictionary-value.md +4 -4
  83. package/src/ui/docs/content/dock.md +54 -19
  84. package/src/ui/docs/content/dot.md +35 -5
  85. package/src/ui/docs/content/drawer.md +63 -19
  86. package/src/ui/docs/content/empty-value.md +20 -6
  87. package/src/ui/docs/content/empty.md +24 -3
  88. package/src/ui/docs/content/events.md +76 -16
  89. package/src/ui/docs/content/executions.md +9 -9
  90. package/src/ui/docs/content/field.md +21 -6
  91. package/src/ui/docs/content/getting-started.md +44 -34
  92. package/src/ui/docs/content/icon-picker.md +19 -13
  93. package/src/ui/docs/content/icon-tile.md +28 -5
  94. package/src/ui/docs/content/input-otp.md +58 -7
  95. package/src/ui/docs/content/input.md +22 -3
  96. package/src/ui/docs/content/item.md +53 -6
  97. package/src/ui/docs/content/kbd.md +5 -1
  98. package/src/ui/docs/content/label.md +13 -4
  99. package/src/ui/docs/content/log.md +5 -5
  100. package/src/ui/docs/content/markdown.md +5 -4
  101. package/src/ui/docs/content/mcp.md +49 -23
  102. package/src/ui/docs/content/menu.md +95 -10
  103. package/src/ui/docs/content/metric-card.md +12 -4
  104. package/src/ui/docs/content/observability.md +10 -9
  105. package/src/ui/docs/content/page.md +118 -12
  106. package/src/ui/docs/content/pagination.md +13 -5
  107. package/src/ui/docs/content/pane.md +55 -5
  108. package/src/ui/docs/content/popover.md +42 -5
  109. package/src/ui/docs/content/presentation.md +91 -47
  110. package/src/ui/docs/content/progress.md +27 -14
  111. package/src/ui/docs/content/queue.md +39 -21
  112. package/src/ui/docs/content/radio-group.md +26 -4
  113. package/src/ui/docs/content/router.md +29 -10
  114. package/src/ui/docs/content/runtime.md +90 -54
  115. package/src/ui/docs/content/scheduler.md +53 -21
  116. package/src/ui/docs/content/scroll-area.md +42 -35
  117. package/src/ui/docs/content/select.md +78 -33
  118. package/src/ui/docs/content/semantic-context.md +21 -2
  119. package/src/ui/docs/content/separator.md +15 -9
  120. package/src/ui/docs/content/sidebar.md +3 -2
  121. package/src/ui/docs/content/skeleton.md +6 -1
  122. package/src/ui/docs/content/slider.md +37 -9
  123. package/src/ui/docs/content/spinner.md +4 -3
  124. package/src/ui/docs/content/split.md +19 -0
  125. package/src/ui/docs/content/storage.md +8 -9
  126. package/src/ui/docs/content/switch.md +10 -7
  127. package/src/ui/docs/content/table.md +46 -8
  128. package/src/ui/docs/content/tabs.md +14 -4
  129. package/src/ui/docs/content/testing.md +76 -31
  130. package/src/ui/docs/content/textarea.md +5 -4
  131. package/src/ui/docs/content/toast.md +48 -7
  132. package/src/ui/docs/content/toggle.md +3 -3
  133. package/src/ui/docs/content/tokens.md +15 -26
  134. package/src/ui/docs/content/tooltip.md +18 -2
  135. package/src/ui/docs/content/truncate.md +36 -6
  136. package/src/ui/docs/content/ui.md +74 -22
  137. package/src/ui/docs/content/upgrading.md +45 -36
  138. package/src/ui/docs/plugin.ts +36 -2
  139. package/src/ui/docs/registry.tsx +6 -0
  140. package/src/ui/drivers/react.tsx +16 -8
  141. package/src/ui/meta.ts +31 -31
  142. package/src/vite/design.ts +16 -4
@@ -10,12 +10,13 @@ superfície, e a escolha entre regiões seccionadas e contínuas pertence a quem
10
10
  Quando a própria região precisar de uma fronteira visível, use `framed`, descrito adiante. Pelo
11
11
  mesmo motivo o `Content` não tem rodapé: um rodapé pertence à superfície que a hospeda.
12
12
 
13
- No caso comum, prefira o shorthand:
13
+ No caso comum, prefira o shorthand. `count` mostra o total de itens ao lado do título:
14
14
 
15
15
  ```tsx preview col
16
16
  render(
17
17
  <Content
18
18
  title="Dispositivos conectados"
19
+ count={3}
19
20
  description="Sessões com acesso à sua conta."
20
21
  actions={<Button variant="outline">Encerrar outras sessões</Button>}
21
22
  >
@@ -132,13 +133,33 @@ do pai correto, filhos estruturais indiretos e a mistura de shorthand com compos
132
133
  ## Coleção nomeada
133
134
 
134
135
  Quando a região principal da página é uma coleção, `Content` nomeia e governa essa coleção, enquanto
135
- `ActionList` executa a consulta. Coloque criar, importar e outras ações sobre o conjunto completo em
136
+ [`ActionList`](/ui/action-list) executa a consulta. Coloque criar, importar e outras ações sobre o conjunto completo em
136
137
  `ContentActions`. Busca, filtros, atualização e operações dependentes do recorte atual permanecem na
137
138
  toolbar da lista. Essa divisão aproxima cada comando do objeto que ele afeta sem criar uma família
138
- paralela de componentes `List*`. Na variante `page`, as ações ficam no extremo oposto ao título. A
139
- criação usa um `Button` textual na variante padrão (`solid`) e no tamanho `default`, sem ícone,
140
- nomeado `Criar recurso`; o diálogo aberto pelo
141
- gatilho repete esse título e a edição usa `Editar recurso`.
139
+ paralela de componentes `List*`.
140
+
141
+ Quando a coleção é o assunto da página, use `variant="page"` com `level={1}`: o `Content` assume a
142
+ escala do cabeçalho de [`Page`](/ui/page) e passa a ser o heading principal. As ações ficam no
143
+ extremo oposto ao título, como em qualquer variante.
144
+
145
+ ```tsx preview col
146
+ render(
147
+ <Content level={1} variant="page" title="Clientes" actions={<Button>Criar cliente</Button>}>
148
+ <div className="rounded-lg border border-border p-4 text-sm text-muted-foreground">
149
+ A lista de clientes ocupa o corpo.
150
+ </div>
151
+ </Content>,
152
+ );
153
+ ```
154
+
155
+ A criação usa um `Button` textual na variante padrão (`solid`) e no tamanho `default`, sem ícone,
156
+ nomeado `Criar {recurso}` (por exemplo, `Criar cliente`); o diálogo aberto pelo gatilho repete esse
157
+ título, e a edição usa `Editar {recurso}`.
158
+
159
+ Em uma [`Page`](/ui/page) com `layout="fill"`, a coleção pode ocupar a altura da página e rolar só os
160
+ resultados. `Content` e `ContentBody` repassam a altura até a [`ActionList`](/ui/action-list) sem
161
+ classes adicionais; um `Content` irmão, sem lista, mantém a altura do próprio conteúdo. A seção
162
+ “Ocupar a altura disponível” da `ActionList` mostra a composição completa.
142
163
 
143
164
  ## Propriedades de Content
144
165
 
@@ -147,12 +168,15 @@ gatilho repete esse título e a edição usa `Editar recurso`.
147
168
  | `title` | `ReactNode` | | Forma curta: título da região (vira o heading ligado à `section`). |
148
169
  | `count` | `number` | | Forma curta: total de itens ao lado do título da região. |
149
170
  | `description` | `ReactNode` | | Forma curta: frase de apoio sob o título. |
150
- | `actions` | `ReactNode` | | Forma curta: ações sobre a região inteira; em `page`, ficam no extremo oposto ao título. |
171
+ | `actions` | `ReactNode` | | Forma curta: ações sobre a região inteira, no extremo oposto ao título. |
151
172
  | `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | Nível semântico do heading, independente do destaque visual. |
152
173
  | `variant` | `'page' \| 'section'` | `'section'` | Hierarquia visual: `page` reproduz o cabeçalho de `Page`; `section` é a região dentro de uma superfície. |
153
174
  | `as` | `'section' \| 'fieldset'` | `'section'` | Usa uma região com heading ou um grupo de campos cujo título é a legenda nativa. |
154
175
  | `framed` | `boolean` | `false` | Envolve o header e o body com a moldura estrutural da região. |
155
176
  | `frameVariant` | `'default' \| 'legend'` | `'default'` | Em `fieldset` com moldura, mantém a borda contínua ou integra visualmente a legenda à borda. Não altera a semântica. |
177
+ | `children` | `ReactNode` | | Corpo da região na forma curta, ou `ContentHeader` e `ContentBody` na composição explícita. |
178
+
179
+ As demais propriedades são as nativas de `section` ou, com `as="fieldset"`, de `fieldset`.
156
180
 
157
181
  Na composição explícita, `ContentHeader` recebe `ContentTitle`, `ContentMeta`, `ContentDescription` e
158
182
  `ContentActions`, e `ContentBody` recebe o conteúdo; nenhum desses slots aceita `title` ou `level`
@@ -1,6 +1,9 @@
1
1
  ## Copiar por ícone
2
2
 
3
- Sem filhos, é um botão-ícone: copia `value` e o ícone vira um check por ~1.5s. Bom para toolbar ou célula estreita, ao lado de um ID/token/slug.
3
+ Use `Copyable` para que a pessoa copie um valor exato com um clique, como um identificador, uma
4
+ chave ou um endereço. Sem filhos, ele é um botão só com ícone: copia `value` e troca o ícone por um
5
+ check durante 1,5 segundo. Essa forma cabe em barras de ferramentas e células estreitas, ao lado do
6
+ valor exibido.
4
7
 
5
8
  ```tsx preview
6
9
  <Copyable value="opus_sk_1a2b3c4d5e6f" className="text-muted-foreground hover:text-foreground" />
@@ -8,7 +11,8 @@ Sem filhos, é um botão-ícone: copia `value` e o ícone vira um check por ~1.5
8
11
 
9
12
  ## Com rótulo
10
13
 
11
- Com filhos, o valor visível fica à esquerda e o ícone à direita — clicar no conjunto copia. Componha o chrome (borda, mono) por fora, via `className`.
14
+ Com filhos, o valor visível fica à esquerda e o ícone à direita — clicar no conjunto copia. Borda,
15
+ fonte monoespaçada e demais detalhes visuais entram por `className`.
12
16
 
13
17
  ```tsx preview
14
18
  <Copyable
@@ -19,21 +23,48 @@ Com filhos, o valor visível fica à esquerda e o ícone à direita — clicar n
19
23
  </Copyable>
20
24
  ```
21
25
 
26
+ ## Nome acessível
27
+
28
+ Com rótulo visível, o nome do botão é o próprio texto dos filhos. Quem usa comando de voz aciona o
29
+ botão dizendo o que vê, e o nome não muda durante a confirmação. Sem filhos, o botão se chama
30
+ “Copiar” e, durante a confirmação, “Copiado”.
31
+
32
+ A confirmação também é anunciada por leitores de tela: uma região de status, invisível na tela e
33
+ fora do botão, diz “Copiado” sem interromper a leitura em andamento.
34
+
35
+ Quando houver mais de um botão só com ícone na mesma tela, passe `aria-label` para distinguir o que
36
+ cada um copia, como “Copiar o ID do pedido”. Com rótulo visível, o `aria-label` raramente é
37
+ necessário; se usá-lo, comece pelo texto que a pessoa vê. Um `aria-label` próprio substitui os nomes
38
+ padrão, inclusive o “Copiado”, e o anúncio da confirmação continua.
39
+
22
40
  ## Duração do feedback
23
41
 
24
- `feedbackMs` ajusta por quanto tempo a confirmação aparece; o padrão é 1500 milissegundos. Se a
25
- área de transferência não estiver disponível, a ação não produz efeito.
42
+ `feedbackMs` ajusta por quanto tempo a confirmação aparece; o padrão é 1500 milissegundos.
26
43
 
27
44
  ```tsx preview
28
- <Copyable value="copiado devagar" feedbackMs={3000} className="text-muted-foreground hover:text-foreground">
29
- copiar (3s)
45
+ <Copyable
46
+ value="https://opus.softize.dev/w/empresa-x"
47
+ feedbackMs={3000}
48
+ className="text-muted-foreground hover:text-foreground"
49
+ >
50
+ Copiar link
30
51
  </Copyable>
31
52
  ```
32
53
 
54
+ A confirmação só aparece depois que a área de transferência aceita o texto. Se ela não estiver
55
+ disponível, como em uma página sem HTTPS, ou recusar a escrita, o ícone não muda.
56
+
33
57
  ## Propriedades de Copyable
34
58
 
35
59
  | Propriedade | Tipo | Padrão | Descrição |
36
60
  |---|---|---|---|
37
- | `value` | `string` | | O texto que vai para a área de transferência no clique. |
61
+ | `value` | `string` | obrigatório | O texto que vai para a área de transferência no clique. |
38
62
  | `feedbackMs` | `number` | `1500` | Duração do estado “copiado”, em milissegundos. |
39
- | `children` | `ReactNode` | | Rótulo ao lado do ícone; sem ele, o controle é só o ícone com nome acessível. |
63
+ | `children` | `ReactNode` | | Rótulo visível ao lado do ícone. Quando presente, é o nome acessível do botão. |
64
+ | `aria-label` | `string` | sem filhos, `'Copiar'`, ou `'Copiado'` durante a confirmação | Nome acessível do botão. Substitui o nome vindo dos filhos e os nomes padrão. |
65
+
66
+ Os demais atributos vão para o elemento `button`. Um `onClick` próprio roda antes da cópia. O
67
+ botão é o único elemento que o Copyable coloca no contêiner: a região de status fica no fim do
68
+ `body`, visível apenas para leitores de tela, e é uma só para todos os Copyable da página. Ela
69
+ aparece com o primeiro Copyable montado e sai com o último, então uma tabela com um botão de copiar
70
+ por linha não multiplica regiões.
@@ -4,15 +4,16 @@ title: Customização
4
4
 
5
5
  # Customização
6
6
 
7
- Cinco alavancas, do global ao pontual — e um limite de propósito. O caminho previsto é compor
8
- e configurar, nunca forkar componente: o que não cabe nas alavancas evolui no Opus (com
9
- divergência declarada), para valer para casa toda.
7
+ Quando a aplicação precisa de outra marca, outra largura ou outro comportamento, o Opus oferece
8
+ cinco alavancas, da mais global à mais pontual. Todas partem de compor e configurar os componentes
9
+ existentes; nenhuma exige copiar um componente para dentro do projeto e mantê-lo à parte. O que não
10
+ couber nelas evolui no próprio Opus, para valer em todos os projetos.
10
11
 
11
12
  ## 1 · Identidade por tokens
12
13
 
13
- > O jeito canônico de vestir a marca. Sobrescreva as vars no `:root` depois de importar o
14
- > `theme.css` — todos os componentes acompanham, sem forkar nada. Dark incluso (a classe `.dark`
15
- > re-declara as vars).
14
+ Para aplicar a marca, sobrescreva as variáveis CSS em `:root` depois de importar `theme.css`. Todos
15
+ os componentes acompanham a mudança, e o tema escuro também: a classe `.dark` redeclara as mesmas
16
+ variáveis.
16
17
 
17
18
  ```css
18
19
  /* index.css do app — depois do import do tema. */
@@ -26,27 +27,20 @@ divergência declarada), para valer para casa toda.
26
27
  ```
27
28
 
28
29
  O Opus deriva de `rounded-xs` a `rounded-2xl` dessa base. Assim, a marca muda a presença dos
29
- cantos sem substituir as utilities escolhidas pelos componentes nem criar tokens por papel.
30
+ cantos sem substituir as utilities escolhidas pelos componentes nem criar tokens por papel. A lista
31
+ completa de variáveis está em [Tokens](/ui/tokens).
30
32
 
31
33
  O tema não define `font-size` em `html`: a fonte raiz pertence ao navegador e à aplicação.
32
34
  Quando o produto precisa de outra densidade, declare essa escolha no CSS do app. Por exemplo,
33
- `html { font-size: 93.75%; }` conserva a proporção que uma raiz de 15 teria sobre a base usual
34
- de 16, sem transformar esse valor em uma regra da biblioteca.
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.
35
+ `html { font-size: 93.75%; }` reduz a raiz a 15/16 da fonte que o navegador oferece, e tudo que o
36
+ Opus mede em `rem` acompanha a proporção. O valor continua sendo uma escolha do app, não uma regra
37
+ da biblioteca.
45
38
 
46
39
  ## 2 · className em tudo
47
40
 
48
- > Todo componente termina em `cn(base, className)` com tailwind-merge: o utilitário do consumidor
49
- > vence o conflito. Para layout local (largura, margem, grid) — não para repintar o visual da casa.
41
+ Todo componente aceita `className` e o combina com as próprias classes por tailwind-merge: quando
42
+ duas classes disputam a mesma propriedade, a do consumidor vence. Use essa alavanca para o layout
43
+ local, como largura, margem e grid, e não para repintar o visual dos componentes.
50
44
 
51
45
  ```tsx
52
46
  <Button className="w-full">Continuar</Button>
@@ -56,73 +50,88 @@ tabelas e componentes.
56
50
 
57
51
  ## 3 · Recomposição estrutural
58
52
 
59
- > Os componentes são explodidos em slots. `asChild` (Radix Slot) renderiza como outro elemento
60
- > mantendo estilo e comportamento; os `*Variants` aplicam a cara da casa em um elemento arbitrário.
53
+ Os componentes são divididos em partes nomeadas, como `CardHeader` e `CardBody`, e a tela usa só as
54
+ que precisa. `asChild` faz o filho assumir o estilo e o comportamento do componente, e as funções
55
+ `*Variants` aplicam o visual de um componente a um elemento qualquer.
61
56
 
62
57
  ```tsx preview
63
58
  <Button asChild variant="outline">
64
- <a href="#" onClick={(e) => e.preventDefault()}>Âncora com cara de botão</a>
59
+ <a href="#" onClick={(e) => e.preventDefault()}>Link com aparência de botão</a>
65
60
  </Button>
66
61
  ```
67
62
 
68
63
  ```tsx
69
- // Slots: só o que a tela pede.
64
+ // Partes: só o que a tela pede.
70
65
  <Card>
71
66
  <CardHeader><CardTitle>Workspace</CardTitle></CardHeader>
72
67
  <CardBody>…</CardBody>
73
68
  </Card>
74
69
 
75
- // asChild: o filho VIRA o botão (sem forkar estilo).
70
+ // asChild: o link assume o estilo e o comportamento do botão.
76
71
  <Button asChild><a href="/docs">Abrir documentação</a></Button>
77
72
 
78
- // buttonVariants: a cara da casa em um elemento qualquer.
73
+ // buttonVariants: o visual do botão em outro elemento.
79
74
  <a className={buttonVariants({ variant: 'outline' })}>Link estilizado</a>
80
75
  ```
81
76
 
82
77
  ## 4 · Comportamento por props
83
78
 
84
- > Modo controlado em tudo que abre ou seleciona (`open`/`onOpenChange`, `value`/`onValueChange`),
85
- > posicionamento (`align`/`side`/`sideOffset`), `showCloseButton`, `delayDuration`… A orquestração
86
- > é do implementador; o visual continua o da casa.
79
+ Tudo o que abre ou seleciona aceita modo controlado (`open` e `onOpenChange`, `value` e
80
+ `onValueChange`). Posicionamento (`align`, `side`, `sideOffset`) e tempos (`delayDuration`) também
81
+ são props. A orquestração fica com a aplicação; o visual continua o dos componentes.
87
82
 
88
83
  ```tsx
89
84
  const [open, setOpen] = useState(false)
90
85
 
91
86
  <Dialog open={open} onOpenChange={setOpen}>…</Dialog>
92
- <DialogContent showCloseButton={false} /> {/* modal que exige ação */}
87
+ <Dialog mode="alert">…</Dialog> {/* exige uma resposta explícita */}
93
88
  <PopoverContent align="start" sideOffset={8} />
94
89
  <TooltipProvider delayDuration={200}>…
95
90
  ```
96
91
 
92
+ Para exigir uma resposta, use `Dialog mode="alert"`: ele não fecha pelo clique externo e não mostra
93
+ o botão de fechar. Apenas esconder o botão com `showCloseButton={false}` não impede o fechamento por
94
+ Esc nem pelo clique externo. Veja [Dialog](/ui/dialog).
95
+
97
96
  ## 5 · Nos patterns, o contrato é a customização
98
97
 
99
- > Os patterns (ActionForm/Search/View/Trigger): o spec dirige. `fields`/`widget`/`showWhen`/
100
- > `fieldOptions` no form, `columns` no search, render prop no view. O caminho previsto é a feature
101
- > EMBRULHAR o pattern — nunca forkar.
98
+ Nos patterns — [`ActionForm`](/ui/action-form), [`ActionList`](/ui/action-list),
99
+ [`ActionView`](/ui/action-view) e [`ActionTrigger`](/ui/action-trigger) —, o contrato da action
100
+ determina campos, colunas e estados. A tela ajusta o que é dela: `fieldOptions` no formulário,
101
+ `columns` na lista, a função de render no `ActionView`. Para ir além, envolva o pattern em um
102
+ componente da feature em vez de copiá-lo.
102
103
 
103
104
  ```tsx
104
- // O form NASCE do contrato; a tela só ajusta o que é seu.
105
+ // O formulário vem do contrato; a tela só fornece as opções que conhece.
105
106
  <ActionForm
106
107
  action={workspaceCreateContract}
107
- fieldOptions={{ skillIds: skillsDoWorkspace }}
108
+ fieldOptions={{ ownerId: workspaceMembers }}
108
109
  onSuccess={(d) => navigate(`/workspaces/${d.id}`)}
109
110
  />
110
111
 
111
- // O search recebe as colunas; a célula é render livre.
112
+ // A lista recebe as colunas; a célula é render livre.
112
113
  <ActionList action={listAction} input={{ q }} columns={[
113
114
  { key: 'name', header: 'Workspace', cell: (w) => w.name },
114
115
  ]} />
115
116
  ```
116
117
 
117
- ## O limite — de propósito
118
+ ## Largura mínima e densidade
118
119
 
119
- > O look curado não é customizável no app: rodapé-faixa do dialog, elevação no dark, `active:brightness-90`
120
- > do botão, a seta do tooltip — é identidade da casa, igual em todo projeto.
120
+ O Opus oferece suporte a partir de `64rem` e não muda a composição conforme a largura da viewport.
121
+ Props de tamanho, como `size="sm"`, escolhem a densidade do componente; não representam
122
+ breakpoints. Overflow e limites contra a janela continuam protegendo o conteúdo.
121
123
 
122
- ```tsx preview
123
- <Badge context="success">Cabe nas alavancas</Badge>
124
- ```
124
+ Não adicione em `className` uma adaptação isolada por viewport ou container. Uma responsividade
125
+ futura precisa começar pelo shell e formar um contrato comum para navegação, splits, tabelas e
126
+ componentes.
127
+
128
+ ## O que fica fora das alavancas
129
+
130
+ Alguns detalhes fazem parte da identidade visual do Opus e são iguais em todo projeto: a faixa do
131
+ rodapé do dialog, a elevação no tema escuro, o escurecimento do botão ao ser pressionado
132
+ (`active:brightness-90`) e a seta do tooltip. Eles não são configuráveis pela aplicação.
125
133
 
126
- Se uma necessidade real não cabe nas alavancas (tokens · className · slots/asChild · props ·
127
- contrato), o movimento não é dialeto local: é evoluir o componente **no Opus**, com a divergência
128
- declarada (skill `build-opus-ui`) — assim a mudança vale para casa toda, e esta doc passa a mostrá-la.
134
+ Se uma necessidade real não cabe nas alavancas (tokens, `className`, partes e `asChild`, props e
135
+ contrato), não crie uma variação local. Proponha a evolução do componente no Opus, com a
136
+ divergência declarada conforme a skill `build-opus-ui`; assim a mudança vale para todos os projetos
137
+ e passa a aparecer nesta documentação.
@@ -1,36 +1,51 @@
1
+ ---
2
+ title: Evolução contínua
3
+ ---
4
+
1
5
  # Evolução contínua
2
6
 
3
- O Opus cresce por **reincidência, nunca por especulação**. Quando o mesmo comportamento genérico reaparece em mais de um projeto, ele vira candidato a peça do Opus — e o projeto troca a implementação local pelo import. Estilo e conveniência não entram; comportamento que se repete, sim.
7
+ Use este fluxo quando o seu projeto precisar de algo que o Opus ainda não oferece ou encontrar um
8
+ defeito nele. O Opus incorpora um comportamento quando ele se repete em mais de um projeto; então o
9
+ projeto troca a implementação local pelo import. Preferência de estilo e conveniência isolada não
10
+ são motivo para incorporar uma peça.
4
11
 
5
- ## Onde você aponta
12
+ ## Registrar um apontamento
6
13
 
7
- Cada apontamento vira uma Issue no repositório do Opus. Antes de abrir, procure uma Issue existente para o mesmo comportamento e comente nela. Dois tipos:
14
+ Cada apontamento vira uma Issue em
15
+ [github.com/softize-dev/opus](https://github.com/softize-dev/opus/issues). Antes de abrir uma nova,
16
+ procure uma Issue sobre o mesmo comportamento e comente nela. Há dois tipos:
8
17
 
9
- - **`enhancement`** — código genérico que o Opus deveria ganhar (serviria a qualquer projeto). Antes de apontar, confira o que o Opus já tem (o inventário de componentes, os exports, esta doc): se já existe, use.
10
- - **`bug`** — um defeito ou limitação no próprio Opus, achado usando-o.
18
+ - **`enhancement`:** código genérico que serviria a qualquer projeto. Antes de apontar, confira se o
19
+ Opus já oferece o recurso no inventário de componentes, nos exports ou nesta documentação; se
20
+ oferecer, use-o.
21
+ - **`bug`:** defeito ou limitação do próprio Opus, encontrado durante o uso.
11
22
 
12
- Descreva cada apontamento com estes campos — um exemplo de cada tipo:
23
+ Descreva cada apontamento com estes campos, como nos exemplos:
13
24
 
14
25
  ```text
15
26
  {"type": "enhancement", "behavior": "o comportamento em uma frase.", "file": "src/onde-vive.ts", "kind": "ui|sdk|driver|infra"}
16
- {"type": "bug", "behavior": "o que quebra ou falta.", "file": "onde se manifesta.", "note": "repro + o workaround aplicado."}
27
+ {"type": "bug", "behavior": "o que quebra ou falta.", "file": "onde se manifesta.", "note": "como reproduzir e o contorno aplicado."}
17
28
  ```
18
29
 
19
- ## Não trave esperando
30
+ O pacote não coleta apontamentos do seu projeto automaticamente.
20
31
 
21
- O ponto do ciclo é **não bloquear a entrega**. Bateu em um gap ou em um bug do Opus:
32
+ ## Seguir sem esperar a correção
22
33
 
23
- 1. **Contorne local** — componha um wrapper no seu projeto. O Opus entrega _source_, então dá para embrulhar qualquer superfície dele. Nunca edite `node_modules` (some no próximo install).
24
- 2. **Entregue** a feature com o workaround.
25
- 3. **Aponte** na Issue e siga em frente.
34
+ Uma lacuna ou um defeito do Opus não precisa bloquear a entrega:
26
35
 
27
- O "depois" — o conserto no Opus — corre em paralelo. Ele não segura o seu trabalho; o único custo de demorar é o workaround viver um pouco mais.
36
+ 1. **Contorne no projeto:** componha um wrapper local. O Opus é distribuído como código-fonte, então
37
+ qualquer superfície dele pode ser envolvida. Não edite `node_modules`, porque a alteração se
38
+ perde na próxima instalação.
39
+ 2. **Entregue** a funcionalidade com o contorno.
40
+ 3. **Registre** o apontamento na Issue.
28
41
 
29
- ## Como fecha
42
+ A correção no Opus acontece em paralelo. Enquanto ela não sai, o contorno continua no projeto.
30
43
 
31
- A triagem acontece em uma **Issue em [github.com/softize-dev/opus](https://github.com/softize-dev/opus/issues)**; procure uma existente antes de abrir. O pacote não coleta apontamentos do seu projeto por conta própria. O desfecho depende do tipo:
44
+ ## Como o apontamento termina
32
45
 
33
- - **Enhancement** aceito (com reincidência) → implementado no Opus → sai em um _bump_ → seu projeto [atualiza o Opus](upgrading) e troca o workaround pelo import.
34
- - **Bug** → vira _fix_ + entrada no `CHANGELOG` → no _bump_, o workaround sai.
46
+ - Um **enhancement** aceito é implementado no Opus e publicado em uma nova versão. Ao
47
+ [atualizar o Opus](/docs/upgrading), o projeto troca o contorno pelo import.
48
+ - Um **bug** vira uma correção com entrada no `CHANGELOG`; na atualização, o contorno sai.
35
49
 
36
- A régua e a decisão ficam com quem mantém o Opus — hoje, a **Softize**. O registro curado das promoções vive no `PROMOTED.md` do pacote.
50
+ A decisão sobre o que entra no Opus fica com a Softize, que mantém o pacote. O registro das peças
51
+ incorporadas está no `PROMOTED.md` do pacote.
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Produtos de Dados
3
+ ---
4
+
5
+ # Produtos de Dados
6
+
7
+ Use um Produto de Dados quando um conjunto de dados precisar de identidade estável para catálogos,
8
+ agentes e revisão, independentemente da action que o entrega. A declaração diz o que o conjunto
9
+ representa, de onde vem, quem responde por ele e quais actions o disponibilizam. Ela não é uma
10
+ consulta, uma tabela nem uma view, e não executa nada: cada action continua sendo a fronteira de
11
+ execução, recorte e autorização.
12
+
13
+ ## Declarar e registrar
14
+
15
+ `defineDataProduct` declara o produto e valida seus campos no momento em que roda. O domínio o
16
+ registra em `dataProducts`, ao lado das actions e entidades citadas:
17
+
18
+ ```ts
19
+ import { defineDataProduct, defineDomain } from '@softize/opus/core'
20
+ import { LeadEntity } from './entity.ts' // defineEntity({ name: 'lead', … })
21
+ import { leadList } from './actions/list.ts' // action 'lead.list'
22
+ import { leadFunnel } from './actions/funnel.ts' // action 'lead.funnel'
23
+
24
+ export const salesLeads = defineDataProduct({
25
+ id: 'sales.leads',
26
+ version: 1,
27
+ label: 'Leads comerciais',
28
+ description: 'Leads e seus resultados comerciais.',
29
+ owner: 'Vendas',
30
+ grain: 'Um lead.',
31
+ classification: 'internal',
32
+ nature: 'real',
33
+ sources: [{ id: 'crm', label: 'CRM comercial' }],
34
+ entities: ['lead'],
35
+ access: {
36
+ permissionContexts: ['sales'],
37
+ organizationalScopes: ['unit', 'team'],
38
+ },
39
+ interfaces: ['lead.list', 'lead.funnel'],
40
+ })
41
+
42
+ export const leadDomain = defineDomain({
43
+ name: 'lead',
44
+ entities: { LeadEntity },
45
+ actions: { leadList, leadFunnel },
46
+ dataProducts: { salesLeads },
47
+ })
48
+ ```
49
+
50
+ `interfaces` e `entities` usam os nomes declarados em `bindAction`/`defineContract` e em
51
+ `defineEntity`, não nomes de arquivo ou de tabela. `sources` identifica os sistemas ou conjuntos de
52
+ origem; uma fonte não precisa ser uma entidade do Opus.
53
+
54
+ ## Verificações
55
+
56
+ A declaração é conferida em três momentos:
57
+
58
+ - `defineDataProduct` lança `TypeError` quando o `id` não tem namespace (como `sales.leads`), a
59
+ `version` não é um inteiro positivo, falta texto obrigatório, `sources` ou `interfaces` estão
60
+ vazias ou há itens repetidos.
61
+ - `defineDomain` recusa um produto duplicado no domínio e referências a actions ou entidades que o
62
+ domínio não declara (`domain.data_product_interface_not_found` e
63
+ `domain.data_product_entity_not_found`).
64
+ - `opus check` lê o código sem executá-lo e confere exportação, `id`, `version`, duplicidade e as
65
+ referências a actions e entidades do projeto, com as regras de prefixo `data-product`. Para isso,
66
+ o produto precisa ser `export const` e usar valores literais.
67
+
68
+ ## Acesso e segurança
69
+
70
+ `access` documenta o alcance esperado de forma verificável. Ele ajuda o catálogo e a revisão a
71
+ perceber, por exemplo, que uma interface deveria respeitar unidade e equipe. `access` não é RLS nem
72
+ autorização automática: cada action listada em `interfaces` precisa aplicar o próprio `authorize` e
73
+ os recortes no handler ou no repositório, inclusive quando chamada por IA ou por
74
+ [MCP](/docs/mcp).
75
+
76
+ `permissionContexts` nomeia chaves do vocabulário de permissão do projeto. Ele não descreve área,
77
+ departamento nem contexto de tela; um projeto que não usa essa dimensão declara uma lista vazia.
78
+ O nome antigo `contexts` ainda é aceito e projetado para compatibilidade, mas declarações novas usam
79
+ `permissionContexts`.
80
+
81
+ ## Onde o produto aparece
82
+
83
+ - `opus gen` publica os produtos no `.opus/manifest.json`, e `opus introspect` os inclui no modelo
84
+ do projeto. Veja [CLI opus](/docs/cli).
85
+ - Em `runtime.aiTools()`, cada action exposta à IA traz em `metadata.dataProducts` os IDs dos
86
+ produtos relacionados e, em `metadata.dataProductLabels`, o `label` de cada ID. Veja
87
+ [IA generativa](/docs/ai).
88
+ - No servidor MCP, as mesmas informações aparecem em `_meta['com.softize.opus/data-products']` e
89
+ `_meta['com.softize.opus/data-product-labels']`.
90
+
91
+ Esses metadados servem a quem hospeda o agente: mostre o `label` às pessoas e use o ID na execução e
92
+ na auditoria. Eles não entram no prompt e não concedem acesso. Nenhuma dessas projeções é a fonte
93
+ da verdade: para mudar um produto, corrija a declaração e gere de novo.
94
+
95
+ ## Evoluir um produto
96
+
97
+ Incremente `version` quando a forma observável do produto mudar. Para substituí-lo, mantenha a
98
+ declaração anterior com `status: 'deprecated'` e `replacedBy` apontando para o novo ID durante a
99
+ migração. Um produto ativo não declara `replacedBy`.
100
+
101
+ ## Referência de DataProductConfig
102
+
103
+ | Campo | Contrato |
104
+ | --- | --- |
105
+ | `id` | Identidade estável com namespace, como `sales.leads`. |
106
+ | `version` | Inteiro positivo; muda com a forma observável do produto. |
107
+ | `label`, `description` | Nome e descrição para pessoas. |
108
+ | `owner` | Responsável técnico ou de negócio. |
109
+ | `grain` | O que uma linha ou observação representa. |
110
+ | `classification`, `nature` | Classificação (como `internal` ou `personal`) e natureza (como `real`, `demo` ou `synthetic`) definidas pelo projeto. |
111
+ | `sources` | Pelo menos uma fonte `{ id, label, description? }`, sem `id` repetido. |
112
+ | `entities` | Nomes das entidades Opus que estruturam o produto; pode ser vazia. |
113
+ | `access` | `permissionContexts` e `organizationalScopes`, ambos listas de texto. |
114
+ | `interfaces` | Nomes das actions que disponibilizam o produto; pelo menos uma. |
115
+ | `status`, `replacedBy` | `active` (padrão) ou `deprecated`; um produto `deprecated` exige `replacedBy`. |
@@ -1,11 +1,25 @@
1
1
  ## Estados
2
2
 
3
- Use `DataState` para apresentar carregamento, erro, vazio e conteúdo de uma mesma consulta. As
4
- superfícies são as mesmas de `PageState`, na escala de uma seção: o carregamento é um contêiner
5
- `role="status"` com o `Spinner`; o vazio compõe `Empty` com moldura sólida e `emptyMessage` como
6
- título; o erro é um `Alert` de contexto `danger` com uma mensagem segura, sem expor detalhes
7
- técnicos, e uma ação `ghost` só com ícone quando há `onRetry`. `retryLabel` fornece o nome acessível
8
- e o tooltip dessa ação. Para uma ação em andamento depois do clique, use `busy` em `Button`.
3
+ Use `DataState` para apresentar carregamento, erro, vazio e conteúdo de uma mesma consulta, na escala
4
+ de uma seção. Para o estado da página inteira, use `PageState`, descrito em [`Page`](/ui/page); para
5
+ uma coleção carregada por uma action `kind: 'list'`, [`ActionList`](/ui/action-list) já trata esses
6
+ estados.
7
+
8
+ Cada estado tem uma apresentação fixa:
9
+
10
+ - **Carregando:** um contêiner `role="status"` com o [`Spinner`](/ui/spinner) centralizado. Quando o
11
+ formato do conteúdo já é conhecido, em vez de passar `loading`, renderize o
12
+ [`Skeleton`](/ui/skeleton) durante a espera: ele reproduz esse formato e evita o salto de layout.
13
+ - **Vazio:** um [`Empty`](/ui/empty) com moldura sólida e `emptyMessage` como frase discreta.
14
+ - **Erro:** um [`Alert`](/ui/alert) de contexto `danger` com a mensagem segura de `errorMessage`,
15
+ sem expor o texto técnico de `error`. Com `onRetry`, o aviso ganha uma ação `ghost` só com ícone,
16
+ e `retryLabel` fornece o nome acessível e o tooltip dessa ação.
17
+
18
+ Quando mais de uma condição vale ao mesmo tempo, vence a primeira desta ordem: erro, carregando,
19
+ vazio. O conteúdo aparece somente quando nenhuma delas vale. Assim, uma nova tentativa com
20
+ `loading` e `error` ativos continua mostrando o erro até que `error` volte a `null`.
21
+
22
+ Para uma ação em andamento depois do clique, use `busy` em [`Button`](/ui/button).
9
23
 
10
24
  ```tsx preview col
11
25
  <div className="w-full space-y-3">
@@ -32,9 +32,9 @@ export const SkillEntity = defineEntity({
32
32
 
33
33
  ## Código em camelCase, banco em snake_case
34
34
 
35
- O schema tipado do Kysely e todas as consultas usam camelCase
36
- (`workspaceId`, `ghRepo`). O banco é snake (a DDL no schema idempotente — `db/schema.sql`).
37
- `CamelCasePlugin` converte nomes nas consultas e nos resultados. Um sink que não usa o Kysely com o
35
+ O schema tipado do Kysely e todas as consultas usam camelCase (`workspaceId`, `ghRepo`). O banco
36
+ usa snake_case, definido na DDL do schema idempotente, como `db/schema.sql`. `CamelCasePlugin`
37
+ converte os nomes nas consultas e nos resultados. Um sink que não usa o Kysely com o
38
38
  plugin, como `audit_log`, mantém os nomes do banco.
39
39
 
40
40
  ```ts
@@ -52,19 +52,20 @@ await db.selectFrom('agents').select(['isDefault', 'roleId']).where('workspaceId
52
52
 
53
53
  Cada etapa possui um papel diferente:
54
54
 
55
- - `opus db migrate` — aplica o **schema idempotente** (`config.schema`, um script SQL
56
- evolutivo: `IF NOT EXISTS` + guards cobrem nascer do zero e upgrade no mesmo artefato)
57
- e roda o drift-check entidade ↔ banco na sequência (exit ≠ 0 se divergir).
58
- - `opus seed` — aplica datasets de desenvolvimento e teste declarados com `defineSeed` e registrados
59
- no `opus.config.ts`; só conecta com `NODE_ENV=development` ou `test`. Ver [CLI opus](cli).
55
+ - `opus db migrate` aplica o schema idempotente (`config.schema`), um único script SQL em que
56
+ `IF NOT EXISTS` e condições de guarda cobrem tanto o banco novo quanto a atualização. Em seguida,
57
+ compara entidades e banco e encerra com exit ≠ 0 quando há divergência.
58
+ - `opus seed` aplica datasets de desenvolvimento e teste declarados com `defineSeed` e registrados no
59
+ `opus.config.ts`. Ele só conecta com `NODE_ENV=development` ou `test`. Veja
60
+ [CLI opus](/docs/cli).
60
61
 
61
62
  ## Campos JSON
62
63
 
63
- `kyselyRepo` serializa campos `t.json()` na escrita (`insert` e `update`), guiado pela
64
- declaração. Sem isso, o pg até stringifica objeto plano — mas **array vira literal de
65
- array do PG** (errado para jsonb) sem quebrar typecheck. String passa direto (quem já
66
- mandava pré-serializado segue valendo); na leitura o pg devolve objeto. Query à mão
67
- (fora do repo) continua responsável pelo próprio stringify.
64
+ `kyselyRepo` serializa os campos `t.json()` na escrita (`insert` e `update`), guiado pela
65
+ declaração da entidade. Sem essa etapa, o driver `pg` converte um objeto simples, mas transforma um
66
+ array em literal de array do Postgres, o que é inválido para `jsonb` e não aparece no typecheck.
67
+ Uma string passa sem alteração, o que preserva valores já serializados; na leitura, o `pg` devolve
68
+ o objeto. Uma consulta escrita fora do repositório continua responsável pela própria serialização.
68
69
 
69
70
  ## SQL gerado por modelo
70
71
 
@@ -79,13 +80,13 @@ const bi = readOnlyContextPool({ pool: new Pool({ connectionString, max: 4 }) })
79
80
  const { rows } = await bi.query(sqlDoLlm, { role: roleFor(ctx) })
80
81
  ```
81
82
 
82
- 1. **Pool com limite** (`max` e/ou `maxConcurrent`) — impede que as consultas consumam todas as
83
+ 1. **Pool com limite** (`max`, `maxConcurrent` ou ambos): impede que as consultas consumam todas as
83
84
  conexões do aplicativo.
84
- 2. **`BEGIN TRANSACTION READ ONLY`** — o servidor rejeita tentativas de escrita;
85
- 3. **`SET LOCAL ROLE` por transação** — o alcance é definido pelo contexto (crie os roles com grants
86
- default-fechado nas suas migrações: sem isso, `sales_read` leria `hr_employees`);
87
- 4. **Protocolo estendido** — o SQL sempre recebe os valores separadamente; múltiplas instruções
88
- (`SELECT 1; DROP …`) é recusada pelo próprio protocolo.
85
+ 2. **`BEGIN TRANSACTION READ ONLY`:** o servidor rejeita tentativas de escrita.
86
+ 3. **`SET LOCAL ROLE` por transação:** o contexto define o alcance da consulta. Crie os roles nas
87
+ suas migrações com grants fechados por padrão; sem isso, `sales_read` leria `hr_employees`.
88
+ 4. **Protocolo estendido:** o SQL sempre recebe os valores separadamente, e múltiplas instruções
89
+ (`SELECT 1; DROP …`) são recusadas pelo próprio protocolo.
89
90
 
90
91
  Ao terminar, `DISCARD ALL` limpa a conexão. Se a limpeza falhar, a conexão é descartada em vez de
91
92
  voltar ao pool. `statement_timeout`, aplicado por transação, limita consultas demoradas; o padrão é