@softize/opus 19.0.0 → 20.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +239 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/check.mjs +198 -10
  4. package/bin/lib/copy.mjs +29 -0
  5. package/bin/lib/mcp.mjs +1 -1
  6. package/docs/adr/0020-icon-tiles-are-explicitly-composed.md +8 -0
  7. package/docs/adr/0021-content-carries-structure-and-semantic-element.md +7 -0
  8. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +19 -1
  9. package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
  10. package/docs/adr/0025-content-frame-treatment-does-not-change-semantics.md +134 -0
  11. package/docs/data-products.md +15 -9
  12. package/docs/protocol.md +3 -4
  13. package/docs/releasing.md +2 -2
  14. package/package.json +2 -2
  15. package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
  16. package/registry/templates/app/package.json +1 -1
  17. package/src/core/runtime.ts +96 -8
  18. package/src/core/types.ts +5 -2
  19. package/src/ui/components/patterns/action-form-card.tsx +11 -1
  20. package/src/ui/components/patterns/content-header.tsx +109 -18
  21. package/src/ui/components/patterns/fill-region.ts +150 -0
  22. package/src/ui/components/patterns/list.tsx +91 -92
  23. package/src/ui/components/patterns/page.tsx +20 -4
  24. package/src/ui/components/patterns/presentation.tsx +52 -37
  25. package/src/ui/components/patterns/split.tsx +44 -4
  26. package/src/ui/components/patterns/surface-anatomy.ts +35 -8
  27. package/src/ui/components/patterns/surface-header.tsx +58 -25
  28. package/src/ui/components/patterns/trigger.tsx +12 -2
  29. package/src/ui/components/primitives/alert.tsx +9 -1
  30. package/src/ui/components/primitives/avatar.tsx +7 -3
  31. package/src/ui/components/primitives/card.tsx +1 -1
  32. package/src/ui/components/primitives/carousel.tsx +34 -5
  33. package/src/ui/components/primitives/checkbox.tsx +11 -3
  34. package/src/ui/components/primitives/command.tsx +2 -1
  35. package/src/ui/components/primitives/copyable.tsx +120 -10
  36. package/src/ui/components/primitives/dialog.tsx +42 -11
  37. package/src/ui/components/primitives/drawer.tsx +10 -2
  38. package/src/ui/components/primitives/empty.tsx +1 -1
  39. package/src/ui/components/primitives/icon-picker.tsx +6 -2
  40. package/src/ui/components/primitives/icon-tile.tsx +7 -0
  41. package/src/ui/components/primitives/input-group.tsx +9 -2
  42. package/src/ui/components/primitives/input-otp.tsx +1 -1
  43. package/src/ui/components/primitives/kbd.tsx +1 -1
  44. package/src/ui/components/primitives/label.tsx +3 -0
  45. package/src/ui/components/primitives/menu.tsx +5 -0
  46. package/src/ui/components/primitives/popover.tsx +1 -1
  47. package/src/ui/components/primitives/progress.tsx +9 -1
  48. package/src/ui/components/primitives/slider.tsx +62 -11
  49. package/src/ui/components/primitives/truncate.tsx +10 -1
  50. package/src/ui/docs/content/accordion.md +74 -27
  51. package/src/ui/docs/content/action-form-card.md +24 -15
  52. package/src/ui/docs/content/action-form-dialog.md +17 -7
  53. package/src/ui/docs/content/action-form.md +92 -77
  54. package/src/ui/docs/content/action-list-dialog.md +75 -38
  55. package/src/ui/docs/content/action-list.md +141 -58
  56. package/src/ui/docs/content/action-trigger.md +27 -10
  57. package/src/ui/docs/content/action-view.md +14 -14
  58. package/src/ui/docs/content/actions.md +161 -66
  59. package/src/ui/docs/content/ai.md +43 -44
  60. package/src/ui/docs/content/alert.md +83 -29
  61. package/src/ui/docs/content/ask.md +51 -26
  62. package/src/ui/docs/content/aspect-ratio.md +22 -24
  63. package/src/ui/docs/content/audit.md +23 -19
  64. package/src/ui/docs/content/auth.md +38 -19
  65. package/src/ui/docs/content/avatar.md +54 -5
  66. package/src/ui/docs/content/badge.md +29 -9
  67. package/src/ui/docs/content/breadcrumb.md +21 -8
  68. package/src/ui/docs/content/button.md +73 -43
  69. package/src/ui/docs/content/calendar.md +134 -26
  70. package/src/ui/docs/content/card.md +41 -15
  71. package/src/ui/docs/content/carousel.md +37 -6
  72. package/src/ui/docs/content/chat.md +118 -37
  73. package/src/ui/docs/content/checkbox.md +73 -7
  74. package/src/ui/docs/content/cli.md +94 -55
  75. package/src/ui/docs/content/collapsible.md +43 -19
  76. package/src/ui/docs/content/command.md +30 -8
  77. package/src/ui/docs/content/composer.md +23 -8
  78. package/src/ui/docs/content/content.md +119 -27
  79. package/src/ui/docs/content/copyable.md +39 -8
  80. package/src/ui/docs/content/customization.md +55 -46
  81. package/src/ui/docs/content/cycle.md +33 -18
  82. package/src/ui/docs/content/data-products.md +115 -0
  83. package/src/ui/docs/content/data-state.md +20 -6
  84. package/src/ui/docs/content/data.md +20 -19
  85. package/src/ui/docs/content/detail.md +12 -5
  86. package/src/ui/docs/content/dialog.md +107 -24
  87. package/src/ui/docs/content/dictionary-value.md +4 -4
  88. package/src/ui/docs/content/dock.md +54 -19
  89. package/src/ui/docs/content/dot.md +35 -5
  90. package/src/ui/docs/content/drawer.md +63 -19
  91. package/src/ui/docs/content/empty-value.md +20 -6
  92. package/src/ui/docs/content/empty.md +24 -3
  93. package/src/ui/docs/content/events.md +76 -16
  94. package/src/ui/docs/content/executions.md +9 -9
  95. package/src/ui/docs/content/field.md +21 -6
  96. package/src/ui/docs/content/getting-started.md +44 -34
  97. package/src/ui/docs/content/icon-picker.md +19 -13
  98. package/src/ui/docs/content/icon-tile.md +38 -15
  99. package/src/ui/docs/content/input-otp.md +58 -7
  100. package/src/ui/docs/content/input.md +22 -3
  101. package/src/ui/docs/content/item.md +53 -6
  102. package/src/ui/docs/content/kbd.md +5 -1
  103. package/src/ui/docs/content/label.md +13 -4
  104. package/src/ui/docs/content/log.md +5 -5
  105. package/src/ui/docs/content/markdown.md +5 -4
  106. package/src/ui/docs/content/mcp.md +49 -23
  107. package/src/ui/docs/content/menu.md +95 -10
  108. package/src/ui/docs/content/metric-card.md +12 -4
  109. package/src/ui/docs/content/observability.md +10 -9
  110. package/src/ui/docs/content/page.md +118 -12
  111. package/src/ui/docs/content/pagination.md +13 -5
  112. package/src/ui/docs/content/pane.md +55 -5
  113. package/src/ui/docs/content/popover.md +42 -5
  114. package/src/ui/docs/content/presentation.md +91 -47
  115. package/src/ui/docs/content/progress.md +27 -14
  116. package/src/ui/docs/content/queue.md +39 -21
  117. package/src/ui/docs/content/radio-group.md +26 -4
  118. package/src/ui/docs/content/router.md +29 -10
  119. package/src/ui/docs/content/runtime.md +90 -54
  120. package/src/ui/docs/content/scheduler.md +53 -21
  121. package/src/ui/docs/content/scroll-area.md +42 -35
  122. package/src/ui/docs/content/select.md +78 -33
  123. package/src/ui/docs/content/semantic-context.md +21 -2
  124. package/src/ui/docs/content/separator.md +15 -9
  125. package/src/ui/docs/content/sidebar.md +3 -2
  126. package/src/ui/docs/content/skeleton.md +6 -1
  127. package/src/ui/docs/content/slider.md +37 -9
  128. package/src/ui/docs/content/spinner.md +4 -3
  129. package/src/ui/docs/content/split.md +19 -0
  130. package/src/ui/docs/content/storage.md +8 -9
  131. package/src/ui/docs/content/switch.md +10 -7
  132. package/src/ui/docs/content/table.md +46 -8
  133. package/src/ui/docs/content/tabs.md +14 -4
  134. package/src/ui/docs/content/testing.md +76 -31
  135. package/src/ui/docs/content/textarea.md +5 -4
  136. package/src/ui/docs/content/toast.md +48 -7
  137. package/src/ui/docs/content/toggle.md +3 -3
  138. package/src/ui/docs/content/tokens.md +15 -26
  139. package/src/ui/docs/content/tooltip.md +18 -2
  140. package/src/ui/docs/content/truncate.md +36 -6
  141. package/src/ui/docs/content/ui.md +74 -22
  142. package/src/ui/docs/content/upgrading.md +45 -36
  143. package/src/ui/docs/plugin.ts +36 -2
  144. package/src/ui/docs/registry.tsx +6 -0
  145. package/src/ui/drivers/react.tsx +16 -8
  146. package/src/ui/meta.ts +32 -32
  147. package/src/ui/react.tsx +1 -0
  148. package/src/vite/design.ts +16 -4
