@softize/opus 18.0.1 → 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 (129) hide show
  1. package/CHANGELOG.md +105 -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/0017-ui-assumes-a-fixed-desktop-layout.md +54 -0
  11. package/docs/adr/{0012-modal-header-only-names-the-surface.md → 0018-modal-header-only-names-the-surface.md} +4 -1
  12. package/docs/adr/0019-productive-surfaces-use-compact-density.md +65 -0
  13. package/docs/code-style.md +2 -2
  14. package/docs/consumer-upgrade-propagation.md +1 -1
  15. package/docs/data-products.md +5 -3
  16. package/docs/protocol.md +6 -6
  17. package/docs/relative-unit-scale.md +9 -2
  18. package/docs/releasing.md +28 -4
  19. package/package.json +1 -1
  20. package/registry/skills/build-opus-ui/references/evaluations.md +3 -1
  21. package/registry/skills/build-opus-ui/references/ui-patterns.md +20 -4
  22. package/src/auth/drivers/jwt.ts +2 -1
  23. package/src/core/runtime.ts +32 -5
  24. package/src/core/types.ts +16 -7
  25. package/src/mcp/index.ts +13 -1
  26. package/src/ui/components/patterns/action-list-dialog.tsx +2 -2
  27. package/src/ui/components/patterns/content-header.tsx +2 -2
  28. package/src/ui/components/patterns/form-dialog.tsx +7 -2
  29. package/src/ui/components/patterns/form.tsx +1 -1
  30. package/src/ui/components/patterns/list.tsx +239 -47
  31. package/src/ui/components/patterns/presentation.tsx +7 -5
  32. package/src/ui/components/patterns/sidebar.tsx +1 -1
  33. package/src/ui/components/patterns/state-surface.tsx +2 -2
  34. package/src/ui/components/patterns/surface-header.tsx +4 -4
  35. package/src/ui/components/primitives/alert.tsx +2 -2
  36. package/src/ui/components/primitives/breadcrumb.tsx +1 -1
  37. package/src/ui/components/primitives/button-group.tsx +1 -1
  38. package/src/ui/components/primitives/button.tsx +3 -3
  39. package/src/ui/components/primitives/calendar.tsx +1 -1
  40. package/src/ui/components/primitives/card.tsx +1 -1
  41. package/src/ui/components/primitives/close-button.tsx +40 -0
  42. package/src/ui/components/primitives/detail.tsx +68 -30
  43. package/src/ui/components/primitives/dialog.tsx +36 -21
  44. package/src/ui/components/primitives/drawer.tsx +26 -19
  45. package/src/ui/components/primitives/empty-value.tsx +3 -3
  46. package/src/ui/components/primitives/empty.tsx +1 -1
  47. package/src/ui/components/primitives/field.tsx +12 -12
  48. package/src/ui/components/primitives/icon-picker.tsx +1 -1
  49. package/src/ui/components/primitives/input-group.tsx +1 -1
  50. package/src/ui/components/primitives/input.tsx +2 -2
  51. package/src/ui/components/primitives/item.tsx +5 -5
  52. package/src/ui/components/primitives/pagination.tsx +4 -4
  53. package/src/ui/components/primitives/radio-group.tsx +1 -1
  54. package/src/ui/components/primitives/select.tsx +3 -3
  55. package/src/ui/components/primitives/table.tsx +26 -17
  56. package/src/ui/components/primitives/tabs.tsx +80 -23
  57. package/src/ui/components/primitives/textarea.tsx +1 -1
  58. package/src/ui/components/primitives/toggle-group.tsx +9 -2
  59. package/src/ui/docs/content/action-form-dialog.md +11 -4
  60. package/src/ui/docs/content/action-form.md +13 -3
  61. package/src/ui/docs/content/action-list-dialog.md +5 -7
  62. package/src/ui/docs/content/action-list.md +53 -5
  63. package/src/ui/docs/content/action-trigger.md +9 -5
  64. package/src/ui/docs/content/action-view.md +12 -8
  65. package/src/ui/docs/content/actions.md +36 -13
  66. package/src/ui/docs/content/ai.md +26 -7
  67. package/src/ui/docs/content/alert.md +6 -3
  68. package/src/ui/docs/content/aspect-ratio.md +2 -2
  69. package/src/ui/docs/content/auth.md +25 -10
  70. package/src/ui/docs/content/avatar.md +1 -1
  71. package/src/ui/docs/content/badge.md +2 -2
  72. package/src/ui/docs/content/breadcrumb.md +3 -2
  73. package/src/ui/docs/content/button.md +33 -8
  74. package/src/ui/docs/content/calendar.md +1 -1
  75. package/src/ui/docs/content/card.md +1 -1
  76. package/src/ui/docs/content/carousel.md +14 -3
  77. package/src/ui/docs/content/chat.md +1 -1
  78. package/src/ui/docs/content/cli.md +13 -7
  79. package/src/ui/docs/content/command.md +34 -2
  80. package/src/ui/docs/content/composer.md +1 -1
  81. package/src/ui/docs/content/content.md +5 -4
  82. package/src/ui/docs/content/customization.md +12 -2
  83. package/src/ui/docs/content/cycle.md +7 -5
  84. package/src/ui/docs/content/data-state.md +6 -5
  85. package/src/ui/docs/content/data.md +3 -3
  86. package/src/ui/docs/content/detail.md +12 -10
  87. package/src/ui/docs/content/dialog.md +14 -7
  88. package/src/ui/docs/content/dictionary-value.md +1 -1
  89. package/src/ui/docs/content/dock.md +23 -2
  90. package/src/ui/docs/content/dot.md +0 -2
  91. package/src/ui/docs/content/drawer.md +7 -4
  92. package/src/ui/docs/content/empty-value.md +4 -4
  93. package/src/ui/docs/content/empty.md +1 -4
  94. package/src/ui/docs/content/events.md +1 -1
  95. package/src/ui/docs/content/field.md +21 -12
  96. package/src/ui/docs/content/getting-started.md +4 -2
  97. package/src/ui/docs/content/icon-picker.md +2 -2
  98. package/src/ui/docs/content/input-otp.md +2 -0
  99. package/src/ui/docs/content/input.md +2 -3
  100. package/src/ui/docs/content/item.md +6 -3
  101. package/src/ui/docs/content/kbd.md +2 -1
  102. package/src/ui/docs/content/mcp.md +10 -4
  103. package/src/ui/docs/content/menu.md +27 -0
  104. package/src/ui/docs/content/page.md +20 -6
  105. package/src/ui/docs/content/pagination.md +9 -2
  106. package/src/ui/docs/content/popover.md +2 -2
  107. package/src/ui/docs/content/presentation.md +48 -47
  108. package/src/ui/docs/content/progress.md +2 -6
  109. package/src/ui/docs/content/runtime.md +8 -5
  110. package/src/ui/docs/content/scheduler.md +1 -1
  111. package/src/ui/docs/content/select.md +13 -8
  112. package/src/ui/docs/content/sidebar.md +3 -2
  113. package/src/ui/docs/content/skeleton.md +1 -1
  114. package/src/ui/docs/content/slider.md +4 -4
  115. package/src/ui/docs/content/spinner.md +3 -3
  116. package/src/ui/docs/content/tabs.md +22 -12
  117. package/src/ui/docs/content/testing.md +4 -2
  118. package/src/ui/docs/content/toast.md +5 -6
  119. package/src/ui/docs/content/toggle.md +37 -0
  120. package/src/ui/docs/content/tokens.md +45 -2
  121. package/src/ui/docs/content/tooltip.md +4 -3
  122. package/src/ui/docs/content/truncate.md +3 -2
  123. package/src/ui/docs/content/ui.md +3 -1
  124. package/src/ui/docs/content/upgrading.md +43 -13
  125. package/src/ui/docs/doc-client.tsx +1 -1
  126. package/src/ui/docs/registry.tsx +30 -5
  127. package/src/ui/meta.ts +4 -4
  128. package/src/ui/react.tsx +1 -0
  129. package/src/ui/theme.css +3 -0
