@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
@@ -8,15 +8,15 @@ de fechamento.
8
8
  ```tsx preview
9
9
  <Drawer>
10
10
  <DrawerTrigger asChild>
11
- <Button variant="outline">Ver detalhe</Button>
11
+ <Button variant="outline">Ver veículo</Button>
12
12
  </DrawerTrigger>
13
13
  <DrawerContent side="right">
14
14
  <DrawerHeader>
15
- <DrawerTitle>Criar ticket</DrawerTitle>
15
+ <DrawerTitle>ABC-1D23</DrawerTitle>
16
16
  </DrawerHeader>
17
- <DrawerBody className="text-sm text-muted-foreground">
18
- <p>Ação · domínio Suporte</p>
19
- <p>Campos, entrada/saída, arquivo…</p>
17
+ <DrawerBody className="space-y-1 text-sm text-muted-foreground">
18
+ <p>Corolla XEi 2022 · 38.400 km</p>
19
+ <p>Última revisão em 12 de maio.</p>
20
20
  </DrawerBody>
21
21
  </DrawerContent>
22
22
  </Drawer>
@@ -36,11 +36,11 @@ neutra e sutil; `closeSize` escolhe entre `xs` e `sm`, com `sm` como padrão.
36
36
  </DrawerTrigger>
37
37
  <DrawerContent side="left">
38
38
  <DrawerHeader>
39
- <DrawerTitle>Editar workspace</DrawerTitle>
39
+ <DrawerTitle>Editar veículo</DrawerTitle>
40
40
  </DrawerHeader>
41
41
  <DrawerBody className="space-y-2">
42
- <Label htmlFor="ws-nome">Nome</Label>
43
- <Input id="ws-nome" defaultValue="Empresa X" />
42
+ <Label htmlFor="vehicle-owner">Responsável</Label>
43
+ <Input id="vehicle-owner" defaultValue="Marina Alves" />
44
44
  </DrawerBody>
45
45
  <DrawerFooter>
46
46
  <ButtonGroup mode="spaced">
@@ -75,11 +75,11 @@ ruído.
75
75
  </DrawerTrigger>
76
76
  <DrawerContent layout="continuous">
77
77
  <DrawerHeader>
78
- <DrawerTitle>Resumo da execução</DrawerTitle>
78
+ <DrawerTitle>Resumo da revisão</DrawerTitle>
79
79
  </DrawerHeader>
80
80
  <DrawerBody>
81
81
  <p className="text-sm text-muted-foreground">
82
- Concluída em 2 minutos, sem falhas registradas.
82
+ Concluída em 2 horas, sem pendências registradas.
83
83
  </p>
84
84
  </DrawerBody>
85
85
  </DrawerContent>
@@ -98,7 +98,7 @@ título ocupa a folga, as ações vão para o extremo oposto e o botão de fecha
98
98
  nó que não seja um desses slots interrompe a renderização com um erro; contexto e conteúdo
99
99
  pertencem ao `DrawerBody`.
100
100
 
101
- `DrawerMedia` é a moldura de ícone da casa — o mesmo [`IconTile`](/ui/icon-tile) —, útil para
101
+ `DrawerMedia` é a moldura de ícone do Opus — o mesmo [`IconTile`](/ui/icon-tile) —, útil para
102
102
  identificar o tipo do recurso aberto. `DrawerNavigation` carrega o retorno de um fluxo encadeado,
103
103
  em que o painel permanece aberto e troca de etapa.
104
104
 
@@ -110,7 +110,7 @@ em que o painel permanece aberto e troca de etapa.
110
110
  <DrawerContent side="right">
111
111
  <DrawerHeader>
112
112
  <DrawerNavigation>
113
- <Button variant="ghost" size="icon-sm" aria-label="Voltar para a lista">
113
+ <Button variant="ghost" size="icon" aria-label="Voltar para a lista">
114
114
  <ArrowLeft />
115
115
  </Button>
116
116
  </DrawerNavigation>
@@ -119,7 +119,7 @@ em que o painel permanece aberto e troca de etapa.
119
119
  </DrawerMedia>
120
120
  <DrawerTitle>ABC-1D23</DrawerTitle>
121
121
  <DrawerActions>
122
- <Button variant="ghost" size="icon-sm" aria-label="Mais ações">
122
+ <Button variant="ghost" size="icon" aria-label="Mais ações">
123
123
  <Ellipsis />
124
124
  </Button>
125
125
  </DrawerActions>
@@ -131,15 +131,57 @@ em que o painel permanece aberto e troca de etapa.
131
131
  </Drawer>
132
132
  ```
133
133
 
134
+ Voltar e ações do cabeçalho usam `Button` `ghost` no tamanho `icon` (2.25rem), o mesmo que a
135
+ [`Presentation`](/ui/presentation) usa no voltar. O botão de fechar é menor: um `CloseButton` no
136
+ quadrado `icon-sm` (1.75rem) por padrão.
137
+
134
138
  O cabeçalho não recebe descrição: o painel já nomeia o recurso, e o contexto que ajuda a decidir
