@softize/opus 13.0.0 → 13.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 (138) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/cli-shared.mjs +131 -0
  6. package/bin/lib/db.mjs +16 -74
  7. package/bin/lib/gen-openapi.mjs +3 -3
  8. package/bin/lib/gen-runner.mjs +1 -1
  9. package/bin/lib/gen.mjs +14 -69
  10. package/bin/lib/mcp.mjs +3 -1
  11. package/bin/lib/seed.mjs +5 -62
  12. package/docs/ownership-vs-shadcn-lock.md +2 -3
  13. package/docs/protocol.md +7 -7
  14. package/docs/releasing.md +8 -2
  15. package/package.json +7 -3
  16. package/registry/templates/app/package.json +1 -1
  17. package/registry/templates/app/src/main.tsx +4 -4
  18. package/src/audit/drivers/console.ts +1 -0
  19. package/src/auth/drivers/better-auth.ts +1 -0
  20. package/src/auth/drivers/jwt.ts +1 -0
  21. package/src/cache/drivers/memory.ts +1 -0
  22. package/src/client/drivers/fetch.ts +2 -1
  23. package/src/core/actions.ts +6 -1
  24. package/src/core/audit.ts +9 -3
  25. package/src/core/contracts.ts +7 -0
  26. package/src/core/domain.ts +1 -1
  27. package/src/core/errors.ts +18 -15
  28. package/src/core/index.ts +4 -2
  29. package/src/core/package-version.ts +26 -0
  30. package/src/core/reactions.ts +1 -1
  31. package/src/core/runtime.ts +33 -23
  32. package/src/core/schedules.ts +1 -1
  33. package/src/core/types.ts +2 -2
  34. package/src/dsl/eval.ts +2 -2
  35. package/src/dsl/kysely.ts +2 -2
  36. package/src/dsl/loads.ts +1 -1
  37. package/src/dsl/parser.ts +5 -5
  38. package/src/events/drivers/mitt.ts +1 -0
  39. package/src/mcp/index.ts +2 -1
  40. package/src/observability/drivers/opentelemetry.ts +1 -0
  41. package/src/queue/drivers/bullmq.ts +3 -3
  42. package/src/scheduler/drivers/node-cron.ts +3 -2
  43. package/src/scheduler/every.ts +7 -7
  44. package/src/schema/openapi.ts +3 -3
  45. package/src/seed/index.ts +29 -0
  46. package/src/server/drivers/fastify.ts +5 -2
  47. package/src/server/drivers/node.ts +9 -6
  48. package/src/server/index.ts +3 -1
  49. package/src/storage/drivers/fs.ts +1 -0
  50. package/src/testing/index.ts +3 -3
  51. package/src/ui/components/patterns/confirm.tsx +2 -2
  52. package/src/ui/components/patterns/content-header.tsx +7 -1
  53. package/src/ui/components/patterns/data-state.tsx +1 -1
  54. package/src/ui/components/patterns/dock.tsx +20 -3
  55. package/src/ui/components/patterns/form.tsx +12 -8
  56. package/src/ui/components/patterns/list.tsx +4 -4
  57. package/src/ui/components/patterns/page.tsx +19 -1
  58. package/src/ui/components/patterns/shell-nav.tsx +10 -3
  59. package/src/ui/components/patterns/sidebar.tsx +17 -6
  60. package/src/ui/components/patterns/trigger.tsx +14 -16
  61. package/src/ui/components/patterns/view.tsx +26 -17
  62. package/src/ui/components/primitives/alert.tsx +11 -5
  63. package/src/ui/components/primitives/ask.tsx +3 -3
  64. package/src/ui/components/primitives/badge.tsx +11 -6
  65. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  66. package/src/ui/components/primitives/button.tsx +16 -3
  67. package/src/ui/components/primitives/calendar.tsx +28 -2
  68. package/src/ui/components/primitives/carousel.tsx +3 -3
  69. package/src/ui/components/primitives/chat.tsx +1 -1
  70. package/src/ui/components/primitives/checkbox.tsx +1 -1
  71. package/src/ui/components/primitives/command.tsx +2 -2
  72. package/src/ui/components/primitives/control.ts +12 -0
  73. package/src/ui/components/primitives/copyable.tsx +1 -1
  74. package/src/ui/components/primitives/dialog.tsx +12 -7
  75. package/src/ui/components/primitives/dot.tsx +5 -0
  76. package/src/ui/components/primitives/drawer.tsx +10 -3
  77. package/src/ui/components/primitives/field.tsx +3 -3
  78. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  79. package/src/ui/components/primitives/input-group.tsx +1 -1
  80. package/src/ui/components/primitives/input-otp.tsx +1 -1
  81. package/src/ui/components/primitives/input.tsx +2 -2
  82. package/src/ui/components/primitives/progress.tsx +32 -3
  83. package/src/ui/components/primitives/radio-group.tsx +1 -1
  84. package/src/ui/components/primitives/resizable.tsx +3 -1
  85. package/src/ui/components/primitives/select.tsx +5 -5
  86. package/src/ui/components/primitives/slider.tsx +5 -1
  87. package/src/ui/components/primitives/sonner.tsx +3 -0
  88. package/src/ui/components/primitives/switch.tsx +1 -0
  89. package/src/ui/components/primitives/tabs.tsx +1 -0
  90. package/src/ui/components/primitives/textarea.tsx +1 -1
  91. package/src/ui/components/primitives/toggle.tsx +1 -1
  92. package/src/ui/components/primitives/tooltip.tsx +1 -0
  93. package/src/ui/docs/changelog.tsx +1 -1
  94. package/src/ui/docs/content/action-form.md +10 -3
  95. package/src/ui/docs/content/action-list.md +10 -1
  96. package/src/ui/docs/content/action-trigger.md +8 -1
  97. package/src/ui/docs/content/action-view.md +9 -2
  98. package/src/ui/docs/content/ask.md +11 -0
  99. package/src/ui/docs/content/calendar.md +13 -0
  100. package/src/ui/docs/content/card.md +26 -0
  101. package/src/ui/docs/content/chat.md +20 -0
  102. package/src/ui/docs/content/cli.md +71 -19
  103. package/src/ui/docs/content/composer.md +15 -0
  104. package/src/ui/docs/content/content.md +15 -0
  105. package/src/ui/docs/content/copyable.md +8 -0
  106. package/src/ui/docs/content/detail.md +19 -1
  107. package/src/ui/docs/content/dictionary-value.md +9 -2
  108. package/src/ui/docs/content/dock.md +8 -0
  109. package/src/ui/docs/content/dot.md +8 -0
  110. package/src/ui/docs/content/empty.md +2 -2
  111. package/src/ui/docs/content/getting-started.md +2 -2
  112. package/src/ui/docs/content/icon-picker.md +11 -0
  113. package/src/ui/docs/content/label.md +7 -0
  114. package/src/ui/docs/content/menu.md +6 -0
  115. package/src/ui/docs/content/metric-card.md +13 -0
  116. package/src/ui/docs/content/page.md +8 -0
  117. package/src/ui/docs/content/popover.md +6 -0
  118. package/src/ui/docs/content/progress.md +8 -11
  119. package/src/ui/docs/content/select.md +5 -5
  120. package/src/ui/docs/content/semantic-context.md +2 -2
  121. package/src/ui/docs/content/sidebar.md +14 -8
  122. package/src/ui/docs/content/skeleton.md +6 -0
  123. package/src/ui/docs/content/split.md +21 -0
  124. package/src/ui/docs/content/textarea.md +7 -0
  125. package/src/ui/docs/content/tokens.md +4 -4
  126. package/src/ui/docs/content/truncate.md +8 -0
  127. package/src/ui/docs/content/ui.md +14 -0
  128. package/src/ui/docs/doc-client.tsx +5 -5
  129. package/src/ui/docs/doc.tsx +26 -14
  130. package/src/ui/docs/registry.tsx +5 -5
  131. package/src/ui/docs/standalone.tsx +2 -2
  132. package/src/ui/drivers/react.tsx +17 -12
  133. package/src/ui/lib/action-errors.ts +45 -0
  134. package/src/ui/lib/zod-pt-br.ts +31 -4
  135. package/src/ui/meta.ts +3 -3
  136. package/src/ui/react.tsx +2 -0
  137. package/src/ui/theme.css +10 -8
  138. package/src/vite/design.ts +6 -18