@@ -6,14 +6,17 @@ recursos e envia o estado correspondente no input. Filtros avançados aparecem n
6
6
  os filtros aplicados permanecem visíveis como chips removíveis.
7
7
 
8
8
  A tabela derivada não cria uma moldura própria. A superfície que recebe o `ActionList` define os
9
- limites visuais da coleção; use `Table` diretamente quando a composição precisar da variante
10
- `framed`.
11
-
12
- A barra se adapta ao espaço disponível. A busca cede largura primeiro e filtros que deixam de caber
13
- migram para o modal; a linha só quebra quando nenhum controle restante puder ceder espaço.
14
- `toolbarActions` acrescenta operações ligadas ao recorte atual no fim da barra. Recarregar e
15
- exibição formam um `ButtonGroup` espaçado; as ações do consumidor vêm depois, com um intervalo
16
- maior para preservar a hierarquia entre controles e operações do recorte.
9
+ limites visuais da coleção; use [`Table`](/ui/table) diretamente quando a composição precisar da
10
+ variante `framed`. Carregamento, erro e vazio usam o mesmo [`DataState`](/ui/data-state) das
11
+ demais coleções.
12
+
13
+ A barra mantém em uma única linha os controles declarados como `inline`: ela não quebra e não move
14
+ filtros para o modal por falta de espaço. Seletores de valor único acompanham o valor escolhido até
15
+ `20rem` e truncam o excedente; a busca varia entre `8rem` e `16rem` conforme a largura
16
+ disponível. Quando a barra ficar longa demais, declare como `advanced` os filtros que não precisam
17
+ ficar sempre à vista, como descreve a seção “Onde cada filtro aparece”. Recarregar e
18
+ exibição formam um `ButtonGroup` espaçado; `toolbarActions` acrescenta, depois deles, operações
19
+ ligadas ao recorte atual.
17
20
 