135
139
  aparece no começo do corpo, como texto ou `Alert`.
136
140
 
141
+ ## Impedir o fechamento durante uma operação
142
+
143
+ Esc, o clique no overlay e o botão de fechar encerram o painel. Enquanto uma operação não puder ser
144
+ interrompida, passe `closeDisabled` a `DrawerContent`: o botão de fechar continua visível, mas
145
+ desabilitado, e Esc e o clique no overlay deixam de fechar o painel. Se você também passar
146
+ `onEscapeKeyDown`, `onPointerDownOutside` ou `onInteractOutside`, esses handlers continuam sendo
147
+ chamados antes do bloqueio.
148
+
149
+ `closeDisabled` não alcança as ações que você compõe no painel, como um `Cancelar` com
150
+ `DrawerClose`; desabilite-as pelo mesmo estado da operação. Quando a pessoa precisar responder antes
151
+ de continuar, use [`Dialog mode="alert"`](/ui/dialog), que não fecha pelo clique externo.
152
+
153
+ ## Acessibilidade
154
+
155
+ - `DrawerTitle` nomeia o painel para tecnologias assistivas. Quando um texto conciso do corpo também
156
+ precisar descrevê-lo, relacione seu `id` por `aria-describedby` em `DrawerContent`.
157
+ - Com `modal` ativo, o padrão, o foco permanece dentro do painel e o restante da página fica
158
+ inacessível até o fechamento.
159
+ - Ao fechar, o foco retorna ao gatilho.
160
+ - O botão de fechar tem o nome acessível `Fechar`. Botões só com ícone no cabeçalho precisam de
161
+ `aria-label`.
162
+
137
163
  ## Propriedades de Drawer
138
164
 
139
- | Propriedade | Tipo | Padrão | Descrição |
140
- | -------------- | ------------------------- | ------ | ------------------------------------------------------------------ |
141
- | `open` | `boolean` | | Estado no modo controlado. |
142
- | `onOpenChange` | `(open: boolean) => void` | | Atualiza o estado para permitir abertura ou fechamento por código. |
165
+ | Propriedade | Tipo | Padrão | Descrição |
166
+ | -------------- | ------------------------- | ------- | ---------------------------------------------------------------------------- |
167
+ | `open` | `boolean` | | Estado no modo controlado. |
168
+ | `defaultOpen` | `boolean` | `false` | Abertura inicial no modo não controlado. |
169
+ | `onOpenChange` | `(open: boolean) => void` | | Recebe as mudanças de abertura, inclusive Esc, overlay e botão de fechar. |
170
+ | `modal` | `boolean` | `true` | Mantém foco e interação restritos ao painel aberto e exibe o overlay. |
171
+
172
+ ## Propriedades de DrawerTrigger
173
+
174
+ | Propriedade | Tipo | Padrão | Descrição |
175
+ | ----------- | ----------- | ------- | -------------------------------------------------- |
176
+ | `asChild` | `boolean` | `false` | Usa o filho como gatilho sem criar outro elemento. |
177
+ | `children` | `ReactNode` | | Controle que abre o painel. |
178
+
179
+ ## Propriedades de DrawerClose
180
+
181
+ | Propriedade | Tipo | Padrão | Descrição |
182
+ | ----------- | ----------- | ------- | ----------------------------------------------------------- |
183
+ | `asChild` | `boolean` | `false` | Aplica o fechamento ao filho; use com `Button`. |
184
+ | `children` | `ReactNode` | | Controle que fecha o painel, como o `Cancelar` do rodapé. |
143
185
 
144
186
  ## Propriedades de DrawerContent
145
187
 
@@ -147,9 +189,11 @@ aparece no começo do corpo, como texto ou `Alert`.
147
189
  | ----------------- | ---------------------------------------- | --------- | ------------------------------------------------------------------------------------------ |
148
190
  | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'right'` | Borda de onde o painel aparece. |
149
191
  | `layout` | `'continuous' \| 'sectioned'` | `'sectioned'` | Anatomia da superfície inteira; os slots leem esse valor e não o recebem. |
150
- | `showCloseButton` | `boolean` | `true` | Inclui a ação de fechamento no cabeçalho. Desative quando o painel exigir uma ação explícita. |
151
- | `closeDisabled` | `boolean` | `false` | Mantém a ação visível e impede fechar enquanto o painel está bloqueado. |
192
+ | `showCloseButton` | `boolean` | `true` | Inclui o botão de fechar no cabeçalho. Sem ele, Esc e o overlay continuam fechando o painel. |
193
+ | `closeDisabled` | `boolean` | `false` | Mantém o botão de fechar visível e desabilitado e impede que Esc e o clique no overlay fechem o painel. |
152
194
  | `closeSize` | `"xs" \| "sm"` | `"sm"` | Define o tamanho da ação circular de fechar. |
195
+ | `className` | `string` | | Ajusta o painel, como a largura. |
196
+ | `children` | `ReactNode` | | Cabeçalho, corpo e rodapé. |
153
197
 
154
198
  ## Propriedades de DrawerHeader
155
199
 
@@ -1,7 +1,7 @@
1
- Valor ausente tem uma representação padrão. Nas células de tabela e nos pares de `DetailField`
2
- aparece o travessão, enquanto a leitura assistiva recebe “Não informado”. As colunas de
3
- `ActionList` e o `DetailField` já fazem isso sozinhos; um renderer customizado reutiliza a
4
- primitiva em vez de repetir a condicional.
1
+ Valor ausente tem uma representação padrão. Nas células de tabela e nos pares de
2
+ [`DetailField`](/ui/detail) aparece o travessão, enquanto a leitura assistiva recebe “Não
3
+ informado”. As colunas de [`ActionList`](/ui/action-list) e o `DetailField` já fazem isso sozinhos;
4
+ uma célula ou um componente próprio usa `EmptyValue` em vez de repetir a condicional.
5
5
 
6
6
  ```tsx preview