@@ -134,8 +134,9 @@ render(
134
134
  Esse é o padrão usado pelo `DocBrowser`: seções são rótulos, grupos nomeados são nós expansíveis e
135
135
  páginas são folhas. Um grupo começa aberto e volta a abrir quando contém a página ativa.
136
136
 
137
- Use `ShellNav` para uma navegação plana que precisa de cabeçalho próprio e grupos ancorados no
138
- rodapé. Ele gerencia a rolagem internamente e se adapta quando estiver dentro de uma `Sidebar`
137
+ `ShellNav` continua disponível para uma navegação plana com cabeçalho próprio e grupos ancorados no
138
+ rodapé, mas está descontinuado: código novo usa `SidebarNav`, que cobre o mesmo caso integrado à
139
+ `Sidebar`. Ele gerencia a rolagem internamente e se adapta quando estiver dentro de uma `Sidebar`
139
140
  recolhida.
140
141
 
141
142
  ```tsx
@@ -156,6 +157,10 @@ recolhida.
156
157
  somente os ícones e expõem os rótulos em tooltips. Por isso, todo destino que aparece no modo
157
158
  recolhido precisa de um ícone reconhecível e de um `label` completo.
158
159
 
160
+ O slot de ícone do `SidebarItem` ocupa `1rem` nos dois estados e normaliza SVGs para essa medida.
161
+ O consumidor escolhe o símbolo e sua cor sem precisar repetir largura ou altura em ícones SVG.
162
+ Outros tipos de `ReactNode` continuam responsáveis pelas próprias dimensões.
163
+
159
164
  ```tsx preview
160
165
  const [collapsed, setCollapsed] = useState(false)
161
166
 
@@ -216,12 +221,13 @@ estrutura evita botões aninhados e permite que cada controle receba foco de for
216
221
  }
217
222
  onClick={() => {}}
218
223
  />
219
- <SidebarItem
220
- label="Criar workspace"
221
- icon={<FilePlus2 />}
222
- className="pl-6"
223
- onClick={() => {}}
224
- />
224
+ <SidebarTreeGroup>
225
+ <SidebarItem
226
+ label="Criar workspace"
227
+ icon={<FilePlus2 />}
228
+ onClick={() => {}}
229
+ />
230
+ </SidebarTreeGroup>
225
231
  </div>