@@ -56,8 +56,11 @@ ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader`
56
56
  `*Content` permanece reservado a raízes técnicas ou painéis cujo papel não é o corpo de uma
57
57
  estrutura, como `DialogContent`, `PopoverContent` e `TabsContent`.
58
58
  - Componentes compartilhados não impõem margem externa; páginas e shells compõem layout.
59
- - A fonte raiz pertence ao navegador e à aplicação. Medidas escaláveis usam `rem` ou a escala
60
- relativa do Tailwind; `px` fica restrito a hairlines e compensações ligadas a essas bordas.
59
+ - A UI assume layout desktop fixo e largura mínima suportada de `64rem`. Não introduzir
60
+ breakpoints de viewport ou container queries isoladas; uma futura responsividade precisa começar
61
+ pelo shell e formar um contrato sistêmico. A fonte raiz pertence ao navegador e à aplicação.
62
+ Medidas escaláveis usam `rem` ou a escala relativa do Tailwind; `px` fica restrito a hairlines
63
+ e compensações ligadas a essas bordas.
61
64
  - Superfície semântica e foreground são um par local (`bg-card text-card-foreground`,
62
65
  `bg-popover text-popover-foreground`); herança do foreground global não substitui o par.
63
66
  - Forma usa a escala `rounded-*`; nome de componente não cria uma segunda escala de radius.
@@ -70,6 +73,11 @@ ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader`
70
73
  - `ActionFilterBar` renderiza busca, filtros, período e ações declarados por uma list action