18
21
  ```tsx preview col
19
22
  render(
@@ -31,8 +34,9 @@ render(
31
34
 
32
35
  ## Coleção nomeada
33
36
 
34
- Em uma página, envolva a listagem com `Content`. O título e a criação ocupam extremos opostos do
35
- cabeçalho da coleção; busca, filtros e atualização permanecem na toolbar do `ActionList`.
37
+ Em uma página, envolva a listagem com [`Content`](/ui/content). O título e a criação ocupam
38
+ extremos opostos do cabeçalho da coleção; busca, filtros e atualização permanecem na toolbar do
39
+ `ActionList`.
36
40
 
37
41
  ```tsx
38
42
  <Content
@@ -46,7 +50,8 @@ cabeçalho da coleção; busca, filtros e atualização permanecem na toolbar do
46
50
  ```
47
51
 
48
52
  Use `toolbarActions` somente quando a operação depender do recorte visível, como exportar os
49
- resultados filtrados. Uma ação icon-only nessa região mantém o nome no tooltip e no `aria-label`.
53
+ resultados filtrados. A propriedade recebe os nós já prontos: em uma ação somente com ícone, forneça
54
+ você mesmo o `aria-label` e o `Tooltip` com o nome da operação.
50
55
 
51
56
  ## Barra de filtros compartilhada
52
57
 
@@ -64,9 +69,9 @@ superfícies que não são listagens, como relatórios. O estado é controlado p
64
69
  />
65
70
  ```
66
71
 
67
- Seletores inline de valor único abraçam o valor selecionado até `max-w-80`. Valores maiores são
72
+ Seletores inline de valor único acompanham o valor selecionado até `20rem`. Valores maiores são
68
73
  truncados no controle, enquanto o dropdown pode crescer além dele para preservar a opção.
69
- Multiseletores mantêm `w-52`; no modal de filtros avançados, o seletor ocupa toda a largura.
74
+ Multiseletores mantêm `13rem`; no modal de filtros avançados, o seletor ocupa toda a largura.
70
75
  `filterIcons` associa um ícone leading ao nome do filtro quando a superfície precisa reforçar
71
76
  dimensões recorrentes, sem substituir o rótulo. Busca, filtros e período aparecem somente quando
72
77
  declarados.
@@ -79,7 +84,7 @@ simples usam o valor vazio como padrão; por isso, opções como “Todos” tam
79
84
  Use `periodPosition` para manter o período antes ou depois dos filtros inline. Quando ele precisa
80
85
  apenas aparecer depois de um filtro específico, use `{ afterFilter: 'dateBasis' }`. Se tipo de data
