@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.
Files changed (96) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/PROMOTED.md +4 -5
  3. package/README.md +5 -4
  4. package/bin/cli.mjs +4 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +3 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +6 -2
  7. package/docs/adr/0011-page-shell-coordinates-persistent-page-chrome.md +6 -1
  8. package/docs/adr/0014-structural-headers-do-not-carry-description.md +2 -2
  9. package/docs/adr/0015-action-size-follows-interaction-density.md +4 -3
  10. package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
  11. package/docs/adr/{0016-productive-surfaces-use-compact-density.md → 0019-productive-surfaces-use-compact-density.md} +4 -1
  12. package/docs/code-style.md +2 -2
  13. package/docs/consumer-upgrade-propagation.md +1 -1
  14. package/docs/data-products.md +5 -3
  15. package/docs/protocol.md +6 -6
  16. package/docs/releasing.md +28 -4
  17. package/package.json +1 -1
  18. package/registry/skills/build-opus-ui/references/ui-patterns.md +11 -1
  19. package/src/auth/drivers/jwt.ts +2 -1
  20. package/src/core/runtime.ts +25 -4
  21. package/src/core/types.ts +4 -4
  22. package/src/mcp/index.ts +9 -0
  23. package/src/ui/components/patterns/content-header.tsx +1 -1
  24. package/src/ui/components/patterns/form.tsx +1 -1
  25. package/src/ui/components/patterns/sidebar.tsx +1 -1
  26. package/src/ui/components/primitives/card.tsx +1 -1
  27. package/src/ui/components/primitives/detail.tsx +7 -7
  28. package/src/ui/components/primitives/radio-group.tsx +1 -1
  29. package/src/ui/components/primitives/select.tsx +1 -1
  30. package/src/ui/docs/content/action-form-dialog.md +11 -4
  31. package/src/ui/docs/content/action-form.md +7 -16
  32. package/src/ui/docs/content/action-list-dialog.md +4 -6
  33. package/src/ui/docs/content/action-list.md +46 -4
  34. package/src/ui/docs/content/action-trigger.md +9 -5
  35. package/src/ui/docs/content/action-view.md +12 -8
  36. package/src/ui/docs/content/actions.md +36 -13
  37. package/src/ui/docs/content/ai.md +26 -7
  38. package/src/ui/docs/content/alert.md +4 -3
  39. package/src/ui/docs/content/aspect-ratio.md +2 -2
  40. package/src/ui/docs/content/auth.md +25 -10
  41. package/src/ui/docs/content/avatar.md +1 -1
  42. package/src/ui/docs/content/badge.md +2 -2
  43. package/src/ui/docs/content/breadcrumb.md +3 -2
  44. package/src/ui/docs/content/button.md +31 -7
  45. package/src/ui/docs/content/calendar.md +1 -1
  46. package/src/ui/docs/content/card.md +1 -1
  47. package/src/ui/docs/content/carousel.md +14 -3
  48. package/src/ui/docs/content/chat.md +1 -1
  49. package/src/ui/docs/content/cli.md +13 -7
  50. package/src/ui/docs/content/command.md +34 -2
  51. package/src/ui/docs/content/composer.md +1 -1
  52. package/src/ui/docs/content/content.md +5 -4
  53. package/src/ui/docs/content/customization.md +1 -1
  54. package/src/ui/docs/content/cycle.md +7 -5
  55. package/src/ui/docs/content/data-state.md +6 -5
  56. package/src/ui/docs/content/data.md +3 -3
  57. package/src/ui/docs/content/detail.md +3 -2
  58. package/src/ui/docs/content/dialog.md +2 -2
  59. package/src/ui/docs/content/dictionary-value.md +1 -1
  60. package/src/ui/docs/content/dock.md +23 -2
  61. package/src/ui/docs/content/dot.md +0 -2
  62. package/src/ui/docs/content/drawer.md +1 -1
  63. package/src/ui/docs/content/empty.md +1 -4
  64. package/src/ui/docs/content/events.md +1 -1
  65. package/src/ui/docs/content/field.md +20 -11
  66. package/src/ui/docs/content/getting-started.md +4 -2
  67. package/src/ui/docs/content/icon-picker.md +2 -2
  68. package/src/ui/docs/content/input-otp.md +2 -0
  69. package/src/ui/docs/content/input.md +2 -3
  70. package/src/ui/docs/content/item.md +5 -4
  71. package/src/ui/docs/content/kbd.md +2 -1
  72. package/src/ui/docs/content/mcp.md +10 -4
  73. package/src/ui/docs/content/menu.md +27 -0
  74. package/src/ui/docs/content/page.md +19 -5
  75. package/src/ui/docs/content/pagination.md +9 -2
  76. package/src/ui/docs/content/popover.md +2 -2
  77. package/src/ui/docs/content/presentation.md +46 -45
  78. package/src/ui/docs/content/progress.md +2 -6
  79. package/src/ui/docs/content/runtime.md +8 -5
  80. package/src/ui/docs/content/scheduler.md +1 -1
  81. package/src/ui/docs/content/select.md +13 -8
  82. package/src/ui/docs/content/sidebar.md +3 -2
  83. package/src/ui/docs/content/skeleton.md +1 -1
  84. package/src/ui/docs/content/slider.md +4 -4
  85. package/src/ui/docs/content/spinner.md +3 -3
  86. package/src/ui/docs/content/tabs.md +6 -6
  87. package/src/ui/docs/content/testing.md +4 -2
  88. package/src/ui/docs/content/toast.md +3 -5
  89. package/src/ui/docs/content/toggle.md +37 -0
  90. package/src/ui/docs/content/tooltip.md +4 -3
  91. package/src/ui/docs/content/truncate.md +3 -2
  92. package/src/ui/docs/content/ui.md +3 -1
  93. package/src/ui/docs/content/upgrading.md +43 -13
  94. package/src/ui/docs/doc-client.tsx +1 -1
  95. package/src/ui/docs/registry.tsx +30 -5
  96. 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 enquanto se digita a busca (cai pro `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
- Quando cancelamento, o rodapé usa um `ButtonGroup` dividido: `Cancelar` aparece em `outline` e
7
- a ação principal permanece `solid`, ambas no tamanho normal de uma decisão modal.
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
- | `…ActionFormProps` | `action, defaultValues, onSuccess, fieldOptions…` | | Demais propriedades repassadas ao `ActionForm` interno. |
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. `submitLabel` e `cancelLabel` nomeiam as ações;
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. Use `outline` quando ele dividir o footer com a ação principal. |
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 size="sm" variant="outline">
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` | | O contrato e os filtros — mudou o input, re-busca; `invalidates` de forms/triggers refaz sozinho. |
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 \| (items) => ReactNode` | `"N no total"` | Nota à esquerda da toolbar o default é derivado dos itens. |
71
+ | `note` | `ReactNode` | | Contexto curto à esquerda da toolbar. O total de itens 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` | | Textos dos estados; `retryLabel` nomeia a ação somente com ícone e seu tooltip. |
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` fica fora (base do futuro column picker); `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
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, paginate cursor). |
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?, destructive? }` — liga a multi-seleção. |
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, description?, actionLabel?, cancelLabel? }` | | Confirmação via prop — sobrepõe o ConfirmSpec do contrato. |
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, erro e vazio no
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('softize-multica')}>Softize · Multica</Button>
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
- <Badge context={ws.status === 'active' ? 'success' : 'warning'}>{ws.status}</Badge>
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` | `ViewAction<TInput, TData>` | | A ViewAction do Opus (kind view, 1 recurso). |
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 | Sobrescreve o estado vazio (200 sem dado). |
54
- | `emptyMessage` | `string` | | Atalho do vazio: a frase na mesma superfície de `DataState` (`Empty` com moldura sólida). |
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' // o mesmo contrato do bloco acima
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: EventEntity.zod(),
91
- mockHandler: () => fake(EventEntity.zod()), // 1 evento fake
95
+ output: eventRowSchema,
96
+ mockHandler: () => fake(eventRowSchema), // 1 evento fake
92
97
  })
93
- // listas: `mockHandler: () => fakeMany(EventEntity.zod(), 20)`
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
- import { opusDesign } from '@softize/opus/vite'
108
- export default defineConfig({ plugins: [react(), opusDesign()] })
109
- // sobe com: vite --mode design
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 { useLookupAction } from '@softize/opus/client'
132
- import { ActionForm } from '@softize/opus/ui/react'
151
+ import { ActionForm, useListAction } from '@softize/opus/ui/react'
133
152
 
134
- // O contrato mantém a lista paginada tipada sem exigir uma definição paralela.
135
- const { rows, fetchNextPage } = useLookupAction(workspaceListContract)
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 contract={workspaceCreateContract} onSuccess={…} />
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 (streaming e memória longa de conversa entram quando um caso real cobrar).
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({ total: z.number() }),
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('Quantas notas a Empresa X emitiu em junho?')
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
- O que é `destructive` ou pede `requiresConfirmation` **não roda sem aprovação**: passe
85
- `run(prompt, { confirm })` o chat mostra o "confirmar?"; sem isso, a action é recusada e o
86
- modelo avisa. Fora do handler, `runtime.aiFor(base)` devolve o `ai` ligado a um contexto
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 ou a descrição ocupam mais linhas. Texto solto como filho também
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
- Os dois modos convivem: com `title` ou `description`, `children` entra depois da mensagem e recebe a
92
- ação.
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
- ## Padrão (16/9)
1
+ ## Vídeo e preview (16/9)
2
2
 
3
- O `ratio` é a proporção entre largura e altura — 16/9 é o padrão de vídeo. Defina a largura no
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({ secret: process.env.JWT_SECRET! })
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`, em cookie ou custom (via `getToken`); o
46
- `secret` pode ser string ou um resolver async por `kid`. O `betterAuthSession` faz fetch da
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
- // No handler: o contexto já vem resolvido.
59
- handler: async (ctx, input) => {
60
- if (!(await ctx.can('nota.emitir'))) throw error({ code: 'forbidden' })
61
- return repo.create({ ...input, tenantId: ctx.tenantId })
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
- A action declara o gate no contrato (`auth: 'nota.emitir'`) e o runtime cobra antes do
66
- handler. Sem `auth` configurado, `ctx.user` é null.
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-emerald-500" />
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, com hover próprio das
36
- variantes.
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 substitui os níveis do meio (que viram um
57
- menu/popover) e mantém a raiz e o destino.
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>