@softize/opus 13.0.0 → 14.0.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 (164) hide show
  1. package/CHANGELOG.md +81 -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/copy.mjs +276 -6
  7. package/bin/lib/db.mjs +16 -74
  8. package/bin/lib/gen-openapi.mjs +3 -3
  9. package/bin/lib/gen-runner.mjs +1 -1
  10. package/bin/lib/gen.mjs +14 -69
  11. package/bin/lib/mcp.mjs +3 -1
  12. package/bin/lib/seed.mjs +5 -62
  13. package/docs/code-style.md +4 -1
  14. package/docs/ownership-vs-shadcn-lock.md +2 -3
  15. package/docs/protocol.md +7 -7
  16. package/docs/releasing.md +8 -2
  17. package/package.json +7 -3
  18. package/registry/instructions/opus.md +3 -3
  19. package/registry/templates/app/package.json +1 -1
  20. package/registry/templates/app/src/App.tsx +11 -6
  21. package/registry/templates/app/src/main.tsx +4 -4
  22. package/src/audit/drivers/console.ts +1 -0
  23. package/src/auth/drivers/better-auth.ts +1 -0
  24. package/src/auth/drivers/jwt.ts +1 -0
  25. package/src/cache/drivers/memory.ts +1 -0
  26. package/src/client/drivers/fetch.ts +2 -1
  27. package/src/core/actions.ts +6 -1
  28. package/src/core/audit.ts +9 -3
  29. package/src/core/contracts.ts +7 -0
  30. package/src/core/domain.ts +1 -1
  31. package/src/core/errors.ts +18 -15
  32. package/src/core/index.ts +4 -2
  33. package/src/core/package-version.ts +26 -0
  34. package/src/core/reactions.ts +1 -1
  35. package/src/core/runtime.ts +33 -23
  36. package/src/core/schedules.ts +1 -1
  37. package/src/core/types.ts +5 -6
  38. package/src/dsl/eval.ts +2 -2
  39. package/src/dsl/kysely.ts +2 -2
  40. package/src/dsl/loads.ts +1 -1
  41. package/src/dsl/parser.ts +5 -5
  42. package/src/events/drivers/mitt.ts +1 -0
  43. package/src/mcp/index.ts +2 -1
  44. package/src/observability/drivers/opentelemetry.ts +1 -0
  45. package/src/queue/drivers/bullmq.ts +3 -3
  46. package/src/scheduler/drivers/node-cron.ts +3 -2
  47. package/src/scheduler/every.ts +7 -7
  48. package/src/schema/openapi.ts +3 -3
  49. package/src/seed/index.ts +29 -0
  50. package/src/server/drivers/fastify.ts +5 -2
  51. package/src/server/drivers/node.ts +9 -6
  52. package/src/server/index.ts +3 -1
  53. package/src/storage/drivers/fs.ts +1 -0
  54. package/src/testing/index.ts +3 -3
  55. package/src/ui/components/patterns/action-list-dialog.tsx +10 -3
  56. package/src/ui/components/patterns/confirm.tsx +2 -31
  57. package/src/ui/components/patterns/content-header.tsx +44 -137
  58. package/src/ui/components/patterns/data-state.tsx +42 -68
  59. package/src/ui/components/patterns/dock.tsx +20 -3
  60. package/src/ui/components/patterns/form.tsx +26 -22
  61. package/src/ui/components/patterns/list.tsx +18 -12
  62. package/src/ui/components/patterns/page-state.tsx +39 -51
  63. package/src/ui/components/patterns/page.tsx +37 -54
  64. package/src/ui/components/patterns/sidebar.tsx +17 -6
  65. package/src/ui/components/patterns/state-surface.tsx +148 -0
  66. package/src/ui/components/patterns/surface-header.tsx +119 -0
  67. package/src/ui/components/patterns/trigger.tsx +21 -25
  68. package/src/ui/components/patterns/view.tsx +29 -22
  69. package/src/ui/components/primitives/alert.tsx +1 -27
  70. package/src/ui/components/primitives/ask.tsx +3 -3
  71. package/src/ui/components/primitives/avatar.tsx +15 -5
  72. package/src/ui/components/primitives/badge.tsx +5 -41
  73. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  74. package/src/ui/components/primitives/button.tsx +39 -30
  75. package/src/ui/components/primitives/calendar.tsx +28 -2
  76. package/src/ui/components/primitives/carousel.tsx +3 -3
  77. package/src/ui/components/primitives/chat.tsx +1 -1
  78. package/src/ui/components/primitives/checkbox.tsx +1 -1
  79. package/src/ui/components/primitives/command.tsx +2 -2
  80. package/src/ui/components/primitives/control.ts +72 -0
  81. package/src/ui/components/primitives/copyable.tsx +1 -1
  82. package/src/ui/components/primitives/dialog.tsx +12 -7
  83. package/src/ui/components/primitives/dot.tsx +1 -25
  84. package/src/ui/components/primitives/drawer.tsx +10 -3
  85. package/src/ui/components/primitives/field.tsx +3 -3
  86. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  87. package/src/ui/components/primitives/input-group.tsx +12 -9
  88. package/src/ui/components/primitives/input-otp.tsx +1 -1
  89. package/src/ui/components/primitives/input.tsx +2 -2
  90. package/src/ui/components/primitives/item.tsx +3 -1
  91. package/src/ui/components/primitives/menu.tsx +1 -7
  92. package/src/ui/components/primitives/pagination.tsx +16 -8
  93. package/src/ui/components/primitives/progress.tsx +32 -3
  94. package/src/ui/components/primitives/radio-group.tsx +1 -1
  95. package/src/ui/components/primitives/resizable.tsx +3 -1
  96. package/src/ui/components/primitives/select.tsx +6 -6
  97. package/src/ui/components/primitives/slider.tsx +5 -1
  98. package/src/ui/components/primitives/sonner.tsx +3 -0
  99. package/src/ui/components/primitives/spinner.tsx +13 -16
  100. package/src/ui/components/primitives/switch.tsx +5 -1
  101. package/src/ui/components/primitives/tabs.tsx +6 -3
  102. package/src/ui/components/primitives/textarea.tsx +1 -1
  103. package/src/ui/components/primitives/toggle.tsx +9 -4
  104. package/src/ui/components/primitives/tooltip.tsx +1 -0
  105. package/src/ui/docs/changelog.tsx +1 -1
  106. package/src/ui/docs/content/action-form.md +36 -3
  107. package/src/ui/docs/content/action-list-dialog.md +2 -2
  108. package/src/ui/docs/content/action-list.md +13 -2
  109. package/src/ui/docs/content/action-trigger.md +12 -5
  110. package/src/ui/docs/content/action-view.md +11 -3
  111. package/src/ui/docs/content/ask.md +11 -0
  112. package/src/ui/docs/content/avatar.md +7 -3
  113. package/src/ui/docs/content/button.md +30 -14
  114. package/src/ui/docs/content/calendar.md +13 -0
  115. package/src/ui/docs/content/card.md +26 -0
  116. package/src/ui/docs/content/chat.md +20 -0
  117. package/src/ui/docs/content/cli.md +71 -19
  118. package/src/ui/docs/content/communication.md +36 -0
  119. package/src/ui/docs/content/composer.md +15 -0
  120. package/src/ui/docs/content/content.md +17 -1
  121. package/src/ui/docs/content/copyable.md +8 -0
  122. package/src/ui/docs/content/data-state.md +17 -13
  123. package/src/ui/docs/content/detail.md +19 -1
  124. package/src/ui/docs/content/dialog.md +1 -4
  125. package/src/ui/docs/content/dictionary-value.md +9 -2
  126. package/src/ui/docs/content/dock.md +8 -0
  127. package/src/ui/docs/content/dot.md +8 -0
  128. package/src/ui/docs/content/empty.md +2 -2
  129. package/src/ui/docs/content/getting-started.md +2 -2
  130. package/src/ui/docs/content/icon-picker.md +11 -0
  131. package/src/ui/docs/content/input.md +1 -1
  132. package/src/ui/docs/content/item.md +1 -1
  133. package/src/ui/docs/content/label.md +7 -0
  134. package/src/ui/docs/content/menu.md +6 -0
  135. package/src/ui/docs/content/metric-card.md +13 -0
  136. package/src/ui/docs/content/page.md +20 -4
  137. package/src/ui/docs/content/pagination.md +11 -9
  138. package/src/ui/docs/content/popover.md +6 -0
  139. package/src/ui/docs/content/progress.md +8 -11
  140. package/src/ui/docs/content/select.md +5 -5
  141. package/src/ui/docs/content/semantic-context.md +5 -4
  142. package/src/ui/docs/content/sidebar.md +13 -47
  143. package/src/ui/docs/content/skeleton.md +6 -0
  144. package/src/ui/docs/content/spinner.md +9 -6
  145. package/src/ui/docs/content/split.md +21 -0
  146. package/src/ui/docs/content/switch.md +1 -1
  147. package/src/ui/docs/content/tabs.md +1 -1
  148. package/src/ui/docs/content/textarea.md +7 -0
  149. package/src/ui/docs/content/toggle.md +1 -1
  150. package/src/ui/docs/content/tokens.md +4 -4
  151. package/src/ui/docs/content/truncate.md +8 -0
  152. package/src/ui/docs/content/ui.md +14 -0
  153. package/src/ui/docs/doc-client.tsx +5 -5
  154. package/src/ui/docs/doc.tsx +26 -14
  155. package/src/ui/docs/registry.tsx +5 -5
  156. package/src/ui/docs/standalone.tsx +2 -2
  157. package/src/ui/drivers/react.tsx +12 -12
  158. package/src/ui/lib/action-errors.ts +45 -0
  159. package/src/ui/lib/zod-pt-br.ts +31 -4
  160. package/src/ui/meta.ts +8 -8
  161. package/src/ui/react.tsx +10 -16
  162. package/src/ui/theme.css +10 -8
  163. package/src/vite/design.ts +6 -18
  164. package/src/ui/components/patterns/shell-nav.tsx +0 -147
