@softize/opus 12.10.0 → 13.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.
- package/CHANGELOG.md +56 -0
- package/bin/lib/check.mjs +1098 -310
- package/bin/lib/copy.mjs +12 -5
- package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
- package/docs/adr/0004-page-content-state-is-composed.md +65 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +180 -0
- package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
- package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
- package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
- package/docs/radius-scale.md +1 -1
- package/package.json +1 -1
- package/registry/instructions/opus.md +5 -0
- package/registry/skills/build-opus-ui/SKILL.md +27 -16
- package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
- package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
- package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
- package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
- package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
- package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
- package/registry/templates/app/src/App.tsx +1 -1
- package/src/core/dictionary.ts +52 -14
- package/src/core/index.ts +10 -0
- package/src/core/ui-context.ts +29 -0
- package/src/schema/drivers/zod.ts +17 -8
- package/src/ui/components/patterns/action-form-card.tsx +18 -12
- package/src/ui/components/patterns/confirm.tsx +163 -40
- package/src/ui/components/patterns/content-header.tsx +335 -61
- package/src/ui/components/patterns/data-state.tsx +23 -10
- package/src/ui/components/patterns/list.tsx +1097 -783
- package/src/ui/components/patterns/page-state.tsx +115 -0
- package/src/ui/components/patterns/page.tsx +231 -41
- package/src/ui/components/patterns/sidebar.tsx +357 -83
- package/src/ui/components/patterns/trigger.tsx +37 -30
- package/src/ui/components/patterns/view.tsx +7 -11
- package/src/ui/components/primitives/alert.tsx +298 -110
- package/src/ui/components/primitives/ask.tsx +2 -1
- package/src/ui/components/primitives/badge.tsx +91 -30
- package/src/ui/components/primitives/button.tsx +99 -60
- package/src/ui/components/primitives/calendar.tsx +39 -39
- package/src/ui/components/primitives/card.tsx +96 -23
- package/src/ui/components/primitives/detail.tsx +2 -2
- package/src/ui/components/primitives/dialog.tsx +196 -39
- package/src/ui/components/primitives/dictionary-value.tsx +9 -14
- package/src/ui/components/primitives/dot.tsx +74 -21
- package/src/ui/components/primitives/drawer.tsx +40 -24
- package/src/ui/components/primitives/empty.tsx +3 -3
- package/src/ui/components/primitives/item.tsx +135 -79
- package/src/ui/components/primitives/menu.tsx +11 -3
- package/src/ui/components/primitives/metric-card.tsx +133 -0
- package/src/ui/components/primitives/sonner.tsx +187 -8
- package/src/ui/components/primitives/table.tsx +2 -2
- package/src/ui/docs/DocBrowser.tsx +104 -25
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/accordion.md +22 -16
- package/src/ui/docs/content/action-form-card.md +8 -8
- package/src/ui/docs/content/action-form-dialog.md +9 -9
- package/src/ui/docs/content/action-form.md +28 -34
- package/src/ui/docs/content/action-list-dialog.md +11 -6
- package/src/ui/docs/content/action-list.md +64 -39
- package/src/ui/docs/content/action-trigger.md +21 -14
- package/src/ui/docs/content/action-view.md +8 -8
- package/src/ui/docs/content/actions.md +9 -9
- package/src/ui/docs/content/ai.md +3 -3
- package/src/ui/docs/content/alert.md +54 -28
- package/src/ui/docs/content/aspect-ratio.md +4 -4
- package/src/ui/docs/content/audit.md +2 -2
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/avatar.md +34 -14
- package/src/ui/docs/content/badge.md +21 -22
- package/src/ui/docs/content/breadcrumb.md +13 -8
- package/src/ui/docs/content/button.md +93 -15
- package/src/ui/docs/content/calendar.md +5 -5
- package/src/ui/docs/content/card.md +6 -6
- package/src/ui/docs/content/carousel.md +16 -11
- package/src/ui/docs/content/chat.md +3 -3
- package/src/ui/docs/content/checkbox.md +7 -7
- package/src/ui/docs/content/cli.md +5 -5
- package/src/ui/docs/content/collapsible.md +8 -8
- package/src/ui/docs/content/command.md +16 -8
- package/src/ui/docs/content/composer.md +2 -2
- package/src/ui/docs/content/content.md +44 -0
- package/src/ui/docs/content/copyable.md +4 -3
- package/src/ui/docs/content/customization.md +7 -7
- package/src/ui/docs/content/cycle.md +3 -3
- package/src/ui/docs/content/data-state.md +11 -12
- package/src/ui/docs/content/data.md +26 -33
- package/src/ui/docs/content/detail.md +8 -5
- package/src/ui/docs/content/dialog.md +339 -31
- package/src/ui/docs/content/dictionary-value.md +19 -18
- package/src/ui/docs/content/dock.md +3 -3
- package/src/ui/docs/content/dot.md +7 -7
- package/src/ui/docs/content/drawer.md +32 -16
- package/src/ui/docs/content/empty-value.md +2 -2
- package/src/ui/docs/content/empty.md +19 -12
- package/src/ui/docs/content/events.md +4 -4
- package/src/ui/docs/content/field.md +34 -12
- package/src/ui/docs/content/getting-started.md +1 -1
- package/src/ui/docs/content/icon-picker.md +8 -4
- package/src/ui/docs/content/input-otp.md +20 -12
- package/src/ui/docs/content/input.md +121 -9
- package/src/ui/docs/content/item.md +64 -24
- package/src/ui/docs/content/kbd.md +19 -11
- package/src/ui/docs/content/label.md +5 -3
- package/src/ui/docs/content/log.md +4 -4
- package/src/ui/docs/content/markdown.md +7 -6
- package/src/ui/docs/content/mcp.md +13 -15
- package/src/ui/docs/content/menu.md +36 -17
- package/src/ui/docs/content/metric-card.md +41 -0
- package/src/ui/docs/content/observability.md +2 -2
- package/src/ui/docs/content/page.md +93 -10
- package/src/ui/docs/content/pagination.md +22 -17
- package/src/ui/docs/content/popover.md +16 -8
- package/src/ui/docs/content/progress.md +7 -5
- package/src/ui/docs/content/queue.md +5 -5
- package/src/ui/docs/content/radio-group.md +20 -12
- package/src/ui/docs/content/router.md +11 -6
- package/src/ui/docs/content/scheduler.md +4 -5
- package/src/ui/docs/content/scroll-area.md +12 -7
- package/src/ui/docs/content/select.md +42 -29
- package/src/ui/docs/content/semantic-context.md +63 -0
- package/src/ui/docs/content/separator.md +5 -5
- package/src/ui/docs/content/sidebar.md +325 -56
- package/src/ui/docs/content/skeleton.md +5 -4
- package/src/ui/docs/content/slider.md +8 -7
- package/src/ui/docs/content/spinner.md +8 -8
- package/src/ui/docs/content/split.md +8 -5
- package/src/ui/docs/content/storage.md +6 -8
- package/src/ui/docs/content/switch.md +8 -7
- package/src/ui/docs/content/table.md +16 -6
- package/src/ui/docs/content/tabs.md +28 -14
- package/src/ui/docs/content/testing.md +9 -11
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +47 -13
- package/src/ui/docs/content/toggle.md +75 -7
- package/src/ui/docs/content/tokens.md +31 -3
- package/src/ui/docs/content/tooltip.md +19 -11
- package/src/ui/docs/content/truncate.md +7 -8
- package/src/ui/docs/content/ui.md +10 -9
- package/src/ui/docs/content/upgrading.md +7 -8
- package/src/ui/docs/doc-client.tsx +2 -2
- package/src/ui/docs/registry.tsx +580 -229
- package/src/ui/lib/semantic-context.ts +30 -0
- package/src/ui/meta.ts +278 -286
- package/src/ui/react.tsx +377 -111
- package/src/ui/theme.css +116 -0
- package/src/ui/components/primitives/alert-dialog.tsx +0 -190
- package/src/ui/docs/content/alert-dialog.md +0 -73
- package/src/ui/docs/content/button-group.md +0 -71
- package/src/ui/docs/content/confirm.md +0 -120
- package/src/ui/docs/content/input-group.md +0 -78
- package/src/ui/docs/content/toggle-group.md +0 -81
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Listagem em um modal
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use `ActionListDialog` para consultar e apresentar uma `ListAction` sem sair do contexto atual. A
|
|
4
|
+
lista carrega ao abrir, refaz a consulta quando `input` muda e acompanha invalidações declaradas por
|
|
5
|
+
outras actions. O consumidor compõe os itens por `children`; o modal mantém os estados e a barra de
|
|
6
|
+
ações.
|
|
4
7
|
|
|
5
8
|
```tsx preview col
|
|
6
9
|
const [open, setOpen] = useState(false)
|
|
@@ -38,9 +41,11 @@ render(
|
|
|
38
41
|
)
|
|
39
42
|
```
|
|
40
43
|
|
|
41
|
-
##
|
|
44
|
+
## Estado adicional do consumidor
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
Quando o modal depender de outra consulta, `loading` combina esse carregamento ao estado da lista.
|
|
47
|
+
Use `empty` para substituir a regra de vazio, por exemplo enquanto um formulário de criação ocupa o
|
|
48
|
+
corpo do modal.
|
|
44
49
|
|
|
45
50
|
```tsx
|
|
46
51
|
<ActionListDialog
|
|
@@ -52,9 +57,9 @@ Dois escape hatches, pros casos em que o modal agrega mais de uma fonte: `loadin
|
|
|
52
57
|
/>
|
|
53
58
|
```
|
|
54
59
|
|
|
55
|
-
##
|
|
60
|
+
## Propriedades de ActionListDialog
|
|
56
61
|
|
|
57
|
-
|
|
|
62
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
58
63
|
|---|---|---|---|
|
|
59
64
|
| `action / input` | `ListAction / TInput` | | O contrato e os filtros — mudou o input, re-busca; `invalidates` de forms/triggers refaz sozinho. |
|
|
60
65
|
| `open / onOpenChange` | `boolean / (open) => void` | | Controle do modal — de quem orquestra. |
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Listagem derivada do contrato
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use `ActionList` para apresentar uma coleção pesquisável descrita por uma `ListAction`. O contrato
|
|
4
|
+
define colunas, busca, filtros, período, ordenação e paginação; a interface materializa esses
|
|
5
|
+
recursos e envia o estado correspondente no input. Filtros avançados aparecem no modal “Filtros” e
|
|
6
|
+
os filtros aplicados permanecem visíveis como chips removíveis.
|
|
7
|
+
|
|
8
|
+
A barra se adapta ao espaço disponível. A busca cede largura primeiro e filtros que deixam de caber
|
|
9
|
+
migram para o modal; a linha só quebra quando nenhum controle restante puder ceder espaço.
|
|
4
10
|
|
|
5
11
|
```tsx preview col
|
|
6
12
|
render(
|
|
@@ -28,18 +34,16 @@ superfícies que não são listagens, como relatórios. O estado é controlado p
|
|
|
28
34
|
/>
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
somente quando declarados. Períodos incluem os presets do
|
|
35
|
-
contrato e o intervalo personalizado no mesmo calendário usado pela `ActionList`.
|
|
37
|
+
Seletores inline mantêm largura previsível (`w-40`; lookup e múltiplo usam `w-52`). Rótulos longos
|
|
38
|
+
são truncados no controle, mas permanecem completos na lista. No modal de filtros avançados, o
|
|
39
|
+
seletor ocupa toda a largura. Busca, filtros e período aparecem somente quando declarados.
|
|
36
40
|
|
|
37
41
|
## Colunas de dicionário
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
Quando o campo de saída usa `t.dict().zod()`, a coluna apresenta o valor com `DictionaryValue` e
|
|
44
|
+
respeita o papel declarado pelo dicionário. Classificações usam badge `outline`; status e estágio
|
|
45
|
+
usam badge tonal; `plain` e papéis ausentes permanecem como texto. Para um campo `string`, a coluna
|
|
46
|
+
pode indicar um dicionário registrado no provider:
|
|
43
47
|
`{ key: 'source', label: 'Fonte', dictionary: 'customerSource' }`. Dimensões independentes, como
|
|
44
48
|
tipo e estágio, ficam em colunas distintas — a leitura de comparação depende disso. `type:
|
|
45
49
|
'badge'` continua sendo o chip `outline` legado; com dicionário, ele mostra o rótulo em vez do
|
|
@@ -56,7 +60,8 @@ Renderer customizado reutiliza `EmptyValue`.
|
|
|
56
60
|
|
|
57
61
|
## Células custom (cells)
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
Use `cells` somente quando uma coluna precisar de apresentação própria, como link, composição ou
|
|
64
|
+
ação. A chave corresponde à `key` da coluna. Valores de dicionário não precisam desse override.
|
|
60
65
|
|
|
61
66
|
```tsx preview col
|
|
62
67
|
render(
|
|
@@ -78,9 +83,12 @@ render(
|
|
|
78
83
|
)
|
|
79
84
|
```
|
|
80
85
|
|
|
81
|
-
## Período
|
|
86
|
+
## Período obrigatório
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
Quando o contrato declara `periods`, a listagem sempre possui um recorte temporal. O preset marcado
|
|
89
|
+
com `default: true`, ou o primeiro da coleção, começa aplicado. Presets usam um valor relativo na
|
|
90
|
+
URL, como `?period=last7`; o padrão é omitido. Um intervalo personalizado usa diretamente as datas,
|
|
91
|
+
como `?period=2026-07-01..2026-07-07`, e chega ao handler pelos parâmetros `from` e `to`.
|
|
84
92
|
|
|
85
93
|
```tsx
|
|
86
94
|
periods: [
|
|
@@ -91,13 +99,20 @@ periods: [
|
|
|
91
99
|
// Presets computáveis: today · yesterday · last7 · last30 · thisMonth · lastMonth.
|
|
92
100
|
```
|
|
93
101
|
|
|
94
|
-
## Paginação e
|
|
102
|
+
## Paginação e carregamento
|
|
95
103
|
|
|
96
|
-
|
|
104
|
+
`ActionList` envia `limit`, definido por `pageSize`, e `page` no input. O handler aplica o recorte e
|
|
105
|
+
devolve `total` na resposta paginada. Quando existe total, o rodapé mostra a página atual, os links
|
|
106
|
+
disponíveis e a quantidade de itens. Alterar busca, filtro, ordenação ou período retorna à primeira
|
|
107
|
+
página. Durante a primeira consulta, `DataState` apresenta o carregamento; em atualizações
|
|
108
|
+
posteriores, a lista permanece visível com `aria-busy`.
|
|
97
109
|
|
|
98
|
-
##
|
|
110
|
+
## Ações em lote
|
|
99
111
|
|
|
100
|
-
`batch`
|
|
112
|
+
`batch` acrescenta seleção à tabela. `can(item)` determina quais linhas podem participar: itens
|
|
113
|
+
inelegíveis não são selecionáveis, “Selecionar todos” inclui somente os elegíveis e `run` recebe
|
|
114
|
+
apenas essa seleção. Com `confirm`, o componente pede confirmação antes de executar. Ao concluir, a
|
|
115
|
+
seleção é limpa e a consulta é refeita.
|
|
101
116
|
|
|
102
117
|
```tsx
|
|
103
118
|
<ActionList action={runList} input={{}}
|
|
@@ -110,9 +125,12 @@ O pattern manda `limit` (= `pageSize`, default 50) e `page` no input; o handler
|
|
|
110
125
|
/>
|
|
111
126
|
```
|
|
112
127
|
|
|
113
|
-
##
|
|
128
|
+
## Outras visualizações
|
|
114
129
|
|
|
115
|
-
|
|
130
|
+
Uma coleção também pode aparecer como quadro, galeria, lista ou calendário. Cada entrada de `views`
|
|
131
|
+
fornece um `render`; `ActionList` preserva a mesma fonte de dados, filtros e estado na URL. Declare
|
|
132
|
+
`icon` para controles somente com ícone; sem ele, o rótulo permanece visível. A visualização
|
|
133
|
+
“Tabela” aparece quando existem colunas.
|
|
116
134
|
|
|
117
135
|
```tsx preview col
|
|
118
136
|
render(
|
|
@@ -146,13 +164,16 @@ render(
|
|
|
146
164
|
)
|
|
147
165
|
```
|
|
148
166
|
|
|
149
|
-
## Filtros dependentes
|
|
167
|
+
## Filtros dependentes e opções remotas
|
|
150
168
|
|
|
151
|
-
|
|
169
|
+
`FilterSpec` também cobre dependências, dicionários e consultas remotas:
|
|
152
170
|
|
|
153
|
-
- **`depends: ['outroFiltro']
|
|
154
|
-
|
|
155
|
-
- **`options: { kind: '
|
|
171
|
+
- **`depends: ['outroFiltro']`:** desabilita o filtro até que suas dependências tenham valor e o
|
|
172
|
+
limpa quando uma delas muda.
|
|
173
|
+
- **`options: { kind: 'dictionary', ref: 'sessionStatus' }`:** resolve opções e rótulos por um
|
|
174
|
+
dicionário registrado no `TbdlibProvider`. `filterOptions` substitui essa fonte quando informado.
|
|
175
|
+
- **`options: { kind: 'lookup', source: 'x.lookup' }`:** consulta uma action com `{ q }` e os valores
|
|
176
|
+
das dependências. A resposta segue o formato `{ value, label }`.
|
|
156
177
|
|
|
157
178
|
```tsx
|
|
158
179
|
filters: {
|
|
@@ -166,9 +187,12 @@ filters: {
|
|
|
166
187
|
}
|
|
167
188
|
```
|
|
168
189
|
|
|
169
|
-
## Exibição e URL
|
|
190
|
+
## Exibição e estado na URL
|
|
170
191
|
|
|
171
|
-
O botão de exibição
|
|
192
|
+
O botão de exibição abre as preferências da listagem em qualquer visualização. Na tabela, permite
|
|
193
|
+
mostrar ou ocultar colunas, inclusive as declaradas com `hidden`; em todas as visualizações, permite
|
|
194
|
+
alterar a quantidade de itens por página. Busca, ordenação, filtros, visualização, colunas e limite
|
|
195
|
+
podem ser serializados na query string pelos helpers do componente:
|
|
172
196
|
|
|
173
197
|
```tsx
|
|
174
198
|
<ActionList
|
|
@@ -181,9 +205,10 @@ O botão de exibição (engrenagem) abre o popover de configuração da listagem
|
|
|
181
205
|
|
|
182
206
|
Convenção compacta (defaults omitidos): `?q=…&sort=chave:dir&status=…&view=board&cols=a,b,c&limit=25`.
|
|
183
207
|
|
|
184
|
-
## Composição
|
|
208
|
+
## Composição dos itens
|
|
185
209
|
|
|
186
|
-
|
|
210
|
+
Passe `children` para substituir a tabela por uma composição própria. A barra de filtros e os
|
|
211
|
+
estados de carregamento, erro e vazio continuam sendo tratados por `ActionList`.
|
|
187
212
|
|
|
188
213
|
```tsx preview col
|
|
189
214
|
render(
|
|
@@ -209,30 +234,30 @@ render(
|
|
|
209
234
|
)
|
|
210
235
|
```
|
|
211
236
|
|
|
212
|
-
##
|
|
237
|
+
## Declaração na ListAction
|
|
213
238
|
|
|
214
239
|
| Chave | O que declara |
|
|
215
240
|
|---|---|
|
|
216
241
|
| `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` fica fora (base do futuro column picker); `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
|
|
217
|
-
| `filters` | `{ [nome]: { label, type, options?, multiple?, advanced?, depends?, … } }` — a toolbar. `advanced` vai
|
|
242
|
+
| `filters` | `{ [nome]: { label, type, options?, multiple?, advanced?, depends?, … } }` — a toolbar. `advanced` vai para o modal; `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
|
|
218
243
|
| `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Com período/filtros no contrato ela fica à direita; sendo a ÚNICA forma de recorte, abre a linha. |
|
|
219
244
|
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
|
|
220
245
|
| `periods` | `{ value, label }[]` — o controle de período (presets + Personalizado com calendário); materializa em `from`/`to` no input. |
|
|
221
246
|
|
|
222
|
-
##
|
|
247
|
+
## Propriedades de ActionList
|
|
223
248
|
|
|
224
|
-
|
|
|
249
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
225
250
|
|---|---|---|---|
|
|
226
251
|
| `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item, paginate cursor). |
|
|
227
252
|
| `input` | `TInput` | | O ESCOPO BASE (ex.: { workspaceId }) — a toolbar soma por cima, nunca sobrescreve. |
|
|
228
253
|
| `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
|
|
229
|
-
| `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime
|
|
254
|
+
| `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para os filtros select/lookup (chave = nome do filtro). |
|
|
230
255
|
| `columns` | `ActionListColumn<TItem>[]` | | Tabela EXPLÍCITA — sobrepõe as colunas do contrato (escape hatch). |
|
|
231
|
-
| `children` | `(items, refetch) => ReactNode` | | Modo
|
|
256
|
+
| `children` | `(items, refetch) => ReactNode` | | Modo composição: layout livre; a toolbar segue. Tem precedência sobre columns. |
|
|
232
257
|
| `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm?, destructive? }` — liga a multi-seleção. |
|
|
233
|
-
| `rowId` | `(item) => string` | `item.id` | Identidade da linha
|
|
234
|
-
| `pageSize` | `number` | `50` | Itens por página
|
|
235
|
-
| `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado —
|
|
258
|
+
| `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
|
|
259
|
+
| `pageSize` | `number` | `50` | Itens por página padrão (vira `limit`/`page` no input; handler devolve `total`). O usuário troca no popover de exibição. |
|
|
260
|
+
| `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado — para quem embala sincronizar com a URL. |
|
|
236
261
|
| `emptyMessage` | `string` | `'Nenhum resultado.'` | O texto do estado vazio. |
|
|
237
|
-
| `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar
|
|
262
|
+
| `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
|
|
238
263
|
| `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita) — apresentação de quem chama, como `cells`; cliques ali não disparam o `onRowClick`. |
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Executar uma action
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Use `ActionTrigger` para executar uma `SimpleAction` a partir de um botão. Sem confirmação, o clique
|
|
4
|
+
inicia a operação, desabilita o controle durante a execução e apresenta as mensagens do contrato em
|
|
5
|
+
um toast. O rótulo padrão vem de `action.label`.
|
|
5
6
|
|
|
6
7
|
```tsx preview
|
|
7
8
|
<DocBrowserActionProvider>
|
|
@@ -11,8 +12,9 @@ Sem confirm, dispara no clique: desabilita enquanto roda e toca o toast do contr
|
|
|
11
12
|
|
|
12
13
|
## Confirmação declarada no contrato
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
Quando a action declara `confirm`, o componente monta o diálogo e aplica o contexto `danger` se a
|
|
16
|
+
operação for destrutiva. Use a propriedade `confirm` somente quando esta ocorrência precisar de uma
|
|
17
|
+
mensagem diferente da declarada no contrato.
|
|
16
18
|
|
|
17
19
|
```tsx preview
|
|
18
20
|
<DocBrowserActionProvider>
|
|
@@ -33,9 +35,11 @@ confirm: {
|
|
|
33
35
|
<ActionTrigger action={sessionDeleteContract} input={{ id: session.id }} />
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
##
|
|
38
|
+
## Ação somente com ícone
|
|
37
39
|
|
|
38
|
-
Com `icon`, o botão
|
|
40
|
+
Com `icon`, o botão exibe somente o ícone e usa `label`, ou `action.label`, no tooltip e no nome
|
|
41
|
+
acessível. O clique não aciona o item clicável ao redor. `itemLabel` identifica o registro na
|
|
42
|
+
mensagem de confirmação.
|
|
39
43
|
|
|
40
44
|
```tsx
|
|
41
45
|
<ActionTrigger
|
|
@@ -47,19 +51,22 @@ Com `icon`, o botão vira icon-only: o `label` (ou o `action.label`) migra pro t
|
|
|
47
51
|
/>
|
|
48
52
|
```
|
|
49
53
|
|
|
50
|
-
>
|
|
54
|
+
> `ActionTrigger` substitui o antigo `DeleteButton`. Ao migrar, declare `confirm` no contrato para
|
|
55
|
+
> preservar a confirmação que o componente anterior sempre apresentava.
|
|
51
56
|
|
|
52
57
|
## Erro que a pessoa entende
|
|
53
58
|
|
|
54
|
-
Quando a action falha,
|
|
59
|
+
Quando a action falha, uma mensagem do servidor substitui o texto genérico somente nos erros de
|
|
60
|
+
negócio `conflict`, `validation` e `not_found`:
|
|
55
61
|
|
|
56
62
|
> Este agente tem 3 conversas — desabilite em vez de excluir.
|
|
57
63
|
|
|
58
|
-
|
|
64
|
+
Erros inesperados mantêm a mensagem genérica do contrato, evitando expor detalhes de banco ou
|
|
65
|
+
infraestrutura. O servidor registra o detalhe técnico para diagnóstico.
|
|
59
66
|
|
|
60
|
-
##
|
|
67
|
+
## Propriedades de ActionTrigger
|
|
61
68
|
|
|
62
|
-
|
|
|
69
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
63
70
|
|---|---|---|---|
|
|
64
71
|
| `action` | `SimpleContract<TInput, TData>` | | A SimpleAction do Opus — label, messages e confirm vêm do contrato. |
|
|
65
72
|
| `input` | `TInput` | | O que a action recebe — geralmente { id }. |
|
|
@@ -67,6 +74,6 @@ Isso é acionável. Erro inesperado carrega texto técnico cru: sem a allowlist,
|
|
|
67
74
|
| `variant / size` | `variants do Button` | `'default' (ou 'destructive' se o confirm declarar) / 'default'` | Visual do botão — o destructive do ConfirmSpec já escolhe sozinho. |
|
|
68
75
|
| `confirm` | `{ title, description?, actionLabel?, cancelLabel? }` | | Confirmação via prop — sobrepõe o ConfirmSpec do contrato. |
|
|
69
76
|
| `onSuccess` | `(data: TData) => void` | | Pós-sucesso (cache já invalidado pelo action.invalidates). |
|
|
70
|
-
| `icon` | `React.ReactNode` | | Torna o botão icon-only: rótulo no tooltip e no `aria-label`, clique que não vaza
|
|
77
|
+
| `icon` | `React.ReactNode` | | Torna o botão icon-only: rótulo no tooltip e no `aria-label`, clique que não vaza para o item. |
|
|
71
78
|
| `itemLabel` | `string` | | Nome do alvo na pergunta (sai entre aspas, em destaque, antes da mensagem do contrato). |
|
|
72
|
-
| `className` | `string` | | Classes do botão (ex.: apertar o tamanho
|
|
79
|
+
| `className` | `string` | | Classes do botão (ex.: apertar o tamanho em uma linha densa). |
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Carregar um recurso
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use `ActionView` para carregar um recurso por uma `ViewAction` e manter carregamento, erro e vazio no
|
|
4
|
+
mesmo fluxo. No exemplo, alterne entre os workspaces para observar o carregamento e a recuperação de
|
|
5
|
+
erro. O consumidor compõe somente o conteúdo disponível.
|
|
6
6
|
|
|
7
7
|
```tsx preview col
|
|
8
8
|
const [id, setId] = useState('empresa-x')
|
|
@@ -35,13 +35,13 @@ render(
|
|
|
35
35
|
)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Propriedades de ActionView
|
|
39
39
|
|
|
40
|
-
|
|
|
40
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
41
41
|
|---|---|---|---|
|
|
42
42
|
| `action` | `ViewAction<TInput, TData>` | | A ViewAction do Opus (kind view, 1 recurso). |
|
|
43
43
|
| `input` | `TInput` | | Geralmente { id } — mudou, recarrega. |
|
|
44
|
-
| `children` | `(data: TData, refetch) => ReactNode` | |
|
|
45
|
-
| `render` | `(data: TData, refetch) => ReactNode` | | Alias de
|
|
44
|
+
| `children` | `(data: TData, refetch) => ReactNode` | | Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
|
|
45
|
+
| `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. |
|
|
46
46
|
| `loading / empty` | `ReactNode` | `3 skeletons / nada` | Sobrescreve os estados padrão quando o contexto pedir. |
|
|
47
47
|
| `error` | `(err, retry) => ReactNode` | `mensagem + Tentar de novo` | Sobrescreve o estado de erro padrão. |
|
|
@@ -10,7 +10,7 @@ o handler e a web usa a mesma definição para executar ou renderizar a operaç
|
|
|
10
10
|
|
|
11
11
|
## Contrato primeiro
|
|
12
12
|
|
|
13
|
-
> `defineContract` no `shared/` — entidades, schemas (zod + `t.*`
|
|
13
|
+
> `defineContract` no `shared/` — entidades, schemas (zod + `t.*` para tipos lógicos) e os
|
|
14
14
|
> metadados da action. `api` e `web` importam; nada se duplica.
|
|
15
15
|
|
|
16
16
|
```ts
|
|
@@ -64,18 +64,18 @@ name → kind → (label / summary / messages / tags…)
|
|
|
64
64
|
→ invalidates
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
Fail-closed por padrão: `input`/`output` via `z.object` + `t.*`
|
|
67
|
+
Fail-closed por padrão: `input`/`output` via `z.object` + `t.*` para os tipos lógicos; `authorize`
|
|
68
68
|
declarado ou gate explícito do adapter.
|
|
69
69
|
|
|
70
|
-
Os átomos do catálogo servem
|
|
70
|
+
Os átomos do catálogo servem também dentro dos schemas de input/output — não re-escreva
|
|
71
71
|
regex do que o Opus já valida: `z.object({ tag: t.slug().zod(), contato: t.email().zod() })`.
|
|
72
72
|
O `.zod()` devolve o schema zod subjacente do tipo lógico (slug, email, phone, money…).
|
|
73
73
|
|
|
74
74
|
## Modo design — o mock mora no contrato
|
|
75
75
|
|
|
76
76
|
Um `mockHandler` no contrato deixa a UI rodar com dado realista **sem backend nem banco**: em
|
|
77
|
-
`OPUS_MODE=design` o runtime roteia o `execute`
|
|
78
|
-
ausente). Como ele fica no
|
|
77
|
+
`OPUS_MODE=design` o runtime roteia o `execute` para o `mockHandler` (cai no `handler` real se
|
|
78
|
+
ausente). Como ele fica no contrato, e não no `bindAction`, é isomorfo — o design autora, o
|
|
79
79
|
handoff pluga o handler, e o mock nem toca `ctx.db`.
|
|
80
80
|
|
|
81
81
|
Alimente com `fake`/`fakeMany` (fixtures determinísticas do próprio schema — ver [Testes](testing)):
|
|
@@ -92,15 +92,15 @@ export const eventGet = defineContract({
|
|
|
92
92
|
})
|
|
93
93
|
// listas: `mockHandler: () => fakeMany(EventEntity.zod(), 20)`
|
|
94
94
|
|
|
95
|
-
// o handoff, depois, só pluga o real —
|
|
95
|
+
// o handoff, depois, só pluga o real — mesmo contrato:
|
|
96
96
|
export const eventGetImpl = bindAction(eventGet, { handler: async (ctx, input) => { /* db */ } })
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
O `mockHandler` roda no **servidor em modo design** (`OPUS_MODE=design`), não no cliente — e
|
|
100
100
|
quem sobe esse servidor é o próprio dev server: o plugin `opusDesign()` (`@softize/opus/vite`)
|
|
101
101
|
monta o runtime Opus DENTRO do vite e serve `/api` in-process, com os mocks respondendo e
|
|
102
|
-
qualquer `server.proxy`
|
|
103
|
-
503 em envelope — nada vaza
|
|
102
|
+
qualquer `server.proxy` para backend externo desligado (prefixo ex-proxy sem cobertura responde
|
|
103
|
+
503 em envelope — nada vaza para prod). A SPA chama `/api` normal e recebe o dado fake, isolado:
|
|
104
104
|
|
|
105
105
|
```ts
|
|
106
106
|
// vite.config.ts
|
|
@@ -118,7 +118,7 @@ persistindo, o envelope traz a mensagem-guia apontando o entry.
|
|
|
118
118
|
|
|
119
119
|
Como o contrato é isomorfo, o `mockHandler` também acompanha o **bundle web** — peso morto lá
|
|
120
120
|
(a SPA nunca o chama; o dado é fake). Tirá-lo do bundle de prod é um transform de build
|
|
121
|
-
(deferido); **não** dá
|
|
121
|
+
(deferido); **não** dá para gatear no call-site com `import.meta.env` sem quebrar o servidor
|
|
122
122
|
(não existe em Node). É a materialização Opus-nativa do "mock = contrato": o design não é
|
|
123
123
|
descartável — vira o contrato que o backend honra.
|
|
124
124
|
|
|
@@ -64,10 +64,10 @@ handler: async (ctx, input) => {
|
|
|
64
64
|
Marque uma action com `ai: { enabled: true }` no contrato e ela vira uma **tool** que o
|
|
65
65
|
modelo pode chamar. Aí `ctx.ai.run(prompt)` roda o loop agêntico sobre as actions `ai:enabled`:
|
|
66
66
|
o modelo escolhe a tool → o runtime executa a action **como o usuário logado** (limitado pelo
|
|
67
|
-
`ctx.can`) → o resultado volta
|
|
67
|
+
`ctx.can`) → o resultado volta para o modelo → repete até a resposta em texto.
|
|
68
68
|
|
|
69
69
|
```ts
|
|
70
|
-
// A action opta por entrar
|
|
70
|
+
// A action opta por entrar; o schema do contrato também descreve a ferramenta:
|
|
71
71
|
export const buscarNotas = defineContract({
|
|
72
72
|
name: 'nota.buscar',
|
|
73
73
|
kind: 'list',
|
|
@@ -76,7 +76,7 @@ export const buscarNotas = defineContract({
|
|
|
76
76
|
ai: { enabled: true, description: 'Busca notas por cliente e mês.' },
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
// No handler — ou fora dele, via runtime.aiFor(base),
|
|
79
|
+
// No handler — ou fora dele, via runtime.aiFor(base), para o backend de um chat:
|
|
80
80
|
const { text } = await ctx.ai!.run('Quantas notas a Empresa X emitiu em junho?')
|
|
81
81
|
// o modelo chamou nota.buscar sozinho, como o usuário logado, e respondeu em texto.
|
|
82
82
|
```
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
## Forma curta
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use a forma curta quando o aviso tiver ícone, título e uma frase. Declare `title`, `description` e
|
|
4
|
+
`icon`; o componente monta os slots e anuncia o conteúdo com `role="alert"`.
|
|
4
5
|
|
|
5
6
|
```tsx preview col
|
|
6
7
|
<Alert icon={<Info />} title="Opus 2.8.0" description="Este workspace usa a versão pinada em opus.json." />
|
|
7
8
|
<Alert
|
|
8
|
-
|
|
9
|
+
context="danger"
|
|
9
10
|
icon={<CircleAlert />}
|
|
10
11
|
title="Sessão encerrada"
|
|
11
12
|
description="O agente parou antes de concluir. Veja o detalhe no log da sessão."
|
|
12
13
|
/>
|
|
13
14
|
<Alert
|
|
14
|
-
|
|
15
|
+
context="success"
|
|
15
16
|
icon={<CircleCheck />}
|
|
16
17
|
title="Skill publicada"
|
|
17
18
|
description="Os agentes do workspace já enxergam a nova versão."
|
|
18
19
|
/>
|
|
19
20
|
<Alert
|
|
20
|
-
|
|
21
|
+
context="warning"
|
|
21
22
|
icon={<CircleAlert />}
|
|
22
23
|
title="Revisão necessária"
|
|
23
24
|
description="Confira os dados antes de continuar."
|
|
@@ -26,51 +27,76 @@ Quase todo alert é ícone + título + uma frase — então isso é UMA linha: `
|
|
|
26
27
|
|
|
27
28
|
## Só a frase
|
|
28
29
|
|
|
29
|
-
Título é opcional — o aviso de uma linha dispensa.
|
|
30
|
+
Título é opcional — o aviso de uma linha dispensa. O contexto continua visível por superfície,
|
|
31
|
+
borda e texto; o conteúdo comunica o significado sem depender somente da cor.
|
|
30
32
|
|
|
31
33
|
```tsx preview col
|
|
32
34
|
<Alert description="Nenhuma sessão aberta neste repositório." />
|
|
33
|
-
<Alert
|
|
35
|
+
<Alert context="success" icon={<CircleCheck />} description="Workspace Empresa X sincronizado." />
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
##
|
|
38
|
+
## Mídia é opcional
|
|
37
39
|
|
|
38
|
-
Sem `icon` o alert
|
|
40
|
+
Sem `icon` o alert mantém somente a coluna de texto. Com ele, a forma curta materializa
|
|
41
|
+
`AlertMedia` à esquerda e `AlertHeader` à direita. A mídia mantém uma moldura quadrada de tamanho
|
|
42
|
+
estável, mesmo quando o título ou a descrição ocupam mais linhas. Texto solto como filho também
|
|
43
|
+
vale (`<Alert>Sincronizado.</Alert>`) e se torna uma descrição.
|
|
39
44
|
|
|
40
45
|
```tsx preview col
|
|
41
46
|
<Alert title="Sem provider próprio" description="As conversas usam o padrão do sistema." />
|
|
42
|
-
<Alert
|
|
47
|
+
<Alert context="success">Workspace Empresa X sincronizado.</Alert>
|
|
43
48
|
```
|
|
44
49
|
|
|
45
50
|
## Composição (conteúdo rico)
|
|
46
51
|
|
|
47
|
-
Quando a
|
|
52
|
+
Quando a mensagem precisa de conteúdo rico, componha os slots da família. `AlertMedia`,
|
|
53
|
+
`AlertHeader` e `AlertActions` são filhos diretos de `Alert`; `AlertTitle` e
|
|
54
|
+
`AlertDescription` pertencem ao header. As ações ficam no fim lógico da superfície e centralizadas
|
|
55
|
+
verticalmente na mesma linha do conteúdo — a mesma posição usada pelas ações do Toast.
|
|
48
56
|
|
|
49
57
|
```tsx preview col
|
|
50
|
-
<Alert
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
<Alert context="danger">
|
|
59
|
+
<AlertMedia>
|
|
60
|
+
<CircleAlert />
|
|
61
|
+
</AlertMedia>
|
|
62
|
+
<AlertHeader>
|
|
63
|
+
<AlertTitle>Não deu para publicar</AlertTitle>
|
|
64
|
+
<AlertDescription>
|
|
65
|
+
<p>O registry recusou a versão 3.0.0 — ela já existe.</p>
|
|
66
|
+
<p>Suba o patch e tente de novo.</p>
|
|
67
|
+
</AlertDescription>
|
|
68
|
+
</AlertHeader>
|
|
69
|
+
<AlertActions>
|
|
70
|
+
<Button size="sm">
|
|
71
|
+
Tentar novamente
|
|
72
|
+
</Button>
|
|
73
|
+
</AlertActions>
|
|
56
74
|
</Alert>
|
|
57
75
|
```
|
|
58
76
|
|
|
59
|
-
Os dois modos convivem: com `title
|
|
77
|
+
Os dois modos convivem: com `title` ou `description`, `children` entra depois da mensagem e recebe a
|
|
78
|
+
ação.
|
|
60
79
|
|
|
61
80
|
```tsx preview col
|
|
62
|
-
<Alert
|
|
63
|
-
<
|
|
81
|
+
<Alert
|
|
82
|
+
icon={<CircleAlert />}
|
|
83
|
+
title="Sessão presa"
|
|
84
|
+
description="O ambiente não subiu no tempo esperado."
|
|
85
|
+
>
|
|
86
|
+
<Button size="sm">
|
|
87
|
+
Reiniciar
|
|
88
|
+
</Button>
|
|
64
89
|
</Alert>
|
|
65
90
|
```
|
|
66
91
|
|
|
67
|
-
##
|
|
92
|
+
## Propriedades de Alert
|
|
68
93
|
|
|
69
|
-
|
|
|
70
|
-
|
|
71
|
-
| `title`
|
|
72
|
-
| `description` | `React.ReactNode`
|
|
73
|
-
| `icon`
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
94
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
95
|
+
| ------------- | ----------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
96
|
+
| `title` | `React.ReactNode` | | Título do alert (a forma curta). Não é o atributo `title` do HTML — esse é tooltip nativo, banido na casa, e o componente não o aceita. |
|
|
97
|
+
| `description` | `React.ReactNode` | | A frase. Sozinha, dispensa título. |
|
|
98
|
+
| `icon` | `React.ReactNode` | | Ícone à esquerda; materializa `AlertMedia` com realce tonal. Decorativo — quem nomeia é o título. |
|
|
99
|
+
| `context` | `'neutral' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'neutral'` | O significado da mensagem e do realce do ícone. |
|
|
100
|
+
| `variant` | `'subtle' \| 'outline'` | `'subtle'` | O tratamento visual aplicado ao contexto. |
|
|
101
|
+
| `children` | `React.ReactNode` | | Composição explícita, texto cru ou, junto da forma curta, ações exibidas após a mensagem. |
|
|
102
|
+
| `style` | `React.CSSProperties` | | Ajuste excepcional da raiz; medidas escaláveis usam `rem`. |
|
|
@@ -15,7 +15,7 @@ contêiner (o pai): a altura o componente deriva sozinho.
|
|
|
15
15
|
|
|
16
16
|
## Quadrado (1/1)
|
|
17
17
|
|
|
18
|
-
ratio={1} trava
|
|
18
|
+
ratio={1} trava em um quadrado — o formato dos avatares de agente e dos ícones de skill, onde a
|
|
19
19
|
moldura precisa ser previsível.
|
|
20
20
|
|
|
21
21
|
```tsx preview col-start
|
|
@@ -57,10 +57,10 @@ layout quando o preview carrega.
|
|
|
57
57
|
</div>
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
##
|
|
60
|
+
## Propriedades de AspectRatio
|
|
61
61
|
|
|
62
|
-
|
|
|
62
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
63
63
|
|---|---|---|---|
|
|
64
|
-
| `ratio` | `number` | `1` | A razão largura/altura. 16/9
|
|
64
|
+
| `ratio` | `number` | `1` | A razão largura/altura. 16/9 para vídeo/preview, 1 para quadrado, 4/3 para clássico. |
|
|
65
65
|
| `children` | `React.ReactNode` | | O conteúdo a enquadrar (img, iframe, div) — preencha com h-full w-full e object-cover. |
|
|
66
66
|
| `className` | `string` | | Estilo do bloco — borda, rounded e overflow-hidden moram aqui, não no filho. |
|
|
@@ -5,7 +5,7 @@ title: Auditoria
|
|
|
5
5
|
# Auditoria
|
|
6
6
|
|
|
7
7
|
Toda action executada vira um registro imutável: quem, o quê, quando, com que entrada e
|
|
8
|
-
resultado. O runtime emite um `AuditRecord` por execução
|
|
8
|
+
resultado. O runtime emite um `AuditRecord` por execução para o(s) sink(s) configurado(s) — de
|
|
9
9
|
graça, sem o handler pedir. O contrato é o `AuditSink`.
|
|
10
10
|
|
|
11
11
|
## O contrato
|
|
@@ -43,7 +43,7 @@ interface AuditRecord {
|
|
|
43
43
|
import { consoleAudit } from '@softize/opus/audit/console'
|
|
44
44
|
import { pgAudit } from '@softize/opus/audit/pg'
|
|
45
45
|
|
|
46
|
-
// Dev: uma linha por action (pretty
|
|
46
|
+
// Dev: uma linha por action (pretty em um TTY, json senão).
|
|
47
47
|
const dev = consoleAudit()
|
|
48
48
|
|
|
49
49
|
// Produção: grava na tabela audit_log (Postgres).
|