7
7
  <div className="flex items-center gap-6 text-sm">
@@ -15,8 +15,19 @@ primitiva em vez de repetir a condicional.
15
15
  ## O que conta como ausência
16
16
 
17
17
  `null`, `undefined`, string vazia e string só com espaços. `0`, `false` e coleções vazias são
18
- valores legítimos: quem apresenta booleanos e listas decide a própria forma. `isAbsentValue`
19
- exporta a mesma regra para renderers customizados.
18
+ valores legítimos: quem apresenta booleanos e listas decide a própria forma. `isAbsentValue` aplica
19
+ a mesma regra em células e componentes próprios.
20
+
21
+ ```ts
22
+ function isAbsentValue(value: unknown): boolean
23
+ ```
24
+
25
+ ```tsx
26
+ import { EmptyValue, isAbsentValue } from '@softize/opus/ui/react'
27
+
28
+ // Em uma célula própria:
29
+ isAbsentValue(row.phone) ? <EmptyValue compact /> : formatPhone(row.phone)
30
+ ```
20
31
 
21
32
  ## O domínio dá o significado
22
33
 
@@ -46,3 +57,6 @@ columns: [
46
57
  |---|---|---|---|
47
58
  | `label` | `string` | `'Não informado'` | O que a ausência significa neste domínio. |
48
59
  | `compact` | `boolean` | `false` | Célula compacta: travessão visível, rótulo só para leitura assistiva. |
60
+
61
+ Os demais atributos, como `className`, vão para o elemento `span`. `EmptyValue` não aceita
62
+ `children`: o texto entra por `label`.
@@ -4,9 +4,9 @@ Use `Empty` quando a própria região está disponível para receber ou criar co
4
4
  tracejada diferencia esse espaço de uma estrutura de dados já estabelecida e pode combinar título,
5
5
  descrição, mídia e ação.
6
6
 
7
- Para uma lista ou tabela carregada, mas sem registros, use `DataState` ou `ActionList`: a moldura
8
- sólida preserva a estrutura. Se a região já estiver emoldurada pelo pai, mantenha a borda somente
9
- no pai para evitar uma moldura dupla.
7
+ Para uma lista ou tabela carregada, mas sem registros, use [`DataState`](/ui/data-state) ou
8
+ [`ActionList`](/ui/action-list): a moldura sólida preserva a estrutura. Se a região já estiver
9
+ emoldurada pelo pai, mantenha a borda somente no pai para evitar uma moldura dupla.
10
10
 
11
11
  ## Com ícone
12
12
 
@@ -78,3 +78,24 @@ ou mídias que tenham presença visual própria.
78
78
  | Propriedade | Tipo | Padrão | Descrição |
79
79
  |---|---|---|---|
80
80
  | `variant` | `'default' \| 'icon'` | `'default'` | `icon` aplica uma moldura quadrada e muted; `default` exibe a mídia sem fundo. |
81
+
82
+ ## Propriedades de EmptyHeader
83
+
84
+ `EmptyHeader` agrupa mídia, título e descrição no centro, com largura limitada para a leitura. Não
85
+ tem props próprias; aceita as de um elemento `div`.
86
+
87
+ ## Propriedades de EmptyTitle
88
+
89
+ `EmptyTitle` exibe a situação em destaque. Ele renderiza um `div`, sem nível de heading; quando a
90
+ região precisar de um título na estrutura da página, passe `role="heading"` e `aria-level`. Não tem
91
+ props próprias; aceita as de um elemento `div`.
92
+
93
+ ## Propriedades de EmptyDescription
94
+
95
+ `EmptyDescription` apresenta a explicação em cor atenuada e sublinha os links filhos diretos. Não
96
+ tem props próprias; aceita as de um elemento `div`.
97
+
98
+ ## Propriedades de EmptyActions
99
+
100
+ `EmptyActions` empilha e centraliza as ações abaixo do cabeçalho. Não tem props próprias; aceita as
101
+ de um elemento `div`.
@@ -4,9 +4,9 @@ title: Eventos
4
4
 
5
5
  # Eventos
6
6
 
7
- Use eventos quando uma action precisar anunciar um fato sem conhecer seus consumidores.
8
- `ctx.emit` publica o evento; reactions e integrações podem assiná-lo pelo `EventBusAdapter`. O
9
- formato declarado permanece o mesmo durante todo o fluxo.
7
+ Use eventos quando uma action precisar anunciar um fato sem conhecer quem vai reagir a ele.
8
+ `ctx.emit` publica o evento; reactions e integrações o recebem pelo `EventBusAdapter`. O formato do
9
+ evento é o mesmo do handler que emite até quem consome.
10
10
 
11
11
  ## O contrato
12
12
 
@@ -24,6 +24,8 @@ interface DomainEvent<T = unknown> {
24
24
  tenant?: string | null
25
25
  data: T
26
26
  source: { action: string; actionId: string; correlation?: string }
27
+ trace?: TraceContext // trace da execução que emitiu
28
+ meta?: Record<string, unknown> // o terceiro argumento de ctx.emit
27
29
  }
28
30
  ```
29
31
 
@@ -32,10 +34,15 @@ interface DomainEvent<T = unknown> {
32
34
  ```ts
33
35
  import { mittEvents } from '@softize/opus/events/mitt'
34
36
 
35
- const bus = mittEvents() // in-process (mesmo processo do runtime)
37
+ const bus = mittEvents() // no mesmo processo do runtime
36
38
  ```
37
39
 
38
- ## No runtime
40
+ O `mitt` entrega pelo nome exato do evento ou por `*`, que recebe todos. Padrões parciais, como
41
+ `nota.*`, não são reconhecidos: assine `*` e filtre no handler.
42
+
43
+ ## Emitir de uma action
44
+
45
+ Configure o barramento no runtime e declare no binding os eventos que a action emite:
39
46
 
40
47
  ```ts
41
48
  const runtime = createRuntime({
@@ -43,19 +50,72 @@ const runtime = createRuntime({
43
50
  eventBus: bus,
44
51
  })
45
52
 
46
- // No handler: emite pelo tipo declarado; o core preenche id/timestamp/actor/source.
47
- handler: async (ctx, input) => {
48
- const nota = await repo.create(input)
49
- await ctx.emit('nota.emitida', { id: nota.id, valor: nota.valor })
50
- return nota
51
- }
53
+ export const notaEmitir = bindAction(notaEmitirContract, {
54
+ emits: ['nota.emitida'],
55
+ handler: async (ctx, input) => {
56
+ const nota = await repo.create(input)
57
+ // O runtime preenche id, timestamp, actor, tenant, source e trace.
58
+ await ctx.emit('nota.emitida', { id: nota.id, valor: nota.valor })
59
+ return nota
60
+ },
61
+ })
52
62
  ```
53
63
 
54
- Actions declaram no contrato o que emitem (`emits: ['nota.emitida']`); o runtime avisa se
55
- o handler emitir algo não-declarado (`emitMode`). Uma reaction assina `nota.emitida` e roda
56
- como sua própria action rastreável (provenance `reaction`).
64
+ `emits` pertence ao binding, como `handler`, e o runtime só o considera em actions `simple` e
65
+ `form`. Quando uma delas declara `emits` e o handler emite um evento fora da lista, o runtime
66
+ registra um aviso no log. Esse aviso vem de `config.warnUndeclaredEmits`, ativo por padrão fora de produção. Sem `emits`
67
+ declarado, nenhum aviso é gerado.
68
+
69
+ Em uma action `list` ou `view`, o tipo do binding aceita `emits`, mas a lista não entra no manifest
70
+ nem na conferência dos eventos emitidos. Para que a declaração não pareça ter efeito, o runtime
71
+ registra o aviso `runtime.action_field_ignored` quando a action é registrada com `config.dev` ativo,
72
+ o padrão sempre que `config.env` não é `'production'`. O [`opus check`](/docs/cli) aponta o mesmo
73
+ caso com a regra `ignored-by-kind`. Se a operação anuncia um fato, declare-a como `simple` ou `form`.
74
+
75
+ `config.emitMode` decide o que acontece quando a publicação não é possível. No padrão, `lenient`, a
76
+ falta de `EventBusAdapter` e uma falha de `publish` viram aviso no log e o handler segue. Em
77
+ `strict`, as duas situações interrompem a action com `emit.no_bus` ou `emit.failed`.
78
+
79
+ ## Reagir a um evento
80
+
81
+ Uma reaction executa código quando um evento é publicado. Como nas actions, a declaração pode ficar
82
+ no código compartilhado e o efeito, no servidor:
83
+
84
+ ```ts
85
+ import { bindReaction, defineReactionContract } from '@softize/opus/core'
86
+
87
+ // shared/ — qual evento e o que a reação significa.
88
+ export const notaAvisarFinanceiro = defineReactionContract<{ id: string; valor: number }>({
89
+ name: 'nota.avisar-financeiro',
90
+ on: 'nota.emitida',
91
+ description: 'Avisa o financeiro quando uma nota é emitida.',
92
+ })
93
+
94
+ // api/ — o efeito.
95
+ export const notaAvisarFinanceiroImpl = bindReaction(notaAvisarFinanceiro, {
96
+ handler: async (ctx, event) => {
97
+ ctx.log.info('nota emitida', { id: event.data.id, valor: event.data.valor })
98
+ },
99
+ })
100
+ ```
101
+
102
+ Registre a reaction com as actions, em `defineDomain({ reactions: [notaAvisarFinanceiroImpl] })`
103
+ ou em `runtime.register`. Com reactions registradas, o `start()` exige um `EventBusAdapter` e falha
104
+ com `runtime.eventbus_required` quando ele não existe. `defineReaction` declara tudo num objeto só,
105
+ quando não há separação entre cliente e servidor.
106
+
107
+ A reaction roda com provenance `reaction`; `ctx.user` traz somente o `id` do autor do evento, e
108
+ `ctx.tenantId`, o tenant do evento. O contexto não tem `can`: a decisão fica no `authorize(ctx, event)` opcional, e uma resposta `false` descarta o evento
109
+ sem erro. Uma falha no handler é registrada no log e não chega à action que emitiu.
57
110
 
58
111
  ## Limites atuais
59
112
 
60
- O driver `mitt` é in-process — sem durabilidade nem entre-processos. Barramentos duráveis/
61
- distribuídos (Redis, NATS…) entram por reincidência de caso real.
113
+ O driver `mitt` funciona no mesmo processo, sem durabilidade nem entrega entre processos.
114
+
115
+ Uma reaction não encadeia outra por evento: dentro dela, `ctx.emit` descarta o evento sem publicá-lo.
116
+ Com `config.dev` ativo, cada descarte registra o aviso `runtime.reaction_emit_ignored` no log, com o
117
+ nome da reaction e do evento; em produção, o descarte continua silencioso. Quando outro consumidor
118
+ precisa desse fato, emita o evento no handler da action que o produz e declare-o em `emits`.
119
+
120
+ `retry`, `timeout`, `dedup` e `concurrency` são aceitos na declaração, mas o runtime ainda não os
121
+ aplica: uma reaction que falha não é repetida.
@@ -9,7 +9,7 @@ O módulo é experimental e opt-in: instalar o Opus não passa a descobrir crons
9
9
  O executor continua dono do estado; este recurso mantém uma fotografia para consulta.
10
10
 
11
11
  Para colocar trabalho em segundo plano, use [Filas](/docs/queue). Para acompanhar trabalho já
12
- existente, adapte seu executor à projeção abaixo. Não usar a projeção para decidir reenvio ou retry.
12
+ existente, adapte seu executor à projeção abaixo. Não use a projeção para decidir reenvio ou retry.
13
13
 
14
14
  ## Publicar uma fotografia
15
15
 
@@ -43,8 +43,8 @@ Cron que enfileira trabalho continua com
43
43
  `trigger: 'schedule'`; entrar na fila não cria outra execução. Retomada mantém o ID e avança
44
44
  revisão/tentativa conforme o executor. A projeção não valida transições de negócio.
45
45
 
46
- Uma falha de projeção lança erro para o adaptador tratar. Registrar essa falha e reconciliar
47
- depois a partir do executor; não deixar o erro provocar retry de um efeito já realizado.
46
+ Uma falha de projeção lança erro para o adaptador tratar. Registre essa falha e reconcilie
47
+ depois a partir do executor; não deixe o erro provocar retry de um efeito já realizado.
48
48
  `observe` não contém proteção automática para o fluxo de negócio do consumidor.
49
49
 
50
50
  ## Persistência e processos separados
@@ -60,7 +60,7 @@ durável; o aviso não é histórico nem fila de entrega.
60
60
 
61
61
  `pgExecutionChanges({ connect, onError, channel?, reconnectMs? })` compartilha uma conexão
62
62
  LISTEN entre seus assinantes. `connect` deve entregar uma conexão nova, já conectada, dedicada
63
- e com timeout de conexão configurado. Não fornecer uma conexão emprestada por um pooler
63
+ e com timeout de conexão configurado. Não forneça uma conexão emprestada por um pooler
64
64
  transacional. O driver fecha a conexão quando sai o último assinante, registra falhas por
65
65
  `onError` e tenta reconectar após `reconnectMs` (padrão: 1.000 ms; mínimo: 100 ms).
66
66
 
@@ -87,15 +87,15 @@ async function openProgress(
87
87
  }
88
88
  ```
89
89
 
90
- Para `authorize(null)`, verificar sessão e direito de acompanhar. Para um aviso, verificar
90
+ Para `authorize(null)`, verifique a sessão e o direito de acompanhar. Para um aviso, verifique
91
91
  também tenant, operação e recurso exato. Essa verificação ocorre novamente durante o stream,
92
92
  inclusive nos heartbeats e antes de emitir uma invalidação. Heartbeats revalidam a sessão
93
93
  independentemente da fila de avisos: a função pode receber um aviso e `null` concorrentemente.
94
94
  As actions de leitura devem aplicar a mesma política: filtrar a
95
95
  consulta por tenant não substitui autorização.
96
96
 
97
- O cliente recebe somente `event: invalidate` com `{}`. Ao recebê-lo, invalidar as queries das
98
- actions autorizadas. Manter uma conexão compartilhada no provider da aplicação, não uma por
97
+ O cliente recebe somente `event: invalidate` com `{}`. Ao recebê-lo, invalide as queries das
98
+ actions autorizadas. Mantenha uma conexão compartilhada no provider da aplicação, não uma por
99
99
  card. A primeira invalidação e a reconexão exigem nova fotografia; não há `Last-Event-ID` nem
100
100
  replay. O navegador pode usar `EventSource` com cookies same-origin e sua reconexão nativa.
101
101
 
@@ -105,9 +105,9 @@ de 128 avisos pendentes e 16 frames na saída; excedê-los encerra a conexão pa
105
105
  sem acumular um log em memória. Sessão é reavaliada no máximo a cada heartbeat enquanto a
106
106
  autorização responde; configure timeout no serviço de autorização para não mantê-la pendente.
107
107
 
108
- IDs, tenant e operação nos avisos são metadados operacionais internos. Não usar CPF, telefone,
108
+ IDs, tenant e operação nos avisos são metadados operacionais internos. Não use CPF, telefone,
109
109
  e-mail ou segredo nesses identificadores. PostgreSQL NOTIFY não é uma fronteira de autorização:
110
- restringir as credenciais e o acesso ao banco. Nada do aviso é copiado para o SSE.
110
+ restrinja as credenciais e o acesso ao banco. Nada do aviso é copiado para o SSE.
111
111
 
112
112
  ## Detectar ausência de sinal
113
113
 
@@ -1,9 +1,11 @@
1
1
  ## Campo empilhado
2
2
 
3
- Use `Field` para manter rótulo, controle, ajuda e erro com espaçamento consistente. Na orientação
4
- vertical, `FieldLabel` e o controle ficam empilhados. A ajuda fica junto ao rótulo, com `LabelHelp`,
5
- o mesmo ícone que o `ActionForm` usa para `FieldSpec.help`. Use `FieldDescription` somente quando a
6
- frase precisar ficar visível o tempo todo abaixo do controle.
3
+ Use `Field` para manter rótulo, controle, ajuda e erro com espaçamento consistente em um formulário
4
+ montado à mão. Quando o formulário executa uma action, prefira [`ActionForm`](/ui/action-form), que
5
+ monta os mesmos campos a partir do contrato. Na orientação vertical, `FieldLabel` e o controle ficam
6
+ empilhados. A ajuda fica junto ao rótulo, com [`LabelHelp`](/ui/action-form), o mesmo ícone que o
7
+ `ActionForm` usa para `FieldSpec.help`. Use `FieldDescription` somente quando a frase precisar
8
+ ficar visível o tempo todo abaixo do controle.
7
9
 
8
10
  ```tsx preview col md
9
11
  <Field>
@@ -15,7 +17,7 @@ frase precisar ficar visível o tempo todo abaixo do controle.
15
17
  </Field>
16
18
  ```
17
19
 
18
- ## Horizontal com toggle
20
+ ## Horizontal com Switch
19
21
 
20
22
  `orientation="horizontal"` posiciona o controle ao lado do texto. `FieldContent` agrupa título e
21
23
  descrição; `FieldTitle` exibe o título quando o texto não puder ser um `<label>`. Como ele não se
@@ -35,7 +37,7 @@ vincula ao controle, ligue-o ao controle por `aria-labelledby` para manter o nom
35
37
 
36
38
  ## Conjunto com legenda e erro
37
39
 
38
- Use `Content as="fieldset"` para nomear um conjunto de campos relacionados. O título vira a legenda
40
+ Use [`Content`](/ui/content) com `as="fieldset"` para nomear um conjunto de campos relacionados. O título vira a legenda
39
41
  nativa, enquanto `FieldGroup` define o espaçamento e `FieldSeparator` marca uma divisão.
40
42
  `FieldError` aparece somente quando existe mensagem de erro. Declare `data-invalid="true"` no
41
43
  `Field` para que o rótulo acompanhe o contexto de erro; `aria-invalid` marca apenas o controle. O
@@ -70,6 +72,18 @@ nativa, enquanto `FieldGroup` define o espaçamento e `FieldSeparator` marca uma
70
72
  </Content>
71
73
  ```
72
74
 
75
+ ## Campo desabilitado
76
+
77
+ Declare `disabled` no controle para bloquear a edição. Para que o rótulo acompanhe o estado,
78
+ declare também `data-disabled="true"` no `Field`: `FieldLabel` e `FieldTitle` esmaecem junto.
79
+
80
+ ```tsx preview col md
81
+ <Field data-disabled="true">
82
+ <FieldLabel htmlFor="workspace-client">Cliente</FieldLabel>
83
+ <Input id="workspace-client" defaultValue="Empresa X" disabled />
84
+ </Field>
85
+ ```
86
+
73
87
  ## Conjunto com moldura
74
88
 
75
89
  Use a moldura para distinguir grupos vizinhos que continuem pertencendo ao mesmo formulário. O
@@ -117,6 +131,7 @@ demais regiões nomeadas. A migração pode ser gradual porque os controles e o
117
131
  | -------------- | -------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------- |
118
132
  | `orientation` | `'vertical' \| 'horizontal' \| 'responsive'` | `'vertical'` | Direção fixa do campo. `responsive` permanece como alias compatível de `horizontal`. |
119
133
  | `data-invalid` | `boolean \| 'true' \| 'false'` | | Com `true`, aplica o contexto de erro ao campo e ao rótulo. O `ActionForm` define o atributo sozinho. |
134
+ | `data-disabled` | `'true' \| 'false'` | | Com `true`, esmaece `FieldLabel` e `FieldTitle`. Declare `disabled` também no controle. |
120
135
 
121
136
  ## Propriedades de FieldLegend
122
137
 
@@ -10,20 +10,18 @@ as ferramentas usam essas definições sem exigir schemas e tipos paralelos.
10
10
 
11
11
  ## O modelo
12
12
 
13
- > Comece pelo problema que o modelo compartilhado resolve antes de conhecer seus arquivos.
14
-
15
13
  As regras de uma aplicação costumam aparecer em vários lugares: validação, transporte, API,
16
- formulários e documentação. No Opus, entidades, dicts e contratos de action descrevem essas
14
+ formulários e documentação. No Opus, entidades, dicionários e contratos de action descrevem essas
17
15
  regras uma vez. A API e a web consomem a mesma definição, enquanto runtime, adapters, UI, CLI
18
16
  e verificações ajudam a mantê-la consistente. Cada projeto acrescenta seu domínio sobre essa
19
17
  base versionada.
20
18
 
21
19
  ## Começar do zero
22
20
 
23
- > Projeto novo não se monta à mão: o esqueleto canônico sai do `opus create`, com os
24
- > pré-requisitos do protocolo, da UI e do preview já plugados — e os gates verdes.
21
+ Para um projeto novo, use `opus create`: ele gera o app com os pré-requisitos do protocolo, da UI e
22
+ do preview já configurados, e os gates passam desde o início.
25
23
 
26
- O Opus e a Base ficam no npm público: leitura não exige token nem configuração de escopo.
24
+ O Opus e a Base ficam no npm público: a instalação não exige token nem configuração de escopo.
27
25
  Crie o projeto e materialize as duas camadas antes dos gates:
28
26
 
29
27
  ```bash
@@ -43,15 +41,21 @@ com `pnpm dlx @softize/opus@<versão> create meu-app` ou desative a quarentena a
43
41
  com `pnpm --config.minimum-release-age=0 dlx …`. Dentro do projeto, o workspace exclui Opus e Base
44
42
  da quarentena para preservar o par validado.
45
43
 
46
- Nasce com: `opus.config.ts` + domínio-exemplo canônico (0 violações, spec documentada),
47
- vite + react + tema do Opus (Tailwind v4 CSS-first), dev server na porta que o Maestro
48
- injeta no preview, `opus.json` (pin), instruções, skills e hooks gerenciados. A Base é
49
- dona do método geral, memória e revisão; o Opus acrescenta somente o conhecimento do SDK.
44
+ O app criado inclui:
45
+
46
+ - `opus.config.ts` e um domínio de exemplo documentado, sem violações no `opus check`;
47
+ - Vite, React e o tema do Opus sobre Tailwind v4, configurado por CSS;
48
+ - um dev server que respeita a variável `PORT`, para que um orquestrador de preview escolha a
49
+ porta;
50
+ - `opus.json`, com a versão adotada, e as instruções, skills e hooks gerenciados.
51
+
52
+ A Base cuida do método geral de trabalho, da memória e da revisão; o Opus acrescenta o conhecimento
53
+ específico do SDK.
50
54
 
51
- **Monorepo:** a raiz nasce do `--monorepo` e cada app nasce dentro dela — o create
52
- detecta o `pnpm-workspace.yaml` e gera só o que é do app (sem workspace yaml aninhado),
53
- **avisando** o que a raiz precisa ter (glob de packages, allowBuilds e exclusões de
54
- quarentena para Opus/Base) sem tocar nos seus arquivos:
55
+ Em um monorepo, crie primeiro a raiz com `--monorepo` e depois cada app dentro dela. O `create`
56
+ detecta o `pnpm-workspace.yaml` e gera só os arquivos do app, sem um workspace aninhado. O que a raiz
57
+ precisa ter, como o glob de packages, `allowBuilds` e as exclusões de quarentena para Opus e Base,
58
+ aparece como aviso, sem alterar os arquivos da raiz:
55
59
 
56
60
  ```bash
57
61
  pnpm dlx @softize/opus create meu-cliente --monorepo # a raiz do workspace
@@ -61,13 +65,10 @@ pnpm dlx @softize/opus create apps/portal # o app (modo detectado)
61
65
 
62
66
  ## Carregar o Opus em um projeto existente
63
67
 
64
- > O `opus.json` registra a versão usada pelo projeto. Se o arquivo ainda não existe, o setup
65
- > cria a estrutura necessária antes das demais etapas.
66
-
67
- A versão do Opus fica pinada em `opus.json`. O `opus setup` é per-app: grava o marcador,
68
- o inventário de copy e as projeções específicas do SDK. O `base setup` materializa o
69
- método geral, memória, agentes e revisão na raiz do repositório. Os componentes vêm do
70
- barrel `@softize/opus/ui/react`; o tema, por CSS.
68
+ O `opus.json` registra a versão do Opus adotada pelo app; se ele ainda não existe, o setup o cria.
69
+ `opus setup` roda em cada app e grava o marcador, o inventário de copy e as projeções específicas do
70
+ SDK. `base setup` materializa na raiz do repositório o método geral, a memória, os agentes e a
71
+ revisão. Os componentes vêm de `@softize/opus/ui/react`, e o tema, de um arquivo CSS.
71
72
 
72
73
  ```bash
73
74
  # Bootstrap idempotente de um projeto existente.
@@ -78,39 +79,48 @@ pnpm exec base setup
78
79
  ```
79
80
 
80
81
  ```css
81
- /* index.css do app — o tema canônico + os componentes do Opus no scan do Tailwind. */
82
+ /* index.css do app — o tema do Opus e os componentes do pacote no scan do Tailwind. */
82
83
  @import '@softize/opus/ui/theme.css';
83
84
  @source '../node_modules/@softize/opus/src/ui/**/*.{ts,tsx}';
84
85
  ```
85
86
 
86
- ## A stack do runtime
87
+ ## Montar o runtime
87
88
 
88
- > `createRuntime` compõe a stack a partir de adapters plugáveis. Cada capacidade (data, auth,
89
- > audit…) é um adapter — troca a implementação sem tocar o domínio.
89
+ No servidor, `createRuntime` reúne as capacidades que o app usa. Cada capacidade, como banco,
90
+ autenticação ou auditoria, é um adapter que pode ser trocado sem alterar o domínio. Os providers da
91
+ interface estão em [Como consumir](/ui/como-consumir).
90
92
 
91
93
  ```ts
92
94
  import { createRuntime } from '@softize/opus/core'
95
+ import { jwtAuth } from '@softize/opus/auth/jwt'
93
96
  import { kyselyData } from '@softize/opus/data/kysely'
94
97
  import { fastifyServer } from '@softize/opus/server/fastify'
95
98
  import { pgAudit } from '@softize/opus/audit/pg'
99
+ import { workspaceDomain } from './domains/workspace/index.ts'
96
100
 
97
101
  const runtime = createRuntime({
98
102
  server: fastifyServer({ app }),
99
- data: kyselyData({ db }), // Kysely + Postgres
100
- auth: makeAuthAdapter(), // gate (IdP)
103
+ data: kyselyData({ db }), // Kysely e Postgres
104
+ auth: jwtAuth({ secret: process.env.JWT_SECRET!, mapUser: (p) => ({ id: String(p.sub) }) }),
101
105
  audit: [pgAudit({ pool })], // trilha de auditoria
102
106
  config: { env, i18n: { defaultLocale: 'pt-BR' } },
103
107
  })
104
108
 
105
- // Domínios = grupos de actions. Não registrado = não existe.
106
- runtime.register([workspacesDomain, agentsDomain, skillsDomain])
109
+ // Só o que foi registrado existe para o runtime.
110
+ runtime.register([workspaceDomain])
107
111
  await runtime.start()
108
112
  ```
109
113
 
110
- ## Antes de mexer
114
+ Cada capacidade e o registro de domínios estão detalhados em [Runtime & adapters](/docs/runtime).
115
+
116
+ ## Antes de alterar o projeto
111
117
 
112
- > O protocolo de leitura: a verdade local mora nos docs do repo, não nesta página.
118
+ A documentação do próprio repositório descreve o que é específico dele; esta página cobre só o que
119
+ vale para todo projeto Opus.
113
120
 
114
- - `CLAUDE.md` do repo e do app — o índice local: mapa, gates, gotchas.
115
- - As `description` das entidades/actions — a spec de negócio, projetada no manifest pelo `opus gen`.
116
- - Gate de build: `opus check` (convenções das actions e da UI) + `opus copy --check` e `base copy check` (inventário e política de copy) + `opus db check` (drift entidade ↔ banco). Rode antes de entregar.
121
+ - O `CLAUDE.md` do repositório e de cada app reúne o mapa do código, os gates e os cuidados locais.
122
+ - A `description` de entidades e actions registra a regra de negócio, que `opus gen` projeta no
123
+ manifest.
124
+ - Antes de entregar, rode `opus check` (convenções das actions e da UI), `opus copy --check` e
125
+ `base copy check` (inventário e política de copy) e, com banco, `opus db check` (divergência entre
126
+ entidades e banco).