226
232
  ```
227
233
 
@@ -33,3 +33,9 @@ O esqueleto reproduz o layout final do card — título, descrição, conteúdo
33
33
  </CardFooter>
34
34
  </Card>
35
35
  ```
36
+
37
+ ## Propriedades de Skeleton
38
+
39
+ | Propriedade | Tipo | Padrão | Descrição |
40
+ |---|---|---|---|
41
+ | `className` | `string` | | Dá forma ao placeholder (`h-4 w-40`, `size-10 rounded-full`); o componente só traz o pulso e o fundo. |
@@ -74,3 +74,24 @@ const defaultLayout = readLayout('workspace-layout')
74
74
  ```
75
75
 
76
76
  Em aplicações renderizadas no servidor, leia o armazenamento somente no cliente. `onLayoutChanged` também permite usar `sessionStorage` ou uma camada própria quando o layout precisa acompanhar outro escopo.
77
+
78
+ ## Propriedades de Split
79
+
80
+ | Propriedade | Tipo | Padrão | Descrição |
81
+ |---|---|---|---|
82
+ | `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | Sentido em que os panes se alinham. |
83
+ | `resizable` | `boolean` | `false` | Cada fronteira ganha um separador acessível e arrastável. |
84
+ | `handle` | `boolean` | `false` | Mostra a alça visual no separador. |
85
+ | `id` | `string` | | Identidade estável do grupo redimensionável. |
86
+ | `defaultLayout` | `Record<string, number>` | | Layout percentual restaurado, indexado pelos ids dos panes. |
87
+ | `onLayoutChanged` | `(layout, { isUserInteraction }) => void` | | Chamado ao concluir uma mudança de layout; persista onde fizer sentido. |
88
+
89
+ ## Propriedades de Pane
90
+
91
+ | Propriedade | Tipo | Padrão | Descrição |
92
+ |---|---|---|---|
93
+ | `id` | `string` | | Identidade estável usada pelo layout redimensionável e persistido. |
94
+ | `initialSize` | `number \| string` | | Tamanho inicial; número é porcentagem, string aceita `%`, `rem`, `em`, `vh`, `vw` e `px`. |
95
+ | `minSize` / `maxSize` | `number \| string` | | Limites quando o split é redimensionável. |
96
+ | `grow` | `boolean` | `false` | Ocupa o espaço remanescente no layout simples. |
97
+ | `inset` | `'none' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Respiro interno; `none` para chrome, navegação ou conteúdo com inset próprio. |
@@ -29,3 +29,10 @@ Associe `htmlFor` no `Label` ao `id` do campo para manter o rótulo acessível.
29
29
  <Textarea aria-invalid placeholder="Conte o contexto da mudança." />
30
30
  <Textarea disabled defaultValue="Sessão encerrada — o brief não pode mais ser editado." />
31
31
  ```
32
+
33
+ ## Propriedades de Textarea
34
+
35
+ | Propriedade | Tipo | Padrão | Descrição |
36
+ |---|---|---|---|
37
+ | `…props` | `ComponentProps<'textarea'>` | | Todos os atributos nativos (`value`, `defaultValue`, `rows`, `disabled`, `aria-invalid`…). A altura acompanha o conteúdo (`field-sizing-content`) a partir de `min-h-16`. |
38
+ | `className` | `string` | | Compõe sobre o estilo padrão. |
@@ -1,8 +1,8 @@
1
1
  ---
2
- title: Tokens & Tema
2
+ title: Tokens e tema
3
3
  ---
4
4
 
5
- # Tokens & Tema
5
+ # Tokens e tema
6
6
 
7
7
  O tema canônico vive no Opus (`theme.css`): tokens com a cor inteira na var, mapeados para o
8
8
  Tailwind via `@theme inline`. Os swatches abaixo leem as vars **ao vivo** — troque o tema do app
