@softize/opus 18.1.0 → 18.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/PROMOTED.md +4 -5
- package/README.md +5 -4
- package/bin/cli.mjs +4 -0
- package/docs/adr/0004-page-content-state-is-composed.md +3 -0
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
- package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
- package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
- package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
- package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
- package/docs/adr/{0016-productive-surfaces-use-compact-density.md → 0019-productive-surfaces-use-compact-density.md} +4 -1
- package/docs/code-style.md +2 -2
- package/docs/consumer-upgrade-propagation.md +1 -1
- package/docs/data-products.md +5 -3
- package/docs/protocol.md +6 -6
- package/docs/releasing.md +28 -4
- package/package.json +1 -1
- package/registry/skills/build-opus-ui/references/ui-patterns.md +11 -1
- package/src/auth/drivers/jwt.ts +2 -1
- package/src/core/runtime.ts +25 -4
- package/src/core/types.ts +4 -4
- package/src/mcp/index.ts +9 -0
- package/src/ui/components/patterns/content-header.tsx +1 -1
- package/src/ui/components/patterns/form.tsx +1 -1
- package/src/ui/components/patterns/sidebar.tsx +1 -1
- package/src/ui/components/primitives/card.tsx +1 -1
- package/src/ui/components/primitives/detail.tsx +7 -7
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/select.tsx +1 -1
- package/src/ui/docs/content/action-form-dialog.md +11 -4
- package/src/ui/docs/content/action-form.md +7 -16
- package/src/ui/docs/content/action-list-dialog.md +4 -6
- package/src/ui/docs/content/action-list.md +46 -4
- package/src/ui/docs/content/action-trigger.md +9 -5
- package/src/ui/docs/content/action-view.md +12 -8
- package/src/ui/docs/content/actions.md +36 -13
- package/src/ui/docs/content/ai.md +26 -7
- package/src/ui/docs/content/alert.md +4 -3
- package/src/ui/docs/content/aspect-ratio.md +2 -2
- package/src/ui/docs/content/auth.md +25 -10
- package/src/ui/docs/content/avatar.md +1 -1
- package/src/ui/docs/content/badge.md +2 -2
- package/src/ui/docs/content/breadcrumb.md +3 -2
- package/src/ui/docs/content/button.md +31 -7
- package/src/ui/docs/content/calendar.md +1 -1
- package/src/ui/docs/content/card.md +1 -1
- package/src/ui/docs/content/carousel.md +14 -3
- package/src/ui/docs/content/chat.md +1 -1
- package/src/ui/docs/content/cli.md +13 -7
- package/src/ui/docs/content/command.md +34 -2
- package/src/ui/docs/content/composer.md +1 -1
- package/src/ui/docs/content/content.md +5 -4
- package/src/ui/docs/content/customization.md +1 -1
- package/src/ui/docs/content/cycle.md +7 -5
- package/src/ui/docs/content/data-state.md +6 -5
- package/src/ui/docs/content/data.md +3 -3
- package/src/ui/docs/content/detail.md +3 -2
- package/src/ui/docs/content/dialog.md +2 -2
- package/src/ui/docs/content/dictionary-value.md +1 -1
- package/src/ui/docs/content/dock.md +23 -2
- package/src/ui/docs/content/dot.md +0 -2
- package/src/ui/docs/content/drawer.md +1 -1
- package/src/ui/docs/content/empty.md +1 -4
- package/src/ui/docs/content/events.md +1 -1
- package/src/ui/docs/content/field.md +20 -11
- package/src/ui/docs/content/getting-started.md +4 -2
- package/src/ui/docs/content/icon-picker.md +2 -2
- package/src/ui/docs/content/input-otp.md +2 -0
- package/src/ui/docs/content/input.md +2 -3
- package/src/ui/docs/content/item.md +5 -4
- package/src/ui/docs/content/kbd.md +2 -1
- package/src/ui/docs/content/mcp.md +10 -4
- package/src/ui/docs/content/menu.md +27 -0
- package/src/ui/docs/content/page.md +19 -5
- package/src/ui/docs/content/pagination.md +9 -2
- package/src/ui/docs/content/popover.md +2 -2
- package/src/ui/docs/content/presentation.md +46 -45
- package/src/ui/docs/content/progress.md +2 -6
- package/src/ui/docs/content/runtime.md +8 -5
- package/src/ui/docs/content/scheduler.md +1 -1
- package/src/ui/docs/content/select.md +13 -8
- package/src/ui/docs/content/sidebar.md +3 -2
- package/src/ui/docs/content/skeleton.md +1 -1
- package/src/ui/docs/content/slider.md +4 -4
- package/src/ui/docs/content/spinner.md +3 -3
- package/src/ui/docs/content/tabs.md +6 -6
- package/src/ui/docs/content/testing.md +4 -2
- package/src/ui/docs/content/toast.md +3 -5
- package/src/ui/docs/content/toggle.md +37 -0
- package/src/ui/docs/content/tooltip.md +4 -3
- package/src/ui/docs/content/truncate.md +3 -2
- package/src/ui/docs/content/ui.md +3 -1
- package/src/ui/docs/content/upgrading.md +43 -13
- package/src/ui/docs/doc-client.tsx +1 -1
- package/src/ui/docs/registry.tsx +30 -5
- package/src/ui/meta.ts +4 -4
|
@@ -92,7 +92,7 @@ interface SelectCustomBase extends SelectBaseProps {
|
|
|
92
92
|
clearable?: boolean
|
|
93
93
|
/** `outline`: compacto com borda; `ghost`: compacto sem borda nem fundo. */
|
|
94
94
|
variant?: 'default' | 'outline' | 'ghost'
|
|
95
|
-
/** Placeholder
|
|
95
|
+
/** Placeholder da linha de busca dentro da lista. Default: `'Buscar…'`. */
|
|
96
96
|
searchPlaceholder?: string
|
|
97
97
|
emptyText?: string
|
|
98
98
|
/** Ação custom no FIM do campo, dentro do controle (antes do chevron) — ex.: um botão que
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
Use `ActionFormDialog` quando o formulário precisar interromper o fluxo atual sem levar a pessoa
|
|
4
4
|
para outra página. O consumidor controla `open`; depois de uma execução bem-sucedida, o componente
|
|
5
5
|
fecha o modal. O cabeçalho e o rodapé permanecem visíveis enquanto os campos podem rolar.
|
|
6
|
-
|
|
7
|
-
a ação principal permanece `solid`, ambas no tamanho normal de uma decisão
|
|
6
|
+
O rodapé agrupa as decisões em um `ButtonGroup` cuja largura acompanha o conteúdo: `Cancelar`
|
|
7
|
+
aparece em `ghost` e a ação principal permanece `solid`, ambas no tamanho normal de uma decisão
|
|
8
|
+
modal. `Cancelar` está sempre presente; sem `onCancel` próprio, ele fecha o modal.
|
|
9
|
+
Com `footerDistribution="equal"`, as duas decisões dividem a faixa e `Cancelar` passa a `outline`,
|
|
10
|
+
salvo escolha explícita em `cancelVariant`.
|
|
8
11
|
|
|
9
12
|
```tsx preview
|
|
10
13
|
const [open, setOpen] = useState(false);
|
|
@@ -26,8 +29,12 @@ render(
|
|
|
26
29
|
|
|
27
30
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
28
31
|
| --------------------- | ------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
-
| `open / onOpenChange` | `boolean / (open: boolean) => void` | | Estado controlado do modal. `onOpenChange(false)` é chamado no sucesso e ao cancelar. |
|
|
32
|
+
| `open / onOpenChange` | `boolean / (open: boolean) => void` | | Estado controlado do modal. `onOpenChange(false)` é chamado no sucesso e, quando não há `onCancel` próprio, ao cancelar. |
|
|
30
33
|
| `title` | `string` | | Nome da tarefa exibido no cabeçalho. |
|
|
31
34
|
| `intro` | `ReactNode` | | Contexto relevante no início do corpo, como texto ou `Alert`. |
|
|
32
35
|
| `submitLabel` | `string` | `'Salvar'` | Resultado da ação principal. Preserve o padrão em criação e edição comuns; sobrescreva apenas quando a operação tiver outro efeito, como `Renomear` ou `Criar nova versão`. |
|
|
33
|
-
|
|
|
36
|
+
| `footerDistribution` | `'content' \| 'equal'` | `'content'` | Mantém a largura das ações pelo conteúdo ou divide a faixa igualmente. |
|
|
37
|
+
| `cancelVariant` | `'ghost' \| 'outline'` | `'ghost'`; `'outline'` com `equal` | Tratamento do `Cancelar`. O valor explícito vence o default derivado de `footerDistribution`. |
|
|
38
|
+
| `onCancel` | `() => void` | fecha o modal | Substitui o fechamento padrão do `Cancelar`; nesse caso, o consumidor decide quando chamar `onOpenChange(false)`. |
|
|
39
|
+
| `onSuccess` | `(data: TData) => void` | | Executado depois do sucesso, antes de o componente fechar o modal. |
|
|
40
|
+
| `…ActionFormProps` | `action, defaultValues, fieldOptions…` | | Demais propriedades repassadas ao `ActionForm` interno. `className`, `body` e `footer` não são repassadas: o wrapper as substitui para montar o corpo rolável e o footer do modal. |
|
|
@@ -34,7 +34,8 @@ invalidação continuam sob responsabilidade de `ActionForm`.
|
|
|
34
34
|
|
|
35
35
|
## Valores iniciais e rótulos
|
|
36
36
|
|
|
37
|
-
`defaultValues` preenche o formulário para edição.
|
|
37
|
+
`defaultValues` preenche o formulário para edição. Criação e edição comuns mantêm o rótulo padrão
|
|
38
|
+
`Salvar`; use `submitLabel` somente quando a operação pedir outro verbo, como `Renomear`.
|
|
38
39
|
`onCancel` devolve ao consumidor a decisão de fechar um painel ou navegar para outra página.
|
|
39
40
|
|
|
40
41
|
```tsx preview col md
|
|
@@ -42,7 +43,6 @@ invalidação continuam sob responsabilidade de `ActionForm`.
|
|
|
42
43
|
<ActionForm
|
|
43
44
|
action={docWorkspaceCreate}
|
|
44
45
|
defaultValues={{ name: "Empresa X", status: "active" }}
|
|
45
|
-
submitLabel="Atualizar"
|
|
46
46
|
onCancel={() => undefined}
|
|
47
47
|
/>
|
|
48
48
|
</DocBrowserActionProvider>
|
|
@@ -50,10 +50,11 @@ invalidação continuam sob responsabilidade de `ActionForm`.
|
|
|
50
50
|
|
|
51
51
|
## Formulário modal
|
|
52
52
|
|
|
53
|
-
`ActionFormDialog` acrescenta a moldura, o corpo rolável e o footer ao formulário. Por padrão, as
|
|
53
|
+
[`ActionFormDialog`](/ui/action-form-dialog) acrescenta a moldura, o corpo rolável e o footer ao formulário. Por padrão, as
|
|
54
54
|
ações ficam alinhadas no fim da faixa e preservam a largura do conteúdo; `Cancelar` usa `ghost`.
|
|
55
55
|
Use `footerDistribution="equal"` somente quando as duas decisões precisarem do mesmo peso visual.
|
|
56
56
|
Nesse caso, o cancelamento muda para `outline`, salvo escolha explícita em `cancelVariant`.
|
|
57
|
+
As propriedades do wrapper estão documentadas na página dele.
|
|
57
58
|
|
|
58
59
|
## Pré-requisitos
|
|
59
60
|
|
|
@@ -82,7 +83,9 @@ ciclo de vida que o pattern não cobre; validação e execução continuam iguai
|
|
|
82
83
|
| `action` | `FormContract<TInput, TData>` | | O contrato da FormAction — dele saem campos (input Zod + fields), mensagens e invalidação de cache. |
|
|
83
84
|
| `defaultValues` | `Partial<TInput>` | | Valores iniciais — o modo edição de um update/patch. |
|
|
84
85
|
| `onSuccess` | `(data: TData) => void` | | Pós-sucesso (o toast e a invalidação de cache já aconteceram). |
|
|
85
|
-
| `submitLabel / cancelLabel / cancelVariant / onCancel` | `string / string / 'ghost' \| 'outline' / () => void` | `'Salvar' / 'Cancelar' / 'ghost'` | Rodapé do form — o Cancelar só aparece com onCancel.
|
|
86
|
+
| `submitLabel / cancelLabel / cancelVariant / onCancel` | `string / string / 'ghost' \| 'outline' / () => void` | `'Salvar' / 'Cancelar' / 'ghost'` | Rodapé do form — o Cancelar só aparece com onCancel. Fora de um modal, mantenha `ghost`; o `ActionFormDialog` usa `outline` somente com `footerDistribution="equal"`. |
|
|
87
|
+
| `disabled` | `boolean` | `false` | Bloqueia campos e ações sem desmontar o formulário. |
|
|
88
|
+
| `onLoadingChange` | `(loading: boolean) => void` | | Informa início e fim da execução a quem coordena ações como um grupo. |
|
|
86
89
|
| `fieldOptions` | `Record<string, SelectOption[]>` | | Opções de runtime por campo (ex.: ids de skills) — sobrepõe as inferidas do z.enum. |
|
|
87
90
|
| `className / body / footer` | `string / (node) => node / (node) => node` | | className = classes do `<form>`. body/footer = slots: recebem os campos / os botões e escolhem o invólucro — é como o ActionFormDialog injeta DialogBody/DialogFooter (scroll + faixa). |
|
|
88
91
|
| `children` | `ReactNode` | | Modo composição: diagrame com `<ActionFormField name />`. Sem children, o automático monta todos os campos na ordem do contrato. |
|
|
@@ -95,18 +98,6 @@ ciclo de vida que o pattern não cobre; validação e execução continuam iguai
|
|
|
95
98
|
| `options` | `SelectOption[]` | Opções por id de runtime — sobrepõe o fieldOptions do form e o z.enum. |
|
|
96
99
|
| `className` | `string` | Classes do invólucro (ex.: `col-span-2` em uma grid). |
|
|
97
100
|
|
|
98
|
-
## Propriedades de ActionFormDialog
|
|
99
|
-
|
|
100
|
-
Além das propriedades de `ActionForm`, o wrapper modal aceita:
|
|
101
|
-
|
|
102
|
-
| Propriedade | Tipo | Padrão | Descrição |
|
|
103
|
-
|---|---|---|---|
|
|
104
|
-
| `open` | `boolean` | | Estado visível do diálogo. |
|
|
105
|
-
| `onOpenChange` | `(open: boolean) => void` | | Recebe abertura e fechamento. |
|
|
106
|
-
| `title` | `string` | | Nomeia a tarefa modal. |
|
|
107
|
-
| `intro` | `ReactNode` | | Contexto relevante apresentado antes dos campos. |
|
|
108
|
-
| `footerDistribution` | `'content' \| 'equal'` | `'content'` | Mantém a largura das ações pelo conteúdo ou divide a faixa igualmente. |
|
|
109
|
-
|
|
110
101
|
## Ajuda na label
|
|
111
102
|
|
|
112
103
|
`FieldSpec.help` aparece como um ícone junto à label, com o texto num tooltip. Escreva ali o
|
|
@@ -18,9 +18,7 @@ render(
|
|
|
18
18
|
onOpenChange={setOpen}
|
|
19
19
|
title="Workspaces"
|
|
20
20
|
actions={
|
|
21
|
-
<Button
|
|
22
|
-
<Plus /> Workspace
|
|
23
|
-
</Button>
|
|
21
|
+
<Button>Criar workspace</Button>
|
|
24
22
|
}
|
|
25
23
|
emptyMessage="Nenhum workspace."
|
|
26
24
|
>
|
|
@@ -65,14 +63,14 @@ corpo do modal.
|
|
|
65
63
|
|
|
66
64
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
67
65
|
| ------------------------------------------ | ----------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------- |
|
|
68
|
-
| `action / input` | `ListAction / TInput` |
|
|
66
|
+
| `action / input` | `ListAction / TInput` | `input`: `{}` | O contrato e o escopo base opcional da consulta — mudou o input, re-busca; `invalidates` de forms/triggers refaz sozinho. |
|
|
69
67
|
| `open / onOpenChange` | `boolean / (open) => void` | | Controle do modal — de quem orquestra. |
|
|
70
68
|
| `title` | `string` | | Nome da lista exibido no cabeçalho fixo. |
|
|
71
69
|
| `intro` | `ReactNode` | | Contexto relevante no início do corpo, como texto ou `Alert`. |
|
|
72
70
|
| `children` | `(items, refetch) => ReactNode` | | O layout dos itens (cards/linhas) — os estados já saíram daqui. |
|
|
73
|
-
| `note` | `ReactNode
|
|
71
|
+
| `note` | `ReactNode` | | Contexto curto à esquerda da toolbar. O total de itens já aparece no rodapé do `ActionList`. |
|
|
74
72
|
| `actions` | `ReactNode` | | Ação à direita da toolbar — em geral o botão de criar. |
|
|
75
73
|
| `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado. |
|
|
76
74
|
| `loading` | `boolean` | | Carga extra agregada à do fetch (query irmã). |
|
|
77
|
-
| `emptyMessage / errorMessage / retryLabel` | `string` |
|
|
75
|
+
| `emptyMessage / errorMessage / retryLabel` | `string` | `'Nenhum resultado.'` / `'Não foi possível carregar.'` / `'Tentar de novo'` | Textos dos estados, herdados do `ActionList`; `retryLabel` nomeia a ação somente com ícone e seu tooltip. |
|
|
78
76
|
| `className` | `string` | `max-w-3xl` | Largura do DialogContent. |
|
|
@@ -64,6 +64,44 @@ Seletores inline mantêm largura previsível (`w-40`; lookup e múltiplo usam `w
|
|
|
64
64
|
são truncados no controle, mas permanecem completos na lista. No modal de filtros avançados, o
|
|
65
65
|
seletor ocupa toda a largura. Busca, filtros e período aparecem somente quando declarados.
|
|
66
66
|
|
|
67
|
+
## Onde cada filtro aparece
|
|
68
|
+
|
|
69
|
+
Cada filtro declara `placement`. `inline`, o padrão, mantém o controle na barra; `advanced` o leva
|
|
70
|
+
ao modal “Filtros”; `external` mantém o filtro no estado navegável e no input, mas deixa a
|
|
71
|
+
apresentação para o consumidor, como cards de status acima da lista. No modal, filtros com a mesma
|
|
72
|
+
`section` formam um grupo, e `advancedFilters.columns` distribui os grupos em até três colunas.
|
|
73
|
+
Campos de data avançados usam calendário. A antiga `advanced: true` continua aceita durante a
|
|
74
|
+
migração e equivale a `placement: 'advanced'`.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// No contrato:
|
|
78
|
+
filters: {
|
|
79
|
+
status: {
|
|
80
|
+
label: 'Status',
|
|
81
|
+
type: 'select',
|
|
82
|
+
placement: 'inline',
|
|
83
|
+
options: { kind: 'dictionary', ref: 'workspace.status' },
|
|
84
|
+
},
|
|
85
|
+
client: {
|
|
86
|
+
label: 'Cliente',
|
|
87
|
+
type: 'lookup',
|
|
88
|
+
placement: 'advanced',
|
|
89
|
+
section: 'Relacionamento',
|
|
90
|
+
options: { kind: 'lookup', source: 'client.lookup' },
|
|
91
|
+
},
|
|
92
|
+
createdAt: { label: 'Criado em', type: 'date', placement: 'advanced', section: 'Datas' },
|
|
93
|
+
stage: {
|
|
94
|
+
label: 'Estágio',
|
|
95
|
+
type: 'select',
|
|
96
|
+
placement: 'external',
|
|
97
|
+
options: { kind: 'dictionary', ref: 'workspace.stage' },
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Na tela:
|
|
102
|
+
<ActionList action={workspaceList} input={{}} advancedFilters={{ columns: 2 }} />
|
|
103
|
+
```
|
|
104
|
+
|
|
67
105
|
## Colunas de dicionário
|
|
68
106
|
|
|
69
107
|
Quando o campo de saída usa `t.dict().zod()`, a coluna apresenta o valor com `DictionaryValue` e
|
|
@@ -305,11 +343,11 @@ render(
|
|
|
305
343
|
|
|
306
344
|
| Chave | O que declara |
|
|
307
345
|
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
308
|
-
| `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden`
|
|
346
|
+
| `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
|
|
309
347
|
| `filters` | `{ [nome]: { label, type, options?, multiple?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
|
|
310
348
|
| `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. |
|
|
311
349
|
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
|
|
312
|
-
| `periods` | `{ value, label }[]` — o controle de período (presets + Personalizado com calendário); materializa em `from`/`to` no input. |
|
|
350
|
+
| `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
|
|
313
351
|
|
|
314
352
|
## useListAction
|
|
315
353
|
|
|
@@ -323,17 +361,21 @@ chamar a action.
|
|
|
323
361
|
|
|
324
362
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
325
363
|
| ----------------------- | ------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
326
|
-
| `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item
|
|
364
|
+
| `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item). A paginação envia `limit`/`page` e usa o `total` devolvido. |
|
|
327
365
|
| `input` | `TInput` | | O ESCOPO BASE (ex.: { workspaceId }) — a toolbar soma por cima, nunca sobrescreve. |
|
|
328
366
|
| `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
|
|
329
367
|
| `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para os filtros select/lookup (chave = nome do filtro). |
|
|
330
368
|
| `advancedFilters` | `{ columns?: 1 \| 2 \| 3 }` | `{ columns: 1 }` | Define as colunas dos grupos de filtros avançados; o Opus deriva a largura correspondente do diálogo. |
|
|
369
|
+
| `views` | `Record<string, ActionListView<TItem>>` | | Visualizações alternativas `{ label, icon?, render }`; o seletor alterna entre a tabela e estas. |
|
|
370
|
+
| `beforeContent` | `ReactNode` | | Conteúdo entre a toolbar e o corpo, como um resumo que reage ao recorte atual. |
|
|
331
371
|
| `columns` | `ActionListColumn<TItem>[]` | | Tabela EXPLÍCITA — sobrepõe as colunas do contrato (escape hatch). |
|
|
332
372
|
| `children` | `(items, refetch) => ReactNode` | | Modo composição: layout livre; a toolbar segue. Tem precedência sobre columns. |
|
|
333
|
-
| `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm
|
|
373
|
+
| `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm?: { title, message? }, destructive? }` — liga a multi-seleção. |
|
|
334
374
|
| `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
|
|
335
375
|
| `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. |
|
|
336
376
|
| `state / onStateChange` | `ActionListState / (next) => void` | interno | Estado da toolbar controlado — para quem embala sincronizar com a URL. |
|
|
377
|
+
| `empty` | `(items) => boolean` | `items.length === 0` | Sobrepõe o vazio derivado, por exemplo quando um formulário inline aberto conta como conteúdo. |
|
|
378
|
+
| `loading` | `boolean` | | Carga extra agregada à da consulta, como uma query irmã de que os `children` dependem. |
|
|
337
379
|
| `emptyMessage` | `string` | `'Nenhum resultado.'` | Frase do estado vazio (o `Empty` do `DataState`). |
|
|
338
380
|
| `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; a ação de tentar de novo refaz a consulta. |
|
|
339
381
|
| `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Use `ActionTrigger` para executar uma `SimpleAction` a partir de um botão. Sem confirmação, o clique
|
|
4
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
|
+
um toast. O rótulo padrão vem de `action.label` quando ele é texto; se o label for uma referência
|
|
6
|
+
de tradução ou estiver ausente, o botão usa `action.name`.
|
|
6
7
|
|
|
7
8
|
```tsx preview
|
|
8
9
|
<DocBrowserActionProvider>
|
|
@@ -58,7 +59,8 @@ não aciona o item clicável ao redor. `itemLabel` identifica o registro na mens
|
|
|
58
59
|
## Erro que a pessoa entende
|
|
59
60
|
|
|
60
61
|
Quando a action falha, uma mensagem do servidor substitui o texto genérico somente nos erros de
|
|
61
|
-
negócio `conflict`, `validation` e `not_found
|
|
62
|
+
negócio `conflict`, `validation` e `not_found`, e nos erros de acesso `authorization` e
|
|
63
|
+
`authentication`, cuja frase o runtime já entrega em pt-BR:
|
|
62
64
|
|
|
63
65
|
> Este agente tem 3 conversas — desabilite em vez de excluir.
|
|
64
66
|
|
|
@@ -78,10 +80,12 @@ um atalho, um arrastar, um item de menu.
|
|
|
78
80
|
|---|---|---|---|
|
|
79
81
|
| `action` | `SimpleContract<TInput, TData>` | | A SimpleAction do Opus — label, messages e confirm vêm do contrato. |
|
|
80
82
|
| `input` | `TInput` | | O que a action recebe — geralmente { id }. |
|
|
81
|
-
| `label` | `string` | `action.label` | Sobrepõe o texto do botão. |
|
|
82
|
-
| `context / variant / size` | `do Button` | gatilho: `primary`/`solid`; `danger` quando a action é `destructive`; no modo ícone, `ghost` e `neutral` (ou `danger` se destrutiva) / `icon-xs` | Visual do gatilho; `context` explícito vence. O botão de confirmar é sempre `solid`: `danger` quando a action é `destructive`, senão a prop `context` do gatilho (ou `primary`). |
|
|
83
|
-
| `confirm` | `{ title,
|
|
83
|
+
| `label` | `string` | `action.label` quando é texto; senão `action.name` | Sobrepõe o texto do botão. |
|
|
84
|
+
| `context / variant / size` | `do Button` | gatilho: `primary`/`solid`; `danger` quando a action é `destructive`; no modo ícone, `ghost` e `neutral` (ou `danger` se destrutiva) / `default`, ou `icon-xs` com `icon` | Visual do gatilho; `context` explícito vence. O botão de confirmar é sempre `solid`: `danger` quando a action é `destructive`, senão a prop `context` do gatilho (ou `primary`). |
|
|
85
|
+
| `confirm` | `{ title, body?, actionLabel?, cancelLabel? }` | | Confirmação via prop — sobrepõe o ConfirmSpec do contrato. |
|
|
86
|
+
| `disabled` | `boolean` | `false` | Desabilita o botão independentemente da execução em andamento. |
|
|
84
87
|
| `onSuccess` | `(data: TData) => void` | | Pós-sucesso (cache já invalidado pelo action.invalidates). |
|
|
88
|
+
| `onLoadingChange` | `(loading: boolean) => void` | | Informa início e fim da execução a quem coordena bloqueio entre várias actions. |
|
|
85
89
|
| `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. |
|
|
86
90
|
| `itemLabel` | `string` | | Nome do alvo na pergunta (sai entre aspas, em destaque, antes da mensagem do contrato). |
|
|
87
91
|
| `className` | `string` | | Classes do botão. O tamanho vem de `size` (escala única); com `icon`, o default é `icon-xs`. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Carregar um recurso
|
|
2
2
|
|
|
3
|
-
Use `ActionView` para carregar um recurso por uma `ViewAction` e manter carregamento
|
|
3
|
+
Use `ActionView` para carregar um recurso por uma `ViewAction` e manter carregamento e erro no
|
|
4
4
|
mesmo fluxo. No exemplo, alterne entre os workspaces para observar o carregamento e a recuperação de
|
|
5
5
|
erro. O consumidor compõe somente o conteúdo disponível.
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ render(
|
|
|
12
12
|
<div className="w-full space-y-3">
|
|
13
13
|
<div className="flex flex-wrap gap-2">
|
|
14
14
|
<Button variant="outline" size="sm" onClick={() => setId('empresa-x')}>Empresa X</Button>
|
|
15
|
-
<Button variant="outline" size="sm" onClick={() => setId('
|
|
15
|
+
<Button variant="outline" size="sm" onClick={() => setId('empresa-y')}>Empresa Y</Button>
|
|
16
16
|
<Button variant="outline" size="sm" onClick={() => setId('sessao-fantasma')}>Inexistente (erro)</Button>
|
|
17
17
|
</div>
|
|
18
18
|
<div className="rounded-lg border border-border p-4">
|
|
@@ -21,7 +21,7 @@ render(
|
|
|
21
21
|
<div className="space-y-1.5">
|
|
22
22
|
<div className="flex items-center gap-2">
|
|
23
23
|
<h3 className="text-sm font-semibold">{ws.name}</h3>
|
|
24
|
-
<
|
|
24
|
+
<DictionaryValue dict={docWorkspaceStatus} value={ws.status} />
|
|
25
25
|
</div>
|
|
26
26
|
<p className="text-sm text-muted-foreground">
|
|
27
27
|
Cliente {ws.client} · {ws.agents} agentes vinculados.
|
|
@@ -35,6 +35,10 @@ render(
|
|
|
35
35
|
)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
O handler da view deve devolver um valor. Uma consulta que resolve `undefined` é tratada como erro
|
|
39
|
+
pelo react-query, então `ActionView` apresenta o estado de erro, não um vazio. Represente um recurso
|
|
40
|
+
ausente com o erro `not_found`, cuja frase aparece para a pessoa.
|
|
41
|
+
|
|
38
42
|
## useViewAction
|
|
39
43
|
|
|
40
44
|
`ActionView` é uma composição sobre `useViewAction(action, input)`, que devolve `data`, `error`,
|
|
@@ -45,11 +49,11 @@ mais de uma região da tela ou quando o carregamento precisa ser orquestrado por
|
|
|
45
49
|
|
|
46
50
|
| Propriedade | Tipo | Padrão | Descrição |
|
|
47
51
|
|---|---|---|---|
|
|
48
|
-
| `action` | `
|
|
52
|
+
| `action` | `ViewContract<TInput, TData>` | | A ViewAction do Opus (kind view, 1 recurso). |
|
|
49
53
|
| `input` | `TInput` | | Geralmente { id } — mudou, recarrega. |
|
|
50
|
-
| `children` | `(data: TData, refetch) => ReactNode` | | Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
|
|
51
|
-
| `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. |
|
|
54
|
+
| `children` | `(data: TData, refetch) => ReactNode` | | Obrigatório, salvo quando `render` é informado. Conteúdo apresentado quando os dados estão disponíveis. `refetch` permite recarregar por código. |
|
|
55
|
+
| `render` | `(data: TData, refetch) => ReactNode` | | Alias de compatibilidade de `children`; `children` tem precedência. Sem nenhum dos dois, o componente lança erro. |
|
|
52
56
|
| `loading` | `ReactNode \| boolean` | `3 skeletons` | Sobrescreve o carregamento: um nó próprio, `true` para o padrão ou `false` para não renderizar nada enquanto carrega. |
|
|
53
|
-
| `empty` | `ReactNode` | `emptyMessage` ou nada |
|
|
54
|
-
| `emptyMessage` | `string` | | Atalho do vazio
|
|
57
|
+
| `empty` | `ReactNode` | `emptyMessage` ou nada | Estado reservado a `data` indefinido. Como o react-query converte esse resultado em erro, ele não aparece na prática; devolva um valor ou o erro `not_found`. |
|
|
58
|
+
| `emptyMessage` | `string` | | Atalho do vazio, sujeito à mesma limitação de `empty`: a frase na mesma superfície de `DataState` (`Empty` com moldura sólida). |
|
|
55
59
|
| `error` | `(err, retry) => ReactNode` | `"Não foi possível carregar" + Tentar de novo` | Sobrescreve o estado de erro padrão. Sem ele, a frase do servidor só aparece quando é legível pela pessoa (`conflict`, `validation`, `not_found`, `authorization`, `authentication`); código técnico nunca vira título, e o botão de tentar de novo some quando o problema é de permissão ou sessão. |
|
|
@@ -37,8 +37,9 @@ export const workspaceListContract = defineContract({
|
|
|
37
37
|
|
|
38
38
|
```ts
|
|
39
39
|
// api/ — server-only. bindAction amarra o handler no contrato compartilhado.
|
|
40
|
+
import { randomUUID } from 'node:crypto'
|
|
40
41
|
import { bindAction } from '@softize/opus/core'
|
|
41
|
-
import { workspaceCreateContract } from '@app/shared' //
|
|
42
|
+
import { workspaceCreateContract } from '@app/shared' // contrato `form`, declarado no shared/ como o acima
|
|
42
43
|
|
|
43
44
|
export const workspaceCreate = bindAction(workspaceCreateContract, {
|
|
44
45
|
handler: async (ctx, input) => {
|
|
@@ -81,16 +82,20 @@ handoff pluga o handler, e o mock nem toca `ctx.db`.
|
|
|
81
82
|
Alimente com `fake`/`fakeMany` (fixtures determinísticas do próprio schema — ver [Testes](testing)):
|
|
82
83
|
|
|
83
84
|
```ts
|
|
85
|
+
import { entityRowSchema } from '@softize/opus/schema'
|
|
84
86
|
import { fake, fakeMany } from '@softize/opus/testing'
|
|
85
87
|
|
|
88
|
+
// `defineEntity` devolve o próprio config; o schema da linha vem de `entityRowSchema`.
|
|
89
|
+
const eventRowSchema = entityRowSchema(EventEntity)
|
|
90
|
+
|
|
86
91
|
export const eventGet = defineContract({
|
|
87
92
|
name: 'event.get',
|
|
88
93
|
kind: 'view',
|
|
89
94
|
input: z.object({ id: z.string().uuid() }),
|
|
90
|
-
output:
|
|
91
|
-
mockHandler: () => fake(
|
|
95
|
+
output: eventRowSchema,
|
|
96
|
+
mockHandler: () => fake(eventRowSchema), // 1 evento fake
|
|
92
97
|
})
|
|
93
|
-
// listas: `mockHandler: () => fakeMany(
|
|
98
|
+
// listas: `mockHandler: () => fakeMany(eventRowSchema, 20)`
|
|
94
99
|
|
|
95
100
|
// o handoff, depois, só pluga o real — mesmo contrato:
|
|
96
101
|
export const eventGetImpl = bindAction(eventGet, { handler: async (ctx, input) => { /* db */ } })
|
|
@@ -103,12 +108,27 @@ qualquer `server.proxy` para backend externo desligado (prefixo ex-proxy sem cob
|
|
|
103
108
|
503 em envelope — nada vaza para prod). A SPA chama `/api` normal e recebe o dado fake, isolado:
|
|
104
109
|
|
|
105
110
|
```ts
|
|
106
|
-
// vite.config.ts
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
// vite.config.ts — o plugin só carrega no modo design, por import dinâmico.
|
|
112
|
+
const DESIGN_RUN =
|
|
113
|
+
process.argv.includes('--mode=design') ||
|
|
114
|
+
process.argv.some((arg, i) => (arg === '--mode' || arg === '-m') && process.argv[i + 1] === 'design')
|
|
115
|
+
|
|
116
|
+
async function designPlugin(): Promise<PluginOption> {
|
|
117
|
+
const { opusDesign } = await import('@softize/opus/vite')
|
|
118
|
+
return opusDesign()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const designPlugins: PluginOption[] = DESIGN_RUN ? [await designPlugin()] : []
|
|
122
|
+
|
|
123
|
+
export default defineConfig({ plugins: [react(), ...designPlugins] })
|
|
124
|
+
// sobe com: vite --mode design --configLoader runner (script dev:design do template)
|
|
110
125
|
```
|
|
111
126
|
|
|
127
|
+
O import fica dinâmico e condicionado porque o Opus publica TypeScript: o carregador padrão de
|
|
128
|
+
config do Vite externaliza um import estático, e o Node 22+ recusa type stripping dentro de
|
|
129
|
+
`node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). Com `--configLoader runner`, o Vite
|
|
130
|
+
transforma o TypeScript antes de avaliar o plugin. O app criado por `opus create` já traz esse arranjo.
|
|
131
|
+
|
|
112
132
|
O `entry` (default `./opus.config.ts`) também aceita **glob** — `opusDesign({ entry:
|
|
113
133
|
'src/domains/*/contract.ts' })` — re-expandido a cada rebuild: **domínio novo entra no ar ao
|
|
114
134
|
salvar o arquivo**, sem editar entry nem reiniciar o dev server (criar o arquivo já invalida o
|
|
@@ -128,12 +148,15 @@ descartável — vira o contrato que o backend honra.
|
|
|
128
148
|
> action inteira a partir do contrato — o spec é a fonte, o pattern é o renderizador.
|
|
129
149
|
|
|
130
150
|
```tsx
|
|
131
|
-
import {
|
|
132
|
-
import { ActionForm } from '@softize/opus/ui/react'
|
|
151
|
+
import { ActionForm, useListAction } from '@softize/opus/ui/react'
|
|
133
152
|
|
|
134
|
-
//
|
|
135
|
-
const {
|
|
153
|
+
// Lista declarativa: busca ao montar e refaz quando uma action invalida `workspace.list`.
|
|
154
|
+
const { items, total, isLoading } = useListAction<WorkspaceRow>(workspaceListContract, { q })
|
|
136
155
|
|
|
137
156
|
// Form contract-driven: os campos (fields) moram NO contrato.
|
|
138
|
-
<ActionForm
|
|
157
|
+
<ActionForm action={workspaceCreateContract} onSuccess={…} />
|
|
139
158
|
```
|
|
159
|
+
|
|
160
|
+
`useListAction` exige `OpusProvider` e um `QueryClientProvider` acima da árvore. Para buscar sob demanda, como num
|
|
161
|
+
autocomplete, use `useLookupAction`: ele não busca ao montar, expõe `run(input)` e `reset()` e
|
|
162
|
+
devolve `items`, `cursor`, `total` e os estados da última execução.
|
|
@@ -5,7 +5,7 @@ title: IA generativa
|
|
|
5
5
|
# IA generativa
|
|
6
6
|
|
|
7
7
|
> **Experimental.** Superfície mínima — cresce por reincidência de caso real, não por
|
|
8
|
-
> especulação (
|
|
8
|
+
> especulação (memória longa de conversa entra quando um caso real cobrar).
|
|
9
9
|
|
|
10
10
|
Três coisas: **completar** texto, **extrair** dado estruturado, e **rodar um agente** sobre
|
|
11
11
|
as actions do app. O contrato é um adapter do core (`AiAdapter`). O diferencial do `extract`
|
|
@@ -20,7 +20,9 @@ interface AiAdapter {
|
|
|
20
20
|
complete(prompt: string, opts?: AiCompleteOptions): Promise<string>
|
|
21
21
|
extract<T>(prompt: string, schema: Schema<T>, opts?: AiCompleteOptions): Promise<T>
|
|
22
22
|
// Loop agêntico: o modelo chama tools (as actions ai:enabled) até responder em texto.
|
|
23
|
-
run(input: string | AiMessage[], opts): Promise<AiRunResult>
|
|
23
|
+
run?(input: string | AiMessage[], opts: AiRunOptions): Promise<AiRunResult>
|
|
24
|
+
// O mesmo loop emitindo ChatEvent conforme acontece (ver Streaming, abaixo).
|
|
25
|
+
runStream?(input: string | AiMessage[], opts: AiRunOptions): AsyncIterable<ChatEvent>
|
|
24
26
|
}
|
|
25
27
|
// opts: { system?, model?, maxTokens?, temperature? } — tudo tem default do driver.
|
|
26
28
|
```
|
|
@@ -71,21 +73,38 @@ o modelo escolhe a tool → o runtime executa a action **como o usuário logado*
|
|
|
71
73
|
export const buscarNotas = defineContract({
|
|
72
74
|
name: 'nota.buscar',
|
|
73
75
|
kind: 'list',
|
|
76
|
+
label: 'Buscar notas',
|
|
74
77
|
input: z.object({ cliente: z.string(), mes: z.string() }),
|
|
75
|
-
output: z.object({
|
|
78
|
+
output: z.object({ numero: z.string(), valor: z.number() }), // em `list`, o schema de cada item
|
|
76
79
|
ai: { enabled: true, description: 'Busca notas por cliente e mês.' },
|
|
77
80
|
})
|
|
78
81
|
|
|
79
82
|
// No handler — ou fora dele, via runtime.aiFor(base), para o backend de um chat:
|
|
80
|
-
const { text } = await ctx.ai!.run('
|
|
83
|
+
const { text } = await ctx.ai!.run('Quais notas a Empresa X emitiu em junho?')
|
|
81
84
|
// o modelo chamou nota.buscar sozinho, como o usuário logado, e respondeu em texto.
|
|
82
85
|
```
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
`runtime.aiTools()` projeta cada action exposta como um `AiTool`: `name` é o identificador técnico
|
|
88
|
+
usado na execução e na auditoria; `title` traz a `label` legível da action, quando existe;
|
|
89
|
+
`description` vem de `ai.description` (ou da `description` da action) e `inputSchema`, do input.
|
|
90
|
+
`metadata.dataProducts` lista os Produtos de Dados relacionados e `metadata.dataProductLabels`
|
|
91
|
+
associa cada identificador ao seu nome legível. Esses metadados servem ao host: não entram no
|
|
92
|
+
prompt e não concedem acesso.
|
|
93
|
+
|
|
94
|
+
O agente só executa nomes que `aiTools()` anunciou; uma chamada a outra action volta como erro
|
|
95
|
+
para o modelo. Actions marcadas com `ai: { destructive: true }` ou `ai: { requiresConfirmation: true }`
|
|
96
|
+
**não rodam sem aprovação**: passe `run(prompt, { confirm })` — o chat mostra o "confirmar?"; sem
|
|
97
|
+
isso, a action é recusada e o modelo avisa. O gate lê somente esses dois campos de `ai`: o
|
|
98
|
+
`confirm: { destructive: true }` da action configura a confirmação da UI e não protege a chamada
|
|
99
|
+
feita pelo agente. Fora do handler, `runtime.aiFor(base)` devolve o `ai` já ligado a um contexto —
|
|
87
100
|
o backend do chat resolve o usuário e chama `.run(historico)`.
|
|
88
101
|
|
|
102
|
+
Para o modelo perguntar algo à pessoa no meio do turno, passe `onAsk` em `run` ou `runStream`. O
|
|
103
|
+
driver Anthropic então oferece a tool reservada `ask_user`, entrega as perguntas (`AskQuestion[]`)
|
|
104
|
+
ao seu `onAsk` e devolve as respostas (`AskAnswer[]`) ao modelo. Sem `onAsk`, uma chamada a
|
|
105
|
+
`ask_user` é recusada com a orientação de perguntar em texto. Na interface, o componente
|
|
106
|
+
[Ask](/ui/ask) apresenta essas perguntas e coleta as respostas.
|
|
107
|
+
|
|
89
108
|
## Streaming — o protocolo de eventos de conversa
|
|
90
109
|
|
|
91
110
|
`runStream` é o mesmo loop agêntico do `run`, emitindo **`ChatEvent`** conforme acontece
|
|
@@ -41,7 +41,7 @@ densidade comum.
|
|
|
41
41
|
|
|
42
42
|
Sem `icon` o alert mantém somente a coluna de texto. Com ele, a forma curta materializa
|
|
43
43
|
`AlertMedia` à esquerda e `AlertHeader` à direita. A mídia mantém uma moldura quadrada de tamanho
|
|
44
|
-
estável, mesmo quando o título
|
|
44
|
+
estável, mesmo quando a descrição ocupa mais linhas; o título fica limitado a uma linha. Texto solto como filho também
|
|
45
45
|
vale (`<Alert>Sincronizado.</Alert>`) e se torna uma descrição.
|
|
46
46
|
|
|
47
47
|
```tsx preview col
|
|
@@ -88,8 +88,9 @@ botão só com ícone, nome acessível e tooltip.
|
|
|
88
88
|
</Alert>
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
A forma curta também aceita uma ação como `children`: com `title` ou `description`, esse conteúdo
|
|
92
|
+
entra depois da mensagem, na posição de `AlertActions`. Não combine a forma curta com os slots
|
|
93
|
+
`AlertMedia`, `AlertHeader` ou `AlertActions`; essa mistura lança erro.
|
|
93
94
|
|
|
94
95
|
```tsx preview col
|
|
95
96
|
<Alert
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Vídeo e preview (16/9)
|
|
2
2
|
|
|
3
|
-
O `ratio` é a proporção entre largura e altura — 16/9 é o
|
|
3
|
+
O `ratio` é a proporção entre largura e altura — 16/9 é o formato comum de vídeo. Defina a largura no
|
|
4
4
|
contêiner (o pai): a altura o componente deriva sozinho.
|
|
5
5
|
|
|
6
6
|
```tsx preview col
|
|
@@ -32,7 +32,10 @@ import { jwtAuth } from '@softize/opus/auth/jwt'
|
|
|
32
32
|
import { betterAuthSession } from '@softize/opus/auth/better-auth'
|
|
33
33
|
|
|
34
34
|
// JWT: valida o token (HS256 por padrão) e mapeia o payload para o User.
|
|
35
|
-
const jwt = jwtAuth({
|
|
35
|
+
const jwt = jwtAuth({
|
|
36
|
+
secret: process.env.JWT_SECRET!,
|
|
37
|
+
mapUser: (payload) => ({ id: String(payload.sub), email: payload.email }),
|
|
38
|
+
})
|
|
36
39
|
|
|
37
40
|
// better-auth: valida a sessão contra um IdP better-auth remoto.
|
|
38
41
|
const idp = betterAuthSession({
|
|
@@ -42,8 +45,10 @@ const idp = betterAuthSession({
|
|
|
42
45
|
})
|
|
43
46
|
```
|
|
44
47
|
|
|
45
|
-
O `jwt` procura o token no `Authorization: Bearer
|
|
46
|
-
`
|
|
48
|
+
O `jwt` procura o token no `Authorization: Bearer` e depois no cookie `auth_token` (troque o nome
|
|
49
|
+
com `cookieName` ou a extração inteira com `tokenExtractor`). `mapUser` é obrigatório; o `can`
|
|
50
|
+
default nega tudo. O `secret` pode ser string, `Buffer` ou um resolver async, hoje chamado sem o
|
|
51
|
+
`kid` do token. O `betterAuthSession` faz fetch da
|
|
47
52
|
sessão no IdP (`timeoutMs`, default 5s) e mapeia para o `User`/tenant/can (o `can` default nega
|
|
48
53
|
tudo — plugue o seu).
|
|
49
54
|
|
|
@@ -55,15 +60,25 @@ const runtime = createRuntime({
|
|
|
55
60
|
auth: idp,
|
|
56
61
|
})
|
|
57
62
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
+
// Na action: `authorize` decide antes do handler, com o contexto já resolvido.
|
|
64
|
+
export const notaEmitir = bindAction(notaEmitirContract, {
|
|
65
|
+
authorize: (ctx) => ctx.can('nota.emitir'),
|
|
66
|
+
handler: async (ctx, input) => repo.create({ ...input, tenantId: ctx.tenantId }),
|
|
67
|
+
})
|
|
63
68
|
```
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
O runtime aplica duas barreiras antes do handler. Sem `public: true`, a action exige
|
|
71
|
+
`ctx.user`; sem usuário, responde `auth.unauthenticated`. Depois, se houver `authorize`, o
|
|
72
|
+
runtime o avalia: `false` vira `auth.forbidden` e um `ActionError` devolvido é lançado como está.
|
|
73
|
+
`authorize` aceita uma closure, como acima, ou uma expressão da DSL avaliada contra `user`,
|
|
74
|
+
`input` e os dados carregados por `loads`.
|
|
75
|
+
|
|
76
|
+
Uma action sem `authorize` fica aberta a qualquer usuário autenticado. `requires` só documenta a
|
|
77
|
+
permissão esperada: o runtime não o executa, e `opus check` acusa `requires` sem `authorize`.
|
|
78
|
+
Quando a decisão depende de dado que só o handler conhece, verifique no próprio handler com
|
|
79
|
+
`ctx.can` e lance `error({ code: 'forbidden', category: 'authorization' })`.
|
|
80
|
+
Sem adapter `auth` configurado, os servidores HTTP resolvem `ctx.user` como null e só actions
|
|
81
|
+
`public` executam.
|
|
67
82
|
|
|
68
83
|
## Limites (por enquanto)
|
|
69
84
|
|
|
@@ -57,7 +57,7 @@ para o ocioso. Tinja com className.
|
|
|
57
57
|
<AvatarFallback>
|
|
58
58
|
<Bot className="size-4" />
|
|
59
59
|
</AvatarFallback>
|
|
60
|
-
<AvatarBadge className="bg-
|
|
60
|
+
<AvatarBadge className="bg-context-success" />
|
|
61
61
|
</Avatar>
|
|
62
62
|
<Avatar>
|
|
63
63
|
<AvatarFallback>RV</AvatarFallback>
|
|
@@ -32,8 +32,8 @@ Um svg filho ganha size-3 automaticamente — bom para reforçar o estado sem cr
|
|
|
32
32
|
|
|
33
33
|
## Como link (asChild)
|
|
34
34
|
|
|
35
|
-
asChild renderiza o filho (Radix Slot) — um `<a>` com cara de badge
|
|
36
|
-
|
|
35
|
+
asChild renderiza o filho (Radix Slot) — um `<a>` com cara de badge. O Badge não aplica estado de
|
|
36
|
+
hover; acrescente-o por `className` quando o link precisar de retorno visual.
|
|
37
37
|
|
|
38
38
|
```tsx preview
|
|
39
39
|
<Badge asChild context="neutral" variant="solid">
|
|
@@ -53,8 +53,9 @@ chevron padrão por outro glifo (aqui, uma barra).
|
|
|
53
53
|
|
|
54
54
|
## Colapsado
|
|
55
55
|
|
|
56
|
-
Trilha funda demais para o espaço: BreadcrumbEllipsis
|
|
57
|
-
|
|
56
|
+
Trilha funda demais para o espaço: BreadcrumbEllipsis marca os níveis do meio omitidos e mantém só a
|
|
57
|
+
raiz e o destino. A elipse é decorativa (`aria-hidden`), não um menu: os níveis ocultos deixam de ser
|
|
58
|
+
alcançáveis pela trilha. Ofereça esse caminho por outro meio, como a navegação da seção.
|
|
58
59
|
|
|
59
60
|
```tsx preview col-start
|
|
60
61
|
<Breadcrumb>
|