81
86
  e período formam uma única decisão, declare `period={{ basisFilter: 'dateBasis' }}`: o Opus os reúne
82
- num só controle, com ícone no início e uma única ação de limpar no fim. Quando a pessoa muda o tipo
87
+ em um só controle, com ícone no início e uma única ação de limpar no fim. Quando a pessoa muda o tipo
83
88
  ou o período padrão, essa ação aparece e restaura os dois valores. Se a âncora de `periodPosition`
84
89
  não existir, o período permanece visível no fim dos filtros inline.
85
90
 
@@ -91,10 +96,11 @@ apresentação para o consumidor, como cards de status acima da lista. No modal,
91
96
  `section` formam um grupo, e `advancedFilters.columns` distribui os grupos em até três colunas.
92
97
  Use `advancedFilters.sectionIcons` quando os grupos representarem dimensões recorrentes; a chave é
93
98
  o mesmo rótulo de `section`, e cada grupo usa `Content as="fieldset"` para manter uma legenda
94
- nativa. Quando os grupos precisarem de limites mais explícitos,
95
- `advancedFilters.sectionVariant: 'framed'` envolve cada seção e pede ao `Content` o acabamento
96
- `frameVariant="legend"`, integrando visualmente a legenda à borda sem atribuir esse design à
97
- semântica de `fieldset`.
99
+ nativa. O ícone ocupa a mídia do grupo, antes do título, e recebe a moldura padrão: passe só o
100
+ glifo. Um `IconTile` é adotado pela mídia com o que declara, e um ícone `null` não desenha nada.
101
+ Quando os grupos precisarem de limites mais explícitos, `advancedFilters.sectionVariant: 'framed'`
102
+ envolve cada seção e pede ao `Content` o acabamento `frameVariant="legend"`, integrando visualmente
103
+ a legenda à borda sem atribuir esse design à semântica de `fieldset`.
98
104
  Um filtro `select` pode declarar `searchable: true` quando a lista fechada for longa; a opção geral
99
105
  continua disponível e participa da busca. Campos de data avançados usam calendário. A antiga `advanced: true` continua aceita durante a
100
106
  migração e equivale a `placement: 'advanced'`.
@@ -133,8 +139,8 @@ filters: {
133
139
  columns: 2,
134
140
  sectionVariant: 'framed',
135
141
  sectionIcons: {
136
- Relacionamento: <IconTile size="xs"><UserRound /></IconTile>,
137
- Datas: <IconTile size="xs"><CalendarDays /></IconTile>,
142
+ Relacionamento: <UserRound />,
143
+ Datas: <CalendarDays />,
138
144
  },
139
145
  }}
140
146
  />
