@softize/opus 12.11.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.
- package/CHANGELOG.md +76 -0
- package/PROMOTED.md +46 -0
- package/README.md +28 -19
- package/bin/cli.mjs +87 -216
- package/bin/lib/check.mjs +2 -7
- package/bin/lib/cli-shared.mjs +131 -0
- package/bin/lib/copy.mjs +1 -5
- package/bin/lib/db.mjs +16 -74
- package/bin/lib/gen-openapi.mjs +3 -3
- package/bin/lib/gen-runner.mjs +1 -1
- package/bin/lib/gen.mjs +14 -69
- package/bin/lib/mcp.mjs +3 -1
- package/bin/lib/seed.mjs +5 -62
- package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +93 -10
- package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
- package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
- package/docs/ownership-vs-shadcn-lock.md +2 -3
- package/docs/protocol.md +7 -7
- package/docs/radius-scale.md +1 -1
- package/docs/releasing.md +8 -2
- package/package.json +7 -3
- package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
- package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
- package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
- package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
- package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
- package/registry/templates/app/package.json +1 -1
- package/registry/templates/app/src/main.tsx +4 -4
- package/src/audit/drivers/console.ts +1 -0
- package/src/auth/drivers/better-auth.ts +1 -0
- package/src/auth/drivers/jwt.ts +1 -0
- package/src/cache/drivers/memory.ts +1 -0
- package/src/client/drivers/fetch.ts +2 -1
- package/src/core/actions.ts +6 -1
- package/src/core/audit.ts +9 -3
- package/src/core/contracts.ts +7 -0
- package/src/core/domain.ts +1 -1
- package/src/core/errors.ts +18 -15
- package/src/core/index.ts +4 -2
- package/src/core/package-version.ts +26 -0
- package/src/core/reactions.ts +1 -1
- package/src/core/runtime.ts +33 -23
- package/src/core/schedules.ts +1 -1
- package/src/core/types.ts +2 -2
- package/src/dsl/eval.ts +2 -2
- package/src/dsl/kysely.ts +2 -2
- package/src/dsl/loads.ts +1 -1
- package/src/dsl/parser.ts +5 -5
- package/src/events/drivers/mitt.ts +1 -0
- package/src/mcp/index.ts +2 -1
- package/src/observability/drivers/opentelemetry.ts +1 -0
- package/src/queue/drivers/bullmq.ts +3 -3
- package/src/scheduler/drivers/node-cron.ts +3 -2
- package/src/scheduler/every.ts +7 -7
- package/src/schema/openapi.ts +3 -3
- package/src/seed/index.ts +29 -0
- package/src/server/drivers/fastify.ts +5 -2
- package/src/server/drivers/node.ts +9 -6
- package/src/server/index.ts +3 -1
- package/src/storage/drivers/fs.ts +1 -0
- package/src/testing/index.ts +3 -3
- package/src/ui/components/patterns/confirm.tsx +142 -42
- package/src/ui/components/patterns/content-header.tsx +7 -1
- package/src/ui/components/patterns/data-state.tsx +1 -1
- package/src/ui/components/patterns/dock.tsx +20 -3
- package/src/ui/components/patterns/form.tsx +12 -8
- package/src/ui/components/patterns/list.tsx +36 -41
- package/src/ui/components/patterns/page-state.tsx +2 -2
- package/src/ui/components/patterns/page.tsx +19 -1
- package/src/ui/components/patterns/shell-nav.tsx +10 -3
- package/src/ui/components/patterns/sidebar.tsx +43 -32
- package/src/ui/components/patterns/trigger.tsx +39 -38
- package/src/ui/components/patterns/view.tsx +26 -17
- package/src/ui/components/primitives/alert.tsx +14 -8
- package/src/ui/components/primitives/ask.tsx +3 -3
- package/src/ui/components/primitives/badge.tsx +11 -6
- package/src/ui/components/primitives/breadcrumb.tsx +2 -2
- package/src/ui/components/primitives/button.tsx +16 -3
- package/src/ui/components/primitives/calendar.tsx +28 -2
- package/src/ui/components/primitives/carousel.tsx +3 -3
- package/src/ui/components/primitives/chat.tsx +1 -1
- package/src/ui/components/primitives/checkbox.tsx +1 -1
- package/src/ui/components/primitives/command.tsx +2 -2
- package/src/ui/components/primitives/control.ts +12 -0
- package/src/ui/components/primitives/copyable.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +202 -40
- package/src/ui/components/primitives/dot.tsx +5 -0
- package/src/ui/components/primitives/drawer.tsx +18 -8
- package/src/ui/components/primitives/empty.tsx +3 -3
- package/src/ui/components/primitives/field.tsx +3 -3
- package/src/ui/components/primitives/icon-picker.tsx +3 -1
- package/src/ui/components/primitives/input-group.tsx +1 -1
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/input.tsx +2 -2
- package/src/ui/components/primitives/item.tsx +3 -3
- package/src/ui/components/primitives/progress.tsx +32 -3
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/resizable.tsx +3 -1
- package/src/ui/components/primitives/select.tsx +5 -5
- package/src/ui/components/primitives/slider.tsx +5 -1
- package/src/ui/components/primitives/sonner.tsx +190 -8
- package/src/ui/components/primitives/switch.tsx +1 -0
- package/src/ui/components/primitives/tabs.tsx +1 -0
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/components/primitives/tooltip.tsx +1 -0
- package/src/ui/docs/DocBrowser.tsx +102 -23
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/accordion.md +22 -16
- package/src/ui/docs/content/action-form-card.md +8 -8
- package/src/ui/docs/content/action-form-dialog.md +9 -9
- package/src/ui/docs/content/action-form.md +37 -36
- package/src/ui/docs/content/action-list-dialog.md +11 -6
- package/src/ui/docs/content/action-list.md +73 -39
- package/src/ui/docs/content/action-trigger.md +29 -15
- package/src/ui/docs/content/action-view.md +17 -10
- package/src/ui/docs/content/actions.md +9 -9
- package/src/ui/docs/content/ai.md +3 -3
- package/src/ui/docs/content/alert.md +14 -12
- package/src/ui/docs/content/ask.md +11 -0
- package/src/ui/docs/content/aspect-ratio.md +4 -4
- package/src/ui/docs/content/audit.md +2 -2
- package/src/ui/docs/content/auth.md +3 -3
- package/src/ui/docs/content/avatar.md +34 -14
- package/src/ui/docs/content/badge.md +3 -3
- package/src/ui/docs/content/breadcrumb.md +13 -8
- package/src/ui/docs/content/button.md +81 -6
- package/src/ui/docs/content/calendar.md +18 -5
- package/src/ui/docs/content/card.md +27 -1
- package/src/ui/docs/content/carousel.md +16 -11
- package/src/ui/docs/content/chat.md +23 -3
- package/src/ui/docs/content/checkbox.md +7 -7
- package/src/ui/docs/content/cli.md +74 -22
- package/src/ui/docs/content/collapsible.md +8 -8
- package/src/ui/docs/content/command.md +16 -8
- package/src/ui/docs/content/composer.md +17 -2
- package/src/ui/docs/content/content.md +17 -2
- package/src/ui/docs/content/copyable.md +12 -3
- package/src/ui/docs/content/customization.md +5 -5
- package/src/ui/docs/content/cycle.md +3 -3
- package/src/ui/docs/content/data-state.md +11 -12
- package/src/ui/docs/content/data.md +26 -33
- package/src/ui/docs/content/detail.md +22 -4
- package/src/ui/docs/content/dialog.md +339 -31
- package/src/ui/docs/content/dictionary-value.md +17 -10
- package/src/ui/docs/content/dock.md +11 -3
- package/src/ui/docs/content/dot.md +8 -0
- package/src/ui/docs/content/drawer.md +27 -14
- package/src/ui/docs/content/empty-value.md +2 -2
- package/src/ui/docs/content/empty.md +19 -12
- package/src/ui/docs/content/events.md +4 -4
- package/src/ui/docs/content/field.md +34 -12
- package/src/ui/docs/content/getting-started.md +3 -3
- package/src/ui/docs/content/icon-picker.md +19 -4
- package/src/ui/docs/content/input-otp.md +20 -12
- package/src/ui/docs/content/input.md +121 -9
- package/src/ui/docs/content/item.md +27 -13
- package/src/ui/docs/content/kbd.md +19 -11
- package/src/ui/docs/content/label.md +12 -3
- package/src/ui/docs/content/log.md +4 -4
- package/src/ui/docs/content/markdown.md +7 -6
- package/src/ui/docs/content/mcp.md +13 -15
- package/src/ui/docs/content/menu.md +40 -16
- package/src/ui/docs/content/metric-card.md +13 -0
- package/src/ui/docs/content/observability.md +2 -2
- package/src/ui/docs/content/page.md +59 -6
- package/src/ui/docs/content/pagination.md +22 -17
- package/src/ui/docs/content/popover.md +22 -8
- package/src/ui/docs/content/progress.md +15 -16
- package/src/ui/docs/content/queue.md +5 -5
- package/src/ui/docs/content/radio-group.md +20 -12
- package/src/ui/docs/content/router.md +11 -6
- package/src/ui/docs/content/scheduler.md +4 -5
- package/src/ui/docs/content/scroll-area.md +12 -7
- package/src/ui/docs/content/select.md +47 -34
- package/src/ui/docs/content/semantic-context.md +2 -2
- package/src/ui/docs/content/separator.md +5 -5
- package/src/ui/docs/content/sidebar.md +329 -54
- package/src/ui/docs/content/skeleton.md +9 -2
- package/src/ui/docs/content/slider.md +8 -7
- package/src/ui/docs/content/spinner.md +8 -8
- package/src/ui/docs/content/split.md +29 -5
- package/src/ui/docs/content/storage.md +6 -8
- package/src/ui/docs/content/switch.md +8 -7
- package/src/ui/docs/content/table.md +13 -3
- package/src/ui/docs/content/tabs.md +28 -14
- package/src/ui/docs/content/testing.md +9 -11
- package/src/ui/docs/content/textarea.md +12 -4
- package/src/ui/docs/content/toast.md +47 -13
- package/src/ui/docs/content/toggle.md +75 -7
- package/src/ui/docs/content/tokens.md +7 -7
- package/src/ui/docs/content/tooltip.md +19 -11
- package/src/ui/docs/content/truncate.md +15 -8
- package/src/ui/docs/content/ui.md +24 -9
- package/src/ui/docs/content/upgrading.md +7 -8
- package/src/ui/docs/doc-client.tsx +5 -5
- package/src/ui/docs/doc.tsx +26 -14
- package/src/ui/docs/registry.tsx +25 -42
- package/src/ui/docs/standalone.tsx +2 -2
- package/src/ui/drivers/react.tsx +17 -12
- package/src/ui/lib/action-errors.ts +45 -0
- package/src/ui/lib/zod-pt-br.ts +31 -4
- package/src/ui/meta.ts +65 -95
- package/src/ui/react.tsx +17 -16
- package/src/ui/theme.css +60 -8
- package/src/vite/design.ts +6 -18
- package/src/ui/components/primitives/alert-dialog.tsx +0 -192
- package/src/ui/docs/content/alert-dialog.md +0 -73
- package/src/ui/docs/content/button-group.md +0 -71
- package/src/ui/docs/content/confirm.md +0 -120
- package/src/ui/docs/content/input-group.md +0 -79
- package/src/ui/docs/content/page-state.md +0 -45
- package/src/ui/docs/content/toggle-group.md +0 -81
|
@@ -4,11 +4,10 @@ title: Atualizar o Opus
|
|
|
4
4
|
|
|
5
5
|
# Atualizar o Opus
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
cada etapa está documentada ou é verificada por um gate que informa o que precisa ser corrigido.
|
|
7
|
+
Use este fluxo para atualizar o Opus por uma ou várias versões. O changelog explica as mudanças e os
|
|
8
|
+
gates apontam as adaptações necessárias no projeto.
|
|
10
9
|
|
|
11
|
-
## 1.
|
|
10
|
+
## 1. Identificar a versão disponível
|
|
12
11
|
|
|
13
12
|
O **Maestro** compara o pin (`opus.json`) com a versão instalada e com a fonte a cada
|
|
14
13
|
sessão — o alerta no rodapé diz a distância (`v2.9.0 → v2.15.2`) e o **Atualizar** faz
|
|
@@ -16,7 +15,7 @@ a parte mecânica: bump da dependência, re-materialização da camada de IA (sk
|
|
|
16
15
|
hooks, agentes) e re-sync do bloco gerenciado do CLAUDE.md. Fora do Maestro:
|
|
17
16
|
`pnpm outdated @softize/opus`.
|
|
18
17
|
|
|
19
|
-
## 2.
|
|
18
|
+
## 2. Ler as mudanças acumuladas
|
|
20
19
|
|
|
21
20
|
O changelog **viaja no pacote** — depois do bump, está em
|
|
22
21
|
`node_modules/@softize/opus/CHANGELOG.md`. Uma entrada por versão; quando quebra,
|
|
@@ -26,7 +25,7 @@ A esteira de release **recusa** publicar versão sem entrada — o arquivo não
|
|
|
26
25
|
- **minor** (2.9 → 2.10): compatível; leia por curiosidade.
|
|
27
26
|
- **major** (2.x → 3.0): pare nas seções Breaking de cada versão pulada, na ordem.
|
|
28
27
|
|
|
29
|
-
## 3.
|
|
28
|
+
## 3. Executar as verificações
|
|
30
29
|
|
|
31
30
|
O Opus ship source: o que quebrou aparece com arquivo e linha. Rode, na ordem:
|
|
32
31
|
|
|
@@ -38,8 +37,8 @@ pnpm manifest:check # a spec projetada ficou fresca?
|
|
|
38
37
|
opus db migrate # schema em dia (drift-check embutido)
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
Com todas as verificações verdes, revise a atualização como qualquer outra mudança de código antes
|
|
41
|
+
de entregá-la.
|
|
43
42
|
|
|
44
43
|
## Pulou muitas versões?
|
|
45
44
|
|
|
@@ -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 {
|
|
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: '
|
|
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',
|
|
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: '
|
|
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
|
-
<
|
|
228
|
+
<OpusProvider client={docClient}>{children}</OpusProvider>
|
|
229
229
|
</QueryClientProvider>
|
|
230
230
|
)
|
|
231
231
|
}
|
package/src/ui/docs/doc.tsx
CHANGED
|
@@ -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-
|
|
76
|
-
<span className="font-medium">
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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%]">
|
|
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>
|
package/src/ui/docs/registry.tsx
CHANGED
|
@@ -27,6 +27,7 @@ import authMd from "./content/auth.md?raw";
|
|
|
27
27
|
import auditMd from "./content/audit.md?raw";
|
|
28
28
|
import eventsMd from "./content/events.md?raw";
|
|
29
29
|
import logMd from "./content/log.md?raw";
|
|
30
|
+
import observabilityMd from "./content/observability.md?raw";
|
|
30
31
|
import queueMd from "./content/queue.md?raw";
|
|
31
32
|
import schedulerMd from "./content/scheduler.md?raw";
|
|
32
33
|
import actionsMd from "./content/actions.md?raw";
|
|
@@ -46,8 +47,6 @@ import customizationMd from "./content/customization.md?raw";
|
|
|
46
47
|
// — Componentes —
|
|
47
48
|
import accordionMd from "./content/accordion.md?raw";
|
|
48
49
|
import alertMd from "./content/alert.md?raw";
|
|
49
|
-
import alertDialogMd from "./content/alert-dialog.md?raw";
|
|
50
|
-
import confirmMd from "./content/confirm.md?raw";
|
|
51
50
|
import aspectRatioMd from "./content/aspect-ratio.md?raw";
|
|
52
51
|
import avatarMd from "./content/avatar.md?raw";
|
|
53
52
|
import badgeMd from "./content/badge.md?raw";
|
|
@@ -55,7 +54,6 @@ import dotMd from "./content/dot.md?raw";
|
|
|
55
54
|
import dockMd from "./content/dock.md?raw";
|
|
56
55
|
import breadcrumbMd from "./content/breadcrumb.md?raw";
|
|
57
56
|
import buttonMd from "./content/button.md?raw";
|
|
58
|
-
import buttonGroupMd from "./content/button-group.md?raw";
|
|
59
57
|
import calendarMd from "./content/calendar.md?raw";
|
|
60
58
|
import cardMd from "./content/card.md?raw";
|
|
61
59
|
import metricCardMd from "./content/metric-card.md?raw";
|
|
@@ -77,7 +75,6 @@ import menuMd from "./content/menu.md?raw";
|
|
|
77
75
|
import emptyMd from "./content/empty.md?raw";
|
|
78
76
|
import fieldMd from "./content/field.md?raw";
|
|
79
77
|
import inputMd from "./content/input.md?raw";
|
|
80
|
-
import inputGroupMd from "./content/input-group.md?raw";
|
|
81
78
|
import inputOtpMd from "./content/input-otp.md?raw";
|
|
82
79
|
import itemMd from "./content/item.md?raw";
|
|
83
80
|
import kbdMd from "./content/kbd.md?raw";
|
|
@@ -99,7 +96,6 @@ import tabsMd from "./content/tabs.md?raw";
|
|
|
99
96
|
import textareaMd from "./content/textarea.md?raw";
|
|
100
97
|
import toastMd from "./content/toast.md?raw";
|
|
101
98
|
import toggleMd from "./content/toggle.md?raw";
|
|
102
|
-
import toggleGroupMd from "./content/toggle-group.md?raw";
|
|
103
99
|
import tooltipMd from "./content/tooltip.md?raw";
|
|
104
100
|
import truncateMd from "./content/truncate.md?raw";
|
|
105
101
|
|
|
@@ -122,7 +118,6 @@ import actionTriggerMd from "./content/action-trigger.md?raw";
|
|
|
122
118
|
import actionSearchDialogMd from "./content/action-list-dialog.md?raw";
|
|
123
119
|
import pageMd from "./content/page.md?raw";
|
|
124
120
|
import contentMd from "./content/content.md?raw";
|
|
125
|
-
import pageStateMd from "./content/page-state.md?raw";
|
|
126
121
|
import sidebarMd from "./content/sidebar.md?raw";
|
|
127
122
|
import splitMd from "./content/split.md?raw";
|
|
128
123
|
import routerMd from "./content/router.md?raw";
|
|
@@ -139,6 +134,8 @@ export interface DocEntry {
|
|
|
139
134
|
export interface DocGroup {
|
|
140
135
|
/** Sub-rótulo opcional dentro da seção (ex.: Primitivos). Ausente = páginas direto na seção. */
|
|
141
136
|
label?: string;
|
|
137
|
+
/** Ícone opcional apresentado no nó expansível do grupo. */
|
|
138
|
+
icon?: React.ReactNode;
|
|
142
139
|
pages: DocEntry[];
|
|
143
140
|
}
|
|
144
141
|
|
|
@@ -178,7 +175,7 @@ const comp =
|
|
|
178
175
|
</DocPage>
|
|
179
176
|
);
|
|
180
177
|
|
|
181
|
-
/** O palco dos patterns no scope dos previews: ícones
|
|
178
|
+
/** O palco dos patterns no scope dos previews: ícones, cliente simulado e contratos de exemplo. */
|
|
182
179
|
const patternScope = {
|
|
183
180
|
...iconScope,
|
|
184
181
|
DocBrowserActionProvider,
|
|
@@ -224,13 +221,13 @@ export const PROTOCOL_SECTIONS: DocSection[] = [
|
|
|
224
221
|
pages: [
|
|
225
222
|
{
|
|
226
223
|
slug: "getting-started",
|
|
227
|
-
title: "
|
|
224
|
+
title: "Primeiros passos",
|
|
228
225
|
render: doc(gettingStartedMd),
|
|
229
226
|
},
|
|
230
227
|
{ slug: "cycle", title: "Evolução contínua", render: doc(cycleMd) },
|
|
231
228
|
{
|
|
232
229
|
slug: "upgrading",
|
|
233
|
-
title: "Atualizar
|
|
230
|
+
title: "Atualizar o Opus",
|
|
234
231
|
render: doc(upgradingMd),
|
|
235
232
|
},
|
|
236
233
|
],
|
|
@@ -291,6 +288,12 @@ export const PROTOCOL_SECTIONS: DocSection[] = [
|
|
|
291
288
|
render: doc(eventsMd),
|
|
292
289
|
},
|
|
293
290
|
{ slug: "log", title: "Log", badge: "log", render: doc(logMd) },
|
|
291
|
+
{
|
|
292
|
+
slug: "observability",
|
|
293
|
+
title: "Observabilidade",
|
|
294
|
+
badge: "observability",
|
|
295
|
+
render: doc(observabilityMd),
|
|
296
|
+
},
|
|
294
297
|
{
|
|
295
298
|
slug: "queue",
|
|
296
299
|
title: "Filas",
|
|
@@ -334,7 +337,7 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
334
337
|
pages: [
|
|
335
338
|
{
|
|
336
339
|
slug: "como-consumir",
|
|
337
|
-
title: "
|
|
340
|
+
title: "Como consumir",
|
|
338
341
|
render: doc(uiMd),
|
|
339
342
|
},
|
|
340
343
|
],
|
|
@@ -346,10 +349,10 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
346
349
|
groups: [
|
|
347
350
|
{
|
|
348
351
|
pages: [
|
|
349
|
-
{ slug: "tokens", title: "Tokens
|
|
352
|
+
{ slug: "tokens", title: "Tokens e tema", render: doc(tokensMd) },
|
|
350
353
|
{
|
|
351
354
|
slug: "semantic-context",
|
|
352
|
-
title: "Contexto
|
|
355
|
+
title: "Contexto e variante",
|
|
353
356
|
render: doc(semanticContextMd),
|
|
354
357
|
},
|
|
355
358
|
{
|
|
@@ -371,6 +374,7 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
371
374
|
groups: [
|
|
372
375
|
{
|
|
373
376
|
label: "Fundamentos",
|
|
377
|
+
icon: <lucideIcons.Blocks className="size-4" />,
|
|
374
378
|
pages: [
|
|
375
379
|
{
|
|
376
380
|
slug: "router",
|
|
@@ -381,6 +385,7 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
381
385
|
},
|
|
382
386
|
{
|
|
383
387
|
label: "Layout",
|
|
388
|
+
icon: <lucideIcons.PanelsTopLeft className="size-4" />,
|
|
384
389
|
pages: [
|
|
385
390
|
{
|
|
386
391
|
slug: "split",
|
|
@@ -402,11 +407,6 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
402
407
|
title: "Content",
|
|
403
408
|
render: pattern("Content", "content", contentMd),
|
|
404
409
|
},
|
|
405
|
-
{
|
|
406
|
-
slug: "page-state",
|
|
407
|
-
title: "Page State",
|
|
408
|
-
render: pattern("Page State", "page-state", pageStateMd),
|
|
409
|
-
},
|
|
410
410
|
{
|
|
411
411
|
slug: "accordion",
|
|
412
412
|
title: "Accordion",
|
|
@@ -442,6 +442,7 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
442
442
|
},
|
|
443
443
|
{
|
|
444
444
|
label: "Actions",
|
|
445
|
+
icon: <lucideIcons.Zap className="size-4" />,
|
|
445
446
|
pages: [
|
|
446
447
|
{
|
|
447
448
|
slug: "action-form",
|
|
@@ -498,6 +499,7 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
498
499
|
},
|
|
499
500
|
{
|
|
500
501
|
label: "IA",
|
|
502
|
+
icon: <lucideIcons.Sparkles className="size-4" />,
|
|
501
503
|
pages: [
|
|
502
504
|
{ slug: "ask", title: "Ask", render: comp("Ask", "ask", askMd) },
|
|
503
505
|
{ slug: "chat", title: "Chat", render: comp("Chat", "chat", chatMd) },
|
|
@@ -510,6 +512,7 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
510
512
|
},
|
|
511
513
|
{
|
|
512
514
|
label: "Formulário",
|
|
515
|
+
icon: <lucideIcons.TextCursorInput className="size-4" />,
|
|
513
516
|
pages: [
|
|
514
517
|
{
|
|
515
518
|
slug: "calendar",
|
|
@@ -536,11 +539,6 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
536
539
|
title: "Input",
|
|
537
540
|
render: comp("Input", "input", inputMd),
|
|
538
541
|
},
|
|
539
|
-
{
|
|
540
|
-
slug: "input-group",
|
|
541
|
-
title: "Input Group",
|
|
542
|
-
render: comp("Input Group", "input-group", inputGroupMd),
|
|
543
|
-
},
|
|
544
542
|
{
|
|
545
543
|
slug: "input-otp",
|
|
546
544
|
title: "Input OTP",
|
|
@@ -580,31 +578,23 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
580
578
|
},
|
|
581
579
|
{
|
|
582
580
|
label: "Botões",
|
|
581
|
+
icon: <lucideIcons.MousePointerClick className="size-4" />,
|
|
583
582
|
pages: [
|
|
584
583
|
{
|
|
585
584
|
slug: "button",
|
|
586
585
|
title: "Button",
|
|
587
586
|
render: comp("Button", "button", buttonMd),
|
|
588
587
|
},
|
|
589
|
-
{
|
|
590
|
-
slug: "button-group",
|
|
591
|
-
title: "Button Group",
|
|
592
|
-
render: comp("Button Group", "button-group", buttonGroupMd),
|
|
593
|
-
},
|
|
594
588
|
{
|
|
595
589
|
slug: "toggle",
|
|
596
590
|
title: "Toggle",
|
|
597
591
|
render: comp("Toggle", "toggle", toggleMd),
|
|
598
592
|
},
|
|
599
|
-
{
|
|
600
|
-
slug: "toggle-group",
|
|
601
|
-
title: "Toggle Group",
|
|
602
|
-
render: comp("Toggle Group", "toggle-group", toggleGroupMd),
|
|
603
|
-
},
|
|
604
593
|
],
|
|
605
594
|
},
|
|
606
595
|
{
|
|
607
596
|
label: "Navegação",
|
|
597
|
+
icon: <lucideIcons.Compass className="size-4" />,
|
|
608
598
|
pages: [
|
|
609
599
|
{
|
|
610
600
|
slug: "breadcrumb",
|
|
@@ -626,6 +616,7 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
626
616
|
},
|
|
627
617
|
{
|
|
628
618
|
label: "Exibição",
|
|
619
|
+
icon: <lucideIcons.Eye className="size-4" />,
|
|
629
620
|
pages: [
|
|
630
621
|
{
|
|
631
622
|
slug: "avatar",
|
|
@@ -689,17 +680,13 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
689
680
|
},
|
|
690
681
|
{
|
|
691
682
|
label: "Feedback",
|
|
683
|
+
icon: <lucideIcons.MessageCircleWarning className="size-4" />,
|
|
692
684
|
pages: [
|
|
693
685
|
{
|
|
694
686
|
slug: "alert",
|
|
695
687
|
title: "Alert",
|
|
696
688
|
render: comp("Alert", "alert", alertMd),
|
|
697
689
|
},
|
|
698
|
-
{
|
|
699
|
-
slug: "confirm",
|
|
700
|
-
title: "Dialog (imperativo)",
|
|
701
|
-
render: comp("Dialog (imperativo)", "confirm", confirmMd),
|
|
702
|
-
},
|
|
703
690
|
{
|
|
704
691
|
slug: "data-state",
|
|
705
692
|
title: "Data State",
|
|
@@ -734,12 +721,8 @@ export const UI_SECTIONS: DocSection[] = [
|
|
|
734
721
|
},
|
|
735
722
|
{
|
|
736
723
|
label: "Sobreposição",
|
|
724
|
+
icon: <lucideIcons.Layers className="size-4" />,
|
|
737
725
|
pages: [
|
|
738
|
-
{
|
|
739
|
-
slug: "alert-dialog",
|
|
740
|
-
title: "Alert Dialog",
|
|
741
|
-
render: comp("Alert Dialog", "alert-dialog", alertDialogMd),
|
|
742
|
-
},
|
|
743
726
|
{
|
|
744
727
|
slug: "dialog",
|
|
745
728
|
title: "Dialog",
|
|
@@ -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 `
|
|
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 ?? '
|
|
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
|
package/src/ui/drivers/react.tsx
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* @softize/opus/ui/react — React driver
|
|
3
3
|
*
|
|
4
4
|
* Hooks + Provider pra invocar actions client-side.
|
|
5
|
-
* - `
|
|
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
|
-
*
|
|
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
|
|
61
|
+
interface OpusContextValue {
|
|
62
62
|
client: ClientAdapter
|
|
63
63
|
dicts: Record<string, DictLike>
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const
|
|
66
|
+
const OpusContext = createContext<OpusContextValue | null>(null)
|
|
67
67
|
|
|
68
|
-
export interface
|
|
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
|
|
77
|
+
export function OpusProvider({
|
|
78
78
|
client,
|
|
79
79
|
dicts,
|
|
80
80
|
children,
|
|
81
|
-
}:
|
|
82
|
-
const value = useMemo<
|
|
83
|
-
return <
|
|
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(
|
|
92
|
+
const ctx = useContext(OpusContext)
|
|
88
93
|
if (ctx === null) {
|
|
89
94
|
throw new Error(
|
|
90
|
-
'useAction/useLookupAction
|
|
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(
|
|
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
|
+
}
|
package/src/ui/lib/zod-pt-br.ts
CHANGED
|
@@ -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:
|
|
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: '
|
|
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
|
}
|