@@ -73,8 +73,8 @@ render(
73
73
 
74
74
  ## Linhas e foco
75
75
 
76
- > Bordas e o anel de foco também são tokens — nada de cinza hardcoded. A aresta de superfície
77
- > elevada (`--edge`) mora na seção Elevação por papel.
76
+ > Bordas e o anel de foco também são tokens — nada de cinza hardcoded. Superfícies elevadas usam
77
+ > a mesma `border-border`; o que muda entre elas é a sombra, descrita na seção Escala de elevação.
78
78
 
79
79
  ```tsx preview
80
80
  const LINES = [
@@ -60,3 +60,11 @@ quando o texto completo ainda não oferecer contexto suficiente.
60
60
 
61
61
  Requer `TooltipProvider` na raiz (o esqueleto do `opus create` já monta). Largura vem do
62
62
  container ou de `className` (`max-w-*`) — o span é `block truncate`.
63
+
64
+ ## Propriedades de Truncate
65
+
66
+ | Propriedade | Tipo | Padrão | Descrição |
67
+ |---|---|---|---|
68
+ | `tooltip` | `ReactNode` | os próprios `children` | Conteúdo da dica quando o texto transborda. |
69
+ | `fade` | `boolean` | `false` | Sinaliza o corte esmaecendo o fim da linha, no lugar das reticências. |
70
+ | `className` | `string` | | Largura (`max-w-*`) e demais ajustes; o span é `block truncate`. |
@@ -30,6 +30,20 @@ import { Button, Dialog, useAction } from '@softize/opus/ui/react'
30
30
  @source '../node_modules/@softize/opus/src/ui';
31
31
  ```
32
32
 
33
+ ## Prebundle do Vite
34
+
35
+ A entrada `@softize/opus/ui/react` é um `.tsx`, e o prebundle do Vite só considera entradas
36
+ `.js`/`.ts`. Sem ajuste, o dev server avisa `Cannot optimize dependency` e serve a árvore do Opus
37
+ arquivo a arquivo na carga fria. Há duas saídas, cada uma com um custo conhecido:
38
+
39
+ - `optimizeDeps.include: ['@softize/opus/ui/react']` com `extensions: ['.tsx']` pré-empacota o
40
+ Opus, mas pode duplicar `@tanstack/react-query` quando o app também o importa direto (dois
41
+ `QueryClient`, hooks fora do provider).
42
+ - `optimizeDeps.exclude: ['@softize/opus/ui/react']` evita a duplicação e infla a carga fria.
43
+
44
+ Escolha pelo sintoma que aparece no seu app e registre a decisão no `vite.config.ts`. A saída
45
+ definitiva (entrada `.ts` no pacote) está no radar do Opus.
46
+
33
47
  ## Origem e anti-drift
34
48
 
35
49
  > Cada componente declara de onde veio — e a regra de sincronização vem junto.
@@ -8,7 +8,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
8
8
  import { z } from 'zod'
9
9
  import { attachLogicalType, defineContract, error } from '../../core/index.ts'
10
10
  import type { ActionDef, ActionResult, ClientAdapter, LogicalTypeMeta, Paginated } from '../../core/index.ts'
11
- import { TbdlibProvider } from '../react.tsx'
11
+ import { OpusProvider } from '../react.tsx'
12
12
 
13
13
  // — Dados enlatados (domínio da casa) —
14
14
  export interface DocBrowserWorkspace {
@@ -37,7 +37,7 @@ export const docWorkspaceStatus: LogicalTypeMeta = {
37
37
  keys: [...STATUS],
38
38
  entries: {
39
39
  active: { label: 'Ativo', context: 'success', description: 'Agentes em operação para o cliente.' },
40
- onboarding: { label: 'Onboarding', context: 'warning' },
40
+ onboarding: { label: 'Em integração', context: 'warning' },
41
41
  paused: { label: 'Pausado' },
42
42
  },
43
43
  presentation: 'status',
@@ -59,7 +59,7 @@ export const docWorkspaceCreate = defineContract({
59
59
  }),
60
60
  output: z.object({ id: z.string() }),
61
61
  fields: {
62
- name: { label: 'Nome', placeholder: 'Ex.: Empresa X', hint: 'Digite "Softize" pra ver o erro de servidor.' },
62
+ name: { label: 'Nome', placeholder: 'Ex.: Empresa X', help: 'Digite Softize para ver o erro de servidor.' },
63
63
  status: { label: 'Status' },
64
64
  contact: { label: 'Contato', placeholder: 'email@cliente.com' },
65
65
  notes: { label: 'Observações', placeholder: 'Contexto do onboarding…' },
@@ -100,7 +100,7 @@ export const docWorkspaceList = defineContract({
100
100
  kind: 'static',
101
101
  items: [
102
102
  { value: 'active', label: 'Ativo' },
103
- { value: 'onboarding', label: 'Onboarding' },
103
+ { value: 'onboarding', label: 'Em integração' },
104
104
  { value: 'paused', label: 'Pausado' },
105
105
  ],
106
106
  },
@@ -225,7 +225,7 @@ const queryClient = new QueryClient()
225
225
  export function DocBrowserActionProvider({ children }: { children: React.ReactNode }): React.ReactElement {
226
226
  return (
227
227
  <QueryClientProvider client={queryClient}>
228
- <TbdlibProvider client={docClient}>{children}</TbdlibProvider>
228
+ <OpusProvider client={docClient}>{children}</OpusProvider>
229
229
  </QueryClientProvider>
230
230
  )
231
231
  }
@@ -18,6 +18,10 @@ import {
18
18
  TableHead,
19
19
  TableHeader,
20
20
  TableRow,
21
+ Tooltip,
22
+ TooltipContent,
23
+ TooltipProvider,
24
+ TooltipTrigger,
21
25
  cn,
22
26
  } from '../react.tsx'
23
27
 
@@ -72,8 +76,8 @@ export function DocPage({
72
76
  </p>
73
77
  )}
74
78
  {meta?.deprecated !== undefined && (
75
- <div className="max-w-2xl rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm leading-relaxed text-foreground">
76
- <span className="font-medium">Deprecated{meta.deprecated.since !== undefined ? ` desde ${meta.deprecated.since}` : ''}.</span>{' '}
79
+ <div className="max-w-2xl rounded-lg border border-context-warning-border bg-context-warning-subtle px-3 py-2 text-sm leading-relaxed text-foreground">
80
+ <span className="font-medium">Descontinuado{meta.deprecated.since !== undefined ? ` desde ${meta.deprecated.since}` : ''}.</span>{' '}
77
81
  Use <InlineMd text={meta.deprecated.alternative} />.
78
82
  </div>
79
83
  )}
@@ -211,17 +215,25 @@ export function CodeBlock({
211
215
  // overflow-hidden: os filhos (fade incluso) são CLIPADOS no raio do contêiner — sem
212
216
  // isso, o overlay desenhava um segundo arco nos cantos (a "borda dobrada").
213
217
  <div className={cn('group relative overflow-hidden rounded-lg border border-border bg-muted/40', className)}>
214
- <button
215
- onClick={() => {
216
- void navigator.clipboard.writeText(clean)
217
- setCopied(true)
218
- setTimeout(() => setCopied(false), 1500)
219
- }}
220
- title="Copiar código"
221
- className="absolute right-2 top-2 z-20 rounded-md p-1.5 text-muted-foreground opacity-0 transition-opacity hover:bg-muted hover:text-foreground group-hover:opacity-100"
222
- >
223
- {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
224
- </button>
218
+ {/* Provider próprio: a doc também é montada fora do shell do app (standalone, testes). */}
219
+ <TooltipProvider delayDuration={200}>
220
+ <Tooltip>
221
+ <TooltipTrigger asChild>
222
+ <button
223
+ onClick={() => {
224
+ void navigator.clipboard.writeText(clean)
225
+ setCopied(true)
226
+ setTimeout(() => setCopied(false), 1500)
227
+ }}
228
+ aria-label="Copiar código"
229
+ className="absolute right-2 top-2 z-20 rounded-md p-1.5 text-muted-foreground opacity-0 transition-opacity hover:bg-muted hover:text-foreground group-hover:opacity-100 focus-visible:opacity-100"
230
+ >
231
+ {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
232
+ </button>
233
+ </TooltipTrigger>
234
+ <TooltipContent>{copied ? 'Copiado' : 'Copiar código'}</TooltipContent>
235
+ </Tooltip>
236
+ </TooltipProvider>
225
237
 
226
238
  <div ref={contentRef} className={cn('relative', collapsed && 'max-h-44 overflow-hidden')}>
227
239
  <Highlight code={clean} language={lang} theme={codeTheme}>
@@ -279,7 +291,7 @@ export function PropsTable({ rows }: { rows: PropRow[] }): React.ReactElement {
279
291
  <Table>
280
292
  <TableHeader>
281
293
  <TableRow>
282
- <TableHead className="w-[18%]">Prop</TableHead>
294
+ <TableHead className="w-[18%]">Propriedade</TableHead>
283
295
  <TableHead className="w-[30%]">Tipo</TableHead>
284
296
  <TableHead className="w-[12%]">Padrão</TableHead>
285
297
  <TableHead>Descrição</TableHead>
@@ -221,13 +221,13 @@ export const PROTOCOL_SECTIONS: DocSection[] = [
221
221
  pages: [
222
222
  {
223
223
  slug: "getting-started",
224
- title: "Getting started",
224
+ title: "Primeiros passos",
225
225
  render: doc(gettingStartedMd),
226
226
  },
227
227
  { slug: "cycle", title: "Evolução contínua", render: doc(cycleMd) },
228
228
  {
229
229
  slug: "upgrading",
230
- title: "Atualizar a base",
230
+ title: "Atualizar o Opus",
231
231
  render: doc(upgradingMd),
232
232
  },
233
233
  ],
@@ -337,7 +337,7 @@ export const UI_SECTIONS: DocSection[] = [
337
337
  pages: [
338
338
  {
339
339
  slug: "como-consumir",
340
- title: "Getting started",
340
+ title: "Como consumir",
341
341
  render: doc(uiMd),
342
342
  },
343
343
  ],
@@ -349,10 +349,10 @@ export const UI_SECTIONS: DocSection[] = [
349
349
  groups: [
350
350
  {
351
351
  pages: [
352
- { slug: "tokens", title: "Tokens & Tema", render: doc(tokensMd) },
352
+ { slug: "tokens", title: "Tokens e tema", render: doc(tokensMd) },
353
353
  {
354
354
  slug: "semantic-context",
355
- title: "Contexto & Variante",
355
+ title: "Contexto e variante",
356
356
  render: doc(semanticContextMd),
357
357
  },
358
358
  {
@@ -66,7 +66,7 @@ export interface MountDocsOptions {
66
66
  basePath?: string
67
67
  /** Elemento alvo. Default: `#root` (cria um se faltar). */
68
68
  el?: HTMLElement
69
- /** Título no topo. Default `Components` (o site-base passa `opus`). */
69
+ /** Título no topo. Default `Componentes` (o site-base passa `opus`). */
70
70
  title?: string
71
71
  /** Legenda ao lado do título. Default `a verdade visual deste projeto`. */
72
72
  subtitle?: string
@@ -78,7 +78,7 @@ export interface MountDocsOptions {
78
78
  /** Monta a doc na página. Chamado pelo entry do plugin opusDocs (depois de importar o CSS do app). */
79
79
  export function mountDocs(options: MountDocsOptions = {}): void {
80
80
  const basePath = options.basePath ?? '/__docs'
81
- const title = options.title ?? 'Components'
81
+ const title = options.title ?? 'Componentes'
82
82
  const subtitle = options.subtitle ?? 'a verdade visual deste projeto'
83
83
  // Tema persistido pela própria doc; senão, a preferência do sistema.
84
84
  let saved: string | null = null
@@ -2,11 +2,11 @@
2
2
  * @softize/opus/ui/react — React driver
3
3
  *
4
4
  * Hooks + Provider pra invocar actions client-side.
5
- * - `TbdlibProvider` — injeta ClientAdapter no contexto
5
+ * - `OpusProvider` — injeta ClientAdapter no contexto (`TbdlibProvider` é alias em retirada)
6
6
  * - `useAction(action)` — invoca actions simple/form/view
7
7
  * - `useLookupAction(action)` — variante pra search (output Paginated)
8
8
  *
9
- * Compatível com React 18 e 19. Não dona estado global de cache —
9
+ * Requer React 19 (peer `react@^19`). Não dona estado global de cache —
10
10
  * cada hook mantém o seu (suficiente pra v0). Integração com
11
11
  * TanStack Query fica em `@softize/opus/client/tanstack` no futuro.
12
12
  */
@@ -58,14 +58,14 @@ export interface DictLike {
58
58
  options(): Array<{ value: string; label: string }>
59
59
  }
60
60
 
61
- interface TbdlibContextValue {
61
+ interface OpusContextValue {
62
62
  client: ClientAdapter
63
63
  dicts: Record<string, DictLike>
64
64
  }
65
65
 
66
- const TbdlibContext = createContext<TbdlibContextValue | null>(null)
66
+ const OpusContext = createContext<OpusContextValue | null>(null)
67
67
 
68
- export interface TbdlibProviderProps {
68
+ export interface OpusProviderProps {
69
69
  client: ClientAdapter
70
70
  /** Dicionários por ref — resolvem `options: { kind: 'dictionary', ref }` de
71
71
  * filters/fields (ActionList/ActionForm). Campo de dict sem registro aqui ainda
@@ -74,20 +74,25 @@ export interface TbdlibProviderProps {
74
74
  children: ReactNode
75
75
  }
76
76
 
77
- export function TbdlibProvider({
77
+ export function OpusProvider({
78
78
  client,
79
79
  dicts,
80
80
  children,
81
- }: TbdlibProviderProps): ReactNode {
82
- const value = useMemo<TbdlibContextValue>(() => ({ client, dicts: dicts ?? {} }), [client, dicts])
83
- return <TbdlibContext.Provider value={value}>{children}</TbdlibContext.Provider>
81
+ }: OpusProviderProps): ReactNode {
82
+ const value = useMemo<OpusContextValue>(() => ({ client, dicts: dicts ?? {} }), [client, dicts])
83
+ return <OpusContext.Provider value={value}>{children}</OpusContext.Provider>
84
84
  }
85
85
 
86
+ /** @deprecated Renomeado para `OpusProvider`; o alias segue até a próxima série. */
87
+ export const TbdlibProvider = OpusProvider
88
+ /** @deprecated Use `OpusProviderProps`. */
89
+ export type TbdlibProviderProps = OpusProviderProps
90
+
86
91
  function useClient(): ClientAdapter {
87
- const ctx = useContext(TbdlibContext)
92
+ const ctx = useContext(OpusContext)
88
93
  if (ctx === null) {
89
94
  throw new Error(
90
- 'useAction/useLookupAction must be used inside <TbdlibProvider>',
95
+ 'useAction/useLookupAction precisam estar dentro de <OpusProvider>.',
91
96
  )
92
97
  }
93
98
  return ctx.client
@@ -98,7 +103,7 @@ const NO_DICTS: Record<string, DictLike> = {}
98
103
  /** Dicts do provider. Default {} — inclusive fora do provider: o resolvedor cai
99
104
  * nos fallbacks (estáticas do spec, meta do `t.dict`) sem exigir registry. */
100
105
  export function useDicts(): Record<string, DictLike> {
101
- return useContext(TbdlibContext)?.dicts ?? NO_DICTS
106
+ return useContext(OpusContext)?.dicts ?? NO_DICTS
102
107
  }
103
108
 
104
109
  // =============================================================================
@@ -0,0 +1,45 @@
1
+ /**
2
+ * O que a pessoa pode ler quando uma action falha.
3
+ *
4
+ * Categorias cuja frase do SERVIDOR vence o rótulo do contrato: "Este agente tem 3 conversas —
5
+ * desabilite em vez de excluir" é acionável; "Falha ao excluir" não. É ALLOWLIST de propósito —
6
+ * erro inesperado carrega texto técnico cru (uma violação de FK viraria
7
+ * `violates foreign key constraint "user_unit_id_fkey"` para quem só clicou no botão), então
8
+ * categoria desconhecida cai no texto genérico, o lado seguro de errar. `authorization` e
9
+ * `authentication` entram porque o runtime já responde em pt-BR ("Acesso negado",
10
+ * "Autenticação necessária") e a pessoa precisa saber que o problema é de permissão, não de rede.
11
+ */
12
+ export const BUSINESS_ERRORS: readonly string[] = [
13
+ 'conflict',
14
+ 'validation',
15
+ 'not_found',
16
+ 'authorization',
17
+ 'authentication',
18
+ ]
19
+
20
+ /** Erro que tentar de novo não resolve: falta de permissão ou de sessão. */
21
+ export function isRetryable(err: HumanizableError): boolean {
22
+ return err.category !== 'authorization' && err.category !== 'authentication'
23
+ }
24
+
25
+ export interface HumanizableError {
26
+ category?: string
27
+ message?: string
28
+ }
29
+
30
+ /** A frase do servidor é de negócio (allowlist) e tem conteúdo. */
31
+ export function isBusinessError(err: HumanizableError): boolean {
32
+ return (
33
+ err.category !== undefined &&
34
+ BUSINESS_ERRORS.includes(err.category) &&
35
+ (err.message ?? '').trim() !== ''
36
+ )
37
+ }
38
+
39
+ /**
40
+ * Texto para a pessoa: a frase do servidor quando ela é de negócio; `fallback` no resto. Código
41
+ * técnico (`internal.unhandled`, `db.constraint`) nunca chega à tela por aqui.
42
+ */
43
+ export function humanizeActionError(err: HumanizableError, fallback: string): string {
44
+ return isBusinessError(err) ? (err.message as string).trim() : fallback
45
+ }
@@ -10,28 +10,55 @@
10
10
  */
11
11
  import { z } from 'zod'
12
12
 
13
+ const INVALID = 'Valor inválido.'
14
+
15
+ function options(count: number | bigint): string {
16
+ return Number(count) === 1 ? '1 opção' : `${count} opções`
17
+ }
18
+
13
19
  export const zodErrorMapPtBr: z.ZodErrorMap = (issue, ctx) => {
14
20
  switch (issue.code) {
15
21
  case z.ZodIssueCode.invalid_type:
16
22
  if (issue.received === 'undefined' || issue.received === 'null') return { message: 'Obrigatório.' }
17
- return { message: 'Valor inválido.' }
23
+ return { message: INVALID }
18
24
  case z.ZodIssueCode.too_small:
19
25
  if (issue.type === 'string') {
20
26
  return { message: Number(issue.minimum) <= 1 ? 'Obrigatório.' : `Mínimo de ${issue.minimum} caracteres.` }
21
27
  }
22
- if (issue.type === 'array') return { message: `Selecione pelo menos ${issue.minimum}.` }
28
+ if (issue.type === 'array') return { message: `Selecione pelo menos ${options(issue.minimum)}.` }
29
+ if (issue.type === 'date') return { message: 'Data anterior ao permitido.' }
23
30
  return { message: `Valor mínimo: ${issue.minimum}.` }
24
31
  case z.ZodIssueCode.too_big:
25
32
  if (issue.type === 'string') return { message: `Máximo de ${issue.maximum} caracteres.` }
26
- if (issue.type === 'array') return { message: `Selecione no máximo ${issue.maximum}.` }
33
+ if (issue.type === 'array') return { message: `Selecione no máximo ${options(issue.maximum)}.` }
34
+ if (issue.type === 'date') return { message: 'Data posterior ao permitido.' }
27
35
  return { message: `Valor máximo: ${issue.maximum}.` }
28
36
  case z.ZodIssueCode.invalid_string:
29
- if (issue.validation === 'email') return { message: 'Email inválido.' }
37
+ if (issue.validation === 'email') return { message: 'E-mail inválido.' }
30
38
  if (issue.validation === 'url') return { message: 'URL inválida.' }
31
39
  if (issue.validation === 'uuid') return { message: 'Identificador inválido.' }
32
40
  return { message: 'Formato inválido.' }
33
41
  case z.ZodIssueCode.invalid_enum_value:
42
+ case z.ZodIssueCode.invalid_union_discriminator:
34
43
  return { message: 'Escolha uma das opções.' }
44
+ case z.ZodIssueCode.invalid_literal:
45
+ case z.ZodIssueCode.invalid_union:
46
+ case z.ZodIssueCode.invalid_intersection_types:
47
+ case z.ZodIssueCode.invalid_arguments:
48
+ case z.ZodIssueCode.invalid_return_type:
49
+ case z.ZodIssueCode.custom:
50
+ return { message: INVALID }
51
+ case z.ZodIssueCode.invalid_date:
52
+ return { message: 'Data inválida.' }
53
+ case z.ZodIssueCode.not_multiple_of:
54
+ return { message: `Use múltiplos de ${issue.multipleOf}.` }
55
+ case z.ZodIssueCode.unrecognized_keys:
56
+ return {
57
+ message:
58
+ issue.keys.length === 1
59
+ ? `Campo não reconhecido: ${issue.keys[0]}.`
60
+ : `Campos não reconhecidos: ${issue.keys.join(', ')}.`,
61
+ }
35
62
  default:
36
63
  return { message: ctx.defaultError }
37
64
  }
package/src/ui/meta.ts CHANGED
@@ -355,10 +355,10 @@ export const componentMeta = {
355
355
  },
356
356
 
357
357
  confirm: {
358
- name: "dialog",
358
+ name: "confirm",
359
359
  ancestry: "opus",
360
360
  whenToUse:
361
- "Solicite reconhecimento, confirmação ou uma resposta curta por uma API imperativa. Monte DialogHost uma vez no shell e use `dialog.alert`, `dialog.confirm` ou `dialog.prompt`; as solicitações são exibidas uma por vez. Para ações declaradas em contrato, prefira ActionTrigger; para formulários, use ActionFormDialog.",
361
+ "Solicite reconhecimento, confirmação, uma escolha ou uma resposta curta por uma API imperativa. Monte DialogHost uma vez no shell e use `dialog.alert`, `dialog.confirm`, `dialog.choose` ou `dialog.prompt`; as solicitações são exibidas uma por vez. Para ações declaradas em contrato, prefira ActionTrigger; para formulários, use ActionFormDialog.",
362
362
  },
363
363
  "action-form": {
364
364
  name: "action-form",
@@ -400,7 +400,7 @@ export const componentMeta = {
400
400
  name: "page",
401
401
  ancestry: "opus",
402
402
  whenToUse:
403
- "O esqueleto de página do back-office: <main> + container centralizado com teto padrão de 80rem (`max-w-7xl`). O shorthand com title, description, count e actions cria a mesma anatomia de PageHeader(PageTitle/PageDescription/PageMeta/PageActions) + PageBody disponível na forma explícita. `className` substitui o teto quando a composição pede outra largura. Quando todo o body estiver carregando, falhar ou estiver vazio, use PageState. Para listagem em modal, ActionListDialog.",
403
+ "O esqueleto de página do back-office: <main> + container centralizado com teto padrão de 80rem (`max-w-7xl`). O shorthand com title, description, count e actions cria a mesma anatomia de PageHeader(PageTitle/PageDescription/PageMeta/PageActions) + PageBody disponível na forma explícita. PageActionsTarget projeta as ações no chrome reservado pelo shell sem retirar sua declaração da Page. `className` substitui o teto quando a composição pede outra largura. Quando todo o body estiver carregando, falhar ou estiver vazio, use PageState. Para listagem em modal, ActionListDialog.",
404
404
  },
405
405
  router: {
406
406
  name: "router",
package/src/ui/react.tsx CHANGED
@@ -426,6 +426,7 @@ export {
426
426
  PageDescription,
427
427
  PageMeta,
428
428
  PageActions,
429
+ PageActionsTarget,
429
430
  PageBody,
430
431
  } from "./components/patterns/page.tsx";
431
432
  export type { PageProps } from "./components/patterns/page.tsx";
@@ -475,6 +476,7 @@ export type {
475
476
  DockProps,
476
477
  DockActionProps,
477
478
  SurfaceStatusProps,
479
+ SurfaceStatusContext,
478
480
  } from "./components/patterns/dock.tsx";
479
481
 
480
482
  // Barra lateral composicional: header/conteúdo/footer e navegação, sem possuir o layout.
package/src/ui/theme.css CHANGED
@@ -59,6 +59,9 @@
59
59
  --context-primary-subtle: color-mix(in oklab, var(--primary) 8%, transparent);
60
60
  --context-primary-emphasis: var(--foreground);
61
61
  --context-primary-border: color-mix(in oklab, var(--primary) 25%, transparent);
62
+ /* info/success/warning: base, foreground, subtle e border (12 tokens) valem nos dois temas
63
+ de propósito — são cores saturadas com alpha, que já se assentam sobre qualquer canvas;
64
+ só `*-emphasis` (texto sobre o popover) precisa clarear no escuro, e é redefinido em .dark. */
62
65
  --context-info: hsl(217 91% 60%);
63
66
  --context-info-foreground: hsl(0 0% 100%);
64
67
  --context-info-subtle: hsl(217 91% 60% / 0.1);
@@ -82,7 +85,7 @@
82
85
  --border: hsl(0 0% 92.5%);
83
86
  --input: hsl(0 0% 92.5%);
84
87
  /* Ring de foco LEVE (pedido da casa): aro sutil, não um halo. Com ring-[0.1875rem] + /50,
85
- ~86% encosta na borda (89.8%) — um sussurro de foco. */
88
+ ~86% encosta na borda (92.5%) — um sussurro de foco. */
86
89
  --ring: hsl(0 0% 86%);
87
90
  --radius: 0.75rem;
88
91
  }
@@ -107,10 +110,9 @@
107
110
  --accent-foreground: hsl(0 0% 98%);
108
111
  /* Vermelho CLARO no escuro (era 30.6% — calibrado só pra FUNDO, com texto branco).
109
112
  Como o menu/alert/field/erros usam `text-destructive` como FOREGROUND, o valor
110
- escuro dava ~1,7:1 sobre o popover — ilegível. ~58% passa o AA como texto e é o
111
- que o success fazia (dark:text-emerald-400 pula pro tom claro). Fundo sólido
112
- (Button/Badge/toast destructive) fica mais vivo — igual ao light, com branco por
113
- cima, como o shadcn novo. */
113
+ escuro dava ~1,7:1 sobre o popover — ilegível. ~58% passa o AA como texto, o mesmo
114
+ salto que os `*-emphasis` abaixo dão para o tom claro. Fundo sólido (Button/Badge/toast
115
+ danger) fica mais vivo — igual ao light, com branco por cima, como o shadcn novo. */
114
116
  --destructive: hsl(0 72% 58%);
115
117
  --destructive-foreground: hsl(0 0% 98%);
116
118
  --context-info-emphasis: hsl(213 94% 68%);
@@ -272,8 +274,8 @@
272
274
  }
273
275
  :root::-webkit-scrollbar-thumb,
274
276
  :root *::-webkit-scrollbar-thumb {
275
- border: 2px solid transparent;
276
- border-radius: 9999px;
277
+ border: 2px solid transparent; /* px: hairline dupla do thumb — não deve escalar com a fonte. */
278
+ border-radius: 9999px; /* px: raio efetivamente infinito do scrollbar nativo. */
277
279
  background-color: transparent;
278
280
  background-clip: content-box;
279
281
  }
@@ -322,7 +324,7 @@
322
324
 
323
325
  /* Code block = superfície da casa (borda + bg muted) — só o <pre> CRU do prose. */
324
326
  .prose :where(pre):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
325
- border: 1px solid var(--border);
327
+ border: 1px solid var(--border); /* px: hairline do bloco de código — 1 pixel físico, não escala. */
326
328
  }
327
329
 
328
330
  /* O Sonner reserva por padrão apenas 1rem para a mídia. A moldura semântica do