71
74
  quando a tela precisa da toolbar sem entregar os resultados a `ActionList`. Manter seu
72
75
  `state` e `onStateChange` ligados à mesma projeção de URL usada pela superfície.
76
+ - Filtros de list action declaram onde aparecem com `placement`: `inline` fica na barra;
77
+ `advanced` vai para o painel de filtros avançados, agrupado por `section` e distribuído em até
78
+ três colunas com `advancedFilters.columns`; `external` continua no estado navegável e no input,
79
+ mas a tela apresenta o controle por conta própria. `advanced: true` segue aceito só durante a
80
+ migração; em código novo, declarar `placement`.
73
81
  - `ActionForm` mantém validação, execução e estados do contrato nos dois modos: sem `children`,
74
82
  renderiza os campos declarados; com `children`, o consumidor diagrama `ActionFormField` e
75
83
  controles customizados pelo contexto. `ActionFormCard` e `ActionFormDialog` acrescentam a
@@ -78,8 +86,13 @@ ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader`
78
86
  a ação principal mantém `solid`. Criação e edição comuns preservam o rótulo padrão `Salvar`;
79
87
  `submitLabel` fica para efeitos específicos, como `Renomear` ou `Criar nova versão`. Em
80
88
  `Dialog mode="alert"`, a saída segura também usa `outline`.
89
+ - `DialogFooter` e `DrawerFooter` cuidam só da faixa. Agrupar e distribuir as decisões cabe a um
90
+ `ButtonGroup` dentro deles, com `distribution="equal"` para o footer 50/50; botões soltos no
91
+ footer não são distribuídos. `ActionFormDialog` já compõe isso: por padrão o footer acompanha o
92
+ conteúdo com o cancelamento em `ghost`, e `footerDistribution="equal"` divide a faixa e passa o
93
+ cancelamento a `outline`.
81
94
  - Cabeçalhos de `Dialog` e `Drawer` nomeiam a superfície com o título e organizam suas ações. O close
82
- padrão é uma action ghost somente com ícone no final do header; não crie uma posição flutuante
95
+ padrão é o `CloseButton` (`neutral`, `subtle`, pílula) no final do header, sem posição flutuante
83
96
  alternativa. Não preencher uma segunda linha por hábito. Consequência, restrição ou instrução que
84
97
  realmente mude a tarefa entra no início de `DialogBody`/`DrawerBody`, como texto ou `Alert`;
85
98
  wrappers usam `intro` e a API imperativa usa `body`. Relacione texto conciso por
@@ -90,7 +103,10 @@ ContentDescription, ContentMeta, ContentActions) + ContentBody`. `ContentHeader`
90
103
  ou views nomeadas. Usar `ActionFilterBar` isoladamente só quando outra superfície assumir a