@@ -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
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,20 @@ 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
86
  function useClient(): ClientAdapter {
87
- const ctx = useContext(TbdlibContext)
87
+ const ctx = useContext(OpusContext)
88
88
  if (ctx === null) {
89
89
  throw new Error(
90
- 'useAction/useLookupAction must be used inside <TbdlibProvider>',
90
+ 'useAction/useLookupAction precisam estar dentro de <OpusProvider>.',
91
91
  )
92
92
  }
93
93
  return ctx.client
@@ -98,7 +98,7 @@ const NO_DICTS: Record<string, DictLike> = {}
98
98
  /** Dicts do provider. Default {} — inclusive fora do provider: o resolvedor cai
99
99
  * nos fallbacks (estáticas do spec, meta do `t.dict`) sem exigir registry. */
100
100
  export function useDicts(): Record<string, DictLike> {
101
- return useContext(TbdlibContext)?.dicts ?? NO_DICTS
101
+ return useContext(OpusContext)?.dicts ?? NO_DICTS
102
102
  }
103
103
 
104
104
  // =============================================================================
@@ -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
@@ -69,7 +69,7 @@ export const componentMeta = {
69
69
  name: "button",
70
70
  ancestry: "shadcn",
71
71
  whenToUse:
72
- "Inicie uma ação com um controle clicável. Use `context` para o significado, `variant` para o tratamento visual e `size` para a escala; `busy` comunica o andamento e impede um novo acionamento. Para ações relacionadas, use ButtonGroup.",
72
+ "Inicie uma ação com um controle clicável. Use `context` para o significado, `variant` para o tratamento visual e `size` para a escala compartilhada (`xs`…`lg` e os quadrados `icon-*`); `icon` recebe um nó e `busy` comunica o andamento e impede um novo acionamento. Para ações relacionadas, use ButtonGroup.",
73
73
  },
74
74
  card: {
75
75
  name: "card",
@@ -117,7 +117,7 @@ export const componentMeta = {
117
117
  name: "content-header",
118
118
  ancestry: "opus",
119
119
  whenToUse:
120
- "Estruture o cabeçalho de um Content com título, descrição, metadados e ações. No caso comum, declare esses valores diretamente em Content; componha ContentHeader apenas quando precisar controlar a anatomia.",
120
+ "Estruture o cabeçalho de um Content com título, contador, descrição e ações — a mesma anatomia de PageHeader. No caso comum, declare esses valores diretamente em Content; componha ContentHeader apenas quando precisar controlar a anatomia.",
121
121
  },
122
122
  copyable: {
123
123
  name: "copyable",
@@ -183,7 +183,7 @@ export const componentMeta = {
183
183
  name: "spinner",
184
184
  ancestry: "shadcn",
185
185
  whenToUse:
186
- "Indique uma espera sem progresso determinado, como uma ação ou consulta em andamento. Para reservar a forma do conteúdo, use Skeleton.",
186
+ "Indique uma espera sem progresso determinado, como uma ação ou consulta em andamento. O ícone é decorativo: o contêiner (`role=\"status\"`) ou o texto ao lado nomeia a espera. Para reservar a forma do conteúdo, use Skeleton.",
187
187
  },
188
188
  table: {
189
189
  name: "table",
@@ -327,7 +327,7 @@ export const componentMeta = {
327
327
  name: "sidebar",
328
328
  ancestry: "opus",
329
329
  whenToUse:
330
- "Organize navegação global ou contextual em uma coluna lateral. Split e Pane definem posição e largura; Sidebar fornece a superfície e o colapso, enquanto PaneHeader, PaneBody e PaneFooter estruturam as regiões fixa e rolável. Use SidebarNav para grupos planos, componha árvores com SidebarItem e SidebarTreeGroup e use ShellNav quando a navegação precisar de cabeçalho e rodapé próprios.",
330
+ "Organize navegação global ou contextual em uma coluna lateral. Split e Pane definem posição e largura; Sidebar fornece a superfície e o colapso, enquanto PaneHeader, PaneBody e PaneFooter estruturam as regiões fixa e rolável. Use SidebarNav para grupos planos e componha árvores com SidebarItem e SidebarTreeGroup; cabeçalho e rodapé próprios ficam em PaneHeader e PaneFooter.",
331
331
  },
332
332
  "scroll-area": {
333
333
  name: "scroll-area",
@@ -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",
@@ -412,7 +412,7 @@ export const componentMeta = {
412
412
  name: "data-state",
413
413
  ancestry: "opus",
414
414
  whenToUse:
415
- "Coordene carregamento, erro, vazio e conteúdo de uma consulta assíncrona. Use DataState dentro da estrutura que receberá os dados; para uma região disponível à criação, use Empty. Ações em andamento pertencem ao estado `busy` do controle que as iniciou.",
415
+ "Coordene carregamento, erro, vazio e conteúdo de uma consulta assíncrona com as mesmas superfícies de PageState: `emptyMessage` compõe Empty com moldura sólida, o erro é um Alert com `onRetry` e `retryLabel`. Use DataState dentro da estrutura que receberá os dados; para uma região disponível à criação, use Empty. Ações em andamento pertencem ao estado `busy` do controle que as iniciou.",
416
416
  },
417
417
  "action-list-dialog": {
418
418
  name: "action-list-dialog",
package/src/ui/react.tsx CHANGED
@@ -18,7 +18,12 @@ export type {
18
18
  ButtonVariant,
19
19
  ButtonSize,
20
20
  } from "./components/primitives/button.tsx";
21
- export type { ControlShape } from "./components/primitives/control.ts";
21
+ export type {
22
+ ControlShape,
23
+ ControlSize,
24
+ ControlIconSize,
25
+ ControlScale,
26
+ } from "./components/primitives/control.ts";
22
27
 
23
28
  export { Input } from "./components/primitives/input.tsx";
24
29
 
@@ -358,6 +363,7 @@ export { useOverflowing } from "./lib/overflow.ts";
358
363
  export {
359
364
  ActionForm,
360
365
  ActionFormField,
366
+ LabelHelp,
361
367
  useActionFormContext,
362
368
  } from "./components/patterns/form.tsx";
363
369
  export type {
@@ -395,12 +401,7 @@ export { ActionView } from "./components/patterns/view.tsx";
395
401
  export type { ActionViewProps } from "./components/patterns/view.tsx";
396
402
 
397
403
  export { ActionTrigger } from "./components/patterns/trigger.tsx";
398
- export {
399
- dialog,
400
- DialogHost,
401
- confirm,
402
- ConfirmHost,
403
- } from "./components/patterns/confirm.tsx";
404
+ export { dialog, DialogHost } from "./components/patterns/confirm.tsx";
404
405
  export type {
405
406
  AlertOptions,
406
407
  ChooseOptions,
@@ -426,6 +427,7 @@ export {
426
427
  PageDescription,
427
428
  PageMeta,
428
429
  PageActions,
430
+ PageActionsTarget,
429
431
  PageBody,
430
432
  } from "./components/patterns/page.tsx";
431
433
  export type { PageProps } from "./components/patterns/page.tsx";
@@ -475,6 +477,7 @@ export type {
475
477
  DockProps,
476
478
  DockActionProps,
477
479
  SurfaceStatusProps,
480
+ SurfaceStatusContext,
478
481
  } from "./components/patterns/dock.tsx";
479
482
 
480
483
  // Barra lateral composicional: header/conteúdo/footer e navegação, sem possuir o layout.
@@ -497,15 +500,6 @@ export type {
497
500
  SidebarNavItem,
498
501
  } from "./components/patterns/sidebar.tsx";
499
502
 
500
- // O menu como primitivo pivotável: recolhe quando composto dentro de Sidebar.
501
- export { ShellNav, ShellNavHeading } from "./components/patterns/shell-nav.tsx";
502
- export type {
503
- ShellNavProps,
504
- ShellNavGroup,
505
- ShellNavItem,
506
- ShellNavHeadingProps,
507
- } from "./components/patterns/shell-nav.tsx";
508
-
509
503
  // Roteamento history-based (o pathname É o estado). Escopo pequeno de propósito: leitura
510
504
  // reativa da URL + navigate — sem tabela de rotas. Ver src/ui/router.ts.
511
505
  export {
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
@@ -21,7 +21,7 @@
21
21
  * // vite --mode design
22
22
  */
23
23
 
24
- import { readdirSync, readFileSync } from 'node:fs'
24
+ import { readdirSync } from 'node:fs'
25
25
  import type { IncomingMessage, ServerResponse } from 'node:http'
26
26
  import path from 'node:path'
27
27
  import type { Plugin, ViteDevServer } from 'vite'
@@ -40,6 +40,7 @@ import {
40
40
  type RuntimeSetup,
41
41
  type User,
42
42
  } from '../core/index.ts'
43
+ import { readPackageVersion } from '../core/package-version.ts'
43
44
  import { mittEvents } from '../events/drivers/mitt.ts'
44
45
  import { nodeServer, type NodeServerHandle } from '../server/drivers/node.ts'
45
46
  import { serializeResult } from '../server/index.ts'
@@ -148,7 +149,7 @@ export function opusDesign(options: OpusDesignOptions = {}): Plugin {
148
149
  const stubEntries = Object.entries(options.stubs ?? {}).sort(
149
150
  (a, b) => b[0].length - a[0].length,
150
151
  )
151
- const opusVersion = readOpusVersion()
152
+ const opusVersion = readPackageVersion()
152
153
 
153
154
  let exProxyPrefixes: string[] = []
154
155
  let current: Promise<DesignState> | undefined
@@ -373,7 +374,7 @@ export function opusDesign(options: OpusDesignOptions = {}): Plugin {
373
374
  sendEnvelope(res, 503, {
374
375
  code: 'design.boot_failed',
375
376
  category: 'dependency',
376
- message: `Design runtime failed to boot: ${String(err)}`,
377
+ message: `O runtime de design não conseguiu iniciar: ${String(err)}`,
377
378
  })
378
379
  return
379
380
  }
@@ -421,7 +422,7 @@ export function opusDesign(options: OpusDesignOptions = {}): Plugin {
421
422
  sendEnvelope(res, 503, {
422
423
  code: 'design.no_backend',
423
424
  category: 'dependency',
424
- message: `No backend for "${orphan}" in design mode add a stub or serve it from the opus runtime`,
425
+ message: `Sem backend para "${orphan}" em modo design — adicione um stub ou sirva a action pelo runtime do Opus`,
425
426
  })
426
427
  return
427
428
  }
@@ -464,7 +465,7 @@ async function loadEntry(server: ViteDevServer, entry: string): Promise<unknown[
464
465
  throw error({
465
466
  code: 'design.invalid_entry',
466
467
  category: 'internal',
467
- message: `Entry "${entry}" must default-export { domains: [...] } or an array of registrables`,
468
+ message: `O entry "${entry}" precisa exportar por default { domains: [...] } ou uma lista de registráveis`,
468
469
  })
469
470
  }
470
471
 
@@ -637,16 +638,3 @@ function sendEnvelope(res: ServerResponse, status: number, input: ErrorInput): v
637
638
  )
638
639
  }
639
640
 
640
- function readOpusVersion(): string {
641
- // `import.meta.url` sobrevive ao bundling do vite.config (o vite injeta a
642
- // URL original do arquivo), então o package.json resolve do source real.
643
- try {
644
- const pkg = JSON.parse(
645
- readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),
646
- ) as { version?: string }
647
- return pkg.version ?? '0.0.0'
648
- /* v8 ignore next 3 — defensivo; o package.json viaja junto do source */
649
- } catch {
650
- return '0.0.0'
651
- }
652
- }