@@ -177,7 +183,7 @@ render(
177
183
  action={docWorkspaceList}
178
184
  input={{}}
179
185
  cells={{
180
- name: (w) => (
186
+ name: (w: { name: string }) => (
181
187
  <a
182
188
  href="#"
183
189
  onClick={(e) => e.preventDefault()}
@@ -218,6 +224,65 @@ disponíveis e a quantidade de itens. Alterar busca, filtro, ordenação ou per
218
224
  página. Durante a primeira consulta, `DataState` apresenta o carregamento; em atualizações
219
225
  posteriores, a lista permanece visível com `aria-busy`.
220
226
 
227
+ ## Ocupar a altura disponível
228
+
229
+ Por padrão, a lista cresce com os resultados e a rolagem acontece na janela ou no contêiner que a
230
+ hospeda. Quando a coleção é a região principal de uma tela de altura fixa, a barra de filtros e o
231
+ rodapé podem continuar visíveis enquanto só os resultados rolam. Para isso, declare
232
+ `layout="fill"` na [`Page`](/ui/page) e componha a lista com `PageBody` e
233
+ [`Content`](/ui/content), como no exemplo abaixo. Role a tabela para ver o cabeçalho fixo.
234
+
235
+ ```tsx preview col
236
+ render(
237
+ <DocBrowserActionProvider>
238
+ <div className="h-80 w-full overflow-x-auto overflow-y-hidden rounded-lg border border-border">
239
+ <Page layout="fill">
240
+ <PageBody>
241
+ <Content level={1} variant="page">
242
+ <ContentHeader>
243
+ <ContentTitle>Workspaces</ContentTitle>
244
+ <ContentActions>
245
+ <Button>Criar workspace</Button>
246
+ </ContentActions>
247
+ </ContentHeader>
248
+ <ContentBody>
249
+ <ActionList action={docWorkspaceList} input={{}} />
250
+ </ContentBody>
251
+ </Content>
252
+ </PageBody>
253
+ </Page>
254
+ </div>
255
+ </DocBrowserActionProvider>,
256
+ );
257
+ ```
258
+
259
+ A página limita a própria altura à do contêiner que a hospeda, normalmente o shell, e `PageBody`,
260
+ `Content` e `ContentBody` repassam essa altura até a lista, que passa a usar `layout="fill"` sem que
261
+ você o declare. Em uma [`Presentation`](/ui/presentation) de lista, basta `layout="fill"` no
262
+ próprio `Presentation`. Só as regiões que contêm a lista mudam: uma seção irmã, como um resumo acima
263
+ da coleção, mantém a altura do próprio conteúdo. Os demais filhos de cada região que repassa a
264
+ altura se esticam à largura dela; dê `self-start` a um botão que deva manter a largura do conteúdo.
265
+
266
+ Com a altura limitada, a barra de filtros, o `beforeContent` e o rodapé mantêm a própria altura. A
267
+ região de resultados ocupa o espaço restante e rola nos dois eixos; o rodapé se mede pelo conteúdo
268
+ e mantém a mesma altura com e sem paginador. Na tabela, o cabeçalho fica fixo no topo da região que
269
+ rola, sobre a cor da superfície e com uma linha inferior, e a moldura dos estados de carregamento,
270
+ erro e vazio ocupa a altura disponível. Em outras visualizações e em `children`, a rolagem interna
271
+ continua, mas não existe cabeçalho fixo.
272
+
273
+ A lista só assume `fill` sozinha quando é filha direta de `PageBody`, `ContentBody` ou do corpo de
274
+ uma `Presentation`, e a altura só chega até ela se não houver um contêiner próprio entre esses
275
+ componentes no caminho. Nos demais casos, a decisão é sua:
276
+
277
+ - Para a lista manter a altura dos resultados, como em uma seção secundária cuja rolagem pertence ao
278
+ `PageBody`, declare `layout="flow"`.
279
+ - Um contêiner próprio entre `ContentBody` e a lista, como um `TabsContent`, não repassa a altura, e
280
+ a lista volta a `flow`. Declare `layout="fill"` na lista e dê ao contêiner as classes
281
+ `flex min-h-0 flex-1 flex-col`.
282
+ - Dentro de [`Card`](/ui/card), [`Dialog`](/ui/dialog), [`Drawer`](/ui/drawer) ou
283
+ [`Pane`](/ui/pane), a lista não herda o `fill` da página: a altura do corpo pertence à
284
+ superfície.
285
+
221
286
  ## Ações em lote
222
287
 
223
288
  `batch` acrescenta seleção à tabela. `can(item)` determina quais linhas podem participar: itens
@@ -250,6 +315,14 @@ fornece um `render`; `ActionList` preserva a mesma fonte de dados, filtros e est
250
315
  “Tabela” aparece quando existem colunas.
251
316
 
252
317
  ```tsx preview col
318
+ type Workspace = {
319
+ id: string;
320
+ name: string;
321
+ client: string;
322
+ status: "active" | "onboarding" | "paused";
323
+ agents: number;
324
+ };
325
+
253
326
  render(
254
327
  <DocBrowserActionProvider>
255
328
  <div className="w-full">
@@ -260,24 +333,20 @@ render(
260
333
  gallery: {
261
334
  label: "Galeria",
262
335
  icon: <LayoutGrid />,
263
- render: (workspaces) => (
336
+ render: (workspaces: Workspace[]) => (
264
337
  <div className="grid grid-cols-2 gap-3">
265
338
  {workspaces.map((w) => (
266
- <div
267
- key={w.id}
268
- className="rounded-lg border border-border p-3"
269
- >
270
- <div className="flex items-center gap-2">
271
- <span className="text-sm font-semibold">{w.name}</span>
272
- <DictionaryValue
273
- dict={docWorkspaceStatus}
274
- value={w.status}
275
- />
276
- </div>
277
- <p className="mt-1 text-xs text-muted-foreground">
278
- {w.agents} agentes.
279
- </p>
280
- </div>
339
+ <Card key={w.id}>
340
+ <CardHeader>
341
+ <CardTitle>{w.name}</CardTitle>
342
+ <CardDescription>
343
+ {w.agents === 1 ? "1 agente" : `${w.agents} agentes`}
344
+ </CardDescription>
345
+ <CardAction>
346
+ <DictionaryValue dict={docWorkspaceStatus} value={w.status} />
347
+ </CardAction>
348
+ </CardHeader>
349
+ </Card>
281
350
  ))}
282
351
  </div>
283
352
  ),
@@ -334,28 +403,42 @@ Convenção compacta (defaults omitidos): `?q=…&sort=chave:dir&status=…&view
334
403
 
335
404
  ## Composição dos itens
336
405
 
337
- Passe `children` para substituir a tabela por uma composição própria. A barra de filtros e os
338
- estados de carregamento, erro e vazio continuam sendo tratados por `ActionList`.
406
+ Passe `children` para substituir a tabela por uma composição própria, como uma lista de
407
+ [`Item`](/ui/item). A barra de filtros e os estados de carregamento, erro e vazio continuam sendo
408
+ tratados por `ActionList`. O contrato não leva o tipo do item até o componente: anote o parâmetro de
409
+ `children` (ou de `render`, em uma visualização) ou declare os genéricos, como em
410
+ `<ActionList<Input, Workspace>>`. Sem uma das duas formas, os itens chegam como `unknown`.
339
411
 
340
412
  ```tsx preview col
413
+ type Workspace = {
414
+ id: string;
415
+ name: string;
416
+ client: string;
417
+ status: "active" | "onboarding" | "paused";
418
+ agents: number;
419
+ };
420
+
341
421
  render(
342
422
  <DocBrowserActionProvider>
343
423
  <div className="w-full">
344
424
  <ActionList action={docWorkspaceList} input={{}}>
345
- {(workspaces) => (
346
- <div className="grid w-full grid-cols-2 gap-3">
347
- {workspaces.map((w) => (
348
- <div key={w.id} className="rounded-lg border border-border p-3">
349
- <div className="flex items-center gap-2">
350
- <span className="text-sm font-semibold">{w.name}</span>
351
- <DictionaryValue dict={docWorkspaceStatus} value={w.status} />
352
- </div>
353
- <p className="mt-1 text-xs text-muted-foreground">
354
- {w.agents} agentes.
355
- </p>
356
- </div>
425
+ {(workspaces: Workspace[]) => (
426
+ <ItemGroup variant="framed">
427
+ {workspaces.map((w, index) => (
428
+ <React.Fragment key={w.id}>
429
+ {index > 0 && <ItemSeparator />}
430
+ <Item>
431
+ <ItemHeader>
432
+ <ItemTitle>{w.name}</ItemTitle>
433
+ <ItemDescription>Cliente {w.client}.</ItemDescription>
434
+ </ItemHeader>
435
+ <ItemActions>
436
+ <DictionaryValue dict={docWorkspaceStatus} value={w.status} />
437
+ </ItemActions>
438
+ </Item>
439
+ </React.Fragment>
357
440
  ))}
358
- </div>
441
+ </ItemGroup>
359
442
  )}
360
443
  </ActionList>
361
444
  </div>
@@ -403,9 +486,9 @@ chamar a action.
403
486
 
404
487
  | Propriedade | Tipo | Padrão | Descrição |
405
488
  | ----------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
406
- | `action` | `ListAction<TInput, TItem>` | | A ListAction do Opus (kind list, output = item). A paginação envia `limit`/`page` e usa o `total` devolvido. |
407
- | `input` | `TInput` | | O ESCOPO BASE (ex.: { workspaceId }) — a toolbar soma por cima, nunca sobrescreve. |
408
- | `layout` | `'flow' \| 'fill'` | `'flow'` | Em `fill`, ocupa a altura disponível e mantém a rolagem dentro da região de resultados. |
489
+ | `action` | `ListActionLike` | | A ListAction do Opus (kind list, output = item). A paginação envia `limit`/`page` e usa o `total` devolvido. |
490
+ | `input` | `TInput` | | O escopo base, como `{ workspaceId }`. A toolbar acrescenta busca, filtros e paginação sem sobrescrever essas chaves. |
491
+ | `layout` | `'flow' \| 'fill'` | `'fill'` como filha direta de `PageBody`, `ContentBody` ou do corpo da `Presentation` em uma página `fill`; `'flow'` nos demais casos | Em `fill`, ocupa a altura de um ancestral limitado e rola somente a região de resultados; na tabela, o cabeçalho fica fixo. Sem ancestral limitado, equivale a `flow`. |
409
492
  | `cells` | `Record<string, (item) => ReactNode>` | | Células custom por cima das colunas do contrato (chave = column.key). |
410
493
  | `filterOptions` | `Record<string, SelectOption[]>` | | Opções de runtime para os filtros select/lookup (chave = nome do filtro). |
411
494
  | `filterDefaults` | `Record<string, string>` | | Valores de repouso aplicados à consulta; o `X` só aparece quando o filtro diverge deles e retorna a eles. |
@@ -415,7 +498,7 @@ chamar a action.
415
498
  | `period` | `{ basisFilter: string }` | | Reúne o período e o filtro inline indicado em um único controle. |
416
499
  | `views` | `Record<string, ActionListView<TItem>>` | | Visualizações alternativas `{ label, icon?, render }`; o seletor alterna entre a tabela e estas. |
417
500
  | `beforeContent` | `ReactNode` | | Conteúdo entre a toolbar e o corpo, como um resumo que reage ao recorte atual. |
418
- | `columns` | `ActionListColumn<TItem>[]` | | Tabela EXPLÍCITA — sobrepõe as colunas do contrato (escape hatch). |
501
+ | `columns` | `ActionListColumn<TItem>[]` | | Tabela explícita, que substitui as colunas declaradas no contrato. |
419
502
  | `children` | `(items, refetch) => ReactNode` | | Modo composição: layout livre; a toolbar segue. Tem precedência sobre columns. |
420
503
  | `batch` | `ActionListBatchAction[]` | | Ações em lote: `{ label, can?, run, confirm?: { title, message? }, destructive? }` — liga a multi-seleção. |
421
504
  | `rowId` | `(item) => string` | `item.id` | Identidade da linha para seleção. |
@@ -427,5 +510,5 @@ chamar a action.
427
510
  | `errorMessage` | `string` | `'Não foi possível carregar.'` | Título do aviso de erro; a ação de tentar de novo refaz a consulta. |
428
511
  | `retryLabel` | `string` | `'Tentar de novo'` | Nome acessível e tooltip da ação de recuperação. |
429
512
  | `onRowClick` | `(item: TItem) => void` | | Clique na linha (ex.: navegar para o detalhe) — só na tabela. |
430
- | `toolbarActions` | `ReactNode` | | Operações ligadas ao recorte atual, no fim da barra e separadas do grupo de controles. |
431
- | `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita). A partir de duas, entram num grupo com intervalo compacto; uma só fica solta, sem grupo. Cliques ali não disparam o `onRowClick`. |
513
+ | `toolbarActions` | `ReactNode` | | Operações ligadas ao recorte atual, no fim da barra e separadas do grupo de controles. Os nós são renderizados como recebidos: nome acessível e tooltip de uma ação somente com ícone ficam com o consumidor. |
514
+ | `rowActions` | `(item: TItem) => ReactNode` | | Ações por linha (coluna final, à direita). A partir de duas, entram em um grupo com intervalo compacto; uma só fica solta, sem grupo. Cliques ali não disparam o `onRowClick`. |
@@ -13,9 +13,11 @@ de tradução ou estiver ausente, o botão usa `action.name`.
13
13
 
14
14
  ## Confirmação declarada no contrato
15
15
 
16
- Quando a action declara `confirm`, o componente monta o diálogo e aplica o contexto `danger` se a
17
- operação for destrutiva. Use a propriedade `confirm` somente quando esta ocorrência precisar de uma
18
- mensagem diferente da declarada no contrato.
16
+ Quando a action declara `confirm`, o componente monta o diálogo antes de executar. Com
17
+ `confirm.destructive: true` no contrato, o gatilho e o botão de confirmação usam o contexto
18
+ `danger`. Use a propriedade `confirm` somente quando esta ocorrência precisar de uma mensagem
19
+ diferente da declarada no contrato; ela substitui os textos, mas o caráter destrutivo continua
20
+ vindo do contrato.
19
21
 
20
22
  ```tsx preview
21
23
  <DocBrowserActionProvider>
@@ -41,20 +43,35 @@ confirm: {
41
43
  Com `icon`, o botão exibe somente o ícone no quadrado `icon-xs` da escala (1.5rem, a ação que mora
42
44
  dentro de uma linha ou card) e usa `label`, ou `action.label`, no tooltip e no nome acessível. Uma
43
45
  composição que peça mais presença, como a barra do `PageHeader`, declara `size="icon"`. O clique
44
- não aciona o item clicável ao redor. `itemLabel` identifica o registro na mensagem de confirmação.
46
+ não aciona o item clicável ao redor.
47
+
48
+ Em uma lista, várias linhas repetem a mesma ação. Inclua o alvo em `label`, como `Excluir Unidade
49
+ Centro`, para que cada botão tenha um nome acessível distinto. `itemLabel` identifica o registro na
50
+ pergunta de confirmação.
45
51
 
46
52
  ```tsx
53
+ // No contrato:
54
+ confirm: {
55
+ title: 'Excluir a unidade?',
56
+ message: 'Os agendamentos desta unidade também serão removidos.',
57
+ confirmLabel: 'Excluir',
58
+ destructive: true,
59
+ }
60
+
61
+ // Na linha da lista:
47
62
  <ActionTrigger
48
63
  action={unitDeleteContract}
49
64
  input={{ id: unit.id }}
50
65
  icon={<Trash2 />}
51
- label="Excluir"
66
+ label={`Excluir ${unit.name}`}
52
67
  itemLabel={unit.name}
53
68
  />
54
69
  ```
55
70
 
56
- > `ActionTrigger` substitui o antigo `DeleteButton`. Ao migrar, declare `confirm` no contrato para
57
- > preservar a confirmação que o componente anterior sempre apresentava.
71
+ Sem `confirmLabel` no contrato ou `actionLabel` na propriedade `confirm`, o botão de confirmação
72
+ usa o `label` da action, como `Excluir`. O nome longo passado em `label` serve à linha; na pergunta,
73
+ o alvo já aparece por `itemLabel`. O `label` do gatilho só nomeia o botão de confirmação quando a
74
+ action não declara `label`.
58
75
 
59
76
  ## Erro que a pessoa entende
60
77
 
@@ -80,9 +97,9 @@ um atalho, um arrastar, um item de menu.
80
97
  |---|---|---|---|
81
98
  | `action` | `SimpleContract<TInput, TData>` | | A SimpleAction do Opus — label, messages e confirm vêm do contrato. |
82
99
  | `input` | `TInput` | | O que a action recebe — geralmente { id }. |
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. |
100
+ | `label` | `string` | `action.label` quando é texto; senão `action.name` | Texto do botão ou, com `icon`, seu nome acessível e tooltip. Só vira o botão de confirmação quando a action não declara `label` e nem o contrato nem `confirm` declaram outro rótulo. |
101
+ | `context / variant / size` | `do Button` | gatilho: `primary`/`solid`; `danger` quando `action.confirm.destructive` é `true`; 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 `action.confirm.destructive` é `true`, senão a prop `context` do gatilho (ou `primary`). |
102
+ | `confirm` | `{ title, body?, actionLabel?, cancelLabel? }` | | Confirmação para esta ocorrência; substitui os textos do `confirm` do contrato. Sem `actionLabel`, o botão de confirmar usa `action.label`. O contexto `danger` continua dependendo de `action.confirm.destructive`. |
86
103
  | `disabled` | `boolean` | `false` | Desabilita o botão independentemente da execução em andamento. |
87
104
  | `onSuccess` | `(data: TData) => void` | | Pós-sucesso (cache já invalidado pelo action.invalidates). |
88
105
  | `onLoadingChange` | `(loading: boolean) => void` | | Informa início e fim da execução a quem coordena bloqueio entre várias actions. |
@@ -2,7 +2,8 @@
2
2
 
3
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
- erro. O consumidor compõe somente o conteúdo disponível.
5
+ erro. O consumidor compõe somente o conteúdo disponível, aqui com [`Detail`](/ui/detail). Para uma
6
+ coleção, use [`ActionList`](/ui/action-list).
6
7
 
7
8
  ```tsx preview col
8
9
  const [id, setId] = useState('empresa-x')
@@ -10,26 +11,25 @@ const [id, setId] = useState('empresa-x')
10
11
  render(
11
12
  <DocBrowserActionProvider>
12
13
  <div className="w-full space-y-3">
13
- <div className="flex flex-wrap gap-2">
14
+ <ButtonGroup mode="spaced">
14
15
  <Button variant="outline" size="sm" onClick={() => setId('empresa-x')}>Empresa X</Button>
15
16
  <Button variant="outline" size="sm" onClick={() => setId('empresa-y')}>Empresa Y</Button>
16
17
  <Button variant="outline" size="sm" onClick={() => setId('sessao-fantasma')}>Inexistente (erro)</Button>
17
- </div>
18
- <div className="rounded-lg border border-border p-4">
18
+ </ButtonGroup>
19
+ <Card className="p-4">
19
20
  <ActionView action={docWorkspaceView} input={{ id }}>
20
21
  {(ws) => (
21
- <div className="space-y-1.5">
22
- <div className="flex items-center gap-2">
23
- <h3 className="text-sm font-semibold">{ws.name}</h3>
24
- <DictionaryValue dict={docWorkspaceStatus} value={ws.status} />
25
- </div>
26
- <p className="text-sm text-muted-foreground">
27
- Cliente {ws.client} · {ws.agents} agentes vinculados.
28
- </p>
29
- </div>
22
+ <DetailGroup columns={3}>
23
+ <DetailField label="Workspace" value={ws.name} />
24
+ <DetailField label="Cliente" value={ws.client} />
25
+ <DetailField
26
+ label="Status"
27
+ value={<DictionaryValue dict={docWorkspaceStatus} value={ws.status} />}
28
+ />
29
+ </DetailGroup>
30
30
  )}
31
31
  </ActionView>
32
- </div>
32
+ </Card>
33
33
  </div>
34
34
  </DocBrowserActionProvider>,
35
35
  )