91
104
  renderização dos resultados. Colunas numéricas não recebem alinhamento ou tipografia por
92
105
  inferência do tipo: declarar `className` no header e na célula quando o produto exigir, de
93
- modo que números preservem o alinhamento natural por padrão.
106
+ modo que números preservem o alinhamento natural por padrão. O valor principal de uma célula
107
+ preserva `text-sm` e o foreground da superfície; `text-xs text-muted-foreground` fica restrito
108
+ a metadado subordinado a outro valor na mesma célula. Cabeçalhos e `EmptyValue` continuam muted;
109
+ não reduzir nem atenuar uma coluna inteira por ela ser técnica, temporal ou menos destacada.
94
110
  - `ItemGroup` representa uma coleção e pode oferecer moldura explícita; `Item` já carrega
95
111
  semântica de item de lista. Preferir essa composição a uma sequência visual sem estrutura.
96
112
  - `Empty` com moldura tracejada comunica uma região disponível para criar ou vincular algo.
@@ -34,7 +34,8 @@ import type { AuthAdapter, CanFn, User } from '../../core/index.ts'
34
34
  // =============================================================================
35
35
 
36
36
  export interface JwtAuthOptions<P extends JwtPayload = JwtPayload> {
37
- /** Secret HMAC ou public key. Pode ser string sync ou async resolver. */
37
+ /** Secret HMAC ou public key, fixo ou por resolver (sync ou async). O resolver é chamado
38
+ * sem argumentos: a escolha da chave pelo `kid` do token ainda não é suportada. */
38
39
  secret: string | Buffer | ((kid?: string) => Promise<string | Buffer> | string | Buffer)
39
40
 
40
41
  /** Algorithms aceitos (default ['HS256']). */
@@ -75,6 +75,23 @@ import type {
75
75
 
76
76
  /* eslint-disable @typescript-eslint/no-explicit-any */
77
77
 
78
+ /**
79
+ * Erro devolvido a um cliente de IA que chama uma tool fora do conjunto exposto.
80
+ *
81
+ * Quem executa tools (o agente co-locado e o servidor MCP) só pode rodar o que `aiTools()`
82
+ * anunciou. O modelo, ou um prompt injetado, pode emitir qualquer nome; sem este corte, uma
83
+ * action sem `ai.enabled` — inclusive destrutiva — rodaria dentro da autorização do contexto.
84
+ * A resposta é a mesma para nome inexistente e para action registrada mas não exposta, para
85
+ * que o cliente não descubra quais actions existem.
86
+ */
87
+ export function aiToolNotFound(name: string): ReturnType<typeof error> {
88
+ return error({
89
+ code: 'runtime.action_not_found',
90
+ category: 'not_found',
91
+ message: `Tool "${name}" não está disponível.`,
92
+ })
93
+ }
94
+
78
95
  /** Normaliza o `ai` da action (`boolean | AIConfig`) → config, ou null se não exposta à IA. */
79
96
  function normalizeAiConfig(ai: boolean | AIConfig | undefined): AIConfig | null {
80
97
  if (ai === undefined || ai === false) return null
@@ -620,10 +637,10 @@ export class Runtime {
620
637
  // — 1. Validate input ————————————————————————————————————————————————
621
638
  const validatedInput = await this.validate(action.input, input, 'input')
622
639
 
623
- // — 2. Load —————————————————————————————————————————————————————————
624
- const loaded = await this.runLoaders(action, ctx, validatedInput)
625
-
626
- // 3. Authenticate (public-or-user) ——————————————————————————————————
640
+ // — 2. Authenticate (public-or-user) ——————————————————————————————————
641
+ // Antes dos loaders: eles consultam dados, e um chamador anônimo não pode nem disparar essas
642
+ // consultas nem distinguir, pelo erro de um loader, se um registro existe. A autenticação
643
+ // não depende de nada carregado; a autorização, sim, e por isso vem depois.
627
644
  if (action.public !== true && ctx.user === null) {
628
645
  throw error({
629
646
  code: 'auth.unauthenticated',
@@ -632,6 +649,9 @@ export class Runtime {
632
649
  })
633
650
  }
634
651
 
652
+ // — 3. Load —————————————————————————————————————————————————————————
653
+ const loaded = await this.runLoaders(action, ctx, validatedInput)
654
+
635
655
  // — 4. Authorize ——————————————————————————————————————————————————————
636
656
  if (action.authorize !== undefined) {
637
657
  const authorizeFn = this.compileAuthorize(action.authorize)
@@ -815,6 +835,7 @@ export class Runtime {
815
835
  for (const action of this.actions.values()) {
816
836
  const cfg = normalizeAiConfig(action.ai)
817
837
  if (cfg === null) continue
838
+ const dataProducts = [...(this.actionDataProducts.get(action.name) ?? [])].sort()
818
839
  tools.push({
819
840
  name: action.name,
820
841
  ...(action.label !== undefined
@@ -822,7 +843,12 @@ export class Runtime {
822
843
  : {}),
823
844
  description: cfg.description ?? action.description ?? action.name,
824
845
  inputSchema: action.input,
825
- metadata: { dataProducts: [...(this.actionDataProducts.get(action.name) ?? [])].sort() },
846
+ metadata: {
847
+ dataProducts,
848
+ dataProductLabels: Object.fromEntries(
849
+ dataProducts.map((id) => [id, this.dataProducts.get(id)?.label ?? id]),
850
+ ),
851
+ },
826
852
  })
827
853
  }
828
854
  this.aiToolsCache = tools
@@ -842,6 +868,7 @@ export class Runtime {
842
868
  ...opts,
843
869
  tools,
844
870
  execute: async (name, toolInput) => {
871
+ if (!tools.some((tool) => tool.name === name)) return { error: aiToolNotFound(name) }
845
872
  const cfg = normalizeAiConfig(this.actions.get(name)?.ai)
846
873
  if (cfg !== null && (cfg.destructive === true || cfg.requiresConfirmation === true)) {
847
874
  const approved = opts?.confirm ? await opts.confirm({ name, input: toolInput }) : false
package/src/core/types.ts CHANGED
@@ -602,8 +602,12 @@ export interface FilterSpec {
602
602
  operators?: FilterOp[]
603
603
  path?: string
604
604
 
605
- /** Filtro AVANÇADO: sai da barra inline e vai pro modal "Filtros" (com contador de
606
- * ativos no botão). Os não-avançados rendem inline na toolbar da listagem. */
605
+ /** Onde o filtro é controlado na interface. `external` mantém o filtro no contrato,
606
+ * no estado navegável e no input efetivo, mas deixa sua apresentação para outra
607
+ * superfície — por exemplo, cards de status acima da lista. */
608
+ placement?: 'inline' | 'advanced' | 'external'
609
+
610
+ /** @deprecated Use `placement: 'advanced'`. Mantido durante a migração dos contratos. */
607
611
  advanced?: boolean
608
612
 
609
613
  section?: string
@@ -616,9 +620,9 @@ export interface FilterSpec {
616
620
 
617
621
  /**
618
622
  * Coluna declarativa de uma `ListAction` — a UI (ActionList) deriva a tabela
619
- * daqui; células custom entram POR CIMA na UI (prop `cells`, chave = key). É a base
620
- * do futuro column picker (o usuário escolher colunas): a lista completa vive no
621
- * contrato, `hidden` marca as que nascem fora.
623
+ * daqui; células custom entram POR CIMA na UI (prop `cells`, chave = key). O seletor
624
+ * de colunas parte desta lista: ela vive completa no contrato, e `hidden` marca as que
625
+ * nascem fora.
622
626
  */
623
627
  export interface ListColumnSpec {
624
628
  /** Chave do item (`Out`) que a coluna mostra. */
@@ -1264,7 +1268,12 @@ export interface AiTool {
1264
1268
  description: string
1265
1269
  inputSchema: unknown
1266
1270
  /** Metadados de governança para o host; não fazem parte do prompt nem concedem acesso. */
1267
- metadata?: { dataProducts?: string[] }
1271
+ metadata?: {
1272
+ /** Identificadores técnicos dos Produtos de Dados relacionados. */
1273
+ dataProducts?: string[]
1274
+ /** Labels humanas por identificador; ausente para runtimes legados. */
1275
+ dataProductLabels?: Record<string, string>
1276
+ }
1268
1277
  }
1269
1278
 
1270
1279
  /** Turno de conversa (histórico multi-turn do chat). */
@@ -1376,7 +1385,7 @@ export interface ScheduleDef {
1376
1385
  cron?: string
1377
1386
  /** Shorthand interval — ex: '1h', '30m', '15s'. */
1378
1387
  every?: string
1379
- /** Timezone IANA — ex: 'America/Sao_Paulo'. Default: UTC. */
1388
+ /** Timezone IANA — ex: 'America/Sao_Paulo'. Sem ela, vale a hora local do processo. */
1380
1389
  timezone?: string
1381
1390
 
1382
1391
  /** Input passado ao runtime.execute(); estático ou dinâmico. */
package/src/mcp/index.ts CHANGED
@@ -14,6 +14,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
14
14
  import { zodToJsonSchema } from 'zod-to-json-schema'
15
15
  import type { ContextBase, Runtime } from '../core/index.ts'
16
16
  import { readPackageVersion } from '../core/package-version.ts'
17
+ import { aiToolNotFound } from '../core/runtime.ts'
17
18
 
18
19
  export interface OpusMcpOptions {
19
20
  name?: string
@@ -47,11 +48,22 @@ export function createOpusMcpServer(runtime: Runtime, opts: OpusMcpOptions = {})
47
48
  ...(t.title !== undefined ? { title: t.title } : {}),
48
49
  description: t.description,
49
50
  inputSchema: toJsonSchema(t.inputSchema) as { type: 'object' },
50
- _meta: { 'com.softize.opus/data-products': t.metadata?.dataProducts ?? [] },
51
+ _meta: {
52
+ 'com.softize.opus/data-products': t.metadata?.dataProducts ?? [],
53
+ 'com.softize.opus/data-product-labels': t.metadata?.dataProductLabels ?? {},
54
+ },
51
55
  })),
52
56
  }))
53
57
 
54
58
  server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
59
+ // Executa só o que ListTools anunciou. Sem este corte, qualquer action registrada — sem
60
+ // `ai.enabled`, inclusive destrutiva — rodaria dentro da autorização do contexto.
61
+ if (!runtime.aiTools().some((tool) => tool.name === req.params.name)) {
62
+ return {
63
+ content: [{ type: 'text', text: JSON.stringify({ error: aiToolNotFound(req.params.name) }) }],
64
+ isError: true,
65
+ }
66
+ }
55
67
  const base = opts.resolveContext ? await opts.resolveContext(extra) : ANON
56
68
  const result = await runtime.execute(req.params.name, req.params.arguments ?? {}, base)
57
69
  const payload = result.ok ? result.data : { error: result.error }
@@ -48,7 +48,7 @@ export interface ActionListDialogProps<
48
48
  empty?: (items: TItem[]) => boolean
49
49
  /** Carga EXTRA agregada à do fetch (ex.: a query irmã que os children precisam). */
50
50
  loading?: boolean
51
- /** Classes do DialogContent (largura). Default: sm:max-w-3xl (lista pede espaço). */
51
+ /** Classes do DialogContent (largura). Default: max-w-3xl (lista pede espaço). */
52
52
  className?: string
53
53
  /** O layout dos itens (cards/linhas) — os estados já saíram daqui. */
54
54
  children: (items: TItem[], refetch: () => Promise<void>) => ReactNode
@@ -78,7 +78,7 @@ export function ActionListDialog<
78
78
  return (
79
79
  <Dialog open={open} onOpenChange={onOpenChange}>
80
80
  <DialogContent
81
- className={cn('sm:max-w-3xl', className)}
81
+ className={cn('max-w-3xl', className)}
82
82
  aria-describedby={intro === undefined ? undefined : introId}
83
83
  >
84
84
  <DialogHeader>
@@ -50,7 +50,7 @@ interface ContentBaseProps extends Omit<
50
50
 
51
51
  interface ContentShorthandProps extends ContentBaseProps {
52
52
  title: ReactNode;
53
- /** Total de itens ao lado do título o mesmo `count` de Page. */
53
+ /** Total de itens ao lado do título da seção. A página não carrega contador (ADR 0009). */
54
54
  count?: number;
55
55
  description?: ReactNode;
56
56
  actions?: ReactNode;
@@ -171,7 +171,7 @@ export function ContentHeader({
171
171
  }}
172
172
  className={cn(
173
173
  content.variant === "page" &&
174
- "flex-row items-center justify-start gap-3 sm:flex-row sm:items-center sm:justify-start",
174
+ "flex-row items-center justify-start gap-3",
175
175
  className,
176
176
  )}
177
177
  {...props}
@@ -29,6 +29,8 @@ export interface ActionFormDialogProps<
29
29
  title: string;
30
30
  /** Conteúdo relevante apresentado antes dos campos, como texto ou Alert. */
31
31
  intro?: ReactNode;
32
+ /** Distribui as ações pela largura disponível somente quando as decisões pedem peso equivalente. */
33
+ footerDistribution?: "content" | "equal";
32
34
  }
33
35
 
34
36
  export function ActionFormDialog<
@@ -42,6 +44,7 @@ export function ActionFormDialog<
42
44
  onSuccess,
43
45
  onCancel,
44
46
  cancelVariant,
47
+ footerDistribution = "content",
45
48
  ...rest
46
49
  }: ActionFormDialogProps<TInput, TData>) {
47
50
  const introId = useId();
@@ -72,7 +75,7 @@ export function ActionFormDialog<
72
75
  )}
73
76
  footer={(actions) => (
74
77
  <DialogFooter>
75
- <ButtonGroup mode="spaced" distribution="equal">
78
+ <ButtonGroup mode="spaced" distribution={footerDistribution}>
76
79
  {actions}
77
80
  </ButtonGroup>
78
81
  </DialogFooter>
@@ -82,7 +85,9 @@ export function ActionFormDialog<
82
85
  onOpenChange(false);
83
86
  }}
84
87
  onCancel={onCancel ?? (() => onOpenChange(false))}
85
- cancelVariant={cancelVariant ?? "outline"}
88
+ cancelVariant={
89
+ cancelVariant ?? (footerDistribution === "equal" ? "outline" : "ghost")
90
+ }
86
91
  />
87
92
  </DialogContent>
88
93
  </Dialog>
@@ -702,7 +702,7 @@ export interface ActionFormProps<
702
702
  onSuccess?: (data: TData) => void;
703
703
  submitLabel?: string;
704
704
  cancelLabel?: string;
705
- /** Tratamento visual do cancelamento. Use outline quando ele dividir o footer com a ação principal. */
705
+ /** Tratamento visual do cancelamento. `ghost` por padrão; `outline` quando o footer distribui as ações por igual. */
706
706
  cancelVariant?: Extract<ButtonVariant, "ghost" | "outline">;
707
707
  onCancel?: () => void;
708
708
  /** Bloqueia campos e ações sem desmontar o formulário. */